Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,19 @@ All notable changes to Agent Relay will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [Unreleased - Major]

### Changed

- `fleet spawn --sandbox` inside a repository starts from an exact shallow Git checkout without waiting for Relayfile. `--checkout` also omits Relayfile unless context is explicitly requested.

### Added

- `fleet relayfile attach` adds optional live context to an existing sandbox without restarting its worker.

### Migration Guidance

- Add `--sandbox-relayfile` to retain the previous required live repository mirror. Combine it with `--checkout` when both a Git checkout and live context must be ready before dispatch.

## [13.4.1] - 2026-10-12

Expand Down
72 changes: 44 additions & 28 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,19 +346,22 @@ sandbox is selected. Ambient credentials and persisted Cloud routing do not
change the local default. Workforce reporting metadata requires remote placement.
Automatic placement and release need only the workspace key.

The sandbox path provisions a fresh hosted instance and makes the Relayfile
mount mandatory by default. Inside a GitHub checkout, Relay infers the Git root,
repository identity, exact `HEAD`, and caller-relative directory. Cloud uses the
pinned workspace's connected GitHub credential to seed that revision into
Relayfile, and the worker starts in the decoded source tree under
`/workspace/github/repos/<owner>/<repo>/contents`. The long-running Relayfile
daemon keeps the mounted source tree synchronized with the workspace while
GitHub push events update the workspace's repository source. Use `--checkout`
when a task needs a separate static Git clone; that mode keeps the live
Relayfile mirror available separately. Use `--sandbox-provider daytona` or
`--sandbox-provider e2b` to require an operator-enabled provider; omit the flag
to let Cloud's sandbox router choose. Pass `--no-sandbox-relayfile` only when a
deliberately bare sandbox is desired. If provisioning times out or the spawn
The sandbox path provisions a fresh hosted instance. Inside a GitHub checkout,
Relay infers the Git root, repository identity, exact pushed `HEAD`, and caller-relative
directory. Cloud makes a depth-1 clone, fetches that exact SHA, verifies `HEAD`,
and starts the worker under `/srv/agent-workforce/<repo>`. Repository tasks start
without waiting for Relayfile context. `--checkout` explicitly selects the same
static mode. Outside Git, the default still requires the workspace Relayfile mount.

Use `--sandbox-relayfile` to require live context before dispatch. Without
`--checkout`, this retains the live repository mirror under
`/workspace/github/repos/<owner>/<repo>/contents`; with `--checkout`, it mounts
context separately from the Git clone. An explicit `--sandbox-relayfile-path`
also requires its selected context before startup. `--no-sandbox-relayfile`
explicitly disables mounting (combine with `--checkout` to clone repository files).
Use `--sandbox-provider daytona` or `--sandbox-provider e2b` to require an
operator-enabled provider; omit the flag to let Cloud's sandbox router choose.
If provisioning times out or the spawn
fails, Relay asks Cloud to delete the newly created sandbox. Runs without a
custom name use one `sbx_<UUID>` identity and the matching
`fleet-sandbox-<UUID>` node name. Legacy custom `--sandbox-name` values remain supported when no
Expand All @@ -380,7 +383,7 @@ reachable from a configured GitHub remote. This prevents a remote worker from
silently starting at a different revision. Commit and push local work before
retrying when Relay reports dirty files or an unreachable commit.

With `--checkout`, sandbox provisioning also clones the attested `HEAD` under
In default repository mode or with `--checkout`, sandbox provisioning clones the attested `HEAD` under
`/srv/agent-workforce/<repo>`. The checkout must have no tracked changes or
untracked source files, and the exact commit must be reachable from an origin
remote. Relay's generated `.agentworkforce/relay/workspace-key.json`,
Expand All @@ -403,7 +406,7 @@ release commands reuse the persisted project route; if that remote session is
unavailable, the command reports the routing failure instead of selecting a
same-named local worker.

From a clean repository already pinned to a Relay workspace, the ordinary live
From a clean repository already pinned to a Relay workspace, the checkout-first
path is:

```bash
Expand All @@ -416,31 +419,44 @@ agent-relay fleet agent list
agent-relay fleet release cloud-zero-config
```

Invoking the live command from `packages/web` starts the worker at
`/workspace/github/repos/<owner>/<repo>/contents/packages/web`. The repository
source metadata is available beside `contents` under `.relayfile`, `.skills`
is mounted from the same workspace, and no `.git` directory is written into the
Relayfile mirror.
Invoking the command from `packages/web` starts the worker at
`/srv/agent-workforce/<repo>/packages/web`, with `.git` available and `HEAD`
pinned to the requested revision.

