@ours.network/fleet 1.2.0-nightly.1 → 1.2.0-nightly.10
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 +95 -21
- package/dist/agent-ours/bridge.d.ts +3 -0
- package/dist/agent-ours/bridge.js +178 -0
- package/dist/agent-ours/controller.d.ts +14 -0
- package/dist/agent-ours/controller.js +60 -0
- package/dist/agent-ours/file-rpc.d.ts +23 -0
- package/dist/agent-ours/file-rpc.js +123 -0
- package/dist/agent-ours/harness.d.ts +14 -0
- package/dist/agent-ours/harness.js +64 -0
- package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
- package/dist/agent-ours/mcp-endpoint.js +111 -0
- package/dist/agent-ours/runtime.d.ts +53 -0
- package/dist/agent-ours/runtime.js +247 -0
- package/dist/agent-ours/service.d.ts +23 -0
- package/dist/agent-ours/service.js +364 -0
- package/dist/agent-ours/state.d.ts +45 -0
- package/dist/agent-ours/state.js +95 -0
- package/dist/agent-ours/wire.d.ts +14 -0
- package/dist/agent-ours/wire.js +55 -0
- package/dist/agent-recovery-gate.js +1 -1
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +121 -0
- package/dist/application/task-room-service.js +55 -19
- package/dist/briefing.js +10 -59
- package/dist/build-info.json +5 -4
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +63 -2
- package/dist/client-profile.d.ts +1 -0
- package/dist/client-profile.js +18 -3
- package/dist/config.d.ts +1 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +3 -14
- package/dist/docs.d.ts +3 -3
- package/dist/docs.js +32 -23
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +29 -10
- package/dist/fleet-command-audit.js +3 -2
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +4 -2
- package/dist/harness/codex-session.js +9 -2
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +4 -3
- package/dist/harness/hermes-session.js +7 -1
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +24 -4
- package/dist/rooms-tasks/archived-absence.d.ts +8 -0
- package/dist/rooms-tasks/archived-absence.js +48 -0
- package/dist/rooms-tasks/cli.js +1 -1
- package/dist/rooms-tasks/close.d.ts +3 -0
- package/dist/rooms-tasks/close.js +95 -17
- package/dist/rooms-tasks/cowork-adapter.d.ts +4 -0
- package/dist/rooms-tasks/cowork-adapter.js +10 -2
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- package/dist/rooms-tasks/deletion.js +40 -3
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +2 -0
- package/dist/rooms-tasks/provision.d.ts +11 -0
- package/dist/rooms-tasks/provision.js +138 -26
- package/dist/rooms-tasks/room-state.js +13 -0
- package/dist/rooms-tasks/task-state.d.ts +13 -3
- package/dist/rooms-tasks/task-state.js +42 -6
- package/dist/rooms-tasks/types.d.ts +12 -0
- package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
- package/dist/rooms-tasks/workspace-artifacts.js +93 -0
- package/dist/rooms-tasks/workspace.d.ts +26 -0
- package/dist/rooms-tasks/workspace.js +195 -0
- package/dist/runner.d.ts +14 -0
- package/dist/runner.js +527 -495
- package/dist/session/codex-app-server-transport.js +1 -1
- package/dist/spawn.js +19 -16
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/supervisor/systemd.js +5 -0
- package/dist/temp-supervisor-recovery.d.ts +3 -0
- package/dist/temp-supervisor-recovery.js +46 -0
- package/dist/watchdog/briefing.js +3 -16
- package/dist/watchdog/run.js +19 -5
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +18 -1
- package/docs/task-workspaces.md +98 -0
- package/package.json +6 -3
- package/presets/fleet/roles/Coordinator.yaml +22 -0
package/dist/doctor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { userInfo, homedir } from 'node:os';
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
4
|
import { attachOursClient, } from '@ours.network/sdk/client';
|
|
4
5
|
import { realExec } from './exec.js';
|
|
@@ -272,21 +273,39 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
272
273
|
}
|
|
273
274
|
catch { /* state dir may not exist yet */ }
|
|
274
275
|
}
|
|
276
|
+
let managedSelection = false;
|
|
275
277
|
try {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
+
// Mark explicit selection before validation so a damaged managed profile does
|
|
279
|
+
// not produce advice to start an unrelated local daemon.
|
|
280
|
+
managedSelection = process.env.OURS_CONFIG !== undefined
|
|
281
|
+
|| existsSync(join(process.env.HOME || homedir(), '.ours-client', 'profile.json'));
|
|
282
|
+
const selected = readClientProfile(process.env);
|
|
283
|
+
managedSelection = selected !== undefined;
|
|
284
|
+
const client = await attachDaemon(selected ? {
|
|
285
|
+
endpoint: selected.endpoint, expectedInstanceId: selected.expectedInstanceId,
|
|
286
|
+
credentialPath: selected.credentialPath, env: {}, sessionMode: 'external',
|
|
278
287
|
leaseToken: `ours-fleet-doctor-${process.pid}`,
|
|
279
|
-
|
|
288
|
+
} : {
|
|
289
|
+
env: process.env, leaseToken: `ours-fleet-doctor-${process.pid}`, clientPid: process.pid,
|
|
280
290
|
});
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
291
|
+
let daemonVersion;
|
|
292
|
+
try {
|
|
293
|
+
const info = await client.version();
|
|
294
|
+
if (info.name !== 'ours' || typeof info.version !== 'string')
|
|
295
|
+
throw new Error('the selected endpoint did not return a valid ours daemon identity');
|
|
296
|
+
daemonVersion = info.version;
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
await client.close();
|
|
300
|
+
}
|
|
301
|
+
checks.push({ name: 'ours daemon', ok: true, detail: `running (${daemonVersion})` });
|
|
285
302
|
}
|
|
286
303
|
catch (error) {
|
|
287
304
|
checks.push({
|
|
288
305
|
name: 'ours daemon', ok: false,
|
|
289
|
-
detail:
|
|
306
|
+
detail: (managedSelection
|
|
307
|
+
? 'selected client profile failed — check its endpoint, instance and issued credential '
|
|
308
|
+
: 'not reachable through the SDK — start it with: ours-daemon start ')
|
|
290
309
|
+ `[${error?.message ?? String(error)}]`,
|
|
291
310
|
});
|
|
292
311
|
}
|
|
@@ -478,7 +497,7 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
478
497
|
catch (e) {
|
|
479
498
|
const target = selected?.endpoint ?? `:${endpoint.port}`;
|
|
480
499
|
detail = `unreachable on ${target} — monitored roles run degraded until it is up ` +
|
|
481
|
-
`(start it: ours
|
|
500
|
+
`(start it: ours-daemon start) [${e?.message ?? e}]`;
|
|
482
501
|
}
|
|
483
502
|
checks.push({ name: checkName, ok, detail });
|
|
484
503
|
}
|
|
@@ -117,6 +117,7 @@ export function consumeFleetAuditCollection() {
|
|
|
117
117
|
const SAFE_READ = new Set(['docs', 'version', 'config', 'ls', 'peek', 'logs', 'status', 'doctor']);
|
|
118
118
|
const AGENT_SURFACES = {
|
|
119
119
|
spawn: new Set(['<none>']),
|
|
120
|
+
ours: new Set(['tools', 'call']),
|
|
120
121
|
template: new Set(['list', 'show', 'validate']),
|
|
121
122
|
task: new Set(['create', 'list', 'lists', 'list-create', 'list-rename', 'list-delete', 'move',
|
|
122
123
|
'show', 'start', 'block', 'unblock', 'review', 'done', 'cancel', 'delete', 'work', 'finish']),
|
|
@@ -132,7 +133,7 @@ export const fleetProxyTopLevelInventory = Object.freeze({
|
|
|
132
133
|
'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
|
|
133
134
|
'watchdog-run', 'watchdog-report', 'rm', 'init', 'web',
|
|
134
135
|
], denied: [...DENIED],
|
|
135
|
-
hidden: ['_run', '_run-temp', '_run-watchdog', '_run-watchdogs'], aliases: ['man'],
|
|
136
|
+
hidden: ['_run', '_run-temp', '_run-temp-worker', '_run-watchdog', '_run-watchdogs'], aliases: ['man'],
|
|
136
137
|
});
|
|
137
138
|
const globalValueOptions = new Set(['-c', '--configuration']);
|
|
138
139
|
/** Classify before Commander parsing. Unknown and internal paths fail closed. */
|
|
@@ -189,7 +190,7 @@ const sensitiveValueFlags = new Set([
|
|
|
189
190
|
'--identity', '--invite', '--token', '--api-token', '--password', '--password-file',
|
|
190
191
|
'--env', '--brief', '--brief-file', '--bio-file', '--persona-file', '--isolation-file', '--loops-file',
|
|
191
192
|
'--configuration', '-c', '--public-invite', '--public-invite-file', '--invite-file',
|
|
192
|
-
'--summary-file', '--text', '--message', '--summary', '--reason', '--goal', '--cwd',
|
|
193
|
+
'--args-file', '--summary-file', '--text', '--message', '--summary', '--reason', '--goal', '--cwd',
|
|
193
194
|
'--identity-cid', '--owner-cid', '--contact-cid', '--codex-config', '--add-dir',
|
|
194
195
|
]);
|
|
195
196
|
function redactUrl(value) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ManagedHarnessOurs } from '../agent-ours/harness.js';
|
|
1
2
|
import type { CommonPermissions, FleetPermissionMode, ResolvedRole } from '../config.js';
|
|
2
3
|
import type { AgentSession } from '../session/types.js';
|
|
3
4
|
import type { Launch, SessionPrep } from './types.js';
|
|
@@ -22,6 +23,7 @@ export interface AgentSessionConfigSelection {
|
|
|
22
23
|
value: string;
|
|
23
24
|
}
|
|
24
25
|
export interface AgentSessionStartOptions {
|
|
26
|
+
managedOurs?: ManagedHarnessOurs;
|
|
25
27
|
role: ResolvedRole;
|
|
26
28
|
prep: SessionPrep;
|
|
27
29
|
launch: PreparedAgentSessionLaunch;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { managedClaudeMeta } from '../agent-ours/harness.js';
|
|
1
2
|
import { AcpSession } from '../session/acp.js';
|
|
2
3
|
import { acpAdapterState, } from './acp-session-transport.js';
|
|
3
4
|
/** Explicit Claude Code implementation of Fleet's shared agent-session factory. */
|
|
@@ -25,8 +26,9 @@ export class ClaudeCodeAgentSessionAdapter {
|
|
|
25
26
|
argv: launch.argv, cwd: options.cwd, env: launch.env,
|
|
26
27
|
stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
|
|
27
28
|
modeId: this.strategy.permissionModeId(role),
|
|
28
|
-
mcpServers: this.strategy.mcpServers(role),
|
|
29
|
-
|
|
29
|
+
mcpServers: options.managedOurs ? [...(this.strategy.mcpServers(role) ?? []).filter(s => s.name !== 'ours'), options.managedOurs.server] : this.strategy.mcpServers(role),
|
|
30
|
+
inheritEnvironment: options.managedOurs ? false : undefined,
|
|
31
|
+
sessionMeta: options.managedOurs ? managedClaudeMeta(this.strategy.sessionMeta(role, prep)) : this.strategy.sessionMeta(role, prep),
|
|
30
32
|
configSelections: this.strategy.sessionConfigSelections(role),
|
|
31
33
|
permissionMode: options.permissionMode,
|
|
32
34
|
permissionMetadataSource: acpAdapterState(launch.adapterState).permissionMetadataSource,
|
|
@@ -23,6 +23,12 @@ export class CodexAgentSessionAdapter {
|
|
|
23
23
|
}
|
|
24
24
|
start(options) {
|
|
25
25
|
const { role, prep, launch } = options;
|
|
26
|
+
const original = this.strategy.nativeConfig(role) ?? {};
|
|
27
|
+
const managed = options.managedOurs?.native;
|
|
28
|
+
const config = managed ? { ...original, ...managed,
|
|
29
|
+
mcp_servers: { ...(original.mcp_servers ?? {}), ...(managed.mcp_servers ?? {}) },
|
|
30
|
+
plugins: { ...(original.plugins ?? {}), ...(managed.plugins ?? {}) },
|
|
31
|
+
} : original;
|
|
26
32
|
if (role.session === 'codex-app-server')
|
|
27
33
|
return this.nativeTransport({
|
|
28
34
|
name: role.name, argv: launch.argv, cwd: options.cwd, env: launch.env,
|
|
@@ -31,7 +37,7 @@ export class CodexAgentSessionAdapter {
|
|
|
31
37
|
model: role.model, effort: role.effort,
|
|
32
38
|
approvalPolicy: this.strategy.approvalPolicy(role),
|
|
33
39
|
sandbox: this.strategy.sandbox(role),
|
|
34
|
-
config
|
|
40
|
+
config,
|
|
35
41
|
addDirs: this.strategy.addDirs(role),
|
|
36
42
|
log: options.log,
|
|
37
43
|
});
|
|
@@ -40,7 +46,8 @@ export class CodexAgentSessionAdapter {
|
|
|
40
46
|
argv: launch.argv, cwd: options.cwd, env: launch.env,
|
|
41
47
|
stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
|
|
42
48
|
modeId: this.strategy.permissionModeId(role),
|
|
43
|
-
mcpServers: this.strategy.mcpServers(role),
|
|
49
|
+
mcpServers: options.managedOurs ? [...(this.strategy.mcpServers(role) ?? []).filter(s => s.name !== 'ours'), options.managedOurs.server] : this.strategy.mcpServers(role),
|
|
50
|
+
inheritEnvironment: options.managedOurs ? false : undefined,
|
|
44
51
|
sessionMeta: this.strategy.sessionMeta(role, prep),
|
|
45
52
|
configSelections: this.strategy.sessionConfigSelections(role),
|
|
46
53
|
permissionMode: options.permissionMode,
|
package/dist/harness/codex.d.ts
CHANGED
|
@@ -6,8 +6,9 @@ import type { AcpSessionTransport } from './acp-session-transport.js';
|
|
|
6
6
|
import type { CodexAppServerSessionTransport } from './codex-session.js';
|
|
7
7
|
/**
|
|
8
8
|
* What an unattended role can actually do under Codex's native settings.
|
|
9
|
-
* `on-request` and `untrusted`
|
|
10
|
-
*
|
|
9
|
+
* `on-request` and `untrusted` can ask even for startup file reads (for example,
|
|
10
|
+
* shell reads or files outside the sandbox). Without a controller the request
|
|
11
|
+
* waits or is denied according to policy. Neither guarantees `read-state`.
|
|
11
12
|
*/
|
|
12
13
|
export declare function codexCapabilities(approval: string, sandbox: string): UnattendedCapability[];
|
|
13
14
|
/** Defense in depth for callers which launch a role after validation was bypassed. */
|
package/dist/harness/codex.js
CHANGED
|
@@ -25,12 +25,13 @@ const CODEX_PROXY_REAL_PATH_ENV = 'OURS_FLEET_REAL_CODEX_PATH';
|
|
|
25
25
|
const CODEX_PROXY_MANIFEST_ENV = 'OURS_FLEET_CODEX_ACP_MANIFEST';
|
|
26
26
|
/**
|
|
27
27
|
* What an unattended role can actually do under Codex's native settings.
|
|
28
|
-
* `on-request` and `untrusted`
|
|
29
|
-
*
|
|
28
|
+
* `on-request` and `untrusted` can ask even for startup file reads (for example,
|
|
29
|
+
* shell reads or files outside the sandbox). Without a controller the request
|
|
30
|
+
* waits or is denied according to policy. Neither guarantees `read-state`.
|
|
30
31
|
*/
|
|
31
32
|
export function codexCapabilities(approval, sandbox) {
|
|
32
33
|
if (approval !== 'never')
|
|
33
|
-
return [
|
|
34
|
+
return [];
|
|
34
35
|
const caps = ['read-state', 'messaging', 'monitor', 'status-commands'];
|
|
35
36
|
if (sandbox !== 'read-only')
|
|
36
37
|
caps.push('write-state', 'workspace-edit');
|
|
@@ -53,6 +53,12 @@ export class HermesAgentSessionAdapter {
|
|
|
53
53
|
// composition; no second ambient merge may reintroduce provider secrets.
|
|
54
54
|
const env = hermesChildEnvironment(role, preparedHome(prep), launch.env, launch.env);
|
|
55
55
|
delete env.OURS_AUTOSTART;
|
|
56
|
+
if (options.managedOurs) {
|
|
57
|
+
for (const key of Object.keys(env))
|
|
58
|
+
if (key.startsWith('OURS_') && !key.startsWith('OURS_FLEET_'))
|
|
59
|
+
delete env[key];
|
|
60
|
+
Object.assign(env, { FLEET_OURS_MANAGED: '1' });
|
|
61
|
+
}
|
|
56
62
|
await this.checks.preflight?.(options, env);
|
|
57
63
|
const nativeProvider = await this.checks.expectedProvider(role, prep);
|
|
58
64
|
if (typeof nativeProvider !== 'string' || !nativeProvider.trim())
|
|
@@ -68,7 +74,7 @@ export class HermesAgentSessionAdapter {
|
|
|
68
74
|
permissions: options.permissions, modeId, requireMode: true,
|
|
69
75
|
permissionMode: { fleetMode: options.permissionMode.fleetMode, nativeMode: modeId },
|
|
70
76
|
permissionTimeoutMs: 50_000,
|
|
71
|
-
mcpServers: hermesMcpServers(role, env),
|
|
77
|
+
mcpServers: options.managedOurs ? [...hermesMcpServers(role, env).filter(s => s.name !== 'ours'), options.managedOurs.server] : hermesMcpServers(role, env),
|
|
72
78
|
scrubObsoleteOursAutostart: true,
|
|
73
79
|
validateStartupResponse: async (initialized, created) => {
|
|
74
80
|
await this.checks.validateArtifact(initialized);
|
|
@@ -14,6 +14,8 @@ export interface OwnerChannelOptions {
|
|
|
14
14
|
harness: string;
|
|
15
15
|
config: OwnerChannelConfig;
|
|
16
16
|
session: AgentSession;
|
|
17
|
+
/** Ordinary owner input must not cancel the initial briefing turn. */
|
|
18
|
+
startupPending?: () => boolean;
|
|
17
19
|
stateDir: string;
|
|
18
20
|
env?: Record<string, string>;
|
|
19
21
|
log(line: string): void;
|
|
@@ -269,6 +271,7 @@ export declare class OwnerChannel implements OwnerChannelHandle {
|
|
|
269
271
|
private attachmentGroups;
|
|
270
272
|
private handleAttachmentGroup;
|
|
271
273
|
private handle;
|
|
274
|
+
private ownerPromptPolicy;
|
|
272
275
|
/**
|
|
273
276
|
* Deterministic command path: the message never becomes an agent prompt.
|
|
274
277
|
* Authorization already happened — the managed-agent relay branch and the
|
|
@@ -1123,6 +1123,13 @@ export class OwnerChannel {
|
|
|
1123
1123
|
const retrieved = unread.length
|
|
1124
1124
|
? parseRetrievedAttachments(await this.client.getFiles(unread.map(file => file.wireId)), unread)
|
|
1125
1125
|
: [];
|
|
1126
|
+
// getFiles returns daemon-local paths, which need not exist in Fleet's
|
|
1127
|
+
// filesystem (for example with an HTTP daemon in a container). Fetch
|
|
1128
|
+
// through the bound client and keep the daemon's integrity metadata for
|
|
1129
|
+
// admission below; never trust or remap the returned filesystem path.
|
|
1130
|
+
for (const file of retrieved) {
|
|
1131
|
+
file.path = await writeRecoveredAttachment(requestDir, file.wireId, await this.client.fetchFile(file.wireId));
|
|
1132
|
+
}
|
|
1126
1133
|
for (const file of historyRecovered) {
|
|
1127
1134
|
if (!group.recovery)
|
|
1128
1135
|
throw new Error('unexpected read attachment without recovery route');
|
|
@@ -1136,8 +1143,7 @@ export class OwnerChannel {
|
|
|
1136
1143
|
await mkdir(outbox, { recursive: true, mode: 0o700 });
|
|
1137
1144
|
const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
|
|
1138
1145
|
const queued = await queueSessionPrompt(this.options.session, this.ownerAttachmentPrompt(sender, originWireId, requestId, admitted, group.caption), {
|
|
1139
|
-
|
|
1140
|
-
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
1146
|
+
...this.ownerPromptPolicy(),
|
|
1141
1147
|
origin: { kind: 'owner', requestId,
|
|
1142
1148
|
...(group.caption ? { displayText: String(group.caption.text ?? '') } : {}) },
|
|
1143
1149
|
});
|
|
@@ -1263,8 +1269,7 @@ export class OwnerChannel {
|
|
|
1263
1269
|
const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
|
|
1264
1270
|
try {
|
|
1265
1271
|
queued = await queueSessionPrompt(this.options.session, this.ownerPrompt(sender, text, wireId), {
|
|
1266
|
-
|
|
1267
|
-
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
1272
|
+
...this.ownerPromptPolicy(),
|
|
1268
1273
|
origin: { kind: 'owner', requestId, displayText: text },
|
|
1269
1274
|
});
|
|
1270
1275
|
}
|
|
@@ -1309,6 +1314,14 @@ export class OwnerChannel {
|
|
|
1309
1314
|
this.completionTasks.add(task);
|
|
1310
1315
|
return true;
|
|
1311
1316
|
}
|
|
1317
|
+
ownerPromptPolicy() {
|
|
1318
|
+
if (this.options.startupPending?.())
|
|
1319
|
+
return { interrupt: false, steer: true };
|
|
1320
|
+
return {
|
|
1321
|
+
interrupt: this.options.config.interrupt,
|
|
1322
|
+
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1312
1325
|
/**
|
|
1313
1326
|
* Deterministic command path: the message never becomes an agent prompt.
|
|
1314
1327
|
* Authorization already happened — the managed-agent relay branch and the
|
|
@@ -1762,6 +1775,13 @@ export class OwnerChannel {
|
|
|
1762
1775
|
const retrieved = unread.length
|
|
1763
1776
|
? parseRetrievedAttachments(await this.client.getFiles(unread.map(file => file.wireId)), unread)
|
|
1764
1777
|
: [];
|
|
1778
|
+
// getFiles returns daemon-local paths, which need not exist in Fleet's
|
|
1779
|
+
// filesystem (for example with an HTTP daemon in a container). Fetch
|
|
1780
|
+
// through the bound client and keep the daemon's integrity metadata for
|
|
1781
|
+
// admission below; never trust or remap the returned filesystem path.
|
|
1782
|
+
for (const file of retrieved) {
|
|
1783
|
+
file.path = await writeRecoveredAttachment(requestDir, file.wireId, await this.client.fetchFile(file.wireId));
|
|
1784
|
+
}
|
|
1765
1785
|
for (const file of historyRecovered) {
|
|
1766
1786
|
if (!group.recovery)
|
|
1767
1787
|
throw new Error('unexpected read attachment without recovery route');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ArchivedMemberAbsence, RoomMemberSeat } from './types.js';
|
|
2
|
+
/** Revalidate live facts; a persisted timestamp is never cleanup authority. */
|
|
3
|
+
export declare function verifyArchivedAbsence(proof: ArchivedMemberAbsence): Promise<void>;
|
|
4
|
+
/** After the verified archive has been consumed by explicit workspace deletion,
|
|
5
|
+
* still fence replacement live state and a recreated daemon identity on every retry.
|
|
6
|
+
*/
|
|
7
|
+
export declare function verifyArchivedMemberStillAbsent(proof: ArchivedMemberAbsence): Promise<void>;
|
|
8
|
+
export declare function proveArchivedAbsence(seat: RoomMemberSeat): Promise<ArchivedMemberAbsence>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { agentDir } from '../paths.js';
|
|
4
|
+
import { tempArchiveForLaunch, tempArchiveForCreationAction, tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
5
|
+
import { assertMemberIdentityAbsent } from './close.js';
|
|
6
|
+
/** Revalidate live facts; a persisted timestamp is never cleanup authority. */
|
|
7
|
+
export async function verifyArchivedAbsence(proof) {
|
|
8
|
+
const absent = () => {
|
|
9
|
+
if (existsSync(agentDir(proof.name, true)))
|
|
10
|
+
throw new Error('Archived member has replacement live state');
|
|
11
|
+
};
|
|
12
|
+
absent();
|
|
13
|
+
const archive = tempArchiveForLaunch(proof.name, proof.launch_id);
|
|
14
|
+
const action = tempArchiveForCreationAction(proof.name, proof.action_id);
|
|
15
|
+
if (!archive || archive !== proof.archive_path || action?.path !== archive
|
|
16
|
+
|| action.launchId !== proof.launch_id
|
|
17
|
+
|| readFileSync(join(archive, '.identity'), 'utf8').trim() !== proof.name)
|
|
18
|
+
throw new Error('Archived member absence ownership proof mismatch');
|
|
19
|
+
if (await tempSupervisorLiveness(archive) !== 'stopped')
|
|
20
|
+
throw new Error('Archived member supervisor absence is not proven');
|
|
21
|
+
await verifyArchivedMemberStillAbsent(proof);
|
|
22
|
+
}
|
|
23
|
+
/** After the verified archive has been consumed by explicit workspace deletion,
|
|
24
|
+
* still fence replacement live state and a recreated daemon identity on every retry.
|
|
25
|
+
*/
|
|
26
|
+
export async function verifyArchivedMemberStillAbsent(proof) {
|
|
27
|
+
const absent = () => {
|
|
28
|
+
if (existsSync(agentDir(proof.name, true)))
|
|
29
|
+
throw new Error('Archived member has replacement live state');
|
|
30
|
+
};
|
|
31
|
+
absent();
|
|
32
|
+
await assertMemberIdentityAbsent({
|
|
33
|
+
role_name: proof.name, slot: 'archived', cowork_role: 'archived', seat_state: 'removed',
|
|
34
|
+
});
|
|
35
|
+
absent();
|
|
36
|
+
}
|
|
37
|
+
export async function proveArchivedAbsence(seat) {
|
|
38
|
+
if (seat.identity_cid || seat.retirement?.phase !== 'identity_absent'
|
|
39
|
+
|| !seat.launch?.launch_id || !seat.launch.action_id
|
|
40
|
+
|| seat.launch.launch_id !== seat.retirement.launch_id || !seat.retirement.archive_path)
|
|
41
|
+
throw new Error('Missing exact archived member absence evidence');
|
|
42
|
+
const proof = {
|
|
43
|
+
name: seat.role_name, launch_id: seat.launch.launch_id, action_id: seat.launch.action_id,
|
|
44
|
+
archive_path: seat.retirement.archive_path, checked_at: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
await verifyArchivedAbsence(proof);
|
|
47
|
+
return proof;
|
|
48
|
+
}
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -1152,7 +1152,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1152
1152
|
const app = taskRoomService(opts.configuration);
|
|
1153
1153
|
for (;;) {
|
|
1154
1154
|
const before = app.taskProvisioningOutcome(id);
|
|
1155
|
-
if (before.kind !== 'in_progress') {
|
|
1155
|
+
if (before.kind !== 'in_progress' || before.next_action) {
|
|
1156
1156
|
await presentDetachedProvisioningOutcome(before);
|
|
1157
1157
|
return;
|
|
1158
1158
|
}
|
|
@@ -2,6 +2,8 @@ import { withFileLock } from '../atomic-file.js';
|
|
|
2
2
|
import { type TempLifecycleDeps } from '../temp-lifecycle.js';
|
|
3
3
|
import { type CoworkAdapter } from './cowork-adapter.js';
|
|
4
4
|
import type { RoomMemberSeat, RoomOrchestrationRecord } from './types.js';
|
|
5
|
+
export declare const CLOSE_LOCK_STALE_MS: number;
|
|
6
|
+
export declare function roomCloseLockPath(roomId: string): string;
|
|
5
7
|
export interface RoomCloseDeps {
|
|
6
8
|
inspectMember?(seat: RoomMemberSeat): Promise<{
|
|
7
9
|
launchId: string;
|
|
@@ -19,6 +21,7 @@ export declare function waitForLivenessAbsent(role: string, launchId: string, li
|
|
|
19
21
|
/** Report whether any daemon identity — under any name — carries this exact CID. */
|
|
20
22
|
export declare function identityCidPresent(cid: string): Promise<boolean>;
|
|
21
23
|
export declare function removeExactMemberIdentity(seat: RoomMemberSeat): Promise<void>;
|
|
24
|
+
export declare function assertMemberIdentityAbsent(seat: RoomMemberSeat): Promise<void>;
|
|
22
25
|
/** One forward-only room close saga shared by every Fleet entry point. */
|
|
23
26
|
export declare function acceptManagedRoomClose(roomId: string): Promise<RoomOrchestrationRecord>;
|
|
24
27
|
export declare function recordManagedRoomCloseError(roomId: string, error: string, recoveryHint: string): Promise<RoomOrchestrationRecord>;
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { deleteWorkspace, assertWorkspaceDeletable } from './workspace.js';
|
|
2
|
+
import { collectWorkspaceArchives } from './workspace-artifacts.js';
|
|
1
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
4
|
import { join } from 'node:path';
|
|
3
5
|
import { randomUUID } from 'node:crypto';
|
|
4
6
|
import { attachOursClient } from '@ours.network/sdk/client';
|
|
5
7
|
import { withFileLock } from '../atomic-file.js';
|
|
6
8
|
import { agentDir, stateRoot } from '../paths.js';
|
|
7
|
-
import {
|
|
9
|
+
import { readClientProfile } from '../client-profile.js';
|
|
10
|
+
import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, tempArchiveForLaunch, tempArchiveForCreationAction, } from '../temp-lifecycle.js';
|
|
8
11
|
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
9
12
|
import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, } from './room-state.js';
|
|
10
|
-
const CLOSE_LOCK_STALE_MS = 5 * 60_000;
|
|
13
|
+
export const CLOSE_LOCK_STALE_MS = 5 * 60_000;
|
|
11
14
|
const STOP_POLLS = 50;
|
|
12
15
|
const STOP_POLL_MS = 100;
|
|
13
|
-
function roomCloseLockPath(roomId) {
|
|
16
|
+
export function roomCloseLockPath(roomId) {
|
|
14
17
|
return join(stateRoot(), 'locks', 'room-close', encodeURIComponent(roomId));
|
|
15
18
|
}
|
|
16
19
|
function errorText(error) {
|
|
@@ -54,16 +57,28 @@ export async function waitForLivenessAbsent(role, launchId, lifecycleDeps = {})
|
|
|
54
57
|
throw new Error(`temporary role '${role}' did not reach proven stopped liveness`);
|
|
55
58
|
}
|
|
56
59
|
async function withIdentityClient(work) {
|
|
57
|
-
const
|
|
60
|
+
const profile = readClientProfile(process.env);
|
|
61
|
+
const leaseToken = `ours-fleet-room-close-${process.pid}-${randomUUID()}`;
|
|
62
|
+
const client = await attachOursClient(profile ? {
|
|
63
|
+
endpoint: profile.endpoint,
|
|
64
|
+
expectedInstanceId: profile.expectedInstanceId,
|
|
65
|
+
credentialPath: profile.credentialPath,
|
|
66
|
+
sessionMode: 'external', env: {}, leaseToken,
|
|
67
|
+
} : {
|
|
58
68
|
env: process.env,
|
|
59
|
-
leaseToken
|
|
69
|
+
leaseToken,
|
|
60
70
|
clientPid: process.pid,
|
|
61
71
|
});
|
|
62
72
|
try {
|
|
63
73
|
return await work(client);
|
|
64
74
|
}
|
|
65
75
|
finally {
|
|
66
|
-
|
|
76
|
+
try {
|
|
77
|
+
await client.releaseLease().catch(() => { });
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await client.close();
|
|
81
|
+
}
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
function listedIdentity(rows, name) {
|
|
@@ -106,11 +121,57 @@ export async function removeExactMemberIdentity(seat) {
|
|
|
106
121
|
}
|
|
107
122
|
});
|
|
108
123
|
}
|
|
124
|
+
export async function assertMemberIdentityAbsent(seat) {
|
|
125
|
+
await withIdentityClient(async (client) => {
|
|
126
|
+
const rows = await client.listIdentities();
|
|
127
|
+
if (rows.some(row => row.name === seat.role_name ||
|
|
128
|
+
(seat.identity_cid && 'cid' in row && row.cid.toLowerCase() === seat.identity_cid.toLowerCase()))) {
|
|
129
|
+
throw new Error(`room member '${seat.role_name}' identity absence is not proven; refusing retirement`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
109
133
|
async function retireMember(roomId, seat, deps) {
|
|
110
134
|
let room = getRoomRecord(roomId);
|
|
111
135
|
let current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
|
|
112
136
|
let retirement = current.retirement;
|
|
137
|
+
if (retirement?.phase === 'identity_absent') {
|
|
138
|
+
if (current.launch?.launch_id && current.launch.launch_id !== retirement.launch_id) {
|
|
139
|
+
if (existsSync(agentDir(current.role_name, true)))
|
|
140
|
+
throw new Error(`room member '${current.role_name}' has a replacement launch after retirement; retire that exact temporary role before retrying`);
|
|
141
|
+
await assertMemberIdentityAbsent(current);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
113
145
|
if (!retirement) {
|
|
146
|
+
if (!existsSync(agentDir(current.role_name, true)) && current.launch?.launch_id && current.launch.action_id) {
|
|
147
|
+
const archived = tempArchiveForLaunch(current.role_name, current.launch.launch_id);
|
|
148
|
+
const created = tempArchiveForCreationAction(current.role_name, current.launch.action_id);
|
|
149
|
+
if (archived && created?.path === archived && created.launchId === current.launch.launch_id) {
|
|
150
|
+
if (readFileSync(join(archived, '.identity'), 'utf8').trim() !== current.role_name)
|
|
151
|
+
throw new Error(`room member '${current.role_name}' archive identity mismatch`);
|
|
152
|
+
if (await tempSupervisorLiveness(archived) !== 'stopped')
|
|
153
|
+
throw new Error(`room member '${current.role_name}' archived supervisor is not proven stopped`);
|
|
154
|
+
// A terminated launch can archive itself before room retirement begins.
|
|
155
|
+
// Accept its exact durable provenance only when no identity needs removal.
|
|
156
|
+
await assertMemberIdentityAbsent(current);
|
|
157
|
+
const latest = getRoomRecord(roomId)?.member_seats.find(seat => seat.role_name === current.role_name);
|
|
158
|
+
if (existsSync(agentDir(current.role_name, true))
|
|
159
|
+
|| latest?.launch?.launch_id !== current.launch.launch_id
|
|
160
|
+
|| latest?.launch?.action_id !== current.launch.action_id
|
|
161
|
+
|| latest?.identity_cid !== current.identity_cid)
|
|
162
|
+
throw new Error(`room member '${current.role_name}' changed during archived retirement proof`);
|
|
163
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', current.launch.launch_id, archived);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (current.launch?.state === 'failed' && !existsSync(agentDir(current.role_name, true))) {
|
|
168
|
+
// A failure before applyRole (for example invite-secret validation) has
|
|
169
|
+
// no supervisor to stop or archive. Settle only proven absence; this
|
|
170
|
+
// path never removes an identity based on missing local evidence.
|
|
171
|
+
await assertMemberIdentityAbsent(current);
|
|
172
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', 'failed-launch-absent');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
114
175
|
if (current.launch?.state === 'pending' && current.launch.attempt === 0) {
|
|
115
176
|
if (existsSync(agentDir(current.role_name, true))) {
|
|
116
177
|
throw new Error(`never-launched room member '${current.role_name}' unexpectedly has Fleet temp state`);
|
|
@@ -159,8 +220,6 @@ export async function closeManagedRoom(input) {
|
|
|
159
220
|
try {
|
|
160
221
|
if (room.close?.phase === 'retire_members') {
|
|
161
222
|
for (const seat of room.member_seats) {
|
|
162
|
-
if (seat.retirement?.phase === 'identity_absent')
|
|
163
|
-
continue;
|
|
164
223
|
await retireMember(input.roomId, seat, deps);
|
|
165
224
|
}
|
|
166
225
|
room = advanceRoomClose(input.roomId, 'close_cowork');
|
|
@@ -180,6 +239,9 @@ export async function closeManagedRoom(input) {
|
|
|
180
239
|
export async function deleteLegacyClosedRooms(input) {
|
|
181
240
|
const deleted = [];
|
|
182
241
|
for (const room of listRoomRecords({ state: 'closed' })) {
|
|
242
|
+
// This compatibility sweep is retirement, never explicit workspace deletion.
|
|
243
|
+
if (room.workspace)
|
|
244
|
+
continue;
|
|
183
245
|
try {
|
|
184
246
|
await input.cowork.deleteRoom(room.room_id);
|
|
185
247
|
}
|
|
@@ -197,13 +259,29 @@ export async function deleteLegacyClosedRooms(input) {
|
|
|
197
259
|
/** Retire live resources through the existing cursor, then delete retained state. */
|
|
198
260
|
export async function deleteManagedRoom(input) {
|
|
199
261
|
await closeManagedRoom(input);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
262
|
+
return withFileLock(roomCloseLockPath(input.roomId), async () => {
|
|
263
|
+
try {
|
|
264
|
+
try {
|
|
265
|
+
await input.cowork.deleteRoom(input.roomId);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
// An earlier explicit delete may have removed the remote room before
|
|
269
|
+
// local filesystem cleanup failed. Never restore it or repeat creation.
|
|
270
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
const retained = getRoomRecord(input.roomId);
|
|
274
|
+
if (retained?.workspace && !retained.task_id) {
|
|
275
|
+
assertWorkspaceDeletable(retained.workspace, 'room', input.roomId);
|
|
276
|
+
collectWorkspaceArchives(retained.workspace);
|
|
277
|
+
deleteWorkspace(retained.workspace, 'room', input.roomId);
|
|
278
|
+
}
|
|
279
|
+
deleteRoomRecord(input.roomId);
|
|
280
|
+
return { room_id: input.roomId, deleted: true };
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
setRoomCloseError(input.roomId, errorText(error), `Fix the cleanup error and explicitly retry room delete ${input.roomId} ${input.roomId}.`);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}, {}, CLOSE_LOCK_STALE_MS);
|
|
209
287
|
}
|
|
@@ -58,6 +58,10 @@ export interface CoworkAdapter {
|
|
|
58
58
|
briefing: string;
|
|
59
59
|
quiet_membership?: boolean;
|
|
60
60
|
anonymous?: boolean;
|
|
61
|
+
activation_requirements?: Array<{
|
|
62
|
+
role: string;
|
|
63
|
+
count: number;
|
|
64
|
+
}>;
|
|
61
65
|
}): Promise<CoworkRoomCreateResult>;
|
|
62
66
|
acceptInvite(roomId: string, invite: string, opts: {
|
|
63
67
|
role: string;
|
|
@@ -8,6 +8,8 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { join, resolve } from 'node:path';
|
|
10
10
|
import { createConnection } from 'node:net';
|
|
11
|
+
import { readClientProfile } from '../client-profile.js';
|
|
12
|
+
import { coworkHttpCall } from './cowork-http.js';
|
|
11
13
|
const RPC_VERSION = 1;
|
|
12
14
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
13
15
|
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
@@ -275,10 +277,15 @@ function rpcCall(socketPath, method, params, timeoutMs, connect) {
|
|
|
275
277
|
});
|
|
276
278
|
}
|
|
277
279
|
export function createCoworkAdapter(options = {}) {
|
|
278
|
-
const
|
|
280
|
+
const env = options.env ?? process.env;
|
|
281
|
+
const explicitLocal = options.socketPath !== undefined || options.configPath !== undefined
|
|
282
|
+
|| env.OURS_COWORK_CONFIG !== undefined || !!env.OURS_COWORK_STATE_DIR;
|
|
283
|
+
const profile = explicitLocal ? undefined : readClientProfile({ ...env, HOME: options.home ?? env.HOME });
|
|
284
|
+
const remote = profile?.serverUrl ? profile : undefined;
|
|
285
|
+
const socketPath = remote ? undefined : resolveCoworkSocketPath(options);
|
|
279
286
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
280
287
|
const connect = options.connect ?? ((path) => createConnection(path));
|
|
281
|
-
const call = (method, params) => rpcCall(socketPath, method, params, timeoutMs, connect);
|
|
288
|
+
const call = (method, params) => remote ? coworkHttpCall(remote, method, params, timeoutMs) : rpcCall(socketPath, method, params, timeoutMs, connect);
|
|
282
289
|
return {
|
|
283
290
|
async available() {
|
|
284
291
|
try {
|
|
@@ -296,6 +303,7 @@ export function createCoworkAdapter(options = {}) {
|
|
|
296
303
|
briefing: opts.briefing,
|
|
297
304
|
...(opts.quiet_membership === undefined ? {} : { quiet_membership: opts.quiet_membership }),
|
|
298
305
|
...(opts.anonymous === undefined ? {} : { anonymous: opts.anonymous }),
|
|
306
|
+
...(opts.activation_requirements === undefined ? {} : { activation_requirements: opts.activation_requirements }),
|
|
299
307
|
}), 'room.create');
|
|
300
308
|
if (!result.identity_cid)
|
|
301
309
|
throw new CoworkProtocolError('room.create', 'created room did not establish an identity CID');
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ExplicitClientProfile } from '../client-profile.js';
|
|
2
|
+
/** Same RPC protocol as the local socket; never replay a possibly delivered mutation. */
|
|
3
|
+
export declare function coworkHttpCall(profile: ExplicitClientProfile, method: string, params: Record<string, unknown>, timeoutMs: number): Promise<unknown>;
|