Skip to content
Draft
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
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Typed personas can be deployed directly, or compiled first when you need a
portable JSON artifact:

```bash
agentworkforce deploy ./examples/review-agent/persona.ts --mode dev --dry-run
agentworkforce deploy ./examples/review-agent/persona.ts --mode local --dry-run
agentworkforce persona compile ./examples/review-agent/persona.ts
```

Expand All @@ -47,10 +47,10 @@ export DAYTONA_API_KEY=...
workforce deploy ./examples/weekly-digest/persona.json --sandbox --byo-sandbox
```

For local iteration, run it in dev mode:
For local iteration, run it in local mode:

```bash
BRAVE_API_KEY=... workforce deploy ./examples/weekly-digest/persona.json --dev
BRAVE_API_KEY=... workforce deploy ./examples/weekly-digest/persona.json --mode local
```

The example searches Brave on a weekly cron schedule, clusters findings, and
Expand Down Expand Up @@ -421,3 +421,23 @@ console.log(selection.personaId, selection.tier);

For lower-level primitives, see
[`packages/workload-router/README.md`](./packages/workload-router/README.md).

### Interactive sandbox sessions

`agentworkforce agent <persona> --mode local` is the default interactive launch.
`agentworkforce agent <persona> --mode sandbox` selects a Cloud sandbox session.
Use `--sandbox-provider daytona|e2b`, `--sandbox-id <id>` to replay an identity,
`--attach-mode view|drive` (default `drive`), and `--byo-sandbox` for BYO auth.
Run `agentworkforce login` first to select an active workspace. Ctrl-C stops the
session and waits for sandbox cleanup. `agent --mode cloud` is invalid; use
`agentworkforce deploy <persona> --mode cloud` for a hosted service.

Interactive sandbox launch requires the Relay SDK `/fleet` and `/attach`
contracts. They are not yet exported by the published Relay SDK checked during
this implementation; until that dependency ships, the command reports the
missing SDK contract. Read-only mount enforcement also requires Cloud support.

For hosted services, use `deploy --mode local|sandbox|cloud`. The previous
`deploy --mode dev` spelling still works and warns; it will be removed in the
next minor release. Existing `devLauncher` and `resolvers.modes.dev` library
callers have the same deprecation window.
2 changes: 2 additions & 0 deletions docs/plans/deploy-v1-workflow-spec.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> Naming update: deploy mode `dev` is now `local`; `dev` remains a deprecated alias for one minor release. This historical plan retains its original terminology.

# Ricky workflow spec — `workforce deploy` v1 cross-repo work

**Status:** ready for Ricky to generate + run a workflow.
Expand Down
2 changes: 2 additions & 0 deletions docs/plans/deploy-v1.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> Naming update: deploy mode `dev` is now `local`; `dev` remains a deprecated alias for one minor release. This historical plan retains its original terminology.

# Plan — `workforce deploy` v1

Status: draft for review
Expand Down
2 changes: 1 addition & 1 deletion examples/linear-shipper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This deployable persona follows the paraglide pattern: a Linear issue triggers a
Connect Linear and GitHub before deploying.

```bash
workforce deploy ./examples/linear-shipper/persona.json --mode dev
workforce deploy ./examples/linear-shipper/persona.json --mode local
```

Set the target repository through the persona inputs: `GITHUB_OWNER`, `GITHUB_REPO`, and `REPO_URL`.
Expand Down
14 changes: 7 additions & 7 deletions examples/proactive-issue-resolver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Local agent that turns a GitHub issue into a spec, hands the spec to the
SDK to generate + run a workflow that opens a PR via
`@agent-relay/github-primitive`, and DMs the result to Slack.

**Important runtime note:** workforce `--mode dev` does NOT subscribe to live
**Important runtime note:** workforce `--mode local` does NOT subscribe to live
GitHub events. The runtime reads NDJSON envelopes from stdin
(`packages/runtime/src/runner.ts:184`); live event ingress is a `--mode cloud`
feature that isn't wired up yet. So v1 is **manually-triggered per-issue** via
Expand All @@ -23,7 +23,7 @@ gates that go with it) lives in [`SPEC.md`](./SPEC.md).
trigger-issue.sh owner repo N
→ gh api repos/owner/repo/issues/N (fetch real issue)
→ wrap as github.issues.opened envelope (NDJSON)
→ pipe → agentworkforce deploy --mode dev (runner consumes one envelope)
→ pipe → agentworkforce deploy --mode local (runner consumes one envelope)
→ handler claims issue (`gh issue edit --add-label ricky-claimed`)
→ handler comments :robot: on the issue
→ claude harness investigates repo + writes spec.md
Expand Down Expand Up @@ -86,7 +86,7 @@ What happens:

1. `gh` fetches issue #123 and the repo metadata.
2. The script wraps both in a `github.issues.opened` envelope and pipes it to
`agentworkforce deploy ... --mode dev`.
`agentworkforce deploy ... --mode local`.
3. The handler adds the `ricky-claimed` label to issue #123 and acquires a
deterministic Git ref lock before dispatch.
4. Handler comments `:robot: Proactive agent picked up #123. Investigating…`.
Expand All @@ -101,7 +101,7 @@ What happens:
REPO_ROOT=$(git rev-parse --show-toplevel)
agentworkforce deploy \
"$REPO_ROOT/examples/proactive-issue-resolver/persona.json" \
--mode dev --dry-run
--mode local --dry-run
```

Should print `ok: proactive-issue-resolver (dry-run)`. The persona is checked
Expand All @@ -112,14 +112,14 @@ against this output today.
- `agentworkforce` CLI v3.0.14 installed at `~/.local/share/mise/installs/node/22.22.1/bin/agentworkforce`.
- `~/.agentworkforce/active.json` shows an active workspace.
- `gh auth status` shows logged-in `khaliqgant` with `repo` scope.
- `agentworkforce deploy ... --mode dev --dry-run` returns `ok`.
- `agentworkforce deploy ... --mode local --dry-run` returns `ok`.
- Handler signature matches `packages/runtime/src/types.ts:259`:
`handler((ctx, event) => ...)`.
- Envelope shape matches `RawGatewayEnvelope` in
`packages/runtime/src/shim.ts:17`; type `github.issues.opened` splits to
source=github, type=issues.opened, payload=resource.
- `--mode dev` pipes parent stdin to runner stdin
(`packages/deploy/src/modes/dev.ts:57`), so single-envelope stdin pipe →
- `--mode local` pipes parent stdin to runner stdin
(`packages/deploy/src/modes/local.ts:57`), so single-envelope stdin pipe →
single dispatch → runner exits.
- esbuild bundles `@agentworkforce/ricky` inline; `@agentworkforce/runtime`
stays external (`packages/deploy/src/bundle.ts:62`).
Expand Down
2 changes: 1 addition & 1 deletion examples/proactive-issue-resolver/specs/cloud-mode-stub.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ The change must:
`ricky.generateCloudWorkflow`, which returns the `runtime-not-wired` stub
response; that surfaces as a `:x:` Slack hard-fail with the cloud error
payload in the failure detail. No PR is opened.
- `agentworkforce deploy ... --mode dev --dry-run` still returns `ok`.
- `agentworkforce deploy ... --mode local --dry-run` still returns `ok`.

## Out of scope

Expand Down
4 changes: 2 additions & 2 deletions examples/proactive-issue-resolver/trigger-issue.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# trigger-issue.sh — manually drive the proactive-issue-resolver persona
# against a single, named GitHub issue.
#
# `agentworkforce deploy --mode dev` reads NDJSON envelopes from stdin. This
# `agentworkforce deploy --mode local` reads NDJSON envelopes from stdin. This
# script fetches a real issue via `gh`, wraps it in a `github.issues.opened`
# envelope, and pipes it to one deploy invocation. The runner processes the
# one envelope, then exits when stdin closes — so the script is short-lived,
Expand Down Expand Up @@ -98,4 +98,4 @@ ENVELOPE=$(jq -n \
}')

echo "→ piping envelope into agentworkforce deploy (cwd=$(pwd))"
printf '%s\n' "$ENVELOPE" | agentworkforce deploy "$PERSONA" --mode dev
printf '%s\n' "$ENVELOPE" | agentworkforce deploy "$PERSONA" --mode local
2 changes: 1 addition & 1 deletion examples/review-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Connect GitHub and Slack before deploying. Because `useSubscription` is enabled,
⚠️ **Memory is not wired.** `ctx.memory` is a stub in v1; see `docs/plans/deploy-v1-schema-cascade-spec.md` § Loud hole. Memory wiring lands in a follow-up workflow (not yet specced).

```bash
workforce deploy ./examples/review-agent/persona.json --mode dev
workforce deploy ./examples/review-agent/persona.json --mode local
```

## Events
Expand Down
2 changes: 1 addition & 1 deletion examples/weekly-digest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ workforce deploy ./examples/weekly-digest/persona.json \

# Run locally as a long-lived process; pipe an envelope on stdin to fire
# the handler immediately. The runner exits when stdin closes.
workforce deploy ./examples/weekly-digest/persona.json --mode dev
workforce deploy ./examples/weekly-digest/persona.json --mode local
```

