@ours.network/install 1.2.1-nightly.32 → 1.2.1-nightly.34

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
@@ -8,9 +8,22 @@ This permanent command contains no tunnel credentials or enrollment expiry. On a
8
8
 
9
9
  `--disable-fleet-agents-setup` installs the Fleet CLI and saves the choice without opening its agent/model wizard or rewriting existing Fleet configuration. Configure agents in the App after device linking. Complete presets still work unattended; partial presets require a terminal and cannot use `OURS_ASSUME_YES`.
10
10
 
11
- The separate `ours-fleet setup-tunnel --file /private/setup-payload` step requires the matching Fleet follow-up release. Its private App payload remains scoped, signed-root-bound and expiring. Then run `ours-fleet link-device` and paste its private single-use code into the App. Do not put the expiring tunnel payload in the permanent installer command.
11
+ ### Fleet task commands from a harness command sandbox
12
12
 
13
- Native packages currently provide server-only installation; full Fleet/App onboarding requires the supported gateway (Docker or Podman). The installer refuses unsupported native full-stack setup before changing anything. This PR does not add a native gateway or release unpublished Fleet commands.
13
+ After Fleet configuration, and again on update, the installer asks the exact Fleet it installed to run `ours-fleet managed-cli setup`. Fleet owns that policy; the installer writes no Codex rule or Claude setting itself, and the step starts no agent session and no model. It runs only when the installed Fleet declares the `managed-cli.setup-v1` capability; the Fleet release this installer pins does. With an earlier Fleet (a retained or development installation) it is skipped (and `--fleet-task-workflow` reports that it was not applied), and it never fails an installation: an unsupported combination is printed with Fleet's reason and left exactly as sandboxed as before.
14
+
15
+ Nothing is prepared unless an Agent declares `managed_cli: [task-workflow]`. To opt agents in from the installer, name them:
16
+
17
+ ```sh
18
+ ours-install client --config /private/profile.json --integrations codex,fleet \
19
+ --fleet-settings /private/fleet-settings.json --fleet-task-workflow FleetCoordinator
20
+ ```
21
+
22
+ `--fleet-task-workflow` takes a comma-separated list of Fleet agent names, requires the Fleet integration and conflicts with `--disable-fleet-agents-setup`. It adds that one key to each named Agent and changes no permission. The prepared scope is the packaged Coordinator task workflow (`task create|start|finish|block|unblock|review|list|show`, `room show|members`, template/plan/help inspection) for one pinned Fleet configuration; `task start` and `task finish` provision and retire rooms and agents, which is real authority, and the installer prints that scope. The result is generated configuration only. It is not evidence that a command reached a supervisor: `ours-fleet managed-cli status` and `ours-fleet doctor` report what running agents actually observed. Fleet's `docs/validation/managed-cli-permissions.md` lists the qualified harnesses and platforms.
23
+
24
+ The separate `ours-fleet setup-tunnel <encoded-v2-grant>` step is supported by the pinned Fleet release. Its private App payload remains scoped, signed-root-bound and expiring. Fleet prints its QR and private single-use connection code immediately after successful setup; scan or paste it into the App. `ours-fleet link-device` remains available for additional devices or expired codes. Do not put the expiring tunnel payload in the permanent installer command.
25
+
26
+ Native packages currently provide server-only installation; full Fleet/App onboarding requires the supported gateway (Docker or Podman). The installer refuses unsupported native full-stack setup before changing anything. This flow does not add a native gateway.
14
27
 
15
28
  See [one-connection setup, localhost:4050 and systemd migration](GATEWAY_SETUP.md) for the gateway-only client workflow.
16
29
 
@@ -624,4 +637,27 @@ Existing installation records keep their ports and identities. A listener acquir
624
637
  by another process after preflight still causes startup to fail safely; no foreign
625
638
  process is stopped and no retained installation is silently moved.
626
639
 
