row 203 FED R2: federation key in the system config (#1701)
The loader validates an optional federation block: self and peers[].id labels, peers[].origin as an https origin with no path, query or userinfo, peers[].label non-empty and at most 80 characters, peer ids unique and distinct from self. Unknown sub-keys refuse. Labels and URLs only; the key grants no trust. Absent key: config loads with no federation output. Tests in scripts/test-config.sh cover each refusal and the no-key case. Notes and mutants M1-M6 in agents/quackerjack/work/queue-203/BUILD.md. Not run yet: the gate runs on CLEAR.
This commit is contained in:
1 parent
f2735bb6a1
commit
e704a18aec
3 files changed
+182
-1
No files matched your search
@@ -0,0 +1,81 @@
|
||||
# Row 203, FED R2: the federation key in the system config (#1701)
|
||||
|
||||
Owner Quackerjack, reviewer Webby, gate owner Sage. Brief: docs/plans/2026-10-20_federation-r1-r2-r5.md,
|
||||
"Brief: FED R2". Design: DESIGN.md section 3.1 (D1), section 4 (the key shape), section 7 row R2, at
|
||||
gizmoduck/federation-design b93f098f0.
|
||||
|
||||
Base: aca16404c on quackerjack/row203. Rebase onto 253's tip after 253 lands, under a new branch name.
|
||||
|
||||
## Shape
|
||||
|
||||
```json
|
||||
"federation": {
|
||||
"self": "hub",
|
||||
"peers": [{ "id": "factory", "origin": "https://mosaic.woltje.com", "label": "Software factory" }]
|
||||
}
|
||||
```
|
||||
|
||||
- Optional. Absent: the config loads and prints what it printed before (no `federation` in the output).
|
||||
- `self` and each `peers[].id` match `^[a-z][a-z0-9-]{0,31}$`. A peer id may not equal `self` or another peer's id.
|
||||
- `peers[].origin` must be an https origin exactly as `new URL(origin).origin` returns it: no path, no query,
|
||||
no userinfo, no trailing slash.
|
||||
- `peers[].label` is a non-empty string of at most 80 characters.
|
||||
- Labels and URLs only. Nothing here grants trust to a peer (section 3.1 D1, section 3.2 W1).
|
||||
|
||||
## Readers of the key (the manifest lists every one)
|
||||
|
||||
Only the loader reads it: `scripts/mosaic-config.mjs` (`validate`, the `federation` branch). Nothing else
|
||||
reads config `federation` at aca16404c (`git grep`).
|
||||
|
||||
Stale wording, NOT changed in this row (Sage rulings sage-2808 and sage-2809):
|
||||
`packages/cli/src/access.mjs` is out of row 203. Megavolt's row 207 changes that file. Follow-up, to be
|
||||
carried to DEFERRED by Sage:
|
||||
- access.mjs lines 26-27 (`ACCESS_HELP` comment): "Peer homes are empty until the federation key lands
|
||||
(row 203)". Still partly true after this row, since the access context does not read the key.
|
||||
- access.mjs line 36 (inside the `ACCESS_HELP` string, user-visible help): "Peer homes are not read yet
|
||||
(row 203 adds the federation key)". The same claim, in help text. No test in `packages/cli/tests` pins it
|
||||
(`git grep` for "row 203" and "Peer homes" finds nothing in tests).
|
||||
- access.mjs line 44 comment: "peers is empty until row 203".
|
||||
- `packages/cli/README.md` line 1107: "until row 203 adds the". Outside the manifest; flagged in the
|
||||
same follow-up.
|
||||
|
||||
Behavior after this row: `accessContextFor` still returns `peers: []` (access.mjs line 58). Row 203 adds the
|
||||
config key only, so grants naming a peer home still refuse. Wiring the key into the access context is not in
|
||||
the FED R2 brief.
|
||||
|
||||
## Refusals and their tests
|
||||
|
||||
`scripts/test-config.sh`, FED R2 block:
|
||||
|
||||
| Case | Refusal (exit 2) | Mutant (killed by) |
|
||||
|---|---|---|
|
||||
| valid key loads and is in the output | none | base |
|
||||
| unknown top-level sub-key | `unsupported federation key: "extra"` | M1 |
|
||||
| unknown peer sub-key | `unsupported federation.peers[0] key: "trust"` | M2 |
|
||||
| origin over http | `federation.peers[0].origin must be an https origin ...` | M3 |
|
||||
| origin with a path | same | M4 |
|
||||
| origin with userinfo | same | M4 |
|
||||
| peer id equal to self | `federation.peers[0].id ... is already named` | M5 |
|
||||
| peer label empty | `federation.peers[0].label must be a non-empty string ...` | M6 |
|
||||
| no federation key | loads, no federation output | base |
|
||||
|
||||
Mutant plan (run on Sage's gate only, one edit each, a named test must fail):
|
||||
|
||||
- M1: `rejectUnknownKeys(value, ["self", "peers"], "federation");` removed. Killed by "federation unknown sub-key".
|
||||
- M2: `rejectUnknownKeys(peer, ["id", "origin", "label"], where);` removed. Killed by "federation peer unknown sub-key".
|
||||
- M3: `url.protocol !== "https:" ||` removed. Killed by "origin over http".
|
||||
- M4: `|| url.origin !== value` removed. Killed by "origin with a path" and "origin with userinfo".
|
||||
- M5: the `ids.has(peer.id)` refusal removed. Killed by "peer id equal to self".
|
||||
|
||||
- M6: `peer.label.trim() === "" ||` removed. Killed by "peer label empty".
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No config is written. `~/.config/mosaic-dev/config.json` is not touched. Tests use `$SANDBOX` only.
|
||||
- No change to `packages/`, `scripts/bootstrap.sh`, or any reader.
|
||||
|
||||
## Status
|
||||
|
||||
- Code and tests written at aca16404c + this branch. NOT YET RUN: no node during Gander's slot (run 253, from
|
||||
16:32Z). The test-config.sh and cli gates run when Sage announces a CLEAR.
|
||||
- Not committed. Per invariant 8 the commit waits until test-config.sh is green.
|
||||
@@ -237,13 +237,66 @@ async function validateConsole(value) {
|
||||
}
|
||||
}
|
||||
|
||||
// FED R2 (#1701, DESIGN.md section 3.1 D1): the optional `federation` key names this instance and its
|
||||
// peers. Ids and origins are labels, not trust: nothing here grants a peer anything (3.1, W1). The
|
||||
// origin must be exactly an https origin, the form `new URL(origin).origin` returns, so a path, a
|
||||
// trailing slash, a port that is the default, or userinfo refuses.
|
||||
const FEDERATION_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
||||
const FEDERATION_LABEL_MAX = 80;
|
||||
|
||||
function validateFederationOrigin(value, where) {
|
||||
let url = null;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
url = null;
|
||||
}
|
||||
if (url === null || url.protocol !== "https:" || url.origin !== value) {
|
||||
fail(2, `${where} must be an https origin with no path, query, or userinfo (got ${JSON.stringify(value)})`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateFederation(value) {
|
||||
if (!isPlainObject(value)) {
|
||||
fail(2, '"federation" must be a JSON object');
|
||||
}
|
||||
rejectUnknownKeys(value, ["self", "peers"], "federation");
|
||||
if (typeof value.self !== "string" || !FEDERATION_ID_PATTERN.test(value.self)) {
|
||||
fail(2, `federation.self must match ${FEDERATION_ID_PATTERN} (got ${JSON.stringify(value.self)})`);
|
||||
}
|
||||
if (!Array.isArray(value.peers)) {
|
||||
fail(2, 'federation.peers must be an array');
|
||||
}
|
||||
const ids = new Set([value.self]);
|
||||
const peers = value.peers.map((peer, index) => {
|
||||
const where = `federation.peers[${index}]`;
|
||||
if (!isPlainObject(peer)) {
|
||||
fail(2, `${where} must be a JSON object`);
|
||||
}
|
||||
rejectUnknownKeys(peer, ["id", "origin", "label"], where);
|
||||
if (typeof peer.id !== "string" || !FEDERATION_ID_PATTERN.test(peer.id)) {
|
||||
fail(2, `${where}.id must match ${FEDERATION_ID_PATTERN} (got ${JSON.stringify(peer.id)})`);
|
||||
}
|
||||
if (ids.has(peer.id)) {
|
||||
fail(2, `${where}.id ${JSON.stringify(peer.id)} is already named (by federation.self or an earlier peer)`);
|
||||
}
|
||||
ids.add(peer.id);
|
||||
validateFederationOrigin(peer.origin, `${where}.origin`);
|
||||
if (typeof peer.label !== "string" || peer.label.trim() === "" || peer.label.length > FEDERATION_LABEL_MAX) {
|
||||
fail(2, `${where}.label must be a non-empty string of at most ${FEDERATION_LABEL_MAX} characters`);
|
||||
}
|
||||
return { id: peer.id, origin: peer.origin, label: peer.label };
|
||||
});
|
||||
return { self: value.self, peers };
|
||||
}
|
||||
|
||||
async function validate(document, file) {
|
||||
if (!isPlainObject(document)) {
|
||||
fail(2, "configuration must be a JSON object");
|
||||
}
|
||||
rejectUnknownKeys(
|
||||
document,
|
||||
["configVersion", "environment", "dataRoot", "execution", "console"],
|
||||
["configVersion", "environment", "dataRoot", "execution", "console", "federation"],
|
||||
"configuration",
|
||||
);
|
||||
|
||||
@@ -281,6 +334,9 @@ async function validate(document, file) {
|
||||
// it, so a config without it prints what it always printed.
|
||||
const consoleKey = Object.hasOwn(document, "console") ? await validateConsole(document.console) : undefined;
|
||||
|
||||
// FED R2: the same rule as `console`. The key appears in the output only when the file has it.
|
||||
const federationKey = Object.hasOwn(document, "federation") ? validateFederation(document.federation) : undefined;
|
||||
|
||||
return {
|
||||
configVersion: document.configVersion,
|
||||
environment: document.environment,
|
||||
@@ -292,6 +348,7 @@ async function validate(document, file) {
|
||||
adapter,
|
||||
},
|
||||
...(consoleKey === undefined ? {} : { console: consoleKey }),
|
||||
...(federationKey === undefined ? {} : { federation: federationKey }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,49 @@ expect_refusal "console.sso slot name outside the pattern exits 2 naming the pat
|
||||
expect_refusal "unknown key under console exits 2 naming the path" \
|
||||
"$(sso_body '{"sso":{"publicOrigin":"https://console.test","providers":['"$(provider)"']},"extra":1}')" 'console.extra is not a supported key'
|
||||
|
||||
# --- FED R2 (#1701, DESIGN.md section 3.1): the optional federation key ---
|
||||
# fed_body FEDERATION-JSON: a config whose federation key is the argument.
|
||||
fed_body() {
|
||||
printf '{"configVersion":1,"environment":"development","dataRoot":"%s","execution":{"backend":"docker","provider":"zai","model":"m"},"federation":%s}' "$DATA_ROOT" "$1"
|
||||
}
|
||||
FED_VALID='{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":"Software factory"}]}'
|
||||
|
||||
cfg fed-valid.json "$(fed_body "$FED_VALID")"
|
||||
OUT="$(MOSAIC_CONFIG="$SANDBOX/fed-valid.json" timeout 20 $CONFIG_OP validate)"
|
||||
[ $? -eq 0 ] && printf '%s' "$OUT" | grep -q '"self": "hub"' && printf '%s' "$OUT" | grep -q '"origin": "https://mosaic.woltje.com"'
|
||||
check "valid federation key loads and is in the output" $?
|
||||
|
||||
# expect_fed_refusal NAME PATH-IN-MESSAGE FEDERATION-JSON: exit 2, and the message names the bad key path.
|
||||
expect_fed_refusal() {
|
||||
cfg fed-bad.json "$(fed_body "$3")"
|
||||
local err rc
|
||||
err="$(MOSAIC_CONFIG="$SANDBOX/fed-bad.json" timeout 20 $CONFIG_OP validate 2>&1 >/dev/null)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 2 ] && printf '%s' "$err" | grep -qF -- "$2"
|
||||
check "$1 (exit $rc)" $?
|
||||
}
|
||||
expect_fed_refusal "federation unknown sub-key exits 2 naming the key" 'unsupported federation key: "extra"' \
|
||||
'{"self":"hub","peers":[],"extra":1}'
|
||||
expect_fed_refusal "federation peer unknown sub-key exits 2 naming the key" 'unsupported federation.peers[0] key: "trust"' \
|
||||
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":"F","trust":"full"}]}'
|
||||
expect_fed_refusal "federation peer origin over http exits 2 naming the key" "federation.peers[0].origin" \
|
||||
'{"self":"hub","peers":[{"id":"factory","origin":"http://mosaic.woltje.com","label":"F"}]}'
|
||||
expect_fed_refusal "federation peer origin with a path exits 2 naming the key" "federation.peers[0].origin" \
|
||||
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com/console","label":"F"}]}'
|
||||
expect_fed_refusal "federation peer origin with userinfo exits 2 naming the key" "federation.peers[0].origin" \
|
||||
'{"self":"hub","peers":[{"id":"factory","origin":"https://[email protected]","label":"F"}]}'
|
||||
expect_fed_refusal "federation peer id equal to self exits 2 naming the key" "federation.peers[0].id" \
|
||||
'{"self":"hub","peers":[{"id":"hub","origin":"https://mosaic.woltje.com","label":"F"}]}'
|
||||
expect_fed_refusal "federation peer label empty exits 2 naming the key" "federation.peers[0].label" \
|
||||
'{"self":"hub","peers":[{"id":"factory","origin":"https://mosaic.woltje.com","label":""}]}'
|
||||
|
||||
# Without the key: no federation in the output, and the config still loads (FED R2 boundary).
|
||||
cfg no-federation.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
|
||||
NO_FED_OUT="$(MOSAIC_CONFIG="$SANDBOX/no-federation.json" timeout 20 $CONFIG_OP validate)"
|
||||
NO_FED_RC=$?
|
||||
[ "$NO_FED_RC" -eq 0 ] && ! printf '%s' "$NO_FED_OUT" | grep -q '"federation"'
|
||||
check "config without federation loads and has no federation output" $?
|
||||
|
||||
# Without the key: the output is exactly what the loader printed before phase 1 (sage-1340, F2).
|
||||
cfg no-console.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
|
||||
EXPECTED="$(printf '{\n "configVersion": 1,\n "environment": "development",\n "dataRoot": "%s",\n "execution": {\n "backend": "docker",\n "provider": "zai",\n "model": "m",\n "adapter": "pi"\n }\n}' "$DATA_ROOT")"
|
||||
|
||||
Reference in new issue
Block a user