@agenthippo-ai/agenthippo 0.1.0 → 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.
- package/out/agentpack/index.js +2 -1
- package/out/agentpack/paths.js +23 -0
- package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
- package/out/analytics/dashboard/webview/panel.js +290 -80
- package/out/analytics/sessionEvents.js +21 -4
- package/out/auth/brokerClient.js +1 -1
- package/out/auth/onboardingPrompt.js +112 -0
- package/out/auth/provider.js +1 -1
- package/out/auth-broker/config.js +8 -3
- package/out/auth-broker/pkce.js +3 -1
- package/out/auth-broker/server.js +43 -1
- package/out/auth-broker/store.js +6 -5
- package/out/cli/commands/ask.js +3 -0
- package/out/cli/commands/chat.js +3 -0
- package/out/cli/commands/deploy.js +49 -10
- package/out/cli/commands/gateway.js +54 -0
- package/out/cli/commands/idp.js +85 -0
- package/out/cli/commands/serve.js +401 -49
- package/out/cli/credential-context.js +47 -69
- package/out/cli/data-plane/broker.js +78 -0
- package/out/cli/data-plane/connector-host.js +121 -0
- package/out/cli/data-plane/egress.js +189 -0
- package/out/cli/data-plane/gateway.js +195 -0
- package/out/cli/data-plane/index.js +137 -0
- package/out/cli/data-plane/registry.js +80 -0
- package/out/cli/deploy/blueprints/registry.js +84 -0
- package/out/cli/deploy/layers/identity.js +91 -0
- package/out/cli/deploy-init.js +1156 -285
- package/out/cli/idp-init.js +303 -0
- package/out/cli/index.js +46 -8
- package/out/cli/pack-loader.js +37 -2
- package/out/cli/runtime.js +10 -3
- package/out/cli/serve-hardening.js +217 -12
- package/out/cli/state-sync.js +318 -29
- package/out/engines/claude.js +26 -5
- package/out/engines/codex.js +24 -5
- package/out/engines/custom.js +46 -12
- package/out/extension.js +2 -0
- package/out/gateway/outboundSend.js +6 -6
- package/out/headless/index.js +24 -6
- package/out/live-probe/connector.mjs +8 -0
- package/out/live-probe/env-report.mjs +8 -0
- package/out/mcp.js +88 -61
- package/out/remote/handleRemoteChat.js +7 -1
- package/out/remote/serveClient.js +27 -13
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +14 -0
- package/out/shared/models.js +1 -1
- package/out/shared/providers.js +5 -2
- package/out/test/analytics.test.js +93 -0
- package/out/test/auth-broker.test.js +59 -2
- package/out/test/blueprint-registry.test.js +105 -0
- package/out/test/credential-context.test.js +55 -0
- package/out/test/custom-env-layering.test.js +151 -0
- package/out/test/data-plane-engine-wiring.test.js +250 -0
- package/out/test/data-plane.test.js +244 -0
- package/out/test/deploy-init.test.js +287 -5
- package/out/test/egress-broker.test.js +172 -0
- package/out/test/fleetDisplayUtils.test.js +4 -0
- package/out/test/idp-init.test.js +151 -0
- package/out/test/mcp-obo.test.js +52 -27
- package/out/test/mcp.test.js +60 -4
- package/out/test/model-routing.test.js +133 -0
- package/out/test/serve-gateway-turn.test.js +267 -0
- package/out/test/serve-hardening.test.js +187 -0
- package/out/test/serveClient.test.js +37 -2
- package/out/test/state-sync-peer.test.js +83 -1
- package/out/utils.js +27 -11
- package/package.json +3 -4
package/out/cli/deploy-init.js
CHANGED
|
@@ -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
|
-
|
|
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/
|
|
76
|
-
: 'ghcr.io/
|
|
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) {
|
|
@@ -108,11 +156,11 @@ exports.DEFAULT_DEPLOY_PUBLIC_HOSTNAME = 'auth.agenthippo.ai';
|
|
|
108
156
|
/** @deprecated Use {@link DEFAULT_DEPLOY_PUBLIC_HOSTNAME}. */
|
|
109
157
|
exports.DEPLOY_PUBLIC_HOSTNAME = exports.DEFAULT_DEPLOY_PUBLIC_HOSTNAME;
|
|
110
158
|
/** npm package for Databricks Apps source deploy (org: agenthippo-ai). */
|
|
111
|
-
exports.AGENTHIPPO_CLI_NPM_PACKAGE = '@agenthippo-ai/
|
|
159
|
+
exports.AGENTHIPPO_CLI_NPM_PACKAGE = '@agenthippo-ai/agenthippo';
|
|
112
160
|
/** Fallback when monorepo package.json is not present (customer runs published CLI). */
|
|
113
161
|
exports.AGENTHIPPO_CLI_NPM_VERSION_FALLBACK = 'latest';
|
|
114
162
|
/**
|
|
115
|
-
* Pin @agenthippo-ai/
|
|
163
|
+
* Pin @agenthippo-ai/agenthippo to the monorepo release version when `deploy init` runs from
|
|
116
164
|
* a dev checkout; otherwise fall back to `latest`.
|
|
117
165
|
*/
|
|
118
166
|
function resolveAgentHippoCliNpmVersion() {
|
|
@@ -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):', '#
|
|
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:...}):');
|
|
@@ -202,7 +253,7 @@ function renderEnvExample(vars, opts) {
|
|
|
202
253
|
}
|
|
203
254
|
}
|
|
204
255
|
if (opts?.edge) {
|
|
205
|
-
lines.push('', '# Public hostname — set once; Caddyfile reads {$AGENTHIPPO_PUBLIC_HOSTNAME}.', `AGENTHIPPO_PUBLIC_HOSTNAME=${hostname}`, '', '# Edge auth secrets (JWT signing key, Google OAuth client/cookie) live in .env.auth,', '# which is loaded ONLY by the auth-broker and oauth2-proxy services — never the agent.', '# Keeping the token-signing key out of the agent container limits blast radius if the', '# agent (which runs model-driven tool calls) is compromised. Copy .env.auth.example.', '', '# Cloudflare Tunnel (free — no Access seats), config-file mode in compose:', '# cloudflared tunnel create <name> → save the JSON as deploy/tunnel-credentials.json (gitignored)', '# set the tunnel id in deploy/cloudflared.yml; cloudflared tunnel route dns <name> <hostname>', '# No token/env needed — the credentials JSON is mounted into the cloudflared container.');
|
|
256
|
+
lines.push('', '# Public hostname — set once; Caddyfile reads {$AGENTHIPPO_PUBLIC_HOSTNAME}.', `AGENTHIPPO_PUBLIC_HOSTNAME=${hostname}`, '', '# Multi-replica (serve-v4 P1): sticky by X-Agent-User in Caddyfile.', '# docker compose up --scale agent=N', '# AGENTHIPPO_STATE_S3_URI=s3://bucket/prefix # S3-compatible (MinIO/R2/GCS/AWS)', '# AGENTHIPPO_STATE_S3_ENDPOINT= # optional, e.g. http://minio:9000', '# AGENTHIPPO_STATE_RESTORE_BOOT=none # restore-on-miss (required when N>1)', '# Scale-down: drain (/ready inflight=0) then remove replica — see serve-v3-RUNBOOK.', '', '# Edge auth secrets (JWT signing key, Google OAuth client/cookie) live in .env.auth,', '# which is loaded ONLY by the auth-broker and oauth2-proxy services — never the agent.', '# Keeping the token-signing key out of the agent container limits blast radius if the', '# agent (which runs model-driven tool calls) is compromised. Copy .env.auth.example.', '', '# Cloudflare Tunnel (free — no Access seats), config-file mode in compose:', '# cloudflared tunnel create <name> → save the JSON as deploy/tunnel-credentials.json (gitignored)', '# set the tunnel id in deploy/cloudflared.yml; cloudflared tunnel route dns <name> <hostname>', '# No token/env needed — the credentials JSON is mounted into the cloudflared container.');
|
|
206
257
|
}
|
|
207
258
|
return lines.join('\n') + '\n';
|
|
208
259
|
}
|
|
@@ -229,6 +280,7 @@ function renderAuthEnvExample(opts) {
|
|
|
229
280
|
'AGENTHIPPO_AUTH_JWT_SECRET= # copy from auth.agenthippo.ai (openssl rand -hex 32 to generate one)',
|
|
230
281
|
`AGENTHIPPO_AUTH_PUBLIC_URL=https://${hostname}`,
|
|
231
282
|
'AGENTHIPPO_AUTH_ALLOWED_DOMAINS=agenthippo.ai',
|
|
283
|
+
'AGENTHIPPO_AUTH_DEV=0 # 1 enables /oauth/dev/login (tests only; never in production)',
|
|
232
284
|
`AGENTHIPPO_AUTH_COOKIE_DOMAIN=${cookieDomain} # scope the shared cookie to all subdomains`,
|
|
233
285
|
`AGENTHIPPO_AUTH_IDP_URL=${idp} # where browsers are sent to log in`,
|
|
234
286
|
'AGENTHIPPO_AUTH_COOKIE_NAME=ah_token',
|
|
@@ -246,6 +298,7 @@ function renderAuthEnvExample(opts) {
|
|
|
246
298
|
'# one Google OAuth client live only at the IdP.',
|
|
247
299
|
].join('\n') + '\n';
|
|
248
300
|
}
|
|
301
|
+
const cookieDomain = opts?.cookieDomain || '.agenthippo.ai';
|
|
249
302
|
return [
|
|
250
303
|
'# Generated by `agenthippo deploy init`. Copy to .env.auth and fill in.',
|
|
251
304
|
'# Loaded only by auth-broker + oauth2-proxy (docker-compose.yml). Never given to the agent.',
|
|
@@ -260,15 +313,16 @@ function renderAuthEnvExample(opts) {
|
|
|
260
313
|
'OAUTH2_PROXY_EMAIL_DOMAINS=example.com',
|
|
261
314
|
'OAUTH2_PROXY_REVERSE_PROXY=true',
|
|
262
315
|
`OAUTH2_PROXY_REDIRECT_URL=https://${hostname}/oauth2/callback`,
|
|
263
|
-
'# Federated SSO (A′) — set when this edge is the central IdP for
|
|
264
|
-
|
|
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`,
|
|
265
318
|
'',
|
|
266
319
|
'# IDE auth broker (team bootstrap for store + LiteLLM routing)',
|
|
267
320
|
'AGENTHIPPO_AUTH_JWT_SECRET= # openssl rand -hex 32',
|
|
268
321
|
'AGENTHIPPO_AUTH_PUBLIC_URL=https://' + hostname,
|
|
269
322
|
'AGENTHIPPO_AUTH_ALLOWED_DOMAINS=example.com',
|
|
323
|
+
'AGENTHIPPO_AUTH_DEV=0 # 1 enables /oauth/dev/login (tests only; never in production)',
|
|
270
324
|
'AGENTHIPPO_AUTH_COOKIE_NAME=ah_token # federated cookie holding the AgentHippo JWT',
|
|
271
|
-
|
|
325
|
+
`AGENTHIPPO_AUTH_COOKIE_DOMAIN=${cookieDomain} # set so /oauth/browser-login stamps a cross-subdomain cookie`,
|
|
272
326
|
'AGENTHIPPO_TEAM_ID=team_default',
|
|
273
327
|
'AGENTHIPPO_TEAM_NAME=Default Team',
|
|
274
328
|
'AGENTHIPPO_TEAM_STORE_REPO=agenthippoai/agenthippo-store',
|
|
@@ -283,7 +337,7 @@ function renderAuthBrokerDockerfile() {
|
|
|
283
337
|
return [
|
|
284
338
|
'# Auth broker for IDE sign-in + team bootstrap API.',
|
|
285
339
|
'# Build context = pack root: docker build -f deploy/Dockerfile.auth-broker -t <pack>-auth-broker:local .',
|
|
286
|
-
'FROM ghcr.io/
|
|
340
|
+
'FROM ghcr.io/agenthippoai/runtime:latest',
|
|
287
341
|
'ENV AGENTHIPPO_AUTH_PORT=9780',
|
|
288
342
|
'ENV AGENTHIPPO_AUTH_DATA_DIR=/data',
|
|
289
343
|
'EXPOSE 9780',
|
|
@@ -292,6 +346,18 @@ function renderAuthBrokerDockerfile() {
|
|
|
292
346
|
'',
|
|
293
347
|
].join('\n');
|
|
294
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
|
+
}
|
|
295
361
|
/** .dockerignore emitted alongside every image-building target's Dockerfile.
|
|
296
362
|
* Keeps skills/, references/, scripts/ (required at runtime) and drops only VCS,
|
|
297
363
|
* cache, and build cruft. node_modules is excluded so the linux/amd64 image
|
|
@@ -324,8 +390,20 @@ function renderDockerignore() {
|
|
|
324
390
|
'.vscode',
|
|
325
391
|
'*.swp',
|
|
326
392
|
'# Secrets anywhere in the pack (defensive; deploy/ secrets are also covered below).',
|
|
393
|
+
'# Keep in sync with isSecretLikeBasename() above.',
|
|
327
394
|
'**/.env',
|
|
328
|
-
'**/*credentials
|
|
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',
|
|
329
407
|
'# deploy/ holds build/deploy artifacts + secrets; only deploy/www/ ships at runtime.',
|
|
330
408
|
'deploy/*',
|
|
331
409
|
'!deploy/www',
|
|
@@ -362,17 +440,23 @@ function skillDepsInstallLines() {
|
|
|
362
440
|
function agentServeHostEnvYaml(indent = ' ') {
|
|
363
441
|
return `${indent}- { name: ${serve_hardening_1.SERVE_HOST_ENV}, value: ${serve_hardening_1.CONTAINER_SERVE_HOST} }`;
|
|
364
442
|
}
|
|
365
|
-
/** 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). */
|
|
366
446
|
function sharedDockerfile(ctx, opts) {
|
|
367
447
|
const cmd = serveCmdArgs(ctx, opts).map((a) => `"${a}"`).join(',');
|
|
368
448
|
return [
|
|
369
|
-
|
|
449
|
+
`${packAttributionComment(ctx)}. Build context = pack root:`,
|
|
370
450
|
`# docker build -f deploy/Dockerfile -t ${ctx.packName}:local .`,
|
|
371
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}"`,
|
|
372
455
|
`COPY . /app/pack/`,
|
|
373
456
|
...skillDepsInstallLines(),
|
|
374
457
|
`ENV AGENTHIPPO_NO_AUTO_INSTALL=1`,
|
|
375
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}`),
|
|
376
460
|
`EXPOSE ${ctx.port}`,
|
|
377
461
|
// Runtime base image sets ENTRYPOINT ["agenthippo"]; CMD args append to it.
|
|
378
462
|
`CMD [${cmd}]`,
|
|
@@ -448,95 +532,66 @@ function renderLandingPage(packName) {
|
|
|
448
532
|
'',
|
|
449
533
|
].join('\n');
|
|
450
534
|
}
|
|
535
|
+
/** Sticky upstream for multi-replica serve (serve-v4 P1.1).
|
|
536
|
+
* Hash on X-Agent-User (SSO identity the edge already injects) so a user's
|
|
537
|
+
* sessions stay on one replica — keeps local state + in-process RateLimiter
|
|
538
|
+
* correct without Redis. Fallback ip_hash for tokened API callers without SSO.
|
|
539
|
+
* Scale: `docker compose up --scale agent=N` (set AGENTHIPPO_STATE_RESTORE_BOOT=none).
|
|
540
|
+
*/
|
|
541
|
+
function renderStickyAgentProxy(port, indent = ' ') {
|
|
542
|
+
return [
|
|
543
|
+
`${indent}# serve-v4 P1.1: sticky by user identity (not session body — edge can't hash POST).`,
|
|
544
|
+
`${indent}# In-process RateLimiter is globally correct per user under this policy.`,
|
|
545
|
+
`${indent}reverse_proxy agent:${port} {`,
|
|
546
|
+
`${indent} lb_policy header X-Agent-User {`,
|
|
547
|
+
`${indent} fallback ip_hash`,
|
|
548
|
+
`${indent} }`,
|
|
549
|
+
`${indent}}`,
|
|
550
|
+
];
|
|
551
|
+
}
|
|
451
552
|
/** Caddy config for the `edge` compose profile (oauth2-proxy + agent on Docker network). */
|
|
452
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
|
+
];
|
|
453
564
|
return [
|
|
454
565
|
'# SSO edge + Cloudflare Tunnel. Set AGENTHIPPO_PUBLIC_HOSTNAME in .env (Caddy reads it below).',
|
|
455
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
|
+
: []),
|
|
456
573
|
'{',
|
|
457
574
|
' auto_https off',
|
|
458
575
|
'}',
|
|
459
576
|
'',
|
|
460
577
|
'http://{$AGENTHIPPO_PUBLIC_HOSTNAME} {',
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
479
|
-
' }',
|
|
480
|
-
' }',
|
|
481
|
-
' handle /oauth/* {',
|
|
482
|
-
' reverse_proxy auth-broker:9780 {',
|
|
483
|
-
' header_up X-Forwarded-Proto https',
|
|
484
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
485
|
-
' }',
|
|
486
|
-
' }',
|
|
487
|
-
' handle /api/ide/* {',
|
|
488
|
-
' reverse_proxy auth-broker:9780 {',
|
|
489
|
-
' header_up X-Forwarded-Proto https',
|
|
490
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
491
|
-
' }',
|
|
492
|
-
' }',
|
|
493
|
-
' handle /oauth2/* {',
|
|
494
|
-
' reverse_proxy oauth2-proxy:4180 {',
|
|
495
|
-
' header_up X-Forwarded-Proto https',
|
|
496
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
497
|
-
' }',
|
|
498
|
-
' }',
|
|
499
|
-
' @health path /health /ready',
|
|
500
|
-
' handle @health {',
|
|
501
|
-
` reverse_proxy agent:${ctx.port}`,
|
|
502
|
-
' }',
|
|
503
|
-
' # API clients (IDE) with a Bearer token: validate via auth-broker, copy identity header.',
|
|
504
|
-
' @apiWithAuth {',
|
|
505
|
-
' path /v1/*',
|
|
506
|
-
' header Authorization *',
|
|
507
|
-
' }',
|
|
508
|
-
' handle @apiWithAuth {',
|
|
509
|
-
' forward_auth auth-broker:9780 {',
|
|
510
|
-
' uri /api/ide/me',
|
|
511
|
-
' copy_headers X-Agent-User',
|
|
512
|
-
' header_up X-Forwarded-Proto https',
|
|
513
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
514
|
-
' @unauth status 401',
|
|
515
|
-
' handle_response @unauth {',
|
|
516
|
-
' respond "Unauthorized" 401',
|
|
517
|
-
' }',
|
|
518
|
-
' }',
|
|
519
|
-
` reverse_proxy agent:${ctx.port}`,
|
|
520
|
-
' }',
|
|
521
|
-
' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User.',
|
|
522
|
-
' handle {',
|
|
523
|
-
' forward_auth oauth2-proxy:4180 {',
|
|
524
|
-
' uri /oauth2/auth',
|
|
525
|
-
' copy_headers X-Auth-Request-Email>X-Agent-User',
|
|
526
|
-
' header_up X-Forwarded-Proto https',
|
|
527
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
528
|
-
' @unauth status 401',
|
|
529
|
-
' handle_response @unauth {',
|
|
530
|
-
' redir * "/oauth2/sign_in?rd={http.request.uri.path}{http.request.uri.query}"',
|
|
531
|
-
' }',
|
|
532
|
-
' }',
|
|
533
|
-
' @api path /v1/*',
|
|
534
|
-
' handle @api {',
|
|
535
|
-
` reverse_proxy agent:${ctx.port}`,
|
|
536
|
-
' }',
|
|
537
|
-
' root * /srv/www',
|
|
538
|
-
' file_server',
|
|
539
|
-
' }',
|
|
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
|
+
})),
|
|
540
595
|
'}',
|
|
541
596
|
'',
|
|
542
597
|
].join('\n');
|
|
@@ -577,9 +632,10 @@ function renderAgentComposeService(ctx, extraLines = []) {
|
|
|
577
632
|
'',
|
|
578
633
|
];
|
|
579
634
|
}
|
|
580
|
-
/** oauth2-proxy + Caddy services for `--profile edge`.
|
|
581
|
-
|
|
582
|
-
|
|
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 ? [] : [
|
|
583
639
|
' oauth2-proxy:',
|
|
584
640
|
' profiles: ["edge"]',
|
|
585
641
|
' image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1',
|
|
@@ -597,6 +653,9 @@ function renderEdgeComposeServices() {
|
|
|
597
653
|
' OAUTH2_PROXY_REVERSE_PROXY: "true"',
|
|
598
654
|
' expose: ["4180"]',
|
|
599
655
|
'',
|
|
656
|
+
];
|
|
657
|
+
return [
|
|
658
|
+
...oauth2Proxy,
|
|
600
659
|
' auth-broker:',
|
|
601
660
|
' profiles: ["edge"]',
|
|
602
661
|
' build:',
|
|
@@ -622,7 +681,7 @@ function renderEdgeComposeServices() {
|
|
|
622
681
|
' required: false',
|
|
623
682
|
' ports: ["80:80", "443:443"] # use 80:80 not 127.0.0.1:80 — breaks vmnetd on some Docker Desktop builds',
|
|
624
683
|
' volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "./www:/srv/www:ro"]',
|
|
625
|
-
|
|
684
|
+
` depends_on: [${fed ? 'agent, auth-broker' : 'oauth2-proxy, agent, auth-broker'}]`,
|
|
626
685
|
'',
|
|
627
686
|
];
|
|
628
687
|
}
|
|
@@ -646,7 +705,7 @@ function renderTunnelComposeServices() {
|
|
|
646
705
|
function renderEdgeTunnelUpScript(hostname) {
|
|
647
706
|
return [
|
|
648
707
|
'#!/usr/bin/env bash',
|
|
649
|
-
'# T0.5b — Cloudflare Tunnel + Google SSO (no Access seats). Run
|
|
708
|
+
'# T0.5b — Cloudflare Tunnel + Google SSO (no Access seats). Run: bash deploy/run-edge-tunnel.sh',
|
|
650
709
|
'set -euo pipefail',
|
|
651
710
|
'cd "$(dirname "$0")"',
|
|
652
711
|
'docker compose --profile edge --profile tunnel up -d --build',
|
|
@@ -654,7 +713,113 @@ function renderEdgeTunnelUpScript(hostname) {
|
|
|
654
713
|
'',
|
|
655
714
|
].join('\n');
|
|
656
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
|
+
}
|
|
657
819
|
function vmArtifacts(ctx) {
|
|
820
|
+
if (ctx.ingress === 'edge-fed' && ctx.colocate) {
|
|
821
|
+
return renderColocatedArtifacts(ctx);
|
|
822
|
+
}
|
|
658
823
|
const hostname = ctx.publicHostname;
|
|
659
824
|
const run = [
|
|
660
825
|
'#!/usr/bin/env bash',
|
|
@@ -715,7 +880,7 @@ function vmArtifacts(ctx) {
|
|
|
715
880
|
'# edge only: docker compose --profile edge up -d (SSO on :443, no tunnel)',
|
|
716
881
|
'services:',
|
|
717
882
|
...renderAgentComposeService(ctx),
|
|
718
|
-
...renderEdgeComposeServices(),
|
|
883
|
+
...renderEdgeComposeServices(ctx.ingress === 'edge-fed'),
|
|
719
884
|
...renderTunnelComposeServices(),
|
|
720
885
|
'volumes:',
|
|
721
886
|
' agenthippo-state:',
|
|
@@ -736,6 +901,10 @@ function vmArtifacts(ctx) {
|
|
|
736
901
|
'README.md': [
|
|
737
902
|
`# Deploy: ${ctx.packName} — T0.5 workbench/VM`,
|
|
738
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
|
+
'',
|
|
739
908
|
'`cp deploy/.env.example deploy/.env` and fill in, then pick a path:',
|
|
740
909
|
'',
|
|
741
910
|
'**T0.5a — VPN only (simplest internal pilot)**',
|
|
@@ -746,18 +915,16 @@ function vmArtifacts(ctx) {
|
|
|
746
915
|
'',
|
|
747
916
|
'**T0.5b — Cloudflare Tunnel + Google SSO (recommended for WFH, no Access seats)**',
|
|
748
917
|
'```bash',
|
|
749
|
-
'agenthippo deploy init . --target vm --force # from pack root; review deploy/',
|
|
750
918
|
'cp deploy/.env.example deploy/.env # agent: model key + public hostname',
|
|
751
919
|
'cp deploy/.env.auth.example deploy/.env.auth # broker/oauth2-proxy: JWT + OAuth secrets (never loaded by the agent)',
|
|
752
920
|
'# 1. Google Cloud → OAuth Web client for https://' + hostname,
|
|
753
921
|
'# Authorized origin + redirect: https://' + hostname + '/oauth2/callback',
|
|
754
922
|
'# 2. cloudflared tunnel create <name>',
|
|
755
|
-
'# cp ~/.cloudflared/<tunnel-id>.json
|
|
923
|
+
'# cp ~/.cloudflared/<tunnel-id>.json ~/.agent-hippo/tunnel-credentials.json # secret',
|
|
756
924
|
'# set tunnel: <tunnel-id> in deploy/cloudflared.yml',
|
|
757
925
|
'# cloudflared tunnel route dns <name> ' + hostname,
|
|
758
926
|
'# 3. Fill deploy/.env (hostname, model key) and deploy/.env.auth (OAUTH2_PROXY_*, AGENTHIPPO_AUTH_JWT_SECRET)',
|
|
759
|
-
'
|
|
760
|
-
'# or from pack root: bash deploy/run-edge-tunnel.sh',
|
|
927
|
+
'bash deploy/run-edge-tunnel.sh',
|
|
761
928
|
'# Users open https://' + hostname + ' → Google login → static chat UI → /v1/chat',
|
|
762
929
|
'```',
|
|
763
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.',
|
|
@@ -770,6 +937,7 @@ function vmArtifacts(ctx) {
|
|
|
770
937
|
'',
|
|
771
938
|
'**Do not commit** `deploy/.env`, `deploy/.env.auth`, or `deploy/tunnel-credentials.json`.',
|
|
772
939
|
'',
|
|
940
|
+
...renderEgressNote(ctx),
|
|
773
941
|
].join('\n'),
|
|
774
942
|
};
|
|
775
943
|
}
|
|
@@ -799,6 +967,9 @@ const OTEL_COLLECTOR_COMPOSE_YAML = [
|
|
|
799
967
|
'',
|
|
800
968
|
].join('\n');
|
|
801
969
|
function composeArtifacts(ctx) {
|
|
970
|
+
if (ctx.ingress === 'edge-fed' && ctx.colocate) {
|
|
971
|
+
return renderColocatedArtifacts(ctx);
|
|
972
|
+
}
|
|
802
973
|
const channels = ctx.gatewayChannels?.trim();
|
|
803
974
|
const hostname = ctx.publicHostname;
|
|
804
975
|
// Agent Anywhere: override the agent command to also run the gateway bridge,
|
|
@@ -820,7 +991,7 @@ function composeArtifacts(ctx) {
|
|
|
820
991
|
' # OPENCLAW_GATEWAY_TOKEN (or a paired device) to connect. Set it in .env.',
|
|
821
992
|
' openclaw-gateway:',
|
|
822
993
|
' profiles: ["channels"]',
|
|
823
|
-
' image: ghcr.io/
|
|
994
|
+
' image: ghcr.io/agenthippoai/openclaw-gateway:2026.06',
|
|
824
995
|
' env_file:',
|
|
825
996
|
' - path: .env',
|
|
826
997
|
' required: false',
|
|
@@ -845,13 +1016,13 @@ function composeArtifacts(ctx) {
|
|
|
845
1016
|
'# e.g.: docker compose -f deploy/docker-compose.yml --profile edge --profile tunnel up -d',
|
|
846
1017
|
'services:',
|
|
847
1018
|
...renderAgentComposeService(ctx, agentExtra),
|
|
848
|
-
...renderEdgeComposeServices(),
|
|
1019
|
+
...renderEdgeComposeServices(ctx.ingress === 'edge-fed'),
|
|
849
1020
|
...renderTunnelComposeServices(),
|
|
850
1021
|
' # LiteLLM gateway — self-contained image bakes the Spotlight config + OTEL hooks',
|
|
851
1022
|
' # (build: docker build -f extensions/agentide/scripts/Dockerfile.litellm -t this-tag .)',
|
|
852
1023
|
' litellm:',
|
|
853
1024
|
' profiles: ["gateway"]',
|
|
854
|
-
' image: ghcr.io/
|
|
1025
|
+
' image: ghcr.io/agenthippoai/litellm-gateway:2026.06',
|
|
855
1026
|
' env_file:',
|
|
856
1027
|
' - path: .env',
|
|
857
1028
|
' required: false',
|
|
@@ -902,6 +1073,7 @@ function composeArtifacts(ctx) {
|
|
|
902
1073
|
'Profiles compose: e.g. `--profile gateway --profile edge --profile tunnel up -d`.',
|
|
903
1074
|
'LiteLLM must speak Anthropic API format — our gateway image does.',
|
|
904
1075
|
'',
|
|
1076
|
+
...renderEgressNote(ctx),
|
|
905
1077
|
].join('\n'),
|
|
906
1078
|
};
|
|
907
1079
|
}
|
|
@@ -921,6 +1093,9 @@ function composeArtifacts(ctx) {
|
|
|
921
1093
|
*/
|
|
922
1094
|
function litellmProviderKeyEnv(model) {
|
|
923
1095
|
const m = (model || '').toLowerCase();
|
|
1096
|
+
if (m === 'ah-auto') {
|
|
1097
|
+
return 'AGENTHIPPO_GATEWAY_KEY';
|
|
1098
|
+
}
|
|
924
1099
|
if (m.includes('codex')) {
|
|
925
1100
|
return 'OPENAI_CODEX_API_KEY';
|
|
926
1101
|
}
|
|
@@ -932,6 +1107,102 @@ function litellmProviderKeyEnv(model) {
|
|
|
932
1107
|
}
|
|
933
1108
|
return 'OPENAI_API_KEY';
|
|
934
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
|
+
}
|
|
935
1206
|
function renderCloudrunServiceYaml(ctx) {
|
|
936
1207
|
const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
|
|
937
1208
|
return [
|
|
@@ -967,6 +1238,11 @@ function renderCloudrunServiceYaml(ctx) {
|
|
|
967
1238
|
' env:',
|
|
968
1239
|
' - { name: AGENTHIPPO_HOME, value: /state }',
|
|
969
1240
|
' - { name: AGENTHIPPO_NO_AUTO_INSTALL, value: "1" }',
|
|
1241
|
+
// Pin serve to trust ONLY the IAP-verified header, so an authenticated caller
|
|
1242
|
+
// cannot impersonate another user via a spoofed X-Agent-User header. The deploy
|
|
1243
|
+
// script fills this with x-goog-authenticated-user-email when IAP is enabled, or
|
|
1244
|
+
// leaves it empty (unpinned) during an ENABLE_IAP=0 bootstrap deploy.
|
|
1245
|
+
' - { name: AGENTHIPPO_IDENTITY_HEADER, value: "${IDENTITY_HEADER_VALUE}" }',
|
|
970
1246
|
agentServeHostEnvYaml(),
|
|
971
1247
|
' - { name: AGENTHIPPO_LITELLM_BASE_URL, value: http://localhost:4443 }',
|
|
972
1248
|
' - name: AGENTHIPPO_LITELLM_API_KEY',
|
|
@@ -1064,7 +1340,7 @@ function renderCloudrunSpotlightDeploy(ctx) {
|
|
|
1064
1340
|
'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
|
|
1065
1341
|
'REPO="${ARTIFACT_REPO:-agenthippo}"',
|
|
1066
1342
|
'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
|
|
1067
|
-
'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/
|
|
1343
|
+
'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippoai/litellm-gateway:2026.07}"',
|
|
1068
1344
|
'STATE_BUCKET="${STATE_BUCKET:?set STATE_BUCKET=your-gcs-bucket (gcloud storage buckets create gs://...)}"',
|
|
1069
1345
|
'# LITELLM_KEY = the proxy key the agent presents to LiteLLM (and LiteLLM master key).',
|
|
1070
1346
|
'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
|
|
@@ -1075,10 +1351,10 @@ function renderCloudrunSpotlightDeploy(ctx) {
|
|
|
1075
1351
|
' || gcloud artifacts repositories create "$REPO" --repository-format=docker --location="$REGION"',
|
|
1076
1352
|
'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
|
|
1077
1353
|
'',
|
|
1078
|
-
'# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/
|
|
1354
|
+
'# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
|
|
1079
1355
|
'# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
|
|
1080
1356
|
'DF_TMP="$(mktemp)"',
|
|
1081
|
-
'sed "s|FROM ghcr.io/
|
|
1357
|
+
'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
|
|
1082
1358
|
'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
|
|
1083
1359
|
'docker push "$IMAGE"',
|
|
1084
1360
|
'',
|
|
@@ -1088,10 +1364,18 @@ function renderCloudrunSpotlightDeploy(ctx) {
|
|
|
1088
1364
|
'ensure_secret "${SERVICE}-litellm-key"; printf %s "$LITELLM_KEY" | gcloud secrets versions add "${SERVICE}-litellm-key" --data-file=-',
|
|
1089
1365
|
'ensure_secret "${SERVICE}-provider-key"; printf %s "$PROVIDER_KEY" | gcloud secrets versions add "${SERVICE}-provider-key" --data-file=-',
|
|
1090
1366
|
'',
|
|
1367
|
+
'# IAP (finalized below) forwards X-Goog-Authenticated-User-Email; pin serve to trust ONLY',
|
|
1368
|
+
'# that header so an authenticated caller cannot impersonate another user via a spoofed',
|
|
1369
|
+
'# X-Agent-User header. Skipped when ENABLE_IAP=0 (first-deploy bootstrap) since IAP never',
|
|
1370
|
+
'# sets the header then.',
|
|
1371
|
+
'IDENTITY_HEADER_VALUE=""',
|
|
1372
|
+
'[[ "${ENABLE_IAP:-1}" == "1" ]] && IDENTITY_HEADER_VALUE="x-goog-authenticated-user-email" || true',
|
|
1373
|
+
'',
|
|
1091
1374
|
'# Render the multi-container template for this project (sed — no gettext dependency) and deploy.',
|
|
1092
1375
|
'RENDERED="$(mktemp)"',
|
|
1093
1376
|
'sed -e "s|\\${SERVICE}|${SERVICE}|g" -e "s|\\${IMAGE}|${IMAGE}|g" \\',
|
|
1094
1377
|
' -e "s|\\${LITELLM_IMAGE}|${LITELLM_IMAGE}|g" -e "s|\\${STATE_BUCKET}|${STATE_BUCKET}|g" \\',
|
|
1378
|
+
' -e "s|\\${IDENTITY_HEADER_VALUE}|${IDENTITY_HEADER_VALUE}|g" \\',
|
|
1095
1379
|
' deploy/service.yaml > "$RENDERED"',
|
|
1096
1380
|
'gcloud run services replace "$RENDERED" --region "$REGION"',
|
|
1097
1381
|
'',
|
|
@@ -1115,85 +1399,15 @@ function renderCloudrunSpotlightDeploy(ctx) {
|
|
|
1115
1399
|
* via `localhost:<port>` (NOT docker-DNS names like the compose edge). Cloud Run terminates TLS
|
|
1116
1400
|
* at the edge, so Caddy listens on HTTP `:{$PORT}` (`auto_https off`). The agent is NOT the
|
|
1117
1401
|
* ingress here, so it listens on a distinct internal port (EDGE_AGENT_PORT) to avoid clashing
|
|
1118
|
-
* 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.
|
|
1119
1405
|
*/
|
|
1120
|
-
const EDGE_AGENT_PORT = 8090;
|
|
1121
|
-
/** Caddyfile for the Cloud Run edge — HTTP on $PORT (Cloud Run terminates TLS), localhost upstreams. */
|
|
1122
|
-
function renderCloudrunEdgeCaddyfile() {
|
|
1123
|
-
return [
|
|
1124
|
-
'# Cloud Run edge ingress. Cloud Run terminates TLS; Caddy listens on HTTP :{$PORT}.',
|
|
1125
|
-
'# Sidecars share the pod\'s localhost → upstreams are 127.0.0.1:<port> (not docker-DNS names).',
|
|
1126
|
-
'{',
|
|
1127
|
-
' auto_https off',
|
|
1128
|
-
'}',
|
|
1129
|
-
'',
|
|
1130
|
-
':{$PORT} {',
|
|
1131
|
-
' # Auth-broker: IDE sign-in callback + JWT validation endpoint.',
|
|
1132
|
-
' handle /oauth/* {',
|
|
1133
|
-
' reverse_proxy localhost:9780 {',
|
|
1134
|
-
' header_up X-Forwarded-Proto https',
|
|
1135
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1136
|
-
' }',
|
|
1137
|
-
' }',
|
|
1138
|
-
' handle /api/ide/* {',
|
|
1139
|
-
' reverse_proxy localhost:9780 {',
|
|
1140
|
-
' header_up X-Forwarded-Proto https',
|
|
1141
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1142
|
-
' }',
|
|
1143
|
-
' }',
|
|
1144
|
-
' # oauth2-proxy: Google SSO callback + sign-in.',
|
|
1145
|
-
' handle /oauth2/* {',
|
|
1146
|
-
' reverse_proxy localhost:4180 {',
|
|
1147
|
-
' header_up X-Forwarded-Proto https',
|
|
1148
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1149
|
-
' }',
|
|
1150
|
-
' }',
|
|
1151
|
-
' @health path /health /ready',
|
|
1152
|
-
' handle @health {',
|
|
1153
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1154
|
-
' }',
|
|
1155
|
-
' # IDE / API clients with a Bearer token: validate via auth-broker, copy identity header.',
|
|
1156
|
-
' @apiWithAuth {',
|
|
1157
|
-
' path /v1/*',
|
|
1158
|
-
' header Authorization *',
|
|
1159
|
-
' }',
|
|
1160
|
-
' handle @apiWithAuth {',
|
|
1161
|
-
' forward_auth localhost:9780 {',
|
|
1162
|
-
' uri /api/ide/me',
|
|
1163
|
-
' copy_headers X-Agent-User',
|
|
1164
|
-
' header_up X-Forwarded-Proto https',
|
|
1165
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1166
|
-
' @unauth status 401',
|
|
1167
|
-
' handle_response @unauth {',
|
|
1168
|
-
' respond "Unauthorized" 401',
|
|
1169
|
-
' }',
|
|
1170
|
-
' }',
|
|
1171
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1172
|
-
' }',
|
|
1173
|
-
' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User, then proxy',
|
|
1174
|
-
' # to the agent (which serves the www landing page at / and the API at /v1/*).',
|
|
1175
|
-
' handle {',
|
|
1176
|
-
' forward_auth localhost:4180 {',
|
|
1177
|
-
' uri /oauth2/auth',
|
|
1178
|
-
' copy_headers X-Auth-Request-Email>X-Agent-User',
|
|
1179
|
-
' header_up X-Forwarded-Proto https',
|
|
1180
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1181
|
-
' @unauth status 401',
|
|
1182
|
-
' handle_response @unauth {',
|
|
1183
|
-
' redir * "/oauth2/sign_in?rd={http.request.uri.path}{http.request.uri.query}"',
|
|
1184
|
-
' }',
|
|
1185
|
-
' }',
|
|
1186
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1187
|
-
' }',
|
|
1188
|
-
'}',
|
|
1189
|
-
'',
|
|
1190
|
-
].join('\n');
|
|
1191
|
-
}
|
|
1192
1406
|
/** Cloud Run multi-container service behind the AgentHippo edge (Option B). Caddy is ingress. */
|
|
1193
1407
|
function renderCloudrunEdgeServiceYaml(ctx) {
|
|
1194
1408
|
const isCursor = ctx.engine.trim().toLowerCase() === 'cursor';
|
|
1195
1409
|
const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
|
|
1196
|
-
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 })
|
|
1197
1411
|
.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ');
|
|
1198
1412
|
// Start order: sidecars before caddy (ingress) so Caddy's first request finds everyone up.
|
|
1199
1413
|
const deps = isCursor
|
|
@@ -1284,7 +1498,7 @@ function renderCloudrunEdgeServiceYaml(ctx) {
|
|
|
1284
1498
|
]),
|
|
1285
1499
|
' volumeMounts:',
|
|
1286
1500
|
' - { name: state, mountPath: /state }',
|
|
1287
|
-
...startupProbeLines(EDGE_AGENT_PORT, true),
|
|
1501
|
+
...startupProbeLines(identity_1.EDGE_AGENT_PORT, true),
|
|
1288
1502
|
...resourceLimits('2000m', '2Gi'),
|
|
1289
1503
|
];
|
|
1290
1504
|
if (!isCursor) {
|
|
@@ -1313,7 +1527,7 @@ function renderCloudrunEdgeDeploy(ctx) {
|
|
|
1313
1527
|
'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
|
|
1314
1528
|
'REPO="${ARTIFACT_REPO:-agenthippo}"',
|
|
1315
1529
|
'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
|
|
1316
|
-
'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/
|
|
1530
|
+
'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippoai/litellm-gateway:2026.07}"',
|
|
1317
1531
|
'# Public hostname (custom domain mapped to the service) + the Google Workspace domain allowed by SSO.',
|
|
1318
1532
|
'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=your.edge.hostname (gcloud run domain-mappings create --service $SERVICE --domain ...)}"',
|
|
1319
1533
|
'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:?set ALLOWED_DOMAINS=yourworkspace.com (oauth2-proxy email domain + broker allowed domains)}"',
|
|
@@ -1336,10 +1550,10 @@ function renderCloudrunEdgeDeploy(ctx) {
|
|
|
1336
1550
|
'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
|
|
1337
1551
|
'',
|
|
1338
1552
|
'# One agent image serves both the agent and the auth-broker containers (CLI ships `auth-broker`).',
|
|
1339
|
-
'# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/
|
|
1553
|
+
'# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
|
|
1340
1554
|
'# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
|
|
1341
1555
|
'DF_TMP="$(mktemp)"',
|
|
1342
|
-
'sed "s|FROM ghcr.io/
|
|
1556
|
+
'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
|
|
1343
1557
|
'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
|
|
1344
1558
|
'docker push "$IMAGE"',
|
|
1345
1559
|
'',
|
|
@@ -1414,6 +1628,7 @@ function cloudrunArtifacts(ctx) {
|
|
|
1414
1628
|
'**Lean alternative (no Spotlight):** a single-container deploy that calls a provider directly',
|
|
1415
1629
|
'(or an external LiteLLM) — set `AGENTHIPPO_LITELLM_BASE_URL` to your endpoint and drop the sidecars.',
|
|
1416
1630
|
'',
|
|
1631
|
+
...renderEgressNote(ctx),
|
|
1417
1632
|
].join('\n'),
|
|
1418
1633
|
};
|
|
1419
1634
|
}
|
|
@@ -1449,13 +1664,21 @@ function cloudrunArtifacts(ctx) {
|
|
|
1449
1664
|
'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
|
|
1450
1665
|
'',
|
|
1451
1666
|
'# Build from pack root; Dockerfile is deploy/Dockerfile (not pack root — gcloud --source . misses it).',
|
|
1452
|
-
'# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/
|
|
1667
|
+
'# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
|
|
1453
1668
|
'# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
|
|
1454
1669
|
'DF_TMP="$(mktemp)"',
|
|
1455
|
-
'sed "s|FROM ghcr.io/
|
|
1670
|
+
'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
|
|
1456
1671
|
'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
|
|
1457
1672
|
'docker push "$IMAGE"',
|
|
1458
1673
|
'',
|
|
1674
|
+
'# IAP (when enabled below) forwards X-Goog-Authenticated-User-Email; pin serve to trust',
|
|
1675
|
+
'# ONLY that header so an authenticated caller cannot impersonate another user via a',
|
|
1676
|
+
'# spoofed X-Agent-User header. Skipped when ENABLE_IAP=0 (first-deploy bootstrap) since',
|
|
1677
|
+
'# IAP never sets the header then — pinning to an absent header would make every request',
|
|
1678
|
+
'# anonymous instead of leaving body-supplied identity usable during bootstrap.',
|
|
1679
|
+
'IDENTITY_ENV=""',
|
|
1680
|
+
'[[ "${ENABLE_IAP:-1}" == "1" ]] && IDENTITY_ENV=",AGENTHIPPO_IDENTITY_HEADER=x-goog-authenticated-user-email" || true',
|
|
1681
|
+
'',
|
|
1459
1682
|
'# Deploy private (no --allow-unauthenticated). IAP + role grants are applied below on the beta track.',
|
|
1460
1683
|
'gcloud run deploy "$SERVICE" \\',
|
|
1461
1684
|
' --image "$IMAGE" \\',
|
|
@@ -1465,7 +1688,7 @@ function cloudrunArtifacts(ctx) {
|
|
|
1465
1688
|
' --timeout 3600 \\',
|
|
1466
1689
|
' --add-volume name=state,type=cloud-storage,bucket="$STATE_BUCKET" \\',
|
|
1467
1690
|
' --add-volume-mount volume=state,mount-path=/state \\',
|
|
1468
|
-
` --set-env-vars ${runEnvVars} \\`,
|
|
1691
|
+
` --set-env-vars "${runEnvVars}\${IDENTITY_ENV}" \\`,
|
|
1469
1692
|
...(runSecrets ? [` --set-secrets ${runSecrets} \\`] : []),
|
|
1470
1693
|
' --region "$REGION"',
|
|
1471
1694
|
'',
|
|
@@ -1519,6 +1742,7 @@ function cloudrunArtifacts(ctx) {
|
|
|
1519
1742
|
' -d \'{"message":"Reply with exactly: CLOUDRUN_OK","session_id":"cr-1"}\'',
|
|
1520
1743
|
'```',
|
|
1521
1744
|
'',
|
|
1745
|
+
...renderEgressNote(ctx),
|
|
1522
1746
|
].join('\n'),
|
|
1523
1747
|
};
|
|
1524
1748
|
}
|
|
@@ -1530,7 +1754,7 @@ function cloudrunEdgeArtifacts(ctx, inbound, isCursor) {
|
|
|
1530
1754
|
const files = {
|
|
1531
1755
|
'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
|
|
1532
1756
|
'www/index.html': renderLandingPage(ctx.packName),
|
|
1533
|
-
'Caddyfile':
|
|
1757
|
+
'Caddyfile': (0, identity_1.renderSidecarEdgeCaddyfile)(),
|
|
1534
1758
|
'service.yaml': renderCloudrunEdgeServiceYaml(ctx),
|
|
1535
1759
|
'cloudrun-deploy.sh': renderCloudrunEdgeDeploy(ctx),
|
|
1536
1760
|
};
|
|
@@ -1544,7 +1768,7 @@ function cloudrunEdgeArtifacts(ctx, inbound, isCursor) {
|
|
|
1544
1768
|
'',
|
|
1545
1769
|
`${sidecars} Sidecars share the pod\'s localhost (Cloud Run multi-container):`,
|
|
1546
1770
|
'- **caddy** (ingress, the only port) → SSO edge: oauth2-proxy for browsers, auth-broker for IDE Bearer',
|
|
1547
|
-
`- **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)`,
|
|
1548
1772
|
'- **auth-broker** → validates IDE AgentHippo JWTs (`/api/ide/me`) + IDE sign-in (`/oauth/*`)',
|
|
1549
1773
|
'- **oauth2-proxy** → Google SSO for browser clients (`provider=google`)',
|
|
1550
1774
|
...(isCursor ? [] : [
|
|
@@ -1577,77 +1801,10 @@ function cloudrunEdgeArtifacts(ctx, inbound, isCursor) {
|
|
|
1577
1801
|
? '**Inbound channel (1b):** `minScale: 1` pins one always-on instance (the socket can\'t scale to zero).'
|
|
1578
1802
|
: '**No inbound channel (1a):** scales to zero; traces + sessions persist on the GCS `/state` volume.',
|
|
1579
1803
|
'',
|
|
1804
|
+
...renderEgressNote(ctx),
|
|
1580
1805
|
].join('\n');
|
|
1581
1806
|
return files;
|
|
1582
1807
|
}
|
|
1583
|
-
/**
|
|
1584
|
-
* A′ — Federated edge Caddyfile. No oauth2-proxy: Google login happens once at the central IdP
|
|
1585
|
-
* (`ctx.idpUrl`); this agent only verifies the shared AgentHippo JWT (Bearer for IDE, cookie for
|
|
1586
|
-
* browser). On a browser 401, redirect to the IdP's `/oauth/browser-login` to stamp the shared
|
|
1587
|
-
* `.agenthippo.ai` cookie, then return.
|
|
1588
|
-
*/
|
|
1589
|
-
function renderCloudrunFedCaddyfile(ctx) {
|
|
1590
|
-
const idp = (ctx.idpUrl || '').trim().replace(/\/+$/, '');
|
|
1591
|
-
const loginRedirect = idp
|
|
1592
|
-
? ` redir * "${idp}/oauth/browser-login?return_to=https://{http.request.host}{http.request.uri}" 302`
|
|
1593
|
-
: ' respond "Unauthorized" 401';
|
|
1594
|
-
return [
|
|
1595
|
-
'# Cloud Run federated edge (A′). Cloud Run terminates TLS; Caddy listens on HTTP :{$PORT}.',
|
|
1596
|
-
`# No oauth2-proxy — Google login happens once at the central IdP (${idp || '<IDP_URL>'});`,
|
|
1597
|
-
'# this agent only verifies the shared .agenthippo.ai AgentHippo JWT (Bearer for IDE, cookie for browser).',
|
|
1598
|
-
'{',
|
|
1599
|
-
' auto_https off',
|
|
1600
|
-
'}',
|
|
1601
|
-
'',
|
|
1602
|
-
':{$PORT} {',
|
|
1603
|
-
' # Auth-broker: IDE bootstrap API + JWT validation (/api/ide/me is also used by forward_auth).',
|
|
1604
|
-
' handle /api/ide/* {',
|
|
1605
|
-
' reverse_proxy localhost:9780 {',
|
|
1606
|
-
' header_up X-Forwarded-Proto https',
|
|
1607
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1608
|
-
' }',
|
|
1609
|
-
' }',
|
|
1610
|
-
' @health path /health /ready',
|
|
1611
|
-
' handle @health {',
|
|
1612
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1613
|
-
' }',
|
|
1614
|
-
' # IDE / API clients with a Bearer token: validate via auth-broker, copy identity header.',
|
|
1615
|
-
' @apiWithAuth {',
|
|
1616
|
-
' path /v1/*',
|
|
1617
|
-
' header Authorization *',
|
|
1618
|
-
' }',
|
|
1619
|
-
' handle @apiWithAuth {',
|
|
1620
|
-
' forward_auth localhost:9780 {',
|
|
1621
|
-
' uri /api/ide/me',
|
|
1622
|
-
' copy_headers X-Agent-User',
|
|
1623
|
-
' header_up X-Forwarded-Proto https',
|
|
1624
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1625
|
-
' @unauth status 401',
|
|
1626
|
-
' handle_response @unauth {',
|
|
1627
|
-
' respond "Unauthorized" 401',
|
|
1628
|
-
' }',
|
|
1629
|
-
' }',
|
|
1630
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1631
|
-
' }',
|
|
1632
|
-
' # Browser clients: the shared .agenthippo.ai cookie is verified by auth-broker. No cookie',
|
|
1633
|
-
' # → redirect to the central IdP to log in (it stamps the cookie), then return.',
|
|
1634
|
-
' handle {',
|
|
1635
|
-
' forward_auth localhost:9780 {',
|
|
1636
|
-
' uri /api/ide/me',
|
|
1637
|
-
' copy_headers X-Agent-User',
|
|
1638
|
-
' header_up X-Forwarded-Proto https',
|
|
1639
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1640
|
-
' @unauth status 401',
|
|
1641
|
-
' handle_response @unauth {',
|
|
1642
|
-
loginRedirect,
|
|
1643
|
-
' }',
|
|
1644
|
-
' }',
|
|
1645
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1646
|
-
' }',
|
|
1647
|
-
'}',
|
|
1648
|
-
'',
|
|
1649
|
-
].join('\n');
|
|
1650
|
-
}
|
|
1651
1808
|
/** Cloud Run requires a startupProbe on any container that is a `depends_on` target, so the
|
|
1652
1809
|
* platform knows when the dependency is ready. `slow` = one generous 240s attempt (agent, which
|
|
1653
1810
|
* may warm an engine); otherwise 30×5s polls (sidecars that start fast). */
|
|
@@ -1690,7 +1847,7 @@ function resourceLimits(cpu, memory) {
|
|
|
1690
1847
|
function renderCloudrunFedServiceYaml(ctx) {
|
|
1691
1848
|
const isCursor = ctx.engine.trim().toLowerCase() === 'cursor';
|
|
1692
1849
|
const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
|
|
1693
|
-
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 })
|
|
1694
1851
|
.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ');
|
|
1695
1852
|
const cookieDomain = (ctx.cookieDomain || '.agenthippo.ai').trim();
|
|
1696
1853
|
const idpUrl = (ctx.idpUrl || 'https://auth.agenthippo.ai').trim().replace(/\/+$/, '');
|
|
@@ -1766,11 +1923,13 @@ function renderCloudrunFedServiceYaml(ctx) {
|
|
|
1766
1923
|
]),
|
|
1767
1924
|
' volumeMounts:',
|
|
1768
1925
|
' - { name: state, mountPath: /state }',
|
|
1769
|
-
...startupProbeLines(EDGE_AGENT_PORT, true),
|
|
1926
|
+
...startupProbeLines(identity_1.EDGE_AGENT_PORT, true),
|
|
1770
1927
|
...resourceLimits('2000m', '2Gi'),
|
|
1771
1928
|
];
|
|
1772
1929
|
if (!isCursor) {
|
|
1773
|
-
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 }',
|
|
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'));
|
|
1774
1933
|
}
|
|
1775
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 }');
|
|
1776
1935
|
if (!isCursor) {
|
|
@@ -1779,6 +1938,141 @@ function renderCloudrunFedServiceYaml(ctx) {
|
|
|
1779
1938
|
lines.push('');
|
|
1780
1939
|
return lines.join('\n');
|
|
1781
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
|
+
}
|
|
1782
2076
|
/** Deploy script for the federated edge (A′): build, upsert secrets (no OAuth), replace, make
|
|
1783
2077
|
* public, map the subdomain. No IAP; no Google OAuth client. */
|
|
1784
2078
|
function renderCloudrunFedDeploy(ctx) {
|
|
@@ -1798,7 +2092,7 @@ function renderCloudrunFedDeploy(ctx) {
|
|
|
1798
2092
|
'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
|
|
1799
2093
|
'REPO="${ARTIFACT_REPO:-agenthippo}"',
|
|
1800
2094
|
'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
|
|
1801
|
-
'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/
|
|
2095
|
+
'LITELLM_IMAGE="${LITELLM_IMAGE:-ghcr.io/agenthippoai/litellm-gateway:2026.07}"',
|
|
1802
2096
|
'# Public hostname = a *.agenthippo.ai subdomain mapped to this service (one-time wildcard DNS).',
|
|
1803
2097
|
'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai}"',
|
|
1804
2098
|
'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:-agenthippo.ai}"',
|
|
@@ -1807,6 +2101,10 @@ function renderCloudrunFedDeploy(ctx) {
|
|
|
1807
2101
|
'AUTH_JWT_SECRET="${AUTH_JWT_SECRET:?set AUTH_JWT_SECRET=<copy from auth.agenthippo.ai>}"',
|
|
1808
2102
|
...(isCursor ? [
|
|
1809
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>}"',
|
|
1810
2108
|
] : [
|
|
1811
2109
|
'# LiteLLM proxy key + the upstream provider key the gateway uses for the model.',
|
|
1812
2110
|
'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
|
|
@@ -1818,10 +2116,10 @@ function renderCloudrunFedDeploy(ctx) {
|
|
|
1818
2116
|
'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
|
|
1819
2117
|
'',
|
|
1820
2118
|
'# One agent image serves both the agent and the auth-broker containers (CLI ships `auth-broker`).',
|
|
1821
|
-
'# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/
|
|
2119
|
+
'# Rewrite the runtime base to this project Artifact Registry (the ghcr.io/agenthippoai/runtime:latest',
|
|
1822
2120
|
'# tag was retired; AR has runtime:latest / :latest-cursor). No-op if the base already targets AR.',
|
|
1823
2121
|
'DF_TMP="$(mktemp)"',
|
|
1824
|
-
'sed "s|FROM ghcr.io/
|
|
2122
|
+
'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
|
|
1825
2123
|
'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
|
|
1826
2124
|
'docker push "$IMAGE"',
|
|
1827
2125
|
'',
|
|
@@ -1839,30 +2137,36 @@ function renderCloudrunFedDeploy(ctx) {
|
|
|
1839
2137
|
}
|
|
1840
2138
|
/** A′ artifacts: Cloud Run behind the federated AgentHippo edge (Caddy + auth-broker; no oauth2-proxy). */
|
|
1841
2139
|
function cloudrunFedArtifacts(ctx, inbound, isCursor) {
|
|
2140
|
+
const gatewayDirect = usesGatewayDirect(ctx) && !isCursor;
|
|
1842
2141
|
const sidecars = isCursor
|
|
1843
2142
|
? 'Caddy (ingress) + auth-broker + agent.'
|
|
1844
|
-
:
|
|
2143
|
+
: gatewayDirect
|
|
2144
|
+
? 'Caddy (ingress) + auth-broker + agent (gateway-direct — no LiteLLM/OTEL sidecars).'
|
|
2145
|
+
: 'Caddy (ingress) + auth-broker + agent + LiteLLM + OTEL collector (Spotlight).';
|
|
1845
2146
|
const idp = (ctx.idpUrl || 'https://auth.agenthippo.ai').trim().replace(/\/+$/, '');
|
|
1846
2147
|
const files = {
|
|
1847
2148
|
'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
|
|
1848
2149
|
'www/index.html': renderLandingPage(ctx.packName),
|
|
1849
|
-
'Caddyfile':
|
|
1850
|
-
'service.yaml': renderCloudrunFedServiceYaml(ctx),
|
|
1851
|
-
'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),
|
|
1852
2153
|
};
|
|
1853
|
-
if (!isCursor) {
|
|
2154
|
+
if (!isCursor && !gatewayDirect) {
|
|
1854
2155
|
files['otel-collector.yaml'] = OTEL_COLLECTOR_COMPOSE_YAML;
|
|
1855
2156
|
}
|
|
1856
2157
|
files['README.md'] = [
|
|
1857
|
-
`# Deploy: ${ctx.packName} — Cloud Run + AgentHippo federated edge (A
|
|
2158
|
+
`# Deploy: ${ctx.packName} — Cloud Run + AgentHippo federated edge (A′${gatewayDirect ? ', gateway-direct' : ''})`,
|
|
1858
2159
|
'',
|
|
1859
2160
|
`Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}. Runtime base: \`${ctx.image}\`.`,
|
|
1860
2161
|
'',
|
|
1861
2162
|
`${sidecars} Sidecars share the pod\'s localhost (Cloud Run multi-container):`,
|
|
1862
2163
|
'- **caddy** (ingress, the only port) → verifies the shared AgentHippo JWT (Bearer for IDE, cookie for browser)',
|
|
1863
|
-
`- **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)`,
|
|
1864
2165
|
'- **auth-broker** → validates the shared `.agenthippo.ai` JWT via `/api/ide/me` (same secret as the IdP)',
|
|
1865
|
-
...(
|
|
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 ? [] : [
|
|
1866
2170
|
'- **litellm** gateway → exports OTLP to `http://localhost:4318`',
|
|
1867
2171
|
'- **otel-collector** → writes JSONL traces to the in-memory `/state` volume',
|
|
1868
2172
|
]),
|
|
@@ -1877,23 +2181,31 @@ function cloudrunFedArtifacts(ctx, inbound, isCursor) {
|
|
|
1877
2181
|
'cp deploy/.env.example deploy/.env && cp deploy/.env.auth.example deploy/.env.auth # fill both',
|
|
1878
2182
|
'export PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai # e.g. cs.agenthippo.ai',
|
|
1879
2183
|
'export AUTH_JWT_SECRET=<copy from auth.agenthippo.ai> # shared signing key',
|
|
1880
|
-
...(isCursor ? ['export PROVIDER_KEY=<cursor api 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=…']),
|
|
1881
2189
|
'bash deploy/cloudrun-deploy.sh # builds linux/amd64, upserts secrets, `gcloud run services replace`, maps the subdomain',
|
|
1882
2190
|
'```',
|
|
1883
2191
|
'',
|
|
1884
2192
|
'**One-time setup (not per agent):** wildcard DNS `*.agenthippo.ai` → Cloud Run (`gctrl.run.app`),',
|
|
1885
2193
|
`and the IdP (${idp}) must run the \`/oauth/browser-login\` route (oauth2-proxy forward_auth →`,
|
|
1886
2194
|
'auth-broker sets the `ah_token` cookie on `.agenthippo.ai`). After that, every new agent is',
|
|
1887
|
-
|
|
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.',
|
|
1888
2198
|
'',
|
|
1889
|
-
|
|
1890
|
-
|
|
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`).',
|
|
1891
2202
|
'`AGENTHIPPO_AUTH_COOKIE_DOMAIN` / `AGENTHIPPO_AUTH_IDP_URL` are baked in at generation time.',
|
|
1892
2203
|
'',
|
|
1893
2204
|
inbound
|
|
1894
2205
|
? '**Inbound channel (1b):** `minScale: 1` pins one always-on instance (the socket can\'t scale to zero).'
|
|
1895
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).',
|
|
1896
2207
|
'',
|
|
2208
|
+
...renderEgressNote(ctx),
|
|
1897
2209
|
].join('\n');
|
|
1898
2210
|
return files;
|
|
1899
2211
|
}
|
|
@@ -1929,7 +2241,17 @@ function databricksAppArtifacts(ctx) {
|
|
|
1929
2241
|
" '--workspace', '/tmp/agenthippo/workspace',",
|
|
1930
2242
|
" '--state-dir', '/tmp/agenthippo/state',",
|
|
1931
2243
|
" '--port', port,",
|
|
1932
|
-
"], { stdio: 'inherit', env: {
|
|
2244
|
+
"], { stdio: 'inherit', env: {",
|
|
2245
|
+
" ...process.env,",
|
|
2246
|
+
" AGENTHIPPO_SERVE_HOST: '0.0.0.0',",
|
|
2247
|
+
" // Databricks Apps workspace SSO sets X-Forwarded-Email; pin serve to trust ONLY that",
|
|
2248
|
+
" // header so an authenticated caller cannot impersonate another user via a spoofed",
|
|
2249
|
+
" // X-Agent-User header (Databricks does not know about or strip that header).",
|
|
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' }),",
|
|
2254
|
+
"} });",
|
|
1933
2255
|
"child.on('exit', (code) => process.exit(code ?? 0));",
|
|
1934
2256
|
'',
|
|
1935
2257
|
].join('\n');
|
|
@@ -1953,6 +2275,7 @@ function databricksAppArtifacts(ctx) {
|
|
|
1953
2275
|
'> spike (serve-v3 §13.2). Without it serve queries as one app identity. Identity',
|
|
1954
2276
|
'> attribution (`user_id`) works today; per-user *data* enforcement needs the spike.',
|
|
1955
2277
|
'',
|
|
2278
|
+
...renderEgressNote(ctx),
|
|
1956
2279
|
].join('\n'),
|
|
1957
2280
|
};
|
|
1958
2281
|
}
|
|
@@ -2034,7 +2357,10 @@ function cloudflareArtifacts(ctx) {
|
|
|
2034
2357
|
migrations: [{ tag: 'v1', new_sqlite_classes: ['Agent'] }],
|
|
2035
2358
|
}, null, 2) + '\n';
|
|
2036
2359
|
return {
|
|
2037
|
-
|
|
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' } }),
|
|
2038
2364
|
'wrangler.jsonc': wrangler,
|
|
2039
2365
|
'README.md': [
|
|
2040
2366
|
`# Deploy: ${ctx.packName} — Cloudflare Container (T1-generic)`,
|
|
@@ -2044,6 +2370,537 @@ function cloudflareArtifacts(ctx) {
|
|
|
2044
2370
|
'then proxies to the container binding. For the *external/unauthenticated* case',
|
|
2045
2371
|
'(customer support), use the full reference at `examples/deploy/external-support/`.',
|
|
2046
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),
|
|
2047
2904
|
].join('\n'),
|
|
2048
2905
|
};
|
|
2049
2906
|
}
|
|
@@ -2054,6 +2911,8 @@ const RENDERERS = {
|
|
|
2054
2911
|
'databricks-app': databricksAppArtifacts,
|
|
2055
2912
|
spcs: spcsArtifacts,
|
|
2056
2913
|
cloudflare: cloudflareArtifacts,
|
|
2914
|
+
'aws-ecs': awsEcsArtifacts,
|
|
2915
|
+
'azure-container-apps': azureContainerAppsArtifacts,
|
|
2057
2916
|
};
|
|
2058
2917
|
/**
|
|
2059
2918
|
* Render every artifact for a target (relative path → file content), always
|
|
@@ -2061,18 +2920,24 @@ const RENDERERS = {
|
|
|
2061
2920
|
*/
|
|
2062
2921
|
function renderTarget(target, ctx) {
|
|
2063
2922
|
const files = RENDERERS[target](ctx);
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
//
|
|
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)));
|
|
2068
2932
|
const hasDockerfile = Object.keys(files).some((k) => k === 'Dockerfile' || k.endsWith('/Dockerfile'));
|
|
2069
|
-
|
|
2933
|
+
const raw = {
|
|
2070
2934
|
...files,
|
|
2071
2935
|
...(hasDockerfile ? { '.dockerignore': renderDockerignore() } : {}),
|
|
2072
2936
|
'.env.example': renderEnvExample(ctx.envVars, {
|
|
2073
2937
|
edge,
|
|
2074
2938
|
engine: ctx.engine,
|
|
2075
2939
|
hostname: ctx.publicHostname,
|
|
2940
|
+
gatewayDirect,
|
|
2076
2941
|
}),
|
|
2077
2942
|
// Edge auth secrets are split out so the agent container never loads them.
|
|
2078
2943
|
...(edge ? {
|
|
@@ -2083,5 +2948,11 @@ function renderTarget(target, ctx) {
|
|
|
2083
2948
|
cookieDomain: ctx.cookieDomain,
|
|
2084
2949
|
}),
|
|
2085
2950
|
} : {}),
|
|
2951
|
+
'DATA-ACCESS.md': renderDataAccessSummary(ctx, target, edge, fed, colocate),
|
|
2086
2952
|
};
|
|
2953
|
+
const out = {};
|
|
2954
|
+
for (const [rel, content] of Object.entries(raw)) {
|
|
2955
|
+
out[rel] = withPackAttribution(ctx, rel, content);
|
|
2956
|
+
}
|
|
2957
|
+
return out;
|
|
2087
2958
|
}
|