@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.
- package/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +3 -2
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +23 -25
- package/out/analytics/sessionEvents.js +98 -23
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +48 -14
- package/out/auth/provider.js +8 -21
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +50 -5
- package/out/auth-broker/server.js +122 -26
- package/out/auth-broker/store.js +25 -24
- package/out/authority/warrant.js +206 -0
- package/out/branding.js +27 -1
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +67 -0
- package/out/cli/commands/deploy.js +1 -0
- package/out/cli/commands/gateway.js +4 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +332 -0
- package/out/cli/commands/serve.js +875 -52
- package/out/cli/commands/store.js +12 -4
- package/out/cli/data-plane/gateway.js +28 -5
- package/out/cli/data-plane/index.js +17 -2
- package/out/cli/data-plane/registry.js +9 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +161 -0
- package/out/cli/deploy/layers/identity.js +85 -4
- package/out/cli/deploy-init.js +89 -24
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +35 -0
- package/out/cli/index.js +155 -16
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +8 -1
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +35 -1
- package/out/cli/serve-hardening.js +80 -3
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +33 -15
- package/out/engines/codex.js +110 -26
- package/out/engines/custom.js +8 -0
- package/out/evidence/sessionShip.js +610 -0
- package/out/extension.js +11 -2
- package/out/gateway/bridge.js +40 -2
- package/out/gateway/client.js +34 -1
- package/out/gateway/installSource.js +68 -0
- package/out/gateway/lifecycle.js +185 -51
- package/out/gateway/outboundPaths.js +4 -1
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +17 -1
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/mcp.js +30 -0
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +309 -0
- package/out/pack/lease-schema.js +270 -0
- package/out/pack/lease.js +640 -0
- package/out/pack/reconcile.js +217 -0
- package/out/pack/signature.js +839 -0
- package/out/remote/handleRemoteChat.js +44 -3
- package/out/remote/remoteTrust.js +277 -0
- package/out/remote/serveClient.js +52 -5
- package/out/runner/host.js +131 -0
- package/out/runner/protocol.js +108 -0
- package/out/runner/remoteEngine.js +130 -0
- package/out/runtime/interfaces/edit-tracker.js +2 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
- package/out/sandbox/containerDriver.js +570 -0
- package/out/sandbox/processDriver.js +315 -0
- package/out/sandbox/sandboxRunner.js +191 -0
- package/out/sandbox/support.js +301 -0
- package/out/sandbox/types.js +55 -0
- package/out/scripts/agentide-litellm.sh +67 -5
- package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -132
- package/out/scripts/analytics/setup-analytics.ps1 +37 -13
- package/out/scripts/analytics/setup-analytics.sh +93 -13
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +11 -110
- package/out/spotlight/install.js +7 -1
- package/out/store/index.js +41 -10
- package/out/sync.js +3 -1
- package/out/test/analytics.test.js +162 -23
- package/out/test/auth-baseurl.test.js +132 -0
- package/out/test/auth-bootstrapApply.test.js +27 -6
- package/out/test/auth-broker-hardening.test.js +227 -0
- package/out/test/auth-broker.test.js +59 -9
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +455 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/data-plane.test.js +67 -0
- package/out/test/deploy-init.test.js +40 -0
- package/out/test/gateway-client.test.js +28 -0
- package/out/test/gateway-install-source.test.js +111 -0
- package/out/test/gateway-lifecycle.test.js +1 -1
- package/out/test/gateway-outbound.test.js +7 -0
- package/out/test/gateway-secrets-preflight.test.js +127 -0
- package/out/test/gateway-session-routing.test.js +18 -0
- package/out/test/governor-hardening.test.js +217 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/lease-signature.test.js +357 -0
- package/out/test/lease-trust-root.test.js +273 -0
- package/out/test/lease-validation.test.js +169 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/mcp.test.js +27 -1
- package/out/test/pack-admission.test.js +216 -0
- package/out/test/pack-digest-parity.test.js +140 -0
- package/out/test/pack-reconcile.test.js +183 -0
- package/out/test/pack-signature-artifact.test.js +103 -0
- package/out/test/pack-signature.test.js +176 -0
- package/out/test/pack-trust-root.test.js +202 -0
- package/out/test/remote-trust.test.js +242 -0
- package/out/test/runner-protocol.test.js +249 -0
- package/out/test/sandbox-driver.test.js +553 -0
- package/out/test/sandbox-hardening.test.js +395 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/serve-hardening.test.js +45 -6
- package/out/test/serveClient.test.js +6 -5
- package/out/test/sessionShip.test.js +560 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/store-install-safe.test.js +69 -0
- package/out/test/tls-bypass.test.js +90 -0
- package/out/test/warrant.test.js +138 -0
- package/out/utils.js +46 -9
- 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
|
|
9
|
-
*
|
|
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
|
package/out/analytics/service.js
CHANGED
|
@@ -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(`[
|
|
807
|
-
'}
|
|
808
|
-
|
|
809
|
-
'
|
|
810
|
-
`
|
|
811
|
-
'
|
|
812
|
-
`
|
|
813
|
-
`
|
|
814
|
-
'
|
|
815
|
-
'
|
|
816
|
-
'
|
|
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(`[
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
+
stamp) {
|
|
143
212
|
this.inner = inner;
|
|
144
213
|
this.writer = writer;
|
|
145
214
|
this.sessionId = sessionId;
|
|
146
215
|
this.requestId = requestId;
|
|
147
|
-
this.
|
|
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
|
-
//
|
|
189
|
-
|
|
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.
|
|
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
|
-
//
|
|
204
|
-
resultPreview
|
|
205
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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: '' });
|
package/out/auth/brokerClient.js
CHANGED
|
@@ -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 ||
|
|
20
|
-
return value
|
|
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}` },
|
package/out/auth/provider.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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;
|