feat(daemon): mount revocation caretaker + deny patterns (PR A of #127)#650
Conversation
|
Pushed
Verification (this deep worktree's absolute socket path exceeds the 108-byte AF_UNIX limit, so
Also filed the design's named CLI-plumbing follow-up as #651. |
Plumb the mount `deniedSegments` creation option through the `endo mount` and `endo mktmp` (scratch-mount) commands, the CLI follow-up deferred when PR A (#650) landed the daemon-side option. A repeatable `--deny <segment>` flag names path segments whose set *replaces* the mount's default restricted set, and `--no-deny` disables denial entirely with an empty set; absent both flags the mount keeps its default `defaultDeniedSegments`. The parsed option resolves to the daemon's `deniedSegments` shape and forwards through `provideMount` / `provideScratchMount`, mirroring the `--read-only` plumbing.
Panel-review follow-ups on PR A of the #127 mount split: - streamBase64 re-checks liveness per chunk, so a revoke mid-read stops delivering the remaining bytes instead of draining the file — matching the per-iteration gating followNameChanges already had. - entry()'s array form now denies restricted segments eagerly at mint, like its string form (both re-deny at resolution regardless; this closes the eager-vs-lazy asymmetry). - Scope the defaultDeniedSegments docstring to the name-based (not realpath) matching it actually enforces: it does not, on its own, catch an in-root symlink alias to a restricted target, and it restricts children not the mount root — defense-in-depth behind confinement. - Reword the daemon.js formula comment: the conditional deniedSegments field preserves the historical persisted formula shape, not "identity" (formula numbers are random, not content-addressed). - Changeset: call out that the deny set is on by default (a behavior change for existing home-dir mounts, retroactive after upgrade; opt out with deniedSegments: []). - Tests: entry()/child() deny (string + array), list() hides at depth, and a base64 file stream refusing on a revoked mount. Replace decorative divider comments with plain ones.
| const makeTempRoot = t => { | ||
| const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mount-revoke-')); | ||
| t.teardown(() => fs.rmSync(dir, { recursive: true, force: true })); | ||
| return dir; | ||
| }; |
There was a problem hiding this comment.
Avoid abbreviation in the name.
There was a problem hiding this comment.
Done in bc68ac3 — renamed dir to directory in the makeTempRoot helper.
There was a problem hiding this comment.
Good catch. Please rename makeTempRoot to makeTemporaryRoot.
There was a problem hiding this comment.
Done in ad18c76e: renamed makeTempRoot → makeTemporaryRoot throughout mount-revocation.test.js. The branch was also rebased onto llm (new frozen base llm-08f5acc) and retconned to a single feat(daemon) commit (implementation + tests + changeset), net diff invariant. Conducting once checks are green.
| rootPath, | ||
| readOnly: false, | ||
| filePowers, | ||
| deniedSegments: [...defaultDeniedSegments], |
There was a problem hiding this comment.
Adjust this test to include a novel denied segments to ensure that it is differentiated from the defaults.
There was a problem hiding this comment.
Done in bc68ac3 — the deny+revocation test now materializes a novel vault segment and passes deniedSegments: [...defaultDeniedSegments, 'vault'], then asserts lookup('vault') is denied. Since vault is not in defaultDeniedSegments, the test now proves the supplied set is actually applied rather than passively matching the defaults.
…n-garden-ece02cb4 [id:endojs/endo-but-for-bots#650:review:4673308688]
- Rename abbreviated `dir` to `directory` in the makeTempRoot helper. - Include a novel denied segment (`vault`) in the deny+revocation test so it proves the supplied deny set is applied, differentiated from the defaults rather than passively matching them. Addresses review #650 (review) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kriskowal
left a comment
There was a problem hiding this comment.
Please rebase, retcon, and conduct onto llm, with the below recommended change.
| const makeTempRoot = t => { | ||
| const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mount-revoke-')); | ||
| t.teardown(() => fs.rmSync(dir, { recursive: true, force: true })); | ||
| return dir; | ||
| }; |
There was a problem hiding this comment.
Good catch. Please rename makeTempRoot to makeTemporaryRoot.
…n-garden-ece02cb4 [id:endojs/endo-but-for-bots#650:review:4673371396]
Add the mount revocation caretaker (makeRevocableMount / EndoMountControl) and the defense-in-depth deny-pattern set with an overridable deniedSegments creation option, waking open followNameChanges streams on revoke. Includes the unit and end-to-end coverage and the changeset. Addresses the panel review and the makeTempRoot -> makeTemporaryRoot rename requested on review 4673371396.
bc68ac3 to
ad18c76
Compare
Adds the missing ../platform project reference to daemon-cas's composite tsconfig — pre-existing drift inherited from the #442 daemon-cas extraction on the llm base. The composite-tsconfig drift check is repo-wide all-or-nothing, so this base drift fails PR lint until regenerated. Independent of the mount-revocation change.
The "Messages to the maintainer" section inlined each message body as a blockquote, but the GitHub issue/PR references in that body (`#652`, `endojs/endo-but-for-bots#650`, full issue/PR URLs) rendered as dead plain text. Turn them into working Markdown links so the maintainer can click straight through from the bulletin — on both the GitHub-rendered journal2 README and the gh-pages client (both render `[label](url)`). msg_body_quote now linkifies each non-fence body line: full GitHub issue/PR URLs become `[url](url)`, `owner/repo#N` links to that repo's /issues/N, and a bare `#N` links to the ORIGINATING PROJECT — resolved from the message's doer (reply_to/from job base) against the bare fork clones under worktrees/ via the new resolve_doer_repo. When the repo can't be resolved with confidence the bare `#N` is left plain (a wrong link is worse than none). Formed links are held aside behind sentinels so nothing is double-linked, and references inside a fenced code block are never rewritten (fences stay intact). Extends SUBTEST 10 (5b): a resolvable maintainer message links owner/repo#N, a bare #N (to the resolved repo), and a full issue/PR URL; fenced references stay plain; an unresolvable doer leaves its bare #N plain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconstructs PR A of the #127 mount-extensions split on the current
llmbranch, perdesigns/mount-extensions-reconstruction.md§ "PR A — revocation and deny patterns" (design PR #648). This is the first of the four stacked reconstruction PRs; #127 stays open as the reference diff until all four are open.What this adds
Revocation caretaker
makeRevocableMount(options) -> { mount, control }mints a mount over a shared mutablerevocation = { revoked }record carried on the mountctx. Because sub-faces are derived by spreadingctx, a singlecontrol.revoke()trips the commonassertLive()gate on every method of the root mount and every face derived from it: sub-views, entries, opened files,readOnly()views,makeDirectoryresults, and any openfollowNameChangesstream.controlis a captiveEndoMountControlexo (revoke(),help()) guarded by the newMountControlInterface.mountandscratch-mountformulas mint throughmakeRevocableMountand wirecontext.onCancel(() => control.revoke()), tying revocation to formula cancellation. The control facet stays internal to the daemon.Deny patterns
defaultDeniedSegmentsset restricts well-known credential/config names (.ssh,.aws,.azure,.gcloud,.config,.gnupg,.password-store,.docker,.npmrc,.env,.env.local,.env.production,.kube,.terraform) case-insensitively. Naming one in a path throwsAccess denied;list()andfollowNameChanges()(both snapshot batch and diff events) omit them. Ordinary dotfiles like.gitignorestay accessible.deniedSegmentscreation option onmakeMount/makeRevocableMountreplaces the default (an empty iterable disables denial; callers extend by spreadingdefaultDeniedSegments). It is plumbed through theMountFormula/ScratchMountFormularecords (included only when overridden, so default mounts keep their historical formula shape) andprovideMount/provideScratchMount.Reconciliation with current
llm(drift from #127)#127's module-levelDENIED_SEGMENTSbecomes the exported, per-mount-overridabledefaultDeniedSegments(review comment 3548865148), enforced at path resolution rather than in the sharedassertValidSegment(which tree-entry-name validation also uses).followNameChangesis implemented onllm(not feat(daemon): mount extensions — revocation, deny patterns, glob/grep/stat/json (re-opened from #37 under the bot) #127's ENOSYS stub), so revocation gating and deny-filtering are added to the real snapshot-then-diff generator.subView, not the removedsubDir.Tests
test/mount-revocation.test.js(20 cases): revocation propagation matrix (mount, subView, file handle, entry,readOnly()view, openfollowNameChangesstream), deny defaults (throw /list()hides / change-stream hides /.gitignoreunaffected / case-insensitive), override behavior (custom set, defaults inert, empty set admits.ssh, spread-extends).yarn ava test/mount*.test.js→ 117 pass;tscclean.Follow-up
deniedSegments(a--denied-segmentsflag on themount/scratch-mountCLI commands) is deferred per the design and tracked in Mount: CLI plumbing for the deniedSegments creation option #651.🤖 Generated with Claude Code