Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
11e6f21
feat(sidecar): untrusted-content taint escalates to HIC approval (HGW-1)
SaulBuilds Oct 15, 2026
9d8f717
feat(sidecar): approvals routes per #74, explicit taint clear, ask-no…
SaulBuilds Oct 15, 2026
bb194f0
feat(sidecar,shell): project write policy and git inside the sandbox …
SaulBuilds Oct 15, 2026
2a67b1d
feat(sidecar): project_create tool, HIC-approved (HGW-3)
SaulBuilds Oct 15, 2026
7c96c68
feat(shell): .env.example and .env.sample stay readable in the sandbo…
SaulBuilds Oct 15, 2026
368b87b
feat(sidecar): auto-approve list for sandboxed project commands + e2e…
SaulBuilds Oct 15, 2026
c697171
docs(sidecar): project_create names the approvals routes (HGW-3)
SaulBuilds Oct 15, 2026
1d13191
feat(sidecar,shell): longer turns and failure output the model can ac…
SaulBuilds Oct 15, 2026
b263b18
feat(sidecar): narrow npm scripts, git hooks off, list route (HGW-5, …
SaulBuilds Oct 15, 2026
176c287
feat(shell): revert risky git state a run adds, report it (HGW-2, own…
SaulBuilds Oct 15, 2026
e58cf4e
feat(sidecar): commits name the member and carry a Hermes trailer (HG…
SaulBuilds Oct 15, 2026
00264e5
Merge main into hgw/w1-taint-hic (cargo-audit dependency updates, #85)
SaulBuilds Oct 15, 2026
6632cf2
Merge hgw/w1-taint-hic into hgw/w1-project-write-policy (cargo-audit …
SaulBuilds Oct 15, 2026
b191538
Merge hgw/w1-project-write-policy into hgw/w1-project-create (cargo-a…
SaulBuilds Oct 15, 2026
8a785b7
Merge hgw/w1-project-create into hgw/w1-loop-capability (cargo-audit …
SaulBuilds Oct 15, 2026
937d241
Merge hgw/w1-loop-capability into hgw/w1-auto-approve (cargo-audit de…
SaulBuilds Oct 15, 2026
ce4ebd3
Merge hgw/w1-auto-approve into hgw/w1-git-safety (cargo-audit depende…
SaulBuilds Oct 15, 2026
d312bd5
feat(shell): git cleanup covers every repository in the write roots
SaulBuilds Oct 15, 2026
e605409
Merge main (squashed #79) into hgw/w1-git-safety
SaulBuilds Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions agent-loop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ pub enum ToolOutcome {
/// The tool ran, but this particular output is untrusted whatever the tool's annotation says
/// (e.g. a file read that resolved outside the granted folders). Taints the session.
Untrusted(String),
/// HGW-5: the tool ran, and its host vouches that this particular output carries only content
/// the session may already trust, whatever the tool's annotation says: an auto-approved
/// `shell_run` (a listed read or build command, in the OS sandbox with no network, writing
/// only a project in the member's grants), whose output derives from the project's own files
/// like a folder-grant file read. Never taints. A host that cannot show that never uses it.
Vouched(String),
/// A human (or policy) declined the effect — nothing happened.
Denied(String),
/// The tool failed.
Expand All @@ -187,14 +193,14 @@ pub enum ToolOutcome {
impl ToolOutcome {
fn to_content(&self) -> String {
match self {
ToolOutcome::Ok(s) | ToolOutcome::Untrusted(s) => s.clone(),
ToolOutcome::Ok(s) | ToolOutcome::Untrusted(s) | ToolOutcome::Vouched(s) => s.clone(),
ToolOutcome::Denied(why) => format!("declined: {why}. Nothing was done."),
ToolOutcome::Error(e) => format!("tool error: {e}"),
}
}
fn status(&self) -> &'static str {
match self {
ToolOutcome::Ok(_) | ToolOutcome::Untrusted(_) => "ok",
ToolOutcome::Ok(_) | ToolOutcome::Untrusted(_) | ToolOutcome::Vouched(_) => "ok",
ToolOutcome::Denied(_) => "denied",
ToolOutcome::Error(_) => "error",
}
Expand Down Expand Up @@ -944,6 +950,7 @@ pub fn run_turn_with(
let ingested_untrusted = ran
&& match &outcome {
ToolOutcome::Untrusted(_) => true,
ToolOutcome::Vouched(_) => false,
ToolOutcome::Ok(_) | ToolOutcome::Error(_) => {
spec.is_some_and(|s| s.annotations.output_untrusted())
}
Expand Down
48 changes: 48 additions & 0 deletions agent-loop/tests/taint_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -771,3 +771,51 @@ fn the_loop_records_each_untrusted_tool_as_a_source() {
"both tools that returned untrusted content are sources"
);
}

/// HGW-5: a result its host vouches for (an auto-approved, sandboxed, offline run inside a
/// project) does not taint, even from a tool annotated untrusted; the same tool's ordinary
/// output still does.
#[test]
fn a_vouched_result_does_not_taint_but_the_same_tools_plain_output_does() {
let llm = ScriptLlm::new(vec![
AssistantTurn::tools(vec![call("c1", "web_fetch")]),
AssistantTurn::tools(vec![call("c2", "write_file")]),
AssistantTurn::tools(vec![call("c3", "node_status")]),
AssistantTurn::text("done"),
]);
let host = Arc::new(PerTool {
inner: RecHost::new(true, ToolOutcome::Ok("ok".into())),
by_name: vec![("web_fetch", ToolOutcome::Vouched("project output".into()))],
});
let tools = ToolRegistry::new(catalog()).with_host(HostKind::Core, host.clone());
let sink = Sink::default();
run_turn(
&cfg(),
&llm,
&tools,
&sink,
&StopFlag::default(),
&mut vec![],
"go",
);
assert!(!tools.taint().is_tainted());
assert!(!sink.kinds().contains(&"tainted"));
assert_eq!(hic_of(&sink.tool_call("c2")).0, None, "no HIC downgrade");
// The same untrusted-annotated tool, answered plainly, taints.
let llm = ScriptLlm::new(vec![
AssistantTurn::tools(vec![call("c1", "web_fetch")]),
AssistantTurn::text("done"),
]);
let plain = Arc::new(RecHost::new(true, ToolOutcome::Ok("page".into())));
let tools = ToolRegistry::new(catalog()).with_host(HostKind::Core, plain);
run_turn(
&cfg(),
&llm,
&tools,
&Sink::default(),
&StopFlag::default(),
&mut vec![],
"go",
);
assert!(tools.taint().is_tainted());
}
1 change: 1 addition & 0 deletions agent-mcp-host/tests/modern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ fn text_of(o: &ToolOutcome) -> String {
match o {
ToolOutcome::Ok(s)
| ToolOutcome::Untrusted(s)
| ToolOutcome::Vouched(s)
| ToolOutcome::Denied(s)
| ToolOutcome::Error(s) => s.clone(),
}
Expand Down
1 change: 1 addition & 0 deletions agent-mcp-host/tests/node_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ fn show(o: &ToolOutcome) -> String {
match o {
ToolOutcome::Ok(s) => format!("ok: {s}"),
ToolOutcome::Untrusted(s) => format!("untrusted: {s}"),
ToolOutcome::Vouched(s) => format!("vouched: {s}"),
ToolOutcome::Denied(s) => format!("denied: {s}"),
ToolOutcome::Error(s) => format!("error: {s}"),
}
Expand Down
1 change: 1 addition & 0 deletions agent-mcp-host/tests/stdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fn text_of(o: &ToolOutcome) -> String {
match o {
ToolOutcome::Ok(s)
| ToolOutcome::Untrusted(s)
| ToolOutcome::Vouched(s)
| ToolOutcome::Denied(s)
| ToolOutcome::Error(s) => s.clone(),
}
Expand Down
Loading
Loading