@ours.network/fleet 1.1.0-nightly.12 → 1.1.0-nightly.13
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/dist/application/role-creation-service.js +8 -1
- package/dist/application/task-room-service.d.ts +44 -4
- package/dist/application/task-room-service.js +137 -20
- package/dist/build-info.json +4 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +18 -5
- package/dist/fleet-command-audit.d.ts +4 -0
- package/dist/fleet-command-audit.js +126 -22
- package/dist/fleet-proxy.d.ts +3 -0
- package/dist/lifecycle-summary.d.ts +118 -0
- package/dist/lifecycle-summary.js +161 -0
- package/dist/owner-channel/channel.d.ts +2 -0
- package/dist/owner-channel/channel.js +45 -9
- package/dist/owner-channel/commands.d.ts +4 -1
- package/dist/owner-channel/commands.js +3 -6
- package/dist/rooms-tasks/cli.js +151 -19
- package/dist/rooms-tasks/close.d.ts +8 -0
- package/dist/rooms-tasks/close.js +10 -3
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/launch-snapshot.d.ts +9 -0
- package/dist/rooms-tasks/launch-snapshot.js +18 -1
- package/dist/rooms-tasks/provision.js +77 -8
- package/dist/rooms-tasks/task-state.d.ts +113 -3
- package/dist/rooms-tasks/task-state.js +333 -9
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +7 -3
- package/dist/rooms-tasks/types.d.ts +48 -0
- package/dist/web/server.js +28 -1
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AgentLaunchConfiguration } from './lifecycle-summary.js';
|
|
1
2
|
export type FleetCommandDecision = 'allow' | 'deny' | 'unsupported';
|
|
2
3
|
export type FleetCommandOutcomeClass = 'success' | 'validation' | 'denied' | 'runtime' | 'timeout' | 'proxy' | 'delivery';
|
|
3
4
|
export declare class FleetCliExit extends Error {
|
|
@@ -21,6 +22,7 @@ export type FleetAuditPresentation = {
|
|
|
21
22
|
parent: string;
|
|
22
23
|
actionId: string;
|
|
23
24
|
inherited: string[];
|
|
25
|
+
configuration?: AgentLaunchConfiguration;
|
|
24
26
|
} | {
|
|
25
27
|
kind: 'task';
|
|
26
28
|
operation: 'create' | 'start' | 'work' | 'block' | 'unblock' | 'review' | 'done' | 'cancel' | 'finish' | 'delete' | 'settling';
|
|
@@ -38,6 +40,7 @@ export type FleetAuditPresentation = {
|
|
|
38
40
|
brain?: string;
|
|
39
41
|
role: string;
|
|
40
42
|
permissions?: string;
|
|
43
|
+
configuration?: AgentLaunchConfiguration;
|
|
41
44
|
}>;
|
|
42
45
|
} | {
|
|
43
46
|
kind: 'room';
|
|
@@ -56,6 +59,7 @@ export type FleetAuditPresentation = {
|
|
|
56
59
|
brain?: string;
|
|
57
60
|
role: string;
|
|
58
61
|
permissions?: string;
|
|
62
|
+
configuration?: AgentLaunchConfiguration;
|
|
59
63
|
}>;
|
|
60
64
|
} | {
|
|
61
65
|
kind: 'lifecycle_failure';
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
3
4
|
import { replaceFileAtomically } from './atomic-file.js';
|
|
4
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';
|
|
5
8
|
export class FleetCliExit extends Error {
|
|
6
9
|
exitCode;
|
|
7
10
|
outcomeClass;
|
|
@@ -330,10 +333,71 @@ export function validateFleetAuditFinish(value) {
|
|
|
330
333
|
|| input.presentations.length > 128 || !input.presentations.every(validPresentation))))
|
|
331
334
|
throw new Error('invalid fleet audit finish fields');
|
|
332
335
|
}
|
|
336
|
+
const APPROVAL_MODES = new Set(['ask', 'auto', 'allow', 'deny']);
|
|
337
|
+
const FILESYSTEM_MODES = new Set(['read-only', 'workspace', 'unrestricted']);
|
|
338
|
+
const UNATTENDED_MODES = new Set(['deny', 'wait']);
|
|
339
|
+
const FLEET_PERMISSION_MODES = new Set(['ask', 'auto', 'allow']);
|
|
340
|
+
const PRESENTATION_TEXT = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u;
|
|
341
|
+
/**
|
|
342
|
+
* Exact-key validation of a current-version launch configuration. A partial
|
|
343
|
+
* v1 configuration is rejected; a legacy presentation simply omits the field.
|
|
344
|
+
*/
|
|
345
|
+
function validConfiguration(value) {
|
|
346
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
347
|
+
return false;
|
|
348
|
+
const c = value;
|
|
349
|
+
const text = (v, max = 256) => typeof v === 'string' && v.length <= max
|
|
350
|
+
&& !PRESENTATION_TEXT.test(v);
|
|
351
|
+
const exact = (record, keys) => Object.keys(record).every(key => keys.includes(key));
|
|
352
|
+
const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
353
|
+
const origin = (v) => {
|
|
354
|
+
if (!record(v))
|
|
355
|
+
return false;
|
|
356
|
+
if (v.kind === 'named')
|
|
357
|
+
return exact(v, ['kind', 'ref']) && text(v.ref, 160);
|
|
358
|
+
if (v.kind === 'inline')
|
|
359
|
+
return exact(v, ['kind', 'fingerprint'])
|
|
360
|
+
&& (v.fingerprint === undefined || (typeof v.fingerprint === 'string' && /^[a-f0-9]{12}$/.test(v.fingerprint)));
|
|
361
|
+
return v.kind === 'unknown' && exact(v, ['kind']);
|
|
362
|
+
};
|
|
363
|
+
const count = (v) => v === undefined
|
|
364
|
+
|| (Number.isSafeInteger(v) && Number(v) >= 0 && Number(v) <= 4_096);
|
|
365
|
+
return exact(c, ['version', 'template', 'role', 'brain', 'harness', 'session', 'model',
|
|
366
|
+
'effort', 'mission', 'approval', 'filesystem', 'unattended', 'permissionMode', 'monitor', 'isolation'])
|
|
367
|
+
&& c.version === 1 && origin(c.role) && origin(c.brain)
|
|
368
|
+
&& text(c.harness, 64) && c.session === 'acp'
|
|
369
|
+
&& (c.model === null || text(c.model, 160))
|
|
370
|
+
&& (c.template === undefined || text(c.template, 160))
|
|
371
|
+
&& (c.effort === undefined || text(c.effort, 32))
|
|
372
|
+
// The builder caps the mission label at 80 code points; enforce the same
|
|
373
|
+
// invariant here so the per-line budget proof holds for wire input too.
|
|
374
|
+
&& (c.mission === undefined || (text(c.mission) && Array.from(String(c.mission)).length <= 80))
|
|
375
|
+
&& APPROVAL_MODES.has(String(c.approval))
|
|
376
|
+
&& FILESYSTEM_MODES.has(String(c.filesystem))
|
|
377
|
+
&& UNATTENDED_MODES.has(String(c.unattended))
|
|
378
|
+
&& record(c.permissionMode)
|
|
379
|
+
&& exact(c.permissionMode, ['fleetMode', 'nativeMode'])
|
|
380
|
+
&& FLEET_PERMISSION_MODES.has(String(c.permissionMode.fleetMode))
|
|
381
|
+
&& text(c.permissionMode.nativeMode, 160)
|
|
382
|
+
&& record(c.monitor) && exact(c.monitor, ['mode', 'interrupt'])
|
|
383
|
+
&& ['fleet', 'native'].includes(String(c.monitor.mode))
|
|
384
|
+
&& (typeof c.monitor.interrupt === 'boolean' || c.monitor.interrupt === 'after_tool')
|
|
385
|
+
&& (c.isolation === undefined || (record(c.isolation)
|
|
386
|
+
&& exact(c.isolation, ['requested', 'on_unavailable', 'network', 'read_mounts', 'write_mounts'])
|
|
387
|
+
&& ['auto', 'bubblewrap', 'podman', 'none'].includes(String(c.isolation.requested))
|
|
388
|
+
&& (c.isolation.on_unavailable === undefined || ['warn', 'strict'].includes(String(c.isolation.on_unavailable)))
|
|
389
|
+
&& (c.isolation.network === undefined || ['broker', 'deny', 'allow', 'allowlist'].includes(String(c.isolation.network)))
|
|
390
|
+
&& count(c.isolation.read_mounts) && count(c.isolation.write_mounts)))
|
|
391
|
+
// Escaping and code-fence growth can push a per-field-valid configuration
|
|
392
|
+
// past the line budget; a v1 configuration whose complete mandatory
|
|
393
|
+
// rendering cannot fit is invalid, never silently trimmed.
|
|
394
|
+
&& mandatoryConfigurationFits(c);
|
|
395
|
+
}
|
|
333
396
|
function validPresentation(value) {
|
|
334
397
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
335
398
|
return false;
|
|
336
399
|
const p = value;
|
|
400
|
+
const configured = (record) => record.configuration === undefined || validConfiguration(record.configuration);
|
|
337
401
|
const safe = (v) => typeof v === 'string' && SAFE_RESOURCE_ID.test(v);
|
|
338
402
|
const text = (v) => typeof v === 'string' && v.length <= 256
|
|
339
403
|
&& !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(v);
|
|
@@ -342,11 +406,11 @@ function validPresentation(value) {
|
|
|
342
406
|
&& allowed[p.operation]?.includes(`${String(p.previousState ?? 'none')}→${p.newState}`);
|
|
343
407
|
if (p.kind === 'agent_started')
|
|
344
408
|
return exact(p, ['kind', 'eventId', 'id', 'name', 'lifetime', 'brain', 'role',
|
|
345
|
-
'harness', 'session', 'model', 'permissions', 'parent', 'actionId', 'inherited'])
|
|
409
|
+
'harness', 'session', 'model', 'permissions', 'parent', 'actionId', 'inherited', 'configuration'])
|
|
346
410
|
&& safe(p.eventId) && safe(p.id) && safe(p.name) && ['permanent', 'temporary'].includes(String(p.lifetime))
|
|
347
411
|
&& text(p.brain) && text(p.role) && safe(p.harness) && p.session === 'acp' && safe(p.parent) && safe(p.actionId)
|
|
348
412
|
&& (p.model === undefined || text(p.model)) && (p.permissions === undefined || text(p.permissions))
|
|
349
|
-
&& Array.isArray(p.inherited) && p.inherited.every(text);
|
|
413
|
+
&& Array.isArray(p.inherited) && p.inherited.every(text) && configured(p);
|
|
350
414
|
if (p.kind === 'task')
|
|
351
415
|
return exact(p, ['kind', 'operation', 'eventId', 'id', 'title', 'previousState',
|
|
352
416
|
'newState', 'template', 'roomId', 'revision', 'list', 'agents'])
|
|
@@ -358,18 +422,24 @@ function validPresentation(value) {
|
|
|
358
422
|
unblock: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
|
|
359
423
|
review: ['active→review'], done: ['review→done'], finish: ['active→done', 'review→done'],
|
|
360
424
|
cancel: ['backlog→cancelled', 'provisioning→cancelled', 'active→cancelled', 'review→cancelled'],
|
|
361
|
-
delete: [
|
|
425
|
+
delete: [
|
|
426
|
+
'backlog→deleting', 'provisioning→deleting', 'active→deleting', 'review→deleting',
|
|
427
|
+
'done→deleting', 'cancelled→deleting', 'failed→deleting',
|
|
428
|
+
'backlog→deleted', 'provisioning→deleted', 'active→deleted', 'review→deleted',
|
|
429
|
+
'done→deleted', 'cancelled→deleted', 'failed→deleted',
|
|
430
|
+
], settling: ['active→active', 'review→review', 'backlog→backlog',
|
|
362
431
|
'provisioning→provisioning'] })
|
|
363
432
|
&& (p.title === undefined || text(p.title)) && (p.previousState === undefined || safe(p.previousState))
|
|
364
433
|
&& (p.template === undefined || text(p.template)) && (p.roomId === undefined || safe(p.roomId))
|
|
365
434
|
&& (p.list === undefined || text(p.list)) && (p.revision === undefined || text(p.revision))
|
|
366
435
|
&& Array.isArray(p.agents) && p.agents.length <= 64
|
|
367
436
|
&& p.agents.every(a => a && typeof a === 'object'
|
|
368
|
-
&& exact(a, ['name', 'brain', 'role', 'permissions'])
|
|
437
|
+
&& exact(a, ['name', 'brain', 'role', 'permissions', 'configuration'])
|
|
369
438
|
&& safe(a.name)
|
|
370
439
|
&& text(a.role)
|
|
371
440
|
&& (a.brain === undefined || text(a.brain))
|
|
372
|
-
&& (a.permissions === undefined || text(a.permissions))
|
|
441
|
+
&& (a.permissions === undefined || text(a.permissions))
|
|
442
|
+
&& configured(a));
|
|
373
443
|
if (p.kind === 'room')
|
|
374
444
|
return exact(p, ['kind', 'operation', 'eventId', 'id', 'previousState', 'newState',
|
|
375
445
|
'revision', 'name', 'template', 'taskId', 'participants'])
|
|
@@ -383,12 +453,13 @@ function validPresentation(value) {
|
|
|
383
453
|
&& (p.taskId === undefined || safe(p.taskId)) && (p.revision === undefined || text(p.revision))
|
|
384
454
|
&& Array.isArray(p.participants) && p.participants.length <= 64
|
|
385
455
|
&& p.participants.every(a => a && typeof a === 'object'
|
|
386
|
-
&& exact(a, ['name', 'id', 'brain', 'role', 'permissions'])
|
|
456
|
+
&& exact(a, ['name', 'id', 'brain', 'role', 'permissions', 'configuration'])
|
|
387
457
|
&& safe(a.name)
|
|
388
458
|
&& text(a.role)
|
|
389
459
|
&& (a.id === undefined || safe(a.id))
|
|
390
460
|
&& (a.brain === undefined || text(a.brain))
|
|
391
|
-
&& (a.permissions === undefined || text(a.permissions))
|
|
461
|
+
&& (a.permissions === undefined || text(a.permissions))
|
|
462
|
+
&& configured(a));
|
|
392
463
|
if (p.kind === 'lifecycle_failure')
|
|
393
464
|
return exact(p, ['kind', 'eventId', 'resource', 'id', 'state', 'category'])
|
|
394
465
|
&& ['Agent', 'Task', 'Room'].includes(String(p.resource))
|
|
@@ -396,6 +467,35 @@ function validPresentation(value) {
|
|
|
396
467
|
'settlement_pending', 'cleanup_failed', 'cleanup_pending'].includes(String(p.category));
|
|
397
468
|
return false;
|
|
398
469
|
}
|
|
470
|
+
const messageCodePoints = (value) => Array.from(value).length;
|
|
471
|
+
const messageBytes = (value) => Buffer.byteLength(value, 'utf8');
|
|
472
|
+
const withinMessageBounds = (value) => messageCodePoints(value) <= MARKDOWN_MAX_CODE_POINTS && messageBytes(value) <= MARKDOWN_MAX_BYTES;
|
|
473
|
+
/**
|
|
474
|
+
* Append per-agent lines to a bounded message. Lines are admitted whole, in
|
|
475
|
+
* the given (deterministic) order; once the shared Markdown bounds would be
|
|
476
|
+
* exceeded the remaining agents collapse into an accurate omission note. The
|
|
477
|
+
* first line is admitted whenever it fits the absolute bounds so an oversized
|
|
478
|
+
* roster still shows at least one complete agent summary.
|
|
479
|
+
*/
|
|
480
|
+
function appendAgentLines(header, heading, lines) {
|
|
481
|
+
if (!lines.length)
|
|
482
|
+
return header;
|
|
483
|
+
let message = `${header}\n${heading}:`;
|
|
484
|
+
const note = (count) => `\n…and ${count} more agent${count === 1 ? '' : 's'} omitted.`;
|
|
485
|
+
for (const [index, line] of lines.entries()) {
|
|
486
|
+
const candidate = `${message}\n- ${line}`;
|
|
487
|
+
const remaining = lines.length - index - 1;
|
|
488
|
+
// A candidate is admitted only when it fits together with the note for
|
|
489
|
+
// every agent still pending, so each stop-return below re-states a bound
|
|
490
|
+
// that was verified when the current message was admitted.
|
|
491
|
+
if (!withinMessageBounds(remaining ? `${candidate}${note(remaining)}` : candidate)) {
|
|
492
|
+
const stopped = `${message}${note(lines.length - index)}`;
|
|
493
|
+
return withinMessageBounds(stopped) ? stopped : `${header}${note(lines.length)}`;
|
|
494
|
+
}
|
|
495
|
+
message = candidate;
|
|
496
|
+
}
|
|
497
|
+
return message;
|
|
498
|
+
}
|
|
399
499
|
/** Compact Owner presentation. It intentionally has no command, argv, environment, or correlation data. */
|
|
400
500
|
export function renderFleetLifecycleEvent(value) {
|
|
401
501
|
if (value.kind === 'lifecycle_failure') {
|
|
@@ -413,25 +513,29 @@ export function renderFleetLifecycleEvent(value) {
|
|
|
413
513
|
+ `state ${value.state}. Action: ${actions[value.category]}`;
|
|
414
514
|
}
|
|
415
515
|
if (value.kind === 'agent_started') {
|
|
416
|
-
const
|
|
516
|
+
const summary = renderAgentConfiguration(value.configuration, {
|
|
517
|
+
role: value.role, brain: value.brain, permissions: value.permissions,
|
|
518
|
+
});
|
|
417
519
|
return `🧑💻 ${value.parent} spawned ${value.lifetime} Agent ${value.name} (${value.id}) — ready. `
|
|
418
|
-
+
|
|
520
|
+
+ `${summary}.`;
|
|
419
521
|
}
|
|
420
522
|
if (value.kind === 'task') {
|
|
421
|
-
const title = value.title ? ` “${value.title}”` : '';
|
|
422
|
-
const context = [value.list ? `List ${value.list}` : undefined,
|
|
423
|
-
value.template ? `template ${value.template}` : undefined,
|
|
523
|
+
const title = value.title ? ` “${markdownProse(value.title)}”` : '';
|
|
524
|
+
const context = [value.list ? `List ${markdownCode(value.list)}` : undefined,
|
|
525
|
+
value.template ? `template ${markdownCode(value.template)}` : undefined,
|
|
424
526
|
value.roomId ? `Room ${value.roomId}` : undefined].filter(Boolean).join('; ');
|
|
425
|
-
const
|
|
426
|
-
+ `${
|
|
427
|
-
return
|
|
428
|
-
|
|
527
|
+
const header = `📋 Task${title} (${value.id}) ${value.operation}: `
|
|
528
|
+
+ `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}`;
|
|
529
|
+
return appendAgentLines(header, 'Agents', value.agents.map(agent => `${markdownCode(agent.name)}: ${renderAgentConfiguration(agent.configuration, {
|
|
530
|
+
role: agent.role, brain: agent.brain, permissions: agent.permissions,
|
|
531
|
+
})}`));
|
|
429
532
|
}
|
|
430
|
-
const context = [value.template ? `template ${value.template}` : undefined,
|
|
533
|
+
const context = [value.template ? `template ${markdownCode(value.template)}` : undefined,
|
|
431
534
|
value.taskId ? `Task ${value.taskId}` : undefined].filter(Boolean).join('; ');
|
|
432
|
-
const
|
|
433
|
-
+ `${
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
535
|
+
const header = `🏠 Room${value.name ? ` “${markdownProse(value.name)}”` : ''} (${value.id}) ${value.operation}: `
|
|
536
|
+
+ `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}`;
|
|
537
|
+
return appendAgentLines(header, 'Participants', value.participants.map(participant => `${markdownCode(participant.name)}${participant.id ? ` (${participant.id})` : ''}: `
|
|
538
|
+
+ renderAgentConfiguration(participant.configuration, {
|
|
539
|
+
role: participant.role, brain: participant.brain, permissions: participant.permissions,
|
|
540
|
+
})));
|
|
437
541
|
}
|
package/dist/fleet-proxy.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MonitorConfig, ResolvedRole } 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";
|
|
@@ -21,6 +22,8 @@ export interface ManagedFleetSpawnResult {
|
|
|
21
22
|
creationActionId: string;
|
|
22
23
|
brainSummary: string;
|
|
23
24
|
roleSummary: string;
|
|
25
|
+
/** Launch configuration captured from the exact resolved role; absent only from pre-upgrade daemons. */
|
|
26
|
+
configuration?: AgentLaunchConfiguration;
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
29
|
* Fill only omitted spawn settings from the live caller. Explicit agent choices
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { AgentSelection, ApprovalMode, FilesystemMode, FleetPermissionMode, MonitorInterrupt, MonitorMode, ResolvedRole, SessionBackendId, UnattendedMode } from './config.js';
|
|
2
|
+
import type { IsolationBackendId, NetworkMode, OnUnavailable } from './isolation/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Operator-facing launch configuration captured at the launch boundary from
|
|
5
|
+
* the exact ResolvedRole that was spawned. It is the single source for every
|
|
6
|
+
* Fleet lifecycle/report surface (CLI and owner-channel/Messenger).
|
|
7
|
+
*
|
|
8
|
+
* Security boundary: this object is a strict whitelist. It must never carry
|
|
9
|
+
* env, cwd or any local path, harness_options, session_options, owner_channel,
|
|
10
|
+
* or auth_proxy content. Hashes are secondary provenance only, never the
|
|
11
|
+
* primary description.
|
|
12
|
+
*/
|
|
13
|
+
export interface AgentLaunchConfiguration {
|
|
14
|
+
version: 1;
|
|
15
|
+
/** Agent template / launch definition label when one was selected. */
|
|
16
|
+
template?: string;
|
|
17
|
+
role: SelectionOrigin;
|
|
18
|
+
brain: SelectionOrigin;
|
|
19
|
+
harness: string;
|
|
20
|
+
session: SessionBackendId;
|
|
21
|
+
/**
|
|
22
|
+
* Model observed effective at launch. `null` reports runtime behavior — the
|
|
23
|
+
* harness ran its own default — not whether the author wrote `model: null`
|
|
24
|
+
* or omitted the field; effectiveRoleModel cannot distinguish those.
|
|
25
|
+
*/
|
|
26
|
+
model: string | null;
|
|
27
|
+
effort?: string;
|
|
28
|
+
/** Concise first-line mission label (or Cowork-role fallback), never the full mission. */
|
|
29
|
+
mission?: string;
|
|
30
|
+
approval: ApprovalMode;
|
|
31
|
+
filesystem: FilesystemMode;
|
|
32
|
+
unattended: UnattendedMode;
|
|
33
|
+
/** Portable policy plus the exact native runtime mode after harness_options. */
|
|
34
|
+
permissionMode: {
|
|
35
|
+
fleetMode: FleetPermissionMode;
|
|
36
|
+
nativeMode: string;
|
|
37
|
+
};
|
|
38
|
+
monitor: {
|
|
39
|
+
mode: MonitorMode;
|
|
40
|
+
interrupt: MonitorInterrupt;
|
|
41
|
+
};
|
|
42
|
+
/** Requested isolation policy only — never resolved runtime facts or host paths. */
|
|
43
|
+
isolation?: {
|
|
44
|
+
requested: IsolationBackendId;
|
|
45
|
+
on_unavailable?: OnUnavailable;
|
|
46
|
+
network?: NetworkMode;
|
|
47
|
+
read_mounts?: number;
|
|
48
|
+
write_mounts?: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Where a Brain/Role selection came from; labels are human, hashes secondary. */
|
|
52
|
+
export type SelectionOrigin = {
|
|
53
|
+
kind: 'named';
|
|
54
|
+
ref: string;
|
|
55
|
+
} | {
|
|
56
|
+
kind: 'inline';
|
|
57
|
+
fingerprint?: string;
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'unknown';
|
|
60
|
+
};
|
|
61
|
+
export declare const MISSION_LABEL_MAX = 80;
|
|
62
|
+
/**
|
|
63
|
+
* The inline fingerprint is always computed here from the canonical inline
|
|
64
|
+
* body; provenance can never be an arbitrary caller string.
|
|
65
|
+
*/
|
|
66
|
+
export declare function selectionOrigin(selection: AgentSelection | undefined): SelectionOrigin;
|
|
67
|
+
export declare function missionLabel(mission: string | undefined): string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Build the presentation from the exact resolved launch state. `origins`
|
|
70
|
+
* carries the authoring-time selections and template label because a merged
|
|
71
|
+
* ResolvedRole no longer distinguishes preset references from inline bodies.
|
|
72
|
+
* `permissionMode` is required at capture time: every supported harness
|
|
73
|
+
* adapter reports it, and the managed-spawn receiver gets it over the wire
|
|
74
|
+
* and must never re-resolve mutable configuration.
|
|
75
|
+
*/
|
|
76
|
+
export declare function summarizeResolvedLaunch(role: ResolvedRole, origins: {
|
|
77
|
+
role: SelectionOrigin;
|
|
78
|
+
brain: SelectionOrigin;
|
|
79
|
+
template?: string;
|
|
80
|
+
permissionMode: {
|
|
81
|
+
fleetMode: FleetPermissionMode;
|
|
82
|
+
nativeMode: string;
|
|
83
|
+
};
|
|
84
|
+
/** Shown when the resolved role has no mission text (e.g. Cowork role name). */
|
|
85
|
+
missionFallback?: string;
|
|
86
|
+
}): AgentLaunchConfiguration;
|
|
87
|
+
/**
|
|
88
|
+
* Component budget for one rendered agent line. The complete mandatory
|
|
89
|
+
* rendering must always fit — that is a validity condition of a current-v1
|
|
90
|
+
* configuration, enforced by mandatoryConfigurationFits at the builder AND
|
|
91
|
+
* the wire boundary — while optional components (monitor, isolation) are
|
|
92
|
+
* dropped whole with a visible `…` marker past this budget.
|
|
93
|
+
*/
|
|
94
|
+
export declare const AGENT_LINE_MAX_CODE_POINTS = 1500;
|
|
95
|
+
export declare const AGENT_LINE_MAX_BYTES = 5000;
|
|
96
|
+
/**
|
|
97
|
+
* Validity condition of a current-v1 configuration: its complete mandatory
|
|
98
|
+
* rendering fits the per-line budget with the optional-omission suffix
|
|
99
|
+
* reserved. Escaping and code-fence growth (a value made of backticks can
|
|
100
|
+
* more than triple its code span) make this depend on rendered size, not raw
|
|
101
|
+
* field lengths, so both summarizeResolvedLaunch and the wire validator call
|
|
102
|
+
* this instead of trusting per-field caps.
|
|
103
|
+
*/
|
|
104
|
+
export declare function mandatoryConfigurationFits(configuration: AgentLaunchConfiguration): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Render one agent's configuration as a single escaped Markdown line segment.
|
|
107
|
+
* This is the only escaping boundary: callers pass raw values and must not
|
|
108
|
+
* escape the result again (it is safe as a markdownItems entry and inside
|
|
109
|
+
* owner-channel lifecycle messages). Every mandatory component always
|
|
110
|
+
* renders — mandatoryConfigurationFits guarantees the fit for every produced
|
|
111
|
+
* or wire-accepted configuration; only optional components may be dropped,
|
|
112
|
+
* whole (never splitting a Markdown span), with a visible `…`.
|
|
113
|
+
*/
|
|
114
|
+
export declare function renderAgentConfiguration(configuration: AgentLaunchConfiguration | undefined, fallback?: {
|
|
115
|
+
role?: string;
|
|
116
|
+
brain?: string;
|
|
117
|
+
permissions?: string;
|
|
118
|
+
}): string;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { canonicalJson } from './canonical-json.js';
|
|
4
|
+
import { effectiveRoleModel } from './model-env.js';
|
|
5
|
+
import { markdownCode, markdownProse } from './rooms-tasks/markdown.js';
|
|
6
|
+
const FINGERPRINT_HEX = 12;
|
|
7
|
+
export const MISSION_LABEL_MAX = 80;
|
|
8
|
+
/**
|
|
9
|
+
* The inline fingerprint is always computed here from the canonical inline
|
|
10
|
+
* body; provenance can never be an arbitrary caller string.
|
|
11
|
+
*/
|
|
12
|
+
export function selectionOrigin(selection) {
|
|
13
|
+
if (!selection)
|
|
14
|
+
return { kind: 'unknown' };
|
|
15
|
+
if ('ref' in selection)
|
|
16
|
+
return { kind: 'named', ref: selection.ref };
|
|
17
|
+
const fingerprint = createHash('sha256').update(canonicalJson(selection.inline)).digest('hex');
|
|
18
|
+
return { kind: 'inline', fingerprint: fingerprint.slice(0, FINGERPRINT_HEX) };
|
|
19
|
+
}
|
|
20
|
+
export function missionLabel(mission) {
|
|
21
|
+
const line = mission?.trim().split('\n', 1)[0]?.trim();
|
|
22
|
+
if (!line)
|
|
23
|
+
return undefined;
|
|
24
|
+
const points = Array.from(line);
|
|
25
|
+
return points.length > MISSION_LABEL_MAX
|
|
26
|
+
? `${points.slice(0, MISSION_LABEL_MAX - 1).join('')}…` : line;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the presentation from the exact resolved launch state. `origins`
|
|
30
|
+
* carries the authoring-time selections and template label because a merged
|
|
31
|
+
* ResolvedRole no longer distinguishes preset references from inline bodies.
|
|
32
|
+
* `permissionMode` is required at capture time: every supported harness
|
|
33
|
+
* adapter reports it, and the managed-spawn receiver gets it over the wire
|
|
34
|
+
* and must never re-resolve mutable configuration.
|
|
35
|
+
*/
|
|
36
|
+
export function summarizeResolvedLaunch(role, origins) {
|
|
37
|
+
const mission = missionLabel(role.mission) ?? missionLabel(origins.missionFallback);
|
|
38
|
+
const fs = role.isolation?.fs;
|
|
39
|
+
const configuration = {
|
|
40
|
+
version: 1,
|
|
41
|
+
...(origins.template ? { template: origins.template } : {}),
|
|
42
|
+
role: origins.role,
|
|
43
|
+
brain: origins.brain,
|
|
44
|
+
harness: role.harness,
|
|
45
|
+
session: role.session,
|
|
46
|
+
model: effectiveRoleModel(role) ?? null,
|
|
47
|
+
...(role.effort ? { effort: role.effort } : {}),
|
|
48
|
+
...(mission ? { mission } : {}),
|
|
49
|
+
approval: role.permissions.approval,
|
|
50
|
+
filesystem: role.permissions.filesystem,
|
|
51
|
+
unattended: role.permissions.unattended,
|
|
52
|
+
permissionMode: origins.permissionMode,
|
|
53
|
+
monitor: { mode: role.monitor.mode, interrupt: role.monitor.interrupt },
|
|
54
|
+
...(role.isolation?.backend ? { isolation: {
|
|
55
|
+
requested: role.isolation.backend,
|
|
56
|
+
...(role.isolation.on_unavailable ? { on_unavailable: role.isolation.on_unavailable } : {}),
|
|
57
|
+
...(role.isolation.network ? { network: role.isolation.network } : {}),
|
|
58
|
+
...(fs?.read?.length ? { read_mounts: fs.read.length } : {}),
|
|
59
|
+
...(fs?.write?.length ? { write_mounts: fs.write.length } : {}),
|
|
60
|
+
} } : {}),
|
|
61
|
+
};
|
|
62
|
+
// A configuration whose complete mandatory rendering cannot fit must never
|
|
63
|
+
// be produced; real resolved values sit far below the budget, so this is a
|
|
64
|
+
// capture defect, not an expected path.
|
|
65
|
+
if (!mandatoryConfigurationFits(configuration))
|
|
66
|
+
throw new Error('agent launch presentation exceeds the mandatory rendering budget');
|
|
67
|
+
return configuration;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Component budget for one rendered agent line. The complete mandatory
|
|
71
|
+
* rendering must always fit — that is a validity condition of a current-v1
|
|
72
|
+
* configuration, enforced by mandatoryConfigurationFits at the builder AND
|
|
73
|
+
* the wire boundary — while optional components (monitor, isolation) are
|
|
74
|
+
* dropped whole with a visible `…` marker past this budget.
|
|
75
|
+
*/
|
|
76
|
+
export const AGENT_LINE_MAX_CODE_POINTS = 1_500;
|
|
77
|
+
export const AGENT_LINE_MAX_BYTES = 5_000;
|
|
78
|
+
const codePoints = (value) => Array.from(value).length;
|
|
79
|
+
const utf8 = (value) => Buffer.byteLength(value, 'utf8');
|
|
80
|
+
const OPTIONAL_OMISSION_SUFFIX = '; …';
|
|
81
|
+
function selectionComponent(kind, value) {
|
|
82
|
+
if (value.kind === 'named')
|
|
83
|
+
return `${kind} preset ${markdownCode(value.ref)}`;
|
|
84
|
+
if (value.kind === 'inline')
|
|
85
|
+
return `inline ${kind}${value.fingerprint ? ` (def ${markdownCode(value.fingerprint)})` : ''}`;
|
|
86
|
+
return `${kind} unknown`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The complete mandatory rendering: template, origins, mission, harness,
|
|
90
|
+
* model, effort, portable policy triple, and fleet/native permission mode.
|
|
91
|
+
* These must never be omitted from an operator-facing line.
|
|
92
|
+
*/
|
|
93
|
+
function mandatoryComponents(configuration) {
|
|
94
|
+
return [
|
|
95
|
+
configuration.template ? `template ${markdownCode(configuration.template)}` : undefined,
|
|
96
|
+
selectionComponent('Role', configuration.role),
|
|
97
|
+
configuration.mission ? `mission “${markdownProse(configuration.mission)}”` : undefined,
|
|
98
|
+
selectionComponent('Brain', configuration.brain),
|
|
99
|
+
`harness ${markdownCode(configuration.harness)}`,
|
|
100
|
+
`model ${configuration.model === null ? 'harness-default' : markdownCode(configuration.model)}`,
|
|
101
|
+
configuration.effort ? `effort ${markdownProse(configuration.effort)}` : undefined,
|
|
102
|
+
`approval=${configuration.approval}, filesystem=${configuration.filesystem}, `
|
|
103
|
+
+ `unattended=${configuration.unattended}`,
|
|
104
|
+
`mode ${configuration.permissionMode.fleetMode}/${markdownProse(configuration.permissionMode.nativeMode)}`,
|
|
105
|
+
].filter((part) => Boolean(part));
|
|
106
|
+
}
|
|
107
|
+
function optionalComponents(configuration) {
|
|
108
|
+
return [
|
|
109
|
+
`monitor ${configuration.monitor.mode}/${String(configuration.monitor.interrupt)}`,
|
|
110
|
+
configuration.isolation
|
|
111
|
+
? `isolation requested ${configuration.isolation.requested}`
|
|
112
|
+
+ `${configuration.isolation.network ? `, net ${configuration.isolation.network}` : ''}`
|
|
113
|
+
+ `${configuration.isolation.on_unavailable ? `, on-unavailable ${configuration.isolation.on_unavailable}` : ''}`
|
|
114
|
+
+ `${configuration.isolation.read_mounts || configuration.isolation.write_mounts
|
|
115
|
+
? `, mounts +${configuration.isolation.read_mounts ?? 0}ro/+${configuration.isolation.write_mounts ?? 0}rw` : ''}`
|
|
116
|
+
: undefined,
|
|
117
|
+
].filter((part) => Boolean(part));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Validity condition of a current-v1 configuration: its complete mandatory
|
|
121
|
+
* rendering fits the per-line budget with the optional-omission suffix
|
|
122
|
+
* reserved. Escaping and code-fence growth (a value made of backticks can
|
|
123
|
+
* more than triple its code span) make this depend on rendered size, not raw
|
|
124
|
+
* field lengths, so both summarizeResolvedLaunch and the wire validator call
|
|
125
|
+
* this instead of trusting per-field caps.
|
|
126
|
+
*/
|
|
127
|
+
export function mandatoryConfigurationFits(configuration) {
|
|
128
|
+
const reserved = `${mandatoryComponents(configuration).join('; ')}${OPTIONAL_OMISSION_SUFFIX}`;
|
|
129
|
+
return codePoints(reserved) <= AGENT_LINE_MAX_CODE_POINTS && utf8(reserved) <= AGENT_LINE_MAX_BYTES;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Render one agent's configuration as a single escaped Markdown line segment.
|
|
133
|
+
* This is the only escaping boundary: callers pass raw values and must not
|
|
134
|
+
* escape the result again (it is safe as a markdownItems entry and inside
|
|
135
|
+
* owner-channel lifecycle messages). Every mandatory component always
|
|
136
|
+
* renders — mandatoryConfigurationFits guarantees the fit for every produced
|
|
137
|
+
* or wire-accepted configuration; only optional components may be dropped,
|
|
138
|
+
* whole (never splitting a Markdown span), with a visible `…`.
|
|
139
|
+
*/
|
|
140
|
+
export function renderAgentConfiguration(configuration, fallback) {
|
|
141
|
+
if (!configuration) {
|
|
142
|
+
const labels = [
|
|
143
|
+
fallback?.role ? `Role ${markdownProse(fallback.role)}` : undefined,
|
|
144
|
+
fallback?.brain ? `Brain ${markdownProse(fallback.brain)}` : undefined,
|
|
145
|
+
fallback?.permissions ? `permissions ${markdownProse(fallback.permissions)}` : undefined,
|
|
146
|
+
].filter(Boolean).join('; ');
|
|
147
|
+
return `legacy launch; resolved details unavailable${labels ? ` (${labels})` : ''}`;
|
|
148
|
+
}
|
|
149
|
+
let line = mandatoryComponents(configuration).join('; ');
|
|
150
|
+
let omitted = false;
|
|
151
|
+
for (const part of optionalComponents(configuration)) {
|
|
152
|
+
const next = `${line}; ${part}`;
|
|
153
|
+
if (codePoints(`${next}${OPTIONAL_OMISSION_SUFFIX}`) > AGENT_LINE_MAX_CODE_POINTS
|
|
154
|
+
|| utf8(`${next}${OPTIONAL_OMISSION_SUFFIX}`) > AGENT_LINE_MAX_BYTES) {
|
|
155
|
+
omitted = true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
line = next;
|
|
159
|
+
}
|
|
160
|
+
return omitted ? `${line}${OPTIONAL_OMISSION_SUFFIX}` : line;
|
|
161
|
+
}
|
|
@@ -284,6 +284,8 @@ export declare class OwnerChannel implements OwnerChannelHandle {
|
|
|
284
284
|
private recoverRoomFromOwner;
|
|
285
285
|
/** Carry a task terminal request through a worker that survives this role. */
|
|
286
286
|
private recoverTaskFromOwner;
|
|
287
|
+
/** Accept a permanent any-state deletion, acknowledge it, then hand cleanup to a durable worker. */
|
|
288
|
+
private deleteTaskFromOwner;
|
|
287
289
|
private terminalTaskFromOwner;
|
|
288
290
|
/** Code-point-safe tail of the worklog, or undefined when there is none. */
|
|
289
291
|
private readWorklogTail;
|
|
@@ -270,7 +270,8 @@ export class OwnerChannel {
|
|
|
270
270
|
lifetime: event.lifetime, brain: event.brainSummary, role: event.roleSummary,
|
|
271
271
|
harness: event.harness, session: event.session, model: event.model,
|
|
272
272
|
permissions: permission, parent: event.caller, actionId: event.creationActionId,
|
|
273
|
-
inherited: event.inherited
|
|
273
|
+
inherited: event.inherited,
|
|
274
|
+
...(event.configuration ? { configuration: event.configuration } : {}) }), `fleet-spawn\0${event.creationActionId}`, 0);
|
|
274
275
|
});
|
|
275
276
|
}
|
|
276
277
|
beginFleetCommandAudit(requestId, argv) {
|
|
@@ -1269,9 +1270,7 @@ export class OwnerChannel {
|
|
|
1269
1270
|
reviewTask: taskId => new TaskRoomApplicationService(this.options.configPath).reviewTask({
|
|
1270
1271
|
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
|
|
1271
1272
|
}),
|
|
1272
|
-
deleteTask: taskId =>
|
|
1273
|
-
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
|
|
1274
|
-
}),
|
|
1273
|
+
deleteTask: taskId => this.deleteTaskFromOwner(sender, taskId, wireId),
|
|
1275
1274
|
listRoomQueries: filter => new TaskRoomApplicationService(this.options.configPath).listRooms(filter),
|
|
1276
1275
|
getRoomQuery: id => new TaskRoomApplicationService(this.options.configPath).getRoomDetail(id),
|
|
1277
1276
|
listTemplateQueries: () => new TaskRoomApplicationService(this.options.configPath).listTemplates(),
|
|
@@ -1401,10 +1400,13 @@ export class OwnerChannel {
|
|
|
1401
1400
|
const app = new TaskRoomApplicationService(this.options.configPath);
|
|
1402
1401
|
const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
|
|
1403
1402
|
const begin = await app.beginTaskRecovery({ actor, taskId });
|
|
1404
|
-
if (begin.kind
|
|
1403
|
+
if (begin.kind !== 'final') {
|
|
1404
|
+
const deleting = begin.kind === 'deletion_worker_required';
|
|
1405
1405
|
await this.send(sender.id, renderMarkdownFailure({
|
|
1406
1406
|
kind: 'pending', subject: `/task recover ${taskId}`,
|
|
1407
|
-
detail:
|
|
1407
|
+
detail: deleting
|
|
1408
|
+
? 'The task is pending deletion; recovery continues its cleanup.'
|
|
1409
|
+
: 'The recovery request was accepted and is still being settled.',
|
|
1408
1410
|
action: `Run /task recover ${taskId} if it remains pending.`,
|
|
1409
1411
|
}), wireId);
|
|
1410
1412
|
this.state.remember(wireId);
|
|
@@ -1412,9 +1414,13 @@ export class OwnerChannel {
|
|
|
1412
1414
|
await this.fleetOps.recoverTask(taskId);
|
|
1413
1415
|
}
|
|
1414
1416
|
catch (error) {
|
|
1415
|
-
|
|
1416
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1417
|
-
recoveryHint:
|
|
1417
|
+
const failure = {
|
|
1418
|
+
actor, taskId, error: error instanceof Error ? error.message : String(error),
|
|
1419
|
+
recoveryHint: deleting
|
|
1420
|
+
? `External delete worker failed to start. Retry /task delete ${taskId} ${taskId}.`
|
|
1421
|
+
: `External settle worker failed to start. Retry /task recover ${taskId}.`,
|
|
1422
|
+
};
|
|
1423
|
+
await (deleting ? app.recordDeletionError(failure) : app.recordSettlementError(failure));
|
|
1418
1424
|
throw error;
|
|
1419
1425
|
}
|
|
1420
1426
|
return;
|
|
@@ -1439,6 +1445,36 @@ export class OwnerChannel {
|
|
|
1439
1445
|
}), wireId);
|
|
1440
1446
|
this.state.remember(wireId);
|
|
1441
1447
|
}
|
|
1448
|
+
/** Accept a permanent any-state deletion, acknowledge it, then hand cleanup to a durable worker. */
|
|
1449
|
+
async deleteTaskFromOwner(sender, taskId, wireId) {
|
|
1450
|
+
const app = new TaskRoomApplicationService(this.options.configPath);
|
|
1451
|
+
const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
|
|
1452
|
+
const accepted = await app.requestTaskDeletion({ actor, taskId });
|
|
1453
|
+
if (accepted.status === 'already_absent') {
|
|
1454
|
+
await this.send(sender.id, renderMarkdownResult({
|
|
1455
|
+
icon: '🗑️', title: 'Task already absent',
|
|
1456
|
+
fields: [{ label: 'ID', value: taskId, kind: 'code' }],
|
|
1457
|
+
}), wireId);
|
|
1458
|
+
this.state.remember(wireId);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
await this.send(sender.id, renderMarkdownFailure({
|
|
1462
|
+
kind: 'pending', subject: `/task delete ${taskId} ${taskId}`,
|
|
1463
|
+
detail: 'The deletion was accepted; cleanup is settling in the background.',
|
|
1464
|
+
action: `Run /task recover ${taskId} or repeat /task delete ${taskId} ${taskId} if it remains pending.`,
|
|
1465
|
+
}), wireId);
|
|
1466
|
+
this.state.remember(wireId);
|
|
1467
|
+
try {
|
|
1468
|
+
await this.fleetOps.settleTaskDeletion(taskId);
|
|
1469
|
+
}
|
|
1470
|
+
catch (error) {
|
|
1471
|
+
await app.recordDeletionError({
|
|
1472
|
+
actor, taskId, error: error instanceof Error ? error.message : String(error),
|
|
1473
|
+
recoveryHint: `External delete worker failed to start. Retry /task delete ${taskId} ${taskId}.`,
|
|
1474
|
+
});
|
|
1475
|
+
throw error;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1442
1478
|
async terminalTaskFromOwner(sender, taskId, kind, outcome, wireId) {
|
|
1443
1479
|
const app = new TaskRoomApplicationService(this.options.configPath);
|
|
1444
1480
|
const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
|