@agenthippo-ai/agenthippo 0.2.3-dataplane → 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 (130) hide show
  1. package/out/analytics/dashboard/provider.js +13 -2
  2. package/out/analytics/dashboard/webview/panel.js +3 -2
  3. package/out/analytics/identity.js +0 -0
  4. package/out/analytics/service.js +23 -25
  5. package/out/analytics/sessionEvents.js +98 -23
  6. package/out/auth/authBaseUrl.js +59 -0
  7. package/out/auth/bootstrapApply.js +9 -10
  8. package/out/auth/brokerClient.js +48 -14
  9. package/out/auth/provider.js +8 -21
  10. package/out/auth/sessionStore.js +3 -40
  11. package/out/auth/signInFlow.js +2 -1
  12. package/out/auth-broker/config.js +50 -5
  13. package/out/auth-broker/server.js +122 -26
  14. package/out/auth-broker/store.js +25 -24
  15. package/out/authority/warrant.js +206 -0
  16. package/out/branding.js +27 -1
  17. package/out/cli/commands/conformance.js +249 -0
  18. package/out/cli/commands/data-plane.js +67 -0
  19. package/out/cli/commands/deploy.js +1 -0
  20. package/out/cli/commands/gateway.js +4 -0
  21. package/out/cli/commands/pack.js +182 -3
  22. package/out/cli/commands/sandbox.js +332 -0
  23. package/out/cli/commands/serve.js +875 -52
  24. package/out/cli/commands/store.js +12 -4
  25. package/out/cli/data-plane/gateway.js +28 -5
  26. package/out/cli/data-plane/index.js +17 -2
  27. package/out/cli/data-plane/registry.js +9 -0
  28. package/out/cli/data-plane/remote.js +106 -0
  29. package/out/cli/data-plane/sidecar.js +161 -0
  30. package/out/cli/deploy/layers/identity.js +85 -4
  31. package/out/cli/deploy-init.js +89 -24
  32. package/out/cli/deps.js +106 -2
  33. package/out/cli/idp-init.js +35 -0
  34. package/out/cli/index.js +155 -16
  35. package/out/cli/pack-install-ref.js +15 -0
  36. package/out/cli/pack-loader.js +8 -1
  37. package/out/cli/pack-runtime.js +61 -0
  38. package/out/cli/runtime.js +35 -1
  39. package/out/cli/serve-hardening.js +80 -3
  40. package/out/cli/utils.js +12 -1
  41. package/out/engines/base.js +32 -3
  42. package/out/engines/claude.js +33 -15
  43. package/out/engines/codex.js +110 -26
  44. package/out/engines/custom.js +8 -0
  45. package/out/evidence/sessionShip.js +610 -0
  46. package/out/extension.js +11 -2
  47. package/out/gateway/bridge.js +40 -2
  48. package/out/gateway/client.js +34 -1
  49. package/out/gateway/installSource.js +68 -0
  50. package/out/gateway/lifecycle.js +185 -51
  51. package/out/gateway/outboundPaths.js +4 -1
  52. package/out/gateway/secretsPreflight.js +90 -0
  53. package/out/gateway/service.js +7 -2
  54. package/out/headless/index.js +17 -1
  55. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  56. package/out/mcp.js +30 -0
  57. package/out/pack/caps.js +271 -0
  58. package/out/pack/governor.js +309 -0
  59. package/out/pack/lease-schema.js +270 -0
  60. package/out/pack/lease.js +640 -0
  61. package/out/pack/reconcile.js +217 -0
  62. package/out/pack/signature.js +839 -0
  63. package/out/remote/handleRemoteChat.js +44 -3
  64. package/out/remote/remoteTrust.js +277 -0
  65. package/out/remote/serveClient.js +52 -5
  66. package/out/runner/host.js +131 -0
  67. package/out/runner/protocol.js +108 -0
  68. package/out/runner/remoteEngine.js +130 -0
  69. package/out/runtime/interfaces/edit-tracker.js +2 -0
  70. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  71. package/out/sandbox/containerDriver.js +570 -0
  72. package/out/sandbox/processDriver.js +315 -0
  73. package/out/sandbox/sandboxRunner.js +191 -0
  74. package/out/sandbox/support.js +301 -0
  75. package/out/sandbox/types.js +55 -0
  76. package/out/scripts/agentide-litellm.sh +67 -5
  77. package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
  78. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -132
  79. package/out/scripts/analytics/setup-analytics.ps1 +37 -13
  80. package/out/scripts/analytics/setup-analytics.sh +93 -13
  81. package/out/scripts/analytics/start-analytics.sh +86 -14
  82. package/out/scripts/gateway/setup-gateway.sh +72 -32
  83. package/out/shared/models.js +11 -110
  84. package/out/spotlight/install.js +7 -1
  85. package/out/store/index.js +41 -10
  86. package/out/sync.js +3 -1
  87. package/out/test/analytics.test.js +162 -23
  88. package/out/test/auth-baseurl.test.js +132 -0
  89. package/out/test/auth-bootstrapApply.test.js +27 -6
  90. package/out/test/auth-broker-hardening.test.js +227 -0
  91. package/out/test/auth-broker.test.js +59 -9
  92. package/out/test/branding.test.js +13 -0
  93. package/out/test/caps-governor.test.js +455 -0
  94. package/out/test/codex-new-file-survival.test.js +119 -0
  95. package/out/test/configuration-scopes.test.js +131 -0
  96. package/out/test/data-plane.test.js +67 -0
  97. package/out/test/deploy-init.test.js +40 -0
  98. package/out/test/gateway-client.test.js +28 -0
  99. package/out/test/gateway-install-source.test.js +111 -0
  100. package/out/test/gateway-lifecycle.test.js +1 -1
  101. package/out/test/gateway-outbound.test.js +7 -0
  102. package/out/test/gateway-secrets-preflight.test.js +127 -0
  103. package/out/test/gateway-session-routing.test.js +18 -0
  104. package/out/test/governor-hardening.test.js +217 -0
  105. package/out/test/headless-sync.test.js +22 -0
  106. package/out/test/lease-signature.test.js +357 -0
  107. package/out/test/lease-trust-root.test.js +273 -0
  108. package/out/test/lease-validation.test.js +169 -0
  109. package/out/test/lease.test.js +114 -0
  110. package/out/test/mcp.test.js +27 -1
  111. package/out/test/pack-admission.test.js +216 -0
  112. package/out/test/pack-digest-parity.test.js +140 -0
  113. package/out/test/pack-reconcile.test.js +183 -0
  114. package/out/test/pack-signature-artifact.test.js +103 -0
  115. package/out/test/pack-signature.test.js +176 -0
  116. package/out/test/pack-trust-root.test.js +202 -0
  117. package/out/test/remote-trust.test.js +242 -0
  118. package/out/test/runner-protocol.test.js +249 -0
  119. package/out/test/sandbox-driver.test.js +553 -0
  120. package/out/test/sandbox-hardening.test.js +395 -0
  121. package/out/test/sandbox-runner.test.js +343 -0
  122. package/out/test/serve-hardening.test.js +45 -6
  123. package/out/test/serveClient.test.js +6 -5
  124. package/out/test/sessionShip.test.js +560 -0
  125. package/out/test/spotlightPublish.test.js +28 -0
  126. package/out/test/store-install-safe.test.js +69 -0
  127. package/out/test/tls-bypass.test.js +90 -0
  128. package/out/test/warrant.test.js +138 -0
  129. package/out/utils.js +46 -9
  130. package/package.json +1 -1
