@theokit/sdk-tools 0.2.0 → 0.3.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 +6 -0
- package/README.md +2 -0
- package/dist/index.cjs +85 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -14
- package/dist/index.d.ts +25 -14
- package/dist/index.js +85 -117
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/dist/index.d.cts
CHANGED
|
@@ -269,17 +269,28 @@ declare function buildEnvContext(cwd: string): string;
|
|
|
269
269
|
declare function buildRepoMap(cwd: string, opts?: RepoMapOptions): string;
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
|
-
* Catastrophic-command guardrail for `shell_exec` (M3-2).
|
|
273
|
-
*
|
|
274
|
-
* `catastrophicShellReason` is a pure, segment-aware deny-list
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
272
|
+
* Catastrophic-command guardrail for `shell_exec` (M3-2; hardened V3-1).
|
|
273
|
+
*
|
|
274
|
+
* `catastrophicShellReason` is a pure, segment-aware deny-list ported from
|
|
275
|
+
* theocode's security-reviewed `shell-guard.ts` (the proven spec: 42-blocked +
|
|
276
|
+
* 24-allowed corpus, 0 misses / 0 false-positives). It splits a command on shell
|
|
277
|
+
* separators (`;`, `&&`, `||`, `|`, `&`, newline), inspects EVERY segment (so a
|
|
278
|
+
* chained `rm -rf <safe>; rm -rf /` cannot hide), and matches at COMMAND POSITION
|
|
279
|
+
* (the executable, not an arbitrary substring) so a mention like `echo "rm -rf /"`
|
|
280
|
+
* is not over-blocked. Returns a human reason for the first catastrophic segment,
|
|
281
|
+
* else `null`.
|
|
282
|
+
*
|
|
283
|
+
* Categories: recursive-force `rm` of an absolute/home/parent path; destructive git
|
|
284
|
+
* (force-push, `reset --hard`, `clean -fd`); remote-code-execution (curl/wget piped
|
|
285
|
+
* OR command-substitution `$( )` / `<( )` / eval / source); disk/raw-device wipe
|
|
286
|
+
* (mkfs, `dd of=/dev/`, `truncate /dev/`, `> /dev/<blockdev>`); recursive chmod/chown
|
|
287
|
+
* of a root path (SDK extra); fork bomb; `find -delete` / `-exec rm`; secret-file
|
|
288
|
+
* exfiltration over the network.
|
|
289
|
+
*
|
|
290
|
+
* This is a heuristic GUARDRAIL, NOT a sandbox: it is bypassable by deep obfuscation
|
|
291
|
+
* (base64/env-indirection) and is best-effort. POSIX `/bin/sh` only; Windows
|
|
292
|
+
* PowerShell is out of scope. True isolation needs a container.
|
|
293
|
+
* referencia: .claude/knowledge-base/references/theocode-shell-guard/server-lib/shell-guard.ts
|
|
283
294
|
*/
|
|
284
295
|
|
|
285
296
|
/** Thrown / reported when a command matches the catastrophic deny-list. */
|
|
@@ -288,10 +299,10 @@ declare class CatastrophicCommandError extends ConfigurationError {
|
|
|
288
299
|
constructor(reason: string);
|
|
289
300
|
}
|
|
290
301
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
302
|
+
* Return a human-readable reason when `command` is catastrophic/irreversible, or `null`.
|
|
303
|
+
* The message is surfaced to the model so it self-corrects.
|
|
293
304
|
*/
|
|
294
|
-
declare function catastrophicShellReason(
|
|
305
|
+
declare function catastrophicShellReason(command: string): string | null;
|
|
295
306
|
|
|
296
307
|
/**
|
|
297
308
|
* ACI (Agent-Computer Interface) helpers for tools (M3-5).
|
package/dist/index.d.ts
CHANGED
|
@@ -269,17 +269,28 @@ declare function buildEnvContext(cwd: string): string;
|
|
|
269
269
|
declare function buildRepoMap(cwd: string, opts?: RepoMapOptions): string;
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
|
-
* Catastrophic-command guardrail for `shell_exec` (M3-2).
|
|
273
|
-
*
|
|
274
|
-
* `catastrophicShellReason` is a pure, segment-aware deny-list
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
272
|
+
* Catastrophic-command guardrail for `shell_exec` (M3-2; hardened V3-1).
|
|
273
|
+
*
|
|
274
|
+
* `catastrophicShellReason` is a pure, segment-aware deny-list ported from
|
|
275
|
+
* theocode's security-reviewed `shell-guard.ts` (the proven spec: 42-blocked +
|
|
276
|
+
* 24-allowed corpus, 0 misses / 0 false-positives). It splits a command on shell
|
|
277
|
+
* separators (`;`, `&&`, `||`, `|`, `&`, newline), inspects EVERY segment (so a
|
|
278
|
+
* chained `rm -rf <safe>; rm -rf /` cannot hide), and matches at COMMAND POSITION
|
|
279
|
+
* (the executable, not an arbitrary substring) so a mention like `echo "rm -rf /"`
|
|
280
|
+
* is not over-blocked. Returns a human reason for the first catastrophic segment,
|
|
281
|
+
* else `null`.
|
|
282
|
+
*
|
|
283
|
+
* Categories: recursive-force `rm` of an absolute/home/parent path; destructive git
|
|
284
|
+
* (force-push, `reset --hard`, `clean -fd`); remote-code-execution (curl/wget piped
|
|
285
|
+
* OR command-substitution `$( )` / `<( )` / eval / source); disk/raw-device wipe
|
|
286
|
+
* (mkfs, `dd of=/dev/`, `truncate /dev/`, `> /dev/<blockdev>`); recursive chmod/chown
|
|
287
|
+
* of a root path (SDK extra); fork bomb; `find -delete` / `-exec rm`; secret-file
|
|
288
|
+
* exfiltration over the network.
|
|
289
|
+
*
|
|
290
|
+
* This is a heuristic GUARDRAIL, NOT a sandbox: it is bypassable by deep obfuscation
|
|
291
|
+
* (base64/env-indirection) and is best-effort. POSIX `/bin/sh` only; Windows
|
|
292
|
+
* PowerShell is out of scope. True isolation needs a container.
|
|
293
|
+
* referencia: .claude/knowledge-base/references/theocode-shell-guard/server-lib/shell-guard.ts
|
|
283
294
|
*/
|
|
284
295
|
|
|
285
296
|
/** Thrown / reported when a command matches the catastrophic deny-list. */
|
|
@@ -288,10 +299,10 @@ declare class CatastrophicCommandError extends ConfigurationError {
|
|
|
288
299
|
constructor(reason: string);
|
|
289
300
|
}
|
|
290
301
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
302
|
+
* Return a human-readable reason when `command` is catastrophic/irreversible, or `null`.
|
|
303
|
+
* The message is surfaced to the model so it self-corrects.
|
|
293
304
|
*/
|
|
294
|
-
declare function catastrophicShellReason(
|
|
305
|
+
declare function catastrophicShellReason(command: string): string | null;
|
|
295
306
|
|
|
296
307
|
/**
|
|
297
308
|
* ACI (Agent-Computer Interface) helpers for tools (M3-5).
|
package/dist/index.js
CHANGED
|
@@ -94,8 +94,8 @@ function isForbiddenPath(input) {
|
|
|
94
94
|
if (first === ".git") return true;
|
|
95
95
|
if (first === "node_modules") return true;
|
|
96
96
|
if (first === ".theo") return true;
|
|
97
|
-
const
|
|
98
|
-
if (LOCK_FILES.has(
|
|
97
|
+
const basename = segments[segments.length - 1];
|
|
98
|
+
if (LOCK_FILES.has(basename)) return true;
|
|
99
99
|
return false;
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -599,128 +599,96 @@ var CatastrophicCommandError = class extends ConfigurationError {
|
|
|
599
599
|
});
|
|
600
600
|
}
|
|
601
601
|
};
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
"
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const a = t[0];
|
|
624
|
-
const b = t[t.length - 1];
|
|
625
|
-
if (a === '"' && b === '"' || a === "'" && b === "'") return t.slice(1, -1);
|
|
626
|
-
}
|
|
627
|
-
return t;
|
|
628
|
-
}
|
|
629
|
-
function tokenize(s) {
|
|
630
|
-
return s.trim().split(/\s+/).filter(Boolean);
|
|
631
|
-
}
|
|
632
|
-
function splitSegments(cmd) {
|
|
633
|
-
return cmd.split(/&&|\|\||;|\|/);
|
|
634
|
-
}
|
|
635
|
-
function stripPrefixTokens(tokens) {
|
|
636
|
-
let t = tokens;
|
|
637
|
-
let head = t[0];
|
|
638
|
-
while (head !== void 0 && PREFIX_TOKENS.has(basename(unquote(head)))) {
|
|
639
|
-
t = t.slice(1);
|
|
640
|
-
head = t[0];
|
|
641
|
-
}
|
|
642
|
-
return t;
|
|
643
|
-
}
|
|
644
|
-
function operandsOf(tokens) {
|
|
645
|
-
return tokens.slice(1).filter((t) => !t.startsWith("-")).map(unquote);
|
|
646
|
-
}
|
|
647
|
-
var HOME_VAR = /^\$\{?HOME\}?$/;
|
|
648
|
-
function isRootishPath(op) {
|
|
649
|
-
if (op === "~" || op === "*" || op === "." || HOME_VAR.test(op)) return true;
|
|
650
|
-
let collapsed = op.replace(/\/+/g, "/");
|
|
651
|
-
if (collapsed.length > 1 && collapsed.endsWith("/")) collapsed = collapsed.slice(0, -1);
|
|
652
|
-
if (collapsed === "/" || collapsed === "/*" || collapsed === "/.") return true;
|
|
653
|
-
return SYSTEM_DIR.test(collapsed);
|
|
654
|
-
}
|
|
655
|
-
function hasRecursiveForce(tokens) {
|
|
656
|
-
const flags = tokens.slice(1).filter((t) => t.startsWith("-"));
|
|
657
|
-
const recursive = flags.some(
|
|
658
|
-
(f) => f === "--recursive" || !f.startsWith("--") && /[rR]/.test(f)
|
|
659
|
-
);
|
|
660
|
-
const force = flags.some((f) => f === "--force" || !f.startsWith("--") && f.includes("f"));
|
|
602
|
+
function unquote(token) {
|
|
603
|
+
return token.replace(/^(['"])(.*)\1$/, "$2").replace(/^['"]|['"]$/g, "");
|
|
604
|
+
}
|
|
605
|
+
function commandSegments(command) {
|
|
606
|
+
return command.split(/&&|\|\||[;|&\n]/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
607
|
+
}
|
|
608
|
+
function commandArgs(segment, name) {
|
|
609
|
+
const tokens = segment.split(/\s+/);
|
|
610
|
+
let i = 0;
|
|
611
|
+
if (tokens[i] === "sudo") i += 1;
|
|
612
|
+
if (tokens[i] !== name) return null;
|
|
613
|
+
return tokens.slice(i + 1);
|
|
614
|
+
}
|
|
615
|
+
function commandSegmentsNamed(command, name) {
|
|
616
|
+
return commandSegments(command).map((s) => commandArgs(s, name)).filter((args) => args !== null);
|
|
617
|
+
}
|
|
618
|
+
function isRecursiveForce(args) {
|
|
619
|
+
const flags = args.filter((t) => t.startsWith("-")).join(" ");
|
|
620
|
+
if (flags.length === 0) return false;
|
|
621
|
+
const recursive = /-[a-z]*r/i.test(flags) || /--recursive/.test(flags);
|
|
622
|
+
const force = /-[a-z]*f/i.test(flags) || /--force/.test(flags);
|
|
661
623
|
return recursive && force;
|
|
662
624
|
}
|
|
663
|
-
function
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
625
|
+
function isRecursive(args) {
|
|
626
|
+
const flags = args.filter((t) => t.startsWith("-")).join(" ");
|
|
627
|
+
return /-[a-z]*r/i.test(flags) || /--recursive/.test(flags);
|
|
628
|
+
}
|
|
629
|
+
var SAFE_ABSOLUTE_TARGET = /^\/(tmp|var\/tmp)(\/|$)/;
|
|
630
|
+
function targetsDangerousPath(args) {
|
|
631
|
+
const targets = args.filter((token) => token.length > 0 && !token.startsWith("-")).map(unquote);
|
|
632
|
+
return targets.some((raw) => {
|
|
633
|
+
const t = raw.replace(/\/+/g, "/");
|
|
634
|
+
if (t === "/dev/null" || SAFE_ABSOLUTE_TARGET.test(t)) return false;
|
|
635
|
+
return /^\/($|\*)/.test(t) || // "/" or "/*"
|
|
636
|
+
/^\/[^/]/.test(t) || // an absolute path like /etc, /usr/local, /home/user/x
|
|
637
|
+
t === "~" || t.startsWith("~/") || /\$\{?HOME\b\}?/.test(t) || // $HOME or ${HOME}
|
|
638
|
+
t === ".." || t.startsWith("../") || t.includes("/..") || t === "*";
|
|
639
|
+
});
|
|
667
640
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
var ddCheck = (cmd0, tokens) => {
|
|
689
|
-
if (cmd0 !== "dd") return null;
|
|
690
|
-
return tokens.some((t) => unquote(t).startsWith("of=/dev/")) ? "dd writing to a device" : null;
|
|
691
|
-
};
|
|
692
|
-
var gitForceCheck = (cmd0, tokens) => {
|
|
693
|
-
if (cmd0 !== "git" || !tokens.includes("push") || tokens.includes("--force-with-lease")) {
|
|
694
|
-
return null;
|
|
641
|
+
var DEVICE_WIPE = [
|
|
642
|
+
/\bmkfs(\.\w+)?\b/,
|
|
643
|
+
/\bdd\b[^\n]*\bof=\/dev\//,
|
|
644
|
+
/\btruncate\b[^\n]*\s\/dev\//,
|
|
645
|
+
/>\s*\/dev\/(sd|nvme|hd|vd|mmcblk|disk|loop|dm-)/
|
|
646
|
+
];
|
|
647
|
+
var checkRemoteExec = (cmd) => /\b(curl|wget|fetch)\b[^\n]*\|\s*(sudo\s+)?(sh|bash|zsh|dash|python[0-9.]*|node|ruby|perl)\b/i.test(
|
|
648
|
+
cmd
|
|
649
|
+
) || /(\$\(|<\()\s*(sudo\s+)?(curl|wget|fetch)\b/i.test(cmd) || /\b(eval|source)\b[^\n]*\b(curl|wget|fetch)\b/i.test(cmd) ? "executes a remote download (pipe / command-substitution / eval) \u2014 remote code execution" : null;
|
|
650
|
+
var checkDeviceWipe = (cmd) => DEVICE_WIPE.some((re) => re.test(cmd)) ? "writes to a raw block device / formats a disk" : null;
|
|
651
|
+
var checkForkBomb = (cmd) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(cmd) ? "fork bomb" : null;
|
|
652
|
+
var checkDestructiveGit = (cmd) => {
|
|
653
|
+
if (/\bgit\b[^\n]*\bpush\b[^\n]*(--force(?!-with-lease)\b|\s-f\b|\s\+\S)/.test(cmd)) {
|
|
654
|
+
return "git force-push (overwrites remote history)";
|
|
655
|
+
}
|
|
656
|
+
if (/\bgit\b[^\n]*\breset\b[^\n]*--hard\b/.test(cmd)) {
|
|
657
|
+
return "git reset --hard (discards committed and working changes)";
|
|
658
|
+
}
|
|
659
|
+
if (/\bgit\b[^\n]*\bclean\b[^\n]*(-[a-z]*f[a-z]*d|-[a-z]*d[a-z]*f)/.test(cmd)) {
|
|
660
|
+
return "git clean -fd (permanently deletes untracked files)";
|
|
695
661
|
}
|
|
696
|
-
|
|
697
|
-
return force ? "git push --force" : null;
|
|
662
|
+
return null;
|
|
698
663
|
};
|
|
699
|
-
var
|
|
700
|
-
|
|
701
|
-
|
|
664
|
+
var checkRm = (cmd) => commandSegmentsNamed(cmd, "rm").some((a) => isRecursiveForce(a) && targetsDangerousPath(a)) ? "recursive force-delete of an absolute, home, or parent path" : null;
|
|
665
|
+
var checkPerm = (cmd) => ["chmod", "chown"].some(
|
|
666
|
+
(name) => commandSegmentsNamed(cmd, name).some((a) => isRecursive(a) && targetsDangerousPath(a))
|
|
667
|
+
) ? "recursive permission change on an absolute, home, or parent path" : null;
|
|
668
|
+
var checkFind = (cmd) => /\bfind\s+(\/\S*|~\S*|\$\{?HOME\}?\S*)\s[^\n]*(-delete\b|-exec\s+rm\b)/.test(cmd) ? "find -delete / -exec rm on an absolute or home path" : null;
|
|
669
|
+
var checkExfiltration = (cmd) => {
|
|
670
|
+
const touchesSecret = /(^|[\s/'"])(\.env(\.\w+)?|id_rsa|id_ed25519|\.ssh(\/|\b)|credentials|\.aws(\/|\b)|\.npmrc)\b/.test(
|
|
671
|
+
cmd
|
|
672
|
+
);
|
|
673
|
+
const sendsNetwork = /\b(curl|wget|nc|netcat|scp|ftp|telnet)\b/.test(cmd) || /\bpython[0-9.]*\s+-m\s+http/.test(cmd);
|
|
674
|
+
return touchesSecret && sendsNetwork ? "sends a secret/credential file over the network (exfiltration)" : null;
|
|
702
675
|
};
|
|
703
|
-
var
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
676
|
+
var CATEGORY_CHECKS = [
|
|
677
|
+
checkRemoteExec,
|
|
678
|
+
checkDeviceWipe,
|
|
679
|
+
checkForkBomb,
|
|
680
|
+
checkDestructiveGit,
|
|
681
|
+
checkRm,
|
|
682
|
+
checkPerm,
|
|
683
|
+
checkFind,
|
|
684
|
+
checkExfiltration
|
|
711
685
|
];
|
|
712
|
-
function catastrophicShellReason(
|
|
713
|
-
|
|
714
|
-
if (
|
|
715
|
-
for (const
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
if (head === void 0) continue;
|
|
719
|
-
const cmd0 = basename(unquote(head));
|
|
720
|
-
for (const check of SEGMENT_CHECKS) {
|
|
721
|
-
const reason = check(cmd0, tokens, seg);
|
|
722
|
-
if (reason) return reason;
|
|
723
|
-
}
|
|
686
|
+
function catastrophicShellReason(command) {
|
|
687
|
+
const cmd = command.trim();
|
|
688
|
+
if (cmd.length === 0) return null;
|
|
689
|
+
for (const check of CATEGORY_CHECKS) {
|
|
690
|
+
const reason = check(cmd);
|
|
691
|
+
if (reason) return reason;
|
|
724
692
|
}
|
|
725
693
|
return null;
|
|
726
694
|
}
|