@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
@@ -126,13 +126,25 @@ class ObservableStreamEmitter {
126
126
  writer;
127
127
  sessionId;
128
128
  requestId;
129
+ userId;
129
130
  missingCallIdCounter = 0;
130
131
  pendingSyntheticCallIds = new Map();
131
- constructor(inner, writer, sessionId, requestId) {
132
+ constructor(inner, writer, sessionId, requestId,
133
+ /**
134
+ * Acting user for the warrant audit trail on tool events. Captured once at
135
+ * wrap time (still inside the request's credential scope) and stored on this
136
+ * per-turn emitter — never re-read from ambient AsyncLocalStorage at emit
137
+ * time. Custom-engine persistent workers deliver tool events on a detached
138
+ * parent IPC callback whose ALS can stick to the spawn request; constructor
139
+ * capture keeps attribution correct for every engine. Undefined outside a
140
+ * serve credential scope (IDE-local / CLI without runWithCredential).
141
+ */
142
+ userId) {
132
143
  this.inner = inner;
133
144
  this.writer = writer;
134
145
  this.sessionId = sessionId;
135
146
  this.requestId = requestId;
147
+ this.userId = userId;
136
148
  }
137
149
  makeCallIdKey(toolName) {
138
150
  return `${this.sessionId}:${this.requestId}:${toolName}`;
@@ -175,6 +187,8 @@ class ObservableStreamEmitter {
175
187
  callId: loggedCallId,
176
188
  // Truncate input to avoid huge JSONL files
177
189
  inputPreview: input?.slice(0, 500),
190
+ // The warrant: who this tool call ran as (captured at wrap; see constructor).
191
+ userId: this.userId,
178
192
  });
179
193
  return this.inner.toolStart(toolName, input, toolCallId, metadata);
180
194
  }
@@ -188,6 +202,7 @@ class ObservableStreamEmitter {
188
202
  isError: isError ?? false,
189
203
  // Truncate result to avoid huge JSONL files
190
204
  resultPreview: result?.slice(0, 500),
205
+ userId: this.userId,
191
206
  });
192
207
  return this.inner.toolEnd(toolName, toolCallId, result, isError, metadata);
193
208
  }
