@agenthippo-ai/agenthippo 0.1.1 → 1.110.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/out/agentpack/index.js +2 -1
  2. package/out/agentpack/paths.js +23 -0
  3. package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
  4. package/out/analytics/dashboard/provider.js +13 -2
  5. package/out/analytics/dashboard/webview/panel.js +293 -82
  6. package/out/analytics/identity.js +0 -0
  7. package/out/analytics/service.js +23 -25
  8. package/out/analytics/sessionEvents.js +109 -22
  9. package/out/auth/authBaseUrl.js +59 -0
  10. package/out/auth/bootstrapApply.js +9 -10
  11. package/out/auth/brokerClient.js +49 -15
  12. package/out/auth/onboardingPrompt.js +112 -0
  13. package/out/auth/provider.js +9 -22
  14. package/out/auth/sessionStore.js +3 -40
  15. package/out/auth/signInFlow.js +2 -1
  16. package/out/auth-broker/config.js +54 -4
  17. package/out/auth-broker/server.js +152 -20
  18. package/out/auth-broker/store.js +25 -24
  19. package/out/authority/warrant.js +206 -0
  20. package/out/branding.js +27 -1
  21. package/out/cli/commands/ask.js +3 -0
  22. package/out/cli/commands/chat.js +3 -0
  23. package/out/cli/commands/conformance.js +249 -0
  24. package/out/cli/commands/data-plane.js +67 -0
  25. package/out/cli/commands/deploy.js +50 -10
  26. package/out/cli/commands/gateway.js +58 -0
  27. package/out/cli/commands/idp.js +85 -0
  28. package/out/cli/commands/pack.js +182 -3
  29. package/out/cli/commands/sandbox.js +332 -0
  30. package/out/cli/commands/serve.js +1165 -82
  31. package/out/cli/commands/store.js +12 -4
  32. package/out/cli/credential-context.js +42 -80
  33. package/out/cli/data-plane/broker.js +78 -0
  34. package/out/cli/data-plane/connector-host.js +121 -0
  35. package/out/cli/data-plane/egress.js +189 -0
  36. package/out/cli/data-plane/gateway.js +218 -0
  37. package/out/cli/data-plane/index.js +152 -0
  38. package/out/cli/data-plane/registry.js +89 -0
  39. package/out/cli/data-plane/remote.js +106 -0
  40. package/out/cli/data-plane/sidecar.js +161 -0
  41. package/out/cli/deploy/blueprints/registry.js +84 -0
  42. package/out/cli/deploy/layers/identity.js +172 -0
  43. package/out/cli/deploy-init.js +1182 -347
  44. package/out/cli/deps.js +106 -2
  45. package/out/cli/idp-init.js +338 -0
  46. package/out/cli/index.js +192 -23
  47. package/out/cli/pack-install-ref.js +15 -0
  48. package/out/cli/pack-loader.js +45 -3
  49. package/out/cli/pack-runtime.js +61 -0
  50. package/out/cli/runtime.js +44 -3
  51. package/out/cli/serve-hardening.js +139 -3
  52. package/out/cli/state-sync.js +16 -15
  53. package/out/cli/utils.js +12 -1
  54. package/out/engines/base.js +32 -3
  55. package/out/engines/claude.js +47 -8
  56. package/out/engines/codex.js +125 -22
  57. package/out/engines/custom.js +54 -12
  58. package/out/evidence/sessionShip.js +610 -0
  59. package/out/extension.js +13 -2
  60. package/out/gateway/bridge.js +40 -2
  61. package/out/gateway/client.js +34 -1
  62. package/out/gateway/installSource.js +68 -0
  63. package/out/gateway/lifecycle.js +185 -51
  64. package/out/gateway/outboundPaths.js +4 -1
  65. package/out/gateway/outboundSend.js +6 -6
  66. package/out/gateway/secretsPreflight.js +90 -0
  67. package/out/gateway/service.js +7 -2
  68. package/out/headless/index.js +40 -9
  69. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  70. package/out/live-probe/connector.mjs +8 -0
  71. package/out/live-probe/env-report.mjs +8 -0
  72. package/out/mcp.js +115 -67
  73. package/out/pack/caps.js +271 -0
  74. package/out/pack/governor.js +309 -0
  75. package/out/pack/lease-schema.js +270 -0
  76. package/out/pack/lease.js +640 -0
  77. package/out/pack/reconcile.js +217 -0
  78. package/out/pack/signature.js +839 -0
  79. package/out/remote/handleRemoteChat.js +44 -3
  80. package/out/remote/remoteTrust.js +277 -0
  81. package/out/remote/serveClient.js +52 -5
  82. package/out/runner/host.js +131 -0
  83. package/out/runner/protocol.js +108 -0
  84. package/out/runner/remoteEngine.js +130 -0
  85. package/out/runtime/interfaces/edit-tracker.js +2 -0
  86. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  87. package/out/sandbox/containerDriver.js +570 -0
  88. package/out/sandbox/processDriver.js +315 -0
  89. package/out/sandbox/sandboxRunner.js +191 -0
  90. package/out/sandbox/support.js +301 -0
  91. package/out/sandbox/types.js +55 -0
  92. package/out/scripts/agentide-litellm.sh +67 -5
  93. package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
  94. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -118
  95. package/out/scripts/analytics/setup-analytics.ps1 +37 -13
  96. package/out/scripts/analytics/setup-analytics.sh +93 -13
  97. package/out/scripts/analytics/start-analytics.sh +86 -14
  98. package/out/scripts/gateway/setup-gateway.sh +72 -32
  99. package/out/shared/models.js +12 -111
  100. package/out/shared/providers.js +2 -2
  101. package/out/spotlight/install.js +7 -1
  102. package/out/store/index.js +41 -10
  103. package/out/sync.js +3 -1
  104. package/out/test/analytics.test.js +198 -34
  105. package/out/test/auth-baseurl.test.js +132 -0
  106. package/out/test/auth-bootstrapApply.test.js +27 -6
  107. package/out/test/auth-broker-hardening.test.js +227 -0
  108. package/out/test/auth-broker.test.js +112 -9
  109. package/out/test/blueprint-registry.test.js +105 -0
  110. package/out/test/branding.test.js +13 -0
  111. package/out/test/caps-governor.test.js +455 -0
  112. package/out/test/codex-new-file-survival.test.js +119 -0
  113. package/out/test/configuration-scopes.test.js +131 -0
  114. package/out/test/credential-context.test.js +23 -28
  115. package/out/test/custom-env-layering.test.js +151 -0
  116. package/out/test/data-plane-engine-wiring.test.js +250 -0
  117. package/out/test/data-plane.test.js +311 -0
  118. package/out/test/deploy-init.test.js +295 -4
  119. package/out/test/egress-broker.test.js +172 -0
  120. package/out/test/fleetDisplayUtils.test.js +4 -0
  121. package/out/test/gateway-client.test.js +28 -0
  122. package/out/test/gateway-install-source.test.js +111 -0
  123. package/out/test/gateway-lifecycle.test.js +1 -1
  124. package/out/test/gateway-outbound.test.js +7 -0
  125. package/out/test/gateway-secrets-preflight.test.js +127 -0
  126. package/out/test/gateway-session-routing.test.js +18 -0
  127. package/out/test/governor-hardening.test.js +217 -0
  128. package/out/test/headless-sync.test.js +22 -0
  129. package/out/test/idp-init.test.js +151 -0
  130. package/out/test/lease-signature.test.js +357 -0
  131. package/out/test/lease-trust-root.test.js +273 -0
  132. package/out/test/lease-validation.test.js +169 -0
  133. package/out/test/lease.test.js +114 -0
  134. package/out/test/mcp-obo.test.js +52 -27
  135. package/out/test/mcp.test.js +69 -3
  136. package/out/test/model-routing.test.js +133 -0
  137. package/out/test/pack-admission.test.js +216 -0
  138. package/out/test/pack-digest-parity.test.js +140 -0
  139. package/out/test/pack-reconcile.test.js +183 -0
  140. package/out/test/pack-signature-artifact.test.js +103 -0
  141. package/out/test/pack-signature.test.js +176 -0
  142. package/out/test/pack-trust-root.test.js +202 -0
  143. package/out/test/remote-trust.test.js +242 -0
  144. package/out/test/runner-protocol.test.js +249 -0
  145. package/out/test/sandbox-driver.test.js +553 -0
  146. package/out/test/sandbox-hardening.test.js +395 -0
  147. package/out/test/sandbox-runner.test.js +343 -0
  148. package/out/test/serve-gateway-turn.test.js +267 -0
  149. package/out/test/serve-hardening.test.js +125 -6
  150. package/out/test/serveClient.test.js +38 -5
  151. package/out/test/sessionShip.test.js +560 -0
  152. package/out/test/spotlightPublish.test.js +28 -0
  153. package/out/test/state-sync-peer.test.js +18 -5
  154. package/out/test/store-install-safe.test.js +69 -0
  155. package/out/test/tls-bypass.test.js +90 -0
  156. package/out/test/warrant.test.js +138 -0
  157. package/out/utils.js +73 -20
  158. package/package.json +1 -1
