Skip to main content

mcp_governance/
mcp_governance.rs

1use agentledger::{AgentContext, InMemoryMCPToolServer, MCPToolAdapter, Runtime, SandboxExecutor, SandboxPolicy, SandboxResult, State, Value};
2
3fn state(items: &[(&str, Value)]) -> State {
4    items
5        .iter()
6        .map(|(key, value)| ((*key).to_string(), value.clone()))
7        .collect()
8}
9
10fn create_pr(_name: &str, args: State) -> agentledger::Result<Value> {
11    Ok(Value::Object(state(&[
12        ("external_id", "PR-1".into()),
13        ("title", args["title"].clone()),
14    ])))
15}
16
17struct DemoSandboxExecutor;
18
19impl SandboxExecutor for DemoSandboxExecutor {
20    fn run_tool(&self, args: State, _policy: &SandboxPolicy) -> SandboxResult {
21        SandboxResult {
22            ok: true,
23            output: create_pr("mcp.github.create_pr", args).unwrap_or(Value::Null),
24            error: None,
25            metadata: state(&[
26                ("executor", "demo-local".into()),
27                ("isolation_level", "none".into()),
28            ]),
29        }
30    }
31}
32
33fn main() -> agentledger::Result<()> {
34    let mut runtime = Runtime::new();
35    runtime.set_sandbox(Box::new(DemoSandboxExecutor));
36    let mut server = InMemoryMCPToolServer::new();
37    server.add_tool(
38        state(&[
39            ("name", "mcp.github.create_pr".into()),
40            (
41                "inputSchema",
42                Value::Object(state(&[
43                    ("type", "object".into()),
44                    (
45                        "required",
46                        Value::Array(vec![Value::String("title".to_string())]),
47                    ),
48                    (
49                        "properties",
50                        Value::Object(state(&[(
51                            "title",
52                            Value::Object(state(&[
53                                ("type", "string".into()),
54                                ("minLength", 1_i64.into()),
55                            ])),
56                        )])),
57                    ),
58                ])),
59            ),
60            (
61                "annotations",
62                Value::Object(state(&[
63                    ("side_effect", "external_write".into()),
64                    ("risk_level", "high".into()),
65                    ("idempotency_required", true.into()),
66                    ("approval_required", true.into()),
67                    ("sandbox_required", true.into()),
68                    (
69                        "sandbox_policy",
70                        Value::Object(state(&[
71                            ("network", "deny".into()),
72                            ("filesystem", "read-only".into()),
73                        ])),
74                    ),
75                ])),
76            ),
77        ]),
78        create_pr,
79    );
80    let adapter = MCPToolAdapter { client_call: create_pr };
81    for descriptor in server.list_tools() {
82        runtime.register_tool(adapter.tool_spec_from_descriptor(&descriptor));
83    }
84
85    let (run_id, _) = runtime.create_run(State::new());
86    let first_claim = runtime.store.claim_step("worker-before-approval", &run_id, 60.0)?;
87    let first_ctx = AgentContext {
88        run_id: run_id.clone(),
89        session_id: first_claim.session_id.clone(),
90        step_id: first_claim.step_id.clone(),
91        agent_role: "MCPAgent".to_string(),
92        lease_token: first_claim.lease_token.clone(),
93        attempt: first_claim.attempt,
94        state_version: first_claim.state_version,
95        pending_patch: State::new(),
96    };
97    let first = runtime.call_tool(
98        &first_ctx,
99        "mcp.github.create_pr",
100        state(&[
101            ("title", "Update runtime docs".into()),
102            ("_logical_operation", "docs-pr".into()),
103        ]),
104    );
105    let approval_id = first
106        .unwrap_err()
107        .0
108        .trim_start_matches("approval required:")
109        .to_string();
110    runtime.store.mark_waiting_human(
111        &run_id,
112        &first_ctx.step_id,
113        "approval required for tool mcp.github.create_pr",
114        &approval_id,
115    );
116    runtime
117        .store
118        .approve_request(&approval_id, "maintainer", "demo approval")?;
119
120    let second_claim = runtime.store.claim_step("worker-after-approval", &run_id, 60.0)?;
121    let mut second_ctx = AgentContext {
122        run_id: run_id.clone(),
123        session_id: second_claim.session_id.clone(),
124        step_id: second_claim.step_id.clone(),
125        agent_role: "MCPAgent".to_string(),
126        lease_token: second_claim.lease_token.clone(),
127        attempt: second_claim.attempt,
128        state_version: second_claim.state_version,
129        pending_patch: State::new(),
130    };
131    let result = runtime.call_tool(
132        &second_ctx,
133        "mcp.github.create_pr",
134        state(&[
135            ("title", "Update runtime docs".into()),
136            ("_logical_operation", "docs-pr".into()),
137        ]),
138    )?;
139    second_ctx.write_state("pull_request", result);
140    runtime.store.commit_state_patch(
141        &run_id,
142        &second_ctx.step_id,
143        &second_ctx.lease_token,
144        second_ctx.state_version,
145        second_ctx.pending_patch,
146    )?;
147
148    let approvals = runtime.store.approval_requests(&run_id);
149    println!(
150        "{{\n  \"run_id\": \"{}\",\n  \"first_attempt_waited_for_approval\": true,\n  \"second_attempt_ok\": true,\n  \"approval_count\": {},\n  \"approval_status\": \"{}\",\n  \"external_action_count\": 1,\n  \"final_state_has_pull_request\": {}\n}}",
151        run_id,
152        approvals.len(),
153        approvals.first().map(|row| row.status.as_str()).unwrap_or(""),
154        runtime
155            .store
156            .final_state(&run_id)?
157            .contains_key("pull_request")
158    );
159    Ok(())
160}