Skip to content

feat(compass): SEA-1732 T10 — operator-set role selects the agent's block-0 prompt - #229

Open
seal-agent wants to merge 3 commits into
mainfrom
seal-1732-t10-role
Open

feat(compass): SEA-1732 T10 — operator-set role selects the agent's block-0 prompt#229
seal-agent wants to merge 3 commits into
mainfrom
seal-1732-t10-role

Conversation

@seal-agent

@seal-agent seal-agent commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What

SEA-1732 T10 — an operator-set role selects the container's block-0 system prompt at provision time, delivered as customSystemPrompt.

Matt ruled (2026-08-07): the role selector is a new role field on AgentAccount, mirroring the existing persona field end to end. Where persona is an APPEND overlay (layered after the default prompt), role REPLACES block-0 — the label selects config/prompts/<role>/SYSTEM.md, materialized to the container as customSystemPrompt. Persona still appends after the role block; the two compose (record §OQ-8).

Delivery chain (mirrors persona; server-authoritative)

  • Migration 0015 adds agent_accounts.role TEXT NOT NULL DEFAULT ''; the store round-trips it through NewAgent/AgentAccount and every scanAccount SELECT.
  • Proto ProvisionAgentWorkspaceRequest.role (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), so a caller cannot inject a prompt.
  • AgentSpec.RoleAgentHandle.Role()AgentEnv.RoleCOMPASS_ROLE env, empty-omitted exactly like COMPASS_PERSONA.
  • cli.ts resolveRole reads COMPASS_ROLE, reads the role prompt via a new tolerant config-reader surface (prompts/<role>/SYSTEM.md), and passes it as customSystemPrompt. Role set but no prompt file → falls back to today's default block-0.
  • Regenerated the compass proto stubs (public go/gen + both client lanes) for the new field. Internal Go lane M-maps compass.proto to the public package, so it needs no separate regen.

MP-1 property

The MP-1 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 (a companion render with toolNames=[] proves the gate is load-bearing).

Verification (all firsthand)

  • go build ./... clean; go vet clean; nilaway (non-gating) introduces no new finding.
  • Go role tests: runner (COMPASS_ROLE export, BuildSpec mapping) + 4 pgtest tests against a real Postgres (store round-trip, defaults-empty, server-authoritative overwrite, non-agent clear) — all pass.
  • compass-agent TS suite: 437 pass / 0 fail (was 427 + 10 new), including the MP-1 property against the real SDK render.
  • biome clean on the 3 changed TS files.

Refs SEA-1732

…lock-0 prompt

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/<role>/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 <matt@sealedsecurity.com>
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

SEA-1732

…review low)

A role selects prompts/<role>/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 <matt@sealedsecurity.com>
…T10 bounce)

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 <matt@sealedsecurity.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant