Skip to content

Commit 385ef88

Browse files
committed
Default simple persona launches to direct mode
1 parent 3a196d0 commit 385ef88

4 files changed

Lines changed: 186 additions & 115 deletions

File tree

‎packages/cli/CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Default simple interactive persona launches to direct unmounted mode; Relayfile mounts now engage only for explicit filesystem policy, sidecars/config files, or non-Claude skill installs that need repo write isolation.
13+
1014
## [3.0.14] - 2026-05-20
1115

1216
### Added

‎packages/cli/README.md‎

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -910,14 +910,22 @@ persona session, add it to the persona's `mcpServers` block.
910910
agentworkforce agent [--install-in-repo] [--no-launch-metadata] <persona>[@<tier>]
911911
```
912912

913-
By default, claude and opencode sessions run inside a sandbox mount — see
914-
[**Sandbox mount**](#sandbox-mount) below. `--install-in-repo` opts out.
913+
By default, simple prompt/MCP launches run directly in the real cwd with
914+
normal harness auth and session-scoped argv/config inputs. The CLI does not
915+
change `$HOME`, run Claude with `--bare`, or otherwise hide OAuth/keychain
916+
credentials in this direct path. A sandbox mount is created only when the
917+
persona needs filesystem mediation: declared `mount` rules, sidecar/config
918+
files that must appear in the harness cwd, or non-Claude skill installs that
919+
would otherwise write repo-relative artifacts. `--install-in-repo` opts out of
920+
session staging and lets installers write to the repo's conventional harness
921+
directories.
915922

916923
1. Resolves the persona, walks the cascade, resolves `$VAR` refs.
917-
2. **Stages skills outside the repo by default** (claude interactive only —
918-
see **Skill staging** below). For codex / opencode, or when
919-
`--install-in-repo` is passed, falls back to the legacy repo-relative
920-
install path (`.claude/skills/`, `.agents/skills/`, `.skills/`).
924+
2. **Stages skills outside the repo by default** for Claude interactive
925+
sessions — see **Skill staging** below. Codex/opencode skill installs still
926+
require a sandbox mount unless `--install-in-repo` is passed, because their
927+
installers write repo-relative paths (`.agents/skills/`, `.skills/`,
928+
lockfiles).
921929
3. Runs skill install (`prpm install …`) if the persona declares any skills,
922930
using the computed target (stage dir or repo).
923931
4. Execs the harness binary with stdio inherited:
@@ -1005,7 +1013,7 @@ the working tree, and the session only sees the skills the persona declares
10051013
**Opt-out — `--install-in-repo`:**
10061014

10071015
Pass `--install-in-repo` to fall back to the legacy behavior (skills land in
1008-
the repo's `.claude/skills/` directory, cleaned on exit):
1016+
the repo's harness directory, then are cleaned on exit):
10091017

10101018
```sh
10111019
agentworkforce install @agentworkforce/personas-core --persona code-reviewer
@@ -1018,24 +1026,33 @@ stage dir conflicts with something else (network filesystem, read-only
10181026

10191027
**Caveats for V1:**
10201028

1021-
- **Claude harness only.** codex and opencode continue to install into their
1022-
conventional repo-relative directories. The SDK throws if `installRoot` is
1023-
passed with a non-claude harness.
1029+
- **Claude installRoot only.** codex and opencode do not support
1030+
out-of-repo install roots yet. When they declare skills, the CLI uses a
1031+
sandbox mount by default so installer output stays out of the real repo.
10241032
- **No cache layer yet.** Every interactive session runs a fresh prpm install
10251033
into a new stage dir. A `~/.agentworkforce/workforce/cache/` content-addressed cache
10261034
is planned but not wired up.
10271035

10281036
## Sandbox mount
10291037

1030-
By default, claude and opencode interactive sessions run inside a
1038+
Interactive sessions normally avoid a filesystem mirror. The CLI creates a
10311039
[`@relayfile/local-mount`](https://www.npmjs.com/package/@relayfile/local-mount)
1032-
mount that hides repo-level harness configuration from the session, applies
1033-
the persona `mount` block plus Relayfile `.agentignore` / `.agentreadonly`
1034-
rules, and routes skill-install writes into the sandbox — so the model sees
1035-
persona context + user-level context, and only the project files the mount
1036-
exposes. Codex sessions never mount (no harness-side support).
1040+
only when a persona needs filesystem-level behavior:
10371041

1038-
`--install-in-repo` opts out and runs against the real cwd.
1042+
- `mount.ignoredPatterns` or `mount.readonlyPatterns`.
1043+
- Persona sidecars (`claudeMd` / `agentsMd`) that must be materialized as
1044+
`CLAUDE.md` / `AGENTS.md` without writing into the real repo.
1045+
- Harness config files such as opencode's per-session `opencode.json`.
1046+
- Codex/opencode skill installs, whose providers still write repo-relative
1047+
directories and lockfiles.
1048+
1049+
When the mount is active, it hides repo-level harness configuration from the
1050+
session, applies the persona `mount` block plus Relayfile `.agentignore` /
1051+
`.agentreadonly` rules, and routes sandbox-only writes away from the real
1052+
checkout.
1053+
1054+
`--install-in-repo` prevents the mount from being used for installer isolation
1055+
and runs installers against the real cwd.
10391056

10401057
The CLI reads these files from the project root before creating the mount:
10411058

@@ -1072,10 +1089,11 @@ the repo):
10721089
still load. The mount scrubs the *project*, not the user. To exclude
10731090
user-level context too, launch under a scratch `$HOME`.
10741091
- **Persona skills.** For claude, the `--plugin-dir` passed to the harness
1075-
resolves to an absolute path *outside* the mount, so staged skills from
1076-
`~/.agentworkforce/workforce/sessions/<id>/claude/plugin/` load normally. For
1077-
opencode, the install runs inside the mount so the writes land in the
1078-
sandbox.
1092+
resolves to an absolute path under
1093+
`~/.agentworkforce/workforce/sessions/<id>/claude/plugin/`, so simple
1094+
Claude personas do not need a mount just to load staged skills. For
1095+
codex/opencode, the install runs inside the mount when skills are declared
1096+
so repo-relative installer output lands in the sandbox.
10791097
- **Keychain auth.** The mount does not pass `--bare`; it only hides
10801098
files. Claude Code's macOS keychain login stays active.
10811099
- **Persona `mcpServers`.** Still passed via `--mcp-config` — unaffected
@@ -1089,9 +1107,10 @@ the repo):
10891107

