@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.9

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 (130) hide show
  1. package/out/analytics/dashboard/provider.js +13 -2
  2. package/out/analytics/dashboard/webview/panel.js +3 -2
  3. package/out/analytics/identity.js +0 -0
  4. package/out/analytics/service.js +23 -25
  5. package/out/analytics/sessionEvents.js +98 -23
  6. package/out/auth/authBaseUrl.js +59 -0
  7. package/out/auth/bootstrapApply.js +9 -10
  8. package/out/auth/brokerClient.js +48 -14
  9. package/out/auth/provider.js +8 -21
  10. package/out/auth/sessionStore.js +3 -40
  11. package/out/auth/signInFlow.js +2 -1
  12. package/out/auth-broker/config.js +50 -5
  13. package/out/auth-broker/server.js +122 -26
  14. package/out/auth-broker/store.js +25 -24
  15. package/out/authority/warrant.js +206 -0
  16. package/out/branding.js +27 -1
  17. package/out/cli/commands/conformance.js +249 -0
  18. package/out/cli/commands/data-plane.js +67 -0
  19. package/out/cli/commands/deploy.js +1 -0
  20. package/out/cli/commands/gateway.js +4 -0
  21. package/out/cli/commands/pack.js +182 -3
  22. package/out/cli/commands/sandbox.js +332 -0
  23. package/out/cli/commands/serve.js +875 -52
  24. package/out/cli/commands/store.js +12 -4
  25. package/out/cli/data-plane/gateway.js +28 -5
  26. package/out/cli/data-plane/index.js +17 -2
  27. package/out/cli/data-plane/registry.js +9 -0
  28. package/out/cli/data-plane/remote.js +106 -0
  29. package/out/cli/data-plane/sidecar.js +161 -0
  30. package/out/cli/deploy/layers/identity.js +85 -4
  31. package/out/cli/deploy-init.js +89 -24
  32. package/out/cli/deps.js +106 -2
  33. package/out/cli/idp-init.js +35 -0
  34. package/out/cli/index.js +155 -16
  35. package/out/cli/pack-install-ref.js +15 -0
  36. package/out/cli/pack-loader.js +8 -1
  37. package/out/cli/pack-runtime.js +61 -0
  38. package/out/cli/runtime.js +35 -1
  39. package/out/cli/serve-hardening.js +80 -3
  40. package/out/cli/utils.js +12 -1
  41. package/out/engines/base.js +32 -3
  42. package/out/engines/claude.js +33 -15
  43. package/out/engines/codex.js +110 -26
  44. package/out/engines/custom.js +8 -0
  45. package/out/evidence/sessionShip.js +610 -0
  46. package/out/extension.js +11 -2
  47. package/out/gateway/bridge.js +40 -2
  48. package/out/gateway/client.js +34 -1
  49. package/out/gateway/installSource.js +68 -0
  50. package/out/gateway/lifecycle.js +185 -51
  51. package/out/gateway/outboundPaths.js +4 -1
  52. package/out/gateway/secretsPreflight.js +90 -0
  53. package/out/gateway/service.js +7 -2
  54. package/out/headless/index.js +17 -1
  55. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  56. package/out/mcp.js +30 -0
  57. package/out/pack/caps.js +271 -0
  58. package/out/pack/governor.js +309 -0
  59. package/out/pack/lease-schema.js +270 -0
  60. package/out/pack/lease.js +640 -0
  61. package/out/pack/reconcile.js +217 -0
  62. package/out/pack/signature.js +839 -0
  63. package/out/remote/handleRemoteChat.js +44 -3
  64. package/out/remote/remoteTrust.js +277 -0
  65. package/out/remote/serveClient.js +52 -5
  66. package/out/runner/host.js +131 -0
  67. package/out/runner/protocol.js +108 -0
  68. package/out/runner/remoteEngine.js +130 -0
  69. package/out/runtime/interfaces/edit-tracker.js +2 -0
  70. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  71. package/out/sandbox/containerDriver.js +570 -0
  72. package/out/sandbox/processDriver.js +315 -0
  73. package/out/sandbox/sandboxRunner.js +191 -0
  74. package/out/sandbox/support.js +301 -0
  75. package/out/sandbox/types.js +55 -0
  76. package/out/scripts/agentide-litellm.sh +67 -5
  77. package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
  78. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -132
  79. package/out/scripts/analytics/setup-analytics.ps1 +37 -13
  80. package/out/scripts/analytics/setup-analytics.sh +93 -13
  81. package/out/scripts/analytics/start-analytics.sh +86 -14
  82. package/out/scripts/gateway/setup-gateway.sh +72 -32
  83. package/out/shared/models.js +11 -110
  84. package/out/spotlight/install.js +7 -1
  85. package/out/store/index.js +41 -10
  86. package/out/sync.js +3 -1
  87. package/out/test/analytics.test.js +162 -23
  88. package/out/test/auth-baseurl.test.js +132 -0
  89. package/out/test/auth-bootstrapApply.test.js +27 -6
  90. package/out/test/auth-broker-hardening.test.js +227 -0
  91. package/out/test/auth-broker.test.js +59 -9
  92. package/out/test/branding.test.js +13 -0
  93. package/out/test/caps-governor.test.js +455 -0
  94. package/out/test/codex-new-file-survival.test.js +119 -0
  95. package/out/test/configuration-scopes.test.js +131 -0
  96. package/out/test/data-plane.test.js +67 -0
  97. package/out/test/deploy-init.test.js +40 -0
  98. package/out/test/gateway-client.test.js +28 -0
  99. package/out/test/gateway-install-source.test.js +111 -0
  100. package/out/test/gateway-lifecycle.test.js +1 -1
  101. package/out/test/gateway-outbound.test.js +7 -0
  102. package/out/test/gateway-secrets-preflight.test.js +127 -0
  103. package/out/test/gateway-session-routing.test.js +18 -0
  104. package/out/test/governor-hardening.test.js +217 -0
  105. package/out/test/headless-sync.test.js +22 -0
  106. package/out/test/lease-signature.test.js +357 -0
  107. package/out/test/lease-trust-root.test.js +273 -0
  108. package/out/test/lease-validation.test.js +169 -0
  109. package/out/test/lease.test.js +114 -0
  110. package/out/test/mcp.test.js +27 -1
  111. package/out/test/pack-admission.test.js +216 -0
  112. package/out/test/pack-digest-parity.test.js +140 -0
  113. package/out/test/pack-reconcile.test.js +183 -0
  114. package/out/test/pack-signature-artifact.test.js +103 -0
  115. package/out/test/pack-signature.test.js +176 -0
  116. package/out/test/pack-trust-root.test.js +202 -0
  117. package/out/test/remote-trust.test.js +242 -0
  118. package/out/test/runner-protocol.test.js +249 -0
  119. package/out/test/sandbox-driver.test.js +553 -0
  120. package/out/test/sandbox-hardening.test.js +395 -0
  121. package/out/test/sandbox-runner.test.js +343 -0
  122. package/out/test/serve-hardening.test.js +45 -6
  123. package/out/test/serveClient.test.js +6 -5
  124. package/out/test/sessionShip.test.js +560 -0
  125. package/out/test/spotlightPublish.test.js +28 -0
  126. package/out/test/store-install-safe.test.js +69 -0
  127. package/out/test/tls-bypass.test.js +90 -0
  128. package/out/test/warrant.test.js +138 -0
  129. package/out/utils.js +46 -9
  130. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  /*---------------------------------------------------------------------------------------------
3
3
  * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ * Licensed under the MIT License. See LICENSE.txt in the project root for license information.
4
5
  *--------------------------------------------------------------------------------------------*/
