LearnNewsExamplesServices
Frontmatter
id13145
titleFleet Manager: stable, traversal-safe per-agent repo-path derivation
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-ada
createdAtJun 14, 2026, 12:49 AM
updatedAtJun 14, 2026, 1:28 AM
githubUrlhttps://github.com/neomjs/neo/issues/13145
authorneo-opus-ada
commentsCount0
parentIssue13015
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 14, 2026, 1:28 AM

Fleet Manager: stable, traversal-safe per-agent repo-path derivation

Closed v13.1.0/archive-v13-1-0-chunk-2 enhancementaiarchitecture
neo-opus-ada
neo-opus-ada commented on Jun 14, 2026, 12:49 AM

Context

Sub-leaf of #13015 (Fleet Manager MVP) — the repo-provisioning surface the epic names as a future, in-scope leaf: "per-agent clone/worktree management under the hood (create, locate, health-check; path stability honored because downstream memory is path-keyed)." The MVP operator loop is define agents → start/stop → repos managed under the hood. The registry (#13031), lifecycle (#13049), settings pane (#13058), and bridge-token (#13065 / #13121) leaves have all landed — but nothing yet derives where an agent's managed checkout lives.

The operator's recorded reality (2026-06-10→12, captured at the epic): operating the swarm means "one repo clone per peer (auto-memory is checkout-path-keyed, so paths are load-bearing)." That single clause makes the path-derivation a correctness boundary, not a convenience: if an agent's checkout path is not stable across restarts, its path-keyed auto-memory silently forks; if two distinct agents can ever collide onto one path, their memory cross-contaminates.

The Problem

Repo provisioning is two concerns welded together: (1) the pure decision of where an agent's checkout for a given repo belongs (deterministic path math), and (2) the side-effectful act of cloning / locating / health-checking it (git + fs). Building them as one service couples a security-and-correctness-critical pure rule to untestable I/O. The pure rule is the load-bearing half and deserves to be isolated, exhaustively unit-tested, and merged first — the same foundation-first decomposition the Extended-NL enforcement used (#13126 LockRegistry pure → #13135 WriteGuard stateful → wiring).

The pure rule also inherits a security obligation. FleetRegistryService already treats agent ids as untrusted keys — null-prototype credential maps + Object.hasOwn lookups, so an id like __proto__ / toString fails closed rather than aliasing a prototype member (FleetRegistryService.mjs:328, :243). An agent id defaults to the GitHub username but can be an arbitrary explicit string (defineAgent({id})"pass an explicit id to register multiple instances per user", FleetRegistryService.mjs:112). The moment that untrusted id is interpolated into a filesystem path, the untrusted-key posture must extend to path-traversal safety: an id like ../../etc or a/b must never escape the managed root.