package/out/cli/index.js CHANGED
@@ -75,6 +75,7 @@ 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");
80
81
  const idp_1 = require("./commands/idp");
@@ -86,6 +87,26 @@ const probe_custom_engine_1 = require("./commands/probe-custom-engine");
86
87
  const deps_1 = require("./deps");
87
88
  const branding_1 = require("../branding");
88
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
+ }
89
110
  const VERSION = '1.110.8';
90
111
  const CLI_DESCRIPTION = [
91
112
  `${branding_1.AGENTIDE_BRAND_NAME} CLI - Run agent packs from the command line`,
@@ -102,14 +123,22 @@ const CLI_DESCRIPTION = [
102
123
  ' AGENTHIPPO_STORE_REPO Override store GitHub repo (owner/repo; CLI store commands)',
103
124
  '',
104
125
  'Quickstart:',
105
- ` 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"`,
106
127
  '',
107
128
  'Agent Packs:',
108
129
  ` Loaded from ~/${branding_1.AGENTIDE_DIR_NAME}/agents/<name>/ or absolute/relative paths.`,
109
130
  ' Pack permissions control filesystem access scope.',
110
131
  ].join('\n');
111
132
  const program = new commander_1.Command();
112
- 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
+ }
113
142
  await (0, agentEnvFile_1.mergeAgentHomeEnvIntoProcessEnv)();
