Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ A contestable, checkable public record for humans and AI.
| [Protocol Spec](./docs/protocol-spec.md) | Object types, schemas, state machines, transition rules |
| [Policy](./docs/policy.md) | Tunable parameters — budgets, thresholds, timing |
| [Technical Architecture](./docs/tech-architecture.md) | Implementation: what to build, how, and why |
| [Agent Shell Supervisor Receipt Example](./examples/agent-shell-supervisor/) | Supervisor-side receipt fixture for `google/agent-shell-tools#29` trust-boundary discussion |

## Status

Expand Down
67 changes: 67 additions & 0 deletions examples/agent-shell-supervisor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Agent Shell Supervisor Receipt Example

This example responds narrowly to the trust-boundary question in `google/agent-shell-tools#29`.

## Core position

`grpc_execd` is not the trusted observer.

In the `Agent outside the sandbox` composition, the agent may be able to run arbitrary commands through `grpc_execd`, and those commands may affect `grpc_execd` itself. A value returned by `grpc_execd` is therefore self-reported correlation data, not trustworthy third-party evidence.

The environment commitment should be reported by the component that owns or manages the execution environment: `wsb`, a wrapper that launches `wsb`, or a separate trust domain that can observe the request and response from outside `grpc_execd`.

The safer composition is:

1. `grpc_exec` stays minimal and returns execution results.
2. The environment owner computes the environment commitment.
3. A supervisor or observer signs a receipt that binds the command digest, working-directory digest, environment commitment, response digest, exit code, and observer identity.

## Useful trust states

| Context | Meaning |
| --- | --- |
| `self-reported` | The process that returned the value also reports the commitment. Useful for local correlation only. |
| `supervisor-attested` | A separate local supervisor observed the request and response and signed the receipt. Useful for stronger local audit. |
| `third-party-attested` | A separate trust domain observed or verified the event and signed the receipt. Useful for external verification. |

## Receipt boundary

The supervisor receipt binds:

| Field | Purpose |
| --- | --- |
| `command.digest` | Commits to the command and arguments. |
| `working_directory.digest` | Commits to the execution directory identity without exporting the full tree. |
| `environment.commitment` | Commits to an allowlisted environment snapshot created by the environment owner. |
| `environment.context` | States whether the commitment is self-reported, supervisor-attested, or third-party-attested. |
| `environment.owner` | Identifies the component that owns or manages the execution environment. |
| `response.digest` | Commits to stdout, stderr, exit code, and response metadata. |
| `observer.id` | Identifies the supervisor or observer. |
| `signature` | Signs the receipt from outside the controlled process. |

## Why not put this directly in `ExecuteResponse`

A raw `environment_commitment` in `ExecuteResponse` is easy to misread as proof. In the `Agent outside the sandbox` composition, it is only a statement from the process being controlled.

That does not make the value useless. It can still be useful for local correlation. It just should not be represented as evidence unless the receipt also says who observed it and which trust boundary produced it.

For Google Agent Shell, the cleanest design appears to be a sidecar receipt emitted by `wsb`, a `wsb` launcher wrapper, or another supervisor. The receipt can live beside the `grpc_exec` response without expanding the `grpc_exec` response shape.

## Files

- `receipt.schema.json`: JSON schema for the supervisor receipt shape.
- `make_sample.go`: deterministic sample generator using Go stdlib Ed25519.
- `sample-execute-receipt.json`: generated receipt with a real signature over deterministic canonical JSON.
- `REPLY-agent-shell-tools-29.md`: concise reply text for the GitHub issue.

## Regenerate sample

```bash
go run make_sample.go
```

Then run the repository-level example verifier:

```bash
npm run verify:examples
```
13 changes: 13 additions & 0 deletions examples/agent-shell-supervisor/REPLY-agent-shell-tools-29.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
That makes sense to me. I agree that `grpc_execd` is the wrong trust boundary for this.

The clean split seems to be:

