diff --git a/docs/designs/DECISIONS.md b/docs/designs/DECISIONS.md index cc2544f3..1196119a 100644 --- a/docs/designs/DECISIONS.md +++ b/docs/designs/DECISIONS.md @@ -101,6 +101,9 @@ check enforces the mechanical half. Full rationale: | DL-266 | The forge delivery cursor (`delivered_revision`) advances on the agent's in-band forge delivery ack — `ForgeNotificationAck{subscription_id, revision}`, an additive AgentFrame variant emitted at turn-end flush — NEVER on dispatch success (Matt's W3 ruling, option beta), aligning this lane with the dispatch arm's own model ("The cursor is never advanced on send — it advances only later on the recipient's delivery_ack", `go/internal/runnerhub/dispatch_control.go:31-33`). Every pre-ack loss (synchronous refusal, async RunnerError via `router.complete`, agent death before flush) leaves the cursor unadvanced and is healed by the reconcile sweep within one backstop interval. The correlation is new work: today's `delivery_ack` carries only a comms `message_id` (`agent.proto:232-239`). This reopens the frozen delivery record's advance-signal clause (`compass-notification-delivery/design.md:894-897`) and rides the freeze-gate ratification, bundled with the fresh-subscription catch-up baseline moving from DL-053's "at Subscribe time" to first-observed-event/sweep (bounded by ≤1 backstop interval) — also decided in W3's ruling. | Active (Matt, 2026-08-26) | [forge agent-notification §W3](product/compass-forge-agent-notification/design.md#resolved-decisions-matt-2026-08-26) | | DL-267 | Forge subscriptions gain CONTAINER-SCOPE granularity via an explicit `ForgeSubscriptionScope` enum on `SubscribeForgeRequest` — Matt's W2 ruling, option (b); the `number = 0` sentinel is REJECTED (proto3 absent-vs-0 blindness would convert a forgotten `number` from an LLM tool caller into a silent whole-repo subscription). Containers are PROVIDER-NATIVE: GitHub's container is the REPO (the existing `repo` slot); Linear's is a PROJECT, carried in a slot of its own and never overloaded onto the team key in `repo` — Linear project scope is thereby IN scope (promoted from deferral). The project identifier lives in a dedicated column (OQ-1, ruled (i): additive `scope` + `project` columns, UNIQUE widened). A container subscription delivers exactly `FORGE_NOTIFICATION_KIND_OPENED` with the new artifact's number/url — never a fan-in of every event on every artifact. Two additive `ForgeNotificationKind` values land with it: `REVIEW = 5` (submitted PR review; free on webhooks where the poll design priced no review endpoint) and `OPENED = 6`. | Active (Matt, 2026-08-26) | [forge agent-notification §subscription model](product/compass-forge-agent-notification/design.md#the-subscription-model-folded-per-matts-rulings-3--w2) | | DL-281 | Board issue ingestion is WEBHOOK-DRIVEN, not polled: the GitHub App webhook ingress (DL-264's `POST /webhooks/github`) fans accepted `issues` events to a board ingest arm that normalizes the repo, gates on `forge_repo_subscriptions` (DL-162, kept), hydrates each coordinate via a conditional GET, and sinks through the one `StripOwner`→`TranslateIssue`→stamp pipeline into `IssueProjection.PublishIssueUpdate`; reliability + cold-start are a bounded per-repo updated-order reconcile sweep (startup + 30-min ticker, per-repo `updated_at` watermark advanced only after sink), the DL-264 backstop pattern at repo-LIST granularity. Retires the DL-161 standing poll driver + `forge_list_cursors`; amends DL-053's "conditional polling in v1" transport clause (webhooks primary, conditional reads only inside the sweep) and DL-163's four-table set (`forge_list_cursors` dropped by editing `0001_init.sql` in place — Compass is pre-live, the init migration is edited directly; additive watermark + `forge_updated_at` recency-guard columns added the same way). The GitHub App is the ONLY GitHub read credential — the static read-path PAT is retired, completing the App-only cutover. Known pre-existing limit (poll had it identically): no transport removes a board row — a forge-deleted/transferred issue persists until manually removed | Active (Matt, 2026-08-27) | [board webhook ingestion §Approach](product/compass-forge-board-webhook-ingestion/design.md#approach) | +| DL-269 | Every request-input account field on the compass proto contract is handle-typed; the Server resolves handle→account_id at the service edge (the `from_handle` posture generalized). An unresolvable, invisible, foreign, or wrong-subtype handle is ONE indistinguishable in-band NOT_FOUND naming the submitted handle — the oracle invariant that forbids a caller distinguishing "exists under another owner" from "no such handle" (the ReparentAgent foreign-parent leg mirrors CreateAgent's same-owner edge pre-check to hold this). No agent or client UI ever resolves an id. | Active (Matt, 2026-08-27) | [handle-addressing cutover §Ledger impact](product/compass-handle-addressing-cutover/design.md#ledger-impact) | +| DL-270 | Response, stored, and event account fields stay id-typed — ids are the stable join keys clients already hold. A response that needs a handle for display carries it as an explicit sibling field (the `RosterEntry.agent_account_id`+`handle` dual), never by retyping the id field; the response-side getters (`SpawnPeerResponse`/`RosterEntry`/`AgentSessionStatus`) keep their id accessors. | Active (Matt, 2026-08-27) | [handle-addressing cutover §Ledger impact](product/compass-handle-addressing-cutover/design.md#ledger-impact) | +| DL-271 | Handle→id resolution is owner-namespaced, stored in a dedicated `account_handles(account_id, handle, owner_user_id NULL)` table with two partial-unique indexes (user/system handles globally unique; agent handles unique per owner). Agents are addressed owner-qualified (`matt/compass-ux`, bare = caller's own owner); users/system bare. Rename is in-place for both tiers; reclaim is allowed for both tiers (no history, tombstone, or reservation) — cross-human reclaim safety is the owner-peering authorization edge (RIG-2796), not handle reservation. The `handle` column stays on `accounts` for display only; `account_handles` is the resolution key. | Active (Matt, 2026-08-27) | [handle-addressing cutover §Ledger impact](product/compass-handle-addressing-cutover/design.md#ledger-impact) | > Note (2026-07-31, SEA-1570 R5): DL-065's retired internal `ResumeContext > resume = 12` envelope field is NOT silently reintroduced — the collapsed diff --git a/go/e2e/agent_ops.go b/go/e2e/agent_ops.go index 59e3423a..00e3ed43 100644 --- a/go/e2e/agent_ops.go +++ b/go/e2e/agent_ops.go @@ -39,10 +39,7 @@ func (f *Fixture) CreateAgent(ctx context.Context, handle, displayName string) ( func (f *Fixture) Provision(ctx context.Context, accountID, clientRequestID string) (containerName string, err error) { rctx, cancel := context.WithTimeout(ctx, rpcTimeout) defer cancel() - resp, err := f.Compass().ProvisionAgentWorkspace(rctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: accountID, - ClientRequestId: clientRequestID, - })) + resp, err := f.Compass().ProvisionAgentWorkspace(rctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: accountID, ClientRequestId: clientRequestID})) if err != nil { return "", fmt.Errorf("ProvisionAgentWorkspace RPC: %w", err) } @@ -337,6 +334,26 @@ func (f *Fixture) waitRunnerEnrolled(ctx context.Context) error { // the gate. const rootSupervisorHandle = "supervisor" +// bootstrapAdminHandle mirrors the server's default bootstrap-admin handle +// (server/serve.go bootstrapAdminHandle) — the same cross-package literal +// coupling as rootSupervisorHandle above. The seeded root supervisor is owned by +// this admin, so the owner-qualified AgentByHandle lookup resolves the admin's +// account id through this handle first. +const bootstrapAdminHandle = "admin" + +// adminAgentByHandle resolves an admin-owned agent by its bare handle. Every leg +// creates its agents through the fixture's admin-authed CreateAgent, so they all +// live in the bootstrap admin's agent namespace; after the RIG-2751 handle +// cutover AgentByHandle is owner-qualified, so the lookup first resolves the +// admin's account id (bare user handle, global index) then the agent under it. +func adminAgentByHandle(ctx context.Context, st *store.Store, handle string) (store.Account, error) { + admin, err := st.UserByHandle(ctx, bootstrapAdminHandle) + if err != nil { + return store.Account{}, err + } + return st.AgentByHandle(ctx, admin.ID, handle) +} + // waitSeedSettled blocks until the first-launch root-supervisor seed has finished // provisioning its container, or the budget elapses. It is the seed counterpart // to waitRunnerEnrolled: the seed (server/serve_seed.go) hangs off the Runner @@ -419,7 +436,17 @@ func (f *Fixture) seedSettledProbe(ctx context.Context, st *store.Store, deadlin } rctx, cancel := context.WithTimeout(ctx, perProbe) defer cancel() - sup, handleErr := st.AgentByHandle(rctx, rootSupervisorHandle) + // The supervisor is a root agent owned by the bootstrap admin, so resolve the + // admin's account id first (bare user handle in the global index) and look + // the supervisor up in that owner's agent namespace (AgentByHandle is + // owner-qualified after the RIG-2751 handle cutover). A not-yet-created admin + // or supervisor is the same "still seeding" ErrNotFound the caller polls + // through. + admin, adminErr := st.UserByHandle(rctx, bootstrapAdminHandle) + if adminErr != nil { + return classifySeedSettle(adminErr, nil) + } + sup, handleErr := st.AgentByHandle(rctx, admin.ID, rootSupervisorHandle) if handleErr != nil { return classifySeedSettle(handleErr, nil) } diff --git a/go/e2e/comms_ops.go b/go/e2e/comms_ops.go index c0746779..39526170 100644 --- a/go/e2e/comms_ops.go +++ b/go/e2e/comms_ops.go @@ -33,7 +33,7 @@ func (f *Fixture) PostMessage(ctx context.Context, channelID, topicName, text st return resp.Msg.GetMessage().GetId(), nil } -// SubscribeMember adds accountID to channelID's membership AND marks it +// SubscribeMember adds memberHandle to channelID's membership AND marks it // subscribed over CommsService.UpdateChannelMembers, so the account joins the // channel's DELIVER set. It is the leg-4 second-recipient join: the reused leg-3 // spawner is subscribed-but-unmentioned onto the mentioned peer's home channel, @@ -41,17 +41,17 @@ func (f *Fixture) PostMessage(ctx context.Context, channelID, topicName, text st // add and the subscribe lists are set in the one request because the deliver set // (store SubscribedAgents, delivery_reads.go) requires the member's subscribed // flag on a non-home, non-mandatory channel — a bare add inserts subscribed=FALSE -// and the account is filtered out of the deliver set; subscribe_account_ids +// and the account is filtered out of the deliver set; subscribe_handles // requires the account already be a current or added member (comms.proto:644-645), // so the two travel together. Returns an error rather than panicking so the // caller (a test) decides fatality; the per-call deadline is threaded from ctx. -func (f *Fixture) SubscribeMember(ctx context.Context, channelID, accountID string) error { +func (f *Fixture) SubscribeMember(ctx context.Context, channelID, memberHandle string) error { rctx, cancel := context.WithTimeout(ctx, rpcTimeout) defer cancel() if _, err := f.Comms().UpdateChannelMembers(rctx, connect.NewRequest(&compassv1.UpdateChannelMembersRequest{ - ChannelId: channelID, - AddMemberAccountIds: []string{accountID}, - SubscribeAccountIds: []string{accountID}, + ChannelId: channelID, + AddMemberHandles: []string{memberHandle}, + SubscribeHandles: []string{memberHandle}, })); err != nil { return fmt.Errorf("UpdateChannelMembers RPC: %w", err) } diff --git a/go/e2e/legcomms_test.go b/go/e2e/legcomms_test.go index 628fd6f5..aae6e927 100644 --- a/go/e2e/legcomms_test.go +++ b/go/e2e/legcomms_test.go @@ -112,7 +112,7 @@ func TestCommsPostMessageThroughAgentLoop(t *testing.T) { // Resolve the poster to get its home channel id (the channel the trigger post // lands on and the channel the agent's own post — channel_id omitted — fans // onto). - poster, err := st.AgentByHandle(ctx, "comms-leg-poster") + poster, err := adminAgentByHandle(ctx, st, "comms-leg-poster") if err != nil { t.Fatalf("AgentByHandle(poster): %v", err) } diff --git a/go/e2e/legfive_test.go b/go/e2e/legfive_test.go index e904c7d1..ed6611bc 100644 --- a/go/e2e/legfive_test.go +++ b/go/e2e/legfive_test.go @@ -92,7 +92,7 @@ func TestLegFivePersistAndResume(t *testing.T) { // exactly why post1 below must be acked (cursor advanced) before the resume: // otherwise container2's start-sweep would redeliver it and consume the // resumed lifetime's canned turn. So each post must precede its settle wait. - acc, err := st.AgentByHandle(ctx, "leg5-persistresume") + acc, err := adminAgentByHandle(ctx, st, "leg5-persistresume") if err != nil { t.Fatalf("AgentByHandle: %v", err) } diff --git a/go/e2e/legsix_test.go b/go/e2e/legsix_test.go index 38c473e0..c7d748df 100644 --- a/go/e2e/legsix_test.go +++ b/go/e2e/legsix_test.go @@ -147,7 +147,7 @@ func TestLegSixTeardownIdempotence(t *testing.T) { t.Fatalf("store.Open (run2): %v", err) } defer st.Close() - persisted, err := st.AgentByHandle(ctx, handle) + persisted, err := adminAgentByHandle(ctx, st, handle) if err != nil { t.Fatalf("AgentByHandle(%q) (run2): %v — the postgres cluster did not re-attach across the restart, so the account run1 minted did not survive and the deterministic-name collision premise is gone", handle, err) } diff --git a/go/e2e/legthreefour_test.go b/go/e2e/legthreefour_test.go index f54f5085..380f0113 100644 --- a/go/e2e/legthreefour_test.go +++ b/go/e2e/legthreefour_test.go @@ -123,7 +123,7 @@ func TestLegThreeFourSpawnAndMessaging(t *testing.T) { return } defer st.Close() - peer, err := st.AgentByHandle(ctx, peerHandle) + peer, err := adminAgentByHandle(ctx, st, peerHandle) if err != nil { return } @@ -159,7 +159,7 @@ func TestLegThreeFourSpawnAndMessaging(t *testing.T) { // session-start sweep only redelivers messages left undelivered from a prior // lifetime (relevant only to leg-5's post1), not this one. Must precede the // settle wait. - spawner, err := st.AgentByHandle(ctx, "leg34-spawner") + spawner, err := adminAgentByHandle(ctx, st, "leg34-spawner") if err != nil { t.Fatalf("AgentByHandle(spawner): %v", err) } @@ -178,7 +178,7 @@ func TestLegThreeFourSpawnAndMessaging(t *testing.T) { // ── Leg 3: fresh peer account (F2 ownership) + a second real container ── // The spawn minted a fresh agent account resolvable by its handle. - peer, err := st.AgentByHandle(ctx, peerHandle) + peer, err := adminAgentByHandle(ctx, st, peerHandle) if err != nil { t.Fatalf("AgentByHandle(%q): %v — the scripted spawn did not mint the peer account (RED until H3 registers the native spawn tool, design.md:655-659)", peerHandle, err) } @@ -283,7 +283,9 @@ func TestLegThreeFourSpawnAndMessaging(t *testing.T) { // Subscribe the spawner onto the peer's home channel so it becomes a plain // deliver target there (the second recipient, no new container). - if err := f.SubscribeMember(ctx, string(peer.Agent.HomeChannelID), spawnerID); err != nil { + // The spawner is a bare agent handle in the caller's own owner namespace + // (created via CreateAgent above); T3 resolves it to the spawner account. + if err := f.SubscribeMember(ctx, string(peer.Agent.HomeChannelID), "leg34-spawner"); err != nil { t.Fatalf("SubscribeMember(spawner → peer home channel): %v", err) } diff --git a/go/e2e/legtwo_test.go b/go/e2e/legtwo_test.go index 8a578436..62fc45df 100644 --- a/go/e2e/legtwo_test.go +++ b/go/e2e/legtwo_test.go @@ -135,7 +135,7 @@ func TestLegTwoRealTurn(t *testing.T) { // AwaitTurnSettled waits on. The session-start sweep only redelivers // messages left undelivered from a prior lifetime (relevant only to leg-5's // post1), not this one. - acc, err := st.AgentByHandle(ctx, "leg2-realturn") + acc, err := adminAgentByHandle(ctx, st, "leg2-realturn") if err != nil { t.Fatalf("AgentByHandle: %v", err) } diff --git a/go/e2e/seed_settle_test.go b/go/e2e/seed_settle_test.go index 84e7ca72..38f4f5f0 100644 --- a/go/e2e/seed_settle_test.go +++ b/go/e2e/seed_settle_test.go @@ -52,7 +52,7 @@ func TestSeedSettledBeforeFixtureReturns(t *testing.T) { // return before the seed even created the supervisor. hctx, hcancel := context.WithTimeout(ctx, rpcTimeout) defer hcancel() - sup, err := st.AgentByHandle(hctx, rootSupervisorHandle) + sup, err := adminAgentByHandle(hctx, st, rootSupervisorHandle) if err != nil { t.Fatalf("root supervisor not resolvable after NewFixture returned: %v (the seed-settle gate did not hold)", err) } diff --git a/go/gen/compass/v1/comms.pb.go b/go/gen/compass/v1/comms.pb.go index 935f364a..1629617c 100644 --- a/go/gen/compass/v1/comms.pb.go +++ b/go/gen/compass/v1/comms.pb.go @@ -2429,10 +2429,10 @@ type CreateAgentRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Handle string `protobuf:"bytes,1,opt,name=handle,proto3" json:"handle,omitempty"` DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` - // Optional parent in the agent tree; empty = root. The server validates it - // (must resolve to an existing agent under the caller's resolved owner) before - // set-at-creation. - ParentAgentId string `protobuf:"bytes,3,opt,name=parent_agent_id,json=parentAgentId,proto3" json:"parent_agent_id,omitempty"` + // Optional parent in the agent tree; empty = root. A `@handle`; the server + // resolves it to an account id; unknown → NOT_FOUND. Resolves owner-qualified + // (a bare handle defaults to the caller's own owner namespace). + ParentHandle string `protobuf:"bytes,3,opt,name=parent_handle,json=parentHandle,proto3" json:"parent_handle,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2481,9 +2481,9 @@ func (x *CreateAgentRequest) GetDisplayName() string { return "" } -func (x *CreateAgentRequest) GetParentAgentId() string { +func (x *CreateAgentRequest) GetParentHandle() string { if x != nil { - return x.ParentAgentId + return x.ParentHandle } return "" } @@ -2916,10 +2916,11 @@ type CreateChannelRequest struct { // owner-scoped channel. GroupId string `protobuf:"bytes,2,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` Kind ChannelKind `protobuf:"varint,3,opt,name=kind,proto3,enum=compass.v1.ChannelKind" json:"kind,omitempty"` - // Initial members party to the channel. - MemberAccountIds []string `protobuf:"bytes,4,rep,name=member_account_ids,json=memberAccountIds,proto3" json:"member_account_ids,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Initial members party to the channel. Each is a `@handle`; the server + // resolves it to an account id; unknown → NOT_FOUND. + MemberHandles []string `protobuf:"bytes,4,rep,name=member_handles,json=memberHandles,proto3" json:"member_handles,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateChannelRequest) Reset() { @@ -2973,9 +2974,9 @@ func (x *CreateChannelRequest) GetKind() ChannelKind { return ChannelKind_CHANNEL_KIND_CHANNEL } -func (x *CreateChannelRequest) GetMemberAccountIds() []string { +func (x *CreateChannelRequest) GetMemberHandles() []string { if x != nil { - return x.MemberAccountIds + return x.MemberHandles } return nil } @@ -3028,16 +3029,21 @@ type UpdateChannelMembersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The channel to mutate. ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` - // Accounts to add as members (join, read access). - AddMemberAccountIds []string `protobuf:"bytes,2,rep,name=add_member_account_ids,json=addMemberAccountIds,proto3" json:"add_member_account_ids,omitempty"` - // Accounts to remove from membership. - RemoveMemberAccountIds []string `protobuf:"bytes,3,rep,name=remove_member_account_ids,json=removeMemberAccountIds,proto3" json:"remove_member_account_ids,omitempty"` + // Accounts to add as members (join, read access). Each is a `@handle`; the + // server resolves it to an account id; unknown → NOT_FOUND. + AddMemberHandles []string `protobuf:"bytes,2,rep,name=add_member_handles,json=addMemberHandles,proto3" json:"add_member_handles,omitempty"` + // Accounts to remove from membership. Each is a `@handle`; the server + // resolves it to an account id; unknown → NOT_FOUND. + RemoveMemberHandles []string `protobuf:"bytes,3,rep,name=remove_member_handles,json=removeMemberHandles,proto3" json:"remove_member_handles,omitempty"` // Members to mark subscribed (push opt-in); must be current or added members. - SubscribeAccountIds []string `protobuf:"bytes,4,rep,name=subscribe_account_ids,json=subscribeAccountIds,proto3" json:"subscribe_account_ids,omitempty"` - // Members to mark unsubscribed (read-only). - UnsubscribeAccountIds []string `protobuf:"bytes,5,rep,name=unsubscribe_account_ids,json=unsubscribeAccountIds,proto3" json:"unsubscribe_account_ids,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Each is a `@handle`; the server resolves it to an account id; unknown → + // NOT_FOUND. + SubscribeHandles []string `protobuf:"bytes,4,rep,name=subscribe_handles,json=subscribeHandles,proto3" json:"subscribe_handles,omitempty"` + // Members to mark unsubscribed (read-only). Each is a `@handle`; the server + // resolves it to an account id; unknown → NOT_FOUND. + UnsubscribeHandles []string `protobuf:"bytes,5,rep,name=unsubscribe_handles,json=unsubscribeHandles,proto3" json:"unsubscribe_handles,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdateChannelMembersRequest) Reset() { @@ -3077,30 +3083,30 @@ func (x *UpdateChannelMembersRequest) GetChannelId() string { return "" } -func (x *UpdateChannelMembersRequest) GetAddMemberAccountIds() []string { +func (x *UpdateChannelMembersRequest) GetAddMemberHandles() []string { if x != nil { - return x.AddMemberAccountIds + return x.AddMemberHandles } return nil } -func (x *UpdateChannelMembersRequest) GetRemoveMemberAccountIds() []string { +func (x *UpdateChannelMembersRequest) GetRemoveMemberHandles() []string { if x != nil { - return x.RemoveMemberAccountIds + return x.RemoveMemberHandles } return nil } -func (x *UpdateChannelMembersRequest) GetSubscribeAccountIds() []string { +func (x *UpdateChannelMembersRequest) GetSubscribeHandles() []string { if x != nil { - return x.SubscribeAccountIds + return x.SubscribeHandles } return nil } -func (x *UpdateChannelMembersRequest) GetUnsubscribeAccountIds() []string { +func (x *UpdateChannelMembersRequest) GetUnsubscribeHandles() []string { if x != nil { - return x.UnsubscribeAccountIds + return x.UnsubscribeHandles } return nil } @@ -3151,12 +3157,14 @@ func (x *UpdateChannelMembersResponse) GetChannel() *Channel { type ReparentAgentRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // The agent to move. - AgentAccountId string `protobuf:"bytes,1,opt,name=agent_account_id,json=agentAccountId,proto3" json:"agent_account_id,omitempty"` - // The new parent; empty promotes the agent to a root. - NewParentAgentId string `protobuf:"bytes,2,opt,name=new_parent_agent_id,json=newParentAgentId,proto3" json:"new_parent_agent_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The agent to move. A `@handle`; the server resolves it to an account id; + // unknown → NOT_FOUND. + AgentHandle string `protobuf:"bytes,1,opt,name=agent_handle,json=agentHandle,proto3" json:"agent_handle,omitempty"` + // The new parent; empty promotes the agent to a root. A `@handle`; the server + // resolves it to an account id; unknown → NOT_FOUND. + NewParentHandle string `protobuf:"bytes,2,opt,name=new_parent_handle,json=newParentHandle,proto3" json:"new_parent_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReparentAgentRequest) Reset() { @@ -3189,16 +3197,16 @@ func (*ReparentAgentRequest) Descriptor() ([]byte, []int) { return file_compass_v1_comms_proto_rawDescGZIP(), []int{41} } -func (x *ReparentAgentRequest) GetAgentAccountId() string { +func (x *ReparentAgentRequest) GetAgentHandle() string { if x != nil { - return x.AgentAccountId + return x.AgentHandle } return "" } -func (x *ReparentAgentRequest) GetNewParentAgentId() string { +func (x *ReparentAgentRequest) GetNewParentHandle() string { if x != nil { - return x.NewParentAgentId + return x.NewParentHandle } return "" } @@ -3253,8 +3261,9 @@ type SetChannelPolicyRequest struct { ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` // The new post policy. PostPolicy ChannelPostPolicy `protobuf:"varint,2,opt,name=post_policy,json=postPolicy,proto3,enum=compass.v1.ChannelPostPolicy" json:"post_policy,omitempty"` - // The owner/operator account for the channel; empty leaves it unowned. - OwnerAccountId string `protobuf:"bytes,3,opt,name=owner_account_id,json=ownerAccountId,proto3" json:"owner_account_id,omitempty"` + // The owner/operator account for the channel; empty leaves it unowned. A + // `@handle`; the server resolves it to an account id; unknown → NOT_FOUND. + OwnerHandle string `protobuf:"bytes,3,opt,name=owner_handle,json=ownerHandle,proto3" json:"owner_handle,omitempty"` // Whether membership implies a non-togglable subscription. MandatorySubscription bool `protobuf:"varint,4,opt,name=mandatory_subscription,json=mandatorySubscription,proto3" json:"mandatory_subscription,omitempty"` unknownFields protoimpl.UnknownFields @@ -3305,9 +3314,9 @@ func (x *SetChannelPolicyRequest) GetPostPolicy() ChannelPostPolicy { return ChannelPostPolicy_CHANNEL_POST_POLICY_OPEN } -func (x *SetChannelPolicyRequest) GetOwnerAccountId() string { +func (x *SetChannelPolicyRequest) GetOwnerHandle() string { if x != nil { - return x.OwnerAccountId + return x.OwnerHandle } return "" } @@ -3368,11 +3377,12 @@ type GetRosterRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The vantage the roster is computed around. Scope RosterScope `protobuf:"varint,1,opt,name=scope,proto3,enum=compass.v1.RosterScope" json:"scope,omitempty"` - // The agent whose vantage to use. Optional for human/UI callers naming a - // vantage point; an agent caller gets it session-resolved server-side. - AgentAccountId string `protobuf:"bytes,2,opt,name=agent_account_id,json=agentAccountId,proto3" json:"agent_account_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The vantage whose roster to compute. A `@handle`; empty ⇒ the caller's own + // vantage (an agent caller is session-resolved server-side), non-empty ⇒ the + // server resolves it to an account id; unknown → NOT_FOUND. + VantageHandle string `protobuf:"bytes,2,opt,name=vantage_handle,json=vantageHandle,proto3" json:"vantage_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetRosterRequest) Reset() { @@ -3412,9 +3422,9 @@ func (x *GetRosterRequest) GetScope() RosterScope { return RosterScope_ROSTER_SCOPE_NEIGHBORHOOD } -func (x *GetRosterRequest) GetAgentAccountId() string { +func (x *GetRosterRequest) GetVantageHandle() string { if x != nil { - return x.AgentAccountId + return x.VantageHandle } return "" } @@ -3752,10 +3762,11 @@ func (x *UpdatePinnedBoardResponse) GetChannel() *Channel { type OpenAgentWorkspaceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // The agent account to open the workspace (ACP surface) for. - AgentAccountId string `protobuf:"bytes,1,opt,name=agent_account_id,json=agentAccountId,proto3" json:"agent_account_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The agent account to open the workspace (ACP surface) for. A `@handle`; the + // server resolves it to an account id; unknown → NOT_FOUND. + AgentHandle string `protobuf:"bytes,1,opt,name=agent_handle,json=agentHandle,proto3" json:"agent_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *OpenAgentWorkspaceRequest) Reset() { @@ -3788,9 +3799,9 @@ func (*OpenAgentWorkspaceRequest) Descriptor() ([]byte, []int) { return file_compass_v1_comms_proto_rawDescGZIP(), []int{51} } -func (x *OpenAgentWorkspaceRequest) GetAgentAccountId() string { +func (x *OpenAgentWorkspaceRequest) GetAgentHandle() string { if x != nil { - return x.AgentAccountId + return x.AgentHandle } return "" } @@ -4875,11 +4886,11 @@ const file_compass_v1_comms_proto_rawDesc = "" + "\x06handle\x18\x01 \x01(\tR\x06handle\x12!\n" + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\"C\n" + "\x12CreateUserResponse\x12-\n" + - "\aaccount\x18\x01 \x01(\v2\x13.compass.v1.AccountR\aaccount\"w\n" + + "\aaccount\x18\x01 \x01(\v2\x13.compass.v1.AccountR\aaccount\"t\n" + "\x12CreateAgentRequest\x12\x16\n" + "\x06handle\x18\x01 \x01(\tR\x06handle\x12!\n" + - "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12&\n" + - "\x0fparent_agent_id\x18\x03 \x01(\tR\rparentAgentId\"D\n" + + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12#\n" + + "\rparent_handle\x18\x03 \x01(\tR\fparentHandle\"D\n" + "\x13CreateAgentResponse\x12-\n" + "\aaccount\x18\x01 \x01(\v2\x13.compass.v1.AccountR\aaccount\"8\n" + "\x13ListAccountsRequest\x12!\n" + @@ -4901,40 +4912,40 @@ const file_compass_v1_comms_proto_rawDesc = "" + "\x13ListChannelsRequest\x12!\n" + "\fsnapshot_seq\x18\x01 \x01(\x04R\vsnapshotSeq\"G\n" + "\x14ListChannelsResponse\x12/\n" + - "\bchannels\x18\x01 \x03(\v2\x13.compass.v1.ChannelR\bchannels\"\xa0\x01\n" + + "\bchannels\x18\x01 \x03(\v2\x13.compass.v1.ChannelR\bchannels\"\x99\x01\n" + "\x14CreateChannelRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + "\bgroup_id\x18\x02 \x01(\tR\agroupId\x12+\n" + - "\x04kind\x18\x03 \x01(\x0e2\x17.compass.v1.ChannelKindR\x04kind\x12,\n" + - "\x12member_account_ids\x18\x04 \x03(\tR\x10memberAccountIds\"F\n" + + "\x04kind\x18\x03 \x01(\x0e2\x17.compass.v1.ChannelKindR\x04kind\x12%\n" + + "\x0emember_handles\x18\x04 \x03(\tR\rmemberHandles\"F\n" + "\x15CreateChannelResponse\x12-\n" + - "\achannel\x18\x01 \x01(\v2\x13.compass.v1.ChannelR\achannel\"\x98\x02\n" + + "\achannel\x18\x01 \x01(\v2\x13.compass.v1.ChannelR\achannel\"\xfc\x01\n" + "\x1bUpdateChannelMembersRequest\x12\x1d\n" + "\n" + - "channel_id\x18\x01 \x01(\tR\tchannelId\x123\n" + - "\x16add_member_account_ids\x18\x02 \x03(\tR\x13addMemberAccountIds\x129\n" + - "\x19remove_member_account_ids\x18\x03 \x03(\tR\x16removeMemberAccountIds\x122\n" + - "\x15subscribe_account_ids\x18\x04 \x03(\tR\x13subscribeAccountIds\x126\n" + - "\x17unsubscribe_account_ids\x18\x05 \x03(\tR\x15unsubscribeAccountIds\"M\n" + + "channel_id\x18\x01 \x01(\tR\tchannelId\x12,\n" + + "\x12add_member_handles\x18\x02 \x03(\tR\x10addMemberHandles\x122\n" + + "\x15remove_member_handles\x18\x03 \x03(\tR\x13removeMemberHandles\x12+\n" + + "\x11subscribe_handles\x18\x04 \x03(\tR\x10subscribeHandles\x12/\n" + + "\x13unsubscribe_handles\x18\x05 \x03(\tR\x12unsubscribeHandles\"M\n" + "\x1cUpdateChannelMembersResponse\x12-\n" + - "\achannel\x18\x01 \x01(\v2\x13.compass.v1.ChannelR\achannel\"o\n" + - "\x14ReparentAgentRequest\x12(\n" + - "\x10agent_account_id\x18\x01 \x01(\tR\x0eagentAccountId\x12-\n" + - "\x13new_parent_agent_id\x18\x02 \x01(\tR\x10newParentAgentId\"F\n" + + "\achannel\x18\x01 \x01(\v2\x13.compass.v1.ChannelR\achannel\"e\n" + + "\x14ReparentAgentRequest\x12!\n" + + "\fagent_handle\x18\x01 \x01(\tR\vagentHandle\x12*\n" + + "\x11new_parent_handle\x18\x02 \x01(\tR\x0fnewParentHandle\"F\n" + "\x15ReparentAgentResponse\x12-\n" + - "\aaccount\x18\x01 \x01(\v2\x13.compass.v1.AccountR\aaccount\"\xd9\x01\n" + + "\aaccount\x18\x01 \x01(\v2\x13.compass.v1.AccountR\aaccount\"\xd2\x01\n" + "\x17SetChannelPolicyRequest\x12\x1d\n" + "\n" + "channel_id\x18\x01 \x01(\tR\tchannelId\x12>\n" + "\vpost_policy\x18\x02 \x01(\x0e2\x1d.compass.v1.ChannelPostPolicyR\n" + - "postPolicy\x12(\n" + - "\x10owner_account_id\x18\x03 \x01(\tR\x0eownerAccountId\x125\n" + + "postPolicy\x12!\n" + + "\fowner_handle\x18\x03 \x01(\tR\vownerHandle\x125\n" + "\x16mandatory_subscription\x18\x04 \x01(\bR\x15mandatorySubscription\"I\n" + "\x18SetChannelPolicyResponse\x12-\n" + - "\achannel\x18\x01 \x01(\v2\x13.compass.v1.ChannelR\achannel\"k\n" + + "\achannel\x18\x01 \x01(\v2\x13.compass.v1.ChannelR\achannel\"h\n" + "\x10GetRosterRequest\x12-\n" + - "\x05scope\x18\x01 \x01(\x0e2\x17.compass.v1.RosterScopeR\x05scope\x12(\n" + - "\x10agent_account_id\x18\x02 \x01(\tR\x0eagentAccountId\"F\n" + + "\x05scope\x18\x01 \x01(\x0e2\x17.compass.v1.RosterScopeR\x05scope\x12%\n" + + "\x0evantage_handle\x18\x02 \x01(\tR\rvantageHandle\"F\n" + "\x11GetRosterResponse\x121\n" + "\aentries\x18\x01 \x03(\v2\x17.compass.v1.RosterEntryR\aentries\"\x9c\x02\n" + "\vRosterEntry\x12(\n" + @@ -4957,9 +4968,9 @@ const file_compass_v1_comms_proto_rawDesc = "" + "message_id\x18\x01 \x01(\tR\tmessageId\x12,\n" + "\x12replace_message_id\x18\x02 \x01(\tR\x10replaceMessageId\"J\n" + "\x19UpdatePinnedBoardResponse\x12-\n" + - "\achannel\x18\x01 \x01(\v2\x13.compass.v1.ChannelR\achannel\"E\n" + - "\x19OpenAgentWorkspaceRequest\x12(\n" + - "\x10agent_account_id\x18\x01 \x01(\tR\x0eagentAccountId\"V\n" + + "\achannel\x18\x01 \x01(\v2\x13.compass.v1.ChannelR\achannel\">\n" + + "\x19OpenAgentWorkspaceRequest\x12!\n" + + "\fagent_handle\x18\x01 \x01(\tR\vagentHandle\"V\n" + "\x1aOpenAgentWorkspaceResponse\x128\n" + "\tworkspace\x18\x01 \x01(\v2\x1a.compass.v1.AgentWorkspaceR\tworkspace\"\xc3\x01\n" + "\x13ListMessagesRequest\x12\x1f\n" + diff --git a/go/gen/compass/v1/compass.pb.go b/go/gen/compass/v1/compass.pb.go index a03cbfa8..884a7e83 100644 --- a/go/gen/compass/v1/compass.pb.go +++ b/go/gen/compass/v1/compass.pb.go @@ -2663,10 +2663,10 @@ func (x *AgentSessionFrame) GetState() AgentSessionState { // handle StartAgentSession then brings online (design: architecture-lineage). type ProvisionAgentWorkspaceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // The owned agent account this workstream belongs to (the AgentAccount id - // from CommsService). Names whose credentials + home channel the container - // is provisioned for. - AgentAccountId string `protobuf:"bytes,1,opt,name=agent_account_id,json=agentAccountId,proto3" json:"agent_account_id,omitempty"` + // The owned agent account this workstream belongs to. A `@handle` + // (owner-qualified, e.g. `matt/compass-ux`); the server resolves it to an + // account id; unknown → NOT_FOUND. + AgentHandle string `protobuf:"bytes,1,opt,name=agent_handle,json=agentHandle,proto3" json:"agent_handle,omitempty"` // Repo carriage removed (SEA-1527, Matt 2026-07-29): spawn/provision no longer // clone a repo for the agent. The container is provisioned with a git // credential + workspace and the agent self-clones whatever it needs after @@ -2734,9 +2734,9 @@ func (*ProvisionAgentWorkspaceRequest) Descriptor() ([]byte, []int) { return file_compass_v1_compass_proto_rawDescGZIP(), []int{33} } -func (x *ProvisionAgentWorkspaceRequest) GetAgentAccountId() string { +func (x *ProvisionAgentWorkspaceRequest) GetAgentHandle() string { if x != nil { - return x.AgentAccountId + return x.AgentHandle } return "" } @@ -3013,10 +3013,10 @@ func (x *StartAgentSessionResponse) GetSessionId() string { // client_request_id, the single call a UI makes to bring an agent online. type SpawnAgentRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // The owned agent account to bring online (the AgentAccount id from - // CommsService). The container is provisioned for, and the session bound to, - // this account. - AgentAccountId string `protobuf:"bytes,1,opt,name=agent_account_id,json=agentAccountId,proto3" json:"agent_account_id,omitempty"` + // The owned agent account to bring online. A `@handle` (owner-qualified, + // e.g. `matt/compass-ux`); the server resolves it to an account id; unknown + // → NOT_FOUND. + AgentHandle string `protobuf:"bytes,1,opt,name=agent_handle,json=agentHandle,proto3" json:"agent_handle,omitempty"` // End-to-end idempotency key. A retry with the same id returns the same // session_id and provisions no second container: the server threads it across // both the internal Provision and Start so the whole composite dedups, and @@ -3058,9 +3058,9 @@ func (*SpawnAgentRequest) Descriptor() ([]byte, []int) { return file_compass_v1_compass_proto_rawDescGZIP(), []int{39} } -func (x *SpawnAgentRequest) GetAgentAccountId() string { +func (x *SpawnAgentRequest) GetAgentHandle() string { if x != nil { - return x.AgentAccountId + return x.AgentHandle } return "" } @@ -3391,10 +3391,10 @@ func (x *GetAgentStatusResponse) GetStatuses() []*AgentSessionStatus { // IssueToken: the admin-only path to mint a bearer token for an account. type IssueTokenRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // The account to mint a token for — the id of an Account created via - // CommsService. The caller's identity is the authenticated connection, never - // a field here. - AccountId string `protobuf:"bytes,1,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` + // The account to mint a token for — a `@handle` naming a user or agent; the + // server resolves it to an account id; unknown → NOT_FOUND. The caller's + // identity is the authenticated connection, never a field here. + AccountHandle string `protobuf:"bytes,1,opt,name=account_handle,json=accountHandle,proto3" json:"account_handle,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3429,9 +3429,9 @@ func (*IssueTokenRequest) Descriptor() ([]byte, []int) { return file_compass_v1_compass_proto_rawDescGZIP(), []int{47} } -func (x *IssueTokenRequest) GetAccountId() string { +func (x *IssueTokenRequest) GetAccountHandle() string { if x != nil { - return x.AccountId + return x.AccountHandle } return "" } @@ -4944,9 +4944,9 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12.\n" + "\x05event\x18\x02 \x01(\v2\x18.compass.v1.SessionEventR\x05event\x123\n" + - "\x05state\x18\x03 \x01(\x0e2\x1d.compass.v1.AgentSessionStateR\x05state\"\xa4\x01\n" + - "\x1eProvisionAgentWorkspaceRequest\x12(\n" + - "\x10agent_account_id\x18\x01 \x01(\tR\x0eagentAccountId\x12*\n" + + "\x05state\x18\x03 \x01(\x0e2\x1d.compass.v1.AgentSessionStateR\x05state\"\x9d\x01\n" + + "\x1eProvisionAgentWorkspaceRequest\x12!\n" + + "\fagent_handle\x18\x01 \x01(\tR\vagentHandle\x12*\n" + "\x11client_request_id\x18\x02 \x01(\tR\x0fclientRequestId\x12\x18\n" + "\apersona\x18\x03 \x01(\tR\apersona\x12\x12\n" + "\x04role\x18\x04 \x01(\tR\x04role\"H\n" + @@ -4961,9 +4961,9 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\x11resume_session_id\x18\x03 \x01(\tR\x0fresumeSessionIdJ\x04\b\x02\x10\x03R\x0einitial_prompt\":\n" + "\x19StartAgentSessionResponse\x12\x1d\n" + "\n" + - "session_id\x18\x01 \x01(\tR\tsessionId\"\x7f\n" + - "\x11SpawnAgentRequest\x12(\n" + - "\x10agent_account_id\x18\x01 \x01(\tR\x0eagentAccountId\x12*\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\"x\n" + + "\x11SpawnAgentRequest\x12!\n" + + "\fagent_handle\x18\x01 \x01(\tR\vagentHandle\x12*\n" + "\x11client_request_id\x18\x03 \x01(\tR\x0fclientRequestIdJ\x04\b\x02\x10\x03R\x0einitial_prompt\"Z\n" + "\x12SpawnAgentResponse\x12\x1d\n" + "\n" + @@ -4983,10 +4983,9 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\"T\n" + "\x16GetAgentStatusResponse\x12:\n" + - "\bstatuses\x18\x01 \x03(\v2\x1e.compass.v1.AgentSessionStatusR\bstatuses\"2\n" + - "\x11IssueTokenRequest\x12\x1d\n" + - "\n" + - "account_id\x18\x01 \x01(\tR\taccountId\"*\n" + + "\bstatuses\x18\x01 \x03(\v2\x1e.compass.v1.AgentSessionStatusR\bstatuses\":\n" + + "\x11IssueTokenRequest\x12%\n" + + "\x0eaccount_handle\x18\x01 \x01(\tR\raccountHandle\"*\n" + "\x12IssueTokenResponse\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\"/\n" + "\x12RevokeTokenRequest\x12\x19\n" + diff --git a/go/internal/comms/agent_tree_pgtest_test.go b/go/internal/comms/agent_tree_pgtest_test.go index 0cd0c0ff..a9e54a30 100644 --- a/go/internal/comms/agent_tree_pgtest_test.go +++ b/go/internal/comms/agent_tree_pgtest_test.go @@ -2,15 +2,18 @@ package comms -// ReparentAgent + CreateAgent-with-parent handler contracts (Record C, T3): -// the happy path emits AccountChanged and returns the mutated account, and each -// §Server validation clause maps to its exact gRPC code at the edge — -// PERMISSION_DENIED (caller authority, same-owner), FAILED_PRECONDITION (cycle), -// NOT_FOUND (missing parent). CreateAgent threads and validates the optional -// parent. Driven in-process via WithActor against a real store + bus. +// ReparentAgent + CreateAgent-with-parent handler contracts (Record C, T3), +// after the RIG-2751 handle cutover: requests carry `@handle`s the edge resolves +// owner-qualified, and the oracle-safe error contract (DL-269) collapses every +// post-resolution authority/visibility failure on a handle-addressed target into +// the SAME NOT_FOUND an unknown handle gets. So the happy path still emits +// AccountChanged and a cycle is still FAILED_PRECONDITION, but the two former +// PERMISSION_DENIED legs (foreign caller, cross-owner parent) are now NOT_FOUND. +// Driven in-process via WithActor against a real store + bus. import ( "context" + "strings" "testing" "connectrpc.com/connect" @@ -27,9 +30,10 @@ func TestReparentAgentHappyPathEmitsAccountChanged(t *testing.T) { events := firstEventAfterBoundary(t, h, owner.ID, &compassv1.SubscribeCommsRequest{SinceSeq: 0}) + // Bare handles resolve in the caller-owner's agent namespace. resp, err := h.svc.ReparentAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.ReparentAgentRequest{ - AgentAccountId: string(b.ID), - NewParentAgentId: string(a.ID), + AgentHandle: "b", + NewParentHandle: "a", })) if err != nil { t.Fatalf("ReparentAgent: %v", err) @@ -51,37 +55,93 @@ func TestReparentAgentHappyPathEmitsAccountChanged(t *testing.T) { } } -func TestReparentAgentForeignCallerPermissionDenied(t *testing.T) { +// TestReparentAgentForeignCallerNotFound: a caller under a different owner names +// the target by an owner-qualified handle it cannot reach — the store's clause-0 +// authority failure is remapped to NOT_FOUND naming the submitted handle +// (DL-269), byte-identical to an unknown target, so the foreign caller cannot +// probe the target's existence. +func TestReparentAgentForeignCallerNotFound(t *testing.T) { svc, st := newHandler(t) ctx := context.Background() owner := mustUser(t, st, "owner") other := mustUser(t, st, "other") a := mustAgent(t, st, owner.ID, "a") - b := mustAgent(t, st, owner.ID, "b") + mustAgent(t, st, owner.ID, "b") intruder := mustAgent(t, st, other.ID, "intruder") - // Clause 0: a caller under a different owner cannot re-parent the target. + // The intruder owner-qualifies the target into owner's namespace; the + // resolver resolves it (AgentByHandle is not viewer-scoped), but the store's + // clause-0 authority check then fails and is remapped to NOT_FOUND. _, err := svc.ReparentAgent(WithActor(ctx, intruder.ID), connect.NewRequest(&compassv1.ReparentAgentRequest{ - AgentAccountId: string(b.ID), - NewParentAgentId: string(a.ID), + AgentHandle: "owner/b", + NewParentHandle: "owner/a", })) - connectCodeIs(t, err, connect.CodePermissionDenied, "foreign caller") + connectCodeIs(t, err, connect.CodeNotFound, "foreign caller") + _ = a } -func TestReparentAgentCrossOwnerParentPermissionDenied(t *testing.T) { +// TestReparentAgentCrossOwnerParentNotFound: a parent under a different owner is +// remapped to NOT_FOUND (was PermissionDenied) AND is byte-identical to an +// UNKNOWN parent — the oracle-safe merge on the new_parent_handle target +// (DL-269). Asserting the message, not just the code, is load-bearing: a +// code-only check passes even if the foreign case names the agent handle while +// the unknown case names the parent handle, which is the exact existence-probe +// (enumerate another owner's agents by owner-qualified handle) the invariant +// forbids. +func TestReparentAgentCrossOwnerParentNotFound(t *testing.T) { svc, st := newHandler(t) ctx := context.Background() owner := mustUser(t, st, "owner") other := mustUser(t, st, "other") - a := mustAgent(t, st, owner.ID, "a") - foreign := mustAgent(t, st, other.ID, "foreign") + mustAgent(t, st, owner.ID, "a") + mustAgent(t, st, other.ID, "foreign") - // Clause 1: a parent under a different owner → PermissionDenied. - _, err := svc.ReparentAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.ReparentAgentRequest{ - AgentAccountId: string(a.ID), - NewParentAgentId: string(foreign.ID), + // Existing-but-foreign parent: resolves (AgentByHandle is not viewer-scoped), + // then the edge same-owner pre-check rejects it as NOT_FOUND naming the + // submitted new_parent_handle. + _, foreignErr := svc.ReparentAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.ReparentAgentRequest{ + AgentHandle: "a", + NewParentHandle: "other/foreign", + })) + connectCodeIs(t, foreignErr, connect.CodeNotFound, "cross-owner parent") + + // Unknown parent under the same owner-qualifier: misses at resolution, + // NOT_FOUND naming the same submitted spelling. + _, unknownErr := svc.ReparentAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.ReparentAgentRequest{ + AgentHandle: "a", + NewParentHandle: "other/ghost", })) - connectCodeIs(t, err, connect.CodePermissionDenied, "cross-owner parent") + connectCodeIs(t, unknownErr, connect.CodeNotFound, "unknown parent") + + // The oracle invariant: a foreign parent must be indistinguishable from an + // unknown one. Both must be NOT_FOUND (above) naming the SUBMITTED + // new_parent_handle — never the agent handle or a resolved id. Before the + // edge same-owner pre-check, the foreign case fell through to the store and + // was re-keyed to name the AGENT handle ("a"), while the unknown case named + // the parent handle — the exact divergence a caller uses to enumerate another + // owner's agents. Asserting each names its own parent spelling (and NOT the + // agent handle) closes that probe; the two cases are structurally identical, + // differing only by the handle the caller itself submitted. + if got := connect.CodeOf(foreignErr); got != connect.CodeOf(unknownErr) { + t.Fatalf("foreign vs unknown parent code differs: foreign=%v unknown=%v", got, connect.CodeOf(unknownErr)) + } + if !strings.Contains(foreignErr.Error(), "other/foreign") { + t.Fatalf("foreign-parent error must name the submitted parent handle, got %q", foreignErr.Error()) + } + if strings.Contains(foreignErr.Error(), `"a"`) { + t.Fatalf("oracle leak: foreign-parent error names the AGENT handle, distinguishing it from an unknown parent: %q", foreignErr.Error()) + } + if !strings.Contains(unknownErr.Error(), "other/ghost") { + t.Fatalf("unknown-parent error must name the submitted parent handle, got %q", unknownErr.Error()) + } + // Byte-identical modulo the caller's own submitted spelling: swapping the + // parent handle in the foreign error for the unknown one's yields the same + // string, proving the only difference is the input the caller already knows. + normalizedForeign := strings.ReplaceAll(foreignErr.Error(), "other/foreign", "PARENT") + normalizedUnknown := strings.ReplaceAll(unknownErr.Error(), "other/ghost", "PARENT") + if normalizedForeign != normalizedUnknown { + t.Fatalf("oracle leak: foreign vs unknown parent errors differ beyond the submitted handle.\n foreign: %q\n unknown: %q", foreignErr.Error(), unknownErr.Error()) + } } func TestReparentAgentCycleFailedPrecondition(t *testing.T) { @@ -93,11 +153,14 @@ func TestReparentAgentCycleFailedPrecondition(t *testing.T) { if err != nil { t.Fatalf("create b under a: %v", err) } + _ = b - // Clause 2: cycle → FailedPrecondition. + // Clause 2: cycle → FailedPrecondition. Both handles resolve (same owner), so + // the cycle check runs and its distinct code survives (it is not an + // authority/visibility failure, so DL-269's merge does not apply). _, err = svc.ReparentAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.ReparentAgentRequest{ - AgentAccountId: string(a.ID), - NewParentAgentId: string(b.ID), + AgentHandle: "a", + NewParentHandle: "b", })) connectCodeIs(t, err, connect.CodeFailedPrecondition, "cycle") } @@ -106,12 +169,12 @@ func TestReparentAgentMissingParentNotFound(t *testing.T) { svc, st := newHandler(t) ctx := context.Background() owner := mustUser(t, st, "owner") - a := mustAgent(t, st, owner.ID, "a") + mustAgent(t, st, owner.ID, "a") - // Clause 3: non-existent parent → NotFound. + // A non-existent parent handle misses at resolution → NotFound. _, err := svc.ReparentAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.ReparentAgentRequest{ - AgentAccountId: string(a.ID), - NewParentAgentId: "no-such-agent", + AgentHandle: "a", + NewParentHandle: "no-such-agent", })) connectCodeIs(t, err, connect.CodeNotFound, "missing parent") } @@ -122,11 +185,11 @@ func TestCreateAgentWithParentValidatesAndPersists(t *testing.T) { owner := mustUser(t, st, "owner") parent := mustAgent(t, st, owner.ID, "parent") - // Happy path: a parent the caller's owner owns is accepted and persisted. + // Happy path: a parent the caller's owner owns (bare handle) is accepted. resp, err := svc.CreateAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateAgentRequest{ - Handle: "child", - DisplayName: "Child", - ParentAgentId: string(parent.ID), + Handle: "child", + DisplayName: "Child", + ParentHandle: "parent", })) if err != nil { t.Fatalf("CreateAgent with parent: %v", err) @@ -135,33 +198,34 @@ func TestCreateAgentWithParentValidatesAndPersists(t *testing.T) { t.Fatalf("created child parent = %q, want %q", got, parent.ID) } - // A parent that does not exist → NotFound (clause 3 on the create path). + // A parent that does not exist → NotFound (resolver miss). _, err = svc.CreateAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateAgentRequest{ - Handle: "orphan", - DisplayName: "Orphan", - ParentAgentId: "no-such-agent", + Handle: "orphan", + DisplayName: "Orphan", + ParentHandle: "no-such-agent", })) connectCodeIs(t, err, connect.CodeNotFound, "create with missing parent") - // A parent under a different owner → PermissionDenied (clauses 0/1). + // A parent under a different owner → NOT_FOUND (was PermissionDenied): the + // owner-qualified foreign parent resolves, but the same-owner check is + // remapped to name the submitted handle (DL-269). other := mustUser(t, st, "other") - foreign := mustAgent(t, st, other.ID, "foreign") + mustAgent(t, st, other.ID, "foreign") _, err = svc.CreateAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateAgentRequest{ - Handle: "cross", - DisplayName: "Cross", - ParentAgentId: string(foreign.ID), + Handle: "cross", + DisplayName: "Cross", + ParentHandle: "other/foreign", })) - connectCodeIs(t, err, connect.CodePermissionDenied, "create with cross-owner parent") + connectCodeIs(t, err, connect.CodeNotFound, "create with cross-owner parent") } // TestCreateAgentByAgentCallerResolvesOwner is the RIG-1644 red-green teeth: // agents spawning agents is core product, so an AGENT caller creating a child // under a same-owner parent must be authorized against its resolved USER owner, -// not its own agent id. Pre-fix CreateAgent used the raw caller id both as the -// parent same-owner key (→ spurious PermissionDenied) and as the store owner (→ -// owner_user_id FK rejects a non-user), so this call errored. The child must be -// created and owned by the resolved user owner. The cross-owner case still fails -// closed, proving the resolution did not open a hole. +// not its own agent id, and the bare parent handle must resolve in that owner's +// namespace. The child must be created and owned by the resolved user owner. The +// cross-owner case still fails closed (now NOT_FOUND), proving the resolution did +// not open a hole. func TestCreateAgentByAgentCallerResolvesOwner(t *testing.T) { svc, st := newHandler(t) ctx := context.Background() @@ -170,9 +234,9 @@ func TestCreateAgentByAgentCallerResolvesOwner(t *testing.T) { callerAgent := mustAgent(t, st, owner.ID, "caller") resp, err := svc.CreateAgent(WithActor(ctx, callerAgent.ID), connect.NewRequest(&compassv1.CreateAgentRequest{ - Handle: "child", - DisplayName: "Child", - ParentAgentId: string(parentAgent.ID), + Handle: "child", + DisplayName: "Child", + ParentHandle: "parent", })) if err != nil { t.Fatalf("CreateAgent by agent caller: %v", err) @@ -189,14 +253,15 @@ func TestCreateAgentByAgentCallerResolvesOwner(t *testing.T) { t.Fatalf("child owner = %q, want resolved user owner %q", gotOwner, owner.ID) } - // An agent caller under a DIFFERENT owner still cannot create under this - // parent: resolution maps it to `other`, so the same-owner check denies it. + // An agent caller under a DIFFERENT owner cannot create under this parent: + // its bare `parent` resolves in `other`'s namespace, where no such agent + // exists → NOT_FOUND (resolver miss). other := mustUser(t, st, "other") intruder := mustAgent(t, st, other.ID, "intruder") _, err = svc.CreateAgent(WithActor(ctx, intruder.ID), connect.NewRequest(&compassv1.CreateAgentRequest{ - Handle: "hijack", - DisplayName: "Hijack", - ParentAgentId: string(parentAgent.ID), + Handle: "hijack", + DisplayName: "Hijack", + ParentHandle: "parent", })) - connectCodeIs(t, err, connect.CodePermissionDenied, "cross-owner agent caller") + connectCodeIs(t, err, connect.CodeNotFound, "cross-owner agent caller") } diff --git a/go/internal/comms/channel_policy_pgtest_test.go b/go/internal/comms/channel_policy_pgtest_test.go index aec3e0f8..9e4db35e 100644 --- a/go/internal/comms/channel_policy_pgtest_test.go +++ b/go/internal/comms/channel_policy_pgtest_test.go @@ -36,7 +36,7 @@ func TestSetChannelPolicyUpdatesAndEchoes(t *testing.T) { resp, err := svc.SetChannelPolicy(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.SetChannelPolicyRequest{ ChannelId: chID, PostPolicy: compassv1.ChannelPostPolicy_CHANNEL_POST_POLICY_OWNER_ONLY, - OwnerAccountId: string(owner.ID), + OwnerHandle: owner.Handle, MandatorySubscription: true, })) if err != nil { @@ -65,7 +65,7 @@ func TestPostMessageOwnerOnlyNonOwnerIsNotFound(t *testing.T) { created, err := svc.CreateChannel(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateChannelRequest{ Name: "room", Kind: compassv1.ChannelKind_CHANNEL_KIND_CHANNEL, - MemberAccountIds: []string{string(other.ID)}, + MemberHandles: []string{other.Handle}, })) if err != nil { t.Fatalf("CreateChannel: %v", err) @@ -73,9 +73,9 @@ func TestPostMessageOwnerOnlyNonOwnerIsNotFound(t *testing.T) { chID := created.Msg.GetChannel().GetId() if _, err := svc.SetChannelPolicy(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.SetChannelPolicyRequest{ - ChannelId: chID, - PostPolicy: compassv1.ChannelPostPolicy_CHANNEL_POST_POLICY_OWNER_ONLY, - OwnerAccountId: string(owner.ID), + ChannelId: chID, + PostPolicy: compassv1.ChannelPostPolicy_CHANNEL_POST_POLICY_OWNER_ONLY, + OwnerHandle: owner.Handle, })); err != nil { t.Fatalf("SetChannelPolicy: %v", err) } @@ -98,7 +98,7 @@ func TestUpdateChannelMembersUnsubscribeMandatoryIsInvalidArgument(t *testing.T) created, err := svc.CreateChannel(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateChannelRequest{ Name: "room", Kind: compassv1.ChannelKind_CHANNEL_KIND_CHANNEL, - MemberAccountIds: []string{string(member.ID)}, + MemberHandles: []string{member.Handle}, })) if err != nil { t.Fatalf("CreateChannel: %v", err) @@ -113,8 +113,8 @@ func TestUpdateChannelMembersUnsubscribeMandatoryIsInvalidArgument(t *testing.T) } _, err = svc.UpdateChannelMembers(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.UpdateChannelMembersRequest{ - ChannelId: chID, - UnsubscribeAccountIds: []string{string(member.ID)}, + ChannelId: chID, + UnsubscribeHandles: []string{member.Handle}, })) connectCodeIs(t, err, connect.CodeInvalidArgument, "unsubscribe on mandatory channel") } diff --git a/go/internal/comms/comms.go b/go/internal/comms/comms.go index c3e72ecd..99face38 100644 --- a/go/internal/comms/comms.go +++ b/go/internal/comms/comms.go @@ -18,7 +18,6 @@ package comms import ( "context" "errors" - "fmt" "connectrpc.com/connect" @@ -117,18 +116,25 @@ func (c *Comms) CreateAgent( // authorized correctly, and the store's owner_user_id FK (which requires a // user) is satisfied. // A supplied parent is validated before the account is minted (§Server - // validation, applied on creation too): it must exist (clause 3 → NotFound) - // and belong to the creating caller's owner (clauses 0/1 → PermissionDenied, - // since the creator owns what it creates). Clause 2 (cycle) cannot arise on - // create — a new account has no descendants. - if parent := req.Msg.GetParentAgentId(); parent != "" { - parentOwner, err := c.store.AgentOwner(ctx, store.AccountID(parent)) + // validation, applied on creation too): the parent_handle must resolve to an + // existing agent (clause 3 → NotFound) that belongs to the creating caller's + // owner. Clause 2 (cycle) cannot arise on create — a new account has no + // descendants. Oracle-safe remap (DL-269): a resolved-but-foreign parent + // (an owner-qualified handle naming another owner's agent) is byte-identical + // to an unknown one — NOT_FOUND naming the SUBMITTED handle, never the old + // PermissionDenied "different owner" that leaked the parent's existence. + var parentID store.AccountID + if parent := req.Msg.GetParentHandle(); parent != "" { + parentID, err = c.resolveAgentHandle(ctx, caller, parent) if err != nil { - return nil, edgeError(err) // ErrNotFound → NotFound + return nil, edgeError(err) // resolver miss → NOT_FOUND naming the handle + } + parentOwner, err := c.store.AgentOwner(ctx, parentID) + if err != nil { + return nil, edgeError(notFoundHandle(err, parent)) } if parentOwner != owner { - return nil, connect.NewError(connect.CodePermissionDenied, - fmt.Errorf("parent agent %q has a different owner", parent)) + return nil, edgeError(notFoundHandle(store.ErrNotFound, parent)) } } // Install the coordination emit buffer so the store's in-tx coordination hook @@ -138,7 +144,7 @@ func (c *Comms) CreateAgent( acc, err := c.store.CreateAgent(ctx, owner, store.NewAgent{ Handle: req.Msg.GetHandle(), DisplayName: req.Msg.GetDisplayName(), - ParentAgentID: store.AccountID(req.Msg.GetParentAgentId()), + ParentAgentID: parentID, }) if err != nil { return nil, edgeError(err) @@ -221,11 +227,16 @@ func (c *Comms) CreateChannel( ctx context.Context, req *connect.Request[compassv1.CreateChannelRequest], ) (*connect.Response[compassv1.CreateChannelResponse], error) { - ch, err := c.store.CreateChannel(ctx, c.actorFromContext(ctx), store.NewChannel{ + caller := c.actorFromContext(ctx) + members, err := c.resolveHandles(ctx, caller, req.Msg.GetMemberHandles()) + if err != nil { + return nil, edgeError(err) + } + ch, err := c.store.CreateChannel(ctx, caller, store.NewChannel{ Name: req.Msg.GetName(), GroupID: store.ChannelGroupID(req.Msg.GetGroupId()), Kind: channelKindFromWire(req.Msg.GetKind()), - MemberAccountIDs: accountIDsFromWire(req.Msg.GetMemberAccountIds()), + MemberAccountIDs: members, }) if err != nil { return nil, edgeError(err) @@ -243,11 +254,16 @@ func (c *Comms) UpdateChannelMembers( ctx context.Context, req *connect.Request[compassv1.UpdateChannelMembersRequest], ) (*connect.Response[compassv1.UpdateChannelMembersResponse], error) { + caller := c.actorFromContext(ctx) + updates, err := c.memberUpdatesFromWire(ctx, caller, req.Msg) + if err != nil { + return nil, edgeError(err) + } ch, removed, err := c.store.UpdateChannelMembers( ctx, - c.actorFromContext(ctx), + caller, store.ChannelID(req.Msg.GetChannelId()), - memberUpdatesFromWire(req.Msg), + updates, ) if err != nil { return nil, edgeError(err) @@ -271,13 +287,54 @@ func (c *Comms) ReparentAgent( // (fired inside ReparentAgent for both the new and old managers) records its // channel changes here; drained + emitted post-commit below (SEA-1722 T5). ctx, coordChanges := withCoordChanges(ctx) + caller := c.actorFromContext(ctx) + agentID, err := c.resolveAgentHandle(ctx, caller, req.Msg.GetAgentHandle()) + if err != nil { + return nil, edgeError(err) + } + // new_parent_handle empty ⇒ promote to root (no parent to resolve). + var newParentID store.AccountID + if h := req.Msg.GetNewParentHandle(); h != "" { + newParentID, err = c.resolveAgentHandle(ctx, caller, h) + if err != nil { + return nil, edgeError(err) + } + // Oracle-safe remap (DL-269), mirroring CreateAgent's parent pre-check: + // a resolved-but-foreign parent (an owner-qualified handle naming another + // owner's agent) must be byte-identical to an unknown one. Resolve the + // caller's owner and reject a foreign parent HERE, naming the SUBMITTED + // new_parent_handle — otherwise the store's clause-1 ErrPermissionDenied + // ("parent agent %q has a different owner") gets re-keyed below to name + // the AGENT handle, which differs from the unknown-parent NOT_FOUND + // (named with the parent handle) and leaks the parent's existence. + owner, err := c.store.ResolveOwner(ctx, caller) + if err != nil { + return nil, edgeError(err) + } + parentOwner, err := c.store.AgentOwner(ctx, newParentID) + if err != nil { + return nil, edgeError(notFoundHandle(err, h)) + } + if parentOwner != owner { + return nil, edgeError(notFoundHandle(store.ErrNotFound, h)) + } + } acc, err := c.store.ReparentAgent( ctx, - c.actorFromContext(ctx), - store.AccountID(req.Msg.GetAgentAccountId()), - store.AccountID(req.Msg.GetNewParentAgentId()), + caller, + agentID, + newParentID, ) if err != nil { + // Oracle-safe remap (DL-269): the store's clause-0 authority failure + // (ErrPermissionDenied "caller may not re-parent agent %q") on a resolved + // but foreign agent must be byte-identical to the unknown-handle + // NOT_FOUND — a real-but-foreign handle is guessable, so it cannot leak a + // distinct code/message. Re-key it to NOT_FOUND naming the SUBMITTED + // agent handle, never the resolved id. + if errors.Is(err, store.ErrPermissionDenied) { + return nil, edgeError(notFoundHandle(store.ErrNotFound, req.Msg.GetAgentHandle())) + } return nil, edgeError(err) } c.publishAccountChanged(acc) @@ -294,14 +351,22 @@ func (c *Comms) OpenAgentWorkspace( ctx context.Context, req *connect.Request[compassv1.OpenAgentWorkspaceRequest], ) (*connect.Response[compassv1.OpenAgentWorkspaceResponse], error) { - ws, err := c.store.OpenAgentWorkspace( - ctx, - c.actorFromContext(ctx), - store.AccountID(req.Msg.GetAgentAccountId()), - ) + caller := c.actorFromContext(ctx) + agentID, err := c.resolveAgentHandle(ctx, caller, req.Msg.GetAgentHandle()) if err != nil { return nil, edgeError(err) } + ws, err := c.store.OpenAgentWorkspace(ctx, caller, agentID) + if err != nil { + // The store names the resolved ACCOUNT ID in its NOT_FOUND (`agent %q`) + // and edgeError maps store errors verbatim — leaking the resolved id of + // an invisible target. Re-key to name the SUBMITTED handle (DL-269), so + // an invisible/unknown/foreign target is byte-identical. + if errors.Is(err, store.ErrNotFound) { + return nil, edgeError(notFoundHandle(store.ErrNotFound, req.Msg.GetAgentHandle())) + } + return nil, edgeError(err) + } c.publishAgentWorkspaceChanged(ws) return connect.NewResponse(&compassv1.OpenAgentWorkspaceResponse{Workspace: workspaceToWire(ws)}), nil } @@ -467,13 +532,25 @@ func (c *Comms) SetChannelPolicy( ctx context.Context, req *connect.Request[compassv1.SetChannelPolicyRequest], ) (*connect.Response[compassv1.SetChannelPolicyResponse], error) { + caller := c.actorFromContext(ctx) + // owner_handle empty ⇒ the channel is left unowned (no resolution). A + // non-empty owner_handle names a user OR agent, so it resolves through the + // general batch resolver; a miss is NOT_FOUND naming the submitted handle. + var ownerID store.AccountID + if h := req.Msg.GetOwnerHandle(); h != "" { + ids, err := c.resolveHandles(ctx, caller, []string{h}) + if err != nil { + return nil, edgeError(err) + } + ownerID = ids[0] + } ch, err := c.store.SetChannelPolicy( ctx, - c.actorFromContext(ctx), + caller, store.ChannelID(req.Msg.GetChannelId()), store.ChannelPolicy{ PostPolicy: channelPostPolicyFromWire(req.Msg.GetPostPolicy()), - OwnerAccountID: store.AccountID(req.Msg.GetOwnerAccountId()), + OwnerAccountID: ownerID, MandatorySubscription: req.Msg.GetMandatorySubscription(), }, ) @@ -492,7 +569,7 @@ func (c *Comms) GetRoster( ctx context.Context, req *connect.Request[compassv1.GetRosterRequest], ) (*connect.Response[compassv1.GetRosterResponse], error) { - entries, err := c.roster(ctx, c.actorFromContext(ctx), req.Msg.GetAgentAccountId(), req.Msg.GetScope()) + entries, err := c.roster(ctx, c.actorFromContext(ctx), req.Msg.GetVantageHandle(), req.Msg.GetScope()) if err != nil { return nil, err } diff --git a/go/internal/comms/comms_test.go b/go/internal/comms/comms_test.go index 22c3d520..dc6f2ad7 100644 --- a/go/internal/comms/comms_test.go +++ b/go/internal/comms/comms_test.go @@ -12,6 +12,7 @@ package comms import ( "context" + "strings" "testing" "connectrpc.com/connect" @@ -106,8 +107,8 @@ func TestMembershipTiersJoinVersusSubscribe(t *testing.T) { // Join tier: add as a member WITHOUT subscribing -> read access // (MemberAccountIDs) but no push (absent from SubscriberAccountIDs). joined, err := svc.UpdateChannelMembers(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.UpdateChannelMembersRequest{ - ChannelId: chID, - AddMemberAccountIds: []string{string(newcomer.ID)}, + ChannelId: chID, + AddMemberHandles: []string{newcomer.Handle}, })) if err != nil { t.Fatalf("UpdateChannelMembers(join): %v", err) @@ -122,8 +123,8 @@ func TestMembershipTiersJoinVersusSubscribe(t *testing.T) { // Subscribe tier: a subscribe toggle flips the per-member boolean, so the // member now appears in SubscriberAccountIDs too. subbed, err := svc.UpdateChannelMembers(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.UpdateChannelMembersRequest{ - ChannelId: chID, - SubscribeAccountIds: []string{string(newcomer.ID)}, + ChannelId: chID, + SubscribeHandles: []string{newcomer.Handle}, })) if err != nil { t.Fatalf("UpdateChannelMembers(subscribe): %v", err) @@ -134,8 +135,8 @@ func TestMembershipTiersJoinVersusSubscribe(t *testing.T) { // Unsubscribe flips it back off while keeping membership. unsubbed, err := svc.UpdateChannelMembers(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.UpdateChannelMembersRequest{ - ChannelId: chID, - UnsubscribeAccountIds: []string{string(newcomer.ID)}, + ChannelId: chID, + UnsubscribeHandles: []string{newcomer.Handle}, })) if err != nil { t.Fatalf("UpdateChannelMembers(unsubscribe): %v", err) @@ -148,6 +149,65 @@ func TestMembershipTiersJoinVersusSubscribe(t *testing.T) { } } +// TestUpdateChannelMembersUnknownHandleIsNotFound: adding an unknown member +// handle to a REAL channel the caller owns fails atomically (OQ-2) with +// CodeNotFound, and the error names the submitted handle — the resolveHandles +// miss leg, distinct from the unknown-channel miss in TestEdgeErrorMapping. +func TestUpdateChannelMembersUnknownHandleIsNotFound(t *testing.T) { + svc, st := newHandler(t) + ctx := context.Background() + owner := mustUser(t, st, "owner") + + created, err := svc.CreateChannel(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateChannelRequest{ + Name: "room", Kind: compassv1.ChannelKind_CHANNEL_KIND_CHANNEL, + })) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + chID := created.Msg.GetChannel().GetId() + + _, addErr := svc.UpdateChannelMembers(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.UpdateChannelMembersRequest{ + ChannelId: chID, + AddMemberHandles: []string{"ghost"}, + })) + connectCodeIs(t, addErr, connect.CodeNotFound, "add an unknown member handle") + if addErr == nil || !strings.Contains(addErr.Error(), "ghost") { + t.Fatalf("error %v must name the submitted handle (ghost)", addErr) + } +} + +// TestUpdateChannelMembersNamesMissesAcrossAllLists: OQ-2 completeness — an +// unresolved handle in a LATER list (subscribe) must be named alongside one in +// an earlier list (add), not swallowed. Before the one-pass resolution the four +// lists resolved sequentially and the error named only the first failing list's +// misses; the design ruling (design.md §OQ-2) requires the error name ALL +// unresolved handles across every list. +func TestUpdateChannelMembersNamesMissesAcrossAllLists(t *testing.T) { + svc, st := newHandler(t) + ctx := context.Background() + owner := mustUser(t, st, "owner") + + created, err := svc.CreateChannel(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateChannelRequest{ + Name: "room", Kind: compassv1.ChannelKind_CHANNEL_KIND_CHANNEL, + })) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + chID := created.Msg.GetChannel().GetId() + + // ghost-add is in the FIRST list, ghost-sub in a LATER one. Both must be + // named — the failure must not stop at the first list. + _, updErr := svc.UpdateChannelMembers(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.UpdateChannelMembersRequest{ + ChannelId: chID, + AddMemberHandles: []string{"ghost-add"}, + SubscribeHandles: []string{"ghost-sub"}, + })) + connectCodeIs(t, updErr, connect.CodeNotFound, "misses across add + subscribe lists") + if updErr == nil || !strings.Contains(updErr.Error(), "ghost-add") || !strings.Contains(updErr.Error(), "ghost-sub") { + t.Fatalf("error %v must name EVERY unresolved handle across all lists (ghost-add AND ghost-sub)", updErr) + } +} + func TestSearchMessagesAuthorizationScoped(t *testing.T) { svc, st := newHandler(t) ctx := context.Background() @@ -763,8 +823,8 @@ func TestEdgeErrorMapping(t *testing.T) { // ErrNotFound -> CodeNotFound: mutating an unknown channel. _, nfErr := svc.UpdateChannelMembers(ctx, connect.NewRequest(&compassv1.UpdateChannelMembersRequest{ - ChannelId: "ghost-channel", - AddMemberAccountIds: []string{"whoever"}, + ChannelId: "ghost-channel", + AddMemberHandles: []string{"whoever"}, })) connectCodeIs(t, nfErr, connect.CodeNotFound, "update members of an unknown channel") } diff --git a/go/internal/comms/coordination_pgtest_test.go b/go/internal/comms/coordination_pgtest_test.go index 23922971..ac334e92 100644 --- a/go/internal/comms/coordination_pgtest_test.go +++ b/go/internal/comms/coordination_pgtest_test.go @@ -56,7 +56,7 @@ func TestCreateAgentWithParentProvisionsCoordinationChannel(t *testing.T) { // A subscriber (the owner) draining the stream will see the coordination // ChannelChanged emitted after the report's create commits. _, err := h.svc.CreateAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateAgentRequest{ - Handle: "report1", DisplayName: "r1", ParentAgentId: string(manager.ID), + Handle: "report1", DisplayName: "r1", ParentHandle: manager.Handle, })) if err != nil { t.Fatalf("CreateAgent(report1): %v", err) @@ -97,7 +97,7 @@ func TestCreateAgentByAgentCallerProvisionsCoordinationChannel(t *testing.T) { // The agent caller (not the owning user) spawns the report under the manager. if _, err := h.svc.CreateAgent(WithActor(ctx, caller.ID), connect.NewRequest(&compassv1.CreateAgentRequest{ - Handle: "report1", DisplayName: "r1", ParentAgentId: string(manager.ID), + Handle: "report1", DisplayName: "r1", ParentHandle: manager.Handle, })); err != nil { t.Fatalf("CreateAgent by agent caller: %v", err) } @@ -138,8 +138,8 @@ func TestReparentInEmitsMembershipMove(t *testing.T) { } if _, err := h.svc.ReparentAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.ReparentAgentRequest{ - AgentAccountId: string(report.ID), - NewParentAgentId: string(mgrB.ID), + AgentHandle: report.Handle, + NewParentHandle: mgrB.Handle, })); err != nil { t.Fatalf("ReparentAgent: %v", err) } @@ -241,7 +241,7 @@ func TestCreateAgentSuffixesAroundUserChannelWithoutWedge(t *testing.T) { // The real RPC that fires the hook: a first report under the manager. if _, err := h.svc.CreateAgent(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateAgentRequest{ - Handle: "report", DisplayName: "r", ParentAgentId: string(manager.ID), + Handle: "report", DisplayName: "r", ParentHandle: manager.Handle, })); err != nil { // (b) The parent-edge write must NOT be wedged by the name collision. t.Fatalf("CreateAgent(report) wedged by coordination collision: %v", err) diff --git a/go/internal/comms/mapping.go b/go/internal/comms/mapping.go index b4b6f7d2..1288ec9b 100644 --- a/go/internal/comms/mapping.go +++ b/go/internal/comms/mapping.go @@ -1,6 +1,8 @@ package comms import ( + "context" + "connectrpc.com/connect" compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" @@ -261,60 +263,80 @@ func channelKindFromWire(k compassv1.ChannelKind) store.ChannelKind { } } -func accountIDsFromWire(ids []string) []store.AccountID { - if len(ids) == 0 { - return nil - } - out := make([]store.AccountID, len(ids)) - for i, id := range ids { - out[i] = store.AccountID(id) - } - return out -} +// memberUpdatesFromWire resolves the UpdateChannelMembers request's four +// parallel HANDLE lists (add / remove / subscribe / unsubscribe) to account ids, +// then collapses them into the store's per-member MemberUpdate set (RT-1). A +// removed member is one update with Remove; an added member and a +// subscribe-toggle for an existing member merge onto the same MemberUpdate so +// one member never yields two conflicting rows. +// +// Merge is keyed POST-resolution (by resolved account id), so two distinct +// spellings of the SAME handle (e.g. `matt/ux` and a bare `ux` from one of +// matt's agents) collapse onto one MemberUpdate rather than yielding two +// conflicting rows. Resolution is ATOMIC across all four lists (OQ-2): the four +// lists resolve in ONE store call, so any unresolved handle fails the whole +// request with NOT_FOUND naming EVERY unresolved handle across all four lists in +// its submitted spelling — not just the first failing list's misses. No store +// mutation runs unless every handle in every list resolved. All four lists +// resolve in the caller's namespace/visibility scope. +func (c *Comms) memberUpdatesFromWire(ctx context.Context, caller store.AccountID, req *compassv1.UpdateChannelMembersRequest) ([]store.MemberUpdate, error) { + addH := req.GetAddMemberHandles() + subscribeH := req.GetSubscribeHandles() + unsubscribeH := req.GetUnsubscribeHandles() + removeH := req.GetRemoveMemberHandles() + + // Resolve all four lists in one AccountsByHandles call so the OQ-2 error + // names every unresolved handle across every list, not just the first list + // with a miss. resolveHandles preserves submitted order, so each list's ids + // are sliced back out by offset. + combined := make([]string, 0, len(addH)+len(subscribeH)+len(unsubscribeH)+len(removeH)) + combined = append(combined, addH...) + combined = append(combined, subscribeH...) + combined = append(combined, unsubscribeH...) + combined = append(combined, removeH...) + resolved, err := c.resolveHandles(ctx, caller, combined) + if err != nil { + return nil, err + } + i := 0 + next := func(n int) []store.AccountID { s := resolved[i : i+n]; i += n; return s } + add := next(len(addH)) + subscribe := next(len(subscribeH)) + unsubscribe := next(len(unsubscribeH)) + remove := next(len(removeH)) -// memberUpdatesFromWire collapses the UpdateChannelMembers request's four -// parallel lists (add / remove / subscribe / unsubscribe) into the store's -// per-member MemberUpdate set (RT-1). A removed member is one update with -// Remove; an added member and a subscribe-toggle for an existing member merge -// onto the same MemberUpdate so one member never yields two conflicting rows. -func memberUpdatesFromWire(req *compassv1.UpdateChannelMembersRequest) []store.MemberUpdate { byID := make(map[store.AccountID]*store.MemberUpdate) - upd := func(id store.AccountID) *store.MemberUpdate { + order := make([]store.AccountID, 0) + touch := func(id store.AccountID) *store.MemberUpdate { if u, ok := byID[id]; ok { return u } u := &store.MemberUpdate{AccountID: id} byID[id] = u + order = append(order, id) return u } - order := make([]store.AccountID, 0) - touch := func(id store.AccountID) *store.MemberUpdate { - if _, seen := byID[id]; !seen { - order = append(order, id) - } - return upd(id) - } - for _, id := range req.GetAddMemberAccountIds() { - touch(store.AccountID(id)) + for _, id := range add { + touch(id) } - for _, id := range req.GetSubscribeAccountIds() { - touch(store.AccountID(id)).Subscribed = true + for _, id := range subscribe { + touch(id).Subscribed = true } - for _, id := range req.GetUnsubscribeAccountIds() { - u := touch(store.AccountID(id)) + for _, id := range unsubscribe { + u := touch(id) u.Subscribed = false u.Unsubscribe = true } - for _, id := range req.GetRemoveMemberAccountIds() { - touch(store.AccountID(id)).Remove = true + for _, id := range remove { + touch(id).Remove = true } out := make([]store.MemberUpdate, len(order)) for i, id := range order { out[i] = *byID[id] } - return out + return out, nil } // blocksFromWire maps wire message blocks onto store blocks, rejecting an empty diff --git a/go/internal/comms/pinned_board_pgtest_test.go b/go/internal/comms/pinned_board_pgtest_test.go index 21516c16..f54ce940 100644 --- a/go/internal/comms/pinned_board_pgtest_test.go +++ b/go/internal/comms/pinned_board_pgtest_test.go @@ -169,7 +169,7 @@ func TestUpdatePinnedBoardNonOwnerOnOwnerOnlyIsNotFound(t *testing.T) { created, err := svc.CreateChannel(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateChannelRequest{ Name: "room", Kind: compassv1.ChannelKind_CHANNEL_KIND_CHANNEL, - MemberAccountIds: []string{string(other.ID)}, + MemberHandles: []string{other.Handle}, })) if err != nil { t.Fatalf("CreateChannel: %v", err) @@ -179,9 +179,9 @@ func TestUpdatePinnedBoardNonOwnerOnOwnerOnlyIsNotFound(t *testing.T) { msg := pinnableMessage(t, st, store.ChannelID(chID), owner.ID, "target") if _, err := svc.SetChannelPolicy(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.SetChannelPolicyRequest{ - ChannelId: chID, - PostPolicy: compassv1.ChannelPostPolicy_CHANNEL_POST_POLICY_OWNER_ONLY, - OwnerAccountId: string(owner.ID), + ChannelId: chID, + PostPolicy: compassv1.ChannelPostPolicy_CHANNEL_POST_POLICY_OWNER_ONLY, + OwnerHandle: owner.Handle, })); err != nil { t.Fatalf("SetChannelPolicy: %v", err) } @@ -341,7 +341,7 @@ func TestUpdatePinnedBoardOwnerOnlyNonOwnerInTxIsNotFound(t *testing.T) { created, err := svc.CreateChannel(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateChannelRequest{ Name: "room", Kind: compassv1.ChannelKind_CHANNEL_KIND_CHANNEL, - MemberAccountIds: []string{string(other.ID)}, + MemberHandles: []string{other.Handle}, })) if err != nil { t.Fatalf("CreateChannel: %v", err) @@ -350,9 +350,9 @@ func TestUpdatePinnedBoardOwnerOnlyNonOwnerInTxIsNotFound(t *testing.T) { msg := pinnableMessage(t, st, store.ChannelID(chID), owner.ID, "target") if _, err := svc.SetChannelPolicy(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.SetChannelPolicyRequest{ - ChannelId: chID, - PostPolicy: compassv1.ChannelPostPolicy_CHANNEL_POST_POLICY_OWNER_ONLY, - OwnerAccountId: string(owner.ID), + ChannelId: chID, + PostPolicy: compassv1.ChannelPostPolicy_CHANNEL_POST_POLICY_OWNER_ONLY, + OwnerHandle: owner.Handle, })); err != nil { t.Fatalf("SetChannelPolicy: %v", err) } diff --git a/go/internal/comms/resolve.go b/go/internal/comms/resolve.go new file mode 100644 index 00000000..f46dceb2 --- /dev/null +++ b/go/internal/comms/resolve.go @@ -0,0 +1,140 @@ +package comms + +import ( + "context" + "errors" + "fmt" + + "github.com/RigelBuild/compass/go/internal/store" +) + +// Handle resolution at the service edge (RIG-2751 handle cutover): every +// request-input account field carries a `@handle`, and the server resolves it to +// an account id here — the store layer stays id-typed. A resolver miss flows +// through edgeError as an in-band NOT_FOUND (never a transport teardown). +// +// Two resolution shapes, per §"Where resolution lives": +// - Member/owner fields (resolveHandles → store.AccountsByHandles) name users +// as well as agents and ARE visibility-scoped (OQ-6 SCOPED): the viewer is +// the caller, so an invisible handle misses like an unknown one. +// - Singular agent fields (resolveAgentHandle → store.AgentByHandle) are +// owner-namespaced but NOT viewer-scoped — an invisible-but-real agent in the +// resolution owner's namespace still resolves. The one exception is the +// roster vantage, which layers its OWN visibility check on top to close the +// vantage-probe oracle (resolveVisibleAgentHandle). + +// resolveHandles resolves a batch of member/owner handles (which legitimately +// name users as well as agents) to their account ids, in the caller's own +// namespace and visibility scope. It is ATOMIC (OQ-2): any unresolved handle +// fails the whole call with store.ErrNotFound naming EVERY unresolved handle in +// its submitted spelling — the caller maps that through edgeError to +// CodeNotFound. Order is preserved: the returned ids follow the submitted order, +// so a caller that also needs per-input identity (memberUpdatesFromWire) can zip +// them back. Empty input is a no-op (nil, nil). +func (c *Comms) resolveHandles(ctx context.Context, caller store.AccountID, handles []string) ([]store.AccountID, error) { + if len(handles) == 0 { + return nil, nil + } + callerOwner, err := c.store.ResolveOwner(ctx, caller) + if err != nil { + return nil, err + } + parsed := make([]store.QualifiedHandle, len(handles)) + for i, h := range handles { + parsed[i] = store.ParseQualifiedHandle(h) + } + // viewer = caller (OQ-6 visibility scope); callerOwner = the bare-agent + // default namespace. + hits, err := c.store.AccountsByHandles(ctx, caller, callerOwner, parsed) + if err != nil { + return nil, err + } + out := make([]store.AccountID, len(handles)) + for i, h := range handles { + out[i] = hits[h] + } + return out, nil +} + +// resolveAgentHandle resolves a singular agent handle (owner-qualified or bare) +// to its agent account id, in the caller's own owner namespace for a bare +// handle. Owner-namespaced but NOT viewer-scoped (§GetRoster's dual vantage: an +// invisible-but-real agent in the resolution owner's namespace still resolves) — +// the roster vantage adds its own visibility check separately. An unknown, +// wrong-owner, or non-agent handle is store.ErrNotFound naming the submitted +// handle (edgeError → CodeNotFound), the oracle-safe merge every handle-addressed +// agent target holds. +func (c *Comms) resolveAgentHandle(ctx context.Context, caller store.AccountID, handle string) (store.AccountID, error) { + acc, err := c.resolveAgentAccount(ctx, caller, handle) + if err != nil { + return "", err + } + return acc.ID, nil +} + +// resolveAgentAccount is resolveAgentHandle's account-returning form. It parses +// the owner qualifier, resolves the owner namespace (bare → caller's own owner, +// qualified → the named user in the global index), then the agent under it via +// store.AgentByHandle. The submitted handle — never the resolved id — names the +// NOT_FOUND. NOT viewer-scoped (see resolveAgentHandle). +func (c *Comms) resolveAgentAccount(ctx context.Context, caller store.AccountID, handle string) (store.Account, error) { + qh := store.ParseQualifiedHandle(handle) + owner, err := c.agentOwnerNamespace(ctx, caller, qh) + if err != nil { + return store.Account{}, notFoundHandle(err, qh.Raw) + } + acc, err := c.store.AgentByHandle(ctx, owner, qh.Handle) + if err != nil { + // Re-key the message to name the SUBMITTED handle (qh.Raw), never the + // resolved owner id or the store's bare-handle spelling — oracle-safe. + return store.Account{}, notFoundHandle(err, qh.Raw) + } + return acc, nil +} + +// resolveVisibleAgentHandle is resolveAgentAccount plus a caller-visibility +// check — the roster vantage's DEFINED error posture (§GetRoster's dual vantage): +// a real-but-caller-invisible vantage maps to the SAME NOT_FOUND an unknown +// handle gets, closing the NOT_FOUND-vs-empty-success vantage-probe oracle. The +// submitted handle names the miss, never the resolved id. +func (c *Comms) resolveVisibleAgentHandle(ctx context.Context, caller store.AccountID, handle string) (store.AccountID, error) { + acc, err := c.resolveAgentAccount(ctx, caller, handle) + if err != nil { + return "", err + } + visible, err := c.store.AccountVisibleTo(ctx, caller, acc.ID) + if err != nil { + return "", err + } + if !visible { + return "", notFoundHandle(store.ErrNotFound, store.ParseQualifiedHandle(handle).Raw) + } + return acc.ID, nil +} + +// agentOwnerNamespace resolves the owner-user id an agent handle is looked up +// under: the caller's own owner for a bare handle, or the named user (global +// index, users are globally unique) for an owner-qualified handle. An owner +// qualifier that resolves to nothing is store.ErrNotFound (indistinguishable +// from an unknown agent once notFoundHandle re-keys it to the submitted handle). +func (c *Comms) agentOwnerNamespace(ctx context.Context, caller store.AccountID, qh store.QualifiedHandle) (store.AccountID, error) { + if qh.Owner == "" { + return c.store.ResolveOwner(ctx, caller) + } + owner, err := c.store.UserByHandle(ctx, qh.Owner) + if err != nil { + return "", err + } + return owner.ID, nil +} + +// notFoundHandle re-keys a store error to name the submitted handle when it is a +// not-found, preserving the store.ErrNotFound sentinel so edgeError maps it to +// CodeNotFound and matching AgentByHandle's `handle %q` template. A non-not-found +// error (a real query fault) passes through unchanged. +func notFoundHandle(err error, handle string) error { + if errors.Is(err, store.ErrNotFound) { + return fmt.Errorf("%w: handle %q", store.ErrNotFound, handle) + } + return err +} diff --git a/go/internal/comms/roster.go b/go/internal/comms/roster.go index f4d82eb9..914cb336 100644 --- a/go/internal/comms/roster.go +++ b/go/internal/comms/roster.go @@ -21,18 +21,26 @@ import ( // even one structurally in the vantage's tree. Tree order (by account id) is // preserved. // -// vantage defaults to the caller when agent_account_id is empty — an agent caller -// is session-resolved to itself (actorFromContext), a human/UI caller names a -// vantage explicitly. +// vantage defaults to the caller when the vantage handle is empty — an agent +// caller is session-resolved to itself (actorFromContext), a human/UI caller +// names a vantage explicitly. A non-empty vantageHandle is a `@handle` the +// server resolves via resolveAgentAccount: unknown → NOT_FOUND, and (roster's +// DEFINED error posture, not inherited) a real-but-caller-invisible vantage maps +// to the SAME NOT_FOUND an unknown handle gets — closing the +// NOT_FOUND-vs-empty-success vantage-probe oracle. func (c *Comms) roster( ctx context.Context, caller store.AccountID, - vantageID string, + vantageHandle string, scope compassv1.RosterScope, ) ([]*compassv1.RosterEntry, error) { - vantage := store.AccountID(vantageID) - if vantage == "" { - vantage = caller + vantage := caller + if vantageHandle != "" { + id, err := c.resolveVisibleAgentHandle(ctx, caller, vantageHandle) + if err != nil { + return nil, edgeError(err) + } + vantage = id } tree, err := c.treeForScope(ctx, vantage, scope) @@ -172,7 +180,7 @@ func (c *Comms) RosterAsAccount( if account == "" { return nil, errNoActor } - entries, err := c.roster(ctx, account, req.GetAgentAccountId(), req.GetScope()) + entries, err := c.roster(ctx, account, req.GetVantageHandle(), req.GetScope()) if err != nil { return nil, err } diff --git a/go/internal/comms/roster_pgtest_test.go b/go/internal/comms/roster_pgtest_test.go index f79398df..f189fb74 100644 --- a/go/internal/comms/roster_pgtest_test.go +++ b/go/internal/comms/roster_pgtest_test.go @@ -74,8 +74,8 @@ func TestGetRosterNeighborhoodScope(t *testing.T) { niece := mustChildAgent(t, st, owner.ID, "niece", sib.ID) // NOT in mid's neighborhood resp, err := svc.GetRoster(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.GetRosterRequest{ - Scope: compassv1.RosterScope_ROSTER_SCOPE_NEIGHBORHOOD, - AgentAccountId: string(mid.ID), + Scope: compassv1.RosterScope_ROSTER_SCOPE_NEIGHBORHOOD, + VantageHandle: mid.Handle, })) if err != nil { t.Fatalf("GetRoster(neighborhood): %v", err) @@ -105,8 +105,8 @@ func TestGetRosterSubtreeScope(t *testing.T) { grand := mustChildAgent(t, st, owner.ID, "grand", child.ID) resp, err := svc.GetRoster(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.GetRosterRequest{ - Scope: compassv1.RosterScope_ROSTER_SCOPE_SUBTREE, - AgentAccountId: string(mid.ID), + Scope: compassv1.RosterScope_ROSTER_SCOPE_SUBTREE, + VantageHandle: mid.Handle, })) if err != nil { t.Fatalf("GetRoster(subtree): %v", err) @@ -138,8 +138,8 @@ func TestGetRosterOwnerScope(t *testing.T) { foreign := mustAgent(t, st, other.ID, "foreign") resp, err := svc.GetRoster(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.GetRosterRequest{ - Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, - AgentAccountId: string(a1.ID), + Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, + VantageHandle: a1.Handle, })) if err != nil { t.Fatalf("GetRoster(owner): %v", err) @@ -155,34 +155,31 @@ func TestGetRosterOwnerScope(t *testing.T) { } } -// TestGetRosterClipsNonVisibleAgent (D9): an agent structurally in the vantage's -// OWNER tree but NOT visible to the CALLER never appears. The caller is a -// different owner's agent that shares no channel with the vantage's agents, so -// the account-visibility clip drops them even though they are in the requested -// tree. This is the security-critical clause: the raw tree read is unscoped, and -// the handler must intersect with the caller's visible set. -func TestGetRosterClipsNonVisibleAgent(t *testing.T) { +// TestGetRosterInvisibleVantageNotFound (D9 + DL-269): naming a vantage the +// CALLER cannot see is NOT_FOUND, not a clipped/empty SUCCESS. The record DEFINES +// this posture (it is not inherited): a bogus vantage and a real-but-invisible +// vantage must be byte-identical, or the NOT_FOUND-vs-empty-success split is a +// vantage-probe oracle. The caller is an agent owned by a DIFFERENT user; it +// owner-qualifies the victim into its real owner's namespace (`owner/victim`), +// so AgentByHandle RESOLVES it (not viewer-scoped) — and the handler's own +// visibility check then maps it to the same NOT_FOUND an unknown handle gets. +func TestGetRosterInvisibleVantageNotFound(t *testing.T) { svc, st := newHandler(t) ctx := context.Background() owner := mustUser(t, st, "owner") intruderOwner := mustUser(t, st, "intruder-owner") victim := mustAgent(t, st, owner.ID, "victim") + _ = victim // The caller is an agent owned by a DIFFERENT user, sharing no channel with // victim: victim is not visible to it (not owned, no shared channel). intruder := mustAgent(t, st, intruderOwner.ID, "intruder") - resp, err := svc.GetRoster(WithActor(ctx, intruder.ID), connect.NewRequest(&compassv1.GetRosterRequest{ - Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, - AgentAccountId: string(victim.ID), // vantage in victim's tree + _, err := svc.GetRoster(WithActor(ctx, intruder.ID), connect.NewRequest(&compassv1.GetRosterRequest{ + Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, + VantageHandle: "owner/victim", // resolves (not viewer-scoped), then invisibility → NOT_FOUND })) - if err != nil { - t.Fatalf("GetRoster(clip): %v", err) - } - got := rosterByID(resp.Msg.GetEntries()) - if _, ok := got[string(victim.ID)]; ok { - t.Fatalf("D9 breach: caller %q saw non-visible agent %q in the roster", intruder.ID, victim.ID) - } + connectCodeIs(t, err, connect.CodeNotFound, "invisible vantage → NOT_FOUND, not empty success") } // TestGetRosterOfflineDefaultAndPresenceJoin: an agent present in the presence @@ -201,8 +198,8 @@ func TestGetRosterOfflineDefaultAndPresenceJoin(t *testing.T) { }}) resp, err := svc.GetRoster(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.GetRosterRequest{ - Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, - AgentAccountId: string(working.ID), + Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, + VantageHandle: working.Handle, })) if err != nil { t.Fatalf("GetRoster(presence): %v", err) @@ -216,6 +213,34 @@ func TestGetRosterOfflineDefaultAndPresenceJoin(t *testing.T) { } } +// TestGetRosterEmptyVantageDefaultsToAgentCaller: an agent caller that names NO +// vantage is session-resolved to itself (the "my roster" path) — the vantage +// defaults to the caller, not to an error. Regression teeth: a resolver that +// treated the empty handle as an unknown `@handle` would return NOT_FOUND before +// this call could ever succeed. The caller always sees ITSELF, so its own entry +// appearing (with no error) is the proof the vantage resolved to the caller +// (descendant visibility is agent-caller-scoped by D9 and tested elsewhere). +func TestGetRosterEmptyVantageDefaultsToAgentCaller(t *testing.T) { + svc, st := newHandler(t) + ctx := context.Background() + owner := mustUser(t, st, "owner") + parent := mustAgent(t, st, owner.ID, "parent") + _ = mustChildAgent(t, st, owner.ID, "child", parent.ID) + + // parent calls with an EMPTY vantage → its own subtree, rooted at itself. + resp, err := svc.GetRoster(WithActor(ctx, parent.ID), connect.NewRequest(&compassv1.GetRosterRequest{ + Scope: compassv1.RosterScope_ROSTER_SCOPE_SUBTREE, + // VantageHandle deliberately empty. + })) + if err != nil { + t.Fatalf("GetRoster(empty vantage, agent caller): %v", err) + } + got := rosterByID(resp.Msg.GetEntries()) + if _, ok := got[string(parent.ID)]; !ok { + t.Fatalf("empty-vantage subtree must root at the caller itself; got %v", got) + } +} + // TestGetRosterActivityRoundTripsThroughDurableStore: SetActivity → GetRoster // surfaces the durable activity string + timestamp; an agent with no row reports // empty activity. @@ -233,8 +258,8 @@ func TestGetRosterActivityRoundTripsThroughDurableStore(t *testing.T) { } resp, err := svc.GetRoster(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.GetRosterRequest{ - Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, - AgentAccountId: string(busy.ID), + Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, + VantageHandle: busy.Handle, })) if err != nil { t.Fatalf("GetRoster(activity): %v", err) @@ -277,8 +302,8 @@ func TestGetRosterActivitySurvivesSimulatedRestart(t *testing.T) { fresh := NewComms(st, newBus(t), owner.ID) resp, err := fresh.GetRoster(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.GetRosterRequest{ - Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, - AgentAccountId: string(agent.ID), + Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, + VantageHandle: agent.Handle, })) if err != nil { t.Fatalf("GetRoster(after restart): %v", err) @@ -318,8 +343,8 @@ func TestSetStatusAsAccountTruncatesOverCap(t *testing.T) { } resp, err := svc.GetRoster(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.GetRosterRequest{ - Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, - AgentAccountId: string(agent.ID), + Scope: compassv1.RosterScope_ROSTER_SCOPE_OWNER, + VantageHandle: agent.Handle, })) if err != nil { t.Fatalf("GetRoster(after set_status): %v", err) diff --git a/go/internal/comms/supervisor_orchestration_pgtest_test.go b/go/internal/comms/supervisor_orchestration_pgtest_test.go index f2e99393..3308bc91 100644 --- a/go/internal/comms/supervisor_orchestration_pgtest_test.go +++ b/go/internal/comms/supervisor_orchestration_pgtest_test.go @@ -55,9 +55,9 @@ func TestSupervisorAssignsToTwoWorkersAuditable(t *testing.T) { // the supervisor and both workers as founding members. (The owner is a // founding member by construction — expandOwnerMembership adds the actor.) created, err := svc.CreateChannel(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateChannelRequest{ - Name: "coordination", - Kind: compassv1.ChannelKind_CHANNEL_KIND_CHANNEL, - MemberAccountIds: []string{string(supervisor.ID), string(workerA.ID), string(workerB.ID)}, + Name: "coordination", + Kind: compassv1.ChannelKind_CHANNEL_KIND_CHANNEL, + MemberHandles: []string{supervisor.Handle, workerA.Handle, workerB.Handle}, })) if err != nil { t.Fatalf("CreateChannel(coordination): %v", err) diff --git a/go/internal/comms/visibility_filter_test.go b/go/internal/comms/visibility_filter_test.go index 0373b7d8..5d4c45f3 100644 --- a/go/internal/comms/visibility_filter_test.go +++ b/go/internal/comms/visibility_filter_test.go @@ -418,7 +418,7 @@ func TestSubscribeCommsRemovedMemberGetsFinalChannelChanged(t *testing.T) { // ChannelChanged with removed_account_ids empty). ch, err := h.svc.CreateChannel(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.CreateChannelRequest{ Name: "room", Kind: compassv1.ChannelKind_CHANNEL_KIND_CHANNEL, - MemberAccountIds: []string{string(removed.ID)}, + MemberHandles: []string{removed.Handle}, })) if err != nil { t.Fatalf("CreateChannel: %v", err) @@ -428,8 +428,8 @@ func TestSubscribeCommsRemovedMemberGetsFinalChannelChanged(t *testing.T) { // Owner removes `removed` (emits a ChannelChanged with removed in // removed_account_ids). if _, err := h.svc.UpdateChannelMembers(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.UpdateChannelMembersRequest{ - ChannelId: chID, - RemoveMemberAccountIds: []string{string(removed.ID)}, + ChannelId: chID, + RemoveMemberHandles: []string{removed.Handle}, })); err != nil { t.Fatalf("UpdateChannelMembers(remove): %v", err) } diff --git a/go/internal/delivery/consumer.go b/go/internal/delivery/consumer.go index 0d930bc8..726a37d8 100644 --- a/go/internal/delivery/consumer.go +++ b/go/internal/delivery/consumer.go @@ -74,9 +74,16 @@ type DeliveryReads interface { //nolint:interfacebloat // one method per store r // state irrelevant), author excluded — the mention→steer routing set (D5, // design.md:526-527), distinct from SubscribedAgents' deliver set. ChannelAgentMembers(ctx context.Context, channel store.ChannelID, author store.AccountID) ([]store.AccountID, error) - // AgentByHandle resolves a mention handle to its agent account; an unknown or - // non-agent (human) handle is store.ErrNotFound (a mention no-op, D5). - AgentByHandle(ctx context.Context, handle string) (store.Account, error) + // AgentByHandle resolves a bare mention handle to its agent account within + // owner's namespace (RIG-2751 handle cutover: agent handles are per-owner); + // the caller passes the posting author's owner, since a mention is a bare + // handle in the author's own namespace. An unknown, wrong-owner, or non-agent + // (human) handle is store.ErrNotFound (a mention no-op, D5). + AgentByHandle(ctx context.Context, owner store.AccountID, handle string) (store.Account, error) + // ResolveOwner resolves the posting author to the owner-user namespace its + // bare mentions resolve in (an agent author → its owner_user_id, a user + // author → itself). + ResolveOwner(ctx context.Context, caller store.AccountID) (store.AccountID, error) // SweepChannels resolves the D1 disjunct channel set an agent sweeps: every // subscribed channel, PLUS its home channel, PLUS any mandatory_subscription // channel it is a member of (T4 policy) — the pin sweep's channel diff --git a/go/internal/delivery/dispatch.go b/go/internal/delivery/dispatch.go index 58ffdeb0..ed0edef2 100644 --- a/go/internal/delivery/dispatch.go +++ b/go/internal/delivery/dispatch.go @@ -272,6 +272,15 @@ func (c *Consumer) resolveMentioned(ctx context.Context, channel store.ChannelID c.log.ErrorContext(ctx, "delivery: resolve channel agent members for mention routing", "error", err, "channel", string(channel)) return nil // drop all mentions; the post still delivers normally } + // A bare mention resolves in the POSTING AUTHOR's owner namespace (RIG-2751: + // agent handles are per-owner; a mention carries no owner qualifier, so the + // author's own namespace is the resolution scope). Resolve it once for the + // per-handle lookups below. + authorOwner, err := c.st.ResolveOwner(ctx, author) + if err != nil { + c.log.ErrorContext(ctx, "delivery: resolve author owner for mention routing", "error", err, "author", string(author)) + return nil // drop all mentions; the post still delivers normally + } memberSet := make(map[store.AccountID]bool, len(members)) for _, m := range members { memberSet[m] = true @@ -288,7 +297,7 @@ func (c *Consumer) resolveMentioned(ctx context.Context, channel store.ChannelID // @users expands to human members only: no agent session to steer. continue } - acc, err := c.st.AgentByHandle(ctx, h) + acc, err := c.st.AgentByHandle(ctx, authorOwner, h) if err != nil { if errors.Is(err, store.ErrNotFound) { continue // unknown or human handle: a no-op diff --git a/go/internal/delivery/helpers_test.go b/go/internal/delivery/helpers_test.go index 39ddfc14..3c2a7731 100644 --- a/go/internal/delivery/helpers_test.go +++ b/go/internal/delivery/helpers_test.go @@ -535,8 +535,10 @@ func (f *fakeReads) ChannelAgentMembers(_ context.Context, channel store.Channel // AgentByHandle resolves a lowercased handle to its seeded agent account; an // unseeded handle is store.ErrNotFound, mirroring the store's fail-closed -// treatment of an unknown or human handle. -func (f *fakeReads) AgentByHandle(_ context.Context, handle string) (store.Account, error) { +// treatment of an unknown or human handle. The owner param (RIG-2751 handle +// cutover: agent handles are per-owner) is ignored here — the fake models one +// owner namespace, so a handle resolves regardless of the owner passed. +func (f *fakeReads) AgentByHandle(_ context.Context, _ store.AccountID, handle string) (store.Account, error) { f.mu.Lock() defer f.mu.Unlock() acc, ok := f.handles[handle] @@ -546,6 +548,13 @@ func (f *fakeReads) AgentByHandle(_ context.Context, handle string) (store.Accou return acc, nil } +// ResolveOwner returns the caller itself — the single-owner fake namespace, so +// the author's mention-resolution owner is stable and every seeded handle +// resolves under it (mirrors the store's user-owns-itself fallback). +func (f *fakeReads) ResolveOwner(_ context.Context, caller store.AccountID) (store.AccountID, error) { + return caller, nil +} + // GetAccount resolves an account by id from the seeded accounts map — the store // read that denormalizes the author's handle onto the deliver/steer control // (RIG-2486 T1). An unseeded id is store.ErrNotFound, mirroring the store's diff --git a/go/internal/gen/compass/v1/agent_gateway.pb.go b/go/internal/gen/compass/v1/agent_gateway.pb.go index 5db47a28..da34a414 100644 --- a/go/internal/gen/compass/v1/agent_gateway.pb.go +++ b/go/internal/gen/compass/v1/agent_gateway.pb.go @@ -768,10 +768,10 @@ func (x *SpawnPeerResponse) GetSessionId() string { // Same-owner authority (F2); the account row, home channel, and history survive // (despawn tears down compute, not identity). type DespawnPeerRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - AgentAccountId string `protobuf:"bytes,1,opt,name=agent_account_id,json=agentAccountId,proto3" json:"agent_account_id,omitempty"` // the peer to tear down - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + AgentHandle string `protobuf:"bytes,1,opt,name=agent_handle,json=agentHandle,proto3" json:"agent_handle,omitempty"` // the peer to tear down; a `@handle` the server resolves to an account id; unknown → NOT_FOUND + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DespawnPeerRequest) Reset() { @@ -804,9 +804,9 @@ func (*DespawnPeerRequest) Descriptor() ([]byte, []int) { return file_compass_v1_agent_gateway_proto_rawDescGZIP(), []int{8} } -func (x *DespawnPeerRequest) GetAgentAccountId() string { +func (x *DespawnPeerRequest) GetAgentHandle() string { if x != nil { - return x.AgentAccountId + return x.AgentHandle } return "" } @@ -2971,9 +2971,9 @@ const file_compass_v1_agent_gateway_proto_rawDesc = "" + "\x10agent_account_id\x18\x01 \x01(\tR\x0eagentAccountId\x12%\n" + "\x0econtainer_name\x18\x02 \x01(\tR\rcontainerName\x12\x1d\n" + "\n" + - "session_id\x18\x03 \x01(\tR\tsessionId\">\n" + - "\x12DespawnPeerRequest\x12(\n" + - "\x10agent_account_id\x18\x01 \x01(\tR\x0eagentAccountId\"\x15\n" + + "session_id\x18\x03 \x01(\tR\tsessionId\"7\n" + + "\x12DespawnPeerRequest\x12!\n" + + "\fagent_handle\x18\x01 \x01(\tR\vagentHandle\"\x15\n" + "\x13DespawnPeerResponse\"\xe4\x01\n" + "\x13LifecycleCallResult\x12\x17\n" + "\acall_id\x18\x01 \x01(\tR\x06callId\x125\n" + diff --git a/go/internal/runner/agent_exec_test.go b/go/internal/runner/agent_exec_test.go index e50b70c7..2852cd12 100644 --- a/go/internal/runner/agent_exec_test.go +++ b/go/internal/runner/agent_exec_test.go @@ -347,7 +347,7 @@ func TestStopReapsBeforeJoiningTheDrains(t *testing.T) { // context.Background() as the test root — the rule's explicit test exemption. ctx := context.Background() - name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "acct-1"}) + name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "acct-1"}) if err != nil { t.Fatalf("Provision = %v", err) } diff --git a/go/internal/runner/config_delivery_e2e_test.go b/go/internal/runner/config_delivery_e2e_test.go index 371460b2..719dc6e4 100644 --- a/go/internal/runner/config_delivery_e2e_test.go +++ b/go/internal/runner/config_delivery_e2e_test.go @@ -305,7 +305,7 @@ func TestConfigDeliveryReloadPicksUpNewBundle(t *testing.T) { // 1. Seed the fleet at v1 and bring the container + agent online. pub.setConfigBundle(AgentConfigBundle{Version: v1hash, Tarball: buildConfigTarball(t, validBundle())}) - container, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "e2e"}) + container, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "e2e"}) if err != nil { t.Fatalf("Provision: %v", err) } diff --git a/go/internal/runner/config_refresh_test.go b/go/internal/runner/config_refresh_test.go index 1f9485ab..e0fe55ea 100644 --- a/go/internal/runner/config_refresh_test.go +++ b/go/internal/runner/config_refresh_test.go @@ -88,7 +88,7 @@ type accountSpecBuilder struct{} func (accountSpecBuilder) BuildSpec(req *compassv1.ProvisionAgentWorkspaceRequest) (runtime.AgentSpec, error) { spec := liveSpec() - spec.Name = "cont-" + req.GetAgentAccountId() + spec.Name = "cont-" + req.GetAgentHandle() return spec, nil } @@ -131,7 +131,7 @@ func shortRuntimeDir(t *testing.T) string { func provisionAndStart(t *testing.T, host *agentHost, account string) string { t.Helper() ctx := context.Background() - name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: account}) + name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: account}) if err != nil { t.Fatalf("Provision(%s) = %v", account, err) } diff --git a/go/internal/runner/e2e_retire_test.go b/go/internal/runner/e2e_retire_test.go index d30e810c..07b190a2 100644 --- a/go/internal/runner/e2e_retire_test.go +++ b/go/internal/runner/e2e_retire_test.go @@ -85,7 +85,7 @@ func TestStopRetiresTheSessionsControlState(t *testing.T) { h := newTransportFixture(t, &recordingRelay{}) ctx := context.Background() - name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "acct-1"}) + name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "acct-1"}) if err != nil { t.Fatalf("Provision = %v", err) } diff --git a/go/internal/runner/e2e_transport_test.go b/go/internal/runner/e2e_transport_test.go index 02004680..0d931264 100644 --- a/go/internal/runner/e2e_transport_test.go +++ b/go/internal/runner/e2e_transport_test.go @@ -154,7 +154,7 @@ func TestE2ERoundTripUnderBoundSession(t *testing.T) { h := newTransportFixture(t, fake) ctx := context.Background() - name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}) + name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("Provision = %v", err) } @@ -209,7 +209,7 @@ func TestE2EFailClosedBeforeStart(t *testing.T) { h := newTransportFixture(t, fake) ctx := context.Background() - name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}) + name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("Provision = %v", err) } @@ -247,7 +247,7 @@ func TestE2EInFlightCallForceClosedAtTeardown(t *testing.T) { h := newTransportFixture(t, fake) ctx := context.Background() - name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}) + name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("Provision = %v", err) } @@ -307,7 +307,7 @@ func TestFreshStartSendsReplayCompleteFirst(t *testing.T) { h := newTransportFixture(t, fake) ctx := context.Background() - name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}) + name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("Provision = %v", err) } @@ -369,7 +369,7 @@ func TestResumeStartSendsReplayCompleteFirst(t *testing.T) { h := newTransportFixture(t, fake) ctx := context.Background() - name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}) + name, err := h.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("Provision = %v", err) } diff --git a/go/internal/runner/host_concurrency_test.go b/go/internal/runner/host_concurrency_test.go index 4e7783c1..40ae3a2f 100644 --- a/go/internal/runner/host_concurrency_test.go +++ b/go/internal/runner/host_concurrency_test.go @@ -68,7 +68,7 @@ func TestStartSameContainerSerializesClosingTOCTOU(t *testing.T) { // Reap both children (RED leaves two live sessions on one container) on exit. t.Cleanup(func() { host.Close(context.Background()) }) - name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "a"}) + name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "a"}) if err != nil { t.Fatalf("Provision = %v", err) } @@ -160,11 +160,11 @@ func TestStartDifferentContainersOverlap(t *testing.T) { ctx := context.Background() t.Cleanup(func() { host.Close(context.Background()) }) - nameA, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "a"}) + nameA, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "a"}) if err != nil { t.Fatalf("Provision(a) = %v", err) } - nameB, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "b"}) + nameB, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "b"}) if err != nil { t.Fatalf("Provision(b) = %v", err) } diff --git a/go/internal/runner/host_test.go b/go/internal/runner/host_test.go index bc749275..4a27ec15 100644 --- a/go/internal/runner/host_test.go +++ b/go/internal/runner/host_test.go @@ -51,7 +51,7 @@ func (b *fakeSpecBuilder) BuildSpec(req *compassv1.ProvisionAgentWorkspaceReques // does (spec.go:98) — so the account threads Provision→spec→handle→session // and the Status stamp (host.go:384/533/537) is exercised end-to-end. spec := b.spec - spec.AgentAccountID = req.GetAgentAccountId() + spec.AgentAccountID = req.GetAgentHandle() return spec, nil } @@ -116,14 +116,14 @@ func TestProvisionDrivesSpecBuilderThenLaunch(t *testing.T) { }} host, engine, registry := newHostFixture(t, specs) - name, err := host.Provision(context.Background(), &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}) + name, err := host.Provision(context.Background(), &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("Provision = %v, want success", err) } if name != "atlas-agent-1" { t.Fatalf("Provision returned name %q, want the spec's container name", name) } - if specs.last == nil || specs.last.GetAgentAccountId() != "0123456789abcdef0123456789abcdef" { + if specs.last == nil || specs.last.GetAgentHandle() != "0123456789abcdef0123456789abcdef" { t.Fatalf("SpecBuilder.BuildSpec was not called with the request; got %+v", specs.last) } // Launch ran (create+start on the engine) and registered the handle so a @@ -362,7 +362,7 @@ func TestStartTwiceSameContainerIsAlreadyRunning(t *testing.T) { host, _, _ := newHostFixture(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } first, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") @@ -430,7 +430,7 @@ func TestRemoveTearsDownContainerAndRetiresSession(t *testing.T) { host, engine, registry := newHostFixture(t, specs) ctx := context.Background() - name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}) + name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("Provision = %v", err) } @@ -468,7 +468,7 @@ func TestRemoveIsIdempotent(t *testing.T) { host, engine, _ := newHostFixture(t, specs) ctx := context.Background() - name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}) + name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("Provision = %v", err) } @@ -505,7 +505,7 @@ func TestRemoveClosesSocketWhenHandleAlreadyGone(t *testing.T) { host, engine, registry := newHostFixture(t, specs) ctx := context.Background() - name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}) + name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("Provision = %v", err) } @@ -540,7 +540,7 @@ func TestRemoveClosesSocketWhenTeardownFails(t *testing.T) { host, engine, _ := newHostFixture(t, specs) ctx := context.Background() - name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}) + name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("Provision = %v", err) } @@ -572,7 +572,7 @@ func TestFailedTeardownLeavesContainerResolvableForRetry(t *testing.T) { host, engine, registry := newHostFixture(t, specs) ctx := context.Background() - name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}) + name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("Provision = %v", err) } @@ -644,7 +644,7 @@ func TestCloseTearsDownProvisionedButNotStartedContainer(t *testing.T) { host, engine, _ := newConfigRefreshFixture(t) ctx := context.Background() - name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "a"}) + name, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "a"}) if err != nil { t.Fatalf("Provision = %v", err) } @@ -792,7 +792,7 @@ func TestStatusIsAnsweredFromLiveSet(t *testing.T) { host, _, _ := newHostFixture(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") @@ -856,7 +856,7 @@ func TestReloadReusesSessionId(t *testing.T) { host, _, _ := newHostFixture(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") @@ -915,7 +915,7 @@ func TestStartExecsAgentWithTheContainersOwnIdentity(t *testing.T) { host, engine, _ := newHostFixtureWithModel(t, specs, "claude-opus-4") ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") @@ -952,7 +952,7 @@ func TestStartOmitsModelWhenRunnerHasNoneConfigured(t *testing.T) { host, engine, _ := newHostFixtureWithModel(t, specs, "") ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") @@ -980,7 +980,7 @@ func TestReloadRelaunchesWithTheSameAgentEnv(t *testing.T) { host, engine, _ := newHostFixtureWithModel(t, specs, "claude-opus-4") ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") @@ -1024,7 +1024,7 @@ func TestReloadWithDeregisteredContainerIsSessionUnknown(t *testing.T) { host, engine, registry := newHostFixture(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") @@ -1115,7 +1115,7 @@ func TestStartMaterializesSecretsBeforeExec(t *testing.T) { }) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") @@ -1146,7 +1146,7 @@ func TestStartFetchesSecretsByContainer(t *testing.T) { host, _, pub := newHostFixtureWithPublish(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") @@ -1179,7 +1179,7 @@ func TestStartAgentExecCarriesNoEnvFile(t *testing.T) { host, engine, _ := newHostFixtureWithPublish(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") @@ -1207,7 +1207,7 @@ func TestStartToleratesNoSecretsSurface(t *testing.T) { pub.setFetchErr(connect.NewError(connect.CodeFailedPrecondition, errors.New("no secret resolver wired"))) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") @@ -1241,7 +1241,7 @@ func TestStartFailsClosedOnFetchError(t *testing.T) { pub.setFetchErr(connect.NewError(tc.code, errors.New(tc.name))) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } if _, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, ""); err == nil { @@ -1287,7 +1287,7 @@ func TestStartWithResumeBodyMaterializesSessionFile(t *testing.T) { host, engine := newHostFixtureWithRecordingExec(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } const resumeID = "sess-abc123" @@ -1343,7 +1343,7 @@ func TestStartWithoutResumeDoesNotMaterializeOrSetEnv(t *testing.T) { host, engine := newHostFixtureWithRecordingExec(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } if _, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, ""); err != nil { @@ -1377,7 +1377,7 @@ func TestStartResumeBodyWithoutIDStartsFresh(t *testing.T) { host, engine := newHostFixtureWithRecordingExec(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } // Body set, id empty: the body must be dropped, not materialized. @@ -1405,7 +1405,7 @@ func TestStartResumeWriteFailureFailsStart(t *testing.T) { host, engine := newHostFixtureWithRecordingExec(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } // Fail ONLY the resume-write Exec. The secrets env-file materialize also @@ -1462,7 +1462,7 @@ func TestStartRejectsResumeIDTraversal(t *testing.T) { host, engine := newHostFixtureWithRecordingExec(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } _, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1", ResumeSessionId: id}, "body") @@ -1498,7 +1498,7 @@ func TestStartResumeIDDotStaysInResumeDir(t *testing.T) { host, engine := newHostFixtureWithRecordingExec(t, specs) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } if _, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1", ResumeSessionId: id}, "body"); err != nil { diff --git a/go/internal/runner/secrets_refresh_test.go b/go/internal/runner/secrets_refresh_test.go index 7cc2c757..f02b71d7 100644 --- a/go/internal/runner/secrets_refresh_test.go +++ b/go/internal/runner/secrets_refresh_test.go @@ -63,7 +63,7 @@ func TestRefreshSecretsMaterializesForBoundSession(t *testing.T) { host, engine := newRefreshHostFixture(t, fetch) ctx := context.Background() - if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, err := host.Provision(ctx, &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v", err) } sessionID, err := host.Start(ctx, &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}, "") diff --git a/go/internal/runner/spec.go b/go/internal/runner/spec.go index 93662b57..99b53bb8 100644 --- a/go/internal/runner/spec.go +++ b/go/internal/runner/spec.go @@ -78,7 +78,7 @@ func NewConfigSpecBuilder(defaults SpecDefaults) (SpecBuilder, error) { // image/egress/workspace-layout from the defaults. func (b *configSpecBuilder) BuildSpec(req *compassv1.ProvisionAgentWorkspaceRequest) (runtime.AgentSpec, error) { d := b.defaults - accountID := req.GetAgentAccountId() + accountID := req.GetAgentHandle() if err := validAccountID(accountID); err != nil { return runtime.AgentSpec{}, err } diff --git a/go/internal/runner/spec_test.go b/go/internal/runner/spec_test.go index d22f9dee..ecbe7d35 100644 --- a/go/internal/runner/spec_test.go +++ b/go/internal/runner/spec_test.go @@ -79,9 +79,7 @@ func TestBuildSpecFillsWorkspaceFromDefaults(t *testing.T) { if err != nil { t.Fatalf("NewConfigSpecBuilder: %v", err) } - spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: strings.Repeat("a", 32), - }) + spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: strings.Repeat("a", 32)}) if err != nil { t.Fatalf("BuildSpec = %v", err) } @@ -103,10 +101,7 @@ func TestBuildSpecMapsPersona(t *testing.T) { if err != nil { t.Fatalf("NewConfigSpecBuilder: %v", err) } - spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: strings.Repeat("a", 32), - Persona: "You are Ada.", - }) + spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: strings.Repeat("a", 32), Persona: "You are Ada."}) if err != nil { t.Fatalf("BuildSpec = %v", err) } @@ -123,10 +118,7 @@ func TestBuildSpecMapsRole(t *testing.T) { if err != nil { t.Fatalf("NewConfigSpecBuilder: %v", err) } - spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: strings.Repeat("a", 32), - Role: "manager", - }) + spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: strings.Repeat("a", 32), Role: "manager"}) if err != nil { t.Fatalf("BuildSpec = %v", err) } @@ -142,9 +134,7 @@ func TestBuildSpecDerivesName(t *testing.T) { if err != nil { t.Fatalf("NewConfigSpecBuilder: %v", err) } - spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: "0123456789abcdef0123456789abcdef", - }) + spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}) if err != nil { t.Fatalf("BuildSpec = %v", err) } @@ -171,9 +161,7 @@ func TestBuildSpecRejectsEmptyAgentAccountID(t *testing.T) { if err != nil { t.Fatalf("NewConfigSpecBuilder: %v", err) } - spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: "", - }) + spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: ""}) if err == nil { t.Fatal("BuildSpec with empty agent_account_id = nil error, want an account-id rejection") } @@ -229,9 +217,7 @@ func TestBuildSpecRejectsAgentAccountIDThatEscapesItsPathElement(t *testing.T) { } for _, tc := range rejected { t.Run(tc.name, func(t *testing.T) { - spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: tc.accountID, - }) + spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: tc.accountID}) if err == nil { t.Fatalf("BuildSpec with agent_account_id %q = nil error, want a path-element rejection", tc.accountID) } @@ -246,9 +232,7 @@ func TestBuildSpecRejectsAgentAccountIDThatEscapesItsPathElement(t *testing.T) { // The ordinary minted shape still builds, so the guard refuses traversal // rather than every id. - spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: strings.Repeat("f", 32), - }) + spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: strings.Repeat("f", 32)}) if err != nil { t.Fatalf("BuildSpec with a 32-hex account id: %v", err) } diff --git a/go/internal/runnerhub/commands.go b/go/internal/runnerhub/commands.go index e37769b3..3d8b0901 100644 --- a/go/internal/runnerhub/commands.go +++ b/go/internal/runnerhub/commands.go @@ -60,7 +60,7 @@ func (h *Hub) Provision(ctx context.Context, requestID string, req *compassv1.Pr // Server's own record, keyed by the container name the Runner returned. This // binding is the LIVE comms binding only, cleared on re-enroll; the DURABLE // container/Runner placement is the caller's store write. - h.bindContainer(resp.GetContainerName(), store.AccountID(req.GetAgentAccountId())) + h.bindContainer(resp.GetContainerName(), store.AccountID(req.GetAgentHandle())) return resp, runnerID, nil } @@ -245,7 +245,7 @@ func provisionDedupID(clientRequestID string, req *compassv1.ProvisionAgentWorks for _, field := range []string{ "compass.provision.v1", // domain separator clientRequestID, - req.GetAgentAccountId(), + req.GetAgentHandle(), } { var lp [8]byte binary.BigEndian.PutUint64(lp[:], uint64(len(field))) diff --git a/go/internal/runnerhub/integration_pgtest_test.go b/go/internal/runnerhub/integration_pgtest_test.go index eeacf7bc..bf3ecbe3 100644 --- a/go/internal/runnerhub/integration_pgtest_test.go +++ b/go/internal/runnerhub/integration_pgtest_test.go @@ -267,9 +267,7 @@ func provisionWhenSeamLive(t *testing.T, ctx context.Context, hub *runnerhub.Hub t.Helper() deadline := time.After(integrationTimeout) for { - resp, _, err := hub.Provision(ctx, "prov-1", &compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(agentID), - }) + resp, _, err := hub.Provision(ctx, "prov-1", &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: string(agentID)}) if err == nil { name := resp.GetContainerName() if name == "" { diff --git a/go/internal/runnerhub/provision_dedup_test.go b/go/internal/runnerhub/provision_dedup_test.go index a2bd1bed..e5be12e5 100644 --- a/go/internal/runnerhub/provision_dedup_test.go +++ b/go/internal/runnerhub/provision_dedup_test.go @@ -77,10 +77,7 @@ func TestProvisionSameClientRequestIdDedups(t *testing.T) { // A fully-specified workspace: the dedup id now binds to the agent // account, so both callers must send the identical request for the retry // to join. Same id + same account = one derived dedup id = one command. - req := &compassv1.ProvisionAgentWorkspaceRequest{ - ClientRequestId: id, - AgentAccountId: "0123456789abcdef0123456789abcdef", - } + req := &compassv1.ProvisionAgentWorkspaceRequest{ClientRequestId: id, AgentHandle: "0123456789abcdef0123456789abcdef"} outcomes := make(chan provisionOutcome, 2) call := func() { resp, _, err := hub.Provision(context.Background(), id, req) @@ -201,14 +198,9 @@ func TestProvisionSameIdDifferentAccountDoesNotDedup(t *testing.T) { defer router.detach(errStreamClosed) const id = "dup" // the SAME client_request_id for both callers - reqA := &compassv1.ProvisionAgentWorkspaceRequest{ - ClientRequestId: id, - AgentAccountId: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - } - reqB := &compassv1.ProvisionAgentWorkspaceRequest{ - ClientRequestId: id, - AgentAccountId: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", // different account, same id - } + reqA := &compassv1.ProvisionAgentWorkspaceRequest{ClientRequestId: id, AgentHandle: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} + // different account, same id: + reqB := &compassv1.ProvisionAgentWorkspaceRequest{ClientRequestId: id, AgentHandle: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"} outcomes := make(chan provisionOutcome, 2) call := func(req *compassv1.ProvisionAgentWorkspaceRequest) { resp, _, err := hub.Provision(context.Background(), id, req) @@ -234,7 +226,7 @@ func TestProvisionSameIdDifferentAccountDoesNotDedup(t *testing.T) { containers := map[string]string{} for _, c := range cmds { rid := c.GetRequestId() - container := "container-for-" + c.GetProvision().GetAgentAccountId() + container := "container-for-" + c.GetProvision().GetAgentHandle() containers[rid] = container router.complete(&compassv1internal.SessionsRequest{ RequestId: rid, diff --git a/go/internal/runnerhub/relay_comms_test.go b/go/internal/runnerhub/relay_comms_test.go index 97d6661f..7a3ad78e 100644 --- a/go/internal/runnerhub/relay_comms_test.go +++ b/go/internal/runnerhub/relay_comms_test.go @@ -356,7 +356,7 @@ func TestProvisionThenStartBindsSessionToProvisionedAccount(t *testing.T) { }) ctx := context.Background() - if _, _, err := hub.Provision(ctx, "req-prov", &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "0123456789abcdef0123456789abcdef"}); err != nil { + if _, _, err := hub.Provision(ctx, "req-prov", &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "0123456789abcdef0123456789abcdef"}); err != nil { t.Fatalf("Provision = %v, want success", err) } if _, err := hub.Start(ctx, "req-start", &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}); err != nil { @@ -401,7 +401,7 @@ func TestProvisionWithEmptyAccountLeavesNoBindingAndFailsClosed(t *testing.T) { }) ctx := context.Background() - if _, _, err := hub.Provision(ctx, "req-prov", &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: ""}); err != nil { + if _, _, err := hub.Provision(ctx, "req-prov", &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: ""}); err != nil { t.Fatalf("Provision (empty account) = %v, want success", err) } if _, err := hub.Start(ctx, "req-start", &compassv1.StartAgentSessionRequest{ContainerName: "cont-1"}); err != nil { diff --git a/go/internal/runnerhub/relay_lifecycle_test.go b/go/internal/runnerhub/relay_lifecycle_test.go index 33033624..ac5eb8e8 100644 --- a/go/internal/runnerhub/relay_lifecycle_test.go +++ b/go/internal/runnerhub/relay_lifecycle_test.go @@ -175,7 +175,7 @@ func TestRelayLifecycleCallDespawnToolErrorIsInBand(t *testing.T) { fake.despawnErr = connect.NewError(connect.CodeNotFound, errors.New("peer not found")) bindLiveSession(hub) - resp, err := hub.RelayLifecycleCall(context.Background(), relayDespawn("sess-1", "lc-4b", &compassv1internal.DespawnPeerRequest{AgentAccountId: "acct-victim"})) + resp, err := hub.RelayLifecycleCall(context.Background(), relayDespawn("sess-1", "lc-4b", &compassv1internal.DespawnPeerRequest{AgentHandle: "acct-victim"})) if err != nil { t.Fatalf("RelayLifecycleCall with a despawn tool error returned a Go error %v, want nil (in-band render)", err) } @@ -239,7 +239,7 @@ func TestRelayLifecycleCallDispatchesSpawnVsDespawn(t *testing.T) { fake.despawnResp = &compassv1internal.DespawnPeerResponse{} bindLiveSession(hub) - resp, err := hub.RelayLifecycleCall(context.Background(), relayDespawn("sess-1", "lc-6b", &compassv1internal.DespawnPeerRequest{AgentAccountId: "acct-victim"})) + resp, err := hub.RelayLifecycleCall(context.Background(), relayDespawn("sess-1", "lc-6b", &compassv1internal.DespawnPeerRequest{AgentHandle: "acct-victim"})) if err != nil { t.Fatalf("RelayLifecycleCall(despawn) = %v, want success", err) } diff --git a/go/internal/runnerhub/runner_dispatch_concurrency_test.go b/go/internal/runnerhub/runner_dispatch_concurrency_test.go index 9ebd590a..45c92aea 100644 --- a/go/internal/runnerhub/runner_dispatch_concurrency_test.go +++ b/go/internal/runnerhub/runner_dispatch_concurrency_test.go @@ -85,7 +85,7 @@ func (f *fakeSessionHost) Provision(ctx context.Context, req *compassv1.Provisio } f.leaveProvision() // Echo the account id back as the container name so the test can correlate. - return "cont-" + req.GetAgentAccountId(), nil + return "cont-" + req.GetAgentHandle(), nil } func (f *fakeSessionHost) Stop(context.Context, string) error { @@ -211,7 +211,7 @@ func TestSlowProvisionDoesNotBlockConcurrentStop(t *testing.T) { // Dispatch a Provision; it parks in the host. provisionDone := make(chan error, 1) go func() { - _, _, err := hub.Provision(context.Background(), "", &compassv1.ProvisionAgentWorkspaceRequest{AgentAccountId: "a"}) + _, _, err := hub.Provision(context.Background(), "", &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "a"}) provisionDone <- err }() select { diff --git a/go/internal/store/account_handles_pgtest_test.go b/go/internal/store/account_handles_pgtest_test.go new file mode 100644 index 00000000..ec795e67 --- /dev/null +++ b/go/internal/store/account_handles_pgtest_test.go @@ -0,0 +1,315 @@ +//go:build pgtest + +package store + +// account_handles resolution index contracts (RIG-2751 handle cutover, T0 + T2). +// +// T0 — the storage shape and its two partial-unique indexes: user/system handles +// are globally unique, agent handles are unique only per owner, an agent handle +// may overlap a global user handle, and both tiers rename-in-place and reclaim. +// The uniqueness is enforced by the two partial-unique indexes authored into +// 0001_init.sql; CreateUser/CreateAgent write the rows, and a rename/reclaim is a +// direct UPDATE/DELETE against the row (no store rename API exists yet — the +// forward-looking write path is the index-level UPDATE these tests exercise). +// +// T2 — AccountsByHandles: owner-qualified and bare resolution, disambiguation by +// owner, bare-agent defaulting to callerOwner, the atomic all-or-nothing miss +// naming every unresolved handle, system exclusion, and the OQ-6 visibility clip +// (invisible ≡ unknown). +// +// context.Background() is the test root (test-root ctx exemption). + +import ( + "context" + "strings" + "testing" +) + +// ---- T0: uniqueness invariants ---- + +// TestUserHandleGloballyUnique: two user accounts cannot share a handle — the +// global (owner_user_id IS NULL) partial-unique index rejects the second. +func TestUserHandleGloballyUnique(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + if _, err := s.CreateUser(ctx, NewUser{Handle: "matt", DisplayName: "Matt"}); err != nil { + t.Fatalf("first CreateUser: %v", err) + } + _, err := s.CreateUser(ctx, NewUser{Handle: "matt", DisplayName: "Matt Two"}) + sentinelIs(t, err, ErrConflict, "second global user handle") +} + +// TestAgentHandleUniquePerOwnerRejectsSecondSameOwner: two agents under the SAME +// owner cannot share a handle — the per-owner agent index rejects the second. +func TestAgentHandleUniquePerOwnerRejectsSecondSameOwner(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + owner := mustUser(t, s, "matt") + if _, err := s.CreateAgent(ctx, owner.ID, NewAgent{Handle: "compass-ux", DisplayName: "UX"}); err != nil { + t.Fatalf("first CreateAgent: %v", err) + } + _, err := s.CreateAgent(ctx, owner.ID, NewAgent{Handle: "compass-ux", DisplayName: "UX Two"}) + sentinelIs(t, err, ErrConflict, "second matt/compass-ux") +} + +// TestAgentHandleCoexistsAcrossOwners: `matt/compass-ux` and `alice/compass-ux` +// coexist — the per-owner index keys on (owner_user_id, handle), so the same +// agent handle under different owners is two distinct rows. +func TestAgentHandleCoexistsAcrossOwners(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + matt := mustUser(t, s, "matt") + alice := mustUser(t, s, "alice") + mattUX := mustAgent(t, s, matt.ID, "compass-ux") + aliceUX := mustAgent(t, s, alice.ID, "compass-ux") + + got, err := s.AgentByHandle(ctx, matt.ID, "compass-ux") + if err != nil { + t.Fatalf("AgentByHandle(matt, compass-ux): %v", err) + } + if got.ID != mattUX.ID { + t.Fatalf("matt/compass-ux resolved to %q, want %q", got.ID, mattUX.ID) + } + got, err = s.AgentByHandle(ctx, alice.ID, "compass-ux") + if err != nil { + t.Fatalf("AgentByHandle(alice, compass-ux): %v", err) + } + if got.ID != aliceUX.ID { + t.Fatalf("alice/compass-ux resolved to %q, want %q", got.ID, aliceUX.ID) + } +} + +// TestAgentHandleMayOverlapUserHandle: an agent handle may equal a global user +// handle with no collision — a user is looked up bare (global index), an agent +// owner-qualified (per-owner index); the two indexes never contend. +func TestAgentHandleMayOverlapUserHandle(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + // A global user named "atlas". + user := mustUser(t, s, "atlas") + // An agent also named "atlas" under an owner — no collision. + owner := mustUser(t, s, "owner") + agent, err := s.CreateAgent(ctx, owner.ID, NewAgent{Handle: "atlas", DisplayName: "Atlas Agent"}) + if err != nil { + t.Fatalf("CreateAgent(atlas) overlapping user handle: %v", err) + } + if agent.ID == user.ID { + t.Fatal("agent and user share an id; they must be distinct accounts") + } + // The user resolves bare in the global index; the agent owner-qualified. + gotUser, err := s.UserByHandle(ctx, "atlas") + if err != nil || gotUser.ID != user.ID { + t.Fatalf("UserByHandle(atlas) = (%v, %v), want the user %q", gotUser.ID, err, user.ID) + } + gotAgent, err := s.AgentByHandle(ctx, owner.ID, "atlas") + if err != nil || gotAgent.ID != agent.ID { + t.Fatalf("AgentByHandle(owner, atlas) = (%v, %v), want the agent %q", gotAgent.ID, err, agent.ID) + } +} + +// TestHandleRenameInPlaceBothTiers: an in-place rename (UPDATE account_handles) +// frees the old handle and claims the new one, for both a user (global index) +// and an agent (per-owner index). Simulates the forward-looking rename write path +// (no store rename API exists yet) against the resolution index directly. +func TestHandleRenameInPlaceBothTiers(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + user := mustUser(t, s, "oldname") + owner := mustUser(t, s, "owner") + agent := mustAgent(t, s, owner.ID, "old-agent") + + // User rename (global tier). + if _, err := s.pool.Exec(ctx, + "UPDATE account_handles SET handle = $2 WHERE account_id = $1", string(user.ID), "newname"); err != nil { + t.Fatalf("rename user handle: %v", err) + } + if _, err := s.UserByHandle(ctx, "oldname"); err == nil { + t.Fatal("old user handle still resolves after rename") + } + got, err := s.UserByHandle(ctx, "newname") + if err != nil || got.ID != user.ID { + t.Fatalf("UserByHandle(newname) = (%v, %v), want %q", got.ID, err, user.ID) + } + + // Agent rename (per-owner tier). + if _, err := s.pool.Exec(ctx, + "UPDATE account_handles SET handle = $2 WHERE account_id = $1", string(agent.ID), "new-agent"); err != nil { + t.Fatalf("rename agent handle: %v", err) + } + if _, err := s.AgentByHandle(ctx, owner.ID, "old-agent"); err == nil { + t.Fatal("old agent handle still resolves after rename") + } + gotA, err := s.AgentByHandle(ctx, owner.ID, "new-agent") + if err != nil || gotA.ID != agent.ID { + t.Fatalf("AgentByHandle(owner, new-agent) = (%v, %v), want %q", gotA.ID, err, agent.ID) + } +} + +// TestHandleReclaimBothTiers: a freed handle (rename away) may be re-registered +// by a NEW account, for both tiers — no tombstone, no reservation. +func TestHandleReclaimBothTiers(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + user := mustUser(t, s, "matt") + // Free the user handle by renaming it away, then a new user reclaims "matt". + if _, err := s.pool.Exec(ctx, + "UPDATE account_handles SET handle = $2 WHERE account_id = $1", string(user.ID), "matt-old"); err != nil { + t.Fatalf("rename away: %v", err) + } + reclaimer, err := s.CreateUser(ctx, NewUser{Handle: "matt", DisplayName: "New Matt"}) + if err != nil { + t.Fatalf("reclaim user handle: %v", err) + } + got, err := s.UserByHandle(ctx, "matt") + if err != nil || got.ID != reclaimer.ID { + t.Fatalf("UserByHandle(matt) after reclaim = (%v, %v), want %q", got.ID, err, reclaimer.ID) + } + + // Agent tier: free `owner/ux`, a new agent reclaims it under the same owner. + owner := mustUser(t, s, "owner") + agent := mustAgent(t, s, owner.ID, "ux") + if _, err := s.pool.Exec(ctx, + "UPDATE account_handles SET handle = $2 WHERE account_id = $1", string(agent.ID), "ux-old"); err != nil { + t.Fatalf("rename agent away: %v", err) + } + reAgent, err := s.CreateAgent(ctx, owner.ID, NewAgent{Handle: "ux", DisplayName: "New UX"}) + if err != nil { + t.Fatalf("reclaim agent handle: %v", err) + } + gotA, err := s.AgentByHandle(ctx, owner.ID, "ux") + if err != nil || gotA.ID != reAgent.ID { + t.Fatalf("AgentByHandle(owner, ux) after reclaim = (%v, %v), want %q", gotA.ID, err, reAgent.ID) + } +} + +// ---- T2: AccountsByHandles resolver ---- + +// qh is a terse QualifiedHandle constructor mirroring ParseQualifiedHandle, so a +// test can state the parsed shape directly. +func qh(raw string) QualifiedHandle { return ParseQualifiedHandle(raw) } + +// TestAccountsByHandlesRoundTripAndDisambiguation: a bare user handle resolves in +// the global index, a bare agent handle in the CALLER's own owner namespace, and +// an owner-qualified handle resolves in the NAMED owner's namespace (matt/… picks +// matt's agent, never alice's homonym). All targets are visible to the viewer — +// the OQ-6 clip on cross-owner-qualified resolution is exercised separately in +// TestAccountsByHandlesInvisibleEqualsUnknown; owner-qualified disambiguation +// WITHOUT a visibility clip is TestAgentHandleCoexistsAcrossOwners. +func TestAccountsByHandlesRoundTripAndDisambiguation(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + matt := mustUser(t, s, "matt") + alice := mustUser(t, s, "alice") + mattUX := mustAgent(t, s, matt.ID, "compass-ux") + // alice also owns a "compass-ux" — matt's owner-qualified lookup must pick + // matt's, proving the owner segment is the disambiguator. + mustAgent(t, s, alice.ID, "compass-ux") + + // viewer=matt (a user sees every user + its own agents), callerOwner=matt. + // matt/compass-ux (own agent, visible) + bare alice (global user). + got, err := s.AccountsByHandles(ctx, matt.ID, matt.ID, []QualifiedHandle{ + qh("matt/compass-ux"), qh("alice"), + }) + if err != nil { + t.Fatalf("AccountsByHandles: %v", err) + } + if got["matt/compass-ux"] != mattUX.ID { + t.Errorf("matt/compass-ux = %q, want matt's %q (not alice's homonym)", got["matt/compass-ux"], mattUX.ID) + } + if got["alice"] != alice.ID { + t.Errorf("bare alice = %q, want the user %q", got["alice"], alice.ID) + } + + // A bare agent handle from a matt-owned caller defaults to matt's namespace. + bare, err := s.AccountsByHandles(ctx, matt.ID, matt.ID, []QualifiedHandle{qh("compass-ux")}) + if err != nil { + t.Fatalf("AccountsByHandles(bare agent): %v", err) + } + if bare["compass-ux"] != mattUX.ID { + t.Errorf("bare compass-ux = %q, want matt's %q (callerOwner default)", bare["compass-ux"], mattUX.ID) + } +} + +// TestAccountsByHandlesBareUserAndSystemGlobal: a bare user handle resolves in the +// global index; the system account is EXCLUDED (never a member/owner target). +func TestAccountsByHandlesBareUserAndSystemGlobal(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + matt := mustUser(t, s, "matt") + if _, err := s.EnsureSystemAccount(ctx); err != nil { + t.Fatalf("EnsureSystemAccount: %v", err) + } + + got, err := s.AccountsByHandles(ctx, matt.ID, matt.ID, []QualifiedHandle{qh("matt")}) + if err != nil { + t.Fatalf("AccountsByHandles(matt): %v", err) + } + if got["matt"] != matt.ID { + t.Errorf("bare matt = %q, want %q", got["matt"], matt.ID) + } + + // The system handle is never a resolvable member/owner target. + _, err = s.AccountsByHandles(ctx, matt.ID, matt.ID, []QualifiedHandle{qh(SystemAccountHandle)}) + sentinelIs(t, err, ErrNotFound, "system handle as a member/owner target") +} + +// TestAccountsByHandlesAtomicMissNamesAll: one unresolved handle fails the whole +// call, and the error names EVERY unresolved handle in its submitted spelling +// (OQ-2 atomic), never the resolved ones. +func TestAccountsByHandlesAtomicMissNamesAll(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + matt := mustUser(t, s, "matt") + + _, err := s.AccountsByHandles(ctx, matt.ID, matt.ID, []QualifiedHandle{ + qh("matt"), qh("ghost-one"), qh("ghost-two"), + }) + sentinelIs(t, err, ErrNotFound, "atomic miss") + if !strings.Contains(err.Error(), "ghost-one") || !strings.Contains(err.Error(), "ghost-two") { + t.Fatalf("error %q must name BOTH unresolved handles (ghost-one, ghost-two)", err) + } + if strings.Contains(err.Error(), "matt") { + t.Fatalf("error %q must NOT name the resolved handle (matt)", err) + } +} + +// TestAccountsByHandlesInvisibleEqualsUnknown (OQ-6 SCOPED): a real agent the +// viewer cannot see misses exactly like an unknown handle — the visibility clip +// intersects the resolution. +func TestAccountsByHandlesInvisibleEqualsUnknown(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + matt := mustUser(t, s, "matt") + alice := mustUser(t, s, "alice") + // alice's agent, which matt shares no channel with → invisible to matt. + mustAgent(t, s, alice.ID, "secret") + + // viewer=matt, resolving alice/secret: the agent is real but invisible to + // matt, so it misses like an unknown handle. + _, err := s.AccountsByHandles(ctx, matt.ID, matt.ID, []QualifiedHandle{qh("alice/secret")}) + sentinelIs(t, err, ErrNotFound, "invisible agent ≡ unknown") + + // Contrast: alice herself resolves alice/secret (she owns it → visible). + got, err := s.AccountsByHandles(ctx, alice.ID, alice.ID, []QualifiedHandle{qh("alice/secret")}) + if err != nil { + t.Fatalf("AccountsByHandles(alice sees own agent): %v", err) + } + if got["alice/secret"] == "" { + t.Fatal("alice cannot resolve her own agent alice/secret") + } +} + +// TestAccountsByHandlesEmptyInputNoOp: empty input is a no-op — an empty map and +// a nil error, never a spurious miss. +func TestAccountsByHandlesEmptyInputNoOp(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + matt := mustUser(t, s, "matt") + got, err := s.AccountsByHandles(ctx, matt.ID, matt.ID, nil) + if err != nil { + t.Fatalf("AccountsByHandles(empty) = %v, want nil", err) + } + if len(got) != 0 { + t.Fatalf("AccountsByHandles(empty) = %v, want empty map", got) + } +} diff --git a/go/internal/store/accounts.go b/go/internal/store/accounts.go index 55c68084..cf7f33fd 100644 --- a/go/internal/store/accounts.go +++ b/go/internal/store/accounts.go @@ -2,11 +2,32 @@ package store import ( "context" + "errors" "fmt" + "strings" "github.com/jackc/pgx/v5" ) +// insertAccountHandle records accountID's row in the account_handles resolution +// index (RIG-2751 handle cutover). ownerUserID is empty for a user/system handle +// (stored NULL, globally unique) and the owning user's id for an agent handle +// (unique only within that owner). A duplicate handle in the applicable +// namespace is ErrConflict, mirroring the former accounts.handle unique. Runs on +// the caller's tx so the handle row commits atomically with the account insert. +func insertAccountHandle(ctx context.Context, tx pgx.Tx, accountID, handle string, ownerUserID AccountID) error { + if _, err := tx.Exec(ctx, + "INSERT INTO account_handles (account_id, handle, owner_user_id) VALUES ($1, $2, NULLIF($3, ''))", + accountID, handle, string(ownerUserID), + ); err != nil { + if pgErrIs(err, pgUniqueViolation) { + return fmt.Errorf("%w: handle %q already taken", ErrConflict, handle) + } + return fmt.Errorf("store: insert account_handle: %w", err) + } + return nil +} + // CreateUser inserts a human account (a regular member; admin elevation is a // separate path, comms.proto:39-42) and returns it with its server-assigned id. // A duplicate handle is ErrConflict. @@ -29,9 +50,6 @@ func (s *Store) CreateUser(ctx context.Context, u NewUser) (Account, error) { "INSERT INTO accounts (id, handle, display_name) VALUES ($1, $2, $3)", id, u.Handle, u.DisplayName, ); err != nil { - if pgErrIs(err, pgUniqueViolation) { - return Account{}, fmt.Errorf("%w: handle %q already taken", ErrConflict, u.Handle) - } return Account{}, fmt.Errorf("store: insert account: %w", err) } if _, err := tx.Exec(ctx, @@ -39,6 +57,12 @@ func (s *Store) CreateUser(ctx context.Context, u NewUser) (Account, error) { ); err != nil { return Account{}, fmt.Errorf("store: insert user_account: %w", err) } + // The handle uniqueness now lives on account_handles (a user handle is + // globally unique, owner_user_id NULL), not accounts.handle: a duplicate + // surfaces here as ErrConflict. + if err := insertAccountHandle(ctx, tx, id, u.Handle, ""); err != nil { + return Account{}, err + } if err := tx.Commit(ctx); err != nil { return Account{}, fmt.Errorf("store: commit create user: %w", err) } @@ -81,10 +105,6 @@ func (s *Store) BootstrapAdmin(ctx context.Context, u NewUser) (Account, error) "INSERT INTO accounts (id, handle, display_name) VALUES ($1, $2, $3)", id, u.Handle, u.DisplayName, ); err != nil { - if pgErrIs(err, pgUniqueViolation) { - // Already bootstrapped (restart): fetch and return the existing admin. - return s.adminByHandle(ctx, u.Handle) - } return Account{}, fmt.Errorf("store: insert account: %w", err) } if _, err := tx.Exec(ctx, @@ -92,6 +112,15 @@ func (s *Store) BootstrapAdmin(ctx context.Context, u NewUser) (Account, error) ); err != nil { return Account{}, fmt.Errorf("store: insert user_account: %w", err) } + // Handle uniqueness lives on account_handles now; the restart's duplicate + // surfaces here (ErrConflict) rather than on the accounts insert. Already + // bootstrapped (restart): fetch and return the existing admin. + if err := insertAccountHandle(ctx, tx, id, u.Handle, ""); err != nil { + if errors.Is(err, ErrConflict) { + return s.adminByHandle(ctx, u.Handle) + } + return Account{}, err + } if err := tx.Commit(ctx); err != nil { return Account{}, fmt.Errorf("store: commit bootstrap admin: %w", err) } @@ -114,11 +143,12 @@ func (s *Store) adminByHandle(ctx context.Context, handle string) (Account, erro u.role, ag.owner_user_id, ag.home_channel_id, ag.persona, ag.role, ag.parent_agent_id, sy.account_id - FROM accounts a + FROM account_handles ah + JOIN accounts a ON a.id = ah.account_id LEFT JOIN user_accounts u ON u.account_id = a.id LEFT JOIN agent_accounts ag ON ag.account_id = a.id LEFT JOIN system_accounts sy ON sy.account_id = a.id - WHERE a.handle = $1` + WHERE ah.owner_user_id IS NULL AND ah.handle = $1` acc, err := scanAccount(s.pool.QueryRow(ctx, q, handle)) if err != nil { return Account{}, err @@ -175,10 +205,6 @@ func (s *Store) ensureSystemSubtypeAccount(ctx context.Context, handle, displayN "INSERT INTO accounts (id, handle, display_name) VALUES ($1, $2, $3)", id, handle, displayName, ); err != nil { - if pgErrIs(err, pgUniqueViolation) { - // Already seeded (restart): fetch and return the existing system account. - return s.systemByHandle(ctx, handle) - } return Account{}, fmt.Errorf("store: insert account: %w", err) } if _, err := tx.Exec(ctx, @@ -186,6 +212,15 @@ func (s *Store) ensureSystemSubtypeAccount(ctx context.Context, handle, displayN ); err != nil { return Account{}, fmt.Errorf("store: insert system_account: %w", err) } + // A system handle is globally unique (owner_user_id NULL) on account_handles; + // the restart's duplicate surfaces here. Already seeded (restart): fetch and + // return the existing system account. + if err := insertAccountHandle(ctx, tx, id, handle, ""); err != nil { + if errors.Is(err, ErrConflict) { + return s.systemByHandle(ctx, handle) + } + return Account{}, err + } if err := tx.Commit(ctx); err != nil { return Account{}, fmt.Errorf("store: commit ensure system account: %w", err) } @@ -209,11 +244,12 @@ func (s *Store) systemByHandle(ctx context.Context, handle string) (Account, err u.role, ag.owner_user_id, ag.home_channel_id, ag.persona, ag.role, ag.parent_agent_id, sy.account_id - FROM accounts a + FROM account_handles ah + JOIN accounts a ON a.id = ah.account_id LEFT JOIN user_accounts u ON u.account_id = a.id LEFT JOIN agent_accounts ag ON ag.account_id = a.id LEFT JOIN system_accounts sy ON sy.account_id = a.id - WHERE a.handle = $1` + WHERE ah.owner_user_id IS NULL AND ah.handle = $1` acc, err := scanAccount(s.pool.QueryRow(ctx, q, handle)) if err != nil { return Account{}, fmt.Errorf("store: resolve system account by handle: %w", err) @@ -254,9 +290,6 @@ func (s *Store) CreateAgent(ctx context.Context, ownerUserID AccountID, a NewAge "INSERT INTO accounts (id, handle, display_name) VALUES ($1, $2, $3)", accountID, a.Handle, a.DisplayName, ); err != nil { - if pgErrIs(err, pgUniqueViolation) { - return Account{}, fmt.Errorf("%w: handle %q already taken", ErrConflict, a.Handle) - } return Account{}, fmt.Errorf("store: insert account: %w", err) } if _, err := tx.Exec(ctx, @@ -277,6 +310,14 @@ func (s *Store) CreateAgent(ctx context.Context, ownerUserID AccountID, a NewAge return Account{}, fmt.Errorf("store: insert agent_account: %w", err) } + // Record the agent handle in the resolution index, scoped to its owner + // (owner_user_id = ownerUserID), so it is unique only within that owner's + // namespace. Handle uniqueness moved off accounts.handle: a duplicate agent + // handle under the same owner surfaces here as ErrConflict. + if err := insertAccountHandle(ctx, tx, accountID, a.Handle, ownerUserID); err != nil { + return Account{}, err + } + // INVARIANT: every write of agent_accounts.parent_agent_id must invoke the // registered coordination hook. The INSERT above just wrote it; invoke the // hook on THIS tx for the new agent's PARENT (the manager that gains this @@ -616,6 +657,12 @@ func validateNewParent(ctx context.Context, tx pgx.Tx, agentAccountID, newParent return fmt.Errorf("store: resolve new parent owner: %w", err) } if AccountID(parentOwner) != agentOwner { + // Cross-owner reparent is rejected. On the ReparentAgent RPC path this + // clause is edge-shadowed: comms.ReparentAgent rejects a foreign parent + // at the service edge (naming the submitted handle, DL-269 oracle + // invariant) BEFORE calling the store, so this ErrPermissionDenied only + // surfaces to a direct store caller (independently tested) — it remains + // as store-layer defense-in-depth, not dead code. return fmt.Errorf("%w: parent agent %q has a different owner", ErrPermissionDenied, newParentAgentID) } @@ -651,28 +698,37 @@ func validateNewParent(ctx context.Context, tx pgx.Tx, agentAccountID, newParent return nil } -// AgentByHandle returns the agent account with the given handle. The crash- -// recovery resume path needs an owner-checkable handle lookup, and the private -// adminByHandle cannot be reused because it asserts admin — this one never -// asserts or elevates. It returns the full Account so the caller owner-checks the -// result itself. A handle that is unknown, or that names a non-agent account, is -// ErrNotFound: a user handle is deliberately indistinguishable from an unknown -// one, so this fails closed and never resolves or elevates a non-agent. -func (s *Store) AgentByHandle(ctx context.Context, handle string) (Account, error) { +// AgentByHandle returns the agent account with the given handle in owner's agent +// namespace (RIG-2751 handle cutover: agent handles are unique only per owner, +// so resolution is owner-qualified over account_handles' agent index, +// `UNIQUE(owner_user_id, handle) WHERE owner_user_id IS NOT NULL`). owner is the +// owning user's account id — from a parsed `owner/` qualifier, or the caller's +// own owner for a bare handle. It returns the full Account so the caller +// owner-checks the result itself. A handle that is unknown in this owner's +// namespace (or that resolves to a non-agent) is ErrNotFound: an unknown, +// wrong-owner, or non-agent handle is deliberately indistinguishable, so this +// fails closed and never resolves or elevates a non-agent. +func (s *Store) AgentByHandle(ctx context.Context, owner AccountID, handle string) (Account, error) { if handle == "" { return Account{}, fmt.Errorf("%w: handle is required", ErrInvalidArgument) } + if owner == "" { + // No owner namespace to resolve in: fail closed exactly like an unknown + // handle (indistinguishable from the wrong-owner miss below). + return Account{}, fmt.Errorf("%w: handle %q", ErrNotFound, handle) + } const q = ` SELECT a.id, a.handle, a.display_name, u.role, ag.owner_user_id, ag.home_channel_id, ag.persona, ag.role, ag.parent_agent_id, sy.account_id - FROM accounts a + FROM account_handles ah + JOIN accounts a ON a.id = ah.account_id LEFT JOIN user_accounts u ON u.account_id = a.id LEFT JOIN agent_accounts ag ON ag.account_id = a.id LEFT JOIN system_accounts sy ON sy.account_id = a.id - WHERE a.handle = $1` - acc, err := scanAccount(s.pool.QueryRow(ctx, q, handle)) + WHERE ah.owner_user_id = $1 AND ah.handle = $2` + acc, err := scanAccount(s.pool.QueryRow(ctx, q, string(owner), handle)) if err != nil { if noRows(err) { return Account{}, fmt.Errorf("%w: handle %q", ErrNotFound, handle) @@ -680,16 +736,213 @@ func (s *Store) AgentByHandle(ctx context.Context, handle string) (Account, erro return Account{}, fmt.Errorf("store: resolve agent by handle: %w", err) } if !acc.IsAgent() { - // Identical wrapped text to the noRows branch above: a user handle must - // be indistinguishable from an unknown one at the message-text level too, - // not just the sentinel — the reason this lookup never reuses the - // admin-asserting adminByHandle. The distinguishing detail stays out of - // the client-visible error (the edge maps the store err verbatim). + // Identical wrapped text to the noRows branch above: a non-agent handle + // must be indistinguishable from an unknown one at the message-text level + // too, not just the sentinel. The distinguishing detail stays out of the + // client-visible error (the edge maps the store err verbatim). return Account{}, fmt.Errorf("%w: handle %q", ErrNotFound, handle) } return acc, nil } +// UserByHandle resolves a bare user/system handle in the global handle index +// (`UNIQUE(handle) WHERE owner_user_id IS NULL`) to its full account. It is the +// global-tier counterpart to the owner-qualified AgentByHandle, for a caller +// that holds a bare user handle and needs the account (e.g. resolving an owner +// namespace before an agent lookup). An unknown handle is ErrNotFound. +func (s *Store) UserByHandle(ctx context.Context, handle string) (Account, error) { + if handle == "" { + return Account{}, fmt.Errorf("%w: handle is required", ErrInvalidArgument) + } + const q = ` + SELECT a.id, a.handle, a.display_name, + u.role, + ag.owner_user_id, ag.home_channel_id, ag.persona, ag.role, ag.parent_agent_id, + sy.account_id + FROM account_handles ah + JOIN accounts a ON a.id = ah.account_id + LEFT JOIN user_accounts u ON u.account_id = a.id + LEFT JOIN agent_accounts ag ON ag.account_id = a.id + LEFT JOIN system_accounts sy ON sy.account_id = a.id + WHERE ah.owner_user_id IS NULL AND ah.handle = $1` + acc, err := scanAccount(s.pool.QueryRow(ctx, q, handle)) + if err != nil { + if noRows(err) { + return Account{}, fmt.Errorf("%w: handle %q", ErrNotFound, handle) + } + return Account{}, fmt.Errorf("store: resolve user by handle: %w", err) + } + return acc, nil +} + +// QualifiedHandle is a submitted account handle parsed into its owner qualifier +// and bare handle. Owner is empty for a bare handle (`matt`, `compass-ux`), +// non-empty for an owner-qualified agent handle (`matt/compass-ux` → Owner +// "matt", Handle "compass-ux"). Raw preserves the exact submitted spelling so a +// resolver error can name it back verbatim (the oracle-safe message contract). +type QualifiedHandle struct { + Owner string + Handle string + Raw string +} + +// ParseQualifiedHandle splits a submitted handle on the FIRST '/': everything +// before it is the owner qualifier, everything after is the agent handle. No +// '/' means a bare handle (Owner empty). It is a pure edge helper — the split +// only; namespace resolution is AccountsByHandles' job. +func ParseQualifiedHandle(raw string) QualifiedHandle { + if owner, handle, ok := strings.Cut(raw, "/"); ok { + return QualifiedHandle{Owner: owner, Handle: handle, Raw: raw} + } + return QualifiedHandle{Handle: raw, Raw: raw} +} + +// AccountsByHandles resolves a batch of owner-qualified-or-bare handles to their +// account ids over the account_handles resolution index (RIG-2751 handle +// cutover), for the member/owner request fields that legitimately name users as +// well as agents. Resolution per input (§"The storage contract"): +// +// - owner-qualified (`matt/compass-ux`): resolve the owner segment bare in the +// user/system global index → its account_id is the owner_user_id → resolve +// the agent segment in that owner's agent index. +// - bare (`matt`, `compass-ux`): resolve EITHER as a user handle in the global +// index OR as an agent handle in the CALLER'S OWN owner namespace +// (callerOwner). The system account is never a member/owner target, so the +// global arm excludes system_accounts rows. +// +// Every arm is intersected with accountVisibleFromWhere keyed on viewer (OQ-6 +// SCOPED): a real-but-invisible handle misses exactly like an unknown one, so +// resolution and the roster clip stay aligned by construction. +// +// ATOMIC (OQ-2): any handle that fails to resolve fails the whole call with +// ErrNotFound naming EVERY unresolved handle in its submitted spelling (same +// message template as AgentByHandle). On success the returned map is keyed by +// each input's submitted spelling (QualifiedHandle.Raw) → resolved id, so the +// caller gets the full hit set (the set-difference is free). Empty input is a +// no-op (empty map, nil error). +func (s *Store) AccountsByHandles(ctx context.Context, viewer, callerOwner AccountID, handles []QualifiedHandle) (map[string]AccountID, error) { + hits := make(map[string]AccountID, len(handles)) + var missing []string + for _, qh := range handles { + id, err := s.resolveOneHandle(ctx, viewer, callerOwner, qh) + if err != nil { + return nil, err + } + if id == "" { + missing = append(missing, qh.Raw) + continue + } + hits[qh.Raw] = id + } + if len(missing) > 0 { + // Name ALL unresolved handles in their submitted spelling, so the caller + // cannot probe which specific handle was the miss (oracle-safe), same + // wrapped-text template as AgentByHandle. + return nil, fmt.Errorf("%w: handle %q", ErrNotFound, strings.Join(missing, ", ")) + } + return hits, nil +} + +// resolveOneHandle resolves a single QualifiedHandle to a visible, non-system +// account id, or returns ("", nil) for a clean miss (unknown, wrong-namespace, +// or invisible — all indistinguishable). A real query fault is a non-nil error. +func (s *Store) resolveOneHandle(ctx context.Context, viewer, callerOwner AccountID, qh QualifiedHandle) (AccountID, error) { + if qh.Handle == "" { + return "", nil + } + if qh.Owner != "" { + // owner-qualified: resolve the owner segment in the global user/system + // index (excluding system, which owns no agents), then the agent segment + // under it. + ownerID, err := s.globalHandleID(ctx, qh.Owner) + if err != nil { + return "", err + } + if ownerID == "" { + return "", nil + } + return s.visibleAgentHandleID(ctx, viewer, ownerID, qh.Handle) + } + // bare: a user/system-tier handle in the global index (visible, non-system), + // OR an agent handle in the caller's own owner namespace. + id, err := s.visibleGlobalHandleID(ctx, viewer, qh.Handle) + if err != nil { + return "", err + } + if id != "" { + return id, nil + } + return s.visibleAgentHandleID(ctx, viewer, callerOwner, qh.Handle) +} + +// globalHandleID resolves a bare handle in the global user/system index +// (owner_user_id IS NULL), excluding the system account, WITHOUT a visibility +// clip — it backs the owner-qualifier lookup, whose owner is a namespace key, +// not an addressed target. Empty id on a clean miss. +func (s *Store) globalHandleID(ctx context.Context, handle string) (AccountID, error) { + var id string + err := s.pool.QueryRow(ctx, ` + SELECT ah.account_id + FROM account_handles ah + WHERE ah.owner_user_id IS NULL AND ah.handle = $1 + AND NOT EXISTS (SELECT 1 FROM system_accounts sy WHERE sy.account_id = ah.account_id)`, + handle, + ).Scan(&id) + if err != nil { + if noRows(err) { + return "", nil + } + return "", fmt.Errorf("store: resolve global handle: %w", err) + } + return AccountID(id), nil +} + +// visibleGlobalHandleID resolves a bare handle in the global user/system index, +// excluding the system account AND intersecting the viewer's account-visible set +// (accountVisibleFromWhere). Empty id on a clean miss (unknown or invisible). +func (s *Store) visibleGlobalHandleID(ctx context.Context, viewer AccountID, handle string) (AccountID, error) { + var id string + err := s.pool.QueryRow(ctx, ` + SELECT ah.account_id + FROM account_handles ah + WHERE ah.owner_user_id IS NULL AND ah.handle = $2 + AND NOT EXISTS (SELECT 1 FROM system_accounts sy WHERE sy.account_id = ah.account_id) + AND EXISTS (SELECT 1`+accountVisibleFromWhere+` AND a.id = ah.account_id)`, + string(viewer), handle, + ).Scan(&id) + if err != nil { + if noRows(err) { + return "", nil + } + return "", fmt.Errorf("store: resolve visible global handle: %w", err) + } + return AccountID(id), nil +} + +// visibleAgentHandleID resolves an agent handle in owner's agent namespace +// (owner_user_id = owner), intersecting the viewer's account-visible set. An +// empty owner (no caller namespace) or a clean miss returns an empty id. +func (s *Store) visibleAgentHandleID(ctx context.Context, viewer, owner AccountID, handle string) (AccountID, error) { + if owner == "" { + return "", nil + } + var id string + err := s.pool.QueryRow(ctx, ` + SELECT ah.account_id + FROM account_handles ah + WHERE ah.owner_user_id = $2 AND ah.handle = $3 + AND EXISTS (SELECT 1`+accountVisibleFromWhere+` AND a.id = ah.account_id)`, + string(viewer), string(owner), handle, + ).Scan(&id) + if err != nil { + if noRows(err) { + return "", nil + } + return "", fmt.Errorf("store: resolve visible agent handle: %w", err) + } + return AccountID(id), nil +} + // accountVisibleFromWhere is the FROM + JOINs + visibility predicate shared by // ListAccounts and AccountVisibleTo, so the stream edge's per-event account // filter cannot drift from the ListAccounts read (the anti-drift guarantee the diff --git a/go/internal/store/accounts_test.go b/go/internal/store/accounts_test.go index 61cd90ad..2bce39da 100644 --- a/go/internal/store/accounts_test.go +++ b/go/internal/store/accounts_test.go @@ -445,7 +445,7 @@ func TestAgentByHandleRoundTrips(t *testing.T) { owner := mustUser(t, s, "owner") agent := mustAgent(t, s, owner.ID, "bot") - got, err := s.AgentByHandle(ctx, "bot") + got, err := s.AgentByHandle(ctx, owner.ID, "bot") if err != nil { t.Fatalf("AgentByHandle: %v", err) } @@ -464,7 +464,8 @@ func TestAgentByHandleRoundTrips(t *testing.T) { // names no account: ErrNotFound, so the resume path resolves nothing to elevate. func TestAgentByHandleUnknownIsNotFound(t *testing.T) { s := newTestStore(t) - _, err := s.AgentByHandle(t.Context(), "nobody") + owner := mustUser(t, s, "owner") + _, err := s.AgentByHandle(t.Context(), owner.ID, "nobody") sentinelIs(t, err, ErrNotFound, "unknown agent handle lookup") } @@ -476,7 +477,10 @@ func TestAgentByHandleUnknownIsNotFound(t *testing.T) { func TestAgentByHandleUserHandleIsNotFound(t *testing.T) { s := newTestStore(t) user := mustUser(t, s, "human") - _, err := s.AgentByHandle(t.Context(), user.Handle) + // A user handle lives in the global index, not any owner's agent namespace, + // so an owner-qualified agent lookup for it misses closed regardless of the + // owner passed — indistinguishable from unknown. + _, err := s.AgentByHandle(t.Context(), user.ID, user.Handle) sentinelIs(t, err, ErrNotFound, "agent handle lookup for a user handle") } @@ -745,11 +749,14 @@ func TestEnsureSystemAccountRoundTripsThroughGetAccount(t *testing.T) { } // TestEnsureSystemAccountWrongShapeSquatterConflicts asserts a pre-existing -// @compass row of the WRONG shape (a user, then separately an agent) fails with -// ErrConflict rather than being silently adopted as the system sender. The -// squatters are inserted directly through the pool because CreateUser/ -// CreateAgent reject the reserved handle (T1), so a squatter can only originate -// from a pre-guard database — exactly what this simulates. +// @compass row of the WRONG shape (a user, then separately an agent) that OWNS +// the reserved handle in the global resolution index fails with ErrConflict +// rather than being silently adopted as the system sender. The squatters are +// inserted directly through the pool because CreateUser/CreateAgent reject the +// reserved handle (T1); the account_handles global row (owner_user_id NULL) is +// planted too, since handle ownership now lives there (RIG-2751 handle cutover) +// and systemByHandle resolves through it — a squatter that owns only the legacy +// accounts.handle column but not the resolution index would not contend. func TestEnsureSystemAccountWrongShapeSquatterConflicts(t *testing.T) { ctx := context.Background() @@ -767,6 +774,14 @@ func TestEnsureSystemAccountWrongShapeSquatterConflicts(t *testing.T) { ); err != nil { t.Fatalf("insert squatter user_account: %v", err) } + // Plant the reserved handle in the GLOBAL index (owner_user_id NULL): the + // squatter owns the resolution key the system seeder contends for. + if _, err := s.pool.Exec(ctx, + "INSERT INTO account_handles (account_id, handle, owner_user_id) VALUES ($1, $2, NULL)", + id, SystemAccountHandle, + ); err != nil { + t.Fatalf("insert squatter handle row: %v", err) + } _, err := s.EnsureSystemAccount(ctx) sentinelIs(t, err, ErrConflict, "user squatter on the reserved handle") }) @@ -786,6 +801,15 @@ func TestEnsureSystemAccountWrongShapeSquatterConflicts(t *testing.T) { ); err != nil { t.Fatalf("insert squatter agent_account: %v", err) } + // Plant the reserved handle in the GLOBAL index (owner_user_id NULL) so + // the squatter actually owns the resolution key the system seeder + // contends for — the wrong-shape-row-in-the-global-index threat. + if _, err := s.pool.Exec(ctx, + "INSERT INTO account_handles (account_id, handle, owner_user_id) VALUES ($1, $2, NULL)", + id, SystemAccountHandle, + ); err != nil { + t.Fatalf("insert squatter handle row: %v", err) + } _, err := s.EnsureSystemAccount(ctx) sentinelIs(t, err, ErrConflict, "agent squatter on the reserved handle") }) diff --git a/go/internal/store/coordination_pgtest_test.go b/go/internal/store/coordination_pgtest_test.go index 01c481d1..8cf99b80 100644 --- a/go/internal/store/coordination_pgtest_test.go +++ b/go/internal/store/coordination_pgtest_test.go @@ -336,7 +336,7 @@ func TestCollisionUserOwnedSuffixes(t *testing.T) { t.Fatalf("suffixed channel not manager-owned+mandatory: %+v", mgrCh.Policy) } // The parent-edge write still succeeded: the report account exists. - if _, err := s.AgentByHandle(ctx, "report"); err != nil { + if _, err := s.AgentByHandle(ctx, owner.ID, "report"); err != nil { t.Fatalf("report account not created (parent-edge write wedged by collision): %v", err) } } diff --git a/go/internal/store/migrations/0001_init.sql b/go/internal/store/migrations/0001_init.sql index 5d57e5b6..eaaad058 100644 --- a/go/internal/store/migrations/0001_init.sql +++ b/go/internal/store/migrations/0001_init.sql @@ -31,11 +31,15 @@ -- ── Accounts ──────────────────────────────────────────────────────────────── -- One row per account; the user/agent split lives in the two subtype tables --- below, mirroring the compass.v1 Account `kind` oneof. handle is globally --- unique (the ErrConflict source for CreateUser/CreateAgent). +-- below, mirroring the compass.v1 Account `kind` oneof. handle is a display +-- column kept in sync with the account's current handle (CreateUser/CreateAgent +-- populate it, a rename UPDATEs it); it is NO LONGER the resolution key — that +-- moved to account_handles below, whose partial-unique indexes express the +-- two-namespace contract (global-unique user/system handles, per-owner agent +-- handles) a single global column could not (RIG-2751 handle cutover). CREATE TABLE accounts ( id TEXT PRIMARY KEY, - handle TEXT NOT NULL UNIQUE, + handle TEXT NOT NULL, display_name TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); @@ -104,6 +108,32 @@ CREATE TABLE system_accounts ( account_id TEXT PRIMARY KEY REFERENCES accounts (id) ON DELETE RESTRICT ); +-- ── Account handles (the resolution index) ────────────────────────────────── +-- Handle→id resolution's source of truth (RIG-2751 handle cutover). One row per +-- account. owner_user_id is NULL for user and system accounts and the owning +-- user's id for agent accounts (it mirrors agent_accounts.owner_user_id), which +-- is what makes an agent handle unique only within its owner's namespace while a +-- user/system handle is globally unique. account_id is PK/FK to accounts (one +-- handle row per account); owner_user_id FKs user_accounts so an agent handle's +-- owner is a real user. Both FKs ON DELETE RESTRICT so a referenced account or +-- owner cannot be orphaned out from under a handle row. +CREATE TABLE account_handles ( + account_id TEXT PRIMARY KEY REFERENCES accounts (id) ON DELETE RESTRICT, + handle TEXT NOT NULL, + owner_user_id TEXT REFERENCES user_accounts (account_id) ON DELETE RESTRICT +); + +-- The two partial-unique indexes ARE the resolution index and enforce the +-- two-namespace contract: +-- * user/system handles (owner_user_id IS NULL) are globally unique — the +-- tier that preserves today's accounts.handle global-unique invariant; +-- * agent handles (owner_user_id IS NOT NULL) are unique only per owner. +-- An agent handle MAY overlap a global user handle with no collision at resolve +-- time: a user is only ever looked up bare (first index) and an agent only ever +-- owner-qualified (second index), so the two never contend on one lookup. +CREATE UNIQUE INDEX account_handles_global_key ON account_handles (handle) WHERE owner_user_id IS NULL; +CREATE UNIQUE INDEX account_handles_owner_key ON account_handles (owner_user_id, handle) WHERE owner_user_id IS NOT NULL; + -- ── Channel groups ────────────────────────────────────────────────────────── -- Namespace nodes. parent_group_id nests them (NULL = a top-level root); -- owner_user_id is the user whose space this is (empty string for a diff --git a/go/internal/store/system_account_exclusion_pgtest_test.go b/go/internal/store/system_account_exclusion_pgtest_test.go index c99687a1..76b5c5b5 100644 --- a/go/internal/store/system_account_exclusion_pgtest_test.go +++ b/go/internal/store/system_account_exclusion_pgtest_test.go @@ -132,7 +132,12 @@ func TestSystemAccountByHandleIsNotFound(t *testing.T) { t.Fatalf("GetAccount(%s) after seed: %v; the account must exist for the ErrNotFound below to prove the IsAgent gate", sys.ID, err) } - _, err = s.AgentByHandle(ctx, SystemAccountHandle) + // A system account carries a global (owner_user_id NULL) handle row, never an + // agent-namespace one, so an owner-qualified agent lookup misses regardless of + // the owner passed — the IsAgent gate's fail-closed, indistinguishable from + // unknown. + owner := mustUser(t, s, "owner") + _, err = s.AgentByHandle(ctx, owner.ID, SystemAccountHandle) sentinelIs(t, err, ErrNotFound, "AgentByHandle on the reserved system handle") } @@ -326,7 +331,8 @@ func TestLinearBridgeAccountByHandleIsNotFound(t *testing.T) { t.Fatalf("GetAccount(%s) after seed: %v; the account must exist for the ErrNotFound below to prove the IsAgent gate", linear.ID, err) } - _, err = s.AgentByHandle(ctx, LinearBridgeAccountHandle) + owner := mustUser(t, s, "owner") + _, err = s.AgentByHandle(ctx, owner.ID, LinearBridgeAccountHandle) sentinelIs(t, err, ErrNotFound, "AgentByHandle on the reserved @linear handle") } diff --git a/go/server/dev_door_pgtest_test.go b/go/server/dev_door_pgtest_test.go index 68dc1765..cc902e2f 100644 --- a/go/server/dev_door_pgtest_test.go +++ b/go/server/dev_door_pgtest_test.go @@ -76,7 +76,7 @@ func TestDevDoorGatesAdminOnlyRPCsWithoutBearer(t *testing.T) { t.Run("IssueToken is PermissionDenied and mints no token", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - resp, err := compassClient.IssueToken(ctx, connect.NewRequest(&compassv1.IssueTokenRequest{AccountId: "any-account"})) + resp, err := compassClient.IssueToken(ctx, connect.NewRequest(&compassv1.IssueTokenRequest{AccountHandle: "any-account"})) if code := connect.CodeOf(err); code != connect.CodePermissionDenied { t.Fatalf("IssueToken on the dev door = %v, want CodePermissionDenied (no bearer → no caller → fail-closed)", code) } diff --git a/go/server/forge_e2e_pgtest_test.go b/go/server/forge_e2e_pgtest_test.go index c5a9ecd3..ed61f814 100644 --- a/go/server/forge_e2e_pgtest_test.go +++ b/go/server/forge_e2e_pgtest_test.go @@ -381,9 +381,7 @@ func TestForgeNoLiveSessionFailsClosedOverTheWire(t *testing.T) { // The seam is already live (the supervisor started in newE2EWire), so a direct // Provision succeeds without the retry gate; a unique request id avoids // colliding with the supervisor's provision. - provResp, _, err := w.hub.Provision(ctx, "prov-unbound", &compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(unboundAgent.ID), - }) + provResp, _, err := w.hub.Provision(ctx, "prov-unbound", &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: string(unboundAgent.ID)}) if err != nil { t.Fatalf("hub.Provision(unbound agent) = %v", err) } diff --git a/go/server/lifecycle.go b/go/server/lifecycle.go index cf90c3f3..494eea9b 100644 --- a/go/server/lifecycle.go +++ b/go/server/lifecycle.go @@ -142,14 +142,20 @@ var errCallerNotAgent = errors.New("resolved caller is not an agent account") // spawn takes. The new agent's owner is the caller agent's owner (F2), resolved // from the store — never the caller itself, never admin, never a client value. // -// Idempotency / resume on a taken handle. A duplicate handle is not blindly an -// error: if the existing agent is owned by the SAME owner and has a live +// Idempotency / resume on a taken handle. A handle already taken in the CALLER'S +// OWN owner namespace is not blindly an error: if that existing agent has a live // placement, the spawn is an idempotent no-op returning the existing -// container/session (a completed-call retry); if it is same-owner but UNPLACED -// (a spawn that crashed after CreateAgent, or one rolled back), it is RESUMED — -// re-provisioned and started against the existing account rather than creating a -// second. A handle owned by a DIFFERENT user (or a non-agent account) is -// CodeAlreadyExists and never resumes/steals it. +// container/session (a completed-call retry); if it is UNPLACED (a spawn that +// crashed after CreateAgent, or one rolled back), it is RESUMED — re-provisioned +// and started against the existing account rather than creating a second. A +// handle owned by a DIFFERENT user is a distinct namespace and spawns a distinct +// peer (DL-271/OQ-7: two owners may each hold an agent named `compass-ux`); the +// caller never touches, resumes, or steals the other owner's agent. +// +// Shadow guard. Storage lets an agent handle overlap a user/system handle (the +// two partial-unique indexes never contend), but a spawned peer must never +// SHADOW a human or the system sender, so a spawn whose handle names an existing +// user/system account is refused CodeAlreadyExists before any create. // // Concurrent-spawn window. Two truly-concurrent same-handle+same-owner spawns // bearing DISTINCT client_request_ids can both reach provisionAndStart before @@ -177,6 +183,22 @@ func (l *lifecycleService) SpawnAsAccount( return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("resolving caller owner: %w", err)) } + // Shadow guard (defense in depth): an agent may never be spawned onto a + // handle a user or the system account already holds. Storage permits the + // overlap — a user/system handle lives in the global partial-unique index + // (owner_user_id IS NULL) and an agent handle in the per-owner one, so they + // never collide on insert — but a peer that shadowed a human's handle could + // be addressed in its place. Refuse it up front with the same in-band + // already_exists a duplicate agent handle gets, never revealing the kind of + // account that holds it. UserByHandle resolves the bare handle in that global + // index (users AND the system sender); a clean miss (ErrNotFound) is the + // common case and falls through to the create. + if _, err := l.store.UserByHandle(ctx, req.GetHandle()); err == nil { + return nil, connect.NewError(connect.CodeAlreadyExists, errHandleTaken) + } else if !errors.Is(err, store.ErrNotFound) { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("checking handle for user shadow: %w", err)) + } + // Persona and role are server-authoritative and empty on spawn // (SpawnPeerRequest carries neither): the new account is created with no // persona and no role, and the values threaded to the Runner come from the @@ -218,7 +240,7 @@ func (l *lifecycleService) DespawnAsAccount( ctx, cancel := context.WithTimeout(ctx, spawnChainTimeout) defer cancel() - target := store.AccountID(req.GetAgentAccountId()) + target := store.AccountID(req.GetAgentHandle()) if target == caller { return nil, connect.NewError(connect.CodeInvalidArgument, errCannotDespawnSelf) } @@ -287,30 +309,27 @@ func (l *lifecycleService) DespawnAsAccount( return &compassv1internal.DespawnPeerResponse{}, nil } -// resumeOrReject handles a spawn whose handle is already taken. Same-owner with -// no live placement resumes the existing account; same-owner already placed is an -// idempotent success returning the existing container/session; a foreign owner -// (or a non-agent handle) is CodeAlreadyExists that never resumes or steals it. +// resumeOrReject handles a spawn whose handle is already taken in the CALLER'S +// OWN owner namespace (CreateAgent conflicted on the per-owner agent index). An +// already-placed agent is an idempotent success returning the existing +// container/session; an unplaced one is resumed. The handle is guaranteed to +// resolve to a same-owner agent here: the shadow guard already excluded +// user/system handles, and a foreign owner's same-name agent lives in a +// separate namespace partition that never conflicts on insert — so a miss is an +// invariant violation, not a foreign or non-agent handle to collapse. func (l *lifecycleService) resumeOrReject( ctx context.Context, callerOwner store.AccountID, req *compassv1internal.SpawnPeerRequest, ) (*compassv1internal.SpawnPeerResponse, error) { - existing, err := l.store.AgentByHandle(ctx, req.GetHandle()) + existing, err := l.store.AgentByHandle(ctx, callerOwner, req.GetHandle()) if err != nil { - // The handle is taken (CreateAgent conflicted) but does not resolve to an - // agent — a non-agent account holds it. Collapse to already_exists; never - // reveal what kind of account it is. - if errors.Is(err, store.ErrNotFound) { - return nil, connect.NewError(connect.CodeAlreadyExists, errHandleTaken) - } + // CreateAgent conflicted on the caller's own agent namespace, so the + // handle MUST resolve to a same-owner agent here. A miss is an invariant + // violation (a torn index or a concurrent delete), not a routine outcome + // — surface it rather than masking a real fault as already_exists. return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("resolving existing handle: %w", err)) } - if existing.Agent.OwnerUserID != callerOwner { - // A DIFFERENT owner's agent holds this handle: never resume or steal it, - // and never reveal whose it is — the same already_exists a human gets. - return nil, connect.NewError(connect.CodeAlreadyExists, errHandleTaken) - } // Same owner: is it already placed (live), or unplaced (resumable)? _, container, err := l.store.PlacementForAgent(ctx, existing.ID) @@ -350,7 +369,7 @@ func (l *lifecycleService) provisionAndStart( req *compassv1internal.SpawnPeerRequest, ) (*compassv1internal.SpawnPeerResponse, error) { resp, runnerID, err := l.hub.Provision(ctx, req.GetClientRequestId(), &compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(agentID), + AgentHandle: string(agentID), ClientRequestId: req.GetClientRequestId(), Persona: persona, Role: role, diff --git a/go/server/lifecycle_e2e_pgtest_test.go b/go/server/lifecycle_e2e_pgtest_test.go index ebf20a02..09819adb 100644 --- a/go/server/lifecycle_e2e_pgtest_test.go +++ b/go/server/lifecycle_e2e_pgtest_test.go @@ -264,9 +264,7 @@ func e2eDespawnPeer(t *testing.T, w *e2eWire, peerID store.AccountID, peerContai ctx := w.ctx resp, err := w.supervisorClient.Lifecycle(ctx, connect.NewRequest(&compassv1internal.LifecycleCallRequest{ CallId: "despawn-call-1", - Call: &compassv1internal.LifecycleCallRequest_Despawn{Despawn: &compassv1internal.DespawnPeerRequest{ - AgentAccountId: string(peerID), - }}, + Call: &compassv1internal.LifecycleCallRequest_Despawn{Despawn: &compassv1internal.DespawnPeerRequest{AgentHandle: string(peerID)}}, })) if err != nil { t.Fatalf("Lifecycle(despawn) over the socket = %v, want the round-trip result", err) @@ -386,9 +384,7 @@ func TestForeignOwnerDespawnOverTheWireIsIndistinguishableNoOp(t *testing.T) { // socket. resp, err := w.supervisorClient.Lifecycle(ctx, connect.NewRequest(&compassv1internal.LifecycleCallRequest{ CallId: "foreign-despawn-1", - Call: &compassv1internal.LifecycleCallRequest_Despawn{Despawn: &compassv1internal.DespawnPeerRequest{ - AgentAccountId: string(peerBID), - }}, + Call: &compassv1internal.LifecycleCallRequest_Despawn{Despawn: &compassv1internal.DespawnPeerRequest{AgentHandle: string(peerBID)}}, })) if err != nil { t.Fatalf("Lifecycle(foreign despawn) over the socket = %v, want an in-band result", err) @@ -414,9 +410,8 @@ func TestForeignOwnerDespawnOverTheWireIsIndistinguishableNoOp(t *testing.T) { // same wire: both must return the SAME in-band not_found. unknownResp, err := w.supervisorClient.Lifecycle(ctx, connect.NewRequest(&compassv1internal.LifecycleCallRequest{ CallId: "unknown-despawn-1", - Call: &compassv1internal.LifecycleCallRequest_Despawn{Despawn: &compassv1internal.DespawnPeerRequest{ - AgentAccountId: "ffffffffffffffffffffffffffffffff", // well-formed, never minted - }}, + // well-formed, never minted: + Call: &compassv1internal.LifecycleCallRequest_Despawn{Despawn: &compassv1internal.DespawnPeerRequest{AgentHandle: "ffffffffffffffffffffffffffffffff"}}, })) if err != nil { t.Fatalf("Lifecycle(unknown despawn) = %v, want an in-band result", err) @@ -768,9 +763,7 @@ func provisionWhenSeamLiveE2E(t *testing.T, ctx context.Context, hub *runnerhub. t.Helper() deadline := time.After(e2eTimeout) for { - resp, _, err := hub.Provision(ctx, "prov-supervisor", &compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(agentID), - }) + resp, _, err := hub.Provision(ctx, "prov-supervisor", &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: string(agentID)}) if err == nil { name := resp.GetContainerName() if name == "" { diff --git a/go/server/lifecycle_pgtest_test.go b/go/server/lifecycle_pgtest_test.go index b8c37275..3a27a0ed 100644 --- a/go/server/lifecycle_pgtest_test.go +++ b/go/server/lifecycle_pgtest_test.go @@ -235,7 +235,7 @@ func TestSpawnMidChainFailureRollsBack(t *testing.T) { t.Fatalf("rollback never sent a Remove for %q; the container is stranded (commands: %v)", fakeContainer, f.runner.commands()) } // The account exists but is UNPLACED — the handle is not burned. - created, err := f.store.AgentByHandle(ctx, "peer-roll") + created, err := f.store.AgentByHandle(ctx, f.ownerAdmin, "peer-roll") if err != nil { t.Fatalf("AgentByHandle(peer-roll) after rollback = %v, want the durable account", err) } @@ -261,23 +261,34 @@ func TestSpawnMidChainFailureRollsBack(t *testing.T) { } } -// TestSpawnSameHandleDifferentOwnerIsAlreadyExists pins that a foreign owner's -// handle is never resumed or stolen: a second user's agent spawning a peer whose -// handle collides with the first owner's peer gets in-band already_exists, never -// a resume of the other owner's account. +// TestSpawnSameHandleDifferentOwnerCreatesDistinctPeer pins the per-owner handle +// namespace (DL-271/OQ-7): two owners may each hold an agent named the same +// handle, so a second user's agent spawning a peer whose handle matches the +// first owner's peer creates its OWN distinct agent under its OWN owner — it +// never resumes, steals, or even touches the first owner's account. // -// Mutation: dropping the owner check on the resume path (resuming any same-handle -// unplaced/placed agent regardless of owner) reddens this — a foreign caller would -// resume someone else's agent and get a success. -func TestSpawnSameHandleDifferentOwnerIsAlreadyExists(t *testing.T) { +// Mutation: dropping the owner qualification on the resume path (resolving the +// handle globally and resuming any same-handle agent) reddens this — owner-B +// would resume owner-A's account and get A's id back, collapsing the two +// distinct peers into one. +func TestSpawnSameHandleDifferentOwnerCreatesDistinctPeer(t *testing.T) { f := newLifecycleFixture(t) ctx := context.Background() - // Owner A's caller spawns peer-shared. - if _, err := f.lc.SpawnAsAccount(ctx, f.agentID, &compassv1internal.SpawnPeerRequest{ + // The two spawns land distinct containers AND distinct session ids (both are + // unique-keyed in the store); without distinct names the second placement, + // and without distinct ids the second session record, would collide on the + // fixture's fixed fakeContainer/fakeSessionID rather than exercising the + // cross-owner path. + f.runner.setContainerNames("compass-agent-a", "compass-agent-b") + f.runner.setStartIDs("sess-a", "sess-b") + + // Owner A's caller (the fixture agent, owned by admin) spawns peer-shared. + respA, err := f.lc.SpawnAsAccount(ctx, f.agentID, &compassv1internal.SpawnPeerRequest{ Handle: "peer-shared", ClientRequestId: "spawn-a", - }); err != nil { + }) + if err != nil { t.Fatalf("owner-A spawn = %v, want success", err) } @@ -291,15 +302,32 @@ func TestSpawnSameHandleDifferentOwnerIsAlreadyExists(t *testing.T) { t.Fatalf("CreateAgent(caller-b) = %v", err) } - _, err = f.lc.SpawnAsAccount(ctx, callerB.ID, &compassv1internal.SpawnPeerRequest{ + // Owner B spawns the SAME handle: a distinct agent in owner B's namespace. + respB, err := f.lc.SpawnAsAccount(ctx, callerB.ID, &compassv1internal.SpawnPeerRequest{ Handle: "peer-shared", ClientRequestId: "spawn-b", }) - if err == nil { - t.Fatal("owner-B spawn of owner-A's handle = success, want in-band already_exists (never steal)") + if err != nil { + t.Fatalf("owner-B spawn of the same handle = %v, want success (distinct per-owner peer)", err) } - if got := connect.CodeOf(err); got != connect.CodeAlreadyExists { - t.Fatalf("owner-B spawn code = %v, want CodeAlreadyExists", got) + if respA.GetAgentAccountId() == respB.GetAgentAccountId() { + t.Fatalf("owner-B spawn resumed owner-A's agent %q — the per-owner namespace is broken (never steal)", respA.GetAgentAccountId()) + } + + // Each owner's handle resolves to its own agent; A's is untouched. + agentA, err := f.store.AgentByHandle(ctx, f.ownerAdmin, "peer-shared") + if err != nil { + t.Fatalf("AgentByHandle(ownerA, peer-shared) = %v, want owner-A's agent", err) + } + if string(agentA.ID) != respA.GetAgentAccountId() { + t.Fatalf("owner-A's handle resolves to %q, want the spawned %q", agentA.ID, respA.GetAgentAccountId()) + } + agentB, err := f.store.AgentByHandle(ctx, userB.ID, "peer-shared") + if err != nil { + t.Fatalf("AgentByHandle(ownerB, peer-shared) = %v, want owner-B's agent", err) + } + if string(agentB.ID) != respB.GetAgentAccountId() { + t.Fatalf("owner-B's handle resolves to %q, want the spawned %q", agentB.ID, respB.GetAgentAccountId()) } } @@ -333,17 +361,13 @@ func TestDespawnDifferentOwnerIsIndistinguishableNotFound(t *testing.T) { } // The fixture caller (owner A) tries to despawn owner B's peer. - _, foreignErr := f.lc.DespawnAsAccount(ctx, f.agentID, &compassv1internal.DespawnPeerRequest{ - AgentAccountId: peerB.GetAgentAccountId(), - }) + _, foreignErr := f.lc.DespawnAsAccount(ctx, f.agentID, &compassv1internal.DespawnPeerRequest{AgentHandle: peerB.GetAgentAccountId()}) if foreignErr == nil { t.Fatal("despawn of a foreign-owner peer = success, want CodeNotFound (never touch a foreign peer)") } // The same caller despawns an entirely unknown id. - _, unknownErr := f.lc.DespawnAsAccount(ctx, f.agentID, &compassv1internal.DespawnPeerRequest{ - AgentAccountId: "acct-does-not-exist", - }) + _, unknownErr := f.lc.DespawnAsAccount(ctx, f.agentID, &compassv1internal.DespawnPeerRequest{AgentHandle: "acct-does-not-exist"}) if unknownErr == nil { t.Fatal("despawn of an unknown id = success, want CodeNotFound") } @@ -367,9 +391,7 @@ func TestDespawnSelfIsInvalidArgument(t *testing.T) { f := newLifecycleFixture(t) ctx := context.Background() - _, err := f.lc.DespawnAsAccount(ctx, f.agentID, &compassv1internal.DespawnPeerRequest{ - AgentAccountId: string(f.agentID), - }) + _, err := f.lc.DespawnAsAccount(ctx, f.agentID, &compassv1internal.DespawnPeerRequest{AgentHandle: string(f.agentID)}) if err == nil { t.Fatal("despawn of self = success, want CodeInvalidArgument") } @@ -403,9 +425,7 @@ func TestDespawnSameOwnerSiblingSucceeds(t *testing.T) { t.Fatalf("CreateAgent(other-sib) = %v", err) } - if _, err := f.lc.DespawnAsAccount(ctx, other.ID, &compassv1internal.DespawnPeerRequest{ - AgentAccountId: target.GetAgentAccountId(), - }); err != nil { + if _, err := f.lc.DespawnAsAccount(ctx, other.ID, &compassv1internal.DespawnPeerRequest{AgentHandle: target.GetAgentAccountId()}); err != nil { t.Fatalf("same-owner sibling despawn = %v, want success (owner authority, not spawner)", err) } @@ -435,7 +455,7 @@ func TestDespawnSecondTimeIsIdempotentSuccess(t *testing.T) { if err != nil { t.Fatalf("spawn = %v, want success", err) } - req := &compassv1internal.DespawnPeerRequest{AgentAccountId: target.GetAgentAccountId()} + req := &compassv1internal.DespawnPeerRequest{AgentHandle: target.GetAgentAccountId()} if _, err := f.lc.DespawnAsAccount(ctx, f.agentID, req); err != nil { t.Fatalf("first despawn = %v, want success", err) @@ -458,10 +478,7 @@ func TestRemoveAgentWorkspaceHandler(t *testing.T) { ctx := context.Background() // Provision a real placement to release. - if _, err := f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(f.agentID), - ClientRequestId: "prov-rm", - })); err != nil { + if _, err := f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: string(f.agentID), ClientRequestId: "prov-rm"})); err != nil { t.Fatalf("ProvisionAgentWorkspace = %v, want success", err) } if _, _, err := f.store.PlacementForAgent(ctx, f.agentID); err != nil { @@ -530,9 +547,7 @@ func TestDespawnCallerNotAnAgentIsInternal(t *testing.T) { } // Target differs from the caller so the self-despawn guard does not fire first. - _, err = f.lc.DespawnAsAccount(ctx, user.ID, &compassv1internal.DespawnPeerRequest{ - AgentAccountId: "acct-some-other-id", - }) + _, err = f.lc.DespawnAsAccount(ctx, user.ID, &compassv1internal.DespawnPeerRequest{AgentHandle: "acct-some-other-id"}) if err == nil { t.Fatal("despawn by a non-agent caller = success, want CodeInternal (errCallerNotAgent)") } @@ -544,15 +559,19 @@ func TestDespawnCallerNotAnAgentIsInternal(t *testing.T) { } } -// TestSpawnHandleCollidesWithUserAccountIsAlreadyExists pins that a spawn Handle -// colliding with a NON-agent (user) account collapses to CodeAlreadyExists — -// NOT a resume, and NOT a leak that the handle belongs to a user. CreateAgent -// conflicts on the taken handle, AgentByHandle fails closed to ErrNotFound for a -// non-agent handle, and resumeOrReject maps that to errHandleTaken — the same -// answer a human gets for any taken handle. +// TestSpawnHandleCollidesWithUserAccountIsAlreadyExists pins the shadow guard: a +// spawn Handle naming an existing USER (or SYSTEM) account collapses to +// CodeAlreadyExists — NOT a resume, and NOT a leak that the handle belongs to a +// user. Storage would permit the overlap (a user handle lives in the global +// partial-unique index, an agent handle in the per-owner one, so they never +// collide on insert), so a dedicated pre-create guard resolves the handle in the +// global user/system index and refuses it with the same already_exists a human +// gets for any taken handle — a peer must never shadow a human or the system +// sender. // -// Mutation: revealing the account kind (e.g. a distinct code for a user-held -// handle) or resuming against a non-agent account reddens this. +// Mutation: dropping the shadow guard lets the spawn succeed onto a human's +// handle (storage permits it); revealing the account kind (a distinct code) +// reddens the indistinguishability. func TestSpawnHandleCollidesWithUserAccountIsAlreadyExists(t *testing.T) { f := newLifecycleFixture(t) ctx := context.Background() @@ -576,3 +595,36 @@ func TestSpawnHandleCollidesWithUserAccountIsAlreadyExists(t *testing.T) { t.Fatalf("user-handle-collision spawn err = %v, want wrapping errHandleTaken", err) } } + +// TestSpawnHandleCollidesWithSystemAccountIsAlreadyExists is the shadow guard's +// system-tier half: a spawn Handle naming the system sender (@compass) is +// refused the same already_exists as a user collision — the system handle also +// lives in the global index UserByHandle resolves, so a peer can never shadow it. +// The load-bearing assertion is the error CODE shape: a system handle is also a +// reserved handle (store/handle.go), so validateHandle is a second, independent +// line of defense that would reject it as CodeInternal — this test pins that a +// system collision returns the INDISTINGUISHABLE CodeAlreadyExists, never the +// reserved path's distinguishable Internal. +func TestSpawnHandleCollidesWithSystemAccountIsAlreadyExists(t *testing.T) { + f := newLifecycleFixture(t) + ctx := context.Background() + + sys, err := f.store.EnsureSystemAccount(ctx) + if err != nil { + t.Fatalf("EnsureSystemAccount = %v", err) + } + + _, err = f.lc.SpawnAsAccount(ctx, f.agentID, &compassv1internal.SpawnPeerRequest{ + Handle: sys.Handle, + ClientRequestId: "spawn-collides-system", + }) + if err == nil { + t.Fatal("spawn onto the system handle = success, want CodeAlreadyExists (never shadow the system sender)") + } + if got := connect.CodeOf(err); got != connect.CodeAlreadyExists { + t.Fatalf("system-handle-collision spawn code = %v, want CodeAlreadyExists", got) + } + if !errors.Is(err, errHandleTaken) { + t.Fatalf("system-handle-collision spawn err = %v, want wrapping errHandleTaken", err) + } +} diff --git a/go/server/lifecycle_test.go b/go/server/lifecycle_test.go index 6d3e80c4..d027a0d1 100644 --- a/go/server/lifecycle_test.go +++ b/go/server/lifecycle_test.go @@ -31,9 +31,7 @@ func TestDespawnSelfIsRefusedBeforeAnyStoreCall(t *testing.T) { lc := newLifecycleService(nil, nil) // nil store + hub: any store/hub call would panic const self = store.AccountID("agent-self") - _, err := lc.DespawnAsAccount(context.Background(), self, &compassv1internal.DespawnPeerRequest{ - AgentAccountId: string(self), - }) + _, err := lc.DespawnAsAccount(context.Background(), self, &compassv1internal.DespawnPeerRequest{AgentHandle: string(self)}) if err == nil { t.Fatal("DespawnAsAccount(self) = nil error, want CodeInvalidArgument (no self-despawn)") } diff --git a/go/server/lifecycle_wake_pgtest_test.go b/go/server/lifecycle_wake_pgtest_test.go index 57f3d0b2..cf02bb86 100644 --- a/go/server/lifecycle_wake_pgtest_test.go +++ b/go/server/lifecycle_wake_pgtest_test.go @@ -50,9 +50,7 @@ func TestWakeAgentLiveIsNoOp(t *testing.T) { // Make the agent LIVE through the real Provision->Start promotion path the // hub drives: bindContainer then promoteSession bind (agent -> live session). - if _, _, err := f.hub.Provision(ctx, "prov-live", &compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(f.agentID), - }); err != nil { + if _, _, err := f.hub.Provision(ctx, "prov-live", &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: string(f.agentID)}); err != nil { t.Fatalf("Provision = %v, want success", err) } if _, err := f.hub.Start(ctx, "start-live", &compassv1.StartAgentSessionRequest{ContainerName: fakeContainer}); err != nil { @@ -257,9 +255,7 @@ func TestWakeAgentSingleflightCoalescesToOneStart(t *testing.T) { // That closes the only residual race: a follower that reaches wakeGroup.Do // just AFTER the leader releases finds the agent live at the not-live // pre-check and no-ops, so it never starts a second session either. - if _, _, err := f.hub.Provision(ctx, "prov-sf", &compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(f.agentID), - }); err != nil { + if _, _, err := f.hub.Provision(ctx, "prov-sf", &compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: string(f.agentID)}); err != nil { t.Fatalf("Provision (bind container): %v", err) } f.runner.forget() diff --git a/go/server/network_door_proxy_bypass_pgtest_test.go b/go/server/network_door_proxy_bypass_pgtest_test.go index 4d99bfce..fc3632b0 100644 --- a/go/server/network_door_proxy_bypass_pgtest_test.go +++ b/go/server/network_door_proxy_bypass_pgtest_test.go @@ -154,7 +154,7 @@ func TestNetworkDoorProxyHeadersDoNotElevate(t *testing.T) { forged := forgedProxyHeaders("admin") t.Run("non-admin bearer + forged admin headers still PermissionDenied on adminOnly RPC", func(t *testing.T) { - req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountId: string(admin)}) + req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountHandle: string(admin)}) req.Header().Set("Authorization", "Bearer "+memberTok) applyHeaders(req.Header(), forged) if _, err := client.IssueToken(t.Context(), req); connect.CodeOf(err) != connect.CodePermissionDenied { @@ -208,7 +208,7 @@ func TestNetworkDoorProxyHeadersAreInertForAdmin(t *testing.T) { // handler and mints a token; the assertion is on success reaching the handler, // identical with and without the forged headers. issue := func(withForged bool) error { - req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountId: string(admin)}) + req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountHandle: string(admin)}) req.Header().Set("Authorization", "Bearer "+adminTok) if withForged { applyHeaders(req.Header(), forgedProxyHeaders("member")) @@ -332,7 +332,7 @@ func TestNetworkDoorSmuggledSecondAuthorizationDoesNotElevate(t *testing.T) { svc := newService("proxy-smuggle", bus, st, nil, nil, nil, nil) client := networkDoorHandler(t, svc, st, admin) - req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountId: string(admin)}) + req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountHandle: string(admin)}) req.Header().Set("Authorization", "Bearer "+memberTok) req.Header().Add("Authorization", "Bearer "+adminTok) diff --git a/go/server/network_door_test.go b/go/server/network_door_test.go index 78b0d477..0dd2b952 100644 --- a/go/server/network_door_test.go +++ b/go/server/network_door_test.go @@ -401,7 +401,7 @@ func TestNetworkDoorBearerAuthAcceptAndReject(t *testing.T) { // issue calls IssueToken (adminOnly) carrying the given bearer value ("" // leaves the Authorization header absent), returning connect's error code. issue := func(bearer string) connect.Code { - req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountId: string(admin)}) + req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountHandle: string(admin)}) if bearer != "" { req.Header().Set("Authorization", bearer) } @@ -412,7 +412,7 @@ func TestNetworkDoorBearerAuthAcceptAndReject(t *testing.T) { } t.Run("valid admin token on adminOnly RPC succeeds", func(t *testing.T) { - req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountId: string(admin)}) + req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountHandle: string(admin)}) req.Header().Set("Authorization", "Bearer "+adminTok) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() @@ -658,7 +658,7 @@ func TestIssueTokenHandlerInputContract(t *testing.T) { t.Run("empty account id is InvalidArgument", func(t *testing.T) { rpcCtx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - _, err := client.IssueToken(rpcCtx, connect.NewRequest(&compassv1.IssueTokenRequest{AccountId: ""})) + _, err := client.IssueToken(rpcCtx, connect.NewRequest(&compassv1.IssueTokenRequest{AccountHandle: ""})) if code := connect.CodeOf(err); code != connect.CodeInvalidArgument { t.Fatalf("empty account_id = %v, want CodeInvalidArgument", code) } @@ -667,7 +667,7 @@ func TestIssueTokenHandlerInputContract(t *testing.T) { t.Run("unknown account id is NotFound", func(t *testing.T) { rpcCtx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - _, err := client.IssueToken(rpcCtx, connect.NewRequest(&compassv1.IssueTokenRequest{AccountId: unknownAccountID})) + _, err := client.IssueToken(rpcCtx, connect.NewRequest(&compassv1.IssueTokenRequest{AccountHandle: unknownAccountID})) if code := connect.CodeOf(err); code != connect.CodeNotFound { t.Fatalf("non-empty but unknown account_id = %v, want CodeNotFound", code) } @@ -676,7 +676,7 @@ func TestIssueTokenHandlerInputContract(t *testing.T) { t.Run("existing account mints a resolvable token", func(t *testing.T) { rpcCtx, cancel := context.WithTimeout(ctx, testTimeout) defer cancel() - resp, err := client.IssueToken(rpcCtx, connect.NewRequest(&compassv1.IssueTokenRequest{AccountId: string(admin)})) + resp, err := client.IssueToken(rpcCtx, connect.NewRequest(&compassv1.IssueTokenRequest{AccountHandle: string(admin)})) if err != nil { t.Fatalf("IssueToken for an existing account: %v", err) } @@ -700,7 +700,7 @@ func TestIssueTokenHandlerInputContract(t *testing.T) { if err != nil { t.Fatalf("EnsureSystemAccount: %v", err) } - resp, err := client.IssueToken(rpcCtx, connect.NewRequest(&compassv1.IssueTokenRequest{AccountId: string(sys.ID)})) + resp, err := client.IssueToken(rpcCtx, connect.NewRequest(&compassv1.IssueTokenRequest{AccountHandle: string(sys.ID)})) if code := connect.CodeOf(err); code != connect.CodePermissionDenied { t.Fatalf("IssueToken for the system account = %v, want CodePermissionDenied — @compass is not authenticatable", code) } @@ -752,7 +752,7 @@ func TestServeWithListenWritesAdminToken0600(t *testing.T) { } client := newTLSClient(t, addr, pool) - req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountId: unknownAccountID}) + req := connect.NewRequest(&compassv1.IssueTokenRequest{AccountHandle: unknownAccountID}) req.Header().Set("Authorization", "Bearer "+token) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() diff --git a/go/server/serve_seed.go b/go/server/serve_seed.go index 4423b7e8..92709538 100644 --- a/go/server/serve_seed.go +++ b/go/server/serve_seed.go @@ -92,7 +92,7 @@ func seedRootSupervisor(ctx context.Context, st *store.Store, svc *service, cm * // Find-or-create the supervisor. AgentByHandle resolves a prior boot's row; // ErrNotFound means it does not exist yet, so fall through to the empty-tree // create gate. - supervisor, err := st.AgentByHandle(ctx, rootSupervisorHandle) + supervisor, err := st.AgentByHandle(ctx, adminID, rootSupervisorHandle) switch { case err == nil: // Exists already (prior boot). The find half resolves by a globally @@ -123,7 +123,7 @@ func seedRootSupervisor(ctx context.Context, st *store.Store, svc *service, cm * // spawn memo make this a no-op for an already-live supervisor, so a re-enroll // re-fire never launches a second container. if _, err := svc.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{ - AgentAccountId: string(supervisor.ID), + AgentHandle: string(supervisor.ID), ClientRequestId: seedClientRequestID, })); err != nil { if connect.CodeOf(err) == connect.CodeAlreadyExists { diff --git a/go/server/serve_seed_pgtest_test.go b/go/server/serve_seed_pgtest_test.go index cd622dff..8b4df2f4 100644 --- a/go/server/serve_seed_pgtest_test.go +++ b/go/server/serve_seed_pgtest_test.go @@ -142,7 +142,7 @@ func TestServeSeedsRootSupervisorOnEmptyTree(t *testing.T) { attachFakeRunner(t, h.store, h.hub, false) // enroll + Sessions attach -> ready hook -> seed on an empty tree h.awaitSeed(t) - supervisor, err := h.store.AgentByHandle(ctx, rootSupervisorHandle) + supervisor, err := h.store.AgentByHandle(ctx, h.adminID, rootSupervisorHandle) if err != nil { t.Fatalf("AgentByHandle(supervisor) after seed = %v, want the seeded root", err) } @@ -190,7 +190,7 @@ func TestServeSeedsNothingOnNonEmptyTree(t *testing.T) { attachFakeRunner(t, h.store, h.hub, false) // enroll + Sessions attach -> ready hook -> seed on a NON-empty tree h.awaitSeed(t) - if _, err := h.store.AgentByHandle(ctx, rootSupervisorHandle); err == nil { + if _, err := h.store.AgentByHandle(ctx, h.adminID, rootSupervisorHandle); err == nil { t.Fatal("a supervisor was seeded on a non-empty tree, want none") } else if !errors.Is(err, store.ErrNotFound) { t.Fatalf("AgentByHandle(supervisor) = %v, want ErrNotFound (never seeded)", err) @@ -243,7 +243,7 @@ func TestServeReDrivesNeverStartedSupervisor(t *testing.T) { // The find half re-drove the EXISTING row: same id + display name, never // re-created, and still the admin's single root. - supervisor, err := h.store.AgentByHandle(ctx, rootSupervisorHandle) + supervisor, err := h.store.AgentByHandle(ctx, h.adminID, rootSupervisorHandle) if err != nil { t.Fatalf("AgentByHandle(supervisor) after re-drive = %v, want the pre-created root", err) } @@ -297,7 +297,7 @@ func TestSeedPostsSetupThreadAsCompass(t *testing.T) { attachFakeRunner(t, h.store, h.hub, false) h.awaitSeed(t) - supervisor, err := h.store.AgentByHandle(ctx, rootSupervisorHandle) + supervisor, err := h.store.AgentByHandle(ctx, h.adminID, rootSupervisorHandle) if err != nil { t.Fatalf("AgentByHandle(supervisor) after seed = %v, want the seeded root", err) } @@ -329,7 +329,7 @@ func TestSeedSetupThreadIdempotentOnReFire(t *testing.T) { attachFakeRunner(t, h.store, h.hub, false) h.awaitSeed(t) - supervisor, err := h.store.AgentByHandle(ctx, rootSupervisorHandle) + supervisor, err := h.store.AgentByHandle(ctx, h.adminID, rootSupervisorHandle) if err != nil { t.Fatalf("AgentByHandle(supervisor) after seed = %v, want the seeded root", err) } @@ -370,7 +370,7 @@ func TestSeedSetupThreadPublishesOneMessagePosted(t *testing.T) { attachFakeRunner(t, h.store, h.hub, false) h.awaitSeed(t) - supervisor, err := h.store.AgentByHandle(ctx, rootSupervisorHandle) + supervisor, err := h.store.AgentByHandle(ctx, h.adminID, rootSupervisorHandle) if err != nil { t.Fatalf("AgentByHandle(supervisor) after seed = %v, want the seeded root", err) } diff --git a/go/server/service.go b/go/server/service.go index e3789fff..5974105d 100644 --- a/go/server/service.go +++ b/go/server/service.go @@ -149,10 +149,10 @@ func (s *service) ProvisionAgentWorkspace( // prompt or a role prompt. A non-agent account carries neither, but the // client values are still cleared for the same reason. The Runner receives // these on the same relayed req.Msg. - acc, err := s.store.GetAccount(ctx, store.AccountID(req.Msg.GetAgentAccountId())) + acc, err := s.store.GetAccount(ctx, store.AccountID(req.Msg.GetAgentHandle())) if err != nil { if errors.Is(err, store.ErrNotFound) { - return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("no account with id %s", req.Msg.GetAgentAccountId())) + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("no account with id %s", req.Msg.GetAgentHandle())) } return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("reading agent account for persona: %w", err)) } @@ -182,7 +182,7 @@ func (s *service) ProvisionAgentWorkspace( // Provision and Start left StartAgentSession unable to say whose session it // was recording. It is also what SEA-1516 reattach recovery reads to name // every agent stranded by a Runner restart. - if err := s.store.RecordAgentPlacement(ctx, store.AccountID(req.Msg.GetAgentAccountId()), runnerID, resp.GetContainerName()); err != nil { + if err := s.store.RecordAgentPlacement(ctx, store.AccountID(req.Msg.GetAgentHandle()), runnerID, resp.GetContainerName()); err != nil { return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("recording agent placement: %w", err)) } return connect.NewResponse(resp), nil @@ -417,7 +417,7 @@ func (s *service) IssueToken( ctx context.Context, req *connect.Request[compassv1.IssueTokenRequest], ) (*connect.Response[compassv1.IssueTokenResponse], error) { - id := store.AccountID(req.Msg.GetAccountId()) + id := store.AccountID(req.Msg.GetAccountHandle()) if id == "" { return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("account_id is required")) diff --git a/go/server/service_placement_pgtest_test.go b/go/server/service_placement_pgtest_test.go index 3fe6ebc7..8e6fc467 100644 --- a/go/server/service_placement_pgtest_test.go +++ b/go/server/service_placement_pgtest_test.go @@ -286,10 +286,7 @@ func TestProvisionAgentWorkspaceRecordsPlacementNamingServingRunner(t *testing.T f := newPlacementFixture(t) ctx := context.Background() // the test root context - resp, err := f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(f.agentID), - ClientRequestId: "prov-1", - })) + resp, err := f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: string(f.agentID), ClientRequestId: "prov-1"})) if err != nil { t.Fatalf("ProvisionAgentWorkspace = %v, want success", err) } @@ -354,11 +351,8 @@ func TestProvisionAgentWorkspaceOverwritesPersonaFromStore(t *testing.T) { } f.runner.forget() // discard the attach probe - if _, err := f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(personaAgent.ID), - ClientRequestId: "prov-persona", - Persona: "CLIENT-INJECTED-EVIL", - })); err != nil { + if _, err := f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: string(personaAgent.ID), ClientRequestId: "prov-persona", + Persona: "CLIENT-INJECTED-EVIL"})); err != nil { t.Fatalf("ProvisionAgentWorkspace = %v, want success", err) } @@ -389,11 +383,8 @@ func TestProvisionAgentWorkspaceClearsPersonaForNonAgentAccount(t *testing.T) { // persona-clear is still observable because the Provision command is recorded // (persona cleared) before the placement write runs. The error is expected // and not what this test pins, so it is deliberately discarded. - _, _ = f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(adminID), - ClientRequestId: "prov-nonagent", - Persona: "CLIENT-INJECTED-EVIL", - })) + _, _ = f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: string(adminID), ClientRequestId: "prov-nonagent", + Persona: "CLIENT-INJECTED-EVIL"})) if got := f.runner.provisionPersona(t); got != "" { t.Fatalf("Runner received persona %q for a non-agent account, want empty (client value must be cleared)", got) @@ -428,11 +419,8 @@ func TestProvisionAgentWorkspaceOverwritesRoleFromStore(t *testing.T) { } f.runner.forget() // discard the attach probe - if _, err := f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(roleAgent.ID), - ClientRequestId: "prov-role", - Role: "client-injected-evil", - })); err != nil { + if _, err := f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: string(roleAgent.ID), ClientRequestId: "prov-role", + Role: "client-injected-evil"})); err != nil { t.Fatalf("ProvisionAgentWorkspace = %v, want success", err) } @@ -463,11 +451,8 @@ func TestProvisionAgentWorkspaceClearsRoleForNonAgentAccount(t *testing.T) { // role-clear is still observable because the Provision command is recorded // (role cleared) before the placement write runs. The error is expected and // not what this test pins, so it is deliberately discarded. - _, _ = f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: string(adminID), - ClientRequestId: "prov-nonagent-role", - Role: "client-injected-evil", - })) + _, _ = f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: string(adminID), ClientRequestId: "prov-nonagent-role", + Role: "client-injected-evil"})) if got := f.runner.provisionRole(t); got != "" { t.Fatalf("Runner received role %q for a non-agent account, want empty (client value must be cleared)", got) @@ -481,11 +466,8 @@ func TestProvisionAgentWorkspaceUnknownAccountIsNotFound(t *testing.T) { f := newPlacementFixture(t) ctx := context.Background() // the test root context - _, err := f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: "acct-does-not-exist", - ClientRequestId: "prov-unknown", - Persona: "whatever", - })) + _, err := f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: "acct-does-not-exist", ClientRequestId: "prov-unknown", + Persona: "whatever"})) if err == nil { t.Fatalf("ProvisionAgentWorkspace = nil error, want CodeNotFound for an unknown account id") } @@ -658,6 +640,13 @@ type recordingRunner struct { // logical session) gets distinct live ids. Empty falls back to answer()'s // fixed fakeSessionID. Read/popped under mu. startIDs []string + // containerNames, when non-empty, overrides the fixed answer() Provision + // container name one per Provision (FIFO) — so a test driving several + // distinct spawns (e.g. two owners spawning the same handle into separate + // namespaces) gets distinct container names instead of colliding on the one + // fakeContainer placement. Empty falls back to answer()'s fixed fakeContainer. + // Read/popped under mu. + containerNames []string // statuses, when set, is what the loop answers a GetAgentStatus command with // (the all-sessions scan the SpawnAgent reject-on-live check reads). Empty // answers an empty set — no live session, so reject-on-live never fires. @@ -740,6 +729,18 @@ func (r *recordingRunner) serve( continue } } + if cmd.GetProvision() != nil { + if name, ok := r.nextContainerName(); ok { + if err := stream.Send(&compassv1internal.SessionsRequest{ + RequestId: cmd.GetRequestId(), + Result: &compassv1internal.SessionsRequest_Provision{Provision: &compassv1.ProvisionAgentWorkspaceResponse{ContainerName: name}}, + }); err != nil { + done <- err + return + } + continue + } + } if cmd.GetStatus() != nil { if err := stream.Send(&compassv1internal.SessionsRequest{ RequestId: cmd.GetRequestId(), @@ -779,7 +780,7 @@ func (r *recordingRunner) commands() []string { for _, c := range r.seen { switch v := c.GetCommand().(type) { case *compassv1internal.SessionsResponse_Provision: - out = append(out, "provision "+v.Provision.GetAgentAccountId()) + out = append(out, "provision "+v.Provision.GetAgentHandle()) case *compassv1internal.SessionsResponse_Start: out = append(out, "start "+v.Start.GetContainerName()) case *compassv1internal.SessionsResponse_Stop: @@ -840,6 +841,28 @@ func (r *recordingRunner) setStartIDs(ids ...string) { r.startIDs = append([]string(nil), ids...) } +// nextContainerName pops the next overriding Provision container name (FIFO), +// returning ok=false once the queue is empty (the loop then falls back to +// answer()'s fixed fakeContainer). +func (r *recordingRunner) nextContainerName() (string, bool) { + r.mu.Lock() + defer r.mu.Unlock() + if len(r.containerNames) == 0 { + return "", false + } + name := r.containerNames[0] + r.containerNames = r.containerNames[1:] + return name, true +} + +// setContainerNames queues the container names the loop answers successive +// Provisions with. Set before the Provisions it should affect are driven. +func (r *recordingRunner) setContainerNames(names ...string) { + r.mu.Lock() + defer r.mu.Unlock() + r.containerNames = append([]string(nil), names...) +} + // setStartGate installs a gate the serve loop blocks each Start on until the gate // is closed. Set before the Starts it should hold. Closing the returned channel // releases every blocked Start. diff --git a/go/server/service_spawn_pgtest_test.go b/go/server/service_spawn_pgtest_test.go index ba0824bf..64de9194 100644 --- a/go/server/service_spawn_pgtest_test.go +++ b/go/server/service_spawn_pgtest_test.go @@ -37,10 +37,7 @@ func TestSpawnAgentRunsProvisionThenStart(t *testing.T) { f := newPlacementFixture(t) ctx := context.Background() - resp, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{ - AgentAccountId: string(f.agentID), - ClientRequestId: "spawn-happy", - })) + resp, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{AgentHandle: string(f.agentID), ClientRequestId: "spawn-happy"})) if err != nil { t.Fatalf("SpawnAgent = %v, want success", err) } @@ -72,17 +69,11 @@ func TestSpawnAgentIsIdempotentOnRepeatedClientRequestId(t *testing.T) { f := newPlacementFixture(t) ctx := context.Background() - first, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{ - AgentAccountId: string(f.agentID), - ClientRequestId: "spawn-dup", - })) + first, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{AgentHandle: string(f.agentID), ClientRequestId: "spawn-dup"})) if err != nil { t.Fatalf("first SpawnAgent = %v, want success", err) } - second, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{ - AgentAccountId: string(f.agentID), - ClientRequestId: "spawn-dup", - })) + second, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{AgentHandle: string(f.agentID), ClientRequestId: "spawn-dup"})) if err != nil { t.Fatalf("retry SpawnAgent = %v, want idempotent success", err) } @@ -119,10 +110,7 @@ func TestSpawnAgentRejectsWhenAgentAlreadyLive(t *testing.T) { AgentAccountId: string(f.agentID), }) - _, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{ - AgentAccountId: string(f.agentID), - ClientRequestId: "spawn-reject", - })) + _, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{AgentHandle: string(f.agentID), ClientRequestId: "spawn-reject"})) if err == nil { t.Fatal("SpawnAgent for a live agent = nil error, want CodeAlreadyExists") } @@ -155,10 +143,7 @@ func TestSpawnAgentFailedRetryReattempts(t *testing.T) { ctx := context.Background() f.runner.setFailStart(true) // the Runner refuses Start: the first spawn fails mid-chain. - _, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{ - AgentAccountId: string(f.agentID), - ClientRequestId: "spawn-reattempt", - })) + _, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{AgentHandle: string(f.agentID), ClientRequestId: "spawn-reattempt"})) if err == nil { t.Fatal("first SpawnAgent with a failing Start = nil error, want the failure surfaced") } @@ -166,10 +151,7 @@ func TestSpawnAgentFailedRetryReattempts(t *testing.T) { // The Runner now accepts Start; a retry of the SAME id must re-attempt and // succeed, proving the failed memo entry was dropped rather than replayed. f.runner.setFailStart(false) - resp, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{ - AgentAccountId: string(f.agentID), - ClientRequestId: "spawn-reattempt", - })) + resp, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{AgentHandle: string(f.agentID), ClientRequestId: "spawn-reattempt"})) if err != nil { t.Fatalf("retry SpawnAgent after a failed first = %v, want re-attempt success (a retained failure would replay the error)", err) } @@ -214,19 +196,13 @@ func TestSpawnAgentCrossAccountSameCridIsDistinct(t *testing.T) { t.Fatalf("CreateAgent(second): %v", err) } - if _, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{ - AgentAccountId: string(f.agentID), - ClientRequestId: "spawn-shared", - })); err != nil { + if _, err := f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{AgentHandle: string(f.agentID), ClientRequestId: "spawn-shared"})); err != nil { t.Fatalf("first-account SpawnAgent = %v, want success", err) } // The second account reuses the SAME client_request_id. With correct // (account, id) keying it does NOT join, so it drives its own Provision; the // crid-alone bug would join and return the first's result with no Provision. - _, err = f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{ - AgentAccountId: string(other.ID), - ClientRequestId: "spawn-shared", - })) + _, err = f.client.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{AgentHandle: string(other.ID), ClientRequestId: "spawn-shared"})) // A distinct spawn reaching Provision is the tooth; the fake's shared // container name then trips a placement conflict, which is fine — a JOIN // (the bug) would instead have returned nil error with no second Provision. @@ -259,10 +235,7 @@ func TestSpawnAgentMemoEvictsSuccessAfterTTL(t *testing.T) { return nil // the captured fn is fired by the test, not a real timer. } - if _, err := f.svc.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{ - AgentAccountId: string(f.agentID), - ClientRequestId: "spawn-evict", - })); err != nil { + if _, err := f.svc.SpawnAgent(ctx, connect.NewRequest(&compassv1.SpawnAgentRequest{AgentHandle: string(f.agentID), ClientRequestId: "spawn-evict"})); err != nil { t.Fatalf("SpawnAgent = %v, want success", err) } diff --git a/go/server/spawn.go b/go/server/spawn.go index 4a10dd86..75cdf19a 100644 --- a/go/server/spawn.go +++ b/go/server/spawn.go @@ -100,7 +100,7 @@ func (s *service) SpawnAgent( // (not the id alone) matches provisionDedupID: a client_request_id reused // across accounts is a distinct spawn, never a cross-account join. if crid != "" { - key := spawnKey{account: req.Msg.GetAgentAccountId(), crid: crid} + key := spawnKey{account: req.Msg.GetAgentHandle(), crid: crid} call, joined := s.joinOrBeginSpawn(key) if joined { // Joined an in-flight or completed spawn: wait for it to settle and @@ -150,12 +150,12 @@ func (s *service) runSpawn(ctx context.Context, msg *compassv1.SpawnAgentRequest // rejected spawn churns no container. Sourced from the Runner's status scan, // never Server in-memory state (which fails open after a reconnect clears the // bindings and would let the spawn collide on the container name mid-Provision). - if err := s.rejectIfAgentLive(ctx, msg.GetAgentAccountId()); err != nil { + if err := s.rejectIfAgentLive(ctx, msg.GetAgentHandle()); err != nil { return nil, err } provResp, err := s.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{ - AgentAccountId: msg.GetAgentAccountId(), + AgentHandle: msg.GetAgentHandle(), ClientRequestId: msg.GetClientRequestId(), })) if err != nil { diff --git a/packages/compass-agent/src/comms.test.ts b/packages/compass-agent/src/comms.test.ts index efb1a452..bab17c0d 100644 --- a/packages/compass-agent/src/comms.test.ts +++ b/packages/compass-agent/src/comms.test.ts @@ -1744,7 +1744,7 @@ describe("comms_list_messages", () => { describe("compass_roster", () => { // Session-resolved: an AGENT caller names no account. The request must leave - // `agentAccountId` at its default empty string — the Server resolves the + // `vantageHandle` at its default empty string — the Server resolves the // vantage from the session it owns — and default the scope to NEIGHBORHOOD // when the model omits it. test("session-resolved with the default neighborhood scope when scope is omitted", async () => { @@ -1757,7 +1757,7 @@ describe("compass_roster", () => { expect(req?.callId).toBe("tc-r1"); expect(req?.call.case).toBe("roster"); if (req?.call.case !== "roster") throw new Error("expected a roster call"); - expect(req.call.value.agentAccountId).toBe(""); + expect(req.call.value.vantageHandle).toBe(""); expect(req.call.value.scope).toBe(RosterScope.NEIGHBORHOOD); }); diff --git a/packages/compass-agent/src/comms.ts b/packages/compass-agent/src/comms.ts index 9362a05a..7d7ac220 100644 --- a/packages/compass-agent/src/comms.ts +++ b/packages/compass-agent/src/comms.ts @@ -727,7 +727,7 @@ export function createCommsTools(broker: CommsBroker): AgentTool[] { parameters: rosterParameters, execute: async (toolCallId, params) => { // The string param maps onto the RosterScope enum; an omitted scope is - // the neighborhood default. `agentAccountId` is intentionally left + // the neighborhood default. `vantageHandle` is intentionally left // unset — an agent caller is session-resolved server-side and never // names an account. const scope = diff --git a/packages/compass-agent/src/gen/compass/v1/agent_gateway_pb.ts b/packages/compass-agent/src/gen/compass/v1/agent_gateway_pb.ts index e49b024f..87101efd 100644 --- a/packages/compass-agent/src/gen/compass/v1/agent_gateway_pb.ts +++ b/packages/compass-agent/src/gen/compass/v1/agent_gateway_pb.ts @@ -48,7 +48,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file compass/v1/agent_gateway.proto. */ export const file_compass_v1_agent_gateway: GenFile = /*@__PURE__*/ - fileDesc("Ch5jb21wYXNzL3YxL2FnZW50X2dhdGV3YXkucHJvdG8SCmNvbXBhc3MudjEiqgIKEENvbW1zQ2FsbFJlcXVlc3QSDwoHY2FsbF9pZBgBIAEoCRIuCgRwb3N0GAIgASgLMh4uY29tcGFzcy52MS5Qb3N0TWVzc2FnZVJlcXVlc3RIABIvCgRsaXN0GAMgASgLMh8uY29tcGFzcy52MS5MaXN0TWVzc2FnZXNSZXF1ZXN0SAASLgoGcm9zdGVyGAQgASgLMhwuY29tcGFzcy52MS5HZXRSb3N0ZXJSZXF1ZXN0SAASNwoKc2V0X3N0YXR1cxgFIAEoCzIhLmNvbXBhc3MudjEuU2V0QWdlbnRTdGF0dXNSZXF1ZXN0SAASMwoDcGluGAYgASgLMiQuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlcXVlc3RIAEIGCgRjYWxsIt0CCg9Db21tc0NhbGxSZXN1bHQSDwoHY2FsbF9pZBgBIAEoCRIvCgRwb3N0GAIgASgLMh8uY29tcGFzcy52MS5Qb3N0TWVzc2FnZVJlc3BvbnNlSAASMAoEbGlzdBgDIAEoCzIgLmNvbXBhc3MudjEuTGlzdE1lc3NhZ2VzUmVzcG9uc2VIABIrCgVlcnJvchgEIAEoCzIaLmNvbXBhc3MudjEuQ29tbXNDYWxsRXJyb3JIABIvCgZyb3N0ZXIYBSABKAsyHS5jb21wYXNzLnYxLkdldFJvc3RlclJlc3BvbnNlSAASOAoKc2V0X3N0YXR1cxgGIAEoCzIiLmNvbXBhc3MudjEuU2V0QWdlbnRTdGF0dXNSZXNwb25zZUgAEjQKA3BpbhgHIAEoCzIlLmNvbXBhc3MudjEuVXBkYXRlUGlubmVkQm9hcmRSZXNwb25zZUgAQggKBnJlc3VsdCIvCg5Db21tc0NhbGxFcnJvchIMCgRjb2RlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkiKQoVU2V0QWdlbnRTdGF0dXNSZXF1ZXN0EhAKCGFjdGl2aXR5GAEgASgJIhgKFlNldEFnZW50U3RhdHVzUmVzcG9uc2UikQEKFExpZmVjeWNsZUNhbGxSZXF1ZXN0Eg8KB2NhbGxfaWQYASABKAkSLQoFc3Bhd24YAiABKAsyHC5jb21wYXNzLnYxLlNwYXduUGVlclJlcXVlc3RIABIxCgdkZXNwYXduGAMgASgLMh4uY29tcGFzcy52MS5EZXNwYXduUGVlclJlcXVlc3RIAEIGCgRjYWxsImkKEFNwYXduUGVlclJlcXVlc3QSDgoGaGFuZGxlGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgEIAEoCUoECAMQBFIOaW5pdGlhbF9wcm9tcHQiWQoRU3Bhd25QZWVyUmVzcG9uc2USGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIWCg5jb250YWluZXJfbmFtZRgCIAEoCRISCgpzZXNzaW9uX2lkGAMgASgJIi4KEkRlc3Bhd25QZWVyUmVxdWVzdBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJIhUKE0Rlc3Bhd25QZWVyUmVzcG9uc2UixQEKE0xpZmVjeWNsZUNhbGxSZXN1bHQSDwoHY2FsbF9pZBgBIAEoCRIuCgVzcGF3bhgCIAEoCzIdLmNvbXBhc3MudjEuU3Bhd25QZWVyUmVzcG9uc2VIABIyCgdkZXNwYXduGAMgASgLMh8uY29tcGFzcy52MS5EZXNwYXduUGVlclJlc3BvbnNlSAASLwoFZXJyb3IYBCABKAsyHi5jb21wYXNzLnYxLkxpZmVjeWNsZUNhbGxFcnJvckgAQggKBnJlc3VsdCIzChJMaWZlY3ljbGVDYWxsRXJyb3ISDAoEY29kZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJIsgFChBGb3JnZUNhbGxSZXF1ZXN0Eg8KB2NhbGxfaWQYASABKAkSNgoMY3JlYXRlX2lzc3VlGAIgASgLMh4uY29tcGFzcy52MS5DcmVhdGVJc3N1ZVJlcXVlc3RIABI9ChBjb21tZW50X29uX2lzc3VlGAMgASgLMiEuY29tcGFzcy52MS5Db21tZW50T25Jc3N1ZVJlcXVlc3RIABIwCglnZXRfaXNzdWUYBCABKAsyGy5jb21wYXNzLnYxLkdldElzc3VlUmVxdWVzdEgAEjQKC2xpc3RfaXNzdWVzGAUgASgLMh0uY29tcGFzcy52MS5MaXN0SXNzdWVzUmVxdWVzdEgAEkMKE2NyZWF0ZV9wdWxsX3JlcXVlc3QYBiABKAsyJC5jb21wYXNzLnYxLkNyZWF0ZVB1bGxSZXF1ZXN0UmVxdWVzdEgAEkoKF2NvbW1lbnRfb25fcHVsbF9yZXF1ZXN0GAcgASgLMicuY29tcGFzcy52MS5Db21tZW50T25QdWxsUmVxdWVzdFJlcXVlc3RIABI9ChBnZXRfcHVsbF9yZXF1ZXN0GAggASgLMiEuY29tcGFzcy52MS5HZXRQdWxsUmVxdWVzdFJlcXVlc3RIABI2CglzdWJzY3JpYmUYCSABKAsyIS5jb21wYXNzLnYxLlN1YnNjcmliZUZvcmdlUmVxdWVzdEgAEjoKC3Vuc3Vic2NyaWJlGAogASgLMiMuY29tcGFzcy52MS5VbnN1YnNjcmliZUZvcmdlUmVxdWVzdEgAEjgKDXN1Ym1pdF9yZXZpZXcYCyABKAsyHy5jb21wYXNzLnYxLlN1Ym1pdFJldmlld1JlcXVlc3RIABIjCgVmb3JnZRgMIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSGQoRY2xpZW50X3JlcXVlc3RfaWQYDSABKAlCBgoEY2FsbCLgAwoPRm9yZ2VDYWxsUmVzdWx0Eg8KB2NhbGxfaWQYASABKAkSIgoFaXNzdWUYAiABKAsyES5jb21wYXNzLnYxLklzc3VlSAASLwoNaXNzdWVfY29tbWVudBgDIAEoCzIWLmNvbXBhc3MudjEuQ29tbWVudFJlZkgAEjAKBmlzc3VlcxgEIAEoCzIeLmNvbXBhc3MudjEuTGlzdElzc3Vlc1Jlc3BvbnNlSAASLwoMcHVsbF9yZXF1ZXN0GAUgASgLMhcuY29tcGFzcy52MS5QdWxsUmVxdWVzdEgAEiwKCnByX2NvbW1lbnQYBiABKAsyFi5jb21wYXNzLnYxLkNvbW1lbnRSZWZIABI4CgpzdWJzY3JpYmVkGAcgASgLMiIuY29tcGFzcy52MS5TdWJzY3JpYmVGb3JnZVJlc3BvbnNlSAASPAoMdW5zdWJzY3JpYmVkGAggASgLMiQuY29tcGFzcy52MS5VbnN1YnNjcmliZUZvcmdlUmVzcG9uc2VIABIrCgVlcnJvchgJIAEoCzIaLmNvbXBhc3MudjEuRm9yZ2VDYWxsRXJyb3JIABInCgZyZXZpZXcYCiABKAsyFS5jb21wYXNzLnYxLlJldmlld1JlZkgAQggKBnJlc3VsdCJHCg5Gb3JnZUNhbGxFcnJvchIMCgRjb2RlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSFgoOcmV0cnlfYWZ0ZXJfbXMYAyABKA0iTwoSQ3JlYXRlSXNzdWVSZXF1ZXN0EgwKBHJlcG8YASABKAkSDQoFdGl0bGUYAiABKAkSDAoEYm9keRgDIAEoCRIOCgZsYWJlbHMYBCADKAkiSQoVQ29tbWVudE9uSXNzdWVSZXF1ZXN0EgwKBHJlcG8YASABKAkSFAoMaXNzdWVfbnVtYmVyGAIgASgEEgwKBGJvZHkYAyABKAkiNQoPR2V0SXNzdWVSZXF1ZXN0EgwKBHJlcG8YASABKAkSFAoMaXNzdWVfbnVtYmVyGAIgASgEIk8KEUxpc3RJc3N1ZXNSZXF1ZXN0EgwKBHJlcG8YASABKAkSDQoFc3RhdGUYAiABKAkSDgoGbGFiZWxzGAMgAygJEg0KBWxpbWl0GAQgASgNIjcKEkxpc3RJc3N1ZXNSZXNwb25zZRIhCgZpc3N1ZXMYASADKAsyES5jb21wYXNzLnYxLklzc3VlIngKGENyZWF0ZVB1bGxSZXF1ZXN0UmVxdWVzdBIMCgRyZXBvGAEgASgJEg0KBXRpdGxlGAIgASgJEgwKBGJvZHkYAyABKAkSEAoIaGVhZF9yZWYYBCABKAkSEAoIYmFzZV9yZWYYBSABKAkSDQoFZHJhZnQYBiABKAgiTgobQ29tbWVudE9uUHVsbFJlcXVlc3RSZXF1ZXN0EgwKBHJlcG8YASABKAkSEwoLcHVsbF9udW1iZXIYAiABKAQSDAoEYm9keRgDIAEoCSI6ChVHZXRQdWxsUmVxdWVzdFJlcXVlc3QSDAoEcmVwbxgBIAEoCRITCgtwdWxsX251bWJlchgCIAEoBCKJAQoTU3VibWl0UmV2aWV3UmVxdWVzdBIMCgRyZXBvGAEgASgJEhMKC3B1bGxfbnVtYmVyGAIgASgEEg8KB3ZlcmRpY3QYAyABKAkSDAoEYm9keRgEIAEoCRIwCghjb21tZW50cxgFIAMoCzIeLmNvbXBhc3MudjEuUmV2aWV3Q29tbWVudElucHV0IkwKElJldmlld0NvbW1lbnRJbnB1dBIMCgRwYXRoGAEgASgJEgwKBGxpbmUYAiABKA0SDAoEc2lkZRgDIAEoCRIMCgRib2R5GAQgASgJIqYBChVTdWJzY3JpYmVGb3JnZVJlcXVlc3QSDAoEcmVwbxgBIAEoCRIrCgRraW5kGAIgASgOMh0uY29tcGFzcy52MS5Gb3JnZUFydGlmYWN0S2luZBIOCgZudW1iZXIYAyABKAQSMQoFc2NvcGUYBCABKA4yIi5jb21wYXNzLnYxLkZvcmdlU3Vic2NyaXB0aW9uU2NvcGUSDwoHcHJvamVjdBgFIAEoCSIxChZTdWJzY3JpYmVGb3JnZVJlc3BvbnNlEhcKD3N1YnNjcmlwdGlvbl9pZBgBIAEoCSIyChdVbnN1YnNjcmliZUZvcmdlUmVxdWVzdBIXCg9zdWJzY3JpcHRpb25faWQYASABKAkiGgoYVW5zdWJzY3JpYmVGb3JnZVJlc3BvbnNlImgKEEJvYXJkQ2FsbFJlcXVlc3QSDwoHY2FsbF9pZBgBIAEoCRI7Cg9zZXRfaXNzdWVfc3RhdGUYAiABKAsyIC5jb21wYXNzLnYxLlNldElzc3VlU3RhdGVSZXF1ZXN0SABCBgoEY2FsbCJPChRTZXRJc3N1ZVN0YXRlUmVxdWVzdBIQCghpc3N1ZV9pZBgBIAEoCRIlCgVzdGF0ZRgCIAEoDjIWLmNvbXBhc3MudjEuSXNzdWVTdGF0ZSI5ChVTZXRJc3N1ZVN0YXRlUmVzcG9uc2USIAoFaXNzdWUYASABKAsyES5jb21wYXNzLnYxLklzc3VlIpcBCg9Cb2FyZENhbGxSZXN1bHQSDwoHY2FsbF9pZBgBIAEoCRI8Cg9zZXRfaXNzdWVfc3RhdGUYAiABKAsyIS5jb21wYXNzLnYxLlNldElzc3VlU3RhdGVSZXNwb25zZUgAEisKBWVycm9yGAMgASgLMhouY29tcGFzcy52MS5Cb2FyZENhbGxFcnJvckgAQggKBnJlc3VsdCIvCg5Cb2FyZENhbGxFcnJvchIMCgRjb2RlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkiPAoTUHVibGlzaEZyYW1lUmVxdWVzdBIlCgVmcmFtZRgBIAEoCzIWLmNvbXBhc3MudjEuQWdlbnRGcmFtZSIWChRQdWJsaXNoRnJhbWVSZXNwb25zZSJeChxQb3N0Q29udmVyc2F0aW9uRnJhbWVSZXF1ZXN0EiUKBWZyYW1lGAEgASgLMhYuY29tcGFzcy52MS5BZ2VudEZyYW1lEhcKD2lkZW1wb3RlbmN5X2tleRgCIAEoCSIfCh1Qb3N0Q29udmVyc2F0aW9uRnJhbWVSZXNwb25zZSIZChdDb250cm9sU3Vic2NyaWJlUmVxdWVzdCqRAQoWRm9yZ2VTdWJzY3JpcHRpb25TY29wZRIoCiRGT1JHRV9TVUJTQ1JJUFRJT05fU0NPUEVfVU5TUEVDSUZJRUQQABIlCiFGT1JHRV9TVUJTQ1JJUFRJT05fU0NPUEVfQVJUSUZBQ1QQARImCiJGT1JHRV9TVUJTQ1JJUFRJT05fU0NPUEVfQ09OVEFJTkVSEAIytAQKDEFnZW50R2F0ZXdheRJCCgVDb21tcxIcLmNvbXBhc3MudjEuQ29tbXNDYWxsUmVxdWVzdBobLmNvbXBhc3MudjEuQ29tbXNDYWxsUmVzdWx0Ek4KCUxpZmVjeWNsZRIgLmNvbXBhc3MudjEuTGlmZWN5Y2xlQ2FsbFJlcXVlc3QaHy5jb21wYXNzLnYxLkxpZmVjeWNsZUNhbGxSZXN1bHQSTgoHUHVibGlzaBIfLmNvbXBhc3MudjEuUHVibGlzaEZyYW1lUmVxdWVzdBogLmNvbXBhc3MudjEuUHVibGlzaEZyYW1lUmVzcG9uc2UoARJsChVQb3N0Q29udmVyc2F0aW9uRnJhbWUSKC5jb21wYXNzLnYxLlBvc3RDb252ZXJzYXRpb25GcmFtZVJlcXVlc3QaKS5jb21wYXNzLnYxLlBvc3RDb252ZXJzYXRpb25GcmFtZVJlc3BvbnNlEkoKB0NvbnRyb2wSIy5jb21wYXNzLnYxLkNvbnRyb2xTdWJzY3JpYmVSZXF1ZXN0GhguY29tcGFzcy52MS5BZ2VudENvbnRyb2wwARJCCgVGb3JnZRIcLmNvbXBhc3MudjEuRm9yZ2VDYWxsUmVxdWVzdBobLmNvbXBhc3MudjEuRm9yZ2VDYWxsUmVzdWx0EkIKBUJvYXJkEhwuY29tcGFzcy52MS5Cb2FyZENhbGxSZXF1ZXN0GhsuY29tcGFzcy52MS5Cb2FyZENhbGxSZXN1bHRiBnByb3RvMw", [file_compass_v1_comms, file_compass_v1_agent, file_compass_v1_compass, file_compass_v1_forge]); + fileDesc("Ch5jb21wYXNzL3YxL2FnZW50X2dhdGV3YXkucHJvdG8SCmNvbXBhc3MudjEiqgIKEENvbW1zQ2FsbFJlcXVlc3QSDwoHY2FsbF9pZBgBIAEoCRIuCgRwb3N0GAIgASgLMh4uY29tcGFzcy52MS5Qb3N0TWVzc2FnZVJlcXVlc3RIABIvCgRsaXN0GAMgASgLMh8uY29tcGFzcy52MS5MaXN0TWVzc2FnZXNSZXF1ZXN0SAASLgoGcm9zdGVyGAQgASgLMhwuY29tcGFzcy52MS5HZXRSb3N0ZXJSZXF1ZXN0SAASNwoKc2V0X3N0YXR1cxgFIAEoCzIhLmNvbXBhc3MudjEuU2V0QWdlbnRTdGF0dXNSZXF1ZXN0SAASMwoDcGluGAYgASgLMiQuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlcXVlc3RIAEIGCgRjYWxsIt0CCg9Db21tc0NhbGxSZXN1bHQSDwoHY2FsbF9pZBgBIAEoCRIvCgRwb3N0GAIgASgLMh8uY29tcGFzcy52MS5Qb3N0TWVzc2FnZVJlc3BvbnNlSAASMAoEbGlzdBgDIAEoCzIgLmNvbXBhc3MudjEuTGlzdE1lc3NhZ2VzUmVzcG9uc2VIABIrCgVlcnJvchgEIAEoCzIaLmNvbXBhc3MudjEuQ29tbXNDYWxsRXJyb3JIABIvCgZyb3N0ZXIYBSABKAsyHS5jb21wYXNzLnYxLkdldFJvc3RlclJlc3BvbnNlSAASOAoKc2V0X3N0YXR1cxgGIAEoCzIiLmNvbXBhc3MudjEuU2V0QWdlbnRTdGF0dXNSZXNwb25zZUgAEjQKA3BpbhgHIAEoCzIlLmNvbXBhc3MudjEuVXBkYXRlUGlubmVkQm9hcmRSZXNwb25zZUgAQggKBnJlc3VsdCIvCg5Db21tc0NhbGxFcnJvchIMCgRjb2RlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkiKQoVU2V0QWdlbnRTdGF0dXNSZXF1ZXN0EhAKCGFjdGl2aXR5GAEgASgJIhgKFlNldEFnZW50U3RhdHVzUmVzcG9uc2UikQEKFExpZmVjeWNsZUNhbGxSZXF1ZXN0Eg8KB2NhbGxfaWQYASABKAkSLQoFc3Bhd24YAiABKAsyHC5jb21wYXNzLnYxLlNwYXduUGVlclJlcXVlc3RIABIxCgdkZXNwYXduGAMgASgLMh4uY29tcGFzcy52MS5EZXNwYXduUGVlclJlcXVlc3RIAEIGCgRjYWxsImkKEFNwYXduUGVlclJlcXVlc3QSDgoGaGFuZGxlGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgEIAEoCUoECAMQBFIOaW5pdGlhbF9wcm9tcHQiWQoRU3Bhd25QZWVyUmVzcG9uc2USGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIWCg5jb250YWluZXJfbmFtZRgCIAEoCRISCgpzZXNzaW9uX2lkGAMgASgJIioKEkRlc3Bhd25QZWVyUmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkiFQoTRGVzcGF3blBlZXJSZXNwb25zZSLFAQoTTGlmZWN5Y2xlQ2FsbFJlc3VsdBIPCgdjYWxsX2lkGAEgASgJEi4KBXNwYXduGAIgASgLMh0uY29tcGFzcy52MS5TcGF3blBlZXJSZXNwb25zZUgAEjIKB2Rlc3Bhd24YAyABKAsyHy5jb21wYXNzLnYxLkRlc3Bhd25QZWVyUmVzcG9uc2VIABIvCgVlcnJvchgEIAEoCzIeLmNvbXBhc3MudjEuTGlmZWN5Y2xlQ2FsbEVycm9ySABCCAoGcmVzdWx0IjMKEkxpZmVjeWNsZUNhbGxFcnJvchIMCgRjb2RlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkiyAUKEEZvcmdlQ2FsbFJlcXVlc3QSDwoHY2FsbF9pZBgBIAEoCRI2CgxjcmVhdGVfaXNzdWUYAiABKAsyHi5jb21wYXNzLnYxLkNyZWF0ZUlzc3VlUmVxdWVzdEgAEj0KEGNvbW1lbnRfb25faXNzdWUYAyABKAsyIS5jb21wYXNzLnYxLkNvbW1lbnRPbklzc3VlUmVxdWVzdEgAEjAKCWdldF9pc3N1ZRgEIAEoCzIbLmNvbXBhc3MudjEuR2V0SXNzdWVSZXF1ZXN0SAASNAoLbGlzdF9pc3N1ZXMYBSABKAsyHS5jb21wYXNzLnYxLkxpc3RJc3N1ZXNSZXF1ZXN0SAASQwoTY3JlYXRlX3B1bGxfcmVxdWVzdBgGIAEoCzIkLmNvbXBhc3MudjEuQ3JlYXRlUHVsbFJlcXVlc3RSZXF1ZXN0SAASSgoXY29tbWVudF9vbl9wdWxsX3JlcXVlc3QYByABKAsyJy5jb21wYXNzLnYxLkNvbW1lbnRPblB1bGxSZXF1ZXN0UmVxdWVzdEgAEj0KEGdldF9wdWxsX3JlcXVlc3QYCCABKAsyIS5jb21wYXNzLnYxLkdldFB1bGxSZXF1ZXN0UmVxdWVzdEgAEjYKCXN1YnNjcmliZRgJIAEoCzIhLmNvbXBhc3MudjEuU3Vic2NyaWJlRm9yZ2VSZXF1ZXN0SAASOgoLdW5zdWJzY3JpYmUYCiABKAsyIy5jb21wYXNzLnYxLlVuc3Vic2NyaWJlRm9yZ2VSZXF1ZXN0SAASOAoNc3VibWl0X3JldmlldxgLIAEoCzIfLmNvbXBhc3MudjEuU3VibWl0UmV2aWV3UmVxdWVzdEgAEiMKBWZvcmdlGAwgASgLMhQuY29tcGFzcy52MS5Gb3JnZVJlZhIZChFjbGllbnRfcmVxdWVzdF9pZBgNIAEoCUIGCgRjYWxsIuADCg9Gb3JnZUNhbGxSZXN1bHQSDwoHY2FsbF9pZBgBIAEoCRIiCgVpc3N1ZRgCIAEoCzIRLmNvbXBhc3MudjEuSXNzdWVIABIvCg1pc3N1ZV9jb21tZW50GAMgASgLMhYuY29tcGFzcy52MS5Db21tZW50UmVmSAASMAoGaXNzdWVzGAQgASgLMh4uY29tcGFzcy52MS5MaXN0SXNzdWVzUmVzcG9uc2VIABIvCgxwdWxsX3JlcXVlc3QYBSABKAsyFy5jb21wYXNzLnYxLlB1bGxSZXF1ZXN0SAASLAoKcHJfY29tbWVudBgGIAEoCzIWLmNvbXBhc3MudjEuQ29tbWVudFJlZkgAEjgKCnN1YnNjcmliZWQYByABKAsyIi5jb21wYXNzLnYxLlN1YnNjcmliZUZvcmdlUmVzcG9uc2VIABI8Cgx1bnN1YnNjcmliZWQYCCABKAsyJC5jb21wYXNzLnYxLlVuc3Vic2NyaWJlRm9yZ2VSZXNwb25zZUgAEisKBWVycm9yGAkgASgLMhouY29tcGFzcy52MS5Gb3JnZUNhbGxFcnJvckgAEicKBnJldmlldxgKIAEoCzIVLmNvbXBhc3MudjEuUmV2aWV3UmVmSABCCAoGcmVzdWx0IkcKDkZvcmdlQ2FsbEVycm9yEgwKBGNvZGUYASABKAkSDwoHbWVzc2FnZRgCIAEoCRIWCg5yZXRyeV9hZnRlcl9tcxgDIAEoDSJPChJDcmVhdGVJc3N1ZVJlcXVlc3QSDAoEcmVwbxgBIAEoCRINCgV0aXRsZRgCIAEoCRIMCgRib2R5GAMgASgJEg4KBmxhYmVscxgEIAMoCSJJChVDb21tZW50T25Jc3N1ZVJlcXVlc3QSDAoEcmVwbxgBIAEoCRIUCgxpc3N1ZV9udW1iZXIYAiABKAQSDAoEYm9keRgDIAEoCSI1Cg9HZXRJc3N1ZVJlcXVlc3QSDAoEcmVwbxgBIAEoCRIUCgxpc3N1ZV9udW1iZXIYAiABKAQiTwoRTGlzdElzc3Vlc1JlcXVlc3QSDAoEcmVwbxgBIAEoCRINCgVzdGF0ZRgCIAEoCRIOCgZsYWJlbHMYAyADKAkSDQoFbGltaXQYBCABKA0iNwoSTGlzdElzc3Vlc1Jlc3BvbnNlEiEKBmlzc3VlcxgBIAMoCzIRLmNvbXBhc3MudjEuSXNzdWUieAoYQ3JlYXRlUHVsbFJlcXVlc3RSZXF1ZXN0EgwKBHJlcG8YASABKAkSDQoFdGl0bGUYAiABKAkSDAoEYm9keRgDIAEoCRIQCghoZWFkX3JlZhgEIAEoCRIQCghiYXNlX3JlZhgFIAEoCRINCgVkcmFmdBgGIAEoCCJOChtDb21tZW50T25QdWxsUmVxdWVzdFJlcXVlc3QSDAoEcmVwbxgBIAEoCRITCgtwdWxsX251bWJlchgCIAEoBBIMCgRib2R5GAMgASgJIjoKFUdldFB1bGxSZXF1ZXN0UmVxdWVzdBIMCgRyZXBvGAEgASgJEhMKC3B1bGxfbnVtYmVyGAIgASgEIokBChNTdWJtaXRSZXZpZXdSZXF1ZXN0EgwKBHJlcG8YASABKAkSEwoLcHVsbF9udW1iZXIYAiABKAQSDwoHdmVyZGljdBgDIAEoCRIMCgRib2R5GAQgASgJEjAKCGNvbW1lbnRzGAUgAygLMh4uY29tcGFzcy52MS5SZXZpZXdDb21tZW50SW5wdXQiTAoSUmV2aWV3Q29tbWVudElucHV0EgwKBHBhdGgYASABKAkSDAoEbGluZRgCIAEoDRIMCgRzaWRlGAMgASgJEgwKBGJvZHkYBCABKAkipgEKFVN1YnNjcmliZUZvcmdlUmVxdWVzdBIMCgRyZXBvGAEgASgJEisKBGtpbmQYAiABKA4yHS5jb21wYXNzLnYxLkZvcmdlQXJ0aWZhY3RLaW5kEg4KBm51bWJlchgDIAEoBBIxCgVzY29wZRgEIAEoDjIiLmNvbXBhc3MudjEuRm9yZ2VTdWJzY3JpcHRpb25TY29wZRIPCgdwcm9qZWN0GAUgASgJIjEKFlN1YnNjcmliZUZvcmdlUmVzcG9uc2USFwoPc3Vic2NyaXB0aW9uX2lkGAEgASgJIjIKF1Vuc3Vic2NyaWJlRm9yZ2VSZXF1ZXN0EhcKD3N1YnNjcmlwdGlvbl9pZBgBIAEoCSIaChhVbnN1YnNjcmliZUZvcmdlUmVzcG9uc2UiaAoQQm9hcmRDYWxsUmVxdWVzdBIPCgdjYWxsX2lkGAEgASgJEjsKD3NldF9pc3N1ZV9zdGF0ZRgCIAEoCzIgLmNvbXBhc3MudjEuU2V0SXNzdWVTdGF0ZVJlcXVlc3RIAEIGCgRjYWxsIk8KFFNldElzc3VlU3RhdGVSZXF1ZXN0EhAKCGlzc3VlX2lkGAEgASgJEiUKBXN0YXRlGAIgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlIjkKFVNldElzc3VlU3RhdGVSZXNwb25zZRIgCgVpc3N1ZRgBIAEoCzIRLmNvbXBhc3MudjEuSXNzdWUilwEKD0JvYXJkQ2FsbFJlc3VsdBIPCgdjYWxsX2lkGAEgASgJEjwKD3NldF9pc3N1ZV9zdGF0ZRgCIAEoCzIhLmNvbXBhc3MudjEuU2V0SXNzdWVTdGF0ZVJlc3BvbnNlSAASKwoFZXJyb3IYAyABKAsyGi5jb21wYXNzLnYxLkJvYXJkQ2FsbEVycm9ySABCCAoGcmVzdWx0Ii8KDkJvYXJkQ2FsbEVycm9yEgwKBGNvZGUYASABKAkSDwoHbWVzc2FnZRgCIAEoCSI8ChNQdWJsaXNoRnJhbWVSZXF1ZXN0EiUKBWZyYW1lGAEgASgLMhYuY29tcGFzcy52MS5BZ2VudEZyYW1lIhYKFFB1Ymxpc2hGcmFtZVJlc3BvbnNlIl4KHFBvc3RDb252ZXJzYXRpb25GcmFtZVJlcXVlc3QSJQoFZnJhbWUYASABKAsyFi5jb21wYXNzLnYxLkFnZW50RnJhbWUSFwoPaWRlbXBvdGVuY3lfa2V5GAIgASgJIh8KHVBvc3RDb252ZXJzYXRpb25GcmFtZVJlc3BvbnNlIhkKF0NvbnRyb2xTdWJzY3JpYmVSZXF1ZXN0KpEBChZGb3JnZVN1YnNjcmlwdGlvblNjb3BlEigKJEZPUkdFX1NVQlNDUklQVElPTl9TQ09QRV9VTlNQRUNJRklFRBAAEiUKIUZPUkdFX1NVQlNDUklQVElPTl9TQ09QRV9BUlRJRkFDVBABEiYKIkZPUkdFX1NVQlNDUklQVElPTl9TQ09QRV9DT05UQUlORVIQAjK0BAoMQWdlbnRHYXRld2F5EkIKBUNvbW1zEhwuY29tcGFzcy52MS5Db21tc0NhbGxSZXF1ZXN0GhsuY29tcGFzcy52MS5Db21tc0NhbGxSZXN1bHQSTgoJTGlmZWN5Y2xlEiAuY29tcGFzcy52MS5MaWZlY3ljbGVDYWxsUmVxdWVzdBofLmNvbXBhc3MudjEuTGlmZWN5Y2xlQ2FsbFJlc3VsdBJOCgdQdWJsaXNoEh8uY29tcGFzcy52MS5QdWJsaXNoRnJhbWVSZXF1ZXN0GiAuY29tcGFzcy52MS5QdWJsaXNoRnJhbWVSZXNwb25zZSgBEmwKFVBvc3RDb252ZXJzYXRpb25GcmFtZRIoLmNvbXBhc3MudjEuUG9zdENvbnZlcnNhdGlvbkZyYW1lUmVxdWVzdBopLmNvbXBhc3MudjEuUG9zdENvbnZlcnNhdGlvbkZyYW1lUmVzcG9uc2USSgoHQ29udHJvbBIjLmNvbXBhc3MudjEuQ29udHJvbFN1YnNjcmliZVJlcXVlc3QaGC5jb21wYXNzLnYxLkFnZW50Q29udHJvbDABEkIKBUZvcmdlEhwuY29tcGFzcy52MS5Gb3JnZUNhbGxSZXF1ZXN0GhsuY29tcGFzcy52MS5Gb3JnZUNhbGxSZXN1bHQSQgoFQm9hcmQSHC5jb21wYXNzLnYxLkJvYXJkQ2FsbFJlcXVlc3QaGy5jb21wYXNzLnYxLkJvYXJkQ2FsbFJlc3VsdGIGcHJvdG8z", [file_compass_v1_comms, file_compass_v1_agent, file_compass_v1_compass, file_compass_v1_forge]); /** * One agent-initiated comms call. `call_id` is the agent-minted correlation id @@ -342,11 +342,11 @@ export const SpawnPeerResponseSchema: GenMessage = /*@__PURE_ */ export type DespawnPeerRequest = Message<"compass.v1.DespawnPeerRequest"> & { /** - * the peer to tear down + * the peer to tear down; a `@handle` the server resolves to an account id; unknown → NOT_FOUND * - * @generated from field: string agent_account_id = 1; + * @generated from field: string agent_handle = 1; */ - agentAccountId: string; + agentHandle: string; }; /** diff --git a/packages/compass-agent/src/gen/compass/v1/comms_pb.ts b/packages/compass-agent/src/gen/compass/v1/comms_pb.ts index 0b57c770..d2c393c4 100644 --- a/packages/compass-agent/src/gen/compass/v1/comms_pb.ts +++ b/packages/compass-agent/src/gen/compass/v1/comms_pb.ts @@ -27,7 +27,7 @@ import type { Message as Message$1 } from "@bufbuild/protobuf"; * Describes the file compass/v1/comms.proto. */ export const file_compass_v1_comms: GenFile = /*@__PURE__*/ - fileDesc("ChZjb21wYXNzL3YxL2NvbW1zLnByb3RvEgpjb21wYXNzLnYxIsQBCgdBY2NvdW50EgoKAmlkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSJwoEdXNlchgKIAEoCzIXLmNvbXBhc3MudjEuVXNlckFjY291bnRIABIpCgVhZ2VudBgLIAEoCzIYLmNvbXBhc3MudjEuQWdlbnRBY2NvdW50SAASKwoGc3lzdGVtGAwgASgLMhkuY29tcGFzcy52MS5TeXN0ZW1BY2NvdW50SABCBgoEa2luZCIPCg1TeXN0ZW1BY2NvdW50IjEKC1VzZXJBY2NvdW50EiIKBHJvbGUYASABKA4yFC5jb21wYXNzLnYxLlVzZXJSb2xlIlcKDEFnZW50QWNjb3VudBIVCg1vd25lcl91c2VyX2lkGAEgASgJEhcKD2hvbWVfY2hhbm5lbF9pZBgCIAEoCRIXCg9wYXJlbnRfYWdlbnRfaWQYAyABKAkikAEKDENoYW5uZWxHcm91cBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhcKD3BhcmVudF9ncm91cF9pZBgDIAEoCRIVCg1vd25lcl91c2VyX2lkGAQgASgJEjYKCnZpc2liaWxpdHkYBSABKA4yIi5jb21wYXNzLnYxLkNoYW5uZWxHcm91cFZpc2liaWxpdHkitwIKB0NoYW5uZWwSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghncm91cF9pZBgDIAEoCRIlCgRraW5kGAQgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIaChJtZW1iZXJfYWNjb3VudF9pZHMYBSADKAkSHgoWc3Vic2NyaWJlcl9hY2NvdW50X2lkcxgGIAMoCRIyCgtwb3N0X3BvbGljeRgHIAEoDjIdLmNvbXBhc3MudjEuQ2hhbm5lbFBvc3RQb2xpY3kSGAoQb3duZXJfYWNjb3VudF9pZBgIIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAkgASgIEi8KDnBpbm5lZF9lbnRyaWVzGAogAygLMhcuY29tcGFzcy52MS5QaW5uZWRFbnRyeSJsCgtQaW5uZWRFbnRyeRISCgptZXNzYWdlX2lkGAEgASgJEhAKCHBvc2l0aW9uGAIgASgFEhkKEXBpbm5lZF9hdF91bml4X21zGAMgASgDEhwKFHBpbm5lZF9ieV9hY2NvdW50X2lkGAQgASgJIjYKDkFnZW50V29ya3NwYWNlEgoKAmlkGAEgASgJEhgKEGFnZW50X2FjY291bnRfaWQYAiABKAkiggEKBVRvcGljEgoKAmlkGAEgASgJEhIKCmNoYW5uZWxfaWQYAiABKAkSDAoEbmFtZRgDIAEoCRIaChJjcmVhdGVkX2F0X3VuaXhfbXMYBCABKAMSHQoVY3JlYXRlZF9ieV9hY2NvdW50X2lkGAUgASgJEhAKCGFyY2hpdmVkGAYgASgIIoABCgdNZXNzYWdlEgoKAmlkGAEgASgJEhAKCHRvcGljX2lkGAIgASgJEhkKEWF1dGhvcl9hY2NvdW50X2lkGAMgASgJEhIKCmF0X3VuaXhfbXMYBCABKAMSKAoGYmxvY2tzGAUgAygLMhguY29tcGFzcy52MS5NZXNzYWdlQmxvY2sieQoMTWVzc2FnZUJsb2NrEg4KBHRleHQYASABKAlIABIeCgNhc2sYAiABKAsyDy5jb21wYXNzLnYxLkFza0gAEjAKCmFza19hbnN3ZXIYAyABKAsyGi5jb21wYXNzLnYxLkFza0Fuc3dlckJsb2NrSABCBwoFYmxvY2siSAoOQXNrQW5zd2VyQmxvY2sSHAoDYXNrGAEgASgLMg8uY29tcGFzcy52MS5Bc2sSGAoQYXNrZXJfYWNjb3VudF9pZBgCIAEoCSJTCgNBc2sSDgoGYXNrX2lkGAEgASgJEioKCXF1ZXN0aW9ucxgCIAMoCzIXLmNvbXBhc3MudjEuQXNrUXVlc3Rpb24SEAoIYW5zd2VyZWQYAyABKAgi8QEKC0Fza1F1ZXN0aW9uEhMKC3F1ZXN0aW9uX2lkGAEgASgJEhAKCHF1ZXN0aW9uGAIgASgJEg4KBmhlYWRlchgDIAEoCRImCgdvcHRpb25zGAQgAygLMhUuY29tcGFzcy52MS5Bc2tPcHRpb24SFgoOYWxsb3dfbXVsdGlwbGUYBSABKAgSGAoLcmVjb21tZW5kZWQYBiABKAVIAIgBARIZChFjaG9zZW5fb3B0aW9uX2lkcxgHIAMoCRITCgtjdXN0b21fdGV4dBgIIAEoCRIRCgl0aW1lZF9vdXQYCSABKAhCDgoMX3JlY29tbWVuZGVkIkwKCUFza09wdGlvbhIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIPCgdwcmV2aWV3GAQgASgJIokFChZTdWJzY3JpYmVDb21tc1Jlc3BvbnNlEgsKA3NlcRgBIAEoBBISCgphdF91bml4X21zGAIgASgDEhYKDmluc3RhbmNlX2Vwb2NoGAMgASgEEhQKDHNuYXBzaG90X3NlcRgEIAEoBBIzCg5tZXNzYWdlX3Bvc3RlZBgKIAEoCzIZLmNvbXBhc3MudjEuTWVzc2FnZVBvc3RlZEgAEjUKD21lc3NhZ2VfdXBkYXRlZBgLIAEoCzIaLmNvbXBhc3MudjEuTWVzc2FnZVVwZGF0ZWRIABI1Cg9jaGFubmVsX2NoYW5nZWQYDCABKAsyGi5jb21wYXNzLnYxLkNoYW5uZWxDaGFuZ2VkSAASQAoVY2hhbm5lbF9ncm91cF9jaGFuZ2VkGA0gASgLMh8uY29tcGFzcy52MS5DaGFubmVsR3JvdXBDaGFuZ2VkSAASNQoPYWNjb3VudF9jaGFuZ2VkGA4gASgLMhouY29tcGFzcy52MS5BY2NvdW50Q2hhbmdlZEgAEkQKF2FnZW50X3dvcmtzcGFjZV9jaGFuZ2VkGA8gASgLMiEuY29tcGFzcy52MS5BZ2VudFdvcmtzcGFjZUNoYW5nZWRIABI6Cg9yZXN5bmNfcmVxdWlyZWQYECABKAsyHy5jb21wYXNzLnYxLkNvbW1zUmVzeW5jUmVxdWlyZWRIABJCChZhZ2VudF9wcmVzZW5jZV9jaGFuZ2VkGBEgASgLMiAuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlQ2hhbmdlZEgAEjMKDnRvcGljX3Vwc2VydGVkGBIgASgLMhkuY29tcGFzcy52MS5Ub3BpY1Vwc2VydGVkSABCCQoHcGF5bG9hZCI1Cg1NZXNzYWdlUG9zdGVkEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiNgoOTWVzc2FnZVVwZGF0ZWQSJAoHbWVzc2FnZRgBIAEoCzITLmNvbXBhc3MudjEuTWVzc2FnZSIxCg1Ub3BpY1Vwc2VydGVkEiAKBXRvcGljGAEgASgLMhEuY29tcGFzcy52MS5Ub3BpYyJTCg5DaGFubmVsQ2hhbmdlZBIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsEhsKE3JlbW92ZWRfYWNjb3VudF9pZHMYAiADKAkiPgoTQ2hhbm5lbEdyb3VwQ2hhbmdlZBInCgVncm91cBgBIAEoCzIYLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwIjYKDkFjY291bnRDaGFuZ2VkEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiRgoVQWdlbnRXb3Jrc3BhY2VDaGFuZ2VkEi0KCXdvcmtzcGFjZRgBIAEoCzIaLmNvbXBhc3MudjEuQWdlbnRXb3Jrc3BhY2UiFQoTQ29tbXNSZXN5bmNSZXF1aXJlZCJvChRBZ2VudFByZXNlbmNlQ2hhbmdlZBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEisKCHByZXNlbmNlGAIgASgOMhkuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlEhAKCGFjdGl2aXR5GAMgASgJIjkKEUNyZWF0ZVVzZXJSZXF1ZXN0Eg4KBmhhbmRsZRgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiOgoSQ3JlYXRlVXNlclJlc3BvbnNlEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiUwoSQ3JlYXRlQWdlbnRSZXF1ZXN0Eg4KBmhhbmRsZRgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSFwoPcGFyZW50X2FnZW50X2lkGAMgASgJIjsKE0NyZWF0ZUFnZW50UmVzcG9uc2USJAoHYWNjb3VudBgBIAEoCzITLmNvbXBhc3MudjEuQWNjb3VudCIrChNMaXN0QWNjb3VudHNSZXF1ZXN0EhQKDHNuYXBzaG90X3NlcRgBIAEoBCI9ChRMaXN0QWNjb3VudHNSZXNwb25zZRIlCghhY2NvdW50cxgBIAMoCzITLmNvbXBhc3MudjEuQWNjb3VudCJ6ChlDcmVhdGVDaGFubmVsR3JvdXBSZXF1ZXN0EgwKBG5hbWUYASABKAkSFwoPcGFyZW50X2dyb3VwX2lkGAIgASgJEjYKCnZpc2liaWxpdHkYAyABKA4yIi5jb21wYXNzLnYxLkNoYW5uZWxHcm91cFZpc2liaWxpdHkiRQoaQ3JlYXRlQ2hhbm5lbEdyb3VwUmVzcG9uc2USJwoFZ3JvdXAYASABKAsyGC5jb21wYXNzLnYxLkNoYW5uZWxHcm91cCIwChhMaXN0Q2hhbm5lbEdyb3Vwc1JlcXVlc3QSFAoMc25hcHNob3Rfc2VxGAEgASgEIkUKGUxpc3RDaGFubmVsR3JvdXBzUmVzcG9uc2USKAoGZ3JvdXBzGAEgAygLMhguY29tcGFzcy52MS5DaGFubmVsR3JvdXAiKwoTTGlzdENoYW5uZWxzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPQoUTGlzdENoYW5uZWxzUmVzcG9uc2USJQoIY2hhbm5lbHMYASADKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwieQoUQ3JlYXRlQ2hhbm5lbFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIQCghncm91cF9pZBgCIAEoCRIlCgRraW5kGAMgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIaChJtZW1iZXJfYWNjb3VudF9pZHMYBCADKAkiPQoVQ3JlYXRlQ2hhbm5lbFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwitAEKG1VwZGF0ZUNoYW5uZWxNZW1iZXJzUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEh4KFmFkZF9tZW1iZXJfYWNjb3VudF9pZHMYAiADKAkSIQoZcmVtb3ZlX21lbWJlcl9hY2NvdW50X2lkcxgDIAMoCRIdChVzdWJzY3JpYmVfYWNjb3VudF9pZHMYBCADKAkSHwoXdW5zdWJzY3JpYmVfYWNjb3VudF9pZHMYBSADKAkiRAocVXBkYXRlQ2hhbm5lbE1lbWJlcnNSZXNwb25zZRIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsIk0KFFJlcGFyZW50QWdlbnRSZXF1ZXN0EhgKEGFnZW50X2FjY291bnRfaWQYASABKAkSGwoTbmV3X3BhcmVudF9hZ2VudF9pZBgCIAEoCSI9ChVSZXBhcmVudEFnZW50UmVzcG9uc2USJAoHYWNjb3VudBgBIAEoCzITLmNvbXBhc3MudjEuQWNjb3VudCKbAQoXU2V0Q2hhbm5lbFBvbGljeVJlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIyCgtwb3N0X3BvbGljeRgCIAEoDjIdLmNvbXBhc3MudjEuQ2hhbm5lbFBvc3RQb2xpY3kSGAoQb3duZXJfYWNjb3VudF9pZBgDIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAQgASgIIkAKGFNldENoYW5uZWxQb2xpY3lSZXNwb25zZRIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsIlQKEEdldFJvc3RlclJlcXVlc3QSJgoFc2NvcGUYASABKA4yFy5jb21wYXNzLnYxLlJvc3RlclNjb3BlEhgKEGFnZW50X2FjY291bnRfaWQYAiABKAkiPQoRR2V0Um9zdGVyUmVzcG9uc2USKAoHZW50cmllcxgBIAMoCzIXLmNvbXBhc3MudjEuUm9zdGVyRW50cnkiwgEKC1Jvc3RlckVudHJ5EhgKEGFnZW50X2FjY291bnRfaWQYASABKAkSDgoGaGFuZGxlGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg9wYXJlbnRfYWdlbnRfaWQYBCABKAkSKwoIcHJlc2VuY2UYBSABKA4yGS5jb21wYXNzLnYxLkFnZW50UHJlc2VuY2USEAoIYWN0aXZpdHkYBiABKAkSGwoTYWN0aXZpdHlfYXRfdW5peF9tcxgHIAEoAyJ3ChhVcGRhdGVQaW5uZWRCb2FyZFJlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIlCgNwaW4YAiABKAsyFi5jb21wYXNzLnYxLlBpbk1lc3NhZ2VIABIaChB1bnBpbl9tZXNzYWdlX2lkGAMgASgJSABCBAoCb3AiPAoKUGluTWVzc2FnZRISCgptZXNzYWdlX2lkGAEgASgJEhoKEnJlcGxhY2VfbWVzc2FnZV9pZBgCIAEoCSJBChlVcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwiNQoZT3BlbkFnZW50V29ya3NwYWNlUmVxdWVzdBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJIksKGk9wZW5BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEi0KCXdvcmtzcGFjZRgBIAEoCzIaLmNvbXBhc3MudjEuQWdlbnRXb3Jrc3BhY2UiigEKE0xpc3RNZXNzYWdlc1JlcXVlc3QSFAoKY2hhbm5lbF9pZBgBIAEoCUgAEg0KBWxpbWl0GAIgASgNEhkKEWJlZm9yZV9tZXNzYWdlX2lkGAMgASgJEhQKDHNuYXBzaG90X3NlcRgEIAEoBBIQCgh0b3BpY19pZBgFIAEoCUILCgljb250YWluZXIiPQoUTGlzdE1lc3NhZ2VzUmVzcG9uc2USJQoIbWVzc2FnZXMYASADKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UirwEKElBvc3RNZXNzYWdlUmVxdWVzdBIUCgpjaGFubmVsX2lkGAEgASgJSAASKAoGYmxvY2tzGAIgAygLMhguY29tcGFzcy52MS5NZXNzYWdlQmxvY2sSEgoIdG9waWNfaWQYAyABKAlIARIUCgp0b3BpY19uYW1lGAQgASgJSAESGQoRY2xpZW50X3JlcXVlc3RfaWQYBSABKAlCCwoJY29udGFpbmVyQgcKBXRvcGljIjsKE1Bvc3RNZXNzYWdlUmVzcG9uc2USJAoHbWVzc2FnZRgBIAEoCzITLmNvbXBhc3MudjEuTWVzc2FnZSJBChFMaXN0VG9waWNzUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYAiABKAgiNwoSTGlzdFRvcGljc1Jlc3BvbnNlEiEKBnRvcGljcxgBIAMoCzIRLmNvbXBhc3MudjEuVG9waWMiZgoSVXBkYXRlVG9waWNSZXF1ZXN0EhAKCHRvcGljX2lkGAEgASgJEhEKBG5hbWUYAiABKAlIAIgBARIVCghhcmNoaXZlZBgDIAEoCEgBiAEBQgcKBV9uYW1lQgsKCV9hcmNoaXZlZCI3ChNVcGRhdGVUb3BpY1Jlc3BvbnNlEiAKBXRvcGljGAEgASgLMhEuY29tcGFzcy52MS5Ub3BpYyJVChNSZXNwb25kVG9Bc2tSZXF1ZXN0Eg4KBmFza19pZBgBIAEoCRIuCgdhbnN3ZXJzGAIgAygLMh0uY29tcGFzcy52MS5Bc2tRdWVzdGlvbkFuc3dlciJYChFBc2tRdWVzdGlvbkFuc3dlchITCgtxdWVzdGlvbl9pZBgBIAEoCRIZChFjaG9zZW5fb3B0aW9uX2lkcxgCIAMoCRITCgtjdXN0b21fdGV4dBgDIAEoCSIWChRSZXNwb25kVG9Bc2tSZXNwb25zZSJqChVTZWFyY2hNZXNzYWdlc1JlcXVlc3QSDQoFcXVlcnkYASABKAkSFAoKY2hhbm5lbF9pZBgCIAEoCUgAEg0KBWxpbWl0GAMgASgNEhQKDHNuYXBzaG90X3NlcRgEIAEoBEIHCgVzY29wZSI/ChZTZWFyY2hNZXNzYWdlc1Jlc3BvbnNlEiUKCG1lc3NhZ2VzGAEgAygLMhMuY29tcGFzcy52MS5NZXNzYWdlIkIKFVN1YnNjcmliZUNvbW1zUmVxdWVzdBIRCglzaW5jZV9zZXEYASABKAQSFgoOaW5zdGFuY2VfZXBvY2gYAiABKAQqNQoIVXNlclJvbGUSFAoQVVNFUl9ST0xFX01FTUJFUhAAEhMKD1VTRVJfUk9MRV9BRE1JThABKmEKFkNoYW5uZWxHcm91cFZpc2liaWxpdHkSIgoeQ0hBTk5FTF9HUk9VUF9WSVNJQklMSVRZX09XTkVSEAASIwofQ0hBTk5FTF9HUk9VUF9WSVNJQklMSVRZX1NIQVJFRBABKlUKEUNoYW5uZWxQb3N0UG9saWN5EhwKGENIQU5ORUxfUE9TVF9QT0xJQ1lfT1BFThAAEiIKHkNIQU5ORUxfUE9TVF9QT0xJQ1lfT1dORVJfT05MWRABKlcKC0NoYW5uZWxLaW5kEhgKFENIQU5ORUxfS0lORF9DSEFOTkVMEAASEwoPQ0hBTk5FTF9LSU5EX0RNEAESGQoVQ0hBTk5FTF9LSU5EX0dST1VQX0RNEAIqnAEKDUFnZW50UHJlc2VuY2USHgoaQUdFTlRfUFJFU0VOQ0VfVU5TUEVDSUZJRUQQABIXChNBR0VOVF9QUkVTRU5DRV9JRExFEAESGgoWQUdFTlRfUFJFU0VOQ0VfV09SS0lORxACEhoKFkFHRU5UX1BSRVNFTkNFX1dBSVRJTkcQAxIaChZBR0VOVF9QUkVTRU5DRV9PRkZMSU5FEAQqXgoLUm9zdGVyU2NvcGUSHQoZUk9TVEVSX1NDT1BFX05FSUdIQk9SSE9PRBAAEhgKFFJPU1RFUl9TQ09QRV9TVUJUUkVFEAESFgoSUk9TVEVSX1NDT1BFX09XTkVSEAIy5g0KDENvbW1zU2VydmljZRJLCgpDcmVhdGVVc2VyEh0uY29tcGFzcy52MS5DcmVhdGVVc2VyUmVxdWVzdBoeLmNvbXBhc3MudjEuQ3JlYXRlVXNlclJlc3BvbnNlEk4KC0NyZWF0ZUFnZW50Eh4uY29tcGFzcy52MS5DcmVhdGVBZ2VudFJlcXVlc3QaHy5jb21wYXNzLnYxLkNyZWF0ZUFnZW50UmVzcG9uc2USUQoMTGlzdEFjY291bnRzEh8uY29tcGFzcy52MS5MaXN0QWNjb3VudHNSZXF1ZXN0GiAuY29tcGFzcy52MS5MaXN0QWNjb3VudHNSZXNwb25zZRJjChJDcmVhdGVDaGFubmVsR3JvdXASJS5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxHcm91cFJlcXVlc3QaJi5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxHcm91cFJlc3BvbnNlEmAKEUxpc3RDaGFubmVsR3JvdXBzEiQuY29tcGFzcy52MS5MaXN0Q2hhbm5lbEdyb3Vwc1JlcXVlc3QaJS5jb21wYXNzLnYxLkxpc3RDaGFubmVsR3JvdXBzUmVzcG9uc2USUQoMTGlzdENoYW5uZWxzEh8uY29tcGFzcy52MS5MaXN0Q2hhbm5lbHNSZXF1ZXN0GiAuY29tcGFzcy52MS5MaXN0Q2hhbm5lbHNSZXNwb25zZRJUCg1DcmVhdGVDaGFubmVsEiAuY29tcGFzcy52MS5DcmVhdGVDaGFubmVsUmVxdWVzdBohLmNvbXBhc3MudjEuQ3JlYXRlQ2hhbm5lbFJlc3BvbnNlEmkKFFVwZGF0ZUNoYW5uZWxNZW1iZXJzEicuY29tcGFzcy52MS5VcGRhdGVDaGFubmVsTWVtYmVyc1JlcXVlc3QaKC5jb21wYXNzLnYxLlVwZGF0ZUNoYW5uZWxNZW1iZXJzUmVzcG9uc2USVAoNUmVwYXJlbnRBZ2VudBIgLmNvbXBhc3MudjEuUmVwYXJlbnRBZ2VudFJlcXVlc3QaIS5jb21wYXNzLnYxLlJlcGFyZW50QWdlbnRSZXNwb25zZRJjChJPcGVuQWdlbnRXb3Jrc3BhY2USJS5jb21wYXNzLnYxLk9wZW5BZ2VudFdvcmtzcGFjZVJlcXVlc3QaJi5jb21wYXNzLnYxLk9wZW5BZ2VudFdvcmtzcGFjZVJlc3BvbnNlElEKDExpc3RNZXNzYWdlcxIfLmNvbXBhc3MudjEuTGlzdE1lc3NhZ2VzUmVxdWVzdBogLmNvbXBhc3MudjEuTGlzdE1lc3NhZ2VzUmVzcG9uc2USTgoLUG9zdE1lc3NhZ2USHi5jb21wYXNzLnYxLlBvc3RNZXNzYWdlUmVxdWVzdBofLmNvbXBhc3MudjEuUG9zdE1lc3NhZ2VSZXNwb25zZRJLCgpMaXN0VG9waWNzEh0uY29tcGFzcy52MS5MaXN0VG9waWNzUmVxdWVzdBoeLmNvbXBhc3MudjEuTGlzdFRvcGljc1Jlc3BvbnNlEk4KC1VwZGF0ZVRvcGljEh4uY29tcGFzcy52MS5VcGRhdGVUb3BpY1JlcXVlc3QaHy5jb21wYXNzLnYxLlVwZGF0ZVRvcGljUmVzcG9uc2USUQoMUmVzcG9uZFRvQXNrEh8uY29tcGFzcy52MS5SZXNwb25kVG9Bc2tSZXF1ZXN0GiAuY29tcGFzcy52MS5SZXNwb25kVG9Bc2tSZXNwb25zZRJXCg5TZWFyY2hNZXNzYWdlcxIhLmNvbXBhc3MudjEuU2VhcmNoTWVzc2FnZXNSZXF1ZXN0GiIuY29tcGFzcy52MS5TZWFyY2hNZXNzYWdlc1Jlc3BvbnNlEl0KEFNldENoYW5uZWxQb2xpY3kSIy5jb21wYXNzLnYxLlNldENoYW5uZWxQb2xpY3lSZXF1ZXN0GiQuY29tcGFzcy52MS5TZXRDaGFubmVsUG9saWN5UmVzcG9uc2USSAoJR2V0Um9zdGVyEhwuY29tcGFzcy52MS5HZXRSb3N0ZXJSZXF1ZXN0Gh0uY29tcGFzcy52MS5HZXRSb3N0ZXJSZXNwb25zZRJgChFVcGRhdGVQaW5uZWRCb2FyZBIkLmNvbXBhc3MudjEuVXBkYXRlUGlubmVkQm9hcmRSZXF1ZXN0GiUuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlElkKDlN1YnNjcmliZUNvbW1zEiEuY29tcGFzcy52MS5TdWJzY3JpYmVDb21tc1JlcXVlc3QaIi5jb21wYXNzLnYxLlN1YnNjcmliZUNvbW1zUmVzcG9uc2UwAWIGcHJvdG8z"); + fileDesc("ChZjb21wYXNzL3YxL2NvbW1zLnByb3RvEgpjb21wYXNzLnYxIsQBCgdBY2NvdW50EgoKAmlkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSJwoEdXNlchgKIAEoCzIXLmNvbXBhc3MudjEuVXNlckFjY291bnRIABIpCgVhZ2VudBgLIAEoCzIYLmNvbXBhc3MudjEuQWdlbnRBY2NvdW50SAASKwoGc3lzdGVtGAwgASgLMhkuY29tcGFzcy52MS5TeXN0ZW1BY2NvdW50SABCBgoEa2luZCIPCg1TeXN0ZW1BY2NvdW50IjEKC1VzZXJBY2NvdW50EiIKBHJvbGUYASABKA4yFC5jb21wYXNzLnYxLlVzZXJSb2xlIlcKDEFnZW50QWNjb3VudBIVCg1vd25lcl91c2VyX2lkGAEgASgJEhcKD2hvbWVfY2hhbm5lbF9pZBgCIAEoCRIXCg9wYXJlbnRfYWdlbnRfaWQYAyABKAkikAEKDENoYW5uZWxHcm91cBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhcKD3BhcmVudF9ncm91cF9pZBgDIAEoCRIVCg1vd25lcl91c2VyX2lkGAQgASgJEjYKCnZpc2liaWxpdHkYBSABKA4yIi5jb21wYXNzLnYxLkNoYW5uZWxHcm91cFZpc2liaWxpdHkitwIKB0NoYW5uZWwSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghncm91cF9pZBgDIAEoCRIlCgRraW5kGAQgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIaChJtZW1iZXJfYWNjb3VudF9pZHMYBSADKAkSHgoWc3Vic2NyaWJlcl9hY2NvdW50X2lkcxgGIAMoCRIyCgtwb3N0X3BvbGljeRgHIAEoDjIdLmNvbXBhc3MudjEuQ2hhbm5lbFBvc3RQb2xpY3kSGAoQb3duZXJfYWNjb3VudF9pZBgIIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAkgASgIEi8KDnBpbm5lZF9lbnRyaWVzGAogAygLMhcuY29tcGFzcy52MS5QaW5uZWRFbnRyeSJsCgtQaW5uZWRFbnRyeRISCgptZXNzYWdlX2lkGAEgASgJEhAKCHBvc2l0aW9uGAIgASgFEhkKEXBpbm5lZF9hdF91bml4X21zGAMgASgDEhwKFHBpbm5lZF9ieV9hY2NvdW50X2lkGAQgASgJIjYKDkFnZW50V29ya3NwYWNlEgoKAmlkGAEgASgJEhgKEGFnZW50X2FjY291bnRfaWQYAiABKAkiggEKBVRvcGljEgoKAmlkGAEgASgJEhIKCmNoYW5uZWxfaWQYAiABKAkSDAoEbmFtZRgDIAEoCRIaChJjcmVhdGVkX2F0X3VuaXhfbXMYBCABKAMSHQoVY3JlYXRlZF9ieV9hY2NvdW50X2lkGAUgASgJEhAKCGFyY2hpdmVkGAYgASgIIoABCgdNZXNzYWdlEgoKAmlkGAEgASgJEhAKCHRvcGljX2lkGAIgASgJEhkKEWF1dGhvcl9hY2NvdW50X2lkGAMgASgJEhIKCmF0X3VuaXhfbXMYBCABKAMSKAoGYmxvY2tzGAUgAygLMhguY29tcGFzcy52MS5NZXNzYWdlQmxvY2sieQoMTWVzc2FnZUJsb2NrEg4KBHRleHQYASABKAlIABIeCgNhc2sYAiABKAsyDy5jb21wYXNzLnYxLkFza0gAEjAKCmFza19hbnN3ZXIYAyABKAsyGi5jb21wYXNzLnYxLkFza0Fuc3dlckJsb2NrSABCBwoFYmxvY2siSAoOQXNrQW5zd2VyQmxvY2sSHAoDYXNrGAEgASgLMg8uY29tcGFzcy52MS5Bc2sSGAoQYXNrZXJfYWNjb3VudF9pZBgCIAEoCSJTCgNBc2sSDgoGYXNrX2lkGAEgASgJEioKCXF1ZXN0aW9ucxgCIAMoCzIXLmNvbXBhc3MudjEuQXNrUXVlc3Rpb24SEAoIYW5zd2VyZWQYAyABKAgi8QEKC0Fza1F1ZXN0aW9uEhMKC3F1ZXN0aW9uX2lkGAEgASgJEhAKCHF1ZXN0aW9uGAIgASgJEg4KBmhlYWRlchgDIAEoCRImCgdvcHRpb25zGAQgAygLMhUuY29tcGFzcy52MS5Bc2tPcHRpb24SFgoOYWxsb3dfbXVsdGlwbGUYBSABKAgSGAoLcmVjb21tZW5kZWQYBiABKAVIAIgBARIZChFjaG9zZW5fb3B0aW9uX2lkcxgHIAMoCRITCgtjdXN0b21fdGV4dBgIIAEoCRIRCgl0aW1lZF9vdXQYCSABKAhCDgoMX3JlY29tbWVuZGVkIkwKCUFza09wdGlvbhIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIPCgdwcmV2aWV3GAQgASgJIokFChZTdWJzY3JpYmVDb21tc1Jlc3BvbnNlEgsKA3NlcRgBIAEoBBISCgphdF91bml4X21zGAIgASgDEhYKDmluc3RhbmNlX2Vwb2NoGAMgASgEEhQKDHNuYXBzaG90X3NlcRgEIAEoBBIzCg5tZXNzYWdlX3Bvc3RlZBgKIAEoCzIZLmNvbXBhc3MudjEuTWVzc2FnZVBvc3RlZEgAEjUKD21lc3NhZ2VfdXBkYXRlZBgLIAEoCzIaLmNvbXBhc3MudjEuTWVzc2FnZVVwZGF0ZWRIABI1Cg9jaGFubmVsX2NoYW5nZWQYDCABKAsyGi5jb21wYXNzLnYxLkNoYW5uZWxDaGFuZ2VkSAASQAoVY2hhbm5lbF9ncm91cF9jaGFuZ2VkGA0gASgLMh8uY29tcGFzcy52MS5DaGFubmVsR3JvdXBDaGFuZ2VkSAASNQoPYWNjb3VudF9jaGFuZ2VkGA4gASgLMhouY29tcGFzcy52MS5BY2NvdW50Q2hhbmdlZEgAEkQKF2FnZW50X3dvcmtzcGFjZV9jaGFuZ2VkGA8gASgLMiEuY29tcGFzcy52MS5BZ2VudFdvcmtzcGFjZUNoYW5nZWRIABI6Cg9yZXN5bmNfcmVxdWlyZWQYECABKAsyHy5jb21wYXNzLnYxLkNvbW1zUmVzeW5jUmVxdWlyZWRIABJCChZhZ2VudF9wcmVzZW5jZV9jaGFuZ2VkGBEgASgLMiAuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlQ2hhbmdlZEgAEjMKDnRvcGljX3Vwc2VydGVkGBIgASgLMhkuY29tcGFzcy52MS5Ub3BpY1Vwc2VydGVkSABCCQoHcGF5bG9hZCI1Cg1NZXNzYWdlUG9zdGVkEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiNgoOTWVzc2FnZVVwZGF0ZWQSJAoHbWVzc2FnZRgBIAEoCzITLmNvbXBhc3MudjEuTWVzc2FnZSIxCg1Ub3BpY1Vwc2VydGVkEiAKBXRvcGljGAEgASgLMhEuY29tcGFzcy52MS5Ub3BpYyJTCg5DaGFubmVsQ2hhbmdlZBIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsEhsKE3JlbW92ZWRfYWNjb3VudF9pZHMYAiADKAkiPgoTQ2hhbm5lbEdyb3VwQ2hhbmdlZBInCgVncm91cBgBIAEoCzIYLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwIjYKDkFjY291bnRDaGFuZ2VkEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiRgoVQWdlbnRXb3Jrc3BhY2VDaGFuZ2VkEi0KCXdvcmtzcGFjZRgBIAEoCzIaLmNvbXBhc3MudjEuQWdlbnRXb3Jrc3BhY2UiFQoTQ29tbXNSZXN5bmNSZXF1aXJlZCJvChRBZ2VudFByZXNlbmNlQ2hhbmdlZBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEisKCHByZXNlbmNlGAIgASgOMhkuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlEhAKCGFjdGl2aXR5GAMgASgJIjkKEUNyZWF0ZVVzZXJSZXF1ZXN0Eg4KBmhhbmRsZRgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiOgoSQ3JlYXRlVXNlclJlc3BvbnNlEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiUQoSQ3JlYXRlQWdlbnRSZXF1ZXN0Eg4KBmhhbmRsZRgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSFQoNcGFyZW50X2hhbmRsZRgDIAEoCSI7ChNDcmVhdGVBZ2VudFJlc3BvbnNlEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiKwoTTGlzdEFjY291bnRzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPQoUTGlzdEFjY291bnRzUmVzcG9uc2USJQoIYWNjb3VudHMYASADKAsyEy5jb21wYXNzLnYxLkFjY291bnQiegoZQ3JlYXRlQ2hhbm5lbEdyb3VwUmVxdWVzdBIMCgRuYW1lGAEgASgJEhcKD3BhcmVudF9ncm91cF9pZBgCIAEoCRI2Cgp2aXNpYmlsaXR5GAMgASgOMiIuY29tcGFzcy52MS5DaGFubmVsR3JvdXBWaXNpYmlsaXR5IkUKGkNyZWF0ZUNoYW5uZWxHcm91cFJlc3BvbnNlEicKBWdyb3VwGAEgASgLMhguY29tcGFzcy52MS5DaGFubmVsR3JvdXAiMAoYTGlzdENoYW5uZWxHcm91cHNSZXF1ZXN0EhQKDHNuYXBzaG90X3NlcRgBIAEoBCJFChlMaXN0Q2hhbm5lbEdyb3Vwc1Jlc3BvbnNlEigKBmdyb3VwcxgBIAMoCzIYLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwIisKE0xpc3RDaGFubmVsc1JlcXVlc3QSFAoMc25hcHNob3Rfc2VxGAEgASgEIj0KFExpc3RDaGFubmVsc1Jlc3BvbnNlEiUKCGNoYW5uZWxzGAEgAygLMhMuY29tcGFzcy52MS5DaGFubmVsInUKFENyZWF0ZUNoYW5uZWxSZXF1ZXN0EgwKBG5hbWUYASABKAkSEAoIZ3JvdXBfaWQYAiABKAkSJQoEa2luZBgDIAEoDjIXLmNvbXBhc3MudjEuQ2hhbm5lbEtpbmQSFgoObWVtYmVyX2hhbmRsZXMYBCADKAkiPQoVQ3JlYXRlQ2hhbm5lbFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwipAEKG1VwZGF0ZUNoYW5uZWxNZW1iZXJzUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEhoKEmFkZF9tZW1iZXJfaGFuZGxlcxgCIAMoCRIdChVyZW1vdmVfbWVtYmVyX2hhbmRsZXMYAyADKAkSGQoRc3Vic2NyaWJlX2hhbmRsZXMYBCADKAkSGwoTdW5zdWJzY3JpYmVfaGFuZGxlcxgFIAMoCSJEChxVcGRhdGVDaGFubmVsTWVtYmVyc1Jlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwiRwoUUmVwYXJlbnRBZ2VudFJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEW5ld19wYXJlbnRfaGFuZGxlGAIgASgJIj0KFVJlcGFyZW50QWdlbnRSZXNwb25zZRIkCgdhY2NvdW50GAEgASgLMhMuY29tcGFzcy52MS5BY2NvdW50IpcBChdTZXRDaGFubmVsUG9saWN5UmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEjIKC3Bvc3RfcG9saWN5GAIgASgOMh0uY29tcGFzcy52MS5DaGFubmVsUG9zdFBvbGljeRIUCgxvd25lcl9oYW5kbGUYAyABKAkSHgoWbWFuZGF0b3J5X3N1YnNjcmlwdGlvbhgEIAEoCCJAChhTZXRDaGFubmVsUG9saWN5UmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCJSChBHZXRSb3N0ZXJSZXF1ZXN0EiYKBXNjb3BlGAEgASgOMhcuY29tcGFzcy52MS5Sb3N0ZXJTY29wZRIWCg52YW50YWdlX2hhbmRsZRgCIAEoCSI9ChFHZXRSb3N0ZXJSZXNwb25zZRIoCgdlbnRyaWVzGAEgAygLMhcuY29tcGFzcy52MS5Sb3N0ZXJFbnRyeSLCAQoLUm9zdGVyRW50cnkSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIOCgZoYW5kbGUYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEhcKD3BhcmVudF9hZ2VudF9pZBgEIAEoCRIrCghwcmVzZW5jZRgFIAEoDjIZLmNvbXBhc3MudjEuQWdlbnRQcmVzZW5jZRIQCghhY3Rpdml0eRgGIAEoCRIbChNhY3Rpdml0eV9hdF91bml4X21zGAcgASgDIncKGFVwZGF0ZVBpbm5lZEJvYXJkUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEiUKA3BpbhgCIAEoCzIWLmNvbXBhc3MudjEuUGluTWVzc2FnZUgAEhoKEHVucGluX21lc3NhZ2VfaWQYAyABKAlIAEIECgJvcCI8CgpQaW5NZXNzYWdlEhIKCm1lc3NhZ2VfaWQYASABKAkSGgoScmVwbGFjZV9tZXNzYWdlX2lkGAIgASgJIkEKGVVwZGF0ZVBpbm5lZEJvYXJkUmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCIxChlPcGVuQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCSJLChpPcGVuQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRItCgl3b3Jrc3BhY2UYASABKAsyGi5jb21wYXNzLnYxLkFnZW50V29ya3NwYWNlIooBChNMaXN0TWVzc2FnZXNSZXF1ZXN0EhQKCmNoYW5uZWxfaWQYASABKAlIABINCgVsaW1pdBgCIAEoDRIZChFiZWZvcmVfbWVzc2FnZV9pZBgDIAEoCRIUCgxzbmFwc2hvdF9zZXEYBCABKAQSEAoIdG9waWNfaWQYBSABKAlCCwoJY29udGFpbmVyIj0KFExpc3RNZXNzYWdlc1Jlc3BvbnNlEiUKCG1lc3NhZ2VzGAEgAygLMhMuY29tcGFzcy52MS5NZXNzYWdlIq8BChJQb3N0TWVzc2FnZVJlcXVlc3QSFAoKY2hhbm5lbF9pZBgBIAEoCUgAEigKBmJsb2NrcxgCIAMoCzIYLmNvbXBhc3MudjEuTWVzc2FnZUJsb2NrEhIKCHRvcGljX2lkGAMgASgJSAESFAoKdG9waWNfbmFtZRgEIAEoCUgBEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAUgASgJQgsKCWNvbnRhaW5lckIHCgV0b3BpYyI7ChNQb3N0TWVzc2FnZVJlc3BvbnNlEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiQQoRTGlzdFRvcGljc1JlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIYChBpbmNsdWRlX2FyY2hpdmVkGAIgASgIIjcKEkxpc3RUb3BpY3NSZXNwb25zZRIhCgZ0b3BpY3MYASADKAsyES5jb21wYXNzLnYxLlRvcGljImYKElVwZGF0ZVRvcGljUmVxdWVzdBIQCgh0b3BpY19pZBgBIAEoCRIRCgRuYW1lGAIgASgJSACIAQESFQoIYXJjaGl2ZWQYAyABKAhIAYgBAUIHCgVfbmFtZUILCglfYXJjaGl2ZWQiNwoTVXBkYXRlVG9waWNSZXNwb25zZRIgCgV0b3BpYxgBIAEoCzIRLmNvbXBhc3MudjEuVG9waWMiVQoTUmVzcG9uZFRvQXNrUmVxdWVzdBIOCgZhc2tfaWQYASABKAkSLgoHYW5zd2VycxgCIAMoCzIdLmNvbXBhc3MudjEuQXNrUXVlc3Rpb25BbnN3ZXIiWAoRQXNrUXVlc3Rpb25BbnN3ZXISEwoLcXVlc3Rpb25faWQYASABKAkSGQoRY2hvc2VuX29wdGlvbl9pZHMYAiADKAkSEwoLY3VzdG9tX3RleHQYAyABKAkiFgoUUmVzcG9uZFRvQXNrUmVzcG9uc2UiagoVU2VhcmNoTWVzc2FnZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEhQKCmNoYW5uZWxfaWQYAiABKAlIABINCgVsaW1pdBgDIAEoDRIUCgxzbmFwc2hvdF9zZXEYBCABKARCBwoFc2NvcGUiPwoWU2VhcmNoTWVzc2FnZXNSZXNwb25zZRIlCghtZXNzYWdlcxgBIAMoCzITLmNvbXBhc3MudjEuTWVzc2FnZSJCChVTdWJzY3JpYmVDb21tc1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEKjUKCFVzZXJSb2xlEhQKEFVTRVJfUk9MRV9NRU1CRVIQABITCg9VU0VSX1JPTEVfQURNSU4QASphChZDaGFubmVsR3JvdXBWaXNpYmlsaXR5EiIKHkNIQU5ORUxfR1JPVVBfVklTSUJJTElUWV9PV05FUhAAEiMKH0NIQU5ORUxfR1JPVVBfVklTSUJJTElUWV9TSEFSRUQQASpVChFDaGFubmVsUG9zdFBvbGljeRIcChhDSEFOTkVMX1BPU1RfUE9MSUNZX09QRU4QABIiCh5DSEFOTkVMX1BPU1RfUE9MSUNZX09XTkVSX09OTFkQASpXCgtDaGFubmVsS2luZBIYChRDSEFOTkVMX0tJTkRfQ0hBTk5FTBAAEhMKD0NIQU5ORUxfS0lORF9ETRABEhkKFUNIQU5ORUxfS0lORF9HUk9VUF9ETRACKpwBCg1BZ2VudFByZXNlbmNlEh4KGkFHRU5UX1BSRVNFTkNFX1VOU1BFQ0lGSUVEEAASFwoTQUdFTlRfUFJFU0VOQ0VfSURMRRABEhoKFkFHRU5UX1BSRVNFTkNFX1dPUktJTkcQAhIaChZBR0VOVF9QUkVTRU5DRV9XQUlUSU5HEAMSGgoWQUdFTlRfUFJFU0VOQ0VfT0ZGTElORRAEKl4KC1Jvc3RlclNjb3BlEh0KGVJPU1RFUl9TQ09QRV9ORUlHSEJPUkhPT0QQABIYChRST1NURVJfU0NPUEVfU1VCVFJFRRABEhYKElJPU1RFUl9TQ09QRV9PV05FUhACMuYNCgxDb21tc1NlcnZpY2USSwoKQ3JlYXRlVXNlchIdLmNvbXBhc3MudjEuQ3JlYXRlVXNlclJlcXVlc3QaHi5jb21wYXNzLnYxLkNyZWF0ZVVzZXJSZXNwb25zZRJOCgtDcmVhdGVBZ2VudBIeLmNvbXBhc3MudjEuQ3JlYXRlQWdlbnRSZXF1ZXN0Gh8uY29tcGFzcy52MS5DcmVhdGVBZ2VudFJlc3BvbnNlElEKDExpc3RBY2NvdW50cxIfLmNvbXBhc3MudjEuTGlzdEFjY291bnRzUmVxdWVzdBogLmNvbXBhc3MudjEuTGlzdEFjY291bnRzUmVzcG9uc2USYwoSQ3JlYXRlQ2hhbm5lbEdyb3VwEiUuY29tcGFzcy52MS5DcmVhdGVDaGFubmVsR3JvdXBSZXF1ZXN0GiYuY29tcGFzcy52MS5DcmVhdGVDaGFubmVsR3JvdXBSZXNwb25zZRJgChFMaXN0Q2hhbm5lbEdyb3VwcxIkLmNvbXBhc3MudjEuTGlzdENoYW5uZWxHcm91cHNSZXF1ZXN0GiUuY29tcGFzcy52MS5MaXN0Q2hhbm5lbEdyb3Vwc1Jlc3BvbnNlElEKDExpc3RDaGFubmVscxIfLmNvbXBhc3MudjEuTGlzdENoYW5uZWxzUmVxdWVzdBogLmNvbXBhc3MudjEuTGlzdENoYW5uZWxzUmVzcG9uc2USVAoNQ3JlYXRlQ2hhbm5lbBIgLmNvbXBhc3MudjEuQ3JlYXRlQ2hhbm5lbFJlcXVlc3QaIS5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxSZXNwb25zZRJpChRVcGRhdGVDaGFubmVsTWVtYmVycxInLmNvbXBhc3MudjEuVXBkYXRlQ2hhbm5lbE1lbWJlcnNSZXF1ZXN0GiguY29tcGFzcy52MS5VcGRhdGVDaGFubmVsTWVtYmVyc1Jlc3BvbnNlElQKDVJlcGFyZW50QWdlbnQSIC5jb21wYXNzLnYxLlJlcGFyZW50QWdlbnRSZXF1ZXN0GiEuY29tcGFzcy52MS5SZXBhcmVudEFnZW50UmVzcG9uc2USYwoST3BlbkFnZW50V29ya3NwYWNlEiUuY29tcGFzcy52MS5PcGVuQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GiYuY29tcGFzcy52MS5PcGVuQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJRCgxMaXN0TWVzc2FnZXMSHy5jb21wYXNzLnYxLkxpc3RNZXNzYWdlc1JlcXVlc3QaIC5jb21wYXNzLnYxLkxpc3RNZXNzYWdlc1Jlc3BvbnNlEk4KC1Bvc3RNZXNzYWdlEh4uY29tcGFzcy52MS5Qb3N0TWVzc2FnZVJlcXVlc3QaHy5jb21wYXNzLnYxLlBvc3RNZXNzYWdlUmVzcG9uc2USSwoKTGlzdFRvcGljcxIdLmNvbXBhc3MudjEuTGlzdFRvcGljc1JlcXVlc3QaHi5jb21wYXNzLnYxLkxpc3RUb3BpY3NSZXNwb25zZRJOCgtVcGRhdGVUb3BpYxIeLmNvbXBhc3MudjEuVXBkYXRlVG9waWNSZXF1ZXN0Gh8uY29tcGFzcy52MS5VcGRhdGVUb3BpY1Jlc3BvbnNlElEKDFJlc3BvbmRUb0FzaxIfLmNvbXBhc3MudjEuUmVzcG9uZFRvQXNrUmVxdWVzdBogLmNvbXBhc3MudjEuUmVzcG9uZFRvQXNrUmVzcG9uc2USVwoOU2VhcmNoTWVzc2FnZXMSIS5jb21wYXNzLnYxLlNlYXJjaE1lc3NhZ2VzUmVxdWVzdBoiLmNvbXBhc3MudjEuU2VhcmNoTWVzc2FnZXNSZXNwb25zZRJdChBTZXRDaGFubmVsUG9saWN5EiMuY29tcGFzcy52MS5TZXRDaGFubmVsUG9saWN5UmVxdWVzdBokLmNvbXBhc3MudjEuU2V0Q2hhbm5lbFBvbGljeVJlc3BvbnNlEkgKCUdldFJvc3RlchIcLmNvbXBhc3MudjEuR2V0Um9zdGVyUmVxdWVzdBodLmNvbXBhc3MudjEuR2V0Um9zdGVyUmVzcG9uc2USYAoRVXBkYXRlUGlubmVkQm9hcmQSJC5jb21wYXNzLnYxLlVwZGF0ZVBpbm5lZEJvYXJkUmVxdWVzdBolLmNvbXBhc3MudjEuVXBkYXRlUGlubmVkQm9hcmRSZXNwb25zZRJZCg5TdWJzY3JpYmVDb21tcxIhLmNvbXBhc3MudjEuU3Vic2NyaWJlQ29tbXNSZXF1ZXN0GiIuY29tcGFzcy52MS5TdWJzY3JpYmVDb21tc1Jlc3BvbnNlMAFiBnByb3RvMw"); /** * An account in the communication layer: a human user or an owned agent. Both @@ -1162,13 +1162,13 @@ export type CreateAgentRequest = Message$1<"compass.v1.CreateAgentRequest"> & { displayName: string; /** - * Optional parent in the agent tree; empty = root. The server validates it - * (must resolve to an existing agent under the caller's resolved owner) before - * set-at-creation. + * Optional parent in the agent tree; empty = root. A `@handle`; the server + * resolves it to an account id; unknown → NOT_FOUND. Resolves owner-qualified + * (a bare handle defaults to the caller's own owner namespace). * - * @generated from field: string parent_agent_id = 3; + * @generated from field: string parent_handle = 3; */ - parentAgentId: string; + parentHandle: string; }; /** @@ -1379,11 +1379,12 @@ export type CreateChannelRequest = Message$1<"compass.v1.CreateChannelRequest"> kind: ChannelKind; /** - * Initial members party to the channel. + * Initial members party to the channel. Each is a `@handle`; the server + * resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: repeated string member_account_ids = 4; + * @generated from field: repeated string member_handles = 4; */ - memberAccountIds: string[]; + memberHandles: string[]; }; /** @@ -1422,32 +1423,37 @@ export type UpdateChannelMembersRequest = Message$1<"compass.v1.UpdateChannelMem channelId: string; /** - * Accounts to add as members (join, read access). + * Accounts to add as members (join, read access). Each is a `@handle`; the + * server resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: repeated string add_member_account_ids = 2; + * @generated from field: repeated string add_member_handles = 2; */ - addMemberAccountIds: string[]; + addMemberHandles: string[]; /** - * Accounts to remove from membership. + * Accounts to remove from membership. Each is a `@handle`; the server + * resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: repeated string remove_member_account_ids = 3; + * @generated from field: repeated string remove_member_handles = 3; */ - removeMemberAccountIds: string[]; + removeMemberHandles: string[]; /** * Members to mark subscribed (push opt-in); must be current or added members. + * Each is a `@handle`; the server resolves it to an account id; unknown → + * NOT_FOUND. * - * @generated from field: repeated string subscribe_account_ids = 4; + * @generated from field: repeated string subscribe_handles = 4; */ - subscribeAccountIds: string[]; + subscribeHandles: string[]; /** - * Members to mark unsubscribed (read-only). + * Members to mark unsubscribed (read-only). Each is a `@handle`; the server + * resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: repeated string unsubscribe_account_ids = 5; + * @generated from field: repeated string unsubscribe_handles = 5; */ - unsubscribeAccountIds: string[]; + unsubscribeHandles: string[]; }; /** @@ -1479,18 +1485,20 @@ export const UpdateChannelMembersResponseSchema: GenMessage & { /** - * The agent to move. + * The agent to move. A `@handle`; the server resolves it to an account id; + * unknown → NOT_FOUND. * - * @generated from field: string agent_account_id = 1; + * @generated from field: string agent_handle = 1; */ - agentAccountId: string; + agentHandle: string; /** - * The new parent; empty promotes the agent to a root. + * The new parent; empty promotes the agent to a root. A `@handle`; the server + * resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: string new_parent_agent_id = 2; + * @generated from field: string new_parent_handle = 2; */ - newParentAgentId: string; + newParentHandle: string; }; /** @@ -1536,11 +1544,12 @@ export type SetChannelPolicyRequest = Message$1<"compass.v1.SetChannelPolicyRequ postPolicy: ChannelPostPolicy; /** - * The owner/operator account for the channel; empty leaves it unowned. + * The owner/operator account for the channel; empty leaves it unowned. A + * `@handle`; the server resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: string owner_account_id = 3; + * @generated from field: string owner_handle = 3; */ - ownerAccountId: string; + ownerHandle: string; /** * Whether membership implies a non-togglable subscription. @@ -1588,12 +1597,13 @@ export type GetRosterRequest = Message$1<"compass.v1.GetRosterRequest"> & { scope: RosterScope; /** - * The agent whose vantage to use. Optional for human/UI callers naming a - * vantage point; an agent caller gets it session-resolved server-side. + * The vantage whose roster to compute. A `@handle`; empty ⇒ the caller's own + * vantage (an agent caller is session-resolved server-side), non-empty ⇒ the + * server resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: string agent_account_id = 2; + * @generated from field: string vantage_handle = 2; */ - agentAccountId: string; + vantageHandle: string; }; /** @@ -1760,11 +1770,12 @@ export const UpdatePinnedBoardResponseSchema: GenMessage & { /** - * The agent account to open the workspace (ACP surface) for. + * The agent account to open the workspace (ACP surface) for. A `@handle`; the + * server resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: string agent_account_id = 1; + * @generated from field: string agent_handle = 1; */ - agentAccountId: string; + agentHandle: string; }; /** diff --git a/packages/compass-agent/src/gen/compass/v1/compass_pb.ts b/packages/compass-agent/src/gen/compass/v1/compass_pb.ts index db8ae4bd..3667a4b8 100644 --- a/packages/compass-agent/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-agent/src/gen/compass/v1/compass_pb.ts @@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file compass/v1/compass.proto. */ export const file_compass_v1_compass: GenFile = /*@__PURE__*/ - fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkInAKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJIkkKEUFnZW50TWVzc2FnZUNodW5rEhIKCnNlc3Npb25faWQYASABKAkSDAoEdGV4dBgCIAEoCRISCgppc190aG91Z2h0GAMgASgIInkKDUFnZW50VG9vbENhbGwSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAkSDQoFdGl0bGUYAyABKAkSLwoGc3RhdHVzGAQgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIkwKCUFnZW50UGxhbhISCgpzZXNzaW9uX2lkGAEgASgJEisKB2VudHJpZXMYAiADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IlMKDkFnZW50UGxhbkVudHJ5Eg8KB2NvbnRlbnQYASABKAkSMAoGc3RhdHVzGAIgASgOMiAuY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeVN0YXR1cyKsAwoMU2Vzc2lvbkV2ZW50EhAKCGV2ZW50X2lkGAEgASgJEhIKCmF0X3VuaXhfbXMYAiABKAMSOgoOYXNzaXN0YW50X3RleHQYAyABKAsyIC5jb21wYXNzLnYxLlNlc3Npb25Bc3Npc3RhbnRUZXh0SAASLwoIdGhpbmtpbmcYBCABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25UaGlua2luZ0gAEjAKCXRvb2xfY2FsbBgFIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsSAASPQoQdG9vbF9jYWxsX3VwZGF0ZRgGIAEoCzIhLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsVXBkYXRlSAASJwoEcGxhbhgHIAEoCzIXLmNvbXBhc3MudjEuU2Vzc2lvblBsYW5IABIrCgZub3RpY2UYCCABKAsyGS5jb21wYXNzLnYxLlNlc3Npb25Ob3RpY2VIABI5ChFzZXNzaW9uX2luamVjdGlvbhgJIAEoCzIcLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbkgAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIm4KEFNlc3Npb25JbmplY3Rpb24SMQoHb3Bfa2luZBgBIAEoDjIgLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbktpbmQSEgoKbWVzc2FnZV9pZBgCIAEoCRITCgtmcm9tX2hhbmRsZRgDIAEoCSIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkifgoRQWdlbnRTZXNzaW9uRnJhbWUSEgoKc2Vzc2lvbl9pZBgBIAEoCRInCgVldmVudBgCIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkV2ZW50EiwKBXN0YXRlGAMgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZSJ0Ch5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCRIPCgdwZXJzb25hGAMgASgJEgwKBHJvbGUYBCABKAkiOQofUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRIWCg5jb250YWluZXJfbmFtZRgBIAEoCSJQChtSZW1vdmVBZ2VudFdvcmtzcGFjZVJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkiHgocUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZSJjChhTdGFydEFnZW50U2Vzc2lvblJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRcmVzdW1lX3Nlc3Npb25faWQYAyABKAlKBAgCEANSDmluaXRpYWxfcHJvbXB0Ii8KGVN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSJeChFTcGF3bkFnZW50UmVxdWVzdBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAMgASgJSgQIAhADUg5pbml0aWFsX3Byb21wdCJAChJTcGF3bkFnZW50UmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIWCg5jb250YWluZXJfbmFtZRgCIAEoCSItChdTdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhoKGFN0b3BBZ2VudFNlc3Npb25SZXNwb25zZSIvChlSZWxvYWRBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiMAoaUmVsb2FkQWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSIrChVHZXRBZ2VudFN0YXR1c1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJKChZHZXRBZ2VudFN0YXR1c1Jlc3BvbnNlEjAKCHN0YXR1c2VzGAEgAygLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXMiJwoRSXNzdWVUb2tlblJlcXVlc3QSEgoKYWNjb3VudF9pZBgBIAEoCSIjChJJc3N1ZVRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkiKAoSUmV2b2tlVG9rZW5SZXF1ZXN0EhIKBXRva2VuGAEgASgJQgOAAQEiFQoTUmV2b2tlVG9rZW5SZXNwb25zZSInChVQdXRBZ2VudENvbmZpZ1JlcXVlc3QSDgoGYnVuZGxlGAEgASgMIikKFlB1dEFnZW50Q29uZmlnUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCSIbChlHZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0IskBChpHZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJEg4KBnNraWxscxgCIAMoCRISCgpleHRlbnNpb25zGAMgAygJEhMKC21jcF9zZXJ2ZXJzGAQgAygJEhQKDGhhc19zZXR0aW5ncxgFIAEoCBIVCg1oYXNfYWdlbnRzX21kGAYgASgIEg0KBXJ1bGVzGAcgAygJEhEKCXN1YmFnZW50cxgIIAMoCRISCgpoYXNfbW9kZWxzGAkgASgIIhoKGERlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdCIbChlEZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlIigKEEFnZW50QXR0cmlidXRpb24SFAoMYWdlbnRfaGFuZGxlGAEgASgJIkUKCEZvcmdlUmVmEisKCHByb3ZpZGVyGAEgASgOMhkuY29tcGFzcy52MS5Gb3JnZVByb3ZpZGVyEgwKBGhvc3QYAiABKAkipAMKBUlzc3VlEgoKAmlkGAEgASgJEiMKBWZvcmdlGAIgASgLMhQuY29tcGFzcy52MS5Gb3JnZVJlZhIMCgRyZXBvGAMgASgJEg4KBm51bWJlchgEIAEoDRINCgV0aXRsZRgFIAEoCRIMCgRib2R5GAYgASgJEhMKC2ZvcmdlX3N0YXRlGAcgASgJEgsKA3VybBgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg4KBmxhYmVscxgLIAMoCRIlCgVzdGF0ZRgMIAEoDjIWLmNvbXBhc3MudjEuSXNzdWVTdGF0ZRIQCghwcmlvcml0eRgNIAEoCRIQCghhc3NpZ25lZRgOIAEoCRIPCgdzdW1tYXJ5GA8gASgJEg4KBmJyYW5jaBgQIAEoCRIkCgNwcnMYESADKAsyFy5jb21wYXNzLnYxLlB1bGxSZXF1ZXN0EicKB3RyYWNrZXIYEiABKAsyFi5jb21wYXNzLnYxLlRyYWNrZXJSZWYingMKC1B1bGxSZXF1ZXN0EiMKBWZvcmdlGAEgASgLMhQuY29tcGFzcy52MS5Gb3JnZVJlZhIMCgRyZXBvGAIgASgJEg4KBm51bWJlchgDIAEoDRINCgV0aXRsZRgEIAEoCRITCgtmb3JnZV9zdGF0ZRgFIAEoCRILCgN1cmwYBiABKAkSEAoIaGVhZF9yZWYYByABKAkSEAoIYmFzZV9yZWYYCCABKAkSKwoFYWdlbnQYCSABKAsyHC5jb21wYXNzLnYxLkFnZW50QXR0cmlidXRpb24SFQoNZm9yZ2VfYWNjb3VudBgKIAEoCRINCgVkcmFmdBgLIAEoCBIpCgdjaGFuZ2VkGAwgASgLMhguY29tcGFzcy52MS5DaGFuZ2VkU3RhdHMSKQoGY2hlY2tzGA0gASgLMhkuY29tcGFzcy52MS5DaGVja3NTdW1tYXJ5EiMKB3Jldmlld3MYDiADKAsyEi5jb21wYXNzLnYxLlJldmlldxIpCgd0aHJlYWRzGA8gAygLMhguY29tcGFzcy52MS5SZXZpZXdUaHJlYWQiUwoNQ2hlY2tzU3VtbWFyeRIQCghoZWFkX3NoYRgBIAEoCRINCgVzdGF0ZRgCIAEoCRIhCgZjaGVja3MYAyADKAsyES5jb21wYXNzLnYxLkNoZWNrIkMKBUNoZWNrEgwKBG5hbWUYASABKAkSDQoFc3RhdGUYAiABKAkSCwoDdXJsGAMgASgJEhAKCHJlcXVpcmVkGAQgASgIIkMKDENoYW5nZWRTdGF0cxINCgVmaWxlcxgBIAEoDRIRCglhZGRpdGlvbnMYAiABKA0SEQoJZGVsZXRpb25zGAMgASgNIkMKClRyYWNrZXJSZWYSDAoEa2luZBgBIAEoCRIKCgJpZBgCIAEoCRIOCgZzdGF0dXMYAyABKAkSCwoDdXJsGAQgASgJIkcKBlJldmlldxIOCgZhdXRob3IYASABKAkSDgoGaXNfYm90GAIgASgIEg8KB3ZlcmRpY3QYAyABKAkSDAoEYm9keRgEIAEoCSJVCgxSZXZpZXdUaHJlYWQSDAoEcGF0aBgBIAEoCRIQCghyZXNvbHZlZBgCIAEoCBIlCghjb21tZW50cxgDIAMoCzITLmNvbXBhc3MudjEuQ29tbWVudCI3CgdDb21tZW50Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDAoEYm9keRgDIAEoCSpkCg5TZWNyZXREZWxpdmVyeRIfChtTRUNSRVRfREVMSVZFUllfVU5TUEVDSUZJRUQQABIYChRTRUNSRVRfREVMSVZFUllfRklMRRABEhcKE1NFQ1JFVF9ERUxJVkVSWV9FTlYQAipwCgpTZWNyZXRLaW5kEhsKF1NFQ1JFVF9LSU5EX1VOU1BFQ0lGSUVEEAASFwoTU0VDUkVUX0tJTkRfR0VORVJJQxABEhgKFFNFQ1JFVF9LSU5EX1BST1ZJREVSEAISEgoOU0VDUkVUX0tJTkRfR0gQAypDCgtTZXJ2ZXJTdGF0ZRIcChhTRVJWRVJfU1RBVEVfVU5TUEVDSUZJRUQQABIWChJTRVJWRVJfU1RBVEVfUkVBRFkQASqCAgoRQWdlbnRTZXNzaW9uU3RhdGUSIwofQUdFTlRfU0VTU0lPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFHRU5UX1NFU1NJT05fU1RBVEVfU1RBUlRJTkcQARIdChlBR0VOVF9TRVNTSU9OX1NUQVRFX1JFQURZEAISHwobQUdFTlRfU0VTU0lPTl9TVEFURV9XT1JLSU5HEAMSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9TVE9QUEVEEAQSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9FUlJPUkVEEAUSJAogQUdFTlRfU0VTU0lPTl9TVEFURV9ESVNDT05ORUNURUQQBirSAQoTQWdlbnRUb29sQ2FsbFN0YXR1cxImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIgoeQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19QRU5ESU5HEAESJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19JTl9QUk9HUkVTUxACEiQKIEFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfQ09NUExFVEVEEAMSIQodQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19GQUlMRUQQBCq0AQoUQWdlbnRQbGFuRW50cnlTdGF0dXMSJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIjCh9BR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19QRU5ESU5HEAESJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfSU5fUFJPR1JFU1MQAhIlCiFBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19DT01QTEVURUQQAyqEAQoUU2Vzc2lvbkluamVjdGlvbktpbmQSJgoiU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9VTlNQRUNJRklFRBAAEiAKHFNFU1NJT05fSU5KRUNUSU9OX0tJTkRfU1RFRVIQARIiCh5TRVNTSU9OX0lOSkVDVElPTl9LSU5EX0RFTElWRVIQAirxAQoKSXNzdWVTdGF0ZRIbChdJU1NVRV9TVEFURV9VTlNQRUNJRklFRBAAEhcKE0lTU1VFX1NUQVRFX0JBQ0tMT0cQARIUChBJU1NVRV9TVEFURV9UT0RPEAISFgoSSVNTVUVfU1RBVEVfUVVFVUVEEAMSFwoTSVNTVUVfU1RBVEVfQkxPQ0tFRBAEEhsKF0lTU1VFX1NUQVRFX0lOX1BST0dSRVNTEAUSGQoVSVNTVUVfU1RBVEVfSU5fUkVWSUVXEAYSFAoQSVNTVUVfU1RBVEVfRE9ORRAHEhgKFElTU1VFX1NUQVRFX0FSQ0hJVkVEEAgqnAEKDUZvcmdlUHJvdmlkZXISHgoaRk9SR0VfUFJPVklERVJfVU5TUEVDSUZJRUQQABIZChVGT1JHRV9QUk9WSURFUl9HSVRIVUIQARIZChVGT1JHRV9QUk9WSURFUl9HSVRMQUIQAhIaChZGT1JHRV9QUk9WSURFUl9GT1JHRUpPEAMSGQoVRk9SR0VfUFJPVklERVJfTElORUFSEAQyrQwKDkNvbXBhc3NTZXJ2aWNlElQKDUdldFNlcnZlckluZm8SIC5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXF1ZXN0GiEuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVzcG9uc2USPwoGV2hvQW1JEhkuY29tcGFzcy52MS5XaG9BbUlSZXF1ZXN0GhouY29tcGFzcy52MS5XaG9BbUlSZXNwb25zZRJcCg9TdWJzY3JpYmVFdmVudHMSIi5jb21wYXNzLnYxLlN1YnNjcmliZUV2ZW50c1JlcXVlc3QaIy5jb21wYXNzLnYxLlN1YnNjcmliZUV2ZW50c1Jlc3BvbnNlMAESWgoPTGlzdEJvYXJkSXNzdWVzEiIuY29tcGFzcy52MS5MaXN0Qm9hcmRJc3N1ZXNSZXF1ZXN0GiMuY29tcGFzcy52MS5MaXN0Qm9hcmRJc3N1ZXNSZXNwb25zZRJyChdQcm92aXNpb25BZ2VudFdvcmtzcGFjZRIqLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GisuY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmAKEVN0YXJ0QWdlbnRTZXNzaW9uEiQuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlcXVlc3QaJS5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USSwoKU3Bhd25BZ2VudBIdLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlcXVlc3QaHi5jb21wYXNzLnYxLlNwYXduQWdlbnRSZXNwb25zZRJdChBTdG9wQWdlbnRTZXNzaW9uEiMuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBokLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlEmkKFFJlbW92ZUFnZW50V29ya3NwYWNlEicuY29tcGFzcy52MS5SZW1vdmVBZ2VudFdvcmtzcGFjZVJlcXVlc3QaKC5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVzcG9uc2USYwoSUmVsb2FkQWdlbnRTZXNzaW9uEiUuY29tcGFzcy52MS5SZWxvYWRBZ2VudFNlc3Npb25SZXF1ZXN0GiYuY29tcGFzcy52MS5SZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRJXCg5HZXRBZ2VudFN0YXR1cxIhLmNvbXBhc3MudjEuR2V0QWdlbnRTdGF0dXNSZXF1ZXN0GiIuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1Jlc3BvbnNlEmIKFVN1YnNjcmliZUFnZW50U2Vzc2lvbhIoLmNvbXBhc3MudjEuU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBodLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uRnJhbWUwARJLCgpJc3N1ZVRva2VuEh0uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVxdWVzdBoeLmNvbXBhc3MudjEuSXNzdWVUb2tlblJlc3BvbnNlEk4KC1Jldm9rZVRva2VuEh4uY29tcGFzcy52MS5SZXZva2VUb2tlblJlcXVlc3QaHy5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVzcG9uc2USVwoOUHV0QWdlbnRDb25maWcSIS5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVxdWVzdBoiLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXNwb25zZRJjChJHZXRBZ2VudENvbmZpZ0luZm8SJS5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3QaJi5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEmAKEURlbGV0ZUFnZW50Q29uZmlnEiQuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QaJS5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2Uy/QEKDlNlY3JldHNTZXJ2aWNlEkgKCVNldFNlY3JldBIcLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVxdWVzdBodLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVzcG9uc2USTgoLTGlzdFNlY3JldHMSHi5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVxdWVzdBofLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXNwb25zZRJRCgxEZWxldGVTZWNyZXQSHy5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlcXVlc3QaIC5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlc3BvbnNlYgZwcm90bzM"); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkInAKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJIkkKEUFnZW50TWVzc2FnZUNodW5rEhIKCnNlc3Npb25faWQYASABKAkSDAoEdGV4dBgCIAEoCRISCgppc190aG91Z2h0GAMgASgIInkKDUFnZW50VG9vbENhbGwSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAkSDQoFdGl0bGUYAyABKAkSLwoGc3RhdHVzGAQgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIkwKCUFnZW50UGxhbhISCgpzZXNzaW9uX2lkGAEgASgJEisKB2VudHJpZXMYAiADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IlMKDkFnZW50UGxhbkVudHJ5Eg8KB2NvbnRlbnQYASABKAkSMAoGc3RhdHVzGAIgASgOMiAuY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeVN0YXR1cyKsAwoMU2Vzc2lvbkV2ZW50EhAKCGV2ZW50X2lkGAEgASgJEhIKCmF0X3VuaXhfbXMYAiABKAMSOgoOYXNzaXN0YW50X3RleHQYAyABKAsyIC5jb21wYXNzLnYxLlNlc3Npb25Bc3Npc3RhbnRUZXh0SAASLwoIdGhpbmtpbmcYBCABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25UaGlua2luZ0gAEjAKCXRvb2xfY2FsbBgFIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsSAASPQoQdG9vbF9jYWxsX3VwZGF0ZRgGIAEoCzIhLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsVXBkYXRlSAASJwoEcGxhbhgHIAEoCzIXLmNvbXBhc3MudjEuU2Vzc2lvblBsYW5IABIrCgZub3RpY2UYCCABKAsyGS5jb21wYXNzLnYxLlNlc3Npb25Ob3RpY2VIABI5ChFzZXNzaW9uX2luamVjdGlvbhgJIAEoCzIcLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbkgAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIm4KEFNlc3Npb25JbmplY3Rpb24SMQoHb3Bfa2luZBgBIAEoDjIgLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbktpbmQSEgoKbWVzc2FnZV9pZBgCIAEoCRITCgtmcm9tX2hhbmRsZRgDIAEoCSIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkifgoRQWdlbnRTZXNzaW9uRnJhbWUSEgoKc2Vzc2lvbl9pZBgBIAEoCRInCgVldmVudBgCIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkV2ZW50EiwKBXN0YXRlGAMgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZSJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLwoZUmVsb2FkQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIjAKGlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkiKwoVR2V0QWdlbnRTdGF0dXNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiSgoWR2V0QWdlbnRTdGF0dXNSZXNwb25zZRIwCghzdGF0dXNlcxgBIAMoCzIeLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdHVzIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3QiyQEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgiGgoYRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0IhsKGURlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2UiKAoQQWdlbnRBdHRyaWJ1dGlvbhIUCgxhZ2VudF9oYW5kbGUYASABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSKkAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiKeAwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZCJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iQwoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkiRwoGUmV2aWV3Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDwoHdmVyZGljdBgDIAEoCRIMCgRib2R5GAQgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKkMKC1NlcnZlclN0YXRlEhwKGFNFUlZFUl9TVEFURV9VTlNQRUNJRklFRBAAEhYKElNFUlZFUl9TVEFURV9SRUFEWRABKoICChFBZ2VudFNlc3Npb25TdGF0ZRIjCh9BR0VOVF9TRVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAASIAocQUdFTlRfU0VTU0lPTl9TVEFURV9TVEFSVElORxABEh0KGUFHRU5UX1NFU1NJT05fU1RBVEVfUkVBRFkQAhIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1dPUktJTkcQAxIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1NUT1BQRUQQBBIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX0VSUk9SRUQQBRIkCiBBR0VOVF9TRVNTSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBAGKtIBChNBZ2VudFRvb2xDYWxsU3RhdHVzEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIiCh5BR0VOVF9UT09MX0NBTExfU1RBVFVTX1BFTkRJTkcQARImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX0lOX1BST0dSRVNTEAISJAogQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19DT01QTEVURUQQAxIhCh1BR0VOVF9UT09MX0NBTExfU1RBVFVTX0ZBSUxFRBAEKrQBChRBZ2VudFBsYW5FbnRyeVN0YXR1cxInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19VTlNQRUNJRklFRBAAEiMKH0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1BFTkRJTkcQARInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19JTl9QUk9HUkVTUxACEiUKIUFHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0NPTVBMRVRFRBADKoQBChRTZXNzaW9uSW5qZWN0aW9uS2luZBImCiJTRVNTSU9OX0lOSkVDVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASIAocU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9TVEVFUhABEiIKHlNFU1NJT05fSU5KRUNUSU9OX0tJTkRfREVMSVZFUhACKvEBCgpJc3N1ZVN0YXRlEhsKF0lTU1VFX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSVNTVUVfU1RBVEVfQkFDS0xPRxABEhQKEElTU1VFX1NUQVRFX1RPRE8QAhIWChJJU1NVRV9TVEFURV9RVUVVRUQQAxIXChNJU1NVRV9TVEFURV9CTE9DS0VEEAQSGwoXSVNTVUVfU1RBVEVfSU5fUFJPR1JFU1MQBRIZChVJU1NVRV9TVEFURV9JTl9SRVZJRVcQBhIUChBJU1NVRV9TVEFURV9ET05FEAcSGAoUSVNTVUVfU1RBVEVfQVJDSElWRUQQCCqcAQoNRm9yZ2VQcm92aWRlchIeChpGT1JHRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX1BST1ZJREVSX0dJVEhVQhABEhkKFUZPUkdFX1BST1ZJREVSX0dJVExBQhACEhoKFkZPUkdFX1BST1ZJREVSX0ZPUkdFSk8QAxIZChVGT1JHRV9QUk9WSURFUl9MSU5FQVIQBDKtDAoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlElwKD1N1YnNjcmliZUV2ZW50cxIiLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBojLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2UwARJaCg9MaXN0Qm9hcmRJc3N1ZXMSIi5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QaIy5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJLCgpTcGF3bkFnZW50Eh0uY29tcGFzcy52MS5TcGF3bkFnZW50UmVxdWVzdBoeLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USTgoLUmV2b2tlVG9rZW4SHi5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVxdWVzdBofLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXNwb25zZRJXCg5QdXRBZ2VudENvbmZpZxIhLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXF1ZXN0GiIuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEmMKEkdldEFnZW50Q29uZmlnSW5mbxIlLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdBomLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USYAoRRGVsZXRlQWdlbnRDb25maWcSJC5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdBolLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZTL9AQoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2ViBnByb3RvMw"); /** * @generated from message compass.v1.SetSecretRequest @@ -1045,13 +1045,13 @@ export const AgentSessionFrameSchema: GenMessage = /*@__PURE_ */ export type ProvisionAgentWorkspaceRequest = Message<"compass.v1.ProvisionAgentWorkspaceRequest"> & { /** - * The owned agent account this workstream belongs to (the AgentAccount id - * from CommsService). Names whose credentials + home channel the container - * is provisioned for. + * The owned agent account this workstream belongs to. A `@handle` + * (owner-qualified, e.g. `matt/compass-ux`); the server resolves it to an + * account id; unknown → NOT_FOUND. * - * @generated from field: string agent_account_id = 1; + * @generated from field: string agent_handle = 1; */ - agentAccountId: string; + agentHandle: string; /** * Repo carriage removed (SEA-1527, Matt 2026-07-29): spawn/provision no longer @@ -1235,13 +1235,13 @@ export const StartAgentSessionResponseSchema: GenMessage & { /** - * The owned agent account to bring online (the AgentAccount id from - * CommsService). The container is provisioned for, and the session bound to, - * this account. + * The owned agent account to bring online. A `@handle` (owner-qualified, + * e.g. `matt/compass-ux`); the server resolves it to an account id; unknown + * → NOT_FOUND. * - * @generated from field: string agent_account_id = 1; + * @generated from field: string agent_handle = 1; */ - agentAccountId: string; + agentHandle: string; /** * End-to-end idempotency key. A retry with the same id returns the same @@ -1403,13 +1403,13 @@ export const GetAgentStatusResponseSchema: GenMessage = */ export type IssueTokenRequest = Message<"compass.v1.IssueTokenRequest"> & { /** - * The account to mint a token for — the id of an Account created via - * CommsService. The caller's identity is the authenticated connection, never - * a field here. + * The account to mint a token for — a `@handle` naming a user or agent; the + * server resolves it to an account id; unknown → NOT_FOUND. The caller's + * identity is the authenticated connection, never a field here. * - * @generated from field: string account_id = 1; + * @generated from field: string account_handle = 1; */ - accountId: string; + accountHandle: string; }; /** diff --git a/packages/compass-agent/src/lifecycle.test.ts b/packages/compass-agent/src/lifecycle.test.ts index b891b51c..05d29914 100644 --- a/packages/compass-agent/src/lifecycle.test.ts +++ b/packages/compass-agent/src/lifecycle.test.ts @@ -326,11 +326,11 @@ describe("agents_spawn_peer", () => { }); describe("agents_despawn_peer", () => { - test("maps agent_account_id and sends NO clientRequestId", async () => { + test("maps agent_handle and sends NO clientRequestId", async () => { const transport = new FakeTransport(despawnResult()); const t = tool(new LifecycleBroker(transport), "agents_despawn_peer"); - await exec(t, "tc-7", { agent_account_id: "acct-3" }); + await exec(t, "tc-7", { agent_handle: "acct-3" }); expect(transport.requests).toHaveLength(1); const req = transport.requests[0]; @@ -338,7 +338,7 @@ describe("agents_despawn_peer", () => { expect(req.call.case).toBe("despawn"); if (req.call.case !== "despawn") throw new Error("expected despawn case"); const despawn = req.call.value; - expect(despawn.agentAccountId).toBe("acct-3"); + expect(despawn.agentHandle).toBe("acct-3"); // The despawn message carries no dedup field at all — assert nothing named // clientRequestId leaked onto it. expect("clientRequestId" in despawn).toBe(false); @@ -348,20 +348,20 @@ describe("agents_despawn_peer", () => { const transport = new FakeTransport(despawnResult()); const t = tool(new LifecycleBroker(transport), "agents_despawn_peer"); - const result = await exec(t, "tc-1", { agent_account_id: "acct-3" }); + const result = await exec(t, "tc-1", { agent_handle: "acct-3" }); expect(textOf(result)).toBe("Despawned peer acct-3."); }); - // `agent_account_id` is caller-supplied but renders into authoritative tool + // `agent_handle` is caller-supplied but renders into authoritative tool // output, so it is guarded as a server value would be. This is the last // uncovered `attr` site. - test("a malformed agent_account_id degrades rather than forging output", async () => { + test("a malformed agent_handle degrades rather than forging output", async () => { const transport = new FakeTransport(despawnResult()); const t = tool(new LifecycleBroker(transport), "agents_despawn_peer"); const result = await exec(t, "tc-1", { - agent_account_id: 'acct"3\ninjected', + agent_handle: 'acct"3\ninjected', }); expect(textOf(result)).toBe("Despawned peer (malformed)."); @@ -373,18 +373,16 @@ describe("agents_despawn_peer", () => { ); const t = tool(new LifecycleBroker(transport), "agents_despawn_peer"); - await expect( - exec(t, "tc-1", { agent_account_id: "acct-3" }), - ).rejects.toThrow("agents_despawn_peer failed: not_found: other owner"); + await expect(exec(t, "tc-1", { agent_handle: "acct-3" })).rejects.toThrow( + "agents_despawn_peer failed: not_found: other owner", + ); }); test("a wrong result case throws a protocol violation", async () => { const transport = new FakeTransport(spawnResult("a", "c", "s")); const t = tool(new LifecycleBroker(transport), "agents_despawn_peer"); - await expect( - exec(t, "tc-1", { agent_account_id: "acct-3" }), - ).rejects.toThrow( + await expect(exec(t, "tc-1", { agent_handle: "acct-3" })).rejects.toThrow( "agents_despawn_peer: protocol violation — expected a despawn result, got spawn", ); }); @@ -401,12 +399,10 @@ describe("lifecycle parameter schemas", () => { expect(rejects(spawnParameters, { handle: "worker-a" })).toBe(false); }); - test("despawn rejects an empty or whitespace-only agent_account_id", () => { + test("despawn rejects an empty or whitespace-only agent_handle", () => { expect(rejects(despawnParameters, {})).toBe(true); - expect(rejects(despawnParameters, { agent_account_id: "" })).toBe(true); - expect(rejects(despawnParameters, { agent_account_id: " " })).toBe(true); - expect(rejects(despawnParameters, { agent_account_id: "acct-3" })).toBe( - false, - ); + expect(rejects(despawnParameters, { agent_handle: "" })).toBe(true); + expect(rejects(despawnParameters, { agent_handle: " " })).toBe(true); + expect(rejects(despawnParameters, { agent_handle: "acct-3" })).toBe(false); }); }); diff --git a/packages/compass-agent/src/lifecycle.ts b/packages/compass-agent/src/lifecycle.ts index fb672ba8..7ad3dd64 100644 --- a/packages/compass-agent/src/lifecycle.ts +++ b/packages/compass-agent/src/lifecycle.ts @@ -89,9 +89,9 @@ export const spawnParameters = type({ /** Exported so a test can validate the wire contract the agent loop enforces. */ export const despawnParameters = type({ - agent_account_id: type("string") + agent_handle: type("string") .narrow((s, ctx) => s.trim().length > 0 || ctx.mustBe("non-blank")) - .describe("The peer's agent account id to tear down; must not be blank"), + .describe("The peer's agent handle to tear down; must not be blank"), }); /** @@ -183,7 +183,7 @@ export function createLifecycleTools(broker: LifecycleBroker): AgentTool[] { label: "Despawn peer agent", approval: "write", description: - "Tear down a peer agent your owner owns, by its agent account id. " + + "Tear down a peer agent your owner owns, by its agent handle. " + "Idempotent: despawning an already-absent peer succeeds.", parameters: despawnParameters, execute: async (toolCallId, params) => { @@ -196,20 +196,20 @@ export function createLifecycleTools(broker: LifecycleBroker): AgentTool[] { // (removing an absent peer succeeds), so the message carries no // dedup field. value: create(DespawnPeerRequestSchema, { - agentAccountId: params.agent_account_id, + agentHandle: params.agent_handle, }), }, }), ); if (result.result.case !== "despawn") throw lifecycleFailure(result, "agents_despawn_peer", "despawn"); - // `agent_account_id` is caller-supplied; guard it as a server value + // `agent_handle` is caller-supplied; guard it as a server value // would be, since it renders into authoritative tool output. return { content: [ { type: "text", - text: `Despawned peer ${attr(params.agent_account_id)}.`, + text: `Despawned peer ${attr(params.agent_handle)}.`, }, ], }; diff --git a/packages/compass-client/src/gen/compass/v1/comms_pb.ts b/packages/compass-client/src/gen/compass/v1/comms_pb.ts index 0b57c770..d2c393c4 100644 --- a/packages/compass-client/src/gen/compass/v1/comms_pb.ts +++ b/packages/compass-client/src/gen/compass/v1/comms_pb.ts @@ -27,7 +27,7 @@ import type { Message as Message$1 } from "@bufbuild/protobuf"; * Describes the file compass/v1/comms.proto. */ export const file_compass_v1_comms: GenFile = /*@__PURE__*/ - fileDesc("ChZjb21wYXNzL3YxL2NvbW1zLnByb3RvEgpjb21wYXNzLnYxIsQBCgdBY2NvdW50EgoKAmlkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSJwoEdXNlchgKIAEoCzIXLmNvbXBhc3MudjEuVXNlckFjY291bnRIABIpCgVhZ2VudBgLIAEoCzIYLmNvbXBhc3MudjEuQWdlbnRBY2NvdW50SAASKwoGc3lzdGVtGAwgASgLMhkuY29tcGFzcy52MS5TeXN0ZW1BY2NvdW50SABCBgoEa2luZCIPCg1TeXN0ZW1BY2NvdW50IjEKC1VzZXJBY2NvdW50EiIKBHJvbGUYASABKA4yFC5jb21wYXNzLnYxLlVzZXJSb2xlIlcKDEFnZW50QWNjb3VudBIVCg1vd25lcl91c2VyX2lkGAEgASgJEhcKD2hvbWVfY2hhbm5lbF9pZBgCIAEoCRIXCg9wYXJlbnRfYWdlbnRfaWQYAyABKAkikAEKDENoYW5uZWxHcm91cBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhcKD3BhcmVudF9ncm91cF9pZBgDIAEoCRIVCg1vd25lcl91c2VyX2lkGAQgASgJEjYKCnZpc2liaWxpdHkYBSABKA4yIi5jb21wYXNzLnYxLkNoYW5uZWxHcm91cFZpc2liaWxpdHkitwIKB0NoYW5uZWwSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghncm91cF9pZBgDIAEoCRIlCgRraW5kGAQgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIaChJtZW1iZXJfYWNjb3VudF9pZHMYBSADKAkSHgoWc3Vic2NyaWJlcl9hY2NvdW50X2lkcxgGIAMoCRIyCgtwb3N0X3BvbGljeRgHIAEoDjIdLmNvbXBhc3MudjEuQ2hhbm5lbFBvc3RQb2xpY3kSGAoQb3duZXJfYWNjb3VudF9pZBgIIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAkgASgIEi8KDnBpbm5lZF9lbnRyaWVzGAogAygLMhcuY29tcGFzcy52MS5QaW5uZWRFbnRyeSJsCgtQaW5uZWRFbnRyeRISCgptZXNzYWdlX2lkGAEgASgJEhAKCHBvc2l0aW9uGAIgASgFEhkKEXBpbm5lZF9hdF91bml4X21zGAMgASgDEhwKFHBpbm5lZF9ieV9hY2NvdW50X2lkGAQgASgJIjYKDkFnZW50V29ya3NwYWNlEgoKAmlkGAEgASgJEhgKEGFnZW50X2FjY291bnRfaWQYAiABKAkiggEKBVRvcGljEgoKAmlkGAEgASgJEhIKCmNoYW5uZWxfaWQYAiABKAkSDAoEbmFtZRgDIAEoCRIaChJjcmVhdGVkX2F0X3VuaXhfbXMYBCABKAMSHQoVY3JlYXRlZF9ieV9hY2NvdW50X2lkGAUgASgJEhAKCGFyY2hpdmVkGAYgASgIIoABCgdNZXNzYWdlEgoKAmlkGAEgASgJEhAKCHRvcGljX2lkGAIgASgJEhkKEWF1dGhvcl9hY2NvdW50X2lkGAMgASgJEhIKCmF0X3VuaXhfbXMYBCABKAMSKAoGYmxvY2tzGAUgAygLMhguY29tcGFzcy52MS5NZXNzYWdlQmxvY2sieQoMTWVzc2FnZUJsb2NrEg4KBHRleHQYASABKAlIABIeCgNhc2sYAiABKAsyDy5jb21wYXNzLnYxLkFza0gAEjAKCmFza19hbnN3ZXIYAyABKAsyGi5jb21wYXNzLnYxLkFza0Fuc3dlckJsb2NrSABCBwoFYmxvY2siSAoOQXNrQW5zd2VyQmxvY2sSHAoDYXNrGAEgASgLMg8uY29tcGFzcy52MS5Bc2sSGAoQYXNrZXJfYWNjb3VudF9pZBgCIAEoCSJTCgNBc2sSDgoGYXNrX2lkGAEgASgJEioKCXF1ZXN0aW9ucxgCIAMoCzIXLmNvbXBhc3MudjEuQXNrUXVlc3Rpb24SEAoIYW5zd2VyZWQYAyABKAgi8QEKC0Fza1F1ZXN0aW9uEhMKC3F1ZXN0aW9uX2lkGAEgASgJEhAKCHF1ZXN0aW9uGAIgASgJEg4KBmhlYWRlchgDIAEoCRImCgdvcHRpb25zGAQgAygLMhUuY29tcGFzcy52MS5Bc2tPcHRpb24SFgoOYWxsb3dfbXVsdGlwbGUYBSABKAgSGAoLcmVjb21tZW5kZWQYBiABKAVIAIgBARIZChFjaG9zZW5fb3B0aW9uX2lkcxgHIAMoCRITCgtjdXN0b21fdGV4dBgIIAEoCRIRCgl0aW1lZF9vdXQYCSABKAhCDgoMX3JlY29tbWVuZGVkIkwKCUFza09wdGlvbhIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIPCgdwcmV2aWV3GAQgASgJIokFChZTdWJzY3JpYmVDb21tc1Jlc3BvbnNlEgsKA3NlcRgBIAEoBBISCgphdF91bml4X21zGAIgASgDEhYKDmluc3RhbmNlX2Vwb2NoGAMgASgEEhQKDHNuYXBzaG90X3NlcRgEIAEoBBIzCg5tZXNzYWdlX3Bvc3RlZBgKIAEoCzIZLmNvbXBhc3MudjEuTWVzc2FnZVBvc3RlZEgAEjUKD21lc3NhZ2VfdXBkYXRlZBgLIAEoCzIaLmNvbXBhc3MudjEuTWVzc2FnZVVwZGF0ZWRIABI1Cg9jaGFubmVsX2NoYW5nZWQYDCABKAsyGi5jb21wYXNzLnYxLkNoYW5uZWxDaGFuZ2VkSAASQAoVY2hhbm5lbF9ncm91cF9jaGFuZ2VkGA0gASgLMh8uY29tcGFzcy52MS5DaGFubmVsR3JvdXBDaGFuZ2VkSAASNQoPYWNjb3VudF9jaGFuZ2VkGA4gASgLMhouY29tcGFzcy52MS5BY2NvdW50Q2hhbmdlZEgAEkQKF2FnZW50X3dvcmtzcGFjZV9jaGFuZ2VkGA8gASgLMiEuY29tcGFzcy52MS5BZ2VudFdvcmtzcGFjZUNoYW5nZWRIABI6Cg9yZXN5bmNfcmVxdWlyZWQYECABKAsyHy5jb21wYXNzLnYxLkNvbW1zUmVzeW5jUmVxdWlyZWRIABJCChZhZ2VudF9wcmVzZW5jZV9jaGFuZ2VkGBEgASgLMiAuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlQ2hhbmdlZEgAEjMKDnRvcGljX3Vwc2VydGVkGBIgASgLMhkuY29tcGFzcy52MS5Ub3BpY1Vwc2VydGVkSABCCQoHcGF5bG9hZCI1Cg1NZXNzYWdlUG9zdGVkEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiNgoOTWVzc2FnZVVwZGF0ZWQSJAoHbWVzc2FnZRgBIAEoCzITLmNvbXBhc3MudjEuTWVzc2FnZSIxCg1Ub3BpY1Vwc2VydGVkEiAKBXRvcGljGAEgASgLMhEuY29tcGFzcy52MS5Ub3BpYyJTCg5DaGFubmVsQ2hhbmdlZBIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsEhsKE3JlbW92ZWRfYWNjb3VudF9pZHMYAiADKAkiPgoTQ2hhbm5lbEdyb3VwQ2hhbmdlZBInCgVncm91cBgBIAEoCzIYLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwIjYKDkFjY291bnRDaGFuZ2VkEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiRgoVQWdlbnRXb3Jrc3BhY2VDaGFuZ2VkEi0KCXdvcmtzcGFjZRgBIAEoCzIaLmNvbXBhc3MudjEuQWdlbnRXb3Jrc3BhY2UiFQoTQ29tbXNSZXN5bmNSZXF1aXJlZCJvChRBZ2VudFByZXNlbmNlQ2hhbmdlZBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEisKCHByZXNlbmNlGAIgASgOMhkuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlEhAKCGFjdGl2aXR5GAMgASgJIjkKEUNyZWF0ZVVzZXJSZXF1ZXN0Eg4KBmhhbmRsZRgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiOgoSQ3JlYXRlVXNlclJlc3BvbnNlEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiUwoSQ3JlYXRlQWdlbnRSZXF1ZXN0Eg4KBmhhbmRsZRgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSFwoPcGFyZW50X2FnZW50X2lkGAMgASgJIjsKE0NyZWF0ZUFnZW50UmVzcG9uc2USJAoHYWNjb3VudBgBIAEoCzITLmNvbXBhc3MudjEuQWNjb3VudCIrChNMaXN0QWNjb3VudHNSZXF1ZXN0EhQKDHNuYXBzaG90X3NlcRgBIAEoBCI9ChRMaXN0QWNjb3VudHNSZXNwb25zZRIlCghhY2NvdW50cxgBIAMoCzITLmNvbXBhc3MudjEuQWNjb3VudCJ6ChlDcmVhdGVDaGFubmVsR3JvdXBSZXF1ZXN0EgwKBG5hbWUYASABKAkSFwoPcGFyZW50X2dyb3VwX2lkGAIgASgJEjYKCnZpc2liaWxpdHkYAyABKA4yIi5jb21wYXNzLnYxLkNoYW5uZWxHcm91cFZpc2liaWxpdHkiRQoaQ3JlYXRlQ2hhbm5lbEdyb3VwUmVzcG9uc2USJwoFZ3JvdXAYASABKAsyGC5jb21wYXNzLnYxLkNoYW5uZWxHcm91cCIwChhMaXN0Q2hhbm5lbEdyb3Vwc1JlcXVlc3QSFAoMc25hcHNob3Rfc2VxGAEgASgEIkUKGUxpc3RDaGFubmVsR3JvdXBzUmVzcG9uc2USKAoGZ3JvdXBzGAEgAygLMhguY29tcGFzcy52MS5DaGFubmVsR3JvdXAiKwoTTGlzdENoYW5uZWxzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPQoUTGlzdENoYW5uZWxzUmVzcG9uc2USJQoIY2hhbm5lbHMYASADKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwieQoUQ3JlYXRlQ2hhbm5lbFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIQCghncm91cF9pZBgCIAEoCRIlCgRraW5kGAMgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIaChJtZW1iZXJfYWNjb3VudF9pZHMYBCADKAkiPQoVQ3JlYXRlQ2hhbm5lbFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwitAEKG1VwZGF0ZUNoYW5uZWxNZW1iZXJzUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEh4KFmFkZF9tZW1iZXJfYWNjb3VudF9pZHMYAiADKAkSIQoZcmVtb3ZlX21lbWJlcl9hY2NvdW50X2lkcxgDIAMoCRIdChVzdWJzY3JpYmVfYWNjb3VudF9pZHMYBCADKAkSHwoXdW5zdWJzY3JpYmVfYWNjb3VudF9pZHMYBSADKAkiRAocVXBkYXRlQ2hhbm5lbE1lbWJlcnNSZXNwb25zZRIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsIk0KFFJlcGFyZW50QWdlbnRSZXF1ZXN0EhgKEGFnZW50X2FjY291bnRfaWQYASABKAkSGwoTbmV3X3BhcmVudF9hZ2VudF9pZBgCIAEoCSI9ChVSZXBhcmVudEFnZW50UmVzcG9uc2USJAoHYWNjb3VudBgBIAEoCzITLmNvbXBhc3MudjEuQWNjb3VudCKbAQoXU2V0Q2hhbm5lbFBvbGljeVJlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIyCgtwb3N0X3BvbGljeRgCIAEoDjIdLmNvbXBhc3MudjEuQ2hhbm5lbFBvc3RQb2xpY3kSGAoQb3duZXJfYWNjb3VudF9pZBgDIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAQgASgIIkAKGFNldENoYW5uZWxQb2xpY3lSZXNwb25zZRIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsIlQKEEdldFJvc3RlclJlcXVlc3QSJgoFc2NvcGUYASABKA4yFy5jb21wYXNzLnYxLlJvc3RlclNjb3BlEhgKEGFnZW50X2FjY291bnRfaWQYAiABKAkiPQoRR2V0Um9zdGVyUmVzcG9uc2USKAoHZW50cmllcxgBIAMoCzIXLmNvbXBhc3MudjEuUm9zdGVyRW50cnkiwgEKC1Jvc3RlckVudHJ5EhgKEGFnZW50X2FjY291bnRfaWQYASABKAkSDgoGaGFuZGxlGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg9wYXJlbnRfYWdlbnRfaWQYBCABKAkSKwoIcHJlc2VuY2UYBSABKA4yGS5jb21wYXNzLnYxLkFnZW50UHJlc2VuY2USEAoIYWN0aXZpdHkYBiABKAkSGwoTYWN0aXZpdHlfYXRfdW5peF9tcxgHIAEoAyJ3ChhVcGRhdGVQaW5uZWRCb2FyZFJlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIlCgNwaW4YAiABKAsyFi5jb21wYXNzLnYxLlBpbk1lc3NhZ2VIABIaChB1bnBpbl9tZXNzYWdlX2lkGAMgASgJSABCBAoCb3AiPAoKUGluTWVzc2FnZRISCgptZXNzYWdlX2lkGAEgASgJEhoKEnJlcGxhY2VfbWVzc2FnZV9pZBgCIAEoCSJBChlVcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwiNQoZT3BlbkFnZW50V29ya3NwYWNlUmVxdWVzdBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJIksKGk9wZW5BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEi0KCXdvcmtzcGFjZRgBIAEoCzIaLmNvbXBhc3MudjEuQWdlbnRXb3Jrc3BhY2UiigEKE0xpc3RNZXNzYWdlc1JlcXVlc3QSFAoKY2hhbm5lbF9pZBgBIAEoCUgAEg0KBWxpbWl0GAIgASgNEhkKEWJlZm9yZV9tZXNzYWdlX2lkGAMgASgJEhQKDHNuYXBzaG90X3NlcRgEIAEoBBIQCgh0b3BpY19pZBgFIAEoCUILCgljb250YWluZXIiPQoUTGlzdE1lc3NhZ2VzUmVzcG9uc2USJQoIbWVzc2FnZXMYASADKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UirwEKElBvc3RNZXNzYWdlUmVxdWVzdBIUCgpjaGFubmVsX2lkGAEgASgJSAASKAoGYmxvY2tzGAIgAygLMhguY29tcGFzcy52MS5NZXNzYWdlQmxvY2sSEgoIdG9waWNfaWQYAyABKAlIARIUCgp0b3BpY19uYW1lGAQgASgJSAESGQoRY2xpZW50X3JlcXVlc3RfaWQYBSABKAlCCwoJY29udGFpbmVyQgcKBXRvcGljIjsKE1Bvc3RNZXNzYWdlUmVzcG9uc2USJAoHbWVzc2FnZRgBIAEoCzITLmNvbXBhc3MudjEuTWVzc2FnZSJBChFMaXN0VG9waWNzUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYAiABKAgiNwoSTGlzdFRvcGljc1Jlc3BvbnNlEiEKBnRvcGljcxgBIAMoCzIRLmNvbXBhc3MudjEuVG9waWMiZgoSVXBkYXRlVG9waWNSZXF1ZXN0EhAKCHRvcGljX2lkGAEgASgJEhEKBG5hbWUYAiABKAlIAIgBARIVCghhcmNoaXZlZBgDIAEoCEgBiAEBQgcKBV9uYW1lQgsKCV9hcmNoaXZlZCI3ChNVcGRhdGVUb3BpY1Jlc3BvbnNlEiAKBXRvcGljGAEgASgLMhEuY29tcGFzcy52MS5Ub3BpYyJVChNSZXNwb25kVG9Bc2tSZXF1ZXN0Eg4KBmFza19pZBgBIAEoCRIuCgdhbnN3ZXJzGAIgAygLMh0uY29tcGFzcy52MS5Bc2tRdWVzdGlvbkFuc3dlciJYChFBc2tRdWVzdGlvbkFuc3dlchITCgtxdWVzdGlvbl9pZBgBIAEoCRIZChFjaG9zZW5fb3B0aW9uX2lkcxgCIAMoCRITCgtjdXN0b21fdGV4dBgDIAEoCSIWChRSZXNwb25kVG9Bc2tSZXNwb25zZSJqChVTZWFyY2hNZXNzYWdlc1JlcXVlc3QSDQoFcXVlcnkYASABKAkSFAoKY2hhbm5lbF9pZBgCIAEoCUgAEg0KBWxpbWl0GAMgASgNEhQKDHNuYXBzaG90X3NlcRgEIAEoBEIHCgVzY29wZSI/ChZTZWFyY2hNZXNzYWdlc1Jlc3BvbnNlEiUKCG1lc3NhZ2VzGAEgAygLMhMuY29tcGFzcy52MS5NZXNzYWdlIkIKFVN1YnNjcmliZUNvbW1zUmVxdWVzdBIRCglzaW5jZV9zZXEYASABKAQSFgoOaW5zdGFuY2VfZXBvY2gYAiABKAQqNQoIVXNlclJvbGUSFAoQVVNFUl9ST0xFX01FTUJFUhAAEhMKD1VTRVJfUk9MRV9BRE1JThABKmEKFkNoYW5uZWxHcm91cFZpc2liaWxpdHkSIgoeQ0hBTk5FTF9HUk9VUF9WSVNJQklMSVRZX09XTkVSEAASIwofQ0hBTk5FTF9HUk9VUF9WSVNJQklMSVRZX1NIQVJFRBABKlUKEUNoYW5uZWxQb3N0UG9saWN5EhwKGENIQU5ORUxfUE9TVF9QT0xJQ1lfT1BFThAAEiIKHkNIQU5ORUxfUE9TVF9QT0xJQ1lfT1dORVJfT05MWRABKlcKC0NoYW5uZWxLaW5kEhgKFENIQU5ORUxfS0lORF9DSEFOTkVMEAASEwoPQ0hBTk5FTF9LSU5EX0RNEAESGQoVQ0hBTk5FTF9LSU5EX0dST1VQX0RNEAIqnAEKDUFnZW50UHJlc2VuY2USHgoaQUdFTlRfUFJFU0VOQ0VfVU5TUEVDSUZJRUQQABIXChNBR0VOVF9QUkVTRU5DRV9JRExFEAESGgoWQUdFTlRfUFJFU0VOQ0VfV09SS0lORxACEhoKFkFHRU5UX1BSRVNFTkNFX1dBSVRJTkcQAxIaChZBR0VOVF9QUkVTRU5DRV9PRkZMSU5FEAQqXgoLUm9zdGVyU2NvcGUSHQoZUk9TVEVSX1NDT1BFX05FSUdIQk9SSE9PRBAAEhgKFFJPU1RFUl9TQ09QRV9TVUJUUkVFEAESFgoSUk9TVEVSX1NDT1BFX09XTkVSEAIy5g0KDENvbW1zU2VydmljZRJLCgpDcmVhdGVVc2VyEh0uY29tcGFzcy52MS5DcmVhdGVVc2VyUmVxdWVzdBoeLmNvbXBhc3MudjEuQ3JlYXRlVXNlclJlc3BvbnNlEk4KC0NyZWF0ZUFnZW50Eh4uY29tcGFzcy52MS5DcmVhdGVBZ2VudFJlcXVlc3QaHy5jb21wYXNzLnYxLkNyZWF0ZUFnZW50UmVzcG9uc2USUQoMTGlzdEFjY291bnRzEh8uY29tcGFzcy52MS5MaXN0QWNjb3VudHNSZXF1ZXN0GiAuY29tcGFzcy52MS5MaXN0QWNjb3VudHNSZXNwb25zZRJjChJDcmVhdGVDaGFubmVsR3JvdXASJS5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxHcm91cFJlcXVlc3QaJi5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxHcm91cFJlc3BvbnNlEmAKEUxpc3RDaGFubmVsR3JvdXBzEiQuY29tcGFzcy52MS5MaXN0Q2hhbm5lbEdyb3Vwc1JlcXVlc3QaJS5jb21wYXNzLnYxLkxpc3RDaGFubmVsR3JvdXBzUmVzcG9uc2USUQoMTGlzdENoYW5uZWxzEh8uY29tcGFzcy52MS5MaXN0Q2hhbm5lbHNSZXF1ZXN0GiAuY29tcGFzcy52MS5MaXN0Q2hhbm5lbHNSZXNwb25zZRJUCg1DcmVhdGVDaGFubmVsEiAuY29tcGFzcy52MS5DcmVhdGVDaGFubmVsUmVxdWVzdBohLmNvbXBhc3MudjEuQ3JlYXRlQ2hhbm5lbFJlc3BvbnNlEmkKFFVwZGF0ZUNoYW5uZWxNZW1iZXJzEicuY29tcGFzcy52MS5VcGRhdGVDaGFubmVsTWVtYmVyc1JlcXVlc3QaKC5jb21wYXNzLnYxLlVwZGF0ZUNoYW5uZWxNZW1iZXJzUmVzcG9uc2USVAoNUmVwYXJlbnRBZ2VudBIgLmNvbXBhc3MudjEuUmVwYXJlbnRBZ2VudFJlcXVlc3QaIS5jb21wYXNzLnYxLlJlcGFyZW50QWdlbnRSZXNwb25zZRJjChJPcGVuQWdlbnRXb3Jrc3BhY2USJS5jb21wYXNzLnYxLk9wZW5BZ2VudFdvcmtzcGFjZVJlcXVlc3QaJi5jb21wYXNzLnYxLk9wZW5BZ2VudFdvcmtzcGFjZVJlc3BvbnNlElEKDExpc3RNZXNzYWdlcxIfLmNvbXBhc3MudjEuTGlzdE1lc3NhZ2VzUmVxdWVzdBogLmNvbXBhc3MudjEuTGlzdE1lc3NhZ2VzUmVzcG9uc2USTgoLUG9zdE1lc3NhZ2USHi5jb21wYXNzLnYxLlBvc3RNZXNzYWdlUmVxdWVzdBofLmNvbXBhc3MudjEuUG9zdE1lc3NhZ2VSZXNwb25zZRJLCgpMaXN0VG9waWNzEh0uY29tcGFzcy52MS5MaXN0VG9waWNzUmVxdWVzdBoeLmNvbXBhc3MudjEuTGlzdFRvcGljc1Jlc3BvbnNlEk4KC1VwZGF0ZVRvcGljEh4uY29tcGFzcy52MS5VcGRhdGVUb3BpY1JlcXVlc3QaHy5jb21wYXNzLnYxLlVwZGF0ZVRvcGljUmVzcG9uc2USUQoMUmVzcG9uZFRvQXNrEh8uY29tcGFzcy52MS5SZXNwb25kVG9Bc2tSZXF1ZXN0GiAuY29tcGFzcy52MS5SZXNwb25kVG9Bc2tSZXNwb25zZRJXCg5TZWFyY2hNZXNzYWdlcxIhLmNvbXBhc3MudjEuU2VhcmNoTWVzc2FnZXNSZXF1ZXN0GiIuY29tcGFzcy52MS5TZWFyY2hNZXNzYWdlc1Jlc3BvbnNlEl0KEFNldENoYW5uZWxQb2xpY3kSIy5jb21wYXNzLnYxLlNldENoYW5uZWxQb2xpY3lSZXF1ZXN0GiQuY29tcGFzcy52MS5TZXRDaGFubmVsUG9saWN5UmVzcG9uc2USSAoJR2V0Um9zdGVyEhwuY29tcGFzcy52MS5HZXRSb3N0ZXJSZXF1ZXN0Gh0uY29tcGFzcy52MS5HZXRSb3N0ZXJSZXNwb25zZRJgChFVcGRhdGVQaW5uZWRCb2FyZBIkLmNvbXBhc3MudjEuVXBkYXRlUGlubmVkQm9hcmRSZXF1ZXN0GiUuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlElkKDlN1YnNjcmliZUNvbW1zEiEuY29tcGFzcy52MS5TdWJzY3JpYmVDb21tc1JlcXVlc3QaIi5jb21wYXNzLnYxLlN1YnNjcmliZUNvbW1zUmVzcG9uc2UwAWIGcHJvdG8z"); + fileDesc("ChZjb21wYXNzL3YxL2NvbW1zLnByb3RvEgpjb21wYXNzLnYxIsQBCgdBY2NvdW50EgoKAmlkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSJwoEdXNlchgKIAEoCzIXLmNvbXBhc3MudjEuVXNlckFjY291bnRIABIpCgVhZ2VudBgLIAEoCzIYLmNvbXBhc3MudjEuQWdlbnRBY2NvdW50SAASKwoGc3lzdGVtGAwgASgLMhkuY29tcGFzcy52MS5TeXN0ZW1BY2NvdW50SABCBgoEa2luZCIPCg1TeXN0ZW1BY2NvdW50IjEKC1VzZXJBY2NvdW50EiIKBHJvbGUYASABKA4yFC5jb21wYXNzLnYxLlVzZXJSb2xlIlcKDEFnZW50QWNjb3VudBIVCg1vd25lcl91c2VyX2lkGAEgASgJEhcKD2hvbWVfY2hhbm5lbF9pZBgCIAEoCRIXCg9wYXJlbnRfYWdlbnRfaWQYAyABKAkikAEKDENoYW5uZWxHcm91cBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhcKD3BhcmVudF9ncm91cF9pZBgDIAEoCRIVCg1vd25lcl91c2VyX2lkGAQgASgJEjYKCnZpc2liaWxpdHkYBSABKA4yIi5jb21wYXNzLnYxLkNoYW5uZWxHcm91cFZpc2liaWxpdHkitwIKB0NoYW5uZWwSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghncm91cF9pZBgDIAEoCRIlCgRraW5kGAQgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIaChJtZW1iZXJfYWNjb3VudF9pZHMYBSADKAkSHgoWc3Vic2NyaWJlcl9hY2NvdW50X2lkcxgGIAMoCRIyCgtwb3N0X3BvbGljeRgHIAEoDjIdLmNvbXBhc3MudjEuQ2hhbm5lbFBvc3RQb2xpY3kSGAoQb3duZXJfYWNjb3VudF9pZBgIIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAkgASgIEi8KDnBpbm5lZF9lbnRyaWVzGAogAygLMhcuY29tcGFzcy52MS5QaW5uZWRFbnRyeSJsCgtQaW5uZWRFbnRyeRISCgptZXNzYWdlX2lkGAEgASgJEhAKCHBvc2l0aW9uGAIgASgFEhkKEXBpbm5lZF9hdF91bml4X21zGAMgASgDEhwKFHBpbm5lZF9ieV9hY2NvdW50X2lkGAQgASgJIjYKDkFnZW50V29ya3NwYWNlEgoKAmlkGAEgASgJEhgKEGFnZW50X2FjY291bnRfaWQYAiABKAkiggEKBVRvcGljEgoKAmlkGAEgASgJEhIKCmNoYW5uZWxfaWQYAiABKAkSDAoEbmFtZRgDIAEoCRIaChJjcmVhdGVkX2F0X3VuaXhfbXMYBCABKAMSHQoVY3JlYXRlZF9ieV9hY2NvdW50X2lkGAUgASgJEhAKCGFyY2hpdmVkGAYgASgIIoABCgdNZXNzYWdlEgoKAmlkGAEgASgJEhAKCHRvcGljX2lkGAIgASgJEhkKEWF1dGhvcl9hY2NvdW50X2lkGAMgASgJEhIKCmF0X3VuaXhfbXMYBCABKAMSKAoGYmxvY2tzGAUgAygLMhguY29tcGFzcy52MS5NZXNzYWdlQmxvY2sieQoMTWVzc2FnZUJsb2NrEg4KBHRleHQYASABKAlIABIeCgNhc2sYAiABKAsyDy5jb21wYXNzLnYxLkFza0gAEjAKCmFza19hbnN3ZXIYAyABKAsyGi5jb21wYXNzLnYxLkFza0Fuc3dlckJsb2NrSABCBwoFYmxvY2siSAoOQXNrQW5zd2VyQmxvY2sSHAoDYXNrGAEgASgLMg8uY29tcGFzcy52MS5Bc2sSGAoQYXNrZXJfYWNjb3VudF9pZBgCIAEoCSJTCgNBc2sSDgoGYXNrX2lkGAEgASgJEioKCXF1ZXN0aW9ucxgCIAMoCzIXLmNvbXBhc3MudjEuQXNrUXVlc3Rpb24SEAoIYW5zd2VyZWQYAyABKAgi8QEKC0Fza1F1ZXN0aW9uEhMKC3F1ZXN0aW9uX2lkGAEgASgJEhAKCHF1ZXN0aW9uGAIgASgJEg4KBmhlYWRlchgDIAEoCRImCgdvcHRpb25zGAQgAygLMhUuY29tcGFzcy52MS5Bc2tPcHRpb24SFgoOYWxsb3dfbXVsdGlwbGUYBSABKAgSGAoLcmVjb21tZW5kZWQYBiABKAVIAIgBARIZChFjaG9zZW5fb3B0aW9uX2lkcxgHIAMoCRITCgtjdXN0b21fdGV4dBgIIAEoCRIRCgl0aW1lZF9vdXQYCSABKAhCDgoMX3JlY29tbWVuZGVkIkwKCUFza09wdGlvbhIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIPCgdwcmV2aWV3GAQgASgJIokFChZTdWJzY3JpYmVDb21tc1Jlc3BvbnNlEgsKA3NlcRgBIAEoBBISCgphdF91bml4X21zGAIgASgDEhYKDmluc3RhbmNlX2Vwb2NoGAMgASgEEhQKDHNuYXBzaG90X3NlcRgEIAEoBBIzCg5tZXNzYWdlX3Bvc3RlZBgKIAEoCzIZLmNvbXBhc3MudjEuTWVzc2FnZVBvc3RlZEgAEjUKD21lc3NhZ2VfdXBkYXRlZBgLIAEoCzIaLmNvbXBhc3MudjEuTWVzc2FnZVVwZGF0ZWRIABI1Cg9jaGFubmVsX2NoYW5nZWQYDCABKAsyGi5jb21wYXNzLnYxLkNoYW5uZWxDaGFuZ2VkSAASQAoVY2hhbm5lbF9ncm91cF9jaGFuZ2VkGA0gASgLMh8uY29tcGFzcy52MS5DaGFubmVsR3JvdXBDaGFuZ2VkSAASNQoPYWNjb3VudF9jaGFuZ2VkGA4gASgLMhouY29tcGFzcy52MS5BY2NvdW50Q2hhbmdlZEgAEkQKF2FnZW50X3dvcmtzcGFjZV9jaGFuZ2VkGA8gASgLMiEuY29tcGFzcy52MS5BZ2VudFdvcmtzcGFjZUNoYW5nZWRIABI6Cg9yZXN5bmNfcmVxdWlyZWQYECABKAsyHy5jb21wYXNzLnYxLkNvbW1zUmVzeW5jUmVxdWlyZWRIABJCChZhZ2VudF9wcmVzZW5jZV9jaGFuZ2VkGBEgASgLMiAuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlQ2hhbmdlZEgAEjMKDnRvcGljX3Vwc2VydGVkGBIgASgLMhkuY29tcGFzcy52MS5Ub3BpY1Vwc2VydGVkSABCCQoHcGF5bG9hZCI1Cg1NZXNzYWdlUG9zdGVkEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiNgoOTWVzc2FnZVVwZGF0ZWQSJAoHbWVzc2FnZRgBIAEoCzITLmNvbXBhc3MudjEuTWVzc2FnZSIxCg1Ub3BpY1Vwc2VydGVkEiAKBXRvcGljGAEgASgLMhEuY29tcGFzcy52MS5Ub3BpYyJTCg5DaGFubmVsQ2hhbmdlZBIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsEhsKE3JlbW92ZWRfYWNjb3VudF9pZHMYAiADKAkiPgoTQ2hhbm5lbEdyb3VwQ2hhbmdlZBInCgVncm91cBgBIAEoCzIYLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwIjYKDkFjY291bnRDaGFuZ2VkEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiRgoVQWdlbnRXb3Jrc3BhY2VDaGFuZ2VkEi0KCXdvcmtzcGFjZRgBIAEoCzIaLmNvbXBhc3MudjEuQWdlbnRXb3Jrc3BhY2UiFQoTQ29tbXNSZXN5bmNSZXF1aXJlZCJvChRBZ2VudFByZXNlbmNlQ2hhbmdlZBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEisKCHByZXNlbmNlGAIgASgOMhkuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlEhAKCGFjdGl2aXR5GAMgASgJIjkKEUNyZWF0ZVVzZXJSZXF1ZXN0Eg4KBmhhbmRsZRgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiOgoSQ3JlYXRlVXNlclJlc3BvbnNlEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiUQoSQ3JlYXRlQWdlbnRSZXF1ZXN0Eg4KBmhhbmRsZRgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSFQoNcGFyZW50X2hhbmRsZRgDIAEoCSI7ChNDcmVhdGVBZ2VudFJlc3BvbnNlEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiKwoTTGlzdEFjY291bnRzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPQoUTGlzdEFjY291bnRzUmVzcG9uc2USJQoIYWNjb3VudHMYASADKAsyEy5jb21wYXNzLnYxLkFjY291bnQiegoZQ3JlYXRlQ2hhbm5lbEdyb3VwUmVxdWVzdBIMCgRuYW1lGAEgASgJEhcKD3BhcmVudF9ncm91cF9pZBgCIAEoCRI2Cgp2aXNpYmlsaXR5GAMgASgOMiIuY29tcGFzcy52MS5DaGFubmVsR3JvdXBWaXNpYmlsaXR5IkUKGkNyZWF0ZUNoYW5uZWxHcm91cFJlc3BvbnNlEicKBWdyb3VwGAEgASgLMhguY29tcGFzcy52MS5DaGFubmVsR3JvdXAiMAoYTGlzdENoYW5uZWxHcm91cHNSZXF1ZXN0EhQKDHNuYXBzaG90X3NlcRgBIAEoBCJFChlMaXN0Q2hhbm5lbEdyb3Vwc1Jlc3BvbnNlEigKBmdyb3VwcxgBIAMoCzIYLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwIisKE0xpc3RDaGFubmVsc1JlcXVlc3QSFAoMc25hcHNob3Rfc2VxGAEgASgEIj0KFExpc3RDaGFubmVsc1Jlc3BvbnNlEiUKCGNoYW5uZWxzGAEgAygLMhMuY29tcGFzcy52MS5DaGFubmVsInUKFENyZWF0ZUNoYW5uZWxSZXF1ZXN0EgwKBG5hbWUYASABKAkSEAoIZ3JvdXBfaWQYAiABKAkSJQoEa2luZBgDIAEoDjIXLmNvbXBhc3MudjEuQ2hhbm5lbEtpbmQSFgoObWVtYmVyX2hhbmRsZXMYBCADKAkiPQoVQ3JlYXRlQ2hhbm5lbFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwipAEKG1VwZGF0ZUNoYW5uZWxNZW1iZXJzUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEhoKEmFkZF9tZW1iZXJfaGFuZGxlcxgCIAMoCRIdChVyZW1vdmVfbWVtYmVyX2hhbmRsZXMYAyADKAkSGQoRc3Vic2NyaWJlX2hhbmRsZXMYBCADKAkSGwoTdW5zdWJzY3JpYmVfaGFuZGxlcxgFIAMoCSJEChxVcGRhdGVDaGFubmVsTWVtYmVyc1Jlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwiRwoUUmVwYXJlbnRBZ2VudFJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEW5ld19wYXJlbnRfaGFuZGxlGAIgASgJIj0KFVJlcGFyZW50QWdlbnRSZXNwb25zZRIkCgdhY2NvdW50GAEgASgLMhMuY29tcGFzcy52MS5BY2NvdW50IpcBChdTZXRDaGFubmVsUG9saWN5UmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEjIKC3Bvc3RfcG9saWN5GAIgASgOMh0uY29tcGFzcy52MS5DaGFubmVsUG9zdFBvbGljeRIUCgxvd25lcl9oYW5kbGUYAyABKAkSHgoWbWFuZGF0b3J5X3N1YnNjcmlwdGlvbhgEIAEoCCJAChhTZXRDaGFubmVsUG9saWN5UmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCJSChBHZXRSb3N0ZXJSZXF1ZXN0EiYKBXNjb3BlGAEgASgOMhcuY29tcGFzcy52MS5Sb3N0ZXJTY29wZRIWCg52YW50YWdlX2hhbmRsZRgCIAEoCSI9ChFHZXRSb3N0ZXJSZXNwb25zZRIoCgdlbnRyaWVzGAEgAygLMhcuY29tcGFzcy52MS5Sb3N0ZXJFbnRyeSLCAQoLUm9zdGVyRW50cnkSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIOCgZoYW5kbGUYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEhcKD3BhcmVudF9hZ2VudF9pZBgEIAEoCRIrCghwcmVzZW5jZRgFIAEoDjIZLmNvbXBhc3MudjEuQWdlbnRQcmVzZW5jZRIQCghhY3Rpdml0eRgGIAEoCRIbChNhY3Rpdml0eV9hdF91bml4X21zGAcgASgDIncKGFVwZGF0ZVBpbm5lZEJvYXJkUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEiUKA3BpbhgCIAEoCzIWLmNvbXBhc3MudjEuUGluTWVzc2FnZUgAEhoKEHVucGluX21lc3NhZ2VfaWQYAyABKAlIAEIECgJvcCI8CgpQaW5NZXNzYWdlEhIKCm1lc3NhZ2VfaWQYASABKAkSGgoScmVwbGFjZV9tZXNzYWdlX2lkGAIgASgJIkEKGVVwZGF0ZVBpbm5lZEJvYXJkUmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCIxChlPcGVuQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCSJLChpPcGVuQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRItCgl3b3Jrc3BhY2UYASABKAsyGi5jb21wYXNzLnYxLkFnZW50V29ya3NwYWNlIooBChNMaXN0TWVzc2FnZXNSZXF1ZXN0EhQKCmNoYW5uZWxfaWQYASABKAlIABINCgVsaW1pdBgCIAEoDRIZChFiZWZvcmVfbWVzc2FnZV9pZBgDIAEoCRIUCgxzbmFwc2hvdF9zZXEYBCABKAQSEAoIdG9waWNfaWQYBSABKAlCCwoJY29udGFpbmVyIj0KFExpc3RNZXNzYWdlc1Jlc3BvbnNlEiUKCG1lc3NhZ2VzGAEgAygLMhMuY29tcGFzcy52MS5NZXNzYWdlIq8BChJQb3N0TWVzc2FnZVJlcXVlc3QSFAoKY2hhbm5lbF9pZBgBIAEoCUgAEigKBmJsb2NrcxgCIAMoCzIYLmNvbXBhc3MudjEuTWVzc2FnZUJsb2NrEhIKCHRvcGljX2lkGAMgASgJSAESFAoKdG9waWNfbmFtZRgEIAEoCUgBEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAUgASgJQgsKCWNvbnRhaW5lckIHCgV0b3BpYyI7ChNQb3N0TWVzc2FnZVJlc3BvbnNlEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiQQoRTGlzdFRvcGljc1JlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIYChBpbmNsdWRlX2FyY2hpdmVkGAIgASgIIjcKEkxpc3RUb3BpY3NSZXNwb25zZRIhCgZ0b3BpY3MYASADKAsyES5jb21wYXNzLnYxLlRvcGljImYKElVwZGF0ZVRvcGljUmVxdWVzdBIQCgh0b3BpY19pZBgBIAEoCRIRCgRuYW1lGAIgASgJSACIAQESFQoIYXJjaGl2ZWQYAyABKAhIAYgBAUIHCgVfbmFtZUILCglfYXJjaGl2ZWQiNwoTVXBkYXRlVG9waWNSZXNwb25zZRIgCgV0b3BpYxgBIAEoCzIRLmNvbXBhc3MudjEuVG9waWMiVQoTUmVzcG9uZFRvQXNrUmVxdWVzdBIOCgZhc2tfaWQYASABKAkSLgoHYW5zd2VycxgCIAMoCzIdLmNvbXBhc3MudjEuQXNrUXVlc3Rpb25BbnN3ZXIiWAoRQXNrUXVlc3Rpb25BbnN3ZXISEwoLcXVlc3Rpb25faWQYASABKAkSGQoRY2hvc2VuX29wdGlvbl9pZHMYAiADKAkSEwoLY3VzdG9tX3RleHQYAyABKAkiFgoUUmVzcG9uZFRvQXNrUmVzcG9uc2UiagoVU2VhcmNoTWVzc2FnZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEhQKCmNoYW5uZWxfaWQYAiABKAlIABINCgVsaW1pdBgDIAEoDRIUCgxzbmFwc2hvdF9zZXEYBCABKARCBwoFc2NvcGUiPwoWU2VhcmNoTWVzc2FnZXNSZXNwb25zZRIlCghtZXNzYWdlcxgBIAMoCzITLmNvbXBhc3MudjEuTWVzc2FnZSJCChVTdWJzY3JpYmVDb21tc1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEKjUKCFVzZXJSb2xlEhQKEFVTRVJfUk9MRV9NRU1CRVIQABITCg9VU0VSX1JPTEVfQURNSU4QASphChZDaGFubmVsR3JvdXBWaXNpYmlsaXR5EiIKHkNIQU5ORUxfR1JPVVBfVklTSUJJTElUWV9PV05FUhAAEiMKH0NIQU5ORUxfR1JPVVBfVklTSUJJTElUWV9TSEFSRUQQASpVChFDaGFubmVsUG9zdFBvbGljeRIcChhDSEFOTkVMX1BPU1RfUE9MSUNZX09QRU4QABIiCh5DSEFOTkVMX1BPU1RfUE9MSUNZX09XTkVSX09OTFkQASpXCgtDaGFubmVsS2luZBIYChRDSEFOTkVMX0tJTkRfQ0hBTk5FTBAAEhMKD0NIQU5ORUxfS0lORF9ETRABEhkKFUNIQU5ORUxfS0lORF9HUk9VUF9ETRACKpwBCg1BZ2VudFByZXNlbmNlEh4KGkFHRU5UX1BSRVNFTkNFX1VOU1BFQ0lGSUVEEAASFwoTQUdFTlRfUFJFU0VOQ0VfSURMRRABEhoKFkFHRU5UX1BSRVNFTkNFX1dPUktJTkcQAhIaChZBR0VOVF9QUkVTRU5DRV9XQUlUSU5HEAMSGgoWQUdFTlRfUFJFU0VOQ0VfT0ZGTElORRAEKl4KC1Jvc3RlclNjb3BlEh0KGVJPU1RFUl9TQ09QRV9ORUlHSEJPUkhPT0QQABIYChRST1NURVJfU0NPUEVfU1VCVFJFRRABEhYKElJPU1RFUl9TQ09QRV9PV05FUhACMuYNCgxDb21tc1NlcnZpY2USSwoKQ3JlYXRlVXNlchIdLmNvbXBhc3MudjEuQ3JlYXRlVXNlclJlcXVlc3QaHi5jb21wYXNzLnYxLkNyZWF0ZVVzZXJSZXNwb25zZRJOCgtDcmVhdGVBZ2VudBIeLmNvbXBhc3MudjEuQ3JlYXRlQWdlbnRSZXF1ZXN0Gh8uY29tcGFzcy52MS5DcmVhdGVBZ2VudFJlc3BvbnNlElEKDExpc3RBY2NvdW50cxIfLmNvbXBhc3MudjEuTGlzdEFjY291bnRzUmVxdWVzdBogLmNvbXBhc3MudjEuTGlzdEFjY291bnRzUmVzcG9uc2USYwoSQ3JlYXRlQ2hhbm5lbEdyb3VwEiUuY29tcGFzcy52MS5DcmVhdGVDaGFubmVsR3JvdXBSZXF1ZXN0GiYuY29tcGFzcy52MS5DcmVhdGVDaGFubmVsR3JvdXBSZXNwb25zZRJgChFMaXN0Q2hhbm5lbEdyb3VwcxIkLmNvbXBhc3MudjEuTGlzdENoYW5uZWxHcm91cHNSZXF1ZXN0GiUuY29tcGFzcy52MS5MaXN0Q2hhbm5lbEdyb3Vwc1Jlc3BvbnNlElEKDExpc3RDaGFubmVscxIfLmNvbXBhc3MudjEuTGlzdENoYW5uZWxzUmVxdWVzdBogLmNvbXBhc3MudjEuTGlzdENoYW5uZWxzUmVzcG9uc2USVAoNQ3JlYXRlQ2hhbm5lbBIgLmNvbXBhc3MudjEuQ3JlYXRlQ2hhbm5lbFJlcXVlc3QaIS5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxSZXNwb25zZRJpChRVcGRhdGVDaGFubmVsTWVtYmVycxInLmNvbXBhc3MudjEuVXBkYXRlQ2hhbm5lbE1lbWJlcnNSZXF1ZXN0GiguY29tcGFzcy52MS5VcGRhdGVDaGFubmVsTWVtYmVyc1Jlc3BvbnNlElQKDVJlcGFyZW50QWdlbnQSIC5jb21wYXNzLnYxLlJlcGFyZW50QWdlbnRSZXF1ZXN0GiEuY29tcGFzcy52MS5SZXBhcmVudEFnZW50UmVzcG9uc2USYwoST3BlbkFnZW50V29ya3NwYWNlEiUuY29tcGFzcy52MS5PcGVuQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GiYuY29tcGFzcy52MS5PcGVuQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJRCgxMaXN0TWVzc2FnZXMSHy5jb21wYXNzLnYxLkxpc3RNZXNzYWdlc1JlcXVlc3QaIC5jb21wYXNzLnYxLkxpc3RNZXNzYWdlc1Jlc3BvbnNlEk4KC1Bvc3RNZXNzYWdlEh4uY29tcGFzcy52MS5Qb3N0TWVzc2FnZVJlcXVlc3QaHy5jb21wYXNzLnYxLlBvc3RNZXNzYWdlUmVzcG9uc2USSwoKTGlzdFRvcGljcxIdLmNvbXBhc3MudjEuTGlzdFRvcGljc1JlcXVlc3QaHi5jb21wYXNzLnYxLkxpc3RUb3BpY3NSZXNwb25zZRJOCgtVcGRhdGVUb3BpYxIeLmNvbXBhc3MudjEuVXBkYXRlVG9waWNSZXF1ZXN0Gh8uY29tcGFzcy52MS5VcGRhdGVUb3BpY1Jlc3BvbnNlElEKDFJlc3BvbmRUb0FzaxIfLmNvbXBhc3MudjEuUmVzcG9uZFRvQXNrUmVxdWVzdBogLmNvbXBhc3MudjEuUmVzcG9uZFRvQXNrUmVzcG9uc2USVwoOU2VhcmNoTWVzc2FnZXMSIS5jb21wYXNzLnYxLlNlYXJjaE1lc3NhZ2VzUmVxdWVzdBoiLmNvbXBhc3MudjEuU2VhcmNoTWVzc2FnZXNSZXNwb25zZRJdChBTZXRDaGFubmVsUG9saWN5EiMuY29tcGFzcy52MS5TZXRDaGFubmVsUG9saWN5UmVxdWVzdBokLmNvbXBhc3MudjEuU2V0Q2hhbm5lbFBvbGljeVJlc3BvbnNlEkgKCUdldFJvc3RlchIcLmNvbXBhc3MudjEuR2V0Um9zdGVyUmVxdWVzdBodLmNvbXBhc3MudjEuR2V0Um9zdGVyUmVzcG9uc2USYAoRVXBkYXRlUGlubmVkQm9hcmQSJC5jb21wYXNzLnYxLlVwZGF0ZVBpbm5lZEJvYXJkUmVxdWVzdBolLmNvbXBhc3MudjEuVXBkYXRlUGlubmVkQm9hcmRSZXNwb25zZRJZCg5TdWJzY3JpYmVDb21tcxIhLmNvbXBhc3MudjEuU3Vic2NyaWJlQ29tbXNSZXF1ZXN0GiIuY29tcGFzcy52MS5TdWJzY3JpYmVDb21tc1Jlc3BvbnNlMAFiBnByb3RvMw"); /** * An account in the communication layer: a human user or an owned agent. Both @@ -1162,13 +1162,13 @@ export type CreateAgentRequest = Message$1<"compass.v1.CreateAgentRequest"> & { displayName: string; /** - * Optional parent in the agent tree; empty = root. The server validates it - * (must resolve to an existing agent under the caller's resolved owner) before - * set-at-creation. + * Optional parent in the agent tree; empty = root. A `@handle`; the server + * resolves it to an account id; unknown → NOT_FOUND. Resolves owner-qualified + * (a bare handle defaults to the caller's own owner namespace). * - * @generated from field: string parent_agent_id = 3; + * @generated from field: string parent_handle = 3; */ - parentAgentId: string; + parentHandle: string; }; /** @@ -1379,11 +1379,12 @@ export type CreateChannelRequest = Message$1<"compass.v1.CreateChannelRequest"> kind: ChannelKind; /** - * Initial members party to the channel. + * Initial members party to the channel. Each is a `@handle`; the server + * resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: repeated string member_account_ids = 4; + * @generated from field: repeated string member_handles = 4; */ - memberAccountIds: string[]; + memberHandles: string[]; }; /** @@ -1422,32 +1423,37 @@ export type UpdateChannelMembersRequest = Message$1<"compass.v1.UpdateChannelMem channelId: string; /** - * Accounts to add as members (join, read access). + * Accounts to add as members (join, read access). Each is a `@handle`; the + * server resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: repeated string add_member_account_ids = 2; + * @generated from field: repeated string add_member_handles = 2; */ - addMemberAccountIds: string[]; + addMemberHandles: string[]; /** - * Accounts to remove from membership. + * Accounts to remove from membership. Each is a `@handle`; the server + * resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: repeated string remove_member_account_ids = 3; + * @generated from field: repeated string remove_member_handles = 3; */ - removeMemberAccountIds: string[]; + removeMemberHandles: string[]; /** * Members to mark subscribed (push opt-in); must be current or added members. + * Each is a `@handle`; the server resolves it to an account id; unknown → + * NOT_FOUND. * - * @generated from field: repeated string subscribe_account_ids = 4; + * @generated from field: repeated string subscribe_handles = 4; */ - subscribeAccountIds: string[]; + subscribeHandles: string[]; /** - * Members to mark unsubscribed (read-only). + * Members to mark unsubscribed (read-only). Each is a `@handle`; the server + * resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: repeated string unsubscribe_account_ids = 5; + * @generated from field: repeated string unsubscribe_handles = 5; */ - unsubscribeAccountIds: string[]; + unsubscribeHandles: string[]; }; /** @@ -1479,18 +1485,20 @@ export const UpdateChannelMembersResponseSchema: GenMessage & { /** - * The agent to move. + * The agent to move. A `@handle`; the server resolves it to an account id; + * unknown → NOT_FOUND. * - * @generated from field: string agent_account_id = 1; + * @generated from field: string agent_handle = 1; */ - agentAccountId: string; + agentHandle: string; /** - * The new parent; empty promotes the agent to a root. + * The new parent; empty promotes the agent to a root. A `@handle`; the server + * resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: string new_parent_agent_id = 2; + * @generated from field: string new_parent_handle = 2; */ - newParentAgentId: string; + newParentHandle: string; }; /** @@ -1536,11 +1544,12 @@ export type SetChannelPolicyRequest = Message$1<"compass.v1.SetChannelPolicyRequ postPolicy: ChannelPostPolicy; /** - * The owner/operator account for the channel; empty leaves it unowned. + * The owner/operator account for the channel; empty leaves it unowned. A + * `@handle`; the server resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: string owner_account_id = 3; + * @generated from field: string owner_handle = 3; */ - ownerAccountId: string; + ownerHandle: string; /** * Whether membership implies a non-togglable subscription. @@ -1588,12 +1597,13 @@ export type GetRosterRequest = Message$1<"compass.v1.GetRosterRequest"> & { scope: RosterScope; /** - * The agent whose vantage to use. Optional for human/UI callers naming a - * vantage point; an agent caller gets it session-resolved server-side. + * The vantage whose roster to compute. A `@handle`; empty ⇒ the caller's own + * vantage (an agent caller is session-resolved server-side), non-empty ⇒ the + * server resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: string agent_account_id = 2; + * @generated from field: string vantage_handle = 2; */ - agentAccountId: string; + vantageHandle: string; }; /** @@ -1760,11 +1770,12 @@ export const UpdatePinnedBoardResponseSchema: GenMessage & { /** - * The agent account to open the workspace (ACP surface) for. + * The agent account to open the workspace (ACP surface) for. A `@handle`; the + * server resolves it to an account id; unknown → NOT_FOUND. * - * @generated from field: string agent_account_id = 1; + * @generated from field: string agent_handle = 1; */ - agentAccountId: string; + agentHandle: string; }; /** diff --git a/packages/compass-client/src/gen/compass/v1/compass_pb.ts b/packages/compass-client/src/gen/compass/v1/compass_pb.ts index db8ae4bd..3667a4b8 100644 --- a/packages/compass-client/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-client/src/gen/compass/v1/compass_pb.ts @@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file compass/v1/compass.proto. */ export const file_compass_v1_compass: GenFile = /*@__PURE__*/ - fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkInAKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJIkkKEUFnZW50TWVzc2FnZUNodW5rEhIKCnNlc3Npb25faWQYASABKAkSDAoEdGV4dBgCIAEoCRISCgppc190aG91Z2h0GAMgASgIInkKDUFnZW50VG9vbENhbGwSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAkSDQoFdGl0bGUYAyABKAkSLwoGc3RhdHVzGAQgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIkwKCUFnZW50UGxhbhISCgpzZXNzaW9uX2lkGAEgASgJEisKB2VudHJpZXMYAiADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IlMKDkFnZW50UGxhbkVudHJ5Eg8KB2NvbnRlbnQYASABKAkSMAoGc3RhdHVzGAIgASgOMiAuY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeVN0YXR1cyKsAwoMU2Vzc2lvbkV2ZW50EhAKCGV2ZW50X2lkGAEgASgJEhIKCmF0X3VuaXhfbXMYAiABKAMSOgoOYXNzaXN0YW50X3RleHQYAyABKAsyIC5jb21wYXNzLnYxLlNlc3Npb25Bc3Npc3RhbnRUZXh0SAASLwoIdGhpbmtpbmcYBCABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25UaGlua2luZ0gAEjAKCXRvb2xfY2FsbBgFIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsSAASPQoQdG9vbF9jYWxsX3VwZGF0ZRgGIAEoCzIhLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsVXBkYXRlSAASJwoEcGxhbhgHIAEoCzIXLmNvbXBhc3MudjEuU2Vzc2lvblBsYW5IABIrCgZub3RpY2UYCCABKAsyGS5jb21wYXNzLnYxLlNlc3Npb25Ob3RpY2VIABI5ChFzZXNzaW9uX2luamVjdGlvbhgJIAEoCzIcLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbkgAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIm4KEFNlc3Npb25JbmplY3Rpb24SMQoHb3Bfa2luZBgBIAEoDjIgLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbktpbmQSEgoKbWVzc2FnZV9pZBgCIAEoCRITCgtmcm9tX2hhbmRsZRgDIAEoCSIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkifgoRQWdlbnRTZXNzaW9uRnJhbWUSEgoKc2Vzc2lvbl9pZBgBIAEoCRInCgVldmVudBgCIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkV2ZW50EiwKBXN0YXRlGAMgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZSJ0Ch5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCRIPCgdwZXJzb25hGAMgASgJEgwKBHJvbGUYBCABKAkiOQofUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRIWCg5jb250YWluZXJfbmFtZRgBIAEoCSJQChtSZW1vdmVBZ2VudFdvcmtzcGFjZVJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkiHgocUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZSJjChhTdGFydEFnZW50U2Vzc2lvblJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRcmVzdW1lX3Nlc3Npb25faWQYAyABKAlKBAgCEANSDmluaXRpYWxfcHJvbXB0Ii8KGVN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSJeChFTcGF3bkFnZW50UmVxdWVzdBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAMgASgJSgQIAhADUg5pbml0aWFsX3Byb21wdCJAChJTcGF3bkFnZW50UmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIWCg5jb250YWluZXJfbmFtZRgCIAEoCSItChdTdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhoKGFN0b3BBZ2VudFNlc3Npb25SZXNwb25zZSIvChlSZWxvYWRBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiMAoaUmVsb2FkQWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSIrChVHZXRBZ2VudFN0YXR1c1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJKChZHZXRBZ2VudFN0YXR1c1Jlc3BvbnNlEjAKCHN0YXR1c2VzGAEgAygLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXMiJwoRSXNzdWVUb2tlblJlcXVlc3QSEgoKYWNjb3VudF9pZBgBIAEoCSIjChJJc3N1ZVRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkiKAoSUmV2b2tlVG9rZW5SZXF1ZXN0EhIKBXRva2VuGAEgASgJQgOAAQEiFQoTUmV2b2tlVG9rZW5SZXNwb25zZSInChVQdXRBZ2VudENvbmZpZ1JlcXVlc3QSDgoGYnVuZGxlGAEgASgMIikKFlB1dEFnZW50Q29uZmlnUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCSIbChlHZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0IskBChpHZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJEg4KBnNraWxscxgCIAMoCRISCgpleHRlbnNpb25zGAMgAygJEhMKC21jcF9zZXJ2ZXJzGAQgAygJEhQKDGhhc19zZXR0aW5ncxgFIAEoCBIVCg1oYXNfYWdlbnRzX21kGAYgASgIEg0KBXJ1bGVzGAcgAygJEhEKCXN1YmFnZW50cxgIIAMoCRISCgpoYXNfbW9kZWxzGAkgASgIIhoKGERlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdCIbChlEZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlIigKEEFnZW50QXR0cmlidXRpb24SFAoMYWdlbnRfaGFuZGxlGAEgASgJIkUKCEZvcmdlUmVmEisKCHByb3ZpZGVyGAEgASgOMhkuY29tcGFzcy52MS5Gb3JnZVByb3ZpZGVyEgwKBGhvc3QYAiABKAkipAMKBUlzc3VlEgoKAmlkGAEgASgJEiMKBWZvcmdlGAIgASgLMhQuY29tcGFzcy52MS5Gb3JnZVJlZhIMCgRyZXBvGAMgASgJEg4KBm51bWJlchgEIAEoDRINCgV0aXRsZRgFIAEoCRIMCgRib2R5GAYgASgJEhMKC2ZvcmdlX3N0YXRlGAcgASgJEgsKA3VybBgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg4KBmxhYmVscxgLIAMoCRIlCgVzdGF0ZRgMIAEoDjIWLmNvbXBhc3MudjEuSXNzdWVTdGF0ZRIQCghwcmlvcml0eRgNIAEoCRIQCghhc3NpZ25lZRgOIAEoCRIPCgdzdW1tYXJ5GA8gASgJEg4KBmJyYW5jaBgQIAEoCRIkCgNwcnMYESADKAsyFy5jb21wYXNzLnYxLlB1bGxSZXF1ZXN0EicKB3RyYWNrZXIYEiABKAsyFi5jb21wYXNzLnYxLlRyYWNrZXJSZWYingMKC1B1bGxSZXF1ZXN0EiMKBWZvcmdlGAEgASgLMhQuY29tcGFzcy52MS5Gb3JnZVJlZhIMCgRyZXBvGAIgASgJEg4KBm51bWJlchgDIAEoDRINCgV0aXRsZRgEIAEoCRITCgtmb3JnZV9zdGF0ZRgFIAEoCRILCgN1cmwYBiABKAkSEAoIaGVhZF9yZWYYByABKAkSEAoIYmFzZV9yZWYYCCABKAkSKwoFYWdlbnQYCSABKAsyHC5jb21wYXNzLnYxLkFnZW50QXR0cmlidXRpb24SFQoNZm9yZ2VfYWNjb3VudBgKIAEoCRINCgVkcmFmdBgLIAEoCBIpCgdjaGFuZ2VkGAwgASgLMhguY29tcGFzcy52MS5DaGFuZ2VkU3RhdHMSKQoGY2hlY2tzGA0gASgLMhkuY29tcGFzcy52MS5DaGVja3NTdW1tYXJ5EiMKB3Jldmlld3MYDiADKAsyEi5jb21wYXNzLnYxLlJldmlldxIpCgd0aHJlYWRzGA8gAygLMhguY29tcGFzcy52MS5SZXZpZXdUaHJlYWQiUwoNQ2hlY2tzU3VtbWFyeRIQCghoZWFkX3NoYRgBIAEoCRINCgVzdGF0ZRgCIAEoCRIhCgZjaGVja3MYAyADKAsyES5jb21wYXNzLnYxLkNoZWNrIkMKBUNoZWNrEgwKBG5hbWUYASABKAkSDQoFc3RhdGUYAiABKAkSCwoDdXJsGAMgASgJEhAKCHJlcXVpcmVkGAQgASgIIkMKDENoYW5nZWRTdGF0cxINCgVmaWxlcxgBIAEoDRIRCglhZGRpdGlvbnMYAiABKA0SEQoJZGVsZXRpb25zGAMgASgNIkMKClRyYWNrZXJSZWYSDAoEa2luZBgBIAEoCRIKCgJpZBgCIAEoCRIOCgZzdGF0dXMYAyABKAkSCwoDdXJsGAQgASgJIkcKBlJldmlldxIOCgZhdXRob3IYASABKAkSDgoGaXNfYm90GAIgASgIEg8KB3ZlcmRpY3QYAyABKAkSDAoEYm9keRgEIAEoCSJVCgxSZXZpZXdUaHJlYWQSDAoEcGF0aBgBIAEoCRIQCghyZXNvbHZlZBgCIAEoCBIlCghjb21tZW50cxgDIAMoCzITLmNvbXBhc3MudjEuQ29tbWVudCI3CgdDb21tZW50Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDAoEYm9keRgDIAEoCSpkCg5TZWNyZXREZWxpdmVyeRIfChtTRUNSRVRfREVMSVZFUllfVU5TUEVDSUZJRUQQABIYChRTRUNSRVRfREVMSVZFUllfRklMRRABEhcKE1NFQ1JFVF9ERUxJVkVSWV9FTlYQAipwCgpTZWNyZXRLaW5kEhsKF1NFQ1JFVF9LSU5EX1VOU1BFQ0lGSUVEEAASFwoTU0VDUkVUX0tJTkRfR0VORVJJQxABEhgKFFNFQ1JFVF9LSU5EX1BST1ZJREVSEAISEgoOU0VDUkVUX0tJTkRfR0gQAypDCgtTZXJ2ZXJTdGF0ZRIcChhTRVJWRVJfU1RBVEVfVU5TUEVDSUZJRUQQABIWChJTRVJWRVJfU1RBVEVfUkVBRFkQASqCAgoRQWdlbnRTZXNzaW9uU3RhdGUSIwofQUdFTlRfU0VTU0lPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFHRU5UX1NFU1NJT05fU1RBVEVfU1RBUlRJTkcQARIdChlBR0VOVF9TRVNTSU9OX1NUQVRFX1JFQURZEAISHwobQUdFTlRfU0VTU0lPTl9TVEFURV9XT1JLSU5HEAMSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9TVE9QUEVEEAQSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9FUlJPUkVEEAUSJAogQUdFTlRfU0VTU0lPTl9TVEFURV9ESVNDT05ORUNURUQQBirSAQoTQWdlbnRUb29sQ2FsbFN0YXR1cxImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIgoeQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19QRU5ESU5HEAESJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19JTl9QUk9HUkVTUxACEiQKIEFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfQ09NUExFVEVEEAMSIQodQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19GQUlMRUQQBCq0AQoUQWdlbnRQbGFuRW50cnlTdGF0dXMSJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIjCh9BR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19QRU5ESU5HEAESJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfSU5fUFJPR1JFU1MQAhIlCiFBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19DT01QTEVURUQQAyqEAQoUU2Vzc2lvbkluamVjdGlvbktpbmQSJgoiU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9VTlNQRUNJRklFRBAAEiAKHFNFU1NJT05fSU5KRUNUSU9OX0tJTkRfU1RFRVIQARIiCh5TRVNTSU9OX0lOSkVDVElPTl9LSU5EX0RFTElWRVIQAirxAQoKSXNzdWVTdGF0ZRIbChdJU1NVRV9TVEFURV9VTlNQRUNJRklFRBAAEhcKE0lTU1VFX1NUQVRFX0JBQ0tMT0cQARIUChBJU1NVRV9TVEFURV9UT0RPEAISFgoSSVNTVUVfU1RBVEVfUVVFVUVEEAMSFwoTSVNTVUVfU1RBVEVfQkxPQ0tFRBAEEhsKF0lTU1VFX1NUQVRFX0lOX1BST0dSRVNTEAUSGQoVSVNTVUVfU1RBVEVfSU5fUkVWSUVXEAYSFAoQSVNTVUVfU1RBVEVfRE9ORRAHEhgKFElTU1VFX1NUQVRFX0FSQ0hJVkVEEAgqnAEKDUZvcmdlUHJvdmlkZXISHgoaRk9SR0VfUFJPVklERVJfVU5TUEVDSUZJRUQQABIZChVGT1JHRV9QUk9WSURFUl9HSVRIVUIQARIZChVGT1JHRV9QUk9WSURFUl9HSVRMQUIQAhIaChZGT1JHRV9QUk9WSURFUl9GT1JHRUpPEAMSGQoVRk9SR0VfUFJPVklERVJfTElORUFSEAQyrQwKDkNvbXBhc3NTZXJ2aWNlElQKDUdldFNlcnZlckluZm8SIC5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXF1ZXN0GiEuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVzcG9uc2USPwoGV2hvQW1JEhkuY29tcGFzcy52MS5XaG9BbUlSZXF1ZXN0GhouY29tcGFzcy52MS5XaG9BbUlSZXNwb25zZRJcCg9TdWJzY3JpYmVFdmVudHMSIi5jb21wYXNzLnYxLlN1YnNjcmliZUV2ZW50c1JlcXVlc3QaIy5jb21wYXNzLnYxLlN1YnNjcmliZUV2ZW50c1Jlc3BvbnNlMAESWgoPTGlzdEJvYXJkSXNzdWVzEiIuY29tcGFzcy52MS5MaXN0Qm9hcmRJc3N1ZXNSZXF1ZXN0GiMuY29tcGFzcy52MS5MaXN0Qm9hcmRJc3N1ZXNSZXNwb25zZRJyChdQcm92aXNpb25BZ2VudFdvcmtzcGFjZRIqLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GisuY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmAKEVN0YXJ0QWdlbnRTZXNzaW9uEiQuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlcXVlc3QaJS5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USSwoKU3Bhd25BZ2VudBIdLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlcXVlc3QaHi5jb21wYXNzLnYxLlNwYXduQWdlbnRSZXNwb25zZRJdChBTdG9wQWdlbnRTZXNzaW9uEiMuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBokLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlEmkKFFJlbW92ZUFnZW50V29ya3NwYWNlEicuY29tcGFzcy52MS5SZW1vdmVBZ2VudFdvcmtzcGFjZVJlcXVlc3QaKC5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVzcG9uc2USYwoSUmVsb2FkQWdlbnRTZXNzaW9uEiUuY29tcGFzcy52MS5SZWxvYWRBZ2VudFNlc3Npb25SZXF1ZXN0GiYuY29tcGFzcy52MS5SZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRJXCg5HZXRBZ2VudFN0YXR1cxIhLmNvbXBhc3MudjEuR2V0QWdlbnRTdGF0dXNSZXF1ZXN0GiIuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1Jlc3BvbnNlEmIKFVN1YnNjcmliZUFnZW50U2Vzc2lvbhIoLmNvbXBhc3MudjEuU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBodLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uRnJhbWUwARJLCgpJc3N1ZVRva2VuEh0uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVxdWVzdBoeLmNvbXBhc3MudjEuSXNzdWVUb2tlblJlc3BvbnNlEk4KC1Jldm9rZVRva2VuEh4uY29tcGFzcy52MS5SZXZva2VUb2tlblJlcXVlc3QaHy5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVzcG9uc2USVwoOUHV0QWdlbnRDb25maWcSIS5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVxdWVzdBoiLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXNwb25zZRJjChJHZXRBZ2VudENvbmZpZ0luZm8SJS5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3QaJi5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEmAKEURlbGV0ZUFnZW50Q29uZmlnEiQuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QaJS5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2Uy/QEKDlNlY3JldHNTZXJ2aWNlEkgKCVNldFNlY3JldBIcLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVxdWVzdBodLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVzcG9uc2USTgoLTGlzdFNlY3JldHMSHi5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVxdWVzdBofLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXNwb25zZRJRCgxEZWxldGVTZWNyZXQSHy5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlcXVlc3QaIC5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlc3BvbnNlYgZwcm90bzM"); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkInAKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJIkkKEUFnZW50TWVzc2FnZUNodW5rEhIKCnNlc3Npb25faWQYASABKAkSDAoEdGV4dBgCIAEoCRISCgppc190aG91Z2h0GAMgASgIInkKDUFnZW50VG9vbENhbGwSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAkSDQoFdGl0bGUYAyABKAkSLwoGc3RhdHVzGAQgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIkwKCUFnZW50UGxhbhISCgpzZXNzaW9uX2lkGAEgASgJEisKB2VudHJpZXMYAiADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IlMKDkFnZW50UGxhbkVudHJ5Eg8KB2NvbnRlbnQYASABKAkSMAoGc3RhdHVzGAIgASgOMiAuY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeVN0YXR1cyKsAwoMU2Vzc2lvbkV2ZW50EhAKCGV2ZW50X2lkGAEgASgJEhIKCmF0X3VuaXhfbXMYAiABKAMSOgoOYXNzaXN0YW50X3RleHQYAyABKAsyIC5jb21wYXNzLnYxLlNlc3Npb25Bc3Npc3RhbnRUZXh0SAASLwoIdGhpbmtpbmcYBCABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25UaGlua2luZ0gAEjAKCXRvb2xfY2FsbBgFIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsSAASPQoQdG9vbF9jYWxsX3VwZGF0ZRgGIAEoCzIhLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsVXBkYXRlSAASJwoEcGxhbhgHIAEoCzIXLmNvbXBhc3MudjEuU2Vzc2lvblBsYW5IABIrCgZub3RpY2UYCCABKAsyGS5jb21wYXNzLnYxLlNlc3Npb25Ob3RpY2VIABI5ChFzZXNzaW9uX2luamVjdGlvbhgJIAEoCzIcLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbkgAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIm4KEFNlc3Npb25JbmplY3Rpb24SMQoHb3Bfa2luZBgBIAEoDjIgLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbktpbmQSEgoKbWVzc2FnZV9pZBgCIAEoCRITCgtmcm9tX2hhbmRsZRgDIAEoCSIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkifgoRQWdlbnRTZXNzaW9uRnJhbWUSEgoKc2Vzc2lvbl9pZBgBIAEoCRInCgVldmVudBgCIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkV2ZW50EiwKBXN0YXRlGAMgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZSJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLwoZUmVsb2FkQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIjAKGlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkiKwoVR2V0QWdlbnRTdGF0dXNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiSgoWR2V0QWdlbnRTdGF0dXNSZXNwb25zZRIwCghzdGF0dXNlcxgBIAMoCzIeLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdHVzIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3QiyQEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgiGgoYRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0IhsKGURlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2UiKAoQQWdlbnRBdHRyaWJ1dGlvbhIUCgxhZ2VudF9oYW5kbGUYASABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSKkAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiKeAwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZCJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iQwoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkiRwoGUmV2aWV3Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDwoHdmVyZGljdBgDIAEoCRIMCgRib2R5GAQgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKkMKC1NlcnZlclN0YXRlEhwKGFNFUlZFUl9TVEFURV9VTlNQRUNJRklFRBAAEhYKElNFUlZFUl9TVEFURV9SRUFEWRABKoICChFBZ2VudFNlc3Npb25TdGF0ZRIjCh9BR0VOVF9TRVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAASIAocQUdFTlRfU0VTU0lPTl9TVEFURV9TVEFSVElORxABEh0KGUFHRU5UX1NFU1NJT05fU1RBVEVfUkVBRFkQAhIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1dPUktJTkcQAxIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1NUT1BQRUQQBBIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX0VSUk9SRUQQBRIkCiBBR0VOVF9TRVNTSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBAGKtIBChNBZ2VudFRvb2xDYWxsU3RhdHVzEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIiCh5BR0VOVF9UT09MX0NBTExfU1RBVFVTX1BFTkRJTkcQARImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX0lOX1BST0dSRVNTEAISJAogQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19DT01QTEVURUQQAxIhCh1BR0VOVF9UT09MX0NBTExfU1RBVFVTX0ZBSUxFRBAEKrQBChRBZ2VudFBsYW5FbnRyeVN0YXR1cxInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19VTlNQRUNJRklFRBAAEiMKH0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1BFTkRJTkcQARInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19JTl9QUk9HUkVTUxACEiUKIUFHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0NPTVBMRVRFRBADKoQBChRTZXNzaW9uSW5qZWN0aW9uS2luZBImCiJTRVNTSU9OX0lOSkVDVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASIAocU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9TVEVFUhABEiIKHlNFU1NJT05fSU5KRUNUSU9OX0tJTkRfREVMSVZFUhACKvEBCgpJc3N1ZVN0YXRlEhsKF0lTU1VFX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSVNTVUVfU1RBVEVfQkFDS0xPRxABEhQKEElTU1VFX1NUQVRFX1RPRE8QAhIWChJJU1NVRV9TVEFURV9RVUVVRUQQAxIXChNJU1NVRV9TVEFURV9CTE9DS0VEEAQSGwoXSVNTVUVfU1RBVEVfSU5fUFJPR1JFU1MQBRIZChVJU1NVRV9TVEFURV9JTl9SRVZJRVcQBhIUChBJU1NVRV9TVEFURV9ET05FEAcSGAoUSVNTVUVfU1RBVEVfQVJDSElWRUQQCCqcAQoNRm9yZ2VQcm92aWRlchIeChpGT1JHRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX1BST1ZJREVSX0dJVEhVQhABEhkKFUZPUkdFX1BST1ZJREVSX0dJVExBQhACEhoKFkZPUkdFX1BST1ZJREVSX0ZPUkdFSk8QAxIZChVGT1JHRV9QUk9WSURFUl9MSU5FQVIQBDKtDAoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlElwKD1N1YnNjcmliZUV2ZW50cxIiLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBojLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2UwARJaCg9MaXN0Qm9hcmRJc3N1ZXMSIi5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QaIy5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJLCgpTcGF3bkFnZW50Eh0uY29tcGFzcy52MS5TcGF3bkFnZW50UmVxdWVzdBoeLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USTgoLUmV2b2tlVG9rZW4SHi5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVxdWVzdBofLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXNwb25zZRJXCg5QdXRBZ2VudENvbmZpZxIhLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXF1ZXN0GiIuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEmMKEkdldEFnZW50Q29uZmlnSW5mbxIlLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdBomLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USYAoRRGVsZXRlQWdlbnRDb25maWcSJC5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdBolLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZTL9AQoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2ViBnByb3RvMw"); /** * @generated from message compass.v1.SetSecretRequest @@ -1045,13 +1045,13 @@ export const AgentSessionFrameSchema: GenMessage = /*@__PURE_ */ export type ProvisionAgentWorkspaceRequest = Message<"compass.v1.ProvisionAgentWorkspaceRequest"> & { /** - * The owned agent account this workstream belongs to (the AgentAccount id - * from CommsService). Names whose credentials + home channel the container - * is provisioned for. + * The owned agent account this workstream belongs to. A `@handle` + * (owner-qualified, e.g. `matt/compass-ux`); the server resolves it to an + * account id; unknown → NOT_FOUND. * - * @generated from field: string agent_account_id = 1; + * @generated from field: string agent_handle = 1; */ - agentAccountId: string; + agentHandle: string; /** * Repo carriage removed (SEA-1527, Matt 2026-07-29): spawn/provision no longer @@ -1235,13 +1235,13 @@ export const StartAgentSessionResponseSchema: GenMessage & { /** - * The owned agent account to bring online (the AgentAccount id from - * CommsService). The container is provisioned for, and the session bound to, - * this account. + * The owned agent account to bring online. A `@handle` (owner-qualified, + * e.g. `matt/compass-ux`); the server resolves it to an account id; unknown + * → NOT_FOUND. * - * @generated from field: string agent_account_id = 1; + * @generated from field: string agent_handle = 1; */ - agentAccountId: string; + agentHandle: string; /** * End-to-end idempotency key. A retry with the same id returns the same @@ -1403,13 +1403,13 @@ export const GetAgentStatusResponseSchema: GenMessage = */ export type IssueTokenRequest = Message<"compass.v1.IssueTokenRequest"> & { /** - * The account to mint a token for — the id of an Account created via - * CommsService. The caller's identity is the authenticated connection, never - * a field here. + * The account to mint a token for — a `@handle` naming a user or agent; the + * server resolves it to an account id; unknown → NOT_FOUND. The caller's + * identity is the authenticated connection, never a field here. * - * @generated from field: string account_id = 1; + * @generated from field: string account_handle = 1; */ - accountId: string; + accountHandle: string; }; /** diff --git a/proto/compass/v1/agent_gateway.proto b/proto/compass/v1/agent_gateway.proto index 6732ef31..31e47928 100644 --- a/proto/compass/v1/agent_gateway.proto +++ b/proto/compass/v1/agent_gateway.proto @@ -181,7 +181,7 @@ message SpawnPeerResponse { // Same-owner authority (F2); the account row, home channel, and history survive // (despawn tears down compute, not identity). message DespawnPeerRequest { - string agent_account_id = 1; // the peer to tear down + string agent_handle = 1; // the peer to tear down; a `@handle` the server resolves to an account id; unknown → NOT_FOUND } message DespawnPeerResponse {} diff --git a/proto/compass/v1/comms.proto b/proto/compass/v1/comms.proto index ffa627c6..777d4fc3 100644 --- a/proto/compass/v1/comms.proto +++ b/proto/compass/v1/comms.proto @@ -584,10 +584,10 @@ message CreateUserResponse { message CreateAgentRequest { string handle = 1; string display_name = 2; - // Optional parent in the agent tree; empty = root. The server validates it - // (must resolve to an existing agent under the caller's resolved owner) before - // set-at-creation. - string parent_agent_id = 3; + // Optional parent in the agent tree; empty = root. A `@handle`; the server + // resolves it to an account id; unknown → NOT_FOUND. Resolves owner-qualified + // (a bare handle defaults to the caller's own owner namespace). + string parent_handle = 3; } message CreateAgentResponse { @@ -643,8 +643,9 @@ message CreateChannelRequest { // owner-scoped channel. string group_id = 2; ChannelKind kind = 3; - // Initial members party to the channel. - repeated string member_account_ids = 4; + // Initial members party to the channel. Each is a `@handle`; the server + // resolves it to an account id; unknown → NOT_FOUND. + repeated string member_handles = 4; } message CreateChannelResponse { @@ -654,14 +655,19 @@ message CreateChannelResponse { message UpdateChannelMembersRequest { // The channel to mutate. string channel_id = 1; - // Accounts to add as members (join, read access). - repeated string add_member_account_ids = 2; - // Accounts to remove from membership. - repeated string remove_member_account_ids = 3; + // Accounts to add as members (join, read access). Each is a `@handle`; the + // server resolves it to an account id; unknown → NOT_FOUND. + repeated string add_member_handles = 2; + // Accounts to remove from membership. Each is a `@handle`; the server + // resolves it to an account id; unknown → NOT_FOUND. + repeated string remove_member_handles = 3; // Members to mark subscribed (push opt-in); must be current or added members. - repeated string subscribe_account_ids = 4; - // Members to mark unsubscribed (read-only). - repeated string unsubscribe_account_ids = 5; + // Each is a `@handle`; the server resolves it to an account id; unknown → + // NOT_FOUND. + repeated string subscribe_handles = 4; + // Members to mark unsubscribed (read-only). Each is a `@handle`; the server + // resolves it to an account id; unknown → NOT_FOUND. + repeated string unsubscribe_handles = 5; } message UpdateChannelMembersResponse { @@ -669,10 +675,12 @@ message UpdateChannelMembersResponse { } message ReparentAgentRequest { - // The agent to move. - string agent_account_id = 1; - // The new parent; empty promotes the agent to a root. - string new_parent_agent_id = 2; + // The agent to move. A `@handle`; the server resolves it to an account id; + // unknown → NOT_FOUND. + string agent_handle = 1; + // The new parent; empty promotes the agent to a root. A `@handle`; the server + // resolves it to an account id; unknown → NOT_FOUND. + string new_parent_handle = 2; } message ReparentAgentResponse { @@ -684,8 +692,9 @@ message SetChannelPolicyRequest { string channel_id = 1; // The new post policy. ChannelPostPolicy post_policy = 2; - // The owner/operator account for the channel; empty leaves it unowned. - string owner_account_id = 3; + // The owner/operator account for the channel; empty leaves it unowned. A + // `@handle`; the server resolves it to an account id; unknown → NOT_FOUND. + string owner_handle = 3; // Whether membership implies a non-togglable subscription. bool mandatory_subscription = 4; } @@ -698,9 +707,10 @@ message SetChannelPolicyResponse { message GetRosterRequest { // The vantage the roster is computed around. RosterScope scope = 1; - // The agent whose vantage to use. Optional for human/UI callers naming a - // vantage point; an agent caller gets it session-resolved server-side. - string agent_account_id = 2; + // The vantage whose roster to compute. A `@handle`; empty ⇒ the caller's own + // vantage (an agent caller is session-resolved server-side), non-empty ⇒ the + // server resolves it to an account id; unknown → NOT_FOUND. + string vantage_handle = 2; } // The scope of a roster read, relative to a vantage agent. @@ -754,8 +764,9 @@ message UpdatePinnedBoardResponse { } message OpenAgentWorkspaceRequest { - // The agent account to open the workspace (ACP surface) for. - string agent_account_id = 1; + // The agent account to open the workspace (ACP surface) for. A `@handle`; the + // server resolves it to an account id; unknown → NOT_FOUND. + string agent_handle = 1; } message OpenAgentWorkspaceResponse { diff --git a/proto/compass/v1/compass.proto b/proto/compass/v1/compass.proto index c7187bb5..af2963b3 100644 --- a/proto/compass/v1/compass.proto +++ b/proto/compass/v1/compass.proto @@ -561,10 +561,10 @@ message AgentSessionFrame { // workstream. Agent ref + repo/workstream spec in, container_name out — the // handle StartAgentSession then brings online (design: architecture-lineage). message ProvisionAgentWorkspaceRequest { - // The owned agent account this workstream belongs to (the AgentAccount id - // from CommsService). Names whose credentials + home channel the container - // is provisioned for. - string agent_account_id = 1; + // The owned agent account this workstream belongs to. A `@handle` + // (owner-qualified, e.g. `matt/compass-ux`); the server resolves it to an + // account id; unknown → NOT_FOUND. + string agent_handle = 1; // Repo carriage removed (SEA-1527, Matt 2026-07-29): spawn/provision no longer // clone a repo for the agent. The container is provisioned with a git // credential + workspace and the agent self-clones whatever it needs after @@ -644,10 +644,10 @@ message StartAgentSessionResponse { // SpawnAgent: the composite start — Provision then Start under one // client_request_id, the single call a UI makes to bring an agent online. message SpawnAgentRequest { - // The owned agent account to bring online (the AgentAccount id from - // CommsService). The container is provisioned for, and the session bound to, - // this account. - string agent_account_id = 1; + // The owned agent account to bring online. A `@handle` (owner-qualified, + // e.g. `matt/compass-ux`); the server resolves it to an account id; unknown + // → NOT_FOUND. + string agent_handle = 1; reserved 2; reserved "initial_prompt"; // End-to-end idempotency key. A retry with the same id returns the same @@ -696,10 +696,10 @@ message GetAgentStatusResponse { // IssueToken: the admin-only path to mint a bearer token for an account. message IssueTokenRequest { - // The account to mint a token for — the id of an Account created via - // CommsService. The caller's identity is the authenticated connection, never - // a field here. - string account_id = 1; + // The account to mint a token for — a `@handle` naming a user or agent; the + // server resolves it to an account id; unknown → NOT_FOUND. The caller's + // identity is the authenticated connection, never a field here. + string account_handle = 1; } message IssueTokenResponse {