Skip to content

core: the merged project-board endpoint is denied when authz is on, and a run binding can put another Project's card on the board #1713

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: ae58f625925b42809f06801630075c4f
Branch: dev/2-project-board-authz
Base: 554d939
At: 2026-10-29T15:25:40Z

Written-by: FlyBot Lead

Two defects in the read model merged as #1703 (core/src/host/project-board.js, live main 554d939e64ad7833e66f1c7e72a3fddded14b763). Both were found by FlyBot Product reading that merge; both reproduce on that commit.

A. project-board is not in the authorisation allowlist, so a signed-in deployment cannot call it.

  • core/src/host/authz.js:60-72 TASK_ENDPOINTS carries projects, project-create, project-read, project-bind, project-of (#1691's five) but not #1703's new read endpoint. The switch is wired (channel.js:674), the store is wired (store.js:440) and the module installs (project-board.js:56) — only the allowlist line is missing.
  • With authzEnforced() true (anyone has signed in), the request falls to the default deny at authz.js:406-414: project-board → bot/not-found: Unknown endpoint project-board, while project-read / project-of / project-bind / tasks all answer ALLOW under the same actor. Reproduced through the real channel with a signed-in member session: project-read answers ok {project:…}, project-board fails with that message.
  • Four reasons this is an omission rather than a design: the three wiring points are all present; the sibling endpoints read the same team state and are allowed; the refusal code is bot/not-found (reads like a typo'd endpoint name) rather than bot/forbidden / a shared-state refusal; and core/test/project-board.test.js:124 ("the project-board endpoint serves the same rows") only runs on the authz-off fixture, where authSessions is empty — a false-green surface for exactly this path.
  • Docs leak the same way: docs/subsystems/core-host.md:209 lists only the five.

B. A run binding can pull another Project's card onto this board.

  • core/src/host/project-board.js:43 asks only "is the card's own board this Project"; when the card's board belongs to a different Project it falls into the runProject branch and is reported as "brought by the run". The comment above it states the intent — a run carries only cards that have no Project of their own (board === null) — the code does not implement that.
  • Reproduced with a group card bound to Project 1 and a run bound to Project 2 holding that card: Project 1's board lists the card with Project 2's runId, and Project 2's board lists a card that belongs to Project 1.

Expected

  • A signed-in actor that may read the Project record may read its board: project-board allowed for the same actors as projects / project-read, and a test that exercises it with a non-empty authSessions so the allowlist path is actually covered (the shape core/test/workflow-store.test.js:120-127 uses for workflow-save).
  • A card appears on exactly one Project board — the Project of its own board when it has one. source:'workflow' and runId/nodeId only when the card has no Project of its own and the run's Project is this one; a card whose own board belongs to another Project never appears here.
  • docs/subsystems/core-host.md:209 lists all six Project endpoints.

Scope — core/src/host/authz.js (one allowlist line), core/src/host/project-board.js (the source/run-binding condition, about four lines), core/test/project-board.test.js (the authz-on regression: an existing test asserts row shape with deepEqual, so keep those assertions in step), docs/subsystems/core-host.md:209, and the regenerated patches/ds-bot/** mirrors for the changed core files.

Acceptance

  1. Red first on 554d939e: with authzEnforced() and a signed-in actor, project-board is refused (assert the refusal code, not just that a call fails); and the cross-Project fixture shows Project 1's board carrying Project 2's run id while Project 2's board carries Project 1's card.
  2. After the fix: the same actor is allowed; Project 1's board lists only its own rows; a card with no Project of its own still appears with source:'workflow' and its runId (existing cases 1 and 4 depend on this and must keep passing).
  3. TZ=UTC ARCH_DOCS_BASE=<live main> npm run verify exit 0 with counts reported; node scripts/ds-bot-patches.mjs check green.

Duplicate check (before opening): searched open and closed issues for project-board, authz, allowlist, project board → #1703 (the merge this fixes, closed), #1710 (Stage 2.3 board view + role-gated actions, enhancement, different scope: UI and role decisions), #1701 (our client projection). No issue covers the allowlist line or the cross-Project leak; not a duplicate. Not folded into #1710 because that is an enhancement owned elsewhere and this is a shipped-defect fix.

Activity

  1. HugoXK commented on Oct 29, 2026

    @HugoXK
    ContributorAuthor

    Release

    Written-by: FlyBot Lead
    Released: ae58f625925b42809f06801630075c4f
    Reason: merged via #1716 (merge_commit f8e4943)
    At: 2026-10-29T16:20:33Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions