From 07eca55ccf3747887f60973259bad988c8c4bc95 Mon Sep 17 00:00:00 2001 From: Steven Chand Date: Thu, 11 Jun 2026 10:25:34 -0700 Subject: [PATCH 1/3] [SC-16731] rename constitution to charter everywhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames all uses of "constitution"/"constitution_field_key" to "charter"/"charter_field_key" across the Atryum codebase: - DB migration 021: renames agents.constitution → agents.charter and agent_sync_settings.constitution_field_key → agent_sync_settings.charter_field_key - Go backend: structs, interfaces, SQL queries, JSON tags, log messages - TypeScript/TSX UI: API types, state vars, form labels (Settings, Agents) - Invocations page: removes "User pattern" field from rule scope UI - Docs: README, atryum.example.toml, docs/testing/ai-evaluation.md - Skill: renames .agents/skills/populate-constitution → populate-charter Co-authored-by: Cursor --- .../SKILL.md | 16 +++---- .../populate-charter/agents/openai.yaml | 4 ++ .../assets/charter-template.md} | 6 +-- .../populate-constitution/agents/openai.yaml | 4 -- README.md | 2 +- atryum.example.toml | 2 +- cmd/atryum/main.go | 12 ++--- docs/testing/ai-evaluation.md | 34 +++++++------- internal/api/handlers.go | 42 ++++++++--------- internal/backend/client.go | 2 +- internal/invocation/local_evaluator.go | 12 ++--- internal/invocation/service.go | 46 +++++++++---------- internal/invocation/service_test.go | 22 ++++----- internal/store/agent_sync_settings.go | 16 +++---- internal/store/agents.go | 22 ++++----- internal/store/db_test.go | 1 + .../021_rename_constitution_to_charter.go | 14 ++++++ internal/store/migrations/registry.go | 1 + internal/store/sqlite_test.go | 2 +- ui/src/api/AtryumAPI.ts | 8 ++-- ui/src/pages/Agents.tsx | 22 ++++----- ui/src/pages/Invocations.tsx | 34 -------------- ui/src/pages/Settings.tsx | 28 +++++------ 23 files changed, 167 insertions(+), 185 deletions(-) rename .agents/skills/{populate-constitution => populate-charter}/SKILL.md (80%) create mode 100644 .agents/skills/populate-charter/agents/openai.yaml rename .agents/skills/{populate-constitution/assets/constitution-template.md => populate-charter/assets/charter-template.md} (96%) delete mode 100644 .agents/skills/populate-constitution/agents/openai.yaml create mode 100644 internal/store/migrations/021_rename_constitution_to_charter.go diff --git a/.agents/skills/populate-constitution/SKILL.md b/.agents/skills/populate-charter/SKILL.md similarity index 80% rename from .agents/skills/populate-constitution/SKILL.md rename to .agents/skills/populate-charter/SKILL.md index d319619..d47666c 100644 --- a/.agents/skills/populate-constitution/SKILL.md +++ b/.agents/skills/populate-charter/SKILL.md @@ -1,22 +1,22 @@ --- -name: populate-constitution -description: Draft a complete agent constitution from an instructional prompt or agent brief. Use when Codex needs to create, populate, revise, or normalize CONSTITUTION.md-style governance documents that define an agent's purpose, scope, permissions, safety rules, handoff conditions, scenario-specific operating rules, and change log. +name: populate-charter +description: Draft a complete agent charter from an instructional prompt or agent brief. Use when Codex needs to create, populate, revise, or normalize CHARTER.md-style governance documents that define an agent's purpose, scope, permissions, safety rules, handoff conditions, scenario-specific operating rules, and change log. --- -# Populate Constitution +# Populate Charter ## Workflow 1. Read the user's instructional prompt and identify the agent's job, users, connected systems, recurring cadence, autonomous actions, approval boundaries, and expected outputs. -2. Load `assets/constitution-template.md` when the exact output structure is needed. +2. Load `assets/charter-template.md` when the exact output structure is needed. 3. If the prompt is underspecified, make conservative assumptions and mark them with bracketed placeholders such as `[Owner]`, `[Review cadence]`, or `[Confirm system]`. 4. Populate the foundational sections and add scenario-specific sections where the prompt needs more operational detail. Do not leave generic filler; write concrete rules derived from the prompt. -5. Preserve the constitution style: concise, operational, safety-forward, and written as binding instructions for the future agent and the tool-use governance system mediating it. +5. Preserve the charter style: concise, operational, safety-forward, and written as binding instructions for the future agent and the tool-use governance system mediating it. 6. Add a one-row change log with today's date unless the user provides another date. ## Drafting Rules -- Use a specific title: `# CONSTITUTION.md - {Agent Name}`. +- Use a specific title: `# CHARTER.md - {Agent Name}`. - Keep `Purpose` to one or two paragraphs plus a concrete `Success looks like` sentence. - Define `Scope` with three subsections: `In scope`, `Out of scope`, and `Handoff conditions`. - Define permission tiers as `Auto`, `Notify`, `Approve`, and `Never`, even if some tiers are sparse. @@ -29,7 +29,7 @@ description: Draft a complete agent constitution from an instructional prompt or ## Interpreting Prompts -Map the prompt into constitution sections: +Map the prompt into charter sections: - Agent mission, cadence, and audience -> `Purpose` - Things the agent may inspect, summarize, change, create, or send -> `In scope` @@ -45,7 +45,7 @@ Map the prompt into constitution sections: Before finalizing, verify that: -- The constitution can stand alone without the original prompt. +- The charter can stand alone without the original prompt. - Every autonomous action has a matching permission tier and a failure default. - The `Never` tier contains hard blocks, not preferences. - Sensitive data handling is explicit. diff --git a/.agents/skills/populate-charter/agents/openai.yaml b/.agents/skills/populate-charter/agents/openai.yaml new file mode 100644 index 0000000..b901535 --- /dev/null +++ b/.agents/skills/populate-charter/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Populate Charter" + short_description: "Draft agent charters from prompts" + default_prompt: "Use $populate-charter to draft a charter from this agent brief." diff --git a/.agents/skills/populate-constitution/assets/constitution-template.md b/.agents/skills/populate-charter/assets/charter-template.md similarity index 96% rename from .agents/skills/populate-constitution/assets/constitution-template.md rename to .agents/skills/populate-charter/assets/charter-template.md index a7b3b42..3b3c7c0 100644 --- a/.agents/skills/populate-constitution/assets/constitution-template.md +++ b/.agents/skills/populate-charter/assets/charter-template.md @@ -1,4 +1,4 @@ -# CONSTITUTION.md - {Agent Name} +# CHARTER.md - {Agent Name} **Version:** 1.0 **Owner:** {Owner or team} @@ -43,8 +43,8 @@ -- {Condition where this constitution has no opinion and should defer} -- {Condition where this constitution has no opinion and should defer} +- {Condition where this charter has no opinion and should defer} +- {Condition where this charter has no opinion and should defer} --- diff --git a/.agents/skills/populate-constitution/agents/openai.yaml b/.agents/skills/populate-constitution/agents/openai.yaml deleted file mode 100644 index 1cc1e63..0000000 --- a/.agents/skills/populate-constitution/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Populate Constitution" - short_description: "Draft agent constitutions from prompts" - default_prompt: "Use $populate-constitution to draft a constitution from this agent brief." diff --git a/README.md b/README.md index 0c1dc49..16e171c 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Auth is OIDC-based and supports multiple authorization servers concurrently (Key For local no-auth MCP runs, when no `[[auth]]` blocks are configured, callers may provide a best-effort agent identity with `?agent_id=` on `/mcp/{server}` and `/api/v1/agent/rules`. For example: `http://localhost:8080/mcp/shortcut?agent_id=hunners-codex`. This ID is ignored as soon as inbound auth is configured. -The Settings UI can also select a default ValidMind agent record. AI Evaluation uses that record when an incoming runtime agent ID is missing or does not map to a synced agent, allowing local no-auth runs to evaluate against a known constitution without adding TOML. +The Settings UI can also select a default ValidMind agent record. AI Evaluation uses that record when an incoming runtime agent ID is missing or does not map to a synced agent, allowing local no-auth runs to evaluate against a known charter without adding TOML. ## HTTP surface diff --git a/atryum.example.toml b/atryum.example.toml index f8e375a..9557c69 100644 --- a/atryum.example.toml +++ b/atryum.example.toml @@ -76,7 +76,7 @@ api_key = "" # name = "staging" # api_key = "" -# Agent sync and AI evaluation settings (org, record type, constitution field) +# Agent sync and AI evaluation settings (org, record type, charter field) # are managed through the Atryum Settings UI and stored in the database. # They no longer need to be configured here. diff --git a/cmd/atryum/main.go b/cmd/atryum/main.go index 1f261ac..2a82fde 100644 --- a/cmd/atryum/main.go +++ b/cmd/atryum/main.go @@ -468,7 +468,7 @@ func (a *agentsLookupAdapter) GetByAgentID(ctx context.Context, agentID string) if err != nil { return invocation.AgentRecord{}, err } - return invocation.AgentRecord{ID: rec.ID, VMCUID: rec.VMCUID, VMOrganizationCUID: rec.VMOrganizationCUID, Constitution: rec.Constitution}, nil + return invocation.AgentRecord{ID: rec.ID, VMCUID: rec.VMCUID, VMOrganizationCUID: rec.VMOrganizationCUID, Charter: rec.Charter}, nil } func (a *agentsLookupAdapter) GetByVMCUID(ctx context.Context, vmCUID string) (invocation.AgentRecord, error) { @@ -476,7 +476,7 @@ func (a *agentsLookupAdapter) GetByVMCUID(ctx context.Context, vmCUID string) (i if err != nil { return invocation.AgentRecord{}, err } - return invocation.AgentRecord{ID: rec.ID, VMCUID: rec.VMCUID, VMOrganizationCUID: rec.VMOrganizationCUID, Constitution: rec.Constitution}, nil + return invocation.AgentRecord{ID: rec.ID, VMCUID: rec.VMCUID, VMOrganizationCUID: rec.VMOrganizationCUID, Charter: rec.Charter}, nil } // llmConfigsLookupAdapter bridges store.LLMConfigsRepo → invocation.LLMConfigProvider. @@ -499,15 +499,15 @@ func (a *llmConfigsLookupAdapter) GetLLMConfig(ctx context.Context, id string) ( } // syncSettingsAdapter bridges store.AgentSyncSettingsRepo → invocation.SyncSettingsProvider. -// ConstitutionFieldKey is read from the DB on every call so that changes saved +// CharterFieldKey is read from the DB on every call so that changes saved // via the Settings UI take effect immediately without a restart. type syncSettingsAdapter struct { repo *store.AgentSyncSettingsRepo } -func (a *syncSettingsAdapter) ConstitutionFieldKey(ctx context.Context) string { +func (a *syncSettingsAdapter) CharterFieldKey(ctx context.Context) string { s, _ := a.repo.Get(ctx) - return s.ConstitutionFieldKey + return s.CharterFieldKey } func (a *syncSettingsAdapter) DefaultAgentVMCUID(ctx context.Context) string { @@ -530,7 +530,7 @@ func (e *evaluatorAdapter) EvaluateToolCall(ctx context.Context, req invocation. ModelConfigCUID: req.ModelConfigCUID, OrgCUID: req.OrgCUID, AgentVMCUID: req.AgentVMCUID, - ConstitutionFieldKey: req.ConstitutionFieldKey, + CharterFieldKey: req.CharterFieldKey, ServerName: req.ServerName, ToolName: req.ToolName, ToolArgs: req.ToolArgs, diff --git a/docs/testing/ai-evaluation.md b/docs/testing/ai-evaluation.md index 218e4a7..317fa21 100644 --- a/docs/testing/ai-evaluation.md +++ b/docs/testing/ai-evaluation.md @@ -15,7 +15,7 @@ This guide walks through manually testing the AI Evaluation rule type end-to-end ## Step 1 — Configure `atryum.toml` -`atryum.toml` only needs two sections for AI Evaluation: `[backend]` (ValidMind credentials) and `[[auth]]` (JWT validation). Agent sync and constitution settings have moved to the UI — see **Step 1b** below. +`atryum.toml` only needs two sections for AI Evaluation: `[backend]` (ValidMind credentials) and `[[auth]]` (JWT validation). Agent sync and charter settings have moved to the UI — see **Step 1b** below. ```toml # ValidMind backend connection (machine-user credentials). @@ -42,7 +42,7 @@ agent_id_claim = "sub" # claim that carries the agent's unique identity ## Step 1b — Configure Agent Sync in the UI -The organization, record type, and constitution field are now configured in the Atryum UI under **Settings → Agent Record Sync**. This replaces the old `[agent_sync]` and `[ai_evaluation]` TOML sections. +The organization, record type, and charter field are now configured in the Atryum UI under **Settings → Agent Record Sync**. This replaces the old `[agent_sync]` and `[ai_evaluation]` TOML sections. 1. Open the Atryum UI and navigate to **Settings**. 2. Under **Agent Record Sync**, fill in the three fields: @@ -51,7 +51,7 @@ The organization, record type, and constitution field are now configured in the |---|---| | **Organization** | Select the ValidMind organization whose agent records should be synced | | **Record Type** | Select the primary record type that identifies agent inventory models (e.g. `ai-agents`) | - | **Constitution Field** | Select the custom field key on inventory models that stores the constitution text (e.g. `constitution`) | + | **Charter Field** | Select the custom field key on inventory models that stores the charter text (e.g. `charter`) | The dropdowns are cascading — selecting an organization loads its record types; selecting a record type loads the custom fields available on that type. @@ -89,13 +89,13 @@ In your identity provider (e.g. Auth0): 3. Click the agent to open it, then paste the **user ID** (`sub` value) from Step 3 into the **Agent IDs** field. 4. Save. -Atryum will now recognise bearer tokens issued for that user as belonging to this agent, enabling constitution lookup and org cross-validation when an AI Evaluation rule fires. +Atryum will now recognise bearer tokens issued for that user as belonging to this agent, enabling charter lookup and org cross-validation when an AI Evaluation rule fires. --- -## Step 5 — Author the Agent Constitution +## Step 5 — Author the Agent Charter -The constitution is stored as a custom field (key = `constitution_field_key`) on the agent's Inventory Model in ValidMind. It is free-form Markdown, but the LLM-as-judge recognises the following sections to decide its verdict: +The charter is stored as a custom field (key = `charter_field_key`) on the agent's Inventory Model in ValidMind. It is free-form Markdown, but the LLM-as-judge recognises the following sections to decide its verdict: ### Permission tiers (tool-level) @@ -112,7 +112,7 @@ List conditions under which the LLM should route the invocation to the human app ```markdown ### Human approval required when - The SQL query modifies data (INSERT, UPDATE, DELETE, DROP, …) -- The request targets a table not explicitly listed in the constitution +- The request targets a table not explicitly listed in the charter - The query returns more than 10,000 rows ``` @@ -125,20 +125,20 @@ List conditions under which the LLM should pass evaluation to the next matching - The tool is not a database tool (let downstream rules handle it) ``` -### Constitution chain and precedence +### Charter chain and precedence -If the agent's Inventory Model has upstream dependencies, Atryum collects constitutions from the entire chain (most-upstream ancestor first, this agent last) and presents them all to the LLM. The judge applies these precedence rules: +If the agent's Inventory Model has upstream dependencies, Atryum collects charters from the entire chain (most-upstream ancestor first, this agent last) and presents them all to the LLM. The judge applies these precedence rules: -1. **Downstream wins**: later constitutions in the chain are more specific and override earlier ones when they conflict. -2. **Explicit delegation is honoured**: if an upstream says "downstream constitutions may override this", a downstream grant (even conditional) replaces the upstream rule. -3. **Most specific match first**: apply the innermost rule that covers the action; fall back to upstream only when no downstream constitution addresses it. +1. **Downstream wins**: later charters in the chain are more specific and override earlier ones when they conflict. +2. **Explicit delegation is honoured**: if an upstream says "downstream charters may override this", a downstream grant (even conditional) replaces the upstream rule. +3. **Most specific match first**: apply the innermost rule that covers the action; fall back to upstream only when no downstream charter addresses it. 4. **Grants override blanket denials**: a downstream "allow with human approval" beats an upstream "deny all". **Example:** ``` [Upstream agent] -Deny all Postgres queries. Downstream constitutions may override this. +Deny all Postgres queries. Downstream charters may override this. --- @@ -178,8 +178,8 @@ Send a tool call through Atryum using a bearer token issued for the agent identi 1. Atryum receives the invocation and matches it to the AI Evaluation rule. 2. It resolves the agent's Inventory Model CUID and org CUID from the stored record. -3. It calls `POST /api/atryum/unstable/evaluate` on the ValidMind backend with the agent details, constitution field key, and tool call context. -4. The backend fetches the constitution chain from the Inventory Model's custom fields, builds a prompt with precedence rules, and asks the configured LLM for a verdict. +3. It calls `POST /api/atryum/unstable/evaluate` on the ValidMind backend with the agent details, charter field key, and tool call context. +4. The backend fetches the charter chain from the Inventory Model's custom fields, builds a prompt with precedence rules, and asks the configured LLM for a verdict. 5. Atryum receives `{ verdict, reason }` and routes the invocation accordingly: | Verdict | Disposition | UI outcome | @@ -199,7 +199,7 @@ In the Atryum UI, open **Invocations** to see the outcome and the disposition re |---|---| | No agents in startup log | `org_cuid` or `agent_record_type_slug` is wrong, or backend credentials are incorrect | | Rule never matches | Agent IDs field is empty, the user's `sub` doesn't match `agent_id_claim`, or the bearer token is not being sent by the agent | -| Evaluation falls back to `human_approval` | `constitution_field_key` doesn't match the custom field name in VM, or the LLM call failed (check logs) | +| Evaluation falls back to `human_approval` | `charter_field_key` doesn't match the custom field name in VM, or the LLM call failed (check logs) | | 403 from `/evaluate` | `org_cuid` in the request doesn't match the agent's organization in ValidMind | | Upstream deny overrides downstream grant | Backend not yet redeployed with the precedence-rules prompt update; check that `verdict` (not `approved`) is present in the `/evaluate` response | -| `next_rule` never advances | Rules are not stacked in priority order, or the constitution does not contain a `### Defer to next rule when` section | +| `next_rule` never advances | Rules are not stacked in priority order, or the charter does not contain a `### Defer to next rule when` section | diff --git a/internal/api/handlers.go b/internal/api/handlers.go index daee08e..94fae1f 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -92,7 +92,7 @@ type agentsRepo interface { GetByVMCUID(ctx context.Context, vmCUID string) (store.AgentRecord, error) UpdateEnabled(ctx context.Context, id string, enabled bool) error UpdateAgentIDs(ctx context.Context, id string, agentIDs string) error - UpdateMeta(ctx context.Context, id, name, description, constitution string) error + UpdateMeta(ctx context.Context, id, name, description, charter string) error Create(ctx context.Context, agent store.AgentRecord) error Delete(ctx context.Context, id string) error DeleteSynced(ctx context.Context) error @@ -348,7 +348,7 @@ type AdminAgent struct { AgentIDs []string `json:"agent_ids"` SyncedAt time.Time `json:"synced_at"` Enabled bool `json:"enabled"` - Constitution string `json:"constitution,omitempty"` + Charter string `json:"charter,omitempty"` // Synced is true when this agent originated from a ValidMind sync // (vm_organization_cuid is non-empty). Synced agents cannot be deleted // manually — they are removed by re-syncing with a different org/record-type. @@ -356,19 +356,19 @@ type AdminAgent struct { } type AdminAgentInput struct { - Enabled bool `json:"enabled"` - AgentIDs []string `json:"agent_ids,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Constitution string `json:"constitution,omitempty"` + Enabled bool `json:"enabled"` + AgentIDs []string `json:"agent_ids,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Charter string `json:"charter,omitempty"` } type AdminAgentCreateInput struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - Enabled bool `json:"enabled"` - AgentIDs []string `json:"agent_ids,omitempty"` - Constitution string `json:"constitution,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Enabled bool `json:"enabled"` + AgentIDs []string `json:"agent_ids,omitempty"` + Charter string `json:"charter,omitempty"` } type AgentListResponse struct { @@ -385,7 +385,7 @@ func toAdminAgent(a store.AgentRecord) AdminAgent { AgentIDs: ids, SyncedAt: a.SyncedAt, Enabled: a.Enabled, - Constitution: a.Constitution, + Charter: a.Charter, Synced: a.VMOrganizationCUID != "", } } @@ -2125,7 +2125,7 @@ func (h *Handler) adminModelConfigs(w http.ResponseWriter, r *http.Request) { type AgentSyncSettingsResponse struct { OrgCUID string `json:"org_cuid"` AgentRecordTypeSlug string `json:"agent_record_type_slug"` - ConstitutionFieldKey string `json:"constitution_field_key"` + CharterFieldKey string `json:"charter_field_key"` SummaryModelConfigCUID string `json:"summary_model_config_cuid"` SummaryAtryumLLMConfigID string `json:"summary_atryum_llm_config_id"` DefaultAgentVMCUID string `json:"default_agent_vm_cuid"` @@ -2138,7 +2138,7 @@ type AgentSyncSettingsResponse struct { type AgentSyncSettingsInput struct { OrgCUID string `json:"org_cuid"` AgentRecordTypeSlug string `json:"agent_record_type_slug"` - ConstitutionFieldKey string `json:"constitution_field_key"` + CharterFieldKey string `json:"charter_field_key"` SummaryModelConfigCUID string `json:"summary_model_config_cuid"` SummaryAtryumLLMConfigID string `json:"summary_atryum_llm_config_id"` DefaultAgentVMCUID string `json:"default_agent_vm_cuid"` @@ -2346,7 +2346,7 @@ func (h *Handler) adminSettings(w http.ResponseWriter, r *http.Request) { resp := AgentSyncSettingsResponse{ OrgCUID: s.OrgCUID, AgentRecordTypeSlug: s.AgentRecordTypeSlug, - ConstitutionFieldKey: s.ConstitutionFieldKey, + CharterFieldKey: s.CharterFieldKey, SummaryModelConfigCUID: s.SummaryModelConfigCUID, SummaryAtryumLLMConfigID: s.SummaryAtryumLLMConfigID, DefaultAgentVMCUID: s.DefaultAgentVMCUID, @@ -2380,7 +2380,7 @@ func (h *Handler) adminSettings(w http.ResponseWriter, r *http.Request) { if err := h.agentSyncSettingsRepo.Save(r.Context(), store.AgentSyncSettings{ OrgCUID: input.OrgCUID, AgentRecordTypeSlug: input.AgentRecordTypeSlug, - ConstitutionFieldKey: input.ConstitutionFieldKey, + CharterFieldKey: input.CharterFieldKey, SummaryModelConfigCUID: input.SummaryModelConfigCUID, SummaryAtryumLLMConfigID: input.SummaryAtryumLLMConfigID, DefaultAgentVMCUID: input.DefaultAgentVMCUID, @@ -2407,7 +2407,7 @@ func (h *Handler) adminSettings(w http.ResponseWriter, r *http.Request) { resp := AgentSyncSettingsResponse{ OrgCUID: s.OrgCUID, AgentRecordTypeSlug: s.AgentRecordTypeSlug, - ConstitutionFieldKey: s.ConstitutionFieldKey, + CharterFieldKey: s.CharterFieldKey, SummaryModelConfigCUID: s.SummaryModelConfigCUID, SummaryAtryumLLMConfigID: s.SummaryAtryumLLMConfigID, DefaultAgentVMCUID: s.DefaultAgentVMCUID, @@ -2617,7 +2617,7 @@ func (h *Handler) adminAgents(w http.ResponseWriter, r *http.Request) { VMDescription: req.Description, AgentIDs: agentIDsJSON, Enabled: req.Enabled, - Constitution: req.Constitution, + Charter: req.Charter, } if err := h.agentsRepo.Create(r.Context(), agent); err != nil { writeError(w, http.StatusInternalServerError, "failed to create agent") @@ -2714,8 +2714,8 @@ func (h *Handler) adminAgentDetail(w http.ResponseWriter, r *http.Request) { return } } - if req.Name != "" || req.Constitution != "" { - if err := h.agentsRepo.UpdateMeta(r.Context(), id, req.Name, req.Description, req.Constitution); err != nil { + if req.Name != "" || req.Charter != "" { + if err := h.agentsRepo.UpdateMeta(r.Context(), id, req.Name, req.Description, req.Charter); err != nil { status := http.StatusInternalServerError if err == sql.ErrNoRows { status = http.StatusNotFound diff --git a/internal/backend/client.go b/internal/backend/client.go index 2614e17..ee46c6b 100644 --- a/internal/backend/client.go +++ b/internal/backend/client.go @@ -348,7 +348,7 @@ type EvaluateRequest struct { ModelConfigCUID string `json:"model_config_cuid"` OrgCUID string `json:"org_cuid,omitempty"` AgentVMCUID string `json:"agent_vm_cuid,omitempty"` - ConstitutionFieldKey string `json:"constitution_field_key,omitempty"` + CharterFieldKey string `json:"charter_field_key,omitempty"` ServerName string `json:"server_name"` ToolName string `json:"tool_name"` ToolArgs map[string]any `json:"tool_args,omitempty"` diff --git a/internal/invocation/local_evaluator.go b/internal/invocation/local_evaluator.go index 0dedb64..a1b5909 100644 --- a/internal/invocation/local_evaluator.go +++ b/internal/invocation/local_evaluator.go @@ -28,16 +28,16 @@ type LocalLLMConfig struct { BaseURL string // required for openai_compatible } -const judgeSystemPrompt = `You are an AI safety judge. Evaluate whether the following tool call should be approved, denied, escalated to a human, or deferred to the next rule, based on the agent's governing constitution. +const judgeSystemPrompt = `You are an AI safety judge. Evaluate whether the following tool call should be approved, denied, escalated to a human, or deferred to the next rule, based on the agent's governing charter. -Constitution: +Charter: %s Verdicts: -- "approved" — the tool call is clearly permitted by the constitution -- "denied" — the tool call is clearly prohibited by the constitution +- "approved" — the tool call is clearly permitted by the charter +- "denied" — the tool call is clearly prohibited by the charter - "human_approval" — the tool call is ambiguous and should be reviewed by a human -- "next_rule" — the constitution does not cover this case; defer to the next matching rule +- "next_rule" — the charter does not cover this case; defer to the next matching rule Respond with valid JSON only — no markdown fences, no extra text: {"verdict": "approved|denied|human_approval|next_rule", "confidence": 0.0, "reason": "..."}` @@ -68,7 +68,7 @@ func (e *LocalEvaluatorClient) EvaluateToolCall(ctx context.Context, req Evaluat } userContent := e.buildUserMessage(req) - systemContent := fmt.Sprintf(judgeSystemPrompt, req.Constitution) + systemContent := fmt.Sprintf(judgeSystemPrompt, req.Charter) var rawResp string switch cfg.Provider { diff --git a/internal/invocation/service.go b/internal/invocation/service.go index b7ec0ff..c68c7bc 100644 --- a/internal/invocation/service.go +++ b/internal/invocation/service.go @@ -66,9 +66,9 @@ type AgentLookup interface { // invocation package to avoid a circular import. type AgentRecord struct { ID string // local Atryum agent UUID (used for rule matching) - VMCUID string // VM inventory model CUID (used for constitution lookup) + VMCUID string // VM inventory model CUID (used for charter lookup) VMOrganizationCUID string // VM organization CUID (used for cross-tenant validation) - Constitution string // governing text for local LLM-as-judge evaluation + Charter string // governing text for local LLM-as-judge evaluation } // EvaluatorClient is the minimal interface required by the invocation service @@ -85,10 +85,10 @@ type SummaryClient interface { // SyncSettingsProvider lets the invocation service read the current agent sync // configuration on demand without importing the store package. The service -// calls ConstitutionFieldKey on every AI evaluation so that changes saved via +// calls CharterFieldKey on every AI evaluation so that changes saved via // the Settings UI are picked up immediately without a restart. type SyncSettingsProvider interface { - ConstitutionFieldKey(ctx context.Context) string + CharterFieldKey(ctx context.Context) string DefaultAgentVMCUID(ctx context.Context) string SummarySettings(ctx context.Context) (orgCUID string, modelConfigCUID string) } @@ -99,12 +99,12 @@ type EvaluateRequest struct { ModelConfigCUID string `json:"model_config_cuid"` OrgCUID string `json:"org_cuid,omitempty"` AgentVMCUID string `json:"agent_vm_cuid,omitempty"` - ConstitutionFieldKey string `json:"constitution_field_key,omitempty"` + CharterFieldKey string `json:"charter_field_key,omitempty"` // AtryumLLMConfigID references a local LLM config for native evaluation. // When set, the local evaluator is used instead of the VM backend. AtryumLLMConfigID string `json:"atryum_llm_config_id,omitempty"` - // Constitution is the agent's governing text sent to the local LLM judge. - Constitution string `json:"constitution,omitempty"` + // Charter is the agent's governing text sent to the local LLM judge. + Charter string `json:"charter,omitempty"` ServerName string `json:"server_name"` ToolName string `json:"tool_name"` ToolArgs map[string]any `json:"tool_args,omitempty"` @@ -173,7 +173,7 @@ type Service struct { agents AgentLookup evaluator EvaluatorClient summarizer SummaryClient - syncSettings SyncSettingsProvider // nil = no constitution lookup + syncSettings SyncSettingsProvider // nil = no charter lookup defaultTimeout time.Duration mu sync.Mutex pendingApprovals map[string]chan approvalDecision @@ -406,12 +406,12 @@ func (s *Service) runAIEvaluation(ctx context.Context, rule *ApprovalRule, serve // --- Local LLM path --- if rule.AtryumLLMConfigID != "" { - if agentRec.Constitution == "" { - slog.Error("ai_evaluation (local): agent has no constitution configured; denying tool call", + if agentRec.Charter == "" { + slog.Error("ai_evaluation (local): agent has no charter configured; denying tool call", "rule_id", rule.ID, "server", serverName, "tool", toolName, "agent_id", agentID) return policy.Decision{ Disposition: policy.DispositionNever, - Reason: "ai_evaluation denied: no constitution configured for this agent", + Reason: "ai_evaluation denied: no charter configured for this agent", }, nil } @@ -428,7 +428,7 @@ func (s *Service) runAIEvaluation(ctx context.Context, rule *ApprovalRule, serve resp, err := s.evaluator.EvaluateToolCall(evalCtx, EvaluateRequest{ AtryumLLMConfigID: rule.AtryumLLMConfigID, - Constitution: agentRec.Constitution, + Charter: agentRec.Charter, ServerName: serverName, ToolName: toolName, ToolArgs: toolArgs, @@ -456,23 +456,23 @@ func (s *Service) runAIEvaluation(ctx context.Context, rule *ApprovalRule, serve agentVMCUID := agentRec.VMCUID orgCUID := agentRec.VMOrganizationCUID - constitutionFieldKey := "" + charterFieldKey := "" if s.syncSettings != nil { - constitutionFieldKey = s.syncSettings.ConstitutionFieldKey(ctx) + charterFieldKey = s.syncSettings.CharterFieldKey(ctx) } - if agentVMCUID == "" || constitutionFieldKey == "" { - slog.Error("ai_evaluation: missing agent or constitution context; denying tool call", + if agentVMCUID == "" || charterFieldKey == "" { + slog.Error("ai_evaluation: missing agent or charter context; denying tool call", "rule_id", rule.ID, "server", serverName, "tool", toolName, "agent_id", agentID, "agent_vm_cuid", agentVMCUID, - "constitution_field_key", constitutionFieldKey, + "charter_field_key", charterFieldKey, ) return policy.Decision{ Disposition: policy.DispositionNever, - Reason: "ai_evaluation denied: no constitution available for this agent", + Reason: "ai_evaluation denied: no charter available for this agent", }, nil } @@ -484,17 +484,17 @@ func (s *Service) runAIEvaluation(ctx context.Context, rule *ApprovalRule, serve "agent_vm_cuid", agentVMCUID, "org_cuid", orgCUID, "model_config_cuid", rule.ModelConfigCUID, - "constitution_field_key", constitutionFieldKey, + "charter_field_key", charterFieldKey, ) evalCtx, cancel := context.WithTimeout(ctx, 60*time.Second) defer cancel() resp, err := s.evaluator.EvaluateToolCall(evalCtx, EvaluateRequest{ - ModelConfigCUID: rule.ModelConfigCUID, - OrgCUID: orgCUID, - AgentVMCUID: agentVMCUID, - ConstitutionFieldKey: constitutionFieldKey, + ModelConfigCUID: rule.ModelConfigCUID, + OrgCUID: orgCUID, + AgentVMCUID: agentVMCUID, + CharterFieldKey: charterFieldKey, ServerName: serverName, ToolName: toolName, ToolArgs: toolArgs, diff --git a/internal/invocation/service_test.go b/internal/invocation/service_test.go index 92e3a27..b804631 100644 --- a/internal/invocation/service_test.go +++ b/internal/invocation/service_test.go @@ -288,7 +288,7 @@ func TestSubmitAIEvaluationUsesDefaultAgentRecordForUnmappedAgentID(t *testing.T invRepo := store.NewInvocationRepo(db) eventRepo := store.NewEventRepo(db) evaluator := &evaluateClientStub{ - resp: invocation.EvaluateResponse{Verdict: "approved", Reason: "default constitution allows this"}, + resp: invocation.EvaluateResponse{Verdict: "approved", Reason: "default charter allows this"}, } defaultAgent := invocation.AgentRecord{ ID: "agent-local-default", @@ -313,8 +313,8 @@ func TestSubmitAIEvaluationUsesDefaultAgentRecordForUnmappedAgentID(t *testing.T agentLookupStub{byVMCUID: map[string]invocation.AgentRecord{defaultAgent.VMCUID: defaultAgent}}, evaluator, summarySettingsStub{ - constitutionFieldKey: "constitution", - defaultAgentVMCUID: defaultAgent.VMCUID, + charterFieldKey: "charter", + defaultAgentVMCUID: defaultAgent.VMCUID, }, ) @@ -337,8 +337,8 @@ func TestSubmitAIEvaluationUsesDefaultAgentRecordForUnmappedAgentID(t *testing.T if req.OrgCUID != defaultAgent.VMOrganizationCUID { t.Fatalf("OrgCUID = %q", req.OrgCUID) } - if req.ConstitutionFieldKey != "constitution" { - t.Fatalf("ConstitutionFieldKey = %q", req.ConstitutionFieldKey) + if req.CharterFieldKey != "charter" { + t.Fatalf("CharterFieldKey = %q", req.CharterFieldKey) } if req.ModelConfigCUID != "model-ai" { t.Fatalf("ModelConfigCUID = %q", req.ModelConfigCUID) @@ -376,14 +376,14 @@ func (s *summaryClientStub) request() invocation.SummaryRequest { } type summarySettingsStub struct { - orgCUID string - modelConfigCUID string - constitutionFieldKey string - defaultAgentVMCUID string + orgCUID string + modelConfigCUID string + charterFieldKey string + defaultAgentVMCUID string } -func (s summarySettingsStub) ConstitutionFieldKey(context.Context) string { - return s.constitutionFieldKey +func (s summarySettingsStub) CharterFieldKey(context.Context) string { + return s.charterFieldKey } func (s summarySettingsStub) DefaultAgentVMCUID(context.Context) string { diff --git a/internal/store/agent_sync_settings.go b/internal/store/agent_sync_settings.go index 29bb5b9..c3529bc 100644 --- a/internal/store/agent_sync_settings.go +++ b/internal/store/agent_sync_settings.go @@ -16,7 +16,7 @@ import ( type AgentSyncSettings struct { OrgCUID string AgentRecordTypeSlug string - ConstitutionFieldKey string + CharterFieldKey string SummaryModelConfigCUID string SummaryAtryumLLMConfigID string DefaultAgentVMCUID string @@ -24,7 +24,7 @@ type AgentSyncSettings struct { } var agentSyncSettingsColumns = []string{ - "org_cuid", "agent_record_type_slug", "constitution_field_key", "summary_model_config_cuid", "summary_atryum_llm_config_id", "default_agent_vm_cuid", "updated_at", + "org_cuid", "agent_record_type_slug", "charter_field_key", "summary_model_config_cuid", "summary_atryum_llm_config_id", "default_agent_vm_cuid", "updated_at", } // AgentSyncSettingsRepo provides read/write access to the singleton @@ -74,21 +74,21 @@ func (r *AgentSyncSettingsRepo) Save(ctx context.Context, s AgentSyncSettings) e var args []any if r.dialect == DialectPostgres { // PostgreSQL upsert - query = `INSERT INTO agent_sync_settings (id, org_cuid, agent_record_type_slug, constitution_field_key, summary_model_config_cuid, summary_atryum_llm_config_id, default_agent_vm_cuid, updated_at) + query = `INSERT INTO agent_sync_settings (id, org_cuid, agent_record_type_slug, charter_field_key, summary_model_config_cuid, summary_atryum_llm_config_id, default_agent_vm_cuid, updated_at) VALUES (1, $1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO UPDATE SET org_cuid = EXCLUDED.org_cuid, agent_record_type_slug = EXCLUDED.agent_record_type_slug, - constitution_field_key = EXCLUDED.constitution_field_key, + charter_field_key = EXCLUDED.charter_field_key, summary_model_config_cuid = EXCLUDED.summary_model_config_cuid, summary_atryum_llm_config_id = EXCLUDED.summary_atryum_llm_config_id, default_agent_vm_cuid = EXCLUDED.default_agent_vm_cuid, updated_at = EXCLUDED.updated_at` - args = []any{s.OrgCUID, s.AgentRecordTypeSlug, s.ConstitutionFieldKey, s.SummaryModelConfigCUID, s.SummaryAtryumLLMConfigID, s.DefaultAgentVMCUID, now} + args = []any{s.OrgCUID, s.AgentRecordTypeSlug, s.CharterFieldKey, s.SummaryModelConfigCUID, s.SummaryAtryumLLMConfigID, s.DefaultAgentVMCUID, now} } else { // SQLite: INSERT OR REPLACE replaces the row when the primary key conflicts. - query = `INSERT OR REPLACE INTO agent_sync_settings (id, org_cuid, agent_record_type_slug, constitution_field_key, summary_model_config_cuid, summary_atryum_llm_config_id, default_agent_vm_cuid, updated_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?)` - args = []any{s.OrgCUID, s.AgentRecordTypeSlug, s.ConstitutionFieldKey, s.SummaryModelConfigCUID, s.SummaryAtryumLLMConfigID, s.DefaultAgentVMCUID, now} + query = `INSERT OR REPLACE INTO agent_sync_settings (id, org_cuid, agent_record_type_slug, charter_field_key, summary_model_config_cuid, summary_atryum_llm_config_id, default_agent_vm_cuid, updated_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?)` + args = []any{s.OrgCUID, s.AgentRecordTypeSlug, s.CharterFieldKey, s.SummaryModelConfigCUID, s.SummaryAtryumLLMConfigID, s.DefaultAgentVMCUID, now} } result, err := r.db.ExecContext(ctx, query, args...) if err != nil { @@ -105,7 +105,7 @@ func scanAgentSyncSettings(row interface{ Scan(dest ...any) error }) (AgentSyncS if err := row.Scan( &s.OrgCUID, &s.AgentRecordTypeSlug, - &s.ConstitutionFieldKey, + &s.CharterFieldKey, &s.SummaryModelConfigCUID, &s.SummaryAtryumLLMConfigID, &s.DefaultAgentVMCUID, diff --git a/internal/store/agents.go b/internal/store/agents.go index 9fc8a34..1fdf35e 100644 --- a/internal/store/agents.go +++ b/internal/store/agents.go @@ -23,10 +23,10 @@ type AgentRecord struct { AgentIDs string // JSON array, e.g. "[]" or "[\"id1\",\"id2\"]" Enabled bool SyncedAt time.Time - // Constitution is the governing text used by local LLM-as-judge evaluation. + // Charter is the governing text used by local LLM-as-judge evaluation. // Set by humans for manually-created agents; ignored for VM-synced agents - // (which get their constitution from the ValidMind custom field at eval time). - Constitution string + // (which get their charter from the ValidMind custom field at eval time). + Charter string } // AgentsRepo provides upsert and query operations for the agents table. @@ -47,7 +47,7 @@ func NewAgentsRepoWithDialect(db *sql.DB, dialect Dialect) *AgentsRepo { var agentColumns = []string{ "id", "vm_organization_cuid", "vm_organization_name", "vm_cuid", "vm_name", "vm_description", - "agent_ids", "enabled", "synced_at", "constitution", + "agent_ids", "enabled", "synced_at", "charter", } // Upsert inserts a new agent record or, on conflict with an existing vm_cuid, @@ -76,7 +76,7 @@ func (r *AgentsRepo) Upsert(ctx context.Context, agent AgentRecord) error { agentIDs, agent.Enabled, syncedAt, - agent.Constitution, + agent.Charter, ). Suffix(`ON CONFLICT (vm_cuid) DO UPDATE SET vm_organization_cuid = excluded.vm_organization_cuid, @@ -85,7 +85,7 @@ func (r *AgentsRepo) Upsert(ctx context.Context, agent AgentRecord) error { vm_description = excluded.vm_description, synced_at = excluded.synced_at`). ToSql() - // Note: constitution is intentionally NOT updated on upsert — it is a + // Note: charter is intentionally NOT updated on upsert — it is a // manually-maintained field and must survive agent re-syncs. if err != nil { return fmt.Errorf("build agent upsert: %w", err) @@ -200,7 +200,7 @@ func (r *AgentsRepo) Create(ctx context.Context, agent AgentRecord) error { agentIDs, agent.Enabled, syncedAt, - agent.Constitution, + agent.Charter, ). ToSql() if err != nil { @@ -210,13 +210,13 @@ func (r *AgentsRepo) Create(ctx context.Context, agent AgentRecord) error { return err } -// UpdateMeta updates the user-visible name, description, and constitution of +// UpdateMeta updates the user-visible name, description, and charter of // the agent with the given id. -func (r *AgentsRepo) UpdateMeta(ctx context.Context, id, name, description, constitution string) error { +func (r *AgentsRepo) UpdateMeta(ctx context.Context, id, name, description, charter string) error { query, args, err := r.sb.Update("agents"). Set("vm_name", name). Set("vm_description", emptyToNil(description)). - Set("constitution", constitution). + Set("charter", charter). Where(sq.Eq{"id": id}). ToSql() if err != nil { @@ -315,7 +315,7 @@ func scanAgent(scanner interface{ Scan(dest ...any) error }) (AgentRecord, error if err := scanner.Scan( &a.ID, &a.VMOrganizationCUID, &a.VMOrganizationName, &a.VMCUID, &a.VMName, &vmDescription, - &a.AgentIDs, &a.Enabled, &a.SyncedAt, &a.Constitution, + &a.AgentIDs, &a.Enabled, &a.SyncedAt, &a.Charter, ); err != nil { return AgentRecord{}, err } diff --git a/internal/store/db_test.go b/internal/store/db_test.go index cae84e5..4638437 100644 --- a/internal/store/db_test.go +++ b/internal/store/db_test.go @@ -73,6 +73,7 @@ func TestMigrationRegistryPreservesExistingVersionsAndNames(t *testing.T) { {18, "018_rule_llm_config"}, {19, "019_settings_summary_llm"}, {20, "020_managed_agent_sessions"}, + {21, "021_rename_constitution_to_charter"}, } for i, w := range want { if migrations[i].Version != w.version || migrations[i].Name != w.name { diff --git a/internal/store/migrations/021_rename_constitution_to_charter.go b/internal/store/migrations/021_rename_constitution_to_charter.go new file mode 100644 index 0000000..51afe4e --- /dev/null +++ b/internal/store/migrations/021_rename_constitution_to_charter.go @@ -0,0 +1,14 @@ +package migrations + +func migration021() Definition { + return Definition{ + Version: 21, + Name: "021_rename_constitution_to_charter", + Steps: []Step{ + Raw("rename agents.constitution to agents.charter", + `ALTER TABLE agents RENAME COLUMN constitution TO charter`), + Raw("rename agent_sync_settings.constitution_field_key to agent_sync_settings.charter_field_key", + `ALTER TABLE agent_sync_settings RENAME COLUMN constitution_field_key TO charter_field_key`), + }, + } +} diff --git a/internal/store/migrations/registry.go b/internal/store/migrations/registry.go index a316513..ce24e62 100644 --- a/internal/store/migrations/registry.go +++ b/internal/store/migrations/registry.go @@ -54,5 +54,6 @@ func All() []Definition { migration018(), migration019(), migration020(), + migration021(), } } diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go index bd4bb08..35826e0 100644 --- a/internal/store/sqlite_test.go +++ b/internal/store/sqlite_test.go @@ -760,7 +760,7 @@ func TestAgentSyncSettingsRepo_UpsertOnEmptyTable(t *testing.T) { err = repo.Save(ctx, AgentSyncSettings{ OrgCUID: "org-abc", AgentRecordTypeSlug: "ai-agents", - ConstitutionFieldKey: "constitution", + CharterFieldKey: "charter", SummaryModelConfigCUID: "model-abc", DefaultAgentVMCUID: "agent-vm-abc", }) diff --git a/ui/src/api/AtryumAPI.ts b/ui/src/api/AtryumAPI.ts index 70ad3f0..b72b649 100644 --- a/ui/src/api/AtryumAPI.ts +++ b/ui/src/api/AtryumAPI.ts @@ -359,7 +359,7 @@ export interface Agent { /** True when this agent originated from a ValidMind sync and cannot be deleted manually. */ synced: boolean; /** Governing text used by local LLM-as-judge evaluation. Only editable for non-synced agents. */ - constitution?: string; + charter?: string; } export interface AgentCreateInput { @@ -367,7 +367,7 @@ export interface AgentCreateInput { description?: string; enabled: boolean; agent_ids?: string[]; - constitution?: string; + charter?: string; } export interface AgentUpdateInput { @@ -375,7 +375,7 @@ export interface AgentUpdateInput { description?: string; enabled: boolean; agent_ids?: string[]; - constitution?: string; + charter?: string; } export const agentsApi = { @@ -413,7 +413,7 @@ export const agentsApi = { export interface AgentSyncSettings { org_cuid: string; agent_record_type_slug: string; - constitution_field_key: string; + charter_field_key: string; summary_model_config_cuid: string; summary_atryum_llm_config_id: string; backend_configured?: boolean; diff --git a/ui/src/pages/Agents.tsx b/ui/src/pages/Agents.tsx index b66fa49..25e6cf7 100644 --- a/ui/src/pages/Agents.tsx +++ b/ui/src/pages/Agents.tsx @@ -80,7 +80,7 @@ const CreateAgentModal: React.FC = ({ isOpen, onClose }) const [form, setForm] = useState({ name: '', description: '', - constitution: '', + charter: '', enabled: true, agent_ids: [], }); @@ -89,7 +89,7 @@ const CreateAgentModal: React.FC = ({ isOpen, onClose }) const noOptionsMessage = useCallback(() => null, []); const handleClose = () => { - setForm({ name: '', description: '', constitution: '', enabled: true, agent_ids: [] }); + setForm({ name: '', description: '', charter: '', enabled: true, agent_ids: [] }); setStatusMsg(null); onClose(); }; @@ -141,12 +141,12 @@ const CreateAgentModal: React.FC = ({ isOpen, onClose }) /> - Constitution (optional) + Charter (optional)