@codyswann/lisa 4.84.5 → 4.84.7
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/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +21 -4
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/block-host-name-leak.mjs +130 -40
- package/plugins/lisa/hooks/block-host-name-leak.sh +5 -0
- package/plugins/lisa/hooks/host-name-command-context.mjs +409 -0
- package/plugins/lisa-agy/hooks/block-host-name-leak.mjs +130 -40
- package/plugins/lisa-agy/hooks/block-host-name-leak.sh +5 -0
- package/plugins/lisa-agy/hooks/host-name-command-context.mjs +409 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-host-name-leak.mjs +130 -40
- package/plugins/lisa-copilot/hooks/block-host-name-leak.sh +5 -0
- package/plugins/lisa-copilot/hooks/host-name-command-context.mjs +409 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-host-name-leak.mjs +130 -40
- package/plugins/lisa-cursor/hooks/block-host-name-leak.sh +5 -0
- package/plugins/lisa-cursor/hooks/host-name-command-context.mjs +409 -0
- package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/hooks/block-host-name-leak.mjs +130 -40
- package/plugins/lisa-devin/hooks/block-host-name-leak.sh +5 -0
- package/plugins/lisa-devin/hooks/host-name-command-context.mjs +409 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-host-name-leak.mjs +130 -40
- package/plugins/src/base/hooks/block-host-name-leak.sh +5 -0
- package/plugins/src/base/hooks/host-name-command-context.mjs +409 -0
- package/scripts/generate-agy-plugin-artifacts.mjs +7 -3
- package/scripts/lib/host-name-expanded-command-cases.mjs +286 -0
- package/scripts/lib/host-name-field-occurrence-cases.mjs +60 -0
- package/scripts/lib/host-name-review-cases.mjs +354 -0
- package/scripts/lib/host-name-target-cases.mjs +298 -0
- package/scripts/two-channel-couplings.json +29 -6
- package/scripts/verify-host-name-target-scope.mjs +373 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/** Bounded literal command/target resolution for the host-name guard; never a shell. */
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { statSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const VALUE_FLAGS = new Set([
|
|
7
|
+
"--repo",
|
|
8
|
+
"-R",
|
|
9
|
+
"--body",
|
|
10
|
+
"-b",
|
|
11
|
+
"--title",
|
|
12
|
+
"-t",
|
|
13
|
+
"--message",
|
|
14
|
+
"-m",
|
|
15
|
+
"--body-file",
|
|
16
|
+
"--input",
|
|
17
|
+
"-f",
|
|
18
|
+
"-F",
|
|
19
|
+
"--field",
|
|
20
|
+
"--raw-field",
|
|
21
|
+
"-X",
|
|
22
|
+
"--method",
|
|
23
|
+
"-H",
|
|
24
|
+
"--header",
|
|
25
|
+
"--hostname",
|
|
26
|
+
"-q",
|
|
27
|
+
"--jq",
|
|
28
|
+
"--template",
|
|
29
|
+
"-p",
|
|
30
|
+
"--preview",
|
|
31
|
+
"--base",
|
|
32
|
+
"--head",
|
|
33
|
+
"--assignee",
|
|
34
|
+
"--label",
|
|
35
|
+
"--milestone",
|
|
36
|
+
"--project",
|
|
37
|
+
"--reviewer",
|
|
38
|
+
]);
|
|
39
|
+
const FIELD_FLAGS = new Set(["-f", "-F", "--field", "--raw-field"]);
|
|
40
|
+
const METHODS = new Set(["POST", "PATCH", "PUT"]);
|
|
41
|
+
const SHORT_VALUES = /^(-[RbBtTmMfFXHqp])(.+)$/u;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read literal words and command separators, retaining expansion uncertainty.
|
|
45
|
+
* @param {string} command Raw Bash command.
|
|
46
|
+
* @returns {object[]} Literal words and separators.
|
|
47
|
+
*/
|
|
48
|
+
function words(command) {
|
|
49
|
+
const result = [];
|
|
50
|
+
let value = "",
|
|
51
|
+
quote = "",
|
|
52
|
+
literal = true,
|
|
53
|
+
started = false;
|
|
54
|
+
const flush = () => {
|
|
55
|
+
if (started) result.push({ value, literal });
|
|
56
|
+
value = "";
|
|
57
|
+
literal = true;
|
|
58
|
+
started = false;
|
|
59
|
+
};
|
|
60
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
61
|
+
const char = command[i];
|
|
62
|
+
if (char === "\\" && quote !== "'") {
|
|
63
|
+
const next = command[++i];
|
|
64
|
+
if (next === undefined) literal = false;
|
|
65
|
+
else if (next !== "\n") {
|
|
66
|
+
if (quote === '"' && !/[$`"\\]/u.test(next)) value += "\\";
|
|
67
|
+
value += next;
|
|
68
|
+
}
|
|
69
|
+
if (next !== "\n") started = true;
|
|
70
|
+
} else if (quote !== "") {
|
|
71
|
+
if (char === quote) quote = "";
|
|
72
|
+
else {
|
|
73
|
+
value += char;
|
|
74
|
+
if (quote === '"' && /[$`]/u.test(char)) literal = false;
|
|
75
|
+
}
|
|
76
|
+
started = true;
|
|
77
|
+
} else if (char === '"' || char === "'") {
|
|
78
|
+
quote = char;
|
|
79
|
+
started = true;
|
|
80
|
+
} else if (/[;&|\n()<>]/u.test(char)) {
|
|
81
|
+
flush();
|
|
82
|
+
const operator =
|
|
83
|
+
char === command[i + 1] && /[&|]/u.test(char)
|
|
84
|
+
? char + command[++i]
|
|
85
|
+
: char;
|
|
86
|
+
result.push({ operator });
|
|
87
|
+
} else if (/\s/u.test(char)) flush();
|
|
88
|
+
else {
|
|
89
|
+
value += char;
|
|
90
|
+
started = true;
|
|
91
|
+
if (/[$`*?[\]~]/u.test(char)) literal = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (quote !== "") literal = false;
|
|
95
|
+
flush();
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Split option values without treating body text as a second option/endpoint.
|
|
101
|
+
* @param {object[]} input Active gh arguments only.
|
|
102
|
+
* @returns {object} Parsed flags, positional words and normalized tokens.
|
|
103
|
+
*/
|
|
104
|
+
function argumentsOf(input) {
|
|
105
|
+
const flags = new Map(),
|
|
106
|
+
fieldOccurrences = [],
|
|
107
|
+
positional = [],
|
|
108
|
+
tokens = [];
|
|
109
|
+
// Any expansion can add/replace routing arguments after shell word splitting.
|
|
110
|
+
// Never evaluate it or certify a target from the remaining literal words.
|
|
111
|
+
let certain = input.every(word => word.literal);
|
|
112
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
113
|
+
const word = input[i];
|
|
114
|
+
if (word.value === "--") {
|
|
115
|
+
positional.push(...input.slice(i + 1));
|
|
116
|
+
tokens.push(...input.slice(i).map(item => item.value));
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
const equals = word.value.indexOf("=");
|
|
120
|
+
const directFlag = equals > 0 ? word.value.slice(0, equals) : word.value;
|
|
121
|
+
const attached = VALUE_FLAGS.has(directFlag)
|
|
122
|
+
? null
|
|
123
|
+
: SHORT_VALUES.exec(word.value);
|
|
124
|
+
const flag =
|
|
125
|
+
attached?.[1] ??
|
|
126
|
+
(equals > 0 && word.value.startsWith("-") ? directFlag : word.value);
|
|
127
|
+
tokens.push(flag);
|
|
128
|
+
if (VALUE_FLAGS.has(flag)) {
|
|
129
|
+
const value = attached
|
|
130
|
+
? { ...word, value: attached[2] }
|
|
131
|
+
: equals > 0
|
|
132
|
+
? { ...word, value: word.value.slice(equals + 1) }
|
|
133
|
+
: input[++i];
|
|
134
|
+
if (value === undefined) {
|
|
135
|
+
certain = false;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Alias spellings share one Cobra option; the last occurrence wins.
|
|
139
|
+
flags.set(
|
|
140
|
+
flag === "-R" ? "--repo" : flag === "-X" ? "--method" : flag,
|
|
141
|
+
value
|
|
142
|
+
);
|
|
143
|
+
// API fields are additive even though routing options use last values.
|
|
144
|
+
if (FIELD_FLAGS.has(flag)) fieldOccurrences.push([flag, value]);
|
|
145
|
+
tokens.push(value.value);
|
|
146
|
+
} else if (!word.value.startsWith("-")) positional.push(word);
|
|
147
|
+
else if (!word.literal) certain = false;
|
|
148
|
+
}
|
|
149
|
+
const nounWord = positional[0],
|
|
150
|
+
verbWord = positional[1];
|
|
151
|
+
const noun = nounWord?.literal ? nounWord.value : undefined,
|
|
152
|
+
verb = verbWord?.value;
|
|
153
|
+
// Command-name expansions can supply several argv words. A following
|
|
154
|
+
// read-looking literal does not prove that the expanded command is a read.
|
|
155
|
+
const uncertainName =
|
|
156
|
+
nounWord?.literal === false ||
|
|
157
|
+
(["issue", "pr"].includes(noun) && verbWord?.literal === false);
|
|
158
|
+
const method = flags.get("--method");
|
|
159
|
+
const writes =
|
|
160
|
+
uncertainName ||
|
|
161
|
+
(noun === "api"
|
|
162
|
+
? method
|
|
163
|
+
? !method.literal || METHODS.has(method.value.toUpperCase())
|
|
164
|
+
: [...FIELD_FLAGS].some(flag => flags.has(flag)) ||
|
|
165
|
+
Boolean(flags.get("--input")?.value)
|
|
166
|
+
: ["issue", "pr"].includes(noun) &&
|
|
167
|
+
["create", "comment", "edit"].includes(verb));
|
|
168
|
+
return {
|
|
169
|
+
flags,
|
|
170
|
+
fieldOccurrences,
|
|
171
|
+
positional,
|
|
172
|
+
tokens: ["gh", ...tokens],
|
|
173
|
+
noun,
|
|
174
|
+
writes,
|
|
175
|
+
certain,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Normalize only repository shapes gh/git actually accept; unresolved syntax is unknown.
|
|
181
|
+
* @param {string} spec Repository argument, URL or Git remote.
|
|
182
|
+
* @returns {string|undefined} Lowercased owner/repository, or unknown.
|
|
183
|
+
*/
|
|
184
|
+
export function repositoryFromSpec(spec) {
|
|
185
|
+
if (typeof spec !== "string" || /[\s${}`*?#[\]]/u.test(spec))
|
|
186
|
+
return undefined;
|
|
187
|
+
const normalized = spec
|
|
188
|
+
.trim()
|
|
189
|
+
.replace(/\.git\/?$/u, "")
|
|
190
|
+
.replace(/\/$/u, "");
|
|
191
|
+
const matched =
|
|
192
|
+
/^(?:(?:https?:\/\/|ssh:\/\/git@)[^/]+\/|git@[^:]+:|[^/:]+\/)?([\w.-]+)\/([\w.-]+)$/u.exec(
|
|
193
|
+
normalized
|
|
194
|
+
);
|
|
195
|
+
return matched ? `${matched[1]}/${matched[2]}`.toLowerCase() : undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Resolve literal cd and leading environment assignments across simple command lists.
|
|
200
|
+
* Pipelines, conditionals and other shell syntax never earn a target exemption.
|
|
201
|
+
* @param {string} command Raw Bash command.
|
|
202
|
+
* @param {string} initialCwd The actual hook process cwd, separate from detector root.
|
|
203
|
+
* @param {object} env The actual inherited environment.
|
|
204
|
+
* @returns {object[]} Active gh calls with their bounded command contexts.
|
|
205
|
+
*/
|
|
206
|
+
export function ghCommands(command, initialCwd, env = process.env) {
|
|
207
|
+
const calls = [];
|
|
208
|
+
const inherited = { ...env };
|
|
209
|
+
let cwd = initialCwd,
|
|
210
|
+
segment = [],
|
|
211
|
+
uncertain = false;
|
|
212
|
+
const consume = () => {
|
|
213
|
+
if (segment.length === 0) return;
|
|
214
|
+
const local = { ...inherited };
|
|
215
|
+
let index = segment[0].value === "env" ? 1 : 0;
|
|
216
|
+
while (/^[A-Za-z_][A-Za-z_0-9]*=/u.test(segment[index]?.value ?? "")) {
|
|
217
|
+
const item = segment[index++],
|
|
218
|
+
split = item.value.indexOf("=");
|
|
219
|
+
local[item.value.slice(0, split)] = item.literal
|
|
220
|
+
? item.value.slice(split + 1)
|
|
221
|
+
: undefined;
|
|
222
|
+
if (!item.literal) uncertain = true;
|
|
223
|
+
}
|
|
224
|
+
const executable = segment[index]?.value;
|
|
225
|
+
if (executable === "cd") {
|
|
226
|
+
const args = segment.slice(index + 1).filter(item => item.value !== "--");
|
|
227
|
+
const directory =
|
|
228
|
+
args.length === 1 && args[0].literal && !args[0].value.startsWith("-")
|
|
229
|
+
? args[0].value
|
|
230
|
+
: undefined;
|
|
231
|
+
try {
|
|
232
|
+
const resolved =
|
|
233
|
+
directory === undefined ||
|
|
234
|
+
(!path.isAbsolute(directory) &&
|
|
235
|
+
!/^\.\.?(?:\/|$)/u.test(directory) &&
|
|
236
|
+
(uncertain || local.CDPATH))
|
|
237
|
+
? undefined
|
|
238
|
+
: path.isAbsolute(directory)
|
|
239
|
+
? directory
|
|
240
|
+
: cwd === undefined
|
|
241
|
+
? undefined
|
|
242
|
+
: path.resolve(cwd, directory);
|
|
243
|
+
cwd =
|
|
244
|
+
resolved !== undefined && statSync(resolved).isDirectory()
|
|
245
|
+
? resolved
|
|
246
|
+
: undefined;
|
|
247
|
+
} catch {
|
|
248
|
+
cwd = undefined;
|
|
249
|
+
}
|
|
250
|
+
} else if (executable === "export") {
|
|
251
|
+
for (const item of segment.slice(index + 1)) {
|
|
252
|
+
const split = item.value.indexOf("=");
|
|
253
|
+
if (split < 1 || !item.literal) {
|
|
254
|
+
uncertain = true;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
inherited[item.value.slice(0, split)] = item.value.slice(split + 1);
|
|
258
|
+
}
|
|
259
|
+
} else if (executable !== undefined && path.basename(executable) === "gh") {
|
|
260
|
+
calls.push({
|
|
261
|
+
...argumentsOf(segment.slice(index + 1)),
|
|
262
|
+
cwd,
|
|
263
|
+
env: local,
|
|
264
|
+
certainContext: !uncertain,
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
// A function/source/pushd or unsupported launcher may change the shell
|
|
268
|
+
// context. Do not grant later writes an exemption based on the old cwd.
|
|
269
|
+
uncertain = true;
|
|
270
|
+
cwd = undefined;
|
|
271
|
+
const nested = segment.findIndex(item => item.value === "gh");
|
|
272
|
+
if (nested !== -1)
|
|
273
|
+
calls.push({
|
|
274
|
+
...argumentsOf(segment.slice(nested + 1)),
|
|
275
|
+
cwd,
|
|
276
|
+
env: local,
|
|
277
|
+
certainContext: false,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
segment = [];
|
|
281
|
+
};
|
|
282
|
+
for (const word of words(command)) {
|
|
283
|
+
if (word.operator === undefined) segment.push(word);
|
|
284
|
+
else {
|
|
285
|
+
consume();
|
|
286
|
+
if (!["&&", ";", "\n"].includes(word.operator)) {
|
|
287
|
+
uncertain = true;
|
|
288
|
+
cwd = undefined;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
consume();
|
|
293
|
+
return calls;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Read gh's local selected default, falling back only to one unambiguous remote.
|
|
298
|
+
* All reads share a five-second ceiling and execute no shell or network query.
|
|
299
|
+
* @param {object} context One gh command context.
|
|
300
|
+
* @returns {string|undefined} Verified repository, or unknown.
|
|
301
|
+
*/
|
|
302
|
+
function localRepository(context) {
|
|
303
|
+
if (!context.certainContext) return undefined;
|
|
304
|
+
if (context.env.GH_REPO) return repositoryFromSpec(context.env.GH_REPO);
|
|
305
|
+
if (context.cwd === undefined) return undefined;
|
|
306
|
+
const deadline = Date.now() + 5000;
|
|
307
|
+
const read = (command, args) => {
|
|
308
|
+
const remaining = deadline - Date.now();
|
|
309
|
+
if (remaining <= 0) throw new Error("Repository lookup deadline expired");
|
|
310
|
+
return execFileSync(command, args, {
|
|
311
|
+
cwd: context.cwd,
|
|
312
|
+
env: { ...context.env, GH_PROMPT_DISABLED: "1" },
|
|
313
|
+
timeout: remaining,
|
|
314
|
+
encoding: "utf8",
|
|
315
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
316
|
+
}).trim();
|
|
317
|
+
};
|
|
318
|
+
try {
|
|
319
|
+
const selected = read("gh", ["repo", "set-default", "--view"]);
|
|
320
|
+
if (selected !== "") return repositoryFromSpec(selected);
|
|
321
|
+
const remotes = read("git", ["remote"]).split("\n").filter(Boolean);
|
|
322
|
+
if (remotes.length !== 1) return undefined;
|
|
323
|
+
return repositoryFromSpec(read("git", ["remote", "get-url", remotes[0]]));
|
|
324
|
+
} catch {
|
|
325
|
+
// probe-direction: fail-closed — failed lookup grants no host exemption;
|
|
326
|
+
// the caller retains its inspection path for an undetermined target.
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Resolve only active command arguments; prose cannot masquerade as routing.
|
|
333
|
+
* @param {object} context One parsed gh call.
|
|
334
|
+
* @returns {string|undefined} Verified owner/repository, or unknown.
|
|
335
|
+
*/
|
|
336
|
+
export function commandRepository(context) {
|
|
337
|
+
if (!context.certain || !context.certainContext) return undefined;
|
|
338
|
+
if (context.noun !== "api") {
|
|
339
|
+
// gh 2.88.1 issue comment/edit and PR finder resolve a URL selector before
|
|
340
|
+
// BaseRepo (--repo/GH_REPO/local default). Body option values are not here.
|
|
341
|
+
const selectors = context.positional.slice(2);
|
|
342
|
+
const batch =
|
|
343
|
+
context.noun === "issue" && context.positional[1]?.value === "edit";
|
|
344
|
+
if (
|
|
345
|
+
(batch && selectors.length > 1) ||
|
|
346
|
+
(["comment", "edit"].includes(context.positional[1]?.value) &&
|
|
347
|
+
selectors.some(item => /^https?:\/\//u.test(item.value)))
|
|
348
|
+
) {
|
|
349
|
+
if (!batch && selectors.length !== 1) return undefined;
|
|
350
|
+
let repository, identity;
|
|
351
|
+
for (const item of selectors) {
|
|
352
|
+
// ParseIssuesFromArgs lets numeric selectors inherit the common URL repo.
|
|
353
|
+
if (batch && /^#?[+-]?\d+$/u.test(item.value)) {
|
|
354
|
+
const number = BigInt(item.value.replace(/^#/u, ""));
|
|
355
|
+
if (BigInt.asIntN(64, number) !== number) return undefined;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const selected =
|
|
359
|
+
/^https?:\/\/[^/]+\/([\w.-]+)\/([\w.-]+)\/(issues|pull)\/\d+(?:[/?#].*)?$/u.exec(
|
|
360
|
+
item.value
|
|
361
|
+
);
|
|
362
|
+
if (!selected || (context.noun === "pr" && selected[3] !== "pull"))
|
|
363
|
+
return undefined;
|
|
364
|
+
try {
|
|
365
|
+
const host = new URL(item.value).hostname
|
|
366
|
+
.replace(/^www\./u, "")
|
|
367
|
+
.toLowerCase();
|
|
368
|
+
const repo = batch
|
|
369
|
+
? `${selected[1]}/${selected[2]}`.toLowerCase()
|
|
370
|
+
: repositoryFromSpec(`${selected[1]}/${selected[2]}`);
|
|
371
|
+
const current = `${host}/${repo}`;
|
|
372
|
+
if (
|
|
373
|
+
!host ||
|
|
374
|
+
!repo ||
|
|
375
|
+
(identity !== undefined && identity !== current)
|
|
376
|
+
)
|
|
377
|
+
return undefined;
|
|
378
|
+
identity = current;
|
|
379
|
+
repository = repo;
|
|
380
|
+
} catch {
|
|
381
|
+
// probe-direction: fail-closed — invalid URL grants no host exemption;
|
|
382
|
+
// the caller retains its inspection path for an undetermined target.
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (repository !== undefined) return repository;
|
|
387
|
+
}
|
|
388
|
+
const explicit = context.flags.get("--repo") ?? context.flags.get("-R");
|
|
389
|
+
return explicit
|
|
390
|
+
? explicit.literal
|
|
391
|
+
? repositoryFromSpec(explicit.value)
|
|
392
|
+
: undefined
|
|
393
|
+
: localRepository(context);
|
|
394
|
+
}
|
|
395
|
+
const endpoint = context.positional[1];
|
|
396
|
+
if (!endpoint?.literal) return undefined;
|
|
397
|
+
let value = endpoint.value;
|
|
398
|
+
if (/\{(?:owner|repo)\}/u.test(value)) {
|
|
399
|
+
const repository = localRepository(context);
|
|
400
|
+
if (repository === undefined) return undefined;
|
|
401
|
+
const [owner, repo] = repository.split("/");
|
|
402
|
+
value = value.replaceAll("{owner}", owner).replaceAll("{repo}", repo);
|
|
403
|
+
}
|
|
404
|
+
const found =
|
|
405
|
+
/^(?:https?:\/\/[^/]+\/)?\/?repos\/([\w.-]+)\/([\w.-]+)(?:\/|$)/u.exec(
|
|
406
|
+
value
|
|
407
|
+
);
|
|
408
|
+
return found ? repositoryFromSpec(`${found[1]}/${found[2]}`) : undefined;
|
|
409
|
+
}
|
|
@@ -310,8 +310,8 @@ const AGY_PLUGIN_HOOKS = [
|
|
|
310
310
|
supportScripts: ["block-blind-automerge.sh", GUARD_DEDUPE_LIB],
|
|
311
311
|
},
|
|
312
312
|
{
|
|
313
|
-
//
|
|
314
|
-
//
|
|
313
|
+
// The classifier and bounded command-context resolver are Node modules,
|
|
314
|
+
// because the denylist they consult is
|
|
315
315
|
// compiled JavaScript and a bash reimplementation would be a second copy of
|
|
316
316
|
// the thing the guard exists to protect.
|
|
317
317
|
sourceScript: "block-host-name-leak.sh",
|
|
@@ -319,7 +319,11 @@ const AGY_PLUGIN_HOOKS = [
|
|
|
319
319
|
event: "PreToolUse",
|
|
320
320
|
matcher: "run_command",
|
|
321
321
|
agyScript: "block-host-name-leak.agy.sh",
|
|
322
|
-
supportScripts: [
|
|
322
|
+
supportScripts: [
|
|
323
|
+
"block-host-name-leak.sh",
|
|
324
|
+
"block-host-name-leak.mjs",
|
|
325
|
+
"host-name-command-context.mjs",
|
|
326
|
+
],
|
|
323
327
|
},
|
|
324
328
|
];
|
|
325
329
|
|