10901108
### Session layout
10911109

1092-
Both the skill install root and the sandbox mount live under a single
1093-
session directory. The session id (`<personaId>-<base36-timestamp>-<hex>`)
1094-
is generated once and both paths are derived from it:
1110+
When a run needs session artifacts, the skill install root and any sandbox
1111+
mount live under a single session directory. The session id
1112+
(`<personaId>-<base36-timestamp>-<hex>`) is generated once and both paths are
1113+
derived from it:
10951114

10961115
```
10971116
~/.agentworkforce/workforce/
@@ -1105,22 +1124,23 @@ is generated once and both paths are derived from it:
11051124
└── <mirrored project tree, minus the hidden patterns>
11061125
```
11071126

1108-
`@relayfile/local-mount` handles mount creation, process spawn,
1109-
SIGINT/SIGTERM forwarding, write syncback, and cleanup on exit. The
1110-
agentworkforce CLI just wires the paths and passes the persona's argv.
1127+
`@relayfile/local-mount` handles mount creation, write syncback, and cleanup
1128+
when the conditional mount branch is active. Plain direct launches skip this
1129+
tree walk entirely and keep the harness's normal auth lookup path.
11111130

11121131
### Example
11131132

11141133
```sh
1115-
# Interactive persona session with the repo's CLAUDE.md, .claude/, and
1116-
# .mcp.json hidden — session sees the persona's staged skills plus your
1117-
# user-level ~/.claude/CLAUDE.md, nothing else from this repo.
1118-
agentworkforce install @agentworkforce/personas-core --persona code-reviewer
1119-
agentworkforce agent code-reviewer@best
1134+
# Interactive persona session with explicit filesystem policy. This uses
1135+
# Relayfile so the session sees only the paths allowed by the persona and
1136+
# project .agentignore/.agentreadonly rules.
1137+
agentworkforce install @agentworkforce/personas-core --persona proactive-agent-builder
1138+
agentworkforce agent proactive-agent-builder
11201139
```
11211140

1122-
On exit: mount is synced back to the real repo, then torn down; skill
1123-
stage dir is cleaned up by the existing `rm -rf` cleanup command.
1141+
On exit, mounted runs sync changes back to the real repo, then tear down the
1142+
mount; the skill stage dir is cleaned up by the existing `rm -rf` cleanup
1143+
command.
11241144

11251145
## Selecting a harness per tier
11261146

‎packages/cli/src/cli.test.ts‎

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ function trapExit(): ExitTrap {
8080
return trap;
8181
}
8282

83-
function writeStandaloneCodexPersona(workforceHome: string, id = 'local-codex'): string {
83+
function writeStandaloneCodexPersona(
84+
workforceHome: string,
85+
id = 'local-codex',
86+
extra: Record<string, unknown> = {}
87+
): string {
8488
const personaDir = join(workforceHome, 'personas');
8589
mkdirSync(personaDir, { recursive: true });
8690
writeFileSync(
@@ -93,7 +97,8 @@ function writeStandaloneCodexPersona(workforceHome: string, id = 'local-codex'):
9397
harness: 'codex',
9498
model: 'test-codex',
9599
systemPrompt: 'Run the local codex test harness.',
96-
harnessSettings: { reasoning: 'medium', timeoutSeconds: 30 }
100+
harnessSettings: { reasoning: 'medium', timeoutSeconds: 30 },
101+
...extra
97102
}),
98103
'utf8'
99104
);
@@ -371,34 +376,35 @@ test('parseCreateArgs: --save-default persists create target in source config',
371376
}
372377
});
373378