package/out/cli/index.js CHANGED
@@ -75,8 +75,10 @@ const commander_1 = require("commander");
75
75
  const chat_1 = require("./commands/chat");
76
76
  const ask_1 = require("./commands/ask");
77
77
  const serve_1 = require("./commands/serve");
78
+ const data_plane_1 = require("./commands/data-plane");
78
79
  const auth_broker_1 = require("./commands/auth-broker");
79
80
  const deploy_1 = require("./commands/deploy");
81
+ const idp_1 = require("./commands/idp");
80
82
  const pack_1 = require("./commands/pack");
81
83
  const store_1 = require("./commands/store");
82
84
  const anywhere_1 = require("./commands/anywhere");
@@ -85,6 +87,26 @@ const probe_custom_engine_1 = require("./commands/probe-custom-engine");
85
87
  const deps_1 = require("./deps");
86
88
  const branding_1 = require("../branding");
87
89
  const agentEnvFile_1 = require("../utils/agentEnvFile");
90
+ const caps_1 = require("../pack/caps");
91
+ /**
92
+ * `--max-concurrent-sandboxes`, honouring the deprecated `--max-concurrent-cells`.
93
+ *
94
+ * The current spelling wins outright when both are given; the old one is honoured with a loud
95
+ * notice rather than dropped, because dropping it would silently REMOVE a blast-radius cap from a
96
+ * deployment that believes it still has one.
97
+ */
98
+ function resolveMaxConcurrentSandboxes(options) {
99
+ const current = parseInt(options.maxConcurrentSandboxes ?? '', 10);
100
+ if (Number.isFinite(current) && current > 0) {
101
+ return current;
102
+ }
103
+ const legacy = parseInt(options.maxConcurrentCells ?? '', 10);
104
+ if (Number.isFinite(legacy) && legacy > 0) {
105
+ console.warn(`Warning: --max-concurrent-cells is DEPRECATED; use ${caps_1.DEPRECATED_CAP_SPELLINGS['--max-concurrent-cells']}. Honouring it for now.`);
106
+ return legacy;
107
+ }
108
+ return undefined;
109
+ }
88
110
  const VERSION = '1.110.8';
