@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
@@ -49,8 +49,10 @@ const child_process_1 = require("child_process");
49
49
  const yaml = __importStar(require("js-yaml"));
50
50
  const deploy_init_1 = require("../cli/deploy-init");
51
51
  const deploy_1 = require("../cli/commands/deploy");
52
+ const idp_init_1 = require("../cli/idp-init");
52
53
  const CTX = {
53
54
  packName: 'databricks-analyst',
55
+ packVersion: '0.1.0',
54
56
  image: 'ghcr.io/agenthippo/runtime:2026.06',
55
57
  port: 8080,
56
58
  engine: 'claude',
@@ -160,6 +162,43 @@ suite('deploy-init: renderTarget produces well-formed artifacts', () => {
160
162
  assert.ok(files['README.md'], `${t} missing README.md`);
161
163
  }
162
164
  });
165
+ test('every target ships a DATA-ACCESS.md summary (governance layer)', () => {
166
+ for (const t of deploy_init_1.DEPLOY_TARGETS) {
167
+ const files = (0, deploy_init_1.renderTarget)(t, CTX);
168
+ const doc = files['DATA-ACCESS.md'];
169
+ assert.ok(doc, `${t} missing DATA-ACCESS.md`);
170
+ assert.ok(doc.includes('## Secrets') && doc.includes('## Egress') && doc.includes('## Blast radius per component'), `${t} DATA-ACCESS.md missing a required section`);
171
+ assert.ok(doc.includes('api.anthropic.com:443'), `${t} DATA-ACCESS.md must name the model API host`);
172
+ }
173
+ });
174
+ test('DATA-ACCESS.md lists auth secrets + oauth2-proxy blast radius only for self-issued edge targets', () => {
175
+ const selfIssued = (0, deploy_init_1.renderTarget)('vm', CTX)['DATA-ACCESS.md'];
176
+ assert.ok(selfIssued.includes('AGENTHIPPO_AUTH_JWT_SECRET'));
177
+ assert.ok(selfIssued.includes('OAUTH2_PROXY_CLIENT_SECRET'));
178
+ assert.ok(selfIssued.includes('**oauth2-proxy**'));
179
+ const federated = (0, deploy_init_1.renderTarget)('vm', { ...CTX, ingress: 'edge-fed', idpUrl: 'https://auth.corp.example' })['DATA-ACCESS.md'];
180
+ assert.ok(federated.includes('AGENTHIPPO_AUTH_JWT_SECRET'));
181
+ assert.ok(!federated.includes('OAUTH2_PROXY_CLIENT_SECRET'), 'federated deployments mint no Google OAuth client');
182
+ assert.ok(!federated.includes('**oauth2-proxy**'), 'federated deployments run no oauth2-proxy');
183
+ const noEdge = (0, deploy_init_1.renderTarget)('databricks-app', CTX)['DATA-ACCESS.md'];
184
+ assert.ok(!noEdge.includes('AGENTHIPPO_AUTH_JWT_SECRET'), 'platform-native targets mint no JWT secret');
185
+ });
186
+ test('image-based targets emit pack provenance header and OCI labels', () => {
187
+ const marker = 'Generated from Agent Pack databricks-analyst@0.1.0 — packs.agenthippo.ai';
188
+ for (const t of ['vm', 'compose', 'cloudrun', 'spcs', 'cloudflare']) {
189
+ const files = (0, deploy_init_1.renderTarget)(t, CTX);
190
+ const df = files['Dockerfile'];
191
+ assert.ok(df.includes(marker), `${t} Dockerfile missing attribution`);
192
+ assert.ok(df.includes('LABEL ai.agenthippo.pack.name="databricks-analyst"'), `${t} missing pack.name label`);
193
+ assert.ok(df.includes('LABEL ai.agenthippo.pack.version="0.1.0"'), `${t} missing pack.version label`);
194
+ assert.ok(df.includes('LABEL ai.agenthippo.pack.engine="claude"'), `${t} missing pack.engine label`);
195
+ assert.ok(files['.env.example'].includes(marker), `${t} .env.example missing attribution`);
196
+ assert.ok(files['README.md'].includes(marker), `${t} README missing attribution`);
197
+ }
198
+ const vmRun = (0, deploy_init_1.renderTarget)('vm', CTX)['run.sh'];
199
+ assert.ok(vmRun.startsWith('#!'), 'run.sh keeps shebang first');
200
+ assert.ok(vmRun.includes(`# ${marker}`), 'run.sh attributes after shebang');
201
+ });
163
202
  test('image-based targets emit a valid 5-line-style Dockerfile referencing the runtime image', () => {
164
203
  for (const t of ['vm', 'compose', 'cloudrun', 'spcs', 'cloudflare']) {
165
204
  const df = (0, deploy_init_1.renderTarget)(t, CTX)['Dockerfile'];
@@ -182,9 +221,13 @@ suite('deploy-init: renderTarget produces well-formed artifacts', () => {
182
221
  // Ship only deploy/www/ from deploy/ (drops deploy/.env, credentials, build artifacts).
183
222
  assert.ok(di.includes('deploy/*'), `${t} should exclude deploy/*`);
184
223
  assert.ok(di.includes('!deploy/www/**'), `${t} should re-include deploy/www/**`);
185
- // Defensive secret ignores.
224
+ // Defensive secret ignores — any-extension credentials + common private-key formats,
225
+ // not just *credentials*.json (see isSecretLikeBasename in deploy-init.ts).
186
226
  assert.ok(di.includes('**/.env'), `${t} should ignore **/.env`);
187
- assert.ok(di.includes('**/*credentials*.json'), `${t} should ignore credentials json`);
227
+ assert.ok(di.includes('**/*credentials*'), `${t} should ignore credentials of any extension`);
228
+ assert.ok(di.includes('**/*.pem'), `${t} should ignore .pem files`);
229
+ assert.ok(di.includes('**/*.key'), `${t} should ignore .key files`);
230
+ assert.ok(di.includes('**/id_rsa'), `${t} should ignore id_rsa`);
188
231
  // skills/, scripts/, references/ must NOT be ignored lines.
189
232
  const ignored = di.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
190
233
  for (const keep of ['skills/', 'scripts/', 'references/', 'skills', 'scripts', 'references']) {
@@ -310,6 +353,30 @@ suite('deploy-init: renderTarget produces well-formed artifacts', () => {
310
353
  assert.ok(files['.env.auth.example'].includes('OAUTH2_PROXY_CLIENT_ID') && files['.env.auth.example'].includes('AGENTHIPPO_AUTH_JWT_SECRET'));
311
354
  // Note: service.yaml is a sed-template (${SERVICE}/${IMAGE}/${PUBLIC_HOSTNAME}/…), so it is
312
355
  // only valid YAML after the deploy script substitutes placeholders — same as the IAP template.
356
+ assert.ok(files['README.md'].includes('**Egress:**'), 'edge README documents the egress allowlist');
357
+ });
358
+ test('cloudrun --ingress edge-fed --model-routing gateway drops LiteLLM/OTEL — agent calls gateway.agenthippo.ai', () => {
359
+ const files = (0, deploy_init_1.renderTarget)('cloudrun', {
360
+ ...CTX, ingress: 'edge-fed', modelRouting: 'gateway', model: 'ah-auto',
361
+ idpUrl: 'https://auth.agenthippo.ai', cookieDomain: '.agenthippo.ai',
362
+ });
363
+ const svc = files['service.yaml'];
364
+ const sh = files['cloudrun-deploy.sh'];
365
+ assert.ok(svc.includes('name: caddy') && svc.includes('name: auth-broker') && svc.includes('name: agent'));
366
+ assert.ok(!svc.includes('name: litellm') && !svc.includes('name: otel-collector'), 'no Spotlight sidecars');
367
+ assert.ok(svc.includes('https://gateway.agenthippo.ai/v1'), 'agent points at org gateway');
368
+ assert.ok(!svc.includes('http://localhost:4443'), 'agent must not route through in-pod LiteLLM');
369
+ assert.ok(!files['otel-collector.yaml'], 'no otel config artifact');
370
+ assert.ok(!sh.includes('LITELLM_KEY') && !sh.includes('otel-config'), 'deploy skips litellm/otel secrets');
371
+ assert.ok(sh.includes('${SERVICE}-provider-key'), 'gateway key stored as provider-key secret');
372
+ assert.ok(files['.env.example'].includes('AGENTHIPPO_LITELLM_BASE_URL=https://gateway.agenthippo.ai/v1'));
373
+ assert.doesNotThrow(() => (0, child_process_1.execFileSync)('bash', ['-n', '-c', sh]));
374
+ const rendered = svc
375
+ .replace(/\$\{SERVICE\}/g, 'emoji-gateway')
376
+ .replace(/\$\{IMAGE\}/g, 'us-docker.pkg.dev/x/y/z:latest')
377
+ .replace(/\$\{PUBLIC_HOSTNAME\}/g, 'emoji.agenthippo.ai')
378
+ .replace(/\$\{ALLOWED_DOMAINS\}/g, 'agenthippo.ai');
379
+ assert.doesNotThrow(() => yaml.load(rendered), 'rendered gateway-direct service.yaml is valid YAML');
313
380
  });
314
381
  test('cloudrun --ingress edge-fed (A′) verifies the shared JWT — no oauth2-proxy, no per-service OAuth', () => {
315
382
  const files = (0, deploy_init_1.renderTarget)('cloudrun', {
@@ -361,6 +428,7 @@ suite('deploy-init: renderTarget produces well-formed artifacts', () => {
361
428
  .replace(/\$\{PUBLIC_HOSTNAME\}/g, 'cs.agenthippo.ai')
362
429
  .replace(/\$\{ALLOWED_DOMAINS\}/g, 'agenthippo.ai');
363
430
  assert.doesNotThrow(() => yaml.load(rendered), 'rendered service.yaml is valid YAML');
431
+ assert.ok(files['README.md'].includes('**Egress:**'), 'edge-fed README documents the egress allowlist');
364
432
  });
365
433
  test('renderAuthEnvExample fed variant has no Google OAuth client', () => {
366
434
  const out = (0, deploy_init_1.renderAuthEnvExample)({ hostname: 'cs.agenthippo.ai', fed: true, idpUrl: 'https://auth.agenthippo.ai', cookieDomain: '.agenthippo.ai' });
@@ -416,12 +484,98 @@ suite('deploy-init: renderTarget produces well-formed artifacts', () => {
416
484
  test('databricks-app pins AGENTHIPPO_IDENTITY_HEADER to x-forwarded-email', () => {
417
485
  const start = (0, deploy_init_1.renderTarget)('databricks-app', CTX)['start.js'];
418
486
  assert.ok(start.includes("AGENTHIPPO_IDENTITY_HEADER: 'x-forwarded-email'"), 'Databricks App start.js must pin identity to the workspace SSO header');
487
+ // Bootstrap escape hatch (analogous to Cloud Run ENABLE_IAP=0).
488
+ assert.ok(start.includes("AGENTHIPPO_PIN_IDENTITY === '0'"), 'Databricks pin must be skippable via AGENTHIPPO_PIN_IDENTITY=0');
419
489
  });
420
490
  test('spcs egress is limited to the model API via one EAI', () => {
421
491
  const sql = (0, deploy_init_1.renderTarget)('spcs', CTX)['deploy.sql'];
422
492
  assert.ok(sql.includes('api.anthropic.com:443'));
423
493
  assert.ok(/EXTERNAL ACCESS INTEGRATION/.test(sql));
424
494
  });
495
+ test('aws-ecs bare (no ingress) is a single-container task with no edge opinion', () => {
496
+ const files = (0, deploy_init_1.renderTarget)('aws-ecs', CTX);
497
+ assert.ok(!files['Caddyfile'], 'bare aws-ecs ships no Caddy');
498
+ assert.ok(!files['.env.auth.example'], 'bare aws-ecs has no local auth secrets');
499
+ const task = JSON.parse(files['task-definition.json']);
500
+ assert.strictEqual(task.networkMode, 'awsvpc');
501
+ assert.deepStrictEqual(task.requiresCompatibilities, ['FARGATE']);
502
+ assert.strictEqual(task.containerDefinitions.length, 1, 'exactly one container: the agent');
503
+ assert.strictEqual(task.containerDefinitions[0].name, 'agent');
504
+ assert.doesNotThrow(() => (0, child_process_1.execFileSync)('bash', ['-n', '-c', files['deploy.sh']]));
505
+ });
506
+ test('aws-ecs --ingress edge reuses the Cloud Run edge Caddyfile verbatim (shared sidecar identity layer)', () => {
507
+ const files = (0, deploy_init_1.renderTarget)('aws-ecs', { ...CTX, ingress: 'edge' });
508
+ const task = JSON.parse(files['task-definition.json']);
509
+ const names = task.containerDefinitions.map((c) => c.name);
510
+ assert.deepStrictEqual(names, ['caddy', 'oauth2-proxy', 'auth-broker', 'agent']);
511
+ assert.strictEqual(task.networkMode, 'awsvpc', 'one task, containers share localhost — same shape as Cloud Run sidecars');
512
+ assert.ok(files['Caddyfile'].includes('localhost:4180') && files['Caddyfile'].includes('localhost:9780'), 'localhost addressing (awsvpc shared network), not docker-DNS names');
513
+ assert.ok(files['.env.auth.example'].includes('OAUTH2_PROXY_CLIENT_ID'));
514
+ assert.doesNotThrow(() => (0, child_process_1.execFileSync)('bash', ['-n', '-c', files['deploy.sh']]));
515
+ });
516
+ test('aws-ecs --ingress edge-fed drops oauth2-proxy from the task and Caddyfile', () => {
517
+ const files = (0, deploy_init_1.renderTarget)('aws-ecs', { ...CTX, ingress: 'edge-fed', idpUrl: 'https://auth.corp.example' });
518
+ const task = JSON.parse(files['task-definition.json']);
519
+ const names = task.containerDefinitions.map((c) => c.name);
520
+ assert.deepStrictEqual(names, ['caddy', 'auth-broker', 'agent']);
521
+ assert.ok(!files['Caddyfile'].includes('localhost:4180'), 'no oauth2-proxy upstream in a federated Caddyfile');
522
+ assert.ok(files['Caddyfile'].includes('forward_auth localhost:9780'), 'verifies via auth-broker');
523
+ assert.ok(files['Caddyfile'].includes('https://auth.corp.example/oauth/browser-login'));
524
+ assert.ok(!files['.env.auth.example'].includes('OAUTH2_PROXY_CLIENT_ID'));
525
+ assert.doesNotThrow(() => (0, child_process_1.execFileSync)('bash', ['-n', '-c', files['deploy.sh']]));
526
+ });
527
+ test('azure-container-apps bare (no ingress) is a single-container app with no edge opinion', () => {
528
+ const files = (0, deploy_init_1.renderTarget)('azure-container-apps', CTX);
529
+ assert.ok(!files['Caddyfile'], 'bare azure-container-apps ships no Caddy');
530
+ assert.ok(!files['.env.auth.example'], 'bare azure-container-apps has no local auth secrets');
531
+ const spec = JSON.parse(files['containerapp.json']);
532
+ assert.strictEqual(spec.properties.template.containers.length, 1, 'exactly one container: the agent');
533
+ assert.strictEqual(spec.properties.template.containers[0].name, 'agent');
534
+ assert.strictEqual(spec.properties.configuration.ingress.external, true);
535
+ assert.doesNotThrow(() => (0, child_process_1.execFileSync)('bash', ['-n', '-c', files['deploy.sh']]));
536
+ });
537
+ test('azure-container-apps --ingress edge reuses the Cloud Run edge Caddyfile verbatim (shared sidecar identity layer)', () => {
538
+ const files = (0, deploy_init_1.renderTarget)('azure-container-apps', { ...CTX, ingress: 'edge' });
539
+ const spec = JSON.parse(files['containerapp.json']);
540
+ const names = spec.properties.template.containers.map((c) => c.name);
541
+ assert.deepStrictEqual(names, ['caddy', 'oauth2-proxy', 'auth-broker', 'agent']);
542
+ assert.ok(files['Caddyfile'].includes('localhost:4180') && files['Caddyfile'].includes('localhost:9780'), 'localhost addressing (containers in one replica share it), not docker-DNS names');
543
+ assert.ok(files['.env.auth.example'].includes('OAUTH2_PROXY_CLIENT_ID'));
544
+ assert.doesNotThrow(() => (0, child_process_1.execFileSync)('bash', ['-n', '-c', files['deploy.sh']]));
545
+ });
546
+ test('azure-container-apps --ingress edge-fed drops oauth2-proxy from the spec and Caddyfile', () => {
547
+ const files = (0, deploy_init_1.renderTarget)('azure-container-apps', { ...CTX, ingress: 'edge-fed', idpUrl: 'https://auth.corp.example' });
548
+ const spec = JSON.parse(files['containerapp.json']);
549
+ const names = spec.properties.template.containers.map((c) => c.name);
550
+ assert.deepStrictEqual(names, ['caddy', 'auth-broker', 'agent']);
551
+ assert.ok(!files['Caddyfile'].includes('localhost:4180'), 'no oauth2-proxy upstream in a federated Caddyfile');
552
+ assert.ok(files['Caddyfile'].includes('forward_auth localhost:9780'), 'verifies via auth-broker');
553
+ assert.ok(files['Caddyfile'].includes('https://auth.corp.example/oauth/browser-login'));
554
+ assert.ok(!files['.env.auth.example'].includes('OAUTH2_PROXY_CLIENT_ID'));
555
+ assert.doesNotThrow(() => (0, child_process_1.execFileSync)('bash', ['-n', '-c', files['deploy.sh']]));
556
+ });
557
+ // Governance (PLAN/deploy.md §6): every target must document what it can reach outbound —
558
+ // spcs enforces it via a network rule (tested above); every other target must at least name
559
+ // the host in its README so a security review doesn't have to reverse-engineer it.
560
+ test('every non-spcs target documents its egress allowlist in README', () => {
561
+ for (const target of deploy_init_1.DEPLOY_TARGETS) {
562
+ if (target === 'spcs') {
563
+ continue;
564
+ }
565
+ const readme = (0, deploy_init_1.renderTarget)(target, CTX)['README.md'];
566
+ assert.ok(readme?.includes('**Egress:**'), `${target} README must document its egress allowlist`);
567
+ assert.ok(readme.includes('api.anthropic.com:443'), `${target} README must name the model API host`);
568
+ }
569
+ });
570
+ // Governance (PLAN/deploy.md §6): a container that might ever be reached directly (bypassing
571
+ // its fronting proxy/Worker) must pin AGENTHIPPO_IDENTITY_HEADER so it can't be spoofed by an
572
+ // unauthenticated caller naming themselves via the same header name serve otherwise trusts as
573
+ // an unpinned alias. iap/databricks-app are covered by their own tests above; cloudflare here.
574
+ test('cloudflare pins AGENTHIPPO_IDENTITY_HEADER so a directly-reached container cannot be spoofed', () => {
575
+ const files = (0, deploy_init_1.renderTarget)('cloudflare', CTX);
576
+ assert.ok(files['Dockerfile'].includes('ENV AGENTHIPPO_IDENTITY_HEADER=x-agent-user'), 'cloudflare Dockerfile must pin identity to the header its own Worker sets');
577
+ assert.ok(files['README.md'].includes('Identity pin'), 'README must document the pin for whoever wires the Worker');
578
+ });
425
579
  test('vm target includes edge compose profile; agent has no SSO-bypass host port', () => {
426
580
  const files = (0, deploy_init_1.renderTarget)('vm', CTX);
427
581
  const compose = files['docker-compose.yml'];
@@ -468,6 +622,105 @@ suite('deploy-init: renderTarget produces well-formed artifacts', () => {
468
622
  assert.ok(files['Caddyfile']);
469
623
  assert.ok(files['.env.auth.example'].includes('OAUTH2_PROXY_PROVIDER'));
470
624
  });
625
+ for (const target of ['vm', 'compose']) {
626
+ test(`${target} --ingress edge-fed verifies the shared JWT — no oauth2-proxy, no per-agent OAuth`, () => {
627
+ const fedCtx = {
628
+ ...CTX, ingress: 'edge-fed', idpUrl: 'https://auth.corp.example', cookieDomain: '.corp.example',
629
+ };
630
+ const files = (0, deploy_init_1.renderTarget)(target, fedCtx);
631
+ const compose = files['docker-compose.yml'];
632
+ const caddy = files['Caddyfile'];
633
+ const auth = files['.env.auth.example'];
634
+ assert.ok(!/^\s*oauth2-proxy:/m.test(compose), `${target} edge-fed must not ship an oauth2-proxy container`);
635
+ assert.ok(compose.includes('depends_on: [agent, auth-broker]'), 'caddy no longer depends on oauth2-proxy');
636
+ assert.ok(!caddy.includes('oauth2-proxy:4180') && !caddy.includes('/oauth2/*'), 'no oauth2-proxy routes in a federated Caddyfile');
637
+ assert.ok(caddy.includes('forward_auth auth-broker:9780'), 'Caddy verifies via auth-broker');
638
+ assert.ok(caddy.includes('https://auth.corp.example/oauth/browser-login'), 'browser 401 redirects to the central IdP browser-login endpoint');
639
+ assert.ok(caddy.includes('return_to='), 'redirect carries a return_to for the round trip');
640
+ // Target-specific behavior (sticky lb / static file split) must survive federation —
641
+ // identity mode and deployment shape are independent axes.
642
+ assert.ok(caddy.includes('lb_policy header X-Agent-User'), 'sticky agent proxy unaffected by identity mode');
643
+ assert.ok(caddy.includes('root * /srv/www'), 'static file serving unaffected by identity mode');
644
+ assert.ok(auth.includes('AGENTHIPPO_AUTH_JWT_SECRET') && auth.includes('AGENTHIPPO_AUTH_IDP_URL'));
645
+ assert.ok(!auth.includes('OAUTH2_PROXY_CLIENT_ID'), 'federated auth env has no Google OAuth client');
646
+ });
647
+ }
648
+ for (const target of ['vm', 'compose']) {
649
+ test(`${target} --ingress edge-fed --colocate ships only the agent — no Caddy/oauth2-proxy/auth-broker`, () => {
650
+ const colocateCtx = {
651
+ ...CTX, ingress: 'edge-fed', idpUrl: 'https://auth.corp.example', colocate: true,
652
+ };
653
+ const files = (0, deploy_init_1.renderTarget)(target, colocateCtx);
654
+ const compose = files['docker-compose.yml'];
655
+ const snippet = files['agent-route.caddy'];
656
+ assert.ok(!files['Caddyfile'], `${target} colocate must not ship its own Caddyfile`);
657
+ assert.ok(!files['.env.auth.example'], `${target} colocate must not ship any local auth secrets`);
658
+ assert.ok(!/^\s*(caddy|oauth2-proxy|auth-broker):/m.test(compose), `${target} colocate compose must contain only the agent service`);
659
+ assert.ok(compose.includes('container_name: databricks-analyst-agent'), 'fixed, predictable container name');
660
+ assert.ok(compose.includes('networks: [colocate]') && compose.includes('external: true'), 'agent joins the IdP\'s external colocate network');
661
+ assert.ok(!compose.includes('ports:'), 'no host port — reached only via the IdP\'s Caddy, over the shared network');
662
+ assert.ok(snippet, 'route snippet generated for the operator to drop onto the IdP host');
663
+ // http:// (not https://) — must match the IdP Caddyfile's TLS model: auto_https is
664
+ // off there and TLS terminates in front, so an imported https:// block wouldn't
665
+ // match plaintext traffic forwarded from the front proxy.
666
+ assert.ok(snippet.includes(`http://${CTX.publicHostname} {`), 'snippet is a plain-HTTP site block, matching the IdP\'s own');
667
+ assert.ok(!snippet.includes(`https://${CTX.publicHostname}`), 'must not use https:// — the IdP Caddyfile has auto_https off');
668
+ assert.ok(snippet.includes('reverse_proxy databricks-analyst-agent:8080'), 'snippet proxies to the fixed container name');
669
+ assert.ok(snippet.includes('forward_auth auth-broker:9780'), 'identity is verified via the IdP\'s own auth-broker');
670
+ assert.ok(!snippet.includes('oauth2-proxy'), 'no oauth2-proxy — federated identity only');
671
+ assert.ok(snippet.includes('https://auth.corp.example/oauth/browser-login'), 'unauthenticated browsers redirect to the IdP');
672
+ const dataAccess = files['DATA-ACCESS.md'];
673
+ assert.ok(!dataAccess.includes('AGENTHIPPO_AUTH_JWT_SECRET'), 'colocated agent holds no JWT secret of its own');
674
+ assert.ok(!dataAccess.includes('**oauth2-proxy**'), 'colocated agent runs no oauth2-proxy');
675
+ assert.ok(files['www/index.html'], 'colocated agent ships browser landing page at GET /');
676
+ assert.ok(files['Dockerfile'].includes('--www-dir'), 'serve enables static chat UI after SSO');
677
+ });
678
+ }
679
+ test('colocate + edgeTurnConnector emits the P1 edge turn-registration block (obo-inprogress §2)', () => {
680
+ const ctx = {
681
+ ...CTX, ingress: 'edge-fed', idpUrl: 'https://auth.corp.example', colocate: true,
682
+ edgeTurnConnector: 'databricks-analyst-connector:9091',
683
+ };
684
+ const snippet = (0, deploy_init_1.renderTarget)('vm', ctx)['agent-route.caddy'];
685
+ // Hygiene: a client-supplied nonce header must never pass through.
686
+ assert.ok(snippet.includes('request_header -X-Agent-Turn-Nonce'), 'inbound X-Agent-Turn-Nonce is stripped at the edge');
687
+ // Only POST chat turns register.
688
+ assert.ok(snippet.includes('@turnStart'), 'chat-turn matcher present');
689
+ assert.ok(/@turnStart\s*\{\s*\n\s*method POST\s*\n\s*path \/v1\/chat \/v1\/chat\/stream/m.test(snippet), '@turnStart matches POST /v1/chat and /v1/chat/stream only');
690
+ // The registration hop itself: forward_auth to the connector control API, nonce copied
691
+ // onto the proxied request, register token from the EDGE stack env (never the agent).
692
+ assert.ok(snippet.includes('forward_auth @turnStart databricks-analyst-connector:9091'), 'turn registration targets the connector control API');
693
+ assert.ok(snippet.includes('uri /edge/turn'), 'registration path is /edge/turn');
694
+ assert.ok(snippet.includes('copy_headers X-Agent-Turn-Nonce'), 'minted nonce copied onto the request');
695
+ assert.ok(snippet.includes('header_up Authorization "Bearer {$DATABRICKS_ANALYST_DATA_PLANE_TOKEN}"'), 'register token comes from the derived edge-stack env var');
696
+ // Ordering (the Caddy footgun the hand-built overlay documents): two forward_auth in one
697
+ // handle do NOT run in source order unless wrapped in route {} — identity must run first
698
+ // so X-Agent-User exists when the sidecar mints the nonce.
699
+ assert.ok(snippet.includes('route {'), 'gated sequence wrapped in route {} to force source order');
700
+ const apiBlock = snippet.slice(snippet.indexOf('handle @apiWithAuth'));
701
+ const identityIdx = apiBlock.indexOf('forward_auth auth-broker:9780');
702
+ const turnIdx = apiBlock.indexOf('forward_auth @turnStart');
703
+ const proxyIdx = apiBlock.indexOf('reverse_proxy databricks-analyst-agent:8080');
704
+ assert.ok(identityIdx >= 0 && turnIdx > identityIdx && proxyIdx > turnIdx, 'order inside route {}: identity → turn registration → proxy');
705
+ // Both gated paths carry the hop: Bearer API clients AND cookie-auth browser clients.
706
+ assert.strictEqual(snippet.split('uri /edge/turn').length - 1, 2, 'turn registration on both the @apiWithAuth and browser catch-all paths');
707
+ // The operator README says where the token goes (IdP stack env, never the agent).
708
+ const readme = (0, deploy_init_1.renderTarget)('vm', ctx)['README.md'];
709
+ assert.ok(readme.includes('DATABRICKS_ANALYST_DATA_PLANE_TOKEN'), 'README documents the edge-stack register token env var');
710
+ });
711
+ test('without edgeTurnConnector the route snippet is unchanged — no /edge/turn, no route {}', () => {
712
+ const ctx = {
713
+ ...CTX, ingress: 'edge-fed', idpUrl: 'https://auth.corp.example', colocate: true,
714
+ };
715
+ const snippet = (0, deploy_init_1.renderTarget)('vm', ctx)['agent-route.caddy'];
716
+ assert.ok(!snippet.includes('/edge/turn') && !snippet.includes('@turnStart')
717
+ && !snippet.includes('X-Agent-Turn-Nonce') && !snippet.includes('route {'), 'turn registration is strictly opt-in; default output must not change');
718
+ });
719
+ test('--colocate is a no-op without --ingress edge-fed (still ships the normal self-issued edge)', () => {
720
+ const files = (0, deploy_init_1.renderTarget)('vm', { ...CTX, colocate: true });
721
+ assert.ok(files['Caddyfile'], 'no ingress edge-fed → colocate does not apply, normal vm artifacts render');
722
+ assert.ok(!files['agent-route.caddy']);
723
+ });
471
724
  test('agent service does not load .env.auth; broker + oauth2-proxy do', () => {
472
725
  const compose = (0, deploy_init_1.renderTarget)('compose', CTX)['docker-compose.yml'];
473
726
  // The agent runs model-driven tool calls — it must never receive the token-signing key.
@@ -483,7 +736,7 @@ suite('deploy-init: deployCommand stages a self-contained pack', () => {
483
736
  setup(() => {
484
737
  pack = fs.mkdtempSync(path.join(os.tmpdir(), 'ah-pack-'));
485
738
  staging = fs.mkdtempSync(path.join(os.tmpdir(), 'ah-stage-'));
486
- fs.writeFileSync(path.join(pack, 'agent.yaml'), 'apiVersion: agenthippo.ai/v1\nkind: Agent\nmetadata:\n name: cs-test\nspec:\n engine: claude\n model: litellm/gpt-5.5\n');
739
+ fs.writeFileSync(path.join(pack, 'agent.yaml'), 'apiVersion: agenthippo.ai/v1\nkind: Agent\nmetadata:\n name: cs-test\n version: 0.1.0\nspec:\n engine: claude\n model: litellm/gpt-5.5\n');
487
740
  fs.writeFileSync(path.join(pack, 'AGENTS.md'), '# cs-test\n');
488
741
  fs.mkdirSync(path.join(pack, 'skills', 'demo', 'scripts'), { recursive: true });
489
742
  fs.writeFileSync(path.join(pack, 'skills', 'demo', 'scripts', 'run.sh'), 'echo hi\n');
@@ -493,6 +746,8 @@ suite('deploy-init: deployCommand stages a self-contained pack', () => {
493
746
  fs.writeFileSync(path.join(pack, 'node_modules', 'foo', 'index.js'), 'x');
494
747
  fs.writeFileSync(path.join(pack, '.env'), 'SECRET=1\n');
495
748
  fs.writeFileSync(path.join(pack, 'my-credentials.json'), '{}\n');
749
+ fs.writeFileSync(path.join(pack, 'service-account.pem'), 'FAKE PEM\n');
750
+ fs.writeFileSync(path.join(pack, 'id_rsa'), 'FAKE PRIVATE KEY\n');
496
751
  fs.mkdirSync(path.join(pack, 'deploy'), { recursive: true });
497
752
  fs.writeFileSync(path.join(pack, 'deploy', 'leftover.yaml'), 'stale\n');
498
753
  });
@@ -510,6 +765,8 @@ suite('deploy-init: deployCommand stages a self-contained pack', () => {
510
765
  assert.ok(!fs.existsSync(path.join(staging, 'node_modules')), 'node_modules must not be copied into staging');
511
766
  assert.ok(!fs.existsSync(path.join(staging, '.env')), '.env must not be copied into staging');
512
767
  assert.ok(!fs.existsSync(path.join(staging, 'my-credentials.json')), 'credentials json must not be copied');
768
+ assert.ok(!fs.existsSync(path.join(staging, 'service-account.pem')), '.pem key must not be copied');
769
+ assert.ok(!fs.existsSync(path.join(staging, 'id_rsa')), 'id_rsa key must not be copied');
513
770
  // The source pack's own deploy/ is not copied (the generator owns staging/deploy/).
514
771
  assert.ok(!fs.existsSync(path.join(staging, 'deploy', 'leftover.yaml')), 'source deploy/ must not be copied');
515
772
  // Generated artifacts under staging/deploy/.
@@ -529,6 +786,25 @@ suite('deploy-init: deployCommand stages a self-contained pack', () => {
529
786
  assert.ok(fs.existsSync(path.join(staging, 'agent.yaml')), 'packdir flag resolved the pack');
530
787
  assert.ok(fs.existsSync(path.join(staging, 'deploy', 'Dockerfile')), 'artifacts generated from packdir pack');
531
788
  });
789
+ test('--blueprint team-pilot --platform cloudrun resolves to IAP (no Caddyfile, no oauth2-proxy)', async () => {
790
+ await (0, deploy_1.deployCommand)('init', pack, {
791
+ target: 'vm', blueprint: 'team-pilot', platform: 'cloudrun', out: staging, port: 8080, force: true,
792
+ });
793
+ assert.ok(fs.existsSync(path.join(staging, 'deploy', 'cloudrun-deploy.sh')), 'cloudrun artifacts generated');
794
+ assert.ok(!fs.existsSync(path.join(staging, 'deploy', 'Caddyfile')), 'team-pilot on cloudrun is IAP — no Caddy edge');
795
+ const sh = fs.readFileSync(path.join(staging, 'deploy', 'cloudrun-deploy.sh'), 'utf8');
796
+ assert.ok(sh.includes('--iap'), 'blueprint-resolved --ingress iap reaches the actual IAP deploy path');
797
+ });
798
+ test('--blueprint enterprise-fleet --platform vm resolves to a federated (no oauth2-proxy) edge', async () => {
799
+ await (0, deploy_1.deployCommand)('init', pack, {
800
+ target: 'cloudrun', blueprint: 'enterprise-fleet', platform: 'vm', out: staging, port: 8080, force: true,
801
+ idpUrl: 'https://auth.corp.example',
802
+ });
803
+ const caddy = fs.readFileSync(path.join(staging, 'deploy', 'Caddyfile'), 'utf8');
804
+ const compose = fs.readFileSync(path.join(staging, 'deploy', 'docker-compose.yml'), 'utf8');
805
+ assert.ok(!caddy.includes('oauth2-proxy:4180'), 'enterprise-fleet federates — no oauth2-proxy in the Caddyfile');
806
+ assert.ok(!/^\s*oauth2-proxy:/m.test(compose), 'enterprise-fleet federates — no oauth2-proxy container');
807
+ });
532
808
  });
533
809
  suite('deploy-init: copyPackToStaging dereferences symlinks + skips broken ones', () => {
534
810
  let pack;
@@ -536,7 +812,7 @@ suite('deploy-init: copyPackToStaging dereferences symlinks + skips broken ones'
536
812
  setup(() => {
537
813
  pack = fs.mkdtempSync(path.join(os.tmpdir(), 'ah-pack-'));
538
814
  staging = fs.mkdtempSync(path.join(os.tmpdir(), 'ah-stage-'));
539
- fs.writeFileSync(path.join(pack, 'agent.yaml'), 'apiVersion: agenthippo.ai/v1\nkind: Agent\nmetadata:\n name: link-test\nspec:\n engine: claude\n');
815
+ fs.writeFileSync(path.join(pack, 'agent.yaml'), 'apiVersion: agenthippo.ai/v1\nkind: Agent\nmetadata:\n name: link-test\n version: 0.1.0\nspec:\n engine: claude\n');
540
816
  // Real skill content + a shared file the skill links to (relative symlink → resolved on copy).
541
817
  fs.mkdirSync(path.join(pack, 'skills', 'demo'), { recursive: true });
542
818
  fs.writeFileSync(path.join(pack, 'skills', 'demo', 'REAL.md'), '# real content\n');
@@ -561,3 +837,18 @@ suite('deploy-init: copyPackToStaging dereferences symlinks + skips broken ones'
561
837
  assert.ok(broken.includes('skills/broken/SKILL.md'), 'broken symlink reported in returned list');
562
838
  });
563
839
  });
840
+ suite('deploy-init: enterprise-deploy/idp/ (customer 0) stays in sync with `agenthippo idp init`', () => {
841
+ // PLAN/deploy.md — AgentHippo's own auth.agenthippo.ai instance is "customer 0": it must be
842
+ // stood up with the exact same `agenthippo idp init` a real enterprise customer would use,
843
+ // not a bespoke hand-maintained copy. This fails loudly if the checked-in files drift from
844
+ // what the generator produces for the same hostname (regenerate with:
845
+ // `agenthippo idp init --hostname auth.agenthippo.ai --out enterprise-deploy/idp --force`).
846
+ test('checked-in files are byte-identical to renderIdpArtifacts({ hostname: "auth.agenthippo.ai" })', () => {
847
+ const idpDir = path.join(__dirname, '..', '..', '..', '..', 'enterprise-deploy', 'idp');
848
+ const files = (0, idp_init_1.renderIdpArtifacts)({ hostname: 'auth.agenthippo.ai' });
849
+ for (const [rel, expected] of Object.entries(files)) {
850
+ const actual = fs.readFileSync(path.join(idpDir, rel), 'utf8');
851
+ assert.strictEqual(actual, expected, `enterprise-deploy/idp/${rel} is stale — regenerate with: agenthippo idp init --hostname auth.agenthippo.ai --out enterprise-deploy/idp --force`);
852
+ }
853
+ });
854
+ });
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Egress broker tests (secure-obo-design §7.6)
4
+ *
5
+ * Boots the real broker (loopback HTTP + shared TurnRegistry) against a temp workspace whose
6
+ * gateway-outbound handle store is seeded like the bridge would seed it, then drives it exactly
7
+ * as the agent-facing `gateway send` broker mode does: bearer nonce, `{message, to?}` body.
8
+ *
9
+ * The `send` function is a spy — no live OpenClaw gateway — so we assert on WHAT the broker
10
+ * resolved and WHETHER it delivered, not on the platform call itself.
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 data_plane_1 = require("../cli/data-plane");
52
+ const outboundPaths_1 = require("../gateway/outboundPaths");
53
+ const AGENT_ID = 'demo-agent';
54
+ /** Seed a handle store the way the bridge's upsertGatewayOutboundHandle would. */
55
+ function seedStore(workspaceRoot, storeKey, handle) {
56
+ const p = (0, outboundPaths_1.getGatewayOutboundFilePath)(workspaceRoot, (0, outboundPaths_1.safeGatewaySessionDirId)(storeKey));
57
+ fs.mkdirSync(path.dirname(p), { recursive: true });
58
+ fs.writeFileSync(p, JSON.stringify({
59
+ version: 1,
60
+ lastByAgent: { [AGENT_ID]: handle },
61
+ targetsByAgent: { [AGENT_ID]: { [handle.target]: handle } },
62
+ }), 'utf8');
63
+ }
64
+ function handle(target, overrides = {}) {
65
+ return {
66
+ agentId: AGENT_ID, channel: 'telegram', target,
67
+ firstSeenAt: 1, lastSeenAt: 2, inboundCount: 1, ...overrides,
68
+ };
69
+ }
70
+ async function post(url, nonce, body) {
71
+ const res = await fetch(url, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json', ...(nonce !== undefined ? { Authorization: `Bearer ${nonce}` } : {}) },
74
+ body: JSON.stringify(body),
75
+ });
76
+ return { status: res.status, json: await res.json().catch(() => ({})) };
77
+ }
78
+ suite('egress broker: server-side resolution + fail-closed (§7.6)', function () {
79
+ this.timeout(15_000);
80
+ let workspaceRoot;
81
+ let registry;
82
+ let broker;
83
+ let sends;
84
+ suiteSetup(async () => {
85
+ workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'egress-ws-'));
86
+ registry = new data_plane_1.TurnRegistry();
87
+ sends = [];
88
+ broker = await (0, data_plane_1.startEgressBroker)({
89
+ registry,
90
+ workspaceRoot,
91
+ agentId: AGENT_ID,
92
+ send: async (h, message, media) => {
93
+ sends.push({ handle: h, message, media });
94
+ return { ok: true, via: 'spy' };
95
+ },
96
+ });
97
+ });
98
+ suiteTeardown(async () => {
99
+ await broker?.close();
100
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
101
+ });
102
+ test('binds loopback only', () => {
103
+ assert.ok(broker.url.startsWith('http://127.0.0.1:'), broker.url);
104
+ });
105
+ test('reply-to-sender: a live nonce delivers to the session\'s bound handle', async () => {
106
+ seedStore(workspaceRoot, 'chat-1', handle('telegram:dm:100'));
107
+ const turn = registry.register({ userId: 'u@corp.com', sessionKey: 's', egressStoreKey: 'chat-1' });
108
+ try {
109
+ const { status, json } = await post(broker.url, turn.nonce, { message: 'hello' });
110
+ assert.strictEqual(status, 200);
111
+ assert.strictEqual(json.ok, true);
112
+ assert.strictEqual(sends.at(-1)?.handle.target, 'telegram:dm:100');
113
+ assert.strictEqual(sends.at(-1)?.message, 'hello');
114
+ }
115
+ finally {
116
+ turn.release();
117
+ }
118
+ });
119
+ test('`to` may only SELECT among bound handles — a new destination is refused, no send', async () => {
120
+ seedStore(workspaceRoot, 'chat-2', handle('telegram:dm:200'));
121
+ const turn = registry.register({ userId: 'u@corp.com', sessionKey: 's', egressStoreKey: 'chat-2' });
122
+ const before = sends.length;
123
+ try {
124
+ const { status, json } = await post(broker.url, turn.nonce, { message: 'exfil', to: 'telegram:dm:ATTACKER' });
125
+ assert.strictEqual(status, 404, 'a target not bound to this session must not resolve');
126
+ assert.strictEqual(json.ok, false);
127
+ assert.strictEqual(sends.length, before, 'nothing may be delivered when resolution fails');
128
+ }
129
+ finally {
130
+ turn.release();
131
+ }
132
+ });
133
+ test('off-turn / released nonce → 401, fail closed, no send', async () => {
134
+ seedStore(workspaceRoot, 'chat-3', handle('telegram:dm:300'));
135
+ const turn = registry.register({ userId: 'u@corp.com', sessionKey: 's', egressStoreKey: 'chat-3' });
136
+ const nonce = turn.nonce;
137
+ turn.release();
138
+ const before = sends.length;
139
+ const { status, json } = await post(broker.url, nonce, { message: 'stale' });
140
+ assert.strictEqual(status, 401);
141
+ assert.strictEqual(json.ok, false);
142
+ assert.strictEqual(sends.length, before);
143
+ });
144
+ test('unknown / missing nonce → 401', async () => {
145
+ assert.strictEqual((await post(broker.url, 'never-issued-nonce', { message: 'x' })).status, 401);
146
+ assert.strictEqual((await post(broker.url, undefined, { message: 'x' })).status, 401);
147
+ });
148
+ test('a turn with no egress store key (bare HTTP data turn) cannot send → 403', async () => {
149
+ const turn = registry.register({ userId: 'u@corp.com', sessionKey: 's' }); // no egressStoreKey
150
+ try {
151
+ const { status } = await post(broker.url, turn.nonce, { message: 'x' });
152
+ assert.strictEqual(status, 403);
153
+ }
154
+ finally {
155
+ turn.release();
156
+ }
157
+ });
158
+ test('one user cannot address another session\'s bound handle (server-side scoping)', async () => {
159
+ // Alice's session is bound to her peer; Bob's turn (his own store key) must not reach it.
160
+ seedStore(workspaceRoot, 'alice-chat', handle('telegram:dm:ALICE'));
161
+ const bob = registry.register({ userId: 'bob@corp.com', sessionKey: 'sb', egressStoreKey: 'bob-chat' }); // no store seeded
162
+ const before = sends.length;
163
+ try {
164
+ const { status } = await post(broker.url, bob.nonce, { message: 'to alice?', to: 'telegram:dm:ALICE' });
165
+ assert.strictEqual(status, 404, 'Bob\'s turn resolves only against Bob\'s own session store');
166
+ assert.strictEqual(sends.length, before);
167
+ }
168
+ finally {
169
+ bob.release();
170
+ }
171
+ });
172
+ });
@@ -45,6 +45,10 @@ suite('fleetDisplayUtils', () => {
45
45
  assert.strictEqual((0, fleetDisplayUtils_1.inferFleetEngineId)('agentide-cursor::auto'), 'cursor');
46
46
  assert.strictEqual((0, fleetDisplayUtils_1.formatFleetEngineLabel)('agentide-cursor::auto'), 'Cursor');
47
47
  });
48
+ test('formats agenthippo auto model label', () => {
49
+ assert.strictEqual((0, fleetDisplayUtils_1.formatFleetModelLabel)('ah-auto', ''), 'Auto');
50
+ assert.strictEqual((0, fleetDisplayUtils_1.formatFleetModelLabel)('agenthippo::ah-auto', ''), 'Auto');
51
+ });
48
52
  test('enriches in_progress row from schedule hint', () => {
49
53
  const hints = (0, fleetDisplayUtils_1.buildScheduleHintsByChatSessionId)([{
50
54
  id: 'sched-1',
@@ -328,6 +328,34 @@ suite('GatewayClient - events', () => {
328
328
  const receivedParams = receivedFrame.params;
329
329
  assert.strictEqual(receivedParams.message, 'Hello from WhatsApp');
330
330
  });
331
+ test('drops frames larger than the cap instead of parsing them', async () => {
332
+ const client = new client_js_1.GatewayClient({ url: 'ws://localhost:18789' });
333
+ const clientInfo = createClientInfo();
334
+ const connectPromise = client.connect(clientInfo);
335
+ await new Promise((resolve) => setTimeout(resolve, 10));
336
+ const connectFrame = JSON.parse(mockWs.sentMessages[0]);
337
+ simulateHelloOk(connectFrame.id);
338
+ await connectPromise;
339
+ let receivedFrame;
340
+ let errorMessage = '';
341
+ client.on('request', (frame) => {
342
+ receivedFrame = frame;
343
+ });
344
+ client.on('error', (err) => {
345
+ errorMessage = err.message;
346
+ });
347
+ mockWs.simulateMessage({
348
+ type: 'req',
349
+ id: 'server-req-huge',
350
+ method: 'agent.run',
351
+ params: {
352
+ message: 'x'.repeat(client_js_1.MAX_GATEWAY_FRAME_BYTES + 1),
353
+ idempotencyKey: 'idem-huge',
354
+ },
355
+ });
356
+ assert.strictEqual(receivedFrame, undefined, 'Oversized frame must not be dispatched');
357
+ assert.ok(errorMessage.includes('exceeds'), `Expected a size error, got: ${errorMessage}`);
358
+ });
331
359
  test('emits event:name for Gateway events', async () => {
332
360
  const client = new client_js_1.GatewayClient({ url: 'ws://localhost:18789' });
333
361
  const clientInfo = createClientInfo();