@tonyclaw/agent-inspector 3.0.29 → 3.0.31
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/.output/cli.js +791 -191
- package/.output/nitro.json +1 -1
- package/.output/public/assets/CompareDrawer-DB2KggLE.js +1 -0
- package/.output/public/assets/InspectorPet-CfwDw6kD.js +4108 -0
- package/.output/public/assets/ProxyViewerContainer-PcrO96_F.js +126 -0
- package/.output/public/assets/{ReplayDialog-CctnITbt.js → ReplayDialog-3Pl7EKjp.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-BrN0RBbL.js → RequestAnatomy-BcPjcHLX.js} +1 -1
- package/.output/public/assets/ResponseView-DOx51wV3.js +2 -0
- package/.output/public/assets/StreamingChunkSequence-LkYJWM1A.js +1 -0
- package/.output/public/assets/_sessionId-BSg32vC6.js +1 -0
- package/.output/public/assets/{_sessionId-DKM91udC.js → _sessionId-C0CA68QV.js} +1 -1
- package/.output/public/assets/{index-Bq-nqolG.js → index-BpBUqdAB.js} +1 -1
- package/.output/public/assets/index-D93dCgAQ.js +1 -0
- package/.output/public/assets/{index-DPiESBo2.js → index-FV5sxqx3.js} +6 -6
- package/.output/public/assets/index-VUsF4nhf.js +76 -0
- package/.output/public/assets/index-hyQHZeQI.css +1 -0
- package/.output/public/assets/{json-viewer-CfaDJwx8.js → json-viewer-WctIbAh_.js} +1 -1
- package/.output/public/assets/jszip.min-DnSIoTix.js +2 -0
- package/.output/public/assets/qwen-mMn3f5ul.webp +0 -0
- package/.output/server/_libs/jszip.mjs +35 -9
- package/.output/server/_libs/lucide-react.mjs +80 -80
- package/.output/server/_libs/radix-ui__react-dialog.mjs +3 -3
- package/.output/server/_libs/tanstack__react-router.mjs +1 -1
- package/.output/server/{_sessionId-CwGXPD4C.mjs → _sessionId-BHBK3VbF.mjs} +15 -15
- package/.output/server/_sessionId-CyuJ6Kfl.mjs +81 -0
- package/.output/server/_ssr/{CompareDrawer-R_P1-VNr.mjs → CompareDrawer-27amlv_a.mjs} +138 -103
- package/.output/server/_ssr/InspectorPet-CQnGXfLs.mjs +925 -0
- package/.output/server/_ssr/{ProxyViewerContainer-DUgxRNbj.mjs → ProxyViewerContainer-DlAl0uF-.mjs} +1569 -1646
- package/.output/server/_ssr/{ReplayDialog-De9wKKzS.mjs → ReplayDialog-5m2jtlLa.mjs} +18 -18
- package/.output/server/_ssr/{RequestAnatomy-Db4MxUYo.mjs → RequestAnatomy--1WtQNY9.mjs} +58 -58
- package/.output/server/_ssr/{ResponseView-8rDXOiNB.mjs → ResponseView-CgbqS8sr.mjs} +60 -71
- package/.output/server/_ssr/{StreamingChunkSequence-N9teZHkA.mjs → StreamingChunkSequence-BwpnopWx.mjs} +16 -16
- package/.output/server/_ssr/{index-D4DvByuW.mjs → index-CNVi1TzT.mjs} +15 -15
- package/.output/server/_ssr/index-DMCjH_4V.mjs +81 -0
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-BjoHRbjS.mjs → json-viewer-DNGN2uoe.mjs} +58 -58
- package/.output/server/_ssr/{router-BYlePDj8.mjs → router-CgsqLnk5.mjs} +16007 -12632
- package/.output/server/_tanstack-start-manifest_v-BH5f_kWV.mjs +4 -0
- package/.output/server/index.mjs +99 -71
- package/.output/workers/logFinalizer.worker.js +16908 -0
- package/.output/workers/sessionWorkerEntry.js +16904 -0
- package/README.md +21 -5
- package/package.json +6 -2
- package/src/assets/logos/qwen.webp +0 -0
- package/src/cli.ts +85 -10
- package/src/components/OnboardingBanner.tsx +41 -21
- package/src/components/ProxyViewer.tsx +198 -269
- package/src/components/ProxyViewerContainer.tsx +128 -17
- package/src/components/ecosystem/AgentLabDialog.tsx +166 -19
- package/src/components/errors/SafeErrorBoundary.tsx +201 -0
- package/src/components/pi-agent/PiAgentPanel.tsx +209 -35
- package/src/components/pi-agent/piAgentChatLogic.ts +263 -0
- package/src/components/providers/ImportWizardDialog.tsx +18 -42
- package/src/components/providers/ProviderLogo.tsx +1 -1
- package/src/components/providers/ProvidersPanel.tsx +8 -5
- package/src/components/providers/SettingsDialog.tsx +34 -11
- package/src/components/proxy-viewer/ApplicationBar.tsx +63 -0
- package/src/components/proxy-viewer/CompareDrawer.tsx +92 -49
- package/src/components/proxy-viewer/LiveConnectionStatus.tsx +186 -0
- package/src/components/proxy-viewer/LogEntry.tsx +283 -275
- package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
- package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
- package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
- package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
- package/src/components/proxy-viewer/accessibility.ts +8 -0
- package/src/components/proxy-viewer/lazy.ts +4 -0
- package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
- package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
- package/src/components/proxy-viewer/viewerState.ts +2 -6
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/tabs.tsx +3 -3
- package/src/components/ui/transient-toast.tsx +7 -6
- package/src/lib/apiClient.ts +17 -2
- package/src/lib/ecosystemContract.ts +34 -0
- package/src/lib/export-logs.ts +1 -1
- package/src/lib/piAgentContract.ts +80 -0
- package/src/lib/providerContract.ts +70 -4
- package/src/lib/providerImportContract.ts +27 -0
- package/src/lib/providerModelMetadata.ts +16 -7
- package/src/lib/resourceLimits.ts +152 -0
- package/src/lib/safeDiagnostic.ts +38 -0
- package/src/lib/useProviders.ts +4 -4
- package/src/mcp/server.ts +39 -6
- package/src/mcp/toolHandlers.ts +131 -4
- package/src/proxy/chunkStorage.ts +20 -6
- package/src/proxy/config.ts +20 -6
- package/src/proxy/dataDir.ts +3 -0
- package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
- package/src/proxy/ecosystemExecutionRoute.ts +116 -0
- package/src/proxy/ecosystemTasks.ts +48 -0
- package/src/proxy/evidenceExporter.ts +23 -9
- package/src/proxy/groupEvidenceExporter.ts +26 -5
- package/src/proxy/groupStore.ts +13 -3
- package/src/proxy/handler.ts +103 -20
- package/src/proxy/identityProxy.ts +333 -14
- package/src/proxy/logFinalizer.ts +73 -6
- package/src/proxy/logImportUpload.ts +128 -0
- package/src/proxy/logImporter.ts +321 -70
- package/src/proxy/logIndex.ts +16 -6
- package/src/proxy/logger.ts +294 -41
- package/src/proxy/privateDataPath.ts +183 -0
- package/src/proxy/providerScanStore.ts +87 -0
- package/src/proxy/providerSecretStore.ts +58 -19
- package/src/proxy/providers.ts +175 -58
- package/src/proxy/rawStreamCapture.ts +66 -5
- package/src/proxy/runStore.ts +13 -3
- package/src/proxy/runtimeAdmission.ts +52 -0
- package/src/proxy/runtimeHealth.ts +206 -0
- package/src/proxy/runtimeShutdown.ts +75 -0
- package/src/proxy/sessionArchive.ts +15 -2
- package/src/proxy/sessionProcess.ts +19 -0
- package/src/proxy/sessionRuntime.ts +7 -0
- package/src/proxy/shutdownCoordinator.ts +90 -0
- package/src/proxy/sqliteLogIndex.ts +18 -2
- package/src/proxy/store.ts +18 -2
- package/src/routes/__root.tsx +18 -1
- package/src/routes/api/alerts.summary.ts +12 -8
- package/src/routes/api/alerts.ts +27 -9
- package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
- package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
- package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
- package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
- package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
- package/src/routes/api/health.ts +9 -2
- package/src/routes/api/knowledge.project-context.ts +11 -0
- package/src/routes/api/knowledge.search.ts +33 -2
- package/src/routes/api/logs.$id.body.ts +16 -2
- package/src/routes/api/logs.import.ts +7 -18
- package/src/routes/api/logs.stream.ts +147 -69
- package/src/routes/api/logs.ts +55 -14
- package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
- package/src/routes/api/providers.$providerId.ts +37 -6
- package/src/routes/api/providers.export.ts +34 -16
- package/src/routes/api/providers.import.ts +42 -8
- package/src/routes/api/providers.scan.ts +13 -8
- package/src/routes/api/providers.ts +36 -5
- package/src/routes/api/runs.ts +12 -12
- package/src/routes/api/sessions.ts +15 -8
- package/src/routes/index.tsx +6 -0
- package/src/routes/livez.ts +13 -0
- package/src/routes/readyz.ts +18 -0
- package/src/routes/session/$sessionId.tsx +6 -0
- package/src/services/piAgent.ts +131 -102
- package/src/services/piAgentRetrieval.ts +1180 -0
- package/styles/globals.css +19 -3
- package/.output/public/assets/CompareDrawer-Cb5ccPeX.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-COBe5GTT.js +0 -4234
- package/.output/public/assets/ResponseView-Cqq87lX_.js +0 -3
- package/.output/public/assets/StreamingChunkSequence-CnTa9mCr.js +0 -1
- package/.output/public/assets/index-DTLNVcgc.js +0 -76
- package/.output/public/assets/index-DliqmwUw.css +0 -1
- package/.output/public/assets/qwen-CONDcHqt.png +0 -0
- package/.output/server/_tanstack-start-manifest_v-DMCA8KK3.mjs +0 -4
- package/src/assets/logos/mcp.png +0 -0
- package/src/assets/logos/qwen.png +0 -0
- package/src/components/ui/mcp-logo.tsx +0 -20
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
renameSync,
|
|
9
|
-
rmSync,
|
|
10
|
-
writeFileSync,
|
|
11
|
-
} from "node:fs";
|
|
12
|
-
import { join } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
13
5
|
import { z } from "zod";
|
|
14
6
|
import { readJsonFile } from "../lib/jsonFile";
|
|
15
7
|
import { logger } from "./logger";
|
|
8
|
+
import {
|
|
9
|
+
ensurePrivateDirectorySync,
|
|
10
|
+
PRIVATE_FILE_MODE,
|
|
11
|
+
securePrivateFileSync,
|
|
12
|
+
} from "./privateDataPath";
|
|
16
13
|
|
|
17
14
|
const SECRET_REFERENCE_PREFIX = "secret:provider:";
|
|
18
15
|
const SECRET_FILE_VERSION = 1;
|
|
@@ -38,21 +35,32 @@ function emptySecretFile(): SecretFile {
|
|
|
38
35
|
return { version: SECRET_FILE_VERSION, secrets: {} };
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
function readSecretFile(path: string): SecretFile {
|
|
38
|
+
function readSecretFile(path: string): SecretFile | null {
|
|
42
39
|
if (!existsSync(path)) return emptySecretFile();
|
|
40
|
+
if (!securePrivateFileSync(path)) {
|
|
41
|
+
logger.error(`[provider-secrets] Refusing secret file with unsafe permissions at ${path}`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
43
44
|
const result = readJsonFile(path, SecretFileSchema);
|
|
44
45
|
if (result.ok) return result.data;
|
|
45
|
-
logger.
|
|
46
|
-
return
|
|
46
|
+
logger.error(`[provider-secrets] Refusing invalid secret file ${path} (${result.reason})`);
|
|
47
|
+
return null;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function writePrivateFile(path: string, content: string): boolean {
|
|
50
51
|
const temporaryPath = `${path}.${String(process.pid)}.tmp`;
|
|
51
52
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
if (!ensurePrivateDirectorySync(dirname(path))) return false;
|
|
54
|
+
writeFileSync(temporaryPath, content, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
55
|
+
if (!securePrivateFileSync(temporaryPath, true)) {
|
|
56
|
+
rmSync(temporaryPath, { force: true });
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
54
59
|
renameSync(temporaryPath, path);
|
|
55
|
-
|
|
60
|
+
if (!securePrivateFileSync(path)) {
|
|
61
|
+
rmSync(path, { force: true });
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
56
64
|
return true;
|
|
57
65
|
} catch (error) {
|
|
58
66
|
rmSync(temporaryPath, { force: true });
|
|
@@ -64,6 +72,10 @@ function writePrivateFile(path: string, content: string): boolean {
|
|
|
64
72
|
function readKey(path: string): Buffer | null {
|
|
65
73
|
try {
|
|
66
74
|
if (!existsSync(path)) return null;
|
|
75
|
+
if (!securePrivateFileSync(path, true)) {
|
|
76
|
+
logger.error(`[provider-secrets] Refusing encryption key with unsafe permissions at ${path}`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
67
79
|
const key = Buffer.from(readFileSync(path, "utf8").trim(), "base64");
|
|
68
80
|
if (key.byteLength === KEY_BYTES) return key;
|
|
69
81
|
logger.error(`[provider-secrets] Invalid encryption key at ${path}`);
|
|
@@ -126,9 +138,25 @@ function providerIdFromReference(reference: string): string | null {
|
|
|
126
138
|
}
|
|
127
139
|
|
|
128
140
|
export function createProviderSecretStore(directory: string): ProviderSecretStore {
|
|
129
|
-
mkdirSync(directory, { recursive: true });
|
|
130
141
|
const keyPath = join(directory, "provider-secrets.key");
|
|
131
142
|
const secretsPath = join(directory, "provider-secrets.json");
|
|
143
|
+
let existingFilesSecured = false;
|
|
144
|
+
|
|
145
|
+
function storageDirectoryIsPrivate(): boolean {
|
|
146
|
+
if (!ensurePrivateDirectorySync(directory)) {
|
|
147
|
+
logger.error(`[provider-secrets] Could not secure provider secret directory ${directory}`);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (existingFilesSecured) return true;
|
|
151
|
+
for (const path of [keyPath, secretsPath]) {
|
|
152
|
+
if (existsSync(path) && !securePrivateFileSync(path, true)) {
|
|
153
|
+
logger.error(`[provider-secrets] Could not secure existing provider secret file ${path}`);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
existingFilesSecured = true;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
132
160
|
|
|
133
161
|
function writeSecrets(data: SecretFile): boolean {
|
|
134
162
|
return writePrivateFile(secretsPath, JSON.stringify(data, null, 2));
|
|
@@ -137,29 +165,40 @@ export function createProviderSecretStore(directory: string): ProviderSecretStor
|
|
|
137
165
|
return {
|
|
138
166
|
isReference: (value) => providerIdFromReference(value) !== null,
|
|
139
167
|
write: (providerId, secret) => {
|
|
168
|
+
if (!storageDirectoryIsPrivate()) return null;
|
|
140
169
|
const key = readOrCreateKey(keyPath, secretsPath);
|
|
141
170
|
if (key === null) return null;
|
|
142
171
|
const encrypted = encrypt(secret, key);
|
|
143
172
|
if (encrypted === null) return null;
|
|
144
173
|
const data = readSecretFile(secretsPath);
|
|
174
|
+
if (data === null) return null;
|
|
145
175
|
const next = {
|
|
146
176
|
...data,
|
|
147
177
|
secrets: { ...data.secrets, [providerId]: encrypted },
|
|
148
178
|
};
|
|
149
179
|
if (!writeSecrets(next)) return null;
|
|
150
|
-
|
|
180
|
+
const reference = `${SECRET_REFERENCE_PREFIX}${providerId}`;
|
|
181
|
+
const persistedSecret = readSecretFile(secretsPath)?.secrets[providerId];
|
|
182
|
+
if (persistedSecret === undefined || decrypt(persistedSecret, key) !== secret) {
|
|
183
|
+
logger.error("[provider-secrets] Could not verify persisted provider secret");
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return reference;
|
|
151
187
|
},
|
|
152
188
|
read: (reference) => {
|
|
189
|
+
if (!storageDirectoryIsPrivate()) return null;
|
|
153
190
|
const providerId = providerIdFromReference(reference);
|
|
154
191
|
if (providerId === null) return null;
|
|
155
192
|
const key = readKey(keyPath);
|
|
156
193
|
if (key === null) return null;
|
|
157
|
-
const encrypted = readSecretFile(secretsPath)
|
|
194
|
+
const encrypted = readSecretFile(secretsPath)?.secrets[providerId];
|
|
158
195
|
if (encrypted === undefined) return null;
|
|
159
196
|
return decrypt(encrypted, key);
|
|
160
197
|
},
|
|
161
198
|
delete: (providerId) => {
|
|
199
|
+
if (!storageDirectoryIsPrivate()) return;
|
|
162
200
|
const data = readSecretFile(secretsPath);
|
|
201
|
+
if (data === null) return;
|
|
163
202
|
if (data.secrets[providerId] === undefined) return;
|
|
164
203
|
const nextSecrets = { ...data.secrets };
|
|
165
204
|
delete nextSecrets[providerId];
|
package/src/proxy/providers.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import Conf from "conf";
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
|
-
import { existsSync,
|
|
4
|
+
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { maskApiKey } from "../lib/mask";
|
|
7
7
|
import { logger } from "./logger";
|
|
8
8
|
import { getDataDir, hasExplicitDataDir } from "./dataDir";
|
|
9
9
|
import {
|
|
10
10
|
ProviderConfigSchema,
|
|
11
|
-
|
|
11
|
+
ProviderModelMetadataListSchema,
|
|
12
|
+
ProviderModelNameSchema,
|
|
13
|
+
ProviderModelsSchema,
|
|
14
|
+
toRedactedProviderDto,
|
|
15
|
+
toRedactedProviderDtos,
|
|
12
16
|
type ProviderConfig,
|
|
13
17
|
type ProviderModelMetadata,
|
|
18
|
+
type RedactedProviderDto,
|
|
14
19
|
} from "../lib/providerContract";
|
|
15
20
|
import { withBuiltinProviderModelMetadata } from "../lib/providerModelMetadata";
|
|
16
21
|
import {
|
|
@@ -18,6 +23,11 @@ import {
|
|
|
18
23
|
secretValuesEqual,
|
|
19
24
|
type ProviderSecretStore,
|
|
20
25
|
} from "./providerSecretStore";
|
|
26
|
+
import {
|
|
27
|
+
ensurePrivateDirectorySync,
|
|
28
|
+
PRIVATE_FILE_MODE,
|
|
29
|
+
securePrivateFileSync,
|
|
30
|
+
} from "./privateDataPath";
|
|
21
31
|
|
|
22
32
|
export { ProviderConfigSchema };
|
|
23
33
|
export type { ProviderConfig };
|
|
@@ -55,8 +65,8 @@ const LegacyProviderConfigSchema = z.object({
|
|
|
55
65
|
type: z.enum(["anthropic", "openai"]).optional(),
|
|
56
66
|
format: z.enum(["anthropic", "openai"]).optional(),
|
|
57
67
|
apiKey: z.string().optional(),
|
|
58
|
-
model:
|
|
59
|
-
models:
|
|
68
|
+
model: ProviderModelNameSchema.optional(),
|
|
69
|
+
models: ProviderModelsSchema.optional(),
|
|
60
70
|
baseUrl: z.string().optional(),
|
|
61
71
|
anthropicBaseUrl: z.string().optional(),
|
|
62
72
|
openaiBaseUrl: z.string().optional(),
|
|
@@ -64,7 +74,7 @@ const LegacyProviderConfigSchema = z.object({
|
|
|
64
74
|
authHeader: z.enum(["bearer", "x-api-key"]).optional(),
|
|
65
75
|
apiDocsUrl: z.string().optional(),
|
|
66
76
|
modelMetadataUrl: z.string().optional(),
|
|
67
|
-
modelMetadata:
|
|
77
|
+
modelMetadata: ProviderModelMetadataListSchema.optional(),
|
|
68
78
|
source: z.enum(["company", "personal"]).optional(),
|
|
69
79
|
createdAt: z.string().optional(),
|
|
70
80
|
updatedAt: z.string().optional(),
|
|
@@ -152,7 +162,10 @@ function migrateFromDataDirConfig(targetStore: Conf<ProvidersStore>, dataDirPath
|
|
|
152
162
|
}
|
|
153
163
|
if (migrated.length === 0) return;
|
|
154
164
|
|
|
155
|
-
|
|
165
|
+
if (!persistProviders(migrated)) {
|
|
166
|
+
logger.warn(`[providers] Data-dir config migration left ${targetStore.path} unchanged`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
156
169
|
logger.info(
|
|
157
170
|
`[providers] Migrated ${migrated.length} provider(s) from ${legacyConfigPath} to ${targetStore.path}`,
|
|
158
171
|
);
|
|
@@ -189,10 +202,13 @@ function migrateFromLegacyConfLocation(targetStore: Conf<ProvidersStore>): void
|
|
|
189
202
|
const legacyProviders = legacyStore.get("providers", []);
|
|
190
203
|
if (legacyProviders.length === 0) return;
|
|
191
204
|
const now = new Date().toISOString();
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
legacyProviders.map((provider) => withBuiltinProviderModelMetadata(provider, now)),
|
|
205
|
+
const migrated = legacyProviders.map((provider) =>
|
|
206
|
+
withBuiltinProviderModelMetadata(provider, now),
|
|
195
207
|
);
|
|
208
|
+
if (!persistProviders(migrated)) {
|
|
209
|
+
logger.warn(`[providers] Legacy migration left ${targetStore.path} unchanged`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
196
212
|
logger.info(
|
|
197
213
|
`[providers] Migrated ${legacyProviders.length} provider(s) from ${legacyStore.path} to ${targetStore.path}`,
|
|
198
214
|
);
|
|
@@ -210,11 +226,11 @@ const configPath = process.env["AGENT_INSPECTOR_CONFIG_PATH"];
|
|
|
210
226
|
// We pass `cwd` so conf writes the file as `<cwd>/<configName>.<ext>`,
|
|
211
227
|
// i.e. `<dataDir>/providers.json`. The runtime config lives at
|
|
212
228
|
// `<dataDir>/config.json`; the different filenames keep the two stores
|
|
213
|
-
// from overwriting each other.
|
|
214
|
-
// a no-op when the directory already exists.
|
|
229
|
+
// from overwriting each other.
|
|
215
230
|
const dataDir = getDataDir();
|
|
216
|
-
|
|
217
|
-
|
|
231
|
+
const providerDataDir = configPath === undefined ? dataDir : dirname(configPath);
|
|
232
|
+
if (!ensurePrivateDirectorySync(providerDataDir)) {
|
|
233
|
+
logger.error(`[providers] Could not secure provider data directory ${providerDataDir}`);
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
export const store = new Conf<ProvidersStore>({
|
|
@@ -228,8 +244,39 @@ export const store = new Conf<ProvidersStore>({
|
|
|
228
244
|
|
|
229
245
|
const providerSecretStore: ProviderSecretStore = createProviderSecretStore(dirname(store.path));
|
|
230
246
|
|
|
247
|
+
function providerMetadataStorageIsPrivate(forceFile = false): boolean {
|
|
248
|
+
if (!ensurePrivateDirectorySync(dirname(store.path))) return false;
|
|
249
|
+
if (!existsSync(store.path)) return true;
|
|
250
|
+
return securePrivateFileSync(store.path, forceFile);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function writeProviderMetadata(providers: readonly ProviderConfig[]): boolean {
|
|
254
|
+
if (!providerMetadataStorageIsPrivate()) {
|
|
255
|
+
logger.error(`[providers] Refusing provider metadata write to unsafe path ${store.path}`);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
store.set("providers", [...providers]);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
logger.error(`[providers] Could not persist provider metadata: ${String(error)}`);
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
if (securePrivateFileSync(store.path, true)) return true;
|
|
265
|
+
logger.error(`[providers] Could not secure provider metadata at ${store.path}`);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!providerMetadataStorageIsPrivate(true)) {
|
|
270
|
+
logger.error(`[providers] Provider metadata path is not private: ${store.path}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
231
273
|
function providerWithResolvedSecret(provider: ProviderConfig): ProviderConfig | null {
|
|
232
|
-
if (!providerSecretStore.isReference(provider.apiKey))
|
|
274
|
+
if (!providerSecretStore.isReference(provider.apiKey)) {
|
|
275
|
+
logger.error(
|
|
276
|
+
`[providers] Refusing plaintext secret for provider ${provider.id}; secure migration is required`,
|
|
277
|
+
);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
233
280
|
const apiKey = providerSecretStore.read(provider.apiKey);
|
|
234
281
|
if (apiKey !== null) return { ...provider, apiKey };
|
|
235
282
|
logger.error(`[providers] Secret is missing or unreadable for provider "${provider.name}"`);
|
|
@@ -239,7 +286,7 @@ function providerWithResolvedSecret(provider: ProviderConfig): ProviderConfig |
|
|
|
239
286
|
function persistProviders(
|
|
240
287
|
providers: readonly ProviderConfig[],
|
|
241
288
|
removedProviderIds: ReadonlySet<string> = new Set(),
|
|
242
|
-
):
|
|
289
|
+
): boolean {
|
|
243
290
|
const existingReferences = new Map<string, string>();
|
|
244
291
|
const unresolvedRecords = new Map<string, ProviderConfig>();
|
|
245
292
|
for (const candidate of store.get("providers", [])) {
|
|
@@ -252,41 +299,108 @@ function persistProviders(
|
|
|
252
299
|
}
|
|
253
300
|
}
|
|
254
301
|
|
|
255
|
-
const persisted =
|
|
302
|
+
const persisted: ProviderConfig[] = [];
|
|
303
|
+
for (const provider of providers) {
|
|
256
304
|
const existingReference = existingReferences.get(provider.id);
|
|
257
305
|
if (existingReference !== undefined) {
|
|
258
306
|
const existingSecret = providerSecretStore.read(existingReference);
|
|
259
307
|
if (existingSecret !== null && secretValuesEqual(existingSecret, provider.apiKey)) {
|
|
260
|
-
|
|
308
|
+
persisted.push({ ...provider, apiKey: existingReference });
|
|
309
|
+
continue;
|
|
261
310
|
}
|
|
262
311
|
}
|
|
263
312
|
|
|
264
313
|
const reference = providerSecretStore.write(provider.id, provider.apiKey);
|
|
265
|
-
if (reference !== null)
|
|
314
|
+
if (reference !== null) {
|
|
315
|
+
persisted.push({ ...provider, apiKey: reference });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
266
318
|
logger.error(
|
|
267
|
-
`[providers]
|
|
319
|
+
`[providers] Secret store unavailable; refusing to persist provider ${provider.id}`,
|
|
268
320
|
);
|
|
269
|
-
return
|
|
270
|
-
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
271
323
|
const persistedIds = new Set(persisted.map((provider) => provider.id));
|
|
272
324
|
for (const [id, provider] of unresolvedRecords) {
|
|
273
325
|
if (!persistedIds.has(id) && !removedProviderIds.has(id)) persisted.push(provider);
|
|
274
326
|
}
|
|
275
|
-
|
|
327
|
+
return writeProviderMetadata(persisted);
|
|
276
328
|
}
|
|
277
329
|
|
|
278
|
-
function migratePlaintextProviderSecrets():
|
|
330
|
+
function migratePlaintextProviderSecrets(): boolean {
|
|
279
331
|
const raw = store.get("providers", []);
|
|
280
332
|
let changed = false;
|
|
281
|
-
const migrated =
|
|
333
|
+
const migrated: ProviderConfig[] = [];
|
|
334
|
+
for (const candidate of raw) {
|
|
282
335
|
const parsed = ProviderConfigSchema.safeParse(candidate);
|
|
283
|
-
if (!parsed.success
|
|
336
|
+
if (!parsed.success) {
|
|
337
|
+
migrated.push(candidate);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (providerSecretStore.isReference(parsed.data.apiKey)) {
|
|
341
|
+
migrated.push(parsed.data);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
284
344
|
const reference = providerSecretStore.write(parsed.data.id, parsed.data.apiKey);
|
|
285
|
-
if (reference === null)
|
|
345
|
+
if (reference === null) {
|
|
346
|
+
logger.error(
|
|
347
|
+
`[providers] Could not migrate provider ${parsed.data.id} to secure storage; leaving the existing file unchanged`,
|
|
348
|
+
);
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
286
351
|
changed = true;
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
if (changed)
|
|
352
|
+
migrated.push({ ...parsed.data, apiKey: reference });
|
|
353
|
+
}
|
|
354
|
+
if (!changed) return true;
|
|
355
|
+
if (writeProviderMetadata(migrated)) return true;
|
|
356
|
+
logger.error("[providers] Could not finish provider secret migration");
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function sanitizeProviderBackupValue(value: unknown): unknown {
|
|
361
|
+
if (Array.isArray(value)) return value.map(sanitizeProviderBackupValue);
|
|
362
|
+
if (value === null || typeof value !== "object") return value;
|
|
363
|
+
|
|
364
|
+
return Object.fromEntries(
|
|
365
|
+
Object.entries(value).map(([key, entry]) => {
|
|
366
|
+
if (key === "apiKey" && typeof entry === "string") {
|
|
367
|
+
return [key, "redacted"];
|
|
368
|
+
}
|
|
369
|
+
return [key, sanitizeProviderBackupValue(entry)];
|
|
370
|
+
}),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function sanitizeProviderBackup(): void {
|
|
375
|
+
const backupPath = `${store.path}.bak`;
|
|
376
|
+
if (!existsSync(backupPath)) return;
|
|
377
|
+
|
|
378
|
+
const temporaryPath = `${backupPath}.${String(process.pid)}.tmp`;
|
|
379
|
+
try {
|
|
380
|
+
const parsed: unknown = JSON.parse(readFileSync(backupPath, "utf-8"));
|
|
381
|
+
const sanitized = sanitizeProviderBackupValue(parsed);
|
|
382
|
+
writeFileSync(temporaryPath, JSON.stringify(sanitized, null, 2), {
|
|
383
|
+
encoding: "utf8",
|
|
384
|
+
mode: PRIVATE_FILE_MODE,
|
|
385
|
+
});
|
|
386
|
+
if (!securePrivateFileSync(temporaryPath, true)) {
|
|
387
|
+
rmSync(temporaryPath, { force: true });
|
|
388
|
+
rmSync(backupPath, { force: true });
|
|
389
|
+
logger.error("[providers] Removed provider backup after temporary permission failure");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
renameSync(temporaryPath, backupPath);
|
|
393
|
+
if (!securePrivateFileSync(backupPath, true)) {
|
|
394
|
+
rmSync(backupPath, { force: true });
|
|
395
|
+
logger.error("[providers] Removed provider backup after permission failure");
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
rmSync(temporaryPath, { force: true });
|
|
399
|
+
rmSync(backupPath, { force: true });
|
|
400
|
+
logger.error(
|
|
401
|
+
`[providers] Removed provider backup that could not be sanitized: ${String(error)}`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
290
404
|
}
|
|
291
405
|
|
|
292
406
|
// Older builds stored providers in `<dataDir>/config.json`. Migrate that
|
|
@@ -297,6 +411,8 @@ migrateFromDataDirConfig(store, dataDir);
|
|
|
297
411
|
// already has data, when the user has set AGENT_INSPECTOR_CONFIG_PATH or
|
|
298
412
|
// an explicit data dir, or when the legacy file is empty.
|
|
299
413
|
migrateFromLegacyConfLocation(store);
|
|
414
|
+
sanitizeProviderBackup();
|
|
415
|
+
const providerSecretsReady = migratePlaintextProviderSecrets();
|
|
300
416
|
|
|
301
417
|
/**
|
|
302
418
|
* Expands the old `{ format, baseUrl }` representation into the matching
|
|
@@ -325,17 +441,17 @@ export function migrateProvider(p: ProviderConfig): ProviderConfig {
|
|
|
325
441
|
* Migrates existing provider configs to preserve Anthropic and OpenAI routing URLs.
|
|
326
442
|
*/
|
|
327
443
|
function migrateProviders(): void {
|
|
328
|
-
const providers =
|
|
444
|
+
const providers = getProviders();
|
|
329
445
|
const updated = providers.map(migrateProvider);
|
|
330
446
|
const migrated = updated.some((p, i) => JSON.stringify(p) !== JSON.stringify(providers[i]));
|
|
331
447
|
|
|
332
448
|
if (migrated) {
|
|
333
|
-
|
|
449
|
+
void persistProviders(updated);
|
|
334
450
|
}
|
|
335
451
|
}
|
|
336
452
|
|
|
337
453
|
// Run migration on module load
|
|
338
|
-
migrateProviders();
|
|
454
|
+
if (providerSecretsReady) migrateProviders();
|
|
339
455
|
|
|
340
456
|
/**
|
|
341
457
|
* Migrate: merge providers with identical API key and routing URLs.
|
|
@@ -343,7 +459,7 @@ migrateProviders();
|
|
|
343
459
|
* value promoted to `models: [model]`.
|
|
344
460
|
*/
|
|
345
461
|
function migrateMultiModel(): void {
|
|
346
|
-
const providers =
|
|
462
|
+
const providers = getProviders();
|
|
347
463
|
if (providers.length === 0) return;
|
|
348
464
|
let changed = false;
|
|
349
465
|
|
|
@@ -400,18 +516,21 @@ function migrateMultiModel(): void {
|
|
|
400
516
|
if (changed && merged.length > 0) {
|
|
401
517
|
// Safety backup before writing
|
|
402
518
|
try {
|
|
403
|
-
writeFileSync(`${store.path}.bak`, readFileSync(store.path, "utf-8")
|
|
519
|
+
writeFileSync(`${store.path}.bak`, readFileSync(store.path, "utf-8"), {
|
|
520
|
+
mode: PRIVATE_FILE_MODE,
|
|
521
|
+
});
|
|
522
|
+
sanitizeProviderBackup();
|
|
404
523
|
} catch {
|
|
405
524
|
// best-effort, ignore
|
|
406
525
|
}
|
|
407
|
-
|
|
526
|
+
void persistProviders(merged);
|
|
408
527
|
}
|
|
409
528
|
}
|
|
410
529
|
|
|
411
|
-
migrateMultiModel();
|
|
530
|
+
if (providerSecretsReady) migrateMultiModel();
|
|
412
531
|
|
|
413
532
|
function migrateBuiltinModelMetadata(): void {
|
|
414
|
-
const providers =
|
|
533
|
+
const providers = getProviders();
|
|
415
534
|
if (providers.length === 0) return;
|
|
416
535
|
|
|
417
536
|
const now = new Date().toISOString();
|
|
@@ -421,11 +540,11 @@ function migrateBuiltinModelMetadata(): void {
|
|
|
421
540
|
);
|
|
422
541
|
|
|
423
542
|
if (changed) {
|
|
424
|
-
|
|
543
|
+
void persistProviders(updated);
|
|
425
544
|
}
|
|
426
545
|
}
|
|
427
546
|
|
|
428
|
-
migrateBuiltinModelMetadata();
|
|
547
|
+
if (providerSecretsReady) migrateBuiltinModelMetadata();
|
|
429
548
|
|
|
430
549
|
// Override with JSON env var if provided (for testing)
|
|
431
550
|
const providersJson = process.env["AGENT_INSPECTOR_PROVIDERS_JSON"];
|
|
@@ -438,17 +557,13 @@ if (providersJson !== undefined) {
|
|
|
438
557
|
const migrated = parsed.data
|
|
439
558
|
.map(migrateProvider)
|
|
440
559
|
.map((provider) => withBuiltinProviderModelMetadata(provider, now));
|
|
441
|
-
|
|
560
|
+
void persistProviders(migrated);
|
|
442
561
|
}
|
|
443
562
|
} catch {
|
|
444
563
|
// Ignore invalid JSON
|
|
445
564
|
}
|
|
446
565
|
}
|
|
447
566
|
|
|
448
|
-
// One-time, backwards-compatible migration. Existing plaintext keys are moved
|
|
449
|
-
// out of providers.json; records retain only opaque references.
|
|
450
|
-
migratePlaintextProviderSecrets();
|
|
451
|
-
|
|
452
567
|
export function getProviders(): ProviderConfig[] {
|
|
453
568
|
const raw = store.get("providers", []);
|
|
454
569
|
// Validate the on-disk store entry-by-entry so a single corrupted record
|
|
@@ -480,15 +595,12 @@ export function getProvider(id: string): ProviderConfig | undefined {
|
|
|
480
595
|
return providers.find((p) => p.id === id);
|
|
481
596
|
}
|
|
482
597
|
|
|
483
|
-
export function redactProvider(provider: ProviderConfig):
|
|
484
|
-
return
|
|
485
|
-
...provider,
|
|
486
|
-
apiKey: "configured",
|
|
487
|
-
};
|
|
598
|
+
export function redactProvider(provider: ProviderConfig): RedactedProviderDto {
|
|
599
|
+
return toRedactedProviderDto(provider);
|
|
488
600
|
}
|
|
489
601
|
|
|
490
|
-
export function redactProviders(providers: readonly ProviderConfig[]):
|
|
491
|
-
return providers
|
|
602
|
+
export function redactProviders(providers: readonly ProviderConfig[]): RedactedProviderDto[] {
|
|
603
|
+
return toRedactedProviderDtos(providers);
|
|
492
604
|
}
|
|
493
605
|
|
|
494
606
|
/**
|
|
@@ -512,7 +624,7 @@ export function addProvider(
|
|
|
512
624
|
modelMetadataUrl?: string,
|
|
513
625
|
modelMetadata?: ProviderModelMetadata[],
|
|
514
626
|
openaiResponsesBaseUrl?: string,
|
|
515
|
-
): ProviderConfig {
|
|
627
|
+
): ProviderConfig | null {
|
|
516
628
|
const providers = getProviders();
|
|
517
629
|
const normalizedKey = normalizeApiKey(apiKey);
|
|
518
630
|
const resolvedAnthropicUrl =
|
|
@@ -546,7 +658,7 @@ export function addProvider(
|
|
|
546
658
|
if (existingIndex >= 0) {
|
|
547
659
|
providers[existingIndex] = enriched;
|
|
548
660
|
}
|
|
549
|
-
persistProviders(providers);
|
|
661
|
+
if (!persistProviders(providers)) return null;
|
|
550
662
|
return enriched;
|
|
551
663
|
}
|
|
552
664
|
|
|
@@ -578,7 +690,7 @@ export function addProvider(
|
|
|
578
690
|
now,
|
|
579
691
|
);
|
|
580
692
|
providers.push(newProvider);
|
|
581
|
-
persistProviders(providers);
|
|
693
|
+
if (!persistProviders(providers)) return null;
|
|
582
694
|
return newProvider;
|
|
583
695
|
}
|
|
584
696
|
|
|
@@ -626,7 +738,7 @@ export function updateProvider(
|
|
|
626
738
|
);
|
|
627
739
|
const index = providers.findIndex((p) => p.id === id);
|
|
628
740
|
providers[index] = updated;
|
|
629
|
-
persistProviders(providers);
|
|
741
|
+
if (!persistProviders(providers)) return null;
|
|
630
742
|
return updated;
|
|
631
743
|
}
|
|
632
744
|
|
|
@@ -635,7 +747,7 @@ export function deleteProvider(id: string): boolean {
|
|
|
635
747
|
const filtered = providers.filter((p) => p.id !== id);
|
|
636
748
|
if (filtered.length === providers.length) return false;
|
|
637
749
|
|
|
638
|
-
persistProviders(filtered, new Set([id]));
|
|
750
|
+
if (!persistProviders(filtered, new Set([id]))) return false;
|
|
639
751
|
providerSecretStore.delete(id);
|
|
640
752
|
return true;
|
|
641
753
|
}
|
|
@@ -733,12 +845,17 @@ export function importProviders(json: string): { imported: number; errors: strin
|
|
|
733
845
|
|
|
734
846
|
newProviders.push(newProvider);
|
|
735
847
|
existingKeys.add(key);
|
|
736
|
-
imported++;
|
|
737
848
|
}
|
|
738
849
|
|
|
739
850
|
// Single write: append all new providers at once
|
|
740
851
|
if (newProviders.length > 0) {
|
|
741
|
-
persistProviders([...existingProviders, ...newProviders])
|
|
852
|
+
if (!persistProviders([...existingProviders, ...newProviders])) {
|
|
853
|
+
return {
|
|
854
|
+
imported: 0,
|
|
855
|
+
errors: ["Secure provider storage is unavailable; no providers were imported"],
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
imported = newProviders.length;
|
|
742
859
|
}
|
|
743
860
|
} catch (err) {
|
|
744
861
|
return {
|