@agenthippo-ai/agenthippo 0.1.1 → 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 (158) hide show
  1. package/out/agentpack/index.js +2 -1
  2. package/out/agentpack/paths.js +23 -0
  3. package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
  4. package/out/analytics/dashboard/provider.js +13 -2
  5. package/out/analytics/dashboard/webview/panel.js +293 -82
  6. package/out/analytics/identity.js +0 -0
  7. package/out/analytics/service.js +23 -25
  8. package/out/analytics/sessionEvents.js +109 -22
  9. package/out/auth/authBaseUrl.js +59 -0
  10. package/out/auth/bootstrapApply.js +9 -10
  11. package/out/auth/brokerClient.js +49 -15
  12. package/out/auth/onboardingPrompt.js +112 -0
  13. package/out/auth/provider.js +9 -22
  14. package/out/auth/sessionStore.js +3 -40
  15. package/out/auth/signInFlow.js +2 -1
  16. package/out/auth-broker/config.js +54 -4
  17. package/out/auth-broker/server.js +152 -20
  18. package/out/auth-broker/store.js +25 -24
  19. package/out/authority/warrant.js +206 -0
  20. package/out/branding.js +27 -1
  21. package/out/cli/commands/ask.js +3 -0
  22. package/out/cli/commands/chat.js +3 -0
  23. package/out/cli/commands/conformance.js +249 -0
  24. package/out/cli/commands/data-plane.js +67 -0
  25. package/out/cli/commands/deploy.js +50 -10
  26. package/out/cli/commands/gateway.js +58 -0
  27. package/out/cli/commands/idp.js +85 -0
  28. package/out/cli/commands/pack.js +182 -3
  29. package/out/cli/commands/sandbox.js +332 -0
  30. package/out/cli/commands/serve.js +1165 -82
  31. package/out/cli/commands/store.js +12 -4
  32. package/out/cli/credential-context.js +42 -80
  33. package/out/cli/data-plane/broker.js +78 -0
  34. package/out/cli/data-plane/connector-host.js +121 -0
  35. package/out/cli/data-plane/egress.js +189 -0
  36. package/out/cli/data-plane/gateway.js +218 -0
  37. package/out/cli/data-plane/index.js +152 -0
  38. package/out/cli/data-plane/registry.js +89 -0
  39. package/out/cli/data-plane/remote.js +106 -0
  40. package/out/cli/data-plane/sidecar.js +161 -0
  41. package/out/cli/deploy/blueprints/registry.js +84 -0
  42. package/out/cli/deploy/layers/identity.js +172 -0
  43. package/out/cli/deploy-init.js +1182 -347
  44. package/out/cli/deps.js +106 -2
  45. package/out/cli/idp-init.js +338 -0
  46. package/out/cli/index.js +192 -23
  47. package/out/cli/pack-install-ref.js +15 -0
  48. package/out/cli/pack-loader.js +45 -3
  49. package/out/cli/pack-runtime.js +61 -0
  50. package/out/cli/runtime.js +44 -3
  51. package/out/cli/serve-hardening.js +139 -3
  52. package/out/cli/state-sync.js +16 -15
  53. package/out/cli/utils.js +12 -1
  54. package/out/engines/base.js +32 -3
  55. package/out/engines/claude.js +47 -8
  56. package/out/engines/codex.js +125 -22
  57. package/out/engines/custom.js +54 -12
  58. package/out/evidence/sessionShip.js +610 -0
  59. package/out/extension.js +13 -2
  60. package/out/gateway/bridge.js +40 -2
  61. package/out/gateway/client.js +34 -1
  62. package/out/gateway/installSource.js +68 -0
  63. package/out/gateway/lifecycle.js +185 -51
  64. package/out/gateway/outboundPaths.js +4 -1
  65. package/out/gateway/outboundSend.js +6 -6
  66. package/out/gateway/secretsPreflight.js +90 -0
  67. package/out/gateway/service.js +7 -2
  68. package/out/headless/index.js +40 -9
  69. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  70. package/out/live-probe/connector.mjs +8 -0
  71. package/out/live-probe/env-report.mjs +8 -0
  72. package/out/mcp.js +115 -67
  73. package/out/pack/caps.js +271 -0
  74. package/out/pack/governor.js +309 -0
  75. package/out/pack/lease-schema.js +270 -0
  76. package/out/pack/lease.js +640 -0
  77. package/out/pack/reconcile.js +217 -0
  78. package/out/pack/signature.js +839 -0
  79. package/out/remote/handleRemoteChat.js +44 -3
  80. package/out/remote/remoteTrust.js +277 -0
  81. package/out/remote/serveClient.js +52 -5
  82. package/out/runner/host.js +131 -0
  83. package/out/runner/protocol.js +108 -0
  84. package/out/runner/remoteEngine.js +130 -0
  85. package/out/runtime/interfaces/edit-tracker.js +2 -0
  86. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  87. package/out/sandbox/containerDriver.js +570 -0
  88. package/out/sandbox/processDriver.js +315 -0
  89. package/out/sandbox/sandboxRunner.js +191 -0
  90. package/out/sandbox/support.js +301 -0
  91. package/out/sandbox/types.js +55 -0
  92. package/out/scripts/agentide-litellm.sh +67 -5
  93. package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
  94. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -118
  95. package/out/scripts/analytics/setup-analytics.ps1 +37 -13
  96. package/out/scripts/analytics/setup-analytics.sh +93 -13
  97. package/out/scripts/analytics/start-analytics.sh +86 -14
  98. package/out/scripts/gateway/setup-gateway.sh +72 -32
  99. package/out/shared/models.js +12 -111
  100. package/out/shared/providers.js +2 -2
  101. package/out/spotlight/install.js +7 -1
  102. package/out/store/index.js +41 -10
  103. package/out/sync.js +3 -1
  104. package/out/test/analytics.test.js +198 -34
  105. package/out/test/auth-baseurl.test.js +132 -0
  106. package/out/test/auth-bootstrapApply.test.js +27 -6
  107. package/out/test/auth-broker-hardening.test.js +227 -0
  108. package/out/test/auth-broker.test.js +112 -9
  109. package/out/test/blueprint-registry.test.js +105 -0
  110. package/out/test/branding.test.js +13 -0
  111. package/out/test/caps-governor.test.js +455 -0
  112. package/out/test/codex-new-file-survival.test.js +119 -0
  113. package/out/test/configuration-scopes.test.js +131 -0
  114. package/out/test/credential-context.test.js +23 -28
  115. package/out/test/custom-env-layering.test.js +151 -0
  116. package/out/test/data-plane-engine-wiring.test.js +250 -0
  117. package/out/test/data-plane.test.js +311 -0
  118. package/out/test/deploy-init.test.js +295 -4
  119. package/out/test/egress-broker.test.js +172 -0
  120. package/out/test/fleetDisplayUtils.test.js +4 -0
  121. package/out/test/gateway-client.test.js +28 -0
  122. package/out/test/gateway-install-source.test.js +111 -0
  123. package/out/test/gateway-lifecycle.test.js +1 -1
  124. package/out/test/gateway-outbound.test.js +7 -0
  125. package/out/test/gateway-secrets-preflight.test.js +127 -0
  126. package/out/test/gateway-session-routing.test.js +18 -0
  127. package/out/test/governor-hardening.test.js +217 -0
  128. package/out/test/headless-sync.test.js +22 -0
  129. package/out/test/idp-init.test.js +151 -0
  130. package/out/test/lease-signature.test.js +357 -0
  131. package/out/test/lease-trust-root.test.js +273 -0
  132. package/out/test/lease-validation.test.js +169 -0
  133. package/out/test/lease.test.js +114 -0
  134. package/out/test/mcp-obo.test.js +52 -27
  135. package/out/test/mcp.test.js +69 -3
  136. package/out/test/model-routing.test.js +133 -0
  137. package/out/test/pack-admission.test.js +216 -0
  138. package/out/test/pack-digest-parity.test.js +140 -0
  139. package/out/test/pack-reconcile.test.js +183 -0
  140. package/out/test/pack-signature-artifact.test.js +103 -0
  141. package/out/test/pack-signature.test.js +176 -0
  142. package/out/test/pack-trust-root.test.js +202 -0
  143. package/out/test/remote-trust.test.js +242 -0
  144. package/out/test/runner-protocol.test.js +249 -0
  145. package/out/test/sandbox-driver.test.js +553 -0
  146. package/out/test/sandbox-hardening.test.js +395 -0
  147. package/out/test/sandbox-runner.test.js +343 -0
  148. package/out/test/serve-gateway-turn.test.js +267 -0
  149. package/out/test/serve-hardening.test.js +125 -6
  150. package/out/test/serveClient.test.js +38 -5
  151. package/out/test/sessionShip.test.js +560 -0
  152. package/out/test/spotlightPublish.test.js +28 -0
  153. package/out/test/state-sync-peer.test.js +18 -5
  154. package/out/test/store-install-safe.test.js +69 -0
  155. package/out/test/tls-bypass.test.js +90 -0
  156. package/out/test/warrant.test.js +138 -0
  157. package/out/utils.js +73 -20
  158. package/package.json +1 -1
@@ -34,49 +34,74 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  const assert = __importStar(require("assert"));
37
- const node_fs_1 = require("node:fs");
38
- const node_os_1 = require("node:os");
39
- const node_path_1 = require("node:path");
40
37
  const credential_context_1 = require("../cli/credential-context");
41
38
  const mcp_1 = require("../mcp");
42
- suite('mcp: OBO token in MCP env (Design B §11.1)', () => {
43
- const orig = process.env.DATABRICKS_TOKEN;
44
- let tokenDir;
39
+ /**
40
+ * The per-turn OBO credential must never reach MCP child env implicitly: the supported
41
+ * transport is the loopback data-plane gateway + turn nonce (secure-obo-design §2/§3).
42
+ * Inline resolution exists only behind the explicit local-dev opt-in.
43
+ */
44
+ suite('mcp: OBO token in MCP env (secure-obo-design §3.4 — inline is opt-in only)', () => {
45
+ const ENV_KEYS = ['DATABRICKS_TOKEN', 'AGENTHIPPO_DATA_TOKEN_ENV_VARS', mcp_1.ALLOW_INLINE_DATA_TOKEN_ENV];
46
+ const saved = {};
45
47
  setup(() => {
46
- tokenDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'mcp-obo-'));
48
+ for (const k of ENV_KEYS) {
49
+ saved[k] = process.env[k];
50
+ delete process.env[k];
51
+ }
47
52
  });
48
53
  teardown(() => {
49
- if (orig === undefined) {
50
- delete process.env.DATABRICKS_TOKEN;
51
- }
52
- else {
53
- process.env.DATABRICKS_TOKEN = orig;
54
+ for (const k of ENV_KEYS) {
55
+ if (saved[k] === undefined) {
56
+ delete process.env[k];
57
+ }
58
+ else {
59
+ process.env[k] = saved[k];
60
+ }
54
61
  }
55
- (0, node_fs_1.rmSync)(tokenDir, { recursive: true, force: true });
56
62
  });
57
- test('static PAT when no credential scope', () => {
63
+ test('static PAT resolves from plain env when no credential scope', () => {
58
64
  process.env.DATABRICKS_TOKEN = 'pat-static';
59
65
  assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('DATABRICKS_TOKEN'), 'pat-static');
60
66
  });
61
- test('OBO token overrides static PAT inside runWithCredential (inline fallback)', async () => {
62
- process.env.DATABRICKS_TOKEN = 'pat-static';
67
+ test('WITHOUT the opt-in, a live OBO token never bakes into MCP child env', async () => {
68
+ process.env.AGENTHIPPO_DATA_TOKEN_ENV_VARS = 'DATABRICKS_TOKEN';
63
69
  await (0, credential_context_1.runWithCredential)({ userId: 'u@x.com', dataToken: 'obo-live' }, async () => {
64
- assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('DATABRICKS_TOKEN'), 'obo-live');
65
- });
66
- });
67
- test('credential placeholder resolves to token file for stdio MCP', async () => {
68
- await (0, credential_context_1.runWithCredential)({ userId: 'u@x.com', dataToken: 'obo-file-token' }, async () => {
70
+ // alias reference falls back to ambient env (unset here → empty), never the credential
71
+ assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('DATABRICKS_TOKEN'), '');
72
+ // explicit placeholder resolves to nothing → the connector fails closed
73
+ assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('dataToken'), '');
69
74
  const resolved = (0, mcp_1.resolveMcpServerEnv)({
70
75
  DATABRICKS_HOST: 'dbc.example.com',
71
76
  DATABRICKS_TOKEN: '${credential:dataToken}',
72
77
  });
73
78
  assert.ok(resolved);
74
79
  assert.strictEqual(resolved.DATABRICKS_HOST, 'dbc.example.com');
75
- assert.strictEqual(resolved.DATABRICKS_TOKEN, undefined);
76
- const file = resolved[mcp_1.AGENTHIPPO_DATA_TOKEN_FILE_ENV];
77
- assert.ok(file);
78
- assert.strictEqual((0, node_fs_1.readFileSync)(file, 'utf8'), 'obo-file-token');
79
- assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('DATABRICKS_TOKEN'), '');
80
- }, { dataTokenDir: tokenDir });
80
+ assert.strictEqual(resolved.DATABRICKS_TOKEN, undefined, 'credential must not appear in spawn env without the opt-in');
81
+ assert.ok(!JSON.stringify(resolved).includes('obo-live'), 'the raw token must not leak anywhere in the resolved env');
82
+ });
83
+ });
84
+ test('WITH the explicit local-dev opt-in, inline resolution works (alias + placeholder)', async () => {
85
+ process.env[mcp_1.ALLOW_INLINE_DATA_TOKEN_ENV] = '1';
86
+ process.env.AGENTHIPPO_DATA_TOKEN_ENV_VARS = 'DATABRICKS_TOKEN';
87
+ process.env.DATABRICKS_TOKEN = 'pat-static';
88
+ await (0, credential_context_1.runWithCredential)({ userId: 'u@x.com', dataToken: 'obo-live' }, async () => {
89
+ assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('DATABRICKS_TOKEN'), 'obo-live', 'OBO token overrides static PAT when opted in');
90
+ const resolved = (0, mcp_1.resolveMcpServerEnv)({ DATABRICKS_TOKEN: '${credential:dataToken}' });
91
+ assert.strictEqual(resolved.DATABRICKS_TOKEN, 'obo-live');
92
+ });
93
+ });
94
+ test('alias list is empty by default — DATABRICKS_TOKEN is no longer an implicit credential alias', async () => {
95
+ process.env[mcp_1.ALLOW_INLINE_DATA_TOKEN_ENV] = '1';
96
+ process.env.DATABRICKS_TOKEN = 'pat-static';
97
+ await (0, credential_context_1.runWithCredential)({ userId: 'u@x.com', dataToken: 'obo-live' }, async () => {
98
+ // No AGENTHIPPO_DATA_TOKEN_ENV_VARS declared → the plain env var wins even opted-in.
99
+ assert.strictEqual((0, mcp_1.resolveMcpEnvVar)('DATABRICKS_TOKEN'), 'pat-static');
100
+ });
101
+ });
102
+ test('no credential scope + placeholder → falls back to ambient env for local dev', () => {
103
+ process.env.DATABRICKS_TOKEN = 'pat-static';
104
+ const resolved = (0, mcp_1.resolveMcpServerEnv)({ DATABRICKS_TOKEN: '${credential:dataToken}' });
105
+ assert.strictEqual(resolved.DATABRICKS_TOKEN, 'pat-static');
81
106
  });
