@ours.network/fleet 0.9.4 → 0.9.5
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 +47 -30
- package/dist/cli.js +153 -15
- package/dist/config.d.ts +24 -0
- package/dist/config.js +79 -1
- package/dist/docs.d.ts +7 -0
- package/dist/docs.js +177 -0
- package/dist/doctor.js +50 -6
- package/dist/harness/acp-agent.d.ts +11 -0
- package/dist/harness/acp-agent.js +27 -0
- package/dist/harness/claude-code.js +31 -1
- package/dist/harness/codex.js +40 -2
- package/dist/harness/types.d.ts +12 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/dist/monitor.d.ts +16 -1
- package/dist/monitor.js +121 -24
- package/dist/runner.js +73 -18
- package/dist/session/acp.d.ts +49 -0
- package/dist/session/acp.js +280 -0
- package/dist/session/control.d.ts +41 -0
- package/dist/session/control.js +218 -0
- package/dist/session/events.d.ts +14 -0
- package/dist/session/events.js +67 -0
- package/dist/session/tmux.d.ts +20 -0
- package/dist/session/tmux.js +46 -0
- package/dist/session/types.d.ts +47 -0
- package/dist/session/types.js +1 -0
- package/dist/spawn.d.ts +5 -0
- package/dist/spawn.js +24 -1
- package/package.json +7 -2
package/dist/docs.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable, AI-friendly CLI and configuration reference.
|
|
3
|
+
*
|
|
4
|
+
* Keep this concise enough to place directly in an agent context. Unlike
|
|
5
|
+
* Commander's per-command help, this describes how the pieces compose.
|
|
6
|
+
*/
|
|
7
|
+
export const AI_DOCS = `# ours-fleet reference
|
|
8
|
+
|
|
9
|
+
ours-fleet runs persistent or temporary, identity-bound AI roles. A role selects
|
|
10
|
+
a harness independently from its session backend:
|
|
11
|
+
|
|
12
|
+
- harness: \`claude-code\` or \`codex\`
|
|
13
|
+
- session: \`tmux\` (default) or \`acp\`
|
|
14
|
+
- lifetime: permanent (supervised, restartable) or \`spawn --temp\`
|
|
15
|
+
|
|
16
|
+
## Discover and validate
|
|
17
|
+
|
|
18
|
+
\`\`\`sh
|
|
19
|
+
ours-fleet docs # this complete reference (\`man\` is an alias)
|
|
20
|
+
ours-fleet help <command> # exact flags for one command
|
|
21
|
+
ours-fleet config [-c FILE] # validate and print the merged plan; no changes
|
|
22
|
+
ours-fleet doctor [-c FILE] [--harness codex|claude-code]
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
Default configuration is \`~/fleet.yaml\` plus sorted \`~/fleet.d/*.yaml\` role
|
|
26
|
+
drop-ins. An explicit \`-c FILE\` replaces \`~/fleet.yaml\`; fleet.d still adds
|
|
27
|
+
roles. Validate with \`config\` and \`doctor\` before starting or restarting.
|
|
28
|
+
|
|
29
|
+
## Lifecycle and console commands
|
|
30
|
+
|
|
31
|
+
\`\`\`sh
|
|
32
|
+
ours-fleet init
|
|
33
|
+
ours-fleet up|down [Name...]
|
|
34
|
+
ours-fleet restart [Name...] # preserve/resume harness context
|
|
35
|
+
ours-fleet force-restart [Name...] # fresh context; briefing is reloaded
|
|
36
|
+
ours-fleet ls
|
|
37
|
+
ours-fleet status|peek|attach|logs Name
|
|
38
|
+
ours-fleet logs -f Name
|
|
39
|
+
ours-fleet send Name "prompt"
|
|
40
|
+
ours-fleet send Name --key Enter # tmux only
|
|
41
|
+
ours-fleet rm Name
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
\`peek\`, \`attach\`, and text \`send\` work with tmux and ACP. ACP attachment
|
|
45
|
+
also accepts \`/permit <permission-id> <option-id>\`, \`/interrupt\`, and
|
|
46
|
+
\`/detach\`. Raw \`--key\` input is tmux-only.
|
|
47
|
+
|
|
48
|
+
## Spawn
|
|
49
|
+
|
|
50
|
+
\`\`\`sh
|
|
51
|
+
ours-fleet spawn [--temp] Name \\
|
|
52
|
+
--harness codex|claude-code --session tmux|acp \\
|
|
53
|
+
--mission "one line" --cwd /absolute/path --identity Identity \\
|
|
54
|
+
--coordinator Coordinator --model MODEL \\
|
|
55
|
+
--approval ask|allow|deny \\
|
|
56
|
+
--filesystem read-only|workspace|unrestricted \\
|
|
57
|
+
--unattended deny|wait \\
|
|
58
|
+
--bio-file /path/bio.md --persona-file /path/persona.md
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
Permanent spawn writes \`~/fleet.d/Name.yaml\` and starts a supervised role.
|
|
62
|
+
\`--temp\` writes ephemeral state, starts a detached supervisor, and removes the
|
|
63
|
+
role after exit/reboot. Both lifetimes support \`--session acp\`.
|
|
64
|
+
|
|
65
|
+
Codex-specific spawn flags: \`--sandbox\`, \`--permission-mode\`, \`--launcher\`,
|
|
66
|
+
\`--profile\`, \`--search\`, repeatable \`--codex-config key=value\`, repeatable
|
|
67
|
+
\`--add-dir\`, and \`--monitor\`. Run \`ours-fleet help spawn\` for exact values.
|
|
68
|
+
|
|
69
|
+
## fleet.yaml
|
|
70
|
+
|
|
71
|
+
\`\`\`yaml
|
|
72
|
+
vars:
|
|
73
|
+
work_root: /home/me/work
|
|
74
|
+
start_stagger_ms: 0
|
|
75
|
+
defaults:
|
|
76
|
+
harness: codex
|
|
77
|
+
session: acp
|
|
78
|
+
model: gpt-model-id
|
|
79
|
+
permissions:
|
|
80
|
+
approval: ask
|
|
81
|
+
filesystem: workspace
|
|
82
|
+
unattended: deny
|
|
83
|
+
monitor:
|
|
84
|
+
enabled: true
|
|
85
|
+
roles:
|
|
86
|
+
Coordinator:
|
|
87
|
+
harness: codex
|
|
88
|
+
session: acp
|
|
89
|
+
identity: Coordinator
|
|
90
|
+
cwd: \${work_root}/project
|
|
91
|
+
mission: Coordinate work and delegate implementation.
|
|
92
|
+
model: gpt-model-id
|
|
93
|
+
permissions:
|
|
94
|
+
approval: ask
|
|
95
|
+
filesystem: workspace
|
|
96
|
+
unattended: deny
|
|
97
|
+
session_options: # advanced overrides; normally omit
|
|
98
|
+
# acp:
|
|
99
|
+
# command: [/custom/codex-acp, --flag]
|
|
100
|
+
tmux:
|
|
101
|
+
boot_grace_ms: 10000
|
|
102
|
+
monitor:
|
|
103
|
+
enabled: true
|
|
104
|
+
wake_sources: [message_received, file_received, local_contact_request, pending_message]
|
|
105
|
+
batch_ms: 2000
|
|
106
|
+
inject: notification
|
|
107
|
+
turn_fail_threshold: 3
|
|
108
|
+
harness_options:
|
|
109
|
+
launcher: auto
|
|
110
|
+
sandbox: workspace-write
|
|
111
|
+
approval: on-request
|
|
112
|
+
search: false
|
|
113
|
+
profile: fleet
|
|
114
|
+
add_dirs: [/data/shared]
|
|
115
|
+
config:
|
|
116
|
+
model_reasoning_effort: high
|
|
117
|
+
bio: Public role card and when peers should engage it.
|
|
118
|
+
persona: Local operating contract, boundaries, and escalation policy.
|
|
119
|
+
briefing_file: /absolute/custom-briefing.md
|
|
120
|
+
coordinator: AnotherCoordinator
|
|
121
|
+
env:
|
|
122
|
+
KEY: value
|
|
123
|
+
oversee:
|
|
124
|
+
- { role: Worker, interval: 5m }
|
|
125
|
+
\`\`\`
|
|
126
|
+
|
|
127
|
+
Role values override defaults. \`\${name}\` substitutes entries from \`vars\`.
|
|
128
|
+
Other role fields include \`max_tokens\`, \`autocompact_pct\`, and \`isolation\`.
|
|
129
|
+
Use README.md for the complete isolation policy and resource-cap schema.
|
|
130
|
+
|
|
131
|
+
## Permissions
|
|
132
|
+
|
|
133
|
+
Prefer the harness-neutral \`permissions\` block:
|
|
134
|
+
|
|
135
|
+
- \`approval: ask|allow|deny\`: whether actions may request or receive approval
|
|
136
|
+
- \`filesystem: read-only|workspace|unrestricted\`: filesystem intent
|
|
137
|
+
- \`unattended: deny|wait\`: what ACP does when no console can answer a request
|
|
138
|
+
|
|
139
|
+
The backend translates this common intent. Harness-native settings in
|
|
140
|
+
\`harness_options\` take precedence where supplied. Do not choose
|
|
141
|
+
\`allow\`/\`unrestricted\`, Codex \`never\`/\`danger-full-access\`, or Claude
|
|
142
|
+
\`bypassPermissions\` without explicit authorization.
|
|
143
|
+
|
|
144
|
+
Claude \`harness_options\`: \`permission_mode\` (default, acceptEdits, plan,
|
|
145
|
+
dontAsk, bypassPermissions), \`plugins\`, \`mem_palace\`, and
|
|
146
|
+
\`mem_palace_midsession_autosave\`.
|
|
147
|
+
|
|
148
|
+
Codex \`harness_options\`: \`launcher\` (auto, ours-codex, codex), \`sandbox\`
|
|
149
|
+
(read-only, workspace-write, danger-full-access), \`approval\` or
|
|
150
|
+
\`permission_mode\` (untrusted, on-request, never), \`profile\`, \`search\`,
|
|
151
|
+
\`config\`, \`add_dirs\`, and \`monitor\`.
|
|
152
|
+
|
|
153
|
+
## ACP adapters
|
|
154
|
+
|
|
155
|
+
The maintained \`@agentclientprotocol/codex-acp\` and
|
|
156
|
+
\`@agentclientprotocol/claude-agent-acp\` runtimes are bundled automatically as
|
|
157
|
+
optional ours-fleet dependencies. The supervisor resolves their executable
|
|
158
|
+
entrypoints internally, so default ACP roles do not depend on global PATH.
|
|
159
|
+
The maintained Claude adapter requires Node 22; tmux and Codex ACP continue to
|
|
160
|
+
work on the ours-fleet core minimum of Node 20.
|
|
161
|
+
|
|
162
|
+
Override an adapter only when necessary with \`session_options.acp.command\`
|
|
163
|
+
(string or argv list). If optional dependencies were deliberately omitted,
|
|
164
|
+
ours-fleet falls back to a compatible globally installed \`codex-acp\` or
|
|
165
|
+
\`claude-agent-acp\`. \`ours-fleet doctor -c FILE\` verifies the resolved adapter.
|
|
166
|
+
|
|
167
|
+
## Reliable mail wake
|
|
168
|
+
|
|
169
|
+
The supervisor monitor is enabled by default. It consumes body-free daemon
|
|
170
|
+
events and advances its durable cursor only after delivery is accepted. ACP uses
|
|
171
|
+
a structured \`session/prompt\`; tmux uses verified console injection. Message
|
|
172
|
+
bodies are released only when the role calls the ours \`get_messages\` tool.
|
|
173
|
+
|
|
174
|
+
Set \`monitor.enabled: false\` only to retain legacy in-session monitoring.
|
|
175
|
+
Inspect \`ours-fleet status Name\`, \`peek Name\`, role logs, and
|
|
176
|
+
\`~/.ours-fleet/agents/Name/.monitor-status\` when diagnosing delivery.
|
|
177
|
+
`;
|
package/dist/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { realExec } from './exec.js';
|
|
4
4
|
import { loadConfig } from './config.js';
|
|
5
5
|
import { getAdapter } from './harness/registry.js';
|
|
6
|
+
import { resolveBundledAcpAgent } from './harness/acp-agent.js';
|
|
6
7
|
import { agentDir, home, deriveXdgRuntimeDir } from './paths.js';
|
|
7
8
|
import { resolveIsolation } from './isolation/policy.js';
|
|
8
9
|
import { makeBubblewrapBackend } from './isolation/bubblewrap.js';
|
|
@@ -43,11 +44,14 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
43
44
|
name: 'node', ok: major >= 20,
|
|
44
45
|
detail: major >= 20 ? `v${process.versions.node}` : `v${process.versions.node} — need >= 20`,
|
|
45
46
|
});
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const roles = loadConfigSafe(opts.configPath);
|
|
48
|
+
if (roles.length === 0 || roles.some(role => (role.session ?? 'tmux') === 'tmux')) {
|
|
49
|
+
const tmux = await exec('tmux', ['-V']);
|
|
50
|
+
checks.push({
|
|
51
|
+
name: 'tmux', ok: tmux.code === 0,
|
|
52
|
+
detail: tmux.code === 0 ? tmux.stdout.trim() : 'not found — apt install tmux / brew install tmux',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
51
55
|
const mcp = await exec('ours-mcp', ['--version']);
|
|
52
56
|
checks.push({
|
|
53
57
|
name: 'ours-mcp', ok: mcp.code === 0,
|
|
@@ -84,7 +88,6 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
84
88
|
// opt-in per role (OQ-1), so a missing bwrap must not fail doctor for fleets that
|
|
85
89
|
// don't use it. Only a role that DECLARES isolation and cannot get it under
|
|
86
90
|
// `strict` is a hard failure.
|
|
87
|
-
const roles = loadConfigSafe(opts.configPath);
|
|
88
91
|
const bw = await makeBubblewrapBackend(exec).available();
|
|
89
92
|
checks.push({
|
|
90
93
|
name: 'isolation: bubblewrap', ok: true,
|
|
@@ -169,6 +172,47 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
169
172
|
checks.push({ name: h, ok: false, detail: e.message });
|
|
170
173
|
}
|
|
171
174
|
}
|
|
175
|
+
for (const role of roles.filter(role => role.session === 'acp')) {
|
|
176
|
+
const configured = role.session_options?.acp?.command;
|
|
177
|
+
const bundled = configured == null
|
|
178
|
+
? role.harness === 'codex'
|
|
179
|
+
? resolveBundledAcpAgent('@agentclientprotocol/codex-acp', 'codex-acp', 'codex-acp')
|
|
180
|
+
: role.harness === 'claude-code'
|
|
181
|
+
? resolveBundledAcpAgent('@agentclientprotocol/claude-agent-acp', 'claude-agent-acp', 'claude-agent-acp')
|
|
182
|
+
: undefined
|
|
183
|
+
: undefined;
|
|
184
|
+
const command = Array.isArray(configured)
|
|
185
|
+
? configured[0]
|
|
186
|
+
: typeof configured === 'string'
|
|
187
|
+
? configured.trim().split(/\s+/)[0]
|
|
188
|
+
: role.harness === 'codex'
|
|
189
|
+
? 'codex-acp'
|
|
190
|
+
: role.harness === 'claude-code'
|
|
191
|
+
? 'claude-agent-acp'
|
|
192
|
+
: '';
|
|
193
|
+
if (!command) {
|
|
194
|
+
checks.push({
|
|
195
|
+
name: `acp: ${role.name}`, ok: false,
|
|
196
|
+
detail: `harness '${role.harness}' has no default ACP agent; set session_options.acp.command`,
|
|
197
|
+
});
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (bundled?.bundled) {
|
|
201
|
+
checks.push({
|
|
202
|
+
name: `acp: ${role.name}`, ok: true,
|
|
203
|
+
detail: `${command} bundled with ours-fleet`,
|
|
204
|
+
});
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const result = await exec('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', 'sh', command]);
|
|
208
|
+
checks.push({
|
|
209
|
+
name: `acp: ${role.name}`,
|
|
210
|
+
ok: result.code === 0,
|
|
211
|
+
detail: result.code === 0
|
|
212
|
+
? `${command} available`
|
|
213
|
+
: `${command} not found or failed — install the ACP adapter or set session_options.acp.command`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
172
216
|
return { ok: checks.every(c => c.ok), checks };
|
|
173
217
|
}
|
|
174
218
|
function loadConfigSafe(configPath) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve an ACP agent shipped as an ours-fleet dependency. Running the JS
|
|
3
|
+
* entrypoint through this process's Node avoids depending on npm exposing a
|
|
4
|
+
* transitive dependency's bin on the user's global PATH.
|
|
5
|
+
*/
|
|
6
|
+
export interface AcpAgentResolution {
|
|
7
|
+
argv: string[];
|
|
8
|
+
bundled: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveBundledAcpAgent(packageName: string, binName: string, fallbackCommand: string): AcpAgentResolution;
|
|
11
|
+
export declare function bundledAcpAgent(packageName: string, binName: string, fallbackCommand: string): string[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
export function resolveBundledAcpAgent(packageName, binName, fallbackCommand) {
|
|
6
|
+
try {
|
|
7
|
+
const manifestPath = require.resolve(`${packageName}/package.json`);
|
|
8
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
9
|
+
const relative = typeof manifest.bin === 'string'
|
|
10
|
+
? manifest.bin
|
|
11
|
+
: manifest.bin?.[binName];
|
|
12
|
+
if (!relative)
|
|
13
|
+
return { argv: [fallbackCommand], bundled: false };
|
|
14
|
+
const entrypoint = resolve(dirname(manifestPath), relative);
|
|
15
|
+
if (!existsSync(entrypoint))
|
|
16
|
+
return { argv: [fallbackCommand], bundled: false };
|
|
17
|
+
return { argv: [process.execPath, entrypoint], bundled: true };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Supports development installs that intentionally omit optional
|
|
21
|
+
// dependencies and existing hosts with a globally installed adapter.
|
|
22
|
+
return { argv: [fallbackCommand], bundled: false };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function bundledAcpAgent(packageName, binName, fallbackCommand) {
|
|
26
|
+
return resolveBundledAcpAgent(packageName, binName, fallbackCommand).argv;
|
|
27
|
+
}
|
|
@@ -3,14 +3,20 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { home } from '../paths.js';
|
|
4
4
|
import { realExec } from '../exec.js';
|
|
5
5
|
import { registerAdapter } from './registry.js';
|
|
6
|
+
import { bundledAcpAgent } from './acp-agent.js';
|
|
6
7
|
const OPTION_KEYS = ['plugins', 'mem_palace', 'mem_palace_midsession_autosave', 'permission_mode'];
|
|
7
8
|
/** Claude Code's accepted --permission-mode values. */
|
|
8
9
|
const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'];
|
|
9
10
|
/** Resolve & validate the per-role permission mode, throwing on an unknown value. */
|
|
10
11
|
function permissionMode(role) {
|
|
11
12
|
const pm = role.harness_options?.permission_mode;
|
|
12
|
-
if (pm == null)
|
|
13
|
+
if (pm == null) {
|
|
14
|
+
if (role.permissions?.approval === 'allow')
|
|
15
|
+
return 'dontAsk';
|
|
16
|
+
if (role.permissions?.approval === 'deny')
|
|
17
|
+
return 'plan';
|
|
13
18
|
return undefined;
|
|
19
|
+
}
|
|
14
20
|
if (!PERMISSION_MODES.includes(pm))
|
|
15
21
|
throw new Error(`invalid harness_options.permission_mode "${pm}"; allowed: ${PERMISSION_MODES.join(', ')}`);
|
|
16
22
|
return pm;
|
|
@@ -108,6 +114,30 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
108
114
|
this.vocabulary.restartPrompt(role.identity, join(stateDir, 'WORKLOG.md'), role)];
|
|
109
115
|
return { argv, env: prep.env };
|
|
110
116
|
},
|
|
117
|
+
buildAcpLaunch(role, prep) {
|
|
118
|
+
const configured = role.session_options?.acp?.command;
|
|
119
|
+
const argv = Array.isArray(configured)
|
|
120
|
+
? [...configured]
|
|
121
|
+
: typeof configured === 'string'
|
|
122
|
+
? ['sh', '-c', configured]
|
|
123
|
+
: bundledAcpAgent('@agentclientprotocol/claude-agent-acp', 'claude-agent-acp', 'claude-agent-acp');
|
|
124
|
+
return { argv, env: prep.env };
|
|
125
|
+
},
|
|
126
|
+
translatePermissions(permissions) {
|
|
127
|
+
const native = permissions.approval === 'allow'
|
|
128
|
+
? 'dontAsk'
|
|
129
|
+
: permissions.approval === 'deny'
|
|
130
|
+
? 'plan'
|
|
131
|
+
: 'default';
|
|
132
|
+
const exact = permissions.filesystem === 'workspace' && permissions.approval === 'ask';
|
|
133
|
+
return {
|
|
134
|
+
native: { permission_mode: native },
|
|
135
|
+
exact,
|
|
136
|
+
warnings: exact ? [] : [
|
|
137
|
+
'Claude permission modes do not exactly represent independent approval and filesystem intent; fleet isolation remains the outer boundary',
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
},
|
|
111
141
|
vocabulary: {
|
|
112
142
|
bindTool: 'choose_identity',
|
|
113
143
|
createTool: 'create_identity',
|
package/dist/harness/codex.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { agentDir } from '../paths.js';
|
|
3
3
|
import { realExec } from '../exec.js';
|
|
4
4
|
import { registerAdapter } from './registry.js';
|
|
5
|
+
import { bundledAcpAgent } from './acp-agent.js';
|
|
5
6
|
const OPTION_KEYS = [
|
|
6
7
|
'launcher', 'sandbox', 'approval', 'permission_mode', 'search', 'profile', 'config', 'add_dirs',
|
|
7
8
|
'monitor',
|
|
@@ -14,8 +15,16 @@ const APPROVAL_POLICIES = ['untrusted', 'on-request', 'never'];
|
|
|
14
15
|
/** Resolve & validate the per-role sandbox mode, throwing on an unknown value. */
|
|
15
16
|
function sandboxMode(role) {
|
|
16
17
|
const s = role.harness_options?.sandbox;
|
|
17
|
-
if (s == null)
|
|
18
|
+
if (s == null) {
|
|
19
|
+
const filesystem = role.permissions?.filesystem;
|
|
20
|
+
if (filesystem === 'read-only')
|
|
21
|
+
return 'read-only';
|
|
22
|
+
if (filesystem === 'unrestricted')
|
|
23
|
+
return 'danger-full-access';
|
|
24
|
+
if (filesystem === 'workspace')
|
|
25
|
+
return 'workspace-write';
|
|
18
26
|
return undefined;
|
|
27
|
+
}
|
|
19
28
|
if (!SANDBOX_MODES.includes(s))
|
|
20
29
|
throw new Error(`invalid harness_options.sandbox "${s}"; allowed: ${SANDBOX_MODES.join(', ')}`);
|
|
21
30
|
return s;
|
|
@@ -24,8 +33,14 @@ function sandboxMode(role) {
|
|
|
24
33
|
function approvalPolicy(role) {
|
|
25
34
|
const o = role.harness_options;
|
|
26
35
|
const a = o?.approval ?? o?.permission_mode;
|
|
27
|
-
if (a == null)
|
|
36
|
+
if (a == null) {
|
|
37
|
+
const approval = role.permissions?.approval;
|
|
38
|
+
if (approval === 'allow')
|
|
39
|
+
return 'never';
|
|
40
|
+
if (approval === 'ask' || approval === 'deny')
|
|
41
|
+
return 'on-request';
|
|
28
42
|
return undefined;
|
|
43
|
+
}
|
|
29
44
|
if (!APPROVAL_POLICIES.includes(a))
|
|
30
45
|
throw new Error(`invalid harness_options.approval "${a}"; allowed: ${APPROVAL_POLICIES.join(', ')}`);
|
|
31
46
|
return a;
|
|
@@ -173,6 +188,29 @@ export function makeCodexAdapter(exec = realExec) {
|
|
|
173
188
|
this.vocabulary.restartPrompt(role.identity, join(stateDir, 'WORKLOG.md'), role)];
|
|
174
189
|
return { argv, env: prep.env };
|
|
175
190
|
},
|
|
191
|
+
buildAcpLaunch(role, prep) {
|
|
192
|
+
const configured = role.session_options?.acp?.command;
|
|
193
|
+
const argv = Array.isArray(configured)
|
|
194
|
+
? [...configured]
|
|
195
|
+
: typeof configured === 'string'
|
|
196
|
+
? ['sh', '-c', configured]
|
|
197
|
+
: bundledAcpAgent('@agentclientprotocol/codex-acp', 'codex-acp', 'codex-acp');
|
|
198
|
+
return { argv, env: prep.env };
|
|
199
|
+
},
|
|
200
|
+
translatePermissions(permissions) {
|
|
201
|
+
return {
|
|
202
|
+
native: {
|
|
203
|
+
approval: permissions.approval === 'allow' ? 'never' : 'on-request',
|
|
204
|
+
sandbox: permissions.filesystem === 'read-only'
|
|
205
|
+
? 'read-only'
|
|
206
|
+
: permissions.filesystem === 'unrestricted'
|
|
207
|
+
? 'danger-full-access'
|
|
208
|
+
: 'workspace-write',
|
|
209
|
+
},
|
|
210
|
+
exact: true,
|
|
211
|
+
warnings: [],
|
|
212
|
+
};
|
|
213
|
+
},
|
|
176
214
|
vocabulary: {
|
|
177
215
|
bindTool: 'choose_identity',
|
|
178
216
|
createTool: 'create_identity',
|
package/dist/harness/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResolvedRole } from '../config.js';
|
|
1
|
+
import type { CommonPermissions, ResolvedRole } from '../config.js';
|
|
2
2
|
export interface PrereqCheck {
|
|
3
3
|
name: string;
|
|
4
4
|
ok: boolean;
|
|
@@ -26,6 +26,15 @@ export interface Launch {
|
|
|
26
26
|
argv: string[];
|
|
27
27
|
env: Record<string, string>;
|
|
28
28
|
}
|
|
29
|
+
export interface AcpLaunch {
|
|
30
|
+
argv: string[];
|
|
31
|
+
env: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
export interface PermissionTranslation {
|
|
34
|
+
native: Record<string, unknown>;
|
|
35
|
+
exact: boolean;
|
|
36
|
+
warnings: string[];
|
|
37
|
+
}
|
|
29
38
|
/** Harness-correct wording/tool names used to generate briefing.md. */
|
|
30
39
|
export interface BriefingVocab {
|
|
31
40
|
bindTool: string;
|
|
@@ -57,6 +66,8 @@ export interface HarnessAdapter {
|
|
|
57
66
|
validateOptions(opts: unknown): ValidationError[];
|
|
58
67
|
prepareSession(role: ResolvedRole, dirs: RoleDirs): Promise<SessionPrep>;
|
|
59
68
|
buildLaunch(role: ResolvedRole, mode: 'fresh' | 'resume', s: SessionState, prep: SessionPrep): Launch;
|
|
69
|
+
buildAcpLaunch?(role: ResolvedRole, prep: SessionPrep): AcpLaunch;
|
|
70
|
+
translatePermissions?(permissions: CommonPermissions): PermissionTranslation;
|
|
60
71
|
vocabulary: BriefingVocab;
|
|
61
72
|
exitPolicy: ExitPolicy;
|
|
62
73
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
export { loadConfig, findRole, ConfigError } from './config.js';
|
|
2
|
-
export type { FleetConfig, ResolvedRole, RoleConfig, OverseeEntry } from './config.js';
|
|
3
|
-
export type { HarnessAdapter, BriefingVocab, ExitPolicy, PrereqReport, PrereqCheck, SessionPrep, SessionState, Launch, RoleDirs, ValidationError, } from './harness/types.js';
|
|
1
|
+
export { loadConfig, findRole, resolvePermissions, ConfigError } from './config.js';
|
|
2
|
+
export type { FleetConfig, ResolvedRole, RoleConfig, OverseeEntry, SessionBackendId, CommonPermissions, SessionOptions, } from './config.js';
|
|
3
|
+
export type { HarnessAdapter, BriefingVocab, ExitPolicy, PrereqReport, PrereqCheck, SessionPrep, SessionState, Launch, AcpLaunch, PermissionTranslation, RoleDirs, ValidationError, } from './harness/types.js';
|
|
4
|
+
export type { SessionHandle, SessionSnapshot, SessionEvent, TurnResult, } from './session/types.js';
|
|
5
|
+
export { AcpSession } from './session/acp.js';
|
|
6
|
+
export { TmuxSession } from './session/tmux.js';
|
|
4
7
|
export { registerAdapter, getAdapter, knownAdapters } from './harness/registry.js';
|
|
5
8
|
export { claudeCodeAdapter, makeClaudeCodeAdapter } from './harness/claude-code.js';
|
|
6
9
|
export { codexAdapter, makeCodexAdapter } from './harness/codex.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export { loadConfig, findRole, ConfigError } from './config.js';
|
|
1
|
+
export { loadConfig, findRole, resolvePermissions, ConfigError } from './config.js';
|
|
2
|
+
export { AcpSession } from './session/acp.js';
|
|
3
|
+
export { TmuxSession } from './session/tmux.js';
|
|
2
4
|
export { registerAdapter, getAdapter, knownAdapters } from './harness/registry.js';
|
|
3
5
|
export { claudeCodeAdapter, makeClaudeCodeAdapter } from './harness/claude-code.js';
|
|
4
6
|
export { codexAdapter, makeCodexAdapter } from './harness/codex.js';
|
package/dist/monitor.d.ts
CHANGED
|
@@ -38,6 +38,13 @@ export interface MonitorDeps {
|
|
|
38
38
|
set(fn: () => void, ms: number): ReturnType<typeof setTimeout>;
|
|
39
39
|
clear(t: ReturnType<typeof setTimeout>): void;
|
|
40
40
|
};
|
|
41
|
+
/** Structured prompt delivery used by ACP sessions. Tmux remains the fallback. */
|
|
42
|
+
delivery?: {
|
|
43
|
+
submit(text: string): Promise<{
|
|
44
|
+
accepted: boolean;
|
|
45
|
+
detail?: string;
|
|
46
|
+
}>;
|
|
47
|
+
};
|
|
41
48
|
}
|
|
42
49
|
/** Best-effort daemon config (issue #17): the fields the MCP client reads. */
|
|
43
50
|
interface DaemonConfig {
|
|
@@ -112,6 +119,8 @@ export declare function looksApiError(pane: string): boolean;
|
|
|
112
119
|
export declare function looksRunning(pane: string): boolean;
|
|
113
120
|
export interface MonitorOpts {
|
|
114
121
|
name: string;
|
|
122
|
+
/** Ours identity whose notification stream is authoritative (may differ from role name). */
|
|
123
|
+
identity?: string;
|
|
115
124
|
agentDir: string;
|
|
116
125
|
cfg: MonitorConfig;
|
|
117
126
|
deps: MonitorDeps;
|
|
@@ -124,12 +133,16 @@ export interface MonitorHandle {
|
|
|
124
133
|
}
|
|
125
134
|
export declare class Monitor {
|
|
126
135
|
private readonly name;
|
|
136
|
+
private readonly identity;
|
|
127
137
|
private readonly cfg;
|
|
128
138
|
private readonly deps;
|
|
129
139
|
private readonly ep;
|
|
130
140
|
private readonly statusPath;
|
|
131
141
|
private readonly cursorPath;
|
|
142
|
+
private readonly statePath;
|
|
132
143
|
private cursor;
|
|
144
|
+
private deliveredCursor;
|
|
145
|
+
private pendingState;
|
|
133
146
|
private fatal;
|
|
134
147
|
private stopped;
|
|
135
148
|
private bootDeadline;
|
|
@@ -137,7 +150,7 @@ export declare class Monitor {
|
|
|
137
150
|
private apiErrorStreak;
|
|
138
151
|
private readonly turnFailThreshold;
|
|
139
152
|
constructor(o: MonitorOpts);
|
|
140
|
-
/**
|
|
153
|
+
/** Resume the last delivered cursor; only a brand-new monitor primes at stream tip. */
|
|
141
154
|
prime(): Promise<void>;
|
|
142
155
|
/** Long-poll → filter → coalesce → inject, until the pane pid dies or stop(). */
|
|
143
156
|
run(pid: number): Promise<void>;
|
|
@@ -175,6 +188,8 @@ export declare class Monitor {
|
|
|
175
188
|
private advance;
|
|
176
189
|
private persistCursor;
|
|
177
190
|
private readPersistedCursor;
|
|
191
|
+
/** Atomically persist body-free delivery state; restart always resumes from deliveredCursor. */
|
|
192
|
+
private persistState;
|
|
178
193
|
private setStatus;
|
|
179
194
|
}
|
|
180
195
|
export declare function createMonitor(o: MonitorOpts): Monitor;
|