@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
@@ -2058,12 +2058,23 @@ class AnalyticsDashboardProvider {
2058
2058
  await fs_1.promises.mkdir(panelDir, { recursive: true });
2059
2059
  const webviewBasePath = this.getWebviewPath();
2060
2060
  const bundledDir = path.join(this.extensionContext.extensionPath, webviewBasePath);
2061
- // Bundled source → user destination filename
2061
+ // Bundled source → user destination filename.
2062
+ // panel.js is deliberately NOT seeded: getHtmlContent always loads the bundled panel.js,
2063
+ // so a seeded copy is never executed. Writing one only created a file that looks editable,
2064
+ // silently diverges from the bundle, and makes "my dashboard edit did nothing" reports.
2062
2065
  const seedMap = [
2063
- { bundled: 'panel.js', userFile: 'panel.js' },
2064
2066
  { bundled: 'styles.css', userFile: 'panel.css' },
2065
2067
  { bundled: 'analytics-panel.html', userFile: 'panel.html' },
2066
2068
  ];
2069
+ // Explain a pre-existing seeded copy rather than deleting the user's file.
2070
+ try {
2071
+ await fs_1.promises.access(path.join(panelDir, 'panel.js'));
2072
+ this.outputChannel.appendLine(`[Dashboard] Note: ${path.join(panelDir, 'panel.js')} is not used — the bundled panel.js always loads. ` +
2073
+ `Edit panel.html / panel.css to customize the dashboard.`);
2074
+ }
2075
+ catch {
2076
+ // No stale copy — nothing to explain.
2077
+ }
2067
2078
  for (const { bundled, userFile } of seedMap) {
2068
2079
  const dest = path.join(panelDir, userFile);
2069
2080
  try {
@@ -5,8 +5,9 @@
5
5
  * Handles artifact display, rendering, and interactions.
6
6
  * Uses panelApi (provided by runtime-bridge.js) instead of raw VS Code API.
7
7
  *
8
- * This file is seeded to .agent-hippo/analytics/panel/panel.js on first use.
9
- * Users can edit the seeded copy to customize the dashboard UI.
8
+ * This bundled file is the ONLY panel.js ever executed (see provider.getHtmlContent).
9
+ * It is not seeded to .agent-hippo/analytics/panel/ — a copy there would never run.
10
+ * Dashboard UI is customized via panel.html / panel.css, which ARE read from that directory.
10
11
  * ----------------------------------------------------------------------------- */
11
12
 
12
13
  // @ts-check
Binary file
@@ -800,21 +800,22 @@ class AnalyticsService {
800
800
  '-ConfigPath',
801
801
  options.configPath,
802
802
  ]);
803
+ // A missing config is not fatal: the launcher re-seeds the managed default
804
+ // (~/.agent-hippo/litellm-otel.yaml) from its bundled template before starting.
803
805
  return [
804
806
  'Write-Host "Starting LiteLLM proxy..."',
805
807
  `if (-not (Test-Path ${q(options.configPath)})) {`,
806
- ` Write-Host ${q(`[WARN] LiteLLM config not found: ${options.configPath}`)} -ForegroundColor Yellow`,
807
- '} else {',
808
- ` $env:LITELLM_CONFIG_PATH = ${q(options.configPath)}`,
809
- ' try {',
810
- ` $proc = Start-Process -FilePath ${q(getWindowsPwshOrPowerShell())} -WindowStyle Hidden -WorkingDirectory ${q(path.dirname(options.litellmScript))} -ArgumentList ${q(launcherArgumentLine)} -RedirectStandardOutput ${q(options.stdoutPath)} -RedirectStandardError ${q(options.stderrPath)} -PassThru -ErrorAction Stop`,
811
- ' Write-Host "[OK] LiteLLM launcher started (PID: $($proc.Id))"',
812
- ` Write-Host ${q(` Logs: ${options.stdoutPath}`)}`,
813
- ` Write-Host ${q(` Errors: ${options.stderrPath}`)}`,
814
- ' } catch {',
815
- ' Write-Host "[ERROR] Failed to start LiteLLM launcher." -ForegroundColor Red',
816
- ' Write-Host $_.Exception.Message -ForegroundColor Red',
817
- ' }',
808
+ ` Write-Host ${q(`[INFO] LiteLLM config missing: ${options.configPath} — launcher will re-create it from the bundled template.`)}`,
809
+ '}',
810
+ `$env:LITELLM_CONFIG_PATH = ${q(options.configPath)}`,
811
+ 'try {',
812
+ ` $proc = Start-Process -FilePath ${q(getWindowsPwshOrPowerShell())} -WindowStyle Hidden -WorkingDirectory ${q(path.dirname(options.litellmScript))} -ArgumentList ${q(launcherArgumentLine)} -RedirectStandardOutput ${q(options.stdoutPath)} -RedirectStandardError ${q(options.stderrPath)} -PassThru -ErrorAction Stop`,
813
+ ' Write-Host "[OK] LiteLLM launcher started (PID: $($proc.Id))"',
814
+ ` Write-Host ${q(` Logs: ${options.stdoutPath}`)}`,
815
+ ` Write-Host ${q(` Errors: ${options.stderrPath}`)}`,
816
+ '} catch {',
817
+ ' Write-Host "[ERROR] Failed to start LiteLLM launcher." -ForegroundColor Red',
818
+ ' Write-Host $_.Exception.Message -ForegroundColor Red',
818
819
  '}',
819
820
  ];
820
821
  }
@@ -854,17 +855,18 @@ class AnalyticsService {
854
855
  if (!options.litellmScript) {
855
856
  return ['echo "[WARN] LiteLLM launcher script not found. Skipping LiteLLM start." >&2'];
856
857
  }
858
+ // A missing config is not fatal: the launcher re-seeds the managed default
859
+ // (~/.agent-hippo/litellm-otel.yaml) from its bundled template before starting.
857
860
  return [
858
861
  'echo "Starting LiteLLM proxy..."',
859
862
  `if [ ! -f ${q(options.configPath)} ]; then`,
860
- ` echo ${q(`[WARN] LiteLLM config not found: ${options.configPath}`)} >&2`,
861
- 'else',
862
- ` export LITELLM_CONFIG_PATH=${q(options.configPath)}`,
863
- ` nohup ${q(options.litellmScript)} --config ${q(options.configPath)} >> ${q(options.stdoutPath)} 2>> ${q(options.stderrPath)} &`,
864
- ' echo "[OK] LiteLLM launcher started (PID: $!)"',
865
- ` echo ${q(` Logs: ${options.stdoutPath}`)}`,
866
- ` echo ${q(` Errors: ${options.stderrPath}`)}`,
863
+ ` echo ${q(`[INFO] LiteLLM config missing: ${options.configPath} — launcher will re-create it from the bundled template.`)}`,
867
864
  'fi',
865
+ `export LITELLM_CONFIG_PATH=${q(options.configPath)}`,
866
+ `nohup ${q(options.litellmScript)} --config ${q(options.configPath)} >> ${q(options.stdoutPath)} 2>> ${q(options.stderrPath)} &`,
867
+ 'echo "[OK] LiteLLM launcher started (PID: $!)"',
868
+ `echo ${q(` Logs: ${options.stdoutPath}`)}`,
869
+ `echo ${q(` Errors: ${options.stderrPath}`)}`,
868
870
  ];
869
871
  }
870
872
  getLifecycleTerminalIntro(action) {
@@ -893,13 +895,9 @@ class AnalyticsService {
893
895
  * Start LiteLLM in a detached background process. Logs go to .agent-hippo/analytics/litellm.{log,error.log}.
894
896
  */
895
897
  async startLiteLLMDirectly() {
898
+ // No existence check: a missing managed config is re-seeded from the bundled
899
+ // template by the launcher itself (deleting it is a "reset to defaults" gesture).
896
900
  const configPath = path.join((0, index_1.getAgentideHome)(), 'litellm-otel.yaml');
897
- try {
898
- await fs_1.promises.access(configPath);
899
- }
900
- catch {
901
- return false;
902
- }
903
901
  const isWindows = process.platform === 'win32';
904
902
  const args = isWindows
905
903
  ? ['-ConfigPath', configPath]
@@ -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,24 +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
61
  const os = __importStar(require("os"));
55
- // -----------------------------------------------------------------------------
56
- // Session Event Writer
57
- // -----------------------------------------------------------------------------
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
+ };
58
68
  /**
59
- * Append-only JSONL writer for session events.
60
- * One file per session: ~/.agent-hippo/analytics/sessions/<session_id>.jsonl
69
+ * Process-wide evidence-write health, for /health-style output.
61
70
  *
62
- * Design: write-through (no buffering). Each event appends immediately.
63
- * 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.
64
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
+ }
65
92
  class SessionEventWriter {
66
93
  baseDir;
67
94
  ensuredDirs = new Set();
95
+ written = 0;
96
+ failed = 0;
97
+ lastError;
68
98
  sanitizeSessionId(sessionId) {
69
99
  const trimmed = sessionId.trim();
70
100
  if (!trimmed) {
@@ -78,6 +108,14 @@ class SessionEventWriter {
78
108
  }
79
109
  /**
80
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.
81
119
  */
82
120
  async append(sessionId, requestId, eventType, payload) {
83
121
  try {
@@ -97,11 +135,27 @@ class SessionEventWriter {
97
135
  const filePath = path.join(this.baseDir, `${safeSessionId}.jsonl`);
98
136
  const line = JSON.stringify(event) + '\n';
99
137
  await fs_1.promises.appendFile(filePath, line);
138
+ this.written++;
139
+ globalStats.written++;
100
140
  }
101
- catch {
102
- // 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
+ }
103
153
  }
104
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
+ }
105
159
  /**
106
160
  * Get the sessions directory path (for DuckDB queries).
107
161
  */
@@ -126,9 +180,15 @@ class ObservableStreamEmitter {
126
180
  writer;
127
181
  sessionId;
128
182
  requestId;
129
- userId;
183
+ stamp;
130
184
  missingCallIdCounter = 0;
131
185
  pendingSyntheticCallIds = new Map();
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;
132
192
  constructor(inner, writer, sessionId, requestId,
133
193
  /**
134
194
  * Acting user for the warrant audit trail on tool events. Captured once at
@@ -138,13 +198,25 @@ class ObservableStreamEmitter {
138
198
  * parent IPC callback whose ALS can stick to the spawn request; constructor
139
199
  * capture keeps attribution correct for every engine. Undefined outside a
140
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).
141
210
  */
142
- userId) {
211
+ stamp) {
143
212
  this.inner = inner;
144
213
  this.writer = writer;
145
214
  this.sessionId = sessionId;
146
215
  this.requestId = requestId;
147
- this.userId = userId;
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);
148
220
  }
149
221
  makeCallIdKey(toolName) {
150
222
  return `${this.sessionId}:${this.requestId}:${toolName}`;
@@ -185,10 +257,12 @@ class ObservableStreamEmitter {
185
257
  await this.writer.append(this.sessionId, this.requestId, 'tool_start', {
186
258
  toolName,
187
259
  callId: loggedCallId,
188
- // Truncate input to avoid huge JSONL files
189
- inputPreview: input?.slice(0, 500),
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),
190
264
  // The warrant: who this tool call ran as (captured at wrap; see constructor).
191
- userId: this.userId,
265
+ userId: this.actorId,
192
266
  });
193
267
  return this.inner.toolStart(toolName, input, toolCallId, metadata);
194
268
  }
@@ -200,9 +274,10 @@ class ObservableStreamEmitter {
200
274
  toolName,
201
275
  callId: loggedCallId,
202
276
  isError: isError ?? false,
203
- // Truncate result to avoid huge JSONL files
204
- resultPreview: result?.slice(0, 500),
205
- userId: this.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,
206
281
  });
207
282
  return this.inner.toolEnd(toolName, toolCallId, result, isError, metadata);
208
283
  }
@@ -215,7 +290,7 @@ class ObservableStreamEmitter {
215
290
  async error(message) {
216
291
  await this.writer.append(this.sessionId, this.requestId, 'error', {
217
292
  message,
218
- userId: this.userId,
293
+ userId: this.actorId,
219
294
  });
220
295
  return this.inner.error(message);
221
296
  }
@@ -226,7 +301,7 @@ class ObservableStreamEmitter {
226
301
  * Emit a turn_start event. Call at the beginning of stream().
227
302
  */
228
303
  async emitTurnStart() {
229
- await this.writer.append(this.sessionId, this.requestId, 'turn_start', { userId: this.userId });
304
+ await this.writer.append(this.sessionId, this.requestId, 'turn_start', { userId: this.actorId, ...this.stamp });
230
305
  }
231
306
  /**
232
307
  * Emit a turn_end event. Call at the end of stream().
@@ -234,7 +309,7 @@ class ObservableStreamEmitter {
234
309
  async emitTurnEnd(success) {
235
310
  await this.writer.append(this.sessionId, this.requestId, 'turn_end', {
236
311
  success,
237
- userId: this.userId,
312
+ userId: this.actorId,
238
313
  });
239
314
  }
240
315
  }
@@ -243,6 +318,6 @@ exports.ObservableStreamEmitter = ObservableStreamEmitter;
243
318
  * Create an ObservableStreamEmitter that wraps the given emitter.
244
319
  * Returns both the wrapped emitter and emit helpers for turn boundaries.
245
320
  */
246
- function wrapEmitterWithObservability(inner, writer, sessionId, requestId, userId) {
247
- return new ObservableStreamEmitter(inner, writer, sessionId, requestId, userId);
321
+ function wrapEmitterWithObservability(inner, writer, sessionId, requestId, userId, stamp) {
322
+ return new ObservableStreamEmitter(inner, writer, sessionId, requestId, userId, stamp);
248
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}` },
@@ -41,7 +41,7 @@ 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");
@@ -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;