Skip to content

feat(server): agent issue-state write path (RelayBoardCall + transition executor) - #206

Open
seal-agent wants to merge 3 commits into
compass-repo-1830-board-call-protofrom
compass-server-sea-1728-board-write-path
Open

feat(server): agent issue-state write path (RelayBoardCall + transition executor)#206
seal-agent wants to merge 3 commits into
compass-repo-1830-board-call-protofrom
compass-server-sea-1728-board-write-path

Conversation

@seal-agent

@seal-agent seal-agent commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

This PR is part of a stack containing 3 PRs:

  1. main
  2. feat(proto): forge-carrier family reconciled to canonical compass.v1 (SEA-1731) #149
  3. feat(proto): BoardCall* agent write family + RelayBoardCall relay (SEA-1830) #201
  4. "feat(server): agent issue-state write path (RelayBoardCall + transition executor)" (this PR)

Implements the agent write leg of the Compass board (SEA-1728 Part 5, T3-a of the frozen compass-agent-primary-lifecycle design). An agent calls Board(SetIssueState) on the AgentGateway; the Runner forwards it to the Server as RelayBoardCall(session_id, call); the Server resolves the session to the caller agent account and runs the one shared compare-and-transition on the issue's canonical lifecycle state.

What lands

  • Hub.RelayBoardCall (relay_board.go) + the BoardCaller seam — the board sibling of the lifecycle/comms relay legs, sharing their trust model exactly: fail-closed guard order (nil-caller CodeUnavailable before resolution; unbound session CodeNotFound, never a stale/admin account; delegate under the resolved caller account). A tool-level failure is returned in-band as BoardCallError; only a resolution miss / no-caller is a Connect error.
  • The single transition executor (server/board.go, boardService.SetIssueState) every state producer will share — the frozen compare-and-transition (compass-issue-model/design.md:513-521): under a serialized transition lock, read current truth, reject an UNSPECIFIED target, no-op-no-publish on same-state (ARCHIVED included), else commit to Postgres, read back, record+publish on the projection, and (nil-safe) mirror outbound (ARCHIVED elided).
  • TransitionSource (kind agent|tracker|auto + optional actor) fully defined so the PR-B poll driver and PR-C tracker ingestion add producers without changing the executor signature. Only SourceAgent is exercised here; the caller AccountID is recorded for attribution, no scope rejection ships (single-trust-domain MVP).
  • IssueProjection.RecordAndPublish — a state-only record+publish (the step-5 tail of PublishIssueUpdate without the forge upsert, which would demand forge fields and could not carry the state column).
  • Wired post-construction at sinks.go (hub.SetBoardCaller), the nil-safe hub-sink idiom; the outbound tracker mirror is left nil this PR (PR-C wires the real forge-tracker write).

Not in this PR

The forge/tracker poll driver (PR-B, SEA-1810), tracker-status ingestion / reverse mapping / echo-suppression / the real outbound mirror (PR-C), the UpdateIssueState CompassService handler (frozen record drops it), any proto or compass-ui change.

Tests

Red-first security tests (relay_board_test.go) mirroring the lifecycle leg (unbound->NotFound reaching the caller 0 times; nil-caller->Unavailable before resolution; resolved-account delegation; tool error in-band; call_id echo; unset-oneof invalid_argument). Default-lane executor tests (board_test.go) over a fake store + real projection: commit+publish on a real transition, reject UNSPECIFIED, unknown-issue NotFound, no-op-no-publish, nil-safe mirror (fires / nil no-op / ARCHIVED elided). Build+vet+scoped tests+gating lint green.

Spec-impact: none. Refs SEA-1728

Co-authored-by: Matt Wilkinson matt@sealedsecurity.com

…on executor)

Implements the agent write leg of the Compass board (SEA-1728 Part 5, T3-a of the frozen `compass-agent-primary-lifecycle` design). An agent calls `Board(SetIssueState)` on the AgentGateway; the Runner forwards it to the Server as `RelayBoardCall(session_id, call)`; the Server resolves the session to the caller agent account and runs the one shared compare-and-transition on the issue's canonical lifecycle state.