## Firing the handler manually
Expand Down
1 change: 1 addition & 0 deletions impl/branch.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
feat/sandbox-session-1789241979891
1 change: 1 addition & 0 deletions impl/summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add the workforce side of interactive persona sandbox sessions: a deterministic persona-to-sandbox parameter mapper, shared authentication and idempotent cleanup helpers, a socket-backed interactive launcher with detach/stop lifecycle handling, CLI sandbox flags and workspace dispatch, and a package-wide guard against invoking the agent-relay executable. Rename hosted deployment mode dev to local while retaining deprecated option/resolver/export aliases, update consumers and documentation, and add mapping, real UNIX-socket, CLI dispatch, and compatibility regression coverage. Production sandbox launch remains blocked on a published Relay SDK exposing the specified fleet/attach contracts and Cloud read-only mount enforcement; no shell fallback or fabricated dependency version is introduced.
47 changes: 47 additions & 0 deletions impl/upstream-blockers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Release blockers

The requested branch contains the workforce changes. It is not ready for the
SPEC's production definition-of-done or a ready-to-merge PR.

1. The npm registry queried on 2026-09-12 reports both `@agent-relay/cloud` and
`@agent-relay/sdk` latest as `12.1.0`. Neither package exports `./fleet` or
`./attach`. The existing workforce dependency remains `^10.1.0`; there is
no verified release to bump to. `pnpm install` leaves the lockfile unchanged.
`sandbox-interactive.ts` has an explicit, lazy SDK dependency boundary so
local and hosted commands remain loadable; production interactive launch
throws an actionable error until the upstream exports are published. Its
local types describe the requested contract; they are not evidence of a
working published SDK. Replace this boundary with typed direct imports and
the verified release bump as part of the upstream integration gate.
2. The local Relay checkout's `FleetNodeAttachProxy` actually exposes
`brokerUrl`, `apiKey`, `requestTimeoutMs`, and `close()`. It does not expose
`socketPath` or `finished`. A thin re-export alone cannot implement the
SPEC: Relay must supply an actual terminal-to-UNIX-socket adapter, harness
execution/exit handling, and the fleet spawn wrapper before publication.
3. The plan assigns Relay SDK implementation, its tests, and the 1630 live
roundtrip to a separate Relay PR that must merge and publish first. Those
changes have not been made in this workforce checkout or published.
4. Cloud must accept and enforce `readonlyPaths` with chmod-444 semantics.
See `packages/persona-repo-router/skills/persona-relayfile-mount.md:66-72`.
Forwarding the paths in contract tests does not establish live enforcement.
5. The installed `flows check` rejects the supplied `wire-up.flow.ts` as
invalid YAML/JSON. That is a tool/flow format incompatibility outside this
checkout. The check was attempted; it did not pass.
6. The full repository test gate is not green: Node 22 fails existing runtime
version requirements; a supplemental Node 26 run also failed unchanged
runtime tests and was stopped. See `impl/validation.md`.
7. No live sandbox/read-only roundtrip, cloud service rollout, landing-page
update, remote CI run, or PR creation is claimed. The surrounding wire-up
flow assigns PR publication to a separate deterministic shipper step.

