pi-openai-toolkit 0.3.0 → 0.4.0
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/README.md +25 -4
- package/README_zh.md +25 -4
- package/package.json +79 -79
- package/src/config.ts +283 -273
- package/src/debug.ts +18 -7
- package/src/extension-runtime.ts +92 -50
- package/src/native-fallback.ts +2 -21
- package/src/runtime.ts +445 -283
- package/src/supported-environment.ts +5 -0
- package/src/types.ts +18 -0
- package/tsconfig.check.json +36 -36
package/src/config.ts
CHANGED
|
@@ -1,273 +1,283 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
5
|
-
import {
|
|
6
|
-
DEFAULT_COMPACTION_CONFIG,
|
|
7
|
-
DEFAULT_TOOLKIT_CONFIG,
|
|
8
|
-
DEFAULT_WEB_SEARCH_CONFIG,
|
|
9
|
-
RESPONSES_COMPACT_CAPABLE_APIS,
|
|
10
|
-
THINKING_LEVELS,
|
|
11
|
-
TOOLKIT_ID,
|
|
12
|
-
type CompactionConfig,
|
|
13
|
-
type LoadedToolkitConfig,
|
|
14
|
-
type ToolkitConfig,
|
|
15
|
-
type WebSearchConfig,
|
|
16
|
-
} from "./types";
|
|
17
|
-
|
|
18
|
-
export const CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", TOOLKIT_ID);
|
|
19
|
-
export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
20
|
-
|
|
21
|
-
const TOP_LEVEL_FIELDS = new Set(["compaction", "webSearch"]);
|
|
22
|
-
const COMPACTION_FIELDS = new Set([
|
|
23
|
-
"enabled",
|
|
24
|
-
"allowCompactionContinuityBreak",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (value ===
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
resolved.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
resolved.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_COMPACTION_CONFIG,
|
|
7
|
+
DEFAULT_TOOLKIT_CONFIG,
|
|
8
|
+
DEFAULT_WEB_SEARCH_CONFIG,
|
|
9
|
+
RESPONSES_COMPACT_CAPABLE_APIS,
|
|
10
|
+
THINKING_LEVELS,
|
|
11
|
+
TOOLKIT_ID,
|
|
12
|
+
type CompactionConfig,
|
|
13
|
+
type LoadedToolkitConfig,
|
|
14
|
+
type ToolkitConfig,
|
|
15
|
+
type WebSearchConfig,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", TOOLKIT_ID);
|
|
19
|
+
export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
20
|
+
|
|
21
|
+
const TOP_LEVEL_FIELDS = new Set(["compaction", "webSearch"]);
|
|
22
|
+
const COMPACTION_FIELDS = new Set([
|
|
23
|
+
"enabled",
|
|
24
|
+
"allowCompactionContinuityBreak",
|
|
25
|
+
"remoteCompactModel",
|
|
26
|
+
"model",
|
|
27
|
+
"thinkingLevel",
|
|
28
|
+
"responsesApis",
|
|
29
|
+
"notifyOnLoad",
|
|
30
|
+
"debug",
|
|
31
|
+
"logProviderPayloads",
|
|
32
|
+
"logCompactResponses",
|
|
33
|
+
"redactSensitiveData",
|
|
34
|
+
"artifactRoot",
|
|
35
|
+
]);
|
|
36
|
+
const WEB_SEARCH_FIELDS = new Set(["enabled", "models"]);
|
|
37
|
+
|
|
38
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isFile(filePath: string): boolean {
|
|
43
|
+
try {
|
|
44
|
+
return fs.statSync(filePath).isFile();
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readJsonObject(filePath: string, warnings: string[]): Record<string, unknown> | undefined {
|
|
51
|
+
if (!isFile(filePath)) return undefined;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
55
|
+
if (isRecord(parsed)) return parsed;
|
|
56
|
+
warnings.push(`Ignoring ${filePath}: expected a JSON object at the top level.`);
|
|
57
|
+
return undefined;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
+
warnings.push(`Ignoring ${filePath}: ${message}`);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveConfiguredPath(rawPath: string, baseDir: string): string {
|
|
66
|
+
if (rawPath.startsWith("~/")) {
|
|
67
|
+
return path.join(os.homedir(), rawPath.slice(2));
|
|
68
|
+
}
|
|
69
|
+
if (path.isAbsolute(rawPath)) {
|
|
70
|
+
return path.resolve(rawPath);
|
|
71
|
+
}
|
|
72
|
+
return path.resolve(baseDir, rawPath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function warnUnknownFields(
|
|
76
|
+
value: Record<string, unknown>,
|
|
77
|
+
knownFields: ReadonlySet<string>,
|
|
78
|
+
fieldPath: string,
|
|
79
|
+
warnings: string[],
|
|
80
|
+
): void {
|
|
81
|
+
for (const key of Object.keys(value)) {
|
|
82
|
+
if (!knownFields.has(key)) {
|
|
83
|
+
warnings.push(`Ignoring ${fieldPath ? `${fieldPath}.` : ""}${key}: unknown field.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toBoolean(value: unknown, fieldPath: string, warnings: string[]): boolean | undefined {
|
|
89
|
+
if (value === undefined) return undefined;
|
|
90
|
+
if (typeof value === "boolean") return value;
|
|
91
|
+
warnings.push(`Ignoring ${fieldPath}: expected a boolean.`);
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toModelSpec(value: unknown, fieldPath: string, warnings: string[]): string | null | undefined {
|
|
96
|
+
if (value === undefined) return undefined;
|
|
97
|
+
if (value === null) return null;
|
|
98
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
99
|
+
return value.trim();
|
|
100
|
+
}
|
|
101
|
+
warnings.push(`Ignoring ${fieldPath}: expected "provider/model-id" or null.`);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toThinkingLevel(value: unknown, fieldPath: string, warnings: string[]): ThinkingLevel | undefined {
|
|
106
|
+
if (value === undefined) return undefined;
|
|
107
|
+
if (typeof value === "string" && (THINKING_LEVELS as readonly string[]).includes(value)) {
|
|
108
|
+
return value as ThinkingLevel;
|
|
109
|
+
}
|
|
110
|
+
warnings.push(`Ignoring ${fieldPath}: expected one of ${THINKING_LEVELS.join(", ")}.`);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toSupportedApis(
|
|
115
|
+
value: unknown,
|
|
116
|
+
fieldPath: string,
|
|
117
|
+
capableApis: readonly string[],
|
|
118
|
+
warnings: string[],
|
|
119
|
+
): string[] | undefined {
|
|
120
|
+
if (value === undefined) return undefined;
|
|
121
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
122
|
+
warnings.push(`Ignoring ${fieldPath}: expected a string array.`);
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const capable = new Set(capableApis);
|
|
127
|
+
const accepted: string[] = [];
|
|
128
|
+
for (const item of new Set(value.map((entry) => entry.trim()).filter(Boolean))) {
|
|
129
|
+
if (capable.has(item)) {
|
|
130
|
+
accepted.push(item);
|
|
131
|
+
} else {
|
|
132
|
+
warnings.push(
|
|
133
|
+
`Ignoring ${fieldPath} entry "${item}": only ${capableApis.join(", ")} are supported.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return accepted;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function toModelAllowlist(value: unknown, fieldPath: string, warnings: string[]): string[] | undefined {
|
|
142
|
+
if (value === undefined) return undefined;
|
|
143
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
144
|
+
warnings.push(`Ignoring ${fieldPath}: expected a string array.`);
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [...new Set(value.map((entry) => entry.trim()).filter(Boolean))];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function cloneDefaults(): ToolkitConfig {
|
|
152
|
+
return {
|
|
153
|
+
compaction: {
|
|
154
|
+
...DEFAULT_COMPACTION_CONFIG,
|
|
155
|
+
responsesApis: [...DEFAULT_COMPACTION_CONFIG.responsesApis],
|
|
156
|
+
},
|
|
157
|
+
webSearch: {
|
|
158
|
+
...DEFAULT_WEB_SEARCH_CONFIG,
|
|
159
|
+
models: [...DEFAULT_WEB_SEARCH_CONFIG.models],
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function applyCompactionConfig(
|
|
165
|
+
raw: Record<string, unknown>,
|
|
166
|
+
resolved: CompactionConfig,
|
|
167
|
+
warnings: string[],
|
|
168
|
+
): void {
|
|
169
|
+
warnUnknownFields(raw, COMPACTION_FIELDS, "compaction", warnings);
|
|
170
|
+
|
|
171
|
+
resolved.enabled = toBoolean(raw.enabled, "compaction.enabled", warnings) ?? resolved.enabled;
|
|
172
|
+
resolved.allowCompactionContinuityBreak =
|
|
173
|
+
toBoolean(
|
|
174
|
+
raw.allowCompactionContinuityBreak,
|
|
175
|
+
"compaction.allowCompactionContinuityBreak",
|
|
176
|
+
warnings,
|
|
177
|
+
) ?? resolved.allowCompactionContinuityBreak;
|
|
178
|
+
resolved.notifyOnLoad =
|
|
179
|
+
toBoolean(raw.notifyOnLoad, "compaction.notifyOnLoad", warnings) ?? resolved.notifyOnLoad;
|
|
180
|
+
resolved.debug = toBoolean(raw.debug, "compaction.debug", warnings) ?? resolved.debug;
|
|
181
|
+
resolved.logProviderPayloads =
|
|
182
|
+
toBoolean(raw.logProviderPayloads, "compaction.logProviderPayloads", warnings) ??
|
|
183
|
+
resolved.logProviderPayloads;
|
|
184
|
+
resolved.logCompactResponses =
|
|
185
|
+
toBoolean(raw.logCompactResponses, "compaction.logCompactResponses", warnings) ??
|
|
186
|
+
resolved.logCompactResponses;
|
|
187
|
+
resolved.redactSensitiveData =
|
|
188
|
+
toBoolean(raw.redactSensitiveData, "compaction.redactSensitiveData", warnings) ??
|
|
189
|
+
resolved.redactSensitiveData;
|
|
190
|
+
|
|
191
|
+
const remoteCompactModelSpec = toModelSpec(
|
|
192
|
+
raw.remoteCompactModel,
|
|
193
|
+
"compaction.remoteCompactModel",
|
|
194
|
+
warnings,
|
|
195
|
+
);
|
|
196
|
+
if (remoteCompactModelSpec !== undefined) {
|
|
197
|
+
resolved.remoteCompactModel = remoteCompactModelSpec === null ? undefined : remoteCompactModelSpec;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const modelSpec = toModelSpec(raw.model, "compaction.model", warnings);
|
|
201
|
+
if (modelSpec !== undefined) {
|
|
202
|
+
resolved.model = modelSpec === null ? undefined : modelSpec;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
resolved.thinkingLevel =
|
|
206
|
+
toThinkingLevel(raw.thinkingLevel, "compaction.thinkingLevel", warnings) ?? resolved.thinkingLevel;
|
|
207
|
+
|
|
208
|
+
const apis = toSupportedApis(
|
|
209
|
+
raw.responsesApis,
|
|
210
|
+
"compaction.responsesApis",
|
|
211
|
+
RESPONSES_COMPACT_CAPABLE_APIS,
|
|
212
|
+
warnings,
|
|
213
|
+
);
|
|
214
|
+
if (apis !== undefined) {
|
|
215
|
+
resolved.responsesApis = apis;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (typeof raw.artifactRoot === "string" && raw.artifactRoot.trim().length > 0) {
|
|
219
|
+
resolved.artifactRoot = raw.artifactRoot.trim();
|
|
220
|
+
} else if (raw.artifactRoot !== undefined) {
|
|
221
|
+
warnings.push("Ignoring compaction.artifactRoot: expected a non-empty string.");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function applyWebSearchConfig(
|
|
226
|
+
raw: Record<string, unknown>,
|
|
227
|
+
resolved: WebSearchConfig,
|
|
228
|
+
warnings: string[],
|
|
229
|
+
): void {
|
|
230
|
+
warnUnknownFields(raw, WEB_SEARCH_FIELDS, "webSearch", warnings);
|
|
231
|
+
resolved.enabled = toBoolean(raw.enabled, "webSearch.enabled", warnings) ?? resolved.enabled;
|
|
232
|
+
|
|
233
|
+
const models = toModelAllowlist(raw.models, "webSearch.models", warnings);
|
|
234
|
+
if (models !== undefined) {
|
|
235
|
+
resolved.models = models;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Load the canonical toolkit config from
|
|
241
|
+
* `~/.pi/agent/extensions/pi-openai-toolkit/config.json`.
|
|
242
|
+
* A missing file silently yields defaults; legacy branded paths are never read.
|
|
243
|
+
*/
|
|
244
|
+
export function loadToolkitConfig(configPath: string = CONFIG_PATH): LoadedToolkitConfig {
|
|
245
|
+
const warnings: string[] = [];
|
|
246
|
+
const resolved = cloneDefaults();
|
|
247
|
+
let source: string | undefined;
|
|
248
|
+
|
|
249
|
+
const raw = readJsonObject(configPath, warnings);
|
|
250
|
+
if (raw) {
|
|
251
|
+
source = configPath;
|
|
252
|
+
warnUnknownFields(raw, TOP_LEVEL_FIELDS, "", warnings);
|
|
253
|
+
|
|
254
|
+
if (raw.compaction !== undefined) {
|
|
255
|
+
if (isRecord(raw.compaction)) {
|
|
256
|
+
applyCompactionConfig(raw.compaction, resolved.compaction, warnings);
|
|
257
|
+
} else {
|
|
258
|
+
warnings.push("Ignoring compaction: expected a JSON object.");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (raw.webSearch !== undefined) {
|
|
263
|
+
if (isRecord(raw.webSearch)) {
|
|
264
|
+
applyWebSearchConfig(raw.webSearch, resolved.webSearch, warnings);
|
|
265
|
+
} else {
|
|
266
|
+
warnings.push("Ignoring webSearch: expected a JSON object.");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
resolved.compaction.artifactRoot = resolveConfiguredPath(
|
|
272
|
+
resolved.compaction.artifactRoot,
|
|
273
|
+
path.dirname(configPath),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
config: resolved,
|
|
278
|
+
source,
|
|
279
|
+
warnings,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export { DEFAULT_TOOLKIT_CONFIG };
|
package/src/debug.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type RedactOptions,
|
|
15
15
|
} from "./types";
|
|
16
16
|
|
|
17
|
+
const CRITICAL_KEY_RE = /(authorization|api[-_]?key|token|credential|oauth|auth|encrypted[_-]?content)/i;
|
|
17
18
|
const SENSITIVE_KEY_RE = /(authorization|api[-_]?key|token|secret|password|cookie|set-cookie|signature|credential|oauth|auth|encrypted[_-]?content)/i;
|
|
18
19
|
const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+/=]+/gi;
|
|
19
20
|
const OPENAI_KEY_RE = /\bsk-[A-Za-z0-9\-_]+\b/g;
|
|
@@ -52,11 +53,11 @@ function redactInlineSecrets(value: string, placeholder: string): string {
|
|
|
52
53
|
.replace(SENSITIVE_QUERY_RE, (_match, prefix: string) => `${prefix}${placeholder}`);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
function redactWithKeyPattern(
|
|
57
|
+
value: unknown,
|
|
58
|
+
keyPattern: RegExp,
|
|
59
|
+
options: RedactOptions = {},
|
|
60
|
+
): unknown {
|
|
60
61
|
const placeholder = options.placeholder ?? REDACTED_VALUE;
|
|
61
62
|
const seen = new WeakSet<object>();
|
|
62
63
|
|
|
@@ -78,7 +79,7 @@ export function redactValue(value: unknown, options: RedactOptions = {}): unknow
|
|
|
78
79
|
|
|
79
80
|
const result: Record<string, unknown> = {};
|
|
80
81
|
for (const [key, item] of Object.entries(input)) {
|
|
81
|
-
result[key] =
|
|
82
|
+
result[key] = keyPattern.test(key) ? placeholder : visit(item);
|
|
82
83
|
}
|
|
83
84
|
return result;
|
|
84
85
|
};
|
|
@@ -86,6 +87,15 @@ export function redactValue(value: unknown, options: RedactOptions = {}): unknow
|
|
|
86
87
|
return visit(value);
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/** Credentials and opaque checkpoints are never written to artifacts, even when optional redaction is disabled. */
|
|
91
|
+
export function redactCriticalValue(value: unknown, options: RedactOptions = {}): unknown {
|
|
92
|
+
return redactWithKeyPattern(value, CRITICAL_KEY_RE, options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function redactValue(value: unknown, options: RedactOptions = {}): unknown {
|
|
96
|
+
return redactWithKeyPattern(value, SENSITIVE_KEY_RE, options);
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
export function resolveArtifactPaths(settings: CompactionConfig, context: ArtifactContext): ArtifactPaths {
|
|
90
100
|
const sessionInfo = toSessionInfo(context);
|
|
91
101
|
const rootDir = settings.artifactRoot.startsWith("~/")
|
|
@@ -149,6 +159,7 @@ export function writeDebugArtifact(
|
|
|
149
159
|
const timestamp = new Date().toISOString();
|
|
150
160
|
const fileName = `${timestamp.replace(/[.:]/g, "-")}-${kind}.json`;
|
|
151
161
|
const filePath = path.join(targetDir, fileName);
|
|
162
|
+
const criticallyRedactedData = redactCriticalValue(data);
|
|
152
163
|
const envelope: DebugArtifactEnvelope = {
|
|
153
164
|
extension: COMPACTION_EXTENSION_ID,
|
|
154
165
|
kind,
|
|
@@ -160,7 +171,7 @@ export function writeDebugArtifact(
|
|
|
160
171
|
redaction: {
|
|
161
172
|
enabled: settings.redactSensitiveData,
|
|
162
173
|
},
|
|
163
|
-
data: settings.redactSensitiveData ? redactValue(
|
|
174
|
+
data: settings.redactSensitiveData ? redactValue(criticallyRedactedData) : criticallyRedactedData,
|
|
164
175
|
};
|
|
165
176
|
|
|
166
177
|
fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`, "utf8");
|