Compare commits

...
2 changed files with 138 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
# Row 193a build: the viewer level on R1's access file
Morgana (Opus 5.5), 2026-10-20. Gitea #1691, parent row 193, design `agents/morgana/work/queue-193/DESIGN.md`
r5 (e2d161e2e on morgana/row193, approved at r4 by Duckworth, duckworth-0014). Go: sage-1373 item 3. Base:
`refactor` at aa1bd209e. Lands after FED R1 and is rebased onto R1's landing.
## What it is
Design 4.2: R1 refuses any grant level but `collaborator`. Row 193a widens that check to `collaborator` or
`viewer` and refuses anything else. `covers` needs no change: FED 3.5 already compares the grant's level
with the one asked for, so a viewer grant matches `{}` and `{level: 'viewer'}`, and fails
`{level: 'collaborator'}`.
## Why the candidate holds only a test now
R1 (`packages/business/src/access.mjs`) has no candidate yet. Sage ruled no stub on refactor (sage-1373
item 3). So this branch carries:
- `packages/business/tests/access-levels.test.mjs`: the row 193a tests, written against R1's interface as
FED r4.5 (acd5f1f42) sections 3.5 and 7 give it.
- The source change, below, to apply on R1's candidate at rebase.
Until R1 lands, the test file fails with `ERR_MODULE_NOT_FOUND` for `../src/access.mjs`. That is expected.
## The source change for the rebase
In R1's level check, the allowed set becomes `["collaborator", "viewer"]`. It is one line wherever R1 keeps
the level rule. Its refusal message must name the key path `grants[<i>].level`, as R1's own rule does.
## R1's interface (confirmed, gizmoduck-0029)
FED r4.5 names `covers(grant, {business, project, instance}, now, {level} = {})` exactly. Gizmoduck
confirmed the validator before R1's manifest went to Duckworth:
- `checkAccess(doc, {providers, peers, businesses, ssoSubjects})` is pure, returns the checked document
frozen, and throws a `BusinessError` (exit 2) naming the key path on any failure;
- `businesses` maps a business id to `{human, instances}`;
- `ssoSubjects` is a required array of `{provider, sub}` from `console.sso.subjects`; the test passes `[]`;
- a grant with no `level` refuses; an expired grant passes `checkAccess` and fails `covers`.
Every call into the validator goes through one `check()` helper at the top of the test file, so a different
name or shape changes one place at rebase. The `covers` calls follow FED exactly.
A grant with no `level` refuses (`undefined` in the refused list), which R1 also does.
## Evidence
Run on 2026-10-20 in the row193a worktree with `node --test --test-reporter=tap
tests/access-levels.test.mjs` from `packages/business`.
- Base (no access.mjs): fails to load, `ERR_MODULE_NOT_FOUND`.
- Throwaway reference: an `access.mjs` with only the level rule, a declared-business rule and FED's
`covers`, placed in `src/`, run, then deleted, never committed. Result: pass 4, fail 0.
Mutants on that reference, each failing exactly one test:
| Mutant | Failing test |
|---|---|
| levels back to collaborator only | 1, "a grant's level is collaborator or viewer" |
| `owner` added to the levels | 2, "any other level refuses with the key path named" |
| `covers` ignores the asked level | 3, "covers tells a viewer grant from a collaborator grant" |
| `covers` passes any grant when collaborator is asked | 3 |
At rebase, the same four mutants run again on R1's real code before the row goes to review.
@@ -0,0 +1,74 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { BusinessError } from "../src/errors.mjs";
import * as access from "../src/access.mjs";
// Row 193a (#1691): the viewer level. R1 (federation design r4.5, section 7) owns access.mjs and its
// tests; this file pins only what row 193 adds: a grant's level is collaborator or viewer, and covers()
// tells the two apart. Every call into R1 goes through check() below, so a change to R1's loader
// interface edits one place.
const CONTEXT = {
providers: ["authentik"],
peers: ["factory"],
businesses: { "creative-hub": { human: "melanie", instances: ["researcher", "writer"] } },
ssoSubjects: [],
};
const check = (doc) => access.checkAccess(doc, CONTEXT);
const PROJECT = "0b6f4a52-6f1e-4c7a-9a0e-3f1d2c4b5a69";
const NOW = new Date("2026-10-20T19:30:00Z");
function file(level) {
const grant = { id: "g-1", user: "jason", business: "creative-hub", projects: ["*"], instances: ["researcher"] };
if (level !== undefined) grant.level = level;
return {
accessVersion: 1,
revision: 1,
users: [{ id: "jason", home: "local", subjects: [{ provider: "authentik", sub: "s-1" }] }],
grants: [grant],
};
}
function refuses(fn, pattern) {
assert.throws(fn, (error) => {
assert.ok(error instanceof BusinessError, `expected BusinessError, got ${error}`);
assert.equal(error.exitCode, 2);
assert.match(error.message, pattern);
return true;
});
}
test("a grant's level is collaborator or viewer", () => {
for (const level of ["collaborator", "viewer"]) {
assert.doesNotThrow(() => check(file(level)), level);
}
});
test("any other level refuses with the key path named", () => {
for (const level of ["owner", "admin", "member", "Viewer", "viewer ", "", null, 1, ["viewer"], undefined]) {
refuses(() => check(file(level)), /grants\[0\]\.level/);
}
});
test("covers tells a viewer grant from a collaborator grant", () => {
const target = { business: "creative-hub", project: PROJECT, instance: "researcher" };
const viewer = file("viewer").grants[0];
const collaborator = file("collaborator").grants[0];
// Share reads ask for any level (design 9); create, open, send and share ask for collaborator.
assert.equal(access.covers(viewer, target, NOW, {}), true);
assert.equal(access.covers(viewer, target, NOW, { level: "viewer" }), true);
assert.equal(access.covers(viewer, target, NOW, { level: "collaborator" }), false);
assert.equal(access.covers(collaborator, target, NOW, {}), true);
assert.equal(access.covers(collaborator, target, NOW, { level: "collaborator" }), true);
assert.equal(access.covers(collaborator, target, NOW, { level: "viewer" }), false);
});
test("a viewer grant still needs its business, project, instance and expiry to cover", () => {
const viewer = { ...file("viewer").grants[0], projects: [PROJECT], expires: "2026-10-21T00:00:00Z" };
const target = { business: "creative-hub", project: PROJECT, instance: "researcher" };
assert.equal(access.covers(viewer, target, NOW, {}), true);
assert.equal(access.covers(viewer, { ...target, business: "other" }, NOW, {}), false);
assert.equal(access.covers(viewer, { ...target, project: "7c1e2d3f-0a9b-4c8d-8e7f-6a5b4c3d2e1f" }, NOW, {}), false);
assert.equal(access.covers(viewer, { ...target, instance: "writer" }, NOW, {}), false);
assert.equal(access.covers(viewer, target, new Date("2026-10-21T00:00:01Z"), {}), false);
});