auto-mode 0.19.3 → 0.20.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.
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +1 -1
- package/dist/{render-verdict-DBcgB5CC.js → render-verdict-BEZSxyzF.js} +677 -110
- package/package.json +1 -1
- package/policy/containment.md +17 -0
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as readHostEnvironment, T as resolveXDGDir, a as classifyAction, b as loadConfig, i as recordScope, m as writeActionDiagnostic, n as parseScopeRecordRequest, p as loadPolicy, r as parseActionRequest, t as renderVerdict, v as findGitEntry } from "./render-verdict-
|
|
2
|
+
import { C as readHostEnvironment, T as resolveXDGDir, a as classifyAction, b as loadConfig, i as recordScope, m as writeActionDiagnostic, n as parseScopeRecordRequest, p as loadPolicy, r as parseActionRequest, t as renderVerdict, v as findGitEntry } from "./render-verdict-BEZSxyzF.js";
|
|
3
3
|
import { chmod, mkdir, open, realpath, stat } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
package/dist/index.d.ts
CHANGED
|
@@ -37,8 +37,18 @@ interface MCPServerFact {
|
|
|
37
37
|
readonly transport: 'stdio' | 'http' | 'sse' | 'ws';
|
|
38
38
|
readonly host: string | null;
|
|
39
39
|
}
|
|
40
|
+
interface NamedTarget {
|
|
41
|
+
readonly kind: string;
|
|
42
|
+
readonly target: string;
|
|
43
|
+
readonly namedAs: string;
|
|
44
|
+
}
|
|
45
|
+
interface ContainmentEvidence {
|
|
46
|
+
readonly reason: string;
|
|
47
|
+
readonly targets: readonly NamedTarget[];
|
|
48
|
+
}
|
|
40
49
|
interface JudgeEvidence extends DeniedRule {
|
|
41
50
|
readonly repositoryContext: RepositoryContext | null;
|
|
51
|
+
readonly containment?: ContainmentEvidence | undefined;
|
|
42
52
|
}
|
|
43
53
|
//#endregion
|
|
44
54
|
//#region src/config/types.d.ts
|
|
@@ -4629,6 +4639,7 @@ interface ActionOutcome {
|
|
|
4629
4639
|
readonly unavailable?: boolean;
|
|
4630
4640
|
readonly diagnostics?: DecisionDiagnostics;
|
|
4631
4641
|
readonly judge?: JudgeDiagnostics;
|
|
4642
|
+
readonly grant?: JudgeDiagnostics;
|
|
4632
4643
|
readonly denials?: {
|
|
4633
4644
|
readonly consecutive: number;
|
|
4634
4645
|
readonly session: number;
|
|
@@ -4648,9 +4659,16 @@ export declare function classifyAction(request: ActionRequest, config: Config$1,
|
|
|
4648
4659
|
export declare function loadClaudeRules(path: string | null | undefined, host: Readonly<HostEnvironment>): Promise<ClaudeRules>;
|
|
4649
4660
|
//#endregion
|
|
4650
4661
|
//#region src/containment/collect-scope-findings.d.ts
|
|
4662
|
+
type ScopeGrant = {
|
|
4663
|
+
readonly kind: 'path' | 'branch';
|
|
4664
|
+
} | {
|
|
4665
|
+
readonly kind: 'pull-request';
|
|
4666
|
+
readonly number: number;
|
|
4667
|
+
};
|
|
4651
4668
|
interface ScopeFinding {
|
|
4652
|
-
readonly kind: 'path' | 'branch' | 'remote-write' | 'credential' | 'prune';
|
|
4669
|
+
readonly kind: 'path' | 'branch' | 'remote-write' | 'credential' | 'prune' | 'discard';
|
|
4653
4670
|
readonly target: string;
|
|
4671
|
+
readonly grant?: ScopeGrant;
|
|
4654
4672
|
}
|
|
4655
4673
|
//#endregion
|
|
4656
4674
|
//#region src/containment/check-containment.d.ts
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as resolveConfigPath, _ as tryClassifyEdit, a as classifyAction, b as loadConfig, c as classifyWithModel, d as DecisionRequestError, f as buildDecisionRequest, g as loadClaudeSettings, h as checkContainment, i as recordScope, l as parseModelVerdict, n as parseScopeRecordRequest, o as buildTaskScope, p as loadPolicy, r as parseActionRequest, s as classifyLocally, t as renderVerdict, u as sendDecision, w as PRESETS, x as resolveApiKey, y as DEFAULT_CONFIG } from "./render-verdict-
|
|
1
|
+
import { S as resolveConfigPath, _ as tryClassifyEdit, a as classifyAction, b as loadConfig, c as classifyWithModel, d as DecisionRequestError, f as buildDecisionRequest, g as loadClaudeSettings, h as checkContainment, i as recordScope, l as parseModelVerdict, n as parseScopeRecordRequest, o as buildTaskScope, p as loadPolicy, r as parseActionRequest, s as classifyLocally, t as renderVerdict, u as sendDecision, w as PRESETS, x as resolveApiKey, y as DEFAULT_CONFIG } from "./render-verdict-BEZSxyzF.js";
|
|
2
2
|
//#region src/config/load-claude-rules.ts
|
|
3
3
|
async function loadClaudeRules(path, host) {
|
|
4
4
|
return (await loadClaudeSettings(path, host)).rules;
|
|
@@ -6032,6 +6032,11 @@ async function tryClassifyEdit(request, scope, host, reader = {}) {
|
|
|
6032
6032
|
}
|
|
6033
6033
|
}
|
|
6034
6034
|
//#endregion
|
|
6035
|
+
//#region mods/auto-mode/contract/find-current-direct-user-message.ts
|
|
6036
|
+
function findCurrentDirectUserMessage(message) {
|
|
6037
|
+
return message === null || message === void 0 || message.freshness === "stale" ? null : message.text;
|
|
6038
|
+
}
|
|
6039
|
+
//#endregion
|
|
6035
6040
|
//#region src/budget/build-retry-key.ts
|
|
6036
6041
|
const DESCRIPTIVE_FIELDS = { Bash: [
|
|
6037
6042
|
"description",
|
|
@@ -6556,13 +6561,14 @@ const CREDENTIAL_PATHS = [
|
|
|
6556
6561
|
".config/gh"
|
|
6557
6562
|
];
|
|
6558
6563
|
function collectPathFindings(path, scope, scratchPaths) {
|
|
6559
|
-
if (CREDENTIAL_PATHS.some((entry) => isUnder(path, join(scope.home, entry)))) return [{
|
|
6564
|
+
if (CREDENTIAL_PATHS.some((entry) => isUnder$1(path, join(scope.home, entry)))) return [{
|
|
6560
6565
|
kind: "credential",
|
|
6561
6566
|
target: path
|
|
6562
6567
|
}];
|
|
6563
6568
|
return isInScope(path, scope, scratchPaths) ? [] : [{
|
|
6564
6569
|
kind: "path",
|
|
6565
|
-
target: path
|
|
6570
|
+
target: path,
|
|
6571
|
+
grant: { kind: "path" }
|
|
6566
6572
|
}];
|
|
6567
6573
|
}
|
|
6568
6574
|
function collectToolFindings(action, scope) {
|
|
@@ -6572,7 +6578,15 @@ function collectToolFindings(action, scope) {
|
|
|
6572
6578
|
const owner = action.input["owner"];
|
|
6573
6579
|
const name = action.input["repo"];
|
|
6574
6580
|
const repo = typeof owner === "string" && typeof name === "string" ? `${owner}/${name}` : void 0;
|
|
6575
|
-
|
|
6581
|
+
if (!verb.includes("merge") && typeof number === "number" && isOwnedPullRequest(number, repo, scope)) return [];
|
|
6582
|
+
return typeof number === "number" ? [{
|
|
6583
|
+
kind: "remote-write",
|
|
6584
|
+
target: action.tool,
|
|
6585
|
+
grant: {
|
|
6586
|
+
kind: "pull-request",
|
|
6587
|
+
number
|
|
6588
|
+
}
|
|
6589
|
+
}] : [{
|
|
6576
6590
|
kind: "remote-write",
|
|
6577
6591
|
target: action.tool
|
|
6578
6592
|
}];
|
|
@@ -6593,13 +6607,120 @@ const DESTINATION_WRITERS = /* @__PURE__ */ new Set([
|
|
|
6593
6607
|
]);
|
|
6594
6608
|
function collectCommandFindings(command, cwd, scope, scratchPaths) {
|
|
6595
6609
|
const findings = [];
|
|
6596
|
-
|
|
6610
|
+
const entries = collectShellCommands(command, cwd, scope.home);
|
|
6611
|
+
const piped = collectPipedSegments(command);
|
|
6612
|
+
const segments = splitShellCommand(command).segments;
|
|
6613
|
+
for (const [index, entry] of entries.entries()) {
|
|
6597
6614
|
const [name, ...args] = entry.words;
|
|
6615
|
+
const fixed = findFixedXargsCommand(entry.words, segments[index]?.text ?? "");
|
|
6616
|
+
if (fixed !== null) findings.push(...collectProgramFindings(fixed.name, fixed.args, entry.directory, cwd, scope, scratchPaths));
|
|
6617
|
+
else if (isBranchSweep(entries.slice(0, index + 1), piped.slice(0, index + 1))) findings.push({
|
|
6618
|
+
kind: "discard",
|
|
6619
|
+
target: "git branch delete by exclusion"
|
|
6620
|
+
});
|
|
6598
6621
|
for (const target of entry.writeTargets) findings.push(...collectTargetFindings(entry.directory, target, scope, scratchPaths));
|
|
6599
6622
|
if (name !== void 0 && name !== "cd") findings.push(...collectProgramFindings(name, args, entry.directory, cwd, scope, scratchPaths));
|
|
6600
6623
|
}
|
|
6601
6624
|
return findings;
|
|
6602
6625
|
}
|
|
6626
|
+
function collectPipedSegments(command) {
|
|
6627
|
+
const piped = [];
|
|
6628
|
+
let separator = null;
|
|
6629
|
+
let isInSegment = false;
|
|
6630
|
+
for (const token of splitShellTokens(command).tokens) if (token.kind === "separator") {
|
|
6631
|
+
separator = token.operator;
|
|
6632
|
+
isInSegment = false;
|
|
6633
|
+
} else if (!isInSegment) {
|
|
6634
|
+
piped.push(separator === "|");
|
|
6635
|
+
isInSegment = true;
|
|
6636
|
+
}
|
|
6637
|
+
return piped;
|
|
6638
|
+
}
|
|
6639
|
+
const XARGS_FLAG = /^(?:-[0oprtx]+|--(?:null|no-run-if-empty|verbose|interactive|exit|open-tty))$/u;
|
|
6640
|
+
const XARGS_VALUE_OPTION = /^-[EPads]$/u;
|
|
6641
|
+
const XARGS_ATTACHED_VALUE = /^(?:-[EPads].|--(?:eof|max-procs|delimiter|arg-file|max-chars|process-slot-var)=)/u;
|
|
6642
|
+
const XARGS_REPLACE = /^(?:-I(?<attached>.+)|-i(?<short>.*)|--replace(?:=(?<long>.*))?)$/u;
|
|
6643
|
+
const PLAIN_WORD = /^(?:[\w./:@%+=-]|\{\})+$/u;
|
|
6644
|
+
function findFixedXargsCommand(words, text) {
|
|
6645
|
+
if (words[0] !== "xargs" || !splitShellTokens(text).tokens.every((token) => token.kind !== "word" || token.raw === token.value && PLAIN_WORD.test(token.value))) return null;
|
|
6646
|
+
let token = null;
|
|
6647
|
+
let start = 1;
|
|
6648
|
+
while (words[start]?.startsWith("-") === true) {
|
|
6649
|
+
const word = words[start] ?? "";
|
|
6650
|
+
const replace = XARGS_REPLACE.exec(word)?.groups;
|
|
6651
|
+
if (word === "--") {
|
|
6652
|
+
start += 1;
|
|
6653
|
+
break;
|
|
6654
|
+
}
|
|
6655
|
+
if (word === "-I") {
|
|
6656
|
+
token = words[start + 1] ?? null;
|
|
6657
|
+
start += 2;
|
|
6658
|
+
} else if (replace !== void 0) {
|
|
6659
|
+
const short = replace["short"];
|
|
6660
|
+
token = replace["attached"] ?? replace["long"] ?? (short === void 0 || short === "" ? "{}" : short);
|
|
6661
|
+
start += 1;
|
|
6662
|
+
} else if (word.startsWith("-J")) {
|
|
6663
|
+
token = null;
|
|
6664
|
+
start += word === "-J" ? 2 : 1;
|
|
6665
|
+
} else if (XARGS_VALUE_OPTION.test(word)) start += 2;
|
|
6666
|
+
else if (XARGS_FLAG.test(word) || XARGS_ATTACHED_VALUE.test(word)) start += 1;
|
|
6667
|
+
else return null;
|
|
6668
|
+
}
|
|
6669
|
+
const [name, ...args] = words.slice(start);
|
|
6670
|
+
if (token === null || token === "" || name === void 0 || [name, ...args].some((word) => word.includes(token))) return null;
|
|
6671
|
+
return {
|
|
6672
|
+
name,
|
|
6673
|
+
args
|
|
6674
|
+
};
|
|
6675
|
+
}
|
|
6676
|
+
const XARGS_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
6677
|
+
"-I",
|
|
6678
|
+
"-J",
|
|
6679
|
+
"-L",
|
|
6680
|
+
"-n",
|
|
6681
|
+
"-P",
|
|
6682
|
+
"-s",
|
|
6683
|
+
"-d",
|
|
6684
|
+
"-E"
|
|
6685
|
+
]);
|
|
6686
|
+
function isBranchSweep(entries, piped) {
|
|
6687
|
+
const deleter = entries.at(-1)?.words ?? [];
|
|
6688
|
+
if (deleter[0] !== "xargs" || piped.at(-1) !== true) return false;
|
|
6689
|
+
let start = 1;
|
|
6690
|
+
while (deleter[start]?.startsWith("-") === true) {
|
|
6691
|
+
if (/^(?:-a|--arg-file)/u.test(deleter[start] ?? "")) return false;
|
|
6692
|
+
start += XARGS_VALUE_OPTIONS.has(deleter[start] ?? "") ? 2 : 1;
|
|
6693
|
+
}
|
|
6694
|
+
const [program, subcommand, ...rest] = deleter.slice(start);
|
|
6695
|
+
if (program !== "git" || subcommand !== "branch" || !rest.some((word) => /^-(?:d|D|-delete)$/u.test(word))) return false;
|
|
6696
|
+
let first = entries.length - 2;
|
|
6697
|
+
while (first > 0 && piped[first] === true) first -= 1;
|
|
6698
|
+
const [listing, ...filters] = entries.slice(first, -1).map((entry) => entry.words);
|
|
6699
|
+
return listing !== void 0 && isWholeBranchListing(listing) && filters.every((filter) => isExcludingFilter(filter));
|
|
6700
|
+
}
|
|
6701
|
+
function isWholeBranchListing(words) {
|
|
6702
|
+
const [program, subcommand, ...rest] = words;
|
|
6703
|
+
if (program !== "git") return false;
|
|
6704
|
+
if (subcommand === "branch") return rest.every((word) => /^(?:-a|-r|-v|-vv|--all|--remotes|--no-color|--format=.*)$/u.test(word));
|
|
6705
|
+
if (subcommand !== "for-each-ref") return false;
|
|
6706
|
+
return rest.filter((_word, index) => !/^--(?:format|sort)$/u.test(rest[index - 1] ?? "")).every((word) => /^--(?:format|sort)(?:=.*)?$/u.test(word) || /^refs\/heads\/?$/u.test(word));
|
|
6707
|
+
}
|
|
6708
|
+
const NAME_EDITORS = /* @__PURE__ */ new Set([
|
|
6709
|
+
"tr",
|
|
6710
|
+
"cut",
|
|
6711
|
+
"sort"
|
|
6712
|
+
]);
|
|
6713
|
+
function isExcludingFilter(words) {
|
|
6714
|
+
const [program, ...args] = words;
|
|
6715
|
+
if (program === "grep" || program === "egrep") return args.some((word) => /^-[a-zA-Z]*v[a-zA-Z]*$|^--invert-match$/u.test(word));
|
|
6716
|
+
if (program === "sed") return isSubstitutionOnly(args);
|
|
6717
|
+
if (program === "uniq") return args.every((word) => !/^-[a-zA-Z]*[dDu]/u.test(word));
|
|
6718
|
+
return program !== void 0 && NAME_EDITORS.has(program);
|
|
6719
|
+
}
|
|
6720
|
+
function isSubstitutionOnly(args) {
|
|
6721
|
+
const scripts = args.filter((word) => word !== "-e" && word !== "-E" && word !== "-r");
|
|
6722
|
+
return scripts.length > 0 && scripts.every((script) => /^s(?<delimiter>[^\w\s;])(?:(?!\k<delimiter>).)*\k<delimiter>(?:(?!\k<delimiter>).)*\k<delimiter>g?$/u.test(script));
|
|
6723
|
+
}
|
|
6603
6724
|
function collectProgramFindings(name, args, directory, cwd, scope, scratchPaths) {
|
|
6604
6725
|
const resolve = (path) => collectTargetFindings(directory, path, scope, scratchPaths);
|
|
6605
6726
|
const operands = args.filter((word) => !word.startsWith("-"));
|
|
@@ -6754,7 +6875,13 @@ function collectGitFindings(args, directory, cwd, scope, scratchPaths) {
|
|
|
6754
6875
|
if (subcommand === "worktree" && operands[0] === "remove" && operands[1] !== void 0) findings.push(...collectTargetFindings(gitDirectory, operands[1], scope, scratchPaths));
|
|
6755
6876
|
if (subcommand === "branch" && rest.some((word) => /^-(?:d|D|-delete)$/u.test(word))) for (const branch of operands.filter((word) => !scope.branches.includes(word))) findings.push({
|
|
6756
6877
|
kind: "branch",
|
|
6757
|
-
target: branch
|
|
6878
|
+
target: branch,
|
|
6879
|
+
grant: { kind: "branch" }
|
|
6880
|
+
});
|
|
6881
|
+
const [resetTarget] = operands;
|
|
6882
|
+
if (subcommand === "reset" && rest.includes("--hard") && resetTarget !== void 0 && resetTarget !== "HEAD" && resetTarget !== "@" && !isUnresolvedWord(resetTarget)) findings.push({
|
|
6883
|
+
kind: "discard",
|
|
6884
|
+
target: `git reset --hard ${resetTarget}`
|
|
6758
6885
|
});
|
|
6759
6886
|
const isConfigRead = rest.some((word) => /^--(?:get|get-all|get-regexp|list)$|^-l$/u.test(word));
|
|
6760
6887
|
if (subcommand === "config" && rest.includes("--global") && !isConfigRead) findings.push({
|
|
@@ -6784,7 +6911,8 @@ function collectPushFindings(rest, operands, currentBranch, scope) {
|
|
|
6784
6911
|
const branch = named === "HEAD" ? currentBranch : named;
|
|
6785
6912
|
if (branch !== null && !isUnresolvedWord(branch) && !branch.startsWith("refs/tags/") && !scope.branches.includes(branch)) findings.push({
|
|
6786
6913
|
kind: "branch",
|
|
6787
|
-
target: branch
|
|
6914
|
+
target: branch,
|
|
6915
|
+
grant: { kind: "branch" }
|
|
6788
6916
|
});
|
|
6789
6917
|
}
|
|
6790
6918
|
return findings;
|
|
@@ -6829,10 +6957,19 @@ function collectGhFindings(args, scope) {
|
|
|
6829
6957
|
const group = args[0] ?? "";
|
|
6830
6958
|
const verb = args[1] ?? "";
|
|
6831
6959
|
if (group === "pr" && GH_PR_WRITES.has(verb)) {
|
|
6832
|
-
const
|
|
6833
|
-
const
|
|
6834
|
-
const
|
|
6835
|
-
|
|
6960
|
+
const pull = findPullRequestSelector(verb, args.slice(2));
|
|
6961
|
+
const number = pull?.number ?? NaN;
|
|
6962
|
+
const isOwned = verb !== "merge" && isOwnedPullRequest(number, pull?.repo, scope);
|
|
6963
|
+
const isAdmin = args.some((word) => /^--admin(?:=|$)/u.test(word));
|
|
6964
|
+
if (isOwned) return [];
|
|
6965
|
+
return Number.isInteger(number) && !isAdmin ? [{
|
|
6966
|
+
kind: "remote-write",
|
|
6967
|
+
target: `gh pr ${verb}`,
|
|
6968
|
+
grant: {
|
|
6969
|
+
kind: "pull-request",
|
|
6970
|
+
number
|
|
6971
|
+
}
|
|
6972
|
+
}] : [{
|
|
6836
6973
|
kind: "remote-write",
|
|
6837
6974
|
target: `gh pr ${verb}`
|
|
6838
6975
|
}];
|
|
@@ -6860,6 +6997,79 @@ function collectGhFindings(args, scope) {
|
|
|
6860
6997
|
}];
|
|
6861
6998
|
return [];
|
|
6862
6999
|
}
|
|
7000
|
+
const GH_PR_VALUE_OPTIONS = {
|
|
7001
|
+
comment: /* @__PURE__ */ new Set([
|
|
7002
|
+
"-b",
|
|
7003
|
+
"--body",
|
|
7004
|
+
"-F",
|
|
7005
|
+
"--body-file",
|
|
7006
|
+
"--attach"
|
|
7007
|
+
]),
|
|
7008
|
+
merge: /* @__PURE__ */ new Set([
|
|
7009
|
+
"-A",
|
|
7010
|
+
"--author-email",
|
|
7011
|
+
"-b",
|
|
7012
|
+
"--body",
|
|
7013
|
+
"-F",
|
|
7014
|
+
"--body-file",
|
|
7015
|
+
"--match-head-commit",
|
|
7016
|
+
"-t",
|
|
7017
|
+
"--subject"
|
|
7018
|
+
]),
|
|
7019
|
+
edit: /* @__PURE__ */ new Set([
|
|
7020
|
+
"--add-assignee",
|
|
7021
|
+
"--add-label",
|
|
7022
|
+
"--add-project",
|
|
7023
|
+
"--add-reviewer",
|
|
7024
|
+
"--attach",
|
|
7025
|
+
"-B",
|
|
7026
|
+
"--base",
|
|
7027
|
+
"-b",
|
|
7028
|
+
"--body",
|
|
7029
|
+
"-F",
|
|
7030
|
+
"--body-file",
|
|
7031
|
+
"-m",
|
|
7032
|
+
"--milestone",
|
|
7033
|
+
"--remove-assignee",
|
|
7034
|
+
"--remove-label",
|
|
7035
|
+
"--remove-project",
|
|
7036
|
+
"--remove-reviewer",
|
|
7037
|
+
"-t",
|
|
7038
|
+
"--title"
|
|
7039
|
+
]),
|
|
7040
|
+
close: /* @__PURE__ */ new Set(["-c", "--comment"]),
|
|
7041
|
+
review: /* @__PURE__ */ new Set([
|
|
7042
|
+
"-b",
|
|
7043
|
+
"--body",
|
|
7044
|
+
"-F",
|
|
7045
|
+
"--body-file"
|
|
7046
|
+
]),
|
|
7047
|
+
ready: /* @__PURE__ */ new Set(),
|
|
7048
|
+
reopen: /* @__PURE__ */ new Set(["-c", "--comment"])
|
|
7049
|
+
};
|
|
7050
|
+
const GH_REPO_OPTIONS = /* @__PURE__ */ new Set(["-R", "--repo"]);
|
|
7051
|
+
function findPullRequestSelector(verb, args) {
|
|
7052
|
+
const valueOptions = GH_PR_VALUE_OPTIONS[verb] ?? /* @__PURE__ */ new Set();
|
|
7053
|
+
let repo;
|
|
7054
|
+
let selector;
|
|
7055
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
7056
|
+
const word = args[index] ?? "";
|
|
7057
|
+
if (GH_REPO_OPTIONS.has(word)) repo = args[index + 1];
|
|
7058
|
+
else if (word.startsWith("--repo=")) repo = word.slice(7);
|
|
7059
|
+
if (GH_REPO_OPTIONS.has(word) || valueOptions.has(word)) index += 1;
|
|
7060
|
+
else if (selector === void 0 && !word.startsWith("-")) selector = word;
|
|
7061
|
+
}
|
|
7062
|
+
if (selector === void 0) return null;
|
|
7063
|
+
const url = /^https?:\/\/(?<repo>[^/]+\/[^/]+\/[^/]+)\/pull\/(?<number>\d+)(?:[/?#]|$)/u.exec(selector)?.groups;
|
|
7064
|
+
if (url !== void 0) return {
|
|
7065
|
+
number: Number(url["number"]),
|
|
7066
|
+
repo: url["repo"]
|
|
7067
|
+
};
|
|
7068
|
+
return {
|
|
7069
|
+
number: /^#?\d+$/u.test(selector) ? Number(selector.replace("#", "")) : null,
|
|
7070
|
+
repo
|
|
7071
|
+
};
|
|
7072
|
+
}
|
|
6863
7073
|
function isOwnedPullRequest(number, repo, scope) {
|
|
6864
7074
|
const wanted = repo?.toLowerCase().replace(/\.git$/u, "");
|
|
6865
7075
|
const slugs = new Set(scope.remotes.map((remote) => toRepositorySlug(remote.url)));
|
|
@@ -6934,14 +7144,14 @@ function collectInPlaceFiles(args) {
|
|
|
6934
7144
|
return files;
|
|
6935
7145
|
}
|
|
6936
7146
|
function isInScope(path, scope, scratchPaths) {
|
|
6937
|
-
if (scratchPaths.some((entry) => isUnder(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
|
|
7147
|
+
if (scratchPaths.some((entry) => isUnder$1(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
|
|
6938
7148
|
return scope.worktrees.some((worktree) => {
|
|
6939
|
-
const isNested = isUnder(path, join(worktree, ".worktrees"));
|
|
6940
|
-
const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder(path, other));
|
|
6941
|
-
return isUnder(path, worktree) && (!isNested || isOtherOwned);
|
|
7149
|
+
const isNested = isUnder$1(path, join(worktree, ".worktrees"));
|
|
7150
|
+
const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder$1(path, other));
|
|
7151
|
+
return isUnder$1(path, worktree) && (!isNested || isOtherOwned);
|
|
6942
7152
|
});
|
|
6943
7153
|
}
|
|
6944
|
-
function isUnder(path, root) {
|
|
7154
|
+
function isUnder$1(path, root) {
|
|
6945
7155
|
return path === root || path.startsWith(`${root}/`);
|
|
6946
7156
|
}
|
|
6947
7157
|
//#endregion
|
|
@@ -6965,27 +7175,103 @@ const KIND_LABELS = {
|
|
|
6965
7175
|
branch: "branch",
|
|
6966
7176
|
"remote-write": "remote target",
|
|
6967
7177
|
credential: "credential or global setting",
|
|
6968
|
-
prune: "shared resource"
|
|
7178
|
+
prune: "shared resource",
|
|
7179
|
+
discard: "discarded work"
|
|
6969
7180
|
};
|
|
6970
7181
|
function formatContainmentReason(findings, scope) {
|
|
6971
7182
|
const targets = findings.slice(0, 5).map((finding) => `${KIND_LABELS[finding.kind]} ${finding.target}`).join(", ");
|
|
6972
7183
|
const more = findings.length > 5 ? ` and ${findings.length - 5} more` : "";
|
|
6973
7184
|
const branches = scope.branches.length === 0 ? "" : ` and ${formatList("branch", "branches", scope.branches)}`;
|
|
6974
|
-
return `This action writes outside the task scope: ${targets}${more}. The task owns ${`${formatList("worktree", "worktrees", scope.worktrees)}${branches}`}.
|
|
7185
|
+
return `This action writes outside the task scope: ${targets}${more}. The task owns ${`${formatList("worktree", "worktrees", scope.worktrees)}${branches}`}. ${formatAdvice(findings)}`;
|
|
7186
|
+
}
|
|
7187
|
+
const DISCARD_ADVICE = "A hard reset onto another commit, or a branch deletion that names no branch, discards commits and changes the task may not have made. Sync a branch with git merge --ff-only or git rebase, and delete branches by name, or ask the user to run this action.";
|
|
7188
|
+
const SCOPE_ADVICE = "Do the work inside the task's own worktree and branch, or ask the user to extend the task scope to this target.";
|
|
7189
|
+
function formatAdvice(findings) {
|
|
7190
|
+
const discards = findings.filter((finding) => finding.kind === "discard").length;
|
|
7191
|
+
if (discards === 0) return SCOPE_ADVICE;
|
|
7192
|
+
return discards === findings.length ? DISCARD_ADVICE : `${DISCARD_ADVICE} ${SCOPE_ADVICE}`;
|
|
6975
7193
|
}
|
|
6976
7194
|
function formatList(one, many, values) {
|
|
6977
7195
|
return `the ${values.length === 1 ? one : many} ${values.join(", ")}`;
|
|
6978
7196
|
}
|
|
6979
7197
|
//#endregion
|
|
7198
|
+
//#region src/scope/protected-branch-names.ts
|
|
7199
|
+
const PROTECTED_BRANCH_NAMES = /* @__PURE__ */ new Set([
|
|
7200
|
+
"main",
|
|
7201
|
+
"master",
|
|
7202
|
+
"trunk",
|
|
7203
|
+
"develop"
|
|
7204
|
+
]);
|
|
7205
|
+
//#endregion
|
|
7206
|
+
//#region src/containment/find-user-named-grant.ts
|
|
7207
|
+
function findUserNamedGrant(findings, message, context) {
|
|
7208
|
+
const words = splitMessageWords(message);
|
|
7209
|
+
const named = [];
|
|
7210
|
+
for (const finding of findings) {
|
|
7211
|
+
const namedAs = findNamingWord(finding, words, context);
|
|
7212
|
+
if (namedAs === null) return null;
|
|
7213
|
+
named.push({
|
|
7214
|
+
kind: finding.kind,
|
|
7215
|
+
target: finding.target,
|
|
7216
|
+
namedAs
|
|
7217
|
+
});
|
|
7218
|
+
}
|
|
7219
|
+
return named.length === 0 ? null : named;
|
|
7220
|
+
}
|
|
7221
|
+
function splitMessageWords(message) {
|
|
7222
|
+
return [...[...message.matchAll(/(?<quote>["'`])(?<text>[^"'`\n]+?)\k<quote>/gu)].map((match) => match.groups?.["text"] ?? ""), ...message.split(/[\s"'`()<>[\]{},;*]+/u)].map((word) => word.trim().replace(/[.:!?]+$/u, "")).filter((word) => word !== "");
|
|
7223
|
+
}
|
|
7224
|
+
function findNamingWord(finding, words, context) {
|
|
7225
|
+
const grant = finding.grant;
|
|
7226
|
+
if (grant === void 0) return null;
|
|
7227
|
+
if (grant.kind === "pull-request") return findPullRequestWord(grant.number, words);
|
|
7228
|
+
if (grant.kind === "path") return findPathWord(finding.target, words, context.home);
|
|
7229
|
+
if (PROTECTED_BRANCH_NAMES.has(finding.target) || finding.target === context.defaultBranch) return null;
|
|
7230
|
+
return words.find((word) => word === finding.target) ?? null;
|
|
7231
|
+
}
|
|
7232
|
+
const PULL_REQUEST_NOUNS = /^(?:pr|pull|request)$/iu;
|
|
7233
|
+
function findPullRequestWord(number, words) {
|
|
7234
|
+
for (const [index, word] of words.entries()) {
|
|
7235
|
+
if (word === `#${number}` || new RegExp(`^(?:pr|pull)#?${number}$`, "iu").test(word) || word.endsWith(`/pull/${number}`)) return word;
|
|
7236
|
+
const previous = words[index - 1] ?? "";
|
|
7237
|
+
if (word === String(number) && PULL_REQUEST_NOUNS.test(previous)) return `${previous} ${word}`;
|
|
7238
|
+
}
|
|
7239
|
+
return null;
|
|
7240
|
+
}
|
|
7241
|
+
function findPathWord(path, words, home) {
|
|
7242
|
+
const worktree = findWorktreeName(path);
|
|
7243
|
+
for (const word of words) {
|
|
7244
|
+
if (worktree !== null && word.split("/").includes(worktree)) return word;
|
|
7245
|
+
const named = resolveMessagePath(word, home);
|
|
7246
|
+
if (named !== null && isUnder(path, named) && !isUnder(home, named) && findWorktreeName(named) === worktree) return word;
|
|
7247
|
+
}
|
|
7248
|
+
return null;
|
|
7249
|
+
}
|
|
7250
|
+
function findWorktreeName(path) {
|
|
7251
|
+
return [...path.matchAll(/\/\.(?:claude\/)?worktrees\/(?<name>[^/]+)/gu)].at(-1)?.groups?.["name"] ?? null;
|
|
7252
|
+
}
|
|
7253
|
+
function resolveMessagePath(word, home) {
|
|
7254
|
+
const expanded = word.startsWith("~/") ? join(home, word.slice(2)) : word;
|
|
7255
|
+
if (!expanded.startsWith("/")) return null;
|
|
7256
|
+
const path = normalize(expanded);
|
|
7257
|
+
return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
7258
|
+
}
|
|
7259
|
+
function isUnder(path, root) {
|
|
7260
|
+
return path === root || root === "/" || path.startsWith(`${root}/`);
|
|
7261
|
+
}
|
|
7262
|
+
//#endregion
|
|
6980
7263
|
//#region src/diagnostics/write-action-diagnostic.ts
|
|
6981
7264
|
async function writeActionDiagnostic(payload, entry, host, warnings) {
|
|
6982
7265
|
const path = host.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(resolveXDGDir(host, "state"), "actions.jsonl");
|
|
6983
7266
|
if (path === "") return;
|
|
7267
|
+
const agentID = payload.decisionContext?.agentID ?? null;
|
|
7268
|
+
const agentKey = agentID === null ? payload.sessionID : `${payload.sessionID}\0${agentID}`;
|
|
6984
7269
|
const record = {
|
|
6985
|
-
schemaVersion:
|
|
7270
|
+
schemaVersion: 6,
|
|
6986
7271
|
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6987
7272
|
invocationID: entry.invocationID,
|
|
6988
7273
|
sessionHash: toHash(payload.sessionID),
|
|
7274
|
+
agentHash: toHash(agentKey),
|
|
6989
7275
|
actionHash: payload.toolUseID === void 0 ? null : toHash(payload.toolUseID),
|
|
6990
7276
|
status: entry.status,
|
|
6991
7277
|
verdict: entry.verdict ?? null,
|
|
@@ -6993,7 +7279,8 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
|
|
|
6993
7279
|
denials: entry.denials ?? null,
|
|
6994
7280
|
escalation: entry.escalation ?? false,
|
|
6995
7281
|
diagnostics: entry.diagnostics ?? null,
|
|
6996
|
-
judge: entry.judge ?? null
|
|
7282
|
+
judge: entry.judge ?? null,
|
|
7283
|
+
grant: entry.grant ?? null
|
|
6997
7284
|
};
|
|
6998
7285
|
try {
|
|
6999
7286
|
await mkdir(dirname(path), {
|
|
@@ -7009,6 +7296,52 @@ function toHash(value) {
|
|
|
7009
7296
|
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
7010
7297
|
}
|
|
7011
7298
|
//#endregion
|
|
7299
|
+
//#region src/git/build-repository-context.ts
|
|
7300
|
+
function buildRepositoryContext(facts) {
|
|
7301
|
+
if (facts.head === null) return null;
|
|
7302
|
+
return {
|
|
7303
|
+
cwd: facts.cwd,
|
|
7304
|
+
branch: facts.head.branch,
|
|
7305
|
+
defaultBranch: facts.head.defaultBranch,
|
|
7306
|
+
remotes: facts.remotes.flatMap((remote) => {
|
|
7307
|
+
const url = normalizeRemoteURL(remote.url);
|
|
7308
|
+
return url === null ? [] : [{
|
|
7309
|
+
name: remote.name,
|
|
7310
|
+
url
|
|
7311
|
+
}];
|
|
7312
|
+
})
|
|
7313
|
+
};
|
|
7314
|
+
}
|
|
7315
|
+
function normalizeRemoteURL(url) {
|
|
7316
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(url)) {
|
|
7317
|
+
const parsed = URL.parse(url);
|
|
7318
|
+
if (parsed === null) return null;
|
|
7319
|
+
parsed.username = "";
|
|
7320
|
+
parsed.password = "";
|
|
7321
|
+
parsed.search = "";
|
|
7322
|
+
parsed.hash = "";
|
|
7323
|
+
return parsed.href;
|
|
7324
|
+
}
|
|
7325
|
+
const slash = url.indexOf("/");
|
|
7326
|
+
const end = slash === -1 ? url.length : slash;
|
|
7327
|
+
const at = url.lastIndexOf("@", end);
|
|
7328
|
+
return at === -1 ? url : url.slice(at + 1);
|
|
7329
|
+
}
|
|
7330
|
+
//#endregion
|
|
7331
|
+
//#region src/git/build-repository-evidence.ts
|
|
7332
|
+
function buildRepositoryEvidence(facts, taskScope) {
|
|
7333
|
+
const context = buildRepositoryContext(facts);
|
|
7334
|
+
if (taskScope === void 0) return context;
|
|
7335
|
+
return {
|
|
7336
|
+
...context ?? {
|
|
7337
|
+
cwd: facts.cwd,
|
|
7338
|
+
branch: null,
|
|
7339
|
+
defaultBranch: null
|
|
7340
|
+
},
|
|
7341
|
+
taskScope
|
|
7342
|
+
};
|
|
7343
|
+
}
|
|
7344
|
+
//#endregion
|
|
7012
7345
|
//#region src/git/load-checkout-remotes.ts
|
|
7013
7346
|
async function loadCheckoutRemotes(commonDir) {
|
|
7014
7347
|
const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
|
|
@@ -7083,11 +7416,6 @@ function parseReference(content, prefix) {
|
|
|
7083
7416
|
return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
|
|
7084
7417
|
}
|
|
7085
7418
|
//#endregion
|
|
7086
|
-
//#region mods/auto-mode/contract/find-current-direct-user-message.ts
|
|
7087
|
-
function findCurrentDirectUserMessage(message) {
|
|
7088
|
-
return message === null || message === void 0 || message.freshness === "stale" ? null : message.text;
|
|
7089
|
-
}
|
|
7090
|
-
//#endregion
|
|
7091
7419
|
//#region src/policy/resolve-shipped-policy-dir.ts
|
|
7092
7420
|
function resolveShippedPolicyDir(startDir = import.meta.dirname, stopDir = parse(startDir).root) {
|
|
7093
7421
|
let dir = startDir;
|
|
@@ -7121,6 +7449,10 @@ function buildJudgeMessage(input) {
|
|
|
7121
7449
|
text: input.rule.text
|
|
7122
7450
|
},
|
|
7123
7451
|
action: input.action,
|
|
7452
|
+
...input.containment === void 0 ? {} : {
|
|
7453
|
+
containment: input.containment.reason,
|
|
7454
|
+
namedTargets: input.containment.targets
|
|
7455
|
+
},
|
|
7124
7456
|
lastDirectUserMessage: input.lastUserMessage,
|
|
7125
7457
|
repository: input.repositoryContext
|
|
7126
7458
|
};
|
|
@@ -7496,11 +7828,11 @@ function getReasonTemplate(rule) {
|
|
|
7496
7828
|
clears: "The replacement policy defines what clears it."
|
|
7497
7829
|
})).exhaustive();
|
|
7498
7830
|
}
|
|
7499
|
-
function buildJudgedVerdict(rule, basis, reply, overturns) {
|
|
7831
|
+
function buildJudgedVerdict(rule, basis, reply, overturns, heldReason) {
|
|
7500
7832
|
const template = {
|
|
7501
7833
|
kind: "deny",
|
|
7502
7834
|
rule: rule.name,
|
|
7503
|
-
reason: buildDenyReason(rule, basis)
|
|
7835
|
+
reason: heldReason ?? buildDenyReason(rule, basis)
|
|
7504
7836
|
};
|
|
7505
7837
|
if (reply === null || reply.kind === "unreadable") return {
|
|
7506
7838
|
verdict: template,
|
|
@@ -7696,6 +8028,7 @@ async function sendJudgeMessage(provider, request, transport, signal) {
|
|
|
7696
8028
|
async function classifyWithJudge(payload, evidence, judge, options) {
|
|
7697
8029
|
const start = performance.now();
|
|
7698
8030
|
const now = options.now ?? Date.now;
|
|
8031
|
+
const overturns = evidence.containment === void 0 ? options.overturns : "consent";
|
|
7699
8032
|
let failure = null;
|
|
7700
8033
|
let verdict;
|
|
7701
8034
|
try {
|
|
@@ -7714,7 +8047,8 @@ async function classifyWithJudge(payload, evidence, judge, options) {
|
|
|
7714
8047
|
input: payload.toolInput
|
|
7715
8048
|
},
|
|
7716
8049
|
lastUserMessage: directMessage,
|
|
7717
|
-
repositoryContext: evidence.repositoryContext
|
|
8050
|
+
repositoryContext: evidence.repositoryContext,
|
|
8051
|
+
containment: evidence.containment
|
|
7718
8052
|
});
|
|
7719
8053
|
const remainingMs = options.deadlineAt === void 0 ? judge.timeoutMs : options.deadlineAt - now();
|
|
7720
8054
|
failure = "timeout";
|
|
@@ -7733,9 +8067,9 @@ async function classifyWithJudge(payload, evidence, judge, options) {
|
|
|
7733
8067
|
throw error;
|
|
7734
8068
|
}));
|
|
7735
8069
|
failure = reply.kind === "unreadable" ? "unreadable" : null;
|
|
7736
|
-
verdict = buildJudgedVerdict(evidence.rule, evidence.basis, reply,
|
|
8070
|
+
verdict = buildJudgedVerdict(evidence.rule, evidence.basis, reply, overturns, evidence.containment?.reason);
|
|
7737
8071
|
} catch {
|
|
7738
|
-
verdict = buildJudgedVerdict(evidence.rule, evidence.basis, null,
|
|
8072
|
+
verdict = buildJudgedVerdict(evidence.rule, evidence.basis, null, overturns, evidence.containment?.reason);
|
|
7739
8073
|
}
|
|
7740
8074
|
return {
|
|
7741
8075
|
...verdict,
|
|
@@ -7763,52 +8097,6 @@ function buildTaskScopeSummary(scope) {
|
|
|
7763
8097
|
};
|
|
7764
8098
|
}
|
|
7765
8099
|
//#endregion
|
|
7766
|
-
//#region src/git/build-repository-context.ts
|
|
7767
|
-
function buildRepositoryContext(facts) {
|
|
7768
|
-
if (facts.head === null) return null;
|
|
7769
|
-
return {
|
|
7770
|
-
cwd: facts.cwd,
|
|
7771
|
-
branch: facts.head.branch,
|
|
7772
|
-
defaultBranch: facts.head.defaultBranch,
|
|
7773
|
-
remotes: facts.remotes.flatMap((remote) => {
|
|
7774
|
-
const url = normalizeRemoteURL(remote.url);
|
|
7775
|
-
return url === null ? [] : [{
|
|
7776
|
-
name: remote.name,
|
|
7777
|
-
url
|
|
7778
|
-
}];
|
|
7779
|
-
})
|
|
7780
|
-
};
|
|
7781
|
-
}
|
|
7782
|
-
function normalizeRemoteURL(url) {
|
|
7783
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(url)) {
|
|
7784
|
-
const parsed = URL.parse(url);
|
|
7785
|
-
if (parsed === null) return null;
|
|
7786
|
-
parsed.username = "";
|
|
7787
|
-
parsed.password = "";
|
|
7788
|
-
parsed.search = "";
|
|
7789
|
-
parsed.hash = "";
|
|
7790
|
-
return parsed.href;
|
|
7791
|
-
}
|
|
7792
|
-
const slash = url.indexOf("/");
|
|
7793
|
-
const end = slash === -1 ? url.length : slash;
|
|
7794
|
-
const at = url.lastIndexOf("@", end);
|
|
7795
|
-
return at === -1 ? url : url.slice(at + 1);
|
|
7796
|
-
}
|
|
7797
|
-
//#endregion
|
|
7798
|
-
//#region src/git/build-repository-evidence.ts
|
|
7799
|
-
function buildRepositoryEvidence(facts, taskScope) {
|
|
7800
|
-
const context = buildRepositoryContext(facts);
|
|
7801
|
-
if (taskScope === void 0) return context;
|
|
7802
|
-
return {
|
|
7803
|
-
...context ?? {
|
|
7804
|
-
cwd: facts.cwd,
|
|
7805
|
-
branch: null,
|
|
7806
|
-
defaultBranch: null
|
|
7807
|
-
},
|
|
7808
|
-
taskScope
|
|
7809
|
-
};
|
|
7810
|
-
}
|
|
7811
|
-
//#endregion
|
|
7812
8100
|
//#region src/git/load-mcp-servers.ts
|
|
7813
8101
|
const serverSchema = looseObject({
|
|
7814
8102
|
type: string().optional(),
|
|
@@ -8365,6 +8653,11 @@ function toMessage(error) {
|
|
|
8365
8653
|
return error instanceof Error ? error.message : String(error);
|
|
8366
8654
|
}
|
|
8367
8655
|
//#endregion
|
|
8656
|
+
//#region src/policy/read-containment-rule.ts
|
|
8657
|
+
async function readContainmentRule(policyDir = resolveShippedPolicyDir()) {
|
|
8658
|
+
return (await readFile(join(policyDir, "containment.md"), "utf8")).trim();
|
|
8659
|
+
}
|
|
8660
|
+
//#endregion
|
|
8368
8661
|
//#region src/policy/read-denial-guidance.ts
|
|
8369
8662
|
async function readDenialGuidance(policyDir = resolveShippedPolicyDir()) {
|
|
8370
8663
|
return (await readFile(join(policyDir, "denial.md"), "utf8")).trim();
|
|
@@ -8388,13 +8681,10 @@ const READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
|
8388
8681
|
"Grep",
|
|
8389
8682
|
"LS",
|
|
8390
8683
|
"NotebookRead",
|
|
8391
|
-
"WebFetch",
|
|
8392
|
-
"WebSearch",
|
|
8393
8684
|
"TodoWrite"
|
|
8394
8685
|
]);
|
|
8395
8686
|
const SHELL_TOOLS = /* @__PURE__ */ new Set(["Bash"]);
|
|
8396
8687
|
const READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
|
|
8397
|
-
"awk",
|
|
8398
8688
|
"basename",
|
|
8399
8689
|
"cat",
|
|
8400
8690
|
"cksum",
|
|
@@ -8407,7 +8697,6 @@ const READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
8407
8697
|
"dirname",
|
|
8408
8698
|
"du",
|
|
8409
8699
|
"echo",
|
|
8410
|
-
"env",
|
|
8411
8700
|
"false",
|
|
8412
8701
|
"fd",
|
|
8413
8702
|
"file",
|
|
@@ -8417,7 +8706,6 @@ const READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
8417
8706
|
"hostname",
|
|
8418
8707
|
"id",
|
|
8419
8708
|
"jq",
|
|
8420
|
-
"less",
|
|
8421
8709
|
"ls",
|
|
8422
8710
|
"md5sum",
|
|
8423
8711
|
"nl",
|
|
@@ -8442,9 +8730,138 @@ const READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
8442
8730
|
"uptime",
|
|
8443
8731
|
"wc",
|
|
8444
8732
|
"which",
|
|
8445
|
-
"whoami"
|
|
8446
|
-
|
|
8447
|
-
|
|
8733
|
+
"whoami"
|
|
8734
|
+
]);
|
|
8735
|
+
const UNSAFE_OPTIONS = /* @__PURE__ */ new Map([
|
|
8736
|
+
["date", {
|
|
8737
|
+
short: ["s"],
|
|
8738
|
+
long: ["set"],
|
|
8739
|
+
valued: {
|
|
8740
|
+
short: [
|
|
8741
|
+
"d",
|
|
8742
|
+
"f",
|
|
8743
|
+
"r"
|
|
8744
|
+
],
|
|
8745
|
+
attached: ["I"],
|
|
8746
|
+
long: [
|
|
8747
|
+
"date",
|
|
8748
|
+
"file",
|
|
8749
|
+
"reference",
|
|
8750
|
+
"rfc-3339"
|
|
8751
|
+
]
|
|
8752
|
+
}
|
|
8753
|
+
}],
|
|
8754
|
+
["fd", {
|
|
8755
|
+
short: ["x", "X"],
|
|
8756
|
+
long: ["exec", "exec-batch"],
|
|
8757
|
+
valued: {
|
|
8758
|
+
short: [
|
|
8759
|
+
"c",
|
|
8760
|
+
"d",
|
|
8761
|
+
"e",
|
|
8762
|
+
"E",
|
|
8763
|
+
"j",
|
|
8764
|
+
"o",
|
|
8765
|
+
"S",
|
|
8766
|
+
"t"
|
|
8767
|
+
],
|
|
8768
|
+
long: [
|
|
8769
|
+
"color",
|
|
8770
|
+
"exclude",
|
|
8771
|
+
"extension",
|
|
8772
|
+
"max-depth",
|
|
8773
|
+
"owner",
|
|
8774
|
+
"size",
|
|
8775
|
+
"threads",
|
|
8776
|
+
"type"
|
|
8777
|
+
]
|
|
8778
|
+
}
|
|
8779
|
+
}],
|
|
8780
|
+
["file", {
|
|
8781
|
+
short: ["C"],
|
|
8782
|
+
long: ["compile"],
|
|
8783
|
+
valued: {
|
|
8784
|
+
short: [
|
|
8785
|
+
"e",
|
|
8786
|
+
"f",
|
|
8787
|
+
"F",
|
|
8788
|
+
"m",
|
|
8789
|
+
"P"
|
|
8790
|
+
],
|
|
8791
|
+
long: [
|
|
8792
|
+
"exclude",
|
|
8793
|
+
"exclude-quiet",
|
|
8794
|
+
"files-from",
|
|
8795
|
+
"magic-file",
|
|
8796
|
+
"parameter",
|
|
8797
|
+
"separator"
|
|
8798
|
+
]
|
|
8799
|
+
}
|
|
8800
|
+
}],
|
|
8801
|
+
["find", { words: [
|
|
8802
|
+
"-delete",
|
|
8803
|
+
"-exec",
|
|
8804
|
+
"-execdir",
|
|
8805
|
+
"-fls",
|
|
8806
|
+
"-fprint",
|
|
8807
|
+
"-fprint0",
|
|
8808
|
+
"-fprintf",
|
|
8809
|
+
"-ok",
|
|
8810
|
+
"-okdir"
|
|
8811
|
+
] }],
|
|
8812
|
+
["hostname", {
|
|
8813
|
+
short: ["b", "F"],
|
|
8814
|
+
long: ["boot", "file"],
|
|
8815
|
+
operands: 0
|
|
8816
|
+
}],
|
|
8817
|
+
["rg", {
|
|
8818
|
+
short: ["z"],
|
|
8819
|
+
long: [
|
|
8820
|
+
"hostname-bin",
|
|
8821
|
+
"pre",
|
|
8822
|
+
"search-zip"
|
|
8823
|
+
]
|
|
8824
|
+
}],
|
|
8825
|
+
["sort", {
|
|
8826
|
+
short: ["o", "T"],
|
|
8827
|
+
long: [
|
|
8828
|
+
"compress-program",
|
|
8829
|
+
"output",
|
|
8830
|
+
"temporary-directory"
|
|
8831
|
+
],
|
|
8832
|
+
valued: {
|
|
8833
|
+
short: [
|
|
8834
|
+
"k",
|
|
8835
|
+
"S",
|
|
8836
|
+
"t"
|
|
8837
|
+
],
|
|
8838
|
+
long: [
|
|
8839
|
+
"batch-size",
|
|
8840
|
+
"buffer-size",
|
|
8841
|
+
"field-separator",
|
|
8842
|
+
"files0-from",
|
|
8843
|
+
"key",
|
|
8844
|
+
"parallel",
|
|
8845
|
+
"random-source",
|
|
8846
|
+
"sort"
|
|
8847
|
+
]
|
|
8848
|
+
}
|
|
8849
|
+
}],
|
|
8850
|
+
["uniq", {
|
|
8851
|
+
operands: 1,
|
|
8852
|
+
valued: {
|
|
8853
|
+
short: [
|
|
8854
|
+
"f",
|
|
8855
|
+
"s",
|
|
8856
|
+
"w"
|
|
8857
|
+
],
|
|
8858
|
+
long: [
|
|
8859
|
+
"check-chars",
|
|
8860
|
+
"skip-chars",
|
|
8861
|
+
"skip-fields"
|
|
8862
|
+
]
|
|
8863
|
+
}
|
|
8864
|
+
}]
|
|
8448
8865
|
]);
|
|
8449
8866
|
const READ_ONLY_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
8450
8867
|
"blame",
|
|
@@ -8452,7 +8869,6 @@ const READ_ONLY_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
8452
8869
|
"diff",
|
|
8453
8870
|
"log",
|
|
8454
8871
|
"ls-files",
|
|
8455
|
-
"ls-remote",
|
|
8456
8872
|
"ls-tree",
|
|
8457
8873
|
"rev-list",
|
|
8458
8874
|
"rev-parse",
|
|
@@ -8460,9 +8876,14 @@ const READ_ONLY_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
8460
8876
|
"show",
|
|
8461
8877
|
"show-ref",
|
|
8462
8878
|
"status",
|
|
8463
|
-
"tag",
|
|
8464
8879
|
"whatchanged"
|
|
8465
8880
|
]);
|
|
8881
|
+
const UNSAFE_GIT_OPTIONS = { long: [
|
|
8882
|
+
"ext-diff",
|
|
8883
|
+
"output",
|
|
8884
|
+
"textconv"
|
|
8885
|
+
] };
|
|
8886
|
+
const SAFE_GIT_GLOBAL_FLAGS = /* @__PURE__ */ new Set(["--no-pager", "-P"]);
|
|
8466
8887
|
const REGENERABLE_DIRS = /* @__PURE__ */ new Set([
|
|
8467
8888
|
".gradle",
|
|
8468
8889
|
".next",
|
|
@@ -8505,21 +8926,90 @@ function classifyLocally(payload) {
|
|
|
8505
8926
|
};
|
|
8506
8927
|
}
|
|
8507
8928
|
function classifySegment(text, cwd) {
|
|
8929
|
+
if (splitShellTokens(text).tokens.some((token) => token.kind === "redirect")) return null;
|
|
8508
8930
|
const words = splitWords(text);
|
|
8509
|
-
|
|
8510
|
-
if (
|
|
8511
|
-
const head = words
|
|
8512
|
-
invariant(head !== void 0, "
|
|
8513
|
-
const rest = words.slice(start + 1);
|
|
8931
|
+
if (words.length === 0 || splitCommandWords(text).length !== words.length) return null;
|
|
8932
|
+
if (words.some((word) => hasShellExpansion(word.raw))) return null;
|
|
8933
|
+
const [head, ...rest] = words;
|
|
8934
|
+
invariant(head !== void 0, "the segment holds at least one word");
|
|
8514
8935
|
const name = head.raw.slice(head.raw.lastIndexOf("/") + 1);
|
|
8515
|
-
|
|
8516
|
-
if (name
|
|
8517
|
-
const
|
|
8518
|
-
return
|
|
8936
|
+
const args = rest.map((word) => word.value);
|
|
8937
|
+
if (READ_ONLY_COMMANDS.has(name)) {
|
|
8938
|
+
const unsafe = UNSAFE_OPTIONS.get(name);
|
|
8939
|
+
return unsafe === void 0 || !hasUnsafeOption(args, unsafe) ? "Read-only actions" : null;
|
|
8519
8940
|
}
|
|
8941
|
+
if (name === "git") return classifyGit(args, cwd);
|
|
8520
8942
|
if (name === "rm") return classifyRemove(rest, cwd);
|
|
8521
8943
|
return null;
|
|
8522
8944
|
}
|
|
8945
|
+
function hasUnsafeOption(args, unsafe) {
|
|
8946
|
+
if (args.some((arg) => unsafe.words?.includes(arg) === true)) return true;
|
|
8947
|
+
let operands = 0;
|
|
8948
|
+
let index = 0;
|
|
8949
|
+
while (index < args.length) {
|
|
8950
|
+
const arg = args[index] ?? "";
|
|
8951
|
+
if (arg === "--") {
|
|
8952
|
+
operands += args.length - index - 1;
|
|
8953
|
+
break;
|
|
8954
|
+
}
|
|
8955
|
+
if (isOptionWord(arg)) {
|
|
8956
|
+
const option = parseOption(arg, unsafe);
|
|
8957
|
+
if (option.isUnsafe) return true;
|
|
8958
|
+
if (option.takesNextWord && !isOptionWord(args[index + 1] ?? "-")) index += 1;
|
|
8959
|
+
} else operands += 1;
|
|
8960
|
+
index += 1;
|
|
8961
|
+
}
|
|
8962
|
+
return unsafe.operands !== void 0 && operands > unsafe.operands;
|
|
8963
|
+
}
|
|
8964
|
+
function parseOption(arg, unsafe) {
|
|
8965
|
+
const valued = unsafe.valued;
|
|
8966
|
+
if (arg.startsWith("--")) {
|
|
8967
|
+
const [name = "", ...value] = arg.slice(2).split("=");
|
|
8968
|
+
return {
|
|
8969
|
+
isUnsafe: name !== "" && unsafe.long?.some((long) => long.startsWith(name)) === true,
|
|
8970
|
+
takesNextWord: value.length === 0 && valued?.long.includes(name) === true
|
|
8971
|
+
};
|
|
8972
|
+
}
|
|
8973
|
+
for (let index = 1; index < arg.length; index += 1) {
|
|
8974
|
+
const letter = arg.charAt(index);
|
|
8975
|
+
if (unsafe.short?.includes(letter) === true) return {
|
|
8976
|
+
isUnsafe: true,
|
|
8977
|
+
takesNextWord: false
|
|
8978
|
+
};
|
|
8979
|
+
if (valued?.attached?.includes(letter) === true) break;
|
|
8980
|
+
if (valued?.short.includes(letter) === true) return {
|
|
8981
|
+
isUnsafe: false,
|
|
8982
|
+
takesNextWord: index === arg.length - 1
|
|
8983
|
+
};
|
|
8984
|
+
}
|
|
8985
|
+
return {
|
|
8986
|
+
isUnsafe: false,
|
|
8987
|
+
takesNextWord: false
|
|
8988
|
+
};
|
|
8989
|
+
}
|
|
8990
|
+
function isOptionWord(arg) {
|
|
8991
|
+
return arg.startsWith("-") && arg !== "-";
|
|
8992
|
+
}
|
|
8993
|
+
function classifyGit(args, cwd) {
|
|
8994
|
+
const git = splitGitCommand(args, cwd, "");
|
|
8995
|
+
const globals = args.slice(0, args.length - git.args.length);
|
|
8996
|
+
const [sub, ...subArgs] = git.args;
|
|
8997
|
+
if (sub === void 0 || !READ_ONLY_GIT_SUBCOMMANDS.has(sub)) return null;
|
|
8998
|
+
return hasOnlySafeGitGlobals(globals) && !hasUnsafeOption(subArgs, UNSAFE_GIT_OPTIONS) ? "Read-only actions" : null;
|
|
8999
|
+
}
|
|
9000
|
+
function hasOnlySafeGitGlobals(globals) {
|
|
9001
|
+
let index = 0;
|
|
9002
|
+
while (index < globals.length) {
|
|
9003
|
+
const option = globals[index] ?? "";
|
|
9004
|
+
if (option === "-C") {
|
|
9005
|
+
const directory = globals[index + 1];
|
|
9006
|
+
if (directory === void 0 || directory.startsWith("-")) return false;
|
|
9007
|
+
index += 2;
|
|
9008
|
+
} else if (SAFE_GIT_GLOBAL_FLAGS.has(option)) index += 1;
|
|
9009
|
+
else return false;
|
|
9010
|
+
}
|
|
9011
|
+
return true;
|
|
9012
|
+
}
|
|
8523
9013
|
function classifyRemove(args, cwd) {
|
|
8524
9014
|
const paths = args.filter((arg) => !arg.raw.startsWith("-"));
|
|
8525
9015
|
if (paths.length === 0) return null;
|
|
@@ -8541,6 +9031,23 @@ function splitWords(text) {
|
|
|
8541
9031
|
value: token.value
|
|
8542
9032
|
}] : []);
|
|
8543
9033
|
}
|
|
9034
|
+
function hasShellExpansion(raw) {
|
|
9035
|
+
let quote = null;
|
|
9036
|
+
let previous = "";
|
|
9037
|
+
for (const char of raw) {
|
|
9038
|
+
if (quote === "'") quote = char === "'" ? null : quote;
|
|
9039
|
+
else if (char === "\\" || char === "$" || char === "`") return true;
|
|
9040
|
+
else if (char === quote) quote = null;
|
|
9041
|
+
else if (quote === null && (char === "'" || char === "\"")) quote = char;
|
|
9042
|
+
else if (quote === null && isExpansionCharacter(char, previous)) return true;
|
|
9043
|
+
previous = char;
|
|
9044
|
+
}
|
|
9045
|
+
return false;
|
|
9046
|
+
}
|
|
9047
|
+
function isExpansionCharacter(char, previous) {
|
|
9048
|
+
if (char === "~") return previous === "" || previous === "=" || previous === ":";
|
|
9049
|
+
return "{}*?[]()!".includes(char) || /\s/u.test(char);
|
|
9050
|
+
}
|
|
8544
9051
|
function isPlainOperand(word) {
|
|
8545
9052
|
return [
|
|
8546
9053
|
word.value,
|
|
@@ -8564,7 +9071,7 @@ function mergeScopeFacts(all) {
|
|
|
8564
9071
|
//#region src/scope/build-task-scope.ts
|
|
8565
9072
|
function buildTaskScope(input) {
|
|
8566
9073
|
const merged = mergeScopeFacts(input.facts);
|
|
8567
|
-
const branches = merged.branches.filter((branch) => branch !== input.defaultBranch && !(input.defaultBranch === null &&
|
|
9074
|
+
const branches = merged.branches.filter((branch) => branch !== input.defaultBranch && !(input.defaultBranch === null && PROTECTED_BRANCH_NAMES.has(branch)));
|
|
8568
9075
|
const repositories = new Set(input.remotes.map((remote) => toRepositorySlug(remote.url)));
|
|
8569
9076
|
const pullRequests = merged.pullRequests.filter((pull) => branches.includes(pull.head) && repositories.has(pull.repository)).map((pull) => ({
|
|
8570
9077
|
number: pull.number,
|
|
@@ -8580,12 +9087,6 @@ function buildTaskScope(input) {
|
|
|
8580
9087
|
pathGlobs: merged.pathGlobs
|
|
8581
9088
|
};
|
|
8582
9089
|
}
|
|
8583
|
-
const DEFAULT_NAMES = /* @__PURE__ */ new Set([
|
|
8584
|
-
"main",
|
|
8585
|
-
"master",
|
|
8586
|
-
"trunk",
|
|
8587
|
-
"develop"
|
|
8588
|
-
]);
|
|
8589
9090
|
//#endregion
|
|
8590
9091
|
//#region src/scope/build-atc-scope-facts.ts
|
|
8591
9092
|
function buildAtcScopeFacts(record, isInRepository) {
|
|
@@ -8818,7 +9319,8 @@ async function classifyAction(request, config, options = {}) {
|
|
|
8818
9319
|
...outcome.denials === void 0 ? {} : { denials: outcome.denials },
|
|
8819
9320
|
...outcome.escalation === void 0 ? {} : { escalation: outcome.escalation },
|
|
8820
9321
|
...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics },
|
|
8821
|
-
...outcome.judge === void 0 ? {} : { judge: outcome.judge }
|
|
9322
|
+
...outcome.judge === void 0 ? {} : { judge: outcome.judge },
|
|
9323
|
+
...outcome.grant === void 0 ? {} : { grant: outcome.grant }
|
|
8822
9324
|
}, host, stderr);
|
|
8823
9325
|
return outcome;
|
|
8824
9326
|
}
|
|
@@ -8874,17 +9376,51 @@ async function classifyStages(request, config, options) {
|
|
|
8874
9376
|
findCheckout: findActionCheckout
|
|
8875
9377
|
}, config, host, options.stderr);
|
|
8876
9378
|
const containment = scope === null ? null : checkContainment(request, scope, host.scratchPaths);
|
|
8877
|
-
|
|
8878
|
-
|
|
9379
|
+
const stageOptions = {
|
|
9380
|
+
...options,
|
|
9381
|
+
claudeSettings,
|
|
9382
|
+
findCheckout: findActionCheckout
|
|
9383
|
+
};
|
|
9384
|
+
const facts = {
|
|
9385
|
+
local,
|
|
9386
|
+
scope,
|
|
9387
|
+
checkout,
|
|
9388
|
+
hasConfiguredDenies
|
|
9389
|
+
};
|
|
9390
|
+
if (scope === null || containment === null) return classifyPastContainment(request, config, stageOptions, facts);
|
|
9391
|
+
const containmentNote = `denied by the containment check: ${containment.findings.map((finding) => finding.target).join(", ")}`;
|
|
9392
|
+
const [grant, guidance] = await Promise.all([classifyUserNamedGrant(request, config, options, {
|
|
9393
|
+
containment,
|
|
9394
|
+
scope,
|
|
9395
|
+
checkout
|
|
9396
|
+
}), tryReadDenialGuidance()]);
|
|
9397
|
+
if (grant === null || grant.verdict.kind === "deny") {
|
|
9398
|
+
const deny = grant?.verdict.kind === "deny" ? grant.verdict : {
|
|
9399
|
+
rule: containment.rule,
|
|
9400
|
+
reason: containment.reason
|
|
9401
|
+
};
|
|
8879
9402
|
return {
|
|
8880
|
-
verdict: buildGuidedDeny(
|
|
9403
|
+
verdict: buildGuidedDeny(deny.rule, deny.reason, guidance),
|
|
8881
9404
|
decidingStage: "containment",
|
|
8882
|
-
note:
|
|
8883
|
-
status: "deny"
|
|
9405
|
+
note: grant === null ? containmentNote : `${containmentNote}; judge ${grant.status} (${grant.diagnostics.elapsedMs}ms)`,
|
|
9406
|
+
status: "deny",
|
|
9407
|
+
...grant === null ? {} : { grant: grant.diagnostics }
|
|
8884
9408
|
};
|
|
8885
9409
|
}
|
|
8886
|
-
|
|
8887
|
-
|
|
9410
|
+
const outcome = await classifyPastContainment(request, config, stageOptions, facts);
|
|
9411
|
+
return {
|
|
9412
|
+
...outcome,
|
|
9413
|
+
note: `containment cleared by the judge for the targets the user named (${grant.diagnostics.elapsedMs}ms); ${outcome.note}`,
|
|
9414
|
+
grant: grant.diagnostics
|
|
9415
|
+
};
|
|
9416
|
+
}
|
|
9417
|
+
async function classifyPastContainment(request, config, options, facts) {
|
|
9418
|
+
const host = options.host;
|
|
9419
|
+
const scope = facts.scope;
|
|
9420
|
+
const findActionCheckout = options.findCheckout;
|
|
9421
|
+
const claudeSettings = options.claudeSettings;
|
|
9422
|
+
if (facts.local.kind === "allow") return buildLocalAllow(facts.local.exception);
|
|
9423
|
+
const edit = facts.hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope, host, { findCheckout: findActionCheckout });
|
|
8888
9424
|
if (edit?.kind === "bypass") return {
|
|
8889
9425
|
verdict: { kind: "allow" },
|
|
8890
9426
|
decidingStage: "bypass",
|
|
@@ -8903,7 +9439,7 @@ async function classifyStages(request, config, options) {
|
|
|
8903
9439
|
taskScope,
|
|
8904
9440
|
host,
|
|
8905
9441
|
claudeSettings,
|
|
8906
|
-
checkout: checkout ?? void 0,
|
|
9442
|
+
checkout: facts.checkout ?? void 0,
|
|
8907
9443
|
findCheckout: findActionCheckout
|
|
8908
9444
|
}), tryReadDenialGuidance()]);
|
|
8909
9445
|
const judge = config.judge ?? null;
|
|
@@ -8933,6 +9469,37 @@ async function classifyStages(request, config, options) {
|
|
|
8933
9469
|
};
|
|
8934
9470
|
}
|
|
8935
9471
|
const READ_ONLY_EXCEPTION = "Read-only actions";
|
|
9472
|
+
async function classifyUserNamedGrant(request, config, options, facts) {
|
|
9473
|
+
const containment = facts.containment;
|
|
9474
|
+
const judge = config.judge ?? null;
|
|
9475
|
+
const message = findCurrentDirectUserMessage(request.decisionContext?.lastDirectUserMessage);
|
|
9476
|
+
if (judge === null || message === null || options.localOnly === true) return null;
|
|
9477
|
+
const targets = findUserNamedGrant(containment.findings, message, {
|
|
9478
|
+
home: facts.scope.home,
|
|
9479
|
+
defaultBranch: facts.checkout?.head?.defaultBranch ?? null
|
|
9480
|
+
});
|
|
9481
|
+
if (targets === null) return null;
|
|
9482
|
+
const text = await readContainmentRule().catch(() => null);
|
|
9483
|
+
if (text === null) return null;
|
|
9484
|
+
return classifyWithJudge(request, {
|
|
9485
|
+
rule: {
|
|
9486
|
+
name: containment.rule,
|
|
9487
|
+
tier: "soft",
|
|
9488
|
+
source: "shipped",
|
|
9489
|
+
text
|
|
9490
|
+
},
|
|
9491
|
+
basis: "matched",
|
|
9492
|
+
repositoryContext: facts.checkout === null ? null : buildRepositoryEvidence(facts.checkout, buildTaskScopeSummary(facts.scope)),
|
|
9493
|
+
containment: {
|
|
9494
|
+
reason: containment.reason,
|
|
9495
|
+
targets
|
|
9496
|
+
}
|
|
9497
|
+
}, judge, {
|
|
9498
|
+
...options,
|
|
9499
|
+
rulesPath: config.rulesPath,
|
|
9500
|
+
overturns: "consent"
|
|
9501
|
+
});
|
|
9502
|
+
}
|
|
8936
9503
|
async function tryLoadTaskScope(request, config, host, stderr) {
|
|
8937
9504
|
try {
|
|
8938
9505
|
return await loadTaskScope({
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
### Outside Task Scope
|
|
2
|
+
Writing to a target the task does not own: a path outside the task's worktrees,
|
|
3
|
+
a branch the task did not make, or a pull request the task did not open.
|
|
4
|
+
auto-mode's containment check found each target. The evidence field
|
|
5
|
+
`containment` holds the check's finding, which names every target and what the
|
|
6
|
+
task owns, and `namedTargets`, which pairs each target with the word of the last
|
|
7
|
+
direct user message that names it. The task scope is a fact the check read, not
|
|
8
|
+
a reading of the action, so this rule never clears as a misread.
|
|
9
|
+
|
|
10
|
+
A name alone is not consent. The message must ask for this operation — write,
|
|
11
|
+
edit, create, delete, remove, move, push, merge, or comment — on every listed
|
|
12
|
+
target. A target the message names only as a source to read, copy, port, or
|
|
13
|
+
compare from, as a place to look, or as a reference is not consent. Neither is a
|
|
14
|
+
request to check or report on it, a different or narrower operation on it, or a
|
|
15
|
+
word that happens to match the target's name but means something else.
|
|
16
|
+
|
|
17
|
+
**Must name:** the operation and every target.
|