@ours.network/fleet 1.1.4 → 1.2.0-nightly.1

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.
Files changed (48) hide show
  1. package/README.md +137 -1
  2. package/dist/briefing.js +4 -3
  3. package/dist/build-info.json +5 -5
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +13 -6
  6. package/dist/client-profile.d.ts +17 -0
  7. package/dist/client-profile.js +115 -0
  8. package/dist/creation.js +10 -7
  9. package/dist/daemon-recovery.d.ts +2 -0
  10. package/dist/daemon-recovery.js +53 -2
  11. package/dist/docs.d.ts +1 -1
  12. package/dist/docs.js +23 -2
  13. package/dist/doctor.js +62 -6
  14. package/dist/harness/acp-mcp.d.ts +14 -0
  15. package/dist/harness/acp-mcp.js +68 -0
  16. package/dist/harness/claude-code.js +1 -69
  17. package/dist/harness/codex.js +4 -0
  18. package/dist/harness/hermes-compatibility.d.ts +24 -0
  19. package/dist/harness/hermes-compatibility.js +191 -0
  20. package/dist/harness/hermes-config.d.ts +12 -0
  21. package/dist/harness/hermes-config.js +367 -0
  22. package/dist/harness/hermes-permissions.d.ts +4 -0
  23. package/dist/harness/hermes-permissions.js +36 -0
  24. package/dist/harness/hermes-session.d.ts +24 -0
  25. package/dist/harness/hermes-session.js +85 -0
  26. package/dist/harness/hermes-startup.d.ts +3 -0
  27. package/dist/harness/hermes-startup.js +21 -0
  28. package/dist/harness/hermes.d.ts +5 -0
  29. package/dist/harness/hermes.js +62 -0
  30. package/dist/harness/registry.js +1 -1
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/dist/init-wizard.d.ts +11 -6
  34. package/dist/init-wizard.js +33 -0
  35. package/dist/monitor.d.ts +34 -3
  36. package/dist/monitor.js +174 -74
  37. package/dist/owner-channel/channel.js +4 -2
  38. package/dist/owner-channel/ours-client.d.ts +12 -5
  39. package/dist/owner-channel/ours-client.js +48 -12
  40. package/dist/runner.js +21 -7
  41. package/dist/session/acp.d.ts +15 -0
  42. package/dist/session/acp.js +22 -7
  43. package/dist/session/codex-app-server.js +31 -5
  44. package/dist/spawn.d.ts +1 -0
  45. package/dist/spawn.js +1 -0
  46. package/dist/supervisor/launchd.js +8 -1
  47. package/examples/fleet/brains/hermes.yaml +5 -0
  48. package/package.json +5 -4
