@agenthippo-ai/agenthippo 0.1.0 → 0.2.3-dataplane

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/out/agentpack/index.js +2 -1
  2. package/out/agentpack/paths.js +23 -0
  3. package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
  4. package/out/analytics/dashboard/webview/panel.js +290 -80
  5. package/out/analytics/sessionEvents.js +21 -4
  6. package/out/auth/brokerClient.js +1 -1
  7. package/out/auth/onboardingPrompt.js +112 -0
  8. package/out/auth/provider.js +1 -1
  9. package/out/auth-broker/config.js +8 -3
  10. package/out/auth-broker/pkce.js +3 -1
  11. package/out/auth-broker/server.js +43 -1
  12. package/out/auth-broker/store.js +6 -5
  13. package/out/cli/commands/ask.js +3 -0
  14. package/out/cli/commands/chat.js +3 -0
  15. package/out/cli/commands/deploy.js +49 -10
  16. package/out/cli/commands/gateway.js +54 -0
  17. package/out/cli/commands/idp.js +85 -0
  18. package/out/cli/commands/serve.js +401 -49
  19. package/out/cli/credential-context.js +47 -69
  20. package/out/cli/data-plane/broker.js +78 -0
  21. package/out/cli/data-plane/connector-host.js +121 -0
  22. package/out/cli/data-plane/egress.js +189 -0
  23. package/out/cli/data-plane/gateway.js +195 -0
  24. package/out/cli/data-plane/index.js +137 -0
  25. package/out/cli/data-plane/registry.js +80 -0
  26. package/out/cli/deploy/blueprints/registry.js +84 -0
  27. package/out/cli/deploy/layers/identity.js +91 -0
  28. package/out/cli/deploy-init.js +1156 -285
  29. package/out/cli/idp-init.js +303 -0
  30. package/out/cli/index.js +46 -8
  31. package/out/cli/pack-loader.js +37 -2
  32. package/out/cli/runtime.js +10 -3
  33. package/out/cli/serve-hardening.js +217 -12
  34. package/out/cli/state-sync.js +318 -29
  35. package/out/engines/claude.js +26 -5
  36. package/out/engines/codex.js +24 -5
  37. package/out/engines/custom.js +46 -12
  38. package/out/extension.js +2 -0
  39. package/out/gateway/outboundSend.js +6 -6
  40. package/out/headless/index.js +24 -6
  41. package/out/live-probe/connector.mjs +8 -0
  42. package/out/live-probe/env-report.mjs +8 -0
  43. package/out/mcp.js +88 -61
  44. package/out/remote/handleRemoteChat.js +7 -1
  45. package/out/remote/serveClient.js +27 -13
  46. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +14 -0
  47. package/out/shared/models.js +1 -1
  48. package/out/shared/providers.js +5 -2
  49. package/out/test/analytics.test.js +93 -0
  50. package/out/test/auth-broker.test.js +59 -2
  51. package/out/test/blueprint-registry.test.js +105 -0
  52. package/out/test/credential-context.test.js +55 -0
  53. package/out/test/custom-env-layering.test.js +151 -0
  54. package/out/test/data-plane-engine-wiring.test.js +250 -0
  55. package/out/test/data-plane.test.js +244 -0
  56. package/out/test/deploy-init.test.js +287 -5
  57. package/out/test/egress-broker.test.js +172 -0
  58. package/out/test/fleetDisplayUtils.test.js +4 -0
  59. package/out/test/idp-init.test.js +151 -0
  60. package/out/test/mcp-obo.test.js +52 -27
  61. package/out/test/mcp.test.js +60 -4
  62. package/out/test/model-routing.test.js +133 -0
  63. package/out/test/serve-gateway-turn.test.js +267 -0
  64. package/out/test/serve-hardening.test.js +187 -0
  65. package/out/test/serveClient.test.js +37 -2
  66. package/out/test/state-sync-peer.test.js +83 -1
  67. package/out/utils.js +27 -11
  68. package/package.json +3 -4
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderIdpArtifacts = renderIdpArtifacts;
7
+ /**
8
+ * `agenthippo idp init` — generator for the central IdP stack (PLAN/deploy.md, "customer 0").
9
+ *
10
+ * Why this exists: the org IdP used to be static files checked into this source repo
11
+ * (enterprise-deploy/idp/), which meant the actual identity routes could drift from
12
+ * renderIdentityLayer() (fixed once already) AND — the bigger problem — an enterprise
13
+ * customer standing up their own IdP had no way to get any of it at all, since they only
14
+ * receive the compiled `agenthippo` binary, never this repo. Generating it here, from the
15
+ * same shared identity/auth-env/Dockerfile renderers every `deploy init` target already
16
+ * uses, fixes both: it ships in the binary, and it can never drift from what agents expect.
17
+ *
18
+ * AgentHippo's own auth.agenthippo.ai instance is "customer 0" — stood up with this exact
19
+ * command, not a bespoke internal setup.
20
+ */
21
+ const identity_1 = require("./deploy/layers/identity");
22
+ const deploy_init_1 = require("./deploy-init");
23
+ function resolveCookieDomain(ctx) {
24
+ if (ctx.cookieDomain?.trim()) {
25
+ return ctx.cookieDomain.trim();
26
+ }
27
+ const parts = ctx.hostname.split('.');
28
+ return parts.length > 2 ? `.${parts.slice(-2).join('.')}` : `.${ctx.hostname}`;
29
+ }
30
+ function renderIdpComposeYaml() {
31
+ return [
32
+ '# Central IdP. See PLAN/deploy-runbook.md scenario E/F.',
33
+ '#',
34
+ '# ./run.sh',
35
+ '#',
36
+ '# Env split (matches `agenthippo deploy init`):',
37
+ '# ~/.agent-hippo/.env (+ optional ./.env) — hostname only',
38
+ '# ~/.agent-hippo/.env.auth (+ optional ./.env.auth) — JWT/OAuth; broker + oauth2-proxy ONLY',
39
+ '# Profiles: edge (SSO stack), tunnel (Cloudflare → Caddy)',
40
+ '# Org identity infra only — no agent runs here. Per-agent edges are generated separately',
41
+ '# by `agenthippo deploy init` and deployed on their own host/service.',
42
+ 'services:',
43
+ ' oauth2-proxy:',
44
+ ' profiles: ["edge"]',
45
+ ' image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1',
46
+ ' env_file:',
47
+ ' - path: ${HOME}/.agent-hippo/.env',
48
+ ' required: false',
49
+ ' - path: .env',
50
+ ' required: false',
51
+ ' - path: ${HOME}/.agent-hippo/.env.auth',
52
+ ' required: false',
53
+ ' - path: .env.auth',
54
+ ' required: false',
55
+ ' environment:',
56
+ ' OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180',
57
+ ' OAUTH2_PROXY_UPSTREAMS: file:///dev/null',
58
+ ' OAUTH2_PROXY_SET_XAUTHREQUEST: "true"',
59
+ ' OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER: "true"',
60
+ ' OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true"',
61
+ ' OAUTH2_PROXY_REVERSE_PROXY: "true"',
62
+ ' expose: ["4180"]',
63
+ '',
64
+ ' auth-broker:',
65
+ ' profiles: ["edge"]',
66
+ ' build:',
67
+ ' context: .',
68
+ ' dockerfile: Dockerfile.auth-broker',
69
+ ' env_file:',
70
+ ' - path: ${HOME}/.agent-hippo/.env',
71
+ ' required: false',
72
+ ' - path: .env',
73
+ ' required: false',
74
+ ' - path: ${HOME}/.agent-hippo/.env.auth',
75
+ ' required: false',
76
+ ' - path: .env.auth',
77
+ ' required: false',
78
+ ' environment:',
79
+ ' AGENTHIPPO_AUTH_PORT: "9780"',
80
+ ' AGENTHIPPO_AUTH_DATA_DIR: /data',
81
+ ' # Proxy hops in front of the broker: 1 = Caddy only (edge profile), 2 = Cloudflare',
82
+ ' # Tunnel → Caddy (run.sh sets 2; at 1 the rate limiter would key every tunnel',
83
+ ' # client on cloudflared\'s IP — one shared bucket).',
84
+ ' AGENTHIPPO_XFF_TRUSTED_HOPS: ${AGENTHIPPO_XFF_TRUSTED_HOPS:-1}',
85
+ ' volumes: ["auth-broker-data:/data"]',
86
+ ' expose: ["9780"]',
87
+ '',
88
+ ' caddy:',
89
+ ' profiles: ["edge"]',
90
+ ' image: caddy:2.8-alpine',
91
+ ' env_file:',
92
+ ' - path: ${HOME}/.agent-hippo/.env',
93
+ ' required: false',
94
+ ' - path: .env',
95
+ ' required: false',
96
+ ' # With the tunnel profile, set AGENTHIPPO_CADDY_BIND=127.0.0.1 so a public-IP host',
97
+ ' # cannot be hit directly on 80/443, bypassing Cloudflare.',
98
+ ' # Bare-metal / BYO nginx or ALB already on 80/443? Set AGENTHIPPO_CADDY_HTTP_PORT /',
99
+ ' # AGENTHIPPO_CADDY_HTTPS_PORT to alternate host ports and point your own reverse proxy',
100
+ ' # at them — the container still listens on 80 internally, only the host mapping moves.',
101
+ ' ports:',
102
+ ' - "${AGENTHIPPO_CADDY_BIND:-0.0.0.0}:${AGENTHIPPO_CADDY_HTTP_PORT:-80}:80"',
103
+ ' - "${AGENTHIPPO_CADDY_BIND:-0.0.0.0}:${AGENTHIPPO_CADDY_HTTPS_PORT:-443}:443"',
104
+ ' volumes:',
105
+ ' - ./Caddyfile:/etc/caddy/Caddyfile:ro',
106
+ ' # Colocated agents (deploy init --colocate) drop a route snippet here — see Caddyfile.',
107
+ ' - ./agents:/etc/caddy/agents:ro',
108
+ ' networks: [default, colocate]',
109
+ ' depends_on: [oauth2-proxy, auth-broker]',
110
+ '',
111
+ ' cloudflared:',
112
+ ' profiles: ["tunnel"]',
113
+ ' image: cloudflare/cloudflared:2024.12.2',
114
+ ' restart: unless-stopped',
115
+ ' command: tunnel --config /etc/cloudflared/config.yml run',
116
+ ' volumes:',
117
+ ' - ./cloudflared.yml:/etc/cloudflared/config.yml:ro',
118
+ ' # Set by run.sh from its argument (./run.sh /path/to/tunnel-credentials.json);',
119
+ ' # falls back to the conventional location for a direct `docker compose` invocation.',
120
+ ' - ${AGENTHIPPO_TUNNEL_CREDENTIALS:-${HOME}/.agent-hippo/tunnel-credentials.json}:/etc/cloudflared/credentials.json:ro',
121
+ ' depends_on: [caddy]',
122
+ '',
123
+ 'volumes:',
124
+ ' auth-broker-data:',
125
+ '',
126
+ 'networks:',
127
+ ' default:',
128
+ ' # Stable, explicitly-named network (not the implicit "idp_default") so a colocated agent\'s',
129
+ ' # own compose project can join it reliably regardless of which directory `docker compose`',
130
+ ' # runs from. Only Caddy needs it — its per-agent snippets `reverse_proxy` straight to the',
131
+ ' # agent container by name; auth-broker is reached over the (already-shared) default network.',
132
+ ' colocate:',
133
+ ' name: agenthippo-idp-colocate',
134
+ '',
135
+ ].join('\n');
136
+ }
137
+ function renderIdpCaddyfile() {
138
+ const identityLines = (0, identity_1.renderIdentityLayer)({
139
+ mode: 'self-issued',
140
+ oauth2ProxyAddr: 'oauth2-proxy:4180',
141
+ authBrokerAddr: 'auth-broker:9780',
142
+ healthProxy: [' reverse_proxy auth-broker:9780'],
143
+ // No agent behind the IdP: a Bearer request to /v1/* doesn't exist here (404, not a
144
+ // silent pass-through), and an unauthenticated browser sees a plain confirmation once
145
+ // past the SSO gate.
146
+ agentProxy: [' respond "not_found" 404'],
147
+ catchallBody: [' respond "AgentHippo IdP" 200'],
148
+ });
149
+ return [
150
+ '# SSO edge + Cloudflare Tunnel. Set AGENTHIPPO_PUBLIC_HOSTNAME in .env (Caddy reads it below).',
151
+ '# TLS terminates at Cloudflare; origin listens on HTTP only.',
152
+ '{',
153
+ ' auto_https off',
154
+ '}',
155
+ '',
156
+ '# Same-host colocation: agents deployed with `deploy init --ingress edge-fed --colocate`',
157
+ '# on THIS host ship a route snippet instead of their own Caddy/oauth2-proxy/auth-broker.',
158
+ '# Drop it in ./agents/<name>.caddy (mounted in docker-compose.yml) and',
159
+ '# `docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile` to pick it up —',
160
+ '# no rebuild, no restart of the IdP itself.',
161
+ 'import agents/*.caddy',
162
+ '',
163
+ `http://{$AGENTHIPPO_PUBLIC_HOSTNAME} {`,
164
+ ...identityLines,
165
+ '}',
166
+ '',
167
+ ].join('\n');
168
+ }
169
+ /** Render every artifact for `agenthippo idp init` (relative path → file content). Pure: no fs writes. */
170
+ function renderIdpArtifacts(ctx) {
171
+ const cookieDomain = resolveCookieDomain(ctx);
172
+ return {
173
+ 'docker-compose.yml': renderIdpComposeYaml(),
174
+ 'Dockerfile.auth-broker': (0, deploy_init_1.renderAuthBrokerDockerfile)(),
175
+ 'Caddyfile': renderIdpCaddyfile(),
176
+ '.env.example': [
177
+ '# Caddy hostname for the central IdP. No agent runs on this stack — auth secrets:',
178
+ '# see .env.auth.example → ~/.agent-hippo/.env.auth (broker + oauth2-proxy only).',
179
+ '',
180
+ `AGENTHIPPO_PUBLIC_HOSTNAME=${ctx.hostname}`,
181
+ '',
182
+ '# Proxy hops / Caddy bind (optional; run.sh sets XFF hops=2 for tunnel)',
183
+ '# AGENTHIPPO_XFF_TRUSTED_HOPS=1',
184
+ '# AGENTHIPPO_CADDY_BIND=0.0.0.0',
185
+ '',
186
+ '# Bare metal / BYO nginx or ALB already on 80/443? Rebind Caddy to alternate host ports',
187
+ '# and point your reverse proxy at them (container still listens on 80 internally):',
188
+ '# AGENTHIPPO_CADDY_HTTP_PORT=8080',
189
+ '# AGENTHIPPO_CADDY_HTTPS_PORT=8443',
190
+ '',
191
+ ].join('\n'),
192
+ '.env.auth.example': (0, deploy_init_1.renderAuthEnvExample)({ hostname: ctx.hostname, fed: false, cookieDomain }),
193
+ 'cloudflared.yml.example': [
194
+ '# Copy to cloudflared.yml; the credentials JSON lives in ~/.agent-hippo/ (never in the repo).',
195
+ '# cloudflared tunnel create agenthippo-auth',
196
+ '# cp ~/.cloudflared/<tunnel-id>.json ~/.agent-hippo/tunnel-credentials.json',
197
+ `# cloudflared tunnel route dns agenthippo-auth ${ctx.hostname}`,
198
+ 'tunnel: REPLACE_WITH_TUNNEL_ID',
199
+ 'credentials-file: /etc/cloudflared/credentials.json',
200
+ 'ingress:',
201
+ ` - hostname: ${ctx.hostname}`,
202
+ ' service: http://caddy:80',
203
+ ' - service: http_status:404',
204
+ '',
205
+ ].join('\n'),
206
+ 'run.sh': [
207
+ '#!/usr/bin/env bash',
208
+ '# Start central IdP: Caddy + oauth2-proxy + auth-broker (+ Cloudflare tunnel, if requested).',
209
+ '# No agent here — per-agent edges are generated separately by `agenthippo deploy init`.',
210
+ '#',
211
+ '# Usage:',
212
+ '# ./run.sh bare metal / BYO nginx or ALB in front',
213
+ '# ./run.sh /path/to/tunnel-credentials.json Cloudflare Tunnel',
214
+ '#',
215
+ '# Bare metal / BYO nginx or ALB: run with no argument — starts --profile edge only.',
216
+ '# If 80/443 are already taken by your reverse proxy, set AGENTHIPPO_CADDY_HTTP_PORT/',
217
+ '# _HTTPS_PORT in .env first so Caddy rebinds elsewhere.',
218
+ 'set -euo pipefail',
219
+ 'cd "$(dirname "$0")"',
220
+ 'if [[ $# -ge 1 ]]; then',
221
+ ' export AGENTHIPPO_TUNNEL_CREDENTIALS="$1"',
222
+ ' [[ -f "$AGENTHIPPO_TUNNEL_CREDENTIALS" ]] || {',
223
+ ' echo "Tunnel credentials file not found: $AGENTHIPPO_TUNNEL_CREDENTIALS" >&2; exit 1; }',
224
+ ' # Two proxy hops behind the tunnel (cloudflared → Caddy) — see docker-compose.yml.',
225
+ ' export AGENTHIPPO_XFF_TRUSTED_HOPS="${AGENTHIPPO_XFF_TRUSTED_HOPS:-2}"',
226
+ ' docker compose --profile edge --profile tunnel up -d --build',
227
+ ` echo "IdP: https://\${AGENTHIPPO_PUBLIC_HOSTNAME:-${ctx.hostname}} (via Cloudflare Tunnel)"`,
228
+ 'else',
229
+ ' echo "No tunnel credentials path given — starting --profile edge only (no tunnel)."',
230
+ ' echo "Front this host with your own nginx/ALB; see AGENTHIPPO_CADDY_HTTP_PORT/AGENTHIPPO_CADDY_BIND in .env."',
231
+ ' echo "(To use a Cloudflare Tunnel instead: ./run.sh /path/to/tunnel-credentials.json)"',
232
+ ' docker compose --profile edge up -d --build',
233
+ ` echo "IdP: http://\${AGENTHIPPO_PUBLIC_HOSTNAME:-${ctx.hostname}} (behind your own reverse proxy)"`,
234
+ 'fi',
235
+ '',
236
+ ].join('\n'),
237
+ 'agents/README.md': [
238
+ '# Colocated agent routes',
239
+ '',
240
+ 'Drop `*.caddy` route snippets here for agents deployed on **this same host** via:',
241
+ '',
242
+ '```bash',
243
+ 'agenthippo deploy init <pack> --target compose --ingress edge-fed --colocate',
244
+ '```',
245
+ '',
246
+ 'That command generates `deploy/agent-route.caddy` (copy it here, renamed to something',
247
+ 'identifying the agent) plus a `docker-compose.yml` containing only the `agent` service',
248
+ '(no Caddy/oauth2-proxy/auth-broker of its own — it joins this IdP\'s',
249
+ '`agenthippo-idp-colocate` network and rides the IdP\'s existing edge).',
250
+ '',
251
+ 'After copying a new snippet in:',
252
+ '',
253
+ '```bash',
254
+ 'docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile',
255
+ '```',
256
+ '',
257
+ 'No rebuild, no restart of the IdP itself. See `../Caddyfile`\'s `import agents/*.caddy` and',
258
+ '`PLAN/deploy-runbook.md` scenario F for the full picture.',
259
+ '',
260
+ ].join('\n'),
261
+ 'README.md': [
262
+ '# Central IdP',
263
+ '',
264
+ '**One per organization.** Hosts Google SSO (oauth2-proxy) and the IDE auth-broker only —',
265
+ 'no agent runs here. Agent edges are generated and deployed separately.',
266
+ '',
267
+ 'Generated by `agenthippo idp init` — regenerate (don\'t hand-edit `Caddyfile`, which comes',
268
+ 'straight from the same identity module every agent\'s Caddyfile uses) if you need to change',
269
+ 'routing: that lives in the CLI itself, not in this output.',
270
+ '',
271
+ 'This stack runs on **any host with Docker** — local laptop, team VM, cloud VM, or on-prem.',
272
+ '`./run.sh` picks the path below based on whether you pass it a tunnel credentials path.',
273
+ '',
274
+ '## Quick start',
275
+ '',
276
+ '```bash',
277
+ 'cp .env.example ~/.agent-hippo/.env',
278
+ 'cp .env.auth.example ~/.agent-hippo/.env.auth # fill in; never put AGENTHIPPO_AUTH_JWT_SECRET in .env',
279
+ '```',
280
+ '',
281
+ '**Cloudflare Tunnel** (no public ports to manage yourself):',
282
+ '',
283
+ '```bash',
284
+ 'cp cloudflared.yml.example cloudflared.yml # set tunnel id',
285
+ 'cloudflared tunnel create agenthippo-auth # writes ~/.cloudflared/<tunnel-id>.json',
286
+ 'cp ~/.cloudflared/<tunnel-id>.json ~/.agent-hippo/tunnel-credentials.json',
287
+ './run.sh ~/.agent-hippo/tunnel-credentials.json',
288
+ '```',
289
+ '',
290
+ '**Bare metal / BYO nginx or ALB already fronting this host:**',
291
+ '',
292
+ '```bash',
293
+ '# skip cloudflared.yml entirely. If 80/443 are already taken by your reverse proxy,',
294
+ '# set AGENTHIPPO_CADDY_HTTP_PORT/AGENTHIPPO_CADDY_HTTPS_PORT in .env first.',
295
+ './run.sh # no argument — starts --profile edge only',
296
+ '# point your nginx/ALB at this host\'s Caddy (AGENTHIPPO_CADDY_BIND / _HTTP_PORT above)',
297
+ '```',
298
+ '',
299
+ 'Agent edges (e.g. `cs.corp.example`) are separate — see `PLAN/deploy-runbook.md` scenarios E/F.',
300
+ '',
301
+ ].join('\n'),
302
+ };
303
+ }
package/out/cli/index.js CHANGED
@@ -77,6 +77,7 @@ const ask_1 = require("./commands/ask");
77
77
  const serve_1 = require("./commands/serve");