5
6
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
7
  if (k2 === undefined) k2 = k;
@@ -42,6 +43,7 @@ exports.generateRequestId = generateRequestId;
42
43
  exports.persistSessionIfNeeded = persistSessionIfNeeded;
43
44
  exports.touchSessionIfNeeded = touchSessionIfNeeded;
44
45
  exports.getPersistedSessionId = getPersistedSessionId;
46
+ const identity_1 = require("../analytics/identity");
45
47
  const session_1 = require("../session");
46
48
  const path = __importStar(require("path"));
47
49
  /**
@@ -54,16 +56,26 @@ const path = __importStar(require("path"));
54
56
  * Langfuse passthrough: langfuse_session_id / langfuse_trace_user_id headers
55
57
  * are included so users who add Langfuse as a LiteLLM callback get session
56
58
  * grouping automatically.
59
+ *
60
+ * IDENTITY: the user id is a salted hash (see analytics/identity.ts). It used to be the raw
61
+ * value — usually an email — sent on two headers to a proxy that logs every request and forwards
62
+ * to a third-party observability backend. The hash is stable, so grouping and per-user analytics
63
+ * still work; it is just no longer a directory of who works here. Set
64
+ * AGENTHIPPO_EVIDENCE_RAW_IDS=1 to send the raw value again.
65
+ *
66
+ * The provider never sees either form: `additional_drop_params: ["user"]` in the LiteLLM config
67
+ * strips the end-user field before dispatch.
57
68
  */
