@ours.network/fleet 1.2.0-nightly.6 → 1.2.0-nightly.8
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 +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/build-info.json +4 -4
- package/dist/cli.js +37 -0
- package/dist/fleet-command-audit.js +2 -1
- package/dist/rooms-tasks/archived-absence.d.ts +4 -0
- package/dist/rooms-tasks/archived-absence.js +37 -0
- package/dist/rooms-tasks/close.d.ts +1 -0
- package/dist/rooms-tasks/close.js +23 -2
- package/dist/rooms-tasks/deletion.js +28 -2
- package/dist/rooms-tasks/task-state.d.ts +9 -3
- package/dist/rooms-tasks/task-state.js +18 -4
- package/dist/rooms-tasks/types.d.ts +8 -0
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +18 -1
- package/package.json +1 -1
- 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 () => {
|
|
@@ -219,7 +219,7 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
219
219
|
});
|
|
220
220
|
partialEndpoint = endpoint;
|
|
221
221
|
const descriptor = join(bridgeDir, 'descriptor.json');
|
|
222
|
-
atomicPrivateWrite(descriptor, { socket, capability, generation });
|
|
222
|
+
atomicPrivateWrite(descriptor, { socket, capability, generation, role: role.name, identity: role.identity, cid: runtime.snapshot.cid });
|
|
223
223
|
return {
|
|
224
224
|
runtime,
|
|
225
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
|
+
}
|
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.8",
|
|
3
|
+
"buildId": "d7d0675388e4",
|
|
4
|
+
"commit": "cd6d7ee4e2a7afcb77c4a7aaba09c82c8ec162d7",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-09-
|
|
6
|
+
"builtAt": "2026-09-23T20:30:52.283Z",
|
|
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;
|
|
@@ -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']),
|
|
@@ -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) {
|
|
@@ -0,0 +1,4 @@
|
|
|
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
|
+
export declare function proveArchivedAbsence(seat: RoomMemberSeat): Promise<ArchivedMemberAbsence>;
|
|
@@ -0,0 +1,37 @@
|
|
|
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 assertMemberIdentityAbsent({
|
|
22
|
+
role_name: proof.name, slot: 'archived', cowork_role: 'archived', seat_state: 'removed',
|
|
23
|
+
});
|
|
24
|
+
absent();
|
|
25
|
+
}
|
|
26
|
+
export async function proveArchivedAbsence(seat) {
|
|
27
|
+
if (seat.identity_cid || seat.retirement?.phase !== 'identity_absent'
|
|
28
|
+
|| !seat.launch?.launch_id || !seat.launch.action_id
|
|
29
|
+
|| seat.launch.launch_id !== seat.retirement.launch_id || !seat.retirement.archive_path)
|
|
30
|
+
throw new Error('Missing exact archived member absence evidence');
|
|
31
|
+
const proof = {
|
|
32
|
+
name: seat.role_name, launch_id: seat.launch.launch_id, action_id: seat.launch.action_id,
|
|
33
|
+
archive_path: seat.retirement.archive_path, checked_at: new Date().toISOString(),
|
|
34
|
+
};
|
|
35
|
+
await verifyArchivedAbsence(proof);
|
|
36
|
+
return proof;
|
|
37
|
+
}
|
|
@@ -20,6 +20,7 @@ export declare function waitForLivenessAbsent(role: string, launchId: string, li
|
|
|
20
20
|
/** Report whether any daemon identity — under any name — carries this exact CID. */
|
|
21
21
|
export declare function identityCidPresent(cid: string): Promise<boolean>;
|
|
22
22
|
export declare function removeExactMemberIdentity(seat: RoomMemberSeat): Promise<void>;
|
|
23
|
+
export declare function assertMemberIdentityAbsent(seat: RoomMemberSeat): Promise<void>;
|
|
23
24
|
/** One forward-only room close saga shared by every Fleet entry point. */
|
|
24
25
|
export declare function acceptManagedRoomClose(roomId: string): Promise<RoomOrchestrationRecord>;
|
|
25
26
|
export declare function recordManagedRoomCloseError(roomId: string, error: string, recoveryHint: string): Promise<RoomOrchestrationRecord>;
|
|
@@ -5,7 +5,7 @@ import { attachOursClient } from '@ours.network/sdk/client';
|
|
|
5
5
|
import { withFileLock } from '../atomic-file.js';
|
|
6
6
|
import { agentDir, stateRoot } from '../paths.js';
|
|
7
7
|
import { readClientProfile } from '../client-profile.js';
|
|
8
|
-
import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, } from '../temp-lifecycle.js';
|
|
8
|
+
import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, tempArchiveForLaunch, tempArchiveForCreationAction, } from '../temp-lifecycle.js';
|
|
9
9
|
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
10
10
|
import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, } from './room-state.js';
|
|
11
11
|
const CLOSE_LOCK_STALE_MS = 5 * 60_000;
|
|
@@ -119,7 +119,7 @@ export async function removeExactMemberIdentity(seat) {
|
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
async function assertMemberIdentityAbsent(seat) {
|
|
122
|
+
export async function assertMemberIdentityAbsent(seat) {
|
|
123
123
|
await withIdentityClient(async (client) => {
|
|
124
124
|
const rows = await client.listIdentities();
|
|
125
125
|
if (rows.some(row => row.name === seat.role_name ||
|
|
@@ -141,6 +141,27 @@ async function retireMember(roomId, seat, deps) {
|
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
143
|
if (!retirement) {
|
|
144
|
+
if (!existsSync(agentDir(current.role_name, true)) && current.launch?.launch_id && current.launch.action_id) {
|
|
145
|
+
const archived = tempArchiveForLaunch(current.role_name, current.launch.launch_id);
|
|
146
|
+
const created = tempArchiveForCreationAction(current.role_name, current.launch.action_id);
|
|
147
|
+
if (archived && created?.path === archived && created.launchId === current.launch.launch_id) {
|
|
148
|
+
if (readFileSync(join(archived, '.identity'), 'utf8').trim() !== current.role_name)
|
|
149
|
+
throw new Error(`room member '${current.role_name}' archive identity mismatch`);
|
|
150
|
+
if (await tempSupervisorLiveness(archived) !== 'stopped')
|
|
151
|
+
throw new Error(`room member '${current.role_name}' archived supervisor is not proven stopped`);
|
|
152
|
+
// A terminated launch can archive itself before room retirement begins.
|
|
153
|
+
// Accept its exact durable provenance only when no identity needs removal.
|
|
154
|
+
await assertMemberIdentityAbsent(current);
|
|
155
|
+
const latest = getRoomRecord(roomId)?.member_seats.find(seat => seat.role_name === current.role_name);
|
|
156
|
+
if (existsSync(agentDir(current.role_name, true))
|
|
157
|
+
|| latest?.launch?.launch_id !== current.launch.launch_id
|
|
158
|
+
|| latest?.launch?.action_id !== current.launch.action_id
|
|
159
|
+
|| latest?.identity_cid !== current.identity_cid)
|
|
160
|
+
throw new Error(`room member '${current.role_name}' changed during archived retirement proof`);
|
|
161
|
+
advanceMemberRetirement(roomId, current.role_name, 'identity_absent', current.launch.launch_id, archived);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
144
165
|
if (current.launch?.state === 'failed' && !existsSync(agentDir(current.role_name, true))) {
|
|
145
166
|
// A failure before applyRole (for example invite-secret validation) has
|
|
146
167
|
// no supervisor to stop or archive. Settle only proven absence; this
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { proveArchivedAbsence, verifyArchivedAbsence } from './archived-absence.js';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { withFileLock } from '../atomic-file.js';
|
|
3
4
|
import { agentDir } from '../paths.js';
|
|
@@ -145,10 +146,30 @@ export async function settleTaskDeletion(input) {
|
|
|
145
146
|
upsertTaskDeletionMembersFromSeats(taskId, record.member_seats);
|
|
146
147
|
await closeManagedRoom({ roomId: record.room_id, cowork: cowork, deps: deps.roomClose });
|
|
147
148
|
const closed = getRoomRecord(record.room_id);
|
|
148
|
-
|
|
149
|
+
const seats = closed?.member_seats ?? record.member_seats;
|
|
150
|
+
const proofs = [];
|
|
151
|
+
for (const seat of seats) {
|
|
152
|
+
if (!seat.identity_cid && seat.retirement?.launch_id !== 'never-launched')
|
|
153
|
+
proofs.push(await proveArchivedAbsence(seat));
|
|
154
|
+
}
|
|
155
|
+
// No await between the last live-state fence and durable checkpoint.
|
|
156
|
+
for (const proof of proofs) {
|
|
157
|
+
const latest = getRoomRecord(record.room_id)?.member_seats.find(seat => seat.role_name === proof.name);
|
|
158
|
+
if (existsSync(agentDir(proof.name, true)) || latest?.identity_cid
|
|
159
|
+
|| latest?.launch?.launch_id !== proof.launch_id || latest.launch.action_id !== proof.action_id)
|
|
160
|
+
throw new Error('Archived absence seat changed before checkpoint');
|
|
161
|
+
}
|
|
162
|
+
importTaskDeletionRetirementEvidence(taskId, seats, proofs);
|
|
149
163
|
await cowork.deleteRoom(record.room_id);
|
|
164
|
+
for (const proof of proofs)
|
|
165
|
+
await verifyArchivedAbsence(proof);
|
|
166
|
+
for (const proof of proofs)
|
|
167
|
+
if (existsSync(agentDir(proof.name, true)))
|
|
168
|
+
throw new Error('Archived member replacement before room unlink');
|
|
150
169
|
deleteRoomRecord(record.room_id);
|
|
151
170
|
}
|
|
171
|
+
for (const proof of getDeletingTask(taskId).deletion.archived_absences ?? [])
|
|
172
|
+
await verifyArchivedAbsence(proof);
|
|
152
173
|
// Members whose room record is gone (crash after record deletion, or
|
|
153
174
|
// legacy state): resume from the durable cursors.
|
|
154
175
|
for (const cursor of getDeletingTask(taskId).deletion.members) {
|
|
@@ -176,7 +197,7 @@ export async function settleTaskDeletion(input) {
|
|
|
176
197
|
completeTaskDeletionReceipt(taskId);
|
|
177
198
|
return { task_id: taskId, deleted: false };
|
|
178
199
|
}
|
|
179
|
-
const finalize = () => withFileLock(taskOperationLockPath(taskId), () => {
|
|
200
|
+
const finalize = () => withFileLock(taskOperationLockPath(taskId), async () => {
|
|
180
201
|
try {
|
|
181
202
|
const task = getDeletingTask(taskId);
|
|
182
203
|
if (task.deletion?.status !== 'pending')
|
|
@@ -185,6 +206,11 @@ export async function settleTaskDeletion(input) {
|
|
|
185
206
|
throw new TaskStateError(`task ${taskId} room records reappeared during deletion finalization`);
|
|
186
207
|
if (task.deletion.members.some(member => member.phase !== 'identity_absent'))
|
|
187
208
|
throw new TaskStateError(`task ${taskId} has unretired members at deletion finalization`);
|
|
209
|
+
for (const proof of task.deletion.archived_absences ?? [])
|
|
210
|
+
await verifyArchivedAbsence(proof);
|
|
211
|
+
for (const proof of task.deletion.archived_absences ?? [])
|
|
212
|
+
if (existsSync(agentDir(proof.name, true)))
|
|
213
|
+
throw new Error('Archived member replacement before task unlink');
|
|
188
214
|
if (cleanup.snapshotHash)
|
|
189
215
|
releaseLaunchSnapshotForDeletingTask(cleanup.snapshotHash, taskId);
|
|
190
216
|
unlinkDeletedTask(taskId);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ArchivedMemberAbsence } from './types.js';
|
|
1
2
|
import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent, TaskDeletionActor, TaskDeletionMemberPhase } from './types.js';
|
|
2
3
|
export declare const tasksDir: () => string;
|
|
3
4
|
export declare class TaskStateError extends Error {
|
|
@@ -74,6 +75,7 @@ export interface TaskDeletionReceipt {
|
|
|
74
75
|
original_state: TaskState;
|
|
75
76
|
room_id?: string;
|
|
76
77
|
member_count: number;
|
|
78
|
+
archived_absences?: ArchivedMemberAbsence[];
|
|
77
79
|
settled_at?: string;
|
|
78
80
|
result?: 'deleted';
|
|
79
81
|
}
|
|
@@ -122,6 +124,10 @@ export declare function advanceTaskDeletionMember(id: string, name: string, phas
|
|
|
122
124
|
interface SeatEvidence {
|
|
123
125
|
role_name: string;
|
|
124
126
|
identity_cid?: string;
|
|
127
|
+
launch?: {
|
|
128
|
+
launch_id?: string;
|
|
129
|
+
action_id?: string;
|
|
130
|
+
};
|
|
125
131
|
retirement?: {
|
|
126
132
|
phase: TaskDeletionMemberPhase;
|
|
127
133
|
launch_id: string;
|
|
@@ -146,10 +152,10 @@ export declare function upsertTaskDeletionMembersFromSeats(id: string, seats: Re
|
|
|
146
152
|
*
|
|
147
153
|
* The room saga is trusted for phase jumps, but partial or corrupt retained
|
|
148
154
|
* records must not become false success: every seat must be identity_absent;
|
|
149
|
-
* a real launch requires archive evidence; an identity-less seat
|
|
150
|
-
*
|
|
155
|
+
* a real launch requires archive evidence; an identity-less seat requires
|
|
156
|
+
* never-launched proof or a freshly verified archived-absence checkpoint.
|
|
151
157
|
*/
|
|
152
|
-
export declare function importTaskDeletionRetirementEvidence(id: string, seats: ReadonlyArray<SeatEvidence>): TaskRecord;
|
|
158
|
+
export declare function importTaskDeletionRetirementEvidence(id: string, seats: ReadonlyArray<SeatEvidence>, proofs?: ReadonlyArray<ArchivedMemberAbsence>): TaskRecord;
|
|
153
159
|
/**
|
|
154
160
|
* Physically remove a deletion-pending task record. Settlement-only: callers
|
|
155
161
|
* must have completed member retirement and room cleanup first. Missing
|
|
@@ -505,7 +505,8 @@ function writeDeletionReceiptForIntent(stored) {
|
|
|
505
505
|
actor: deletion.actor,
|
|
506
506
|
original_state: stored.state,
|
|
507
507
|
room_id: deletion.room_id,
|
|
508
|
-
member_count: deletion.members.length,
|
|
508
|
+
member_count: deletion.members.length + (deletion.archived_absences?.length ?? 0),
|
|
509
|
+
...(deletion.archived_absences?.length ? { archived_absences: deletion.archived_absences } : {}),
|
|
509
510
|
};
|
|
510
511
|
replaceFileAtomically(deletionReceiptPath(stored.task_id), JSON.stringify(receipt, null, 2) + '\n');
|
|
511
512
|
}
|
|
@@ -688,10 +689,10 @@ export function upsertTaskDeletionMembersFromSeats(id, seats) {
|
|
|
688
689
|
*
|
|
689
690
|
* The room saga is trusted for phase jumps, but partial or corrupt retained
|
|
690
691
|
* records must not become false success: every seat must be identity_absent;
|
|
691
|
-
* a real launch requires archive evidence; an identity-less seat
|
|
692
|
-
*
|
|
692
|
+
* a real launch requires archive evidence; an identity-less seat requires
|
|
693
|
+
* never-launched proof or a freshly verified archived-absence checkpoint.
|
|
693
694
|
*/
|
|
694
|
-
export function importTaskDeletionRetirementEvidence(id, seats) {
|
|
695
|
+
export function importTaskDeletionRetirementEvidence(id, seats, proofs = []) {
|
|
695
696
|
return withTaskLock(id, () => {
|
|
696
697
|
assertCanonicalTaskId(id);
|
|
697
698
|
const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
|
|
@@ -708,6 +709,18 @@ export function importTaskDeletionRetirementEvidence(id, seats) {
|
|
|
708
709
|
if (!seat.identity_cid) {
|
|
709
710
|
if (neverLaunched)
|
|
710
711
|
continue; // provably never held a managed identity
|
|
712
|
+
const proof = proofs.find(value => value.name === seat.role_name);
|
|
713
|
+
if (proof && proof.launch_id === evidence.launch_id && proof.archive_path === evidence.archive_path
|
|
714
|
+
&& proof.launch_id === seat.launch?.launch_id && proof.action_id === seat.launch?.action_id) {
|
|
715
|
+
const existing = stored.deletion.archived_absences?.find(value => value.name === proof.name);
|
|
716
|
+
if (existing && (existing.launch_id !== proof.launch_id || existing.action_id !== proof.action_id
|
|
717
|
+
|| existing.archive_path !== proof.archive_path))
|
|
718
|
+
throw new TaskStateError('Archived absence checkpoint ownership is immutable');
|
|
719
|
+
stored.deletion.archived_absences ??= [];
|
|
720
|
+
if (!existing)
|
|
721
|
+
stored.deletion.archived_absences.push({ ...proof });
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
711
724
|
throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' has retirement evidence but no identity CID`);
|
|
712
725
|
}
|
|
713
726
|
let cursor = findCursorForSeat(id, stored, seat);
|
|
@@ -729,6 +742,7 @@ export function importTaskDeletionRetirementEvidence(id, seats) {
|
|
|
729
742
|
cursor.updated_at = now;
|
|
730
743
|
}
|
|
731
744
|
writeTask(stored);
|
|
745
|
+
writeDeletionReceiptForIntent(stored);
|
|
732
746
|
return presentTaskLenient(stored);
|
|
733
747
|
});
|
|
734
748
|
}
|
|
@@ -73,6 +73,13 @@ export interface TaskDeletionMemberCursor {
|
|
|
73
73
|
* evidence. While pending, the task is hidden from normal operation and every
|
|
74
74
|
* lifecycle mutation or room publication is rejected.
|
|
75
75
|
*/
|
|
76
|
+
export interface ArchivedMemberAbsence {
|
|
77
|
+
name: string;
|
|
78
|
+
launch_id: string;
|
|
79
|
+
action_id: string;
|
|
80
|
+
archive_path: string;
|
|
81
|
+
checked_at: string;
|
|
82
|
+
}
|
|
76
83
|
export interface TaskDeletionIntent {
|
|
77
84
|
status: 'pending';
|
|
78
85
|
accepted_at: string;
|
|
@@ -80,6 +87,7 @@ export interface TaskDeletionIntent {
|
|
|
80
87
|
room_id?: string;
|
|
81
88
|
/** Snapshot of managed members at acceptance; the missing-room retirement evidence. */
|
|
82
89
|
members: TaskDeletionMemberCursor[];
|
|
90
|
+
archived_absences?: ArchivedMemberAbsence[];
|
|
83
91
|
error?: string;
|
|
84
92
|
error_at?: string;
|
|
85
93
|
recovery_hint?: string;
|
package/dist/web/server.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SupervisorOursTools } from '../application/supervisor-ours-tools.js';
|
|
1
2
|
import { type FastifyInstance } from 'fastify';
|
|
2
3
|
import type { FleetQueryService } from '../application/fleet-query-service.js';
|
|
3
4
|
import type { RoleRepository } from '../application/role-repository.js';
|
|
@@ -31,6 +32,7 @@ export interface WebServices {
|
|
|
31
32
|
topologyPromote?: TopologyPromoteService;
|
|
32
33
|
removal?: RoleRemovalService;
|
|
33
34
|
taskRooms?: TaskRoomApplicationService;
|
|
35
|
+
oursTools?: Pick<SupervisorOursTools, 'list' | 'call'>;
|
|
34
36
|
}
|
|
35
37
|
export interface WebServer {
|
|
36
38
|
app: FastifyInstance;
|
package/dist/web/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SupervisorOursTools } from '../application/supervisor-ours-tools.js';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -51,7 +52,11 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
51
52
|
}
|
|
52
53
|
});
|
|
53
54
|
app.setErrorHandler(async (error, request, reply) => {
|
|
54
|
-
const
|
|
55
|
+
const privateToolParseError = request.routeOptions.url === '/api/v1/roles/:id/ours/call'
|
|
56
|
+
&& error instanceof Error && 'code' in error
|
|
57
|
+
&& typeof error.code === 'string' && error.code.startsWith('FST_ERR_CTP_');
|
|
58
|
+
const fleetError = normalizeError(privateToolParseError
|
|
59
|
+
? new FleetError('invalid_request', 'expected a valid JSON tool request') : error, request.id);
|
|
55
60
|
await audit.record({
|
|
56
61
|
requestId: request.id, action: `${request.method} ${request.routeOptions.url ?? request.url}`,
|
|
57
62
|
result: 'rejected', errorCode: fleetError.code,
|
|
@@ -311,6 +316,18 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
311
316
|
});
|
|
312
317
|
return result;
|
|
313
318
|
});
|
|
319
|
+
const oursTools = services.oursTools ?? new SupervisorOursTools();
|
|
320
|
+
app.get('/api/v1/roles/:id/ours/tools', async (request) => {
|
|
321
|
+
auth.authenticate(request);
|
|
322
|
+
return oursTools.list(request.params.id);
|
|
323
|
+
});
|
|
324
|
+
app.post('/api/v1/roles/:id/ours/call', async (request) => {
|
|
325
|
+
const session = auth.authenticate(request, true);
|
|
326
|
+
const result = await oursTools.call(request.params.id, request.body);
|
|
327
|
+
await audit.record({ requestId: request.id, browser: session.id,
|
|
328
|
+
action: 'ours.call', result: result.result.isError === true ? 'tool_error' : 'succeeded' });
|
|
329
|
+
return result;
|
|
330
|
+
});
|
|
314
331
|
app.get('/api/v1/roles/:id', async (request) => {
|
|
315
332
|
auth.authenticate(request);
|
|
316
333
|
return services.query.detail(request.params.id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.2.0-nightly.
|
|
3
|
+
"version": "1.2.0-nightly.8",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|
|
@@ -12,6 +12,28 @@ persona: |
|
|
|
12
12
|
Only execute task work yourself when the owner explicitly orders you to override this boundary
|
|
13
13
|
for that specific task.
|
|
14
14
|
|
|
15
|
+
Agent identity and contact management requested by the owner is Fleet management work.
|
|
16
|
+
For requests such as "generate an invite for this agent", "accept this invite for that agent",
|
|
17
|
+
"list its contacts", or "send this message as that agent", select the named agent and call the
|
|
18
|
+
corresponding deterministic supervisor operation through the Fleet CLI or REST API. The
|
|
19
|
+
supervisor owns and binds that agent's identity; verify that the selected agent and returned
|
|
20
|
+
identity match the owner's request. Use the shared supervisor tool interface for these
|
|
21
|
+
operations, including generate_invite, add_contact, list_contacts, and send_message.
|
|
22
|
+
Discover schemas with `ours-fleet ours tools <agent>`; invoke with
|
|
23
|
+
`ours-fleet ours call <agent> <tool> --args-file <private-json-file>` (omit the file for {}).
|
|
24
|
+
REST equivalents are GET `/api/v1/roles/<agent>/ours/tools` and POST
|
|
25
|
+
`/api/v1/roles/<agent>/ours/call` with body {"tool":"<tool>","arguments":{...}}.
|
|
26
|
+
Use the normal authenticated Fleet session and CSRF token for REST calls.
|
|
27
|
+
Do not ask the agent in its conversation to perform these operations, send the invite to its
|
|
28
|
+
LLM session, or use session steering as an identity-management API. Do not create a separate
|
|
29
|
+
identity, choose or rebind an identity, force a binding, or restart a supervisor to perform a
|
|
30
|
+
contact operation. If the supported supervisor operation is unavailable or identity matching
|
|
31
|
+
fails, report that exact blocker and retain the request without attempting a substitute path.
|
|
32
|
+
Keep invite inputs in the supported private-file or request-body channel, out of logs and
|
|
33
|
+
unrelated rooms. Report the operation's actual result: accepting an invite may leave contact
|
|
34
|
+
verification pending, so do not claim that contact establishment or message delivery is complete
|
|
35
|
+
until the corresponding result confirms it.
|
|
36
|
+
|
|
15
37
|
For every new task:
|
|
16
38
|
1. Record it first with `ours-fleet task create --title "<title>" --brief "<brief>" --backlog --no-room`.
|
|
17
39
|
2. Ask the owner which room template to use: `single`, `pair`, or `team`.
|