78
78
  const auth_broker_1 = require("./commands/auth-broker");
79
79
  const deploy_1 = require("./commands/deploy");
80
+ const idp_1 = require("./commands/idp");
80
81
  const pack_1 = require("./commands/pack");
81
82
  const store_1 = require("./commands/store");
82
83
  const anywhere_1 = require("./commands/anywhere");
@@ -212,7 +213,15 @@ program
212
213
  .option('--state-dir <path>', 'Persist session state here (honors AGENTHIPPO_HOME; replaces the tmpdir default)')
213
214
  .option('--max-turns-per-session <n>', 'Cap turns per session (abuse/runaway guard); 0 = unlimited', '0')
214
215
  .option('--www-dir <path>', 'Serve deploy/www/index.html at GET / (static browser chat UI)')
216
+ // Deprecated no-ops: kept so deploy artifacts generated by older CLIs (which baked these
217
+ // into the serve command line) don't crash with "unknown option" after a runtime upgrade.
218
+ // Serve now trusts its front proxy for auth/CORS; these flags no longer do anything.
219
+ .option('--production', 'Deprecated: no-op (serve trusts its front proxy; hardening moved to the deploy edge)')
220
+ .option('--cors-origin <origins>', 'Deprecated: no-op (CORS is handled by the front proxy)')
215
221
  .action(async (options) => {
222
+ if (options.production || options.corsOrigin) {
223
+ console.warn('[serve] --production/--cors-origin are deprecated no-ops; auth and CORS are enforced by the deploy edge proxy.');
224
+ }
216
225
  const verbose = program.opts().verbose;
217
226
  await (0, deps_1.ensureDeps)(verbose);
218
227
  await (0, serve_1.serveCommand)({
@@ -239,22 +248,29 @@ program
239
248
  // Deploy command - generate platform-native deploy artifacts
240
249
  program
241
250
  .command('deploy <action> [pack]')
242
- .description('Generate a self-contained deploy staging dir (init). Targets: vm, compose, cloudrun, databricks-app, spcs, cloudflare, all')
251
+ .description('Generate a self-contained deploy staging dir (init). Targets: vm, compose, cloudrun, databricks-app, spcs, cloudflare, aws-ecs, azure-container-apps, all. Or pick a --blueprint instead of raw --target/--ingress.')
243
252
  .option('-t, --target <target>', 'Deploy target', 'vm')
244
- .option('-o, --out <path>', 'Staging directory (default: ./deploy-out/<pack>-<target>)')
253
+ .option('--blueprint <id>', 'Named preset (sandbox, prospect-demo, team-pilot, regulated-air-gapped, enterprise-fleet) — resolves --target/--ingress for you (PLAN/deploy.md §3); overrides both when set')
254
+ .option('--platform <platform>', 'Which platform to use within --blueprint\'s supported set (defaults to the blueprint\'s first); ignored without --blueprint')
255
+ .option('-o, --out <path>', 'Staging directory (default: ./deploy-out/<pack>-<target> relative to cwd; run from the pack parent, not from inside the pack)')
245
256
  .option('--packdir <path>', 'Pack directory (defaults to [pack] arg or cwd)')
246
- .option('--image <ref>', 'Runtime image ref', 'ghcr.io/agenthippo/runtime:latest')
257
+ .option('--image <ref>', 'Runtime image ref (default: engine runtime:latest, or :latest-cursor for cursor packs)')
247
258
  .option('-p, --port <port>', 'Container port', '8080')
248
259
  .option('--gateway <channels>', 'Agent Anywhere: bind channels (e.g. "slack") + add OpenClaw gateway service (compose target)')
249
260
  .option('--hostname <host>', 'Public hostname for SSO edge (Caddy, OAuth, tunnel)', 'auth.agenthippo.ai')
250
- .option('--ingress <mode>', 'Cloud Run ingress: iap (built-in IAP), edge (per-service AgentHippo edge + Google OAuth), or edge-fed (federated: auth via auth.agenthippo.ai, no per-service OAuth)', 'iap')
261
+ .option('--ingress <mode>', 'iap (Cloud Run built-in IAP only), edge (self-issued AgentHippo edge + Google OAuth — vm/compose/cloudrun/aws-ecs/azure-container-apps), or edge-fed (federated: auth via the central IdP, no per-target OAuth client — same targets as edge)', 'iap')
251
262
  .option('--idp-url <url>', 'edge-fed: central IdP base URL browsers redirect to for login (e.g. https://auth.agenthippo.ai)', 'https://auth.agenthippo.ai')
252
263
  .option('--cookie-domain <domain>', 'edge-fed: parent domain the shared auth cookie is scoped to (e.g. .agenthippo.ai)', '.agenthippo.ai')
264
+ .option('--colocate', 'edge-fed + vm/compose only: ride the org IdP\'s already-running Caddy + auth-broker instead of shipping this agent\'s own (PLAN/deploy.md §4 state 4)')
265
+ .option('--model-routing <mode>', 'Cloud Run + edge-fed: sidecar (in-pod LiteLLM+OTEL, default) or gateway (agent → gateway.agenthippo.ai directly, lean)')
253
266
  .option('--force', 'Overwrite existing files')
254
267
  .action(async (action, pack, options) => {
255
268
  const ingress = options.ingress === 'edge' ? 'edge' : options.ingress === 'edge-fed' ? 'edge-fed' : 'iap';
269
+ const modelRouting = options.modelRouting === 'gateway' ? 'gateway' : options.modelRouting === 'sidecar' ? 'sidecar' : undefined;
256
270
  await (0, deploy_1.deployCommand)(action, pack, {
257
271
  target: options.target,
272
+ blueprint: options.blueprint,
273
+ platform: options.platform,
258
274
  out: options.out,
259
275
  image: options.image,
260
276
  gateway: options.gateway,
@@ -265,6 +281,24 @@ program
265
281
  ingress,
266
282
  idpUrl: options.idpUrl,
267
283
  cookieDomain: options.cookieDomain,
284
+ colocate: Boolean(options.colocate),
285
+ modelRouting,
286
+ });
287
+ });
288
+ // IdP command - generate the central IdP stack (org identity infra, set up once)
289
+ program
290
+ .command('idp <action>')
291
+ .description('Generate the central IdP stack (init) — Caddy + oauth2-proxy + auth-broker. One per organization; agents federate against it with `deploy init --ingress edge-fed`.')
292
+ .option('--hostname <host>', 'Public hostname the IdP is reachable at (e.g. auth.corp.example) — required')
293
+ .option('--cookie-domain <domain>', 'Parent domain the shared auth cookie is scoped to (e.g. .corp.example); defaults to the hostname\'s own domain')
294
+ .option('-o, --out <path>', 'Output directory (default: ./enterprise-deploy/idp)')
295
+ .option('--force', 'Overwrite existing files')
296
+ .action(async (action, options) => {
297
+ await (0, idp_1.idpCommand)(action, {
298
+ hostname: options.hostname,
299
+ cookieDomain: options.cookieDomain,
300
+ out: options.out,
301
+ force: Boolean(options.force),
268
302
  });
269
303
  });
270
304
  // Pack command - pack management
@@ -318,8 +352,12 @@ const gatewayCmd = program.command('gateway').description('Gateway outbound mess
318
352
  gatewayCmd
319
353
  .command('send')
320
354
  .description('Send a proactive message via the gateway (WebSocket RPC; hippoclaw CLI fallback)')
321
- .requiredOption('--outbound <path>', 'Session gateway-outbound.json (see AGENTHIPPO_GATEWAY_OUTBOUND_FILE)')
322
- .requiredOption('-a, --agent <id>', 'Agent pack id')
355
+ // --outbound/--agent are optional: under `serve` with the egress broker active
356
+ // (AGENTHIPPO_EGRESS_URL + AGENTHIPPO_TURN_NONCE set), the broker resolves the destination
357
+ // server-side from the turn nonce and neither is needed (secure-obo-design §7.6). Off-serve
358
+ // the direct path still requires both (validated in gatewaySendCommand).
359
+ .option('--outbound <path>', 'Session gateway-outbound.json (see AGENTHIPPO_GATEWAY_OUTBOUND_FILE)')
360
+ .option('-a, --agent <id>', 'Agent pack id')
323
361
  .requiredOption('-m, --message <text>', 'Message to send')
324
362
  .option('--media <path-or-url>', 'Attach media (image/audio/video/document); local path or URL. Sent via gateway RPC so the bot token resolves from the gateway runtime snapshot.')
325
363
  .option('-t, --target <target>', 'Explicit target (e.g. channel:C01234567); defaults to last inbound route')
@@ -328,8 +366,8 @@ gatewayCmd
328
366
  .option('--json', 'Output handle + result as JSON')
329
367
  .action(async (options) => {
330
368
  await (0, gateway_1.gatewaySendCommand)({
331
- outbound: options.outbound,
332
- agent: options.agent,
369
+ outbound: options.outbound ?? '',
370
+ agent: options.agent ?? '',
333
371
  message: options.message,
334
372
  media: options.media,
335
373
  target: options.target,
@@ -42,6 +42,7 @@ exports.loadAgentPack = loadAgentPack;
42
42
  exports.getDefaultConfig = getDefaultConfig;
43
43
  exports.getEnvDefaultModel = getEnvDefaultModel;
44
44
  exports.getApiConfig = getApiConfig;
45
+ exports.hasCredentialForEngine = hasCredentialForEngine;
45
46
  exports.getFileAccess = getFileAccess;
46
47
  /**
47
48
  * Agent Pack Loader
@@ -330,6 +331,14 @@ function validateManifest(manifest, sourcePath) {
330
331
  if (manifest.spec?.cheapModel !== undefined && typeof manifest.spec.cheapModel !== 'string') {
331
332
  errors.push('Invalid spec.cheapModel: must be a string when provided');
332
333
  }
334
+ if (manifest.spec?.dataPlane !== undefined) {
335
+ if (typeof manifest.spec.dataPlane !== 'object' || !manifest.spec.dataPlane.connector?.trim()) {
336
+ errors.push('Invalid spec.dataPlane: requires a `connector` path (stdio MCP module run broker-side)');
337
+ }
338
+ if (manifest.spec.dataPlane?.provider !== undefined && typeof manifest.spec.dataPlane.provider !== 'string') {
339
+ errors.push('Invalid spec.dataPlane.provider: must be a string when provided');
340
+ }
341
+ }
333
342
  if (errors.length > 0) {
334
343
  throw new Error(`Invalid agent manifest at ${sourcePath}:\n - ${errors.join('\n - ')}`);
335
344
  }
@@ -354,20 +363,46 @@ function getEnvDefaultModel() {
354
363
  }
355
364
  /**
356
365
  * Get API configuration from environment.
366
+ *
367
+ * Returns the credentials keyed by destination (litellm / openai / anthropic) rather than a
368
+ * single collapsed key. The engine selects the one matching the endpoint it dials — see
369
+ * resolveApiRouting — so a codex deployment can no longer send an Anthropic key to OpenAI.
357
370
  */
358
371
  function getApiConfig() {
359
372
  const litellmApiKey = getEnvVar('AGENTHIPPO_LITELLM_API_KEY', 'AGENTIDE_LITELLM_API_KEY');
360
373
  const litellmBaseUrl = getEnvVar('AGENTHIPPO_LITELLM_BASE_URL', 'AGENTIDE_LITELLM_BASE_URL');
361
374
  const anthropicApiKey = process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY;
362
375
  const openaiApiKey = process.env.OPENAI_API_KEY;
363
- const apiKey = litellmApiKey || anthropicApiKey || openaiApiKey;
364
376
  const useLiteLLM = Boolean(litellmApiKey && litellmBaseUrl);
365
377
  return {
366
- apiKey,
378
+ // Kept only for the "is any credential present at all?" boot check; routing never uses it.
379
+ apiKey: litellmApiKey || anthropicApiKey || openaiApiKey,
367
380
  baseUrl: litellmBaseUrl,
368
381
  useLiteLLM,
382
+ litellmApiKey,
383
+ openaiApiKey,
384
+ anthropicApiKey,
369
385
  };
370
386
  }
387
+ /**
388
+ * Does the environment carry a usable credential for `engine`? Engine-aware so a codex
389
+ * deployment with only an Anthropic key (or vice-versa) fails loudly at boot instead of
390
+ * 401-ing at the first model call. A configured LiteLLM proxy satisfies every engine.
391
+ */
392
+ function hasCredentialForEngine(engine, cfg = getApiConfig()) {
393
+ if (cfg.useLiteLLM) {
394
+ return true;
395
+ }
396
+ const e = engine.trim().toLowerCase();
397
+ if (e === 'claude') {
398
+ return Boolean(cfg.anthropicApiKey);
399
+ }
400
+ if (e === 'codex') {
401
+ return Boolean(cfg.openaiApiKey);
402
+ }
403
+ // Custom engines can authenticate via their own manifest-declared env var — don't gate them here.
404
+ return true;
405
+ }
371
406
  /**
372
407
  * Get file access mode from pack manifest.
373
408
  */
@@ -315,9 +315,16 @@ function buildCliEnvPatch(args) {
315
315
  chatSessionId: args.chatSessionId,
316
316
  }),
317
317
  };
318
- const gatewayToken = readOpenClawGatewayToken(args.baseEnv);
319
- if (gatewayToken) {
320
- env[exports.OPENCLAW_GATEWAY_TOKEN_ENV] = gatewayToken;
318
+ // When serve runs the egress broker (secure-obo-design §7.6), the gateway token must NOT
319
+ // reach the agent's trust domain: the broker holds it, the agent presents only the turn
320
+ // nonce. AGENTHIPPO_EGRESS_URL in the process env is that signal — with it set, never bake
321
+ // the token into a child env (this closes §7.4 gap 1 for custom engines; codex/claude are
322
+ // covered by serve stripping the token from its own process.env).
323
+ if (!process.env.AGENTHIPPO_EGRESS_URL?.trim()) {
324
+ const gatewayToken = readOpenClawGatewayToken(args.baseEnv);
325
+ if (gatewayToken) {
326
+ env[exports.OPENCLAW_GATEWAY_TOKEN_ENV] = gatewayToken;
327
+ }
321
328
  }
322
329
  // Claude SDK refuses nested sessions when CLAUDECODE is present.
323
330
  // Force-unset in child runtime for CLI ask/chat/serve subprocesses.