Hooks write smoke artifacts to /tmp/lunacycle-smoke, which the hook jail turns into a lying diagnostic — move them to state/ #64
Labels
No labels
area/auth
area/backend
area/ci
area/deploy
area/e2e
area/frontend
area/shared
blocked
good-first-issue
needs-info
priority/high
priority/low
priority/medium
type/bug
type/chore
type/docs
type/feature
type/tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
celilo/lunacycle#64
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
celilo/scripts/health-check.ts:57andcelilo/scripts/smoke-handler.ts:49both define:and pass it as
artifactDir(health-check.ts:420,smoke-handler.ts:103). When the smoke check fails,health-check.ts:433tells the operator where to look:Why this breaks, and why it breaks silently
celilo is putting module hooks inside a jail (celilo/celilo#1001,
openspec/changes/hook-process-boundary). Stage 2 derives each hook's mount set from its hook context, and/tmpbecomes a fresh tmpfs per run.Nothing errors. The
mkdirsucceeds, the screenshot writes, the hook reports success, and the sentence athealth-check.ts:433still prints. The artifacts are gone the moment the hook process exits, so an operator follows a message naming a directory that no longer exists and finds nothing.That is worse than a failure. A diagnostic path that lies is consulted exactly when something is already wrong, and it costs the reader their first hour.
The sanctioned destination now exists
Until today a hook had nowhere to write. Anything it left beside itself failed
module auditasextra, and the only reasonscreenshots/andcookies.jsondid not was a hardcoded allow-list of paths someone had already been bitten by.celilo/celilo#1000 closed that. A module now has a
state/directory in its own root, classifiedderived, which means writable, survivesmodule update, not audited, not pruned. It landed onmaininapps/celilo/src/module/packaging/package-rules.ts:105.This module is the first real consumer of it.
Scope
celilo/scripts/health-check.ts:57—SPA_ARTIFACT_DIR.celilo/scripts/health-check.ts:420— where it is passed asartifactDir.celilo/scripts/health-check.ts:433— the operator-facing sentence, which has to name the new location.celilo/scripts/smoke-handler.ts:49,103— the same constant and the same use. Two files define it independently, which is its own small defect: fix that by defining it once.Acceptance
state/rather than a hardcoded/tmppath, and the constant is defined once rather than twice.health-check.ts:433's message names the real location, so it stays true under the jail.module auditis clean. Both halves matter: the point ofstate/is that it is writable and not reported asextra.state/survivesmodule updateby construction, which is right for a cursor and arguably wrong for an unbounded pile of screenshots. If they need bounding, bound them here rather than discovering it at restore, since celilo's backup picksstate/up through its existing per-module walk.Related
state/decision and the one-line change that shipped it./tmpa fresh tmpfs.Found while answering task 1.5 of
hook-process-boundary, which asks whether any lunacycle hook reads outside its module tree. Two other findings from that sweep, recorded here so they are not re-derived:Environment variables: none. Zero
process.envand zeroBun.envacross every non-test hook script. The jail's environment allow-list costs this module nothing.build_artifacts_diris a separate problem and is celilo's to solve, not lunacycle's.setup-web.ts:125reads it from module config and then reads and uploads from under it. It is an arbitrary operator-supplied absolute path on the management host, and the jail's mount set is derived from the hook context, so no rule can predict it. celilo is adding a path type to variable declarations so a declared path config value becomes a mount input. Nothing to change here until that lands.Already fixed on
main, four days before this issue was filed. No code change needed.bc8ea9b("feat(smoke): launch celilo's provisioned browser, and keep the artifacts", 2026-08-18) deleted both copies ofSPA_ARTIFACT_DIRand moved both call sites onto the framework. This issue was written against the line numbers from before that commit, which is whyhealth-check.ts:57andsmoke-handler.ts:49no longer say what it quotes.Where the artifacts go now
The hook takes the directory from its context.
health-check.ts:602destructuresscreenshotDiroffHookContextand passes it in asartifactDir:The bus subscriber genuinely has no
HookContext, so it calls celilo's own definition of that layout directly (smoke-handler.ts:122):moduleArtifactDiris a public export of@celilo/capabilities(packages/capabilities/src/index.ts:85), and it is the same function celilo's own hook executor calls to buildscreenshotDir(apps/celilo/src/hooks/executor.ts:630). Both sites therefore land in one place:<modulePath>/screenshots/<runKey>. Nothing is hand-rolled and nothing is hardcoded.Acceptance, item by item
Resolved from the hook context, constant defined once. Met, and more strongly than asked. The constant is not defined once, it is defined zero times. Both sites derive the path from the framework, so there is no literal left to drift.
The operator-facing sentence names the real location. Met. Both messages interpolate the resolved value rather than a literal, so the sentence is true by construction:
Artifacts survive the process exiting, and
module auditstays clean. Met on both halves, but viascreenshots/rather thanstate/.<modulePath>/screenshots/is in the module store on disk, not/tmp.hook-process-boundarytask 4.2 already namesthis run's screenshots/<run>in the read-write mount set alongsidestate/andgenerated/, so the jail carries it.package-rules.ts:105classifiesscreenshotsasderived, in the same condition asstate.Deliberate decision on retention. Bounded, and celilo does the bounding — this module needs no policy of its own.
apps/celilo/src/hooks/artifact-retention.tsprunes by mtime:ARTIFACT_RETENTION_MSis 24 hours, with a 64 MB per-module size ceiling as a backstop. The executor callspruneModuleArtifacts()on every hook run and discards the run directory entirely when the hook wrote nothing. The subscriber's directories share the same artifact root, so a health-check run prunes them too.This is the part where
screenshots/is the better destination andstate/would have been the wrong one. The issue flags the risk itself:state/is "not pruned", which is right for a cursor and wrong for an unbounded pile of screenshots, and celilo's backup walksstate/per module. Putting failure screenshots there would have put an unbounded pile into every backup.screenshots/is the directory that already has the age-based retention this data wants.Gates
From a clean worktree at
644bf61:bun testincelilo/scripts— 69 pass, 0 failtsc -p celilo/scripts/tsconfig.json --noEmit— exit 0The only surviving mention of the old path is a doc comment at
health-check.ts:95, which describes it accurately in the past tense as the thing that was replaced.One note for celilo, not actionable here
There is still no
stateDironHookContext.celilo#1000shipped thestate/classification without a surface, somodule audittolerates the directory but nothing tells a hook its path. That did not block this issue, because the data in question is artifacts andscreenshotDiralready covers artifacts. It would block the first hook that wants durable cursor-like state. Raised at celilo/celilo#1000, not lunacycle's to fix.Closing as already resolved.
Closing: already fixed before this was filed. PR #62, "feat(smoke): launch celilo's provisioned browser, and keep the artifacts", landed the change and I did not check
mainbefore writing the issue.On
origin/maintoday:The hook takes
screenshotDirfrom theHookContextand passes it through asartifactDir.SPA_ARTIFACT_DIRis gone. The only surviving mention of/tmp/lunacycle-smokeis a doc comment athealth-check.ts:95describing it in the past tense, which is accurate.And the destination it chose is better than the one this issue proposed. The issue said move to
state/.screenshots/is the right home, for reasons that are properties of the two directories rather than of which name is newer:screenshots/is classifiedderivedin the same condition asstate(package-rules.ts:105), somodule auditis equally clean either way.screenshots/<run>besidestate/andgenerated/.state/carries none of that. It is explicitly never pruned, and celilo's backup was believed to walk it per module. An unbounded pile of failure screenshots and DOM dumps is exactly the wrong thing to put somewhere with no ceiling.That last point got sharper after this issue was written.
state/turns out not to be backed up at all — see celilo/celilo#1102. A module backup's envelope ismanifest.jsonplusdata/, anddata/holds only what the module's ownon_backuphook wrote. Nothing walks the install tree. Sostate/currently loses its contents on restore, silently. Sending smoke artifacts there would have been wrong twice over.The two findings from the sweep that filed this are still worth keeping
They came from answering task 1.5 of celilo/celilo#1001, which asks whether any lunacycle hook reads outside its module tree or reads environment variables. Neither is affected by #62.
Environment variables: none. Zero
process.envand zeroBun.envacross every non-test script incelilo/scripts. The hook jail's environment allow-list costs this module nothing.build_artifacts_diris still an open problem, and it is celilo's, not lunacycle's.setup-web.ts:125reads it from module config, then readsjoin(buildArtifactsDir, 'dist', 'apps', 'lunacycle-web', 'config.js')at:140and uploads from:148. It is an arbitrary operator-supplied absolute path on the management host, and the jail derives its mount set from the hook context, so no rule can predict it. Under the jail as currently specified,on_installfails. celilo is fixing that by giving variable declarations a path type so a declared path config value becomes a mount input. Nothing to change here until that lands.The remote paths are fine and need no action:
/var/lib/lunacycle/db.sqlite(backup.ts:31,restore.ts:40) and/opt/lunacycle/{migrate.sh,migrations}(restore.ts:41-42) are all on the deployed system, reached over remote ops. The jail does not span the remote box.