Repository navigation
feat(ns-exec): add exec session supervisor skeleton (#538) - #64
Draft
mengmengmengqiang wants to merge 3 commits into
Draft
mengmengmengqiang wants to merge 3 commits into
mengmengmengqiang wants to merge 3 commits into
Conversation
…#538) One supervisor owns one command across the Ephemeral-AI-Lab#499 mount-namespace seam: - hidden `ns-exec-supervisor` receives the daemon's mount-namespace descriptor over SCM_RIGHTS, spawns `ns-exec-child`, and closes its own copy. The supervisor never setns()es and never chdir()es into the Workspace, so it cannot become a mount holder; only the child joins. - the child runs Ephemeral-AI-Lab#499's sequence (setns, chdir, drop uid/gid) and then takes a new session with setsid(); it sets PR_SET_PDEATHSIG and re-checks its parent so a lost supervisor does not leave an unsupervised command. - a 0600 Unix control socket frames Started/Exited/Status/Signal/Stop with a four-byte length and a small fixed body. Signal and Stop only target the child's own process group (kill(-pgid, signo)); the peer must have the supervisor's effective uid. - wait4 reaps exactly once, reports exit code or signal, and reports group_alive (kill(-pgid, 0)) when grandchildren remain instead of pretending a clean exit. Grandchildren are not killed in this slice. Deliberately deferred: client-presence detection, the daemon session registry, force-kill orchestration and close(force) integration. Tests (Linux, mount lane): join the namespace served over SCM_RIGHTS and report exit 7; pgid == pid and different from the supervisor; supervisor stays outside the Workspace namespace/cwd; identity drop to 65534; sleep + SIGTERM -> 143; Status + SIGKILL -> 137; group_alive for a background sleep. Design note in core/docs/issues/538/EXEC-SUPERVISOR-SKELETON.md.
mengmengmengqiang
added a commit
that referenced
this pull request
Nov 4, 2026
…AI-Lab#538) Replace the rejected `sandbox.close(force)` recovery with a workspace-scoped force kill. `workspace.unmount(ws, discard?, force?, kill?)` takes `kill` as a string enum; only `"workspace"` is accepted and any other value, or `force` together with `kill`, is refused `InvalidInput`. With `kill: "workspace"` the call enumerates only the target Workspace's holders — foreground `/tmp` records naming that path and background processes the binding started there — and cancels each: TERM to its process group, a bounded wait, then KILL only if it is still there, and a second wait. It then retries a *normal* daemon unmount (`unmount_checked`), never the force/abort path, and it never stops or deletes the Sandbox and never touches another Workspace. A holder that survives answers `Busy` with its id. Precedence for a holder's pid/start time is the supervisor registry (future, #64), then the foreground record, then the background map; `/proc` validates pid and start time at kill time, so a stale or reused record is not a holder. A group with a member whose cwd or open file is outside the target Workspace is not killed at all: the call answers `Busy` with `scope_conflict` instead of taking collateral. That check is best-effort until #64's registry is the authority. `discard` still applies only to the normal unmount that follows a successful cancel; the call never drops data while the mount is still Busy. The old `force` gate is kept: a forced unmount with a live foreground record answers `Busy` with the ids instead of producing `Retained(Detach)`. Docs: SDK workspace reference, agent doc table, agent/backend docs. The `sandbox.close(force)` API and its verify script are gone; an invisible escaped holder (Ephemeral-AI-Lab#460) is described as needing a future operator/CLI-only `sandbox.destroy(id, { confirm: true })`, not a close force mode. Tests: `layerfs-node` unit tests for the kill validation, the force/kill conflict, holder merging and the per-Workspace record filter. New E2E `workspace_kill_real.mjs`: a client SIGKILLed during a foreground `sleep 30` leaves an orphan; unmount is `Busy` with its id; `kill: "workspace"` answers `Unmounted`, `/proc/<pid>/stat` and the record are gone, another Workspace's foreground orphan and live background command survive, the Sandbox container still exists; a `cd /tmp && sleep 30` holder is `scope_conflict` and is not killed; a target's background holder is killed while another Workspace's survives.
mengmengmengqiang
marked this pull request as draft
November 4, 2026 04:23
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 6, 2026
…emon open (layerfs-project #64) - A PostgreSQL Store opens with shared readers (`Store::with_shared_readers`): every Workspace's healthy reader returns to the idle readers at unmount (at most IDLE_READERS = 4) and the next mount takes one, so a mount after an unmount opens no connection. SQLite keeps its readers as before. - An idle reader is checked before a mount takes it (`StoreReader::usable`, `PostgresStorage::usable`: one empty query, outside the ReadSet lock); a quarantined or dead one is dropped and a new reader opens, so no operation runs on a broken connection and none is retried. Quarantine rules in transaction.rs are unchanged. - The control reader of a PostgreSQL Store opens at its first use (`ReadSet::control`), not at Store open. In the daemon the first Commit uses it, so its connection moves from sandbox.create to the first Commit. - Readers and a Sandbox daemon's writer open with the light check (#63); the host Store service and Init keep the full check. - Lane pre-open is NOT changed: the lane connection still opens inside the first Commit (owner decision). Server connections per daemon: writer + lane (first Commit) + control (first use) + one per mounted Workspace + up to 4 idle. Counts (measure count test 0bdc970, PG conn/rt): store_open 2/28 at base -> 2/2 after #63 -> 1/1; workspace_reader_open 1/30 -> 1/17 (first mount of a fresh daemon still opens); commit.net_empty_1000 connects 1 -> 2 (lane + lazily opened control). Wire proxy (postgresql_open_cost): daemon Store open 2 conns/34 RTs at base -> 1/4; second mount 1/17 -> 0/1; a mount after the idle reader's connection was cut opens a new reader (1/4). Tests: PASS postgresql_open_cost, store-service postgresql_open, postgresql_round_trips (PG); SQLite workspace_reader_cache, sdk_fixed_costs, object_demand_errors, daemon store_reader_lifecycle. daemon mounted_store_read NOT_RUN here (needs fusectl mount in a privileged container; fails at fusectl.rs:55 before any LayerFS code). Clippy clean for persistence, store-service, daemon (postgresql features). No tests deleted. Production LOC: 138049 -> 138163 (delta +114); tools/production_loc.py combined scope, parent tree vs staged tree via git archive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 6, 2026
rustfmt +1.85.1 on the files these commits changed; no behaviour change. Production LOC: 138429 -> 138459 (delta +30, formatting only: wrapped lines); tools/production_loc.py combined scope, parent tree vs staged tree via git archive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 9, 2026
… session (layerfs-project #64) A forwarded Sandbox's workspace.mount and workspace.status (Docker, Lima: `sandbox/direct.rs`) found the Branch through `store_call`, which opened a whole Store service in the caller's process: a fully checked writer and, at the first read, its control reader. A CLI process is new per call, so every mount paid both connections. These lookups now run on the process's one held read session (`store::reader`, #65), as the native and worker routes already did: one light-checked connection, no writer, no full check, no overlay template. A process that already holds the Store's service open reads there and opens nothing. SQLite is unchanged. Counts (store-service `postgresql_open_cost`, wire proxy, PostgreSQL 17): caller side of a mount 2 connections / 16 round trips -> 1 / 12. With measure's statement split (Ephemeral-AI-Lab#94): caller 2 / 15 statements -> 1 / 9; mount 3 / 30 -> 2 / 24. The daemon side is unchanged: a fresh daemon's first mount opens its Workspace reader (1 / 4); later mounts reuse an idle one (0 / 1). - store/call.rs: `held_branch`, the PostgreSQL route of the lookup. - store/reader.rs: `branch` (id and head) on the held session; shared `branch_on` with `branch_id`. - store/open.rs: `holds`, whether this process holds a Store open. - sandbox/direct.rs (create agent's file, 3 lines): `branch_of` asks `held_branch` first. - Test: postgresql_open_cost gains the before and after host-mount rows. - Docs: workspace.mount, routing table, Store service (first mount). No tests deleted. Production LOC: 139048 -> 139101 (delta +53); api 20764 -> 20817 (+53), core, engine and connect unchanged Scope: tools/production_loc.py combined (core, engine, connect, api); parent tree vs staged tree; reference absent (retired R9c; last counted 65,417 at tree 498dd19). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 9, 2026
…ts held session Production LOC: 139048 -> 139101 (delta +53), first-parent comparison (pg-r2 b4fa173 -> merged tree), counted with tools/production_loc.py; all in api. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 9, 2026
…rfs-project #64 option B) Owner decision 2026-11-06: mount option B approved. A CLI mount opened a PostgreSQL connection in the caller to find the Branch's id (option A: one light-checked held session, 9 statements). The caller now sends the daemon the project's name and the Branch's (`Request::MountNamed`, wire tag 15), and the daemon finds the Branch with its snapshot in one statement on the Workspace's own Store reader, the read it already made by id. The caller reads no Store on any route (worker, local, forwarded). - core history: `NamedSnapshot` and `HistoryCatalog::branch_snapshot_named`, a default method (stack, Branch, snapshot reads) providers may answer in one statement. - persistence: `branch_snapshot_named` statement (SQLite and PostgreSQL; PostgreSQL runs it alone, no BEGIN/COMMIT) and the provider override. A new query; no schema or format change. - store-service: `Store::bind_named` (`NamedBindRequest`, `BindIntent`; `BindError::NoSuchProject` / `NoSuchBranch`), `ReadSession::snapshot_named`; `lookup::no_project` / `no_branch` are the one source of the refusals. - bridge: `Request::MountNamed`. - daemon: serves it; a missing project or Branch is `Reply::SdkRefused` with the lookup's code and message, after the admission is withdrawn (an uncertain cleanup keeps the original failure). - control: `WorkspaceApi::mount_named`; `serve::mount` sends names and passes the daemon's refusal through unchanged; the caller's Branch-id lookups (`store::service::branch_id`, `store::reader::branch_id`) are removed. `sandbox/direct.rs` (create agent's file): mount calls `serve::mount`. Status still reads its head on the caller's held session (option A). - Exact refusals kept: invalid project name, missing project -> NoSuchProject; missing Branch, `main`, invalid Branch name -> NoSuchBranch, same messages. Tests: daemon `named_mount` (new: success, the six refusals, withdrawn admission); bridge `control_records` gains MountNamed; store-service `postgresql_open_cost` gains the by-name snapshot rows; `postgresql_phase_attribution` models the new mount (no caller steps; `mount.caller_open` and `mount.caller_lookup` removed with the route). No tests deleted. Counts (PostgreSQL 17): CLI mount caller 1 connection / 9 statements (option A) -> 0 / 0; daemon bind unchanged at 1 snapshot statement. Production LOC: 139305 -> 139503 (delta +198); core 38706 -> 38728 (+22), engine 63714 -> 63893 (+179), api 20817 -> 20814 (-3), connect 16068 unchanged; reference absent (retired R9c; last counted 65,417 at tree 498dd19). Method: python3 tools/production_loc.py --root on the parent (git archive HEAD) and the staged tree (git checkout-index -a). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 9, 2026
…s (layerfs-project #64) Owner decision 2026-11-06: one eager idle reader, opened after the daemon reported ready, never delaying readiness. A fresh daemon held only its writer at its first mount, which then opened the Workspace's reader (one connection, the light check). After a `Hello` answered ControlReady has been sent, the daemon now opens one reader into its idle readers on a thread of its own; the first mount takes it (one usable check) and opens no connection. - store-service `ReadSet::prefill` / `Store::prefill_reader`: once per Store (later calls return at once), only with shared readers (PostgreSQL), only when no reader is idle, under the idle cap of 4. One attempt: a failure is counted (`ReuseWork::prefill_failures`) and returned, nothing is kept and nothing opens it again; the first mount then opens its own. `ReuseWork::readers_prefilled` counts the opened one. - daemon `application/connection.rs`: after the ControlReady `Hello` reply is written, `reader_after_ready` spawns the open; a failure goes to the daemon's log. owner.rs and bootstrap.rs are untouched. - A mount that arrives before the idle reader is open opens its own, as before; the late one joins the idle readers. - Server connections per idle PostgreSQL daemon after create: 1 -> 2 (the writer and the idle reader). The maximum is unchanged: writer, lane, control reader, one per mounted Workspace, 4 idle. Tests: store-service `postgresql_open_cost` gains the eager-reader rows (open once, first mount takes it, a refused open is one counted failure and is not tried again); `postgresql_phase_attribution` models it as `create.daemon_idle_reader`; daemon `named_mount` checks a SQLite Store opens none. No tests deleted. Counts (PostgreSQL 17): first mount after readiness 1 connection / 4 round trips -> 0 / 1 (the idle reader's usable check); the open itself, 1 / 4, runs after the create's readiness reply. Create's counts are unchanged. Production LOC: 139503 -> 139590 (delta +87); engine 63893 -> 63980 (+87), core 38728, api 20814 and connect 16068 unchanged; reference absent (retired R9c; last counted 65,417 at tree 498dd19). Method: python3 tools/production_loc.py --root on the parent (git archive HEAD) and the staged tree (git checkout-index -a). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 9, 2026
…e reader after readiness Conflicts resolved: persistence postgresql/history/statements.rs (kept Ephemeral-AI-Lab#95's by-id and #64's named statements) and docs store-calls/service.md (kept #64's eager-reader text with Ephemeral-AI-Lab#95's validate note). Production LOC: 139330 -> 139615 (delta +285), first-parent comparison (pg-r2 2bb6782 -> merged tree), counted with tools/production_loc.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 9, 2026
…fs-project #64) `BindRefusal::request` became a `BindIntent` in 9962df8 (option B), and two daemon test targets still read `.branch` / `.workspace` on it, so they no longer compiled: `store_reader_lifecycle` (a failed bind returns the original request) and `installed_store` (a duplicate bind after stop). Both now require `BindIntent::Id` and compare the same two fields, so each assertion is as strong as before and also checks the variant. The route they drive (bind by identity) survives; no tests deleted. Production LOC: 139590 -> 139590 (delta 0); test-only change. Reference absent (retired R9c; last counted 65,417 at tree 498dd19). Method: python3 tools/production_loc.py --root on the parent (git archive HEAD) and the staged tree (git checkout-index -a). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 9, 2026
Tests only. Production LOC: 139680 -> 139680 (delta 0), first-parent comparison, counted with tools/production_loc.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 9, 2026
…ent (layerfs-project Ephemeral-AI-Lab#96) A Sandbox create's reach check (control `store/reader.rs` `reach`, on the launching side's held read session) finds its project with `layer_stack_named`: one `layer_stack` row by its unique name. On PostgreSQL that ran as a read transaction (BEGIN REPEATABLE READ with SET LOCAL search_path, the SELECT, COMMIT). It is one statement, so it now runs as `Mode::Statement` (one autocommit statement, one snapshot) with a schema-qualified text, as Ephemeral-AI-Lab#95 did for a Commit or Layer by id. SQLite is unchanged (`read_statement` is its one read transaction). Create counts (`postgresql_case_attribution`, C01/C13/C14/C16, server statements via pg_stat_statements; base 91198bf): - create.reach: 5 statements / 5 round trips -> 2 statements / 3 round trips (light check + prepare + SELECT), 1 connection - create.daemon_open: 1 statement, 1 connection (unchanged) - create total: 6 statements / 2 connections -> 3 / 2 (with a held reach session: 5/1 -> 2/1) - create.daemon_idle_reader (#64, after readiness, off the create path): 1 connection, 1 statement, unchanged and not counted above `postgresql_open_cost` "host create (attach + project lookup)": 1 connection, 8 -> 6 round trips. Kept: the held reach session (it still serves a status's head and is reused by the next create within 60 s; closing it would save nothing per create) and the daemon's light-checked open (one statement; lower needs dropping the check or an eager connection). Nothing moves into mount or Commit. Docs: store/postgresql.md lists the read under `Statement`; api/sdk/sandbox/create.md gives the new counts, the idle reader, and drops the stale mount use of the held session (#64 option B). Granted minimal edit in persistence history reads (`history/catalog.rs`, `backend/postgresql/history/statements.rs`, one new SQL text). Tests (Linux, PostgreSQL 17.11): persistence postgresql_open_check 3/3, postgresql_commit_round_trips 1/1, postgresql_acquisition 4/4, postgresql_identical_ids 3/3, layerstack_removal 2/2; store-service postgresql_round_trips 2/2, postgresql_open_cost 1/1, postgresql_open 1/1, postgresql_phase_attribution (case) 1/1, operator_history (SQLite) 2/2; project init_postgresql 1/1; api postgres_connect 2/2, postgres_refusals 4/4, postgres_sandbox_reach 1/1 (with layerfs-api/machine). Clippy -D warnings for layerfs-persistence with postgresql, all targets. No tests deleted. Production LOC: 139680 -> 139684 (delta +4) Scope: tools/production_loc.py (pinned) on the parent tree and the staged tree, combined core/engine/connect/api production source (includes shipped runtime SQL); reference absent (retired R9c; last counted 65,417 at tree 498dd19). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 9, 2026
Brings in Ephemeral-AI-Lab#582-Ephemeral-AI-Lab#593 (SQLite idle WAL checkpoint, ring admission, chain-root rebase, PG Init cleanup). One conflict: api/crates/layerfs-control/src/store/open.rs, both sides only added functions (#64 holds; Ephemeral-AI-Lab#86 idle-checkpoint helpers); kept both. Production LOC: 139821 -> 140178 (delta +357), first-parent comparison (pg-r2 76a32a9 -> merged tree), counted with tools/production_loc.py; the delta is the upstream changes. Checked on the merged tree: Clippy -D warnings (persistence, store-service, daemon, control, storage, content, workspace, history, bridge with postgresql/machine) and rustfmt clean; 219 test binaries of storage, content, workspace, persistence and store-service run one by one against PostgreSQL 17: 218 pass (1,020 tests), filesystem_attributes fails 1/13 only because its fixture path is relative to the crate directory (known, same at base). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 9, 2026
PostgreSQL round 2 (layerfs-project #61: Ephemeral-AI-Lab#94-Ephemeral-AI-Lab#98, #64, #66): fewer connections and round trips in create, mount, Commit and reads, now on top of Ephemeral-AI-Lab#84's storage changes (Ephemeral-AI-Lab#594, Ephemeral-AI-Lab#595/Ephemeral-AI-Lab#596). Quick verification on this exact tree dd191ba (layerfs-benchmark#10): backup B1-B4 51/51; fs-bench-quick q84-2 + q84-2x 36/36 PASS with PostgreSQL counts equal to the round-2 control except C16 Commit 251 -> 224 statements, no aggregate or per-row timing flag; fs-storage-quick Q1/Q5/Q6 x SQLite/PostgreSQL 6/6 PASS, every metric 'same' as the Ephemeral-AI-Lab#84 final. Clippy -D warnings and cargo fmt --check clean. Production LOC: 140338 -> 141118 (delta +780) Scope: combined first-party production source, tools/production_loc.py --json on git archive trees, first parent e20c6a2. +773 round 2 (Ephemeral-AI-Lab#94-Ephemeral-AI-Lab#98, #64) and +7 from a rustfmt-only commit on Ephemeral-AI-Lab#92 files. Reference absent (retired R9c; last counted 65,417 at tree 498dd19). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mengmengmengqiang
pushed a commit
that referenced
this pull request
Nov 9, 2026
…ct#101 M-1) Integrates the advanced base by merge, not rebase, so the commits cited in the Ephemeral-AI-Lab#102-Ephemeral-AI-Lab#107 issue comments keep their ids (owner, 2026-11-06, M-1). 44 base commits: PostgreSQL round-trip work (#64, Ephemeral-AI-Lab#90, Ephemeral-AI-Lab#94-Ephemeral-AI-Lab#97), inode value blocks and schema v2 (Ephemeral-AI-Lab#91), directory/extent page deltas (Ephemeral-AI-Lab#92), provider exit polling (#66) and Workspace hard links to symlink inodes (Ephemeral-AI-Lab#580). Hand-ported hunks in the files the split moved (git's rename detection mapped all 11; 5 code files and 8 docs conflicted): - #64 option B, the daemon finds a mount's Branch by name: `MountNamed` in layerfs-sandbox `daemon/{channel,workspace/mount,workspace/recovery}.rs`; `workspace/serve.rs` `mount` sends project and Branch names and keeps a daemon lookup refusal as the answer; `mount_with` and `layerfs_store::service::branch_id` are gone. The forwarded-Sandbox mount and status arms, which the split moved from `direct.rs` to `workspace/direct.rs`, take the base's change there. - #64 status head read on the held PostgreSQL session: `layerfs_store::call::held_branch` (pub: layerfs-sandbox calls it), `open::holds`, `postgresql/reader.rs` `branch`. - #66 provider exit polling: layerfs-platform `setup/tool.rs` and its `machine_probe.rs` test (auto-merged). - Docs: the base's wording with the split's link targets. Checks (all pass, /tmp/cs101/checks-merge.txt): test --no-run; clippy -D warnings on the workspace, with machine,postgresql,tls, and on Linux; fmt; boundary guard; tools unittest (173); host and guest CLI builds, guest tree without layerfs-platform; per-package tests for all 12 control and api packages, each under 120 s: 143 targets pass, including all 98 targets of the Ephemeral-AI-Lab#107 run with the same pass counts. Deleted tests: none. Production LOC: 139843 -> 141554 (delta +1711), first-parent comparison, `python3 tools/production_loc.py --detail` on `git archive` snapshots of the first parent and the merge tree. The base itself grew 139405 -> 141113 (+1708) from 8b701f4 to b180233; the other +3 are the ported hunks' imports at their new paths. control 33926 -> 33981 (platform 4160, sandbox 6186, store 6177); api 3422 unchanged; core 38811 -> 39399, engine 63684 -> 64752 (base only). Reference absent (retired R9c; last counted 65,417 at tree 498dd19). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on Ephemeral-AI-Lab#499 (fork base
feat/ns-exec-helper, head386f1b22). This PR is the skeleton only; it is not wired to the Engine and does not changeworkspace.execbehavior.Scope
A hidden
ns-exec-supervisormode that owns one command across the Ephemeral-AI-Lab#499 mount-namespace seam, plus one hiddenns-exec-childmode on the same binary.SCM_RIGHTSsocket, spawns the child, and closes its own copy. It never callssetnsand neverchdirs into the Workspace, so it cannot become a FUSE mount holder./proc/self/fd(with the existingmnt:check),setns(CLONE_NEWNS),chdir(workspace), drops supplementary groups/gid/uid exactly as feat(sandbox): run workspace.exec in the daemon's mount namespace (ns-exec helper) Ephemeral-AI-Lab/layerfs#499'sns-execdoes, thensetsid()andPR_SET_PDEATHSIG(SIGKILL), re-checksgetppid, and execs the command.Started {pid, pgid},Exited {code, signal, group_alive},Status/StatusReply,Signal {signo}, and a thinStop {grace_ms}SIGTERM wrapper. A late caller receives the currentStarted/Exitedsnapshot under the same lock that registers it.Signal/Stopusekill(-pgid, signo)only for the child's own group; the supervisor's own group and arbitrary pids are refused. Peer admission is the supervisor's effective uid; further authorization is deferred.128 + signalwhen signalled, and reportsgroup_alive: true(viakill(-pgid, 0)) when grandchildren remain. Grandchildren are not killed in this slice.Architecture modules
The design note is organized in layers (module map, interfaces, startup/signal/exit sequences, failure modes and invariants, implemented vs deferred, test matrix): EXEC-SUPERVISOR-SKELETON.md.
setns/chdirs.setns,chdir, drops groups/gid/uid,setsid,PDEATHSIG, checks its parent andexecs the command.0600socket, four-byte length frames, peer-uid admission,Started/Exited/Status/Signal/Stop, and a snapshot for late callers under the same lock as registration.pid,pgid,started,exit, clients) behind one mutex; all state dies with the supervisor.The note also records gaps that the skeleton does not close:
starttimeis not captured (identity is only(pid, pgid)),Stopis implemented but untested, a session-poll timeout yieldsExitedwithoutStarted, andgroup_aliveis an observable-group probe rather than ownership.Presence + registry decisions
The six decisions the design note left open are now locked — launcher ownership, lease granularity, daemon-restart semantics, lease authentication, background/disconnect policy and cancellation states. Full text and rationale: Presence + registry decisions (locked).
SO_PEERCREDplus a daemon-issued per-session capability/token delivered over the authenticated control channel; never in argv, because same-uid processes can read/proc/<pid>/cmdline. Hostile same-Sandbox multi-tenancy additionally requires user/namespace isolation.mode: foreground|backgroundand a separateonDisconnect: cancel|keep|reconnectWindow. This phase ships background + cancel;reconnectWindowcomes later.detachOnDisconnectisforeground+keep.TERM→KILLcleanup time.Phase impact: P1 registry +
starttime; P2 presence + reaper; P3 reconnect/background; P4 Engine wiring.Deliberately deferred
close(force)integration;Tests
engine/crates/layerfs-daemon/tests/exec_supervisor.rs(Linux-gated; runs in the mount lane withCAP_SYS_ADMIN, likens_exec.rs). It builds a private mount namespace withunshare -m, serves its/proc/<pid>/ns/mntdescriptor overSCM_RIGHTS, and drives the real binary over the control socket:Startedhaspgid == pidand pid != supervisor, exit 765534:65534when told to drop theresleep 30+Signal(SIGTERM)→Exited {signal: 15}, supervisor exit 143Statusreports the running child;Signal(SIGKILL)→ signal 9, exit 137sh -c 'sleep 5 & sleep 1'→group_alive: trueinstead of a clean exitRun in a privileged
rust:1.85.1-bookwormcontainer againsta20f5ae81:cargo fmt --all -- --check— passcargo check -p layerfs-daemon --all-targets— passcargo clippy -p layerfs-daemon --all-targets -- -D warnings— passcargo test -p layerfs-daemon --test exec_supervisor --no-runthenexec_supervisor --test-threads=1— 6 passed, 0 failed; repeated 10 consecutive runs, all green.Docs
core/docs/issues/538/EXEC-SUPERVISOR-SKELETON.mdis the modular architecture note: module map, interfaces (SCM_RIGHTShandoff, control frames, process identity, exit/group_alivesemantics), startup/signal/exit sequences, failure modes and invariants, implemented-vs-deferred table and test matrix.Dependency
Depends on Ephemeral-AI-Lab#499 (
feat/ns-exec-helper,386f1b22). Merging this before Ephemeral-AI-Lab#499 would not compile because it builds on Ephemeral-AI-Lab#499'sns_fd/cliseam. No changes to Ephemeral-AI-Lab#499 or upstream.