@blogic-cz/agent-tools 1.14.2 → 1.14.4
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 +27 -0
- package/dist/credential-guard/backlog.test.d.ts +2 -0
- package/dist/credential-guard/backlog.test.d.ts.map +1 -0
- package/dist/credential-guard/engine.test.d.ts.map +1 -1
- package/dist/credential-guard/policy.d.ts.map +1 -1
- package/dist/credential-guard/registry.d.ts +2 -1
- package/dist/credential-guard/registry.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/credential-guard/backlog.test.ts +343 -0
- package/src/credential-guard/engine.test.ts +80 -0
- package/src/credential-guard/policy.ts +62 -36
- package/src/credential-guard/registry.ts +288 -9
package/README.md
CHANGED
|
@@ -730,6 +730,33 @@ Literal heredoc bodies can be written as data. Unquoted heredoc expansion is ana
|
|
|
730
730
|
|
|
731
731
|
Copy operations in a Herdr chain require `credentialGuard.chainCopyDestinations`. Copies also receive this destination proof whenever the configured list is nonempty. The synchronous filesystem check refuses errors, symlinks, missing parent directories, and destinations outside the configured directories. Ordinary copies without a Herdr invocation or configured destinations still receive normal source and destination checks.
|
|
732
732
|
|
|
733
|
+
#### Agent instruction and configuration writes
|
|
734
|
+
|
|
735
|
+
The Bash `agent-config-write` rule refuses writes to these paths:
|
|
736
|
+
|
|
737
|
+
- Any file named `AGENTS.md`, `CLAUDE.md`, or `.mcp.json`, with case normalized.
|
|
738
|
+
- Project `.codex/` trees, `.claude/settings*.json`, and `.claude/commands/`, `.claude/agents/`, and `.claude/skills/` trees.
|
|
739
|
+
- Home `.codex/config.toml` and `.codex/prompts/`.
|
|
740
|
+
- Home `.claude.json`, `.claude/settings*.json`, and `.claude/commands/`, `.claude/agents/`, `.claude/skills/`, and `.claude/hooks/` trees.
|
|
741
|
+
- Home `.agents/skills/` and replacement of the home `.agents` directory itself.
|
|
742
|
+
- Replacement of a `.claude` directory itself.
|
|
743
|
+
|
|
744
|
+
The rule checks redirects, `tee`, in-place `sed`, copy and link destinations, and supported writer operands. These include `rsync`, `ditto`, `tmutil restore` destinations, archive output files and destinations, `git checkout` paths with or without `--`, `patch`, `truncate`, `touch`, and `wget` output paths. Without an output-document option, the `wget` URL basename supplies the output name. A glob that can match a protected name refuses. A bracket expression that the matcher cannot translate also refuses. A configured copy grant remains subject to the filesystem proof above.
|
|
745
|
+
|
|
746
|
+
Work data such as `~/.agents/jobs/`, `~/.claude/plans/`, and `~/.claude/projects/` remains writable. Ordinary document paths such as `README.md` remain writable. Reading an instruction file or copying it to `/tmp/x.md` can pass.
|
|
747
|
+
|
|
748
|
+
The guard treats an extensionless destination outside temporary roots as a possible directory and also checks that destination plus the source basename. The added candidate does not apply under `/tmp`, `/private/tmp`, `/var/folders`, or `/private/var/folders`. A literal local `TMPDIR` binding resolves through the same path checks. An unknown ambient `TMPDIR` still requires environment approval and a resolvable path. Explicit directory syntax retains its existing checks.
|
|
749
|
+
|
|
750
|
+
The check is static. It cannot establish whether a dotted destination is an existing directory. It does not inspect archive members, patch contents, remote transfers, source directory contents, or filesystem aliases. Named archive members such as `tar -xf x.tar AGENTS.md` also stay outside this check because the archive contents are unknown. Those contents can contain protected names that the command text does not name. General path checks do not resolve symlinks. The copy proof above is the bounded exception. PowerShell does not receive the Bash agent-configuration write rule.
|
|
751
|
+
|
|
752
|
+
#### Herdr launches
|
|
753
|
+
|
|
754
|
+
The `agent-launch-override` rule checks native arguments after `herdr agent start ... --`. It also checks modeled `tab create` and `pane split` forms that directly launch Codex or Claude. Global Herdr options do not skip the check. Prompt text stays data.
|
|
755
|
+
|
|
756
|
+
Codex accepts `-m` or `--model`, `--add-dir`, `--cd` or `-C`, and `--sandbox` or `-s` with `workspace-write` or `read-only`. Supported `-c` settings are `model_reasoning_effort`, `sandbox_workspace_write.network_access`, and the literal `shell_environment_policy.set.HERDR_*` values used by the supported launch form. Claude accepts `--model` and `--add-dir`. Both accept `--prompt` and one trailing prompt. Other native arguments refuse, including permission bypasses, arbitrary config overrides, and config profiles.
|
|
757
|
+
|
|
758
|
+
`herdr pane run`, `send-keys`, and `send-text` refuse with `no-analyzer` because the terminal state is unknown. The guard does not certify a running agent or an existing terminal.
|
|
759
|
+
|
|
733
760
|
Sensitive paths include credential files and configured blocked paths. Known template paths and explicit allowed paths can provide exceptions. Process-environment files cannot be allowed by a path exception. Slash direction and Windows path case are normalized independently of the host OS. General path checks do not resolve filesystem symlinks or confine commands to the repository; the copy-destination check above is the bounded filesystem exception. Custom command patterns apply to the original text, nested shell text, and resolved invocations.
|
|
734
761
|
|
|
735
762
|
The kubeconfig pattern protects path segments containing `kubeconfig`. A final ordinary Markdown basename such as `check-kubeconfig-topology.md` has a documentation exception; `kubeconfig.md` and kubeconfig-containing parent directories remain protected. Explicit allowed paths take precedence over ordinary blocked patterns. This filename convention does not attest file contents.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backlog.test.d.ts","sourceRoot":"","sources":["../../src/credential-guard/backlog.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.test.d.ts","sourceRoot":"","sources":["../../src/credential-guard/engine.test.ts"],"names":[],"mappings":"AA+
|
|
1
|
+
{"version":3,"file":"engine.test.d.ts","sourceRoot":"","sources":["../../src/credential-guard/engine.test.ts"],"names":[],"mappings":"AA+fA,eAAO,MAAM,cAAc,UAoI1B,CAAC;AACF,eAAO,MAAM,gBAAgB,UA0T5B,CAAC;AA4FF,eAAO,MAAM,oBAAoB,UAShC,CAAC;AACF,eAAO,MAAM,sBAAsB,UA+BlC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/credential-guard/policy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAiB,UAAU,EAAsB,MAAM,EAAE,IAAI,EAAQ,MAAM,MAAM,CAAC;AAO9F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAmB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAuB5D,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;AAQ7D,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC;AAC9F,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;CAAE,GACnF;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;CAAE,CAAC;AAChG,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG;IACxC,QAAQ,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,KAAK,IAAI,CAAC;IAC5F,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACnD,QAAQ,CAAC,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnD,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;CAC3E,CAAC;AACF,MAAM,MAAM,KAAK,GACb;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC/D;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAA;CAAE,CAAC;AAiBxD,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,cAAc,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/credential-guard/policy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAiB,UAAU,EAAsB,MAAM,EAAE,IAAI,EAAQ,MAAM,MAAM,CAAC;AAO9F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAmB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAuB5D,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;AAQ7D,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC;AAC9F,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;CAAE,GACnF;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;CAAE,CAAC;AAChG,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG;IACxC,QAAQ,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,KAAK,IAAI,CAAC;IAC5F,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACnD,QAAQ,CAAC,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnD,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;CAC3E,CAAC;AACF,MAAM,MAAM,KAAK,GACb;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC/D;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAA;CAAE,CAAC;AAiBxD,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,cAAc,QAAQ,CAAC;AA4JpC,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,EAAE,aAAa,GAAG,QAAQ,CA67CnF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { XargsInput } from "./xargs";
|
|
2
2
|
export type Language = "bash" | "powershell" | "jq" | "sed" | "awk" | "runtime";
|
|
3
|
-
export type OperandRole = "data" | "path" | "content" | "path-pattern" | "search-filter" | "names" | "env-name" | "script" | "test-filter" | "member" | "rg-type" | "write" | {
|
|
3
|
+
export type OperandRole = "data" | "path" | "content" | "path-pattern" | "search-filter" | "names" | "env-name" | "script" | "test-filter" | "member" | "rg-type" | "write" | "write-name" | {
|
|
4
4
|
readonly code: Language;
|
|
5
5
|
};
|
|
6
6
|
export type Issue = {
|
|
@@ -15,6 +15,7 @@ export type Option = {
|
|
|
15
15
|
};
|
|
16
16
|
export type ExecutableEntry = {
|
|
17
17
|
readonly name: string;
|
|
18
|
+
readonly rejectsAbbreviations?: boolean;
|
|
18
19
|
readonly readsContent?: boolean;
|
|
19
20
|
readonly readsStdinCode?: boolean;
|
|
20
21
|
readonly builtinOnly?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/credential-guard/registry.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/credential-guard/registry.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
import { parseBash } from "./bash";
|
|
3
|
+
import { judge } from "./policy";
|
|
4
|
+
|
|
5
|
+
const bash = (command: string) => judge(parseBash(command), { cwd: "/repo", home: "/users/test" });
|
|
6
|
+
|
|
7
|
+
it.each([
|
|
8
|
+
"tar -cf /tmp/o.tar -L ~/.ssh/id_rsa",
|
|
9
|
+
"tar -cf /tmp/o.tar -L ~/.ssh/id_rsa src",
|
|
10
|
+
"tar -cf /tmp/o.tar -I ~/.ssh/id_rsa",
|
|
11
|
+
"tar -cf /tmp/o.tar -I ~/.aws/credentials",
|
|
12
|
+
"tar -cf /tmp/o.tar -I ~/.ssh/id_rsa src",
|
|
13
|
+
"tar -cf /tmp/o.tar -b ~/.ssh/id_rsa",
|
|
14
|
+
"tar -cf /tmp/o.tar -s ~/.ssh/id_rsa",
|
|
15
|
+
"tar -cf /tmp/o.tar --exclude ~/.ssh/id_rsa src",
|
|
16
|
+
"tar -cf /tmp/o.tar --exclude=~/.ssh/id_rsa src",
|
|
17
|
+
"tar -cf/tmp/o.tar -I~/.ssh/id_rsa src",
|
|
18
|
+
"tar -cf /tmp/o.tar -W file=~/.ssh/id_rsa src",
|
|
19
|
+
])("checks every tar value as a read: %s", (command) => {
|
|
20
|
+
expect(bash(command)).toMatchObject({
|
|
21
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "sensitive-path" })]),
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it.each([
|
|
26
|
+
"tar -c -Bf AGENTS.md src",
|
|
27
|
+
"tar -c -Sf AGENTS.md src",
|
|
28
|
+
"tar -c -wf AGENTS.md src",
|
|
29
|
+
"tar -r -Bf AGENTS.md src",
|
|
30
|
+
"tar --create -Sf AGENTS.md src",
|
|
31
|
+
"tar -Bf AGENTS.md -c src",
|
|
32
|
+
"tar Bf AGENTS.md -c src",
|
|
33
|
+
"tar -cLf AGENTS.md src",
|
|
34
|
+
"tar -cvLf AGENTS.md src",
|
|
35
|
+
"tar cLf AGENTS.md src",
|
|
36
|
+
"tar -cLf ~/.codex/config.toml src",
|
|
37
|
+
"tar -c -L -f AGENTS.md src",
|
|
38
|
+
"tar -c -W file=AGENTS.md src",
|
|
39
|
+
"tar -cW file=AGENTS.md src",
|
|
40
|
+
"tar -c -Wfile=AGENTS.md src",
|
|
41
|
+
"tar -cf /tmp/o.tar -W file=AGENTS.md src",
|
|
42
|
+
"tar -cf /tmp/x.tar -T AGENTS.md",
|
|
43
|
+
"tar -c -BfAGENTS.md src",
|
|
44
|
+
"tar -cf /tmp/o.tar --unknown=file=AGENTS.md src",
|
|
45
|
+
"tar -Af AGENTS.md src",
|
|
46
|
+
"tar Af AGENTS.md src",
|
|
47
|
+
"tar --catenate -f AGENTS.md src",
|
|
48
|
+
"tar --concatenate --file=AGENTS.md src",
|
|
49
|
+
"tar -W create -f AGENTS.md src",
|
|
50
|
+
"tar -W append -f AGENTS.md src",
|
|
51
|
+
"tar -W create -W file=AGENTS.md src",
|
|
52
|
+
"tar -f AGENTS.md -W create src",
|
|
53
|
+
"tar -W create -f ~/.codex/config.toml src",
|
|
54
|
+
"tar -W catenate -f AGENTS.md x.tar",
|
|
55
|
+
"tar -W concatenate -f AGENTS.md x.tar",
|
|
56
|
+
"tar -W update -f AGENTS.md src",
|
|
57
|
+
"tar -Wcreate -f AGENTS.md src",
|
|
58
|
+
])("checks every tar value in whole-command write mode: %s", (command) => {
|
|
59
|
+
expect(bash(command)).toMatchObject({
|
|
60
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "agent-config-write" })]),
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("checks BSD tar -W write mode against startup files", () => {
|
|
65
|
+
expect(bash("tar -W create -f ~/.zshrc src")).toMatchObject({
|
|
66
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "startup-mutation" })]),
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it.each([
|
|
71
|
+
["tar -c -W file=/users/test/.zshrc src", "startup-mutation"],
|
|
72
|
+
["tar -c -W file=/users/test/.ssh/config src", "sensitive-path"],
|
|
73
|
+
["tar -xf x.tar --directory=/users/test/.zshrc", "startup-mutation"],
|
|
74
|
+
["tar -xf x.tar -C /users/test/.ssh/config", "sensitive-path"],
|
|
75
|
+
])("applies every tar destination rule: %s", (command, rule) => {
|
|
76
|
+
expect(bash(command)).toMatchObject({
|
|
77
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule })]),
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it.each(["tar -xf x.tar", "tar -tvf x.tar", "tar -Af /tmp/x.tar src", "tar -W verbose -xf x.tar"])(
|
|
82
|
+
"accepts ordinary tar modes: %s",
|
|
83
|
+
(command) => expect(bash(command)).toEqual({ kind: "pass" }),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
it.each([
|
|
87
|
+
"tar -cf out.tar -T files.txt",
|
|
88
|
+
"tar -cf out.tar --files-from files.txt",
|
|
89
|
+
"tar -cf out.tar -X excl.txt src",
|
|
90
|
+
"tar -cvf out.tar -I zstd src",
|
|
91
|
+
"tar -cf out.tar -b 20 src",
|
|
92
|
+
"tar -cf out.tar -L 100 src",
|
|
93
|
+
"tar -cf /tmp/o.tar -n src",
|
|
94
|
+
"tar -cf /tmp/o.tar -l src",
|
|
95
|
+
"tar -cvf /tmp/o.tar -s /a/b/ src",
|
|
96
|
+
"tar -xf x.tar -O",
|
|
97
|
+
"tar -xOf x.tar",
|
|
98
|
+
"tar xOf x.tar",
|
|
99
|
+
"tar -xf x.tar -k",
|
|
100
|
+
"tar -xkf x.tar",
|
|
101
|
+
"tar xkf x.tar",
|
|
102
|
+
"tar -xmf x.tar",
|
|
103
|
+
"tar xmf x.tar",
|
|
104
|
+
"tar -xf x.tar -U",
|
|
105
|
+
"tar -xvf x.tar -W",
|
|
106
|
+
"tar -xf x.tar -s /a/b/",
|
|
107
|
+
"tar -cf out.tar --exclude build src",
|
|
108
|
+
"tar -cf out.tar -q src",
|
|
109
|
+
"tar -xqf x.tar",
|
|
110
|
+
"tar xqf x.tar",
|
|
111
|
+
])("accepts tar options outside ambiguous write keys: %s", (command) => {
|
|
112
|
+
expect(bash(command)).toEqual({ kind: "pass" });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it.each([
|
|
116
|
+
"wget --execute=output_document=AGENTS.md https://example.com/x",
|
|
117
|
+
"wget --execute=output_document=/Users/test/.zshrc https://example.com/x",
|
|
118
|
+
"wget --execute=dir_prefix=/Users/test/.codex https://example.com/config.toml",
|
|
119
|
+
"wget --execute=input=urls.txt https://example.com/x",
|
|
120
|
+
"wget --config=/tmp/evil.rc https://example.com/x",
|
|
121
|
+
"wget --input-metalink=x.meta4",
|
|
122
|
+
"wget -e input=urls.txt",
|
|
123
|
+
"wget -e dir_prefix=/tmp https://example.com/x",
|
|
124
|
+
"wget -e output_document=AGENTS.md https://example.com/x",
|
|
125
|
+
"wget --execute output_document=AGENTS.md https://example.com/x",
|
|
126
|
+
"wget --config /tmp/evil.rc https://example.com/x",
|
|
127
|
+
"wget --input-metalink x.meta4",
|
|
128
|
+
"wget -eoutput_document=AGENTS.md https://example.com/x",
|
|
129
|
+
])("refuses indirect wget output configuration: %s", (command) => {
|
|
130
|
+
expect(bash(command)).toMatchObject({
|
|
131
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "no-analyzer" })]),
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it.each([
|
|
136
|
+
"printf x > ./AGENT?.md",
|
|
137
|
+
"printf x > ./AGENT[]S].md",
|
|
138
|
+
"printf x >> ./AGENT[]S].md",
|
|
139
|
+
"printf x > ./AGENT[S].md",
|
|
140
|
+
"echo x | tee /tmp/output > ./AGENT[]S].md",
|
|
141
|
+
"echo x | tee /tmp/output > ./AGENT?.md",
|
|
142
|
+
"cp /tmp/evil/AGENTS.md ~/repo",
|
|
143
|
+
"cp -R .claude ~/claude-backup",
|
|
144
|
+
"cp -R /tmp/evil/.codex ~/somedir",
|
|
145
|
+
"ln -s /tmp/evil/AGENTS.md /repo",
|
|
146
|
+
"cp -r /tmp/evil/.claude ~/repo",
|
|
147
|
+
"cp /tmp/evil/.mcp.json /repo",
|
|
148
|
+
"rsync /tmp/x ~/.codex/AGENTS.md",
|
|
149
|
+
"rsync -a /tmp/evil/ ~/.claude/",
|
|
150
|
+
"ditto /tmp/x ~/.codex/AGENTS.md",
|
|
151
|
+
"wget -O AGENTS.md https://example.com/x",
|
|
152
|
+
"tar -xf /tmp/x.tar -C ~/.claude",
|
|
153
|
+
"unzip /tmp/x.zip -d ~/.codex",
|
|
154
|
+
"git checkout /tmp/x -- AGENTS.md",
|
|
155
|
+
"patch AGENTS.md /tmp/x.patch",
|
|
156
|
+
"truncate -s 0 AGENTS.md",
|
|
157
|
+
"touch ~/.codex/AGENTS.md",
|
|
158
|
+
"touch AGENT?.md",
|
|
159
|
+
"touch ./AGENT[]S].md",
|
|
160
|
+
"sed --in-pl=bak -e s/a/b/ AGENTS.md",
|
|
161
|
+
"sed --in-pl AGENTS.md",
|
|
162
|
+
"sed --in-pl s/a/b/ AGENTS.md",
|
|
163
|
+
"sed --in s/a/b/ ~/.codex/AGENTS.md",
|
|
164
|
+
"sort --out=AGENTS.md /tmp/x",
|
|
165
|
+
"sort --outp=./AGENTS.md in.txt",
|
|
166
|
+
"base64 --out=AGENTS.md /tmp/x",
|
|
167
|
+
"tar --dir=~/.claude -xf /tmp/x.tar",
|
|
168
|
+
"tar --direc=~/.claude -xf /tmp/x.tar",
|
|
169
|
+
"patch --out=AGENTS.md public.txt /tmp/x.patch",
|
|
170
|
+
"wget --output-d=AGENTS.md https://example.com/x",
|
|
171
|
+
"wget --output-doc=AGENTS.md https://example.com/x",
|
|
172
|
+
"tmutil restore /Volumes/TM/x/AGENTS.md AGENTS.md",
|
|
173
|
+
"tmutil restore /Volumes/Backup/x ~/.claude/settings.json",
|
|
174
|
+
"tar -cf AGENTS.md /tmp/evil",
|
|
175
|
+
"tar --create --file=AGENTS.md /tmp/evil",
|
|
176
|
+
"tar -cv -f AGENTS.md /tmp/evil",
|
|
177
|
+
"tar --cre -f AGENTS.md /tmp/evil",
|
|
178
|
+
"wget https://example.com/AGENTS.md",
|
|
179
|
+
"wget --directory-prefix=~/.codex https://example.com/config.toml",
|
|
180
|
+
"wget -P ~/.codex https://example.com/config.toml",
|
|
181
|
+
"wget -P ~/.claude https://example.com/settings.json",
|
|
182
|
+
"tar czf ~/.claude/settings.json src",
|
|
183
|
+
"tar czf AGENTS.md src",
|
|
184
|
+
"tar -rf ~/.codex/config.toml src",
|
|
185
|
+
"tar -rf AGENTS.md x",
|
|
186
|
+
"tar -uf AGENTS.md src",
|
|
187
|
+
"tar -uf AGENTS.md x",
|
|
188
|
+
"tar --append -f AGENTS.md x",
|
|
189
|
+
"tar --append --file=AGENTS.md src",
|
|
190
|
+
"tar --app -f AGENTS.md src",
|
|
191
|
+
"tar --update -f AGENTS.md src",
|
|
192
|
+
"tar cfz AGENTS.md src",
|
|
193
|
+
"tar cpf ~/.claude/settings.json src",
|
|
194
|
+
"tar cpzf AGENTS.md src",
|
|
195
|
+
"tar caf AGENTS.md src",
|
|
196
|
+
"tar chf AGENTS.md src",
|
|
197
|
+
"tar cPf AGENTS.md src",
|
|
198
|
+
"tar -cpf AGENTS.md src",
|
|
199
|
+
"tar -caf AGENTS.md src",
|
|
200
|
+
"tar -cvpf AGENTS.md src",
|
|
201
|
+
"tar -cpzf ~/.codex/config.toml src",
|
|
202
|
+
"wget -P ~/.codex -i urls.txt",
|
|
203
|
+
"tar vczf AGENTS.md src",
|
|
204
|
+
"tar rf AGENTS.md src",
|
|
205
|
+
"tar uf AGENTS.md src",
|
|
206
|
+
"wget -a AGENTS.md https://example.com/x",
|
|
207
|
+
"wget --append-output=AGENTS.md https://example.com/x",
|
|
208
|
+
"git checkout HEAD AGENTS.md",
|
|
209
|
+
"git checkout -p HEAD ./AGENTS.md",
|
|
210
|
+
])("refuses protected writes: %s", (command) => {
|
|
211
|
+
expect(bash(command)).toMatchObject({
|
|
212
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "agent-config-write" })]),
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it.each([
|
|
217
|
+
"cp AGENTS.md /tmp/x.md",
|
|
218
|
+
"cp AGENTS.md ~/backup.md",
|
|
219
|
+
"printf x > ./REPORT?.md",
|
|
220
|
+
"cp -R .claude /tmp/claude-backup",
|
|
221
|
+
"cp AGENTS.md /private/tmp/backup",
|
|
222
|
+
"cp AGENTS.md /var/folders/test/backup",
|
|
223
|
+
"cp AGENTS.md /private/var/folders/test/backup",
|
|
224
|
+
'TMPDIR=/private/tmp/job; cp AGENTS.md "$TMPDIR/backup"',
|
|
225
|
+
"rsync /tmp/x /tmp/y",
|
|
226
|
+
"rsync -e ssh -a src/ host:/srv/",
|
|
227
|
+
"rsync --rsh='ssh -p 2222' -a src/ host:/srv/",
|
|
228
|
+
"ditto /tmp/x /tmp/y",
|
|
229
|
+
"wget -O /tmp/x https://example.com/x",
|
|
230
|
+
"tar -xf /tmp/x.tar -C /tmp/out",
|
|
231
|
+
"tar -cf /tmp/out.tar public.txt",
|
|
232
|
+
"tar -xf/repo/.codex/config.toml",
|
|
233
|
+
"wget https://example.com/public.txt",
|
|
234
|
+
"wget -P downloads https://example.com/x",
|
|
235
|
+
"wget -P /tmp https://example.com/x",
|
|
236
|
+
"wget -t 3 https://example.com/x",
|
|
237
|
+
"wget -t 3 https://example.com/x.zip",
|
|
238
|
+
"wget --tries 3 https://example.com/x.zip",
|
|
239
|
+
"wget -T 10 https://example.com/x.zip",
|
|
240
|
+
"wget -U agent https://example.com/x.zip",
|
|
241
|
+
"wget --limit-rate 1m https://example.com/x.zip",
|
|
242
|
+
"wget -a log.txt https://example.com/x",
|
|
243
|
+
"wget --directory-prefix downloads --timeout 10 --user-agent agent --header 'Accept: text/plain' -l 2 -q -c -N -r -np -nd -nc https://example.com/x",
|
|
244
|
+
"tar czf /tmp/out.tar src",
|
|
245
|
+
"tar -czf /tmp/x.tgz src",
|
|
246
|
+
"tar -cpf /tmp/x.tar src",
|
|
247
|
+
"wget -i urls.txt -O /tmp/out",
|
|
248
|
+
"wget --input-file=urls.txt --output-document=/tmp/out",
|
|
249
|
+
"wget -P /tmp -i urls.txt",
|
|
250
|
+
"wget --directory-prefix=/private/tmp --input-file=urls.txt",
|
|
251
|
+
"wget -P /var/folders/job -i urls.txt",
|
|
252
|
+
'TMPDIR=/private/tmp/job; wget -P "$TMPDIR" -i urls.txt',
|
|
253
|
+
"tar cfz /tmp/out.tar src",
|
|
254
|
+
"tar -rf /tmp/out.tar src",
|
|
255
|
+
"tar -uf /tmp/out.tar src",
|
|
256
|
+
"tar --append --file=/tmp/out.tar src",
|
|
257
|
+
"tar vczf /tmp/out.tar src",
|
|
258
|
+
"tar tf /tmp/out.tar",
|
|
259
|
+
"tar xf /tmp/out.tar",
|
|
260
|
+
"wget -O /tmp/x https://example.com/AGENTS.md",
|
|
261
|
+
"unzip /tmp/x.zip -d /tmp/out",
|
|
262
|
+
"git checkout main -- public.txt",
|
|
263
|
+
"git checkout main public.txt",
|
|
264
|
+
"patch public.txt /tmp/x.patch",
|
|
265
|
+
"truncate -s 0 public.txt",
|
|
266
|
+
"touch public.txt",
|
|
267
|
+
"touch .env",
|
|
268
|
+
"touch build/*.o",
|
|
269
|
+
"tmutil restore public /tmp/x",
|
|
270
|
+
"tmutil restore /Volumes/TM/a /tmp/x",
|
|
271
|
+
"tmutil restore",
|
|
272
|
+
])("accepts ordinary operands: %s", (command) => {
|
|
273
|
+
expect(bash(command)).toEqual({ kind: "pass" });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it.each(["jq --rawf /tmp/x.txt file.txt", "jq --slurpf=/tmp/x.txt file.txt"])(
|
|
277
|
+
"keeps dangerous prefix fallback: %s",
|
|
278
|
+
(command) => {
|
|
279
|
+
expect(bash(command)).toMatchObject({
|
|
280
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "no-analyzer" })]),
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
it("keeps restore startup writes blocked", () => {
|
|
286
|
+
expect(bash("tmutil restore /Volumes/Backup/x ~/.zshrc")).toMatchObject({
|
|
287
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "startup-mutation" })]),
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it.each([
|
|
292
|
+
"wget -i urls.txt",
|
|
293
|
+
"wget --input-file urls.txt",
|
|
294
|
+
"wget --input-file=urls.txt",
|
|
295
|
+
"wget -i -",
|
|
296
|
+
"wget -B https://example.com/ -i urls.txt",
|
|
297
|
+
"wget -P /tmp/../repo -i urls.txt",
|
|
298
|
+
"tar -cqf AGENTS.md src",
|
|
299
|
+
"tar cqf AGENTS.md src",
|
|
300
|
+
])("refuses unknown output paths: %s", (command) => {
|
|
301
|
+
expect(bash(command)).toMatchObject({
|
|
302
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "no-analyzer" })]),
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it.each([
|
|
307
|
+
"wget -P ~ https://example.com/.zshrc",
|
|
308
|
+
"wget -P ~ https://example.com/.profile",
|
|
309
|
+
"wget -P ~ https://example.com/.bashrc",
|
|
310
|
+
"wget -O ~/.zshrc https://example.com/x",
|
|
311
|
+
])("refuses wget startup writes: %s", (command) => {
|
|
312
|
+
expect(bash(command)).toMatchObject({
|
|
313
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "startup-mutation" })]),
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it.each([
|
|
318
|
+
"wget -P ~ https://example.com/.npmrc",
|
|
319
|
+
"wget -P ~/.ssh https://example.com/config",
|
|
320
|
+
"wget -P ~ https://example.com/.envrc",
|
|
321
|
+
])("refuses wget sensitive destinations: %s", (command) => {
|
|
322
|
+
expect(bash(command)).toMatchObject({
|
|
323
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "sensitive-path" })]),
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it.each([
|
|
328
|
+
"rsync -e 'ssh -F ~/.ssh/config' src/ host:/srv/",
|
|
329
|
+
"rsync -e 'cat ~/.ssh/id_rsa' src/ host:/srv/",
|
|
330
|
+
])("inspects remote shell arguments: %s", (command) => {
|
|
331
|
+
expect(bash(command)).toMatchObject({
|
|
332
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "sensitive-path" })]),
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it.each(["tmutil restore ~/.ssh/id_rsa /tmp/x", "tmutil restore /tmp/x ~/.aws/credentials"])(
|
|
337
|
+
"keeps sensitive restore refusal: %s",
|
|
338
|
+
(command) => {
|
|
339
|
+
expect(bash(command)).toMatchObject({
|
|
340
|
+
findings: expect.arrayContaining([expect.objectContaining({ rule: "sensitive-path" })]),
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
);
|
|
@@ -207,6 +207,86 @@ describe("agent configuration boundaries", () => {
|
|
|
207
207
|
]),
|
|
208
208
|
});
|
|
209
209
|
});
|
|
210
|
+
it.each([
|
|
211
|
+
"--remote unix:// --sandbox read-only",
|
|
212
|
+
"--sandbox read-only --remote unix://",
|
|
213
|
+
"--remote=unix:// --sandbox=read-only",
|
|
214
|
+
"--remote unix:// -s read-only",
|
|
215
|
+
"-sread-only --remote=unix://",
|
|
216
|
+
"-s=read-only --remote unix://",
|
|
217
|
+
"--remote unix:// -s read-only -m example -c model_reasoning_effort=low 'Read the brief'",
|
|
218
|
+
])("allows local read-only remote launch %s", (args) => {
|
|
219
|
+
for (const prefix of ["", "rtk proxy "])
|
|
220
|
+
expect(
|
|
221
|
+
bash(`${prefix}herdr agent start worker --kind codex --pane w1:p2 -- ${args}`).kind,
|
|
222
|
+
).toBe("pass");
|
|
223
|
+
});
|
|
224
|
+
it.each([
|
|
225
|
+
"--remote unix://",
|
|
226
|
+
"--remote",
|
|
227
|
+
"--remote unix:// --sandbox",
|
|
228
|
+
"--remote unix:// --sandbox workspace-write",
|
|
229
|
+
"--sandbox workspace-write --remote unix://",
|
|
230
|
+
"--remote unix:// -sworkspace-write",
|
|
231
|
+
"--remote unix:// --sandbox read-only -s read-only",
|
|
232
|
+
"--remote unix:// --sandbox read-only -s workspace-write",
|
|
233
|
+
"--remote unix:// --remote unix:// --sandbox read-only",
|
|
234
|
+
"--remote unix:// --remote=wss://example --sandbox read-only",
|
|
235
|
+
"--remote unix:// --sandbox read-only --add-dir /repo",
|
|
236
|
+
"--add-dir=/repo --sandbox read-only --remote unix://",
|
|
237
|
+
"--remote unix:// --sandbox read-only --yolo",
|
|
238
|
+
"--remote unix:// --sandbox read-only --dangerously-bypass-approvals-and-sandbox",
|
|
239
|
+
"--remote unix:// --sandbox read-only --dangerously-skip-permissions",
|
|
240
|
+
"--remote unix:// --sandbox read-only -c sandbox_mode=read-only",
|
|
241
|
+
"--remote unix:// --sandbox read-only -c approval_policy=never",
|
|
242
|
+
"--remote unix:// --sandbox read-only -c shell_environment_policy.inherit=all",
|
|
243
|
+
"--remote unix:// --sandbox read-only -c shell_environment_policy.exclude=[]",
|
|
244
|
+
"--remote unix:// --sandbox read-only --unknown-option value",
|
|
245
|
+
])("refuses remote launch override %s even before a trailing prompt", (args) => {
|
|
246
|
+
for (const prompt of ["", " 'Read the brief'"])
|
|
247
|
+
expect(
|
|
248
|
+
bash(`rtk proxy herdr agent start worker --kind codex --pane w1:p2 -- ${args}${prompt}`),
|
|
249
|
+
).toMatchObject({
|
|
250
|
+
findings: expect.arrayContaining([
|
|
251
|
+
expect.objectContaining({ rule: "agent-launch-override" }),
|
|
252
|
+
]),
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
it.each([
|
|
256
|
+
"ws://localhost",
|
|
257
|
+
"wss://localhost",
|
|
258
|
+
"tcp://localhost:9000",
|
|
259
|
+
"/users/test/.codex/server.sock",
|
|
260
|
+
"unix:///users/test/.codex/server.sock",
|
|
261
|
+
"/users/test/.codex-other/server.sock",
|
|
262
|
+
"/users/test/.codex/../server.sock",
|
|
263
|
+
".codex/server.sock",
|
|
264
|
+
"../.codex/server.sock",
|
|
265
|
+
"unix://relative.sock",
|
|
266
|
+
"unix:///tmp/server.sock",
|
|
267
|
+
"unix:///",
|
|
268
|
+
])("refuses unproved remote endpoint %s", (endpoint) => {
|
|
269
|
+
expect(
|
|
270
|
+
bash(
|
|
271
|
+
`herdr agent start worker --kind codex --pane w1:p2 -- --sandbox read-only --remote ${endpoint}`,
|
|
272
|
+
),
|
|
273
|
+
).toMatchObject({
|
|
274
|
+
findings: expect.arrayContaining([
|
|
275
|
+
expect.objectContaining({ rule: "agent-launch-override" }),
|
|
276
|
+
]),
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
it("keeps remote launch support exclusive to Codex", () => {
|
|
280
|
+
expect(
|
|
281
|
+
bash(
|
|
282
|
+
"herdr agent start worker --kind claude --pane w1:p2 -- --remote unix:// --sandbox read-only",
|
|
283
|
+
),
|
|
284
|
+
).toMatchObject({
|
|
285
|
+
findings: expect.arrayContaining([
|
|
286
|
+
expect.objectContaining({ rule: "agent-launch-override" }),
|
|
287
|
+
]),
|
|
288
|
+
});
|
|
289
|
+
});
|
|
210
290
|
it.each([
|
|
211
291
|
"herdr tab create -- codex -c shell_environment_policy.inherit=all",
|
|
212
292
|
"herdr pane split w1:p2 -- codex -c shell_environment_policy.inherit=all",
|
|
@@ -96,6 +96,41 @@ const glob = (text: string): Value => ({ ...opaque(text), globPatterns: [text] }
|
|
|
96
96
|
function mentions(value: Value): readonly string[] {
|
|
97
97
|
return value.kind === "literal" ? value.texts : value.kind === "opaque" ? value.mentions : [];
|
|
98
98
|
}
|
|
99
|
+
function agentConfigGlob(value: Value): boolean {
|
|
100
|
+
return mentions(value).some((text) =>
|
|
101
|
+
text
|
|
102
|
+
.replace(/\\/g, "/")
|
|
103
|
+
.split("/")
|
|
104
|
+
.some((part) => {
|
|
105
|
+
if (!/[*?[]/.test(part)) return false;
|
|
106
|
+
if (part.replace(/\[[A-Za-z0-9._]\]/g, "").includes("[")) return true;
|
|
107
|
+
const pattern = new RegExp(
|
|
108
|
+
`^${part
|
|
109
|
+
.replace(/[.+^${}()|\\]/g, "\\$&")
|
|
110
|
+
.replace(/\[([A-Za-z0-9_]|\\\.)\]/g, "$1")
|
|
111
|
+
.replace(/\*/g, ".*")
|
|
112
|
+
.replace(/\?/g, ".")}$`,
|
|
113
|
+
"i",
|
|
114
|
+
);
|
|
115
|
+
return [
|
|
116
|
+
"AGENTS.md",
|
|
117
|
+
"CLAUDE.md",
|
|
118
|
+
".mcp.json",
|
|
119
|
+
".claude",
|
|
120
|
+
".claude.json",
|
|
121
|
+
".codex",
|
|
122
|
+
".agents",
|
|
123
|
+
"config.toml",
|
|
124
|
+
"settings.json",
|
|
125
|
+
"settings.local.json",
|
|
126
|
+
"commands",
|
|
127
|
+
"agents",
|
|
128
|
+
"skills",
|
|
129
|
+
"hooks",
|
|
130
|
+
].some((name) => pattern.test(name));
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
99
134
|
function join(a: Value, b: Value): Value {
|
|
100
135
|
const texts = [...new Set([...mentions(a), ...mentions(b)])];
|
|
101
136
|
return a.kind === "literal" && b.kind === "literal" && texts.length <= MAX_VALUE_ALTERNATIVES
|
|
@@ -251,7 +286,7 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
|
|
|
251
286
|
path.startsWith(`${home}/.agents/skills/`);
|
|
252
287
|
const agentConfigPath = (path: string): boolean =>
|
|
253
288
|
agentConfigDirectory(path) ||
|
|
254
|
-
/(?:^|\/)(?:agents\.md|claude\.md|\.mcp\.json|\.claude)
|
|
289
|
+
/(?:^|\/)(?:agents\.md|claude\.md|\.mcp\.json|\.claude)\/?$/.test(path) ||
|
|
255
290
|
/(?:^|\/)\.claude\/settings[^/]*\.json$/.test(path) ||
|
|
256
291
|
path === `${home}/.agents` ||
|
|
257
292
|
path === `${home}/.codex/config.toml` ||
|
|
@@ -527,7 +562,7 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
|
|
|
527
562
|
stdin = target.kind === "stream" ? target.stdout : { ...opaque(...paths), supplied: true };
|
|
528
563
|
}
|
|
529
564
|
if (redirect.op !== "<") {
|
|
530
|
-
if (current.dialect === "bash" && paths.some(agentConfigPath))
|
|
565
|
+
if (current.dialect === "bash" && (paths.some(agentConfigPath) || agentConfigGlob(target)))
|
|
531
566
|
emit("agent-config-write", "write to agent instructions or configuration", span, true);
|
|
532
567
|
outputs.push(
|
|
533
568
|
...paths.map((path) => ({
|
|
@@ -759,6 +794,24 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
|
|
|
759
794
|
? literal(operand.literal)
|
|
760
795
|
: (values[operand.index] ?? opaque());
|
|
761
796
|
const role = operand.role;
|
|
797
|
+
if (role === "write-name") {
|
|
798
|
+
if (
|
|
799
|
+
(value.kind !== "literal" && !(value.kind === "opaque" && value.globPatterns)) ||
|
|
800
|
+
invocationScope.cwd.kind !== "literal"
|
|
801
|
+
)
|
|
802
|
+
emit("opaque-path", "write target is not a known name or glob", span, true);
|
|
803
|
+
if (
|
|
804
|
+
scope.dialect === "bash" &&
|
|
805
|
+
(agentConfigGlob(value) ||
|
|
806
|
+
mentions(value).some((text) =>
|
|
807
|
+
mentions(invocationScope.cwd).some((cwd) =>
|
|
808
|
+
agentConfigPath(normalizePath(text, context.home, cwd)),
|
|
809
|
+
),
|
|
810
|
+
))
|
|
811
|
+
)
|
|
812
|
+
emit("agent-config-write", "write to agent instructions or configuration", span, true);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
762
815
|
if (typeof role !== "string") {
|
|
763
816
|
const analyzedOutput = analyze(
|
|
764
817
|
role.code,
|
|
@@ -841,39 +894,7 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
|
|
|
841
894
|
scope.dialect === "bash" &&
|
|
842
895
|
(role === "write" ||
|
|
843
896
|
plan.directoryWrites.some((write) => write.sources.includes(operand.index))) &&
|
|
844
|
-
|
|
845
|
-
text
|
|
846
|
-
.replace(/\\/g, "/")
|
|
847
|
-
.split("/")
|
|
848
|
-
.some((part) => {
|
|
849
|
-
if (!/[*?[]/.test(part)) return false;
|
|
850
|
-
if (part.replace(/\[[A-Za-z0-9._]\]/g, "").includes("[")) return true;
|
|
851
|
-
const pattern = new RegExp(
|
|
852
|
-
`^${part
|
|
853
|
-
.replace(/[.+^${}()|\\]/g, "\\$&")
|
|
854
|
-
.replace(/\[([A-Za-z0-9_]|\\\.)\]/g, "$1")
|
|
855
|
-
.replace(/\*/g, ".*")
|
|
856
|
-
.replace(/\?/g, ".")}$`,
|
|
857
|
-
"i",
|
|
858
|
-
);
|
|
859
|
-
return [
|
|
860
|
-
"AGENTS.md",
|
|
861
|
-
"CLAUDE.md",
|
|
862
|
-
".mcp.json",
|
|
863
|
-
".claude",
|
|
864
|
-
".claude.json",
|
|
865
|
-
".codex",
|
|
866
|
-
".agents",
|
|
867
|
-
"config.toml",
|
|
868
|
-
"settings.json",
|
|
869
|
-
"settings.local.json",
|
|
870
|
-
"commands",
|
|
871
|
-
"agents",
|
|
872
|
-
"skills",
|
|
873
|
-
"hooks",
|
|
874
|
-
].some((name) => pattern.test(name));
|
|
875
|
-
}),
|
|
876
|
-
)
|
|
897
|
+
agentConfigGlob(value)
|
|
877
898
|
)
|
|
878
899
|
emit(
|
|
879
900
|
"agent-config-write",
|
|
@@ -954,6 +975,8 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
|
|
|
954
975
|
);
|
|
955
976
|
const directory =
|
|
956
977
|
destination.directory ||
|
|
978
|
+
(!/^\/(?:tmp|private\/tmp|(?:private\/)?var\/folders)(?:\/|$)/.test(path) &&
|
|
979
|
+
mentions(value).some((text) => !/\.[^./]+$/.test(text))) ||
|
|
957
980
|
path === normalizePath(context.home, context.home, context.cwd) ||
|
|
958
981
|
agentConfigDirectory(path) ||
|
|
959
982
|
mentions(value).some((text) => /(?:\/|^\.\.?|\/\.\.?)$/.test(text));
|
|
@@ -961,7 +984,10 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
|
|
|
961
984
|
if (directory || plan.copyDestination) targets.push(target);
|
|
962
985
|
}
|
|
963
986
|
if (
|
|
964
|
-
plan.copyDestination
|
|
987
|
+
(plan.copyDestination ||
|
|
988
|
+
executableName(first.texts[0] ?? "") === "wget" ||
|
|
989
|
+
executableName(first.texts[0] ?? "") === "tar" ||
|
|
990
|
+
(executableName(first.texts[0] ?? "") === "tmutil" && argv[1] === "restore")) &&
|
|
965
991
|
targets.some((target) =>
|
|
966
992
|
/(?:^|\/)(?:\.?(?:bashrc|bash_profile|profile|zshrc|zshenv|zprofile)|config\.fish)$/.test(
|
|
967
993
|
target,
|
|
@@ -18,6 +18,7 @@ export type OperandRole =
|
|
|
18
18
|
| "member"
|
|
19
19
|
| "rg-type"
|
|
20
20
|
| "write"
|
|
21
|
+
| "write-name"
|
|
21
22
|
| { readonly code: Language };
|
|
22
23
|
export type Issue = {
|
|
23
24
|
readonly kind: "violation" | "missing-knowledge";
|
|
@@ -40,6 +41,7 @@ export type Option = {
|
|
|
40
41
|
};
|
|
41
42
|
export type ExecutableEntry = {
|
|
42
43
|
readonly name: string;
|
|
44
|
+
readonly rejectsAbbreviations?: boolean;
|
|
43
45
|
readonly readsContent?: boolean;
|
|
44
46
|
readonly readsStdinCode?: boolean;
|
|
45
47
|
readonly builtinOnly?: boolean;
|
|
@@ -1431,6 +1433,7 @@ export const EXECUTABLES: readonly ExecutableEntry[] = [
|
|
|
1431
1433
|
},
|
|
1432
1434
|
{
|
|
1433
1435
|
name: "jq",
|
|
1436
|
+
rejectsAbbreviations: true,
|
|
1434
1437
|
readsContent: true,
|
|
1435
1438
|
startupFiles: [/(?:^|\/)\.jq$/],
|
|
1436
1439
|
operands: [code("jq"), path],
|
|
@@ -1957,8 +1960,7 @@ export const EXECUTABLES: readonly ExecutableEntry[] = [
|
|
|
1957
1960
|
name,
|
|
1958
1961
|
{
|
|
1959
1962
|
operands: [path],
|
|
1960
|
-
effect:
|
|
1961
|
-
name === "restore" ? unknown("unmodeled Time Machine restore operation") : "none",
|
|
1963
|
+
effect: "none",
|
|
1962
1964
|
} satisfies Omit<ExecutableEntry, "name">,
|
|
1963
1965
|
]),
|
|
1964
1966
|
),
|
|
@@ -3091,6 +3093,137 @@ export const EXECUTABLES: readonly ExecutableEntry[] = [
|
|
|
3091
3093
|
),
|
|
3092
3094
|
},
|
|
3093
3095
|
},
|
|
3096
|
+
{
|
|
3097
|
+
name: "rsync",
|
|
3098
|
+
operands: [path],
|
|
3099
|
+
options: {
|
|
3100
|
+
...flags("-a", "-r", "-v", "-z", "--archive", "--recursive", "--delete"),
|
|
3101
|
+
"-e": arg(code("bash")),
|
|
3102
|
+
"--rsh": arg(code("bash")),
|
|
3103
|
+
},
|
|
3104
|
+
},
|
|
3105
|
+
{ name: "ditto", operands: [path], options: flags("-V", "-v", "--rsrc", "--norsrc") },
|
|
3106
|
+
{
|
|
3107
|
+
name: "tar",
|
|
3108
|
+
operands: [path],
|
|
3109
|
+
options: {
|
|
3110
|
+
...flags(
|
|
3111
|
+
"-x",
|
|
3112
|
+
"--extract",
|
|
3113
|
+
"-c",
|
|
3114
|
+
"--create",
|
|
3115
|
+
"-r",
|
|
3116
|
+
"--append",
|
|
3117
|
+
"-u",
|
|
3118
|
+
"--update",
|
|
3119
|
+
"-A",
|
|
3120
|
+
"--catenate",
|
|
3121
|
+
"--concatenate",
|
|
3122
|
+
"-t",
|
|
3123
|
+
"--list",
|
|
3124
|
+
"-v",
|
|
3125
|
+
"-z",
|
|
3126
|
+
"-j",
|
|
3127
|
+
"-J",
|
|
3128
|
+
"-p",
|
|
3129
|
+
"-a",
|
|
3130
|
+
"-h",
|
|
3131
|
+
"-P",
|
|
3132
|
+
"-O",
|
|
3133
|
+
"--to-stdout",
|
|
3134
|
+
"-k",
|
|
3135
|
+
"-m",
|
|
3136
|
+
"-U",
|
|
3137
|
+
"-W",
|
|
3138
|
+
"-n",
|
|
3139
|
+
"-l",
|
|
3140
|
+
),
|
|
3141
|
+
"-f": arg(path),
|
|
3142
|
+
"--file": arg(path),
|
|
3143
|
+
"-T": arg(path),
|
|
3144
|
+
"--files-from": arg(path),
|
|
3145
|
+
"-X": arg(path),
|
|
3146
|
+
"--exclude-from": arg(path),
|
|
3147
|
+
"--exclude": arg(data),
|
|
3148
|
+
"-b": arg(data),
|
|
3149
|
+
"-L": arg(data),
|
|
3150
|
+
"-I": arg(data),
|
|
3151
|
+
"-s": arg(data),
|
|
3152
|
+
"-C": arg("write"),
|
|
3153
|
+
"--directory": arg("write"),
|
|
3154
|
+
},
|
|
3155
|
+
},
|
|
3156
|
+
{
|
|
3157
|
+
name: "unzip",
|
|
3158
|
+
operands: [path],
|
|
3159
|
+
options: { ...flags("-q", "-o", "-n", "-l", "-t", "-v"), "-d": arg("write") },
|
|
3160
|
+
},
|
|
3161
|
+
{
|
|
3162
|
+
name: "patch",
|
|
3163
|
+
operands: ["write", path],
|
|
3164
|
+
options: {
|
|
3165
|
+
"-i": arg(path),
|
|
3166
|
+
"--input": arg(path),
|
|
3167
|
+
"-o": arg("write"),
|
|
3168
|
+
"--output": arg("write"),
|
|
3169
|
+
"-d": arg("write"),
|
|
3170
|
+
"--directory": arg("write"),
|
|
3171
|
+
"-p": arg(data),
|
|
3172
|
+
...flags("-s", "-f", "-R", "--dry-run"),
|
|
3173
|
+
},
|
|
3174
|
+
},
|
|
3175
|
+
{
|
|
3176
|
+
name: "truncate",
|
|
3177
|
+
operands: ["write"],
|
|
3178
|
+
options: {
|
|
3179
|
+
"-s": arg(data),
|
|
3180
|
+
"--size": arg(data),
|
|
3181
|
+
"-r": arg(path),
|
|
3182
|
+
"--reference": arg(path),
|
|
3183
|
+
...flags("-c", "--no-create"),
|
|
3184
|
+
},
|
|
3185
|
+
},
|
|
3186
|
+
{
|
|
3187
|
+
name: "touch",
|
|
3188
|
+
operands: ["write-name"],
|
|
3189
|
+
options: {
|
|
3190
|
+
"-t": arg(data),
|
|
3191
|
+
"-d": arg(data),
|
|
3192
|
+
"-r": arg(path),
|
|
3193
|
+
...flags("-a", "-m", "-c", "-h", "--no-create"),
|
|
3194
|
+
},
|
|
3195
|
+
},
|
|
3196
|
+
{
|
|
3197
|
+
name: "wget",
|
|
3198
|
+
operands: [data],
|
|
3199
|
+
options: {
|
|
3200
|
+
"-O": arg("write"),
|
|
3201
|
+
"--output-document": arg("write"),
|
|
3202
|
+
"-o": arg("write"),
|
|
3203
|
+
"--output-file": arg("write"),
|
|
3204
|
+
"-a": arg("write"),
|
|
3205
|
+
"--append-output": arg("write"),
|
|
3206
|
+
"-P": arg("write"),
|
|
3207
|
+
"--directory-prefix": arg("write"),
|
|
3208
|
+
"-t": arg(data),
|
|
3209
|
+
"--tries": arg(data),
|
|
3210
|
+
"-T": arg(data),
|
|
3211
|
+
"--timeout": arg(data),
|
|
3212
|
+
"-U": arg(data),
|
|
3213
|
+
"--user-agent": arg(data),
|
|
3214
|
+
"--header": arg(data),
|
|
3215
|
+
"--limit-rate": arg(data),
|
|
3216
|
+
"-l": arg(data),
|
|
3217
|
+
"--level": arg(data),
|
|
3218
|
+
"-i": arg(path),
|
|
3219
|
+
"--input-file": arg(path),
|
|
3220
|
+
"-e": { roles: [data], issue: unknown("wget output configuration") },
|
|
3221
|
+
"--execute": { roles: [data], issue: unknown("wget output configuration") },
|
|
3222
|
+
"--config": { roles: [path], issue: unknown("wget output configuration") },
|
|
3223
|
+
"--input-metalink": { roles: [path], issue: unknown("wget output configuration") },
|
|
3224
|
+
...flags("-q", "-c", "-nv", "-N", "-r", "-np", "-nd", "-nc"),
|
|
3225
|
+
},
|
|
3226
|
+
},
|
|
3094
3227
|
{ name: "seq", operands: [data], semantics: "seq" },
|
|
3095
3228
|
{ name: "tr", operands: [data], stdout: "opaque", options: flags("-c", "-C", "-d", "-s", "-t") },
|
|
3096
3229
|
{
|
|
@@ -3116,7 +3249,7 @@ export const EXECUTABLES: readonly ExecutableEntry[] = [
|
|
|
3116
3249
|
},
|
|
3117
3250
|
{
|
|
3118
3251
|
name: "rm",
|
|
3119
|
-
aliases: ["
|
|
3252
|
+
aliases: ["du", "stat", "chown", "df"],
|
|
3120
3253
|
options: {
|
|
3121
3254
|
...flags("-f", "-r", "-R", "-v", "-n", "-i", "-p", "-a", "-m", "-c", "-h", "-s"),
|
|
3122
3255
|
"-t": arg(data),
|
|
@@ -3521,6 +3654,9 @@ function classifyInvocation(
|
|
|
3521
3654
|
let codePositionals: number[] = [];
|
|
3522
3655
|
const directoryWrites: Array<InvocationPlan["directoryWrites"][number]> = [];
|
|
3523
3656
|
const writerCandidates: { index: number; literal: string }[] = [];
|
|
3657
|
+
let wgetOutputDocument = false;
|
|
3658
|
+
let wgetInputFile = false;
|
|
3659
|
+
let wgetDirectoryPrefix: string | undefined = ".";
|
|
3524
3660
|
let inPlaceBackupSuffix: string | null = null;
|
|
3525
3661
|
let sedEmptySuffix = false;
|
|
3526
3662
|
let maxArgs: number | null = null;
|
|
@@ -4065,6 +4201,31 @@ function classifyInvocation(
|
|
|
4065
4201
|
}
|
|
4066
4202
|
return result();
|
|
4067
4203
|
}
|
|
4204
|
+
const writesArchive =
|
|
4205
|
+
entry.name === "tar" &&
|
|
4206
|
+
argv.slice(1).some((word, index) => {
|
|
4207
|
+
if (word === undefined) return false;
|
|
4208
|
+
const longOption =
|
|
4209
|
+
word === "-W"
|
|
4210
|
+
? argv[index + 2]?.split("=", 1)[0]
|
|
4211
|
+
: word.startsWith("-W")
|
|
4212
|
+
? word.slice(2).split("=", 1)[0]
|
|
4213
|
+
: undefined;
|
|
4214
|
+
if (
|
|
4215
|
+
longOption &&
|
|
4216
|
+
["create", "append", "update", "catenate", "concatenate"].some((option) =>
|
|
4217
|
+
option.startsWith(longOption),
|
|
4218
|
+
)
|
|
4219
|
+
)
|
|
4220
|
+
return true;
|
|
4221
|
+
if (word.startsWith("--"))
|
|
4222
|
+
return ["--create", "--append", "--update", "--catenate", "--concatenate"].some(
|
|
4223
|
+
(option) => word.length > 2 && option.startsWith(word.split("=")[0] ?? word),
|
|
4224
|
+
);
|
|
4225
|
+
const cluster =
|
|
4226
|
+
/^-([A-Za-z]+)/.exec(word)?.[1] ?? (index === 0 && /^[A-Za-z]+$/.test(word) ? word : "");
|
|
4227
|
+
return /[cruA]/.test(cluster);
|
|
4228
|
+
});
|
|
4068
4229
|
while (position < argv.length) {
|
|
4069
4230
|
if (skippedValues.has(position)) {
|
|
4070
4231
|
position++;
|
|
@@ -4147,6 +4308,25 @@ function classifyInvocation(
|
|
|
4147
4308
|
...mode.options,
|
|
4148
4309
|
}
|
|
4149
4310
|
: { ...entry.options, ...mode.options };
|
|
4311
|
+
if (
|
|
4312
|
+
entry.name === "tar" &&
|
|
4313
|
+
position === 1 &&
|
|
4314
|
+
!endOptions &&
|
|
4315
|
+
/^[A-Za-z]+$/.test(a) &&
|
|
4316
|
+
[...a].some((letter) => "cxtru".includes(letter))
|
|
4317
|
+
) {
|
|
4318
|
+
for (const letter of a) {
|
|
4319
|
+
if (!options[`-${letter}`] && writesArchive && a.includes("f"))
|
|
4320
|
+
issues.push(unknown(`unclassified tar option -${letter}`));
|
|
4321
|
+
for (const role of options[`-${letter}`]?.roles ?? []) {
|
|
4322
|
+
const index = ++position;
|
|
4323
|
+
if (index >= argv.length) issues.push(unknown(`missing -${letter} argument`));
|
|
4324
|
+
optionRoles.push({ index, role });
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
position++;
|
|
4328
|
+
continue;
|
|
4329
|
+
}
|
|
4150
4330
|
if (entry.semantics === "sed" && !endOptions && /^-[^-]/.test(a)) {
|
|
4151
4331
|
const inPlaceIndex = a.indexOf("i", 1);
|
|
4152
4332
|
if (
|
|
@@ -4180,7 +4360,9 @@ function classifyInvocation(
|
|
|
4180
4360
|
if (!endOptions && !(optionName in options) && /^-[^-].+/.test(a)) {
|
|
4181
4361
|
let offset = 1;
|
|
4182
4362
|
const directoryWriter =
|
|
4183
|
-
entry.semantics === "copy" ||
|
|
4363
|
+
entry.semantics === "copy" ||
|
|
4364
|
+
["mv", "install", "ln"].includes(entry.name) ||
|
|
4365
|
+
(entry.name === "tar" && writesArchive && a.includes("f"));
|
|
4184
4366
|
while (offset < a.length) {
|
|
4185
4367
|
const option = options[`-${a[offset]}`];
|
|
4186
4368
|
if (
|
|
@@ -4232,7 +4414,7 @@ function classifyInvocation(
|
|
|
4232
4414
|
: [];
|
|
4233
4415
|
const option =
|
|
4234
4416
|
options[optionName] ??
|
|
4235
|
-
(longOptions.length === 1 ? longOptions[0]?.[1] : undefined) ??
|
|
4417
|
+
(!entry.rejectsAbbreviations && longOptions.length === 1 ? longOptions[0]?.[1] : undefined) ??
|
|
4236
4418
|
longOptions.find(
|
|
4237
4419
|
([, candidate]) =>
|
|
4238
4420
|
candidate.issue || candidate.roles.some((role) => typeof role !== "string"),
|
|
@@ -4242,6 +4424,8 @@ function classifyInvocation(
|
|
|
4242
4424
|
([key]) => key.toLowerCase() === optionName.toLowerCase(),
|
|
4243
4425
|
)?.[1]
|
|
4244
4426
|
: undefined);
|
|
4427
|
+
if (option && !entry.rejectsAbbreviations && longOptions.length === 1 && !options[optionName])
|
|
4428
|
+
optionName = longOptions[0]?.[0] ?? optionName;
|
|
4245
4429
|
if (
|
|
4246
4430
|
!endOptions &&
|
|
4247
4431
|
optionName.startsWith("--") &&
|
|
@@ -4265,6 +4449,10 @@ function classifyInvocation(
|
|
|
4265
4449
|
issues.push(unknown("git config override"));
|
|
4266
4450
|
}
|
|
4267
4451
|
if (!endOptions && option && a.startsWith("-")) {
|
|
4452
|
+
if (entry.name === "wget" && ["-O", "--output-document"].includes(optionName))
|
|
4453
|
+
wgetOutputDocument = true;
|
|
4454
|
+
if (entry.name === "wget" && ["-i", "--input-file"].includes(optionName))
|
|
4455
|
+
wgetInputFile = true;
|
|
4268
4456
|
if (
|
|
4269
4457
|
entry.name === "bun" &&
|
|
4270
4458
|
["--filter", "--cwd"].includes(optionName) &&
|
|
@@ -4287,6 +4475,8 @@ function classifyInvocation(
|
|
|
4287
4475
|
for (let i = 0; i < option.roles.length; i++) {
|
|
4288
4476
|
const role = option.roles[i] ?? data;
|
|
4289
4477
|
const index = i === 0 && attached !== undefined ? position : ++position;
|
|
4478
|
+
if (entry.name === "wget" && ["-P", "--directory-prefix"].includes(optionName))
|
|
4479
|
+
wgetDirectoryPrefix = attached ?? argv[index];
|
|
4290
4480
|
if (
|
|
4291
4481
|
index >= argv.length &&
|
|
4292
4482
|
!(
|
|
@@ -4310,7 +4500,13 @@ function classifyInvocation(
|
|
|
4310
4500
|
optionRoles.push({
|
|
4311
4501
|
index,
|
|
4312
4502
|
role,
|
|
4313
|
-
...(
|
|
4503
|
+
...(entry.name === "rsync" &&
|
|
4504
|
+
typeof role !== "string" &&
|
|
4505
|
+
(attached ?? argv[index]) !== undefined
|
|
4506
|
+
? { literal: `${attached ?? argv[index]} guard.invalid` }
|
|
4507
|
+
: i === 0 && attached !== undefined
|
|
4508
|
+
? { literal: attached }
|
|
4509
|
+
: {}),
|
|
4314
4510
|
});
|
|
4315
4511
|
if (entry.semantics === "python" && optionName === "-m") {
|
|
4316
4512
|
pythonModule = { index, name: attached ?? argv[index] };
|
|
@@ -4614,7 +4810,7 @@ function classifyInvocation(
|
|
|
4614
4810
|
}
|
|
4615
4811
|
if (inPlaceBackupSuffix !== null) {
|
|
4616
4812
|
for (const operand of roles) if (operand.role === "path") operand.role = "write";
|
|
4617
|
-
if (sedEmptySuffix && !filterOption && positional[0] !== undefined)
|
|
4813
|
+
if ((sedEmptySuffix || positional.length === 1) && !filterOption && positional[0] !== undefined)
|
|
4618
4814
|
roles.push({ index: positional[0], role: "write" });
|
|
4619
4815
|
}
|
|
4620
4816
|
if (entry.name === "install") {
|
|
@@ -4630,7 +4826,74 @@ function classifyInvocation(
|
|
|
4630
4826
|
if (operand.role === "write")
|
|
4631
4827
|
directoryWrites.push({ index: operand.index, sources: candidates, directory: true });
|
|
4632
4828
|
}
|
|
4633
|
-
if (entry.
|
|
4829
|
+
if (entry.name === "git" && mode === entry.subcommands?.checkout) {
|
|
4830
|
+
const separator = argv.indexOf("--");
|
|
4831
|
+
for (const operand of roles)
|
|
4832
|
+
if (separator >= 0 ? operand.index > separator : positional.slice(1).includes(operand.index))
|
|
4833
|
+
operand.role = "write";
|
|
4834
|
+
}
|
|
4835
|
+
if (entry.name === "tar") {
|
|
4836
|
+
const candidates = optionRoles.map(({ index, literal }) => ({ index, literal }));
|
|
4837
|
+
for (let index = 1; index < argv.length; index++) {
|
|
4838
|
+
const word = argv[index];
|
|
4839
|
+
if (word === undefined || !/^--?[A-Za-z0-9-]+$/.test(word)) {
|
|
4840
|
+
candidates.push({ index, literal: word });
|
|
4841
|
+
if (word?.startsWith("-") && !word.startsWith("--")) {
|
|
4842
|
+
for (
|
|
4843
|
+
let offset = 2;
|
|
4844
|
+
offset < word.length && /[A-Za-z]/.test(word[offset - 1] ?? "");
|
|
4845
|
+
offset++
|
|
4846
|
+
)
|
|
4847
|
+
candidates.push({ index, literal: word.slice(offset) });
|
|
4848
|
+
candidates.push({ index, literal: word.slice(1) });
|
|
4849
|
+
}
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
for (const candidate of candidates) {
|
|
4853
|
+
roles.push({ ...candidate, role: path });
|
|
4854
|
+
if (writesArchive) roles.push({ ...candidate, role: "write" });
|
|
4855
|
+
const word = candidate.literal ?? argv[candidate.index];
|
|
4856
|
+
if (word !== undefined) {
|
|
4857
|
+
for (let equal = word.indexOf("="); equal >= 0; equal = word.indexOf("=", equal + 1)) {
|
|
4858
|
+
const value = { index: candidate.index, literal: word.slice(equal + 1) };
|
|
4859
|
+
roles.push({ ...value, role: path });
|
|
4860
|
+
if (writesArchive) roles.push({ ...value, role: "write" });
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
if (entry.name === "wget" && !wgetOutputDocument) {
|
|
4866
|
+
if (
|
|
4867
|
+
wgetInputFile &&
|
|
4868
|
+
(wgetDirectoryPrefix === undefined ||
|
|
4869
|
+
!/^\/(?:tmp|private\/tmp|(?:private\/)?var\/folders)(?:\/|$)/.test(wgetDirectoryPrefix) ||
|
|
4870
|
+
wgetDirectoryPrefix.split("/").includes(".."))
|
|
4871
|
+
)
|
|
4872
|
+
issues.push(unknown("wget input file output names are not known"));
|
|
4873
|
+
for (const index of positional) {
|
|
4874
|
+
const url = argv[index];
|
|
4875
|
+
if (url === undefined || wgetDirectoryPrefix === undefined)
|
|
4876
|
+
issues.push(unknown("wget output name is not known"));
|
|
4877
|
+
else {
|
|
4878
|
+
try {
|
|
4879
|
+
const basename = new URL(url).pathname.split("/").at(-1);
|
|
4880
|
+
if (basename)
|
|
4881
|
+
roles.push({
|
|
4882
|
+
index,
|
|
4883
|
+
role: "write",
|
|
4884
|
+
literal: `${wgetDirectoryPrefix}/${decodeURIComponent(basename)}`,
|
|
4885
|
+
});
|
|
4886
|
+
} catch {
|
|
4887
|
+
issues.push(unknown("wget output name is not known"));
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
if (
|
|
4893
|
+
entry.semantics === "copy" ||
|
|
4894
|
+
["mv", "ln", "rsync", "ditto"].includes(entry.name) ||
|
|
4895
|
+
(entry.name === "tmutil" && mode === entry.subcommands?.restore)
|
|
4896
|
+
) {
|
|
4634
4897
|
const targetDirectory = optionRoles.find((operand) => operand.role === "write");
|
|
4635
4898
|
const destination = targetDirectory?.index ?? positional.at(-1);
|
|
4636
4899
|
if (destination !== undefined) {
|
|
@@ -4693,13 +4956,16 @@ function classifyInvocation(
|
|
|
4693
4956
|
const command = argv[commandIndex];
|
|
4694
4957
|
const action = argv[commandIndex + 1];
|
|
4695
4958
|
const checkAgentArguments = (kind: string | undefined, start: number): void => {
|
|
4959
|
+
const launchOptions = new Map<string, (string | undefined)[]>();
|
|
4696
4960
|
for (let index = start; index < argv.length; index++) {
|
|
4697
4961
|
const argument = argv[index] ?? "";
|
|
4698
4962
|
if (index === argv.length - 1 && argv[index] !== undefined && !argument.startsWith("-"))
|
|
4699
|
-
|
|
4963
|
+
break;
|
|
4700
4964
|
const attached = /^(--[^=]+)=(.*)$/.exec(argument) ?? /^(-[mcCs])=?(.+)$/.exec(argument);
|
|
4701
4965
|
const option = attached?.[1] ?? argument;
|
|
4702
4966
|
const value = attached ? attached[2] : argv[++index];
|
|
4967
|
+
const key = option === "-s" ? "--sandbox" : option;
|
|
4968
|
+
launchOptions.set(key, [...(launchOptions.get(key) ?? []), value]);
|
|
4703
4969
|
const configuration =
|
|
4704
4970
|
value !== undefined &&
|
|
4705
4971
|
(/^model_reasoning_effort=(?:minimal|low|medium|high|xhigh|max)$/.test(value) ||
|
|
@@ -4715,6 +4981,7 @@ function classifyInvocation(
|
|
|
4715
4981
|
["codex", "claude"].includes(kind ?? "")) ||
|
|
4716
4982
|
(kind === "codex" &&
|
|
4717
4983
|
(["-m", "--cd", "-C"].includes(option) ||
|
|
4984
|
+
(option === "--remote" && value === "unix://") ||
|
|
4718
4985
|
(["--sandbox", "-s"].includes(option) &&
|
|
4719
4986
|
["workspace-write", "read-only"].includes(value)) ||
|
|
4720
4987
|
(option === "-c" && configuration))));
|
|
@@ -4727,6 +4994,18 @@ function classifyInvocation(
|
|
|
4727
4994
|
return;
|
|
4728
4995
|
}
|
|
4729
4996
|
}
|
|
4997
|
+
if (
|
|
4998
|
+
launchOptions.has("--remote") &&
|
|
4999
|
+
(launchOptions.get("--remote")?.length !== 1 ||
|
|
5000
|
+
launchOptions.get("--sandbox")?.length !== 1 ||
|
|
5001
|
+
launchOptions.get("--sandbox")?.[0] !== "read-only" ||
|
|
5002
|
+
launchOptions.has("--add-dir"))
|
|
5003
|
+
)
|
|
5004
|
+
issues.push({
|
|
5005
|
+
kind: "missing-knowledge",
|
|
5006
|
+
rule: "agent-launch-override",
|
|
5007
|
+
detail: "remote agents require one local endpoint and one read-only sandbox",
|
|
5008
|
+
});
|
|
4730
5009
|
};
|
|
4731
5010
|
if (command === "agent" && action === "start" && separator >= 0) {
|
|
4732
5011
|
let kind: string | undefined;
|