Skip to content

feat(proto): org-management comms + spawn arms for agent gateway (RIG-2673) - #628

Merged
mattwilkinsonn merged 1 commit into
mainfrom
compass-server/rig-2724-org-mgmt-proto
Aug 30, 2026
Merged

feat(proto): org-management comms + spawn arms for agent gateway (RIG-2673)#628
mattwilkinsonn merged 1 commit into
mainfrom
compass-server/rig-2724-org-mgmt-proto

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

This PR is part of a stack containing 3 PRs:

  1. main
  2. "feat(proto): org-management comms + spawn arms for agent gateway (RIG-2673)" (this PR)
  3. feat(comms): org-management agent tools — comms adapters, hub dispatch, spawn role/persona (RIG-2673) #630
  4. feat(proto): OpenDM RPC + gateway arm; GROUP_DM retirement; DM/convert fields (RIG-2962) #738

Add the three org-management comms-call arms (create_channel=7,
update_members=8, create_channel_group=9 on CommsCallRequest.call; the
matching results =8/=9/=10 on CommsCallResult.result) and the Manager-
creation role=5/persona=6 fields on SpawnPeerRequest, reusing the existing
comms.proto payload messages verbatim. Regenerated the internal Go
(go/internal/gen) and agent TS (packages/compass-agent/src/gen) lanes.