114
143
  });
115
144
  program
@@ -126,7 +155,7 @@ program
126
155
  .description('Start interactive chat session')
127
156
  .option('-a, --agent <name>', 'Agent pack to use')
128
157
  .option('-e, --engine <id>', 'Override pack engine: claude|codex|<custom-engine-id>')
129
- .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)')
130
159
  .option('-w, --workspace <path>', 'Workspace directory', process.cwd())
131
160
  .action(async (options) => {
132
161
  const verbose = program.opts().verbose;
@@ -145,7 +174,7 @@ program
145
174
  .description('Send a single message and exit')
146
175
  .option('-a, --agent <name>', 'Agent pack to use')
147
176
  .option('-e, --engine <id>', 'Override pack engine: claude|codex|<custom-engine-id>')
148
- .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)')
149
178
  .option('-w, --workspace <path>', 'Workspace directory', process.cwd())
150
179
  .option('-f, --file <path>', 'Include file content in context')
151
180
  .option('--json', 'Output as JSON')
@@ -176,7 +205,7 @@ program
176
205
  .requiredOption('--prompt <message>', 'Prompt text')
177
206
  .option('--pack <name>', 'Agent pack to use')
178
207
  .option('-e, --engine <id>', 'Override pack engine: claude|codex|<custom-engine-id>')
179
- .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)')
180
209
  .option('-w, --workspace <path>', 'Workspace directory', process.cwd())
181
210
  .option('--session-id <id>', 'Optional session id for the caller')
182
211
  .option('--system-prompt <text>', 'Optional system prompt override')
@@ -204,7 +233,7 @@ program
204
233
  .description('Start HTTP server')
205
234
  .requiredOption('-a, --agent <name>', 'Agent pack to use (required)')
206
235
  .option('-e, --engine <id>', 'Override pack engine: claude|codex|<custom-engine-id>')
207
- .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)')
208
237
  .option('-p, --port <port>', 'Port to listen on', '3000')
209
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)')
210
239
  .option('-w, --workspace <path>', 'Default workspace directory', process.cwd())
@@ -213,18 +242,52 @@ program
213
242
  .option('--state-dir <path>', 'Persist session state here (honors AGENTHIPPO_HOME; replaces the tmpdir default)')
214
243
  .option('--max-turns-per-session <n>', 'Cap turns per session (abuse/runaway guard); 0 = unlimited', '0')
215
244
  .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)')
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')
221
263
  .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
- }
225
264
  const verbose = program.opts().verbose;
226
265
  await (0, deps_1.ensureDeps)(verbose);
227
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,
228
291
  agent: options.agent,
229
292
  engine: options.engine,
230
293
  model: options.model,
@@ -239,6 +302,68 @@ program
239
302
  verbose,
240
303
  });
241
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
+ });
242
367
  program
243
368
  .command('auth-broker')
244
369
  .description('Start the AgentHippo auth broker (OAuth + IDE bootstrap API)')
@@ -262,6 +387,7 @@ program
262
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')
263
388
  .option('--cookie-domain <domain>', 'edge-fed: parent domain the shared auth cookie is scoped to (e.g. .agenthippo.ai)', '.agenthippo.ai')