For a static Git checkout, opt in explicitly:
Attach optional context later using `cloudWorkspaceId` and `sandboxId` from the
spawn response (this uses Cloud sign-in):

```bash
agent-relay fleet spawn codex \
--name cloud-checkout \
--task "Inspect this repository and report its current commit" \
--sandbox \
--checkout
agent-relay fleet relayfile attach \
--cloud-workspace-id <Cloud-workspace-UUID> \
--sandbox-id <sbx_UUID> \
--path '/linear' '/.skills'
```

Omit `--path` for the full workspace. For a dev sandbox, add
`--cloud-api-url https://dev.agentrelay.com/cloud`. The command waits at most
five minutes by default, reports the separate mount path, and does not restart
the worker or retry a failed attachment. Pending detached preparation is polled
against the same sandbox within that shared deadline. Tell an already running worker the
returned path when its task needs that context. The mount uses its own directory;
it never overlays the Git checkout. Attachment failure leaves the repository task
running.

For a live repository mirror that must be ready before the task starts, add
`--sandbox-relayfile` to spawn. Its decoded tree contains no `.git`; its source
metadata sits beside `contents` under `.relayfile`. Use
`--checkout --sandbox-relayfile` for a Git checkout plus required live context.

In checkout mode, invoking spawn from `packages/web` places the worker in that
same relative directory in the remote clone. In both modes, private repositories
use the pinned workspace's connected GitHub access; a repository-access error
means that connection must be granted access to the repository. No GitHub token
or workspace key needs to be copied into the task, mount, or checkout.

