prae-cli 0.1.1 → 0.2.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/README.md +2 -2
- package/lib/bin.js +2 -1
- package/lib/{cli-DGVWMDT0.js → cli-6hDu8-nF.js} +314 -940
- package/lib/index.js +2 -1
- package/lib/instructions-ijuj4IP1.js +133 -0
- package/lib/playground.js +184 -9
- package/lib/prae-baseline.prae.json +102 -0
- package/lib/src-BrZXeEez.js +275 -0
- package/lib/src-Dd2_8m2g.js +153 -0
- package/lib/views-BU0sOciR.js +929 -0
- package/package.json +5 -3
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-
|
|
1
|
+
import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-6hDu8-nF.js";
|
|
2
|
+
import "./views-BU0sOciR.js";
|
|
2
3
|
|
|
3
4
|
export { applyWrite, defaultProfile, dshHome, main, planWrite, praeRow, profileAt };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { c as renderShell, o as esc, s as renderNav, t as navFor } from "./views-BU0sOciR.js";
|
|
2
|
+
|
|
3
|
+
//#region src/console/instructions.ts
|
|
4
|
+
const STYLE = `
|
|
5
|
+
.stack { padding: 22px 30px 8px; }
|
|
6
|
+
.layer {
|
|
7
|
+
display: grid; grid-template-columns: 92px 1fr 120px; gap: 16px;
|
|
8
|
+
padding: 12px 0; border-bottom: 1px solid var(--line-faint);
|
|
9
|
+
font-family: var(--proof); font-size: 12px; align-items: baseline;
|
|
10
|
+
}
|
|
11
|
+
.layer .who { letter-spacing: 0.1em; font-size: 11px; }
|
|
12
|
+
.layer .who.operator { color: var(--signal-text); }
|
|
13
|
+
.layer .who.project { color: var(--muted); }
|
|
14
|
+
.layer .file { color: var(--ink-row); overflow-wrap: anywhere; }
|
|
15
|
+
.layer .count { color: var(--muted); text-align: right; }
|
|
16
|
+
|
|
17
|
+
.proposal {
|
|
18
|
+
padding: 20px 30px; border-bottom: 1px solid var(--line);
|
|
19
|
+
display: grid; grid-template-columns: 1fr 200px; gap: 24px;
|
|
20
|
+
}
|
|
21
|
+
.proposal h3 {
|
|
22
|
+
font-size: 16px; font-weight: 600; letter-spacing: -0.01em; margin: 0 0 10px;
|
|
23
|
+
}
|
|
24
|
+
.quote {
|
|
25
|
+
border-left: 2px solid var(--line-strong); padding: 6px 0 6px 14px;
|
|
26
|
+
font-family: var(--proof); font-size: 12px; color: var(--ink-row);
|
|
27
|
+
line-height: 1.7; margin: 0 0 10px;
|
|
28
|
+
}
|
|
29
|
+
.quote .where {
|
|
30
|
+
display: block; color: var(--muted); font-size: 11px; margin-top: 6px;
|
|
31
|
+
}
|
|
32
|
+
.reading { font-size: 13px; color: var(--secondary); margin: 0 0 10px; }
|
|
33
|
+
.matcher {
|
|
34
|
+
font-family: var(--proof); font-size: 11.5px; color: var(--dim);
|
|
35
|
+
background: var(--sunk); padding: 8px 10px; border-radius: 2px;
|
|
36
|
+
overflow-wrap: anywhere;
|
|
37
|
+
}
|
|
38
|
+
.verdict-col { text-align: right; }
|
|
39
|
+
.verdict-col .kind {
|
|
40
|
+
font-family: var(--proof); font-size: 20px; letter-spacing: 0.08em;
|
|
41
|
+
font-weight: 600;
|
|
42
|
+
}
|
|
43
|
+
.verdict-col .conf {
|
|
44
|
+
font-family: var(--proof); font-size: 10px; letter-spacing: 0.12em;
|
|
45
|
+
color: var(--muted); margin-top: 6px;
|
|
46
|
+
}
|
|
47
|
+
.context-only { padding: 22px 30px 30px; }
|
|
48
|
+
.context-only ul {
|
|
49
|
+
margin: 10px 0 0; padding-left: 18px; font-size: 13px; line-height: 1.9;
|
|
50
|
+
color: var(--secondary); columns: 2; column-gap: 40px;
|
|
51
|
+
}
|
|
52
|
+
@media (max-width: 860px) {
|
|
53
|
+
.proposal { grid-template-columns: 1fr; }
|
|
54
|
+
.verdict-col { text-align: left; }
|
|
55
|
+
.context-only ul { columns: 1; }
|
|
56
|
+
}
|
|
57
|
+
@media print { .context-only ul { columns: 1; } }
|
|
58
|
+
`;
|
|
59
|
+
function renderInstructions(data, read, found) {
|
|
60
|
+
const layers = read.files.map((file) => ` <div class="layer">
|
|
61
|
+
<div class="who ${esc(file.layer)}">${esc(file.layer.toUpperCase())}</div>
|
|
62
|
+
<div class="file">${esc(file.path)}</div>
|
|
63
|
+
<div class="count">${file.sections.reduce((n, s) => n + s.statements.length, 0)} statements</div>
|
|
64
|
+
</div>`).join("\n");
|
|
65
|
+
const proposals = found.proposals.map((proposal) => ` <section class="proposal">
|
|
66
|
+
<div>
|
|
67
|
+
<h3>${esc(proposal.rule.id)} · ${esc(proposal.rule.title ?? "")}</h3>
|
|
68
|
+
<blockquote class="quote">${esc(proposal.from.text)}
|
|
69
|
+
<span class="where">${esc(proposal.from.path)}:${proposal.from.line} · ${esc(proposal.from.layer)}</span>
|
|
70
|
+
</blockquote>
|
|
71
|
+
<p class="reading">Read as: ${esc(proposal.why)}.</p>
|
|
72
|
+
<div class="matcher">${esc(JSON.stringify(proposal.rule.when))}</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="verdict-col">
|
|
75
|
+
<div class="kind e-${esc(proposal.rule.effect.kind)}">${esc(proposal.rule.effect.kind.toUpperCase())}</div>
|
|
76
|
+
<div class="conf">${esc(proposal.confidence.toUpperCase())} CONFIDENCE</div>
|
|
77
|
+
</div>
|
|
78
|
+
</section>`).join("\n");
|
|
79
|
+
const body = `<style>${STYLE}</style>
|
|
80
|
+
<section class="stats">
|
|
81
|
+
<div class="stat"><div class="v">${read.files.length}</div><div class="k">FILES</div></div>
|
|
82
|
+
<div class="stat"><div class="v">${read.statements.length}</div><div class="k">STATEMENTS</div></div>
|
|
83
|
+
<div class="stat"><div class="v hold">${found.proposals.length}</div><div class="k">PROPOSED</div></div>
|
|
84
|
+
<div class="stat"><div class="v">${found.skipped.length}</div><div class="k">CONTEXT ONLY</div></div>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
<section class="sect">
|
|
88
|
+
<h2>Precedence</h2>
|
|
89
|
+
<p>Least specific first. An operator file is named by the profile and
|
|
90
|
+
applies to every repository the agent is pointed at; a project file was
|
|
91
|
+
found by walking up from the work and applies only to the checkout that
|
|
92
|
+
shipped it.</p>
|
|
93
|
+
</section>
|
|
94
|
+
<div class="stack">
|
|
95
|
+
${layers}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<section class="sect">
|
|
99
|
+
<h2>Proposed</h2>
|
|
100
|
+
<p>Nothing here is in force. Each rule is a <em>reading</em> of the
|
|
101
|
+
sentence beside it, and the only thing that turns a reading into policy
|
|
102
|
+
is a person putting it in a pack. Read every one against its sentence
|
|
103
|
+
before it enforces anything.</p>
|
|
104
|
+
</section>
|
|
105
|
+
${found.proposals.length === 0 ? " <div class=\"empty\">Nothing was proposed. On most real instruction files that is the expected result, not a failure.</div>" : proposals}
|
|
106
|
+
|
|
107
|
+
<section class="context-only">
|
|
108
|
+
<div class="k" style="font-family:var(--proof);font-size:10px;letter-spacing:0.14em;color:var(--muted)">
|
|
109
|
+
${found.skipped.length} STATEMENTS PROPOSED NOTHING AND REMAIN CONTEXT
|
|
110
|
+
</div>
|
|
111
|
+
<p style="font-size:13px;color:var(--secondary);max-width:76ch;margin:10px 0 0">
|
|
112
|
+
Most engineering guidance is about code that gets written, not calls an
|
|
113
|
+
agent makes. These are served to the agent and sealed into the ledger;
|
|
114
|
+
they are not enforced, and pretending otherwise would be inventing
|
|
115
|
+
policy nobody wrote.
|
|
116
|
+
</p>
|
|
117
|
+
<ul>
|
|
118
|
+
${found.skipped.slice(0, 40).map((statement) => ` <li>${esc(statement.text)}</li>`).join("\n")}
|
|
119
|
+
</ul>
|
|
120
|
+
</section>`;
|
|
121
|
+
return renderShell({
|
|
122
|
+
title: "Instructions",
|
|
123
|
+
nav: renderNav(navFor(data), "Instructions"),
|
|
124
|
+
head: ` <header class="head">
|
|
125
|
+
<div><h1>Instructions</h1><div class="meta">${esc(read.hash.slice(0, 16))} · ${read.files.length} files · ${found.proposals.length} proposed, unreviewed</div></div>
|
|
126
|
+
<div class="actions"><button type="button" class="btn" onclick="window.print()">Export evidence</button></div>
|
|
127
|
+
</header>`,
|
|
128
|
+
body
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
export { renderInstructions };
|
package/lib/playground.js
CHANGED
|
@@ -1,6 +1,143 @@
|
|
|
1
1
|
(function() {
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
//#region ../kernel/src/command.ts
|
|
5
|
+
/** Wrappers whose first operand is the program actually being run. */
|
|
6
|
+
const WRAPPERS = new Set([
|
|
7
|
+
"sudo",
|
|
8
|
+
"command",
|
|
9
|
+
"env",
|
|
10
|
+
"nohup",
|
|
11
|
+
"time",
|
|
12
|
+
"xargs"
|
|
13
|
+
]);
|
|
14
|
+
/** Wrapper options that consume the next token, so it is not the program. */
|
|
15
|
+
const OPTION_WITH_VALUE = new Set([
|
|
16
|
+
"-u",
|
|
17
|
+
"-g",
|
|
18
|
+
"-p",
|
|
19
|
+
"-h",
|
|
20
|
+
"-C",
|
|
21
|
+
"-r",
|
|
22
|
+
"-t",
|
|
23
|
+
"-U",
|
|
24
|
+
"-I",
|
|
25
|
+
"-n",
|
|
26
|
+
"-P",
|
|
27
|
+
"-L",
|
|
28
|
+
"-s"
|
|
29
|
+
]);
|
|
30
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
31
|
+
/**
|
|
32
|
+
* Split a line into pipeline segments of words, one scan, quote-aware.
|
|
33
|
+
*
|
|
34
|
+
* Splitting on `|` before parsing quotes is the classic mistake — it turns
|
|
35
|
+
* `echo "a | b"` into two segments — so quoting and separators are read
|
|
36
|
+
* together.
|
|
37
|
+
*/
|
|
38
|
+
function scan(command) {
|
|
39
|
+
const parts = [];
|
|
40
|
+
let tokens = [];
|
|
41
|
+
let current = "";
|
|
42
|
+
let quote;
|
|
43
|
+
const endToken = () => {
|
|
44
|
+
if (current !== "") tokens.push(current);
|
|
45
|
+
current = "";
|
|
46
|
+
};
|
|
47
|
+
const endSegment = () => {
|
|
48
|
+
endToken();
|
|
49
|
+
if (tokens.length > 0) parts.push(tokens);
|
|
50
|
+
tokens = [];
|
|
51
|
+
};
|
|
52
|
+
for (let at = 0; at < command.length; at += 1) {
|
|
53
|
+
const ch = command[at];
|
|
54
|
+
if (quote !== void 0) {
|
|
55
|
+
if (ch === quote) quote = void 0;
|
|
56
|
+
else current += ch;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === "'" || ch === "\"") {
|
|
60
|
+
quote = ch;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === ";") {
|
|
64
|
+
endSegment();
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (ch === "|" || ch === "&") {
|
|
68
|
+
endSegment();
|
|
69
|
+
if (command[at + 1] === ch) at += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (/\s/.test(ch)) {
|
|
73
|
+
endToken();
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
current += ch;
|
|
77
|
+
}
|
|
78
|
+
endSegment();
|
|
79
|
+
return parts;
|
|
80
|
+
}
|
|
81
|
+
/** Decompose a command line into pipeline segments. */
|
|
82
|
+
function segments(command) {
|
|
83
|
+
const found = [];
|
|
84
|
+
for (const tokens of scan(command)) {
|
|
85
|
+
let at = 0;
|
|
86
|
+
while (at < tokens.length) {
|
|
87
|
+
const token = tokens[at];
|
|
88
|
+
if (ASSIGNMENT.test(token)) {
|
|
89
|
+
at += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (WRAPPERS.has(token)) {
|
|
93
|
+
at += 1;
|
|
94
|
+
while (at < tokens.length) {
|
|
95
|
+
const option = tokens[at];
|
|
96
|
+
if (!option.startsWith("-")) break;
|
|
97
|
+
at += OPTION_WITH_VALUE.has(option) ? 2 : 1;
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const name = tokens[at];
|
|
104
|
+
if (name === void 0) continue;
|
|
105
|
+
found.push({
|
|
106
|
+
name: (name.split("/").pop() ?? name).toLowerCase(),
|
|
107
|
+
tokens: tokens.slice(at + 1)
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
112
|
+
/** `git*` matches `git`, `github`; bare strings match exactly. */
|
|
113
|
+
function matches$1(pattern, value) {
|
|
114
|
+
if (pattern.endsWith("*")) return value.startsWith(pattern.slice(0, -1));
|
|
115
|
+
return value === pattern;
|
|
116
|
+
}
|
|
117
|
+
const list = (v) => v === void 0 ? [] : typeof v === "string" ? [v] : v;
|
|
118
|
+
/**
|
|
119
|
+
* Judge one command line against one matcher. Every present field must hold.
|
|
120
|
+
*/
|
|
121
|
+
function commandMatches(match, command) {
|
|
122
|
+
const parts = segments(command);
|
|
123
|
+
if (parts.length === 0) return false;
|
|
124
|
+
const names = list(match.name);
|
|
125
|
+
const args = list(match.token);
|
|
126
|
+
const pipes = list(match.pipedTo);
|
|
127
|
+
if (names.length > 0) {
|
|
128
|
+
const hit = parts.filter((segment) => names.some((pattern) => matches$1(pattern, segment.name)));
|
|
129
|
+
if (hit.length === 0) return false;
|
|
130
|
+
if (args.length > 0 && !hit.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
|
|
131
|
+
} else if (args.length > 0) {
|
|
132
|
+
if (!parts.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
|
|
133
|
+
}
|
|
134
|
+
if (pipes.length > 0) {
|
|
135
|
+
if (!parts.slice(1).some((segment) => pipes.some((pattern) => matches$1(pattern, segment.name)))) return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
//#endregion
|
|
4
141
|
//#region ../kernel/src/path.ts
|
|
5
142
|
/**
|
|
6
143
|
* Path normalization and glob containment, with no filesystem access.
|
|
@@ -92,6 +229,8 @@
|
|
|
92
229
|
function within(path, glob) {
|
|
93
230
|
const target = normalize(path);
|
|
94
231
|
if (target.escaped) return false;
|
|
232
|
+
const trimmed = glob.trim();
|
|
233
|
+
if (!(trimmed === "**" || trimmed.startsWith("**/")) && target.absolute !== trimmed.startsWith("/")) return false;
|
|
95
234
|
const pattern = normalize(glob);
|
|
96
235
|
if (matchSegments(target.segments, pattern.segments)) return true;
|
|
97
236
|
return matchSegments(target.segments, [...pattern.segments, "**"]);
|
|
@@ -123,8 +262,8 @@
|
|
|
123
262
|
* an operator writing the suffix form means the organisation, not a rule that
|
|
124
263
|
* covers every subdomain except the one they first thought of.
|
|
125
264
|
*/
|
|
126
|
-
function hostIn(host, list) {
|
|
127
|
-
return list.some((entry) => {
|
|
265
|
+
function hostIn(host, list$1) {
|
|
266
|
+
return list$1.some((entry) => {
|
|
128
267
|
const want = entry.toLowerCase();
|
|
129
268
|
if (want.startsWith(".")) return host === want.slice(1) || host.endsWith(want);
|
|
130
269
|
return host === want;
|
|
@@ -177,6 +316,11 @@
|
|
|
177
316
|
if (!listed(facts.capability, when.capability)) return false;
|
|
178
317
|
if (!listed(facts.plugin, when.plugin)) return false;
|
|
179
318
|
if (!listed(facts.agent, when.agent)) return false;
|
|
319
|
+
if (when.command !== void 0) {
|
|
320
|
+
const raw = facts.args.command ?? facts.args.cmd ?? facts.args.script;
|
|
321
|
+
if (typeof raw !== "string") return false;
|
|
322
|
+
if (!commandMatches(when.command, raw)) return false;
|
|
323
|
+
}
|
|
180
324
|
if (when.path !== void 0) {
|
|
181
325
|
const raw = facts.args[when.path.arg ?? "path"];
|
|
182
326
|
if (typeof raw !== "string") return false;
|
|
@@ -326,6 +470,32 @@
|
|
|
326
470
|
return value;
|
|
327
471
|
}
|
|
328
472
|
};
|
|
473
|
+
function readCommandMatch(check, path, value) {
|
|
474
|
+
if (!isObject(value)) {
|
|
475
|
+
check.fail(path, "expected an object");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
check.keys(path, value, [
|
|
479
|
+
"name",
|
|
480
|
+
"token",
|
|
481
|
+
"pipedTo"
|
|
482
|
+
]);
|
|
483
|
+
if (value.name === void 0 && value.token === void 0 && value.pipedTo === void 0) check.fail(path, "expected `name`, `token` or `pipedTo`; a command matcher with none matches nothing");
|
|
484
|
+
const out = {};
|
|
485
|
+
if (value.token !== void 0) {
|
|
486
|
+
const tokens = check.names(`${path}.token`, value.token);
|
|
487
|
+
if (tokens !== void 0) out.token = tokens;
|
|
488
|
+
}
|
|
489
|
+
if (value.name !== void 0) {
|
|
490
|
+
const names = check.names(`${path}.name`, value.name);
|
|
491
|
+
if (names !== void 0) out.name = names;
|
|
492
|
+
}
|
|
493
|
+
if (value.pipedTo !== void 0) {
|
|
494
|
+
const pipes = check.names(`${path}.pipedTo`, value.pipedTo);
|
|
495
|
+
if (pipes !== void 0) out.pipedTo = pipes;
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
329
499
|
function readPathMatch(check, path, value) {
|
|
330
500
|
if (!isObject(value)) {
|
|
331
501
|
check.fail(path, "expected an object");
|
|
@@ -369,12 +539,12 @@
|
|
|
369
539
|
if (arg !== void 0) out.arg = arg;
|
|
370
540
|
}
|
|
371
541
|
if (value.in !== void 0) {
|
|
372
|
-
const list = check.globs(`${path}.in`, value.in);
|
|
373
|
-
if (list !== void 0) out.in = list;
|
|
542
|
+
const list$1 = check.globs(`${path}.in`, value.in);
|
|
543
|
+
if (list$1 !== void 0) out.in = list$1;
|
|
374
544
|
}
|
|
375
545
|
if (value.notIn !== void 0) {
|
|
376
|
-
const list = check.globs(`${path}.notIn`, value.notIn);
|
|
377
|
-
if (list !== void 0) out.notIn = list;
|
|
546
|
+
const list$1 = check.globs(`${path}.notIn`, value.notIn);
|
|
547
|
+
if (list$1 !== void 0) out.notIn = list$1;
|
|
378
548
|
}
|
|
379
549
|
return out;
|
|
380
550
|
}
|
|
@@ -419,6 +589,7 @@
|
|
|
419
589
|
"capability",
|
|
420
590
|
"plugin",
|
|
421
591
|
"agent",
|
|
592
|
+
"command",
|
|
422
593
|
"path",
|
|
423
594
|
"host",
|
|
424
595
|
"arg",
|
|
@@ -434,6 +605,10 @@
|
|
|
434
605
|
const names = check.names(`${path}.${key}`, value[key]);
|
|
435
606
|
if (names !== void 0) out[key] = names;
|
|
436
607
|
}
|
|
608
|
+
if (value.command !== void 0) {
|
|
609
|
+
const match = readCommandMatch(check, `${path}.command`, value.command);
|
|
610
|
+
if (match !== void 0) out.command = match;
|
|
611
|
+
}
|
|
437
612
|
if (value.path !== void 0) {
|
|
438
613
|
const match = readPathMatch(check, `${path}.path`, value.path);
|
|
439
614
|
if (match !== void 0) out.path = match;
|
|
@@ -444,12 +619,12 @@
|
|
|
444
619
|
}
|
|
445
620
|
if (value.arg !== void 0) if (!Array.isArray(value.arg)) check.fail(`${path}.arg`, "expected an array of argument matchers");
|
|
446
621
|
else {
|
|
447
|
-
const list = [];
|
|
622
|
+
const list$1 = [];
|
|
448
623
|
value.arg.forEach((entry, index) => {
|
|
449
624
|
const one = readArgMatch(check, `${path}.arg[${index}]`, entry);
|
|
450
|
-
if (one !== void 0) list.push(one);
|
|
625
|
+
if (one !== void 0) list$1.push(one);
|
|
451
626
|
});
|
|
452
|
-
out.arg = list;
|
|
627
|
+
out.arg = list$1;
|
|
453
628
|
}
|
|
454
629
|
if (value.count !== void 0) if (!isObject(value.count)) check.fail(`${path}.count`, "expected an object");
|
|
455
630
|
else {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"prae": "0.1",
|
|
3
|
+
"pack": "prae-baseline",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"id": "BASE-001",
|
|
8
|
+
"title": "No download-and-execute",
|
|
9
|
+
"when": {
|
|
10
|
+
"command": {
|
|
11
|
+
"name": ["curl", "wget", "fetch"],
|
|
12
|
+
"pipedTo": ["sh", "bash", "zsh", "python", "python3", "node"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"effect": {
|
|
16
|
+
"kind": "deny",
|
|
17
|
+
"because": "piping a download into an interpreter runs code nobody has read"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "BASE-002",
|
|
22
|
+
"title": "Recursive force-delete is held",
|
|
23
|
+
"when": { "command": { "name": ["rm", "shred"], "token": "-rf" } },
|
|
24
|
+
"effect": {
|
|
25
|
+
"kind": "ask",
|
|
26
|
+
"because": "a recursive force-delete is not reversible and needs a human"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "BASE-003",
|
|
31
|
+
"title": "Privilege escalation is held",
|
|
32
|
+
"when": { "command": { "name": ["su", "doas", "pkexec"] } },
|
|
33
|
+
"effect": {
|
|
34
|
+
"kind": "ask",
|
|
35
|
+
"because": "escalating privileges needs a human"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "BASE-004",
|
|
40
|
+
"title": "Filesystem-wide destruction is refused",
|
|
41
|
+
"when": { "command": { "name": ["mkfs*", "dd", "diskutil", "fdisk"] } },
|
|
42
|
+
"effect": {
|
|
43
|
+
"kind": "deny",
|
|
44
|
+
"because": "block-device and filesystem tools are not granted to agents"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "BASE-005",
|
|
49
|
+
"title": "Shell history and credentials stay unread",
|
|
50
|
+
"when": {
|
|
51
|
+
"capability": "fs:read",
|
|
52
|
+
"path": {
|
|
53
|
+
"inside": [
|
|
54
|
+
".env",
|
|
55
|
+
".env.*",
|
|
56
|
+
"**/.ssh/**",
|
|
57
|
+
"**/.aws/**",
|
|
58
|
+
"**/credentials*",
|
|
59
|
+
"**/*.pem",
|
|
60
|
+
"**/*_history"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"effect": {
|
|
65
|
+
"kind": "deny",
|
|
66
|
+
"because": "secrets and credential stores are out of scope for agents"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "BASE-006",
|
|
71
|
+
"title": "Writes outside the workspace are held",
|
|
72
|
+
"when": { "capability": "fs:write", "path": { "outside": ["./**"] } },
|
|
73
|
+
"effect": {
|
|
74
|
+
"kind": "ask",
|
|
75
|
+
"because": "writing outside the working tree needs a human"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "BASE-007",
|
|
80
|
+
"title": "Force-push is held",
|
|
81
|
+
"when": { "command": { "name": "git", "token": "--force*" } },
|
|
82
|
+
"effect": {
|
|
83
|
+
"kind": "ask",
|
|
84
|
+
"because": "rewriting shared history needs a human"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "BASE-008",
|
|
89
|
+
"title": "Package publishes are held",
|
|
90
|
+
"when": {
|
|
91
|
+
"command": {
|
|
92
|
+
"name": ["npm", "pnpm", "yarn", "pip", "cargo", "gem"],
|
|
93
|
+
"token": "publish"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"effect": {
|
|
97
|
+
"kind": "ask",
|
|
98
|
+
"because": "publishing is outward-facing and needs a human"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|