264
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')
265
391
  .option('--model-routing <mode>', 'Cloud Run + edge-fed: sidecar (in-pod LiteLLM+OTEL, default) or gateway (agent → gateway.agenthippo.ai directly, lean)')
266
392
  .option('--force', 'Overwrite existing files')
267
393
  .action(async (action, pack, options) => {
@@ -283,6 +409,7 @@ program
283
409
  cookieDomain: options.cookieDomain,
284
410
  colocate: Boolean(options.colocate),
285
411
  modelRouting,
412
+ edgeTurnConnector: options.edgeTurnConnector,
286
413
  });
287
414
  });
288
415
  // IdP command - generate the central IdP stack (org identity infra, set up once)
@@ -304,16 +431,28 @@ program
304
431
  // Pack command - pack management
305
432
  program
306
433
  .command('pack <action>')
307
- .description('Manage agent packs (list, validate, install)')
434
+ .description('Manage agent packs (list, validate, sign, verify, digest, keygen, install)')
308
435
  .argument('[name]', 'Pack name or path')
309
- .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')
310
443
  .option('-w, --workspace <path>', 'Workspace directory for local packs', process.cwd())
311
444
  .action(async (action, name, options) => {
312
445
  await (0, pack_1.packCommand)({
313
446
  action,
314
447
  name,
315
448
  output: options.output,
449
+ to: options.to,
450
+ force: options.force,
316
451
  workspace: options.workspace,
452
+ key: options.key,
453
+ kid: options.kid,
454
+ keys: options.keys,
455
+ jwks: options.jwks,
317
456
  verbose: program.opts().verbose,
318
457
  });
319
458
  });