1. `grpc_exec` returns execution results and stays minimal.
2. The environment owner, for example `//wsb` or a wrapper that launches/manages `//wsb`, computes the environment commitment.
3. A separate observer signs a receipt binding the command digest, response digest, exit code, environment commitment, and observer identity.

I tightened the example here: https://github.com/VeritasActa/Acta/pull/2

I will not pursue an `ExecuteResponse` field based on this discussion. If useful later, I can turn the sketch into a small `wsb`-side example or wrapper that emits a sidecar receipt, but I agree that it should not come from `grpc_execd` itself.

Happy to close this issue as out of scope for `grpc_exec`, or leave it open as a marker for a possible future `wsb`/supervisor-side experiment, whichever you prefer.
122 changes: 122 additions & 0 deletions examples/agent-shell-supervisor/make_sample.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package main

import (
"crypto/ed25519"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"sort"
"strings"
)

func digest(v any) string {
b := []byte(canonical(v))
sum := sha256.Sum256(b)
return "sha256:" + hex.EncodeToString(sum[:])
}

func canonical(v any) string {
switch x := v.(type) {
case map[string]any:
keys := make([]string, 0, len(x))
for k := range x {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
kb, _ := json.Marshal(k)
parts = append(parts, string(kb)+":"+canonical(x[k]))
}
return "{" + strings.Join(parts, ",") + "}"
case []any:
parts := make([]string, 0, len(x))
for _, item := range x {
parts = append(parts, canonical(item))
}
return "[" + strings.Join(parts, ",") + "]"
default:
b, _ := json.Marshal(v)
return string(b)
}
}

func withoutFields(r map[string]any, fields ...string) map[string]any {
omit := map[string]bool{}
for _, f := range fields {
omit[f] = true
}
out := make(map[string]any, len(r))
for k, v := range r {
if omit[k] {
continue
}
out[k] = v
}
return out
}

