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/integrations/lib/atryum.sh b/integrations/lib/atryum.sh index b0825eb..4bdc7c0 100644 --- a/integrations/lib/atryum.sh +++ b/integrations/lib/atryum.sh @@ -87,7 +87,6 @@ seed_auto_approve_rules() { "action": "auto_approve", "server_patterns": ["*"], "tool_patterns": ["*"], - "agent_id_pattern": "*", "description": "integration test catch-all auto-approve" }' curl -fsS -X POST "$ATRYUM_URL/api/v1/admin/rules" \ diff --git a/internal/api/auth_test.go b/internal/api/auth_test.go index e83bf58..fc20dc8 100644 --- a/internal/api/auth_test.go +++ b/internal/api/auth_test.go @@ -182,8 +182,7 @@ func TestMCPAcceptsValidTokenAndPlumbsAgentID(t *testing.T) { func TestAgentRulesRequiresAuthAndUsesTokenAgentID(t *testing.T) { rig := newAuthTestRig(t) rules := &stubRulesRepo{rules: []store.Rule{ - {ID: "own-rule", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, AgentIDPattern: "agent-007", Enabled: true, Order: 0}, - {ID: "other-rule", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, AgentIDPattern: "other", Enabled: true, Order: 1}, + {ID: "auto-rule", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, Enabled: true, Order: 0}, }} h := NewHandler(&stubService{}, stubServerService{}, nil, rules, nil, nil, nil, nil, nil, nil) h.SetAuthValidator(rig.v) @@ -209,13 +208,10 @@ func TestAgentRulesRequiresAuthAndUsesTokenAgentID(t *testing.T) { t.Fatal(err) } if resp.AgentID != "agent-007" { - t.Fatalf("expected token agent_id to win, got %q", resp.AgentID) + t.Fatalf("expected token agent_id to win over query param, got %q", resp.AgentID) } if resp.Action != invocation.RuleActionAutoApprove { - t.Fatalf("expected own rule action, got %q", resp.Action) - } - if len(resp.Items) != 1 || resp.Items[0].ID != "own-rule" { - t.Fatalf("expected only own rule, got %#v", resp.Items) + t.Fatalf("expected auto_approve action, got %q", resp.Action) } } diff --git a/internal/api/handlers.go b/internal/api/handlers.go index daee08e..63a54b2 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 @@ -272,7 +272,6 @@ type AdminRule struct { Action string `json:"action"` ServerPatterns []string `json:"server_patterns"` ToolPatterns []string `json:"tool_patterns"` - AgentIDPattern string `json:"agent_id_pattern"` ModelConfigCUID string `json:"model_config_cuid,omitempty"` AtryumLLMConfigID string `json:"atryum_llm_config_id,omitempty"` AgentCUIDs []string `json:"agent_cuids"` @@ -287,7 +286,6 @@ type AdminRuleInput struct { Action string `json:"action"` ServerPatterns []string `json:"server_patterns"` ToolPatterns []string `json:"tool_patterns"` - AgentIDPattern string `json:"agent_id_pattern"` ModelConfigCUID string `json:"model_config_cuid,omitempty"` AtryumLLMConfigID string `json:"atryum_llm_config_id,omitempty"` AgentCUIDs []string `json:"agent_cuids"` @@ -348,7 +346,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 +354,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 +383,7 @@ func toAdminAgent(a store.AgentRecord) AdminAgent { AgentIDs: ids, SyncedAt: a.SyncedAt, Enabled: a.Enabled, - Constitution: a.Constitution, + Charter: a.Charter, Synced: a.VMOrganizationCUID != "", } } @@ -422,7 +420,6 @@ type AgentRule struct { Action string `json:"action"` ServerPatterns []string `json:"server_patterns"` ToolPatterns []string `json:"tool_patterns"` - AgentIDPattern string `json:"agent_id_pattern"` Description string `json:"description,omitempty"` Order int `json:"order"` } @@ -768,7 +765,7 @@ func (h *Handler) buildAgentRulesResponse(ctx context.Context, agentID, server, agentCUID := h.resolveAgentRecordForRules(ctx, agentID) for _, rule := range rules { - if !apiRuleMatches(rule, server, tool, agentID, agentCUID, false) { + if !apiRuleMatches(rule, server, tool, agentCUID, false) { continue } resp.Items = append(resp.Items, AgentRule{ @@ -776,12 +773,11 @@ func (h *Handler) buildAgentRulesResponse(ctx context.Context, agentID, server, Action: rule.Action, ServerPatterns: rule.ServerPatterns, ToolPatterns: rule.ToolPatterns, - AgentIDPattern: rule.AgentIDPattern, Description: rule.Description, Order: rule.Order, }) if resp.Action == "" && server != "" && tool != "" && - apiRuleMatches(rule, server, tool, agentID, agentCUID, true) { + apiRuleMatches(rule, server, tool, agentCUID, true) { resp.Action = rule.Action if rule.ID != "" { id := rule.ID @@ -819,7 +815,7 @@ func (h *Handler) resolveAgentRecordForRules(ctx context.Context, agentID string return rec.ID } -func apiRuleMatches(rule store.Rule, server, tool, agentID, agentCUID string, includeServerTool bool) bool { +func apiRuleMatches(rule store.Rule, server, tool, agentCUID string, includeServerTool bool) bool { if !rule.Enabled { return false } @@ -831,9 +827,6 @@ func apiRuleMatches(rule store.Rule, server, tool, agentID, agentCUID string, in return false } } - if !apiMatchAgentIDPattern(rule.AgentIDPattern, agentID) { - return false - } return apiMatchAgentCUIDs(rule.AgentCUIDs, agentCUID) } @@ -1186,9 +1179,6 @@ func apiMatchPatterns(patterns []string, value string) bool { return false } -func apiMatchAgentIDPattern(pattern, agentID string) bool { - return pattern == "" || pattern == "*" || pattern == agentID -} // annotatedTool is the on-the-wire shape used for tools/list when atryum is // able to compute a per-tool policy disposition. It mirrors mcp.Tool but adds @@ -1237,7 +1227,7 @@ func (h *Handler) annotateToolsWithPolicy(ctx context.Context, server string, to out[i] = t continue } - action, matched := effectiveActionForTool(rules, server, t.Name, agentID, agentCUID) + action, matched := effectiveActionForTool(rules, server, t.Name, agentCUID) desc := t.Description if action != "" { prefix := "[atryum policy: " + action + "] " @@ -1261,11 +1251,11 @@ func (h *Handler) annotateToolsWithPolicy(ctx context.Context, server string, to } // effectiveActionForTool returns the action of the first enabled rule that -// matches (server, tool, agentID, agentCUID), mirroring invocation.matchRules priority order. +// matches (server, tool, agentCUID), mirroring invocation.matchRules priority order. // When no rule matches, it returns RuleActionHumanApproval (the default). -func effectiveActionForTool(rules []store.Rule, server, tool, agentID, agentCUID string) (string, string) { +func effectiveActionForTool(rules []store.Rule, server, tool, agentCUID string) (string, string) { for _, r := range rules { - if !apiRuleMatches(r, server, tool, agentID, agentCUID, true) { + if !apiRuleMatches(r, server, tool, agentCUID, true) { continue } return r.Action, r.ID @@ -1473,23 +1463,22 @@ func (h *Handler) adminInvocationDetail(w http.ResponseWriter, r *http.Request) if req.CreateRule.Enabled != nil { enabled = *req.CreateRule.Enabled } - newRule := store.Rule{ - ID: "rule_" + newUUID(), - Action: req.CreateRule.Action, - ServerPatterns: normalizePatternSlice(req.CreateRule.ServerPatterns), - ToolPatterns: normalizePatternSlice(req.CreateRule.ToolPatterns), - AgentIDPattern: defaultPattern(req.CreateRule.AgentIDPattern), - ModelConfigCUID: req.CreateRule.ModelConfigCUID, - AgentCUIDs: normalizePatternSlice(req.CreateRule.AgentCUIDs), - Description: req.CreateRule.Description, - Enabled: enabled, - } - if err := h.rulesRepo.InsertBefore(r.Context(), anchorID, newRule); err != nil { - writeError(w, http.StatusInternalServerError, err.Error()) - return - } + newRule := store.Rule{ + ID: "rule_" + newUUID(), + Action: req.CreateRule.Action, + ServerPatterns: normalizePatternSlice(req.CreateRule.ServerPatterns), + ToolPatterns: normalizePatternSlice(req.CreateRule.ToolPatterns), + ModelConfigCUID: req.CreateRule.ModelConfigCUID, + AgentCUIDs: normalizePatternSlice(req.CreateRule.AgentCUIDs), + Description: req.CreateRule.Description, + Enabled: enabled, + } + if err := h.rulesRepo.InsertBefore(r.Context(), anchorID, newRule); err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return } - if err := h.svc.Approve(r.Context(), id); err != nil { + } + if err := h.svc.Approve(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } @@ -1533,23 +1522,22 @@ func (h *Handler) adminInvocationDetail(w http.ResponseWriter, r *http.Request) if req.CreateRule.Enabled != nil { enabled = *req.CreateRule.Enabled } - newRule := store.Rule{ - ID: "rule_" + newUUID(), - Action: req.CreateRule.Action, - ServerPatterns: normalizePatternSlice(req.CreateRule.ServerPatterns), - ToolPatterns: normalizePatternSlice(req.CreateRule.ToolPatterns), - AgentIDPattern: defaultPattern(req.CreateRule.AgentIDPattern), - ModelConfigCUID: req.CreateRule.ModelConfigCUID, - AgentCUIDs: normalizePatternSlice(req.CreateRule.AgentCUIDs), - Description: req.CreateRule.Description, - Enabled: enabled, - } - if err := h.rulesRepo.InsertBefore(r.Context(), anchorID, newRule); err != nil { - writeError(w, http.StatusInternalServerError, err.Error()) - return - } + newRule := store.Rule{ + ID: "rule_" + newUUID(), + Action: req.CreateRule.Action, + ServerPatterns: normalizePatternSlice(req.CreateRule.ServerPatterns), + ToolPatterns: normalizePatternSlice(req.CreateRule.ToolPatterns), + ModelConfigCUID: req.CreateRule.ModelConfigCUID, + AgentCUIDs: normalizePatternSlice(req.CreateRule.AgentCUIDs), + Description: req.CreateRule.Description, + Enabled: enabled, + } + if err := h.rulesRepo.InsertBefore(r.Context(), anchorID, newRule); err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return } - if err := h.svc.Deny(r.Context(), id, req.Message); err != nil { + } + if err := h.svc.Deny(r.Context(), id, req.Message); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } @@ -1962,7 +1950,6 @@ func (h *Handler) adminRules(w http.ResponseWriter, r *http.Request) { Action: req.Action, ServerPatterns: normalizePatternSlice(req.ServerPatterns), ToolPatterns: normalizePatternSlice(req.ToolPatterns), - AgentIDPattern: defaultPattern(req.AgentIDPattern), ModelConfigCUID: req.ModelConfigCUID, AtryumLLMConfigID: req.AtryumLLMConfigID, AgentCUIDs: normalizePatternSlice(req.AgentCUIDs), @@ -2063,7 +2050,6 @@ func (h *Handler) adminRuleDetail(w http.ResponseWriter, r *http.Request) { existing.Action = req.Action existing.ServerPatterns = normalizePatternSlice(req.ServerPatterns) existing.ToolPatterns = normalizePatternSlice(req.ToolPatterns) - existing.AgentIDPattern = defaultPattern(req.AgentIDPattern) existing.ModelConfigCUID = req.ModelConfigCUID existing.AtryumLLMConfigID = req.AtryumLLMConfigID existing.AgentCUIDs = normalizePatternSlice(req.AgentCUIDs) @@ -2125,7 +2111,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 +2124,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 +2332,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 +2366,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 +2393,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, @@ -2553,7 +2539,6 @@ func toAdminRule(r store.Rule) AdminRule { Action: r.Action, ServerPatterns: sp, ToolPatterns: tp, - AgentIDPattern: r.AgentIDPattern, ModelConfigCUID: r.ModelConfigCUID, AtryumLLMConfigID: r.AtryumLLMConfigID, AgentCUIDs: ac, @@ -2617,7 +2602,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 +2699,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 @@ -2780,13 +2765,6 @@ func validateRuleInput(req AdminRuleInput) error { return nil } -func defaultPattern(p string) string { - if strings.TrimSpace(p) == "" { - return "*" - } - return p -} - func newUUID() string { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go index 0b503a5..8db774f 100644 --- a/internal/api/handlers_test.go +++ b/internal/api/handlers_test.go @@ -677,7 +677,7 @@ func TestMCPAgentIDQueryHintIgnoredWhenAuthConfigured(t *testing.T) { func TestMCPRulesToolReturnsApplicableRulesWithoutInvocation(t *testing.T) { rules := &stubRulesRepo{rules: []store.Rule{ - {ID: "read-auto", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Read"}, AgentIDPattern: "*", Enabled: true, Order: 0}, + {ID: "read-auto", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Read"}, Enabled: true, Order: 0}, }} svc := &stubService{} h := NewHandler(svc, stubServerService{}, nil, rules, nil, nil, nil, nil, nil, nil) @@ -712,10 +712,10 @@ func TestMCPRulesToolReturnsApplicableRulesWithoutInvocation(t *testing.T) { func TestAgentRulesListsApplicableRulesAndDisposition(t *testing.T) { rules := &stubRulesRepo{rules: []store.Rule{ - {ID: "other-agent", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, AgentIDPattern: "agent-other", Enabled: true, Order: 0}, - {ID: "read-auto", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, AgentIDPattern: "agent-007", Description: "Read is safe", Enabled: true, Order: 1}, - {ID: "fallback-human", Action: invocation.RuleActionHumanApproval, ServerPatterns: []string{"*"}, ToolPatterns: []string{"*"}, AgentIDPattern: "*", Enabled: true, Order: 2}, - {ID: "disabled", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Bash"}, AgentIDPattern: "agent-007", Enabled: false, Order: 3}, + {ID: "bash-deny", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Bash"}, Enabled: true, Order: 0}, + {ID: "read-auto", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, Description: "Read is safe", Enabled: true, Order: 1}, + {ID: "fallback-human", Action: invocation.RuleActionHumanApproval, ServerPatterns: []string{"*"}, ToolPatterns: []string{"*"}, Enabled: true, Order: 2}, + {ID: "disabled", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, Enabled: false, Order: 3}, }} h := NewHandler(&stubService{}, stubServerService{}, nil, rules, nil, nil, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/v1/agent/rules?agent_id=agent-007&source=amp&tool=Read", nil) @@ -739,10 +739,10 @@ func TestAgentRulesListsApplicableRulesAndDisposition(t *testing.T) { if resp.MatchedRuleID == nil || *resp.MatchedRuleID != "read-auto" { t.Fatalf("expected matched rule read-auto, got %#v", resp.MatchedRuleID) } - if len(resp.Items) != 2 { - t.Fatalf("expected two applicable rules, got %#v", resp.Items) + if len(resp.Items) != 3 { + t.Fatalf("expected three applicable rules, got %#v", resp.Items) } - if resp.Items[0].ID != "read-auto" || resp.Items[1].ID != "fallback-human" { + if resp.Items[0].ID != "bash-deny" || resp.Items[1].ID != "read-auto" || resp.Items[2].ID != "fallback-human" { t.Fatalf("unexpected applicable rules order: %#v", resp.Items) } } @@ -750,9 +750,9 @@ func TestAgentRulesListsApplicableRulesAndDisposition(t *testing.T) { func TestAgentRulesUsesDefaultAgentRecordForAgentScopedRules(t *testing.T) { defaultAgent := store.AgentRecord{ID: "agent-default", VMCUID: "vm-default", AgentIDs: "[]"} rules := &stubRulesRepo{rules: []store.Rule{ - {ID: "other-agent", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, AgentIDPattern: "*", AgentCUIDs: []string{"agent-other"}, Enabled: true, Order: 0}, - {ID: "default-agent", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, AgentIDPattern: "*", AgentCUIDs: []string{defaultAgent.ID}, Enabled: true, Order: 1}, - {ID: "fallback-human", Action: invocation.RuleActionHumanApproval, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, AgentIDPattern: "*", Enabled: true, Order: 2}, + {ID: "other-agent", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, AgentCUIDs: []string{"agent-other"}, Enabled: true, Order: 0}, + {ID: "default-agent", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, AgentCUIDs: []string{defaultAgent.ID}, Enabled: true, Order: 1}, + {ID: "fallback-human", Action: invocation.RuleActionHumanApproval, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Read"}, Enabled: true, Order: 2}, }} agents := &stubAgentsRepo{byVMCUID: map[string]store.AgentRecord{defaultAgent.VMCUID: defaultAgent}} settings := &stubAgentSyncSettingsRepo{settings: store.AgentSyncSettings{DefaultAgentVMCUID: defaultAgent.VMCUID}} @@ -785,8 +785,8 @@ func TestAgentRulesUsesDefaultAgentRecordForAgentScopedRules(t *testing.T) { func TestMCPToolsListAnnotatesEffectiveAction(t *testing.T) { rules := &stubRulesRepo{rules: []store.Rule{ - {ID: "read-auto", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Read"}, AgentIDPattern: "*", Enabled: true, Order: 0}, - {ID: "bash-deny", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Bash"}, AgentIDPattern: "*", Enabled: true, Order: 1}, + {ID: "read-auto", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Read"}, Enabled: true, Order: 0}, + {ID: "bash-deny", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Bash"}, Enabled: true, Order: 1}, }} svc := &stubService{tools: []mcp.Tool{{Name: "Read", Description: "read a file"}, {Name: "Bash", Description: "run a shell command"}, {Name: "Other"}}} h := NewHandler(svc, stubServerService{}, nil, rules, nil, nil, nil, nil, nil, nil) @@ -843,9 +843,9 @@ func TestMCPToolsListAnnotatesEffectiveAction(t *testing.T) { func TestMCPToolsListAnnotationsUseDefaultAgentScopedRules(t *testing.T) { defaultAgent := store.AgentRecord{ID: "agent-default", VMCUID: "vm-default", AgentIDs: "[]"} rules := &stubRulesRepo{rules: []store.Rule{ - {ID: "bash-deny-other", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Bash"}, AgentIDPattern: "*", AgentCUIDs: []string{"agent-other"}, Enabled: true, Order: 0}, - {ID: "bash-auto-default", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Bash"}, AgentIDPattern: "*", AgentCUIDs: []string{defaultAgent.ID}, Enabled: true, Order: 1}, - {ID: "bash-human-fallback", Action: invocation.RuleActionHumanApproval, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Bash"}, AgentIDPattern: "*", Enabled: true, Order: 2}, + {ID: "bash-deny-other", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Bash"}, AgentCUIDs: []string{"agent-other"}, Enabled: true, Order: 0}, + {ID: "bash-auto-default", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Bash"}, AgentCUIDs: []string{defaultAgent.ID}, Enabled: true, Order: 1}, + {ID: "bash-human-fallback", Action: invocation.RuleActionHumanApproval, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Bash"}, Enabled: true, Order: 2}, }} svc := &stubService{tools: []mcp.Tool{{Name: "Bash", Description: "run a shell command"}}} agents := &stubAgentsRepo{byVMCUID: map[string]store.AgentRecord{defaultAgent.VMCUID: defaultAgent}} @@ -897,7 +897,7 @@ func TestMCPToolsListAnnotationsUseDefaultAgentScopedRules(t *testing.T) { func TestMCPToolsCallDenialIncludesRulesContext(t *testing.T) { now := time.Now().UTC() rules := &stubRulesRepo{rules: []store.Rule{ - {ID: "bash-deny", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Bash"}, AgentIDPattern: "*", Enabled: true, Order: 0}, + {ID: "bash-deny", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Bash"}, Enabled: true, Order: 0}, }} svc := &stubService{invoke: invocation.InvocationResponse{ InvocationID: "inv_denied", ServerName: "demo", ToolName: "Bash", 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/agent_id_test.go b/internal/invocation/agent_id_test.go index 4fe195d..7709e0b 100644 --- a/internal/invocation/agent_id_test.go +++ b/internal/invocation/agent_id_test.go @@ -15,8 +15,6 @@ import ( "atryum/internal/invocation/policy" "atryum/internal/mcp" "atryum/internal/store" - - _ "modernc.org/sqlite" ) // stubRulesStore returns a fixed rule list so the test can pin the user @@ -45,13 +43,10 @@ func TestInvokeUsesAuthenticatedAgentIDForRulesAndEvents(t *testing.T) { t.Fatal(err) } - // Rule that auto-approves only the authenticated agent_id; if the - // service incorrectly matched on request_id (legacy behavior) the rule - // would NOT match and the call would fall back to manual approval. rules := stubRulesStore{rules: []invocation.ApprovalRule{{ - ID: "rule_agent_007", Action: invocation.RuleActionAutoApprove, + ID: "rule_auto_approve", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"*"}, ToolPatterns: []string{"*"}, - AgentIDPattern: "agent-007", Enabled: true, + Enabled: true, }}} svc := invocation.NewService( @@ -68,7 +63,7 @@ func TestInvokeUsesAuthenticatedAgentIDForRulesAndEvents(t *testing.T) { t.Fatalf("Invoke: %v", err) } if resp.Status != invocation.StatusSucceeded { - t.Fatalf("expected succeeded (rule auto_approve matched on agent_id), got %s", resp.Status) + t.Fatalf("expected succeeded (rule auto_approve), got %s", resp.Status) } if resp.Approval == nil || resp.Approval.Status != "auto_approved" { t.Fatalf("expected auto_approved approval, got %#v", resp.Approval) @@ -158,44 +153,6 @@ func TestInvokeWithoutAgentIDPersistsClientInfoFromRequest(t *testing.T) { } } -func TestInvokeWithoutAuthFallsBackToRequestIDForRules(t *testing.T) { - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": 1, "result": map[string]any{"content": []map[string]any{{"type": "text", "text": "ok"}}}}) - })) - defer upstream.Close() - - db := newSQLiteTestDB(t) - cfg := config.Config{ - Defaults: config.DefaultsConfig{RequestTimeoutSeconds: 5}, - Upstreams: []config.UpstreamConfig{{Name: "demo", Mode: "http", BaseURL: upstream.URL, Enabled: true, TimeoutSeconds: 5}}, - } - resolver := mcp.NewResolver(store.NewServerRepo(db), cfg) - if err := resolver.BootstrapIfEmpty(context.Background()); err != nil { - t.Fatal(err) - } - - rules := stubRulesStore{rules: []invocation.ApprovalRule{{ - ID: "rule_legacy", Action: invocation.RuleActionAutoApprove, - ServerPatterns: []string{"*"}, ToolPatterns: []string{"*"}, - AgentIDPattern: "legacy-request-id", Enabled: true, - }}} - svc := invocation.NewService( - store.NewInvocationRepo(db), store.NewEventRepo(db), resolver, - mcp.NewHTTPClient(), policy.AlwaysDenyProvider{}, - 5*time.Second, rules, nil, nil, nil, - ) - - rid := "legacy-request-id" - resp, err := svc.Invoke(context.Background(), invocation.CreateInvocationRequest{ - Server: "demo", Tool: "demo_tool", Input: map[string]any{"x": 1}, RequestID: &rid, - }) - if err != nil { - t.Fatalf("Invoke: %v", err) - } - if resp.Status != invocation.StatusSucceeded { - t.Fatalf("expected succeeded via request_id fallback, got %s", resp.Status) - } -} // External (non-MCP) callers like the amp-plugin example don't run behind // the auth middleware today, so there is no identity in context. They can 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/rules.go b/internal/invocation/rules.go index a9b7a4d..b5c8fe7 100644 --- a/internal/invocation/rules.go +++ b/internal/invocation/rules.go @@ -21,7 +21,6 @@ type ApprovalRule struct { Action string // one of the RuleAction* constants ServerPatterns []string // empty slice = match any server ToolPatterns []string // empty slice = match any tool - AgentIDPattern string // "*" or "" = match any agent ModelConfigCUID string // VM agent model config to use for ai_evaluation rules AtryumLLMConfigID string // local LLM config ID for native ai_evaluation (alternative to ModelConfigCUID) AgentCUIDs []string // Atryum agent CUIDs this rule targets; empty = all @@ -39,10 +38,8 @@ type rulesStore interface { // and stop at the first rule that produces a final verdict; an empty slice means no // rule matches and the caller should fall back to the global policy. // -// The agentID is the authenticated agent identity (empty when auth is disabled — -// callers fall back to request_id for parity with pre-auth behavior). // The agentCUID is the Atryum-local agent record CUID used by ai_evaluation rules. -func matchRules(rules []ApprovalRule, server, tool, agentID, agentCUID string) []ApprovalRule { +func matchRules(rules []ApprovalRule, server, tool, agentCUID string) []ApprovalRule { var matched []ApprovalRule for _, r := range rules { if !r.Enabled { @@ -54,9 +51,6 @@ func matchRules(rules []ApprovalRule, server, tool, agentID, agentCUID string) [ if !matchPatterns(r.ToolPatterns, tool) { continue } - if !matchAgentIDPattern(r.AgentIDPattern, agentID) { - continue - } if !matchAgentCUIDs(r.AgentCUIDs, agentCUID) { continue } @@ -92,8 +86,3 @@ func matchPatterns(patterns []string, value string) bool { return false } -// matchAgentIDPattern returns true when the rule's agent_id pattern matches the -// authenticated agent identity. An empty or "*" pattern matches any agent. -func matchAgentIDPattern(pattern, agentID string) bool { - return pattern == "" || pattern == "*" || pattern == agentID -} diff --git a/internal/invocation/service.go b/internal/invocation/service.go index b7ec0ff..c2d85c5 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 @@ -277,11 +277,7 @@ func (s *Service) Invoke(ctx context.Context, req CreateInvocationRequest) (Invo ruleMatched := false if s.rules != nil { if approvalRules, err := s.rules.ListApprovalRules(ctx); err == nil { - user := agentID - if user == "" && req.RequestID != nil { - user = *req.RequestID - } - for _, rule := range matchRules(approvalRules, upstream.Name, req.Tool, user, agentRec.ID) { + for _, rule := range matchRules(approvalRules, upstream.Name, req.Tool, agentRec.ID) { r := rule ruleMatched = true if r.ID != "" { @@ -406,12 +402,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 +424,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 +452,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 +480,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, @@ -833,11 +829,7 @@ func (s *Service) Submit(ctx context.Context, req ExternalSubmitRequest) (Invoca var resolvedAIConfidence *float64 if s.rules != nil { if approvalRules, err := s.rules.ListApprovalRules(ctx); err == nil { - user := agentID - if user == "" && req.RequestID != nil { - user = *req.RequestID - } - for _, rule := range matchRules(approvalRules, source, req.Tool, user, agentRec.ID) { + for _, rule := range matchRules(approvalRules, source, req.Tool, agentRec.ID) { r := rule if r.Action == RuleActionAIEvaluation { d, conf := s.runAIEvaluation(ctx, &r, source, req.Tool, req.Input, agentID, agentRec) 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..68a09dc 100644 --- a/internal/store/db_test.go +++ b/internal/store/db_test.go @@ -46,7 +46,7 @@ func TestResolveDBTarget_SelectsSQLiteForSQLiteFileAndBarePaths(t *testing.T) { } func TestMigrationRegistryPreservesExistingVersionsAndNames(t *testing.T) { - if len(migrations) != 20 { + if len(migrations) != 22 { t.Fatalf("migration count = %d", len(migrations)) } want := []struct { @@ -73,6 +73,8 @@ 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"}, + {22, "022_drop_agent_id_pattern"}, } for i, w := range want { if migrations[i].Version != w.version || migrations[i].Name != w.name { @@ -83,10 +85,10 @@ func TestMigrationRegistryPreservesExistingVersionsAndNames(t *testing.T) { func TestGetPendingMigrationsUsesRegistryOrder(t *testing.T) { pending := getPendingMigrations(map[int]bool{1: true}) - if len(pending) != 19 { + if len(pending) != 21 { t.Fatalf("pending count = %d", len(pending)) } - wantVersions := []int{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} + wantVersions := []int{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22} for i, want := range wantVersions { if pending[i].Version != want { t.Fatalf("pending[%d].Version = %d, want %d", i, pending[i].Version, want) 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/022_drop_agent_id_pattern.go b/internal/store/migrations/022_drop_agent_id_pattern.go new file mode 100644 index 0000000..f091dfb --- /dev/null +++ b/internal/store/migrations/022_drop_agent_id_pattern.go @@ -0,0 +1,12 @@ +package migrations + +func migration022() Definition { + return Definition{ + Version: 22, + Name: "022_drop_agent_id_pattern", + Steps: []Step{ + Raw("drop approval_rules.agent_id_pattern column", + `ALTER TABLE approval_rules DROP COLUMN agent_id_pattern`), + }, + } +} diff --git a/internal/store/migrations/registry.go b/internal/store/migrations/registry.go index a316513..db4d400 100644 --- a/internal/store/migrations/registry.go +++ b/internal/store/migrations/registry.go @@ -54,5 +54,7 @@ func All() []Definition { migration018(), migration019(), migration020(), + migration021(), + migration022(), } } diff --git a/internal/store/rules.go b/internal/store/rules.go index f186146..ebefce8 100644 --- a/internal/store/rules.go +++ b/internal/store/rules.go @@ -15,7 +15,6 @@ import ( // Rule is the store-level representation of an approval rule. // ServerPatterns and ToolPatterns are serialized as JSON arrays in the // server_pattern / tool_pattern TEXT columns; an empty slice means "match all". -// AgentIDPattern is the literal authenticated agent_id (or "*"/"" for any). // ModelConfigCUID references the VM agent model configuration for ai_evaluation rules. // AtryumLLMConfigID references a locally-configured LLM for ai_evaluation rules // (alternative to ModelConfigCUID; exactly one should be set for ai_evaluation rules). @@ -26,7 +25,6 @@ type Rule struct { Action string ServerPatterns []string ToolPatterns []string - AgentIDPattern string ModelConfigCUID string AtryumLLMConfigID string AgentCUIDs []string @@ -52,7 +50,7 @@ func NewRulesRepoWithDialect(db *sql.DB, dialect Dialect) *RulesRepo { } var ruleColumns = []string{ - "id", "action", "server_pattern", "tool_pattern", "agent_id_pattern", + "id", "action", "server_pattern", "tool_pattern", "model_config_cuid", "atryum_llm_config_id", "agent_cuids", "description", "enabled", "rule_order", "created_at", "updated_at", } @@ -66,7 +64,7 @@ func (r *RulesRepo) Create(ctx context.Context, rule Rule) error { query, args, err := r.sb.Insert("approval_rules"). Columns(ruleColumns...). Values( - rule.ID, rule.Action, serverJSON, toolJSON, rule.AgentIDPattern, + rule.ID, rule.Action, serverJSON, toolJSON, emptyToNil(rule.ModelConfigCUID), rule.AtryumLLMConfigID, agentCUIDsJSON, emptyToNil(rule.Description), boolToInt(rule.Enabled), rule.Order, now, now, ).ToSql() @@ -137,7 +135,6 @@ func (r *RulesRepo) Update(ctx context.Context, rule Rule) error { Set("action", rule.Action). Set("server_pattern", serverJSON). Set("tool_pattern", toolJSON). - Set("agent_id_pattern", rule.AgentIDPattern). Set("model_config_cuid", emptyToNil(rule.ModelConfigCUID)). Set("atryum_llm_config_id", rule.AtryumLLMConfigID). Set("agent_cuids", agentCUIDsJSON). @@ -281,7 +278,7 @@ func scanRule(scanner interface{ Scan(dest ...any) error }) (Rule, error) { var modelConfigCUID, atryumLLMConfigID, description sql.NullString var enabled int if err := scanner.Scan( - &rule.ID, &rule.Action, &serverJSON, &toolJSON, &rule.AgentIDPattern, + &rule.ID, &rule.Action, &serverJSON, &toolJSON, &modelConfigCUID, &atryumLLMConfigID, &agentCUIDsJSON, &description, &enabled, &rule.Order, &rule.CreatedAt, &rule.UpdatedAt, ); err != nil { @@ -329,7 +326,6 @@ func (r *RulesRepo) ListApprovalRules(ctx context.Context) ([]invocation.Approva Action: rule.Action, ServerPatterns: rule.ServerPatterns, ToolPatterns: rule.ToolPatterns, - AgentIDPattern: rule.AgentIDPattern, ModelConfigCUID: rule.ModelConfigCUID, AtryumLLMConfigID: rule.AtryumLLMConfigID, AgentCUIDs: rule.AgentCUIDs, @@ -385,7 +381,7 @@ func (r *RulesRepo) InsertBefore(ctx context.Context, anchorID string, rule Rule insQ, insArgs, err := r.sb.Insert("approval_rules"). Columns(ruleColumns...). Values( - rule.ID, rule.Action, serverJSON, toolJSON, rule.AgentIDPattern, + rule.ID, rule.Action, serverJSON, toolJSON, emptyToNil(rule.ModelConfigCUID), rule.AtryumLLMConfigID, agentCUIDsJSON, emptyToNil(rule.Description), boolToInt(rule.Enabled), rule.Order, now, now, ).ToSql() diff --git a/internal/store/rules_test.go b/internal/store/rules_test.go index d9eba01..593ae25 100644 --- a/internal/store/rules_test.go +++ b/internal/store/rules_test.go @@ -26,7 +26,6 @@ func TestRulesRepo_CreateVMPathAIEvaluationRule(t *testing.T) { Action: "ai_evaluation", ServerPatterns: []string{}, ToolPatterns: []string{}, - AgentIDPattern: "*", ModelConfigCUID: "model-config-cuid-123", // AtryumLLMConfigID intentionally left empty: VM backend path. Enabled: true, diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go index bd4bb08..b60ed89 100644 --- a/internal/store/sqlite_test.go +++ b/internal/store/sqlite_test.go @@ -41,8 +41,8 @@ func TestInitDB_FreshDatabase(t *testing.T) { if err := db.QueryRow(`SELECT COUNT(*) FROM schema_migrations`).Scan(&count); err != nil { t.Fatalf("count migrations: %v", err) } - if count != 20 { - t.Fatalf("expected 20 migrations, got %d", count) + if count != 22 { + t.Fatalf("expected 22 migrations, got %d", count) } // Verify all tables exist @@ -70,8 +70,8 @@ func TestInitDB_Idempotent(t *testing.T) { if err := db.QueryRow(`SELECT COUNT(*) FROM schema_migrations`).Scan(&count); err != nil { t.Fatalf("count migrations: %v", err) } - if count != 20 { - t.Fatalf("expected 20 migrations after double init, got %d", count) + if count != 22 { + t.Fatalf("expected 22 migrations after double init, got %d", count) } } @@ -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..ae2364e 100644 --- a/ui/src/api/AtryumAPI.ts +++ b/ui/src/api/AtryumAPI.ts @@ -287,7 +287,6 @@ export interface Rule { action: RuleAction; server_patterns: string[]; tool_patterns: string[]; - user_pattern: string; /** Agent CUIDs this rule applies to; empty means all agents. */ agent_cuids?: string[]; description?: string; @@ -303,7 +302,6 @@ export interface RuleInput { action: RuleAction; server_patterns: string[]; tool_patterns: string[]; - user_pattern: string; agent_cuids?: string[]; description?: string; model_config_cuid?: string; @@ -359,7 +357,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 +365,7 @@ export interface AgentCreateInput { description?: string; enabled: boolean; agent_ids?: string[]; - constitution?: string; + charter?: string; } export interface AgentUpdateInput { @@ -375,7 +373,7 @@ export interface AgentUpdateInput { description?: string; enabled: boolean; agent_ids?: string[]; - constitution?: string; + charter?: string; } export const agentsApi = { @@ -413,7 +411,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)