@@ -200,6 +215,7 @@ class ObservableStreamEmitter {
200
215
  async error(message) {
201
216
  await this.writer.append(this.sessionId, this.requestId, 'error', {
202
217
  message,
218
+ userId: this.userId,
203
219
  });
204
220
  return this.inner.error(message);
205
221
  }
@@ -210,7 +226,7 @@ class ObservableStreamEmitter {
210
226
  * Emit a turn_start event. Call at the beginning of stream().
211
227
  */
212
228
  async emitTurnStart() {
213
- await this.writer.append(this.sessionId, this.requestId, 'turn_start', {});
229
+ await this.writer.append(this.sessionId, this.requestId, 'turn_start', { userId: this.userId });
214
230
  }
215
231
  /**
216
232
  * Emit a turn_end event. Call at the end of stream().
@@ -218,6 +234,7 @@ class ObservableStreamEmitter {
218
234
  async emitTurnEnd(success) {
219
235
  await this.writer.append(this.sessionId, this.requestId, 'turn_end', {
220
236
  success,
237
+ userId: this.userId,
221
238
  });
222
239
  }
223
240
  }
@@ -226,6 +243,6 @@ exports.ObservableStreamEmitter = ObservableStreamEmitter;
226
243
  * Create an ObservableStreamEmitter that wraps the given emitter.
227
244
  * Returns both the wrapped emitter and emit helpers for turn boundaries.
228
245
  */
229
- function wrapEmitterWithObservability(inner, writer, sessionId, requestId) {
230
- return new ObservableStreamEmitter(inner, writer, sessionId, requestId);
246
+ function wrapEmitterWithObservability(inner, writer, sessionId, requestId, userId) {
247
+ return new ObservableStreamEmitter(inner, writer, sessionId, requestId, userId);
231
248
  }
@@ -118,7 +118,7 @@ function buildAuthorizeUrl(baseUrl, redirectUri, codeChallenge, state, loginHint
118
118
  }
119
119
  return url.toString();
120
120
  }
121
- async function completeDevPkceLogin(baseUrl, email, redirectUri = 'agenthippo://auth/callback') {
121
+ async function completeDevPkceLogin(baseUrl, email, redirectUri = 'http://127.0.0.1:1/callback') {
122
122
  const verifier = (0, pkce_js_1.generateCodeVerifier)();
123
123
  const challenge = (0, pkce_js_1.codeChallengeFromVerifier)(verifier);
124
124
  const code = await devLoginForTests(baseUrl, email, redirectUri, challenge);
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.showSignInOnboardingPromptIfNeeded = showSignInOnboardingPromptIfNeeded;
40
+ exports.scheduleSignInOnboardingPrompt = scheduleSignInOnboardingPrompt;
41
+ const vscode = __importStar(require("vscode"));
42
+ const sessionStore_js_1 = require("./sessionStore.js");
43
+ const PROMPT_DISMISSED_KEY = 'agentide.signInPrompt.dismissed';
44
+ const PROVIDERS_LIST_KEY = 'agentide.providers';
45
+ const API_KEY_PREFIX = 'agentide.apikey.';
46
+ const OAUTH_PREFIX = 'agentide.oauth.';
47
+ const ONBOARDING_MESSAGE = 'BYOK or Sign in for instant model access — no API key needed.';
48
+ async function hasByokConfigured(secrets) {
49
+ const providersJson = await secrets.get(PROVIDERS_LIST_KEY);
50
+ if (!providersJson) {
51
+ return false;
52
+ }
53
+ try {
54
+ const providers = JSON.parse(providersJson);
55
+ if (!Array.isArray(providers)) {
56
+ return false;
57
+ }
58
+ for (const provider of providers) {
59
+ if (provider === 'agenthippo') {
60
+ continue;
61
+ }
62
+ const apiKey = await secrets.get(`${API_KEY_PREFIX}${provider}`);
63
+ if (apiKey?.trim()) {
64
+ return true;
65
+ }
66
+ const oauth = await secrets.get(`${OAUTH_PREFIX}${provider}`);
67
+ if (oauth?.trim()) {
68
+ return true;
69
+ }
70
+ }
71
+ }
72
+ catch {
73
+ // ignore malformed provider list
74
+ }
75
+ return false;
76
+ }
77
+ async function shouldShowOnboardingPrompt(context) {
78
+ if (context.globalState.get(PROMPT_DISMISSED_KEY)) {
79
+ return false;
80
+ }
81
+ if (await (0, sessionStore_js_1.getStoredAuthSession)(context.secrets)) {
82
+ return false;
83
+ }
84
+ if (await hasByokConfigured(context.secrets)) {
85
+ return false;
86
+ }
87
+ return true;
88
+ }
89
+ async function showSignInOnboardingPromptIfNeeded(context) {
90
+ if (!(await shouldShowOnboardingPrompt(context))) {
91
+ return;
92
+ }
93
+ const choice = await vscode.window.showInformationMessage(ONBOARDING_MESSAGE, 'Sign In', 'Add API Key', 'Later');
94
+ if (choice === 'Sign In') {
95
+ await vscode.commands.executeCommand('agenthippo.signIn');
96
+ return;
97
+ }
98
+ if (choice === 'Add API Key') {
99
+ await vscode.commands.executeCommand('agentide.addAPIKey');
100
+ return;
101
+ }
102
+ if (choice === 'Later' || choice === undefined) {
103
+ await context.globalState.update(PROMPT_DISMISSED_KEY, true);
104
+ }
105
+ }
106
+ /** Delay so the workbench finishes restoring before the toast appears. */
107
+ function scheduleSignInOnboardingPrompt(context) {
108
+ const handle = setTimeout(() => {
109
+ void showSignInOnboardingPromptIfNeeded(context);
110
+ }, 3000);
111
+ context.subscriptions.push({ dispose: () => clearTimeout(handle) });
112
+ }
@@ -48,7 +48,7 @@ const signInFlow_js_1 = require("./signInFlow.js");
48
48
  const sessionStore_js_1 = require("./sessionStore.js");
49
49
  var sessionStore_js_2 = require("./sessionStore.js");
50
50
  Object.defineProperty(exports, "getAgentHippoAccessToken", { enumerable: true, get: function () { return sessionStore_js_2.getAgentHippoAccessToken; } });
51
- const DEFAULT_AGENTHIPPO_MODEL_GATEWAY_URL = 'https://api.agenthippo.ai/v1';
51
+ const DEFAULT_AGENTHIPPO_MODEL_GATEWAY_URL = 'https://gateway.agenthippo.ai/v1';
52
52
  function getAgentHippoModelGatewayUrl() {
53
53
  return vscode.workspace.getConfiguration('agentide').get('AgentHippoModelGateway.url')?.trim() || DEFAULT_AGENTHIPPO_MODEL_GATEWAY_URL;
54
54
  }
@@ -3,7 +3,7 @@
3
3
  * Copyright (c) AgentHippo.ai. All rights reserved.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DEFAULT_JWT_SECRET = void 0;
6
+ exports.DEFAULT_LITELLM_API_KEY = exports.DEFAULT_JWT_SECRET = void 0;
7
7
  exports.loadAuthBrokerConfig = loadAuthBrokerConfig;
8
8
  exports.emailDomainAllowed = emailDomainAllowed;
9
9
  function parseDomains(raw) {
@@ -14,6 +14,8 @@ function parseDomains(raw) {
14
14
  }
15
15
  /** Well-known default secret used in dev/test. Never acceptable in production. */
16
16
  exports.DEFAULT_JWT_SECRET = 'dev-agenthippo-auth-secret-change-me';
17
+ /** Well-known default LiteLLM team key used in dev/test. Never acceptable in production. */
18
+ exports.DEFAULT_LITELLM_API_KEY = 'ahk_team_dev_key';
17
19
  function loadAuthBrokerConfig(env = process.env) {
18
20
  const jwtSecret = env.AGENTHIPPO_AUTH_JWT_SECRET?.trim() || exports.DEFAULT_JWT_SECRET;
19
21
  const publicBaseUrl = (env.AGENTHIPPO_AUTH_PUBLIC_URL?.trim() || `http://127.0.0.1:${env.AGENTHIPPO_AUTH_PORT?.trim() || '9780'}`).replace(/\/+$/, '');
@@ -27,7 +29,10 @@ function loadAuthBrokerConfig(env = process.env) {
27
29
  accessTokenTtlSec: Number(env.AGENTHIPPO_AUTH_ACCESS_TTL_SEC || 3600),
28
30
  refreshTokenTtlSec: Number(env.AGENTHIPPO_AUTH_REFRESH_TTL_SEC || 30 * 24 * 3600),
29
31
  allowedEmailDomains: parseDomains(env.AGENTHIPPO_AUTH_ALLOWED_DOMAINS),
30
- devMode: env.AGENTHIPPO_AUTH_DEV === '1' || env.NODE_ENV === 'test',
32
+ // Deliberately not gated on NODE_ENV=test: that value is too easy to inherit by accident
33
+ // (test runners, base images, wrapper scripts) and this flag unlocks an unauthenticated
34
+ // login bypass (/oauth/dev/login, login_hint auto-auth) — require the explicit opt-in.
35
+ devMode: env.AGENTHIPPO_AUTH_DEV === '1',
31
36
  rateLimitPerMin: env.AGENTHIPPO_AUTH_RATE_LIMIT !== undefined ? Number(env.AGENTHIPPO_AUTH_RATE_LIMIT) : 60,
32
37
  logPii: env.AGENTHIPPO_AUTH_LOG_PII === '1',
33
38
  cookieName: env.AGENTHIPPO_AUTH_COOKIE_NAME?.trim() || 'ah_token',
@@ -41,7 +46,7 @@ function loadAuthBrokerConfig(env = process.env) {
41
46
  storeIndexUrl: env.AGENTHIPPO_TEAM_STORE_INDEX_URL?.trim() || `https://${storeOwner}.github.io/${storeName}`,
42
47
  githubOrg: env.AGENTHIPPO_TEAM_GITHUB_ORG?.trim() || storeOwner,
43
48
  litellmBaseUrl: env.AGENTHIPPO_TEAM_LITELLM_BASE_URL?.trim() || 'http://127.0.0.1:4000',
44
- litellmApiKey: env.AGENTHIPPO_TEAM_LITELLM_API_KEY?.trim() || 'ahk_team_dev_key',
49
+ litellmApiKey: env.AGENTHIPPO_TEAM_LITELLM_API_KEY?.trim() || exports.DEFAULT_LITELLM_API_KEY,
45
50
  },
46
51
  };
47
52
  }
@@ -53,5 +53,7 @@ function codeChallengeFromVerifier(verifier) {
53
53
  return crypto.createHash('sha256').update(verifier).digest('base64url');
54
54
  }
55
55
  function verifyPkceChallenge(verifier, challenge) {
56
- return codeChallengeFromVerifier(verifier) === challenge;
56
+ const expected = Buffer.from(codeChallengeFromVerifier(verifier));
57
+ const actual = Buffer.from(challenge);
58
+ return expected.length === actual.length && crypto.timingSafeEqual(expected, actual);
57
59
  }
@@ -47,7 +47,29 @@ const store_js_1 = require("./store.js");
47
47
  const pkce_js_1 = require("./pkce.js");
48
48
  const jwt_js_1 = require("./jwt.js");
49
49
  const types_js_1 = require("../auth/types.js");
50
+ const callbackUriParse_js_1 = require("../auth/callbackUriParse.js");
50
51
  const serve_hardening_js_1 = require("../cli/serve-hardening.js");
52
+ /**
53
+ * Restrict `redirect_uri` to the shapes our own clients actually send — the CLI/IDE loopback
54
+ * callback (127.0.0.1/localhost, any OS-assigned port, see loopbackAuthServer.ts) and the IDE
55
+ * URI-scheme callback (any editor scheme, fixed authority+path, see callbackUri.ts). Without
56
+ * this, `/oauth/authorize` would hand a real auth code to whatever `redirect_uri` a caller
57
+ * supplies — an attacker can send a victim (who has an active SSO session) a link with their
58
+ * own redirect_uri and code_challenge and steal the resulting code via their own server.
59
+ */
60
+ function isAllowedRedirectUri(redirectUri) {
61
+ let target;
62
+ try {
63
+ target = new url_1.URL(redirectUri);
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ if (target.protocol === 'http:') {
69
+ return (target.hostname === '127.0.0.1' || target.hostname === 'localhost') && target.pathname === '/callback';
70
+ }
71
+ return (0, callbackUriParse_js_1.isAuthCallbackParts)(target.host, target.pathname);
72
+ }
51
73
  function sendJson(res, status, body) {
52
74
  res.writeHead(status, { 'Content-Type': 'application/json' });
53
75
  res.end(JSON.stringify(body));
@@ -114,6 +136,9 @@ function extractCookieToken(cookieHeader, name) {
114
136
  function safeReturnTo(rawReturnTo, cookieDomain, fallback) {
115
137
  try {
116
138
  const target = new url_1.URL(rawReturnTo);
139
+ if (target.protocol !== 'http:' && target.protocol !== 'https:') {
140
+ return fallback;
141
+ }
117
142
  if (cookieDomain) {
118
143
  const parent = cookieDomain.replace(/^\./, '').toLowerCase();
119
144
  if (target.hostname.toLowerCase() === parent || target.hostname.toLowerCase().endsWith('.' + parent)) {
@@ -136,6 +161,12 @@ function createAuthBrokerServer(config) {
136
161
  throw new Error('AGENTHIPPO_AUTH_JWT_SECRET must be set to a strong secret in production. ' +
137
162
  'Generate one with: openssl rand -hex 32');
138
163
  }
164
+ // Fail-closed: a known-default LiteLLM key would hand every team member the same
165
+ // publicly-known credential if the operator forgot to set the real one.
166
+ if (!resolved.devMode && resolved.team.litellmApiKey === config_js_1.DEFAULT_LITELLM_API_KEY) {
167
+ throw new Error('AGENTHIPPO_TEAM_LITELLM_API_KEY must be set in production. ' +
168
+ 'The default dev key must not be issued to real users via bootstrap.');
169
+ }
139
170
  // Warn when no domain restriction is configured: any SSO-authenticated email can sign in.
140
171
  if (!resolved.devMode && resolved.allowedEmailDomains.length === 0) {
141
172
  console.warn('[auth-broker] Warning: AGENTHIPPO_AUTH_ALLOWED_DOMAINS is not set — ' +
@@ -256,6 +287,11 @@ function createAuthBrokerServer(config) {
256
287
  sendJson(res, 400, { error: 'invalid_request' });
257
288
  return;
258
289
  }
290
+ if (!isAllowedRedirectUri(redirectUri)) {
291
+ audit('authorize.invalid_redirect_uri', { client_id: clientId });
292
+ sendJson(res, 400, { error: 'invalid_request', error_description: 'redirect_uri not allowed' });
293
+ return;
294
+ }
259
295
  // Dev mode with explicit login_hint: auto-authenticate (backward compat for tests).
260
296
  if (resolved.devMode && loginHint) {
261
297
  const user = store.upsertUser(loginHint);
@@ -508,10 +544,16 @@ function createAuthBrokerServer(config) {
508
544
  sendJson(res, 404, { error: 'not_found' });
509
545
  }
510
546
  catch (err) {
511
- if (err.status === 413) {
547
+ const status = err.status;
548
+ if (status === 413) {
512
549
  sendJson(res, 413, { error: 'request_entity_too_large' });
513
550
  return;
514
551
  }
552
+ if (status === 403) {
553
+ audit('forbidden', { reason: 'email_domain' });
554
+ sendJson(res, 403, { error: 'forbidden', error_description: 'email domain not allowed' });
555
+ return;
556
+ }
515
557
  // Log the real error internally; return a generic message so clients don't see
516
558
  // stack fragments / internal paths in the 500 body.
517
559
  console.error('[auth-broker] internal error', err);
@@ -123,7 +123,9 @@ class AuthBrokerStore {
123
123
  upsertUser(email, displayName) {
124
124
  const normalized = email.trim().toLowerCase();
125
125
  if (!(0, config_js_1.emailDomainAllowed)(normalized, this.config.allowedEmailDomains)) {
126
- throw new Error(`Email domain not allowed for sign-in: ${normalized}`);
126
+ const err = new Error(`Email domain not allowed for sign-in: ${normalized}`);
127
+ err.status = 403;
128
+ throw err;
127
129
  }
128
130
  for (const user of this.users.values()) {
129
131
  if (user.email === normalized) {
@@ -219,7 +221,6 @@ class AuthBrokerStore {
219
221
  };
220
222
  }
221
223
  buildBootstrap(user) {
222
- const expiresAt = new Date(Date.now() + this.config.accessTokenTtlSec * 1000).toISOString();
223
224
  return {
224
225
  version: 1,
225
226
  user: {
@@ -240,7 +241,8 @@ class AuthBrokerStore {
240
241
  routing: {
241
242
  litellmBaseUrl: this.config.team.litellmBaseUrl,
242
243
  litellmApiKey: this.config.team.litellmApiKey,
243
- litellmApiKeyExpiresAt: expiresAt,
244
+ // No litellmApiKeyExpiresAt: the team key is static — a fabricated expiry
245
+ // would only pretend rotation exists. Per-user virtual keys are the upgrade path.
244
246
  },
245
247
  policy: {
246
248
  byokAllowed: true,
@@ -249,10 +251,9 @@ class AuthBrokerStore {
249
251
  };
250
252
  }
251
253
  refreshRoutingCredentials() {
252
- const litellmApiKeyExpiresAt = new Date(Date.now() + this.config.accessTokenTtlSec * 1000).toISOString();
254
+ // The team key is static (no rotation backend yet), so no expiry is reported.
253
255
  return {
254
256
  litellmApiKey: this.config.team.litellmApiKey,
255
- litellmApiKeyExpiresAt,
256
257
  };
257
258
  }
258
259
  buildMe(user) {
@@ -173,6 +173,9 @@ async function askCommand(options) {
173
173
  apiKey: apiConfig.apiKey,
174
174
  baseUrl: apiConfig.baseUrl,
175
175
  useLiteLLM: apiConfig.useLiteLLM,
176
+ litellmApiKey: apiConfig.litellmApiKey,
177
+ openaiApiKey: apiConfig.openaiApiKey,
178
+ anthropicApiKey: apiConfig.anthropicApiKey,
176
179
  logger,
177
180
  freshState: !options.sessionId?.trim(), // Persist native session when continuing a conversation
178
181
  permissionHandler,
@@ -152,6 +152,9 @@ async function chatCommand(options) {
152
152
  apiKey: apiConfig.apiKey,
153
153
  baseUrl: apiConfig.baseUrl,
154
154
  useLiteLLM: apiConfig.useLiteLLM,
155
+ litellmApiKey: apiConfig.litellmApiKey,
156
+ openaiApiKey: apiConfig.openaiApiKey,
157
+ anthropicApiKey: apiConfig.anthropicApiKey,
155
158
  logger,
156
159
  freshState: false, // Persist session within REPL
157
160
  claudeCodeExecutablePath: claudeCodePath,
@@ -48,9 +48,10 @@ const fs = __importStar(require("fs"));
48
48
  const path = __importStar(require("path"));
49
49
  const yaml = __importStar(require("js-yaml"));
50
50
  const deploy_init_1 = require("../deploy-init");
51
+ const registry_1 = require("../deploy/blueprints/registry");
51
52
  const utils_1 = require("../utils");
52
53
  const DEFAULT_PORT = 8080;
53
- /** Read pack id + engine + model from agent.yaml without the full loader (no workspace/sync needed). */
54
+ /** Read pack id + version + engine + model from agent.yaml without the full loader (no workspace/sync needed). */
54
55
  function readManifest(packDir) {
55
56
  const manifestPath = path.join(packDir, 'agent.yaml');
56
57
  if (!fs.existsSync(manifestPath)) {
@@ -61,9 +62,14 @@ function readManifest(packDir) {
61
62
  if (!name) {
62
63
  throw new Error(`agent.yaml is missing metadata.name in ${packDir}`);
63
64
  }
65
+ const version = parsed?.metadata?.version?.trim();
66
+ if (!version) {
67
+ throw new Error(`agent.yaml is missing metadata.version in ${packDir}`);
68
+ }
64
69
  const model = parsed?.spec?.model?.trim();
65
70
  return {
66
71
  name,
72
+ version,
67
73
  engine: parsed?.spec?.engine?.trim() || 'claude',
68
74
  ...(model ? { model } : {}),
69
75
  };
@@ -123,7 +129,7 @@ function copyPackToStaging(packDir, stagingDir) {
123
129
  return false;
124
130
  if (base.endsWith('.log'))
125
131
  return false;
126
- if (/credentials/i.test(base) && base.endsWith('.json'))
132
+ if ((0, deploy_init_1.isSecretLikeBasename)(base))
127
133
  return false;
128
134
  return true;
129
135
  },
@@ -135,8 +141,35 @@ async function deployCommand(action, packArg, options) {
135
141
  console.error(`Unknown deploy action "${action}". Supported: init`);
136
142
  process.exit(1);
137
143
  }
144
+ // Blueprint resolution (PLAN/deploy.md §3) picks --target/--ingress for you; --platform
145
+ // selects which platform within the blueprint's supported set. Raw --target/--ingress stay
146
+ // fully usable when --blueprint is omitted — nothing here changes that path.
147
+ let resolvedTarget = options.target;
148
+ let resolvedIngress = options.ingress;
149
+ if (options.blueprint) {
150
+ if (!(0, registry_1.isBlueprintId)(options.blueprint)) {
151
+ console.error(`Unknown --blueprint "${options.blueprint}". Supported: ${Object.keys(registry_1.BLUEPRINTS).join(', ')}`);
152
+ process.exit(1);
153
+ return;
154
+ }
155
+ if (options.platform && !(0, deploy_init_1.isDeployTarget)(options.platform)) {
156
+ console.error(`Unknown --platform "${options.platform}". Supported: ${deploy_init_1.DEPLOY_TARGETS.join(', ')}`);
157
+ process.exit(1);
158
+ return;
159
+ }
160
+ try {
161
+ const resolved = (0, registry_1.resolveBlueprint)(options.blueprint, options.platform);
162
+ resolvedTarget = resolved.target;
163
+ resolvedIngress = resolved.ingress;
164
+ }
165
+ catch (err) {
166
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
167
+ process.exit(1);
168
+ return;
169
+ }
170
+ }
138
171
  const packDir = path.resolve(options.packdir || packArg || process.cwd());
139
- const targetsRequested = options.target === 'all' ? [...deploy_init_1.DEPLOY_TARGETS] : [options.target];
172
+ const targetsRequested = resolvedTarget === 'all' ? [...deploy_init_1.DEPLOY_TARGETS] : [resolvedTarget];
140
173
  for (const t of targetsRequested) {
141
174
  if (!(0, deploy_init_1.isDeployTarget)(t)) {
142
175
  console.error(`Unknown --target "${t}". Supported: ${deploy_init_1.DEPLOY_TARGETS.join(', ')}, all`);
@@ -162,8 +195,14 @@ async function deployCommand(action, packArg, options) {
162
195
  }
163
196
  }
164
197
  }
198
+ const modelRouting = options.modelRouting === 'gateway' ? 'gateway' : options.modelRouting === 'sidecar' ? 'sidecar' : undefined;
199
+ if (options.modelRouting && options.modelRouting !== 'gateway' && options.modelRouting !== 'sidecar') {
200
+ console.error(`Unknown --model-routing "${options.modelRouting}". Supported: sidecar, gateway`);
201
+ process.exit(1);
202
+ }
165
203
  const ctx = {
166
204
  packName: manifest.name,
205
+ packVersion: manifest.version,
167
206
  image: options.image || (0, deploy_init_1.defaultRuntimeImage)(manifest.engine),
168
207
  port: options.port || DEFAULT_PORT,
169
208
  engine: manifest.engine,
@@ -171,18 +210,17 @@ async function deployCommand(action, packArg, options) {
171
210
  envVars,
172
211
  gatewayChannels: gateway,
173
212
  publicHostname: (0, deploy_init_1.resolveDeployPublicHostname)(options.hostname),
174
- ingress: options.ingress,
213
+ ingress: resolvedIngress,
175
214
  idpUrl: options.idpUrl,
176
215
  cookieDomain: options.cookieDomain,
216
+ colocate: options.colocate,
217
+ modelRouting,
177
218
  };
178
219
  for (const target of targetsRequested) {
179
- // Staging dir = a self-contained pack (pack content + generated deploy/). Default
180
- // ./deploy-out/<pack>-<target> (cwd-relative); --out overrides (per-target subdir if
181
- // multiple targets). Build context = staging root, so COPY . /app/pack/ is correct
182
- // regardless of where the script is invoked from.
220
+ const routingSuffix = modelRouting === 'gateway' ? '-gateway' : '';
183
221
  const outDir = options.out
184
- ? (targetsRequested.length > 1 ? path.join(path.resolve(options.out), target) : path.resolve(options.out))
185
- : path.join(process.cwd(), 'deploy-out', `${manifest.name}-${target}`);
222
+ ? (targetsRequested.length > 1 ? path.join(path.resolve(options.out), target + routingSuffix) : path.resolve(options.out))
223
+ : path.join(process.cwd(), 'deploy-out', `${manifest.name}-${target}${routingSuffix}`);
186
224
  fs.mkdirSync(outDir, { recursive: true });
187
225
  // Copy the pack into the staging root (filtered: no VCS/caches/secrets, no source deploy/).
188
226
  const brokenLinks = copyPackToStaging(packDir, outDir);
@@ -220,4 +258,5 @@ async function deployCommand(action, packArg, options) {
220
258
  console.log(`\n${utils_1.COLORS.dim}Scanned pack secrets into .env.example: ${envVars.join(', ')}${utils_1.COLORS.reset}`);
221
259
  }
222
260
  console.log(`\nNext: cd into the staging dir above, \`cp deploy/.env.example deploy/.env\`, then run the command in deploy/README.md (scripts anchor themselves to the staging root).`);
261
+ console.log(`${utils_1.COLORS.dim}Tip: run deploy init from the pack's parent directory — not from inside the pack (default deploy-out/ inside the pack copies into itself and fails).${utils_1.COLORS.reset}`);
223
262
  }
@@ -60,7 +60,61 @@ async function ensureStoreFileExists(storePath) {
60
60
  process.exit(1);
61
61
  }
62
62
  }
63
+ /**
64
+ * Broker-mediated send (secure-obo-design §7.6): under `agenthippo serve` with the egress
65
+ * broker active, the agent's env carries AGENTHIPPO_EGRESS_URL + AGENTHIPPO_TURN_NONCE but NOT
66
+ * the gateway token. Route the send through the broker, which resolves the destination
67
+ * server-side from THIS turn's bound handles and delivers with the broker-held token.
68
+ * `--outbound`/`--agent` are ignored here — the broker derives both from the verified nonce.
69
+ */
70
+ async function sendViaEgressBroker(egressUrl, nonce, options) {
71
+ const message = options.message.trim();
72
+ if (!message) {
73
+ console.error('Error: --message is required');
74
+ process.exit(1);
75
+ }
76
+ if (options.dryRun) {
77
+ console.log(`${utils_js_1.COLORS.dim}Dry run (egress broker):${utils_js_1.COLORS.reset} POST ${egressUrl} { to: ${options.target ?? '(reply-to-sender)'} }`);
78
+ process.exit(0);
79
+ }
80
+ let res;
81
+ try {
82
+ res = await fetch(egressUrl, {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${nonce}` },
85
+ body: JSON.stringify({ message, to: options.target?.trim() || undefined, media: options.media?.trim() || undefined }),
86
+ });
87
+ }
88
+ catch (err) {
89
+ console.error(`${utils_js_1.COLORS.red}✗${utils_js_1.COLORS.reset} Egress broker unreachable: ${err instanceof Error ? err.message : String(err)}`);
90
+ process.exit(1);
91
+ }
92
+ const bodyText = await res.text();
93
+ let body = {};
94
+ try {
95
+ body = JSON.parse(bodyText);
96
+ }
97
+ catch { /* non-JSON error body */ }
98
+ if (options.json) {
99
+ console.log(JSON.stringify({ via: 'egress-broker', status: res.status, result: body }, null, 2));
100
+ }
101
+ else if (res.ok && body.ok) {
102
+ console.log(`${utils_js_1.COLORS.green}✓${utils_js_1.COLORS.reset} Sent (via egress broker${body.via ? `, ${body.via}` : ''})`);
103
+ }
104
+ else {
105
+ console.error(`${utils_js_1.COLORS.red}✗${utils_js_1.COLORS.reset} Egress send rejected (HTTP ${res.status}): ${body.error || bodyText || 'unknown error'}`);
106
+ process.exit(1);
107
+ }
108
+ process.exit(res.ok && body.ok ? 0 : 1);
109
+ }
63
110
  async function gatewaySendCommand(options) {
111
+ // Prefer the broker whenever serve exposes it: the agent has no gateway token to send directly.
112
+ const egressUrl = process.env.AGENTHIPPO_EGRESS_URL?.trim();
113
+ const nonce = process.env.AGENTHIPPO_TURN_NONCE?.trim();
114
+ if (egressUrl && nonce) {
115
+ await sendViaEgressBroker(egressUrl, nonce, options);
116
+ return;
117
+ }
64
118
  const storePath = resolveOutboundPath(options.outbound);
65
119
  await ensureStoreFileExists(storePath);
66
120
  const agentId = options.agent.trim();