@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
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  /*---------------------------------------------------------------------------------------------
3
3
  * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ * Licensed under the MIT License. See LICENSE.txt in the project root for license information.
4
5
  *--------------------------------------------------------------------------------------------*/
5
6
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
7
  if (k2 === undefined) k2 = k;
@@ -37,6 +38,8 @@ var __importStar = (this && this.__importStar) || (function () {
37
38
  })();
38
39
  Object.defineProperty(exports, "__esModule", { value: true });
39
40
  exports.ObservableStreamEmitter = exports.SessionEventWriter = void 0;
41
+ exports.getSessionEventWriterStats = getSessionEventWriterStats;
42
+ exports.resetSessionEventWriterStatsForTest = resetSessionEventWriterStatsForTest;
40
43
  exports.wrapEmitterWithObservability = wrapEmitterWithObservability;
41
44
  /**
42
45
  * Session Event Writer & Observable Stream Emitter
@@ -47,25 +50,51 @@ exports.wrapEmitterWithObservability = wrapEmitterWithObservability;
47
50
  *
48
51
  * Events are written to ~/.agent-hippo/analytics/sessions/<session_id>.jsonl
49
52
  * and queryable by DuckDB alongside OTEL traces.
53
+ *
54
+ * PRIVACY: user identifiers are salted hashes and tool I/O is a sha256 + length, unless an
55
+ * operator opts in — see ./identity.ts. These records are shipped wholesale to the evidence
56
+ * server, so whatever lands in this file leaves the machine.
50
57
  */
51
58
  const path = __importStar(require("path"));
52
59
  const fs_1 = require("fs");
53
60
  const branding_1 = require("../branding");
54
- const credential_context_1 = require("../cli/credential-context");
55
61
  const os = __importStar(require("os"));
56
- // -----------------------------------------------------------------------------
57
- // Session Event Writer
58
- // -----------------------------------------------------------------------------
62
+ const identity_1 = require("./identity");
63
+ /** Process-wide counters, aggregated across every writer instance. */
64
+ const globalStats = {
65
+ written: 0,
66
+ failed: 0,
67
+ };
59
68
  /**
60
- * Append-only JSONL writer for session events.
61
- * One file per session: ~/.agent-hippo/analytics/sessions/<session_id>.jsonl
69
+ * Process-wide evidence-write health, for /health-style output.
62
70
  *
63
- * Design: write-through (no buffering). Each event appends immediately.
64
- * JSONL is safe for concurrent writes and crash-safe.
71
+ * `failed > 0` means at least one consequential act produced no record (R-1 completeness).
72
+ */
73
+ function getSessionEventWriterStats() {
74
+ return { written: globalStats.written, failed: globalStats.failed, lastError: globalStats.lastError };
75
+ }
76
+ /** Reset the process-wide counters. Tests only. */
77
+ function resetSessionEventWriterStatsForTest() {
78
+ globalStats.written = 0;
79
+ globalStats.failed = 0;
80
+ globalStats.lastError = undefined;
81
+ }
82
+ /**
83
+ * Set when evidence is REQUIRED: a failed write then rejects rather than returning, so the turn
84
+ * fails instead of proceeding unrecorded. Off by default — an IDE user should not lose a turn
85
+ * because a disk was full — but a regulated deployment wants the opposite trade.
65
86
  */
