@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,202 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * The trust root — where the fleet learns which code it is allowed to run.
4
+ *
5
+ * Every one of these pins a property that was absent: the JWKS URL was built from whatever
6
+ * AGENTHIPPO_LEASE_URL pointed at, fetched with a bare `fetch()`, with no scheme requirement, no
7
+ * pinning, and nothing signing the document. Whoever could answer that URL chose the trust root.
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 warrant_1 = require("../authority/warrant");
46
+ const signature_1 = require("../pack/signature");
47
+ const key = (0, warrant_1.generateWarrantKeypair)('k1');
48
+ const other = (0, warrant_1.generateWarrantKeypair)('k2');
49
+ const okResponse = (body) => ({
50
+ ok: true,
51
+ status: 200,
52
+ json: async () => body,
53
+ });
54
+ const fetchReturning = (body) => (async () => okResponse(body));
55
+ suite('trust root — scheme', () => {
56
+ test('an http JWKS origin is refused: the keyring that decides what runs must not be rewritable in flight', async () => {
57
+ const root = await (0, signature_1.loadPackTrustRoot)({
58
+ env: { [signature_1.PACK_JWKS_URL_ENV]: 'http://plane.example/.well-known/jwks.json' },
59
+ fetchImpl: fetchReturning({ pemsByKid: { k1: key.publicKeyPem } }),
60
+ });
61
+ assert.strictEqual(root.status, 'unavailable');
62
+ assert.match(root.reason ?? '', /must be https/);
63
+ assert.deepStrictEqual(root.keys, {});
64
+ });
65
+ test('https is accepted with no warning', async () => {
66
+ const root = await (0, signature_1.loadPackTrustRoot)({
67
+ env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://plane.example/.well-known/jwks.json' },
68
+ fetchImpl: fetchReturning({ pemsByKid: { k1: key.publicKeyPem } }),
69
+ });
70
+ assert.strictEqual(root.status, 'configured');
71
+ assert.deepStrictEqual(Object.keys(root.keys), ['k1']);
72
+ });
73
+ test('the local-dev escape hatch works and is LOUD', async () => {
74
+ const root = await (0, signature_1.loadPackTrustRoot)({
75
+ env: {
76
+ [signature_1.PACK_JWKS_URL_ENV]: 'http://localhost:9000/.well-known/jwks.json',
77
+ [signature_1.ALLOW_INSECURE_TRUST_ROOT_ENV]: '1',
78
+ },
79
+ fetchImpl: fetchReturning({ pemsByKid: { k1: key.publicKeyPem } }),
80
+ });
81
+ assert.strictEqual(root.status, 'configured');
82
+ assert.ok(root.warnings.some((w) => /INSECURE/.test(w)), 'the opt-out must announce itself');
83
+ });
84
+ test('assertSecureTrustUrl rejects a malformed URL rather than assuming a scheme', () => {
85
+ const r = (0, signature_1.assertSecureTrustUrl)('not a url', 'the trust root', {});
86
+ assert.strictEqual(r.ok, false);
87
+ });
88
+ });
89
+ suite('trust root — independence from the lease origin', () => {
90
+ test('a dedicated JWKS URL takes precedence over anything derived from the lease', async () => {
91
+ let asked = '';
92
+ const root = await (0, signature_1.loadPackTrustRoot)({
93
+ env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://trust.example/jwks.json' },
94
+ leaseUrl: 'https://attacker.example/lease',
95
+ fetchImpl: (async (u) => { asked = u; return okResponse({ pemsByKid: { k1: key.publicKeyPem } }); }),
96
+ });
97
+ assert.strictEqual(asked, 'https://trust.example/jwks.json');
98
+ assert.strictEqual(root.status, 'configured');
99
+ });
100
+ test('deriving from the lease origin still works but says it is weaker', async () => {
101
+ const root = await (0, signature_1.loadPackTrustRoot)({
102
+ env: {},
103
+ leaseUrl: 'https://plane.example/lease',
104
+ fetchImpl: fetchReturning({ pemsByKid: { k1: key.publicKeyPem } }),
105
+ });
106
+ assert.strictEqual(root.status, 'configured');
107
+ assert.strictEqual(root.derivedFromLease, true);
108
+ assert.ok(root.warnings.some((w) => /derived from AGENTHIPPO_LEASE_URL/.test(w)));
109
+ });
110
+ test('the derived URL asks for the pack-signing purpose, so a plane that splits keys by purpose still answers it', async () => {
111
+ let asked = '';
112
+ const root = await (0, signature_1.loadPackTrustRoot)({
113
+ env: {},
114
+ leaseUrl: 'https://plane.example/lease',
115
+ fetchImpl: (async (u) => { asked = u; return okResponse({ pemsByKid: { k1: key.publicKeyPem } }); }),
116
+ });
117
+ // Without the query string a plane that now serves one purpose per document can hand back
118
+ // a set with no pack-signing key in it — an empty trust root, which with signing fail-closed
119
+ // means every instance refuses to boot at once.
120
+ assert.strictEqual(asked, 'https://plane.example/.well-known/jwks.json?purpose=pack-signing');
121
+ assert.strictEqual(root.status, 'configured');
122
+ });
123
+ test('nothing configured is UNCONFIGURED, not an error — an unmanaged deployment has no plane', async () => {
124
+ const root = await (0, signature_1.loadPackTrustRoot)({ env: {} });
125
+ assert.strictEqual(root.status, 'unconfigured');
126
+ assert.deepStrictEqual(root.keys, {});
127
+ });
128
+ });
129
+ suite('trust root — thumbprint pinning', () => {
130
+ test('a key that matches no pin is REFUSED, by either thumbprint form', async () => {
131
+ const tp = (0, signature_1.publicKeyThumbprints)(key.publicKeyPem);
132
+ for (const pin of [tp.jwk, tp.spki, `sha256:${tp.spki.toUpperCase()}`]) {
133
+ const root = await (0, signature_1.loadPackTrustRoot)({
134
+ env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://t.example/j', [signature_1.PACK_JWKS_PINS_ENV]: pin },
135
+ fetchImpl: fetchReturning({ pemsByKid: { k1: key.publicKeyPem, k2: other.publicKeyPem } }),
136
+ });
137
+ assert.strictEqual(root.status, 'configured', `pin form ${pin} should match`);
138
+ assert.deepStrictEqual(Object.keys(root.keys), ['k1'], 'the unpinned key must be dropped');
139
+ assert.ok(root.warnings.some((w) => /k2 does not match any pin/.test(w)));
140
+ }
141
+ });
142
+ test('a swapped trust root that matches NO pin is unavailable, not silently empty', async () => {
143
+ const tp = (0, signature_1.publicKeyThumbprints)(key.publicKeyPem);
144
+ const root = await (0, signature_1.loadPackTrustRoot)({
145
+ env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://t.example/j', [signature_1.PACK_JWKS_PINS_ENV]: tp.spki },
146
+ fetchImpl: fetchReturning({ pemsByKid: { k2: other.publicKeyPem } }),
147
+ });
148
+ assert.strictEqual(root.status, 'unavailable');
149
+ assert.match(root.reason ?? '', /pin check/);
150
+ });
151
+ test('parseJwksPins tolerates whitespace, prefixes and blanks', () => {
152
+ assert.deepStrictEqual((0, signature_1.parseJwksPins)(' sha256:AB , ,cd '), ['ab', 'cd']);
153
+ assert.deepStrictEqual((0, signature_1.parseJwksPins)(undefined), []);
154
+ });
155
+ });
156
+ suite('trust root — A4: unavailable is not the same as unconfigured', () => {
157
+ test('a non-200 JWKS is an ERROR, not an empty keyring', async () => {
158
+ const root = await (0, signature_1.loadPackTrustRoot)({
159
+ env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://t.example/j' },
160
+ fetchImpl: (async () => ({ ok: false, status: 503 })),
161
+ });
162
+ assert.strictEqual(root.status, 'unavailable');
163
+ assert.match(root.reason ?? '', /HTTP 503/);
164
+ });
165
+ test('a transport failure is an ERROR', async () => {
166
+ const root = await (0, signature_1.loadPackTrustRoot)({
167
+ env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://t.example/j' },
168
+ fetchImpl: (async () => { throw new Error('ECONNREFUSED'); }),
169
+ });
170
+ assert.strictEqual(root.status, 'unavailable');
171
+ assert.match(root.reason ?? '', /ECONNREFUSED/);
172
+ });
173
+ test('a garbled inline keyring is an ERROR — a typo must not read as "unmanaged"', async () => {
174
+ const root = await (0, signature_1.loadPackTrustRoot)({ env: { AGENTHIPPO_PACK_SIGNING_KEYS: '{not json' } });
175
+ assert.strictEqual(root.status, 'unavailable');
176
+ assert.match(root.reason ?? '', /not valid JSON/);
177
+ });
178
+ test('a JWKS with no usable ES256 keys is an ERROR', async () => {
179
+ const root = await (0, signature_1.loadPackTrustRoot)({
180
+ env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://t.example/j' },
181
+ fetchImpl: fetchReturning({ keys: [{ kty: 'RSA', kid: 'r1', n: 'x', e: 'AQAB' }] }),
182
+ });
183
+ assert.strictEqual(root.status, 'unavailable');
184
+ });
185
+ });
186
+ suite('trust root — document shapes', () => {
187
+ test('a standard JWKS (what authority/warrant.ts publishes) is accepted', async () => {
188
+ const root = await (0, signature_1.loadPackTrustRoot)({
189
+ env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://t.example/j' },
190
+ fetchImpl: fetchReturning((0, warrant_1.jwks)([key])),
191
+ });
192
+ assert.strictEqual(root.status, 'configured');
193
+ assert.deepStrictEqual(Object.keys(root.keys), ['k1']);
194
+ });
195
+ test('inline keys need no network at all (air-gapped)', async () => {
196
+ const root = await (0, signature_1.loadPackTrustRoot)({
197
+ env: { AGENTHIPPO_PACK_SIGNING_KEYS: JSON.stringify({ k1: key.publicKeyPem }) },
198
+ fetchImpl: (async () => { throw new Error('must not be called'); }),
199
+ });
200
+ assert.strictEqual(root.status, 'configured');
201
+ });
202
+ });
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *
5
+ * `spec.remote.url` comes from a pack manifest — a file that arrives from the store, a PR, or a
6
+ * teammate. These tests pin that it can no longer be turned into a token-exfiltration sink.
7
+ *--------------------------------------------------------------------------------------------*/
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ require("mocha");
43
+ const assert = __importStar(require("assert"));
44
+ const fs = __importStar(require("node:fs"));
45
+ const os = __importStar(require("node:os"));
46
+ const path = __importStar(require("node:path"));
47
+ const remoteTrust_1 = require("../remote/remoteTrust");
48
+ const serveClient_1 = require("../remote/serveClient");
49
+ suite('remote serve URL classification', () => {
50
+ test('loopback is its own level — allowed, never prompted', () => {
51
+ for (const url of ['http://127.0.0.1:8080', 'http://localhost:9000/base', 'https://127.0.0.1:8443']) {
52
+ const v = (0, remoteTrust_1.classifyRemoteServeUrl)(url);
53
+ assert.strictEqual(v.level, 'loopback', url);
54
+ assert.strictEqual((0, remoteTrust_1.mayReceiveAccessToken)(v), true, `${url} is this machine`);
55
+ }
56
+ });
57
+ test('an untrusted host may NOT receive the access token', () => {
58
+ assert.strictEqual((0, remoteTrust_1.mayReceiveAccessToken)((0, remoteTrust_1.classifyRemoteServeUrl)('https://attacker.example')), false);
59
+ });
60
+ test('the product domain and its subdomains are trusted', () => {
61
+ assert.strictEqual((0, remoteTrust_1.classifyRemoteServeUrl)('https://agenthippo.ai').level, 'trusted');
62
+ assert.strictEqual((0, remoteTrust_1.classifyRemoteServeUrl)('https://serve.agenthippo.ai/v1').level, 'trusted');
63
+ });
64
+ test('a lookalike domain is NOT trusted', () => {
65
+ // The suffix match must be on a label boundary, not a substring.
66
+ assert.strictEqual((0, remoteTrust_1.classifyRemoteServeUrl)('https://agenthippo.ai.attacker.tld').level, 'untrusted');
67
+ assert.strictEqual((0, remoteTrust_1.classifyRemoteServeUrl)('https://notagenthippo.ai').level, 'untrusted');
68
+ assert.strictEqual((0, remoteTrust_1.classifyRemoteServeUrl)('https://evil-agenthippo.ai').level, 'untrusted');
69
+ });
70
+ test('cleartext http to a non-loopback host is refused outright — no consent path', () => {
71
+ const v = (0, remoteTrust_1.classifyRemoteServeUrl)('http://attacker.example/serve');
72
+ assert.strictEqual(v.level, 'refused');
73
+ assert.ok(v.reason?.includes('access token'), 'the reason names what would leak');
74
+ });
75
+ test('a non-http scheme and a malformed URL are refused', () => {
76
+ assert.strictEqual((0, remoteTrust_1.classifyRemoteServeUrl)('ftp://attacker.example').level, 'refused');
77
+ assert.strictEqual((0, remoteTrust_1.classifyRemoteServeUrl)('¬not a url').level, 'refused');
78
+ });
79
+ test('an explicitly approved origin becomes trusted', () => {
80
+ const v = (0, remoteTrust_1.classifyRemoteServeUrl)('https://serve.corp.example:8443/x', {
81
+ approvedOrigins: ['https://serve.corp.example:8443'],
82
+ });
83
+ assert.strictEqual(v.level, 'trusted');
84
+ });
85
+ test('an approval is per origin — a different port is a different host', () => {
86
+ const v = (0, remoteTrust_1.classifyRemoteServeUrl)('https://serve.corp.example:9999/x', {
87
+ approvedOrigins: ['https://serve.corp.example:8443'],
88
+ });
89
+ assert.strictEqual(v.level, 'untrusted');
90
+ });
91
+ });
92
+ suite('remote trust gate', () => {
93
+ function memoryStore(initial = []) {
94
+ const origins = [...initial];
95
+ return {
96
+ origins,
97
+ readApprovedOrigins: () => [...origins],
98
+ approveOrigin: (o) => { origins.push(o); },
99
+ };
100
+ }
101
+ test('with no prompt available, an untrusted host is REFUSED, not silently contacted', async () => {
102
+ const gate = (0, remoteTrust_1.createRemoteTrustGate)({ store: memoryStore(), trustedDomains: ['agenthippo.ai'] });
103
+ const result = await gate.evaluate('https://attacker.example/serve');
104
+ assert.strictEqual(result.allowed, false);
105
+ assert.strictEqual(result.sendAuth, false);
106
+ assert.ok(result.verdict.reason?.includes('attacker.example'));
107
+ });
108
+ test('declining the prompt refuses the turn and stores nothing', async () => {
109
+ const store = memoryStore();
110
+ const gate = (0, remoteTrust_1.createRemoteTrustGate)({
111
+ store,
112
+ trustedDomains: ['agenthippo.ai'],
113
+ prompt: async () => false,
114
+ });
115
+ const result = await gate.evaluate('https://attacker.example/serve');
116
+ assert.strictEqual(result.allowed, false);
117
+ assert.strictEqual(result.sendAuth, false);
118
+ assert.deepStrictEqual(store.origins, [], 'a declined host is not remembered');
119
+ });
120
+ test('approving the prompt allows the turn and remembers the origin', async () => {
121
+ const store = memoryStore();
122
+ let promptedWith;
123
+ const gate = (0, remoteTrust_1.createRemoteTrustGate)({
124
+ store,
125
+ trustedDomains: ['agenthippo.ai'],
126
+ prompt: async (v) => { promptedWith = v; return true; },
127
+ });
128
+ const result = await gate.evaluate('https://serve.corp.example/x');
129
+ assert.strictEqual(result.allowed, true);
130
+ assert.strictEqual(result.sendAuth, true);
131
+ assert.strictEqual(promptedWith?.hostname, 'serve.corp.example', 'the prompt names the host');
132
+ assert.deepStrictEqual(store.origins, ['https://serve.corp.example']);
133
+ });
134
+ test('a remembered origin is not prompted again', async () => {
135
+ const store = memoryStore(['https://serve.corp.example']);
136
+ let prompted = 0;
137
+ const gate = (0, remoteTrust_1.createRemoteTrustGate)({
138
+ store,
139
+ trustedDomains: ['agenthippo.ai'],
140
+ prompt: async () => { prompted++; return true; },
141
+ });
142
+ const result = await gate.evaluate('https://serve.corp.example/x');
143
+ assert.strictEqual(result.allowed, true);
144
+ assert.strictEqual(result.sendAuth, true);
145
+ assert.strictEqual(prompted, 0);
146
+ });
147
+ test('a refused scheme is never offered for consent', async () => {
148
+ let prompted = 0;
149
+ const gate = (0, remoteTrust_1.createRemoteTrustGate)({
150
+ store: memoryStore(),
151
+ prompt: async () => { prompted++; return true; },
152
+ });
153
+ const result = await gate.evaluate('http://attacker.example/serve');
154
+ assert.strictEqual(result.allowed, false);
155
+ assert.strictEqual(prompted, 0, 'cleartext is not a consent decision');
156
+ });
157
+ test('loopback is allowed without a prompt and never forces a sign-in', async () => {
158
+ let prompted = 0;
159
+ const gate = (0, remoteTrust_1.createRemoteTrustGate)({
160
+ store: memoryStore(),
161
+ prompt: async () => { prompted++; return true; },
162
+ });
163
+ const result = await gate.evaluate('http://127.0.0.1:7777');
164
+ assert.strictEqual(result.allowed, true);
165
+ assert.strictEqual(result.sendAuth, true, 'a token we already hold may go to our own machine');
166
+ assert.strictEqual(result.requireAuth, false, 'an unauthenticated local serve must keep working');
167
+ assert.strictEqual(prompted, 0);
168
+ });
169
+ test('a trusted host DOES force a sign-in — that is the point of signing in', async () => {
170
+ const gate = (0, remoteTrust_1.createRemoteTrustGate)({ store: memoryStore(), trustedDomains: ['agenthippo.ai'] });
171
+ const result = await gate.evaluate('https://serve.agenthippo.ai');
172
+ assert.strictEqual(result.allowed, true);
173
+ assert.strictEqual(result.sendAuth, true);
174
+ assert.strictEqual(result.requireAuth, true);
175
+ });
176
+ });
177
+ suite('remote trust store', () => {
178
+ test('approvals persist to a 0600 file outside any workspace, and a missing file approves nothing', () => {
179
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ah-remote-trust-'));
180
+ const storePath = path.join(dir, 'nested', 'remote-trust.json');
181
+ try {
182
+ const store = new remoteTrust_1.RemoteTrustStore(storePath);
183
+ assert.deepStrictEqual(store.readApprovedOrigins(), [], 'no file ⇒ nothing approved (fail closed)');
184
+ store.approveOrigin('https://serve.corp.example');
185
+ assert.deepStrictEqual(store.readApprovedOrigins(), ['https://serve.corp.example']);
186
+ store.approveOrigin('https://serve.corp.example');
187
+ assert.strictEqual(store.readApprovedOrigins().length, 1, 'approving twice does not duplicate');
188
+ if (process.platform !== 'win32') {
189
+ assert.strictEqual(fs.statSync(storePath).mode & 0o777, 0o600, 'file is user-only');
190
+ }
191
+ store.revokeOrigin('https://serve.corp.example');
192
+ assert.deepStrictEqual(store.readApprovedOrigins(), []);
193
+ }
194
+ finally {
195
+ fs.rmSync(dir, { recursive: true, force: true });
196
+ }
197
+ });
198
+ test('a corrupt store file approves nothing rather than throwing', () => {
199
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ah-remote-trust-'));
200
+ const storePath = path.join(dir, 'remote-trust.json');
201
+ try {
202
+ fs.writeFileSync(storePath, '{not json');
203
+ assert.deepStrictEqual(new remoteTrust_1.RemoteTrustStore(storePath).readApprovedOrigins(), []);
204
+ }
205
+ finally {
206
+ fs.rmSync(dir, { recursive: true, force: true });
207
+ }
208
+ });
209
+ test('extra trusted domains come from the environment, not from settings a repo could reach', () => {
210
+ assert.deepStrictEqual((0, remoteTrust_1.trustedDomainsFromEnv)({ AGENTHIPPO_REMOTE_TRUSTED_HOSTS: 'a.example, b.example ,' }), ['a.example', 'b.example']);
211
+ assert.deepStrictEqual((0, remoteTrust_1.trustedDomainsFromEnv)({}), []);
212
+ });
213
+ });
214
+ suite('serve client headers', () => {
215
+ test('no self-asserted identity header is sent alongside the bearer token', () => {
216
+ const headers = (0, serveClient_1.buildServeAuthHeaders)({ accessToken: 'tok', userEmail: 'victim@example.com' });
217
+ assert.strictEqual(headers.Authorization, 'Bearer tok');
218
+ // The serve side trusts `x-agent-user` as its first identity alias; sending a client-chosen
219
+ // value made cross-user session/state access a one-header change.
220
+ const identityHeaders = Object.keys(headers).filter((k) => k.toLowerCase() === 'x-agent-user');
221
+ assert.deepStrictEqual(identityHeaders, [], 'X-Agent-User must not be sent');
222
+ });
223
+ test('no credential means no headers at all', () => {
224
+ assert.deepStrictEqual((0, serveClient_1.buildServeAuthHeaders)(undefined), {});
225
+ assert.deepStrictEqual((0, serveClient_1.buildServeAuthHeaders)({ accessToken: '', userEmail: 'x@y.z' }), {});
226
+ });
227
+ });
228
+ suite('manifest remote URL extraction', () => {
229
+ test('a usable URL is returned unchanged', () => {
230
+ assert.strictEqual((0, serveClient_1.getRemoteServeUrl)({ spec: { remote: { url: 'https://serve.agenthippo.ai' } } }), 'https://serve.agenthippo.ai');
231
+ assert.strictEqual((0, serveClient_1.getRemoteServeUrl)({ spec: { remote: { url: 'http://127.0.0.1:8080' } } }), 'http://127.0.0.1:8080');
232
+ });
233
+ test('a cleartext remote host is dropped, with a diagnostic explaining why', () => {
234
+ const manifest = { spec: { remote: { url: 'http://attacker.example' } } };
235
+ assert.strictEqual((0, serveClient_1.getRemoteServeUrl)(manifest), undefined);
236
+ assert.ok((0, serveClient_1.getRemoteServeUrlDiagnostic)(manifest)?.includes('http://'), 'diagnostic explains the drop');
237
+ });
238
+ test('an absent remote block yields no URL and no diagnostic', () => {
239
+ assert.strictEqual((0, serveClient_1.getRemoteServeUrl)(undefined), undefined);
240
+ assert.strictEqual((0, serveClient_1.getRemoteServeUrlDiagnostic)({}), undefined);
241
+ });
242
+ });