From 23be4d0c59d5482a9a42914182679861554c3551 Mon Sep 17 00:00:00 2001 From: seal Date: Fri, 7 Aug 2026 16:38:18 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(compass):=20SEA-1732=20T10=20=E2=80=94?= =?UTF-8?q?=20operator-set=20role=20selects=20the=20agent's=20block-0=20pr?= =?UTF-8?q?ompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `role` field on AgentAccount, mirroring `persona` end to end, that selects a container's block-0 system prompt at provision time. Where persona is an APPEND overlay layered after the default prompt, role REPLACES block-0: the operator-set label selects `config/prompts//SYSTEM.md` from the mounted config tree, delivered to the container as `customSystemPrompt`. Delivery chain (server-authoritative, mirroring persona so a caller cannot inject a prompt): - migration 0015 adds `agent_accounts.role TEXT NOT NULL DEFAULT ''`; the store round-trips it through NewAgent/AgentAccount + every scanAccount SELECT. - `ProvisionAgentWorkspaceRequest.role` (proto field 7). Both provision paths overwrite the client value from the store account — service.go for the RPC path, lifecycle.go for the spawn path (which bypasses the RPC overwrite). - AgentSpec.Role -> AgentHandle.Role() -> AgentEnv.Role -> COMPASS_ROLE env, empty-omitted exactly like COMPASS_PERSONA. - cli.ts resolveRole reads COMPASS_ROLE, reads the role prompt through a new tolerant config-reader surface, and passes it as customSystemPrompt. Persona still appends AFTER the role block: customSystemPrompt and the systemPrompt callback are orthogonal SDK options that compose (replace runs first, append second). Role set but no prompt file falls back to today's default block-0. The MP-1 property test renders the real SDK prompt and pins the invariant a replaced block-0 must preserve: the skills list, rules list, and project footer survive, and skills injection stays gated on the read tool. Regenerated the compass proto stubs (public + client lanes) for the new field. Co-authored-by: Matt Wilkinson --- go/gen/compass/v1/compass.pb.go | 26 +- go/internal/runner/agent_exec.go | 17 +- go/internal/runner/agentenv_test.go | 31 +++ go/internal/runner/host.go | 1 + go/internal/runner/spec.go | 1 + go/internal/runner/spec_test.go | 20 ++ go/internal/runtime/agent.go | 8 + go/internal/store/accounts.go | 21 +- go/internal/store/accounts_test.go | 66 +++++ go/internal/store/inputs.go | 6 + .../store/migrations/0015_agent_role.sql | 15 + go/internal/store/types.go | 5 + go/server/lifecycle.go | 21 +- go/server/service.go | 13 +- go/server/service_placement_pgtest_test.go | 91 ++++++ packages/compass-agent/src/cli.test.ts | 263 ++++++++++++++++++ packages/compass-agent/src/cli.ts | 70 ++++- packages/compass-agent/src/config-reader.ts | 29 ++ .../src/gen/compass/v1/compass_pb.ts | 19 +- .../src/gen/compass/v1/compass_pb.ts | 19 +- proto/compass/v1/compass.proto | 12 + 21 files changed, 719 insertions(+), 35 deletions(-) create mode 100644 go/internal/store/migrations/0015_agent_role.sql diff --git a/go/gen/compass/v1/compass.pb.go b/go/gen/compass/v1/compass.pb.go index 90b8c2fd..ca2daf1a 100644 --- a/go/gen/compass/v1/compass.pb.go +++ b/go/gen/compass/v1/compass.pb.go @@ -2525,7 +2525,19 @@ type ProvisionAgentWorkspaceRequest struct { // provision path (not by this wire-settable field). The Runner materializes // it into the container // (compass-runner consumer). Empty = no persona baked (default). - Persona string `protobuf:"bytes,6,opt,name=persona,proto3" json:"persona,omitempty"` + Persona string `protobuf:"bytes,6,opt,name=persona,proto3" json:"persona,omitempty"` + // The agent's operator-set role, selecting the container's block-0 system + // prompt at provision so it survives compaction (a system-prompt config block + // is not part of the message history a snapcompact archives). SERVER- + // AUTHORITATIVE: the Server is expected to populate this by reading + // AgentAccount.role from the store on the provision path and to overwrite any + // client-supplied value, so a caller cannot inject a role prompt — an + // invariant enforced by the server provision path (not by this wire-settable + // field). Where persona (field 6) is an APPEND overlay, role REPLACES block-0: + // the label selects config/prompts//SYSTEM.md, materialized by the + // Runner into the container's customSystemPrompt (compass-runner consumer). + // Empty = no role (default OMP block-0). + Role string `protobuf:"bytes,7,opt,name=role,proto3" json:"role,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2581,6 +2593,13 @@ func (x *ProvisionAgentWorkspaceRequest) GetPersona() string { return "" } +func (x *ProvisionAgentWorkspaceRequest) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + type ProvisionAgentWorkspaceResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The stable container name — the handle passed to StartAgentSession and the @@ -4559,11 +4578,12 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12.\n" + "\x05event\x18\x02 \x01(\v2\x18.compass.v1.SessionEventR\x05event\x123\n" + - "\x05state\x18\x03 \x01(\x0e2\x1d.compass.v1.AgentSessionStateR\x05state\"\xbf\x01\n" + + "\x05state\x18\x03 \x01(\x0e2\x1d.compass.v1.AgentSessionStateR\x05state\"\xd3\x01\n" + "\x1eProvisionAgentWorkspaceRequest\x12(\n" + "\x10agent_account_id\x18\x01 \x01(\tR\x0eagentAccountId\x12*\n" + "\x11client_request_id\x18\x05 \x01(\tR\x0fclientRequestId\x12\x18\n" + - "\apersona\x18\x06 \x01(\tR\apersonaJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04J\x04\b\x04\x10\x05R\n" + + "\apersona\x18\x06 \x01(\tR\apersona\x12\x12\n" + + "\x04role\x18\a \x01(\tR\x04roleJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04J\x04\b\x04\x10\x05R\n" + "remote_urlR\n" + "local_pathR\x03ref\"H\n" + "\x1fProvisionAgentWorkspaceResponse\x12%\n" + diff --git a/go/internal/runner/agent_exec.go b/go/internal/runner/agent_exec.go index 7acd1ea2..a1dd8ff4 100644 --- a/go/internal/runner/agent_exec.go +++ b/go/internal/runner/agent_exec.go @@ -36,11 +36,12 @@ var agentCommand = []string{"compass-agent"} // (packages/compass-agent/src/cli.ts): HOME locates the provider seed, // COMPASS_WORKDIR is the session cwd, COMPASS_MODEL selects the model, // COMPASS_PERSONA is the identity overlay appended to the system prompt, -// COMPASS_RESUME_SESSION_FILE is the absolute in-container path of a -// server-reconstructed session file the agent loads to resume. Empty Model, -// Persona, or ResumeSessionFile is omitted rather than exported blank, so the -// agent falls back to its SDK default (or a fresh session) instead of receiving -// a value it must special-case. +// COMPASS_ROLE is the operator-set block-0 selector delivered as the +// container's customSystemPrompt, COMPASS_RESUME_SESSION_FILE is the absolute +// in-container path of a server-reconstructed session file the agent loads to +// resume. Empty Model, Persona, Role, or ResumeSessionFile is omitted rather +// than exported blank, so the agent falls back to its SDK default (or a fresh +// session) instead of receiving a value it must special-case. type AgentEnv struct { // UID is the agent user the exec runs as. Set explicitly because podman // strips the container's ambient capabilities only when --user is passed: @@ -58,6 +59,9 @@ type AgentEnv struct { Model string // Persona is the server-authoritative identity overlay, or empty for none. Persona string + // Role is the server-authoritative operator-set block-0 selector, delivered + // as the container's customSystemPrompt, or empty for none. + Role string // ResumeSessionFile is the absolute in-container path of the materialized // resume session file, or empty for a fresh start. ResumeSessionFile string @@ -82,6 +86,9 @@ func (e AgentEnv) execSpec() runtime.StreamingExecSpec { if e.Persona != "" { spec.Env["COMPASS_PERSONA"] = e.Persona } + if e.Role != "" { + spec.Env["COMPASS_ROLE"] = e.Role + } if e.ResumeSessionFile != "" { spec.Env["COMPASS_RESUME_SESSION_FILE"] = e.ResumeSessionFile } diff --git a/go/internal/runner/agentenv_test.go b/go/internal/runner/agentenv_test.go index b5b5c1b1..3056b804 100644 --- a/go/internal/runner/agentenv_test.go +++ b/go/internal/runner/agentenv_test.go @@ -109,6 +109,37 @@ func TestExecSpecExportsPersonaOnlyWhenConfigured(t *testing.T) { } } +// COMPASS_ROLE is exported only when a role is configured. An empty Role must +// leave the key ABSENT, not mapped to "": the agent treats an absent var as "no +// role" and stays on its default block-0 prompt, so exporting a blank value +// would force it to special-case an empty string. A non-empty Role is exported +// verbatim — this is the assertion that proves the block-0 selector is wired +// from the AgentEnv seam at all. +func TestExecSpecExportsRoleOnlyWhenConfigured(t *testing.T) { + tests := []struct { + name string + role string + want string + present bool + }{ + {name: "empty role omits the key entirely", role: "", present: false}, + {name: "configured role is exported verbatim", role: "manager", want: "manager", present: true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + spec := AgentEnv{UID: 1000, HomeDir: "/home/coder", Workdir: "/srv/checkout", Role: tc.role}.execSpec() + + got, ok := spec.Env["COMPASS_ROLE"] + if ok != tc.present { + t.Fatalf("COMPASS_ROLE present = %v (value %q), want present = %v", ok, got, tc.present) + } + if ok && got != tc.want { + t.Fatalf("COMPASS_ROLE = %q, want %q", got, tc.want) + } + }) + } +} + // SECURITY-LOAD-BEARING. The container is created with --cap-add NET_ADMIN // (runtime/agent.go:212) so its root entrypoint can arm the nft egress // firewall. Podman strips a container's ambient capabilities from an exec ONLY diff --git a/go/internal/runner/host.go b/go/internal/runner/host.go index c60fb8e3..e65e0a0a 100644 --- a/go/internal/runner/host.go +++ b/go/internal/runner/host.go @@ -657,6 +657,7 @@ func (h *agentHost) agentEnv(handle *runtime.AgentHandle) AgentEnv { Workdir: handle.CheckoutDir(), Model: h.model, Persona: handle.Persona(), + Role: handle.Role(), } } diff --git a/go/internal/runner/spec.go b/go/internal/runner/spec.go index e49f4f56..6e80d744 100644 --- a/go/internal/runner/spec.go +++ b/go/internal/runner/spec.go @@ -94,6 +94,7 @@ func (b *configSpecBuilder) BuildSpec(req *compassv1.ProvisionAgentWorkspaceRequ Egress: d.Egress, Mounts: d.Mounts, Persona: req.GetPersona(), + Role: req.GetRole(), }, nil } diff --git a/go/internal/runner/spec_test.go b/go/internal/runner/spec_test.go index 9d44d0cb..be5b4fe3 100644 --- a/go/internal/runner/spec_test.go +++ b/go/internal/runner/spec_test.go @@ -115,6 +115,26 @@ func TestBuildSpecMapsPersona(t *testing.T) { } } +// BuildSpec maps the request's server-authoritative role onto the AgentSpec so +// it rides through to the agent's block-0 customSystemPrompt. A bug that dropped +// it would boot every agent with no role and the default block-0 prompt. +func TestBuildSpecMapsRole(t *testing.T) { + builder, err := NewConfigSpecBuilder(goodDefaults()) + if err != nil { + t.Fatalf("NewConfigSpecBuilder: %v", err) + } + spec, err := builder.BuildSpec(&compassv1.ProvisionAgentWorkspaceRequest{ + AgentAccountId: strings.Repeat("a", 32), + Role: "manager", + }) + if err != nil { + t.Fatalf("BuildSpec = %v", err) + } + if spec.Role != "manager" { + t.Fatalf("spec.Role = %q, want %q (req.Role must reach the AgentSpec)", spec.Role, "manager") + } +} + // The container name is prefix+agent_account_id. A bug in the name derivation // would collide containers or misroute the per-agent workspace. func TestBuildSpecDerivesName(t *testing.T) { diff --git a/go/internal/runtime/agent.go b/go/internal/runtime/agent.go index 6e95b48c..4183bb63 100644 --- a/go/internal/runtime/agent.go +++ b/go/internal/runtime/agent.go @@ -45,6 +45,10 @@ type AgentSpec struct { // Persona is the server-authoritative identity overlay for this agent, // appended to the agent's system prompt at boot. Empty means no overlay. Persona string + // Role is the server-authoritative operator-set block-0 selector for this + // agent, delivered as the container's customSystemPrompt at boot. Empty + // means no role (default OMP block-0). + Role string } // AgentHandle is a live agent container: the resolved id plus the spec it was @@ -76,6 +80,10 @@ func (h *AgentHandle) HomeDir() string { return h.spec.Workspace.HomeDir } // empty for none. func (h *AgentHandle) Persona() string { return h.spec.Persona } +// Role returns the server-authoritative operator-set block-0 selector for this +// agent, or empty for none. +func (h *AgentHandle) Role() string { return h.spec.Role } + // StageError wraps a container runtime error with the lifecycle stage it // failed at, so a failure is diagnosable without a container inspect. type StageError struct { diff --git a/go/internal/store/accounts.go b/go/internal/store/accounts.go index 767010a0..73fe12ce 100644 --- a/go/internal/store/accounts.go +++ b/go/internal/store/accounts.go @@ -106,7 +106,7 @@ func (s *Store) adminByHandle(ctx context.Context, handle string) (Account, erro const q = ` SELECT a.id, a.handle, a.display_name, u.role, - ag.owner_user_id, ag.home_channel_id, ag.persona, ag.parent_agent_id + ag.owner_user_id, ag.home_channel_id, ag.persona, ag.role, ag.parent_agent_id FROM accounts a LEFT JOIN user_accounts u ON u.account_id = a.id LEFT JOIN agent_accounts ag ON ag.account_id = a.id @@ -154,8 +154,8 @@ func (s *Store) CreateAgent(ctx context.Context, ownerUserID AccountID, a NewAge return Account{}, fmt.Errorf("store: insert account: %w", err) } if _, err := tx.Exec(ctx, - "INSERT INTO agent_accounts (account_id, owner_user_id, home_channel_id, persona, parent_agent_id) VALUES ($1, $2, $3, $4, NULLIF($5, ''))", - accountID, string(ownerUserID), channelID, a.Persona, string(a.ParentAgentID), + "INSERT INTO agent_accounts (account_id, owner_user_id, home_channel_id, persona, role, parent_agent_id) VALUES ($1, $2, $3, $4, $5, NULLIF($6, ''))", + accountID, string(ownerUserID), channelID, a.Persona, a.Role, string(a.ParentAgentID), ); err != nil { // Both FKs on agent_accounts land here: parent_agent_id (a supplied // parent that does not resolve to an agent) and owner_user_id (an @@ -212,6 +212,7 @@ func (s *Store) CreateAgent(ctx context.Context, ownerUserID AccountID, a NewAge OwnerUserID: ownerUserID, HomeChannelID: ChannelID(channelID), Persona: a.Persona, + Role: a.Role, ParentAgentID: a.ParentAgentID, }, }, nil @@ -225,7 +226,7 @@ func (s *Store) GetAccount(ctx context.Context, id AccountID) (Account, error) { const q = ` SELECT a.id, a.handle, a.display_name, u.role, - ag.owner_user_id, ag.home_channel_id, ag.persona, ag.parent_agent_id + ag.owner_user_id, ag.home_channel_id, ag.persona, ag.role, ag.parent_agent_id FROM accounts a LEFT JOIN user_accounts u ON u.account_id = a.id LEFT JOIN agent_accounts ag ON ag.account_id = a.id @@ -398,7 +399,7 @@ func (s *Store) ReparentAgent(ctx context.Context, caller, agentAccountID, newPa const q = ` SELECT a.id, a.handle, a.display_name, u.role, - ag.owner_user_id, ag.home_channel_id, ag.persona, ag.parent_agent_id + ag.owner_user_id, ag.home_channel_id, ag.persona, ag.role, ag.parent_agent_id FROM accounts a LEFT JOIN user_accounts u ON u.account_id = a.id LEFT JOIN agent_accounts ag ON ag.account_id = a.id @@ -484,7 +485,7 @@ func (s *Store) AgentByHandle(ctx context.Context, handle string) (Account, erro const q = ` SELECT a.id, a.handle, a.display_name, u.role, - ag.owner_user_id, ag.home_channel_id, ag.persona, ag.parent_agent_id + ag.owner_user_id, ag.home_channel_id, ag.persona, ag.role, ag.parent_agent_id FROM accounts a LEFT JOIN user_accounts u ON u.account_id = a.id LEFT JOIN agent_accounts ag ON ag.account_id = a.id @@ -542,7 +543,7 @@ func (s *Store) ListAccounts(ctx context.Context, visibleTo AccountID) ([]Accoun const q = ` SELECT a.id, a.handle, a.display_name, u.role, - ag.owner_user_id, ag.home_channel_id, ag.persona, ag.parent_agent_id` + + ag.owner_user_id, ag.home_channel_id, ag.persona, ag.role, ag.parent_agent_id` + accountVisibleFromWhere + ` ORDER BY a.handle` rows, err := s.pool.Query(ctx, q, string(visibleTo)) @@ -594,9 +595,10 @@ func scanAccount(row pgx.Row) (Account, error) { ownerUserID *string homeChannelID *string persona *string + agRole *string parentAgentID *string ) - if err := row.Scan(&id, &handle, &displayName, &role, &ownerUserID, &homeChannelID, &persona, &parentAgentID); err != nil { + if err := row.Scan(&id, &handle, &displayName, &role, &ownerUserID, &homeChannelID, &persona, &agRole, &parentAgentID); err != nil { return Account{}, err } acc.ID = AccountID(id) @@ -613,6 +615,9 @@ func scanAccount(row pgx.Row) (Account, error) { if persona != nil { agent.Persona = *persona } + if agRole != nil { + agent.Role = *agRole + } if parentAgentID != nil { agent.ParentAgentID = AccountID(*parentAgentID) } diff --git a/go/internal/store/accounts_test.go b/go/internal/store/accounts_test.go index 2b3a475b..5f2e1085 100644 --- a/go/internal/store/accounts_test.go +++ b/go/internal/store/accounts_test.go @@ -199,6 +199,72 @@ func TestCreateAgentPersonaDefaultsEmpty(t *testing.T) { } } +func TestCreateAgentRoleRoundTrips(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + owner := mustUser(t, s, "owner") + + const role = "manager" + created, err := s.CreateAgent(ctx, owner.ID, + NewAgent{Handle: "agent", DisplayName: "Agent", Role: role}) + if err != nil { + t.Fatalf("CreateAgent: %v", err) + } + if created.Agent == nil || created.Agent.Role != role { + t.Fatalf("CreateAgent returned role = %q, want %q", created.Agent.Role, role) + } + + got, err := s.GetAccount(ctx, created.ID) + if err != nil { + t.Fatalf("GetAccount: %v", err) + } + if !got.IsAgent() || got.Agent.Role != role { + t.Fatalf("GetAccount role = %q, want %q", got.Agent.Role, role) + } + + // The third scanAccount-feeding SELECT: the role must also round-trip + // through the owner-scoped ListAccounts projection, not just the id reads. + listed, err := s.ListAccounts(ctx, owner.ID) + if err != nil { + t.Fatalf("ListAccounts(owner): %v", err) + } + var found *Account + for i := range listed { + if listed[i].ID == created.ID { + found = &listed[i] + break + } + } + if found == nil { + t.Fatalf("ListAccounts(owner) did not return created agent %s", created.ID) + } + if !found.IsAgent() || found.Agent.Role != role { + t.Fatalf("ListAccounts role = %q, want %q", found.Agent.Role, role) + } +} + +func TestCreateAgentRoleDefaultsEmpty(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + owner := mustUser(t, s, "owner") + + created, err := s.CreateAgent(ctx, owner.ID, NewAgent{Handle: "agent", DisplayName: "Agent"}) + if err != nil { + t.Fatalf("CreateAgent: %v", err) + } + if created.Agent == nil || created.Agent.Role != "" { + t.Fatalf("CreateAgent default role = %q, want empty", created.Agent.Role) + } + + got, err := s.GetAccount(ctx, created.ID) + if err != nil { + t.Fatalf("GetAccount: %v", err) + } + if !got.IsAgent() || got.Agent.Role != "" { + t.Fatalf("GetAccount default role = %q, want empty", got.Agent.Role) + } +} + func TestBootstrapAdminCreatesAdmin(t *testing.T) { ctx := context.Background() s := newTestStore(t) diff --git a/go/internal/store/inputs.go b/go/internal/store/inputs.go index 84d7a9f9..c25b80bd 100644 --- a/go/internal/store/inputs.go +++ b/go/internal/store/inputs.go @@ -24,6 +24,12 @@ type NewAgent struct { // Empty means no persona override; the caller supplies it and the server // stores it verbatim, never synthesizing one. Persona string + // Role is the agent's operator-set block-0 selector (SEA-1732 T10). Empty + // means no role (default OMP block-0); the caller supplies it and the server + // stores it verbatim, never synthesizing one. Unlike Persona (an append + // overlay), the label selects config/prompts//SYSTEM.md, delivered as + // the container's customSystemPrompt. + Role string // ParentAgentID is the agent's parent in the agent tree; empty = root. The // server validates it (same resolved owner, must exist) before insert; an // empty value inserts SQL NULL, satisfying the FK. diff --git a/go/internal/store/migrations/0015_agent_role.sql b/go/internal/store/migrations/0015_agent_role.sql new file mode 100644 index 00000000..48c52830 --- /dev/null +++ b/go/internal/store/migrations/0015_agent_role.sql @@ -0,0 +1,15 @@ +-- 0015_agent_role: the agent's operator-set role selector (SEA-1732 T10). +-- role is the operator-set selector for the agent's block-0 system prompt, its +-- source-of-truth field on the agent account (Matt-ruled 2026-08-07: source = +-- AgentAccount, mirroring persona). Where persona is an APPEND overlay layered +-- after the default prompt, role REPLACES block-0 via customSystemPrompt: the +-- label selects config/prompts//SYSTEM.md from the mounted config tree. +-- Empty string means no role — the agent keeps OMP's default block-0. It is +-- read at provision time and materialized to the container by the runner (a +-- separate lane); this migration is only the durable column + the store +-- round-trip that carries it. +-- +-- NOT NULL DEFAULT '' so every existing and future agent row always has a value: +-- a joined agent row reads the empty default rather than NULL, matching the +-- "empty = no role" contract and keeping the create/read path branch-free. +ALTER TABLE agent_accounts ADD COLUMN role TEXT NOT NULL DEFAULT ''; diff --git a/go/internal/store/types.go b/go/internal/store/types.go index 572a885b..e9942634 100644 --- a/go/internal/store/types.go +++ b/go/internal/store/types.go @@ -137,6 +137,11 @@ type AgentAccount struct { // Persona is the agent's system-prompt text, baked at creation (SEA-1571); // empty means no persona override. Persona string + // Role is the agent's operator-set block-0 selector (SEA-1732 T10); empty + // means no role (default OMP block-0). Unlike Persona (an append overlay), + // the label selects config/prompts//SYSTEM.md, delivered as the + // container's customSystemPrompt. + Role string // ParentAgentID is the agent's parent in the agent tree; empty = root. Set // at creation and editable via ReparentAgent (comms.proto). ParentAgentID AccountID diff --git a/go/server/lifecycle.go b/go/server/lifecycle.go index a156ee19..2626dadf 100644 --- a/go/server/lifecycle.go +++ b/go/server/lifecycle.go @@ -131,14 +131,16 @@ func (l *lifecycleService) SpawnAsAccount( return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("resolving caller owner: %w", err)) } - // Persona is server-authoritative and empty on spawn (SpawnPeerRequest - // carries none): the new account is created with no persona, and the value - // threaded to the Runner comes from the store account below, never the - // caller — a caller cannot inject a system prompt. + // 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. created, err := l.store.CreateAgent(ctx, callerOwner, store.NewAgent{ Handle: req.GetHandle(), DisplayName: req.GetDisplayName(), Persona: "", + Role: "", // 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. @@ -146,7 +148,7 @@ func (l *lifecycleService) SpawnAsAccount( }) switch { case err == nil: - return l.provisionAndStart(ctx, created.ID, created.Agent.Persona, req) + return l.provisionAndStart(ctx, created.ID, created.Agent.Persona, created.Agent.Role, req) case errors.Is(err, store.ErrConflict): return l.resumeOrReject(ctx, callerOwner, req) default: @@ -280,7 +282,7 @@ func (l *lifecycleService) resumeOrReject( case errors.Is(err, store.ErrNotFound): // Unplaced: a spawn that crashed after CreateAgent (or was rolled back). // Resume — re-provision and start the existing account, not a second. - return l.provisionAndStart(ctx, existing.ID, existing.Agent.Persona, req) + return l.provisionAndStart(ctx, existing.ID, existing.Agent.Persona, existing.Agent.Role, req) default: return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("resolving placement for existing agent: %w", err)) } @@ -291,18 +293,21 @@ func (l *lifecycleService) resumeOrReject( // placement, Start, record the session ownership. On any post-Provision failure // it rolls the container back (bounded Stop + Remove + DeleteAgentPlacement) so // the account is left UNPLACED and the handle is not burned — a re-spawn of the -// same handle then resumes. persona is the store's server-authoritative value -// for the account, threaded to the Runner so no caller value is trusted. +// same handle then resumes. persona and role are the store's server- +// authoritative values for the account, threaded to the Runner so no caller +// value is trusted. func (l *lifecycleService) provisionAndStart( ctx context.Context, agentID store.AccountID, persona string, + role string, req *compassv1internal.SpawnPeerRequest, ) (*compassv1internal.SpawnPeerResponse, error) { resp, runnerID, err := l.hub.Provision(ctx, req.GetClientRequestId(), &compassv1.ProvisionAgentWorkspaceRequest{ AgentAccountId: string(agentID), ClientRequestId: req.GetClientRequestId(), Persona: persona, + Role: role, }) if err != nil { // Already Connect-coded by the hub relay — return it for in-band render. diff --git a/go/server/service.go b/go/server/service.go index fd57ba73..ad667c62 100644 --- a/go/server/service.go +++ b/go/server/service.go @@ -131,11 +131,12 @@ func (s *service) ProvisionAgentWorkspace( if s.hub == nil { return nil, connect.NewError(connect.CodeUnavailable, errNoRunnerHub) } - // SERVER-AUTHORITATIVE persona (proto compass.proto persona=6): populate the - // outgoing persona from the store's AgentAccount, overwriting whatever the - // client sent, so a caller cannot inject a system prompt. A non-agent account - // carries no persona, but the client value is still cleared for the same - // reason. The Runner receives this on the same relayed req.Msg. + // SERVER-AUTHORITATIVE persona (compass.proto persona=6) and role (role=7): + // populate the outgoing persona and role from the store's AgentAccount, + // overwriting whatever the client sent, so a caller cannot inject a system + // prompt or a role prompt. A non-agent account carries neither, but the + // client values are still cleared for the same reason. The Runner receives + // these on the same relayed req.Msg. acc, err := s.store.GetAccount(ctx, store.AccountID(req.Msg.GetAgentAccountId())) if err != nil { if errors.Is(err, store.ErrNotFound) { @@ -145,8 +146,10 @@ func (s *service) ProvisionAgentWorkspace( } if acc.IsAgent() { req.Msg.Persona = acc.Agent.Persona + req.Msg.Role = acc.Agent.Role } else { req.Msg.Persona = "" + req.Msg.Role = "" } resp, runnerID, err := s.hub.Provision(ctx, req.Msg.GetClientRequestId(), req.Msg) if err != nil { diff --git a/go/server/service_placement_pgtest_test.go b/go/server/service_placement_pgtest_test.go index e29a14c6..03a2d3e5 100644 --- a/go/server/service_placement_pgtest_test.go +++ b/go/server/service_placement_pgtest_test.go @@ -394,6 +394,80 @@ func TestProvisionAgentWorkspaceClearsPersonaForNonAgentAccount(t *testing.T) { } } +// TestProvisionAgentWorkspaceOverwritesRoleFromStore pins the +// server-authoritative role invariant: on the provision path the Server +// populates the outgoing role from the store's AgentAccount.role and overwrites +// any client-supplied value, so a caller cannot inject a role prompt (proto +// compass.proto role=7). The client sends a bogus role; the Runner must receive +// the store's value instead. Under the pre-fix handler the client value passes +// straight through and the Runner sees "client-injected-evil". +func TestProvisionAgentWorkspaceOverwritesRoleFromStore(t *testing.T) { + f := newPlacementFixture(t) + ctx := context.Background() // the test root context + + // The fixture's default agent has an empty role, so seed a second agent + // under the same owner with a real role to prove the read-through. + seed, err := f.store.GetAccount(ctx, f.agentID) + if err != nil { + t.Fatalf("GetAccount(%q): %v", f.agentID, err) + } + const wantRole = "manager" + roleAgent, err := f.store.CreateAgent(ctx, seed.Agent.OwnerUserID, store.NewAgent{ + Handle: "withrole", + DisplayName: "R", + Role: wantRole, + }) + if err != nil { + t.Fatalf("CreateAgent(withrole): %v", err) + } + + f.runner.forget() // discard the attach probe + if _, err := f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{ + AgentAccountId: string(roleAgent.ID), + ClientRequestId: "prov-role", + Role: "client-injected-evil", + })); err != nil { + t.Fatalf("ProvisionAgentWorkspace = %v, want success", err) + } + + if got := f.runner.provisionRole(t); got != wantRole { + t.Fatalf("Runner received role %q, want %q (server must overwrite the client value)", got, wantRole) + } +} + +// TestProvisionAgentWorkspaceClearsRoleForNonAgentAccount pins the non-agent +// branch of the server-authoritative role invariant: when a user-account id is +// passed as agent_account_id, the store read-through finds a non-agent account +// (acc.IsAgent()==false) and must clear the client-supplied role to empty, so a +// caller cannot inject a role prompt via a non-agent account. +func TestProvisionAgentWorkspaceClearsRoleForNonAgentAccount(t *testing.T) { + f := newPlacementFixture(t) + ctx := context.Background() // the test root context + + // The admin (a user account, not an agent) is the fixture agent's owner. + seed, err := f.store.GetAccount(ctx, f.agentID) + if err != nil { + t.Fatalf("GetAccount(%q): %v", f.agentID, err) + } + adminID := seed.Agent.OwnerUserID + + f.runner.forget() // discard the attach probe + // This call is EXPECTED to error: admin is a user account, absent from + // agent_accounts, so the placement write fails on its FK (CodeInternal). The + // role-clear is still observable because the Provision command is recorded + // (role cleared) before the placement write runs. The error is expected and + // not what this test pins, so it is deliberately discarded. + _, _ = f.client.ProvisionAgentWorkspace(ctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{ + AgentAccountId: string(adminID), + ClientRequestId: "prov-nonagent-role", + Role: "client-injected-evil", + })) + + if got := f.runner.provisionRole(t); got != "" { + t.Fatalf("Runner received role %q for a non-agent account, want empty (client value must be cleared)", got) + } +} + // TestProvisionAgentWorkspaceUnknownAccountIsNotFound pins that the persona // read-through fails closed: an unknown agent_account_id yields CodeNotFound, // short-circuiting container creation before any Provision or placement. @@ -785,6 +859,23 @@ func (r *recordingRunner) provisionPersona(t *testing.T) string { return "" } +// provisionRole returns the role on the recorded Provision command — the value +// the Runner actually received on the wire. Fails if no Provision was seen, so +// a silent miss cannot masquerade as an empty role. +func (r *recordingRunner) provisionRole(t *testing.T) string { + t.Helper() + r.mu.Lock() + defer r.mu.Unlock() + for _, c := range r.seen { + switch v := c.GetCommand().(type) { + case *compassv1internal.SessionsResponse_Provision: + return v.Provision.GetRole() + } + } + t.Fatalf("no Provision command recorded, saw %v", r.commands()) + return "" +} + // runnerBearer stamps the Runner's bearer on every outbound RPC (unary + // streaming), mirroring the Runner-side interceptor so the door authenticates. type runnerBearer string diff --git a/packages/compass-agent/src/cli.test.ts b/packages/compass-agent/src/cli.test.ts index c35c6782..d2c28967 100644 --- a/packages/compass-agent/src/cli.test.ts +++ b/packages/compass-agent/src/cli.test.ts @@ -32,6 +32,7 @@ import type { } from "@oh-my-pi/pi-coding-agent"; import { SessionManager } from "@oh-my-pi/pi-coding-agent"; import { serializeTitleSlot } from "@oh-my-pi/pi-coding-agent/session/session-title-slot"; +import { buildSystemPrompt } from "@oh-my-pi/pi-coding-agent/system-prompt"; import { AGENT_SOCKET_PATH, authSeedPath, @@ -42,6 +43,7 @@ import { parseEnvFile, resolveModelSelector, resolvePersona, + resolveRole, } from "./cli"; import { CommsBroker, createCommsTools } from "./comms"; import { @@ -156,6 +158,29 @@ describe("resolvePersona", () => { }); }); +// COMPASS_ROLE is the server-authoritative block-0 role selector (SEA-1732 T10). +// The entrypoint resolves the LABEL here (trimmed; blank → unset), then reads its +// `prompts//SYSTEM.md` from the mount and injects it as customSystemPrompt. +// Same unset/trim semantics as the model selector and persona. +describe("resolveRole", () => { + test("returns the COMPASS_ROLE value when set", () => { + expect(resolveRole({ COMPASS_ROLE: "manager" })).toBe("manager"); + }); + + test("returns undefined when COMPASS_ROLE is unset (default block-0 applies)", () => { + expect(resolveRole({})).toBeUndefined(); + }); + + test("treats an empty or whitespace-only value as unset", () => { + expect(resolveRole({ COMPASS_ROLE: "" })).toBeUndefined(); + expect(resolveRole({ COMPASS_ROLE: " " })).toBeUndefined(); + }); + + test("trims surrounding whitespace so a padded env value still resolves", () => { + expect(resolveRole({ COMPASS_ROLE: " supervisor " })).toBe("supervisor"); + }); +}); + // The seed path is the frozen T5 placement: a 0600 `$HOME/.compass/auth-seed.json` // written by the Runner's materializer. describe("authSeedPath", () => { @@ -1608,6 +1633,11 @@ interface SeenConfig { customTools?: unknown[]; enableMCP?: boolean; autoApprove?: boolean; + customSystemPrompt?: string; + systemPrompt?: + | string + | string[] + | ((defaultPrompt: string[]) => string | string[]); } // The `name` of each captured skill, narrowing with `in`/`typeof` (no fabricated @@ -1859,6 +1889,239 @@ describe("main wires the mounted agent-config into createAgentSession", () => { expect(seen[0].customTools).toHaveLength(6); }); + // ── SEA-1732 T10: COMPASS_ROLE → prompts//SYSTEM.md → customSystemPrompt ── + // + // The role selector delivers a per-role block-0 as `customSystemPrompt` (which + // REPLACES OMP's default block-0), while persona STILL appends AFTER (record + // §OQ-8). These pin the four compose states at the createSession seam; the + // MP-1 render property (skills/rules/footer survival + read-tool gate) is the + // SDK-render test that follows. + test("COMPASS_ROLE with a shipped prompt → its text reaches customSystemPrompt", async () => { + const mount = scratch(); + writeMount( + mount, + "prompts/manager/SYSTEM.md", + "# Manager\nYou coordinate the fleet.\n", + ); + const session = fakeSession(); + const seen: SeenConfig[] = []; + await main( + { HOME: scratch(), COMPASS_ROLE: "manager" }, + { + configMount: mount, + createSession: (options) => { + seen.push({ + customSystemPrompt: options.customSystemPrompt, + systemPrompt: options.systemPrompt, + }); + return Promise.resolve({ + session: session as unknown as AgentSession, + }); + }, + createTransport: () => + fakeCarrier(emptyLog(), { control: emptyControlStream }), + }, + ); + expect(seen).toHaveLength(1); + // The role prompt's TEXT (block-0 replacement) reached customSystemPrompt. + // Non-vacuity: dropping the `customSystemPrompt: rolePrompt` spread leaves + // this undefined → red. + expect(seen[0].customSystemPrompt).toBe( + "# Manager\nYou coordinate the fleet.\n", + ); + // No persona set → no append customizer; behavior is role-replace only. + expect(seen[0].systemPrompt).toBeUndefined(); + }); + + test("COMPASS_ROLE set but NO prompt file → falls back to today's behavior (no customSystemPrompt)", async () => { + // A role the operator set but the config bundle never shipped a + // prompts//SYSTEM.md for. The reader returns undefined, so main must + // OMIT customSystemPrompt entirely — never inject an empty replace, which + // would still route through the block-0-replacing custom template. + const mount = scratch(); + const session = fakeSession(); + const seen: SeenConfig[] = []; + await main( + { HOME: scratch(), COMPASS_ROLE: "ghost" }, + { + configMount: mount, + createSession: (options) => { + seen.push({ customSystemPrompt: options.customSystemPrompt }); + return Promise.resolve({ + session: session as unknown as AgentSession, + }); + }, + createTransport: () => + fakeCarrier(emptyLog(), { control: emptyControlStream }), + }, + ); + expect(seen).toHaveLength(1); + expect(seen[0].customSystemPrompt).toBeUndefined(); + }); + + test("COMPASS_ROLE unset → no customSystemPrompt (exactly today's behavior)", async () => { + const mount = scratch(); + writeMount(mount, "prompts/manager/SYSTEM.md", "# Manager\n"); + const session = fakeSession(); + const seen: SeenConfig[] = []; + await main( + { HOME: scratch() }, + { + configMount: mount, + createSession: (options) => { + seen.push({ customSystemPrompt: options.customSystemPrompt }); + return Promise.resolve({ + session: session as unknown as AgentSession, + }); + }, + createTransport: () => + fakeCarrier(emptyLog(), { control: emptyControlStream }), + }, + ); + expect(seen).toHaveLength(1); + // Non-vacuity: reading the role prompt unconditionally (not gated on a set + // role) would surface the shipped manager prompt here → red. + expect(seen[0].customSystemPrompt).toBeUndefined(); + }); + + test("role + persona compose: role reaches customSystemPrompt AND persona appends after", async () => { + // The OQ-8 composition: customSystemPrompt (role, REPLACE block-0) and the + // systemPrompt append customizer (persona) are ORTHOGONAL keys, so both + // apply. The customizer runs over whatever default array the SDK built — + // which, with a role, already carries the role block-0 — so persona lands + // LAST, after the role block. Drive the customizer with a fake default that + // stands in for [role block-0, …skills/rules, project footer]. + const mount = scratch(); + writeMount(mount, "prompts/manager/SYSTEM.md", "# Manager block-0\n"); + const session = fakeSession(); + const seen: SeenConfig[] = []; + await main( + { + HOME: scratch(), + COMPASS_ROLE: "manager", + COMPASS_PERSONA: "You are Ada.", + }, + { + configMount: mount, + createSession: (options) => { + seen.push({ + customSystemPrompt: options.customSystemPrompt, + systemPrompt: options.systemPrompt, + }); + return Promise.resolve({ + session: session as unknown as AgentSession, + }); + }, + createTransport: () => + fakeCarrier(emptyLog(), { control: emptyControlStream }), + }, + ); + expect(seen).toHaveLength(1); + expect(seen[0].customSystemPrompt).toBe("# Manager block-0\n"); + const customizer = seen[0].systemPrompt; + if (typeof customizer !== "function") { + throw new Error("systemPrompt was not the append customizer function"); + } + // Persona appends AFTER the role block (and everything else the SDK built). + expect( + customizer(["# Manager block-0", "skills+rules", "project footer"]), + ).toEqual([ + "# Manager block-0", + "skills+rules", + "project footer", + "You are Ada.", + ]); + }); + + // ── MP-1 PROPERTY (frozen record §MP-1) ─────────────────────────────────── + // + // Passing a role prompt as `customSystemPrompt` REPLACES OMP's block-0 — but + // the SDK's custom-system-prompt template STILL auto-injects skills + rules, + // and the project footer stays a separate block. Two pins the record names: + // (1) skills injection is GATED on the `read` tool being in the tool set + // (system-prompt.ts:819-820) — so the tool set MUST retain `read`; + // (2) the rendered prompt RETAINS skills + rules + the project footer even + // though block-0 is the role text, not the default. + // This is a real SDK render (buildSystemPrompt), not a seam spy: it exercises + // the actual template the SDK routes customSystemPrompt through. + test("MP-1: a role prompt as customSystemPrompt REPLACES block-0 while skills, rules, and the project footer survive (read-tool gate held)", async () => { + const roleBlock0 = "ROLE-BLOCK-0-SENTINEL: you are the manager."; + const { systemPrompt } = await buildSystemPrompt({ + cwd: scratch(), + // The role prompt injected as the block-0 replacement. + resolvedCustomPrompt: roleBlock0, + // The read tool is the skills-injection gate (system-prompt.ts:819). + toolNames: ["read"], + skills: [ + { + name: "mp1-skill", + path: "/mnt/skills/mp1-skill/SKILL.md", + content: "# skill", + level: "user", + _source: { + provider: "compass-config", + path: "/mnt/skills/mp1-skill/SKILL.md", + level: "user", + }, + }, + ] as never, + rules: [ + { + name: "mp1-rule", + description: "MP1-RULE-SENTINEL constraint", + path: "/mnt/rules/mp1-rule.md", + }, + ], + }); + const rendered = systemPrompt.join("\n\n"); + // (1) block-0 is REPLACED: the role text is present… + expect(rendered).toContain(roleBlock0); + // …and the default block-0's opening sentinel is GONE. + expect(rendered).not.toContain( + "You are a helpful assistant the team trusts with load-bearing changes", + ); + // (2) skills survived (the custom template's list) — this is the + // read-tool gate holding: drop `read` from toolNames and this vanishes. + expect(rendered).toContain("mp1-skill"); + expect(rendered).toContain(""); + // (2) rules survived (the custom template's list). + expect(rendered).toContain("MP1-RULE-SENTINEL"); + expect(rendered).toContain(""); + // (2) the project footer survived as its own block (environment + cwd). + expect(rendered).toContain("PROJECT"); + expect(rendered).toContain("current working directory"); + // The read tool stayed in the set (the gate's precondition). + expect(systemPrompt.join("\n")).toContain("read"); + }); + + test("MP-1 gate: WITHOUT the read tool the custom template drops the skills list (proves the gate is live)", async () => { + // The non-vacuity companion to the property above: skills injection is + // GATED on `read` (system-prompt.ts:819-820). With no read tool the same + // role-as-customSystemPrompt render must NOT carry the skills list — so the + // Compass tool set retaining `read` is load-bearing, not incidental. + const { systemPrompt } = await buildSystemPrompt({ + cwd: scratch(), + resolvedCustomPrompt: "ROLE-BLOCK-0-SENTINEL", + toolNames: [], + skills: [ + { + name: "mp1-skill", + path: "/mnt/skills/mp1-skill/SKILL.md", + content: "# skill", + level: "user", + _source: { + provider: "compass-config", + path: "/mnt/skills/mp1-skill/SKILL.md", + level: "user", + }, + }, + ] as never, + }); + const rendered = systemPrompt.join("\n\n"); + expect(rendered).not.toContain("mp1-skill"); + expect(rendered).not.toContain(""); + }); + // The MCP manager teardown — what main() alone owns (the SDK never // disconnects a manager it did not build). disconnect must run on BOTH the // clean and error paths, or the container leaks every MCP subprocess/HTTP diff --git a/packages/compass-agent/src/cli.ts b/packages/compass-agent/src/cli.ts index 76b816d0..778c592f 100644 --- a/packages/compass-agent/src/cli.ts +++ b/packages/compass-agent/src/cli.ts @@ -8,8 +8,11 @@ // per container (`internal/runner/host.go:33`), chosen "so the agent needs no // per-session configuration" (`host.go:28-29`); // - the model selector from `COMPASS_MODEL`; -// - the persona identity overlay from `COMPASS_PERSONA`, appended to the -// agent's default system prompt; +// - the block-0 role selector from `COMPASS_ROLE`, naming a +// `prompts//SYSTEM.md` in the mount that REPLACES the agent's default +// block-0 (delivered as `customSystemPrompt`); +// - the persona identity overlay from `COMPASS_PERSONA`, appended AFTER the +// agent's default system prompt (or after the role block-0 when both set); // - the provider credential from the 0600 `$HOME/.compass/auth-seed.json` the // Runner's materializer writes (design §T5); // - the materialized tool/MCP secrets from the 0600 `$HOME/.compass/env` the @@ -53,8 +56,10 @@ import { CompassAgent } from "./agent"; import { CommsBroker, createCommsTools } from "./comms"; import { AGENT_CONFIG_MOUNT_PATH, + currentConfigDir, loadMountedConfig, type MountedMcp, + readMountedRolePrompt, } from "./config-reader"; import type { FrameSink } from "./frame"; import { createLifecycleTools, LifecycleBroker } from "./lifecycle"; @@ -155,6 +160,26 @@ export function resolvePersona( return raw ? raw : undefined; } +/** + * The block-0 role for this container, from `COMPASS_ROLE`. + * + * A REPLACEMENT selector, not an overlay: the label names a + * `prompts//SYSTEM.md` in the mount, whose text `main` injects as + * `customSystemPrompt` — replacing OMP's default block-0 (persona still appends + * AFTER, record §OQ-8). This function only resolves the LABEL; the file lookup + + * fallback (absent file → today's behavior) live in `main`. + * + * Unset (or blank) is a legitimate configuration: the Runner empty-omits the env + * var (`go/internal/runner/agent_exec.go` `execSpec`), so an absent role leaves + * the agent on its default block-0. Same unset/trim semantics as `resolvePersona`. + */ +export function resolveRole( + env: Record, +): string | undefined { + const raw = env.COMPASS_ROLE?.trim(); + return raw ? raw : undefined; +} + /** One provider's credential in the seed file. Mirrors the SDK's `ApiKeyCredential`. */ interface SeedEntry { readonly type?: string; @@ -489,6 +514,13 @@ export async function main( // already normalized a blank value to undefined. const persona = resolvePersona(env); + // The block-0 role selector; undefined when unset or whitespace-only. When + // set, `main` reads its `prompts//SYSTEM.md` from the mount (below) and + // injects it as `customSystemPrompt` — REPLACING OMP's default block-0. The + // resolve here only yields the LABEL; the file lookup + fallback live below, + // after the mount is loaded. + const role = resolveRole(env); + // The workdir the session is keyed to. `||`, not `??`: an empty or // whitespace-only COMPASS_WORKDIR is unset, not a valid cwd. The Runner sets // it unconditionally (relay.go `execSpec`), so a caller that builds an @@ -591,6 +623,21 @@ export async function main( console.error(`[compass-agent] config version: ${mounted.version}`); } + // The role's block-0 prompt (SEA-1732 T10): when a role is set, read its + // `prompts//SYSTEM.md` from the same mount and inject it below as + // `customSystemPrompt` — REPLACING OMP's default block-0. The read is + // tolerant (absent/empty file → undefined), so a set-but-unshipped role + // FALLS BACK to today's behavior (no customSystemPrompt) rather than + // injecting an empty replace. The mount is read through `current/`, the + // symlink the Runner flips, so a ConfigVersion flip stays live. Persona still + // appends AFTER this block (record §OQ-8) — see the createSession call. + const rolePrompt = role + ? await readMountedRolePrompt( + currentConfigDir(deps.configMount ?? AGENT_CONFIG_MOUNT_PATH), + role, + ) + : undefined; + // Fleet OMP config passthrough (SEA-1678, design compass-agent-config-passthrough // §CP-1/CP-2/CP-4), applied AFTER loadMountedConfig and BEFORE // createAgentSession. Matt's pivot: the mount stays the delivery vehicle, but @@ -709,8 +756,23 @@ export async function main( rules, ...(contextFiles ? { contextFiles } : {}), ...(fleetSettings ? { settingsManager: fleetSettings } : {}), - // Persona is an identity OVERLAY, not a replacement: append it after the - // default prompt so block-0 base instructions + project footer survive. + // Role (SEA-1732 T10) + persona compose INDEPENDENTLY, and BOTH apply: + // - `customSystemPrompt` (role): the role's block-0 text, routed through + // the SDK's custom-system-prompt template (sdk.ts:2727) — REPLACES + // OMP's default block-0 while the template STILL injects skills + rules + // and the project footer stays a separate block (record §MP-1). Passed + // ONLY when a role prompt was found; absent → key omitted → today's + // default block-0 (no empty replace). + // - `systemPrompt` (persona): the identity OVERLAY, APPENDED after the + // built default array (record §OQ-8: persona appends AFTER the role + // block). The callback transforms whatever `defaultPrompt` the SDK + // built — with a role, that array already carries the role block-0 + + // skills/rules/footer, so persona lands LAST, after the role block. + // Passed ONLY when a persona is set. + // The two are orthogonal keys, so all four states compose: neither, role + // only (replace block-0), persona only (append, today's behavior), both + // (role replaces block-0, persona appends after). + ...(rolePrompt ? { customSystemPrompt: rolePrompt } : {}), ...(persona ? { systemPrompt: (defaultPrompt: string[]) => [ diff --git a/packages/compass-agent/src/config-reader.ts b/packages/compass-agent/src/config-reader.ts index 6a0c8d9e..32d1a845 100644 --- a/packages/compass-agent/src/config-reader.ts +++ b/packages/compass-agent/src/config-reader.ts @@ -14,6 +14,9 @@ // - `mcp/.json` — MCP server configs (credential-free by MVP rule; // servers read tokens from the aggregate env file // main() already sources into process.env) +// - `prompts//SYSTEM.md` — per-role block-0 system prompts; the +// operator-set role label selects one, delivered as +// `customSystemPrompt` (REPLACES block-0) // - `version` — bundle hash, observability only // // UNCONFIGURED — no `current` symlink, or the whole mount absent — is a VALID @@ -352,6 +355,32 @@ export async function readMountedAgentsMd( } } +/** + * The role's block-0 system prompt `prompts//SYSTEM.md` (SEA-1732 T10), + * read as TEXT for direct injection via `createAgentSession({ customSystemPrompt })` + * — which REPLACES OMP's block-0 (routed through the SDK's custom-system-prompt + * template, sdk.ts:2727). The operator-set role label (COMPASS_ROLE) selects the + * subtree; `main()` reads it only when a role is set. + * + * Tolerant, mirroring the other per-surface readers: an absent file, an + * unreadable one, or an EMPTY/whitespace-only body → `undefined`, never throws — + * so an unconfigured role falls back to the default block-0 rather than injecting + * a blank customSystemPrompt (which would still route through the replace path). + */ +export async function readMountedRolePrompt( + currentDir: string, + role: string, +): Promise { + const path = join(currentDir, "prompts", role, "SYSTEM.md"); + try { + if (!(await stat(path)).isFile()) return undefined; + const content = await Bun.file(path).text(); + return content.trim() ? content : undefined; + } catch { + return undefined; + } +} + /** * The fleet `models.yml` (CP-4), when present as a top-level regular file. * `main()` symlinks it to the user-level agent dir so the SDK's `ModelRegistry` diff --git a/packages/compass-agent/src/gen/compass/v1/compass_pb.ts b/packages/compass-agent/src/gen/compass/v1/compass_pb.ts index 14129a01..c558fc8c 100644 --- a/packages/compass-agent/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-agent/src/gen/compass/v1/compass_pb.ts @@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file compass/v1/compass.proto. */ export const file_compass_v1_compass: GenFile = /*@__PURE__*/ - fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkIlYKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZSJJChFBZ2VudE1lc3NhZ2VDaHVuaxISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHRleHQYAiABKAkSEgoKaXNfdGhvdWdodBgDIAEoCCJ5Cg1BZ2VudFRvb2xDYWxsEhIKCnNlc3Npb25faWQYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJEg0KBXRpdGxlGAMgASgJEi8KBnN0YXR1cxgEIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cyJMCglBZ2VudFBsYW4SEgoKc2Vzc2lvbl9pZBgBIAEoCRIrCgdlbnRyaWVzGAIgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSJTCg5BZ2VudFBsYW5FbnRyeRIPCgdjb250ZW50GAEgASgJEjAKBnN0YXR1cxgCIAEoDjIgLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnlTdGF0dXMi8QIKDFNlc3Npb25FdmVudBIQCghldmVudF9pZBgBIAEoCRISCgphdF91bml4X21zGAIgASgDEjoKDmFzc2lzdGFudF90ZXh0GAMgASgLMiAuY29tcGFzcy52MS5TZXNzaW9uQXNzaXN0YW50VGV4dEgAEi8KCHRoaW5raW5nGAQgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVGhpbmtpbmdIABIwCgl0b29sX2NhbGwYBSABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbEgAEj0KEHRvb2xfY2FsbF91cGRhdGUYBiABKAsyIS5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbFVwZGF0ZUgAEicKBHBsYW4YByABKAsyFy5jb21wYXNzLnYxLlNlc3Npb25QbGFuSAASKwoGbm90aWNlGAggASgLMhkuY29tcGFzcy52MS5TZXNzaW9uTm90aWNlSABCBwoFZXZlbnQiOAoUU2Vzc2lvbkFzc2lzdGFudFRleHQSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJIjMKD1Nlc3Npb25UaGlua2luZxIMCgR0ZXh0GAEgASgJEhIKCm1lc3NhZ2VfaWQYAiABKAkiZwoPU2Vzc2lvblRvb2xDYWxsEhQKDHRvb2xfY2FsbF9pZBgBIAEoCRINCgV0aXRsZRgCIAEoCRIvCgZzdGF0dXMYAyABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMimgEKFVNlc3Npb25Ub29sQ2FsbFVwZGF0ZRIUCgx0b29sX2NhbGxfaWQYASABKAkSLwoGc3RhdHVzGAIgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzEg4KBm91dHB1dBgDIAEoCRIqCgVkaWZmcxgEIAMoCzIbLmNvbXBhc3MudjEuU2Vzc2lvbkZpbGVEaWZmIlUKD1Nlc3Npb25GaWxlRGlmZhIMCgRwYXRoGAEgASgJEhUKCG9sZF90ZXh0GAIgASgJSACIAQESEAoIbmV3X3RleHQYAyABKAlCCwoJX29sZF90ZXh0IjoKC1Nlc3Npb25QbGFuEisKB2VudHJpZXMYASADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IjkKDVNlc3Npb25Ob3RpY2USDAoEdGV4dBgBIAEoCRIRCgRsaW5rGAIgASgJSACIAQFCBwoFX2xpbmsiMgocU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIn4KEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUilQEKHlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVxdWVzdBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAUgASgJEg8KB3BlcnNvbmEYBiABKAlKBAgCEANKBAgDEARKBAgEEAVSCnJlbW90ZV91cmxSCmxvY2FsX3BhdGhSA3JlZiI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImUKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIWCg5pbml0aWFsX3Byb21wdBgCIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCSIvChlTdGFydEFnZW50U2Vzc2lvblJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLwoZUmVsb2FkQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIjAKGlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkiKwoVR2V0QWdlbnRTdGF0dXNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiSgoWR2V0QWdlbnRTdGF0dXNSZXNwb25zZRIwCghzdGF0dXNlcxgBIAMoCzIeLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdHVzIicKEUlzc3VlVG9rZW5SZXF1ZXN0EhIKCmFjY291bnRfaWQYASABKAkiIwoSSXNzdWVUb2tlblJlc3BvbnNlEg0KBXRva2VuGAEgASgJIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3QiyQEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgiGgoYRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0IhsKGURlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2UiTAoQQWdlbnRBdHRyaWJ1dGlvbhIUCgxhZ2VudF9oYW5kbGUYASABKAlKBAgCEANKBAgDEARSDG93bmVyX2hhbmRsZVIIdmVyaWZpZWQiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSKkAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiKeAwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZCJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iQwoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkiRwoGUmV2aWV3Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDwoHdmVyZGljdBgDIAEoCRIMCgRib2R5GAQgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKkMKC1NlcnZlclN0YXRlEhwKGFNFUlZFUl9TVEFURV9VTlNQRUNJRklFRBAAEhYKElNFUlZFUl9TVEFURV9SRUFEWRABKoICChFBZ2VudFNlc3Npb25TdGF0ZRIjCh9BR0VOVF9TRVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAASIAocQUdFTlRfU0VTU0lPTl9TVEFURV9TVEFSVElORxABEh0KGUFHRU5UX1NFU1NJT05fU1RBVEVfUkVBRFkQAhIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1dPUktJTkcQAxIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1NUT1BQRUQQBBIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX0VSUk9SRUQQBRIkCiBBR0VOVF9TRVNTSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBAGKtIBChNBZ2VudFRvb2xDYWxsU3RhdHVzEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIiCh5BR0VOVF9UT09MX0NBTExfU1RBVFVTX1BFTkRJTkcQARImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX0lOX1BST0dSRVNTEAISJAogQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19DT01QTEVURUQQAxIhCh1BR0VOVF9UT09MX0NBTExfU1RBVFVTX0ZBSUxFRBAEKrQBChRBZ2VudFBsYW5FbnRyeVN0YXR1cxInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19VTlNQRUNJRklFRBAAEiMKH0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1BFTkRJTkcQARInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19JTl9QUk9HUkVTUxACEiUKIUFHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0NPTVBMRVRFRBADKvEBCgpJc3N1ZVN0YXRlEhsKF0lTU1VFX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSVNTVUVfU1RBVEVfQkFDS0xPRxABEhQKEElTU1VFX1NUQVRFX1RPRE8QAhIWChJJU1NVRV9TVEFURV9RVUVVRUQQAxIXChNJU1NVRV9TVEFURV9CTE9DS0VEEAQSGwoXSVNTVUVfU1RBVEVfSU5fUFJPR1JFU1MQBRIZChVJU1NVRV9TVEFURV9JTl9SRVZJRVcQBhIUChBJU1NVRV9TVEFURV9ET05FEAcSGAoUSVNTVUVfU1RBVEVfQVJDSElWRUQQCCqcAQoNRm9yZ2VQcm92aWRlchIeChpGT1JHRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX1BST1ZJREVSX0dJVEhVQhABEhkKFUZPUkdFX1BST1ZJREVSX0dJVExBQhACEhoKFkZPUkdFX1BST1ZJREVSX0ZPUkdFSk8QAxIZChVGT1JHRV9QUk9WSURFUl9MSU5FQVIQBDKQCwoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlElwKD1N1YnNjcmliZUV2ZW50cxIiLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBojLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2UwARJaCg9MaXN0Qm9hcmRJc3N1ZXMSIi5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QaIy5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJdChBTdG9wQWdlbnRTZXNzaW9uEiMuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBokLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlEmkKFFJlbW92ZUFnZW50V29ya3NwYWNlEicuY29tcGFzcy52MS5SZW1vdmVBZ2VudFdvcmtzcGFjZVJlcXVlc3QaKC5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVzcG9uc2USYwoSUmVsb2FkQWdlbnRTZXNzaW9uEiUuY29tcGFzcy52MS5SZWxvYWRBZ2VudFNlc3Npb25SZXF1ZXN0GiYuY29tcGFzcy52MS5SZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRJXCg5HZXRBZ2VudFN0YXR1cxIhLmNvbXBhc3MudjEuR2V0QWdlbnRTdGF0dXNSZXF1ZXN0GiIuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1Jlc3BvbnNlEmIKFVN1YnNjcmliZUFnZW50U2Vzc2lvbhIoLmNvbXBhc3MudjEuU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBodLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uRnJhbWUwARJLCgpJc3N1ZVRva2VuEh0uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVxdWVzdBoeLmNvbXBhc3MudjEuSXNzdWVUb2tlblJlc3BvbnNlElcKDlB1dEFnZW50Q29uZmlnEiEuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1JlcXVlc3QaIi5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVzcG9uc2USYwoSR2V0QWdlbnRDb25maWdJbmZvEiUuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0GiYuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRJgChFEZWxldGVBZ2VudENvbmZpZxIkLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0GiUuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlMv0BCg5TZWNyZXRzU2VydmljZRJICglTZXRTZWNyZXQSHC5jb21wYXNzLnYxLlNldFNlY3JldFJlcXVlc3QaHS5jb21wYXNzLnYxLlNldFNlY3JldFJlc3BvbnNlEk4KC0xpc3RTZWNyZXRzEh4uY29tcGFzcy52MS5MaXN0U2VjcmV0c1JlcXVlc3QaHy5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVzcG9uc2USUQoMRGVsZXRlU2VjcmV0Eh8uY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXF1ZXN0GiAuY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXNwb25zZWIGcHJvdG8z"); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkIlYKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZSJJChFBZ2VudE1lc3NhZ2VDaHVuaxISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHRleHQYAiABKAkSEgoKaXNfdGhvdWdodBgDIAEoCCJ5Cg1BZ2VudFRvb2xDYWxsEhIKCnNlc3Npb25faWQYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJEg0KBXRpdGxlGAMgASgJEi8KBnN0YXR1cxgEIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cyJMCglBZ2VudFBsYW4SEgoKc2Vzc2lvbl9pZBgBIAEoCRIrCgdlbnRyaWVzGAIgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSJTCg5BZ2VudFBsYW5FbnRyeRIPCgdjb250ZW50GAEgASgJEjAKBnN0YXR1cxgCIAEoDjIgLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnlTdGF0dXMi8QIKDFNlc3Npb25FdmVudBIQCghldmVudF9pZBgBIAEoCRISCgphdF91bml4X21zGAIgASgDEjoKDmFzc2lzdGFudF90ZXh0GAMgASgLMiAuY29tcGFzcy52MS5TZXNzaW9uQXNzaXN0YW50VGV4dEgAEi8KCHRoaW5raW5nGAQgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVGhpbmtpbmdIABIwCgl0b29sX2NhbGwYBSABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbEgAEj0KEHRvb2xfY2FsbF91cGRhdGUYBiABKAsyIS5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbFVwZGF0ZUgAEicKBHBsYW4YByABKAsyFy5jb21wYXNzLnYxLlNlc3Npb25QbGFuSAASKwoGbm90aWNlGAggASgLMhkuY29tcGFzcy52MS5TZXNzaW9uTm90aWNlSABCBwoFZXZlbnQiOAoUU2Vzc2lvbkFzc2lzdGFudFRleHQSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJIjMKD1Nlc3Npb25UaGlua2luZxIMCgR0ZXh0GAEgASgJEhIKCm1lc3NhZ2VfaWQYAiABKAkiZwoPU2Vzc2lvblRvb2xDYWxsEhQKDHRvb2xfY2FsbF9pZBgBIAEoCRINCgV0aXRsZRgCIAEoCRIvCgZzdGF0dXMYAyABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMimgEKFVNlc3Npb25Ub29sQ2FsbFVwZGF0ZRIUCgx0b29sX2NhbGxfaWQYASABKAkSLwoGc3RhdHVzGAIgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzEg4KBm91dHB1dBgDIAEoCRIqCgVkaWZmcxgEIAMoCzIbLmNvbXBhc3MudjEuU2Vzc2lvbkZpbGVEaWZmIlUKD1Nlc3Npb25GaWxlRGlmZhIMCgRwYXRoGAEgASgJEhUKCG9sZF90ZXh0GAIgASgJSACIAQESEAoIbmV3X3RleHQYAyABKAlCCwoJX29sZF90ZXh0IjoKC1Nlc3Npb25QbGFuEisKB2VudHJpZXMYASADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IjkKDVNlc3Npb25Ob3RpY2USDAoEdGV4dBgBIAEoCRIRCgRsaW5rGAIgASgJSACIAQFCBwoFX2xpbmsiMgocU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIn4KEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUiowEKHlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVxdWVzdBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAUgASgJEg8KB3BlcnNvbmEYBiABKAkSDAoEcm9sZRgHIAEoCUoECAIQA0oECAMQBEoECAQQBVIKcmVtb3RlX3VybFIKbG9jYWxfcGF0aFIDcmVmIjkKH1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USFgoOY29udGFpbmVyX25hbWUYASABKAkiUAobUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJIh4KHFJlbW92ZUFnZW50V29ya3NwYWNlUmVzcG9uc2UiZQoYU3RhcnRBZ2VudFNlc3Npb25SZXF1ZXN0EhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJEhYKDmluaXRpYWxfcHJvbXB0GAIgASgJEhkKEXJlc3VtZV9zZXNzaW9uX2lkGAMgASgJIi8KGVN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSItChdTdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhoKGFN0b3BBZ2VudFNlc3Npb25SZXNwb25zZSIvChlSZWxvYWRBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiMAoaUmVsb2FkQWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSIrChVHZXRBZ2VudFN0YXR1c1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJKChZHZXRBZ2VudFN0YXR1c1Jlc3BvbnNlEjAKCHN0YXR1c2VzGAEgAygLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXMiJwoRSXNzdWVUb2tlblJlcXVlc3QSEgoKYWNjb3VudF9pZBgBIAEoCSIjChJJc3N1ZVRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkiJwoVUHV0QWdlbnRDb25maWdSZXF1ZXN0Eg4KBmJ1bmRsZRgBIAEoDCIpChZQdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkiGwoZR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdCLJAQoaR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCRIOCgZza2lsbHMYAiADKAkSEgoKZXh0ZW5zaW9ucxgDIAMoCRITCgttY3Bfc2VydmVycxgEIAMoCRIUCgxoYXNfc2V0dGluZ3MYBSABKAgSFQoNaGFzX2FnZW50c19tZBgGIAEoCBINCgVydWxlcxgHIAMoCRIRCglzdWJhZ2VudHMYCCADKAkSEgoKaGFzX21vZGVscxgJIAEoCCIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSJMChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCUoECAIQA0oECAMQBFIMb3duZXJfaGFuZGxlUgh2ZXJpZmllZCJFCghGb3JnZVJlZhIrCghwcm92aWRlchgBIAEoDjIZLmNvbXBhc3MudjEuRm9yZ2VQcm92aWRlchIMCgRob3N0GAIgASgJIqQDCgVJc3N1ZRIKCgJpZBgBIAEoCRIjCgVmb3JnZRgCIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgDIAEoCRIOCgZudW1iZXIYBCABKA0SDQoFdGl0bGUYBSABKAkSDAoEYm9keRgGIAEoCRITCgtmb3JnZV9zdGF0ZRgHIAEoCRILCgN1cmwYCCABKAkSKwoFYWdlbnQYCSABKAsyHC5jb21wYXNzLnYxLkFnZW50QXR0cmlidXRpb24SFQoNZm9yZ2VfYWNjb3VudBgKIAEoCRIOCgZsYWJlbHMYCyADKAkSJQoFc3RhdGUYDCABKA4yFi5jb21wYXNzLnYxLklzc3VlU3RhdGUSEAoIcHJpb3JpdHkYDSABKAkSEAoIYXNzaWduZWUYDiABKAkSDwoHc3VtbWFyeRgPIAEoCRIOCgZicmFuY2gYECABKAkSJAoDcHJzGBEgAygLMhcuY29tcGFzcy52MS5QdWxsUmVxdWVzdBInCgd0cmFja2VyGBIgASgLMhYuY29tcGFzcy52MS5UcmFja2VyUmVmIp4DCgtQdWxsUmVxdWVzdBIjCgVmb3JnZRgBIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgCIAEoCRIOCgZudW1iZXIYAyABKA0SDQoFdGl0bGUYBCABKAkSEwoLZm9yZ2Vfc3RhdGUYBSABKAkSCwoDdXJsGAYgASgJEhAKCGhlYWRfcmVmGAcgASgJEhAKCGJhc2VfcmVmGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDQoFZHJhZnQYCyABKAgSKQoHY2hhbmdlZBgMIAEoCzIYLmNvbXBhc3MudjEuQ2hhbmdlZFN0YXRzEikKBmNoZWNrcxgNIAEoCzIZLmNvbXBhc3MudjEuQ2hlY2tzU3VtbWFyeRIjCgdyZXZpZXdzGA4gAygLMhIuY29tcGFzcy52MS5SZXZpZXcSKQoHdGhyZWFkcxgPIAMoCzIYLmNvbXBhc3MudjEuUmV2aWV3VGhyZWFkIlMKDUNoZWNrc1N1bW1hcnkSEAoIaGVhZF9zaGEYASABKAkSDQoFc3RhdGUYAiABKAkSIQoGY2hlY2tzGAMgAygLMhEuY29tcGFzcy52MS5DaGVjayJDCgVDaGVjaxIMCgRuYW1lGAEgASgJEg0KBXN0YXRlGAIgASgJEgsKA3VybBgDIAEoCRIQCghyZXF1aXJlZBgEIAEoCCJDCgxDaGFuZ2VkU3RhdHMSDQoFZmlsZXMYASABKA0SEQoJYWRkaXRpb25zGAIgASgNEhEKCWRlbGV0aW9ucxgDIAEoDSJDCgpUcmFja2VyUmVmEgwKBGtpbmQYASABKAkSCgoCaWQYAiABKAkSDgoGc3RhdHVzGAMgASgJEgsKA3VybBgEIAEoCSJHCgZSZXZpZXcSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIPCgd2ZXJkaWN0GAMgASgJEgwKBGJvZHkYBCABKAkiVQoMUmV2aWV3VGhyZWFkEgwKBHBhdGgYASABKAkSEAoIcmVzb2x2ZWQYAiABKAgSJQoIY29tbWVudHMYAyADKAsyEy5jb21wYXNzLnYxLkNvbW1lbnQiNwoHQ29tbWVudBIOCgZhdXRob3IYASABKAkSDgoGaXNfYm90GAIgASgIEgwKBGJvZHkYAyABKAkqZAoOU2VjcmV0RGVsaXZlcnkSHwobU0VDUkVUX0RFTElWRVJZX1VOU1BFQ0lGSUVEEAASGAoUU0VDUkVUX0RFTElWRVJZX0ZJTEUQARIXChNTRUNSRVRfREVMSVZFUllfRU5WEAIqcAoKU2VjcmV0S2luZBIbChdTRUNSRVRfS0lORF9VTlNQRUNJRklFRBAAEhcKE1NFQ1JFVF9LSU5EX0dFTkVSSUMQARIYChRTRUNSRVRfS0lORF9QUk9WSURFUhACEhIKDlNFQ1JFVF9LSU5EX0dIEAMqQwoLU2VydmVyU3RhdGUSHAoYU0VSVkVSX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSU0VSVkVSX1NUQVRFX1JFQURZEAEqggIKEUFnZW50U2Vzc2lvblN0YXRlEiMKH0FHRU5UX1NFU1NJT05fU1RBVEVfVU5TUEVDSUZJRUQQABIgChxBR0VOVF9TRVNTSU9OX1NUQVRFX1NUQVJUSU5HEAESHQoZQUdFTlRfU0VTU0lPTl9TVEFURV9SRUFEWRACEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfV09SS0lORxADEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfU1RPUFBFRBAEEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfRVJST1JFRBAFEiQKIEFHRU5UX1NFU1NJT05fU1RBVEVfRElTQ09OTkVDVEVEEAYq0gEKE0FnZW50VG9vbENhbGxTdGF0dXMSJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19VTlNQRUNJRklFRBAAEiIKHkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfUEVORElORxABEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfSU5fUFJPR1JFU1MQAhIkCiBBR0VOVF9UT09MX0NBTExfU1RBVFVTX0NPTVBMRVRFRBADEiEKHUFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfRkFJTEVEEAQqtAEKFEFnZW50UGxhbkVudHJ5U3RhdHVzEicKI0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIwofQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfUEVORElORxABEicKI0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0lOX1BST0dSRVNTEAISJQohQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfQ09NUExFVEVEEAMq8QEKCklzc3VlU3RhdGUSGwoXSVNTVUVfU1RBVEVfVU5TUEVDSUZJRUQQABIXChNJU1NVRV9TVEFURV9CQUNLTE9HEAESFAoQSVNTVUVfU1RBVEVfVE9ETxACEhYKEklTU1VFX1NUQVRFX1FVRVVFRBADEhcKE0lTU1VFX1NUQVRFX0JMT0NLRUQQBBIbChdJU1NVRV9TVEFURV9JTl9QUk9HUkVTUxAFEhkKFUlTU1VFX1NUQVRFX0lOX1JFVklFVxAGEhQKEElTU1VFX1NUQVRFX0RPTkUQBxIYChRJU1NVRV9TVEFURV9BUkNISVZFRBAIKpwBCg1Gb3JnZVByb3ZpZGVyEh4KGkZPUkdFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASGQoVRk9SR0VfUFJPVklERVJfR0lUSFVCEAESGQoVRk9SR0VfUFJPVklERVJfR0lUTEFCEAISGgoWRk9SR0VfUFJPVklERVJfRk9SR0VKTxADEhkKFUZPUkdFX1BST1ZJREVSX0xJTkVBUhAEMpALCg5Db21wYXNzU2VydmljZRJUCg1HZXRTZXJ2ZXJJbmZvEiAuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBohLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1Jlc3BvbnNlEj8KBldob0FtSRIZLmNvbXBhc3MudjEuV2hvQW1JUmVxdWVzdBoaLmNvbXBhc3MudjEuV2hvQW1JUmVzcG9uc2USXAoPU3Vic2NyaWJlRXZlbnRzEiIuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXF1ZXN0GiMuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXNwb25zZTABEloKD0xpc3RCb2FyZElzc3VlcxIiLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBojLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2UScgoXUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2USKi5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVxdWVzdBorLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJgChFTdGFydEFnZW50U2Vzc2lvbhIkLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXF1ZXN0GiUuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USVwoOUHV0QWdlbnRDb25maWcSIS5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVxdWVzdBoiLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXNwb25zZRJjChJHZXRBZ2VudENvbmZpZ0luZm8SJS5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3QaJi5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEmAKEURlbGV0ZUFnZW50Q29uZmlnEiQuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QaJS5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2Uy/QEKDlNlY3JldHNTZXJ2aWNlEkgKCVNldFNlY3JldBIcLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVxdWVzdBodLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVzcG9uc2USTgoLTGlzdFNlY3JldHMSHi5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVxdWVzdBofLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXNwb25zZRJRCgxEZWxldGVTZWNyZXQSHy5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlcXVlc3QaIC5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlc3BvbnNlYgZwcm90bzM"); /** * @generated from message compass.v1.SetSecretRequest @@ -1016,6 +1016,23 @@ export type ProvisionAgentWorkspaceRequest = Message<"compass.v1.ProvisionAgentW * @generated from field: string persona = 6; */ persona: string; + + /** + * The agent's operator-set role, selecting the container's block-0 system + * prompt at provision so it survives compaction (a system-prompt config block + * is not part of the message history a snapcompact archives). SERVER- + * AUTHORITATIVE: the Server is expected to populate this by reading + * AgentAccount.role from the store on the provision path and to overwrite any + * client-supplied value, so a caller cannot inject a role prompt — an + * invariant enforced by the server provision path (not by this wire-settable + * field). Where persona (field 6) is an APPEND overlay, role REPLACES block-0: + * the label selects config/prompts//SYSTEM.md, materialized by the + * Runner into the container's customSystemPrompt (compass-runner consumer). + * Empty = no role (default OMP block-0). + * + * @generated from field: string role = 7; + */ + role: string; }; /** diff --git a/packages/compass-client/src/gen/compass/v1/compass_pb.ts b/packages/compass-client/src/gen/compass/v1/compass_pb.ts index 14129a01..c558fc8c 100644 --- a/packages/compass-client/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-client/src/gen/compass/v1/compass_pb.ts @@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file compass/v1/compass.proto. */ export const file_compass_v1_compass: GenFile = /*@__PURE__*/ - fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkIlYKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZSJJChFBZ2VudE1lc3NhZ2VDaHVuaxISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHRleHQYAiABKAkSEgoKaXNfdGhvdWdodBgDIAEoCCJ5Cg1BZ2VudFRvb2xDYWxsEhIKCnNlc3Npb25faWQYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJEg0KBXRpdGxlGAMgASgJEi8KBnN0YXR1cxgEIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cyJMCglBZ2VudFBsYW4SEgoKc2Vzc2lvbl9pZBgBIAEoCRIrCgdlbnRyaWVzGAIgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSJTCg5BZ2VudFBsYW5FbnRyeRIPCgdjb250ZW50GAEgASgJEjAKBnN0YXR1cxgCIAEoDjIgLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnlTdGF0dXMi8QIKDFNlc3Npb25FdmVudBIQCghldmVudF9pZBgBIAEoCRISCgphdF91bml4X21zGAIgASgDEjoKDmFzc2lzdGFudF90ZXh0GAMgASgLMiAuY29tcGFzcy52MS5TZXNzaW9uQXNzaXN0YW50VGV4dEgAEi8KCHRoaW5raW5nGAQgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVGhpbmtpbmdIABIwCgl0b29sX2NhbGwYBSABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbEgAEj0KEHRvb2xfY2FsbF91cGRhdGUYBiABKAsyIS5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbFVwZGF0ZUgAEicKBHBsYW4YByABKAsyFy5jb21wYXNzLnYxLlNlc3Npb25QbGFuSAASKwoGbm90aWNlGAggASgLMhkuY29tcGFzcy52MS5TZXNzaW9uTm90aWNlSABCBwoFZXZlbnQiOAoUU2Vzc2lvbkFzc2lzdGFudFRleHQSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJIjMKD1Nlc3Npb25UaGlua2luZxIMCgR0ZXh0GAEgASgJEhIKCm1lc3NhZ2VfaWQYAiABKAkiZwoPU2Vzc2lvblRvb2xDYWxsEhQKDHRvb2xfY2FsbF9pZBgBIAEoCRINCgV0aXRsZRgCIAEoCRIvCgZzdGF0dXMYAyABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMimgEKFVNlc3Npb25Ub29sQ2FsbFVwZGF0ZRIUCgx0b29sX2NhbGxfaWQYASABKAkSLwoGc3RhdHVzGAIgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzEg4KBm91dHB1dBgDIAEoCRIqCgVkaWZmcxgEIAMoCzIbLmNvbXBhc3MudjEuU2Vzc2lvbkZpbGVEaWZmIlUKD1Nlc3Npb25GaWxlRGlmZhIMCgRwYXRoGAEgASgJEhUKCG9sZF90ZXh0GAIgASgJSACIAQESEAoIbmV3X3RleHQYAyABKAlCCwoJX29sZF90ZXh0IjoKC1Nlc3Npb25QbGFuEisKB2VudHJpZXMYASADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IjkKDVNlc3Npb25Ob3RpY2USDAoEdGV4dBgBIAEoCRIRCgRsaW5rGAIgASgJSACIAQFCBwoFX2xpbmsiMgocU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIn4KEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUilQEKHlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVxdWVzdBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAUgASgJEg8KB3BlcnNvbmEYBiABKAlKBAgCEANKBAgDEARKBAgEEAVSCnJlbW90ZV91cmxSCmxvY2FsX3BhdGhSA3JlZiI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImUKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIWCg5pbml0aWFsX3Byb21wdBgCIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCSIvChlTdGFydEFnZW50U2Vzc2lvblJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLwoZUmVsb2FkQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIjAKGlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkiKwoVR2V0QWdlbnRTdGF0dXNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiSgoWR2V0QWdlbnRTdGF0dXNSZXNwb25zZRIwCghzdGF0dXNlcxgBIAMoCzIeLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdHVzIicKEUlzc3VlVG9rZW5SZXF1ZXN0EhIKCmFjY291bnRfaWQYASABKAkiIwoSSXNzdWVUb2tlblJlc3BvbnNlEg0KBXRva2VuGAEgASgJIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3QiyQEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgiGgoYRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0IhsKGURlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2UiTAoQQWdlbnRBdHRyaWJ1dGlvbhIUCgxhZ2VudF9oYW5kbGUYASABKAlKBAgCEANKBAgDEARSDG93bmVyX2hhbmRsZVIIdmVyaWZpZWQiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSKkAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiKeAwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZCJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iQwoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkiRwoGUmV2aWV3Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDwoHdmVyZGljdBgDIAEoCRIMCgRib2R5GAQgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKkMKC1NlcnZlclN0YXRlEhwKGFNFUlZFUl9TVEFURV9VTlNQRUNJRklFRBAAEhYKElNFUlZFUl9TVEFURV9SRUFEWRABKoICChFBZ2VudFNlc3Npb25TdGF0ZRIjCh9BR0VOVF9TRVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAASIAocQUdFTlRfU0VTU0lPTl9TVEFURV9TVEFSVElORxABEh0KGUFHRU5UX1NFU1NJT05fU1RBVEVfUkVBRFkQAhIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1dPUktJTkcQAxIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1NUT1BQRUQQBBIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX0VSUk9SRUQQBRIkCiBBR0VOVF9TRVNTSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBAGKtIBChNBZ2VudFRvb2xDYWxsU3RhdHVzEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIiCh5BR0VOVF9UT09MX0NBTExfU1RBVFVTX1BFTkRJTkcQARImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX0lOX1BST0dSRVNTEAISJAogQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19DT01QTEVURUQQAxIhCh1BR0VOVF9UT09MX0NBTExfU1RBVFVTX0ZBSUxFRBAEKrQBChRBZ2VudFBsYW5FbnRyeVN0YXR1cxInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19VTlNQRUNJRklFRBAAEiMKH0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1BFTkRJTkcQARInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19JTl9QUk9HUkVTUxACEiUKIUFHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0NPTVBMRVRFRBADKvEBCgpJc3N1ZVN0YXRlEhsKF0lTU1VFX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSVNTVUVfU1RBVEVfQkFDS0xPRxABEhQKEElTU1VFX1NUQVRFX1RPRE8QAhIWChJJU1NVRV9TVEFURV9RVUVVRUQQAxIXChNJU1NVRV9TVEFURV9CTE9DS0VEEAQSGwoXSVNTVUVfU1RBVEVfSU5fUFJPR1JFU1MQBRIZChVJU1NVRV9TVEFURV9JTl9SRVZJRVcQBhIUChBJU1NVRV9TVEFURV9ET05FEAcSGAoUSVNTVUVfU1RBVEVfQVJDSElWRUQQCCqcAQoNRm9yZ2VQcm92aWRlchIeChpGT1JHRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX1BST1ZJREVSX0dJVEhVQhABEhkKFUZPUkdFX1BST1ZJREVSX0dJVExBQhACEhoKFkZPUkdFX1BST1ZJREVSX0ZPUkdFSk8QAxIZChVGT1JHRV9QUk9WSURFUl9MSU5FQVIQBDKQCwoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlElwKD1N1YnNjcmliZUV2ZW50cxIiLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBojLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2UwARJaCg9MaXN0Qm9hcmRJc3N1ZXMSIi5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QaIy5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJdChBTdG9wQWdlbnRTZXNzaW9uEiMuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBokLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlEmkKFFJlbW92ZUFnZW50V29ya3NwYWNlEicuY29tcGFzcy52MS5SZW1vdmVBZ2VudFdvcmtzcGFjZVJlcXVlc3QaKC5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVzcG9uc2USYwoSUmVsb2FkQWdlbnRTZXNzaW9uEiUuY29tcGFzcy52MS5SZWxvYWRBZ2VudFNlc3Npb25SZXF1ZXN0GiYuY29tcGFzcy52MS5SZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRJXCg5HZXRBZ2VudFN0YXR1cxIhLmNvbXBhc3MudjEuR2V0QWdlbnRTdGF0dXNSZXF1ZXN0GiIuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1Jlc3BvbnNlEmIKFVN1YnNjcmliZUFnZW50U2Vzc2lvbhIoLmNvbXBhc3MudjEuU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBodLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uRnJhbWUwARJLCgpJc3N1ZVRva2VuEh0uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVxdWVzdBoeLmNvbXBhc3MudjEuSXNzdWVUb2tlblJlc3BvbnNlElcKDlB1dEFnZW50Q29uZmlnEiEuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1JlcXVlc3QaIi5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVzcG9uc2USYwoSR2V0QWdlbnRDb25maWdJbmZvEiUuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0GiYuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRJgChFEZWxldGVBZ2VudENvbmZpZxIkLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0GiUuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlMv0BCg5TZWNyZXRzU2VydmljZRJICglTZXRTZWNyZXQSHC5jb21wYXNzLnYxLlNldFNlY3JldFJlcXVlc3QaHS5jb21wYXNzLnYxLlNldFNlY3JldFJlc3BvbnNlEk4KC0xpc3RTZWNyZXRzEh4uY29tcGFzcy52MS5MaXN0U2VjcmV0c1JlcXVlc3QaHy5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVzcG9uc2USUQoMRGVsZXRlU2VjcmV0Eh8uY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXF1ZXN0GiAuY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXNwb25zZWIGcHJvdG8z"); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkIlYKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZSJJChFBZ2VudE1lc3NhZ2VDaHVuaxISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHRleHQYAiABKAkSEgoKaXNfdGhvdWdodBgDIAEoCCJ5Cg1BZ2VudFRvb2xDYWxsEhIKCnNlc3Npb25faWQYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJEg0KBXRpdGxlGAMgASgJEi8KBnN0YXR1cxgEIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cyJMCglBZ2VudFBsYW4SEgoKc2Vzc2lvbl9pZBgBIAEoCRIrCgdlbnRyaWVzGAIgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSJTCg5BZ2VudFBsYW5FbnRyeRIPCgdjb250ZW50GAEgASgJEjAKBnN0YXR1cxgCIAEoDjIgLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnlTdGF0dXMi8QIKDFNlc3Npb25FdmVudBIQCghldmVudF9pZBgBIAEoCRISCgphdF91bml4X21zGAIgASgDEjoKDmFzc2lzdGFudF90ZXh0GAMgASgLMiAuY29tcGFzcy52MS5TZXNzaW9uQXNzaXN0YW50VGV4dEgAEi8KCHRoaW5raW5nGAQgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVGhpbmtpbmdIABIwCgl0b29sX2NhbGwYBSABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbEgAEj0KEHRvb2xfY2FsbF91cGRhdGUYBiABKAsyIS5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbFVwZGF0ZUgAEicKBHBsYW4YByABKAsyFy5jb21wYXNzLnYxLlNlc3Npb25QbGFuSAASKwoGbm90aWNlGAggASgLMhkuY29tcGFzcy52MS5TZXNzaW9uTm90aWNlSABCBwoFZXZlbnQiOAoUU2Vzc2lvbkFzc2lzdGFudFRleHQSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJIjMKD1Nlc3Npb25UaGlua2luZxIMCgR0ZXh0GAEgASgJEhIKCm1lc3NhZ2VfaWQYAiABKAkiZwoPU2Vzc2lvblRvb2xDYWxsEhQKDHRvb2xfY2FsbF9pZBgBIAEoCRINCgV0aXRsZRgCIAEoCRIvCgZzdGF0dXMYAyABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMimgEKFVNlc3Npb25Ub29sQ2FsbFVwZGF0ZRIUCgx0b29sX2NhbGxfaWQYASABKAkSLwoGc3RhdHVzGAIgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzEg4KBm91dHB1dBgDIAEoCRIqCgVkaWZmcxgEIAMoCzIbLmNvbXBhc3MudjEuU2Vzc2lvbkZpbGVEaWZmIlUKD1Nlc3Npb25GaWxlRGlmZhIMCgRwYXRoGAEgASgJEhUKCG9sZF90ZXh0GAIgASgJSACIAQESEAoIbmV3X3RleHQYAyABKAlCCwoJX29sZF90ZXh0IjoKC1Nlc3Npb25QbGFuEisKB2VudHJpZXMYASADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IjkKDVNlc3Npb25Ob3RpY2USDAoEdGV4dBgBIAEoCRIRCgRsaW5rGAIgASgJSACIAQFCBwoFX2xpbmsiMgocU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIn4KEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUiowEKHlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVxdWVzdBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAUgASgJEg8KB3BlcnNvbmEYBiABKAkSDAoEcm9sZRgHIAEoCUoECAIQA0oECAMQBEoECAQQBVIKcmVtb3RlX3VybFIKbG9jYWxfcGF0aFIDcmVmIjkKH1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USFgoOY29udGFpbmVyX25hbWUYASABKAkiUAobUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJIh4KHFJlbW92ZUFnZW50V29ya3NwYWNlUmVzcG9uc2UiZQoYU3RhcnRBZ2VudFNlc3Npb25SZXF1ZXN0EhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJEhYKDmluaXRpYWxfcHJvbXB0GAIgASgJEhkKEXJlc3VtZV9zZXNzaW9uX2lkGAMgASgJIi8KGVN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSItChdTdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhoKGFN0b3BBZ2VudFNlc3Npb25SZXNwb25zZSIvChlSZWxvYWRBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiMAoaUmVsb2FkQWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSIrChVHZXRBZ2VudFN0YXR1c1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJKChZHZXRBZ2VudFN0YXR1c1Jlc3BvbnNlEjAKCHN0YXR1c2VzGAEgAygLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXMiJwoRSXNzdWVUb2tlblJlcXVlc3QSEgoKYWNjb3VudF9pZBgBIAEoCSIjChJJc3N1ZVRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkiJwoVUHV0QWdlbnRDb25maWdSZXF1ZXN0Eg4KBmJ1bmRsZRgBIAEoDCIpChZQdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkiGwoZR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdCLJAQoaR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCRIOCgZza2lsbHMYAiADKAkSEgoKZXh0ZW5zaW9ucxgDIAMoCRITCgttY3Bfc2VydmVycxgEIAMoCRIUCgxoYXNfc2V0dGluZ3MYBSABKAgSFQoNaGFzX2FnZW50c19tZBgGIAEoCBINCgVydWxlcxgHIAMoCRIRCglzdWJhZ2VudHMYCCADKAkSEgoKaGFzX21vZGVscxgJIAEoCCIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSJMChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCUoECAIQA0oECAMQBFIMb3duZXJfaGFuZGxlUgh2ZXJpZmllZCJFCghGb3JnZVJlZhIrCghwcm92aWRlchgBIAEoDjIZLmNvbXBhc3MudjEuRm9yZ2VQcm92aWRlchIMCgRob3N0GAIgASgJIqQDCgVJc3N1ZRIKCgJpZBgBIAEoCRIjCgVmb3JnZRgCIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgDIAEoCRIOCgZudW1iZXIYBCABKA0SDQoFdGl0bGUYBSABKAkSDAoEYm9keRgGIAEoCRITCgtmb3JnZV9zdGF0ZRgHIAEoCRILCgN1cmwYCCABKAkSKwoFYWdlbnQYCSABKAsyHC5jb21wYXNzLnYxLkFnZW50QXR0cmlidXRpb24SFQoNZm9yZ2VfYWNjb3VudBgKIAEoCRIOCgZsYWJlbHMYCyADKAkSJQoFc3RhdGUYDCABKA4yFi5jb21wYXNzLnYxLklzc3VlU3RhdGUSEAoIcHJpb3JpdHkYDSABKAkSEAoIYXNzaWduZWUYDiABKAkSDwoHc3VtbWFyeRgPIAEoCRIOCgZicmFuY2gYECABKAkSJAoDcHJzGBEgAygLMhcuY29tcGFzcy52MS5QdWxsUmVxdWVzdBInCgd0cmFja2VyGBIgASgLMhYuY29tcGFzcy52MS5UcmFja2VyUmVmIp4DCgtQdWxsUmVxdWVzdBIjCgVmb3JnZRgBIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgCIAEoCRIOCgZudW1iZXIYAyABKA0SDQoFdGl0bGUYBCABKAkSEwoLZm9yZ2Vfc3RhdGUYBSABKAkSCwoDdXJsGAYgASgJEhAKCGhlYWRfcmVmGAcgASgJEhAKCGJhc2VfcmVmGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDQoFZHJhZnQYCyABKAgSKQoHY2hhbmdlZBgMIAEoCzIYLmNvbXBhc3MudjEuQ2hhbmdlZFN0YXRzEikKBmNoZWNrcxgNIAEoCzIZLmNvbXBhc3MudjEuQ2hlY2tzU3VtbWFyeRIjCgdyZXZpZXdzGA4gAygLMhIuY29tcGFzcy52MS5SZXZpZXcSKQoHdGhyZWFkcxgPIAMoCzIYLmNvbXBhc3MudjEuUmV2aWV3VGhyZWFkIlMKDUNoZWNrc1N1bW1hcnkSEAoIaGVhZF9zaGEYASABKAkSDQoFc3RhdGUYAiABKAkSIQoGY2hlY2tzGAMgAygLMhEuY29tcGFzcy52MS5DaGVjayJDCgVDaGVjaxIMCgRuYW1lGAEgASgJEg0KBXN0YXRlGAIgASgJEgsKA3VybBgDIAEoCRIQCghyZXF1aXJlZBgEIAEoCCJDCgxDaGFuZ2VkU3RhdHMSDQoFZmlsZXMYASABKA0SEQoJYWRkaXRpb25zGAIgASgNEhEKCWRlbGV0aW9ucxgDIAEoDSJDCgpUcmFja2VyUmVmEgwKBGtpbmQYASABKAkSCgoCaWQYAiABKAkSDgoGc3RhdHVzGAMgASgJEgsKA3VybBgEIAEoCSJHCgZSZXZpZXcSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIPCgd2ZXJkaWN0GAMgASgJEgwKBGJvZHkYBCABKAkiVQoMUmV2aWV3VGhyZWFkEgwKBHBhdGgYASABKAkSEAoIcmVzb2x2ZWQYAiABKAgSJQoIY29tbWVudHMYAyADKAsyEy5jb21wYXNzLnYxLkNvbW1lbnQiNwoHQ29tbWVudBIOCgZhdXRob3IYASABKAkSDgoGaXNfYm90GAIgASgIEgwKBGJvZHkYAyABKAkqZAoOU2VjcmV0RGVsaXZlcnkSHwobU0VDUkVUX0RFTElWRVJZX1VOU1BFQ0lGSUVEEAASGAoUU0VDUkVUX0RFTElWRVJZX0ZJTEUQARIXChNTRUNSRVRfREVMSVZFUllfRU5WEAIqcAoKU2VjcmV0S2luZBIbChdTRUNSRVRfS0lORF9VTlNQRUNJRklFRBAAEhcKE1NFQ1JFVF9LSU5EX0dFTkVSSUMQARIYChRTRUNSRVRfS0lORF9QUk9WSURFUhACEhIKDlNFQ1JFVF9LSU5EX0dIEAMqQwoLU2VydmVyU3RhdGUSHAoYU0VSVkVSX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSU0VSVkVSX1NUQVRFX1JFQURZEAEqggIKEUFnZW50U2Vzc2lvblN0YXRlEiMKH0FHRU5UX1NFU1NJT05fU1RBVEVfVU5TUEVDSUZJRUQQABIgChxBR0VOVF9TRVNTSU9OX1NUQVRFX1NUQVJUSU5HEAESHQoZQUdFTlRfU0VTU0lPTl9TVEFURV9SRUFEWRACEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfV09SS0lORxADEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfU1RPUFBFRBAEEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfRVJST1JFRBAFEiQKIEFHRU5UX1NFU1NJT05fU1RBVEVfRElTQ09OTkVDVEVEEAYq0gEKE0FnZW50VG9vbENhbGxTdGF0dXMSJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19VTlNQRUNJRklFRBAAEiIKHkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfUEVORElORxABEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfSU5fUFJPR1JFU1MQAhIkCiBBR0VOVF9UT09MX0NBTExfU1RBVFVTX0NPTVBMRVRFRBADEiEKHUFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfRkFJTEVEEAQqtAEKFEFnZW50UGxhbkVudHJ5U3RhdHVzEicKI0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIwofQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfUEVORElORxABEicKI0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0lOX1BST0dSRVNTEAISJQohQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfQ09NUExFVEVEEAMq8QEKCklzc3VlU3RhdGUSGwoXSVNTVUVfU1RBVEVfVU5TUEVDSUZJRUQQABIXChNJU1NVRV9TVEFURV9CQUNLTE9HEAESFAoQSVNTVUVfU1RBVEVfVE9ETxACEhYKEklTU1VFX1NUQVRFX1FVRVVFRBADEhcKE0lTU1VFX1NUQVRFX0JMT0NLRUQQBBIbChdJU1NVRV9TVEFURV9JTl9QUk9HUkVTUxAFEhkKFUlTU1VFX1NUQVRFX0lOX1JFVklFVxAGEhQKEElTU1VFX1NUQVRFX0RPTkUQBxIYChRJU1NVRV9TVEFURV9BUkNISVZFRBAIKpwBCg1Gb3JnZVByb3ZpZGVyEh4KGkZPUkdFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASGQoVRk9SR0VfUFJPVklERVJfR0lUSFVCEAESGQoVRk9SR0VfUFJPVklERVJfR0lUTEFCEAISGgoWRk9SR0VfUFJPVklERVJfRk9SR0VKTxADEhkKFUZPUkdFX1BST1ZJREVSX0xJTkVBUhAEMpALCg5Db21wYXNzU2VydmljZRJUCg1HZXRTZXJ2ZXJJbmZvEiAuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBohLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1Jlc3BvbnNlEj8KBldob0FtSRIZLmNvbXBhc3MudjEuV2hvQW1JUmVxdWVzdBoaLmNvbXBhc3MudjEuV2hvQW1JUmVzcG9uc2USXAoPU3Vic2NyaWJlRXZlbnRzEiIuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXF1ZXN0GiMuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXNwb25zZTABEloKD0xpc3RCb2FyZElzc3VlcxIiLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBojLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2UScgoXUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2USKi5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVxdWVzdBorLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJgChFTdGFydEFnZW50U2Vzc2lvbhIkLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXF1ZXN0GiUuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USVwoOUHV0QWdlbnRDb25maWcSIS5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVxdWVzdBoiLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXNwb25zZRJjChJHZXRBZ2VudENvbmZpZ0luZm8SJS5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3QaJi5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEmAKEURlbGV0ZUFnZW50Q29uZmlnEiQuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QaJS5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2Uy/QEKDlNlY3JldHNTZXJ2aWNlEkgKCVNldFNlY3JldBIcLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVxdWVzdBodLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVzcG9uc2USTgoLTGlzdFNlY3JldHMSHi5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVxdWVzdBofLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXNwb25zZRJRCgxEZWxldGVTZWNyZXQSHy5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlcXVlc3QaIC5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlc3BvbnNlYgZwcm90bzM"); /** * @generated from message compass.v1.SetSecretRequest @@ -1016,6 +1016,23 @@ export type ProvisionAgentWorkspaceRequest = Message<"compass.v1.ProvisionAgentW * @generated from field: string persona = 6; */ persona: string; + + /** + * The agent's operator-set role, selecting the container's block-0 system + * prompt at provision so it survives compaction (a system-prompt config block + * is not part of the message history a snapcompact archives). SERVER- + * AUTHORITATIVE: the Server is expected to populate this by reading + * AgentAccount.role from the store on the provision path and to overwrite any + * client-supplied value, so a caller cannot inject a role prompt — an + * invariant enforced by the server provision path (not by this wire-settable + * field). Where persona (field 6) is an APPEND overlay, role REPLACES block-0: + * the label selects config/prompts//SYSTEM.md, materialized by the + * Runner into the container's customSystemPrompt (compass-runner consumer). + * Empty = no role (default OMP block-0). + * + * @generated from field: string role = 7; + */ + role: string; }; /** diff --git a/proto/compass/v1/compass.proto b/proto/compass/v1/compass.proto index ea7b7c23..a74b8ae1 100644 --- a/proto/compass/v1/compass.proto +++ b/proto/compass/v1/compass.proto @@ -528,6 +528,18 @@ message ProvisionAgentWorkspaceRequest { // it into the container // (compass-runner consumer). Empty = no persona baked (default). string persona = 6; + // The agent's operator-set role, selecting the container's block-0 system + // prompt at provision so it survives compaction (a system-prompt config block + // is not part of the message history a snapcompact archives). SERVER- + // AUTHORITATIVE: the Server is expected to populate this by reading + // AgentAccount.role from the store on the provision path and to overwrite any + // client-supplied value, so a caller cannot inject a role prompt — an + // invariant enforced by the server provision path (not by this wire-settable + // field). Where persona (field 6) is an APPEND overlay, role REPLACES block-0: + // the label selects config/prompts//SYSTEM.md, materialized by the + // Runner into the container's customSystemPrompt (compass-runner consumer). + // Empty = no role (default OMP block-0). + string role = 7; } message ProvisionAgentWorkspaceResponse { From 4ca98e5583d8a2ccdd5b6741ed3336451e4a50e7 Mon Sep 17 00:00:00 2001 From: seal Date: Fri, 7 Aug 2026 16:54:17 -0400 Subject: [PATCH 2/3] fix(compass-agent): guard the role label against path traversal (T10 review low) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A role selects prompts//SYSTEM.md; reject a label carrying a path separator or '..' so it can never traverse outside the prompts/ subtree. Defense in depth — role is store-set out-of-band today, but the guard costs nothing and closes the traversal the moment a client-facing setter lands. A rejected label reads as 'no prompt' and falls back to the default block-0. Co-authored-by: Matt Wilkinson --- packages/compass-agent/src/cli.test.ts | 32 +++++++++++++++++++++ packages/compass-agent/src/config-reader.ts | 8 ++++++ 2 files changed, 40 insertions(+) diff --git a/packages/compass-agent/src/cli.test.ts b/packages/compass-agent/src/cli.test.ts index d2c28967..f0ede6e0 100644 --- a/packages/compass-agent/src/cli.test.ts +++ b/packages/compass-agent/src/cli.test.ts @@ -1959,6 +1959,38 @@ describe("main wires the mounted agent-config into createAgentSession", () => { expect(seen[0].customSystemPrompt).toBeUndefined(); }); + test("COMPASS_ROLE with a path-traversal label → rejected, no customSystemPrompt", async () => { + // A role is a flat directory name; a label carrying a separator or `..` + // must never traverse outside prompts/. The decoy sits at current/SYSTEM.md + // — exactly where role="../" resolves (join(current, "prompts", "../", + // "SYSTEM.md") = current/SYSTEM.md) — so WITHOUT the guard the traversal + // would find it and inject it as block-0 (customSystemPrompt defined). The + // guard rejects the label first, so main falls back to today's behavior. + // This placement is what makes the test non-vacuous: drop the guard and it + // fails. Defense in depth: role is store-set out-of-band today, but the + // guard holds the moment a client-facing setter lands. + const mount = scratch(); + writeMount(mount, "SYSTEM.md", "# Escaped\n"); + const session = fakeSession(); + const seen: SeenConfig[] = []; + await main( + { HOME: scratch(), COMPASS_ROLE: "../" }, + { + configMount: mount, + createSession: (options) => { + seen.push({ customSystemPrompt: options.customSystemPrompt }); + return Promise.resolve({ + session: session as unknown as AgentSession, + }); + }, + createTransport: () => + fakeCarrier(emptyLog(), { control: emptyControlStream }), + }, + ); + expect(seen).toHaveLength(1); + expect(seen[0].customSystemPrompt).toBeUndefined(); + }); + test("COMPASS_ROLE unset → no customSystemPrompt (exactly today's behavior)", async () => { const mount = scratch(); writeMount(mount, "prompts/manager/SYSTEM.md", "# Manager\n"); diff --git a/packages/compass-agent/src/config-reader.ts b/packages/compass-agent/src/config-reader.ts index 32d1a845..01814081 100644 --- a/packages/compass-agent/src/config-reader.ts +++ b/packages/compass-agent/src/config-reader.ts @@ -371,6 +371,14 @@ export async function readMountedRolePrompt( currentDir: string, role: string, ): Promise { + // Guard the label as a path segment: a role is a flat directory name + // (`manager`, `supervisor`), never a path. Reject a separator or `..` so the + // label can never traverse outside the `prompts/` subtree. Defense in depth — + // today `role` is set out-of-band in the store (no RPC populates it, so the + // value is trusted), but the guard costs nothing and closes the traversal the + // moment a client-facing setter lands. A rejected label reads as "no prompt" + // (undefined), so it falls back to the default block-0 like any absent file. + if (/[/\\]|\.\./.test(role)) return undefined; const path = join(currentDir, "prompts", role, "SYSTEM.md"); try { if (!(await stat(path)).isFile()) return undefined; From 2a105fdd27ee9a2f6ba38e9e76ac9df3067789d4 Mon Sep 17 00:00:00 2001 From: seal Date: Fri, 7 Aug 2026 17:21:29 -0400 Subject: [PATCH 3/3] fix(compass-agent): add ag.role to the shared agent-tree projection (T10 bounce) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The T10 role chain added ag.role to scanAccount (9 destinations) and to the five ListAccounts-family projections in accounts.go, but missed the shared agentTreeProjection in agent_tree.go, which feeds every roster/tree read (AgentsByOwner, neighborhood, subtree) through the same scanAccount helper. That projection still selected 8 account columns, so every tree read failed at scan time with "number of field descriptions must equal number of destinations, got 8 and 9" — surfaced by the real-Postgres CI leg on #229 (TestGetRoster*, TestSetStatusAsAccount*). Add ag.role in the same positional slot the accounts.go projections use (after ag.persona, before ag.parent_agent_id) so the tree projection stays a byte-for-byte mirror, and refresh the stale accounts.go line reference in the projection's doc comment. Verified vs a throwaway Postgres container: go build + go vet clean; the comms package (TestGetRoster*, TestSetStatusAsAccount*) is green. Co-authored-by: Matt Wilkinson --- go/internal/store/agent_tree.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/internal/store/agent_tree.go b/go/internal/store/agent_tree.go index 4ab0b726..4c16a9df 100644 --- a/go/internal/store/agent_tree.go +++ b/go/internal/store/agent_tree.go @@ -6,7 +6,7 @@ import ( ) // agentTreeProjection is the account column list + FROM/JOINs shared by the -// three tree reads. It mirrors the ListAccounts projection (accounts.go:498-501) +// three tree reads. It mirrors the ListAccounts projection (accounts.go:544-546) // exactly so a tree-read row scans through the same scanAccount helper and reads // identically to every other account read. The join to agent_accounts is INNER: // the tree is agents-only, so a user account can never appear in a tree read. @@ -14,7 +14,7 @@ import ( const agentTreeProjection = ` SELECT a.id, a.handle, a.display_name, u.role, - ag.owner_user_id, ag.home_channel_id, ag.persona, ag.parent_agent_id + ag.owner_user_id, ag.home_channel_id, ag.persona, ag.role, ag.parent_agent_id FROM accounts a LEFT JOIN user_accounts u ON u.account_id = a.id JOIN agent_accounts ag ON ag.account_id = a.id`