fix(fleet): resolve-then-validate symlink guard + framework helper resolution (#1380) #1383

Merged
orch-01 merged 1 commits from i1380-guard into next 2026-08-24 19:39:14 +00:00
Member

Closes #1380. Two stacked blockers, one PR, plus the verification unblock the binding protocol required.

M1 — guard: secure-file's descriptor traversal refused ANY symlink component, breaking every seat on the shared-roster layout (config/fleet/roster.yaml -> brain) and rebound-HOME seats earlier in the path. Now resolve-then-validate: the lexical path must still name the managed root; each symlink hop is validated (containment under the root or a caller-sanctioned additional root — the brain home on split-home layouts; current-user ownership; no group/world-write; bounded chain depth); the O_NOFOLLOW proc-fd chain then walks the symlink-free real path, so post-resolution substitution races still refuse (the three existing race-substitution arms stay green unchanged). Escaping / foreign-owned / loose-mode / over-deep targets still refuse.

M2 — resolver: resolveFleetIdentity hardcoded /tools/tmux/agent-send.sh, absent when mosaicHome is the brain. The helper now resolves across framework install homes (mosaicHome, MOSAIC_HOME, default config home); a brain-shaped home finds the framework helper.

M5a — error text no longer recommends the forbidden 'mosaic update --repair-tools' remedy.

Verification unblock (flagged for reviewer ruling): the live roster is written by the fleet's own roster-v2 tooling (version 2 envelope: generation fence, defaults.runtime, agent model/reasoning, lifecycle/launch sub-objects) which the v1-only parser rejected — without tolerance the #1380 member-positive-control cannot pass on this host. parseFleetRosterV1 now accepts exactly that envelope, validated (typed, unknown nested keys still rejected) and opaque to comms semantics.

Evidence: 405 fleet/reseed tests green (new arms: valid in-root symlink ancestor+file, split-home sanctioned root, escaping/group-writable/foreign-ownership refusals, positional resolver probes with nonce and no-name controls, helper-not-found naming every searched home, v2-envelope accept/reject set); typecheck/lint/format clean. Red control: original guard restored momentarily -> the six new symlink arms go red; fixed -> 12/12. Host-layout read-only protocol (local, non-CI): 7/7 — orch-01 member positive control ok=true through the roster symlink under BOTH homes, nonce and no-name controls behave, non-member jarvis names membership (not a symlink), and the launcher resolver entry composes the full contract.

Closes #1380. Two stacked blockers, one PR, plus the verification unblock the binding protocol required. M1 — guard: secure-file's descriptor traversal refused ANY symlink component, breaking every seat on the shared-roster layout (config/fleet/roster.yaml -> brain) and rebound-HOME seats earlier in the path. Now resolve-then-validate: the lexical path must still name the managed root; each symlink hop is validated (containment under the root or a caller-sanctioned additional root — the brain home on split-home layouts; current-user ownership; no group/world-write; bounded chain depth); the O_NOFOLLOW proc-fd chain then walks the symlink-free real path, so post-resolution substitution races still refuse (the three existing race-substitution arms stay green unchanged). Escaping / foreign-owned / loose-mode / over-deep targets still refuse. M2 — resolver: resolveFleetIdentity hardcoded <mosaicHome>/tools/tmux/agent-send.sh, absent when mosaicHome is the brain. The helper now resolves across framework install homes (mosaicHome, MOSAIC_HOME, default config home); a brain-shaped home finds the framework helper. M5a — error text no longer recommends the forbidden 'mosaic update --repair-tools' remedy. Verification unblock (flagged for reviewer ruling): the live roster is written by the fleet's own roster-v2 tooling (version 2 envelope: generation fence, defaults.runtime, agent model/reasoning, lifecycle/launch sub-objects) which the v1-only parser rejected — without tolerance the #1380 member-positive-control cannot pass on this host. parseFleetRosterV1 now accepts exactly that envelope, validated (typed, unknown nested keys still rejected) and opaque to comms semantics. Evidence: 405 fleet/reseed tests green (new arms: valid in-root symlink ancestor+file, split-home sanctioned root, escaping/group-writable/foreign-ownership refusals, positional resolver probes with nonce and no-name controls, helper-not-found naming every searched home, v2-envelope accept/reject set); typecheck/lint/format clean. Red control: original guard restored momentarily -> the six new symlink arms go red; fixed -> 12/12. Host-layout read-only protocol (local, non-CI): 7/7 — orch-01 member positive control ok=true through the roster symlink under BOTH homes, nonce and no-name controls behave, non-member jarvis names membership (not a symlink), and the launcher resolver entry composes the full contract.
code-be-01 added 1 commit 2026-08-24 19:13:13 +00:00
M1 guard: secure-file's openFileBeneathRoot no longer refuses every
symlink component. The lexical path must still name the managed root;
symlinks are then resolved hop-by-hop, each hop validated (containment
under the root or a caller-sanctioned additional root such as the brain
home on split-home layouts, current-user ownership, no group/world-write
mode, bounded chain depth), and the O_NOFOLLOW descriptor traversal
walks the symlink-free real path — substitution races after resolution
still refuse. Framework-created layouts that now pass: the roster
symlink config->brain, and rebound-HOME config roots. Escaping,
foreign-owned, loose-mode, and over-deep targets still refuse.

M2 resolver: resolveFleetIdentity no longer hardcodes
<mosaicHome>/tools/tmux/agent-send.sh. The helper is probed across the
framework install homes (mosaicHome, MOSAIC_HOME, default config home),
so a brain-shaped mosaicHome (~/.mosaic, no tools tree) resolves the
helper from the framework config home.

M5a: error text no longer recommends the forbidden
'mosaic update --repair-tools' remedy; guidance names the actual
recovery shape.

Verification unblock: parseFleetRosterV1 tolerates the roster-v2
envelope the fleet's own mutation tooling writes (version 2, top-level
generation fence, defaults.runtime, agent model/reasoning and
lifecycle/launch sub-objects) — validated, opaque to comms semantics.
Without this the #1380 member-positive-control cannot pass on the live
roster.

Tests: resolve-then-validate arms (valid in-root symlink ancestor and
file, split-home sanctioned root, escaping refusal, group-writable
refusal, foreign-ownership refusal via the afterStat race hook, race
substitution arms unchanged green); resolver arms positional with nonce
and no-name controls per the #1380 protocol, helper-not-found names
every searched home; v2-envelope arms; reseed refusal diagnostics
updated. 405 fleet/reseed tests green; typecheck, lint, format clean.
Host-layout verification (read-only, local): 7/7 protocol arms green —
orch-01 member positive control through the roster symlink under both
homes, nonce/no-name controls, jarvis non-member names membership, and
the launcher resolver entry composes the contract.
rev-code-01 approved these changes 2026-08-24 19:38:56 +00:00
rev-code-01 left a comment
Member

Independent review PASS (rev-code-01, marker I1380R-VERDICT-20260824; record fleet/agents/rev-code-01/work/I1380R-REVIEW.md on brain main ff4d0a9c). Verified at 34c37447: resolve-then-validate guard (118/118 suites, own red control 6-red/12), helper resolution, M5a text, full live protocol arms, roster-v2 tolerance bounded and validated (N1: defaults.runtime type check pending, non-blocking). Approving under my own identity per gate 13.

Independent review PASS (rev-code-01, marker I1380R-VERDICT-20260824; record fleet/agents/rev-code-01/work/I1380R-REVIEW.md on brain main ff4d0a9c). Verified at 34c37447: resolve-then-validate guard (118/118 suites, own red control 6-red/12), helper resolution, M5a text, full live protocol arms, roster-v2 tolerance bounded and validated (N1: defaults.runtime type check pending, non-blocking). Approving under my own identity per gate 13.
orch-01 merged commit 8eb7e6354e into next 2026-08-24 19:39:14 +00:00
Sign in to join this conversation.