@@ -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
+ }
@@ -373,7 +373,14 @@ function getApiConfig() {
373
373
  const litellmBaseUrl = getEnvVar('AGENTHIPPO_LITELLM_BASE_URL', 'AGENTIDE_LITELLM_BASE_URL');
374
374
  const anthropicApiKey = process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY;
375
375
  const openaiApiKey = process.env.OPENAI_API_KEY;
376
- 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);
377
384
  return {
378
385
  // Kept only for the "is any credential present at all?" boot check; routing never uses it.
379
386
  apiKey: litellmApiKey || anthropicApiKey || openaiApiKey,
@@ -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"));
@@ -320,7 +322,7 @@ function buildCliEnvPatch(args) {
320
322
  // nonce. AGENTHIPPO_EGRESS_URL in the process env is that signal — with it set, never bake
321
323
  // the token into a child env (this closes §7.4 gap 1 for custom engines; codex/claude are
322
324
  // covered by serve stripping the token from its own process.env).
323
- if (!process.env.AGENTHIPPO_EGRESS_URL?.trim()) {
325
+ if (!args.omitGatewayToken && !process.env.AGENTHIPPO_EGRESS_URL?.trim()) {
324
326
  const gatewayToken = readOpenClawGatewayToken(args.baseEnv);
325
327
  if (gatewayToken) {
326
328
  env[exports.OPENCLAW_GATEWAY_TOKEN_ENV] = gatewayToken;
@@ -368,6 +370,38 @@ function buildCliEnvPatch(args) {
368
370
  }
369
371
  return env;
370
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
+ }
371
405
  async function runCli(binaryPath, args, opts) {
372
406
  return await new Promise((resolve, reject) => {
373
407
  const child = (0, child_process_1.spawn)(binaryPath, args, {
@@ -36,11 +36,13 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.TurnGate = exports.TURN_LIMIT_BUSY_MESSAGE = exports.RATE_LIMIT_BUSY_MESSAGE = exports.TURN_GATE_BUSY_MESSAGE = exports.TURN_GATE_RETRY_AFTER_SECONDS = exports.TURN_QUEUE_ENV = exports.MAX_CONCURRENT_TURNS_ENV = exports.TurnLimiter = exports.SESSION_WITHOUT_USER_REASON = exports.IDENTITY_HEADER_ENV = exports.RateLimiter = exports.XFF_TRUSTED_HOPS_ENV = exports.CONTAINER_SERVE_HOST = exports.SERVE_HOST_ENV = exports.DEFAULT_BIND_HOST = exports.DEFAULT_MAX_BODY_BYTES = void 0;
39
+ exports.TurnGate = exports.TURN_LIMIT_BUSY_MESSAGE = exports.RATE_LIMIT_BUSY_MESSAGE = exports.TURN_GATE_BUSY_MESSAGE = exports.TURN_GATE_RETRY_AFTER_SECONDS = exports.TURN_QUEUE_ENV = exports.MAX_CONCURRENT_TURNS_ENV = exports.TurnLimiter = exports.SESSION_WITHOUT_USER_REASON = exports.IDENTITY_HEADER_ENV = exports.PIN_ONLY_IDENTITY_HEADERS = exports.RateLimiter = exports.XFF_TRUSTED_HOPS_ENV = exports.CONTAINER_SERVE_HOST = exports.SERVE_HOST_ENV = exports.DEFAULT_BIND_HOST = exports.DEFAULT_MAX_BODY_BYTES = void 0;
40
40
  exports.resolveStateDir = resolveStateDir;
41
41
  exports.resolveAnalyticsSessionsDir = resolveAnalyticsSessionsDir;
42
+ exports.isLoopbackHost = isLoopbackHost;
42
43
  exports.resolveBindHost = resolveBindHost;
43
44
  exports.clientIp = clientIp;
45
+ exports.identityHeaderAdvice = identityHeaderAdvice;
44
46
  exports.resolveUserId = resolveUserId;
45
47
  exports.resolveDataToken = resolveDataToken;
46
48
  exports.peerToUserId = peerToUserId;
@@ -109,6 +111,11 @@ function resolveAnalyticsSessionsDir(stateDir, stateDirEphemeral, env = process.
109
111
  }
110
112
  /** Loopback default — safe everywhere a human runs `serve` locally. */
111
113
  exports.DEFAULT_BIND_HOST = '127.0.0.1';
114
+ /** True when a bind address is reachable only from this machine. */
115
+ function isLoopbackHost(host) {
116
+ const h = host.trim().toLowerCase().replace(/^\[|\]$/g, '');
117
+ return h === 'localhost' || h === '::1' || /^127\./.test(h);
118
+ }
112
119
  /** Env var for the listen address (used by generated deploy Dockerfiles). */
113
120
  exports.SERVE_HOST_ENV = 'AGENTHIPPO_SERVE_HOST';
114
121
  /** Bind address baked into container deploy artifacts (Docker / Cloud Run sidecars). */
@@ -204,15 +211,78 @@ class RateLimiter {
204
211
  }
205
212
  }
206
213
  exports.RateLimiter = RateLimiter;
214
+ /**
215
+ * Identity headers trusted when NOTHING is pinned.
216
+ *
217
+ * Every entry here is a header that exactly one platform sets AND owns the request path for, so
218
+ * a client-supplied copy cannot reach serve without that platform having been bypassed entirely.
219
+ *
220
+ * `x-agent-user` is deliberately NOT in this list, and this is a breaking change for anything
221
+ * that relied on it being trusted by default. It is AgentHippo's own canonical header — the name
222
+ * an arbitrary client is most likely to guess — it belongs to no platform that strips it, and it
223
+ * used to sit FIRST, so it beat every platform header that follows. The consequence on any
224
+ * deployment that had not pinned `AGENTHIPPO_IDENTITY_HEADER`: any HTTP client could send
225
+ * `x-agent-user: victim@example.com` and be namespaced into that user's session state, attach to
226
+ * their conversation, and be issued a data-plane turn nonce scoped to them. Cloud Run IAP makes
227
+ * this concrete — it strips `X-Goog-*` and forwards a client-supplied `x-agent-user` untouched.
228
+ *
229
+ * It remains perfectly valid as a PINNED header (`AGENTHIPPO_IDENTITY_HEADER=x-agent-user`),
230
+ * which is what a deployment whose proxy really does set and strip it should use — including the
231
+ * Caddy `forward_auth` flow, where `auth-broker/server.ts` sets `X-Agent-User` on `/api/ide/me`
232
+ * so Caddy can copy a VALIDATED identity upstream. That direction is the correct and only source
233
+ * of the header, and it is unaffected: pinning is exactly the declaration that it is trustworthy.
234
+ */
207
235
  const HEADER_ALIASES = [
208
- 'x-agent-user', // AgentHippo canonical (set by our Worker / generic proxy)
209
236
  'x-forwarded-email', // Databricks Apps
210
237
  'x-forwarded-preferred-username', // Databricks Apps
211
238
  'x-goog-authenticated-user-email', // Cloud Run IAP (prefixed value, stripped below)
212
239
  'cf-access-authenticated-user-email', // Cloudflare Access
213
240
  ];
241
+ /**
242
+ * Headers that are only ever trusted when explicitly pinned.
243
+ *
244
+ * Kept as a named list rather than deleted, because "why is my identity header ignored?" needs an
245
+ * answer, and `identityHeaderAdvice` gives one that names the env var to set.
246
+ */
247
+ exports.PIN_ONLY_IDENTITY_HEADERS = ['x-agent-user'];
214
248
  /** Env var: pin the single identity header the deploy layer actually sets (and strips). */
215
249
  exports.IDENTITY_HEADER_ENV = 'AGENTHIPPO_IDENTITY_HEADER';
250
+ /**
251
+ * Boot-time advice about identity trust. Returns the lines serve must print, loudest first.
252
+ *
253
+ * Not a refusal. Refusing to boot without a pin would stop deployments that are configured
254
+ * correctly today (Databricks Apps, Cloudflare Access) and whose platform header is genuinely
255
+ * unforgeable — a fail-closed default that mostly punishes the compliant is not a security win.
256
+ * What was actually dangerous was `x-agent-user` being trusted by default, and that is now gone.
257
+ */
258
+ function identityHeaderAdvice(env = process.env, bindHost) {
259
+ const pinned = env[exports.IDENTITY_HEADER_ENV]?.trim();
260
+ if (pinned) {
261
+ return [];
262
+ }
263
+ // Severity follows reachability. On loopback nothing off-box can reach the port, so the same
264
+ // paragraph every local run is warning fatigue — and fatigue is why the message gets ignored
265
+ // on the deploy where it decides whether strangers can name themselves as another user.
266
+ const host = bindHost?.trim();
267
+ if (host && isLoopbackHost(host)) {
268
+ return [
269
+ `${exports.IDENTITY_HEADER_ENV} is not set. Fine for this loopback bind; pin it before exposing ` +
270
+ `serve on a network interface.`,
271
+ ];
272
+ }
273
+ return [
274
+ ...(host
275
+ ? [`serve is bound to ${host} — reachable beyond this machine — with no ${exports.IDENTITY_HEADER_ENV} pinned. ` +
276
+ `Anyone who can reach this port is trusted to state who they are.`]
277
+ : []),
278
+ `${exports.IDENTITY_HEADER_ENV} is not set, so identity falls back to the platform header aliases ` +
279
+ `(${HEADER_ALIASES.join(', ')}). That is only safe when your proxy sets AND STRIPS every one of them. ` +
280
+ `Pin the single header your deploy layer really controls, e.g. ${exports.IDENTITY_HEADER_ENV}=x-goog-authenticated-user-email.`,
281
+ `NOTE: ${exports.PIN_ONLY_IDENTITY_HEADERS.join(', ')} is NO LONGER trusted by default — it is not stripped by any ` +
282
+ `platform, so an arbitrary client could send it and be namespaced as another user. If your proxy does set it ` +
283
+ `(the Caddy forward_auth / auth-broker flow does), pin it: ${exports.IDENTITY_HEADER_ENV}=x-agent-user.`,
284
+ ];
285
+ }
216
286
  /**
217
287
  * Resolve the end-user identity for a request.
218
288
  *
@@ -222,7 +292,10 @@ exports.IDENTITY_HEADER_ENV = 'AGENTHIPPO_IDENTITY_HEADER';
222
292
  * that pins a header has declared it has a real proxy-verified identity, so an
223
293
  * unauthenticated caller must not be able to name themselves via the client-
224
294
  * controlled body. Body fallback is dev/internal convenience for the *unpinned*
225
- * case only (e.g. direct curl against a local serve with no identity proxy).
295
+ * case only (e.g. direct curl against a local serve with no identity proxy) —
296
+ * and it must be OPTED INTO via AGENTHIPPO_ALLOW_BODY_IDENTITY=1 (serve-v4 §10
297
+ * hole 3). Before the gate it was safe "by convention": any deployment that
298
+ * forgot to pin a header silently let every caller name an arbitrary victim.
226
299
  *
227
300
  * The alias-list default (used when unpinned) is only safe when the proxy strips
228
301
  * or overwrites ALL of the aliases; platforms that strip only their own header
@@ -257,6 +330,10 @@ function resolveUserId(headers, body, env = process.env) {
257
330
  // access, gcloud run services proxy bypassing IAP) name an arbitrary victim.
258
331
  return undefined;
259
332
  }
333
+ // Unpinned body identity is an explicit opt-in, never a default (§10 hole 3).
334
+ if (env.AGENTHIPPO_ALLOW_BODY_IDENTITY?.trim() !== '1') {
335
+ return undefined;
336
+ }
260
337
  const fromBody = body?.user_id?.trim();
261
338
  return fromBody || undefined;
262
339
  }
package/out/cli/utils.js CHANGED
@@ -77,6 +77,13 @@ function findClaudeCodePath() {
77
77
  if (depsPath) {
78
78
  return depsPath;
79
79
  }
80
+ // 2b. Our OWN dependency tree — the case where the runtime ships as a node app with the
81
+ // SDK already in node_modules. Without this, such a deployment fails with "Claude Code CLI
82
+ // not found" while the SDK sits right there (hit on the Hanoi VM, 2026-07-27).
83
+ const ownCli = (0, deps_1.resolveFromOwnDeps)('@anthropic-ai/claude-agent-sdk', 'cli.js');
84
+ if (ownCli) {
85
+ return ownCli;
86
+ }
80
87
  // 3. Check bundled location (next to binary) - legacy fallback
81
88
  const binaryDir = getBinaryDir();
82
89
  const bundledPath = path.join(binaryDir, 'claude-sdk', 'cli.js');
@@ -131,6 +138,10 @@ function findCodexPath() {
131
138
  return process.env.CODEX_PATH;
132
139
  }
133
140
  // 2. Check npm-installed location (.agenthippo-deps/node_modules)
141
+ const ownCodex = (0, deps_1.resolveFromOwnDeps)('@openai/codex', 'bin/codex.js');
142
+ if (ownCodex) {
143
+ return ownCodex;
144
+ }
134
145
  const depsPath = (0, deps_1.getCodexSdkBinaryPath)();
135
146
  if (depsPath) {
136
147
  return depsPath;
@@ -211,7 +222,7 @@ exports.COLORS = {
211
222
  red: '\x1b[31m',
212
223
  yellow: '\x1b[33m',
213
224
  };
214
- exports.SUGGESTED_LITELLM_SMOKE_TEST_COMMAND = 'AGENTHIPPO_LITELLM_BASE_URL="http://localhost:4000" AGENTHIPPO_LITELLM_API_KEY="dummy" AGENTHIPPO_DEFAULT_MODEL="litellm/gpt-5.3-codex" AGENTHIPPO_DEFAULT_ENGINE="codex" agenthippo ask "Reply with exactly: CLI_TEST_OK"';
225
+ exports.SUGGESTED_LITELLM_SMOKE_TEST_COMMAND = 'AGENTHIPPO_LITELLM_BASE_URL="http://localhost:4000" AGENTHIPPO_LITELLM_API_KEY="dummy" AGENTHIPPO_DEFAULT_MODEL="litellm/gpt-5.5" AGENTHIPPO_DEFAULT_ENGINE="codex" agenthippo ask "Reply with exactly: CLI_TEST_OK"';
215
226
  function getLiteLLMQuickstartHintLines() {
216
227
  return [
217
228
  'Try local LiteLLM quickstart:',