@ours.network/fleet 0.15.1 → 0.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +49 -13
  2. package/dist/application/fleet-query-service.js +3 -0
  3. package/dist/application/model-catalog.d.ts +20 -0
  4. package/dist/application/model-catalog.js +57 -0
  5. package/dist/application/role-creation-service.d.ts +7 -0
  6. package/dist/application/role-creation-service.js +21 -4
  7. package/dist/application/role-removal-service.d.ts +32 -0
  8. package/dist/application/role-removal-service.js +87 -0
  9. package/dist/application/role-repository.js +13 -1
  10. package/dist/application/session-control.d.ts +74 -0
  11. package/dist/application/session-control.js +66 -1
  12. package/dist/application/types.d.ts +18 -0
  13. package/dist/briefing.js +21 -1
  14. package/dist/cli.js +39 -7
  15. package/dist/config.d.ts +4 -1
  16. package/dist/config.js +3 -2
  17. package/dist/creation.d.ts +6 -3
  18. package/dist/creation.js +5 -1
  19. package/dist/docs.d.ts +1 -1
  20. package/dist/docs.js +47 -11
  21. package/dist/fleet-proxy.d.ts +25 -0
  22. package/dist/fleet-proxy.js +38 -0
  23. package/dist/harness/claude-code.js +20 -3
  24. package/dist/harness/codex.js +14 -2
  25. package/dist/harness/types.d.ts +6 -1
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.js +1 -0
  28. package/dist/owner-channel/channel.d.ts +13 -0
  29. package/dist/owner-channel/channel.js +191 -9
  30. package/dist/owner-channel/state.d.ts +7 -1
  31. package/dist/owner-channel/state.js +41 -4
  32. package/dist/permissions.d.ts +5 -0
  33. package/dist/permissions.js +7 -0
  34. package/dist/runner.d.ts +2 -0
  35. package/dist/runner.js +86 -2
  36. package/dist/session/acp.d.ts +61 -1
  37. package/dist/session/acp.js +398 -20
  38. package/dist/session/arbiter.d.ts +10 -1
  39. package/dist/session/arbiter.js +24 -0
  40. package/dist/session/control.d.ts +33 -2
  41. package/dist/session/control.js +158 -5
  42. package/dist/session/conversation-normalizer.d.ts +34 -0
  43. package/dist/session/conversation-normalizer.js +356 -0
  44. package/dist/session/conversation-store.d.ts +88 -0
  45. package/dist/session/conversation-store.js +347 -0
  46. package/dist/session/conversation-types.d.ts +274 -0
  47. package/dist/session/conversation-types.js +1 -0
  48. package/dist/session/types.d.ts +40 -0
  49. package/dist/spawn.d.ts +6 -1
  50. package/dist/spawn.js +23 -16
  51. package/dist/web/auth.d.ts +1 -1
  52. package/dist/web/fleet-config-service.d.ts +47 -0
  53. package/dist/web/fleet-config-service.js +204 -0
  54. package/dist/web/runtime.js +14 -1
  55. package/dist/web/server.d.ts +6 -0
  56. package/dist/web/server.js +181 -9
  57. package/dist/web/topology.d.ts +31 -0
  58. package/dist/web/topology.js +61 -0
  59. package/dist/web-app/assets/{TerminalView-DMoT8udI.js → TerminalView-hZpyUFY_.js} +1 -1
  60. package/dist/web-app/assets/index-COg4Azq1.css +1 -0
  61. package/dist/web-app/assets/index-Cde9auW0.js +10 -0
  62. package/dist/web-app/index.html +2 -2
  63. package/package.json +1 -1
  64. package/dist/web-app/assets/index-B-jtLAkp.css +0 -1
  65. package/dist/web-app/assets/index-B6T8JLSd.js +0 -9
