@ours.network/fleet 1.1.5 → 1.2.0-nightly.2

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/dist/runner.js CHANGED
@@ -908,9 +908,11 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
908
908
  void recoveryController.recover(observation.generation).catch(error => deps.log(`[${name}] daemon recovery controller failed: ${error?.name ?? 'Error'}`));
909
909
  }
910
910
  }
911
- if (temp && deps.shouldStop?.()) {
912
- retirementReason = requestedTempStopReason(dir) ?? 'supervisor-signal';
913
- deps.log(`[${name}] temporary supervisor retirement requested (${retirementReason})`);
911
+ if (deps.shouldStop?.()) {
912
+ if (temp) {
913
+ retirementReason = requestedTempStopReason(dir) ?? 'supervisor-signal';
914
+ deps.log(`[${name}] temporary supervisor retirement requested (${retirementReason})`);
915
+ }
914
916
  await sessionHandle.close();
915
917
  sessionClosed = true;
916
918
  break;
@@ -1002,7 +1004,9 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
1002
1004
  rotated = true;
1003
1005
  deps.log(`[${name}] ${why} -> rotated session-id; next start is FRESH`);
1004
1006
  };
1005
- if (exitRecord.detail.includes(ACP_CANCEL_DEADLINE_EXCEEDED)
1007
+ if (!temp && deps.shouldStop?.())
1008
+ deps.log(`[${name}] supervisor stop requested -> next start RESUMES context`);
1009
+ else if (exitRecord.detail.includes(ACP_CANCEL_DEADLINE_EXCEEDED)
1006
1010
  || exitRecord.detail.includes(CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED))
1007
1011
  // This is a deliberate adapter reclamation, not evidence that resume state
1008
1012
  // is poisoned. Preserve the context even when the resumed generation hits
@@ -1049,7 +1053,10 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
1049
1053
  const deps = { ...defaultDeps(), ...partialDeps };
1050
1054
  const dir = agentDir(name);
1051
1055
  mkdirSync(dir, { recursive: true });
1052
- const shouldStop = deps.shouldStop ?? (() => false);
1056
+ let stopping = false;
1057
+ const requestStop = () => { stopping = true; };
1058
+ const shouldStop = () => stopping || (partialDeps.shouldStop?.() ?? false);
1059
+ deps.shouldStop = shouldStop;
1053
1060
  const stamp = () => new Date(deps.now()).toISOString();
1054
1061
  // Record how the PREVIOUS supervisor process ended before doing anything
1055
1062
  // else. An external kill writes nothing itself, and the next ledger write is
@@ -1071,6 +1078,9 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
1071
1078
  deps.log(`[${name}] previous supervisor run (started ${termination.runStartedAt}) `
1072
1079
  + `ended abruptly: ${termination.detail}; abrupt terminations recorded: ${abrupt}`);
1073
1080
  }
1081
+ // Service-manager stops must run the same cleanup as a completed session.
1082
+ process.on('SIGTERM', requestStop);
1083
+ process.on('SIGINT', requestStop);
1074
1084
  try {
1075
1085
  while (!shouldStop()) {
1076
1086
  let ledger = readRestartLedger(dir);
@@ -1125,6 +1135,8 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
1125
1135
  mode: 'fresh',
1126
1136
  };
1127
1137
  }
1138
+ if (stopping)
1139
+ break;
1128
1140
  // Re-read: the attempt itself may have taken minutes, and an operator may
1129
1141
  // have reset the ledger meanwhile.
1130
1142
  ledger = readRestartLedger(dir);
@@ -1189,8 +1201,10 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
1189
1201
  return readRestartLedger(dir);
1190
1202
  }
1191
1203
  finally {
1192
- // Only an orderly return through this loop clears the marker; a signal or
1193
- // an OOM-kill leaves it, which is exactly how the successor detects them.
1204
+ process.off('SIGTERM', requestStop);
1205
+ process.off('SIGINT', requestStop);
1206
+ // An orderly shutdown clears the marker; an unhandled signal or OOM-kill
1207
+ // leaves it so the successor can identify an abrupt termination.
1194
1208
  releaseSupervisorRun(dir);
1195
1209
  }
1196
1210
  }
@@ -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 ?? {};
@@ -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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.1.5",
3
+ "version": "1.2.0-nightly.2",
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.2",
45
+ "@ours.network/sdk": "3.8.1-nightly.4",
45
46
  "commander": "^12.1.0",
46
47
  "fastify": "^5.4.0",
47
48
  "react": "^19.1.1",