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 @@
Download Atryum and register a server in Atryum for testing.
+ +curl -fsSL https://github.com/validmind/atryum/raw/main/install_atryum.sh | bash./atryum setup demo./atryum run --init-serverslocalhost:8080 to open the Atryum local web user interface.Here, you can view servers, manage tool invocation approvals, configure rules, and more.
ready.localhost:8080/mcp/calc.http://localhost:8080/mcp/calcUse the calculator tools and show me 2*2Pending Approval — human approval is required by default.Succeeded and that the approval was decided by a human.