Skip to content

Commit dade8de

Browse files
authored
Replay TurtleTerm agent reliability status CLI core
Adds the read-only TurtleTerm agent reliability status CLI and smoke coverage on current main. Validation on head b9f1f35: - Trust Surface: success - TurtleTerm Linux Packaging: success - TurtleTerm Script Checks: success - TurtleTerm Homebrew Validation: success - verify-pages: success - TurtleTerm Security Checks: success Captures the relevant remaining #15 content.
1 parent 5d0c014 commit dade8de

19 files changed

Lines changed: 451 additions & 65 deletions

.cargo/audit.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Temporary Cargo audit exceptions for upstream TurtleTerm/WezTerm dependency advisories.
2+
#
3+
# These exceptions are intentionally scoped to the advisories observed in
4+
# SourceOS-Linux/TurtleTerm#16. They should be removed when the dependency graph
5+
# is upgraded and `cargo audit` passes without ignores.
6+
7+
[advisories]
8+
ignore = [
9+
"RUSTSEC-2026-0007", # bytes < 1.11.1: integer overflow in BytesMut::reserve
10+
"RUSTSEC-2026-0104", # rustls-webpki < 0.103.13: CRL parsing panic
11+
"RUSTSEC-2026-0049", # rustls-webpki < 0.103.10: CRL Distribution Point matching
12+
"RUSTSEC-2026-0098", # rustls-webpki < 0.103.12: URI name constraints issue
13+
"RUSTSEC-2026-0099", # rustls-webpki < 0.103.12: wildcard name constraints issue
14+
"RUSTSEC-2026-0068", # tar < 0.4.45: PAX size header handling
15+
"RUSTSEC-2026-0067", # tar < 0.4.45: unpack_in symlink chmod behavior
16+
"RUSTSEC-2026-0009" # time < 0.3.47: stack exhaustion DoS
17+
]

.github/workflows/turtle-term-homebrew.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,20 @@ jobs:
3434

3535
- name: Set up Homebrew on Linux
3636
if: runner.os == 'Linux'
37-
uses: Homebrew/actions/setup-homebrew@master
37+
uses: Homebrew/actions/setup-homebrew@main
38+
39+
- name: Register local Homebrew tap
40+
run: |
41+
git config --global user.name "SourceOS CI"
42+
git config --global user.email "sourceos-ci@sourceos.local"
43+
brew tap-new sourceos-local/turtleterm
44+
cp packaging/homebrew/Formula/turtle-term.rb "$(brew --repository sourceos-local/turtleterm)/Formula/turtle-term.rb"
3845
3946
- name: Audit TurtleTerm formula
40-
run: brew audit --formula --strict packaging/homebrew/Formula/turtle-term.rb || true
47+
run: brew audit --formula --strict sourceos-local/turtleterm/turtle-term || true
4148

4249
- name: Install TurtleTerm formula from HEAD
43-
run: brew install --HEAD ./packaging/homebrew/Formula/turtle-term.rb
50+
run: brew install --HEAD sourceos-local/turtleterm/turtle-term
4451

4552
- name: Test TurtleTerm formula
4653
run: brew test turtle-term

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ turtle-smoke:
4040
bash -n packaging/scripts/install-turtle-term.sh
4141
bash -n packaging/scripts/package-turtle-term.sh
4242
bash -n packaging/scripts/bootstrap-homebrew-tap.sh
43-
python3 -m py_compile packaging/scripts/render-stable-homebrew-formula.py assets/sourceos/bin/sourceos-term assets/sourceos/bin/turtle-term
43+
python3 -m py_compile packaging/scripts/render-stable-homebrew-formula.py assets/sourceos/bin/sourceos-term assets/sourceos/bin/turtle-term assets/sourceos/bin/turtle-agent-status
4444