### What lands

- `Hub.RelayBoardCall` (`relay_board.go`) + the `BoardCaller` seam — the board sibling of the lifecycle/comms relay legs, sharing their trust model exactly: fail-closed guard order (nil-caller `CodeUnavailable` before resolution; unbound session `CodeNotFound`, never a stale/admin account; delegate under the **resolved** caller account). A tool-level failure is returned in-band as `BoardCallError`; only a resolution miss / no-caller is a Connect error.
- The single transition executor (`server/board.go`, `boardService.SetIssueState`) every state producer will share — the frozen compare-and-transition (`compass-issue-model/design.md:513-521`): under a serialized transition lock, read current truth, reject an `UNSPECIFIED` target, no-op-no-publish on same-state (ARCHIVED included), else commit to Postgres, read back, record+publish on the projection, and (nil-safe) mirror outbound (ARCHIVED elided).
- `TransitionSource` (`kind` agent|tracker|auto + optional actor) fully defined so the PR-B poll driver and PR-C tracker ingestion add producers without changing the executor signature. Only `SourceAgent` is exercised here; the caller AccountID is recorded for attribution, no scope rejection ships (single-trust-domain MVP).
- `IssueProjection.RecordAndPublish` — a state-only record+publish (the step-5 tail of `PublishIssueUpdate` without the forge upsert, which would demand forge fields and could not carry the state column).
- Wired post-construction at `sinks.go` (`hub.SetBoardCaller`), the nil-safe hub-sink idiom; the outbound tracker mirror is left nil this PR (PR-C wires the real forge-tracker write).

### Not in this PR

The forge/tracker poll driver (PR-B, SEA-1810), tracker-status ingestion / reverse mapping / echo-suppression / the real outbound mirror (PR-C), the `UpdateIssueState` `CompassService` handler (frozen record drops it), any proto or compass-ui change.

### Tests

Red-first security tests (`relay_board_test.go`) mirroring the lifecycle leg (unbound->NotFound reaching the caller 0 times; nil-caller->Unavailable before resolution; resolved-account delegation; tool error in-band; call_id echo; unset-oneof invalid_argument). Default-lane executor tests (`board_test.go`) over a fake store + real projection: commit+publish on a real transition, reject UNSPECIFIED, unknown-issue NotFound, no-op-no-publish, nil-safe mirror (fires / nil no-op / ARCHIVED elided). Build+vet+scoped tests+gating lint green.

Spec-impact: none. Refs SEA-1728

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

SEA-1728

@seal-agent seal-agent closed this Aug 7, 2026
@seal-agent seal-agent reopened this Aug 7, 2026
seal-agent and others added 2 commits August 7, 2026 01:45
Addresses the sole review's one medium finding on the agent write path: three error-injection fields on the test fakes were declared but never exercised, leaving the executor's commit-failure, post-commit read-back-failure, and mirror-failure branches uncovered.

Adds three default-lane cases (red-first) that drive each branch and assert the mapped Connect code plus the fan-out invariant: a commit failure maps to CodeInternal with no publish; a read-back failure maps to CodeInternal after the commit landed (setWrites==1) but before any publish, pinning the commit-then-read-back ordering; a mirror failure surfaces as CodeInternal after the publish already fired, pinning the publish-before-mirror ordering. Adds a `readBackErr` fake field gated on setWrites>0 to isolate the post-commit read.

Also folds the two low findings as PR-C deferral comments at the mirror call site (error-after-commit ordering; the mirror must move off transitionMu once it does real network I/O). No production behavior change.

Spec-impact: none. Refs SEA-1728

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
Round-2 review flagged fakeIssueStore.getErr as the last dead injection field: read in GetIssue but assigned by no test. The prior commit wired the post-commit read-error case through the new readBackErr field, orphaning getErr. Coverage is unaffected by its removal — the pre-commit ErrNotFound arm is pinned by TestSetIssueStateUnknownIssueIsNotFound and transitionStoreError's default arm by the readBackErr test (both GetIssue call sites route through the same mapper).

Spec-impact: none. Refs SEA-1728

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant