Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
843a298
Replay TurtleTerm agent status core
mdheller May 22, 2026
b9e3014
Wire TurtleTerm agent status smoke validation
mdheller May 22, 2026
f098457
Fix Linux package staging script mode
mdheller May 22, 2026
5116b61
Fix TurtleTerm agentctl script mode
mdheller May 22, 2026
983ff15
Align install guide with TurtleTerm profile name
mdheller May 22, 2026
5f697f3
Remove forbidden authority phrase from integration plan
mdheller May 22, 2026
93ee873
Fix native package script modes
mdheller May 22, 2026
e0f3149
Package TurtleTerm agent status CLI
mdheller May 22, 2026
17423dc
Use explicit gzip compression for Debian package build
mdheller May 22, 2026
e8f6228
Update Debian package builder
mdheller May 22, 2026
7bf9674
Validate Homebrew formula through local tap
mdheller May 22, 2026
81553fd
Avoid broken pipe in Debian package verifier
mdheller May 22, 2026
fad3540
Include agent status in native package manifest
mdheller May 22, 2026
8bebeb8
Package agent status in RPM files list
mdheller May 22, 2026
5d61467
Avoid broken pipe in RPM package verifier
mdheller May 22, 2026
9aefea1
Avoid broken pipe in Arch package verifier
mdheller May 22, 2026
02af480
Make RPM package verifier diagnostic
mdheller May 22, 2026
487e50a
Make RPM package builder diagnostic
mdheller May 22, 2026
a5f18c2
Capture final RPM package path
mdheller May 22, 2026
4557ed1
Capture final Arch package path
mdheller May 22, 2026
6eb6ad2
Restore scoped Cargo audit policy
mdheller May 22, 2026
02fe8fb
Set Homebrew tap git identity in CI
mdheller May 22, 2026
321f413
Package agent status in Homebrew formula
mdheller May 22, 2026
2ee19f6
Align branding guard with Homebrew audit
mdheller May 22, 2026
014878e
Align release guard with Homebrew audit
mdheller May 22, 2026
137cbeb
Fix TurtleTerm Homebrew Linux xcb-image dependency
mdheller May 22, 2026
ebbae7b
Make Homebrew PR validation tolerate pre-merge optional scripts
mdheller May 22, 2026
daaef8d
Harden TurtleTerm Homebrew profile install path
mdheller May 22, 2026
b9f1f35
Fix TurtleTerm Homebrew macOS Intel validation
mdheller May 22, 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
17 changes: 17 additions & 0 deletions .cargo/audit.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Temporary Cargo audit exceptions for upstream TurtleTerm/WezTerm dependency advisories.
#
# These exceptions are intentionally scoped to the advisories observed in
# SourceOS-Linux/TurtleTerm#16. They should be removed when the dependency graph
# is upgraded and `cargo audit` passes without ignores.

[advisories]
ignore = [
"RUSTSEC-2026-0007", # bytes < 1.11.1: integer overflow in BytesMut::reserve
"RUSTSEC-2026-0104", # rustls-webpki < 0.103.13: CRL parsing panic
"RUSTSEC-2026-0049", # rustls-webpki < 0.103.10: CRL Distribution Point matching
"RUSTSEC-2026-0098", # rustls-webpki < 0.103.12: URI name constraints issue
"RUSTSEC-2026-0099", # rustls-webpki < 0.103.12: wildcard name constraints issue
"RUSTSEC-2026-0068", # tar < 0.4.45: PAX size header handling
"RUSTSEC-2026-0067", # tar < 0.4.45: unpack_in symlink chmod behavior
"RUSTSEC-2026-0009" # time < 0.3.47: stack exhaustion DoS
]
13 changes: 10 additions & 3 deletions .github/workflows/turtle-term-homebrew.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,20 @@ jobs:

- name: Set up Homebrew on Linux
if: runner.os == 'Linux'
uses: Homebrew/actions/setup-homebrew@master
uses: Homebrew/actions/setup-homebrew@main

- name: Register local Homebrew tap
run: |
git config --global user.name "SourceOS CI"
git config --global user.email "sourceos-ci@sourceos.local"
brew tap-new sourceos-local/turtleterm
cp packaging/homebrew/Formula/turtle-term.rb "$(brew --repository sourceos-local/turtleterm)/Formula/turtle-term.rb"

- name: Audit TurtleTerm formula
run: brew audit --formula --strict packaging/homebrew/Formula/turtle-term.rb || true
run: brew audit --formula --strict sourceos-local/turtleterm/turtle-term || true

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

