@ours.network/fleet 1.1.0-nightly.3 → 1.1.0-nightly.30
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 +244 -40
- 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 +4 -4
- package/dist/application/role-creation-service.js +19 -1
- 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 +93 -40
- package/dist/application/task-room-service.js +579 -173
- package/dist/briefing.js +85 -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 +217 -51
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +38 -3
- package/dist/config.js +240 -28
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- 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 +213 -42
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +54 -4
- package/dist/fleet-command-audit.d.ts +181 -0
- package/dist/fleet-command-audit.js +738 -0
- package/dist/fleet-proxy.d.ts +7 -2
- 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 +4 -3
- package/dist/index.js +3 -2
- 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 +631 -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-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +75 -3
- package/dist/owner-channel/channel.js +531 -123
- package/dist/owner-channel/commands.d.ts +28 -6
- package/dist/owner-channel/commands.js +112 -36
- package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
- package/dist/owner-channel/lifecycle-outbox.js +87 -0
- package/dist/owner-channel/ours-client.d.ts +19 -0
- package/dist/owner-channel/ours-client.js +3 -0
- 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 +450 -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 +519 -261
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +23 -7
- package/dist/rooms-tasks/config.js +16 -29
- package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
- package/dist/rooms-tasks/cowork-adapter.js +13 -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 +9 -0
- package/dist/rooms-tasks/external-worker.js +52 -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/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +9 -2
- package/dist/rooms-tasks/provision.js +146 -56
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-room-name.d.ts +12 -0
- package/dist/rooms-tasks/task-room-name.js +61 -0
- 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 +10 -4
- package/dist/rooms-tasks/types.d.ts +94 -8
- package/dist/rooms-tasks/types.js +14 -1
- package/dist/runner.d.ts +12 -2
- package/dist/runner.js +167 -56
- 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 +51 -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 +9 -0
- package/dist/spawn.js +77 -9
- package/dist/temp-lifecycle.d.ts +1 -1
- 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-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
- package/dist/web-app/index.html +1 -1
- package/package.json +5 -4
- package/presets/brain-catalog.json +17 -0
- package/presets/fleet/agent_templates/Critic.yaml +5 -0
- package/presets/fleet/agent_templates/Developer.yaml +5 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -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/brains/codex-gpt-6-astra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +30 -0
- package/presets/fleet/roles/Critic.yaml +28 -0
- package/presets/fleet/roles/Developer.yaml +28 -0
- package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
- package/presets/fleet/room_templates/pair.yaml +13 -0
- package/presets/fleet/room_templates/single.yaml +9 -0
- package/presets/fleet/room_templates/team.yaml +16 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +6 -0
|
@@ -0,0 +1,738 @@
|
|
|
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 } 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
|
+
let lifecycleCheckpoint;
|
|
32
|
+
export function setFleetAuditLifecycleCheckpoint(checkpoint) { lifecycleCheckpoint = checkpoint; }
|
|
33
|
+
/**
|
|
34
|
+
* Deliver already-recorded lifecycle events while a managed command is still
|
|
35
|
+
* running. On a failed checkpoint the events remain in the final audit batch,
|
|
36
|
+
* so a temporary Owner-channel outage loses timing, never the notice itself.
|
|
37
|
+
*/
|
|
38
|
+
export async function checkpointFleetAuditPresentations() {
|
|
39
|
+
if (!collection || !lifecycleCheckpoint || !collection.presentations.length)
|
|
40
|
+
return;
|
|
41
|
+
const pending = collection.presentations.splice(0);
|
|
42
|
+
try {
|
|
43
|
+
await lifecycleCheckpoint(structuredClone(pending));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
collection.presentations.unshift(...pending);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function recordFleetAuditResource(kind, id) {
|
|
50
|
+
if (id && collection)
|
|
51
|
+
collection.resourceIds[kind] = id;
|
|
52
|
+
}
|
|
53
|
+
const PRESENTATION_LABEL_MAX_CODE_POINTS = 160;
|
|
54
|
+
const PRESENTATION_LABEL_MAX_BYTES = 640;
|
|
55
|
+
const PRESENTATION_LABEL_CONTROLS = /[\u0000-\u001f\u007f-\u009f]/gu;
|
|
56
|
+
const PRESENTATION_LABEL_SPOOFING = /[\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff]/gu;
|
|
57
|
+
/** Immutable, single-line human label captured before mutable Task/Room state can disappear. */
|
|
58
|
+
export function fleetPresentationLabel(value) {
|
|
59
|
+
const normalized = String(value ?? '')
|
|
60
|
+
.replace(/\r\n?/gu, '\n')
|
|
61
|
+
.replace(PRESENTATION_LABEL_SPOOFING, '�')
|
|
62
|
+
.replace(PRESENTATION_LABEL_CONTROLS, ' ')
|
|
63
|
+
.replace(/\s+/gu, ' ')
|
|
64
|
+
.trim();
|
|
65
|
+
if (!normalized)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (Array.from(normalized).length <= PRESENTATION_LABEL_MAX_CODE_POINTS
|
|
68
|
+
&& Buffer.byteLength(normalized, 'utf8') <= PRESENTATION_LABEL_MAX_BYTES)
|
|
69
|
+
return normalized;
|
|
70
|
+
let bounded = '';
|
|
71
|
+
for (const point of normalized) {
|
|
72
|
+
const candidate = `${bounded}${point}…`;
|
|
73
|
+
if (Array.from(candidate).length > PRESENTATION_LABEL_MAX_CODE_POINTS
|
|
74
|
+
|| Buffer.byteLength(candidate, 'utf8') > PRESENTATION_LABEL_MAX_BYTES)
|
|
75
|
+
break;
|
|
76
|
+
bounded += point;
|
|
77
|
+
}
|
|
78
|
+
return `${bounded.replace(/\s+$/u, '')}…`;
|
|
79
|
+
}
|
|
80
|
+
export function recordFleetAuditPresentation(value) {
|
|
81
|
+
if (collection && FLEET_LIFECYCLE_EVENT_KINDS.includes(value.kind)) {
|
|
82
|
+
const basis = value.eventId ?? (value.kind === 'agent_started' ? value.actionId
|
|
83
|
+
: value.kind === 'task' || value.kind === 'room'
|
|
84
|
+
? `${value.operation}:${value.revision ?? `${value.previousState ?? 'none'}:${value.newState}`}`
|
|
85
|
+
: `${value.category}:${value.state}`);
|
|
86
|
+
const labels = value.kind === 'task'
|
|
87
|
+
? { title: fleetPresentationLabel(value.title), roomName: fleetPresentationLabel(value.roomName),
|
|
88
|
+
reason: fleetPresentationLabel(value.reason) }
|
|
89
|
+
: value.kind === 'room'
|
|
90
|
+
? { name: fleetPresentationLabel(value.name) }
|
|
91
|
+
: value.kind === 'lifecycle_failure'
|
|
92
|
+
? { label: fleetPresentationLabel(value.label) }
|
|
93
|
+
: {};
|
|
94
|
+
const presentation = structuredClone({ ...value, ...labels, eventId: basis });
|
|
95
|
+
const digest = lifecycleEventDigestBasis(presentation);
|
|
96
|
+
if (!collection.presentations.some(existing => lifecycleEventDigestBasis(existing) === digest))
|
|
97
|
+
collection.presentations.push(presentation);
|
|
98
|
+
if (value.kind === 'task')
|
|
99
|
+
collection.resourceIds.task = value.id;
|
|
100
|
+
if (value.kind === 'room')
|
|
101
|
+
collection.resourceIds.room = value.id;
|
|
102
|
+
if (value.kind === 'agent_started')
|
|
103
|
+
collection.resourceIds.agent = value.id;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function recordFleetAuditFailure(failure) { if (collection)
|
|
107
|
+
collection.failure = failure; }
|
|
108
|
+
export function consumeFleetAuditCollection() {
|
|
109
|
+
const current = collection;
|
|
110
|
+
collection = undefined;
|
|
111
|
+
return {
|
|
112
|
+
...(current && Object.keys(current.resourceIds).length ? { resourceIds: current.resourceIds } : {}),
|
|
113
|
+
...(current?.presentations.length ? { presentations: current.presentations } : {}),
|
|
114
|
+
...(current?.failure ? { failure: current.failure } : {}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const SAFE_READ = new Set(['docs', 'version', 'config', 'ls', 'peek', 'logs', 'status', 'doctor']);
|
|
118
|
+
const AGENT_SURFACES = {
|
|
119
|
+
spawn: new Set(['<none>']),
|
|
120
|
+
template: new Set(['list', 'show', 'validate']),
|
|
121
|
+
task: new Set(['create', 'list', 'lists', 'list-create', 'list-rename', 'list-delete', 'move',
|
|
122
|
+
'show', 'start', 'block', 'unblock', 'review', 'done', 'cancel', 'delete', 'work', 'finish']),
|
|
123
|
+
room: new Set(['create', 'list', 'show', 'open', 'members', 'delete', 'close']),
|
|
124
|
+
};
|
|
125
|
+
export const fleetProxyCommandInventory = Object.freeze(Object.fromEntries(Object.entries(AGENT_SURFACES).map(([surface, commands]) => [surface, [...commands]])));
|
|
126
|
+
const DENIED = new Set([
|
|
127
|
+
'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
|
|
128
|
+
'watchdog-run', 'watchdog-report', 'rm', 'init', 'migrate-agent-templates', 'migrate-role-defaults', 'web',
|
|
129
|
+
]);
|
|
130
|
+
export const fleetProxyTopLevelInventory = Object.freeze({
|
|
131
|
+
safeRead: [...SAFE_READ], agent: Object.keys(AGENT_SURFACES), public: [
|
|
132
|
+
'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
|
|
133
|
+
'watchdog-run', 'watchdog-report', 'rm', 'init', 'web',
|
|
134
|
+
], denied: [...DENIED],
|
|
135
|
+
hidden: ['_run', '_run-temp', '_run-watchdog', '_run-watchdogs'], aliases: ['man'],
|
|
136
|
+
});
|
|
137
|
+
const globalValueOptions = new Set(['-c', '--configuration']);
|
|
138
|
+
/** Classify before Commander parsing. Unknown and internal paths fail closed. */
|
|
139
|
+
export function classifyFleetArgv(argv) {
|
|
140
|
+
let command = '';
|
|
141
|
+
let commandIndex = -1;
|
|
142
|
+
for (let index = 0; index < argv.length; index++) {
|
|
143
|
+
const arg = argv[index];
|
|
144
|
+
if (arg === '--') {
|
|
145
|
+
command = argv[index + 1] ?? '';
|
|
146
|
+
commandIndex = index + 1;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (globalValueOptions.has(arg)) {
|
|
150
|
+
index++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (arg.startsWith('--configuration=') || arg.startsWith('-c='))
|
|
154
|
+
continue;
|
|
155
|
+
if (arg.startsWith('-c') && arg.length > 2)
|
|
156
|
+
continue;
|
|
157
|
+
if (arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V')
|
|
158
|
+
return { command: arg, route: 'supervisor-proxy/read-only', decision: 'allow' };
|
|
159
|
+
if (!arg.startsWith('-')) {
|
|
160
|
+
command = arg;
|
|
161
|
+
commandIndex = index;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!command)
|
|
166
|
+
return { command: '<none>', route: 'supervisor-proxy/unsupported', decision: 'unsupported' };
|
|
167
|
+
if (command.startsWith('_'))
|
|
168
|
+
return { command, route: 'supervisor-proxy/internal', decision: 'deny' };
|
|
169
|
+
if (SAFE_READ.has(command) || command === 'man')
|
|
170
|
+
return { command, route: 'supervisor-proxy/read-only', decision: 'allow' };
|
|
171
|
+
if (AGENT_SURFACES[command]) {
|
|
172
|
+
const sub = argv[commandIndex + 1] ?? '<none>';
|
|
173
|
+
if (sub === '--help' || sub === '-h')
|
|
174
|
+
return { command: `${command} ${sub}`, route: 'supervisor-proxy/read-only', decision: 'allow' };
|
|
175
|
+
if (sub.startsWith('_'))
|
|
176
|
+
return { command: `${command} ${sub}`, route: 'supervisor-proxy/internal', decision: 'deny' };
|
|
177
|
+
if (command !== 'spawn' && !AGENT_SURFACES[command].has(sub))
|
|
178
|
+
return { command: `${command} ${sub}`, route: `supervisor-proxy/${command}`, decision: 'allow' };
|
|
179
|
+
return { command: sub.startsWith('-') ? command : `${command} ${sub}`,
|
|
180
|
+
route: `supervisor-proxy/${command}`, decision: 'allow' };
|
|
181
|
+
}
|
|
182
|
+
// The proxy is attribution and routing, not a product permission boundary.
|
|
183
|
+
// Commander validates every public command. Hidden process entry points above
|
|
184
|
+
// remain guarded because recursively invoking them breaks the supervisor protocol.
|
|
185
|
+
return { command, route: 'supervisor-proxy/public', decision: 'allow' };
|
|
186
|
+
}
|
|
187
|
+
const marker = (value) => value === '' ? '[REDACTED:empty]' : '[REDACTED:value]';
|
|
188
|
+
const sensitiveValueFlags = new Set([
|
|
189
|
+
'--identity', '--invite', '--token', '--api-token', '--password', '--password-file',
|
|
190
|
+
'--env', '--brief', '--brief-file', '--bio-file', '--persona-file', '--isolation-file', '--loops-file',
|
|
191
|
+
'--configuration', '-c', '--public-invite', '--public-invite-file', '--invite-file',
|
|
192
|
+
'--summary-file', '--text', '--message', '--summary', '--reason', '--goal', '--cwd',
|
|
193
|
+
'--identity-cid', '--owner-cid', '--contact-cid', '--codex-config', '--add-dir',
|
|
194
|
+
]);
|
|
195
|
+
function redactUrl(value) {
|
|
196
|
+
try {
|
|
197
|
+
const url = new URL(value);
|
|
198
|
+
let changed = false;
|
|
199
|
+
if (url.username || url.password) {
|
|
200
|
+
url.username = 'REDACTED';
|
|
201
|
+
url.password = 'REDACTED';
|
|
202
|
+
changed = true;
|
|
203
|
+
}
|
|
204
|
+
for (const key of [...url.searchParams.keys()])
|
|
205
|
+
if (isSensitiveConfigKey(key)) {
|
|
206
|
+
url.searchParams.set(key, marker(url.searchParams.get(key) ?? ''));
|
|
207
|
+
changed = true;
|
|
208
|
+
}
|
|
209
|
+
return changed ? url.toString() : value;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function redactInline(value) {
|
|
216
|
+
if (!value.startsWith('inline:'))
|
|
217
|
+
return redactUrl(value);
|
|
218
|
+
const source = value.slice('inline:'.length);
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(source);
|
|
221
|
+
const walk = (node, key) => {
|
|
222
|
+
if (key && isSensitiveConfigKey(key))
|
|
223
|
+
return typeof node === 'string' ? marker(node) : '[REDACTED:value]';
|
|
224
|
+
if (Array.isArray(node))
|
|
225
|
+
return node.map(child => walk(child));
|
|
226
|
+
if (node && typeof node === 'object')
|
|
227
|
+
return Object.fromEntries(Object.entries(node).map(([childKey, child]) => [childKey, walk(child, childKey)]));
|
|
228
|
+
return typeof node === 'string' ? redactUrl(node) : node;
|
|
229
|
+
};
|
|
230
|
+
return `inline:${JSON.stringify(walk(parsed))}`;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return 'inline:[REDACTED:invalid-definition]';
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Faithful ordered argv with deterministic structural redaction. */
|
|
237
|
+
export function redactFleetArgv(argv) {
|
|
238
|
+
const result = [...argv];
|
|
239
|
+
for (let index = 0; index < result.length; index++) {
|
|
240
|
+
const arg = result[index];
|
|
241
|
+
if (arg.startsWith('-c=') || (arg.startsWith('-c') && arg.length > 2 && !arg.startsWith('--'))) {
|
|
242
|
+
const prefix = arg.startsWith('-c=') ? '-c=' : '-c';
|
|
243
|
+
result[index] = `${prefix}${marker(arg.slice(prefix.length))}`;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const equal = /^(--[^=]+)=(.*)$/su.exec(arg);
|
|
247
|
+
if (equal && sensitiveValueFlags.has(equal[1])) {
|
|
248
|
+
result[index] = `${equal[1]}=${marker(equal[2])}`;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (sensitiveValueFlags.has(arg) && index + 1 < result.length) {
|
|
252
|
+
result[index + 1] = marker(result[index + 1]);
|
|
253
|
+
index++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
result[index] = redactInline(arg);
|
|
257
|
+
}
|
|
258
|
+
// `send <role> <text...>` has positional content. Locate it using the same
|
|
259
|
+
// global-option scan as classification, never absolute argv offsets.
|
|
260
|
+
let commandIndex = -1;
|
|
261
|
+
for (let index = 0; index < result.length; index++) {
|
|
262
|
+
const arg = result[index];
|
|
263
|
+
if (globalValueOptions.has(arg)) {
|
|
264
|
+
index++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (arg.startsWith('--configuration=') || arg.startsWith('-c=') || (arg.startsWith('-c') && arg.length > 2))
|
|
268
|
+
continue;
|
|
269
|
+
if (!arg.startsWith('-')) {
|
|
270
|
+
commandIndex = index;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (result[commandIndex] === 'send')
|
|
275
|
+
for (let index = commandIndex + 2; index < result.length; index++)
|
|
276
|
+
result[index] = marker(result[index]);
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
/** Durable write-once attempt ledger; contains redacted argv only. */
|
|
280
|
+
export class FleetCommandAuditStore {
|
|
281
|
+
path;
|
|
282
|
+
deps;
|
|
283
|
+
attempts = [];
|
|
284
|
+
constructor(path, deps = { now: () => new Date(), uuid: () => randomUUID() }) {
|
|
285
|
+
this.path = path;
|
|
286
|
+
this.deps = deps;
|
|
287
|
+
if (!existsSync(path))
|
|
288
|
+
return;
|
|
289
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
290
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.attempts))
|
|
291
|
+
throw new Error('invalid fleet command audit ledger');
|
|
292
|
+
this.attempts = parsed.attempts;
|
|
293
|
+
let recovered = false;
|
|
294
|
+
for (const attempt of this.attempts) {
|
|
295
|
+
if (attempt.invocation === 'sending') {
|
|
296
|
+
attempt.invocation = 'uncertain';
|
|
297
|
+
recovered = true;
|
|
298
|
+
}
|
|
299
|
+
if (attempt.outcome?.delivery === 'sending') {
|
|
300
|
+
attempt.outcome.delivery = 'uncertain';
|
|
301
|
+
recovered = true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (recovered)
|
|
305
|
+
this.persist();
|
|
306
|
+
}
|
|
307
|
+
list() { return this.attempts.map(item => structuredClone(item)); }
|
|
308
|
+
begin(requestId, caller, argv) {
|
|
309
|
+
const existing = this.attempts.find(item => item.caller === caller && item.requestId === requestId);
|
|
310
|
+
if (existing) {
|
|
311
|
+
if (JSON.stringify(existing.argv) !== JSON.stringify(redactFleetArgv(argv)))
|
|
312
|
+
throw new Error('fleet command request ID was reused with different argv');
|
|
313
|
+
return structuredClone(existing);
|
|
314
|
+
}
|
|
315
|
+
const attempt = { version: 1, correlationId: this.deps.uuid(), requestId,
|
|
316
|
+
caller, invokedAt: this.deps.now().toISOString(), classification: classifyFleetArgv(argv),
|
|
317
|
+
argv: redactFleetArgv(argv), invocation: 'sending' };
|
|
318
|
+
this.attempts.push(attempt);
|
|
319
|
+
this.persist();
|
|
320
|
+
return structuredClone(attempt);
|
|
321
|
+
}
|
|
322
|
+
invocation(correlationId, caller, delivery) {
|
|
323
|
+
const attempt = this.owned(correlationId, caller);
|
|
324
|
+
if (attempt.invocation !== 'sending' && attempt.invocation !== delivery)
|
|
325
|
+
throw new Error('conflicting fleet command invocation delivery state');
|
|
326
|
+
attempt.invocation = delivery;
|
|
327
|
+
this.persist();
|
|
328
|
+
return structuredClone(attempt);
|
|
329
|
+
}
|
|
330
|
+
finish(correlationId, caller, outcome) {
|
|
331
|
+
const attempt = this.owned(correlationId, caller);
|
|
332
|
+
if (!attempt.outcome)
|
|
333
|
+
attempt.outcome = { ...outcome, completedAt: this.deps.now().toISOString(), delivery: 'sending' };
|
|
334
|
+
else {
|
|
335
|
+
const prior = { class: attempt.outcome.class, effect: attempt.outcome.effect,
|
|
336
|
+
...(attempt.outcome.exitCode === undefined ? {} : { exitCode: attempt.outcome.exitCode }),
|
|
337
|
+
...(attempt.outcome.resourceIds ? { resourceIds: attempt.outcome.resourceIds } : {}),
|
|
338
|
+
...(attempt.outcome.presentations ? { presentations: attempt.outcome.presentations } : {}) };
|
|
339
|
+
if (JSON.stringify(prior) !== JSON.stringify(outcome))
|
|
340
|
+
throw new Error('conflicting fleet command outcome for existing correlation');
|
|
341
|
+
}
|
|
342
|
+
this.persist();
|
|
343
|
+
return structuredClone(attempt);
|
|
344
|
+
}
|
|
345
|
+
outcome(correlationId, caller, delivery) {
|
|
346
|
+
const attempt = this.owned(correlationId, caller);
|
|
347
|
+
if (!attempt.outcome)
|
|
348
|
+
throw new Error('fleet command audit outcome was not recorded');
|
|
349
|
+
if (attempt.outcome.delivery !== 'sending' && attempt.outcome.delivery !== delivery)
|
|
350
|
+
throw new Error('conflicting fleet command outcome delivery state');
|
|
351
|
+
attempt.outcome.delivery = delivery;
|
|
352
|
+
this.persist();
|
|
353
|
+
return structuredClone(attempt);
|
|
354
|
+
}
|
|
355
|
+
owned(correlationId, caller) {
|
|
356
|
+
const attempt = this.attempts.find(item => item.correlationId === correlationId);
|
|
357
|
+
if (!attempt || attempt.caller !== caller)
|
|
358
|
+
throw new Error('unknown or cross-role fleet command correlation');
|
|
359
|
+
return attempt;
|
|
360
|
+
}
|
|
361
|
+
persist() {
|
|
362
|
+
replaceFileAtomically(this.path, `${JSON.stringify({ version: 1, attempts: this.attempts })}\n`, 0o600);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
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;
|
|
366
|
+
const SAFE_RESOURCE_ID = /^[\p{L}\p{N}][\p{L}\p{N}._:@-]{0,159}$/u;
|
|
367
|
+
const OUTCOMES = new Set(['success', 'validation', 'denied', 'runtime', 'timeout', 'proxy', 'delivery']);
|
|
368
|
+
const EFFECTS = new Set(['not_started', 'completed', 'unknown']);
|
|
369
|
+
export function validateFleetAuditBegin(value) {
|
|
370
|
+
const input = value;
|
|
371
|
+
if (!input || Object.keys(input).some(key => !['requestId', 'argv'].includes(key))
|
|
372
|
+
|| typeof input.requestId !== 'string' || !UUID.test(input.requestId)
|
|
373
|
+
|| !Array.isArray(input.argv) || input.argv.length > 256
|
|
374
|
+
|| !input.argv.every(arg => typeof arg === 'string' && Buffer.byteLength(arg) <= 16_384)
|
|
375
|
+
|| Buffer.byteLength(JSON.stringify(input.argv)) > 48 * 1024)
|
|
376
|
+
throw new Error('invalid fleet audit begin fields');
|
|
377
|
+
}
|
|
378
|
+
export function validateFleetAuditFinish(value) {
|
|
379
|
+
const input = value;
|
|
380
|
+
const allowed = ['correlationId', 'class', 'exitCode', 'effect', 'resourceIds', 'presentations'];
|
|
381
|
+
const resources = input?.resourceIds;
|
|
382
|
+
if (!input || Object.keys(input).some(key => !allowed.includes(key))
|
|
383
|
+
|| typeof input.correlationId !== 'string' || !UUID.test(input.correlationId)
|
|
384
|
+
|| !OUTCOMES.has(input.class) || !EFFECTS.has(String(input.effect))
|
|
385
|
+
|| (input.exitCode !== undefined && (!Number.isSafeInteger(input.exitCode) || Number(input.exitCode) < 0 || Number(input.exitCode) > 255))
|
|
386
|
+
|| (resources !== undefined && (!resources || typeof resources !== 'object' || Array.isArray(resources)
|
|
387
|
+
|| Object.entries(resources).some(([key, id]) => !['agent', 'task', 'room'].includes(key)
|
|
388
|
+
|| typeof id !== 'string' || !SAFE_RESOURCE_ID.test(id))))
|
|
389
|
+
|| (input.presentations !== undefined && (!Array.isArray(input.presentations)
|
|
390
|
+
|| input.presentations.length > 128 || !input.presentations.every(validPresentation))))
|
|
391
|
+
throw new Error('invalid fleet audit finish fields');
|
|
392
|
+
}
|
|
393
|
+
const APPROVAL_MODES = new Set(['ask', 'auto', 'allow', 'deny']);
|
|
394
|
+
const FILESYSTEM_MODES = new Set(['read-only', 'workspace', 'unrestricted']);
|
|
395
|
+
const UNATTENDED_MODES = new Set(['deny', 'wait']);
|
|
396
|
+
const FLEET_PERMISSION_MODES = new Set(['ask', 'auto', 'allow']);
|
|
397
|
+
const PRESENTATION_TEXT = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u;
|
|
398
|
+
/**
|
|
399
|
+
* Exact-key validation of a current-version launch configuration. A partial
|
|
400
|
+
* v1 configuration is rejected; a legacy presentation simply omits the field.
|
|
401
|
+
*/
|
|
402
|
+
function validConfiguration(value) {
|
|
403
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
404
|
+
return false;
|
|
405
|
+
const c = value;
|
|
406
|
+
const text = (v, max = 256) => typeof v === 'string' && v.length <= max
|
|
407
|
+
&& !PRESENTATION_TEXT.test(v);
|
|
408
|
+
const exact = (record, keys) => Object.keys(record).every(key => keys.includes(key));
|
|
409
|
+
const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
410
|
+
const origin = (v) => {
|
|
411
|
+
if (!record(v))
|
|
412
|
+
return false;
|
|
413
|
+
if (v.kind === 'named')
|
|
414
|
+
return exact(v, ['kind', 'ref']) && text(v.ref, 160);
|
|
415
|
+
if (v.kind === 'inline')
|
|
416
|
+
return exact(v, ['kind', 'fingerprint'])
|
|
417
|
+
&& (v.fingerprint === undefined || (typeof v.fingerprint === 'string' && /^[a-f0-9]{12}$/.test(v.fingerprint)));
|
|
418
|
+
return v.kind === 'unknown' && exact(v, ['kind']);
|
|
419
|
+
};
|
|
420
|
+
const count = (v) => v === undefined
|
|
421
|
+
|| (Number.isSafeInteger(v) && Number(v) >= 0 && Number(v) <= 4_096);
|
|
422
|
+
return exact(c, ['version', 'template', 'role', 'brain', 'harness', 'session', 'model',
|
|
423
|
+
'effort', 'mission', 'approval', 'filesystem', 'unattended', 'permissionMode', 'monitor', 'isolation', 'loops'])
|
|
424
|
+
&& c.version === 1 && origin(c.role) && origin(c.brain)
|
|
425
|
+
&& text(c.harness, 64) && SESSION_BACKENDS.has(c.session)
|
|
426
|
+
&& (c.model === null || text(c.model, 160))
|
|
427
|
+
&& (c.template === undefined || text(c.template, 160))
|
|
428
|
+
&& (c.effort === undefined || text(c.effort, 32))
|
|
429
|
+
// The builder caps the mission label at 80 code points; enforce the same
|
|
430
|
+
// invariant here so the per-line budget proof holds for wire input too.
|
|
431
|
+
&& (c.mission === undefined || (text(c.mission) && Array.from(String(c.mission)).length <= 80))
|
|
432
|
+
&& APPROVAL_MODES.has(String(c.approval))
|
|
433
|
+
&& FILESYSTEM_MODES.has(String(c.filesystem))
|
|
434
|
+
&& UNATTENDED_MODES.has(String(c.unattended))
|
|
435
|
+
&& record(c.permissionMode)
|
|
436
|
+
&& exact(c.permissionMode, ['fleetMode', 'nativeMode'])
|
|
437
|
+
&& FLEET_PERMISSION_MODES.has(String(c.permissionMode.fleetMode))
|
|
438
|
+
&& text(c.permissionMode.nativeMode, 160)
|
|
439
|
+
&& record(c.monitor) && exact(c.monitor, ['mode', 'interrupt'])
|
|
440
|
+
&& ['fleet', 'native'].includes(String(c.monitor.mode))
|
|
441
|
+
&& (typeof c.monitor.interrupt === 'boolean' || c.monitor.interrupt === 'after_tool')
|
|
442
|
+
&& (c.isolation === undefined || (record(c.isolation)
|
|
443
|
+
&& exact(c.isolation, ['requested', 'on_unavailable', 'network', 'read_mounts', 'write_mounts'])
|
|
444
|
+
&& ['auto', 'bubblewrap', 'podman', 'none'].includes(String(c.isolation.requested))
|
|
445
|
+
&& (c.isolation.on_unavailable === undefined || ['warn', 'strict'].includes(String(c.isolation.on_unavailable)))
|
|
446
|
+
&& (c.isolation.network === undefined || ['broker', 'deny', 'allow', 'allowlist'].includes(String(c.isolation.network)))
|
|
447
|
+
&& count(c.isolation.read_mounts) && count(c.isolation.write_mounts)))
|
|
448
|
+
&& (c.loops === undefined || (record(c.loops)
|
|
449
|
+
&& exact(c.loops, ['source', 'policy', 'entries'])
|
|
450
|
+
&& ['agent-template', 'cli', 'omitted'].includes(String(c.loops.source))
|
|
451
|
+
&& c.loops.policy === 'skip-if-busy'
|
|
452
|
+
&& Array.isArray(c.loops.entries) && c.loops.entries.length <= 64
|
|
453
|
+
&& c.loops.entries.every(entry => record(entry)
|
|
454
|
+
&& exact(entry, ['name', 'enabled', 'intervalMs', 'initialDelayMs', 'jitterMs', 'prompt'])
|
|
455
|
+
&& text(entry.name, 64) && typeof entry.enabled === 'boolean'
|
|
456
|
+
&& [entry.intervalMs, entry.initialDelayMs, entry.jitterMs].every(Number.isSafeInteger)
|
|
457
|
+
&& record(entry.prompt) && exact(entry.prompt, ['bytes', 'sha256'])
|
|
458
|
+
&& Number.isSafeInteger(entry.prompt.bytes) && Number(entry.prompt.bytes) >= 0
|
|
459
|
+
&& typeof entry.prompt.sha256 === 'string' && /^[a-f0-9]{64}$/.test(entry.prompt.sha256))))
|
|
460
|
+
// Escaping and code-fence growth can push a per-field-valid configuration
|
|
461
|
+
// past the line budget; a v1 configuration whose complete mandatory
|
|
462
|
+
// rendering cannot fit is invalid, never silently trimmed.
|
|
463
|
+
&& mandatoryConfigurationFits(c);
|
|
464
|
+
}
|
|
465
|
+
function validPresentation(value) {
|
|
466
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
467
|
+
return false;
|
|
468
|
+
const p = value;
|
|
469
|
+
const configured = (record) => record.configuration === undefined || validConfiguration(record.configuration);
|
|
470
|
+
const safe = (v) => typeof v === 'string' && SAFE_RESOURCE_ID.test(v);
|
|
471
|
+
const text = (v) => typeof v === 'string' && v.length <= 256
|
|
472
|
+
&& !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(v);
|
|
473
|
+
const presentationLabel = (v) => typeof v === 'string'
|
|
474
|
+
&& fleetPresentationLabel(v) === v;
|
|
475
|
+
const exact = (record, keys) => Object.keys(record).every(key => keys.includes(key));
|
|
476
|
+
const transition = (allowed) => typeof p.operation === 'string' && typeof p.newState === 'string'
|
|
477
|
+
&& allowed[p.operation]?.includes(`${String(p.previousState ?? 'none')}→${p.newState}`);
|
|
478
|
+
if (p.kind === 'agent_started')
|
|
479
|
+
return exact(p, ['kind', 'eventId', 'id', 'name', 'lifetime', 'brain', 'role',
|
|
480
|
+
'harness', 'session', 'model', 'permissions', 'parent', 'actionId', 'inherited', 'configuration'])
|
|
481
|
+
&& safe(p.eventId) && safe(p.id) && safe(p.name) && ['permanent', 'temporary'].includes(String(p.lifetime))
|
|
482
|
+
&& text(p.brain) && text(p.role) && safe(p.harness)
|
|
483
|
+
&& SESSION_BACKENDS.has(p.session) && safe(p.parent) && safe(p.actionId)
|
|
484
|
+
&& (p.model === undefined || text(p.model)) && (p.permissions === undefined || text(p.permissions))
|
|
485
|
+
&& Array.isArray(p.inherited) && p.inherited.every(text) && configured(p);
|
|
486
|
+
if (p.kind === 'task')
|
|
487
|
+
return exact(p, ['kind', 'operation', 'eventId', 'id', 'title', 'previousState',
|
|
488
|
+
'newState', 'template', 'roomId', 'roomName', 'reason', 'revision', 'list', 'agents'])
|
|
489
|
+
&& ['create', 'start', 'work', 'block', 'unblock', 'review', 'done',
|
|
490
|
+
'cancel', 'finish', 'delete', 'settling'].includes(String(p.operation)) && safe(p.eventId) && safe(p.id) && safe(p.newState)
|
|
491
|
+
&& transition({ create: ['none→backlog', 'none→provisioning', 'none→active'],
|
|
492
|
+
start: ['backlog→provisioning'], work: ['provisioning→active'],
|
|
493
|
+
block: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
|
|
494
|
+
unblock: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
|
|
495
|
+
review: ['active→review'], done: ['review→done'], finish: ['active→done', 'review→done'],
|
|
496
|
+
cancel: ['backlog→cancelled', 'provisioning→cancelled', 'active→cancelled', 'review→cancelled'],
|
|
497
|
+
delete: [
|
|
498
|
+
'backlog→deleting', 'provisioning→deleting', 'active→deleting', 'review→deleting',
|
|
499
|
+
'done→deleting', 'cancelled→deleting', 'failed→deleting',
|
|
500
|
+
'backlog→deleted', 'provisioning→deleted', 'active→deleted', 'review→deleted',
|
|
501
|
+
'done→deleted', 'cancelled→deleted', 'failed→deleted',
|
|
502
|
+
], settling: ['active→active', 'review→review', 'backlog→backlog',
|
|
503
|
+
'provisioning→provisioning'] })
|
|
504
|
+
&& (p.title === undefined || presentationLabel(p.title))
|
|
505
|
+
&& (p.roomName === undefined || presentationLabel(p.roomName))
|
|
506
|
+
&& (p.reason === undefined || presentationLabel(p.reason))
|
|
507
|
+
&& (p.previousState === undefined || safe(p.previousState))
|
|
508
|
+
&& (p.template === undefined || text(p.template)) && (p.roomId === undefined || safe(p.roomId))
|
|
509
|
+
&& (p.list === undefined || text(p.list)) && (p.revision === undefined || text(p.revision))
|
|
510
|
+
&& Array.isArray(p.agents) && p.agents.length <= 64
|
|
511
|
+
&& p.agents.every(a => a && typeof a === 'object'
|
|
512
|
+
&& exact(a, ['name', 'brain', 'role', 'permissions', 'configuration'])
|
|
513
|
+
&& safe(a.name)
|
|
514
|
+
&& text(a.role)
|
|
515
|
+
&& (a.brain === undefined || text(a.brain))
|
|
516
|
+
&& (a.permissions === undefined || text(a.permissions))
|
|
517
|
+
&& configured(a));
|
|
518
|
+
if (p.kind === 'room')
|
|
519
|
+
return exact(p, ['kind', 'operation', 'eventId', 'id', 'previousState', 'newState',
|
|
520
|
+
'revision', 'name', 'template', 'taskId', 'anonymous', 'memberCount', 'ownerAttached', 'participants'])
|
|
521
|
+
&& ['create', 'activate', 'close', 'delete'].includes(String(p.operation))
|
|
522
|
+
&& safe(p.eventId) && safe(p.id) && safe(p.newState)
|
|
523
|
+
&& transition({ create: ['none→provisioning'], activate: ['provisioning→active'],
|
|
524
|
+
close: ['active→closing', 'provisioning→closing'],
|
|
525
|
+
delete: ['closing→deleted'] })
|
|
526
|
+
&& (p.previousState === undefined || safe(p.previousState))
|
|
527
|
+
&& (p.name === undefined || presentationLabel(p.name)) && (p.template === undefined || text(p.template))
|
|
528
|
+
&& (p.taskId === undefined || safe(p.taskId)) && (p.revision === undefined || text(p.revision))
|
|
529
|
+
&& (p.anonymous === undefined || typeof p.anonymous === 'boolean')
|
|
530
|
+
&& (p.ownerAttached === undefined || typeof p.ownerAttached === 'boolean')
|
|
531
|
+
&& (p.memberCount === undefined || (Number.isSafeInteger(p.memberCount)
|
|
532
|
+
&& Number(p.memberCount) >= 0 && Number(p.memberCount) <= 64))
|
|
533
|
+
&& Array.isArray(p.participants) && p.participants.length <= 64
|
|
534
|
+
&& p.participants.every(a => a && typeof a === 'object'
|
|
535
|
+
&& exact(a, ['name', 'id', 'brain', 'role', 'permissions', 'configuration'])
|
|
536
|
+
&& safe(a.name)
|
|
537
|
+
&& text(a.role)
|
|
538
|
+
&& (a.id === undefined || safe(a.id))
|
|
539
|
+
&& (a.brain === undefined || text(a.brain))
|
|
540
|
+
&& (a.permissions === undefined || text(a.permissions))
|
|
541
|
+
&& configured(a));
|
|
542
|
+
if (p.kind === 'lifecycle_failure')
|
|
543
|
+
return exact(p, ['kind', 'eventId', 'resource', 'id', 'label', 'state', 'category'])
|
|
544
|
+
&& ['Agent', 'Task', 'Room'].includes(String(p.resource))
|
|
545
|
+
&& safe(p.eventId) && safe(p.id) && safe(p.state)
|
|
546
|
+
&& (p.label === undefined || presentationLabel(p.label))
|
|
547
|
+
&& ['provision_failed', 'provision_pending', 'readiness_failed', 'settlement_failed',
|
|
548
|
+
'settlement_pending', 'cleanup_failed', 'cleanup_pending'].includes(String(p.category));
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
export function validateFleetAuditPresentations(value) {
|
|
552
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 128
|
|
553
|
+
|| !value.every(validPresentation))
|
|
554
|
+
throw new Error('invalid Fleet lifecycle presentations');
|
|
555
|
+
}
|
|
556
|
+
const messageCodePoints = (value) => Array.from(value).length;
|
|
557
|
+
const messageBytes = (value) => Buffer.byteLength(value, 'utf8');
|
|
558
|
+
const withinMessageBounds = (value) => messageCodePoints(value) <= MARKDOWN_MAX_CODE_POINTS && messageBytes(value) <= MARKDOWN_MAX_BYTES;
|
|
559
|
+
function titleCase(value) {
|
|
560
|
+
return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value;
|
|
561
|
+
}
|
|
562
|
+
function commaInteger(value) {
|
|
563
|
+
return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
564
|
+
}
|
|
565
|
+
function renderStructuredMessage(headline, fields, action) {
|
|
566
|
+
return `${headline}`
|
|
567
|
+
+ fields.map(([label, value]) => `\n- **${label}:** ${value}`).join('')
|
|
568
|
+
+ (action ? `\n\n${action}` : '');
|
|
569
|
+
}
|
|
570
|
+
function structuredMessage(headline, fields, action) {
|
|
571
|
+
const visible = fields.filter((field) => field[1] !== undefined);
|
|
572
|
+
const render = (selected) => renderStructuredMessage(headline, selected, action);
|
|
573
|
+
const complete = render(visible);
|
|
574
|
+
if (withinMessageBounds(complete))
|
|
575
|
+
return complete;
|
|
576
|
+
const marker = ['Details omitted', 'Additional lifecycle details'];
|
|
577
|
+
const selected = [];
|
|
578
|
+
for (const field of visible) {
|
|
579
|
+
if (!withinMessageBounds(render([...selected, field, marker])))
|
|
580
|
+
break;
|
|
581
|
+
selected.push(field);
|
|
582
|
+
}
|
|
583
|
+
const bounded = render([...selected, marker]);
|
|
584
|
+
if (!withinMessageBounds(bounded))
|
|
585
|
+
throw new Error('mandatory lifecycle presentation exceeds message bounds');
|
|
586
|
+
return bounded;
|
|
587
|
+
}
|
|
588
|
+
function originFields(label, origin) {
|
|
589
|
+
if (origin.kind === 'named')
|
|
590
|
+
return [[label, `Preset ${markdownCode(origin.ref)}`]];
|
|
591
|
+
if (origin.kind === 'inline')
|
|
592
|
+
return [
|
|
593
|
+
[label, 'Inline definition'],
|
|
594
|
+
...(origin.fingerprint ? [[`${label} fingerprint`, markdownCode(origin.fingerprint)]] : []),
|
|
595
|
+
];
|
|
596
|
+
return [[label, 'Unknown']];
|
|
597
|
+
}
|
|
598
|
+
function interruptSummary(value) {
|
|
599
|
+
return value === false ? 'off' : value === true ? 'on' : markdownProse(value);
|
|
600
|
+
}
|
|
601
|
+
function agentConfigurationFieldGroups(configuration) {
|
|
602
|
+
const optional = [
|
|
603
|
+
[['Monitor mode', markdownProse(configuration.monitor.mode)],
|
|
604
|
+
['Interrupt', interruptSummary(configuration.monitor.interrupt)]],
|
|
605
|
+
];
|
|
606
|
+
if (configuration.isolation)
|
|
607
|
+
optional.push([
|
|
608
|
+
['Isolation', markdownProse(configuration.isolation.requested)],
|
|
609
|
+
['Network', configuration.isolation.network ? markdownProse(configuration.isolation.network) : undefined],
|
|
610
|
+
['If unavailable', configuration.isolation.on_unavailable
|
|
611
|
+
? configuration.isolation.on_unavailable === 'strict' ? 'Fail' : 'Warn' : undefined],
|
|
612
|
+
['Read-only mounts', String(configuration.isolation.read_mounts ?? 0)],
|
|
613
|
+
['Read-write mounts', String(configuration.isolation.write_mounts ?? 0)],
|
|
614
|
+
]);
|
|
615
|
+
for (const entry of configuration.loops?.entries ?? [])
|
|
616
|
+
optional.push([
|
|
617
|
+
['Loop', markdownCode(entry.name)],
|
|
618
|
+
['Loop status', entry.enabled ? 'Enabled' : 'Disabled'],
|
|
619
|
+
['Loop interval', `${commaInteger(entry.intervalMs)} ms`],
|
|
620
|
+
['Loop delay', `${commaInteger(entry.initialDelayMs)} ms`],
|
|
621
|
+
['Loop jitter', `${commaInteger(entry.jitterMs)} ms`],
|
|
622
|
+
['Loop prompt size', `${entry.prompt.bytes} B`],
|
|
623
|
+
['Loop prompt hash', markdownCode(entry.prompt.sha256.slice(0, 12))],
|
|
624
|
+
]);
|
|
625
|
+
return { mandatory: [
|
|
626
|
+
['Template', configuration.template ? markdownCode(configuration.template) : undefined],
|
|
627
|
+
...originFields('Role', configuration.role),
|
|
628
|
+
['Mission', configuration.mission ? `“${markdownProse(configuration.mission)}”` : undefined],
|
|
629
|
+
...originFields('Brain', configuration.brain),
|
|
630
|
+
['Harness', markdownCode(configuration.harness)],
|
|
631
|
+
['Model', configuration.model === null ? 'Harness default' : markdownCode(configuration.model)],
|
|
632
|
+
['Effort', configuration.effort ? markdownProse(configuration.effort) : undefined],
|
|
633
|
+
['Approval', markdownProse(configuration.approval)],
|
|
634
|
+
['Filesystem', markdownProse(configuration.filesystem)],
|
|
635
|
+
['Wait', markdownProse(configuration.unattended)],
|
|
636
|
+
['Fleet mode', markdownProse(configuration.permissionMode.fleetMode)],
|
|
637
|
+
['Native mode', markdownProse(configuration.permissionMode.nativeMode)],
|
|
638
|
+
], optional, tail: [
|
|
639
|
+
['Loop policy', configuration.loops ? 'Skip if busy' : undefined],
|
|
640
|
+
['Loop source', configuration.loops
|
|
641
|
+
? markdownProse(configuration.loops.source.replace(/-/g, ' ')) : undefined],
|
|
642
|
+
] };
|
|
643
|
+
}
|
|
644
|
+
function boundedAgentMessage(headline, prefix, optionalGroups, suffix, action) {
|
|
645
|
+
const visible = (fields) => fields.filter((field) => field[1] !== undefined);
|
|
646
|
+
const requiredPrefix = visible(prefix);
|
|
647
|
+
const requiredSuffix = visible(suffix);
|
|
648
|
+
const groups = optionalGroups.map(visible).filter(group => group.length > 0);
|
|
649
|
+
const marker = ['Details omitted', 'Additional optional Agent configuration'];
|
|
650
|
+
const render = (optional, omitted) => renderStructuredMessage(headline, [...requiredPrefix, ...optional, ...(omitted ? [marker] : []), ...requiredSuffix], action);
|
|
651
|
+
const all = groups.flat();
|
|
652
|
+
const complete = render(all, false);
|
|
653
|
+
if (withinMessageBounds(complete))
|
|
654
|
+
return complete;
|
|
655
|
+
const included = [];
|
|
656
|
+
for (const group of groups) {
|
|
657
|
+
const candidate = render([...included, ...group], true);
|
|
658
|
+
if (!withinMessageBounds(candidate))
|
|
659
|
+
break;
|
|
660
|
+
included.push(...group);
|
|
661
|
+
}
|
|
662
|
+
const bounded = render(included, true);
|
|
663
|
+
if (!withinMessageBounds(bounded))
|
|
664
|
+
throw new Error('mandatory Agent presentation exceeds message bounds');
|
|
665
|
+
return bounded;
|
|
666
|
+
}
|
|
667
|
+
/** Compact Owner presentation. It intentionally has no command, argv, environment, or correlation data. */
|
|
668
|
+
export function renderFleetLifecycleEvent(value) {
|
|
669
|
+
if (value.kind === 'lifecycle_failure') {
|
|
670
|
+
const label = markdownProse(value.label ?? value.id);
|
|
671
|
+
if (value.category === 'provision_failed')
|
|
672
|
+
return structuredMessage(`⚠️ Couldn’t prepare ${value.resource} “${label}”`, [['Status', 'Provisioning failed'], ['Resource', value.resource], [`${value.resource} ID`, markdownCode(value.id)]], `**Next:** Check the Fleet service logs, correct the configuration, then run the same ${value.resource === 'Room' ? 'create' : 'start'} command again.`);
|
|
673
|
+
if (value.category === 'provision_pending')
|
|
674
|
+
return structuredMessage(`⏳ ${value.resource} “${label}” is still getting ready`, [['Status', 'Waiting for Agents'], ['Resource', value.resource], [`${value.resource} ID`, markdownCode(value.id)]], value.resource === 'Task'
|
|
675
|
+
? `**Next:** Check member readiness, then run ${markdownCode(`task start ${value.id}`)}.`
|
|
676
|
+
: '**Next:** Check member readiness, then repeat the same Room command.');
|
|
677
|
+
if (value.category === 'readiness_failed')
|
|
678
|
+
return structuredMessage(`⚠️ Agent didn’t become ready: ${label}`, [['Status', 'Readiness failed'], ['Agent ID', markdownCode(value.id)]], '**Next:** Check the Agent log, correct its configuration, then create it again.');
|
|
679
|
+
if (value.resource === 'Task')
|
|
680
|
+
return structuredMessage(`⚠️ Task cleanup is incomplete: ${label}`, [['Status', 'Finish pending'], ['Task ID', markdownCode(value.id)]], '**Next:** Check the Fleet service logs, then repeat the same `done`, `finish`, or `cancel` command.');
|
|
681
|
+
return structuredMessage(`⚠️ Room cleanup is incomplete: ${label}`, [['Status', 'Deletion pending'], ['Room ID', markdownCode(value.id)]], '**Next:** Check the Fleet service logs, then repeat the same confirmed Room delete command.');
|
|
682
|
+
}
|
|
683
|
+
if (value.kind === 'agent_started') {
|
|
684
|
+
const prefix = [
|
|
685
|
+
['Started by', markdownProse(value.parent)],
|
|
686
|
+
['Lifetime', titleCase(value.lifetime)],
|
|
687
|
+
];
|
|
688
|
+
if (value.configuration) {
|
|
689
|
+
const configuration = agentConfigurationFieldGroups(value.configuration);
|
|
690
|
+
return boundedAgentMessage(`🚀 Agent launched: ${markdownProse(value.name)}`, [...prefix, ...configuration.mandatory], configuration.optional, [...configuration.tail, ['Agent ID', markdownCode(value.id)]], 'Launch was accepted; harness and room-seat readiness converge separately.');
|
|
691
|
+
}
|
|
692
|
+
return structuredMessage(`🚀 Agent launched: ${markdownProse(value.name)}`, [...prefix,
|
|
693
|
+
['Resolved details', 'Unavailable'],
|
|
694
|
+
['Role', markdownProse(value.role)],
|
|
695
|
+
['Brain', markdownProse(value.brain)],
|
|
696
|
+
...(value.permissions ? [['Permissions', markdownProse(value.permissions)]] : []),
|
|
697
|
+
['Agent ID', markdownCode(value.id)]], 'Launch was accepted; harness and room-seat readiness converge separately.');
|
|
698
|
+
}
|
|
699
|
+
if (value.kind === 'task') {
|
|
700
|
+
const title = markdownProse(value.title ?? value.id);
|
|
701
|
+
const id = markdownCode(value.id);
|
|
702
|
+
const list = value.list ? markdownProse(value.list) : undefined;
|
|
703
|
+
if (value.operation === 'create' && value.newState === 'backlog')
|
|
704
|
+
return structuredMessage('✅ Task added to Backlog', [['Task', `“${title}”`], ['Task ID', id], ['List', list],
|
|
705
|
+
['Template', value.template ? markdownCode(value.template) : undefined]]);
|
|
706
|
+
if (value.operation === 'create' || value.operation === 'start' || value.operation === 'work')
|
|
707
|
+
return structuredMessage(`🚀 Task started: ${title}`, [['Status', 'Active'], ['List', list], ['Room', value.roomId
|
|
708
|
+
? markdownProse(value.roomName ?? value.roomId) : undefined],
|
|
709
|
+
['Room ID', value.roomId ? markdownCode(value.roomId) : undefined],
|
|
710
|
+
['Team', `${value.agents.length} Agent${value.agents.length === 1 ? '' : 's'} ready`], ['Task ID', id]], 'The task room and its Agents are ready to work.');
|
|
711
|
+
if (value.operation === 'block')
|
|
712
|
+
return structuredMessage(`⛔ Task blocked: ${title}`, [['Status', titleCase(value.newState)], ['Blocked', 'Yes'],
|
|
713
|
+
['Reason', value.reason ? markdownProse(value.reason) : undefined], ['Task ID', id]]);
|
|
714
|
+
if (value.operation === 'unblock')
|
|
715
|
+
return structuredMessage(`✅ Task unblocked: ${title}`, [['Status', titleCase(value.newState)], ['Task ID', id]]);
|
|
716
|
+
if (value.operation === 'review')
|
|
717
|
+
return structuredMessage(`🔎 Task ready for review: ${title}`, [['List', list], ['Task ID', id]]);
|
|
718
|
+
if (value.operation === 'done' || value.operation === 'finish')
|
|
719
|
+
return structuredMessage(`🎉 Task completed: ${title}`, [['Status', 'Done'], ['List', list], ['Task ID', id]], 'The task room and temporary Agents have been cleaned up.');
|
|
720
|
+
if (value.operation === 'cancel')
|
|
721
|
+
return structuredMessage(`🚫 Task cancelled: ${title}`, [['Status', 'Cancelled'], ['List', list], ['Task ID', id]], 'The task room and temporary Agents have been cleaned up.');
|
|
722
|
+
if (value.operation === 'delete' && value.newState === 'deleting')
|
|
723
|
+
return structuredMessage(`🗑️ Task deletion started: ${title}`, [['Current status', 'Deleting'], ['Task ID', id]], 'Fleet is removing the linked room and temporary Agents in the background.');
|
|
724
|
+
if (value.operation === 'delete')
|
|
725
|
+
return structuredMessage(`🗑️ Task deleted: ${title}`, [['Task ID', id]], 'Temporary Agents were cleaned up.');
|
|
726
|
+
return structuredMessage(`⚠️ Task cleanup is incomplete: ${title}`, [['Status', 'Finish pending'], ['Task ID', id]], '**Next:** Check the Fleet service logs, then repeat the same `done`, `finish`, or `cancel` command.');
|
|
727
|
+
}
|
|
728
|
+
const name = markdownProse(value.name ?? value.id);
|
|
729
|
+
if (value.operation === 'activate')
|
|
730
|
+
return structuredMessage(`🏠 Room ready: ${name}`, [['Status', 'Active'], ['Template', value.template ? markdownCode(value.template) : undefined],
|
|
731
|
+
['Team', `${value.memberCount ?? value.participants.length} Agent${(value.memberCount ?? value.participants.length) === 1 ? '' : 's'} ready`],
|
|
732
|
+
['Room ID', markdownCode(value.id)]]);
|
|
733
|
+
if (value.operation === 'delete')
|
|
734
|
+
return structuredMessage(`🗑️ Room deleted: ${name}`, [['Room ID', markdownCode(value.id)]], 'Temporary Agents were cleaned up.');
|
|
735
|
+
if (value.operation === 'close')
|
|
736
|
+
return structuredMessage(`🗑️ Room deletion started: ${name}`, [['Current status', 'Deleting'], ['Room ID', markdownCode(value.id)]]);
|
|
737
|
+
return structuredMessage(`⏳ Room “${name}” is still getting ready`, [['Status', 'Waiting for Agents'], ['Resource', 'Room'], ['Room ID', markdownCode(value.id)]], '**Next:** Check member readiness, then repeat the same Room command.');
|
|
738
|
+
}
|