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/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
- "model",
26
- "thinkingLevel",
27
- "responsesApis",
28
- "notifyOnLoad",
29
- "debug",
30
- "logProviderPayloads",
31
- "logCompactResponses",
32
- "redactSensitiveData",
33
- "artifactRoot",
34
- ]);
35
- const WEB_SEARCH_FIELDS = new Set(["enabled", "models"]);
36
-
37
- function isRecord(value: unknown): value is Record<string, unknown> {
38
- return !!value && typeof value === "object" && !Array.isArray(value);
39
- }
40
-
41
- function isFile(filePath: string): boolean {
42
- try {
43
- return fs.statSync(filePath).isFile();
44
- } catch {
45
- return false;
46
- }
47
- }
48
-
49
- function readJsonObject(filePath: string, warnings: string[]): Record<string, unknown> | undefined {
50
- if (!isFile(filePath)) return undefined;
51
-
52
- try {
53
- const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
54
- if (isRecord(parsed)) return parsed;
55
- warnings.push(`Ignoring ${filePath}: expected a JSON object at the top level.`);
56
- return undefined;
57
- } catch (error) {
58
- const message = error instanceof Error ? error.message : String(error);
59
- warnings.push(`Ignoring ${filePath}: ${message}`);
60
- return undefined;
61
- }
62
- }
63
-
64
- function resolveConfiguredPath(rawPath: string, baseDir: string): string {
65
- if (rawPath.startsWith("~/")) {
66
- return path.join(os.homedir(), rawPath.slice(2));
67
- }
68
- if (path.isAbsolute(rawPath)) {
69
- return path.resolve(rawPath);
70
- }
71
- return path.resolve(baseDir, rawPath);
72
- }
73
-
74
- function warnUnknownFields(
75
- value: Record<string, unknown>,
76
- knownFields: ReadonlySet<string>,
77
- fieldPath: string,
78
- warnings: string[],
79
- ): void {
80
- for (const key of Object.keys(value)) {
81
- if (!knownFields.has(key)) {
82
- warnings.push(`Ignoring ${fieldPath ? `${fieldPath}.` : ""}${key}: unknown field.`);
83
- }
84
- }
85
- }
86
-
87
- function toBoolean(value: unknown, fieldPath: string, warnings: string[]): boolean | undefined {
88
- if (value === undefined) return undefined;
89
- if (typeof value === "boolean") return value;
90
- warnings.push(`Ignoring ${fieldPath}: expected a boolean.`);
91
- return undefined;
92
- }
93
-
94
- function toModelSpec(value: unknown, fieldPath: string, warnings: string[]): string | null | undefined {
95
- if (value === undefined) return undefined;
96
- if (value === null) return null;
97
- if (typeof value === "string" && value.trim().length > 0) {
98
- return value.trim();
99
- }
100
- warnings.push(`Ignoring ${fieldPath}: expected "provider/model-id" or null.`);
101
- return undefined;
102
- }
103
-
104
- function toThinkingLevel(value: unknown, fieldPath: string, warnings: string[]): ThinkingLevel | undefined {
105
- if (value === undefined) return undefined;
106
- if (typeof value === "string" && (THINKING_LEVELS as readonly string[]).includes(value)) {
107
- return value as ThinkingLevel;
108
- }
109
- warnings.push(`Ignoring ${fieldPath}: expected one of ${THINKING_LEVELS.join(", ")}.`);
110
- return undefined;
111
- }
112
-
113
- function toSupportedApis(
114
- value: unknown,
115
- fieldPath: string,
116
- capableApis: readonly string[],
117
- warnings: string[],
118
- ): string[] | undefined {
119
- if (value === undefined) return undefined;
120
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
121
- warnings.push(`Ignoring ${fieldPath}: expected a string array.`);
122
- return undefined;
123
- }
124
-
125
- const capable = new Set(capableApis);
126
- const accepted: string[] = [];
127
- for (const item of new Set(value.map((entry) => entry.trim()).filter(Boolean))) {
128
- if (capable.has(item)) {
129
- accepted.push(item);
130
- } else {
131
- warnings.push(
132
- `Ignoring ${fieldPath} entry "${item}": only ${capableApis.join(", ")} are supported.`,
133
- );
134
- }
135
- }
136
-
137
- return accepted;
138
- }
139
-
140
- function toModelAllowlist(value: unknown, fieldPath: string, warnings: string[]): string[] | undefined {
141
- if (value === undefined) return undefined;
142
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
143
- warnings.push(`Ignoring ${fieldPath}: expected a string array.`);
144
- return undefined;
145
- }
146
-
147
- return [...new Set(value.map((entry) => entry.trim()).filter(Boolean))];
148
- }
149
-
150
- function cloneDefaults(): ToolkitConfig {
151
- return {
152
- compaction: {
153
- ...DEFAULT_COMPACTION_CONFIG,
154
- responsesApis: [...DEFAULT_COMPACTION_CONFIG.responsesApis],
155
- },
156
- webSearch: {
157
- ...DEFAULT_WEB_SEARCH_CONFIG,
158
- models: [...DEFAULT_WEB_SEARCH_CONFIG.models],
159
- },
160
- };
161
- }
162
-
163
- function applyCompactionConfig(
164
- raw: Record<string, unknown>,
165
- resolved: CompactionConfig,
166
- warnings: string[],
167
- ): void {
168
- warnUnknownFields(raw, COMPACTION_FIELDS, "compaction", warnings);
169
-
170
- resolved.enabled = toBoolean(raw.enabled, "compaction.enabled", warnings) ?? resolved.enabled;
171
- resolved.allowCompactionContinuityBreak =
172
- toBoolean(
173
- raw.allowCompactionContinuityBreak,
174
- "compaction.allowCompactionContinuityBreak",
175
- warnings,
176
- ) ?? resolved.allowCompactionContinuityBreak;
177
- resolved.notifyOnLoad =
178
- toBoolean(raw.notifyOnLoad, "compaction.notifyOnLoad", warnings) ?? resolved.notifyOnLoad;
179
- resolved.debug = toBoolean(raw.debug, "compaction.debug", warnings) ?? resolved.debug;
180
- resolved.logProviderPayloads =
181
- toBoolean(raw.logProviderPayloads, "compaction.logProviderPayloads", warnings) ??
182
- resolved.logProviderPayloads;
183
- resolved.logCompactResponses =
184
- toBoolean(raw.logCompactResponses, "compaction.logCompactResponses", warnings) ??
185
- resolved.logCompactResponses;
186
- resolved.redactSensitiveData =
187
- toBoolean(raw.redactSensitiveData, "compaction.redactSensitiveData", warnings) ??
188
- resolved.redactSensitiveData;
189
-
190
- const modelSpec = toModelSpec(raw.model, "compaction.model", warnings);
191
- if (modelSpec !== undefined) {
192
- resolved.model = modelSpec === null ? undefined : modelSpec;
193
- }
194
-
195
- resolved.thinkingLevel =
196
- toThinkingLevel(raw.thinkingLevel, "compaction.thinkingLevel", warnings) ?? resolved.thinkingLevel;
197
-
198
- const apis = toSupportedApis(
199
- raw.responsesApis,
200
- "compaction.responsesApis",
201
- RESPONSES_COMPACT_CAPABLE_APIS,
202
- warnings,
203
- );
204
- if (apis !== undefined) {
205
- resolved.responsesApis = apis;
206
- }
207
-
208
- if (typeof raw.artifactRoot === "string" && raw.artifactRoot.trim().length > 0) {
209
- resolved.artifactRoot = raw.artifactRoot.trim();
210
- } else if (raw.artifactRoot !== undefined) {
211
- warnings.push("Ignoring compaction.artifactRoot: expected a non-empty string.");
212
- }
213
- }
214
-
215
- function applyWebSearchConfig(
216
- raw: Record<string, unknown>,
217
- resolved: WebSearchConfig,
218
- warnings: string[],
219
- ): void {
220
- warnUnknownFields(raw, WEB_SEARCH_FIELDS, "webSearch", warnings);
221
- resolved.enabled = toBoolean(raw.enabled, "webSearch.enabled", warnings) ?? resolved.enabled;
222
-
223
- const models = toModelAllowlist(raw.models, "webSearch.models", warnings);
224
- if (models !== undefined) {
225
- resolved.models = models;
226
- }
227
- }
228
-
229
- /**
230
- * Load the canonical toolkit config from
231
- * `~/.pi/agent/extensions/pi-openai-toolkit/config.json`.
232
- * A missing file silently yields defaults; legacy branded paths are never read.
233
- */
234
- export function loadToolkitConfig(configPath: string = CONFIG_PATH): LoadedToolkitConfig {
235
- const warnings: string[] = [];
236
- const resolved = cloneDefaults();
237
- let source: string | undefined;
238
-
239
- const raw = readJsonObject(configPath, warnings);
240
- if (raw) {
241
- source = configPath;
242
- warnUnknownFields(raw, TOP_LEVEL_FIELDS, "", warnings);
243
-
244
- if (raw.compaction !== undefined) {
245
- if (isRecord(raw.compaction)) {
246
- applyCompactionConfig(raw.compaction, resolved.compaction, warnings);
247
- } else {
248
- warnings.push("Ignoring compaction: expected a JSON object.");
249
- }
250
- }
251
-
252
- if (raw.webSearch !== undefined) {
253
- if (isRecord(raw.webSearch)) {
254
- applyWebSearchConfig(raw.webSearch, resolved.webSearch, warnings);
255
- } else {
256
- warnings.push("Ignoring webSearch: expected a JSON object.");
257
- }
258
- }
259
- }
260
-
261
- resolved.compaction.artifactRoot = resolveConfiguredPath(
262
- resolved.compaction.artifactRoot,
263
- path.dirname(configPath),
264
- );
265
-
266
- return {
267
- config: resolved,
268
- source,
269
- warnings,
270
- };
271
- }
272
-
273
- export { DEFAULT_TOOLKIT_CONFIG };
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 shouldRedactKey(key: string): boolean {
56
- return SENSITIVE_KEY_RE.test(key);
57
- }
58
-
59
- export function redactValue(value: unknown, options: RedactOptions = {}): unknown {
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] = shouldRedactKey(key) ? placeholder : visit(item);
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(data) : data,
174
+ data: settings.redactSensitiveData ? redactValue(criticallyRedactedData) : criticallyRedactedData,
164
175
  };
165
176
 
166
177
  fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`, "utf8");