diff --git a/.agentworkforce/features/manifest.yaml b/.agentworkforce/features/manifest.yaml index f95f9a189..190c3aed0 100644 --- a/.agentworkforce/features/manifest.yaml +++ b/.agentworkforce/features/manifest.yaml @@ -724,6 +724,13 @@ categories: location: packages/cli/src/cli/commands/fleet.ts verify_tier: 5 + - id: fleet-spawn-status + name: Read Fleet Spawn Dispatch Status + cli: relay fleet spawn-status + description: Read a recorded fleet spawn dispatch without spawning again, separating confirmed readiness, terminal failure, and an outcome the node has not reported yet + location: packages/cli/src/cli/commands/fleet.ts + verify_tier: 5 + - id: fleet-release name: Release Fleet Agent cli: relay fleet release [--reason ] [--delete-agent] diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf1fa6ed..d2fe8b292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `agent-relay mcp` server adds `upload_file` (local path or base64) and `download_file`, and `send_group_dm` accepts `attachments`. - Agents spawned by the broker receive message attachments (up to 25 MiB each) as local files: the injected message ends with an `Attachments:` list giving each file's path under `.agent-relay/attachments/` in the agent's working directory, or an `agent-relay message file download ` command when a file could not be fetched. Messages that carry only attachments are now delivered too. - `@agent-relay/sdk` adds `files.upload` / `files.get` / `files.download` on `AgentRelay` and `RelaycastMessagingClient`, plus `uploadRelayFile` / `downloadRelayFile` helpers. +- `agent-relay fleet spawn-status ` reads a recorded spawn dispatch without spawning again, separating confirmed readiness, a node-reported failure, and an outcome the node has not reported yet. ### Changed @@ -40,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A confirmed `fleet spawn --task` (the default) succeeds only once the agent's harness has visibly accepted the task (activity or a cleared composer, not merely the task echoed into the composer). If the task cannot be delivered, the worker is released and the spawn fails with `spawn_task_failed`. If delivery cannot be confirmed, the spawn fails with `spawn_task_unconfirmed`, names the agent that is still running, and says not to retry. With `--no-confirm` the spawn reports as soon as the node accepts it, without waiting for the task to arrive. - `agent-relay message file upload` now uploads and attaches the file; it always failed with "Invalid attachments: file ids must exist in workspace and be complete". - Broker-managed messages retry failed delivery up to three times, and later sends wait until delivery succeeds. +- `fleet spawn` (and SDK placement confirmation) now waits six minutes for launch confirmation instead of two, so agents that register minutes after dispatch are confirmed rather than reported as unconfirmed; the timeout error names the `fleet spawn-status` poll for its invocation id instead of suggesting a redispatch. ## [13.2.0] - 2026-10-08 diff --git a/packages/cli/README.md b/packages/cli/README.md index 3b2800434..5fe528630 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -317,6 +317,23 @@ set `RELAY_AGENT_TOKEN` to the token returned by login (`agent-relay cloud login`) but does not need an agent token: when one is absent, it creates and removes a short-lived launcher identity automatically. +A targeted spawn waits for the node to confirm the launch, for six minutes by +default (`--confirm-timeout `, minimum 95000). Agents that take minutes to +register are confirmed inside that window rather than reported as unconfirmed. +If the window does expire, the error carries the invocation id: read that +dispatch instead of spawning again, because a retry under the same name +collides with a worker that may still be starting. + +```bash +agent-relay fleet spawn-status inv_01J... +``` + +The reply's `placement.state` separates the outcomes: `ready` (launched and +confirmed), `failed` (the node reported a terminal failure), `accepted` +(launched, readiness unverified), and `unconfirmed_may_be_running` (the node +still has not reported — not evidence that nothing started). Polling never +dispatches another agent. + Without placement options, `fleet spawn` connects to the local project's broker and passes the caller's exact directory, including a nested package, to the worker. Start the local broker with `agent-relay node up` if it is not running; diff --git a/packages/cli/src/cli/bootstrap.test.ts b/packages/cli/src/cli/bootstrap.test.ts index 325f501d9..088c8d0a3 100644 --- a/packages/cli/src/cli/bootstrap.test.ts +++ b/packages/cli/src/cli/bootstrap.test.ts @@ -46,6 +46,7 @@ const expectedLeafCommands = [ 'fleet nodes list', 'fleet release', 'fleet spawn', + 'fleet spawn-status', 'fleet status', // cloud 'cloud login', diff --git a/packages/cli/src/cli/commands/fleet.test.ts b/packages/cli/src/cli/commands/fleet.test.ts index 493f9e5fd..dbeb4ed71 100644 --- a/packages/cli/src/cli/commands/fleet.test.ts +++ b/packages/cli/src/cli/commands/fleet.test.ts @@ -892,7 +892,7 @@ describe('fleet command support', () => { // #1430: acceptance by the node is not evidence of a launch, so a // targeted spawn asks the node to confirm unless told not to. confirm: true, - confirmTimeoutMs: 120_000, + confirmTimeoutMs: 360_000, input: { name: 'api-worker', cli: 'codex', @@ -5767,3 +5767,119 @@ describe('fleet command support', () => { expect(errors.join('\n')).toMatch(/relay cloud enroll/); }); }); + +describe('fleet spawn-status', () => { + it.each([ + ['invoked', undefined, 'unconfirmed_may_be_running'], + ['completed', { spawned: true, ready: true }, 'ready'], + ['failed', undefined, 'failed'], + ['completed', { spawned: true, ready: false }, 'accepted'], + ])('reports %s without redispatching or exposing raw output', async (status, output, state) => { + const getInvocation = vi.fn(async () => ({ + invocationId: 'inv_late', + status, + output: { ...output, secret: 'raw-secret' }, + error: 'raw-secret', + input: { task: 'private-task' }, + })); + const spawn = vi.fn(); + const logs: string[] = []; + const program = new Command(); + program.exitOverride(); + registerFleetCommands(program, { + sdk: { + createAgentRelay: vi.fn(() => ({ + messaging: { commands: { getInvocation }, placement: { spawn } }, + })) as never, + log: (message: unknown) => logs.push(String(message)), + error: vi.fn(), + exit: vi.fn() as never, + }, + }); + await program.parseAsync(['fleet', 'spawn-status', 'inv_late', '--token', 'at_test'], { from: 'user' }); + expect(getInvocation).toHaveBeenCalledWith('spawn', 'inv_late'); + expect(spawn).not.toHaveBeenCalled(); + expect(JSON.parse(logs[0]!).invocation.placement).toMatchObject({ invocationId: 'inv_late', state }); + expect(logs.join('')).not.toContain('raw-secret'); + expect(logs.join('')).not.toContain('private-task'); + }); + + // #1430 follow-up: the reported failure was a caller unable to tell a worker + // that launched late from one that never started. The state alone is not that + // evidence — the launch proof and the node that answered have to survive the + // read, and a silent dispatch must not acquire a launch claim it never made. + it.each([ + [ + 'a node that has not reported yet', + { invocationId: 'inv_late', status: 'invoked', dispatched_node_id: 'node_sf_mini' }, + { state: 'unconfirmed_may_be_running', dispatchState: 'dispatched', dispatchedNodeId: 'node_sf_mini' }, + undefined, + ], + [ + 'a launch confirmed after the spawn gave up', + { + invocationId: 'inv_late', + status: 'completed', + handler_node_id: 'node_sf_mini', + output: { spawned: true, ready: true }, + }, + { state: 'ready', dispatchState: 'dispatched', handlerNodeId: 'node_sf_mini' }, + { spawned: true, ready: true }, + ], + ])('keeps the launch evidence for %s', async (_label, invocation, placement, output) => { + const logs: string[] = []; + const program = new Command(); + program.exitOverride(); + registerFleetCommands(program, { + sdk: { + createAgentRelay: vi.fn(() => ({ + messaging: { commands: { getInvocation: vi.fn(async () => invocation) } }, + })) as never, + log: (message: unknown) => logs.push(String(message)), + error: vi.fn(), + exit: vi.fn() as never, + }, + }); + await program.parseAsync(['fleet', 'spawn-status', 'inv_late', '--token', 'at_test'], { from: 'user' }); + + const reported = JSON.parse(logs[0]!).invocation; + expect(reported.placement).toMatchObject(placement); + expect(reported.output).toEqual(output); + }); + + it('mints and releases a temporary reader for spawn-status with workspace credentials', async () => { + vi.stubEnv('RELAY_AGENT_TOKEN', ''); + vi.stubEnv('RELAY_BASE_URL', 'https://relay.isolated.test'); + const register = vi.fn(async () => ({ token: 'at_reader' })); + const release = vi.fn(async () => undefined); + const getInvocation = vi.fn(async () => ({ invocationId: 'inv_late', status: 'failed' })); + const createAgentRelay = vi.fn(() => ({ messaging: { commands: { getInvocation } } })); + const program = new Command(); + program.exitOverride(); + registerFleetCommands(program, { + sdk: { + createWorkspaceRelay: vi.fn(() => ({ workspace: { register, release } })) as never, + createAgentRelay: createAgentRelay as never, + log: vi.fn(), + error: vi.fn(), + exit: vi.fn() as never, + }, + }); + await program.parseAsync(['fleet', 'spawn-status', 'inv_late', '--workspace-key', 'rk_test'], { + from: 'user', + }); + expect(register).toHaveBeenCalledWith( + expect.objectContaining({ metadata: { purpose: 'fleet-spawn-status' } }), + { strict: true } + ); + // The minted token reads alone, from the gateway that minted it: passing + // the workspace key too is rejected by createAgentRelay. + expect(createAgentRelay).toHaveBeenCalledWith({ + token: 'at_reader', + baseUrl: 'https://relay.isolated.test', + }); + expect(release).toHaveBeenCalledWith( + expect.objectContaining({ name: register.mock.calls[0]![0].name, deleteAgent: true }) + ); + }); +}); diff --git a/packages/cli/src/cli/commands/fleet.ts b/packages/cli/src/cli/commands/fleet.ts index a0b6078e3..be876b65f 100644 --- a/packages/cli/src/cli/commands/fleet.ts +++ b/packages/cli/src/cli/commands/fleet.ts @@ -58,7 +58,7 @@ import { redactSecrets } from '../lib/redact.js'; import { attributableReleaseReason } from '../lib/release-reason.js'; import { retireOwnedIntegrationBindings } from './integration.js'; import { resolveSandboxRepository, type SandboxRepositorySelection } from '../lib/sandbox-repo.js'; -import { spawnPlacementReceipt } from '../lib/spawn-lifecycle.js'; +import { sanitizedSpawnReceipt, spawnPlacementReceipt } from '../lib/spawn-lifecycle.js'; import { resolveAgentToken, resolveWorkspaceSelection, @@ -451,6 +451,66 @@ export function registerFleetCommands( await runFleetAgentList(deps, options); }); + addSdkOptions( + group + .command('spawn-status') + .description('Read an existing spawn dispatch without launching another agent') + .argument('', 'Invocation ID returned by fleet spawn') + ).action(async (invocationId: string, options: Record) => { + await runSdk(deps.sdk, async () => { + const clientOptions = sdkOptionsFromOpts(options); + let token = resolveAgentToken(clientOptions); + let launcherName: string | undefined; + let workspace: ReturnType | undefined; + try { + if (!token) { + workspace = deps.sdk.createWorkspaceRelay(clientOptions); + const name = `fleet-status-launcher-${randomUUID().slice(0, 8)}`; + const launcher = await workspace.workspace.register( + { name, metadata: { purpose: 'fleet-spawn-status' } }, + { strict: true } + ); + launcherName = name; + token = launcher.token; + if (!token) throw new Error('The temporary fleet status launcher did not receive an agent token.'); + } + // A minted launcher token must read from the gateway that minted it, and + // never alongside the workspace key that minted it (createAgentRelay + // rejects that pair), matching the spawn path's minted-token client. + const relay = deps.sdk.createAgentRelay( + launcherName + ? { token, baseUrl: resolveWorkspaceTransport(clientOptions).baseUrl } + : { ...clientOptions, token } + ); + const invocation = await relay.messaging.commands.getInvocation('spawn', invocationId); + // Keep this read-only surface on the existing safe receipt projection. + const receipt = sanitizedSpawnReceipt(invocation as unknown as Record); + const result = spawnInvocationWithPlacement(receipt); + const output = receipt.output as Record | undefined; + // --no-confirm dispatches may legitimately report launch without readiness. + // Preserve that evidence instead of claiming the launched worker failed. + if ( + ['completed', 'success', 'succeeded'].includes(String(receipt.status).toLowerCase()) && + output?.spawned === true && + output.ready === false + ) { + result.placement = { ...spawnPlacementReceipt(receipt), state: 'accepted' }; + } + printJson(deps.sdk, { invocation: result }); + } finally { + if (launcherName && workspace) { + await workspace.workspace + .release({ + name: launcherName, + reason: 'Temporary fleet spawn status launcher completed', + deleteAgent: true, + }) + .catch((error) => deps.warn(redactSecrets(String(error)))); + } + } + }); + }); + addSdkOptions( group .command('spawn') @@ -505,7 +565,7 @@ export function registerFleetCommands( .option( '--confirm-timeout ', `How long a targeted spawn waits for harness readiness (minimum ${MIN_VERIFIED_CONFIRM_TIMEOUT_MS}ms)`, - '120000' + '360000' ) ).action(async (cli: string, options: Record) => { await runSdk(deps.sdk, async () => { @@ -592,7 +652,7 @@ export function registerFleetCommands( { organization, project, workstream, role, objective }, task ); - const confirmTimeoutText = optionalText(options.confirmTimeout, 'Confirm timeout') ?? '120000'; + const confirmTimeoutText = optionalText(options.confirmTimeout, 'Confirm timeout') ?? '360000'; const confirmTimeoutMs = Number(confirmTimeoutText); if (!Number.isFinite(confirmTimeoutMs) || confirmTimeoutMs <= 0) { throw new Error('--confirm-timeout must be a positive number of milliseconds.'); diff --git a/packages/cli/src/cli/lib/fleet-spawn-confirmation.test.ts b/packages/cli/src/cli/lib/fleet-spawn-confirmation.test.ts index ea9f0a0b8..db083833d 100644 --- a/packages/cli/src/cli/lib/fleet-spawn-confirmation.test.ts +++ b/packages/cli/src/cli/lib/fleet-spawn-confirmation.test.ts @@ -524,3 +524,39 @@ describe('targeted spawn readiness contract', () => { } }); }); + +it('confirms a five-minute launch using the default budget without dispatching again', async () => { + vi.useFakeTimers(); + try { + const started = Date.now(); + const { client, invoke } = createClient(async (name, invocationId) => ({ + invocation_id: invocationId, + action_name: name, + status: Date.now() - started >= 300_000 ? 'completed' : 'invoked', + output: { spawned: true, ready: true }, + })); + const pending = client.placement.spawn(spawnInput({ confirm: true })); + await vi.advanceTimersByTimeAsync(300_500); + expect((await pending).placement.confirmed).toBe(true); + expect(invoke).toHaveBeenCalledTimes(1); + } finally { + vi.useRealTimers(); + } +}); + +it('returns a pollable invocation ID when the extended default budget expires', async () => { + vi.useFakeTimers(); + try { + const { client, invoke } = createClient(async () => ({ status: 'invoked' })); + const pending = client.placement.spawn(spawnInput({ confirm: true })).catch((error) => error); + await vi.advanceTimersByTimeAsync(360_500); + const error = await pending; + expect(error.code).toBe('spawn_unconfirmed'); + expect(error.state).toBe('unconfirmed_may_be_running'); + expect(error.invocationId).toBe('inv-1430'); + expect(error.message).toContain('fleet spawn-status inv-1430'); + expect(invoke).toHaveBeenCalledTimes(1); + } finally { + vi.useRealTimers(); + } +}); diff --git a/packages/sdk/src/__tests__/relaycast-translate.test.ts b/packages/sdk/src/__tests__/relaycast-translate.test.ts index f6d888ce0..4e118897c 100644 --- a/packages/sdk/src/__tests__/relaycast-translate.test.ts +++ b/packages/sdk/src/__tests__/relaycast-translate.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { normalizeWebhookSubscription, toRelayNode } from '../messaging/relaycast-translate.js'; +import { + normalizeActionInvocation, + normalizeWebhookSubscription, + toRelayNode, +} from '../messaging/relaycast-translate.js'; describe('toRelayNode fleet liveness', () => { it('omits stale active-agent load for an offline node', () => { @@ -19,6 +23,25 @@ describe('toRelayNode fleet liveness', () => { }); }); +describe('normalizeActionInvocation dispatch evidence', () => { + it('keeps the node that handled or was dispatched the invocation', () => { + const invocation = normalizeActionInvocation({ + invocation_id: 'inv_late', + action_name: 'spawn', + status: 'failed', + handler_node_id: 'node_a', + dispatched_node_id: 'node_b', + }); + expect(invocation).toMatchObject({ handlerNodeId: 'node_a', dispatchedNodeId: 'node_b' }); + }); + + it('omits node ids the server did not report', () => { + const invocation = normalizeActionInvocation({ invocation_id: 'inv', status: 'invoked' }); + expect(invocation).not.toHaveProperty('handlerNodeId'); + expect(invocation).not.toHaveProperty('dispatchedNodeId'); + }); +}); + describe('subscription event normalization', () => { it.each([ { events: ['message.created', 'thread.reply'] }, diff --git a/packages/sdk/src/messaging/relaycast-translate.ts b/packages/sdk/src/messaging/relaycast-translate.ts index a925573c0..ec4dfca6e 100644 --- a/packages/sdk/src/messaging/relaycast-translate.ts +++ b/packages/sdk/src/messaging/relaycast-translate.ts @@ -232,6 +232,12 @@ export function normalizeActionInvocation(raw: unknown): RelayActionInvocation { actionName: readStr(record, 'actionName', 'action_name') ?? '', callerId: (readStr(record, 'callerId', 'caller_id') ?? null) as string | null, callerName: (readStr(record, 'callerName', 'caller_name') ?? null) as string | null, + ...(readStr(record, 'handlerNodeId', 'handler_node_id') + ? { handlerNodeId: readStr(record, 'handlerNodeId', 'handler_node_id') } + : {}), + ...(readStr(record, 'dispatchedNodeId', 'dispatched_node_id') + ? { dispatchedNodeId: readStr(record, 'dispatchedNodeId', 'dispatched_node_id') } + : {}), input: readRecord(record, 'input') ?? {}, output: readRecord(record, 'output') ?? null, status: readStr(record, 'status') ?? 'invoked', diff --git a/packages/sdk/src/messaging/relaycast.ts b/packages/sdk/src/messaging/relaycast.ts index fe5929175..3855d2aed 100644 --- a/packages/sdk/src/messaging/relaycast.ts +++ b/packages/sdk/src/messaging/relaycast.ts @@ -130,7 +130,7 @@ export { RelayPlacementError } from './relaycast-placement.js'; export type { RelaySpawnDispatchState, RelaySpawnPlacementState } from './relaycast-placement.js'; export type { RelaycastMessagingOptions } from './relaycast-client.js'; -const DEFAULT_CONFIRM_TIMEOUT_MS = 120_000; +const DEFAULT_CONFIRM_TIMEOUT_MS = 360_000; const DEFAULT_CONFIRM_POLL_MS = 500; /** `setTimeout` clamps anything larger, firing immediately instead of waiting. */ const MAX_CONFIRM_TIMEOUT_MS = 2_147_483_647; @@ -1114,7 +1114,7 @@ export class RelaycastMessagingClient implements RelayMessagingClient { `node '${context.node}' accepted ${actionName} (invocation ${invocationId}) but never reported a result within ${budgetMs}ms. ` + `The node advertised capacity and acknowledged the dispatch; nothing confirmed that it launched. ` + `The invocation may still be running, so do not retry blindly. ` + - `Check that node's broker version, or re-run without confirmation to accept an unconfirmed dispatch.` + + `Poll this dispatch with agent-relay fleet spawn-status ${invocationId}; an unknown outcome is not proof that no process started.` + (lastReadError ? ` Last read error: ${lastReadError}` : ''), { ...errorContext, diff --git a/packages/sdk/src/messaging/types.ts b/packages/sdk/src/messaging/types.ts index dedd37ca2..e1bbb6062 100644 --- a/packages/sdk/src/messaging/types.ts +++ b/packages/sdk/src/messaging/types.ts @@ -599,6 +599,8 @@ export interface RelayActionInvocation { actionName: string; callerId?: string | null; callerName?: string | null; + handlerNodeId?: string | null; + dispatchedNodeId?: string | null; input?: Record; output?: Record | null; status: string; @@ -707,7 +709,7 @@ export interface RelaySpawnPlacementInput { * How long to wait for that terminal result. Must exceed the node's own * readiness window (the broker's `verify_ready` mode holds the action open * for up to 90s, and starts that clock only once the launch completes). - * Defaults to 120000; `fleet spawn` enforces a 95000 floor for verified + * Defaults to 360000; `fleet spawn` enforces a 95000 floor for verified * targeted spawns so the two windows nest. */ confirmTimeoutMs?: number; diff --git a/tests/fixtures/verify-fleet-daytona.test.ts b/tests/fixtures/verify-fleet-daytona.test.ts index 015572b5e..004588934 100644 --- a/tests/fixtures/verify-fleet-daytona.test.ts +++ b/tests/fixtures/verify-fleet-daytona.test.ts @@ -2059,10 +2059,10 @@ describe('complete Daytona Fleet board', () => { const actual = await collectFleetCliInventory('packages/cli/dist/cli/index.js'); // The trusted verifier runs from current main while the candidate CLI is // hydrated separately. Keep this assertion pinned to main's known - // 30-executable-leaf/36-record surface; candidate inventory equality is checked in + // 31-executable-leaf/37-record surface; candidate inventory equality is checked in // the qualification job against the hydrated artifact. - expect(actual.commands).toHaveLength(36); - expect(actual.commands.filter(({ leaf }: { leaf: boolean }) => leaf)).toHaveLength(30); + expect(actual.commands).toHaveLength(37); + expect(actual.commands.filter(({ leaf }: { leaf: boolean }) => leaf)).toHaveLength(31); expect(inventorySha256(actual)).toMatch(/^[a-f0-9]{64}$/); for (const verb of ['config', 'enable', 'disable', 'inherit']) { expect(actual.commands.find(({ path }: { path: string }) => path === `fleet ${verb}`)).toMatchObject({ @@ -2082,6 +2082,15 @@ describe('complete Daytona Fleet board', () => { hidden: false, leaf: true, }); + // The read-only poll for a spawn whose confirmation window expired. It has + // to be on the published surface for the timeout error that names it to be + // actionable. + expect(actual.commands.find(({ path }: { path: string }) => path === 'fleet spawn-status')).toMatchObject( + { + hidden: false, + leaf: true, + } + ); expect( actual.commands .find(({ path }: { path: string }) => path === 'node up') diff --git a/tests/relayflows/cleanroom/fleet-cli-inventory.json b/tests/relayflows/cleanroom/fleet-cli-inventory.json index 498bdbd84..0bf5b75ed 100644 --- a/tests/relayflows/cleanroom/fleet-cli-inventory.json +++ b/tests/relayflows/cleanroom/fleet-cli-inventory.json @@ -1044,7 +1044,7 @@ "conflictsWith": [], "implied": null, "envVar": null, - "defaultValue": "120000", + "defaultValue": "360000", "presetArg": null }, { @@ -1457,6 +1457,91 @@ } ] }, + { + "path": "fleet spawn-status", + "aliases": [], + "hidden": false, + "leaf": true, + "arguments": [ + { + "name": "invocation-id", + "required": true, + "variadic": false, + "choices": null, + "defaultValue": null + } + ], + "options": [ + { + "flags": "--base-url ", + "short": null, + "long": "--base-url", + "mandatory": false, + "valueRequired": true, + "valueOptional": false, + "variadic": false, + "negate": false, + "hidden": false, + "choices": null, + "conflictsWith": [], + "implied": null, + "envVar": null, + "defaultValue": null, + "presetArg": null + }, + { + "flags": "--token ", + "short": null, + "long": "--token", + "mandatory": false, + "valueRequired": true, + "valueOptional": false, + "variadic": false, + "negate": false, + "hidden": false, + "choices": null, + "conflictsWith": [], + "implied": null, + "envVar": null, + "defaultValue": null, + "presetArg": null + }, + { + "flags": "--wk ", + "short": null, + "long": "--wk", + "mandatory": false, + "valueRequired": true, + "valueOptional": false, + "variadic": false, + "negate": false, + "hidden": false, + "choices": null, + "conflictsWith": [], + "implied": null, + "envVar": null, + "defaultValue": null, + "presetArg": null + }, + { + "flags": "--workspace-key ", + "short": null, + "long": "--workspace-key", + "mandatory": false, + "valueRequired": true, + "valueOptional": false, + "variadic": false, + "negate": false, + "hidden": false, + "choices": null, + "conflictsWith": [], + "implied": null, + "envVar": null, + "defaultValue": null, + "presetArg": null + } + ] + }, { "path": "fleet status", "aliases": [], diff --git a/tests/relayflows/cleanroom/fleet-daytona.matrix.json b/tests/relayflows/cleanroom/fleet-daytona.matrix.json index 15ab20607..b31bebbca 100644 --- a/tests/relayflows/cleanroom/fleet-daytona.matrix.json +++ b/tests/relayflows/cleanroom/fleet-daytona.matrix.json @@ -8,7 +8,7 @@ "requiredSnapshotRelayVersion": "11.10.3", "artifactRoot": ".workflow-artifacts/verify-fleet-daytona", "inventoryFile": "fleet-cli-inventory.json", - "inventorySha256": "8bdc4be9341d4efb5a5e04ae8fe601fd30a9d664681b82ab2c4c729f46b4ac16", + "inventorySha256": "88839ab72869ab2cfc6d0757700dd7f0253581b921c5c3d9097dd08ff4ba23df", "commandSurface": { "fleet agent list": [ "fleet-agent-list-json", @@ -118,7 +118,7 @@ "node-agent-set-model-app-server-a": ["node agent spawn", "node agent set-model"], "node-agent-set-model-app-server-b": ["node agent spawn", "node agent set-model"] }, - "deferredCommandSurface": [], + "deferredCommandSurface": ["fleet spawn-status"], "optionCoverage": { "fleet agent list": [ { @@ -572,6 +572,28 @@ "operationId": "fleet-spawn-metadata-channel-model-cwd" } ], + "fleet spawn-status": [ + { + "option": "--base-url", + "status": "skipped", + "reason": "The proof uses the credential-bound workspace endpoint; alternate endpoint selection is covered by CLI connection tests without changing the Daytona target." + }, + { + "option": "--token", + "status": "skipped", + "reason": "Token argv would duplicate reveal-once credentials; the board proves redaction and uses the bound workspace key." + }, + { + "option": "--wk", + "status": "skipped", + "reason": "The legacy --wk credential alias is covered by CLI connection tests; the board must not mint a second credential source." + }, + { + "option": "--workspace-key", + "status": "skipped", + "reason": "The bound workspace key is injected only where the operation needs it; repeating it on every Fleet read would multiply secret-bearing argv." + } + ], "fleet status": [ { "option": "--base-url",