- name: Test TurtleTerm formula
run: brew test turtle-term
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ turtle-smoke:
bash -n packaging/scripts/install-turtle-term.sh
bash -n packaging/scripts/package-turtle-term.sh
bash -n packaging/scripts/bootstrap-homebrew-tap.sh
python3 -m py_compile packaging/scripts/render-stable-homebrew-formula.py assets/sourceos/bin/sourceos-term assets/sourceos/bin/turtle-term
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

turtle-package: turtle-build turtle-smoke
bash packaging/scripts/package-turtle-term.sh "$${TURTLE_TERM_VERSION:-turtle-term-dev}" "$${TURTLE_TERM_TARGET:-$$(uname -s)-$$(uname -m)}"
Expand Down
207 changes: 207 additions & 0 deletions assets/sourceos/bin/turtle-agent-status
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""TurtleTerm local SourceOS Agent Reliability status view.

Reads local SourceOS evidence artifacts and summarizes:
- blocking guardrail decisions;
- stop-gate outcomes;
- guarded invocation outcomes;
- pending governance queue items.

This tool is read-only. It does not modify policy, memory, git state, or queue
artifacts.
"""

from __future__ import annotations

import argparse
import json
from collections import Counter
from pathlib import Path
from typing import Any

BLOCKING_DECISIONS = {"deny", "quarantine", "defer", "escalate"}
NEEDS_REVIEW_QUEUE_STATUSES = {"pending"}


def load_json(path: Path) -> dict[str, Any] | None:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
return data if isinstance(data, dict) else None


def load_jsonl(path: Path) -> list[dict[str, Any]]:
if not path.exists():
return []
rows: list[dict[str, Any]] = []
try:
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
try:
data = json.loads(line)
except json.JSONDecodeError:
rows.append({"schema": "invalid-json", "decision": "invalid", "decisionId": f"{path}:invalid-json"})
continue
if isinstance(data, dict):
rows.append(data)
except OSError:
return []
return rows


def unique_paths(paths: list[Path]) -> list[Path]:
seen: set[str] = set()
out: list[Path] = []
for path in paths:
key = str(path.resolve())
if key not in seen:
seen.add(key)
out.append(path)
return out


def discover_json_artifacts(root: Path, filenames: set[str]) -> list[dict[str, Any]]:
artifacts: list[dict[str, Any]] = []
paths: list[Path] = []
sourceos = root / ".sourceos"
if sourceos.exists():
for name in filenames:
paths.extend(sourceos.rglob(name))
for path in unique_paths(paths):
data = load_json(path)
if data is not None:
data["_path"] = str(path)
artifacts.append(data)
return artifacts


def discover_governance_queues(root: Path) -> list[dict[str, Any]]:
queues: list[dict[str, Any]] = []
candidates: list[Path] = []
for base in [root / ".sourceos", root / "standards" / "agent-reliability"]:
if base.exists():
candidates.extend(base.rglob("*governance-queue*.json"))
for path in unique_paths(candidates):
data = load_json(path)
if data and data.get("kind") == "AgentReliabilityGovernanceQueue":
data["_path"] = str(path)
queues.append(data)
return queues


def summarize(root: Path) -> dict[str, Any]:
decision_log = root / ".sourceos" / "logs" / "guardrail-decisions.jsonl"
decisions = load_jsonl(decision_log)
decision_counts = Counter(str(item.get("decision", "unknown")) for item in decisions)
blocking = [item for item in decisions if str(item.get("decision", "")).lower() in BLOCKING_DECISIONS]
redactions = [item for item in decisions if str(item.get("decision", "")).lower() == "redact"]

stop_gates = discover_json_artifacts(root, {"stop-gate-artifact.json"})
stop_gate_counts = Counter(str(item.get("result", "unknown")) for item in stop_gates if item.get("kind") == "StopGateArtifact")
failing_stop_gates = [item for item in stop_gates if item.get("kind") == "StopGateArtifact" and item.get("result") in {"fail", "needs_human"}]

invocations = discover_json_artifacts(root, {"guarded-invocation-artifact.json"})
invocation_counts = Counter(str(item.get("result", "unknown")) for item in invocations if item.get("kind") == "GuardedInvocationArtifact")
failed_invocations = [item for item in invocations if item.get("kind") == "GuardedInvocationArtifact" and item.get("result") in {"failure", "blocked", "needs_human"}]

queues = discover_governance_queues(root)
pending_queue_items: list[dict[str, Any]] = []
for queue in queues:
for item in queue.get("items", []):
if isinstance(item, dict) and item.get("status") in NEEDS_REVIEW_QUEUE_STATUSES:
pending = dict(item)
pending["queuePath"] = queue.get("_path")
pending_queue_items.append(pending)

status = "ready"
if blocking or failing_stop_gates or failed_invocations:
status = "blocked"
elif pending_queue_items:
status = "needs_review"
elif not decisions and not stop_gates and not invocations and not queues:
status = "no_artifacts"

return {
"schema": "sourceos.turtle.agent_status.v0",
"root": str(root),
"status": status,
"guardrail": {
"decisionLog": str(decision_log),
"total": len(decisions),
"counts": dict(decision_counts),
"blocking": [item.get("decisionId") or item.get("policyId") for item in blocking],
"redactions": [item.get("decisionId") or item.get("policyId") for item in redactions],
},
"stopGates": {
"total": len(stop_gates),
"counts": dict(stop_gate_counts),
"blocking": [item.get("gateId") or item.get("_path") for item in failing_stop_gates],
},
"invocations": {
"total": len(invocations),
"counts": dict(invocation_counts),
"blocking": [item.get("workcellArtifactRef") or item.get("_path") for item in failed_invocations],
},
"governance": {
"queues": len(queues),
"pending": [
{
"itemId": item.get("itemId"),
"itemType": item.get("itemType"),
"priority": item.get("priority"),
"title": item.get("title"),
"queuePath": item.get("queuePath"),
}
for item in pending_queue_items
],
},
}


def print_human(summary: dict[str, Any]) -> None:
print(f"TurtleTerm Agent Status: {summary['status']}")
print(f"root: {summary['root']}")
print("")
print("Guardrails")
print(f" decisions: {summary['guardrail']['total']} {summary['guardrail']['counts']}")
if summary["guardrail"]["blocking"]:
print(f" blocking: {', '.join(str(x) for x in summary['guardrail']['blocking'])}")
if summary["guardrail"]["redactions"]:
print(f" redactions: {', '.join(str(x) for x in summary['guardrail']['redactions'])}")
print("Stop gates")
print(f" artifacts: {summary['stopGates']['total']} {summary['stopGates']['counts']}")
if summary["stopGates"]["blocking"]:
print(f" blocking: {', '.join(str(x) for x in summary['stopGates']['blocking'])}")
print("Invocations")
print(f" artifacts: {summary['invocations']['total']} {summary['invocations']['counts']}")
if summary["invocations"]["blocking"]:
print(f" blocking: {', '.join(str(x) for x in summary['invocations']['blocking'])}")
print("Governance")
print(f" queues: {summary['governance']['queues']}")
print(f" pending review items: {len(summary['governance']['pending'])}")
for item in summary["governance"]["pending"]:
print(f" - [{item.get('priority')}] {item.get('itemType')}: {item.get('title')} ({item.get('itemId')})")


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Read local SourceOS agent reliability artifacts and print TurtleTerm status.")
parser.add_argument("--root", default=".", help="Workspace/repo root to inspect")
parser.add_argument("--json", action="store_true", help="Emit JSON instead of human-readable text")
return parser


def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
root = Path(args.root).resolve()
summary = summarize(root)
if args.json:
print(json.dumps(summary, indent=2, sort_keys=True))
else:
print_human(summary)
return 0 if summary["status"] in {"ready", "no_artifacts"} else 2


if __name__ == "__main__":
raise SystemExit(main())
Empty file modified assets/sourceos/bin/turtle-agentctl
100644 → 100755
Empty file.
84 changes: 84 additions & 0 deletions assets/sourceos/tests/test_sourceos_term_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
REPO_ROOT = Path(__file__).resolve().parents[3]
SOURCEOS_WRAPPER = REPO_ROOT / "assets" / "sourceos" / "bin" / "sourceos-term"
TURTLE_WRAPPER = REPO_ROOT / "assets" / "sourceos" / "bin" / "turtle-term"
AGENT_STATUS = REPO_ROOT / "assets" / "sourceos" / "bin" / "turtle-agent-status"


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


def write_json(path: Path, data: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")


def run_agent_status(root: Path, expect_code: int) -> dict:
result = subprocess.run(
[sys.executable, str(AGENT_STATUS), "--root", str(root), "--json"],
cwd=str(REPO_ROOT),
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
assert result.returncode == expect_code, result.stderr
return json.loads(result.stdout)


def test_agent_status_no_artifacts() -> None:
with tempfile.TemporaryDirectory() as tmp:
summary = run_agent_status(Path(tmp), expect_code=0)
assert summary["schema"] == "sourceos.turtle.agent_status.v0"
assert summary["status"] == "no_artifacts"


def test_agent_status_blocked_by_guardrail() -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
decision_log = root / ".sourceos" / "logs" / "guardrail-decisions.jsonl"
decision_log.parent.mkdir(parents=True, exist_ok=True)
decision_log.write_text(
json.dumps({"schema": "sourceos.guardrail.decision.v0.1", "decisionId": "deny-1", "decision": "deny", "policyId": "sourceos/shell/block-privilege-escalation"}) + "\n",
encoding="utf-8",
)
summary = run_agent_status(root, expect_code=2)
assert summary["status"] == "blocked"
assert summary["guardrail"]["blocking"] == ["deny-1"]


def test_agent_status_needs_review_from_governance_queue() -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
queue = {
"apiVersion": "sociosphere.governance-queue/v1",
"kind": "AgentReliabilityGovernanceQueue",
"items": [
{
"itemId": "review-1",
"itemType": "memory-learning-review",
"status": "pending",
"priority": "medium",
"title": "Review learning proposal",
}
],
}
write_json(root / ".sourceos" / "governance" / "governance-queue.json", queue)
summary = run_agent_status(root, expect_code=2)
assert summary["status"] == "needs_review"
assert summary["governance"]["pending"][0]["itemId"] == "review-1"


def test_agent_status_ready_from_passing_artifacts() -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
write_json(
root / ".sourceos" / "logs" / "stop-gate-artifact.json",
{"kind": "StopGateArtifact", "gateId": "sourceos.default.agent-completion", "result": "pass"},
)
write_json(
root / ".sourceos" / "logs" / "invocations" / "s1" / "guarded-invocation-artifact.json",
{"kind": "GuardedInvocationArtifact", "workcellArtifactRef": "workcell-1", "result": "success"},
)
summary = run_agent_status(root, expect_code=0)
assert summary["status"] == "ready"
assert summary["stopGates"]["counts"] == {"pass": 1}
assert summary["invocations"]["counts"] == {"success": 1}


def main() -> int:
_, sourceos_session = run_wrapper(SOURCEOS_WRAPPER, "sourceos-term-test", "sourceos-test", "sourceos-smoke")
assert sourceos_session["frontend"] == "sourceos-term"

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

test_agent_status_no_artifacts()
test_agent_status_blocked_by_guardrail()
test_agent_status_needs_review_from_governance_queue()
test_agent_status_ready_from_passing_artifacts()

return 0


Expand Down
2 changes: 1 addition & 1 deletion assets/sourceos/tests/test_turtle_term_branding.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def main() -> int:

assert "class TurtleTerm < Formula" in formula
assert "class TurtleTerm < Formula" in template
assert "desc \"TurtleTerm: SourceOS policy-aware agent terminal fabric\"" in formula
assert "desc \"SourceOS policy-aware agent terminal fabric\"" in formula
assert "To launch TurtleTerm:" in formula
assert "turtleterm" in formula
assert "turtleterm.lua" in formula
Expand Down
4 changes: 3 additions & 1 deletion assets/sourceos/tests/test_turtle_term_release_readiness.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"assets/sourceos/bin/sourceos-term",
"assets/sourceos/bin/turtle-agentd",
"assets/sourceos/bin/turtle-agentctl",
"assets/sourceos/bin/turtle-agent-status",
"assets/sourceos/bin/turtle-tmux",
"assets/sourceos/bin/turtle-cloudfog",
"assets/sourceos/bin/turtle-superconscious",
Expand Down Expand Up @@ -88,10 +89,11 @@

REQUIRED_FORMULA_SNIPPETS = [
"class TurtleTerm < Formula",
"desc \"TurtleTerm: SourceOS policy-aware agent terminal fabric\"",
"desc \"SourceOS policy-aware agent terminal fabric\"",
"libexec/\"turtle-term\"",
"turtleterm",
"turtleterm.lua",
"turtle-agent-status",
"turtle-cloudfog",
"turtle-superconscious",
"turtle-agent-machine",
Expand Down
Loading
Loading