Skip to content

feat(comms): UpdatePinnedBoard handler + pin relay arm (SEA-1723) - #165

Merged
mattwilkinsonn merged 2 commits into
mainfrom
compass-comms-1723-t6-pinned-board
Aug 6, 2026
Merged

feat(comms): UpdatePinnedBoard handler + pin relay arm (SEA-1723)#165
mattwilkinsonn merged 2 commits into
mainfrom
compass-comms-1723-t6-pinned-board

Conversation

@seal-agent

Copy link
Copy Markdown
Contributor

What

Manager-comms substrate T6 (handler half, SEA-1723): the UpdatePinnedBoard handler + the agent-initiated pin relay arm, over the already-merged channel_pins store (#152). Design record section T6-handler (design.md:594-637).

Stacked on #161 (T4, compass-comms-1722-t4-channel-policy) — base is the T4 branch, not main. The board authz reads Channel.Policy.PostPolicy / OwnerAccountID, which T4 adds. Merge #161 first.

Changes

  • comms.UpdatePinnedBoard resolves the channel (GetChannel), authorizes at the edge via mayMutateBoard (post_policy: any member on OPEN, owner-only on OWNER_ONLY), applies the op via applyBoardOp (pin / CAS-repoint → PinMessage; unpin → UnpinMessage; unset oneof → CodeInvalidArgument), re-reads, and emits ChannelChanged carrying the updated board.
  • No-oracle authz: a non-member OR a non-owner on an OWNER_ONLY channel collapses to the same in-band CodeNotFound, mirroring the store's PostMessage OWNER_ONLY enforcement (store/messages.go:79-81) so the board leaks no existence signal.
  • pinnedEntriesToWire maps store-native []PinnedEntry → wire, position order preserved (channelToWire does not project the board; the event and response ship from the same board-carrying wire).
  • UpdatePinnedBoardAsAccount (WithActor + shared handler path), mirroring PostAsAccount.
  • runnerhub: executeCall gains the pin arm (CommsCallRequest_PinUpdatePinnedBoardAsAccount, wraps CommsCallResult_Pin); CommsCaller iface + fakeCommsCaller extended. Fail-closed default untouched.
  • store: exported GetChannel(ctx, id) over the existing unexported getChannel — the id-addressed read (members + policy) the edge authz needs.

Tests (red-first, 9 pgtest handler cases)

Watched fail on a disabled mayMutateBoard guard (if false && … → both authz tests got nil error, want not_found; guard restored, green). Cases: pin of a message from another channel → in-band CodeNotFound; repoint swaps atomically (old gone / new present / position preserved); stale CAS → CodeAlreadyExists; cap+1 → CodeFailedPrecondition; non-owner on OWNER_ONLY and non-member both → no-oracle CodeNotFound (owner positive path asserted); unpin removes the entry; unset op → CodeInvalidArgument; ChannelChanged carries the board. The store FOR-UPDATE two-concurrent-cap-edge race is covered at the store layer (TestPinMessageConcurrentCapEdge, #152).

Verification

go build ./..., go vet, CI-exact golangci-lint run --config .golangci.yml ./... all clean (0 issues); internal/comms (28.2s), internal/runnerhub (17.6s), internal/store (85.5s, migrations 0009→0013 contiguous) pgtest suites green under -race.

Merge-fold note (for the collector)

T6 wires the pin relay arm; #163 (T2) wires roster + set_status in a separate workspace. The CommsCaller iface, fakeCommsCaller, and executeCall switch each gain additive entries in both (T6's comms executors in agent_caller.go, T2's in roster.go — disjoint), so whichever lands second gets a clean 3-spot union. T6 leaves executeCall's fail-closed default at its original "post/list" text (its base predates #163); at union, take #163's reworded default and append pin so it names all five recognized variants (post/list/roster/set_status/pin) — cosmetic, behavior unchanged.

Refs SEA-1723

@linear-code

linear-code Bot commented Aug 5, 2026

Copy link
Copy Markdown

SEA-1723

seal-agent added a commit that referenced this pull request Aug 5, 2026
Review-fix on T6 (PR #165), applying Matt's in-transaction ruling plus grounded
auto-fixes from the mandatory adversarial review.

Board-mutation authz TOCTOU (Matt-ruled: close it in-tx). UpdatePinnedBoard
decided authz on a non-transactional GetChannel snapshot at the handler edge,
then called the store pin op — whose transaction re-checked nothing but the
channels-row lock and message membership. A membership revocation or policy flip
committing between the edge read and the store tx let a just-removed member or a
now-unauthorized non-owner still mutate the board.

Authz now rides the store's board transaction. lockChannelForPins folds the
post_policy/owner read into its existing FOR UPDATE SELECT (no extra
round-trip), and a shared requireBoardMutator enforces the same two-gate
no-oracle check PostMessage uses — the actor must be a member, and on OWNER_ONLY
the owner — under the row lock, before the pin/unpin write. So the who-may-act
decision is serialized against a concurrent membership/policy change. UnpinMessage
gains the actor parameter (PinMessage already carried it for pinned_by
attribution and now reuses it as the authz principal); the handler's redundant
edge gate and the now-dead mayMutateBoard helper are removed, leaving one
authoritative in-tx authz site. The handler godoc's false "races nothing it must
serialize" claim is corrected, and a stale messages.go citation fixed.

Tests: TestUpdatePinnedBoardMembershipRevokedMidFlightIsNotFound and
TestUpdatePinnedBoardOwnerOnlyNonOwnerInTxIsNotFound pin the in-tx membership and
post_policy gates (both verified red-first by neutering requireBoardMutator);
TestUpdatePinnedBoardNonOwnerMemberOnOpenSucceeds pins the OPEN half (any member
may mutate), verified red-first against an owner-only narrowing.

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
seal-agent added a commit that referenced this pull request Aug 5, 2026
Review-fix on T6 (PR #165), applying Matt's in-transaction ruling plus grounded
auto-fixes from the mandatory adversarial review.

Board-mutation authz TOCTOU (Matt-ruled: close it in-tx). UpdatePinnedBoard
decided authz on a non-transactional GetChannel snapshot at the handler edge,
then called the store pin op — whose transaction re-checked nothing but the
channels-row lock and message membership. A membership revocation or policy flip
committing between the edge read and the store tx let a just-removed member or a
now-unauthorized non-owner still mutate the board.

Authz now rides the store's board transaction. lockChannelForPins folds the
post_policy/owner read into its existing FOR UPDATE SELECT (no extra
round-trip), and a shared requireBoardMutator enforces the same two-gate
no-oracle check PostMessage uses — the actor must be a member, and on OWNER_ONLY
the owner — under the row lock, before the pin/unpin write. So the who-may-act
decision is serialized against a concurrent membership/policy change. UnpinMessage
gains the actor parameter (PinMessage already carried it for pinned_by
attribution and now reuses it as the authz principal); the handler's redundant
edge gate and the now-dead mayMutateBoard helper are removed, leaving one
authoritative in-tx authz site. The handler godoc's false "races nothing it must
serialize" claim is corrected, and a stale messages.go citation fixed.

Tests: TestUpdatePinnedBoardMembershipRevokedMidFlightIsNotFound and
TestUpdatePinnedBoardOwnerOnlyNonOwnerInTxIsNotFound pin the in-tx membership and
post_policy gates (both verified red-first by neutering requireBoardMutator);
TestUpdatePinnedBoardNonOwnerMemberOnOpenSucceeds pins the OPEN half (any member
may mutate), verified red-first against an owner-only narrowing.

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
@seal-agent
seal-agent force-pushed the compass-comms-1723-t6-pinned-board branch 2 times, most recently from 4567c9c to bced164 Compare August 5, 2026 21:14
seal-agent added a commit that referenced this pull request Aug 5, 2026
Review-fix on T6 (PR #165), applying Matt's in-transaction ruling plus grounded
auto-fixes from the mandatory adversarial review.

Board-mutation authz TOCTOU (Matt-ruled: close it in-tx). UpdatePinnedBoard
decided authz on a non-transactional GetChannel snapshot at the handler edge,
then called the store pin op — whose transaction re-checked nothing but the
channels-row lock and message membership. A membership revocation or policy flip
committing between the edge read and the store tx let a just-removed member or a
now-unauthorized non-owner still mutate the board.

Authz now rides the store's board transaction. lockChannelForPins folds the
post_policy/owner read into its existing FOR UPDATE SELECT (no extra
round-trip), and a shared requireBoardMutator enforces the same two-gate
no-oracle check PostMessage uses — the actor must be a member, and on OWNER_ONLY
the owner — under the row lock, before the pin/unpin write. So the who-may-act
decision is serialized against a concurrent membership/policy change. UnpinMessage
gains the actor parameter (PinMessage already carried it for pinned_by
attribution and now reuses it as the authz principal); the handler's redundant
edge gate and the now-dead mayMutateBoard helper are removed, leaving one
authoritative in-tx authz site. The handler godoc's false "races nothing it must
serialize" claim is corrected, and a stale messages.go citation fixed.

Tests: TestUpdatePinnedBoardMembershipRevokedMidFlightIsNotFound and
TestUpdatePinnedBoardOwnerOnlyNonOwnerInTxIsNotFound pin the in-tx membership and
post_policy gates (both verified red-first by neutering requireBoardMutator);
TestUpdatePinnedBoardNonOwnerMemberOnOpenSucceeds pins the OPEN half (any member
may mutate), verified red-first against an owner-only narrowing.

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
@seal-agent
seal-agent force-pushed the compass-comms-1722-t4-channel-policy branch from 4074868 to bfccd5f Compare August 5, 2026 21:14
Base automatically changed from compass-comms-1722-t4-channel-policy to main August 6, 2026 00:28
seal-agent added a commit that referenced this pull request Aug 6, 2026
Review-fix on T6 (PR #165), applying Matt's in-transaction ruling plus grounded
auto-fixes from the mandatory adversarial review.

Board-mutation authz TOCTOU (Matt-ruled: close it in-tx). UpdatePinnedBoard
decided authz on a non-transactional GetChannel snapshot at the handler edge,
then called the store pin op — whose transaction re-checked nothing but the
channels-row lock and message membership. A membership revocation or policy flip
committing between the edge read and the store tx let a just-removed member or a
now-unauthorized non-owner still mutate the board.

Authz now rides the store's board transaction. lockChannelForPins folds the
post_policy/owner read into its existing FOR UPDATE SELECT (no extra
round-trip), and a shared requireBoardMutator enforces the same two-gate
no-oracle check PostMessage uses — the actor must be a member, and on OWNER_ONLY
the owner — under the row lock, before the pin/unpin write. So the who-may-act
decision is serialized against a concurrent membership/policy change. UnpinMessage
gains the actor parameter (PinMessage already carried it for pinned_by
attribution and now reuses it as the authz principal); the handler's redundant
edge gate and the now-dead mayMutateBoard helper are removed, leaving one
authoritative in-tx authz site. The handler godoc's false "races nothing it must
serialize" claim is corrected, and a stale messages.go citation fixed.

Tests: TestUpdatePinnedBoardMembershipRevokedMidFlightIsNotFound and
TestUpdatePinnedBoardOwnerOnlyNonOwnerInTxIsNotFound pin the in-tx membership and
post_policy gates (both verified red-first by neutering requireBoardMutator);
TestUpdatePinnedBoardNonOwnerMemberOnOpenSucceeds pins the OPEN half (any member
may mutate), verified red-first against an owner-only narrowing.

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
@seal-agent
seal-agent force-pushed the compass-comms-1723-t6-pinned-board branch from bced164 to fab620a Compare August 6, 2026 00:42
seal-agent and others added 2 commits August 5, 2026 21:07
Manager-comms substrate T6 (handler half). Replaces the T1 UpdatePinnedBoard
CodeUnimplemented stub with the real handler and wires the agent-initiated pin
relay arm, over the already-merged channel_pins store (#152).

- comms: UpdatePinnedBoard resolves the channel (GetChannel), authorizes at the
  edge via mayMutateBoard (post_policy: any member on OPEN, owner-only on
  OWNER_ONLY), applies the op via applyBoardOp (pin/CAS-repoint via PinMessage,
  unpin via UnpinMessage; unset oneof -> CodeInvalidArgument), then re-reads and
  emits ChannelChanged carrying the updated board. A non-member OR a non-owner on
  an OWNER_ONLY channel collapses to the SAME in-band CodeNotFound — the no-oracle
  not-found/forbidden merge, mirroring the store's PostMessage OWNER_ONLY
  enforcement (store/messages.go:79-81) so the board leaks no existence signal.
- comms: pinnedEntriesToWire maps store-native []PinnedEntry -> wire, preserving
  position order (channelToWire does not project the board, so the event and the
  response are published from the same board-carrying wire).
- comms: UpdatePinnedBoardAsAccount (WithActor + shared handler path), the
  agent-initiated leg mirroring PostAsAccount.
- runnerhub: executeCall gains the pin arm (CommsCallRequest_Pin ->
  UpdatePinnedBoardAsAccount, wraps CommsCallResult_Pin); CommsCaller interface +
  fakeCommsCaller extended. The fail-closed default is untouched.
- store: exported GetChannel(ctx, id) over the existing unexported getChannel —
  the id-addressed read (members + policy) the edge authz needs.

Red-first (9 pgtest handler cases, watched fail on a disabled mayMutateBoard
guard): pin of a message from another channel -> in-band CodeNotFound; repoint
swaps atomically (old gone / new present / position preserved); stale CAS ->
CodeAlreadyExists; cap+1 -> CodeFailedPrecondition; non-owner on OWNER_ONLY and
non-member both -> no-oracle CodeNotFound (owner positive path asserted);
unpin removes the entry; unset op -> CodeInvalidArgument; ChannelChanged carries
the board. The store FOR-UPDATE two-concurrent-cap-edge race is covered at the
store layer (TestPinMessageConcurrentCapEdge).

Refs SEA-1723

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
Review-fix on T6 (PR #165), applying Matt's in-transaction ruling plus grounded
auto-fixes from the mandatory adversarial review.

Board-mutation authz TOCTOU (Matt-ruled: close it in-tx). UpdatePinnedBoard
decided authz on a non-transactional GetChannel snapshot at the handler edge,
then called the store pin op — whose transaction re-checked nothing but the
channels-row lock and message membership. A membership revocation or policy flip
committing between the edge read and the store tx let a just-removed member or a
now-unauthorized non-owner still mutate the board.

Authz now rides the store's board transaction. lockChannelForPins folds the
post_policy/owner read into its existing FOR UPDATE SELECT (no extra
round-trip), and a shared requireBoardMutator enforces the same two-gate
no-oracle check PostMessage uses — the actor must be a member, and on OWNER_ONLY
the owner — under the row lock, before the pin/unpin write. So the who-may-act
decision is serialized against a concurrent membership/policy change. UnpinMessage
gains the actor parameter (PinMessage already carried it for pinned_by
attribution and now reuses it as the authz principal); the handler's redundant
edge gate and the now-dead mayMutateBoard helper are removed, leaving one
authoritative in-tx authz site. The handler godoc's false "races nothing it must
serialize" claim is corrected, and a stale messages.go citation fixed.

Tests: TestUpdatePinnedBoardMembershipRevokedMidFlightIsNotFound and
TestUpdatePinnedBoardOwnerOnlyNonOwnerInTxIsNotFound pin the in-tx membership and
post_policy gates (both verified red-first by neutering requireBoardMutator);
TestUpdatePinnedBoardNonOwnerMemberOnOpenSucceeds pins the OPEN half (any member
may mutate), verified red-first against an owner-only narrowing.

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
@seal-agent
seal-agent force-pushed the compass-comms-1723-t6-pinned-board branch from fab620a to 5666fb2 Compare August 6, 2026 01:11
@mattwilkinsonn
mattwilkinsonn merged commit fff0d0e into main Aug 6, 2026
1 check passed
@mattwilkinsonn
mattwilkinsonn deleted the compass-comms-1723-t6-pinned-board branch August 6, 2026 01:28
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.

2 participants