With `--checkout`, the Git checkout and Relayfile mirror are separate trees. In
With `--checkout --sandbox-relayfile`, the Git checkout and Relayfile mirror are
separate trees. Checkout-only mode has no Relayfile mirror or workspace skills. In
the ordinary live mode, source files are decoded from Relayfile records without
placing `.git` in the mirror. Workspace `.skills` are exposed through the agent
placing `.git` in the mirror. When a live mount is requested at startup, workspace `.skills` are exposed through the agent
CLIs' usual skill directories, and the worker's task context identifies the
mirror and exact source revision.

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/cli/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const expectedLeafCommands = [
// fleet (serve is a hidden error stub; config/enable/disable/inherit are hidden no-ops)
'fleet agent list',
'fleet nodes list',
'fleet relayfile attach',
'fleet release',
'fleet spawn',
'fleet spawn-status',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ describe('fleet CLI lifecycle routing', () => {
'spawn',
'codex',
'--sandbox',
'--sandbox-relayfile',
'--sandbox-provider',
'agent37',
'--sandbox-id',
Expand Down
122 changes: 122 additions & 0 deletions packages/cli/src/cli/commands/fleet-sandbox-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function registerSandboxCommand(overrides: {
materializeCloudRelayfileRepository?: ReturnType<typeof vi.fn>;
ensureCloudFleetSandbox: ReturnType<typeof vi.fn>;
placement?: ReturnType<typeof vi.fn>;
attachCloudFleetSandboxRelayfile?: ReturnType<typeof vi.fn>;
}): Command {
const placement = overrides.placement ?? vi.fn(async () => ({ invocationId: 'inv_test' }));
const register = vi.fn(async () => ({ token: 'at_live_launcher' }));
Expand Down Expand Up @@ -70,6 +71,9 @@ function registerSandboxCommand(overrides: {
? { materializeCloudRelayfileRepository: overrides.materializeCloudRelayfileRepository as never }
: {}),
ensureCloudFleetSandbox: overrides.ensureCloudFleetSandbox as never,
...(overrides.attachCloudFleetSandboxRelayfile
? { attachCloudFleetSandboxRelayfile: overrides.attachCloudFleetSandboxRelayfile as never }
: {}),
resolveWorkspaceSelection: () => ({
key: 'rk_live_workspace',
source: 'project',
Expand Down Expand Up @@ -109,6 +113,123 @@ function materialization(revision: string) {
}

describe('fleet sandbox command regressions', () => {
it.each([{ extra: [] }, { extra: ['--checkout'] }])(
'starts an exact repository checkout without waiting for Relayfile (%j)',
async ({ extra }) => {
const projectRoot = temporaryDirectory('relay-fleet-checkout-first-');
const selection = {
repository: 'AgentWorkforce/relay',
repositoryName: 'relay',
revision: REVISION,
projectRoot,
repositoryRelativeCwd: 'packages/cli',
workerCwd: '/srv/agent-workforce/relay/packages/cli',
};
// Any materialization call hangs forever: this task must dispatch without it.
const materialize = vi.fn(() => new Promise(() => {}));
const ensure = vi.fn(async () => ({
outcome: 'provisioned' as const,
providerId: 'agent37' as const,
cloudWorkspaceId: 'cloud-workspace',
nodeId: 'node-checkout',
nodeName: 'checkout-node',
sandboxId: 'sandbox-checkout',
providerSandboxId: 'provider-checkout',
relayWorkspaceId: TARGET.workspaceId,
relaycastTarget: TARGET,
relayfileMounted: false,
repoRevisions: { 'AgentWorkforce/relay': REVISION },
}));
const placement = vi.fn(async () => ({ invocationId: 'inv_checkout' }));
const program = registerSandboxCommand({
projectRoot,
resolveSandboxRepository: () => selection,
materializeCloudRelayfileRepository: materialize,
ensureCloudFleetSandbox: ensure,
placement,
});
await program.parseAsync(
[
'fleet',
'spawn',
'codex',
'--sandbox',
'--sandbox-provider',
'agent37',
'--workspace-id',
TARGET.workspaceId,
'--name',
'checkout-worker',
'--task',
'Read the repository',
...extra,
],
{ from: 'user' }
);
expect(materialize).not.toHaveBeenCalled();
expect(ensure).toHaveBeenCalledWith(
expect.objectContaining({
mountRelayfile: false,
repos: ['AgentWorkforce/relay'],
repoRevisions: { 'AgentWorkforce/relay': REVISION },
}),
expect.anything()
);
expect(placement).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
worker_cwd: selection.workerCwd,
task: 'Read the repository',
}),
})
);
}
);

it('attaches optional context without re-provisioning or dispatching the repository task', async () => {
const ensure = vi.fn();
const placement = vi.fn();
const attach = vi.fn(async () => ({
sandboxId: REPLAY_SANDBOX_ID,
relayfileMounted: true,
relayWorkspaceId: TARGET.workspaceId,
relayfileMountPath: '/home/node/workspace',
}));
const program = registerSandboxCommand({
projectRoot: temporaryDirectory('relay-fleet-late-context-'),
resolveSandboxRepository: vi.fn(),
ensureCloudFleetSandbox: ensure,
placement,
attachCloudFleetSandboxRelayfile: attach,
});
await program.parseAsync(
[
'fleet',
'relayfile',
'attach',
'--cloud-workspace-id',
'50587328-441d-4acb-b8f3-dbe1b3c5de99',
'--sandbox-id',
REPLAY_SANDBOX_ID,
'--path',
'/linear',
'--cloud-api-url',
'https://dev.agentrelay.com/cloud',
],
{ from: 'user' }
);
expect(attach).toHaveBeenCalledExactlyOnceWith(
{
cloudWorkspaceId: '50587328-441d-4acb-b8f3-dbe1b3c5de99',
sandboxId: REPLAY_SANDBOX_ID,
paths: ['/linear'],
},
{ timeoutMs: 300000, apiUrl: 'https://dev.agentrelay.com/cloud' }
);
expect(ensure).not.toHaveBeenCalled();
expect(placement).not.toHaveBeenCalled();
});

it('documents local repo-relative cwd inference and absolute remote cwd overrides', () => {
const program = registerSandboxCommand({
projectRoot: temporaryDirectory('relay-fleet-help-'),
Expand Down Expand Up @@ -233,6 +354,7 @@ describe('fleet sandbox command regressions', () => {
'spawn',
'codex',
'--sandbox',
'--sandbox-relayfile',
'--sandbox-provider',
'agent37',
'--sandbox-id',
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/cli/commands/fleet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,7 @@ describe('fleet command support', () => {
'spawn',
'codex',
'--sandbox',
'--sandbox-relayfile',
'--sandbox-provider',
'agent37',
'--name',
Expand Down Expand Up @@ -1556,6 +1557,7 @@ describe('fleet command support', () => {
'spawn',
'codex',
'--sandbox',
'--sandbox-relayfile',
'--sandbox-provider',
'agent37',
'--name',
Expand Down Expand Up @@ -2445,7 +2447,7 @@ describe('fleet command support', () => {
expect.objectContaining({
input: expect.objectContaining({
worker_cwd: '/srv/agent-workforce/cloud/packages/web',
task: expect.stringContaining('Relayfile records are available at /workspace'),
task: 'Review the Cloud web package',
}),
})
);
Expand Down
Loading
Loading