diff --git a/go/internal/comms/agent_caller.go b/go/internal/comms/agent_caller.go index 1464c527..8b053b7f 100644 --- a/go/internal/comms/agent_caller.go +++ b/go/internal/comms/agent_caller.go @@ -272,6 +272,72 @@ func (c *Comms) UpdatePinnedBoardAsAccount( return resp.Msg, nil } +// CreateChannelAsAccount executes one agent-initiated CreateChannel as account, +// mirroring UpdatePinnedBoardAsAccount: WithActor + the shared CreateChannel +// handler path, so channel authz, the store ops (including expandOwnerMembership, +// which seats the actor as a founding member so the Manager can immediately post +// to the channel it made), and the ChannelChanged fan-out are identical to a +// human caller's. A group the agent cannot see collapses to the same code a human +// gets. The request names no channel, so there is no home-channel defaulting. +func (c *Comms) CreateChannelAsAccount( + ctx context.Context, + account store.AccountID, + req *compassv1.CreateChannelRequest, +) (*compassv1.CreateChannelResponse, error) { + if account == "" { + return nil, errNoActor + } + resp, err := c.CreateChannel(WithActor(ctx, account), connect.NewRequest(req)) + if err != nil { + return nil, err + } + return resp.Msg, nil +} + +// UpdateChannelMembersAsAccount executes one agent-initiated UpdateChannelMembers +// as account, mirroring UpdatePinnedBoardAsAccount: WithActor + the shared +// UpdateChannelMembers handler path, so the membership authz, the store ops, and +// the ChannelChanged fan-out are identical to a human caller's. A non-member or +// invisible channel collapses to the same code a human gets. The request always +// names its channel explicitly (channel_id), so there is no home-channel +// defaulting here. +func (c *Comms) UpdateChannelMembersAsAccount( + ctx context.Context, + account store.AccountID, + req *compassv1.UpdateChannelMembersRequest, +) (*compassv1.UpdateChannelMembersResponse, error) { + if account == "" { + return nil, errNoActor + } + resp, err := c.UpdateChannelMembers(WithActor(ctx, account), connect.NewRequest(req)) + if err != nil { + return nil, err + } + return resp.Msg, nil +} + +// CreateChannelGroupAsAccount executes one agent-initiated CreateChannelGroup as +// account, mirroring UpdatePinnedBoardAsAccount: WithActor + the shared +// CreateChannelGroup handler path, so the group is created under the agent's +// account (D9 owner-scoped by requireGroupCreateAuthz, resolved server-side from +// the actor context) and the ChannelGroupChanged fan-out is identical to a human +// caller's. The request names no channel (only an optional parent group), so +// there is no home-channel defaulting here. +func (c *Comms) CreateChannelGroupAsAccount( + ctx context.Context, + account store.AccountID, + req *compassv1.CreateChannelGroupRequest, +) (*compassv1.CreateChannelGroupResponse, error) { + if account == "" { + return nil, errNoActor + } + resp, err := c.CreateChannelGroup(WithActor(ctx, account), connect.NewRequest(req)) + if err != nil { + return nil, err + } + return resp.Msg, nil +} + // CommitAgentPost commits one relayed MessagePosted frame as a durable comms row // under account. It builds a PostMessageRequest from the frame's blocks and // delegates to PostAsAccount, so this is the SAME PostMessage handler path a diff --git a/go/internal/comms/org_mgmt_pgtest_test.go b/go/internal/comms/org_mgmt_pgtest_test.go new file mode 100644 index 00000000..ab4a3fae --- /dev/null +++ b/go/internal/comms/org_mgmt_pgtest_test.go @@ -0,0 +1,271 @@ +//go:build pgtest + +package comms + +// Agent-initiated org-management adapter contracts (RIG-2673 T2): the +// CreateChannelAsAccount / UpdateChannelMembersAsAccount / CreateChannelGroupAsAccount +// adapters run the SAME handler path a human caller takes under WithActor, so +// authz collapses to the same codes (an invisible/non-member target → the code a +// human gets), an empty account short-circuits to errNoActor (CodeInvalidArgument), +// and founding membership + the ChannelChanged fan-out are identical. Driven +// in-process against a real store and bus. context.Background() is the test root +// (test-root ctx exemption). + +import ( + "context" + "slices" + "testing" + + "connectrpc.com/connect" + + compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/store" +) + +// TestCreateChannelAsAccountSeatsFounderAndEmitsChannelChanged: an agent creates +// a channel in a group its owning user owns → the Channel is returned, the +// creating agent account IS in the returned member set (founding membership via +// expandOwnerMembership, which is what lets the Manager immediately post to the +// channel it made), and a ChannelChanged is fanned out. +func TestCreateChannelAsAccountSeatsFounderAndEmitsChannelChanged(t *testing.T) { + h := newStreamHarness(t) + ctx := context.Background() + owner := mustUser(t, h.store, "owner") + agent := mustAgent(t, h.store, owner.ID, "manager") + grp, err := h.store.CreateChannelGroup(ctx, owner.ID, store.NewChannelGroup{Name: "team", Visibility: store.VisibilityOwner}) + if err != nil { + t.Fatalf("CreateChannelGroup: %v", err) + } + + events := firstEventAfterBoundary(t, h, owner.ID, &compassv1.SubscribeCommsRequest{SinceSeq: 0}) + + resp, err := h.svc.CreateChannelAsAccount(ctx, agent.ID, &compassv1.CreateChannelRequest{ + Name: "room", Kind: compassv1.ChannelKind_CHANNEL_KIND_CHANNEL, GroupId: string(grp.ID), + }) + if err != nil { + t.Fatalf("CreateChannelAsAccount: %v", err) + } + members := resp.GetChannel().GetMemberAccountIds() + if !slices.Contains(members, string(agent.ID)) { + t.Fatalf("founding member set = %v, want it to contain the creating agent %q", members, agent.ID) + } + + got := awaitFirst(t, events) + cc := got.GetChannelChanged() + if cc == nil { + t.Fatalf("event payload = %T, want ChannelChanged", got.GetPayload()) + } + if cc.GetChannel().GetId() != resp.GetChannel().GetId() { + t.Fatalf("ChannelChanged channel id = %q, want %q", cc.GetChannel().GetId(), resp.GetChannel().GetId()) + } +} + +// TestCreateChannelAsAccountEmptyAccountIsNoActor: an empty account short-circuits +// to errNoActor (CodeInvalidArgument) before any handler work. +func TestCreateChannelAsAccountEmptyAccountIsNoActor(t *testing.T) { + svc, _ := newHandler(t) + _, err := svc.CreateChannelAsAccount(context.Background(), "", &compassv1.CreateChannelRequest{Name: "room"}) + connectCodeIs(t, err, connect.CodeInvalidArgument, "CreateChannelAsAccount empty account") +} + +// TestCreateChannelAsAccountInvisibleGroupIsNotFound: an agent creating a channel +// in a group it neither owns nor may see collapses to the SAME CodeNotFound a +// human non-owner gets — the agent never learns the group exists. +func TestCreateChannelAsAccountInvisibleGroupIsNotFound(t *testing.T) { + svc, st := newHandler(t) + ctx := context.Background() + owner := mustUser(t, st, "owner") + stranger := mustUser(t, st, "stranger") + strangerAgent := mustAgent(t, st, stranger.ID, "outsider") + grp, err := st.CreateChannelGroup(ctx, owner.ID, store.NewChannelGroup{Name: "private", Visibility: store.VisibilityOwner}) + if err != nil { + t.Fatalf("CreateChannelGroup: %v", err) + } + + _, err = svc.CreateChannelAsAccount(ctx, strangerAgent.ID, &compassv1.CreateChannelRequest{ + Name: "intrusion", Kind: compassv1.ChannelKind_CHANNEL_KIND_CHANNEL, GroupId: string(grp.ID), + }) + connectCodeIs(t, err, connect.CodeNotFound, "CreateChannelAsAccount in invisible group") +} + +// TestUpdateChannelMembersAsAccountAddsMember: an agent adds a member to a channel +// it authored (and so can mutate) → the updated Channel carries the new member, +// and a ChannelChanged is fanned out (parity with the human caller's path). +func TestUpdateChannelMembersAsAccountAddsMember(t *testing.T) { + h := newStreamHarness(t) + ctx := context.Background() + owner := mustUser(t, h.store, "owner") + agent := mustAgent(t, h.store, owner.ID, "manager") + newcomer := mustUser(t, h.store, "newcomer") + + ch, err := h.store.CreateChannel(ctx, agent.ID, store.NewChannel{Name: "room", Kind: store.ChannelKindChannel}) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + + events := firstEventAfterBoundary(t, h, owner.ID, &compassv1.SubscribeCommsRequest{SinceSeq: 0}) + + resp, err := h.svc.UpdateChannelMembersAsAccount(ctx, agent.ID, &compassv1.UpdateChannelMembersRequest{ + ChannelId: string(ch.ID), + AddMemberHandles: []string{newcomer.Handle}, + }) + if err != nil { + t.Fatalf("UpdateChannelMembersAsAccount: %v", err) + } + if !slices.Contains(resp.GetChannel().GetMemberAccountIds(), string(newcomer.ID)) { + t.Fatalf("member set = %v, want it to contain the added %q", resp.GetChannel().GetMemberAccountIds(), newcomer.ID) + } + + got := awaitFirst(t, events) + cc := got.GetChannelChanged() + if cc == nil { + t.Fatalf("event payload = %T, want ChannelChanged", got.GetPayload()) + } + if cc.GetChannel().GetId() != resp.GetChannel().GetId() { + t.Fatalf("ChannelChanged channel id = %q, want %q", cc.GetChannel().GetId(), resp.GetChannel().GetId()) + } +} + +// TestUpdateChannelMembersAsAccountUnknownMemberHandleIsNotFound: an agent adds a +// member naming a handle that resolves to no account → the T3 batch resolver +// (AccountsByHandles, OQ-2) fails the whole call with the oracle-safe CodeNotFound +// naming the submitted handle, identical to the code a human caller gets. The +// org-management adapter inherits that resolution; it never partially applies a +// member set with an unresolved handle in it. +func TestUpdateChannelMembersAsAccountUnknownMemberHandleIsNotFound(t *testing.T) { + svc, st := newHandler(t) + ctx := context.Background() + owner := mustUser(t, st, "owner") + agent := mustAgent(t, st, owner.ID, "manager") + + ch, err := st.CreateChannel(ctx, agent.ID, store.NewChannel{Name: "room", Kind: store.ChannelKindChannel}) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + + _, err = svc.UpdateChannelMembersAsAccount(ctx, agent.ID, &compassv1.UpdateChannelMembersRequest{ + ChannelId: string(ch.ID), + AddMemberHandles: []string{"ghost"}, + }) + connectCodeIs(t, err, connect.CodeNotFound, "UpdateChannelMembersAsAccount with an unresolvable member handle") +} + +// TestUpdateChannelMembersAsAccountInvisibleMemberHandleIsNotFound: an agent adds +// a member naming a handle that IS a real account but one the caller cannot see — +// an agent living only under a DIFFERENT owner's per-owner namespace (DL-271). The +// bare handle misses the global user index and misses the caller-owner agent index, +// so it resolves to nothing and collapses to the SAME oracle-safe CodeNotFound an +// entirely-unknown handle gets: the caller cannot distinguish "no such handle" from +// "a handle I'm not allowed to see", so it cannot probe another owner's roster by +// naming its agents as members. +func TestUpdateChannelMembersAsAccountInvisibleMemberHandleIsNotFound(t *testing.T) { + svc, st := newHandler(t) + ctx := context.Background() + owner := mustUser(t, st, "owner") + agent := mustAgent(t, st, owner.ID, "manager") + // A second owner and its agent — a real account under a foreign per-owner + // namespace, invisible to the caller's bare-handle resolution. + otherOwner := mustUser(t, st, "other") + otherAgent := mustAgent(t, st, otherOwner.ID, "hidden") + + ch, err := st.CreateChannel(ctx, agent.ID, store.NewChannel{Name: "room", Kind: store.ChannelKindChannel}) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + + _, err = svc.UpdateChannelMembersAsAccount(ctx, agent.ID, &compassv1.UpdateChannelMembersRequest{ + ChannelId: string(ch.ID), + AddMemberHandles: []string{otherAgent.Handle}, + }) + connectCodeIs(t, err, connect.CodeNotFound, "UpdateChannelMembersAsAccount with a foreign-owner (invisible) member handle") +} + +// TestUpdateChannelMembersAsAccountEmptyAccountIsNoActor: an empty account → +// errNoActor (CodeInvalidArgument). +func TestUpdateChannelMembersAsAccountEmptyAccountIsNoActor(t *testing.T) { + svc, _ := newHandler(t) + _, err := svc.UpdateChannelMembersAsAccount(context.Background(), "", &compassv1.UpdateChannelMembersRequest{ChannelId: "ch-1"}) + connectCodeIs(t, err, connect.CodeInvalidArgument, "UpdateChannelMembersAsAccount empty account") +} + +// TestUpdateChannelMembersAsAccountNonMemberIsNotFound: an agent mutating a +// channel it cannot see collapses to the SAME CodeNotFound a human non-member gets. +func TestUpdateChannelMembersAsAccountNonMemberIsNotFound(t *testing.T) { + svc, st := newHandler(t) + ctx := context.Background() + owner := mustUser(t, st, "owner") + stranger := mustUser(t, st, "stranger") + strangerAgent := mustAgent(t, st, stranger.ID, "outsider") + + ch, err := st.CreateChannel(ctx, owner.ID, store.NewChannel{Name: "room", Kind: store.ChannelKindChannel}) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + + _, err = svc.UpdateChannelMembersAsAccount(ctx, strangerAgent.ID, &compassv1.UpdateChannelMembersRequest{ + ChannelId: string(ch.ID), + AddMemberHandles: []string{stranger.Handle}, + }) + connectCodeIs(t, err, connect.CodeNotFound, "UpdateChannelMembersAsAccount on invisible channel") +} + +// TestCreateChannelGroupAsAccountReturnsGroup: an agent creates a top-level group +// → the ChannelGroup is returned, created under the agent's account (the store +// stamps owner = the actor account, resolved server-side from the actor context), +// and a ChannelGroupChanged is fanned out (parity with the human caller's path). +func TestCreateChannelGroupAsAccountReturnsGroup(t *testing.T) { + h := newStreamHarness(t) + ctx := context.Background() + owner := mustUser(t, h.store, "owner") + agent := mustAgent(t, h.store, owner.ID, "manager") + + events := firstEventAfterBoundary(t, h, agent.ID, &compassv1.SubscribeCommsRequest{SinceSeq: 0}) + + resp, err := h.svc.CreateChannelGroupAsAccount(ctx, agent.ID, &compassv1.CreateChannelGroupRequest{Name: "team"}) + if err != nil { + t.Fatalf("CreateChannelGroupAsAccount: %v", err) + } + if resp.GetGroup().GetId() == "" { + t.Fatalf("returned group has no id") + } + if resp.GetGroup().GetName() != "team" { + t.Fatalf("returned group name = %q, want team", resp.GetGroup().GetName()) + } + + got := awaitFirst(t, events) + cg := got.GetChannelGroupChanged() + if cg == nil { + t.Fatalf("event payload = %T, want ChannelGroupChanged", got.GetPayload()) + } + if cg.GetGroup().GetId() != resp.GetGroup().GetId() { + t.Fatalf("ChannelGroupChanged group id = %q, want %q", cg.GetGroup().GetId(), resp.GetGroup().GetId()) + } +} + +// TestCreateChannelGroupAsAccountEmptyAccountIsNoActor: an empty account → +// errNoActor (CodeInvalidArgument). +func TestCreateChannelGroupAsAccountEmptyAccountIsNoActor(t *testing.T) { + svc, _ := newHandler(t) + _, err := svc.CreateChannelGroupAsAccount(context.Background(), "", &compassv1.CreateChannelGroupRequest{Name: "team"}) + connectCodeIs(t, err, connect.CodeInvalidArgument, "CreateChannelGroupAsAccount empty account") +} + +// TestCreateChannelGroupAsAccountInvisibleParentIsNotFound: an agent creating a +// group under a parent it neither owns nor may see collapses to the SAME +// CodeNotFound a human non-owner gets. +func TestCreateChannelGroupAsAccountInvisibleParentIsNotFound(t *testing.T) { + svc, st := newHandler(t) + ctx := context.Background() + owner := mustUser(t, st, "owner") + stranger := mustUser(t, st, "stranger") + strangerAgent := mustAgent(t, st, stranger.ID, "outsider") + parent, err := st.CreateChannelGroup(ctx, owner.ID, store.NewChannelGroup{Name: "private", Visibility: store.VisibilityOwner}) + if err != nil { + t.Fatalf("CreateChannelGroup: %v", err) + } + + _, err = svc.CreateChannelGroupAsAccount(ctx, strangerAgent.ID, &compassv1.CreateChannelGroupRequest{ + Name: "child", ParentGroupId: string(parent.ID), Visibility: compassv1.ChannelGroupVisibility_CHANNEL_GROUP_VISIBILITY_OWNER, + }) + connectCodeIs(t, err, connect.CodeNotFound, "CreateChannelGroupAsAccount under invisible parent") +} diff --git a/go/internal/gen/compass/v1/agent_gateway.pb.go b/go/internal/gen/compass/v1/agent_gateway.pb.go index a511809d..701a50ec 100644 --- a/go/internal/gen/compass/v1/agent_gateway.pb.go +++ b/go/internal/gen/compass/v1/agent_gateway.pb.go @@ -121,6 +121,9 @@ type CommsCallRequest struct { // *CommsCallRequest_Roster // *CommsCallRequest_SetStatus // *CommsCallRequest_Pin + // *CommsCallRequest_CreateChannel + // *CommsCallRequest_UpdateMembers + // *CommsCallRequest_CreateChannelGroup Call isCommsCallRequest_Call `protobuf_oneof:"call"` // The W3C `traceparent` of the delivered message that triggered this outbound // call, re-attached by the agent so the Server can LINK the reply's fresh @@ -226,6 +229,33 @@ func (x *CommsCallRequest) GetPin() *v1.UpdatePinnedBoardRequest { return nil } +func (x *CommsCallRequest) GetCreateChannel() *v1.CreateChannelRequest { + if x != nil { + if x, ok := x.Call.(*CommsCallRequest_CreateChannel); ok { + return x.CreateChannel + } + } + return nil +} + +func (x *CommsCallRequest) GetUpdateMembers() *v1.UpdateChannelMembersRequest { + if x != nil { + if x, ok := x.Call.(*CommsCallRequest_UpdateMembers); ok { + return x.UpdateMembers + } + } + return nil +} + +func (x *CommsCallRequest) GetCreateChannelGroup() *v1.CreateChannelGroupRequest { + if x != nil { + if x, ok := x.Call.(*CommsCallRequest_CreateChannelGroup); ok { + return x.CreateChannelGroup + } + } + return nil +} + func (x *CommsCallRequest) GetTriggerTraceparent() string { if x != nil { return x.TriggerTraceparent @@ -257,6 +287,18 @@ type CommsCallRequest_Pin struct { Pin *v1.UpdatePinnedBoardRequest `protobuf:"bytes,6,opt,name=pin,proto3,oneof"` } +type CommsCallRequest_CreateChannel struct { + CreateChannel *v1.CreateChannelRequest `protobuf:"bytes,7,opt,name=create_channel,json=createChannel,proto3,oneof"` +} + +type CommsCallRequest_UpdateMembers struct { + UpdateMembers *v1.UpdateChannelMembersRequest `protobuf:"bytes,8,opt,name=update_members,json=updateMembers,proto3,oneof"` +} + +type CommsCallRequest_CreateChannelGroup struct { + CreateChannelGroup *v1.CreateChannelGroupRequest `protobuf:"bytes,9,opt,name=create_channel_group,json=createChannelGroup,proto3,oneof"` +} + func (*CommsCallRequest_Post) isCommsCallRequest_Call() {} func (*CommsCallRequest_List) isCommsCallRequest_Call() {} @@ -267,10 +309,19 @@ func (*CommsCallRequest_SetStatus) isCommsCallRequest_Call() {} func (*CommsCallRequest_Pin) isCommsCallRequest_Call() {} +func (*CommsCallRequest_CreateChannel) isCommsCallRequest_Call() {} + +func (*CommsCallRequest_UpdateMembers) isCommsCallRequest_Call() {} + +func (*CommsCallRequest_CreateChannelGroup) isCommsCallRequest_Call() {} + // The result of one comms call, correlated by `call_id`. A successful call sets -// `post`/`list`; an in-band failure (a tool error — non-member channel, bad -// input) sets `error`, which is NOT a transport teardown. The same message is -// the `RelayCommsCallResponse.result` payload on the Runner->Server leg. +// the response arm matching the request's operation (`post`, `list`, `roster`, +// `set_status`, `pin`, `create_channel`, `update_members`, +// `create_channel_group`); an in-band failure (a tool error — non-member +// channel, bad input) sets `error`, which is NOT a transport teardown. The same +// message is the `RelayCommsCallResponse.result` payload on the Runner->Server +// leg. type CommsCallResult struct { state protoimpl.MessageState `protogen:"open.v1"` CallId string `protobuf:"bytes,1,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` @@ -282,6 +333,9 @@ type CommsCallResult struct { // *CommsCallResult_Roster // *CommsCallResult_SetStatus // *CommsCallResult_Pin + // *CommsCallResult_CreateChannel + // *CommsCallResult_UpdateMembers + // *CommsCallResult_CreateChannelGroup Result isCommsCallResult_Result `protobuf_oneof:"result"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -385,6 +439,33 @@ func (x *CommsCallResult) GetPin() *v1.UpdatePinnedBoardResponse { return nil } +func (x *CommsCallResult) GetCreateChannel() *v1.CreateChannelResponse { + if x != nil { + if x, ok := x.Result.(*CommsCallResult_CreateChannel); ok { + return x.CreateChannel + } + } + return nil +} + +func (x *CommsCallResult) GetUpdateMembers() *v1.UpdateChannelMembersResponse { + if x != nil { + if x, ok := x.Result.(*CommsCallResult_UpdateMembers); ok { + return x.UpdateMembers + } + } + return nil +} + +func (x *CommsCallResult) GetCreateChannelGroup() *v1.CreateChannelGroupResponse { + if x != nil { + if x, ok := x.Result.(*CommsCallResult_CreateChannelGroup); ok { + return x.CreateChannelGroup + } + } + return nil +} + type isCommsCallResult_Result interface { isCommsCallResult_Result() } @@ -413,6 +494,18 @@ type CommsCallResult_Pin struct { Pin *v1.UpdatePinnedBoardResponse `protobuf:"bytes,7,opt,name=pin,proto3,oneof"` } +type CommsCallResult_CreateChannel struct { + CreateChannel *v1.CreateChannelResponse `protobuf:"bytes,8,opt,name=create_channel,json=createChannel,proto3,oneof"` +} + +type CommsCallResult_UpdateMembers struct { + UpdateMembers *v1.UpdateChannelMembersResponse `protobuf:"bytes,9,opt,name=update_members,json=updateMembers,proto3,oneof"` +} + +type CommsCallResult_CreateChannelGroup struct { + CreateChannelGroup *v1.CreateChannelGroupResponse `protobuf:"bytes,10,opt,name=create_channel_group,json=createChannelGroup,proto3,oneof"` +} + func (*CommsCallResult_Post) isCommsCallResult_Result() {} func (*CommsCallResult_List) isCommsCallResult_Result() {} @@ -425,6 +518,12 @@ func (*CommsCallResult_SetStatus) isCommsCallResult_Result() {} func (*CommsCallResult_Pin) isCommsCallResult_Result() {} +func (*CommsCallResult_CreateChannel) isCommsCallResult_Result() {} + +func (*CommsCallResult_UpdateMembers) isCommsCallResult_Result() {} + +func (*CommsCallResult_CreateChannelGroup) isCommsCallResult_Result() {} + // An in-band comms-call failure: a tool error the agent renders to the model, // never a stream teardown. `code` is a short stable token (e.g. "not_found"); // `message` is human-readable detail. @@ -667,8 +766,13 @@ type SpawnPeerRequest struct { Handle string `protobuf:"bytes,1,opt,name=handle,proto3" json:"handle,omitempty"` // the new agent's account handle (unique) DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` ClientRequestId string `protobuf:"bytes,4,opt,name=client_request_id,json=clientRequestId,proto3" json:"client_request_id,omitempty"` // whole-chain idempotency key (handler-level join + Provision dedup) - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Optional role/persona for the spawned agent, applied at creation only + // (org-management Manager creation, RIG-2673). Empty = the server's default + // role/persona for a peer. + Role string `protobuf:"bytes,5,opt,name=role,proto3" json:"role,omitempty"` + Persona string `protobuf:"bytes,6,opt,name=persona,proto3" json:"persona,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SpawnPeerRequest) Reset() { @@ -722,6 +826,20 @@ func (x *SpawnPeerRequest) GetClientRequestId() string { return "" } +func (x *SpawnPeerRequest) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *SpawnPeerRequest) GetPersona() string { + if x != nil { + return x.Persona + } + return "" +} + type SpawnPeerResponse 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"` @@ -2950,7 +3068,7 @@ var File_compass_v1_agent_gateway_proto protoreflect.FileDescriptor const file_compass_v1_agent_gateway_proto_rawDesc = "" + "\n" + "\x1ecompass/v1/agent_gateway.proto\x12\n" + - "compass.v1\x1a\x16compass/v1/comms.proto\x1a\x16compass/v1/agent.proto\x1a\x18compass/v1/compass.proto\x1a\x16compass/v1/forge.proto\"\x87\x03\n" + + "compass.v1\x1a\x16compass/v1/comms.proto\x1a\x16compass/v1/agent.proto\x1a\x18compass/v1/compass.proto\x1a\x16compass/v1/forge.proto\"\xff\x04\n" + "\x10CommsCallRequest\x12\x17\n" + "\acall_id\x18\x01 \x01(\tR\x06callId\x124\n" + "\x04post\x18\x02 \x01(\v2\x1e.compass.v1.PostMessageRequestH\x00R\x04post\x125\n" + @@ -2958,10 +3076,13 @@ const file_compass_v1_agent_gateway_proto_rawDesc = "" + "\x06roster\x18\x04 \x01(\v2\x1c.compass.v1.GetRosterRequestH\x00R\x06roster\x12B\n" + "\n" + "set_status\x18\x05 \x01(\v2!.compass.v1.SetAgentStatusRequestH\x00R\tsetStatus\x128\n" + - "\x03pin\x18\x06 \x01(\v2$.compass.v1.UpdatePinnedBoardRequestH\x00R\x03pin\x12/\n" + + "\x03pin\x18\x06 \x01(\v2$.compass.v1.UpdatePinnedBoardRequestH\x00R\x03pin\x12I\n" + + "\x0ecreate_channel\x18\a \x01(\v2 .compass.v1.CreateChannelRequestH\x00R\rcreateChannel\x12P\n" + + "\x0eupdate_members\x18\b \x01(\v2'.compass.v1.UpdateChannelMembersRequestH\x00R\rupdateMembers\x12Y\n" + + "\x14create_channel_group\x18\t \x01(\v2%.compass.v1.CreateChannelGroupRequestH\x00R\x12createChannelGroup\x12/\n" + "\x13trigger_traceparent\x18\n" + " \x01(\tR\x12triggerTraceparentB\x06\n" + - "\x04call\"\x90\x03\n" + + "\x04call\"\x8b\x05\n" + "\x0fCommsCallResult\x12\x17\n" + "\acall_id\x18\x01 \x01(\tR\x06callId\x125\n" + "\x04post\x18\x02 \x01(\v2\x1f.compass.v1.PostMessageResponseH\x00R\x04post\x126\n" + @@ -2970,7 +3091,11 @@ const file_compass_v1_agent_gateway_proto_rawDesc = "" + "\x06roster\x18\x05 \x01(\v2\x1d.compass.v1.GetRosterResponseH\x00R\x06roster\x12C\n" + "\n" + "set_status\x18\x06 \x01(\v2\".compass.v1.SetAgentStatusResponseH\x00R\tsetStatus\x129\n" + - "\x03pin\x18\a \x01(\v2%.compass.v1.UpdatePinnedBoardResponseH\x00R\x03pinB\b\n" + + "\x03pin\x18\a \x01(\v2%.compass.v1.UpdatePinnedBoardResponseH\x00R\x03pin\x12J\n" + + "\x0ecreate_channel\x18\b \x01(\v2!.compass.v1.CreateChannelResponseH\x00R\rcreateChannel\x12Q\n" + + "\x0eupdate_members\x18\t \x01(\v2(.compass.v1.UpdateChannelMembersResponseH\x00R\rupdateMembers\x12Z\n" + + "\x14create_channel_group\x18\n" + + " \x01(\v2&.compass.v1.CreateChannelGroupResponseH\x00R\x12createChannelGroupB\b\n" + "\x06result\">\n" + "\x0eCommsCallError\x12\x12\n" + "\x04code\x18\x01 \x01(\tR\x04code\x12\x18\n" + @@ -2982,11 +3107,13 @@ const file_compass_v1_agent_gateway_proto_rawDesc = "" + "\acall_id\x18\x01 \x01(\tR\x06callId\x124\n" + "\x05spawn\x18\x02 \x01(\v2\x1c.compass.v1.SpawnPeerRequestH\x00R\x05spawn\x12:\n" + "\adespawn\x18\x03 \x01(\v2\x1e.compass.v1.DespawnPeerRequestH\x00R\adespawnB\x06\n" + - "\x04call\"\x8f\x01\n" + + "\x04call\"\xbd\x01\n" + "\x10SpawnPeerRequest\x12\x16\n" + "\x06handle\x18\x01 \x01(\tR\x06handle\x12!\n" + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12*\n" + - "\x11client_request_id\x18\x04 \x01(\tR\x0fclientRequestIdJ\x04\b\x03\x10\x04R\x0einitial_prompt\"\x83\x01\n" + + "\x11client_request_id\x18\x04 \x01(\tR\x0fclientRequestId\x12\x12\n" + + "\x04role\x18\x05 \x01(\tR\x04role\x12\x18\n" + + "\apersona\x18\x06 \x01(\tR\apersonaJ\x04\b\x03\x10\x04R\x0einitial_prompt\"\x83\x01\n" + "\x11SpawnPeerResponse\x12(\n" + "\x10agent_account_id\x18\x01 \x01(\tR\x0eagentAccountId\x12%\n" + "\x0econtainer_name\x18\x02 \x01(\tR\rcontainerName\x12\x1d\n" + @@ -3152,63 +3279,69 @@ func file_compass_v1_agent_gateway_proto_rawDescGZIP() []byte { var file_compass_v1_agent_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_compass_v1_agent_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 39) var file_compass_v1_agent_gateway_proto_goTypes = []any{ - (ForgeSubscriptionScope)(0), // 0: compass.v1.ForgeSubscriptionScope - (*CommsCallRequest)(nil), // 1: compass.v1.CommsCallRequest - (*CommsCallResult)(nil), // 2: compass.v1.CommsCallResult - (*CommsCallError)(nil), // 3: compass.v1.CommsCallError - (*SetAgentStatusRequest)(nil), // 4: compass.v1.SetAgentStatusRequest - (*SetAgentStatusResponse)(nil), // 5: compass.v1.SetAgentStatusResponse - (*LifecycleCallRequest)(nil), // 6: compass.v1.LifecycleCallRequest - (*SpawnPeerRequest)(nil), // 7: compass.v1.SpawnPeerRequest - (*SpawnPeerResponse)(nil), // 8: compass.v1.SpawnPeerResponse - (*DespawnPeerRequest)(nil), // 9: compass.v1.DespawnPeerRequest - (*DespawnPeerResponse)(nil), // 10: compass.v1.DespawnPeerResponse - (*LifecycleCallResult)(nil), // 11: compass.v1.LifecycleCallResult - (*LifecycleCallError)(nil), // 12: compass.v1.LifecycleCallError - (*ForgeCallRequest)(nil), // 13: compass.v1.ForgeCallRequest - (*ForgeCallResult)(nil), // 14: compass.v1.ForgeCallResult - (*ForgeCallError)(nil), // 15: compass.v1.ForgeCallError - (*CreateIssueRequest)(nil), // 16: compass.v1.CreateIssueRequest - (*CommentOnIssueRequest)(nil), // 17: compass.v1.CommentOnIssueRequest - (*GetIssueRequest)(nil), // 18: compass.v1.GetIssueRequest - (*ListIssuesRequest)(nil), // 19: compass.v1.ListIssuesRequest - (*ListIssuesResponse)(nil), // 20: compass.v1.ListIssuesResponse - (*CreatePullRequestRequest)(nil), // 21: compass.v1.CreatePullRequestRequest - (*CommentOnPullRequestRequest)(nil), // 22: compass.v1.CommentOnPullRequestRequest - (*GetPullRequestRequest)(nil), // 23: compass.v1.GetPullRequestRequest - (*SubmitReviewRequest)(nil), // 24: compass.v1.SubmitReviewRequest - (*ReviewCommentInput)(nil), // 25: compass.v1.ReviewCommentInput - (*SubscribeForgeRequest)(nil), // 26: compass.v1.SubscribeForgeRequest - (*SubscribeForgeResponse)(nil), // 27: compass.v1.SubscribeForgeResponse - (*UnsubscribeForgeRequest)(nil), // 28: compass.v1.UnsubscribeForgeRequest - (*UnsubscribeForgeResponse)(nil), // 29: compass.v1.UnsubscribeForgeResponse - (*BoardCallRequest)(nil), // 30: compass.v1.BoardCallRequest - (*SetIssueStateRequest)(nil), // 31: compass.v1.SetIssueStateRequest - (*SetIssueStateResponse)(nil), // 32: compass.v1.SetIssueStateResponse - (*BoardCallResult)(nil), // 33: compass.v1.BoardCallResult - (*BoardCallError)(nil), // 34: compass.v1.BoardCallError - (*PublishFrameRequest)(nil), // 35: compass.v1.PublishFrameRequest - (*PublishFrameResponse)(nil), // 36: compass.v1.PublishFrameResponse - (*PostConversationFrameRequest)(nil), // 37: compass.v1.PostConversationFrameRequest - (*PostConversationFrameResponse)(nil), // 38: compass.v1.PostConversationFrameResponse - (*ControlSubscribeRequest)(nil), // 39: compass.v1.ControlSubscribeRequest - (*v1.PostMessageRequest)(nil), // 40: compass.v1.PostMessageRequest - (*v1.ListMessagesRequest)(nil), // 41: compass.v1.ListMessagesRequest - (*v1.GetRosterRequest)(nil), // 42: compass.v1.GetRosterRequest - (*v1.UpdatePinnedBoardRequest)(nil), // 43: compass.v1.UpdatePinnedBoardRequest - (*v1.PostMessageResponse)(nil), // 44: compass.v1.PostMessageResponse - (*v1.ListMessagesResponse)(nil), // 45: compass.v1.ListMessagesResponse - (*v1.GetRosterResponse)(nil), // 46: compass.v1.GetRosterResponse - (*v1.UpdatePinnedBoardResponse)(nil), // 47: compass.v1.UpdatePinnedBoardResponse - (*v1.ForgeRef)(nil), // 48: compass.v1.ForgeRef - (*v1.Issue)(nil), // 49: compass.v1.Issue - (*CommentRef)(nil), // 50: compass.v1.CommentRef - (*v1.PullRequest)(nil), // 51: compass.v1.PullRequest - (*ReviewRef)(nil), // 52: compass.v1.ReviewRef - (ForgeArtifactKind)(0), // 53: compass.v1.ForgeArtifactKind - (v1.IssueState)(0), // 54: compass.v1.IssueState - (*AgentFrame)(nil), // 55: compass.v1.AgentFrame - (*AgentControl)(nil), // 56: compass.v1.AgentControl + (ForgeSubscriptionScope)(0), // 0: compass.v1.ForgeSubscriptionScope + (*CommsCallRequest)(nil), // 1: compass.v1.CommsCallRequest + (*CommsCallResult)(nil), // 2: compass.v1.CommsCallResult + (*CommsCallError)(nil), // 3: compass.v1.CommsCallError + (*SetAgentStatusRequest)(nil), // 4: compass.v1.SetAgentStatusRequest + (*SetAgentStatusResponse)(nil), // 5: compass.v1.SetAgentStatusResponse + (*LifecycleCallRequest)(nil), // 6: compass.v1.LifecycleCallRequest + (*SpawnPeerRequest)(nil), // 7: compass.v1.SpawnPeerRequest + (*SpawnPeerResponse)(nil), // 8: compass.v1.SpawnPeerResponse + (*DespawnPeerRequest)(nil), // 9: compass.v1.DespawnPeerRequest + (*DespawnPeerResponse)(nil), // 10: compass.v1.DespawnPeerResponse + (*LifecycleCallResult)(nil), // 11: compass.v1.LifecycleCallResult + (*LifecycleCallError)(nil), // 12: compass.v1.LifecycleCallError + (*ForgeCallRequest)(nil), // 13: compass.v1.ForgeCallRequest + (*ForgeCallResult)(nil), // 14: compass.v1.ForgeCallResult + (*ForgeCallError)(nil), // 15: compass.v1.ForgeCallError + (*CreateIssueRequest)(nil), // 16: compass.v1.CreateIssueRequest + (*CommentOnIssueRequest)(nil), // 17: compass.v1.CommentOnIssueRequest + (*GetIssueRequest)(nil), // 18: compass.v1.GetIssueRequest + (*ListIssuesRequest)(nil), // 19: compass.v1.ListIssuesRequest + (*ListIssuesResponse)(nil), // 20: compass.v1.ListIssuesResponse + (*CreatePullRequestRequest)(nil), // 21: compass.v1.CreatePullRequestRequest + (*CommentOnPullRequestRequest)(nil), // 22: compass.v1.CommentOnPullRequestRequest + (*GetPullRequestRequest)(nil), // 23: compass.v1.GetPullRequestRequest + (*SubmitReviewRequest)(nil), // 24: compass.v1.SubmitReviewRequest + (*ReviewCommentInput)(nil), // 25: compass.v1.ReviewCommentInput + (*SubscribeForgeRequest)(nil), // 26: compass.v1.SubscribeForgeRequest + (*SubscribeForgeResponse)(nil), // 27: compass.v1.SubscribeForgeResponse + (*UnsubscribeForgeRequest)(nil), // 28: compass.v1.UnsubscribeForgeRequest + (*UnsubscribeForgeResponse)(nil), // 29: compass.v1.UnsubscribeForgeResponse + (*BoardCallRequest)(nil), // 30: compass.v1.BoardCallRequest + (*SetIssueStateRequest)(nil), // 31: compass.v1.SetIssueStateRequest + (*SetIssueStateResponse)(nil), // 32: compass.v1.SetIssueStateResponse + (*BoardCallResult)(nil), // 33: compass.v1.BoardCallResult + (*BoardCallError)(nil), // 34: compass.v1.BoardCallError + (*PublishFrameRequest)(nil), // 35: compass.v1.PublishFrameRequest + (*PublishFrameResponse)(nil), // 36: compass.v1.PublishFrameResponse + (*PostConversationFrameRequest)(nil), // 37: compass.v1.PostConversationFrameRequest + (*PostConversationFrameResponse)(nil), // 38: compass.v1.PostConversationFrameResponse + (*ControlSubscribeRequest)(nil), // 39: compass.v1.ControlSubscribeRequest + (*v1.PostMessageRequest)(nil), // 40: compass.v1.PostMessageRequest + (*v1.ListMessagesRequest)(nil), // 41: compass.v1.ListMessagesRequest + (*v1.GetRosterRequest)(nil), // 42: compass.v1.GetRosterRequest + (*v1.UpdatePinnedBoardRequest)(nil), // 43: compass.v1.UpdatePinnedBoardRequest + (*v1.CreateChannelRequest)(nil), // 44: compass.v1.CreateChannelRequest + (*v1.UpdateChannelMembersRequest)(nil), // 45: compass.v1.UpdateChannelMembersRequest + (*v1.CreateChannelGroupRequest)(nil), // 46: compass.v1.CreateChannelGroupRequest + (*v1.PostMessageResponse)(nil), // 47: compass.v1.PostMessageResponse + (*v1.ListMessagesResponse)(nil), // 48: compass.v1.ListMessagesResponse + (*v1.GetRosterResponse)(nil), // 49: compass.v1.GetRosterResponse + (*v1.UpdatePinnedBoardResponse)(nil), // 50: compass.v1.UpdatePinnedBoardResponse + (*v1.CreateChannelResponse)(nil), // 51: compass.v1.CreateChannelResponse + (*v1.UpdateChannelMembersResponse)(nil), // 52: compass.v1.UpdateChannelMembersResponse + (*v1.CreateChannelGroupResponse)(nil), // 53: compass.v1.CreateChannelGroupResponse + (*v1.ForgeRef)(nil), // 54: compass.v1.ForgeRef + (*v1.Issue)(nil), // 55: compass.v1.Issue + (*CommentRef)(nil), // 56: compass.v1.CommentRef + (*v1.PullRequest)(nil), // 57: compass.v1.PullRequest + (*ReviewRef)(nil), // 58: compass.v1.ReviewRef + (ForgeArtifactKind)(0), // 59: compass.v1.ForgeArtifactKind + (v1.IssueState)(0), // 60: compass.v1.IssueState + (*AgentFrame)(nil), // 61: compass.v1.AgentFrame + (*AgentControl)(nil), // 62: compass.v1.AgentControl } var file_compass_v1_agent_gateway_proto_depIdxs = []int32{ 40, // 0: compass.v1.CommsCallRequest.post:type_name -> compass.v1.PostMessageRequest @@ -3216,67 +3349,73 @@ var file_compass_v1_agent_gateway_proto_depIdxs = []int32{ 42, // 2: compass.v1.CommsCallRequest.roster:type_name -> compass.v1.GetRosterRequest 4, // 3: compass.v1.CommsCallRequest.set_status:type_name -> compass.v1.SetAgentStatusRequest 43, // 4: compass.v1.CommsCallRequest.pin:type_name -> compass.v1.UpdatePinnedBoardRequest - 44, // 5: compass.v1.CommsCallResult.post:type_name -> compass.v1.PostMessageResponse - 45, // 6: compass.v1.CommsCallResult.list:type_name -> compass.v1.ListMessagesResponse - 3, // 7: compass.v1.CommsCallResult.error:type_name -> compass.v1.CommsCallError - 46, // 8: compass.v1.CommsCallResult.roster:type_name -> compass.v1.GetRosterResponse - 5, // 9: compass.v1.CommsCallResult.set_status:type_name -> compass.v1.SetAgentStatusResponse - 47, // 10: compass.v1.CommsCallResult.pin:type_name -> compass.v1.UpdatePinnedBoardResponse - 7, // 11: compass.v1.LifecycleCallRequest.spawn:type_name -> compass.v1.SpawnPeerRequest - 9, // 12: compass.v1.LifecycleCallRequest.despawn:type_name -> compass.v1.DespawnPeerRequest - 8, // 13: compass.v1.LifecycleCallResult.spawn:type_name -> compass.v1.SpawnPeerResponse - 10, // 14: compass.v1.LifecycleCallResult.despawn:type_name -> compass.v1.DespawnPeerResponse - 12, // 15: compass.v1.LifecycleCallResult.error:type_name -> compass.v1.LifecycleCallError - 16, // 16: compass.v1.ForgeCallRequest.create_issue:type_name -> compass.v1.CreateIssueRequest - 17, // 17: compass.v1.ForgeCallRequest.comment_on_issue:type_name -> compass.v1.CommentOnIssueRequest - 18, // 18: compass.v1.ForgeCallRequest.get_issue:type_name -> compass.v1.GetIssueRequest - 19, // 19: compass.v1.ForgeCallRequest.list_issues:type_name -> compass.v1.ListIssuesRequest - 21, // 20: compass.v1.ForgeCallRequest.create_pull_request:type_name -> compass.v1.CreatePullRequestRequest - 22, // 21: compass.v1.ForgeCallRequest.comment_on_pull_request:type_name -> compass.v1.CommentOnPullRequestRequest - 23, // 22: compass.v1.ForgeCallRequest.get_pull_request:type_name -> compass.v1.GetPullRequestRequest - 26, // 23: compass.v1.ForgeCallRequest.subscribe:type_name -> compass.v1.SubscribeForgeRequest - 28, // 24: compass.v1.ForgeCallRequest.unsubscribe:type_name -> compass.v1.UnsubscribeForgeRequest - 24, // 25: compass.v1.ForgeCallRequest.submit_review:type_name -> compass.v1.SubmitReviewRequest - 48, // 26: compass.v1.ForgeCallRequest.forge:type_name -> compass.v1.ForgeRef - 49, // 27: compass.v1.ForgeCallResult.issue:type_name -> compass.v1.Issue - 50, // 28: compass.v1.ForgeCallResult.issue_comment:type_name -> compass.v1.CommentRef - 20, // 29: compass.v1.ForgeCallResult.issues:type_name -> compass.v1.ListIssuesResponse - 51, // 30: compass.v1.ForgeCallResult.pull_request:type_name -> compass.v1.PullRequest - 50, // 31: compass.v1.ForgeCallResult.pr_comment:type_name -> compass.v1.CommentRef - 27, // 32: compass.v1.ForgeCallResult.subscribed:type_name -> compass.v1.SubscribeForgeResponse - 29, // 33: compass.v1.ForgeCallResult.unsubscribed:type_name -> compass.v1.UnsubscribeForgeResponse - 15, // 34: compass.v1.ForgeCallResult.error:type_name -> compass.v1.ForgeCallError - 52, // 35: compass.v1.ForgeCallResult.review:type_name -> compass.v1.ReviewRef - 49, // 36: compass.v1.ListIssuesResponse.issues:type_name -> compass.v1.Issue - 25, // 37: compass.v1.SubmitReviewRequest.comments:type_name -> compass.v1.ReviewCommentInput - 53, // 38: compass.v1.SubscribeForgeRequest.kind:type_name -> compass.v1.ForgeArtifactKind - 0, // 39: compass.v1.SubscribeForgeRequest.scope:type_name -> compass.v1.ForgeSubscriptionScope - 31, // 40: compass.v1.BoardCallRequest.set_issue_state:type_name -> compass.v1.SetIssueStateRequest - 54, // 41: compass.v1.SetIssueStateRequest.state:type_name -> compass.v1.IssueState - 49, // 42: compass.v1.SetIssueStateResponse.issue:type_name -> compass.v1.Issue - 32, // 43: compass.v1.BoardCallResult.set_issue_state:type_name -> compass.v1.SetIssueStateResponse - 34, // 44: compass.v1.BoardCallResult.error:type_name -> compass.v1.BoardCallError - 55, // 45: compass.v1.PublishFrameRequest.frame:type_name -> compass.v1.AgentFrame - 55, // 46: compass.v1.PostConversationFrameRequest.frame:type_name -> compass.v1.AgentFrame - 1, // 47: compass.v1.AgentGateway.Comms:input_type -> compass.v1.CommsCallRequest - 6, // 48: compass.v1.AgentGateway.Lifecycle:input_type -> compass.v1.LifecycleCallRequest - 35, // 49: compass.v1.AgentGateway.Publish:input_type -> compass.v1.PublishFrameRequest - 37, // 50: compass.v1.AgentGateway.PostConversationFrame:input_type -> compass.v1.PostConversationFrameRequest - 39, // 51: compass.v1.AgentGateway.Control:input_type -> compass.v1.ControlSubscribeRequest - 13, // 52: compass.v1.AgentGateway.Forge:input_type -> compass.v1.ForgeCallRequest - 30, // 53: compass.v1.AgentGateway.Board:input_type -> compass.v1.BoardCallRequest - 2, // 54: compass.v1.AgentGateway.Comms:output_type -> compass.v1.CommsCallResult - 11, // 55: compass.v1.AgentGateway.Lifecycle:output_type -> compass.v1.LifecycleCallResult - 36, // 56: compass.v1.AgentGateway.Publish:output_type -> compass.v1.PublishFrameResponse - 38, // 57: compass.v1.AgentGateway.PostConversationFrame:output_type -> compass.v1.PostConversationFrameResponse - 56, // 58: compass.v1.AgentGateway.Control:output_type -> compass.v1.AgentControl - 14, // 59: compass.v1.AgentGateway.Forge:output_type -> compass.v1.ForgeCallResult - 33, // 60: compass.v1.AgentGateway.Board:output_type -> compass.v1.BoardCallResult - 54, // [54:61] is the sub-list for method output_type - 47, // [47:54] is the sub-list for method input_type - 47, // [47:47] is the sub-list for extension type_name - 47, // [47:47] is the sub-list for extension extendee - 0, // [0:47] is the sub-list for field type_name + 44, // 5: compass.v1.CommsCallRequest.create_channel:type_name -> compass.v1.CreateChannelRequest + 45, // 6: compass.v1.CommsCallRequest.update_members:type_name -> compass.v1.UpdateChannelMembersRequest + 46, // 7: compass.v1.CommsCallRequest.create_channel_group:type_name -> compass.v1.CreateChannelGroupRequest + 47, // 8: compass.v1.CommsCallResult.post:type_name -> compass.v1.PostMessageResponse + 48, // 9: compass.v1.CommsCallResult.list:type_name -> compass.v1.ListMessagesResponse + 3, // 10: compass.v1.CommsCallResult.error:type_name -> compass.v1.CommsCallError + 49, // 11: compass.v1.CommsCallResult.roster:type_name -> compass.v1.GetRosterResponse + 5, // 12: compass.v1.CommsCallResult.set_status:type_name -> compass.v1.SetAgentStatusResponse + 50, // 13: compass.v1.CommsCallResult.pin:type_name -> compass.v1.UpdatePinnedBoardResponse + 51, // 14: compass.v1.CommsCallResult.create_channel:type_name -> compass.v1.CreateChannelResponse + 52, // 15: compass.v1.CommsCallResult.update_members:type_name -> compass.v1.UpdateChannelMembersResponse + 53, // 16: compass.v1.CommsCallResult.create_channel_group:type_name -> compass.v1.CreateChannelGroupResponse + 7, // 17: compass.v1.LifecycleCallRequest.spawn:type_name -> compass.v1.SpawnPeerRequest + 9, // 18: compass.v1.LifecycleCallRequest.despawn:type_name -> compass.v1.DespawnPeerRequest + 8, // 19: compass.v1.LifecycleCallResult.spawn:type_name -> compass.v1.SpawnPeerResponse + 10, // 20: compass.v1.LifecycleCallResult.despawn:type_name -> compass.v1.DespawnPeerResponse + 12, // 21: compass.v1.LifecycleCallResult.error:type_name -> compass.v1.LifecycleCallError + 16, // 22: compass.v1.ForgeCallRequest.create_issue:type_name -> compass.v1.CreateIssueRequest + 17, // 23: compass.v1.ForgeCallRequest.comment_on_issue:type_name -> compass.v1.CommentOnIssueRequest + 18, // 24: compass.v1.ForgeCallRequest.get_issue:type_name -> compass.v1.GetIssueRequest + 19, // 25: compass.v1.ForgeCallRequest.list_issues:type_name -> compass.v1.ListIssuesRequest + 21, // 26: compass.v1.ForgeCallRequest.create_pull_request:type_name -> compass.v1.CreatePullRequestRequest + 22, // 27: compass.v1.ForgeCallRequest.comment_on_pull_request:type_name -> compass.v1.CommentOnPullRequestRequest + 23, // 28: compass.v1.ForgeCallRequest.get_pull_request:type_name -> compass.v1.GetPullRequestRequest + 26, // 29: compass.v1.ForgeCallRequest.subscribe:type_name -> compass.v1.SubscribeForgeRequest + 28, // 30: compass.v1.ForgeCallRequest.unsubscribe:type_name -> compass.v1.UnsubscribeForgeRequest + 24, // 31: compass.v1.ForgeCallRequest.submit_review:type_name -> compass.v1.SubmitReviewRequest + 54, // 32: compass.v1.ForgeCallRequest.forge:type_name -> compass.v1.ForgeRef + 55, // 33: compass.v1.ForgeCallResult.issue:type_name -> compass.v1.Issue + 56, // 34: compass.v1.ForgeCallResult.issue_comment:type_name -> compass.v1.CommentRef + 20, // 35: compass.v1.ForgeCallResult.issues:type_name -> compass.v1.ListIssuesResponse + 57, // 36: compass.v1.ForgeCallResult.pull_request:type_name -> compass.v1.PullRequest + 56, // 37: compass.v1.ForgeCallResult.pr_comment:type_name -> compass.v1.CommentRef + 27, // 38: compass.v1.ForgeCallResult.subscribed:type_name -> compass.v1.SubscribeForgeResponse + 29, // 39: compass.v1.ForgeCallResult.unsubscribed:type_name -> compass.v1.UnsubscribeForgeResponse + 15, // 40: compass.v1.ForgeCallResult.error:type_name -> compass.v1.ForgeCallError + 58, // 41: compass.v1.ForgeCallResult.review:type_name -> compass.v1.ReviewRef + 55, // 42: compass.v1.ListIssuesResponse.issues:type_name -> compass.v1.Issue + 25, // 43: compass.v1.SubmitReviewRequest.comments:type_name -> compass.v1.ReviewCommentInput + 59, // 44: compass.v1.SubscribeForgeRequest.kind:type_name -> compass.v1.ForgeArtifactKind + 0, // 45: compass.v1.SubscribeForgeRequest.scope:type_name -> compass.v1.ForgeSubscriptionScope + 31, // 46: compass.v1.BoardCallRequest.set_issue_state:type_name -> compass.v1.SetIssueStateRequest + 60, // 47: compass.v1.SetIssueStateRequest.state:type_name -> compass.v1.IssueState + 55, // 48: compass.v1.SetIssueStateResponse.issue:type_name -> compass.v1.Issue + 32, // 49: compass.v1.BoardCallResult.set_issue_state:type_name -> compass.v1.SetIssueStateResponse + 34, // 50: compass.v1.BoardCallResult.error:type_name -> compass.v1.BoardCallError + 61, // 51: compass.v1.PublishFrameRequest.frame:type_name -> compass.v1.AgentFrame + 61, // 52: compass.v1.PostConversationFrameRequest.frame:type_name -> compass.v1.AgentFrame + 1, // 53: compass.v1.AgentGateway.Comms:input_type -> compass.v1.CommsCallRequest + 6, // 54: compass.v1.AgentGateway.Lifecycle:input_type -> compass.v1.LifecycleCallRequest + 35, // 55: compass.v1.AgentGateway.Publish:input_type -> compass.v1.PublishFrameRequest + 37, // 56: compass.v1.AgentGateway.PostConversationFrame:input_type -> compass.v1.PostConversationFrameRequest + 39, // 57: compass.v1.AgentGateway.Control:input_type -> compass.v1.ControlSubscribeRequest + 13, // 58: compass.v1.AgentGateway.Forge:input_type -> compass.v1.ForgeCallRequest + 30, // 59: compass.v1.AgentGateway.Board:input_type -> compass.v1.BoardCallRequest + 2, // 60: compass.v1.AgentGateway.Comms:output_type -> compass.v1.CommsCallResult + 11, // 61: compass.v1.AgentGateway.Lifecycle:output_type -> compass.v1.LifecycleCallResult + 36, // 62: compass.v1.AgentGateway.Publish:output_type -> compass.v1.PublishFrameResponse + 38, // 63: compass.v1.AgentGateway.PostConversationFrame:output_type -> compass.v1.PostConversationFrameResponse + 62, // 64: compass.v1.AgentGateway.Control:output_type -> compass.v1.AgentControl + 14, // 65: compass.v1.AgentGateway.Forge:output_type -> compass.v1.ForgeCallResult + 33, // 66: compass.v1.AgentGateway.Board:output_type -> compass.v1.BoardCallResult + 60, // [60:67] is the sub-list for method output_type + 53, // [53:60] is the sub-list for method input_type + 53, // [53:53] is the sub-list for extension type_name + 53, // [53:53] is the sub-list for extension extendee + 0, // [0:53] is the sub-list for field type_name } func init() { file_compass_v1_agent_gateway_proto_init() } @@ -3292,6 +3431,9 @@ func file_compass_v1_agent_gateway_proto_init() { (*CommsCallRequest_Roster)(nil), (*CommsCallRequest_SetStatus)(nil), (*CommsCallRequest_Pin)(nil), + (*CommsCallRequest_CreateChannel)(nil), + (*CommsCallRequest_UpdateMembers)(nil), + (*CommsCallRequest_CreateChannelGroup)(nil), } file_compass_v1_agent_gateway_proto_msgTypes[1].OneofWrappers = []any{ (*CommsCallResult_Post)(nil), @@ -3300,6 +3442,9 @@ func file_compass_v1_agent_gateway_proto_init() { (*CommsCallResult_Roster)(nil), (*CommsCallResult_SetStatus)(nil), (*CommsCallResult_Pin)(nil), + (*CommsCallResult_CreateChannel)(nil), + (*CommsCallResult_UpdateMembers)(nil), + (*CommsCallResult_CreateChannelGroup)(nil), } file_compass_v1_agent_gateway_proto_msgTypes[5].OneofWrappers = []any{ (*LifecycleCallRequest_Spawn)(nil), diff --git a/go/internal/runnerhub/helpers_test.go b/go/internal/runnerhub/helpers_test.go index d6e43e53..a4fbedef 100644 --- a/go/internal/runnerhub/helpers_test.go +++ b/go/internal/runnerhub/helpers_test.go @@ -182,6 +182,10 @@ type commsCall struct { roster *compassv1.GetRosterRequest setStatus string pin *compassv1.UpdatePinnedBoardRequest + + createChannel *compassv1.CreateChannelRequest + updateMembers *compassv1.UpdateChannelMembersRequest + createChannelGroup *compassv1.CreateChannelGroupRequest } // fakeCommsCaller is a hand-written CommsCaller: it records every call (account @@ -210,6 +214,15 @@ type fakeCommsCaller struct { setStatusReturned string pinResp *compassv1.UpdatePinnedBoardResponse pinErr error + + createChannelResp *compassv1.CreateChannelResponse + createChannelErr error + + updateMembersResp *compassv1.UpdateChannelMembersResponse + updateMembersErr error + + createChannelGroupResp *compassv1.CreateChannelGroupResponse + createChannelGroupErr error } func (f *fakeCommsCaller) PostAsAccount(_ context.Context, account store.AccountID, req *compassv1.PostMessageRequest) (*compassv1.PostMessageResponse, error) { @@ -285,6 +298,36 @@ func (f *fakeCommsCaller) UpdatePinnedBoardAsAccount(_ context.Context, account return f.pinResp, nil } +func (f *fakeCommsCaller) CreateChannelAsAccount(_ context.Context, account store.AccountID, req *compassv1.CreateChannelRequest) (*compassv1.CreateChannelResponse, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.calls = append(f.calls, commsCall{account: account, createChannel: req}) + if f.createChannelErr != nil { + return nil, f.createChannelErr + } + return f.createChannelResp, nil +} + +func (f *fakeCommsCaller) UpdateChannelMembersAsAccount(_ context.Context, account store.AccountID, req *compassv1.UpdateChannelMembersRequest) (*compassv1.UpdateChannelMembersResponse, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.calls = append(f.calls, commsCall{account: account, updateMembers: req}) + if f.updateMembersErr != nil { + return nil, f.updateMembersErr + } + return f.updateMembersResp, nil +} + +func (f *fakeCommsCaller) CreateChannelGroupAsAccount(_ context.Context, account store.AccountID, req *compassv1.CreateChannelGroupRequest) (*compassv1.CreateChannelGroupResponse, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.calls = append(f.calls, commsCall{account: account, createChannelGroup: req}) + if f.createChannelGroupErr != nil { + return nil, f.createChannelGroupErr + } + return f.createChannelGroupResp, nil +} + func (f *fakeCommsCaller) snapshot() []commsCall { f.mu.Lock() defer f.mu.Unlock() diff --git a/go/internal/runnerhub/hub.go b/go/internal/runnerhub/hub.go index 30921590..b20a81a2 100644 --- a/go/internal/runnerhub/hub.go +++ b/go/internal/runnerhub/hub.go @@ -255,6 +255,9 @@ type CommsCaller interface { // half of the set_status ordered write-then-publish (RIG-1721 T2 / T3). SetStatusAsAccount(ctx context.Context, account store.AccountID, activity string) (string, error) UpdatePinnedBoardAsAccount(ctx context.Context, account store.AccountID, req *compassv1.UpdatePinnedBoardRequest) (*compassv1.UpdatePinnedBoardResponse, error) + CreateChannelAsAccount(ctx context.Context, account store.AccountID, req *compassv1.CreateChannelRequest) (*compassv1.CreateChannelResponse, error) + UpdateChannelMembersAsAccount(ctx context.Context, account store.AccountID, req *compassv1.UpdateChannelMembersRequest) (*compassv1.UpdateChannelMembersResponse, error) + CreateChannelGroupAsAccount(ctx context.Context, account store.AccountID, req *compassv1.CreateChannelGroupRequest) (*compassv1.CreateChannelGroupResponse, error) } // Hub is the Server-side seam: enrollment registry + command router + the diff --git a/go/internal/runnerhub/relay_comms.go b/go/internal/runnerhub/relay_comms.go index c18fbf6d..2a8e772f 100644 --- a/go/internal/runnerhub/relay_comms.go +++ b/go/internal/runnerhub/relay_comms.go @@ -459,10 +459,34 @@ func (h *Hub) executeCall( return &compassv1internal.CommsCallResult{ Result: &compassv1internal.CommsCallResult_Pin{Pin: resp}, }, nil + case *compassv1internal.CommsCallRequest_CreateChannel: + resp, err := h.comms.CreateChannelAsAccount(ctx, account, c.CreateChannel) + if err != nil { + return nil, err + } + return &compassv1internal.CommsCallResult{ + Result: &compassv1internal.CommsCallResult_CreateChannel{CreateChannel: resp}, + }, nil + case *compassv1internal.CommsCallRequest_UpdateMembers: + resp, err := h.comms.UpdateChannelMembersAsAccount(ctx, account, c.UpdateMembers) + if err != nil { + return nil, err + } + return &compassv1internal.CommsCallResult{ + Result: &compassv1internal.CommsCallResult_UpdateMembers{UpdateMembers: resp}, + }, nil + case *compassv1internal.CommsCallRequest_CreateChannelGroup: + resp, err := h.comms.CreateChannelGroupAsAccount(ctx, account, c.CreateChannelGroup) + if err != nil { + return nil, err + } + return &compassv1internal.CommsCallResult{ + Result: &compassv1internal.CommsCallResult_CreateChannelGroup{CreateChannelGroup: resp}, + }, nil default: return nil, connect.NewError( connect.CodeInvalidArgument, - errors.New("runnerhub: comms call has no recognized variant set (post/list/roster/set_status/pin)"), + errors.New("runnerhub: comms call has no recognized variant set (post/list/roster/set_status/pin/create_channel/update_members/create_channel_group)"), ) } } diff --git a/go/internal/runnerhub/relay_org_mgmt_test.go b/go/internal/runnerhub/relay_org_mgmt_test.go new file mode 100644 index 00000000..c89a0129 --- /dev/null +++ b/go/internal/runnerhub/relay_org_mgmt_test.go @@ -0,0 +1,222 @@ +//go:build unix + +package runnerhub + +// The org-management relay arms (RIG-2673 T3): RelayCommsCall dispatches a +// create_channel call to CreateChannelAsAccount, an update_members call to +// UpdateChannelMembersAsAccount, and a create_channel_group call to +// CreateChannelGroupAsAccount — each under the bound account, wrapping the +// matching result oneof, with call_id round-tripped. A tool error on an arm is +// rendered in-band as a CommsCallError, never a transport teardown. Driven +// through the fakeCommsCaller, no store. context.Background() is the test root +// (test-root ctx exemption). + +import ( + "context" + "errors" + "testing" + + "connectrpc.com/connect" + + compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" + compassv1internal "github.com/RigelBuild/compass/go/internal/gen/compass/v1" +) + +// relayCreateChannel builds a RelayCommsCallRequest carrying a create_channel variant. +func relayCreateChannel(sessionID, callID string, req *compassv1.CreateChannelRequest) *compassv1internal.RelayCommsCallRequest { + return &compassv1internal.RelayCommsCallRequest{ + SessionId: sessionID, + Call: &compassv1internal.CommsCallRequest{ + CallId: callID, + Call: &compassv1internal.CommsCallRequest_CreateChannel{CreateChannel: req}, + }, + } +} + +// relayUpdateMembers builds a RelayCommsCallRequest carrying an update_members variant. +func relayUpdateMembers(sessionID, callID string, req *compassv1.UpdateChannelMembersRequest) *compassv1internal.RelayCommsCallRequest { + return &compassv1internal.RelayCommsCallRequest{ + SessionId: sessionID, + Call: &compassv1internal.CommsCallRequest{ + CallId: callID, + Call: &compassv1internal.CommsCallRequest_UpdateMembers{UpdateMembers: req}, + }, + } +} + +// relayCreateChannelGroup builds a RelayCommsCallRequest carrying a create_channel_group variant. +func relayCreateChannelGroup(sessionID, callID string, req *compassv1.CreateChannelGroupRequest) *compassv1internal.RelayCommsCallRequest { + return &compassv1internal.RelayCommsCallRequest{ + SessionId: sessionID, + Call: &compassv1internal.CommsCallRequest{ + CallId: callID, + Call: &compassv1internal.CommsCallRequest_CreateChannelGroup{CreateChannelGroup: req}, + }, + } +} + +// TestRelayCommsCallCreateChannelArmForwardsUnderBoundAccount: a create_channel +// call forwards the request under the bound account and wraps the create_channel +// result oneof, with call_id round-tripped. +func TestRelayCommsCallCreateChannelArmForwardsUnderBoundAccount(t *testing.T) { + hub, comms := newHubWithComms() + comms.createChannelResp = &compassv1.CreateChannelResponse{Channel: &compassv1.Channel{Id: "ch-1"}} + bindLiveSession(hub) + + req := &compassv1.CreateChannelRequest{Name: "room"} + resp, err := hub.RelayCommsCall(context.Background(), relayCreateChannel("sess-1", "tc-cc", req)) + if err != nil { + t.Fatalf("RelayCommsCall(create_channel) = %v, want success", err) + } + calls := comms.snapshot() + if len(calls) != 1 { + t.Fatalf("caller invoked %d times, want 1", len(calls)) + } + if calls[0].account != "acct-agent" { + t.Fatalf("create_channel attributed to %q, want bound acct-agent", calls[0].account) + } + if calls[0].createChannel != req { + t.Fatalf("caller received a different CreateChannelRequest than relayed") + } + if resp.GetResult().GetCreateChannel() != comms.createChannelResp { + t.Fatalf("result oneof = %T, want the caller's create_channel response", resp.GetResult().GetResult()) + } + if got := resp.GetResult().GetCallId(); got != "tc-cc" { + t.Fatalf("response call_id = %q, want tc-cc", got) + } +} + +// TestRelayCommsCallUpdateMembersArmForwardsUnderBoundAccount: an update_members +// call forwards under the bound account and wraps the update_members result oneof. +func TestRelayCommsCallUpdateMembersArmForwardsUnderBoundAccount(t *testing.T) { + hub, comms := newHubWithComms() + comms.updateMembersResp = &compassv1.UpdateChannelMembersResponse{Channel: &compassv1.Channel{Id: "ch-1"}} + bindLiveSession(hub) + + req := &compassv1.UpdateChannelMembersRequest{ChannelId: "ch-1", AddMemberHandles: []string{"a-2"}} + resp, err := hub.RelayCommsCall(context.Background(), relayUpdateMembers("sess-1", "tc-um", req)) + if err != nil { + t.Fatalf("RelayCommsCall(update_members) = %v, want success", err) + } + calls := comms.snapshot() + if len(calls) != 1 { + t.Fatalf("caller invoked %d times, want 1", len(calls)) + } + if calls[0].account != "acct-agent" { + t.Fatalf("update_members attributed to %q, want bound acct-agent", calls[0].account) + } + if calls[0].updateMembers != req { + t.Fatalf("caller received a different UpdateChannelMembersRequest than relayed") + } + if resp.GetResult().GetUpdateMembers() != comms.updateMembersResp { + t.Fatalf("result oneof = %T, want the caller's update_members response", resp.GetResult().GetResult()) + } + if got := resp.GetResult().GetCallId(); got != "tc-um" { + t.Fatalf("response call_id = %q, want tc-um", got) + } +} + +// TestRelayCommsCallCreateChannelGroupArmForwardsUnderBoundAccount: a +// create_channel_group call forwards under the bound account and wraps the +// create_channel_group result oneof. +func TestRelayCommsCallCreateChannelGroupArmForwardsUnderBoundAccount(t *testing.T) { + hub, comms := newHubWithComms() + comms.createChannelGroupResp = &compassv1.CreateChannelGroupResponse{Group: &compassv1.ChannelGroup{Id: "grp-1"}} + bindLiveSession(hub) + + req := &compassv1.CreateChannelGroupRequest{Name: "team"} + resp, err := hub.RelayCommsCall(context.Background(), relayCreateChannelGroup("sess-1", "tc-cg", req)) + if err != nil { + t.Fatalf("RelayCommsCall(create_channel_group) = %v, want success", err) + } + calls := comms.snapshot() + if len(calls) != 1 { + t.Fatalf("caller invoked %d times, want 1", len(calls)) + } + if calls[0].account != "acct-agent" { + t.Fatalf("create_channel_group attributed to %q, want bound acct-agent", calls[0].account) + } + if calls[0].createChannelGroup != req { + t.Fatalf("caller received a different CreateChannelGroupRequest than relayed") + } + if resp.GetResult().GetCreateChannelGroup() != comms.createChannelGroupResp { + t.Fatalf("result oneof = %T, want the caller's create_channel_group response", resp.GetResult().GetResult()) + } + if got := resp.GetResult().GetCallId(); got != "tc-cg" { + t.Fatalf("response call_id = %q, want tc-cg", got) + } +} + +// TestRelayCommsCallCreateChannelToolErrorIsInBandNotStreamError: a tool-level +// failure on the create_channel arm is rendered IN-BAND as a CommsCallError, not +// as a Connect stream error — the "tool failure != transport teardown" invariant +// on a new org-management arm (mirrors the post-arm invariant). +func TestRelayCommsCallCreateChannelToolErrorIsInBandNotStreamError(t *testing.T) { + hub, comms := newHubWithComms() + comms.createChannelErr = connect.NewError(connect.CodeNotFound, errors.New("group not found")) + bindLiveSession(hub) + + resp, err := hub.RelayCommsCall(context.Background(), relayCreateChannel("sess-1", "tc-cc-err", &compassv1.CreateChannelRequest{Name: "room"})) + if err != nil { + t.Fatalf("RelayCommsCall returned a stream error %v, want in-band tool error", err) + } + toolErr := resp.GetResult().GetError() + if toolErr == nil { + t.Fatal("response has no in-band CommsCallError, want the tool failure rendered in-band") + } + if toolErr.GetCode() != "not_found" { + t.Fatalf("in-band error code = %q, want not_found (the D9 collapse token)", toolErr.GetCode()) + } + if got := resp.GetResult().GetCallId(); got != "tc-cc-err" { + t.Fatalf("response call_id = %q, want tc-cc-err", got) + } +} + +// TestRelayCommsCallUpdateMembersToolErrorIsInBandNotStreamError: a tool-level +// failure on the update_members arm renders IN-BAND as a CommsCallError, not a +// Connect stream error — the same tool-failure-is-not-a-teardown invariant, on +// the update_members arm. +func TestRelayCommsCallUpdateMembersToolErrorIsInBandNotStreamError(t *testing.T) { + hub, comms := newHubWithComms() + comms.updateMembersErr = connect.NewError(connect.CodeNotFound, errors.New("channel not found")) + bindLiveSession(hub) + + resp, err := hub.RelayCommsCall(context.Background(), relayUpdateMembers("sess-1", "tc-um-err", &compassv1.UpdateChannelMembersRequest{ChannelId: "ch-x", AddMemberHandles: []string{"a-2"}})) + if err != nil { + t.Fatalf("RelayCommsCall returned a stream error %v, want in-band tool error", err) + } + toolErr := resp.GetResult().GetError() + if toolErr == nil { + t.Fatal("response has no in-band CommsCallError, want the tool failure rendered in-band") + } + if toolErr.GetCode() != "not_found" { + t.Fatalf("in-band error code = %q, want not_found (the D9 collapse token)", toolErr.GetCode()) + } + if got := resp.GetResult().GetCallId(); got != "tc-um-err" { + t.Fatalf("response call_id = %q, want tc-um-err", got) + } +} + +// TestRelayCommsCallCreateChannelGroupToolErrorIsInBandNotStreamError: a +// tool-level failure on the create_channel_group arm renders IN-BAND as a +// CommsCallError, not a Connect stream error. +func TestRelayCommsCallCreateChannelGroupToolErrorIsInBandNotStreamError(t *testing.T) { + hub, comms := newHubWithComms() + comms.createChannelGroupErr = connect.NewError(connect.CodeNotFound, errors.New("parent group not found")) + bindLiveSession(hub) + + resp, err := hub.RelayCommsCall(context.Background(), relayCreateChannelGroup("sess-1", "tc-cg-err", &compassv1.CreateChannelGroupRequest{Name: "team"})) + if err != nil { + t.Fatalf("RelayCommsCall returned a stream error %v, want in-band tool error", err) + } + toolErr := resp.GetResult().GetError() + if toolErr == nil { + t.Fatal("response has no in-band CommsCallError, want the tool failure rendered in-band") + } + if toolErr.GetCode() != "not_found" { + t.Fatalf("in-band error code = %q, want not_found (the D9 collapse token)", toolErr.GetCode()) + } + if got := resp.GetResult().GetCallId(); got != "tc-cg-err" { + t.Fatalf("response call_id = %q, want tc-cg-err", got) + } +} diff --git a/go/server/lifecycle.go b/go/server/lifecycle.go index 494eea9b..01cfd603 100644 --- a/go/server/lifecycle.go +++ b/go/server/lifecycle.go @@ -199,16 +199,18 @@ func (l *lifecycleService) SpawnAsAccount( 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 - // store account below, never the caller — a caller cannot inject a system - // prompt or a role prompt. + // Persona and role are set-at-creation from the spawn request (org-management + // Manager creation): under the D9 owner-acts model the caller's OWNER is the + // authority, so a Manager-creating spawn legitimately carries role+persona at + // creation. They are stored via CreateAgent (the source of record) and then + // threaded to the Runner from the CREATED store account below, never from the + // request directly — so an empty role+persona spawn is byte-identical to + // today's field-less spawn. created, err := l.store.CreateAgent(ctx, callerOwner, store.NewAgent{ Handle: req.GetHandle(), DisplayName: req.GetDisplayName(), - Persona: "", - Role: "", + Persona: req.GetPersona(), + Role: req.GetRole(), // Set-at-creation: the spawned peer's parent in the agent tree is its // spawner (§T3). A new account has no descendants, so this edge cannot // form a cycle — the cycle check lives only on the mutable ReparentAgent. diff --git a/go/server/spawn_role_persona_pgtest_test.go b/go/server/spawn_role_persona_pgtest_test.go new file mode 100644 index 00000000..29092a34 --- /dev/null +++ b/go/server/spawn_role_persona_pgtest_test.go @@ -0,0 +1,232 @@ +//go:build pgtest && unix + +package server + +// SpawnAsAccount role/persona thread-through (RIG-2673 T4): a Manager-creating +// spawn carries role+persona set-at-creation. The values are stored via +// store.CreateAgent (source of record) and threaded to the Runner's Provision +// wire from the CREATED store account — so a spawn with role/persona set lands +// them in agent_accounts AND on the Provision wire, and an empty-field spawn is +// byte-identical to today (empty on both). Idempotent re-spawn keeps the stored +// values. Driven through newLifecycleFixture directly, as the other spawn tests. +// context.Background() is the test root (test-root ctx exemption). + +import ( + "context" + "testing" + + compassv1internal "github.com/RigelBuild/compass/go/internal/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/store" +) + +// TestSpawnStoresAndThreadsRole: a spawn with role "manager" lands the role in +// the created agent_accounts row AND on the Provision wire (threaded from the +// store account, the source of record). +func TestSpawnStoresAndThreadsRole(t *testing.T) { + f := newLifecycleFixture(t) + ctx := context.Background() + + resp, err := f.lc.SpawnAsAccount(ctx, f.agentID, &compassv1internal.SpawnPeerRequest{ + Handle: "peer-role", + DisplayName: "Peer Role", + ClientRequestId: "spawn-role", + Role: "manager", + }) + if err != nil { + t.Fatalf("SpawnAsAccount = %v, want success", err) + } + newID := store.AccountID(resp.GetAgentAccountId()) + + acc, err := f.store.GetAccount(ctx, newID) + if err != nil { + t.Fatalf("GetAccount(spawned) = %v", err) + } + if got := acc.Agent.Role; got != "manager" { + t.Fatalf("stored role = %q, want manager (set-at-creation from the spawn request)", got) + } + if got := f.runner.provisionRole(t); got != "manager" { + t.Fatalf("Provision wire role = %q, want manager (threaded from the store account)", got) + } +} + +// TestSpawnStoresAndThreadsPersona: a spawn with a persona lands it in the +// created agent_accounts row AND on the Provision wire. +func TestSpawnStoresAndThreadsPersona(t *testing.T) { + f := newLifecycleFixture(t) + ctx := context.Background() + + const wantPersona = "You are a diligent manager." + resp, err := f.lc.SpawnAsAccount(ctx, f.agentID, &compassv1internal.SpawnPeerRequest{ + Handle: "peer-persona", + DisplayName: "Peer Persona", + ClientRequestId: "spawn-persona", + Persona: wantPersona, + }) + if err != nil { + t.Fatalf("SpawnAsAccount = %v, want success", err) + } + newID := store.AccountID(resp.GetAgentAccountId()) + + acc, err := f.store.GetAccount(ctx, newID) + if err != nil { + t.Fatalf("GetAccount(spawned) = %v", err) + } + if got := acc.Agent.Persona; got != wantPersona { + t.Fatalf("stored persona = %q, want %q (set-at-creation from the spawn request)", got, wantPersona) + } + if got := f.runner.provisionPersona(t); got != wantPersona { + t.Fatalf("Provision wire persona = %q, want %q (threaded from the store account)", got, wantPersona) + } +} + +// TestSpawnEmptyRolePersonaIsByteIdenticalToToday: a spawn naming neither role +// nor persona stores empty strings AND carries empty strings on the Provision +// wire — the field-less spawn is unchanged from pre-T4 behavior. +func TestSpawnEmptyRolePersonaIsByteIdenticalToToday(t *testing.T) { + f := newLifecycleFixture(t) + ctx := context.Background() + + resp, err := f.lc.SpawnAsAccount(ctx, f.agentID, &compassv1internal.SpawnPeerRequest{ + Handle: "peer-empty", + DisplayName: "Peer Empty", + ClientRequestId: "spawn-empty", + }) + if err != nil { + t.Fatalf("SpawnAsAccount = %v, want success", err) + } + newID := store.AccountID(resp.GetAgentAccountId()) + + acc, err := f.store.GetAccount(ctx, newID) + if err != nil { + t.Fatalf("GetAccount(spawned) = %v", err) + } + if got := acc.Agent.Role; got != "" { + t.Fatalf("stored role = %q, want empty for a field-less spawn", got) + } + if got := acc.Agent.Persona; got != "" { + t.Fatalf("stored persona = %q, want empty for a field-less spawn", got) + } + if got := f.runner.provisionRole(t); got != "" { + t.Fatalf("Provision wire role = %q, want empty for a field-less spawn", got) + } + if got := f.runner.provisionPersona(t); got != "" { + t.Fatalf("Provision wire persona = %q, want empty for a field-less spawn", got) + } +} + +// TestSpawnIdempotentReSpawnKeepsStoredRolePersona: a second spawn for the SAME +// handle+client_request_id, once the first is live, returns the SAME peer under +// the STORED role/persona — the re-spawn never rewrites them (set-at-creation). +func TestSpawnIdempotentReSpawnKeepsStoredRolePersona(t *testing.T) { + f := newLifecycleFixture(t) + ctx := context.Background() + + const ( + wantRole = "manager" + wantPersona = "You are a diligent manager." + ) + first, err := f.lc.SpawnAsAccount(ctx, f.agentID, &compassv1internal.SpawnPeerRequest{ + Handle: "peer-dup-rp", + ClientRequestId: "spawn-dup-rp", + Role: wantRole, + Persona: wantPersona, + }) + if err != nil { + t.Fatalf("first SpawnAsAccount = %v, want success", err) + } + + // A retry naming a DIFFERENT role/persona must not rewrite the stored values. + second, err := f.lc.SpawnAsAccount(ctx, f.agentID, &compassv1internal.SpawnPeerRequest{ + Handle: "peer-dup-rp", + ClientRequestId: "spawn-dup-rp", + Role: "impostor", + Persona: "different", + }) + if err != nil { + t.Fatalf("retry SpawnAsAccount = %v, want idempotent success", err) + } + if second.GetAgentAccountId() != first.GetAgentAccountId() { + t.Fatalf("retry returned a different agent id %q, want the first %q", second.GetAgentAccountId(), first.GetAgentAccountId()) + } + + acc, err := f.store.GetAccount(ctx, store.AccountID(first.GetAgentAccountId())) + if err != nil { + t.Fatalf("GetAccount(spawned) = %v", err) + } + if got := acc.Agent.Role; got != wantRole { + t.Fatalf("stored role after re-spawn = %q, want the first-spawn %q (set-at-creation, never rewritten)", got, wantRole) + } + if got := acc.Agent.Persona; got != wantPersona { + t.Fatalf("stored persona after re-spawn = %q, want the first-spawn %q (set-at-creation, never rewritten)", got, wantPersona) + } +} + +// TestSpawnResumeReprovisionThreadsStoredRolePersona covers the UNPLACED-resume +// branch (lifecycle.go resumeOrReject -> provisionAndStart with the EXISTING +// account's stored persona/role, design.md:215): a spawn that crashed after +// CreateAgent leaves the account unplaced, and a re-spawn of the same handle +// re-provisions it. This path threads the STORED role/persona to the Runner, so +// a retry naming DIFFERENT values must still provision under the first-spawn's +// stored values — never the retry's. Without this test a regression passing +// req.GetRole()/GetPersona() on the resume branch (the exact mistake T4 fixes on +// the create branch) would ship green: the stored row stays correct while an +// injected role reaches the Runner on re-provision. +func TestSpawnResumeReprovisionThreadsStoredRolePersona(t *testing.T) { + f := newLifecycleFixture(t) + ctx := context.Background() + + const ( + wantRole = "manager" + wantPersona = "You are a diligent manager." + ) + // First spawn crashes mid-chain: the Runner refuses Start, so provisionAndStart + // rolls the container back and leaves the account UNPLACED with no recorded + // session — the genuine crashed-after-CreateAgent state the resume path exists + // to recover (mirrors lifecycle_pgtest_test.go's rollback-then-resume). + f.runner.setFailStart(true) + _, err := f.lc.SpawnAsAccount(ctx, f.agentID, &compassv1internal.SpawnPeerRequest{ + Handle: "peer-resume-rp", + ClientRequestId: "spawn-resume-rp-1", + Role: wantRole, + Persona: wantPersona, + }) + if err == nil { + t.Fatal("first SpawnAsAccount = nil, want a mid-chain failure (Runner refused Start)") + } + // The account exists (CreateAgent committed) with its stored role/persona, but + // is unplaced. Confirm the stored values are the first-spawn's. + created, err := f.store.AgentByHandle(ctx, f.ownerAdmin, "peer-resume-rp") + if err != nil { + t.Fatalf("AgentByHandle(peer-resume-rp) after crash = %v, want the created-but-unplaced account", err) + } + if created.Agent.Role != wantRole || created.Agent.Persona != wantPersona { + t.Fatalf("stored (role,persona) = (%q,%q), want (%q,%q)", created.Agent.Role, created.Agent.Persona, wantRole, wantPersona) + } + f.runner.setFailStart(false) // the Runner accepts Start again: the resume can complete + f.runner.forget() // drop the failed first attempt; assert only on the resume + + // Re-spawn the same handle under a DISTINCT client_request_id (so it reaches + // CreateAgent, conflicts on the handle, and resumes the unplaced account) + // naming DIFFERENT role/persona — the injection attempt the resume must ignore. + second, err := f.lc.SpawnAsAccount(ctx, f.agentID, &compassv1internal.SpawnPeerRequest{ + Handle: "peer-resume-rp", + ClientRequestId: "spawn-resume-rp-2", + Role: "impostor", + Persona: "different", + }) + if err != nil { + t.Fatalf("resume SpawnAsAccount = %v, want success", err) + } + if second.GetAgentAccountId() != string(created.ID) { + t.Fatalf("resume returned a different agent id %q, want the first %q (a resume, not a second account)", second.GetAgentAccountId(), created.ID) + } + + // The load-bearing assertion: the Provision wire on the RE-provision carries + // the STORED first-spawn values, not the retry's injected ones. + if got := f.runner.provisionRole(t); got != wantRole { + t.Fatalf("re-provision wire role = %q, want the stored %q (never the retry's injected role)", got, wantRole) + } + if got := f.runner.provisionPersona(t); got != wantPersona { + t.Fatalf("re-provision wire persona = %q, want the stored %q (never the retry's injected persona)", got, wantPersona) + } +} 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 76782e98..c373cf85 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 @@ -34,7 +34,7 @@ import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; -import type { GetRosterRequest, GetRosterResponse, ListMessagesRequest, ListMessagesResponse, PostMessageRequest, PostMessageResponse, UpdatePinnedBoardRequest, UpdatePinnedBoardResponse } from "./comms_pb"; +import type { CreateChannelGroupRequest, CreateChannelGroupResponse, CreateChannelRequest, CreateChannelResponse, GetRosterRequest, GetRosterResponse, ListMessagesRequest, ListMessagesResponse, PostMessageRequest, PostMessageResponse, UpdateChannelMembersRequest, UpdateChannelMembersResponse, UpdatePinnedBoardRequest, UpdatePinnedBoardResponse } from "./comms_pb"; import { file_compass_v1_comms } from "./comms_pb"; import type { AgentControlSchema, AgentFrame } from "./agent_pb"; import { file_compass_v1_agent } from "./agent_pb"; @@ -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("Ch5jb21wYXNzL3YxL2FnZW50X2dhdGV3YXkucHJvdG8SCmNvbXBhc3MudjEixwIKEENvbW1zQ2FsbFJlcXVlc3QSDwoHY2FsbF9pZBgBIAEoCRIuCgRwb3N0GAIgASgLMh4uY29tcGFzcy52MS5Qb3N0TWVzc2FnZVJlcXVlc3RIABIvCgRsaXN0GAMgASgLMh8uY29tcGFzcy52MS5MaXN0TWVzc2FnZXNSZXF1ZXN0SAASLgoGcm9zdGVyGAQgASgLMhwuY29tcGFzcy52MS5HZXRSb3N0ZXJSZXF1ZXN0SAASNwoKc2V0X3N0YXR1cxgFIAEoCzIhLmNvbXBhc3MudjEuU2V0QWdlbnRTdGF0dXNSZXF1ZXN0SAASMwoDcGluGAYgASgLMiQuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlcXVlc3RIABIbChN0cmlnZ2VyX3RyYWNlcGFyZW50GAogASgJQgYKBGNhbGwi3QIKD0NvbW1zQ2FsbFJlc3VsdBIPCgdjYWxsX2lkGAEgASgJEi8KBHBvc3QYAiABKAsyHy5jb21wYXNzLnYxLlBvc3RNZXNzYWdlUmVzcG9uc2VIABIwCgRsaXN0GAMgASgLMiAuY29tcGFzcy52MS5MaXN0TWVzc2FnZXNSZXNwb25zZUgAEisKBWVycm9yGAQgASgLMhouY29tcGFzcy52MS5Db21tc0NhbGxFcnJvckgAEi8KBnJvc3RlchgFIAEoCzIdLmNvbXBhc3MudjEuR2V0Um9zdGVyUmVzcG9uc2VIABI4CgpzZXRfc3RhdHVzGAYgASgLMiIuY29tcGFzcy52MS5TZXRBZ2VudFN0YXR1c1Jlc3BvbnNlSAASNAoDcGluGAcgASgLMiUuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlSABCCAoGcmVzdWx0Ii8KDkNvbW1zQ2FsbEVycm9yEgwKBGNvZGUYASABKAkSDwoHbWVzc2FnZRgCIAEoCSIpChVTZXRBZ2VudFN0YXR1c1JlcXVlc3QSEAoIYWN0aXZpdHkYASABKAkiGAoWU2V0QWdlbnRTdGF0dXNSZXNwb25zZSKRAQoUTGlmZWN5Y2xlQ2FsbFJlcXVlc3QSDwoHY2FsbF9pZBgBIAEoCRItCgVzcGF3bhgCIAEoCzIcLmNvbXBhc3MudjEuU3Bhd25QZWVyUmVxdWVzdEgAEjEKB2Rlc3Bhd24YAyABKAsyHi5jb21wYXNzLnYxLkRlc3Bhd25QZWVyUmVxdWVzdEgAQgYKBGNhbGwiaQoQU3Bhd25QZWVyUmVxdWVzdBIOCgZoYW5kbGUYASABKAkSFAoMZGlzcGxheV9uYW1lGAIgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAQgASgJSgQIAxAEUg5pbml0aWFsX3Byb21wdCJZChFTcGF3blBlZXJSZXNwb25zZRIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEhYKDmNvbnRhaW5lcl9uYW1lGAIgASgJEhIKCnNlc3Npb25faWQYAyABKAkiKgoSRGVzcGF3blBlZXJSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCSIVChNEZXNwYXduUGVlclJlc3BvbnNlIsUBChNMaWZlY3ljbGVDYWxsUmVzdWx0Eg8KB2NhbGxfaWQYASABKAkSLgoFc3Bhd24YAiABKAsyHS5jb21wYXNzLnYxLlNwYXduUGVlclJlc3BvbnNlSAASMgoHZGVzcGF3bhgDIAEoCzIfLmNvbXBhc3MudjEuRGVzcGF3blBlZXJSZXNwb25zZUgAEi8KBWVycm9yGAQgASgLMh4uY29tcGFzcy52MS5MaWZlY3ljbGVDYWxsRXJyb3JIAEIICgZyZXN1bHQiMwoSTGlmZWN5Y2xlQ2FsbEVycm9yEgwKBGNvZGUYASABKAkSDwoHbWVzc2FnZRgCIAEoCSLIBQoQRm9yZ2VDYWxsUmVxdWVzdBIPCgdjYWxsX2lkGAEgASgJEjYKDGNyZWF0ZV9pc3N1ZRgCIAEoCzIeLmNvbXBhc3MudjEuQ3JlYXRlSXNzdWVSZXF1ZXN0SAASPQoQY29tbWVudF9vbl9pc3N1ZRgDIAEoCzIhLmNvbXBhc3MudjEuQ29tbWVudE9uSXNzdWVSZXF1ZXN0SAASMAoJZ2V0X2lzc3VlGAQgASgLMhsuY29tcGFzcy52MS5HZXRJc3N1ZVJlcXVlc3RIABI0CgtsaXN0X2lzc3VlcxgFIAEoCzIdLmNvbXBhc3MudjEuTGlzdElzc3Vlc1JlcXVlc3RIABJDChNjcmVhdGVfcHVsbF9yZXF1ZXN0GAYgASgLMiQuY29tcGFzcy52MS5DcmVhdGVQdWxsUmVxdWVzdFJlcXVlc3RIABJKChdjb21tZW50X29uX3B1bGxfcmVxdWVzdBgHIAEoCzInLmNvbXBhc3MudjEuQ29tbWVudE9uUHVsbFJlcXVlc3RSZXF1ZXN0SAASPQoQZ2V0X3B1bGxfcmVxdWVzdBgIIAEoCzIhLmNvbXBhc3MudjEuR2V0UHVsbFJlcXVlc3RSZXF1ZXN0SAASNgoJc3Vic2NyaWJlGAkgASgLMiEuY29tcGFzcy52MS5TdWJzY3JpYmVGb3JnZVJlcXVlc3RIABI6Cgt1bnN1YnNjcmliZRgKIAEoCzIjLmNvbXBhc3MudjEuVW5zdWJzY3JpYmVGb3JnZVJlcXVlc3RIABI4Cg1zdWJtaXRfcmV2aWV3GAsgASgLMh8uY29tcGFzcy52MS5TdWJtaXRSZXZpZXdSZXF1ZXN0SAASIwoFZm9yZ2UYDCABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEhkKEWNsaWVudF9yZXF1ZXN0X2lkGA0gASgJQgYKBGNhbGwi4AMKD0ZvcmdlQ2FsbFJlc3VsdBIPCgdjYWxsX2lkGAEgASgJEiIKBWlzc3VlGAIgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAEi8KDWlzc3VlX2NvbW1lbnQYAyABKAsyFi5jb21wYXNzLnYxLkNvbW1lbnRSZWZIABIwCgZpc3N1ZXMYBCABKAsyHi5jb21wYXNzLnYxLkxpc3RJc3N1ZXNSZXNwb25zZUgAEi8KDHB1bGxfcmVxdWVzdBgFIAEoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3RIABIsCgpwcl9jb21tZW50GAYgASgLMhYuY29tcGFzcy52MS5Db21tZW50UmVmSAASOAoKc3Vic2NyaWJlZBgHIAEoCzIiLmNvbXBhc3MudjEuU3Vic2NyaWJlRm9yZ2VSZXNwb25zZUgAEjwKDHVuc3Vic2NyaWJlZBgIIAEoCzIkLmNvbXBhc3MudjEuVW5zdWJzY3JpYmVGb3JnZVJlc3BvbnNlSAASKwoFZXJyb3IYCSABKAsyGi5jb21wYXNzLnYxLkZvcmdlQ2FsbEVycm9ySAASJwoGcmV2aWV3GAogASgLMhUuY29tcGFzcy52MS5SZXZpZXdSZWZIAEIICgZyZXN1bHQiRwoORm9yZ2VDYWxsRXJyb3ISDAoEY29kZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJEhYKDnJldHJ5X2FmdGVyX21zGAMgASgNIk8KEkNyZWF0ZUlzc3VlUmVxdWVzdBIMCgRyZXBvGAEgASgJEg0KBXRpdGxlGAIgASgJEgwKBGJvZHkYAyABKAkSDgoGbGFiZWxzGAQgAygJIkkKFUNvbW1lbnRPbklzc3VlUmVxdWVzdBIMCgRyZXBvGAEgASgJEhQKDGlzc3VlX251bWJlchgCIAEoBBIMCgRib2R5GAMgASgJIjUKD0dldElzc3VlUmVxdWVzdBIMCgRyZXBvGAEgASgJEhQKDGlzc3VlX251bWJlchgCIAEoBCJPChFMaXN0SXNzdWVzUmVxdWVzdBIMCgRyZXBvGAEgASgJEg0KBXN0YXRlGAIgASgJEg4KBmxhYmVscxgDIAMoCRINCgVsaW1pdBgEIAEoDSI3ChJMaXN0SXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSJ4ChhDcmVhdGVQdWxsUmVxdWVzdFJlcXVlc3QSDAoEcmVwbxgBIAEoCRINCgV0aXRsZRgCIAEoCRIMCgRib2R5GAMgASgJEhAKCGhlYWRfcmVmGAQgASgJEhAKCGJhc2VfcmVmGAUgASgJEg0KBWRyYWZ0GAYgASgIIk4KG0NvbW1lbnRPblB1bGxSZXF1ZXN0UmVxdWVzdBIMCgRyZXBvGAEgASgJEhMKC3B1bGxfbnVtYmVyGAIgASgEEgwKBGJvZHkYAyABKAkiOgoVR2V0UHVsbFJlcXVlc3RSZXF1ZXN0EgwKBHJlcG8YASABKAkSEwoLcHVsbF9udW1iZXIYAiABKAQiiQEKE1N1Ym1pdFJldmlld1JlcXVlc3QSDAoEcmVwbxgBIAEoCRITCgtwdWxsX251bWJlchgCIAEoBBIPCgd2ZXJkaWN0GAMgASgJEgwKBGJvZHkYBCABKAkSMAoIY29tbWVudHMYBSADKAsyHi5jb21wYXNzLnYxLlJldmlld0NvbW1lbnRJbnB1dCJMChJSZXZpZXdDb21tZW50SW5wdXQSDAoEcGF0aBgBIAEoCRIMCgRsaW5lGAIgASgNEgwKBHNpZGUYAyABKAkSDAoEYm9keRgEIAEoCSKmAQoVU3Vic2NyaWJlRm9yZ2VSZXF1ZXN0EgwKBHJlcG8YASABKAkSKwoEa2luZBgCIAEoDjIdLmNvbXBhc3MudjEuRm9yZ2VBcnRpZmFjdEtpbmQSDgoGbnVtYmVyGAMgASgEEjEKBXNjb3BlGAQgASgOMiIuY29tcGFzcy52MS5Gb3JnZVN1YnNjcmlwdGlvblNjb3BlEg8KB3Byb2plY3QYBSABKAkiMQoWU3Vic2NyaWJlRm9yZ2VSZXNwb25zZRIXCg9zdWJzY3JpcHRpb25faWQYASABKAkiMgoXVW5zdWJzY3JpYmVGb3JnZVJlcXVlc3QSFwoPc3Vic2NyaXB0aW9uX2lkGAEgASgJIhoKGFVuc3Vic2NyaWJlRm9yZ2VSZXNwb25zZSJoChBCb2FyZENhbGxSZXF1ZXN0Eg8KB2NhbGxfaWQYASABKAkSOwoPc2V0X2lzc3VlX3N0YXRlGAIgASgLMiAuY29tcGFzcy52MS5TZXRJc3N1ZVN0YXRlUmVxdWVzdEgAQgYKBGNhbGwiTwoUU2V0SXNzdWVTdGF0ZVJlcXVlc3QSEAoIaXNzdWVfaWQYASABKAkSJQoFc3RhdGUYAiABKA4yFi5jb21wYXNzLnYxLklzc3VlU3RhdGUiOQoVU2V0SXNzdWVTdGF0ZVJlc3BvbnNlEiAKBWlzc3VlGAEgASgLMhEuY29tcGFzcy52MS5Jc3N1ZSKXAQoPQm9hcmRDYWxsUmVzdWx0Eg8KB2NhbGxfaWQYASABKAkSPAoPc2V0X2lzc3VlX3N0YXRlGAIgASgLMiEuY29tcGFzcy52MS5TZXRJc3N1ZVN0YXRlUmVzcG9uc2VIABIrCgVlcnJvchgDIAEoCzIaLmNvbXBhc3MudjEuQm9hcmRDYWxsRXJyb3JIAEIICgZyZXN1bHQiLwoOQm9hcmRDYWxsRXJyb3ISDAoEY29kZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJIjwKE1B1Ymxpc2hGcmFtZVJlcXVlc3QSJQoFZnJhbWUYASABKAsyFi5jb21wYXNzLnYxLkFnZW50RnJhbWUiFgoUUHVibGlzaEZyYW1lUmVzcG9uc2UiXgocUG9zdENvbnZlcnNhdGlvbkZyYW1lUmVxdWVzdBIlCgVmcmFtZRgBIAEoCzIWLmNvbXBhc3MudjEuQWdlbnRGcmFtZRIXCg9pZGVtcG90ZW5jeV9rZXkYAiABKAkiHwodUG9zdENvbnZlcnNhdGlvbkZyYW1lUmVzcG9uc2UiGQoXQ29udHJvbFN1YnNjcmliZVJlcXVlc3QqkQEKFkZvcmdlU3Vic2NyaXB0aW9uU2NvcGUSKAokRk9SR0VfU1VCU0NSSVBUSU9OX1NDT1BFX1VOU1BFQ0lGSUVEEAASJQohRk9SR0VfU1VCU0NSSVBUSU9OX1NDT1BFX0FSVElGQUNUEAESJgoiRk9SR0VfU1VCU0NSSVBUSU9OX1NDT1BFX0NPTlRBSU5FUhACMrQECgxBZ2VudEdhdGV3YXkSQgoFQ29tbXMSHC5jb21wYXNzLnYxLkNvbW1zQ2FsbFJlcXVlc3QaGy5jb21wYXNzLnYxLkNvbW1zQ2FsbFJlc3VsdBJOCglMaWZlY3ljbGUSIC5jb21wYXNzLnYxLkxpZmVjeWNsZUNhbGxSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaWZlY3ljbGVDYWxsUmVzdWx0Ek4KB1B1Ymxpc2gSHy5jb21wYXNzLnYxLlB1Ymxpc2hGcmFtZVJlcXVlc3QaIC5jb21wYXNzLnYxLlB1Ymxpc2hGcmFtZVJlc3BvbnNlKAESbAoVUG9zdENvbnZlcnNhdGlvbkZyYW1lEiguY29tcGFzcy52MS5Qb3N0Q29udmVyc2F0aW9uRnJhbWVSZXF1ZXN0GikuY29tcGFzcy52MS5Qb3N0Q29udmVyc2F0aW9uRnJhbWVSZXNwb25zZRJKCgdDb250cm9sEiMuY29tcGFzcy52MS5Db250cm9sU3Vic2NyaWJlUmVxdWVzdBoYLmNvbXBhc3MudjEuQWdlbnRDb250cm9sMAESQgoFRm9yZ2USHC5jb21wYXNzLnYxLkZvcmdlQ2FsbFJlcXVlc3QaGy5jb21wYXNzLnYxLkZvcmdlQ2FsbFJlc3VsdBJCCgVCb2FyZBIcLmNvbXBhc3MudjEuQm9hcmRDYWxsUmVxdWVzdBobLmNvbXBhc3MudjEuQm9hcmRDYWxsUmVzdWx0YgZwcm90bzM", [file_compass_v1_comms, file_compass_v1_agent, file_compass_v1_compass, file_compass_v1_forge]); + fileDesc("Ch5jb21wYXNzL3YxL2FnZW50X2dhdGV3YXkucHJvdG8SCmNvbXBhc3MudjEijQQKEENvbW1zQ2FsbFJlcXVlc3QSDwoHY2FsbF9pZBgBIAEoCRIuCgRwb3N0GAIgASgLMh4uY29tcGFzcy52MS5Qb3N0TWVzc2FnZVJlcXVlc3RIABIvCgRsaXN0GAMgASgLMh8uY29tcGFzcy52MS5MaXN0TWVzc2FnZXNSZXF1ZXN0SAASLgoGcm9zdGVyGAQgASgLMhwuY29tcGFzcy52MS5HZXRSb3N0ZXJSZXF1ZXN0SAASNwoKc2V0X3N0YXR1cxgFIAEoCzIhLmNvbXBhc3MudjEuU2V0QWdlbnRTdGF0dXNSZXF1ZXN0SAASMwoDcGluGAYgASgLMiQuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlcXVlc3RIABI6Cg5jcmVhdGVfY2hhbm5lbBgHIAEoCzIgLmNvbXBhc3MudjEuQ3JlYXRlQ2hhbm5lbFJlcXVlc3RIABJBCg51cGRhdGVfbWVtYmVycxgIIAEoCzInLmNvbXBhc3MudjEuVXBkYXRlQ2hhbm5lbE1lbWJlcnNSZXF1ZXN0SAASRQoUY3JlYXRlX2NoYW5uZWxfZ3JvdXAYCSABKAsyJS5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxHcm91cFJlcXVlc3RIABIbChN0cmlnZ2VyX3RyYWNlcGFyZW50GAogASgJQgYKBGNhbGwipgQKD0NvbW1zQ2FsbFJlc3VsdBIPCgdjYWxsX2lkGAEgASgJEi8KBHBvc3QYAiABKAsyHy5jb21wYXNzLnYxLlBvc3RNZXNzYWdlUmVzcG9uc2VIABIwCgRsaXN0GAMgASgLMiAuY29tcGFzcy52MS5MaXN0TWVzc2FnZXNSZXNwb25zZUgAEisKBWVycm9yGAQgASgLMhouY29tcGFzcy52MS5Db21tc0NhbGxFcnJvckgAEi8KBnJvc3RlchgFIAEoCzIdLmNvbXBhc3MudjEuR2V0Um9zdGVyUmVzcG9uc2VIABI4CgpzZXRfc3RhdHVzGAYgASgLMiIuY29tcGFzcy52MS5TZXRBZ2VudFN0YXR1c1Jlc3BvbnNlSAASNAoDcGluGAcgASgLMiUuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlSAASOwoOY3JlYXRlX2NoYW5uZWwYCCABKAsyIS5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxSZXNwb25zZUgAEkIKDnVwZGF0ZV9tZW1iZXJzGAkgASgLMiguY29tcGFzcy52MS5VcGRhdGVDaGFubmVsTWVtYmVyc1Jlc3BvbnNlSAASRgoUY3JlYXRlX2NoYW5uZWxfZ3JvdXAYCiABKAsyJi5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxHcm91cFJlc3BvbnNlSABCCAoGcmVzdWx0Ii8KDkNvbW1zQ2FsbEVycm9yEgwKBGNvZGUYASABKAkSDwoHbWVzc2FnZRgCIAEoCSIpChVTZXRBZ2VudFN0YXR1c1JlcXVlc3QSEAoIYWN0aXZpdHkYASABKAkiGAoWU2V0QWdlbnRTdGF0dXNSZXNwb25zZSKRAQoUTGlmZWN5Y2xlQ2FsbFJlcXVlc3QSDwoHY2FsbF9pZBgBIAEoCRItCgVzcGF3bhgCIAEoCzIcLmNvbXBhc3MudjEuU3Bhd25QZWVyUmVxdWVzdEgAEjEKB2Rlc3Bhd24YAyABKAsyHi5jb21wYXNzLnYxLkRlc3Bhd25QZWVyUmVxdWVzdEgAQgYKBGNhbGwiiAEKEFNwYXduUGVlclJlcXVlc3QSDgoGaGFuZGxlGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgEIAEoCRIMCgRyb2xlGAUgASgJEg8KB3BlcnNvbmEYBiABKAlKBAgDEARSDmluaXRpYWxfcHJvbXB0IlkKEVNwYXduUGVlclJlc3BvbnNlEhgKEGFnZW50X2FjY291bnRfaWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCSIqChJEZXNwYXduUGVlclJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJIhUKE0Rlc3Bhd25QZWVyUmVzcG9uc2UixQEKE0xpZmVjeWNsZUNhbGxSZXN1bHQSDwoHY2FsbF9pZBgBIAEoCRIuCgVzcGF3bhgCIAEoCzIdLmNvbXBhc3MudjEuU3Bhd25QZWVyUmVzcG9uc2VIABIyCgdkZXNwYXduGAMgASgLMh8uY29tcGFzcy52MS5EZXNwYXduUGVlclJlc3BvbnNlSAASLwoFZXJyb3IYBCABKAsyHi5jb21wYXNzLnYxLkxpZmVjeWNsZUNhbGxFcnJvckgAQggKBnJlc3VsdCIzChJMaWZlY3ljbGVDYWxsRXJyb3ISDAoEY29kZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJIsgFChBGb3JnZUNhbGxSZXF1ZXN0Eg8KB2NhbGxfaWQYASABKAkSNgoMY3JlYXRlX2lzc3VlGAIgASgLMh4uY29tcGFzcy52MS5DcmVhdGVJc3N1ZVJlcXVlc3RIABI9ChBjb21tZW50X29uX2lzc3VlGAMgASgLMiEuY29tcGFzcy52MS5Db21tZW50T25Jc3N1ZVJlcXVlc3RIABIwCglnZXRfaXNzdWUYBCABKAsyGy5jb21wYXNzLnYxLkdldElzc3VlUmVxdWVzdEgAEjQKC2xpc3RfaXNzdWVzGAUgASgLMh0uY29tcGFzcy52MS5MaXN0SXNzdWVzUmVxdWVzdEgAEkMKE2NyZWF0ZV9wdWxsX3JlcXVlc3QYBiABKAsyJC5jb21wYXNzLnYxLkNyZWF0ZVB1bGxSZXF1ZXN0UmVxdWVzdEgAEkoKF2NvbW1lbnRfb25fcHVsbF9yZXF1ZXN0GAcgASgLMicuY29tcGFzcy52MS5Db21tZW50T25QdWxsUmVxdWVzdFJlcXVlc3RIABI9ChBnZXRfcHVsbF9yZXF1ZXN0GAggASgLMiEuY29tcGFzcy52MS5HZXRQdWxsUmVxdWVzdFJlcXVlc3RIABI2CglzdWJzY3JpYmUYCSABKAsyIS5jb21wYXNzLnYxLlN1YnNjcmliZUZvcmdlUmVxdWVzdEgAEjoKC3Vuc3Vic2NyaWJlGAogASgLMiMuY29tcGFzcy52MS5VbnN1YnNjcmliZUZvcmdlUmVxdWVzdEgAEjgKDXN1Ym1pdF9yZXZpZXcYCyABKAsyHy5jb21wYXNzLnYxLlN1Ym1pdFJldmlld1JlcXVlc3RIABIjCgVmb3JnZRgMIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSGQoRY2xpZW50X3JlcXVlc3RfaWQYDSABKAlCBgoEY2FsbCLgAwoPRm9yZ2VDYWxsUmVzdWx0Eg8KB2NhbGxfaWQYASABKAkSIgoFaXNzdWUYAiABKAsyES5jb21wYXNzLnYxLklzc3VlSAASLwoNaXNzdWVfY29tbWVudBgDIAEoCzIWLmNvbXBhc3MudjEuQ29tbWVudFJlZkgAEjAKBmlzc3VlcxgEIAEoCzIeLmNvbXBhc3MudjEuTGlzdElzc3Vlc1Jlc3BvbnNlSAASLwoMcHVsbF9yZXF1ZXN0GAUgASgLMhcuY29tcGFzcy52MS5QdWxsUmVxdWVzdEgAEiwKCnByX2NvbW1lbnQYBiABKAsyFi5jb21wYXNzLnYxLkNvbW1lbnRSZWZIABI4CgpzdWJzY3JpYmVkGAcgASgLMiIuY29tcGFzcy52MS5TdWJzY3JpYmVGb3JnZVJlc3BvbnNlSAASPAoMdW5zdWJzY3JpYmVkGAggASgLMiQuY29tcGFzcy52MS5VbnN1YnNjcmliZUZvcmdlUmVzcG9uc2VIABIrCgVlcnJvchgJIAEoCzIaLmNvbXBhc3MudjEuRm9yZ2VDYWxsRXJyb3JIABInCgZyZXZpZXcYCiABKAsyFS5jb21wYXNzLnYxLlJldmlld1JlZkgAQggKBnJlc3VsdCJHCg5Gb3JnZUNhbGxFcnJvchIMCgRjb2RlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSFgoOcmV0cnlfYWZ0ZXJfbXMYAyABKA0iTwoSQ3JlYXRlSXNzdWVSZXF1ZXN0EgwKBHJlcG8YASABKAkSDQoFdGl0bGUYAiABKAkSDAoEYm9keRgDIAEoCRIOCgZsYWJlbHMYBCADKAkiSQoVQ29tbWVudE9uSXNzdWVSZXF1ZXN0EgwKBHJlcG8YASABKAkSFAoMaXNzdWVfbnVtYmVyGAIgASgEEgwKBGJvZHkYAyABKAkiNQoPR2V0SXNzdWVSZXF1ZXN0EgwKBHJlcG8YASABKAkSFAoMaXNzdWVfbnVtYmVyGAIgASgEIk8KEUxpc3RJc3N1ZXNSZXF1ZXN0EgwKBHJlcG8YASABKAkSDQoFc3RhdGUYAiABKAkSDgoGbGFiZWxzGAMgAygJEg0KBWxpbWl0GAQgASgNIjcKEkxpc3RJc3N1ZXNSZXNwb25zZRIhCgZpc3N1ZXMYASADKAsyES5jb21wYXNzLnYxLklzc3VlIngKGENyZWF0ZVB1bGxSZXF1ZXN0UmVxdWVzdBIMCgRyZXBvGAEgASgJEg0KBXRpdGxlGAIgASgJEgwKBGJvZHkYAyABKAkSEAoIaGVhZF9yZWYYBCABKAkSEAoIYmFzZV9yZWYYBSABKAkSDQoFZHJhZnQYBiABKAgiTgobQ29tbWVudE9uUHVsbFJlcXVlc3RSZXF1ZXN0EgwKBHJlcG8YASABKAkSEwoLcHVsbF9udW1iZXIYAiABKAQSDAoEYm9keRgDIAEoCSI6ChVHZXRQdWxsUmVxdWVzdFJlcXVlc3QSDAoEcmVwbxgBIAEoCRITCgtwdWxsX251bWJlchgCIAEoBCKJAQoTU3VibWl0UmV2aWV3UmVxdWVzdBIMCgRyZXBvGAEgASgJEhMKC3B1bGxfbnVtYmVyGAIgASgEEg8KB3ZlcmRpY3QYAyABKAkSDAoEYm9keRgEIAEoCRIwCghjb21tZW50cxgFIAMoCzIeLmNvbXBhc3MudjEuUmV2aWV3Q29tbWVudElucHV0IkwKElJldmlld0NvbW1lbnRJbnB1dBIMCgRwYXRoGAEgASgJEgwKBGxpbmUYAiABKA0SDAoEc2lkZRgDIAEoCRIMCgRib2R5GAQgASgJIqYBChVTdWJzY3JpYmVGb3JnZVJlcXVlc3QSDAoEcmVwbxgBIAEoCRIrCgRraW5kGAIgASgOMh0uY29tcGFzcy52MS5Gb3JnZUFydGlmYWN0S2luZBIOCgZudW1iZXIYAyABKAQSMQoFc2NvcGUYBCABKA4yIi5jb21wYXNzLnYxLkZvcmdlU3Vic2NyaXB0aW9uU2NvcGUSDwoHcHJvamVjdBgFIAEoCSIxChZTdWJzY3JpYmVGb3JnZVJlc3BvbnNlEhcKD3N1YnNjcmlwdGlvbl9pZBgBIAEoCSIyChdVbnN1YnNjcmliZUZvcmdlUmVxdWVzdBIXCg9zdWJzY3JpcHRpb25faWQYASABKAkiGgoYVW5zdWJzY3JpYmVGb3JnZVJlc3BvbnNlImgKEEJvYXJkQ2FsbFJlcXVlc3QSDwoHY2FsbF9pZBgBIAEoCRI7Cg9zZXRfaXNzdWVfc3RhdGUYAiABKAsyIC5jb21wYXNzLnYxLlNldElzc3VlU3RhdGVSZXF1ZXN0SABCBgoEY2FsbCJPChRTZXRJc3N1ZVN0YXRlUmVxdWVzdBIQCghpc3N1ZV9pZBgBIAEoCRIlCgVzdGF0ZRgCIAEoDjIWLmNvbXBhc3MudjEuSXNzdWVTdGF0ZSI5ChVTZXRJc3N1ZVN0YXRlUmVzcG9uc2USIAoFaXNzdWUYASABKAsyES5jb21wYXNzLnYxLklzc3VlIpcBCg9Cb2FyZENhbGxSZXN1bHQSDwoHY2FsbF9pZBgBIAEoCRI8Cg9zZXRfaXNzdWVfc3RhdGUYAiABKAsyIS5jb21wYXNzLnYxLlNldElzc3VlU3RhdGVSZXNwb25zZUgAEisKBWVycm9yGAMgASgLMhouY29tcGFzcy52MS5Cb2FyZENhbGxFcnJvckgAQggKBnJlc3VsdCIvCg5Cb2FyZENhbGxFcnJvchIMCgRjb2RlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkiPAoTUHVibGlzaEZyYW1lUmVxdWVzdBIlCgVmcmFtZRgBIAEoCzIWLmNvbXBhc3MudjEuQWdlbnRGcmFtZSIWChRQdWJsaXNoRnJhbWVSZXNwb25zZSJeChxQb3N0Q29udmVyc2F0aW9uRnJhbWVSZXF1ZXN0EiUKBWZyYW1lGAEgASgLMhYuY29tcGFzcy52MS5BZ2VudEZyYW1lEhcKD2lkZW1wb3RlbmN5X2tleRgCIAEoCSIfCh1Qb3N0Q29udmVyc2F0aW9uRnJhbWVSZXNwb25zZSIZChdDb250cm9sU3Vic2NyaWJlUmVxdWVzdCqRAQoWRm9yZ2VTdWJzY3JpcHRpb25TY29wZRIoCiRGT1JHRV9TVUJTQ1JJUFRJT05fU0NPUEVfVU5TUEVDSUZJRUQQABIlCiFGT1JHRV9TVUJTQ1JJUFRJT05fU0NPUEVfQVJUSUZBQ1QQARImCiJGT1JHRV9TVUJTQ1JJUFRJT05fU0NPUEVfQ09OVEFJTkVSEAIytAQKDEFnZW50R2F0ZXdheRJCCgVDb21tcxIcLmNvbXBhc3MudjEuQ29tbXNDYWxsUmVxdWVzdBobLmNvbXBhc3MudjEuQ29tbXNDYWxsUmVzdWx0Ek4KCUxpZmVjeWNsZRIgLmNvbXBhc3MudjEuTGlmZWN5Y2xlQ2FsbFJlcXVlc3QaHy5jb21wYXNzLnYxLkxpZmVjeWNsZUNhbGxSZXN1bHQSTgoHUHVibGlzaBIfLmNvbXBhc3MudjEuUHVibGlzaEZyYW1lUmVxdWVzdBogLmNvbXBhc3MudjEuUHVibGlzaEZyYW1lUmVzcG9uc2UoARJsChVQb3N0Q29udmVyc2F0aW9uRnJhbWUSKC5jb21wYXNzLnYxLlBvc3RDb252ZXJzYXRpb25GcmFtZVJlcXVlc3QaKS5jb21wYXNzLnYxLlBvc3RDb252ZXJzYXRpb25GcmFtZVJlc3BvbnNlEkoKB0NvbnRyb2wSIy5jb21wYXNzLnYxLkNvbnRyb2xTdWJzY3JpYmVSZXF1ZXN0GhguY29tcGFzcy52MS5BZ2VudENvbnRyb2wwARJCCgVGb3JnZRIcLmNvbXBhc3MudjEuRm9yZ2VDYWxsUmVxdWVzdBobLmNvbXBhc3MudjEuRm9yZ2VDYWxsUmVzdWx0EkIKBUJvYXJkEhwuY29tcGFzcy52MS5Cb2FyZENhbGxSZXF1ZXN0GhsuY29tcGFzcy52MS5Cb2FyZENhbGxSZXN1bHRiBnByb3RvMw", [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 @@ -96,6 +96,24 @@ export type CommsCallRequest = Message<"compass.v1.CommsCallRequest"> & { */ value: UpdatePinnedBoardRequest; case: "pin"; + } | { + /** + * @generated from field: compass.v1.CreateChannelRequest create_channel = 7; + */ + value: CreateChannelRequest; + case: "createChannel"; + } | { + /** + * @generated from field: compass.v1.UpdateChannelMembersRequest update_members = 8; + */ + value: UpdateChannelMembersRequest; + case: "updateMembers"; + } | { + /** + * @generated from field: compass.v1.CreateChannelGroupRequest create_channel_group = 9; + */ + value: CreateChannelGroupRequest; + case: "createChannelGroup"; } | { case: undefined; value?: undefined }; /** @@ -124,9 +142,12 @@ export const CommsCallRequestSchema: GenMessage = /*@__PURE__* /** * The result of one comms call, correlated by `call_id`. A successful call sets - * `post`/`list`; an in-band failure (a tool error — non-member channel, bad - * input) sets `error`, which is NOT a transport teardown. The same message is - * the `RelayCommsCallResponse.result` payload on the Runner->Server leg. + * the response arm matching the request's operation (`post`, `list`, `roster`, + * `set_status`, `pin`, `create_channel`, `update_members`, + * `create_channel_group`); an in-band failure (a tool error — non-member + * channel, bad input) sets `error`, which is NOT a transport teardown. The same + * message is the `RelayCommsCallResponse.result` payload on the Runner->Server + * leg. * * @generated from message compass.v1.CommsCallResult */ @@ -175,6 +196,24 @@ export type CommsCallResult = Message<"compass.v1.CommsCallResult"> & { */ value: UpdatePinnedBoardResponse; case: "pin"; + } | { + /** + * @generated from field: compass.v1.CreateChannelResponse create_channel = 8; + */ + value: CreateChannelResponse; + case: "createChannel"; + } | { + /** + * @generated from field: compass.v1.UpdateChannelMembersResponse update_members = 9; + */ + value: UpdateChannelMembersResponse; + case: "updateMembers"; + } | { + /** + * @generated from field: compass.v1.CreateChannelGroupResponse create_channel_group = 10; + */ + value: CreateChannelGroupResponse; + case: "createChannelGroup"; } | { case: undefined; value?: undefined }; }; @@ -313,6 +352,20 @@ export type SpawnPeerRequest = Message<"compass.v1.SpawnPeerRequest"> & { * @generated from field: string client_request_id = 4; */ clientRequestId: string; + + /** + * Optional role/persona for the spawned agent, applied at creation only + * (org-management Manager creation, RIG-2673). Empty = the server's default + * role/persona for a peer. + * + * @generated from field: string role = 5; + */ + role: string; + + /** + * @generated from field: string persona = 6; + */ + persona: string; }; /** diff --git a/proto/compass/v1/agent_gateway.proto b/proto/compass/v1/agent_gateway.proto index 85ddbccb..2e041952 100644 --- a/proto/compass/v1/agent_gateway.proto +++ b/proto/compass/v1/agent_gateway.proto @@ -111,6 +111,9 @@ message CommsCallRequest { GetRosterRequest roster = 4; SetAgentStatusRequest set_status = 5; UpdatePinnedBoardRequest pin = 6; + CreateChannelRequest create_channel = 7; + UpdateChannelMembersRequest update_members = 8; + CreateChannelGroupRequest create_channel_group = 9; } // The W3C `traceparent` of the delivered message that triggered this outbound // call, re-attached by the agent so the Server can LINK the reply's fresh @@ -126,9 +129,12 @@ message CommsCallRequest { } // The result of one comms call, correlated by `call_id`. A successful call sets -// `post`/`list`; an in-band failure (a tool error — non-member channel, bad -// input) sets `error`, which is NOT a transport teardown. The same message is -// the `RelayCommsCallResponse.result` payload on the Runner->Server leg. +// the response arm matching the request's operation (`post`, `list`, `roster`, +// `set_status`, `pin`, `create_channel`, `update_members`, +// `create_channel_group`); an in-band failure (a tool error — non-member +// channel, bad input) sets `error`, which is NOT a transport teardown. The same +// message is the `RelayCommsCallResponse.result` payload on the Runner->Server +// leg. message CommsCallResult { string call_id = 1; oneof result { @@ -138,6 +144,9 @@ message CommsCallResult { GetRosterResponse roster = 5; SetAgentStatusResponse set_status = 6; UpdatePinnedBoardResponse pin = 7; + CreateChannelResponse create_channel = 8; + UpdateChannelMembersResponse update_members = 9; + CreateChannelGroupResponse create_channel_group = 10; } } @@ -181,6 +190,11 @@ message SpawnPeerRequest { reserved 3; reserved "initial_prompt"; string client_request_id = 4; // whole-chain idempotency key (handler-level join + Provision dedup) + // Optional role/persona for the spawned agent, applied at creation only + // (org-management Manager creation, RIG-2673). Empty = the server's default + // role/persona for a peer. + string role = 5; + string persona = 6; } message SpawnPeerResponse { string agent_account_id = 1;