82
107
  });
@@ -43,6 +43,7 @@ const os = __importStar(require("os"));
43
43
  const fs_1 = require("fs");
44
44
  const mcp_1 = require("../mcp");
45
45
  const branding_1 = require("../branding");
46
+ const credential_context_1 = require("../cli/credential-context");
46
47
  suite('MCP Server Loading', () => {
47
48
  let testDir;
48
49
  const getAgentMcpDir = (agentId) => path.join(testDir, branding_1.AGENTIDE_DIR_NAME, 'agents', agentId, 'mcp');
@@ -245,16 +246,19 @@ suite('MCP Server Loading', () => {
245
246
  // A bare (unquoted) dotted key would be misparsed as a nested table.
246
247
  assert.ok(!result.includes('my.token = '));
247
248
  });
248
- test('autoApprove emits default_tools_approval_mode = "auto" per server', () => {
249
+ test('autoApprove emits default_tools_approval_mode = "approve" per server', () => {
249
250
  const servers = [
250
251
  { name: 'stdio-srv', label: 's', command: 'node', args: ['s.js'] },
251
252
  { name: 'http-srv', label: 'h', command: '', args: [], url: 'https://mcp.example.com/sse' },
252
253
  ];
253
254
  const result = (0, mcp_1.generateCodexMcpConfig)(servers, { autoApprove: true });
254
- assert.ok(result.includes('default_tools_approval_mode = "auto"'));
255
+ assert.ok(result.includes('default_tools_approval_mode = "approve"'));
255
256
  assert.ok(result.includes('url = "https://mcp.example.com/sse"'));
256
- // Invalid legacy key/value must never be emitted.
257
+ // Invalid legacy key/value must never be emitted. "auto" is not a valid enum value
258
+ // either — Codex ≥0.131 ignores it, falls back to `prompt`, and headless exec then
259
+ // cancels every MCP tool call ("user cancelled MCP tool call").
257
260
  assert.ok(!result.includes('approval_mode = "never"'));
261
+ assert.ok(!result.includes('"auto"'));
258
262
  });
259
263
  test('generateCodexMcpConfig emits http_headers for URL servers', () => {
260
264
  const servers = [
@@ -271,6 +275,27 @@ suite('MCP Server Loading', () => {
271
275
  assert.ok(result.includes('url = "https://mcp.example.com/sse"'));
272
276
  assert.ok(result.includes('http_headers = { "Authorization" = "Bearer abc", "X-Custom" = "val" }'));
273
277
  });
278
+ test('bearerTokenEnv emits bearer_token_env_var and NEVER a per-turn value — config.toml stays turn-invariant (secure-obo-design §3.2)', async () => {
279
+ const servers = [
280
+ {
281
+ name: 'data',
282
+ label: 'data',
283
+ command: '',
284
+ args: [],
285
+ url: 'http://127.0.0.1:39999/mcp',
286
+ bearerTokenEnv: 'AGENTHIPPO_TURN_NONCE',
287
+ },
288
+ ];
289
+ const outsideScope = (0, mcp_1.generateCodexMcpConfig)(servers);
290
+ assert.ok(outsideScope.includes('bearer_token_env_var = "AGENTHIPPO_TURN_NONCE"'));
291
+ assert.ok(!outsideScope.includes('http_headers'), 'no resolved header may carry the nonce into the file');
292
+ // Byte-identical no matter which user's credential scope is ambient — this is the
293
+ // exact property that removes the cross-user config.toml race (defect 1).
294
+ const inAlice = await (0, credential_context_1.runWithCredential)({ userId: 'alice@corp.com', dataToken: 'tok-a' }, async () => (0, mcp_1.generateCodexMcpConfig)(servers));
295
+ const inBob = await (0, credential_context_1.runWithCredential)({ userId: 'bob@corp.com', dataToken: 'tok-b' }, async () => (0, mcp_1.generateCodexMcpConfig)(servers));
296
+ assert.strictEqual(inAlice, outsideScope);
297
+ assert.strictEqual(inBob, outsideScope);
298
+ });
274
299
  });
275
300
  suite('inline MCP helpers', () => {
276
301
  test('formatInlineMcpServers maps resolved servers by name', () => {
@@ -306,6 +331,47 @@ suite('MCP Server Loading', () => {
306
331
  remote: { type: 'http', url: 'https://mcp.example.com/sse', headers: { Authorization: 'Bearer abc' } },
307
332
  });
308
333
  });
334
+ test('bearerTokenEnv resolves against the PER-TURN env into an Authorization header (cursor/custom path)', () => {
335
+ const server = {
336
+ name: 'data', label: 'data', command: '', args: [],
337
+ url: 'http://127.0.0.1:39999/mcp', bearerTokenEnv: 'AGENTHIPPO_TURN_NONCE',
338
+ };
339
+ const inline = (0, mcp_1.formatInlineMcpServers)([server], { AGENTHIPPO_TURN_NONCE: 'nonce-123' });
340
+ assert.deepStrictEqual(inline, {
341
+ data: { type: 'http', url: 'http://127.0.0.1:39999/mcp', headers: { Authorization: 'Bearer nonce-123' } },
342
+ });
343
+ // No nonce in the turn env (e.g. IDE) → no Authorization header, entry still valid.
344
+ const withoutNonce = (0, mcp_1.formatInlineMcpServers)([server], {});
345
+ assert.deepStrictEqual(withoutNonce, {
346
+ data: { type: 'http', url: 'http://127.0.0.1:39999/mcp' },
347
+ });
348
+ });
349
+ /**
350
+ * The claude path must ship a `${NAME}` REFERENCE, never the resolved secret: the Agent
351
+ * SDK serializes mcpServers into `--mcp-config <json>` argv, and /proc/<pid>/cmdline is
352
+ * world-readable — a resolved bearer there leaks the turn nonce to every uid on the host.
353
+ * The CLI expands the reference from its own env (verified live on 2.1.220).
354
+ */
355
+ test('referenceHttpMcpHeaders emits a ${NAME} reference, never the secret (claude path)', () => {
356
+ const headers = (0, mcp_1.referenceHttpMcpHeaders)({ bearerTokenEnv: 'AGENTHIPPO_TURN_NONCE', headers: { 'X-Static': 'kept' } }, { AGENTHIPPO_TURN_NONCE: 'nonce-123' });
357
+ assert.deepStrictEqual(headers, {
358
+ 'X-Static': 'kept',
359
+ 'Authorization': 'Bearer ${AGENTHIPPO_TURN_NONCE}',
360
+ });
361
+ // The reference is emitted even when the env has no value yet: expansion happens in
362
+ // the CLI at connect time, and an unset name passes through literally, which the data
363
+ // gateway rejects — fail closed, never a silently-authorized request.
364
+ const unresolved = (0, mcp_1.referenceHttpMcpHeaders)({ bearerTokenEnv: 'AGENTHIPPO_TURN_NONCE' }, {});
365
+ assert.deepStrictEqual(unresolved, { Authorization: 'Bearer ${AGENTHIPPO_TURN_NONCE}' });
366
+ });
367
+ test('referenceHttpMcpHeaders falls back to in-memory resolution for a non-expandable name', () => {
368
+ // A name outside the CLI's `${NAME}` grammar would be sent verbatim as garbage; the
369
+ // fallback resolves it here instead, accepting environ-level exposure over a broken header.
370
+ const headers = (0, mcp_1.referenceHttpMcpHeaders)({ bearerTokenEnv: 'weird-name.v2' }, { 'weird-name.v2': 'tok-1' });
371
+ assert.deepStrictEqual(headers, { Authorization: 'Bearer tok-1' });
372
+ // No bearerTokenEnv and no static headers → no headers object at all.
373
+ assert.strictEqual((0, mcp_1.referenceHttpMcpHeaders)({}, {}), undefined);
374
+ });
309
375
  test('serialize and parse round-trip inline MCP', () => {
310
376
  const inline = (0, mcp_1.formatInlineMcpServers)([
311
377
  { name: 'echo', label: 'echo', command: 'echo', args: ['hi'] },
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Model routing / credential selection tests.
4
+ *
5
+ * Guards the fix that replaced the provider-blind precedence
6
+ * (apiKey = litellm || anthropic || openai) with destination-based selection: the credential
7
+ * is chosen by the endpoint the engine dials, so a codex turn never authenticates to OpenAI
8
+ * with an Anthropic key (the Databricks 401), and a claude turn never uses the OpenAI key.
9
+ *--------------------------------------------------------------------------------------------*/
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ require("mocha");
45
+ const assert = __importStar(require("assert"));
46
+ const utils_1 = require("../utils");
47
+ const pack_loader_1 = require("../cli/pack-loader");
48
+ const OPENAI = 'sk-openai-xxx';
49
+ const ANTHROPIC = 'sk-ant-yyy';
50
+ const LITELLM = 'sk-litellm-zzz';
51
+ function opts(over = {}) {
52
+ return { workspaceRoot: '/ws', ...over };
53
+ }
54
+ suite('resolveApiRouting: direct path selects the ENGINE\'s provider key (no collapse)', () => {
55
+ // The exact Databricks scenario: both direct keys present, no proxy configured.
56
+ const bothKeys = { openaiApiKey: OPENAI, anthropicApiKey: ANTHROPIC };
57
+ test('codex (openai) picks the OpenAI key even when an Anthropic key is also present', () => {
58
+ const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false }), bothKeys, 'openai');
59
+ assert.strictEqual(r.apiKey, OPENAI);
60
+ assert.strictEqual(r.useLiteLLM, false);
61
+ });
62
+ test('claude (anthropic) picks the Anthropic key even when an OpenAI key is also present', () => {
63
+ const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false }), bothKeys, 'anthropic');
64
+ assert.strictEqual(r.apiKey, ANTHROPIC);
65
+ });
66
+ test('the codex→OpenAI-with-Anthropic-key 401 is impossible now', () => {
67
+ const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false }), bothKeys, 'openai');
68
+ assert.notStrictEqual(r.apiKey, ANTHROPIC, 'codex must never receive the Anthropic key');
69
+ });
70
+ test('missing provider key → empty (fail visibly), never the other provider\'s key', () => {
71
+ const onlyAnthropic = { anthropicApiKey: ANTHROPIC };
72
+ const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false }), onlyAnthropic, 'openai');
73
+ assert.strictEqual(r.apiKey, '', 'codex with only an Anthropic key gets no key, not the wrong one');
74
+ });
75
+ });
76
+ suite('resolveApiRouting: LiteLLM path uses the proxy key for every engine', () => {
77
+ const creds = { litellmApiKey: LITELLM, litellmBaseUrl: 'http://proxy:4000', openaiApiKey: OPENAI, anthropicApiKey: ANTHROPIC };
78
+ test('codex through the proxy uses the LiteLLM key + base URL', () => {
79
+ const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: true }), creds, 'openai');
80
+ assert.strictEqual(r.apiKey, LITELLM);
81
+ assert.strictEqual(r.baseUrl, 'http://proxy:4000');
82
+ assert.strictEqual(r.useLiteLLM, true);
83
+ });
84
+ test('claude through the proxy also uses the LiteLLM key (one key routes all)', () => {
85
+ const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: true }), creds, 'anthropic');
86
+ assert.strictEqual(r.apiKey, LITELLM);
87
+ });
88
+ });
89
+ suite('resolveApiRouting: explicit options.apiKey (IDE single key) still wins', () => {
90
+ test('IDE explicit key overrides the provider fallback on the direct path', () => {
91
+ const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false, apiKey: 'sk-ide-explicit' }), { openaiApiKey: OPENAI }, 'openai');
92
+ assert.strictEqual(r.apiKey, 'sk-ide-explicit');
93
+ });
94
+ test('IDE with only an explicit key and no creds still works (back-compat)', () => {
95
+ const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: false, apiKey: 'sk-ide-explicit' }), {}, 'anthropic');
96
+ assert.strictEqual(r.apiKey, 'sk-ide-explicit');
97
+ });
98
+ test('proxy path keeps the sk-dummy fallback when nothing is configured', () => {
99
+ const r = (0, utils_1.resolveApiRouting)(opts({ useLiteLLM: true }), {}, 'openai');
100
+ assert.strictEqual(r.apiKey, 'sk-dummy');
101
+ });
102
+ });
103
+ suite('inferModelProvider: custom engines pick provider from the model id', () => {
104
+ test('claude/anthropic model ids → anthropic', () => {
105
+ assert.strictEqual((0, utils_1.inferModelProvider)('claude-sonnet-4-5'), 'anthropic');
106
+ assert.strictEqual((0, utils_1.inferModelProvider)('litellm/claude-haiku-4-5'), 'anthropic');
107
+ assert.strictEqual((0, utils_1.inferModelProvider)('anthropic/claude-opus'), 'anthropic');
108
+ });
109
+ test('everything else → openai (the safe default for gpt/codex/unknown)', () => {
110
+ assert.strictEqual((0, utils_1.inferModelProvider)('gpt-5.5'), 'openai');
111
+ assert.strictEqual((0, utils_1.inferModelProvider)('litellm/gpt-5.4-nano'), 'openai');
112
+ assert.strictEqual((0, utils_1.inferModelProvider)(undefined), 'openai');
113
+ });
114
+ });
115
+ suite('hasCredentialForEngine: engine-aware boot check', () => {
116
+ const base = { apiKey: undefined, baseUrl: undefined, useLiteLLM: false, litellmApiKey: undefined, openaiApiKey: undefined, anthropicApiKey: undefined };
117
+ test('codex needs an OpenAI key; an Anthropic-only env fails the check', () => {
118
+ assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('codex', { ...base, anthropicApiKey: ANTHROPIC }), false);
119
+ assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('codex', { ...base, openaiApiKey: OPENAI }), true);
120
+ });
121
+ test('claude needs an Anthropic key; an OpenAI-only env fails the check', () => {
122
+ assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('claude', { ...base, openaiApiKey: OPENAI }), false);
123
+ assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('claude', { ...base, anthropicApiKey: ANTHROPIC }), true);
124
+ });
125
+ test('a configured LiteLLM proxy satisfies every engine', () => {
126
+ const proxy = { ...base, useLiteLLM: true, litellmApiKey: LITELLM, baseUrl: 'http://proxy:4000' };
127
+ assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('codex', proxy), true);
128
+ assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('claude', proxy), true);
129
+ });
130
+ test('custom engines are not gated here (they authenticate via their own manifest env var)', () => {
131
+ assert.strictEqual((0, pack_loader_1.hasCredentialForEngine)('openclaw', base), true);
132
+ });
133
+ });
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Pack admission: fail closed by default, re-verified per turn, and say WHY.
4
+ *
5
+ * Three gaps, one suite:
6
+ * - the default was warn-and-run, so "verify before run, fail closed" was not true of the
7
+ * shipped shape;
8
+ * - the verdict was computed once at boot and cached for the life of the process, while the
9
+ * default deployment runs the engine in-process against a `fileAccess: full` pack directory;
10
+ * - each distinct failure needs its own message, because each has a different remedy.
11
+ *--------------------------------------------------------------------------------------------*/
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ require("mocha");
47
+ const assert = __importStar(require("assert"));
48
+ const fs = __importStar(require("fs"));
49
+ const os = __importStar(require("os"));
50
+ const path = __importStar(require("path"));
51
+ const warrant_1 = require("../authority/warrant");
52
+ const signature_1 = require("../pack/signature");
53
+ const key = (0, warrant_1.generateWarrantKeypair)('k1');
54
+ const keys = { k1: key.publicKeyPem };
55
+ function makePack(files = { 'agent.yaml': 'name: demo\n' }) {
56
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pack-admission-'));
57
+ for (const [rel, body] of Object.entries(files)) {
58
+ const abs = path.join(dir, rel);
59
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
60
+ fs.writeFileSync(abs, body);
61
+ }
62
+ return dir;
63
+ }
64
+ const sign = (dir) => (0, signature_1.signPack)({ packDir: dir, pack: 'demo', packVersion: '1.0.0', privateKeyPem: key.privateKeyPem, kid: 'k1' });
65
+ suite('signing is required by DEFAULT', () => {
66
+ test('nothing configured ⇒ a signature is required', () => {
67
+ assert.strictEqual((0, signature_1.resolveRequireSignedPack)({}), true);
68
+ });
69
+ test('the escape hatch relaxes only this instance', () => {
70
+ assert.strictEqual((0, signature_1.resolveRequireSignedPack)({ localAllowUnsigned: true }), false);
71
+ });
72
+ test('a lease demand OVERRIDES the escape hatch — tighten-only survives the flip', () => {
73
+ assert.strictEqual((0, signature_1.resolveRequireSignedPack)({ lease: true, localAllowUnsigned: true }), true, 'an instance must never be able to waive what the control plane demanded');
74
+ });
75
+ test('--require-signed-pack still means something, and still cannot be waived', () => {
76
+ assert.strictEqual((0, signature_1.resolveRequireSignedPack)({ localRequire: true, localAllowUnsigned: true }), true);
77
+ });
78
+ test('the underlying tighten-only OR is unchanged', () => {
79
+ assert.strictEqual((0, signature_1.requireSignedPack)(undefined, undefined), false);
80
+ assert.strictEqual((0, signature_1.requireSignedPack)(true, undefined), true);
81
+ assert.strictEqual((0, signature_1.requireSignedPack)(undefined, true), true);
82
+ assert.strictEqual((0, signature_1.requireSignedPack)(true, true), true);
83
+ });
84
+ test('the escape hatch env var is explicit, not merely present', () => {
85
+ assert.strictEqual((0, signature_1.allowUnsignedPackFromEnv)({ [signature_1.ALLOW_UNSIGNED_PACK_ENV]: '1' }), true);
86
+ assert.strictEqual((0, signature_1.allowUnsignedPackFromEnv)({ [signature_1.ALLOW_UNSIGNED_PACK_ENV]: 'true' }), true);
87
+ assert.strictEqual((0, signature_1.allowUnsignedPackFromEnv)({ [signature_1.ALLOW_UNSIGNED_PACK_ENV]: '0' }), false);
88
+ assert.strictEqual((0, signature_1.allowUnsignedPackFromEnv)({}), false);
89
+ });
90
+ });
91
+ suite('each refusal names its own remedy', () => {
92
+ const cases = [
93
+ ['unsigned', /pack sign/],
94
+ ['no-trust-root', /AGENTHIPPO_PACK_JWKS_URL/],
95
+ ['trust-root-unavailable', /could not be loaded/],
96
+ ['unknown-key', /does not trust/],
97
+ ['digest-mismatch', /changed after signing/],
98
+ ['identity-mismatch', /different pack or version/],
99
+ ];
100
+ for (const [kind, expected] of cases) {
101
+ test(`${kind} produces its own actionable message`, () => {
102
+ const msg = (0, signature_1.describePackRefusal)({ kind, detail: 'detail here', pack: 'demo', packVersion: '1.0.0', packDir: '/p' });
103
+ assert.match(msg, /refusing to run demo@1\.0\.0/);
104
+ assert.match(msg, expected);
105
+ assert.match(msg, /AGENTHIPPO_ALLOW_UNSIGNED_PACK=1/, 'every refusal must state how to get moving again');
106
+ assert.match(msg, /cannot be waived locally/, 'and that the lease still wins');
107
+ });
108
+ }
109
+ test('"no trust root" and "unsigned" are different messages with different fixes', () => {
110
+ const unsigned = (0, signature_1.describePackRefusal)({ kind: 'unsigned', detail: 'x', pack: 'demo' });
111
+ const noRoot = (0, signature_1.describePackRefusal)({ kind: 'no-trust-root', detail: 'x', pack: 'demo' });
112
+ assert.notStrictEqual(unsigned, noRoot);
113
+ assert.ok(!/NOT a problem with the pack/.test(unsigned));
114
+ assert.match(noRoot, /NOT a problem with the pack/);
115
+ });
116
+ });
117
+ suite('the verifier refuses each distinct failure with its kind', () => {
118
+ test('unsigned', () => {
119
+ const v = (0, signature_1.createPackVerifier)();
120
+ const res = v.verify({ packDir: makePack(), pack: 'demo', packVersion: '1.0.0', keys });
121
+ assert.strictEqual(res.ok, false);
122
+ assert.strictEqual(res.kind, 'unsigned');
123
+ });
124
+ test('no trust root — distinguishable from unsigned', () => {
125
+ const dir = makePack();
126
+ sign(dir);
127
+ const res = (0, signature_1.createPackVerifier)().verify({ packDir: dir, pack: 'demo', packVersion: '1.0.0', keys: {} });
128
+ assert.strictEqual(res.ok, false);
129
+ assert.strictEqual(res.kind, 'no-trust-root');
130
+ });
131
+ test('unknown signing key', () => {
132
+ const dir = makePack();
133
+ sign(dir);
134
+ const stranger = (0, warrant_1.generateWarrantKeypair)('other');
135
+ const res = (0, signature_1.createPackVerifier)().verify({ packDir: dir, pack: 'demo', packVersion: '1.0.0', keys: { other: stranger.publicKeyPem } });
136
+ assert.strictEqual(res.ok, false);
137
+ assert.strictEqual(res.kind, 'unknown-key');
138
+ });
139
+ test('digest mismatch', () => {
140
+ const dir = makePack();
141
+ sign(dir);
142
+ fs.writeFileSync(path.join(dir, 'agent.yaml'), 'name: demo\nevil: true\n');
143
+ const res = (0, signature_1.createPackVerifier)().verify({ packDir: dir, pack: 'demo', packVersion: '1.0.0', keys });
144
+ assert.strictEqual(res.ok, false);
145
+ assert.strictEqual(res.kind, 'digest-mismatch');
146
+ });
147
+ test('identity mismatch — a valid signature for a different pack', () => {
148
+ const dir = makePack();
149
+ sign(dir);
150
+ const res = (0, signature_1.createPackVerifier)().verify({ packDir: dir, pack: 'payments', packVersion: '1.0.0', keys });
151
+ assert.strictEqual(res.ok, false);
152
+ assert.strictEqual(res.kind, 'identity-mismatch');
153
+ });
154
+ });
155
+ suite('re-verification on the turn path', () => {
156
+ /**
157
+ * The exact scenario: no --sandbox, so the engine runs in-process with `fileAccess: full`. The
158
+ * agent edits its own pack directory after boot. Under the old boot-once cache every subsequent
159
+ * turn passed on a stale `true`.
160
+ */
161
+ test('a pack edited AFTER the first verification is caught on the next turn', () => {
162
+ const dir = makePack();
163
+ sign(dir);
164
+ const v = (0, signature_1.createPackVerifier)({ probeIntervalMs: 0 });
165
+ const input = { packDir: dir, pack: 'demo', packVersion: '1.0.0', keys };
166
+ assert.ok(v.verify(input, 1_000).ok, 'clean at boot');
167
+ fs.writeFileSync(path.join(dir, '.claude', 'settings.json').replace('.claude/', ''), 'x');
168
+ fs.mkdirSync(path.join(dir, '.claude'), { recursive: true });
169
+ fs.writeFileSync(path.join(dir, '.claude', 'settings.json'), '{"hooks":{"PreToolUse":[{"command":"sh"}]}}');
170
+ const after = v.verify(input, 2_000);
171
+ assert.strictEqual(after.ok, false, 'the injected hook must be caught on a later turn');
172
+ assert.strictEqual(after.kind, 'digest-mismatch');
173
+ });
174
+ test('an unchanged pack is NOT re-hashed on every turn — the probe is what makes this affordable', () => {
175
+ const dir = makePack();
176
+ sign(dir);
177
+ const v = (0, signature_1.createPackVerifier)({ probeIntervalMs: 0, fullReverifyIntervalMs: 1_000_000 });
178
+ const input = { packDir: dir, pack: 'demo', packVersion: '1.0.0', keys };
179
+ v.verify(input, 1_000);
180
+ const afterFirst = v.fullVerifications();
181
+ for (let i = 0; i < 50; i++) {
182
+ assert.ok(v.verify(input, 1_000 + i).ok);
183
+ }
184
+ assert.strictEqual(v.fullVerifications(), afterFirst, '50 turns, one digest');
185
+ });
186
+ test('a full re-verify happens on an interval regardless of how clean the probe looks', () => {
187
+ const dir = makePack();
188
+ sign(dir);
189
+ const v = (0, signature_1.createPackVerifier)({ probeIntervalMs: 0, fullReverifyIntervalMs: 5_000 });
190
+ const input = { packDir: dir, pack: 'demo', packVersion: '1.0.0', keys };
191
+ v.verify(input, 0);
192
+ const before = v.fullVerifications();
193
+ v.verify(input, 1_000);
194
+ assert.strictEqual(v.fullVerifications(), before, 'within the interval, the probe decides');
195
+ v.verify(input, 6_000);
196
+ assert.strictEqual(v.fullVerifications(), before + 1, 'past it, we hash regardless — bounds the mtime-restoring attacker');
197
+ });
198
+ test('changing the keyring or the version invalidates the cached verdict outright', () => {
199
+ const dir = makePack();
200
+ sign(dir);
201
+ const v = (0, signature_1.createPackVerifier)({ fullReverifyIntervalMs: 1_000_000 });
202
+ assert.ok(v.verify({ packDir: dir, pack: 'demo', packVersion: '1.0.0', keys }, 0).ok);
203
+ const res = v.verify({ packDir: dir, pack: 'demo', packVersion: '2.0.0', keys }, 1);
204
+ assert.strictEqual(res.ok, false, 'a version change must not reuse the previous verdict');
205
+ });
206
+ test('a probe within the probe interval is not even taken', () => {
207
+ const dir = makePack();
208
+ sign(dir);
209
+ const v = (0, signature_1.createPackVerifier)({ probeIntervalMs: 10_000, fullReverifyIntervalMs: 1_000_000 });
210
+ const input = { packDir: dir, pack: 'demo', packVersion: '1.0.0', keys };
211
+ v.verify(input, 0);
212
+ fs.writeFileSync(path.join(dir, 'agent.yaml'), 'tampered');
213
+ assert.ok(v.verify(input, 5_000).ok, 'inside the probe interval the cached verdict stands (documented)');
214
+ assert.strictEqual(v.verify(input, 11_000).ok, false, 'and the very next probe catches it');
215
+ });
216
+ });