Before rollout: publish and verify Relay's exports and request/response types,
bump both workforce consumers and the lockfile, verify real persona terminal
execution and auth modes, pass the opt-in smoke and Relay 1630 live test,
resolve the flow checker format, and obtain green CI.

Implementation clarification: the existing sandbox launcher executes a Node
handler, and `PersonaSpec` explicitly permits such handlers to omit `harness`
or set `sandbox: false` for their internal runtime. Applying interactive
eligibility validation to them would be a regression. A shared pure
`personaToSandboxContext` supplies the same identity/env derivation to those
handlers; interactive launches still reject all three specified invalid shapes.
42 changes: 42 additions & 0 deletions impl/validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Validation

- `pnpm install`: passed; lockfile unchanged because no published SDK release
satisfies the required exports.
- `pnpm -r build`: passed.
- `pnpm run typecheck`: passed, including examples.
- `pnpm run lint`: passed.
- Persona mapper focused suite: 11 passed.
- Complete deploy suite after final fixes: 292 passed, 1 skipped (opt-in live
Daytona smoke). Includes real UNIX-socket byte piping, attach timing,
stop/detach, cleanup errors, and unchanged Node handler mint payload.
- Complete local-surface suite: 14 passed.
- Focused CLI/parser/dispatch/runtime-picker suites: 127 passed.
- The complete CLI suite under Node 22 also failed 30 existing invocation/
permission tests plus the existing config-directory whitespace test; the Node
26 root rerun stopped earlier in runtime, so it did not reach the full CLI suite.
- Root static tests include a package-wide no-agent-relay-shell scan and a
non-vacuous matcher test. They pass.
- Initial `pnpm run test` under default Node 22.22.2 failed in 14 existing
runtime local-preview tests, which require Node >=26.3.1. A supplemental
rerun used the already-installed Node 26.8.2 with a command-local PATH
override. Its release-workflow checks passed, but unchanged local-preview
and broker-log tests also failed; the slower rerun was stopped after those
failures were confirmed. The repository test gate is NOT green. The root
agent-card E2E stage was not reached.
- Installed Relay `/fleet` import check: fails with
`ERR_PACKAGE_PATH_NOT_EXPORTED`, consistent with registry metadata for
latest `@agent-relay/cloud@12.1.0` and `@agent-relay/sdk@12.1.0` lacking both
required subpaths.
- `flows check ../flows/examples/agentworkforce-sandbox-session/wire-up.flow.ts`:
failed with `REFUSED [invalid_spec] ... contains invalid YAML or JSON`.
- Veto diff review: `warn`; host-supplied specialist review identified the
missing published SDK as a high-severity release dependency and unverified
Cloud read-only enforcement as a medium-severity gate. No live secrets found.
MCP sampling was unavailable; this was a host review submitted through Veto,
not an independent agent review. No subagents were spawned.
- Final deploy typecheck includes the updated live-smoke credential gate.
- `git diff --check` and staged diff whitespace check: passed.

