@ours.network/fleet 1.2.0-nightly.53 → 1.2.0-nightly.55
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 +22 -12
- package/dist/agent-ours/bridge.js +10 -3
- package/dist/agent-ours/file-rpc.d.ts +8 -0
- package/dist/agent-ours/file-rpc.js +11 -1
- package/dist/agent-ours/harness.js +47 -3
- package/dist/agent-ours/mcp-endpoint.d.ts +5 -0
- package/dist/agent-ours/mcp-endpoint.js +4 -0
- package/dist/agent-ours/service.d.ts +6 -1
- package/dist/agent-ours/service.js +73 -8
- package/dist/application/conversation-history.d.ts +2 -0
- package/dist/application/conversation-history.js +25 -2
- package/dist/application/log-service.js +8 -8
- package/dist/application/role-creation-service.js +2 -1
- package/dist/application/role-removal-service.js +7 -1
- package/dist/application/role-repository.js +17 -0
- package/dist/application/supervisor-ours-tools.js +3 -1
- package/dist/application/task-layouts.js +12 -2
- package/dist/application/task-room-service.js +19 -17
- package/dist/application/types.d.ts +2 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +31 -3
- package/dist/config.d.ts +11 -0
- package/dist/config.js +13 -2
- package/dist/file-delivery/reader.d.ts +17 -0
- package/dist/file-delivery/reader.js +94 -0
- package/dist/file-delivery/store.d.ts +15 -0
- package/dist/file-delivery/store.js +124 -0
- package/dist/file-delivery/types.d.ts +19 -0
- package/dist/file-delivery/types.js +1 -0
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/codex-app-server-proxy.js +6 -2
- package/dist/harness/codex-file-inventory.d.ts +15 -0
- package/dist/harness/codex-file-inventory.js +113 -0
- package/dist/harness/codex-session.js +2 -2
- package/dist/harness/codex.js +2 -1
- package/dist/isolation/policy.js +4 -1
- package/dist/managed-recovery.d.ts +13 -0
- package/dist/managed-recovery.js +31 -0
- package/dist/ops.d.ts +2 -0
- package/dist/ops.js +15 -2
- package/dist/rooms-tasks/cli.js +21 -0
- package/dist/rooms-tasks/close.d.ts +2 -0
- package/dist/rooms-tasks/close.js +66 -1
- package/dist/rooms-tasks/deletion.js +10 -0
- package/dist/rooms-tasks/erasure.js +4 -2
- package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
- package/dist/rooms-tasks/layout-member-retirement.js +90 -0
- package/dist/rooms-tasks/layout-service.d.ts +1 -0
- package/dist/rooms-tasks/layout-service.js +31 -13
- package/dist/rooms-tasks/layout.d.ts +7 -0
- package/dist/rooms-tasks/layout.js +35 -0
- package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
- package/dist/rooms-tasks/legacy-task-member.js +116 -0
- package/dist/rooms-tasks/member-ownership.d.ts +5 -0
- package/dist/rooms-tasks/member-ownership.js +40 -0
- package/dist/rooms-tasks/provision.js +26 -1
- package/dist/rooms-tasks/recovery.d.ts +19 -0
- package/dist/rooms-tasks/recovery.js +110 -0
- package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
- package/dist/rooms-tasks/task-service-retirement.js +21 -0
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +16 -1
- package/dist/rooms-tasks/types.d.ts +3 -0
- package/dist/rooms-tasks/workspace.d.ts +2 -0
- package/dist/rooms-tasks/workspace.js +5 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.js +161 -22
- package/dist/session/acp.d.ts +5 -0
- package/dist/session/acp.js +68 -8
- package/dist/session/arbiter.d.ts +5 -0
- package/dist/session/arbiter.js +16 -2
- package/dist/session/codex-app-server.d.ts +1 -0
- package/dist/session/codex-app-server.js +5 -0
- package/dist/session/conversation-types.d.ts +4 -2
- package/dist/session/types.d.ts +3 -0
- package/dist/spawn.d.ts +5 -1
- package/dist/spawn.js +32 -3
- package/dist/startup-readiness.d.ts +1 -1
- package/dist/startup-readiness.js +3 -3
- package/dist/supervisor/adoption.d.ts +8 -0
- package/dist/supervisor/adoption.js +78 -0
- package/dist/supervisor/catalog.d.ts +62 -0
- package/dist/supervisor/catalog.js +254 -0
- package/dist/supervisor/fleet.d.ts +8 -0
- package/dist/supervisor/fleet.js +89 -0
- package/dist/supervisor/index.js +3 -6
- package/dist/supervisor/launchd.d.ts +1 -1
- package/dist/supervisor/launchd.js +24 -12
- package/dist/supervisor/legacy.d.ts +35 -0
- package/dist/supervisor/legacy.js +524 -0
- package/dist/supervisor/manager.d.ts +15 -0
- package/dist/supervisor/manager.js +258 -0
- package/dist/supervisor/scope.d.ts +4 -0
- package/dist/supervisor/scope.js +10 -0
- package/dist/supervisor/systemd.d.ts +2 -2
- package/dist/supervisor/systemd.js +14 -14
- package/dist/task-supervision.d.ts +9 -0
- package/dist/task-supervision.js +145 -0
- package/dist/temp-lifecycle.d.ts +17 -7
- package/dist/temp-lifecycle.js +85 -53
- package/dist/temp-supervisor-recovery.d.ts +1 -1
- package/dist/temp-supervisor-recovery.js +2 -2
- package/dist/web/server.js +24 -0
- package/docs/acp-files.md +102 -0
- package/docs/fleet-migration.md +54 -0
- package/docs/task-workspaces.md +26 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@ turns such sessions into **roles**: long-lived agents that
|
|
|
10
10
|
|
|
11
11
|
- **run through structured managed sessions** behind one provider-neutral
|
|
12
12
|
agent-session interface, which you can inspect or prompt,
|
|
13
|
-
- are **supervised** — systemd (Linux) or
|
|
14
|
-
|
|
13
|
+
- are **supervised** — one Fleet systemd service (Linux) or LaunchAgent (macOS)
|
|
14
|
+
runs their existing agent runners, restarts them on crash, and restores the
|
|
15
|
+
desired running roster at boot or login,
|
|
15
16
|
- **resume their context** across restarts (when the harness supports it),
|
|
16
17
|
- **bind an ours.network identity**, so you — and every other agent — can message
|
|
17
18
|
them by name over an end-to-end-encrypted channel
|
|
@@ -44,11 +45,11 @@ brain: { inline: { harness: codex, session: codex-app-server } }
|
|
|
44
45
|
|
|
45
46
|
```
|
|
46
47
|
~/fleet.yaml + ~/fleet/{agents,agent_templates,roles,brains,room_templates}/*.yaml your declaration
|
|
47
|
-
│ ours-fleet up
|
|
48
|
+
│ ours-fleet init, then up
|
|
48
49
|
▼
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
one Fleet OS service ──► private member catalog + existing agent runners
|
|
51
|
+
│
|
|
52
|
+
briefing.md per member ──► session adapter ──► native app-server or ACP transport
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
Each role gets a state dir (`~/.ours-fleet/agents/<Name>/`) holding its briefing,
|
|
@@ -81,9 +82,10 @@ The state dir contract:
|
|
|
81
82
|
| a harness CLI, logged in | the agent itself | e.g. Claude Code (`claude`) or Codex CLI (`codex`) |
|
|
82
83
|
| `ours` CLI + shared daemon | identity + agent-to-agent messaging | `ours-install client --config /absolute/private/profile.json --integrations fleet --fleet-settings /absolute/fleet-settings.json` |
|
|
83
84
|
|
|
84
|
-
Linux only: `ours-fleet init` enables *linger* so
|
|
85
|
-
and
|
|
86
|
-
equivalent)
|
|
85
|
+
Linux only: `ours-fleet init` enables *linger* so Fleet runs without a login session
|
|
86
|
+
and restores running members after reboot. On macOS the Fleet LaunchAgent starts
|
|
87
|
+
**at login** (no linger equivalent). Member logs live in each member's state
|
|
88
|
+
directory; use `ours-fleet logs <Name>` to read them.
|
|
87
89
|
|
|
88
90
|
## Install
|
|
89
91
|
|
|
@@ -103,6 +105,12 @@ maintained adapters require Node 22 or newer.
|
|
|
103
105
|
Each OS user manages their own fleet — to host roles under a sandboxed account,
|
|
104
106
|
become that account and repeat.
|
|
105
107
|
|
|
108
|
+
When upgrading from per-agent services, coordinate package installation with
|
|
109
|
+
explicit `ours-fleet init`. Preserve operator readiness hooks on the common
|
|
110
|
+
parent before migration; new task/temporary creation requires that parent.
|
|
111
|
+
See the [migration and rollback runbook](docs/fleet-migration.md)
|
|
112
|
+
for changed stop/restart behaviour and legacy task transfer limits.
|
|
113
|
+
|
|
106
114
|
## Quickstart
|
|
107
115
|
|
|
108
116
|
```sh
|
|
@@ -331,9 +339,9 @@ Installs predating the stamp are compared by hashing their `dist/`, so two of
|
|
|
331
339
|
those are still told apart.
|
|
332
340
|
|
|
333
341
|
A permanent role brought up via `-c custom.yaml` remembers that file (`.config-path`
|
|
334
|
-
in its state dir) across supervisor-triggered
|
|
335
|
-
|
|
336
|
-
the default `~/fleet.yaml
|
|
342
|
+
in its state dir and its private member registration) across supervisor-triggered
|
|
343
|
+
restarts. Its runner resolves that original configuration instead of falling back
|
|
344
|
+
to the default `~/fleet.yaml`.
|
|
337
345
|
|
|
338
346
|
## split configuration field reference
|
|
339
347
|
|
|
@@ -1920,3 +1928,5 @@ Prerelease rollout requires this Fleet capability to be published first, then th
|
|
|
1920
1928
|
installer's normal generated version/integrity lock updated, then installer and
|
|
1921
1929
|
App released. The currently pinned Fleet nightly.41 does not support v2 grants.
|
|
1922
1930
|
These source changes do not publish, merge, or deploy any component.
|
|
1931
|
+
|
|
1932
|
+
See [ACP file sending](docs/acp-files.md) for the opt-in same-chat `send_file` flow and supported configurations.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { openExportFile } from '../file-delivery/reader.js';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { connect } from 'node:net';
|
|
4
5
|
import { access, mkdir, open, readFile } from 'node:fs/promises';
|
|
@@ -71,7 +72,7 @@ export async function runBridge(descriptorPath) {
|
|
|
71
72
|
value = false;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
|
-
else if (op === 'read-open' || op === 'write-open') {
|
|
75
|
+
else if (op === 'read-open' || op === 'write-open' || op === 'export-open') {
|
|
75
76
|
if (handles.size >= 32 ||
|
|
76
77
|
typeof args.handle !== 'string' ||
|
|
77
78
|
handles.has(args.handle) ||
|
|
@@ -80,8 +81,9 @@ export async function runBridge(descriptorPath) {
|
|
|
80
81
|
const path = resolve(args.path);
|
|
81
82
|
if (op === 'write-open')
|
|
82
83
|
await mkdir(dirname(path), { recursive: true });
|
|
83
|
-
const file = await open(path, op === 'write-open' ? 'w' : 'r');
|
|
84
|
-
|
|
84
|
+
const file = op === 'export-open' ? await openExportFile(args.root, process.cwd(), args.path) : await open(path, op === 'write-open' ? 'w' : 'r');
|
|
85
|
+
const stat = await file.stat();
|
|
86
|
+
handles.set(args.handle, { file, call, write: op === 'write-open', ...(op === 'export-open' ? { original: { size: stat.size, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs } } : {}) });
|
|
85
87
|
value =
|
|
86
88
|
op === 'write-open'
|
|
87
89
|
? path
|
|
@@ -105,6 +107,11 @@ export async function runBridge(descriptorPath) {
|
|
|
105
107
|
value = null;
|
|
106
108
|
}
|
|
107
109
|
else if (op === 'read' && !row.write) {
|
|
110
|
+
if (row.original) {
|
|
111
|
+
const stat = await row.file.stat();
|
|
112
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size !== row.original.size || stat.mtimeMs !== row.original.mtimeMs || stat.ctimeMs !== row.original.ctimeMs)
|
|
113
|
+
throw Error('EXPORT_SOURCE_CHANGED');
|
|
114
|
+
}
|
|
108
115
|
const buffer = Buffer.alloc(MAX_CHUNK_BYTES);
|
|
109
116
|
const { bytesRead } = await row.file.read(buffer);
|
|
110
117
|
value = bytesRead ? buffer.subarray(0, bytesRead).toString('base64') : null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExportRoot } from '../file-delivery/reader.js';
|
|
1
2
|
import type { FileExecutionContext } from '@ours.network/mcp/server';
|
|
2
3
|
import type { ToolRequestExtra } from '@ours.network/mcp/dist/types/mcp/tool.js';
|
|
3
4
|
import { type Wire } from './wire.js';
|
|
@@ -16,6 +17,13 @@ export declare class FileCallbacks implements FileExecutionContext {
|
|
|
16
17
|
body: ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
17
18
|
close: () => Promise<void>;
|
|
18
19
|
}>;
|
|
20
|
+
readExport(path: string, root: ExportRoot, extra: ToolRequestExtra): Promise<{
|
|
21
|
+
filename: any;
|
|
22
|
+
size: any;
|
|
23
|
+
body: ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
24
|
+
close: () => Promise<void>;
|
|
25
|
+
}>;
|
|
26
|
+
private openRead;
|
|
19
27
|
write(path: string, body: ReadableStream<Uint8Array>, extra: ToolRequestExtra): Promise<{
|
|
20
28
|
path: string;
|
|
21
29
|
size: number;
|
|
@@ -59,14 +59,24 @@ export class FileCallbacks {
|
|
|
59
59
|
return (await this.rpc(String(extra.requestId), 'probe', { path }, extra.signal)) === true;
|
|
60
60
|
}
|
|
61
61
|
async read(path, extra) {
|
|
62
|
+
return this.openRead(path, extra);
|
|
63
|
+
}
|
|
64
|
+
async readExport(path, root, extra) {
|
|
65
|
+
return this.openRead(path, extra, root);
|
|
66
|
+
}
|
|
67
|
+
async openRead(path, extra, root) {
|
|
62
68
|
const call = String(extra.requestId), handle = randomUUID();
|
|
63
|
-
const meta = await this.rpc(call, 'read-open', { path, handle }, extra.signal);
|
|
69
|
+
const meta = await this.rpc(call, root ? 'export-open' : 'read-open', { path, handle, ...(root ? { root } : {}) }, extra.signal);
|
|
64
70
|
if (!meta ||
|
|
65
71
|
typeof meta.filename !== 'string' ||
|
|
66
72
|
!Number.isSafeInteger(meta.size) ||
|
|
67
73
|
meta.size < 0)
|
|
68
74
|
throw Error('INVALID_FILE_METADATA');
|
|
75
|
+
let closed = false;
|
|
69
76
|
const close = async () => {
|
|
77
|
+
if (closed)
|
|
78
|
+
return;
|
|
79
|
+
closed = true;
|
|
70
80
|
await this.rpc(call, 'close', { handle }, new AbortController().signal).catch(() => { });
|
|
71
81
|
};
|
|
72
82
|
const body = new ReadableStream({
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
2
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { resolveBundledAcpAgent } from '../harness/acp-agent.js';
|
|
3
6
|
/** Do not pass the supervisor's ours connection credentials to harness children. */
|
|
4
7
|
export function managedChildEnvironment(input) {
|
|
5
8
|
const out = {};
|
|
6
9
|
for (const [key, value] of Object.entries(input)) {
|
|
7
|
-
if (value === undefined || (/^OURS_/i.test(key) && !key.startsWith('OURS_FLEET_')))
|
|
10
|
+
if (key === 'FLEET_CURRENT_CHAT_FILES' || value === undefined || (/^OURS_/i.test(key) && !key.startsWith('OURS_FLEET_')))
|
|
8
11
|
continue;
|
|
9
12
|
out[key] = value;
|
|
10
13
|
}
|
|
@@ -13,6 +16,9 @@ export function managedChildEnvironment(input) {
|
|
|
13
16
|
}
|
|
14
17
|
/** Replace ours only; retain the user's ordinary harness configuration and plugins. */
|
|
15
18
|
export function prepareManagedHarness(role, stateDir, cwd, descriptor, env) {
|
|
19
|
+
const child = managedChildEnvironment({ ...process.env, ...env });
|
|
20
|
+
if (role.file_delivery?.enabled)
|
|
21
|
+
qualifyFileHarness(role, cwd, child);
|
|
16
22
|
const bridge = fileURLToPath(new URL('./bridge.js', import.meta.url));
|
|
17
23
|
const server = {
|
|
18
24
|
name: 'ours',
|
|
@@ -37,7 +43,6 @@ export function prepareManagedHarness(role, stateDir, cwd, descriptor, env) {
|
|
|
37
43
|
},
|
|
38
44
|
plugins: { 'ours@ours-codex-marketplace': { enabled: false } },
|
|
39
45
|
};
|
|
40
|
-
const child = managedChildEnvironment({ ...process.env, ...env });
|
|
41
46
|
if (role.harness === 'codex') {
|
|
42
47
|
const config = child.CODEX_CONFIG ? JSON.parse(child.CODEX_CONFIG) : {};
|
|
43
48
|
child.CODEX_CONFIG = JSON.stringify({
|
|
@@ -70,3 +75,42 @@ export function managedClaudeMeta(base) {
|
|
|
70
75
|
},
|
|
71
76
|
};
|
|
72
77
|
}
|
|
78
|
+
/** Bounded stock-adapter support; opt-in never silently erases unrelated tools. */
|
|
79
|
+
function qualifyFileHarness(role, cwd, child) {
|
|
80
|
+
if (process.platform !== 'linux')
|
|
81
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: Linux required');
|
|
82
|
+
if (role.session !== 'acp' || role.session_options?.acp?.command)
|
|
83
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: stock ACP adapter required');
|
|
84
|
+
if (role.harness === 'claude-code') {
|
|
85
|
+
if (resolveBundledAcpAgent('@agentclientprotocol/claude-agent-acp', 'claude-agent-acp', 'claude-agent-acp').version !== '0.63.0')
|
|
86
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: qualified Claude ACP 0.63.0 required');
|
|
87
|
+
const options = role.harness_options ?? {}, servers = options.mcp_servers;
|
|
88
|
+
if (options.mcp_servers_only !== true || !servers || Object.keys(servers).length !== 1 || !servers.ours)
|
|
89
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: Claude requires explicit mcp_servers_only and a single ours connector');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (role.harness !== 'codex' || !child.OURS_FLEET_CODEX_ACP_MANIFEST || child.OURS_FLEET_CODEX_DISABLE_INHERITED_MCP === '1')
|
|
93
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: managed Codex inventory proxy required');
|
|
94
|
+
const config = JSON.parse(child.CODEX_CONFIG ?? '{}');
|
|
95
|
+
if (config.mcp_servers?.ours?.enabled === false)
|
|
96
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: ours MCP disabled');
|
|
97
|
+
// Public inventory sees effective injected config. Before replacing ours, refuse
|
|
98
|
+
// inherited TOML connector configuration, including disabled/project/profile
|
|
99
|
+
// entries. No TOML guessing or override of an operator's explicit disable.
|
|
100
|
+
const home = child.CODEX_HOME ?? join(child.HOME ?? homedir(), '.codex');
|
|
101
|
+
const paths = [join(home, 'config.toml')];
|
|
102
|
+
const profile = role.harness_options?.profile;
|
|
103
|
+
if (typeof profile === 'string')
|
|
104
|
+
paths.push(join(home, profile + '.config.toml'));
|
|
105
|
+
for (let p = resolve(cwd); p !== resolve(child.HOME ?? homedir());) {
|
|
106
|
+
paths.push(join(p, '.codex/config.toml'));
|
|
107
|
+
const next = dirname(p);
|
|
108
|
+
if (next === p)
|
|
109
|
+
break;
|
|
110
|
+
p = next;
|
|
111
|
+
}
|
|
112
|
+
for (const path of new Set(paths))
|
|
113
|
+
if (existsSync(path) && /mcp_servers|mcpServers/.test(readFileSync(path, 'utf8')))
|
|
114
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: inherited Codex MCP configuration requires inventory support before replacement');
|
|
115
|
+
child.FLEET_CURRENT_CHAT_FILES = '1';
|
|
116
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { FileDeliveryInput, DeliveredFile } from '../file-delivery/types.js';
|
|
2
|
+
import type { ToolRequestExtra } from '@ours.network/mcp/dist/types/mcp/tool.js';
|
|
1
3
|
import type { ApplicationIdentityStore } from '@ours.network/mcp/application-identities';
|
|
2
4
|
import type { OursClient } from '@ours.network/sdk/client';
|
|
5
|
+
import { FileCallbacks } from './file-rpc.js';
|
|
3
6
|
import type { AgentOursRuntime } from './runtime.js';
|
|
4
7
|
export interface EndpointOptions {
|
|
5
8
|
socket: string;
|
|
@@ -9,6 +12,8 @@ export interface EndpointOptions {
|
|
|
9
12
|
client: OursClient;
|
|
10
13
|
identities: ApplicationIdentityStore;
|
|
11
14
|
remoteDaemonFiles: boolean;
|
|
15
|
+
currentChatFileDirectory?: string;
|
|
16
|
+
currentChatFile?: (input: FileDeliveryInput, extra: ToolRequestExtra, files: FileCallbacks) => Promise<DeliveredFile>;
|
|
12
17
|
}
|
|
13
18
|
export declare function startMcpEndpoint(options: EndpointOptions): Promise<{
|
|
14
19
|
close(): Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as managedMcp from '@ours.network/mcp/server';
|
|
1
2
|
import { createServer } from 'node:net';
|
|
2
3
|
import { chmod, unlink } from 'node:fs/promises';
|
|
3
4
|
import { timingSafeEqual } from 'node:crypto';
|
|
@@ -6,6 +7,8 @@ import { JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
|
6
7
|
import { FileCallbacks } from './file-rpc.js';
|
|
7
8
|
import { Wire } from './wire.js';
|
|
8
9
|
export async function startMcpEndpoint(options) {
|
|
10
|
+
if (options.currentChatFile && managedMcp.MANAGED_FILE_DELIVERY_VERSION !== 1)
|
|
11
|
+
throw Error('INCOMPATIBLE_MANAGED_FILE_DELIVERY');
|
|
9
12
|
const sockets = new Set();
|
|
10
13
|
const server = createServer((socket) => {
|
|
11
14
|
if (sockets.size >= 4) {
|
|
@@ -33,6 +36,7 @@ export async function startMcpEndpoint(options) {
|
|
|
33
36
|
};
|
|
34
37
|
const mcp = createManagedOursMcpServer(options.client, 'managed-v1', options.identities, {
|
|
35
38
|
remoteDaemonFiles: options.remoteDaemonFiles,
|
|
39
|
+
...(options.currentChatFile ? { currentChatFileDirectory: options.currentChatFileDirectory, currentChatFile: (input, extra) => options.currentChatFile(input, extra, files) } : {}),
|
|
36
40
|
fileContext: files,
|
|
37
41
|
admit: async () => {
|
|
38
42
|
if (!authenticated || inFlight >= 32)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentSession } from '../session/types.js';
|
|
1
2
|
import type { ResolvedRole } from '../config.js';
|
|
2
3
|
import { AgentOursRuntime } from './runtime.js';
|
|
3
4
|
export declare const privateRuntimeRoot: () => string;
|
|
@@ -5,6 +6,7 @@ export interface ManagedAgentService {
|
|
|
5
6
|
runtime: AgentOursRuntime;
|
|
6
7
|
descriptor: string;
|
|
7
8
|
privatePaths: string[];
|
|
9
|
+
setFileDelivery?(sender: AgentSession['sendFileToChat']): void;
|
|
8
10
|
close(terminal: boolean): Promise<void>;
|
|
9
11
|
}
|
|
10
12
|
/** Only trusted launch orchestration may write this descriptor, never the child. */
|
|
@@ -20,4 +22,7 @@ export declare function readRoomReadiness(roomCid: string, name: string): {
|
|
|
20
22
|
generation: number;
|
|
21
23
|
} | undefined;
|
|
22
24
|
/** Explicit logical-instance termination, independent of harness/bridge transport. */
|
|
23
|
-
export declare function releaseManagedAgent(role: ResolvedRole
|
|
25
|
+
export declare function releaseManagedAgent(role: ResolvedRole, expected?: {
|
|
26
|
+
cid: string;
|
|
27
|
+
action: string;
|
|
28
|
+
}): Promise<void>;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { pinExportRoot, exportPath, ensureExportDirectory } from '../file-delivery/reader.js';
|
|
2
|
+
import { ArtifactStore } from '../file-delivery/store.js';
|
|
3
|
+
import { resolve, basename, extname } from 'node:path';
|
|
1
4
|
import { socketPath as privateSocketPath } from '../socket-path.js';
|
|
2
5
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
7
|
import { join } from 'node:path';
|
|
5
8
|
import { attachOursClient } from '@ours.network/sdk/client';
|
|
6
9
|
import { ApplicationIdentityStore } from '@ours.network/mcp/application-identities';
|
|
10
|
+
import { assertSafeAncestors } from '../rooms-tasks/workspace.js';
|
|
7
11
|
import { readClientProfile } from '../client-profile.js';
|
|
8
12
|
import { stateRoot } from '../paths.js';
|
|
9
13
|
import { createCoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
|
|
@@ -94,6 +98,8 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
94
98
|
}
|
|
95
99
|
const controller = await RuntimeController.acquire(root, profile.expectedInstanceId, role.identity, instance);
|
|
96
100
|
let closeClient;
|
|
101
|
+
let exportRoot;
|
|
102
|
+
let fileSender;
|
|
97
103
|
let partialEndpoint;
|
|
98
104
|
try {
|
|
99
105
|
let prior = controller.journal.read();
|
|
@@ -210,6 +216,18 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
210
216
|
const bridgeDir = join(stateDir, '.ours-bridge');
|
|
211
217
|
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
212
218
|
const capability = randomBytes(32).toString('hex'), socket = privateSocketPath(join(bridgeDir, `g${generation}.sock`));
|
|
219
|
+
const cwd = role.cwd && existsSync(role.cwd) ? resolve(role.cwd) : resolve(stateDir);
|
|
220
|
+
if (role.file_delivery?.enabled) {
|
|
221
|
+
if (role.session !== 'acp' || !['codex', 'claude-code'].includes(role.harness))
|
|
222
|
+
throw Error('FILE_DELIVERY_REQUIRES_CODEX_OR_CLAUDE_ACP');
|
|
223
|
+
const rootPath = resolve(cwd, role.file_delivery.directory);
|
|
224
|
+
const forbidden = [privateRuntimeRoot(), join(stateRoot(), 'private-file-delivery'), profile.configPath, profile.credentialPath];
|
|
225
|
+
if (forbidden.some(p => rootPath === resolve(p) || rootPath.startsWith(resolve(p) + '/') || resolve(p).startsWith(rootPath + '/')))
|
|
226
|
+
throw Error('PRIVATE_STATE_CANNOT_BE_EXPORTED');
|
|
227
|
+
await ensureExportDirectory(cwd, role.file_delivery.directory);
|
|
228
|
+
exportRoot = await pinExportRoot(rootPath);
|
|
229
|
+
}
|
|
230
|
+
const artifactStore = new ArtifactStore(stateDir);
|
|
213
231
|
const endpoint = await startMcpEndpoint({
|
|
214
232
|
socket,
|
|
215
233
|
capability,
|
|
@@ -218,6 +236,26 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
218
236
|
client,
|
|
219
237
|
identities,
|
|
220
238
|
remoteDaemonFiles: true,
|
|
239
|
+
...(exportRoot ? { currentChatFileDirectory: role.file_delivery.directory, currentChatFile: (async (input, extra, files) => {
|
|
240
|
+
if (!fileSender || !exportRoot)
|
|
241
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE');
|
|
242
|
+
exportPath(exportRoot.root, cwd, input.path); // validate before approval/read
|
|
243
|
+
const name = input.filename ?? basename(input.path);
|
|
244
|
+
if (!name || Buffer.byteLength(name) > 240 || /[\/\\\x00-\x1f]/.test(name) || name === '.' || name === '..')
|
|
245
|
+
throw Error('INVALID_FILENAME');
|
|
246
|
+
const mime = input.mime ?? { '.txt': 'text/plain', '.md': 'text/markdown', '.pdf': 'application/pdf', '.json': 'application/json', '.html': 'text/html', '.png': 'image/png' }[extname(name).toLowerCase()] ?? 'application/octet-stream';
|
|
247
|
+
if (mime.length > 200 || !/^[A-Za-z0-9.+-]+\/[A-Za-z0-9.+-]+$/.test(mime))
|
|
248
|
+
throw Error('INVALID_MIME');
|
|
249
|
+
return fileSender(input, extra.signal, async (binding) => {
|
|
250
|
+
const source = await files.readExport(input.path, exportRoot.root, extra);
|
|
251
|
+
try {
|
|
252
|
+
return await artifactStore.copy(source, name, mime, binding, extra.signal);
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await source.close();
|
|
256
|
+
}
|
|
257
|
+
}, role.permissions);
|
|
258
|
+
}) } : {}),
|
|
221
259
|
});
|
|
222
260
|
partialEndpoint = endpoint;
|
|
223
261
|
const descriptor = join(bridgeDir, 'descriptor.json');
|
|
@@ -225,10 +263,13 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
225
263
|
return {
|
|
226
264
|
runtime,
|
|
227
265
|
descriptor,
|
|
228
|
-
privatePaths: [root, profile.configPath, profile.credentialPath],
|
|
266
|
+
privatePaths: [root, profile.configPath, profile.credentialPath, join(stateRoot(), 'private-file-delivery')],
|
|
267
|
+
setFileDelivery: sender => { fileSender = sender; },
|
|
229
268
|
close: async (terminal) => {
|
|
230
269
|
try {
|
|
270
|
+
fileSender = undefined;
|
|
231
271
|
await endpoint.close();
|
|
272
|
+
await exportRoot?.handle.close();
|
|
232
273
|
if (terminal)
|
|
233
274
|
await runtime.terminal();
|
|
234
275
|
else
|
|
@@ -248,6 +289,7 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
248
289
|
catch (error) {
|
|
249
290
|
try {
|
|
250
291
|
await partialEndpoint?.close();
|
|
292
|
+
await exportRoot?.handle.close();
|
|
251
293
|
}
|
|
252
294
|
finally {
|
|
253
295
|
try {
|
|
@@ -318,24 +360,47 @@ export function readRoomReadiness(roomCid, name) {
|
|
|
318
360
|
}
|
|
319
361
|
}
|
|
320
362
|
/** Explicit logical-instance termination, independent of harness/bridge transport. */
|
|
321
|
-
export async function releaseManagedAgent(role) {
|
|
363
|
+
export async function releaseManagedAgent(role, expected) {
|
|
322
364
|
const profile = readClientProfile({ ...process.env, ...role.env });
|
|
323
365
|
if (!profile)
|
|
324
366
|
throw Error('MANAGED_OURS_REQUIRES_EXPLICIT_DAEMON_PROFILE');
|
|
325
367
|
const root = privateRuntimeRoot(), dir = join(root, binderKey(profile.expectedInstanceId, role.identity));
|
|
326
|
-
if (!existsSync(join(dir, 'instance.json')))
|
|
368
|
+
if (!existsSync(join(dir, 'instance.json'))) {
|
|
369
|
+
if (expected)
|
|
370
|
+
throw Error('MEMBER_RUNTIME_RETIREMENT_MISSING');
|
|
327
371
|
return;
|
|
328
|
-
|
|
372
|
+
}
|
|
373
|
+
const read = (file) => {
|
|
374
|
+
const path = join(dir, file);
|
|
375
|
+
if (expected) {
|
|
376
|
+
assertSafeAncestors(dir);
|
|
377
|
+
const stat = lstatSync(path);
|
|
378
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024)
|
|
379
|
+
throw Error('MEMBER_RUNTIME_UNSAFE_PROOF');
|
|
380
|
+
}
|
|
381
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
382
|
+
};
|
|
383
|
+
const instance = read('instance.json');
|
|
384
|
+
if (expected) {
|
|
385
|
+
read('state.json');
|
|
386
|
+
read('owner.json');
|
|
387
|
+
}
|
|
329
388
|
if (instance.role !== role.name)
|
|
330
389
|
throw Error('IDENTITY_ASSIGNED_TO_OTHER_ROLE');
|
|
331
390
|
const controller = await RuntimeController.acquire(root, profile.expectedInstanceId, role.identity, instance.instance);
|
|
332
391
|
try {
|
|
333
392
|
const state = controller.journal.read();
|
|
334
|
-
if (!state || state.
|
|
393
|
+
if (expected && (!state || state.name !== role.identity || state.daemon !== profile.expectedInstanceId
|
|
394
|
+
|| state.instance !== instance.instance || state.lifetime !== 'temporary' || instance.temporary !== true
|
|
395
|
+
|| state.cid !== expected.cid || state.action !== expected.action))
|
|
396
|
+
throw Error('MEMBER_RUNTIME_RETIREMENT_MISMATCH');
|
|
397
|
+
if (!state)
|
|
335
398
|
return;
|
|
336
|
-
const owner =
|
|
337
|
-
if (owner.instance !== state.instance)
|
|
399
|
+
const owner = read('owner.json');
|
|
400
|
+
if (owner.instance !== state.instance || typeof owner.token !== 'string' || !owner.token)
|
|
338
401
|
throw Error('OWNER_RECORD_MISMATCH');
|
|
402
|
+
if (state.phase === 'RELEASED')
|
|
403
|
+
return;
|
|
339
404
|
const client = await attachOursClient({
|
|
340
405
|
endpoint: profile.endpoint,
|
|
341
406
|
expectedInstanceId: profile.expectedInstanceId,
|
|
@@ -23,3 +23,5 @@ export declare function resumedConversationPage(stateDir: string, request: {
|
|
|
23
23
|
after?: string;
|
|
24
24
|
limit?: number;
|
|
25
25
|
}, live: ConversationPageView, expectedSessionId: string): Promise<ConversationPageView>;
|
|
26
|
+
/** Download authorization requires an actual published attachment, never an orphan copy. */
|
|
27
|
+
export declare function conversationHasAttachment(stateDir: string, attachment: import('../file-delivery/types.js').DeliveredFile): Promise<boolean>;
|
|
@@ -2,7 +2,7 @@ import { createReadStream } from 'node:fs';
|
|
|
2
2
|
import { open, readFile, readdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
|
-
const HISTORICAL_KINDS = ['fleet.task_created', 'prompt.admitted', 'message.chunk', 'message.replace', 'tool.upsert', 'tool.content_chunk', 'turn.completed'];
|
|
5
|
+
const HISTORICAL_KINDS = ['file.attached', 'fleet.task_created', 'prompt.admitted', 'message.chunk', 'message.replace', 'tool.upsert', 'tool.content_chunk', 'turn.completed'];
|
|
6
6
|
/** Whether a ledger event belongs in the conversation: the live generation, or history of the resumed ACP session. */
|
|
7
7
|
export function conversationEventVisible(event, sessionGeneration, resumedSessionId) {
|
|
8
8
|
return event.source !== 'agent_replay' && (event.sessionGeneration === sessionGeneration || (!!resumedSessionId && HISTORICAL_KINDS.includes(event.kind) && event.acpSessionId === resumedSessionId));
|
|
@@ -162,7 +162,7 @@ export async function resumedConversationPage(stateDir, request, live, expectedS
|
|
|
162
162
|
const cursor = Number(event.seq);
|
|
163
163
|
if (!Number.isSafeInteger(cursor) || cursor <= after)
|
|
164
164
|
continue;
|
|
165
|
-
const historical = ['fleet.task_created', 'prompt.admitted', 'message.chunk', 'message.replace', 'tool.upsert', 'tool.content_chunk', 'turn.completed'].includes(event.kind);
|
|
165
|
+
const historical = ['file.attached', 'fleet.task_created', 'prompt.admitted', 'message.chunk', 'message.replace', 'tool.upsert', 'tool.content_chunk', 'turn.completed'].includes(event.kind);
|
|
166
166
|
const visible = event.source !== 'agent_replay' && (event.sessionGeneration === live.snapshot.sessionGeneration || (historical && event.acpSessionId === sessionId));
|
|
167
167
|
if (!visible) {
|
|
168
168
|
next = String(cursor);
|
|
@@ -187,3 +187,26 @@ export async function resumedConversationPage(stateDir, request, live, expectedS
|
|
|
187
187
|
return live;
|
|
188
188
|
return { ...live, events, firstAvailableCursor: first, nextCursor: next, hasMore };
|
|
189
189
|
}
|
|
190
|
+
/** Download authorization requires an actual published attachment, never an orphan copy. */
|
|
191
|
+
export async function conversationHasAttachment(stateDir, attachment) {
|
|
192
|
+
const dir = join(stateDir, '.conversation');
|
|
193
|
+
const names = (await readdir(dir)).filter(n => /^events-\d+\.jsonl$/.test(n)).sort().reverse();
|
|
194
|
+
let found = false;
|
|
195
|
+
for (const name of names) {
|
|
196
|
+
await readLinesBackward(join(dir, name), line => {
|
|
197
|
+
try {
|
|
198
|
+
const e = JSON.parse(line);
|
|
199
|
+
const a = e.payload.attachment;
|
|
200
|
+
if (e.kind === 'file.attached' && e.source === 'agent' && a?.id === attachment.id
|
|
201
|
+
&& e.sessionGeneration === attachment.sessionGeneration && e.acpSessionId === attachment.acpSessionId
|
|
202
|
+
&& e.turnId === attachment.turnId && JSON.stringify(a) === JSON.stringify(attachment))
|
|
203
|
+
found = true;
|
|
204
|
+
}
|
|
205
|
+
catch { /* unreadable lines cannot authorize a download */ }
|
|
206
|
+
return !found;
|
|
207
|
+
});
|
|
208
|
+
if (found)
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
return found;
|
|
212
|
+
}
|
|
@@ -3,7 +3,6 @@ import { createHmac, randomBytes } from 'node:crypto';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { realExec } from '../exec.js';
|
|
5
5
|
import { logsRoot } from '../paths.js';
|
|
6
|
-
import { unitFor } from '../supervisor/systemd.js';
|
|
7
6
|
import { ROLE_NAME_RE } from '../config.js';
|
|
8
7
|
import { FleetError } from './errors.js';
|
|
9
8
|
const STANDARD_REDACTIONS = [
|
|
@@ -78,12 +77,13 @@ export class StructuredLogService {
|
|
|
78
77
|
}
|
|
79
78
|
async tail(roleId, requested, cursor) {
|
|
80
79
|
const limit = Math.min(Math.max(requested, 1), 1_000);
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
const source = this.backend.logsArgs(roleId, false);
|
|
81
|
+
const args = { cmd: source.cmd, args: [...source.args] };
|
|
82
|
+
const count = args.args.indexOf('-n');
|
|
83
|
+
if (count >= 0)
|
|
84
|
+
args.args[count + 1] = String(limit);
|
|
85
|
+
if (args.cmd === 'journalctl')
|
|
86
|
+
args.args.push('-o', 'json', '--no-pager', '-n', String(limit));
|
|
87
87
|
if (!['journalctl', 'tail', 'tmux'].includes(args.cmd))
|
|
88
88
|
throw new FleetError('capability_unavailable', 'log backend is unsupported');
|
|
89
89
|
const result = await this.exec(args.cmd, args.args);
|
|
@@ -93,7 +93,7 @@ export class StructuredLogService {
|
|
|
93
93
|
const parsed = lines.map((line, index) => {
|
|
94
94
|
let text = line;
|
|
95
95
|
let at;
|
|
96
|
-
if (
|
|
96
|
+
if (args.cmd === 'journalctl') {
|
|
97
97
|
try {
|
|
98
98
|
const json = JSON.parse(line);
|
|
99
99
|
text = typeof json.MESSAGE === 'string' ? json.MESSAGE : line;
|
|
@@ -59,7 +59,8 @@ export class RoleCreationService {
|
|
|
59
59
|
}
|
|
60
60
|
async createManaged(caller, requested) {
|
|
61
61
|
const plan = this.previewSpawn({ origin: 'managed', caller, options: requested });
|
|
62
|
-
const creationActionId =
|
|
62
|
+
const creationActionId = requested.roomMemberStartup?.task_id
|
|
63
|
+
? requested.creationActionId ?? randomUUID() : randomUUID();
|
|
63
64
|
plan.options.creationActionId = creationActionId;
|
|
64
65
|
const statePath = await this.launchSync(plan.options);
|
|
65
66
|
const selectionSummary = (kind) => {
|
|
@@ -3,7 +3,7 @@ import { cpSync, existsSync, mkdirSync, readdirSync, renameSync } from 'node:fs'
|
|
|
3
3
|
import { basename, dirname, join } from 'node:path';
|
|
4
4
|
import { loadConfig, ROLE_NAME_RE } from '../config.js';
|
|
5
5
|
import { agentDir, stateRoot } from '../paths.js';
|
|
6
|
-
import { rmRole } from '../ops.js';
|
|
6
|
+
import { assertStandaloneRoleRemoval, rmRole } from '../ops.js';
|
|
7
7
|
import { FleetError } from './errors.js';
|
|
8
8
|
import { provesGeneratedAgentSource } from '../generated-agent-source.js';
|
|
9
9
|
export class RoleRemovalService {
|
|
@@ -19,6 +19,12 @@ export class RoleRemovalService {
|
|
|
19
19
|
if (!ROLE_NAME_RE.test(requested))
|
|
20
20
|
throw new FleetError('invalid_request', 'invalid role name');
|
|
21
21
|
const cfg = loadConfig(this.options.configPath);
|
|
22
|
+
try {
|
|
23
|
+
assertStandaloneRoleRemoval(cfg, requested);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw new FleetError('conflict', error instanceof Error ? error.message : String(error));
|
|
27
|
+
}
|
|
22
28
|
const names = new Set(cfg.roles.map(role => role.name));
|
|
23
29
|
for (const root of [dirname(agentDir('_')), dirname(agentDir('_', true))]) {
|
|
24
30
|
try {
|
|
@@ -5,6 +5,21 @@ import { loadConfig, resolveSnapshotMonitor, resolvePermissions, ROLE_NAME_RE, }
|
|
|
5
5
|
import { agentsRoot, tmpRoot } from '../paths.js';
|
|
6
6
|
import { readSpawnLineage } from '../web/topology.js';
|
|
7
7
|
const MAX_SNAPSHOT_BYTES = 256 * 1024;
|
|
8
|
+
function retainedTaskId(dir, name) {
|
|
9
|
+
try {
|
|
10
|
+
const path = join(dir, '.temp-supervisor.json');
|
|
11
|
+
const stat = lstatSync(path);
|
|
12
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024)
|
|
13
|
+
return undefined;
|
|
14
|
+
const record = JSON.parse(readFileSync(path, 'utf8'));
|
|
15
|
+
const id = record?.taskOwner?.taskId;
|
|
16
|
+
return record?.version === 1 && record?.role === name && typeof id === 'string'
|
|
17
|
+
&& /^[0-9a-z]{9}[0-9a-f]{8}$/.test(id) ? id : undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
8
23
|
function safeDirs(root) {
|
|
9
24
|
try {
|
|
10
25
|
return readdirSync(root, { withFileTypes: true })
|
|
@@ -128,10 +143,12 @@ export class RoleRepository {
|
|
|
128
143
|
const stateRef = inTemporary ? { lifetime: 'temporary' }
|
|
129
144
|
: inPermanent ? { lifetime: 'permanent' } : undefined;
|
|
130
145
|
const tempStateDir = inTemporary ? join(temporaryRoot, name) : undefined;
|
|
146
|
+
const taskId = !config && tempStateDir ? retainedTaskId(tempStateDir, name) : undefined;
|
|
131
147
|
return {
|
|
132
148
|
id: name,
|
|
133
149
|
lifetime: config ? 'permanent' : inTemporary ? 'temporary' : 'orphan',
|
|
134
150
|
configured: Boolean(config),
|
|
151
|
+
...(taskId ? { taskId } : {}),
|
|
135
152
|
config: config ? view(config) : tempRole ? view(tempRole) : undefined,
|
|
136
153
|
stateRef,
|
|
137
154
|
stateHealth,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { socketPath } from '../socket-path.js';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
4
|
import { join } from 'node:path';
|
|
@@ -35,7 +36,8 @@ export class SupervisorOursTools {
|
|
|
35
36
|
const identity = legacy
|
|
36
37
|
? legacySupervisorIdentity(role, identityName, selected.temporary, descriptor.generation)
|
|
37
38
|
: { name: identityName, cid: descriptor.cid, generation: descriptor.generation };
|
|
38
|
-
|
|
39
|
+
const legacySocket = join(selected.dir, '.ours-bridge', `g${descriptor.generation}.sock`);
|
|
40
|
+
if (legacy && descriptor.socket !== legacySocket && descriptor.socket !== socketPath(legacySocket))
|
|
39
41
|
throw new FleetError('capability_unavailable', 'legacy supervisor socket does not match the selected agent');
|
|
40
42
|
if ((!legacy && (descriptor.role !== role || descriptor.identity !== identityName))
|
|
41
43
|
|| !/^[a-f0-9]{64}$/i.test(identity.cid ?? '') || !Number.isSafeInteger(descriptor.generation) || descriptor.generation < 1)
|