@agenthippo-ai/agenthippo 0.1.1 → 0.2.3-dataplane

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 (64) 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/webview/panel.js +290 -80
  5. package/out/analytics/sessionEvents.js +21 -9
  6. package/out/auth/brokerClient.js +1 -1
  7. package/out/auth/onboardingPrompt.js +112 -0
  8. package/out/auth/provider.js +1 -1
  9. package/out/auth-broker/config.js +8 -3
  10. package/out/auth-broker/server.js +36 -0
  11. package/out/cli/commands/ask.js +3 -0
  12. package/out/cli/commands/chat.js +3 -0
  13. package/out/cli/commands/deploy.js +49 -10
  14. package/out/cli/commands/gateway.js +54 -0
  15. package/out/cli/commands/idp.js +85 -0
  16. package/out/cli/commands/serve.js +315 -55
  17. package/out/cli/credential-context.js +42 -80
  18. package/out/cli/data-plane/broker.js +78 -0
  19. package/out/cli/data-plane/connector-host.js +121 -0
  20. package/out/cli/data-plane/egress.js +189 -0
  21. package/out/cli/data-plane/gateway.js +195 -0
  22. package/out/cli/data-plane/index.js +137 -0
  23. package/out/cli/data-plane/registry.js +80 -0
  24. package/out/cli/deploy/blueprints/registry.js +84 -0
  25. package/out/cli/deploy/layers/identity.js +91 -0
  26. package/out/cli/deploy-init.js +1105 -335
  27. package/out/cli/idp-init.js +303 -0
  28. package/out/cli/index.js +37 -7
  29. package/out/cli/pack-loader.js +37 -2
  30. package/out/cli/runtime.js +10 -3
  31. package/out/cli/serve-hardening.js +60 -1
  32. package/out/cli/state-sync.js +16 -15
  33. package/out/engines/claude.js +26 -5
  34. package/out/engines/codex.js +24 -5
  35. package/out/engines/custom.js +46 -12
  36. package/out/extension.js +2 -0
  37. package/out/gateway/outboundSend.js +6 -6
  38. package/out/headless/index.js +24 -9
  39. package/out/live-probe/connector.mjs +8 -0
  40. package/out/live-probe/env-report.mjs +8 -0
  41. package/out/mcp.js +85 -67
  42. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +14 -0
  43. package/out/shared/models.js +1 -1
  44. package/out/shared/providers.js +2 -2
  45. package/out/test/analytics.test.js +42 -17
  46. package/out/test/auth-broker.test.js +53 -0
  47. package/out/test/blueprint-registry.test.js +105 -0
  48. package/out/test/credential-context.test.js +23 -28
  49. package/out/test/custom-env-layering.test.js +151 -0
  50. package/out/test/data-plane-engine-wiring.test.js +250 -0
  51. package/out/test/data-plane.test.js +244 -0
  52. package/out/test/deploy-init.test.js +255 -4
  53. package/out/test/egress-broker.test.js +172 -0
  54. package/out/test/fleetDisplayUtils.test.js +4 -0
  55. package/out/test/idp-init.test.js +151 -0
  56. package/out/test/mcp-obo.test.js +52 -27
  57. package/out/test/mcp.test.js +43 -3
  58. package/out/test/model-routing.test.js +133 -0
  59. package/out/test/serve-gateway-turn.test.js +267 -0
  60. package/out/test/serve-hardening.test.js +80 -0
  61. package/out/test/serveClient.test.js +32 -0
  62. package/out/test/state-sync-peer.test.js +18 -5
  63. package/out/utils.js +27 -11
  64. package/package.json +1 -1
@@ -39,12 +39,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.AGENTHIPPO_CLI_NPM_VERSION_FALLBACK = exports.AGENTHIPPO_CLI_NPM_PACKAGE = exports.DEPLOY_PUBLIC_HOSTNAME = exports.DEFAULT_DEPLOY_PUBLIC_HOSTNAME = exports.DEPLOY_TARGETS = void 0;
40
40
  exports.isDeployTarget = isDeployTarget;
41
41
  exports.defaultRuntimeImage = defaultRuntimeImage;
42
+ exports.packAttributionBody = packAttributionBody;
43
+ exports.packAttributionComment = packAttributionComment;
44
+ exports.withPackAttribution = withPackAttribution;
42
45
  exports.serveCmdArgs = serveCmdArgs;
43
46
  exports.resolveAgentHippoCliNpmVersion = resolveAgentHippoCliNpmVersion;
44
47
  exports.resolveDeployPublicHostname = resolveDeployPublicHostname;
45
48
  exports.scanEnvRefs = scanEnvRefs;
46
49
  exports.renderEnvExample = renderEnvExample;
47
50
  exports.renderAuthEnvExample = renderAuthEnvExample;
51
+ exports.renderAuthBrokerDockerfile = renderAuthBrokerDockerfile;
52
+ exports.isSecretLikeBasename = isSecretLikeBasename;
48
53
  exports.renderTarget = renderTarget;
49
54
  /**
50
55
  * `agenthippo deploy init` — generator for platform-native deploy artifacts
@@ -65,15 +70,58 @@ exports.renderTarget = renderTarget;
65
70
  const fs = __importStar(require("fs"));
66
71
  const path = __importStar(require("path"));
67
72
  const serve_hardening_1 = require("./serve-hardening");
68
- exports.DEPLOY_TARGETS = ['vm', 'compose', 'cloudrun', 'databricks-app', 'spcs', 'cloudflare'];
73
+ const identity_1 = require("./deploy/layers/identity");
74
+ exports.DEPLOY_TARGETS = ['vm', 'compose', 'cloudrun', 'databricks-app', 'spcs', 'cloudflare', 'aws-ecs', 'azure-container-apps'];
69
75
  function isDeployTarget(value) {
70
76
  return exports.DEPLOY_TARGETS.includes(value);
71
77
  }
72
78
  /** Default runtime image for a pack engine (cursor needs the /engines layer). */
73
79
  function defaultRuntimeImage(engine) {
74
80
  return engine.trim().toLowerCase() === 'cursor'
75
- ? 'ghcr.io/agenthippo/runtime:latest-cursor'
76
- : 'ghcr.io/agenthippo/runtime:latest';
81
+ ? 'ghcr.io/agenthippoai/runtime:latest-cursor'
82
+ : 'ghcr.io/agenthippoai/runtime:latest';
83
+ }
84
+ /** Provenance line for generated deploy artifacts (pack-standard WS2). */
85
+ function packAttributionBody(ctx) {
86
+ return `Generated from Agent Pack ${ctx.packName}@${ctx.packVersion} — packs.agenthippo.ai`;
87
+ }
88
+ function packAttributionComment(ctx, kind = 'hash') {
89
+ const body = packAttributionBody(ctx);
90
+ if (kind === 'html') {
91
+ return `<!-- ${body} -->`;
92
+ }
93
+ if (kind === 'sql') {
94
+ return `-- ${body}`;
95
+ }
96
+ return `# ${body}`;
97
+ }
98
+ /**
99
+ * Prepend pack provenance to a generated file. Skips JSON/TOML/JSONC (no comment syntax).
100
+ * Keeps shebangs first. Idempotent if packs.agenthippo.ai is already present near the top.
101
+ */
102
+ function withPackAttribution(ctx, relPath, content) {
103
+ const head = content.slice(0, 400);
104
+ if (head.includes('packs.agenthippo.ai')) {
105
+ return content;
106
+ }
107
+ const base = path.basename(relPath);
108
+ const ext = path.extname(base).toLowerCase();
109
+ // Structured configs: don't invent comment syntax.
110
+ if (ext === '.json' || ext === '.jsonc' || base === 'wrangler.toml' || base === 'package.json') {
111
+ return content;
112
+ }
113
+ const kind = ext === '.html' || ext === '.md' ? 'html' :
114
+ ext === '.sql' ? 'sql' :
115
+ 'hash';
116
+ const line = packAttributionComment(ctx, kind);
117
+ if (content.startsWith('#!')) {
118
+ const nl = content.indexOf('\n');
119
+ if (nl === -1) {
120
+ return `${content}\n${line}\n`;
121
+ }
122
+ return `${content.slice(0, nl + 1)}${line}\n${content.slice(nl + 1)}`;
123
+ }
124
+ return `${line}\n${content}`;
77
125
  }
78
126
  /** `serve` argv baked into generated Dockerfiles (matches agent.yaml engine/model). */
