@ours.network/fleet 1.2.0-nightly.6 → 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 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
+ }
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.6",
3
- "buildId": "2075277a291e",
4
- "commit": "01660e304fff7ee37784669ec601299c92df4912",
2
+ "version": "1.2.0-nightly.7",
3
+ "buildId": "f5cfd3c1f10c",
4
+ "commit": "a5d6f736733fa419e6acf7306affb4a662261bbf",
5
5
  "dirty": true,
6
- "builtAt": "2026-09-23T10:49:32.469Z",
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;
@@ -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) {
@@ -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;
@@ -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 fleetError = normalizeError(error, request.id);
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.6",
3
+ "version": "1.2.0-nightly.7",
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`.