approval-md 0.4.6 → 0.4.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/SPEC.md +11 -1
- package/dist/src/channels/contract.d.ts +15 -0
- package/dist/src/channels/contract.js +23 -1
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/telegram.d.ts +75 -2
- package/dist/src/channels/telegram.js +99 -6
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/cli/channel-telegram.d.ts +14 -0
- package/dist/src/cli/channel-telegram.js +44 -2
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/help.d.ts +2 -0
- package/dist/src/cli/help.js +36 -0
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook.d.ts +29 -0
- package/dist/src/cli/hook.js +178 -15
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/instructions.js +9 -6
- package/dist/src/cli/instructions.js.map +1 -1
- package/dist/src/cli/main.js +9 -0
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/study.d.ts +22 -0
- package/dist/src/cli/study.js +153 -0
- package/dist/src/cli/study.js.map +1 -0
- package/dist/src/cli/verb-registry.d.ts +14 -0
- package/dist/src/cli/verb-registry.js +37 -0
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/core/gate.d.ts +38 -0
- package/dist/src/core/gate.js +26 -0
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/sender-identity.d.ts +13 -1
- package/dist/src/core/sender-identity.js +15 -0
- package/dist/src/core/sender-identity.js.map +1 -1
- package/dist/src/core/study-withdraw.d.ts +137 -0
- package/dist/src/core/study-withdraw.js +280 -0
- package/dist/src/core/study-withdraw.js.map +1 -0
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/mcp/server.js +1 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/cli-reference.md +63 -0
- package/package.json +2 -2
- package/schema/event.schema.json +212 -0
- package/schema/fixtures/event/invalid/approval-granted-study.json +23 -0
- package/schema/fixtures/event/invalid/approval-rejected-looks-wrong-cli-channel.json +23 -0
- package/schema/fixtures/event/invalid/approval-rejected-looks-wrong-operator.json +24 -0
- package/schema/fixtures/event/invalid/approval-rejected-study-extra-key.json +24 -0
- package/schema/fixtures/event/invalid/approval-rejected-study-unknown-reason.json +23 -0
- package/schema/fixtures/event/invalid/approval-rejected-study-withdrawn-email-operator.json +20 -0
- package/schema/fixtures/event/invalid/approval-rejected-study-withdrawn-no-operator.json +19 -0
- package/schema/fixtures/event/invalid/approval-rejected-study-withdrawn-no-ref.json +19 -0
- package/schema/fixtures/event/invalid/approval-rejected-study-withdrawn-wrong-actor.json +20 -0
- package/schema/fixtures/event/invalid/approval-rejected-study-withdrawn-wrong-channel.json +20 -0
- package/schema/fixtures/event/invalid/approval-revoked-study.json +20 -0
- package/schema/fixtures/event/valid/approval-rejected-looks-wrong.json +23 -0
- package/schema/fixtures/event/valid/approval-rejected-study-withdrawn.json +20 -0
package/dist/src/cli/hook.d.ts
CHANGED
|
@@ -1281,6 +1281,35 @@ export declare const POST_TOOL_CODES: readonly [
|
|
|
1281
1281
|
export type PostToolCode = (typeof POST_TOOL_CODES)[number];
|
|
1282
1282
|
/** The environment variable that turns the sandbox requirement on (APRV-193). */
|
|
1283
1283
|
export declare const REQUIRE_SANDBOX_ENV = "APPROVAL_HOOK_REQUIRE_SANDBOX";
|
|
1284
|
+
/**
|
|
1285
|
+
* The fixed remedy a `fetch-output-unknown` refusal carries when a `$`-first
|
|
1286
|
+
* word in curl's or wget's option slot caused it (0.4.7, EDGEOS-BLOCK).
|
|
1287
|
+
*
|
|
1288
|
+
* Core 0.4.6 (APRV-511) holds `curl "${BASE:-https://h}/x"`: the expansion
|
|
1289
|
+
* could be `-o <organ path>`, so the download's output is unknown, the class is
|
|
1290
|
+
* `policy.core`, and a policy that reserves that class denies outright. The
|
|
1291
|
+
* refusal said why and nothing about the shape that runs. The same word as
|
|
1292
|
+
* `--url`'s value sits in a value slot, where curl reads it as the URL whatever
|
|
1293
|
+
* it starts with, and classifies by its method. wget has no `--url`, so the
|
|
1294
|
+
* line names curl for both.
|
|
1295
|
+
*
|
|
1296
|
+
* Text only: the class, the rule and the code are unchanged, and nothing here
|
|
1297
|
+
* widens a verdict.
|
|
1298
|
+
*/
|
|
1299
|
+
export declare const FETCH_DOLLAR_WORD_REMEDY = "Remedy: a word that starts with `$` stands where curl and wget read options, so its value could turn it into `-o <path>` and the download's output cannot be known. If that word is the URL, pass it as the value of `--url` (`curl --url \"$VAR/...\"`), which keeps the output known; wget has no `--url`, so use `curl --url` instead.";
|
|
1300
|
+
/**
|
|
1301
|
+
* {@link FETCH_DOLLAR_WORD_REMEDY}, prefixed with a space, when a segment that
|
|
1302
|
+
* earned `reserved` did so by rule `fetch-output-unknown` on a word that
|
|
1303
|
+
* starts with `$`; otherwise the empty string, so every other cause of the rule
|
|
1304
|
+
* (an unknown option, a config source, an ambiguous prefix, an unplaceable
|
|
1305
|
+
* output value) keeps its refusal byte for byte.
|
|
1306
|
+
*
|
|
1307
|
+
* `fetchOutputs` reports the first unknowable word verbatim, and only its
|
|
1308
|
+
* option-slot test reports a positional word; every other cause reports the
|
|
1309
|
+
* option as spelled, which starts with `-`. So a bound path starting with `$`
|
|
1310
|
+
* is exactly the `$`-first option-slot cause.
|
|
1311
|
+
*/
|
|
1312
|
+
export declare function fetchOutputRemedy(segments: readonly ClassifiedSegment[] | undefined, reserved: string): string;
|
|
1284
1313
|
/**
|
|
1285
1314
|
* Must this command have been written `approval sandbox -- …`? (APRV-193.)
|
|
1286
1315
|
*
|
package/dist/src/cli/hook.js
CHANGED
|
@@ -2409,6 +2409,109 @@ const NON_OPENING_BINS = new Set([
|
|
|
2409
2409
|
]);
|
|
2410
2410
|
/** Of those, the ones whose words reach a pipe's next command as text. */
|
|
2411
2411
|
const PRINTING_BINS = new Set(["echo", "printf"]);
|
|
2412
|
+
/**
|
|
2413
|
+
* A first character the shell may still expand into something else (a
|
|
2414
|
+
* parameter, a substitution, a tilde, an escape, a glob or extended glob, a
|
|
2415
|
+
* brace list, zsh's `=cmd`, `^`, `<N-M>` and `(a|b)` patterns, a process
|
|
2416
|
+
* substitution), so the value curl finally receives may open with `@`. A
|
|
2417
|
+
* leading `-` is here too: a word this walk takes for a value may be an option
|
|
2418
|
+
* curl reads instead (`-K.env`), because the walk does not know every option
|
|
2419
|
+
* that takes a value.
|
|
2420
|
+
*/
|
|
2421
|
+
const CURL_VALUE_UNKNOWN_OPENING = /^[\s\-$`~\\*?[\]{}()!+^<>=#%|&;'"]/u;
|
|
2422
|
+
/** Is this option value one curl (or wget) sends as written and never opens? */
|
|
2423
|
+
function curlValueSentAsWritten(value, opensFileWithAt) {
|
|
2424
|
+
if (value.length === 0)
|
|
2425
|
+
return false;
|
|
2426
|
+
// curl reads `-H @FILE` as a file of headers and `--url @FILE` (8.13+) as a
|
|
2427
|
+
// file of URLs. wget has no such spelling; the rule is the same anyway.
|
|
2428
|
+
if (opensFileWithAt && value.startsWith("@"))
|
|
2429
|
+
return false;
|
|
2430
|
+
if (CURL_VALUE_UNKNOWN_OPENING.test(value))
|
|
2431
|
+
return false;
|
|
2432
|
+
// A `file:` URL opens a local file. Which file it reads is the network
|
|
2433
|
+
// classifier's to say, never this pass's; such a value keeps the judgement
|
|
2434
|
+
// it had, and a header that opens `file:` is judged with it, since the walk
|
|
2435
|
+
// may have taken a positional URL for a header value.
|
|
2436
|
+
return !/^file:/iu.test(value);
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* The curl and wget option values that are not file reads (EDGEOS-BLOCK,
|
|
2440
|
+
* APRV-533), taken out of the argv the credential pass judges.
|
|
2441
|
+
*
|
|
2442
|
+
* Every resident box runs its terminal in `$HERMES_HOME`, which holds
|
|
2443
|
+
* credential material, and the pass reads a word with an unexpanded `$` as a
|
|
2444
|
+
* read of the directory it may land in. So a header carrying `$EDGEOS_API_KEY`
|
|
2445
|
+
* and a `--url "${EDGEOS_API_BASE:-…}/…"` were each a credential read there.
|
|
2446
|
+
* curl opens neither: it reads a header from a file only for `-H @FILE`, and a
|
|
2447
|
+
* URL from a file only for `--url @FILE`. What is taken out:
|
|
2448
|
+
*
|
|
2449
|
+
* - curl's `-H`/`--header` value (separate, or attached as `-HVALUE`) and
|
|
2450
|
+
* wget's `--header` value (separate, or `--header=VALUE`), when it opens
|
|
2451
|
+
* with neither `@` nor a character the shell may still expand
|
|
2452
|
+
* ({@link CURL_VALUE_UNKNOWN_OPENING}). `-H "$X"` and `-H @"$F"` are judged
|
|
2453
|
+
* as before: the expansion may open with `@`.
|
|
2454
|
+
* - curl's `--url` value under the same rule, and additionally only when it
|
|
2455
|
+
* holds no unexpanded span, or names a literal scheme other than `file:`
|
|
2456
|
+
* before its first one (`https://h/${P}`).
|
|
2457
|
+
* - curl's `--url` value opening with an unexpanded span moves to
|
|
2458
|
+
* `spannedUrls`, which the caller judges as the path an `@` or `file:`
|
|
2459
|
+
* expansion would read; a value whose last component is the span (`"$U"`)
|
|
2460
|
+
* stays a read of the working directory, as before.
|
|
2461
|
+
*
|
|
2462
|
+
* Everything else stays: every other option (`-d`, `--data*`, `-F`, `-T`,
|
|
2463
|
+
* `-K`, `-o`, `-b`, `-c`, `--cacert`, `--cert`, `--key`, `-E`,
|
|
2464
|
+
* `--netrc-file`, `--proxy-header`, `--variable`, …), a short-option cluster
|
|
2465
|
+
* ending in `H` (`-sH`), curl's `--header=…` (curl takes no `=` spelling), and
|
|
2466
|
+
* every word after `--`. The pure classifier's text rule
|
|
2467
|
+
* ({@link credentialReadWords}) still reads every one of these words.
|
|
2468
|
+
*/
|
|
2469
|
+
function curlValuesNotRead(bin, args) {
|
|
2470
|
+
if (bin !== "curl" && bin !== "wget")
|
|
2471
|
+
return { args, spannedUrls: [] };
|
|
2472
|
+
const curl = bin === "curl";
|
|
2473
|
+
const kept = [];
|
|
2474
|
+
const spannedUrls = [];
|
|
2475
|
+
for (let at = 0; at < args.length; at += 1) {
|
|
2476
|
+
const arg = args[at];
|
|
2477
|
+
if (arg === "--") {
|
|
2478
|
+
kept.push(...args.slice(at));
|
|
2479
|
+
break;
|
|
2480
|
+
}
|
|
2481
|
+
const next = args[at + 1];
|
|
2482
|
+
if ((curl ? arg === "-H" || arg === "--header" : arg === "--header") && next !== undefined) {
|
|
2483
|
+
kept.push(arg);
|
|
2484
|
+
if (curlValueSentAsWritten(next, true))
|
|
2485
|
+
at += 1;
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
if (curl && arg.startsWith("-H") && arg.length > 2) {
|
|
2489
|
+
if (!curlValueSentAsWritten(arg.slice(2), true))
|
|
2490
|
+
kept.push(arg);
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
if (!curl && arg.startsWith("--header=")) {
|
|
2494
|
+
if (!curlValueSentAsWritten(arg.slice("--header=".length), true))
|
|
2495
|
+
kept.push(arg);
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
if (curl && arg === "--url" && next !== undefined) {
|
|
2499
|
+
kept.push(arg);
|
|
2500
|
+
const span = next.search(/[$`]/u);
|
|
2501
|
+
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):/u.exec(next);
|
|
2502
|
+
if (span === 0) {
|
|
2503
|
+
spannedUrls.push(next);
|
|
2504
|
+
at += 1;
|
|
2505
|
+
}
|
|
2506
|
+
else if (curlValueSentAsWritten(next, true) && (span === -1 || (scheme !== null && scheme[0].length <= span))) {
|
|
2507
|
+
at += 1;
|
|
2508
|
+
}
|
|
2509
|
+
continue;
|
|
2510
|
+
}
|
|
2511
|
+
kept.push(arg);
|
|
2512
|
+
}
|
|
2513
|
+
return { args: kept, spannedUrls };
|
|
2514
|
+
}
|
|
2412
2515
|
/**
|
|
2413
2516
|
* Every existing path a glob matches, any component of it a pattern
|
|
2414
2517
|
* (APRV-522), or `null` when a directory on the way cannot be listed or the
|
|
@@ -2673,7 +2776,9 @@ export function credentialReadLandings(command, cwd, env = process.env) {
|
|
|
2673
2776
|
return steered.some((tail) => words.some((segments) => steeredReadCouldNameCredential(tail, segments, hermesNames)));
|
|
2674
2777
|
};
|
|
2675
2778
|
// `steeredToo` is false only for git's verb word, which git never opens.
|
|
2676
|
-
|
|
2779
|
+
// `homeSpelled` is false only for a curl `--url` value that opens with an
|
|
2780
|
+
// unexpanded span ({@link curlValuesNotRead}); see its use below.
|
|
2781
|
+
const judgeOne = (word, recursive, steeredToo = true, homeSpelled = true) => {
|
|
2677
2782
|
// `$PWD` is the directory the segment runs in, which is each of `cwds`.
|
|
2678
2783
|
const pwdRef = /^\$(?:\{PWD\}|PWD)(?=\/|$)/u;
|
|
2679
2784
|
if (pwdRef.test(word)) {
|
|
@@ -2682,7 +2787,7 @@ export function credentialReadLandings(command, cwd, env = process.env) {
|
|
|
2682
2787
|
return;
|
|
2683
2788
|
}
|
|
2684
2789
|
for (const from of cwds)
|
|
2685
|
-
judgeOne(word.replace(pwdRef, from), recursive, steeredToo);
|
|
2790
|
+
judgeOne(word.replace(pwdRef, from), recursive, steeredToo, homeSpelled);
|
|
2686
2791
|
if (steeredToo && steered.length > 0 && steeredRead(word.replace(pwdRef, "."), recursive))
|
|
2687
2792
|
hit(word);
|
|
2688
2793
|
return;
|
|
@@ -2749,7 +2854,7 @@ export function credentialReadLandings(command, cwd, env = process.env) {
|
|
|
2749
2854
|
const lexical = resolvePathSegments(from, prefix);
|
|
2750
2855
|
for (const landed of new Set([lexical, canonicalPath(lexical), physicalLanding(from, prefix)])) {
|
|
2751
2856
|
if ((lastComponent && lands(landed, true)) ||
|
|
2752
|
-
landedSpellings(landed, rest, configuredHome).some(unresolvedReadCouldNameCredential)) {
|
|
2857
|
+
landedSpellings(landed, rest, homeSpelled ? configuredHome : null).some(unresolvedReadCouldNameCredential)) {
|
|
2753
2858
|
hit(word);
|
|
2754
2859
|
return;
|
|
2755
2860
|
}
|
|
@@ -2799,7 +2904,7 @@ export function credentialReadLandings(command, cwd, env = process.env) {
|
|
|
2799
2904
|
hit(physical);
|
|
2800
2905
|
}
|
|
2801
2906
|
};
|
|
2802
|
-
const judge = (word, recursive, steeredToo = true) => {
|
|
2907
|
+
const judge = (word, recursive, steeredToo = true, homeSpelled = true) => {
|
|
2803
2908
|
// APRV-524 (review-b M1): a word holding the substitution sentinel is
|
|
2804
2909
|
// judged a second time with every sentinel removed. That is how the
|
|
2805
2910
|
// release before APRV-511 read it, its lexer adding nothing for a
|
|
@@ -2811,7 +2916,7 @@ export function credentialReadLandings(command, cwd, env = process.env) {
|
|
|
2811
2916
|
// name, and one after a glob that matches such a directory on disk are
|
|
2812
2917
|
// judged too. Both judgements only record, so the stricter stands.
|
|
2813
2918
|
if (word.includes(SUBSTITUTION_SENTINEL)) {
|
|
2814
|
-
judge(word.split(SUBSTITUTION_SENTINEL).join(""), recursive, steeredToo);
|
|
2919
|
+
judge(word.split(SUBSTITUTION_SENTINEL).join(""), recursive, steeredToo, homeSpelled);
|
|
2815
2920
|
}
|
|
2816
2921
|
const spellings = new Set();
|
|
2817
2922
|
for (const parameter of parameterSpellings(word)) {
|
|
@@ -2826,7 +2931,7 @@ export function credentialReadLandings(command, cwd, env = process.env) {
|
|
|
2826
2931
|
spellings.add(spelling);
|
|
2827
2932
|
}
|
|
2828
2933
|
for (const spelling of spellings)
|
|
2829
|
-
judgeOne(spelling, recursive, steeredToo);
|
|
2934
|
+
judgeOne(spelling, recursive, steeredToo, homeSpelled);
|
|
2830
2935
|
};
|
|
2831
2936
|
for (const target of shape.reads)
|
|
2832
2937
|
judge(target, false);
|
|
@@ -2838,7 +2943,10 @@ export function credentialReadLandings(command, cwd, env = process.env) {
|
|
|
2838
2943
|
if (NON_OPENING_BINS.has(bin) && !(PRINTING_BINS.has(bin) && (feedsPipe || substituted)))
|
|
2839
2944
|
continue;
|
|
2840
2945
|
const recursive = RECURSIVE_READERS.has(bin) || DIRECTORY_COPIERS.has(bin) || DIRECTORY_READING_BINS.has(bin);
|
|
2841
|
-
|
|
2946
|
+
// EDGEOS-BLOCK (APRV-533): the curl and wget option values the tool sends
|
|
2947
|
+
// and never opens are not read words ({@link curlValuesNotRead}).
|
|
2948
|
+
const sent = curlValuesNotRead(bin, shape.args);
|
|
2949
|
+
const words = credentialReadWords(bin, sent.args);
|
|
2842
2950
|
// A copy's destination is WRITTEN, and the write half (the protected
|
|
2843
2951
|
// scan, the Hermes organ pass) prices it; judged as a read it would make
|
|
2844
2952
|
// `cp /tmp/x .` in a home a credential read of the home.
|
|
@@ -2863,6 +2971,15 @@ export function credentialReadLandings(command, cwd, env = process.env) {
|
|
|
2863
2971
|
const verb = verbAt === null ? undefined : shape.args[verbAt];
|
|
2864
2972
|
for (const [at, word] of words.entries())
|
|
2865
2973
|
judge(word, recursive, !(at === 0 && verb !== undefined && word === verb));
|
|
2974
|
+
// A `--url` value opening with an unexpanded span may expand to `@FILE`
|
|
2975
|
+
// (curl 8.13+ reads a URL list from FILE) or to a `file:` URL, and either
|
|
2976
|
+
// reads the path the rest of the word spells. It is judged as that path
|
|
2977
|
+
// from every working directory, the configured home not re-spelled as
|
|
2978
|
+
// `$HERMES_HOME`: that spelling makes the known home one more unknown
|
|
2979
|
+
// segment, and two unknowns in a row then read as an approval home's
|
|
2980
|
+
// `vault*`, which the literal landing already rules in or out.
|
|
2981
|
+
for (const word of sent.spannedUrls)
|
|
2982
|
+
judge(word, false, true, false);
|
|
2866
2983
|
// M-E: a recursive search reads the working directory.
|
|
2867
2984
|
if (readsWorkingDirectory(bin, shape.args, words))
|
|
2868
2985
|
judge(".", true);
|
|
@@ -6399,17 +6516,20 @@ function resolvedPathClasses(raw, segments, cwd, protectedPaths, readRoots = [],
|
|
|
6399
6516
|
add("policy.core");
|
|
6400
6517
|
}
|
|
6401
6518
|
};
|
|
6402
|
-
|
|
6519
|
+
// `homeSpelled` is false only for a curl `--url` value that opens with an
|
|
6520
|
+
// unexpanded span (EDGEOS-BLOCK, APRV-533; {@link curlValuesNotRead}).
|
|
6521
|
+
const judgeRead = (word, recursive, homeSpelled = true) => {
|
|
6403
6522
|
const braced = braceCandidates(word);
|
|
6404
6523
|
if (braced === null) {
|
|
6405
6524
|
add("account.credential");
|
|
6406
6525
|
return;
|
|
6407
6526
|
}
|
|
6408
|
-
for (const each of braced)
|
|
6527
|
+
for (const each of braced) {
|
|
6409
6528
|
for (const spelling of tildeReadings(each))
|
|
6410
|
-
judgeReadWord(spelling, recursive);
|
|
6529
|
+
judgeReadWord(spelling, recursive, homeSpelled);
|
|
6530
|
+
}
|
|
6411
6531
|
};
|
|
6412
|
-
const judgeReadWord = (word, recursive) => {
|
|
6532
|
+
const judgeReadWord = (word, recursive, homeSpelled = true) => {
|
|
6413
6533
|
const target = expand(word);
|
|
6414
6534
|
// Fix round 1 (M-C): an unexpanded tilde prefix lands somewhere unknown.
|
|
6415
6535
|
if (target.hermesUnknown || target.landingUnknown) {
|
|
@@ -6442,7 +6562,7 @@ function resolvedPathClasses(raw, segments, cwd, protectedPaths, readRoots = [],
|
|
|
6442
6562
|
if (landing.unknown)
|
|
6443
6563
|
add("account.credential");
|
|
6444
6564
|
for (const landed of [resolvePathSegments(spelled), ...landing.paths]) {
|
|
6445
|
-
for (const spelling of landedSpellings(landed, rest, configuredHome)) {
|
|
6565
|
+
for (const spelling of landedSpellings(landed, rest, homeSpelled ? configuredHome : null)) {
|
|
6446
6566
|
if (unresolvedReadCouldNameCredential(spelling))
|
|
6447
6567
|
add("account.credential");
|
|
6448
6568
|
}
|
|
@@ -6708,8 +6828,16 @@ function resolvedPathClasses(raw, segments, cwd, protectedPaths, readRoots = [],
|
|
|
6708
6828
|
const readWords = credentialReadWords(bin, shape.args);
|
|
6709
6829
|
const recursive = RECURSIVE_READERS.has(bin) || (DIRECTORY_READING_BINS.has(bin) && readWords.length > 0);
|
|
6710
6830
|
if (segment.class.startsWith("read.") || recursive) {
|
|
6711
|
-
|
|
6831
|
+
// EDGEOS-BLOCK (APRV-533): the curl and wget option values the tool
|
|
6832
|
+
// sends and never opens are not read here either, and a span-led curl
|
|
6833
|
+
// `--url` value is judged as the path an `@` or `file:` expansion would
|
|
6834
|
+
// read, as the credential pass judges it ({@link curlValuesNotRead}).
|
|
6835
|
+
const sent = curlValuesNotRead(bin, shape.args);
|
|
6836
|
+
const readOperands = sent.args === shape.args ? operands : sent.args.filter((arg) => !arg.startsWith("-"));
|
|
6837
|
+
for (const operand of readOperands)
|
|
6712
6838
|
judgeRead(operand, recursive);
|
|
6839
|
+
for (const url of sent.spannedUrls)
|
|
6840
|
+
judgeRead(url, recursive, false);
|
|
6713
6841
|
// APRV-511 (cred-asymmetry round 3, N2): a recursive reader that no
|
|
6714
6842
|
// argument proves names a path walks the working directory.
|
|
6715
6843
|
if (walksWorkingDirectory(bin, shape.args))
|
|
@@ -7509,6 +7637,41 @@ function landingPaths(spelled, volatile = null) {
|
|
|
7509
7637
|
}
|
|
7510
7638
|
/** The environment variable that turns the sandbox requirement on (APRV-193). */
|
|
7511
7639
|
export const REQUIRE_SANDBOX_ENV = "APPROVAL_HOOK_REQUIRE_SANDBOX";
|
|
7640
|
+
/**
|
|
7641
|
+
* The fixed remedy a `fetch-output-unknown` refusal carries when a `$`-first
|
|
7642
|
+
* word in curl's or wget's option slot caused it (0.4.7, EDGEOS-BLOCK).
|
|
7643
|
+
*
|
|
7644
|
+
* Core 0.4.6 (APRV-511) holds `curl "${BASE:-https://h}/x"`: the expansion
|
|
7645
|
+
* could be `-o <organ path>`, so the download's output is unknown, the class is
|
|
7646
|
+
* `policy.core`, and a policy that reserves that class denies outright. The
|
|
7647
|
+
* refusal said why and nothing about the shape that runs. The same word as
|
|
7648
|
+
* `--url`'s value sits in a value slot, where curl reads it as the URL whatever
|
|
7649
|
+
* it starts with, and classifies by its method. wget has no `--url`, so the
|
|
7650
|
+
* line names curl for both.
|
|
7651
|
+
*
|
|
7652
|
+
* Text only: the class, the rule and the code are unchanged, and nothing here
|
|
7653
|
+
* widens a verdict.
|
|
7654
|
+
*/
|
|
7655
|
+
export const FETCH_DOLLAR_WORD_REMEDY = 'Remedy: a word that starts with `$` stands where curl and wget read options, so its value could turn it into `-o <path>` and the download\'s output cannot be known. If that word is the URL, pass it as the value of `--url` (`curl --url "$VAR/..."`), which keeps the output known; wget has no `--url`, so use `curl --url` instead.';
|
|
7656
|
+
/**
|
|
7657
|
+
* {@link FETCH_DOLLAR_WORD_REMEDY}, prefixed with a space, when a segment that
|
|
7658
|
+
* earned `reserved` did so by rule `fetch-output-unknown` on a word that
|
|
7659
|
+
* starts with `$`; otherwise the empty string, so every other cause of the rule
|
|
7660
|
+
* (an unknown option, a config source, an ambiguous prefix, an unplaceable
|
|
7661
|
+
* output value) keeps its refusal byte for byte.
|
|
7662
|
+
*
|
|
7663
|
+
* `fetchOutputs` reports the first unknowable word verbatim, and only its
|
|
7664
|
+
* option-slot test reports a positional word; every other cause reports the
|
|
7665
|
+
* option as spelled, which starts with `-`. So a bound path starting with `$`
|
|
7666
|
+
* is exactly the `$`-first option-slot cause.
|
|
7667
|
+
*/
|
|
7668
|
+
export function fetchOutputRemedy(segments, reserved) {
|
|
7669
|
+
const fired = (segments ?? []).some((segment) => segment.class === reserved &&
|
|
7670
|
+
segment.rule === "fetch-output-unknown" &&
|
|
7671
|
+
segment.path !== undefined &&
|
|
7672
|
+
segment.path.startsWith("$"));
|
|
7673
|
+
return fired ? ` ${FETCH_DOLLAR_WORD_REMEDY}` : "";
|
|
7674
|
+
}
|
|
7512
7675
|
/**
|
|
7513
7676
|
* Must this command have been written `approval sandbox -- …`? (APRV-193.)
|
|
7514
7677
|
*
|
|
@@ -7692,7 +7855,7 @@ decidedOn) {
|
|
|
7692
7855
|
}
|
|
7693
7856
|
const reserved = classes.find((cls) => resolvePolicy(load, cls).autonomy === "human-only");
|
|
7694
7857
|
if (reserved !== undefined) {
|
|
7695
|
-
return deny(streams, "hook-class-human-only", `${humanOnlyRefusal(reserved, "this command may not run under an agent")} An open window does not reach it: the window suspends what the policy DECIDES, and a human-only class is one the policy reserves to human hands, which a window opened by a human does not lend to an agent (SPEC.md §11.1 invariant 9)
|
|
7858
|
+
return deny(streams, "hook-class-human-only", `${humanOnlyRefusal(reserved, "this command may not run under an agent")} An open window does not reach it: the window suspends what the policy DECIDES, and a human-only class is one the policy reserves to human hands, which a window opened by a human does not lend to an agent (SPEC.md §11.1 invariant 9).${fetchOutputRemedy(described.segments, reserved)}`, adapter.kind);
|
|
7696
7859
|
}
|
|
7697
7860
|
}
|
|
7698
7861
|
// APRV-227, resolved here because here is where a record is written. The
|
|
@@ -8276,7 +8439,7 @@ function* decideHarnessSteps(decide) {
|
|
|
8276
8439
|
return {
|
|
8277
8440
|
permission: "deny",
|
|
8278
8441
|
code: "hook-class-human-only",
|
|
8279
|
-
detail: `${humanOnlyRefusal(reserved, "this command may not run under an agent")} The gate's own code for this fact is \`class-human-only
|
|
8442
|
+
detail: `${humanOnlyRefusal(reserved, "this command may not run under an agent")} The gate's own code for this fact is \`class-human-only\`.${fetchOutputRemedy(described.segments, reserved)}`,
|
|
8280
8443
|
};
|
|
8281
8444
|
}
|
|
8282
8445
|
// APRV-193, and BELOW the human-only deny for the same reason that one sits
|