@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 +201 -0
- package/NOTICE +9 -0
- package/README.md +18 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.js +113 -37
- package/dist/lib/injection.d.ts +31 -0
- package/dist/lib/injection.js +176 -6
- package/dist/lib/ipv6-scan.d.ts +16 -0
- package/dist/lib/ipv6-scan.js +73 -0
- package/dist/lib/literal.d.ts +55 -0
- package/dist/lib/literal.js +146 -0
- package/dist/lib/pii.d.ts +19 -30
- package/dist/lib/pii.js +71 -8
- package/dist/lib/policy.d.ts +24 -2
- package/dist/lib/policy.js +176 -79
- package/dist/lib/secrets.d.ts +8 -0
- package/dist/lib/secrets.js +43 -5
- package/dist/lib/text.d.ts +61 -2
- package/dist/lib/text.js +174 -5
- package/dist/paths.d.ts +21 -2
- package/dist/paths.js +72 -13
- package/package.json +12 -6
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
|
-
- -
|
|
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
|
-
*
|
|
79
|
-
*
|
|
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
|
|
82
|
-
if (
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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("
|
|
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;
|
package/dist/lib/injection.d.ts
CHANGED
|
@@ -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. */
|