@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
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ require("mocha");
40
+ const assert = __importStar(require("assert"));
41
+ const fs = __importStar(require("node:fs"));
42
+ const os = __importStar(require("node:os"));
43
+ const path = __importStar(require("node:path"));
44
+ const reconcile_1 = require("../pack/reconcile");
45
+ suite('store install path containment', () => {
46
+ test('safeJoin refuses traversal paths', () => {
47
+ const root = path.join(os.tmpdir(), 'pack-install-root');
48
+ assert.strictEqual((0, reconcile_1.safeJoin)(root, '../../etc/passwd'), undefined);
49
+ assert.strictEqual((0, reconcile_1.safeJoin)(root, '/etc/passwd'), undefined);
50
+ const ok = (0, reconcile_1.safeJoin)(root, 'agent.yaml');
51
+ assert.ok(ok?.endsWith(`${path.sep}agent.yaml`));
52
+ });
53
+ test('install extract pattern refuses escape before write', async () => {
54
+ const installDir = fs.mkdtempSync(path.join(os.tmpdir(), 'store-install-'));
55
+ const files = [{ path: '../escaped.txt', content: Buffer.from('nope') }];
56
+ let refused = false;
57
+ for (const file of files) {
58
+ const filePath = (0, reconcile_1.safeJoin)(installDir, file.path);
59
+ if (!filePath) {
60
+ refused = true;
61
+ continue;
62
+ }
63
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
64
+ await fs.promises.writeFile(filePath, file.content);
65
+ }
66
+ assert.strictEqual(refused, true);
67
+ assert.equal(fs.existsSync(path.join(path.dirname(installDir), 'escaped.txt')), false);
68
+ });
69
+ });
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *
5
+ * `applyLocalTlsBypass` sets NODE_TLS_REJECT_UNAUTHORIZED=0 in an engine child's environment.
6
+ * It used to decide with `url.includes('localhost')`, so `https://localhost.attacker.tld/v1`
7
+ * disabled certificate verification for that child.
8
+ *--------------------------------------------------------------------------------------------*/
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ require("mocha");
44
+ const assert = __importStar(require("assert"));
45
+ const utils_1 = require("../utils");
46
+ suite('local HTTPS detection', () => {
47
+ test('recognises the genuinely local endpoints', () => {
48
+ assert.ok((0, utils_1.isLocalHttps)('https://localhost:4000/v1'));
49
+ assert.ok((0, utils_1.isLocalHttps)('https://127.0.0.1:4000/v1'));
50
+ assert.ok((0, utils_1.isLocalHttps)('https://0.0.0.0:4000/v1'));
51
+ assert.ok((0, utils_1.isLocalHttps)('https://[::1]:4000/v1'));
52
+ assert.ok((0, utils_1.isLocalHttps)('https://LOCALHOST:4000/v1'), 'hostname comparison is case-insensitive');
53
+ });
54
+ test('a registrable domain that merely CONTAINS a loopback name is not local', () => {
55
+ // The bug: an attacker registers localhost.attacker.tld, points a routing setting at it,
56
+ // and the engine child stops verifying certificates for every TLS connection it makes.
57
+ assert.ok(!(0, utils_1.isLocalHttps)('https://localhost.attacker.tld/v1'));
58
+ assert.ok(!(0, utils_1.isLocalHttps)('https://127.0.0.1.attacker.tld/v1'));
59
+ assert.ok(!(0, utils_1.isLocalHttps)('https://not-localhost.example/v1'));
60
+ assert.ok(!(0, utils_1.isLocalHttps)('https://evil.example/?redirect=localhost'));
61
+ assert.ok(!(0, utils_1.isLocalHttps)('https://evil.example/localhost'));
62
+ assert.ok(!(0, utils_1.isLocalHttps)('https://evil.example/#127.0.0.1'));
63
+ assert.ok(!(0, utils_1.isLocalHttps)('https://user@localhost.attacker.tld/v1'));
64
+ });
65
+ test('http, empty and malformed values are not local HTTPS', () => {
66
+ assert.ok(!(0, utils_1.isLocalHttps)('http://localhost:4000/v1'), 'plain http needs no TLS bypass');
67
+ assert.ok(!(0, utils_1.isLocalHttps)(undefined));
68
+ assert.ok(!(0, utils_1.isLocalHttps)(''));
69
+ assert.ok(!(0, utils_1.isLocalHttps)('https://'));
70
+ assert.ok(!(0, utils_1.isLocalHttps)('localhost:4000'));
71
+ });
72
+ });
73
+ suite('local TLS bypass application', () => {
74
+ test('sets the bypass only for a genuinely local HTTPS endpoint', () => {
75
+ const env = {};
76
+ assert.strictEqual((0, utils_1.applyLocalTlsBypass)(env, 'https://127.0.0.1:4000/v1'), true);
77
+ assert.strictEqual(env.NODE_TLS_REJECT_UNAUTHORIZED, '0');
78
+ });
79
+ test('leaves the child env untouched for a lookalike host', () => {
80
+ const env = {};
81
+ assert.strictEqual((0, utils_1.applyLocalTlsBypass)(env, 'https://localhost.attacker.tld/v1'), false);
82
+ assert.strictEqual(env.NODE_TLS_REJECT_UNAUTHORIZED, undefined);
83
+ assert.deepStrictEqual(env, {}, 'nothing at all is added');
84
+ });
85
+ test('leaves the child env untouched for a remote provider', () => {
86
+ const env = {};
87
+ assert.strictEqual((0, utils_1.applyLocalTlsBypass)(env, 'https://api.anthropic.com/v1'), false);
88
+ assert.strictEqual(env.NODE_TLS_REJECT_UNAUTHORIZED, undefined);
89
+ });
90
+ });
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Signed warrants (PLAN/serve-v4-clean.md §6).
4
+ *
5
+ * The properties under test are the ones that delete the P1 bug class:
6
+ * - a gateway can verify but NEVER mint (no shared secret anywhere);
7
+ * - a warrant is bound to its sandbox, so a lifted one fails from another sandbox;
8
+ * - audience, expiry, and algorithm confusion are all refused.
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 crypto = __importStar(require("node:crypto"));
47
+ const warrant_1 = require("../authority/warrant");
48
+ const KEY = (0, warrant_1.generateWarrantKeypair)('kid-1');
49
+ const PUBS = { [KEY.kid]: KEY.publicKeyPem };
50
+ function mint(over = {}) {
51
+ return (0, warrant_1.mintWarrant)({
52
+ user: 'alice@corp.com', pack: 'analyst', packVersion: '1.4.2',
53
+ sandboxId: 'sandbox-1', tier: 2, aud: 'data', ...over,
54
+ }, KEY);
55
+ }
56
+ suite('signed warrants', () => {
57
+ test('round-trips and carries the full evidence key', () => {
58
+ const res = (0, warrant_1.verifyWarrant)(mint(), PUBS, { audience: 'data', channelSandboxId: 'sandbox-1' });
59
+ assert.ok(res.ok, `expected ok, got ${JSON.stringify(res)}`);
60
+ if (!res.ok) {
61
+ return;
62
+ }
63
+ assert.strictEqual(res.claims.user, 'alice@corp.com');
64
+ assert.strictEqual(res.claims.packVersion, '1.4.2');
65
+ assert.strictEqual(res.claims.tier, 2, 'the isolation tier in force is recorded on every turn');
66
+ assert.ok(res.claims.jti, 'warrant id present — the key observers join evidence on');
67
+ });
68
+ test('a warrant lifted into another sandbox FAILS — channel binding, not bearer', () => {
69
+ const stolen = mint({ sandboxId: 'sandbox-1' });
70
+ const asOwner = (0, warrant_1.verifyWarrant)(stolen, PUBS, { audience: 'data', channelSandboxId: 'sandbox-1' });
71
+ const asThief = (0, warrant_1.verifyWarrant)(stolen, PUBS, { audience: 'data', channelSandboxId: 'sandbox-2' });
72
+ assert.ok(asOwner.ok, 'valid in its own sandbox');
73
+ assert.ok(!asThief.ok && /not bound to this channel/.test(asThief.reason), 'the same signature is refused from a different sandbox');
74
+ });
75
+ test('a gateway holding only the public key cannot mint', () => {
76
+ // The structural claim: verification material is not signing material. Attempting to
77
+ // sign with the SPKI public key throws — there is no path from verify to mint.
78
+ assert.throws(() => {
79
+ crypto.sign('sha256', Buffer.from('x.y'), {
80
+ key: crypto.createPrivateKey(KEY.publicKeyPem),
81
+ dsaEncoding: 'ieee-p1363',
82
+ });
83
+ }, 'a public key cannot be loaded as a private key');
84
+ const doc = (0, warrant_1.jwks)([KEY]);
85
+ const serialized = JSON.stringify(doc);
86
+ assert.ok(!serialized.includes('PRIVATE'), 'JWKS publishes no private material');
87
+ assert.ok(!doc.keys[0].d, 'the EC private scalar `d` must never appear in JWKS');
88
+ });
89
+ test('wrong audience is refused — a data warrant cannot spend at the inference proxy', () => {
90
+ const res = (0, warrant_1.verifyWarrant)(mint({ aud: 'data' }), PUBS, { audience: 'inference', channelSandboxId: 'sandbox-1' });
91
+ assert.ok(!res.ok && /wrong audience/.test(res.reason));
92
+ });
93
+ test('expiry is enforced and TTL is hard-capped at turn length', () => {
94
+ const now = 1_800_000_000;
95
+ const t = mint({ nowSeconds: now, ttlSeconds: 10 });
96
+ assert.ok((0, warrant_1.verifyWarrant)(t, PUBS, { audience: 'data', channelSandboxId: 'sandbox-1', nowSeconds: now + 5 }).ok);
97
+ const late = (0, warrant_1.verifyWarrant)(t, PUBS, { audience: 'data', channelSandboxId: 'sandbox-1', nowSeconds: now + 11 });
98
+ assert.ok(!late.ok && /expired/.test(late.reason));
99
+ // A caller asking for a day-long warrant gets a turn-long one — there is no revocation
100
+ // registry, so lifetime IS the bound.
101
+ const greedy = mint({ nowSeconds: now, ttlSeconds: 86_400 });
102
+ const capped = (0, warrant_1.verifyWarrant)(greedy, PUBS, { audience: 'data', channelSandboxId: 'sandbox-1', nowSeconds: now + 301 });
103
+ assert.ok(!capped.ok && /expired/.test(capped.reason), 'TTL capped at 300s regardless of request');
104
+ });
105
+ test('algorithm confusion is refused (alg=none, alg=HS256)', () => {
106
+ const claims = Buffer.from(JSON.stringify({
107
+ user: 'mallory', pack: 'p', packVersion: '1', sandboxId: 'sandbox-1', tier: 2,
108
+ aud: 'data', iat: 0, exp: 9_999_999_999, jti: 'x',
109
+ })).toString('base64url');
110
+ const none = `${Buffer.from(JSON.stringify({ alg: 'none', kid: KEY.kid })).toString('base64url')}.${claims}.`;
111
+ assert.ok(!(0, warrant_1.verifyWarrant)(none, PUBS, { audience: 'data' }).ok, 'alg=none refused');
112
+ // The classic break: sign with HMAC using the PUBLIC key as the secret.
113
+ const hdr = Buffer.from(JSON.stringify({ alg: 'HS256', kid: KEY.kid })).toString('base64url');
114
+ const forged = crypto.createHmac('sha256', KEY.publicKeyPem).update(`${hdr}.${claims}`).digest('base64url');
115
+ assert.ok(!(0, warrant_1.verifyWarrant)(`${hdr}.${claims}.${forged}`, PUBS, { audience: 'data' }).ok, 'HS256 confusion refused');
116
+ });
117
+ test('a tampered claim invalidates the signature', () => {
118
+ const [h, c, s] = mint().split('.');
119
+ const swapped = JSON.parse(Buffer.from(c, 'base64url').toString('utf8'));
120
+ swapped.user = 'mallory@evil.com'; // escalate to another principal
121
+ const tampered = `${h}.${Buffer.from(JSON.stringify(swapped)).toString('base64url')}.${s}`;
122
+ const res = (0, warrant_1.verifyWarrant)(tampered, PUBS, { audience: 'data', channelSandboxId: 'sandbox-1' });
123
+ assert.ok(!res.ok && /bad signature/.test(res.reason), 'rewriting the principal breaks the signature');
124
+ });
125
+ test('an unknown key id is refused (no implicit trust on rotation)', () => {
126
+ const other = (0, warrant_1.generateWarrantKeypair)('kid-2');
127
+ const res = (0, warrant_1.verifyWarrant)((0, warrant_1.mintWarrant)({
128
+ user: 'a', pack: 'p', packVersion: '1', sandboxId: 'sandbox-1', tier: 2, aud: 'data',
129
+ }, other), PUBS, { audience: 'data', channelSandboxId: 'sandbox-1' });
130
+ assert.ok(!res.ok && /unknown kid/.test(res.reason));
131
+ });
132
+ test('malformed input never throws — the sandbox is untrusted toward the gateway', () => {
133
+ for (const bad of ['', 'a', 'a.b', 'a.b.c', '..', 'null.null.null']) {
134
+ const res = (0, warrant_1.verifyWarrant)(bad, PUBS, { audience: 'data' });
135
+ assert.ok(!res.ok, `must refuse: ${bad}`);
136
+ }
137
+ });
138
+ });
package/out/utils.js CHANGED
@@ -55,6 +55,7 @@ exports.shortSig = shortSig;
55
55
  exports.isLocalHttps = isLocalHttps;
56
56
  exports.applyLocalTlsBypass = applyLocalTlsBypass;
57
57
  exports.getOverrideOpenAIBaseUrl = getOverrideOpenAIBaseUrl;
58
+ exports.inferModelProvider = inferModelProvider;
58
59
  exports.resolveApiRouting = resolveApiRouting;
59
60
  exports.getExplicitConfigValue = getExplicitConfigValue;
60
61
  exports.getAgentideEngineSettings = getAgentideEngineSettings;
@@ -75,6 +76,7 @@ const path = __importStar(require("path"));
75
76
  const crypto = __importStar(require("crypto"));
76
77
  const shared_1 = require("./shared");
77
78
  const branding_1 = require("./branding");
79
+ const customRegistry_1 = require("./engines/customRegistry");
78
80
  const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
79
81
  // Lazy-load vscode to allow bundling for CLI/headless mode
80
82
  // Use eval to prevent Bun from statically analyzing the require
@@ -462,12 +464,19 @@ async function syncDirByFileLinks(sourceDir, targetDir, logger, label) {
462
464
  };
463
465
  await cleanup(targetDir, '');
464
466
  }
467
+ function getManagedWorkspaceDirs(workspaceRoot, extraSearchPath) {
468
+ const builtins = Object.values(shared_1.AGENT_ENGINES).map(engine => engine.workspaceDir);
469
+ const custom = (0, customRegistry_1.discoverCustomEngineManifests)(workspaceRoot, extraSearchPath)
470
+ .filter(manifest => manifest.workspaceSync?.enabled)
471
+ .map(manifest => manifest.workspaceSync.dir);
472
+ return [...new Set([...builtins, ...custom])];
473
+ }
465
474
  /**
466
475
  * Ensure workspace root has git initialized and .gitignore entries for agent directories.
467
476
  * If the folder is not a git repository, runs `git init`.
468
- * Adds the agent root, .claude, .codex to the root .gitignore if not already present.
477
+ * Adds managed engine workspace dirs (builtins + custom engines with workspaceSync) to root .gitignore.
469
478
  */