87
+ const EVIDENCE_REQUIRED_ENV = 'AGENTHIPPO_EVIDENCE_REQUIRED';
88
+ function evidenceRequired(env = process.env) {
89
+ const v = (env[EVIDENCE_REQUIRED_ENV] ?? '').trim().toLowerCase();
90
+ return v === '1' || v === 'true' || v === 'yes' || v === 'on';
91
+ }
66
92
  class SessionEventWriter {
67
93
  baseDir;
68
94
  ensuredDirs = new Set();
95
+ written = 0;
96
+ failed = 0;
97
+ lastError;
69
98
  sanitizeSessionId(sessionId) {
70
99
  const trimmed = sessionId.trim();
71
100
  if (!trimmed) {
@@ -79,6 +108,14 @@ class SessionEventWriter {
79
108
  }
80
109
  /**
81
110
  * Append a session event to the session's JSONL file.
111
+ *
112
+ * A failed write is a HOLE IN THE RECORD, not a nuisance: the act still happened, and nothing
113
+ * downstream can tell the difference between "no tool ran" and "the tool event never landed".
114
+ * So every failure is logged at error level and counted into /health-visible state. With
115
+ * AGENTHIPPO_EVIDENCE_REQUIRED set it also rejects, so the caller fails the turn rather than
116
+ * completing it unrecorded.
117
+ *
118
+ * This used to swallow every error in an empty catch block — silence on every failure.
82
119
  */
83
120
  async append(sessionId, requestId, eventType, payload) {
84
121
  try {
@@ -98,11 +135,27 @@ class SessionEventWriter {
98
135
  const filePath = path.join(this.baseDir, `${safeSessionId}.jsonl`);
99
136
  const line = JSON.stringify(event) + '\n';
100
137
  await fs_1.promises.appendFile(filePath, line);
138
+ this.written++;
139
+ globalStats.written++;
101
140
  }
102
- catch {
103
- // Best-effort: don't crash the agent if event writing fails
141
+ catch (err) {
142
+ const message = err instanceof Error ? err.message : String(err);
143
+ this.failed++;
144
+ globalStats.failed++;
145
+ const record = { at: Date.now(), message, eventType };
146
+ this.lastError = record;
147
+ globalStats.lastError = record;
148
+ console.error(`[evidence] LOST session event ${eventType} (session=${sessionId} request=${requestId}): ` +
149
+ `${message} — ${globalStats.failed} evidence write(s) lost since start`);
150
+ if (evidenceRequired()) {
151
+ throw new Error(`evidence write failed for ${eventType}: ${message}`);
152
+ }
104
153
  }
105
154
  }
155
+ /** Per-writer evidence-write health. See {@link getSessionEventWriterStats} for the process view. */
156
+ getStats() {
157
+ return { written: this.written, failed: this.failed, lastError: this.lastError };
158
+ }
106
159
  /**
107
160
  * Get the sessions directory path (for DuckDB queries).
108
161
  */
@@ -127,13 +180,43 @@ class ObservableStreamEmitter {
127
180
  writer;
128
181
  sessionId;
129
182
  requestId;
183
+ stamp;
130
184
  missingCallIdCounter = 0;
131
185
  pendingSyntheticCallIds = new Map();
132
- constructor(inner, writer, sessionId, requestId) {
186
+ /**
187
+ * Salted pseudonym of the acting user — the warrant, in the form the evidence index wants
188
+ * (PLAN/compliance.md §2 "salted identity hashes"). Raw only under
189
+ * AGENTHIPPO_EVIDENCE_RAW_IDS=1. Derived in the constructor; the raw id is never a field.
190
+ */
191
+ actorId;
192
+ constructor(inner, writer, sessionId, requestId,
193
+ /**
194
+ * Acting user for the warrant audit trail on tool events. Captured once at
195
+ * wrap time (still inside the request's credential scope) and stored on this
196
+ * per-turn emitter — never re-read from ambient AsyncLocalStorage at emit
197
+ * time. Custom-engine persistent workers deliver tool events on a detached
198
+ * parent IPC callback whose ALS can stick to the spawn request; constructor
199
+ * capture keeps attribution correct for every engine. Undefined outside a
200
+ * serve credential scope (IDE-local / CLI without runWithCredential).
201
+ *
202
+ * NOT a field: it is hashed into `actorId` in the constructor and the raw value is then
203
+ * unreachable from any emit path.
204
+ */
205
+ userId,
206
+ /**
207
+ * Provenance stamp merged into turn_start (policy_version, pack_digest, engine_version,
208
+ * tier) — which rules and which artifacts governed this turn, recorded at the moment of
209
+ * the act rather than reconstructed later (PLAN/compliance.md §3.1).
210
+ */
211
+ stamp) {
133
212
  this.inner = inner;
134
213
  this.writer = writer;
135
214
  this.sessionId = sessionId;
136
215
  this.requestId = requestId;
216
+ this.stamp = stamp;
217
+ // Hash once at construction: the raw id is never stored on this object, so no later edit
218
+ // to an emit path can leak it by reaching for `this.userId`.
219
+ this.actorId = (0, identity_1.hashIdentity)(userId);
137
220
  }
138
221
  makeCallIdKey(toolName) {
139
222
  return `${this.sessionId}:${this.requestId}:${toolName}`;
@@ -174,11 +257,12 @@ class ObservableStreamEmitter {
174
257
  await this.writer.append(this.sessionId, this.requestId, 'tool_start', {
175
258
  toolName,
176
259
  callId: loggedCallId,
177
- // Truncate input to avoid huge JSONL files
178
- inputPreview: input?.slice(0, 500),
179
- // The warrant: who this tool call ran as, read live at call time (never cached).
180
- // Undefined outside a runWithCredential scope (e.g. IDE-local, non-serve usage).
181
- userId: (0, credential_context_1.currentCredential)()?.userId,
260
+ // Tool arguments are content — paths, queries, credentials people paste into prompts.
261
+ // Digest by default (input_sha256 + input_length); `inputPreview` returns only under
262
+ // AGENTHIPPO_EVIDENCE_CONTENT_CAPTURE=1.
263
+ ...(0, identity_1.describeContent)('input', input),
264
+ // The warrant: who this tool call ran as (captured at wrap; see constructor).
265
+ userId: this.actorId,
182
266
  });
183
267
  return this.inner.toolStart(toolName, input, toolCallId, metadata);
184
268
  }
@@ -190,9 +274,10 @@ class ObservableStreamEmitter {
190
274
  toolName,
191
275
  callId: loggedCallId,
192
276
  isError: isError ?? false,
193
- // Truncate result to avoid huge JSONL files
194
- resultPreview: result?.slice(0, 500),
195
- userId: (0, credential_context_1.currentCredential)()?.userId,
277
+ // Tool output is content — file contents, command output, customer data. Digest by
278
+ // default; `resultPreview` returns only under AGENTHIPPO_EVIDENCE_CONTENT_CAPTURE=1.
279
+ ...(0, identity_1.describeContent)('result', result),
280
+ userId: this.actorId,
196
281
  });
197
282
  return this.inner.toolEnd(toolName, toolCallId, result, isError, metadata);
198
283
  }
@@ -205,6 +290,7 @@ class ObservableStreamEmitter {
205
290
  async error(message) {
206
291
  await this.writer.append(this.sessionId, this.requestId, 'error', {
207
292
  message,
293
+ userId: this.actorId,
208
294
  });
209
295
  return this.inner.error(message);
210
296
  }
@@ -215,7 +301,7 @@ class ObservableStreamEmitter {
215
301
  * Emit a turn_start event. Call at the beginning of stream().
216
302
  */
217
303
  async emitTurnStart() {
218
- await this.writer.append(this.sessionId, this.requestId, 'turn_start', {});
304
+ await this.writer.append(this.sessionId, this.requestId, 'turn_start', { userId: this.actorId, ...this.stamp });
219
305
  }
220
306
  /**
221
307
  * Emit a turn_end event. Call at the end of stream().
@@ -223,6 +309,7 @@ class ObservableStreamEmitter {
223
309
  async emitTurnEnd(success) {
224
310
  await this.writer.append(this.sessionId, this.requestId, 'turn_end', {
225
311
  success,
312
+ userId: this.actorId,
226
313
  });
227
314
  }
228
315
  }
@@ -231,6 +318,6 @@ exports.ObservableStreamEmitter = ObservableStreamEmitter;
231
318
  * Create an ObservableStreamEmitter that wraps the given emitter.
232
319
  * Returns both the wrapped emitter and emit helpers for turn boundaries.
233
320
  */
234
- function wrapEmitterWithObservability(inner, writer, sessionId, requestId) {
235
- return new ObservableStreamEmitter(inner, writer, sessionId, requestId);
321
+ function wrapEmitterWithObservability(inner, writer, sessionId, requestId, userId, stamp) {
322
+ return new ObservableStreamEmitter(inner, writer, sessionId, requestId, userId, stamp);
236
323
  }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.assertSafeAuthBaseUrl = void 0;
7
+ exports.pickTrustedAuthBaseUrl = pickTrustedAuthBaseUrl;
8
+ exports.resolveAuthBrokerBaseUrl = resolveAuthBrokerBaseUrl;
9
+ /**
10
+ * Where `agentide.auth.baseUrl` is allowed to come from.
11
+ *
12
+ * The setting is declared `"scope": "application"` in package.json, which is what actually stops a
13
+ * repo's `.vscode/settings.json` from setting it. This module is the defence in depth behind that
14
+ * declaration: it re-derives the value from `inspect()` and takes ONLY the user/application/default
15
+ * layers, so even if the contribution scope is ever loosened by accident (or the value arrives
16
+ * through a remote/folder layer this extension does not expect), a workspace-supplied endpoint is
17
+ * dropped rather than trusted.
18
+ *
19
+ * Why this matters concretely: the resolved URL receives the PKCE authorization code, then
20
+ * `Authorization: Bearer <AgentHippo access token>` on `/api/ide/bootstrap`, and the response
21
+ * configures team store settings plus `agentide.AgentHippoModelGateway.url` (application-scoped).
22
+ * The access token is also synced to the `agenthippo` model provider. A workspace that could
23
+ * steer the auth endpoint would capture the token AND every subsequent prompt.
24
+ */
25
+ const brokerClient_js_1 = require("./brokerClient.js");
26
+ Object.defineProperty(exports, "assertSafeAuthBaseUrl", { enumerable: true, get: function () { return brokerClient_js_1.assertSafeAuthBaseUrl; } });
27
+ /**
28
+ * Pick the auth base URL from the trusted layers only.
29
+ *
30
+ * Precedence matches VS Code's own (most specific trusted layer wins): remote user → local user →
31
+ * user → default. `workspaceValue` / `workspaceFolderValue` are never consulted for the result;
32
+ * they are only reported so the caller can warn.
33
+ */
34
+ function pickTrustedAuthBaseUrl(inspect) {
35
+ if (!inspect) {
36
+ return {};
37
+ }
38
+ const ignored = inspect.workspaceFolderValue?.trim() || inspect.workspaceValue?.trim() || undefined;
39
+ const trusted = inspect.globalRemoteValue?.trim() ||
40
+ inspect.globalLocalValue?.trim() ||
41
+ inspect.globalValue?.trim() ||
42
+ inspect.defaultValue?.trim() ||
43
+ undefined;
44
+ // A workspace value that merely repeats the trusted value is not an override worth warning about.
45
+ const ignoredWorkspaceValue = ignored && ignored !== trusted ? ignored : undefined;
46
+ return { value: trusted, ...(ignoredWorkspaceValue ? { ignoredWorkspaceValue } : {}) };
47
+ }
48
+ /**
49
+ * Resolve the auth broker base URL from configuration, dropping workspace-scoped values and
50
+ * enforcing the https/loopback requirement. Throws (with an actionable message) on a bad URL.
51
+ */
52
+ function resolveAuthBrokerBaseUrl(config, warn) {
53
+ const picked = pickTrustedAuthBaseUrl(config?.inspect('auth.baseUrl'));
54
+ if (picked.ignoredWorkspaceValue) {
55
+ warn?.(`[AgentIDE] Ignoring workspace-provided "agentide.auth.baseUrl" (${picked.ignoredWorkspaceValue}). ` +
56
+ `This endpoint receives your AgentHippo access token, so it is only honoured from user/application settings.`);
57
+ }
58
+ return (0, brokerClient_js_1.getAuthBrokerBaseUrl)(picked.value);
59
+ }
@@ -19,7 +19,6 @@ async function applyIdeBootstrap(bootstrap, target) {
19
19
  const skipped = [];
20
20
  const teamManaged = bootstrap.policy.teamManagedSettings ?? [];
21
21
  const storeConfig = target.getConfiguration('agentide');
22
- const litellmConfig = target.getConfiguration('agentide.providers.litellm');
23
22
  if (shouldApplySetting(target.globalStateGet(exports.TEAM_MANAGED_STORE_KEY), teamManaged, 'store')) {
24
23
  await storeConfig.update('storeRepo', bootstrap.store.repo, target.ConfigurationTarget.Global);
25
24
  await storeConfig.update('store.indexUrl', bootstrap.store.indexUrl, target.ConfigurationTarget.Global);
@@ -29,22 +28,19 @@ async function applyIdeBootstrap(bootstrap, target) {
29
28
  else {
30
29
  skipped.push('store');
31
30
  }
31
+ // Routing configures the `agenthippo` provider slot ONLY. The `litellm` slot stays user-owned
32
+ // (a developer's local Spotlight proxy) — sign-in must never overwrite it. No credential is
33
+ // written here: the account JWT is synced to the provider by the auth provider's timer, and
34
+ // the bootstrap carries no routing key at all.
32
35
  if (shouldApplySetting(target.globalStateGet(exports.TEAM_MANAGED_ROUTING_KEY), teamManaged, 'routing')) {
33
- await litellmConfig.update('baseUrl', bootstrap.routing.litellmBaseUrl, target.ConfigurationTarget.Global);
34
- if (target.setParticipantApiKey) {
35
- await target.setParticipantApiKey({
36
- provider: 'litellm',
37
- apiKey: bootstrap.routing.litellmApiKey,
38
- baseUrl: bootstrap.routing.litellmBaseUrl,
39
- });
40
- }
36
+ await storeConfig.update('AgentHippoModelGateway.url', bootstrap.routing.inferenceGatewayUrl, target.ConfigurationTarget.Global);
41
37
  await target.globalStateUpdate(exports.TEAM_MANAGED_ROUTING_KEY, teamManaged.includes('routing'));
42
38
  applied.push('routing');
43
39
  }
44
40
  else {
45
41
  skipped.push('routing');
46
42
  }
47
- await target.globalStateUpdate(exports.TEAM_ID_KEY, bootstrap.team.id);
43
+ await target.globalStateUpdate(exports.TEAM_ID_KEY, bootstrap.team?.id);
48
44
  await target.globalStateUpdate(exports.TEAM_LAST_BOOTSTRAP_KEY, new Date().toISOString());
49
45
  await target.globalStateUpdate(exports.TEAM_BOOTSTRAP_CACHE_KEY, bootstrap);
50
46
  return { applied, skipped };
@@ -59,6 +55,9 @@ async function revertTeamManagedBootstrap(target) {
59
55
  await storeConfig.update('store.indexUrl', 'https://agenthippoai.github.io/agenthippo-store', target.ConfigurationTarget.Global);
60
56
  }
61
57
  if (routingManaged) {
58
+ await storeConfig.update('AgentHippoModelGateway.url', undefined, target.ConfigurationTarget.Global);
59
+ // Clean up state written by pre-separation installs, where team routing overwrote the
60
+ // user-owned litellm slot with the team base URL and a shared key.
62
61
  await litellmConfig.update('baseUrl', '', target.ConfigurationTarget.Global);
63
62
  if (target.setParticipantApiKey) {
64
63
  await target.setParticipantApiKey({ provider: 'litellm', apiKey: '', baseUrl: '' });
@@ -3,21 +3,66 @@
3
3
  * Copyright (c) AgentHippo.ai. All rights reserved.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_AUTH_BROKER_BASE_URL = void 0;
7
+ exports.isLoopbackAuthHost = isLoopbackAuthHost;
8
+ exports.assertSafeAuthBaseUrl = assertSafeAuthBaseUrl;
6
9
  exports.getAuthBrokerBaseUrl = getAuthBrokerBaseUrl;
7
10
  exports.devLoginForTests = devLoginForTests;
8
11
  exports.exchangeAuthorizationCode = exchangeAuthorizationCode;
9
12
  exports.refreshAccessToken = refreshAccessToken;
10
- exports.fetchBootstrapRefresh = fetchBootstrapRefresh;
11
13
  exports.fetchBootstrap = fetchBootstrap;
12
14
  exports.fetchMe = fetchMe;
13
15
  exports.buildAuthorizeUrl = buildAuthorizeUrl;
14
16
  exports.completeDevPkceLogin = completeDevPkceLogin;
15
17
  const types_js_1 = require("./types.js");
16
18
  const pkce_js_1 = require("./pkce.js");
19
+ exports.DEFAULT_AUTH_BROKER_BASE_URL = 'https://auth.agenthippo.ai';
20
+ /** Hostnames for which plain `http://` is accepted — an operator running a broker locally. */
21
+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
22
+ function isLoopbackAuthHost(hostname) {
23
+ return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase());
24
+ }
25
+ /**
26
+ * Refuse an auth base URL that cannot safely carry a bearer token.
27
+ *
28
+ * Every request built from this value carries either the PKCE authorization code or the user's
29
+ * AgentHippo access token (`/oauth/token`, `/api/ide/bootstrap`, `/api/ide/me`), and the bootstrap
30
+ * response is written straight into the user's model-routing settings. A cleartext or non-http(s)
31
+ * endpoint therefore leaks the credential on the wire; anything but https is refused unless the
32
+ * host is loopback, which is the local-development case and never crosses a network.
33
+ *
34
+ * Returns the normalized (trailing-slash-stripped) URL, or throws with an actionable message.
35
+ */
36
+ function assertSafeAuthBaseUrl(value) {
37
+ const trimmed = value.trim();
38
+ let url;
39
+ try {
40
+ url = new URL(trimmed);
41
+ }
42
+ catch {
43
+ throw new Error(`AgentHippo auth base URL "${trimmed}" is not a valid URL. ` +
44
+ `Fix "agentide.auth.baseUrl" (or AGENTHIPPO_AUTH_BASE_URL), or clear it to use ${exports.DEFAULT_AUTH_BROKER_BASE_URL}.`);
45
+ }
46
+ if (url.protocol === 'https:') {
47
+ return trimmed.replace(/\/+$/, '');
48
+ }
49
+ if (url.protocol === 'http:' && isLoopbackAuthHost(url.hostname)) {
50
+ return trimmed.replace(/\/+$/, '');
51
+ }
52
+ throw new Error(`Refusing to use AgentHippo auth base URL "${trimmed}": it must be https:// ` +
53
+ `(plain http:// is accepted only for loopback hosts such as http://127.0.0.1:9780). ` +
54
+ `Your AgentHippo access token is sent to this host, so it is never sent over cleartext. ` +
55
+ `Fix "agentide.auth.baseUrl" (or AGENTHIPPO_AUTH_BASE_URL), or clear it to use ${exports.DEFAULT_AUTH_BROKER_BASE_URL}.`);
56
+ }
57
+ /**
58
+ * Resolve the auth broker base URL. `configured` must already have been filtered for scope by the
59
+ * caller (see `auth/authBaseUrl.ts` — a workspace-scoped value is dropped before it gets here);
60
+ * this function is the second gate and enforces the transport requirement.
61
+ */
17
62
  function getAuthBrokerBaseUrl(configured) {
18
63
  const fromEnv = process.env.AGENTHIPPO_AUTH_BASE_URL?.trim();
19
- const value = configured?.trim() || fromEnv || 'https://auth.agenthippo.ai';
20
- return value.replace(/\/+$/, '');
64
+ const value = configured?.trim() || fromEnv || exports.DEFAULT_AUTH_BROKER_BASE_URL;
65
+ return assertSafeAuthBaseUrl(value);
21
66
  }
22
67
  async function devLoginForTests(baseUrl, email, redirectUri, codeChallenge) {
23
68
  const res = await fetch(`${baseUrl}/oauth/dev/login`, {
@@ -73,17 +118,6 @@ async function refreshAccessToken(baseUrl, refreshToken) {
73
118
  }
74
119
  return res.json();
75
120
  }
76
- async function fetchBootstrapRefresh(baseUrl, accessToken) {
77
- const res = await fetch(`${baseUrl}/api/ide/bootstrap/refresh`, {
78
- method: 'POST',
79
- headers: { Authorization: `Bearer ${accessToken}` },
80
- });
81
- if (!res.ok) {
82
- const text = await res.text();
83
- throw new Error(`bootstrap refresh failed: ${res.status} ${text}`.trim());
84
- }
85
- return res.json();
86
- }
87
121
  async function fetchBootstrap(baseUrl, accessToken) {
88
122
  const res = await fetch(`${baseUrl}/api/ide/bootstrap`, {
89
123
  headers: { Authorization: `Bearer ${accessToken}` },
@@ -118,7 +152,7 @@ function buildAuthorizeUrl(baseUrl, redirectUri, codeChallenge, state, loginHint
118
152
  }
119
153
  return url.toString();
120
154
  }
121
- async function completeDevPkceLogin(baseUrl, email, redirectUri = 'agenthippo://auth/callback') {
155
+ async function completeDevPkceLogin(baseUrl, email, redirectUri = 'http://127.0.0.1:1/callback') {
122
156
  const verifier = (0, pkce_js_1.generateCodeVerifier)();
123
157
  const challenge = (0, pkce_js_1.codeChallengeFromVerifier)(verifier);
124
158
  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
+ }
@@ -41,14 +41,14 @@ exports.registerAgentHippoAuth = registerAgentHippoAuth;
41
41
  exports.refreshBootstrapIfSignedIn = refreshBootstrapIfSignedIn;
42
42
  exports.startAgentHippoTokenRefreshTimer = startAgentHippoTokenRefreshTimer;
43
43
  const vscode = __importStar(require("vscode"));
44
- const brokerClient_js_1 = require("./brokerClient.js");
44
+ const authBaseUrl_js_1 = require("./authBaseUrl.js");
45
45
  const bootstrapApply_js_1 = require("./bootstrapApply.js");
46
46
  const types_js_1 = require("./types.js");
47
47
  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
  }
@@ -111,7 +111,7 @@ class AgentHippoAuthProvider {
111
111
  const stored = await (0, sessionStore_js_1.getStoredAuthSession)(this.context.secrets);
112
112
  const removedSession = stored ? (0, sessionStore_js_1.storedSessionToVsCode)(stored) : undefined;
113
113
  if (stored) {
114
- const baseUrl = (0, brokerClient_js_1.getAuthBrokerBaseUrl)(vscode.workspace.getConfiguration('agentide').get('auth.baseUrl'));
114
+ const baseUrl = (0, authBaseUrl_js_1.resolveAuthBrokerBaseUrl)(vscode.workspace.getConfiguration('agentide'), (m) => console.warn(m));
115
115
  try {
116
116
  await fetch(`${baseUrl}/api/ide/signout`, {
117
117
  method: 'POST',
@@ -184,20 +184,17 @@ async function refreshBootstrapIfSignedIn(context) {
184
184
  void syncAgentHippoModelGatewayToken(token.accessToken);
185
185
  }
186
186
  /**
187
- * Refresh cadence for the model-gateway JWT and LiteLLM routing key. The access token TTL is
188
- * ~1h; getAgentHippoAccessToken refreshes it inside a 5-min pre-expiry window, so a 60s tick
189
- * always lands inside that window with buffer and re-syncs the agenthippo provider key before
190
- * the old JWT expires (fixes stale-key 401s on long sessions). Routing credentials are refreshed
191
- * less often (every ~5 min) so a server-side rotation of the team LiteLLM key is picked up
192
- * without re-sign-in. Both syncs are diff-gated so we don't churn SecretStorage / settings /
193
- * onDidChangeKeys every tick when nothing changed.
187
+ * Refresh cadence for the model-gateway JWT. The access token TTL is ~1h;
188
+ * getAgentHippoAccessToken refreshes it inside a 5-min pre-expiry window, so a 60s tick always
189
+ * lands inside that window with buffer and re-syncs the agenthippo provider key before the old
190
+ * JWT expires (fixes stale-key 401s on long sessions). The sync is diff-gated so we don't churn
191
+ * SecretStorage / onDidChangeKeys every tick when nothing changed. There is no routing-key
192
+ * refresh: the bootstrap carries no routing credential — the JWT is the credential.
194
193
  */
195
194
  const TOKEN_REFRESH_INTERVAL_MS = 60_000;
196
- const ROUTING_REFRESH_EVERY_N_TICKS = 5;
197
195
  function startAgentHippoTokenRefreshTimer(context) {
198
196
  let lastSyncedAccessToken = '';
199
197
  let running = false;
200
- let tick = 0;
201
198
  const handle = setInterval(() => {
202
199
  // Guard against overlapping ticks: a stalled fetch shouldn't let the next tick stack up.
203
200
  if (running) {
@@ -219,16 +216,6 @@ function startAgentHippoTokenRefreshTimer(context) {
219
216
  lastSyncedAccessToken = token.accessToken;
220
217
  }
221
218
  }
222
- // Routing key rotation is rare; refresh infrequently and let the helper skip the
223
- // settings write when the key hasn't changed.
224
- if (++tick % ROUTING_REFRESH_EVERY_N_TICKS === 0) {
225
- try {
226
- await (0, sessionStore_js_1.refreshRoutingCredentialsForContext)(context, token.accessToken);
227
- }
228
- catch {
229
- // best-effort routing refresh; next cycle retries
230
- }
231
- }
232
219
  }
233
220
  finally {
234
221
  running = false;