fleetd #669 Unit C: COLLABORATOR role and authorization row #699

Closed
agent wants to merge 0 commits from worker/669-1b786a-1 into main
Member

Implements Unit C of fleetd #669: adds Role.COLLABORATOR and its Authz grant, on top of Unit A/B which landed as f0ff252.

What this does

  • Role.COLLABORATOR: a config-declared, human-opened tab, never spawned.
  • Principal.collaborator(name, terminal, pid) / isCollaborator() / describe() arm.
  • isSpawnedMember() stays WORKER || ARCHITECT (unchanged) — a collaborator must not be enrolled in the presence map as an available member.
  • Authz.permits: new 4-argument form taking a Predicate<String> knownLeadOrCollaborator classifier, consulted only for a collaborator's SEND. Granted: READ, METRICS, SEND (target-limited), REPLY/ASK (via existing ownership check). Denied: SPAWN, STOP, DRAIN, HANDOVER, ANSWER, COORD_SEND, COORD_READ, TASK_READ (deliberate — ticket ids are a sequential counter with no owner check).
  • Authz.NO_KNOWN_LEAD_OR_COLLABORATOR: the shared always-false classifier both production gates pass today, since the terminal-to-tab registry doesn't exist until a later unit.
  • FleetMcp.denyFor and the new FleetApp.permitsFor (a test seam mirroring FleetMcp#denyFor/#deny) both call the 4-argument permits explicitly with that shared constant.
  • fleet_whoami reports role: "collaborator" + the configured name under a collaborator key + sessionId, with no leader key (branch added before the existing lead fallback, which would otherwise have produced a self-contradicting answer).

A same-day correction to my own brief, applied

A ticket comment on #669 (posted while this unit was in flight) corrected the original brief: do not delete the 3-argument Authz.permits overload. Deleting it would have forced 47 mechanical edits across AuthzTest/CallerResolverTest for no safety gain, since a fail-closed default (delegating to the classifier-bearing form with NO_KNOWN_LEAD_OR_COLLABORATOR) is already safe. Implemented as corrected: the 3-arg overload is kept, fails closed, and both production gates still call the 4-arg form explicitly. The 47 pre-existing call sites are untouched (verified: git diff origin/main -- src/test/java/dev/ltms/fleet/auth/CallerResolverTest.java is empty). A new test pins the overload's fail-closed default.

Mutation proof (acceptance criterion 2)

Removed the classifier conjunct from the SEND arm (collaborator could then send anywhere). Result: exactly 3 tests go red, one per the three places this is proven (the Authz unit test, FleetMcpAuthzTest through denyFor, FleetAppAuthTest through the REST permitsFor seam), no cascade. Restored, confirmed byte-clean against a backup via diff, re-ran green.

Build

mvn clean install: BUILD SUCCESS, Tests run: 1974, Failures: 0, Errors: 0, Skipped: 0 (baseline at f0ff252 was 1964; the +10 are the new collaborator tests). Cross-checked independently from target/surefire-reports/*.xml after rm -rf: 173 report files, aggregate tests=1974 failures=0 errors=0.

Out of scope (per the brief, untouched)

CallerResolver, LeadTabScanner, HerdrRouter, FleetConfig, MemberRole, fleetd.example.yaml. The role still cannot be reached in production after this unit — that's expected; Unit D wires the resolver, Unit E fixes deliverability.

Shape-sweep finding (reported, not fixed, per the brief)

The audit-skip asymmetry the brief flagged is real and measured: FleetMcp.denyFor skips the audit "allowed" entry for READ/TASK_READ only (FleetMcp.java:693), while FleetApp.allow skips READ/METRICS/TASK_READ (FleetApp.java:294-295). One added fact: grep -n "Action.METRICS" src/main/java/dev/ltms/fleet/mcp/FleetMcp.java returns nothing — no MCP tool ever passes Action.METRICS to denyFor (it's REST-only, scraped via /metrics), so today's asymmetry has no live effect, but the invariant ("these reads would drown the trail") is still stated once and enforced with two different lists.

Implements Unit C of fleetd #669: adds `Role.COLLABORATOR` and its `Authz` grant, on top of Unit A/B which landed as `f0ff252`. ## What this does - `Role.COLLABORATOR`: a config-declared, human-opened tab, never spawned. - `Principal.collaborator(name, terminal, pid)` / `isCollaborator()` / `describe()` arm. - `isSpawnedMember()` stays `WORKER || ARCHITECT` (unchanged) — a collaborator must not be enrolled in the presence map as an available member. - `Authz.permits`: new 4-argument form taking a `Predicate<String> knownLeadOrCollaborator` classifier, consulted only for a collaborator's `SEND`. Granted: `READ`, `METRICS`, `SEND` (target-limited), `REPLY`/`ASK` (via existing ownership check). Denied: `SPAWN`, `STOP`, `DRAIN`, `HANDOVER`, `ANSWER`, `COORD_SEND`, `COORD_READ`, `TASK_READ` (deliberate — ticket ids are a sequential counter with no owner check). - `Authz.NO_KNOWN_LEAD_OR_COLLABORATOR`: the shared always-false classifier both production gates pass today, since the terminal-to-tab registry doesn't exist until a later unit. - `FleetMcp.denyFor` and the new `FleetApp.permitsFor` (a test seam mirroring `FleetMcp#denyFor`/`#deny`) both call the 4-argument `permits` explicitly with that shared constant. - `fleet_whoami` reports `role: "collaborator"` + the configured name under a `collaborator` key + `sessionId`, with **no** `leader` key (branch added before the existing lead fallback, which would otherwise have produced a self-contradicting answer). ### A same-day correction to my own brief, applied A ticket comment on #669 (posted while this unit was in flight) corrected the original brief: **do not delete the 3-argument `Authz.permits` overload.** Deleting it would have forced 47 mechanical edits across `AuthzTest`/`CallerResolverTest` for no safety gain, since a fail-closed default (delegating to the classifier-bearing form with `NO_KNOWN_LEAD_OR_COLLABORATOR`) is already safe. Implemented as corrected: the 3-arg overload is kept, fails closed, and both production gates still call the 4-arg form explicitly. The 47 pre-existing call sites are untouched (verified: `git diff origin/main -- src/test/java/dev/ltms/fleet/auth/CallerResolverTest.java` is empty). A new test pins the overload's fail-closed default. ## Mutation proof (acceptance criterion 2) Removed the classifier conjunct from the `SEND` arm (collaborator could then send anywhere). Result: exactly 3 tests go red, one per the three places this is proven (the `Authz` unit test, `FleetMcpAuthzTest` through `denyFor`, `FleetAppAuthTest` through the REST `permitsFor` seam), no cascade. Restored, confirmed byte-clean against a backup via `diff`, re-ran green. ## Build `mvn clean install`: **BUILD SUCCESS, Tests run: 1974, Failures: 0, Errors: 0, Skipped: 0** (baseline at `f0ff252` was 1964; the +10 are the new collaborator tests). Cross-checked independently from `target/surefire-reports/*.xml` after `rm -rf`: 173 report files, aggregate tests=1974 failures=0 errors=0. ## Out of scope (per the brief, untouched) `CallerResolver`, `LeadTabScanner`, `HerdrRouter`, `FleetConfig`, `MemberRole`, `fleetd.example.yaml`. The role still cannot be reached in production after this unit — that's expected; Unit D wires the resolver, Unit E fixes deliverability. ## Shape-sweep finding (reported, not fixed, per the brief) The audit-skip asymmetry the brief flagged is real and measured: `FleetMcp.denyFor` skips the audit "allowed" entry for `READ`/`TASK_READ` only (`FleetMcp.java:693`), while `FleetApp.allow` skips `READ`/`METRICS`/`TASK_READ` (`FleetApp.java:294-295`). One added fact: `grep -n "Action.METRICS" src/main/java/dev/ltms/fleet/mcp/FleetMcp.java` returns nothing — no MCP tool ever passes `Action.METRICS` to `denyFor` (it's REST-only, scraped via `/metrics`), so today's asymmetry has no live effect, but the invariant ("these reads would drown the trail") is still stated once and enforced with two different lists.
agent added 1 commit 2026-10-04 00:25:21 +02:00
fleetd #669 Unit C: add the COLLABORATOR role and its authorization row
CI / shell-tests (pull_request) Failing after 7s
CI / contract (pull_request) Successful in 53s
CI / build (pull_request) Failing after 1m48s
bb29b001e4
Adds Role.COLLABORATOR, Principal.collaborator(), and the Authz.permits grant:
READ/METRICS open, REPLY/ASK via ownership, SEND limited to a configured lead
or collaborator via a new classifier parameter, everything else denied.
isSpawnedMember() stays WORKER || ARCHITECT. fleet_whoami reports role and
collaborator name with no leader key.

Per a same-day ticket correction, the existing 3-argument Authz.permits is
kept (fail-closed default via the new NO_KNOWN_LEAD_OR_COLLABORATOR
classifier) rather than deleted, so the 47 pre-existing call sites in
AuthzTest/CallerResolverTest are untouched; both production gates
(FleetMcp#denyFor, FleetApp#allow via the new permitsFor seam) call the
4-argument form explicitly with the shared constant.

Mutation-verified: removing the classifier conjunct from the SEND arm kills
exactly 3 tests (AuthzTest, FleetMcpAuthzTest, FleetAppAuthTest), one per
gate, no cascade.

Full mvn clean install at this commit: 1974 tests, 0 failures (baseline at
f0ff252 was 1964; +10 are the new collaborator-matrix tests). Independently
counted from target/surefire-reports/*.xml after rm -rf: 173 report files,
aggregate tests=1974 failures=0 errors=0.
ltms closed this pull request 2026-10-04 00:34:23 +02:00
Some checks are pending
CI / shell-tests (pull_request) Failing after 7s
CI / contract (pull_request) Successful in 53s
CI / build (pull_request) Failing after 1m48s

Pull request closed

Sign in to join this conversation.