specpi 0.10.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/CHANGELOG.md +150 -0
- package/LICENSE +21 -0
- package/NPM_RELEASE.md +110 -0
- package/README.md +155 -0
- package/SECURITY.md +85 -0
- package/SECURITY_MODEL.md +107 -0
- package/THIRD_PARTY.md +61 -0
- package/browser-runtime/package-lock.json +86 -0
- package/browser-runtime/package.json +15 -0
- package/extensions/browser/core.mjs +306 -0
- package/extensions/browser/index.ts +723 -0
- package/extensions/browser/smoke.mjs +47 -0
- package/extensions/command-guard/bash.mjs +1426 -0
- package/extensions/command-guard/cmd.mjs +369 -0
- package/extensions/command-guard/core.mjs +506 -0
- package/extensions/command-guard/index.ts +634 -0
- package/extensions/command-guard/managed-files.mjs +22 -0
- package/extensions/command-guard/paths.mjs +398 -0
- package/extensions/command-guard/powershell-parser.ps1 +47 -0
- package/extensions/command-guard/powershell.mjs +655 -0
- package/extensions/command-guard/redact.mjs +65 -0
- package/extensions/command-guard/rules.mjs +2557 -0
- package/extensions/command-guard/smoke.mjs +422 -0
- package/extensions/files/core.mjs +422 -0
- package/extensions/files/index.ts +678 -0
- package/extensions/spec/core.mjs +47 -0
- package/extensions/spec.ts +457 -0
- package/extensions/tool-wishlist/capabilities.json +114 -0
- package/extensions/tool-wishlist/core.mjs +1525 -0
- package/extensions/tool-wishlist/index.ts +804 -0
- package/extensions/tool-wishlist/registry.mjs +99 -0
- package/extensions/tool-wishlist/validators.mjs +345 -0
- package/extensions/ui-refresh/index.ts +54 -0
- package/extensions/workflow-controls/challenge.mjs +196 -0
- package/extensions/workflow-controls/experiments.mjs +628 -0
- package/extensions/workflow-controls/index.ts +1144 -0
- package/extensions/workflow-controls/scope.mjs +272 -0
- package/extensions/workflow-controls/smoke.mjs +201 -0
- package/package.json +98 -0
- package/scripts/check-package.mjs +483 -0
- package/scripts/check-pi-package.mjs +223 -0
- package/scripts/check-release-order.mjs +97 -0
- package/scripts/lib.mjs +182 -0
- package/scripts/lock.mjs +122 -0
- package/scripts/specpi.mjs +2037 -0
- package/scripts/verify-artifact.mjs +21 -0
- package/shell/pi-profiles.sh +14 -0
- package/site/logo.svg +9 -0
- package/site/self-improvement-loop-v2.svg +108 -0
- package/skills/donsetch/SKILL.md +76 -0
- package/skills/specpi-improve/SKILL.md +54 -0
- package/specpi +4 -0
- package/specpi.cmd +4 -0
- package/templates/AGENTS.md +23 -0
- package/templates/settings.json +10 -0
- package/themes/specpi-spec.json +96 -0
- package/themes/tea-house.json +89 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { decideCommand, decidePath } from "./core.mjs";
|
|
5
|
+
|
|
6
|
+
export async function runCommandGuardSmoke() {
|
|
7
|
+
const checks = [
|
|
8
|
+
[
|
|
9
|
+
"safe listing",
|
|
10
|
+
decideCommand("printf '%s\\n' hello", { shell: "bash", mode: "guard", cwd: process.cwd(), hasUI: false }),
|
|
11
|
+
"allow",
|
|
12
|
+
],
|
|
13
|
+
[
|
|
14
|
+
"root deletion",
|
|
15
|
+
decideCommand("rm -rf /", { shell: "bash", mode: "guard", cwd: process.cwd(), hasUI: false }),
|
|
16
|
+
"deny",
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
"malformed quote",
|
|
20
|
+
decideCommand("printf 'unterminated", { shell: "bash", mode: "guard", cwd: process.cwd(), hasUI: false }),
|
|
21
|
+
"deny",
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
"protected write",
|
|
25
|
+
decidePath("/etc/passwd", "write", { mode: "guard", cwd: "/work", platform: "linux", hasUI: false }),
|
|
26
|
+
"deny",
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
"wrapped root deletion",
|
|
30
|
+
decideCommand("sudo -u root bash -lc 'rm -rf /*'", {
|
|
31
|
+
shell: "bash",
|
|
32
|
+
mode: "guard",
|
|
33
|
+
cwd: process.cwd(),
|
|
34
|
+
hasUI: true,
|
|
35
|
+
}),
|
|
36
|
+
"deny",
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
"nested fork bomb",
|
|
40
|
+
decideCommand("bash -c ':(){ :|:& };:'", { shell: "bash", mode: "guard", cwd: process.cwd(), hasUI: true }),
|
|
41
|
+
"deny",
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"wrapped executable heredoc",
|
|
45
|
+
decideCommand("env sh <<EOF\nrm -rf /\nEOF", {
|
|
46
|
+
shell: "bash",
|
|
47
|
+
mode: "guard",
|
|
48
|
+
cwd: process.cwd(),
|
|
49
|
+
hasUI: true,
|
|
50
|
+
}),
|
|
51
|
+
"deny",
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"decoded catastrophic interpreter pipe",
|
|
55
|
+
decideCommand("printf cm0gLXJmIC8K | base64 -d | sh", {
|
|
56
|
+
shell: "bash",
|
|
57
|
+
mode: "guard",
|
|
58
|
+
cwd: process.cwd(),
|
|
59
|
+
hasUI: true,
|
|
60
|
+
}),
|
|
61
|
+
"deny",
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"decoded benign interpreter pipe",
|
|
65
|
+
decideCommand("printf cHJpbnRmIHNhZmU= | base64 -d | sh", {
|
|
66
|
+
shell: "bash",
|
|
67
|
+
mode: "guard",
|
|
68
|
+
cwd: process.cwd(),
|
|
69
|
+
hasUI: true,
|
|
70
|
+
}),
|
|
71
|
+
"allow",
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
"sed shell escape",
|
|
75
|
+
decideCommand("sed '1e rm -rf /' /etc/hosts", {
|
|
76
|
+
shell: "bash",
|
|
77
|
+
mode: "guard",
|
|
78
|
+
cwd: process.cwd(),
|
|
79
|
+
hasUI: true,
|
|
80
|
+
}),
|
|
81
|
+
"deny",
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
"xargs input uncertainty",
|
|
85
|
+
decideCommand("printf / | xargs rm -rf", { shell: "bash", mode: "guard", cwd: process.cwd(), hasUI: true }),
|
|
86
|
+
"ask",
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
"Bash cmd cleanup syntax mismatch",
|
|
90
|
+
decideCommand("rmdir /s /q F:\\Temp\\case", {
|
|
91
|
+
shell: "bash",
|
|
92
|
+
mode: "guard",
|
|
93
|
+
cwd: "C:\\work",
|
|
94
|
+
platform: "win32",
|
|
95
|
+
hasUI: true,
|
|
96
|
+
}),
|
|
97
|
+
"deny",
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
"Bash ordinary temporary cleanup",
|
|
101
|
+
decideCommand("rm -rf -- F:/Temp/case", {
|
|
102
|
+
shell: "bash",
|
|
103
|
+
mode: "guard",
|
|
104
|
+
cwd: "C:\\work",
|
|
105
|
+
platform: "win32",
|
|
106
|
+
hasUI: true,
|
|
107
|
+
}),
|
|
108
|
+
"allow",
|
|
109
|
+
],
|
|
110
|
+
[
|
|
111
|
+
"nested cmd deletion",
|
|
112
|
+
decideCommand('cmd /c "cmd /c rd /s /q C:\\\\Windows"', {
|
|
113
|
+
shell: "cmd",
|
|
114
|
+
mode: "guard",
|
|
115
|
+
cwd: "C:\\\\work",
|
|
116
|
+
platform: "win32",
|
|
117
|
+
hasUI: true,
|
|
118
|
+
}),
|
|
119
|
+
"deny",
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
"attached cmd deletion",
|
|
123
|
+
decideCommand('cmd /c"rd /s /q C:/Windows"', {
|
|
124
|
+
shell: "cmd",
|
|
125
|
+
mode: "guard",
|
|
126
|
+
cwd: "C:\\work",
|
|
127
|
+
platform: "win32",
|
|
128
|
+
hasUI: true,
|
|
129
|
+
}),
|
|
130
|
+
"deny",
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
"quoted cmd metacharacters",
|
|
134
|
+
decideCommand('cmd /c echo "safe & rd /s /q C:/Windows"', {
|
|
135
|
+
shell: "cmd",
|
|
136
|
+
mode: "guard",
|
|
137
|
+
cwd: "C:\\work",
|
|
138
|
+
platform: "win32",
|
|
139
|
+
hasUI: true,
|
|
140
|
+
}),
|
|
141
|
+
"allow",
|
|
142
|
+
],
|
|
143
|
+
[
|
|
144
|
+
"START ignores later host-looking data",
|
|
145
|
+
decideCommand("start cmd /c echo powershell.exe -Command Remove-Item -Recurse -Force C:/Windows", {
|
|
146
|
+
shell: "cmd",
|
|
147
|
+
mode: "guard",
|
|
148
|
+
cwd: "C:\\work",
|
|
149
|
+
platform: "win32",
|
|
150
|
+
hasUI: true,
|
|
151
|
+
}),
|
|
152
|
+
"allow",
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
"Bash to cmd PowerShell deletion",
|
|
156
|
+
decideCommand("cmd /c powershell.exe -Command Remove-Item -Recurse -Force C:/Windows", {
|
|
157
|
+
shell: "bash",
|
|
158
|
+
mode: "guard",
|
|
159
|
+
cwd: "C:\\work",
|
|
160
|
+
platform: "win32",
|
|
161
|
+
hasUI: false,
|
|
162
|
+
}),
|
|
163
|
+
"deny",
|
|
164
|
+
],
|
|
165
|
+
[
|
|
166
|
+
"implicit PowerShell host",
|
|
167
|
+
decideCommand("cmd /c powershell.exe -NoProfile", {
|
|
168
|
+
shell: "cmd",
|
|
169
|
+
mode: "guard",
|
|
170
|
+
cwd: "C:\\work",
|
|
171
|
+
platform: "win32",
|
|
172
|
+
hasUI: true,
|
|
173
|
+
}),
|
|
174
|
+
"ask",
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
"strict workspace mutation without UI",
|
|
178
|
+
decidePath("file.txt", "write", { mode: "strict", cwd: process.cwd(), hasUI: false }),
|
|
179
|
+
"deny",
|
|
180
|
+
],
|
|
181
|
+
[
|
|
182
|
+
"guard package install",
|
|
183
|
+
decideCommand("npm install lodash", { shell: "bash", mode: "guard", cwd: process.cwd(), hasUI: true }),
|
|
184
|
+
"allow",
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
"strict package install",
|
|
188
|
+
decideCommand("npm install lodash", { shell: "bash", mode: "strict", cwd: process.cwd(), hasUI: true }),
|
|
189
|
+
"ask",
|
|
190
|
+
],
|
|
191
|
+
[
|
|
192
|
+
"guard force push",
|
|
193
|
+
decideCommand("git push --force", {
|
|
194
|
+
shell: "bash",
|
|
195
|
+
mode: "guard",
|
|
196
|
+
cwd: process.cwd(),
|
|
197
|
+
platform: "linux",
|
|
198
|
+
hasUI: true,
|
|
199
|
+
}),
|
|
200
|
+
"ask",
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
"strict force push",
|
|
204
|
+
decideCommand("git push --force", {
|
|
205
|
+
shell: "bash",
|
|
206
|
+
mode: "strict",
|
|
207
|
+
cwd: process.cwd(),
|
|
208
|
+
platform: "linux",
|
|
209
|
+
hasUI: true,
|
|
210
|
+
}),
|
|
211
|
+
"ask",
|
|
212
|
+
],
|
|
213
|
+
[
|
|
214
|
+
"guard ordinary push",
|
|
215
|
+
decideCommand("git push origin main", {
|
|
216
|
+
shell: "bash",
|
|
217
|
+
mode: "guard",
|
|
218
|
+
cwd: process.cwd(),
|
|
219
|
+
platform: "linux",
|
|
220
|
+
hasUI: true,
|
|
221
|
+
}),
|
|
222
|
+
"allow",
|
|
223
|
+
],
|
|
224
|
+
[
|
|
225
|
+
"guard destructive git",
|
|
226
|
+
decideCommand("git reset --hard", {
|
|
227
|
+
shell: "bash",
|
|
228
|
+
mode: "guard",
|
|
229
|
+
cwd: process.cwd(),
|
|
230
|
+
platform: "linux",
|
|
231
|
+
hasUI: true,
|
|
232
|
+
}),
|
|
233
|
+
"ask",
|
|
234
|
+
],
|
|
235
|
+
[
|
|
236
|
+
"guard remote ref deletion",
|
|
237
|
+
decideCommand("git push --delete origin topic", {
|
|
238
|
+
shell: "bash",
|
|
239
|
+
mode: "guard",
|
|
240
|
+
cwd: process.cwd(),
|
|
241
|
+
platform: "linux",
|
|
242
|
+
hasUI: true,
|
|
243
|
+
}),
|
|
244
|
+
"ask",
|
|
245
|
+
],
|
|
246
|
+
[
|
|
247
|
+
"guard bounded system-tree write",
|
|
248
|
+
decidePath("/etc/unused-review-note", "write", {
|
|
249
|
+
mode: "guard",
|
|
250
|
+
cwd: "/work",
|
|
251
|
+
platform: "linux",
|
|
252
|
+
hasUI: true,
|
|
253
|
+
}),
|
|
254
|
+
"allow",
|
|
255
|
+
],
|
|
256
|
+
[
|
|
257
|
+
"strict bounded system-tree write",
|
|
258
|
+
decidePath("/etc/unused-review-note", "write", {
|
|
259
|
+
mode: "strict",
|
|
260
|
+
cwd: "/work",
|
|
261
|
+
platform: "linux",
|
|
262
|
+
hasUI: true,
|
|
263
|
+
}),
|
|
264
|
+
"ask",
|
|
265
|
+
],
|
|
266
|
+
[
|
|
267
|
+
"argv-prefix runner laundering",
|
|
268
|
+
decideCommand("setsid rm -rf /etc", {
|
|
269
|
+
shell: "bash",
|
|
270
|
+
mode: "guard",
|
|
271
|
+
cwd: "/home/pi/work",
|
|
272
|
+
platform: "linux",
|
|
273
|
+
hasUI: true,
|
|
274
|
+
}),
|
|
275
|
+
"deny",
|
|
276
|
+
],
|
|
277
|
+
[
|
|
278
|
+
"command-string runner laundering",
|
|
279
|
+
decideCommand("watch 'rm -rf /etc'", {
|
|
280
|
+
shell: "bash",
|
|
281
|
+
mode: "guard",
|
|
282
|
+
cwd: "/home/pi/work",
|
|
283
|
+
platform: "linux",
|
|
284
|
+
hasUI: true,
|
|
285
|
+
}),
|
|
286
|
+
"deny",
|
|
287
|
+
],
|
|
288
|
+
[
|
|
289
|
+
"guard environment enumeration",
|
|
290
|
+
decideCommand("printenv", {
|
|
291
|
+
shell: "bash",
|
|
292
|
+
mode: "guard",
|
|
293
|
+
cwd: "/home/pi/work",
|
|
294
|
+
platform: "linux",
|
|
295
|
+
hasUI: true,
|
|
296
|
+
}),
|
|
297
|
+
"allow",
|
|
298
|
+
],
|
|
299
|
+
[
|
|
300
|
+
"strict environment enumeration",
|
|
301
|
+
decideCommand("printenv", {
|
|
302
|
+
shell: "bash",
|
|
303
|
+
mode: "strict",
|
|
304
|
+
cwd: "/home/pi/work",
|
|
305
|
+
platform: "linux",
|
|
306
|
+
hasUI: true,
|
|
307
|
+
}),
|
|
308
|
+
"ask",
|
|
309
|
+
],
|
|
310
|
+
[
|
|
311
|
+
"guard process environment pseudo-file",
|
|
312
|
+
decideCommand("cat /proc/self/environ", {
|
|
313
|
+
shell: "bash",
|
|
314
|
+
mode: "guard",
|
|
315
|
+
cwd: "/home/pi/work",
|
|
316
|
+
platform: "linux",
|
|
317
|
+
hasUI: true,
|
|
318
|
+
}),
|
|
319
|
+
"allow",
|
|
320
|
+
],
|
|
321
|
+
[
|
|
322
|
+
"strict process environment pseudo-file",
|
|
323
|
+
decideCommand("cat /proc/self/environ", {
|
|
324
|
+
shell: "bash",
|
|
325
|
+
mode: "strict",
|
|
326
|
+
cwd: "/home/pi/work",
|
|
327
|
+
platform: "linux",
|
|
328
|
+
hasUI: true,
|
|
329
|
+
}),
|
|
330
|
+
"ask",
|
|
331
|
+
],
|
|
332
|
+
[
|
|
333
|
+
"strict ordinary cat",
|
|
334
|
+
decideCommand("cat README.md", { shell: "bash", mode: "strict", cwd: process.cwd(), hasUI: true }),
|
|
335
|
+
"allow",
|
|
336
|
+
],
|
|
337
|
+
[
|
|
338
|
+
"critical survives leaf limit",
|
|
339
|
+
decideCommand(`rm -rf /; ${Array.from({ length: 140 }, () => "true").join(";")}`, {
|
|
340
|
+
shell: "bash",
|
|
341
|
+
mode: "guard",
|
|
342
|
+
cwd: process.cwd(),
|
|
343
|
+
hasUI: true,
|
|
344
|
+
}),
|
|
345
|
+
"deny",
|
|
346
|
+
],
|
|
347
|
+
[
|
|
348
|
+
"macOS system root",
|
|
349
|
+
decidePath("/Library/LaunchDaemons/x.plist", "write", {
|
|
350
|
+
mode: "guard",
|
|
351
|
+
cwd: "/Users/alice/work",
|
|
352
|
+
platform: "darwin",
|
|
353
|
+
hasUI: true,
|
|
354
|
+
}),
|
|
355
|
+
"deny",
|
|
356
|
+
],
|
|
357
|
+
[
|
|
358
|
+
"ordinary awk pipeline",
|
|
359
|
+
decideCommand("printf 'a b\\n' | awk '{print $1}'", {
|
|
360
|
+
shell: "bash",
|
|
361
|
+
mode: "guard",
|
|
362
|
+
cwd: "/home/pi/work",
|
|
363
|
+
platform: "linux",
|
|
364
|
+
hasUI: true,
|
|
365
|
+
}),
|
|
366
|
+
"allow",
|
|
367
|
+
],
|
|
368
|
+
// hasUI:false so the check is deterministic: a host with a PowerShell parser denies this critically, one
|
|
369
|
+
// without it can only reach "unresolved", and both must refuse without an approval prompt.
|
|
370
|
+
[
|
|
371
|
+
"abbreviated encoded command",
|
|
372
|
+
decideCommand(
|
|
373
|
+
`powershell.exe -enc ${Buffer.from("Remove-Item -Recurse -Force C:\\Windows", "utf16le").toString("base64")}`,
|
|
374
|
+
{ shell: "bash", mode: "guard", cwd: "C:\\work", platform: "win32", hasUI: false },
|
|
375
|
+
),
|
|
376
|
+
"deny",
|
|
377
|
+
],
|
|
378
|
+
[
|
|
379
|
+
"plain find is read-only",
|
|
380
|
+
decideCommand("find src -type f -print", {
|
|
381
|
+
shell: "bash",
|
|
382
|
+
mode: "guard",
|
|
383
|
+
cwd: "/home/pi/work",
|
|
384
|
+
platform: "linux",
|
|
385
|
+
hasUI: true,
|
|
386
|
+
}),
|
|
387
|
+
"allow",
|
|
388
|
+
],
|
|
389
|
+
[
|
|
390
|
+
"find deletion on a protected root",
|
|
391
|
+
decideCommand("find /etc -delete", {
|
|
392
|
+
shell: "bash",
|
|
393
|
+
mode: "guard",
|
|
394
|
+
cwd: "/home/pi/work",
|
|
395
|
+
platform: "linux",
|
|
396
|
+
hasUI: true,
|
|
397
|
+
}),
|
|
398
|
+
"deny",
|
|
399
|
+
],
|
|
400
|
+
];
|
|
401
|
+
for (const [name, result, expected] of checks) {
|
|
402
|
+
if (result.action !== expected) {
|
|
403
|
+
throw new Error(`${name} expected ${expected}, got ${result.action}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return `command-guard-smoke passed: ${checks.length} inert policy checks`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const invokedDirectly =
|
|
411
|
+
Boolean(process.argv[1]) &&
|
|
412
|
+
(process.platform === "win32"
|
|
413
|
+
? path.resolve(process.argv[1]).toLowerCase() === fileURLToPath(import.meta.url).toLowerCase()
|
|
414
|
+
: path.resolve(process.argv[1]) === fileURLToPath(import.meta.url));
|
|
415
|
+
if (invokedDirectly) {
|
|
416
|
+
runCommandGuardSmoke()
|
|
417
|
+
.then((message) => console.log(message))
|
|
418
|
+
.catch((error) => {
|
|
419
|
+
console.error(`command-guard-smoke failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
420
|
+
process.exitCode = 1;
|
|
421
|
+
});
|
|
422
|
+
}
|