@nebutra/execution-policy 0.1.2 → 0.1.3
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/package.json +24 -14
- package/.turbo/turbo-build.log +0 -17
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-typecheck.log +0 -4
- package/src/index.test.ts +0 -68
- package/src/index.ts +0 -245
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/execution-policy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Shared command permission and approval policy primitives for runtime and execution packages",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,17 +22,26 @@
|
|
|
22
22
|
"category": "ai",
|
|
23
23
|
"summary": "Single shared owner for command permission matching and shell approval defaults"
|
|
24
24
|
},
|
|
25
|
-
"main": "./
|
|
26
|
-
"types": "./
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
27
|
"exports": {
|
|
28
|
-
".":
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
29
38
|
},
|
|
30
39
|
"dependencies": {},
|
|
31
40
|
"devDependencies": {
|
|
32
|
-
"@types/node": "
|
|
33
|
-
"tsup": "
|
|
34
|
-
"typescript": "
|
|
35
|
-
"vitest": "
|
|
41
|
+
"@types/node": "catalog:",
|
|
42
|
+
"tsup": "catalog:",
|
|
43
|
+
"typescript": "catalog:",
|
|
44
|
+
"vitest": "catalog:"
|
|
36
45
|
},
|
|
37
46
|
"homepage": "https://github.com/Nebutra/Nebutra-Sailor/tree/main/packages/ai/execution-policy#readme",
|
|
38
47
|
"repository": {
|
|
@@ -46,9 +55,10 @@
|
|
|
46
55
|
"publishConfig": {
|
|
47
56
|
"access": "public"
|
|
48
57
|
},
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
"files": [
|
|
59
|
+
"dist",
|
|
60
|
+
"README.md",
|
|
61
|
+
"LICENSE",
|
|
62
|
+
"CHANGELOG.md"
|
|
63
|
+
]
|
|
64
|
+
}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/execution-policy@0.1.2 build /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/execution-policy
|
|
3
|
-
> tsup
|
|
4
|
-
|
|
5
|
-
CLI Building entry: src/index.ts
|
|
6
|
-
CLI Using tsconfig: tsconfig.json
|
|
7
|
-
CLI tsup v8.5.1
|
|
8
|
-
CLI Using tsup config: /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/execution-policy/tsup.config.ts
|
|
9
|
-
CLI Target: es2022
|
|
10
|
-
CLI Cleaning output folder
|
|
11
|
-
ESM Build start
|
|
12
|
-
ESM dist/index.js 3.25 KB
|
|
13
|
-
ESM dist/index.js.map 10.62 KB
|
|
14
|
-
ESM ⚡️ Build success in 10ms
|
|
15
|
-
DTS Build start
|
|
16
|
-
DTS ⚡️ Build success in 1585ms
|
|
17
|
-
DTS dist/index.d.ts 4.35 KB
|
package/.turbo/turbo-test.log
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/execution-policy@0.1.1 test /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/execution-policy
|
|
3
|
-
> vitest run
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
RUN v4.1.4 /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/execution-policy
|
|
7
|
-
|
|
8
|
-
✓ src/index.test.ts (5 tests) 5ms
|
|
9
|
-
|
|
10
|
-
Test Files 1 passed (1)
|
|
11
|
-
Tests 5 passed (5)
|
|
12
|
-
Start at 09:38:30
|
|
13
|
-
Duration 306ms (transform 57ms, setup 0ms, import 73ms, tests 5ms, environment 0ms)
|
|
14
|
-
|
package/src/index.test.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
type Action,
|
|
4
|
-
BUILTIN_ARITY,
|
|
5
|
-
commandPermissionKey,
|
|
6
|
-
commandPrefix,
|
|
7
|
-
DEFAULT_SHELL_APPROVAL_RULES,
|
|
8
|
-
evaluate,
|
|
9
|
-
type Rule,
|
|
10
|
-
type Ruleset,
|
|
11
|
-
shellApprovalRequired,
|
|
12
|
-
wildcardMatch,
|
|
13
|
-
} from "./index";
|
|
14
|
-
|
|
15
|
-
describe("wildcardMatch", () => {
|
|
16
|
-
it("matches anchored globs with optional trailing space-star", () => {
|
|
17
|
-
expect(wildcardMatch("git", "git")).toBe(true);
|
|
18
|
-
expect(wildcardMatch("git status", "git")).toBe(false);
|
|
19
|
-
expect(wildcardMatch("git", "git *")).toBe(true);
|
|
20
|
-
expect(wildcardMatch("git status", "git *")).toBe(true);
|
|
21
|
-
expect(wildcardMatch("npm", "git *")).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("evaluate", () => {
|
|
26
|
-
it("resolves first matching permission and pattern pair", () => {
|
|
27
|
-
const allowGitRead: Rule = { permission: "bash", pattern: "git status", action: "allow" };
|
|
28
|
-
const askGit: Rule = { permission: "bash", pattern: "git *", action: "ask" };
|
|
29
|
-
const denyRm: Rule = { permission: "bash", pattern: "rm *", action: "deny" };
|
|
30
|
-
const ruleset: Ruleset = [allowGitRead, askGit, denyRm];
|
|
31
|
-
|
|
32
|
-
expect(evaluate("bash", "git status", ruleset)).toEqual(allowGitRead);
|
|
33
|
-
expect(evaluate("bash", "git push", ruleset)).toEqual(askGit);
|
|
34
|
-
expect(evaluate("edit", "git status", ruleset)).toEqual({
|
|
35
|
-
permission: "edit",
|
|
36
|
-
pattern: "*",
|
|
37
|
-
action: "ask",
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("keeps the documented action vocabulary", () => {
|
|
42
|
-
const actions: Action[] = ["allow", "deny", "ask"];
|
|
43
|
-
expect(actions).toHaveLength(3);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe("commandPermissionKey", () => {
|
|
48
|
-
it("uses the shared command arity table", () => {
|
|
49
|
-
expect(BUILTIN_ARITY.git).toBe(2);
|
|
50
|
-
expect(commandPrefix(["git", "checkout", "main"])).toEqual(["git", "checkout"]);
|
|
51
|
-
expect(commandPermissionKey("pnpm run test -- --watch")).toBe("pnpm run test");
|
|
52
|
-
expect(commandPermissionKey("rm -rf build")).toBe("rm");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("shellApprovalRequired", () => {
|
|
57
|
-
it("keeps destructive defaults in the shared execution policy contract", () => {
|
|
58
|
-
expect(DEFAULT_SHELL_APPROVAL_RULES.length).toBeGreaterThanOrEqual(5);
|
|
59
|
-
expect(shellApprovalRequired("rm -rf build")).toMatchObject({
|
|
60
|
-
requireApproval: "always",
|
|
61
|
-
reason: "destructive recursive removal",
|
|
62
|
-
});
|
|
63
|
-
expect(shellApprovalRequired("git push origin main")).toMatchObject({
|
|
64
|
-
requireApproval: "once_per_session",
|
|
65
|
-
});
|
|
66
|
-
expect(shellApprovalRequired("pnpm test")).toBeNull();
|
|
67
|
-
});
|
|
68
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permission ruleset evaluator.
|
|
3
|
-
*
|
|
4
|
-
* A small, pure, stateless re-expression of a two-dimensional wildcard
|
|
5
|
-
* permission model plus a bash-command-prefix extractor. No global state,
|
|
6
|
-
* no I/O, no mutation of inputs — every function is referentially transparent.
|
|
7
|
-
*
|
|
8
|
-
* The model has two independent dimensions per rule:
|
|
9
|
-
* - `permission` — the capability namespace (e.g. "bash", "edit", "net")
|
|
10
|
-
* - `pattern` — the concrete subject within that namespace
|
|
11
|
-
* A rule applies only when BOTH dimensions match the query via {@link wildcardMatch}.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/** The decision a rule yields. Unknown queries fail safe to "ask". */
|
|
15
|
-
export type Action = "allow" | "deny" | "ask";
|
|
16
|
-
|
|
17
|
-
/** A single permission rule. Both dimensions are matched as wildcard globs. */
|
|
18
|
-
export interface Rule {
|
|
19
|
-
permission: string;
|
|
20
|
-
pattern: string;
|
|
21
|
-
action: Action;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** An ordered list of rules. Earlier rules take precedence. */
|
|
25
|
-
export type Ruleset = Rule[];
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Anchored full-string glob matcher.
|
|
29
|
-
*
|
|
30
|
-
* Semantics:
|
|
31
|
-
* - `*` matches any run of characters, including the empty run.
|
|
32
|
-
* - `?` matches exactly one character.
|
|
33
|
-
* - Every other character is matched literally (regex metacharacters in
|
|
34
|
-
* `pattern` carry no special meaning).
|
|
35
|
-
* - The match is anchored: the entire `str` must be consumed.
|
|
36
|
-
*
|
|
37
|
-
* Special rule (ported faithfully): if `pattern` ends with `" *"` (a single
|
|
38
|
-
* space immediately followed by `*`), that trailing ` *` is OPTIONAL. The
|
|
39
|
-
* pattern then matches both `"<head> <rest>"` and exactly `"<head>"` with
|
|
40
|
-
* nothing after it. For example `"git *"` matches `"git"` and `"git status"`.
|
|
41
|
-
*
|
|
42
|
-
* Implemented with a backtracking two-pointer scan whose `*` handling uses a
|
|
43
|
-
* single saved restart position, giving O(|str| * |pattern|) worst case with
|
|
44
|
-
* no catastrophic blow-up.
|
|
45
|
-
*/
|
|
46
|
-
export function wildcardMatch(str: string, pattern: string): boolean {
|
|
47
|
-
if (isOptionalTrailingStar(pattern)) {
|
|
48
|
-
const head = pattern.slice(0, -2); // drop the trailing " *"
|
|
49
|
-
// Head-only branch: the whole string equals the head, matched as a glob.
|
|
50
|
-
if (globMatch(str, head)) {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
// Otherwise the full "<head> *" pattern must match (space is consumed).
|
|
54
|
-
return globMatch(str, pattern);
|
|
55
|
-
}
|
|
56
|
-
return globMatch(str, pattern);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* True when `pattern` ends in a literal space followed by `*`, and that `*`
|
|
61
|
-
* is the final character. A bare `"*"` (no preceding space) is NOT treated
|
|
62
|
-
* as the optional-suffix form.
|
|
63
|
-
*/
|
|
64
|
-
function isOptionalTrailingStar(pattern: string): boolean {
|
|
65
|
-
return pattern.length >= 2 && pattern.endsWith(" *");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Core anchored glob match using linear-time backtracking. Only `*` can
|
|
70
|
-
* backtrack, and it uses a single restart marker (the classic two-pointer
|
|
71
|
-
* algorithm), so there is no exponential backtracking.
|
|
72
|
-
*/
|
|
73
|
-
function globMatch(str: string, pattern: string): boolean {
|
|
74
|
-
let s = 0;
|
|
75
|
-
let p = 0;
|
|
76
|
-
let starP = -1;
|
|
77
|
-
let starS = 0;
|
|
78
|
-
|
|
79
|
-
while (s < str.length) {
|
|
80
|
-
const pc = p < pattern.length ? pattern[p] : undefined;
|
|
81
|
-
if (pc === "*") {
|
|
82
|
-
// Record the restart point and tentatively consume zero chars.
|
|
83
|
-
starP = p;
|
|
84
|
-
starS = s;
|
|
85
|
-
p += 1;
|
|
86
|
-
} else if (pc === "?" || pc === str[s]) {
|
|
87
|
-
p += 1;
|
|
88
|
-
s += 1;
|
|
89
|
-
} else if (starP !== -1) {
|
|
90
|
-
// Backtrack: let the last `*` swallow one more character.
|
|
91
|
-
p = starP + 1;
|
|
92
|
-
starS += 1;
|
|
93
|
-
s = starS;
|
|
94
|
-
} else {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Consume any trailing `*` segments in the pattern.
|
|
100
|
-
while (p < pattern.length && pattern[p] === "*") {
|
|
101
|
-
p += 1;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return p === pattern.length;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Resolve a permission query against one or more rulesets.
|
|
109
|
-
*
|
|
110
|
-
* Rulesets are concatenated in argument order (no mutation) and scanned
|
|
111
|
-
* front-to-back. The FIRST rule whose `permission` and `pattern` both match
|
|
112
|
-
* the query (via {@link wildcardMatch}) is returned. If no rule matches, a
|
|
113
|
-
* fail-safe default is returned: the queried permission/pattern with action
|
|
114
|
-
* `"ask"` (unknown → ask, never silently allow).
|
|
115
|
-
*/
|
|
116
|
-
export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule {
|
|
117
|
-
for (const ruleset of rulesets) {
|
|
118
|
-
for (const rule of ruleset) {
|
|
119
|
-
if (wildcardMatch(permission, rule.permission) && wildcardMatch(pattern, rule.pattern)) {
|
|
120
|
-
return rule;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return { permission, pattern: "*", action: "ask" };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Built-in command arity table. Maps a (possibly multi-word) command prefix
|
|
129
|
-
* to the number of leading tokens that constitute its "human-understandable
|
|
130
|
-
* command" for permission matching. Longest matching prefix wins.
|
|
131
|
-
*
|
|
132
|
-
* Frozen so the shared default cannot be mutated by callers.
|
|
133
|
-
*/
|
|
134
|
-
export const BUILTIN_ARITY: Readonly<Record<string, number>> = Object.freeze({
|
|
135
|
-
git: 2,
|
|
136
|
-
npm: 2,
|
|
137
|
-
"npm run": 3,
|
|
138
|
-
docker: 2,
|
|
139
|
-
kubectl: 2,
|
|
140
|
-
cargo: 2,
|
|
141
|
-
pnpm: 2,
|
|
142
|
-
"pnpm run": 3,
|
|
143
|
-
yarn: 2,
|
|
144
|
-
"yarn run": 3,
|
|
145
|
-
go: 2,
|
|
146
|
-
ls: 1,
|
|
147
|
-
cat: 1,
|
|
148
|
-
cd: 1,
|
|
149
|
-
rm: 1,
|
|
150
|
-
cp: 1,
|
|
151
|
-
mv: 1,
|
|
152
|
-
mkdir: 1,
|
|
153
|
-
echo: 1,
|
|
154
|
-
grep: 1,
|
|
155
|
-
python: 1,
|
|
156
|
-
node: 1,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Extract the human-understandable command from already-split, flag-free
|
|
161
|
-
* shell tokens.
|
|
162
|
-
*
|
|
163
|
-
* Strategy: try the longest prefix first. For `len` from `tokens.length` down
|
|
164
|
-
* to 1, if `tokens.slice(0, len).join(" ")` is a key in the (merged) arity
|
|
165
|
-
* table, return `tokens.slice(0, arity[thatPrefix])`. If the tokens are empty,
|
|
166
|
-
* return `[]`. Otherwise default to the first token only.
|
|
167
|
-
*
|
|
168
|
-
* `arity` is shallow-merged OVER the built-in table; neither the caller's
|
|
169
|
-
* object nor the built-in table is mutated.
|
|
170
|
-
*/
|
|
171
|
-
export function commandPrefix(
|
|
172
|
-
tokens: string[],
|
|
173
|
-
arity?: Record<string, number> | undefined,
|
|
174
|
-
): string[] {
|
|
175
|
-
if (tokens.length === 0) {
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
const table: Record<string, number> = { ...BUILTIN_ARITY, ...(arity ?? {}) };
|
|
179
|
-
|
|
180
|
-
for (let len = tokens.length; len >= 1; len -= 1) {
|
|
181
|
-
const prefixKey = tokens.slice(0, len).join(" ");
|
|
182
|
-
const take = table[prefixKey];
|
|
183
|
-
if (take !== undefined) {
|
|
184
|
-
// Clamp to the available token count; never expand beyond the input.
|
|
185
|
-
return tokens.slice(0, Math.min(take, tokens.length));
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return tokens.slice(0, 1);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Derive the `pattern` to feed {@link evaluate} for a bash permission.
|
|
194
|
-
*
|
|
195
|
-
* Splits `command` on arbitrary whitespace, drops tokens that begin with `-`
|
|
196
|
-
* (flags are not conceptually part of the command identity), applies
|
|
197
|
-
* {@link commandPrefix}, and joins the result with single spaces.
|
|
198
|
-
*/
|
|
199
|
-
export function commandPermissionKey(command: string): string {
|
|
200
|
-
const tokens = command.split(/\s+/).filter((token) => token.length > 0 && !token.startsWith("-"));
|
|
201
|
-
return commandPrefix(tokens).join(" ");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export type ShellApprovalMode = "always" | "once_per_session" | "never";
|
|
205
|
-
|
|
206
|
-
export interface ShellApprovalRule {
|
|
207
|
-
readonly match: string | RegExp;
|
|
208
|
-
readonly requireApproval: ShellApprovalMode;
|
|
209
|
-
readonly reason: string;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export const DEFAULT_SHELL_APPROVAL_RULES: readonly ShellApprovalRule[] = Object.freeze([
|
|
213
|
-
{ match: /^rm\s+-rf\b/, requireApproval: "always", reason: "destructive recursive removal" },
|
|
214
|
-
{
|
|
215
|
-
match: /\b(format|mkfs)\b/,
|
|
216
|
-
requireApproval: "always",
|
|
217
|
-
reason: "destructive filesystem operation",
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
match: /\bDROP\s+(DATABASE|SCHEMA|TABLE)\b/i,
|
|
221
|
-
requireApproval: "always",
|
|
222
|
-
reason: "destructive database operation",
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
match: /\b(npm|pnpm|yarn)\s+publish\b/,
|
|
226
|
-
requireApproval: "always",
|
|
227
|
-
reason: "package publishing",
|
|
228
|
-
},
|
|
229
|
-
{ match: /^git\s+push\b/, requireApproval: "once_per_session", reason: "remote git mutation" },
|
|
230
|
-
]);
|
|
231
|
-
|
|
232
|
-
export function matchesShellApprovalRule(command: string, rule: ShellApprovalRule): boolean {
|
|
233
|
-
return typeof rule.match === "string" ? command.startsWith(rule.match) : rule.match.test(command);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function shellApprovalRequired(
|
|
237
|
-
command: string,
|
|
238
|
-
rules: readonly ShellApprovalRule[] = DEFAULT_SHELL_APPROVAL_RULES,
|
|
239
|
-
): ShellApprovalRule | null {
|
|
240
|
-
return (
|
|
241
|
-
rules.find(
|
|
242
|
-
(rule) => rule.requireApproval !== "never" && matchesShellApprovalRule(command, rule),
|
|
243
|
-
) ?? null
|
|
244
|
-
);
|
|
245
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../../tsconfig.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"target": "esnext",
|
|
7
|
-
"types": ["node"],
|
|
8
|
-
"incremental": false
|
|
9
|
-
},
|
|
10
|
-
"include": ["src"],
|
|
11
|
-
"exclude": ["node_modules", "dist"]
|
|
12
|
-
}
|