@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 +201 -0
- package/NOTICE +9 -0
- package/README.md +11 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +15 -1
- package/dist/permission-subject.d.ts +365 -0
- package/dist/permission-subject.js +1688 -0
- package/package.json +12 -8
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
|
-
/**
|
|
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
|
|
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[];
|