627
- Workspace setup pins Fleet `1.2.0-nightly.41` with its recorded registry integrity. This release includes first-time Fleet setup through the host API for the account App, direct App access to the workspace services with the device credential, separate `ours-fleet setup-tunnel` and `ours-fleet link-device` commands, host prerequisite checks before the one-time workspace proof (`setup-tunnel --check`), `setup-tunnel --resume` to finish a setup that stopped after the host saved its private pending setup record (not when the proof's answer was lost, or after the setup window expired with an unconfirmed non-default port), and persists the available Fleet loopback port used by the workspace tunnel. It also accepts a setup-generated configuration left behind after the host enrollment state was removed, runs the host Codex when it is at least as new as the packaged one, offers only the Codex models the signed-in account can use, and removes the temporary setup contact after confirmation. Fleet serves no pages and opens no browser: every screen is in the App. Fleet also serves the workspace notification routes, produces notifications through the server gateway with the imported producer credential, accepts the App's device credential on the notification presence socket so an open App receives no push, and defaults agent wakes to `monitor.interrupt: after_tool` (Hermes: no interrupt; role snapshots saved before this default keep no interrupt). Existing installation identities and configured ports remain retained; a retained-port collision fails safely.
640
+ Workspace setup pins Fleet `1.2.0-nightly.45` with its recorded registry integrity. This release includes first-time Fleet setup through the host API for the account App, direct App access to the workspace services with the device credential, v2 positional one-use grants and automatic QR/connection-code output from `ours-fleet setup-tunnel`, optional private file/stdin inputs for legacy v1 payloads, a separate `ours-fleet link-device` command for additional devices, host prerequisite checks before the one-time workspace proof (`setup-tunnel --check`), `setup-tunnel --resume` to finish a setup that stopped after the host saved its private pending setup record (not when the proof's answer was lost, or after the setup window expired with an unconfirmed non-default port), and persists the available Fleet loopback port used by the workspace tunnel. It also accepts a setup-generated configuration left behind after the host enrollment state was removed, runs the host Codex when it is at least as new as the packaged one, offers only the Codex models the signed-in account can use, and removes the temporary setup contact after confirmation. Fleet serves no pages and opens no browser: every screen is in the App. Fleet also serves the workspace notification routes, produces notifications through the server gateway with the imported producer credential, accepts the App's device credential on the notification presence socket so an open App receives no push, and defaults agent wakes to `monitor.interrupt: after_tool` (Hermes: no interrupt; role snapshots saved before this default keep no interrupt). Existing installation identities and configured ports remain retained; a retained-port collision fails safely.
641
+
642
+ ### App tunnel setup prerelease sequencing
643
+
644
+ The simplified App flow shows one copyable `ours-fleet setup-tunnel <encoded-v2-grant>`
645
+ command for the installed host, including an SSH session on a VPS. Its argument
646
+ contains only the existing v2 single-use grant (`version`, `appOrigin`, `code`,
647
+ `expiresAt`), never a connector token or broad provider credential. The command can
648
+ appear in shell history and process listings: keep it private and remove it from
649
+ history after use. Base64 is encoding, not encryption. Server-side expiry and
650
+ one-time redemption remain enforced. Fleet checks prerequisites before HTTPS POST
651
+ redemption; it never prints the grant or puts it in a URL, and retains existing
652
+ signed-root binding and scoped connector token-file controls. It prints the QR and
653
+ connection code automatically; the App checks readiness and opens the code field.
654
+ Private `--file`/`--stdin` transports remain optional and are the only transports
655
+ for legacy v1 payloads with long-lived credentials.
656
+
657
+ The nightly installer release manifest selects published Fleet `1.2.0-nightly.45`
658
+ and its actual registry integrity through the normal release manifest/source-policy
659
+ generators. This release includes v2 argument and private-input support. The former
660
+ nightly.41 selection predates this capability. Merge and publish the repinned
661
+ installer before deploying the App; the pin change alone is not an installer
662
+ publication. Existing v1 private-file enrollment remains compatible. The legacy
663
+ App wrapper refuses an older Fleet before reading or redeeming its grant.
@@ -13,7 +13,7 @@
13
13
  "@ours.network/codex": "1.2.0-nightly.10",
14
14
  "@ours.network/cowork": "1.3.3-nightly.20261001.6d49d3a",
15
15
  "@ours.network/daemon": "3.8.1-nightly.7",
16
- "@ours.network/fleet": "1.2.0-nightly.41",
16
+ "@ours.network/fleet": "1.2.0-nightly.45",
17
17
  "@ours.network/mcp": "1.2.0-nightly.10",
18
18
  "@ours.network/messenger-server": "1.0.31-nightly.14",
19
19
  "@ours.network/notifications": "0.3.1-nightly.1",
@@ -712,9 +712,9 @@
712
712
  }
713
713
  },
