@openclaw-cloud/agent-controller 3.0.0-beta.2 → 3.0.0-beta.21

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 (169) hide show
  1. package/bin/agent-controller.js +5 -9
  2. package/dist/api.d.ts +16 -0
  3. package/dist/api.js.map +1 -1
  4. package/dist/commands/agent-update-cli.js +23 -38
  5. package/dist/commands/agent-update-cli.js.map +1 -1
  6. package/dist/commands/backup-cli.js +6 -4
  7. package/dist/commands/backup-cli.js.map +1 -1
  8. package/dist/commands/bootstrap.d.ts +10 -1
  9. package/dist/commands/bootstrap.js +33 -73
  10. package/dist/commands/bootstrap.js.map +1 -1
  11. package/dist/commands/channel-server.js +4 -3
  12. package/dist/commands/channel-server.js.map +1 -1
  13. package/dist/commands/heartbeat-cli.js +2 -6
  14. package/dist/commands/heartbeat-cli.js.map +1 -1
  15. package/dist/commands/install-deps.d.ts +10 -0
  16. package/dist/commands/install-deps.js +33 -43
  17. package/dist/commands/install-deps.js.map +1 -1
  18. package/dist/commands/knowledge-sync-cli.js +6 -4
  19. package/dist/commands/knowledge-sync-cli.js.map +1 -1
  20. package/dist/commands/plugin-update-cli.js +4 -8
  21. package/dist/commands/plugin-update-cli.js.map +1 -1
  22. package/dist/commands/self-update.d.ts +1 -0
  23. package/dist/commands/self-update.js +76 -37
  24. package/dist/commands/self-update.js.map +1 -1
  25. package/dist/commands/update-skills.js +7 -6
  26. package/dist/commands/update-skills.js.map +1 -1
  27. package/dist/config-file.d.ts +24 -0
  28. package/dist/config-file.js +59 -20
  29. package/dist/config-file.js.map +1 -1
  30. package/dist/config.d.ts +10 -0
  31. package/dist/config.js +28 -0
  32. package/dist/config.js.map +1 -1
  33. package/dist/connection.d.ts +4 -0
  34. package/dist/connection.js +50 -5
  35. package/dist/connection.js.map +1 -1
  36. package/dist/handlers/agent-update.d.ts +4 -0
  37. package/dist/handlers/agent-update.js +16 -44
  38. package/dist/handlers/agent-update.js.map +1 -1
  39. package/dist/handlers/deploy.d.ts +1 -2
  40. package/dist/handlers/deploy.js +4 -20
  41. package/dist/handlers/deploy.js.map +1 -1
  42. package/dist/handlers/diagnostics.js +0 -12
  43. package/dist/handlers/diagnostics.js.map +1 -1
  44. package/dist/handlers/file-delete.js +4 -5
  45. package/dist/handlers/file-delete.js.map +1 -1
  46. package/dist/handlers/file-write.js +4 -4
  47. package/dist/handlers/file-write.js.map +1 -1
  48. package/dist/handlers/knowledge-sync.js +2 -14
  49. package/dist/handlers/knowledge-sync.js.map +1 -1
  50. package/dist/handlers/memory.d.ts +11 -1
  51. package/dist/handlers/memory.js.map +1 -1
  52. package/dist/handlers/onboarding.d.ts +1 -2
  53. package/dist/handlers/onboarding.js +3 -20
  54. package/dist/handlers/onboarding.js.map +1 -1
  55. package/dist/handlers/package-install.js +1 -1
  56. package/dist/handlers/package-install.js.map +1 -1
  57. package/dist/handlers/pair.js +0 -12
  58. package/dist/handlers/pair.js.map +1 -1
  59. package/dist/handlers/restart.d.ts +1 -2
  60. package/dist/handlers/restart.js +3 -8
  61. package/dist/handlers/restart.js.map +1 -1
  62. package/dist/handlers/slack-event.d.ts +13 -0
  63. package/dist/handlers/slack-event.js +40 -0
  64. package/dist/handlers/slack-event.js.map +1 -0
  65. package/dist/handlers/stop.js +0 -7
  66. package/dist/handlers/stop.js.map +1 -1
  67. package/dist/handlers/telegram-webhook.js +3 -22
  68. package/dist/handlers/telegram-webhook.js.map +1 -1
  69. package/dist/handlers/update-config.js +4 -13
  70. package/dist/handlers/update-config.js.map +1 -1
  71. package/dist/heartbeat.d.ts +7 -1
  72. package/dist/heartbeat.js +35 -6
  73. package/dist/heartbeat.js.map +1 -1
  74. package/dist/index.d.ts +0 -8
  75. package/dist/index.js +96 -119
  76. package/dist/index.js.map +1 -1
  77. package/dist/lockfile.js +2 -1
  78. package/dist/lockfile.js.map +1 -1
  79. package/dist/mock-board-worker.d.ts +31 -0
  80. package/dist/mock-board-worker.js +125 -0
  81. package/dist/mock-board-worker.js.map +1 -0
  82. package/dist/platform/linux.js +6 -5
  83. package/dist/platform/linux.js.map +1 -1
  84. package/dist/platform/macos.js +3 -2
  85. package/dist/platform/macos.js.map +1 -1
  86. package/dist/platform/windows.js +3 -2
  87. package/dist/platform/windows.js.map +1 -1
  88. package/dist/providers/claude-code/channel-server.js +2 -1
  89. package/dist/providers/claude-code/channel-server.js.map +1 -1
  90. package/dist/providers/claude-code/settings-writer.d.ts +3 -2
  91. package/dist/providers/claude-code/settings-writer.js +6 -15
  92. package/dist/providers/claude-code/settings-writer.js.map +1 -1
  93. package/dist/providers/index.d.ts +13 -0
  94. package/dist/providers/index.js +25 -0
  95. package/dist/providers/index.js.map +1 -1
  96. package/dist/providers/openclaw/gateway-adapter.js +2 -0
  97. package/dist/providers/openclaw/gateway-adapter.js.map +1 -1
  98. package/dist/providers/openclaw/gateway-client.js +6 -1
  99. package/dist/providers/openclaw/gateway-client.js.map +1 -1
  100. package/dist/providers/openclaw/index.js +34 -13
  101. package/dist/providers/openclaw/index.js.map +1 -1
  102. package/dist/slack-relay-server.d.ts +48 -0
  103. package/dist/slack-relay-server.js +197 -0
  104. package/dist/slack-relay-server.js.map +1 -0
  105. package/dist/types.d.ts +16 -1
  106. package/dist/utils/anthropic-auth.d.ts +14 -0
  107. package/dist/utils/anthropic-auth.js +24 -0
  108. package/dist/utils/anthropic-auth.js.map +1 -1
  109. package/dist/utils/apply-config.d.ts +65 -12
  110. package/dist/utils/apply-config.js +308 -100
  111. package/dist/utils/apply-config.js.map +1 -1
  112. package/dist/utils/claude-env.d.ts +6 -0
  113. package/dist/utils/claude-env.js +29 -28
  114. package/dist/utils/claude-env.js.map +1 -1
  115. package/dist/utils/config-merge-paths.d.ts +15 -1
  116. package/dist/utils/config-merge-paths.js +17 -3
  117. package/dist/utils/config-merge-paths.js.map +1 -1
  118. package/dist/utils/deploy-type.d.ts +16 -0
  119. package/dist/utils/deploy-type.js +34 -0
  120. package/dist/utils/deploy-type.js.map +1 -0
  121. package/dist/utils/env.d.ts +0 -1
  122. package/dist/utils/env.js +0 -8
  123. package/dist/utils/env.js.map +1 -1
  124. package/dist/utils/exec.d.ts +11 -0
  125. package/dist/utils/exec.js +47 -1
  126. package/dist/utils/exec.js.map +1 -1
  127. package/dist/utils/gateway-restart-handler.d.ts +6 -2
  128. package/dist/utils/gateway-restart-handler.js +11 -2
  129. package/dist/utils/gateway-restart-handler.js.map +1 -1
  130. package/dist/utils/gateway-restart.d.ts +7 -0
  131. package/dist/utils/gateway-restart.js +12 -4
  132. package/dist/utils/gateway-restart.js.map +1 -1
  133. package/dist/utils/knowledge-graph.d.ts +13 -1
  134. package/dist/utils/knowledge-graph.js +75 -29
  135. package/dist/utils/knowledge-graph.js.map +1 -1
  136. package/dist/utils/legacy-mcp-servers.d.ts +12 -0
  137. package/dist/utils/legacy-mcp-servers.js +37 -0
  138. package/dist/utils/legacy-mcp-servers.js.map +1 -0
  139. package/dist/utils/log-collector.js +2 -1
  140. package/dist/utils/log-collector.js.map +1 -1
  141. package/dist/utils/openclaw-ops.d.ts +45 -3
  142. package/dist/utils/openclaw-ops.js +94 -15
  143. package/dist/utils/openclaw-ops.js.map +1 -1
  144. package/dist/utils/openclaw-stack-update.d.ts +31 -0
  145. package/dist/utils/openclaw-stack-update.js +50 -0
  146. package/dist/utils/openclaw-stack-update.js.map +1 -0
  147. package/dist/utils/package-install.js +4 -12
  148. package/dist/utils/package-install.js.map +1 -1
  149. package/dist/utils/package-version.d.ts +10 -0
  150. package/dist/utils/package-version.js +18 -0
  151. package/dist/utils/package-version.js.map +1 -0
  152. package/dist/utils/parse-id-at-version.d.ts +15 -0
  153. package/dist/utils/parse-id-at-version.js +22 -0
  154. package/dist/utils/parse-id-at-version.js.map +1 -0
  155. package/dist/utils/plugin-reconcile.d.ts +46 -0
  156. package/dist/utils/plugin-reconcile.js +201 -0
  157. package/dist/utils/plugin-reconcile.js.map +1 -0
  158. package/dist/utils/response.js +1 -0
  159. package/dist/utils/response.js.map +1 -1
  160. package/dist/utils/write-home-files.d.ts +27 -0
  161. package/dist/utils/write-home-files.js +72 -0
  162. package/dist/utils/write-home-files.js.map +1 -0
  163. package/dist/utils/write-workspace-files.d.ts +1 -1
  164. package/dist/utils/write-workspace-files.js +19 -7
  165. package/dist/utils/write-workspace-files.js.map +1 -1
  166. package/dist/workspace.d.ts +11 -0
  167. package/dist/workspace.js +20 -0
  168. package/dist/workspace.js.map +1 -1
  169. package/package.json +9 -4
