feat(operator): hh-agent manifest — VMs carry a self-describing handoff record
Add ICM-aligned agent-manifest tooling so a hack-house sandbox/VM can be shared, traded, and resumed without a human briefing. manifest.py is a dependency-light (stdlib + optional PyYAML) library + CLI that writes a `.hh-agent/` bundle: manifest.yaml (canonical machine record) plus rendered AGENT.md / last-state.md / summary.md / goals.yaml views. Wire a `manifest` op into the operator bridge (push/pull/update) that moves the bundle in and out of the *target sandbox* via the same exec path as write/get, so the VM itself carries its purpose, goals, user intent, live state, and a provenance chain — the unit of agent-to-agent work transmission. Pairs with `spawn`: stamp → push → hand a child "load the .hh-agent manifest and continue." Proven end-to-end against a real Kali podman container (push → on-disk verify → update → pull-back) and covered by 6 new offline tests (34 green total). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -427,3 +427,99 @@ def test_watch_matches_event_and_idle_and_timeout():
|
||||
res = await b._op_watch({"for": "never-ever", "timeout": 0.3})
|
||||
assert res["reason"] == "timeout" and res["matched"] is False
|
||||
asyncio.run(go())
|
||||
|
||||
|
||||
# ── agent-manifest (the unit of work-transmission) ──────────────────────────
|
||||
from cmd_chat.operator import manifest as mfst # noqa: E402
|
||||
|
||||
|
||||
def test_manifest_roundtrip_dict():
|
||||
m = mfst.Manifest(name="box", purpose="p",
|
||||
goals={"objective": "o", "user_intent": "u",
|
||||
"stop_conditions": ["done"]})
|
||||
d = m.to_dict()
|
||||
# canonical key order keeps schema/id/name scannable at the top
|
||||
assert list(d)[:3] == ["schema", "id", "name"]
|
||||
m2 = mfst.Manifest.from_dict(d)
|
||||
assert m2.name == "box" and m2.goals["objective"] == "o"
|
||||
# unknown keys from a newer writer are ignored, not fatal
|
||||
m3 = mfst.Manifest.from_dict({**d, "future_field": 123})
|
||||
assert m3.name == "box"
|
||||
|
||||
|
||||
def test_manifest_update_appends_and_replaces():
|
||||
m = mfst.Manifest(name="box")
|
||||
first = m.updated_at
|
||||
m.update_state(done="wrote tooling", todo=["wire verb", "test"], status="in_progress")
|
||||
assert m.state["done"] == ["wrote tooling"]
|
||||
assert m.state["todo"] == ["wire verb", "test"]
|
||||
m.update_state(done=["more"], append=True)
|
||||
assert m.state["done"] == ["wrote tooling", "more"]
|
||||
m.update_state(todo=["only"], append=False) # replace
|
||||
assert m.state["todo"] == ["only"]
|
||||
assert m.updated_at >= first
|
||||
|
||||
|
||||
def test_manifest_provenance_and_render():
|
||||
m = mfst.Manifest(name="recon", purpose="kali box")
|
||||
m.add_provenance(by="op-a", action="init", note="created")
|
||||
m.add_provenance(by="op-b", note="handed off")
|
||||
agent = mfst.render_agent_md(m)
|
||||
assert "AGENT.md — recon" in agent and "kali box" in agent
|
||||
last = mfst.render_last_state_md(m)
|
||||
assert "op-a" in last and "op-b" in last and "handed off" in last
|
||||
files = mfst.bundle_files(m)
|
||||
assert set(files) == {"manifest.yaml", "AGENT.md", "last-state.md",
|
||||
"summary.md", "goals.yaml"}
|
||||
|
||||
|
||||
def test_manifest_write_read_bundle(tmp_path):
|
||||
m = mfst.Manifest(name="box", purpose="p",
|
||||
goals={"objective": "ship", "user_intent": "i",
|
||||
"stop_conditions": ["x"]})
|
||||
bdir = mfst.write_bundle(m, str(tmp_path))
|
||||
assert Path(bdir).name == mfst.MANIFEST_DIR
|
||||
loaded = mfst.read_bundle(str(tmp_path))
|
||||
assert loaded.id == m.id and loaded.goals["objective"] == "ship"
|
||||
|
||||
|
||||
def test_op_manifest_push_pull_update_via_local(tmp_path):
|
||||
async def go():
|
||||
b = _bridge("oracle")
|
||||
b._own_engine, b._own_name = "local", "" # exec on host into tmp_path
|
||||
root = str(tmp_path)
|
||||
r = await b._op_manifest({
|
||||
"action": "push", "root": root, "name": "demo-box",
|
||||
"purpose": "build manifest tooling live", "objective": "hand it off",
|
||||
"stop": ["round-trips"]})
|
||||
assert r["ok"] and r["id"].startswith("hh-")
|
||||
# the VM now carries its own bundle on disk
|
||||
assert (tmp_path / ".hh-agent" / "manifest.yaml").is_file()
|
||||
assert (tmp_path / ".hh-agent" / "AGENT.md").is_file()
|
||||
# pull it back parsed — what a spawn/handoff reads to resume
|
||||
r = await b._op_manifest({"action": "pull", "root": root})
|
||||
assert r["ok"] and r["manifest"]["name"] == "demo-box"
|
||||
assert r["manifest"]["created_by"] == "oracle"
|
||||
# update state + provenance, re-rendered into the bundle
|
||||
r = await b._op_manifest({
|
||||
"action": "update", "root": root, "status": "done",
|
||||
"progress": "tooling shipped", "done": ["wrote it"],
|
||||
"note": "operator handing to child"})
|
||||
assert r["ok"] and r["status"] == "done"
|
||||
r = await b._op_manifest({"action": "pull", "root": root})
|
||||
st = r["manifest"]["state"]
|
||||
assert st["status"] == "done" and "wrote it" in st["done"]
|
||||
prov = r["manifest"]["provenance"]
|
||||
assert any(p["action"] == "update" for p in prov)
|
||||
asyncio.run(go())
|
||||
|
||||
|
||||
def test_op_manifest_pull_missing_is_clean_error():
|
||||
async def go():
|
||||
b = _bridge("oracle")
|
||||
b._own_engine, b._own_name = "local", ""
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
r = await b._op_manifest({"action": "pull", "root": d})
|
||||
assert r["ok"] is False and "no manifest" in r["error"]
|
||||
asyncio.run(go())
|
||||
|
||||
Reference in New Issue
Block a user