The Architectural Reality

  • Lands at ai/services/fleet/deriveAgentRepoPath.mjs (Brain side, ai/ — the epic's stated home for fleet machinery; co-located with FleetRegistryService / FleetLifecycleService). A plain exported pure function, no Neo class machinery — the established shape for pure helpers (ai/services/neural-link/resolveCallTarget.mjs, src/ai/deriveSubtreePath.mjs).
  • No hidden defaults (the config-is-SSOT contract): managedRoot is a required argument, resolved by the future side-effectful caller from config — the pure function never derives, defaults, or reads a root from env/fs. It is pure string→string path math (Node path only).
  • The side-effectful repo-provisioning service (a later leaf) consumes this to clone / locate / health-check; "the settings pane must not bundle it" (#13015) — this leaf is the first, foundational half.
  • Not blocked on #12984 (session-id canonicalization): the path keys on (agentId, repoSlug), never on a session id, so it sidesteps the multi-universe session-id blocker that gates the identity-env / wake-subscription provisioning leaves.

The Fix

export function deriveAgentRepoPath({managedRoot, agentId, repoSlug}) → an absolute, stable, collision-free, traversal-contained checkout path of shape <managedRoot>/<agentSegment>/<repoSegment>.

Each path segment is safeSegment(raw) = a sanitized, length-bounded, readable prefix (unsafe chars → -; dot-runs collapsed; leading/trailing dots + dashes trimmed — so . / .. can never be a bare segment) plus a deterministic sha256(raw).slice(0,12) suffix. The hash suffix is load-bearing twice over: it guarantees stability (deterministic in the raw id) AND collision-freeness even when sanitization is lossy (two distinct raw ids that sanitize identically still diverge by hash). A final containment assertion (path.resolve result must equal or sit under path.resolve(managedRoot)) is defense-in-depth over the sanitizer — and throws loudly if ever violated, because a silently-wrong path would clone into the wrong place.

Contract: throws on invalid trusted-caller input (non-string / empty managedRoot / agentId / repoSlug; non-absolute managedRoot); safely contains (never throws on) traversal-bearing or unsafe-char ids — producing a safe path is the function's job, not rejecting the id.

Acceptance Criteria

  • ai/services/fleet/deriveAgentRepoPath.mjs exports a pure deriveAgentRepoPath({managedRoot, agentId, repoSlug}); no fs / env / config / Neo access.
  • Stable: identical inputs always return the identical path (idempotent across calls).
  • Collision-free: distinct agentIds (and distinct repoSlugs) yield distinct paths — including two ids that sanitize to the same readable form (hash-disambiguated).
  • Traversal-contained: agentId / repoSlug values such as ../../etc/passwd, .., /abs, a/b/../.., __proto__ never resolve outside managedRoot.
  • Readable: the agent segment contains the sanitized human-readable form so the operator can navigate the managed tree.
  • Fail-loud on contract violation: empty / non-string managedRoot / agentId / repoSlug, and a non-absolute managedRoot, throw.
  • Unit spec test/playwright/unit/ai/deriveAgentRepoPath.spec.mjs covers all of the above and passes under the custom unit config.

Out of Scope

  • The side-effectful clone / worktree creation / health-check (a later repo-provisioning leaf consuming this).
  • managedRoot resolution / config wiring (the consuming service's concern; never defaulted here).
  • Identity-env + wake-subscription provisioning (separate leaves; carry the #12984 blocker).
  • Worktree-vs-clone policy selection — this derives a path; the strategy is the caller's.

Avoided Traps

  • Pure-hash path (no readable segment) — collision-free but the operator cannot navigate <root>/a1b2…/c3d4…; rejected for a sanitize + hash hybrid that stays human-navigable.
  • Sanitize-only (no hash) — readable but lossy: a/b and a-b collide → memory cross-contamination; rejected.
  • Default the managed root inside the function — violates the config-is-SSOT no-hidden-defaults contract; managedRoot is required and caller-resolved.
  • Reject traversal ids by throwing — turns a safety property into a liveness footgun (a legitimately-odd explicit id would break provisioning); the function sanitizes-and-contains instead.

Decision Record impact

none — no ADR governs fleet path-derivation; this introduces a new pure helper consistent with existing fleet-service conventions.

Related

  • Parent: #13015 (Fleet Manager MVP); grandparent #13012.
  • Sibling leaves: #13031 (registry / credentials), #13049 (lifecycle), #13058 (settings pane), #13065 / #13121 (bridge-token).
  • Pattern precedent: #13126 (LockRegistry pure module), #13140 (deriveSubtreePath), #13130 (resolveCallTarget) — same pure-module + flattened-spec shape.

Live latest-open sweep: checked latest 20 open issues + a fleet / provision / clone / worktree / checkout scan at 2026-06-13T22:47Z; no equivalent found.

Release classification: post-release (Fleet Manager is a product line; nothing v13.x-blocking).

Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e

Handoff Retrieval Hint: "Fleet Manager per-agent repo path derivation traversal-safe stable"; commit anchor — the deriveAgentRepoPath.mjs introduction.