func main() {
seed := make([]byte, ed25519.SeedSize)
for i := range seed {
seed[i] = byte(i + 1)
}
priv := ed25519.NewKeyFromSeed(seed)
pub := priv.Public().(ed25519.PublicKey)

command := map[string]any{
"argv": []any{"python", "-m", "pytest", "tests/test_policy.py"},
"digest": digest(map[string]any{"argv": []any{"python", "-m", "pytest", "tests/test_policy.py"}}),
}
workdir := map[string]any{
"path_hint": "/workspace/project",
"digest": digest(map[string]any{"git_commit": "abc123", "path": "/workspace/project"}),
}
env := map[string]any{
"commitment": digest(map[string]any{"PATH": "/usr/bin:/bin", "PYTHONPATH": "tests"}),
"context": "supervisor-attested",
"owner": "wsb-launcher.local",
"allowlist": []any{"PATH", "PYTHONPATH"},
}
response := map[string]any{
"exit_code": 0,
"digest": digest(map[string]any{"stdout": "2 passed", "stderr": "", "exit_code": 0}),
}

receipt := map[string]any{
"acta_version": "0.1",
"receipt_type": "acta:exec-observation",
"subject": "grpc_exec.ExecuteResponse",
"observed_at": "2026-04-30T00:00:00Z",
"command": command,
"working_directory": workdir,
"environment": env,
"response": response,
"observer": map[string]any{
"id": "agent-shell-supervisor.local",
"trust_domain": "local-supervisor",
},
}
receipt["receipt_id"] = digest(receipt)

signable := withoutFields(receipt, "signature")
sig := ed25519.Sign(priv, []byte(canonical(signable)))
receipt["signature"] = map[string]any{
"alg": "Ed25519",
"kid": "did:key:example-agent-shell-supervisor",
"public_key": hex.EncodeToString(pub),
"sig": hex.EncodeToString(sig),
}

out, err := json.MarshalIndent(receipt, "", " ")
if err != nil {
panic(err)
}
out = append(out, '\n')
if err := os.WriteFile("sample-execute-receipt.json", out, 0o644); err != nil {
panic(err)
}
fmt.Println("wrote sample-execute-receipt.json")
}
85 changes: 85 additions & 0 deletions examples/agent-shell-supervisor/receipt.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/agent-shell-supervisor-receipt.schema.json",
"title": "Agent Shell Supervisor Execution Receipt",
"type": "object",
"required": [
"acta_version",
"receipt_type",
"receipt_id",
"subject",
"observed_at",
"command",
"working_directory",
"environment",
"response",
"observer",
"signature"
],
"properties": {
"acta_version": { "type": "string" },
"receipt_type": { "const": "acta:exec-observation" },
"receipt_id": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
"subject": { "const": "grpc_exec.ExecuteResponse" },
"observed_at": { "type": "string", "format": "date-time" },
"command": {
"type": "object",
"required": ["argv", "digest"],
"properties": {
"argv": { "type": "array", "items": { "type": "string" } },
"digest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
},
"additionalProperties": false
},
"working_directory": {
"type": "object",
"required": ["path_hint", "digest"],
"properties": {
"path_hint": { "type": "string" },
"digest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
},
"additionalProperties": false
},
"environment": {
"type": "object",
"required": ["commitment", "context", "owner", "allowlist"],
"properties": {
"commitment": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
"context": { "enum": ["self-reported", "supervisor-attested", "third-party-attested"] },
"owner": { "type": "string", "minLength": 1 },
"allowlist": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": false
},
"response": {
"type": "object",
"required": ["exit_code", "digest"],
"properties": {
"exit_code": { "type": "integer" },
"digest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
},
"additionalProperties": false
},
"observer": {
"type": "object",
"required": ["id", "trust_domain"],
"properties": {
"id": { "type": "string" },
"trust_domain": { "type": "string" }
},
"additionalProperties": false
},
"signature": {
"type": "object",
"required": ["alg", "kid", "public_key", "sig"],
"properties": {
"alg": { "const": "Ed25519" },
"kid": { "type": "string" },
"public_key": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"sig": { "type": "string", "pattern": "^[a-f0-9]{128}$" }
},
"additionalProperties": false
}
},
"additionalProperties": false
}
43 changes: 43 additions & 0 deletions examples/agent-shell-supervisor/sample-execute-receipt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"acta_version": "0.1",
"command": {
"argv": [
"python",
"-m",
"pytest",
"tests/test_policy.py"
],
"digest": "sha256:d8ad885e20e03f5d1180eb01b4bdfe2075eecec31232daed47b6ee758a90acaf"
},
"environment": {
"allowlist": [
"PATH",
"PYTHONPATH"
],
"commitment": "sha256:ec9c819d304bd0e353373645303d9d5fc29cb9bf584a68d48a2657e04b49222d",
"context": "supervisor-attested",
"owner": "wsb-launcher.local"
},
"observed_at": "2026-04-30T00:00:00Z",
"observer": {
"id": "agent-shell-supervisor.local",
"trust_domain": "local-supervisor"
},
"receipt_id": "sha256:191f26ad301c198e3e1e9c3dca74aaabbd0010fb5efbd1a8517f91ffddf77b84",
"receipt_type": "acta:exec-observation",
"response": {
"digest": "sha256:0aca7e06260b79b4f15791b83f17775f1538d9fcfb4c08b56718a653e185ec93",
"exit_code": 0
},
"signature": {
"alg": "Ed25519",
"kid": "did:key:example-agent-shell-supervisor",
"public_key": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664",
"sig": "c8126d965ebb0b179b65e17d10d388c0e4320ca708dc43cba2676d4e69d4d5ddaf60adaee5d5d0773bc724ce4d8c58556f2becf463c98dbb2bd519045f27c106"
},
"subject": "grpc_exec.ExecuteResponse",
"working_directory": {
"digest": "sha256:74b92206b71369e383d267a2c7f1031549d4568771aacc44db4c41993ccdc59e",
"path_hint": "/workspace/project"
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"test": "vitest",
"verify": "node tools/verify.js"
"verify": "node tools/verify.js",
"verify:examples": "node tools/verify-examples.js"
},
"dependencies": {
"@noble/curves": "^1.9.7"
Expand Down
Loading