@@ -1,5 +1,11 @@
1
1
  /**
2
2
  * Write ~/.claude/settings.json, ~/.claude/cc-notify.sh, and auth-profiles.json
3
3
  * from a flat env vars dict (ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL).
4
+ *
5
+ * This is a WHOLESALE rewrite of the user's Claude settings, so it is gated on
6
+ * the install being managed ('vm'/'k8s'). The guard lives here rather than in
7
+ * the callers deliberately: it is defense in depth that holds for every current
8
+ * and future caller. `agent-controller bootstrap` on a developer laptop once
9
+ * blew away ~/.claude/settings.json — that must not be re-reachable.
4
10
  */
5
11
  export declare function writeClaudeEnv(envVars: Record<string, string>): Promise<void>;
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
- import { classifyAnthropicAuth } from './anthropic-auth.js';
4
+ import { buildClaudeSettings, classifyAnthropicAuth, CLAUDE_PERMISSIONS, } from './anthropic-auth.js';
5
+ import { getResolvedDeployType, isManagedDeploy } from './deploy-type.js';
5
6
  const CC_NOTIFY_SH = `#!/bin/bash
6
7
  # Claude Code hook: notify OpenClaw on Stop/PermissionRequest
7
8
  EVENT_TYPE="\${CC_HOOK_EVENT:-unknown}"
@@ -41,8 +42,18 @@ exit 0
41
42
  /**
42
43
  * Write ~/.claude/settings.json, ~/.claude/cc-notify.sh, and auth-profiles.json
43
44
  * from a flat env vars dict (ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL).
45
+ *
46
+ * This is a WHOLESALE rewrite of the user's Claude settings, so it is gated on
47
+ * the install being managed ('vm'/'k8s'). The guard lives here rather than in
48
+ * the callers deliberately: it is defense in depth that holds for every current
49
+ * and future caller. `agent-controller bootstrap` on a developer laptop once
50
+ * blew away ~/.claude/settings.json — that must not be re-reachable.
44
51
  */
45
52
  export async function writeClaudeEnv(envVars) {
53
+ if (!isManagedDeploy()) {
54
+ console.log(`[claude-env] skipped: not a managed install (deployType=${getResolvedDeployType() ?? 'unknown'})`);
55
+ return;
56
+ }
46
57
  const claudeDir = path.join(os.homedir(), '.claude');
47
58
  await fs.mkdir(claudeDir, { recursive: true });
48
59
  // Write cc-notify.sh
@@ -50,10 +61,6 @@ export async function writeClaudeEnv(envVars) {
50
61
  await fs.writeFile(notifyPath, CC_NOTIFY_SH);
51
62
  await fs.chmod(notifyPath, 0o755);
52
63
  const auth = classifyAnthropicAuth({ anthropicApiKey: envVars.ANTHROPIC_API_KEY });
53
- // This flow only handles the OAuth-vs-API-key split (no explicit
54
- // anthropicAuthToken input); an `auth_token` classification here always
55
- // means "the API key was in fact an OAuth token".
56
- const isOAuthToken = auth.kind === 'auth_token';
57
64
  const hooks = {
58
65
  PreToolUse: [{ matcher: 'Bash', hooks: [] }],
59
66
  PostToolUse: [{ matcher: 'Write|Edit', hooks: [] }],
@@ -69,29 +76,23 @@ export async function writeClaudeEnv(envVars) {
69
76
  },
70
77
  ],
71
78
  };
72
- const permissions = {
73
- allow: ['Bash(*)', 'Read(*)', 'Write(*)', 'Edit(*)', 'mcp__*'],
74
- };
75
- let settings;
76
- if (isOAuthToken) {
77
- // OAuth token flow: set ANTHROPIC_AUTH_TOKEN, no apiKeyHelper, no ANTHROPIC_BASE_URL
78
- settings = {
79
- permissions,
80
- env: { ANTHROPIC_AUTH_TOKEN: auth.value },
81
- hooks,
82
- };
83
- }
84
- else {
85
- // API key flow (default): apiKeyHelper + optional ANTHROPIC_BASE_URL
86
- settings = {
87
- permissions,
88
- apiKeyHelper: "bash -c 'source /opt/openclaw-cloud/agent-controller/.env && echo $ANTHROPIC_API_KEY'",
89
- ...(envVars.ANTHROPIC_BASE_URL
90
- ? { env: { ANTHROPIC_BASE_URL: envVars.ANTHROPIC_BASE_URL } }
91
- : {}),
92
- hooks,
93
- };
94
- }
79
+ // This flow only handles the OAuth-vs-API-key split (no explicit
80
+ // anthropicAuthToken input); an `auth_token` classification here always
81
+ // means "the API key was in fact an OAuth token".
82
+ const settings = auth.kind === 'auth_token'
83
+ ? // OAuth token flow: ANTHROPIC_AUTH_TOKEN, no apiKeyHelper, no ANTHROPIC_BASE_URL
84
+ { ...buildClaudeSettings(auth), hooks }
85
+ : // API key flow (default): the key is NOT embedded — apiKeyHelper reads it
86
+ // out of the VM .env at runtime — so this branch cannot use the shared
87
+ // `env` variant. Optional ANTHROPIC_BASE_URL rides along.
88
+ {
89
+ permissions: CLAUDE_PERMISSIONS,
90
+ apiKeyHelper: "bash -c 'source /opt/openclaw-cloud/agent-controller/.env && echo $ANTHROPIC_API_KEY'",
91
+ ...(envVars.ANTHROPIC_BASE_URL
92
+ ? { env: { ANTHROPIC_BASE_URL: envVars.ANTHROPIC_BASE_URL } }
93
+ : {}),
94
+ hooks,
95
+ };
95
96
  // Contains ANTHROPIC_AUTH_TOKEN on OAuth flow — chmod 0o600.
96
97
  await fs.writeFile(path.join(claudeDir, 'settings.json'), JSON.stringify(settings, null, 2) + '\n', { mode: 0o600 });
97
98
  // auth-profiles.json is now written by applyBootstrapConfig from openclaw.json
@@ -1 +1 @@
1
- {"version":3,"file":"claude-env.js","sourceRoot":"","sources":["../../src/utils/claude-env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCpB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAA+B;IAClE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE/C,qBAAqB;IACrB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;IACxD,MAAM,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAC7C,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAElC,MAAM,IAAI,GAAG,qBAAqB,CAAC,EAAE,eAAe,EAAE,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;IACnF,iEAAiE;IACjE,wEAAwE;IACxE,kDAAkD;IAClD,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,KAAK,YAAY,CAAC;IAEhD,MAAM,KAAK,GAAG;QACZ,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QAC5C,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QACnD,IAAI,EAAE;YACJ;gBACE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,2CAA2C,EAAE,CAAC;aACnF;SACF;QACD,YAAY,EAAE;YACZ;gBACE,OAAO,EAAE,mBAAmB;gBAC5B,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,mDAAmD,EAAE,CAAC;aAC3F;SACF;KACF,CAAC;IAEF,MAAM,WAAW,GAAG;QAClB,KAAK,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,CAAC;KAC/D,CAAC;IAEF,IAAI,QAAiC,CAAC;IACtC,IAAI,YAAY,EAAE,CAAC;QACjB,qFAAqF;QACrF,QAAQ,GAAG;YACT,WAAW;YACX,GAAG,EAAE,EAAE,oBAAoB,EAAE,IAAI,CAAC,KAAK,EAAE;YACzC,KAAK;SACN,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,qEAAqE;QACrE,QAAQ,GAAG;YACT,WAAW;YACX,YAAY,EACV,uFAAuF;YACzF,GAAG,CAAC,OAAO,CAAC,kBAAkB;gBAC5B,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,kBAAkB,EAAE,OAAO,CAAC,kBAAkB,EAAE,EAAE;gBAC7D,CAAC,CAAC,EAAE,CAAC;YACP,KAAK;SACN,CAAC;IACJ,CAAC;IAED,6DAA6D;IAC7D,MAAM,EAAE,CAAC,SAAS,CAChB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,EACrC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EACxC,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAC;IAEF,+EAA+E;AACjF,CAAC"}
1
+ {"version":3,"file":"claude-env.js","sourceRoot":"","sources":["../../src/utils/claude-env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAE1E,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCpB,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAA+B;IAClE,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CACT,2DAA2D,qBAAqB,EAAE,IAAI,SAAS,GAAG,CACnG,CAAC;QACF,OAAO;IACT,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE/C,qBAAqB;IACrB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;IACxD,MAAM,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAC7C,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAElC,MAAM,IAAI,GAAG,qBAAqB,CAAC,EAAE,eAAe,EAAE,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;IAEnF,MAAM,KAAK,GAAG;QACZ,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QAC5C,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QACnD,IAAI,EAAE;YACJ;gBACE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,2CAA2C,EAAE,CAAC;aACnF;SACF;QACD,YAAY,EAAE;YACZ;gBACE,OAAO,EAAE,mBAAmB;gBAC5B,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,mDAAmD,EAAE,CAAC;aAC3F;SACF;KACF,CAAC;IAEF,iEAAiE;IACjE,wEAAwE;IACxE,kDAAkD;IAClD,MAAM,QAAQ,GACZ,IAAI,CAAC,IAAI,KAAK,YAAY;QACxB,CAAC,CAAC,iFAAiF;YACjF,EAAE,GAAG,mBAAmB,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE;QACzC,CAAC,CAAC,0EAA0E;YAC1E,uEAAuE;YACvE,0DAA0D;YAC1D;gBACE,WAAW,EAAE,kBAAkB;gBAC/B,YAAY,EACV,uFAAuF;gBACzF,GAAG,CAAC,OAAO,CAAC,kBAAkB;oBAC5B,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,kBAAkB,EAAE,OAAO,CAAC,kBAAkB,EAAE,EAAE;oBAC7D,CAAC,CAAC,EAAE,CAAC;gBACP,KAAK;aACN,CAAC;IAER,6DAA6D;IAC7D,MAAM,EAAE,CAAC,SAAS,CAChB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,EACrC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EACxC,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAC;IAEF,+EAA+E;AACjF,CAAC"}
@@ -19,12 +19,26 @@
19
19
  * To extend: add the path here. Keep it tight — every entry is an implicit
20
20
  * promise that merging it won't clobber user work.
21
21
  */
22
+ /**
23
+ * The one backend-managed section of openclaw.json. Merged REMOTE-wins per
24
+ * server name (see `applyMergeableConfig`) and the only section whose change
25
+ * forces a gateway restart on standalone — openclaw does not hot-reload it.
26
+ */
27
+ export declare const MCP_SERVERS_PATH = "mcp.servers";
22
28
  export declare const MERGEABLE_CONFIG_PATHS: readonly string[];
23
29
  /**
24
30
  * Paths within ~/.claude/settings.json that are safe to merge from a backend
25
- * envVars/settings payload. Everything else (apiKeyHelper, permissions, hooks,
31
+ * bootstrap payload. Everything else (apiKeyHelper, permissions, hooks, `env`,
26
32
  * user-owned keys) is left untouched.
27
33
  *
34
+ * `env` is DELIBERATELY EXCLUDED and must never be added back. The backend's
35
+ * `envVars` carry `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL`; merging them into
36
+ * a standalone user's ~/.claude/settings.json points ALL of that user's own
37
+ * Claude Code sessions at the platform proxy — which, when it is not running,
38
+ * breaks Claude Code on the machine outright. That incident happened twice
39
+ * (latest 2026-08-02) before the path was removed. Managed installs ('vm'/'k8s')
40
+ * still get their credentials, via `writeClaudeEnv()` — not via this merge.
41
+ *
28
42
  * NOTE: `mcp.servers` is a DOTTED path — Claude Code's modern settings.json
29
43
  * schema nests MCP servers under `mcp.servers`, not the legacy flat
30
44
  * `mcpServers` key. Agent-controller migrates any pre-existing flat
@@ -19,22 +19,36 @@
19
19
  * To extend: add the path here. Keep it tight — every entry is an implicit
20
20
  * promise that merging it won't clobber user work.
21
21
  */
22
+ /**
23
+ * The one backend-managed section of openclaw.json. Merged REMOTE-wins per
24
+ * server name (see `applyMergeableConfig`) and the only section whose change
25
+ * forces a gateway restart on standalone — openclaw does not hot-reload it.
26
+ */
27
+ export const MCP_SERVERS_PATH = 'mcp.servers';
22
28
  export const MERGEABLE_CONFIG_PATHS = [
23
- 'mcp.servers',
29
+ MCP_SERVERS_PATH,
24
30
  'tools.media.audio',
25
31
  'tools.media.voice',
26
32
  'tools.transcription',
27
33
  ];
28
34
  /**
29
35
  * Paths within ~/.claude/settings.json that are safe to merge from a backend
30
- * envVars/settings payload. Everything else (apiKeyHelper, permissions, hooks,
36
+ * bootstrap payload. Everything else (apiKeyHelper, permissions, hooks, `env`,
31
37
  * user-owned keys) is left untouched.
32
38
  *
39
+ * `env` is DELIBERATELY EXCLUDED and must never be added back. The backend's
40
+ * `envVars` carry `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL`; merging them into
41
+ * a standalone user's ~/.claude/settings.json points ALL of that user's own
42
+ * Claude Code sessions at the platform proxy — which, when it is not running,
43
+ * breaks Claude Code on the machine outright. That incident happened twice
44
+ * (latest 2026-08-02) before the path was removed. Managed installs ('vm'/'k8s')
45
+ * still get their credentials, via `writeClaudeEnv()` — not via this merge.
46
+ *
33
47
  * NOTE: `mcp.servers` is a DOTTED path — Claude Code's modern settings.json
34
48
  * schema nests MCP servers under `mcp.servers`, not the legacy flat
35
49
  * `mcpServers` key. Agent-controller migrates any pre-existing flat
36
50
  * `mcpServers` into `mcp.servers` on first merge and deletes the flat key
37
51
  * (see `applyMergeableConfig` in apply-config.ts).
38
52
  */
39
- export const MERGEABLE_SETTINGS_PATHS = ['mcp.servers', 'env'];
53
+ export const MERGEABLE_SETTINGS_PATHS = ['mcp.servers'];
40
54
  //# sourceMappingURL=config-merge-paths.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config-merge-paths.js","sourceRoot":"","sources":["../../src/utils/config-merge-paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAsB;IACvD,aAAa;IACb,mBAAmB;IACnB,mBAAmB;IACnB,qBAAqB;CACb,CAAC;AAEX;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAsB,CAAC,aAAa,EAAE,KAAK,CAAU,CAAC"}
1
+ {"version":3,"file":"config-merge-paths.js","sourceRoot":"","sources":["../../src/utils/config-merge-paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,aAAa,CAAC;AAE9C,MAAM,CAAC,MAAM,sBAAsB,GAAsB;IACvD,gBAAgB;IAChB,mBAAmB;IACnB,mBAAmB;IACnB,qBAAqB;CACb,CAAC;AAEX;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAsB,CAAC,aAAa,CAAU,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Record the deployment type reported by a bootstrap payload. Ignores empty
3
+ * values so an older backend that omits the field cannot clear a known type.
4
+ */
5
+ export declare function setResolvedDeployType(t?: string): void;
6
+ /**
7
+ * The payload-reported deployment type if one has arrived, otherwise the
8
+ * `AGENT_DEPLOY_TYPE` env var. `undefined` means "we do not know".
9
+ */
10
+ export declare function getResolvedDeployType(): string | undefined;
11
+ /**
12
+ * True only for managed installs ('vm' / 'k8s'). Unknown deployment types
13
+ * deliberately resolve to false — fail safe, because the expensive mistake is
14
+ * rewriting a developer's ~/.claude/settings.json, not skipping a full apply.
15
+ */
16
+ export declare function isManagedDeploy(): boolean;
@@ -0,0 +1,34 @@
1
+ import { config } from '../config.js';
2
+ /**
3
+ * Deployment types describing a machine our platform provisioned end to end.
4
+ * Everything else — 'standalone', 'external', or an unknown/absent type — is a
5
+ * third-party machine whose config files we are only allowed to merge into.
6
+ */
7
+ const MANAGED_TYPES = new Set(['vm', 'k8s']);
8
+ /** Set from the bootstrap payload; survives for the life of the process. */
9
+ let resolvedDeployType;
10
+ /**
11
+ * Record the deployment type reported by a bootstrap payload. Ignores empty
12
+ * values so an older backend that omits the field cannot clear a known type.
13
+ */
14
+ export function setResolvedDeployType(t) {
15
+ if (t)
16
+ resolvedDeployType = t;
17
+ }
18
+ /**
19
+ * The payload-reported deployment type if one has arrived, otherwise the
20
+ * `AGENT_DEPLOY_TYPE` env var. `undefined` means "we do not know".
21
+ */
22
+ export function getResolvedDeployType() {
23
+ return resolvedDeployType ?? config.agentDeployType;
24
+ }
25
+ /**
26
+ * True only for managed installs ('vm' / 'k8s'). Unknown deployment types
27
+ * deliberately resolve to false — fail safe, because the expensive mistake is
28
+ * rewriting a developer's ~/.claude/settings.json, not skipping a full apply.
29
+ */
30
+ export function isManagedDeploy() {
31
+ const type = getResolvedDeployType();
32
+ return type !== undefined && MANAGED_TYPES.has(type);
33
+ }
34
+ //# sourceMappingURL=deploy-type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy-type.js","sourceRoot":"","sources":["../../src/utils/deploy-type.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC;;;;GAIG;AACH,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;AAE7C,4EAA4E;AAC5E,IAAI,kBAAsC,CAAC;AAE3C;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,CAAU;IAC9C,IAAI,CAAC;QAAE,kBAAkB,GAAG,CAAC,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,kBAAkB,IAAI,MAAM,CAAC,eAAe,CAAC;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,IAAI,GAAG,qBAAqB,EAAE,CAAC;IACrC,OAAO,IAAI,KAAK,SAAS,IAAI,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACvD,CAAC"}
@@ -1,2 +1 @@
1
- export declare function requireEnv(name: string): string;
2
1
  export declare function getBackendUrl(): string;
package/dist/utils/env.js CHANGED
@@ -1,12 +1,4 @@
1
1
  import { config, requireConfig } from '../config.js';
2
- export function requireEnv(name) {
3
- const value = process.env[name];
4
- if (!value) {
5
- console.error(`Missing required environment variable: ${name}`);
6
- process.exit(1);
7
- }
8
- return value;
9
- }
10
2
  export function getBackendUrl() {
11
3
  return config.backendUrl ?? requireConfig('backendUrl');
12
4
  }
@@ -1 +1 @@
1
- {"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/utils/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,0CAA0C,IAAI,EAAE,CAAC,CAAC;QAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,MAAM,CAAC,UAAU,IAAI,aAAa,CAAC,YAAY,CAAC,CAAC;AAC1D,CAAC"}
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/utils/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,UAAU,aAAa;IAC3B,OAAO,MAAM,CAAC,UAAU,IAAI,aAAa,CAAC,YAAY,CAAC,CAAC;AAC1D,CAAC"}
@@ -1 +1,12 @@
1
+ /** PATH handed to child processes: the inherited one plus {@link extraPathDirs}. */
2
+ export declare function execPath(): string;
3
+ /**
4
+ * Run a command through a shell and resolve its trimmed stdout.
5
+ *
6
+ * `timeout` defaults to 0 (no timeout) to preserve the behaviour of existing
7
+ * no-timeout callers (fast systemctl/launchctl/which calls); long-running
8
+ * callers already pass an explicit timeout. The raised maxBuffer prevents
9
+ * verbose output (e.g. `npm install -g`) from rejecting with
10
+ * ERR_CHILD_PROCESS_STDOUT_MAXBUFFER.
11
+ */
1
12
  export declare function execAsync(cmd: string, timeout?: number): Promise<string>;
@@ -1,7 +1,53 @@
1
1
  import { exec } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { config } from '../config.js';
4
+ // 16 MB — avoid ERR_CHILD_PROCESS_STDOUT_MAXBUFFER on chatty commands like `npm install -g`.
5
+ const DEFAULT_MAX_BUFFER = 16 * 1024 * 1024;
6
+ /**
7
+ * Directories APPENDED to PATH for every child process we spawn.
8
+ *
9
+ * Why: the binaries we shell out to (`openclaw`, `npm`) live in a global npm
10
+ * prefix that is not on the ambient PATH in every context we run from, and a
11
+ * missing PATH entry has cost us the fleet twice:
12
+ * - VM first boot: the user-data step `su - ubuntu -c "openclaw plugins
13
+ * install @openclaw-cloud/mcp-warmup@latest"` died with `openclaw: command
14
+ * not found` on a whole cohort (~/.profile exports the npm-global bin only
15
+ * later in the bootstrap), and the `|| echo` swallowed it.
16
+ * - macOS launchd: the service PATH lacks /opt/homebrew/bin, so the
17
+ * best-effort `openclaw gateway restart` after a config merge always failed
18
+ * with `/bin/sh: openclaw: command not found`.
19
+ *
20
+ * APPENDED, never prepended — an operator's own PATH entry still wins, so this
21
+ * cannot hijack a deliberately pinned binary. Non-existent dirs are harmless.
22
+ */
23
+ function extraPathDirs() {
24
+ return [
25
+ path.join(config.home, '.npm-global', 'bin'), // npm prefix used by the VM provisioner
26
+ '/usr/local/bin',
27
+ '/opt/homebrew/bin', // Homebrew on Apple silicon
28
+ ];
29
+ }
30
+ /** PATH handed to child processes: the inherited one plus {@link extraPathDirs}. */
31
+ export function execPath() {
32
+ const parts = (process.env.PATH ?? '').split(path.delimiter).filter(Boolean);
33
+ for (const dir of extraPathDirs()) {
34
+ if (!parts.includes(dir))
35
+ parts.push(dir);
36
+ }
37
+ return parts.join(path.delimiter);
38
+ }
39
+ /**
40
+ * Run a command through a shell and resolve its trimmed stdout.
41
+ *
42
+ * `timeout` defaults to 0 (no timeout) to preserve the behaviour of existing
43
+ * no-timeout callers (fast systemctl/launchctl/which calls); long-running
44
+ * callers already pass an explicit timeout. The raised maxBuffer prevents
45
+ * verbose output (e.g. `npm install -g`) from rejecting with
46
+ * ERR_CHILD_PROCESS_STDOUT_MAXBUFFER.
47
+ */
2
48
  export function execAsync(cmd, timeout = 0) {
3
49
  return new Promise((resolve, reject) => {
4
- exec(cmd, { timeout }, (err, stdout) => {
50
+ exec(cmd, { timeout, maxBuffer: DEFAULT_MAX_BUFFER, env: { ...process.env, PATH: execPath() } }, (err, stdout) => {
5
51
  if (err)
6
52
  reject(err);
7
53
  else
@@ -1 +1 @@
1
- {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/utils/exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAE1C,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,OAAO,GAAG,CAAC;IAChD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,GAAG;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;gBAChB,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/utils/exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,6FAA6F;AAC7F,MAAM,kBAAkB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,aAAa;IACpB,OAAO;QACL,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,wCAAwC;QACtF,gBAAgB;QAChB,mBAAmB,EAAE,4BAA4B;KAClD,CAAC;AACJ,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,QAAQ;IACtB,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7E,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AACpC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,OAAO,GAAG,CAAC;IAChD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,CACF,GAAG,EACH,EAAE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,EACrF,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YACd,IAAI,GAAG;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;gBAChB,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;QACzC,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -1,6 +1,10 @@
1
1
  import type { AgentCommand, AgentResponse } from '../types.js';
2
2
  /**
3
- * Shared handler logic for commands that restart the OpenClaw gateway.
4
- * Used by handleRestart and handleOnboardingComplete.
3
+ * The handler behind every command whose whole job is "restart the OpenClaw
4
+ * gateway": `restart`, `deploy` and `onboarding_complete`. They were three
5
+ * byte-identical copies of this wrapper.
6
+ *
7
+ * Emits only the domain event (`gateway_restart`) — the command lifecycle
8
+ * events are owned by the dispatch loop in connection.ts.
5
9
  */
6
10
  export declare function handleGatewayRestartCommand(command: AgentCommand): Promise<AgentResponse>;
@@ -1,9 +1,18 @@
1
1
  import { restartGateway } from './gateway-restart.js';
2
+ import { logCollector } from '../connection.js';
2
3
  /**
3
- * Shared handler logic for commands that restart the OpenClaw gateway.
4
- * Used by handleRestart and handleOnboardingComplete.
4
+ * The handler behind every command whose whole job is "restart the OpenClaw
5
+ * gateway": `restart`, `deploy` and `onboarding_complete`. They were three
6
+ * byte-identical copies of this wrapper.
7
+ *
8
+ * Emits only the domain event (`gateway_restart`) — the command lifecycle
9
+ * events are owned by the dispatch loop in connection.ts.
5
10
  */
6
11
  export async function handleGatewayRestartCommand(command) {
12
+ logCollector?.push('gateway_restart', 'info', 'Gateway restart requested', {
13
+ commandId: command.id,
14
+ type: command.type,
15
+ });
7
16
  try {
8
17
  const { stdout, stderr } = await restartGateway();
9
18
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"gateway-restart-handler.js","sourceRoot":"","sources":["../../src/utils/gateway-restart-handler.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,OAAqB;IACrE,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,cAAc,EAAE,CAAC;QAClD,OAAO;YACL,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;SACzB,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAAmD,CAAC;QAC9D,OAAO;YACL,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE;YACxD,KAAK,EAAE,CAAC,CAAC,OAAO;SACjB,CAAC;IACJ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"gateway-restart-handler.js","sourceRoot":"","sources":["../../src/utils/gateway-restart-handler.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,OAAqB;IACrE,YAAY,EAAE,IAAI,CAAC,iBAAiB,EAAE,MAAM,EAAE,2BAA2B,EAAE;QACzE,SAAS,EAAE,OAAO,CAAC,EAAE;QACrB,IAAI,EAAE,OAAO,CAAC,IAAI;KACnB,CAAC,CAAC;IACH,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,cAAc,EAAE,CAAC;QAClD,OAAO;YACL,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;SACzB,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAAmD,CAAC;QAC9D,OAAO;YACL,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE;YACxD,KAAK,EAAE,CAAC,CAAC,OAAO;SACjB,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -5,6 +5,13 @@ export interface GatewayRestartResult {
5
5
  /**
6
6
  * Restart the gateway via the configured agent provider.
7
7
  * Returns { stdout: '', stderr: '' } for interface compatibility.
8
+ *
9
+ * `configureProvider()` only ever runs inside the daemon's `main()`, so in
10
+ * EVERY CLI invocation `getProvider()` is null. This used to warn and return
11
+ * success, which made `agent-controller restart` (and the restart tail of
12
+ * `deploy` / `agent-update` / `self-update`) a silent no-op that still exited
13
+ * 0. We now build a throwaway provider from config instead, and throw when
14
+ * even that is impossible so the caller can exit non-zero.
8
15
  */
9
16
  export declare function restartGateway(): Promise<GatewayRestartResult>;
10
17
  /**
@@ -1,13 +1,21 @@
1
- import { getProvider } from '../providers/index.js';
1
+ import { createProviderFromConfig, getProvider } from '../providers/index.js';
2
+ import { config } from '../config.js';
2
3
  /**
3
4
  * Restart the gateway via the configured agent provider.
4
5
  * Returns { stdout: '', stderr: '' } for interface compatibility.
6
+ *
7
+ * `configureProvider()` only ever runs inside the daemon's `main()`, so in
8
+ * EVERY CLI invocation `getProvider()` is null. This used to warn and return
9
+ * success, which made `agent-controller restart` (and the restart tail of
10
+ * `deploy` / `agent-update` / `self-update`) a silent no-op that still exited
11
+ * 0. We now build a throwaway provider from config instead, and throw when
12
+ * even that is impossible so the caller can exit non-zero.
5
13
  */
6
14
  export async function restartGateway() {
7
- const provider = getProvider();
15
+ const provider = getProvider() ?? createProviderFromConfig();
8
16
  if (!provider) {
9
- console.warn('No provider configured, cannot restart gateway');
10
- return { stdout: '', stderr: '' };
17
+ throw new Error(`Cannot restart gateway: unknown agent provider '${config.agentProvider}' ` +
18
+ '(set AGENT_PROVIDER to openclaw, claude-code or mock)');
11
19
  }
12
20
  await provider.restart();
13
21
  return { stdout: '', stderr: '' };
@@ -1 +1 @@
1
- {"version":3,"file":"gateway-restart.js","sourceRoot":"","sources":["../../src/utils/gateway-restart.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAOpD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;IAC/B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;QAC/D,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACpC,CAAC;IACD,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;IACzB,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,cAAc,CAAC"}
1
+ {"version":3,"file":"gateway-restart.js","sourceRoot":"","sources":["../../src/utils/gateway-restart.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAOtC;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,QAAQ,GAAG,WAAW,EAAE,IAAI,wBAAwB,EAAE,CAAC;IAC7D,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,mDAAmD,MAAM,CAAC,aAAa,IAAI;YACzE,uDAAuD,CAC1D,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;IACzB,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,cAAc,CAAC"}
@@ -48,8 +48,20 @@ export declare class KnowledgeGraphManager extends EventEmitter {
48
48
  /** Load graph from disk into memory. Call once at startup. */
49
49
  init(): Promise<void>;
50
50
  private readGraphFromDisk;
51
- /** Serialized persist: enqueues writes so concurrent calls never interleave. */
51
+ /**
52
+ * Serialized persist: enqueues writes so concurrent calls never interleave.
53
+ * The queue tail swallows rejections so persistQueue is always fulfilled — a
54
+ * failed write never poisons the chain (the next write still runs) — while the
55
+ * caller of THIS write still observes its real success/failure.
56
+ */
52
57
  private persist;
58
+ /**
59
+ * Persist after an in-memory mutation, rolling the graph back to `snapshot`
60
+ * if the write fails. This keeps memory consistent with disk and with the
61
+ * backend (which is told the mutation failed and receives no delta) instead
62
+ * of leaving the failed mutation in memory to land on the next write.
63
+ */
64
+ private persistWithRollback;
53
65
  /** Atomic persist: write to .tmp, then fs.rename(). */
54
66
  private doPersist;
55
67
  /** Emit delta event after mutation (for sync to backend). */
@@ -33,24 +33,35 @@ export class KnowledgeGraphManager extends EventEmitter {
33
33
  try {
34
34
  const data = await fs.readFile(this.memoryFilePath, 'utf-8');
35
35
  const lines = data.split('\n').filter((line) => line.trim() !== '');
36
- return lines.reduce((graph, line) => {
37
- const item = JSON.parse(line);
38
- if (item.type === 'entity') {
36
+ const graph = { entities: [], relations: [] };
37
+ for (const line of lines) {
38
+ let item;
39
+ try {
40
+ item = JSON.parse(line);
41
+ }
42
+ catch {
43
+ // Skip a single corrupt/truncated line instead of discarding the
44
+ // entire graph — recover every well-formed record around it.
45
+ continue;
46
+ }
47
+ if (item.type === 'entity' && typeof item.name === 'string') {
39
48
  graph.entities.push({
40
49
  name: item.name,
41
- entityType: item.entityType,
42
- observations: item.observations,
50
+ entityType: item.entityType ?? '',
51
+ observations: item.observations ?? [],
43
52
  });
44
53
  }
45
- if (item.type === 'relation') {
54
+ else if (item.type === 'relation' &&
55
+ typeof item.from === 'string' &&
56
+ typeof item.to === 'string') {
46
57
  graph.relations.push({
47
58
  from: item.from,
48
59
  to: item.to,
49
- relationType: item.relationType,
60
+ relationType: item.relationType ?? '',
50
61
  });
51
62
  }
52
- return graph;
53
- }, { entities: [], relations: [] });
63
+ }
64
+ return graph;
54
65
  }
55
66
  catch (error) {
56
67
  if (error?.code === 'ENOENT') {
@@ -59,10 +70,31 @@ export class KnowledgeGraphManager extends EventEmitter {
59
70
  throw error;
60
71
  }
61
72
  }
62
- /** Serialized persist: enqueues writes so concurrent calls never interleave. */
73
+ /**
74
+ * Serialized persist: enqueues writes so concurrent calls never interleave.
75
+ * The queue tail swallows rejections so persistQueue is always fulfilled — a
76
+ * failed write never poisons the chain (the next write still runs) — while the
77
+ * caller of THIS write still observes its real success/failure.
78
+ */
63
79
  persist() {
64
- this.persistQueue = this.persistQueue.then(() => this.doPersist()).catch(() => { });
65
- return this.persistQueue;
80
+ const run = this.persistQueue.then(() => this.doPersist());
81
+ this.persistQueue = run.then(() => undefined, () => undefined);
82
+ return run;
83
+ }
84
+ /**
85
+ * Persist after an in-memory mutation, rolling the graph back to `snapshot`
86
+ * if the write fails. This keeps memory consistent with disk and with the
87
+ * backend (which is told the mutation failed and receives no delta) instead
88
+ * of leaving the failed mutation in memory to land on the next write.
89
+ */
90
+ async persistWithRollback(snapshot) {
91
+ try {
92
+ await this.persist();
93
+ }
94
+ catch (err) {
95
+ this.graph = snapshot;
96
+ throw err;
97
+ }
66
98
  }
67
99
  /** Atomic persist: write to .tmp, then fs.rename(). */
68
100
  async doPersist() {
@@ -89,8 +121,9 @@ export class KnowledgeGraphManager extends EventEmitter {
89
121
  // -------------------------------------------------------------------------
90
122
  async createEntities(entities) {
91
123
  const newEntities = entities.filter((e) => !this.graph.entities.some((existing) => existing.name === e.name));
124
+ const snapshot = structuredClone(this.graph);
92
125
  this.graph.entities.push(...newEntities);
93
- await this.persist();
126
+ await this.persistWithRollback(snapshot);
94
127
  this.emitDelta('entities_created', newEntities);
95
128
  return newEntities;
96
129
  }
@@ -98,44 +131,57 @@ export class KnowledgeGraphManager extends EventEmitter {
98
131
  const newRelations = relations.filter((r) => !this.graph.relations.some((existing) => existing.from === r.from &&
99
132
  existing.to === r.to &&
100
133
  existing.relationType === r.relationType));
134
+ const snapshot = structuredClone(this.graph);
101
135
  this.graph.relations.push(...newRelations);
102
- await this.persist();
136
+ await this.persistWithRollback(snapshot);
103
137
  this.emitDelta('relations_created', newRelations);
104
138
  return newRelations;
105
139
  }
106
140
  async addObservations(observations) {
107
- const results = observations.map((o) => {
108
- const entity = this.graph.entities.find((e) => e.name === o.entityName);
109
- if (!entity) {
110
- throw new Error(`Entity with name ${o.entityName} not found`);
111
- }
112
- const newObservations = o.contents.filter((content) => !entity.observations.includes(content));
113
- entity.observations.push(...newObservations);
114
- return { entityName: o.entityName, addedObservations: newObservations };
115
- });
116
- await this.persist();
117
- this.emitDelta('observations_added', results);
118
- return results;
141
+ const snapshot = structuredClone(this.graph);
142
+ try {
143
+ const results = observations.map((o) => {
144
+ const entity = this.graph.entities.find((e) => e.name === o.entityName);
145
+ if (!entity) {
146
+ throw new Error(`Entity with name ${o.entityName} not found`);
147
+ }
148
+ const newObservations = o.contents.filter((content) => !entity.observations.includes(content));
149
+ entity.observations.push(...newObservations);
150
+ return { entityName: o.entityName, addedObservations: newObservations };
151
+ });
152
+ await this.persistWithRollback(snapshot);
153
+ this.emitDelta('observations_added', results);
154
+ return results;
155
+ }
156
+ catch (err) {
157
+ // Roll back any partial in-memory mutation (a missing entity throws
158
+ // mid-map after earlier entities were already mutated).
159
+ this.graph = snapshot;
160
+ throw err;
161
+ }
119
162
  }
120
163
  async deleteEntities(names) {
164
+ const snapshot = structuredClone(this.graph);
121
165
  this.graph.entities = this.graph.entities.filter((e) => !names.includes(e.name));
122
166
  this.graph.relations = this.graph.relations.filter((r) => !names.includes(r.from) && !names.includes(r.to));
123
- await this.persist();
167
+ await this.persistWithRollback(snapshot);
124
168
  this.emitDelta('entities_deleted', names);
125
169
  }
126
170
  async deleteObservations(deletions) {
171
+ const snapshot = structuredClone(this.graph);
127
172
  deletions.forEach((d) => {
128
173
  const entity = this.graph.entities.find((e) => e.name === d.entityName);
129
174
  if (entity) {
130
175
  entity.observations = entity.observations.filter((o) => !d.observations.includes(o));
131
176
  }
132
177
  });
133
- await this.persist();
178
+ await this.persistWithRollback(snapshot);
134
179
  this.emitDelta('observations_deleted', deletions);
135
180
  }
136
181
  async deleteRelations(relations) {
182
+ const snapshot = structuredClone(this.graph);
137
183
  this.graph.relations = this.graph.relations.filter((r) => !relations.some((del) => r.from === del.from && r.to === del.to && r.relationType === del.relationType));
138
- await this.persist();
184
+ await this.persistWithRollback(snapshot);
139
185
  this.emitDelta('relations_deleted', relations);
140
186
  }
141
187
  // -------------------------------------------------------------------------