374-
test('decideCleanMode: claude defaults to mount (parity with opencode)', () => {
375-
// claude and opencode both default to the sandbox mount; the includeGit
376-
// path in relayfile 0.6 keeps `.git` in the mount so git operations work
377-
// inside it. --install-in-repo is the only opt-out.
378-
assert.deepEqual(decideCleanMode('claude'), { useClean: true });
379+
test('decideCleanMode: claude defaults to direct launch without a mount', () => {
380+
assert.deepEqual(decideCleanMode('claude'), { useClean: false });
379381
});
380382

381-
test('decideCleanMode: claude + --install-in-repo disengages mount', () => {
383+
test('decideCleanMode: claude + --install-in-repo stays unmounted', () => {
382384
assert.deepEqual(decideCleanMode('claude', true), { useClean: false });
383385
});
384386

385-
test('decideCleanMode: opencode defaults to mount (skills would otherwise land in repo)', () => {
386-
// Opencode has no installRoot support in the SDK, so the mount is the only
387-
// way to keep `.opencode/skills/`, `.agents/skills/`, prpm.lock, etc. out
388-
// of the real repo. Default-on for non-in-repo runs.
389-
assert.deepEqual(decideCleanMode('opencode'), { useClean: true });
387+
test('decideCleanMode: opencode mounts only when cwd config or skills require it', () => {
388+
assert.deepEqual(decideCleanMode('opencode'), { useClean: false });
389+
assert.deepEqual(decideCleanMode('opencode', { hasConfigFiles: true }), { useClean: true });
390+
assert.deepEqual(decideCleanMode('opencode', { installNeedsSandbox: true }), { useClean: true });
390391
});
391392

392393
test('decideCleanMode: opencode + --install-in-repo → no mount', () => {
393-
assert.deepEqual(decideCleanMode('opencode', true), { useClean: false });
394+
assert.deepEqual(
395+
decideCleanMode('opencode', { installInRepo: true, hasConfigFiles: true }),
396+
{ useClean: false }
397+
);
394398
});
395399

396-
test('decideCleanMode: codex defaults to mount (parity with claude/opencode)', () => {
397-
// All three harnesses default to the mount; --install-in-repo is the
398-
// single opt-out. Codex needs the mount so persona-supplied AGENTS.md
399-
// sidecars can be materialized without overwriting the user's real-cwd
400-
// copy, and so any per-session writes stay sandboxed.
401-
assert.deepEqual(decideCleanMode('codex'), { useClean: true });
400+
test('decideCleanMode: codex mounts only for filesystem-affecting persona features', () => {
401+
assert.deepEqual(decideCleanMode('codex'), { useClean: false });
402+
assert.deepEqual(decideCleanMode('codex', { hasSidecar: true }), { useClean: true });
403+
assert.deepEqual(decideCleanMode('codex', { installNeedsSandbox: true }), { useClean: true });
404+
assert.deepEqual(
405+
decideCleanMode('codex', { mount: { readonlyPatterns: ['docs/**'] } }),
406+
{ useClean: true }
407+
);
402408
assert.deepEqual(decideCleanMode('codex', true), { useClean: false });
403409
});
404410

@@ -964,34 +970,50 @@ test('loadSidecarForSelection: opencode picks agentsMd, not claudeMd', () => {
964970
assert.equal(sidecar?.personaContent, '# agents\n');
965971
});
966972

967-
test('main: codex sessions engage the sandbox mount by default', async () => {
968-
const root = mkdtempSync(join(tmpdir(), 'aw-cli-mount-'));
973+
test('main: codex sessions skip the sandbox mount by default', async () => {
974+
const root = mkdtempSync(join(tmpdir(), 'aw-cli-direct-'));
969975
try {
970976
const workforceHome = join(root, '.agentworkforce', 'workforce');
971977
const personaId = writeStandaloneCodexPersona(workforceHome);
972-
// Codex defaults to a relayfile mount in parity with claude/opencode so
973-
// persona-supplied AGENTS.md sidecars and per-session writes stay sandboxed.
978+
const { stderr } = await runCliCapturingStderr(
979+
['agent', `${personaId}`],
980+
{ AGENT_WORKFORCE_HOME: workforceHome }
981+
);
982+
assert.ok(
983+
!/sandbox mount →/.test(stderr),
984+
`expected the direct launch branch to skip the mount; saw stderr:\n${stderr}`
985+
);
986+
} finally {
987+
rmSync(root, { recursive: true, force: true });
988+
}
989+
});
990+
991+
test('main: codex sessions use the sandbox mount for declared mount policy', async () => {
992+
const root = mkdtempSync(join(tmpdir(), 'aw-cli-mount-'));
993+
try {
994+
const workforceHome = join(root, '.agentworkforce', 'workforce');
995+
const personaId = writeStandaloneCodexPersona(workforceHome, 'local-codex-mounted', {
996+
mount: { readonlyPatterns: ['README.md'] }
997+
});
974998
const { stderr } = await runCliCapturingStderr(
975999
['agent', `${personaId}`],
9761000
{ AGENT_WORKFORCE_HOME: workforceHome }
9771001
);
9781002
assert.match(
9791003
stderr,
9801004
/sandbox mount →/,
981-
`expected the mount branch to engage; saw stderr:\n${stderr}`
1005+
`expected the mount branch to engage for mount policy; saw stderr:\n${stderr}`
9821006
);
9831007
} finally {
9841008
rmSync(root, { recursive: true, force: true });
9851009
}
9861010
});
9871011

988-
test('main: codex --install-in-repo disengages the sandbox mount', async () => {
1012+
test('main: codex --install-in-repo keeps direct launch unmounted', async () => {
9891013
const root = mkdtempSync(join(tmpdir(), 'aw-cli-no-mount-'));
9901014
try {
9911015
const workforceHome = join(root, '.agentworkforce', 'workforce');
9921016
const personaId = writeStandaloneCodexPersona(workforceHome);
993-
// The single opt-out: --install-in-repo. Confirms parity with claude/
994-
// opencode where the same flag turns the mount off.
9951017
const { stderr } = await runCliCapturingStderr(
9961018
['agent', `${personaId}`, '--install-in-repo'],
9971019
{ AGENT_WORKFORCE_HOME: workforceHome }

0 commit comments

Comments
 (0)