@poolzin/pool-bot 2026.2.23 → 2026.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -1,7 +1,46 @@
1
- function isPlainObject(value) {
2
- return typeof value === "object" && value !== null && !Array.isArray(value);
1
+ import { isPlainObject } from "../utils.js";
2
+ function isObjectWithStringId(value) {
3
+ if (!isPlainObject(value)) {
4
+ return false;
5
+ }
6
+ return typeof value.id === "string" && value.id.length > 0;
3
7
  }
4
- export function applyMergePatch(base, patch) {
8
+ /**
9
+ * Merge arrays of object-like entries keyed by `id`.
10
+ *
11
+ * Contract:
12
+ * - Base array must be fully id-keyed; otherwise return undefined (caller should replace).
13
+ * - Patch entries with valid id merge by id (or append when the id is new).
14
+ * - Patch entries without valid id append as-is, avoiding destructive full-array replacement.
15
+ */
16
+ function mergeObjectArraysById(base, patch, options) {
17
+ if (!base.every(isObjectWithStringId)) {
18
+ return undefined;
19
+ }
20
+ const merged = [...base];
21
+ const indexById = new Map();
22
+ for (const [index, entry] of merged.entries()) {
23
+ if (!isObjectWithStringId(entry)) {
24
+ return undefined;
25
+ }
26
+ indexById.set(entry.id, index);
27
+ }
28
+ for (const patchEntry of patch) {
29
+ if (!isObjectWithStringId(patchEntry)) {
30
+ merged.push(structuredClone(patchEntry));
31
+ continue;
32
+ }
33
+ const existingIndex = indexById.get(patchEntry.id);
34
+ if (existingIndex === undefined) {
35
+ merged.push(structuredClone(patchEntry));
36
+ indexById.set(patchEntry.id, merged.length - 1);
37
+ continue;
38
+ }
39
+ merged[existingIndex] = applyMergePatch(merged[existingIndex], patchEntry, options);
40
+ }
41
+ return merged;
42
+ }
43
+ export function applyMergePatch(base, patch, options = {}) {
5
44
  if (!isPlainObject(patch)) {
6
45
  return patch;
7
46
  }
@@ -11,9 +50,16 @@ export function applyMergePatch(base, patch) {
11
50
  delete result[key];
12
51
  continue;
13
52
  }
53
+ if (options.mergeObjectArraysById && Array.isArray(result[key]) && Array.isArray(value)) {
54
+ const mergedArray = mergeObjectArraysById(result[key], value, options);
55
+ if (mergedArray) {
56
+ result[key] = mergedArray;
57
+ continue;
58
+ }
59
+ }
14
60
  if (isPlainObject(value)) {
15
61
  const baseValue = result[key];
16
- result[key] = applyMergePatch(isPlainObject(baseValue) ? baseValue : {}, value);
62
+ result[key] = applyMergePatch(isPlainObject(baseValue) ? baseValue : {}, value, options);
17
63
  continue;
18
64
  }
19
65
  result[key] = value;
@@ -1,97 +1,223 @@
1
+ import { createSubsystemLogger } from "../logging/subsystem.js";
2
+ import { isSensitiveConfigPath } from "./schema.hints.js";
3
+ const log = createSubsystemLogger("config/redaction");
4
+ const ENV_VAR_PLACEHOLDER_PATTERN = /^\$\{[^}]*\}$/;
5
+ function isSensitivePath(path) {
6
+ if (path.endsWith("[]")) {
7
+ return isSensitiveConfigPath(path.slice(0, -2));
8
+ }
9
+ else {
10
+ return isSensitiveConfigPath(path);
11
+ }
12
+ }
13
+ function isEnvVarPlaceholder(value) {
14
+ return ENV_VAR_PLACEHOLDER_PATTERN.test(value.trim());
15
+ }
16
+ function isExtensionPath(path) {
17
+ return (path === "plugins" ||
18
+ path.startsWith("plugins.") ||
19
+ path === "channels" ||
20
+ path.startsWith("channels."));
21
+ }
22
+ function isExplicitlyNonSensitivePath(hints, paths) {
23
+ if (!hints) {
24
+ return false;
25
+ }
26
+ return paths.some((path) => hints[path]?.sensitive === false);
27
+ }
1
28
  /**
2
29
  * Sentinel value used to replace sensitive config fields in gateway responses.
3
30
  * Write-side handlers (config.set, config.apply, config.patch) detect this
4
31
  * sentinel and restore the original value from the on-disk config, so a
5
32
  * round-trip through the Web UI does not corrupt credentials.
6
33
  */
7
- export const REDACTED_SENTINEL = "__POOLBOT_REDACTED__";
8
- /**
9
- * Patterns that identify sensitive config field names.
10
- * Aligned with the UI-hint logic in schema.ts.
11
- */
12
- const SENSITIVE_KEY_PATTERNS = [/token/i, /password/i, /secret/i, /api.?key/i];
13
- function isSensitiveKey(key) {
14
- return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key));
34
+ export const REDACTED_SENTINEL = "__CLAWDBOT_REDACTED__";
35
+ // ConfigUiHints' keys look like this:
36
+ // - path.subpath.key (nested objects)
37
+ // - path.subpath[].key (object in array in object)
38
+ // - path.*.key (object in record in object)
39
+ // records are handled by the lookup, but arrays need two entries in
40
+ // the Set, as their first lookup is done before the code knows it's
41
+ // an array.
42
+ function buildRedactionLookup(hints) {
43
+ let result = new Set();
44
+ for (const [path, hint] of Object.entries(hints)) {
45
+ if (!hint.sensitive) {
46
+ continue;
47
+ }
48
+ const parts = path.split(".");
49
+ let joinedPath = parts.shift() ?? "";
50
+ result.add(joinedPath);
51
+ if (joinedPath.endsWith("[]")) {
52
+ result.add(joinedPath.slice(0, -2));
53
+ }
54
+ for (const part of parts) {
55
+ if (part.endsWith("[]")) {
56
+ result.add(`${joinedPath}.${part.slice(0, -2)}`);
57
+ }
58
+ // hey, greptile, notice how this is *NOT* in an else block?
59
+ joinedPath = `${joinedPath}.${part}`;
60
+ result.add(joinedPath);
61
+ }
62
+ }
63
+ if (result.size !== 0) {
64
+ result.add("");
65
+ }
66
+ return result;
15
67
  }
16
68
  /**
17
- * Deep-walk an object and replace values whose key matches a sensitive pattern
69
+ * Deep-walk an object and replace string values at sensitive paths
18
70
  * with the redaction sentinel.
19
71
  */
20
- function redactObject(obj) {
21
- if (obj === null || obj === undefined) {
22
- return obj;
72
+ function redactObject(obj, hints) {
73
+ if (hints) {
74
+ const lookup = buildRedactionLookup(hints);
75
+ return lookup.has("")
76
+ ? redactObjectWithLookup(obj, lookup, "", [], hints)
77
+ : redactObjectGuessing(obj, "", [], hints);
23
78
  }
24
- if (typeof obj !== "object") {
25
- return obj;
26
- }
27
- if (Array.isArray(obj)) {
28
- return obj.map(redactObject);
79
+ else {
80
+ return redactObjectGuessing(obj, "", []);
29
81
  }
30
- const result = {};
31
- for (const [key, value] of Object.entries(obj)) {
32
- if (isSensitiveKey(key) && value !== null && value !== undefined) {
33
- result[key] = REDACTED_SENTINEL;
34
- }
35
- else if (typeof value === "object" && value !== null) {
36
- result[key] = redactObject(value);
82
+ }
83
+ /**
84
+ * Collect all sensitive string values from a config object.
85
+ * Used for text-based redaction of the raw JSON5 source.
86
+ */
87
+ function collectSensitiveValues(obj, hints) {
88
+ const result = [];
89
+ if (hints) {
90
+ const lookup = buildRedactionLookup(hints);
91
+ if (lookup.has("")) {
92
+ redactObjectWithLookup(obj, lookup, "", result, hints);
37
93
  }
38
94
  else {
39
- result[key] = value;
95
+ redactObjectGuessing(obj, "", result, hints);
40
96
  }
41
97
  }
98
+ else {
99
+ redactObjectGuessing(obj, "", result);
100
+ }
42
101
  return result;
43
102
  }
44
- export function redactConfigObject(value) {
45
- return redactObject(value);
46
- }
47
103
  /**
48
- * Collect all sensitive string values from a config object.
49
- * Used for text-based redaction of the raw JSON5 source.
104
+ * Worker for redactObject() and collectSensitiveValues().
105
+ * Used when there are ConfigUiHints available.
50
106
  */
51
- function collectSensitiveValues(obj) {
52
- const values = [];
53
- if (obj === null || obj === undefined || typeof obj !== "object") {
54
- return values;
107
+ function redactObjectWithLookup(obj, lookup, prefix, values, hints) {
108
+ if (obj === null || obj === undefined) {
109
+ return obj;
55
110
  }
56
111
  if (Array.isArray(obj)) {
57
- for (const item of obj) {
58
- values.push(...collectSensitiveValues(item));
112
+ const path = `${prefix}[]`;
113
+ if (!lookup.has(path)) {
114
+ if (!isExtensionPath(prefix)) {
115
+ return obj;
116
+ }
117
+ return redactObjectGuessing(obj, prefix, values, hints);
59
118
  }
60
- return values;
119
+ return obj.map((item) => {
120
+ if (typeof item === "string" && !isEnvVarPlaceholder(item)) {
121
+ values.push(item);
122
+ return REDACTED_SENTINEL;
123
+ }
124
+ return redactObjectWithLookup(item, lookup, path, values, hints);
125
+ });
61
126
  }
62
- for (const [key, value] of Object.entries(obj)) {
63
- if (isSensitiveKey(key) && typeof value === "string" && value.length > 0) {
64
- values.push(value);
127
+ if (typeof obj === "object") {
128
+ const result = {};
129
+ for (const [key, value] of Object.entries(obj)) {
130
+ const path = prefix ? `${prefix}.${key}` : key;
131
+ const wildcardPath = prefix ? `${prefix}.*` : "*";
132
+ let matched = false;
133
+ for (const candidate of [path, wildcardPath]) {
134
+ result[key] = value;
135
+ if (lookup.has(candidate)) {
136
+ matched = true;
137
+ // Hey, greptile, look here, this **IS** only applied to strings
138
+ if (typeof value === "string" && !isEnvVarPlaceholder(value)) {
139
+ result[key] = REDACTED_SENTINEL;
140
+ values.push(value);
141
+ }
142
+ else if (typeof value === "object" && value !== null) {
143
+ result[key] = redactObjectWithLookup(value, lookup, candidate, values, hints);
144
+ }
145
+ break;
146
+ }
147
+ }
148
+ if (!matched && isExtensionPath(path)) {
149
+ const markedNonSensitive = isExplicitlyNonSensitivePath(hints, [path, wildcardPath]);
150
+ if (typeof value === "string" &&
151
+ !markedNonSensitive &&
152
+ isSensitivePath(path) &&
153
+ !isEnvVarPlaceholder(value)) {
154
+ result[key] = REDACTED_SENTINEL;
155
+ values.push(value);
156
+ }
157
+ else if (typeof value === "object" && value !== null) {
158
+ result[key] = redactObjectGuessing(value, path, values, hints);
159
+ }
160
+ }
65
161
  }
66
- else if (typeof value === "object" && value !== null) {
67
- values.push(...collectSensitiveValues(value));
162
+ return result;
163
+ }
164
+ return obj;
165
+ }
166
+ /**
167
+ * Worker for redactObject() and collectSensitiveValues().
168
+ * Used when ConfigUiHints are NOT available.
169
+ */
170
+ function redactObjectGuessing(obj, prefix, values, hints) {
171
+ if (obj === null || obj === undefined) {
172
+ return obj;
173
+ }
174
+ if (Array.isArray(obj)) {
175
+ return obj.map((item) => {
176
+ const path = `${prefix}[]`;
177
+ if (!isExplicitlyNonSensitivePath(hints, [path]) &&
178
+ isSensitivePath(path) &&
179
+ typeof item === "string" &&
180
+ !isEnvVarPlaceholder(item)) {
181
+ values.push(item);
182
+ return REDACTED_SENTINEL;
183
+ }
184
+ return redactObjectGuessing(item, path, values, hints);
185
+ });
186
+ }
187
+ if (typeof obj === "object") {
188
+ const result = {};
189
+ for (const [key, value] of Object.entries(obj)) {
190
+ const dotPath = prefix ? `${prefix}.${key}` : key;
191
+ const wildcardPath = prefix ? `${prefix}.*` : "*";
192
+ if (!isExplicitlyNonSensitivePath(hints, [dotPath, wildcardPath]) &&
193
+ isSensitivePath(dotPath) &&
194
+ typeof value === "string" &&
195
+ !isEnvVarPlaceholder(value)) {
196
+ result[key] = REDACTED_SENTINEL;
197
+ values.push(value);
198
+ }
199
+ else if (typeof value === "object" && value !== null) {
200
+ result[key] = redactObjectGuessing(value, dotPath, values, hints);
201
+ }
202
+ else {
203
+ result[key] = value;
204
+ }
68
205
  }
206
+ return result;
69
207
  }
70
- return values;
208
+ return obj;
71
209
  }
72
210
  /**
73
211
  * Replace known sensitive values in a raw JSON5 string with the sentinel.
74
212
  * Values are replaced longest-first to avoid partial matches.
75
213
  */
76
- function redactRawText(raw, config) {
77
- const sensitiveValues = collectSensitiveValues(config);
214
+ function redactRawText(raw, config, hints) {
215
+ const sensitiveValues = collectSensitiveValues(config, hints);
78
216
  sensitiveValues.sort((a, b) => b.length - a.length);
79
217
  let result = raw;
80
218
  for (const value of sensitiveValues) {
81
- const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82
- result = result.replace(new RegExp(escaped, "g"), REDACTED_SENTINEL);
219
+ result = result.replaceAll(value, REDACTED_SENTINEL);
83
220
  }
84
- const keyValuePattern = /(^|[{\s,])((["'])([^"']+)\3|([A-Za-z0-9_$.-]+))(\s*:\s*)(["'])([^"']*)\7/g;
85
- result = result.replace(keyValuePattern, (match, prefix, keyExpr, _keyQuote, keyQuoted, keyBare, sep, valQuote, val) => {
86
- const key = (keyQuoted ?? keyBare);
87
- if (!key || !isSensitiveKey(key)) {
88
- return match;
89
- }
90
- if (val === REDACTED_SENTINEL) {
91
- return match;
92
- }
93
- return `${prefix}${keyExpr}${sep}${valQuote}${REDACTED_SENTINEL}${valQuote}`;
94
- });
95
221
  return result;
96
222
  }
97
223
  /**
@@ -101,49 +227,251 @@ function redactRawText(raw, config) {
101
227
  *
102
228
  * Both `config` (the parsed object) and `raw` (the JSON5 source) are scrubbed
103
229
  * so no credential can leak through either path.
230
+ *
231
+ * When `uiHints` are provided, sensitivity is determined from the schema hints.
232
+ * Without hints, falls back to regex-based detection via `isSensitivePath()`.
104
233
  */
105
- export function redactConfigSnapshot(snapshot) {
106
- const redactedConfig = redactConfigObject(snapshot.config);
107
- const redactedRaw = snapshot.raw ? redactRawText(snapshot.raw, snapshot.config) : null;
108
- const redactedParsed = snapshot.parsed ? redactConfigObject(snapshot.parsed) : snapshot.parsed;
234
+ /**
235
+ * Redact sensitive fields from a plain config object (not a full snapshot).
236
+ * Used by write endpoints (config.set, config.patch, config.apply) to avoid
237
+ * leaking credentials in their responses.
238
+ */
239
+ export function redactConfigObject(value, uiHints) {
240
+ return redactObject(value, uiHints);
241
+ }
242
+ export function redactConfigSnapshot(snapshot, uiHints) {
243
+ if (!snapshot.valid) {
244
+ // This is bad. We could try to redact the raw string using known key names,
245
+ // but then we would not be able to restore them, and would trash the user's
246
+ // credentials. Less than ideal---we should never delete important data.
247
+ // On the other hand, we cannot hand out "raw" if we're not sure we have
248
+ // properly redacted all sensitive data. Handing out a partially or, worse,
249
+ // unredacted config string would be bad.
250
+ // Therefore, the only safe route is to reject handling out broken configs.
251
+ return {
252
+ ...snapshot,
253
+ config: {},
254
+ raw: null,
255
+ parsed: null,
256
+ resolved: {},
257
+ };
258
+ }
259
+ // else: snapshot.config must be valid and populated, as that is what
260
+ // readConfigFileSnapshot() does when it creates the snapshot.
261
+ const redactedConfig = redactObject(snapshot.config, uiHints);
262
+ const redactedRaw = snapshot.raw ? redactRawText(snapshot.raw, snapshot.config, uiHints) : null;
263
+ const redactedParsed = snapshot.parsed ? redactObject(snapshot.parsed, uiHints) : snapshot.parsed;
264
+ // Also redact the resolved config (contains values after ${ENV} substitution)
265
+ const redactedResolved = redactConfigObject(snapshot.resolved, uiHints);
109
266
  return {
110
267
  ...snapshot,
111
268
  config: redactedConfig,
112
269
  raw: redactedRaw,
113
270
  parsed: redactedParsed,
271
+ resolved: redactedResolved,
114
272
  };
115
273
  }
116
274
  /**
117
275
  * Deep-walk `incoming` and replace any {@link REDACTED_SENTINEL} values
118
- * (on sensitive keys) with the corresponding value from `original`.
276
+ * (on sensitive paths) with the corresponding value from `original`.
119
277
  *
120
278
  * This is called by config.set / config.apply / config.patch before writing,
121
279
  * so that credentials survive a Web UI round-trip unmodified.
122
280
  */
123
- export function restoreRedactedValues(incoming, original) {
281
+ export function restoreRedactedValues(incoming, original, hints) {
124
282
  if (incoming === null || incoming === undefined) {
125
- return incoming;
283
+ return { ok: false, error: "no input" };
126
284
  }
127
285
  if (typeof incoming !== "object") {
286
+ return { ok: false, error: "input not an object" };
287
+ }
288
+ try {
289
+ if (hints) {
290
+ const lookup = buildRedactionLookup(hints);
291
+ if (lookup.has("")) {
292
+ return {
293
+ ok: true,
294
+ result: restoreRedactedValuesWithLookup(incoming, original, lookup, "", hints),
295
+ };
296
+ }
297
+ else {
298
+ return { ok: true, result: restoreRedactedValuesGuessing(incoming, original, "", hints) };
299
+ }
300
+ }
301
+ else {
302
+ return { ok: true, result: restoreRedactedValuesGuessing(incoming, original, "") };
303
+ }
304
+ }
305
+ catch (err) {
306
+ if (err instanceof RedactionError) {
307
+ return {
308
+ ok: false,
309
+ humanReadableMessage: `Sentinel value "${REDACTED_SENTINEL}" in key ${err.key} is not valid as real data`,
310
+ };
311
+ }
312
+ throw err; // some coding error, pass through
313
+ }
314
+ }
315
+ class RedactionError extends Error {
316
+ key;
317
+ constructor(key) {
318
+ super("internal error class---should never escape");
319
+ this.key = key;
320
+ this.name = "RedactionError";
321
+ }
322
+ }
323
+ function restoreOriginalValueOrThrow(params) {
324
+ if (params.key in params.original) {
325
+ return params.original[params.key];
326
+ }
327
+ log.warn(`Cannot un-redact config key ${params.path} as it doesn't have any value`);
328
+ throw new RedactionError(params.path);
329
+ }
330
+ function mapRedactedArray(params) {
331
+ const originalArray = Array.isArray(params.original) ? params.original : [];
332
+ if (params.incoming.length < originalArray.length) {
333
+ log.warn(`Redacted config array key ${params.path} has been truncated`);
334
+ }
335
+ return params.incoming.map((item, index) => params.mapItem(item, index, originalArray));
336
+ }
337
+ function toObjectRecord(value) {
338
+ if (value && typeof value === "object" && !Array.isArray(value)) {
339
+ return value;
340
+ }
341
+ return {};
342
+ }
343
+ function shouldPassThroughRestoreValue(incoming) {
344
+ return incoming === null || incoming === undefined || typeof incoming !== "object";
345
+ }
346
+ function toRestoreArrayContext(incoming, prefix) {
347
+ if (!Array.isArray(incoming)) {
348
+ return null;
349
+ }
350
+ return { incoming, path: `${prefix}[]` };
351
+ }
352
+ function restoreArrayItemWithLookup(params) {
353
+ if (params.item === REDACTED_SENTINEL) {
354
+ return params.originalArray[params.index];
355
+ }
356
+ return restoreRedactedValuesWithLookup(params.item, params.originalArray[params.index], params.lookup, params.path, params.hints);
357
+ }
358
+ function restoreArrayItemWithGuessing(params) {
359
+ if (!isExplicitlyNonSensitivePath(params.hints, [params.path]) &&
360
+ isSensitivePath(params.path) &&
361
+ params.item === REDACTED_SENTINEL) {
362
+ return params.originalArray[params.index];
363
+ }
364
+ return restoreRedactedValuesGuessing(params.item, params.originalArray[params.index], params.path, params.hints);
365
+ }
366
+ function restoreGuessingArray(incoming, original, path, hints) {
367
+ return mapRedactedArray({
368
+ incoming,
369
+ original,
370
+ path,
371
+ mapItem: (item, index, originalArray) => restoreArrayItemWithGuessing({
372
+ item,
373
+ index,
374
+ originalArray,
375
+ path,
376
+ hints,
377
+ }),
378
+ });
379
+ }
380
+ /**
381
+ * Worker for restoreRedactedValues().
382
+ * Used when there are ConfigUiHints available.
383
+ */
384
+ function restoreRedactedValuesWithLookup(incoming, original, lookup, prefix, hints) {
385
+ if (shouldPassThroughRestoreValue(incoming)) {
128
386
  return incoming;
129
387
  }
130
- if (Array.isArray(incoming)) {
131
- const origArr = Array.isArray(original) ? original : [];
132
- return incoming.map((item, i) => restoreRedactedValues(item, origArr[i]));
388
+ const arrayContext = toRestoreArrayContext(incoming, prefix);
389
+ if (arrayContext) {
390
+ // Note: If the user removed an item in the middle of the array,
391
+ // we have no way of knowing which one. In this case, the last
392
+ // element(s) get(s) chopped off. Not good, so please don't put
393
+ // sensitive string array in the config...
394
+ const { incoming: incomingArray, path } = arrayContext;
395
+ if (!lookup.has(path)) {
396
+ if (!isExtensionPath(prefix)) {
397
+ return incomingArray;
398
+ }
399
+ return restoreRedactedValuesGuessing(incomingArray, original, prefix, hints);
400
+ }
401
+ return mapRedactedArray({
402
+ incoming: incomingArray,
403
+ original,
404
+ path,
405
+ mapItem: (item, index, originalArray) => restoreArrayItemWithLookup({
406
+ item,
407
+ index,
408
+ originalArray,
409
+ lookup,
410
+ path,
411
+ hints,
412
+ }),
413
+ });
133
414
  }
134
- const orig = original && typeof original === "object" && !Array.isArray(original)
135
- ? original
136
- : {};
415
+ const orig = toObjectRecord(original);
137
416
  const result = {};
138
417
  for (const [key, value] of Object.entries(incoming)) {
139
- if (isSensitiveKey(key) && value === REDACTED_SENTINEL) {
140
- if (!(key in orig)) {
141
- throw new Error(`config write rejected: "${key}" is redacted; set an explicit value instead of ${REDACTED_SENTINEL}`);
418
+ result[key] = value;
419
+ const path = prefix ? `${prefix}.${key}` : key;
420
+ const wildcardPath = prefix ? `${prefix}.*` : "*";
421
+ let matched = false;
422
+ for (const candidate of [path, wildcardPath]) {
423
+ if (lookup.has(candidate)) {
424
+ matched = true;
425
+ if (value === REDACTED_SENTINEL) {
426
+ result[key] = restoreOriginalValueOrThrow({ key, path: candidate, original: orig });
427
+ }
428
+ else if (typeof value === "object" && value !== null) {
429
+ result[key] = restoreRedactedValuesWithLookup(value, orig[key], lookup, candidate, hints);
430
+ }
431
+ break;
142
432
  }
143
- result[key] = orig[key];
433
+ }
434
+ if (!matched && isExtensionPath(path)) {
435
+ const markedNonSensitive = isExplicitlyNonSensitivePath(hints, [path, wildcardPath]);
436
+ if (!markedNonSensitive && isSensitivePath(path) && value === REDACTED_SENTINEL) {
437
+ result[key] = restoreOriginalValueOrThrow({ key, path, original: orig });
438
+ }
439
+ else if (typeof value === "object" && value !== null) {
440
+ result[key] = restoreRedactedValuesGuessing(value, orig[key], path, hints);
441
+ }
442
+ }
443
+ }
444
+ return result;
445
+ }
446
+ /**
447
+ * Worker for restoreRedactedValues().
448
+ * Used when ConfigUiHints are NOT available.
449
+ */
450
+ function restoreRedactedValuesGuessing(incoming, original, prefix, hints) {
451
+ if (shouldPassThroughRestoreValue(incoming)) {
452
+ return incoming;
453
+ }
454
+ const arrayContext = toRestoreArrayContext(incoming, prefix);
455
+ if (arrayContext) {
456
+ // Note: If the user removed an item in the middle of the array,
457
+ // we have no way of knowing which one. In this case, the last
458
+ // element(s) get(s) chopped off. Not good, so please don't put
459
+ // sensitive string array in the config...
460
+ const { incoming: incomingArray, path } = arrayContext;
461
+ return restoreGuessingArray(incomingArray, original, path, hints);
462
+ }
463
+ const orig = toObjectRecord(original);
464
+ const result = {};
465
+ for (const [key, value] of Object.entries(incoming)) {
466
+ const path = prefix ? `${prefix}.${key}` : key;
467
+ const wildcardPath = prefix ? `${prefix}.*` : "*";
468
+ if (!isExplicitlyNonSensitivePath(hints, [path, wildcardPath]) &&
469
+ isSensitivePath(path) &&
470
+ value === REDACTED_SENTINEL) {
471
+ result[key] = restoreOriginalValueOrThrow({ key, path, original: orig });
144
472
  }
145
473
  else if (typeof value === "object" && value !== null) {
146
- result[key] = restoreRedactedValues(value, orig[key]);
474
+ result[key] = restoreRedactedValuesGuessing(value, orig[key], path, hints);
147
475
  }
148
476
  else {
149
477
  result[key] = value;