714
714
  "node_modules/@fastify/websocket": {
715
- "version": "11.3.2",
716
- "resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.3.2.tgz",
717
- "integrity": "sha512-WATdeYgpwf/3/AmKJ0TcP9dQtThJEeppbfgV5t2jkp4BJT6KgvKBgIK2sns/2xlyfoxZEiYkE4JBxH3PwVKkrw==",
715
+ "version": "11.3.3",
716
+ "resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.3.3.tgz",
717
+ "integrity": "sha512-NBJoykKpFwrdX9Ezod1rt30p5NoPFu7vgeuUowSM3JVI5/hFNRPhetqkWMTiF/ifIzctwitbJut1gZ6FDg1mwQ==",
718
718
  "funding": [
719
719
  {
720
720
  "type": "github",
@@ -1001,9 +1001,9 @@
1001
1001
  }
1002
1002
  },
1003
1003
  "node_modules/@ours.network/fleet": {
1004
- "version": "1.2.0-nightly.41",
1005
- "resolved": "https://registry.npmjs.org/@ours.network/fleet/-/fleet-1.2.0-nightly.41.tgz",
1006
- "integrity": "sha512-7Ws+tGwSgoQVaAb0va6RNzs2lLH0TBJAOTWJn/rt4UcWJFDXzGyoWLgGou2KdKM9S54z4x0E7BGeRM5ZefLw9g==",
1004
+ "version": "1.2.0-nightly.45",
1005
+ "resolved": "https://registry.npmjs.org/@ours.network/fleet/-/fleet-1.2.0-nightly.45.tgz",
1006
+ "integrity": "sha512-Mwcwt6wpS74QgmLYTmtJaq2nLtQMK7+xmn+gARkWiLH0klzEClPNkpPPs0p8DWMiQtsAShq94iS5iOo/GzzL2Q==",
1007
1007
  "license": "FSL-1.1-Apache-2.0",
1008
1008
  "dependencies": {
1009
1009
  "@agentclientprotocol/sdk": "^1.3.0",
@@ -2120,9 +2120,9 @@
2120
2120
  "license": "MIT"
2121
2121
  },
2122
2122
  "node_modules/hono": {
2123
- "version": "4.13.12",
2124
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.12.tgz",
2125
- "integrity": "sha512-6E2QDAc9Ick9Sq77ZrGS/dk2WUYni91aufTw6LJKpV7w8kW5/GxVUc650FOADOmlwg3K+f7Pun6XlV+pYmW6gw==",
2123
+ "version": "4.13.13",
2124
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz",
2125
+ "integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==",
2126
2126
  "license": "MIT",
2127
2127
  "engines": {
2128
2128
  "node": ">=16.9.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "channel": "nightly",
4
- "installerVersion": "1.2.1-nightly.32",
4
+ "installerVersion": "1.2.1-nightly.34",
5
5
  "packages": {
6
6
  "@ours.network/sdk": {
7
7
  "version": "3.8.1-nightly.15",
@@ -28,8 +28,8 @@
28
28
  "integrity": "sha512-rIkrtixoHqBfgCKDDCEYFl4W2Zzh0hOw3axX2+GppQ9gYkujKrk9M2KLGoYhu8RwwTMRl497jgMcTBe7UAHCgA=="
29
29
  },
30
30
  "@ours.network/fleet": {
31
- "version": "1.2.0-nightly.41",
32
- "integrity": "sha512-7Ws+tGwSgoQVaAb0va6RNzs2lLH0TBJAOTWJn/rt4UcWJFDXzGyoWLgGou2KdKM9S54z4x0E7BGeRM5ZefLw9g=="
31
+ "version": "1.2.0-nightly.45",
32
+ "integrity": "sha512-Mwcwt6wpS74QgmLYTmtJaq2nLtQMK7+xmn+gARkWiLH0klzEClPNkpPPs0p8DWMiQtsAShq94iS5iOo/GzzL2Q=="
33
33
  },
34
34
  "@ours.network/mcp": {
35
35
  "version": "1.2.0-nightly.10",
@@ -2,7 +2,7 @@
2
2
  "release": {
3
3
  "schema": 1,
4
4
  "channel": "nightly",
5
- "installerVersion": "1.2.1-nightly.32",
5
+ "installerVersion": "1.2.1-nightly.34",
6
6
  "packages": {
7
7
  "@ours.network/sdk": {
8
8
  "version": "3.8.1-nightly.15",
@@ -29,8 +29,8 @@
29
29
  "integrity": "sha512-rIkrtixoHqBfgCKDDCEYFl4W2Zzh0hOw3axX2+GppQ9gYkujKrk9M2KLGoYhu8RwwTMRl497jgMcTBe7UAHCgA=="
30
30
  },
31
31
  "@ours.network/fleet": {
32
- "version": "1.2.0-nightly.41",
33
- "integrity": "sha512-7Ws+tGwSgoQVaAb0va6RNzs2lLH0TBJAOTWJn/rt4UcWJFDXzGyoWLgGou2KdKM9S54z4x0E7BGeRM5ZefLw9g=="
32
+ "version": "1.2.0-nightly.45",
33
+ "integrity": "sha512-Mwcwt6wpS74QgmLYTmtJaq2nLtQMK7+xmn+gARkWiLH0klzEClPNkpPPs0p8DWMiQtsAShq94iS5iOo/GzzL2Q=="
34
34
  },
35
35
  "@ours.network/mcp": {
36
36
  "version": "1.2.0-nightly.10",
@@ -87,7 +87,7 @@
87
87
  },
88
88
  "@ours.network/fleet": {
89
89
  "type": "npm",
90
- "version": "1.2.0-nightly.41"
90
+ "version": "1.2.0-nightly.45"
91
91
  },
92
92
  "@ours.network/mcp": {
93
93
  "type": "npm",
package/lib/effects.mjs CHANGED
@@ -1436,6 +1436,13 @@ export function networkEffects(effects) {
1436
1436
  const localPackages = Object.fromEntries(integrations.filter(n => n !== 'fleet').map(name => [name, join(root, 'node_modules', '@ours.network', name)]));
1437
1437
  return { localPackages, packages: {}, cliBin, fleetBin: integrations.includes('fleet') ? join(root, 'node_modules/.bin/ours-fleet') : null };
1438
1438
  },
1439
+ /** Capability tokens the given Fleet executable reports for itself; [] when it reports none. */
1440
+ async fleetCapabilities(fleetBin, env = null) {
1441
+ try {
1442
+ const capabilities = JSON.parse((await effects.run(fleetBin, ['version', '--json'], { env })).stdout).capabilities;
1443
+ return Array.isArray(capabilities) ? capabilities.filter(item => typeof item === 'string') : [];
1444
+ } catch { return []; }
1445
+ },
1439
1446
  async prepareClientMarketplace(name, packagePath) {
1440
1447
  const acquisitionRoot = dirname(dirname(dirname(packagePath)));
1441
1448
  const sourcePath = join(acquisitionRoot, 'sources.json');
@@ -993,6 +993,58 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
993
993
  return rows;
994
994
  }
995
995
 
996
+ export const FLEET_MANAGED_CLI_CAPABILITY = 'managed-cli.setup-v1';
997
+
998
+ /**
999
+ * Ask the Fleet release that was just installed to prepare and validate its own
1000
+ * managed CLI policy. Fleet owns the commands, paths and harness rules; nothing
1001
+ * about them is duplicated here. The command is static: it starts no agent
1002
+ * session and no model. Its findings are reported, never turned into an
1003
+ * installation failure, and never widened on the operator's behalf.
1004
+ */
1005
+ export async function runFleetManagedCliSetup(args, effects, plan, target) {
1006
+ const fleet = args.acquiredFleet || plan.init[0];
1007
+ const env = target.mode === 'host-profile' ? profileEnv(target) : daemonEnv(target.stateDir, target.port);
1008
+ const requested = args.fleetTaskWorkflowAgents ?? [];
1009
+ const manual = `${fleet === plan.init[0] ? fleet : shellQuote(fleet)} managed-cli setup --configuration ${shellQuote(plan.configPath)}`;
1010
+ // Version-matched: the capability is asked of the exact Fleet that was installed.
1011
+ const capabilities = await effects.fleetCapabilities(fleet, env);
1012
+ if (!capabilities.includes(FLEET_MANAGED_CLI_CAPABILITY)) {
1013
+ if (requested.length) effects.out(warn(`The installed Fleet does not provide managed CLI setup (${FLEET_MANAGED_CLI_CAPABILITY}); --fleet-task-workflow was not applied to ${requested.join(', ')} and nothing was changed.`));
1014
+ return { state: 'unavailable' };
1015
+ }
1016
+ let report;
1017
+ try {
1018
+ const result = await effects.run(fleet, ['managed-cli', 'setup', '--configuration', plan.configPath, '--json',
1019
+ ...requested.flatMap(agent => ['--enable', agent])], { env, allowCodes: [0, 1] });
1020
+ report = JSON.parse(result.stdout ?? '');
1021
+ if (report?.version !== 1 || !Array.isArray(report.roles)) throw new Error('unrecognised report');
1022
+ } catch (error) {
1023
+ effects.out(warn(`Fleet managed CLI setup did not complete (${installerFailure(error)}). Nothing else was changed; retry: ${manual}`));
1024
+ return { state: 'failed' };
1025
+ }
1026
+ for (const item of report.enabled ?? [])
1027
+ effects.out(ok(`${item.changed ? 'Enabled' : 'Already enabled:'} Fleet task commands for ${item.agent} (${item.file}). Its permissions were not changed.`));
1028
+ const declared = report.roles.filter(role => role.setup?.state !== 'not-declared');
1029
+ if (!declared.length) {
1030
+ effects.out(info('No Fleet agent is set up to run Fleet task commands from a command sandbox; nothing was generated and no permission changed.'));
1031
+ effects.out(info(`To allow it for one agent, re-run with --fleet-task-workflow <Agent> or run: ${manual} --enable <Agent>`));
1032
+ return { state: 'none', report };
1033
+ }
1034
+ const good = ['prepared', 'generated-at-launch', 'not-required'];
1035
+ for (const role of declared) {
1036
+ const line = `${role.role} (${role.harness}/${role.session}): ${role.setup.state} — ${role.setup.detail}`;
1037
+ effects.out((good.includes(role.setup.state) ? ok : warn)(`Fleet task commands, ${line}`));
1038
+ for (const scope of role.scope ?? []) effects.out(info(` ${scope}`));
1039
+ for (const note of role.warnings ?? []) effects.out(info(` note: ${note}`));
1040
+ if (role.session_policy === 'restart-required')
1041
+ effects.out(warn(` ${role.role} is still running the previous policy; restart it (ours-fleet restart ${role.role}) to load this one.`));
1042
+ }
1043
+ for (const item of report.removed ?? []) effects.out(info(`Removed obsolete Fleet-generated policy ${item.path} (${item.detail ?? 'no longer needed'}).`));
1044
+ effects.out(info('This is generated configuration only. It was not executed: no agent session or model was started, and it is not evidence that a command reached a supervisor. Check with: ours-fleet doctor'));
1045
+ return { state: declared.every(role => good.includes(role.setup.state)) ? 'prepared' : 'attention', report };
1046
+ }
1047
+
996
1048
  /** Install Fleet and let its interactive wizard publish the owned configuration. */
997
1049
  export async function runFleetPhase(args, effects, { target, isDefaultStateDir }) {
998
1050
  effects.out(heading('ours-fleet (your always-online agent team)'));
@@ -1012,6 +1064,8 @@ export async function runFleetPhase(args, effects, { target, isDefaultStateDir }
1012
1064
  if (args.disableFleetAgentsSetup) {
1013
1065
  if (!install.ok) return { key: 'fleet', label: plan.label, state: 'failed', note: 'Fleet installation failed' };
1014
1066
  effects.out(ok('Fleet CLI installed; agent and model setup deferred to the App. Existing Fleet configuration was retained.'));
1067
+ // A retained configuration may already declare the workflow; its generated policy names the package path this run just replaced.
1068
+ if (!args.dryRun && effects.readText(plan.configPath) !== null) await runFleetManagedCliSetup(args, effects, plan, target);
1015
1069
  return { key: 'fleet', label: plan.label, state: 'installed', note: 'agents setup deferred' };
1016
1070
  }
1017
1071
  // Fleet owns its v2 manifest, roles, models and permissions. With no prepared
@@ -1037,6 +1091,7 @@ export async function runFleetPhase(args, effects, { target, isDefaultStateDir }
1037
1091
  return { key: 'fleet', label: plan.label, state: 'failed', note: 'Fleet configuration was not published' };
1038
1092
  }
1039
1093
  effects.out(ok('ours-fleet installed and initialized; no fleet roles were started.'));
1094
+ if (!args.dryRun) await runFleetManagedCliSetup(args, effects, plan, target);
1040
1095
  if (plan.instruction) effects.out(info(plan.instruction));
1041
1096
  return { key: 'fleet', label: plan.label, state: 'installed', note: `configured at ${plan.configPath}; stopped` };
1042
1097
  }
@@ -1517,7 +1572,8 @@ export async function runClientCommand(command, effects) {
1517
1572
  phase = 'Acquire client packages';
1518
1573
  const exactSuite = await effects.acquireClientPackages(imported.configPath, imported.settings.sourcesPath, integrations, { refresh: !!command.preset });
1519
1574
  const args = { assumeYes: true, dryRun: false, channel: 'latest', clientIntegrations: integrations,
1520
- acquiredFleet: exactSuite.fleetBin, fleetSettingsPath: imported.settings.fleetSettingsPath, disableFleetAgentsSetup: imported.settings.disableFleetAgentsSetup };
1575
+ acquiredFleet: exactSuite.fleetBin, fleetSettingsPath: imported.settings.fleetSettingsPath, disableFleetAgentsSetup: imported.settings.disableFleetAgentsSetup,
1576
+ fleetTaskWorkflowAgents: command.fleetTaskWorkflowAgents };
1521
1577
  const target = { mode: 'host-profile', managed: true, configPath: imported.configPath, profile: imported.profile, endpoint: imported.profile.endpoint };
1522
1578
  effects.out(progress(2, 4, 'Client integrations', 'Register the selected agent integrations.'));
1523
1579
  phase = 'Register client integrations';
@@ -7,7 +7,7 @@ const integrations = ['codex', 'claude-code', 'fleet'];
7
7
  const valueFlags = new Map(Object.entries({
8
8
  '--container-engine': 'containerEngine', '--server-url': 'serverUrl', '--scope': 'scope', '--action': 'operation', '--mode': 'mode', '--state-dir': 'stateDir',
9
9
  '--username': 'localUsername', '--name': 'name', '--surname': 'surname', '--hostname': 'hostname',
10
- '--identity-name': 'identityName', '--integrations': 'integrations', '--fleet-settings': 'fleetSettingsPath',
10
+ '--identity-name': 'identityName', '--integrations': 'integrations', '--fleet-settings': 'fleetSettingsPath', '--fleet-task-workflow': 'fleetTaskWorkflowAgents',
11
11
  '--config': 'config', '--sources': 'sources', '--migrate-from': 'migrateFrom', '--port': 'port', '--cowork-port': 'coworkPort', '--messenger-port': 'messengerPort',
12
12
  }));
13
13
  const boolFlags = new Map([['--compatible', 'compatible'], ['--dry-run', 'dryRun'], ['--migrate', 'migrate'], ['--disable-fleet-agents-setup', 'disableFleetAgentsSetup']]);
@@ -54,6 +54,15 @@ export function validateSetupOptions(input, { interactive = input?.interactive =
54
54
  } else if (options.hostname !== undefined) throw new Error('--hostname requires username, name and surname');
55
55
  if (options.disableFleetAgentsSetup !== undefined && typeof options.disableFleetAgentsSetup !== 'boolean') throw new Error('disableFleetAgentsSetup must be a boolean');
56
56
  if (options.disableFleetAgentsSetup && options.fleetSettingsPath) throw new Error('--disable-fleet-agents-setup conflicts with --fleet-settings');
57
+ if (options.fleetTaskWorkflowAgents !== undefined) {
58
+ // Fleet owns what the workflow is; the installer only carries which agents the operator named.
59
+ const agents = options.fleetTaskWorkflowAgents;
60
+ if (!Array.isArray(agents) || !agents.length || agents.some(name => typeof name !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(name)) || new Set(agents).size !== agents.length)
61
+ throw new Error('--fleet-task-workflow requires unique Fleet agent names separated by commas');
62
+ if (!options.integrations?.includes?.('fleet')) throw new Error('--fleet-task-workflow requires the fleet integration');
63
+ if (options.disableFleetAgentsSetup) throw new Error('--fleet-task-workflow conflicts with --disable-fleet-agents-setup: no agents are configured by this run');
64
+ options.fleetTaskWorkflowAgents = [...agents];
65
+ }
57
66
  const missing = [];
58
67
  if (server) for (const [key, flag] of [['mode', '--mode'], ['stateDir', '--state-dir'], ...(options.migrateFrom === undefined ? [['identityName', '--identity-name']] : [])]) if (!nonempty(options[key])) missing.push(flag);
59
68
  if (client && options.integrations === undefined) missing.push('--integrations (use none to skip)');
@@ -135,6 +144,7 @@ export function parseSetupArgs(argv, { home, validate = true } = {}) {
135
144
  if (!nonempty(value) || value.startsWith('--')) throw new Error(`${flag} requires a value`);
136
145
  const key = valueFlags.get(flag);
137
146
  if (key === 'integrations') put(key, value === 'none' ? [] : value.split(',').map(item => item.trim()));
147
+ else if (key === 'fleetTaskWorkflowAgents') put(key, value.split(',').map(item => item.trim()));
138
148
  else if (Object.hasOwn(defaults, key)) {
139
149
  if (!/^[1-9]\d*$/.test(value)) throw new Error(`${flag} requires an integer port`);
140
150
  put(key, Number(value)); options.explicitPorts.push(key);
package/lib/setup.mjs CHANGED
@@ -136,10 +136,11 @@ export async function executeSetupPlan(plan, effects, { server = runServerComman
136
136
  if (plan.scope !== 'server') {
137
137
  const result = await client({ role: 'client', operation: 'install', config: clientConfig,
138
138
  integrations: plan.integrations, fleetSettingsPath: plan.fleetSettingsPath, sourcePolicy: clientPolicy,
139
- disableFleetAgentsSetup: plan.disableFleetAgentsSetup, preset: true, nonInteractive: !plan.interactive }, effects);
139
+ disableFleetAgentsSetup: plan.disableFleetAgentsSetup, fleetTaskWorkflowAgents: plan.fleetTaskWorkflowAgents,
140
+ preset: true, nonInteractive: !plan.interactive }, effects);
140
141
  if (result !== 0) return result;
141
142
  }
142
- if (plan.disableFleetAgentsSetup && plan.integrations?.includes('fleet')) effects.out(info('Fleet is installed. Run ours-fleet setup-tunnel with the private expiring App setup file, then ours-fleet link-device. Configure agents and models in the App after linking.'));
143
+ if (plan.disableFleetAgentsSetup && plan.integrations?.includes('fleet')) effects.out(info('Fleet is installed. Run ours-fleet setup-tunnel with the expiring one-use payload from the App command; a compatible Fleet prints the QR and connection code automatically. Configure agents and models in the App after linking.'));
143
144
  effects.out(ok(`Requested ${plan.operation} completed. Existing identities were retained.`));
144
145
  if (plan.integrations?.includes('fleet') && !plan.disableFleetAgentsSetup) effects.out(info('Fleet is configured but stopped. Review its settings, then run ours-fleet doctor, ours-fleet config and ours-fleet up.'));
145
146
  return 0;
package/lib/usage.mjs CHANGED
@@ -37,6 +37,7 @@ export const USAGE = `ours-install — interactive setup with optional CLI prese
37
37
  --identity-name NAME desired Human name for a fresh server; existing root is retained
38
38
  --integrations LIST codex,claude-code,fleet; use none to skip clients explicitly
39
39
  --fleet-settings PATH JSON settings for Fleet; required unless Fleet agents setup is disabled
40
+ --fleet-task-workflow A[,B] let these Fleet agents run Fleet task commands outside their command sandbox
40
41
  --config PATH complete connection profile for client-only setup
41
42
  --sources PATH explicit full development source policy override
42
43
  --port N public gateway port for Docker (default 3050; e.g. 4050)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/install",
3
- "version": "1.2.1-nightly.32",
3
+ "version": "1.2.1-nightly.34",
4
4
  "private": false,
5
5
  "description": "The all-in-one ours.network installer: one shared daemon, MCP, cowork, Telegram, Fleet initialization, harness plugins, Human identity, progress UI, and guided next steps.",
6
6
  "type": "module",