@ours.network/fleet 1.2.0-nightly.5 → 1.2.0-nightly.7
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 +40 -0
- package/dist/agent-ours/bridge.js +9 -0
- package/dist/agent-ours/service.js +21 -9
- 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 +30 -16
- package/dist/build-info.json +4 -4
- package/dist/cli.js +37 -0
- package/dist/docs.d.ts +2 -2
- package/dist/docs.js +3 -3
- package/dist/fleet-command-audit.js +2 -1
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +4 -3
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +24 -4
- package/dist/rooms-tasks/cli.js +1 -1
- package/dist/rooms-tasks/close.d.ts +1 -0
- package/dist/rooms-tasks/close.js +42 -6
- package/dist/rooms-tasks/cowork-adapter.d.ts +4 -0
- package/dist/rooms-tasks/cowork-adapter.js +1 -0
- package/dist/rooms-tasks/provision.d.ts +11 -0
- package/dist/rooms-tasks/provision.js +107 -7
- package/dist/rooms-tasks/room-state.js +6 -0
- package/dist/runner.js +22 -20
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +18 -1
- package/package.json +5 -5
- package/presets/fleet/roles/Coordinator.yaml +22 -0
package/README.md
CHANGED
|
@@ -1710,3 +1710,43 @@ Explicit Cowork socket, config or state-directory overrides continue to select
|
|
|
1710
1710
|
local Unix management. Profiles without `serverUrl` retain legacy behavior. The
|
|
1711
1711
|
HTTP API grants operator room authority; use the installer's supported private
|
|
1712
1712
|
or authenticated external gateway entry, and keep its backend ports private.
|
|
1713
|
+
|
|
1714
|
+
### Agent ours tools through the supervisor
|
|
1715
|
+
|
|
1716
|
+
Operators can invoke the running agent's ordinary ours MCP tools without sending
|
|
1717
|
+
an LLM prompt or binding another session to its identity:
|
|
1718
|
+
|
|
1719
|
+
```sh
|
|
1720
|
+
ours-fleet ours tools Critic1
|
|
1721
|
+
ours-fleet ours call Critic1 current_identity
|
|
1722
|
+
ours-fleet ours call Critic1 list_contacts
|
|
1723
|
+
ours-fleet ours call Critic1 generate_invite --args-file /private/invite-options.json
|
|
1724
|
+
ours-fleet ours call Critic1 add_contact --args-file /private/contact.json
|
|
1725
|
+
```
|
|
1726
|
+
|
|
1727
|
+
`tools` returns the managed MCP tool names and argument schemas, plus the selected
|
|
1728
|
+
agent's identity name, CID, and supervisor generation. `call` returns the same
|
|
1729
|
+
identity metadata and the MCP result. The private JSON file contains the tool's
|
|
1730
|
+
arguments (for example `{"invite":"…"}` for `add_contact`); omitted arguments mean
|
|
1731
|
+
`{}`. Protect files containing invites and protect command output containing a
|
|
1732
|
+
new invite. Tool errors set a nonzero CLI exit status.
|
|
1733
|
+
|
|
1734
|
+
The authenticated REST equivalents are `GET /api/v1/roles/:id/ours/tools` and
|
|
1735
|
+
`POST /api/v1/roles/:id/ours/call`, with the normal Fleet session and CSRF token.
|
|
1736
|
+
The POST body is `{"tool":"list_contacts","arguments":{}}`. MCP tool errors
|
|
1737
|
+
remain in `result.isError`; transport failures use the normal Fleet error envelope.
|
|
1738
|
+
Request arguments and results are excluded from the Fleet audit log.
|
|
1739
|
+
|
|
1740
|
+
Both interfaces use the supervisor's existing fixed-identity MCP server and tool
|
|
1741
|
+
policy. Identity creation, removal, switching, and binding are not exposed. The
|
|
1742
|
+
supervisor must be running. Older descriptors are supported only when a unique
|
|
1743
|
+
existing runtime journal, instance record, and identity pin prove the selected
|
|
1744
|
+
agent and generation, and the same MCP connection confirms its identity and
|
|
1745
|
+
lifetime before the operation. This reads existing state without rewriting the
|
|
1746
|
+
descriptor or restarting the agent. Missing, ambiguous, stale, or mismatched
|
|
1747
|
+
proofs fail closed. Unknown identity descriptions also fail closed. Closing an operator call
|
|
1748
|
+
retains the supervisor's identity. An interrupted mutation can have an unknown
|
|
1749
|
+
outcome: inspect state before retrying, because there is no automatic retry or
|
|
1750
|
+
exactly-once guarantee. Contact acceptance alone does not prove peer verification
|
|
1751
|
+
or message delivery. File tools resolve paths in the invoking CLI process or
|
|
1752
|
+
Fleet web server's filesystem context, with that process's access permissions.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { connect } from 'node:net';
|
|
3
4
|
import { access, mkdir, open, readFile } from 'node:fs/promises';
|
|
4
5
|
import { constants } from 'node:fs';
|
|
@@ -12,6 +13,14 @@ export async function runBridge(descriptorPath) {
|
|
|
12
13
|
typeof descriptor.capability !== 'string' ||
|
|
13
14
|
!Number.isInteger(descriptor.generation))
|
|
14
15
|
throw Error('INVALID_BRIDGE_DESCRIPTOR');
|
|
16
|
+
if (process.env.FLEET_OURS_BRIDGE_EXPECTED) {
|
|
17
|
+
const expected = JSON.parse(process.env.FLEET_OURS_BRIDGE_EXPECTED);
|
|
18
|
+
const digest = createHash('sha256').update(JSON.stringify([descriptor.socket, descriptor.capability, descriptor.generation])).digest('hex');
|
|
19
|
+
if (expected.transportDigest !== digest)
|
|
20
|
+
throw Error('SUPERVISOR_TRANSPORT_CHANGED');
|
|
21
|
+
if (['role', 'identity', 'cid', 'generation'].some(key => descriptor[key] !== expected[key]))
|
|
22
|
+
throw Error('SUPERVISOR_SELECTION_CHANGED');
|
|
23
|
+
}
|
|
15
24
|
const wire = new Wire(connect(descriptor.socket));
|
|
16
25
|
const handles = new Map();
|
|
17
26
|
const cleanup = async () => {
|
|
@@ -11,19 +11,31 @@ import { atomicPrivateWrite, binderKey } from './state.js';
|
|
|
11
11
|
import { AgentOursRuntime } from './runtime.js';
|
|
12
12
|
import { startMcpEndpoint } from './mcp-endpoint.js';
|
|
13
13
|
export const privateRuntimeRoot = () => join(stateRoot(), 'private-ours');
|
|
14
|
+
function roomSecretPath(role) {
|
|
15
|
+
const startup = role.roomMemberStartup;
|
|
16
|
+
return join(privateRuntimeRoot(), 'room-inputs', binderKey(startup.room_identity_cid, JSON.stringify([role.name, startup.invite_id])) + '.json');
|
|
17
|
+
}
|
|
18
|
+
function matchesRoomSecret(secret, role) {
|
|
19
|
+
const startup = role.roomMemberStartup;
|
|
20
|
+
return secret.room_id === startup.room_id && secret.room_identity_cid === startup.room_identity_cid
|
|
21
|
+
&& secret.invite_id === startup.invite_id && secret.identity_name === role.identity
|
|
22
|
+
&& secret.role === startup.role;
|
|
23
|
+
}
|
|
14
24
|
/** Only trusted launch orchestration may write this descriptor, never the child. */
|
|
15
25
|
export function storeRoomSecret(role) {
|
|
16
26
|
const startup = role.roomMemberStartup;
|
|
17
27
|
if (!startup?.invite)
|
|
18
28
|
return;
|
|
29
|
+
if (startup.identity_name !== role.identity)
|
|
30
|
+
throw Error('ROOM_SECRET_MISMATCH');
|
|
19
31
|
const root = join(privateRuntimeRoot(), 'room-inputs');
|
|
20
32
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
21
|
-
|
|
33
|
+
// Failed attempts retain private evidence. A new invite must never overwrite
|
|
34
|
+
// or consume a previous attempt's descriptor.
|
|
35
|
+
const path = roomSecretPath(role);
|
|
22
36
|
if (existsSync(path)) {
|
|
23
37
|
const old = JSON.parse(readFileSync(path, 'utf8'));
|
|
24
|
-
if (old.
|
|
25
|
-
old.invite_id !== startup.invite_id ||
|
|
26
|
-
old.identity_name !== startup.identity_name)
|
|
38
|
+
if (!matchesRoomSecret(old, role) || old.invite !== startup.invite)
|
|
27
39
|
throw Error('ROOM_SECRET_COLLISION');
|
|
28
40
|
return;
|
|
29
41
|
}
|
|
@@ -144,7 +156,9 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
144
156
|
let room;
|
|
145
157
|
if (role.roomMemberStartup) {
|
|
146
158
|
const startup = role.roomMemberStartup;
|
|
147
|
-
const
|
|
159
|
+
const legacyPath = join(root, 'room-inputs', binderKey(startup.room_identity_cid, role.name) + '.json');
|
|
160
|
+
const currentPath = roomSecretPath(role);
|
|
161
|
+
const path = existsSync(currentPath) ? currentPath : legacyPath;
|
|
148
162
|
const cowork = createCoworkAdapter();
|
|
149
163
|
room = {
|
|
150
164
|
id: startup.room_id,
|
|
@@ -153,9 +167,7 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
153
167
|
action: startup.invite_id,
|
|
154
168
|
redeem: async (attached) => {
|
|
155
169
|
const secret = JSON.parse(readFileSync(path, 'utf8'));
|
|
156
|
-
if (secret
|
|
157
|
-
secret.invite_id !== startup.invite_id ||
|
|
158
|
-
secret.identity_name !== role.identity)
|
|
170
|
+
if (!matchesRoomSecret(secret, role))
|
|
159
171
|
throw Error('ROOM_SECRET_MISMATCH');
|
|
160
172
|
return attached.addContact({ invite: secret.invite });
|
|
161
173
|
},
|
|
@@ -207,7 +219,7 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
207
219
|
});
|
|
208
220
|
partialEndpoint = endpoint;
|
|
209
221
|
const descriptor = join(bridgeDir, 'descriptor.json');
|
|
210
|
-
atomicPrivateWrite(descriptor, { socket, capability, generation });
|
|
222
|
+
atomicPrivateWrite(descriptor, { socket, capability, generation, role: role.name, identity: role.identity, cid: runtime.snapshot.cid });
|
|
211
223
|
return {
|
|
212
224
|
runtime,
|
|
213
225
|
descriptor,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Read-only compatibility proof for supervisors predating descriptor identity metadata. */
|
|
2
|
+
export declare function legacySupervisorIdentity(role: string, name: string, temporary: boolean, generation: number): {
|
|
3
|
+
name: string;
|
|
4
|
+
cid: string;
|
|
5
|
+
generation: number;
|
|
6
|
+
proof: string;
|
|
7
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { stateRoot } from '../paths.js';
|
|
5
|
+
import { binderKey } from '../agent-ours/state.js';
|
|
6
|
+
import { FleetError } from './errors.js';
|
|
7
|
+
/** Read-only compatibility proof for supervisors predating descriptor identity metadata. */
|
|
8
|
+
export function legacySupervisorIdentity(role, name, temporary, generation) {
|
|
9
|
+
const root = join(stateRoot(), 'private-ours');
|
|
10
|
+
try {
|
|
11
|
+
const matches = [];
|
|
12
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
13
|
+
if (!entry.isDirectory() || !/^[a-f0-9]{64}$/.test(entry.name))
|
|
14
|
+
continue;
|
|
15
|
+
const dir = join(root, entry.name);
|
|
16
|
+
const instance = JSON.parse(readFileSync(join(dir, 'instance.json'), 'utf8'));
|
|
17
|
+
if (instance.role !== role)
|
|
18
|
+
continue;
|
|
19
|
+
const state = JSON.parse(readFileSync(join(dir, 'state.json'), 'utf8'));
|
|
20
|
+
const pin = JSON.parse(readFileSync(join(dir, 'identity-pin.json'), 'utf8'));
|
|
21
|
+
if (!Number.isSafeInteger(generation) || generation < 1
|
|
22
|
+
|| typeof state.action !== 'string' || !state.action
|
|
23
|
+
|| instance.temporary !== temporary || typeof instance.instance !== 'string' || !instance.instance
|
|
24
|
+
|| state.version !== 1 || state.instance !== instance.instance
|
|
25
|
+
|| state.name !== name || state.lifetime !== (temporary ? 'temporary' : 'permanent')
|
|
26
|
+
|| state.generation !== generation || !['READY', 'SERVING'].includes(state.phase)
|
|
27
|
+
|| typeof state.daemon !== 'string' || !state.daemon || binderKey(state.daemon, name) !== entry.name
|
|
28
|
+
|| !/^[a-f0-9]{64}$/i.test(state.cid ?? '')
|
|
29
|
+
|| pin.daemon !== state.daemon || pin.name !== name || pin.cid !== state.cid)
|
|
30
|
+
throw new Error('mismatched legacy ownership proof');
|
|
31
|
+
matches.push({ name, cid: state.cid, generation, proof: createHash('sha256').update(JSON.stringify([
|
|
32
|
+
instance.instance, state.action, state.daemon, name, state.cid, state.lifetime, generation,
|
|
33
|
+
])).digest('hex') });
|
|
34
|
+
}
|
|
35
|
+
if (matches.length !== 1)
|
|
36
|
+
throw new Error('missing or ambiguous legacy ownership proof');
|
|
37
|
+
return matches[0];
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// The private directory also contains ownership material: never echo parse errors.
|
|
41
|
+
throw new FleetError('capability_unavailable', 'legacy supervisor ownership proof is missing, ambiguous, or mismatched');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export interface SupervisorToolRequest {
|
|
2
|
+
tool: string;
|
|
3
|
+
arguments?: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
/** CLI and REST use the same fixed-identity MCP server as the agent harness. */
|
|
6
|
+
export declare class SupervisorOursTools {
|
|
7
|
+
private withClient;
|
|
8
|
+
list(role: string): Promise<{
|
|
9
|
+
tools: {
|
|
10
|
+
inputSchema: {
|
|
11
|
+
[x: string]: unknown;
|
|
12
|
+
type: "object";
|
|
13
|
+
properties?: Record<string, object> | undefined;
|
|
14
|
+
required?: string[] | undefined;
|
|
15
|
+
};
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string | undefined;
|
|
18
|
+
outputSchema?: {
|
|
19
|
+
[x: string]: unknown;
|
|
20
|
+
type: "object";
|
|
21
|
+
properties?: Record<string, object> | undefined;
|
|
22
|
+
required?: string[] | undefined;
|
|
23
|
+
} | undefined;
|
|
24
|
+
annotations?: {
|
|
25
|
+
title?: string | undefined;
|
|
26
|
+
readOnlyHint?: boolean | undefined;
|
|
27
|
+
destructiveHint?: boolean | undefined;
|
|
28
|
+
idempotentHint?: boolean | undefined;
|
|
29
|
+
openWorldHint?: boolean | undefined;
|
|
30
|
+
} | undefined;
|
|
31
|
+
execution?: {
|
|
32
|
+
taskSupport?: "optional" | "required" | "forbidden" | undefined;
|
|
33
|
+
} | undefined;
|
|
34
|
+
_meta?: Record<string, unknown> | undefined;
|
|
35
|
+
icons?: {
|
|
36
|
+
src: string;
|
|
37
|
+
mimeType?: string | undefined;
|
|
38
|
+
sizes?: string[] | undefined;
|
|
39
|
+
theme?: "light" | "dark" | undefined;
|
|
40
|
+
}[] | undefined;
|
|
41
|
+
title?: string | undefined;
|
|
42
|
+
}[];
|
|
43
|
+
_meta?: {
|
|
44
|
+
[x: string]: unknown;
|
|
45
|
+
progressToken?: string | number | undefined;
|
|
46
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
47
|
+
taskId: string;
|
|
48
|
+
} | undefined;
|
|
49
|
+
} | undefined;
|
|
50
|
+
nextCursor?: string | undefined;
|
|
51
|
+
agent: string;
|
|
52
|
+
identity: {
|
|
53
|
+
name: string;
|
|
54
|
+
cid: string;
|
|
55
|
+
generation: number;
|
|
56
|
+
};
|
|
57
|
+
}>;
|
|
58
|
+
call(role: string, request: SupervisorToolRequest): Promise<{
|
|
59
|
+
agent: string;
|
|
60
|
+
identity: {
|
|
61
|
+
name: string;
|
|
62
|
+
cid: string;
|
|
63
|
+
generation: number;
|
|
64
|
+
};
|
|
65
|
+
result: {
|
|
66
|
+
[x: string]: unknown;
|
|
67
|
+
content: ({
|
|
68
|
+
type: "text";
|
|
69
|
+
text: string;
|
|
70
|
+
annotations?: {
|
|
71
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
72
|
+
priority?: number | undefined;
|
|
73
|
+
lastModified?: string | undefined;
|
|
74
|
+
} | undefined;
|
|
75
|
+
_meta?: Record<string, unknown> | undefined;
|
|
76
|
+
} | {
|
|
77
|
+
type: "image";
|
|
78
|
+
data: string;
|
|
79
|
+
mimeType: string;
|
|
80
|
+
annotations?: {
|
|
81
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
82
|
+
priority?: number | undefined;
|
|
83
|
+
lastModified?: string | undefined;
|
|
84
|
+
} | undefined;
|
|
85
|
+
_meta?: Record<string, unknown> | undefined;
|
|
86
|
+
} | {
|
|
87
|
+
type: "audio";
|
|
88
|
+
data: string;
|
|
89
|
+
mimeType: string;
|
|
90
|
+
annotations?: {
|
|
91
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
92
|
+
priority?: number | undefined;
|
|
93
|
+
lastModified?: string | undefined;
|
|
94
|
+
} | undefined;
|
|
95
|
+
_meta?: Record<string, unknown> | undefined;
|
|
96
|
+
} | {
|
|
97
|
+
type: "resource";
|
|
98
|
+
resource: {
|
|
99
|
+
uri: string;
|
|
100
|
+
text: string;
|
|
101
|
+
mimeType?: string | undefined;
|
|
102
|
+
_meta?: Record<string, unknown> | undefined;
|
|
103
|
+
} | {
|
|
104
|
+
uri: string;
|
|
105
|
+
blob: string;
|
|
106
|
+
mimeType?: string | undefined;
|
|
107
|
+
_meta?: Record<string, unknown> | undefined;
|
|
108
|
+
};
|
|
109
|
+
annotations?: {
|
|
110
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
111
|
+
priority?: number | undefined;
|
|
112
|
+
lastModified?: string | undefined;
|
|
113
|
+
} | undefined;
|
|
114
|
+
_meta?: Record<string, unknown> | undefined;
|
|
115
|
+
} | {
|
|
116
|
+
uri: string;
|
|
117
|
+
name: string;
|
|
118
|
+
type: "resource_link";
|
|
119
|
+
description?: string | undefined;
|
|
120
|
+
mimeType?: string | undefined;
|
|
121
|
+
size?: number | undefined;
|
|
122
|
+
annotations?: {
|
|
123
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
124
|
+
priority?: number | undefined;
|
|
125
|
+
lastModified?: string | undefined;
|
|
126
|
+
} | undefined;
|
|
127
|
+
_meta?: {
|
|
128
|
+
[x: string]: unknown;
|
|
129
|
+
} | undefined;
|
|
130
|
+
icons?: {
|
|
131
|
+
src: string;
|
|
132
|
+
mimeType?: string | undefined;
|
|
133
|
+
sizes?: string[] | undefined;
|
|
134
|
+
theme?: "light" | "dark" | undefined;
|
|
135
|
+
}[] | undefined;
|
|
136
|
+
title?: string | undefined;
|
|
137
|
+
})[];
|
|
138
|
+
_meta?: {
|
|
139
|
+
[x: string]: unknown;
|
|
140
|
+
progressToken?: string | number | undefined;
|
|
141
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
142
|
+
taskId: string;
|
|
143
|
+
} | undefined;
|
|
144
|
+
} | undefined;
|
|
145
|
+
structuredContent?: Record<string, unknown> | undefined;
|
|
146
|
+
isError?: boolean | undefined;
|
|
147
|
+
} | {
|
|
148
|
+
[x: string]: unknown;
|
|
149
|
+
toolResult: unknown;
|
|
150
|
+
_meta?: {
|
|
151
|
+
[x: string]: unknown;
|
|
152
|
+
progressToken?: string | number | undefined;
|
|
153
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
154
|
+
taskId: string;
|
|
155
|
+
} | undefined;
|
|
156
|
+
} | undefined;
|
|
157
|
+
};
|
|
158
|
+
}>;
|
|
159
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
6
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
7
|
+
import { agentDir } from '../paths.js';
|
|
8
|
+
import { ROLE_NAME_RE } from '../config.js';
|
|
9
|
+
import { legacySupervisorIdentity } from './legacy-supervisor-identity.js';
|
|
10
|
+
import { FleetError } from './errors.js';
|
|
11
|
+
/** CLI and REST use the same fixed-identity MCP server as the agent harness. */
|
|
12
|
+
export class SupervisorOursTools {
|
|
13
|
+
async withClient(role, work) {
|
|
14
|
+
if (!ROLE_NAME_RE.test(role))
|
|
15
|
+
throw new FleetError('invalid_request', 'invalid agent name');
|
|
16
|
+
const findCandidates = () => [false, true].map(temporary => ({ temporary, dir: agentDir(role, temporary) }))
|
|
17
|
+
.map(row => ({ ...row, path: join(row.dir, '.ours-bridge', 'descriptor.json') }))
|
|
18
|
+
.filter(row => existsSync(row.path));
|
|
19
|
+
const candidates = findCandidates();
|
|
20
|
+
if (candidates.length !== 1)
|
|
21
|
+
throw new FleetError('capability_unavailable', 'agent supervisor endpoint is missing or ambiguous');
|
|
22
|
+
const selected = candidates[0];
|
|
23
|
+
let descriptor;
|
|
24
|
+
let identityName;
|
|
25
|
+
try {
|
|
26
|
+
descriptor = JSON.parse(readFileSync(selected.path, 'utf8'));
|
|
27
|
+
identityName = readFileSync(join(selected.dir, '.identity'), 'utf8').trim();
|
|
28
|
+
if (!descriptor || typeof descriptor !== 'object')
|
|
29
|
+
throw new Error();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new FleetError('capability_unavailable', 'supervisor identity metadata cannot be read');
|
|
33
|
+
}
|
|
34
|
+
const legacy = ['role', 'identity', 'cid'].every(key => descriptor[key] === undefined);
|
|
35
|
+
const identity = legacy
|
|
36
|
+
? legacySupervisorIdentity(role, identityName, selected.temporary, descriptor.generation)
|
|
37
|
+
: { name: identityName, cid: descriptor.cid, generation: descriptor.generation };
|
|
38
|
+
if (legacy && descriptor.socket !== join(selected.dir, '.ours-bridge', `g${descriptor.generation}.sock`))
|
|
39
|
+
throw new FleetError('capability_unavailable', 'legacy supervisor socket does not match the selected agent');
|
|
40
|
+
if ((!legacy && (descriptor.role !== role || descriptor.identity !== identityName))
|
|
41
|
+
|| !/^[a-f0-9]{64}$/i.test(identity.cid ?? '') || !Number.isSafeInteger(descriptor.generation) || descriptor.generation < 1)
|
|
42
|
+
throw new FleetError('capability_unavailable', 'supervisor identity metadata is unavailable or mismatched');
|
|
43
|
+
const verifySelection = () => {
|
|
44
|
+
try {
|
|
45
|
+
const current = findCandidates();
|
|
46
|
+
const freshDescriptor = JSON.parse(readFileSync(selected.path, 'utf8'));
|
|
47
|
+
if (current.length !== 1 || current[0].path !== selected.path
|
|
48
|
+
|| readFileSync(join(selected.dir, '.identity'), 'utf8').trim() !== identityName
|
|
49
|
+
|| ['role', 'identity', 'cid', 'socket', 'capability', 'generation']
|
|
50
|
+
.some(key => freshDescriptor[key] !== descriptor[key]))
|
|
51
|
+
throw new Error();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new FleetError('capability_unavailable', 'selected supervisor assignment changed');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const client = new Client({ name: 'ours-fleet-supervisor-tools', version: '1' });
|
|
58
|
+
const transport = new StdioClientTransport({
|
|
59
|
+
command: process.execPath,
|
|
60
|
+
args: [fileURLToPath(new URL('../agent-ours/bridge.js', import.meta.url))],
|
|
61
|
+
env: { ...Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined)),
|
|
62
|
+
FLEET_OURS_BRIDGE_DESCRIPTOR: selected.path,
|
|
63
|
+
FLEET_OURS_BRIDGE_EXPECTED: JSON.stringify({ role: descriptor.role, identity: descriptor.identity, cid: descriptor.cid, generation: descriptor.generation,
|
|
64
|
+
transportDigest: createHash('sha256').update(JSON.stringify([descriptor.socket, descriptor.capability, descriptor.generation])).digest('hex') }) },
|
|
65
|
+
stderr: 'pipe',
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
await client.connect(transport);
|
|
69
|
+
const verify = async () => {
|
|
70
|
+
if (!legacy) {
|
|
71
|
+
verifySelection();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const actual = await client.callTool({ name: 'current_identity', arguments: {} });
|
|
75
|
+
const blocks = actual.content;
|
|
76
|
+
const lines = blocks?.length === 1 && blocks[0].type === 'text' ? blocks[0].text?.split('\n') : undefined;
|
|
77
|
+
const prefix = `Bound to "${identity.name}" (${identity.cid})`;
|
|
78
|
+
const suffix = lines?.[0].startsWith(prefix) ? lines[0].slice(prefix.length) : undefined;
|
|
79
|
+
const roleSuffix = typeof suffix === 'string' && /^ — role "[^"\r\n]+" under root "[^"\r\n]+"\.$/.test(suffix);
|
|
80
|
+
const temporaryLine = 'TEMPORARY identity owned by the Fleet supervisor for this logical agent instance. Bridge or harness disconnect retains it; terminal supervisor release deletes local state with best-effort peer notices.';
|
|
81
|
+
if (actual.isError || !(roleSuffix || (selected.temporary && suffix === '.'))
|
|
82
|
+
|| (selected.temporary ? lines?.[1] !== temporaryLine : lines?.[1]?.startsWith('TEMPORARY')))
|
|
83
|
+
throw new FleetError('capability_unavailable', 'connected supervisor identity does not match legacy ownership proof');
|
|
84
|
+
const fresh = legacySupervisorIdentity(role, identityName, selected.temporary, descriptor.generation);
|
|
85
|
+
if (!('proof' in identity) || fresh.proof !== identity.proof)
|
|
86
|
+
throw new FleetError('capability_unavailable', 'legacy supervisor identity changed');
|
|
87
|
+
verifySelection();
|
|
88
|
+
};
|
|
89
|
+
await verify();
|
|
90
|
+
return await work(client, { name: identity.name, cid: identity.cid, generation: identity.generation }, verify);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof FleetError)
|
|
94
|
+
throw error;
|
|
95
|
+
// Do not leak arguments, invites, capabilities, or raw transport errors.
|
|
96
|
+
throw new FleetError('control_unavailable', 'supervisor tool request failed; its outcome may be unknown, do not retry a mutation automatically', { retryable: false });
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await client.close().catch(() => { });
|
|
100
|
+
await transport.close().catch(() => { });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
list(role) {
|
|
104
|
+
return this.withClient(role, async (client, identity) => ({
|
|
105
|
+
agent: role, identity, ...(await client.listTools()),
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
call(role, request) {
|
|
109
|
+
if (!request || typeof request.tool !== 'string'
|
|
110
|
+
|| (request.arguments !== undefined && (!request.arguments || typeof request.arguments !== 'object' || Array.isArray(request.arguments))))
|
|
111
|
+
throw new FleetError('invalid_request', 'tool and object arguments are required');
|
|
112
|
+
return this.withClient(role, async (client, identity, verify) => {
|
|
113
|
+
const { tools } = await client.listTools();
|
|
114
|
+
if (!tools.some(tool => tool.name === request.tool))
|
|
115
|
+
throw new FleetError('forbidden', 'tool is not exposed by the fixed-identity supervisor');
|
|
116
|
+
await verify();
|
|
117
|
+
const result = await client.callTool({ name: request.tool, arguments: request.arguments ?? {} });
|
|
118
|
+
return { agent: role, identity, result };
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -136,7 +136,7 @@ export class TaskRoomApplicationService {
|
|
|
136
136
|
}
|
|
137
137
|
catch (error) {
|
|
138
138
|
if (error instanceof CoworkUnavailableError)
|
|
139
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
139
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
140
140
|
// Once the durable Room exists, provisioning errors are resumable
|
|
141
141
|
// saga state. Return that explicit state so the command can launch a
|
|
142
142
|
// continuation and report the durable in-progress outcome.
|
|
@@ -234,17 +234,23 @@ export class TaskRoomApplicationService {
|
|
|
234
234
|
const ready = task.state === 'active' && room?.state === 'active'
|
|
235
235
|
&& active === expected && launched === expected;
|
|
236
236
|
const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
|
|
237
|
-
const nextAction =
|
|
238
|
-
? `
|
|
239
|
-
: room?.
|
|
240
|
-
? `
|
|
241
|
-
: room?.provisioning_detail === '
|
|
242
|
-
? `
|
|
243
|
-
: room?.provisioning_detail === '
|
|
244
|
-
? `
|
|
245
|
-
:
|
|
246
|
-
? `
|
|
247
|
-
:
|
|
237
|
+
const nextAction = task.terminal_intent
|
|
238
|
+
? `Complete the accepted ${task.terminal_intent.kind} operation; do not restart provisioning.`
|
|
239
|
+
: room?.state === 'closing' || room?.state === 'closed'
|
|
240
|
+
? `Complete room cleanup; do not restart provisioning.`
|
|
241
|
+
: room?.provisioning_detail === 'member_failed'
|
|
242
|
+
? `Inspect the failed launch, then run ours-fleet task start ${task.task_id}.`
|
|
243
|
+
: room?.provisioning_detail === 'waiting_owner_authorization'
|
|
244
|
+
? `Ensure ours-cowork 1.3.0 or newer is running and available, then run ours-fleet task start ${task.task_id}.`
|
|
245
|
+
: room?.provisioning_detail === 'waiting_owner_invite'
|
|
246
|
+
? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
|
|
247
|
+
: room?.provisioning_detail === 'owner_cid_mismatch'
|
|
248
|
+
? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
|
|
249
|
+
: room?.provisioning_detail === 'waiting_cowork'
|
|
250
|
+
? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
|
|
251
|
+
: failed
|
|
252
|
+
? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
|
|
253
|
+
: undefined;
|
|
248
254
|
return {
|
|
249
255
|
kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
|
|
250
256
|
launch: {
|
|
@@ -410,7 +416,9 @@ export class TaskRoomApplicationService {
|
|
|
410
416
|
let task = readTask(input.taskId);
|
|
411
417
|
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
412
418
|
const issues = [];
|
|
413
|
-
if (task.state !== 'provisioning'
|
|
419
|
+
if (task.state !== 'provisioning' || task.terminal_intent
|
|
420
|
+
|| room?.state === 'closing' || room?.state === 'closed'
|
|
421
|
+
|| room?.provisioning_detail === 'member_failed')
|
|
414
422
|
return {
|
|
415
423
|
kind: 'no_op', task, room, issues,
|
|
416
424
|
};
|
|
@@ -784,7 +792,7 @@ export class TaskRoomApplicationService {
|
|
|
784
792
|
}
|
|
785
793
|
catch (error) {
|
|
786
794
|
if (error instanceof CoworkUnavailableError)
|
|
787
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
795
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
788
796
|
const current = readTask(task.task_id);
|
|
789
797
|
if (!current.room_id || !getRoomRecord(current.room_id))
|
|
790
798
|
throw error;
|
|
@@ -802,7 +810,7 @@ export class TaskRoomApplicationService {
|
|
|
802
810
|
}
|
|
803
811
|
catch (error) {
|
|
804
812
|
if (error instanceof CoworkUnavailableError)
|
|
805
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
813
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
806
814
|
task = readTask(task.task_id);
|
|
807
815
|
return { task, status: 'in_progress' };
|
|
808
816
|
}
|
|
@@ -820,7 +828,7 @@ export class TaskRoomApplicationService {
|
|
|
820
828
|
}
|
|
821
829
|
catch (error) {
|
|
822
830
|
if (error instanceof CoworkUnavailableError)
|
|
823
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
831
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
824
832
|
task = readTask(task.task_id);
|
|
825
833
|
}
|
|
826
834
|
}
|
|
@@ -878,11 +886,17 @@ export class TaskRoomApplicationService {
|
|
|
878
886
|
if (fresh.deletion?.status === 'pending')
|
|
879
887
|
throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
|
|
880
888
|
}
|
|
889
|
+
const requiredRoles = new Map();
|
|
890
|
+
if (attachOwner)
|
|
891
|
+
requiredRoles.set(rooms.owner.role, 1);
|
|
892
|
+
for (const member of launchTemplate?.members ?? [])
|
|
893
|
+
requiredRoles.set(member.role, (requiredRoles.get(member.role) ?? 0) + member.count);
|
|
881
894
|
const created = await cowork.createRoom({
|
|
882
895
|
room_name: roomName, goal: task.goal?.trim() || task.title,
|
|
883
896
|
briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
|
|
884
897
|
quiet_membership: launchTemplate?.room?.quiet_membership,
|
|
885
898
|
anonymous: policy.anonymous,
|
|
899
|
+
activation_requirements: [...requiredRoles].map(([role, count]) => ({ role, count })),
|
|
886
900
|
});
|
|
887
901
|
const record = createRoomRecord({
|
|
888
902
|
room_id: created.room_id, room_name: roomName, room_identity_cid: created.identity_cid,
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.7",
|
|
3
|
+
"buildId": "f5cfd3c1f10c",
|
|
4
|
+
"commit": "a5d6f736733fa419e6acf7306affb4a662261bbf",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-09-
|
|
6
|
+
"builtAt": "2026-09-23T20:14:37.547Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"cowork.http-management-v1",
|
|
9
9
|
"monitor.interrupt.after_tool"
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { SupervisorOursTools } from './application/supervisor-ours-tools.js';
|
|
2
3
|
import { runTempSupervisor, TEMP_RECYCLE_EXIT } from './temp-supervisor-recovery.js';
|
|
3
4
|
import { spawn as spawnChild } from 'node:child_process';
|
|
4
5
|
import { randomUUID } from 'node:crypto';
|
|
@@ -496,6 +497,40 @@ program.command('peek <name> [lines]').description('pane snapshot without attach
|
|
|
496
497
|
die(controlFailure(name, 'peek', e));
|
|
497
498
|
}
|
|
498
499
|
});
|
|
500
|
+
const oursToolsCommand = program.command('ours').description('invoke tools through a named agent supervisor and its fixed identity');
|
|
501
|
+
oursToolsCommand.command('tools <agent>').description('list the same tools and schemas exposed to the agent MCP')
|
|
502
|
+
.action(async (agent) => {
|
|
503
|
+
try {
|
|
504
|
+
console.log(JSON.stringify(await new SupervisorOursTools().list(agent), null, 2));
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
die(error);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
oursToolsCommand.command('call <agent> <tool>').description('call a supervisor MCP tool without prompting the agent')
|
|
511
|
+
.option('--args-file <path>', 'JSON object arguments from a private file; omitted means {}')
|
|
512
|
+
.action(async (agent, tool, options) => {
|
|
513
|
+
try {
|
|
514
|
+
let args = {};
|
|
515
|
+
if (options.argsFile) {
|
|
516
|
+
try {
|
|
517
|
+
args = JSON.parse(readFileSync(options.argsFile, 'utf8'));
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
throw new Error('cannot read tool arguments: expected a readable JSON object file');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const response = await new SupervisorOursTools().call(agent, { tool, arguments: args });
|
|
524
|
+
console.log(JSON.stringify(response, null, 2));
|
|
525
|
+
if ('isError' in response.result && response.result.isError)
|
|
526
|
+
throw new FleetCliExit(1, 'runtime', 'unknown');
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
if (error instanceof FleetCliExit)
|
|
530
|
+
throw error;
|
|
531
|
+
die(error);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
499
534
|
program.command('send <name> [text...]').description("type into the agent's console")
|
|
500
535
|
.action(async (name, text, opts) => {
|
|
501
536
|
const stateDir = acpStateDir(name);
|
|
@@ -1612,6 +1647,8 @@ async function parseFleetCli() {
|
|
|
1612
1647
|
await program.parseAsync(process.argv);
|
|
1613
1648
|
}
|
|
1614
1649
|
catch (error) {
|
|
1650
|
+
if (error instanceof FleetCliExit)
|
|
1651
|
+
throw error;
|
|
1615
1652
|
const commander = error;
|
|
1616
1653
|
if (commander.exitCode === 0)
|
|
1617
1654
|
return;
|
package/dist/docs.d.ts
CHANGED
|
@@ -17,8 +17,8 @@ export declare const AI_DOCS = "# ours-fleet reference\n\nours-fleet runs persis
|
|
|
17
17
|
* \`forbidden\` is the more important half. The old skills prescribed
|
|
18
18
|
* \`--approval ask --filesystem workspace --unattended deny\` as a blanket
|
|
19
19
|
* default while also telling the agent to stop at a failed doctor check — and
|
|
20
|
-
* that combination is exactly what \`doctor\` FAILS, because \`ask\`
|
|
21
|
-
* unattended
|
|
20
|
+
* that combination is exactly what \`doctor\` FAILS, because \`ask\` cannot
|
|
21
|
+
* guarantee unattended capabilities and \`deny\` makes the shortfall
|
|
22
22
|
* fatal. Following the skill produced a role the CLI then refused.
|
|
23
23
|
*/
|
|
24
24
|
export declare const SPAWN_SKILL_CONTRACT: {
|