Live sandbox execution and Relay's 1630 readonly roundtrip have not run. No
production rollout, remote CI success, or PR creation is claimed. See
`impl/upstream-blockers.md` for the concrete remaining cross-repo requirements.
20 changes: 20 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1244,3 +1244,23 @@ If a persona uses MCP, use `claude` or `codex` tiers.
- **Local file silently missing from the list** — Scroll up for a
`warning: [layer] file.json: …` line. Common causes: invalid JSON, `id`
missing, or `extends` pointing at something that isn't in a lower layer.

### Interactive sandbox sessions

`agentworkforce agent <persona> --mode local` is the default interactive launch.
`agentworkforce agent <persona> --mode sandbox` selects a Cloud sandbox session.
Use `--sandbox-provider daytona|e2b`, `--sandbox-id <id>` to replay an identity,
`--attach-mode view|drive` (default `drive`), and `--byo-sandbox` for BYO auth.
Run `agentworkforce login` first to select an active workspace. Ctrl-C stops the
session and waits for sandbox cleanup. `agent --mode cloud` is invalid; use
`agentworkforce deploy <persona> --mode cloud` for a hosted service.

Interactive sandbox launch requires the Relay SDK `/fleet` and `/attach`
contracts. They are not yet exported by the published Relay SDK checked during
this implementation; until that dependency ships, the command reports the
missing SDK contract. Read-only mount enforcement also requires Cloud support.

For hosted services, use `deploy --mode local|sandbox|cloud`. The previous
`deploy --mode dev` spelling still works and warns; it will be removed in the
next minor release. Existing `devLauncher` and `resolvers.modes.dev` library
callers have the same deprecation window.
66 changes: 66 additions & 0 deletions packages/cli/src/agent-sandbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { PassThrough } from 'node:stream';
import type { InteractiveSandboxInput } from '@agentworkforce/deploy';
import { parseAgentArgs, runAgentSandbox } from './cli-impl.js';

const persona: InteractiveSandboxInput['persona'] = { id: 'demo', intent: 'documentation', description: '', tags: [], skills: [], harness: 'claude', harnessSettings: { reasoning: 'medium', timeoutSeconds: 300 } };
function fixture() {
const exits: number[] = [];
const processLike = Object.assign(new EventEmitter(), {
stdin: new PassThrough(), stdout: new PassThrough(), stderr: new PassThrough(), exit: (code: number) => { exits.push(code); },
});
let stops = 0;
const calls: InteractiveSandboxInput[] = [];
const deps = {
processLike,
resolveWorkspace: async (): Promise<string | undefined> => 'workspace',
launchInteractiveSandbox: async (input: InteractiveSandboxInput) => {
calls.push(input);
return { sandboxId: 'sb', nodeId: 'node', attached: Promise.resolve(), finished: Promise.resolve(3), detach: async () => {}, stop: async () => { stops++; } };
},
};
return { deps, calls, exits, stops: () => stops };
}
for (const byo of [false, true]) {
test(`agent sandbox dispatch forwards stdio and options (${byo ? 'BYO' : 'managed'})`, async () => {
const f = fixture();
const { flags } = parseAgentArgs(['--mode=sandbox', '--sandbox-provider=e2b', '--sandbox-id=sb', '--attach-mode=view', ...(byo ? ['--byo-sandbox'] : [])]);
await runAgentSandbox(persona, flags, f.deps);
assert.equal(f.calls.length, 1);
const call = f.calls[0];
assert.equal(call.persona, persona); assert.equal(call.workspace, 'workspace');
assert.equal(call.authMode, byo ? 'byo' : 'managed'); assert.equal(call.provider, 'e2b');
assert.equal(call.sandboxId, 'sb'); assert.equal(call.attachMode, 'view');
for (const stream of ['stdin', 'stdout', 'stderr'] as const) assert.equal(call.stdio[stream], f.deps.processLike[stream]);
assert.deepEqual(f.exits, [3]); assert.equal(f.stops(), 1);
assert.equal(f.deps.processLike.listenerCount('SIGINT'), 0);
});
}
test('agent local dispatch never launches a sandbox or resolves workspace', async () => {
const f = fixture();
f.deps.resolveWorkspace = async () => { throw new Error('must not resolve'); };
await runAgentSandbox(persona, parseAgentArgs([]).flags, f.deps);
assert.equal(f.calls.length, 0);
});
test('agent sandbox requires active workspace with login hint', async () => {
const f = fixture(); f.deps.resolveWorkspace = async () => undefined;
await assert.rejects(runAgentSandbox(persona, parseAgentArgs(['--mode=sandbox']).flags, f.deps), /agentworkforce login/);
assert.equal(f.calls.length, 0);
});
test('agent sandbox signal stops once and awaits cleanup even if finished settles first', async () => {
const f = fixture();
let finish!: (code: number) => void;
const finished = new Promise<number>(resolve => { finish = resolve; });
let deleted = false; let stopCalls = 0;
f.deps.launchInteractiveSandbox = async () => {
setImmediate(() => { f.deps.processLike.emit('SIGINT'); f.deps.processLike.emit('SIGTERM'); });
return { sandboxId: 'sb', nodeId: 'n', attached: Promise.resolve(), finished, detach: async () => {}, stop: async () => {
stopCalls++; finish(3); await new Promise(resolve => setImmediate(resolve)); deleted = true;
} };
};
f.deps.processLike.exit = code => { assert.equal(deleted, true); f.exits.push(code); };
await runAgentSandbox(persona, parseAgentArgs(['--mode=sandbox']).flags, f.deps);
assert.equal(stopCalls, 1); assert.equal(f.exits.length, 1);
});
Loading
Loading