agent-relay 12.1.1 → 12.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -8
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.d.ts.map +1 -1
- package/dist/cli/commands/fleet.js +258 -38
- package/dist/cli/commands/fleet.js.map +1 -1
- package/dist/cli/commands/local-agent.d.ts +3 -1
- package/dist/cli/commands/local-agent.d.ts.map +1 -1
- package/dist/cli/commands/local-agent.js +63 -5
- package/dist/cli/commands/local-agent.js.map +1 -1
- package/dist/cli/lib/fleet-attach-target.d.ts +18 -0
- package/dist/cli/lib/fleet-attach-target.d.ts.map +1 -0
- package/dist/cli/lib/fleet-attach-target.js +57 -0
- package/dist/cli/lib/fleet-attach-target.js.map +1 -0
- package/dist/cli/lib/message-delivery-receipts.d.ts +2 -0
- package/dist/cli/lib/message-delivery-receipts.d.ts.map +1 -1
- package/dist/cli/lib/message-delivery-receipts.js +15 -8
- package/dist/cli/lib/message-delivery-receipts.js.map +1 -1
- package/dist/cli/lib/sandbox-repo.d.ts +23 -0
- package/dist/cli/lib/sandbox-repo.d.ts.map +1 -0
- package/dist/cli/lib/sandbox-repo.js +234 -0
- package/dist/cli/lib/sandbox-repo.js.map +1 -0
- package/dist/cli/lib/sdk-client.d.ts +2 -0
- package/dist/cli/lib/sdk-client.d.ts.map +1 -1
- package/dist/cli/lib/sdk-client.js +13 -2
- package/dist/cli/lib/sdk-client.js.map +1 -1
- package/dist/cli/lib/workspace-session.d.ts.map +1 -1
- package/dist/cli/lib/workspace-session.js +4 -2
- package/dist/cli/lib/workspace-session.js.map +1 -1
- package/dist/cli/mcp/messaging-tools.d.ts.map +1 -1
- package/dist/cli/mcp/messaging-tools.js +10 -1
- package/dist/cli/mcp/messaging-tools.js.map +1 -1
- package/package.json +9 -9
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { InvalidArgumentError } from 'commander';
|
|
3
|
-
import {
|
|
4
|
+
import { findProjectRoot } from '@agent-relay/config';
|
|
5
|
+
import { CloudFleetSandboxProvisionError, deleteCloudFleetSandbox, ensureCloudFleetSandbox, materializeCloudRelayfileRepository, resolveWorkspaceByKey, } from '@agent-relay/cloud';
|
|
4
6
|
import { HarnessDriverClient } from '@agent-relay/harness-driver';
|
|
5
7
|
import { createWorkspaceClient, RelayPlacementError, } from '@agent-relay/sdk';
|
|
6
8
|
import { withDefaults } from './core.js';
|
|
7
9
|
import { buildRows, collectWithRetry, formatPretty, readRemoteLiveAgents, readLocalBrokerMaps, } from './fleet-agent.js';
|
|
8
10
|
import { readBrokerConnection } from '../lib/broker-lifecycle.js';
|
|
11
|
+
import { spawnAgentWithClient } from '../lib/client-factory.js';
|
|
12
|
+
import { connectProjectBrokerClient } from '../lib/project-broker-client.js';
|
|
9
13
|
import { isAvailableFleetNode } from '../lib/fleet-live-agents.js';
|
|
10
14
|
import { declaredWorkforceMetadata } from '../lib/registration-metadata.js';
|
|
11
15
|
import { redactSecrets } from '../lib/redact.js';
|
|
12
16
|
import { attributableReleaseReason } from '../lib/release-reason.js';
|
|
17
|
+
import { resolveSandboxRepository } from '../lib/sandbox-repo.js';
|
|
13
18
|
import { spawnPlacementReceipt } from '../lib/spawn-lifecycle.js';
|
|
14
19
|
import { resolveAgentToken, resolveWorkspaceSelection, persistWorkspaceRelaycastTarget, resolveWorkspaceKeyWithSource, resolveWorkspaceTransport, } from '../lib/sdk-client.js';
|
|
15
20
|
import { addSdkOptions, printJson, runSdk, sdkOptionsFromOpts, withSdkDefaults, } from '../lib/sdk-command.js';
|
|
@@ -20,6 +25,56 @@ const CLOUD_SANDBOX_ID_PATTERN = /^sbx_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-
|
|
|
20
25
|
function spawnInvocationWithPlacement(invocation) {
|
|
21
26
|
return { ...invocation, placement: spawnPlacementReceipt(invocation) };
|
|
22
27
|
}
|
|
28
|
+
function assertSandboxRepositoryRevision(sandbox, selection) {
|
|
29
|
+
if (!selection)
|
|
30
|
+
return;
|
|
31
|
+
const expected = { [selection.repository]: selection.revision };
|
|
32
|
+
if (sandbox.outcome === 'provisioning_timeout') {
|
|
33
|
+
throw new Error(`Sandbox node '${sandbox.nodeName}' did not become ready within ${sandbox.waitedMs}ms; the repository revision was not verified.`);
|
|
34
|
+
}
|
|
35
|
+
const actual = sandbox.repoRevisions?.[selection.repository];
|
|
36
|
+
if (actual !== selection.revision || Object.keys(sandbox.repoRevisions ?? {}).length !== 1) {
|
|
37
|
+
throw new Error(`Cloud did not echo the requested repository revision for ${selection.repository}; update Cloud before retrying this sandbox launch.`);
|
|
38
|
+
}
|
|
39
|
+
// Keep the shape check explicit at the CLI boundary too: injected/test
|
|
40
|
+
// implementations and older Cloud clients must not bypass the attestation.
|
|
41
|
+
if (JSON.stringify(sandbox.repoRevisions) !== JSON.stringify(expected)) {
|
|
42
|
+
throw new Error(`Cloud returned an unexpected repository revision for ${selection.repository}.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function pathContains(parent, child) {
|
|
46
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child));
|
|
47
|
+
return (relative === '' ||
|
|
48
|
+
(relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)));
|
|
49
|
+
}
|
|
50
|
+
function liveRelayfileMountPaths(materialization, requested) {
|
|
51
|
+
const contentRoot = materialization.contentRoot;
|
|
52
|
+
const sentinelRoot = path.posix.dirname(materialization.sentinelPath);
|
|
53
|
+
const requestedPaths = requested ?? [];
|
|
54
|
+
if (requestedPaths.length > 13) {
|
|
55
|
+
throw new Error('--sandbox-relayfile-path accepts at most 13 paths when a live repository, its source metadata, and workspace skills are mounted.');
|
|
56
|
+
}
|
|
57
|
+
const contentAncestor = requestedPaths.find((candidate) => {
|
|
58
|
+
const root = candidate
|
|
59
|
+
.trim()
|
|
60
|
+
.replace(/\/\*\*$/, '')
|
|
61
|
+
.replace(/\/$/, '');
|
|
62
|
+
return contentRoot === root || contentRoot.startsWith(`${root}/`);
|
|
63
|
+
});
|
|
64
|
+
if (contentAncestor && contentAncestor.trim() !== `${contentRoot}/**`) {
|
|
65
|
+
throw new Error(`Relayfile path ${JSON.stringify(contentAncestor)} contains the repository source root; omit it so Relay can mount ${contentRoot}/** as a decoded working tree.`);
|
|
66
|
+
}
|
|
67
|
+
return [...new Set([`${contentRoot}/**`, `${sentinelRoot}/**`, '/.skills/**', ...requestedPaths])];
|
|
68
|
+
}
|
|
69
|
+
function liveRelayfileWorkerCwd(mountRoot, materialization, relativeCwd) {
|
|
70
|
+
const root = mountRoot.replace(/\/+$/, '') || '/';
|
|
71
|
+
const sourceRoot = `${root === '/' ? '' : root}${materialization.contentRoot}`;
|
|
72
|
+
return relativeCwd ? `${sourceRoot}/${relativeCwd}` : sourceRoot;
|
|
73
|
+
}
|
|
74
|
+
function mountedRelayfilePath(mountRoot, remotePath) {
|
|
75
|
+
const root = mountRoot.replace(/\/+$/, '') || '/';
|
|
76
|
+
return `${root === '/' ? '' : root}${remotePath}`;
|
|
77
|
+
}
|
|
23
78
|
// The targeted spawn path (relay.messaging.placement.spawn) returns an
|
|
24
79
|
// invocation whose `placement` is the SDK's own evidence object
|
|
25
80
|
// (`state: 'accepted' | 'ready'`, `confirmed`). `spawnPlacementReceipt`
|
|
@@ -67,13 +122,19 @@ function withFleetDefaults(overrides = {}) {
|
|
|
67
122
|
return {
|
|
68
123
|
core,
|
|
69
124
|
sdk,
|
|
125
|
+
cwd: () => process.cwd(),
|
|
126
|
+
connectLocalBroker: async (cwd) => connectProjectBrokerClient(cwd),
|
|
70
127
|
createFleetWorkspaceClient: (options) => {
|
|
71
128
|
const { workspaceKey, baseUrl } = resolveWorkspaceTransport(options);
|
|
72
129
|
return createWorkspaceClient({ workspaceKey, baseUrl });
|
|
73
130
|
},
|
|
74
131
|
resolveWorkspaceSelection,
|
|
132
|
+
resolveSandboxRepository,
|
|
133
|
+
findProjectRoot,
|
|
134
|
+
resolveWorkspaceByKey,
|
|
75
135
|
persistWorkspaceRelaycastTarget,
|
|
76
136
|
ensureCloudFleetSandbox,
|
|
137
|
+
materializeCloudRelayfileRepository,
|
|
77
138
|
deleteCloudFleetSandbox,
|
|
78
139
|
log: (...args) => console.log(...args),
|
|
79
140
|
warn: (...args) => console.warn(...args),
|
|
@@ -138,13 +199,15 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
138
199
|
});
|
|
139
200
|
addSdkOptions(group
|
|
140
201
|
.command('spawn')
|
|
141
|
-
.description('Spawn
|
|
202
|
+
.description('Spawn locally by default, or select a fleet node or Cloud sandbox explicitly')
|
|
142
203
|
.argument('<cli>', 'AI CLI to launch', parseFleetCli)
|
|
143
204
|
.requiredOption('--name <name>', 'Worker agent name')
|
|
144
205
|
.requiredOption('--task <text>', 'Initial task instructions')
|
|
206
|
+
.option('--auto-place', 'Request automatic eligible-node placement in the Relay workspace')
|
|
145
207
|
.option('--node <name>', 'Target a specific fleet node')
|
|
146
208
|
.option('--target-node <name>', 'Alias for --node')
|
|
147
|
-
.option('--sandbox', 'Provision a fresh Cloud sandbox
|
|
209
|
+
.option('--sandbox', 'Provision a fresh Cloud sandbox and spawn with a live Relayfile workspace/repository mount')
|
|
210
|
+
.option('--checkout', 'Materialize the current Git checkout at its exact pushed HEAD (static; requires --sandbox)')
|
|
148
211
|
.option('--sandbox-name <name>', 'Explicit sandbox node name (custom unless --sandbox-id requires matching fleet-sandbox-<UUID>)')
|
|
149
212
|
.option('--sandbox-id <id>', 'Reuse a caller-declared sbx_<UUID> identity for an exact replay')
|
|
150
213
|
.option('--workspace-id <id>', 'Explicit Relay workspace identity required for sandbox provisioning')
|
|
@@ -154,7 +217,7 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
154
217
|
.option('--channel <name>', 'Channel for the worker to join')
|
|
155
218
|
.option('--persona <persona>', 'Worker persona (automatic placement)')
|
|
156
219
|
.option('--model <model>', 'Model powering the worker')
|
|
157
|
-
.option('--cwd <path>', '
|
|
220
|
+
.option('--cwd <path>', 'Working directory: defaults to the caller directory for local spawn; maps a local repo-relative path with --sandbox; otherwise selects a path on the remote node')
|
|
158
221
|
.option('--organization <organization>', 'Declared organization for workforce reporting')
|
|
159
222
|
.option('--project <project>', 'Declared project for workforce reporting')
|
|
160
223
|
.option('--workstream <workstream>', 'Declared workstream for workforce reporting')
|
|
@@ -164,14 +227,29 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
164
227
|
.option('--no-confirm', 'Report a targeted spawn as soon as the node accepts it, without waiting for the node to confirm the agent actually launched')
|
|
165
228
|
.option('--confirm-timeout <ms>', 'How long a targeted spawn waits for the node to confirm the launch', '120000')).action(async (cli, options) => {
|
|
166
229
|
await runSdk(deps.sdk, async () => {
|
|
167
|
-
warnIfInferredFromProjectSession(options, deps.warn);
|
|
168
230
|
const clientOptions = sdkOptionsFromOpts(options);
|
|
169
231
|
const name = requiredText(options.name, 'Worker name');
|
|
170
232
|
const task = requiredText(options.task, 'Task');
|
|
171
233
|
let targetNode = optionalText(options.targetNode, 'Target node') ?? optionalText(options.node, 'Node');
|
|
172
234
|
const useSandbox = options.sandbox === true;
|
|
235
|
+
// Explicit hosted credentials/transport and personas retain their legacy
|
|
236
|
+
// automatic-placement contract. Ambient credentials and a persisted
|
|
237
|
+
// Cloud target must never turn a flag-free local spawn into a remote one.
|
|
238
|
+
const automaticPlacement = options.autoPlace === true ||
|
|
239
|
+
['workspaceKey', 'token', 'baseUrl', 'persona'].some((key) => options[key] !== undefined);
|
|
240
|
+
if (options.autoPlace === true && (useSandbox || targetNode)) {
|
|
241
|
+
throw new Error('--auto-place cannot be combined with --sandbox, --node, or --target-node.');
|
|
242
|
+
}
|
|
243
|
+
if (useSandbox || targetNode || automaticPlacement) {
|
|
244
|
+
warnIfInferredFromProjectSession(options, deps.warn);
|
|
245
|
+
}
|
|
246
|
+
const checkoutRepository = options.checkout === true;
|
|
173
247
|
const sandboxName = optionalText(options.sandboxName, 'Sandbox name');
|
|
174
248
|
const sandboxIdOption = optionalText(options.sandboxId, 'Sandbox ID');
|
|
249
|
+
// An explicit sandbox identity is a retained/replayable resource. Never
|
|
250
|
+
// delete it as collateral when a later verification or dispatch step
|
|
251
|
+
// fails; only clean up sandboxes whose identity this invocation minted.
|
|
252
|
+
const shouldCleanupSandbox = sandboxIdOption === undefined;
|
|
175
253
|
const explicitWorkspaceId = optionalText(options.workspaceId, 'Workspace ID');
|
|
176
254
|
if (sandboxIdOption !== undefined && !CLOUD_SANDBOX_ID_PATTERN.test(sandboxIdOption)) {
|
|
177
255
|
throw new Error('--sandbox-id must match lowercase sbx_<UUID> using an RFC 4122 UUID.');
|
|
@@ -195,6 +273,9 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
195
273
|
if (!useSandbox && sandboxName) {
|
|
196
274
|
throw new Error('--sandbox-name requires --sandbox.');
|
|
197
275
|
}
|
|
276
|
+
if (!useSandbox && checkoutRepository) {
|
|
277
|
+
throw new Error('--checkout requires --sandbox.');
|
|
278
|
+
}
|
|
198
279
|
if (!useSandbox && sandboxIdOption) {
|
|
199
280
|
throw new Error('--sandbox-id requires --sandbox.');
|
|
200
281
|
}
|
|
@@ -215,7 +296,8 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
215
296
|
}
|
|
216
297
|
const channel = optionalText(options.channel, 'Channel');
|
|
217
298
|
const model = optionalText(options.model, 'Model');
|
|
218
|
-
|
|
299
|
+
const requestedCwd = optionalText(options.cwd, 'Worker cwd');
|
|
300
|
+
let workerCwd = requestedCwd;
|
|
219
301
|
const organization = optionalText(options.organization, 'Organization');
|
|
220
302
|
const project = optionalText(options.project, 'Project');
|
|
221
303
|
const workstream = optionalText(options.workstream, 'Workstream');
|
|
@@ -229,26 +311,90 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
229
311
|
throw new Error('--confirm-timeout must be a positive number of milliseconds.');
|
|
230
312
|
}
|
|
231
313
|
let sandbox;
|
|
314
|
+
let sandboxRepository;
|
|
315
|
+
let liveRepository;
|
|
316
|
+
let attachProjectRoot;
|
|
232
317
|
let workspaceRelay;
|
|
233
318
|
let relaycastClientOptions = clientOptions;
|
|
234
319
|
let legacyWorkspaceClientOptions = clientOptions;
|
|
235
320
|
if (useSandbox) {
|
|
321
|
+
const coreProjectRoot = deps.core.getProjectPaths().projectRoot;
|
|
322
|
+
const hasExplicitProjectOverride = Boolean(deps.core.env?.AGENT_RELAY_PROJECT?.trim() || process.env.AGENT_RELAY_PROJECT?.trim());
|
|
323
|
+
if (checkoutRepository || mountSandboxRelayfile) {
|
|
324
|
+
// AGENT_RELAY_PROJECT selects the workspace namespace, while static
|
|
325
|
+
// checkout and live Relayfile source inference remain anchored to
|
|
326
|
+
// the actual Git tree. This also lets --cwd point at a sibling
|
|
327
|
+
// checkout when explicitly asked.
|
|
328
|
+
const repositoryRootHint = hasExplicitProjectOverride ? process.cwd() : coreProjectRoot;
|
|
329
|
+
sandboxRepository = deps.resolveSandboxRepository(repositoryRootHint, requestedCwd);
|
|
330
|
+
if (checkoutRepository && !sandboxRepository) {
|
|
331
|
+
throw new Error('--checkout requires a GitHub checkout with a clean, pushed commit.');
|
|
332
|
+
}
|
|
333
|
+
if (checkoutRepository && sandboxRepository) {
|
|
334
|
+
workerCwd = sandboxRepository.workerCwd;
|
|
335
|
+
}
|
|
336
|
+
else if (sandboxRepository) {
|
|
337
|
+
// Relative/local --cwd values were consumed by repository
|
|
338
|
+
// inference and must be remapped beneath the remote live source
|
|
339
|
+
// root after Cloud returns its provider-specific mount path.
|
|
340
|
+
workerCwd =
|
|
341
|
+
requestedCwd && /^\/(?:srv\/agent-workforce|workspace)(?:\/|$)/.test(requestedCwd)
|
|
342
|
+
? requestedCwd
|
|
343
|
+
: undefined;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const localRequestedCwd = sandboxRepository &&
|
|
347
|
+
requestedCwd &&
|
|
348
|
+
!/^\/(?:srv\/agent-workforce|workspace)(?:\/|$)/.test(requestedCwd)
|
|
349
|
+
? path.resolve(process.cwd(), requestedCwd)
|
|
350
|
+
: undefined;
|
|
351
|
+
// With --checkout, `--cwd` selects both the local checkout subdirectory
|
|
352
|
+
// and its Relay project namespace. Resolve an intentional nested pin
|
|
353
|
+
// before mapping that path to the static remote checkout; only
|
|
354
|
+
// placement-safe Git identity crosses the Cloud boundary.
|
|
355
|
+
const workspaceProjectRoot = hasExplicitProjectOverride
|
|
356
|
+
? coreProjectRoot
|
|
357
|
+
: localRequestedCwd
|
|
358
|
+
? deps.findProjectRoot(localRequestedCwd)
|
|
359
|
+
: sandboxRepository && pathContains(sandboxRepository.projectRoot, coreProjectRoot)
|
|
360
|
+
? coreProjectRoot
|
|
361
|
+
: (sandboxRepository?.projectRoot ?? coreProjectRoot);
|
|
362
|
+
if (path.resolve(workspaceProjectRoot) !== path.resolve(coreProjectRoot)) {
|
|
363
|
+
attachProjectRoot = workspaceProjectRoot;
|
|
364
|
+
}
|
|
365
|
+
const sandboxClientOptions = {
|
|
366
|
+
...clientOptions,
|
|
367
|
+
projectRoot: workspaceProjectRoot,
|
|
368
|
+
};
|
|
369
|
+
relaycastClientOptions = sandboxClientOptions;
|
|
236
370
|
// Cloud must be the first network authority for a sandbox invocation.
|
|
237
371
|
// A canonical Relaycast info call would both leak the workspace key and
|
|
238
372
|
// make it impossible to prove that Cloud's isolated target is the one
|
|
239
373
|
// subsequently used for registration and dispatch.
|
|
240
|
-
const workspaceSelection = deps.resolveWorkspaceSelection(
|
|
374
|
+
const workspaceSelection = deps.resolveWorkspaceSelection({
|
|
375
|
+
...sandboxClientOptions,
|
|
376
|
+
});
|
|
241
377
|
legacyWorkspaceClientOptions = {
|
|
242
|
-
...
|
|
378
|
+
...sandboxClientOptions,
|
|
243
379
|
...(sandboxProvider === 'agent37' ? {} : { ignorePersistedRelaycastTarget: true }),
|
|
244
380
|
};
|
|
245
381
|
let relayWorkspaceId = explicitWorkspaceId ?? workspaceSelection?.workspaceId?.trim();
|
|
382
|
+
// Older/rebound project pins contain only the canonical key. Resolve
|
|
383
|
+
// that exact selection with Cloud using a POST body, never a key URL
|
|
384
|
+
// or an ambient active workspace. Successful target persistence below
|
|
385
|
+
// records the identity for the next invocation.
|
|
386
|
+
if (!relayWorkspaceId &&
|
|
387
|
+
workspaceSelection?.key &&
|
|
388
|
+
(sandboxProvider === undefined || sandboxProvider === 'agent37')) {
|
|
389
|
+
const resolved = await deps.resolveWorkspaceByKey(workspaceSelection.key);
|
|
390
|
+
relayWorkspaceId = resolved.cloudWorkspaceId;
|
|
391
|
+
}
|
|
246
392
|
// Legacy providers remain backward compatible: they may resolve the
|
|
247
393
|
// workspace from canonical Relaycast. Agent37 may not, because even a
|
|
248
394
|
// read there mutates rate-limit/presence accounting on the shared
|
|
249
395
|
// service and defeats the zero-shared-traffic canary proof.
|
|
250
396
|
if (!relayWorkspaceId && sandboxProvider === undefined) {
|
|
251
|
-
throw new Error('Sandbox provisioning without --sandbox-provider requires a persisted Relay workspace identity; run `relay workspace
|
|
397
|
+
throw new Error('Sandbox provisioning without --sandbox-provider requires a persisted Relay workspace identity; run `relay workspace rebind <name>` or pass --workspace-id.');
|
|
252
398
|
}
|
|
253
399
|
if (!relayWorkspaceId && sandboxProvider !== undefined && sandboxProvider !== 'agent37') {
|
|
254
400
|
workspaceRelay = deps.sdk.createWorkspaceRelay(legacyWorkspaceClientOptions);
|
|
@@ -257,9 +403,21 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
257
403
|
}
|
|
258
404
|
if (!relayWorkspaceId) {
|
|
259
405
|
throw new Error(sandboxProvider === 'agent37'
|
|
260
|
-
? 'Agent37 sandbox provisioning requires a persisted Relay workspace identity; run `relay workspace
|
|
406
|
+
? 'Agent37 sandbox provisioning requires a persisted Relay workspace identity; run `relay workspace rebind <name>` or pass --workspace-id.'
|
|
261
407
|
: 'The current Relay workspace did not report an ID for Cloud provisioning.');
|
|
262
408
|
}
|
|
409
|
+
if (explicitWorkspaceId !== undefined &&
|
|
410
|
+
workspaceSelection?.workspaceId !== undefined &&
|
|
411
|
+
explicitWorkspaceId !== workspaceSelection.workspaceId.trim()) {
|
|
412
|
+
throw new Error('--workspace-id does not match the captured workspace identity.');
|
|
413
|
+
}
|
|
414
|
+
if (!checkoutRepository && mountSandboxRelayfile && sandboxRepository) {
|
|
415
|
+
liveRepository = await deps.materializeCloudRelayfileRepository({
|
|
416
|
+
workspaceId: relayWorkspaceId,
|
|
417
|
+
repository: sandboxRepository.repository,
|
|
418
|
+
revision: sandboxRepository.revision,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
263
421
|
const sandboxId = sandboxIdOption ?? (sandboxName === undefined ? `sbx_${randomUUID()}` : undefined);
|
|
264
422
|
const deterministicSandboxName = sandboxId === undefined ? undefined : `fleet-sandbox-${sandboxId.slice('sbx_'.length)}`;
|
|
265
423
|
if (sandboxIdOption !== undefined &&
|
|
@@ -276,28 +434,33 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
276
434
|
const workloadProfile = sandboxProvider === undefined || sandboxProvider === 'agent37'
|
|
277
435
|
? 'long-running-agent'
|
|
278
436
|
: 'standard-long-running-agent';
|
|
279
|
-
if (explicitWorkspaceId !== undefined &&
|
|
280
|
-
workspaceSelection?.workspaceId !== undefined &&
|
|
281
|
-
explicitWorkspaceId !== workspaceSelection.workspaceId.trim()) {
|
|
282
|
-
throw new Error('--workspace-id does not match the captured workspace identity.');
|
|
283
|
-
}
|
|
284
437
|
try {
|
|
285
438
|
sandbox = await deps.ensureCloudFleetSandbox({
|
|
286
439
|
workspaceId: relayWorkspaceId,
|
|
287
440
|
requiredCapability: `spawn:${cli}`,
|
|
288
441
|
maxAgents: 1,
|
|
289
442
|
mountRelayfile: mountSandboxRelayfile,
|
|
290
|
-
...(
|
|
443
|
+
...(liveRepository
|
|
444
|
+
? { relayfilePaths: liveRelayfileMountPaths(liveRepository, sandboxRelayfilePaths) }
|
|
445
|
+
: sandboxRelayfilePaths === undefined
|
|
446
|
+
? {}
|
|
447
|
+
: { relayfilePaths: sandboxRelayfilePaths }),
|
|
291
448
|
...(sandboxId === undefined ? {} : { sandboxId }),
|
|
292
449
|
forceProvision: true,
|
|
293
450
|
...(sandboxProvider === undefined ? {} : { providerId: sandboxProvider }),
|
|
294
451
|
workloadProfile,
|
|
295
452
|
waitTimeoutMs: 90_000,
|
|
296
453
|
...(effectiveSandboxName === undefined ? {} : { name: effectiveSandboxName }),
|
|
454
|
+
...(checkoutRepository && sandboxRepository ? { repos: [sandboxRepository.repository] } : {}),
|
|
455
|
+
...(checkoutRepository && sandboxRepository
|
|
456
|
+
? { repoRevisions: { [sandboxRepository.repository]: sandboxRepository.revision } }
|
|
457
|
+
: {}),
|
|
297
458
|
});
|
|
459
|
+
assertSandboxRepositoryRevision(sandbox, checkoutRepository ? sandboxRepository : undefined);
|
|
298
460
|
}
|
|
299
461
|
catch (error) {
|
|
300
|
-
if (
|
|
462
|
+
if (shouldCleanupSandbox &&
|
|
463
|
+
error instanceof CloudFleetSandboxProvisionError &&
|
|
301
464
|
error.confirmedProvisioned &&
|
|
302
465
|
error.cloudWorkspaceId &&
|
|
303
466
|
error.sandboxId) {
|
|
@@ -314,7 +477,8 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
314
477
|
else if (error instanceof CloudFleetSandboxProvisionError && error.outcomeUnknown) {
|
|
315
478
|
deps.warn(`Cloud did not return a complete provisioning response. The outcome is unknown; check Cloud Fleet for node '${error.nodeName ?? effectiveSandboxName ?? 'the requested sandbox'}'${sandboxId === undefined ? '' : ` before retrying with --sandbox-id '${sandboxId}'`} so a sandbox is not left running.`);
|
|
316
479
|
}
|
|
317
|
-
else if (
|
|
480
|
+
else if (shouldCleanupSandbox &&
|
|
481
|
+
error instanceof CloudFleetSandboxProvisionError &&
|
|
318
482
|
error.cloudWorkspaceId &&
|
|
319
483
|
error.sandboxId) {
|
|
320
484
|
await deps
|
|
@@ -327,6 +491,17 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
327
491
|
deps.warn(`Provisioning failed after Cloud created sandbox '${error.sandboxId}', and automatic cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
328
492
|
});
|
|
329
493
|
}
|
|
494
|
+
if (shouldCleanupSandbox && sandbox && sandbox.outcome !== 'reused') {
|
|
495
|
+
await deps
|
|
496
|
+
.deleteCloudFleetSandbox({
|
|
497
|
+
cloudWorkspaceId: sandbox.cloudWorkspaceId,
|
|
498
|
+
sandboxId: sandbox.sandboxId,
|
|
499
|
+
...(sandbox.providerId === undefined ? {} : { providerId: sandbox.providerId }),
|
|
500
|
+
})
|
|
501
|
+
.catch((cleanupError) => {
|
|
502
|
+
deps.warn(`Sandbox repository verification failed and cleanup also failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
330
505
|
throw error;
|
|
331
506
|
}
|
|
332
507
|
if (sandbox.outcome !== 'provisioning_timeout' && sandbox.relaycastTarget) {
|
|
@@ -347,7 +522,7 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
347
522
|
: 'Cloud returned a Relaycast target for a different workspace.');
|
|
348
523
|
}
|
|
349
524
|
relaycastClientOptions = {
|
|
350
|
-
...
|
|
525
|
+
...relaycastClientOptions,
|
|
351
526
|
workspaceKey: target.relaycastApiKey,
|
|
352
527
|
baseUrl: target.baseUrl,
|
|
353
528
|
};
|
|
@@ -364,7 +539,7 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
364
539
|
}
|
|
365
540
|
}
|
|
366
541
|
catch (error) {
|
|
367
|
-
if (sandbox.outcome === 'provisioned') {
|
|
542
|
+
if (shouldCleanupSandbox && sandbox.outcome === 'provisioned') {
|
|
368
543
|
await deps
|
|
369
544
|
.deleteCloudFleetSandbox({
|
|
370
545
|
cloudWorkspaceId: sandbox.cloudWorkspaceId,
|
|
@@ -386,20 +561,22 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
386
561
|
relaycastClientOptions = legacyWorkspaceClientOptions;
|
|
387
562
|
}
|
|
388
563
|
if (sandbox.outcome === 'provisioning_timeout') {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
564
|
+
if (shouldCleanupSandbox) {
|
|
565
|
+
await deps
|
|
566
|
+
.deleteCloudFleetSandbox({
|
|
567
|
+
cloudWorkspaceId: sandbox.cloudWorkspaceId,
|
|
568
|
+
sandboxId: sandbox.sandboxId,
|
|
569
|
+
...(sandbox.providerId === undefined ? {} : { providerId: sandbox.providerId }),
|
|
570
|
+
})
|
|
571
|
+
.catch((error) => {
|
|
572
|
+
deps.warn(`The timed-out sandbox could not be cleaned up automatically: ${error instanceof Error ? error.message : String(error)}`);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
398
575
|
throw new Error(`Sandbox node '${sandbox.nodeName}' did not become ready within ${sandbox.waitedMs}ms.`);
|
|
399
576
|
}
|
|
400
577
|
if (mountSandboxRelayfile &&
|
|
401
578
|
(sandbox.outcome !== 'provisioned' || sandbox.relayfileMounted !== true)) {
|
|
402
|
-
if (sandbox.outcome === 'provisioned') {
|
|
579
|
+
if (shouldCleanupSandbox && sandbox.outcome === 'provisioned') {
|
|
403
580
|
await deps
|
|
404
581
|
.deleteCloudFleetSandbox({
|
|
405
582
|
cloudWorkspaceId: sandbox.cloudWorkspaceId,
|
|
@@ -413,6 +590,13 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
413
590
|
throw new Error('Cloud returned a sandbox node without the required Relayfile mount.');
|
|
414
591
|
}
|
|
415
592
|
targetNode = sandbox.nodeName;
|
|
593
|
+
if (liveRepository &&
|
|
594
|
+
sandboxRepository &&
|
|
595
|
+
!workerCwd &&
|
|
596
|
+
sandbox.outcome === 'provisioned' &&
|
|
597
|
+
sandbox.relayfileMounted) {
|
|
598
|
+
workerCwd = liveRelayfileWorkerCwd(sandbox.relayfileMountPath ?? '/workspace', liveRepository, sandboxRepository.repositoryRelativeCwd);
|
|
599
|
+
}
|
|
416
600
|
if (!workerCwd && sandbox.outcome === 'provisioned' && sandbox.relayfileMounted) {
|
|
417
601
|
workerCwd = sandbox.relayfileMountPath ?? '/workspace';
|
|
418
602
|
}
|
|
@@ -454,6 +638,9 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
454
638
|
// the invocation and launches nothing, which is indistinguishable from
|
|
455
639
|
// success here — so wait for the node to confirm unless asked not to.
|
|
456
640
|
const confirm = options.confirm !== false;
|
|
641
|
+
const liveSandboxContext = sandbox?.outcome === 'provisioned' && liveRepository && sandboxRepository
|
|
642
|
+
? `Agent Relay sandbox context: ${liveRepository.repository} is mounted as a live Relayfile working tree at ${liveRelayfileWorkerCwd(sandbox.relayfileMountPath ?? '/workspace', liveRepository, '')}. Its exact source revision is ${liveRepository.revision}; the same attestation is recorded at ${mountedRelayfilePath(sandbox.relayfileMountPath ?? '/workspace', liveRepository.sentinelPath)}. Workspace skills are under ${mountedRelayfilePath(sandbox.relayfileMountPath ?? '/workspace', '/.skills')}. The Relayfile daemon synchronizes this tree; it intentionally has no .git directory.`
|
|
643
|
+
: undefined;
|
|
457
644
|
const invocation = await relay.messaging.placement.spawn({
|
|
458
645
|
capability: `spawn:${cli}`,
|
|
459
646
|
node: targetNode,
|
|
@@ -463,7 +650,15 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
463
650
|
input: {
|
|
464
651
|
name,
|
|
465
652
|
cli,
|
|
466
|
-
task
|
|
653
|
+
task: sandbox &&
|
|
654
|
+
checkoutRepository &&
|
|
655
|
+
sandboxRepository &&
|
|
656
|
+
mountSandboxRelayfile &&
|
|
657
|
+
sandbox.outcome === 'provisioned'
|
|
658
|
+
? `${task}\n\nAgent Relay sandbox context: Relayfile records are available at ${sandbox.relayfileMountPath ?? '/workspace'}. The source checkout is separate; use ${workerCwd ?? 'the worker checkout'} for repository files and the mount for Relayfile records.`
|
|
659
|
+
: liveSandboxContext
|
|
660
|
+
? `${task}\n\n${liveSandboxContext}`
|
|
661
|
+
: task,
|
|
467
662
|
...(channel ? { channels: [channel] } : {}),
|
|
468
663
|
...(model ? { model } : {}),
|
|
469
664
|
...(workerCwd ? { worker_cwd: workerCwd } : {}),
|
|
@@ -485,19 +680,15 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
485
680
|
...(sandbox
|
|
486
681
|
? {
|
|
487
682
|
sandbox: printableSandbox,
|
|
488
|
-
attachCommand:
|
|
489
|
-
`--node ${shellQuote(targetNode)} --mode drive` +
|
|
490
|
-
((sandbox.outcome === 'provisioned' || sandbox.outcome === 'reused') &&
|
|
491
|
-
sandbox.relaycastTarget
|
|
492
|
-
? ` --base-url ${shellQuote(sandbox.relaycastTarget.baseUrl)}`
|
|
493
|
-
: ''),
|
|
683
|
+
attachCommand: sandboxAttachCommand(name, attachProjectRoot),
|
|
494
684
|
}
|
|
495
685
|
: {}),
|
|
496
686
|
invocation: spawnInvocationWithMergedPlacement(invocation),
|
|
497
687
|
});
|
|
498
688
|
}
|
|
499
689
|
catch (error) {
|
|
500
|
-
if (
|
|
690
|
+
if (shouldCleanupSandbox &&
|
|
691
|
+
sandbox?.outcome === 'provisioned' &&
|
|
501
692
|
!(error instanceof RelayPlacementError && error.state === 'unconfirmed_may_be_running')) {
|
|
502
693
|
await deps
|
|
503
694
|
.deleteCloudFleetSandbox({
|
|
@@ -530,6 +721,31 @@ export function registerFleetCommands(program, overrides = {}) {
|
|
|
530
721
|
throw new Error('--session-ref requires --node or --target-node.');
|
|
531
722
|
}
|
|
532
723
|
const persona = optionalText(options.persona, 'Persona');
|
|
724
|
+
if (!automaticPlacement) {
|
|
725
|
+
if (organization || project || workstream || role || objective) {
|
|
726
|
+
throw new Error('Workforce metadata requires --auto-place, --node, or --sandbox.');
|
|
727
|
+
}
|
|
728
|
+
const callerCwd = deps.cwd();
|
|
729
|
+
const localCwd = path.resolve(callerCwd, requestedCwd ?? '.');
|
|
730
|
+
const local = await deps.connectLocalBroker(deps.findProjectRoot(callerCwd));
|
|
731
|
+
try {
|
|
732
|
+
await spawnAgentWithClient(local, {
|
|
733
|
+
name,
|
|
734
|
+
cli,
|
|
735
|
+
task,
|
|
736
|
+
channels: [channel ?? 'general'],
|
|
737
|
+
...(model ? { model } : {}),
|
|
738
|
+
// A broker can be shared by nested packages. Never inherit its
|
|
739
|
+
// startup directory when the caller requested a local checkout.
|
|
740
|
+
cwd: localCwd,
|
|
741
|
+
});
|
|
742
|
+
printJson(deps.sdk, { local: { name, cli, cwd: localCwd } });
|
|
743
|
+
}
|
|
744
|
+
finally {
|
|
745
|
+
local.disconnect();
|
|
746
|
+
}
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
533
749
|
const workspace = deps.createFleetWorkspaceClient(clientOptions);
|
|
534
750
|
const invocation = await workspace.agents.spawn({
|
|
535
751
|
name,
|
|
@@ -635,6 +851,10 @@ function optionalTextList(value, label) {
|
|
|
635
851
|
function shellQuote(value) {
|
|
636
852
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
637
853
|
}
|
|
854
|
+
function sandboxAttachCommand(name, projectRoot) {
|
|
855
|
+
const attach = `agent-relay node agent attach ${shellQuote(name)} --mode drive`;
|
|
856
|
+
return projectRoot ? `cd ${shellQuote(projectRoot)} && ${attach}` : attach;
|
|
857
|
+
}
|
|
638
858
|
/**
|
|
639
859
|
* Warn (on stderr, so it never pollutes the JSON on stdout) when the workspace
|
|
640
860
|
* key was inferred from the project's persisted session rather than named
|