@@ -0,0 +1,356 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Reduce one ACP v1 `session/update` into one conversation-domain event draft.
4
+ *
5
+ * This is the protocol seam: v1 create/update pairs (and a future v2 upsert
6
+ * stream) both land in the same domain shapes. The function is pure and total —
7
+ * it never throws, never passes raw wire objects through, caps every payload,
8
+ * and quarantines namespaced `_meta` so unknown extensions cannot leak into
9
+ * generic rendering paths.
10
+ */
11
+ /** Cap for any single normalized text payload (spec §5.3). */
12
+ export const MAX_TEXT_BYTES = 256 * 1024;
13
+ /** Cap for one adapter `_meta` namespace value. */
14
+ export const MAX_META_BYTES = 16 * 1024;
15
+ /** Cap for serialized raw tool input/output retained as structured JSON. */
16
+ export const MAX_RAW_JSON_BYTES = 64 * 1024;
17
+ /** Cap for the sanitized preview of an unsupported update. */
18
+ export const MAX_UNSUPPORTED_PREVIEW_CHARS = 2_048;
19
+ const digest24 = (value) => createHash('sha256').update(value).digest('hex').slice(0, 24);
20
+ const asString = (value) => typeof value === 'string' ? value : undefined;
21
+ const asFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
22
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
23
+ /** Truncate to a byte budget without splitting a UTF-8 code point. */
24
+ function truncateUtf8(text, maxBytes) {
25
+ if (Buffer.byteLength(text) <= maxBytes)
26
+ return text;
27
+ const buffer = Buffer.from(text).subarray(0, maxBytes);
28
+ return buffer.toString('utf8').replace(/�+$/u, '');
29
+ }
30
+ function cappedText(raw, redact) {
31
+ const text = asString(raw) ?? '';
32
+ const bytes = Buffer.byteLength(text);
33
+ if (redact !== undefined)
34
+ return { text: redact, bytes, truncated: true, digest: digest24(text) };
35
+ if (bytes <= MAX_TEXT_BYTES)
36
+ return { text, bytes };
37
+ return {
38
+ text: truncateUtf8(text, MAX_TEXT_BYTES), bytes,
39
+ truncated: true, digest: digest24(text),
40
+ };
41
+ }
42
+ function normalizedText(raw, redact) {
43
+ const text = asString(raw) ?? '';
44
+ const bytes = Buffer.byteLength(text);
45
+ if (redact !== undefined)
46
+ return { type: 'text', text: redact, bytes, redacted: true, digest: digest24(text) };
47
+ const capped = cappedText(text);
48
+ return {
49
+ type: 'text', text: capped.text, bytes,
50
+ ...(capped.truncated ? { truncated: true, digest: capped.digest } : {}),
51
+ };
52
+ }
53
+ function normalizeContentBlock(block, redact) {
54
+ if (!isRecord(block))
55
+ return normalizedText('', redact);
56
+ switch (block.type) {
57
+ case 'text':
58
+ return normalizedText(block.text, redact);
59
+ case 'image':
60
+ case 'audio':
61
+ // Described, not carried: media rendering has its own validation phase,
62
+ // and base64 payloads must never ride into the durable store unchecked.
63
+ return {
64
+ type: block.type,
65
+ mimeType: asString(block.mimeType) ?? 'application/octet-stream',
66
+ bytes: Buffer.byteLength(asString(block.data) ?? ''),
67
+ ...(asString(block.uri) ? { uri: asString(block.uri) } : {}),
68
+ };
69
+ case 'resource_link':
70
+ return {
71
+ type: 'resource_link',
72
+ uri: asString(block.uri) ?? '',
73
+ ...(asString(block.name) ? { name: asString(block.name) } : {}),
74
+ ...(asString(block.mimeType) ? { mimeType: asString(block.mimeType) } : {}),
75
+ };
76
+ case 'resource': {
77
+ const resource = isRecord(block.resource) ? block.resource : {};
78
+ const body = asString(resource.text) ?? asString(resource.blob) ?? '';
79
+ return {
80
+ type: 'resource',
81
+ ...(asString(resource.uri) ? { uri: asString(resource.uri) } : {}),
82
+ ...(asString(resource.mimeType) ? { mimeType: asString(resource.mimeType) } : {}),
83
+ bytes: Buffer.byteLength(body),
84
+ };
85
+ }
86
+ default:
87
+ return normalizedText(`[${asString(block.type) ?? 'unknown-content'}]`);
88
+ }
89
+ }
90
+ const SENSITIVE_JSON_KEYS = new Set([
91
+ 'auth', 'authorization', 'cookie', 'password', 'passwd', 'secret', 'token',
92
+ 'apikey', 'accesskey', 'privatekey',
93
+ ]);
94
+ function redactSensitiveJson(value) {
95
+ const seen = new WeakSet();
96
+ let redacted = false;
97
+ const visit = (current, depth) => {
98
+ if (depth > 32)
99
+ return '[depth capped]';
100
+ if (Array.isArray(current)) {
101
+ if (seen.has(current))
102
+ throw new TypeError('circular JSON');
103
+ seen.add(current);
104
+ return current.map(item => visit(item, depth + 1));
105
+ }
106
+ if (!isRecord(current))
107
+ return current;
108
+ if (seen.has(current))
109
+ throw new TypeError('circular JSON');
110
+ seen.add(current);
111
+ const output = {};
112
+ for (const [key, nested] of Object.entries(current)) {
113
+ const compact = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
114
+ if (compact === 'env' || compact === 'environment') {
115
+ redacted = true;
116
+ output[key] = isRecord(nested)
117
+ ? Object.fromEntries(Object.keys(nested).map(name => [name, '<redacted>']))
118
+ : '<redacted>';
119
+ }
120
+ else if (SENSITIVE_JSON_KEYS.has(compact)
121
+ || [...SENSITIVE_JSON_KEYS].some(sensitive => compact.endsWith(sensitive))) {
122
+ redacted = true;
123
+ output[key] = '<redacted>';
124
+ }
125
+ else
126
+ output[key] = visit(nested, depth + 1);
127
+ }
128
+ return output;
129
+ };
130
+ return { value: visit(value, 0), redacted };
131
+ }
132
+ function boundedJson(value) {
133
+ let serialized;
134
+ let safe;
135
+ let redacted = false;
136
+ try {
137
+ const result = redactSensitiveJson(value);
138
+ safe = result.value;
139
+ redacted = result.redacted;
140
+ serialized = JSON.stringify(safe) ?? 'null';
141
+ }
142
+ catch {
143
+ // Circular or otherwise unserializable: keep the fact, drop the value.
144
+ return { bytes: 0, truncated: true };
145
+ }
146
+ const bytes = Buffer.byteLength(serialized);
147
+ if (bytes <= MAX_RAW_JSON_BYTES)
148
+ return { json: safe, bytes, ...(redacted ? { redacted: true } : {}) };
149
+ return {
150
+ bytes, truncated: true, digest: digest24(serialized),
151
+ ...(redacted ? { redacted: true } : {}),
152
+ };
153
+ }
154
+ function quarantineMeta(meta) {
155
+ if (!isRecord(meta))
156
+ return undefined;
157
+ const entries = [];
158
+ for (const [namespace, value] of Object.entries(meta)) {
159
+ let serialized;
160
+ try {
161
+ serialized = JSON.stringify(value) ?? 'null';
162
+ }
163
+ catch {
164
+ entries.push({ namespace, truncated: true });
165
+ continue;
166
+ }
167
+ const bytes = Buffer.byteLength(serialized);
168
+ if (bytes <= MAX_META_BYTES)
169
+ entries.push({ namespace, value });
170
+ else
171
+ entries.push({ namespace, truncated: true, bytes });
172
+ }
173
+ return entries.length ? entries : undefined;
174
+ }
175
+ function planEntries(raw, redact) {
176
+ if (!Array.isArray(raw))
177
+ return [];
178
+ return raw.filter(isRecord).map(entry => ({
179
+ content: cappedText(entry.content, redact),
180
+ priority: entry.priority === 'high' || entry.priority === 'medium' || entry.priority === 'low'
181
+ ? entry.priority : 'medium',
182
+ status: entry.status === 'pending' || entry.status === 'in_progress' || entry.status === 'completed'
183
+ ? entry.status : 'pending',
184
+ }));
185
+ }
186
+ function normalizeToolContent(raw, redact) {
187
+ if (!Array.isArray(raw))
188
+ return undefined;
189
+ return raw.filter(isRecord).map((item) => {
190
+ switch (item.type) {
191
+ case 'diff':
192
+ return {
193
+ type: 'diff',
194
+ path: asString(item.path) ?? '',
195
+ newText: cappedText(item.newText, redact),
196
+ ...(item.oldText != null ? { oldText: cappedText(item.oldText, redact) } : {}),
197
+ };
198
+ case 'terminal':
199
+ return { type: 'terminal', terminalId: asString(item.terminalId) ?? '' };
200
+ case 'content':
201
+ default:
202
+ return { type: 'content', content: normalizeContentBlock(item.content, redact) };
203
+ }
204
+ });
205
+ }
206
+ function toolUpsert(update, snapshot, redact) {
207
+ const payload = {
208
+ toolCallId: asString(update.toolCallId) ?? '',
209
+ snapshot,
210
+ };
211
+ if (asString(update.title) !== undefined)
212
+ payload.title = redact ?? asString(update.title);
213
+ if (asString(update.kind) !== undefined)
214
+ payload.kind = asString(update.kind);
215
+ if (asString(update.status) !== undefined)
216
+ payload.status = asString(update.status);
217
+ const content = normalizeToolContent(update.content, redact);
218
+ if (content)
219
+ payload.content = content;
220
+ if (Array.isArray(update.locations)) {
221
+ payload.locations = update.locations.filter(isRecord).map(location => ({
222
+ path: asString(location.path) ?? '',
223
+ ...(asFiniteNumber(location.line) !== undefined ? { line: asFiniteNumber(location.line) } : {}),
224
+ }));
225
+ }
226
+ if (update.rawInput !== undefined)
227
+ payload.rawInput = boundedJson(update.rawInput);
228
+ if (update.rawOutput !== undefined)
229
+ payload.rawOutput = boundedJson(update.rawOutput);
230
+ return payload;
231
+ }
232
+ function unsupported(update) {
233
+ let serialized;
234
+ try {
235
+ serialized = JSON.stringify(update) ?? String(update);
236
+ }
237
+ catch {
238
+ serialized = '[unserializable update]';
239
+ }
240
+ const kind = isRecord(update) ? asString(update.sessionUpdate) : undefined;
241
+ return {
242
+ sessionUpdate: kind ?? 'unknown',
243
+ bytes: Buffer.byteLength(serialized),
244
+ preview: serialized.slice(0, MAX_UNSUPPORTED_PREVIEW_CHARS),
245
+ };
246
+ }
247
+ export function normalizeSessionUpdate(update, options = {}) {
248
+ const redact = options.redactText;
249
+ const raw = update;
250
+ if (!isRecord(raw) || typeof raw.sessionUpdate !== 'string')
251
+ return { kind: 'unsupported', payload: unsupported(raw) };
252
+ const adapterMeta = quarantineMeta(raw._meta);
253
+ const withMeta = (result) => adapterMeta ? { ...result, adapterMeta } : result;
254
+ switch (raw.sessionUpdate) {
255
+ case 'user_message_chunk':
256
+ case 'agent_message_chunk': {
257
+ const payload = {
258
+ role: raw.sessionUpdate === 'user_message_chunk' ? 'user' : 'assistant',
259
+ content: normalizeContentBlock(raw.content, redact),
260
+ };
261
+ const messageId = asString(raw.messageId);
262
+ return withMeta({ kind: 'message.chunk', payload, ...(messageId ? { messageId } : {}) });
263
+ }
264
+ case 'agent_thought_chunk': {
265
+ const messageId = asString(raw.messageId);
266
+ return withMeta({
267
+ kind: 'thought.chunk',
268
+ payload: { content: normalizeContentBlock(raw.content, redact) },
269
+ ...(messageId ? { messageId } : {}),
270
+ });
271
+ }
272
+ case 'tool_call':
273
+ case 'tool_call_update': {
274
+ const payload = toolUpsert(raw, raw.sessionUpdate === 'tool_call', redact);
275
+ return withMeta({
276
+ kind: 'tool.upsert', payload,
277
+ ...(payload.toolCallId ? { toolCallId: payload.toolCallId } : {}),
278
+ });
279
+ }
280
+ case 'plan':
281
+ return withMeta({
282
+ kind: 'plan.replace',
283
+ payload: { entries: planEntries(raw.entries, redact) },
284
+ });
285
+ case 'plan_update': {
286
+ // Unstable representation: normalize what is structured, reference the rest.
287
+ const plan = isRecord(raw.plan) ? raw.plan : {};
288
+ const payload = {
289
+ ...(asString(plan.planId) ? { planId: asString(plan.planId) } : {}),
290
+ };
291
+ if (plan.type === 'items')
292
+ payload.entries = planEntries(plan.entries, redact);
293
+ else if (plan.type === 'file' && asString(plan.uri))
294
+ payload.file = { uri: asString(plan.uri) };
295
+ else if (plan.type === 'markdown')
296
+ payload.markdown = cappedText(plan.content, redact);
297
+ return withMeta({ kind: 'plan.replace', payload });
298
+ }
299
+ case 'plan_removed':
300
+ return withMeta({
301
+ kind: 'plan.replace',
302
+ payload: {
303
+ ...(asString(raw.planId) ? { planId: asString(raw.planId) } : {}),
304
+ removed: true,
305
+ },
306
+ });
307
+ case 'usage_update': {
308
+ const cost = isRecord(raw.cost)
309
+ && asFiniteNumber(raw.cost.amount) !== undefined && asString(raw.cost.currency)
310
+ ? { amount: asFiniteNumber(raw.cost.amount), currency: asString(raw.cost.currency) }
311
+ : undefined;
312
+ return withMeta({
313
+ kind: 'usage.updated',
314
+ payload: {
315
+ used: asFiniteNumber(raw.used) ?? 0,
316
+ size: asFiniteNumber(raw.size) ?? 0,
317
+ ...(cost ? { cost } : {}),
318
+ },
319
+ });
320
+ }
321
+ case 'current_mode_update':
322
+ return withMeta({
323
+ kind: 'session.state',
324
+ payload: { currentModeId: asString(raw.currentModeId) },
325
+ });
326
+ case 'session_info_update':
327
+ return withMeta({
328
+ kind: 'session.info',
329
+ payload: {
330
+ ...(raw.title !== undefined ? { title: asString(raw.title) ?? null } : {}),
331
+ ...(raw.updatedAt !== undefined ? { updatedAt: asString(raw.updatedAt) ?? null } : {}),
332
+ },
333
+ });
334
+ case 'available_commands_update': {
335
+ const commands = Array.isArray(raw.availableCommands)
336
+ ? raw.availableCommands.filter(isRecord).map(command => ({
337
+ name: asString(command.name) ?? '',
338
+ description: cappedText(command.description),
339
+ ...(isRecord(command.input) && asString(command.input.hint)
340
+ ? { inputHint: asString(command.input.hint) } : {}),
341
+ }))
342
+ : [];
343
+ const payload = { commands };
344
+ return withMeta({ kind: 'capabilities.updated', payload });
345
+ }
346
+ case 'config_option_update':
347
+ // Structured but adapter-shaped: retained as bounded JSON for later,
348
+ // capability-gated rendering rather than trusted field-by-field today.
349
+ return withMeta({
350
+ kind: 'capabilities.updated',
351
+ payload: { configOptions: boundedJson(raw.configOptions) },
352
+ });
353
+ default:
354
+ return withMeta({ kind: 'unsupported', payload: unsupported(raw) });
355
+ }
356
+ }
@@ -0,0 +1,88 @@
1
+ import type { ConversationEventV1, PromptReceipt } from './conversation-types.js';
2
+ export interface ConversationStoreOptions {
3
+ roleId: string;
4
+ segmentBytes?: number;
5
+ log?(line: string): void;
6
+ }
7
+ export interface OpenPrompt {
8
+ promptId: string;
9
+ state: 'admitted' | 'started';
10
+ /** The prompt body, when it was persisted (browser/local sources). */
11
+ text?: string;
12
+ commandId?: string;
13
+ sessionGeneration: string;
14
+ }
15
+ export interface ConversationPageRequest {
16
+ after?: string;
17
+ limit?: number;
18
+ }
19
+ export interface ConversationStorePage {
20
+ events: ConversationEventV1[];
21
+ firstAvailableCursor?: string;
22
+ nextCursor?: string;
23
+ hasMore: boolean;
24
+ }
25
+ export declare class IdempotencyConflictError extends Error {
26
+ constructor();
27
+ }
28
+ type EventDraft = Omit<ConversationEventV1, 'schemaVersion' | 'roleId' | 'eventId' | 'seq' | 'at'>;
29
+ export declare class ConversationEventStore {
30
+ private readonly dir;
31
+ private nextSeq;
32
+ private segments;
33
+ private tail;
34
+ private readonly listeners;
35
+ private readonly commands;
36
+ private readonly promptStates;
37
+ private activeFd?;
38
+ private activeBytes;
39
+ private _degraded;
40
+ private degradedReason?;
41
+ private readonly segmentBytes;
42
+ private readonly roleId;
43
+ private readonly log;
44
+ constructor(dir: string, options: ConversationStoreOptions);
45
+ /** First 24 hex chars of sha-256; the idempotency body-digest convention. */
46
+ static bodyDigest(body: string): string;
47
+ get degraded(): boolean;
48
+ get degradedDetail(): string | undefined;
49
+ /**
50
+ * Durably append one event. Throws when the record cannot be persisted —
51
+ * the caller must fail its command rather than acknowledge a lost prompt.
52
+ */
53
+ append(draft: EventDraft): ConversationEventV1;
54
+ /**
55
+ * Append an agent-stream event; on failure record degradation and keep the
56
+ * role alive. Conversation durability may degrade, active work must not die.
57
+ */
58
+ appendSafe(draft: EventDraft): ConversationEventV1 | undefined;
59
+ page(request?: ConversationPageRequest): ConversationStorePage;
60
+ subscribe(listener: (event: ConversationEventV1) => void): () => void;
61
+ /** Store the receipt a repeated command must get back. */
62
+ recordReceipt(commandId: string, receipt: PromptReceipt, bodyDigest: string): void;
63
+ /**
64
+ * The receipt for a previously accepted command, or undefined for a new one.
65
+ * A reused ID with a different body digest is a conflict, never a replay.
66
+ */
67
+ receiptFor(commandId: string, bodyDigest: string): PromptReceipt | undefined;
68
+ /**
69
+ * Prompts with no terminal event, classified for restart recovery:
70
+ * `admitted` never started and is safe to restore into the FIFO;
71
+ * `started` may already have had side effects and must not be replayed.
72
+ */
73
+ openPrompts(): OpenPrompt[];
74
+ lastCursor(): string | undefined;
75
+ close(): void;
76
+ private recover;
77
+ private readManifest;
78
+ private discoverSegments;
79
+ private readSegment;
80
+ private rebuildCommandIndex;
81
+ private trackPromptState;
82
+ private segmentFd;
83
+ private writeManifest;
84
+ private firstStoredSeq;
85
+ private eventsAfter;
86
+ private markDegraded;
87
+ }
88
+ export {};