@crewhaus/tool-executor 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 ADDED
@@ -0,0 +1,11 @@
1
+ # @crewhaus/tool-executor
2
+
3
+ Execute a single tool: validate → permission-check → invoke → format result
4
+
5
+ Part of [CrewHaus](https://github.com/crewhaus/factory). Most people install the CLI (`npm install -g crewhaus`) rather than this package.
6
+
7
+ Which tools a spec can turn on, and how: [tools reference](https://github.com/crewhaus/docs/blob/main/TOOLS-REFERENCE.md).
8
+
9
+ Requires [Bun](https://bun.sh) `>=1.3.14`; plain Node is not supported.
10
+
11
+ [Source](https://github.com/crewhaus/factory/tree/main/packages/tool-executor)
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { CrewhausError } from "@crewhaus/errors";
2
2
  import type { RegisteredTool, ToolExecuteModel, ToolExecuteResult } from "@crewhaus/tool-catalog";
3
+ import { type PathCanonicalizer } from "./permission-subject";
4
+ export { type PathCanonicalizer, type PermissionSubject, type PermissionSubjectOptions, type ProgramPathReader, type ProgramPathReaderOptions, type ProgramPathReading, lexicalPathValues, operativeValuesFor, operativeValuesOf, preparePermissionSubject, readOperativeField, } from "./permission-subject";
3
5
  /**
4
6
  * Section 14 — `content` widened from `string` to `string | ToolResultContent`
5
7
  * so tools like `ReadImage` can return Anthropic image content blocks. Error
@@ -13,8 +15,18 @@ export type ToolResult = {
13
15
  };
14
16
  export type ExecutionContext = {
15
17
  readonly toolUseId: string;
16
- /** When present, the tool call must match at least one pattern. Absent = allow all. */
18
+ /**
19
+ * When present, the tool call must match at least one pattern, read as an
20
+ * allow rule: every operative value of the PARSED input must match its
21
+ * argument glob. Absent = allow all.
22
+ */
17
23
  readonly allowedPatterns?: ReadonlyArray<string>;
24
+ /**
25
+ * How a path-kind operative value is canonicalised for `allowedPatterns`.
26
+ * Default: {@link lexicalPathValues}, which needs no filesystem. A Node
27
+ * caller that has a workspace passes one that also follows symlinks.
28
+ */
29
+ readonly canonicalizePath?: PathCanonicalizer;
18
30
  /** Optional cooperative-cancellation signal forwarded to the tool. */
19
31
  readonly signal?: AbortSignal;
20
32
  /**
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { CrewhausError, isRunFailedError } from "@crewhaus/errors";
2
2
  import { compilePattern, matchesPattern } from "@crewhaus/tool-permission-matcher";
3
3
  import { validateToolInput } from "@crewhaus/tool-validate";
4
+ import { operativeValuesFor } from "./permission-subject";
5
+ export { lexicalPathValues, operativeValuesFor, operativeValuesOf, preparePermissionSubject, readOperativeField, } from "./permission-subject";
4
6
  export class ToolPermissionError extends CrewhausError {
5
7
  name = "ToolPermissionError";
6
8
  toolName;
@@ -16,8 +18,20 @@ export async function executeTool(tool, rawInput, context) {
16
18
  return { toolUseId, content: validation.error.message, isError: true };
17
19
  }
18
20
  if (allowedPatterns !== undefined) {
21
+ // Match what the tool will run on — the parsed input, with its operative
22
+ // values canonicalised — never the raw input, which can carry a decoy key
23
+ // the schema strips (#145, security-1#0).
19
24
  const compiled = allowedPatterns.map(compilePattern);
20
- const permitted = compiled.some((p) => matchesPattern(p, tool.name, rawInput));
25
+ const operativeValues = operativeValuesFor(tool, validation.value, {
26
+ ...(context.canonicalizePath !== undefined
27
+ ? { canonicalizePath: context.canonicalizePath }
28
+ : {}),
29
+ ...(context.toolConfig !== undefined ? { toolConfig: context.toolConfig } : {}),
30
+ });
31
+ const permitted = compiled.some((p) => matchesPattern(p, tool.name, validation.value, {
32
+ polarity: "allow",
33
+ ...(operativeValues !== undefined ? { operativeValues } : {}),
34
+ }));
21
35
  if (!permitted) {
22
36
  return {
23
37
  toolUseId,
@@ -0,0 +1,365 @@
1
+ /**
2
+ * What a permission rule is checked against, for one tool call.
3
+ *
4
+ * A rule used to be matched against the raw input the model sent, while the
5
+ * tool ran on that input AFTER its schema had parsed it — unknown keys
6
+ * stripped, defaults filled in. The two could disagree, and every
7
+ * disagreement was a way past a rule: a decoy key the tool never reads, an
8
+ * extra argument that stopped a deny from matching, a left-out field the tool
9
+ * then filled with the very value a deny named. And a path was matched as a
10
+ * string, so `build/../src` passed `build/**` and a symlinked directory led
11
+ * somewhere no rule had looked.
12
+ *
13
+ * So a call is described the way the tool will see it:
14
+ *
15
+ * 1. The input is parsed with the tool's own schema. A call that does not
16
+ * parse cannot run, and is refused before any rule is read.
17
+ * 2. When the tool declares `operativeArgs`, the values of those fields are
18
+ * read from the PARSED input (a declared `default` stands in for an
19
+ * omitted field) and canonicalised by kind:
20
+ * - a `path` goes through `canonicalizePath`. The Node runtime passes one
21
+ * that resolves the path against the workspace root, collapses `..` and
22
+ * follows symlinked directories, so the rule sees the place the tool
23
+ * will actually touch (runtime-core's `workspacePathCanonicalizer`).
24
+ * Without one — the edge worker has no filesystem to ask — `..` is
25
+ * collapsed lexically and a relative path that climbs above its start
26
+ * is flagged as outside, which no allow rule matches and every deny
27
+ * does.
28
+ * - a `url` is parsed (WHATWG) and matched as its `href`.
29
+ * - a `command` held as an array (an argv) is joined with spaces; each
30
+ * word is kept as another spelling, so a deny or ask rule naming one
31
+ * word (`RunCommand(rm)`) still fires on the whole argv. A word that
32
+ * names a file through `.`, `..` or a doubled `/` is also read with
33
+ * them collapsed (and with the `./` it was written with kept), so
34
+ * `scripts//release.sh` is `scripts/release.sh`.
35
+ * - the words of a command that name a file are read where the kernel
36
+ * finds them (`readProgramPath`; runtime-core's reader walks each name
37
+ * from the directory the command runs in, a `..` after a symlink
38
+ * climbing out of the link's target): its program when written with a
39
+ * `/` (also through wrappers such as `env` or `sudo`), a bare program on
40
+ * each entry of a PATH a wrapper or the line sets (`env PATH=dir x`,
41
+ * `PATH=dir x`), and each script an interpreter (`sh`, `node`, `bun`,
42
+ * `python`, …) is handed — from a file on its input too (`sh < x.sh`).
43
+ * One that lands elsewhere than it reads is put in its place for an
44
+ * allow: `./scripts/../evil.sh` is `./evil.sh`, which
45
+ * `RunCommand(./scripts/**)` does not grant, and a deny or ask reads
46
+ * that place too — `link/../run.sh` with `link → secrets/deep` is
47
+ * `secrets/run.sh`. Such a word is also read with its `..` folded as
48
+ * text first, the way node, `bun run` and a `#!/usr/bin/env node`
49
+ * script open it — `node lnk/../x.mjs` with `lnk → safe/sub` runs
50
+ * `./x.mjs` — and where the two readings land apart, a deny or ask reads
51
+ * both and an allow must name both, unless the word is the script of
52
+ * a shell, Python 3, Perl or Ruby, which open it where the kernel walks.
53
+ * A program reached through a symlink to it
54
+ * (`node_modules/.bin/eslint`) is named by the link; a deny or ask also
55
+ * reads where the link leads. A program named by an absolute path
56
+ * outside the workspace is read as written unless it has a `..` after
57
+ * a name: the system's own links (`/bin → usr/bin`) are not the call's
58
+ * to move. One read where a wrapper moved the command (`env -C`), or
59
+ * found on a PATH entry, cannot be put in its place in the argv: no
60
+ * scoped allow grants it. Every other word of an argv with a `/` (any
61
+ * word, where a wrapper moved it) is read the same way, for a deny or
62
+ * ask only — another program may run it (`arch x`, `find -exec x`) —
63
+ * and a grant is bound to where it lands.
64
+ * - a line a shell parses runs each command where the `cd`s before it
65
+ * leave it: joined as text the way `cd` does by default, walked when it
66
+ * is `cd -P`, and each place a `cd` that may not have run leaves it
67
+ * (`cd a; x` may run x in either). A wrapper's directory (`env -C`) is
68
+ * walked as the kernel walks it. A line cannot have a word put in its
69
+ * place, so an allow must also name each command whose words moved,
70
+ * with them put in: `Bash(./scripts/**)` does not grant
71
+ * `./scripts/lnk/run.sh` (`lnk → ../evil`), and `Bash(node **)` still
72
+ * grants a script reached through a linked package directory. `$PWD`
73
+ * is the directory the command runs in; folded as text, it is the
74
+ * `cd`s the shell joined, a link's name and all, so after `cd lnk` node
75
+ * runs `$PWD/../x.mjs` as `./x.mjs` (where that text cannot be told —
76
+ * after a wrapper's or a `cd -P`'s move — one that climbs out of it
77
+ * cannot be placed). Spelled any other way (`${PWD%/*}`, `$OLDPWD`),
78
+ * the shell's own directory cannot be placed. A word read from a directory
79
+ * that cannot be told (`cd "$X"`, `cd -`, a `cd` in a loop), or that a
80
+ * variable the line does not set or a `~` starts, is read as written;
81
+ * with a `..` after a name it cannot be placed. A pattern the shell
82
+ * expands is read as every path it can list; a script that is one with
83
+ * a wildcard before its last name cannot be placed. A word that cannot
84
+ * be placed — also one with a `..` after a name and no reader (the edge
85
+ * worker) — is flagged `outsideWorkspace`.
86
+ * - a field declared `within` another is written `<qualifier>/<value>` —
87
+ * except a `command`, whose `within` names the directory it runs in:
88
+ * the program `./build.sh` names is another program in `src/`. A
89
+ * command run anywhere but the workspace root loses its canonical
90
+ * value, so no scoped allow covers it (the call asks) except one that
91
+ * names every command (`RunCommand(**)`). A deny or ask reads it as
92
+ * written, and each word that may name a file also as that file from
93
+ * the root, so `alwaysDeny RunCommand(*scripts/release.sh*)` fires on
94
+ * `[sh, release.sh]` run in `scripts/`. Only the directory's
95
+ * workspace-relative spellings (and the one the call wrote) are joined:
96
+ * the workspace's own absolute path is not something the call named,
97
+ * so `RunCommand(**prod**)` does not fire because the workspace lives
98
+ * in `prod-agent/`. A bare program name (`git`) is not joined — PATH
99
+ * finds it, not the directory. A command inside an array of objects
100
+ * (`steps.argv`) runs in its element's own directory field when it has
101
+ * one, else the top-level one.
102
+ * A path declared `within` a directory the call leaves out is read from
103
+ * the root, or from its declared `withinDefault` (TempDir's `.tmp`).
104
+ * A path declared `within` a directory that is absolute or has a `..`
105
+ * segment — the call's directory or the root when it gives none — is
106
+ * flagged `outsideWorkspace`: the tool hands it on as written to a
107
+ * program running there, and the OS follows a symlink before the `..`
108
+ * after it, so `../x.ts` run in `link/` (`link → secrets/deep`) is
109
+ * `secrets/x.ts`, not the `x.ts` a collapsed reading names (see
110
+ * `leavesItsDirectory`).
111
+ * Unless it is a path or a command, the value alone and the qualifier
112
+ * alone are kept as other spellings, so a deny or ask rule written
113
+ * without the qualifier — `alwaysDeny EvmCall(0xdAC17F…)`,
114
+ * `EvmCall(*)`, the way 0.7.0 matched every string in the call — still
115
+ * fires. An allow must name the qualified value, so it cannot be
116
+ * widened by leaving the qualifier out.
117
+ * - a `command` declared `shell` and held as a string is a line a shell
118
+ * parses: it is marked `shell`, and the matcher reads each simple
119
+ * command in it (an allow must match every one, a deny or ask fires on
120
+ * any). A `command` held as an argv is never split — no shell reads it
121
+ * — but when the program it names is a shell given `-c`, an `eval` or
122
+ * an `env -S` (after wrappers such as `env` or `sudo`), the line that
123
+ * runs is added as a value only a deny or ask reads, marked `shell`:
124
+ * `alwaysDeny RunCommand(rm **)` fires on `["sh", "-c", "a && rm x"]`.
125
+ * - a `command` declared with an `env` field — the variables the call
126
+ * sets in the child's environment — can run another program than its
127
+ * words say: PATH decides what a bare program name is, and BASH_ENV,
128
+ * NODE_OPTIONS, LD_PRELOAD and the like load code of their own. So a
129
+ * call that sets any variable loses its canonical value (only an
130
+ * allow naming every command covers it), and a deny or ask also reads
131
+ * a bare program as it is found on each PATH entry the call sets, and
132
+ * every value it sets — each word that may name a file also joined to
133
+ * where it resolves. An
134
+ * environment too large to read that way (more than
135
+ * {@link MAX_ENV_VARS} variables, {@link MAX_ENV_WORDS} words in a
136
+ * value, or a value longer than {@link MAX_ENV_VALUE_CHARS} characters)
137
+ * is flagged `outsideWorkspace`: which program runs could not be worked
138
+ * out, so every deny or ask fires, and only an allow naming every
139
+ * command covers it.
140
+ * - a field left out whose declared default is `*` stands for every
141
+ * value (`standsForAny`), whatever its kind: a deny or ask naming any
142
+ * one value there fires on it, and an allow grants it only when it
143
+ * covers every value there. A path stands for every path under its
144
+ * directory (the workspace root when it has none); a URL, or a
145
+ * command, for any at all. When the field is declared `within` another
146
+ * that the call leaves out as well, it stands for every
147
+ * `<qualifier>/<value>` (`anyQualifier`): a code search naming no owner
148
+ * reaches every repository.
149
+ * - a field declared `prefix` (KvList's `prefix`) stands for every value
150
+ * that starts with it, whether the call gives it or leaves it out (the
151
+ * empty prefix): `<qualifier>/<prefix>` followed by any run, since a
152
+ * key may hold `/`, and the prefix alone for a deny written without
153
+ * the qualifier (read one segment on, so a deny naming another
154
+ * namespace's key does not fire on every listing).
155
+ * - an `id` or `recipient` declared with a `service` field (codehost's
156
+ * `baseUrl`, a Chat tool's `apiBaseUrl`) is matched with the origin of
157
+ * the API the call goes to in front (`https://ghe.example.com/acme/app`):
158
+ * the one it names, else the one the tool's `serviceDefaults` says it
159
+ * goes to (read from `toolConfig`). An allow must name the origin,
160
+ * unless the API is the one the tool is set up for (its `home`): a call
161
+ * to it, named or not, is also matched without one. A deny or ask also
162
+ * reads the value without the origin, after the origin's other
163
+ * spellings (no root dot, no port, the other of http and https) and
164
+ * after its host alone (`ghe.example.com/acme/app`), and the URL itself
165
+ * as a URL. A URL with no http or https origin, or with a user name or
166
+ * password, cannot be placed: the value is flagged `outsideWorkspace`
167
+ * (see {@link servedValue}). A tool with no `serviceDefaults` reads a
168
+ * call that names no API without an origin.
169
+ * - a `url` declared as its own `service` (RateLimitStatus's `baseUrl`)
170
+ * is the API the call goes to, matched as a URL: the one it names, else
171
+ * the one the tool's `serviceDefaults` say it goes to, read as the tool
172
+ * joins its request paths to it. A deny or ask also reads it after the
173
+ * origin's other spellings and its host alone (see {@link apiRootValue}).
174
+ * - an `id` or `recipient` declared with an `overlay` field (a deploy
175
+ * tenant), on a call that names it, is matched with its value in front
176
+ * (`acme/billing/staging`): the call acts on that owner's own copy. A
177
+ * deny or ask also reads the value without it, and its value alone
178
+ * (see {@link overlaidValue}).
179
+ * - a `path` declared `beneath` (a directory the tool walks: RemovePath,
180
+ * Grep, a git pathspec) also carries its spellings as `beneath`
181
+ * prefixes, so a deny or ask naming anything under it fires —
182
+ * unless the canonicaliser marked it `notDirectory`. A walk that
183
+ * skips hidden names reads them too when the call sets the switch its
184
+ * `hiddenWhen` names (Tree's `includeHidden`), and one declared with a
185
+ * `beneathWhen` switch (TempDir's `clear`) is read beneath only when
186
+ * the call sets it: otherwise the tool acts on the path alone. One declared
187
+ * `defaultAtRoot` and left out is the workspace root, not its `within`
188
+ * directory: a git command given no path acts on the whole repository.
189
+ * Each relative prefix is also read with a leading `./`, as a direct
190
+ * path is, so `alwaysDeny Tree(./secrets/**)` fires on `Tree .`.
191
+ * - a `path` declaring `opens` (the fixed files a tool opens inside the
192
+ * directory it names: ApprovalStatus's ledger) also carries each of
193
+ * them, joined to the directory and canonicalised, as a value only a
194
+ * deny or ask reads.
195
+ * - a `path` declared `glob` (the Glob tool's pattern) is marked
196
+ * `globPattern`: the matcher reads it as every path it can list.
197
+ * - a `relocates` field left out stands in with its default, which a
198
+ * deny or ask reads; when the call carries another operative value an
199
+ * allow skips it (`restrictOnly`), because the grant is about the
200
+ * record the call names, not the store it lives in. One declared
201
+ * `beneath` too (a store the tool reads whole) carries its prefixes.
202
+ * - an `id` declared `secretRef` is a secret reference, trimmed as the
203
+ * tool trims it; a `file:` or `envfile:` one is matched with the file
204
+ * it names canonicalised in it, and the file is also a value only a
205
+ * deny or ask reads; a bare NAME and an `env:NAME` are also read, by a
206
+ * deny or ask only, as the env files they are looked up in or loaded
207
+ * from (see {@link secretRefValues}).
208
+ * - a field declared `source` (where the tool takes a value from) is read
209
+ * by a deny or ask only, and so is one declared `ignoredWhen` another
210
+ * field the call names (ChatPost's `channel` beside `webhookUrlEnv`):
211
+ * the tool does not use it then.
212
+ * - an `id`, `recipient` or `text` value that is `0x` hex (an address, a
213
+ * hash — `0x` or `0X`, both of which a node accepts) is marked
214
+ * `caseInsensitive`: its letter case is at most an EIP-55 checksum, so
215
+ * a deny or ask rule must not be dodged by it.
216
+ * A tool that declares `[]` has no field that decides where it acts, and
217
+ * is matched on its string values like a tool that declares nothing.
218
+ *
219
+ * Nothing here touches the filesystem or imports a `node:` builtin: this
220
+ * package is part of the worker runtime's import graph.
221
+ */
222
+ import type { OperativeArg, RegisteredTool } from "@crewhaus/tool-catalog";
223
+ import { type OperativeValue } from "@crewhaus/tool-permission-matcher";
224
+ /** Turns one path-kind operative value into the value(s) a rule is matched against. */
225
+ export type PathCanonicalizer = (raw: string) => ReadonlyArray<OperativeValue>;
226
+ /**
227
+ * Where a word of a command that names a file lands — a program run by its
228
+ * path, or a script handed to an interpreter — read the way the kernel reads
229
+ * it from the directory the command runs in (`runsIn` as the call wrote it;
230
+ * `undefined` for the workspace root): each directory on the way followed,
231
+ * and each `..` applied AFTER the link before it, so `link/../run.sh` with
232
+ * `link → secrets/deep` is `secrets/run.sh`. `undefined` when that cannot be
233
+ * worked out (a loop of links, a directory that cannot be searched, a path
234
+ * longer than any path can be): no tool can run the word either.
235
+ */
236
+ export type ProgramPathReader = (word: string, runsIn: string | undefined, opts?: ProgramPathReaderOptions) => ProgramPathReading | undefined;
237
+ /** How a {@link ProgramPathReader} is asked about one word. */
238
+ export type ProgramPathReaderOptions = {
239
+ /**
240
+ * The word is an argument of the command, or a candidate on a PATH it
241
+ * sets, rather than its program or script: a reader that keeps a record
242
+ * of where words land (for an approval to be bound to) keeps it only
243
+ * when the word lands elsewhere than it reads, or is a symlink.
244
+ */
245
+ readonly argument?: boolean;
246
+ /**
247
+ * Where the command moved from `runsIn` before the word is opened,
248
+ * walked as the kernel walks it: a wrapper's directory (`env -C dir`), a
249
+ * `cd -P`. The word is read from there; `asWritten` joins all three as
250
+ * text.
251
+ */
252
+ readonly prefix?: string;
253
+ };
254
+ /** One word read by a {@link ProgramPathReader}. */
255
+ export type ProgramPathReading = {
256
+ /** Absolute: where the word lands, its last name as written (not followed). */
257
+ readonly at: string;
258
+ /** `at` relative to the workspace root (`.` for the root, never `./`), when it is inside it. */
259
+ readonly inWorkspace?: string;
260
+ /** Absolute: the same word read as text — joined to the directory, `..` collapsed, nothing followed. */
261
+ readonly asWritten: string;
262
+ /** Whether {@link asWritten} is inside the workspace. */
263
+ readonly writtenInWorkspace: boolean;
264
+ /** Where the last name leads when it is a symlink: the file the kernel runs. */
265
+ readonly target?: {
266
+ readonly at: string;
267
+ readonly inWorkspace?: string;
268
+ };
269
+ };
270
+ export type PermissionSubject = {
271
+ readonly ok: true;
272
+ /** The schema-parsed input: what the tool's `execute` will receive. */
273
+ readonly input: unknown;
274
+ /** Canonical values of the tool's `operativeArgs`; absent when the tool declares none. */
275
+ readonly operativeValues?: ReadonlyArray<OperativeValue>;
276
+ } | {
277
+ readonly ok: false;
278
+ /** The schema's validation message, naming what is wrong with the input. */
279
+ readonly reason: string;
280
+ };
281
+ export type PermissionSubjectOptions = {
282
+ /**
283
+ * How a path-kind value is canonicalised. Default: {@link lexicalPathValues}
284
+ * — `..` collapsed, no symlinks followed, no workspace root known.
285
+ */
286
+ readonly canonicalizePath?: PathCanonicalizer;
287
+ /**
288
+ * Where a command word that names a file lands. Default: none — the words
289
+ * are read as text, and one with a `..` after a name is flagged outside,
290
+ * since a symlink before the `..` could not be seen.
291
+ */
292
+ readonly readProgramPath?: ProgramPathReader;
293
+ /**
294
+ * The tool's block of the serving candidate's `tool_config`, as the tool
295
+ * will be handed it (`ToolExecuteContext.toolConfig`); left out, the tool
296
+ * reads the config it was registered with. The tool's `serviceDefaults`
297
+ * read it to tell which API a call that names none goes to.
298
+ */
299
+ readonly toolConfig?: unknown;
300
+ };
301
+ /**
302
+ * Parse `rawInput` with the tool's schema and read its operative values. The
303
+ * input passed here must be exactly the one that will be handed to
304
+ * `executeTool`, so the rule and the tool see the same call.
305
+ */
306
+ export declare function preparePermissionSubject(tool: RegisteredTool, rawInput: unknown, opts?: PermissionSubjectOptions): PermissionSubject;
307
+ /**
308
+ * The canonical operative values of an ALREADY PARSED input, or `undefined`
309
+ * when the tool declares no `operativeArgs`, or declares `[]` (the matcher
310
+ * then falls back to the input's string values).
311
+ */
312
+ export declare function operativeValuesFor(tool: RegisteredTool, parsedInput: unknown, opts?: PermissionSubjectOptions): ReadonlyArray<OperativeValue> | undefined;
313
+ /**
314
+ * {@link operativeValuesFor} from a declaration alone — for a caller that has
315
+ * a tool's `operativeArgs` as data (the builtin manifest) but not the tool.
316
+ * The values are read the same way, except that a call naming no API in a
317
+ * `service` field is read without an origin: which API it goes to is the
318
+ * tool's to say. The caller is responsible for passing an input the tool's
319
+ * schema has parsed, when it can.
320
+ */
321
+ export declare function operativeValuesOf(operativeArgs: ReadonlyArray<OperativeArg> | undefined, parsedInput: unknown, opts?: PermissionSubjectOptions): ReadonlyArray<OperativeValue> | undefined;
322
+ /**
323
+ * Every value of one declared field. Dots descend into objects; an array
324
+ * anywhere on the way is walked element by element — except a `command`
325
+ * field that IS an array of strings, which is one argv and is joined. A
326
+ * field declared `within` another comes back as `<qualifier>/<value>` — a
327
+ * `..` path too, so a row shows the directory it climbs from — and an
328
+ * absolute path as it is.
329
+ */
330
+ export declare function readOperativeField(input: unknown, arg: OperativeArg): string[];
331
+ /**
332
+ * The most variables, words in one value, and characters in one value, of an
333
+ * environment a command's call sets that a deny or ask reads one by one. A
334
+ * call past any of them is flagged `outsideWorkspace` instead: which program
335
+ * it runs could not be worked out, so every deny or ask fires on it.
336
+ */
337
+ export declare const MAX_ENV_VARS = 64;
338
+ export declare const MAX_ENV_WORDS = 64;
339
+ export declare const MAX_ENV_VALUE_CHARS = 8192;
340
+ /**
341
+ * `text` without its trailing `/` and `\`: what `text.replace(/[\\/]+$/, "")`
342
+ * returned, in one pass from the end. That pattern tries every start inside
343
+ * a run of separators that is not at the end and rescans the rest of the run
344
+ * from each, so it was quadratic in the run's length, and a directory or a
345
+ * PATH entry is whatever the call wrote.
346
+ */
347
+ export declare function withoutTrailingSeparators(text: string): string;
348
+ /**
349
+ * A command word with its `.` and `..` folded as text, as `path.resolve`
350
+ * folds them before anything is opened — the way node and `bun run` read
351
+ * the script they are handed, and a `#!/usr/bin/env node` script the path it
352
+ * was run by. A `..` after a name takes the name off; one at the start of a
353
+ * relative word stays, climbing from the directory the command runs in; one
354
+ * at the root of an absolute word stays at the root. A relative result
355
+ * without a `/` keeps a `./`, so it still names a file in that directory,
356
+ * not a program to look up.
357
+ */
358
+ export declare function foldedAsText(word: string): string;
359
+ /**
360
+ * The filesystem-free reading of a path: `..` and `.` collapsed, nothing
361
+ * followed. A relative path that climbs above its start is flagged as
362
+ * outside. Used where there is no workspace to ask (the edge worker); the
363
+ * Node runtime passes a canonicaliser that also follows symlinks.
364
+ */
365
+ export declare function lexicalPathValues(raw: string): OperativeValue[];