@ours.network/fleet 1.1.0-nightly.9 → 1.1.0
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 +231 -46
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +7 -4
- 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 +8 -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 +92 -39
- package/dist/application/task-room-service.js +562 -289
- package/dist/briefing.js +79 -22
- package/dist/build-info.json +5 -5
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +129 -47
- package/dist/config.d.ts +38 -3
- package/dist/config.js +193 -28
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +181 -41
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +40 -4
- package/dist/fleet-command-audit.d.ts +22 -1
- package/dist/fleet-command-audit.js +355 -50
- package/dist/fleet-proxy.d.ts +5 -2
- package/dist/harness/claude-code-session.js +2 -1
- 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 +9 -2
- package/dist/harness/codex-session.js +18 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +104 -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 +1 -1
- 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 +19 -3
- package/dist/owner-channel/channel.js +301 -120
- package/dist/owner-channel/commands.d.ts +28 -6
- package/dist/owner-channel/commands.js +109 -27
- 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/permissions.js +1 -1
- 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 +361 -285
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +23 -7
- package/dist/rooms-tasks/config.js +16 -25
- 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 +8 -0
- package/dist/rooms-tasks/external-worker.js +38 -0
- 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 -83
- 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 -1
- package/dist/rooms-tasks/templates.js +34 -3
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +10 -4
- package/dist/rooms-tasks/types.d.ts +92 -7
- package/dist/rooms-tasks/types.js +14 -1
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +91 -44
- package/dist/session/acp.d.ts +4 -1
- package/dist/session/acp.js +29 -13
- 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 +1 -1
- package/dist/session/control.js +17 -1
- 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/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +25 -7
- 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 +4 -4
- package/presets/brain-catalog.json +17 -0
- package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
- package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
- package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
- 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 +37 -0
- package/presets/fleet/roles/Critic.yaml +23 -1
- package/presets/fleet/roles/Developer.yaml +27 -5
- package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
- package/presets/fleet/room_templates/pair.yaml +9 -7
- package/presets/fleet/room_templates/single.yaml +5 -4
- package/presets/fleet/room_templates/team.yaml +12 -8
- package/presets/manifest.json +2 -1
- package/presets/fleet/agents/Architect.yaml +0 -3
- package/presets/fleet/agents/Secretary.yaml +0 -3
- package/presets/fleet/agents/Tester.yaml +0 -3
- package/presets/fleet/roles/Agent.yaml +0 -6
- package/presets/fleet/roles/Architect.yaml +0 -6
- package/presets/fleet/roles/Secretary.yaml +0 -6
- package/presets/fleet/roles/Tester.yaml +0 -6
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync, realpathSync
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
import { parse } from 'yaml';
|
|
4
5
|
import { advanceSaga, setSagaError, updateMemberSeats, updateMemberStartup, activateRoom, getRoomRecord, } from './room-state.js';
|
|
5
6
|
import { activateTask, updateTaskMembers, blockTask, unblockTask, getTask, } from './task-state.js';
|
|
7
|
+
import { storedRoomLaunchPolicy } from './types.js';
|
|
6
8
|
import { spawnTemp } from '../spawn.js';
|
|
7
|
-
import {
|
|
9
|
+
import { effectivePermissionMode } from '../permissions.js';
|
|
10
|
+
import { selectionOrigin, summarizeResolvedLaunch, } from '../lifecycle-summary.js';
|
|
11
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
12
|
+
import { readLaunchSnapshot, redactLaunchDefinition } from './launch-snapshot.js';
|
|
8
13
|
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
|
|
9
14
|
import { controlRequest } from '../session/control.js';
|
|
10
15
|
import { SessionControlError } from '../session/types.js';
|
|
11
16
|
import { closeManagedRoom } from './close.js';
|
|
17
|
+
import { withFileLock } from '../atomic-file.js';
|
|
18
|
+
import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
|
|
19
|
+
import { TaskStateError, taskDeletionState } from './task-state.js';
|
|
12
20
|
import { buildRoomMemberTask, sha256Text } from './member-startup.js';
|
|
13
21
|
import { agentDir } from '../paths.js';
|
|
14
22
|
import { readProvenance } from '../creation.js';
|
|
15
23
|
import { readTempSupervisor, secureStoppedTempArchive, tempArchiveForCreationAction, tempSupervisorLiveness, } from '../temp-lifecycle.js';
|
|
16
|
-
import { recordFleetAuditPresentation } from '../fleet-command-audit.js';
|
|
17
24
|
export function getBinPath() {
|
|
18
25
|
try {
|
|
19
26
|
return realpathSync(process.argv[1]);
|
|
@@ -22,42 +29,9 @@ export function getBinPath() {
|
|
|
22
29
|
return process.argv[1];
|
|
23
30
|
}
|
|
24
31
|
}
|
|
25
|
-
function canonicalJson(value) {
|
|
26
|
-
if (Array.isArray(value))
|
|
27
|
-
return `[${value.map(canonicalJson).join(',')}]`;
|
|
28
|
-
if (value && typeof value === 'object')
|
|
29
|
-
return `{${Object.entries(value)
|
|
30
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
31
|
-
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(',')}}`;
|
|
32
|
-
return JSON.stringify(value);
|
|
33
|
-
}
|
|
34
|
-
function selectionSummary(selection) {
|
|
35
|
-
if ('ref' in selection)
|
|
36
|
-
return `ref:${selection.ref} (reference)`;
|
|
37
|
-
return `inline:sha256:${createHash('sha256').update(canonicalJson(selection.inline)).digest('hex').slice(0, 16)} (inline)`;
|
|
38
|
-
}
|
|
39
|
-
function permissionSummary(definition) {
|
|
40
|
-
const permissions = definition.permissions;
|
|
41
|
-
if (!permissions)
|
|
42
|
-
return undefined;
|
|
43
|
-
return `approval=${permissions.approval},filesystem=${permissions.filesystem},unattended=${permissions.unattended}`;
|
|
44
|
-
}
|
|
45
32
|
function launchDefinition(definition) {
|
|
46
|
-
const selectionEvidence = (selection) => 'ref' in selection ? { kind: 'ref', id: selection.ref } : {
|
|
47
|
-
kind: 'inline', fingerprint: `sha256:${createHash('sha256')
|
|
48
|
-
.update(canonicalJson(selection.inline)).digest('hex').slice(0, 16)}`,
|
|
49
|
-
};
|
|
50
33
|
return {
|
|
51
|
-
projection:
|
|
52
|
-
brain: selectionEvidence(definition.brain), role: selectionEvidence(definition.role),
|
|
53
|
-
...(definition.permissions ? { permissions: {
|
|
54
|
-
approval: definition.permissions.approval,
|
|
55
|
-
filesystem: definition.permissions.filesystem,
|
|
56
|
-
unattended: definition.permissions.unattended,
|
|
57
|
-
} } : {}),
|
|
58
|
-
operationalFields: Object.keys(definition)
|
|
59
|
-
.filter(key => key !== 'brain' && key !== 'role').sort(),
|
|
60
|
-
},
|
|
34
|
+
projection: redactLaunchDefinition(definition),
|
|
61
35
|
fingerprint: createHash('sha256').update(canonicalJson(definition)).digest('hex'),
|
|
62
36
|
};
|
|
63
37
|
}
|
|
@@ -81,8 +55,27 @@ async function spawnRoomMember(options, binPath) {
|
|
|
81
55
|
statePath: result.statePath,
|
|
82
56
|
creationActionId: result.creationActionId,
|
|
83
57
|
callerRole: result.caller,
|
|
58
|
+
...(result.configuration ? { configuration: result.configuration } : {}),
|
|
84
59
|
};
|
|
85
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Launch-boundary capture: the exact ResolvedRole the spawn persisted,
|
|
63
|
+
* whitelisted into the operator-facing presentation. Capture is required for
|
|
64
|
+
* every launched room member — launchMatches has already proved role.yaml
|
|
65
|
+
* readable, and effectivePermissionMode is total over registered harnesses —
|
|
66
|
+
* so a failure here is a capture defect and must surface, not degrade to the
|
|
67
|
+
* legacy rendering.
|
|
68
|
+
*/
|
|
69
|
+
function presentationFromStatePath(statePath, settings, coworkRole) {
|
|
70
|
+
const resolved = parse(readFileSync(join(statePath, 'role.yaml'), 'utf8'));
|
|
71
|
+
return summarizeResolvedLaunch(resolved, {
|
|
72
|
+
role: selectionOrigin(settings.definition.role),
|
|
73
|
+
brain: selectionOrigin(settings.definition.brain),
|
|
74
|
+
template: settings.template,
|
|
75
|
+
permissionMode: effectivePermissionMode(resolved),
|
|
76
|
+
missionFallback: coworkRole,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
86
79
|
function shortId(id) { return id.slice(0, 8); }
|
|
87
80
|
function expandMembers(template, prefix) {
|
|
88
81
|
const result = [];
|
|
@@ -92,39 +85,41 @@ function expandMembers(template, prefix) {
|
|
|
92
85
|
name: `${prefix}-${slot.slot}-${i}`,
|
|
93
86
|
slot: slot.slot,
|
|
94
87
|
coworkRole: slot.role,
|
|
95
|
-
|
|
88
|
+
agentTemplate: slot.agent_template,
|
|
89
|
+
launchDefinitionId: slot.launch_definition_id ?? slot.agent_template,
|
|
90
|
+
agentTemplateHash: slot.agent_template_hash,
|
|
91
|
+
loopSource: slot.loop_source ?? 'omitted',
|
|
96
92
|
});
|
|
97
93
|
}
|
|
98
94
|
}
|
|
99
95
|
return result;
|
|
100
96
|
}
|
|
101
|
-
function settingsFor(member, cfg) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
loadConfig(cfg.files[0], { additionalAgent: {
|
|
106
|
-
id: `RoomPreflight${member.name.replace(/[^A-Za-z0-9_-]/g, '')}`.slice(0, 64),
|
|
107
|
-
definition: member.agent,
|
|
108
|
-
} });
|
|
109
|
-
return { definition: structuredClone(member.agent),
|
|
110
|
-
...('inline' in role && typeof role.inline.persona === 'string'
|
|
111
|
-
? { persona: role.inline.persona } : {}) };
|
|
112
|
-
}
|
|
113
|
-
const refRole = findRole(cfg, member.agent.ref);
|
|
114
|
-
const definition = cfg.agentDefinitions?.[member.agent.ref];
|
|
97
|
+
function settingsFor(member, cfg, sealed) {
|
|
98
|
+
const definition = sealed
|
|
99
|
+
? sealed[member.launchDefinitionId]
|
|
100
|
+
: cfg.agentTemplates?.[member.agentTemplate];
|
|
115
101
|
if (!definition)
|
|
116
|
-
throw new Error(
|
|
102
|
+
throw new Error(sealed
|
|
103
|
+
? `Sealed Agent definition '${member.launchDefinitionId}' not found`
|
|
104
|
+
: `Agent Template '${member.agentTemplate}' not found`);
|
|
105
|
+
const role = definition.role;
|
|
117
106
|
return {
|
|
118
107
|
definition: structuredClone(definition),
|
|
119
|
-
|
|
108
|
+
...('inline' in role && typeof role.inline.persona === 'string'
|
|
109
|
+
? { persona: role.inline.persona } : {}),
|
|
110
|
+
template: member.agentTemplate,
|
|
111
|
+
templateHash: member.agentTemplateHash
|
|
112
|
+
?? createHash('sha256').update(canonicalJson(definition)).digest('hex'),
|
|
113
|
+
loopSource: member.loopSource,
|
|
120
114
|
};
|
|
121
115
|
}
|
|
122
|
-
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid) {
|
|
116
|
+
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid, anonymous) {
|
|
123
117
|
return buildRoomMemberTask({
|
|
124
118
|
taskId: input.taskId,
|
|
125
119
|
roomId: input.roomId,
|
|
126
120
|
roomIdentityCid,
|
|
127
121
|
ownerSeatCid,
|
|
122
|
+
anonymous,
|
|
128
123
|
goal: input.goal,
|
|
129
124
|
brief: input.brief,
|
|
130
125
|
contract: input.template.contract,
|
|
@@ -139,7 +134,7 @@ function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCi
|
|
|
139
134
|
})),
|
|
140
135
|
});
|
|
141
136
|
}
|
|
142
|
-
function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid, expectedInviteId) {
|
|
137
|
+
function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid, expectedInviteId, anonymous = false) {
|
|
143
138
|
const provenance = readProvenance(dir);
|
|
144
139
|
if (provenance?.creationActionId !== actionId || provenance.role !== member.name)
|
|
145
140
|
return false;
|
|
@@ -151,6 +146,7 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
|
|
|
151
146
|
&& startup.room_identity_cid === roomIdentityCid
|
|
152
147
|
&& startup.identity_name === member.name
|
|
153
148
|
&& startup.role === member.coworkRole
|
|
149
|
+
&& (startup.anonymous ?? false) === anonymous
|
|
154
150
|
&& sha256Text(startup.task ?? '') === taskSha
|
|
155
151
|
&& (expectedInviteId === undefined || startup.invite_id === expectedInviteId)
|
|
156
152
|
&& typeof startup.invite === 'string'
|
|
@@ -161,21 +157,22 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
|
|
|
161
157
|
}
|
|
162
158
|
}
|
|
163
159
|
async function retainRunningLaunch(input) {
|
|
164
|
-
const { provision, member, task, roomIdentityCid } = input;
|
|
160
|
+
const { provision, member, settings, task, roomIdentityCid } = input;
|
|
165
161
|
let seat = getRoomRecord(provision.roomId).member_seats
|
|
166
162
|
.find(candidate => candidate.role_name === member.name);
|
|
167
163
|
const dir = agentDir(member.name, true);
|
|
168
164
|
const taskSha = sha256Text(task);
|
|
165
|
+
const anonymous = storedRoomLaunchPolicy(getRoomRecord(provision.roomId)?.room_policy).anonymous;
|
|
169
166
|
if ((seat.launch?.state === 'intent' || seat.launch?.state === 'launched'
|
|
170
167
|
|| seat.launch?.state === 'failed') && existsSync(dir)) {
|
|
171
|
-
if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
168
|
+
if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous)) {
|
|
172
169
|
const provenance = readProvenance(dir);
|
|
173
170
|
const adoptable = (seat.launch.state === 'intent' || seat.launch.state === 'failed')
|
|
174
171
|
&& Boolean(seat.launch.caller_role)
|
|
175
172
|
&& provenance?.surface === 'agent'
|
|
176
173
|
&& provenance.callerRole === seat.launch.caller_role
|
|
177
174
|
&& typeof provenance.creationActionId === 'string'
|
|
178
|
-
&& launchMatches(dir, member, provenance.creationActionId, taskSha, provision.roomId, roomIdentityCid, seat.invite_id);
|
|
175
|
+
&& launchMatches(dir, member, provenance.creationActionId, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous);
|
|
179
176
|
if (!adoptable)
|
|
180
177
|
throw new Error(`existing launch for ${member.name} does not match its durable intent`);
|
|
181
178
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
@@ -193,9 +190,14 @@ async function retainRunningLaunch(input) {
|
|
|
193
190
|
throw new Error(`existing launch for ${member.name} has unknown liveness`);
|
|
194
191
|
const retainedLaunch = seat.launch;
|
|
195
192
|
if (live === 'running') {
|
|
193
|
+
// A crash between spawn and the seat update (or a pre-upgrade launch)
|
|
194
|
+
// can retain a running member without a captured presentation; backfill
|
|
195
|
+
// it from the same persisted resolved role the match was proved against.
|
|
196
|
+
const presentation = retainedLaunch.presentation
|
|
197
|
+
?? presentationFromStatePath(dir, settings, member.coworkRole);
|
|
196
198
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
197
199
|
...retainedLaunch, state: 'launched', launch_id: supervisor.launchId,
|
|
198
|
-
updated_at: new Date().toISOString(),
|
|
200
|
+
presentation, updated_at: new Date().toISOString(),
|
|
199
201
|
} });
|
|
200
202
|
return true;
|
|
201
203
|
}
|
|
@@ -210,7 +212,7 @@ async function retainRunningLaunch(input) {
|
|
|
210
212
|
if (!seat.launch.launch_id || !seat.launch.action_id)
|
|
211
213
|
throw new Error(`missing durable launch identity for disappeared ${member.name}`);
|
|
212
214
|
const archive = await secureStoppedTempArchive(member.name, seat.launch.launch_id);
|
|
213
|
-
if (!launchMatches(archive, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
215
|
+
if (!launchMatches(archive, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous)) {
|
|
214
216
|
throw new Error(`archive for disappeared ${member.name} does not match its durable intent`);
|
|
215
217
|
}
|
|
216
218
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
@@ -222,7 +224,7 @@ async function retainRunningLaunch(input) {
|
|
|
222
224
|
if (!seat.launch.action_id)
|
|
223
225
|
throw new Error(`missing action ID for disappeared launch intent ${member.name}`);
|
|
224
226
|
const archive = tempArchiveForCreationAction(member.name, seat.launch.action_id);
|
|
225
|
-
if (!archive || !launchMatches(archive.path, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
227
|
+
if (!archive || !launchMatches(archive.path, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous)) {
|
|
226
228
|
throw new Error(`launch intent for ${member.name} has no exact live or terminated archive evidence`);
|
|
227
229
|
}
|
|
228
230
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
@@ -232,7 +234,25 @@ async function retainRunningLaunch(input) {
|
|
|
232
234
|
}
|
|
233
235
|
return false;
|
|
234
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Member-launch publication window: for a task-bound room, the durable launch
|
|
239
|
+
* intent, spawn, and launch record publish under the common task-operation
|
|
240
|
+
* lock so an accepted deletion linearizes strictly before (bounded abort — no
|
|
241
|
+
* spawn happens) or strictly after (the seat's launch record is visible to
|
|
242
|
+
* deletion retirement). The lock is held per member only, never across
|
|
243
|
+
* seat-wait loops.
|
|
244
|
+
*/
|
|
235
245
|
async function launchMember(input) {
|
|
246
|
+
const taskId = input.provision.taskId;
|
|
247
|
+
if (!taskId)
|
|
248
|
+
return launchMemberUnlocked(input);
|
|
249
|
+
return withFileLock(taskOperationLockPath(taskId), () => {
|
|
250
|
+
if (taskDeletionState(taskId) !== 'none')
|
|
251
|
+
throw new Error(`task ${taskId} is pending deletion; aborting member launch for ${input.member.name}`);
|
|
252
|
+
return launchMemberUnlocked(input);
|
|
253
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
254
|
+
}
|
|
255
|
+
async function launchMemberUnlocked(input) {
|
|
236
256
|
const { provision, member, settings, startup } = input;
|
|
237
257
|
const seat = getRoomRecord(provision.roomId).member_seats
|
|
238
258
|
.find(candidate => candidate.role_name === member.name);
|
|
@@ -247,6 +267,7 @@ async function launchMember(input) {
|
|
|
247
267
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
248
268
|
state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
|
|
249
269
|
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
270
|
+
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
250
271
|
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
251
272
|
updated_at: new Date().toISOString(),
|
|
252
273
|
} });
|
|
@@ -256,6 +277,9 @@ async function launchMember(input) {
|
|
|
256
277
|
temp: true,
|
|
257
278
|
identity: member.name,
|
|
258
279
|
agentDefinition: settings.definition,
|
|
280
|
+
...(settings.loopSource === 'cli' && settings.definition.loops === undefined
|
|
281
|
+
? { noLoops: true } : {}),
|
|
282
|
+
loopSource: settings.loopSource,
|
|
259
283
|
surface: 'agent',
|
|
260
284
|
creationActionId: actionId,
|
|
261
285
|
roomMemberStartup: startup,
|
|
@@ -266,36 +290,38 @@ async function launchMember(input) {
|
|
|
266
290
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
267
291
|
state: 'intent', attempt, action_id: launched.creationActionId,
|
|
268
292
|
mission_sha256: taskSha, agent_definition: agentDefinition,
|
|
269
|
-
agent_fingerprint: agentFingerprint,
|
|
293
|
+
agent_fingerprint: agentFingerprint,
|
|
294
|
+
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
295
|
+
updated_at: new Date().toISOString(),
|
|
270
296
|
...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
|
|
271
297
|
} });
|
|
272
298
|
}
|
|
273
299
|
const supervisor = readTempSupervisor(launchedDir);
|
|
274
|
-
if (!supervisor || supervisor.role !== member.name || !launchMatches(launchedDir, member, launched.creationActionId, taskSha, provision.roomId, startup.room_identity_cid, startup.invite_id)) {
|
|
300
|
+
if (!supervisor || supervisor.role !== member.name || !launchMatches(launchedDir, member, launched.creationActionId, taskSha, provision.roomId, startup.room_identity_cid, startup.invite_id, startup.anonymous ?? false)) {
|
|
275
301
|
throw new Error(`new launch for ${member.name} did not persist matching provenance`);
|
|
276
302
|
}
|
|
303
|
+
const presentation = launched.configuration
|
|
304
|
+
? { ...launched.configuration, template: settings.template,
|
|
305
|
+
...(launched.configuration.mission ? {} : { mission: member.coworkRole }) }
|
|
306
|
+
: presentationFromStatePath(launchedDir, settings, member.coworkRole);
|
|
277
307
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
278
308
|
state: 'launched', attempt, action_id: launched.creationActionId, mission_sha256: taskSha,
|
|
279
309
|
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
310
|
+
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
311
|
+
presentation,
|
|
280
312
|
...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
|
|
281
313
|
launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
|
|
282
314
|
} });
|
|
283
|
-
recordFleetAuditPresentation({ kind: 'agent_started', id: member.name, name: member.name,
|
|
284
|
-
lifetime: 'temporary', brain: selectionSummary(settings.definition.brain),
|
|
285
|
-
role: selectionSummary(settings.definition.role), harness: 'resolved', session: 'acp',
|
|
286
|
-
permissions: permissionSummary(settings.definition), parent: provision.roomId,
|
|
287
|
-
actionId: launched.creationActionId, inherited: [] });
|
|
288
315
|
}
|
|
289
316
|
catch (error) {
|
|
290
317
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
291
318
|
state: 'failed', attempt, action_id: effectiveActionId, mission_sha256: taskSha,
|
|
292
319
|
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
320
|
+
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
293
321
|
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
294
322
|
updated_at: new Date().toISOString(),
|
|
295
323
|
error: error instanceof Error ? error.message : String(error),
|
|
296
324
|
} });
|
|
297
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Agent', id: member.name,
|
|
298
|
-
state: 'failed', category: 'readiness_failed', eventId: effectiveActionId });
|
|
299
325
|
throw error;
|
|
300
326
|
}
|
|
301
327
|
}
|
|
@@ -313,9 +339,10 @@ function exactCoworkSeat(observed, member, expectedInviteId) {
|
|
|
313
339
|
}
|
|
314
340
|
return seat;
|
|
315
341
|
}
|
|
316
|
-
function reconcileMemberSeats(roomId, members, observed) {
|
|
342
|
+
function reconcileMemberSeats(roomId, members, observed, ownerSeatCid) {
|
|
317
343
|
const current = getRoomRecord(roomId);
|
|
318
|
-
let complete =
|
|
344
|
+
let complete = !ownerSeatCid || observed.some(seat => seat.identity_cid.toLowerCase() === ownerSeatCid.toLowerCase()
|
|
345
|
+
&& seat.seat_state === 'active');
|
|
319
346
|
const seats = current.member_seats.map(seat => {
|
|
320
347
|
const member = members.find(candidate => candidate.name === seat.role_name);
|
|
321
348
|
const found = exactCoworkSeat(observed, member, seat.invite_id);
|
|
@@ -333,24 +360,35 @@ function reconcileMemberSeats(roomId, members, observed) {
|
|
|
333
360
|
updateMemberSeats(roomId, seats);
|
|
334
361
|
return { complete, seats };
|
|
335
362
|
}
|
|
363
|
+
function assertCoworkRoomPolicy(room, expectedAnonymous) {
|
|
364
|
+
if ((room.anonymous ?? false) !== expectedAnonymous)
|
|
365
|
+
throw new Error(`Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(expectedAnonymous)})`);
|
|
366
|
+
}
|
|
336
367
|
export async function provisionMembers(input) {
|
|
337
368
|
const { cfg, cowork, roomId, taskId, template } = input;
|
|
369
|
+
// Deletion-epoch pre-check; each member launch re-checks under the lock.
|
|
370
|
+
if (taskId && taskDeletionState(taskId) !== 'none')
|
|
371
|
+
throw new Error(`task ${taskId} is pending deletion; refusing to provision members`);
|
|
338
372
|
const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
|
|
339
373
|
const members = expandMembers(template, prefix);
|
|
340
374
|
// Resolve every Agent before persisting launch intent or touching Cowork membership.
|
|
341
|
-
const
|
|
375
|
+
const sealed = template.launch_snapshot_hash
|
|
376
|
+
? readLaunchSnapshot(template.launch_snapshot_hash) : undefined;
|
|
377
|
+
const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg, sealed)]));
|
|
342
378
|
const existing = getRoomRecord(roomId);
|
|
343
379
|
if (!existing?.room_identity_cid)
|
|
344
380
|
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
345
381
|
const roomIdentityCid = existing.room_identity_cid;
|
|
346
382
|
const ownerSeatCid = existing.owner_seat_cid ?? null;
|
|
383
|
+
const roomPolicy = storedRoomLaunchPolicy(existing.room_policy);
|
|
347
384
|
const persistedNames = new Set(existing.member_seats.map(seat => seat.role_name));
|
|
348
385
|
const resuming = members.length > 0
|
|
349
386
|
&& members.every(member => persistedNames.has(member.name));
|
|
350
387
|
if (!resuming) {
|
|
351
388
|
advanceSaga(roomId, 'create_members', 3);
|
|
352
389
|
updateMemberSeats(roomId, members.map(member => {
|
|
353
|
-
const
|
|
390
|
+
const memberSettings = settings.get(member.name);
|
|
391
|
+
const evidence = launchDefinition(memberSettings.definition);
|
|
354
392
|
return ({
|
|
355
393
|
role_name: member.name,
|
|
356
394
|
slot: member.slot,
|
|
@@ -358,6 +396,8 @@ export async function provisionMembers(input) {
|
|
|
358
396
|
seat_state: 'pending',
|
|
359
397
|
launch: { state: 'pending', attempt: 0,
|
|
360
398
|
agent_definition: evidence.projection, agent_fingerprint: evidence.fingerprint,
|
|
399
|
+
agent_template: memberSettings.template,
|
|
400
|
+
agent_template_hash: memberSettings.templateHash,
|
|
361
401
|
updated_at: new Date().toISOString() },
|
|
362
402
|
});
|
|
363
403
|
}));
|
|
@@ -370,7 +410,7 @@ export async function provisionMembers(input) {
|
|
|
370
410
|
throw new Error(`Agent definition drift for ${member.name}; durable launch intent does not match current configuration`);
|
|
371
411
|
}
|
|
372
412
|
}
|
|
373
|
-
const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid)]));
|
|
413
|
+
const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid, roomPolicy.anonymous)]));
|
|
374
414
|
const policy = {
|
|
375
415
|
timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
|
|
376
416
|
initialDelayMs: input.startupWait?.initialDelayMs ?? 250,
|
|
@@ -381,20 +421,23 @@ export async function provisionMembers(input) {
|
|
|
381
421
|
advanceSaga(roomId, 'join_role_groups', 4);
|
|
382
422
|
try {
|
|
383
423
|
const initialRoom = await cowork.recoverRoom(roomId);
|
|
384
|
-
|
|
424
|
+
assertCoworkRoomPolicy(initialRoom, roomPolicy.anonymous);
|
|
425
|
+
reconcileMemberSeats(roomId, members, initialRoom.seats, existing.owner_seat_cid);
|
|
385
426
|
for (const member of members) {
|
|
386
427
|
const task = tasks.get(member.name);
|
|
387
428
|
const currentSeat = getRoomRecord(roomId).member_seats
|
|
388
429
|
.find(seat => seat.role_name === member.name);
|
|
389
430
|
if (currentSeat.seat_state === 'active') {
|
|
390
431
|
if (!await retainRunningLaunch({
|
|
391
|
-
provision: input, member, task, roomIdentityCid,
|
|
432
|
+
provision: input, member, settings: settings.get(member.name), task, roomIdentityCid,
|
|
392
433
|
})) {
|
|
393
434
|
throw new Error(`active Cowork seat ${member.name} has no matching live Fleet launch`);
|
|
394
435
|
}
|
|
395
436
|
continue;
|
|
396
437
|
}
|
|
397
|
-
if (await retainRunningLaunch({
|
|
438
|
+
if (await retainRunningLaunch({
|
|
439
|
+
provision: input, member, settings: settings.get(member.name), task, roomIdentityCid,
|
|
440
|
+
}))
|
|
398
441
|
continue;
|
|
399
442
|
const issued = await cowork.issueInvite(roomId, {
|
|
400
443
|
mode: 'one_time', role: member.coworkRole, min_accepts: 1,
|
|
@@ -415,6 +458,7 @@ export async function provisionMembers(input) {
|
|
|
415
458
|
role: member.coworkRole,
|
|
416
459
|
task,
|
|
417
460
|
owner_seat_cid: ownerSeatCid,
|
|
461
|
+
anonymous: roomPolicy.anonymous,
|
|
418
462
|
},
|
|
419
463
|
});
|
|
420
464
|
}
|
|
@@ -426,9 +470,18 @@ export async function provisionMembers(input) {
|
|
|
426
470
|
}
|
|
427
471
|
catch (error) {
|
|
428
472
|
const reason = error instanceof Error ? error.message : String(error);
|
|
429
|
-
setSagaError(roomId, reason, 'Member invite or launch failed.
|
|
430
|
-
if (taskId)
|
|
431
|
-
|
|
473
|
+
setSagaError(roomId, reason, 'Member invite or launch failed. Inspect role logs, then retry with `task start`.', 'member_failed');
|
|
474
|
+
if (taskId) {
|
|
475
|
+
// A deletion-pending (or already-terminal) task rejects the block
|
|
476
|
+
// overlay; the original launch failure must still propagate.
|
|
477
|
+
try {
|
|
478
|
+
blockTask(taskId, reason);
|
|
479
|
+
}
|
|
480
|
+
catch (blockError) {
|
|
481
|
+
if (!(blockError instanceof TaskStateError))
|
|
482
|
+
throw blockError;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
432
485
|
throw error;
|
|
433
486
|
}
|
|
434
487
|
advanceSaga(roomId, 'wait_seats', 5);
|
|
@@ -436,7 +489,8 @@ export async function provisionMembers(input) {
|
|
|
436
489
|
let delay = policy.initialDelayMs;
|
|
437
490
|
for (;;) {
|
|
438
491
|
const remote = await cowork.recoverRoom(roomId);
|
|
439
|
-
|
|
492
|
+
assertCoworkRoomPolicy(remote, roomPolicy.anonymous);
|
|
493
|
+
const reconciled = reconcileMemberSeats(roomId, members, remote.seats, ownerSeatCid ?? undefined);
|
|
440
494
|
if (reconciled.complete)
|
|
441
495
|
break;
|
|
442
496
|
if (policy.now() >= deadline) {
|
|
@@ -447,6 +501,15 @@ export async function provisionMembers(input) {
|
|
|
447
501
|
await policy.sleep(delay);
|
|
448
502
|
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
449
503
|
}
|
|
504
|
+
// Admission alone is insufficient: every expected member must still have
|
|
505
|
+
// the exact live launch that owns its authenticated Cowork seat.
|
|
506
|
+
for (const member of members) {
|
|
507
|
+
if (!await retainRunningLaunch({
|
|
508
|
+
provision: input, member, settings: settings.get(member.name),
|
|
509
|
+
task: tasks.get(member.name), roomIdentityCid,
|
|
510
|
+
}))
|
|
511
|
+
throw new Error(`active Cowork seat ${member.name} has no matching live Fleet launch`);
|
|
512
|
+
}
|
|
450
513
|
if (taskId) {
|
|
451
514
|
const taskMembers = getRoomRecord(roomId).member_seats.map(seat => {
|
|
452
515
|
if (!seat.identity_cid)
|
|
@@ -9,6 +9,7 @@ export interface CreateRoomInput {
|
|
|
9
9
|
room_identity_cid?: string;
|
|
10
10
|
task_id?: string;
|
|
11
11
|
template_snapshot?: import('./types.js').TemplateSnapshot;
|
|
12
|
+
room_policy?: import('./types.js').RoomLaunchPolicy;
|
|
12
13
|
}
|
|
13
14
|
export declare function createRoomRecord(input: CreateRoomInput): RoomOrchestrationRecord;
|
|
14
15
|
export declare function getRoomRecord(id: string): RoomOrchestrationRecord | undefined;
|
|
@@ -2,6 +2,8 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:f
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
4
4
|
import { stateRoot } from '../paths.js';
|
|
5
|
+
import { storedRoomLaunchPolicy } from './types.js';
|
|
6
|
+
import { releaseLaunchSnapshot } from './launch-snapshot.js';
|
|
5
7
|
export const roomsDir = () => join(stateRoot(), 'rooms');
|
|
6
8
|
function roomPath(id) { return join(roomsDir(), `${id}.json`); }
|
|
7
9
|
export class RoomStateError extends Error {
|
|
@@ -18,8 +20,13 @@ function writeRoom(record) {
|
|
|
18
20
|
}
|
|
19
21
|
export function createRoomRecord(input) {
|
|
20
22
|
const existing = getRoomRecord(input.room_id);
|
|
21
|
-
if (existing)
|
|
23
|
+
if (existing) {
|
|
24
|
+
const before = storedRoomLaunchPolicy(existing.room_policy);
|
|
25
|
+
const requested = storedRoomLaunchPolicy(input.room_policy);
|
|
26
|
+
if (JSON.stringify(before) !== JSON.stringify(requested))
|
|
27
|
+
throw new RoomStateError(`room ${input.room_id} launch policy mismatch`);
|
|
22
28
|
return existing;
|
|
29
|
+
}
|
|
23
30
|
const record = {
|
|
24
31
|
room_id: input.room_id,
|
|
25
32
|
room_name: input.room_name,
|
|
@@ -27,6 +34,7 @@ export function createRoomRecord(input) {
|
|
|
27
34
|
room_identity_cid: input.room_identity_cid,
|
|
28
35
|
task_id: input.task_id,
|
|
29
36
|
template_snapshot: input.template_snapshot,
|
|
37
|
+
room_policy: input.room_policy,
|
|
30
38
|
saga: { phase: 'persist_intent', step_index: 0 },
|
|
31
39
|
member_seats: [],
|
|
32
40
|
state: 'provisioning',
|
|
@@ -46,8 +54,11 @@ export function getRoomRecord(id) {
|
|
|
46
54
|
/** Remove a terminal orchestration record from the live room inventory. */
|
|
47
55
|
export function deleteRoomRecord(id) {
|
|
48
56
|
const path = roomPath(id);
|
|
57
|
+
const hash = getRoomRecord(id)?.template_snapshot?.launch_snapshot_hash;
|
|
49
58
|
if (existsSync(path))
|
|
50
59
|
rmSync(path);
|
|
60
|
+
if (hash)
|
|
61
|
+
releaseLaunchSnapshot(hash);
|
|
51
62
|
}
|
|
52
63
|
export function listRoomRecords(filter) {
|
|
53
64
|
const dir = roomsDir();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cowork room identities are named `ours-cowork:<room name>` and both the
|
|
3
|
+
* room and identity contracts count NFC-normalized Unicode code points.
|
|
4
|
+
* Keeping the complete task ID inside that shared budget makes every derived
|
|
5
|
+
* name stable, directly correlated, and unique even when the title is empty,
|
|
6
|
+
* sanitized, or truncated.
|
|
7
|
+
*/
|
|
8
|
+
export declare const COWORK_ROOM_IDENTITY_PREFIX = "ours-cowork:";
|
|
9
|
+
export declare const COWORK_IDENTITY_NAME_MAX_CODE_POINTS = 64;
|
|
10
|
+
export declare const TASK_ROOM_NAME_MAX_CODE_POINTS: number;
|
|
11
|
+
/** Canonical Cowork room name for a Fleet task; never mutates the task title. */
|
|
12
|
+
export declare function deriveTaskRoomName(title: string, taskId: string): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cowork room identities are named `ours-cowork:<room name>` and both the
|
|
3
|
+
* room and identity contracts count NFC-normalized Unicode code points.
|
|
4
|
+
* Keeping the complete task ID inside that shared budget makes every derived
|
|
5
|
+
* name stable, directly correlated, and unique even when the title is empty,
|
|
6
|
+
* sanitized, or truncated.
|
|
7
|
+
*/
|
|
8
|
+
export const COWORK_ROOM_IDENTITY_PREFIX = 'ours-cowork:';
|
|
9
|
+
export const COWORK_IDENTITY_NAME_MAX_CODE_POINTS = 64;
|
|
10
|
+
export const TASK_ROOM_NAME_MAX_CODE_POINTS = COWORK_IDENTITY_NAME_MAX_CODE_POINTS
|
|
11
|
+
- Array.from(COWORK_ROOM_IDENTITY_PREFIX).length;
|
|
12
|
+
const TASK_ID_PATTERN = /^[0-9a-z]{9}[0-9a-f]{8}$/;
|
|
13
|
+
const FORBIDDEN = /[\\/\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u;
|
|
14
|
+
const DASH_SEPARATOR = /\p{Pd}/u;
|
|
15
|
+
const WHITESPACE = /\s/u;
|
|
16
|
+
const FALLBACK_TITLE = 'Task';
|
|
17
|
+
function sanitizeTitle(title) {
|
|
18
|
+
const normalized = title.normalize('NFC');
|
|
19
|
+
let result = '';
|
|
20
|
+
let pending;
|
|
21
|
+
for (const character of normalized) {
|
|
22
|
+
if (FORBIDDEN.test(character) || DASH_SEPARATOR.test(character)) {
|
|
23
|
+
pending = 'separator';
|
|
24
|
+
}
|
|
25
|
+
else if (WHITESPACE.test(character)) {
|
|
26
|
+
if (pending !== 'separator')
|
|
27
|
+
pending = 'space';
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
if (result && pending)
|
|
31
|
+
result += pending === 'separator' ? ' - ' : ' ';
|
|
32
|
+
result += character;
|
|
33
|
+
pending = undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
/** Truncate to a code-point budget without cutting an extended grapheme. */
|
|
39
|
+
function graphemeBound(value, maxCodePoints) {
|
|
40
|
+
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
41
|
+
let result = '';
|
|
42
|
+
let length = 0;
|
|
43
|
+
for (const { segment } of segmenter.segment(value)) {
|
|
44
|
+
const segmentLength = Array.from(segment).length;
|
|
45
|
+
if (length + segmentLength > maxCodePoints)
|
|
46
|
+
break;
|
|
47
|
+
result += segment;
|
|
48
|
+
length += segmentLength;
|
|
49
|
+
}
|
|
50
|
+
return result.trimEnd();
|
|
51
|
+
}
|
|
52
|
+
/** Canonical Cowork room name for a Fleet task; never mutates the task title. */
|
|
53
|
+
export function deriveTaskRoomName(title, taskId) {
|
|
54
|
+
if (!TASK_ID_PATTERN.test(taskId))
|
|
55
|
+
throw new Error(`invalid canonical task ID: ${taskId}`);
|
|
56
|
+
const suffix = ` [${taskId}]`;
|
|
57
|
+
const titleBudget = TASK_ROOM_NAME_MAX_CODE_POINTS - Array.from(suffix).length;
|
|
58
|
+
const readable = sanitizeTitle(title);
|
|
59
|
+
const bounded = graphemeBound(readable || FALLBACK_TITLE, titleBudget) || FALLBACK_TITLE;
|
|
60
|
+
return `${bounded}${suffix}`;
|
|
61
|
+
}
|