Skip to content

feat(daemon): mount revocation caretaker + deny patterns (PR A of #127)#650

Merged
kriscendobot merged 2 commits into
llmfrom
feat/mount-revocation
Jul 10, 2026
Merged

feat(daemon): mount revocation caretaker + deny patterns (PR A of #127)#650
kriscendobot merged 2 commits into
llmfrom
feat/mount-revocation

Conversation

@kriscendobot

@kriscendobot kriscendobot commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Reconstructs PR A of the #127 mount-extensions split on the current llm branch, per designs/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 mutable revocation = { revoked } record carried on the mount ctx. Because sub-faces are derived by spreading ctx, a single control.revoke() trips the common assertLive() gate on every method of the root mount and every face derived from it: sub-views, entries, opened files, readOnly() views, makeDirectory results, and any open followNameChanges stream.
  • control is a captive EndoMountControl exo (revoke(), help()) guarded by the new MountControlInterface.
  • The daemon's mount and scratch-mount formulas mint through makeRevocableMount and wire context.onCancel(() => control.revoke()), tying revocation to formula cancellation. The control facet stays internal to the daemon.

Deny patterns

  • An exported defaultDeniedSegments set 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 throws Access denied; list() and followNameChanges() (both snapshot batch and diff events) omit them. Ordinary dotfiles like .gitignore stay accessible.
  • A deniedSegments creation option on makeMount / makeRevocableMount replaces the default (an empty iterable disables denial; callers extend by spreading defaultDeniedSegments). It is plumbed through the MountFormula / ScratchMountFormula records (included only when overridden, so default mounts keep their historical formula shape) and provideMount / provideScratchMount.

Reconciliation with current llm (drift from #127)

Tests

test/mount-revocation.test.js (20 cases): revocation propagation matrix (mount, subView, file handle, entry, readOnly() view, open followNameChanges stream), deny defaults (throw / list() hides / change-stream hides / .gitignore unaffected / case-insensitive), override behavior (custom set, defaults inert, empty set admits .ssh, spread-extends). yarn ava test/mount*.test.js → 117 pass; tsc clean.

Follow-up

🤖 Generated with Claude Code

@kriscendobot

Copy link
Copy Markdown
Collaborator Author

Pushed 104895edd224510091403e240b582d9d3c1c3c72 adding two daemon-level tests to test/endo.test.js, closing a gap against the design's PR A test list (which names formula cancellation revokes, not only a direct control.revoke()):

  • cancelling the mount formula revokes live mount and file handles — provisions a real mount via provideMount, hands out a subView and a file handle, then E(host).cancel(...) the formula and asserts the mount, sub-view, and file handle all throw revoked. This exercises the daemon's context.onCancel(() => control.revoke()) wiring end-to-end, which the unit-level mount-revocation.test.js (calling control.revoke() directly) does not reach.
  • mount denies sensitive segments through the mount formula — asserts list() hides .ssh/.env and direct reads throw restricted path through the real formula path.

Verification (this deep worktree's absolute socket path exceeds the 108-byte AF_UNIX limit, so endo.test.js's daemon-integration tests cannot bind here — this is pre-existing and affects every daemon test in this worktree, not these two):

  • Both new tests were verified against a real daemon via a standalone driver using a short /tmp socket path: all seven assertions passed (deny listing, deny direct-read x2, pre-revoke read, post-cancel revoked x3).
  • test/mount-revocation.test.js + test/mount.test.js + test/mount-platform-fs-conformance.test.js → 112 pass.
  • tsc clean on the touched files (the one host.js diagnostic is the pre-existing baseline error, shifted by the added lines); eslint 0 errors; prettier --check clean.

Also filed the design's named CLI-plumbing follow-up as #651.

kriscendobot added a commit that referenced this pull request Jul 10, 2026
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.
kriscendobot pushed a commit that referenced this pull request Jul 10, 2026
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.
@kriscendobot kriscendobot marked this pull request as ready for review July 10, 2026 01:47
Comment on lines +36 to +40
const makeTempRoot = t => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mount-revoke-'));
t.teardown(() => fs.rmSync(dir, { recursive: true, force: true }));
return dir;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid abbreviation in the name.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in bc68ac3 — renamed dir to directory in the makeTempRoot helper.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Please rename makeTempRoot to makeTemporaryRoot.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ad18c76e: renamed makeTempRootmakeTemporaryRoot 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],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjust this test to include a novel denied segments to ensure that it is differentiated from the defaults.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

kriscendobot pushed a commit to kriskowal/garden that referenced this pull request Jul 10, 2026
kriscendobot pushed a commit that referenced this pull request Jul 10, 2026
- 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 kriskowal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rebase, retcon, and conduct onto llm, with the below recommended change.

Comment on lines +36 to +40
const makeTempRoot = t => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mount-revoke-'));
t.teardown(() => fs.rmSync(dir, { recursive: true, force: true }));
return dir;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Please rename makeTempRoot to makeTemporaryRoot.

kriscendobot pushed a commit to kriskowal/garden that referenced this pull request Jul 10, 2026
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.
@kriscendobot kriscendobot force-pushed the feat/mount-revocation branch from bc68ac3 to ad18c76 Compare July 10, 2026 16:59
@kriscendobot kriscendobot changed the base branch from llm-7870da1 to llm-08f5acc July 10, 2026 16:59
@kriscendobot kriscendobot changed the base branch from llm-08f5acc to llm July 10, 2026 17:03
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.
@kriscendobot kriscendobot merged commit 5e3232c into llm Jul 10, 2026
24 checks passed
@kriscendobot kriscendobot deleted the feat/mount-revocation branch July 10, 2026 20:11
kriscendobot added a commit to kriskowal/garden that referenced this pull request Jul 10, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants