@ours.network/fleet 0.10.2 → 0.10.4
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 +66 -0
- package/dist/application/capabilities.d.ts +6 -0
- package/dist/application/capabilities.js +37 -0
- package/dist/application/errors.d.ts +31 -0
- package/dist/application/errors.js +51 -0
- package/dist/application/fleet-query-service.d.ts +31 -0
- package/dist/application/fleet-query-service.js +180 -0
- package/dist/application/log-service.d.ts +28 -0
- package/dist/application/log-service.js +146 -0
- package/dist/application/role-command-service.d.ts +37 -0
- package/dist/application/role-command-service.js +82 -0
- package/dist/application/role-creation-service.d.ts +142 -0
- package/dist/application/role-creation-service.js +374 -0
- package/dist/application/role-repository.d.ts +20 -0
- package/dist/application/role-repository.js +168 -0
- package/dist/application/session-control.d.ts +55 -0
- package/dist/application/session-control.js +115 -0
- package/dist/application/types.d.ts +156 -0
- package/dist/application/types.js +1 -0
- package/dist/cli.js +141 -0
- package/dist/config.d.ts +7 -2
- package/dist/config.js +18 -4
- package/dist/creation.d.ts +11 -0
- package/dist/creation.js +22 -5
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +34 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +9 -1
- package/dist/runner.js +10 -2
- package/dist/session/acp.js +5 -1
- package/dist/session/control.d.ts +4 -2
- package/dist/session/control.js +45 -13
- package/dist/spawn.d.ts +20 -2
- package/dist/spawn.js +94 -24
- package/dist/supervisor/launchd.js +17 -0
- package/dist/supervisor/none.js +17 -0
- package/dist/supervisor/systemd.js +4 -0
- package/dist/supervisor/types.d.ts +6 -0
- package/dist/tmux.d.ts +2 -0
- package/dist/tmux.js +8 -0
- package/dist/web/audit.d.ts +22 -0
- package/dist/web/audit.js +54 -0
- package/dist/web/auth.d.ts +61 -0
- package/dist/web/auth.js +186 -0
- package/dist/web/control.d.ts +14 -0
- package/dist/web/control.js +110 -0
- package/dist/web/device-store.d.ts +27 -0
- package/dist/web/device-store.js +155 -0
- package/dist/web/events.d.ts +15 -0
- package/dist/web/events.js +34 -0
- package/dist/web/lock.d.ts +5 -0
- package/dist/web/lock.js +69 -0
- package/dist/web/runtime.d.ts +12 -0
- package/dist/web/runtime.js +170 -0
- package/dist/web/server.d.ts +35 -0
- package/dist/web/server.js +261 -0
- package/dist/web/service.d.ts +42 -0
- package/dist/web/service.js +180 -0
- package/dist/web/terminal/bridge.d.ts +27 -0
- package/dist/web/terminal/bridge.js +317 -0
- package/dist/web-app/assets/TerminalView-DcImdrI1.js +9 -0
- package/dist/web-app/assets/index-BokQN1Ao.js +9 -0
- package/dist/web-app/assets/index-lAXzaOZM.css +1 -0
- package/dist/web-app/icons/ours-fleet-maskable.svg +4 -0
- package/dist/web-app/icons/ours-fleet.svg +4 -0
- package/dist/web-app/index.html +17 -0
- package/dist/web-app/manifest.webmanifest +15 -0
- package/dist/web-app/offline.html +18 -0
- package/dist/web-app/sw.js +51 -0
- package/package.json +26 -3
package/dist/config.d.ts
CHANGED
|
@@ -73,7 +73,8 @@ export interface RoleConfig {
|
|
|
73
73
|
persona?: string;
|
|
74
74
|
bio?: string;
|
|
75
75
|
briefing_file?: string;
|
|
76
|
-
|
|
76
|
+
/** Explicit null means use the selected harness's own default, bypassing fleet defaults. */
|
|
77
|
+
model?: string | null;
|
|
77
78
|
model_chain?: string[];
|
|
78
79
|
max_tokens?: number;
|
|
79
80
|
autocompact_pct?: number;
|
|
@@ -85,7 +86,7 @@ export interface RoleConfig {
|
|
|
85
86
|
worklog?: WorklogPolicy;
|
|
86
87
|
auth_proxy?: Partial<AuthProxyConfig>;
|
|
87
88
|
}
|
|
88
|
-
export interface ResolvedRole extends RoleConfig {
|
|
89
|
+
export interface ResolvedRole extends Omit<RoleConfig, 'model'> {
|
|
89
90
|
name: string;
|
|
90
91
|
harness: string;
|
|
91
92
|
session: SessionBackendId;
|
|
@@ -98,6 +99,7 @@ export interface ResolvedRole extends RoleConfig {
|
|
|
98
99
|
*/
|
|
99
100
|
permissionsDeclared: boolean;
|
|
100
101
|
identity: string;
|
|
102
|
+
model?: string;
|
|
101
103
|
sourceFile: string;
|
|
102
104
|
monitor: MonitorConfig;
|
|
103
105
|
worklog?: WorklogPolicy;
|
|
@@ -115,6 +117,8 @@ export interface FleetConfig {
|
|
|
115
117
|
}
|
|
116
118
|
export declare class ConfigError extends Error {
|
|
117
119
|
}
|
|
120
|
+
/** Resolve a model without leaking a default that belongs to another harness. */
|
|
121
|
+
export declare function resolveRoleModel(model: string | null | undefined, harness: string | undefined, defaults: Record<string, unknown>): string | undefined;
|
|
118
122
|
/**
|
|
119
123
|
* The runtime facts the isolation resolver needs for a role. Single-sourced so
|
|
120
124
|
* config validation, doctor, and the runner all judge the SAME mount set — a
|
|
@@ -122,6 +126,7 @@ export declare class ConfigError extends Error {
|
|
|
122
126
|
* a check at all.
|
|
123
127
|
*/
|
|
124
128
|
export declare function isolationContextFor(role: ResolvedRole): WrapContext;
|
|
129
|
+
export declare const ROLE_NAME_RE: RegExp;
|
|
125
130
|
/** Load ~/fleet.yaml (or an explicit path) merged with ~/fleet.d/*.yaml drop-ins. */
|
|
126
131
|
export declare function loadConfig(configPath?: string, options?: {
|
|
127
132
|
yamlMode?: YamlMode;
|
package/dist/config.js
CHANGED
|
@@ -60,6 +60,16 @@ export function validateMonitorConfig(raw) {
|
|
|
60
60
|
}
|
|
61
61
|
export class ConfigError extends Error {
|
|
62
62
|
}
|
|
63
|
+
/** Resolve a model without leaking a default that belongs to another harness. */
|
|
64
|
+
export function resolveRoleModel(model, harness, defaults) {
|
|
65
|
+
if (model === null)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (typeof model === 'string' && model.trim())
|
|
68
|
+
return model.trim();
|
|
69
|
+
const defaultHarness = defaults.harness ?? 'claude-code';
|
|
70
|
+
const effectiveHarness = harness ?? defaultHarness;
|
|
71
|
+
return effectiveHarness === defaultHarness ? defaults.model : undefined;
|
|
72
|
+
}
|
|
63
73
|
/**
|
|
64
74
|
* The runtime facts the isolation resolver needs for a role. Single-sourced so
|
|
65
75
|
* config validation, doctor, and the runner all judge the SAME mount set — a
|
|
@@ -91,7 +101,7 @@ export function isolationContextFor(role) {
|
|
|
91
101
|
harnessSharedPaths: split?.shared,
|
|
92
102
|
};
|
|
93
103
|
}
|
|
94
|
-
const
|
|
104
|
+
export const ROLE_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
95
105
|
const ROLE_KEYS = [
|
|
96
106
|
'harness', 'session', 'session_options', 'permissions', 'identity', 'cwd', 'coordinator', 'mission', 'persona', 'bio',
|
|
97
107
|
'briefing_file', 'model', 'model_chain', 'max_tokens', 'autocompact_pct', 'env', 'oversee', 'harness_options',
|
|
@@ -143,7 +153,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
143
153
|
const roles = [];
|
|
144
154
|
for (const { file, doc } of docs) {
|
|
145
155
|
for (const [name, raw] of Object.entries((doc.roles ?? {}))) {
|
|
146
|
-
if (!
|
|
156
|
+
if (!ROLE_NAME_RE.test(name))
|
|
147
157
|
throw new ConfigError(`${file}: invalid role name '${name}' (allowed: [A-Za-z0-9_-])`);
|
|
148
158
|
const prev = seen.get(name);
|
|
149
159
|
if (prev)
|
|
@@ -177,9 +187,13 @@ export function loadConfig(configPath, options = {}) {
|
|
|
177
187
|
const monitor = resolveMonitorConfig(defaults.monitor, r.monitor, { base, file, name });
|
|
178
188
|
const worklog = resolveWorklogPolicy(defaults.worklog, r.worklog, file, name);
|
|
179
189
|
const authProxy = resolveAuthProxy(defaults.auth_proxy, r.auth_proxy, file, name);
|
|
180
|
-
const model = r.model ?? defaults.model;
|
|
181
|
-
const modelChain = resolveModelChain(model, r.model_chain ?? defaults.model_chain, file, name);
|
|
182
190
|
const harness = r.harness ?? defaults.harness ?? 'claude-code';
|
|
191
|
+
const defaultHarness = defaults.harness ?? 'claude-code';
|
|
192
|
+
const inheritsModelDefaults = harness === defaultHarness && r.model !== null;
|
|
193
|
+
const model = resolveRoleModel(r.model, r.harness, defaults);
|
|
194
|
+
const modelChain = resolveModelChain(model, r.model_chain ?? (inheritsModelDefaults
|
|
195
|
+
? defaults.model_chain
|
|
196
|
+
: undefined), file, name);
|
|
183
197
|
if (authProxy && harness !== 'claude-code')
|
|
184
198
|
throw new ConfigError(`${file}: role '${name}' auth_proxy is supported only by claude-code`);
|
|
185
199
|
const env = {
|
package/dist/creation.d.ts
CHANGED
|
@@ -24,7 +24,10 @@ export interface CreationDeps {
|
|
|
24
24
|
identityRegistry?: IdentityRegistry;
|
|
25
25
|
/** Verify/create the ours identity. Injectable so tests need no daemon. */
|
|
26
26
|
identityProvisioner?: IdentityProvisioner;
|
|
27
|
+
/** Descriptive progress around the existing transaction; never a second workflow. */
|
|
28
|
+
onStage?(stage: CreationCoreStage, evidence?: Record<string, string | boolean>): void;
|
|
27
29
|
}
|
|
30
|
+
export type CreationCoreStage = 'reserving' | 'checking_identity' | 'writing_role' | 'registering_supervisor' | 'starting_temp';
|
|
28
31
|
/**
|
|
29
32
|
* The contract the ours daemon must satisfy for identity names to be reserved
|
|
30
33
|
* atomically across ALL of its clients, not just across fleet processes.
|
|
@@ -101,12 +104,15 @@ export interface IdentityProvisioner {
|
|
|
101
104
|
}
|
|
102
105
|
export type IdentityGuarantee = {
|
|
103
106
|
state: 'verified';
|
|
107
|
+
evidence: 'verified';
|
|
104
108
|
detail: string;
|
|
105
109
|
} | {
|
|
106
110
|
state: 'created';
|
|
111
|
+
evidence: 'missing';
|
|
107
112
|
detail: string;
|
|
108
113
|
} | {
|
|
109
114
|
state: 'unverified';
|
|
115
|
+
evidence: 'missing' | 'unknown';
|
|
110
116
|
detail: string;
|
|
111
117
|
};
|
|
112
118
|
/**
|
|
@@ -148,6 +154,9 @@ export interface CreationProvenance {
|
|
|
148
154
|
createdAt: string;
|
|
149
155
|
lifetime: 'permanent' | 'temporary';
|
|
150
156
|
role: string;
|
|
157
|
+
/** Additive correlation for non-CLI creation surfaces; never contains request data. */
|
|
158
|
+
surface?: 'cli' | 'web';
|
|
159
|
+
creationActionId?: string;
|
|
151
160
|
/** Effective settings, each tagged with where its value came from. */
|
|
152
161
|
settings: Record<string, ProvenanceEntry>;
|
|
153
162
|
}
|
|
@@ -170,6 +179,8 @@ export declare function buildProvenance(o: {
|
|
|
170
179
|
fleetVersion: string;
|
|
171
180
|
now?: Date;
|
|
172
181
|
settings: Record<string, ProvenanceEntry>;
|
|
182
|
+
surface?: 'cli' | 'web';
|
|
183
|
+
creationActionId?: string;
|
|
173
184
|
}): CreationProvenance;
|
|
174
185
|
/** Write the provenance record atomically, before the role is started. */
|
|
175
186
|
export declare function writeProvenance(stateDir: string, p: CreationProvenance): void;
|
package/dist/creation.js
CHANGED
|
@@ -150,7 +150,10 @@ function readdirSyncSafe(dir) {
|
|
|
150
150
|
*/
|
|
151
151
|
export async function ensureIdentity(name, profile, provisioner, log = () => { }) {
|
|
152
152
|
if (!provisioner)
|
|
153
|
-
return {
|
|
153
|
+
return {
|
|
154
|
+
state: 'unverified', evidence: 'unknown',
|
|
155
|
+
detail: 'no identity provisioner is configured',
|
|
156
|
+
};
|
|
154
157
|
let present;
|
|
155
158
|
try {
|
|
156
159
|
present = await provisioner.exists(name);
|
|
@@ -160,18 +163,30 @@ export async function ensureIdentity(name, profile, provisioner, log = () => { }
|
|
|
160
163
|
log(`identity '${name}': could not be verified (${e.message})`);
|
|
161
164
|
}
|
|
162
165
|
if (present === true)
|
|
163
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
state: 'verified', evidence: 'verified',
|
|
168
|
+
detail: 'the ours daemon reports it exists',
|
|
169
|
+
};
|
|
164
170
|
if (present === 'unknown')
|
|
165
|
-
return {
|
|
171
|
+
return {
|
|
172
|
+
state: 'unverified', evidence: 'unknown',
|
|
173
|
+
detail: 'the ours daemon could not be asked',
|
|
174
|
+
};
|
|
166
175
|
if (!provisioner.create) {
|
|
167
176
|
// Loud, and named. The briefing will tell the agent to mint it — which is
|
|
168
177
|
// what actually happens today — but nobody is told it was "predefined".
|
|
169
178
|
log(`identity '${name}' does not exist and this host cannot create one automatically — `
|
|
170
179
|
+ `the role will be told to mint it on first boot. Create it in advance to avoid that.`);
|
|
171
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
state: 'unverified', evidence: 'missing',
|
|
182
|
+
detail: 'it does not exist and cannot be created here',
|
|
183
|
+
};
|
|
172
184
|
}
|
|
173
185
|
await provisioner.create(name, profile);
|
|
174
|
-
return {
|
|
186
|
+
return {
|
|
187
|
+
state: 'created', evidence: 'missing',
|
|
188
|
+
detail: 'created during spawn, with its bio and persona published',
|
|
189
|
+
};
|
|
175
190
|
}
|
|
176
191
|
/**
|
|
177
192
|
* Ask the running ours daemon whether an identity exists, over the same
|
|
@@ -230,6 +245,8 @@ export function buildProvenance(o) {
|
|
|
230
245
|
createdAt: (o.now ?? new Date()).toISOString(),
|
|
231
246
|
lifetime: o.lifetime,
|
|
232
247
|
role: o.role,
|
|
248
|
+
surface: o.surface ?? 'cli',
|
|
249
|
+
creationActionId: o.creationActionId,
|
|
233
250
|
settings: o.settings,
|
|
234
251
|
};
|
|
235
252
|
}
|
package/dist/docs.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Keep this concise enough to place directly in an agent context. Unlike
|
|
5
5
|
* Commander's per-command help, this describes how the pieces compose.
|
|
6
6
|
*/
|
|
7
|
-
export declare const AI_DOCS = "# ours-fleet reference\n\nours-fleet runs persistent or temporary, identity-bound AI roles. A role selects\na harness independently from its session backend:\n\n- harness: `claude-code` or `codex`\n- session: `tmux` (default) or `acp`\n- lifetime: permanent (supervised, restartable) or `spawn --temp`\n\n## Discover and validate\n\n```sh\nours-fleet docs # this complete reference (`man` is an alias)\nours-fleet help <command> # exact flags for one command\nours-fleet config [-c FILE] # validate and print the merged plan; no changes\nours-fleet doctor [-c FILE] [--harness codex|claude-code]\n```\n\nDefault configuration is `~/fleet.yaml` plus sorted `~/fleet.d/*.yaml` role\ndrop-ins. An explicit `-c FILE` replaces `~/fleet.yaml`; fleet.d still adds\nroles. Validate with `config` and `doctor` before starting or restarting.\n\n## Lifecycle and console commands\n\n```sh\nours-fleet init\nours-fleet up|down [Name...]\nours-fleet restart [Name...] # preserve/resume harness context\nours-fleet force-restart [Name...] # fresh context; briefing is reloaded\nours-fleet ls\nours-fleet status|peek|attach|logs Name\nours-fleet logs -f Name\nours-fleet send Name \"prompt\"\nours-fleet send Name --key Enter # tmux only\nours-fleet rm Name\n```\n\n`peek`, `attach`, and text `send` work with tmux and ACP. ACP attachment\nalso accepts `/permit <permission-id> <option-id>`, `/interrupt`, and\n`/detach`. Raw `--key` input is tmux-only.\n\n## Spawn\n\n```sh\nours-fleet spawn [--temp] Name \\\n --harness codex|claude-code --session tmux|acp \\\n --mission \"one line\" --cwd /absolute/path --identity Identity \\\n --coordinator Coordinator --model MODEL \\\n --approval ask|allow|deny \\\n --filesystem read-only|workspace|unrestricted \\\n --unattended deny|wait \\\n --bio-file /path/bio.md --persona-file /path/persona.md\n```\n\nPermanent spawn writes `~/fleet.d/Name.yaml` and starts a supervised role.\n`--temp` writes ephemeral state, starts a detached supervisor, and removes the\nrole after exit/reboot. Both lifetimes support `--session acp`.\n\nCodex-specific spawn flags: `--sandbox`, `--permission-mode`, `--launcher`,\n`--profile`, `--search`, repeatable `--codex-config key=value`, repeatable\n`--add-dir`, and legacy `--monitor` (consent for the native Codex monitor,\nnot the `monitor.mode` wake-owner selector). Run `ours-fleet help spawn` for\nexact values.\n\n## fleet.yaml\n\n```yaml\nvars:\n work_root: /home/me/work\nstart_stagger_ms: 0\ndefaults:\n harness: codex\n session: acp\n model: gpt-model-id\n permissions:\n approval: ask\n filesystem: workspace\n unattended: deny\n monitor:\n mode: fleet # fleet (default) | native\nroles:\n Coordinator:\n harness: codex\n session: acp\n identity: Coordinator\n cwd: ${work_root}/project\n mission: Coordinate work and delegate implementation.\n model: gpt-model-id\n permissions:\n approval: ask\n filesystem: workspace\n unattended: deny\n session_options: # advanced overrides; normally omit\n # acp:\n # command: [/custom/codex-acp, --flag]\n tmux:\n boot_grace_ms: 10000\n monitor:\n mode: fleet # fleet supervisor | native harness monitor\n interrupt: false # true cancels active work before every configured wake\n wake_sources: [message_received, file_received, local_contact_request, pending_message]\n batch_ms: 2000\n inject: notification\n turn_fail_threshold: 3\n harness_options:\n launcher: auto\n sandbox: workspace-write\n approval: on-request\n search: false\n profile: fleet\n add_dirs: [/data/shared]\n config:\n model_reasoning_effort: high\n bio: Public role card and when peers should engage it.\n persona: Local operating contract, boundaries, and escalation policy.\n briefing_file: /absolute/custom-briefing.md\n coordinator: AnotherCoordinator\n env:\n KEY: value\n oversee:\n - { role: Worker, interval: 5m }\n```\n\nRole values override defaults. `${name}` substitutes entries from `vars`.\nOther role fields include `max_tokens`, `autocompact_pct`, and `isolation`.\nUse README.md for the complete isolation policy and resource-cap schema.\n\n## Permissions\n\nPrefer the harness-neutral `permissions` block:\n\n- `approval: ask|allow|deny`: whether actions may request or receive approval\n- `filesystem: read-only|workspace|unrestricted`: filesystem intent\n- `unattended: deny|wait`: what ACP does when no console can answer a request\n\nThe backend translates this common intent. Harness-native settings in\n`harness_options` take precedence where supplied. Do not choose\n`allow`/`unrestricted`, Codex `never`/`danger-full-access`, or Claude\n`bypassPermissions` without explicit authorization.\n\n### Creation-time isolation\n\n`ours-fleet spawn --isolation-file <path>` supplies a role's sandbox policy at\ncreation, so the FIRST launch is already confined \u2014 a role that only gains\n`isolation:` on a later `up` ran unsandboxed until then.\n\nThe file holds exactly the `isolation:` mapping documented above and nothing\nelse \u2014 the same schema, validated by the same code, so a policy written here\ncannot mean something different from the identical block in fleet.yaml:\n\n```yaml\nnetwork: deny\nfs:\n read: [/opt/reference]\nresources:\n mem: 2G\n```\n\nInvalid files are rejected before anything is created: no config, no state\ndirectory, no identity reservation. Works for both permanent and `--temp` roles.\n\n### Never-prompt failure\n\nThe failure this section exists to prevent leaves no error message anywhere.\n\nAn unattended role has no console. When the harness needs a permission decision\nthere is nobody to ask, so the request is refused INSIDE the harness \u2014 no\nprompt, no error, no log line. The agent simply does less than its briefing told\nit to, reports success, and nothing distinguishes that from having done the\nwork. Two settings produce it:\n\n1. a permission mode that suppresses the prompt without granting the action\n (Claude `dontAsk`, which is why neutral `allow` maps to\n `bypassPermissions` instead); and\n2. `unattended: deny`, which refuses every request that reaches it.\n\n**Automatic decisions are now recorded.** Every permission request decided\nwithout a human emits a completed event into\n`~/.ours-fleet/agents/<Name>/.session-events.jsonl` carrying the decision,\nwhether policy or a person made it, the policy that produced it\n(`permissions.unattended=deny` vs `permissions.approval=deny`/`=allow`),\nthe reason, and the option selected. `ours-fleet peek` and `attach` render\nthem. Automatic denial asks for a one-shot rejection, never a standing one, so a\nsingle unattended refusal cannot disable a tool for the rest of the session.\n\nA role that can auto-deny logs one line at startup saying so.\n\nTo detect an under-permissioned role BEFORE it runs, use the capability floor\nbelow: `ours-fleet doctor` fails such a role rather than letting it discover\nthe problem silently at work.\n\n### The unattended capability floor\n\nAn unattended role has no console, so a permission request cannot be answered \u2014\nit is refused, silently, inside the harness. The agent then does less than it\nwas told to and reports no error. To make that visible before launch,\n`ours-fleet config` and `ours-fleet doctor` resolve each role's neutral\npermissions through its harness and check the result against a fixed floor:\n\n- `read-state` \u2014 read its briefing, ROUTINES.md, and WORKLOG.md\n- `write-state` \u2014 append its WORKLOG and its own state files\n- `messaging` \u2014 bind its identity, send and receive ours mail\n- `monitor` \u2014 arm and observe its mail monitor\n- `workspace-edit` \u2014 edit and test files in its working directory\n- `status-commands` \u2014 run the inspection commands its briefing prescribes\n\n`doctor` reports this per role as `unattended floor: <Role>`. A role with\n`unattended: deny` that cannot meet the floor FAILS doctor, because it will\ndeny those requests with nobody to see it; with `unattended: wait` it warns,\nbecause a human can still attach and answer.\n\nSecurity meaning: `approval: allow` maps to Claude's `bypassPermissions`,\nwhich genuinely permits the actions the role was authorized to take \u2014\n`dontAsk` only suppresses the prompt while still refusing the action. Nothing\nother than an explicit `allow` is elevated: `ask` stays on Claude's default\nmode and `deny` maps to `plan`. `allow` is therefore a real grant and\nrequires explicit authorization; per-role `isolation:` remains the outer\nboundary that a permission mode cannot cross.\n\nSee also: `spawn --approval/--filesystem/--unattended` set this intent at\ncreation, and `ours-fleet config` prints each role's neutral settings, their\nnative translation, and any warning \u2014 the same text `doctor` reports.\n\nClaude `harness_options`: `permission_mode` (default, acceptEdits, plan,\ndontAsk, bypassPermissions), `plugins`, `mem_palace`, and\n`mem_palace_midsession_autosave`.\n\nCodex `harness_options`: `launcher` (auto, ours-codex, codex), `sandbox`\n(read-only, workspace-write, danger-full-access), `approval` or\n`permission_mode` (untrusted, on-request, never), `profile`, `search`,\n`config`, `add_dirs`, and `monitor`.\n\n## ACP adapters\n\nThe maintained `@agentclientprotocol/codex-acp` and\n`@agentclientprotocol/claude-agent-acp` runtimes are bundled automatically as\noptional ours-fleet dependencies. The supervisor resolves their executable\nentrypoints internally, so default ACP roles do not depend on global PATH.\nThe maintained Claude adapter requires Node 22; tmux and Codex ACP continue to\nwork on the ours-fleet core minimum of Node 20.\n\nOverride an adapter only when necessary with `session_options.acp.command`\n(string or argv list). If optional dependencies were deliberately omitted,\nours-fleet falls back to a compatible globally installed `codex-acp` or\n`claude-agent-acp`. `ours-fleet doctor -c FILE` verifies the resolved adapter.\n\n## Reliable mail wake\n\n`monitor.mode` selects exactly one wake owner:\n\n- `fleet` (default): the ours-fleet supervisor consumes body-free daemon\n events and advances its durable cursor only after delivery is accepted. ACP\n uses live steering when supported and falls back to structured\n `session/prompt`; tmux uses verified console injection.\n- `native`: ours-fleet starts no supervisor monitor; the generated briefing\n instructs Claude Code or Codex to arm its harness-native wake mechanism.\n\nSet `monitor.interrupt: true` in fleet mode to cancel active work before every\nconfigured wake. The policy is content-blind because the supervisor cannot\ninspect encrypted message bodies. Message bodies are released only when the\nrole calls the ours `get_messages` tool.\n\nLegacy `monitor.enabled: true|false` remains accepted as an alias for\n`mode: fleet|native`; use `mode` in new configuration. Codex's separate\n`harness_options.monitor: true` is native-monitor consent, not monitor-owner\nselection.\nInspect `ours-fleet status Name`, `peek Name`, role logs, and\n`~/.ours-fleet/agents/Name/.monitor-status` when diagnosing delivery.\n\n## Stable config and YAML migration\n\n`ours-fleet config --json` emits schemaVersion 1 resolved plans. Environment\nvalues and mission/persona/bio bodies are withheld; environment keys are sorted\nand values are marked redacted. Additive fields may appear in schema 1, while a\nremoval or semantic reuse requires a new schema version.\n\nYAML parsing always rejects duplicate keys. The current default\n`--yaml-mode compat` warns with file/line/column for anchors, aliases, explicit\ntags, non-scalar keys, and multiple documents. Use `--yaml-mode strict` in CI\nnow; strict becomes the next-major default and compat is the temporary migration\nescape hatch.\n\n## Bounded worklogs, auth proxy, and model recovery\n\nAn optional `worklog: { max_kb, keep_tail_kb, max_archives }` policy rotates a\nstable snapshot at fleet-owned lifecycle points. Concurrent changes defer\nrotation. Archives remain beside WORKLOG.md with the same sensitive-state\nboundary; retention deletes only recognized fleet archive names.\n\n`auth_proxy: { kind: anthropic, base_url, required, health_url }` is Claude-only\nand loopback-only. Fleet injects only ANTHROPIC_BASE_URL and doctor rejects\ncredential env keys. The privileged reference companion is\n`contrib/anthropic-auth-proxy.mjs`; deploy it separately as a dedicated account\nwith a 0600 token file and per-role listener access. Fleet never installs it or\nreads its credential.\n\n`model_chain` is an ordered authorization list and its first entry must equal\n`model`. Only sustained high-confidence entitlement/quota 429 evidence advances\none entry. Transient 429, overload, auth, policy, and unknown errors never\ndown-shift. Runtime state is atomic in .model-recovery.json; exhaustion is\nfail-closed and held down. Change the declared chain/model and restart to\nreconcile explicitly; no chain preserves detection-only behavior.\n";
|
|
7
|
+
export declare const AI_DOCS = "# ours-fleet reference\n\nours-fleet runs persistent or temporary, identity-bound AI roles. A role selects\na harness independently from its session backend:\n\n- harness: `claude-code` or `codex`\n- session: `tmux` (default) or `acp`\n- lifetime: permanent (supervised, restartable) or `spawn --temp`\n\n## Discover and validate\n\n```sh\nours-fleet docs # this complete reference (`man` is an alias)\nours-fleet help <command> # exact flags for one command\nours-fleet config [-c FILE] # validate and print the merged plan; no changes\nours-fleet doctor [-c FILE] [--harness codex|claude-code]\n```\n\nDefault configuration is `~/fleet.yaml` plus sorted `~/fleet.d/*.yaml` role\ndrop-ins. An explicit `-c FILE` replaces `~/fleet.yaml`; fleet.d still adds\nroles. Validate with `config` and `doctor` before starting or restarting.\n\n## Lifecycle and console commands\n\n```sh\nours-fleet init\nours-fleet up|down [Name...]\nours-fleet restart [Name...] # preserve/resume harness context\nours-fleet force-restart [Name...] # fresh context; briefing is reloaded\nours-fleet ls\nours-fleet status|peek|attach|logs Name\nours-fleet logs -f Name\nours-fleet send Name \"prompt\"\nours-fleet send Name --key Enter # tmux only\nours-fleet rm Name\n```\n\n`peek`, `attach`, and text `send` work with tmux and ACP. ACP attachment\nalso accepts `/permit <permission-id> <option-id>`, `/interrupt`, and\n`/detach`. Raw `--key` input is tmux-only.\n\n## Local web console\n\nThe npm package includes the web console; installed users do not clone the repo\nor run `npm run build`:\n\n```sh\nnpm i -g @ours.network/fleet\nours-fleet init\nours-fleet doctor\nours-fleet web # install/update service, start, pair browser\n```\n\nThe normal command uses stable `http://127.0.0.1:49271/`, installs an\nowner-level systemd user service (Linux) or LaunchAgent (macOS), and opens a\nfive-minute one-use pairing link in the local browser. After pairing, bookmark\nthe plain URL or install the PWA. To pair a new, signed-out, or revoked browser,\nrun `ours-fleet web open`.\n\n```sh\nours-fleet web status\nours-fleet web start|stop|restart\nours-fleet web open\nours-fleet web revoke-all # revoke every browser and active session\nours-fleet web uninstall\nours-fleet web serve --port 0 --no-open # isolated foreground/testing mode\n```\n\nThe console is intentionally IPv4-loopback-only. It has no LAN/Internet host,\nproxy, TLS, or remote-access mode. Do not expose port 49271 through a reverse\nproxy. Browser credentials are HttpOnly/SameSite, and `revoke-all` invalidates\nall trusted devices. Role creation offers harness-scoped known-model choices\nwhile still accepting a typed model ID; blank explicitly uses the selected\nharness's own default.\n\n## Spawn\n\n```sh\nours-fleet spawn [--temp] Name \\\n --harness codex|claude-code --session tmux|acp \\\n --mission \"one line\" --cwd /absolute/path --identity Identity \\\n --coordinator Coordinator --model MODEL \\\n --approval ask|allow|deny \\\n --filesystem read-only|workspace|unrestricted \\\n --unattended deny|wait \\\n --bio-file /path/bio.md --persona-file /path/persona.md\n```\n\nPermanent spawn writes `~/fleet.d/Name.yaml` and starts a supervised role.\n`--temp` writes ephemeral state, starts a detached supervisor, and removes the\nrole after exit/reboot. Both lifetimes support `--session acp`.\n\nCodex-specific spawn flags: `--sandbox`, `--permission-mode`, `--launcher`,\n`--profile`, `--search`, repeatable `--codex-config key=value`, repeatable\n`--add-dir`, and legacy `--monitor` (consent for the native Codex monitor,\nnot the `monitor.mode` wake-owner selector). Run `ours-fleet help spawn` for\nexact values.\n\n## fleet.yaml\n\n```yaml\nvars:\n work_root: /home/me/work\nstart_stagger_ms: 0\ndefaults:\n harness: codex\n session: acp\n model: gpt-model-id\n permissions:\n approval: ask\n filesystem: workspace\n unattended: deny\n monitor:\n mode: fleet # fleet (default) | native\nroles:\n Coordinator:\n harness: codex\n session: acp\n identity: Coordinator\n cwd: ${work_root}/project\n mission: Coordinate work and delegate implementation.\n model: gpt-model-id\n permissions:\n approval: ask\n filesystem: workspace\n unattended: deny\n session_options: # advanced overrides; normally omit\n # acp:\n # command: [/custom/codex-acp, --flag]\n tmux:\n boot_grace_ms: 10000\n monitor:\n mode: fleet # fleet supervisor | native harness monitor\n interrupt: false # true cancels active work before every configured wake\n wake_sources: [message_received, file_received, local_contact_request, pending_message]\n batch_ms: 2000\n inject: notification\n turn_fail_threshold: 3\n harness_options:\n launcher: auto\n sandbox: workspace-write\n approval: on-request\n search: false\n profile: fleet\n add_dirs: [/data/shared]\n config:\n model_reasoning_effort: high\n bio: Public role card and when peers should engage it.\n persona: Local operating contract, boundaries, and escalation policy.\n briefing_file: /absolute/custom-briefing.md\n coordinator: AnotherCoordinator\n env:\n KEY: value\n oversee:\n - { role: Worker, interval: 5m }\n```\n\nRole values override defaults. `${name}` substitutes entries from `vars`.\nOther role fields include `max_tokens`, `autocompact_pct`, and `isolation`.\nUse README.md for the complete isolation policy and resource-cap schema.\n\n## Permissions\n\nPrefer the harness-neutral `permissions` block:\n\n- `approval: ask|allow|deny`: whether actions may request or receive approval\n- `filesystem: read-only|workspace|unrestricted`: filesystem intent\n- `unattended: deny|wait`: what ACP does when no console can answer a request\n\nThe backend translates this common intent. Harness-native settings in\n`harness_options` take precedence where supplied. Do not choose\n`allow`/`unrestricted`, Codex `never`/`danger-full-access`, or Claude\n`bypassPermissions` without explicit authorization.\n\n### Creation-time isolation\n\n`ours-fleet spawn --isolation-file <path>` supplies a role's sandbox policy at\ncreation, so the FIRST launch is already confined \u2014 a role that only gains\n`isolation:` on a later `up` ran unsandboxed until then.\n\nThe file holds exactly the `isolation:` mapping documented above and nothing\nelse \u2014 the same schema, validated by the same code, so a policy written here\ncannot mean something different from the identical block in fleet.yaml:\n\n```yaml\nnetwork: deny\nfs:\n read: [/opt/reference]\nresources:\n mem: 2G\n```\n\nInvalid files are rejected before anything is created: no config, no state\ndirectory, no identity reservation. Works for both permanent and `--temp` roles.\n\n### Never-prompt failure\n\nThe failure this section exists to prevent leaves no error message anywhere.\n\nAn unattended role has no console. When the harness needs a permission decision\nthere is nobody to ask, so the request is refused INSIDE the harness \u2014 no\nprompt, no error, no log line. The agent simply does less than its briefing told\nit to, reports success, and nothing distinguishes that from having done the\nwork. Two settings produce it:\n\n1. a permission mode that suppresses the prompt without granting the action\n (Claude `dontAsk`, which is why neutral `allow` maps to\n `bypassPermissions` instead); and\n2. `unattended: deny`, which refuses every request that reaches it.\n\n**Automatic decisions are now recorded.** Every permission request decided\nwithout a human emits a completed event into\n`~/.ours-fleet/agents/<Name>/.session-events.jsonl` carrying the decision,\nwhether policy or a person made it, the policy that produced it\n(`permissions.unattended=deny` vs `permissions.approval=deny`/`=allow`),\nthe reason, and the option selected. `ours-fleet peek` and `attach` render\nthem. Automatic denial asks for a one-shot rejection, never a standing one, so a\nsingle unattended refusal cannot disable a tool for the rest of the session.\n\nA role that can auto-deny logs one line at startup saying so.\n\nTo detect an under-permissioned role BEFORE it runs, use the capability floor\nbelow: `ours-fleet doctor` fails such a role rather than letting it discover\nthe problem silently at work.\n\n### The unattended capability floor\n\nAn unattended role has no console, so a permission request cannot be answered \u2014\nit is refused, silently, inside the harness. The agent then does less than it\nwas told to and reports no error. To make that visible before launch,\n`ours-fleet config` and `ours-fleet doctor` resolve each role's neutral\npermissions through its harness and check the result against a fixed floor:\n\n- `read-state` \u2014 read its briefing, ROUTINES.md, and WORKLOG.md\n- `write-state` \u2014 append its WORKLOG and its own state files\n- `messaging` \u2014 bind its identity, send and receive ours mail\n- `monitor` \u2014 arm and observe its mail monitor\n- `workspace-edit` \u2014 edit and test files in its working directory\n- `status-commands` \u2014 run the inspection commands its briefing prescribes\n\n`doctor` reports this per role as `unattended floor: <Role>`. A role with\n`unattended: deny` that cannot meet the floor FAILS doctor, because it will\ndeny those requests with nobody to see it; with `unattended: wait` it warns,\nbecause a human can still attach and answer.\n\nSecurity meaning: `approval: allow` maps to Claude's `bypassPermissions`,\nwhich genuinely permits the actions the role was authorized to take \u2014\n`dontAsk` only suppresses the prompt while still refusing the action. Nothing\nother than an explicit `allow` is elevated: `ask` stays on Claude's default\nmode and `deny` maps to `plan`. `allow` is therefore a real grant and\nrequires explicit authorization; per-role `isolation:` remains the outer\nboundary that a permission mode cannot cross.\n\nSee also: `spawn --approval/--filesystem/--unattended` set this intent at\ncreation, and `ours-fleet config` prints each role's neutral settings, their\nnative translation, and any warning \u2014 the same text `doctor` reports.\n\nClaude `harness_options`: `permission_mode` (default, acceptEdits, plan,\ndontAsk, bypassPermissions), `plugins`, `mem_palace`, and\n`mem_palace_midsession_autosave`.\n\nCodex `harness_options`: `launcher` (auto, ours-codex, codex), `sandbox`\n(read-only, workspace-write, danger-full-access), `approval` or\n`permission_mode` (untrusted, on-request, never), `profile`, `search`,\n`config`, `add_dirs`, and `monitor`.\n\n## ACP adapters\n\nThe maintained `@agentclientprotocol/codex-acp` and\n`@agentclientprotocol/claude-agent-acp` runtimes are bundled automatically as\noptional ours-fleet dependencies. The supervisor resolves their executable\nentrypoints internally, so default ACP roles do not depend on global PATH.\nThe maintained Claude adapter requires Node 22; tmux and Codex ACP continue to\nwork on the ours-fleet core minimum of Node 20.\n\nOverride an adapter only when necessary with `session_options.acp.command`\n(string or argv list). If optional dependencies were deliberately omitted,\nours-fleet falls back to a compatible globally installed `codex-acp` or\n`claude-agent-acp`. `ours-fleet doctor -c FILE` verifies the resolved adapter.\n\n## Reliable mail wake\n\n`monitor.mode` selects exactly one wake owner:\n\n- `fleet` (default): the ours-fleet supervisor consumes body-free daemon\n events and advances its durable cursor only after delivery is accepted. ACP\n uses live steering when supported and falls back to structured\n `session/prompt`; tmux uses verified console injection.\n- `native`: ours-fleet starts no supervisor monitor; the generated briefing\n instructs Claude Code or Codex to arm its harness-native wake mechanism.\n\nSet `monitor.interrupt: true` in fleet mode to cancel active work before every\nconfigured wake. The policy is content-blind because the supervisor cannot\ninspect encrypted message bodies. Message bodies are released only when the\nrole calls the ours `get_messages` tool.\n\nLegacy `monitor.enabled: true|false` remains accepted as an alias for\n`mode: fleet|native`; use `mode` in new configuration. Codex's separate\n`harness_options.monitor: true` is native-monitor consent, not monitor-owner\nselection.\nInspect `ours-fleet status Name`, `peek Name`, role logs, and\n`~/.ours-fleet/agents/Name/.monitor-status` when diagnosing delivery.\n\n## Stable config and YAML migration\n\n`ours-fleet config --json` emits schemaVersion 1 resolved plans. Environment\nvalues and mission/persona/bio bodies are withheld; environment keys are sorted\nand values are marked redacted. Additive fields may appear in schema 1, while a\nremoval or semantic reuse requires a new schema version.\n\nYAML parsing always rejects duplicate keys. The current default\n`--yaml-mode compat` warns with file/line/column for anchors, aliases, explicit\ntags, non-scalar keys, and multiple documents. Use `--yaml-mode strict` in CI\nnow; strict becomes the next-major default and compat is the temporary migration\nescape hatch.\n\n## Bounded worklogs, auth proxy, and model recovery\n\nAn optional `worklog: { max_kb, keep_tail_kb, max_archives }` policy rotates a\nstable snapshot at fleet-owned lifecycle points. Concurrent changes defer\nrotation. Archives remain beside WORKLOG.md with the same sensitive-state\nboundary; retention deletes only recognized fleet archive names.\n\n`auth_proxy: { kind: anthropic, base_url, required, health_url }` is Claude-only\nand loopback-only. Fleet injects only ANTHROPIC_BASE_URL and doctor rejects\ncredential env keys. The privileged reference companion is\n`contrib/anthropic-auth-proxy.mjs`; deploy it separately as a dedicated account\nwith a 0600 token file and per-role listener access. Fleet never installs it or\nreads its credential.\n\n`model_chain` is an ordered authorization list and its first entry must equal\n`model`. Only sustained high-confidence entitlement/quota 429 evidence advances\none entry. Transient 429, overload, auth, policy, and unknown errors never\ndown-shift. Runtime state is atomic in .model-recovery.json; exhaustion is\nfail-closed and held down. Change the declared chain/model and restart to\nreconcile explicitly; no chain preserves detection-only behavior.\n";
|
|
8
8
|
/**
|
|
9
9
|
* What every shipped spawn-skill variant must say, and must not say (7.1).
|
|
10
10
|
*
|
package/dist/docs.js
CHANGED
|
@@ -45,6 +45,40 @@ ours-fleet rm Name
|
|
|
45
45
|
also accepts \`/permit <permission-id> <option-id>\`, \`/interrupt\`, and
|
|
46
46
|
\`/detach\`. Raw \`--key\` input is tmux-only.
|
|
47
47
|
|
|
48
|
+
## Local web console
|
|
49
|
+
|
|
50
|
+
The npm package includes the web console; installed users do not clone the repo
|
|
51
|
+
or run \`npm run build\`:
|
|
52
|
+
|
|
53
|
+
\`\`\`sh
|
|
54
|
+
npm i -g @ours.network/fleet
|
|
55
|
+
ours-fleet init
|
|
56
|
+
ours-fleet doctor
|
|
57
|
+
ours-fleet web # install/update service, start, pair browser
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
60
|
+
The normal command uses stable \`http://127.0.0.1:49271/\`, installs an
|
|
61
|
+
owner-level systemd user service (Linux) or LaunchAgent (macOS), and opens a
|
|
62
|
+
five-minute one-use pairing link in the local browser. After pairing, bookmark
|
|
63
|
+
the plain URL or install the PWA. To pair a new, signed-out, or revoked browser,
|
|
64
|
+
run \`ours-fleet web open\`.
|
|
65
|
+
|
|
66
|
+
\`\`\`sh
|
|
67
|
+
ours-fleet web status
|
|
68
|
+
ours-fleet web start|stop|restart
|
|
69
|
+
ours-fleet web open
|
|
70
|
+
ours-fleet web revoke-all # revoke every browser and active session
|
|
71
|
+
ours-fleet web uninstall
|
|
72
|
+
ours-fleet web serve --port 0 --no-open # isolated foreground/testing mode
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
The console is intentionally IPv4-loopback-only. It has no LAN/Internet host,
|
|
76
|
+
proxy, TLS, or remote-access mode. Do not expose port 49271 through a reverse
|
|
77
|
+
proxy. Browser credentials are HttpOnly/SameSite, and \`revoke-all\` invalidates
|
|
78
|
+
all trusted devices. Role creation offers harness-scoped known-model choices
|
|
79
|
+
while still accepting a typed model ID; blank explicitly uses the selected
|
|
80
|
+
harness's own default.
|
|
81
|
+
|
|
48
82
|
## Spawn
|
|
49
83
|
|
|
50
84
|
\`\`\`sh
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,16 @@ export { generateBriefing } from './briefing.js';
|
|
|
11
11
|
export { pickBackend } from './supervisor/index.js';
|
|
12
12
|
export type { SupervisorBackend } from './supervisor/types.js';
|
|
13
13
|
export { up, down, restartRoles, rmRole, applyRole } from './ops.js';
|
|
14
|
-
export { spawnPermanent, spawnTemp } from './spawn.js';
|
|
14
|
+
export { spawnPermanent, spawnTemp, buildRoleConfig, profileValues, validateSpawnOpts, } from './spawn.js';
|
|
15
|
+
export { RoleRepository } from './application/role-repository.js';
|
|
16
|
+
export { FleetQueryService } from './application/fleet-query-service.js';
|
|
17
|
+
export { AcpRoleSessionAdapter, TmuxRoleSessionAdapter } from './application/session-control.js';
|
|
18
|
+
export { RoleCreationService } from './application/role-creation-service.js';
|
|
19
|
+
export { RoleCommandService } from './application/role-command-service.js';
|
|
20
|
+
export { StructuredLogService } from './application/log-service.js';
|
|
21
|
+
export { roleCapabilities } from './application/capabilities.js';
|
|
22
|
+
export { FleetError, normalizeError } from './application/errors.js';
|
|
23
|
+
export type * from './application/types.js';
|
|
15
24
|
export { doctor } from './doctor.js';
|
|
16
25
|
export { runOnce, runTemp } from './runner.js';
|
|
17
26
|
export { Tmux } from './tmux.js';
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,15 @@ export { codexAdapter, makeCodexAdapter } from './harness/codex.js';
|
|
|
7
7
|
export { generateBriefing } from './briefing.js';
|
|
8
8
|
export { pickBackend } from './supervisor/index.js';
|
|
9
9
|
export { up, down, restartRoles, rmRole, applyRole } from './ops.js';
|
|
10
|
-
export { spawnPermanent, spawnTemp } from './spawn.js';
|
|
10
|
+
export { spawnPermanent, spawnTemp, buildRoleConfig, profileValues, validateSpawnOpts, } from './spawn.js';
|
|
11
|
+
export { RoleRepository } from './application/role-repository.js';
|
|
12
|
+
export { FleetQueryService } from './application/fleet-query-service.js';
|
|
13
|
+
export { AcpRoleSessionAdapter, TmuxRoleSessionAdapter } from './application/session-control.js';
|
|
14
|
+
export { RoleCreationService } from './application/role-creation-service.js';
|
|
15
|
+
export { RoleCommandService } from './application/role-command-service.js';
|
|
16
|
+
export { StructuredLogService } from './application/log-service.js';
|
|
17
|
+
export { roleCapabilities } from './application/capabilities.js';
|
|
18
|
+
export { FleetError, normalizeError } from './application/errors.js';
|
|
11
19
|
export { doctor } from './doctor.js';
|
|
12
20
|
export { runOnce, runTemp } from './runner.js';
|
|
13
21
|
export { Tmux } from './tmux.js';
|
package/dist/runner.js
CHANGED
|
@@ -60,8 +60,16 @@ export function recordMonitorOwner(dir, owner) {
|
|
|
60
60
|
* still sees the real exit code.
|
|
61
61
|
*/
|
|
62
62
|
export function buildPaneCommand(launch, roleEnv, exitStatusPath, paneArgv = launch.argv) {
|
|
63
|
-
const env = {
|
|
64
|
-
|
|
63
|
+
const env = {
|
|
64
|
+
PATH: process.env.PATH ?? '', COLORTERM: 'truecolor', ...launch.env, ...(roleEnv ?? {}),
|
|
65
|
+
};
|
|
66
|
+
// Interactive panes should advertise colour even when the supervisor itself
|
|
67
|
+
// was launched with NO_COLOR. A role may still deliberately opt back in to
|
|
68
|
+
// NO_COLOR (or replace COLORTERM) through its explicit env block.
|
|
69
|
+
const unsetNoColor = Object.prototype.hasOwnProperty.call(roleEnv ?? {}, 'NO_COLOR')
|
|
70
|
+
? '' : '-u NO_COLOR ';
|
|
71
|
+
const envPfx = 'env ' + unsetNoColor
|
|
72
|
+
+ Object.entries(env).map(([k, v]) => `${k}=${shq(v)}`).join(' ');
|
|
65
73
|
const cmd = paneArgv.map(shq).join(' ');
|
|
66
74
|
// Write a structured record, not a bare number: the wait status alone cannot
|
|
67
75
|
// say whether the file is missing because the program never exited or because
|
package/dist/session/acp.js
CHANGED
|
@@ -118,7 +118,11 @@ export class AcpSession {
|
|
|
118
118
|
throw new SessionControlError('offline', this.lastError ?? 'ACP session is offline');
|
|
119
119
|
if (options.interrupt)
|
|
120
120
|
await this.interrupt();
|
|
121
|
-
|
|
121
|
+
// Interrupting delivery must still use steering when supported. With no
|
|
122
|
+
// live turn, the extension starts one and acknowledges `startedNewTurn`
|
|
123
|
+
// immediately; a normal session/prompt would keep the monitor blocked until
|
|
124
|
+
// the entire wake-triggered turn terminated.
|
|
125
|
+
if (options.steer && this.steeringSupported) {
|
|
122
126
|
const promptId = randomUUID();
|
|
123
127
|
return { promptId, queuedBehind: 0, completion: this.steerPrompt(text) };
|
|
124
128
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { type Socket } from 'node:net';
|
|
2
2
|
import type { ControlFailureKind, SessionHandle } from './types.js';
|
|
3
3
|
export interface ControlRequest {
|
|
4
|
-
version: 1;
|
|
4
|
+
version: 1 | 2;
|
|
5
5
|
id: string;
|
|
6
6
|
token: string;
|
|
7
|
-
command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow';
|
|
7
|
+
command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since';
|
|
8
8
|
text?: string;
|
|
9
9
|
permissionId?: string;
|
|
10
10
|
optionId?: string;
|
|
11
11
|
since?: number;
|
|
12
|
+
/** Existing clients omit this and remain interactive controllers. */
|
|
13
|
+
controller?: boolean;
|
|
12
14
|
}
|
|
13
15
|
export interface ControlResponse {
|
|
14
16
|
version: 1;
|
package/dist/session/control.js
CHANGED
|
@@ -132,6 +132,7 @@ export class RoleControlServer {
|
|
|
132
132
|
socket.setEncoding('utf8');
|
|
133
133
|
let buffer = '';
|
|
134
134
|
let unsubscribe;
|
|
135
|
+
let controllerAttached = false;
|
|
135
136
|
socket.on('data', chunk => {
|
|
136
137
|
buffer += chunk;
|
|
137
138
|
if (Buffer.byteLength(buffer) > MAX_LINE_BYTES) {
|
|
@@ -146,13 +147,15 @@ export class RoleControlServer {
|
|
|
146
147
|
buffer = buffer.slice(newline + 1);
|
|
147
148
|
if (!line.trim())
|
|
148
149
|
continue;
|
|
149
|
-
void this.handle(line, socket).then(
|
|
150
|
-
if (
|
|
150
|
+
void this.handle(line, socket).then(follow => {
|
|
151
|
+
if (follow) {
|
|
151
152
|
if (unsubscribe) {
|
|
152
153
|
unsubscribe();
|
|
153
|
-
|
|
154
|
+
if (controllerAttached)
|
|
155
|
+
this.session.setControllerAttached(false);
|
|
154
156
|
}
|
|
155
|
-
unsubscribe = stop;
|
|
157
|
+
unsubscribe = follow.stop;
|
|
158
|
+
controllerAttached = follow.controller;
|
|
156
159
|
}
|
|
157
160
|
});
|
|
158
161
|
}
|
|
@@ -161,7 +164,8 @@ export class RoleControlServer {
|
|
|
161
164
|
this.sockets.delete(socket);
|
|
162
165
|
if (unsubscribe) {
|
|
163
166
|
unsubscribe();
|
|
164
|
-
|
|
167
|
+
if (controllerAttached)
|
|
168
|
+
this.session.setControllerAttached(false);
|
|
165
169
|
}
|
|
166
170
|
});
|
|
167
171
|
socket.on('error', error => this.log(`control socket: ${error.message}`));
|
|
@@ -175,7 +179,7 @@ export class RoleControlServer {
|
|
|
175
179
|
this.write(socket, { version: 1, id: '?', ok: false, error: 'invalid JSON' });
|
|
176
180
|
return;
|
|
177
181
|
}
|
|
178
|
-
if (request.version !== 1 || typeof request.id !== 'string'
|
|
182
|
+
if ((request.version !== 1 && request.version !== 2) || typeof request.id !== 'string'
|
|
179
183
|
|| !sameToken(this.token, request.token ?? '')) {
|
|
180
184
|
this.write(socket, { version: 1, id: request.id ?? '?', ok: false, error: 'unauthorized' });
|
|
181
185
|
return;
|
|
@@ -184,7 +188,13 @@ export class RoleControlServer {
|
|
|
184
188
|
switch (request.command) {
|
|
185
189
|
case 'status':
|
|
186
190
|
case 'snapshot':
|
|
187
|
-
this.write(socket, {
|
|
191
|
+
this.write(socket, {
|
|
192
|
+
version: 1, id: request.id, ok: true,
|
|
193
|
+
result: {
|
|
194
|
+
...this.session.snapshot(), protocolVersion: 2,
|
|
195
|
+
features: ['events_since', 'observer_follow', 'retained_range'],
|
|
196
|
+
},
|
|
197
|
+
});
|
|
188
198
|
return;
|
|
189
199
|
case 'submit_prompt': {
|
|
190
200
|
if (!request.text?.trim())
|
|
@@ -217,17 +227,39 @@ export class RoleControlServer {
|
|
|
217
227
|
await this.session.interrupt();
|
|
218
228
|
this.write(socket, { version: 1, id: request.id, ok: true });
|
|
219
229
|
return;
|
|
220
|
-
case '
|
|
230
|
+
case 'events_since': {
|
|
221
231
|
const since = Number.isFinite(request.since) ? Number(request.since) : 0;
|
|
232
|
+
const events = this.session.eventsSince(since);
|
|
233
|
+
const all = this.session.eventsSince(0);
|
|
222
234
|
this.write(socket, {
|
|
223
235
|
version: 1, id: request.id, ok: true,
|
|
224
|
-
result: {
|
|
236
|
+
result: {
|
|
237
|
+
events, snapshot: this.session.snapshot(),
|
|
238
|
+
firstSeq: all[0]?.seq ?? 0, lastSeq: all.at(-1)?.seq ?? 0,
|
|
239
|
+
truncated: Boolean(all[0] && since > 0 && since < all[0].seq - 1),
|
|
240
|
+
},
|
|
225
241
|
});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
case 'follow': {
|
|
245
|
+
const since = Number.isFinite(request.since) ? Number(request.since) : 0;
|
|
246
|
+
const events = this.session.eventsSince(since);
|
|
247
|
+
const all = this.session.eventsSince(0);
|
|
248
|
+
this.write(socket, {
|
|
249
|
+
version: 1, id: request.id, ok: true,
|
|
250
|
+
result: {
|
|
251
|
+
events, snapshot: this.session.snapshot(),
|
|
252
|
+
firstSeq: all[0]?.seq ?? 0, lastSeq: all.at(-1)?.seq ?? 0,
|
|
253
|
+
truncated: Boolean(all[0] && since > 0 && since < all[0].seq - 1),
|
|
254
|
+
},
|
|
230
255
|
});
|
|
256
|
+
const controller = request.controller !== false;
|
|
257
|
+
if (controller)
|
|
258
|
+
this.session.setControllerAttached(true);
|
|
259
|
+
return { controller, stop: this.session.subscribe(event => {
|
|
260
|
+
if (!socket.destroyed)
|
|
261
|
+
socket.write(JSON.stringify({ version: 1, event }) + '\n');
|
|
262
|
+
}) };
|
|
231
263
|
}
|
|
232
264
|
}
|
|
233
265
|
}
|
package/dist/spawn.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { IsolationConfig } from './isolation/types.js';
|
|
2
|
-
import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type RoleConfig, type SessionBackendId, type UnattendedMode } from './config.js';
|
|
2
|
+
import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type RoleConfig, type MonitorConfig, type SessionBackendId, type UnattendedMode } from './config.js';
|
|
3
3
|
import { type OpsDeps } from './ops.js';
|
|
4
4
|
import { type CreationDeps, type CreationProvenance } from './creation.js';
|
|
5
|
+
import './harness/claude-code.js';
|
|
6
|
+
import './harness/codex.js';
|
|
5
7
|
/**
|
|
6
8
|
* The provenance record written by the most recent spawn in this process, so
|
|
7
9
|
* the CLI can print the same summary it persisted rather than rebuilding it.
|
|
@@ -17,7 +19,8 @@ export interface SpawnOpts {
|
|
|
17
19
|
identity?: string;
|
|
18
20
|
cwd?: string;
|
|
19
21
|
coordinator?: string;
|
|
20
|
-
|
|
22
|
+
/** null explicitly selects the harness default; undefined retains normal fleet inheritance. */
|
|
23
|
+
model?: string | null;
|
|
21
24
|
permissionMode?: string;
|
|
22
25
|
approval?: ApprovalMode;
|
|
23
26
|
filesystem?: FilesystemMode;
|
|
@@ -29,8 +32,16 @@ export interface SpawnOpts {
|
|
|
29
32
|
codexConfig?: Record<string, string | number | boolean>;
|
|
30
33
|
addDirs?: string[];
|
|
31
34
|
monitor?: boolean;
|
|
35
|
+
/** Typed external monitor configuration used by trusted creation surfaces. */
|
|
36
|
+
monitorConfig?: Partial<MonitorConfig>;
|
|
32
37
|
bioFile?: string;
|
|
33
38
|
personaFile?: string;
|
|
39
|
+
/** Inline profile values for trusted typed callers such as the local web service. */
|
|
40
|
+
bio?: string;
|
|
41
|
+
persona?: string;
|
|
42
|
+
/** Internal, non-sensitive provenance correlation for typed presentation layers. */
|
|
43
|
+
surface?: 'cli' | 'web';
|
|
44
|
+
creationActionId?: string;
|
|
34
45
|
/**
|
|
35
46
|
* Path to a file holding exactly the existing `isolation:` mapping — the same
|
|
36
47
|
* schema fleet.yaml uses, not a second policy language. The ONE new operator
|
|
@@ -42,6 +53,12 @@ export interface SpawnOpts {
|
|
|
42
53
|
dryRun?: boolean;
|
|
43
54
|
json?: boolean;
|
|
44
55
|
}
|
|
56
|
+
export declare function profileValues(o: SpawnOpts): {
|
|
57
|
+
bio?: string;
|
|
58
|
+
persona?: string;
|
|
59
|
+
};
|
|
60
|
+
/** Pure option-to-role mapping shared by CLI and application services. */
|
|
61
|
+
export declare function buildRoleConfig(o: SpawnOpts, defaultHarness?: string): RoleConfig;
|
|
45
62
|
/**
|
|
46
63
|
* Read and validate an `--isolation-file`. The file is the existing
|
|
47
64
|
* `isolation:` mapping and nothing else — the same schema, the same validator
|
|
@@ -52,6 +69,7 @@ export interface SpawnOpts {
|
|
|
52
69
|
* must fail before any artifact exists.
|
|
53
70
|
*/
|
|
54
71
|
export declare function readIsolationFile(path: string): IsolationConfig;
|
|
72
|
+
export declare function validateSpawnOpts(o: SpawnOpts): void;
|
|
55
73
|
/** Read mission text without trimming or newline rewriting. */
|
|
56
74
|
export declare function readMissionFile(path: string): string;
|
|
57
75
|
/** The ours identity a spawn will bind: explicit, else the role name. */
|