T1 of the frozen org-management-tools record (RIG-2673, #589); the single
additive proto+regen commit both compass-server and compass-agent consume.

Co-authored-by: Matt Wilkinson matt@rigel.build

@linear-code

linear-code Bot commented Aug 25, 2026

Copy link
Copy Markdown

RIG-2673

RIG-2724

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-server-rig-2724-org.compass-eng-docs.pages.dev

Deployed from compass-server/rig-2724-org-mgmt-proto at c5f84cd.

@rigel-mintaka
rigel-mintaka force-pushed the compass-server/rig-2724-org-mgmt-proto branch from 60728dd to 836c472 Compare August 25, 2026 23:05
rigel-mintaka added a commit that referenced this pull request Aug 25, 2026
…h, spawn role/persona (RIG-2673)

Wires the compass-server half of the org-management agent tools (RIG-2673 T2–T4) on top of the T1 proto arms (#628): the three comms adapters, their hub dispatch, and the spawn role/persona thread-through. Every operation flows through the existing agent→Runner→Server relay under the server-resolved account, so the authz, idempotency, and event fan-out are identical to a human caller's.

### T2 — comms adapters (`go/internal/comms/agent_caller.go`)

`CreateChannelAsAccount`, `UpdateChannelMembersAsAccount`, `CreateChannelGroupAsAccount` — each mirrors `UpdatePinnedBoardAsAccount`: guard empty account → `errNoActor`, then `WithActor` + the shared handler path, returning `resp.Msg`. No home-channel defaulting (create-channel names no channel; update-members carries `channel_id` explicitly; create-group names only an optional parent). Founding membership rides `CreateChannel`'s `expandOwnerMembership`, so a Manager can immediately post to the channel it just made.

### T3 — hub dispatch (`go/internal/runnerhub/hub.go`, `relay_comms.go`)

Extended the `CommsCaller` interface with the three methods and added three `executeCall` cases on the `Pin` pattern, each wrapping the matching `CommsCallResult` oneof. The default-case error string's variant list now includes the three new arms. A tool error renders in-band as `CommsCallError`, never a stream teardown.

### T4 — spawn role/persona thread-through (`go/server/lifecycle.go`)

Dropped the `Persona: ""` / `Role: ""` hardcodes in the `store.CreateAgent` literal for `req.GetPersona()` / `req.GetRole()`. Under the D9 owner-acts model the caller's owner is the authority, so a Manager-creating spawn legitimately carries role+persona set-at-creation; the values are stored via `CreateAgent` (the source of record) and threaded to the Runner from the created store account — an empty-field spawn is byte-identical to today's field-less spawn.

### Tests (red-green)

- T2 pgtests (`org_mgmt_pgtest_test.go`): founder-membership + `ChannelChanged` fan-out, member-add, group-create; each with empty-account → `errNoActor` and an invisible/non-member target → `CodeNotFound` (the D9 collapse a human gets).
- T3 relay tests (`relay_org_mgmt_test.go`): each arm forwards under the bound account with `call_id` round-trip; a create-channel tool error is in-band, not a transport teardown.
- T4 spawn pgtests (`spawn_role_persona_pgtest_test.go`): role and persona each stored + on the Provision wire; empty-field spawn stores + wires empty (wire-level regression); idempotent re-spawn keeps stored values.

Verified: `go build ./...`, `gofmt`, `compass-go:lint` (0 issues), and the affected `internal/comms` / `internal/runnerhub` / `server` suites all green (pgtests via the throwaway-container path).

Refs RIG-2724, RIG-2673.

Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka added a commit that referenced this pull request Aug 25, 2026
…h, spawn role/persona (RIG-2673)

Wires the compass-server half of the org-management agent tools (RIG-2673 T2–T4) on top of the T1 proto arms (#628): the three comms adapters, their hub dispatch, and the spawn role/persona thread-through. Every operation flows through the existing agent→Runner→Server relay under the server-resolved account, so the authz, idempotency, and event fan-out are identical to a human caller's.

### T2 — comms adapters (`go/internal/comms/agent_caller.go`)

`CreateChannelAsAccount`, `UpdateChannelMembersAsAccount`, `CreateChannelGroupAsAccount` — each mirrors `UpdatePinnedBoardAsAccount`: guard empty account → `errNoActor`, then `WithActor` + the shared handler path, returning `resp.Msg`. No home-channel defaulting (create-channel names no channel; update-members carries `channel_id` explicitly; create-group names only an optional parent). Founding membership rides `CreateChannel`'s `expandOwnerMembership`, so a Manager can immediately post to the channel it just made.

### T3 — hub dispatch (`go/internal/runnerhub/hub.go`, `relay_comms.go`)

Extended the `CommsCaller` interface with the three methods and added three `executeCall` cases on the `Pin` pattern, each wrapping the matching `CommsCallResult` oneof. The default-case error string's variant list now includes the three new arms. A tool error renders in-band as `CommsCallError`, never a stream teardown.

### T4 — spawn role/persona thread-through (`go/server/lifecycle.go`)

Dropped the `Persona: ""` / `Role: ""` hardcodes in the `store.CreateAgent` literal for `req.GetPersona()` / `req.GetRole()`. Under the D9 owner-acts model the caller's owner is the authority, so a Manager-creating spawn legitimately carries role+persona set-at-creation; the values are stored via `CreateAgent` (the source of record) and threaded to the Runner from the created store account — an empty-field spawn is byte-identical to today's field-less spawn.

### Tests (red-green)

- T2 pgtests (`org_mgmt_pgtest_test.go`): founder-membership + `ChannelChanged` fan-out, member-add, group-create; each with empty-account → `errNoActor` and an invisible/non-member target → `CodeNotFound` (the D9 collapse a human gets).
- T3 relay tests (`relay_org_mgmt_test.go`): each arm forwards under the bound account with `call_id` round-trip; a create-channel tool error is in-band, not a transport teardown.
- T4 spawn pgtests (`spawn_role_persona_pgtest_test.go`): role and persona each stored + on the Provision wire; empty-field spawn stores + wires empty (wire-level regression); idempotent re-spawn keeps stored values.

Verified: `go build ./...`, `gofmt`, `compass-go:lint` (0 issues), and the affected `internal/comms` / `internal/runnerhub` / `server` suites all green (pgtests via the throwaway-container path).

Refs RIG-2724, RIG-2673.

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka marked this pull request as draft August 26, 2026 03:15
@rigelbuild-bot

Copy link
Copy Markdown
Contributor

Holding this (converted to draft) — Matt ruled RIG-2751 contract-wide: no agent or client UI ever resolves account ids; every request field takes a @handle and the server resolves handle→account_id (the from_handle pattern). The three new arms here (create_channel.member_account_ids, update_members.*_account_ids, create_channel_group) are part of "the entire contract," so landing them id-typed would birth deprecated fields against a fresh ruling.

These arms get re-authored handle-first as part of the RIG-2751 cutover (design record → Matt freeze → one coherent proto change across comms.proto + agent_gateway.proto). #630's handlers rebase onto the reshaped proto and do the server-side handle→id resolution. Un-drafting once RIG-2751 reshapes + rebases them.

Ref: RIG-2751

rigel-mintaka added a commit that referenced this pull request Aug 27, 2026
Design record for the contract-wide id→handle cutover of the compass request surface (RIG-2751), per Matt's Option-A ruling: every request-input account field takes a `@handle`; the server resolves it to an account id exactly as the existing `from_handle` field does; no agent or client UI ever resolves an id; an unresolved handle surfaces as an in-band NOT_FOUND.

## Scope

Field inventory verified complete against the current tree (all 7 proto files grepped): the request-input account fields across `comms.proto` (CreateChannel members, UpdateChannelMembers add/remove/subscribe/unsubscribe, ReparentAgent agent+new-parent, SetChannelPolicy owner, GetRoster vantage, OpenAgentWorkspace, CreateAgent parent) and `agent_gateway.proto` (DespawnPeer), plus the three new `CommsCallRequest` arms held in draft on #628 (re-authored handle-first here). `agents_spawn_peer` already takes a handle — the precedent this mirrors.

Explicit non-goal (a decided boundary, not a reflex flip): response/stored/event fields keep ids — handing a handle back on a response pushes resolution onto the client, the opposite of the ruling. The one exception is surfaced in OQ-5.

## Design-critic pass folded

Red-teamed by `CritiqueHandleCutover`; the architecture (edge-not-store resolution, in-band NOT_FOUND, dual-vantage-by-emptiness, T1-first sequencing) survived. Seven oracle-safety and grounding improvements folded: a per-handler NOT_FOUND remap table (two handlers today leak an unknown-vs-foreign oracle via PermissionDenied); T4's ordering preserves the lifecycle constant-query-shape bar; GetRoster's invisible-vantage posture is now defined, not inherited; the agent ListMessages author-id fence named as the concrete non-goal exception; error names all unresolved handles; a bounded skew window stated; and two load-bearing grounding corrections (the store returns ErrInvalidArgument, not NOT_FOUND, for unknown members).

## Open questions (freeze-gate decisions for Matt)

- **OQ-1** — field naming (taste).
- **OQ-1b** — field numbering: a CONFIRM of DL-186's rename-in-place, not an open fork.
- **OQ-2** — repeated-field NOT_FOUND: a CONFIRM of atomic-failure, error naming all unresolved handles.
- **OQ-3** — GetRoster vantage field name (taste).
- **OQ-4** — compass.proto admin lane: extend the ruling to the 3 admin fields or keep ids (real scope fork).
- **OQ-5** — response handles: ship the DL-NEW-2 additive `author_handle` sibling with this cutover (the agent's ListMessages author-id fence is the identified surface) or defer.
- **OQ-6 (NEW, load-bearing)** — member-resolution visibility scoping: (a) viewer-scoped `AccountsByHandles` [rec — the only option consistent with the oracle posture] vs (b) unscoped; T2's resolver signature depends on this ruling.

## Ledger-impact

DL-NEW-1 (handle-typed request inputs, server-edge resolution, one indistinguishable in-band NOT_FOUND), DL-NEW-2 (responses/stored/events keep ids; display handles only as additive sibling fields). No existing DL row constrains request addressing (DL-094/186/188/191/202 checked; DL-186 rules the rename-in-place field-numbering). Next-free ids assigned at freeze.

Ref: RIG-2751

Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka added a commit that referenced this pull request Aug 27, 2026
Design record for the contract-wide id→handle cutover of the compass request surface (RIG-2751), per Matt's Option-A ruling: every request-input account field takes a `@handle`; the server resolves it to an account id exactly as the existing `from_handle` field does; no agent or client UI ever resolves an id; an unresolved handle surfaces as an in-band NOT_FOUND.

## Scope

Field inventory verified complete against the current tree (all 7 proto files grepped): the request-input account fields across `comms.proto` (CreateChannel members, UpdateChannelMembers add/remove/subscribe/unsubscribe, ReparentAgent agent+new-parent, SetChannelPolicy owner, GetRoster vantage, OpenAgentWorkspace, CreateAgent parent) and `agent_gateway.proto` (DespawnPeer), plus the three new `CommsCallRequest` arms held in draft on #628 (re-authored handle-first here). `agents_spawn_peer` already takes a handle — the precedent this mirrors.

Explicit non-goal (a decided boundary, not a reflex flip): response/stored/event fields keep ids — handing a handle back on a response pushes resolution onto the client, the opposite of the ruling. The one exception is surfaced in OQ-5.

## Design-critic pass folded

Red-teamed by `CritiqueHandleCutover`; the architecture (edge-not-store resolution, in-band NOT_FOUND, dual-vantage-by-emptiness, T1-first sequencing) survived. Seven oracle-safety and grounding improvements folded: a per-handler NOT_FOUND remap table (two handlers today leak an unknown-vs-foreign oracle via PermissionDenied); T4's ordering preserves the lifecycle constant-query-shape bar; GetRoster's invisible-vantage posture is now defined, not inherited; the agent ListMessages author-id fence named as the concrete non-goal exception; error names all unresolved handles; a bounded skew window stated; and two load-bearing grounding corrections (the store returns ErrInvalidArgument, not NOT_FOUND, for unknown members).

## Open questions (freeze-gate decisions for Matt)

- **OQ-1** — field naming (taste).
- **OQ-1b** — field numbering: a CONFIRM of DL-186's rename-in-place, not an open fork.
- **OQ-2** — repeated-field NOT_FOUND: a CONFIRM of atomic-failure, error naming all unresolved handles.
- **OQ-3** — GetRoster vantage field name (taste).
- **OQ-4** — compass.proto admin lane: extend the ruling to the 3 admin fields or keep ids (real scope fork).
- **OQ-5** — response handles: ship the DL-NEW-2 additive `author_handle` sibling with this cutover (the agent's ListMessages author-id fence is the identified surface) or defer.
- **OQ-6 (NEW, load-bearing)** — member-resolution visibility scoping: (a) viewer-scoped `AccountsByHandles` [rec — the only option consistent with the oracle posture] vs (b) unscoped; T2's resolver signature depends on this ruling.

## Ledger-impact

DL-NEW-1 (handle-typed request inputs, server-edge resolution, one indistinguishable in-band NOT_FOUND), DL-NEW-2 (responses/stored/events keep ids; display handles only as additive sibling fields). No existing DL row constrains request addressing (DL-094/186/188/191/202 checked; DL-186 rules the rename-in-place field-numbering). Next-free ids assigned at freeze.

Ref: RIG-2751

Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka added a commit that referenced this pull request Aug 27, 2026
Design record for the contract-wide id→handle cutover of the compass request surface (RIG-2751), per Matt's Option-A ruling: every request-input account field takes a `@handle`; the server resolves it to an account id exactly as the existing `from_handle` field does; no agent or client UI ever resolves an id; an unresolved handle surfaces as an in-band NOT_FOUND.

## Scope

Field inventory verified complete against the current tree (all 7 proto files grepped): the request-input account fields across `comms.proto` (CreateChannel members, UpdateChannelMembers add/remove/subscribe/unsubscribe, ReparentAgent agent+new-parent, SetChannelPolicy owner, GetRoster vantage, OpenAgentWorkspace, CreateAgent parent) and `agent_gateway.proto` (DespawnPeer), plus the three new `CommsCallRequest` arms held in draft on #628 (re-authored handle-first here). `agents_spawn_peer` already takes a handle — the precedent this mirrors.

Explicit non-goal (a decided boundary, not a reflex flip): response/stored/event fields keep ids — handing a handle back on a response pushes resolution onto the client, the opposite of the ruling. The one exception is surfaced in OQ-5.

## Design-critic pass folded

Red-teamed by `CritiqueHandleCutover`; the architecture (edge-not-store resolution, in-band NOT_FOUND, dual-vantage-by-emptiness, T1-first sequencing) survived. Seven oracle-safety and grounding improvements folded: a per-handler NOT_FOUND remap table (two handlers today leak an unknown-vs-foreign oracle via PermissionDenied); T4's ordering preserves the lifecycle constant-query-shape bar; GetRoster's invisible-vantage posture is now defined, not inherited; the agent ListMessages author-id fence named as the concrete non-goal exception; error names all unresolved handles; a bounded skew window stated; and two load-bearing grounding corrections (the store returns ErrInvalidArgument, not NOT_FOUND, for unknown members).

## Open questions (freeze-gate decisions for Matt)

- **OQ-1** — field naming (taste).
- **OQ-1b** — field numbering: a CONFIRM of DL-186's rename-in-place, not an open fork.
- **OQ-2** — repeated-field NOT_FOUND: a CONFIRM of atomic-failure, error naming all unresolved handles.
- **OQ-3** — GetRoster vantage field name (taste).
- **OQ-4** — compass.proto admin lane: extend the ruling to the 3 admin fields or keep ids (real scope fork).
- **OQ-5** — response handles: ship the DL-NEW-2 additive `author_handle` sibling with this cutover (the agent's ListMessages author-id fence is the identified surface) or defer.
- **OQ-6 (NEW, load-bearing)** — member-resolution visibility scoping: (a) viewer-scoped `AccountsByHandles` [rec — the only option consistent with the oracle posture] vs (b) unscoped; T2's resolver signature depends on this ruling.

## Ledger-impact

DL-NEW-1 (handle-typed request inputs, server-edge resolution, one indistinguishable in-band NOT_FOUND), DL-NEW-2 (responses/stored/events keep ids; display handles only as additive sibling fields). No existing DL row constrains request addressing (DL-094/186/188/191/202 checked; DL-186 rules the rename-in-place field-numbering). Next-free ids assigned at freeze.

Ref: RIG-2751

Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka added a commit that referenced this pull request Aug 27, 2026
Design record for the contract-wide id→handle cutover of the compass request surface (RIG-2751), per Matt's Option-A ruling: every request-input account field takes a `@handle`; the server resolves it to an account id exactly as the existing `from_handle` field does; no agent or client UI ever resolves an id; an unresolved handle surfaces as an in-band NOT_FOUND.

## Scope

Field inventory verified complete against the current tree (all 7 proto files grepped): the request-input account fields across `comms.proto` (CreateChannel members, UpdateChannelMembers add/remove/subscribe/unsubscribe, ReparentAgent agent+new-parent, SetChannelPolicy owner, GetRoster vantage, OpenAgentWorkspace, CreateAgent parent) and `agent_gateway.proto` (DespawnPeer), plus the three new `CommsCallRequest` arms held in draft on #628 (re-authored handle-first here). `agents_spawn_peer` already takes a handle — the precedent this mirrors.

Explicit non-goal (a decided boundary, not a reflex flip): response/stored/event fields keep ids — handing a handle back on a response pushes resolution onto the client, the opposite of the ruling. The one exception is surfaced in OQ-5.

## Design-critic pass folded

Red-teamed by `CritiqueHandleCutover`; the architecture (edge-not-store resolution, in-band NOT_FOUND, dual-vantage-by-emptiness, T1-first sequencing) survived. Seven oracle-safety and grounding improvements folded: a per-handler NOT_FOUND remap table (two handlers today leak an unknown-vs-foreign oracle via PermissionDenied); T4's ordering preserves the lifecycle constant-query-shape bar; GetRoster's invisible-vantage posture is now defined, not inherited; the agent ListMessages author-id fence named as the concrete non-goal exception; error names all unresolved handles; a bounded skew window stated; and two load-bearing grounding corrections (the store returns ErrInvalidArgument, not NOT_FOUND, for unknown members).

## Open questions (freeze-gate decisions for Matt)

- **OQ-1** — field naming (taste).
- **OQ-1b** — field numbering: a CONFIRM of DL-186's rename-in-place, not an open fork.
- **OQ-2** — repeated-field NOT_FOUND: a CONFIRM of atomic-failure, error naming all unresolved handles.
- **OQ-3** — GetRoster vantage field name (taste).
- **OQ-4** — compass.proto admin lane: extend the ruling to the 3 admin fields or keep ids (real scope fork).
- **OQ-5** — response handles: ship the DL-NEW-2 additive `author_handle` sibling with this cutover (the agent's ListMessages author-id fence is the identified surface) or defer.
- **OQ-6 (NEW, load-bearing)** — member-resolution visibility scoping: (a) viewer-scoped `AccountsByHandles` [rec — the only option consistent with the oracle posture] vs (b) unscoped; T2's resolver signature depends on this ruling.

## Ledger-impact

DL-NEW-1 (handle-typed request inputs, server-edge resolution, one indistinguishable in-band NOT_FOUND), DL-NEW-2 (responses/stored/events keep ids; display handles only as additive sibling fields). No existing DL row constrains request addressing (DL-094/186/188/191/202 checked; DL-186 rules the rename-in-place field-numbering). Next-free ids assigned at freeze.

Ref: RIG-2751

Co-authored-by: Matt Wilkinson <matt@rigel.build>
mattwilkinsonn added a commit that referenced this pull request Aug 27, 2026
…51) (#645)

* docs(product): design contract-wide id→handle request cutover (RIG-2751)

Design record for the contract-wide id→handle cutover of the compass request surface (RIG-2751), per Matt's Option-A ruling: every request-input account field takes a `@handle`; the server resolves it to an account id exactly as the existing `from_handle` field does; no agent or client UI ever resolves an id; an unresolved handle surfaces as an in-band NOT_FOUND.

## Scope

Field inventory verified complete against the current tree (all 7 proto files grepped): the request-input account fields across `comms.proto` (CreateChannel members, UpdateChannelMembers add/remove/subscribe/unsubscribe, ReparentAgent agent+new-parent, SetChannelPolicy owner, GetRoster vantage, OpenAgentWorkspace, CreateAgent parent) and `agent_gateway.proto` (DespawnPeer), plus the three new `CommsCallRequest` arms held in draft on #628 (re-authored handle-first here). `agents_spawn_peer` already takes a handle — the precedent this mirrors.

Explicit non-goal (a decided boundary, not a reflex flip): response/stored/event fields keep ids — handing a handle back on a response pushes resolution onto the client, the opposite of the ruling. The one exception is surfaced in OQ-5.

## Design-critic pass folded

Red-teamed by `CritiqueHandleCutover`; the architecture (edge-not-store resolution, in-band NOT_FOUND, dual-vantage-by-emptiness, T1-first sequencing) survived. Seven oracle-safety and grounding improvements folded: a per-handler NOT_FOUND remap table (two handlers today leak an unknown-vs-foreign oracle via PermissionDenied); T4's ordering preserves the lifecycle constant-query-shape bar; GetRoster's invisible-vantage posture is now defined, not inherited; the agent ListMessages author-id fence named as the concrete non-goal exception; error names all unresolved handles; a bounded skew window stated; and two load-bearing grounding corrections (the store returns ErrInvalidArgument, not NOT_FOUND, for unknown members).

## Open questions (freeze-gate decisions for Matt)

- **OQ-1** — field naming (taste).
- **OQ-1b** — field numbering: a CONFIRM of DL-186's rename-in-place, not an open fork.
- **OQ-2** — repeated-field NOT_FOUND: a CONFIRM of atomic-failure, error naming all unresolved handles.
- **OQ-3** — GetRoster vantage field name (taste).
- **OQ-4** — compass.proto admin lane: extend the ruling to the 3 admin fields or keep ids (real scope fork).
- **OQ-5** — response handles: ship the DL-NEW-2 additive `author_handle` sibling with this cutover (the agent's ListMessages author-id fence is the identified surface) or defer.
- **OQ-6 (NEW, load-bearing)** — member-resolution visibility scoping: (a) viewer-scoped `AccountsByHandles` [rec — the only option consistent with the oracle posture] vs (b) unscoped; T2's resolver signature depends on this ruling.

## Ledger-impact

DL-NEW-1 (handle-typed request inputs, server-edge resolution, one indistinguishable in-band NOT_FOUND), DL-NEW-2 (responses/stored/events keep ids; display handles only as additive sibling fields). No existing DL row constrains request addressing (DL-094/186/188/191/202 checked; DL-186 rules the rename-in-place field-numbering). Next-free ids assigned at freeze.

Ref: RIG-2751

Co-authored-by: Matt Wilkinson <matt@rigel.build>

* docs(product): fold ratified storage contract + owner-qualified wire into RIG-2751 cutover

Matt ratified the RIG-2751 handles/id contract (2026-08-26, "LGTM, can
start"). Fold the frozen contract into the cutover record, which previously
designed only the wire flip against a flat global handle lookup:

- New §"The storage contract": account_handles table + two partial-unique
  indexes (user/system handles globally unique; agent handles unique per
  owner), owner-qualified resolution (matt/compass-ux, bare = caller's own
  owner), in-place rename + reclaim allowed both tiers (no history/reservation),
  cross-human safety relocated to owner peering (RIG-2796).
- Re-spec the edge resolvers for owner-qualified parsing and re-key
  AgentByHandle / AccountsByHandles onto account_handles.
- New T0 migration task (0002_account_handles.sql + backfill + relax
  accounts.handle global-unique); T2 resolver signature carries callerOwner +
  parsed QualifiedHandle; T4 despawn handle owner-qualified.
- Ledger rows assigned concrete numbers (DL-269 request handle-typed /
  DL-270 responses keep ids / DL-271 owner-namespaced storage; next-free after
  the DL-268 dedup this stack sits on).
- OQ section: ratification preamble (which forks closed), OQ-7 owner-qualifier
  grammar added, OQ-5/OQ-6 DL refs + stale signature corrected.

Ref: RIG-2751

Co-authored-by: Matt Wilkinson <matt@rigel.build>

* docs(product): review fixes — DL-186 line cite + stale global claim + OQ-5 scoping (RIG-2751)

Review round 1 (ReviewPR645disposition): 0 high / 2 medium / 1 low. All fixed:
- medium: DL-186 citation was DECISIONS.md:199 (that's DL-129); DL-186 is at
  :203. Fixed all 3 refs (Global Constraints, Ledger-impact, OQ-1b).
- medium: \u00a7GetRoster called AgentByHandle 'global' in a post-flip frame,
  contradicting the record's own re-key to owner-namespaced. Reworded to
  'owner-namespaced but NOT viewer-scoped', keeping the load-bearing no-viewer-
  scoping point that motivates the post-resolve ListAccounts visibility check.
- low: OQ-5 conflated two response-handle mechanisms under DL-270; scoped them
  (author_handle sibling = DL-270's path; from_handle denorm = control-plane,
  outside DL-270's field wording). OQ-5 stays open for Matt.

Co-authored-by: Matt Wilkinson <matt@rigel.build>

* docs(product): T0 schema in 0001, drop backfill — pre-dogfood no data (RIG-2751)

Matt's ruling on #645: don't backfill (Compass isn't in use yet), collapse the account_handles schema into the single existing 0001_init.sql migration rather than shipping an incremental 0002.

Rewrites T0: account_handles is authored directly into 0001_init.sql with accounts.handle authored without its former global-unique constraint; no incremental migration, no backfill step (no deployed rows exist to migrate). Drops the backfill-parity pgtest assertion from the test cycle and updates the Tasks checklist row.

Refs RIG-2751
Co-authored-by: Matt Wilkinson <matt@rigel.build>

* docs(product): fix T0 handle-population attribution — runtime writers, not a 0001 seed (RIG-2751)

Review finding (#645, low): the T0 rewrite said accounts.handle is populated by 'the 0001_init.sql seed', but 0001_init.sql has no INSERT/seed statements — the handle column is populated at runtime by store.CreateUser/CreateAgent (accounts.go:29,81). Re-attributes to the true runtime writers so an implementing agent doesn't hunt for a nonexistent seed. Design conclusion (column retained for display) is unchanged.

Refs RIG-2751
Co-authored-by: Matt Wilkinson <matt@rigel.build>

* docs(product): correct CreateAgent handle-writer line cite (RIG-2751)

Round-2 review (#645, low): the prior fix cited accounts.go:29,81 for CreateUser/CreateAgent, but :81 is BootstrapAdmin's INSERT — CreateAgent's is at :254. Corrected to accounts.go:29,254 (both verified: CreateUser func at :13/INSERT :29, CreateAgent func at :234/INSERT :254). Substance unchanged; accurate line pointer.

Refs RIG-2751
Co-authored-by: Matt Wilkinson <matt@rigel.build>

* docs(product): fold Matt's OQ-1..7 rulings into handle-cutover record + close OQ section (RIG-2751)

Matt ruled all remaining wire-cutover open questions (RIG-2751 comment
f59001de, 2026-08-27). Fold the rulings into the design record and close the
Open Questions section as Rulings, so the record is ready to freeze on merge.

- OQ-4 EXTEND: admin/ops lane (SpawnAgent / ProvisionAgentWorkspace /
  IssueToken on CompassService) flips to handles too — inventory rows 14-16,
  new task T8. No id-typed request field survives anywhere.
- OQ-5 ship-now: additive Message.author_handle sibling; agent fence renders
  handle-only and drops the id; author_account_id stays on the wire for the UI
  join — new task T9.
- OQ-6 SCOPED: T2 resolver intersects accountVisibleFromWhere; note the
  Matt-directed D9-widening follow-on as its OWN compass-server change (filed
  separately), not folded into this freeze.
- OQ-1/1b/2/3/7 confirmed at documented defaults (names, rename-in-place,
  atomic NOT_FOUND, vantage_handle, '/' owner-qualifier grammar).

markdownlint 0 errors; design-ledger-gate OK (DL-269/270/271 collision-free).

* docs(product): tighten OQ-fold review nits — stale OQ labels + grounding cites (RIG-2751)

Sub-floor review fixes on the OQ-fold record (all doc-only, no design change):
- Drop stale (OQ-5) tag from the non-goal forward-looking sentence (OQ-5 is
  ruled/closed; §6 ships additive author_handle).
- Rewrite T2's 'per the OQ-2 working assumption' → 'per the OQ-2 ruling'.
- Enumerate account_handle (IssueToken row 16) in T1's final-names list.
- T8: split service.go:152 (GetAccount lookup) from :166 (hub.Provision relay)
  so the cite labels each construct correctly.
- T9: tighten Message.author_account_id cite comms.proto:322 -> :332 (the field,
  not the message-block doc-comment).

---------

Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka added a commit that referenced this pull request Aug 28, 2026
…solver, comms edge (RIG-2880)

The handle-addressing cutover: every request-input account field carries a `@handle`, resolved to an account id at the service edge, per `docs/designs/product/compass-handle-addressing-cutover/design.md` (Plan T0-T3, plus the compass-agent T6 tool rewire folded in to keep the tree green).

**T0 — resolution index** (`0001_init.sql`). Adds `account_handles(account_id, handle, owner_user_id NULL)` with two partial-unique indexes: user/system handles globally unique (`owner_user_id IS NULL`), agent handles unique per owner. `accounts.handle` stays as a display column, no longer the resolution key. No backfill (pre-dogfood); store write paths insert the handle row in the same tx as the account.

**T1 — proto flip** (`comms.proto`, `agent_gateway.proto`, `compass.proto`). Every request-input account field renamed to its `@handle` form (`member_handles`, `add/remove/subscribe/unsubscribe_handles`, `agent_handle`, `new_parent_handle`, `parent_handle`, `owner_handle`, `vantage_handle`, `account_handle`), field numbers kept in place (rename-in-place, DL-186). Response, stored, and event account fields stay id-typed (the stable join keys clients hold). All four gen lanes regenerated; gen-fence clean.

**T2 — resolver** (`accounts.go`). `AccountsByHandles` is an owner-qualified batch resolve over `account_handles`: atomic (any miss fails the whole call, naming every unresolved handle in submitted spelling — OQ-2), visibility-scoped (an invisible handle misses exactly like an unknown one — OQ-6), system-excluded. `AgentByHandle` re-keyed to `(owner, handle)`; `UserByHandle` global-tier helper; `QualifiedHandle`/`ParseQualifiedHandle` split on the first `/`.

**T3 — comms edge** (`resolve.go` new, `mapping.go`, `roster.go`). Handles resolve before the store calls, with oracle-safe NOT_FOUND remaps on the authority/visibility legs (DL-269): CreateAgent foreign parent, ReparentAgent `ErrPermissionDenied` merge, OpenAgentWorkspace re-key to the submitted handle, roster vantage visibility-close. A delivery mention `@handle` resolves in the posting author's owner namespace. `memberUpdatesFromWire` merges post-resolution so two spellings of one handle cannot yield two conflicting `MemberUpdate`s.

**T6 (folded) — compass-agent despawn tool** (`lifecycle.ts`). `agents_despawn_peer` takes `agent_handle` (non-blank) and builds `DespawnPeerRequest{agentHandle}`; the description drops "by its agent account id". The roster tool is unchanged (it never set the vantage). Folded into this PR so the whole tree — Go, proto, and the bun lane the proto regen touches — stays green in one atomic change rather than landing a red intermediate.

Admin/ops (T8) and lifecycle despawn (T4) server handlers are mechanically renamed to the handle-typed fields but still consume the value as an id (documented id-passthrough skew); their real resolution lands in the follow-up T4/T8 commits on this stack.

**Verification.** `go vet ./...` and `-tags "pgtest podman unix microvm"` clean, `go build ./...` clean, all test binaries compile, `compass-proto:drift`/`gen-fence` pass, store/comms/delivery pgtests pass against live Postgres, `go test -race` clean; `compass-agent` typecheck + 610 bun tests green; `compass-ui`/`compass-client` typecheck green.

Adds DL-269 (request-input account fields are handle-typed), DL-270 (response/stored/event fields stay id-typed), DL-271 (owner-namespaced `account_handles` resolution) on merge.

Unblocks the remaining cutover lanes: T5 rebase of #628/#630, T8 admin/ops resolution, T9 additive `Message.author_handle`, and the compass-ui T7 sweep.

Spec-impact: none. Refs RIG-2880

Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka added a commit that referenced this pull request Aug 28, 2026
…solver, comms edge (RIG-2880)

The handle-addressing cutover: every request-input account field carries a `@handle`, resolved to an account id at the service edge, per `docs/designs/product/compass-handle-addressing-cutover/design.md` (Plan T0-T3, plus the compass-agent T6 tool rewire folded in to keep the tree green).

**T0 — resolution index** (`0001_init.sql`). Adds `account_handles(account_id, handle, owner_user_id NULL)` with two partial-unique indexes: user/system handles globally unique (`owner_user_id IS NULL`), agent handles unique per owner. `accounts.handle` stays as a display column, no longer the resolution key. No backfill (pre-dogfood); store write paths insert the handle row in the same tx as the account.

**T1 — proto flip** (`comms.proto`, `agent_gateway.proto`, `compass.proto`). Every request-input account field renamed to its `@handle` form (`member_handles`, `add/remove/subscribe/unsubscribe_handles`, `agent_handle`, `new_parent_handle`, `parent_handle`, `owner_handle`, `vantage_handle`, `account_handle`), field numbers kept in place (rename-in-place, DL-186). Response, stored, and event account fields stay id-typed (the stable join keys clients hold). All four gen lanes regenerated; gen-fence clean.

**T2 — resolver** (`accounts.go`). `AccountsByHandles` is an owner-qualified batch resolve over `account_handles`: atomic (any miss fails the whole call, naming every unresolved handle in submitted spelling — OQ-2), visibility-scoped (an invisible handle misses exactly like an unknown one — OQ-6), system-excluded. `AgentByHandle` re-keyed to `(owner, handle)`; `UserByHandle` global-tier helper; `QualifiedHandle`/`ParseQualifiedHandle` split on the first `/`.

**T3 — comms edge** (`resolve.go` new, `mapping.go`, `roster.go`). Handles resolve before the store calls, with oracle-safe NOT_FOUND remaps on the authority/visibility legs (DL-269): CreateAgent foreign parent, ReparentAgent `ErrPermissionDenied` merge, OpenAgentWorkspace re-key to the submitted handle, roster vantage visibility-close. A delivery mention `@handle` resolves in the posting author's owner namespace. `memberUpdatesFromWire` merges post-resolution so two spellings of one handle cannot yield two conflicting `MemberUpdate`s.

**T6 (folded) — compass-agent despawn tool** (`lifecycle.ts`). `agents_despawn_peer` takes `agent_handle` (non-blank) and builds `DespawnPeerRequest{agentHandle}`; the description drops "by its agent account id". The roster tool is unchanged (it never set the vantage). Folded into this PR so the whole tree — Go, proto, and the bun lane the proto regen touches — stays green in one atomic change rather than landing a red intermediate.

Admin/ops (T8) and lifecycle despawn (T4) server handlers are mechanically renamed to the handle-typed fields but still consume the value as an id (documented id-passthrough skew); their real resolution lands in the follow-up T4/T8 commits on this stack.

**Verification.** `go vet ./...` and `-tags "pgtest podman unix microvm"` clean, `go build ./...` clean, all test binaries compile, `compass-proto:drift`/`gen-fence` pass, store/comms/delivery pgtests pass against live Postgres, `go test -race` clean; `compass-agent` typecheck + 610 bun tests green; `compass-ui`/`compass-client` typecheck green.

Adds DL-269 (request-input account fields are handle-typed), DL-270 (response/stored/event fields stay id-typed), DL-271 (owner-namespaced `account_handles` resolution) on merge.

Unblocks the remaining cutover lanes: T5 rebase of #628/#630, T8 admin/ops resolution, T9 additive `Message.author_handle`, and the compass-ui T7 sweep.

Spec-impact: none. Refs RIG-2880

Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka added a commit that referenced this pull request Aug 28, 2026
…solver, comms edge (RIG-2880)

The handle-addressing cutover: every request-input account field carries a `@handle`, resolved to an account id at the service edge, per `docs/designs/product/compass-handle-addressing-cutover/design.md` (Plan T0-T3, plus the compass-agent T6 tool rewire folded in to keep the tree green).

**T0 — resolution index** (`0001_init.sql`). Adds `account_handles(account_id, handle, owner_user_id NULL)` with two partial-unique indexes: user/system handles globally unique (`owner_user_id IS NULL`), agent handles unique per owner. `accounts.handle` stays as a display column, no longer the resolution key. No backfill (pre-dogfood); store write paths insert the handle row in the same tx as the account.

**T1 — proto flip** (`comms.proto`, `agent_gateway.proto`, `compass.proto`). Every request-input account field renamed to its `@handle` form (`member_handles`, `add/remove/subscribe/unsubscribe_handles`, `agent_handle`, `new_parent_handle`, `parent_handle`, `owner_handle`, `vantage_handle`, `account_handle`), field numbers kept in place (rename-in-place, DL-186). Response, stored, and event account fields stay id-typed (the stable join keys clients hold). All four gen lanes regenerated; gen-fence clean.

**T2 — resolver** (`accounts.go`). `AccountsByHandles` is an owner-qualified batch resolve over `account_handles`: atomic (any miss fails the whole call, naming every unresolved handle in submitted spelling — OQ-2), visibility-scoped (an invisible handle misses exactly like an unknown one — OQ-6), system-excluded. `AgentByHandle` re-keyed to `(owner, handle)`; `UserByHandle` global-tier helper; `QualifiedHandle`/`ParseQualifiedHandle` split on the first `/`.

**T3 — comms edge** (`resolve.go` new, `mapping.go`, `roster.go`). Handles resolve before the store calls, with oracle-safe NOT_FOUND remaps on the authority/visibility legs (DL-269): CreateAgent foreign parent, ReparentAgent `ErrPermissionDenied` merge, OpenAgentWorkspace re-key to the submitted handle, roster vantage visibility-close. A delivery mention `@handle` resolves in the posting author's owner namespace. `memberUpdatesFromWire` merges post-resolution so two spellings of one handle cannot yield two conflicting `MemberUpdate`s.

**T6 (folded) — compass-agent despawn tool** (`lifecycle.ts`). `agents_despawn_peer` takes `agent_handle` (non-blank) and builds `DespawnPeerRequest{agentHandle}`; the description drops "by its agent account id". The roster tool is unchanged (it never set the vantage). Folded into this PR so the whole tree — Go, proto, and the bun lane the proto regen touches — stays green in one atomic change rather than landing a red intermediate.

Admin/ops (T8) and lifecycle despawn (T4) server handlers are mechanically renamed to the handle-typed fields but still consume the value as an id (documented id-passthrough skew); their real resolution lands in the follow-up T4/T8 commits on this stack.

**Verification.** `go vet ./...` and `-tags "pgtest podman unix microvm"` clean, `go build ./...` clean, all test binaries compile, `compass-proto:drift`/`gen-fence` pass, store/comms/delivery pgtests pass against live Postgres, `go test -race` clean; `compass-agent` typecheck + 610 bun tests green; `compass-ui`/`compass-client` typecheck green.

Adds DL-269 (request-input account fields are handle-typed), DL-270 (response/stored/event fields stay id-typed), DL-271 (owner-namespaced `account_handles` resolution) on merge.

Unblocks the remaining cutover lanes: T5 rebase of #628/#630, T8 admin/ops resolution, T9 additive `Message.author_handle`, and the compass-ui T7 sweep.

Spec-impact: none. Refs RIG-2880

Co-authored-by: Matt Wilkinson <matt@rigel.build>
mattwilkinsonn added a commit that referenced this pull request Aug 29, 2026
…solver, comms edge (RIG-2880) (#698)

* feat(compass): handle-address request inputs — schema, proto flip, resolver, comms edge (RIG-2880)

The handle-addressing cutover: every request-input account field carries a `@handle`, resolved to an account id at the service edge, per `docs/designs/product/compass-handle-addressing-cutover/design.md` (Plan T0-T3, plus the compass-agent T6 tool rewire folded in to keep the tree green).

**T0 — resolution index** (`0001_init.sql`). Adds `account_handles(account_id, handle, owner_user_id NULL)` with two partial-unique indexes: user/system handles globally unique (`owner_user_id IS NULL`), agent handles unique per owner. `accounts.handle` stays as a display column, no longer the resolution key. No backfill (pre-dogfood); store write paths insert the handle row in the same tx as the account.

**T1 — proto flip** (`comms.proto`, `agent_gateway.proto`, `compass.proto`). Every request-input account field renamed to its `@handle` form (`member_handles`, `add/remove/subscribe/unsubscribe_handles`, `agent_handle`, `new_parent_handle`, `parent_handle`, `owner_handle`, `vantage_handle`, `account_handle`), field numbers kept in place (rename-in-place, DL-186). Response, stored, and event account fields stay id-typed (the stable join keys clients hold). All four gen lanes regenerated; gen-fence clean.

**T2 — resolver** (`accounts.go`). `AccountsByHandles` is an owner-qualified batch resolve over `account_handles`: atomic (any miss fails the whole call, naming every unresolved handle in submitted spelling — OQ-2), visibility-scoped (an invisible handle misses exactly like an unknown one — OQ-6), system-excluded. `AgentByHandle` re-keyed to `(owner, handle)`; `UserByHandle` global-tier helper; `QualifiedHandle`/`ParseQualifiedHandle` split on the first `/`.

**T3 — comms edge** (`resolve.go` new, `mapping.go`, `roster.go`). Handles resolve before the store calls, with oracle-safe NOT_FOUND remaps on the authority/visibility legs (DL-269): CreateAgent foreign parent, ReparentAgent `ErrPermissionDenied` merge, OpenAgentWorkspace re-key to the submitted handle, roster vantage visibility-close. A delivery mention `@handle` resolves in the posting author's owner namespace. `memberUpdatesFromWire` merges post-resolution so two spellings of one handle cannot yield two conflicting `MemberUpdate`s.

**T6 (folded) — compass-agent despawn tool** (`lifecycle.ts`). `agents_despawn_peer` takes `agent_handle` (non-blank) and builds `DespawnPeerRequest{agentHandle}`; the description drops "by its agent account id". The roster tool is unchanged (it never set the vantage). Folded into this PR so the whole tree — Go, proto, and the bun lane the proto regen touches — stays green in one atomic change rather than landing a red intermediate.

Admin/ops (T8) and lifecycle despawn (T4) server handlers are mechanically renamed to the handle-typed fields but still consume the value as an id (documented id-passthrough skew); their real resolution lands in the follow-up T4/T8 commits on this stack.

**Verification.** `go vet ./...` and `-tags "pgtest podman unix microvm"` clean, `go build ./...` clean, all test binaries compile, `compass-proto:drift`/`gen-fence` pass, store/comms/delivery pgtests pass against live Postgres, `go test -race` clean; `compass-agent` typecheck + 610 bun tests green; `compass-ui`/`compass-client` typecheck green.

Adds DL-269 (request-input account fields are handle-typed), DL-270 (response/stored/event fields stay id-typed), DL-271 (owner-namespaced `account_handles` resolution) on merge.

Unblocks the remaining cutover lanes: T5 rebase of #628/#630, T8 admin/ops resolution, T9 additive `Message.author_handle`, and the compass-ui T7 sweep.

Spec-impact: none. Refs RIG-2880

Co-authored-by: Matt Wilkinson <matt@rigel.build>

* fix(compass): close ReparentAgent oracle leak, complete OQ-2 miss naming, add DL rows (RIG-2880)

Review-fix commit on the handle-addressing foundation. Addresses the review of the parent commit (three code findings plus the mandated ledger rows); the low-2 finding (globally-visible user directory) is a pre-existing decision surfaced to Matt, not changed here.

HIGH — ReparentAgent oracle leak (DL-269 violation). A foreign new_parent_handle (one that exists under another owner) fell through the edge to the store, which re-keyed the error to name the AGENT handle, while an unknown parent named the submitted parent handle. The divergence let a caller distinguish "this parent exists under another owner" from "no such handle" — an enumeration probe against another owner's tree. Fixed by mirroring CreateAgent's same-owner edge pre-check in ReparentAgent (go/internal/comms/comms.go): after resolving the new parent, resolve the caller's owner and the parent's owner, and if they differ return the indistinguishable NOT_FOUND naming the submitted new_parent_handle. The store's clause-0 permission-denied catch stays correct for agent-authority; the store clause-1 foreign-parent path is now unreachable via the edge.

MEDIUM — reparent cross-owner tests asserted code only, not message. TestReparentAgentCrossOwnerParentNotFound now captures both the foreign-parent and unknown-parent errors and asserts each names its own submitted parent handle, that the foreign error does NOT name the agent handle, and that the two are byte-identical modulo the caller's own submitted spelling — the oracle invariant, not just the code.

LOW-1 — memberUpdatesFromWire OQ-2 completeness. The four member lists (add/subscribe/unsubscribe/remove) resolved sequentially, so a miss in a later list was masked by an earlier list's miss — the error named only the first failing list. Rewritten to one combined resolveHandles call over all four lists, slicing results back per-list by offset; every unresolved handle across every list is now named in one pass. New TestUpdateChannelMembersNamesMissesAcrossAllLists defends this.

Ledger — adds the three rows the frozen record's §Ledger impact mandates (DL-269 request-input handle-typing + oracle invariant, DL-270 response/stored/event stay id-typed, DL-271 owner-namespaced account_handles resolution), placed in the Storage section. The design PR merged without adding them; per house convention the impl PR fills its allocated numbers.

Verification: go build + go vet (all tags) clean; gofmt + compass-go:lint (0 issues) clean; comms pgtest suite green against live Postgres (reparent oracle fix, one-pass member resolution, and both new tests); design-ledger-gate green (244 rows, anchor resolves).

Co-authored-by: Matt Wilkinson <matt@rigel.build>

* docs(store): note ReparentAgent clause-1 is edge-shadowed on the RPC path (RIG-2880)

Review-fix on #698 (low, non-gating). The re-review noted that comms.ReparentAgent's new edge pre-check makes the store's validateNewParent same-owner ErrPermissionDenied unreachable via the RPC path — a future reader at the store clause might mistake it for dead code. Adds a one-line note that it is edge-shadowed on the RPC path and remains as store-layer defense-in-depth (the store is independently callable and tested).

Co-authored-by: Matt Wilkinson <matt@rigel.build>

* fix(compass): spawn allows cross-owner same-name agents, guards against shadowing a human (RIG-2880)

The per-owner `account_handles` namespace (DL-271) changes the spawn-collision
domain from global to per-owner, which the two `go/server` lifecycle spawn tests
did not yet reflect — they asserted the pre-cutover global-unique behavior and
reddened the `pgtest` CI suite. Matt ruled the intended `SpawnPeer` contract
(2026-08-27): allow cross-owner same-name agents, but keep a defense-in-depth
guard so an agent can never shadow a human or the system sender.

- **Shadow guard** (`lifecycle.go`): `SpawnAsAccount` resolves the bare handle in
  the global user/system index (`UserByHandle`) before `CreateAgent`; a hit is
  the same in-band `already_exists`/`errHandleTaken` a duplicate agent handle
  gets, never revealing the account kind. Storage permits an agent handle to
  overlap a user/system handle (the two partial-unique indexes never contend),
  but resolution resolves the global tier first (OQ-7), so this closes the one
  way a peer could be created onto a human's handle.

- **`resumeOrReject` simplification** (`lifecycle.go`): with the guard excluding
  user/system handles and the per-owner namespace isolating foreign owners, a
  `CreateAgent` `ErrConflict` can only mean a same-owner agent already holds the
  handle. The two former reject branches (non-agent handle, foreign owner) are
  now unreachable and removed; an `AgentByHandle(callerOwner, handle)` miss there
  is an invariant violation (`CodeInternal`), not a routine collapse.

- **Tests** (`lifecycle_pgtest_test.go`): `...DifferentOwnerIsAlreadyExists` is
  rewritten as `...CreatesDistinctPeer` — owner B spawning owner A's handle now
  creates a distinct owner-B peer, A untouched, each owner's handle resolving to
  its own agent. `...CollidesWithUserAccountIsAlreadyExists` keeps its
  `already_exists` assertion, now backed by the guard, with an added
  system-handle sibling pinning the indistinguishable code shape.

- **Fake runner** (`service_placement_pgtest_test.go`): an opt-in FIFO
  container-name override (mirroring the existing `startIDs`), so the cross-owner
  test's two genuinely-distinct spawns get distinct container names instead of
  colliding on the fixture's constant `fakeContainer` (production derives the
  name from the globally-unique account id, so real spawns never collide).

Verification: `go build ./...`, `go vet -tags "pgtest podman unix microvm"`, and
the `go/server`, `go/internal/store`, `go/internal/comms` pgtest suites all green
against live Postgres. Self-driven `review` agent: 0 high / 0 medium / 2 low
(both no-change informational).

Refs RIG-2880

Co-authored-by: Matt Wilkinson <matt@rigel.build>

---------

Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka added a commit that referenced this pull request Aug 29, 2026
…h, spawn role/persona (RIG-2673)

Wires the compass-server half of the org-management agent tools (RIG-2673 T2–T4) on top of the T1 proto arms (#628): the three comms adapters, their hub dispatch, and the spawn role/persona thread-through. Every operation flows through the existing agent→Runner→Server relay under the server-resolved account, so the authz, idempotency, and event fan-out are identical to a human caller's.

### T2 — comms adapters (`go/internal/comms/agent_caller.go`)

`CreateChannelAsAccount`, `UpdateChannelMembersAsAccount`, `CreateChannelGroupAsAccount` — each mirrors `UpdatePinnedBoardAsAccount`: guard empty account → `errNoActor`, then `WithActor` + the shared handler path, returning `resp.Msg`. No home-channel defaulting (create-channel names no channel; update-members carries `channel_id` explicitly; create-group names only an optional parent). Founding membership rides `CreateChannel`'s `expandOwnerMembership`, so a Manager can immediately post to the channel it just made.

### T3 — hub dispatch (`go/internal/runnerhub/hub.go`, `relay_comms.go`)

Extended the `CommsCaller` interface with the three methods and added three `executeCall` cases on the `Pin` pattern, each wrapping the matching `CommsCallResult` oneof. The default-case error string's variant list now includes the three new arms. A tool error renders in-band as `CommsCallError`, never a stream teardown.

### T4 — spawn role/persona thread-through (`go/server/lifecycle.go`)

Dropped the `Persona: ""` / `Role: ""` hardcodes in the `store.CreateAgent` literal for `req.GetPersona()` / `req.GetRole()`. Under the D9 owner-acts model the caller's owner is the authority, so a Manager-creating spawn legitimately carries role+persona set-at-creation; the values are stored via `CreateAgent` (the source of record) and threaded to the Runner from the created store account — an empty-field spawn is byte-identical to today's field-less spawn.

### Tests (red-green)

- T2 pgtests (`org_mgmt_pgtest_test.go`): founder-membership + `ChannelChanged` fan-out, member-add, group-create; each with empty-account → `errNoActor` and an invisible/non-member target → `CodeNotFound` (the D9 collapse a human gets).
- T3 relay tests (`relay_org_mgmt_test.go`): each arm forwards under the bound account with `call_id` round-trip; a create-channel tool error is in-band, not a transport teardown.
- T4 spawn pgtests (`spawn_role_persona_pgtest_test.go`): role and persona each stored + on the Provision wire; empty-field spawn stores + wires empty (wire-level regression); idempotent re-spawn keeps stored values.

Verified: `go build ./...`, `gofmt`, `compass-go:lint` (0 issues), and the affected `internal/comms` / `internal/runnerhub` / `server` suites all green (pgtests via the throwaway-container path).

Refs RIG-2724, RIG-2673.

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka force-pushed the compass-server/rig-2724-org-mgmt-proto branch from 836c472 to 8374738 Compare August 29, 2026 20:36
@rigel-mintaka
rigel-mintaka marked this pull request as ready for review August 29, 2026 20:36
…-2673)

Add the three org-management comms-call arms (create_channel=7,
update_members=8, create_channel_group=9 on CommsCallRequest.call; the
matching results =8/=9/=10 on CommsCallResult.result) and the Manager-
creation role=5/persona=6 fields on SpawnPeerRequest, reusing the existing
comms.proto payload messages verbatim. Regenerated the internal Go
(go/internal/gen) and agent TS (packages/compass-agent/src/gen) lanes.

T1 of the frozen org-management-tools record (RIG-2673, #589); the single
additive proto+regen commit both compass-server and compass-agent consume.

Co-authored-by: Matt Wilkinson <matt@rigel.build>

jj-hooks: autofixes for compass-server/rig-2724-org-mgmt-proto

regen-resolve
rigel-mintaka added a commit that referenced this pull request Aug 30, 2026
…h, spawn role/persona (RIG-2673)

Wires the compass-server half of the org-management agent tools (RIG-2673 T2–T4) on top of the T1 proto arms (#628): the three comms adapters, their hub dispatch, and the spawn role/persona thread-through. Every operation flows through the existing agent→Runner→Server relay under the server-resolved account, so the authz, idempotency, and event fan-out are identical to a human caller's.

### T2 — comms adapters (`go/internal/comms/agent_caller.go`)

`CreateChannelAsAccount`, `UpdateChannelMembersAsAccount`, `CreateChannelGroupAsAccount` — each mirrors `UpdatePinnedBoardAsAccount`: guard empty account → `errNoActor`, then `WithActor` + the shared handler path, returning `resp.Msg`. No home-channel defaulting (create-channel names no channel; update-members carries `channel_id` explicitly; create-group names only an optional parent). Founding membership rides `CreateChannel`'s `expandOwnerMembership`, so a Manager can immediately post to the channel it just made.

### T3 — hub dispatch (`go/internal/runnerhub/hub.go`, `relay_comms.go`)

Extended the `CommsCaller` interface with the three methods and added three `executeCall` cases on the `Pin` pattern, each wrapping the matching `CommsCallResult` oneof. The default-case error string's variant list now includes the three new arms. A tool error renders in-band as `CommsCallError`, never a stream teardown.

### T4 — spawn role/persona thread-through (`go/server/lifecycle.go`)

Dropped the `Persona: ""` / `Role: ""` hardcodes in the `store.CreateAgent` literal for `req.GetPersona()` / `req.GetRole()`. Under the D9 owner-acts model the caller's owner is the authority, so a Manager-creating spawn legitimately carries role+persona set-at-creation; the values are stored via `CreateAgent` (the source of record) and threaded to the Runner from the created store account — an empty-field spawn is byte-identical to today's field-less spawn.

### Tests (red-green)

- T2 pgtests (`org_mgmt_pgtest_test.go`): founder-membership + `ChannelChanged` fan-out, member-add, group-create; each with empty-account → `errNoActor` and an invisible/non-member target → `CodeNotFound` (the D9 collapse a human gets).
- T3 relay tests (`relay_org_mgmt_test.go`): each arm forwards under the bound account with `call_id` round-trip; a create-channel tool error is in-band, not a transport teardown.
- T4 spawn pgtests (`spawn_role_persona_pgtest_test.go`): role and persona each stored + on the Provision wire; empty-field spawn stores + wires empty (wire-level regression); idempotent re-spawn keeps stored values.

Verified: `go build ./...`, `gofmt`, `compass-go:lint` (0 issues), and the affected `internal/comms` / `internal/runnerhub` / `server` suites all green (pgtests via the throwaway-container path).

Refs RIG-2724, RIG-2673.

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka force-pushed the compass-server/rig-2724-org-mgmt-proto branch from 8374738 to c5f84cd Compare August 30, 2026 15:00
@mattwilkinsonn
mattwilkinsonn merged commit dd4f291 into main Aug 30, 2026
14 checks passed
mattwilkinsonn added a commit that referenced this pull request Aug 30, 2026
…h, spawn role/persona (RIG-2673) (#630)

* feat(proto): org-management comms + spawn arms for agent gateway (RIG-2673)

Add the three org-management comms-call arms (create_channel=7,
update_members=8, create_channel_group=9 on CommsCallRequest.call; the
matching results =8/=9/=10 on CommsCallResult.result) and the Manager-
creation role=5/persona=6 fields on SpawnPeerRequest, reusing the existing
comms.proto payload messages verbatim. Regenerated the internal Go
(go/internal/gen) and agent TS (packages/compass-agent/src/gen) lanes.

T1 of the frozen org-management-tools record (RIG-2673, #589); the single
additive proto+regen commit both compass-server and compass-agent consume.

Co-authored-by: Matt Wilkinson <matt@rigel.build>

jj-hooks: autofixes for compass-server/rig-2724-org-mgmt-proto

regen-resolve

* feat(comms): org-management agent tools — comms adapters, hub dispatch, spawn role/persona (RIG-2673)

Wires the compass-server half of the org-management agent tools (RIG-2673 T2–T4) on top of the T1 proto arms (#628): the three comms adapters, their hub dispatch, and the spawn role/persona thread-through. Every operation flows through the existing agent→Runner→Server relay under the server-resolved account, so the authz, idempotency, and event fan-out are identical to a human caller's.

### T2 — comms adapters (`go/internal/comms/agent_caller.go`)

`CreateChannelAsAccount`, `UpdateChannelMembersAsAccount`, `CreateChannelGroupAsAccount` — each mirrors `UpdatePinnedBoardAsAccount`: guard empty account → `errNoActor`, then `WithActor` + the shared handler path, returning `resp.Msg`. No home-channel defaulting (create-channel names no channel; update-members carries `channel_id` explicitly; create-group names only an optional parent). Founding membership rides `CreateChannel`'s `expandOwnerMembership`, so a Manager can immediately post to the channel it just made.

### T3 — hub dispatch (`go/internal/runnerhub/hub.go`, `relay_comms.go`)

Extended the `CommsCaller` interface with the three methods and added three `executeCall` cases on the `Pin` pattern, each wrapping the matching `CommsCallResult` oneof. The default-case error string's variant list now includes the three new arms. A tool error renders in-band as `CommsCallError`, never a stream teardown.

### T4 — spawn role/persona thread-through (`go/server/lifecycle.go`)

Dropped the `Persona: ""` / `Role: ""` hardcodes in the `store.CreateAgent` literal for `req.GetPersona()` / `req.GetRole()`. Under the D9 owner-acts model the caller's owner is the authority, so a Manager-creating spawn legitimately carries role+persona set-at-creation; the values are stored via `CreateAgent` (the source of record) and threaded to the Runner from the created store account — an empty-field spawn is byte-identical to today's field-less spawn.

### Tests (red-green)

- T2 pgtests (`org_mgmt_pgtest_test.go`): founder-membership + `ChannelChanged` fan-out, member-add, group-create; each with empty-account → `errNoActor` and an invisible/non-member target → `CodeNotFound` (the D9 collapse a human gets).
- T3 relay tests (`relay_org_mgmt_test.go`): each arm forwards under the bound account with `call_id` round-trip; a create-channel tool error is in-band, not a transport teardown.
- T4 spawn pgtests (`spawn_role_persona_pgtest_test.go`): role and persona each stored + on the Provision wire; empty-field spawn stores + wires empty (wire-level regression); idempotent re-spawn keeps stored values.

Verified: `go build ./...`, `gofmt`, `compass-go:lint` (0 issues), and the affected `internal/comms` / `internal/runnerhub` / `server` suites all green (pgtests via the throwaway-container path).

Refs RIG-2724, RIG-2673.

Co-authored-by: Matt Wilkinson <matt@rigel.build>

---------

Co-authored-by: Matt Wilkinson <matt@rigel.build>
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.

3 participants