diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 2134a4b..d0051f3 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -608,18 +608,11 @@ func parseAgentIDs(raw string) []string { return out } -const agentRulesToolName = "atryum.rules.get" - // atryumInitializeInstructions is returned in the MCP `initialize` result so -// MCP clients can surface it to the model as system-level guidance. The goal -// is to make the agent aware that approval rules govern tool access and that -// it should consult `atryum.rules.get` before choosing tools. -const atryumInitializeInstructions = "This MCP server (atryum) gates tool calls through approval rules. " + - "Before calling any tool, call the `atryum.rules.get` tool to retrieve the rules that apply to you " + - "and the effective action (`auto_approve`, `human_approval`, or `auto_deny`) for a given server/tool. " + - "Prefer tools that resolve to `auto_approve`. Tools that resolve to `human_approval` will block until a " + - "human responds; tools that resolve to `auto_deny` will be rejected. " + - "Pass the target `server` (or `source`) and `tool` arguments to `atryum.rules.get` to get a disposition." +// MCP clients can surface it to the model as system-level guidance. +const atryumInitializeInstructions = "This MCP server is gated by the Atryum harness. " + + "Atryum may approve, deny, or request human approval for tool calls according to configured rules. " + + "Those rules may change between conversation turns, so treat each tool call as subject to the current Atryum policy." type AgentRule struct { ID string `json:"id"` @@ -640,8 +633,6 @@ type AgentRulesResponse struct { Items []AgentRule `json:"items"` } -var agentRulesToolInputSchema = json.RawMessage(`{"type":"object","properties":{"server":{"type":"string","description":"MCP server/upstream name to evaluate"},"source":{"type":"string","description":"External executor source to evaluate, such as amp"},"tool":{"type":"string","description":"Tool name to evaluate"}},"additionalProperties":false}`) - // ───────────────────────────────────────────────────────────────────────────── type jsonRPCRequest struct { @@ -1130,7 +1121,6 @@ func (h *Handler) handleMCPProxy(w http.ResponseWriter, r *http.Request, server h.writeRPCError(w, req.ID, -32000, err.Error()) return } - tools = appendAtryumTools(tools) _ = h.emitTraceEvent(r.Context(), server, "mcp.tools.list", map[string]any{"tool_count": len(tools), "request_id": requestID}) annotated := h.annotateToolsWithPolicy(r.Context(), server, tools) h.writeRPCResult(w, req.ID, map[string]any{"tools": annotated}) @@ -1143,16 +1133,6 @@ func (h *Handler) handleMCPProxy(w http.ResponseWriter, r *http.Request, server h.writeRPCError(w, req.ID, -32602, "invalid params") return } - if params.Name == agentRulesToolName { - result, err := h.callAgentRulesTool(r.Context(), server, params.Arguments) - if err != nil { - h.writeRPCError(w, req.ID, -32000, err.Error()) - return - } - _ = h.emitTraceEvent(r.Context(), server, "mcp.tools.call", map[string]any{"request_id": requestID, "status": "succeeded", "tool": params.Name}) - h.writeRPCResult(w, req.ID, result) - return - } toolReq := invocation.CreateInvocationRequest{Server: server, Tool: params.Name, Input: params.Arguments} if requestID != "" { toolReq.RequestID = stringPtr(requestID) @@ -1434,10 +1414,6 @@ func (h *Handler) annotateToolsWithPolicy(ctx context.Context, server string, to agentID := auth.AgentIDFromContext(ctx) agentCUID := h.resolveAgentRecordForRules(ctx, agentID) for i, t := range tools { - if t.Name == agentRulesToolName { - out[i] = t - continue - } action, matched := effectiveActionForTool(rules, server, t.Name, agentCUID) desc := t.Description if action != "" { @@ -1475,9 +1451,7 @@ func effectiveActionForTool(rules []store.Rule, server, tool, agentCUID string) } // appendRulesContextToToolResult adds an extra text content block to a denied -// tool call result describing the applicable rules and effective action, so -// the model can learn the policy through feedback instead of having to call -// atryum.rules.get explicitly. +// tool call result describing the applicable rules and effective action. func (h *Handler) appendRulesContextToToolResult(ctx context.Context, result any, server, tool string) any { if h.rulesRepo == nil { return result @@ -1507,61 +1481,6 @@ func (h *Handler) appendRulesContextToToolResult(ctx context.Context, result any return m } -func appendAtryumTools(tools []mcp.Tool) []mcp.Tool { - for _, tool := range tools { - if tool.Name == agentRulesToolName { - return tools - } - } - return append(tools, mcp.Tool{ - Name: agentRulesToolName, - Description: "Return the approval rules and effective action for this agent. Call this before choosing tools so you can prefer auto_approve tools and avoid tools that require human_approval or auto_deny.", - InputSchema: agentRulesToolInputSchema, - }) -} - -func (h *Handler) callAgentRulesTool(ctx context.Context, currentServer string, args map[string]any) (map[string]any, error) { - server := strings.TrimSpace(currentServer) - if server == "" { - server = stringArg(args, "server") - } - if server == "" { - server = stringArg(args, "source") - } - tool := stringArg(args, "tool") - resp, err := h.buildAgentRulesResponse(ctx, auth.AgentIDFromContext(ctx), server, tool) - if err != nil { - return nil, err - } - body, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return nil, err - } - return map[string]any{ - "content": []map[string]any{{ - "type": "text", - "text": string(body), - }}, - "isError": false, - }, nil -} - -func stringArg(args map[string]any, name string) string { - if args == nil { - return "" - } - value, ok := args[name] - if !ok { - return "" - } - switch v := value.(type) { - case string: - return strings.TrimSpace(v) - default: - return strings.TrimSpace(fmt.Sprint(v)) - } -} - func (h *Handler) adminInvocations(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method not allowed") diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go index a022eb6..bad632a 100644 --- a/internal/api/handlers_test.go +++ b/internal/api/handlers_test.go @@ -399,8 +399,8 @@ func TestMCPInitializeNegotiatesProtocolVersion(t *testing.T) { t.Fatalf("expected protocol version 2025-06-18, got %#v", result["protocolVersion"]) } instructions, ok := result["instructions"].(string) - if !ok || !strings.Contains(instructions, "atryum.rules.get") { - t.Fatalf("expected initialize instructions to mention atryum.rules.get, got %#v", result["instructions"]) + if !ok || !strings.Contains(instructions, "gated by the Atryum harness") || !strings.Contains(instructions, "rules may change between conversation turns") { + t.Fatalf("expected initialize instructions to describe Atryum gating and changing rules, got %#v", result["instructions"]) } } @@ -790,8 +790,8 @@ func TestMCPToolsList(t *testing.T) { if !strings.Contains(w.Body.String(), `"demo_tool"`) { t.Fatalf("expected tools list, got %s", w.Body.String()) } - if !strings.Contains(w.Body.String(), `"atryum.rules.get"`) { - t.Fatalf("expected synthetic rules tool, got %s", w.Body.String()) + if strings.Contains(w.Body.String(), `"atryum.rules.get"`) { + t.Fatalf("did not expect synthetic rules tool, got %s", w.Body.String()) } } @@ -880,41 +880,6 @@ 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"}, Enabled: true, Order: 0}, - }} - svc := &stubService{} - h := NewHandler(svc, stubServerService{}, nil, rules, nil, nil, nil, nil, nil, nil) - req := httptest.NewRequest(http.MethodPost, "/mcp/demo", strings.NewReader(`{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"atryum.rules.get","arguments":{"tool":"Read"}}}`)) - w := httptest.NewRecorder() - - h.Routes().ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String()) - } - if svc.invokedReq != nil { - t.Fatalf("rules tool should not create invocation, got %#v", svc.invokedReq) - } - var rpcResp struct { - Result struct { - Content []struct { - Text string `json:"text"` - } `json:"content"` - } `json:"result"` - } - if err := json.Unmarshal(w.Body.Bytes(), &rpcResp); err != nil { - t.Fatal(err) - } - if len(rpcResp.Result.Content) != 1 { - t.Fatalf("expected one content item, got %#v", rpcResp.Result.Content) - } - if !strings.Contains(rpcResp.Result.Content[0].Text, `"read-auto"`) || !strings.Contains(rpcResp.Result.Content[0].Text, `"action": "auto_approve"`) { - t.Fatalf("expected rules payload in tool result, got %s", rpcResp.Result.Content[0].Text) - } -} - func TestAgentRulesListsApplicableRulesAndDisposition(t *testing.T) { rules := &stubRulesRepo{rules: []store.Rule{ {ID: "bash-deny", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Bash"}, Enabled: true, Order: 0}, @@ -1039,9 +1004,8 @@ func TestMCPToolsListAnnotatesEffectiveAction(t *testing.T) { if otherTool.Annotations == nil || otherTool.Annotations.Atryum.EffectiveAction != invocation.RuleActionHumanApproval { t.Fatalf("Other tool annotations: %#v", otherTool.Annotations) } - rulesTool := rpcResp.Result.Tools[byName["atryum.rules.get"]] - if rulesTool.Annotations != nil { - t.Fatalf("synthetic rules tool should not be annotated, got %#v", rulesTool.Annotations) + if _, ok := byName["atryum.rules.get"]; ok { + t.Fatalf("did not expect synthetic rules tool in tools/list") } } diff --git a/website/documentation/1_quickstart.html b/website/documentation/1_quickstart.html index 929383a..af77f59 100644 --- a/website/documentation/1_quickstart.html +++ b/website/documentation/1_quickstart.html @@ -46,6 +46,8 @@

