Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
137 changes: 137 additions & 0 deletions packages/agentmesh-integrations/sb-runtime-skill/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# sb-runtime + AgentMesh Governance Skill

**Public Preview.** Governance skill that evaluates policy and emits Ed25519-signed decision receipts in the [Veritas Acta receipt format](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/). Parallel to `openshell-skill`: same policy contract, drop-in replacement at the governance layer, with receipts added.

> sb-runtime is one implementation of the Veritas Acta receipt format. This skill is its AgentMesh integration point. See [docs/integrations/sb-runtime.md](../../../docs/integrations/sb-runtime.md) for the architecture overview and for guidance on composing with [nono](https://github.com/always-further/nono) as the Linux sandbox primitive.

## Install

```bash
pip install sb-runtime-agentmesh
```

## What the skill adds over `openshell-skill`

| Capability | openshell-skill | sb-runtime-skill |
|---|:---:|:---:|
| YAML policy loading | yes | yes (same schema) |
| Trust score tracking | yes | yes |
| Audit log | yes | yes |
| Ed25519-signed decision receipts | no | yes |
| Receipt chain linkage (`previousReceiptHash`) | no | yes |
| Policy digest pinned into receipts | no | yes (`sha256:...`) |
| Sandbox backend recorded in receipt | no | yes (`nono` \| `openshell` \| `sb_runtime_builtin` \| `none`) |
| Offline verification (`@veritasacta/verify`) | no | yes |

## Usage

### As a library

```python
from pathlib import Path
from sb_runtime_agentmesh import GovernanceSkill, SandboxBackend

skill = GovernanceSkill(
policy_dir=Path("./policies"),
sandbox_backend=SandboxBackend.NONO, # wrap the agent in a nono sandbox
ring=2, # sb-runtime does policy + receipts; nono does the sandbox
)

decision = skill.check_policy(
action="shell:curl https://api.github.com/repos/org/repo/issues",
context={"agent_did": "did:agent:researcher-1"},
)

if decision.allowed:
# Ring 2: execute inside the nono capability set
...

# The signed receipt is on decision.receipt - Veritas Acta format.
import json
print(json.dumps(decision.receipt, indent=2))
```

### As a CLI

```bash
# Generate an operator key once
python -c "from sb_runtime_agentmesh import Signer; print(Signer.generate().private_pem().decode())" > operator.pem

# Evaluate policy, sign receipt, write to disk
sb-runtime-governance check-policy \
--action shell:python \
--policy-dir ./policies \
--sandbox-backend nono \
--ring 2 \
--key operator.pem \
--receipts-dir ./receipts

# Verify a written receipt
sb-runtime-governance verify ./receipts/20260419T133001123456Z.json \
--public-key operator-public.pem
```

## Deployment modes

### Standalone sb-runtime (Ring 3)

One binary owns everything: Cedar evaluation, Landlock + seccomp sandbox, receipt signing. Set `sandbox_backend=SandboxBackend.SB_RUNTIME_BUILTIN` and `ring=3`.

### sb-runtime + nono composition (recommended on Linux)

nono owns the sandbox layer; this skill contributes only Cedar + signed receipts. Set `sandbox_backend=SandboxBackend.NONO` and `ring=2`. Wrap your agent process in nono externally:

```bash
nono run --policy ./nono-capabilities.yaml -- \
python -m your_agent # calls the skill in-process
```

The receipt's `payload.sandbox_backend == "nono"` field makes the composition visible to auditors.

### sb-runtime + OpenShell composition

OpenShell owns the container boundary; this skill contributes Cedar + receipts. Set `sandbox_backend=SandboxBackend.OPENSHELL` and `ring=2`.

## Receipt format

Every decision produces an envelope of the form:

```json
{
"payload": {
"type": "sb-runtime:decision",
"agent_id": "did:agent:researcher-1",
"action": "shell:python",
"decision": "allow",
"ring": 2,
"sandbox_backend": "nono",
"policy_id": "allow-shell",
"policy_digest": "sha256:...",
"trust_score": 1.0,
"issuer_id": "sb:issuer:...",
"issued_at": "2026-04-19T13:30:01.123Z",
"previousReceiptHash": "..."
},
"signature": {
"alg": "EdDSA",
"kid": "...",
"sig": "..."
}
}
```

The canonical form is JCS-RFC 8785 with ASCII-only keys per [AIP-0001](https://github.com/VeritasActa/Acta/blob/main/specs/aip/AIP-0001.md). Verification does not depend on this skill, sb-runtime, or AgentMesh:

```bash
npx @veritasacta/verify receipt.json --key operator-public.pem
```

## Spec alignment

- [draft-farley-acta-signed-receipts-02](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)
- [AIP-0001](https://github.com/VeritasActa/Acta) (receipt format, ASCII-only JCS)
- [VeritasActa/agt-integration-profile](https://github.com/VeritasActa/agt-integration-profile) (AGT to Veritas Acta normative field mapping)

## License

MIT. See `LICENSE` at the repo root.
35 changes: 35 additions & 0 deletions packages/agentmesh-integrations/sb-runtime-skill/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "sb_runtime_agentmesh"
version = "0.1.0"
description = "Public Preview - sb-runtime governance skill with signed receipts (Veritas Acta format)"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [{ name = "Tom Farley (ScopeBlind)", email = "tommy@scopeblind.com" }]
dependencies = [
"pyyaml>=6.0,<7.0",
"cryptography>=41.0,<47.0",
]

[project.optional-dependencies]
agentmesh = ["agentmesh-platform>=3.0,<4.0"]
dev = ["pytest>=7.0"]

[project.urls]
Homepage = "https://github.com/microsoft/agent-governance-toolkit"
Documentation = "https://github.com/microsoft/agent-governance-toolkit/blob/main/docs/integrations/sb-runtime.md"
"sb-runtime source" = "https://github.com/ScopeBlind/sb-runtime"
"Veritas Acta draft" = "https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/"

[project.scripts]
sb-runtime-governance = "sb_runtime_agentmesh.cli:main"

[tool.hatch.build.targets.wheel]
packages = ["sb_runtime_agentmesh"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright (c) 2026 Tom Farley (ScopeBlind).
# Licensed under the MIT License.
"""sb-runtime governance skill: policy evaluation + Ed25519-signed decision receipts (Veritas Acta format)."""

from sb_runtime_agentmesh.skill import GovernanceSkill, PolicyDecision, SandboxBackend
from sb_runtime_agentmesh.receipts import Signer, sign_receipt, verify_receipt

__all__ = [
"GovernanceSkill",
"PolicyDecision",
"SandboxBackend",
"Signer",
"sign_receipt",
"verify_receipt",
]
__version__ = "0.1.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright (c) 2026 Tom Farley (ScopeBlind).
# Licensed under the MIT License.
"""CLI entry point for the sb-runtime governance skill."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

from sb_runtime_agentmesh.receipts import Signer, verify_receipt
from sb_runtime_agentmesh.skill import GovernanceSkill, SandboxBackend


def _load_signer(key_path: Path | None) -> Signer:
if key_path is None:
return Signer.generate()
return Signer.from_pem(key_path.read_bytes())


def main(argv=None) -> int:
parser = argparse.ArgumentParser(prog="sb-runtime-governance")
sub = parser.add_subparsers(dest="command")

cp = sub.add_parser(
"check-policy",
help="Evaluate policy and emit a signed decision receipt",
)
cp.add_argument("--action", required=True)
cp.add_argument("--context", default="{}")
cp.add_argument("--policy-dir", required=True)
cp.add_argument(
"--sandbox-backend",
choices=[b.value for b in SandboxBackend],
default=SandboxBackend.SB_RUNTIME_BUILTIN.value,
)
cp.add_argument("--ring", type=int, default=3)
cp.add_argument("--key", type=Path, default=None, help="Operator Ed25519 key (PEM)")
cp.add_argument("--no-sign", action="store_true", help="Skip receipt signing")
cp.add_argument(
"--receipts-dir",
type=Path,
default=None,
help="If set, write the signed receipt to <receipts-dir>/<timestamp>.json",
)

ts = sub.add_parser("trust-score")
ts.add_argument("--agent-did", required=True)

vf = sub.add_parser("verify", help="Verify a receipt file against an Ed25519 public key (PEM)")
vf.add_argument("receipt", type=Path)
vf.add_argument("--public-key", type=Path, required=True)

pk = sub.add_parser("public-key", help="Print operator public key (PEM)")
pk.add_argument("--key", type=Path, default=None)

args = parser.parse_args(argv)

if args.command == "check-policy":
signer = _load_signer(args.key)
skill = GovernanceSkill(
policy_dir=Path(args.policy_dir),
signer=signer,
sandbox_backend=SandboxBackend(args.sandbox_backend),
ring=args.ring,
)
ctx = json.loads(args.context) if args.context else {}
decision = skill.check_policy(args.action, ctx, sign=not args.no_sign)
output = {
"allowed": decision.allowed,
"action": decision.action,
"reason": decision.reason,
"policy_name": decision.policy_name,
"policy_digest": decision.policy_digest,
"ring": decision.ring,
"sandbox_backend": decision.sandbox_backend.value,
"receipt": decision.receipt,
}
if args.receipts_dir and decision.receipt is not None:
args.receipts_dir.mkdir(parents=True, exist_ok=True)
from datetime import datetime, timezone

stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
out_path = args.receipts_dir / f"{stamp}.json"
out_path.write_text(json.dumps(decision.receipt, indent=2, sort_keys=True))
output["receipt_path"] = str(out_path)
print(json.dumps(output, indent=2, sort_keys=True))
return 0 if decision.allowed else 1

if args.command == "trust-score":
skill = GovernanceSkill()
print(
json.dumps(
{
"agent_did": args.agent_did,
"trust_score": skill.get_trust_score(args.agent_did),
}
)
)
return 0

if args.command == "verify":
from cryptography.hazmat.primitives import serialization

pub = serialization.load_pem_public_key(args.public_key.read_bytes())
envelope = json.loads(args.receipt.read_text())
ok = verify_receipt(envelope, pub)
print(json.dumps({"verified": ok, "kid": envelope.get("signature", {}).get("kid")}))
return 0 if ok else 1

if args.command == "public-key":
signer = _load_signer(args.key)
sys.stdout.write(signer.public_pem().decode("ascii"))
return 0

parser.print_help()
return 1


if __name__ == "__main__":
sys.exit(main())
Loading
Loading