@yagni-app/code-staging 1.1.0-staging.1327.1 → 1.1.0-staging.1328.1
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.
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Pure: no I/O, no network, no model. Loads at startup and classifies
|
|
6
6
|
* synchronously. The curated default set auto-allows read-only commands
|
|
7
|
-
* (ls, cat, rg, git status/log/diff), forbids destructive ones (rm
|
|
8
|
-
* git reset --hard, git push --force, pipe-to-shell
|
|
9
|
-
* ambiguous middle band (
|
|
7
|
+
* (ls, cat, rg, git status/log/diff), forbids destructive ones (recursive rm,
|
|
8
|
+
* git reset --hard, git push --force, pipe-to-shell, bare-interpreter
|
|
9
|
+
* pipes), and prompts for the ambiguous middle band (non-recursive rm -f,
|
|
10
|
+
* inline-code interpreter pipes, npm install, git commit, curl, …).
|
|
10
11
|
*
|
|
11
12
|
* The `prompt` band is what the Guardian arbitrates — see guardian.ts.
|
|
12
13
|
*
|
|
@@ -126,8 +127,10 @@ export declare function tokenize(command: string): string[];
|
|
|
126
127
|
* each is classified independently; the strictest decision wins (forbidden >
|
|
127
128
|
* prompt > allow). Commands with shell constructs (substitution, redirects,
|
|
128
129
|
* background &) have a floor of `prompt`, and their substitution inner text
|
|
129
|
-
* is danger-scanned against the forbidden rules.
|
|
130
|
-
*
|
|
130
|
+
* is danger-scanned against the forbidden rules. Pipes into shells and
|
|
131
|
+
* network relays, or into a bare interpreter (stdin executed as the program),
|
|
132
|
+
* are always forbidden; an interpreter carrying inline code (-c/-e) falls to
|
|
133
|
+
* the prompt band — the code rides the command string the Guardian can read.
|
|
131
134
|
*/
|
|
132
135
|
export declare function classifyCommand(command: string, policy: ExecPolicy): ExecClassification;
|
|
133
136
|
/** Curated default rules — the shipped safety floor. */
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Pure: no I/O, no network, no model. Loads at startup and classifies
|
|
6
6
|
* synchronously. The curated default set auto-allows read-only commands
|
|
7
|
-
* (ls, cat, rg, git status/log/diff), forbids destructive ones (rm
|
|
8
|
-
* git reset --hard, git push --force, pipe-to-shell
|
|
9
|
-
* ambiguous middle band (
|
|
7
|
+
* (ls, cat, rg, git status/log/diff), forbids destructive ones (recursive rm,
|
|
8
|
+
* git reset --hard, git push --force, pipe-to-shell, bare-interpreter
|
|
9
|
+
* pipes), and prompts for the ambiguous middle band (non-recursive rm -f,
|
|
10
|
+
* inline-code interpreter pipes, npm install, git commit, curl, …).
|
|
10
11
|
*
|
|
11
12
|
* The `prompt` band is what the Guardian arbitrates — see guardian.ts.
|
|
12
13
|
*
|
|
@@ -391,11 +392,28 @@ export function extractSubstitutions(command) {
|
|
|
391
392
|
}
|
|
392
393
|
return found;
|
|
393
394
|
}
|
|
394
|
-
/**
|
|
395
|
+
/**
|
|
396
|
+
* Words that, when piped into, are always forbidden: shells (execute stdin)
|
|
397
|
+
* and network relays (remote-shell class). Interpreters are separate — see
|
|
398
|
+
* INTERPRETER_INLINE_FLAGS — because with an inline-code flag (-c/-e) the
|
|
399
|
+
* piped data is just input to a script the Guardian can read on the command
|
|
400
|
+
* line; without one, stdin IS the program (download-and-execute).
|
|
401
|
+
*/
|
|
395
402
|
const PIPE_TO_SHELL = new Set([
|
|
396
|
-
"sh", "bash", "zsh", "fish", "nc", "ncat", "socat",
|
|
403
|
+
"sh", "bash", "zsh", "fish", "dash", "ksh", "nc", "ncat", "socat",
|
|
404
|
+
]);
|
|
405
|
+
/** Interpreters that execute piped stdin as a program unless given inline code. */
|
|
406
|
+
const PIPE_INTERPRETERS = new Set([
|
|
397
407
|
"python", "python3", "perl", "ruby", "node",
|
|
398
408
|
]);
|
|
409
|
+
/** Per-interpreter flags that mean "the code is on the command line". */
|
|
410
|
+
const INTERPRETER_INLINE_FLAGS = {
|
|
411
|
+
python: new Set(["-c"]),
|
|
412
|
+
python3: new Set(["-c"]),
|
|
413
|
+
perl: new Set(["-e"]),
|
|
414
|
+
ruby: new Set(["-e"]),
|
|
415
|
+
node: new Set(["-e", "-p"]),
|
|
416
|
+
};
|
|
399
417
|
/** Wrapper words that forward to another command (`sudo rm …` runs rm). */
|
|
400
418
|
const WRAPPER_WORDS = new Set(["sudo", "env", "command", "builtin", "exec", "nohup", "time", "nice"]);
|
|
401
419
|
/** Shell reserved words that can precede a command inside control flow. */
|
|
@@ -710,23 +728,83 @@ function classifySegmentTokens(rawTokens, policy, opts) {
|
|
|
710
728
|
// No rule matched → prompt (fail toward review, not toward allow)
|
|
711
729
|
return { decision: "prompt", justification: `no policy rule matched for "${tokens[0]}"` };
|
|
712
730
|
}
|
|
713
|
-
/**
|
|
731
|
+
/**
|
|
732
|
+
* Check if any segment pipes into a known shell/network interpreter, or into
|
|
733
|
+
* a bare interpreter (no inline-code flag — stdin is executed as the program).
|
|
734
|
+
* Interpreters carrying an inline-code flag WITH its code argument
|
|
735
|
+
* (`… | python3 -c '…'`) are NOT caught here: they fall through to normal
|
|
736
|
+
* segment classification (prompt band), because the code rides the command
|
|
737
|
+
* string the Guardian can read. The pipe target is resolved through the SAME
|
|
738
|
+
* stripLeadingTokens walk used for command words, so wrapper spellings
|
|
739
|
+
* (`… | env sh`, `… | env -i python3`, `… | /usr/bin/env python3 -c '…'`)
|
|
740
|
+
* resolve identically — one resolver, no drift between the two paths.
|
|
741
|
+
*/
|
|
714
742
|
function isPipeToShell(command) {
|
|
715
743
|
const parsed = shellParse(command);
|
|
716
744
|
for (let i = 0; i < parsed.length - 1; i++) {
|
|
717
745
|
const t = parsed[i];
|
|
718
|
-
if (typeof t
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
746
|
+
if (typeof t !== "object" || t.op !== "pipe")
|
|
747
|
+
continue;
|
|
748
|
+
// Collect the pipe target's leading plain tokens (up to the next op),
|
|
749
|
+
// then resolve the program word through the shared wrapper strip.
|
|
750
|
+
const target = [];
|
|
751
|
+
let j = i + 1;
|
|
752
|
+
while (j < parsed.length && typeof parsed[j] === "string") {
|
|
753
|
+
target.push(parsed[j]);
|
|
754
|
+
j++;
|
|
755
|
+
}
|
|
756
|
+
const { tokens: resolved, stripped: targetStripped } = stripLeadingTokens(target);
|
|
757
|
+
if (resolved.length === 0)
|
|
758
|
+
continue;
|
|
759
|
+
// Candidate program words: the resolved word, plus — when wrappers were
|
|
760
|
+
// stripped — every non-flag token after it. stripLeadingTokens stops at
|
|
761
|
+
// the first non-flag token after a wrapper, which for flags that TAKE a
|
|
762
|
+
// value (`env -u FOO python3`) makes the value the resolved word and can
|
|
763
|
+
// let a bare interpreter slip past. Checking every candidate is a
|
|
764
|
+
// deliberate conservative tradeoff: it can only ADD forbidden verdicts,
|
|
765
|
+
// never remove them, so a benign target like `… | env grep python3`
|
|
766
|
+
// (grep is the program, python3 an argument) is hard-forbidden instead
|
|
767
|
+
// of prompted — a false positive we accept to keep bare interpreters
|
|
768
|
+
// from escaping the floor through wrapper-flag spellings.
|
|
769
|
+
const candidates = [basenameToken(resolved[0])];
|
|
770
|
+
if (targetStripped) {
|
|
771
|
+
for (let k = 1; k < resolved.length; k++) {
|
|
772
|
+
if (!resolved[k].startsWith("-"))
|
|
773
|
+
candidates.push(basenameToken(resolved[k]));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
for (const word of candidates) {
|
|
723
777
|
if (PIPE_TO_SHELL.has(word))
|
|
724
778
|
return true;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
779
|
+
}
|
|
780
|
+
if (candidates.some((word) => PIPE_INTERPRETERS.has(word))) {
|
|
781
|
+
if (!hasInlineCodeArg(resolved))
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Does a resolved pipe-target token list contain an interpreter carrying an
|
|
789
|
+
* inline-code flag WITH its code argument? An inline flag only counts when
|
|
790
|
+
* its argument follows in the same segment: argumentless `… | python3 -c`
|
|
791
|
+
* still leaves stdin as the program, so it stays forbidden. ANY interpreter
|
|
792
|
+
* in the segment satisfying flag+arg is enough — multi-interpreter segments
|
|
793
|
+
* (`… | python3 -c - python3 -c '…'`) downgrade to prompt when any word has
|
|
794
|
+
* its code on the command line, so the scan never early-returns on the first
|
|
795
|
+
* flag-without-arg.
|
|
796
|
+
*/
|
|
797
|
+
function hasInlineCodeArg(resolved) {
|
|
798
|
+
for (let k = 0; k < resolved.length; k++) {
|
|
799
|
+
const w = basenameToken(resolved[k]);
|
|
800
|
+
if (!PIPE_INTERPRETERS.has(w))
|
|
801
|
+
continue;
|
|
802
|
+
const inlineFlags = INTERPRETER_INLINE_FLAGS[w] ?? new Set();
|
|
803
|
+
for (let m = k + 1; m < resolved.length; m++) {
|
|
804
|
+
if (inlineFlags.has(resolved[m])) {
|
|
805
|
+
if (m + 1 < resolved.length && !resolved[m + 1].startsWith("-"))
|
|
729
806
|
return true;
|
|
807
|
+
break;
|
|
730
808
|
}
|
|
731
809
|
}
|
|
732
810
|
}
|
|
@@ -814,8 +892,10 @@ function dangerScanSubstitutions(command, policy, depth) {
|
|
|
814
892
|
* each is classified independently; the strictest decision wins (forbidden >
|
|
815
893
|
* prompt > allow). Commands with shell constructs (substitution, redirects,
|
|
816
894
|
* background &) have a floor of `prompt`, and their substitution inner text
|
|
817
|
-
* is danger-scanned against the forbidden rules.
|
|
818
|
-
*
|
|
895
|
+
* is danger-scanned against the forbidden rules. Pipes into shells and
|
|
896
|
+
* network relays, or into a bare interpreter (stdin executed as the program),
|
|
897
|
+
* are always forbidden; an interpreter carrying inline code (-c/-e) falls to
|
|
898
|
+
* the prompt band — the code rides the command string the Guardian can read.
|
|
819
899
|
*/
|
|
820
900
|
export function classifyCommand(command, policy) {
|
|
821
901
|
// Pipe-to-shell is always forbidden regardless of other rules.
|
|
@@ -858,11 +938,16 @@ export const DEFAULT_EXEC_POLICY = {
|
|
|
858
938
|
// --- forbidden: position-independent dangerous-flag rules (checked first;
|
|
859
939
|
// GNU getopt permutes flags, so `rm x -rf` and `git push origin
|
|
860
940
|
// --force` carry the flag after positional args) ---
|
|
941
|
+
// Recursive deletion only: any -r/-R-bearing flag bundle. Combined
|
|
942
|
+
// bundles where r is not first (-fr, -fR) are listed explicitly because
|
|
943
|
+
// the trailing-star globs only anchor at the leading char. Exotic bundles
|
|
944
|
+
// (-fir) miss this rule and land on the -f prompt rule below — fail-closed
|
|
945
|
+
// toward Guardian review, never auto-allowed.
|
|
861
946
|
{
|
|
862
947
|
pattern: ["rm"],
|
|
863
|
-
flagsAnywhere: ["-r*", "-
|
|
948
|
+
flagsAnywhere: ["-r*", "-R*", "-fr", "-fR", "--recursive*"],
|
|
864
949
|
decision: "forbidden",
|
|
865
|
-
justification: "recursive
|
|
950
|
+
justification: "recursive deletion is destructive and irreversible",
|
|
866
951
|
},
|
|
867
952
|
{
|
|
868
953
|
// Exact --force/-f only: --force-with-lease is the guarded variant and
|
|
@@ -900,9 +985,9 @@ export const DEFAULT_EXEC_POLICY = {
|
|
|
900
985
|
},
|
|
901
986
|
// --- forbidden: destructive commands (positional) ---
|
|
902
987
|
{
|
|
903
|
-
pattern: ["rm", ["-rf", "-fr", "-r", "-
|
|
988
|
+
pattern: ["rm", ["-rf", "-fr", "-r", "-R", "--recursive"]],
|
|
904
989
|
decision: "forbidden",
|
|
905
|
-
justification: "recursive
|
|
990
|
+
justification: "recursive deletion is destructive and irreversible",
|
|
906
991
|
},
|
|
907
992
|
{ pattern: ["git", "reset", "--hard"], decision: "forbidden", justification: "hard reset discards uncommitted changes irreversibly" },
|
|
908
993
|
{ pattern: ["git", "checkout", "--"], decision: "forbidden", justification: "discards working tree changes" },
|
|
@@ -1049,6 +1134,14 @@ export const DEFAULT_EXEC_POLICY = {
|
|
|
1049
1134
|
{ pattern: ["printenv"], decision: "allow", justification: "print environment variables (read-only)" },
|
|
1050
1135
|
{ pattern: ["npm", ["view", "info"]], decision: "allow", justification: "read package metadata from registry" },
|
|
1051
1136
|
// --- prompt: potentially destructive but context-dependent ---
|
|
1137
|
+
// Non-recursive forced deletion — Guardian-reviewable, single file.
|
|
1138
|
+
// Must precede the generic rm prompt rule; ordered before it in this list.
|
|
1139
|
+
{
|
|
1140
|
+
pattern: ["rm"],
|
|
1141
|
+
flagsAnywhere: ["-f*", "--force*"],
|
|
1142
|
+
decision: "prompt",
|
|
1143
|
+
justification: "forced deletion of a single file — review the target",
|
|
1144
|
+
},
|
|
1052
1145
|
{ pattern: ["rm"], decision: "prompt", justification: "file deletion — review the target" },
|
|
1053
1146
|
{ pattern: ["git", "commit"], decision: "prompt", justification: "creates a commit — confirm intent" },
|
|
1054
1147
|
{ pattern: ["git", "push"], decision: "prompt", justification: "pushes to remote — confirm intent" },
|
|
@@ -1079,6 +1172,19 @@ export const DEFAULT_EXEC_POLICY = {
|
|
|
1079
1172
|
{ pattern: ["mkdir"], decision: "prompt", justification: "creates directories" },
|
|
1080
1173
|
{ pattern: ["touch"], decision: "prompt", justification: "creates or updates file timestamps" },
|
|
1081
1174
|
{ pattern: ["tar"], decision: "prompt", justification: "archive operation" },
|
|
1175
|
+
// Interpreters with inline code — the code is on the command line where
|
|
1176
|
+
// the Guardian can read it. Bare interpreters (program from stdin or a
|
|
1177
|
+
// file) never reach these: the pipe-to-interpreter check forbids the piped
|
|
1178
|
+
// form, and the file form is covered by the prompt rules below. Ordered
|
|
1179
|
+
// AFTER the `node --version`/`node -v` allow rules so version checks stay
|
|
1180
|
+
// auto-allowed.
|
|
1181
|
+
{ pattern: ["python"], flagsAnywhere: ["-c"], decision: "prompt", justification: "python runs inline code — review the script" },
|
|
1182
|
+
{ pattern: ["python3"], flagsAnywhere: ["-c"], decision: "prompt", justification: "python runs inline code — review the script" },
|
|
1183
|
+
{ pattern: ["node", ["-e", "-p"]], decision: "prompt", justification: "node runs inline code — review the script" },
|
|
1184
|
+
{ pattern: ["perl", "-e"], decision: "prompt", justification: "perl runs inline code — review the script" },
|
|
1185
|
+
{ pattern: ["ruby", "-e"], decision: "prompt", justification: "ruby runs inline code — review the script" },
|
|
1186
|
+
{ pattern: ["python", "-m"], decision: "prompt", justification: "python runs a module — review the module and args" },
|
|
1187
|
+
{ pattern: ["python3", "-m"], decision: "prompt", justification: "python runs a module — review the module and args" },
|
|
1082
1188
|
{ pattern: ["zip"], decision: "prompt", justification: "archive operation" },
|
|
1083
1189
|
{ pattern: ["unzip"], decision: "prompt", justification: "archive operation" },
|
|
1084
1190
|
{ pattern: ["kill"], decision: "prompt", justification: "sends a signal to a process" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.1.0-staging.
|
|
3
|
+
"version": "1.1.0-staging.1328.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "34b64c98d42aa85f408a4f85de7ef103722038f7"
|
|
62
62
|
}
|