Install & initialize Atryum

Download Atryum and register a server in Atryum for testing.

+ +

Download Atryum

curl -fsSL https://github.com/validmind/atryum/raw/main/install_atryum.sh | bash
@@ -56,6 +58,8 @@

Download Atryum

Set up Atryum test server

+ +
  1. Generate a minimal testing configuration with a simple calculator Model Context Protocol (MCP) server that does not require any external credentials:
    ./atryum setup demo
  2. Start the Atryum service and register the test calculator server in Atryum's local database:
    ./atryum run --init-servers
  3. In your browser, navigate to localhost:8080 to open the Atryum local web user interface.

    Here, you can view servers, manage tool invocation approvals, configure rules, and more.

  4. Within Atryum, click Servers in the left sidebar. Confirm that your test calculator server was successfully registered:
  5. Connect your preferred coding agent to Atryum:
  6. Trigger a test tool call from your agent. For example:
    Use the calculator tools and show me 2*2
  7. Within Atryum, click Invocations in the left sidebar to review tool calls. Confirm that the calculator invocation request is Pending Approval — human approval is required by default.
diff --git a/website/md-drafts/1_quickstart.md b/website/md-drafts/1_quickstart.md index 02192fa..a1524c1 100644 --- a/website/md-drafts/1_quickstart.md +++ b/website/md-drafts/1_quickstart.md @@ -6,6 +6,10 @@ Install and initialize Atryum, then integrate Atryum with ValidMind and your oth Download Atryum and register a server in Atryum for testing. +::: +Atryum is not production ready yet. +::: + ### Download Atryum ```bash @@ -18,6 +22,10 @@ The install script downloads the latest Atryum release and selects the correct b ### Set up Atryum test server +::: +This quickstart will run atryum with a local sqlite. This is great for first use, bad for performance and real work. See [docker-compose.yaml](https://github.com/validmind/atryum/blob/main/docker-compose.yml#L2) for an example running with postgres. +::: + 1. Generate a minimal testing configuration with a simple calculator Model Context Protocol (MCP) server that does not require any external credentials: ```bash