mirror of
https://github.com/bdbrown4/peltier.io.git
synced 2026-11-07 10:58:28 +00:00
Merge pull request #6 from bdbrown4/claude/test-wsk42i
This commit is contained in:
@@ -57,6 +57,7 @@ just gates <target> # corpus pin + upstream tests + golden replay
|
|||||||
just pin-check <target> # verify corpus MANIFEST.sha256
|
just pin-check <target> # verify corpus MANIFEST.sha256
|
||||||
just calibrate <cmd> <out> # automated A/A + regression-injection calibration
|
just calibrate <cmd> <out> # automated A/A + regression-injection calibration
|
||||||
just verdict <t> <bin> ... # gates + bench vs pristine-rebuilt baseline + ledger row
|
just verdict <t> <bin> ... # gates + bench vs pristine-rebuilt baseline + ledger row
|
||||||
|
cargo run -p harnessd # agent IPC daemon: six-tool JSON surface (SPEC §3.5)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Definition of done — one optimization attempt
|
## Definition of done — one optimization attempt
|
||||||
|
|||||||
Generated
+12
@@ -234,6 +234,18 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "harnessd"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"diff-test",
|
||||||
|
"ledger",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ members = [
|
|||||||
"crates/ledger",
|
"crates/ledger",
|
||||||
"crates/report",
|
"crates/report",
|
||||||
"crates/verdict",
|
"crates/verdict",
|
||||||
|
"crates/harnessd",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Client for harnessd — the agent's only door to the trust layer.
|
||||||
|
|
||||||
|
Spawns the Rust daemon and speaks one JSON object per line over its
|
||||||
|
stdio. The client is deliberately dumb: no retries that could double-
|
||||||
|
apply a patch, no local fallbacks that could fabricate evidence.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class HarnessError(RuntimeError):
|
||||||
|
"""The harness refused or failed an operation."""
|
||||||
|
|
||||||
|
|
||||||
|
class HarnessClient:
|
||||||
|
def __init__(self, repo_root: str, binary: str = "target/debug/harnessd"):
|
||||||
|
self._proc = subprocess.Popen(
|
||||||
|
[binary],
|
||||||
|
cwd=repo_root,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def call(self, op: str, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
assert self._proc.stdin and self._proc.stdout
|
||||||
|
self._proc.stdin.write(json.dumps({"op": op, **kwargs}) + "\n")
|
||||||
|
self._proc.stdin.flush()
|
||||||
|
line = self._proc.stdout.readline()
|
||||||
|
if not line:
|
||||||
|
raise HarnessError("harnessd exited")
|
||||||
|
resp = json.loads(line)
|
||||||
|
if not resp.get("ok"):
|
||||||
|
raise HarnessError(resp.get("error", "unknown harness error"))
|
||||||
|
return resp["result"]
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if self._proc.stdin:
|
||||||
|
self._proc.stdin.close()
|
||||||
|
self._proc.wait(timeout=10)
|
||||||
@@ -1,59 +1,71 @@
|
|||||||
"""Tool surface exposed to the model (SPEC §3.5).
|
"""Tool surface exposed to the model (SPEC §3.5).
|
||||||
|
|
||||||
Exactly six tools, all mediated by the trust-layer harness over IPC —
|
Exactly six tools, all mediated by harnessd over line-delimited JSON —
|
||||||
the agent process has no direct filesystem or shell capability beyond
|
the agent process has no direct filesystem or shell capability beyond
|
||||||
what these return. Deliberately NOT exposed: shell on the host, writes
|
what these return. Deliberately NOT exposed: shell on the host, writes
|
||||||
outside targets/<name>/, any access to crates/, config/, corpora/.
|
outside targets/<name>/, any access to crates/, config/, corpora/.
|
||||||
|
|
||||||
Phase 2 wiring: each function below becomes a Claude Agent SDK tool
|
Phase 2 wiring: each function becomes a Claude Agent SDK tool
|
||||||
definition whose implementation calls the harness. The signatures and
|
definition. The bodies talk to the live harness; construct a client
|
||||||
docstrings are the contract; the bodies are placeholders until the
|
with `connect(repo_root)` first.
|
||||||
harness IPC exists (Phase 1 exit criteria).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
class HarnessUnavailable(RuntimeError):
|
from .harness import HarnessClient
|
||||||
"""Raised until the Phase 1 trust-layer harness IPC is wired up."""
|
|
||||||
|
_client: HarnessClient | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def connect(repo_root: str, binary: str = "target/debug/harnessd") -> None:
|
||||||
|
global _client
|
||||||
|
_client = HarnessClient(repo_root, binary)
|
||||||
|
|
||||||
|
|
||||||
|
def _c() -> HarnessClient:
|
||||||
|
if _client is None:
|
||||||
|
raise RuntimeError("call connect(repo_root) first")
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
def read_profile(target: str) -> dict[str, Any]:
|
def read_profile(target: str) -> dict[str, Any]:
|
||||||
"""Latest profile for a target: ranked hotspots (symbol, exclusive %,
|
"""Latest profile for a target: ranked hotspots. Read-only."""
|
||||||
source mapping) plus perf stat counters. Read-only."""
|
return _c().call("read_profile", target=target)
|
||||||
raise HarnessUnavailable("profiler adapter not wired (Phase 1)")
|
|
||||||
|
|
||||||
|
|
||||||
def read_ledger(target: str) -> list[dict[str, Any]]:
|
def read_ledger(target: str) -> dict[str, Any]:
|
||||||
"""All prior attempts against this target — the anti-double-attempt
|
"""Prior attempts against this target — the anti-double-attempt
|
||||||
memory. The agent must not re-grind a (hotspot, playbook_class,
|
memory. Never re-grind a class that already has a verdict."""
|
||||||
hypothesis) combination that already has a verdict."""
|
return _c().call("read_ledger", target=target)
|
||||||
raise HarnessUnavailable("ledger IPC not wired (Phase 1)")
|
|
||||||
|
|
||||||
|
|
||||||
def read_playbook(class_number: int) -> str:
|
def read_playbook(class_number: int) -> str:
|
||||||
"""Playbook class markdown (1-7): preconditions, procedure,
|
"""Playbook class markdown (1-7)."""
|
||||||
verification notes, known failure modes."""
|
return _c().call("read_playbook", **{"class": class_number})["markdown"]
|
||||||
raise HarnessUnavailable("playbook IPC not wired (Phase 1)")
|
|
||||||
|
|
||||||
|
|
||||||
def read_target_source(target: str, path: str) -> str:
|
def read_target_source(target: str, path: str) -> str:
|
||||||
"""Read-only view of the target's source tree at the pinned commit."""
|
"""Read-only view of the target workspace at the pinned commit."""
|
||||||
raise HarnessUnavailable("source IPC not wired (Phase 1)")
|
return _c().call("read_target_source", target=target, path=path)["content"]
|
||||||
|
|
||||||
|
|
||||||
def propose_patch(target: str, diff: str, hypothesis: str) -> str:
|
def propose_patch(target: str, diff: str, hypothesis: str) -> str:
|
||||||
"""Submit a unified diff plus the hypothesis it tests. The harness
|
"""Submit a unified diff plus its hypothesis. The harness applies it
|
||||||
applies it via `git apply` after a path-allowlist check (SPEC §10);
|
via `git apply` after a path-allowlist check (SPEC §10); anything
|
||||||
a diff touching anything outside targets/<target>/ is auto-rejected
|
outside targets/<target>/workspace is rejected before any gate
|
||||||
before any gate runs. Returns a patch id."""
|
runs. Returns a patch id."""
|
||||||
raise HarnessUnavailable("patch IPC not wired (Phase 1)")
|
return _c().call("propose_patch", target=target, diff=diff, hypothesis=hypothesis)["patch_id"]
|
||||||
|
|
||||||
|
|
||||||
def run_verdict(patch_id: str) -> dict[str, Any]:
|
def run_verdict(patch_id: str, run_id: str, playbook_class: int, hotspot: str) -> dict[str, Any]:
|
||||||
"""Run the full gate + bench sequence on a proposed patch and return
|
"""Gates + bench + ledger row for a proposed patch. This is the only
|
||||||
the ledger row (gates, bench CIs, verdict). This is the only way the
|
way the agent learns whether a change 'worked'."""
|
||||||
agent ever learns whether a change 'worked' — there is no other
|
return _c().call(
|
||||||
number to optimize."""
|
"run_verdict",
|
||||||
raise HarnessUnavailable("verdict IPC not wired (Phase 1)")
|
patch_id=patch_id,
|
||||||
|
run_id=run_id,
|
||||||
|
playbook_class=str(playbook_class),
|
||||||
|
hotspot=hotspot,
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "harnessd"
|
||||||
|
description = "Agent-facing IPC daemon: the six-tool surface (SPEC §3.5) over line-delimited JSON on stdio; patches applied only after path-allowlist checks"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
diff-test = { path = "../diff-test" }
|
||||||
|
ledger = { path = "../ledger" }
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
//! harnessd — the only door between the agent and the trust layer
|
||||||
|
//! (SPEC §3.5, §10). One JSON request per stdin line, one JSON response
|
||||||
|
//! per stdout line. The agent gets exactly six operations; everything
|
||||||
|
//! else (shell, trust-layer writes, patches outside the target
|
||||||
|
//! workspace) simply has no code path here. Phase 1 caveat, recorded:
|
||||||
|
//! same-uid filesystem read-only enforcement still requires the
|
||||||
|
//! separate-user/container setup (Phase 1 infra gap).
|
||||||
|
|
||||||
|
use anyhow::{anyhow, ensure, Result};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::io::{BufRead, Write};
|
||||||
|
use std::path::{Component, Path};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let root = std::env::current_dir()?.canonicalize()?;
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
let mut stdout = std::io::stdout().lock();
|
||||||
|
for line in stdin.lock().lines() {
|
||||||
|
let line = line?;
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let resp = match handle(&root, &line) {
|
||||||
|
Ok(v) => json!({"ok": true, "result": v}),
|
||||||
|
Err(e) => json!({"ok": false, "error": e.to_string()}),
|
||||||
|
};
|
||||||
|
writeln!(stdout, "{resp}")?;
|
||||||
|
stdout.flush()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field<'a>(req: &'a Value, key: &str) -> Result<&'a str> {
|
||||||
|
req.get(key)
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.ok_or_else(|| anyhow!("missing string field: {key}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A target name must be a plain directory name — no separators, no dots.
|
||||||
|
fn check_target(name: &str) -> Result<()> {
|
||||||
|
ensure!(
|
||||||
|
!name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|
||||||
|
"invalid target name"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Relative paths only; no parent components, no .git, no absolutes.
|
||||||
|
fn check_rel_path(p: &str) -> Result<()> {
|
||||||
|
let path = Path::new(p);
|
||||||
|
ensure!(!path.is_absolute(), "absolute paths forbidden");
|
||||||
|
for c in path.components() {
|
||||||
|
match c {
|
||||||
|
Component::Normal(seg) => ensure!(seg != ".git", ".git access forbidden"),
|
||||||
|
Component::CurDir => {}
|
||||||
|
_ => anyhow::bail!("path escapes workspace: {p}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(root: &Path, line: &str) -> Result<Value> {
|
||||||
|
let req: Value = serde_json::from_str(line)?;
|
||||||
|
match field(&req, "op")? {
|
||||||
|
"read_profile" => {
|
||||||
|
let t = field(&req, "target")?;
|
||||||
|
check_target(t)?;
|
||||||
|
let text = std::fs::read_to_string(root.join(format!("results/{t}/hotspots.txt")))?;
|
||||||
|
Ok(json!({"hotspots_text": text}))
|
||||||
|
}
|
||||||
|
"read_ledger" => {
|
||||||
|
let t = field(&req, "target")?;
|
||||||
|
check_target(t)?;
|
||||||
|
// Read-only surface over the append-only DB.
|
||||||
|
let ledger = ledger::Ledger::open(&root.join("results/ledger.sqlite"))?;
|
||||||
|
let classes = ledger.attempted_classes(t)?;
|
||||||
|
Ok(json!({"attempted_playbook_classes": classes, "total_attempts": ledger.count()?}))
|
||||||
|
}
|
||||||
|
"read_playbook" => {
|
||||||
|
let class = match req.get("class") {
|
||||||
|
Some(Value::Number(n)) => n.as_u64().ok_or_else(|| anyhow!("bad class"))? as u8,
|
||||||
|
Some(Value::String(s)) => s.parse()?,
|
||||||
|
_ => anyhow::bail!("missing class"),
|
||||||
|
};
|
||||||
|
ensure!((1..=7).contains(&class), "playbook class must be 1-7");
|
||||||
|
let dir = root.join("playbook");
|
||||||
|
let entry = std::fs::read_dir(&dir)?
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.find(|e| e.file_name().to_string_lossy().starts_with(&format!("{class:02}-")))
|
||||||
|
.ok_or_else(|| anyhow!("no playbook file for class {class}"))?;
|
||||||
|
Ok(json!({"markdown": std::fs::read_to_string(entry.path())?}))
|
||||||
|
}
|
||||||
|
"read_target_source" => {
|
||||||
|
let t = field(&req, "target")?;
|
||||||
|
let p = field(&req, "path")?;
|
||||||
|
check_target(t)?;
|
||||||
|
check_rel_path(p)?;
|
||||||
|
let full = root.join(format!("targets/{t}/workspace")).join(p);
|
||||||
|
Ok(json!({"content": std::fs::read_to_string(full)?}))
|
||||||
|
}
|
||||||
|
"propose_patch" => {
|
||||||
|
let t = field(&req, "target")?;
|
||||||
|
let diff = field(&req, "diff")?;
|
||||||
|
let hypothesis = field(&req, "hypothesis")?;
|
||||||
|
check_target(t)?;
|
||||||
|
// Allowlist: every path named by the diff must be a safe
|
||||||
|
// relative path (the git -C below roots them in the target
|
||||||
|
// workspace; nothing outside it is reachable).
|
||||||
|
for l in diff.lines() {
|
||||||
|
if let Some(p) = l.strip_prefix("--- a/").or_else(|| l.strip_prefix("+++ b/")) {
|
||||||
|
check_rel_path(p.trim())?;
|
||||||
|
}
|
||||||
|
ensure!(!l.starts_with("--- /") && !l.starts_with("+++ /"), "absolute diff paths forbidden");
|
||||||
|
}
|
||||||
|
let ws = root.join(format!("targets/{t}/workspace"));
|
||||||
|
let mut check = Command::new("git")
|
||||||
|
.args(["-C", ws.to_str().unwrap(), "apply", "--check", "-"])
|
||||||
|
.stdin(std::process::Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
check.stdin.take().unwrap().write_all(diff.as_bytes())?;
|
||||||
|
ensure!(check.wait()?.success(), "diff does not apply cleanly");
|
||||||
|
let mut apply = Command::new("git")
|
||||||
|
.args(["-C", ws.to_str().unwrap(), "apply", "-"])
|
||||||
|
.stdin(std::process::Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
apply.stdin.take().unwrap().write_all(diff.as_bytes())?;
|
||||||
|
ensure!(apply.wait()?.success(), "git apply failed");
|
||||||
|
let patch_id = format!("{:x}", Sha256::digest(diff.as_bytes()))[..12].to_string();
|
||||||
|
let pending = root.join("results/pending");
|
||||||
|
std::fs::create_dir_all(&pending)?;
|
||||||
|
std::fs::write(
|
||||||
|
pending.join(format!("{patch_id}.json")),
|
||||||
|
serde_json::to_string_pretty(&json!({"target": t, "hypothesis": hypothesis, "diff": diff}))?,
|
||||||
|
)?;
|
||||||
|
Ok(json!({"patch_id": patch_id}))
|
||||||
|
}
|
||||||
|
"run_verdict" => {
|
||||||
|
let patch_id = field(&req, "patch_id")?;
|
||||||
|
ensure!(patch_id.chars().all(|c| c.is_ascii_hexdigit()) && patch_id.len() == 12, "bad patch id");
|
||||||
|
let run_id = field(&req, "run_id")?;
|
||||||
|
let class = field(&req, "playbook_class")?;
|
||||||
|
let hotspot = field(&req, "hotspot")?;
|
||||||
|
let pending: Value = serde_json::from_str(&std::fs::read_to_string(
|
||||||
|
root.join(format!("results/pending/{patch_id}.json")),
|
||||||
|
)?)?;
|
||||||
|
let t = pending["target"].as_str().unwrap();
|
||||||
|
let spec = diff_test::target::TargetSpec::load(root, t)?;
|
||||||
|
// Build the candidate from the patched workspace into an
|
||||||
|
// isolated dir, then hand off to the verdict pipeline.
|
||||||
|
let cand_dir = root.join(format!("targets/{t}/candidate-{patch_id}"));
|
||||||
|
let build = Command::new("sh")
|
||||||
|
.arg("-c").arg(&spec.build.baseline)
|
||||||
|
.current_dir(root)
|
||||||
|
.env("CARGO_TARGET_DIR", &cand_dir)
|
||||||
|
.status()?;
|
||||||
|
ensure!(build.success(), "candidate build failed");
|
||||||
|
let bin = Path::new(&spec.build.binary).file_name().unwrap().to_str().unwrap();
|
||||||
|
let diff_path = root.join(format!("results/pending/{patch_id}.json.diff"));
|
||||||
|
std::fs::write(&diff_path, pending["diff"].as_str().unwrap())?;
|
||||||
|
let out = Command::new("cargo")
|
||||||
|
.args([
|
||||||
|
"run", "-q", "-p", "verdict", "--", t,
|
||||||
|
"--rebuild-baseline",
|
||||||
|
"--candidate-bin", &format!("targets/{t}/candidate-{patch_id}/release/{bin}"),
|
||||||
|
"--run-id", run_id,
|
||||||
|
"--playbook-class", class,
|
||||||
|
"--hypothesis", pending["hypothesis"].as_str().unwrap(),
|
||||||
|
"--hotspot", hotspot,
|
||||||
|
"--patch-file", diff_path.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.current_dir(root)
|
||||||
|
.output()?;
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
|
Ok(json!({"exit_ok": out.status.success(), "report": stdout,
|
||||||
|
"stderr_tail": String::from_utf8_lossy(&out.stderr).chars().rev().take(500).collect::<String>().chars().rev().collect::<String>()}))
|
||||||
|
}
|
||||||
|
other => Err(anyhow!("unknown op: {other} (six ops only, SPEC §3.5)")),
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,9 @@
|
|||||||
|
diff --git a/README.md b/README.md
|
||||||
|
index 5ab2632..dadf761 100644
|
||||||
|
--- a/README.md
|
||||||
|
+++ b/README.md
|
||||||
|
@@ -586,3 +586,4 @@ See [the graph](https://github.com/XAMPPRocky/tokei/graphs/contributors) for a f
|
||||||
|
Tokei is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
|
||||||
|
|
||||||
|
See [LICENCE-APACHE](./LICENCE-APACHE), [LICENCE-MIT](./LICENCE-MIT) for more information.
|
||||||
|
+<!-- hotpath ipc e2e marker -->
|
||||||
Reference in New Issue
Block a user