diff --git a/README.md b/README.md index 223975f..786bb5b 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,11 @@ The core pattern is platform/security architecture for autonomous agents: treat > is illustrative (`example-network`, `platform-host`, `agent-runtime`). Nothing points at real > infrastructure, and no secrets are present — secret *references* only. The repository proves a > reproducible lab skeleton and executable validation checks, not a hosted managed product. +> +> Public/private release boundaries and example evidence are documented in +> [`docs/public-private-boundary.md`](docs/public-private-boundary.md), +> [`docs/declassification-checklist.md`](docs/declassification-checklist.md), and +> [`docs/evidence-model.md`](docs/evidence-model.md). ## Architecture at a glance @@ -207,6 +212,8 @@ and [`docs/evidence/substrate-validation-receipt.md`](docs/evidence/substrate-va | `control-plane/` | Promotion / status / rollback scripts — **dry-run by default**, `--apply` to act. | | `deploy/agent-gateway/` | The templated multi-profile gateway (unit + launcher + example envs). | | `examples/` | Illustrative manifests and state-as-truth files (not live). | +| `examples/reference-cell/` | Fake policy and evidence examples for the public reference cell. | +| `scripts/public-safety-scan` | Local scan for common public-boundary mistakes in a branch diff. | ## Design principles diff --git a/docs/declassification-checklist.md b/docs/declassification-checklist.md new file mode 100644 index 0000000..85c72e2 --- /dev/null +++ b/docs/declassification-checklist.md @@ -0,0 +1,51 @@ +# Declassification Checklist + +Use this checklist before publishing public docs, examples, scripts, release notes, or pull request +text from `BoundaryKit`. + +## Required Result + +The public diff must be explainable as a standalone reference architecture. It must not require, imply, +or reveal a private deployment. + +## Content Checks + +- [ ] All examples are marked `example_only`, `illustrative`, or clearly fake. +- [ ] No private source code, image layers, SBOMs, manifests, evidence logs, task ledgers, raw + transcripts, screenshots, or operator approvals are copied into the public repo. +- [ ] No real hostnames, IP addresses, registry paths, source paths, service names, client names, + topology, runtime paths, service units, tunnels, DNS settings, or deploy state appear. +- [ ] No credentials, private key material, token values, token ids, token hashes, signing key paths, + `.env` file contents, or secret values appear. +- [ ] Secret examples use references only, such as `secret://demo/...`. +- [ ] Artifact examples use fake registries and fake digests. +- [ ] Tool examples use fake tool names and fake schema hashes. +- [ ] Evidence examples are fake receipts, not excerpts from real logs. +- [ ] Public text does not claim that a private system, client, or workload was validated. + +## Governance Checks + +- [ ] The diff frames agents as untrusted, tool-wielding workloads. +- [ ] Trust boundaries are explicit. +- [ ] Capability escalation is gated. +- [ ] Promotion requires evidence. +- [ ] Rollback is represented as a tested drill. +- [ ] Audit records omit secret values. +- [ ] Runtime mutation, public image publishing, and registry pushes remain separate approval gates. + +## Scan And Review + +Run: + +```bash +scripts/public-safety-scan +git diff --check +``` + +Then review the diff manually. Treat scanner success as necessary but not sufficient: it does not +prove conceptual safety. + +## Stop Conditions + +Stop and rewrite before publication if any item is uncertain. Public examples should be regenerated +from fake values rather than edited down from private evidence. diff --git a/docs/evidence-model.md b/docs/evidence-model.md new file mode 100644 index 0000000..40aa588 --- /dev/null +++ b/docs/evidence-model.md @@ -0,0 +1,54 @@ +# Evidence Model + +`BoundaryKit` uses evidence to keep production claims honest. A workload is not ready because a file +exists or a command was intended to run; it is ready only when current evidence proves the gate. + +This document describes the public reference model. Example receipts in this repo are fake and marked +`example_only`. + +## Evidence Principles + +- Evidence is observed, not invented. +- A receipt records exact inputs, commands, outputs, and stop conditions. +- Secret values are never evidence; only secret references or redacted identifiers may appear. +- A failing receipt is still useful if it preserves the exact failure. +- Promotion gates advance only after evidence proves the gate. +- Rollback evidence must be captured before relying on promotion. + +## Common Receipt Fields + +| Field | Purpose | +|---|---| +| `status` | gate state, such as `example_only_pass`, `blocked`, or `failed` | +| `scope` | what the receipt does and does not authorize | +| `artifact` | digest-pinned fake or real artifact reference, depending on repo context | +| `source` | source revision or synthetic example revision | +| `policy` | policy file and hash, if applicable | +| `sbom` | SBOM path and checksum, if generated | +| `signature` | signing and verification result | +| `validation` | commands run and observed result | +| `audit` | audit sink or local example log result | +| `rollback` | rollback target and drill evidence | +| `boundary` | actions not performed, such as no push, no deploy, or no traffic shift | +| `risks` | unresolved issues and review questions | + +## Gate Evidence + +| Gate | Evidence required | +|---|---| +| Build | artifact digest, source revision, build command, checksum or image id | +| SBOM | generated SBOM path and checksum | +| Signature | signature command result and verification result | +| Policy | tool allowlist, denied tools absent, schema hash behavior, fail-closed behavior | +| Egress | default deny proof and audit event | +| Deploy-only canary | health, readiness, running digest matches manifest | +| Read-only canary | expected read behavior, denied write/tool behavior, audit trail | +| Rollback | target, command, elapsed time, post-rollback health | +| Promotion | evidence packet review and explicit decision | + +## Fake Evidence In This Repo + +Files under `examples/reference-cell/evidence/` demonstrate receipt shape only. They use fake +registries, fake digests, fake signatures, fake checksums, and synthetic command output. + +Do not treat public example receipts as proof of any private deployment. diff --git a/docs/public-private-boundary.md b/docs/public-private-boundary.md new file mode 100644 index 0000000..6cdbeb5 --- /dev/null +++ b/docs/public-private-boundary.md @@ -0,0 +1,83 @@ +# Public / Private Boundary + +`BoundaryKit` is a public reference architecture for governed, untrusted AI-agent workloads. It is meant +to show the control pattern: trust boundaries, explicit capability contracts, promotion gates, +rollback, auditability, and evidence-backed operations. + +It is not a copy of any private operational deployment. Public material must be newly written, +illustrative, and safe to inspect without access to private infrastructure. + +## Public Reference Material + +Public material may include: + +- generic architecture docs for isolation, promotion, gateway layout, and governance; +- toy workloads that run with fake tools and local-only behavior; +- fake policies with placeholder tools, fake schema hashes, and example-only secret references; +- fake evidence receipts that show the shape of build/sign/verify, rollback, and audit records; +- scripts that scan public diffs for common leak classes; +- diagrams with placeholder hosts, clients, registries, and audit sinks. + +Public examples should use obviously fictional values, such as: + +- `demo-client` +- `demo-registry.example` +- `audit.demo.invalid` +- `secret://demo/...` +- all-zero or clearly fake `sha256:` digests +- synthetic commit ids such as `0000000` + +## Private Operational Material + +Private operational repos remain the place for: + +- real source code for private services; +- real container images, SBOMs, signatures, logs, manifests, receipts, and task ledgers; +- real hostnames, IP addresses, registry paths, source paths, service names, client names, and + topology; +- real runtime configuration, service units, tunnels, DNS details, and deploy state; +- real credentials, key paths, token ids, token hashes, signing details, and secret references tied + to actual systems; +- raw transcripts, operator approvals, runtime truth, incident notes, and audit data. + +Do not sanitize these artifacts in place and publish them. Recreate public examples from scratch with +fake values. + +## Trust Boundaries + +The public architecture treats every agent as an untrusted, tool-wielding workload. The host-side +operator and promotion control plane are higher-trust domains; agent runtimes are lower-trust domains; +high-risk work moves to stronger isolation. + +The public examples should preserve that shape: + +- operators approve mutations; +- control-plane scripts are dry-run by default; +- manifests pin immutable artifacts; +- tool access is an allowlist; +- denied tools are absent from discovery; +- egress defaults to deny; +- secrets are references, not values; +- audit records are evidence, not decoration; +- rollback is proven before promotion. + +## Promotion Boundary + +The public repo may document a promotion flow, but it must not imply that example files are production +evidence. A public example can show the receipt format; it cannot claim that a private deployment was +validated. + +Allowed public progression: + +```text +example_only lab -> deploy-only example -> read-only canary example -> rollback example -> promotion decision example +``` + +Real deployment, registry publication, traffic movement, or production promotion belong outside this +public reference repo unless a separate public release gate explicitly approves them. + +## Release Rule + +Before publishing a branch, run the public safety scan and perform a human declassification review. +The scan catches common string and secret classes. Human review decides whether the concepts, +examples, and claims are safe. diff --git a/examples/reference-cell/evidence/demo-build-sign-receipt.md b/examples/reference-cell/evidence/demo-build-sign-receipt.md new file mode 100644 index 0000000..e196ee9 --- /dev/null +++ b/examples/reference-cell/evidence/demo-build-sign-receipt.md @@ -0,0 +1,50 @@ +# Demo Build / Sign Receipt + +Status: `example_only_pass` + +This receipt is fake. It demonstrates the evidence shape for a public reference cell and does not +describe a real build, registry push, signature, SBOM, deployment, or private workload. + +## Scope + +Allowed in this example: + +- local toy workload build; +- fake SBOM checksum; +- fake signature verification result; +- fake policy validation result. + +Not authorized by this example: + +- public registry push; +- demo image publication; +- production deploy; +- traffic shift; +- runtime mutation. + +## Example Artifact + +| Field | Example value | +|---|---| +| Workload | `boundarykit-reference-orchestrator` | +| Source revision | `0000000-example` | +| Image | `demo-registry.example/boundarykit-reference-orchestrator@sha256:0000000000000000000000000000000000000000000000000000000000000000` | +| SBOM | `examples/reference-cell/evidence/demo-sbom.spdx.json` | +| SBOM SHA256 | `0000000000000000000000000000000000000000000000000000000000000000` | +| Signature mode | `example-only local test key` | +| Verification result | `example_only: claims validated and signature verified` | + +## Example Validation + +```text +example_only$ validate-demo-tool-policy examples/reference-cell/policies/demo-tool-policy.yaml +PASS example_only: allowed=3 denied_absent=3 fail_closed=true + +example_only$ validate-demo-egress examples/reference-cell/policies/demo-egress-policy.yaml +PASS example_only: default=deny audit_on_deny=true +``` + +## Boundary + +No real registry, credential, signing key path, SBOM, log, service name, runtime path, deployment, or +operator approval is represented here. diff --git a/examples/reference-cell/evidence/demo-rollback-receipt.md b/examples/reference-cell/evidence/demo-rollback-receipt.md new file mode 100644 index 0000000..b5ff7f4 --- /dev/null +++ b/examples/reference-cell/evidence/demo-rollback-receipt.md @@ -0,0 +1,32 @@ +# Demo Rollback Receipt + +Status: `example_only_pass` + +This receipt is fake. It demonstrates the public evidence shape for rollback and does not describe a +real service, host, endpoint, client, runtime, or deployment. + +## Example Rollback Target + +| Field | Example value | +|---|---| +| Workload | `boundarykit-reference-orchestrator` | +| Previous artifact | `demo-registry.example/boundarykit-reference-orchestrator@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | +| Candidate artifact | `demo-registry.example/boundarykit-reference-orchestrator@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb` | +| Rollback command | `example_only rollback-reference-cell --target previous` | +| Elapsed time | `42s example_only` | +| Health after rollback | `healthy example_only` | +| Audit event | `rollback.executed example_only` | + +## Example Observations + +```text +example_only 00:00 rollback requested by demo-operator +example_only 00:10 previous digest selected +example_only 00:28 health check passed +example_only 00:42 rollback receipt closed +``` + +## Boundary + +No live endpoint, service unit, client configuration, traffic shift, production incident, or private +runtime state is represented here. diff --git a/examples/reference-cell/policies/demo-egress-policy.yaml b/examples/reference-cell/policies/demo-egress-policy.yaml new file mode 100644 index 0000000..0e1a3c4 --- /dev/null +++ b/examples/reference-cell/policies/demo-egress-policy.yaml @@ -0,0 +1,20 @@ +# example_only: fake egress policy for a public reference cell. +policy_version: 1 +workload: boundarykit-reference-orchestrator +default: deny +allow: + - name: demo-audit-sink + purpose: append_only_audit + destination: https://audit.demo.invalid/ingest + methods: [POST] +deny_examples: + - name: arbitrary-internet + destination: https://example.invalid/ + expected_audit_event: egress.denied + - name: model-provider-direct + destination: https://model-api.demo.invalid/ + expected_audit_event: egress.denied +audit: + on_allow: true + on_deny: true + include_secret_values: false diff --git a/examples/reference-cell/policies/demo-secret-refs.yaml b/examples/reference-cell/policies/demo-secret-refs.yaml new file mode 100644 index 0000000..599f28f --- /dev/null +++ b/examples/reference-cell/policies/demo-secret-refs.yaml @@ -0,0 +1,16 @@ +# example_only: fake secret-reference manifest for a public reference cell. +policy_version: 1 +workload: boundarykit-reference-orchestrator +secrets: + - name: demo-readonly-token + ref: secret://demo/reference-cell/readonly-token + mount: file + exposes_value_in_manifest: false + - name: demo-audit-sink-token + ref: secret://demo/reference-cell/audit-sink-token + mount: env_ref + exposes_value_in_manifest: false +rules: + inline_secret_values_allowed: false + log_secret_values_allowed: false + rotate_before_public_demo: true diff --git a/examples/reference-cell/policies/demo-tool-policy.yaml b/examples/reference-cell/policies/demo-tool-policy.yaml new file mode 100644 index 0000000..5f5ecb5 --- /dev/null +++ b/examples/reference-cell/policies/demo-tool-policy.yaml @@ -0,0 +1,31 @@ +# example_only: fake tool policy for a public reference cell. +policy_version: 1 +workload: boundarykit-reference-orchestrator +risk_tier: L1-internal-read-only +allowed_tools: + - name: demo_context_list + schema_hash: "sha256:1111111111111111111111111111111111111111111111111111111111111111" + risk: L1 + side_effects: none + - name: demo_context_get + schema_hash: "sha256:2222222222222222222222222222222222222222222222222222222222222222" + risk: L1 + side_effects: none + - name: demo_status_read + schema_hash: "sha256:3333333333333333333333333333333333333333333333333333333333333333" + risk: L1 + side_effects: none +denied_tools_absent_from_list: + - demo_message_send + - demo_secret_read_value + - demo_shell_exec +forbidden_capability_classes: + - terminal + - file_mutation + - browser_control + - payment + - external_dispatch +fail_closed: + unknown_allowed_tool: true + schema_hash_drift_blocks: true + policy_load_failure_refuses_to_serve: true diff --git a/scripts/public-safety-scan b/scripts/public-safety-scan new file mode 100755 index 0000000..4ec1b62 --- /dev/null +++ b/scripts/public-safety-scan @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""Scan a public diff for common declassification mistakes. + +The default mode scans changed and untracked files only. Pass paths to scan a +specific set of files or directories. +""" + +from __future__ import annotations + +import os +import pathlib +import re +import subprocess +import sys + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SELF = pathlib.Path(__file__).resolve() + +TEXT_SUFFIXES = { + ".md", + ".txt", + ".yaml", + ".yml", + ".json", + ".py", + ".sh", + ".service", + ".toml", +} + +SECRET_PATTERNS = [ + ("private-key-block", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")), + ("openai-style-token", re.compile(r"\bsk-[A-Za-z0-9_-]{20,}\b")), + ("github-token", re.compile(r"\bghp_[A-Za-z0-9_]{20,}\b")), + ("github-pat", re.compile(r"\bgithub_pat_[A-Za-z0-9_]{20,}\b")), + ("aws-access-key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")), + ( + "bearer-token-value", + re.compile(r"\bAuthorization:\s*Bearer\s+[A-Za-z0-9._~+/-]{16,}", re.IGNORECASE), + ), +] + +PATH_PATTERNS = [ + ("absolute-user-home", re.compile(r"(? list[str]: + try: + result = subprocess.run( + ["git", *args], + cwd=ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError: + return [] + return [line for line in result.stdout.splitlines() if line.strip()] + + +def default_paths() -> list[pathlib.Path]: + changed = run_git(["diff", "--name-only", "--diff-filter=ACMRT", "HEAD", "--"]) + untracked = run_git(["ls-files", "--others", "--exclude-standard"]) + paths = [] + for item in changed + untracked: + p = (ROOT / item).resolve() + if p.exists(): + paths.append(p) + return sorted(set(paths)) + + +def expand_paths(args: list[str]) -> list[pathlib.Path]: + if not args: + return default_paths() + + found: list[pathlib.Path] = [] + for arg in args: + p = (ROOT / arg).resolve() + if p.is_dir(): + for child in p.rglob("*"): + if child.is_file(): + found.append(child) + elif p.exists(): + found.append(p) + return sorted(set(found)) + + +def is_text_file(path: pathlib.Path) -> bool: + if path.resolve() == SELF: + return False + if ".git" in path.parts: + return False + if path.suffix not in TEXT_SUFFIXES and path.name not in {"README", "LICENSE"}: + return False + try: + chunk = path.read_bytes()[:4096] + except OSError: + return False + return b"\0" not in chunk + + +def rel(path: pathlib.Path) -> str: + return str(path.resolve().relative_to(ROOT)) + + +def has_placeholder(line: str) -> bool: + lower = line.lower() + return any(word in lower for word in PLACEHOLDER_WORDS) + + +def scan_file(path: pathlib.Path) -> list[tuple[str, int, str]]: + findings: list[tuple[str, int, str]] = [] + try: + lines = path.read_text(encoding="utf-8").splitlines() + except UnicodeDecodeError: + lines = path.read_text(encoding="utf-8", errors="replace").splitlines() + + relative = rel(path) + text = "\n".join(lines) + + if relative.startswith("examples/reference-cell/") and "example_only" not in text: + findings.append(("missing-example-only-marker", 1, relative)) + + for lineno, line in enumerate(lines, start=1): + for name, pattern in SECRET_PATTERNS + PATH_PATTERNS + BROAD_MOUNT_PATTERNS: + if pattern.search(line): + findings.append((name, lineno, line.strip())) + + match = INLINE_SECRET.search(line) + if match and not has_placeholder(line): + findings.append(("inline-secret-like-value", lineno, line.strip())) + + image = CONTAINER_REF.search(line) + if image and not has_placeholder(image.group(1)): + findings.append(("non-placeholder-image-reference", lineno, line.strip())) + + return findings + + +def main() -> int: + paths = [p for p in expand_paths(sys.argv[1:]) if is_text_file(p)] + findings: list[tuple[str, str, int, str]] = [] + + for path in paths: + for name, lineno, detail in scan_file(path): + findings.append((rel(path), name, lineno, detail)) + + if findings: + print("public-safety-scan: FAIL") + for path, name, lineno, detail in findings: + print(f"{path}:{lineno}: {name}: {detail}") + return 1 + + print(f"public-safety-scan: PASS ({len(paths)} text files scanned)") + return 0 + + +if __name__ == "__main__": + os.chdir(ROOT) + raise SystemExit(main())