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:
jason.woltje committed 2026-10-20 20:09:16 -05:00
1 parent 3cf63efb4b
commit c43008749b
9 files changed
+112 -59

No files matched your search

+59 -18
View File
@@ -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
+8 -6
View File
@@ -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(",
+1 -2
View File
@@ -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([
+4 -2
View File
@@ -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/);
+5 -24
View File
@@ -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]));
}
+25
View File
@@ -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([
+2 -2
View File
@@ -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
View File
@@ -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.