agent-coord-mcp 0.26.36 → 0.26.40
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/dist/auth-faults.js +145 -0
- package/dist/auth-faults.js.map +1 -0
- package/dist/capabilities.js +113 -2
- package/dist/capabilities.js.map +1 -1
- package/dist/launch-unit.js +282 -0
- package/dist/launch-unit.js.map +1 -0
- package/dist/server.js +46 -7
- package/dist/server.js.map +1 -1
- package/dist/store.js +139 -1
- package/dist/store.js.map +1 -1
- package/dist/test-count-key.js +22 -0
- package/dist/test-count-key.js.map +1 -0
- package/dist/tools/away.js +80 -17
- package/dist/tools/away.js.map +1 -1
- package/dist/tools/events.js +77 -3
- package/dist/tools/events.js.map +1 -1
- package/dist/tools/herdr-delivery.js +51 -36
- package/dist/tools/herdr-delivery.js.map +1 -1
- package/dist/tools/herdr-tail.js +99 -77
- package/dist/tools/herdr-tail.js.map +1 -1
- package/dist/tools/registry.js +50 -3
- package/dist/tools/registry.js.map +1 -1
- package/dist/transports/herdr.js +92 -2
- package/dist/transports/herdr.js.map +1 -1
- package/package.json +1 -1
- package/scripts/check-test-count.mjs +80 -6
- package/scripts/coord-attention-clock.mjs +19 -21
- package/scripts/coord-stall-clock.mjs +23 -24
- package/scripts/lib/clock-unit-cli.mjs +134 -0
- package/src/auth-faults.ts +166 -0
- package/src/capabilities.ts +115 -2
- package/src/launch-unit.ts +350 -0
- package/src/server.ts +47 -6
- package/src/store.ts +146 -1
- package/src/test-count-key.ts +21 -0
- package/src/tools/away.ts +113 -28
- package/src/tools/events.ts +89 -4
- package/src/tools/herdr-delivery.ts +46 -31
- package/src/tools/herdr-tail.ts +84 -65
- package/src/tools/registry.ts +55 -3
- package/src/transports/herdr.ts +93 -2
|
@@ -42,31 +42,30 @@ const dist = path.join(here, "..", "dist");
|
|
|
42
42
|
|
|
43
43
|
const LABEL = "com.davidbalzan.coord-stall-clock";
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// ⟨q-6d37c349⟩ `--install` INSTALLS, and refuses unless launchd reports the label loaded. It used to
|
|
46
|
+
// print this plist to stdout with instructions for a human, and nobody ever ran them: measured
|
|
47
|
+
// 2026-09-26, `launchctl print gui/501/com.davidbalzan.coord-stall-clock` exits 113 and no plist
|
|
48
|
+
// exists, so every one of this clock's 200 recorded runs was a HAND run by whoever last polled it.
|
|
49
|
+
// The printed recipe is still available, named for what it is: `--print-plist`.
|
|
50
|
+
if (argv.install || argv.verify || argv.uninstall || argv["print-plist"]) {
|
|
51
|
+
const { handleUnitArgs } = await import("./lib/clock-unit-cli.mjs");
|
|
48
52
|
const every = Number(argv.every ?? 30);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
</dict></plist>
|
|
66
|
-
|
|
67
|
-
# Linux — cron:
|
|
68
|
-
# */${every} * * * * ${node} ${self} --repo=${repo} --duty=${duty}
|
|
69
|
-
`);
|
|
53
|
+
await handleUnitArgs(argv, {
|
|
54
|
+
dist,
|
|
55
|
+
repoRoot: path.join(here, "..", "..", ".."),
|
|
56
|
+
label: LABEL,
|
|
57
|
+
program: process.execPath,
|
|
58
|
+
programArgs: [
|
|
59
|
+
path.join(here, "coord-stall-clock.mjs"),
|
|
60
|
+
`--repo=${String(argv.repo ?? process.cwd())}`,
|
|
61
|
+
`--duty=${String(argv.duty ?? "<duty-officer-agent-id>")}`,
|
|
62
|
+
...(argv.david ? [`--david=${String(argv.david)}`] : []),
|
|
63
|
+
...(argv["board-owner"] ? [`--board-owner=${String(argv["board-owner"])}`] : []),
|
|
64
|
+
],
|
|
65
|
+
everyMinutes: every,
|
|
66
|
+
stderrPath: path.join(homedir(), "agent-coord", "logs", "stall-clock.err"),
|
|
67
|
+
stdoutPath: path.join(homedir(), "agent-coord", "logs", "stall-clock.log"),
|
|
68
|
+
});
|
|
70
69
|
process.exit(0);
|
|
71
70
|
}
|
|
72
71
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ⟨q-6d37c349⟩ THE INSTALL/VERIFY/UNINSTALL CLI BOTH CLOCKS SHARE.
|
|
3
|
+
*
|
|
4
|
+
* Both clocks had the same `--install` that PRINTED a plist and instructions for a human, and neither
|
|
5
|
+
* unit was ever created: measured 2026-09-26, `launchctl print gui/501/<label>` exits 113 for both.
|
|
6
|
+
* Writing this once means a third clock gets a real installer by importing it, and — the part that
|
|
7
|
+
* matters — it cannot drift into two versions where one reads launchd back and the other does not.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ `--install` SUCCEEDS ONLY IF launchd REPORTS THE LABEL LOADED, and exits non-zero otherwise. The
|
|
10
|
+
* old behaviour is still available as `--print-plist`, explicitly named as a recipe rather than an
|
|
11
|
+
* installation, because a printed plist was the thing everyone mistook for one.
|
|
12
|
+
*/
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
export const runner = (cmd, args) => {
|
|
18
|
+
const r = spawnSync(cmd, args, { encoding: "utf8" });
|
|
19
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
20
|
+
};
|
|
21
|
+
export const unitFs = { writeFileSync, mkdirSync, existsSync, readFileSync, rmSync };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Handle `--install`, `--verify`, `--uninstall` and `--print-plist` for one clock. Returns false when
|
|
25
|
+
* the argv asked for none of them, so the caller falls through to its actual work.
|
|
26
|
+
*
|
|
27
|
+
* ⛔ macOS ONLY, AND IT SAYS SO RATHER THAN PRETENDING. On any other platform `--install` REFUSES and
|
|
28
|
+
* prints the cron line: claiming an installation this code cannot verify is the failure being fixed,
|
|
29
|
+
* and a cron line printed under a refusal cannot be mistaken for a unit that exists.
|
|
30
|
+
*/
|
|
31
|
+
export async function handleUnitArgs(argv, spec, io = {}) {
|
|
32
|
+
/*
|
|
33
|
+
* ⛔ THE EXECUTOR IS INJECTED, AND THAT IS NOT TIDINESS — IT IS THE FIX FOR AN INCIDENT.
|
|
34
|
+
*
|
|
35
|
+
* The test for the linked-worktree refusal used to SPAWN this script with `--install`. On 2026-09-26 a
|
|
36
|
+
* mutation matrix removed the refusal (`if (false)`) and that test then installed
|
|
37
|
+
* `com.davidbalzan.coord-stall-clock` into the operator's own launchd — twice — while still failing its
|
|
38
|
+
* assertion. A refusal suite hides unsafe success: the assertion fires AFTER the side effect.
|
|
39
|
+
*
|
|
40
|
+
* With `run` and `exit` injectable, the refusal can be exercised against a RECORDING runner, and the
|
|
41
|
+
* test asserts something stronger than the exit code: that ZERO launchctl calls were made. A guard
|
|
42
|
+
* mutated away then fails the test by making calls, without those calls reaching launchd.
|
|
43
|
+
*/
|
|
44
|
+
const run = io.run ?? runner;
|
|
45
|
+
const exit = io.exit ?? ((code) => process.exit(code));
|
|
46
|
+
/*
|
|
47
|
+
* ⛔ AND THE FILESYSTEM IS INJECTED TOO, because injecting only the RUNNER was not enough and I found
|
|
48
|
+
* that the same way I found everything else here — by measuring. With `run` recorded but `fs` real, a
|
|
49
|
+
* guard mutated away still WROTE the plist into the operator's `~/Library/LaunchAgents`; only the
|
|
50
|
+
* bootstrap was withheld, so the label stayed unloaded and the file did not. Three layers of the same
|
|
51
|
+
* lesson: a refusal test has to inject EVERY executor the path can reach, and prove it with a
|
|
52
|
+
* postcondition on the real world rather than on the return value.
|
|
53
|
+
*/
|
|
54
|
+
const fs = io.fs ?? unitFs;
|
|
55
|
+
const { label, program, programArgs, everyMinutes, stderrPath, stdoutPath, cronNote } = spec;
|
|
56
|
+
const mod = await import(path.join(spec.dist, "launch-unit.js"));
|
|
57
|
+
const unitIo = { run, fs };
|
|
58
|
+
const startIntervalSec = Math.round(everyMinutes * 60);
|
|
59
|
+
const unitSpec = { label, program, programArgs, startIntervalSec, stderrPath, stdoutPath };
|
|
60
|
+
|
|
61
|
+
if (argv["print-plist"]) {
|
|
62
|
+
console.log(`# A RECIPE, NOT AN INSTALLATION — ${label}`);
|
|
63
|
+
console.log(`# \`--install\` writes this, bootstraps it, and refuses unless launchd reports it loaded.`);
|
|
64
|
+
console.log(mod.plistFor(unitSpec));
|
|
65
|
+
console.log(`# Linux — cron:\n# */${everyMinutes} * * * * ${program} ${programArgs.join(" ")}${cronNote ? ` # ${cronNote}` : ""}`);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (argv.verify) {
|
|
70
|
+
const s = mod.isLoaded(label, run);
|
|
71
|
+
console.log(`${s.loaded ? "LOADED " : "ABSENT "} ${label} — ${s.evidence}`);
|
|
72
|
+
if (!s.loaded) {
|
|
73
|
+
console.error(
|
|
74
|
+
`⛔ NOTHING IS SCHEDULED for ${label}. A plist on disk that was never bootstrapped is exactly as\n` +
|
|
75
|
+
` inert as no plist at all, and it looks more installed. Install it: --install --repo=<path> …`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return exit(s.loaded ? 0 : 1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (argv.uninstall) {
|
|
82
|
+
const r = mod.uninstallUnit(label, unitIo);
|
|
83
|
+
console.log(`${r.ok ? "removed" : "REFUSED"} ${label} — wasLoaded=${r.wasLoaded} plistRemoved=${r.plistRemoved} · ${r.evidence}`);
|
|
84
|
+
return exit(r.ok ? 0 : 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (argv.install) {
|
|
88
|
+
/*
|
|
89
|
+
* ⛔ NEVER PIN A UNIT TO A DISPOSABLE TREE. `ProgramArguments` holds an absolute path to the script,
|
|
90
|
+
* and a slice worktree is REMOVED after its PR merges — so a unit installed from one runs until that
|
|
91
|
+
* directory disappears and then fails every tick forever, which on disk is indistinguishable from a
|
|
92
|
+
* clock that was never installed. A linked worktree has a `.git` FILE where the primary checkout has
|
|
93
|
+
* a directory, which is how this is detected rather than guessed from the path's spelling.
|
|
94
|
+
*/
|
|
95
|
+
const gitEntry = path.join(spec.repoRoot ?? "", ".git");
|
|
96
|
+
if (spec.repoRoot && existsSync(gitEntry) && !statSync(gitEntry).isDirectory()) {
|
|
97
|
+
const primary = (() => {
|
|
98
|
+
try {
|
|
99
|
+
return readFileSync(gitEntry, "utf8").replace(/^gitdir:\s*/, "").trim().replace(/\/worktrees\/.*$/, "").replace(/\/\.git$/, "");
|
|
100
|
+
} catch {
|
|
101
|
+
return "<the primary checkout>";
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
console.error(
|
|
105
|
+
`⛔ INSTALL REFUSED — ${label} would be pinned to a LINKED WORKTREE:\n` +
|
|
106
|
+
` ${spec.repoRoot}\n` +
|
|
107
|
+
` A slice tree is removed once its PR merges, and the unit would then fail every tick forever —\n` +
|
|
108
|
+
` which looks exactly like a clock that was never installed. Install from the primary checkout:\n` +
|
|
109
|
+
` ${primary}`,
|
|
110
|
+
);
|
|
111
|
+
return exit(2);
|
|
112
|
+
}
|
|
113
|
+
if (process.platform !== "darwin") {
|
|
114
|
+
console.error(
|
|
115
|
+
`⛔ REFUSED — no installer for platform "${process.platform}". This would have printed a cron line and\n` +
|
|
116
|
+
` exited 0, which is how a documented unit gets mistaken for an installed one. Install it yourself:\n` +
|
|
117
|
+
` */${everyMinutes} * * * * ${program} ${programArgs.join(" ")}`,
|
|
118
|
+
);
|
|
119
|
+
return exit(2);
|
|
120
|
+
}
|
|
121
|
+
const r = mod.installUnit(unitSpec, unitIo);
|
|
122
|
+
if (!r.ok) {
|
|
123
|
+
console.error(`⛔ INSTALL REFUSED — ${r.label}\n ${r.why}\n ${r.evidence}`);
|
|
124
|
+
return exit(1);
|
|
125
|
+
}
|
|
126
|
+
console.log(
|
|
127
|
+
`${r.action} ${r.label} — every ${everyMinutes}m · plist ${r.plistPath}\n` +
|
|
128
|
+
` VERIFIED BY READBACK: ${r.evidence}`,
|
|
129
|
+
);
|
|
130
|
+
return exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// ⟨q-a441f1e1⟩ — ONE 401 STOOD FOR EVERY AUTH FAULT, AND IT READ AS A BROKEN DAEMON IN ALL OF THEM.
|
|
2
|
+
//
|
|
3
|
+
// Asked for by David 2026-09-21. The HTTP bus answered `unauthorized` — one string — whether the
|
|
4
|
+
// bearer was never minted, had just been rotated, or was absent from the request entirely. Measured
|
|
5
|
+
// consequence: the other machine spent real time reading a stale tokens.json as infrastructure.
|
|
6
|
+
// The daemon knows which fault it is at the moment it refuses; it simply did not say.
|
|
7
|
+
//
|
|
8
|
+
// ⛔ THE SPLIT THAT DECIDES WHERE EACH FAULT MAY BE NAMED (coordinator's ruling, 2026-09-24,
|
|
9
|
+
// escalated to David as a risk acceptance):
|
|
10
|
+
//
|
|
11
|
+
// A fault about the REQUEST'S SHAPE may be named freely to the caller.
|
|
12
|
+
// A fault about the CREDENTIAL'S VALIDITY must not be distinguishable to the caller.
|
|
13
|
+
//
|
|
14
|
+
// Shape faults — no header, wrong scheme, empty bearer — describe what the caller just sent, so
|
|
15
|
+
// naming them tells an attacker nothing they did not already know, and tells a misconfigured client
|
|
16
|
+
// exactly what to fix. Validity faults are different in kind: distinguishing "never minted" from
|
|
17
|
+
// "retired at 11:02 for agent worker-3" answers a question ONLY the holder of a real former
|
|
18
|
+
// credential can ask, and the answer is "yes, that was genuine" — it confirms the leak source,
|
|
19
|
+
// confirms the agent exists, and dates the rotation, for someone who has just failed to
|
|
20
|
+
// authenticate. The legitimate operator diagnosing "my seat stopped working" reads the DAEMON LOG;
|
|
21
|
+
// an attacker cannot. That asymmetry is the point.
|
|
22
|
+
//
|
|
23
|
+
// ⛔ SO `unknown-bearer`, `retired-bearer` AND `advisory-mismatch` SHARE ONE RESPONSE STRING, BYTE
|
|
24
|
+
// FOR BYTE. If they differed by so much as punctuation the string itself would be the oracle. A
|
|
25
|
+
// test compares them for EQUALITY (not "both non-empty"), so any mutation that makes the response
|
|
26
|
+
// distinguish them turns it red.
|
|
27
|
+
//
|
|
28
|
+
// ⛔ AND NO REFUSAL OR LOG LINE MAY CONTAIN ANY PART OF A BEARER — not the value, not a prefix, not
|
|
29
|
+
// its hash, not a hash prefix. Messages are built from the fault kind, an agent id and a timestamp
|
|
30
|
+
// only. qa measured this across 8 refusals on ⟨q-4b9cdeab⟩; the same bar applies here and there is
|
|
31
|
+
// a test that feeds a real bearer through every branch and greps every output for it.
|
|
32
|
+
//
|
|
33
|
+
// ⚠ WHAT THIS DOES NOT COVER, MEASURED AT MAIN `1e4712a` RATHER THAN ASSUMED: the queue row lists
|
|
34
|
+
// "(b) the bearer maps to a DIFFERENT agent than the one the caller claims". That fault CANNOT
|
|
35
|
+
// REACH THIS BOUNDARY — at the auth gate the caller claims nothing; identity arrives FROM the
|
|
36
|
+
// bearer. Where a claim does meet a bearer it is already named: the tool path refuses with
|
|
37
|
+
// `identity bound to '<bound>'; rejected attempt to act as '<claimed>'` (server.ts:362, 413, 587)
|
|
38
|
+
// and a session opened by another agent's bearer refuses 403 `session/identity mismatch`
|
|
39
|
+
// (server.ts:1190-1196). ⟨q-0c5476ec⟩ removed the fault at its source and ⟨q-4b9cdeab⟩'s `claim()`
|
|
40
|
+
// closed the last way two agents could share a key. A (b) branch here would be an empty case that
|
|
41
|
+
// reads as coverage.
|
|
42
|
+
|
|
43
|
+
/** Why a request was refused at the auth gate. One kind per genuinely distinct operator action. */
|
|
44
|
+
export type AuthFaultKind =
|
|
45
|
+
| "no-header" // SHAPE: no Authorization header at all
|
|
46
|
+
| "not-bearer" // SHAPE: an Authorization header that is not a Bearer scheme
|
|
47
|
+
| "empty-bearer" // SHAPE: `Bearer` with nothing after it
|
|
48
|
+
| "unknown-bearer" // VALIDITY: a bearer the token map has never held
|
|
49
|
+
| "retired-bearer" // VALIDITY: a bearer this daemon SAW leave the map
|
|
50
|
+
| "advisory-mismatch"; // VALIDITY: advisory mode, the shared bearer does not match
|
|
51
|
+
|
|
52
|
+
/** Which half of the split a kind belongs to. Exported so the rule is checkable, not folklore. */
|
|
53
|
+
export const SHAPE_FAULTS: readonly AuthFaultKind[] = ["no-header", "not-bearer", "empty-bearer"];
|
|
54
|
+
export const VALIDITY_FAULTS: readonly AuthFaultKind[] = ["unknown-bearer", "retired-bearer", "advisory-mismatch"];
|
|
55
|
+
|
|
56
|
+
/** What this daemon remembers about a bearer that left the map while it was watching. */
|
|
57
|
+
export type RetiredBearer = { agentId: string; at: number; cause: "replaced" | "revoked" };
|
|
58
|
+
|
|
59
|
+
export type AuthFault = { kind: AuthFaultKind; retired?: RetiredBearer };
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ⛔ THE ONE STRING EVERY VALIDITY FAULT RETURNS. Named as a constant rather than repeated, because
|
|
63
|
+
* three copies of a sentence are three chances for one of them to drift into an oracle.
|
|
64
|
+
*
|
|
65
|
+
* ⭐ AND IT NAMES WHERE TO LOOK, WHICH IS PART OF DAVID'S RULING (2026-09-25, "ok do as you
|
|
66
|
+
* recommend") RATHER THAN A COURTESY. Log-only is defensible ONLY because the legitimate operator
|
|
67
|
+
* always has the log and an attacker never does — so a 401 whose log the reader cannot REACH has not
|
|
68
|
+
* protected the information, it has LOST it. That loss is not hypothetical: it is the hour David's
|
|
69
|
+
* other machine spent reading a stale tokens.json as a broken daemon, which is why this row exists.
|
|
70
|
+
*
|
|
71
|
+
* ⛔ THE POINTER NAMES THE LOG BY ROLE, NEVER BY ABSOLUTE PATH. An unauthenticated caller must not be
|
|
72
|
+
* handed `/Users/<name>/agent-coord/…` — that discloses the operator's account for no benefit to
|
|
73
|
+
* them, since they cannot read the file either way. The EXACT path is printed to the daemon log at
|
|
74
|
+
* bind, where only someone who can already read it will see it.
|
|
75
|
+
*/
|
|
76
|
+
export const VALIDITY_REFUSAL =
|
|
77
|
+
"unauthorized: this bearer is not current — the reason is recorded in this bus's daemon log " +
|
|
78
|
+
"(the stderr of the process serving this port); ask whoever runs the bus if you cannot reach it";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The switch the coordinator asked to keep while David rules on the disclosure trade-off. Default
|
|
82
|
+
* is log-only. `AGENT_COORD_401_DETAIL=response` moves the validity detail into the response.
|
|
83
|
+
* ⚠ It is read per call, not cached, so a test can flip it without a fresh process.
|
|
84
|
+
*/
|
|
85
|
+
export function detailInResponse(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
86
|
+
return env.AGENT_COORD_401_DETAIL === "response";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** What the CALLER is told. Shape faults are specific; validity faults are uniform. */
|
|
90
|
+
export function responseText(fault: AuthFault, env: NodeJS.ProcessEnv = process.env): string {
|
|
91
|
+
switch (fault.kind) {
|
|
92
|
+
case "no-header":
|
|
93
|
+
return "unauthorized: no Authorization header. This bus is in bound mode; send `Authorization: Bearer <token>` minted by `coord-token add <agent-id>`.";
|
|
94
|
+
case "not-bearer":
|
|
95
|
+
return "unauthorized: the Authorization header is not a Bearer scheme. Send `Authorization: Bearer <token>`.";
|
|
96
|
+
case "empty-bearer":
|
|
97
|
+
return "unauthorized: the Bearer scheme carried no token — the header is present and empty, which is what an unset variable in a client config produces.";
|
|
98
|
+
default:
|
|
99
|
+
// Every validity fault, one string. The escape hatch exists for David's ruling and is the
|
|
100
|
+
// ONLY way these can differ; with it unset they are identical by construction, not by care.
|
|
101
|
+
return detailInResponse(env) ? logText(fault) : VALIDITY_REFUSAL;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* What the DAEMON LOG is told: every fault distinguished, including which retirement it was and
|
|
107
|
+
* when. This is the half an operator diagnosing "my seat stopped working" actually reads.
|
|
108
|
+
*
|
|
109
|
+
* ⛔ EACH KIND GETS ITS OWN SENTENCE AND ITS OWN REMEDY — a message covering two faults sends the
|
|
110
|
+
* reader to the wrong file, which is the defect this row exists for. The guard is a PAIRWISE
|
|
111
|
+
* inequality over every kind, so collapsing two branches into one string cannot pass.
|
|
112
|
+
*/
|
|
113
|
+
export function logText(fault: AuthFault): string {
|
|
114
|
+
switch (fault.kind) {
|
|
115
|
+
case "no-header":
|
|
116
|
+
return "401 no Authorization header — bound mode expects `Bearer <token>`";
|
|
117
|
+
case "not-bearer":
|
|
118
|
+
return "401 Authorization header is not a Bearer scheme";
|
|
119
|
+
case "empty-bearer":
|
|
120
|
+
return "401 Bearer scheme carried no token (header present, value empty)";
|
|
121
|
+
case "unknown-bearer":
|
|
122
|
+
return "401 bearer is not in the token map — never minted on this bus, or minted for another bus. `coord-token add <agent-id>`, then SIGHUP (the daemon also reloads when the file changes)";
|
|
123
|
+
case "retired-bearer": {
|
|
124
|
+
// Reaching this kind REQUIRES the record; without it the line would print "undefined" into an
|
|
125
|
+
// auth log, so the fallback says what it knows rather than what it does not.
|
|
126
|
+
if (!fault.retired) return "401 bearer is no longer current (no retirement record in this process)";
|
|
127
|
+
const when = new Date(fault.retired.at).toISOString();
|
|
128
|
+
const how = fault.retired.cause === "revoked" ? "revoked" : "replaced by a newer token";
|
|
129
|
+
return `401 bearer was ${how} at ${when} for agent '${fault.retired.agentId}' — it authenticated until then, so this daemon is healthy and the client holds a superseded token`;
|
|
130
|
+
}
|
|
131
|
+
case "advisory-mismatch":
|
|
132
|
+
return "401 shared bearer does not match — this bus has no tokens.json, so it is in advisory mode and checks one shared secret";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Classify a refusal. Pure: every input is a value, so the whole taxonomy is testable without a
|
|
138
|
+
* daemon, a socket or a file — which is what lets a mutation matrix reach each branch.
|
|
139
|
+
*
|
|
140
|
+
* `lookup` answers "is this bearer live?" and `retiredLookup` "did I watch it leave?". Both are
|
|
141
|
+
* injected rather than imported so a test can pose a state the filesystem cannot.
|
|
142
|
+
*/
|
|
143
|
+
export function classifyAuthFault(args: {
|
|
144
|
+
authHeader: string | undefined;
|
|
145
|
+
bound: boolean;
|
|
146
|
+
lookup: (bearer: string) => string | undefined;
|
|
147
|
+
retiredLookup: (bearer: string) => RetiredBearer | undefined;
|
|
148
|
+
sharedMatches?: boolean;
|
|
149
|
+
}): { ok: true; agentId?: string } | { ok: false; fault: AuthFault } {
|
|
150
|
+
const { authHeader, bound, lookup, retiredLookup, sharedMatches } = args;
|
|
151
|
+
if (!authHeader) return { ok: false, fault: { kind: "no-header" } };
|
|
152
|
+
if (!authHeader.startsWith("Bearer ")) return { ok: false, fault: { kind: "not-bearer" } };
|
|
153
|
+
const bearer = authHeader.slice("Bearer ".length);
|
|
154
|
+
|
|
155
|
+
if (!bound) {
|
|
156
|
+
// Advisory mode: there is no map, so "unknown" and "retired" are not answerable and must not be
|
|
157
|
+
// guessed at. One fault, and the LOG says which mode the daemon is in.
|
|
158
|
+
return sharedMatches ? { ok: true } : { ok: false, fault: { kind: "advisory-mismatch" } };
|
|
159
|
+
}
|
|
160
|
+
if (bearer === "") return { ok: false, fault: { kind: "empty-bearer" } };
|
|
161
|
+
const agentId = lookup(bearer);
|
|
162
|
+
if (agentId) return { ok: true, agentId };
|
|
163
|
+
const retired = retiredLookup(bearer);
|
|
164
|
+
if (retired) return { ok: false, fault: { kind: "retired-bearer", retired } };
|
|
165
|
+
return { ok: false, fault: { kind: "unknown-bearer" } };
|
|
166
|
+
}
|
package/src/capabilities.ts
CHANGED
|
@@ -39,7 +39,7 @@ import path from "node:path";
|
|
|
39
39
|
import { prRefsIn } from "./tools/record-events.js";
|
|
40
40
|
import { EVENT_KIND_IDS } from "./tools/event-kinds.js";
|
|
41
41
|
import { suggestRecordType, typedRecordMode } from "./typed-records.js";
|
|
42
|
-
import { LEAD_REFUSED, PARKED_CATEGORIES } from "./tools/away.js";
|
|
42
|
+
import { LEAD_REFUSED, PARKED_CATEGORIES, meetsCoverageThreshold } from "./tools/away.js";
|
|
43
43
|
import { recordAuthorityFor } from "./roles.js";
|
|
44
44
|
// ⟨q-e5cb3538⟩ The renderer and the byte class live in hooks/, shared with both pushers.
|
|
45
45
|
// @ts-expect-error — untyped .mjs sibling, deliberately not duplicated in TS
|
|
@@ -61,7 +61,10 @@ import {
|
|
|
61
61
|
type TransportKind,
|
|
62
62
|
} from "./transports/index.js";
|
|
63
63
|
import { readAllTransportMarkers, markerHoldsLiveProcess } from "./tools/registry.js";
|
|
64
|
-
import { decideTokenMapRefresh, bearerHash, TOKEN_HASH_ALGS, moveCursorOffset, planArchive } from "./store.js";
|
|
64
|
+
import { decideTokenMapRefresh, bearerHash, TOKEN_HASH_ALGS, moveCursorOffset, planArchive, withDeliveryLock, DELIVERY_LOCK_STALE_MS } from "./store.js";
|
|
65
|
+
import { pushLogLine, sourceKey as deliverySourceKey } from "./tools/herdr-delivery.js";
|
|
66
|
+
import { testCountKey } from "./test-count-key.js";
|
|
67
|
+
import { SHAPE_FAULTS, VALIDITY_FAULTS, responseText, logText, detailInResponse } from "./auth-faults.js";
|
|
65
68
|
import { queueWriteSchema } from "./tools/queue-write.js";
|
|
66
69
|
import { treeProvenance } from "./tools/tree-provenance.js";
|
|
67
70
|
import { detectSpread } from "./server-spread.js";
|
|
@@ -667,6 +670,38 @@ const PROBES: Probe[] = [
|
|
|
667
670
|
// APPLICABLE and says so with both counts; a landing whose PR announced no window is a problem,
|
|
668
671
|
// which is the violation the control exists to catch and the one a count-based rule would skip
|
|
669
672
|
// green. Asked of the running function, so a server on the pre-fix build answers false.
|
|
673
|
+
// ⟨q-a441f1e1⟩ — one 401 stood for every auth fault, and it read as a broken daemon in all of
|
|
674
|
+
// them. Asked of the classifier's own message builders, which the HTTP path calls: the SHAPE
|
|
675
|
+
// faults must be named distinctly to a caller, and every VALIDITY fault must return ONE
|
|
676
|
+
// byte-identical string while the LOG tells them apart. A build without the split answers false.
|
|
677
|
+
id: "auth-faults-named-by-class",
|
|
678
|
+
since: "0.26.38",
|
|
679
|
+
run: () => {
|
|
680
|
+
// ⛔ THE REAL ENV, NEVER `{}`. qa, gating this PR: with a hard-coded empty env the probe
|
|
681
|
+
// evaluated the code's DEFAULT instead of THIS BUS's behaviour, so a daemon running with
|
|
682
|
+
// AGENT_COORD_401_DETAIL=response — actively distinguishing validity faults to unauthenticated
|
|
683
|
+
// callers — still answered `validityUniform=true`. Capabilities is how a seat interrogates a
|
|
684
|
+
// LIVE daemon; a probe that says "uniform" on a bus that is leaking is worse than no probe,
|
|
685
|
+
// because it is the thing a reader trusts instead of checking.
|
|
686
|
+
const retired = { agentId: "a", at: 0, cause: "replaced" } as const;
|
|
687
|
+
const shape = SHAPE_FAULTS.map((kind) => responseText({ kind }, process.env));
|
|
688
|
+
const validity = VALIDITY_FAULTS.map((kind) => responseText({ kind, retired }, process.env));
|
|
689
|
+
const logs = [...SHAPE_FAULTS, ...VALIDITY_FAULTS].map((kind) => logText({ kind, retired }));
|
|
690
|
+
const shapeDistinct = new Set(shape).size === shape.length;
|
|
691
|
+
const validityUniform = new Set(validity).size === 1;
|
|
692
|
+
const logsDistinct = new Set(logs).size === logs.length;
|
|
693
|
+
const present = shapeDistinct && validityUniform && logsDistinct;
|
|
694
|
+
return {
|
|
695
|
+
present,
|
|
696
|
+
evidence:
|
|
697
|
+
`AGENT_COORD_401_DETAIL=${process.env.AGENT_COORD_401_DETAIL || "unset"} (detail in response=${detailInResponse()}) · ` +
|
|
698
|
+
`shape faults distinct=${shapeDistinct} (${shape.length}) · validity responses uniform=${validityUniform} ` +
|
|
699
|
+
`(${new Set(validity).size} distinct; >1 means THIS BUS is answering an unauthenticated caller's question about a credential) · ` +
|
|
700
|
+
`log lines distinct=${logsDistinct} (${logs.length}) -> present=${present}`,
|
|
701
|
+
};
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
{
|
|
670
705
|
// ⟨q-4b9cdeab⟩ RELEASE A: can this daemon authenticate a HASHED tokens.json, and does it refuse an
|
|
671
706
|
// alg it cannot evaluate? A version label cannot answer either — and the second half is the one
|
|
672
707
|
// that matters, because ignoring an unknown entry drops a seat silently while treating it as
|
|
@@ -738,6 +773,62 @@ const PROBES: Probe[] = [
|
|
|
738
773
|
return { present, evidence: `live pane -> ${live.state} · gone pane -> ${gone.state} · foreign marker -> ${foreign.state} -> present=${present}` };
|
|
739
774
|
},
|
|
740
775
|
},
|
|
776
|
+
{
|
|
777
|
+
// ⟨q-524305dd⟩ — a message is typed into a pane at most once, whichever processes deliver it. The sender's
|
|
778
|
+
// send-time push and the recipient's tail both typed a message while the other was mid-verify; the only guard
|
|
779
|
+
// was a per-process flag. Both now take ONE cross-process lock per (seat, source), on ONE key spelling, and log
|
|
780
|
+
// which path typed. A probe runs synchronously and must not write the bus's lock directory, so it cannot run
|
|
781
|
+
// the exclusion itself — delivery-once.test.mjs does that against a real second process. What it asks here is
|
|
782
|
+
// what a pre-fix build does not have: the shared key, a stale bound above the lock library's floor, and push
|
|
783
|
+
// lines that name their path. A server on the pre-fix build has none of them and answers false.
|
|
784
|
+
// ⟨q-b0d4e45a⟩ — `coord_away` armed a fleet on `checked: 0, measurable: 0, blind: []` while its own
|
|
785
|
+
// population said `inFlight: 2`: every in-flight lane carried a prose cell, so each left the scored
|
|
786
|
+
// subset before it was counted, and `checked` was the divisor. `0 >= 0` armed. So the guard INVERTED at
|
|
787
|
+
// its own limit — `2 of 5` refused while `0 of 2` armed silently.
|
|
788
|
+
// ⛔ ASKED WITH THE CELL THAT ACTUALLY ARMED, AND IT IS `checked: 0` THAT MAKES IT DISCRIMINATE. The
|
|
789
|
+
// incident was `checked 0 · measurable 0 · inFlight 2`: both in-flight lanes left the SCORED subset
|
|
790
|
+
// before being counted, so the old predicate compared `0 >= 0` and said COVERED. The new one divides by
|
|
791
|
+
// the population and refuses. A pre-fix build answers TRUE here; this build answers FALSE.
|
|
792
|
+
// ⚠ A `3 of 5` CELL WOULD BE INERT AND AN EARLIER VERSION OF THIS PROBE SHIPPED ONE (qa caught it):
|
|
793
|
+
// `3 >= checked` was already false before the fix, so old and new agree and the cell proves nothing.
|
|
794
|
+
// Being SHORT of coverage always refused; being short with an EMPTY scored subset is what armed.
|
|
795
|
+
// ⛔ AND THE CELL IS PASSED IN THE SHAPE THE REAL CALLER USES, `checked` INCLUDED: the earlier probe
|
|
796
|
+
// omitted `checked`, which turned `3 >= undefined` into false on BOTH predicates — the discriminating
|
|
797
|
+
// field was the one dropped between verifying the cell and shipping it.
|
|
798
|
+
// ⭐ The IDLE cell is the opposite direction, so the two cannot both pass on one predicate: nothing in
|
|
799
|
+
// flight must still ARM, where the old predicate refused because it compared against a non-zero
|
|
800
|
+
// `checked`. (`inFlight === 0` is documentation rather than arithmetic — `0 >= 0` is already true — so
|
|
801
|
+
// this cell is about the DIVISOR, not about that ternary.)
|
|
802
|
+
id: "coverage-counts-the-in-flight-population",
|
|
803
|
+
since: "0.26.40",
|
|
804
|
+
run: () => {
|
|
805
|
+
const armedSilently = meetsCoverageThreshold({ checked: 0, measurable: 0, inFlight: 2 } as unknown as { measurable: number; inFlight: number });
|
|
806
|
+
const idle = meetsCoverageThreshold({ checked: 5, measurable: 0, inFlight: 0 } as unknown as { measurable: number; inFlight: number });
|
|
807
|
+
const present = armedSilently === false && idle === true;
|
|
808
|
+
return {
|
|
809
|
+
present,
|
|
810
|
+
// ⛔ CANON, filed 2026-09-27 off this very probe: AN EVIDENCE STRING NAMES AN INPUT AND A RESULT
|
|
811
|
+
// THAT WERE OBSERVED — PRINT THE COMPARISON, NEVER DESCRIBE IT. This process runs ONE predicate, so
|
|
812
|
+
// what the pre-fix build "said" is not observable from here and does not belong in the output. The
|
|
813
|
+
// expected values are printed as the required ones; WHY each cell discriminates is in the comment
|
|
814
|
+
// above, where a reader can check it against the diff rather than take a string's word for it.
|
|
815
|
+
evidence: `meetsCoverageThreshold(checked 0 · measurable 0 · inFlight 2) -> ${armedSilently} [required false] · meetsCoverageThreshold(checked 5 · measurable 0 · inFlight 0) -> ${idle} [required true] -> present=${present}`,
|
|
816
|
+
};
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
id: "delivery-lock-shared-by-both-paths",
|
|
821
|
+
since: "0.26.39",
|
|
822
|
+
run: () => {
|
|
823
|
+
const room = deliverySourceKey({ kind: "room", chan: "kit" });
|
|
824
|
+
const dm = deliverySourceKey({ kind: "dm" });
|
|
825
|
+
const lines = [pushLogLine("send-time", "m1", "w1:p1", "delivered"), pushLogLine("tail", "m1", "w1:p1", "delivered")];
|
|
826
|
+
const named = /path=send-time$/.test(lines[0]!) && /path=tail$/.test(lines[1]!);
|
|
827
|
+
const bounded = DELIVERY_LOCK_STALE_MS >= 2_000 && Number.isFinite(DELIVERY_LOCK_STALE_MS);
|
|
828
|
+
const present = room === "room:kit" && dm === "dm" && named && bounded && typeof withDeliveryLock === "function";
|
|
829
|
+
return { present, evidence: `lock key room -> ${room}, dm -> ${dm} · stale ${DELIVERY_LOCK_STALE_MS}ms · push lines name their path: ${named} · withDeliveryLock: ${typeof withDeliveryLock} -> present=${present}` };
|
|
830
|
+
},
|
|
831
|
+
},
|
|
741
832
|
{
|
|
742
833
|
// ⟨q-f44f17a7⟩ — a compaction moves each cursor in ITS OWN unit. adjustCursors subtracted the LINES
|
|
743
834
|
// removed from every cursor file, and a push cursor is a BYTE offset, so each compaction left a
|
|
@@ -779,6 +870,28 @@ const PROBES: Probe[] = [
|
|
|
779
870
|
};
|
|
780
871
|
},
|
|
781
872
|
},
|
|
873
|
+
{
|
|
874
|
+
// ⟨q-f2cf9e0f⟩ (0.26.34) — a passing test is attributed to the file it ran in, by a key relative
|
|
875
|
+
// to the PACKAGE. The old key was `test/${basename}`, identical for every file directly in
|
|
876
|
+
// `test/` and wrong for any below it: when the live conformance suite moved to `test/live/`, its
|
|
877
|
+
// passes landed on a phantom key while its real record entry read zero. Asked of the function
|
|
878
|
+
// check-test-count keys its comparison with, so a server on the pre-fix build answers false.
|
|
879
|
+
id: "test-count-key-is-package-relative",
|
|
880
|
+
since: "0.26.37",
|
|
881
|
+
run: () => {
|
|
882
|
+
const root = "/pkg";
|
|
883
|
+
const nested = testCountKey(root, "/pkg/test/live/x.test.mjs");
|
|
884
|
+
const flat = testCountKey(root, "/pkg/test/y.test.mjs");
|
|
885
|
+
const none = testCountKey(root, undefined);
|
|
886
|
+
const present = nested === "test/live/x.test.mjs" && flat === "test/y.test.mjs" && none === "(unattributed)";
|
|
887
|
+
return {
|
|
888
|
+
present,
|
|
889
|
+
evidence:
|
|
890
|
+
`nested -> ${nested} (the pre-fix key was test/x.test.mjs) · flat -> ${flat} (unchanged, which is why this went unseen) · ` +
|
|
891
|
+
`no file -> ${none} -> present=${present}`,
|
|
892
|
+
};
|
|
893
|
+
},
|
|
894
|
+
},
|
|
782
895
|
{
|
|
783
896
|
// ⟨q-0c5476ec⟩ — a seat's identity is stated once, and the launcher refuses a config that states
|
|
784
897
|
// another seat. Probed through the function coord-seat itself calls, so a build whose launcher
|