89
111
  const CLI_DESCRIPTION = [
90
112
  `${branding_1.AGENTIDE_BRAND_NAME} CLI - Run agent packs from the command line`,
@@ -101,14 +123,22 @@ const CLI_DESCRIPTION = [
101
123
  ' AGENTHIPPO_STORE_REPO Override store GitHub repo (owner/repo; CLI store commands)',
102
124
  '',
103
125
  'Quickstart:',
104
- ` AGENTHIPPO_LITELLM_BASE_URL="http://localhost:4000" AGENTHIPPO_LITELLM_API_KEY="dummy" AGENTHIPPO_DEFAULT_MODEL="litellm/gpt-5.3-codex" AGENTHIPPO_DEFAULT_ENGINE="codex" ${branding_1.AGENTIDE_CLI_NAME} ask "Reply with exactly: CLI_TEST_OK"`,
126
+ ` AGENTHIPPO_LITELLM_BASE_URL="http://localhost:4000" AGENTHIPPO_LITELLM_API_KEY="dummy" AGENTHIPPO_DEFAULT_MODEL="litellm/gpt-5.5" AGENTHIPPO_DEFAULT_ENGINE="codex" ${branding_1.AGENTIDE_CLI_NAME} ask "Reply with exactly: CLI_TEST_OK"`,
105
127
  '',
106
128
  'Agent Packs:',
107
129
  ` Loaded from ~/${branding_1.AGENTIDE_DIR_NAME}/agents/<name>/ or absolute/relative paths.`,
108
130
  ' Pack permissions control filesystem access scope.',
109
131
  ].join('\n');
110
132
  const program = new commander_1.Command();
111
- program.hook('preAction', async () => {
133
+ program.hook('preAction', async (_thisCommand, actionCommand) => {
134
+ // A SANDBOX must never absorb ambient credentials. Every other command merges ~/.agent-hippo/.env
135
+ // as a convenience, but doing that inside a sandbox would silently hand the sandboxed engine
136
+ // every key on the host — defeating the driver's curated env allowlist (observed live: the
137
+ // cursor engine picked up CURSOR_API_KEY this way). A sandbox receives only what its driver
138
+ // explicitly passes.
139
+ if (actionCommand?.name() === 'sandbox') {
140
+ return;
141
+ }
112
142
  await (0, agentEnvFile_1.mergeAgentHomeEnvIntoProcessEnv)();
113
143
  });
114
144
  program
@@ -125,7 +155,7 @@ program
125
155
  .description('Start interactive chat session')
126
156
  .option('-a, --agent <name>', 'Agent pack to use')
127
157
  .option('-e, --engine <id>', 'Override pack engine: claude|codex|<custom-engine-id>')
128
- .option('-m, --model <id>', 'Override pack model (e.g. litellm/gpt-4.1-nano)')
158
+ .option('-m, --model <id>', 'Override pack model (e.g. litellm/gpt-5.6-luna)')
129
159
  .option('-w, --workspace <path>', 'Workspace directory', process.cwd())
130
160
  .action(async (options) => {
131
161
  const verbose = program.opts().verbose;
@@ -144,7 +174,7 @@ program
144
174
  .description('Send a single message and exit')
145
175
  .option('-a, --agent <name>', 'Agent pack to use')
146
176
  .option('-e, --engine <id>', 'Override pack engine: claude|codex|<custom-engine-id>')
147
- .option('-m, --model <id>', 'Override pack model (e.g. litellm/gpt-4.1-nano)')
177
+ .option('-m, --model <id>', 'Override pack model (e.g. litellm/gpt-5.6-luna)')
148
178
  .option('-w, --workspace <path>', 'Workspace directory', process.cwd())
149
179
  .option('-f, --file <path>', 'Include file content in context')
150
180
  .option('--json', 'Output as JSON')
@@ -175,7 +205,7 @@ program
175
205
  .requiredOption('--prompt <message>', 'Prompt text')
176
206
  .option('--pack <name>', 'Agent pack to use')
177
207
  .option('-e, --engine <id>', 'Override pack engine: claude|codex|<custom-engine-id>')
178
- .option('-m, --model <id>', 'Override pack model (e.g. litellm/gpt-4.1-nano)')
208
+ .option('-m, --model <id>', 'Override pack model (e.g. litellm/gpt-5.6-luna)')
179
209
  .option('-w, --workspace <path>', 'Workspace directory', process.cwd())
180
210
  .option('--session-id <id>', 'Optional session id for the caller')
181
211
  .option('--system-prompt <text>', 'Optional system prompt override')
@@ -203,7 +233,7 @@ program
203
233
  .description('Start HTTP server')
204
234
  .requiredOption('-a, --agent <name>', 'Agent pack to use (required)')
205
235
  .option('-e, --engine <id>', 'Override pack engine: claude|codex|<custom-engine-id>')
206
- .option('-m, --model <id>', 'Override pack model (e.g. litellm/gpt-4.1-nano)')
236
+ .option('-m, --model <id>', 'Override pack model (e.g. litellm/gpt-5.6-luna)')
207
237
  .option('-p, --port <port>', 'Port to listen on', '3000')
208
238
  .option('--host <address>', 'Address to bind the HTTP server (default 127.0.0.1; use 0.0.0.0 in containers/behind a proxy)')
209
239
  .option('-w, --workspace <path>', 'Default workspace directory', process.cwd())
@@ -212,18 +242,52 @@ program
212
242
  .option('--state-dir <path>', 'Persist session state here (honors AGENTHIPPO_HOME; replaces the tmpdir default)')
213
243
  .option('--max-turns-per-session <n>', 'Cap turns per session (abuse/runaway guard); 0 = unlimited', '0')
214
244
  .option('--www-dir <path>', 'Serve deploy/www/index.html at GET / (static browser chat UI)')
215
- // Deprecated no-ops: kept so deploy artifacts generated by older CLIs (which baked these
216
- // into the serve command line) don't crash with "unknown option" after a runtime upgrade.
217
- // Serve now trusts its front proxy for auth/CORS; these flags no longer do anything.
218
- .option('--production', 'Deprecated: no-op (serve trusts its front proxy; hardening moved to the deploy edge)')
219
- .option('--cors-origin <origins>', 'Deprecated: no-op (CORS is handled by the front proxy)')
245
+ .option('--sandbox <mode>', 'Run each session\'s engine in a sandbox: process (tier 0, no boundary) or container (tier 2, one disposable container per session with only its declared routes). Unset = in-process (default, unchanged)')
246
+ .option('--sandbox-image <ref>', '--sandbox container: sandbox image pinned BY DIGEST (image@sha256:...)')
247
+ .option('--pull-packs', 'Fetch the version the lease pins from the store instead of only refusing others. Always verifies the signature; a fetch or verification failure keeps serving the current version')
248
+ .option('--require-signed-pack', 'Refuse to start a sandbox unless the pack is signed by a trusted key (the lease may also require this; neither side can waive the other). Now the DEFAULT — kept so existing invocations still mean something')
249
+ .option('--allow-unsigned-pack', 'LOCAL DEVELOPMENT ONLY: run a pack this instance cannot verify. Signature checking is required by default; this disables it and warns loudly on every boot. A lease that sets requireSignedPack still overrides it')
250
+ .option('--require-signed-lease', 'Refuse to act on a lease that is not signed (ES256 compact JWS) by a trusted key. Tighten-only with lease.requireSignedLease, and sticky once a signed lease has been seen')
251
+ .option('--lease-max-staleness <seconds>', 'Refuse turns after this long with no successful lease refresh, so network denial cannot hold a revoked fleet open (default 86400; 0 disables)')
252
+ .option('--sandbox-ttl <seconds>', '--sandbox: reap an idle sandbox after N seconds; the next message recreates it and resumes the session', '1800')
253
+ .option('--shared-workspace', '--sandbox container: mount ONE workspace into every session\'s sandbox (default: one per session). Sessions can then read and overwrite each other\'s files — only for a single trust domain')
254
+ // Control plane (serve-v4-clean §5). Caps are enforced by revoking a sandbox's routes, so they
255
+ // need --sandbox; a lease may impose tighter values than these, and never looser ones.
256
+ .option('--max-session-seconds <seconds>', '--sandbox: cap one session\'s wall-clock lifetime; a breach revokes or destroys its sandbox')
257
+ .option('--max-concurrent-sandboxes <n>', '--sandbox: cap live sandboxes on this instance (blast radius)')
258
+ // Deprecated pre-rename spelling. Hidden so it does not advertise itself, honoured so an
259
+ // existing deployment does not silently lose its cap, and loud so it gets migrated.
260
+ .addOption(new commander_1.Option('--max-concurrent-cells <n>', 'DEPRECATED alias for --max-concurrent-sandboxes').hideHelp())
261
+ .option('--cap-on-breach <action>', '--sandbox: what a cap breach does — revoke-inference (default) or destroy-sandbox. An unrecognised value fails CLOSED to destroy-sandbox')
262
+ .option('--lease-poll <seconds>', '--sandbox: how often to re-check the lease, so a kill switch stops RUNNING sandboxes. Worst-case kill latency is 2x this (poll + cache); an admin refresh is immediate', '30')
220
263
  .action(async (options) => {
221
- if (options.production || options.corsOrigin) {
222
- console.warn('[serve] --production/--cors-origin are deprecated no-ops; auth and CORS are enforced by the deploy edge proxy.');
223
- }
224
264
  const verbose = program.opts().verbose;
225
265
  await (0, deps_1.ensureDeps)(verbose);
226
266
  await (0, serve_1.serveCommand)({
267
+ sandbox: options.sandbox === 'container' ? 'container' : options.sandbox === 'process' ? 'process' : undefined,
268
+ sandboxImage: options.sandboxImage,
269
+ requireSignedPack: Boolean(options.requireSignedPack),
270
+ allowUnsignedPack: Boolean(options.allowUnsignedPack),
271
+ requireSignedLease: Boolean(options.requireSignedLease),
272
+ leaseMaxStalenessSeconds: options.leaseMaxStaleness !== undefined
273
+ ? (Number.isFinite(Number(options.leaseMaxStaleness)) ? Number(options.leaseMaxStaleness) : undefined)
274
+ : undefined,
275
+ pullPacks: Boolean(options.pullPacks),
276
+ sandboxTtlSeconds: parseInt(options.sandboxTtl, 10) || undefined,
277
+ sharedWorkspace: Boolean(options.sharedWorkspace),
278
+ maxSessionSeconds: parseInt(options.maxSessionSeconds, 10) || undefined,
279
+ maxConcurrentSandboxes: resolveMaxConcurrentSandboxes(options),
280
+ // Shared with the env path (pack/caps.ts) so a value that is valid in one is valid in the
281
+ // other. An unrecognised action no longer becomes `undefined` — which mergeCaps then
282
+ // defaulted to the WEAKER revoke-inference, silently downgrading what the operator typed.
283
+ capOnBreach: (() => {
284
+ const r = (0, caps_1.resolveOnBreach)(options.capOnBreach, '--cap-on-breach');
285
+ for (const w of r.warnings) {
286
+ console.warn(`Warning: ${w}`);
287
+ }
288
+ return r.action;
289
+ })(),
290
+ leasePollSeconds: parseInt(options.leasePoll, 10) || undefined,
227
291
  agent: options.agent,
228
292
  engine: options.engine,
229
293
  model: options.model,
@@ -238,6 +302,68 @@ program
238
302
  verbose,
239
303
  });
240
304
  });
305
+ // Conformance — the deployment proves its own claims (serve-v4-clean.md §11 step 5).
306
+ program
307
+ .command('conformance')
308
+ .description('Probe a deployment against its own claims: credential confinement, isolation tier, cost-cap binding, warrant integrity, rollback/kill switch. Exit 1 on any failure, 2 when no deployment was probed (--sandbox/--tier omitted) — only exit 0 means a real deployment was measured.')
309
+ .option('--sandbox <id>', 'Running sandbox container id/name — required to probe INSIDE a sandbox')
310
+ .option('--tier <n>', 'Declared isolation tier (0-3); probed, not trusted')
311
+ .option('--runtime <bin>', 'Container runtime binary', 'docker')
312
+ .option('--json', 'Emit JSON')
313
+ .action(async (options) => {
314
+ const { conformanceCommand } = await import('./commands/conformance.js');
315
+ const code = await conformanceCommand({
316
+ sandbox: options.sandbox,
317
+ tier: options.tier === undefined ? undefined : parseInt(options.tier, 10),
318
+ runtime: options.runtime,
319
+ json: Boolean(options.json),
320
+ });
321
+ process.exitCode = code;
322
+ });
323
+ // Sandbox runner — runs INSIDE a sandbox; holds no credential (serve-v4-clean.md §3).
324
+ program
325
+ .command('sandbox')
326
+ .description('Run the in-sandbox engine host (sandbox runner). Speaks the frame protocol to serve over a unix socket or stdio; holds no data or model credential.')
327
+ .requiredOption('-a, --agent <path>', 'Pack directory mounted into the sandbox')
328
+ .option('--socket <path>', 'Unix socket to listen on (container driver)')
329
+ .option('--stdio', 'Speak the protocol on stdin/stdout instead (process driver)')
330
+ // No default: it must fall back to <state-dir>/workspace inside the command, or a
331
+ // host-launched sandbox (where --state-dir is a real host path) inherits the container-image
332
+ // path and dies on mkdir.
333
+ .option('-w, --workspace <path>', 'Workspace directory (default: <state-dir>/workspace)')
334
+ .option('--state-dir <path>', 'State directory', '/state')
335
+ .option('--sandbox-id <id>', 'Sandbox id (joins evidence records)')
336
+ .action(async (options) => {
337
+ const { sandboxCommand } = await import('./commands/sandbox.js');
338
+ await sandboxCommand({
339
+ agent: options.agent,
340
+ socket: options.socket,
341
+ stdio: Boolean(options.stdio),
342
+ workspace: options.workspace,
343
+ stateDir: options.stateDir,
344
+ sandboxId: options.sandboxId,
345
+ });
346
+ });
347
+ program
348
+ .command('data-plane')
349
+ .description('Run the connector as a sidecar (holds the backend base credential; the agent container does not)')
350
+ .requiredOption('-a, --agent <name>', 'Agent pack (reads spec.dataPlane connector + provider)')
351
+ .option('-w, --workspace <path>', 'Workspace directory', process.cwd())
352
+ .option('--gateway-port <port>', 'Port the engine dials (nonce-auth MCP)', '9090')
353
+ .option('--control-port <port>', 'Port serve dials to register turns (bearer-auth)', '9091')
354
+ .option('--host <address>', 'Bind address (default 0.0.0.0 for the pod network)')
355
+ .action(async (options) => {
356
+ const verbose = program.opts().verbose;
357
+ await (0, deps_1.ensureDeps)(verbose);
358
+ await (0, data_plane_1.dataPlaneSidecarCommand)({
359
+ agent: options.agent,
360
+ workspace: options.workspace,
361
+ gatewayPort: parseInt(options.gatewayPort, 10),
362
+ controlPort: parseInt(options.controlPort, 10),
363
+ host: options.host,
364
+ verbose,
365
+ });
366
+ });
241
367
  program
242
368
  .command('auth-broker')
243
369
  .description('Start the AgentHippo auth broker (OAuth + IDE bootstrap API)')
@@ -247,22 +373,30 @@ program
247
373
  // Deploy command - generate platform-native deploy artifacts
248
374
  program
249
375
  .command('deploy <action> [pack]')
250
- .description('Generate a self-contained deploy staging dir (init). Targets: vm, compose, cloudrun, databricks-app, spcs, cloudflare, all')
376
+ .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.')
251
377
  .option('-t, --target <target>', 'Deploy target', 'vm')
252
- .option('-o, --out <path>', 'Staging directory (default: ./deploy-out/<pack>-<target>)')
378
+ .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')
379
+ .option('--platform <platform>', 'Which platform to use within --blueprint\'s supported set (defaults to the blueprint\'s first); ignored without --blueprint')
380
+ .option('-o, --out <path>', 'Staging directory (default: ./deploy-out/<pack>-<target> relative to cwd; run from the pack parent, not from inside the pack)')
253
381
  .option('--packdir <path>', 'Pack directory (defaults to [pack] arg or cwd)')
254
382
  .option('--image <ref>', 'Runtime image ref (default: engine runtime:latest, or :latest-cursor for cursor packs)')
255
383
  .option('-p, --port <port>', 'Container port', '8080')
256
384
  .option('--gateway <channels>', 'Agent Anywhere: bind channels (e.g. "slack") + add OpenClaw gateway service (compose target)')
257
385
  .option('--hostname <host>', 'Public hostname for SSO edge (Caddy, OAuth, tunnel)', 'auth.agenthippo.ai')
258
- .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')
386
+ .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')
259
387
  .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')
260
388
  .option('--cookie-domain <domain>', 'edge-fed: parent domain the shared auth cookie is scoped to (e.g. .agenthippo.ai)', '.agenthippo.ai')
389
+ .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)')
390
+ .option('--edge-turn-connector <addr>', 'EDGE turn registration (P1 fix): connector sidecar control-API address (e.g. mypack-connector:9091); the generated route registers each chat turn via forward_auth /edge/turn and copies X-Agent-Turn-Nonce — the agent container never holds the register token')
391
+ .option('--model-routing <mode>', 'Cloud Run + edge-fed: sidecar (in-pod LiteLLM+OTEL, default) or gateway (agent → gateway.agenthippo.ai directly, lean)')
261
392
  .option('--force', 'Overwrite existing files')
262
393
  .action(async (action, pack, options) => {
263
394
  const ingress = options.ingress === 'edge' ? 'edge' : options.ingress === 'edge-fed' ? 'edge-fed' : 'iap';
395
+ const modelRouting = options.modelRouting === 'gateway' ? 'gateway' : options.modelRouting === 'sidecar' ? 'sidecar' : undefined;
264
396
  await (0, deploy_1.deployCommand)(action, pack, {
265
397
  target: options.target,
398
+ blueprint: options.blueprint,
399
+ platform: options.platform,
266
400
  out: options.out,
267
401
  image: options.image,
268
402
  gateway: options.gateway,
@@ -273,21 +407,52 @@ program
273
407
  ingress,
274
408
  idpUrl: options.idpUrl,
275
409
  cookieDomain: options.cookieDomain,
410
+ colocate: Boolean(options.colocate),
411
+ modelRouting,
412
+ edgeTurnConnector: options.edgeTurnConnector,
413
+ });
414
+ });
415
+ // IdP command - generate the central IdP stack (org identity infra, set up once)
416
+ program
417
+ .command('idp <action>')
418
+ .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`.')
419
+ .option('--hostname <host>', 'Public hostname the IdP is reachable at (e.g. auth.corp.example) — required')
420
+ .option('--cookie-domain <domain>', 'Parent domain the shared auth cookie is scoped to (e.g. .corp.example); defaults to the hostname\'s own domain')
421
+ .option('-o, --out <path>', 'Output directory (default: ./enterprise-deploy/idp)')
422
+ .option('--force', 'Overwrite existing files')
423
+ .action(async (action, options) => {
424
+ await (0, idp_1.idpCommand)(action, {
425
+ hostname: options.hostname,
426
+ cookieDomain: options.cookieDomain,
427
+ out: options.out,
428
+ force: Boolean(options.force),
276
429
  });
277
430
  });
278
431
  // Pack command - pack management
279
432
  program
280
433
  .command('pack <action>')
281
- .description('Manage agent packs (list, validate, install)')
434
+ .description('Manage agent packs (list, validate, sign, verify, digest, keygen, install)')
282
435
  .argument('[name]', 'Pack name or path')
283
- .option('-o, --output <path>', 'Output path for export')
436
+ .option('-o, --output <path>', 'Output path for export, or for a generated signing key')
437
+ .option('--key <path>', 'pack sign: PEM file holding the ES256 private key')
438
+ .option('--kid <id>', 'pack sign/keygen: key id published in JWKS so verifiers can find the public half')
439
+ .option('--keys <path>', 'pack verify: JSON file of kid -> public PEM')
440
+ .option('--jwks <url>', 'pack verify: JWKS endpoint to fetch public halves from')
441
+ .option('--to <dir>', 'pack install: destination folder (flat layout for serve/deploy)')
442
+ .option('-f, --force', 'pack install: overwrite an existing folder')
284
443
  .option('-w, --workspace <path>', 'Workspace directory for local packs', process.cwd())
285
444
  .action(async (action, name, options) => {
286
445
  await (0, pack_1.packCommand)({
287
446
  action,
288
447
  name,
289
448
  output: options.output,
449
+ to: options.to,
450
+ force: options.force,
290
451
  workspace: options.workspace,
452
+ key: options.key,
453
+ kid: options.kid,
454
+ keys: options.keys,
455
+ jwks: options.jwks,
291
456
  verbose: program.opts().verbose,
292
457
  });
293
458
  });
@@ -326,8 +491,12 @@ const gatewayCmd = program.command('gateway').description('Gateway outbound mess
326
491
  gatewayCmd
327
492
  .command('send')
328
493
  .description('Send a proactive message via the gateway (WebSocket RPC; hippoclaw CLI fallback)')
329
- .requiredOption('--outbound <path>', 'Session gateway-outbound.json (see AGENTHIPPO_GATEWAY_OUTBOUND_FILE)')
330
- .requiredOption('-a, --agent <id>', 'Agent pack id')
494
+ // --outbound/--agent are optional: under `serve` with the egress broker active
495
+ // (AGENTHIPPO_EGRESS_URL + AGENTHIPPO_TURN_NONCE set), the broker resolves the destination
496
+ // server-side from the turn nonce and neither is needed (secure-obo-design §7.6). Off-serve
497
+ // the direct path still requires both (validated in gatewaySendCommand).
498
+ .option('--outbound <path>', 'Session gateway-outbound.json (see AGENTHIPPO_GATEWAY_OUTBOUND_FILE)')
499
+ .option('-a, --agent <id>', 'Agent pack id')
331
500
  .requiredOption('-m, --message <text>', 'Message to send')
332
501
  .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.')
333
502
  .option('-t, --target <target>', 'Explicit target (e.g. channel:C01234567); defaults to last inbound route')
@@ -336,8 +505,8 @@ gatewayCmd
336
505
  .option('--json', 'Output handle + result as JSON')
337
506
  .action(async (options) => {
338
507
  await (0, gateway_1.gatewaySendCommand)({
339
- outbound: options.outbound,
340
- agent: options.agent,
508
+ outbound: options.outbound ?? '',
509
+ agent: options.agent ?? '',
341
510
  message: options.message,
342
511
  media: options.media,
343
512
  target: options.target,
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parsePackInstallRef = parsePackInstallRef;
7
+ /** Parse `slug` or `slug@version` from `pack install` positional arg. */
8
+ function parsePackInstallRef(name) {
9
+ const trimmed = name.trim();
10
+ const at = trimmed.lastIndexOf('@');
11
+ if (at > 0) {
12
+ return { slug: trimmed.slice(0, at), version: trimmed.slice(at + 1) || undefined };
13
+ }
14
+ return { slug: trimmed };
15
+ }
@@ -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,53 @@ 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
- const useLiteLLM = Boolean(litellmApiKey && litellmBaseUrl);
376
+ // The URL alone defines the trust domain. When a LiteLLM/gateway URL is configured, ALL
377
+ // engine traffic routes there — so it must authenticate with the LiteLLM key (or
378
+ // resolveApiRouting's 'sk-dummy' fallback), NEVER a provider secret, and must carry the
379
+ // analytics metadata headers. Requiring key+URL here once created a silent half-state:
380
+ // URL-only routed every call to the gateway with the REAL provider key as auth and no
381
+ // metadata at all (verified live 2026-07-27 with a header-capture stub). A missing key now
382
+ // yields a clean 401 against a strict gateway instead of a leaked credential.
383
+ const useLiteLLM = Boolean(litellmBaseUrl);
365
384
  return {
366
- apiKey,
385
+ // Kept only for the "is any credential present at all?" boot check; routing never uses it.
386
+ apiKey: litellmApiKey || anthropicApiKey || openaiApiKey,
367
387
  baseUrl: litellmBaseUrl,
368
388
  useLiteLLM,
389
+ litellmApiKey,
390
+ openaiApiKey,
391
+ anthropicApiKey,
369
392
  };
370
393
  }
394
+ /**
395
+ * Does the environment carry a usable credential for `engine`? Engine-aware so a codex
396
+ * deployment with only an Anthropic key (or vice-versa) fails loudly at boot instead of
397
+ * 401-ing at the first model call. A configured LiteLLM proxy satisfies every engine.
398
+ */
399
+ function hasCredentialForEngine(engine, cfg = getApiConfig()) {
400
+ if (cfg.useLiteLLM) {
401
+ return true;
402
+ }
403
+ const e = engine.trim().toLowerCase();
404
+ if (e === 'claude') {
405
+ return Boolean(cfg.anthropicApiKey);
406
+ }
407
+ if (e === 'codex') {
408
+ return Boolean(cfg.openaiApiKey);
409
+ }
410
+ // Custom engines can authenticate via their own manifest-declared env var — don't gate them here.
411
+ return true;
412
+ }
371
413
  /**
372
414
  * Get file access mode from pack manifest.
373
415
  */
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createPackRunner = createPackRunner;
7
+ /**
8
+ * One way to stand up a `HeadlessRunner` for an agent pack.
9
+ *
10
+ * `serve` and `sandbox` run the SAME engines against the SAME pack; the only difference is where
11
+ * the process sits. Every time the sandbox re-derived a piece of that setup itself it drifted from
12
+ * serve and broke live (the hand-rolled agent.yaml parser, the engine executable paths, the
13
+ * skipped `ensureDeps`). So the wiring lives here once and both call it: engine discovery,
14
+ * bundled prompts, the child env patch, and the credential shape.
15
+ *
16
+ * What is NOT here is deliberately not shared: credentials are the caller's business. serve
17
+ * holds provider keys; a sandbox only ever gets a per-sandbox, budget-capped model token that arrives
18
+ * on the run frame. Both express that through `credentials`, and nothing in this module can
19
+ * widen it.
20
+ */
21
+ const headless_1 = require("../headless");
22
+ const runtime_1 = require("./runtime");
23
+ const utils_1 = require("./utils");
24
+ /**
25
+ * Build the runner. Engine executables are resolved here rather than by the SDKs themselves:
26
+ * the engines ship WITH agenthippo, and a runtime image does not carry the SDK's own bundled
27
+ * cli.js — without these paths every turn inside a sandbox fails with "executable not found".
28
+ */
29
+ function createPackRunner(options) {
30
+ return (0, headless_1.createHeadlessRunner)({
31
+ storagePath: options.storagePath,
32
+ analyticsSessionsDir: options.analyticsSessionsDir,
33
+ packId: options.packId,
34
+ packVersion: options.packVersion,
35
+ provenance: options.provenance,
36
+ bundledPromptsDir: (0, utils_1.findBundledBasePromptsDir)(),
37
+ apiKey: options.credentials.apiKey,
38
+ baseUrl: options.credentials.baseUrl,
39
+ useLiteLLM: options.credentials.useLiteLLM,
40
+ litellmApiKey: options.credentials.litellmApiKey,
41
+ openaiApiKey: options.credentials.openaiApiKey,
42
+ anthropicApiKey: options.credentials.anthropicApiKey,
43
+ logger: options.logger,
44
+ freshState: options.freshState ?? false,
45
+ claudeCodeExecutablePath: (0, utils_1.findClaudeCodePath)(),
46
+ codexExecutablePath: (0, utils_1.findCodexPath)(),
47
+ permissionHandler: options.permissionHandler,
48
+ // Gives engine children AGENTHIPPO_WORKSPACE_ROOT / AGENTHIPPO_ACTIVE_AGENT and puts the
49
+ // agenthippo binary on their PATH — without it a pack skill that shells out to the CLI
50
+ // cannot find it, which is exactly the kind of silent difference a sandbox must not have.
51
+ cliEnvPatch: (0, runtime_1.buildCliEnvPatch)({
52
+ cliPath: process.execPath,
53
+ workspaceRoot: options.workspaceRoot,
54
+ agentId: options.packId,
55
+ chatSessionId: options.chatSessionId,
56
+ baseEnv: process.env,
57
+ omitGatewayToken: options.omitGatewayToken,
58
+ }),
59
+ ...(options.claudeCheapModel ? { engineSettings: { claudeCheapModel: options.claudeCheapModel } } : {}),
60
+ });
61
+ }
@@ -42,6 +42,8 @@ exports.getBundledToolsHealth = getBundledToolsHealth;
42
42
  exports.getCliHealth = getCliHealth;
43
43
  exports.buildSessionEnvPatch = buildSessionEnvPatch;
44
44
  exports.buildCliEnvPatch = buildCliEnvPatch;
45
+ exports.readProvenanceEnvFromProcess = readProvenanceEnvFromProcess;
46
+ exports.buildProvenanceEnv = buildProvenanceEnv;
45
47
  exports.runCli = runCli;
46
48
  const fs = __importStar(require("fs"));
47
49
  const os = __importStar(require("os"));
@@ -315,9 +317,16 @@ function buildCliEnvPatch(args) {
315
317
  chatSessionId: args.chatSessionId,
316
318
  }),
317
319
  };
318
- const gatewayToken = readOpenClawGatewayToken(args.baseEnv);
319
- if (gatewayToken) {
320
- env[exports.OPENCLAW_GATEWAY_TOKEN_ENV] = gatewayToken;
320
+ // When serve runs the egress broker (secure-obo-design §7.6), the gateway token must NOT
321
+ // reach the agent's trust domain: the broker holds it, the agent presents only the turn
322
+ // nonce. AGENTHIPPO_EGRESS_URL in the process env is that signal — with it set, never bake
323
+ // the token into a child env (this closes §7.4 gap 1 for custom engines; codex/claude are
324
+ // covered by serve stripping the token from its own process.env).
325
+ if (!args.omitGatewayToken && !process.env.AGENTHIPPO_EGRESS_URL?.trim()) {
326
+ const gatewayToken = readOpenClawGatewayToken(args.baseEnv);
327
+ if (gatewayToken) {
328
+ env[exports.OPENCLAW_GATEWAY_TOKEN_ENV] = gatewayToken;
329
+ }
321
330
  }
322
331
  // Claude SDK refuses nested sessions when CLAUDECODE is present.
323
332
  // Force-unset in child runtime for CLI ask/chat/serve subprocesses.
@@ -361,6 +370,38 @@ function buildCliEnvPatch(args) {
361
370
  }
362
371
  return env;
363
372
  }
373
+ /** Read provenance stamp env vars injected into sandbox children by serve. */
374
+ function readProvenanceEnvFromProcess() {
375
+ const pick = (key) => process.env[key]?.trim() || undefined;
376
+ const tierRaw = pick('AGENTIDE_ANALYTICS_TIER');
377
+ const tier = tierRaw !== undefined ? Number(tierRaw) : undefined;
378
+ return {
379
+ policyVersion: pick('AGENTIDE_ANALYTICS_POLICY_VERSION'),
380
+ packDigest: pick('AGENTIDE_ANALYTICS_PACK_DIGEST'),
381
+ engineVersion: pick('AGENTIDE_ANALYTICS_ENGINE_VERSION'),
382
+ tier: tier !== undefined && Number.isFinite(tier) ? tier : undefined,
383
+ instanceId: pick('AGENTIDE_ANALYTICS_INSTANCE_ID'),
384
+ };
385
+ }
386
+ function buildProvenanceEnv(stamp) {
387
+ const env = {};
388
+ if (stamp.policyVersion?.trim()) {
389
+ env.AGENTIDE_ANALYTICS_POLICY_VERSION = stamp.policyVersion.trim();
390
+ }
391
+ if (stamp.packDigest?.trim()) {
392
+ env.AGENTIDE_ANALYTICS_PACK_DIGEST = stamp.packDigest.trim();
393
+ }
394
+ if (stamp.engineVersion?.trim()) {
395
+ env.AGENTIDE_ANALYTICS_ENGINE_VERSION = stamp.engineVersion.trim();
396
+ }
397
+ if (stamp.tier !== undefined) {
398
+ env.AGENTIDE_ANALYTICS_TIER = String(stamp.tier);
399
+ }
400
+ if (stamp.instanceId?.trim()) {
401
+ env.AGENTIDE_ANALYTICS_INSTANCE_ID = stamp.instanceId.trim();
402
+ }
403
+ return env;
404
+ }
364
405
  async function runCli(binaryPath, args, opts) {
365
406
  return await new Promise((resolve, reject) => {
366
407
  const child = (0, child_process_1.spawn)(binaryPath, args, {