@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.21
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 +204 -55
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +20 -5
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +6 -25
- package/dist/application/role-creation-service.js +34 -72
- package/dist/application/role-repository.d.ts +4 -2
- package/dist/application/role-repository.js +6 -3
- package/dist/application/session-control.d.ts +3 -1
- package/dist/application/session-control.js +4 -2
- package/dist/application/task-room-service.d.ts +58 -6
- package/dist/application/task-room-service.js +435 -79
- package/dist/briefing.js +74 -24
- package/dist/build-info.json +4 -4
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +241 -76
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +75 -3
- package/dist/config.js +274 -34
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +3 -2
- package/dist/daemon-recovery.d.ts +93 -0
- package/dist/daemon-recovery.js +328 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +192 -66
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +54 -4
- package/dist/fleet-command-audit.d.ts +165 -0
- package/dist/fleet-command-audit.js +555 -0
- package/dist/fleet-proxy.d.ts +7 -2
- package/dist/fleet-proxy.js +30 -8
- package/dist/harness/agent-session.d.ts +5 -0
- package/dist/harness/claude-code-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +6 -1
- package/dist/harness/claude-code.js +2 -0
- package/dist/harness/codex-app-server-proxy.d.ts +40 -0
- package/dist/harness/codex-app-server-proxy.js +348 -14
- package/dist/harness/codex-session.d.ts +11 -2
- package/dist/harness/codex-session.js +22 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +110 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +4 -3
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +629 -0
- package/dist/lifecycle-summary.d.ts +134 -0
- package/dist/lifecycle-summary.js +179 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +38 -5
- package/dist/model-env.d.ts +2 -2
- package/dist/model-env.js +1 -1
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +58 -0
- package/dist/owner-channel/channel.js +321 -42
- package/dist/owner-channel/commands.d.ts +6 -1
- package/dist/owner-channel/commands.js +34 -19
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/permissions.js +1 -1
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -0
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +414 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.js +13 -1
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +514 -75
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +20 -4
- package/dist/rooms-tasks/config.js +16 -14
- package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
- package/dist/rooms-tasks/cowork-adapter.js +6 -0
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +181 -0
- package/dist/rooms-tasks/member-overrides.d.ts +30 -0
- package/dist/rooms-tasks/member-overrides.js +204 -0
- package/dist/rooms-tasks/provision.js +184 -49
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-state.d.ts +115 -3
- package/dist/rooms-tasks/task-state.js +361 -11
- package/dist/rooms-tasks/templates.d.ts +4 -2
- package/dist/rooms-tasks/templates.js +42 -78
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +7 -3
- package/dist/rooms-tasks/types.d.ts +96 -17
- package/dist/rooms-tasks/types.js +14 -4
- package/dist/runner.d.ts +12 -2
- package/dist/runner.js +124 -43
- package/dist/session/acp.d.ts +9 -1
- package/dist/session/acp.js +35 -9
- package/dist/session/arbiter.d.ts +2 -1
- package/dist/session/arbiter.js +2 -0
- package/dist/session/codex-app-server-transport.d.ts +53 -0
- package/dist/session/codex-app-server-transport.js +184 -0
- package/dist/session/codex-app-server.d.ts +122 -0
- package/dist/session/codex-app-server.js +1031 -0
- package/dist/session/control.d.ts +14 -1
- package/dist/session/control.js +38 -0
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/types.d.ts +24 -6
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +15 -31
- package/dist/spawn.js +117 -237
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/watchdog/config.d.ts +10 -7
- package/dist/watchdog/config.js +29 -19
- package/dist/watchdog/run.js +4 -13
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +27 -8
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web/topology-promote.js +8 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
- package/dist/web-app/index.html +1 -1
- package/package.json +3 -2
- package/presets/brain-catalog.json +16 -0
- package/presets/fleet/agent_templates/Critic.yaml +4 -0
- package/presets/fleet/agent_templates/Developer.yaml +4 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
- package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +30 -0
- package/presets/fleet/roles/Critic.yaml +19 -0
- package/presets/fleet/roles/Developer.yaml +19 -0
- package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
- package/presets/fleet/room_templates/pair.yaml +10 -0
- package/presets/fleet/room_templates/single.yaml +7 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +6 -0
- package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
4
|
+
import { replaceFileAtomically } from './atomic-file.js';
|
|
5
|
+
import { isSensitiveConfigKey } from './sensitive-config.js';
|
|
6
|
+
import { mandatoryConfigurationFits, renderAgentConfiguration, } from './lifecycle-summary.js';
|
|
7
|
+
import { MARKDOWN_MAX_BYTES, MARKDOWN_MAX_CODE_POINTS, markdownCode, markdownProse, } from './rooms-tasks/markdown.js';
|
|
8
|
+
const SESSION_BACKENDS = new Set(['acp', 'codex-app-server']);
|
|
9
|
+
export class FleetCliExit extends Error {
|
|
10
|
+
exitCode;
|
|
11
|
+
outcomeClass;
|
|
12
|
+
effect;
|
|
13
|
+
constructor(exitCode, outcomeClass = 'runtime', effect = 'unknown') {
|
|
14
|
+
super(`fleet CLI exited ${exitCode}`);
|
|
15
|
+
this.exitCode = exitCode;
|
|
16
|
+
this.outcomeClass = outcomeClass;
|
|
17
|
+
this.effect = effect;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Commands are Owner-silent unless they record one of these semantic lifecycle kinds. */
|
|
21
|
+
export const FLEET_LIFECYCLE_EVENT_KINDS = Object.freeze([
|
|
22
|
+
'agent_started', 'task', 'room', 'lifecycle_failure',
|
|
23
|
+
]);
|
|
24
|
+
/** Stable semantic dedupe basis: resource kind + stable resource ID + transition/action ID. */
|
|
25
|
+
export function lifecycleEventDigestBasis(value) {
|
|
26
|
+
const resourceKind = value.kind === 'lifecycle_failure' ? value.resource : value.kind;
|
|
27
|
+
return `${resourceKind}\0${value.id}\0${value.eventId}`;
|
|
28
|
+
}
|
|
29
|
+
let collection;
|
|
30
|
+
export function beginFleetAuditCollection() { collection = { resourceIds: {}, presentations: [] }; }
|
|
31
|
+
export function recordFleetAuditResource(kind, id) {
|
|
32
|
+
if (id && collection)
|
|
33
|
+
collection.resourceIds[kind] = id;
|
|
34
|
+
}
|
|
35
|
+
export function recordFleetAuditPresentation(value) {
|
|
36
|
+
if (collection && FLEET_LIFECYCLE_EVENT_KINDS.includes(value.kind)) {
|
|
37
|
+
const basis = value.eventId ?? (value.kind === 'agent_started' ? value.actionId
|
|
38
|
+
: value.kind === 'task' || value.kind === 'room'
|
|
39
|
+
? `${value.operation}:${value.revision ?? `${value.previousState ?? 'none'}:${value.newState}`}`
|
|
40
|
+
: `${value.category}:${value.state}`);
|
|
41
|
+
collection.presentations.push(structuredClone({ ...value, eventId: basis }));
|
|
42
|
+
if (value.kind === 'task')
|
|
43
|
+
collection.resourceIds.task = value.id;
|
|
44
|
+
if (value.kind === 'room')
|
|
45
|
+
collection.resourceIds.room = value.id;
|
|
46
|
+
if (value.kind === 'agent_started')
|
|
47
|
+
collection.resourceIds.agent = value.id;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function recordFleetAuditFailure(failure) { if (collection)
|
|
51
|
+
collection.failure = failure; }
|
|
52
|
+
export function consumeFleetAuditCollection() {
|
|
53
|
+
const current = collection;
|
|
54
|
+
collection = undefined;
|
|
55
|
+
return {
|
|
56
|
+
...(current && Object.keys(current.resourceIds).length ? { resourceIds: current.resourceIds } : {}),
|
|
57
|
+
...(current?.presentations.length ? { presentations: current.presentations } : {}),
|
|
58
|
+
...(current?.failure ? { failure: current.failure } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const SAFE_READ = new Set(['docs', 'version', 'config', 'ls', 'peek', 'logs', 'status', 'doctor']);
|
|
62
|
+
const AGENT_SURFACES = {
|
|
63
|
+
spawn: new Set(['<none>']),
|
|
64
|
+
template: new Set(['list', 'show', 'validate']),
|
|
65
|
+
task: new Set(['create', 'list', 'lists', 'list-create', 'list-rename', 'list-delete', 'move',
|
|
66
|
+
'show', 'start', 'block', 'unblock', 'review', 'done', 'cancel', 'delete', 'recover', 'work', 'finish']),
|
|
67
|
+
room: new Set(['create', 'list', 'show', 'open', 'members', 'delete', 'close', 'recover']),
|
|
68
|
+
};
|
|
69
|
+
export const fleetProxyCommandInventory = Object.freeze(Object.fromEntries(Object.entries(AGENT_SURFACES).map(([surface, commands]) => [surface, [...commands]])));
|
|
70
|
+
const DENIED = new Set([
|
|
71
|
+
'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
|
|
72
|
+
'watchdog-run', 'watchdog-report', 'rm', 'init', 'migrate-agent-templates', 'migrate-role-defaults', 'web',
|
|
73
|
+
]);
|
|
74
|
+
export const fleetProxyTopLevelInventory = Object.freeze({
|
|
75
|
+
safeRead: [...SAFE_READ], agent: Object.keys(AGENT_SURFACES), public: [
|
|
76
|
+
'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
|
|
77
|
+
'watchdog-run', 'watchdog-report', 'rm', 'init', 'web',
|
|
78
|
+
], denied: [...DENIED],
|
|
79
|
+
hidden: ['_run', '_run-temp', '_run-watchdog', '_run-watchdogs'], aliases: ['man'],
|
|
80
|
+
});
|
|
81
|
+
const globalValueOptions = new Set(['-c', '--configuration']);
|
|
82
|
+
/** Classify before Commander parsing. Unknown and internal paths fail closed. */
|
|
83
|
+
export function classifyFleetArgv(argv) {
|
|
84
|
+
let command = '';
|
|
85
|
+
let commandIndex = -1;
|
|
86
|
+
for (let index = 0; index < argv.length; index++) {
|
|
87
|
+
const arg = argv[index];
|
|
88
|
+
if (arg === '--') {
|
|
89
|
+
command = argv[index + 1] ?? '';
|
|
90
|
+
commandIndex = index + 1;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (globalValueOptions.has(arg)) {
|
|
94
|
+
index++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (arg.startsWith('--configuration=') || arg.startsWith('-c='))
|
|
98
|
+
continue;
|
|
99
|
+
if (arg.startsWith('-c') && arg.length > 2)
|
|
100
|
+
continue;
|
|
101
|
+
if (arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V')
|
|
102
|
+
return { command: arg, route: 'supervisor-proxy/read-only', decision: 'allow' };
|
|
103
|
+
if (!arg.startsWith('-')) {
|
|
104
|
+
command = arg;
|
|
105
|
+
commandIndex = index;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!command)
|
|
110
|
+
return { command: '<none>', route: 'supervisor-proxy/unsupported', decision: 'unsupported' };
|
|
111
|
+
if (command.startsWith('_'))
|
|
112
|
+
return { command, route: 'supervisor-proxy/internal', decision: 'deny' };
|
|
113
|
+
if (SAFE_READ.has(command) || command === 'man')
|
|
114
|
+
return { command, route: 'supervisor-proxy/read-only', decision: 'allow' };
|
|
115
|
+
if (AGENT_SURFACES[command]) {
|
|
116
|
+
const sub = argv[commandIndex + 1] ?? '<none>';
|
|
117
|
+
if (sub === '--help' || sub === '-h')
|
|
118
|
+
return { command: `${command} ${sub}`, route: 'supervisor-proxy/read-only', decision: 'allow' };
|
|
119
|
+
if (sub.startsWith('_'))
|
|
120
|
+
return { command: `${command} ${sub}`, route: 'supervisor-proxy/internal', decision: 'deny' };
|
|
121
|
+
if (command !== 'spawn' && !AGENT_SURFACES[command].has(sub))
|
|
122
|
+
return { command: `${command} ${sub}`, route: `supervisor-proxy/${command}`, decision: 'allow' };
|
|
123
|
+
return { command: sub.startsWith('-') ? command : `${command} ${sub}`,
|
|
124
|
+
route: `supervisor-proxy/${command}`, decision: 'allow' };
|
|
125
|
+
}
|
|
126
|
+
// The proxy is attribution and routing, not a product permission boundary.
|
|
127
|
+
// Commander validates every public command. Hidden process entry points above
|
|
128
|
+
// remain guarded because recursively invoking them breaks the supervisor protocol.
|
|
129
|
+
return { command, route: 'supervisor-proxy/public', decision: 'allow' };
|
|
130
|
+
}
|
|
131
|
+
const marker = (value) => value === '' ? '[REDACTED:empty]' : '[REDACTED:value]';
|
|
132
|
+
const sensitiveValueFlags = new Set([
|
|
133
|
+
'--identity', '--invite', '--token', '--api-token', '--password', '--password-file',
|
|
134
|
+
'--env', '--brief', '--brief-file', '--bio-file', '--persona-file', '--isolation-file', '--loops-file',
|
|
135
|
+
'--configuration', '-c', '--public-invite', '--public-invite-file', '--invite-file',
|
|
136
|
+
'--summary-file', '--text', '--message', '--summary', '--reason', '--goal', '--cwd',
|
|
137
|
+
'--identity-cid', '--owner-cid', '--contact-cid', '--codex-config', '--add-dir',
|
|
138
|
+
]);
|
|
139
|
+
function redactUrl(value) {
|
|
140
|
+
try {
|
|
141
|
+
const url = new URL(value);
|
|
142
|
+
let changed = false;
|
|
143
|
+
if (url.username || url.password) {
|
|
144
|
+
url.username = 'REDACTED';
|
|
145
|
+
url.password = 'REDACTED';
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
for (const key of [...url.searchParams.keys()])
|
|
149
|
+
if (isSensitiveConfigKey(key)) {
|
|
150
|
+
url.searchParams.set(key, marker(url.searchParams.get(key) ?? ''));
|
|
151
|
+
changed = true;
|
|
152
|
+
}
|
|
153
|
+
return changed ? url.toString() : value;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function redactInline(value) {
|
|
160
|
+
if (!value.startsWith('inline:'))
|
|
161
|
+
return redactUrl(value);
|
|
162
|
+
const source = value.slice('inline:'.length);
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(source);
|
|
165
|
+
const walk = (node, key) => {
|
|
166
|
+
if (key && isSensitiveConfigKey(key))
|
|
167
|
+
return typeof node === 'string' ? marker(node) : '[REDACTED:value]';
|
|
168
|
+
if (Array.isArray(node))
|
|
169
|
+
return node.map(child => walk(child));
|
|
170
|
+
if (node && typeof node === 'object')
|
|
171
|
+
return Object.fromEntries(Object.entries(node).map(([childKey, child]) => [childKey, walk(child, childKey)]));
|
|
172
|
+
return typeof node === 'string' ? redactUrl(node) : node;
|
|
173
|
+
};
|
|
174
|
+
return `inline:${JSON.stringify(walk(parsed))}`;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return 'inline:[REDACTED:invalid-definition]';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Faithful ordered argv with deterministic structural redaction. */
|
|
181
|
+
export function redactFleetArgv(argv) {
|
|
182
|
+
const result = [...argv];
|
|
183
|
+
for (let index = 0; index < result.length; index++) {
|
|
184
|
+
const arg = result[index];
|
|
185
|
+
if (arg.startsWith('-c=') || (arg.startsWith('-c') && arg.length > 2 && !arg.startsWith('--'))) {
|
|
186
|
+
const prefix = arg.startsWith('-c=') ? '-c=' : '-c';
|
|
187
|
+
result[index] = `${prefix}${marker(arg.slice(prefix.length))}`;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const equal = /^(--[^=]+)=(.*)$/su.exec(arg);
|
|
191
|
+
if (equal && sensitiveValueFlags.has(equal[1])) {
|
|
192
|
+
result[index] = `${equal[1]}=${marker(equal[2])}`;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (sensitiveValueFlags.has(arg) && index + 1 < result.length) {
|
|
196
|
+
result[index + 1] = marker(result[index + 1]);
|
|
197
|
+
index++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
result[index] = redactInline(arg);
|
|
201
|
+
}
|
|
202
|
+
// `send <role> <text...>` has positional content. Locate it using the same
|
|
203
|
+
// global-option scan as classification, never absolute argv offsets.
|
|
204
|
+
let commandIndex = -1;
|
|
205
|
+
for (let index = 0; index < result.length; index++) {
|
|
206
|
+
const arg = result[index];
|
|
207
|
+
if (globalValueOptions.has(arg)) {
|
|
208
|
+
index++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (arg.startsWith('--configuration=') || arg.startsWith('-c=') || (arg.startsWith('-c') && arg.length > 2))
|
|
212
|
+
continue;
|
|
213
|
+
if (!arg.startsWith('-')) {
|
|
214
|
+
commandIndex = index;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (result[commandIndex] === 'send')
|
|
219
|
+
for (let index = commandIndex + 2; index < result.length; index++)
|
|
220
|
+
result[index] = marker(result[index]);
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
/** Durable write-once attempt ledger; contains redacted argv only. */
|
|
224
|
+
export class FleetCommandAuditStore {
|
|
225
|
+
path;
|
|
226
|
+
deps;
|
|
227
|
+
attempts = [];
|
|
228
|
+
constructor(path, deps = { now: () => new Date(), uuid: () => randomUUID() }) {
|
|
229
|
+
this.path = path;
|
|
230
|
+
this.deps = deps;
|
|
231
|
+
if (!existsSync(path))
|
|
232
|
+
return;
|
|
233
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
234
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.attempts))
|
|
235
|
+
throw new Error('invalid fleet command audit ledger');
|
|
236
|
+
this.attempts = parsed.attempts;
|
|
237
|
+
let recovered = false;
|
|
238
|
+
for (const attempt of this.attempts) {
|
|
239
|
+
if (attempt.invocation === 'sending') {
|
|
240
|
+
attempt.invocation = 'uncertain';
|
|
241
|
+
recovered = true;
|
|
242
|
+
}
|
|
243
|
+
if (attempt.outcome?.delivery === 'sending') {
|
|
244
|
+
attempt.outcome.delivery = 'uncertain';
|
|
245
|
+
recovered = true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (recovered)
|
|
249
|
+
this.persist();
|
|
250
|
+
}
|
|
251
|
+
list() { return this.attempts.map(item => structuredClone(item)); }
|
|
252
|
+
begin(requestId, caller, argv) {
|
|
253
|
+
const existing = this.attempts.find(item => item.caller === caller && item.requestId === requestId);
|
|
254
|
+
if (existing) {
|
|
255
|
+
if (JSON.stringify(existing.argv) !== JSON.stringify(redactFleetArgv(argv)))
|
|
256
|
+
throw new Error('fleet command request ID was reused with different argv');
|
|
257
|
+
return structuredClone(existing);
|
|
258
|
+
}
|
|
259
|
+
const attempt = { version: 1, correlationId: this.deps.uuid(), requestId,
|
|
260
|
+
caller, invokedAt: this.deps.now().toISOString(), classification: classifyFleetArgv(argv),
|
|
261
|
+
argv: redactFleetArgv(argv), invocation: 'sending' };
|
|
262
|
+
this.attempts.push(attempt);
|
|
263
|
+
this.persist();
|
|
264
|
+
return structuredClone(attempt);
|
|
265
|
+
}
|
|
266
|
+
invocation(correlationId, caller, delivery) {
|
|
267
|
+
const attempt = this.owned(correlationId, caller);
|
|
268
|
+
if (attempt.invocation !== 'sending' && attempt.invocation !== delivery)
|
|
269
|
+
throw new Error('conflicting fleet command invocation delivery state');
|
|
270
|
+
attempt.invocation = delivery;
|
|
271
|
+
this.persist();
|
|
272
|
+
return structuredClone(attempt);
|
|
273
|
+
}
|
|
274
|
+
finish(correlationId, caller, outcome) {
|
|
275
|
+
const attempt = this.owned(correlationId, caller);
|
|
276
|
+
if (!attempt.outcome)
|
|
277
|
+
attempt.outcome = { ...outcome, completedAt: this.deps.now().toISOString(), delivery: 'sending' };
|
|
278
|
+
else {
|
|
279
|
+
const prior = { class: attempt.outcome.class, effect: attempt.outcome.effect,
|
|
280
|
+
...(attempt.outcome.exitCode === undefined ? {} : { exitCode: attempt.outcome.exitCode }),
|
|
281
|
+
...(attempt.outcome.resourceIds ? { resourceIds: attempt.outcome.resourceIds } : {}),
|
|
282
|
+
...(attempt.outcome.presentations ? { presentations: attempt.outcome.presentations } : {}) };
|
|
283
|
+
if (JSON.stringify(prior) !== JSON.stringify(outcome))
|
|
284
|
+
throw new Error('conflicting fleet command outcome for existing correlation');
|
|
285
|
+
}
|
|
286
|
+
this.persist();
|
|
287
|
+
return structuredClone(attempt);
|
|
288
|
+
}
|
|
289
|
+
outcome(correlationId, caller, delivery) {
|
|
290
|
+
const attempt = this.owned(correlationId, caller);
|
|
291
|
+
if (!attempt.outcome)
|
|
292
|
+
throw new Error('fleet command audit outcome was not recorded');
|
|
293
|
+
if (attempt.outcome.delivery !== 'sending' && attempt.outcome.delivery !== delivery)
|
|
294
|
+
throw new Error('conflicting fleet command outcome delivery state');
|
|
295
|
+
attempt.outcome.delivery = delivery;
|
|
296
|
+
this.persist();
|
|
297
|
+
return structuredClone(attempt);
|
|
298
|
+
}
|
|
299
|
+
owned(correlationId, caller) {
|
|
300
|
+
const attempt = this.attempts.find(item => item.correlationId === correlationId);
|
|
301
|
+
if (!attempt || attempt.caller !== caller)
|
|
302
|
+
throw new Error('unknown or cross-role fleet command correlation');
|
|
303
|
+
return attempt;
|
|
304
|
+
}
|
|
305
|
+
persist() {
|
|
306
|
+
replaceFileAtomically(this.path, `${JSON.stringify({ version: 1, attempts: this.attempts })}\n`, 0o600);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
310
|
+
const SAFE_RESOURCE_ID = /^[\p{L}\p{N}][\p{L}\p{N}._:@-]{0,159}$/u;
|
|
311
|
+
const OUTCOMES = new Set(['success', 'validation', 'denied', 'runtime', 'timeout', 'proxy', 'delivery']);
|
|
312
|
+
const EFFECTS = new Set(['not_started', 'completed', 'unknown']);
|
|
313
|
+
export function validateFleetAuditBegin(value) {
|
|
314
|
+
const input = value;
|
|
315
|
+
if (!input || Object.keys(input).some(key => !['requestId', 'argv'].includes(key))
|
|
316
|
+
|| typeof input.requestId !== 'string' || !UUID.test(input.requestId)
|
|
317
|
+
|| !Array.isArray(input.argv) || input.argv.length > 256
|
|
318
|
+
|| !input.argv.every(arg => typeof arg === 'string' && Buffer.byteLength(arg) <= 16_384)
|
|
319
|
+
|| Buffer.byteLength(JSON.stringify(input.argv)) > 48 * 1024)
|
|
320
|
+
throw new Error('invalid fleet audit begin fields');
|
|
321
|
+
}
|
|
322
|
+
export function validateFleetAuditFinish(value) {
|
|
323
|
+
const input = value;
|
|
324
|
+
const allowed = ['correlationId', 'class', 'exitCode', 'effect', 'resourceIds', 'presentations'];
|
|
325
|
+
const resources = input?.resourceIds;
|
|
326
|
+
if (!input || Object.keys(input).some(key => !allowed.includes(key))
|
|
327
|
+
|| typeof input.correlationId !== 'string' || !UUID.test(input.correlationId)
|
|
328
|
+
|| !OUTCOMES.has(input.class) || !EFFECTS.has(String(input.effect))
|
|
329
|
+
|| (input.exitCode !== undefined && (!Number.isSafeInteger(input.exitCode) || Number(input.exitCode) < 0 || Number(input.exitCode) > 255))
|
|
330
|
+
|| (resources !== undefined && (!resources || typeof resources !== 'object' || Array.isArray(resources)
|
|
331
|
+
|| Object.entries(resources).some(([key, id]) => !['agent', 'task', 'room'].includes(key)
|
|
332
|
+
|| typeof id !== 'string' || !SAFE_RESOURCE_ID.test(id))))
|
|
333
|
+
|| (input.presentations !== undefined && (!Array.isArray(input.presentations)
|
|
334
|
+
|| input.presentations.length > 128 || !input.presentations.every(validPresentation))))
|
|
335
|
+
throw new Error('invalid fleet audit finish fields');
|
|
336
|
+
}
|
|
337
|
+
const APPROVAL_MODES = new Set(['ask', 'auto', 'allow', 'deny']);
|
|
338
|
+
const FILESYSTEM_MODES = new Set(['read-only', 'workspace', 'unrestricted']);
|
|
339
|
+
const UNATTENDED_MODES = new Set(['deny', 'wait']);
|
|
340
|
+
const FLEET_PERMISSION_MODES = new Set(['ask', 'auto', 'allow']);
|
|
341
|
+
const PRESENTATION_TEXT = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u;
|
|
342
|
+
/**
|
|
343
|
+
* Exact-key validation of a current-version launch configuration. A partial
|
|
344
|
+
* v1 configuration is rejected; a legacy presentation simply omits the field.
|
|
345
|
+
*/
|
|
346
|
+
function validConfiguration(value) {
|
|
347
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
348
|
+
return false;
|
|
349
|
+
const c = value;
|
|
350
|
+
const text = (v, max = 256) => typeof v === 'string' && v.length <= max
|
|
351
|
+
&& !PRESENTATION_TEXT.test(v);
|
|
352
|
+
const exact = (record, keys) => Object.keys(record).every(key => keys.includes(key));
|
|
353
|
+
const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
354
|
+
const origin = (v) => {
|
|
355
|
+
if (!record(v))
|
|
356
|
+
return false;
|
|
357
|
+
if (v.kind === 'named')
|
|
358
|
+
return exact(v, ['kind', 'ref']) && text(v.ref, 160);
|
|
359
|
+
if (v.kind === 'inline')
|
|
360
|
+
return exact(v, ['kind', 'fingerprint'])
|
|
361
|
+
&& (v.fingerprint === undefined || (typeof v.fingerprint === 'string' && /^[a-f0-9]{12}$/.test(v.fingerprint)));
|
|
362
|
+
return v.kind === 'unknown' && exact(v, ['kind']);
|
|
363
|
+
};
|
|
364
|
+
const count = (v) => v === undefined
|
|
365
|
+
|| (Number.isSafeInteger(v) && Number(v) >= 0 && Number(v) <= 4_096);
|
|
366
|
+
return exact(c, ['version', 'template', 'role', 'brain', 'harness', 'session', 'model',
|
|
367
|
+
'effort', 'mission', 'approval', 'filesystem', 'unattended', 'permissionMode', 'monitor', 'isolation', 'loops'])
|
|
368
|
+
&& c.version === 1 && origin(c.role) && origin(c.brain)
|
|
369
|
+
&& text(c.harness, 64) && SESSION_BACKENDS.has(c.session)
|
|
370
|
+
&& (c.model === null || text(c.model, 160))
|
|
371
|
+
&& (c.template === undefined || text(c.template, 160))
|
|
372
|
+
&& (c.effort === undefined || text(c.effort, 32))
|
|
373
|
+
// The builder caps the mission label at 80 code points; enforce the same
|
|
374
|
+
// invariant here so the per-line budget proof holds for wire input too.
|
|
375
|
+
&& (c.mission === undefined || (text(c.mission) && Array.from(String(c.mission)).length <= 80))
|
|
376
|
+
&& APPROVAL_MODES.has(String(c.approval))
|
|
377
|
+
&& FILESYSTEM_MODES.has(String(c.filesystem))
|
|
378
|
+
&& UNATTENDED_MODES.has(String(c.unattended))
|
|
379
|
+
&& record(c.permissionMode)
|
|
380
|
+
&& exact(c.permissionMode, ['fleetMode', 'nativeMode'])
|
|
381
|
+
&& FLEET_PERMISSION_MODES.has(String(c.permissionMode.fleetMode))
|
|
382
|
+
&& text(c.permissionMode.nativeMode, 160)
|
|
383
|
+
&& record(c.monitor) && exact(c.monitor, ['mode', 'interrupt'])
|
|
384
|
+
&& ['fleet', 'native'].includes(String(c.monitor.mode))
|
|
385
|
+
&& (typeof c.monitor.interrupt === 'boolean' || c.monitor.interrupt === 'after_tool')
|
|
386
|
+
&& (c.isolation === undefined || (record(c.isolation)
|
|
387
|
+
&& exact(c.isolation, ['requested', 'on_unavailable', 'network', 'read_mounts', 'write_mounts'])
|
|
388
|
+
&& ['auto', 'bubblewrap', 'podman', 'none'].includes(String(c.isolation.requested))
|
|
389
|
+
&& (c.isolation.on_unavailable === undefined || ['warn', 'strict'].includes(String(c.isolation.on_unavailable)))
|
|
390
|
+
&& (c.isolation.network === undefined || ['broker', 'deny', 'allow', 'allowlist'].includes(String(c.isolation.network)))
|
|
391
|
+
&& count(c.isolation.read_mounts) && count(c.isolation.write_mounts)))
|
|
392
|
+
&& (c.loops === undefined || (record(c.loops)
|
|
393
|
+
&& exact(c.loops, ['source', 'policy', 'entries'])
|
|
394
|
+
&& ['agent-template', 'cli', 'omitted'].includes(String(c.loops.source))
|
|
395
|
+
&& c.loops.policy === 'skip-if-busy'
|
|
396
|
+
&& Array.isArray(c.loops.entries) && c.loops.entries.length <= 64
|
|
397
|
+
&& c.loops.entries.every(entry => record(entry)
|
|
398
|
+
&& exact(entry, ['name', 'enabled', 'intervalMs', 'initialDelayMs', 'jitterMs', 'prompt'])
|
|
399
|
+
&& text(entry.name, 64) && typeof entry.enabled === 'boolean'
|
|
400
|
+
&& [entry.intervalMs, entry.initialDelayMs, entry.jitterMs].every(Number.isSafeInteger)
|
|
401
|
+
&& record(entry.prompt) && exact(entry.prompt, ['bytes', 'sha256'])
|
|
402
|
+
&& Number.isSafeInteger(entry.prompt.bytes) && Number(entry.prompt.bytes) >= 0
|
|
403
|
+
&& typeof entry.prompt.sha256 === 'string' && /^[a-f0-9]{64}$/.test(entry.prompt.sha256))))
|
|
404
|
+
// Escaping and code-fence growth can push a per-field-valid configuration
|
|
405
|
+
// past the line budget; a v1 configuration whose complete mandatory
|
|
406
|
+
// rendering cannot fit is invalid, never silently trimmed.
|
|
407
|
+
&& mandatoryConfigurationFits(c);
|
|
408
|
+
}
|
|
409
|
+
function validPresentation(value) {
|
|
410
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
411
|
+
return false;
|
|
412
|
+
const p = value;
|
|
413
|
+
const configured = (record) => record.configuration === undefined || validConfiguration(record.configuration);
|
|
414
|
+
const safe = (v) => typeof v === 'string' && SAFE_RESOURCE_ID.test(v);
|
|
415
|
+
const text = (v) => typeof v === 'string' && v.length <= 256
|
|
416
|
+
&& !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(v);
|
|
417
|
+
const exact = (record, keys) => Object.keys(record).every(key => keys.includes(key));
|
|
418
|
+
const transition = (allowed) => typeof p.operation === 'string' && typeof p.newState === 'string'
|
|
419
|
+
&& allowed[p.operation]?.includes(`${String(p.previousState ?? 'none')}→${p.newState}`);
|
|
420
|
+
if (p.kind === 'agent_started')
|
|
421
|
+
return exact(p, ['kind', 'eventId', 'id', 'name', 'lifetime', 'brain', 'role',
|
|
422
|
+
'harness', 'session', 'model', 'permissions', 'parent', 'actionId', 'inherited', 'configuration'])
|
|
423
|
+
&& safe(p.eventId) && safe(p.id) && safe(p.name) && ['permanent', 'temporary'].includes(String(p.lifetime))
|
|
424
|
+
&& text(p.brain) && text(p.role) && safe(p.harness)
|
|
425
|
+
&& SESSION_BACKENDS.has(p.session) && safe(p.parent) && safe(p.actionId)
|
|
426
|
+
&& (p.model === undefined || text(p.model)) && (p.permissions === undefined || text(p.permissions))
|
|
427
|
+
&& Array.isArray(p.inherited) && p.inherited.every(text) && configured(p);
|
|
428
|
+
if (p.kind === 'task')
|
|
429
|
+
return exact(p, ['kind', 'operation', 'eventId', 'id', 'title', 'previousState',
|
|
430
|
+
'newState', 'template', 'roomId', 'revision', 'list', 'agents'])
|
|
431
|
+
&& ['create', 'start', 'work', 'block', 'unblock', 'review', 'done',
|
|
432
|
+
'cancel', 'finish', 'delete', 'settling'].includes(String(p.operation)) && safe(p.eventId) && safe(p.id) && safe(p.newState)
|
|
433
|
+
&& transition({ create: ['none→backlog', 'none→provisioning', 'none→active'],
|
|
434
|
+
start: ['backlog→provisioning'], work: ['provisioning→active'],
|
|
435
|
+
block: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
|
|
436
|
+
unblock: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
|
|
437
|
+
review: ['active→review'], done: ['review→done'], finish: ['active→done', 'review→done'],
|
|
438
|
+
cancel: ['backlog→cancelled', 'provisioning→cancelled', 'active→cancelled', 'review→cancelled'],
|
|
439
|
+
delete: [
|
|
440
|
+
'backlog→deleting', 'provisioning→deleting', 'active→deleting', 'review→deleting',
|
|
441
|
+
'done→deleting', 'cancelled→deleting', 'failed→deleting',
|
|
442
|
+
'backlog→deleted', 'provisioning→deleted', 'active→deleted', 'review→deleted',
|
|
443
|
+
'done→deleted', 'cancelled→deleted', 'failed→deleted',
|
|
444
|
+
], settling: ['active→active', 'review→review', 'backlog→backlog',
|
|
445
|
+
'provisioning→provisioning'] })
|
|
446
|
+
&& (p.title === undefined || text(p.title)) && (p.previousState === undefined || safe(p.previousState))
|
|
447
|
+
&& (p.template === undefined || text(p.template)) && (p.roomId === undefined || safe(p.roomId))
|
|
448
|
+
&& (p.list === undefined || text(p.list)) && (p.revision === undefined || text(p.revision))
|
|
449
|
+
&& Array.isArray(p.agents) && p.agents.length <= 64
|
|
450
|
+
&& p.agents.every(a => a && typeof a === 'object'
|
|
451
|
+
&& exact(a, ['name', 'brain', 'role', 'permissions', 'configuration'])
|
|
452
|
+
&& safe(a.name)
|
|
453
|
+
&& text(a.role)
|
|
454
|
+
&& (a.brain === undefined || text(a.brain))
|
|
455
|
+
&& (a.permissions === undefined || text(a.permissions))
|
|
456
|
+
&& configured(a));
|
|
457
|
+
if (p.kind === 'room')
|
|
458
|
+
return exact(p, ['kind', 'operation', 'eventId', 'id', 'previousState', 'newState',
|
|
459
|
+
'revision', 'name', 'template', 'taskId', 'participants'])
|
|
460
|
+
&& ['create', 'activate', 'close', 'delete'].includes(String(p.operation))
|
|
461
|
+
&& safe(p.eventId) && safe(p.id) && safe(p.newState)
|
|
462
|
+
&& transition({ create: ['none→provisioning'], activate: ['provisioning→active'],
|
|
463
|
+
close: ['active→closing', 'provisioning→closing'],
|
|
464
|
+
delete: ['closing→deleted'] })
|
|
465
|
+
&& (p.previousState === undefined || safe(p.previousState))
|
|
466
|
+
&& (p.name === undefined || text(p.name)) && (p.template === undefined || text(p.template))
|
|
467
|
+
&& (p.taskId === undefined || safe(p.taskId)) && (p.revision === undefined || text(p.revision))
|
|
468
|
+
&& Array.isArray(p.participants) && p.participants.length <= 64
|
|
469
|
+
&& p.participants.every(a => a && typeof a === 'object'
|
|
470
|
+
&& exact(a, ['name', 'id', 'brain', 'role', 'permissions', 'configuration'])
|
|
471
|
+
&& safe(a.name)
|
|
472
|
+
&& text(a.role)
|
|
473
|
+
&& (a.id === undefined || safe(a.id))
|
|
474
|
+
&& (a.brain === undefined || text(a.brain))
|
|
475
|
+
&& (a.permissions === undefined || text(a.permissions))
|
|
476
|
+
&& configured(a));
|
|
477
|
+
if (p.kind === 'lifecycle_failure')
|
|
478
|
+
return exact(p, ['kind', 'eventId', 'resource', 'id', 'state', 'category'])
|
|
479
|
+
&& ['Agent', 'Task', 'Room'].includes(String(p.resource))
|
|
480
|
+
&& safe(p.eventId) && safe(p.id) && safe(p.state) && ['provision_failed', 'provision_pending', 'readiness_failed', 'settlement_failed',
|
|
481
|
+
'settlement_pending', 'cleanup_failed', 'cleanup_pending'].includes(String(p.category));
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
const messageCodePoints = (value) => Array.from(value).length;
|
|
485
|
+
const messageBytes = (value) => Buffer.byteLength(value, 'utf8');
|
|
486
|
+
const withinMessageBounds = (value) => messageCodePoints(value) <= MARKDOWN_MAX_CODE_POINTS && messageBytes(value) <= MARKDOWN_MAX_BYTES;
|
|
487
|
+
/**
|
|
488
|
+
* Append per-agent lines to a bounded message. Lines are admitted whole, in
|
|
489
|
+
* the given (deterministic) order; once the shared Markdown bounds would be
|
|
490
|
+
* exceeded the remaining agents collapse into an accurate omission note. The
|
|
491
|
+
* first line is admitted whenever it fits the absolute bounds so an oversized
|
|
492
|
+
* roster still shows at least one complete agent summary.
|
|
493
|
+
*/
|
|
494
|
+
function appendAgentLines(header, heading, lines) {
|
|
495
|
+
if (!lines.length)
|
|
496
|
+
return header;
|
|
497
|
+
let message = `${header}\n${heading}:`;
|
|
498
|
+
const note = (count) => `\n…and ${count} more agent${count === 1 ? '' : 's'} omitted.`;
|
|
499
|
+
for (const [index, line] of lines.entries()) {
|
|
500
|
+
const candidate = `${message}\n- ${line}`;
|
|
501
|
+
const remaining = lines.length - index - 1;
|
|
502
|
+
// A candidate is admitted only when it fits together with the note for
|
|
503
|
+
// every agent still pending, so each stop-return below re-states a bound
|
|
504
|
+
// that was verified when the current message was admitted.
|
|
505
|
+
if (!withinMessageBounds(remaining ? `${candidate}${note(remaining)}` : candidate)) {
|
|
506
|
+
const stopped = `${message}${note(lines.length - index)}`;
|
|
507
|
+
return withinMessageBounds(stopped) ? stopped : `${header}${note(lines.length)}`;
|
|
508
|
+
}
|
|
509
|
+
message = candidate;
|
|
510
|
+
}
|
|
511
|
+
return message;
|
|
512
|
+
}
|
|
513
|
+
/** Compact Owner presentation. It intentionally has no command, argv, environment, or correlation data. */
|
|
514
|
+
export function renderFleetLifecycleEvent(value) {
|
|
515
|
+
if (value.kind === 'lifecycle_failure') {
|
|
516
|
+
const actions = {
|
|
517
|
+
provision_failed: 'Inspect Fleet service logs, correct configuration, then recover the resource.',
|
|
518
|
+
provision_pending: 'Run Task or Room recover after checking member readiness.',
|
|
519
|
+
readiness_failed: 'Inspect the Agent log, correct its configuration, then retry creation.',
|
|
520
|
+
settlement_failed: 'Inspect Fleet service logs, then run Task recover.',
|
|
521
|
+
settlement_pending: 'Run Task recover to continue settlement.',
|
|
522
|
+
cleanup_failed: 'Inspect Fleet service logs, then run Room recover.',
|
|
523
|
+
cleanup_pending: 'Run Room recover to continue cleanup.',
|
|
524
|
+
};
|
|
525
|
+
const lifecycle = value.category.endsWith('_pending') ? 'lifecycle pending' : 'lifecycle failure';
|
|
526
|
+
return `⚠️ ${value.resource} ${value.id} ${lifecycle} (${value.category}); `
|
|
527
|
+
+ `state ${value.state}. Action: ${actions[value.category]}`;
|
|
528
|
+
}
|
|
529
|
+
if (value.kind === 'agent_started') {
|
|
530
|
+
const summary = renderAgentConfiguration(value.configuration, {
|
|
531
|
+
role: value.role, brain: value.brain, permissions: value.permissions,
|
|
532
|
+
});
|
|
533
|
+
return `🧑💻 ${value.parent} spawned ${value.lifetime} Agent ${value.name} (${value.id}) — ready. `
|
|
534
|
+
+ `${summary}.`;
|
|
535
|
+
}
|
|
536
|
+
if (value.kind === 'task') {
|
|
537
|
+
const title = value.title ? ` “${markdownProse(value.title)}”` : '';
|
|
538
|
+
const context = [value.list ? `List ${markdownCode(value.list)}` : undefined,
|
|
539
|
+
value.template ? `template ${markdownCode(value.template)}` : undefined,
|
|
540
|
+
value.roomId ? `Room ${value.roomId}` : undefined].filter(Boolean).join('; ');
|
|
541
|
+
const header = `📋 Task${title} (${value.id}) ${value.operation}: `
|
|
542
|
+
+ `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}`;
|
|
543
|
+
return appendAgentLines(header, 'Agents', value.agents.map(agent => `${markdownCode(agent.name)}: ${renderAgentConfiguration(agent.configuration, {
|
|
544
|
+
role: agent.role, brain: agent.brain, permissions: agent.permissions,
|
|
545
|
+
})}`));
|
|
546
|
+
}
|
|
547
|
+
const context = [value.template ? `template ${markdownCode(value.template)}` : undefined,
|
|
548
|
+
value.taskId ? `Task ${value.taskId}` : undefined].filter(Boolean).join('; ');
|
|
549
|
+
const header = `🏠 Room${value.name ? ` “${markdownProse(value.name)}”` : ''} (${value.id}) ${value.operation}: `
|
|
550
|
+
+ `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}`;
|
|
551
|
+
return appendAgentLines(header, 'Participants', value.participants.map(participant => `${markdownCode(participant.name)}${participant.id ? ` (${participant.id})` : ''}: `
|
|
552
|
+
+ renderAgentConfiguration(participant.configuration, {
|
|
553
|
+
role: participant.role, brain: participant.brain, permissions: participant.permissions,
|
|
554
|
+
})));
|
|
555
|
+
}
|
package/dist/fleet-proxy.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { MonitorConfig, ResolvedRole } from './config.js';
|
|
1
|
+
import type { MonitorConfig, ResolvedRole, SessionBackendId } from './config.js';
|
|
2
|
+
import type { AgentLaunchConfiguration } from './lifecycle-summary.js';
|
|
2
3
|
import type { SpawnOpts } from './spawn.js';
|
|
3
4
|
/** Present only inside a managed role process. The CLI treats it as a routing hint, not authority. */
|
|
4
5
|
export declare const FLEET_PROXY_STATE_DIR_ENV = "OURS_FLEET_PROXY_STATE_DIR";
|
|
@@ -9,7 +10,7 @@ export interface ManagedFleetSpawnResult {
|
|
|
9
10
|
lifetime: 'permanent' | 'temporary';
|
|
10
11
|
statePath: string;
|
|
11
12
|
harness: string;
|
|
12
|
-
session:
|
|
13
|
+
session: SessionBackendId;
|
|
13
14
|
model?: string;
|
|
14
15
|
monitor: Pick<MonitorConfig, 'mode' | 'interrupt'>;
|
|
15
16
|
/** Adapter-resolved portable policy and exact native runtime mode. */
|
|
@@ -19,6 +20,10 @@ export interface ManagedFleetSpawnResult {
|
|
|
19
20
|
};
|
|
20
21
|
inherited: string[];
|
|
21
22
|
creationActionId: string;
|
|
23
|
+
brainSummary: string;
|
|
24
|
+
roleSummary: string;
|
|
25
|
+
/** Launch configuration captured from the exact resolved role; absent only from pre-upgrade daemons. */
|
|
26
|
+
configuration?: AgentLaunchConfiguration;
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* Fill only omitted spawn settings from the live caller. Explicit agent choices
|
package/dist/fleet-proxy.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { inheritedPermissionMode } from './permissions.js';
|
|
2
|
+
import { isSensitiveConfigKey } from './sensitive-config.js';
|
|
2
3
|
/** Present only inside a managed role process. The CLI treats it as a routing hint, not authority. */
|
|
3
4
|
export const FLEET_PROXY_STATE_DIR_ENV = 'OURS_FLEET_PROXY_STATE_DIR';
|
|
4
5
|
export const FLEET_PROXY_CALLER_ENV = 'OURS_FLEET_PROXY_CALLER';
|
|
@@ -9,15 +10,41 @@ export const FLEET_PROXY_CALLER_ENV = 'OURS_FLEET_PROXY_CALLER';
|
|
|
9
10
|
export function inheritCallerSpawnDefaults(caller, requested, configPath) {
|
|
10
11
|
const options = { ...requested };
|
|
11
12
|
const inherited = [];
|
|
13
|
+
if (requested.agentDefinition) {
|
|
14
|
+
options.configPath = configPath;
|
|
15
|
+
options.surface = 'agent';
|
|
16
|
+
options.callerRole = caller.name;
|
|
17
|
+
options.inheritedFromCaller = [];
|
|
18
|
+
return { options, inherited };
|
|
19
|
+
}
|
|
12
20
|
const take = (key, value) => {
|
|
13
21
|
if (options[key] !== undefined || value === undefined)
|
|
14
22
|
return;
|
|
15
23
|
options[key] = value;
|
|
16
24
|
inherited.push(String(key));
|
|
17
25
|
};
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
const sensitive = (value) => {
|
|
27
|
+
if (Array.isArray(value))
|
|
28
|
+
return value.some(sensitive);
|
|
29
|
+
if (!value || typeof value !== 'object')
|
|
30
|
+
return false;
|
|
31
|
+
return Object.entries(value)
|
|
32
|
+
.some(([key, child]) => isSensitiveConfigKey(key) || sensitive(child));
|
|
33
|
+
};
|
|
34
|
+
if (options.brain === undefined) {
|
|
35
|
+
const brain = caller.agentSelections?.brain;
|
|
36
|
+
if (!brain)
|
|
37
|
+
throw new Error('caller has no inspectable Brain selection; pass --brain explicitly');
|
|
38
|
+
if ('inline' in brain && sensitive(brain.inline))
|
|
39
|
+
throw new Error('caller inline Brain contains sensitive configuration; pass --brain explicitly');
|
|
40
|
+
take('brain', structuredClone(brain));
|
|
41
|
+
}
|
|
42
|
+
if (options.role === undefined) {
|
|
43
|
+
const role = caller.agentSelections?.role;
|
|
44
|
+
if (!role)
|
|
45
|
+
throw new Error('caller has no inspectable Role selection; pass --role explicitly');
|
|
46
|
+
take('role', structuredClone(role));
|
|
47
|
+
}
|
|
21
48
|
take('cwd', caller.cwd);
|
|
22
49
|
take('coordinator', caller.name);
|
|
23
50
|
if (options.approval === undefined)
|
|
@@ -25,11 +52,6 @@ export function inheritCallerSpawnDefaults(caller, requested, configPath) {
|
|
|
25
52
|
take('filesystem', caller.permissions.filesystem);
|
|
26
53
|
take('unattended', caller.permissions.unattended);
|
|
27
54
|
take('monitorConfig', structuredClone(caller.monitor));
|
|
28
|
-
// A model name and native harness options are not portable across harnesses.
|
|
29
|
-
// When the caller explicitly switches harness, let that harness/fleet defaults
|
|
30
|
-
// select its model instead of copying (for example) a Codex model into Claude.
|
|
31
|
-
if (sameHarness)
|
|
32
|
-
take('model', caller.model);
|
|
33
55
|
options.configPath = configPath;
|
|
34
56
|
options.surface = 'agent';
|
|
35
57
|
options.callerRole = caller.name;
|