Skip to main content

speccoder_runtime/substrate/
provenance.rs

1//! Where content came from.
2//!
3//! agentplane's gates — what may go to which model, which arguments may
4//! drive a mutating tool — decide on labels. So every value speccoder hands
5//! the runtime says where it came from: speccoder's own instructions and the
6//! person's committed spec and decisions are trusted; a check's output, the
7//! worktree's content, an MCP server's words and the model's answers are not.
8//!
9//! Labels are given where content enters — a skill input is assembled from
10//! labelled parts — and read back with `label_at`, never inferred from what
11//! the content looks like: agentplane journals the field labels a run was
12//! admitted with (0.50), live and in replay.
13
14use agentplane::core::{Label, SourceId, Tainted};
15use serde_json::Value;
16
17/// Check output quoted in a retry digest: the verifier's facts.
18pub const VERIFIER: &str = "speccoder://verifier";
19
20/// Worktree content speccoder inlines into a prompt (a patch, files, a manifest).
21pub const WORKTREE: &str = "speccoder://worktree";
22
23/// A GitHub issue's title and body, fetched for a bug (`--issue N`):
24/// anyone who can open an issue wrote them — outside content, not the
25/// worktree's.
26pub const GITHUB_ISSUE: &str = "speccoder://github-issue";
27
28/// A person's answer to a question: their input, not speccoder's;
29/// who they were is on the decision's record, not in the label.
30pub const PERSON: &str = "speccoder://person";
31
32/// The source of a tool's results and of an MCP tool's description.
33#[must_use]
34pub fn tool(server: &str, name: &str) -> SourceId {
35    agentplane::tools::ToolId::new(server, name).source()
36}
37
38/// An untrusted label from `source`.
39#[must_use]
40pub fn from(source: &str) -> Label {
41    Label::untrusted(SourceId::new(source))
42}
43
44/// A prompt whose `/system` is speccoder's constant instruction (trusted)
45/// and whose every other part carries `label`.
46#[must_use]
47pub fn labelled_prompt(prompt: Value, label: &Label) -> Tainted<Value> {
48    let Value::Object(parts) = prompt else {
49        return Tainted::with_label(prompt, label.clone());
50    };
51    Tainted::object(parts.into_iter().map(|(k, v)| {
52        let part = if k == "system" {
53            Tainted::trusted(v)
54        } else {
55            Tainted::with_label(v, label.clone())
56        };
57        (k, part)
58    }))
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use serde_json::json;
65
66    #[test]
67    fn the_instruction_stays_trusted_and_the_rest_carries_its_sources() {
68        let p = labelled_prompt(
69            json!({ "system": "be careful", "messages": [{ "role": "user", "content": "x" }] }),
70            &from(VERIFIER),
71        );
72        assert!(!p.label_at("/system").is_some_and(Label::is_untrusted));
73        assert!(p.label_at("/messages").is_some_and(Label::is_untrusted));
74        assert!(p.label().is_untrusted());
75    }
76}