79
127
  function serveCmdArgs(ctx, opts) {
@@ -192,8 +240,11 @@ function renderEnvExample(vars, opts) {
192
240
  if (isCursor) {
193
241
  lines.push('# Cursor engine (pack default model: auto):', 'CURSOR_API_KEY= # https://cursor.com/settings → API keys');
194
242
  }
243
+ else if (opts?.gatewayDirect) {
244
+ lines.push('# Gateway-direct routing (agent → gateway.agenthippo.ai — no in-pod LiteLLM sidecar):', 'AGENTHIPPO_LITELLM_BASE_URL=https://gateway.agenthippo.ai/v1', 'AGENTHIPPO_LITELLM_API_KEY= # ahgw_… from create-gateway-key on the gateway host');
245
+ }
195
246
  else {
196
- lines.push('# Model credential — pick ONE path (serve-v3 §10.2):', '# ANTHROPIC_API_KEY= # direct Anthropic', '# CLAUDE_CODE_USE_BEDROCK=1 # existing AWS Bedrock entitlement (no new key)', '# CLAUDE_CODE_USE_VERTEX=1 # existing GCP Vertex entitlement (no new key)', 'ANTHROPIC_API_KEY=');
247
+ lines.push('# Model credential — pick ONE path (serve-v3 §10.2):', '# AgentHippo hosted gateway (colocated agents / CI — long-lived service key):', '# AGENTHIPPO_LITELLM_BASE_URL=https://gateway.agenthippo.ai/v1', '# AGENTHIPPO_LITELLM_API_KEY=ahgw_… # bun run create-gateway-key on gateway host', '# Direct Anthropic:', 'ANTHROPIC_API_KEY=', '# CLAUDE_CODE_USE_BEDROCK=1 # existing AWS Bedrock entitlement (no new key)', '# CLAUDE_CODE_USE_VERTEX=1 # existing GCP Vertex entitlement (no new key)');
197
248
  }
198
249
  if (vars.length > 0) {
199
250
  lines.push('', '# Pack-referenced secrets (scanned from ${env:...}):');
@@ -247,6 +298,7 @@ function renderAuthEnvExample(opts) {
247
298
  '# one Google OAuth client live only at the IdP.',
248
299
  ].join('\n') + '\n';
249
300
  }
301
+ const cookieDomain = opts?.cookieDomain || '.agenthippo.ai';
250
302
  return [
251
303
  '# Generated by `agenthippo deploy init`. Copy to .env.auth and fill in.',
252
304
  '# Loaded only by auth-broker + oauth2-proxy (docker-compose.yml). Never given to the agent.',
@@ -261,8 +313,8 @@ function renderAuthEnvExample(opts) {
261
313
  'OAUTH2_PROXY_EMAIL_DOMAINS=example.com',
262
314
  'OAUTH2_PROXY_REVERSE_PROXY=true',
263
315
  `OAUTH2_PROXY_REDIRECT_URL=https://${hostname}/oauth2/callback`,
264
- '# Federated SSO (A′) — set when this edge is the central IdP for *.agenthippo.ai agents:',
265
- 'OAUTH2_PROXY_COOKIE_DOMAINS=.agenthippo.ai # scope the oauth2-proxy session cookie to all subdomains',
316
+ '# Federated SSO (A′) — set when this edge is the central IdP for a fleet of agents:',
317
+ `OAUTH2_PROXY_COOKIE_DOMAINS=${cookieDomain} # scope the oauth2-proxy session cookie to all subdomains`,
266
318
  '',
267
319
  '# IDE auth broker (team bootstrap for store + LiteLLM routing)',
268
320
  'AGENTHIPPO_AUTH_JWT_SECRET= # openssl rand -hex 32',
@@ -270,7 +322,7 @@ function renderAuthEnvExample(opts) {
270
322
  'AGENTHIPPO_AUTH_ALLOWED_DOMAINS=example.com',
271
323
  'AGENTHIPPO_AUTH_DEV=0 # 1 enables /oauth/dev/login (tests only; never in production)',
272
324
  'AGENTHIPPO_AUTH_COOKIE_NAME=ah_token # federated cookie holding the AgentHippo JWT',
273
- 'AGENTHIPPO_AUTH_COOKIE_DOMAIN=.agenthippo.ai # set so /oauth/browser-login stamps a cross-subdomain cookie',
325
+ `AGENTHIPPO_AUTH_COOKIE_DOMAIN=${cookieDomain} # set so /oauth/browser-login stamps a cross-subdomain cookie`,
274
326
  'AGENTHIPPO_TEAM_ID=team_default',
275
327
  'AGENTHIPPO_TEAM_NAME=Default Team',
276
328
  'AGENTHIPPO_TEAM_STORE_REPO=agenthippoai/agenthippo-store',
@@ -285,7 +337,7 @@ function renderAuthBrokerDockerfile() {
285
337
  return [
286
338
  '# Auth broker for IDE sign-in + team bootstrap API.',
287
339
  '# Build context = pack root: docker build -f deploy/Dockerfile.auth-broker -t <pack>-auth-broker:local .',
288
- 'FROM ghcr.io/agenthippo/runtime:local-cursor',
340
+ 'FROM ghcr.io/agenthippoai/runtime:latest',
289
341
  'ENV AGENTHIPPO_AUTH_PORT=9780',
290
342
  'ENV AGENTHIPPO_AUTH_DATA_DIR=/data',
291
343
  'EXPOSE 9780',
@@ -294,6 +346,18 @@ function renderAuthBrokerDockerfile() {
294
346
  '',
295
347
  ].join('\n');
296
348
  }
349
+ /** Secret-like basenames beyond `.env` and `*credentials*.json` — private keys, keystores, and
350
+ * credential files of any extension. Shared by renderDockerignore() below and copyPackToStaging()
351
+ * in commands/deploy.ts so the staging dir on disk and the generated .dockerignore never diverge. */
352
+ function isSecretLikeBasename(base) {
353
+ if (/credentials/i.test(base))
354
+ return true;
355
+ if (/\.(pem|key|p12|pfx|jks|gpg|pgp)$/i.test(base))
356
+ return true;
357
+ if (/^id_(rsa|dsa|ecdsa|ed25519)$/i.test(base))
358
+ return true;
359
+ return false;
360
+ }
297
361
  /** .dockerignore emitted alongside every image-building target's Dockerfile.
298
362
  * Keeps skills/, references/, scripts/ (required at runtime) and drops only VCS,
299
363
  * cache, and build cruft. node_modules is excluded so the linux/amd64 image
@@ -326,8 +390,20 @@ function renderDockerignore() {
326
390
  '.vscode',
327
391
  '*.swp',
328
392
  '# Secrets anywhere in the pack (defensive; deploy/ secrets are also covered below).',
393
+ '# Keep in sync with isSecretLikeBasename() above.',
329
394
  '**/.env',
330
- '**/*credentials*.json',
395
+ '**/*credentials*',
396
+ '**/*.pem',
397
+ '**/*.key',
398
+ '**/*.p12',
399
+ '**/*.pfx',
400
+ '**/*.jks',
401
+ '**/*.gpg',
402
+ '**/*.pgp',
403
+ '**/id_rsa',
404
+ '**/id_dsa',
405
+ '**/id_ecdsa',
406
+ '**/id_ed25519',
331
407
  '# deploy/ holds build/deploy artifacts + secrets; only deploy/www/ ships at runtime.',
332
408
  'deploy/*',
333
409
  '!deploy/www',
@@ -364,17 +440,23 @@ function skillDepsInstallLines() {
364
440
  function agentServeHostEnvYaml(indent = ' ') {
365
441
  return `${indent}- { name: ${serve_hardening_1.SERVE_HOST_ENV}, value: ${serve_hardening_1.CONTAINER_SERVE_HOST} }`;
366
442
  }
367
- /** The Dockerfile shared by image-based targets (serve-v3 §A2). */
443
+ /** The Dockerfile shared by image-based targets (serve-v3 §A2).
444
+ * `extraEnv`: platform-specific ENV lines — e.g. pinning AGENTHIPPO_IDENTITY_HEADER for a
445
+ * target whose container may be directly reachable (governance: identity-header pinning). */
368
446
  function sharedDockerfile(ctx, opts) {
369
447
  const cmd = serveCmdArgs(ctx, opts).map((a) => `"${a}"`).join(',');
370
448
  return [
371
- `# Generated by \`agenthippo deploy init\`. Build context = pack root:`,
449
+ `${packAttributionComment(ctx)}. Build context = pack root:`,
372
450
  `# docker build -f deploy/Dockerfile -t ${ctx.packName}:local .`,
373
451
  `FROM ${ctx.image}`,
452
+ `LABEL ai.agenthippo.pack.name="${ctx.packName}"`,
453
+ `LABEL ai.agenthippo.pack.version="${ctx.packVersion}"`,
454
+ `LABEL ai.agenthippo.pack.engine="${ctx.engine}"`,
374
455
  `COPY . /app/pack/`,
375
456
  ...skillDepsInstallLines(),
376
457
  `ENV AGENTHIPPO_NO_AUTO_INSTALL=1`,
377
458
  `ENV ${serve_hardening_1.SERVE_HOST_ENV}=${serve_hardening_1.CONTAINER_SERVE_HOST}`,
459
+ ...Object.entries(opts?.extraEnv ?? {}).map(([k, v]) => `ENV ${k}=${v}`),
378
460
  `EXPOSE ${ctx.port}`,
379
461
  // Runtime base image sets ENTRYPOINT ["agenthippo"]; CMD args append to it.
380
462
  `CMD [${cmd}]`,
@@ -469,111 +551,47 @@ function renderStickyAgentProxy(port, indent = ' ') {
469
551
  }
470
552
  /** Caddy config for the `edge` compose profile (oauth2-proxy + agent on Docker network). */
471
553
  function renderCaddyfile(ctx) {
554
+ const fed = ctx.ingress === 'edge-fed';
555
+ const idp = (ctx.idpUrl || '').trim().replace(/\/+$/, '');
556
+ const catchallBody = [
557
+ ' @api path /v1/*',
558
+ ' handle @api {',
559
+ ...renderStickyAgentProxy(ctx.port, ' '),
560
+ ' }',
561
+ ' root * /srv/www',
562
+ ' file_server',
563
+ ];
472
564
  return [
473
565
  '# SSO edge + Cloudflare Tunnel. Set AGENTHIPPO_PUBLIC_HOSTNAME in .env (Caddy reads it below).',
474
566
  '# TLS terminates at Cloudflare; origin listens on HTTP only.',
567
+ ...(fed
568
+ ? [
569
+ `# No oauth2-proxy — Google login happens once at the central IdP (${idp || '<IDP_URL>'});`,
570
+ '# this agent only verifies the shared AgentHippo JWT (Bearer for IDE, cookie for browser).',
571
+ ]
572
+ : []),
475
573
  '{',
476
574
  ' auto_https off',
477
575
  '}',
478
576
  '',
479
577
  'http://{$AGENTHIPPO_PUBLIC_HOSTNAME} {',
480
- ' # IDE PKCE sign-in: broker requires X-Auth-Request-Email from oauth2-proxy. Must come',
481
- ' # before the generic /oauth/* handle so authorize is gated (not dumped straight to broker).',
482
- ' handle /oauth/authorize {',
483
- ' forward_auth oauth2-proxy:4180 {',
484
- ' uri /oauth2/auth',
485
- ' copy_headers X-Auth-Request-Email',
486
- ' header_up X-Forwarded-Proto https',
487
- ' header_up X-Forwarded-Host {http.request.host}',
488
- ' @unauth status 401',
489
- ' handle_response @unauth {',
490
- ' redir * "/oauth2/sign_in?rd={http.request.uri}"',
491
- ' }',
492
- ' }',
493
- ' reverse_proxy auth-broker:9780 {',
494
- ' header_up X-Forwarded-Proto https',
495
- ' header_up X-Forwarded-Host {http.request.host}',
496
- ' }',
497
- ' }',
498
- ' # Federated SSO (A′): browsers from *.agenthippo.ai subdomains land here to log in.',
499
- ' # Gated behind oauth2-proxy (Google) → auth-broker mints + sets the shared .agenthippo.ai',
500
- ' # cookie (ah_token), then redirects back to the agent subdomain (`return_to`). Must come',
501
- ' # before the generic /oauth/* handle so it wins for this exact path.',
502
- ' handle /oauth/browser-login {',
503
- ' forward_auth oauth2-proxy:4180 {',
504
- ' uri /oauth2/auth',
505
- ' copy_headers X-Auth-Request-Email',
506
- ' header_up X-Forwarded-Proto https',
507
- ' header_up X-Forwarded-Host {http.request.host}',
508
- ' @unauth status 401',
509
- ' handle_response @unauth {',
510
- ' redir * "/oauth2/sign_in?rd=/oauth/browser-login{http.request.uri.query}"',
511
- ' }',
512
- ' }',
513
- ' reverse_proxy auth-broker:9780 {',
514
- ' header_up X-Forwarded-Proto https',
515
- ' header_up X-Forwarded-Host {http.request.host}',
516
- ' }',
517
- ' }',
518
- ' handle /oauth/* {',
519
- ' reverse_proxy auth-broker:9780 {',
520
- ' header_up X-Forwarded-Proto https',
521
- ' header_up X-Forwarded-Host {http.request.host}',
522
- ' }',
523
- ' }',
524
- ' handle /api/ide/* {',
525
- ' reverse_proxy auth-broker:9780 {',
526
- ' header_up X-Forwarded-Proto https',
527
- ' header_up X-Forwarded-Host {http.request.host}',
528
- ' }',
529
- ' }',
530
- ' handle /oauth2/* {',
531
- ' reverse_proxy oauth2-proxy:4180 {',
532
- ' header_up X-Forwarded-Proto https',
533
- ' header_up X-Forwarded-Host {http.request.host}',
534
- ' }',
535
- ' }',
536
- ' @health path /health /ready',
537
- ' handle @health {',
538
- ...renderStickyAgentProxy(ctx.port, ' '),
539
- ' }',
540
- ' # API clients (IDE) with a Bearer token: validate via auth-broker, copy identity header.',
541
- ' @apiWithAuth {',
542
- ' path /v1/*',
543
- ' header Authorization *',
544
- ' }',
545
- ' handle @apiWithAuth {',
546
- ' forward_auth auth-broker:9780 {',
547
- ' uri /api/ide/me',
548
- ' copy_headers X-Agent-User',
549
- ' header_up X-Forwarded-Proto https',
550
- ' header_up X-Forwarded-Host {http.request.host}',
551
- ' @unauth status 401',
552
- ' handle_response @unauth {',
553
- ' respond "Unauthorized" 401',
554
- ' }',
555
- ' }',
556
- ...renderStickyAgentProxy(ctx.port, ' '),
557
- ' }',
558
- ' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User.',
559
- ' handle {',
560
- ' forward_auth oauth2-proxy:4180 {',
561
- ' uri /oauth2/auth',
562
- ' copy_headers X-Auth-Request-Email>X-Agent-User',
563
- ' header_up X-Forwarded-Proto https',
564
- ' header_up X-Forwarded-Host {http.request.host}',
565
- ' @unauth status 401',
566
- ' handle_response @unauth {',
567
- ' redir * "/oauth2/sign_in?rd={http.request.uri.path}{http.request.uri.query}"',
568
- ' }',
569
- ' }',
570
- ' @api path /v1/*',
571
- ' handle @api {',
572
- ...renderStickyAgentProxy(ctx.port, ' '),
573
- ' }',
574
- ' root * /srv/www',
575
- ' file_server',
576
- ' }',
578
+ ...(fed
579
+ ? (0, identity_1.renderIdentityLayer)({
580
+ mode: 'federated',
581
+ authBrokerAddr: 'auth-broker:9780',
582
+ idpUrl: idp,
583
+ healthProxy: renderStickyAgentProxy(ctx.port, ' '),
584
+ agentProxy: renderStickyAgentProxy(ctx.port, ' '),
585
+ catchallBody,
586
+ })
587
+ : (0, identity_1.renderIdentityLayer)({
588
+ mode: 'self-issued',
589
+ oauth2ProxyAddr: 'oauth2-proxy:4180',
590
+ authBrokerAddr: 'auth-broker:9780',
591
+ healthProxy: renderStickyAgentProxy(ctx.port, ' '),
592
+ agentProxy: renderStickyAgentProxy(ctx.port, ' '),
593
+ catchallBody,
594
+ })),
577
595
  '}',
578
596
  '',
579
597
  ].join('\n');
@@ -614,9 +632,10 @@ function renderAgentComposeService(ctx, extraLines = []) {
614
632
  '',
615
633
  ];
616
634
  }
617
- /** oauth2-proxy + Caddy services for `--profile edge`. */
618
- function renderEdgeComposeServices() {
619
- return [
635
+ /** oauth2-proxy (self-issued only) + auth-broker + Caddy services for `--profile edge`.
636
+ * `fed`: federated (verify-only against a central IdP) — no oauth2-proxy, no local Google client. */
637
+ function renderEdgeComposeServices(fed) {
638
+ const oauth2Proxy = fed ? [] : [
620
639
  ' oauth2-proxy:',
621
640
  ' profiles: ["edge"]',
622
641
  ' image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1',
@@ -634,6 +653,9 @@ function renderEdgeComposeServices() {
634
653
  ' OAUTH2_PROXY_REVERSE_PROXY: "true"',
635
654
  ' expose: ["4180"]',
636
655
  '',
656
+ ];
657
+ return [
658
+ ...oauth2Proxy,
637
659
  ' auth-broker:',
638
660
  ' profiles: ["edge"]',
639
661
  ' build:',
@@ -659,7 +681,7 @@ function renderEdgeComposeServices() {
659
681
  ' required: false',
660
682
  ' ports: ["80:80", "443:443"] # use 80:80 not 127.0.0.1:80 — breaks vmnetd on some Docker Desktop builds',
661
683
  ' volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "./www:/srv/www:ro"]',
662
- ' depends_on: [oauth2-proxy, agent, auth-broker]',
684
+ ` depends_on: [${fed ? 'agent, auth-broker' : 'oauth2-proxy, agent, auth-broker'}]`,
663
685
  '',
664
686
  ];
665
687
  }
@@ -683,7 +705,7 @@ function renderTunnelComposeServices() {
683
705
  function renderEdgeTunnelUpScript(hostname) {
684
706
  return [
685
707
  '#!/usr/bin/env bash',
686
- '# T0.5b — Cloudflare Tunnel + Google SSO (no Access seats). Run from pack root.',
708
+ '# T0.5b — Cloudflare Tunnel + Google SSO (no Access seats). Run: bash deploy/run-edge-tunnel.sh',
687
709
  'set -euo pipefail',
688
710
  'cd "$(dirname "$0")"',
689
711
  'docker compose --profile edge --profile tunnel up -d --build',
@@ -691,7 +713,113 @@ function renderEdgeTunnelUpScript(hostname) {
691
713
  '',
692
714
  ].join('\n');
693
715
  }
716
+ /**
717
+ * Same-host colocation (PLAN/deploy.md §4 state 4, `d5-colocate`): this agent rides the org
718
+ * IdP's already-running Caddy + auth-broker instead of shipping its own — no Caddy, no
719
+ * oauth2-proxy, no local auth-broker, no local auth secrets at all. Ships only the agent
720
+ * container (joining the IdP's `agenthippo-idp-colocate` docker network via a fixed,
721
+ * predictable container name) plus a Caddy route snippet for the operator to drop into the
722
+ * IdP's `agents/` import directory (see enterprise-deploy/idp/Caddyfile + agents/README.md).
723
+ */
724
+ function renderColocatedArtifacts(ctx) {
725
+ const idp = (ctx.idpUrl || '').trim().replace(/\/+$/, '');
726
+ const containerName = `${ctx.packName}-agent`;
727
+ const compose = [
728
+ '# Colocated agent (PLAN/deploy.md §4 state 4) — no Caddy/oauth2-proxy/auth-broker of its',
729
+ '# own. Joins the org IdP\'s docker network so its already-running Caddy can reach this',
730
+ '# agent by name; identity is verified entirely by the IdP\'s auth-broker (see',
731
+ '# deploy/agent-route.caddy for the route to copy onto the IdP host).',
732
+ `name: ${ctx.packName}`,
733
+ 'services:',
734
+ ' agent:',
735
+ ` container_name: ${containerName}`,
736
+ ' build:',
737
+ ' context: ..',
738
+ ' dockerfile: deploy/Dockerfile',
739
+ ' env_file:',
740
+ ' - path: .env',
741
+ ' required: false',
742
+ ' volumes: ["agenthippo-state:/state"]',
743
+ ' networks: [colocate]',
744
+ ` expose: ["${ctx.port}"]`,
745
+ '',
746
+ 'networks:',
747
+ ' colocate:',
748
+ ' external: true',
749
+ ' name: ${AGENTHIPPO_IDP_NETWORK:-agenthippo-idp-colocate}',
750
+ '',
751
+ 'volumes:',
752
+ ' agenthippo-state:',
753
+ '',
754
+ ].join('\n');
755
+ const snippet = [
756
+ `# Colocated route for ${ctx.publicHostname} — copy into enterprise-deploy/idp/agents/`,
757
+ '# (rename e.g. to ' + ctx.packName + '.caddy), then on the IdP host:',
758
+ '# docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile',
759
+ '# Requires this agent\'s docker-compose.yml (already generated) to be up, so',
760
+ `# ${containerName} exists on the shared \`agenthippo-idp-colocate\` network.`,
761
+ '# http:// (not https://) to match the IdP Caddyfile\'s TLS model: auto_https is off',
762
+ '# there and TLS terminates in front (Cloudflare Tunnel/nginx), so every site block —',
763
+ '# including this imported one — must be plain HTTP, same as the IdP\'s own site address.',
764
+ `http://${ctx.publicHostname} {`,
765
+ ...(0, identity_1.renderIdentityLayer)({
766
+ mode: 'federated',
767
+ authBrokerAddr: 'auth-broker:9780',
768
+ idpUrl: idp,
769
+ healthProxy: [` reverse_proxy ${containerName}:${ctx.port}`],
770
+ agentProxy: [` reverse_proxy ${containerName}:${ctx.port}`],
771
+ }),
772
+ '}',
773
+ '',
774
+ ].join('\n');
775
+ return {
776
+ 'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
777
+ 'docker-compose.yml': compose,
778
+ 'agent-route.caddy': snippet,
779
+ 'www/index.html': renderLandingPage(ctx.packName),
780
+ 'README.md': [
781
+ `# Deploy: ${ctx.packName} — colocated with the org IdP`,
782
+ '',
783
+ 'No Caddy, no oauth2-proxy, no local auth-broker, no auth secrets on this host — this',
784
+ 'agent rides the org IdP\'s already-running edge (`enterprise-deploy/idp/`) instead.',
785
+ '',
786
+ '```bash',
787
+ 'cp deploy/.env.example deploy/.env # model key only — no auth secrets here at all',
788
+ '# Point at the IdP\'s colocate network if it was created under a different compose',
789
+ '# project name: export AGENTHIPPO_IDP_NETWORK=agenthippo-idp-colocate (the default)',
790
+ 'docker compose -f deploy/docker-compose.yml up -d --build',
791
+ '',
792
+ '# Wire the route into the IdP\'s Caddy (one-time per agent):',
793
+ `cp deploy/agent-route.caddy <idp-host>:enterprise-deploy/idp/agents/${ctx.packName}.caddy`,
794
+ 'docker compose -f enterprise-deploy/idp/docker-compose.yml exec caddy \\',
795
+ ' caddy reload --config /etc/caddy/Caddyfile',
796
+ '```',
797
+ '',
798
+ `DNS: point \`${ctx.publicHostname}\` at the IdP host (same as every other agent it fronts).`,
799
+ '',
800
+ '**hippopennyg / bare-metal (Cloudflare → nginx:443 → IdP Caddy :8080):** `deploy init` does',
801
+ '**not** create DNS or nginx for you. Without an explicit Cloudflare **A record**, the name',
802
+ 'falls through to the `*.agenthippo.ai` wildcard (`ghs.googlehosted.com` from',
803
+ '`setup-agenthippo-domain.sh` for Cloud Run) — the site will look "broken" or hit Google.',
804
+ 'After the steps above, run:',
805
+ '',
806
+ '```bash',
807
+ './enterprise-deploy/scripts/setup-colocated-agent-edge.sh \\',
808
+ ` --hostname ${ctx.publicHostname} --ssh root@168.119.174.239`,
809
+ '```',
810
+ '',
811
+ 'See `enterprise-deploy/idp/agents/README.md` for the full checklist.',
812
+ '**Browser:** after Google SSO, open `https://' + ctx.publicHostname + '/` for the static chat UI',
813
+ '(`deploy/www/index.html` served by the agent container at GET /).',
814
+ '',
815
+ ...renderEgressNote(ctx),
816
+ ].join('\n'),
817
+ };
818
+ }
694
819
  function vmArtifacts(ctx) {
820
+ if (ctx.ingress === 'edge-fed' && ctx.colocate) {
821
+ return renderColocatedArtifacts(ctx);
822
+ }
695
823
  const hostname = ctx.publicHostname;
696
824
  const run = [
697
825
  '#!/usr/bin/env bash',
@@ -752,7 +880,7 @@ function vmArtifacts(ctx) {
752
880
  '# edge only: docker compose --profile edge up -d (SSO on :443, no tunnel)',
753
881
  'services:',
754
882
  ...renderAgentComposeService(ctx),
755
- ...renderEdgeComposeServices(),
883
+ ...renderEdgeComposeServices(ctx.ingress === 'edge-fed'),
756
884
  ...renderTunnelComposeServices(),
757
885
  'volumes:',
758
886
  ' agenthippo-state:',
@@ -773,6 +901,10 @@ function vmArtifacts(ctx) {
773
901
  'README.md': [
774
902
  `# Deploy: ${ctx.packName} — T0.5 workbench/VM`,
775
903
  '',
904
+ 'Staging root = this directory\'s parent (full pack copy). Commands below run from the staging root unless noted.',
905
+ '',
906
+ 'To regenerate: run `agenthippo deploy init <path-to-pack> --target vm` from the **parent** of the pack (not from inside the pack), then `cd deploy-out/' + ctx.packName + '-vm`.',
907
+ '',
776
908
  '`cp deploy/.env.example deploy/.env` and fill in, then pick a path:',
777
909
  '',
778
910
  '**T0.5a — VPN only (simplest internal pilot)**',
@@ -783,18 +915,16 @@ function vmArtifacts(ctx) {
783
915
  '',
784
916
  '**T0.5b — Cloudflare Tunnel + Google SSO (recommended for WFH, no Access seats)**',
785
917
  '```bash',
786
- 'agenthippo deploy init . --target vm --force # from pack root; review deploy/',
787
918
  'cp deploy/.env.example deploy/.env # agent: model key + public hostname',
788
919
  'cp deploy/.env.auth.example deploy/.env.auth # broker/oauth2-proxy: JWT + OAuth secrets (never loaded by the agent)',
789
920
  '# 1. Google Cloud → OAuth Web client for https://' + hostname,
790
921
  '# Authorized origin + redirect: https://' + hostname + '/oauth2/callback',
791
922
  '# 2. cloudflared tunnel create <name>',
792
- '# cp ~/.cloudflared/<tunnel-id>.json deploy/tunnel-credentials.json # gitignore; secret',
923
+ '# cp ~/.cloudflared/<tunnel-id>.json ~/.agent-hippo/tunnel-credentials.json # secret',
793
924
  '# set tunnel: <tunnel-id> in deploy/cloudflared.yml',
794
925
  '# cloudflared tunnel route dns <name> ' + hostname,
795
926
  '# 3. Fill deploy/.env (hostname, model key) and deploy/.env.auth (OAUTH2_PROXY_*, AGENTHIPPO_AUTH_JWT_SECRET)',
796
- 'cd deploy && docker compose --profile edge --profile tunnel up -d --build',
797
- '# or from pack root: bash deploy/run-edge-tunnel.sh',
927
+ 'bash deploy/run-edge-tunnel.sh',
798
928
  '# Users open https://' + hostname + ' → Google login → static chat UI → /v1/chat',
799
929
  '```',
800
930
  'Tunnel routing is **config-file mode** (`deploy/cloudflared.yml` → `http://caddy:80` on the compose network), not the Cloudflare dashboard public-hostname UI alone.',
@@ -807,6 +937,7 @@ function vmArtifacts(ctx) {
807
937
  '',
808
938
  '**Do not commit** `deploy/.env`, `deploy/.env.auth`, or `deploy/tunnel-credentials.json`.',
809
939
  '',
940
+ ...renderEgressNote(ctx),
810
941
  ].join('\n'),
811
942
  };
812
943
  }
@@ -836,6 +967,9 @@ const OTEL_COLLECTOR_COMPOSE_YAML = [
836
967
  '',
837
968
  ].join('\n');
838
969
  function composeArtifacts(ctx) {
970
+ if (ctx.ingress === 'edge-fed' && ctx.colocate) {
971
+ return renderColocatedArtifacts(ctx);
972
+ }
839
973
  const channels = ctx.gatewayChannels?.trim();
840
974
  const hostname = ctx.publicHostname;
841
975
  // Agent Anywhere: override the agent command to also run the gateway bridge,
@@ -857,7 +991,7 @@ function composeArtifacts(ctx) {
857
991
  ' # OPENCLAW_GATEWAY_TOKEN (or a paired device) to connect. Set it in .env.',
858
992
  ' openclaw-gateway:',
859
993
  ' profiles: ["channels"]',
860
- ' image: ghcr.io/agenthippo/openclaw-gateway:2026.06',
994
+ ' image: ghcr.io/agenthippoai/openclaw-gateway:2026.06',
861
995
  ' env_file:',
862
996
  ' - path: .env',
863
997
  ' required: false',
@@ -882,13 +1016,13 @@ function composeArtifacts(ctx) {
882
1016
  '# e.g.: docker compose -f deploy/docker-compose.yml --profile edge --profile tunnel up -d',
883
1017
  'services:',
884
1018
  ...renderAgentComposeService(ctx, agentExtra),
885
- ...renderEdgeComposeServices(),
1019
+ ...renderEdgeComposeServices(ctx.ingress === 'edge-fed'),
886
1020
  ...renderTunnelComposeServices(),
887
1021
  ' # LiteLLM gateway — self-contained image bakes the Spotlight config + OTEL hooks',
888
1022
  ' # (build: docker build -f extensions/agentide/scripts/Dockerfile.litellm -t this-tag .)',
889
1023
  ' litellm:',
890
1024
  ' profiles: ["gateway"]',
891
- ' image: ghcr.io/agenthippo/litellm-gateway:2026.06',
1025
+ ' image: ghcr.io/agenthippoai/litellm-gateway:2026.06',
892
1026
  ' env_file:',
893
1027
  ' - path: .env',
894
1028
  ' required: false',
@@ -939,6 +1073,7 @@ function composeArtifacts(ctx) {
939
1073
  'Profiles compose: e.g. `--profile gateway --profile edge --profile tunnel up -d`.',
940
1074
  'LiteLLM must speak Anthropic API format — our gateway image does.',
941
1075
  '',
1076
+ ...renderEgressNote(ctx),
942
1077
  ].join('\n'),
943
1078
  };
944
1079
  }
@@ -958,6 +1093,9 @@ function composeArtifacts(ctx) {
958
1093
  */
959
1094
  function litellmProviderKeyEnv(model) {
960
1095
  const m = (model || '').toLowerCase();
1096
+ if (m === 'ah-auto') {
1097
+ return 'AGENTHIPPO_GATEWAY_KEY';
1098
+ }
961
1099
  if (m.includes('codex')) {
962
1100
  return 'OPENAI_CODEX_API_KEY';
963
1101
  }
@@ -969,6 +1107,102 @@ function litellmProviderKeyEnv(model) {
969
1107
  }
970
1108
  return 'OPENAI_API_KEY';
971
1109
  }
1110
+ /** True when the pack uses the managed Auto placeholder (routes via gateway.agenthippo.ai). */
1111
+ function isAhAutoModel(model) {
1112
+ return (model || '').trim().toLowerCase() === 'ah-auto';
1113
+ }
1114
+ /** Org gateway base URL — agent gateway-direct mode points here (same as colocated agents). */
1115
+ const AGENTHIPPO_GATEWAY_BASE_URL = 'https://gateway.agenthippo.ai/v1';
1116
+ function usesGatewayDirect(ctx) {
1117
+ return ctx.modelRouting === 'gateway';
1118
+ }
1119
+ /** The model-provider API host this pack's engine actually talks to — the one thing every
1120
+ * target's container needs outbound network access to. Generalizes the `spcs` target's
1121
+ * enforced "one EAI to api.anthropic.com" egress allowlist into a documented note every
1122
+ * other target ships too (governance layer, PLAN/deploy.md §6). */
1123
+ function egressHost(ctx) {
1124
+ if (ctx.engine.trim().toLowerCase() === 'cursor') {
1125
+ return 'api2.cursor.sh';
1126
+ }
1127
+ if (isAhAutoModel(ctx.model)) {
1128
+ return 'gateway.agenthippo.ai';
1129
+ }
1130
+ const m = (ctx.model || '').toLowerCase();
1131
+ if (m.includes('codex') || m.includes('gpt') || m.includes('openai')) {
1132
+ return 'api.openai.com';
1133
+ }
1134
+ return 'api.anthropic.com';
1135
+ }
1136
+ /** Egress allowlist note, appended to a target's README (governance layer). */
1137
+ function renderEgressNote(ctx) {
1138
+ const sidecarNote = usesGatewayDirect(ctx)
1139
+ ? '.'
1140
+ : ' — and, if the LiteLLM gateway/Spotlight sidecar is enabled, its own outbound is the same host.';
1141
+ return [
1142
+ `**Egress:** this container only needs outbound access to \`${egressHost(ctx)}:443\` (your model provider)${sidecarNote}`,
1143
+ 'Restrict egress to this allowlist if your org requires it.',
1144
+ '',
1145
+ ];
1146
+ }
1147
+ /**
1148
+ * Data & access summary (governance layer, PLAN/deploy.md §3/§6): one generated page
1149
+ * enumerating what secrets exist, what leaves the network, and the blast radius per
1150
+ * component — pre-answering the questions a security review asks before it asks them.
1151
+ * The `spcs` target's "one EAI to api.anthropic.com" README line is the model this
1152
+ * generalizes to every target instead of leaving it to just one platform.
1153
+ */
1154
+ function renderDataAccessSummary(ctx, target, edge, fed, colocate) {
1155
+ const secrets = [
1156
+ `\`${litellmProviderKeyEnv(ctx.model)}\` (or the pack's configured provider key) — your model provider credential`,
1157
+ ];
1158
+ const blastRadius = [
1159
+ '**agent container** — can reach your model provider and read/write its own state volume. ' +
1160
+ 'Never holds the JWT signing secret or any OAuth client secret (`.env` vs `.env.auth` split).',
1161
+ ];
1162
+ if (edge) {
1163
+ secrets.push('`AGENTHIPPO_AUTH_JWT_SECRET` — signs/verifies IDE bootstrap tokens (auth-broker only, never the agent)');
1164
+ blastRadius.push('**auth-broker** — holds the JWT signing secret' +
1165
+ (fed ? ' (shared with the central IdP)' : '') +
1166
+ '; mints/verifies IDE tokens. Does not hold model provider credentials.');
1167
+ if (!fed) {
1168
+ secrets.push('`OAUTH2_PROXY_CLIENT_SECRET` + `OAUTH2_PROXY_COOKIE_SECRET` — this deployment\'s own Google OAuth client (oauth2-proxy only, never the agent)');
1169
+ blastRadius.push('**oauth2-proxy** — holds this deployment\'s Google OAuth client secret; terminates the Google ' +
1170
+ 'login flow. Does not hold the JWT signing secret or model credentials.');
1171
+ }
1172
+ }
1173
+ else if (colocate) {
1174
+ blastRadius.push('**this deployment holds no auth secrets at all** — identity is verified entirely by the ' +
1175
+ 'org IdP\'s own auth-broker (see `agent-route.caddy`); nothing here signs or stores a JWT secret.');
1176
+ }
1177
+ return [
1178
+ `# Data & access summary — ${ctx.packName} (${target}${ctx.ingress ? ` / ${ctx.ingress}` : ''})`,
1179
+ '',
1180
+ 'Generated by `agenthippo deploy init` — answers the questions a security review asks first.',
1181
+ '',
1182
+ '## Secrets',
1183
+ ...secrets.map((s) => `- ${s}`),
1184
+ '',
1185
+ '## Egress',
1186
+ `- Outbound network access is needed only to \`${egressHost(ctx)}:443\` (your model provider).`,
1187
+ ...(edge && fed ? [
1188
+ '- API/Bearer requests are verified locally against the shared JWT secret — no network call ' +
1189
+ 'to the central IdP per request. The one-time browser login flow (unauthenticated visitors) ' +
1190
+ 'does redirect to the central IdP\'s public URL to sign in.',
1191
+ ] : []),
1192
+ ...(edge && !fed ? [
1193
+ '- Google SSO calls `accounts.google.com` during login only (oauth2-proxy); no other third party is contacted per request.',
1194
+ ] : []),
1195
+ ...(colocate ? [
1196
+ '- Identity is verified via the org IdP\'s own auth-broker over the shared private docker ' +
1197
+ 'network (`agenthippo-idp-colocate`), not a public network call. The one-time browser login ' +
1198
+ 'flow (unauthenticated visitors) still redirects to the central IdP\'s public URL to sign in.',
1199
+ ] : []),
1200
+ '',
1201
+ '## Blast radius per component',
1202
+ ...blastRadius.map((b) => `- ${b}`),
1203
+ '',
1204
+ ].join('\n');
1205
+ }
972
1206
  function renderCloudrunServiceYaml(ctx) {
973
1207
  const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
974
1208
  return [
@@ -1106,7 +1340,7 @@ function renderCloudrunSpotlightDeploy(ctx) {
1106
1340
  'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
1107
1341
  'REPO="${ARTIFACT_REPO:-agenthippo}"',
1108
1342
  'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
1109
- 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippo/litellm-gateway:2026.06}"',
1343
+ 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippoai/litellm-gateway:2026.07}"',
1110
1344
  'STATE_BUCKET="${STATE_BUCKET:?set STATE_BUCKET=your-gcs-bucket (gcloud storage buckets create gs://...)}"',
1111
1345
  '# LITELLM_KEY = the proxy key the agent presents to LiteLLM (and LiteLLM master key).',
1112
1346
  'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
@@ -1117,10 +1351,10 @@ function renderCloudrunSpotlightDeploy(ctx) {
1117
1351
  ' || gcloud artifacts repositories create "$REPO" --repository-format=docker --location="$REGION"',
1118
1352
  'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
1119
1353
  '',
1120
- '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippo/runtime:latest',
1354
+ '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
1121
1355
  '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
1122
1356
  'DF_TMP="$(mktemp)"',
1123
- 'sed "s|FROM ghcr.io/agenthippo/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1357
+ 'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1124
1358
  'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
1125
1359
  'docker push "$IMAGE"',
1126
1360
  '',
@@ -1165,121 +1399,15 @@ function renderCloudrunSpotlightDeploy(ctx) {
1165
1399
  * via `localhost:<port>` (NOT docker-DNS names like the compose edge). Cloud Run terminates TLS
1166
1400
  * at the edge, so Caddy listens on HTTP `:{$PORT}` (`auto_https off`). The agent is NOT the
1167
1401
  * ingress here, so it listens on a distinct internal port (EDGE_AGENT_PORT) to avoid clashing
1168
- * with Caddy on $PORT.
1402
+ * with Caddy on $PORT. The Caddyfile renderers themselves live in `deploy/layers/identity.ts`
1403
+ * (`renderSidecarEdgeCaddyfile`/`renderSidecarFedCaddyfile`) — reused verbatim by aws-ecs and
1404
+ * azure-container-apps, which share this exact "TLS in front, one localhost" shape.
1169
1405
  */
1170
- const EDGE_AGENT_PORT = 8090;
1171
- /** Caddyfile for the Cloud Run edge — HTTP on $PORT (Cloud Run terminates TLS), localhost upstreams. */
1172
- function renderCloudrunEdgeCaddyfile() {
1173
- return [
1174
- '# Cloud Run edge ingress. Cloud Run terminates TLS; Caddy listens on HTTP :{$PORT}.',
1175
- '# Sidecars share the pod\'s localhost → upstreams are 127.0.0.1:<port> (not docker-DNS names).',
1176
- '{',
1177
- ' auto_https off',
1178
- '}',
1179
- '',
1180
- ':{$PORT} {',
1181
- ' # IDE PKCE sign-in: broker requires X-Auth-Request-Email from oauth2-proxy. Must come',
1182
- ' # before the generic /oauth/* handle so authorize is gated (not dumped straight to broker).',
1183
- ' handle /oauth/authorize {',
1184
- ' forward_auth localhost:4180 {',
1185
- ' uri /oauth2/auth',
1186
- ' copy_headers X-Auth-Request-Email',
1187
- ' header_up X-Forwarded-Proto https',
1188
- ' header_up X-Forwarded-Host {http.request.host}',
1189
- ' @unauth status 401',
1190
- ' handle_response @unauth {',
1191
- ' redir * "/oauth2/sign_in?rd={http.request.uri}"',
1192
- ' }',
1193
- ' }',
1194
- ' reverse_proxy localhost:9780 {',
1195
- ' header_up X-Forwarded-Proto https',
1196
- ' header_up X-Forwarded-Host {http.request.host}',
1197
- ' }',
1198
- ' }',
1199
- ' # Federated SSO (A′): browsers from other subdomains land here to stamp the shared cookie.',
1200
- ' # Same oauth2-proxy gate as authorize; must precede the generic /oauth/* handle.',
1201
- ' handle /oauth/browser-login {',
1202
- ' forward_auth localhost:4180 {',
1203
- ' uri /oauth2/auth',
1204
- ' copy_headers X-Auth-Request-Email',
1205
- ' header_up X-Forwarded-Proto https',
1206
- ' header_up X-Forwarded-Host {http.request.host}',
1207
- ' @unauth status 401',
1208
- ' handle_response @unauth {',
1209
- ' redir * "/oauth2/sign_in?rd=/oauth/browser-login{http.request.uri.query}"',
1210
- ' }',
1211
- ' }',
1212
- ' reverse_proxy localhost:9780 {',
1213
- ' header_up X-Forwarded-Proto https',
1214
- ' header_up X-Forwarded-Host {http.request.host}',
1215
- ' }',
1216
- ' }',
1217
- ' # Auth-broker: token exchange + other /oauth/* routes that must stay ungated.',
1218
- ' handle /oauth/* {',
1219
- ' reverse_proxy localhost:9780 {',
1220
- ' header_up X-Forwarded-Proto https',
1221
- ' header_up X-Forwarded-Host {http.request.host}',
1222
- ' }',
1223
- ' }',
1224
- ' handle /api/ide/* {',
1225
- ' reverse_proxy localhost:9780 {',
1226
- ' header_up X-Forwarded-Proto https',
1227
- ' header_up X-Forwarded-Host {http.request.host}',
1228
- ' }',
1229
- ' }',
1230
- ' # oauth2-proxy: Google SSO callback + sign-in.',
1231
- ' handle /oauth2/* {',
1232
- ' reverse_proxy localhost:4180 {',
1233
- ' header_up X-Forwarded-Proto https',
1234
- ' header_up X-Forwarded-Host {http.request.host}',
1235
- ' }',
1236
- ' }',
1237
- ' @health path /health /ready',
1238
- ' handle @health {',
1239
- ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1240
- ' }',
1241
- ' # IDE / API clients with a Bearer token: validate via auth-broker, copy identity header.',
1242
- ' @apiWithAuth {',
1243
- ' path /v1/*',
1244
- ' header Authorization *',
1245
- ' }',
1246
- ' handle @apiWithAuth {',
1247
- ' forward_auth localhost:9780 {',
1248
- ' uri /api/ide/me',
1249
- ' copy_headers X-Agent-User',
1250
- ' header_up X-Forwarded-Proto https',
1251
- ' header_up X-Forwarded-Host {http.request.host}',
1252
- ' @unauth status 401',
1253
- ' handle_response @unauth {',
1254
- ' respond "Unauthorized" 401',
1255
- ' }',
1256
- ' }',
1257
- ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1258
- ' }',
1259
- ' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User, then proxy',
1260
- ' # to the agent (which serves the www landing page at / and the API at /v1/*).',
1261
- ' handle {',
1262
- ' forward_auth localhost:4180 {',
1263
- ' uri /oauth2/auth',
1264
- ' copy_headers X-Auth-Request-Email>X-Agent-User',
1265
- ' header_up X-Forwarded-Proto https',
1266
- ' header_up X-Forwarded-Host {http.request.host}',
1267
- ' @unauth status 401',
1268
- ' handle_response @unauth {',
1269
- ' redir * "/oauth2/sign_in?rd={http.request.uri.path}{http.request.uri.query}"',
1270
- ' }',
1271
- ' }',
1272
- ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1273
- ' }',
1274
- '}',
1275
- '',
1276
- ].join('\n');
1277
- }
1278
1406
  /** Cloud Run multi-container service behind the AgentHippo edge (Option B). Caddy is ingress. */
1279
1407
  function renderCloudrunEdgeServiceYaml(ctx) {
1280
1408
  const isCursor = ctx.engine.trim().toLowerCase() === 'cursor';
1281
1409
  const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
1282
- const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: EDGE_AGENT_PORT })
1410
+ const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: identity_1.EDGE_AGENT_PORT })
1283
1411
  .map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ');
1284
1412
  // Start order: sidecars before caddy (ingress) so Caddy's first request finds everyone up.
1285
1413
  const deps = isCursor
@@ -1370,7 +1498,7 @@ function renderCloudrunEdgeServiceYaml(ctx) {
1370
1498
  ]),
1371
1499
  ' volumeMounts:',
1372
1500
  ' - { name: state, mountPath: /state }',
1373
- ...startupProbeLines(EDGE_AGENT_PORT, true),
1501
+ ...startupProbeLines(identity_1.EDGE_AGENT_PORT, true),
1374
1502
  ...resourceLimits('2000m', '2Gi'),
1375
1503
  ];
1376
1504
  if (!isCursor) {
@@ -1399,7 +1527,7 @@ function renderCloudrunEdgeDeploy(ctx) {
1399
1527
  'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
1400
1528
  'REPO="${ARTIFACT_REPO:-agenthippo}"',
1401
1529
  'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
1402
- 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippo/litellm-gateway:2026.06}"',
1530
+ 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippoai/litellm-gateway:2026.07}"',
1403
1531
  '# Public hostname (custom domain mapped to the service) + the Google Workspace domain allowed by SSO.',
1404
1532
  'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=your.edge.hostname (gcloud run domain-mappings create --service $SERVICE --domain ...)}"',
1405
1533
  'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:?set ALLOWED_DOMAINS=yourworkspace.com (oauth2-proxy email domain + broker allowed domains)}"',
@@ -1422,10 +1550,10 @@ function renderCloudrunEdgeDeploy(ctx) {
1422
1550
  'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
1423
1551
  '',
1424
1552
  '# One agent image serves both the agent and the auth-broker containers (CLI ships `auth-broker`).',
1425
- '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippo/runtime:latest',
1553
+ '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
1426
1554
  '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
1427
1555
  'DF_TMP="$(mktemp)"',
1428
- 'sed "s|FROM ghcr.io/agenthippo/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1556
+ 'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1429
1557
  'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
1430
1558
  'docker push "$IMAGE"',
1431
1559
  '',
@@ -1500,6 +1628,7 @@ function cloudrunArtifacts(ctx) {
1500
1628
  '**Lean alternative (no Spotlight):** a single-container deploy that calls a provider directly',
1501
1629
  '(or an external LiteLLM) — set `AGENTHIPPO_LITELLM_BASE_URL` to your endpoint and drop the sidecars.',
1502
1630
  '',
1631
+ ...renderEgressNote(ctx),
1503
1632
  ].join('\n'),
1504
1633
  };
1505
1634
  }
@@ -1535,10 +1664,10 @@ function cloudrunArtifacts(ctx) {
1535
1664
  'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
1536
1665
  '',
1537
1666
  '# Build from pack root; Dockerfile is deploy/Dockerfile (not pack root — gcloud --source . misses it).',
1538
- '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippo/runtime:latest',
1667
+ '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
1539
1668
  '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
1540
1669
  'DF_TMP="$(mktemp)"',
1541
- 'sed "s|FROM ghcr.io/agenthippo/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1670
+ 'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1542
1671
  'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
1543
1672
  'docker push "$IMAGE"',
1544
1673
  '',
@@ -1613,6 +1742,7 @@ function cloudrunArtifacts(ctx) {
1613
1742
  ' -d \'{"message":"Reply with exactly: CLOUDRUN_OK","session_id":"cr-1"}\'',
1614
1743
  '```',
1615
1744
  '',
1745
+ ...renderEgressNote(ctx),
1616
1746
  ].join('\n'),
1617
1747
  };
1618
1748
  }
@@ -1624,7 +1754,7 @@ function cloudrunEdgeArtifacts(ctx, inbound, isCursor) {
1624
1754
  const files = {
1625
1755
  'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
1626
1756
  'www/index.html': renderLandingPage(ctx.packName),
1627
- 'Caddyfile': renderCloudrunEdgeCaddyfile(),
1757
+ 'Caddyfile': (0, identity_1.renderSidecarEdgeCaddyfile)(),
1628
1758
  'service.yaml': renderCloudrunEdgeServiceYaml(ctx),
1629
1759
  'cloudrun-deploy.sh': renderCloudrunEdgeDeploy(ctx),
1630
1760
  };
@@ -1638,7 +1768,7 @@ function cloudrunEdgeArtifacts(ctx, inbound, isCursor) {
1638
1768
  '',
1639
1769
  `${sidecars} Sidecars share the pod\'s localhost (Cloud Run multi-container):`,
1640
1770
  '- **caddy** (ingress, the only port) → SSO edge: oauth2-proxy for browsers, auth-broker for IDE Bearer',
1641
- `- **agent** → serves www + /v1/* on localhost:${EDGE_AGENT_PORT} (NOT ingress)`,
1771
+ `- **agent** → serves www + /v1/* on localhost:${identity_1.EDGE_AGENT_PORT} (NOT ingress)`,
1642
1772
  '- **auth-broker** → validates IDE AgentHippo JWTs (`/api/ide/me`) + IDE sign-in (`/oauth/*`)',
1643
1773
  '- **oauth2-proxy** → Google SSO for browser clients (`provider=google`)',
1644
1774
  ...(isCursor ? [] : [
@@ -1671,77 +1801,10 @@ function cloudrunEdgeArtifacts(ctx, inbound, isCursor) {
1671
1801
  ? '**Inbound channel (1b):** `minScale: 1` pins one always-on instance (the socket can\'t scale to zero).'
1672
1802
  : '**No inbound channel (1a):** scales to zero; traces + sessions persist on the GCS `/state` volume.',
1673
1803
  '',
1804
+ ...renderEgressNote(ctx),
1674
1805
  ].join('\n');
1675
1806
  return files;
1676
1807
  }
1677
- /**
1678
- * A′ — Federated edge Caddyfile. No oauth2-proxy: Google login happens once at the central IdP
1679
- * (`ctx.idpUrl`); this agent only verifies the shared AgentHippo JWT (Bearer for IDE, cookie for
1680
- * browser). On a browser 401, redirect to the IdP's `/oauth/browser-login` to stamp the shared
1681
- * `.agenthippo.ai` cookie, then return.
1682
- */
1683
- function renderCloudrunFedCaddyfile(ctx) {
1684
- const idp = (ctx.idpUrl || '').trim().replace(/\/+$/, '');
1685
- const loginRedirect = idp
1686
- ? ` redir * "${idp}/oauth/browser-login?return_to=https://{http.request.host}{http.request.uri}" 302`
1687
- : ' respond "Unauthorized" 401';
1688
- return [
1689
- '# Cloud Run federated edge (A′). Cloud Run terminates TLS; Caddy listens on HTTP :{$PORT}.',
1690
- `# No oauth2-proxy — Google login happens once at the central IdP (${idp || '<IDP_URL>'});`,
1691
- '# this agent only verifies the shared .agenthippo.ai AgentHippo JWT (Bearer for IDE, cookie for browser).',
1692
- '{',
1693
- ' auto_https off',
1694
- '}',
1695
- '',
1696
- ':{$PORT} {',
1697
- ' # Auth-broker: IDE bootstrap API + JWT validation (/api/ide/me is also used by forward_auth).',
1698
- ' handle /api/ide/* {',
1699
- ' reverse_proxy localhost:9780 {',
1700
- ' header_up X-Forwarded-Proto https',
1701
- ' header_up X-Forwarded-Host {http.request.host}',
1702
- ' }',
1703
- ' }',
1704
- ' @health path /health /ready',
1705
- ' handle @health {',
1706
- ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1707
- ' }',
1708
- ' # IDE / API clients with a Bearer token: validate via auth-broker, copy identity header.',
1709
- ' @apiWithAuth {',
1710
- ' path /v1/*',
1711
- ' header Authorization *',
1712
- ' }',
1713
- ' handle @apiWithAuth {',
1714
- ' forward_auth localhost:9780 {',
1715
- ' uri /api/ide/me',
1716
- ' copy_headers X-Agent-User',
1717
- ' header_up X-Forwarded-Proto https',
1718
- ' header_up X-Forwarded-Host {http.request.host}',
1719
- ' @unauth status 401',
1720
- ' handle_response @unauth {',
1721
- ' respond "Unauthorized" 401',
1722
- ' }',
1723
- ' }',
1724
- ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1725
- ' }',
1726
- ' # Browser clients: the shared .agenthippo.ai cookie is verified by auth-broker. No cookie',
1727
- ' # → redirect to the central IdP to log in (it stamps the cookie), then return.',
1728
- ' handle {',
1729
- ' forward_auth localhost:9780 {',
1730
- ' uri /api/ide/me',
1731
- ' copy_headers X-Agent-User',
1732
- ' header_up X-Forwarded-Proto https',
1733
- ' header_up X-Forwarded-Host {http.request.host}',
1734
- ' @unauth status 401',
1735
- ' handle_response @unauth {',
1736
- loginRedirect,
1737
- ' }',
1738
- ' }',
1739
- ` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
1740
- ' }',
1741
- '}',
1742
- '',
1743
- ].join('\n');
1744
- }
1745
1808
  /** Cloud Run requires a startupProbe on any container that is a `depends_on` target, so the
1746
1809
  * platform knows when the dependency is ready. `slow` = one generous 240s attempt (agent, which
1747
1810
  * may warm an engine); otherwise 30×5s polls (sidecars that start fast). */
@@ -1784,7 +1847,7 @@ function resourceLimits(cpu, memory) {
1784
1847
  function renderCloudrunFedServiceYaml(ctx) {
1785
1848
  const isCursor = ctx.engine.trim().toLowerCase() === 'cursor';
1786
1849
  const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
1787
- const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: EDGE_AGENT_PORT })
1850
+ const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: identity_1.EDGE_AGENT_PORT })
1788
1851
  .map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ');
1789
1852
  const cookieDomain = (ctx.cookieDomain || '.agenthippo.ai').trim();
1790
1853
  const idpUrl = (ctx.idpUrl || 'https://auth.agenthippo.ai').trim().replace(/\/+$/, '');
@@ -1860,11 +1923,13 @@ function renderCloudrunFedServiceYaml(ctx) {
1860
1923
  ]),
1861
1924
  ' volumeMounts:',
1862
1925
  ' - { name: state, mountPath: /state }',
1863
- ...startupProbeLines(EDGE_AGENT_PORT, true),
1926
+ ...startupProbeLines(identity_1.EDGE_AGENT_PORT, true),
1864
1927
  ...resourceLimits('2000m', '2Gi'),
1865
1928
  ];
1866
1929
  if (!isCursor) {
1867
- lines.push(' # LiteLLM gateway sidecar — exports every call to the collector (Spotlight).', ' - name: litellm', ' image: ${LITELLM_IMAGE}', ' env:', ' - { name: OTEL_EXPORTER, value: otlp_http }', ' - { name: OTEL_EXPORTER_OTLP_ENDPOINT, value: http://localhost:4318 }', ' - name: LITELLM_MASTER_KEY', ' valueFrom:', ' secretKeyRef: { name: ${SERVICE}-litellm-key, key: latest }', ` # Upstream provider key for ${ctx.model || 'this model'} (matches the gateway's os.environ/${providerKeyEnv} ref).`, ` - name: ${providerKeyEnv}`, ' valueFrom:', ' secretKeyRef: { name: ${SERVICE}-provider-key, key: latest }', ...startupProbeLines(4443), ...resourceLimits('1000m', '1Gi'), ' # Spotlight OTEL collector sidecar — writes JSONL traces into the shared /state volume.', ' - name: otel-collector', ' image: otel/opentelemetry-collector-contrib:0.118.0', ' args: ["--config=/etc/otel/config.yaml"]', ' env:', ' - { name: AGENT_HOME_DIR, value: /state }', ' volumeMounts:', ' - { name: otel-config, mountPath: /etc/otel, readOnly: true }', ' - { name: state, mountPath: /state }', ...startupProbeLines(4318), ...resourceLimits('1000m', '512Mi'));
1930
+ lines.push(' # LiteLLM gateway sidecar — exports every call to the collector (Spotlight).', ' - name: litellm', ' image: ${LITELLM_IMAGE}', ' env:', ' - { name: OTEL_EXPORTER, value: otlp_http }', ' - { name: OTEL_EXPORTER_OTLP_ENDPOINT, value: http://localhost:4318 }', ' - name: LITELLM_MASTER_KEY', ' valueFrom:', ' secretKeyRef: { name: ${SERVICE}-litellm-key, key: latest }', isAhAutoModel(ctx.model)
1931
+ ? ' # ah-auto → gateway.agenthippo.ai (AGENTHIPPO_GATEWAY_KEY = ahgw_… service key).'
1932
+ : ` # Upstream provider key for ${ctx.model || 'this model'} (matches os.environ/${providerKeyEnv}).`, ` - name: ${providerKeyEnv}`, ' valueFrom:', ' secretKeyRef: { name: ${SERVICE}-provider-key, key: latest }', ...startupProbeLines(4443), ...resourceLimits('1000m', '1Gi'), ' # Spotlight OTEL collector sidecar — writes JSONL traces into the shared /state volume.', ' - name: otel-collector', ' image: otel/opentelemetry-collector-contrib:0.118.0', ' args: ["--config=/etc/otel/config.yaml"]', ' env:', ' - { name: AGENT_HOME_DIR, value: /state }', ' volumeMounts:', ' - { name: otel-config, mountPath: /etc/otel, readOnly: true }', ' - { name: state, mountPath: /state }', ...startupProbeLines(4318), ...resourceLimits('1000m', '512Mi'));
1868
1933
  }
1869
1934
  lines.push(' volumes:', ' # In-memory state (matches the working spotlight deploy — no GCS SA perms needed).', ' - name: state', ' emptyDir:', ' medium: Memory', ' sizeLimit: 1Gi', ' - name: caddy-config', ' secret:', ' secretName: ${SERVICE}-caddyfile', ' items:', ' - { key: latest, path: Caddyfile }');
1870
1935
  if (!isCursor) {
@@ -1873,6 +1938,141 @@ function renderCloudrunFedServiceYaml(ctx) {
1873
1938
  lines.push('');
1874
1939
  return lines.join('\n');
1875
1940
  }
1941
+ /** A′ gateway-direct — federated edge without LiteLLM/OTEL sidecars. Agent calls the org gateway
1942
+ * (gateway.agenthippo.ai) directly — same wiring as colocated agents on hippopennyg. */
1943
+ function renderCloudrunFedGatewayServiceYaml(ctx) {
1944
+ const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: identity_1.EDGE_AGENT_PORT })
1945
+ .map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ');
1946
+ const cookieDomain = (ctx.cookieDomain || '.agenthippo.ai').trim();
1947
+ const idpUrl = (ctx.idpUrl || 'https://auth.agenthippo.ai').trim().replace(/\/+$/, '');
1948
+ const lines = [
1949
+ '# Cloud Run + AgentHippo federated edge (A′) — gateway-direct (lean):',
1950
+ '# Caddy (ingress) + auth-broker + agent. No LiteLLM/OTEL sidecars.',
1951
+ '# Agent routes ah-auto (and other gateway models) to gateway.agenthippo.ai directly.',
1952
+ '# Generated by `agenthippo deploy init --target cloudrun --ingress edge-fed --model-routing gateway`.',
1953
+ 'apiVersion: serving.knative.dev/v1',
1954
+ 'kind: Service',
1955
+ 'metadata:',
1956
+ ' name: ${SERVICE}',
1957
+ 'spec:',
1958
+ ' template:',
1959
+ ' metadata:',
1960
+ ' annotations:',
1961
+ ' run.googleapis.com/cpu-throttling: "false"',
1962
+ ' run.googleapis.com/startup-cpu-boost: "true"',
1963
+ " run.googleapis.com/container-dependencies: '{\"caddy\":[\"agent\",\"auth-broker\"]}'",
1964
+ ' autoscaling.knative.dev/maxScale: "1"',
1965
+ ...(ctx.gatewayChannels?.trim() ? [' autoscaling.knative.dev/minScale: "1"'] : []),
1966
+ ' spec:',
1967
+ ' timeoutSeconds: 3600',
1968
+ ' containers:',
1969
+ ' - name: caddy',
1970
+ ' image: caddy:2.8-alpine',
1971
+ ' ports:',
1972
+ ' - name: http1',
1973
+ ` containerPort: ${ctx.port}`,
1974
+ ' volumeMounts:',
1975
+ ' - { name: caddy-config, mountPath: /etc/caddy, readOnly: true }',
1976
+ ...resourceLimits('1000m', '512Mi'),
1977
+ ' - name: auth-broker',
1978
+ ' image: ${IMAGE}',
1979
+ ' args: ["auth-broker"]',
1980
+ ' env:',
1981
+ ' - { name: AGENTHIPPO_AUTH_PORT, value: "9780" }',
1982
+ ' - { name: AGENTHIPPO_AUTH_DATA_DIR, value: /data }',
1983
+ ' - { name: AGENTHIPPO_AUTH_PUBLIC_URL, value: https://${PUBLIC_HOSTNAME} }',
1984
+ ' - { name: AGENTHIPPO_AUTH_ALLOWED_DOMAINS, value: ${ALLOWED_DOMAINS} }',
1985
+ ` - { name: AGENTHIPPO_AUTH_COOKIE_DOMAIN, value: "${cookieDomain}" }`,
1986
+ ` - { name: AGENTHIPPO_AUTH_IDP_URL, value: "${idpUrl}" }`,
1987
+ ' - { name: AGENTHIPPO_AUTH_COOKIE_NAME, value: "ah_token" }',
1988
+ ' - name: AGENTHIPPO_AUTH_JWT_SECRET',
1989
+ ' valueFrom:',
1990
+ ' secretKeyRef: { name: ${SERVICE}-auth-jwt-secret, key: latest }',
1991
+ ...startupProbeLines(9780),
1992
+ ...resourceLimits('1000m', '512Mi'),
1993
+ ' - name: agent',
1994
+ ' image: ${IMAGE}',
1995
+ ` args: [${agentArgs}]`,
1996
+ ' env:',
1997
+ ' - { name: AGENTHIPPO_HOME, value: /state }',
1998
+ ' - { name: AGENTHIPPO_NO_AUTO_INSTALL, value: "1" }',
1999
+ agentServeHostEnvYaml(),
2000
+ ` - { name: AGENTHIPPO_LITELLM_BASE_URL, value: "${AGENTHIPPO_GATEWAY_BASE_URL}" }`,
2001
+ ' - name: AGENTHIPPO_LITELLM_API_KEY',
2002
+ ' valueFrom:',
2003
+ ' secretKeyRef: { name: ${SERVICE}-provider-key, key: latest }',
2004
+ ' volumeMounts:',
2005
+ ' - { name: state, mountPath: /state }',
2006
+ ...startupProbeLines(identity_1.EDGE_AGENT_PORT, true),
2007
+ ...resourceLimits('2000m', '2Gi'),
2008
+ ' volumes:',
2009
+ ' - name: state',
2010
+ ' emptyDir:',
2011
+ ' medium: Memory',
2012
+ ' sizeLimit: 1Gi',
2013
+ ' - name: caddy-config',
2014
+ ' secret:',
2015
+ ' secretName: ${SERVICE}-caddyfile',
2016
+ ' items:',
2017
+ ' - { key: latest, path: Caddyfile }',
2018
+ '',
2019
+ ];
2020
+ return lines.join('\n');
2021
+ }
2022
+ /** Deploy script for gateway-direct federated edge — no LiteLLM/OTEL secrets or sidecars. */
2023
+ function renderCloudrunFedGatewayDeploy(ctx) {
2024
+ const modelNote = ctx.model?.trim() ? `, model ${ctx.model.trim()}` : '';
2025
+ const providerHint = isAhAutoModel(ctx.model)
2026
+ ? 'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<ahgw_… from create-gateway-key on gateway host>}"'
2027
+ : 'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<gateway service key or provider key>}"';
2028
+ return [
2029
+ '#!/usr/bin/env bash',
2030
+ '# Cloud Run + AgentHippo federated edge (A′) — gateway-direct (lean).',
2031
+ `# Engine: ${ctx.engine}${modelNote}. Caddy + auth-broker + agent only — agent calls`,
2032
+ '# gateway.agenthippo.ai directly (no in-pod LiteLLM/OTEL sidecars). Same model routing',
2033
+ '# as colocated agents on hippopennyg.',
2034
+ 'set -euo pipefail',
2035
+ ...anchorToStagingRoot(),
2036
+ `SERVICE="${ctx.packName}"`,
2037
+ 'REGION="${REGION:-us-central1}"',
2038
+ 'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
2039
+ 'REPO="${ARTIFACT_REPO:-agenthippo}"',
2040
+ 'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
2041
+ 'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai}"',
2042
+ 'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:-agenthippo.ai}"',
2043
+ 'AUTH_JWT_SECRET="${AUTH_JWT_SECRET:?set AUTH_JWT_SECRET=<copy from auth.agenthippo.ai>}"',
2044
+ providerHint,
2045
+ '',
2046
+ 'gcloud artifacts repositories describe "$REPO" --location="$REGION" 2>/dev/null \\',
2047
+ ' || gcloud artifacts repositories create "$REPO" --repository-format=docker --location="$REGION"',
2048
+ 'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
2049
+ '',
2050
+ 'DF_TMP="$(mktemp)"',
2051
+ 'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
2052
+ 'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
2053
+ 'docker push "$IMAGE"',
2054
+ '',
2055
+ 'ensure_secret() { gcloud secrets describe "$1" >/dev/null 2>&1 || gcloud secrets create "$1" --replication-policy=automatic; }',
2056
+ 'ensure_secret "${SERVICE}-caddyfile"; gcloud secrets versions add "${SERVICE}-caddyfile" --data-file=deploy/Caddyfile',
2057
+ 'ensure_secret "${SERVICE}-auth-jwt-secret"; printf %s "$AUTH_JWT_SECRET" | gcloud secrets versions add "${SERVICE}-auth-jwt-secret" --data-file=-',
2058
+ 'ensure_secret "${SERVICE}-provider-key"; printf %s "$PROVIDER_KEY" | gcloud secrets versions add "${SERVICE}-provider-key" --data-file=-',
2059
+ '',
2060
+ 'RENDERED="$(mktemp)"',
2061
+ 'sed -e "s|\\${SERVICE}|${SERVICE}|g" -e "s|\\${IMAGE}|${IMAGE}|g" \\',
2062
+ ' -e "s|\\${PUBLIC_HOSTNAME}|${PUBLIC_HOSTNAME}|g" -e "s|\\${ALLOWED_DOMAINS}|${ALLOWED_DOMAINS}|g" \\',
2063
+ ' deploy/service.yaml > "$RENDERED"',
2064
+ 'gcloud run services replace "$RENDERED" --region "$REGION"',
2065
+ '',
2066
+ 'gcloud run services add-iam-policy-binding "$SERVICE" --region "$REGION" \\',
2067
+ ' --member=allUsers --role=roles/run.invoker >/dev/null 2>&1 || true',
2068
+ '',
2069
+ 'if [[ "$PUBLIC_HOSTNAME" != *.run.app ]]; then \\',
2070
+ ' gcloud beta run domain-mappings describe "$PUBLIC_HOSTNAME" --region "$REGION" >/dev/null 2>&1 \\',
2071
+ ' || gcloud beta run domain-mappings create --service "$SERVICE" --region "$REGION" --domain "$PUBLIC_HOSTNAME"; \\',
2072
+ 'fi',
2073
+ '',
2074
+ ].join('\n');
2075
+ }
1876
2076
  /** Deploy script for the federated edge (A′): build, upsert secrets (no OAuth), replace, make
1877
2077
  * public, map the subdomain. No IAP; no Google OAuth client. */
1878
2078
  function renderCloudrunFedDeploy(ctx) {
@@ -1892,7 +2092,7 @@ function renderCloudrunFedDeploy(ctx) {
1892
2092
  'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
1893
2093
  'REPO="${ARTIFACT_REPO:-agenthippo}"',
1894
2094
  'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
1895
- 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippo/litellm-gateway:2026.06}"',
2095
+ 'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippoai/litellm-gateway:2026.07}"',
1896
2096
  '# Public hostname = a *.agenthippo.ai subdomain mapped to this service (one-time wildcard DNS).',
1897
2097
  'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai}"',
1898
2098
  'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:-agenthippo.ai}"',
@@ -1901,6 +2101,10 @@ function renderCloudrunFedDeploy(ctx) {
1901
2101
  'AUTH_JWT_SECRET="${AUTH_JWT_SECRET:?set AUTH_JWT_SECRET=<copy from auth.agenthippo.ai>}"',
1902
2102
  ...(isCursor ? [
1903
2103
  'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<cursor api key>}"',
2104
+ ] : isAhAutoModel(ctx.model) ? [
2105
+ '# LiteLLM proxy key + AgentHippo gateway service key (ahgw_…) for ah-auto routing.',
2106
+ 'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
2107
+ 'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<ahgw_… from create-gateway-key on gateway host>}"',
1904
2108
  ] : [
1905
2109
  '# LiteLLM proxy key + the upstream provider key the gateway uses for the model.',
1906
2110
  'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
@@ -1912,10 +2116,10 @@ function renderCloudrunFedDeploy(ctx) {
1912
2116
  'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
1913
2117
  '',
1914
2118
  '# One agent image serves both the agent and the auth-broker containers (CLI ships `auth-broker`).',
1915
- '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippo/runtime:latest',
2119
+ '# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
1916
2120
  '# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
1917
2121
  'DF_TMP="$(mktemp)"',
1918
- 'sed "s|FROM ghcr.io/agenthippo/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
2122
+ 'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
1919
2123
  'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
1920
2124
  'docker push "$IMAGE"',
1921
2125
  '',
@@ -1933,30 +2137,36 @@ function renderCloudrunFedDeploy(ctx) {
1933
2137
  }
1934
2138
  /** A′ artifacts: Cloud Run behind the federated AgentHippo edge (Caddy + auth-broker; no oauth2-proxy). */
1935
2139
  function cloudrunFedArtifacts(ctx, inbound, isCursor) {
2140
+ const gatewayDirect = usesGatewayDirect(ctx) && !isCursor;
1936
2141
  const sidecars = isCursor
1937
2142
  ? 'Caddy (ingress) + auth-broker + agent.'
1938
- : 'Caddy (ingress) + auth-broker + agent + LiteLLM + OTEL collector (Spotlight).';
2143
+ : gatewayDirect
2144
+ ? 'Caddy (ingress) + auth-broker + agent (gateway-direct — no LiteLLM/OTEL sidecars).'
2145
+ : 'Caddy (ingress) + auth-broker + agent + LiteLLM + OTEL collector (Spotlight).';
1939
2146
  const idp = (ctx.idpUrl || 'https://auth.agenthippo.ai').trim().replace(/\/+$/, '');
1940
2147
  const files = {
1941
2148
  'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
1942
2149
  'www/index.html': renderLandingPage(ctx.packName),
1943
- 'Caddyfile': renderCloudrunFedCaddyfile(ctx),
1944
- 'service.yaml': renderCloudrunFedServiceYaml(ctx),
1945
- 'cloudrun-deploy.sh': renderCloudrunFedDeploy(ctx),
2150
+ 'Caddyfile': (0, identity_1.renderSidecarFedCaddyfile)(ctx.idpUrl),
2151
+ 'service.yaml': gatewayDirect ? renderCloudrunFedGatewayServiceYaml(ctx) : renderCloudrunFedServiceYaml(ctx),
2152
+ 'cloudrun-deploy.sh': gatewayDirect ? renderCloudrunFedGatewayDeploy(ctx) : renderCloudrunFedDeploy(ctx),
1946
2153
  };
1947
- if (!isCursor) {
2154
+ if (!isCursor && !gatewayDirect) {
1948
2155
  files['otel-collector.yaml'] = OTEL_COLLECTOR_COMPOSE_YAML;
1949
2156
  }
1950
2157
  files['README.md'] = [
1951
- `# Deploy: ${ctx.packName} — Cloud Run + AgentHippo federated edge (A′)`,
2158
+ `# Deploy: ${ctx.packName} — Cloud Run + AgentHippo federated edge (A′${gatewayDirect ? ', gateway-direct' : ''})`,
1952
2159
  '',
1953
2160
  `Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}. Runtime base: \`${ctx.image}\`.`,
1954
2161
  '',
1955
2162
  `${sidecars} Sidecars share the pod\'s localhost (Cloud Run multi-container):`,
1956
2163
  '- **caddy** (ingress, the only port) → verifies the shared AgentHippo JWT (Bearer for IDE, cookie for browser)',
1957
- `- **agent** → serves www + /v1/* on localhost:${EDGE_AGENT_PORT} (NOT ingress)`,
2164
+ `- **agent** → serves www + /v1/* on localhost:${identity_1.EDGE_AGENT_PORT} (NOT ingress)`,
1958
2165
  '- **auth-broker** → validates the shared `.agenthippo.ai` JWT via `/api/ide/me` (same secret as the IdP)',
1959
- ...(isCursor ? [] : [
2166
+ ...(gatewayDirect ? [
2167
+ `- **model routing** → agent calls \`${AGENTHIPPO_GATEWAY_BASE_URL}\` directly (ahgw_… service key) — same as colocated agents`,
2168
+ '- **no Spotlight sidecars** — traces are not collected in-pod; use gateway/Spotlight fleet analytics instead',
2169
+ ] : isCursor ? [] : [
1960
2170
  '- **litellm** gateway → exports OTLP to `http://localhost:4318`',
1961
2171
  '- **otel-collector** → writes JSONL traces to the in-memory `/state` volume',
1962
2172
  ]),
@@ -1971,23 +2181,31 @@ function cloudrunFedArtifacts(ctx, inbound, isCursor) {
1971
2181
  'cp deploy/.env.example deploy/.env && cp deploy/.env.auth.example deploy/.env.auth # fill both',
1972
2182
  'export PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai # e.g. cs.agenthippo.ai',
1973
2183
  'export AUTH_JWT_SECRET=<copy from auth.agenthippo.ai> # shared signing key',
1974
- ...(isCursor ? ['export PROVIDER_KEY=<cursor api key>'] : ['export LITELLM_KEY=… PROVIDER_KEY=…']),
2184
+ ...(isCursor ? ['export PROVIDER_KEY=<cursor api key>'] : gatewayDirect
2185
+ ? ['export PROVIDER_KEY=<ahgw_… from create-gateway-key on gateway host>']
2186
+ : isAhAutoModel(ctx.model)
2187
+ ? ['export LITELLM_KEY=… PROVIDER_KEY=<ahgw_… from create-gateway-key on gateway host>']
2188
+ : ['export LITELLM_KEY=… PROVIDER_KEY=…']),
1975
2189
  'bash deploy/cloudrun-deploy.sh # builds linux/amd64, upserts secrets, `gcloud run services replace`, maps the subdomain',
1976
2190
  '```',
1977
2191
  '',
1978
2192
  '**One-time setup (not per agent):** wildcard DNS `*.agenthippo.ai` → Cloud Run (`gctrl.run.app`),',
1979
2193
  `and the IdP (${idp}) must run the \`/oauth/browser-login\` route (oauth2-proxy forward_auth →`,
1980
2194
  'auth-broker sets the `ah_token` cookie on `.agenthippo.ai`). After that, every new agent is',
1981
- 'just: `deploy init --ingress edge-fed` + this script — zero Console/OAuth work.',
2195
+ gatewayDirect
2196
+ ? 'just: `deploy init --ingress edge-fed --model-routing gateway` + this script — zero Console/OAuth work.'
2197
+ : 'just: `deploy init --ingress edge-fed` + this script — zero Console/OAuth work.',
1982
2198
  '',
1983
- '`service.yaml` placeholders (`${SERVICE} ${IMAGE} ${LITELLM_IMAGE} ${PUBLIC_HOSTNAME} ${ALLOWED_DOMAINS}`)',
1984
- 'are substituted by the deploy script. The Caddyfile is stored in Secret Manager (`${SERVICE}-caddyfile`).',
2199
+ gatewayDirect
2200
+ ? '`service.yaml` placeholders (`${SERVICE} ${IMAGE} ${PUBLIC_HOSTNAME} ${ALLOWED_DOMAINS}`) are substituted by the deploy script. The Caddyfile is stored in Secret Manager (`${SERVICE}-caddyfile`).'
2201
+ : '`service.yaml` placeholders (`${SERVICE} ${IMAGE} ${LITELLM_IMAGE} ${PUBLIC_HOSTNAME} ${ALLOWED_DOMAINS}`) are substituted by the deploy script. The Caddyfile is stored in Secret Manager (`${SERVICE}-caddyfile`).',
1985
2202
  '`AGENTHIPPO_AUTH_COOKIE_DOMAIN` / `AGENTHIPPO_AUTH_IDP_URL` are baked in at generation time.',
1986
2203
  '',
1987
2204
  inbound
1988
2205
  ? '**Inbound channel (1b):** `minScale: 1` pins one always-on instance (the socket can\'t scale to zero).'
1989
2206
  : '**No inbound channel (1a):** scales to zero; in-memory `/state` is wiped on cold start (use the `edge` mode + GCS bucket if you need durable state).',
1990
2207
  '',
2208
+ ...renderEgressNote(ctx),
1991
2209
  ].join('\n');
1992
2210
  return files;
1993
2211
  }
@@ -2029,7 +2247,10 @@ function databricksAppArtifacts(ctx) {
2029
2247
  " // Databricks Apps workspace SSO sets X-Forwarded-Email; pin serve to trust ONLY that",
2030
2248
  " // header so an authenticated caller cannot impersonate another user via a spoofed",
2031
2249
  " // X-Agent-User header (Databricks does not know about or strip that header).",
2032
- " AGENTHIPPO_IDENTITY_HEADER: 'x-forwarded-email',",
2250
+ " // Skipped when AGENTHIPPO_PIN_IDENTITY=0 (bootstrap escape hatch) — same idea as",
2251
+ " // Cloud Run's ENABLE_IAP=0: pinning to an absent header would make every request",
2252
+ " // anonymous instead of leaving body-supplied identity usable during bootstrap.",
2253
+ " ...(process.env.AGENTHIPPO_PIN_IDENTITY === '0' ? {} : { AGENTHIPPO_IDENTITY_HEADER: 'x-forwarded-email' }),",
2033
2254
  "} });",
2034
2255
  "child.on('exit', (code) => process.exit(code ?? 0));",
2035
2256
  '',
@@ -2054,6 +2275,7 @@ function databricksAppArtifacts(ctx) {
2054
2275
  '> spike (serve-v3 §13.2). Without it serve queries as one app identity. Identity',
2055
2276
  '> attribution (`user_id`) works today; per-user *data* enforcement needs the spike.',
2056
2277
  '',
2278
+ ...renderEgressNote(ctx),
2057
2279
  ].join('\n'),
2058
2280
  };
2059
2281
  }
@@ -2135,7 +2357,10 @@ function cloudflareArtifacts(ctx) {
2135
2357
  migrations: [{ tag: 'v1', new_sqlite_classes: ['Agent'] }],
2136
2358
  }, null, 2) + '\n';
2137
2359
  return {
2138
- 'Dockerfile': sharedDockerfile(ctx),
2360
+ // Pin identity to X-Agent-User so serve trusts ONLY the header the fronting Worker sets —
2361
+ // unpinned, serve falls back to the same header name as an alias and a caller who reaches
2362
+ // the container directly (bypassing the Worker/Access) could spoof it themselves.
2363
+ 'Dockerfile': sharedDockerfile(ctx, { extraEnv: { AGENTHIPPO_IDENTITY_HEADER: 'x-agent-user' } }),
2139
2364
  'wrangler.jsonc': wrangler,
2140
2365
  'README.md': [
2141
2366
  `# Deploy: ${ctx.packName} — Cloudflare Container (T1-generic)`,
@@ -2145,6 +2370,537 @@ function cloudflareArtifacts(ctx) {
2145
2370
  'then proxies to the container binding. For the *external/unauthenticated* case',
2146
2371
  '(customer support), use the full reference at `examples/deploy/external-support/`.',
2147
2372
  '',
2373
+ '**Identity pin:** the container trusts *only* `X-Agent-User` (`AGENTHIPPO_IDENTITY_HEADER`,',
2374
+ 'baked into the image) — the Worker must set it after Cloudflare Access verifies the user.',
2375
+ 'Requests without it resolve to no identity, even if the container is ever reachable',
2376
+ 'directly, bypassing the Worker.',
2377
+ '',
2378
+ '**Egress:** this container only needs outbound access to your model provider',
2379
+ '(e.g. `api.anthropic.com:443`) — restrict it with a Cloudflare Worker/Zero Trust',
2380
+ 'egress policy if your org requires one.',
2381
+ '',
2382
+ ].join('\n'),
2383
+ };
2384
+ }
2385
+ /**
2386
+ * AWS ECS Fargate (T1-generic, `d8-aws-adapter`). Fargate's `awsvpc` network mode shares one
2387
+ * task's network namespace across its containers — architecturally identical to Cloud Run's
2388
+ * sidecar model, so the edge/edge-fed Caddyfiles and `EDGE_AGENT_PORT` convention are reused
2389
+ * verbatim (PLAN/deploy.md §5, "what adding AWS looks like"): no new identity code, just a new
2390
+ * platform adapter around the same shared identity layer.
2391
+ *
2392
+ * Scope: identity/exposure only (bare / edge / edge-fed) — no Spotlight/LiteLLM sidecar variant
2393
+ * yet (that's the orthogonal model-gateway layer, not part of this pass). Newer and far less
2394
+ * exercised than the Cloud Run path — read `deploy.sh` before running it against a real account.
2395
+ */
2396
+ function awsEcsArtifacts(ctx) {
2397
+ const fed = ctx.ingress === 'edge-fed';
2398
+ const edge = ctx.ingress === 'edge' || fed;
2399
+ const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
2400
+ if (!edge) {
2401
+ // Bare single-container task — sits behind whatever ingress (ALB, API Gateway, internal
2402
+ // VPC access) the org already runs, same positioning as cloudflare/databricks-app/spcs.
2403
+ const taskDef = {
2404
+ family: '${SERVICE}',
2405
+ networkMode: 'awsvpc',
2406
+ requiresCompatibilities: ['FARGATE'],
2407
+ cpu: '512',
2408
+ memory: '1024',
2409
+ containerDefinitions: [
2410
+ {
2411
+ name: 'agent',
2412
+ image: '${IMAGE}',
2413
+ essential: true,
2414
+ portMappings: [{ containerPort: ctx.port, protocol: 'tcp' }],
2415
+ environment: [{ name: 'AGENTHIPPO_NO_AUTO_INSTALL', value: '1' }],
2416
+ secrets: [{ name: providerKeyEnv, valueFrom: '${PROVIDER_KEY_SECRET_ARN}' }],
2417
+ logConfiguration: {
2418
+ logDriver: 'awslogs',
2419
+ options: {
2420
+ 'awslogs-group': '/ecs/${SERVICE}',
2421
+ 'awslogs-region': '${REGION}',
2422
+ 'awslogs-stream-prefix': 'agent',
2423
+ },
2424
+ },
2425
+ },
2426
+ ],
2427
+ };
2428
+ const deploy = [
2429
+ '#!/usr/bin/env bash',
2430
+ '# T1-generic: AWS ECS Fargate, single container — bring your own ingress (ALB/API',
2431
+ '# Gateway/internal VPC). For a self-contained ALB + AgentHippo edge, use --ingress edge',
2432
+ '# or --ingress edge-fed instead.',
2433
+ 'set -euo pipefail',
2434
+ ...anchorToStagingRoot(),
2435
+ 'SERVICE="' + ctx.packName + '"',
2436
+ 'REGION="${AWS_REGION:-us-east-1}"',
2437
+ 'ACCOUNT_ID="${ACCOUNT_ID:-$(aws sts get-caller-identity --query Account --output text)}"',
2438
+ 'REPO="agenthippo/${SERVICE}"',
2439
+ 'IMAGE="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPO}:latest"',
2440
+ '',
2441
+ 'aws ecr describe-repositories --repository-names "$REPO" --region "$REGION" >/dev/null 2>&1 \\',
2442
+ ' || aws ecr create-repository --repository-name "$REPO" --region "$REGION"',
2443
+ 'aws ecr get-login-password --region "$REGION" \\',
2444
+ ' | docker login --username AWS --password-stdin "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com"',
2445
+ '',
2446
+ 'docker build --platform linux/amd64 -f deploy/Dockerfile -t "$IMAGE" .',
2447
+ 'docker push "$IMAGE"',
2448
+ '',
2449
+ `PROVIDER_KEY_SECRET_ARN="\${PROVIDER_KEY_SECRET_ARN:?set PROVIDER_KEY_SECRET_ARN — Secrets Manager ARN holding ${providerKeyEnv}}"`,
2450
+ 'TASK_JSON="$(sed -e "s|\\${IMAGE}|${IMAGE}|g" -e "s|\\${SERVICE}|${SERVICE}|g" \\',
2451
+ ' -e "s|\\${REGION}|${REGION}|g" -e "s|\\${PROVIDER_KEY_SECRET_ARN}|${PROVIDER_KEY_SECRET_ARN}|g" \\',
2452
+ ' deploy/task-definition.json)"',
2453
+ 'aws ecs register-task-definition --region "$REGION" --cli-input-json "$TASK_JSON" >/dev/null',
2454
+ '',
2455
+ 'CLUSTER="${CLUSTER:?set CLUSTER — the ECS cluster to run in}"',
2456
+ 'aws ecs describe-services --cluster "$CLUSTER" --services "$SERVICE" --region "$REGION" 2>/dev/null \\',
2457
+ ' | grep -q \'"status": "ACTIVE"\' \\',
2458
+ ' && aws ecs update-service --cluster "$CLUSTER" --service "$SERVICE" \\',
2459
+ ' --task-definition "$SERVICE" --region "$REGION" >/dev/null \\',
2460
+ ' || aws ecs create-service --cluster "$CLUSTER" --service-name "$SERVICE" \\',
2461
+ ' --task-definition "$SERVICE" --desired-count 1 --launch-type FARGATE \\',
2462
+ ' --network-configuration "awsvpcConfiguration={subnets=[${SUBNETS:?set SUBNETS}],securityGroups=[${SECURITY_GROUPS:?set SECURITY_GROUPS}],assignPublicIp=ENABLED}" \\',
2463
+ ' --region "$REGION" >/dev/null',
2464
+ 'echo "Deployed $SERVICE to ECS cluster $CLUSTER"',
2465
+ '',
2466
+ ].join('\n');
2467
+ return {
2468
+ 'Dockerfile': sharedDockerfile(ctx),
2469
+ 'task-definition.json': JSON.stringify(taskDef, null, 2) + '\n',
2470
+ 'deploy.sh': deploy,
2471
+ 'README.md': [
2472
+ `# Deploy: ${ctx.packName} — AWS ECS Fargate (T1-generic)`,
2473
+ '',
2474
+ `Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}.`,
2475
+ '',
2476
+ 'Single container, no ingress opinion — front it with whatever your org already runs',
2477
+ '(ALB, API Gateway, internal VPC access). Use `--ingress edge` or `--ingress edge-fed`',
2478
+ 'for a self-contained ALB + AgentHippo edge instead.',
2479
+ '',
2480
+ '```bash',
2481
+ 'export CLUSTER=<ecs-cluster> SUBNETS=<subnet-ids> SECURITY_GROUPS=<sg-ids>',
2482
+ 'export PROVIDER_KEY_SECRET_ARN=<Secrets Manager ARN for your model provider key>',
2483
+ 'bash deploy/deploy.sh',
2484
+ '```',
2485
+ '',
2486
+ '> ⚠️ Newer and far less exercised than the Cloud Run target — read `deploy.sh` before',
2487
+ '> running it against a real account.',
2488
+ '',
2489
+ ...renderEgressNote(ctx),
2490
+ ].join('\n'),
2491
+ };
2492
+ }
2493
+ // Edge / edge-fed: Caddy + (self-issued: oauth2-proxy) + auth-broker + agent, one Fargate
2494
+ // task (awsvpc network mode = shared localhost, exactly like Cloud Run sidecars) — reuses
2495
+ // the identical Caddyfiles Cloud Run already renders.
2496
+ const containers = [
2497
+ {
2498
+ name: 'caddy',
2499
+ image: 'caddy:2.8-alpine',
2500
+ essential: true,
2501
+ portMappings: [{ containerPort: 8080, protocol: 'tcp' }],
2502
+ environment: [{ name: 'AGENTHIPPO_PUBLIC_HOSTNAME', value: ctx.publicHostname }],
2503
+ // Caddyfile is written to a mounted config volume by the deploy script (sed-templated,
2504
+ // same "checked-in template, script substitutes it" pattern as the Cloud Run edge).
2505
+ mountPoints: [{ sourceVolume: 'caddy-config', containerPath: '/etc/caddy' }],
2506
+ logConfiguration: {
2507
+ logDriver: 'awslogs',
2508
+ options: { 'awslogs-group': '/ecs/${SERVICE}', 'awslogs-region': '${REGION}', 'awslogs-stream-prefix': 'caddy' },
2509
+ },
2510
+ },
2511
+ ...(fed ? [] : [{
2512
+ name: 'oauth2-proxy',
2513
+ image: 'quay.io/oauth2-proxy/oauth2-proxy:v7.7.1',
2514
+ essential: true,
2515
+ environment: [
2516
+ { name: 'OAUTH2_PROXY_HTTP_ADDRESS', value: '0.0.0.0:4180' },
2517
+ { name: 'OAUTH2_PROXY_UPSTREAMS', value: 'file:///dev/null' },
2518
+ { name: 'OAUTH2_PROXY_SET_XAUTHREQUEST', value: 'true' },
2519
+ { name: 'OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER', value: 'true' },
2520
+ { name: 'OAUTH2_PROXY_SKIP_PROVIDER_BUTTON', value: 'true' },
2521
+ { name: 'OAUTH2_PROXY_REVERSE_PROXY', value: 'true' },
2522
+ ],
2523
+ secrets: [
2524
+ { name: 'OAUTH2_PROXY_CLIENT_ID', valueFrom: '${OAUTH_CLIENT_ID_SECRET_ARN}' },
2525
+ { name: 'OAUTH2_PROXY_CLIENT_SECRET', valueFrom: '${OAUTH_CLIENT_SECRET_SECRET_ARN}' },
2526
+ { name: 'OAUTH2_PROXY_COOKIE_SECRET', valueFrom: '${OAUTH_COOKIE_SECRET_SECRET_ARN}' },
2527
+ ],
2528
+ logConfiguration: {
2529
+ logDriver: 'awslogs',
2530
+ options: { 'awslogs-group': '/ecs/${SERVICE}', 'awslogs-region': '${REGION}', 'awslogs-stream-prefix': 'oauth2-proxy' },
2531
+ },
2532
+ }]),
2533
+ {
2534
+ name: 'auth-broker',
2535
+ image: '${IMAGE}',
2536
+ essential: true,
2537
+ command: ['auth-broker'],
2538
+ environment: [
2539
+ { name: 'AGENTHIPPO_AUTH_PORT', value: '9780' },
2540
+ ...(fed ? [
2541
+ { name: 'AGENTHIPPO_AUTH_COOKIE_DOMAIN', value: ctx.cookieDomain || '' },
2542
+ { name: 'AGENTHIPPO_AUTH_IDP_URL', value: ctx.idpUrl || '' },
2543
+ ] : []),
2544
+ ],
2545
+ secrets: [{ name: 'AGENTHIPPO_AUTH_JWT_SECRET', valueFrom: '${AUTH_JWT_SECRET_SECRET_ARN}' }],
2546
+ logConfiguration: {
2547
+ logDriver: 'awslogs',
2548
+ options: { 'awslogs-group': '/ecs/${SERVICE}', 'awslogs-region': '${REGION}', 'awslogs-stream-prefix': 'auth-broker' },
2549
+ },
2550
+ },
2551
+ {
2552
+ name: 'agent',
2553
+ image: '${IMAGE}',
2554
+ essential: true,
2555
+ command: serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: identity_1.EDGE_AGENT_PORT }),
2556
+ environment: [{ name: 'AGENTHIPPO_NO_AUTO_INSTALL', value: '1' }],
2557
+ secrets: [{ name: providerKeyEnv, valueFrom: '${PROVIDER_KEY_SECRET_ARN}' }],
2558
+ logConfiguration: {
2559
+ logDriver: 'awslogs',
2560
+ options: { 'awslogs-group': '/ecs/${SERVICE}', 'awslogs-region': '${REGION}', 'awslogs-stream-prefix': 'agent' },
2561
+ },
2562
+ },
2563
+ ];
2564
+ const taskDef = {
2565
+ family: '${SERVICE}',
2566
+ networkMode: 'awsvpc',
2567
+ requiresCompatibilities: ['FARGATE'],
2568
+ cpu: '1024',
2569
+ memory: '2048',
2570
+ volumes: [{ name: 'caddy-config' }],
2571
+ containerDefinitions: containers,
2572
+ };
2573
+ const secretArgs = fed
2574
+ ? 'PROVIDER_KEY_SECRET_ARN=<...> AUTH_JWT_SECRET_SECRET_ARN=<...>'
2575
+ : 'PROVIDER_KEY_SECRET_ARN=<...> AUTH_JWT_SECRET_SECRET_ARN=<...> ' +
2576
+ 'OAUTH_CLIENT_ID_SECRET_ARN=<...> OAUTH_CLIENT_SECRET_SECRET_ARN=<...> OAUTH_COOKIE_SECRET_SECRET_ARN=<...>';
2577
+ const deploy = [
2578
+ '#!/usr/bin/env bash',
2579
+ `# AWS ECS Fargate + AgentHippo edge (${fed ? 'edge-fed: federated identity, no oauth2-proxy' : 'edge: self-issued'}).`,
2580
+ '# One-time setup (not per deploy): create an ALB + target group (protocol HTTP, port 8080,',
2581
+ '# target type ip) pointed at this service, and an HTTPS listener forwarding to it — ALB',
2582
+ '# terminates TLS, same as Cloud Run\'s edge path (Caddy listens on plain HTTP behind it).',
2583
+ 'set -euo pipefail',
2584
+ ...anchorToStagingRoot(),
2585
+ 'SERVICE="' + ctx.packName + '"',
2586
+ 'REGION="${AWS_REGION:-us-east-1}"',
2587
+ 'ACCOUNT_ID="${ACCOUNT_ID:-$(aws sts get-caller-identity --query Account --output text)}"',
2588
+ 'REPO="agenthippo/${SERVICE}"',
2589
+ 'IMAGE="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPO}:latest"',
2590
+ '',
2591
+ 'aws ecr describe-repositories --repository-names "$REPO" --region "$REGION" >/dev/null 2>&1 \\',
2592
+ ' || aws ecr create-repository --repository-name "$REPO" --region "$REGION"',
2593
+ 'aws ecr get-login-password --region "$REGION" \\',
2594
+ ' | docker login --username AWS --password-stdin "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com"',
2595
+ '',
2596
+ '# auth-broker reuses the agent image (CLI ships `auth-broker`).',
2597
+ 'docker build --platform linux/amd64 -f deploy/Dockerfile -t "$IMAGE" .',
2598
+ 'docker push "$IMAGE"',
2599
+ '',
2600
+ 'PROVIDER_KEY_SECRET_ARN="${PROVIDER_KEY_SECRET_ARN:?set PROVIDER_KEY_SECRET_ARN}"',
2601
+ 'AUTH_JWT_SECRET_SECRET_ARN="${AUTH_JWT_SECRET_SECRET_ARN:?set AUTH_JWT_SECRET_SECRET_ARN}"',
2602
+ ...(fed ? [] : [
2603
+ 'OAUTH_CLIENT_ID_SECRET_ARN="${OAUTH_CLIENT_ID_SECRET_ARN:?set OAUTH_CLIENT_ID_SECRET_ARN}"',
2604
+ 'OAUTH_CLIENT_SECRET_SECRET_ARN="${OAUTH_CLIENT_SECRET_SECRET_ARN:?set OAUTH_CLIENT_SECRET_SECRET_ARN}"',
2605
+ 'OAUTH_COOKIE_SECRET_SECRET_ARN="${OAUTH_COOKIE_SECRET_SECRET_ARN:?set OAUTH_COOKIE_SECRET_SECRET_ARN}"',
2606
+ ]),
2607
+ 'TASK_JSON="$(sed -e "s|\\${IMAGE}|${IMAGE}|g" -e "s|\\${SERVICE}|${SERVICE}|g" -e "s|\\${REGION}|${REGION}|g" \\',
2608
+ ' -e "s|\\${PROVIDER_KEY_SECRET_ARN}|${PROVIDER_KEY_SECRET_ARN}|g" \\',
2609
+ ' -e "s|\\${AUTH_JWT_SECRET_SECRET_ARN}|${AUTH_JWT_SECRET_SECRET_ARN}|g" \\',
2610
+ ...(fed ? [] : [
2611
+ ' -e "s|\\${OAUTH_CLIENT_ID_SECRET_ARN}|${OAUTH_CLIENT_ID_SECRET_ARN}|g" \\',
2612
+ ' -e "s|\\${OAUTH_CLIENT_SECRET_SECRET_ARN}|${OAUTH_CLIENT_SECRET_SECRET_ARN}|g" \\',
2613
+ ' -e "s|\\${OAUTH_COOKIE_SECRET_SECRET_ARN}|${OAUTH_COOKIE_SECRET_SECRET_ARN}|g" \\',
2614
+ ]),
2615
+ ' deploy/task-definition.json)"',
2616
+ 'aws ecs register-task-definition --region "$REGION" --cli-input-json "$TASK_JSON" >/dev/null',
2617
+ '',
2618
+ 'CLUSTER="${CLUSTER:?set CLUSTER}"',
2619
+ 'TARGET_GROUP_ARN="${TARGET_GROUP_ARN:?set TARGET_GROUP_ARN — from the one-time ALB setup above}"',
2620
+ 'aws ecs describe-services --cluster "$CLUSTER" --services "$SERVICE" --region "$REGION" 2>/dev/null \\',
2621
+ ' | grep -q \'"status": "ACTIVE"\' \\',
2622
+ ' && aws ecs update-service --cluster "$CLUSTER" --service "$SERVICE" \\',
2623
+ ' --task-definition "$SERVICE" --region "$REGION" >/dev/null \\',
2624
+ ' || aws ecs create-service --cluster "$CLUSTER" --service-name "$SERVICE" \\',
2625
+ ' --task-definition "$SERVICE" --desired-count 1 --launch-type FARGATE \\',
2626
+ ' --load-balancers "targetGroupArn=$TARGET_GROUP_ARN,containerName=caddy,containerPort=8080" \\',
2627
+ ' --network-configuration "awsvpcConfiguration={subnets=[${SUBNETS:?set SUBNETS}],securityGroups=[${SECURITY_GROUPS:?set SECURITY_GROUPS}],assignPublicIp=ENABLED}" \\',
2628
+ ' --region "$REGION" >/dev/null',
2629
+ 'echo "Deployed $SERVICE behind the AgentHippo edge on ECS cluster $CLUSTER"',
2630
+ '',
2631
+ ].join('\n');
2632
+ return {
2633
+ 'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
2634
+ 'www/index.html': renderLandingPage(ctx.packName),
2635
+ 'Caddyfile': fed ? (0, identity_1.renderSidecarFedCaddyfile)(ctx.idpUrl) : (0, identity_1.renderSidecarEdgeCaddyfile)(),
2636
+ 'task-definition.json': JSON.stringify(taskDef, null, 2) + '\n',
2637
+ 'deploy.sh': deploy,
2638
+ 'README.md': [
2639
+ `# Deploy: ${ctx.packName} — AWS ECS Fargate + AgentHippo edge (${fed ? 'edge-fed' : 'edge'})`,
2640
+ '',
2641
+ `Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}.`,
2642
+ '',
2643
+ 'One Fargate task, containers share the task\'s localhost (`awsvpc` network mode) —',
2644
+ 'the same sidecar shape Cloud Run uses, so the Caddyfile is identical:',
2645
+ '- **caddy** (ingress, the only published port) → ' + (fed
2646
+ ? 'verifies the shared AgentHippo JWT'
2647
+ : 'SSO edge: oauth2-proxy for browsers, auth-broker for IDE Bearer'),
2648
+ `- **agent** → serves www + /v1/* on localhost:${identity_1.EDGE_AGENT_PORT} (not published)`,
2649
+ '- **auth-broker** → validates IDE AgentHippo JWTs (`/api/ide/me`)' + (fed ? ' against the shared IdP secret' : ''),
2650
+ ...(fed ? [] : ['- **oauth2-proxy** → Google SSO for browser clients']),
2651
+ '',
2652
+ '**One-time setup (not per deploy):** an ALB + HTTPS listener + target group (HTTP, port',
2653
+ '8080, target type `ip`) pointed at this service — ALB terminates TLS, Caddy stays HTTP.',
2654
+ '',
2655
+ '```bash',
2656
+ `export CLUSTER=<ecs-cluster> SUBNETS=<subnet-ids> SECURITY_GROUPS=<sg-ids> TARGET_GROUP_ARN=<from ALB setup>`,
2657
+ `export ${secretArgs}`,
2658
+ 'bash deploy/deploy.sh',
2659
+ '```',
2660
+ '',
2661
+ '> ⚠️ Newer and far less exercised than the Cloud Run target — read `deploy.sh` before',
2662
+ '> running it against a real account.',
2663
+ '',
2664
+ ...renderEgressNote(ctx),
2665
+ ].join('\n'),
2666
+ };
2667
+ }
2668
+ /**
2669
+ * Azure Container Apps (T1-generic, `d9-azure-adapter`). One Container App's containers run
2670
+ * as a single replica sharing localhost — the same sidecar shape as Cloud Run/ECS Fargate, so
2671
+ * the edge/edge-fed Caddyfiles and `EDGE_AGENT_PORT` convention are reused verbatim again
2672
+ * (PLAN/deploy.md §5). Container Apps environments provide built-in ingress + TLS termination,
2673
+ * so unlike aws-ecs there's no separate one-time load-balancer setup — `--target-port` on the
2674
+ * container app itself is the whole ingress story.
2675
+ *
2676
+ * Scope: identity/exposure only (bare / edge / edge-fed), same as aws-ecs — no Spotlight/LiteLLM
2677
+ * sidecar variant yet. Newer and far less exercised than the Cloud Run path.
2678
+ */
2679
+ function azureContainerAppsArtifacts(ctx) {
2680
+ const fed = ctx.ingress === 'edge-fed';
2681
+ const edge = ctx.ingress === 'edge' || fed;
2682
+ const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
2683
+ const PORT = 8080;
2684
+ if (!edge) {
2685
+ const spec = {
2686
+ properties: {
2687
+ configuration: {
2688
+ ingress: { external: true, targetPort: ctx.port },
2689
+ secrets: [{ name: 'provider-key', value: '${PROVIDER_KEY}' }],
2690
+ },
2691
+ template: {
2692
+ containers: [
2693
+ {
2694
+ name: 'agent',
2695
+ image: '${IMAGE}',
2696
+ env: [
2697
+ { name: 'AGENTHIPPO_NO_AUTO_INSTALL', value: '1' },
2698
+ { name: providerKeyEnv, secretRef: 'provider-key' },
2699
+ ],
2700
+ resources: { cpu: 0.5, memory: '1Gi' },
2701
+ },
2702
+ ],
2703
+ },
2704
+ },
2705
+ };
2706
+ const deploy = [
2707
+ '#!/usr/bin/env bash',
2708
+ '# T1-generic: Azure Container Apps, single container — built-in ingress + TLS, no separate',
2709
+ '# load balancer to stand up. For a self-contained AgentHippo edge, use --ingress edge or',
2710
+ '# --ingress edge-fed instead.',
2711
+ 'set -euo pipefail',
2712
+ ...anchorToStagingRoot(),
2713
+ 'SERVICE="' + ctx.packName + '"',
2714
+ 'RESOURCE_GROUP="${RESOURCE_GROUP:?set RESOURCE_GROUP}"',
2715
+ 'ENVIRONMENT="${CONTAINERAPPS_ENVIRONMENT:?set CONTAINERAPPS_ENVIRONMENT — az containerapp env create (one-time)}"',
2716
+ 'ACR_NAME="${ACR_NAME:?set ACR_NAME — an Azure Container Registry (one-time: az acr create)}"',
2717
+ 'IMAGE="${ACR_NAME}.azurecr.io/${SERVICE}:latest"',
2718
+ `PROVIDER_KEY="\${PROVIDER_KEY:?set PROVIDER_KEY — your ${providerKeyEnv}}"`,
2719
+ '',
2720
+ '# Builds in ACR (no local docker login/push step needed).',
2721
+ 'az acr build --registry "$ACR_NAME" --image "${SERVICE}:latest" --file deploy/Dockerfile .',
2722
+ '',
2723
+ 'az containerapp show --name "$SERVICE" --resource-group "$RESOURCE_GROUP" >/dev/null 2>&1 \\',
2724
+ ' && az containerapp update --name "$SERVICE" --resource-group "$RESOURCE_GROUP" \\',
2725
+ ' --image "$IMAGE" >/dev/null \\',
2726
+ ' || az containerapp create --name "$SERVICE" --resource-group "$RESOURCE_GROUP" \\',
2727
+ ' --environment "$ENVIRONMENT" --image "$IMAGE" \\',
2728
+ ' --target-port ' + ctx.port + ' --ingress external \\',
2729
+ ' --secrets "provider-key=$PROVIDER_KEY" \\',
2730
+ ` --env-vars "AGENTHIPPO_NO_AUTO_INSTALL=1" "${providerKeyEnv}=secretref:provider-key" \\`,
2731
+ ' >/dev/null',
2732
+ 'echo "Deployed $SERVICE to Container Apps environment $ENVIRONMENT"',
2733
+ '',
2734
+ ].join('\n');
2735
+ return {
2736
+ 'Dockerfile': sharedDockerfile(ctx),
2737
+ 'containerapp.json': JSON.stringify(spec, null, 2) + '\n',
2738
+ 'deploy.sh': deploy,
2739
+ 'README.md': [
2740
+ `# Deploy: ${ctx.packName} — Azure Container Apps (T1-generic)`,
2741
+ '',
2742
+ `Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}.`,
2743
+ '',
2744
+ 'Single container, built-in ingress + TLS — no separate load balancer to stand up.',
2745
+ 'Use `--ingress edge` or `--ingress edge-fed` for a self-contained AgentHippo edge instead.',
2746
+ '',
2747
+ '```bash',
2748
+ 'export RESOURCE_GROUP=<rg> CONTAINERAPPS_ENVIRONMENT=<env> ACR_NAME=<registry>',
2749
+ `export PROVIDER_KEY=<your ${providerKeyEnv}>`,
2750
+ 'bash deploy/deploy.sh',
2751
+ '```',
2752
+ '',
2753
+ '> ⚠️ Newer and far less exercised than the Cloud Run target — read `deploy.sh` before',
2754
+ '> running it against a real account.',
2755
+ '',
2756
+ ...renderEgressNote(ctx),
2757
+ ].join('\n'),
2758
+ };
2759
+ }
2760
+ // Edge / edge-fed: Caddy + (self-issued: oauth2-proxy) + auth-broker + agent, one Container
2761
+ // App (containers in one replica share localhost, same sidecar shape as Cloud Run/ECS) —
2762
+ // reuses the identical Caddyfiles Cloud Run already renders.
2763
+ const containers = [
2764
+ {
2765
+ name: 'caddy',
2766
+ image: 'caddy:2.8-alpine',
2767
+ env: [{ name: 'PORT', value: String(PORT) }, { name: 'AGENTHIPPO_PUBLIC_HOSTNAME', value: ctx.publicHostname }],
2768
+ resources: { cpu: 0.25, memory: '0.5Gi' },
2769
+ },
2770
+ ...(fed ? [] : [{
2771
+ name: 'oauth2-proxy',
2772
+ image: 'quay.io/oauth2-proxy/oauth2-proxy:v7.7.1',
2773
+ env: [
2774
+ { name: 'OAUTH2_PROXY_HTTP_ADDRESS', value: '0.0.0.0:4180' },
2775
+ { name: 'OAUTH2_PROXY_UPSTREAMS', value: 'file:///dev/null' },
2776
+ { name: 'OAUTH2_PROXY_SET_XAUTHREQUEST', value: 'true' },
2777
+ { name: 'OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER', value: 'true' },
2778
+ { name: 'OAUTH2_PROXY_SKIP_PROVIDER_BUTTON', value: 'true' },
2779
+ { name: 'OAUTH2_PROXY_REVERSE_PROXY', value: 'true' },
2780
+ { name: 'OAUTH2_PROXY_CLIENT_ID', secretRef: 'oauth-client-id' },
2781
+ { name: 'OAUTH2_PROXY_CLIENT_SECRET', secretRef: 'oauth-client-secret' },
2782
+ { name: 'OAUTH2_PROXY_COOKIE_SECRET', secretRef: 'oauth-cookie-secret' },
2783
+ ],
2784
+ resources: { cpu: 0.25, memory: '0.5Gi' },
2785
+ }]),
2786
+ {
2787
+ name: 'auth-broker',
2788
+ image: '${IMAGE}',
2789
+ command: ['agenthippo', 'auth-broker'],
2790
+ env: [
2791
+ { name: 'AGENTHIPPO_AUTH_PORT', value: '9780' },
2792
+ { name: 'AGENTHIPPO_AUTH_JWT_SECRET', secretRef: 'auth-jwt-secret' },
2793
+ ...(fed ? [
2794
+ { name: 'AGENTHIPPO_AUTH_COOKIE_DOMAIN', value: ctx.cookieDomain || '' },
2795
+ { name: 'AGENTHIPPO_AUTH_IDP_URL', value: ctx.idpUrl || '' },
2796
+ ] : []),
2797
+ ],
2798
+ resources: { cpu: 0.25, memory: '0.5Gi' },
2799
+ },
2800
+ {
2801
+ name: 'agent',
2802
+ image: '${IMAGE}',
2803
+ command: serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: identity_1.EDGE_AGENT_PORT }),
2804
+ env: [
2805
+ { name: 'AGENTHIPPO_NO_AUTO_INSTALL', value: '1' },
2806
+ { name: providerKeyEnv, secretRef: 'provider-key' },
2807
+ ],
2808
+ resources: { cpu: 0.5, memory: '1Gi' },
2809
+ },
2810
+ ];
2811
+ const spec = {
2812
+ properties: {
2813
+ configuration: {
2814
+ ingress: { external: true, targetPort: PORT },
2815
+ secrets: [
2816
+ { name: 'auth-jwt-secret', value: '${AUTH_JWT_SECRET}' },
2817
+ { name: 'provider-key', value: '${PROVIDER_KEY}' },
2818
+ ...(fed ? [] : [
2819
+ { name: 'oauth-client-id', value: '${OAUTH_CLIENT_ID}' },
2820
+ { name: 'oauth-client-secret', value: '${OAUTH_CLIENT_SECRET}' },
2821
+ { name: 'oauth-cookie-secret', value: '${OAUTH_COOKIE_SECRET}' },
2822
+ ]),
2823
+ ],
2824
+ },
2825
+ template: { containers },
2826
+ },
2827
+ };
2828
+ const secretExports = [
2829
+ 'AUTH_JWT_SECRET="${AUTH_JWT_SECRET:?set AUTH_JWT_SECRET}"',
2830
+ `PROVIDER_KEY="\${PROVIDER_KEY:?set PROVIDER_KEY — your ${providerKeyEnv}}"`,
2831
+ ...(fed ? [] : [
2832
+ 'OAUTH_CLIENT_ID="${OAUTH_CLIENT_ID:?set OAUTH_CLIENT_ID}"',
2833
+ 'OAUTH_CLIENT_SECRET="${OAUTH_CLIENT_SECRET:?set OAUTH_CLIENT_SECRET}"',
2834
+ 'OAUTH_COOKIE_SECRET="${OAUTH_COOKIE_SECRET:?set OAUTH_COOKIE_SECRET}"',
2835
+ ]),
2836
+ ];
2837
+ const deploy = [
2838
+ '#!/usr/bin/env bash',
2839
+ `# Azure Container Apps + AgentHippo edge (${fed ? 'edge-fed: federated identity, no oauth2-proxy' : 'edge: self-issued'}).`,
2840
+ '# Container Apps environments provide built-in ingress + TLS — no separate load balancer.',
2841
+ 'set -euo pipefail',
2842
+ ...anchorToStagingRoot(),
2843
+ 'SERVICE="' + ctx.packName + '"',
2844
+ 'RESOURCE_GROUP="${RESOURCE_GROUP:?set RESOURCE_GROUP}"',
2845
+ 'ENVIRONMENT="${CONTAINERAPPS_ENVIRONMENT:?set CONTAINERAPPS_ENVIRONMENT — az containerapp env create (one-time)}"',
2846
+ 'ACR_NAME="${ACR_NAME:?set ACR_NAME — an Azure Container Registry (one-time: az acr create)}"',
2847
+ 'IMAGE="${ACR_NAME}.azurecr.io/${SERVICE}:latest"',
2848
+ '',
2849
+ '# auth-broker reuses the agent image (CLI ships `auth-broker`). Builds in ACR.',
2850
+ 'az acr build --registry "$ACR_NAME" --image "${SERVICE}:latest" --file deploy/Dockerfile .',
2851
+ '',
2852
+ ...secretExports,
2853
+ 'SPEC_JSON="$(sed -e "s|\\${IMAGE}|${IMAGE}|g" -e "s|\\${AUTH_JWT_SECRET}|${AUTH_JWT_SECRET}|g" \\',
2854
+ ' -e "s|\\${PROVIDER_KEY}|${PROVIDER_KEY}|g" \\',
2855
+ ...(fed ? [] : [
2856
+ ' -e "s|\\${OAUTH_CLIENT_ID}|${OAUTH_CLIENT_ID}|g" -e "s|\\${OAUTH_CLIENT_SECRET}|${OAUTH_CLIENT_SECRET}|g" \\',
2857
+ ' -e "s|\\${OAUTH_COOKIE_SECRET}|${OAUTH_COOKIE_SECRET}|g" \\',
2858
+ ]),
2859
+ ' deploy/containerapp.json)"',
2860
+ 'echo "$SPEC_JSON" > /tmp/${SERVICE}-containerapp.json',
2861
+ '',
2862
+ '# `--yaml` also accepts JSON (JSON is valid YAML) — kept as .json so it\'s reviewable',
2863
+ '# alongside the other checked-in templates without a format switch.',
2864
+ 'az containerapp show --name "$SERVICE" --resource-group "$RESOURCE_GROUP" >/dev/null 2>&1 \\',
2865
+ ' && az containerapp update --name "$SERVICE" --resource-group "$RESOURCE_GROUP" \\',
2866
+ ' --yaml /tmp/${SERVICE}-containerapp.json >/dev/null \\',
2867
+ ' || az containerapp create --name "$SERVICE" --resource-group "$RESOURCE_GROUP" \\',
2868
+ ' --environment "$ENVIRONMENT" --yaml /tmp/${SERVICE}-containerapp.json >/dev/null',
2869
+ 'echo "Deployed $SERVICE behind the AgentHippo edge in $ENVIRONMENT"',
2870
+ '',
2871
+ ].join('\n');
2872
+ return {
2873
+ 'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
2874
+ 'www/index.html': renderLandingPage(ctx.packName),
2875
+ 'Caddyfile': fed ? (0, identity_1.renderSidecarFedCaddyfile)(ctx.idpUrl) : (0, identity_1.renderSidecarEdgeCaddyfile)(),
2876
+ 'containerapp.json': JSON.stringify(spec, null, 2) + '\n',
2877
+ 'deploy.sh': deploy,
2878
+ 'README.md': [
2879
+ `# Deploy: ${ctx.packName} — Azure Container Apps + AgentHippo edge (${fed ? 'edge-fed' : 'edge'})`,
2880
+ '',
2881
+ `Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}.`,
2882
+ '',
2883
+ 'One Container App, containers share the replica\'s localhost — the same sidecar shape',
2884
+ 'Cloud Run/ECS Fargate use, so the Caddyfile is identical:',
2885
+ '- **caddy** (ingress, the only published port) → ' + (fed
2886
+ ? 'verifies the shared AgentHippo JWT'
2887
+ : 'SSO edge: oauth2-proxy for browsers, auth-broker for IDE Bearer'),
2888
+ `- **agent** → serves www + /v1/* on localhost:${identity_1.EDGE_AGENT_PORT} (not published)`,
2889
+ '- **auth-broker** → validates IDE AgentHippo JWTs (`/api/ide/me`)' + (fed ? ' against the shared IdP secret' : ''),
2890
+ ...(fed ? [] : ['- **oauth2-proxy** → Google SSO for browser clients']),
2891
+ '',
2892
+ 'Built-in ingress + TLS termination — no separate load balancer to stand up.',
2893
+ '',
2894
+ '```bash',
2895
+ 'export RESOURCE_GROUP=<rg> CONTAINERAPPS_ENVIRONMENT=<env> ACR_NAME=<registry>',
2896
+ ...secretExports.map((l) => 'export ' + l.split('=')[0] + '=<...>'),
2897
+ 'bash deploy/deploy.sh',
2898
+ '```',
2899
+ '',
2900
+ '> ⚠️ Newer and far less exercised than the Cloud Run target — read `deploy.sh` before',
2901
+ '> running it against a real account.',
2902
+ '',
2903
+ ...renderEgressNote(ctx),
2148
2904
  ].join('\n'),
2149
2905
  };
2150
2906
  }
@@ -2155,6 +2911,8 @@ const RENDERERS = {
2155
2911
  'databricks-app': databricksAppArtifacts,
2156
2912
  spcs: spcsArtifacts,
2157
2913
  cloudflare: cloudflareArtifacts,
2914
+ 'aws-ecs': awsEcsArtifacts,
2915
+ 'azure-container-apps': azureContainerAppsArtifacts,
2158
2916
  };
2159
2917
  /**
2160
2918
  * Render every artifact for a target (relative path → file content), always
@@ -2162,18 +2920,24 @@ const RENDERERS = {
2162
2920
  */
2163
2921
  function renderTarget(target, ctx) {
2164
2922
  const files = RENDERERS[target](ctx);
2165
- const fed = target === 'cloudrun' && ctx.ingress === 'edge-fed';
2166
- const edge = target === 'vm' || target === 'compose' || (target === 'cloudrun' && ctx.ingress === 'edge') || fed;
2167
- // .dockerignore only alongside a Dockerfile (image-based targets). databricks-app
2168
- // is source-based (platform builds it) and ships no Dockerfile.
2923
+ // edge-fed applies to any Caddy-based target (vm/compose/cloudrun) — federates identity
2924
+ // against a central IdP instead of each target running its own oauth2-proxy + Google client.
2925
+ const fed = ctx.ingress === 'edge-fed';
2926
+ // Colocated agents (state 4) ship no edge of their own at all — no Caddy, no oauth2-proxy,
2927
+ // no local auth-broker, so no auth secrets (.env.auth.example / DATA-ACCESS.md) either.
2928
+ const colocate = Boolean(ctx.colocate) && fed && (target === 'vm' || target === 'compose');
2929
+ const gatewayDirect = usesGatewayDirect(ctx) && target === 'cloudrun' && fed;
2930
+ const edge = !colocate && (target === 'vm' || target === 'compose'
2931
+ || ((target === 'cloudrun' || target === 'aws-ecs' || target === 'azure-container-apps') && (ctx.ingress === 'edge' || fed)));
2169
2932
  const hasDockerfile = Object.keys(files).some((k) => k === 'Dockerfile' || k.endsWith('/Dockerfile'));
2170
- return {
2933
+ const raw = {
2171
2934
  ...files,
2172
2935
  ...(hasDockerfile ? { '.dockerignore': renderDockerignore() } : {}),
2173
2936
  '.env.example': renderEnvExample(ctx.envVars, {
2174
2937
  edge,
2175
2938
  engine: ctx.engine,
2176
2939
  hostname: ctx.publicHostname,
2940
+ gatewayDirect,
2177
2941
  }),
2178
2942
  // Edge auth secrets are split out so the agent container never loads them.
2179
2943
  ...(edge ? {
@@ -2184,5 +2948,11 @@ function renderTarget(target, ctx) {
2184
2948
  cookieDomain: ctx.cookieDomain,
2185
2949
  }),
2186
2950
  } : {}),
2951
+ 'DATA-ACCESS.md': renderDataAccessSummary(ctx, target, edge, fed, colocate),
2187
2952
  };
2953
+ const out = {};
2954
+ for (const [rel, content] of Object.entries(raw)) {
2955
+ out[rel] = withPackAttribution(ctx, rel, content);
2956
+ }
2957
+ return out;
2188
2958
  }