@@ -332,7 +332,7 @@ export class AcpSession {
332
332
  // Obsolete ours-mcp lifecycle flags are presence-sensitive. The shared
333
333
  // daemon remains operator-owned; managed ACP children are clients only.
334
334
  const childEnv = {
335
- ...process.env,
335
+ ...(options.inheritEnvironment === false ? {} : process.env),
336
336
  ...options.env,
337
337
  };
338
338
  if (options.scrubObsoleteOursAutostart)
@@ -1067,6 +1067,7 @@ export class AcpSession {
1067
1067
  ? readFileSync(this.sessionFile, 'utf8').trim()
1068
1068
  : '';
1069
1069
  let advertisedConfigOptions;
1070
+ let advertisedModes;
1070
1071
  let advertisedModelId;
1071
1072
  if (persisted && this.agentCapabilities?.sessionCapabilities?.resume != null) {
1072
1073
  const resumed = await this.connection.agent.request(acp.methods.agent.session.resume, {
@@ -1075,6 +1076,7 @@ export class AcpSession {
1075
1076
  mcpServers: this.declaredMcpServers(),
1076
1077
  });
1077
1078
  advertisedConfigOptions = resumed.configOptions;
1079
+ advertisedModes = resumed.modes;
1078
1080
  advertisedModelId = resumed.models?.currentModelId;
1079
1081
  this.captureRuntimeMetadata(advertisedConfigOptions, advertisedModelId);
1080
1082
  this.sessionId = persisted;
@@ -1090,6 +1092,7 @@ export class AcpSession {
1090
1092
  mcpServers: this.declaredMcpServers(),
1091
1093
  });
1092
1094
  advertisedConfigOptions = loaded.configOptions;
1095
+ advertisedModes = loaded.modes;
1093
1096
  advertisedModelId = loaded.models?.currentModelId;
1094
1097
  this.captureRuntimeMetadata(advertisedConfigOptions, advertisedModelId);
1095
1098
  }
@@ -1105,6 +1108,8 @@ export class AcpSession {
1105
1108
  ...(this.options.sessionMeta ? { _meta: this.options.sessionMeta } : {}),
1106
1109
  });
1107
1110
  this.sessionId = created.sessionId;
1111
+ await this.options.validateStartupResponse?.(initialized, created);
1112
+ advertisedModes = created.modes;
1108
1113
  advertisedConfigOptions = created.configOptions;
1109
1114
  advertisedModelId = created.models?.currentModelId;
1110
1115
  this.captureRuntimeMetadata(advertisedConfigOptions, advertisedModelId);
@@ -1130,14 +1135,18 @@ export class AcpSession {
1130
1135
  if (applied?.currentValue !== selection.value)
1131
1136
  throw new Error(`ACP agent did not apply required session config option '${selection.configId}' value '${selection.value}'`);
1132
1137
  }
1133
- // Do not persist a session until every required Brain choice is live. A
1134
- // failed startup closes the ACP session; persisting its id first would make
1135
- // a later resume repeatedly target that invalid session.
1136
- writeFileSync(this.sessionFile, this.sessionId + '\n', { mode: 0o600 });
1138
+ if (this.options.requireMode) {
1139
+ if (!this.options.modeId?.trim())
1140
+ throw new Error('ACP required session mode requires a non-empty modeId');
1141
+ if (!advertisedModes?.availableModes.some(mode => mode.id === this.options.modeId))
1142
+ throw new Error(`ACP agent did not advertise required session mode '${this.options.modeId}'`);
1143
+ }
1144
+ // Preserve existing persistence timing for optional permission modes.
1145
+ if (!this.options.requireMode)
1146
+ writeFileSync(this.sessionFile, this.sessionId + '\n', { mode: 0o600 });
1137
1147
  // Deliver the configured permission mode whichever way the session came up
1138
1148
  // (new, resume or load) — the launch flag never reaches an ACP agent. A
1139
- // refusal is loud but never fatal: the session then simply runs at the
1140
- // agent's own default.
1149
+ // refusal is fatal only when the adapter explicitly requires this mode.
1141
1150
  if (this.options.modeId) {
1142
1151
  try {
1143
1152
  await this.connection.agent.request(acp.methods.agent.session.setMode, {
@@ -1146,10 +1155,16 @@ export class AcpSession {
1146
1155
  });
1147
1156
  }
1148
1157
  catch (e) {
1158
+ if (this.options.requireMode)
1159
+ throw new Error(`ACP agent refused required session mode '${this.options.modeId}': `
1160
+ + (e instanceof Error ? e.message : String(e)));
1149
1161
  this.options.log(`[${this.options.name}] acp: session/set_mode "${this.options.modeId}" failed ` +
1150
1162
  `(${e instanceof Error ? e.message : String(e)}) — session runs at the agent default permission mode`);
1151
1163
  }
1152
1164
  }
1165
+ // A required mode must succeed before its session becomes resumable.
1166
+ if (this.options.requireMode)
1167
+ writeFileSync(this.sessionFile, this.sessionId + '\n', { mode: 0o600 });
1153
1168
  this.readiness = 'idle';
1154
1169
  this.events.emit('state', { status: 'idle', text: `ACP session ${this.sessionId}` });
1155
1170
  this.conversation.appendSafe({
@@ -879,6 +879,21 @@ export class CodexAppServerSession {
879
879
  this.transport.respond(id, { currentTimeAt: Math.floor(Date.now() / 1_000) });
880
880
  return;
881
881
  }
882
+ if (method === 'mcpServer/elicitation/request') {
883
+ const schema = params.requestedSchema;
884
+ // The permission UI can answer a confirmation, but cannot collect form
885
+ // fields or perform URL-based verification on the user's behalf.
886
+ if (params.mode !== 'form' || !isObject(schema) || schema.type !== 'object'
887
+ || !isObject(schema.properties) || Object.keys(schema.properties).length
888
+ || (schema.required !== undefined
889
+ && (!Array.isArray(schema.required) || schema.required.length))) {
890
+ this.options.log(`[${this.options.name}] codex app-server: unsupported MCP elicitation form`);
891
+ this.transport.respond(id, { action: 'decline', content: null, _meta: null });
892
+ return;
893
+ }
894
+ this.requestPermission(method, id, params);
895
+ return;
896
+ }
882
897
  if (method === 'item/tool/requestUserInput') {
883
898
  this.options.log(`[${this.options.name}] codex app-server: requestUserInput has no interactive Fleet mapping; returning no answers`);
884
899
  this.transport.respond(id, { answers: {} });
@@ -896,9 +911,11 @@ export class CodexAppServerSession {
896
911
  { optionId: 'allow_session', name: 'Allow for session', kind: 'allow_always', native: 'acceptForSession' },
897
912
  { optionId: 'deny_once', name: 'Deny', kind: 'reject_once', native: 'decline' },
898
913
  { optionId: 'cancel', name: 'Cancel', kind: 'reject_once', native: 'cancel' },
899
- ].filter(option => method === 'item/fileChange/requestApproval'
900
- || method === 'item/permissions/requestApproval'
901
- || !available.length || available.includes(option.native))
914
+ ].filter(option => method === 'mcpServer/elicitation/request'
915
+ ? option.optionId !== 'allow_session'
916
+ : method === 'item/fileChange/requestApproval'
917
+ || method === 'item/permissions/requestApproval'
918
+ || !available.length || available.includes(option.native))
902
919
  .map(({ native: _native, ...option }) => option);
903
920
  const pending = {
904
921
  requestId, method, toolCallId, options,
@@ -909,7 +926,8 @@ export class CodexAppServerSession {
909
926
  // `approval=allow` covers native operations inside the declared boundary;
910
927
  // it must never silently widen filesystem or network authority.
911
928
  if (this.options.permissions.approval === 'allow'
912
- && method !== 'item/permissions/requestApproval') {
929
+ && method !== 'item/permissions/requestApproval'
930
+ && method !== 'mcpServer/elicitation/request') {
913
931
  this.transport.respond(requestId, this.permissionResponse(pending, 'accept'));
914
932
  this.resolvePermission(permissionId, pending, 'allowed', 'automatic', 'allow_once', 'permissions.approval=allow', 'the native request is inside the configured Codex boundary');
915
933
  return;
@@ -925,7 +943,9 @@ export class CodexAppServerSession {
925
943
  this.readiness = 'awaiting_permission';
926
944
  const timeoutMs = this.options.permissionTimeoutMs ?? PERMISSION_TIMEOUT_MS;
927
945
  const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
928
- const title = string(params.command) ?? string(params.reason)
946
+ const title = (method === 'mcpServer/elicitation/request'
947
+ ? `${string(params.serverName) ?? 'MCP'}: ${string(params.message) ?? 'Confirmation requested'}`
948
+ : undefined) ?? string(params.command) ?? string(params.reason)
929
949
  ?? (method === 'item/fileChange/requestApproval' ? 'Apply file changes'
930
950
  : method === 'item/permissions/requestApproval' ? 'Grant additional permissions'
931
951
  : 'Run command');
@@ -993,6 +1013,12 @@ export class CodexAppServerSession {
993
1013
  return !turnId || !this.activeTurn.nativeTurnId || turnId === this.activeTurn.nativeTurnId;
994
1014
  }
995
1015
  permissionResponse(pending, decision) {
1016
+ if (pending.method === 'mcpServer/elicitation/request')
1017
+ return {
1018
+ action: decision === 'acceptForSession' ? 'accept' : decision,
1019
+ content: decision.startsWith('accept') ? {} : null,
1020
+ _meta: null,
1021
+ };
996
1022
  if (pending.method !== 'item/permissions/requestApproval')
997
1023
  return { decision };
998
1024
  const requested = pending.requestedPermissions ?? {};
package/dist/spawn.d.ts CHANGED
@@ -5,6 +5,7 @@ import { type OpsDeps } from './ops.js';
5
5
  import { type CreationDeps, type CreationProvenance } from './creation.js';
6
6
  import './harness/claude-code.js';
7
7
  import './harness/codex.js';
8
+ import './harness/hermes.js';
8
9
  import { type SupervisorLauncher } from './temp-lifecycle.js';
9
10
  /**
10
11
  * The provenance record written by the most recent spawn in this process, so
package/dist/spawn.js CHANGED
@@ -12,6 +12,7 @@ import { VERSION } from './version.js';
12
12
  import { recordGeneratedAgentSource } from './generated-agent-source.js';
13
13
  import './harness/claude-code.js';
14
14
  import './harness/codex.js';
15
+ import './harness/hermes.js';
15
16
  import { archiveTempState, makeTempSupervisorLauncher, prepareTempSupervisor, reclaimStaleTempState, } from './temp-lifecycle.js';
16
17
  /**
17
18
  * The provenance record written by the most recent spawn in this process, so
@@ -14,8 +14,15 @@ const LAST_EXIT_RE = /^\s*last exit (?:code|status|reason)\s*=\s*(.+?)\s*$/mi;
14
14
  const STATE_RE = /^\s*state\s*=\s*(.+?)\s*$/m;
15
15
  const agentsDir = () => join(home(), 'Library', 'LaunchAgents');
16
16
  const plistPath = (name) => join(agentsDir(), `${labelFor(name)}.plist`);
17
+ const xml = (value) => value
18
+ .replaceAll('&', '&')
19
+ .replaceAll('<', '&lt;')
20
+ .replaceAll('>', '&gt;')
21
+ .replaceAll('"', '&quot;')
22
+ .replaceAll("'", '&apos;');
17
23
  function plist(name, binPath) {
18
24
  const log = join(logsRoot(), `${name}.log`);
25
+ const config = process.env.OURS_CONFIG;
19
26
  return `<?xml version="1.0" encoding="UTF-8"?>
20
27
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
21
28
  <plist version="1.0">
@@ -23,7 +30,7 @@ function plist(name, binPath) {
23
30
  <key>Label</key><string>${labelFor(name)}</string>
24
31
  <key>ProgramArguments</key>
25
32
  <array><string>${binPath}</string><string>_run</string><string>${name}</string></array>
26
- <!-- The runner owns the child-session restart loop. launchd must only
33
+ ${config ? ` <key>EnvironmentVariables</key><dict><key>OURS_CONFIG</key><string>${xml(config)}</string></dict>\n` : ''} <!-- The runner owns the child-session restart loop. launchd must only
27
34
  recover the runner PROCESS crashing: a bare KeepAlive would resume the
28
35
  uncounted relaunch loop and restart a deliberately held-down agent. -->
29
36
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
@@ -0,0 +1,5 @@
1
+ # Illustrative model ID: choose one supported by the provider provisioned in
2
+ # the stopped role's Hermes home. This is not a Fleet default model.
3
+ harness: hermes
4
+ session: acp
5
+ model: gpt-5.6-sol
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.1.4",
3
+ "version": "1.2.0-nightly.1",
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",
@@ -34,14 +34,15 @@
34
34
  "test:e2e": "npm run build && playwright test",
35
35
  "dev:web": "vite",
36
36
  "prepack": "npm run build",
37
- "prepublishOnly": "npm run build && npm test"
37
+ "prepublishOnly": "npm run build && npm test",
38
+ "test:v1-recovery": "node test/fleet-v1-recovery.integration.mjs retry && node test/fleet-v1-recovery.integration.mjs terminal-after-failure && node test/fleet-v1-recovery.integration.mjs terminal-release-failure"
38
39
  },
39
40
  "dependencies": {
40
41
  "@agentclientprotocol/sdk": "^1.3.0",
41
42
  "@fastify/static": "^10.1.2",
42
43
  "@fastify/websocket": "^11.2.0",
43
- "@ours.network/cli": "1.0.1",
44
- "@ours.network/sdk": "3.7.0",
44
+ "@ours.network/cli": "2.8.1-nightly.1",
45
+ "@ours.network/sdk": "3.8.1-nightly.3",
45
46
  "commander": "^12.1.0",
46
47
  "fastify": "^5.4.0",
47
48
  "react": "^19.1.1",