Give module hooks a writable directory — today anything they write fails module audit as extra #1000
Open
opened 2026-08-20 18:29:25 +00:00 by forgejo-admin
·
5 comments
No Branch/Tag specified
main
changeset-release/main
fix/static-converge-skips-paused
jeremybanka/fj-shell-completions
fix/deploy-preserves-terraform-state
polecat/ce-f4pu
polecat/ce-6otx
polecat/ce-cfzx
polecat/ce-b9gk
polecat/ce-fzl8
polecat/ce-e41c
docs/host-suite-live-check
polecat/ce-d5q5
polecat/ce-sxb3
polecat/ce-wlrd
polecat/ce-60d6
polecat/ce-ce95
polecat/ce-bmbd
polecat/ce-bfut
design/main-runs-what-the-mac-cannot
polecat/ce-5aph
polecat/ce-6ide
polecat/ce-p55w
polecat/ce-qyo1
polecat/ce-8ece
polecat/ce-tgz7
polecat/ce-yzpr
fix/publish-skip-module
polecat/ce-94oi
polecat/ce-ix76
fix/empty-package-name-changeset
polecat/ce-78xh
polecat/ce-ojdn
polecat/ce-w0vp
polecat/ce-pjkv
polecat/ce-75xu
polecat/ce-3eud
polecat/ce-ywxr
polecat/ce-7m72
polecat/ce-3964
polecat/ce-9653
polecat/ce-q10u
polecat/ce-esbz
fleet-readiness-sweep
polecat/ce-o6gq
polecat/ce-dtah
polecat/ce-muia
polecat/ce-rzf8
polecat/ce-zdsc
polecat/ce-8mxz
census/ce-1m8r
polecat/ce-mlva
wip/crew-cli-bun142-20260909
jeremybanka/scoped-transit-service-access
chore/agent-agnostic-config
crew-cli/bun-1.4.2
polecat/ce-5l3r
per-module-jail-policy
fix/bake-consumer-mode-skips-restage
fix/register-module-deploy-keep-flag
fix/pack-staging-keeps-hook-runtime
polecat/ce-trpw
design/generated-is-ephemeral
polecat/ce-gkn8
design/control-plane-stops-building-modules
polecat/ce-a2id
polecat/ce-dmo7
polecat/ce-8832
polecat/ce-i0k-rebased
polecat/ce-r1rx
polecat/ce-5kg5
polecat/ce-2k87
polecat/ce-qzh7
polecat/ce-5bkj
plan/gascity-dev-module
polecat/ce-5b1v
mayor/mise-github-token-401
mayor/release-pins-bun-via-mise
polecat/ce-5bum
polecat/ce-h04y
polecat/ce-pvii
polecat/ce-vmq1
polecat/ce-2p9d
polecat/ce-l0e5
polecat/ce-2r5y
polecat/ce-23cv
polecat/ce-35kg
polecat/ce-ig0z
polecat/ce-yuzi
polecat/ce-ypsl
polecat/ce-lbnu
mayor/e2e-suite-recovery-status-2026-09-07
polecat/ce-qzxm
polecat/ce-rvps
polecat/ce-23gb
polecat/ce-aqza
polecat/ce-21hp
polecat/ce-7jbk
polecat/ce-g6o6
polecat/ce-3ei3
polecat/ce-bltm
polecat/ce-gxxz
polecat/ce-06ar
polecat/ce-ywix
wip/ce-r1rx-slit-preserved
polecat/ce-uols
polecat/ce-d0pv
e2e-suite-recovery
crew-e2e-census
e2e-rig-perf
oscar/block-timing-drop-marker
jeremybanka/burner
jeremybanka/firewall-managed-address-placement
jeremybanka/module-update-package-retention
jeremybanka/scoped-isp-transit-access
polecat/ce-sdit
ce-i0k
ce-eltf
polecat/ce-2ni
polecat/ce-xe4
chore/openspec-slice-driven
polecat/ce-ujm
local-merge-gate-typescript
polecat/ce-7o0
polecat/ce-91c
polecat/ce-3eb
polecat/ce-1ed
polecat/ce-116
refinery-full-gates
docs/reconcile-openspec-status
hook-mgmt-backup-framework
ce-9mb-firewall-chain
hook-boundary-group-e
docs/consolidate-agent-memories
ci-gate-defects
hook-jail-wire-caller
multi-instance-impl
bwrap-deb-depends
apparmor-ci-gates
design-persist-managed-ingress
reconcile-hpb-tasks
archive-completed-changes
fix-version-pr-circular-dep
web-ui-console-impl
control-plane-api-name
claude-md-blind-checks
fix-publish-side-bump
publish-sweep-revisions
alert-board-icon-seam
hook-name-sweep-revisions
fix-release-ordering
hook-mount-set
e2e-bubblewrap
hook-jail-profile
builder-bubblewrap
record-premise-pattern
submodules-quarantine-dns
hook-boundary-stage2
stage3-declaration-driven-injection
jb-managed-domains-closeout
module-icons
submodules-trigger
jb-golden-dnsmasq
submodules-design-correction
owned-system-module-ids
stage3-retire-injection
jb-group8-dhcp-e2e
jb-dns-notify
fix-e2e-build-context
hook-boundary-stage1
stage4-static-content-converge
jb-group7-replication-e2e
jb-group7-register-transfer-peer
stage3-blocker-note
jb-group5-isp-transit
stage2-capability-declares-tables
jb-publish-transfer-peer-contract
jb-group6-transit-e2e
jb-group7-transfer-peer
jb-group7-dns-secondary
claude-md-never-say-lan
jb-ticks-and-secondary-inventory
fix-forgejo-concurrency
module-upstream-upgrades
sync-web-ui-specs-main
web-ui-explore
claude-md-trust-direction
sync-web-ui-specs
stage1-provider-arrival-backfill
jb-group1-capabilities-api
jb-group3-reference-driven-secrets
jb-group2-capability-selection
scope-capability-secret-gate
stage0-finish-migration-delivery
integrate-jeremybanka-fork-proposal
openspec-capability-owned-tables
fix-stale-lockfile
celilo-triage-skill
docs-ci-status-state-trap
close-module-integrity-rigor
caddy-acme-waitfor
probe-http-impl
fix-mcp-server-bus-flake
fix-mcp-server-test-flake
multi-instance-modules
opsx-archive-complete
probe-http-capabilities
firewall-drift-normalisation
wire-check-schema-ci
module-integrity-apply
iptables-live-rule-drift
wg-reconcile-peers-onto-main
wg-list-peers-hook
wg-reconcile-peers
fix-generated-files-overwrite
module-integrity-rigor
fix-spec-pointers
sync-spec-module-pause
sync-spec-wireguard-manager
sync-spec-external-web
sync-spec-network-declaration
sync-spec-proxmox-simulator
merging-rule-reconcile
airportd-network-thrash
propose-nightly-issue-triage
wg-peer-hooks-pending
publish-time-script-scan
openspec-validate-gate
baseline-issue-refs
dedup-hook-context
no-ssh-remaining-tasks
hook-name-list-peers
wg-apply-peers-in-place
probe-primitive-redesign
wg-manager-logout
fix-mcp-ssh-leak
secrets-are-generated
remove-902-workaround
ipam-reservation-edit
aspect-fanout-upstream-dns
aspect-fanout-impl
fix-cics-upstream-dns
archive-wgm-app-oidc
wg-manager-oidc
aspect-fanout-new-systems
spec-artifacts-reach-details
browser-artifacts
design-corrections-browser-runtime
managed-browser-runtime
module-barrier-gate
celilo-module-barrier-audit
playwright-platform-design
consumer-removal-modules
consumer-removal-cleanup
tasks-truth
wireguard-manager-e2e
mcp-dev-server-scaffold-changeset
fix/responder-pkill
design-ratify-implementation
caddy-internal
hook-names-one-list
wireguard-manager-module
consumer-cleanup-hook
fix/ci-timeout-misdiagnosis
manager-app-client
docs/land-the-proposal-rule
provider-knows-its-consumer
fix-sweep-skip-attribution
fix-apt-publish-prefix
fix-core-interview-gate
private-web-framework-impl
fix-integrity-blocks-import
reconcile-clients-hook-impl
fix-collection-integrity
fix-machine-occupancy
fix-doctor-host-liveness
fix-machine-alert-key
wireguard-manager-app
release-manual-dispatch
fix-inbound-ack-grammar
fix-operation-error-masking
capability-inventory-docs
reconcile-clients-hook
private-web-capability
wireguard-provides-control-plane-vpn
fix-is-in-subnet
control-plane-vpn-capability
test/proxmox-client-over-msw
wireguard-peer-authority
fix/e2e-show-config-fail-fast
fix/db-busy-timeout
wireguard-manager-explore
networks-declared
docs/decision-brief-2-2
networks-supersede-disclosure
wireguard-internal-endpoint
derived-config-cli-surface
derived-recompute
declared-networks-proposals
ci-release-on-push
docs-designs-live-in-openspec
fw-migration-doc
fw-wireguard-selfsufficient
fw-e2e-classification
fw-interface-list
fw-baseline-enforcement
cadence-overrides-health
cadence-overrides-retention
cadence-overrides
axon-dhcp-pool
fw-converge-findings
fw-iptables-selectors
backup-frequency
axon-custom-dns
dhcp-dns-reassert
fw-wireguard-declare-order
backup-envelope-oom
forgejo-bundle-binary
forgejo-checksum-conditional
fix-d1-justification
forgejo-backup-manual
fw-audit-interface-classification
fw-retire-classifiers
remove-clack
fw-shared-classifier
ci-drop-duplicate-gate-step
remove-errors-not-prompts
ci-run-integration-tests
slim-ci-test-logs
fw-two-hop-wan-owner
stop-infra-provisioned-only
fw-e2e-declared-legs
release-deliver-version
cele2e-preflight
module-pause
fw-wireguard-wan-grep
fix-test-migrations-folder
fw-e2e-topology-coverage
fw-e2e-public-edge-renumber
openspec-module-pause-lifecycle
fix-backup-hook-budgets
name-the-iss-numbers
name-the-iss-numbers-code
firewall-interface-classification
fix-signal-backup-dir
axon-router-module
slim-claude-md-and-ignore-beads
converge-prev-baseline
archive-public-dns-reachability
public-dns-reachability-modules
inspect-failed-deliveries
fix-module-ip-fallout
dns-registrar-contract
public-dns-reachability
fix-module-publish-gate
pin-core-subscriber-naming
www-public-reachability
forgejo-runner-org-scope
doctor-core-subscribers
honor-subscriber-timeout
release-unblock
fix-phantom-backup-records
implement-park-unanswerable-interviews
source-forge-runner-scopes
claudemd-seed-legacy-state
npm-cache-node-unpin-ip
wireguard-rollout-refresh
wellspring-fleet-alerts
park-unanswerable-interviews
fix-dispatcher-unit-scope
claudemd-semver-is-mine
upgrade-restarts-dispatcher
headless-breaking-update-consent
smoke-always-reports
ci-single-validate-run
biome-excludes-generated-files
e2e-module-import-sigint
disk-space-monitor
backup-staging-leak-rate-correction
fix-astro-imports
firewall-converge-safety
visualizer-mask-placement
mgr-disk-space
lint-typecheck-every-package
sweep-abandoned-operations
fix-dup-dispatchers
wellspring-menubar-archive
wellspring-observation
wellspring-menubar
wellspring-phase1
visualizer-pulse-glow-perf
wellspring-network-doctor
storage-set-path
fix-icon-board-flake
e2e-sim-address-space
seal-sim-apt
document-poll-cd
transport-read-staleness
release-pin-delivery-version
persist-read-outcome
signal-health-contract
deb-decoupled-from-npm
fix-bus-concurrent-open
event-bus-busy-timeout
lockfile-version-sync
e2e-teardown-volumes
signal-release-touch
fix-module-dirty-scope
ci-watching-guidance
release-publish-decoupled
fix-interview-answer-grammar
commit-bun-lock
e2e-ack-suite
pack-test-ci-diag
mcp-docs-shipping
e2e-ack-coverage
fix-ack-counter
mcp-devserver-ci-gap
audit-checks-that-cannot-fail
framework-config-keys
forgiving-ack-parser
module-log-tail
fix-e2e-cleanup-without-compose
signal-note-to-self-ack
unignore-e2e-changesets
fix-e2e-network-leak
agent-coordination-guardrails
celilo-release-skill-ci
scheduled-backup-runner
fix-default-backup-storage
fix-poll-cd-not-running
pin-node-toolchain
signal-ack-roundtrip
escalation-policy-not-applied
sticky-container-storage
fix-stuck-operation-lock
e2e-audit
close-wordpress-static-publish
ci-comment-placement
typechecking-modules
fix-public-dns-source-ip
cpanel-provider-release-touch
signal-healthcheck-test-types
isitup-no-signal
fix-machines-reachable-local
signal-notification-import
alerting-failure-reasons
alerting-notify-visibility
fix-release-idempotent
signal-tpl-comment
signal-types-drift
signal-terraform
audit-fleet-rollout
docs/rollout-release-path
alerting
wordpress-impl
chore/remove-dead-derived-persistence
docs/hook-owned-state-pr
docs/hook-owned-state
wordpress
wireguard
propose/wireguard-module
fix/dns-aspect-secure-mgmt
fix/secure-mgmt-config-keys
fix/pins-in-version-phase
fix/consumer-pins-0.7.1
fix/module-versions-and-inventory
fix/aspect-local-connection
feat/recognize-management-network
docs/changeset-protocol
fix/registry-hostname-guard
fix/technitium-dhcp-hang
propose/firewall-converge-safety
fix/release-single-publisher
fix/skills-mcp-transport
fix/release-jq
fix/apt-publish-step
fix/deb-nfpm
chore/bump-capability-pins
fix/detect-no-git
fix/release-pat
fix/npm-auth-npmrc
fix/changeset-e2e-ignored
e2e-web
openspec
polecat/nux/ce-qdv@mrsg3ack
polecat/slit/ce-af5@mrsg3ndq
polecat/furiosa/ce-1ch@mrsg2vji
polecat/rictus/ce-u2c@mrsgkt02
polecat/toast/ce-di8@mrsgk7lt
polecat/rictus/ce-qrn@mrsfnkur
feat/visualizer-flow-lee-seeds
module-versioning-guidance
polecat/cheedo/ce-2vu@mrsdn18c
polecat/dag/ce-jje@mrsdit5n
polecat/furiosa/ce-2vu@mrsdf07i
polecat/keeper/ce-8g3@mrsdkj67
polecat/slit/ce-7aa@mrsdh4ke
ce-s23-design
mcp-dev-server-scaffold-tests
polecat/furiosa/ce-foa@mrqn59em
polecat/furiosa/ce-foa@mrqngd71
polecat/nux/ce-lp0@mrqna61g
unified-mgmt
mcp-dev-server
fix/workspace-package-graph
ce-1bb-celilo-core-in-pack-lists
land-stranded-e2e-design-docs
ce-dae-single-source-repo-root
ce-dbc-findmonorepoRoot-probe
polecat/rictus/ce-77i.5@mri9qtq4
polecat/dementus/ce-77i.6@mri9r8uk
polecat/rictus/ce-77i.2@mri7f1dk
e2e-0.9.1-release
polecat/rictus/ce-77i.1@mri541xq
polecat/furiosa/ce-l3j@mri4zszv
celilo-mcp-service
fix-zsh-completion-apostrophe
release-cli-0.11.0
api-conversion
polecat/nux/ce-nwn
release/npm-version-bumps
polecat/furiosa/ce-um6@mrfhmnk3
polecat/nux/ce-z5i
polecat/nux/ce-3rs@mrfilctn
polecat/cheedo/ce-z1b.5@mr5cx775
polecat/ace/ce-22d@mr5ctvea
npm-fleet-registry
polecat/valkyrie/ce-22d@mr58074a
npm-consumer-path
polecat/cheedo/ce-22d@mr586n8c
v2-report-out
polecat-idle
polecat/furiosa/hq-s1l@mr2eirhf
polecat/furiosa/ce-c56-cardgrid
chore/ponytail-dedup
iss-fullstack-acme-race
ship-cli-0.9.1
iss-258-shuffle-no-ambient-red
iss-255-self-diagnosing-waits
iss-254-pollution-boundary
fix-aspect-responder-hang
iss-257-celilo-computer-collision
fix-260-technitium-net0-zone
chore/ponytail-dead-code-cleanup
iss-cele2e-flake-instrumentation
release-iss-0156-publish
iss-install-sh-waitfor-diag
release-iss-0156-versions
iss-firewall-ssh-timeout-robustness
ISS-0156
e2e-contention
iss-241-222-cele2e-fixes
iss-cele2e-diagnostics
migrate-issues-log
iss-0150-read-model
subsystem-inventory
iss-0168-backup-gate-stale-manifest
upgrade-foregjo
hotfix-celilo-0.8.1
release-celilo-0.8.0
iss-0157-tmpfs-fix
cele2e-isolated-suite
modules-capabilities-0.5.0
reconcile-celilo-version
iss-0151-module-version-engine
iss-0150-resize
standards-instance-sizing
docs-lunacycle-deployed
iss-app-cd-stale-manifest
docs-build-bus-status
docs-no-allow-stale
celilo-website-source-dir-build
build-bus-rollout
e2e-output-fixes
fix-vision-typo
celilo-visualizer
ci-pattern-git-native-mise
fix-runner-node
build-bus-e2e
build-bus
iss-celilo-install-mode
iss-0138-module-upgrade
build-bus-phase-issues
docs-build-bus-rework
fix-mise-double-encoding
step4-workflows-and-labels
release-cli-alpha12
fix-vm-state-parsing
release-cli-alpha11
fix-vm-template-qcow2-extension
fix-vm-template-import-content
iss-0133-worktree-deps-misleading-failure
release-cli-alpha9
iss-0069-infra-config-contract
iss-0127-headless-interview
docs-ci-local-gates-procedure
iss-0090-deploy-follows-reality
iss-0060-pt2-reconcile-display
iss-0060-proxmox-introspect-reconcile
iss-0103-e2e-pack-explicit-rewrite
iss-0132-node-capacity-check
release-cli-alpha8
docs-claude-md-vitest-fix
iss-0131-no-mgmt-server-build
iss-0130-silent-recreation
iss-phase-d-builder-vm
release-cli-alpha7
proxmox-vm-template-build
forgejo-builder-runtime
fix-duplicate-iss-0123
phase2-design
vantage-fleet-pr
phase1-ci-validation
v4.1.0
v4.0.0
v3.1.0
v3.0.0
v2.3.0
v2.2.1
v2.2.0
v2.1.0
v1.14.0
v1.13.0
v1.12.0
v1.10.0
v1.9.0
v1.8.0
v1.7.0
v1.6.0
v1.5.0
v1.4.0
v1.3.0
v1.2.0
v1.1.0
v1.0.0
v0.27.0
v0.26.1
v0.26.0
v0.25.1
v0.25.0
v0.24.1
v0.24.0
v0.23.0
v0.21.0
v0.20.0
v0.19.0
v0.18.0
v0.17.0
v0.16.2
v0.15.0
v0.14.4
v0.14.2
v0.14.1
v0.14.0
v0.13.3
v0.13.2
v0.13.1
v0.12.1
@celilo/cli@0.8.0
Labels
Clear labels
autofix-approved
Operator has approved unattended work on this issue. ONLY a human applies this label; /celilo-triage is forbidden from applying it.
autofix-candidate
Nominated by /celilo-triage as small, well-specified and mechanically checkable. Applied by the agent; awaiting operator review.
category:bug
category:codegen
category:design
category:docs
category:enhancement
category:feature
category:policy
category:task
category:tech-debt
category:tooling
e2e-confidence
Initiative: make cele2e trustworthy — isolation, self-diagnosis, no silent no-ops
in-progress
severity:high
severity:low
severity:medium
No labels
autofix-approved
autofix-candidate
category:bug
category:codegen
category:design
category:docs
category:enhancement
category:feature
category:policy
category:task
category:tech-debt
category:tooling
e2e-confidence
in-progress
severity:high
severity:low
severity:medium
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set.
Reference
celilo/celilo#1000
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?
A module's install root is checksummed, and the integrity audit reports every file it did not expect:
successisviolations.length === 0, so one such file fails the audit.But hooks do write into their own root, and so does the framework on their behalf.
packages/capabilities/src/browser.ts:237is celilo's own code doing it:The only reason that does not fail today is that
classifyModulePathcarries a hardcoded allow-list of the specific paths someone has already been bitten by:screenshots/andcookies.jsonare two patches for one capability. The default for anything else ispackage, so the next hook that writes a cache file, a cursor, a lock, or a scratch artifact next to itself getsUnexpected file:and a red audit — with no hint that writing there was the mistake, because nothing says where it should have written instead.That is whack-a-mole with a hardcoded list, and the list lives in celilo core while the writers live in modules and capabilities. It also makes the audit's own contract unclear:
extrais supposed to mean "something is in this install that we did not put there", which is a real security property, and it currently also means "a hook did its job".celilo already has the pattern for the fix.
BROWSER_ROOT = '/var/lib/celilo/browsers'(packages/capabilities/src/browser.ts:28) is a celilo-owned tree outside any module directory. A per-module equivalent — a writable directory the framework creates, injects intoHookContext, and classifiesderivedwholesale — would give hooks somewhere to write that is safe by construction rather than by allow-list entry.Open questions for whoever picks this up, rather than a decided design:
<module>/var/, classified derived likegenerated/) or a celilo-owned tree outside the module (/var/lib/celilo/module-state/<id>/)? The first keeps everything about a module in one place and travels with backup's existing per-module walk. The second keeps the install root genuinely immutable, which is a stronger integrity claim.screenshots/andcookies.jsonmigrate into it and the allow-list entries be deleted, or stay as compatibility?module update?generated/does; a scratch cache might not want to.Scope
apps/celilo/src/module/packaging/audit.ts—classifyModulePath(the allow-list) and theextracheck at:198.packages/capabilities/src/browser.ts:237— the framework write intomodulePath.packages/capabilities/src/types.ts:83,define-hook.ts:84— the screenshot-directory surface handed to hooks.join(modulePathandjoin(sourcePathacrosspackages/capabilitiesandmodules/*/scriptsfor other writers.packages/capabilities/src/browser.ts:28(BROWSER_ROOT) as the existing precedent for a celilo-owned writable tree.Acceptance
extraviolation, and does so by rule rather than by an entry in a hardcoded list.extrarecovers its real meaning: something is present that celilo did not put there.screenshots/andcookies.jsoneither migrate or are documented as deliberate exceptions with a reason.Related
openspec/changes/multi-instance-modules(renaming tosubmodules) — D4 gives each instance a private root reached through a symlink farm, precisely so N instances' hook writes do not land on top of each other. That change needs this question answered to know where those writes should go, but does not block on it.Closing the open question in the body: put it inside the module root as a named
state/directory classifiedderived. The machinery already exists and only the name is missing.I was leaning toward a celilo-owned tree outside the module (the
BROWSER_ROOTshape), on the argument that it keeps the install root immutable andextraat full strength.module-update.tssays otherwise:and at
:338-345:So
derivedalready means precisely "writable, survivesmodule update, not audited, not pruned". That is the entire contract a sanctioned scratch location needs, and it is already implemented and already load-bearing. What is missing is not machinery. It is a named directory carrying that classification, instead of two ad-hoc entries (screenshots/,cookies.json) that someone patched in after being bitten.That collapses the fix to roughly one line in
classifyModulePath:and deleting the two ad-hoc entries once their writers move.
The integrity objection I was going to raise answers itself. Carving a hole in
extrais not a new compromise — the hole exists, it is calledderived, andgenerated/is a far larger one. A single named directory is that same compromise applied consistently, and it is a net reduction: one rule replacing two unrelated special cases.Everything the outside-the-module option would have needed — a path helper,
HookContextinjection, backup integration, an update-lifecycle decision, cleanup on module remove — is work to re-derive semanticsderivedalready provides.Name:
state/, notvar/.varcollides with celilo's ownvariables.*manifest vocabulary and would read as "the module's variables" to anyone skimming an install tree.Two consequences worth carrying into the work:
state/survivesmodule updateby construction, which is what a cursor wants. If some future scratch wants discarding on update, that is a second directory with a different rule, not a reason to reject this one.The decision above is not on any branch, and three separate changes are now waiting on it. Recording that here so it is visible from the issue rather than only from the agents blocked by it.
The comment above closes the design question and collapses the fix to roughly one line in
classifyModulePathplus deleting two ad-hoc entries once their writers move. Checked today against bothorigin/mainandorigin/multi-instance-impl:returns nothing on either.
classifyModulePath(package-rules.ts:98-103) still returnsderivedforgenerated/,screenshots/,checksums.json,signature.sig,celilo/types.d.tsandcookies.json, and nothing else. The line proposed above is written nowhere. This issue is still open and still unassigned.Who is waiting.
openspec/changes/hook-process-boundarytask 4.2 derives the jail's mount set and needsstate/as its sanctioned writable row. Its task 7.2 says "confirm celilo#1000'sstate/has shipped before task 4.2 depends on it. Do not re-decide it and do not touchclassifyModulePath" — an instruction that cannot be satisfied by waiting, because nothing is in flight to wait for. Its design D11 states the decision above as settled input.openspec/changes/capability-owned-tablestask 4.7 defers celilo#1018'sclientConfigwrite into the module tree to this decision, on the stated grounds that "a fourth answer to where does a deploy-time artifact live is worse than a late one" (design.md:258). That agent confirms it has never touchedclassifyModulePathand does not own this.openspec/changes/multi-instance-modules(submodules) group 8 carries celilo#1000 as a task. Its agent has not started it.Each of the three has independently declined to own it, and each declined for a defensible reason: re-deciding a decided question, or scattering a fourth answer. So the gap is not neglect, it is three correct local decisions summing to nobody writing the line.
Two things need an operator, and they are different questions.
forgejo-adminbecause that is the API token identity every agent writes under, so authorship is not visible from the issue itself. The argument is substantial and reasoned —derivedalready means "writable, survivesmodule update, not audited, not pruned", so the contract exists and only a name was missing — and nobody who has read it wants to reopen it. It has simply never been ratified. That is cheap to answer and worth answering explicitly, because task 7.2 tells a future implementer not to re-decide it, which is only safe advice if it was decided by someone entitled to.One dissent worth recording, from the
hook-process-boundaryagent, offered as a mild preference and explicitly not as grounds to reopen. Inside the module root means binding<store>/<id>read-only and then binding<store>/<id>/stateread-write on top of it, a nested read-write exception carved into a read-only tree. Bubblewrap handles it, since a later--bindwins over an earlier--ro-bind, so it works. But D9's clean claim is "the module's own tree is bound read-only", and a nested hole makes that sentence slightly less true. Outside the root there is no exception at all. It confirms this does not change the mount set either way — one writable row, a different string in it — and that stage 2 can build the mount set now and fill the row in when the shape is fixed.Landed on
mainin #1091 (package-rules.ts:105), with the recurrence gate atapps/celilo/src/module/packaging/module-state-directory.test.ts.The location decision in the comment above was ratified by peba before implementation, so it is an operator's call rather than a peer's.
The gate uses generated filenames, not literals, which is the part worth keeping. The failure this issue describes is not "we forgot to allow
state/cursor.json" — it is that the allow-list was a list of literals patched in one at a time after each one bit someone, so it could only ever cover names somebody had already been surprised by. A test asserting a literal name would reproduce exactly that weakness.Watched failing first (Rule 7.6), with the gate written and the line not yet added:
That
"type": "extra"is this issue's defect, reproduced end to end throughauditModuleagainst a real tree and a real database rather than synthesised on both sides.The one test that passed before and after is the contrast, and it is load-bearing: the same generated names outside
state/still classifypackage, so they are still scanned and still reported. If that ever goes green alongside the others, the fix widened rather than named.Left open deliberately
screenshots/andcookies.jsonare untouched. This issue leaves their migration to when their writers move, and folding it in turns a one-line change into a capability refactor.Two consequences for other work
The jail gets one nested exception.
state/sits inside the module root, sohook-process-boundary's D9 mount set binds<store>/<id>read-only and then binds<store>/<id>/stateread-write on top of it. Bubblewrap resolves that correctly (a later--bindwins over an earlier--ro-bind), but D9's sentence "the module's own tree is bound read-only" now has exactly one carved exception. Recorded in that change's task 4.2 so whoever writes the mount set meets it there.lunacycle now has a destination it did not have. Its
health-check.ts:57andsmoke-handler.ts:49write post-mortem artifacts to a hardcoded/tmp/lunacycle-smokeon the management host, andhealth-check.ts:433prints an operator-facing message naming that directory. Under the jail,/tmpis a fresh tmpfs per run, so the writes would succeed, the hook would report success, and the artifacts would be gone before an operator followed the sentence telling them where to look — with no error anywhere.state/is the answer to that, and it is a one-line destination change now rather than an open question. Out-of-repo, so it is lunacycle's to make./tmp/lunacycle-smoke, which the hook jail turns into a lying diagnostic — move them tostate/#64/tmp/lunacycle-smoke, which the hook jail turns into a lying diagnostic — move them tostate/#64Reopening. I closed this early and the first acceptance criterion is not met.
#1091 shipped the classification and not the surface.
classifyModulePathtoleratesstate/, and the gate proves an unanticipated filename there leavesmodule auditclean. ButHookContexthas nostateDir. Greppingorigin/mainacrossapps/celilo/srcandpackages/capabilities/srcfinds nothing computing<moduleRoot>/statefor a hook to receive.packages/capabilities/src/types.ts:84carriesscreenshotDirand nothing else of that shape.So a module author has to construct the path themselves, which is exactly what "framework-provided" was written to prevent.
The tell was in my own test and I did not read it.
module-state-directory.test.tsbuilds the path by hand:If the gate has to hand-roll the path, so does every module. I verified the classification landed and treated that as the issue being done.
Found by the first real consumer, not by review.
celilo/lunacycle#64is moving smoke artifacts out of/tmp/lunacycle-smokeintostate/and is blocked on having nothing to ask. That is the right way for this to surface and the wrong way for it to have been necessary.What is still owed
stateDironHookContext, populated the wayscreenshotDiralready is (executor.ts:630computes it,:641injects it,:675creates it withmkdirSync(recursive)).state/yet, and a hook should not have tomkdir -pits own sanctioned location.reference/MODULE_DEVELOPMENT_GUIDE.mdthat makes "documented" true.Two decisions recorded here rather than left implicit
A plain path, not an accessor.
openspec/changes/hook-owned-stateis turningcontext.secretsandcontext.configintoget/set/deleteaccessors, which raises the fair question of whetherstate/should match. It should not, and the reason is that they are not the same kind of thing. Those accessors mediate values celilo persists in the database.state/is a directory a module writes files into — a SQLite file, a screenshot, a DOM dump. An accessor over a directory means celilo mediating file I/O, which is the "capability-only filesystem" optionhook-process-boundaryD8 rejected outright: 15 hook-reachable module files usenode:fs, and a path is what they can consume.screenshotDiris the correct sibling.Per module, long-lived. Not per run.
screenshotDiris per-run (moduleArtifactDir(modulePath,${hookName}-${startTime})) because artifacts are one invocation's diagnostic output and retention prunes them.state/is the opposite by construction: this issue's whole argument is thatderivedmeans "survivesmodule update", which is what a cursor wants. Per-run state is a contradiction.The consequence, stated so it is a decision and not a surprise: two concurrent hooks for the same module share one directory. That is possible today — a
health_checkcan run while a deploy is in flight, and the bus dispatcher spawns handlers concurrently. celilo will not isolate them, because isolating them defeats the purpose: a cursor written in one run must be readable in the next. A module needing atomicity inside its own storage does what any program does (atomic rename, distinct names, a lock file). This goes in the guide next to the path.I am taking this.
Surface landed in #1101. Verified on
mainby artifact rather than by the merge response:packages/capabilities/src/types.ts:97stateDir: stringonHookContextpackages/capabilities/src/browser.ts:257export function moduleStateDir(modulePath)apps/celilo/src/hooks/executor.ts:635, :647, :686reference/MODULE_DEVELOPMENT_GUIDE.mdapps/celilo/src/hooks/hook-state-dir.test.tsAcceptance
ctx.stateDir, created before the hook starts so it exists on a module's first ever run.extraviolation, by rule rather than by an allow-list entry.classifyModulePathclassifiesstate/**asderived, and the gate uses generated filenames precisely so it cannot be satisfied by a literal somebody remembered.extrarecovers its real meaning. The same generated names outsidestate/still classifypackage, so they are still scanned and still reported. That contrast test is the load-bearing half.screenshots/andcookies.jsoneither migrate or are documented as deliberate exceptions. Not done, deliberately. See below.The one criterion left, and why it is not being quietly dropped
screenshots/andcookies.jsonare still two ad-hoc entries besidestate/inclassifyModulePath. This issue's own text left their migration to "when their writers move", and folding it into the one-line change would have turned it into a capability refactor.They are not equivalent, and whoever picks this up should not treat them as one task.
screenshots/should not migrate. It is per-run, namespaced bymoduleArtifactDir(modulePath, runKey), and pruned by retention.state/is one directory per module that is never pruned. Merging them would either start deleting state or stop pruning artifacts. The right end state is two named directories with opposite lifetimes, which is what exists now — soscreenshots/is a deliberate exception and this comment is the documentation that criterion asks for.cookies.jsonis the real migration. It is a single hardcoded filename at the module root, which is exactly the shape this issue was filed about. It belongs instate/, and moving it deletes an allow-list entry rather than adding one.I am leaving the issue open on that last box rather than closing it a second time on a criterion I have not met.
Two notes for whoever reads this next
I closed this early once. The classification landed in #1091 and I treated that as done, with the tell sitting in my own test, which built the path by hand. It was caught by the first real consumer (
celilo/lunacycle#64) rather than by review. The gate now asserts the hook learns the path and never its spelling, because a test comparingctx.stateDirto a path it computed itself passes with no surface at all.Two concurrent hooks for the same module share this directory. Reachable today: a
health_checkcan run while a deploy is in flight, and the bus dispatches handlers concurrently. celilo does not isolate them, because isolating them defeats the point — a cursor written by one run must be readable by the next. Recorded inmoduleStateDir's docblock and in the guide, with the ordinary answer (atomic rename, distinct names, a lock file).