Plan deployment-managed cache permissions

This commit is contained in:
2026-09-11 14:24:51 +00:00
parent 77ae132e56
commit 7d9adc77ee
3 changed files with 593 additions and 0 deletions

View File

@@ -0,0 +1,347 @@
# Deployment-Managed Cache Permissions Implementation Plan
## Purpose
Implement the target contract in
[Deployment-Managed Cache Permissions](deployment-managed-cache-permissions.md):
Notarius must preserve safe, confined, atomic cache I/O while allowing normal
Unix creation semantics to determine who can access newly created chunk-plan
and checkpoint state.
This document is the execution sequence. The feature roadmap is authoritative
for user intent, policy, scope, and the target end state. Each numbered stage
below is sized for one `gpt-5.6-terra` implementation prompt and must be
completed in order.
## Cross-Stage Requirements
Every stage must preserve these decisions:
- Do not add a `cache.access` field, environment variable, CLI flag, or an
application-level private/group policy.
- New cache directories request mode `0777`; new cache files request mode
`0666`. The kernel applies umask and default ACL policy.
- Do not chmod, chown, chgrp, or recursively migrate existing cache entries to
enforce access policy.
- Keep chunk-plan and checkpoint confinement, symlink rejection, identity,
validation, atomic replacement, cleanup, and recovery behavior intact.
- Do not change output or debug permissions as an incidental consequence of a
shared-helper change.
- Do not add cross-process locking or promise transactional concurrent writes
to one checkpoint identity.
- Follow `docs/policy/testing.md`: protect the observable permission boundary
and meaningful filesystem risks without snapshotting helper structure or
duplicating all cases at every layer.
- Keep the repository buildable and the tests relevant to the completed stage
passing. Do not retire either roadmap document during implementation; they
remain review evidence until the completed feature is accepted.
## Stage 1: Record The Decision And Add An Umask-Respecting Atomic Writer
### Goal
Record the durable ownership boundary and add the narrow filesystem primitive
needed by checkpoint storage, without changing any production caller yet.
### Required Changes
1. Add `docs/adr/0017-defer-cache-access-policy-to-deployment.md` using the
repository's Nygard ADR format, with status `Accepted` and the implementation
date. The decision must:
- assign cache access policy to deployment provisioning, process umask,
setgid inheritance, and default ACLs;
- distinguish access policy from Notarius-owned confinement, validation,
atomicity, and recovery;
- reject both forced private modes and a Notarius-specific private/group
setting;
- state that source-derived cache contents may be sensitive and that the
operator owns access and retention;
- state that API credentials must remain outside cache payloads under the
existing secret boundary;
- explain that existing cache modes are not migrated automatically;
- reference ADR-0005, ADR-0006, and ADR-0007 without superseding them; and
- record application-enforced `0700`/`0600`, configurable permission modes,
and recursive permission convergence as rejected alternatives.
2. Extend `internal/core/fileio` with exactly two cache-oriented entry points:
- `WriteBytesWithProcessPermissions(root, name string, data []byte) error`;
- `WriteJSONWithProcessPermissions(root, name string, payload any) error`.
3. The new byte writer must reuse `SafePath` and symlink-component rejection,
call `os.MkdirAll` with `0777`, and atomically replace the target. The JSON
wrapper must retain the existing indented JSON plus trailing-newline format.
4. Implement the new atomic path independently from the existing exact-mode
`WriteBytes` path:
- generate a 16-byte cryptographically random suffix;
- attempt at most 32 unique sibling names of the form
`.<target-base>.tmp-<hex-suffix>`;
- open with `os.O_WRONLY|os.O_CREATE|os.O_EXCL` and mode `0666` so the kernel
applies umask and default ACLs at creation;
- retry only name collisions;
- write, close, rename, and remove an unpublished temporary file on every
failure path; and
- do not call `chmod` or inspect/change the process umask in production code.
5. Leave `WriteBytes`, `WriteJSON`, and their existing callers unchanged so
output and debug behavior cannot drift in this stage.
### Tests
- Add supported-Unix (`linux || darwin`) behavioral tests using
`syscall.Umask`. Each test must restore the prior umask with `defer` and must
not use `t.Parallel`.
- Prove a restrictive umask (`0077`) produces `0700` nested directories and a
`0600` final file.
- Prove a group-oriented umask (`0007`) produces `0770` nested directories and
a `0660` final file.
- Pre-create the selected root with a distinctive mode, write beneath it, and
prove the root mode is unchanged.
- Replace an existing file created under a different mode and prove the new
inode follows the current process policy and contains the complete new
content.
- Prove no temporary sibling remains after success. Retain all existing
confinement, symlink, and exact-mode writer tests.
### Validation
Run:
```sh
gofmt -w internal/core/fileio/*.go
go test ./internal/core/fileio
go test -race ./internal/core/fileio
git diff --check
```
### Stage Acceptance
The ADR records the agreed boundary; the new writer demonstrates both target
umasks without chmod; existing file-writing behavior has not changed; and the
stage is small enough for one implementation prompt.
## Stage 2: Convert Canonical Chunk-Plan Storage
### Goal
Make every chunk-plan creation and replacement path follow deployment-managed
permissions while preserving the rooted filesystem and cache-recovery
protocol.
### Required Changes
1. In `internal/framework/chunkplan`:
- request `0777` when creating the configured root;
- remove the chmod of an opened existing root;
- request `0777` when creating a source-digest directory;
- remove the chmod branch for an opened existing digest directory;
- retain the before/open/after identity checks that defend against entry
replacement races;
- change the existing rooted `O_CREATE|O_EXCL` temporary-file open mode from
`0600` to `0666`; and
- remove the subsequent `temp.Chmod(0600)` call while preserving write,
sync, close, rename, cleanup, and interruption hooks.
2. Do not replace `os.Root` operations with ordinary path operations, weaken
unexpected-entry handling, or create a missing root during read-only load.
3. Rewrite `TestFilesystemStorePermissions` into a behavioral Unix test that
controls and restores umask. Consolidate permission cases rather than
retaining a stale forced-private assertion alongside the new contract.
### Tests
- Under umask `0077`, prove the root, digest directory, and published plan are
`0700`, `0700`, and `0600`.
- Under umask `0007`, prove they are `0770`, `0770`, and `0660`.
- Because the published plan is the renamed temporary inode, its final mode
proves the temporary-file creation policy; use the existing pre-rename hook
only if a separate assertion is needed to prevent a plausible regression.
- Pre-create both the root and digest directory with deliberate modes, save a
plan, and prove neither directory is modified.
- Retain the existing missing-root side-effect, symlink, entry-replacement,
interruption, atomic-replacement, record-validation, and recovery tests.
### Validation
Run:
```sh
gofmt -w internal/framework/chunkplan/*.go
go test ./internal/framework/chunkplan
go test -race ./internal/framework/chunkplan
git diff --check
```
### Stage Acceptance
Chunk-plan writes honor both controlled deployment policies, existing
directories are not converged, and all rooted atomicity and recovery coverage
passes. This stage is small enough for one implementation prompt.
## Stage 3: Convert Checkpoints And Prove Assembled Cache Behavior
### Goal
Move checkpoints to the new shared writer, remove stale private-cache
expectations at the CLI boundary, and prove one assembled run applies the same
deployment policy to both cache families.
### Required Changes
1. Change the checkpoint recorder's JSON write boundary in
`internal/framework/checkpoint` to call
`fileio.WriteJSONWithProcessPermissions`. Do not thread modes through the
recorder or introduce a cache permission configuration value.
2. Replace the checkpoint round-trip test's forced `0700`/`0600` subsection
with focused POSIX permission coverage. Use a cache root that does not exist
before the recorder writes so root creation is covered.
3. Preserve checkpoint JSON representation, path layout, manifests, payloads,
identity, recording enablement, reuse eligibility, loader decisions, and
corrupt/incompatible-state behavior.
4. In `internal/cli/state_hardening_test.go`, stop applying
`assertRestrictedTree` to checkpoints. Retain that helper and assertion for
debug state, whose permission contract is unchanged.
5. Add one supported-Unix CLI integration test using the existing offline
state-test harness:
- configure both chunk-plan caching and checkpoint recording beneath
previously absent roots;
- set and reliably restore umask `0007` around the run;
- prove representative directories and final files in both cache families
are group-accessible (`0770`/`0660`);
- prove the run completes and the stored state remains reusable; and
- do not duplicate every package-level permission or recovery case.
6. Keep output and debug mode assertions unchanged and confirm that no
`cache.access` parsing or composition code exists.
### Tests
- Add checkpoint-level cases for umask `0077` and `0007`, covering
representative nested directories and both manifest and payload files.
- Prove a pre-existing checkpoint root is not chmod modified.
- Retain the existing all-stage round trip as the owner of serialization and
loader behavior rather than cloning it into each permission case.
- Ensure every umask-changing test is non-parallel and restores global state.
### Validation
Run:
```sh
gofmt -w internal/framework/checkpoint/*.go internal/cli/*.go
go test ./internal/framework/checkpoint ./internal/cli
go test -race ./internal/framework/checkpoint ./internal/cli
go test ./internal/framework/chunkplan ./internal/core/fileio
git diff --check
```
### Stage Acceptance
Both cache families follow process/filesystem policy through the real CLI
composition, checkpoint reuse remains intact, and output/debug permission
contracts have not changed. This stage is small enough for one implementation
prompt.
## Stage 4: Update Canonical Documentation
### Goal
Make the implemented behavior discoverable without duplicating volatile
details across documentation owners.
### Required Changes
1. Update `docs/policy/architecture.md` with the normative boundary:
deployments own cache access policy; Notarius owns confined paths, atomic
publication, validation, and recovery. Do not generalize the rule to output
or debug state in this work.
2. Update `docs/operations.md`, the canonical operational owner, to:
- replace claims that cache entries are forcibly `0700`/`0600`;
- explain requested maximum modes `0777`/`0666`, umask filtering, default
ACLs, and setgid group inheritance without claiming literal mode-bit
inheritance;
- provide concise private (`0077`) and trusted-group (`0007` plus a
deployment-provisioned setgid root, or an equivalent default ACL)
examples;
- state that cache data is reconstructible but may contain source-derived
content and operators own access and retention;
- explain that pre-feature cache trees retain their existing inode modes and
must be deliberately recreated or administratively migrated; and
- accurately limit concurrent-use claims to the existing atomicity and
validation guarantees.
3. Update `docs/config.md` only to clarify that selecting chunk-plan or
checkpoint directories does not configure their filesystem access policy.
Do not document a nonexistent permission field.
4. Update `docs/internal/state.md` with the two implemented writer paths:
rooted chunk-plan atomic creation and checkpoint use of the
process-permission file writer. Link to Operations for operator-facing mode
and provisioning guidance instead of duplicating it.
5. Review `docs/internal/overview.md` and `docs/development.md`; update links or
routing only if the existing navigation no longer reaches the canonical
state and operations documents.
6. Do not edit a historical release note. The release procedure will summarize
this change when the next release candidate is prepared.
### Validation
- Verify every changed relative Markdown link and heading anchor.
- Search current-behavior documentation for stale assertions that cache modes
are always `0700`/`0600`; distinguish cache from still-valid debug claims.
- Confirm current-behavior documentation outside `docs/roadmap/` contains
neither `cache.access` nor instructions for Notarius-managed
chmod/chown/chgrp/ACL mutation.
- Run `git diff --check`.
### Stage Acceptance
Each policy and operational fact has one canonical owner, current docs describe
only implemented behavior, and the feature roadmap remains the owner of future
status until final review. This stage is small enough for one implementation
prompt.
## Stage 5: Repository-Wide Verification And Contract Audit
### Goal
Prove the complete target state and catch accidental permission, safety, or
documentation drift before review.
### Required Review
1. Inspect production cache code for remaining `0700`, `0600`, or `Chmod`
operations. Every remaining occurrence must be a fixture, an unrelated
output/debug contract, or otherwise justified; neither cache writer may
enforce those values.
2. Search configuration, CLI, examples, and documentation to confirm no
`cache.access` surface or private/group application policy was introduced.
3. Confirm output and debug production paths and their contract tests are
unchanged except for necessary documentation disambiguation.
4. Confirm cache reads remain side-effect free where previously guaranteed and
every cache write remains confined and atomic.
5. Review all new tests against `docs/policy/testing.md`. Consolidate redundant
mode assertions while retaining one owner for each realistic regression:
kernel filtering, existing-root preservation, each cache writer, and
assembled CLI wiring.
6. Review the ADR and canonical docs for the agreed scope. Do not describe
Unix permissions as literal inheritance from a parent directory.
### Validation
Run the complete offline suite:
```sh
gofmt -w internal/core/fileio/*.go internal/framework/chunkplan/*.go internal/framework/checkpoint/*.go internal/cli/*.go
go test ./...
go test -race ./internal/core/fileio ./internal/framework/chunkplan ./internal/framework/checkpoint ./internal/cli
go vet ./...
go build ./cmd/notarius
go run ./cmd/notarius config validate --config examples/dnd-minimal.config.yml --pipeline dnd-session
OPENROUTER_API_KEY=validation-placeholder go run ./cmd/notarius config validate --config examples/dnd-complete.config.yml --pipeline dnd-session
git diff --check
```
The config validations must perform no provider requests. Also inspect
`git status --short --untracked-files=all` for unintended files, generated
artifacts, or unrelated edits.
### Final Acceptance
Every acceptance criterion in the feature roadmap is supported by direct code,
test, or documentation evidence; all prescribed commands pass; the working
diff contains only this feature; and the two roadmap documents remain in place
for the post-implementation review. This stage is small enough for one final
verification prompt.