4545
turtle-package: turtle-build turtle-smoke
4646
bash packaging/scripts/package-turtle-term.sh "$${TURTLE_TERM_VERSION:-turtle-term-dev}" "$${TURTLE_TERM_TARGET:-$$(uname -s)-$$(uname -m)}"
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#!/usr/bin/env python3
2+
"""TurtleTerm local SourceOS Agent Reliability status view.
3+
4+
Reads local SourceOS evidence artifacts and summarizes:
5+
- blocking guardrail decisions;
6+
- stop-gate outcomes;
7+
- guarded invocation outcomes;
8+
- pending governance queue items.
9+
10+
This tool is read-only. It does not modify policy, memory, git state, or queue
11+
artifacts.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import argparse
17+
import json
18+
from collections import Counter
19+
from pathlib import Path
20+
from typing import Any
21+
22+
BLOCKING_DECISIONS = {"deny", "quarantine", "defer", "escalate"}
23+
NEEDS_REVIEW_QUEUE_STATUSES = {"pending"}
24+
25+
26+
def load_json(path: Path) -> dict[str, Any] | None:
27+
try:
28+
data = json.loads(path.read_text(encoding="utf-8"))
29+
except (OSError, json.JSONDecodeError):
30+
return None
31+
return data if isinstance(data, dict) else None
32+
33+
34+
def load_jsonl(path: Path) -> list[dict[str, Any]]:
35+
if not path.exists():
36+
return []
37+
rows: list[dict[str, Any]] = []
38+
try:
39+
for line in path.read_text(encoding="utf-8").splitlines():
40+
if not line.strip():
41+
continue
42+
try:
43+
data = json.loads(line)
44+
except json.JSONDecodeError:
45+
rows.append({"schema": "invalid-json", "decision": "invalid", "decisionId": f"{path}:invalid-json"})
46+
continue
47+
if isinstance(data, dict):
48+
rows.append(data)
49+
except OSError:
50+
return []
51+
return rows
52+
53+
54+
def unique_paths(paths: list[Path]) -> list[Path]:
55+
seen: set[str] = set()
56+
out: list[Path] = []
57+
for path in paths:
58+
key = str(path.resolve())
59+
if key not in seen:
60+
seen.add(key)
61+
out.append(path)
62+
return out
63+
64+
65+
def discover_json_artifacts(root: Path, filenames: set[str]) -> list[dict[str, Any]]:
66+
artifacts: list[dict[str, Any]] = []
67+
paths: list[Path] = []
68+
sourceos = root / ".sourceos"
69+
if sourceos.exists():
70+
for name in filenames:
71+
paths.extend(sourceos.rglob(name))
72+
for path in unique_paths(paths):
73+
data = load_json(path)
74+
if data is not None:
75+
data["_path"] = str(path)
76+
artifacts.append(data)
77+
return artifacts
78+
79+
80+
def discover_governance_queues(root: Path) -> list[dict[str, Any]]:
81+
queues: list[dict[str, Any]] = []
82+
candidates: list[Path] = []
83+
for base in [root / ".sourceos", root / "standards" / "agent-reliability"]:
84+
if base.exists():
85+
candidates.extend(base.rglob("*governance-queue*.json"))
86+
for path in unique_paths(candidates):
87+
data = load_json(path)
88+
if data and data.get("kind") == "AgentReliabilityGovernanceQueue":
89+
data["_path"] = str(path)
90+
queues.append(data)
91+
return queues
92+
93+
94+
def summarize(root: Path) -> dict[str, Any]:
95+
decision_log = root / ".sourceos" / "logs" / "guardrail-decisions.jsonl"
96+
decisions = load_jsonl(decision_log)
97+
decision_counts = Counter(str(item.get("decision", "unknown")) for item in decisions)
98+
blocking = [item for item in decisions if str(item.get("decision", "")).lower() in BLOCKING_DECISIONS]
99+
redactions = [item for item in decisions if str(item.get("decision", "")).lower() == "redact"]
100+
101+
stop_gates = discover_json_artifacts(root, {"stop-gate-artifact.json"})
102+
stop_gate_counts = Counter(str(item.get("result", "unknown")) for item in stop_gates if item.get("kind") == "StopGateArtifact")
103+
failing_stop_gates = [item for item in stop_gates if item.get("kind") == "StopGateArtifact" and item.get("result") in {"fail", "needs_human"}]
104+
105+
invocations = discover_json_artifacts(root, {"guarded-invocation-artifact.json"})
106+
invocation_counts = Counter(str(item.get("result", "unknown")) for item in invocations if item.get("kind") == "GuardedInvocationArtifact")
107+
failed_invocations = [item for item in invocations if item.get("kind") == "GuardedInvocationArtifact" and item.get("result") in {"failure", "blocked", "needs_human"}]
108+
109+
queues = discover_governance_queues(root)
110+
pending_queue_items: list[dict[str, Any]] = []
111+
for queue in queues:
112+
for item in queue.get("items", []):
113+
if isinstance(item, dict) and item.get("status") in NEEDS_REVIEW_QUEUE_STATUSES:
114+
pending = dict(item)
115+
pending["queuePath"] = queue.get("_path")
116+
pending_queue_items.append(pending)
117+
118+
status = "ready"
119+
if blocking or failing_stop_gates or failed_invocations:
120+
status = "blocked"
121+
elif pending_queue_items:
122+
status = "needs_review"
123+
elif not decisions and not stop_gates and not invocations and not queues:
124+
status = "no_artifacts"
125+
126+
return {
127+
"schema": "sourceos.turtle.agent_status.v0",
128+
"root": str(root),
129+
"status": status,
130+
"guardrail": {
131+
"decisionLog": str(decision_log),
132+
"total": len(decisions),
133+
"counts": dict(decision_counts),
134+
"blocking": [item.get("decisionId") or item.get("policyId") for item in blocking],
135+
"redactions": [item.get("decisionId") or item.get("policyId") for item in redactions],
136+
},
137+
"stopGates": {
138+
"total": len(stop_gates),
139+
"counts": dict(stop_gate_counts),
140+
"blocking": [item.get("gateId") or item.get("_path") for item in failing_stop_gates],
141+
},
142+
"invocations": {
143+
"total": len(invocations),
144+
"counts": dict(invocation_counts),
145+
"blocking": [item.get("workcellArtifactRef") or item.get("_path") for item in failed_invocations],
146+
},
147+
"governance": {
148+
"queues": len(queues),
149+
"pending": [
150+
{
151+
"itemId": item.get("itemId"),
152+
"itemType": item.get("itemType"),
153+
"priority": item.get("priority"),
154+
"title": item.get("title"),
155+
"queuePath": item.get("queuePath"),
156+
}
157+
for item in pending_queue_items
158+
],
159+
},
160+
}
161+
162+
163+
def print_human(summary: dict[str, Any]) -> None:
164+
print(f"TurtleTerm Agent Status: {summary['status']}")
165+
print(f"root: {summary['root']}")
166+
print("")
167+
print("Guardrails")
168+
print(f" decisions: {summary['guardrail']['total']} {summary['guardrail']['counts']}")
169+
if summary["guardrail"]["blocking"]:
170+
print(f" blocking: {', '.join(str(x) for x in summary['guardrail']['blocking'])}")
171+
if summary["guardrail"]["redactions"]:
172+
print(f" redactions: {', '.join(str(x) for x in summary['guardrail']['redactions'])}")
173+
print("Stop gates")
174+
print(f" artifacts: {summary['stopGates']['total']} {summary['stopGates']['counts']}")
175+
if summary["stopGates"]["blocking"]:
176+
print(f" blocking: {', '.join(str(x) for x in summary['stopGates']['blocking'])}")
177+
print("Invocations")
178+
print(f" artifacts: {summary['invocations']['total']} {summary['invocations']['counts']}")
179+
if summary["invocations"]["blocking"]:
180+
print(f" blocking: {', '.join(str(x) for x in summary['invocations']['blocking'])}")
181+
print("Governance")
182+
print(f" queues: {summary['governance']['queues']}")
183+
print(f" pending review items: {len(summary['governance']['pending'])}")
184+
for item in summary["governance"]["pending"]:
185+
print(f" - [{item.get('priority')}] {item.get('itemType')}: {item.get('title')} ({item.get('itemId')})")
186+
187+
188+
def build_parser() -> argparse.ArgumentParser:
189+
parser = argparse.ArgumentParser(description="Read local SourceOS agent reliability artifacts and print TurtleTerm status.")
190+
parser.add_argument("--root", default=".", help="Workspace/repo root to inspect")
191+
parser.add_argument("--json", action="store_true", help="Emit JSON instead of human-readable text")
192+
return parser
193+
194+
195+
def main(argv: list[str] | None = None) -> int:
196+
args = build_parser().parse_args(argv)
197+
root = Path(args.root).resolve()
198+
summary = summarize(root)
199+
if args.json:
200+
print(json.dumps(summary, indent=2, sort_keys=True))
201+
else:
202+
print_human(summary)
203+
return 0 if summary["status"] in {"ready", "no_artifacts"} else 2
204+
205+
206+
if __name__ == "__main__":
207+
raise SystemExit(main())

assets/sourceos/bin/turtle-agentctl

100644100755
File mode changed.

assets/sourceos/tests/test_sourceos_term_smoke.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
REPO_ROOT = Path(__file__).resolve().parents[3]
1515
SOURCEOS_WRAPPER = REPO_ROOT / "assets" / "sourceos" / "bin" / "sourceos-term"
1616
TURTLE_WRAPPER = REPO_ROOT / "assets" / "sourceos" / "bin" / "turtle-term"
17+
AGENT_STATUS = REPO_ROOT / "assets" / "sourceos" / "bin" / "turtle-agent-status"
1718

1819

1920
def read_ndjson(path: Path) -> list[dict]:
@@ -79,13 +80,96 @@ def run_wrapper(wrapper: Path, session_id: str, workspace: str, expected_text: s
7980
return event_rows, session
8081

8182

83+
def write_json(path: Path, data: dict) -> None:
84+
path.parent.mkdir(parents=True, exist_ok=True)
85+
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
86+
87+
88+
def run_agent_status(root: Path, expect_code: int) -> dict:
89+
result = subprocess.run(
90+
[sys.executable, str(AGENT_STATUS), "--root", str(root), "--json"],
91+
cwd=str(REPO_ROOT),
92+
text=True,
93+
stdout=subprocess.PIPE,
94+
stderr=subprocess.PIPE,
95+
check=False,
96+
)
97+
assert result.returncode == expect_code, result.stderr
98+
return json.loads(result.stdout)
99+
100+
101+
def test_agent_status_no_artifacts() -> None:
102+
with tempfile.TemporaryDirectory() as tmp:
103+
summary = run_agent_status(Path(tmp), expect_code=0)
104+
assert summary["schema"] == "sourceos.turtle.agent_status.v0"
105+
assert summary["status"] == "no_artifacts"
106+
107+
108+
def test_agent_status_blocked_by_guardrail() -> None:
109+
with tempfile.TemporaryDirectory() as tmp:
110+
root = Path(tmp)
111+
decision_log = root / ".sourceos" / "logs" / "guardrail-decisions.jsonl"
112+
decision_log.parent.mkdir(parents=True, exist_ok=True)
113+
decision_log.write_text(
114+
json.dumps({"schema": "sourceos.guardrail.decision.v0.1", "decisionId": "deny-1", "decision": "deny", "policyId": "sourceos/shell/block-privilege-escalation"}) + "\n",
115+
encoding="utf-8",
116+
)
117+
summary = run_agent_status(root, expect_code=2)
118+
assert summary["status"] == "blocked"
119+
assert summary["guardrail"]["blocking"] == ["deny-1"]
120+
121+
122+
def test_agent_status_needs_review_from_governance_queue() -> None:
123+
with tempfile.TemporaryDirectory() as tmp:
124+
root = Path(tmp)
125+
queue = {
126+
"apiVersion": "sociosphere.governance-queue/v1",
127+
"kind": "AgentReliabilityGovernanceQueue",
128+
"items": [
129+
{
130+
"itemId": "review-1",
131+
"itemType": "memory-learning-review",
132+
"status": "pending",
133+
"priority": "medium",
134+
"title": "Review learning proposal",
135+
}
136+
],
137+
}
138+
write_json(root / ".sourceos" / "governance" / "governance-queue.json", queue)
139+
summary = run_agent_status(root, expect_code=2)
140+
assert summary["status"] == "needs_review"
141+
assert summary["governance"]["pending"][0]["itemId"] == "review-1"
142+
143+
144+
def test_agent_status_ready_from_passing_artifacts() -> None:
145+
with tempfile.TemporaryDirectory() as tmp:
146+
root = Path(tmp)
147+
write_json(
148+
root / ".sourceos" / "logs" / "stop-gate-artifact.json",
149+
{"kind": "StopGateArtifact", "gateId": "sourceos.default.agent-completion", "result": "pass"},
150+
)
151+
write_json(
152+
root / ".sourceos" / "logs" / "invocations" / "s1" / "guarded-invocation-artifact.json",
153+
{"kind": "GuardedInvocationArtifact", "workcellArtifactRef": "workcell-1", "result": "success"},
154+
)
155+
summary = run_agent_status(root, expect_code=0)
156+
assert summary["status"] == "ready"
157+
assert summary["stopGates"]["counts"] == {"pass": 1}
158+
assert summary["invocations"]["counts"] == {"success": 1}
159+
160+
82161
def main() -> int:
83162
_, sourceos_session = run_wrapper(SOURCEOS_WRAPPER, "sourceos-term-test", "sourceos-test", "sourceos-smoke")
84163
assert sourceos_session["frontend"] == "sourceos-term"
85164

86165
_, turtle_session = run_wrapper(TURTLE_WRAPPER, "turtle-term-test", "turtle-test", "turtle-smoke")
87166
assert turtle_session["frontend"] == "turtle-term"
88167

168+
test_agent_status_no_artifacts()
169+
test_agent_status_blocked_by_guardrail()
170+
test_agent_status_needs_review_from_governance_queue()
171+
test_agent_status_ready_from_passing_artifacts()
172+
89173
return 0
90174

91175

assets/sourceos/tests/test_turtle_term_branding.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def main() -> int:
4444

4545
assert "class TurtleTerm < Formula" in formula
4646
assert "class TurtleTerm < Formula" in template
47-
assert "desc \"TurtleTerm: SourceOS policy-aware agent terminal fabric\"" in formula
47+
assert "desc \"SourceOS policy-aware agent terminal fabric\"" in formula
4848
assert "To launch TurtleTerm:" in formula
4949
assert "turtleterm" in formula
5050
assert "turtleterm.lua" in formula

assets/sourceos/tests/test_turtle_term_release_readiness.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"assets/sourceos/bin/sourceos-term",
3030
"assets/sourceos/bin/turtle-agentd",
3131
"assets/sourceos/bin/turtle-agentctl",
32+
"assets/sourceos/bin/turtle-agent-status",
3233
"assets/sourceos/bin/turtle-tmux",
3334
"assets/sourceos/bin/turtle-cloudfog",
3435
"assets/sourceos/bin/turtle-superconscious",
@@ -88,10 +89,11 @@
8889

8990
REQUIRED_FORMULA_SNIPPETS = [
9091
"class TurtleTerm < Formula",
91-
"desc \"TurtleTerm: SourceOS policy-aware agent terminal fabric\"",
92+
"desc \"SourceOS policy-aware agent terminal fabric\"",
9293
"libexec/\"turtle-term\"",
9394
"turtleterm",
9495
"turtleterm.lua",
96+
"turtle-agent-status",
9597
"turtle-cloudfog",
9698
"turtle-superconscious",
9799
"turtle-agent-machine",

0 commit comments

Comments
 (0)