Skip to content

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

Merged
mattwilkinsonn merged 8 commits into
mainfrom
compass-server/rig-2751-handle-cutover-design
Aug 27, 2026
Merged

docs(product): design contract-wide id→handle request cutover (RIG-2751)#645
mattwilkinsonn merged 8 commits into
mainfrom
compass-server/rig-2751-handle-cutover-design

Conversation

@rigel-mintaka

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

Copy link
Copy Markdown
Contributor

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.

This PR now carries the full ratified contract. The second commit folds in the storage/format contract Matt ratified 2026-08-26 ("LGTM, can start") after the RIG-2751 negotiation, which the original record (first commit) predated.

The ratified contract (folded, second commit)

  • Storage — a dedicated account_handles(account_id, handle, owner_user_id NULL) table with two partial-unique indexes: user/system handles globally unique (WHERE owner_user_id IS NULL), agent handles unique per owner (WHERE owner_user_id IS NOT NULL). Handle ownership moves off the single global-unique accounts.handle column.
  • Owner-qualified wire — agents are addressed owner/handle (matt/compass-ux); a bare handle is a user/system handle or the caller's own agent. Resolution parses the qualifier at the service edge, resolving in the matching namespace.
  • Rename/reclaim — in-place UPDATE for both tiers; reclaim allowed for both (no history, tombstone, or reservation). A user rename has zero cascade to agents (they key on the owner's stable account_id). Cross-human reclaim safety is the owner-peering authorization edge (RIG-2796), not handle reservation.
  • New T0 task0002_account_handles.sql migration + backfill + relax accounts.handle global-unique. T2's resolver carries callerOwner + parsed QualifiedHandle; AgentByHandle/AccountsByHandles re-key onto account_handles.

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)

The 2026-08-26 ratification CLOSED the storage/wire/rename forks (now the frozen contract above). The remaining OQs are the wire-cutover forks it did not decide:

  • 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-270 additive author_handle sibling with this cutover (the agent's ListMessages author-id fence is the identified surface) or defer.
  • OQ-6 (load-bearing) — member-resolution visibility scoping: (a) viewer-scoped [rec — the only option consistent with the oracle posture] vs (b) unscoped; T2's resolver signature depends on this ruling.
  • OQ-7 (NEW — from the storage contract) — owner-qualifier grammar: / separator, no nesting, bare-handle collision precedence (user/system wins). Matt confirms the grammar.

Ledger-impact

Adds three rows (next-free at authoring, confirm at freeze): DL-269 (handle-typed request inputs, server-edge resolution, one indistinguishable in-band NOT_FOUND), DL-270 (responses/stored/events keep ids; display handles only as additive sibling fields), DL-271 (owner-namespaced account_handles storage + rename/reclaim policy). No existing DL row constrains request addressing (DL-094/186/188/191/202 checked; DL-186 rules the rename-in-place field-numbering).

Stacked on #664 (DL-264 dedup), which clears the duplicate-id gate failure this record's row numbering depends on.

Ref: RIG-2751

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

@linear-code

linear-code Bot commented Aug 26, 2026

Copy link
Copy Markdown

RIG-2751

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-server-rig-2751-hand.compass-eng-docs.pages.dev

Deployed from compass-server/rig-2751-handle-cutover-design at 101d4dd.

Changed pages:

@rigel-mintaka
rigel-mintaka force-pushed the compass-server/rig-2751-handle-cutover-design branch from 40da6b8 to df32e04 Compare August 27, 2026 02:20
@rigel-mintaka
rigel-mintaka changed the base branch from main to compass-server/dedup-dl264-collision August 27, 2026 02:20
@rigel-mintaka
rigel-mintaka force-pushed the compass-server/rig-2751-handle-cutover-design branch from df32e04 to 3edf871 Compare August 27, 2026 02:23
Base automatically changed from compass-server/dedup-dl264-collision to main August 27, 2026 02:53
@rigel-mintaka
rigel-mintaka force-pushed the compass-server/rig-2751-handle-cutover-design branch from 7059a7b to ec87cb3 Compare August 27, 2026 03:01
rigel-mintaka added a commit that referenced this pull request Aug 27, 2026
… (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>
@rigel-mintaka
rigel-mintaka force-pushed the compass-server/rig-2751-handle-cutover-design branch from ec87cb3 to 780933f Compare August 27, 2026 18:04
rigel-mintaka added a commit that referenced this pull request Aug 27, 2026
…, 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>
rigel-mintaka added a commit that referenced this pull request Aug 27, 2026
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>
rigel-mintaka and others added 7 commits August 27, 2026 15:37
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>
…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>
… 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>
… (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>
…, 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>
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>
… + 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).
@rigel-mintaka
rigel-mintaka force-pushed the compass-server/rig-2751-handle-cutover-design branch from 8f8de1a to 27dc90f Compare August 27, 2026 19:39
…ing 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).
@mattwilkinsonn
mattwilkinsonn merged commit 6edbfd5 into main Aug 27, 2026
13 checks passed
@mattwilkinsonn
mattwilkinsonn deleted the compass-server/rig-2751-handle-cutover-design branch August 27, 2026 21:07
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