This is the first executable leaf of #16168 under the v13.2 Fleet Manager containerization lane. The graduated source discussion (#16176) names the prerequisite directly: prepareManagedAgentWorkspace() currently mixes plan derivation with host filesystem effects, so the plan/apply boundary must exist before a container-owned envelope can be honest.
The current implementation is 1,989 lines and has exactly one production caller: startAgentProvisioned.mjs. Its present call chain is:
prepareManagedAgentWorkspace() currently combines two contracts:
deterministic workspace and MCP intent derivation; and
host-owned capability checks, path binding, hydration, filesystem convergence, permissions, and transport receipts.
That makes a future container boundary false by construction: the logical planner cannot be container-owned while it also reads and mutates host paths, and the host apply edge must not become an alternative policy owner for agent identity, targets, credentials, or authorization.
The file already contains the beginnings of the right seam—private createMcpPlan(), render functions, seat-config generators, deriveHarnessLaunchSpec(), deriveAgentInstanceHome(), and resolveMcpMatrix()—but they are still composed inside the effectful monolith.
Architectural Reality
The current effect surface includes:
stat / lstat / access capability and executable checks;
symlink, containment, and real-directory validation;
repo hydration;
bounded file reads;
create-only and atomic writes;
mkdir, rename, chmod 0600, and unlink;
artifact convergence and transport-receipt replacement/removal.
The current MCP plan carries no secret values—it carries public URLs and environment-slot names—but it also contains host-derived absolute paths such as commands, entrypoints, and working directories. Those bindings belong at the host edge, not in the container-owned logical plan.
Intended Solution
Split workspace preparation into two explicit contracts:
Pure logical planner — an exported, deterministic function that accepts immutable agent/transport intent and returns closed-schema harness, artifact, and MCP intent. It may contain opaque seat/target identifiers, public URLs, environment-slot names, and curated artifact intent. It performs no filesystem, environment, process, or global-config reads and contains neither secret values nor absolute host paths.
Host-owned apply edge — binds the opaque target to resolved repo/home/main-checkout/Node paths and performs the existing capability checks, hydration, reads, writes, convergence, permission enforcement, and receipts. It does not decide registry, credential, identity, or authorization policy.
Keep prepareManagedAgentWorkspace() as the compatibility composer—plan, then apply—during this migration. Preserve its public return shape, adapter behavior, local↔remote transitions, and fail-closed semantics.
This leaf creates the seam only. It does not add an RPC, container service, command ledger, or remote execution protocol.
#15048 plus the current startAgentProvisioned() consumer
Preserve the current option names and six-field async return {repoPath, instanceHome, mcpMatrix, mcpPlan, hydration, artifacts}; resolve the injected MCP-matrix seam, project the closed logical input, call createManagedAgentWorkspacePlan() exactly once, then call applyManagedAgentWorkspacePlan() exactly once
No legacy bypass: planner or apply rejection propagates as today; no launchable result is returned beside a failed plan/apply. Existing local/remote transition behavior stays unchanged
Existing function JSDoc is updated to name the composition and link both new exports
Existing preparation corpus remains green; a composition spy proves one planner call followed by one apply call and unchanged returned shape
createManagedAgentWorkspacePlan(input) (new pure export; exact schema below)
Validate a closed, path-free agent/MCP intent and return a recursively frozen logical plan containing only opaque agent id, curated harness/artifact profile, MCP enablement, relative entrypoint vocabulary, public remote URLs, and environment-slot names
Synchronous TypeError for missing, unknown, malformed, absolute-path, owner/authorization, or secret-value fields; synchronous RangeError for an unsupported harness/MCP/transport combination. No partial result
Full public JSDoc on the new export and its closed input/output typedefs; internal seam, so no separate learn guide
Equal-input/deep-equal witness; recursive immutability witness; forbidden-field table; module-load deny hook plus throwing fs/env/process/global-config seams prove zero reads/effects
applyManagedAgentWorkspacePlan(options) (new host-effect export; exact schema below)
#16176 / #16168 host-apply boundary plus the current convergence/safety corpus
Validate the logical plan, bind repo/home/main-checkout/Node absolute paths, prove adapter/capability/executable safety, hydrate, converge artifacts and receipts, and return the existing six-field prepared result
Reject with ManagedWorkspacePreparationError on invalid plan/binding, unsafe paths, unsupported capability, divergent owned content, or an effect failure. No rollback fiction: successfully completed hydration or atomic/create-only artifacts may remain as convergent partial state; retry must converge them, while no partial file bytes, secret value, or process spawn is permitted
Full public JSDoc names host-only authority, bounded effect set, return schema, and partial-state/retry semantics
Operation recorder proves the bounded effect set; negative controls retain no-unsafe-write behavior; injected mid-apply failure plus retry proves convergent partial state; existing artifact/receipt specs remain green
startAgentProvisioned(options) (existing sole production consumer)
Current production call chain
Continue preparing once, validate the canonical returned repoPath, inspect a prepared remote adapter when applicable, and call FleetLifecycleService.start() only after successful apply
Any planner/apply rejection propagates; explicit no-spawn assertion; already-running and no-metadata.repo behavior remains unchanged
Existing JSDoc updated only if the internal composition name changes
Existing ordering specs plus one explicit planner-fail and one apply-fail no-spawn witness
Generated harness artifacts and transport receipts
Current unit corpus and #16176 secret/path boundary
Preserve byte/projection ownership, create-only/atomic-write/0600 rules, local-to-remote transitions, receipt replacement/removal, and the existing public artifact-state vocabulary
Divergent resident-owned content still rejects; an interrupted operation never exposes partial bytes or secret material; retry reconverges completed atomic artifacts
Generator/convergence JSDoc remains authoritative; new apply JSDoc links it
agent is exactly {id: String, harnessType: String}; unknown keys are rejected.
mcpMatrix is a closed Boolean record whose keys are the canonical MCP_SERVERS keys; missing canonical keys resolve through the existing catalog policy before this call, and unknown keys are rejected.
mcpTarget is null or exactly {kind: 'tenant', credentialEnvVar: String, resources: {'memory-core': {url: String}, 'knowledge-base': {url: String}}}. It contains the credential slot name, never a credential value.
Caller-supplied owner, ownerPrincipal, authorization/grant fields, credential/bearer values, repoPath, instanceRoot, mainCheckout, nodePath, cwd, command, args, and any absolute path are forbidden at every depth.
Return is recursively frozen and exactly:
{agent: {id, harnessType}, artifactProfile: String, mcpMatrix: Record<String, Boolean>, mcpServers: Array<{key, name, enabled, target, transport, entrypoint, url, credentialEnvVar, runtimeEnv, requiredRuntimeEnv, secretEnv}>}.
entrypoint is a repository-relative curated entrypoint or null; url is a public remote resource URL or null; every array/object is immutable. No host path, secret value, owner, grant, or executable command is present.
plan must satisfy the exact output schema above; structural clones are accepted after validation so this seam does not depend on object identity.
repoPath, instanceRoot, mainCheckout, and nodePath are absolute host bindings introduced only here.
remoteMcpCapability is the existing non-secret installed-adapter proof. The remaining injectable seams retain the current test/host behavior and are not serialized into the plan.
Return remains exactly Promise<{repoPath: String, instanceHome: String, mcpMatrix: Object, mcpPlan: Object[], hydration: Object, artifacts: Object[]}>. The returned mcpPlan is the host-bound execution projection and may contain absolute paths; the logical input plan never does.
Acceptance Criteria
A mechanical caller/effect census is captured in JSDoc or a test-owned table. It identifies the single production caller and every current effect class listed above.
A pure logical planner is exported and tested under throwing filesystem, environment, process, and global-config seams. Equal inputs produce deeply equal, immutable outputs.
The planner emits a closed schema containing only opaque seat/target identity, public URLs, environment-slot names, and curated harness/artifact intent. It rejects caller-supplied owner, absolute-path, bearer-value, and authorization fields.
Absolute host paths are introduced only by the host-owned apply edge.
An operation recorder or equivalent spy proves that planning performs zero effects and that apply performs only the expected bounded effect set.
Existing adapter and local↔remote transition coverage remains green with unchanged artifact/receipt semantics.
Negative controls for symlink/path escape, divergent content, missing capability/executable, and unsafe existing state remain fail-closed before unsafe writes or process spawn.
startAgentProvisioned() invokes the compatibility seam once and calls FleetLifecycleService.start() only after successful apply; a failed apply has an explicit no-spawn assertion.
No RPC, MCP tool, container service, or command ledger is added in this leaf.
Defining transport RPCs or a durable command ledger.
Fleet credential issuance, identity governance, or authorization policy.
Reworking unrelated Fleet lifecycle or SDK boundaries.
Avoided Traps
Do not move the monolith behind an RPC and call that containerization.
Do not serialize absolute host paths or bearer values into the logical plan.
Do not let the host apply edge re-derive agent identity or target policy.
Do not weaken current create-only, atomic-write, 0600, symlink, or containment guarantees.
Do not break the one-call compatibility surface merely to expose the internal split.
Do not prescribe a plan digest in this leaf before the later transport contract establishes what must be signed or replayed.
Decision Record Impact
No ADR amendment is required. This implements the plan/apply separation already graduated in #16176; container/RPC authority remains with parent epic #16168 and its later leaves.
Context
This is the first executable leaf of #16168 under the v13.2 Fleet Manager containerization lane. The graduated source discussion (#16176) names the prerequisite directly:
prepareManagedAgentWorkspace()currently mixes plan derivation with host filesystem effects, so the plan/apply boundary must exist before a container-owned envelope can be honest.The current implementation is 1,989 lines and has exactly one production caller:
startAgentProvisioned.mjs. Its present call chain is:FleetManager.startAgent()→startAgentProvisioned()→ensureAgentRepo()→prepareManagedAgentWorkspace()→FleetLifecycleService.start()Problem
prepareManagedAgentWorkspace()currently combines two contracts:That makes a future container boundary false by construction: the logical planner cannot be container-owned while it also reads and mutates host paths, and the host apply edge must not become an alternative policy owner for agent identity, targets, credentials, or authorization.
The file already contains the beginnings of the right seam—private
createMcpPlan(), render functions, seat-config generators,deriveHarnessLaunchSpec(),deriveAgentInstanceHome(), andresolveMcpMatrix()—but they are still composed inside the effectful monolith.Architectural Reality
The current effect surface includes:
stat/lstat/accesscapability and executable checks;mkdir,rename,chmod 0600, andunlink;The current MCP plan carries no secret values—it carries public URLs and environment-slot names—but it also contains host-derived absolute paths such as commands, entrypoints, and working directories. Those bindings belong at the host edge, not in the container-owned logical plan.
Intended Solution
Split workspace preparation into two explicit contracts:
Keep
prepareManagedAgentWorkspace()as the compatibility composer—plan, then apply—during this migration. Preserve its public return shape, adapter behavior, local↔remote transitions, and fail-closed semantics.This leaf creates the seam only. It does not add an RPC, container service, command ledger, or remote execution protocol.
Contract Ledger
prepareManagedAgentWorkspace(options)(existing compatibility export)startAgentProvisioned()consumer{repoPath, instanceHome, mcpMatrix, mcpPlan, hydration, artifacts}; resolve the injected MCP-matrix seam, project the closed logical input, callcreateManagedAgentWorkspacePlan()exactly once, then callapplyManagedAgentWorkspacePlan()exactly oncecreateManagedAgentWorkspacePlan(input)(new pure export; exact schema below)TypeErrorfor missing, unknown, malformed, absolute-path, owner/authorization, or secret-value fields; synchronousRangeErrorfor an unsupported harness/MCP/transport combination. No partial resultapplyManagedAgentWorkspacePlan(options)(new host-effect export; exact schema below)ManagedWorkspacePreparationErroron invalid plan/binding, unsafe paths, unsupported capability, divergent owned content, or an effect failure. No rollback fiction: successfully completed hydration or atomic/create-only artifacts may remain as convergent partial state; retry must converge them, while no partial file bytes, secret value, or process spawn is permittedstartAgentProvisioned(options)(existing sole production consumer)repoPath, inspect a prepared remote adapter when applicable, and callFleetLifecycleService.start()only after successful applymetadata.repobehavior remains unchanged0600rules, local-to-remote transitions, receipt replacement/removal, and the existing public artifact-state vocabularyNew exported contract schemas
createManagedAgentWorkspacePlan({agent, mcpMatrix, mcpTarget = null} = {})agentis exactly{id: String, harnessType: String}; unknown keys are rejected.mcpMatrixis a closed Boolean record whose keys are the canonicalMCP_SERVERSkeys; missing canonical keys resolve through the existing catalog policy before this call, and unknown keys are rejected.mcpTargetisnullor exactly{kind: 'tenant', credentialEnvVar: String, resources: {'memory-core': {url: String}, 'knowledge-base': {url: String}}}. It contains the credential slot name, never a credential value.owner,ownerPrincipal, authorization/grant fields,credential/bearervalues,repoPath,instanceRoot,mainCheckout,nodePath,cwd,command,args, and any absolute path are forbidden at every depth.{agent: {id, harnessType}, artifactProfile: String, mcpMatrix: Record<String, Boolean>, mcpServers: Array<{key, name, enabled, target, transport, entrypoint, url, credentialEnvVar, runtimeEnv, requiredRuntimeEnv, secretEnv}>}.entrypointis a repository-relative curated entrypoint ornull;urlis a public remote resource URL ornull; every array/object is immutable. No host path, secret value, owner, grant, or executable command is present.applyManagedAgentWorkspacePlan({plan, repoPath, instanceRoot, mainCheckout = DEFAULT_MAIN_CHECKOUT, nodePath = process.execPath, remoteMcpCapability = null, hydrateWorkspace = hydrateCurrentWorktree, deriveInstanceHome = deriveAgentInstanceHome, fileSystem = fs, log = () => {}} = {})planmust satisfy the exact output schema above; structural clones are accepted after validation so this seam does not depend on object identity.repoPath,instanceRoot,mainCheckout, andnodePathare absolute host bindings introduced only here.remoteMcpCapabilityis the existing non-secret installed-adapter proof. The remaining injectable seams retain the current test/host behavior and are not serialized into the plan.Promise<{repoPath: String, instanceHome: String, mcpMatrix: Object, mcpPlan: Object[], hydration: Object, artifacts: Object[]}>. The returnedmcpPlanis the host-bound execution projection and may contain absolute paths; the logical input plan never does.Acceptance Criteria
startAgentProvisioned()invokes the compatibility seam once and callsFleetLifecycleService.start()only after successful apply; a failed apply has an explicit no-spawn assertion.Out of Scope
Avoided Traps
0600, symlink, or containment guarantees.Decision Record Impact
No ADR amendment is required. This implements the plan/apply separation already graduated in #16176; container/RPC authority remains with parent epic #16168 and its later leaves.
Related
Creation-Gate Record
ai/services/fleet;prepareManagedAgentWorkspace.mjsand its sole production caller are sibling-owned there.Origin Session ID
abdf06f7-5c90-4124-ad28-f0e2897214eeRetrieval Hint
Query:
Fleet prepareManagedAgentWorkspace pure plan host apply containerization filesystem effects #16168