@ours.network/fleet 1.2.0-nightly.44 → 1.2.0-nightly.46
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 +75 -0
- package/dist/application/role-removal-service.js +2 -0
- package/dist/briefing.d.ts +2 -0
- package/dist/briefing.js +8 -1
- package/dist/build-info.json +5 -4
- package/dist/capabilities.d.ts +3 -1
- package/dist/capabilities.js +3 -0
- package/dist/cli.js +180 -12
- package/dist/config.d.ts +10 -0
- package/dist/config.js +13 -2
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +82 -0
- package/dist/doctor.js +27 -0
- package/dist/fleet-command-audit.d.ts +6 -0
- package/dist/fleet-command-audit.js +15 -3
- package/dist/managed-cli.d.ts +253 -0
- package/dist/managed-cli.js +922 -0
- package/dist/notifications/fleet-producer.d.ts +1 -0
- package/dist/notifications/fleet-producer.js +82 -3
- package/dist/notifications/outbox.d.ts +4 -0
- package/dist/notifications/outbox.js +9 -0
- package/dist/notifications/target-cleanup.d.ts +8 -0
- package/dist/notifications/target-cleanup.js +41 -0
- package/dist/ops.js +5 -1
- package/dist/resolved-plan.js +1 -0
- package/dist/runner.js +9 -1
- package/dist/spawn.js +1 -1
- package/dist/tunnel-setup-input.d.ts +11 -0
- package/dist/tunnel-setup-input.js +42 -0
- package/dist/web/workspace-devices.d.ts +2 -0
- package/dist/web/workspace-devices.js +13 -0
- package/dist/workspace-enrollment.d.ts +30 -2
- package/dist/workspace-enrollment.js +205 -12
- package/dist/workspace-replacement.d.ts +66 -0
- package/dist/workspace-replacement.js +220 -0
- package/docs/validation/managed-cli-permissions.md +222 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -286,10 +286,33 @@ ours-fleet loops reload <Role>
|
|
|
286
286
|
ours-fleet loops run-now|disable|enable <Role> <Loop>
|
|
287
287
|
ours-fleet rm <Name>
|
|
288
288
|
ours-fleet doctor [--harness H]
|
|
289
|
+
ours-fleet managed-cli setup [--enable <Agent>]... | status [--json] native harness policy for the task workflow
|
|
289
290
|
ours-fleet version [--json] build identity, capabilities, installs on PATH
|
|
290
291
|
ours-fleet init
|
|
291
292
|
```
|
|
292
293
|
|
|
294
|
+
### Fleet task commands from a harness command sandbox
|
|
295
|
+
|
|
296
|
+
A Codex or Claude command sandbox refuses the supervisor socket, so a managed
|
|
297
|
+
`ours-fleet` call fails with `connect EPERM` before it parses anything. Fleet
|
|
298
|
+
prepares the native harness policy for the packaged Coordinator task workflow;
|
|
299
|
+
you do not write Codex rules or Claude sandbox settings by hand:
|
|
300
|
+
|
|
301
|
+
```sh
|
|
302
|
+
ours-fleet managed-cli setup --enable FleetCoordinator # adds managed_cli: [task-workflow] to that Agent
|
|
303
|
+
ours-fleet managed-cli status # static setup, session policy, observed commands
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The opt-in is an explicit Agent key and changes no permission. Setup starts no
|
|
307
|
+
session or model, is repeated at every agent start, and `ours-install` runs it
|
|
308
|
+
for you. It prepares `task create|start|finish|block|unblock|review|list|show`,
|
|
309
|
+
`room show|members` and template/plan/help inspection for one pinned
|
|
310
|
+
configuration, and nothing else; `task start`/`task finish` are real authority.
|
|
311
|
+
Qualified end to end on Linux with Codex (native and ACP) and Claude ACP; macOS
|
|
312
|
+
is prepared the same way but not yet qualified. Scope, boundaries, unsupported
|
|
313
|
+
combinations and the evidence are in
|
|
314
|
+
[docs/validation/managed-cli-permissions.md](docs/validation/managed-cli-permissions.md).
|
|
315
|
+
|
|
293
316
|
### Which build am I running?
|
|
294
317
|
|
|
295
318
|
`--version` prints a semver, and a semver does not identify an artifact: version
|
|
@@ -1833,6 +1856,58 @@ pipe with a 32KiB limit and refuses a terminal. Legacy v1 payloads, which contai
|
|
|
1833
1856
|
long-lived scoped credentials, are accepted only through file/stdin, never as
|
|
1834
1857
|
command arguments. Use only one payload transport.
|
|
1835
1858
|
|
|
1859
|
+
When this host already has a registration, Fleet inspects the one-use grant without
|
|
1860
|
+
consuming it. The same workspace reruns without a deletion prompt or retirement.
|
|
1861
|
+
An account-origin change for that workspace requires `--migrate-app-origin`; a
|
|
1862
|
+
changed server identity is refused. An older App without inspection support falls
|
|
1863
|
+
back to asking before redemption. An unavailable inspection service preserves the
|
|
1864
|
+
grant and setup. Fleet verifies the redeemed destination against the inspection.
|
|
1865
|
+
|
|
1866
|
+
For another workspace, setup describes the existing workspace, tunnel hostname and
|
|
1867
|
+
account origin and asks `[y/N]`. No answer or `no` preserves the setup and grant; a
|
|
1868
|
+
noninteractive caller must pass `--replace-registration`, otherwise it exits with
|
|
1869
|
+
an error. A legacy binding without a server identity must rerun its own workspace
|
|
1870
|
+
first. The original Human root signs retirement. Fleet waits for the account's
|
|
1871
|
+
durable cleanup receipt before stopping the old service and revoking its linked
|
|
1872
|
+
devices. If a migrated database has no old registration, the signed command confirms
|
|
1873
|
+
absence; the service has no old provider resources to delete. Installation identities,
|
|
1874
|
+
profile, agents, history, stable host ID and local pairing are preserved.
|
|
1875
|
+
|
|
1876
|
+
Replacement recovery is saved in an owned private `workspace/replacement.json`
|
|
1877
|
+
after root, host, service and server-contact preconditions pass. The App must
|
|
1878
|
+
explicitly support deadline-enforced retirement; an older App without that
|
|
1879
|
+
capability preserves the existing setup. `setup-tunnel --resume` retries its nonce
|
|
1880
|
+
and saved operation deadline (24 hours less a five-minute clock margin).
|
|
1881
|
+
If the enrollment challenge expires, obtain a fresh App command for the same target
|
|
1882
|
+
and pass `--replace-registration`; completed retirement is retained. The old server
|
|
1883
|
+
identity is pinned: only its own invitation can reconnect it.
|
|
1884
|
+
|
|
1885
|
+
`setup-tunnel --abandon-replacement` removes the private recovery payload only when
|
|
1886
|
+
a fresh private receipt confirms no deletion and the operation was never dispatched
|
|
1887
|
+
or was terminally rejected. A lost transport response remains uncertain. Expired
|
|
1888
|
+
operations require a fresh receipt proving the old binding still exists after the
|
|
1889
|
+
deadline plus five minutes and an explicit server guarantee that the deadline is enforced;
|
|
1890
|
+
older recovery records without that guarantee must be resumed. Abandoning preserves
|
|
1891
|
+
the existing binding, connector and installation data. A terminally rejected operation
|
|
1892
|
+
requires abandonment and a new App command, rather than reusing its nonce.
|
|
1893
|
+
After that deadline, a fresh command for the same target can also resume with a
|
|
1894
|
+
new operation nonce after checking the enforced expiry. This confirms signed
|
|
1895
|
+
absence if an old successful receipt was lost and later pruned.
|
|
1896
|
+
|
|
1897
|
+
If the App accepted the successor proof before Fleet wrote its local binding,
|
|
1898
|
+
a fresh command for that target retains the original private connector from the
|
|
1899
|
+
recovery record. If that successor cannot be recovered, remove it in the App,
|
|
1900
|
+
then use `setup-tunnel --discard-retired-replacement` and a fresh App command.
|
|
1901
|
+
Discard is allowed only after retirement and local cleanup, while the old local
|
|
1902
|
+
binding remains. It removes only the recovery record. This host has no working
|
|
1903
|
+
registration until the new setup completes; the old service was stopped and its
|
|
1904
|
+
linked devices revoked.
|
|
1905
|
+
|
|
1906
|
+
Renewal commands contain no existing live connector credential. Fleet keeps the
|
|
1907
|
+
current private connector byte for byte, and accepts renewal only for its recorded
|
|
1908
|
+
workspace, server, origin and tunnel hostname. Restore a missing or unsafe connector,
|
|
1909
|
+
or remove and re-create that workspace in the App before setting it up again.
|
|
1910
|
+
|
|
1836
1911
|
After service and signed tunnel-target confirmation, setup prints both a QR and
|
|
1837
1912
|
ordinary private single-use connection code. The App checks readiness automatically
|
|
1838
1913
|
and immediately opens the code field. If setup or code output fails after binding,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { retireNotificationTarget } from '../notifications/target-cleanup.js';
|
|
1
2
|
import { cpSync, existsSync, mkdirSync, readdirSync, renameSync } from 'node:fs';
|
|
2
3
|
import { basename, dirname, join } from 'node:path';
|
|
3
4
|
import { loadConfig, ROLE_NAME_RE } from '../config.js';
|
|
@@ -87,6 +88,7 @@ export class RoleRemovalService {
|
|
|
87
88
|
await this.options.ops.backend.uninstall(preview.role);
|
|
88
89
|
if (existsSync(sourceState))
|
|
89
90
|
renameSync(sourceState, join(recoveryPath, 'orphan-state'));
|
|
91
|
+
await retireNotificationTarget(`/fleet/chats?chat=${encodeURIComponent(preview.role)}`).catch(() => { });
|
|
90
92
|
this.options.ops.log(`removed orphaned backend/state '${preview.role}'`);
|
|
91
93
|
}
|
|
92
94
|
return { ...preview, removed: true, recoveryPath };
|
package/dist/briefing.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface BriefingOpts {
|
|
|
15
15
|
identityGuarantee?: 'verified' | 'created' | 'unverified';
|
|
16
16
|
/** Temporary spawn whose newly-created identity should share the session lifecycle. */
|
|
17
17
|
temporaryIdentity?: boolean;
|
|
18
|
+
/** Fleet-prepared invocation guidance (src/managed-cli.ts); empty when none applies. */
|
|
19
|
+
managedCli?: string[];
|
|
18
20
|
}
|
|
19
21
|
/** Render a role's briefing.md: narrative (or curated body) + mechanical boot steps. */
|
|
20
22
|
export declare function generateBriefing(role: ResolvedRole, v: BriefingVocab, opts: BriefingOpts): string;
|
package/dist/briefing.js
CHANGED
|
@@ -197,9 +197,16 @@ export function generateBriefing(role, v, opts) {
|
|
|
197
197
|
L.push('For omitted settings, the supervisor inherits your canonical Brain and Role selections,');
|
|
198
198
|
L.push('working directory, neutral permissions, coordinator, and fleet monitor policy. Every');
|
|
199
199
|
L.push('explicit option wins; identity/session-local and secret material never inherit.');
|
|
200
|
-
L.push('All public CLI surfaces are
|
|
200
|
+
L.push('All public CLI surfaces are routed this way; hidden worker entry points remain internal.');
|
|
201
|
+
if (role.permissions && role.permissions.filesystem !== 'unrestricted' && !opts.managedCli?.length) {
|
|
202
|
+
L.push('Routing is not reachability: every call first connects to the supervisor socket, and a');
|
|
203
|
+
L.push('harness command sandbox can refuse that connection (`connect EPERM`), even for `--help`.');
|
|
204
|
+
L.push('No Fleet command is prepared to run outside your sandbox. If one is refused, report it');
|
|
205
|
+
L.push('as a blocker; the owner enables a workflow with `ours-fleet managed-cli setup --enable`.');
|
|
206
|
+
}
|
|
201
207
|
L.push('Lifecycle notice delivery failures stay in service diagnostics and never rerun effects.');
|
|
202
208
|
L.push('This proxy is attribution and convenience, not a security boundary for unisolated roles.');
|
|
209
|
+
L.push(...(opts.managedCli ?? []));
|
|
203
210
|
}
|
|
204
211
|
if (role.coordinator) {
|
|
205
212
|
L.push(`7. ANNOUNCE yourself: call **${v.sendTool}** to contact "${role.coordinator}" with text:`);
|
package/dist/build-info.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.46",
|
|
3
|
+
"buildId": "a75b12e57308",
|
|
4
|
+
"commit": "f608ced10209f4b9ecf9dc9b573d8cab46136a68",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-04T23:11:11.430Z",
|
|
7
7
|
"capabilities": [
|
|
8
|
+
"managed-cli.setup-v1",
|
|
8
9
|
"workspace.enroll.preserve-profile-v1",
|
|
9
10
|
"cowork.http-management-v1",
|
|
10
11
|
"monitor.interrupt.after_tool"
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
export declare const CAP_MONITOR_INTERRUPT_AFTER_TOOL = "monitor.interrupt.after_tool";
|
|
17
17
|
export declare const CAP_HTTP_COWORK = "cowork.http-management-v1";
|
|
18
18
|
export declare const CAP_WORKSPACE_PRESERVE_PROFILE = "workspace.enroll.preserve-profile-v1";
|
|
19
|
-
|
|
19
|
+
/** `ours-fleet managed-cli setup|status` and the `managed_cli` Agent key (#233). */
|
|
20
|
+
export declare const CAP_MANAGED_CLI_SETUP = "managed-cli.setup-v1";
|
|
21
|
+
export declare const CAPABILITIES: readonly ["managed-cli.setup-v1", "workspace.enroll.preserve-profile-v1", "cowork.http-management-v1", "monitor.interrupt.after_tool"];
|
|
20
22
|
export type Capability = (typeof CAPABILITIES)[number];
|
|
21
23
|
/** Does this build declare `token`? */
|
|
22
24
|
export declare const hasCapability: (token: string) => boolean;
|
package/dist/capabilities.js
CHANGED
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
export const CAP_MONITOR_INTERRUPT_AFTER_TOOL = 'monitor.interrupt.after_tool';
|
|
17
17
|
export const CAP_HTTP_COWORK = 'cowork.http-management-v1';
|
|
18
18
|
export const CAP_WORKSPACE_PRESERVE_PROFILE = 'workspace.enroll.preserve-profile-v1';
|
|
19
|
+
/** `ours-fleet managed-cli setup|status` and the `managed_cli` Agent key (#233). */
|
|
20
|
+
export const CAP_MANAGED_CLI_SETUP = 'managed-cli.setup-v1';
|
|
19
21
|
export const CAPABILITIES = [
|
|
22
|
+
CAP_MANAGED_CLI_SETUP,
|
|
20
23
|
CAP_WORKSPACE_PRESERVE_PROFILE,
|
|
21
24
|
CAP_HTTP_COWORK,
|
|
22
25
|
CAP_MONITOR_INTERRUPT_AFTER_TOOL,
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { beginWorkspaceReplacement, abandonWorkspaceReplacement, discardRetiredWorkspaceReplacement, clearWorkspaceReplacement, confirmWorkspaceReplacement, finishWorkspaceReplacement, readWorkspaceBinding, readWorkspaceReplacement, requiresWorkspaceReplacement, refreshWorkspaceReplacement } from './workspace-replacement.js';
|
|
2
3
|
import { completeTunnelSetup } from './tunnel-setup-completion.js';
|
|
3
|
-
import { readTunnelSetupFile, readTunnelSetupStdin, readTunnelSetupArgument, redeemTunnelSetup } from './tunnel-setup-input.js';
|
|
4
|
+
import { readTunnelSetupFile, readTunnelSetupStdin, readTunnelSetupArgument, redeemTunnelSetup, inspectTunnelSetup, verifyTunnelSetupTarget } from './tunnel-setup-input.js';
|
|
4
5
|
import { ensureMinimalSetup } from './minimal-setup.js';
|
|
5
|
-
import { confirmWorkspaceTarget } from './workspace-enrollment.js';
|
|
6
|
+
import { confirmWorkspaceTarget, checkWorkspaceConnector } from './workspace-enrollment.js';
|
|
6
7
|
import { clearPendingTunnelSetup, readPendingTunnelSetup, requiredPortAfterExpiry, savePendingTunnelSetup, setupTunnelPreflight } from './setup-tunnel-preflight.js';
|
|
7
8
|
import { availableWebPort } from './web/available-port.js';
|
|
8
9
|
import QRCode from 'qrcode';
|
|
@@ -56,6 +57,8 @@ import './harness/claude-code.js'; // registers the claude-code adapter
|
|
|
56
57
|
import './harness/codex.js'; // registers the codex adapter
|
|
57
58
|
import './harness/hermes.js';
|
|
58
59
|
import { registerLayoutCommands } from './rooms-tasks/layout-cli.js';
|
|
60
|
+
import { MANAGED_CONFIGURATION_FLAG, assertPinnedConfiguration, enableManagedCli, managedCliReport, parseManagedEntry, } from './managed-cli.js';
|
|
61
|
+
import { MANAGED_CLI_WORKFLOW_IDS } from './config.js';
|
|
59
62
|
import { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './rooms-tasks/cli.js';
|
|
60
63
|
import { RoleCreationService } from './application/role-creation-service.js';
|
|
61
64
|
import { RoleRemovalService } from './application/role-removal-service.js';
|
|
@@ -99,6 +102,22 @@ const program = new Command()
|
|
|
99
102
|
.exitOverride()
|
|
100
103
|
.version(VERSION);
|
|
101
104
|
const cOpt = (cmd) => cmd.option('-c, --configuration <file>', 'manifest (default: ~/fleet.yaml; documents under ~/fleet/)');
|
|
105
|
+
/**
|
|
106
|
+
* Set only for the pinned managed entry form (see src/managed-cli.ts). Checked
|
|
107
|
+
* against the PARSED option, so every spelling of a later `-c` is covered.
|
|
108
|
+
*/
|
|
109
|
+
let pinnedConfiguration;
|
|
110
|
+
program.hook('preAction', (_program, action) => {
|
|
111
|
+
if (pinnedConfiguration === undefined)
|
|
112
|
+
return;
|
|
113
|
+
try {
|
|
114
|
+
assertPinnedConfiguration(pinnedConfiguration, action.opts().configuration);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error(`error: ${error.message}`);
|
|
118
|
+
throw new FleetCliExit(1, 'validation', 'not_started');
|
|
119
|
+
}
|
|
120
|
+
});
|
|
102
121
|
const collect = (value, previous) => [...previous, value];
|
|
103
122
|
program.command('docs')
|
|
104
123
|
.alias('man')
|
|
@@ -1299,6 +1318,78 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
|
|
|
1299
1318
|
die(e);
|
|
1300
1319
|
}
|
|
1301
1320
|
});
|
|
1321
|
+
function renderManagedCliReport(report) {
|
|
1322
|
+
const lines = [
|
|
1323
|
+
`Managed CLI setup for ${report.configuration}`,
|
|
1324
|
+
` node ${report.node}`, ` cli ${report.cli}`,
|
|
1325
|
+
` platform ${report.platform.os}${report.platform.declared ? '' : ' (not a declared platform)'}`,
|
|
1326
|
+
];
|
|
1327
|
+
for (const role of report.roles) {
|
|
1328
|
+
if (role.setup.state === 'not-declared')
|
|
1329
|
+
continue;
|
|
1330
|
+
lines.push('', `${role.role} (${role.harness}/${role.session}) — ${role.workflows.join(', ')}`);
|
|
1331
|
+
lines.push(` setup (static): ${role.setup.state}${role.setup.action ? ` [${role.setup.action}]` : ''} — ${role.setup.detail}`);
|
|
1332
|
+
if (role.setup.artifact)
|
|
1333
|
+
lines.push(` ${role.setup.artifact}`);
|
|
1334
|
+
lines.push(` running session: ${role.session_policy}`);
|
|
1335
|
+
lines.push(` observed (audit ledger): ${role.observed.summary}`);
|
|
1336
|
+
if (role.commandPrefix)
|
|
1337
|
+
lines.push(` invoke as: ${role.commandPrefix} <command>`);
|
|
1338
|
+
for (const line of role.scope)
|
|
1339
|
+
lines.push(` scope: ${line}`);
|
|
1340
|
+
for (const line of role.warnings)
|
|
1341
|
+
lines.push(` note: ${line}`);
|
|
1342
|
+
}
|
|
1343
|
+
if (!report.roles.some(role => role.setup.state !== 'not-declared'))
|
|
1344
|
+
lines.push('', 'No agent declares managed_cli. Nothing was generated and no permission changed.', 'Enable it for one agent with: ours-fleet managed-cli setup --enable <Agent>');
|
|
1345
|
+
for (const item of report.removed)
|
|
1346
|
+
lines.push('', `${item.action}: ${item.path} — ${item.detail ?? ''}`);
|
|
1347
|
+
lines.push('', ...report.disclosure.map(line => `* ${line}`));
|
|
1348
|
+
return lines.join('\n');
|
|
1349
|
+
}
|
|
1350
|
+
const managedCliCommand = program.command('managed-cli')
|
|
1351
|
+
.description('prepare and inspect native harness policy for Fleet commands run from a command sandbox');
|
|
1352
|
+
for (const [name, write, description] of [
|
|
1353
|
+
['setup', true, 'generate the policy for agents declaring managed_cli; starts no session and no model'],
|
|
1354
|
+
['status', false, 'compare generated policy and observed supervisor access without changing anything'],
|
|
1355
|
+
]) {
|
|
1356
|
+
const command = cOpt(managedCliCommand.command(name).description(description)).option('--json', 'JSON output');
|
|
1357
|
+
if (write)
|
|
1358
|
+
command.option('--enable <agent>', 'first add managed_cli: [task-workflow] to this Agent file (repeatable); its permissions are not changed', (value, all) => [...all, value], [])
|
|
1359
|
+
.option('--workflow <id>', `workflow --enable adds (${MANAGED_CLI_WORKFLOW_IDS.join(', ')})`, 'task-workflow');
|
|
1360
|
+
command.action((opts) => {
|
|
1361
|
+
try {
|
|
1362
|
+
const enabled = [];
|
|
1363
|
+
if (opts.enable?.length) {
|
|
1364
|
+
if (!MANAGED_CLI_WORKFLOW_IDS.includes(opts.workflow ?? ''))
|
|
1365
|
+
throw new Error(`unknown workflow '${opts.workflow}'; supported: ${MANAGED_CLI_WORKFLOW_IDS.join(', ')}`);
|
|
1366
|
+
// Validate every name before the first file is touched.
|
|
1367
|
+
const cfg = loadConfig(opts.configuration);
|
|
1368
|
+
for (const agent of opts.enable)
|
|
1369
|
+
findRole(cfg, agent);
|
|
1370
|
+
for (const agent of opts.enable)
|
|
1371
|
+
enabled.push({ agent, ...enableManagedCli(opts.configuration, agent, opts.workflow) });
|
|
1372
|
+
}
|
|
1373
|
+
const report = managedCliReport(loadConfig(opts.configuration), opts.configuration, { write });
|
|
1374
|
+
if (opts.json) {
|
|
1375
|
+
console.log(JSON.stringify({ ...report, enabled }, null, 2));
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
for (const item of enabled)
|
|
1379
|
+
console.log(`${item.changed ? 'Enabled' : 'Already enabled'} ${opts.workflow} for ${item.agent} in ${item.file}`);
|
|
1380
|
+
console.log(renderManagedCliReport(report));
|
|
1381
|
+
}
|
|
1382
|
+
const failing = report.roles.filter(role => ['unsupported', 'conflict', 'stale', 'missing'].includes(role.setup.state));
|
|
1383
|
+
if (failing.length)
|
|
1384
|
+
throw new FleetCliExit(1);
|
|
1385
|
+
}
|
|
1386
|
+
catch (error) {
|
|
1387
|
+
if (error instanceof FleetCliExit)
|
|
1388
|
+
throw error;
|
|
1389
|
+
die(error);
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1302
1393
|
cOpt(program.command('doctor').description('prerequisite report'))
|
|
1303
1394
|
.option('--harness <id>', 'check one harness explicitly')
|
|
1304
1395
|
.option('--yaml-mode <mode>', 'non-plain YAML policy: compat|strict', 'compat')
|
|
@@ -1519,9 +1610,23 @@ webCommand.command('uninstall').description('stop and uninstall the owner web se
|
|
|
1519
1610
|
});
|
|
1520
1611
|
async function workspaceEnrollmentCommand(opts, setupTunnel = false, argument) {
|
|
1521
1612
|
try {
|
|
1613
|
+
if (opts.discardRetiredReplacement) {
|
|
1614
|
+
if (!setupTunnel || argument !== undefined || opts.file || opts.stdin || opts.check || opts.resume || opts.migrateAppOrigin || opts.replaceRegistration || opts.configuration || opts.abandonReplacement)
|
|
1615
|
+
throw Error('--discard-retired-replacement takes no payload or other setup options');
|
|
1616
|
+
discardRetiredWorkspaceReplacement();
|
|
1617
|
+
process.stdout.write('Retired replacement recovery discarded. This host has no working registration: the old service was stopped and linked devices revoked. After removing the failed successor in the App, use a fresh command to complete a new registration. Installation data and the old local binding are preserved.\n');
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
if (opts.abandonReplacement) {
|
|
1621
|
+
if (!setupTunnel || argument !== undefined || opts.file || opts.stdin || opts.check || opts.resume || opts.migrateAppOrigin || opts.replaceRegistration || opts.configuration)
|
|
1622
|
+
throw Error('--abandon-replacement takes no payload or other setup options');
|
|
1623
|
+
await abandonWorkspaceReplacement();
|
|
1624
|
+
process.stdout.write('Unsent or rejected replacement abandoned; existing local setup preserved. Obtain a fresh App command for another setup.\n');
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1522
1627
|
if (setupTunnel && opts.check && opts.resume)
|
|
1523
1628
|
throw Error('Use either --check or --resume');
|
|
1524
|
-
if (setupTunnel && (opts.check || opts.resume) && (argument !== undefined || opts.file || opts.stdin || opts.migrateAppOrigin))
|
|
1629
|
+
if (setupTunnel && (opts.check || opts.resume) && (argument !== undefined || opts.file || opts.stdin || opts.migrateAppOrigin || opts.replaceRegistration))
|
|
1525
1630
|
throw Error('--check and --resume take no payload or migration flag');
|
|
1526
1631
|
if (setupTunnel && opts.check) {
|
|
1527
1632
|
await setupTunnelPreflight();
|
|
@@ -1531,10 +1636,22 @@ async function workspaceEnrollmentCommand(opts, setupTunnel = false, argument) {
|
|
|
1531
1636
|
}
|
|
1532
1637
|
let pending;
|
|
1533
1638
|
if (setupTunnel && opts.resume) {
|
|
1534
|
-
|
|
1535
|
-
if (
|
|
1536
|
-
|
|
1537
|
-
|
|
1639
|
+
const replacement = readWorkspaceReplacement();
|
|
1640
|
+
if (replacement) {
|
|
1641
|
+
if (opts.configuration && resolvePath(opts.configuration) !== replacement.configuration)
|
|
1642
|
+
throw Error('Resume replacement with its original configuration');
|
|
1643
|
+
await setupTunnelPreflight();
|
|
1644
|
+
const result = await finishWorkspaceReplacement(replacement);
|
|
1645
|
+
pending = { appOrigin: replacement.payload.appOrigin, origin: result.origin, hostWorkspaceId: result.hostWorkspaceId, rootCid: result.rootCid, challenge: replacement.payload.challenge, configuration: replacement.configuration };
|
|
1646
|
+
savePendingTunnelSetup(pending);
|
|
1647
|
+
clearWorkspaceReplacement();
|
|
1648
|
+
}
|
|
1649
|
+
else {
|
|
1650
|
+
pending = readPendingTunnelSetup();
|
|
1651
|
+
if (opts.configuration && resolvePath(opts.configuration) !== pending.configuration)
|
|
1652
|
+
throw Error(`This setup started with configuration ${pending.configuration}; resume it without --configuration`);
|
|
1653
|
+
await setupTunnelPreflight();
|
|
1654
|
+
}
|
|
1538
1655
|
}
|
|
1539
1656
|
else {
|
|
1540
1657
|
if (argument === undefined && !opts.file && !opts.stdin)
|
|
@@ -1544,18 +1661,49 @@ async function workspaceEnrollmentCommand(opts, setupTunnel = false, argument) {
|
|
|
1544
1661
|
if (opts.stdin && process.stdin.isTTY)
|
|
1545
1662
|
throw Error('--stdin requires a pipe; use an owned private --file instead');
|
|
1546
1663
|
const input = setupTunnel ? (argument !== undefined ? readTunnelSetupArgument(argument) : opts.stdin ? await readTunnelSetupStdin() : readTunnelSetupFile(opts.file)) : undefined;
|
|
1664
|
+
const previous = setupTunnel ? readWorkspaceBinding() : undefined;
|
|
1665
|
+
const replacement = setupTunnel ? readWorkspaceReplacement() : undefined;
|
|
1666
|
+
if (replacement && !opts.replaceRegistration)
|
|
1667
|
+
throw Error('An unfinished replacement exists. Run setup-tunnel --resume, use a fresh command for the same target with --replace-registration, or --abandon-replacement when retirement was never sent or definitively rejected. If retirement and cleanup completed but the successor cannot be recovered, remove the successor in the App and use --discard-retired-replacement');
|
|
1668
|
+
if (replacement && resolvePath(opts.configuration ?? defaultConfigPath()) !== replacement.configuration)
|
|
1669
|
+
throw Error('Replacement requires its original configuration');
|
|
1670
|
+
const target = previous && !replacement ? await inspectTunnelSetup(input) : undefined;
|
|
1671
|
+
const different = previous && !replacement && (!target || requiresWorkspaceReplacement(previous, target, opts.migrateAppOrigin));
|
|
1672
|
+
if (different && !(await confirmWorkspaceReplacement(previous, opts.replaceRegistration))) {
|
|
1673
|
+
if (!process.stdin.isTTY)
|
|
1674
|
+
throw Error('Replacement confirmation required; existing setup and grant preserved');
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1547
1677
|
if (setupTunnel) {
|
|
1548
1678
|
await setupTunnelPreflight();
|
|
1549
1679
|
await checkWorkspaceConfiguration(opts.configuration ?? defaultConfigPath());
|
|
1550
1680
|
}
|
|
1551
|
-
|
|
1681
|
+
let payload = setupTunnel ? await redeemTunnelSetup(input) : readWorkspacePayload(opts.file);
|
|
1682
|
+
verifyTunnelSetupTarget(target, payload);
|
|
1683
|
+
const replace = previous && !replacement && requiresWorkspaceReplacement(previous, { workspaceId: payload.challenge.workspaceId, serverCid: payload.serverCid, appOrigin: payload.appOrigin }, opts.migrateAppOrigin);
|
|
1684
|
+
if (replacement) {
|
|
1685
|
+
refreshWorkspaceReplacement(replacement, payload, opts.configuration ?? defaultConfigPath());
|
|
1686
|
+
payload = replacement.payload;
|
|
1687
|
+
}
|
|
1688
|
+
checkWorkspaceConnector(payload);
|
|
1552
1689
|
if (setupTunnel)
|
|
1553
1690
|
await ensureMinimalSetup(opts.configuration ?? defaultConfigPath());
|
|
1554
|
-
|
|
1691
|
+
let result;
|
|
1692
|
+
if (replacement) {
|
|
1693
|
+
result = await finishWorkspaceReplacement(replacement);
|
|
1694
|
+
}
|
|
1695
|
+
else if (replace) {
|
|
1696
|
+
const record = await beginWorkspaceReplacement(previous, payload, opts.configuration ?? defaultConfigPath());
|
|
1697
|
+
result = await finishWorkspaceReplacement(record);
|
|
1698
|
+
}
|
|
1699
|
+
else
|
|
1700
|
+
result = await enrollWorkspace(payload, opts.configuration, { migrateAppOrigin: opts.migrateAppOrigin, preserveProfile: setupTunnel || opts.preserveProfile });
|
|
1555
1701
|
pending = { appOrigin: payload.appOrigin, origin: result.origin, hostWorkspaceId: result.hostWorkspaceId, rootCid: result.rootCid, challenge: payload.challenge, configuration: resolvePath(opts.configuration ?? defaultConfigPath()) };
|
|
1556
|
-
//
|
|
1557
|
-
if (setupTunnel)
|
|
1702
|
+
// Keep both the replacement capability and the proof until the next recovery record is durable.
|
|
1703
|
+
if (setupTunnel) {
|
|
1558
1704
|
savePendingTunnelSetup(pending);
|
|
1705
|
+
clearWorkspaceReplacement();
|
|
1706
|
+
}
|
|
1559
1707
|
}
|
|
1560
1708
|
try {
|
|
1561
1709
|
await finishWorkspaceService(pending, opts.resume ? pending.configuration : opts.configuration, setupTunnel);
|
|
@@ -1649,7 +1797,10 @@ cOpt(program.command('setup-tunnel').argument('[payload]', 'minimal one-use v2 A
|
|
|
1649
1797
|
.option('--file <path>', 'owned private App grant or legacy payload file (chmod 600)')
|
|
1650
1798
|
.option('--stdin', 'read the private App grant or legacy payload from a pipe')
|
|
1651
1799
|
.option('--check', 'only verify host prerequisites; changes nothing and needs no payload')
|
|
1652
|
-
.option('--resume', 'finish
|
|
1800
|
+
.option('--resume', 'finish an interrupted replacement or setup; needs no new payload')
|
|
1801
|
+
.option('--discard-retired-replacement', 'forget a failed successor after retirement and local cleanup; remove it in the App first')
|
|
1802
|
+
.option('--abandon-replacement', 'discard an unsent or definitively rejected replacement after checking its private receipt')
|
|
1803
|
+
.option('--replace-registration', 'confirm retirement of the existing account registration when the new one differs')
|
|
1653
1804
|
.option('--migrate-app-origin', 'explicitly migrate the same workspace to its new account origin after verified signed proof')
|
|
1654
1805
|
.action(async (payload, opts) => workspaceEnrollmentCommand(opts, true, payload));
|
|
1655
1806
|
program.command('link-device').description('create a single-use workspace device connection code (expires in five minutes)')
|
|
@@ -1797,7 +1948,15 @@ async function parseFleetCli() {
|
|
|
1797
1948
|
async function runFleetCli() {
|
|
1798
1949
|
const stateDir = process.env[FLEET_PROXY_STATE_DIR_ENV];
|
|
1799
1950
|
const caller = process.env[FLEET_PROXY_CALLER_ENV];
|
|
1951
|
+
const entry = parseManagedEntry(process.argv.slice(2));
|
|
1800
1952
|
if (!stateDir || !caller) {
|
|
1953
|
+
// The pinned form exists for managed sessions; outside one there is no
|
|
1954
|
+
// supervisor to audit the call, so it is refused rather than run unaudited.
|
|
1955
|
+
if (entry.kind !== 'none') {
|
|
1956
|
+
console.error(`error: ${entry.kind === 'error' ? entry.message
|
|
1957
|
+
: `${MANAGED_CONFIGURATION_FLAG} is only valid inside a Fleet-managed agent session, where the supervisor audits the call`}`);
|
|
1958
|
+
throw new FleetCliExit(1, 'validation', 'not_started');
|
|
1959
|
+
}
|
|
1801
1960
|
await parseFleetCli();
|
|
1802
1961
|
return;
|
|
1803
1962
|
}
|
|
@@ -1832,6 +1991,15 @@ async function runFleetCli() {
|
|
|
1832
1991
|
const originalExit = process.exit;
|
|
1833
1992
|
process.exit = ((code) => { throw new FleetCliExit(code ?? 0); });
|
|
1834
1993
|
try {
|
|
1994
|
+
if (entry.kind === 'error') {
|
|
1995
|
+
console.error(`error: ${entry.message}`);
|
|
1996
|
+
throw new FleetCliExit(1, 'validation', 'not_started');
|
|
1997
|
+
}
|
|
1998
|
+
if (entry.kind === 'pinned') {
|
|
1999
|
+
// The supervisor audited the argv as typed; Commander parses the ordinary form.
|
|
2000
|
+
pinnedConfiguration = entry.configuration;
|
|
2001
|
+
process.argv = [process.argv[0], process.argv[1], ...entry.argv];
|
|
2002
|
+
}
|
|
1835
2003
|
await parseFleetCli();
|
|
1836
2004
|
}
|
|
1837
2005
|
catch (error) {
|
package/dist/config.d.ts
CHANGED
|
@@ -36,6 +36,12 @@ export type FilesystemMode = 'read-only' | 'workspace' | 'unrestricted';
|
|
|
36
36
|
export type UnattendedMode = 'deny' | 'wait';
|
|
37
37
|
/** Monitor wake policy: preserve legacy booleans and add one explicit safe boundary. */
|
|
38
38
|
export type MonitorInterrupt = boolean | 'after_tool';
|
|
39
|
+
/**
|
|
40
|
+
* Workflows Fleet prepares native harness execution policy for. Declared per
|
|
41
|
+
* Agent; never inferred from a role's name or persona, and never inherited.
|
|
42
|
+
*/
|
|
43
|
+
export declare const MANAGED_CLI_WORKFLOW_IDS: readonly ["task-workflow"];
|
|
44
|
+
export type ManagedCliWorkflowId = (typeof MANAGED_CLI_WORKFLOW_IDS)[number];
|
|
39
45
|
export interface CommonPermissions {
|
|
40
46
|
approval: ApprovalMode;
|
|
41
47
|
filesystem: FilesystemMode;
|
|
@@ -146,6 +152,8 @@ export interface RoleConfig {
|
|
|
146
152
|
owner_channel?: OwnerChannelConfigInput;
|
|
147
153
|
worklog?: WorklogPolicyInput;
|
|
148
154
|
auth_proxy?: Partial<AuthProxyConfig>;
|
|
155
|
+
/** Fleet-prepared CLI workflows this role may run outside its command sandbox. */
|
|
156
|
+
managed_cli?: ManagedCliWorkflowId[];
|
|
149
157
|
}
|
|
150
158
|
export type AgentSelection<T extends Record<string, unknown> = Record<string, unknown>> = {
|
|
151
159
|
ref: string;
|
|
@@ -167,6 +175,8 @@ export interface AgentDefinition {
|
|
|
167
175
|
owner_channel?: OwnerChannelConfigInput;
|
|
168
176
|
worklog?: WorklogPolicyInput;
|
|
169
177
|
auth_proxy?: Partial<AuthProxyConfig>;
|
|
178
|
+
/** Fleet-prepared CLI workflows this Agent may run outside its command sandbox. */
|
|
179
|
+
managed_cli?: ManagedCliWorkflowId[];
|
|
170
180
|
/** Scheduled turns scoped only to temporary launches; persistent Agents reject this field. */
|
|
171
181
|
loops?: AgentLoopsConfig;
|
|
172
182
|
}
|
package/dist/config.js
CHANGED
|
@@ -24,6 +24,11 @@ export const NOTIFY_EVENT_TYPES = [
|
|
|
24
24
|
'message_received', 'file_received', 'sibling_contact_added', 'local_contact_request',
|
|
25
25
|
'pending_message', 'contact_restored', 'inbound_error', 'state_import_failed',
|
|
26
26
|
];
|
|
27
|
+
/**
|
|
28
|
+
* Workflows Fleet prepares native harness execution policy for. Declared per
|
|
29
|
+
* Agent; never inferred from a role's name or persona, and never inherited.
|
|
30
|
+
*/
|
|
31
|
+
export const MANAGED_CLI_WORKFLOW_IDS = ['task-workflow'];
|
|
27
32
|
/** Default wake sources when a role does not list its own. */
|
|
28
33
|
export const DEFAULT_WAKE_SOURCES = ['message_received', 'file_received', 'local_contact_request', 'pending_message'];
|
|
29
34
|
const MONITOR_KEYS = [
|
|
@@ -139,7 +144,7 @@ export const ROLE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
|
139
144
|
const ROLE_KEYS = [
|
|
140
145
|
'harness', 'session', 'session_options', 'permissions', 'identity', 'cwd', 'coordinator', 'mission', 'persona', 'bio',
|
|
141
146
|
'briefing_file', 'model', 'effort', 'model_chain', 'max_tokens', 'autocompact_pct', 'env', 'oversee', 'harness_options',
|
|
142
|
-
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
|
|
147
|
+
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'managed_cli',
|
|
143
148
|
];
|
|
144
149
|
export const ROLE_PRESET_KEYS = ['mission', 'persona', 'bio', 'briefing_file'];
|
|
145
150
|
export const BRAIN_PRESET_KEYS = [
|
|
@@ -148,7 +153,7 @@ export const BRAIN_PRESET_KEYS = [
|
|
|
148
153
|
];
|
|
149
154
|
export const AGENT_KEYS = [
|
|
150
155
|
'role', 'brain', 'permissions', 'identity', 'cwd', 'coordinator', 'env', 'oversee',
|
|
151
|
-
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'loops',
|
|
156
|
+
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'loops', 'managed_cli',
|
|
152
157
|
];
|
|
153
158
|
const AGENT_INSTANCE_KEYS = ['template', 'overrides'];
|
|
154
159
|
const TEMPLATE_FORBIDDEN_KEYS = ['identity'];
|
|
@@ -236,6 +241,12 @@ function validateAgentScalars(value, file, id) {
|
|
|
236
241
|
if (current !== undefined && (typeof current !== 'string' || !current.trim()))
|
|
237
242
|
schemaError(file, `/agents/${id}/${key}`, 'must be a non-blank string');
|
|
238
243
|
}
|
|
244
|
+
if (value.managed_cli !== undefined) {
|
|
245
|
+
const ids = value.managed_cli;
|
|
246
|
+
if (!Array.isArray(ids) || ids.some(entry => !MANAGED_CLI_WORKFLOW_IDS.includes(entry))
|
|
247
|
+
|| new Set(ids).size !== ids.length)
|
|
248
|
+
schemaError(file, `/agents/${id}/managed_cli`, `must be a list of distinct workflow IDs; supported: ${MANAGED_CLI_WORKFLOW_IDS.join(', ')}`);
|
|
249
|
+
}
|
|
239
250
|
if (value.coordinator !== undefined
|
|
240
251
|
&& (typeof value.coordinator !== 'string' || !ROLE_NAME_RE.test(value.coordinator)))
|
|
241
252
|
schemaError(file, `/agents/${id}/coordinator`, 'must be a valid Agent ID');
|