470
- async function ensureWorkspaceGitAndGitignore(workspaceRoot, logger) {
479
+ async function ensureWorkspaceGitAndGitignore(workspaceRoot, logger, options) {
471
480
  const gitDir = path.join(workspaceRoot, '.git');
472
481
  // Check if .git exists
473
482
  const gitExists = await pathExists(gitDir);
@@ -484,10 +493,7 @@ async function ensureWorkspaceGitAndGitignore(workspaceRoot, logger) {
484
493
  }
485
494
  // Ensure root .gitignore has entries for agent directories
486
495
  const rootGitignorePath = path.join(workspaceRoot, '.gitignore');
487
- // Build the list of directories to ignore from AGENT_ENGINES
488
- const agentideDirs = [
489
- ...Object.values(shared_1.AGENT_ENGINES).map(engine => engine.workspaceDir),
490
- ];
496
+ const agentideDirs = getManagedWorkspaceDirs(workspaceRoot, options?.extraSearchPath);
491
497
  const managedBlockLines = [
492
498
  `# ${branding_1.AGENTIDE_BRAND_NAME} (managed)`,
493
499
  `# These directories are managed by ${branding_1.AGENTIDE_BRAND_NAME} and should not be committed.`,
@@ -608,17 +614,49 @@ function shortSig(input) {
608
614
  // Environment Utilities
609
615
  // -----------------------------------------------------------------------------
610
616
  /**
611
- * Check if a URL is a local HTTPS address (localhost, 127.0.0.1, 0.0.0.0).
617
+ * Hostnames that are unambiguously this machine. Compared against the PARSED hostname, never a
618
+ * substring of the URL.
619
+ */
620
+ const LOCAL_TLS_HOSTNAMES = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', '[::1]']);
621
+ /**
622
+ * Is this URL an HTTPS endpoint on the local machine (a dev LiteLLM/proxy with a self-signed cert)?
623
+ *
624
+ * This used to be `url.includes('localhost')`, which is a substring test on the WHOLE URL — so
625
+ * `https://localhost.attacker.tld/v1` matched, and the caller then disabled TLS verification for
626
+ * the engine child process talking to it. The hostname is parsed and compared exactly now: a
627
+ * registrable domain that merely *contains* "localhost" is a remote host like any other.
628
+ *
629
+ * Note the IPv6 form: `new URL('https://[::1]/').hostname` is `[::1]` (brackets included), so both
630
+ * spellings are in the set.
612
631
  */
613
632
  function isLocalHttps(url) {
614
633
  if (!url) {
615
634
  return false;
616
635
  }
617
- return url.startsWith('https://') && (url.includes('localhost') || url.includes('127.0.0.1') || url.includes('0.0.0.0'));
636
+ try {
637
+ const parsed = new URL(url);
638
+ return parsed.protocol === 'https:' && LOCAL_TLS_HOSTNAMES.has(parsed.hostname.toLowerCase());
639
+ }
640
+ catch {
641
+ return false;
642
+ }
618
643
  }
619
644
  /**
620
645
  * Apply TLS verification bypass for local HTTPS in an environment object.
621
- * Returns the modified env and whether TLS was disabled.
646
+ * Returns whether TLS verification was disabled.
647
+ *
648
+ * **Residual risk, stated plainly.** `NODE_TLS_REJECT_UNAUTHORIZED=0` is PROCESS-WIDE inside the
649
+ * engine child: it disables certificate verification for every TLS connection that child makes,
650
+ * not just the one to the local proxy. Scoping it per-request is not available to us here — the
651
+ * engines are vendor SDKs (Claude Agent SDK, Codex SDK, arbitrary custom-engine binaries) spawned
652
+ * as separate processes, and the only channel we have into their HTTP stack is the environment.
653
+ * Passing a CA bundle instead (`NODE_EXTRA_CA_CERTS`) would be the real fix and requires the local
654
+ * proxy to publish its cert, which it does not today.
655
+ *
656
+ * What bounds the risk is therefore entirely the check above: the bypass is only ever enabled when
657
+ * the routing endpoint is literally this machine, which is also the only place a self-signed cert
658
+ * is expected. That is why the substring match mattered — it turned an opt-in for `127.0.0.1` into
659
+ * an opt-in for any hostname an attacker could register.
622
660
  */
623
661
  function applyLocalTlsBypass(env, baseUrl) {
624
662
  if (isLocalHttps(baseUrl)) {
@@ -651,20 +689,35 @@ function getOverrideOpenAIBaseUrl(baseUrl, configProvider) {
651
689
  }
652
690
  return undefined;
653
691
  }
692
+ /** Infer the direct provider from a model id — used by engines (e.g. custom) that aren't pinned to one vendor. */
693
+ function inferModelProvider(modelId) {
694
+ return modelId && /claude|anthropic/i.test(modelId) ? 'anthropic' : 'openai';
695
+ }
654
696
  /**
655
- * Resolve API key and base URL based on LiteLLM routing configuration.
656
- * Handles fallback to global litellm* settings when useLiteLLM is true.
657
- * Returns both baseUrl (for Claude/Anthropic) and openaiBaseUrl (with overrideOpenAIBaseUrl applied for Codex).
697
+ * Resolve the API key + base URL for a turn by the DESTINATION it dials, not a global
698
+ * precedence over provider keys.
699
+ *
700
+ * - LiteLLM proxy selected (`useLiteLLM`) → the LiteLLM key.
701
+ * - direct → the key for THIS engine's provider
702
+ * (codex→openai, claude→anthropic; custom infers
703
+ * from its model). Never the "other" provider's key.
704
+ *
705
+ * An explicit per-request `options.apiKey` (the IDE's single configured key) still wins — the
706
+ * per-provider `creds` are the headless/serve fallback that replaces the old
707
+ * `anthropic || openai` collapse (which sent, e.g., a codex turn to OpenAI with an Anthropic key).
658
708
  *
659
- * @param options - Agent request options
660
- * @param litellmApiKey - Fallback LiteLLM API key
661
- * @param litellmBaseUrl - Fallback LiteLLM base URL
662
- * @param configProvider - Optional config provider for resolving overrides
709
+ * @param options - Agent request options (carries an optional explicit apiKey / baseUrl / useLiteLLM)
710
+ * @param creds - Per-destination credentials (EngineConfig satisfies this)
711
+ * @param provider - The engine's direct provider, used only when NOT routing through LiteLLM
712
+ * @param configProvider - Optional config provider for resolving overrideOpenAIBaseUrl
663
713
  */
664
- function resolveApiRouting(options, litellmApiKey, litellmBaseUrl, configProvider) {
714
+ function resolveApiRouting(options, creds, provider, configProvider) {
665
715
  const useLiteLLM = options.useLiteLLM === true;
666
- const baseUrl = options.baseUrl || (useLiteLLM ? litellmBaseUrl : undefined);
667
- const liteLLMResolvedApiKey = options.apiKey || litellmApiKey || 'sk-dummy';
716
+ const baseUrl = options.baseUrl || (useLiteLLM ? creds.litellmBaseUrl : undefined);
717
+ const directKey = provider === 'anthropic' ? creds.anthropicApiKey : creds.openaiApiKey;
718
+ const resolvedApiKey = useLiteLLM
719
+ ? (options.apiKey || creds.litellmApiKey || 'sk-dummy')
720
+ : (options.apiKey || directKey || '');
668
721
  // For OpenAI-compatible endpoints, check for per-provider overrideOpenAIBaseUrl
669
722
  let openaiBaseUrl = baseUrl;
670
723
  if (baseUrl) {
@@ -674,7 +727,7 @@ function resolveApiRouting(options, litellmApiKey, litellmBaseUrl, configProvide
674
727
  }
675
728
  }
676
729
  return {
677
- apiKey: useLiteLLM ? liteLLMResolvedApiKey : (options.apiKey || ''),
730
+ apiKey: resolvedApiKey,
678
731
  baseUrl,
679
732
  openaiBaseUrl,
680
733
  useLiteLLM,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenthippo-ai/agenthippo",
3
- "version": "0.1.1",
3
+ "version": "1.110.9",
4
4
  "description": "AgentHippo CLI — serve, chat, and deploy agent packs",
5
5
  "author": "AgentHippo <dev@agenthippo.ai>",
6
6
  "license": "SEE LICENSE IN LICENSE",