@codyswann/lisa 4.34.14 → 4.34.16
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/all/copy-overwrite/scripts/check-npm-publish-landed.mjs +250 -0
- package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +3 -5
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +20 -3
- package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +33 -8
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +80 -5
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +1 -1
- package/dist/codex/scripts/block-no-verify.sh +80 -5
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +28 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- 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 +24 -15
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-no-verify.sh +80 -5
- package/expo/copy-overwrite/scripts/check-lighthouse-details.mjs +13 -7
- package/package.json +5 -5
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +90 -0
- package/plugins/lisa/hooks/block-direct-issue-create.sh +20 -3
- package/plugins/lisa/hooks/block-managed-file-edits.sh +33 -8
- package/plugins/lisa/hooks/block-no-verify.agy.sh +80 -5
- package/plugins/lisa/hooks/block-no-verify.sh +80 -5
- package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +90 -0
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +20 -3
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +80 -5
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +90 -0
- 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-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +20 -3
- package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +33 -8
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +80 -5
- package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +90 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +20 -3
- package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +33 -8
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +80 -5
- package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +90 -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-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-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-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-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-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-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-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/src/base/hooks/block-direct-issue-create.sh +20 -3
- package/plugins/src/base/hooks/block-managed-file-edits.sh +33 -8
- package/plugins/src/base/hooks/block-no-verify.agy.sh +80 -5
- package/plugins/src/base/hooks/block-no-verify.sh +80 -5
- package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +90 -0
- package/scripts/check-npm-publish-landed.mjs +30 -0
- package/scripts/two-channel-couplings.json +17 -4
- package/typescript/copy-overwrite/.claude/hooks/worktree-create.sh +3 -1
- package/typescript/copy-overwrite/.prettierignore +1 -0
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +14 -54
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
3
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Prove a version reached the npm registry, rather than trusting the exit code
|
|
7
|
+
* of the command that was supposed to put it there.
|
|
8
|
+
*
|
|
9
|
+
* Measured incident (CodySwannGT/lisa#3684): `v4.33.7` was tagged, a GitHub
|
|
10
|
+
* Release was cut for it, and a provenance statement was written to the sigstore
|
|
11
|
+
* transparency log — and `registry.npmjs.org/@codyswann/lisa/4.33.7` returned
|
|
12
|
+
* 404, and still did a day later. Three artifacts asserted the release had
|
|
13
|
+
* happened; the registry, the only one a consumer can install from, disagreed.
|
|
14
|
+
* Nothing retried it and nothing flagged it: the version counter moved on to
|
|
15
|
+
* 4.33.8, which makes the hole invisible in every downstream view.
|
|
16
|
+
*
|
|
17
|
+
* The publish itself had failed with `E401 Unauthorized` from npm's OIDC
|
|
18
|
+
* trusted-publisher exchange. There is no long-lived token in that path to
|
|
19
|
+
* rotate — the credential is minted per run — and the next release published
|
|
20
|
+
* fine 17 minutes later through the identical code path. So the failure is
|
|
21
|
+
* INTERMITTENT, and that is exactly why it needs a check: a permanently broken
|
|
22
|
+
* credential announces itself on the next release, while one that fails once in
|
|
23
|
+
* N produces a permanently skipped version with no signal at all.
|
|
24
|
+
*
|
|
25
|
+
* ## The exact-version endpoint, never `dist-tags.latest`
|
|
26
|
+
*
|
|
27
|
+
* `<registry>/<name>/<version>` answers about the version in hand.
|
|
28
|
+
* `<registry>/<name>` → `dist-tags.latest` answers about whichever version the
|
|
29
|
+
* registry currently considers newest, and it lags a successful publish by
|
|
30
|
+
* several minutes (CodySwannGT/lisa#3685). Verifying against `latest` would
|
|
31
|
+
* report a false 404 for a publish that actually landed, which trains everyone
|
|
32
|
+
* to ignore the check — the failure mode that ends with the check deleted.
|
|
33
|
+
*
|
|
34
|
+
* ## Three verdicts, because two would hide one of them
|
|
35
|
+
*
|
|
36
|
+
* - `published` — the registry served this exact version. Exit 0.
|
|
37
|
+
* - `missing` — the registry answered, and said 404. Exit 1.
|
|
38
|
+
* - `unprovable` — the registry could not be asked: transport error, 5xx, or a
|
|
39
|
+
* 200 whose body names a different version. Exit 1.
|
|
40
|
+
*
|
|
41
|
+
* `unprovable` blocks. It is not a pass, because nothing was proved, and a
|
|
42
|
+
* check that returns success for an input it never examined is the defect this
|
|
43
|
+
* script exists to catch, one layer up. It is not `missing` either: drafting a
|
|
44
|
+
* GitHub Release or alarming a human on a registry blip would be a false
|
|
45
|
+
* accusation, so the caller gets to tell the two apart and act differently.
|
|
46
|
+
* That distinction is the whole reason the verdict is a word on stdout and not
|
|
47
|
+
* just an exit code.
|
|
48
|
+
* @module scripts/check-npm-publish-landed
|
|
49
|
+
*/
|
|
50
|
+
import process from "node:process";
|
|
51
|
+
|
|
52
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
53
|
+
|
|
54
|
+
/** Public npm registry, the default for every Lisa release. */
|
|
55
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
56
|
+
|
|
57
|
+
/** How many times to ask before settling on an answer. */
|
|
58
|
+
const DEFAULT_ATTEMPTS = 5;
|
|
59
|
+
|
|
60
|
+
/** Pause between attempts, in milliseconds. */
|
|
61
|
+
const DEFAULT_DELAY_MS = 3000;
|
|
62
|
+
|
|
63
|
+
/** The line a caller greps for. Never printed without a real verdict behind it. */
|
|
64
|
+
const VERDICT_PREFIX = "npm-publish-landed:";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The URL that answers "is this exact version on the registry".
|
|
68
|
+
*
|
|
69
|
+
* Deliberately the only URL this module ever builds. A packument read
|
|
70
|
+
* (`<registry>/<name>`) would carry `dist-tags`, and having that object in hand
|
|
71
|
+
* is all it takes for someone to start consulting it — so the shape that could
|
|
72
|
+
* regress is simply never fetched.
|
|
73
|
+
* @param {string} registry - Registry origin, no trailing slash.
|
|
74
|
+
* @param {string} name - Package name, scope included.
|
|
75
|
+
* @param {string} version - Exact version, no range syntax.
|
|
76
|
+
* @returns {string} The exact-version manifest URL.
|
|
77
|
+
*/
|
|
78
|
+
export function exactVersionUrl(registry, name, version) {
|
|
79
|
+
// Trailing slashes trimmed without a regex: `/\/+$/` is a super-linear
|
|
80
|
+
// pattern the shipped ruleset rejects outright, and an origin arriving from
|
|
81
|
+
// configuration is exactly the kind of attacker-adjacent input that rule
|
|
82
|
+
// exists for.
|
|
83
|
+
let origin = registry;
|
|
84
|
+
while (origin.endsWith("/")) origin = origin.slice(0, -1);
|
|
85
|
+
return `${origin}/${name}/${version}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Ask the registry once.
|
|
90
|
+
* @param {string} url - The exact-version manifest URL.
|
|
91
|
+
* @param {typeof fetch} fetchImpl - Injected for tests.
|
|
92
|
+
* @param {string} version - The version the answer must name.
|
|
93
|
+
* @returns {Promise<{verdict: string, detail: string}>} One attempt's outcome.
|
|
94
|
+
*/
|
|
95
|
+
async function askOnce(url, fetchImpl, version) {
|
|
96
|
+
let response;
|
|
97
|
+
try {
|
|
98
|
+
response = await fetchImpl(url, {
|
|
99
|
+
headers: { accept: "application/json" },
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return { verdict: "unprovable", detail: `network: ${error.message}` };
|
|
103
|
+
}
|
|
104
|
+
if (response.status === 404) {
|
|
105
|
+
return { verdict: "missing", detail: "registry returned 404" };
|
|
106
|
+
}
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
return { verdict: "unprovable", detail: `HTTP ${response.status}` };
|
|
109
|
+
}
|
|
110
|
+
let manifest;
|
|
111
|
+
try {
|
|
112
|
+
manifest = await response.json();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
verdict: "unprovable",
|
|
116
|
+
detail: `unparseable body: ${error.message}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// A 200 that names a different version is not this version being present. The
|
|
120
|
+
// registry is not expected to do this; treating it as proof anyway is how a
|
|
121
|
+
// surprise becomes a silent pass.
|
|
122
|
+
if (manifest?.version !== version) {
|
|
123
|
+
return {
|
|
124
|
+
verdict: "unprovable",
|
|
125
|
+
detail: `HTTP 200 but the body names version ${String(manifest?.version)}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return { verdict: "published", detail: "registry served this exact version" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Whether an attempt's verdict is worth another try.
|
|
133
|
+
*
|
|
134
|
+
* Both non-published verdicts are retried, for different reasons. `missing` is
|
|
135
|
+
* retried because a publish that just succeeded may not have propagated yet;
|
|
136
|
+
* `unprovable` because a 5xx or a dropped connection says nothing about the
|
|
137
|
+
* package. Neither is allowed to become `published` — a retry can only confirm
|
|
138
|
+
* or replace a non-answer, never upgrade one.
|
|
139
|
+
* @param {string} verdict - The attempt's verdict.
|
|
140
|
+
* @returns {boolean} Whether to ask again.
|
|
141
|
+
*/
|
|
142
|
+
const worthRetrying = verdict => verdict !== "published";
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Prove, or fail to prove, that one version is on the registry.
|
|
146
|
+
* @param {object} options - Everything the check needs.
|
|
147
|
+
* @param {string} options.packageName - Package name, scope included.
|
|
148
|
+
* @param {string} options.version - Exact version to look for.
|
|
149
|
+
* @param {string} [options.registry] - Registry origin.
|
|
150
|
+
* @param {number} [options.attempts] - How many times to ask.
|
|
151
|
+
* @param {number} [options.delayMs] - Pause between attempts.
|
|
152
|
+
* @param {typeof fetch} [options.fetchImpl] - Injected for tests.
|
|
153
|
+
* @param {(ms: number) => Promise<void>} [options.sleep] - Injected for tests.
|
|
154
|
+
* @returns {Promise<{verdict: string, detail: string, urls: string[]}>} Outcome.
|
|
155
|
+
*/
|
|
156
|
+
export async function verifyPublish({
|
|
157
|
+
packageName,
|
|
158
|
+
version,
|
|
159
|
+
registry = DEFAULT_REGISTRY,
|
|
160
|
+
attempts = DEFAULT_ATTEMPTS,
|
|
161
|
+
delayMs = DEFAULT_DELAY_MS,
|
|
162
|
+
fetchImpl = fetch,
|
|
163
|
+
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
164
|
+
}) {
|
|
165
|
+
const url = exactVersionUrl(registry, packageName, version);
|
|
166
|
+
const urls = [];
|
|
167
|
+
let outcome = { verdict: "unprovable", detail: "no attempt was made" };
|
|
168
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
169
|
+
urls.push(url);
|
|
170
|
+
outcome = await askOnce(url, fetchImpl, version);
|
|
171
|
+
if (!worthRetrying(outcome.verdict)) break;
|
|
172
|
+
if (attempt < attempts) await sleep(delayMs);
|
|
173
|
+
}
|
|
174
|
+
return { ...outcome, urls };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Return a required CLI option, or exit naming it. */
|
|
178
|
+
function requiredOption(argv, name) {
|
|
179
|
+
const index = argv.indexOf(name);
|
|
180
|
+
const value = index === -1 ? undefined : argv[index + 1];
|
|
181
|
+
if (value === undefined || value.startsWith("--")) {
|
|
182
|
+
throw new Error(`Missing required option ${name}`);
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Return an optional CLI option. */
|
|
188
|
+
function option(argv, name, fallback) {
|
|
189
|
+
const index = argv.indexOf(name);
|
|
190
|
+
const value = index === -1 ? undefined : argv[index + 1];
|
|
191
|
+
return value === undefined || value.startsWith("--") ? fallback : value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Run the check and report, for a caller that is a workflow step.
|
|
196
|
+
* @param {string[]} argv - Arguments after the script path.
|
|
197
|
+
* @param {object} [injected] - Test seams forwarded to {@link verifyPublish}.
|
|
198
|
+
* @returns {Promise<number>} Process exit code.
|
|
199
|
+
*/
|
|
200
|
+
export async function main(argv, injected = {}) {
|
|
201
|
+
const packageName = requiredOption(argv, "--package");
|
|
202
|
+
const version = requiredOption(argv, "--version");
|
|
203
|
+
const registry = option(argv, "--registry", DEFAULT_REGISTRY);
|
|
204
|
+
const attempts = Number(option(argv, "--attempts", String(DEFAULT_ATTEMPTS)));
|
|
205
|
+
const delayMs = Number(option(argv, "--delay-ms", String(DEFAULT_DELAY_MS)));
|
|
206
|
+
|
|
207
|
+
const { verdict, detail, urls } = await verifyPublish({
|
|
208
|
+
packageName,
|
|
209
|
+
version,
|
|
210
|
+
registry,
|
|
211
|
+
attempts,
|
|
212
|
+
delayMs,
|
|
213
|
+
...injected,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
process.stdout.write(
|
|
217
|
+
`${VERDICT_PREFIX} ${verdict} package=${packageName} version=${version}\n`
|
|
218
|
+
);
|
|
219
|
+
if (verdict === "published") {
|
|
220
|
+
process.stdout.write(`✅ ${packageName}@${version} is on ${registry}.\n`);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
const asked = urls[0] ?? exactVersionUrl(registry, packageName, version);
|
|
224
|
+
process.stderr.write(
|
|
225
|
+
verdict === "missing"
|
|
226
|
+
? `❌ ${packageName}@${version} is NOT on the registry.\n` +
|
|
227
|
+
` ${asked} → 404 after ${urls.length} attempt(s).\n` +
|
|
228
|
+
` A tag, a GitHub Release and a provenance statement can all exist\n` +
|
|
229
|
+
` for a version consumers cannot install. This is that state.\n`
|
|
230
|
+
: `❌ Could not prove ${packageName}@${version} reached the registry.\n` +
|
|
231
|
+
` ${asked} → ${detail} (after ${urls.length} attempt(s)).\n` +
|
|
232
|
+
` This is NOT a clean result and NOT a confirmed miss — nothing was\n` +
|
|
233
|
+
` proved either way, so it blocks rather than reporting a verdict it\n` +
|
|
234
|
+
` does not have.\n`
|
|
235
|
+
);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* c8 ignore start -- CLI wiring, exercised through main() in tests */
|
|
240
|
+
if (invokedAsScript(import.meta.url)) {
|
|
241
|
+
main(process.argv.slice(2))
|
|
242
|
+
.then(code => {
|
|
243
|
+
process.exitCode = code;
|
|
244
|
+
})
|
|
245
|
+
.catch(error => {
|
|
246
|
+
process.stderr.write(`❌ ${error.message}\n`);
|
|
247
|
+
process.exitCode = 1;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/* c8 ignore stop */
|
|
@@ -486,12 +486,10 @@ function killedVerdict(code, load) {
|
|
|
486
486
|
* way this whole module can do harm.
|
|
487
487
|
*/
|
|
488
488
|
const RESOURCE_REFUSAL_PATTERNS = Object.freeze([
|
|
489
|
-
/\
|
|
489
|
+
/\bchild setpgid\s*\(\d+\s+to\s+\d+\):\s*Operation not permitted/,
|
|
490
490
|
/\bfork:\s*Resource temporarily unavailable/,
|
|
491
|
-
/\bposix_spawn\b[^\n]*\b(?:EAGAIN|ENOMEM)\b/,
|
|
492
|
-
/\bspawn\b[^\n]*\
|
|
493
|
-
/\bEMFILE\b|\bToo many open files\b/,
|
|
494
|
-
/\bENOMEM\b|\bCannot allocate memory\b/,
|
|
491
|
+
/\bposix_spawn\b[^\n]*\b(?:EAGAIN|ENOMEM|EMFILE)\b/,
|
|
492
|
+
/\bspawn\b[^\n]*\b(?:EAGAIN|ENOMEM|EMFILE)\b/,
|
|
495
493
|
]);
|
|
496
494
|
|
|
497
495
|
/**
|
|
@@ -1435,8 +1435,20 @@ EXEC_WRAPPERS = {
|
|
|
1435
1435
|
}
|
|
1436
1436
|
|
|
1437
1437
|
# Interpreter options that mean "no script file follows": a command string or a
|
|
1438
|
-
# module name, both of which the guard reads by other means or not at all.
|
|
1439
|
-
|
|
1438
|
+
# module name, both of which the guard reads by other means or not at all. These
|
|
1439
|
+
# are interpreter-specific on purpose. `bash -e create.sh` and `sh -m create.sh`
|
|
1440
|
+
# still execute the following file; treating Python and Node flags as universal
|
|
1441
|
+
# silently stopped the scan before those shell operands.
|
|
1442
|
+
NO_SCRIPT_OPTIONS = {
|
|
1443
|
+
"bun": {"-e", "--eval"},
|
|
1444
|
+
"node": {"-e", "--eval"},
|
|
1445
|
+
"nodejs": {"-e", "--eval"},
|
|
1446
|
+
"perl": {"-e"},
|
|
1447
|
+
"php": {"-r"},
|
|
1448
|
+
"python": {"-c", "-m", "--command", "--module"},
|
|
1449
|
+
"python3": {"-c", "-m", "--command", "--module"},
|
|
1450
|
+
"ruby": {"-e"},
|
|
1451
|
+
}
|
|
1440
1452
|
|
|
1441
1453
|
# Programs that READ their operands and never execute them as programs.
|
|
1442
1454
|
#
|
|
@@ -1572,7 +1584,12 @@ def executed_operand(argv):
|
|
|
1572
1584
|
if argument.startswith("-"):
|
|
1573
1585
|
# A command string or a module name; no script file follows.
|
|
1574
1586
|
head = argument.split("=", 1)[0]
|
|
1575
|
-
if
|
|
1587
|
+
if program in {"bash", "dash", "ksh", "sh", "zsh"}:
|
|
1588
|
+
# Shell `-c` may be clustered (`-ec`), while `-e` and `-m` are
|
|
1589
|
+
# ordinary shell options and do not consume the script path.
|
|
1590
|
+
if not head.startswith("--") and "c" in head[1:]:
|
|
1591
|
+
return None
|
|
1592
|
+
elif head in NO_SCRIPT_OPTIONS.get(program, set()):
|
|
1576
1593
|
return None
|
|
1577
1594
|
continue
|
|
1578
1595
|
return argument
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
# from mine", and guessing "behind" is how a fork's stronger guard gets silently
|
|
94
94
|
# replaced by a weaker upstream one. Add a name here in the same commit that
|
|
95
95
|
# closes a vector.
|
|
96
|
-
# lisa-guard-capabilities: managed-path-resolution, lisaignore-precedence, generated-path-rebuild, redirect-target, tee-target, sed-in-place-all-spellings, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand
|
|
96
|
+
# lisa-guard-capabilities: managed-path-resolution, lisaignore-precedence, generated-path-rebuild, redirect-target, tee-target, sed-in-place-all-spellings, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand, direct-script-command-reach, analyzer-failure-visible
|
|
97
97
|
set -euo pipefail
|
|
98
98
|
|
|
99
99
|
input="$(cat)"
|
|
@@ -610,7 +610,9 @@ def command_word(statement):
|
|
|
610
610
|
statement: One statement's tokens.
|
|
611
611
|
|
|
612
612
|
Returns:
|
|
613
|
-
A
|
|
613
|
+
A triple (token, program, args); (None, None, []) when no program is
|
|
614
|
+
named. The raw token is retained because `./edit.sh` has no interpreter:
|
|
615
|
+
its command word is itself the executed script.
|
|
614
616
|
"""
|
|
615
617
|
index = 0
|
|
616
618
|
while index < len(statement):
|
|
@@ -620,7 +622,7 @@ def command_word(statement):
|
|
|
620
622
|
continue
|
|
621
623
|
program = token.rsplit("/", 1)[-1]
|
|
622
624
|
if program not in FOLLOW_WRAPPERS:
|
|
623
|
-
return (program, statement[index + 1 :])
|
|
625
|
+
return (token, program, statement[index + 1 :])
|
|
624
626
|
separate, positional = FOLLOW_WRAPPERS[program]
|
|
625
627
|
index += 1
|
|
626
628
|
while index < len(statement) and statement[index].startswith("-"):
|
|
@@ -630,7 +632,7 @@ def command_word(statement):
|
|
|
630
632
|
break
|
|
631
633
|
index += 2 if option in separate else 1
|
|
632
634
|
index += positional
|
|
633
|
-
return (None, [])
|
|
635
|
+
return (None, None, [])
|
|
634
636
|
|
|
635
637
|
|
|
636
638
|
def shell_script_operand(args):
|
|
@@ -677,7 +679,7 @@ def write_targets(statement):
|
|
|
677
679
|
Candidate paths this statement writes.
|
|
678
680
|
"""
|
|
679
681
|
found = []
|
|
680
|
-
program, args = command_word(statement)
|
|
682
|
+
_token, program, args = command_word(statement)
|
|
681
683
|
for index, token in enumerate(statement):
|
|
682
684
|
if token in REDIRECT_OUT and index + 1 < len(statement):
|
|
683
685
|
found.append(statement[index + 1])
|
|
@@ -700,7 +702,7 @@ def executed_script(statement):
|
|
|
700
702
|
Returns:
|
|
701
703
|
The operand token naming a script that will run, or None.
|
|
702
704
|
"""
|
|
703
|
-
program, args = command_word(statement)
|
|
705
|
+
token, program, args = command_word(statement)
|
|
704
706
|
if program is None:
|
|
705
707
|
return None
|
|
706
708
|
if program in SHELL_PROGRAMS:
|
|
@@ -711,6 +713,12 @@ def executed_script(statement):
|
|
|
711
713
|
return shell_script_operand(args)
|
|
712
714
|
if program in SOURCE_BUILTINS:
|
|
713
715
|
return args[0] if args else None
|
|
716
|
+
# A script in command position executes through its shebang. This also
|
|
717
|
+
# covers a direct path after wrappers such as `sudo ./edit.sh`; `command_word`
|
|
718
|
+
# deliberately keeps the raw token while using its basename for wrapper and
|
|
719
|
+
# shell classification.
|
|
720
|
+
if resolve(token) is not None:
|
|
721
|
+
return token
|
|
714
722
|
return None
|
|
715
723
|
|
|
716
724
|
|
|
@@ -801,10 +809,27 @@ for target in targets:
|
|
|
801
809
|
if cleaned:
|
|
802
810
|
print(cleaned)
|
|
803
811
|
PY
|
|
804
|
-
|
|
812
|
+
analyzer_stderr="$(mktemp "${TMPDIR:-/tmp}/lisa-managed-edit-analyzer.XXXXXX")" || {
|
|
813
|
+
printf 'block-managed-file-edits: could not allocate analyzer stderr capture; Bash write protection is NOT active\n' >&2
|
|
814
|
+
exit 0
|
|
815
|
+
}
|
|
816
|
+
if write_targets="$(printf '%s' "$analyzer" |
|
|
805
817
|
MANAGED_EDIT_COMMAND="$command_str" \
|
|
806
818
|
MANAGED_EDIT_PROJECT="$project_root" \
|
|
807
|
-
python3 - 2
|
|
819
|
+
python3 - 2>"$analyzer_stderr")"; then
|
|
820
|
+
analyzer_status=0
|
|
821
|
+
else
|
|
822
|
+
analyzer_status=$?
|
|
823
|
+
fi
|
|
824
|
+
if [ "$analyzer_status" -ne 0 ]; then
|
|
825
|
+
printf 'block-managed-file-edits: Bash analyzer failed (exit %s); Bash write protection is NOT active\n' "$analyzer_status" >&2
|
|
826
|
+
while IFS= read -r analyzer_line; do
|
|
827
|
+
[ -n "$analyzer_line" ] && printf ' python3: %s\n' "$analyzer_line" >&2
|
|
828
|
+
done <"$analyzer_stderr"
|
|
829
|
+
rm -f "$analyzer_stderr"
|
|
830
|
+
exit 0
|
|
831
|
+
fi
|
|
832
|
+
rm -f "$analyzer_stderr"
|
|
808
833
|
while IFS= read -r token; do
|
|
809
834
|
[ -n "$token" ] || continue
|
|
810
835
|
if source_path="$(managed_source "$token")"; then
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
# Add a name here in the same commit that closes a vector. A hardening that
|
|
37
37
|
# forgets to is invisible to refresh, and shows up as an unexplained diff at
|
|
38
38
|
# review time instead of a named capability.
|
|
39
|
-
# lisa-guard-capabilities: no-verify-abbrev, husky-env, hookspath-allowlist, config-env, env-split-string, git-config-key, git-config-parameters, git-config-parameters-append, git-config-parameters-expansion, heredoc-shell-word, herestring-aware, no-verify-short, nested-shell-no-verify, nested-shell-long-options, env-split-string-abbrev, command-wrapper-normalization, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand, dispatcher-exec-position
|
|
39
|
+
# lisa-guard-capabilities: no-verify-abbrev, husky-env, hookspath-allowlist, config-env, env-split-string, git-config-key, git-config-parameters, git-config-parameters-append, git-config-parameters-expansion, heredoc-shell-word, herestring-aware, no-verify-short, nested-shell-no-verify, nested-shell-long-options, env-split-string-abbrev, command-wrapper-normalization, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand, dispatcher-exec-position, tilde-script-reach, shell-option-stdin-reach
|
|
40
40
|
#
|
|
41
41
|
# Shell-token matching avoids false positives from issue bodies, heredocs, and
|
|
42
42
|
# commit-message prose while still catching quoted real argv values such as
|
|
@@ -268,8 +268,38 @@ ENV_LONG_SEPARATE_VALUE = {"--argv0", "--chdir", "--split-string", "--unset"}
|
|
|
268
268
|
|
|
269
269
|
# env's single-letter options, by whether the rest of a cluster is more
|
|
270
270
|
# options or the option's value.
|
|
271
|
+
#
|
|
272
|
+
# BOTH PLATFORMS' SETS, not one. These began as GNU's, and an option that is
|
|
273
|
+
# in neither set falls through to `ambiguous`, which this guard treats as
|
|
274
|
+
# suspicious. That default is correct and stays — an unknown option genuinely
|
|
275
|
+
# could take a value and swallow the command name. What is wrong is a
|
|
276
|
+
# well-known option being unknown, and on a macOS fleet `env -P` is exactly
|
|
277
|
+
# that: BSD's "search altpath for the utility", refused on every ordinary
|
|
278
|
+
# command it prefixes.
|
|
279
|
+
#
|
|
280
|
+
# Measured, one option at a time, against the guard before this change. BSD's
|
|
281
|
+
# complete short set is `0 i v u C P S`, from the synopsis in env(1):
|
|
282
|
+
#
|
|
283
|
+
# allowed -0 -i -v -u NAME -C DIR already covered
|
|
284
|
+
# BLOCKED -P altpath the defect, and the ONLY one
|
|
285
|
+
# BLOCKED -S string correct: split-string reparses
|
|
286
|
+
# an opaque payload, so what it
|
|
287
|
+
# runs cannot be proven here
|
|
288
|
+
# BLOCKED -Z -Q correct: unknown stays suspicious
|
|
289
|
+
#
|
|
290
|
+
# So the population is one letter, not a class. The two platforms' short
|
|
291
|
+
# options coincide everywhere else, which is why "detect the platform and
|
|
292
|
+
# choose a table" would be machinery for a single character — and would fail
|
|
293
|
+
# on a machine running GNU coreutils under a BSD userland, or the reverse.
|
|
294
|
+
# Recognising the union is both smaller and more robust.
|
|
295
|
+
#
|
|
296
|
+
# Adding a letter here is a WEAKENING, so it needs the same evidence as any
|
|
297
|
+
# other: ten bypass shapes routed through `-P` — long flag, attached value,
|
|
298
|
+
# short cluster, abbreviation, an assignment, `HUSKY=0`, a hooksPath disable,
|
|
299
|
+
# a nested `-c` shell, an executed script, and `-P` swallowing the command
|
|
300
|
+
# word — were all refused before this change and are all still refused after.
|
|
271
301
|
ENV_SHORT_NO_VALUE = frozenset("i0v")
|
|
272
|
-
ENV_SHORT_TAKES_VALUE = frozenset("
|
|
302
|
+
ENV_SHORT_TAKES_VALUE = frozenset("uCaSP")
|
|
273
303
|
|
|
274
304
|
# Builtins and wrappers that run whatever FOLLOWS them without changing what
|
|
275
305
|
# it is: `command env -S ...` runs exactly the `env` that `env -S ...` runs,
|
|
@@ -980,7 +1010,7 @@ def resolve_script(token):
|
|
|
980
1010
|
"""
|
|
981
1011
|
if COMPUTED_VALUE.search(token):
|
|
982
1012
|
return (None, "a computed path the guard cannot resolve before the shell does")
|
|
983
|
-
text = token.strip().strip("'\"")
|
|
1013
|
+
text = os.path.expanduser(token.strip().strip("'\""))
|
|
984
1014
|
# `bash -` and a bare `-` read the script from stdin; there is no file.
|
|
985
1015
|
if not text or text == "-":
|
|
986
1016
|
return (None, None)
|
|
@@ -1002,6 +1032,48 @@ def resolve_script(token):
|
|
|
1002
1032
|
return (None, "a script that does not exist or cannot be read")
|
|
1003
1033
|
|
|
1004
1034
|
|
|
1035
|
+
SHELL_COMMAND_STRING = object()
|
|
1036
|
+
SHELL_STDIN = object()
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def shell_execution_operand(args):
|
|
1040
|
+
"""The file a shell runs, or a sentinel for `-c` and stdin modes.
|
|
1041
|
+
|
|
1042
|
+
`shell_script_operand` intentionally collapses both cases to None for the
|
|
1043
|
+
nested-command scanner. Reach cannot: `bash -c '...'` has already exposed
|
|
1044
|
+
its text, while `bash -x`, `bash -e`, and `bash --` still await a script on
|
|
1045
|
+
stdin and must remain linked to a following `< file` separator.
|
|
1046
|
+
"""
|
|
1047
|
+
index = 0
|
|
1048
|
+
opaque_option_seen = False
|
|
1049
|
+
while index < len(args):
|
|
1050
|
+
token = args[index]
|
|
1051
|
+
if token == "--":
|
|
1052
|
+
index += 1
|
|
1053
|
+
break
|
|
1054
|
+
if not token.startswith(("-", "+")):
|
|
1055
|
+
if not opaque_option_seen:
|
|
1056
|
+
break
|
|
1057
|
+
index += 1
|
|
1058
|
+
opaque_option_seen = False
|
|
1059
|
+
continue
|
|
1060
|
+
if token in SHELL_OPTIONS_SEPARATE_VALUE:
|
|
1061
|
+
index += 2
|
|
1062
|
+
continue
|
|
1063
|
+
if token.startswith(SHELL_OPTIONS_INLINE_VALUE):
|
|
1064
|
+
index += 1
|
|
1065
|
+
continue
|
|
1066
|
+
if token.startswith("--"):
|
|
1067
|
+
if token not in SHELL_LONG_OPTIONS_NO_VALUE:
|
|
1068
|
+
opaque_option_seen = True
|
|
1069
|
+
index += 1
|
|
1070
|
+
continue
|
|
1071
|
+
if "c" in token[1:]:
|
|
1072
|
+
return SHELL_COMMAND_STRING
|
|
1073
|
+
index += 1
|
|
1074
|
+
return args[index] if index < len(args) else SHELL_STDIN
|
|
1075
|
+
|
|
1076
|
+
|
|
1005
1077
|
def executed_scripts(tokens):
|
|
1006
1078
|
"""The files a command line will RUN, and the executions it cannot follow.
|
|
1007
1079
|
|
|
@@ -1036,9 +1108,12 @@ def executed_scripts(tokens):
|
|
|
1036
1108
|
if name in SOURCE_BUILTINS:
|
|
1037
1109
|
operand = arguments[0] if arguments else None
|
|
1038
1110
|
elif name in SHELL_PROGRAMS:
|
|
1039
|
-
operand =
|
|
1040
|
-
if operand is
|
|
1111
|
+
operand = shell_execution_operand(arguments)
|
|
1112
|
+
if operand is SHELL_STDIN:
|
|
1041
1113
|
previous_shell_awaiting_stdin = True
|
|
1114
|
+
continue
|
|
1115
|
+
if operand is SHELL_COMMAND_STRING:
|
|
1116
|
+
continue
|
|
1042
1117
|
else:
|
|
1043
1118
|
continue
|
|
1044
1119
|
if operand is None:
|
|
@@ -3475,7 +3475,7 @@ function recordDeployRole(ref, before, role) {
|
|
|
3475
3475
|
* that wrongly did.
|
|
3476
3476
|
* @param {string} ref Canonical work-item reference.
|
|
3477
3477
|
* @param {object} contract The resolved tracker contract.
|
|
3478
|
-
* @returns {{applied: boolean, merged: number[], report: string, terminal: string|null}}
|
|
3478
|
+
* @returns {{applied: boolean, bases: string, merged: number[], report: string, terminal: string|null}}
|
|
3479
3479
|
* What was applied, and why.
|
|
3480
3480
|
*/
|
|
3481
3481
|
function completeGithubWorkItem(ref, contract) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# `git -c "core.hooksPath=/dev/null"`.
|
|
11
11
|
#
|
|
12
12
|
# Capability names below are the canonical guard's, kept identical on every agent.
|
|
13
|
-
# lisa-guard-capabilities: no-verify-abbrev, husky-env, hookspath-allowlist, config-env, env-split-string, git-config-key, git-config-parameters, git-config-parameters-append, git-config-parameters-expansion, heredoc-shell-word, herestring-aware, no-verify-short, nested-shell-no-verify, nested-shell-long-options, env-split-string-abbrev, command-wrapper-normalization, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand, dispatcher-exec-position
|
|
13
|
+
# lisa-guard-capabilities: no-verify-abbrev, husky-env, hookspath-allowlist, config-env, env-split-string, git-config-key, git-config-parameters, git-config-parameters-append, git-config-parameters-expansion, heredoc-shell-word, herestring-aware, no-verify-short, nested-shell-no-verify, nested-shell-long-options, env-split-string-abbrev, command-wrapper-normalization, executed-script-reach, source-builtin-reach, stdin-redirect-reach, wrapper-positional-operand, dispatcher-exec-position, tilde-script-reach, shell-option-stdin-reach
|
|
14
14
|
set -euo pipefail
|
|
15
15
|
|
|
16
16
|
input="$(cat 2>/dev/null || true)"
|
|
@@ -171,8 +171,38 @@ ENV_LONG_SEPARATE_VALUE = {"--argv0", "--chdir", "--split-string", "--unset"}
|
|
|
171
171
|
|
|
172
172
|
# env's single-letter options, by whether the rest of a cluster is more
|
|
173
173
|
# options or the option's value.
|
|
174
|
+
#
|
|
175
|
+
# BOTH PLATFORMS' SETS, not one. These began as GNU's, and an option that is
|
|
176
|
+
# in neither set falls through to `ambiguous`, which this guard treats as
|
|
177
|
+
# suspicious. That default is correct and stays — an unknown option genuinely
|
|
178
|
+
# could take a value and swallow the command name. What is wrong is a
|
|
179
|
+
# well-known option being unknown, and on a macOS fleet `env -P` is exactly
|
|
180
|
+
# that: BSD's "search altpath for the utility", refused on every ordinary
|
|
181
|
+
# command it prefixes.
|
|
182
|
+
#
|
|
183
|
+
# Measured, one option at a time, against the guard before this change. BSD's
|
|
184
|
+
# complete short set is `0 i v u C P S`, from the synopsis in env(1):
|
|
185
|
+
#
|
|
186
|
+
# allowed -0 -i -v -u NAME -C DIR already covered
|
|
187
|
+
# BLOCKED -P altpath the defect, and the ONLY one
|
|
188
|
+
# BLOCKED -S string correct: split-string reparses
|
|
189
|
+
# an opaque payload, so what it
|
|
190
|
+
# runs cannot be proven here
|
|
191
|
+
# BLOCKED -Z -Q correct: unknown stays suspicious
|
|
192
|
+
#
|
|
193
|
+
# So the population is one letter, not a class. The two platforms' short
|
|
194
|
+
# options coincide everywhere else, which is why "detect the platform and
|
|
195
|
+
# choose a table" would be machinery for a single character — and would fail
|
|
196
|
+
# on a machine running GNU coreutils under a BSD userland, or the reverse.
|
|
197
|
+
# Recognising the union is both smaller and more robust.
|
|
198
|
+
#
|
|
199
|
+
# Adding a letter here is a WEAKENING, so it needs the same evidence as any
|
|
200
|
+
# other: ten bypass shapes routed through `-P` — long flag, attached value,
|
|
201
|
+
# short cluster, abbreviation, an assignment, `HUSKY=0`, a hooksPath disable,
|
|
202
|
+
# a nested `-c` shell, an executed script, and `-P` swallowing the command
|
|
203
|
+
# word — were all refused before this change and are all still refused after.
|
|
174
204
|
ENV_SHORT_NO_VALUE = frozenset("i0v")
|
|
175
|
-
ENV_SHORT_TAKES_VALUE = frozenset("
|
|
205
|
+
ENV_SHORT_TAKES_VALUE = frozenset("uCaSP")
|
|
176
206
|
|
|
177
207
|
# Builtins and wrappers that run whatever FOLLOWS them without changing what
|
|
178
208
|
# it is: `command env -S ...` runs exactly the `env` that `env -S ...` runs,
|
|
@@ -883,7 +913,7 @@ def resolve_script(token):
|
|
|
883
913
|
"""
|
|
884
914
|
if COMPUTED_VALUE.search(token):
|
|
885
915
|
return (None, "a computed path the guard cannot resolve before the shell does")
|
|
886
|
-
text = token.strip().strip("'\"")
|
|
916
|
+
text = os.path.expanduser(token.strip().strip("'\""))
|
|
887
917
|
# `bash -` and a bare `-` read the script from stdin; there is no file.
|
|
888
918
|
if not text or text == "-":
|
|
889
919
|
return (None, None)
|
|
@@ -905,6 +935,48 @@ def resolve_script(token):
|
|
|
905
935
|
return (None, "a script that does not exist or cannot be read")
|
|
906
936
|
|
|
907
937
|
|
|
938
|
+
SHELL_COMMAND_STRING = object()
|
|
939
|
+
SHELL_STDIN = object()
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def shell_execution_operand(args):
|
|
943
|
+
"""The file a shell runs, or a sentinel for `-c` and stdin modes.
|
|
944
|
+
|
|
945
|
+
`shell_script_operand` intentionally collapses both cases to None for the
|
|
946
|
+
nested-command scanner. Reach cannot: `bash -c '...'` has already exposed
|
|
947
|
+
its text, while `bash -x`, `bash -e`, and `bash --` still await a script on
|
|
948
|
+
stdin and must remain linked to a following `< file` separator.
|
|
949
|
+
"""
|
|
950
|
+
index = 0
|
|
951
|
+
opaque_option_seen = False
|
|
952
|
+
while index < len(args):
|
|
953
|
+
token = args[index]
|
|
954
|
+
if token == "--":
|
|
955
|
+
index += 1
|
|
956
|
+
break
|
|
957
|
+
if not token.startswith(("-", "+")):
|
|
958
|
+
if not opaque_option_seen:
|
|
959
|
+
break
|
|
960
|
+
index += 1
|
|
961
|
+
opaque_option_seen = False
|
|
962
|
+
continue
|
|
963
|
+
if token in SHELL_OPTIONS_SEPARATE_VALUE:
|
|
964
|
+
index += 2
|
|
965
|
+
continue
|
|
966
|
+
if token.startswith(SHELL_OPTIONS_INLINE_VALUE):
|
|
967
|
+
index += 1
|
|
968
|
+
continue
|
|
969
|
+
if token.startswith("--"):
|
|
970
|
+
if token not in SHELL_LONG_OPTIONS_NO_VALUE:
|
|
971
|
+
opaque_option_seen = True
|
|
972
|
+
index += 1
|
|
973
|
+
continue
|
|
974
|
+
if "c" in token[1:]:
|
|
975
|
+
return SHELL_COMMAND_STRING
|
|
976
|
+
index += 1
|
|
977
|
+
return args[index] if index < len(args) else SHELL_STDIN
|
|
978
|
+
|
|
979
|
+
|
|
908
980
|
def executed_scripts(tokens):
|
|
909
981
|
"""The files a command line will RUN, and the executions it cannot follow.
|
|
910
982
|
|
|
@@ -939,9 +1011,12 @@ def executed_scripts(tokens):
|
|
|
939
1011
|
if name in SOURCE_BUILTINS:
|
|
940
1012
|
operand = arguments[0] if arguments else None
|
|
941
1013
|
elif name in SHELL_PROGRAMS:
|
|
942
|
-
operand =
|
|
943
|
-
if operand is
|
|
1014
|
+
operand = shell_execution_operand(arguments)
|
|
1015
|
+
if operand is SHELL_STDIN:
|
|
944
1016
|
previous_shell_awaiting_stdin = True
|
|
1017
|
+
continue
|
|
1018
|
+
if operand is SHELL_COMMAND_STRING:
|
|
1019
|
+
continue
|
|
945
1020
|
else:
|
|
946
1021
|
continue
|
|
947
1022
|
if operand is None:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA28BjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA82BjC,CAAC"}
|