58
69
  function buildLiteLLMMetadataHeaders(useLiteLLM, metadata) {
59
70
  if (!useLiteLLM) {
60
71
  return [];
61
72
  }
62
73
  const headers = [];
63
- if (metadata.analyticsUserId?.trim()) {
64
- headers.push(`x-litellm-metadata-user-id: ${metadata.analyticsUserId.trim()}`);
74
+ const userId = (0, identity_1.hashIdentity)(metadata.analyticsUserId);
75
+ if (userId) {
76
+ headers.push(`x-litellm-metadata-user-id: ${userId}`);
65
77
  // Langfuse passthrough: maps to Langfuse trace user_id when Langfuse callback is enabled
66
- headers.push(`langfuse_trace_user_id: ${metadata.analyticsUserId.trim()}`);
78
+ headers.push(`langfuse_trace_user_id: ${userId}`);
67
79
  }
68
80
  if (metadata.sessionId?.trim()) {
69
81
  headers.push(`x-litellm-metadata-session-id: ${metadata.sessionId.trim()}`);
@@ -88,6 +100,23 @@ function buildLiteLLMMetadataHeaders(useLiteLLM, metadata) {
88
100
  if (metadata.requestId?.trim()) {
89
101
  headers.push(`x-litellm-metadata-request-id: ${metadata.requestId.trim()}`);
90
102
  }
103
+ // Provenance stamp (PLAN/compliance.md §3.1): lands in requester_metadata on every OTEL
104
+ // span, which is where the evidence index reads policy_version / pack_digest from.
105
+ if (metadata.policyVersion?.trim()) {
106
+ headers.push(`x-litellm-metadata-policy-version: ${metadata.policyVersion.trim()}`);
107
+ }
108
+ if (metadata.packDigest?.trim()) {
109
+ headers.push(`x-litellm-metadata-pack-digest: ${metadata.packDigest.trim()}`);
110
+ }
111
+ if (metadata.engineVersion?.trim()) {
112
+ headers.push(`x-litellm-metadata-engine-version: ${metadata.engineVersion.trim()}`);
113
+ }
114
+ if (metadata.tier !== undefined) {
115
+ headers.push(`x-litellm-metadata-tier: ${metadata.tier}`);
116
+ }
117
+ if (metadata.instanceId?.trim()) {
118
+ headers.push(`x-litellm-metadata-instance-id: ${metadata.instanceId.trim()}`);
119
+ }
91
120
  return headers;
92
121
  }
93
122
  /**
@@ -39,14 +39,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.ClaudeEngine = void 0;
40
40
  const fs_1 = require("fs");
41
41
  const path = __importStar(require("path"));
42
+ const branding_1 = require("../branding");
42
43
  const base_1 = require("./base");
43
44
  const utils_1 = require("../utils");
44
45
  const permissions_1 = require("../permissions");
45
46
  const mcp_1 = require("../mcp");
46
- const data_plane_1 = require("../cli/data-plane");
47
47
  const runtime_1 = require("../cli/runtime");
48
48
  const session_1 = require("../session");
49
49
  const agentpack_1 = require("../agentpack");
50
+ const credential_context_1 = require("../cli/credential-context");
50
51
  const shared_1 = require("../shared");
51
52
  // File tools we track for edit diffs via hooks (Set for O(1) lookup)
52
53
  // Includes Claude SDK tools, Computer Use tools, and common variations
@@ -261,6 +262,11 @@ class ClaudeEngine {
261
262
  source: 'agenthippo',
262
263
  engine: 'claude',
263
264
  requestId: options.requestId,
265
+ policyVersion: options.policyVersion,
266
+ packDigest: options.packDigest,
267
+ instanceId: options.instanceId,
268
+ engineVersion: options.engineVersion,
269
+ tier: options.tier,
264
270
  });
265
271
  // Build environment for Claude SDK.
266
272
  // Setup-token auth must use Claude's OAuth token env var instead of ANTHROPIC_API_KEY.
@@ -270,11 +276,13 @@ class ClaudeEngine {
270
276
  'DEBUG_CLAUDE_AGENT_SDK': '1',
271
277
  'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC': '1',
272
278
  };
273
- // Egress broker active ⇒ the gateway token stays broker-side; scrub it from the child
274
- // env so it never reaches the model (secure-obo-design §7.6 / §7.4 gap 1).
275
- if (claudeEnv[data_plane_1.EGRESS_URL_ENV]) {
276
- delete claudeEnv[runtime_1.OPENCLAW_GATEWAY_TOKEN_ENV];
277
- }
279
+ // The gateway token stays broker-side; scrub it from the child env so it never reaches
280
+ // the model (secure-obo-design §7.6 / §7.4 gap 1). UNCONDITIONALLY (serve-v4 §10 hole 2):
281
+ // gating this on the egress broker let a deployment without --gateway-bridge re-inherit
282
+ // the token, including via buildCliEnvPatch's bake from the host's ~/.openclaw config.
283
+ delete claudeEnv[runtime_1.OPENCLAW_GATEWAY_TOKEN_ENV];
284
+ // Data-plane sidecar control token: never the engine's to hold (secure-obo-design §7.6).
285
+ delete claudeEnv['AGENTHIPPO_DATA_PLANE_TOKEN'];
278
286
  const isSetupTokenAuth = (0, shared_1.isAnthropicSetupToken)(apiKey);
279
287
  if (isSetupTokenAuth) {
280
288
  const normalizedSetupToken = (0, shared_1.normalizeAnthropicSetupToken)(apiKey);
@@ -303,8 +311,13 @@ class ClaudeEngine {
303
311
  // Note: cwd is ALWAYS workspaceRoot - active agent content is synced to workspace's .claude/.codex
304
312
  const agentId = options.agentPackId ?? 'default';
305
313
  const runtimePaths = (0, agentpack_1.resolveRuntimePaths)(options.workspaceRoot, this.config.storagePath, agentId, options.agentPackVersion);
306
- // Claude config directory (isolated per agent for session/state isolation)
307
- const claudeConfigDir = runtimePaths.claudeConfigDir;
314
+ // Claude config directory (isolated per agent for session/state isolation). In
315
+ // multi-tenant serve, additionally key it by the verified request user — the same
316
+ // §8 Phase 0 scoping codex.ts applies to CODEX_HOME. A shared per-agent dir lets
317
+ // concurrent turns of different users race each other's per-turn credential state
318
+ // (the agentpack/paths.ts race); per-user scoping makes any remaining race
319
+ // same-user. No credential frame (IDE, single-tenant CLI) ⇒ unchanged path.
320
+ const claudeConfigDir = (0, agentpack_1.userScopedEngineHome)(runtimePaths.claudeConfigDir, (0, credential_context_1.currentCredential)()?.userId);
308
321
  claudeEnv['CLAUDE_CONFIG_DIR'] = claudeConfigDir;
309
322
  await fs_1.promises.mkdir(claudeConfigDir, { recursive: true });
310
323
  // Always set AGENT_ID for agent to know its identity
@@ -549,9 +562,10 @@ class ClaudeEngine {
549
562
  logger.debug(`[AgentIDE] SessionStart hook called! event=${input.hook_event_name}`);
550
563
  return CONTINUE;
551
564
  };
552
- // Load MCP servers for the active agent and pass directly to SDK. claudeEnv is the
553
- // per-turn child env — URL servers resolve their bearerTokenEnv (turn nonce) from it,
554
- // in memory, so no per-turn value ever lands in a shared config file.
565
+ // Load MCP servers for the active agent and pass directly to SDK. URL servers carry a
566
+ // `${NAME}` bearer reference the CLI expands from claudeEnv at connect time, so the
567
+ // turn nonce rides neither a shared config file nor the SDK's --mcp-config argv —
568
+ // claudeEnv is passed for the rare non-expandable-name fallback only.
555
569
  const mcpServers = await this.loadMcpServersForClaude(logger, options.workspaceRoot, agentId, claudeEnv);
556
570
  // Determine cwd and settingSources
557
571
  // cwd is ALWAYS workspaceRoot - active agent content has been synced to workspace's .claude/
@@ -1068,7 +1082,7 @@ class ClaudeEngine {
1068
1082
  logger.info(`[AgentIDE] Claude thinking summary: startBlocks=${thinkingStartBlockCount}, redactedBlocks=${redactedThinkingBlockCount}, deltas=${thinkingDeltaCount}, chars=${thinkingCharCount}`);
1069
1083
  logger.info(`[AgentIDE] Claude tool summary: toolBlocks=${toolUseBlockCount}, toolEmits=${toolUseEmitCount}, toolProgressMsgs=${toolProgressCount}, inputJsonDeltas=${inputJsonDeltaCount}`);
1070
1084
  if (thinkingStartBlockCount === 0 && thinkingDeltaCount === 0 && redactedThinkingBlockCount === 0) {
1071
- logger.debug('[AgentIDE] Claude emitted no thinking blocks/deltas. Ensure model supports extended thinking and set agentide.claude.maxThinkingTokens >= 1024.');
1085
+ logger.debug(`[AgentIDE] Claude emitted no thinking blocks/deltas. Ensure model supports extended thinking and set ${(0, branding_1.toSettingsDisplayKey)('agentide.claude.maxThinkingTokens')} >= 1024.`);
1072
1086
  }
1073
1087
  if (toolUseBlockCount === 0 && toolProgressCount === 0) {
1074
1088
  logger.debug('[AgentIDE] Claude emitted no tool usage events. If tools were used, ensure includePartialMessages=true and parse tool_use blocks.');
@@ -1202,8 +1216,12 @@ class ClaudeEngine {
1202
1216
  * - Pack agents: uses pack's mcp/ directory
1203
1217
  *
1204
1218
  * The SDK accepts mcpServers directly in query options — including HTTP Streamable
1205
- * servers (`{type:'http', url, headers}`). `turnEnv` is the per-turn child env; a URL
1206
- * server's `bearerTokenEnv` (e.g. AGENTHIPPO_TURN_NONCE) resolves against it in memory.
1219
+ * servers (`{type:'http', url, headers}`). A URL server's `bearerTokenEnv` (e.g.
1220
+ * AGENTHIPPO_TURN_NONCE) becomes a `${NAME}` reference the CLI expands from its own env,
1221
+ * NOT a resolved header: the SDK serializes mcpServers into `--mcp-config` argv, and a
1222
+ * resolved bearer there is world-readable via /proc/<pid>/cmdline (see
1223
+ * referenceHttpMcpHeaders). `turnEnv` is the per-turn child env, used only for the
1224
+ * fallback when a name is outside the CLI's expansion grammar.
1207
1225
  */
1208
1226
  async loadMcpServersForClaude(logger, workspaceRoot, agentId = 'default', turnEnv) {
1209
1227
  const basePromptsDir = this.getBundledBasePromptsDir();
@@ -1214,7 +1232,7 @@ class ClaudeEngine {
1214
1232
  const mcpServers = {};
1215
1233
  for (const s of servers) {
1216
1234
  if (s.url) {
1217
- const headers = (0, mcp_1.resolveHttpMcpHeaders)(s, turnEnv ?? process.env);
1235
+ const headers = (0, mcp_1.referenceHttpMcpHeaders)(s, turnEnv ?? process.env);
1218
1236
  mcpServers[s.name] = {
1219
1237
  type: 'http',
1220
1238
  url: s.url,
@@ -37,6 +37,7 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.CodexEngine = void 0;
40
+ exports.registerNewFilesWithTracker = registerNewFilesWithTracker;
40
41
  const fs_1 = require("fs");
41
42
  const path = __importStar(require("path"));
42
43
  const mcp_1 = require("../mcp");
@@ -47,8 +48,8 @@ const permissions_1 = require("../permissions");
47
48
  const session_1 = require("../session");
48
49
  const agentpack_1 = require("../agentpack");
49
50
  const credential_context_1 = require("../cli/credential-context");
50
- const data_plane_1 = require("../cli/data-plane");
51
51
  const runtime_1 = require("../cli/runtime");
52
+ const types_1 = require("../sandbox/types");
52
53
  // Note: HTTP edit server removed - file changes are now handled directly via file_change events
53
54
  // -----------------------------------------------------------------------------
54
55
  // Constants
@@ -65,11 +66,56 @@ const AGENTIDE_CODEX_AGENT_VERSION_ENV_VAR = 'AGENTIDE_ANALYTICS_AGENT_VERSION';
65
66
  const AGENTIDE_CODEX_SOURCE_ENV_VAR = 'AGENTIDE_ANALYTICS_SOURCE';
66
67
  const AGENTIDE_CODEX_ENGINE_ENV_VAR = 'AGENTIDE_ANALYTICS_ENGINE';
67
68
  const AGENTIDE_CODEX_REQUEST_ID_ENV_VAR = 'AGENTIDE_ANALYTICS_REQUEST_ID';
69
+ const AGENTIDE_CODEX_POLICY_VERSION_ENV_VAR = 'AGENTIDE_ANALYTICS_POLICY_VERSION';
70
+ const AGENTIDE_CODEX_PACK_DIGEST_ENV_VAR = 'AGENTIDE_ANALYTICS_PACK_DIGEST';
71
+ const AGENTIDE_CODEX_ENGINE_VERSION_ENV_VAR = 'AGENTIDE_ANALYTICS_ENGINE_VERSION';
72
+ const AGENTIDE_CODEX_TIER_ENV_VAR = 'AGENTIDE_ANALYTICS_TIER';
73
+ const AGENTIDE_CODEX_INSTANCE_ID_ENV_VAR = 'AGENTIDE_ANALYTICS_INSTANCE_ID';
68
74
  // Bump when we change generated config semantics so long-lived sessions regenerate config.toml.
69
- const AGENTIDE_CODEX_CONFIG_VERSION = 20; // v20: default_tools_approval_mode "auto"→"approve" ("auto" is invalid on codex ≥0.131 and cancels every headless MCP tool call)
75
+ const AGENTIDE_CODEX_CONFIG_VERSION = 21; // v21: sandbox_mode "danger-full-access" now requires tier 2, not merely being inside a sandbox — a tier-0 home written by v20 must be rewritten
70
76
  // Per-process runtime context for agents (not analytics): lets the agent locate its per-session context export.
71
77
  const AGENTIDE_CONTEXT_EXPORT_PATH_ENV_VAR = 'AGENTIDE_CONTEXT_EXPORT_PATH';
72
78
  const AGENTIDE_TERMINAL_LOG_PATH_ENV_VAR = 'AGENTIDE_TERMINAL_LOG_PATH';
79
+ /**
80
+ * Hand files codex just CREATED to the edit tracker so the IDE can render them against an empty
81
+ * original — without ever risking the agent's work.
82
+ *
83
+ * The IDE trick is read → delete → `applyEdit`. That delete caused live data loss: headless
84
+ * (`serve`/`ask`/`chat` and every sandbox) gets `NoOpEditTracker`, whose `applyEdit` does nothing, so
85
+ * every new file codex created was silently destroyed. The command exited 0, the file was there
86
+ * during the command, and it was gone by the next one — while standalone `codex exec` wrote it
87
+ * fine. Two guards now: skip trackers that do not write back, and restore the file if it is
88
+ * missing when we are done, whatever the tracker did or threw.
89
+ *
90
+ * Exported so the invariant "a file the agent created still exists after the turn" is testable.
91
+ */
92
+ async function registerNewFilesWithTracker(files, editTracker, sessionId) {
93
+ // A tracker that does not write the file back must never see a delete.
94
+ if (editTracker.appliesEdits === false) {
95
+ return;
96
+ }
97
+ for (const f of files) {
98
+ try {
99
+ if ((0, utils_1.isBinaryFile)(f)) {
100
+ continue; // Binary would be corrupted by the UTF-8 read/write round trip.
101
+ }
102
+ if (!(await (0, utils_1.pathExists)(f))) {
103
+ continue;
104
+ }
105
+ const code = await fs_1.promises.readFile(f, 'utf-8');
106
+ try {
107
+ await fs_1.promises.unlink(f);
108
+ await editTracker.applyEdit(sessionId, f, code, true);
109
+ }
110
+ finally {
111
+ if (!(await (0, utils_1.pathExists)(f))) {
112
+ await fs_1.promises.writeFile(f, code, 'utf-8');
113
+ }
114
+ }
115
+ }
116
+ catch { /* ignore — never fail a turn over edit-diff bookkeeping */ }
117
+ }
118
+ }
73
119
  function isCodexItemEvent(event) {
74
120
  return event.type === 'item.started' || event.type === 'item.updated' || event.type === 'item.completed';
75
121
  }
@@ -130,13 +176,17 @@ class CodexEngine {
130
176
  ...(runtime.cliEnvPatch ?? {}),
131
177
  OPENAI_LOG: 'debug',
132
178
  };
133
- // When serve runs the egress broker, the OpenClaw gateway token must never enter the
134
- // agent's trust domain (secure-obo-design §7.6 / §7.4 gap 1). Scrub it from the child env
135
- // at the spawn boundary — belt-and-suspenders with serve's boot-time process.env strip,
136
- // so a token in process.env (e.g. captured before the strip) still cannot reach the model.
137
- if (codexEnv[data_plane_1.EGRESS_URL_ENV]) {
138
- delete codexEnv[runtime_1.OPENCLAW_GATEWAY_TOKEN_ENV];
139
- }
179
+ // The OpenClaw gateway token must never enter the agent's trust domain (secure-obo-design
180
+ // §7.6 / §7.4 gap 1). UNCONDITIONALLY (serve-v4 §10 hole 2): the scrub used to depend on
181
+ // the egress broker being up, so a deployment without --gateway-bridge re-inherited the
182
+ // token — and buildCliEnvPatch can bake it in from the host's ~/.openclaw config, not just
183
+ // process.env. This engine never needs it: outbound goes through the broker's turn nonce,
184
+ // and the openclaw CLI reads its own config file on single-tenant hosts.
185
+ delete codexEnv[runtime_1.OPENCLAW_GATEWAY_TOKEN_ENV];
186
+ // Data-plane sidecar control token: the engine only needs the gateway URL + its per-turn
187
+ // nonce, never the register token — scrub it so a compromised agent can't forge a turn as
188
+ // another user (secure-obo-design §7.6). Harmless when absent (in-process/no data plane).
189
+ delete codexEnv['AGENTHIPPO_DATA_PLANE_TOKEN'];
140
190
  // Always expose the VS Code chat session id to the agent process so it can locate:
141
191
  // .agentide/sessions/<sessionId>/context.md
142
192
  const rawChatSessionId = sessionId?.trim();
@@ -179,7 +229,15 @@ class CodexEngine {
179
229
  agentId, // Pass agent ID for correct skills path
180
230
  runtime.permissionHandler instanceof interfaces_1.AutoApprovePermissionHandler);
181
231
  // LiteLLM analytics metadata (enriched - see PLAN/analytics-design.md)
182
- if (useLiteLLM && this.config.codexHome) {
232
+ //
233
+ // Gate on the codexHome actually in use (resolved above), NOT `this.config.codexHome`:
234
+ // headless/index.ts passes `codexHome: undefined`, so under serve/ask/chat and inside
235
+ // every sandbox this block was skipped and codex sent NO metadata headers at all — no
236
+ // session-id, user-id, agent-id, source, request-id, or engine. Spotlight lost codex
237
+ // attribution outside the IDE, and the gateway could not see `x-litellm-metadata-engine`,
238
+ // so engine-conditional ah-auto routing never fired. config.toml was written correctly
239
+ // the whole time (it uses this same resolved home) — only the env vars it reads were missing.
240
+ if (useLiteLLM && codexHome) {
183
241
  if (analyticsUserId?.trim()) {
184
242
  codexEnv[AGENTIDE_CODEX_USER_ENV_VAR] = analyticsUserId.trim();
185
243
  }
@@ -197,6 +255,21 @@ class CodexEngine {
197
255
  if (options.requestId) {
198
256
  codexEnv[AGENTIDE_CODEX_REQUEST_ID_ENV_VAR] = options.requestId;
199
257
  }
258
+ if (options.policyVersion?.trim()) {
259
+ codexEnv[AGENTIDE_CODEX_POLICY_VERSION_ENV_VAR] = options.policyVersion.trim();
260
+ }
261
+ if (options.packDigest?.trim()) {
262
+ codexEnv[AGENTIDE_CODEX_PACK_DIGEST_ENV_VAR] = options.packDigest.trim();
263
+ }
264
+ if (options.engineVersion?.trim()) {
265
+ codexEnv[AGENTIDE_CODEX_ENGINE_VERSION_ENV_VAR] = options.engineVersion.trim();
266
+ }
267
+ if (options.tier !== undefined) {
268
+ codexEnv[AGENTIDE_CODEX_TIER_ENV_VAR] = String(options.tier);
269
+ }
270
+ if (options.instanceId?.trim()) {
271
+ codexEnv[AGENTIDE_CODEX_INSTANCE_ID_ENV_VAR] = options.instanceId.trim();
272
+ }
200
273
  }
201
274
  // For custom providers (OpenRouter, LiteLLM), set API key in CODEX_API_KEY.
202
275
  // The config.toml's `env_key = "CODEX_API_KEY"` tells the CLI to read from this variable.
@@ -225,6 +298,16 @@ class CodexEngine {
225
298
  ];
226
299
  // Map unified permissions to Codex SDK options
227
300
  const codexPermOpts = (0, permissions_1.toCodexOptions)(effectivePerms, workspaceFolders);
301
+ // One boundary, not two — but only when the OTHER boundary is real. Inside a tier-2
302
+ // container Codex's own bubblewrap sandbox cannot start (it needs a user namespace
303
+ // `--cap-drop ALL` + `no-new-privileges` deny), so standing it down is the difference
304
+ // between working shell commands and silent failures. The tier-0 process driver
305
+ // provides NO boundary — same uid, same filesystem as serve — so there Codex's own
306
+ // sandbox is the only thing between the agent and the host and must stay on.
307
+ // The SDK's ThreadOptions.sandboxMode overrides config.toml, so force it here too.
308
+ if ((0, types_1.isContainerSandbox)()) {
309
+ codexPermOpts.sandboxMode = 'danger-full-access';
310
+ }
228
311
  logger.info(`[AgentIDE] Codex mapped: sandboxMode=${codexPermOpts.sandboxMode}, approvalPolicy=${codexPermOpts.approvalPolicy}`);
229
312
  // Build thread options
230
313
  // workingDirectory is ALWAYS workspaceRoot - pack content is synced to workspace's .codex/
@@ -411,21 +494,7 @@ class CodexEngine {
411
494
  catch { /* ignore */ }
412
495
  pendingOperations.delete(item.id);
413
496
  }
414
- // New files: read, delete, apply (so VS Code sees original=empty)
415
- // Skip binary files - they would be corrupted by UTF-8 read/write
416
- for (const f of pendingNewFiles.get(item.id) ?? []) {
417
- try {
418
- if ((0, utils_1.isBinaryFile)(f)) {
419
- continue; // Don't track binary files for edit diffs
420
- }
421
- if (await (0, utils_1.pathExists)(f)) {
422
- const code = await fs_1.promises.readFile(f, 'utf-8');
423
- await fs_1.promises.unlink(f);
424
- await editTracker.applyEdit(rawChatSessionId, f, code, true);
425
- }
426
- }
427
- catch { /* ignore */ }
428
- }
497
+ await registerNewFilesWithTracker(pendingNewFiles.get(item.id) ?? [], editTracker, rawChatSessionId);
429
498
  pendingNewFiles.delete(item.id);
430
499
  }
431
500
  }
@@ -637,6 +706,16 @@ class CodexEngine {
637
706
  // Codex CLI loads a single "model instructions" file that acts like a system/developer prompt overlay.
638
707
  // Note: experimental_instructions_file is deprecated and ignored by newer Codex CLI versions.
639
708
  codexInstructionsFile ? `model_instructions_file = ${JSON.stringify(codexInstructionsFile)}` : undefined,
709
+ // Inside a TIER-2 sandbox the container is the boundary, so Codex's own sandbox is not
710
+ // just redundant — it cannot start. It shells out to bubblewrap, which needs to create
711
+ // a user namespace and scratch space under /tmp; a container drops those capabilities by
712
+ // design (`--cap-drop ALL`, `no-new-privileges`), so every shell command and file
713
+ // write failed while still reporting exit 0. One boundary, not two — but the tier-0
714
+ // process driver is not a boundary at all, so it never earns this. Keyed on the tier
715
+ // the driver reports, not on merely being inside a sandbox.
716
+ ...((0, types_1.isContainerSandbox)()
717
+ ? ['sandbox_mode = "danger-full-access"']
718
+ : []),
640
719
  ];
641
720
  // Add custom provider section for LiteLLM or custom providers (e.g., OpenRouter)
642
721
  if (customBaseUrl) {
@@ -655,7 +734,12 @@ class CodexEngine {
655
734
  `"x-litellm-metadata-agent-version" = "${AGENTIDE_CODEX_AGENT_VERSION_ENV_VAR}", ` +
656
735
  `"x-litellm-metadata-source" = "${AGENTIDE_CODEX_SOURCE_ENV_VAR}", ` +
657
736
  `"x-litellm-metadata-engine" = "${AGENTIDE_CODEX_ENGINE_ENV_VAR}", ` +
658
- `"x-litellm-metadata-request-id" = "${AGENTIDE_CODEX_REQUEST_ID_ENV_VAR}" }`);
737
+ `"x-litellm-metadata-request-id" = "${AGENTIDE_CODEX_REQUEST_ID_ENV_VAR}", ` +
738
+ `"x-litellm-metadata-policy-version" = "${AGENTIDE_CODEX_POLICY_VERSION_ENV_VAR}", ` +
739
+ `"x-litellm-metadata-pack-digest" = "${AGENTIDE_CODEX_PACK_DIGEST_ENV_VAR}", ` +
740
+ `"x-litellm-metadata-engine-version" = "${AGENTIDE_CODEX_ENGINE_VERSION_ENV_VAR}", ` +
741
+ `"x-litellm-metadata-tier" = "${AGENTIDE_CODEX_TIER_ENV_VAR}", ` +
742
+ `"x-litellm-metadata-instance-id" = "${AGENTIDE_CODEX_INSTANCE_ID_ENV_VAR}" }`);
659
743
  }
660
744
  }
661
745
  // Add MCP servers section (pack-aware)
@@ -47,6 +47,7 @@ const fs_1 = require("fs");
47
47
  const path = __importStar(require("path"));
48
48
  const url_1 = require("url");
49
49
  const base_1 = require("./base");
50
+ const runtime_1 = require("../cli/runtime");
50
51
  const customRegistry_1 = require("./customRegistry");
51
52
  const paths_1 = require("../agentpack/paths");
52
53
  const utils_1 = require("../utils");
@@ -534,6 +535,8 @@ class CustomEngine {
534
535
  AGENTHIPPO_MODEL_ID: modelId,
535
536
  AGENTHIPPO_USE_LITELLM: routing.useLiteLLM ? 'true' : 'false',
536
537
  };
538
+ delete env[runtime_1.OPENCLAW_GATEWAY_TOKEN_ENV];
539
+ delete env['AGENTHIPPO_DATA_PLANE_TOKEN'];
537
540
  prependNodePath(env, manifest.manifestDir);
538
541
  if (routingApiKey) {
539
542
  env.AGENTHIPPO_API_KEY = routingApiKey;
@@ -560,6 +563,11 @@ class CustomEngine {
560
563
  source: 'agenthippo',
561
564
  engine: `custom:${manifest.id}`,
562
565
  requestId: options.requestId,
566
+ policyVersion: options.policyVersion,
567
+ packDigest: options.packDigest,
568
+ instanceId: options.instanceId,
569
+ engineVersion: options.engineVersion,
570
+ tier: options.tier,
563
571
  });
564
572
  if (litellmMetadataHeaders.length > 0) {
565
573
  env.AGENTHIPPO_LITELLM_METADATA_HEADERS = litellmMetadataHeaders.join('\n');