@crewhaus/tool-secure 0.7.0 → 0.8.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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ CrewHaus Factory
2
+ Copyright 2026 Max Meier
3
+
4
+ This product includes software developed by Max Meier
5
+ (operating as StudioMax — https://studiomax.io) and contributors.
6
+
7
+ The CrewHaus name and logo are trademarks of Max Meier. See
8
+ https://github.com/crewhaus/factory/blob/main/TRADEMARK.md for the
9
+ use-of-marks policy.
package/README.md CHANGED
@@ -32,7 +32,7 @@ exists, or that it belongs to anyone in particular.
32
32
  ```yaml
33
33
  tools:
34
34
  - all-secure # every tool below
35
- - -Depseudonymize # ...except this one
35
+ - -depseudonymize # ...except this one
36
36
  ```
37
37
 
38
38
  | Tool | What it does |
@@ -180,6 +180,23 @@ variable — and reads it itself. The key never appears in an argument, a
180
180
  result, an error message or a transcript. An unset variable is refused by
181
181
  name, loudly.
182
182
 
183
+ **Only a variable the operator listed can be a key.** Name them in the spec:
184
+
185
+ ```yaml
186
+ tool_config:
187
+ secure:
188
+ key_env_vars: [AUDIT_SIGNING_KEY, EXPORT_PSEUDONYM_KEY]
189
+ ```
190
+
191
+ A call may pick among the listed names and never add one. Before 0.7.1 any
192
+ variable could key these tools, and they are read-only, so plan and auto mode
193
+ ran them unasked: a signature over a known payload under `DB_PASSWORD` or
194
+ `SLACK_SIGNING_SECRET` is an offline oracle for the secret and a forgery for
195
+ whatever trusts it. An unlisted name is refused with the same message whether
196
+ or not it is set, so the refusal does not reveal which variables exist. The
197
+ block can also sit under a keyed tool's own key (`tool_config.signPayload`),
198
+ and a model-pool candidate's block replaces it for that candidate's calls.
199
+
183
200
  Pseudonym tokens come in two flavours, and the difference is reported in
184
201
  every result that uses them. **Keyed** tokens are `HMAC-SHA256(key,
185
202
  "<type>:<canonical>")` truncated: stable across documents, and unlinkable
package/dist/index.d.ts CHANGED
@@ -2,6 +2,22 @@ import type { RegisteredTool } from "@crewhaus/tool-catalog";
2
2
  import { PII_TYPES, SUPPORTED_PHONE_COUNTRIES } from "./lib/pii";
3
3
  import { SECRET_RULES } from "./lib/secrets";
4
4
  import { MAX_TEXT_CHARS } from "./lib/text";
5
+ /** The tool_config key an operator edits to allow a signing-key variable. */
6
+ export declare const KEY_ENV_VARS_CONFIG_KEY = "tool_config.secure.key_env_vars";
7
+ /** What `tool_config.secure` (or a keyed tool's own block) may set. */
8
+ export type SecureConfig = {
9
+ /** The ONLY environment variables the keyed tools may read a key from. */
10
+ readonly key_env_vars?: ReadonlyArray<string>;
11
+ /** The same list, camelCase. Setting both spellings is refused. */
12
+ readonly keyEnvVars?: ReadonlyArray<string>;
13
+ };
14
+ /**
15
+ * Boot seam: the `tool_config.secure` block (or SignPayload's, VerifyPayload's,
16
+ * Pseudonymize's, PiiRedact's or RedactForExport's own block). Its
17
+ * `key_env_vars` is the only set of environment variables those tools may
18
+ * read an HMAC key from (flag-truth-2#0).
19
+ */
20
+ export declare function registerSecureConfig(config: unknown): void;
5
21
  export declare const piiScan: RegisteredTool;
6
22
  export declare const piiRedact: RegisteredTool;
7
23
  export declare const pseudonymize: RegisteredTool;
package/dist/index.js CHANGED
@@ -25,13 +25,14 @@
25
25
  * instance of its format. They still do not prove the account exists.
26
26
  */
27
27
  import { buildTool } from "@crewhaus/tool-builder";
28
+ import { isEnvName, looksLikePastedSecret, resolveCredentialEnv } from "@crewhaus/tool-safety/env";
28
29
  import { z } from "zod";
29
30
  import { DEFAULT_ENTROPY_THRESHOLDS, MIN_HIGH_ENTROPY_LENGTH, classifyCharset, roundBits, shannonEntropy, } from "./lib/entropy";
30
31
  import { HASH_ALGORITHMS, MAX_CHAIN_RECORDS, SIGNATURE_ENCODINGS, sha256Hex, signPayload as signPayloadFn, verifyChain, verifyPayload as verifyPayloadFn, } from "./lib/evidence";
31
32
  import { DEFAULT_MAX_FILE_BYTES, DEFAULT_SKIP_DIRS, DEFAULT_WALK_LIMITS, MAX_FILE_BYTES_LIMIT, readTextBounded, relLabel, walkTextFiles, } from "./lib/files";
32
33
  import { scanInjection } from "./lib/injection";
33
34
  import { PII_TYPES, SUPPORTED_PHONE_COUNTRIES, canonicalPiiValue, scanPii, } from "./lib/pii";
34
- import { MAX_POLICY_RULES, POLICY_RULE_KINDS, evaluatePolicy } from "./lib/policy";
35
+ import { MAX_POLICY_RULES, POLICY_RULE_KINDS, evaluatePolicy, } from "./lib/policy";
35
36
  import { MAX_MAPPING_ENTRIES, TOKEN_SHAPE, applyMapping, derivePseudonym, digestPseudonym, invertMapping, placeholderFor, pseudonymPlaceholder, redactFindings, } from "./lib/redact";
36
37
  import { SECRET_RULES, rulesRunFor, scanSecrets, secretSpans, severityCounts, } from "./lib/secrets";
37
38
  import { MAX_TEXT_CHARS, assertTextSize, compareStrings, countByType, dedupeOverlaps, maskValue, } from "./lib/text";
@@ -74,25 +75,84 @@ function countBy(items, key) {
74
75
  return out;
75
76
  }
76
77
  const DETECTION_NOTE = "Heuristic. These rules found what they match; an empty or short result does not mean the input is clean. Check rulesRun to see what was actually looked for.";
78
+ /** The tool_config key an operator edits to allow a signing-key variable. */
79
+ export const KEY_ENV_VARS_CONFIG_KEY = "tool_config.secure.key_env_vars";
80
+ /** The operator's list from the boot block; empty until one is registered. */
81
+ let registeredKeyEnvVars = [];
77
82
  /**
78
- * Fetch a signing key by env-var name. The key itself never enters a result,
79
- * an argument or an error message — only the variable's name does.
83
+ * The key-variable list a `tool_config` block allows. Throws, naming where
84
+ * the block sits, when the list is not a list of variable names. An entry is
85
+ * never quoted back: one that is not a name is most often a pasted key.
80
86
  */
81
- function keyFromEnv(name) {
82
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
83
- return {
84
- ok: false,
85
- message: `"${name}" is not a valid environment variable name`,
86
- };
87
+ function keyEnvVarsOf(config, where) {
88
+ if (config === undefined || config === null)
89
+ return [];
90
+ if (typeof config !== "object" || Array.isArray(config)) {
91
+ throw new Error(`${where} must be a mapping, such as { key_env_vars: [AUDIT_SIGNING_KEY] }`);
87
92
  }
88
- const value = process.env[name];
89
- if (value === undefined || value === "") {
90
- return {
91
- ok: false,
92
- message: `environment variable ${name} is unset or empty — set it in the harness environment; the key is never passed as an argument, so it cannot end up in a transcript`,
93
- };
93
+ const block = config;
94
+ const snake = block["key_env_vars"];
95
+ const camel = block["keyEnvVars"];
96
+ if (snake !== undefined && camel !== undefined) {
97
+ throw new Error(`${where} sets both key_env_vars and keyEnvVars; keep one`);
94
98
  }
95
- return { ok: true, key: value };
99
+ const list = snake ?? camel;
100
+ if (list === undefined)
101
+ return [];
102
+ if (!Array.isArray(list)) {
103
+ throw new Error(`${where}.key_env_vars must be a list of environment variable names`);
104
+ }
105
+ return list.map((entry, i) => {
106
+ // A pasted token can be a legal name (`ghp_…`), and a listed name is
107
+ // repeated in every refusal, so one that looks like a key is refused too.
108
+ const pasted = typeof entry === "string" && looksLikePastedSecret(entry);
109
+ if (typeof entry !== "string" || !isEnvName(entry) || pasted) {
110
+ throw new Error(`${where}.key_env_vars[${i}] is not an environment variable name${pasted ? "; it looks like a key itself, which has not been echoed back — list the NAME of the variable that holds it, and treat the value as exposed" : ""}`);
111
+ }
112
+ return entry;
113
+ });
114
+ }
115
+ /**
116
+ * Boot seam: the `tool_config.secure` block (or SignPayload's, VerifyPayload's,
117
+ * Pseudonymize's, PiiRedact's or RedactForExport's own block). Its
118
+ * `key_env_vars` is the only set of environment variables those tools may
119
+ * read an HMAC key from (flag-truth-2#0).
120
+ */
121
+ export function registerSecureConfig(config) {
122
+ registeredKeyEnvVars = keyEnvVarsOf(config, "tool_config.secure");
123
+ }
124
+ /**
125
+ * Fetch a signing key by env-var name — only a name the operator listed.
126
+ *
127
+ * 0.7.0 read ANY variable the call named, so these read-only, auto-allowed
128
+ * tools were an HMAC oracle over every secret in the process: a signature
129
+ * under DB_PASSWORD over a known payload is checkable offline, which recovers
130
+ * a weak one by dictionary, and forges a webhook or token for any service
131
+ * keyed by an env secret (flag-truth-2#0). The model may pick among the
132
+ * listed names and never add one; an unlisted name gets the same refusal
133
+ * whether or not it is set. The key never enters a result, an argument or an
134
+ * error — only the variable's name does.
135
+ *
136
+ * A model-pool candidate's own block (`ctx.toolConfig`) replaces the boot
137
+ * block for its calls, as every tool_config block does.
138
+ */
139
+ function keyFromEnv(tool, name, ctx) {
140
+ let allowed;
141
+ try {
142
+ allowed =
143
+ typeof ctx?.toolConfig === "object" && ctx.toolConfig !== null
144
+ ? keyEnvVarsOf(ctx.toolConfig, `the tool_config block for ${tool}`)
145
+ : registeredKeyEnvVars;
146
+ }
147
+ catch (err) {
148
+ return { ok: false, message: err.message };
149
+ }
150
+ const found = resolveCredentialEnv(name, {
151
+ allowed,
152
+ purpose: `${tool}'s HMAC key`,
153
+ configKey: KEY_ENV_VARS_CONFIG_KEY,
154
+ });
155
+ return found.ok ? { ok: true, key: found.value } : { ok: false, message: found.reason };
96
156
  }
97
157
  const piiTypeSchema = z.enum(PII_TYPES);
98
158
  const countrySchema = z.enum(SUPPORTED_PHONE_COUNTRIES);
@@ -171,7 +231,7 @@ export const piiScan = buildTool({
171
231
  });
172
232
  export const piiRedact = buildTool({
173
233
  name: "PiiRedact",
174
- description: "Replace detected personal data with a stable token — either a plain type placeholder or an HMAC pseudonym keyed from a named environment variable, so the same value redacts identically across documents without being reversible. Use it when a document has to leave the system but the records still need to line up afterwards; it returns the redacted text plus counts by type, never the values it removed.",
234
+ description: "Replace detected personal data with a stable token — either a plain type placeholder or an HMAC pseudonym keyed from a named environment variable the operator allowed, so the same value redacts identically across documents without being reversible. Use it when a document has to leave the system but the records still need to line up afterwards; it returns the redacted text plus counts by type, never the values it removed.",
175
235
  inputSchema: z.object({
176
236
  text: z.string().describe("the text to redact"),
177
237
  mode: z
@@ -181,7 +241,7 @@ export const piiRedact = buildTool({
181
241
  keyEnvVar: z
182
242
  .string()
183
243
  .optional()
184
- .describe("name of the env var holding the HMAC key; required for pseudonym mode"),
244
+ .describe("NAME of the environment variable holding the HMAC key, one the operator listed in tool_config.secure.key_env_vars; required for pseudonym mode"),
185
245
  tokenLength: z
186
246
  .number()
187
247
  .int()
@@ -193,7 +253,7 @@ export const piiRedact = buildTool({
193
253
  }),
194
254
  readOnly: true,
195
255
  concurrencySafe: true,
196
- execute: async (input) => {
256
+ execute: async (input, ctx) => {
197
257
  try {
198
258
  assertTextSize(input.text, "text");
199
259
  const mode = input.mode ?? "placeholder";
@@ -202,7 +262,7 @@ export const piiRedact = buildTool({
202
262
  if (input.keyEnvVar === undefined) {
203
263
  return "PiiRedact: pseudonym mode needs keyEnvVar, the NAME of an environment variable holding the HMAC key. Without a key the tokens would be plain hashes, which an attacker can reverse by hashing every candidate value.";
204
264
  }
205
- const found = keyFromEnv(input.keyEnvVar);
265
+ const found = keyFromEnv("PiiRedact", input.keyEnvVar, ctx);
206
266
  if (!found.ok)
207
267
  return `PiiRedact: ${found.message}`;
208
268
  key = found.key;
@@ -248,7 +308,7 @@ export const pseudonymize = buildTool({
248
308
  keyEnvVar: z
249
309
  .string()
250
310
  .optional()
251
- .describe("env var holding an HMAC key; without it, tokens are plain hashes and reversible by enumeration"),
311
+ .describe("NAME of the environment variable holding an HMAC key, one the operator listed in tool_config.secure.key_env_vars; without it, tokens are plain hashes and reversible by enumeration"),
252
312
  tokenLength: z.number().int().min(4).max(64).optional(),
253
313
  })
254
314
  .optional()
@@ -256,7 +316,7 @@ export const pseudonymize = buildTool({
256
316
  }),
257
317
  readOnly: true,
258
318
  concurrencySafe: true,
259
- execute: async (input) => {
319
+ execute: async (input, ctx) => {
260
320
  try {
261
321
  assertTextSize(input.text, "text");
262
322
  const mapping = { ...input.mapping };
@@ -265,7 +325,7 @@ export const pseudonymize = buildTool({
265
325
  const tokenLength = input.mint.tokenLength ?? 12;
266
326
  let key;
267
327
  if (input.mint.keyEnvVar !== undefined) {
268
- const found = keyFromEnv(input.mint.keyEnvVar);
328
+ const found = keyFromEnv("Pseudonymize", input.mint.keyEnvVar, ctx);
269
329
  if (!found.ok)
270
330
  return `Pseudonymize: ${found.message}`;
271
331
  key = found.key;
@@ -361,6 +421,8 @@ const secretOptionsShape = {
361
421
  };
362
422
  export const secretScan = buildTool({
363
423
  name: "SecretScan",
424
+ // The file or directory it scans (absent when it scans `text`).
425
+ operativeArgs: [{ field: "path", kind: "path", beneath: "visible", hiddenWhen: "includeHidden" }],
364
426
  description: "Look for credentials in a string, a file, or a directory tree — vendor key shapes, PEM private-key blocks, JWTs, URLs carrying a password, and high-entropy strings near credential-shaped names. Use it before publishing a repo or pasting a log somewhere; every finding reports the rule, the location and a MASKED sample, and the secret itself is never included in the result.",
365
427
  inputSchema: z.object({
366
428
  text: z.string().optional().describe("text to scan; provide this or path, not both"),
@@ -718,6 +780,14 @@ export const allowlistCheck = buildTool({
718
780
  }
719
781
  },
720
782
  });
783
+ /** The ctx fields a tool's `execute` receives that a pattern run uses. */
784
+ function policyRunContext(ctx) {
785
+ const c = ctx;
786
+ return {
787
+ ...(c?.signal === undefined ? {} : { signal: c.signal }),
788
+ ...(typeof c?.runContext?.sessionId === "string" ? { runawayKey: c.runContext.sessionId } : {}),
789
+ };
790
+ }
721
791
  export const contentPolicyCheck = buildTool({
722
792
  name: "ContentPolicyCheck",
723
793
  description: "Evaluate text against operator-written rules — a required disclaimer being present, a forbidden phrase being absent, a claim pattern that needs a human look — returning a pass, fail or review verdict per rule with locations. Use it as the mechanical half of a review so a model is only asked about the part that needs judgement; a rule whose regex does not compile is reported as an error for that rule alone, never as a silent pass.",
@@ -739,15 +809,15 @@ export const contentPolicyCheck = buildTool({
739
809
  }),
740
810
  readOnly: true,
741
811
  concurrencySafe: true,
742
- execute: async (input) => {
812
+ execute: async (input, ctx) => {
743
813
  try {
744
814
  assertTextSize(input.text, "text");
745
- const result = evaluatePolicy(input.text, input.rules);
815
+ const result = await evaluatePolicy(input.text, input.rules, policyRunContext(ctx));
746
816
  return json({
747
817
  pass: result.pass,
748
818
  counts: result.counts,
749
819
  outcomes: result.outcomes,
750
- note: "Mechanical only. A required phrase can be present and still wrong, and a forbidden claim can be made in other words — review_pattern rules exist for exactly that, and a review outcome does not fail the check. A rule whose pattern does not compile is reported as error and DOES fail the check, because an unevaluated rule is not a passed one. Matching is case-insensitive unless caseSensitive is set.",
820
+ note: "Mechanical only. A required phrase can be present and still wrong, and a forbidden claim can be made in other words — review_pattern rules exist for exactly that, and a review outcome does not fail the check. A rule whose pattern does not compile, is refused as one that could run away, or could not be run to the end of the text is reported as error and DOES fail the check, because an unevaluated rule is not a passed one. Matching is case-insensitive unless caseSensitive is set.",
751
821
  });
752
822
  }
753
823
  catch (err) {
@@ -806,19 +876,22 @@ export const hashChainVerify = buildTool({
806
876
  });
807
877
  export const signPayload = buildTool({
808
878
  name: "SignPayload",
809
- description: "Produce an HMAC over a payload using a key read from a named environment variable. Use it to stamp a record so a later reader can tell it was not altered; the key is named, never passed, so it cannot end up in a transcript, and it never appears in the result.",
879
+ description: "Produce an HMAC over a payload using a key read from a named environment variable, one the operator listed in tool_config.secure.key_env_vars (no other variable is read). Use it to stamp a record so a later reader can tell it was not altered; the key is named, never passed, so it cannot end up in a transcript, and it never appears in the result.",
810
880
  inputSchema: z.object({
811
881
  payload: z.string().describe("exactly the bytes to sign; canonicalize before calling"),
812
- keyEnvVar: z.string().min(1).describe("NAME of the environment variable holding the key"),
882
+ keyEnvVar: z
883
+ .string()
884
+ .min(1)
885
+ .describe("NAME of the environment variable holding the key, one the operator listed in tool_config.secure.key_env_vars"),
813
886
  algorithm: algorithmSchema.optional().describe("defaults to sha256"),
814
887
  encoding: encodingSchema.optional().describe("hex or base64url; defaults to hex"),
815
888
  }),
816
889
  readOnly: true,
817
890
  concurrencySafe: true,
818
- execute: async (input) => {
891
+ execute: async (input, ctx) => {
819
892
  try {
820
893
  assertTextSize(input.payload, "payload");
821
- const found = keyFromEnv(input.keyEnvVar);
894
+ const found = keyFromEnv("SignPayload", input.keyEnvVar, ctx);
822
895
  if (!found.ok)
823
896
  return `SignPayload: ${found.message}`;
824
897
  const algorithm = (input.algorithm ?? "sha256");
@@ -840,20 +913,23 @@ export const signPayload = buildTool({
840
913
  });
841
914
  export const verifyPayload = buildTool({
842
915
  name: "VerifyPayload",
843
- description: "Check an HMAC against a payload in constant time, with the key read from a named environment variable. Use it before trusting a record that claims to be unaltered; the comparison is a double HMAC, so a wrong-length or malformed signature returns false rather than leaking timing.",
916
+ description: "Check an HMAC against a payload in constant time, with the key read from a named environment variable, one the operator listed in tool_config.secure.key_env_vars (no other variable is read). Use it before trusting a record that claims to be unaltered; the comparison is a double HMAC, so a wrong-length or malformed signature returns false rather than leaking timing.",
844
917
  inputSchema: z.object({
845
918
  payload: z.string().describe("the bytes that were signed"),
846
919
  signature: z.string().min(1).describe("the signature to check"),
847
- keyEnvVar: z.string().min(1).describe("NAME of the environment variable holding the key"),
920
+ keyEnvVar: z
921
+ .string()
922
+ .min(1)
923
+ .describe("NAME of the environment variable holding the key, one the operator listed in tool_config.secure.key_env_vars"),
848
924
  algorithm: algorithmSchema.optional().describe("defaults to sha256"),
849
925
  encoding: encodingSchema.optional().describe("hex or base64url; defaults to hex"),
850
926
  }),
851
927
  readOnly: true,
852
928
  concurrencySafe: true,
853
- execute: async (input) => {
929
+ execute: async (input, ctx) => {
854
930
  try {
855
931
  assertTextSize(input.payload, "payload");
856
- const found = keyFromEnv(input.keyEnvVar);
932
+ const found = keyFromEnv("VerifyPayload", input.keyEnvVar, ctx);
857
933
  if (!found.ok)
858
934
  return `VerifyPayload: ${found.message}`;
859
935
  const algorithm = (input.algorithm ?? "sha256");
@@ -889,14 +965,14 @@ export const redactForExport = buildTool({
889
965
  keyEnvVar: z
890
966
  .string()
891
967
  .optional()
892
- .describe("env var with the HMAC key; required for pseudonym mode"),
968
+ .describe("NAME of the environment variable holding the HMAC key, one the operator listed in tool_config.secure.key_env_vars; required for pseudonym mode"),
893
969
  tokenLength: z.number().int().min(4).max(64).optional(),
894
970
  ...piiSelectionShape,
895
971
  ...secretOptionsShape,
896
972
  }),
897
973
  readOnly: true,
898
974
  concurrencySafe: true,
899
- execute: async (input) => {
975
+ execute: async (input, ctx) => {
900
976
  try {
901
977
  if ((input.text === undefined) === (input.path === undefined)) {
902
978
  return "RedactForExport needs exactly one of text or path.";
@@ -923,7 +999,7 @@ export const redactForExport = buildTool({
923
999
  if (input.keyEnvVar === undefined) {
924
1000
  return "RedactForExport: pseudonym mode needs keyEnvVar, the NAME of an environment variable holding the HMAC key.";
925
1001
  }
926
- const found = keyFromEnv(input.keyEnvVar);
1002
+ const found = keyFromEnv("RedactForExport", input.keyEnvVar, ctx);
927
1003
  if (!found.ok)
928
1004
  return `RedactForExport: ${found.message}`;
929
1005
  key = found.key;
@@ -5,9 +5,40 @@ export type InjectionRule = {
5
5
  readonly weight: number;
6
6
  readonly description: string;
7
7
  readonly pattern: RegExp;
8
+ /**
9
+ * The pattern's matches when a plain global search would rescan a run;
10
+ * the same matches as `matchAll(text, pattern)`.
11
+ */
12
+ readonly matches?: (text: string) => Iterable<{
13
+ index: number;
14
+ match: RegExpExecArray;
15
+ }>;
8
16
  };
9
17
  /** The phrase rules. Also run against decoded base64, once, no recursion. */
10
18
  export declare const INJECTION_RULES: ReadonlyArray<InjectionRule>;
19
+ /**
20
+ * HTML_COMMENT's matches. From each `<!--` it reads up to 4,000 characters
21
+ * for a `-->`, so a text of `<!--` with none read 4,000 characters again
22
+ * from every one: thousands of steps a character, seconds over 2M. A try is
23
+ * made only where the first `-->` after the opener lies within reach; that
24
+ * `-->` only moves forward, so it is searched for once.
25
+ */
26
+ export declare function htmlCommentMatches(text: string): Iterable<{
27
+ index: number;
28
+ match: RegExpExecArray;
29
+ }>;
30
+ /**
31
+ * MARKDOWN_LINK's matches. From each `[` it reads up to 200 label and 500
32
+ * target characters, so a run of `[` read 200 again from every one. A try
33
+ * is made only where it matches: the label runs from the `[` to the first
34
+ * `]` or `\n` (1 to 200 characters, ending at `]`), `(` follows, and the
35
+ * target runs to the first `)` or blank (1 to 500, ending at `)`). Both ends
36
+ * only move forward, so each is found once.
37
+ */
38
+ export declare function markdownLinkMatches(text: string): Iterable<{
39
+ index: number;
40
+ match: RegExpExecArray;
41
+ }>;
11
42
  /** Decoded blobs are bounded so one base64 wall cannot blow up memory. */
12
43
  export declare const MAX_BASE64_CANDIDATE_CHARS = 100000;
13
44
  /** Decoding happens once. Content inside a decoded blob is never decoded again. */