@agenthippo-ai/agenthippo 0.1.1 → 1.110.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +293 -82
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +23 -25
- package/out/analytics/sessionEvents.js +109 -22
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +49 -15
- package/out/auth/onboardingPrompt.js +112 -0
- package/out/auth/provider.js +9 -22
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +54 -4
- package/out/auth-broker/server.js +152 -20
- package/out/auth-broker/store.js +25 -24
- package/out/authority/warrant.js +206 -0
- package/out/branding.js +27 -1
- package/out/cli/commands/ask.js +3 -0
- package/out/cli/commands/chat.js +3 -0
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +67 -0
- package/out/cli/commands/deploy.js +50 -10
- package/out/cli/commands/gateway.js +58 -0
- package/out/cli/commands/idp.js +85 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +332 -0
- package/out/cli/commands/serve.js +1165 -82
- package/out/cli/commands/store.js +12 -4
- package/out/cli/credential-context.js +42 -80
- 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 +218 -0
- package/out/cli/data-plane/index.js +152 -0
- package/out/cli/data-plane/registry.js +89 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +161 -0
- package/out/cli/deploy/blueprints/registry.js +84 -0
- package/out/cli/deploy/layers/identity.js +172 -0
- package/out/cli/deploy-init.js +1182 -347
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +338 -0
- package/out/cli/index.js +192 -23
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +45 -3
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +44 -3
- package/out/cli/serve-hardening.js +139 -3
- package/out/cli/state-sync.js +16 -15
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +47 -8
- package/out/engines/codex.js +125 -22
- package/out/engines/custom.js +54 -12
- package/out/evidence/sessionShip.js +610 -0
- package/out/extension.js +13 -2
- package/out/gateway/bridge.js +40 -2
- package/out/gateway/client.js +34 -1
- package/out/gateway/installSource.js +68 -0
- package/out/gateway/lifecycle.js +185 -51
- package/out/gateway/outboundPaths.js +4 -1
- package/out/gateway/outboundSend.js +6 -6
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +40 -9
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/live-probe/connector.mjs +8 -0
- package/out/live-probe/env-report.mjs +8 -0
- package/out/mcp.js +115 -67
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +309 -0
- package/out/pack/lease-schema.js +270 -0
- package/out/pack/lease.js +640 -0
- package/out/pack/reconcile.js +217 -0
- package/out/pack/signature.js +839 -0
- package/out/remote/handleRemoteChat.js +44 -3
- package/out/remote/remoteTrust.js +277 -0
- package/out/remote/serveClient.js +52 -5
- package/out/runner/host.js +131 -0
- package/out/runner/protocol.js +108 -0
- package/out/runner/remoteEngine.js +130 -0
- package/out/runtime/interfaces/edit-tracker.js +2 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
- package/out/sandbox/containerDriver.js +570 -0
- package/out/sandbox/processDriver.js +315 -0
- package/out/sandbox/sandboxRunner.js +191 -0
- package/out/sandbox/support.js +301 -0
- package/out/sandbox/types.js +55 -0
- package/out/scripts/agentide-litellm.sh +67 -5
- package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -118
- package/out/scripts/analytics/setup-analytics.ps1 +37 -13
- package/out/scripts/analytics/setup-analytics.sh +93 -13
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +12 -111
- package/out/shared/providers.js +2 -2
- package/out/spotlight/install.js +7 -1
- package/out/store/index.js +41 -10
- package/out/sync.js +3 -1
- package/out/test/analytics.test.js +198 -34
- package/out/test/auth-baseurl.test.js +132 -0
- package/out/test/auth-bootstrapApply.test.js +27 -6
- package/out/test/auth-broker-hardening.test.js +227 -0
- package/out/test/auth-broker.test.js +112 -9
- package/out/test/blueprint-registry.test.js +105 -0
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +455 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/credential-context.test.js +23 -28
- 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 +311 -0
- package/out/test/deploy-init.test.js +295 -4
- package/out/test/egress-broker.test.js +172 -0
- package/out/test/fleetDisplayUtils.test.js +4 -0
- package/out/test/gateway-client.test.js +28 -0
- package/out/test/gateway-install-source.test.js +111 -0
- package/out/test/gateway-lifecycle.test.js +1 -1
- package/out/test/gateway-outbound.test.js +7 -0
- package/out/test/gateway-secrets-preflight.test.js +127 -0
- package/out/test/gateway-session-routing.test.js +18 -0
- package/out/test/governor-hardening.test.js +217 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/idp-init.test.js +151 -0
- package/out/test/lease-signature.test.js +357 -0
- package/out/test/lease-trust-root.test.js +273 -0
- package/out/test/lease-validation.test.js +169 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/mcp-obo.test.js +52 -27
- package/out/test/mcp.test.js +69 -3
- package/out/test/model-routing.test.js +133 -0
- package/out/test/pack-admission.test.js +216 -0
- package/out/test/pack-digest-parity.test.js +140 -0
- package/out/test/pack-reconcile.test.js +183 -0
- package/out/test/pack-signature-artifact.test.js +103 -0
- package/out/test/pack-signature.test.js +176 -0
- package/out/test/pack-trust-root.test.js +202 -0
- package/out/test/remote-trust.test.js +242 -0
- package/out/test/runner-protocol.test.js +249 -0
- package/out/test/sandbox-driver.test.js +553 -0
- package/out/test/sandbox-hardening.test.js +395 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/serve-gateway-turn.test.js +267 -0
- package/out/test/serve-hardening.test.js +125 -6
- package/out/test/serveClient.test.js +38 -5
- package/out/test/sessionShip.test.js +560 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/state-sync-peer.test.js +18 -5
- package/out/test/store-install-safe.test.js +69 -0
- package/out/test/tls-bypass.test.js +90 -0
- package/out/test/warrant.test.js +138 -0
- package/out/utils.js +73 -20
- package/package.json +1 -1
package/out/cli/deploy-init.js
CHANGED
|
@@ -36,15 +36,22 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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;
|
|
39
|
+
exports.AGENTHIPPO_CLI_NPM_VERSION_FALLBACK = exports.AGENTHIPPO_CLI_NPM_PACKAGE = exports.DEPLOY_PUBLIC_HOSTNAME = exports.DEFAULT_DEPLOY_PUBLIC_HOSTNAME = exports.CLAUDE_CODE_NPM_VERSION = exports.AGENTHIPPO_IMAGE_TAG = exports.AGENTHIPPO_IMAGE_REGISTRY = exports.DEPLOY_TARGETS = void 0;
|
|
40
40
|
exports.isDeployTarget = isDeployTarget;
|
|
41
|
+
exports.agenthippoImage = agenthippoImage;
|
|
41
42
|
exports.defaultRuntimeImage = defaultRuntimeImage;
|
|
43
|
+
exports.packAttributionBody = packAttributionBody;
|
|
44
|
+
exports.packAttributionComment = packAttributionComment;
|
|
45
|
+
exports.withPackAttribution = withPackAttribution;
|
|
42
46
|
exports.serveCmdArgs = serveCmdArgs;
|
|
43
47
|
exports.resolveAgentHippoCliNpmVersion = resolveAgentHippoCliNpmVersion;
|
|
44
48
|
exports.resolveDeployPublicHostname = resolveDeployPublicHostname;
|
|
45
49
|
exports.scanEnvRefs = scanEnvRefs;
|
|
46
50
|
exports.renderEnvExample = renderEnvExample;
|
|
47
51
|
exports.renderAuthEnvExample = renderAuthEnvExample;
|
|
52
|
+
exports.renderAuthBrokerDockerfile = renderAuthBrokerDockerfile;
|
|
53
|
+
exports.isSecretLikeBasename = isSecretLikeBasename;
|
|
54
|
+
exports.dataPlaneTokenEnvVar = dataPlaneTokenEnvVar;
|
|
48
55
|
exports.renderTarget = renderTarget;
|
|
49
56
|
/**
|
|
50
57
|
* `agenthippo deploy init` — generator for platform-native deploy artifacts
|
|
@@ -65,15 +72,87 @@ exports.renderTarget = renderTarget;
|
|
|
65
72
|
const fs = __importStar(require("fs"));
|
|
66
73
|
const path = __importStar(require("path"));
|
|
67
74
|
const serve_hardening_1 = require("./serve-hardening");
|
|
68
|
-
|
|
75
|
+
const identity_1 = require("./deploy/layers/identity");
|
|
76
|
+
exports.DEPLOY_TARGETS = ['vm', 'compose', 'cloudrun', 'databricks-app', 'spcs', 'cloudflare', 'aws-ecs', 'azure-container-apps'];
|
|
69
77
|
function isDeployTarget(value) {
|
|
70
78
|
return exports.DEPLOY_TARGETS.includes(value);
|
|
71
79
|
}
|
|
80
|
+
/** GHCR namespace for every AgentHippo-published image. Canonical — `agenthippoai`,
|
|
81
|
+
* matching what scripts/release/publish-runtime-image.sh actually pushes to.
|
|
82
|
+
* (`ghcr.io/agenthippo/` — no trailing "ai" — is a dead namespace; do not reintroduce it.) */
|
|
83
|
+
exports.AGENTHIPPO_IMAGE_REGISTRY = 'ghcr.io/agenthippoai';
|
|
84
|
+
/**
|
|
85
|
+
* The ONE dated tag every generated artifact pins to.
|
|
86
|
+
*
|
|
87
|
+
* Generated customer artifacts must never say `:latest`: a floating tag silently
|
|
88
|
+
* re-points a customer's deployment at a different image, which is exactly what the
|
|
89
|
+
* dated-tag policy in scripts/release/publish-runtime-image.sh exists to prevent.
|
|
90
|
+
*
|
|
91
|
+
* MUST match the `--version` used by scripts/release/publish-runtime-image.sh for the
|
|
92
|
+
* currently promoted release (scheme: `YYYY.MM` or `YYYY.MM.DD` — see
|
|
93
|
+
* scripts/release/README.md § Versioning). Bump here and in the publish run together.
|
|
94
|
+
*/
|
|
95
|
+
exports.AGENTHIPPO_IMAGE_TAG = '2026.07';
|
|
96
|
+
/**
|
|
97
|
+
* Claude Code CLI version pinned into generated customer artifacts.
|
|
98
|
+
*
|
|
99
|
+
* `"latest"` here meant every customer install resolved a different version, so two
|
|
100
|
+
* deploys of the same pack were not the same software. Keep this equal to the
|
|
101
|
+
* `CLAUDE_CODE_VERSION` build ARG in extensions/agentide/scripts/Dockerfile.runtime,
|
|
102
|
+
* so the source-based (Databricks) path and the container path ship the same engine.
|
|
103
|
+
*/
|
|
104
|
+
exports.CLAUDE_CODE_NPM_VERSION = '2.1.220';
|
|
105
|
+
/** `ghcr.io/agenthippoai/<name>:<dated tag>` — the only way to name a published image. */
|
|
106
|
+
function agenthippoImage(name, variant) {
|
|
107
|
+
return `${exports.AGENTHIPPO_IMAGE_REGISTRY}/${name}:${exports.AGENTHIPPO_IMAGE_TAG}${variant ? `-${variant}` : ''}`;
|
|
108
|
+
}
|
|
72
109
|
/** Default runtime image for a pack engine (cursor needs the /engines layer). */
|
|
73
110
|
function defaultRuntimeImage(engine) {
|
|
74
111
|
return engine.trim().toLowerCase() === 'cursor'
|
|
75
|
-
? '
|
|
76
|
-
: '
|
|
112
|
+
? agenthippoImage('runtime', 'cursor')
|
|
113
|
+
: agenthippoImage('runtime');
|
|
114
|
+
}
|
|
115
|
+
/** Provenance line for generated deploy artifacts (pack-standard WS2). */
|
|
116
|
+
function packAttributionBody(ctx) {
|
|
117
|
+
return `Generated from Agent Pack ${ctx.packName}@${ctx.packVersion} — packs.agenthippo.ai`;
|
|
118
|
+
}
|
|
119
|
+
function packAttributionComment(ctx, kind = 'hash') {
|
|
120
|
+
const body = packAttributionBody(ctx);
|
|
121
|
+
if (kind === 'html') {
|
|
122
|
+
return `<!-- ${body} -->`;
|
|
123
|
+
}
|
|
124
|
+
if (kind === 'sql') {
|
|
125
|
+
return `-- ${body}`;
|
|
126
|
+
}
|
|
127
|
+
return `# ${body}`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Prepend pack provenance to a generated file. Skips JSON/TOML/JSONC (no comment syntax).
|
|
131
|
+
* Keeps shebangs first. Idempotent if packs.agenthippo.ai is already present near the top.
|
|
132
|
+
*/
|
|
133
|
+
function withPackAttribution(ctx, relPath, content) {
|
|
134
|
+
const head = content.slice(0, 400);
|
|
135
|
+
if (head.includes('packs.agenthippo.ai')) {
|
|
136
|
+
return content;
|
|
137
|
+
}
|
|
138
|
+
const base = path.basename(relPath);
|
|
139
|
+
const ext = path.extname(base).toLowerCase();
|
|
140
|
+
// Structured configs: don't invent comment syntax.
|
|
141
|
+
if (ext === '.json' || ext === '.jsonc' || base === 'wrangler.toml' || base === 'package.json') {
|
|
142
|
+
return content;
|
|
143
|
+
}
|
|
144
|
+
const kind = ext === '.html' || ext === '.md' ? 'html' :
|
|
145
|
+
ext === '.sql' ? 'sql' :
|
|
146
|
+
'hash';
|
|
147
|
+
const line = packAttributionComment(ctx, kind);
|
|
148
|
+
if (content.startsWith('#!')) {
|
|
149
|
+
const nl = content.indexOf('\n');
|
|
150
|
+
if (nl === -1) {
|
|
151
|
+
return `${content}\n${line}\n`;
|
|
152
|
+
}
|
|
153
|
+
return `${content.slice(0, nl + 1)}${line}\n${content.slice(nl + 1)}`;
|
|
154
|
+
}
|
|
155
|
+
return `${line}\n${content}`;
|
|
77
156
|
}
|
|
78
157
|
/** `serve` argv baked into generated Dockerfiles (matches agent.yaml engine/model). */
|
|
79
158
|
function serveCmdArgs(ctx, opts) {
|
|
@@ -192,8 +271,11 @@ function renderEnvExample(vars, opts) {
|
|
|
192
271
|
if (isCursor) {
|
|
193
272
|
lines.push('# Cursor engine (pack default model: auto):', 'CURSOR_API_KEY= # https://cursor.com/settings → API keys');
|
|
194
273
|
}
|
|
274
|
+
else if (opts?.gatewayDirect) {
|
|
275
|
+
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');
|
|
276
|
+
}
|
|
195
277
|
else {
|
|
196
|
-
lines.push('# Model credential — pick ONE path (serve-v3 §10.2):', '#
|
|
278
|
+
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
279
|
}
|
|
198
280
|
if (vars.length > 0) {
|
|
199
281
|
lines.push('', '# Pack-referenced secrets (scanned from ${env:...}):');
|
|
@@ -234,19 +316,21 @@ function renderAuthEnvExample(opts) {
|
|
|
234
316
|
`AGENTHIPPO_AUTH_IDP_URL=${idp} # where browsers are sent to log in`,
|
|
235
317
|
'AGENTHIPPO_AUTH_COOKIE_NAME=ah_token',
|
|
236
318
|
'',
|
|
237
|
-
'# Team bootstrap (store +
|
|
319
|
+
'# Team bootstrap (store + gateway routing) — same as the IdP if you want parity.',
|
|
238
320
|
'AGENTHIPPO_TEAM_ID=team_default',
|
|
239
321
|
'AGENTHIPPO_TEAM_NAME=Default Team',
|
|
240
322
|
'AGENTHIPPO_TEAM_STORE_REPO=agenthippoai/agenthippo-store',
|
|
241
323
|
'AGENTHIPPO_TEAM_STORE_INDEX_URL=https://agenthippoai.github.io/agenthippo-store',
|
|
242
324
|
'AGENTHIPPO_TEAM_GITHUB_ORG=agenthippoai',
|
|
243
|
-
'
|
|
244
|
-
'
|
|
325
|
+
'# An agenthippo-gateway deployment (JWT lane) — NOT bare LiteLLM. No key: the account',
|
|
326
|
+
'# JWT is the routing credential.',
|
|
327
|
+
'AGENTHIPPO_TEAM_GATEWAY_URL=https://gateway.agenthippo.ai/v1',
|
|
245
328
|
'',
|
|
246
329
|
'# No OAUTH2_PROXY_* here — this agent does not talk to Google. The one oauth2-proxy +',
|
|
247
330
|
'# one Google OAuth client live only at the IdP.',
|
|
248
331
|
].join('\n') + '\n';
|
|
249
332
|
}
|
|
333
|
+
const cookieDomain = opts?.cookieDomain || '.agenthippo.ai';
|
|
250
334
|
return [
|
|
251
335
|
'# Generated by `agenthippo deploy init`. Copy to .env.auth and fill in.',
|
|
252
336
|
'# Loaded only by auth-broker + oauth2-proxy (docker-compose.yml). Never given to the agent.',
|
|
@@ -261,8 +345,8 @@ function renderAuthEnvExample(opts) {
|
|
|
261
345
|
'OAUTH2_PROXY_EMAIL_DOMAINS=example.com',
|
|
262
346
|
'OAUTH2_PROXY_REVERSE_PROXY=true',
|
|
263
347
|
`OAUTH2_PROXY_REDIRECT_URL=https://${hostname}/oauth2/callback`,
|
|
264
|
-
'# Federated SSO (A′) — set when this edge is the central IdP for
|
|
265
|
-
|
|
348
|
+
'# Federated SSO (A′) — set when this edge is the central IdP for a fleet of agents:',
|
|
349
|
+
`OAUTH2_PROXY_COOKIE_DOMAINS=${cookieDomain} # scope the oauth2-proxy session cookie to all subdomains`,
|
|
266
350
|
'',
|
|
267
351
|
'# IDE auth broker (team bootstrap for store + LiteLLM routing)',
|
|
268
352
|
'AGENTHIPPO_AUTH_JWT_SECRET= # openssl rand -hex 32',
|
|
@@ -270,14 +354,15 @@ function renderAuthEnvExample(opts) {
|
|
|
270
354
|
'AGENTHIPPO_AUTH_ALLOWED_DOMAINS=example.com',
|
|
271
355
|
'AGENTHIPPO_AUTH_DEV=0 # 1 enables /oauth/dev/login (tests only; never in production)',
|
|
272
356
|
'AGENTHIPPO_AUTH_COOKIE_NAME=ah_token # federated cookie holding the AgentHippo JWT',
|
|
273
|
-
|
|
357
|
+
`AGENTHIPPO_AUTH_COOKIE_DOMAIN=${cookieDomain} # set so /oauth/browser-login stamps a cross-subdomain cookie`,
|
|
274
358
|
'AGENTHIPPO_TEAM_ID=team_default',
|
|
275
359
|
'AGENTHIPPO_TEAM_NAME=Default Team',
|
|
276
360
|
'AGENTHIPPO_TEAM_STORE_REPO=agenthippoai/agenthippo-store',
|
|
277
361
|
'AGENTHIPPO_TEAM_STORE_INDEX_URL=https://agenthippoai.github.io/agenthippo-store',
|
|
278
362
|
'AGENTHIPPO_TEAM_GITHUB_ORG=agenthippoai',
|
|
279
|
-
'
|
|
280
|
-
'
|
|
363
|
+
'# An agenthippo-gateway deployment (JWT lane) — NOT bare LiteLLM. No key: the account',
|
|
364
|
+
'# JWT is the routing credential.',
|
|
365
|
+
'AGENTHIPPO_TEAM_GATEWAY_URL=https://gateway.agenthippo.ai/v1',
|
|
281
366
|
].join('\n') + '\n';
|
|
282
367
|
}
|
|
283
368
|
/** Auth broker sidecar for IDE sign-in + team bootstrap API. */
|
|
@@ -285,7 +370,7 @@ function renderAuthBrokerDockerfile() {
|
|
|
285
370
|
return [
|
|
286
371
|
'# Auth broker for IDE sign-in + team bootstrap API.',
|
|
287
372
|
'# Build context = pack root: docker build -f deploy/Dockerfile.auth-broker -t <pack>-auth-broker:local .',
|
|
288
|
-
|
|
373
|
+
`FROM ${agenthippoImage('runtime')}`,
|
|
289
374
|
'ENV AGENTHIPPO_AUTH_PORT=9780',
|
|
290
375
|
'ENV AGENTHIPPO_AUTH_DATA_DIR=/data',
|
|
291
376
|
'EXPOSE 9780',
|
|
@@ -294,6 +379,18 @@ function renderAuthBrokerDockerfile() {
|
|
|
294
379
|
'',
|
|
295
380
|
].join('\n');
|
|
296
381
|
}
|
|
382
|
+
/** Secret-like basenames beyond `.env` and `*credentials*.json` — private keys, keystores, and
|
|
383
|
+
* credential files of any extension. Shared by renderDockerignore() below and copyPackToStaging()
|
|
384
|
+
* in commands/deploy.ts so the staging dir on disk and the generated .dockerignore never diverge. */
|
|
385
|
+
function isSecretLikeBasename(base) {
|
|
386
|
+
if (/credentials/i.test(base))
|
|
387
|
+
return true;
|
|
388
|
+
if (/\.(pem|key|p12|pfx|jks|gpg|pgp)$/i.test(base))
|
|
389
|
+
return true;
|
|
390
|
+
if (/^id_(rsa|dsa|ecdsa|ed25519)$/i.test(base))
|
|
391
|
+
return true;
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
297
394
|
/** .dockerignore emitted alongside every image-building target's Dockerfile.
|
|
298
395
|
* Keeps skills/, references/, scripts/ (required at runtime) and drops only VCS,
|
|
299
396
|
* cache, and build cruft. node_modules is excluded so the linux/amd64 image
|
|
@@ -326,8 +423,27 @@ function renderDockerignore() {
|
|
|
326
423
|
'.vscode',
|
|
327
424
|
'*.swp',
|
|
328
425
|
'# Secrets anywhere in the pack (defensive; deploy/ secrets are also covered below).',
|
|
426
|
+
'# Keep in sync with isSecretLikeBasename() above.',
|
|
329
427
|
'**/.env',
|
|
330
|
-
|
|
428
|
+
// `**/.env` alone does NOT match `.env.auth` / `.env.local` / `.env.production`.
|
|
429
|
+
// `.env.auth` carries AGENTHIPPO_AUTH_JWT_SECRET and OAUTH2_PROXY_* — baking it
|
|
430
|
+
// into a published layer leaks fleet-wide token-forgery material.
|
|
431
|
+
'**/.env.*',
|
|
432
|
+
'**/*credentials*',
|
|
433
|
+
'**/*.pem',
|
|
434
|
+
'**/*.key',
|
|
435
|
+
'**/*.p12',
|
|
436
|
+
'**/*.pfx',
|
|
437
|
+
'**/*.jks',
|
|
438
|
+
'**/*.gpg',
|
|
439
|
+
'**/*.pgp',
|
|
440
|
+
'**/id_rsa',
|
|
441
|
+
'**/id_dsa',
|
|
442
|
+
'**/id_ecdsa',
|
|
443
|
+
'**/id_ed25519',
|
|
444
|
+
'# …but *.example templates are safe and are what the README tells customers to copy.',
|
|
445
|
+
'!**/.env.example',
|
|
446
|
+
'!**/.env.*.example',
|
|
331
447
|
'# deploy/ holds build/deploy artifacts + secrets; only deploy/www/ ships at runtime.',
|
|
332
448
|
'deploy/*',
|
|
333
449
|
'!deploy/www',
|
|
@@ -364,17 +480,23 @@ function skillDepsInstallLines() {
|
|
|
364
480
|
function agentServeHostEnvYaml(indent = ' ') {
|
|
365
481
|
return `${indent}- { name: ${serve_hardening_1.SERVE_HOST_ENV}, value: ${serve_hardening_1.CONTAINER_SERVE_HOST} }`;
|
|
366
482
|
}
|
|
367
|
-
/** The Dockerfile shared by image-based targets (serve-v3 §A2).
|
|
483
|
+
/** The Dockerfile shared by image-based targets (serve-v3 §A2).
|
|
484
|
+
* `extraEnv`: platform-specific ENV lines — e.g. pinning AGENTHIPPO_IDENTITY_HEADER for a
|
|
485
|
+
* target whose container may be directly reachable (governance: identity-header pinning). */
|
|
368
486
|
function sharedDockerfile(ctx, opts) {
|
|
369
487
|
const cmd = serveCmdArgs(ctx, opts).map((a) => `"${a}"`).join(',');
|
|
370
488
|
return [
|
|
371
|
-
|
|
489
|
+
`${packAttributionComment(ctx)}. Build context = pack root:`,
|
|
372
490
|
`# docker build -f deploy/Dockerfile -t ${ctx.packName}:local .`,
|
|
373
491
|
`FROM ${ctx.image}`,
|
|
492
|
+
`LABEL ai.agenthippo.pack.name="${ctx.packName}"`,
|
|
493
|
+
`LABEL ai.agenthippo.pack.version="${ctx.packVersion}"`,
|
|
494
|
+
`LABEL ai.agenthippo.pack.engine="${ctx.engine}"`,
|
|
374
495
|
`COPY . /app/pack/`,
|
|
375
496
|
...skillDepsInstallLines(),
|
|
376
497
|
`ENV AGENTHIPPO_NO_AUTO_INSTALL=1`,
|
|
377
498
|
`ENV ${serve_hardening_1.SERVE_HOST_ENV}=${serve_hardening_1.CONTAINER_SERVE_HOST}`,
|
|
499
|
+
...Object.entries(opts?.extraEnv ?? {}).map(([k, v]) => `ENV ${k}=${v}`),
|
|
378
500
|
`EXPOSE ${ctx.port}`,
|
|
379
501
|
// Runtime base image sets ENTRYPOINT ["agenthippo"]; CMD args append to it.
|
|
380
502
|
`CMD [${cmd}]`,
|
|
@@ -469,111 +591,47 @@ function renderStickyAgentProxy(port, indent = ' ') {
|
|
|
469
591
|
}
|
|
470
592
|
/** Caddy config for the `edge` compose profile (oauth2-proxy + agent on Docker network). */
|
|
471
593
|
function renderCaddyfile(ctx) {
|
|
594
|
+
const fed = ctx.ingress === 'edge-fed';
|
|
595
|
+
const idp = (ctx.idpUrl || '').trim().replace(/\/+$/, '');
|
|
596
|
+
const catchallBody = [
|
|
597
|
+
' @api path /v1/*',
|
|
598
|
+
' handle @api {',
|
|
599
|
+
...renderStickyAgentProxy(ctx.port, ' '),
|
|
600
|
+
' }',
|
|
601
|
+
' root * /srv/www',
|
|
602
|
+
' file_server',
|
|
603
|
+
];
|
|
472
604
|
return [
|
|
473
605
|
'# SSO edge + Cloudflare Tunnel. Set AGENTHIPPO_PUBLIC_HOSTNAME in .env (Caddy reads it below).',
|
|
474
606
|
'# TLS terminates at Cloudflare; origin listens on HTTP only.',
|
|
607
|
+
...(fed
|
|
608
|
+
? [
|
|
609
|
+
`# No oauth2-proxy — Google login happens once at the central IdP (${idp || '<IDP_URL>'});`,
|
|
610
|
+
'# this agent only verifies the shared AgentHippo JWT (Bearer for IDE, cookie for browser).',
|
|
611
|
+
]
|
|
612
|
+
: []),
|
|
475
613
|
'{',
|
|
476
614
|
' auto_https off',
|
|
477
615
|
'}',
|
|
478
616
|
'',
|
|
479
617
|
'http://{$AGENTHIPPO_PUBLIC_HOSTNAME} {',
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
' }',
|
|
498
|
-
' # Federated SSO (A′): browsers from *.agenthippo.ai subdomains land here to log in.',
|
|
499
|
-
' # Gated behind oauth2-proxy (Google) → auth-broker mints + sets the shared .agenthippo.ai',
|
|
500
|
-
' # cookie (ah_token), then redirects back to the agent subdomain (`return_to`). Must come',
|
|
501
|
-
' # before the generic /oauth/* handle so it wins for this exact path.',
|
|
502
|
-
' handle /oauth/browser-login {',
|
|
503
|
-
' forward_auth oauth2-proxy:4180 {',
|
|
504
|
-
' uri /oauth2/auth',
|
|
505
|
-
' copy_headers X-Auth-Request-Email',
|
|
506
|
-
' header_up X-Forwarded-Proto https',
|
|
507
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
508
|
-
' @unauth status 401',
|
|
509
|
-
' handle_response @unauth {',
|
|
510
|
-
' redir * "/oauth2/sign_in?rd=/oauth/browser-login{http.request.uri.query}"',
|
|
511
|
-
' }',
|
|
512
|
-
' }',
|
|
513
|
-
' reverse_proxy auth-broker:9780 {',
|
|
514
|
-
' header_up X-Forwarded-Proto https',
|
|
515
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
516
|
-
' }',
|
|
517
|
-
' }',
|
|
518
|
-
' handle /oauth/* {',
|
|
519
|
-
' reverse_proxy auth-broker:9780 {',
|
|
520
|
-
' header_up X-Forwarded-Proto https',
|
|
521
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
522
|
-
' }',
|
|
523
|
-
' }',
|
|
524
|
-
' handle /api/ide/* {',
|
|
525
|
-
' reverse_proxy auth-broker:9780 {',
|
|
526
|
-
' header_up X-Forwarded-Proto https',
|
|
527
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
528
|
-
' }',
|
|
529
|
-
' }',
|
|
530
|
-
' handle /oauth2/* {',
|
|
531
|
-
' reverse_proxy oauth2-proxy:4180 {',
|
|
532
|
-
' header_up X-Forwarded-Proto https',
|
|
533
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
534
|
-
' }',
|
|
535
|
-
' }',
|
|
536
|
-
' @health path /health /ready',
|
|
537
|
-
' handle @health {',
|
|
538
|
-
...renderStickyAgentProxy(ctx.port, ' '),
|
|
539
|
-
' }',
|
|
540
|
-
' # API clients (IDE) with a Bearer token: validate via auth-broker, copy identity header.',
|
|
541
|
-
' @apiWithAuth {',
|
|
542
|
-
' path /v1/*',
|
|
543
|
-
' header Authorization *',
|
|
544
|
-
' }',
|
|
545
|
-
' handle @apiWithAuth {',
|
|
546
|
-
' forward_auth auth-broker:9780 {',
|
|
547
|
-
' uri /api/ide/me',
|
|
548
|
-
' copy_headers X-Agent-User',
|
|
549
|
-
' header_up X-Forwarded-Proto https',
|
|
550
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
551
|
-
' @unauth status 401',
|
|
552
|
-
' handle_response @unauth {',
|
|
553
|
-
' respond "Unauthorized" 401',
|
|
554
|
-
' }',
|
|
555
|
-
' }',
|
|
556
|
-
...renderStickyAgentProxy(ctx.port, ' '),
|
|
557
|
-
' }',
|
|
558
|
-
' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User.',
|
|
559
|
-
' handle {',
|
|
560
|
-
' forward_auth oauth2-proxy:4180 {',
|
|
561
|
-
' uri /oauth2/auth',
|
|
562
|
-
' copy_headers X-Auth-Request-Email>X-Agent-User',
|
|
563
|
-
' header_up X-Forwarded-Proto https',
|
|
564
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
565
|
-
' @unauth status 401',
|
|
566
|
-
' handle_response @unauth {',
|
|
567
|
-
' redir * "/oauth2/sign_in?rd={http.request.uri.path}{http.request.uri.query}"',
|
|
568
|
-
' }',
|
|
569
|
-
' }',
|
|
570
|
-
' @api path /v1/*',
|
|
571
|
-
' handle @api {',
|
|
572
|
-
...renderStickyAgentProxy(ctx.port, ' '),
|
|
573
|
-
' }',
|
|
574
|
-
' root * /srv/www',
|
|
575
|
-
' file_server',
|
|
576
|
-
' }',
|
|
618
|
+
...(fed
|
|
619
|
+
? (0, identity_1.renderIdentityLayer)({
|
|
620
|
+
mode: 'federated',
|
|
621
|
+
authBrokerAddr: 'auth-broker:9780',
|
|
622
|
+
idpUrl: idp,
|
|
623
|
+
healthProxy: renderStickyAgentProxy(ctx.port, ' '),
|
|
624
|
+
agentProxy: renderStickyAgentProxy(ctx.port, ' '),
|
|
625
|
+
catchallBody,
|
|
626
|
+
})
|
|
627
|
+
: (0, identity_1.renderIdentityLayer)({
|
|
628
|
+
mode: 'self-issued',
|
|
629
|
+
oauth2ProxyAddr: 'oauth2-proxy:4180',
|
|
630
|
+
authBrokerAddr: 'auth-broker:9780',
|
|
631
|
+
healthProxy: renderStickyAgentProxy(ctx.port, ' '),
|
|
632
|
+
agentProxy: renderStickyAgentProxy(ctx.port, ' '),
|
|
633
|
+
catchallBody,
|
|
634
|
+
})),
|
|
577
635
|
'}',
|
|
578
636
|
'',
|
|
579
637
|
].join('\n');
|
|
@@ -614,9 +672,10 @@ function renderAgentComposeService(ctx, extraLines = []) {
|
|
|
614
672
|
'',
|
|
615
673
|
];
|
|
616
674
|
}
|
|
617
|
-
/** oauth2-proxy + Caddy services for `--profile edge`.
|
|
618
|
-
|
|
619
|
-
|
|
675
|
+
/** oauth2-proxy (self-issued only) + auth-broker + Caddy services for `--profile edge`.
|
|
676
|
+
* `fed`: federated (verify-only against a central IdP) — no oauth2-proxy, no local Google client. */
|
|
677
|
+
function renderEdgeComposeServices(fed) {
|
|
678
|
+
const oauth2Proxy = fed ? [] : [
|
|
620
679
|
' oauth2-proxy:',
|
|
621
680
|
' profiles: ["edge"]',
|
|
622
681
|
' image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1',
|
|
@@ -634,6 +693,9 @@ function renderEdgeComposeServices() {
|
|
|
634
693
|
' OAUTH2_PROXY_REVERSE_PROXY: "true"',
|
|
635
694
|
' expose: ["4180"]',
|
|
636
695
|
'',
|
|
696
|
+
];
|
|
697
|
+
return [
|
|
698
|
+
...oauth2Proxy,
|
|
637
699
|
' auth-broker:',
|
|
638
700
|
' profiles: ["edge"]',
|
|
639
701
|
' build:',
|
|
@@ -659,7 +721,7 @@ function renderEdgeComposeServices() {
|
|
|
659
721
|
' required: false',
|
|
660
722
|
' ports: ["80:80", "443:443"] # use 80:80 not 127.0.0.1:80 — breaks vmnetd on some Docker Desktop builds',
|
|
661
723
|
' volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "./www:/srv/www:ro"]',
|
|
662
|
-
|
|
724
|
+
` depends_on: [${fed ? 'agent, auth-broker' : 'oauth2-proxy, agent, auth-broker'}]`,
|
|
663
725
|
'',
|
|
664
726
|
];
|
|
665
727
|
}
|
|
@@ -683,7 +745,7 @@ function renderTunnelComposeServices() {
|
|
|
683
745
|
function renderEdgeTunnelUpScript(hostname) {
|
|
684
746
|
return [
|
|
685
747
|
'#!/usr/bin/env bash',
|
|
686
|
-
'# T0.5b — Cloudflare Tunnel + Google SSO (no Access seats). Run
|
|
748
|
+
'# T0.5b — Cloudflare Tunnel + Google SSO (no Access seats). Run: bash deploy/run-edge-tunnel.sh',
|
|
687
749
|
'set -euo pipefail',
|
|
688
750
|
'cd "$(dirname "$0")"',
|
|
689
751
|
'docker compose --profile edge --profile tunnel up -d --build',
|
|
@@ -691,7 +753,133 @@ function renderEdgeTunnelUpScript(hostname) {
|
|
|
691
753
|
'',
|
|
692
754
|
].join('\n');
|
|
693
755
|
}
|
|
756
|
+
/**
|
|
757
|
+
* Same-host colocation (PLAN/deploy.md §4 state 4, `d5-colocate`): this agent rides the org
|
|
758
|
+
* IdP's already-running Caddy + auth-broker instead of shipping its own — no Caddy, no
|
|
759
|
+
* oauth2-proxy, no local auth-broker, no local auth secrets at all. Ships only the agent
|
|
760
|
+
* container (joining the IdP's `agenthippo-idp-colocate` docker network via a fixed,
|
|
761
|
+
* predictable container name) plus a Caddy route snippet for the operator to drop into the
|
|
762
|
+
* IdP's `agents/` import directory (see enterprise-deploy/idp/Caddyfile + agents/README.md).
|
|
763
|
+
*/
|
|
764
|
+
/** Env var (in the IdP/edge stack's env) holding the connector sidecar's register token —
|
|
765
|
+
* matches the connector's own AGENTHIPPO_DATA_PLANE_TOKEN. Convention proven by the
|
|
766
|
+
* dynamodb-analyst overlay: pack `dynamodb-analyst` → `DYNAMODB_ANALYST_DATA_PLANE_TOKEN`. */
|
|
767
|
+
function dataPlaneTokenEnvVar(packName) {
|
|
768
|
+
return packName.toUpperCase().replace(/[^A-Z0-9]/g, '_') + '_DATA_PLANE_TOKEN';
|
|
769
|
+
}
|
|
770
|
+
function renderColocatedArtifacts(ctx) {
|
|
771
|
+
const idp = (ctx.idpUrl || '').trim().replace(/\/+$/, '');
|
|
772
|
+
const containerName = `${ctx.packName}-agent`;
|
|
773
|
+
const turnTokenEnvVar = dataPlaneTokenEnvVar(ctx.packName);
|
|
774
|
+
const compose = [
|
|
775
|
+
'# Colocated agent (PLAN/deploy.md §4 state 4) — no Caddy/oauth2-proxy/auth-broker of its',
|
|
776
|
+
'# own. Joins the org IdP\'s docker network so its already-running Caddy can reach this',
|
|
777
|
+
'# agent by name; identity is verified entirely by the IdP\'s auth-broker (see',
|
|
778
|
+
'# deploy/agent-route.caddy for the route to copy onto the IdP host).',
|
|
779
|
+
`name: ${ctx.packName}`,
|
|
780
|
+
'services:',
|
|
781
|
+
' agent:',
|
|
782
|
+
` container_name: ${containerName}`,
|
|
783
|
+
' build:',
|
|
784
|
+
' context: ..',
|
|
785
|
+
' dockerfile: deploy/Dockerfile',
|
|
786
|
+
' env_file:',
|
|
787
|
+
' - path: .env',
|
|
788
|
+
' required: false',
|
|
789
|
+
' volumes: ["agenthippo-state:/state"]',
|
|
790
|
+
' networks: [colocate]',
|
|
791
|
+
` expose: ["${ctx.port}"]`,
|
|
792
|
+
'',
|
|
793
|
+
'networks:',
|
|
794
|
+
' colocate:',
|
|
795
|
+
' external: true',
|
|
796
|
+
' name: ${AGENTHIPPO_IDP_NETWORK:-agenthippo-idp-colocate}',
|
|
797
|
+
'',
|
|
798
|
+
'volumes:',
|
|
799
|
+
' agenthippo-state:',
|
|
800
|
+
'',
|
|
801
|
+
].join('\n');
|
|
802
|
+
const snippet = [
|
|
803
|
+
`# Colocated route for ${ctx.publicHostname} — copy into enterprise-deploy/idp/agents/`,
|
|
804
|
+
'# (rename e.g. to ' + ctx.packName + '.caddy), then on the IdP host:',
|
|
805
|
+
'# docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile',
|
|
806
|
+
'# Requires this agent\'s docker-compose.yml (already generated) to be up, so',
|
|
807
|
+
`# ${containerName} exists on the shared \`agenthippo-idp-colocate\` network.`,
|
|
808
|
+
'# http:// (not https://) to match the IdP Caddyfile\'s TLS model: auto_https is off',
|
|
809
|
+
'# there and TLS terminates in front (Cloudflare Tunnel/nginx), so every site block —',
|
|
810
|
+
'# including this imported one — must be plain HTTP, same as the IdP\'s own site address.',
|
|
811
|
+
`http://${ctx.publicHostname} {`,
|
|
812
|
+
...(0, identity_1.renderIdentityLayer)({
|
|
813
|
+
mode: 'federated',
|
|
814
|
+
authBrokerAddr: 'auth-broker:9780',
|
|
815
|
+
idpUrl: idp,
|
|
816
|
+
healthProxy: [` reverse_proxy ${containerName}:${ctx.port}`],
|
|
817
|
+
agentProxy: [` reverse_proxy ${containerName}:${ctx.port}`],
|
|
818
|
+
...(ctx.edgeTurnConnector
|
|
819
|
+
? { turnRegistration: { connectorAddr: ctx.edgeTurnConnector, tokenEnvVar: turnTokenEnvVar } }
|
|
820
|
+
: {}),
|
|
821
|
+
}),
|
|
822
|
+
'}',
|
|
823
|
+
'',
|
|
824
|
+
].join('\n');
|
|
825
|
+
return {
|
|
826
|
+
'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
|
|
827
|
+
'docker-compose.yml': compose,
|
|
828
|
+
'agent-route.caddy': snippet,
|
|
829
|
+
'www/index.html': renderLandingPage(ctx.packName),
|
|
830
|
+
'README.md': [
|
|
831
|
+
`# Deploy: ${ctx.packName} — colocated with the org IdP`,
|
|
832
|
+
'',
|
|
833
|
+
'No Caddy, no oauth2-proxy, no local auth-broker, no auth secrets on this host — this',
|
|
834
|
+
'agent rides the org IdP\'s already-running edge (`enterprise-deploy/idp/`) instead.',
|
|
835
|
+
'',
|
|
836
|
+
'```bash',
|
|
837
|
+
'cp deploy/.env.example deploy/.env # model key only — no auth secrets here at all',
|
|
838
|
+
'# Point at the IdP\'s colocate network if it was created under a different compose',
|
|
839
|
+
'# project name: export AGENTHIPPO_IDP_NETWORK=agenthippo-idp-colocate (the default)',
|
|
840
|
+
'docker compose -f deploy/docker-compose.yml up -d --build',
|
|
841
|
+
'',
|
|
842
|
+
'# Wire the route into the IdP\'s Caddy (one-time per agent):',
|
|
843
|
+
`cp deploy/agent-route.caddy <idp-host>:enterprise-deploy/idp/agents/${ctx.packName}.caddy`,
|
|
844
|
+
...(ctx.edgeTurnConnector
|
|
845
|
+
? [
|
|
846
|
+
'',
|
|
847
|
+
'# EDGE turn registration (P1 fix): the route registers each chat turn with the connector',
|
|
848
|
+
`# sidecar (${ctx.edgeTurnConnector}). Set the register token in the IdP stack's env —`,
|
|
849
|
+
`# same value as the connector's AGENTHIPPO_DATA_PLANE_TOKEN — before reloading:`,
|
|
850
|
+
`# echo "${turnTokenEnvVar}=<token>" >> enterprise-deploy/idp/.env`,
|
|
851
|
+
'# The agent container never holds this token (that is the point).',
|
|
852
|
+
]
|
|
853
|
+
: []),
|
|
854
|
+
'docker compose -f enterprise-deploy/idp/docker-compose.yml exec caddy \\',
|
|
855
|
+
' caddy reload --config /etc/caddy/Caddyfile',
|
|
856
|
+
'```',
|
|
857
|
+
'',
|
|
858
|
+
`DNS: point \`${ctx.publicHostname}\` at the IdP host (same as every other agent it fronts).`,
|
|
859
|
+
'',
|
|
860
|
+
'**hippopennyg / bare-metal (Cloudflare → nginx:443 → IdP Caddy :8080):** `deploy init` does',
|
|
861
|
+
'**not** create DNS or nginx for you. Without an explicit Cloudflare **A record**, the name',
|
|
862
|
+
'falls through to the `*.agenthippo.ai` wildcard (`ghs.googlehosted.com` from',
|
|
863
|
+
'`setup-agenthippo-domain.sh` for Cloud Run) — the site will look "broken" or hit Google.',
|
|
864
|
+
'After the steps above, run:',
|
|
865
|
+
'',
|
|
866
|
+
'```bash',
|
|
867
|
+
'./enterprise-deploy/scripts/setup-colocated-agent-edge.sh \\',
|
|
868
|
+
` --hostname ${ctx.publicHostname} --ssh root@168.119.174.239`,
|
|
869
|
+
'```',
|
|
870
|
+
'',
|
|
871
|
+
'See `enterprise-deploy/idp/agents/README.md` for the full checklist.',
|
|
872
|
+
'**Browser:** after Google SSO, open `https://' + ctx.publicHostname + '/` for the static chat UI',
|
|
873
|
+
'(`deploy/www/index.html` served by the agent container at GET /).',
|
|
874
|
+
'',
|
|
875
|
+
...renderEgressNote(ctx),
|
|
876
|
+
].join('\n'),
|
|
877
|
+
};
|
|
878
|
+
}
|
|
694
879
|
function vmArtifacts(ctx) {
|
|
880
|
+
if (ctx.ingress === 'edge-fed' && ctx.colocate) {
|
|
881
|
+
return renderColocatedArtifacts(ctx);
|
|
882
|
+
}
|
|
695
883
|
const hostname = ctx.publicHostname;
|
|
696
884
|
const run = [
|
|
697
885
|
'#!/usr/bin/env bash',
|
|
@@ -752,7 +940,7 @@ function vmArtifacts(ctx) {
|
|
|
752
940
|
'# edge only: docker compose --profile edge up -d (SSO on :443, no tunnel)',
|
|
753
941
|
'services:',
|
|
754
942
|
...renderAgentComposeService(ctx),
|
|
755
|
-
...renderEdgeComposeServices(),
|
|
943
|
+
...renderEdgeComposeServices(ctx.ingress === 'edge-fed'),
|
|
756
944
|
...renderTunnelComposeServices(),
|
|
757
945
|
'volumes:',
|
|
758
946
|
' agenthippo-state:',
|
|
@@ -773,6 +961,10 @@ function vmArtifacts(ctx) {
|
|
|
773
961
|
'README.md': [
|
|
774
962
|
`# Deploy: ${ctx.packName} — T0.5 workbench/VM`,
|
|
775
963
|
'',
|
|
964
|
+
'Staging root = this directory\'s parent (full pack copy). Commands below run from the staging root unless noted.',
|
|
965
|
+
'',
|
|
966
|
+
'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`.',
|
|
967
|
+
'',
|
|
776
968
|
'`cp deploy/.env.example deploy/.env` and fill in, then pick a path:',
|
|
777
969
|
'',
|
|
778
970
|
'**T0.5a — VPN only (simplest internal pilot)**',
|
|
@@ -783,18 +975,16 @@ function vmArtifacts(ctx) {
|
|
|
783
975
|
'',
|
|
784
976
|
'**T0.5b — Cloudflare Tunnel + Google SSO (recommended for WFH, no Access seats)**',
|
|
785
977
|
'```bash',
|
|
786
|
-
'agenthippo deploy init . --target vm --force # from pack root; review deploy/',
|
|
787
978
|
'cp deploy/.env.example deploy/.env # agent: model key + public hostname',
|
|
788
979
|
'cp deploy/.env.auth.example deploy/.env.auth # broker/oauth2-proxy: JWT + OAuth secrets (never loaded by the agent)',
|
|
789
980
|
'# 1. Google Cloud → OAuth Web client for https://' + hostname,
|
|
790
981
|
'# Authorized origin + redirect: https://' + hostname + '/oauth2/callback',
|
|
791
982
|
'# 2. cloudflared tunnel create <name>',
|
|
792
|
-
'# cp ~/.cloudflared/<tunnel-id>.json
|
|
983
|
+
'# cp ~/.cloudflared/<tunnel-id>.json ~/.agent-hippo/tunnel-credentials.json # secret',
|
|
793
984
|
'# set tunnel: <tunnel-id> in deploy/cloudflared.yml',
|
|
794
985
|
'# cloudflared tunnel route dns <name> ' + hostname,
|
|
795
986
|
'# 3. Fill deploy/.env (hostname, model key) and deploy/.env.auth (OAUTH2_PROXY_*, AGENTHIPPO_AUTH_JWT_SECRET)',
|
|
796
|
-
'
|
|
797
|
-
'# or from pack root: bash deploy/run-edge-tunnel.sh',
|
|
987
|
+
'bash deploy/run-edge-tunnel.sh',
|
|
798
988
|
'# Users open https://' + hostname + ' → Google login → static chat UI → /v1/chat',
|
|
799
989
|
'```',
|
|
800
990
|
'Tunnel routing is **config-file mode** (`deploy/cloudflared.yml` → `http://caddy:80` on the compose network), not the Cloudflare dashboard public-hostname UI alone.',
|
|
@@ -807,6 +997,7 @@ function vmArtifacts(ctx) {
|
|
|
807
997
|
'',
|
|
808
998
|
'**Do not commit** `deploy/.env`, `deploy/.env.auth`, or `deploy/tunnel-credentials.json`.',
|
|
809
999
|
'',
|
|
1000
|
+
...renderEgressNote(ctx),
|
|
810
1001
|
].join('\n'),
|
|
811
1002
|
};
|
|
812
1003
|
}
|
|
@@ -836,6 +1027,9 @@ const OTEL_COLLECTOR_COMPOSE_YAML = [
|
|
|
836
1027
|
'',
|
|
837
1028
|
].join('\n');
|
|
838
1029
|
function composeArtifacts(ctx) {
|
|
1030
|
+
if (ctx.ingress === 'edge-fed' && ctx.colocate) {
|
|
1031
|
+
return renderColocatedArtifacts(ctx);
|
|
1032
|
+
}
|
|
839
1033
|
const channels = ctx.gatewayChannels?.trim();
|
|
840
1034
|
const hostname = ctx.publicHostname;
|
|
841
1035
|
// Agent Anywhere: override the agent command to also run the gateway bridge,
|
|
@@ -857,7 +1051,7 @@ function composeArtifacts(ctx) {
|
|
|
857
1051
|
' # OPENCLAW_GATEWAY_TOKEN (or a paired device) to connect. Set it in .env.',
|
|
858
1052
|
' openclaw-gateway:',
|
|
859
1053
|
' profiles: ["channels"]',
|
|
860
|
-
|
|
1054
|
+
` image: ${agenthippoImage('openclaw-gateway')}`,
|
|
861
1055
|
' env_file:',
|
|
862
1056
|
' - path: .env',
|
|
863
1057
|
' required: false',
|
|
@@ -882,19 +1076,24 @@ function composeArtifacts(ctx) {
|
|
|
882
1076
|
'# e.g.: docker compose -f deploy/docker-compose.yml --profile edge --profile tunnel up -d',
|
|
883
1077
|
'services:',
|
|
884
1078
|
...renderAgentComposeService(ctx, agentExtra),
|
|
885
|
-
...renderEdgeComposeServices(),
|
|
1079
|
+
...renderEdgeComposeServices(ctx.ingress === 'edge-fed'),
|
|
886
1080
|
...renderTunnelComposeServices(),
|
|
887
1081
|
' # LiteLLM gateway — self-contained image bakes the Spotlight config + OTEL hooks',
|
|
888
1082
|
' # (build: docker build -f extensions/agentide/scripts/Dockerfile.litellm -t this-tag .)',
|
|
889
1083
|
' litellm:',
|
|
890
1084
|
' profiles: ["gateway"]',
|
|
891
|
-
|
|
1085
|
+
` image: ${agenthippoImage('litellm-gateway')}`,
|
|
892
1086
|
' env_file:',
|
|
893
1087
|
' - path: .env',
|
|
894
1088
|
' required: false',
|
|
895
1089
|
' environment:',
|
|
896
1090
|
' OTEL_EXPORTER: otlp_http',
|
|
897
1091
|
' OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318',
|
|
1092
|
+
' # The image defaults to --host 127.0.0.1 (safe when someone runs it bare).',
|
|
1093
|
+
' # Here it must answer the agent container over the compose network, so bind',
|
|
1094
|
+
' # 0.0.0.0 — and keep `expose:` rather than `ports:` so it is never reachable',
|
|
1095
|
+
' # from the host. Requires general_settings.master_key in the baked config.',
|
|
1096
|
+
' command: ["--config", "/config/litellm.config.yaml", "--port", "4443", "--host", "0.0.0.0"]',
|
|
898
1097
|
' expose: ["4443"]',
|
|
899
1098
|
' depends_on: [otel-collector]',
|
|
900
1099
|
'',
|
|
@@ -939,6 +1138,7 @@ function composeArtifacts(ctx) {
|
|
|
939
1138
|
'Profiles compose: e.g. `--profile gateway --profile edge --profile tunnel up -d`.',
|
|
940
1139
|
'LiteLLM must speak Anthropic API format — our gateway image does.',
|
|
941
1140
|
'',
|
|
1141
|
+
...renderEgressNote(ctx),
|
|
942
1142
|
].join('\n'),
|
|
943
1143
|
};
|
|
944
1144
|
}
|
|
@@ -958,6 +1158,9 @@ function composeArtifacts(ctx) {
|
|
|
958
1158
|
*/
|
|
959
1159
|
function litellmProviderKeyEnv(model) {
|
|
960
1160
|
const m = (model || '').toLowerCase();
|
|
1161
|
+
if (m === 'ah-auto') {
|
|
1162
|
+
return 'AGENTHIPPO_GATEWAY_KEY';
|
|
1163
|
+
}
|
|
961
1164
|
if (m.includes('codex')) {
|
|
962
1165
|
return 'OPENAI_CODEX_API_KEY';
|
|
963
1166
|
}
|
|
@@ -969,6 +1172,102 @@ function litellmProviderKeyEnv(model) {
|
|
|
969
1172
|
}
|
|
970
1173
|
return 'OPENAI_API_KEY';
|
|
971
1174
|
}
|
|
1175
|
+
/** True when the pack uses the managed Auto placeholder (routes via gateway.agenthippo.ai). */
|
|
1176
|
+
function isAhAutoModel(model) {
|
|
1177
|
+
return (model || '').trim().toLowerCase() === 'ah-auto';
|
|
1178
|
+
}
|
|
1179
|
+
/** Org gateway base URL — agent gateway-direct mode points here (same as colocated agents). */
|
|
1180
|
+
const AGENTHIPPO_GATEWAY_BASE_URL = 'https://gateway.agenthippo.ai/v1';
|
|
1181
|
+
function usesGatewayDirect(ctx) {
|
|
1182
|
+
return ctx.modelRouting === 'gateway';
|
|
1183
|
+
}
|
|
1184
|
+
/** The model-provider API host this pack's engine actually talks to — the one thing every
|
|
1185
|
+
* target's container needs outbound network access to. Generalizes the `spcs` target's
|
|
1186
|
+
* enforced "one EAI to api.anthropic.com" egress allowlist into a documented note every
|
|
1187
|
+
* other target ships too (governance layer, PLAN/deploy.md §6). */
|
|
1188
|
+
function egressHost(ctx) {
|
|
1189
|
+
if (ctx.engine.trim().toLowerCase() === 'cursor') {
|
|
1190
|
+
return 'api2.cursor.sh';
|
|
1191
|
+
}
|
|
1192
|
+
if (isAhAutoModel(ctx.model)) {
|
|
1193
|
+
return 'gateway.agenthippo.ai';
|
|
1194
|
+
}
|
|
1195
|
+
const m = (ctx.model || '').toLowerCase();
|
|
1196
|
+
if (m.includes('codex') || m.includes('gpt') || m.includes('openai')) {
|
|
1197
|
+
return 'api.openai.com';
|
|
1198
|
+
}
|
|
1199
|
+
return 'api.anthropic.com';
|
|
1200
|
+
}
|
|
1201
|
+
/** Egress allowlist note, appended to a target's README (governance layer). */
|
|
1202
|
+
function renderEgressNote(ctx) {
|
|
1203
|
+
const sidecarNote = usesGatewayDirect(ctx)
|
|
1204
|
+
? '.'
|
|
1205
|
+
: ' — and, if the LiteLLM gateway/Spotlight sidecar is enabled, its own outbound is the same host.';
|
|
1206
|
+
return [
|
|
1207
|
+
`**Egress:** this container only needs outbound access to \`${egressHost(ctx)}:443\` (your model provider)${sidecarNote}`,
|
|
1208
|
+
'Restrict egress to this allowlist if your org requires it.',
|
|
1209
|
+
'',
|
|
1210
|
+
];
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Data & access summary (governance layer, PLAN/deploy.md §3/§6): one generated page
|
|
1214
|
+
* enumerating what secrets exist, what leaves the network, and the blast radius per
|
|
1215
|
+
* component — pre-answering the questions a security review asks before it asks them.
|
|
1216
|
+
* The `spcs` target's "one EAI to api.anthropic.com" README line is the model this
|
|
1217
|
+
* generalizes to every target instead of leaving it to just one platform.
|
|
1218
|
+
*/
|
|
1219
|
+
function renderDataAccessSummary(ctx, target, edge, fed, colocate) {
|
|
1220
|
+
const secrets = [
|
|
1221
|
+
`\`${litellmProviderKeyEnv(ctx.model)}\` (or the pack's configured provider key) — your model provider credential`,
|
|
1222
|
+
];
|
|
1223
|
+
const blastRadius = [
|
|
1224
|
+
'**agent container** — can reach your model provider and read/write its own state volume. ' +
|
|
1225
|
+
'Never holds the JWT signing secret or any OAuth client secret (`.env` vs `.env.auth` split).',
|
|
1226
|
+
];
|
|
1227
|
+
if (edge) {
|
|
1228
|
+
secrets.push('`AGENTHIPPO_AUTH_JWT_SECRET` — signs/verifies IDE bootstrap tokens (auth-broker only, never the agent)');
|
|
1229
|
+
blastRadius.push('**auth-broker** — holds the JWT signing secret' +
|
|
1230
|
+
(fed ? ' (shared with the central IdP)' : '') +
|
|
1231
|
+
'; mints/verifies IDE tokens. Does not hold model provider credentials.');
|
|
1232
|
+
if (!fed) {
|
|
1233
|
+
secrets.push('`OAUTH2_PROXY_CLIENT_SECRET` + `OAUTH2_PROXY_COOKIE_SECRET` — this deployment\'s own Google OAuth client (oauth2-proxy only, never the agent)');
|
|
1234
|
+
blastRadius.push('**oauth2-proxy** — holds this deployment\'s Google OAuth client secret; terminates the Google ' +
|
|
1235
|
+
'login flow. Does not hold the JWT signing secret or model credentials.');
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
else if (colocate) {
|
|
1239
|
+
blastRadius.push('**this deployment holds no auth secrets at all** — identity is verified entirely by the ' +
|
|
1240
|
+
'org IdP\'s own auth-broker (see `agent-route.caddy`); nothing here signs or stores a JWT secret.');
|
|
1241
|
+
}
|
|
1242
|
+
return [
|
|
1243
|
+
`# Data & access summary — ${ctx.packName} (${target}${ctx.ingress ? ` / ${ctx.ingress}` : ''})`,
|
|
1244
|
+
'',
|
|
1245
|
+
'Generated by `agenthippo deploy init` — answers the questions a security review asks first.',
|
|
1246
|
+
'',
|
|
1247
|
+
'## Secrets',
|
|
1248
|
+
...secrets.map((s) => `- ${s}`),
|
|
1249
|
+
'',
|
|
1250
|
+
'## Egress',
|
|
1251
|
+
`- Outbound network access is needed only to \`${egressHost(ctx)}:443\` (your model provider).`,
|
|
1252
|
+
...(edge && fed ? [
|
|
1253
|
+
'- API/Bearer requests are verified locally against the shared JWT secret — no network call ' +
|
|
1254
|
+
'to the central IdP per request. The one-time browser login flow (unauthenticated visitors) ' +
|
|
1255
|
+
'does redirect to the central IdP\'s public URL to sign in.',
|
|
1256
|
+
] : []),
|
|
1257
|
+
...(edge && !fed ? [
|
|
1258
|
+
'- Google SSO calls `accounts.google.com` during login only (oauth2-proxy); no other third party is contacted per request.',
|
|
1259
|
+
] : []),
|
|
1260
|
+
...(colocate ? [
|
|
1261
|
+
'- Identity is verified via the org IdP\'s own auth-broker over the shared private docker ' +
|
|
1262
|
+
'network (`agenthippo-idp-colocate`), not a public network call. The one-time browser login ' +
|
|
1263
|
+
'flow (unauthenticated visitors) still redirects to the central IdP\'s public URL to sign in.',
|
|
1264
|
+
] : []),
|
|
1265
|
+
'',
|
|
1266
|
+
'## Blast radius per component',
|
|
1267
|
+
...blastRadius.map((b) => `- ${b}`),
|
|
1268
|
+
'',
|
|
1269
|
+
].join('\n');
|
|
1270
|
+
}
|
|
972
1271
|
function renderCloudrunServiceYaml(ctx) {
|
|
973
1272
|
const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
|
|
974
1273
|
return [
|
|
@@ -1106,7 +1405,7 @@ function renderCloudrunSpotlightDeploy(ctx) {
|
|
|
1106
1405
|
'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
|
|
1107
1406
|
'REPO="${ARTIFACT_REPO:-agenthippo}"',
|
|
1108
1407
|
'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
|
|
1109
|
-
|
|
1408
|
+
`LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('litellm-gateway')}}"`,
|
|
1110
1409
|
'STATE_BUCKET="${STATE_BUCKET:?set STATE_BUCKET=your-gcs-bucket (gcloud storage buckets create gs://...)}"',
|
|
1111
1410
|
'# LITELLM_KEY = the proxy key the agent presents to LiteLLM (and LiteLLM master key).',
|
|
1112
1411
|
'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
|
|
@@ -1117,10 +1416,10 @@ function renderCloudrunSpotlightDeploy(ctx) {
|
|
|
1117
1416
|
' || gcloud artifacts repositories create "$REPO" --repository-format=docker --location="$REGION"',
|
|
1118
1417
|
'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
|
|
1119
1418
|
'',
|
|
1120
|
-
'# Rewrite the runtime base to this project Artifact Registry
|
|
1121
|
-
'#
|
|
1419
|
+
'# Rewrite the runtime base to this project Artifact Registry. Customer artifacts pin a',
|
|
1420
|
+
'# dated ghcr.io/agenthippoai/runtime tag; mirror that exact tag into AR. No-op if already AR.',
|
|
1122
1421
|
'DF_TMP="$(mktemp)"',
|
|
1123
|
-
'sed "s|FROM ghcr.io/
|
|
1422
|
+
'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
|
|
1124
1423
|
'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
|
|
1125
1424
|
'docker push "$IMAGE"',
|
|
1126
1425
|
'',
|
|
@@ -1165,121 +1464,15 @@ function renderCloudrunSpotlightDeploy(ctx) {
|
|
|
1165
1464
|
* via `localhost:<port>` (NOT docker-DNS names like the compose edge). Cloud Run terminates TLS
|
|
1166
1465
|
* at the edge, so Caddy listens on HTTP `:{$PORT}` (`auto_https off`). The agent is NOT the
|
|
1167
1466
|
* ingress here, so it listens on a distinct internal port (EDGE_AGENT_PORT) to avoid clashing
|
|
1168
|
-
* with Caddy on $PORT.
|
|
1467
|
+
* with Caddy on $PORT. The Caddyfile renderers themselves live in `deploy/layers/identity.ts`
|
|
1468
|
+
* (`renderSidecarEdgeCaddyfile`/`renderSidecarFedCaddyfile`) — reused verbatim by aws-ecs and
|
|
1469
|
+
* azure-container-apps, which share this exact "TLS in front, one localhost" shape.
|
|
1169
1470
|
*/
|
|
1170
|
-
const EDGE_AGENT_PORT = 8090;
|
|
1171
|
-
/** Caddyfile for the Cloud Run edge — HTTP on $PORT (Cloud Run terminates TLS), localhost upstreams. */
|
|
1172
|
-
function renderCloudrunEdgeCaddyfile() {
|
|
1173
|
-
return [
|
|
1174
|
-
'# Cloud Run edge ingress. Cloud Run terminates TLS; Caddy listens on HTTP :{$PORT}.',
|
|
1175
|
-
'# Sidecars share the pod\'s localhost → upstreams are 127.0.0.1:<port> (not docker-DNS names).',
|
|
1176
|
-
'{',
|
|
1177
|
-
' auto_https off',
|
|
1178
|
-
'}',
|
|
1179
|
-
'',
|
|
1180
|
-
':{$PORT} {',
|
|
1181
|
-
' # IDE PKCE sign-in: broker requires X-Auth-Request-Email from oauth2-proxy. Must come',
|
|
1182
|
-
' # before the generic /oauth/* handle so authorize is gated (not dumped straight to broker).',
|
|
1183
|
-
' handle /oauth/authorize {',
|
|
1184
|
-
' forward_auth localhost:4180 {',
|
|
1185
|
-
' uri /oauth2/auth',
|
|
1186
|
-
' copy_headers X-Auth-Request-Email',
|
|
1187
|
-
' header_up X-Forwarded-Proto https',
|
|
1188
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1189
|
-
' @unauth status 401',
|
|
1190
|
-
' handle_response @unauth {',
|
|
1191
|
-
' redir * "/oauth2/sign_in?rd={http.request.uri}"',
|
|
1192
|
-
' }',
|
|
1193
|
-
' }',
|
|
1194
|
-
' reverse_proxy localhost:9780 {',
|
|
1195
|
-
' header_up X-Forwarded-Proto https',
|
|
1196
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1197
|
-
' }',
|
|
1198
|
-
' }',
|
|
1199
|
-
' # Federated SSO (A′): browsers from other subdomains land here to stamp the shared cookie.',
|
|
1200
|
-
' # Same oauth2-proxy gate as authorize; must precede the generic /oauth/* handle.',
|
|
1201
|
-
' handle /oauth/browser-login {',
|
|
1202
|
-
' forward_auth localhost:4180 {',
|
|
1203
|
-
' uri /oauth2/auth',
|
|
1204
|
-
' copy_headers X-Auth-Request-Email',
|
|
1205
|
-
' header_up X-Forwarded-Proto https',
|
|
1206
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1207
|
-
' @unauth status 401',
|
|
1208
|
-
' handle_response @unauth {',
|
|
1209
|
-
' redir * "/oauth2/sign_in?rd=/oauth/browser-login{http.request.uri.query}"',
|
|
1210
|
-
' }',
|
|
1211
|
-
' }',
|
|
1212
|
-
' reverse_proxy localhost:9780 {',
|
|
1213
|
-
' header_up X-Forwarded-Proto https',
|
|
1214
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1215
|
-
' }',
|
|
1216
|
-
' }',
|
|
1217
|
-
' # Auth-broker: token exchange + other /oauth/* routes that must stay ungated.',
|
|
1218
|
-
' handle /oauth/* {',
|
|
1219
|
-
' reverse_proxy localhost:9780 {',
|
|
1220
|
-
' header_up X-Forwarded-Proto https',
|
|
1221
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1222
|
-
' }',
|
|
1223
|
-
' }',
|
|
1224
|
-
' handle /api/ide/* {',
|
|
1225
|
-
' reverse_proxy localhost:9780 {',
|
|
1226
|
-
' header_up X-Forwarded-Proto https',
|
|
1227
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1228
|
-
' }',
|
|
1229
|
-
' }',
|
|
1230
|
-
' # oauth2-proxy: Google SSO callback + sign-in.',
|
|
1231
|
-
' handle /oauth2/* {',
|
|
1232
|
-
' reverse_proxy localhost:4180 {',
|
|
1233
|
-
' header_up X-Forwarded-Proto https',
|
|
1234
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1235
|
-
' }',
|
|
1236
|
-
' }',
|
|
1237
|
-
' @health path /health /ready',
|
|
1238
|
-
' handle @health {',
|
|
1239
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1240
|
-
' }',
|
|
1241
|
-
' # IDE / API clients with a Bearer token: validate via auth-broker, copy identity header.',
|
|
1242
|
-
' @apiWithAuth {',
|
|
1243
|
-
' path /v1/*',
|
|
1244
|
-
' header Authorization *',
|
|
1245
|
-
' }',
|
|
1246
|
-
' handle @apiWithAuth {',
|
|
1247
|
-
' forward_auth localhost:9780 {',
|
|
1248
|
-
' uri /api/ide/me',
|
|
1249
|
-
' copy_headers X-Agent-User',
|
|
1250
|
-
' header_up X-Forwarded-Proto https',
|
|
1251
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1252
|
-
' @unauth status 401',
|
|
1253
|
-
' handle_response @unauth {',
|
|
1254
|
-
' respond "Unauthorized" 401',
|
|
1255
|
-
' }',
|
|
1256
|
-
' }',
|
|
1257
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1258
|
-
' }',
|
|
1259
|
-
' # Browser clients: SSO via oauth2-proxy, identity injected as X-Agent-User, then proxy',
|
|
1260
|
-
' # to the agent (which serves the www landing page at / and the API at /v1/*).',
|
|
1261
|
-
' handle {',
|
|
1262
|
-
' forward_auth localhost:4180 {',
|
|
1263
|
-
' uri /oauth2/auth',
|
|
1264
|
-
' copy_headers X-Auth-Request-Email>X-Agent-User',
|
|
1265
|
-
' header_up X-Forwarded-Proto https',
|
|
1266
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1267
|
-
' @unauth status 401',
|
|
1268
|
-
' handle_response @unauth {',
|
|
1269
|
-
' redir * "/oauth2/sign_in?rd={http.request.uri.path}{http.request.uri.query}"',
|
|
1270
|
-
' }',
|
|
1271
|
-
' }',
|
|
1272
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1273
|
-
' }',
|
|
1274
|
-
'}',
|
|
1275
|
-
'',
|
|
1276
|
-
].join('\n');
|
|
1277
|
-
}
|
|
1278
1471
|
/** Cloud Run multi-container service behind the AgentHippo edge (Option B). Caddy is ingress. */
|
|
1279
1472
|
function renderCloudrunEdgeServiceYaml(ctx) {
|
|
1280
1473
|
const isCursor = ctx.engine.trim().toLowerCase() === 'cursor';
|
|
1281
1474
|
const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
|
|
1282
|
-
const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: EDGE_AGENT_PORT })
|
|
1475
|
+
const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: identity_1.EDGE_AGENT_PORT })
|
|
1283
1476
|
.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ');
|
|
1284
1477
|
// Start order: sidecars before caddy (ingress) so Caddy's first request finds everyone up.
|
|
1285
1478
|
const deps = isCursor
|
|
@@ -1370,7 +1563,7 @@ function renderCloudrunEdgeServiceYaml(ctx) {
|
|
|
1370
1563
|
]),
|
|
1371
1564
|
' volumeMounts:',
|
|
1372
1565
|
' - { name: state, mountPath: /state }',
|
|
1373
|
-
...startupProbeLines(EDGE_AGENT_PORT, true),
|
|
1566
|
+
...startupProbeLines(identity_1.EDGE_AGENT_PORT, true),
|
|
1374
1567
|
...resourceLimits('2000m', '2Gi'),
|
|
1375
1568
|
];
|
|
1376
1569
|
if (!isCursor) {
|
|
@@ -1399,7 +1592,7 @@ function renderCloudrunEdgeDeploy(ctx) {
|
|
|
1399
1592
|
'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
|
|
1400
1593
|
'REPO="${ARTIFACT_REPO:-agenthippo}"',
|
|
1401
1594
|
'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
|
|
1402
|
-
|
|
1595
|
+
`LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('litellm-gateway')}}"`,
|
|
1403
1596
|
'# Public hostname (custom domain mapped to the service) + the Google Workspace domain allowed by SSO.',
|
|
1404
1597
|
'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=your.edge.hostname (gcloud run domain-mappings create --service $SERVICE --domain ...)}"',
|
|
1405
1598
|
'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:?set ALLOWED_DOMAINS=yourworkspace.com (oauth2-proxy email domain + broker allowed domains)}"',
|
|
@@ -1422,10 +1615,10 @@ function renderCloudrunEdgeDeploy(ctx) {
|
|
|
1422
1615
|
'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
|
|
1423
1616
|
'',
|
|
1424
1617
|
'# One agent image serves both the agent and the auth-broker containers (CLI ships `auth-broker`).',
|
|
1425
|
-
'# Rewrite the runtime base to this project Artifact Registry
|
|
1426
|
-
'#
|
|
1618
|
+
'# Rewrite the runtime base to this project Artifact Registry. Customer artifacts pin a',
|
|
1619
|
+
'# dated ghcr.io/agenthippoai/runtime tag; mirror that exact tag into AR. No-op if already AR.',
|
|
1427
1620
|
'DF_TMP="$(mktemp)"',
|
|
1428
|
-
'sed "s|FROM ghcr.io/
|
|
1621
|
+
'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
|
|
1429
1622
|
'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
|
|
1430
1623
|
'docker push "$IMAGE"',
|
|
1431
1624
|
'',
|
|
@@ -1500,6 +1693,7 @@ function cloudrunArtifacts(ctx) {
|
|
|
1500
1693
|
'**Lean alternative (no Spotlight):** a single-container deploy that calls a provider directly',
|
|
1501
1694
|
'(or an external LiteLLM) — set `AGENTHIPPO_LITELLM_BASE_URL` to your endpoint and drop the sidecars.',
|
|
1502
1695
|
'',
|
|
1696
|
+
...renderEgressNote(ctx),
|
|
1503
1697
|
].join('\n'),
|
|
1504
1698
|
};
|
|
1505
1699
|
}
|
|
@@ -1535,10 +1729,10 @@ function cloudrunArtifacts(ctx) {
|
|
|
1535
1729
|
'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
|
|
1536
1730
|
'',
|
|
1537
1731
|
'# Build from pack root; Dockerfile is deploy/Dockerfile (not pack root — gcloud --source . misses it).',
|
|
1538
|
-
'# Rewrite the runtime base to this project Artifact Registry
|
|
1539
|
-
'#
|
|
1732
|
+
'# Rewrite the runtime base to this project Artifact Registry. Customer artifacts pin a',
|
|
1733
|
+
'# dated ghcr.io/agenthippoai/runtime tag; mirror that exact tag into AR. No-op if already AR.',
|
|
1540
1734
|
'DF_TMP="$(mktemp)"',
|
|
1541
|
-
'sed "s|FROM ghcr.io/
|
|
1735
|
+
'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
|
|
1542
1736
|
'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
|
|
1543
1737
|
'docker push "$IMAGE"',
|
|
1544
1738
|
'',
|
|
@@ -1571,7 +1765,7 @@ function cloudrunArtifacts(ctx) {
|
|
|
1571
1765
|
: '# - No inbound channel (1a): scales to zero; state on the GCS volume survives the instance dying.',
|
|
1572
1766
|
'# - GCS-FUSE adds latency on session read/write vs local disk; fine for chat, watch for heavy I/O.',
|
|
1573
1767
|
isCursor
|
|
1574
|
-
?
|
|
1768
|
+
? `# - Cursor engine: CURSOR_API_KEY secret + ${agenthippoImage('runtime', 'cursor')} base (AGENTHIPPO_CUSTOM_ENGINES_DIR=/engines).`
|
|
1575
1769
|
: '# - Vertex path = keyless Claude on your existing GCP entitlement; swap for ANTHROPIC_API_KEY if preferred.',
|
|
1576
1770
|
'# - First deploy without IAP org setup: ENABLE_IAP=0 bash deploy/cloudrun-deploy.sh',
|
|
1577
1771
|
'',
|
|
@@ -1613,6 +1807,7 @@ function cloudrunArtifacts(ctx) {
|
|
|
1613
1807
|
' -d \'{"message":"Reply with exactly: CLOUDRUN_OK","session_id":"cr-1"}\'',
|
|
1614
1808
|
'```',
|
|
1615
1809
|
'',
|
|
1810
|
+
...renderEgressNote(ctx),
|
|
1616
1811
|
].join('\n'),
|
|
1617
1812
|
};
|
|
1618
1813
|
}
|
|
@@ -1624,7 +1819,7 @@ function cloudrunEdgeArtifacts(ctx, inbound, isCursor) {
|
|
|
1624
1819
|
const files = {
|
|
1625
1820
|
'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
|
|
1626
1821
|
'www/index.html': renderLandingPage(ctx.packName),
|
|
1627
|
-
'Caddyfile':
|
|
1822
|
+
'Caddyfile': (0, identity_1.renderSidecarEdgeCaddyfile)(),
|
|
1628
1823
|
'service.yaml': renderCloudrunEdgeServiceYaml(ctx),
|
|
1629
1824
|
'cloudrun-deploy.sh': renderCloudrunEdgeDeploy(ctx),
|
|
1630
1825
|
};
|
|
@@ -1638,7 +1833,7 @@ function cloudrunEdgeArtifacts(ctx, inbound, isCursor) {
|
|
|
1638
1833
|
'',
|
|
1639
1834
|
`${sidecars} Sidecars share the pod\'s localhost (Cloud Run multi-container):`,
|
|
1640
1835
|
'- **caddy** (ingress, the only port) → SSO edge: oauth2-proxy for browsers, auth-broker for IDE Bearer',
|
|
1641
|
-
`- **agent** → serves www + /v1/* on localhost:${EDGE_AGENT_PORT} (NOT ingress)`,
|
|
1836
|
+
`- **agent** → serves www + /v1/* on localhost:${identity_1.EDGE_AGENT_PORT} (NOT ingress)`,
|
|
1642
1837
|
'- **auth-broker** → validates IDE AgentHippo JWTs (`/api/ide/me`) + IDE sign-in (`/oauth/*`)',
|
|
1643
1838
|
'- **oauth2-proxy** → Google SSO for browser clients (`provider=google`)',
|
|
1644
1839
|
...(isCursor ? [] : [
|
|
@@ -1671,77 +1866,10 @@ function cloudrunEdgeArtifacts(ctx, inbound, isCursor) {
|
|
|
1671
1866
|
? '**Inbound channel (1b):** `minScale: 1` pins one always-on instance (the socket can\'t scale to zero).'
|
|
1672
1867
|
: '**No inbound channel (1a):** scales to zero; traces + sessions persist on the GCS `/state` volume.',
|
|
1673
1868
|
'',
|
|
1869
|
+
...renderEgressNote(ctx),
|
|
1674
1870
|
].join('\n');
|
|
1675
1871
|
return files;
|
|
1676
1872
|
}
|
|
1677
|
-
/**
|
|
1678
|
-
* A′ — Federated edge Caddyfile. No oauth2-proxy: Google login happens once at the central IdP
|
|
1679
|
-
* (`ctx.idpUrl`); this agent only verifies the shared AgentHippo JWT (Bearer for IDE, cookie for
|
|
1680
|
-
* browser). On a browser 401, redirect to the IdP's `/oauth/browser-login` to stamp the shared
|
|
1681
|
-
* `.agenthippo.ai` cookie, then return.
|
|
1682
|
-
*/
|
|
1683
|
-
function renderCloudrunFedCaddyfile(ctx) {
|
|
1684
|
-
const idp = (ctx.idpUrl || '').trim().replace(/\/+$/, '');
|
|
1685
|
-
const loginRedirect = idp
|
|
1686
|
-
? ` redir * "${idp}/oauth/browser-login?return_to=https://{http.request.host}{http.request.uri}" 302`
|
|
1687
|
-
: ' respond "Unauthorized" 401';
|
|
1688
|
-
return [
|
|
1689
|
-
'# Cloud Run federated edge (A′). Cloud Run terminates TLS; Caddy listens on HTTP :{$PORT}.',
|
|
1690
|
-
`# No oauth2-proxy — Google login happens once at the central IdP (${idp || '<IDP_URL>'});`,
|
|
1691
|
-
'# this agent only verifies the shared .agenthippo.ai AgentHippo JWT (Bearer for IDE, cookie for browser).',
|
|
1692
|
-
'{',
|
|
1693
|
-
' auto_https off',
|
|
1694
|
-
'}',
|
|
1695
|
-
'',
|
|
1696
|
-
':{$PORT} {',
|
|
1697
|
-
' # Auth-broker: IDE bootstrap API + JWT validation (/api/ide/me is also used by forward_auth).',
|
|
1698
|
-
' handle /api/ide/* {',
|
|
1699
|
-
' reverse_proxy localhost:9780 {',
|
|
1700
|
-
' header_up X-Forwarded-Proto https',
|
|
1701
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1702
|
-
' }',
|
|
1703
|
-
' }',
|
|
1704
|
-
' @health path /health /ready',
|
|
1705
|
-
' handle @health {',
|
|
1706
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1707
|
-
' }',
|
|
1708
|
-
' # IDE / API clients with a Bearer token: validate via auth-broker, copy identity header.',
|
|
1709
|
-
' @apiWithAuth {',
|
|
1710
|
-
' path /v1/*',
|
|
1711
|
-
' header Authorization *',
|
|
1712
|
-
' }',
|
|
1713
|
-
' handle @apiWithAuth {',
|
|
1714
|
-
' forward_auth localhost:9780 {',
|
|
1715
|
-
' uri /api/ide/me',
|
|
1716
|
-
' copy_headers X-Agent-User',
|
|
1717
|
-
' header_up X-Forwarded-Proto https',
|
|
1718
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1719
|
-
' @unauth status 401',
|
|
1720
|
-
' handle_response @unauth {',
|
|
1721
|
-
' respond "Unauthorized" 401',
|
|
1722
|
-
' }',
|
|
1723
|
-
' }',
|
|
1724
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1725
|
-
' }',
|
|
1726
|
-
' # Browser clients: the shared .agenthippo.ai cookie is verified by auth-broker. No cookie',
|
|
1727
|
-
' # → redirect to the central IdP to log in (it stamps the cookie), then return.',
|
|
1728
|
-
' handle {',
|
|
1729
|
-
' forward_auth localhost:9780 {',
|
|
1730
|
-
' uri /api/ide/me',
|
|
1731
|
-
' copy_headers X-Agent-User',
|
|
1732
|
-
' header_up X-Forwarded-Proto https',
|
|
1733
|
-
' header_up X-Forwarded-Host {http.request.host}',
|
|
1734
|
-
' @unauth status 401',
|
|
1735
|
-
' handle_response @unauth {',
|
|
1736
|
-
loginRedirect,
|
|
1737
|
-
' }',
|
|
1738
|
-
' }',
|
|
1739
|
-
` reverse_proxy localhost:${EDGE_AGENT_PORT}`,
|
|
1740
|
-
' }',
|
|
1741
|
-
'}',
|
|
1742
|
-
'',
|
|
1743
|
-
].join('\n');
|
|
1744
|
-
}
|
|
1745
1873
|
/** Cloud Run requires a startupProbe on any container that is a `depends_on` target, so the
|
|
1746
1874
|
* platform knows when the dependency is ready. `slow` = one generous 240s attempt (agent, which
|
|
1747
1875
|
* may warm an engine); otherwise 30×5s polls (sidecars that start fast). */
|
|
@@ -1784,7 +1912,7 @@ function resourceLimits(cpu, memory) {
|
|
|
1784
1912
|
function renderCloudrunFedServiceYaml(ctx) {
|
|
1785
1913
|
const isCursor = ctx.engine.trim().toLowerCase() === 'cursor';
|
|
1786
1914
|
const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
|
|
1787
|
-
const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: EDGE_AGENT_PORT })
|
|
1915
|
+
const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: identity_1.EDGE_AGENT_PORT })
|
|
1788
1916
|
.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ');
|
|
1789
1917
|
const cookieDomain = (ctx.cookieDomain || '.agenthippo.ai').trim();
|
|
1790
1918
|
const idpUrl = (ctx.idpUrl || 'https://auth.agenthippo.ai').trim().replace(/\/+$/, '');
|
|
@@ -1860,11 +1988,13 @@ function renderCloudrunFedServiceYaml(ctx) {
|
|
|
1860
1988
|
]),
|
|
1861
1989
|
' volumeMounts:',
|
|
1862
1990
|
' - { name: state, mountPath: /state }',
|
|
1863
|
-
...startupProbeLines(EDGE_AGENT_PORT, true),
|
|
1991
|
+
...startupProbeLines(identity_1.EDGE_AGENT_PORT, true),
|
|
1864
1992
|
...resourceLimits('2000m', '2Gi'),
|
|
1865
1993
|
];
|
|
1866
1994
|
if (!isCursor) {
|
|
1867
|
-
lines.push(' # LiteLLM gateway sidecar — exports every call to the collector (Spotlight).', ' - name: litellm', ' image: ${LITELLM_IMAGE}', ' env:', ' - { name: OTEL_EXPORTER, value: otlp_http }', ' - { name: OTEL_EXPORTER_OTLP_ENDPOINT, value: http://localhost:4318 }', ' - name: LITELLM_MASTER_KEY', ' valueFrom:', ' secretKeyRef: { name: ${SERVICE}-litellm-key, key: latest }',
|
|
1995
|
+
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)
|
|
1996
|
+
? ' # ah-auto → gateway.agenthippo.ai (AGENTHIPPO_GATEWAY_KEY = ahgw_… service key).'
|
|
1997
|
+
: ` # Upstream provider key for ${ctx.model || 'this model'} (matches os.environ/${providerKeyEnv}).`, ` - name: ${providerKeyEnv}`, ' valueFrom:', ' secretKeyRef: { name: ${SERVICE}-provider-key, key: latest }', ...startupProbeLines(4443), ...resourceLimits('1000m', '1Gi'), ' # Spotlight OTEL collector sidecar — writes JSONL traces into the shared /state volume.', ' - name: otel-collector', ' image: otel/opentelemetry-collector-contrib:0.118.0', ' args: ["--config=/etc/otel/config.yaml"]', ' env:', ' - { name: AGENT_HOME_DIR, value: /state }', ' volumeMounts:', ' - { name: otel-config, mountPath: /etc/otel, readOnly: true }', ' - { name: state, mountPath: /state }', ...startupProbeLines(4318), ...resourceLimits('1000m', '512Mi'));
|
|
1868
1998
|
}
|
|
1869
1999
|
lines.push(' volumes:', ' # In-memory state (matches the working spotlight deploy — no GCS SA perms needed).', ' - name: state', ' emptyDir:', ' medium: Memory', ' sizeLimit: 1Gi', ' - name: caddy-config', ' secret:', ' secretName: ${SERVICE}-caddyfile', ' items:', ' - { key: latest, path: Caddyfile }');
|
|
1870
2000
|
if (!isCursor) {
|
|
@@ -1873,6 +2003,141 @@ function renderCloudrunFedServiceYaml(ctx) {
|
|
|
1873
2003
|
lines.push('');
|
|
1874
2004
|
return lines.join('\n');
|
|
1875
2005
|
}
|
|
2006
|
+
/** A′ gateway-direct — federated edge without LiteLLM/OTEL sidecars. Agent calls the org gateway
|
|
2007
|
+
* (gateway.agenthippo.ai) directly — same wiring as colocated agents on hippopennyg. */
|
|
2008
|
+
function renderCloudrunFedGatewayServiceYaml(ctx) {
|
|
2009
|
+
const agentArgs = serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: identity_1.EDGE_AGENT_PORT })
|
|
2010
|
+
.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ');
|
|
2011
|
+
const cookieDomain = (ctx.cookieDomain || '.agenthippo.ai').trim();
|
|
2012
|
+
const idpUrl = (ctx.idpUrl || 'https://auth.agenthippo.ai').trim().replace(/\/+$/, '');
|
|
2013
|
+
const lines = [
|
|
2014
|
+
'# Cloud Run + AgentHippo federated edge (A′) — gateway-direct (lean):',
|
|
2015
|
+
'# Caddy (ingress) + auth-broker + agent. No LiteLLM/OTEL sidecars.',
|
|
2016
|
+
'# Agent routes ah-auto (and other gateway models) to gateway.agenthippo.ai directly.',
|
|
2017
|
+
'# Generated by `agenthippo deploy init --target cloudrun --ingress edge-fed --model-routing gateway`.',
|
|
2018
|
+
'apiVersion: serving.knative.dev/v1',
|
|
2019
|
+
'kind: Service',
|
|
2020
|
+
'metadata:',
|
|
2021
|
+
' name: ${SERVICE}',
|
|
2022
|
+
'spec:',
|
|
2023
|
+
' template:',
|
|
2024
|
+
' metadata:',
|
|
2025
|
+
' annotations:',
|
|
2026
|
+
' run.googleapis.com/cpu-throttling: "false"',
|
|
2027
|
+
' run.googleapis.com/startup-cpu-boost: "true"',
|
|
2028
|
+
" run.googleapis.com/container-dependencies: '{\"caddy\":[\"agent\",\"auth-broker\"]}'",
|
|
2029
|
+
' autoscaling.knative.dev/maxScale: "1"',
|
|
2030
|
+
...(ctx.gatewayChannels?.trim() ? [' autoscaling.knative.dev/minScale: "1"'] : []),
|
|
2031
|
+
' spec:',
|
|
2032
|
+
' timeoutSeconds: 3600',
|
|
2033
|
+
' containers:',
|
|
2034
|
+
' - name: caddy',
|
|
2035
|
+
' image: caddy:2.8-alpine',
|
|
2036
|
+
' ports:',
|
|
2037
|
+
' - name: http1',
|
|
2038
|
+
` containerPort: ${ctx.port}`,
|
|
2039
|
+
' volumeMounts:',
|
|
2040
|
+
' - { name: caddy-config, mountPath: /etc/caddy, readOnly: true }',
|
|
2041
|
+
...resourceLimits('1000m', '512Mi'),
|
|
2042
|
+
' - name: auth-broker',
|
|
2043
|
+
' image: ${IMAGE}',
|
|
2044
|
+
' args: ["auth-broker"]',
|
|
2045
|
+
' env:',
|
|
2046
|
+
' - { name: AGENTHIPPO_AUTH_PORT, value: "9780" }',
|
|
2047
|
+
' - { name: AGENTHIPPO_AUTH_DATA_DIR, value: /data }',
|
|
2048
|
+
' - { name: AGENTHIPPO_AUTH_PUBLIC_URL, value: https://${PUBLIC_HOSTNAME} }',
|
|
2049
|
+
' - { name: AGENTHIPPO_AUTH_ALLOWED_DOMAINS, value: ${ALLOWED_DOMAINS} }',
|
|
2050
|
+
` - { name: AGENTHIPPO_AUTH_COOKIE_DOMAIN, value: "${cookieDomain}" }`,
|
|
2051
|
+
` - { name: AGENTHIPPO_AUTH_IDP_URL, value: "${idpUrl}" }`,
|
|
2052
|
+
' - { name: AGENTHIPPO_AUTH_COOKIE_NAME, value: "ah_token" }',
|
|
2053
|
+
' - name: AGENTHIPPO_AUTH_JWT_SECRET',
|
|
2054
|
+
' valueFrom:',
|
|
2055
|
+
' secretKeyRef: { name: ${SERVICE}-auth-jwt-secret, key: latest }',
|
|
2056
|
+
...startupProbeLines(9780),
|
|
2057
|
+
...resourceLimits('1000m', '512Mi'),
|
|
2058
|
+
' - name: agent',
|
|
2059
|
+
' image: ${IMAGE}',
|
|
2060
|
+
` args: [${agentArgs}]`,
|
|
2061
|
+
' env:',
|
|
2062
|
+
' - { name: AGENTHIPPO_HOME, value: /state }',
|
|
2063
|
+
' - { name: AGENTHIPPO_NO_AUTO_INSTALL, value: "1" }',
|
|
2064
|
+
agentServeHostEnvYaml(),
|
|
2065
|
+
` - { name: AGENTHIPPO_LITELLM_BASE_URL, value: "${AGENTHIPPO_GATEWAY_BASE_URL}" }`,
|
|
2066
|
+
' - name: AGENTHIPPO_LITELLM_API_KEY',
|
|
2067
|
+
' valueFrom:',
|
|
2068
|
+
' secretKeyRef: { name: ${SERVICE}-provider-key, key: latest }',
|
|
2069
|
+
' volumeMounts:',
|
|
2070
|
+
' - { name: state, mountPath: /state }',
|
|
2071
|
+
...startupProbeLines(identity_1.EDGE_AGENT_PORT, true),
|
|
2072
|
+
...resourceLimits('2000m', '2Gi'),
|
|
2073
|
+
' volumes:',
|
|
2074
|
+
' - name: state',
|
|
2075
|
+
' emptyDir:',
|
|
2076
|
+
' medium: Memory',
|
|
2077
|
+
' sizeLimit: 1Gi',
|
|
2078
|
+
' - name: caddy-config',
|
|
2079
|
+
' secret:',
|
|
2080
|
+
' secretName: ${SERVICE}-caddyfile',
|
|
2081
|
+
' items:',
|
|
2082
|
+
' - { key: latest, path: Caddyfile }',
|
|
2083
|
+
'',
|
|
2084
|
+
];
|
|
2085
|
+
return lines.join('\n');
|
|
2086
|
+
}
|
|
2087
|
+
/** Deploy script for gateway-direct federated edge — no LiteLLM/OTEL secrets or sidecars. */
|
|
2088
|
+
function renderCloudrunFedGatewayDeploy(ctx) {
|
|
2089
|
+
const modelNote = ctx.model?.trim() ? `, model ${ctx.model.trim()}` : '';
|
|
2090
|
+
const providerHint = isAhAutoModel(ctx.model)
|
|
2091
|
+
? 'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<ahgw_… from create-gateway-key on gateway host>}"'
|
|
2092
|
+
: 'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<gateway service key or provider key>}"';
|
|
2093
|
+
return [
|
|
2094
|
+
'#!/usr/bin/env bash',
|
|
2095
|
+
'# Cloud Run + AgentHippo federated edge (A′) — gateway-direct (lean).',
|
|
2096
|
+
`# Engine: ${ctx.engine}${modelNote}. Caddy + auth-broker + agent only — agent calls`,
|
|
2097
|
+
'# gateway.agenthippo.ai directly (no in-pod LiteLLM/OTEL sidecars). Same model routing',
|
|
2098
|
+
'# as colocated agents on hippopennyg.',
|
|
2099
|
+
'set -euo pipefail',
|
|
2100
|
+
...anchorToStagingRoot(),
|
|
2101
|
+
`SERVICE="${ctx.packName}"`,
|
|
2102
|
+
'REGION="${REGION:-us-central1}"',
|
|
2103
|
+
'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
|
|
2104
|
+
'REPO="${ARTIFACT_REPO:-agenthippo}"',
|
|
2105
|
+
'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
|
|
2106
|
+
'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai}"',
|
|
2107
|
+
'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:-agenthippo.ai}"',
|
|
2108
|
+
'AUTH_JWT_SECRET="${AUTH_JWT_SECRET:?set AUTH_JWT_SECRET=<copy from auth.agenthippo.ai>}"',
|
|
2109
|
+
providerHint,
|
|
2110
|
+
'',
|
|
2111
|
+
'gcloud artifacts repositories describe "$REPO" --location="$REGION" 2>/dev/null \\',
|
|
2112
|
+
' || gcloud artifacts repositories create "$REPO" --repository-format=docker --location="$REGION"',
|
|
2113
|
+
'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
|
|
2114
|
+
'',
|
|
2115
|
+
'DF_TMP="$(mktemp)"',
|
|
2116
|
+
'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
|
|
2117
|
+
'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
|
|
2118
|
+
'docker push "$IMAGE"',
|
|
2119
|
+
'',
|
|
2120
|
+
'ensure_secret() { gcloud secrets describe "$1" >/dev/null 2>&1 || gcloud secrets create "$1" --replication-policy=automatic; }',
|
|
2121
|
+
'ensure_secret "${SERVICE}-caddyfile"; gcloud secrets versions add "${SERVICE}-caddyfile" --data-file=deploy/Caddyfile',
|
|
2122
|
+
'ensure_secret "${SERVICE}-auth-jwt-secret"; printf %s "$AUTH_JWT_SECRET" | gcloud secrets versions add "${SERVICE}-auth-jwt-secret" --data-file=-',
|
|
2123
|
+
'ensure_secret "${SERVICE}-provider-key"; printf %s "$PROVIDER_KEY" | gcloud secrets versions add "${SERVICE}-provider-key" --data-file=-',
|
|
2124
|
+
'',
|
|
2125
|
+
'RENDERED="$(mktemp)"',
|
|
2126
|
+
'sed -e "s|\\${SERVICE}|${SERVICE}|g" -e "s|\\${IMAGE}|${IMAGE}|g" \\',
|
|
2127
|
+
' -e "s|\\${PUBLIC_HOSTNAME}|${PUBLIC_HOSTNAME}|g" -e "s|\\${ALLOWED_DOMAINS}|${ALLOWED_DOMAINS}|g" \\',
|
|
2128
|
+
' deploy/service.yaml > "$RENDERED"',
|
|
2129
|
+
'gcloud run services replace "$RENDERED" --region "$REGION"',
|
|
2130
|
+
'',
|
|
2131
|
+
'gcloud run services add-iam-policy-binding "$SERVICE" --region "$REGION" \\',
|
|
2132
|
+
' --member=allUsers --role=roles/run.invoker >/dev/null 2>&1 || true',
|
|
2133
|
+
'',
|
|
2134
|
+
'if [[ "$PUBLIC_HOSTNAME" != *.run.app ]]; then \\',
|
|
2135
|
+
' gcloud beta run domain-mappings describe "$PUBLIC_HOSTNAME" --region "$REGION" >/dev/null 2>&1 \\',
|
|
2136
|
+
' || gcloud beta run domain-mappings create --service "$SERVICE" --region "$REGION" --domain "$PUBLIC_HOSTNAME"; \\',
|
|
2137
|
+
'fi',
|
|
2138
|
+
'',
|
|
2139
|
+
].join('\n');
|
|
2140
|
+
}
|
|
1876
2141
|
/** Deploy script for the federated edge (A′): build, upsert secrets (no OAuth), replace, make
|
|
1877
2142
|
* public, map the subdomain. No IAP; no Google OAuth client. */
|
|
1878
2143
|
function renderCloudrunFedDeploy(ctx) {
|
|
@@ -1892,7 +2157,7 @@ function renderCloudrunFedDeploy(ctx) {
|
|
|
1892
2157
|
'PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project)}"',
|
|
1893
2158
|
'REPO="${ARTIFACT_REPO:-agenthippo}"',
|
|
1894
2159
|
'IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/${SERVICE}:latest"',
|
|
1895
|
-
|
|
2160
|
+
`LITELLM_IMAGE="\${LITELLM_IMAGE:-${agenthippoImage('litellm-gateway')}}"`,
|
|
1896
2161
|
'# Public hostname = a *.agenthippo.ai subdomain mapped to this service (one-time wildcard DNS).',
|
|
1897
2162
|
'PUBLIC_HOSTNAME="${PUBLIC_HOSTNAME:?set PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai}"',
|
|
1898
2163
|
'ALLOWED_DOMAINS="${ALLOWED_DOMAINS:-agenthippo.ai}"',
|
|
@@ -1901,6 +2166,10 @@ function renderCloudrunFedDeploy(ctx) {
|
|
|
1901
2166
|
'AUTH_JWT_SECRET="${AUTH_JWT_SECRET:?set AUTH_JWT_SECRET=<copy from auth.agenthippo.ai>}"',
|
|
1902
2167
|
...(isCursor ? [
|
|
1903
2168
|
'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<cursor api key>}"',
|
|
2169
|
+
] : isAhAutoModel(ctx.model) ? [
|
|
2170
|
+
'# LiteLLM proxy key + AgentHippo gateway service key (ahgw_…) for ah-auto routing.',
|
|
2171
|
+
'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
|
|
2172
|
+
'PROVIDER_KEY="${PROVIDER_KEY:?set PROVIDER_KEY=<ahgw_… from create-gateway-key on gateway host>}"',
|
|
1904
2173
|
] : [
|
|
1905
2174
|
'# LiteLLM proxy key + the upstream provider key the gateway uses for the model.',
|
|
1906
2175
|
'LITELLM_KEY="${LITELLM_KEY:?set LITELLM_KEY=<litellm proxy key>}"',
|
|
@@ -1912,10 +2181,10 @@ function renderCloudrunFedDeploy(ctx) {
|
|
|
1912
2181
|
'gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet',
|
|
1913
2182
|
'',
|
|
1914
2183
|
'# One agent image serves both the agent and the auth-broker containers (CLI ships `auth-broker`).',
|
|
1915
|
-
'# Rewrite the runtime base to this project Artifact Registry
|
|
1916
|
-
'#
|
|
2184
|
+
'# Rewrite the runtime base to this project Artifact Registry. Customer artifacts pin a',
|
|
2185
|
+
'# dated ghcr.io/agenthippoai/runtime tag; mirror that exact tag into AR. No-op if already AR.',
|
|
1917
2186
|
'DF_TMP="$(mktemp)"',
|
|
1918
|
-
'sed "s|FROM ghcr.io/
|
|
2187
|
+
'sed "s|FROM ghcr.io/agenthippoai/runtime:|FROM ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/runtime:|" deploy/Dockerfile > "$DF_TMP"',
|
|
1919
2188
|
'docker build --pull --platform linux/amd64 -f "$DF_TMP" -t "$IMAGE" .',
|
|
1920
2189
|
'docker push "$IMAGE"',
|
|
1921
2190
|
'',
|
|
@@ -1933,30 +2202,36 @@ function renderCloudrunFedDeploy(ctx) {
|
|
|
1933
2202
|
}
|
|
1934
2203
|
/** A′ artifacts: Cloud Run behind the federated AgentHippo edge (Caddy + auth-broker; no oauth2-proxy). */
|
|
1935
2204
|
function cloudrunFedArtifacts(ctx, inbound, isCursor) {
|
|
2205
|
+
const gatewayDirect = usesGatewayDirect(ctx) && !isCursor;
|
|
1936
2206
|
const sidecars = isCursor
|
|
1937
2207
|
? 'Caddy (ingress) + auth-broker + agent.'
|
|
1938
|
-
:
|
|
2208
|
+
: gatewayDirect
|
|
2209
|
+
? 'Caddy (ingress) + auth-broker + agent (gateway-direct — no LiteLLM/OTEL sidecars).'
|
|
2210
|
+
: 'Caddy (ingress) + auth-broker + agent + LiteLLM + OTEL collector (Spotlight).';
|
|
1939
2211
|
const idp = (ctx.idpUrl || 'https://auth.agenthippo.ai').trim().replace(/\/+$/, '');
|
|
1940
2212
|
const files = {
|
|
1941
2213
|
'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
|
|
1942
2214
|
'www/index.html': renderLandingPage(ctx.packName),
|
|
1943
|
-
'Caddyfile':
|
|
1944
|
-
'service.yaml': renderCloudrunFedServiceYaml(ctx),
|
|
1945
|
-
'cloudrun-deploy.sh': renderCloudrunFedDeploy(ctx),
|
|
2215
|
+
'Caddyfile': (0, identity_1.renderSidecarFedCaddyfile)(ctx.idpUrl),
|
|
2216
|
+
'service.yaml': gatewayDirect ? renderCloudrunFedGatewayServiceYaml(ctx) : renderCloudrunFedServiceYaml(ctx),
|
|
2217
|
+
'cloudrun-deploy.sh': gatewayDirect ? renderCloudrunFedGatewayDeploy(ctx) : renderCloudrunFedDeploy(ctx),
|
|
1946
2218
|
};
|
|
1947
|
-
if (!isCursor) {
|
|
2219
|
+
if (!isCursor && !gatewayDirect) {
|
|
1948
2220
|
files['otel-collector.yaml'] = OTEL_COLLECTOR_COMPOSE_YAML;
|
|
1949
2221
|
}
|
|
1950
2222
|
files['README.md'] = [
|
|
1951
|
-
`# Deploy: ${ctx.packName} — Cloud Run + AgentHippo federated edge (A
|
|
2223
|
+
`# Deploy: ${ctx.packName} — Cloud Run + AgentHippo federated edge (A′${gatewayDirect ? ', gateway-direct' : ''})`,
|
|
1952
2224
|
'',
|
|
1953
2225
|
`Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}. Runtime base: \`${ctx.image}\`.`,
|
|
1954
2226
|
'',
|
|
1955
2227
|
`${sidecars} Sidecars share the pod\'s localhost (Cloud Run multi-container):`,
|
|
1956
2228
|
'- **caddy** (ingress, the only port) → verifies the shared AgentHippo JWT (Bearer for IDE, cookie for browser)',
|
|
1957
|
-
`- **agent** → serves www + /v1/* on localhost:${EDGE_AGENT_PORT} (NOT ingress)`,
|
|
2229
|
+
`- **agent** → serves www + /v1/* on localhost:${identity_1.EDGE_AGENT_PORT} (NOT ingress)`,
|
|
1958
2230
|
'- **auth-broker** → validates the shared `.agenthippo.ai` JWT via `/api/ide/me` (same secret as the IdP)',
|
|
1959
|
-
...(
|
|
2231
|
+
...(gatewayDirect ? [
|
|
2232
|
+
`- **model routing** → agent calls \`${AGENTHIPPO_GATEWAY_BASE_URL}\` directly (ahgw_… service key) — same as colocated agents`,
|
|
2233
|
+
'- **no Spotlight sidecars** — traces are not collected in-pod; use gateway/Spotlight fleet analytics instead',
|
|
2234
|
+
] : isCursor ? [] : [
|
|
1960
2235
|
'- **litellm** gateway → exports OTLP to `http://localhost:4318`',
|
|
1961
2236
|
'- **otel-collector** → writes JSONL traces to the in-memory `/state` volume',
|
|
1962
2237
|
]),
|
|
@@ -1971,23 +2246,31 @@ function cloudrunFedArtifacts(ctx, inbound, isCursor) {
|
|
|
1971
2246
|
'cp deploy/.env.example deploy/.env && cp deploy/.env.auth.example deploy/.env.auth # fill both',
|
|
1972
2247
|
'export PUBLIC_HOSTNAME=<subdomain>.agenthippo.ai # e.g. cs.agenthippo.ai',
|
|
1973
2248
|
'export AUTH_JWT_SECRET=<copy from auth.agenthippo.ai> # shared signing key',
|
|
1974
|
-
...(isCursor ? ['export PROVIDER_KEY=<cursor api key>'] :
|
|
2249
|
+
...(isCursor ? ['export PROVIDER_KEY=<cursor api key>'] : gatewayDirect
|
|
2250
|
+
? ['export PROVIDER_KEY=<ahgw_… from create-gateway-key on gateway host>']
|
|
2251
|
+
: isAhAutoModel(ctx.model)
|
|
2252
|
+
? ['export LITELLM_KEY=… PROVIDER_KEY=<ahgw_… from create-gateway-key on gateway host>']
|
|
2253
|
+
: ['export LITELLM_KEY=… PROVIDER_KEY=…']),
|
|
1975
2254
|
'bash deploy/cloudrun-deploy.sh # builds linux/amd64, upserts secrets, `gcloud run services replace`, maps the subdomain',
|
|
1976
2255
|
'```',
|
|
1977
2256
|
'',
|
|
1978
2257
|
'**One-time setup (not per agent):** wildcard DNS `*.agenthippo.ai` → Cloud Run (`gctrl.run.app`),',
|
|
1979
2258
|
`and the IdP (${idp}) must run the \`/oauth/browser-login\` route (oauth2-proxy forward_auth →`,
|
|
1980
2259
|
'auth-broker sets the `ah_token` cookie on `.agenthippo.ai`). After that, every new agent is',
|
|
1981
|
-
|
|
2260
|
+
gatewayDirect
|
|
2261
|
+
? 'just: `deploy init --ingress edge-fed --model-routing gateway` + this script — zero Console/OAuth work.'
|
|
2262
|
+
: 'just: `deploy init --ingress edge-fed` + this script — zero Console/OAuth work.',
|
|
1982
2263
|
'',
|
|
1983
|
-
|
|
1984
|
-
|
|
2264
|
+
gatewayDirect
|
|
2265
|
+
? '`service.yaml` placeholders (`${SERVICE} ${IMAGE} ${PUBLIC_HOSTNAME} ${ALLOWED_DOMAINS}`) are substituted by the deploy script. The Caddyfile is stored in Secret Manager (`${SERVICE}-caddyfile`).'
|
|
2266
|
+
: '`service.yaml` placeholders (`${SERVICE} ${IMAGE} ${LITELLM_IMAGE} ${PUBLIC_HOSTNAME} ${ALLOWED_DOMAINS}`) are substituted by the deploy script. The Caddyfile is stored in Secret Manager (`${SERVICE}-caddyfile`).',
|
|
1985
2267
|
'`AGENTHIPPO_AUTH_COOKIE_DOMAIN` / `AGENTHIPPO_AUTH_IDP_URL` are baked in at generation time.',
|
|
1986
2268
|
'',
|
|
1987
2269
|
inbound
|
|
1988
2270
|
? '**Inbound channel (1b):** `minScale: 1` pins one always-on instance (the socket can\'t scale to zero).'
|
|
1989
2271
|
: '**No inbound channel (1a):** scales to zero; in-memory `/state` is wiped on cold start (use the `edge` mode + GCS bucket if you need durable state).',
|
|
1990
2272
|
'',
|
|
2273
|
+
...renderEgressNote(ctx),
|
|
1991
2274
|
].join('\n');
|
|
1992
2275
|
return files;
|
|
1993
2276
|
}
|
|
@@ -2011,7 +2294,7 @@ function databricksAppArtifacts(ctx) {
|
|
|
2011
2294
|
private: true,
|
|
2012
2295
|
dependencies: {
|
|
2013
2296
|
[exports.AGENTHIPPO_CLI_NPM_PACKAGE]: resolveAgentHippoCliNpmVersion(),
|
|
2014
|
-
'@anthropic-ai/claude-code':
|
|
2297
|
+
'@anthropic-ai/claude-code': exports.CLAUDE_CODE_NPM_VERSION,
|
|
2015
2298
|
},
|
|
2016
2299
|
}, null, 2) + '\n';
|
|
2017
2300
|
const start = [
|
|
@@ -2029,7 +2312,10 @@ function databricksAppArtifacts(ctx) {
|
|
|
2029
2312
|
" // Databricks Apps workspace SSO sets X-Forwarded-Email; pin serve to trust ONLY that",
|
|
2030
2313
|
" // header so an authenticated caller cannot impersonate another user via a spoofed",
|
|
2031
2314
|
" // X-Agent-User header (Databricks does not know about or strip that header).",
|
|
2032
|
-
"
|
|
2315
|
+
" // Skipped when AGENTHIPPO_PIN_IDENTITY=0 (bootstrap escape hatch) — same idea as",
|
|
2316
|
+
" // Cloud Run's ENABLE_IAP=0: pinning to an absent header would make every request",
|
|
2317
|
+
" // anonymous instead of leaving body-supplied identity usable during bootstrap.",
|
|
2318
|
+
" ...(process.env.AGENTHIPPO_PIN_IDENTITY === '0' ? {} : { AGENTHIPPO_IDENTITY_HEADER: 'x-forwarded-email' }),",
|
|
2033
2319
|
"} });",
|
|
2034
2320
|
"child.on('exit', (code) => process.exit(code ?? 0));",
|
|
2035
2321
|
'',
|
|
@@ -2054,6 +2340,7 @@ function databricksAppArtifacts(ctx) {
|
|
|
2054
2340
|
'> spike (serve-v3 §13.2). Without it serve queries as one app identity. Identity',
|
|
2055
2341
|
'> attribution (`user_id`) works today; per-user *data* enforcement needs the spike.',
|
|
2056
2342
|
'',
|
|
2343
|
+
...renderEgressNote(ctx),
|
|
2057
2344
|
].join('\n'),
|
|
2058
2345
|
};
|
|
2059
2346
|
}
|
|
@@ -2135,7 +2422,10 @@ function cloudflareArtifacts(ctx) {
|
|
|
2135
2422
|
migrations: [{ tag: 'v1', new_sqlite_classes: ['Agent'] }],
|
|
2136
2423
|
}, null, 2) + '\n';
|
|
2137
2424
|
return {
|
|
2138
|
-
|
|
2425
|
+
// Pin identity to X-Agent-User so serve trusts ONLY the header the fronting Worker sets —
|
|
2426
|
+
// unpinned, serve falls back to the same header name as an alias and a caller who reaches
|
|
2427
|
+
// the container directly (bypassing the Worker/Access) could spoof it themselves.
|
|
2428
|
+
'Dockerfile': sharedDockerfile(ctx, { extraEnv: { AGENTHIPPO_IDENTITY_HEADER: 'x-agent-user' } }),
|
|
2139
2429
|
'wrangler.jsonc': wrangler,
|
|
2140
2430
|
'README.md': [
|
|
2141
2431
|
`# Deploy: ${ctx.packName} — Cloudflare Container (T1-generic)`,
|
|
@@ -2145,6 +2435,537 @@ function cloudflareArtifacts(ctx) {
|
|
|
2145
2435
|
'then proxies to the container binding. For the *external/unauthenticated* case',
|
|
2146
2436
|
'(customer support), use the full reference at `examples/deploy/external-support/`.',
|
|
2147
2437
|
'',
|
|
2438
|
+
'**Identity pin:** the container trusts *only* `X-Agent-User` (`AGENTHIPPO_IDENTITY_HEADER`,',
|
|
2439
|
+
'baked into the image) — the Worker must set it after Cloudflare Access verifies the user.',
|
|
2440
|
+
'Requests without it resolve to no identity, even if the container is ever reachable',
|
|
2441
|
+
'directly, bypassing the Worker.',
|
|
2442
|
+
'',
|
|
2443
|
+
'**Egress:** this container only needs outbound access to your model provider',
|
|
2444
|
+
'(e.g. `api.anthropic.com:443`) — restrict it with a Cloudflare Worker/Zero Trust',
|
|
2445
|
+
'egress policy if your org requires one.',
|
|
2446
|
+
'',
|
|
2447
|
+
].join('\n'),
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* AWS ECS Fargate (T1-generic, `d8-aws-adapter`). Fargate's `awsvpc` network mode shares one
|
|
2452
|
+
* task's network namespace across its containers — architecturally identical to Cloud Run's
|
|
2453
|
+
* sidecar model, so the edge/edge-fed Caddyfiles and `EDGE_AGENT_PORT` convention are reused
|
|
2454
|
+
* verbatim (PLAN/deploy.md §5, "what adding AWS looks like"): no new identity code, just a new
|
|
2455
|
+
* platform adapter around the same shared identity layer.
|
|
2456
|
+
*
|
|
2457
|
+
* Scope: identity/exposure only (bare / edge / edge-fed) — no Spotlight/LiteLLM sidecar variant
|
|
2458
|
+
* yet (that's the orthogonal model-gateway layer, not part of this pass). Newer and far less
|
|
2459
|
+
* exercised than the Cloud Run path — read `deploy.sh` before running it against a real account.
|
|
2460
|
+
*/
|
|
2461
|
+
function awsEcsArtifacts(ctx) {
|
|
2462
|
+
const fed = ctx.ingress === 'edge-fed';
|
|
2463
|
+
const edge = ctx.ingress === 'edge' || fed;
|
|
2464
|
+
const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
|
|
2465
|
+
if (!edge) {
|
|
2466
|
+
// Bare single-container task — sits behind whatever ingress (ALB, API Gateway, internal
|
|
2467
|
+
// VPC access) the org already runs, same positioning as cloudflare/databricks-app/spcs.
|
|
2468
|
+
const taskDef = {
|
|
2469
|
+
family: '${SERVICE}',
|
|
2470
|
+
networkMode: 'awsvpc',
|
|
2471
|
+
requiresCompatibilities: ['FARGATE'],
|
|
2472
|
+
cpu: '512',
|
|
2473
|
+
memory: '1024',
|
|
2474
|
+
containerDefinitions: [
|
|
2475
|
+
{
|
|
2476
|
+
name: 'agent',
|
|
2477
|
+
image: '${IMAGE}',
|
|
2478
|
+
essential: true,
|
|
2479
|
+
portMappings: [{ containerPort: ctx.port, protocol: 'tcp' }],
|
|
2480
|
+
environment: [{ name: 'AGENTHIPPO_NO_AUTO_INSTALL', value: '1' }],
|
|
2481
|
+
secrets: [{ name: providerKeyEnv, valueFrom: '${PROVIDER_KEY_SECRET_ARN}' }],
|
|
2482
|
+
logConfiguration: {
|
|
2483
|
+
logDriver: 'awslogs',
|
|
2484
|
+
options: {
|
|
2485
|
+
'awslogs-group': '/ecs/${SERVICE}',
|
|
2486
|
+
'awslogs-region': '${REGION}',
|
|
2487
|
+
'awslogs-stream-prefix': 'agent',
|
|
2488
|
+
},
|
|
2489
|
+
},
|
|
2490
|
+
},
|
|
2491
|
+
],
|
|
2492
|
+
};
|
|
2493
|
+
const deploy = [
|
|
2494
|
+
'#!/usr/bin/env bash',
|
|
2495
|
+
'# T1-generic: AWS ECS Fargate, single container — bring your own ingress (ALB/API',
|
|
2496
|
+
'# Gateway/internal VPC). For a self-contained ALB + AgentHippo edge, use --ingress edge',
|
|
2497
|
+
'# or --ingress edge-fed instead.',
|
|
2498
|
+
'set -euo pipefail',
|
|
2499
|
+
...anchorToStagingRoot(),
|
|
2500
|
+
'SERVICE="' + ctx.packName + '"',
|
|
2501
|
+
'REGION="${AWS_REGION:-us-east-1}"',
|
|
2502
|
+
'ACCOUNT_ID="${ACCOUNT_ID:-$(aws sts get-caller-identity --query Account --output text)}"',
|
|
2503
|
+
'REPO="agenthippo/${SERVICE}"',
|
|
2504
|
+
'IMAGE="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPO}:latest"',
|
|
2505
|
+
'',
|
|
2506
|
+
'aws ecr describe-repositories --repository-names "$REPO" --region "$REGION" >/dev/null 2>&1 \\',
|
|
2507
|
+
' || aws ecr create-repository --repository-name "$REPO" --region "$REGION"',
|
|
2508
|
+
'aws ecr get-login-password --region "$REGION" \\',
|
|
2509
|
+
' | docker login --username AWS --password-stdin "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com"',
|
|
2510
|
+
'',
|
|
2511
|
+
'docker build --platform linux/amd64 -f deploy/Dockerfile -t "$IMAGE" .',
|
|
2512
|
+
'docker push "$IMAGE"',
|
|
2513
|
+
'',
|
|
2514
|
+
`PROVIDER_KEY_SECRET_ARN="\${PROVIDER_KEY_SECRET_ARN:?set PROVIDER_KEY_SECRET_ARN — Secrets Manager ARN holding ${providerKeyEnv}}"`,
|
|
2515
|
+
'TASK_JSON="$(sed -e "s|\\${IMAGE}|${IMAGE}|g" -e "s|\\${SERVICE}|${SERVICE}|g" \\',
|
|
2516
|
+
' -e "s|\\${REGION}|${REGION}|g" -e "s|\\${PROVIDER_KEY_SECRET_ARN}|${PROVIDER_KEY_SECRET_ARN}|g" \\',
|
|
2517
|
+
' deploy/task-definition.json)"',
|
|
2518
|
+
'aws ecs register-task-definition --region "$REGION" --cli-input-json "$TASK_JSON" >/dev/null',
|
|
2519
|
+
'',
|
|
2520
|
+
'CLUSTER="${CLUSTER:?set CLUSTER — the ECS cluster to run in}"',
|
|
2521
|
+
'aws ecs describe-services --cluster "$CLUSTER" --services "$SERVICE" --region "$REGION" 2>/dev/null \\',
|
|
2522
|
+
' | grep -q \'"status": "ACTIVE"\' \\',
|
|
2523
|
+
' && aws ecs update-service --cluster "$CLUSTER" --service "$SERVICE" \\',
|
|
2524
|
+
' --task-definition "$SERVICE" --region "$REGION" >/dev/null \\',
|
|
2525
|
+
' || aws ecs create-service --cluster "$CLUSTER" --service-name "$SERVICE" \\',
|
|
2526
|
+
' --task-definition "$SERVICE" --desired-count 1 --launch-type FARGATE \\',
|
|
2527
|
+
' --network-configuration "awsvpcConfiguration={subnets=[${SUBNETS:?set SUBNETS}],securityGroups=[${SECURITY_GROUPS:?set SECURITY_GROUPS}],assignPublicIp=ENABLED}" \\',
|
|
2528
|
+
' --region "$REGION" >/dev/null',
|
|
2529
|
+
'echo "Deployed $SERVICE to ECS cluster $CLUSTER"',
|
|
2530
|
+
'',
|
|
2531
|
+
].join('\n');
|
|
2532
|
+
return {
|
|
2533
|
+
'Dockerfile': sharedDockerfile(ctx),
|
|
2534
|
+
'task-definition.json': JSON.stringify(taskDef, null, 2) + '\n',
|
|
2535
|
+
'deploy.sh': deploy,
|
|
2536
|
+
'README.md': [
|
|
2537
|
+
`# Deploy: ${ctx.packName} — AWS ECS Fargate (T1-generic)`,
|
|
2538
|
+
'',
|
|
2539
|
+
`Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}.`,
|
|
2540
|
+
'',
|
|
2541
|
+
'Single container, no ingress opinion — front it with whatever your org already runs',
|
|
2542
|
+
'(ALB, API Gateway, internal VPC access). Use `--ingress edge` or `--ingress edge-fed`',
|
|
2543
|
+
'for a self-contained ALB + AgentHippo edge instead.',
|
|
2544
|
+
'',
|
|
2545
|
+
'```bash',
|
|
2546
|
+
'export CLUSTER=<ecs-cluster> SUBNETS=<subnet-ids> SECURITY_GROUPS=<sg-ids>',
|
|
2547
|
+
'export PROVIDER_KEY_SECRET_ARN=<Secrets Manager ARN for your model provider key>',
|
|
2548
|
+
'bash deploy/deploy.sh',
|
|
2549
|
+
'```',
|
|
2550
|
+
'',
|
|
2551
|
+
'> ⚠️ Newer and far less exercised than the Cloud Run target — read `deploy.sh` before',
|
|
2552
|
+
'> running it against a real account.',
|
|
2553
|
+
'',
|
|
2554
|
+
...renderEgressNote(ctx),
|
|
2555
|
+
].join('\n'),
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
// Edge / edge-fed: Caddy + (self-issued: oauth2-proxy) + auth-broker + agent, one Fargate
|
|
2559
|
+
// task (awsvpc network mode = shared localhost, exactly like Cloud Run sidecars) — reuses
|
|
2560
|
+
// the identical Caddyfiles Cloud Run already renders.
|
|
2561
|
+
const containers = [
|
|
2562
|
+
{
|
|
2563
|
+
name: 'caddy',
|
|
2564
|
+
image: 'caddy:2.8-alpine',
|
|
2565
|
+
essential: true,
|
|
2566
|
+
portMappings: [{ containerPort: 8080, protocol: 'tcp' }],
|
|
2567
|
+
environment: [{ name: 'AGENTHIPPO_PUBLIC_HOSTNAME', value: ctx.publicHostname }],
|
|
2568
|
+
// Caddyfile is written to a mounted config volume by the deploy script (sed-templated,
|
|
2569
|
+
// same "checked-in template, script substitutes it" pattern as the Cloud Run edge).
|
|
2570
|
+
mountPoints: [{ sourceVolume: 'caddy-config', containerPath: '/etc/caddy' }],
|
|
2571
|
+
logConfiguration: {
|
|
2572
|
+
logDriver: 'awslogs',
|
|
2573
|
+
options: { 'awslogs-group': '/ecs/${SERVICE}', 'awslogs-region': '${REGION}', 'awslogs-stream-prefix': 'caddy' },
|
|
2574
|
+
},
|
|
2575
|
+
},
|
|
2576
|
+
...(fed ? [] : [{
|
|
2577
|
+
name: 'oauth2-proxy',
|
|
2578
|
+
image: 'quay.io/oauth2-proxy/oauth2-proxy:v7.7.1',
|
|
2579
|
+
essential: true,
|
|
2580
|
+
environment: [
|
|
2581
|
+
{ name: 'OAUTH2_PROXY_HTTP_ADDRESS', value: '0.0.0.0:4180' },
|
|
2582
|
+
{ name: 'OAUTH2_PROXY_UPSTREAMS', value: 'file:///dev/null' },
|
|
2583
|
+
{ name: 'OAUTH2_PROXY_SET_XAUTHREQUEST', value: 'true' },
|
|
2584
|
+
{ name: 'OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER', value: 'true' },
|
|
2585
|
+
{ name: 'OAUTH2_PROXY_SKIP_PROVIDER_BUTTON', value: 'true' },
|
|
2586
|
+
{ name: 'OAUTH2_PROXY_REVERSE_PROXY', value: 'true' },
|
|
2587
|
+
],
|
|
2588
|
+
secrets: [
|
|
2589
|
+
{ name: 'OAUTH2_PROXY_CLIENT_ID', valueFrom: '${OAUTH_CLIENT_ID_SECRET_ARN}' },
|
|
2590
|
+
{ name: 'OAUTH2_PROXY_CLIENT_SECRET', valueFrom: '${OAUTH_CLIENT_SECRET_SECRET_ARN}' },
|
|
2591
|
+
{ name: 'OAUTH2_PROXY_COOKIE_SECRET', valueFrom: '${OAUTH_COOKIE_SECRET_SECRET_ARN}' },
|
|
2592
|
+
],
|
|
2593
|
+
logConfiguration: {
|
|
2594
|
+
logDriver: 'awslogs',
|
|
2595
|
+
options: { 'awslogs-group': '/ecs/${SERVICE}', 'awslogs-region': '${REGION}', 'awslogs-stream-prefix': 'oauth2-proxy' },
|
|
2596
|
+
},
|
|
2597
|
+
}]),
|
|
2598
|
+
{
|
|
2599
|
+
name: 'auth-broker',
|
|
2600
|
+
image: '${IMAGE}',
|
|
2601
|
+
essential: true,
|
|
2602
|
+
command: ['auth-broker'],
|
|
2603
|
+
environment: [
|
|
2604
|
+
{ name: 'AGENTHIPPO_AUTH_PORT', value: '9780' },
|
|
2605
|
+
...(fed ? [
|
|
2606
|
+
{ name: 'AGENTHIPPO_AUTH_COOKIE_DOMAIN', value: ctx.cookieDomain || '' },
|
|
2607
|
+
{ name: 'AGENTHIPPO_AUTH_IDP_URL', value: ctx.idpUrl || '' },
|
|
2608
|
+
] : []),
|
|
2609
|
+
],
|
|
2610
|
+
secrets: [{ name: 'AGENTHIPPO_AUTH_JWT_SECRET', valueFrom: '${AUTH_JWT_SECRET_SECRET_ARN}' }],
|
|
2611
|
+
logConfiguration: {
|
|
2612
|
+
logDriver: 'awslogs',
|
|
2613
|
+
options: { 'awslogs-group': '/ecs/${SERVICE}', 'awslogs-region': '${REGION}', 'awslogs-stream-prefix': 'auth-broker' },
|
|
2614
|
+
},
|
|
2615
|
+
},
|
|
2616
|
+
{
|
|
2617
|
+
name: 'agent',
|
|
2618
|
+
image: '${IMAGE}',
|
|
2619
|
+
essential: true,
|
|
2620
|
+
command: serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: identity_1.EDGE_AGENT_PORT }),
|
|
2621
|
+
environment: [{ name: 'AGENTHIPPO_NO_AUTO_INSTALL', value: '1' }],
|
|
2622
|
+
secrets: [{ name: providerKeyEnv, valueFrom: '${PROVIDER_KEY_SECRET_ARN}' }],
|
|
2623
|
+
logConfiguration: {
|
|
2624
|
+
logDriver: 'awslogs',
|
|
2625
|
+
options: { 'awslogs-group': '/ecs/${SERVICE}', 'awslogs-region': '${REGION}', 'awslogs-stream-prefix': 'agent' },
|
|
2626
|
+
},
|
|
2627
|
+
},
|
|
2628
|
+
];
|
|
2629
|
+
const taskDef = {
|
|
2630
|
+
family: '${SERVICE}',
|
|
2631
|
+
networkMode: 'awsvpc',
|
|
2632
|
+
requiresCompatibilities: ['FARGATE'],
|
|
2633
|
+
cpu: '1024',
|
|
2634
|
+
memory: '2048',
|
|
2635
|
+
volumes: [{ name: 'caddy-config' }],
|
|
2636
|
+
containerDefinitions: containers,
|
|
2637
|
+
};
|
|
2638
|
+
const secretArgs = fed
|
|
2639
|
+
? 'PROVIDER_KEY_SECRET_ARN=<...> AUTH_JWT_SECRET_SECRET_ARN=<...>'
|
|
2640
|
+
: 'PROVIDER_KEY_SECRET_ARN=<...> AUTH_JWT_SECRET_SECRET_ARN=<...> ' +
|
|
2641
|
+
'OAUTH_CLIENT_ID_SECRET_ARN=<...> OAUTH_CLIENT_SECRET_SECRET_ARN=<...> OAUTH_COOKIE_SECRET_SECRET_ARN=<...>';
|
|
2642
|
+
const deploy = [
|
|
2643
|
+
'#!/usr/bin/env bash',
|
|
2644
|
+
`# AWS ECS Fargate + AgentHippo edge (${fed ? 'edge-fed: federated identity, no oauth2-proxy' : 'edge: self-issued'}).`,
|
|
2645
|
+
'# One-time setup (not per deploy): create an ALB + target group (protocol HTTP, port 8080,',
|
|
2646
|
+
'# target type ip) pointed at this service, and an HTTPS listener forwarding to it — ALB',
|
|
2647
|
+
'# terminates TLS, same as Cloud Run\'s edge path (Caddy listens on plain HTTP behind it).',
|
|
2648
|
+
'set -euo pipefail',
|
|
2649
|
+
...anchorToStagingRoot(),
|
|
2650
|
+
'SERVICE="' + ctx.packName + '"',
|
|
2651
|
+
'REGION="${AWS_REGION:-us-east-1}"',
|
|
2652
|
+
'ACCOUNT_ID="${ACCOUNT_ID:-$(aws sts get-caller-identity --query Account --output text)}"',
|
|
2653
|
+
'REPO="agenthippo/${SERVICE}"',
|
|
2654
|
+
'IMAGE="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPO}:latest"',
|
|
2655
|
+
'',
|
|
2656
|
+
'aws ecr describe-repositories --repository-names "$REPO" --region "$REGION" >/dev/null 2>&1 \\',
|
|
2657
|
+
' || aws ecr create-repository --repository-name "$REPO" --region "$REGION"',
|
|
2658
|
+
'aws ecr get-login-password --region "$REGION" \\',
|
|
2659
|
+
' | docker login --username AWS --password-stdin "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com"',
|
|
2660
|
+
'',
|
|
2661
|
+
'# auth-broker reuses the agent image (CLI ships `auth-broker`).',
|
|
2662
|
+
'docker build --platform linux/amd64 -f deploy/Dockerfile -t "$IMAGE" .',
|
|
2663
|
+
'docker push "$IMAGE"',
|
|
2664
|
+
'',
|
|
2665
|
+
'PROVIDER_KEY_SECRET_ARN="${PROVIDER_KEY_SECRET_ARN:?set PROVIDER_KEY_SECRET_ARN}"',
|
|
2666
|
+
'AUTH_JWT_SECRET_SECRET_ARN="${AUTH_JWT_SECRET_SECRET_ARN:?set AUTH_JWT_SECRET_SECRET_ARN}"',
|
|
2667
|
+
...(fed ? [] : [
|
|
2668
|
+
'OAUTH_CLIENT_ID_SECRET_ARN="${OAUTH_CLIENT_ID_SECRET_ARN:?set OAUTH_CLIENT_ID_SECRET_ARN}"',
|
|
2669
|
+
'OAUTH_CLIENT_SECRET_SECRET_ARN="${OAUTH_CLIENT_SECRET_SECRET_ARN:?set OAUTH_CLIENT_SECRET_SECRET_ARN}"',
|
|
2670
|
+
'OAUTH_COOKIE_SECRET_SECRET_ARN="${OAUTH_COOKIE_SECRET_SECRET_ARN:?set OAUTH_COOKIE_SECRET_SECRET_ARN}"',
|
|
2671
|
+
]),
|
|
2672
|
+
'TASK_JSON="$(sed -e "s|\\${IMAGE}|${IMAGE}|g" -e "s|\\${SERVICE}|${SERVICE}|g" -e "s|\\${REGION}|${REGION}|g" \\',
|
|
2673
|
+
' -e "s|\\${PROVIDER_KEY_SECRET_ARN}|${PROVIDER_KEY_SECRET_ARN}|g" \\',
|
|
2674
|
+
' -e "s|\\${AUTH_JWT_SECRET_SECRET_ARN}|${AUTH_JWT_SECRET_SECRET_ARN}|g" \\',
|
|
2675
|
+
...(fed ? [] : [
|
|
2676
|
+
' -e "s|\\${OAUTH_CLIENT_ID_SECRET_ARN}|${OAUTH_CLIENT_ID_SECRET_ARN}|g" \\',
|
|
2677
|
+
' -e "s|\\${OAUTH_CLIENT_SECRET_SECRET_ARN}|${OAUTH_CLIENT_SECRET_SECRET_ARN}|g" \\',
|
|
2678
|
+
' -e "s|\\${OAUTH_COOKIE_SECRET_SECRET_ARN}|${OAUTH_COOKIE_SECRET_SECRET_ARN}|g" \\',
|
|
2679
|
+
]),
|
|
2680
|
+
' deploy/task-definition.json)"',
|
|
2681
|
+
'aws ecs register-task-definition --region "$REGION" --cli-input-json "$TASK_JSON" >/dev/null',
|
|
2682
|
+
'',
|
|
2683
|
+
'CLUSTER="${CLUSTER:?set CLUSTER}"',
|
|
2684
|
+
'TARGET_GROUP_ARN="${TARGET_GROUP_ARN:?set TARGET_GROUP_ARN — from the one-time ALB setup above}"',
|
|
2685
|
+
'aws ecs describe-services --cluster "$CLUSTER" --services "$SERVICE" --region "$REGION" 2>/dev/null \\',
|
|
2686
|
+
' | grep -q \'"status": "ACTIVE"\' \\',
|
|
2687
|
+
' && aws ecs update-service --cluster "$CLUSTER" --service "$SERVICE" \\',
|
|
2688
|
+
' --task-definition "$SERVICE" --region "$REGION" >/dev/null \\',
|
|
2689
|
+
' || aws ecs create-service --cluster "$CLUSTER" --service-name "$SERVICE" \\',
|
|
2690
|
+
' --task-definition "$SERVICE" --desired-count 1 --launch-type FARGATE \\',
|
|
2691
|
+
' --load-balancers "targetGroupArn=$TARGET_GROUP_ARN,containerName=caddy,containerPort=8080" \\',
|
|
2692
|
+
' --network-configuration "awsvpcConfiguration={subnets=[${SUBNETS:?set SUBNETS}],securityGroups=[${SECURITY_GROUPS:?set SECURITY_GROUPS}],assignPublicIp=ENABLED}" \\',
|
|
2693
|
+
' --region "$REGION" >/dev/null',
|
|
2694
|
+
'echo "Deployed $SERVICE behind the AgentHippo edge on ECS cluster $CLUSTER"',
|
|
2695
|
+
'',
|
|
2696
|
+
].join('\n');
|
|
2697
|
+
return {
|
|
2698
|
+
'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
|
|
2699
|
+
'www/index.html': renderLandingPage(ctx.packName),
|
|
2700
|
+
'Caddyfile': fed ? (0, identity_1.renderSidecarFedCaddyfile)(ctx.idpUrl) : (0, identity_1.renderSidecarEdgeCaddyfile)(),
|
|
2701
|
+
'task-definition.json': JSON.stringify(taskDef, null, 2) + '\n',
|
|
2702
|
+
'deploy.sh': deploy,
|
|
2703
|
+
'README.md': [
|
|
2704
|
+
`# Deploy: ${ctx.packName} — AWS ECS Fargate + AgentHippo edge (${fed ? 'edge-fed' : 'edge'})`,
|
|
2705
|
+
'',
|
|
2706
|
+
`Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}.`,
|
|
2707
|
+
'',
|
|
2708
|
+
'One Fargate task, containers share the task\'s localhost (`awsvpc` network mode) —',
|
|
2709
|
+
'the same sidecar shape Cloud Run uses, so the Caddyfile is identical:',
|
|
2710
|
+
'- **caddy** (ingress, the only published port) → ' + (fed
|
|
2711
|
+
? 'verifies the shared AgentHippo JWT'
|
|
2712
|
+
: 'SSO edge: oauth2-proxy for browsers, auth-broker for IDE Bearer'),
|
|
2713
|
+
`- **agent** → serves www + /v1/* on localhost:${identity_1.EDGE_AGENT_PORT} (not published)`,
|
|
2714
|
+
'- **auth-broker** → validates IDE AgentHippo JWTs (`/api/ide/me`)' + (fed ? ' against the shared IdP secret' : ''),
|
|
2715
|
+
...(fed ? [] : ['- **oauth2-proxy** → Google SSO for browser clients']),
|
|
2716
|
+
'',
|
|
2717
|
+
'**One-time setup (not per deploy):** an ALB + HTTPS listener + target group (HTTP, port',
|
|
2718
|
+
'8080, target type `ip`) pointed at this service — ALB terminates TLS, Caddy stays HTTP.',
|
|
2719
|
+
'',
|
|
2720
|
+
'```bash',
|
|
2721
|
+
`export CLUSTER=<ecs-cluster> SUBNETS=<subnet-ids> SECURITY_GROUPS=<sg-ids> TARGET_GROUP_ARN=<from ALB setup>`,
|
|
2722
|
+
`export ${secretArgs}`,
|
|
2723
|
+
'bash deploy/deploy.sh',
|
|
2724
|
+
'```',
|
|
2725
|
+
'',
|
|
2726
|
+
'> ⚠️ Newer and far less exercised than the Cloud Run target — read `deploy.sh` before',
|
|
2727
|
+
'> running it against a real account.',
|
|
2728
|
+
'',
|
|
2729
|
+
...renderEgressNote(ctx),
|
|
2730
|
+
].join('\n'),
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
/**
|
|
2734
|
+
* Azure Container Apps (T1-generic, `d9-azure-adapter`). One Container App's containers run
|
|
2735
|
+
* as a single replica sharing localhost — the same sidecar shape as Cloud Run/ECS Fargate, so
|
|
2736
|
+
* the edge/edge-fed Caddyfiles and `EDGE_AGENT_PORT` convention are reused verbatim again
|
|
2737
|
+
* (PLAN/deploy.md §5). Container Apps environments provide built-in ingress + TLS termination,
|
|
2738
|
+
* so unlike aws-ecs there's no separate one-time load-balancer setup — `--target-port` on the
|
|
2739
|
+
* container app itself is the whole ingress story.
|
|
2740
|
+
*
|
|
2741
|
+
* Scope: identity/exposure only (bare / edge / edge-fed), same as aws-ecs — no Spotlight/LiteLLM
|
|
2742
|
+
* sidecar variant yet. Newer and far less exercised than the Cloud Run path.
|
|
2743
|
+
*/
|
|
2744
|
+
function azureContainerAppsArtifacts(ctx) {
|
|
2745
|
+
const fed = ctx.ingress === 'edge-fed';
|
|
2746
|
+
const edge = ctx.ingress === 'edge' || fed;
|
|
2747
|
+
const providerKeyEnv = litellmProviderKeyEnv(ctx.model);
|
|
2748
|
+
const PORT = 8080;
|
|
2749
|
+
if (!edge) {
|
|
2750
|
+
const spec = {
|
|
2751
|
+
properties: {
|
|
2752
|
+
configuration: {
|
|
2753
|
+
ingress: { external: true, targetPort: ctx.port },
|
|
2754
|
+
secrets: [{ name: 'provider-key', value: '${PROVIDER_KEY}' }],
|
|
2755
|
+
},
|
|
2756
|
+
template: {
|
|
2757
|
+
containers: [
|
|
2758
|
+
{
|
|
2759
|
+
name: 'agent',
|
|
2760
|
+
image: '${IMAGE}',
|
|
2761
|
+
env: [
|
|
2762
|
+
{ name: 'AGENTHIPPO_NO_AUTO_INSTALL', value: '1' },
|
|
2763
|
+
{ name: providerKeyEnv, secretRef: 'provider-key' },
|
|
2764
|
+
],
|
|
2765
|
+
resources: { cpu: 0.5, memory: '1Gi' },
|
|
2766
|
+
},
|
|
2767
|
+
],
|
|
2768
|
+
},
|
|
2769
|
+
},
|
|
2770
|
+
};
|
|
2771
|
+
const deploy = [
|
|
2772
|
+
'#!/usr/bin/env bash',
|
|
2773
|
+
'# T1-generic: Azure Container Apps, single container — built-in ingress + TLS, no separate',
|
|
2774
|
+
'# load balancer to stand up. For a self-contained AgentHippo edge, use --ingress edge or',
|
|
2775
|
+
'# --ingress edge-fed instead.',
|
|
2776
|
+
'set -euo pipefail',
|
|
2777
|
+
...anchorToStagingRoot(),
|
|
2778
|
+
'SERVICE="' + ctx.packName + '"',
|
|
2779
|
+
'RESOURCE_GROUP="${RESOURCE_GROUP:?set RESOURCE_GROUP}"',
|
|
2780
|
+
'ENVIRONMENT="${CONTAINERAPPS_ENVIRONMENT:?set CONTAINERAPPS_ENVIRONMENT — az containerapp env create (one-time)}"',
|
|
2781
|
+
'ACR_NAME="${ACR_NAME:?set ACR_NAME — an Azure Container Registry (one-time: az acr create)}"',
|
|
2782
|
+
'IMAGE="${ACR_NAME}.azurecr.io/${SERVICE}:latest"',
|
|
2783
|
+
`PROVIDER_KEY="\${PROVIDER_KEY:?set PROVIDER_KEY — your ${providerKeyEnv}}"`,
|
|
2784
|
+
'',
|
|
2785
|
+
'# Builds in ACR (no local docker login/push step needed).',
|
|
2786
|
+
'az acr build --registry "$ACR_NAME" --image "${SERVICE}:latest" --file deploy/Dockerfile .',
|
|
2787
|
+
'',
|
|
2788
|
+
'az containerapp show --name "$SERVICE" --resource-group "$RESOURCE_GROUP" >/dev/null 2>&1 \\',
|
|
2789
|
+
' && az containerapp update --name "$SERVICE" --resource-group "$RESOURCE_GROUP" \\',
|
|
2790
|
+
' --image "$IMAGE" >/dev/null \\',
|
|
2791
|
+
' || az containerapp create --name "$SERVICE" --resource-group "$RESOURCE_GROUP" \\',
|
|
2792
|
+
' --environment "$ENVIRONMENT" --image "$IMAGE" \\',
|
|
2793
|
+
' --target-port ' + ctx.port + ' --ingress external \\',
|
|
2794
|
+
' --secrets "provider-key=$PROVIDER_KEY" \\',
|
|
2795
|
+
` --env-vars "AGENTHIPPO_NO_AUTO_INSTALL=1" "${providerKeyEnv}=secretref:provider-key" \\`,
|
|
2796
|
+
' >/dev/null',
|
|
2797
|
+
'echo "Deployed $SERVICE to Container Apps environment $ENVIRONMENT"',
|
|
2798
|
+
'',
|
|
2799
|
+
].join('\n');
|
|
2800
|
+
return {
|
|
2801
|
+
'Dockerfile': sharedDockerfile(ctx),
|
|
2802
|
+
'containerapp.json': JSON.stringify(spec, null, 2) + '\n',
|
|
2803
|
+
'deploy.sh': deploy,
|
|
2804
|
+
'README.md': [
|
|
2805
|
+
`# Deploy: ${ctx.packName} — Azure Container Apps (T1-generic)`,
|
|
2806
|
+
'',
|
|
2807
|
+
`Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}.`,
|
|
2808
|
+
'',
|
|
2809
|
+
'Single container, built-in ingress + TLS — no separate load balancer to stand up.',
|
|
2810
|
+
'Use `--ingress edge` or `--ingress edge-fed` for a self-contained AgentHippo edge instead.',
|
|
2811
|
+
'',
|
|
2812
|
+
'```bash',
|
|
2813
|
+
'export RESOURCE_GROUP=<rg> CONTAINERAPPS_ENVIRONMENT=<env> ACR_NAME=<registry>',
|
|
2814
|
+
`export PROVIDER_KEY=<your ${providerKeyEnv}>`,
|
|
2815
|
+
'bash deploy/deploy.sh',
|
|
2816
|
+
'```',
|
|
2817
|
+
'',
|
|
2818
|
+
'> ⚠️ Newer and far less exercised than the Cloud Run target — read `deploy.sh` before',
|
|
2819
|
+
'> running it against a real account.',
|
|
2820
|
+
'',
|
|
2821
|
+
...renderEgressNote(ctx),
|
|
2822
|
+
].join('\n'),
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
// Edge / edge-fed: Caddy + (self-issued: oauth2-proxy) + auth-broker + agent, one Container
|
|
2826
|
+
// App (containers in one replica share localhost, same sidecar shape as Cloud Run/ECS) —
|
|
2827
|
+
// reuses the identical Caddyfiles Cloud Run already renders.
|
|
2828
|
+
const containers = [
|
|
2829
|
+
{
|
|
2830
|
+
name: 'caddy',
|
|
2831
|
+
image: 'caddy:2.8-alpine',
|
|
2832
|
+
env: [{ name: 'PORT', value: String(PORT) }, { name: 'AGENTHIPPO_PUBLIC_HOSTNAME', value: ctx.publicHostname }],
|
|
2833
|
+
resources: { cpu: 0.25, memory: '0.5Gi' },
|
|
2834
|
+
},
|
|
2835
|
+
...(fed ? [] : [{
|
|
2836
|
+
name: 'oauth2-proxy',
|
|
2837
|
+
image: 'quay.io/oauth2-proxy/oauth2-proxy:v7.7.1',
|
|
2838
|
+
env: [
|
|
2839
|
+
{ name: 'OAUTH2_PROXY_HTTP_ADDRESS', value: '0.0.0.0:4180' },
|
|
2840
|
+
{ name: 'OAUTH2_PROXY_UPSTREAMS', value: 'file:///dev/null' },
|
|
2841
|
+
{ name: 'OAUTH2_PROXY_SET_XAUTHREQUEST', value: 'true' },
|
|
2842
|
+
{ name: 'OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER', value: 'true' },
|
|
2843
|
+
{ name: 'OAUTH2_PROXY_SKIP_PROVIDER_BUTTON', value: 'true' },
|
|
2844
|
+
{ name: 'OAUTH2_PROXY_REVERSE_PROXY', value: 'true' },
|
|
2845
|
+
{ name: 'OAUTH2_PROXY_CLIENT_ID', secretRef: 'oauth-client-id' },
|
|
2846
|
+
{ name: 'OAUTH2_PROXY_CLIENT_SECRET', secretRef: 'oauth-client-secret' },
|
|
2847
|
+
{ name: 'OAUTH2_PROXY_COOKIE_SECRET', secretRef: 'oauth-cookie-secret' },
|
|
2848
|
+
],
|
|
2849
|
+
resources: { cpu: 0.25, memory: '0.5Gi' },
|
|
2850
|
+
}]),
|
|
2851
|
+
{
|
|
2852
|
+
name: 'auth-broker',
|
|
2853
|
+
image: '${IMAGE}',
|
|
2854
|
+
command: ['agenthippo', 'auth-broker'],
|
|
2855
|
+
env: [
|
|
2856
|
+
{ name: 'AGENTHIPPO_AUTH_PORT', value: '9780' },
|
|
2857
|
+
{ name: 'AGENTHIPPO_AUTH_JWT_SECRET', secretRef: 'auth-jwt-secret' },
|
|
2858
|
+
...(fed ? [
|
|
2859
|
+
{ name: 'AGENTHIPPO_AUTH_COOKIE_DOMAIN', value: ctx.cookieDomain || '' },
|
|
2860
|
+
{ name: 'AGENTHIPPO_AUTH_IDP_URL', value: ctx.idpUrl || '' },
|
|
2861
|
+
] : []),
|
|
2862
|
+
],
|
|
2863
|
+
resources: { cpu: 0.25, memory: '0.5Gi' },
|
|
2864
|
+
},
|
|
2865
|
+
{
|
|
2866
|
+
name: 'agent',
|
|
2867
|
+
image: '${IMAGE}',
|
|
2868
|
+
command: serveCmdArgs(ctx, { wwwDir: '/app/pack/deploy/www', port: identity_1.EDGE_AGENT_PORT }),
|
|
2869
|
+
env: [
|
|
2870
|
+
{ name: 'AGENTHIPPO_NO_AUTO_INSTALL', value: '1' },
|
|
2871
|
+
{ name: providerKeyEnv, secretRef: 'provider-key' },
|
|
2872
|
+
],
|
|
2873
|
+
resources: { cpu: 0.5, memory: '1Gi' },
|
|
2874
|
+
},
|
|
2875
|
+
];
|
|
2876
|
+
const spec = {
|
|
2877
|
+
properties: {
|
|
2878
|
+
configuration: {
|
|
2879
|
+
ingress: { external: true, targetPort: PORT },
|
|
2880
|
+
secrets: [
|
|
2881
|
+
{ name: 'auth-jwt-secret', value: '${AUTH_JWT_SECRET}' },
|
|
2882
|
+
{ name: 'provider-key', value: '${PROVIDER_KEY}' },
|
|
2883
|
+
...(fed ? [] : [
|
|
2884
|
+
{ name: 'oauth-client-id', value: '${OAUTH_CLIENT_ID}' },
|
|
2885
|
+
{ name: 'oauth-client-secret', value: '${OAUTH_CLIENT_SECRET}' },
|
|
2886
|
+
{ name: 'oauth-cookie-secret', value: '${OAUTH_COOKIE_SECRET}' },
|
|
2887
|
+
]),
|
|
2888
|
+
],
|
|
2889
|
+
},
|
|
2890
|
+
template: { containers },
|
|
2891
|
+
},
|
|
2892
|
+
};
|
|
2893
|
+
const secretExports = [
|
|
2894
|
+
'AUTH_JWT_SECRET="${AUTH_JWT_SECRET:?set AUTH_JWT_SECRET}"',
|
|
2895
|
+
`PROVIDER_KEY="\${PROVIDER_KEY:?set PROVIDER_KEY — your ${providerKeyEnv}}"`,
|
|
2896
|
+
...(fed ? [] : [
|
|
2897
|
+
'OAUTH_CLIENT_ID="${OAUTH_CLIENT_ID:?set OAUTH_CLIENT_ID}"',
|
|
2898
|
+
'OAUTH_CLIENT_SECRET="${OAUTH_CLIENT_SECRET:?set OAUTH_CLIENT_SECRET}"',
|
|
2899
|
+
'OAUTH_COOKIE_SECRET="${OAUTH_COOKIE_SECRET:?set OAUTH_COOKIE_SECRET}"',
|
|
2900
|
+
]),
|
|
2901
|
+
];
|
|
2902
|
+
const deploy = [
|
|
2903
|
+
'#!/usr/bin/env bash',
|
|
2904
|
+
`# Azure Container Apps + AgentHippo edge (${fed ? 'edge-fed: federated identity, no oauth2-proxy' : 'edge: self-issued'}).`,
|
|
2905
|
+
'# Container Apps environments provide built-in ingress + TLS — no separate load balancer.',
|
|
2906
|
+
'set -euo pipefail',
|
|
2907
|
+
...anchorToStagingRoot(),
|
|
2908
|
+
'SERVICE="' + ctx.packName + '"',
|
|
2909
|
+
'RESOURCE_GROUP="${RESOURCE_GROUP:?set RESOURCE_GROUP}"',
|
|
2910
|
+
'ENVIRONMENT="${CONTAINERAPPS_ENVIRONMENT:?set CONTAINERAPPS_ENVIRONMENT — az containerapp env create (one-time)}"',
|
|
2911
|
+
'ACR_NAME="${ACR_NAME:?set ACR_NAME — an Azure Container Registry (one-time: az acr create)}"',
|
|
2912
|
+
'IMAGE="${ACR_NAME}.azurecr.io/${SERVICE}:latest"',
|
|
2913
|
+
'',
|
|
2914
|
+
'# auth-broker reuses the agent image (CLI ships `auth-broker`). Builds in ACR.',
|
|
2915
|
+
'az acr build --registry "$ACR_NAME" --image "${SERVICE}:latest" --file deploy/Dockerfile .',
|
|
2916
|
+
'',
|
|
2917
|
+
...secretExports,
|
|
2918
|
+
'SPEC_JSON="$(sed -e "s|\\${IMAGE}|${IMAGE}|g" -e "s|\\${AUTH_JWT_SECRET}|${AUTH_JWT_SECRET}|g" \\',
|
|
2919
|
+
' -e "s|\\${PROVIDER_KEY}|${PROVIDER_KEY}|g" \\',
|
|
2920
|
+
...(fed ? [] : [
|
|
2921
|
+
' -e "s|\\${OAUTH_CLIENT_ID}|${OAUTH_CLIENT_ID}|g" -e "s|\\${OAUTH_CLIENT_SECRET}|${OAUTH_CLIENT_SECRET}|g" \\',
|
|
2922
|
+
' -e "s|\\${OAUTH_COOKIE_SECRET}|${OAUTH_COOKIE_SECRET}|g" \\',
|
|
2923
|
+
]),
|
|
2924
|
+
' deploy/containerapp.json)"',
|
|
2925
|
+
'echo "$SPEC_JSON" > /tmp/${SERVICE}-containerapp.json',
|
|
2926
|
+
'',
|
|
2927
|
+
'# `--yaml` also accepts JSON (JSON is valid YAML) — kept as .json so it\'s reviewable',
|
|
2928
|
+
'# alongside the other checked-in templates without a format switch.',
|
|
2929
|
+
'az containerapp show --name "$SERVICE" --resource-group "$RESOURCE_GROUP" >/dev/null 2>&1 \\',
|
|
2930
|
+
' && az containerapp update --name "$SERVICE" --resource-group "$RESOURCE_GROUP" \\',
|
|
2931
|
+
' --yaml /tmp/${SERVICE}-containerapp.json >/dev/null \\',
|
|
2932
|
+
' || az containerapp create --name "$SERVICE" --resource-group "$RESOURCE_GROUP" \\',
|
|
2933
|
+
' --environment "$ENVIRONMENT" --yaml /tmp/${SERVICE}-containerapp.json >/dev/null',
|
|
2934
|
+
'echo "Deployed $SERVICE behind the AgentHippo edge in $ENVIRONMENT"',
|
|
2935
|
+
'',
|
|
2936
|
+
].join('\n');
|
|
2937
|
+
return {
|
|
2938
|
+
'Dockerfile': sharedDockerfile(ctx, { wwwDir: '/app/pack/deploy/www' }),
|
|
2939
|
+
'www/index.html': renderLandingPage(ctx.packName),
|
|
2940
|
+
'Caddyfile': fed ? (0, identity_1.renderSidecarFedCaddyfile)(ctx.idpUrl) : (0, identity_1.renderSidecarEdgeCaddyfile)(),
|
|
2941
|
+
'containerapp.json': JSON.stringify(spec, null, 2) + '\n',
|
|
2942
|
+
'deploy.sh': deploy,
|
|
2943
|
+
'README.md': [
|
|
2944
|
+
`# Deploy: ${ctx.packName} — Azure Container Apps + AgentHippo edge (${fed ? 'edge-fed' : 'edge'})`,
|
|
2945
|
+
'',
|
|
2946
|
+
`Engine: **${ctx.engine}**${ctx.model ? `, model: **${ctx.model}**` : ''}.`,
|
|
2947
|
+
'',
|
|
2948
|
+
'One Container App, containers share the replica\'s localhost — the same sidecar shape',
|
|
2949
|
+
'Cloud Run/ECS Fargate use, so the Caddyfile is identical:',
|
|
2950
|
+
'- **caddy** (ingress, the only published port) → ' + (fed
|
|
2951
|
+
? 'verifies the shared AgentHippo JWT'
|
|
2952
|
+
: 'SSO edge: oauth2-proxy for browsers, auth-broker for IDE Bearer'),
|
|
2953
|
+
`- **agent** → serves www + /v1/* on localhost:${identity_1.EDGE_AGENT_PORT} (not published)`,
|
|
2954
|
+
'- **auth-broker** → validates IDE AgentHippo JWTs (`/api/ide/me`)' + (fed ? ' against the shared IdP secret' : ''),
|
|
2955
|
+
...(fed ? [] : ['- **oauth2-proxy** → Google SSO for browser clients']),
|
|
2956
|
+
'',
|
|
2957
|
+
'Built-in ingress + TLS termination — no separate load balancer to stand up.',
|
|
2958
|
+
'',
|
|
2959
|
+
'```bash',
|
|
2960
|
+
'export RESOURCE_GROUP=<rg> CONTAINERAPPS_ENVIRONMENT=<env> ACR_NAME=<registry>',
|
|
2961
|
+
...secretExports.map((l) => 'export ' + l.split('=')[0] + '=<...>'),
|
|
2962
|
+
'bash deploy/deploy.sh',
|
|
2963
|
+
'```',
|
|
2964
|
+
'',
|
|
2965
|
+
'> ⚠️ Newer and far less exercised than the Cloud Run target — read `deploy.sh` before',
|
|
2966
|
+
'> running it against a real account.',
|
|
2967
|
+
'',
|
|
2968
|
+
...renderEgressNote(ctx),
|
|
2148
2969
|
].join('\n'),
|
|
2149
2970
|
};
|
|
2150
2971
|
}
|
|
@@ -2155,6 +2976,8 @@ const RENDERERS = {
|
|
|
2155
2976
|
'databricks-app': databricksAppArtifacts,
|
|
2156
2977
|
spcs: spcsArtifacts,
|
|
2157
2978
|
cloudflare: cloudflareArtifacts,
|
|
2979
|
+
'aws-ecs': awsEcsArtifacts,
|
|
2980
|
+
'azure-container-apps': azureContainerAppsArtifacts,
|
|
2158
2981
|
};
|
|
2159
2982
|
/**
|
|
2160
2983
|
* Render every artifact for a target (relative path → file content), always
|
|
@@ -2162,18 +2985,24 @@ const RENDERERS = {
|
|
|
2162
2985
|
*/
|
|
2163
2986
|
function renderTarget(target, ctx) {
|
|
2164
2987
|
const files = RENDERERS[target](ctx);
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
//
|
|
2988
|
+
// edge-fed applies to any Caddy-based target (vm/compose/cloudrun) — federates identity
|
|
2989
|
+
// against a central IdP instead of each target running its own oauth2-proxy + Google client.
|
|
2990
|
+
const fed = ctx.ingress === 'edge-fed';
|
|
2991
|
+
// Colocated agents (state 4) ship no edge of their own at all — no Caddy, no oauth2-proxy,
|
|
2992
|
+
// no local auth-broker, so no auth secrets (.env.auth.example / DATA-ACCESS.md) either.
|
|
2993
|
+
const colocate = Boolean(ctx.colocate) && fed && (target === 'vm' || target === 'compose');
|
|
2994
|
+
const gatewayDirect = usesGatewayDirect(ctx) && target === 'cloudrun' && fed;
|
|
2995
|
+
const edge = !colocate && (target === 'vm' || target === 'compose'
|
|
2996
|
+
|| ((target === 'cloudrun' || target === 'aws-ecs' || target === 'azure-container-apps') && (ctx.ingress === 'edge' || fed)));
|
|
2169
2997
|
const hasDockerfile = Object.keys(files).some((k) => k === 'Dockerfile' || k.endsWith('/Dockerfile'));
|
|
2170
|
-
|
|
2998
|
+
const raw = {
|
|
2171
2999
|
...files,
|
|
2172
3000
|
...(hasDockerfile ? { '.dockerignore': renderDockerignore() } : {}),
|
|
2173
3001
|
'.env.example': renderEnvExample(ctx.envVars, {
|
|
2174
3002
|
edge,
|
|
2175
3003
|
engine: ctx.engine,
|
|
2176
3004
|
hostname: ctx.publicHostname,
|
|
3005
|
+
gatewayDirect,
|
|
2177
3006
|
}),
|
|
2178
3007
|
// Edge auth secrets are split out so the agent container never loads them.
|
|
2179
3008
|
...(edge ? {
|
|
@@ -2184,5 +3013,11 @@ function renderTarget(target, ctx) {
|
|
|
2184
3013
|
cookieDomain: ctx.cookieDomain,
|
|
2185
3014
|
}),
|
|
2186
3015
|
} : {}),
|
|
3016
|
+
'DATA-ACCESS.md': renderDataAccessSummary(ctx, target, edge, fed, colocate),
|
|
2187
3017
|
};
|
|
3018
|
+
const out = {};
|
|
3019
|
+
for (const [rel, content] of Object.entries(raw)) {
|
|
3020
|
+
out[rel] = withPackAttribution(ctx, rel, content);
|
|
3021
|
+
}
|
|
3022
|
+
return out;
|
|
2188
3023
|
}
|