row 210 (#1707): round 4 for sage-1617 B1, argv check into vocabulary.mjs so the sandboxed broker loads it, import pin, host.sudo in the pm and chief-of-staff hard wait lists (Gizmoduck)
This commit is contained in:
1 parent
3cf63efb4b
commit
c43008749b
9 files changed
+112
-59
No files matched your search
@@ -1,4 +1,4 @@
|
||||
# Row 210 round 2 (#424, Gitea #1707): agent host access with sudo through the credential store
|
||||
# Row 210 round 4 (#424, Gitea #1707): agent host access with sudo through the credential store
|
||||
|
||||
Gizmoduck, 2026-10-20. Reviewer Duckworth, gate owner Sage.
|
||||
|
||||
@@ -20,6 +20,9 @@ Gizmoduck, 2026-10-20. Reviewer Duckworth, gate owner Sage.
|
||||
`git diff 07141966e cb5fdb064` is empty on every path this row touches, so the approved round-2 code is
|
||||
unchanged and `candidate-manifest.sha256` still verifies. The old branch is kept as it was, not
|
||||
force-pushed or deleted (mos-1086). Singles on cb5fdb064 are under "Checks".
|
||||
- Round 4 answers sage-1617 B1 on the same branch and base. See "Round 4" below. `candidate-manifest.sha256`
|
||||
is regenerated from the round-4 tree: 23 files, the 20 before plus `project-runtime.test.mjs` and the two
|
||||
role contracts.
|
||||
- Candidate: the commit that adds this file. `candidate-manifest.sha256` lists the code and test files.
|
||||
- Scope (sage-1468): the `host.run` broker op, the grant check, the executor and the receipts, over
|
||||
`broker.sock`. No `packages/harness` change. The `host_run` tool is Sage's follow-up row.
|
||||
@@ -30,11 +33,11 @@ Gizmoduck, 2026-10-20. Reviewer Duckworth, gate owner Sage.
|
||||
|--------|------|-------|
|
||||
| D1 kind `host.<id>`, `{"service": "host"}` with no scopes, `rotateBy` | vocabulary.mjs:26-28, 111-115, role.mjs:110-118, business credentials.mjs:26, credential-verbs.mjs:8, cli credentials.mjs:11-12 | T2, T4, T23 |
|
||||
| D2 value check, one unencrypted ed25519 key | bus credentials.mjs:26 (`parseHostKey`), used at :227 | T4 |
|
||||
| D3 registry, own-machine refusal, grant, key reference | hosts.mjs:37-129, business.mjs:124-131, 154, 210-211, 271 | T1, T2, T26 |
|
||||
| D3 registry, own-machine refusal, grant, key reference | hosts.mjs:35-127, business.mjs:124-131, 154, 210-211, 271 | T1, T2, T26 |
|
||||
| D3 `organization.set`, `limits.set` and `instances.set` refuse `hosts` | broker `keys()` (199) | T3 |
|
||||
| D4 `host.sudo` action, decision bound to host, argv and stdin, spent once | vocabulary.mjs:28, broker.mjs:101-102, 797, 830, 1956-2006 | T9, T25 |
|
||||
| D5 `host.run` verb, check order, events | broker.mjs:96, 1715-1766, server.mjs:12-13, 113-116, resolve.mjs:90, bus business.mjs:21 | T5, T6, T8, T20, T22 |
|
||||
| D6 argv shape, literal prefix, refused runners, quoting, `sudo -n --` | hosts.mjs:16-35, 93-113, 131-152, host-run.mjs:25-26 | T7, T8, T10, T11, T26 |
|
||||
| D4 `host.sudo` action, decision bound to host, argv and stdin, spent once | vocabulary.mjs:28, broker.mjs:100-101, 796, 829, 1955-2005 | T9, T25 |
|
||||
| D5 `host.run` verb, check order, events | broker.mjs:95, 1714-1765, server.mjs:12-13, 113-116, resolve.mjs:90, bus business.mjs:21 | T5, T6, T8, T20, T22 |
|
||||
| D6 argv shape, literal prefix, refused runners, quoting, `sudo -n --` | hosts.mjs:20-33, 92-112, vocabulary.mjs:121-144, host-run.mjs:25-26 | T7, T8, T10, T11, T26 |
|
||||
| D6 exit, signal, timeout, output cap | host-run.mjs:95-145 | T12, T13 |
|
||||
| D7 run dir, ssh-agent, `ssh-add -t`, `--pdeathsig`, pinned known_hosts, pids with start times | host-run.mjs:17-84, 147-187 | T14, T15, T18, T19, T27 |
|
||||
| D7 start sweep | host-run.mjs:189-226, runtime.mjs:185 | T27 (two cases) |
|
||||
@@ -53,7 +56,7 @@ The rest are in `packages/bus/tests/host-run.test.mjs`, against a scratch sshd o
|
||||
edited in place to `/nonexistent/sshd` (backup, restored, sha checked), the file exits 1: tests 21, pass 8,
|
||||
fail 13, skipped 0. The binary test and all 12 executor tests fail with "/nonexistent/sshd is missing".
|
||||
- N1. A `host.sudo` raise now checks the raiser's own grant for the target and the argv prefix before it asks the
|
||||
human (broker.mjs:1964-1967). It uses the same check `host.run` uses (:1732-1735). T5 to T8 now raise for an
|
||||
human (broker.mjs:1963-1966). It uses the same check `host.run` uses (:1731-1734). T5 to T8 now raise for an
|
||||
ungranted host (`host-not-granted`) and an argv outside the grant (`command-not-granted`), and count zero
|
||||
`host.sudo` decisions after. T9 grants web2 through a new `coderWeb2` fixture option. `hard-waits.test.mjs`
|
||||
raised `host.sudo` from a coder with no grant, so its fixture now grants web1 `systemctl status`.
|
||||
@@ -71,17 +74,42 @@ The rest are in `packages/bus/tests/host-run.test.mjs`, against a scratch sshd o
|
||||
- N4. The deviations are accepted. No change.
|
||||
- N5. The business test helper leaving `mosaic-business-*` tmp dirs predates this row. It is #1624, below.
|
||||
|
||||
## Round 4 (sage-1617)
|
||||
|
||||
- B1. The gate failed pkg-bus 497/1 on 92fac9904. broker.mjs:7 imported `validArgv` and `commandAllowed` from
|
||||
`business/src/hosts.mjs`, which isn't in RUNNER_FILES and imports `node:net`, `errors.mjs` and `util.mjs`. The
|
||||
confined worker can't load it. The fix follows Sage's rule: broker.mjs imports only import-free files in
|
||||
RUNNER_FILES.
|
||||
- `ARGV_MAX`, `ARG_BYTES`, `ARGV_BYTES`, `validArgv` and `commandAllowed` move unchanged into `vocabulary.mjs`
|
||||
(:121-144). They need nothing but `Buffer`, a global. `vocabulary.mjs` is already in RUNNER_FILES and still
|
||||
has no import, so RUNNER_FILES and the seat sandbox pin don't change and no new file is mounted.
|
||||
- `hosts.mjs` imports `ARGV_MAX` back for `checkCommands` and re-exports all five (:7-11), so `hosts.test.mjs:9`, the
|
||||
only other reader, imports them as before.
|
||||
- broker.mjs:6 now reads `credentialKind`, `validArgv` and `commandAllowed` from the vocabulary, and the
|
||||
hosts.mjs line is gone. Every broker cite in this file after :7 is one lower than in round 2.
|
||||
- `project-runtime.test.mjs:12` pins `createHash` and the new vocabulary line, each with a Row 210 comment line
|
||||
in the existing style.
|
||||
- `mutants.py` points M04a to M04e and M05 at `vocabulary.mjs`. The anchors and the edits are unchanged.
|
||||
- Found by the whole business suite, not by the gate: `workflow-roles.test.mjs:105` failed 197/1. Round 1 put
|
||||
`host.sudo` in ACTIONS and not in ROUTED (vocabulary.mjs:28), so it is in GATED_ONLY, a hard wait. The hard
|
||||
wait lists in `roles/pm.md:54-57` and `roles/chief-of-staff.md:74-77` didn't name it, and the test pins those
|
||||
lists to GATED_ONLY. It failed the same way on 3cf63efb4, where `roles/` is unchanged from cb5fdb064. Both
|
||||
lists now name `host.sudo`. That only writes down the routing the broker already applies. No role gains
|
||||
or loses a grant. The other tests that read these files pass whole (Checks).
|
||||
- No other change. The host-run, hard-waits and credential code and tests are byte-identical to round 2.
|
||||
|
||||
## Deviations from the design
|
||||
|
||||
1. `resolve.mjs` filters `host.*` out of the session's credentials (D5). The bus needs them, so
|
||||
`bus/src/business.mjs` carries them back onto the bus role entry. D10 lists only the bus side.
|
||||
2. `business/src/hosts.mjs` is a new file D10 doesn't list. It holds the D3 and D6 checks so `business.mjs`
|
||||
and the broker share one argv check. `business/src/index.mjs` is unchanged.
|
||||
and the broker share one argv check. `business/src/index.mjs` is unchanged. Since round 4 the argv check
|
||||
itself lives in `vocabulary.mjs`, because the sandboxed broker can't load hosts.mjs.
|
||||
3. N1 runner list. Beyond Duckworth's list, `python*`, `*awk`, `sed`, `vi`, `vim`, `less` and `php*` refuse
|
||||
as a prefix (hosts.mjs:16-24).
|
||||
as a prefix (hosts.mjs:20-28).
|
||||
4. `runGit` isn't shared. Its shape doesn't fit an agent socket and a stdin pipe, so host-run.mjs has its own
|
||||
spawn with the same group-kill shape.
|
||||
5. The `host-busy` check runs before the decision is spent (broker.mjs:1741), so a busy refusal leaves the
|
||||
5. The `host-busy` check runs before the decision is spent (broker.mjs:1740), so a busy refusal leaves the
|
||||
decision usable. T20 checks that the held decision runs after the first call ends. The executor checks
|
||||
again under its own map (credentials.mjs:355).
|
||||
6. T15's HOME part. ssh reads the passwd home, not `$HOME`, so a planted `~/.ssh/config` in a scratch HOME
|
||||
@@ -95,7 +123,7 @@ The rest are in `packages/bus/tests/host-run.test.mjs`, against a scratch sshd o
|
||||
10. T12. ssh exits 255 when it gets SIGTERM, so a timed-out call answers `signal: 'SIGTERM'` from the
|
||||
timeout flag and exit 255. That matches D6's "answers `signal`".
|
||||
11. T3 runs at the broker. The writer not being called stands in for "the file is unchanged". `organization.set`
|
||||
reaches the writer as `limits.set` (broker.mjs:1379). The nested paths can't carry a host grant:
|
||||
reaches the writer as `limits.set` (broker.mjs:1378). The nested paths can't carry a host grant:
|
||||
launch-settings.mjs:172 takes only a string model key and instance-settings.mjs:101 writes only
|
||||
`{definition, vars}`.
|
||||
12. T24's use-site list is `host-run.mjs`, `runtime.mjs` and `tasks/adapter.mjs`. It walks the
|
||||
@@ -113,13 +141,14 @@ The rest are in `packages/bus/tests/host-run.test.mjs`, against a scratch sshd o
|
||||
- M25 is a to d with crafted keys (cipher and kdf name, public type, private type, key count). The CRLF
|
||||
mutant is left out because it is equivalent. The `\r` check (credentials.mjs:27) is redundant with the PEM
|
||||
frame and line regex (:29, :31).
|
||||
- M04a to M04e and M05 edit `vocabulary.mjs` since round 4.
|
||||
- M28 is a and b. M34 is a to d (no-op sweep, bus pid ignored, start time ignored, bus start never sweeps).
|
||||
- M12 dies on T15's argv check as well as T14, because `accept-new` still refuses a changed known key.
|
||||
- Round 2 adds M36a and M36b (N1 raise grant and prefix checks), M37a and M37b (N2 limit and abort) and M38a and
|
||||
M38b (N3 mismatch read from the remote stderr, `-E` dropped). M02's anchor now starts at the line break, since
|
||||
the raise's check at 10 spaces holds the host.run line at 6 as a substring.
|
||||
|
||||
All 53 anchors match once (dry count on the round-2 tree, no `__pycache__`). In round 1 I ran M34d by hand once
|
||||
All 53 anchors match once (dry count on the round-2 tree and again on the round-4 tree, no `__pycache__`). In round 1 I ran M34d by hand once
|
||||
as a single test file: it fails in under a second and the file restored to its sha. The new round-2 mutants are
|
||||
not run by hand. **The batch is not run**, as it needs Sage's slot.
|
||||
Run it as `python3 -I agents/gizmoduck/work/queue-210/mutants.py <checkout> <empty scratch dir>`. It runs
|
||||
@@ -128,9 +157,21 @@ the files restored at the end, and exits 1 on any survivor or missed anchor.
|
||||
|
||||
## Checks
|
||||
|
||||
Single files on cb5fdb064 after the rebase (sage-1599), each file run whole, no slot. 17 files, 138 pass, 0 fail,
|
||||
0 skipped (awk sum over the singles log). The one more than on 07141966e is in bus credential-store (12 now, 11
|
||||
before), a test that came with 199's landing. Every other count is unchanged. Per file on cb5fdb064:
|
||||
Round 4 on cb5fdb064, the final tree, after the role contract edit. Counts are awk sums over the logs.
|
||||
|
||||
- Suites, each run whole: business 198 pass, 0 fail, seat 164 pass, 0 fail, bus 498 pass, 0 fail, all 0
|
||||
skipped. Before the role edit, business was 197/1 (workflow-roles, above).
|
||||
- Singles, each file run whole, no slot. 22 files, 172 pass, 0 fail, 0 skipped. These are round 3's 17 files
|
||||
with the same counts, plus:
|
||||
|
||||
| File | Pass |
|
||||
|------|------|
|
||||
| bus project-runtime (the import pin) | 3 |
|
||||
| seat sandbox (the RUNNER_FILES walk and the three confined sessions, none skipped) | 10 |
|
||||
| business workflow-roles, chief-of-staff | 7, 10 |
|
||||
| bus workflow-roles | 4 |
|
||||
|
||||
Round 3's 17 files on cb5fdb064, unchanged in round 4:
|
||||
|
||||
| File | Pass |
|
||||
|------|------|
|
||||
@@ -141,11 +182,11 @@ before), a test that came with 199's landing. Every other count is unchanged. Pe
|
||||
| bus publish, transport, instances-verb | 12, 5, 2 |
|
||||
| cli credentials (199's new rotate test included) | 5 |
|
||||
|
||||
- After each host-run run, no sshd, ssh or ssh-agent of mine is left and no `h-` dir is under the run roots.
|
||||
- Hash pins: the only 64-hex pin in a test is thread-migration.test.mjs:46 (a DDL hash), which none of these
|
||||
files touch.
|
||||
- After the runs, no sshd of mine is left and no `h-` dir is under the run roots.
|
||||
- Hash pins: no test pins a hash of broker.mjs, hosts.mjs, vocabulary.mjs or a role contract. The 64-hex pins
|
||||
are migration DDL, channel-proof's four files and one webui render, none touched here.
|
||||
- `node tools/unslop-hook/unslop-check.js` is clean on this file.
|
||||
- Not run: the bus, business and cli suites, `scripts/test-*.sh` and the mutant batch. All need Sage's slot.
|
||||
- Not run: the cli and other suites, `scripts/test-*.sh` and the mutant batch. They need Sage's slot.
|
||||
|
||||
## Follow-up, N5
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
8fa8a321933e75315babe21f49ba434a61dbeffbfcfded6933ebfaf6c73d3aee packages/business/src/business.mjs
|
||||
e72f0898b5f373c5a76d4c41fbe504904f55e18b183768260a6423510625683e packages/business/src/credentials.mjs
|
||||
d4646df073a8b411608f54a586b9ea61362aab51649e83c59dcc066cc989f381 packages/business/src/hosts.mjs
|
||||
6339bf85c946a41a5cbe8e94d51b264e298a9d16900e5dfe46b01b803e76ca41 packages/business/src/hosts.mjs
|
||||
ab5f7dc38a2ca5978b02d257068f4f82706db847cf0d6400f7cd548f8f6abe43 packages/business/src/resolve.mjs
|
||||
636916b9868b3a3e7f962894cbfa1e91629b5713a568f55f4716f942dbcd4910 packages/business/src/role.mjs
|
||||
830e690a8eea825c210747d9e91d24453d648d0302ab507589aeb978f19c7fcf packages/business/src/vocabulary.mjs
|
||||
77c432e879f8611a077feb2fb7c9b67e826ade558e41dcf01433fc2cdab2e0fa packages/business/src/vocabulary.mjs
|
||||
6a2c9b371fc9236bb0730ed108cbd770e9487b8ba733dbebccd11891ccba6f17 packages/business/tests/hard-waits.test.mjs
|
||||
9ef8b8be3c290f072e7c943b176a1ec688fda03fab4ba114d31213caf2216e42 packages/business/tests/hosts.test.mjs
|
||||
dbd0276dd299ad95d10a5a2d61a213bb92c3794fbd75ea4495cd6a107dd08343 packages/bus/src/broker.mjs
|
||||
fcaf50728e4599e5f10875f01c22b03610044b634c999830ef9684ef74cc62f3 packages/bus/src/broker.mjs
|
||||
ff927da3cf22fbacc9ace77b2e8b803bffc2953fb58cf7ab4e5f6f6493732996 packages/bus/src/business.mjs
|
||||
27d2ae155a2919788d7ad02da10a7056c23f5b71228e9af406b1b8aa0de55efc packages/bus/src/credentials.mjs
|
||||
86b4321dd436b97e389101873197dbab95b3d2202e5ef7da712c4978f4f9159f packages/bus/src/credential-verbs.mjs
|
||||
@@ -18,3 +18,6 @@ ff927da3cf22fbacc9ace77b2e8b803bffc2953fb58cf7ab4e5f6f6493732996 packages/bus/s
|
||||
be4e1eb88b689e8e4f859de75218af151feae8dce2c1409b726cb9c3dcd5ffe9 packages/bus/tests/spec-fixtures.mjs
|
||||
19755f7bb002d8455990c5dc7c458e64aeb95252ec7231a2ed2fd9c638fcb511 packages/cli/src/credentials.mjs
|
||||
152a3e57f7241db45bf8262fbf86ee029cf3b98f0d75503990abd46ac151a4b2 packages/cli/tests/credentials.test.mjs
|
||||
bdf88833cdb0c5ef76acec50c3c983b55c6053987a5c427cbc400ebd5f3c7110 packages/bus/tests/project-runtime.test.mjs
|
||||
e222a5fc3c117f62dbc854ef7397f7b5589e6f5d52e45398db0deeb43c53a9d5 roles/chief-of-staff.md
|
||||
d1016f8c34ee04601ab751c7514a20dfbeec0359d46dba8f94c44538dcc16133 roles/pm.md
|
||||
@@ -18,6 +18,8 @@ BROKER = "packages/bus/src/broker.mjs"
|
||||
CREDS = "packages/bus/src/credentials.mjs"
|
||||
RUN = "packages/bus/src/host-run.mjs"
|
||||
HOSTS = "packages/business/src/hosts.mjs"
|
||||
# Row 210 r4: validArgv and commandAllowed moved here so the sandboxed broker can read them.
|
||||
VOCAB = "packages/business/src/vocabulary.mjs"
|
||||
BUSINESS = "packages/business/src/business.mjs"
|
||||
RUNTIME = "packages/bus/src/runtime.mjs"
|
||||
|
||||
@@ -34,21 +36,21 @@ MUTANTS = [
|
||||
[("const grant = Object.hasOwn(b.roles[s.role]?.hosts ?? {}, a.host) ? b.roles[s.role].hosts[a.host] : null;",
|
||||
"const any = Object.assign({}, ...Object.values(b.roles).map((r) => r.hosts ?? {})); const grant = any[a.host] ?? null;")],
|
||||
[HOST_TEST]),
|
||||
("M04a argv array and count", HOSTS,
|
||||
("M04a argv array and count", VOCAB,
|
||||
[("if (!Array.isArray(argv) || argv.length === 0 || argv.length > ARGV_MAX) return false;",
|
||||
"if (!Array.isArray(argv)) return false;")], [HOSTS_TEST, HOST_TEST]),
|
||||
("M04b argv element string, empty, NUL", HOSTS,
|
||||
("M04b argv element string, empty, NUL", VOCAB,
|
||||
[(' if (typeof element !== "string" || element === "" || element.includes("\\0")) return false;\n', "")],
|
||||
[HOSTS_TEST, HOST_TEST]),
|
||||
("M04c argv element bytes", HOSTS,
|
||||
("M04c argv element bytes", VOCAB,
|
||||
[(" if (bytes > ARG_BYTES) return false;\n", "")], [HOSTS_TEST, HOST_TEST]),
|
||||
("M04d argv total bytes", HOSTS,
|
||||
("M04d argv total bytes", VOCAB,
|
||||
[(' return total <= ARGV_BYTES && (!argv[0].includes("/") || argv[0].startsWith("/"));',
|
||||
' return (!argv[0].includes("/") || argv[0].startsWith("/"));')], [HOSTS_TEST, HOST_TEST]),
|
||||
("M04e argv[0] path form", HOSTS,
|
||||
("M04e argv[0] path form", VOCAB,
|
||||
[(' return total <= ARGV_BYTES && (!argv[0].includes("/") || argv[0].startsWith("/"));',
|
||||
" return total <= ARGV_BYTES;")], [HOSTS_TEST, HOST_TEST]),
|
||||
("M05 prefix compares argv[0] only", HOSTS,
|
||||
("M05 prefix compares argv[0] only", VOCAB,
|
||||
[("prefix.every((element, i) => element === argv[i])", "prefix[0] === argv[0]")], [HOSTS_TEST, HOST_TEST]),
|
||||
("M06 string commands accepted", HOSTS,
|
||||
[(" if (!Array.isArray(commands)) refuse(",
|
||||
|
||||
@@ -3,8 +3,7 @@ import { isAbsolute } from 'node:path';
|
||||
import { NAME as HARNESS_NAME } from '../../plugins/src/kinds.mjs';
|
||||
import { loadRequirements } from '../../tasks/src/requirements.mjs';
|
||||
import { MERGE, LandingRefusal, readLanding } from './landing.mjs';
|
||||
import { ID_PATTERN, THREAD_DEFINITION, END_PROTECTED, forkProblem, forkUrl, credentialKind } from '../../business/src/vocabulary.mjs';
|
||||
import { validArgv, commandAllowed } from '../../business/src/hosts.mjs';
|
||||
import { ID_PATTERN, THREAD_DEFINITION, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed } from '../../business/src/vocabulary.mjs';
|
||||
|
||||
// Kept closed while S1 lands; its vocabulary is the integration authority.
|
||||
export const ACTIONS = Object.freeze([
|
||||
|
||||
@@ -12,7 +12,8 @@ import { fixture } from './spec-fixtures.mjs';
|
||||
test('Broker keeps the sandbox client import closure: only the listed imports, no lazy project import', () => {
|
||||
const source = readFileSync(new URL('../src/broker.mjs', import.meta.url), 'utf8');
|
||||
assert.deepEqual([...source.matchAll(/^import .*;$/gm)].map(m => m[0]), [
|
||||
"import { randomUUID, randomBytes } from 'node:crypto';",
|
||||
// Row 210 (#1707): createHash gives host.run events the argv, stdin and output sha256 values.
|
||||
"import { randomUUID, randomBytes, createHash } from 'node:crypto';",
|
||||
// Row 65 (#1558): bindLaunch checks a run's transcript path is absolute.
|
||||
"import { isAbsolute } from 'node:path';",
|
||||
// Row 65: the plug-in name pattern a harness takes. kinds.mjs is in RUNNER_FILES and imports nothing.
|
||||
@@ -23,7 +24,8 @@ test('Broker keeps the sandbox client import closure: only the listed imports, n
|
||||
// Row 141 (#1635): the business ID_PATTERN for instance names. The vocabulary is already in
|
||||
// RUNNER_FILES and imports nothing. Row 155 (#1649) reads THREAD_DEFINITION and END_PROTECTED
|
||||
// from it too. Row 199 (#1697) reads the D9c fork parse, forkProblem and forkUrl, from it.
|
||||
"import { ID_PATTERN, THREAD_DEFINITION, END_PROTECTED, forkProblem, forkUrl } from '../../business/src/vocabulary.mjs';",
|
||||
// Row 210 (#1707) reads credentialKind and the D6 argv check, validArgv and commandAllowed, from it.
|
||||
"import { ID_PATTERN, THREAD_DEFINITION, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed } from '../../business/src/vocabulary.mjs';",
|
||||
]);
|
||||
assert.doesNotMatch(source, /\bimport\s*\(/);
|
||||
assert.doesNotMatch(readFileSync(new URL('../src/landing.mjs', import.meta.url), 'utf8'), /\bimport\b/);
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
|
||||
import { isIP } from "node:net";
|
||||
import { refuse } from "./errors.mjs";
|
||||
import { HOST_ID } from "./vocabulary.mjs";
|
||||
import { HOST_ID, ARGV_MAX } from "./vocabulary.mjs";
|
||||
// Row 210 r4 (#1707): the argv check lives in vocabulary.mjs, which the worker sandbox mounts
|
||||
// and which imports nothing, so the broker can read it. Re-exported so the D6 checks keep one
|
||||
// business home here.
|
||||
export { ARGV_MAX, ARG_BYTES, ARGV_BYTES, validArgv, commandAllowed } from "./vocabulary.mjs";
|
||||
import { requireObject, rejectUnknownKeys, isPlainObject } from "./util.mjs";
|
||||
|
||||
const DNS = /^[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?$/;
|
||||
@@ -23,10 +27,6 @@ export const REFUSED_COMMANDS = Object.freeze([
|
||||
// Versioned names: python, python3, python3.12; awk, gawk, mawk, nawk; php, php8.2.
|
||||
const REFUSED_FORMS = Object.freeze([/^python[0-9.]*$/, /^[gmn]?awk$/, /^php[0-9.]*$/]);
|
||||
|
||||
export const ARGV_MAX = 256;
|
||||
export const ARG_BYTES = 4096;
|
||||
export const ARGV_BYTES = 32768;
|
||||
|
||||
export function refusedCommand(first) {
|
||||
const base = first.split("/").pop();
|
||||
return REFUSED_COMMANDS.includes(base) || REFUSED_FORMS.some((form) => form.test(base));
|
||||
@@ -125,22 +125,3 @@ export function checkHostGrants(grants, registry, where) {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// D6: a call's argv. 1 to 256 strings, each 1 to 4096 bytes with no NUL, 32768 bytes in all, and
|
||||
// argv[0] a bare name or an absolute path. Also the broker's check, at raise and at call.
|
||||
export function validArgv(argv) {
|
||||
if (!Array.isArray(argv) || argv.length === 0 || argv.length > ARGV_MAX) return false;
|
||||
let total = 0;
|
||||
for (const element of argv) {
|
||||
if (typeof element !== "string" || element === "" || element.includes("\0")) return false;
|
||||
const bytes = Buffer.byteLength(element);
|
||||
if (bytes > ARG_BYTES) return false;
|
||||
total += bytes;
|
||||
}
|
||||
return total <= ARGV_BYTES && (!argv[0].includes("/") || argv[0].startsWith("/"));
|
||||
}
|
||||
|
||||
// True when argv starts with one of the grant's prefixes, element by element, literally.
|
||||
export function commandAllowed(argv, commands) {
|
||||
return commands.some((prefix) => prefix.length <= argv.length && prefix.every((element, i) => element === argv[i]));
|
||||
}
|
||||
@@ -118,6 +118,31 @@ export const isHostKind = (name) =>
|
||||
// Every place that checks a credential kind asks this, so the closed set has one definition.
|
||||
export const credentialKind = (name) => SERVICES.includes(name) || isHostKind(name);
|
||||
|
||||
// Row 210 r4 (#1707): the broker checks argv with these. They sit here, not in hosts.mjs,
|
||||
// because broker.mjs runs in the worker sandbox and reads only mounted files that need nothing else.
|
||||
export const ARGV_MAX = 256;
|
||||
export const ARG_BYTES = 4096;
|
||||
export const ARGV_BYTES = 32768;
|
||||
|
||||
// D6: a call's argv. 1 to 256 strings, each 1 to 4096 bytes with no NUL, 32768 bytes in all, and
|
||||
// argv[0] a bare name or an absolute path. Also the broker's check, at raise and at call.
|
||||
export function validArgv(argv) {
|
||||
if (!Array.isArray(argv) || argv.length === 0 || argv.length > ARGV_MAX) return false;
|
||||
let total = 0;
|
||||
for (const element of argv) {
|
||||
if (typeof element !== "string" || element === "" || element.includes("\0")) return false;
|
||||
const bytes = Buffer.byteLength(element);
|
||||
if (bytes > ARG_BYTES) return false;
|
||||
total += bytes;
|
||||
}
|
||||
return total <= ARGV_BYTES && (!argv[0].includes("/") || argv[0].startsWith("/"));
|
||||
}
|
||||
|
||||
// True when argv starts with one of the grant's prefixes, element by element, literally.
|
||||
export function commandAllowed(argv, commands) {
|
||||
return commands.some((prefix) => prefix.length <= argv.length && prefix.every((element, i) => element === argv[i]));
|
||||
}
|
||||
|
||||
// Gitea token scopes are <read|write>:<category>. "all" and the admin
|
||||
// category are never granted to a role.
|
||||
export const GITEA_SCOPE_CATEGORIES = Object.freeze([
|
||||
|
||||
@@ -73,8 +73,8 @@ business's arbiter for its domain. Pass `domain` to `raise_decision`,
|
||||
gated raise, and every hard wait, goes to the human through the inbox.
|
||||
The hard waits are `spend`, `data.delete`, `network.expose`,
|
||||
`account.create`, `credential.mint`, `message.external`,
|
||||
`git.push.protected`, `git.push.remote` and `deploy`; a citation never
|
||||
moves them.
|
||||
`git.push.protected`, `git.push.remote`, `host.sudo` and `deploy`; a
|
||||
citation never moves them.
|
||||
A `role.revoke` raise whose target is the assistant is a hard wait too:
|
||||
it goes to the human, cited or not.
|
||||
|
||||
|
||||
+2
-2
@@ -53,8 +53,8 @@ business's arbiter for its domain. Pass `domain` to `raise_decision`,
|
||||
gated raise, and every hard wait, goes to the human through the inbox.
|
||||
The hard waits are `spend`, `data.delete`, `network.expose`,
|
||||
`account.create`, `credential.mint`, `message.external`,
|
||||
`git.push.protected`, `git.push.remote` and `deploy`; a citation never
|
||||
moves them.
|
||||
`git.push.protected`, `git.push.remote`, `host.sudo` and `deploy`; a
|
||||
citation never moves them.
|
||||
A `role.revoke` raise whose target is the assistant is a hard wait too:
|
||||
it goes to the human, cited or not.
|
||||
|
||||
|
||||
Reference in new issue
Block a user