agents-handoff 0.0.0-stage → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/LICENSE +21 -0
- package/README.md +110 -2
- package/SKILL.md +147 -0
- package/capability-registry.json +27 -0
- package/docs/ARCHITECTURE.md +164 -0
- package/docs/CHANGELOG.md +151 -0
- package/docs/CLI.md +196 -0
- package/docs/COMPATIBILITY.md +124 -0
- package/docs/CONTRIBUTING.md +134 -0
- package/docs/FORMAT.md +157 -0
- package/docs/INSTALL.md +179 -0
- package/docs/INTEGRATION.md +188 -0
- package/docs/LEVEL4.md +202 -0
- package/docs/LEVEL5.md +96 -0
- package/docs/PERMISSIONS.md +145 -0
- package/docs/PROVENANCE.md +83 -0
- package/docs/SECURITY.md +93 -0
- package/docs/SESSIONS.md +66 -0
- package/docs/TROUBLESHOOTING.md +158 -0
- package/docs/UNINSTALL.md +122 -0
- package/docs/UPGRADE.md +139 -0
- package/docs/_config.yml +16 -0
- package/docs/_data/nav.yml +36 -0
- package/docs/_layouts/default.html +31 -0
- package/docs/assets/style.css +88 -0
- package/docs/index.md +83 -0
- package/handoff.config.example.json +35 -0
- package/handoff.config.schema.json +117 -0
- package/install/CHANGELOG.md +48 -0
- package/install/README.md +76 -0
- package/install/install.mjs +856 -0
- package/install/package.json +39 -0
- package/package.json +66 -4
- package/permission-policy.json +33 -0
- package/refs/ADAPTERS.md +33 -0
- package/refs/bootstrap.md +59 -0
- package/refs/brief-checklist.md +79 -0
- package/refs/handbook.md +58 -0
- package/refs/protocol.md +117 -0
- package/refs/roles.md +75 -0
- package/refs/validator.md +73 -0
- package/schemas/handoff.schema.json +275 -0
- package/skill.json +147 -0
- package/templates/HANDOFF.llm.schema.json +144 -0
- package/templates/HANDOFF.template.md +40 -0
- package/tests/acceptance/acceptance.yaml +209 -0
- package/tests/fixtures/minimal-transcript.jsonl +2 -0
- package/tools/agent-handoff.mjs +410 -0
- package/tools/capability-registry.mjs +120 -0
- package/tools/handoff.mjs +398 -0
- package/tools/handoff.test.mjs +465 -0
- package/tools/lib/handoff-root.mjs +161 -0
- package/tools/runtime-engine.mjs +330 -0
package/docs/LEVEL4.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Level 4 — the dynamic runtime layer
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Level 4 — the dynamic runtime layer
|
|
6
|
+
|
|
7
|
+
The engine ([handoff.mjs](https://github.com/Alot1z/agent-handoff/blob/main/tools/handoff.mjs)) is passive: something has to invoke it. The
|
|
8
|
+
runtime layer is [tools/agent-handoff.mjs](https://github.com/Alot1z/agent-handoff/blob/main/tools/agent-handoff.mjs), which acts on the
|
|
9
|
+
state of the store — it probes for staleness, checks a handoff against a contract, composes
|
|
10
|
+
sessions, imports other stores and maintains the index.
|
|
11
|
+
|
|
12
|
+
Every mutating command takes a lock, so two runs cannot capture or merge the same session at
|
|
13
|
+
once. Locks live in `<root>/.locks/` and are named after a hash of the operation target.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
node tools/agent-handoff.mjs <command> [args]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Command | Purpose |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `auto --source <file>` | Build only when the source is newer than the stored manifest. |
|
|
22
|
+
| `verify-gate <id-prefix>` | Five checks: sha, counts, payload, contract, evidence. |
|
|
23
|
+
| `promote <id-prefix>` | Stamp promotion metadata on a handoff's manifest. |
|
|
24
|
+
| `merge <a> <b>` | Compose two sessions of one project into one handoff. |
|
|
25
|
+
| `dispatch <id-prefix> --task <objective>` | Hand the continuation to a worker (Level 5). |
|
|
26
|
+
| `federated-merge --from <root>` | Import sessions from another store root. |
|
|
27
|
+
| `self-improve` | Collect brief shortfalls into a rules candidate file. |
|
|
28
|
+
| `index` | Rebuild `INDEX.json` and report stale sessions. |
|
|
29
|
+
|
|
30
|
+
An unknown command prints that list and exits 2.
|
|
31
|
+
|
|
32
|
+
## `auto` — self-triggering capture
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
node tools/agent-handoff.mjs auto --source transcript.jsonl \
|
|
36
|
+
[--session <id>] [--harness <name>] [--project <name>] [--min-fresh-ms <n>]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`--source` is required. Two gates run before any build:
|
|
40
|
+
|
|
41
|
+
1. **Freshness.** If the session's manifest exists and `source_mtime - manifest.updated_at`
|
|
42
|
+
is below `--min-fresh-ms` (default 60000), nothing is built and the command prints
|
|
43
|
+
`{"action":"skip-fresh"}`.
|
|
44
|
+
2. **Lock.** A capture already in flight for the same project and session exits 3.
|
|
45
|
+
|
|
46
|
+
It then runs the engine's `build` and prints `{"action":"captured",...}`. A missing source
|
|
47
|
+
file exits 2.
|
|
48
|
+
|
|
49
|
+
## `verify-gate` — the evidence gate
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
node tools/agent-handoff.mjs verify-gate <id-prefix>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Check | Passes when |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `sha` | `manifest_sha256` equals the hash of the manifest without that field. |
|
|
58
|
+
| `counts` | Lines in `timeline.jsonl` equal `turn_count`. |
|
|
59
|
+
| `payload` | `HANDOFF.llm.json` parses as JSON. |
|
|
60
|
+
| `contract` | `HANDOFF.md` contains all seven contract fields, each at the start of a line. |
|
|
61
|
+
| `evidence` | Not `RESULT: DONE` with no `EVIDENCE:` line. |
|
|
62
|
+
|
|
63
|
+
The contract fields are `RESULT`, `WHAT_CHANGED`, `VALIDATION`, `EVIDENCE`, `BLOCKERS`,
|
|
64
|
+
`RISKS`, `FOLLOW_UP`. The verdict is `VERIFIED` when every check is `PASS`, otherwise
|
|
65
|
+
`REJECTED`, printed as JSON with the per-check table.
|
|
66
|
+
|
|
67
|
+
**The command exits 0 in both cases.** A caller must read `ok` or `verdict` from the JSON —
|
|
68
|
+
the exit code alone does not report a rejection. Only a bad prefix (exit 3) or a missing
|
|
69
|
+
manifest is visible as a nonzero status.
|
|
70
|
+
|
|
71
|
+
## `promote` — stamping verified work
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
node tools/agent-handoff.mjs promote <id-prefix>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The manifest is backed up to `manifest.json.bak`, the gate above is run and its verdict is
|
|
78
|
+
printed, and the manifest is then stamped with `promoted_at` and `promoted_by` and re-hashed.
|
|
79
|
+
|
|
80
|
+
Two honest caveats:
|
|
81
|
+
|
|
82
|
+
- The gate result is **printed, not enforced**. The stamp is written even when the verdict is
|
|
83
|
+
`REJECTED`; a caller that wants a gate must run `verify-gate` and branch on `verdict`
|
|
84
|
+
before calling `promote`.
|
|
85
|
+
- Promotion is local. It writes one flag pair into one manifest file; it contacts no external
|
|
86
|
+
system and publishes nothing.
|
|
87
|
+
|
|
88
|
+
## `merge` — composing two sessions
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
node tools/agent-handoff.mjs merge <id-prefix-a> <id-prefix-b>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Timelines are concatenated and sorted by `ts`, and written to a new session directory named
|
|
95
|
+
`<a>+merge+<b>` under the first project. The merged manifest records `harness: "merged"`,
|
|
96
|
+
`merged_from: [a, b]` and a fresh `raw_sha256` over the joined timeline. A note is appended
|
|
97
|
+
to `links/<project-b>.md` with a `<!-- merged:<id> -->` marker, so a re-run does not duplicate
|
|
98
|
+
it.
|
|
99
|
+
|
|
100
|
+
A merged session has no `HANDOFF.md`, no payload and no `TOOLS.md`, so it fails `verify-gate`
|
|
101
|
+
on `contract` and `payload` until a brief is written for it, and `index` reports it as stale.
|
|
102
|
+
|
|
103
|
+
## `federated-merge` — importing another store root
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
node tools/agent-handoff.mjs federated-merge --from <remote-root> [--from <root> ...] [--dry-run]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Each remote root is expected to have the same `projects/<project>/<session>/` layout. A
|
|
110
|
+
session is imported when its `manifest_sha256` differs from the local copy, or when no local
|
|
111
|
+
copy exists; identical sessions are counted as skipped, so re-running imports only what
|
|
112
|
+
changed. `--dry-run` lists the actions without writing.
|
|
113
|
+
|
|
114
|
+
On import: an existing local manifest is backed up to `manifest.json.bak-federated`, the
|
|
115
|
+
session directory is copied, and `federated_from` and `federated_at` are stamped on the
|
|
116
|
+
canonical manifest, which is then re-hashed. Every imported session is verified with the
|
|
117
|
+
engine's `verify` afterwards, and the index is rebuilt. Per-session failures are listed in
|
|
118
|
+
the `failed` array rather than aborting the run. A root without a `projects/` directory is
|
|
119
|
+
recorded as failed, and a missing `--from` exits 2.
|
|
120
|
+
|
|
121
|
+
## `self-improve` — brief shortfalls
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
node tools/agent-handoff.mjs self-improve
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Every session is scanned, and a session with 40 or more USER+AGENT turns whose `HANDOFF.md`
|
|
128
|
+
is shorter than 800 characters is reported as a candidate. The output is written to
|
|
129
|
+
`docs/self-improve-candidates.json` inside the skill directory — deliberately skill-relative,
|
|
130
|
+
so a configured store never collects rule candidates. The file lists counts, not content.
|
|
131
|
+
|
|
132
|
+
## `index` — the store index
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
node tools/agent-handoff.mjs index
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Rebuilds `<root>/INDEX.json` from the manifests, with one entry per session (`id`, `uuid`,
|
|
139
|
+
`project`, `harness`, `turns`, `revisions`, `updated`, `manifest_sha256`), and reports
|
|
140
|
+
sessions that have a manifest but no `HANDOFF.md`.
|
|
141
|
+
|
|
142
|
+
## Bounded execution — `runtime-engine.mjs`
|
|
143
|
+
|
|
144
|
+
[tools/runtime-engine.mjs](https://github.com/Alot1z/agent-handoff/blob/main/tools/runtime-engine.mjs) is the command-execution half of the
|
|
145
|
+
runtime: every operation is evaluated against `permission-policy.json` **before** it runs, and
|
|
146
|
+
a denied operation is recorded but never executed.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
node tools/runtime-engine.mjs evaluate --risk R0..R4 [--target <path>] [--json]
|
|
150
|
+
node tools/runtime-engine.mjs run --risk R0|R1 --op read --target <path>
|
|
151
|
+
node tools/runtime-engine.mjs run --risk R1 --op spawn --args "<node args>"
|
|
152
|
+
node tools/runtime-engine.mjs policy
|
|
153
|
+
node tools/runtime-engine.mjs job --session <s> --steps <n> [--fail-at <k>] [--json]
|
|
154
|
+
node tools/runtime-engine.mjs resume --session <s> --steps <n> [--json]
|
|
155
|
+
node tools/runtime-engine.mjs status --session <s> [--json]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| Verdict | Meaning |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `ALLOWED` | The grant for the risk class's level is `allow`. |
|
|
161
|
+
| `DENIED` | Refused: an explicit denial, a personal-data path, an unknown risk, or an unknown grant. |
|
|
162
|
+
| `NEEDS_AUTH` | The grant is `needs_auth`; the operation waits for authorization and does not run. |
|
|
163
|
+
|
|
164
|
+
Targets are classified in a fixed order: an explicit denial first, then the engine's own
|
|
165
|
+
state directory, then the approved workspaces, then the broad personal-data roots, then
|
|
166
|
+
system read-only roots, otherwise external. A personal-data or denied path is refused at any
|
|
167
|
+
risk. A read-only level is allowed inside an approved workspace or a system path; every other
|
|
168
|
+
level requires an approved workspace. Levels, risk classes and grants are documented in
|
|
169
|
+
[PERMISSIONS.md](PERMISSIONS.md).
|
|
170
|
+
|
|
171
|
+
The `spawn` operation is bounded: it runs the local node binary with the given arguments, the
|
|
172
|
+
working directory pinned to the repository, a 15000 ms timeout, and the output captured and
|
|
173
|
+
hashed rather than streamed. Each decision — allowed, refused or failed — is written to
|
|
174
|
+
`<state>/executions/<id>.json` with `enforced_before_execution: true`.
|
|
175
|
+
|
|
176
|
+
Checkpoints make a job resumable from disk alone: after each step, `<state>/checkpoints/<session>.json`
|
|
177
|
+
is rewritten with a sha256 seal, and the step is appended to `<state>/jobs/<session>/work.log`.
|
|
178
|
+
Each step is permission-gated at `R1`. `--fail-at <k>` simulates an abrupt kill before step
|
|
179
|
+
`k` executes, leaving the durable state at `k-1`.
|
|
180
|
+
|
|
181
|
+
The state directory is `<repo>/.agent-handoff`, or `AGENT_HANDOFF_STATE_DIR` when set.
|
|
182
|
+
|
|
183
|
+
| Code | Meaning |
|
|
184
|
+
|---|---|
|
|
185
|
+
| 0 | Allowed, executed, or completed. |
|
|
186
|
+
| 1 | Execution error, or the child produced no exit code. |
|
|
187
|
+
| 2 | Usage or configuration error. |
|
|
188
|
+
| 3 | Denied. |
|
|
189
|
+
| 4 | Needs authorization, or no checkpoint to resume from. |
|
|
190
|
+
| 5 | Checkpoint corrupt, or its integrity seal does not match. |
|
|
191
|
+
| 137 | Simulated abrupt kill. |
|
|
192
|
+
| other | The captured exit code of the spawned child. |
|
|
193
|
+
|
|
194
|
+
## Exit codes
|
|
195
|
+
|
|
196
|
+
| Code | Meaning |
|
|
197
|
+
|---|---|
|
|
198
|
+
| 0 | Success. |
|
|
199
|
+
| 1 | A mutating command failed; the lock was released. |
|
|
200
|
+
| 2 | Usage or configuration error, including an unresolvable store root. |
|
|
201
|
+
| 3 | A lock is held, an id prefix is ambiguous, or a referenced path is missing. |
|
|
202
|
+
| 4 | No session matches the prefix. |
|
package/docs/LEVEL5.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Level 5 — collaborative dispatch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Level 5 — collaborative dispatch
|
|
6
|
+
|
|
7
|
+
A handoff normally ends as files on disk, and something reads it later. The dispatch command
|
|
8
|
+
lets a handoff whose own record is complete hand its continuation to a worker through a local
|
|
9
|
+
broker, instead of waiting for a reader.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
node tools/agent-handoff.mjs dispatch <id-prefix> --task "<objective>" \
|
|
13
|
+
[--role <role>] [--parent <parentTaskId>] [--broker <broker-root>] [--live]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Argument | Default | Meaning |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `<id-prefix>` | required | The handoff that is handing off its continuation. |
|
|
19
|
+
| `--task <objective>` | required | What the worker is being asked to do. |
|
|
20
|
+
| `--role <role>` | `implementation-agent` | The role the request is addressed to. |
|
|
21
|
+
| `--parent <parentTaskId>` | `handoff:<id>` | The parent the request is attached to. |
|
|
22
|
+
| `--broker <root>` | none | Root of the broker program. |
|
|
23
|
+
| `--live` | off | Enqueue the request instead of printing it. |
|
|
24
|
+
|
|
25
|
+
## The gate
|
|
26
|
+
|
|
27
|
+
Dispatch refuses to hand over work from a handoff that is not internally complete. Before
|
|
28
|
+
anything is built, `HANDOFF.md` is read and two things are required:
|
|
29
|
+
|
|
30
|
+
1. all seven contract fields are present, each at the start of a line — `RESULT`,
|
|
31
|
+
`WHAT_CHANGED`, `VALIDATION`, `EVIDENCE`, `BLOCKERS`, `RISKS`, `FOLLOW_UP`;
|
|
32
|
+
2. a record that says `RESULT: DONE` also carries an `EVIDENCE:` line.
|
|
33
|
+
|
|
34
|
+
When either fails, the command prints the refusal and exits 1:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{ "ok": false, "status": "GATE_REJECTED", "session": "<id>",
|
|
38
|
+
"reason": "handoff fails the evidence gate (missing contract fields: ...; DONE-without-EVIDENCE=true)" }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The gate reads `HANDOFF.md` only. It does not recompute the manifest hash, check the timeline
|
|
42
|
+
count, or parse the payload — those are `verify-gate` checks. A handoff can therefore pass the
|
|
43
|
+
dispatch gate while failing `verify-gate` on `sha`, `counts` or `payload`; run
|
|
44
|
+
`verify-gate <id>` first when that matters.
|
|
45
|
+
|
|
46
|
+
## The envelope
|
|
47
|
+
|
|
48
|
+
| Field | Value |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `from_handoff` | The session id the request came from. |
|
|
51
|
+
| `project` | The project that session belongs to. |
|
|
52
|
+
| `role` | `--role`. |
|
|
53
|
+
| `parentTaskId` | `--parent`, by default `handoff:<id>`. |
|
|
54
|
+
| `objective` | `--task`. |
|
|
55
|
+
| `evidence` | The session's `manifest_sha256`, so the receiver can check what it was given. |
|
|
56
|
+
| `evidenceRequirements` | `["verified-handoff"]`. |
|
|
57
|
+
| `dispatchedAt` | Timestamp of the envelope. |
|
|
58
|
+
| `broker_mode` | `live` or `dry-run`. |
|
|
59
|
+
| `handoff_dir` | Absolute path of the session directory. |
|
|
60
|
+
|
|
61
|
+
## States
|
|
62
|
+
|
|
63
|
+
| State | When | Exit |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `GATE_REJECTED` | The contract or evidence check failed. Nothing is sent. | 1 |
|
|
66
|
+
| `DRY_RUN` | `--live` was not passed, or no broker root was given. The envelope is printed. | 0 |
|
|
67
|
+
| `DISPATCHED` | `--live` and a broker root were given and the broker answered. | 0 |
|
|
68
|
+
|
|
69
|
+
`DRY_RUN` is the default. It prints the envelope and one line of advice: either re-run with
|
|
70
|
+
`--live`, or pass a broker root and `--live`. A dry run writes nothing and starts no process.
|
|
71
|
+
|
|
72
|
+
## Live dispatch
|
|
73
|
+
|
|
74
|
+
With both `--live` and a broker root, the command requires
|
|
75
|
+
`<broker-root>/runtime/request-child-worker.mjs`; if that file is missing it exits 3 and
|
|
76
|
+
sends nothing. It then runs:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
node <broker-root>/runtime/request-child-worker.mjs <broker-root> <parentTaskId> <role> <task> handoff:<id>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The broker's stdout is parsed as JSON and returned as `broker_output` alongside the envelope.
|
|
83
|
+
A broker that fails to start, or that prints something that is not JSON, exits 1.
|
|
84
|
+
|
|
85
|
+
Two limits worth knowing:
|
|
86
|
+
|
|
87
|
+
- **No dedupe and no lock.** Unlike `auto`, `merge` and `promote`, dispatch takes no lock and
|
|
88
|
+
keeps no record of a previous dispatch. Running a live dispatch twice enqueues the request
|
|
89
|
+
twice; a caller that needs once-only delivery must guard it.
|
|
90
|
+
- **The broker is a separate program.** This skill builds the envelope, checks the gate and
|
|
91
|
+
invokes the broker entry point. What the broker does with the request — how it queues it,
|
|
92
|
+
which worker picks it up, whether it dedupes — is not implemented or verified here.
|
|
93
|
+
|
|
94
|
+
The traceability chain that does exist: `parentTaskId` defaults to `handoff:<id>`, and
|
|
95
|
+
`evidence` carries the handoff's manifest hash, so a request can be traced back to the session
|
|
96
|
+
directory, its manifest, and the source transcripts recorded in `source_paths`.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Permissions
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Permissions
|
|
6
|
+
|
|
7
|
+
The runtime layer evaluates a permission policy before it executes anything. The policy is a
|
|
8
|
+
JSON file; the implementation is `tools/runtime-engine.mjs`.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
node tools/runtime-engine.mjs run --risk R0 --op read --target tests/fixtures/minimal-transcript.jsonl
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Every operation is evaluated first and executed only on an `ALLOWED` verdict. A `DENIED` or
|
|
15
|
+
`NEEDS_AUTH` operation does not run, and its decision record is written with
|
|
16
|
+
`enforced_before_execution: true`.
|
|
17
|
+
|
|
18
|
+
## The policy file
|
|
19
|
+
|
|
20
|
+
`permission-policy.json` in the skill root is the default. Every runtime verb accepts
|
|
21
|
+
`--policy <path>`; a relative path resolves against the skill root.
|
|
22
|
+
|
|
23
|
+
| Key | Shipped value |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `approved_workspaces` | `.` (the skill root), `repo-upstream`, `.agent-handoff`, `.context` |
|
|
26
|
+
| `system_read_only_roots` | `C:\Windows`, `C:\Program Files`, `C:\Program Files (x86)` |
|
|
27
|
+
| `personal_data_roots` | `C:\Users` |
|
|
28
|
+
| `denied_roots` | empty |
|
|
29
|
+
| `grants` | `DISCOVERY_ONLY` allow, `READ_ONLY` allow, `WORKSPACE_WRITE` allow, `EXTERNAL_EFFECT` needs_auth, `DESTRUCTIVE` needs_auth, `IRREVERSIBLE` needs_auth |
|
|
30
|
+
| `risk_to_level` | `R0` → `READ_ONLY`, `R1` → `WORKSPACE_WRITE`, `R2` → `EXTERNAL_EFFECT`, `R3` → `DESTRUCTIVE`, `R4` → `IRREVERSIBLE` |
|
|
31
|
+
|
|
32
|
+
The shipped roots are Windows-shaped. On macOS and Linux they match no existing path, so
|
|
33
|
+
anything outside `approved_workspaces` classifies as `EXTERNAL_PATH`. Set platform-appropriate
|
|
34
|
+
roots before relying on classification outside the workspace.
|
|
35
|
+
|
|
36
|
+
A policy that is missing, is not valid JSON, or lacks `grants` or `risk_to_level` is a
|
|
37
|
+
configuration error: exit 2, nothing executed.
|
|
38
|
+
|
|
39
|
+
## Levels and grants
|
|
40
|
+
|
|
41
|
+
| Level | Grant in the shipped policy | Meaning |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `DISCOVERY_ONLY` | allow | Read metadata, list, inspect. |
|
|
44
|
+
| `READ_ONLY` | allow | Read files and state. |
|
|
45
|
+
| `WORKSPACE_WRITE` | allow | Write inside an approved workspace. |
|
|
46
|
+
| `EXTERNAL_EFFECT` | needs_auth | Network or external service. |
|
|
47
|
+
| `DESTRUCTIVE` | needs_auth | Delete or overwrite. |
|
|
48
|
+
| `IRREVERSIBLE` | needs_auth | Operations that cannot be undone. |
|
|
49
|
+
|
|
50
|
+
A grant value of `allow` yields `ALLOWED`, `needs_auth` yields `NEEDS_AUTH`, and any other
|
|
51
|
+
value yields `DENIED`. Unknown risk classes are denied, not allowed.
|
|
52
|
+
|
|
53
|
+
Because grants are keyed by level rather than by operation, an operator can permit every
|
|
54
|
+
`EXTERNAL_EFFECT` operation, or require authorization for every workspace write, with one
|
|
55
|
+
edit.
|
|
56
|
+
|
|
57
|
+
## Target classification
|
|
58
|
+
|
|
59
|
+
`classify()` tests the target against the policy roots, most specific rule first:
|
|
60
|
+
|
|
61
|
+
| Order | Class | Source |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| 1 | `DENIED_ROOT` | `denied_roots` |
|
|
64
|
+
| 2 | `APPROVED_WORKSPACE` | the runtime's own state directory, wherever `AGENT_HANDOFF_STATE_DIR` points |
|
|
65
|
+
| 3 | `APPROVED_WORKSPACE` | `approved_workspaces` |
|
|
66
|
+
| 4 | `PERSONAL_DATA_PATH` | `personal_data_roots` |
|
|
67
|
+
| 5 | `SYSTEM_PATH` | `system_read_only_roots` |
|
|
68
|
+
| 6 | `EXTERNAL_PATH` | nothing matched |
|
|
69
|
+
|
|
70
|
+
Rule 3 must precede rule 4. A globally installed skill lives under the user's home
|
|
71
|
+
directory, which is normally inside a personal-data root; if the broad root won, every
|
|
72
|
+
operation inside a real installation would be denied at any risk.
|
|
73
|
+
|
|
74
|
+
## Verdicts
|
|
75
|
+
|
|
76
|
+
| Target class | `READ_ONLY` / `DISCOVERY_ONLY` | `WORKSPACE_WRITE` and above |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `APPROVED_WORKSPACE` | allowed | grant applies |
|
|
79
|
+
| `SYSTEM_PATH` | allowed | denied |
|
|
80
|
+
| `PERSONAL_DATA_PATH` | denied | denied |
|
|
81
|
+
| `DENIED_ROOT` | denied | denied |
|
|
82
|
+
| `EXTERNAL_PATH` | denied | denied |
|
|
83
|
+
|
|
84
|
+
Personal data and explicitly denied roots are denied at every risk level. Writes and effects
|
|
85
|
+
are allowed only inside an approved workspace.
|
|
86
|
+
|
|
87
|
+
| Verdict | Exit code |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `ALLOWED` | 0 |
|
|
90
|
+
| `DENIED` | 3 |
|
|
91
|
+
| `NEEDS_AUTH` | 4 |
|
|
92
|
+
| usage or configuration error | 2 |
|
|
93
|
+
|
|
94
|
+
## What the gateway covers
|
|
95
|
+
|
|
96
|
+
| Operation | How it is gated |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `run --op read --target <path>` | Evaluated against the target. On `ALLOWED` the file is read and the record carries `bytes` and `content_sha256`. |
|
|
99
|
+
| `run --op spawn --args "<node args>"` | Evaluated against the workspace root. The child is the running Node binary with the given arguments, working directory pinned to the skill root, 15-second timeout, stdout and stderr digested into the record, child exit code propagated. |
|
|
100
|
+
| `job` / `resume` steps | Each step is evaluated at `R1` against the job state directory before the work log is appended. |
|
|
101
|
+
| `evaluate` | Decides and prints; executes nothing. |
|
|
102
|
+
| `policy` | Prints the loaded policy and exits 0. |
|
|
103
|
+
|
|
104
|
+
Every decision, allowed or denied, is written to
|
|
105
|
+
`<state dir>/executions/<timestamp>-<pid>-<random>.json`. The state directory is
|
|
106
|
+
`AGENT_HANDOFF_STATE_DIR` when set, and `<skill>/.agent-handoff` otherwise; it is treated as
|
|
107
|
+
an approved workspace wherever it points.
|
|
108
|
+
|
|
109
|
+
## Declared but not enforced
|
|
110
|
+
|
|
111
|
+
- `DISCOVERY_ONLY` has a grant, but the shipped `risk_to_level` maps no risk to it, so no
|
|
112
|
+
`--risk` value reaches that level. It becomes reachable only if a policy maps a risk class
|
|
113
|
+
to it.
|
|
114
|
+
- The policy gates the runtime engine only. `tools/handoff.mjs`, the capability registry and
|
|
115
|
+
the installer do not consult it: the handoff engine writes to its resolved store without a
|
|
116
|
+
permission decision, and the registry probes are classified by their own declarations.
|
|
117
|
+
- There is no interactive approval prompt. `NEEDS_AUTH` reports that authorization is
|
|
118
|
+
required; granting it means changing the policy, and the decision is recorded either way.
|
|
119
|
+
|
|
120
|
+
## Changing the policy
|
|
121
|
+
|
|
122
|
+
1. Decide the level. Operations are keyed by risk class (`R0`–`R4`), which maps to a level.
|
|
123
|
+
2. Edit `permission-policy.json`, or write a separate policy file and pass
|
|
124
|
+
`--policy <path>`.
|
|
125
|
+
3. Change `grants` to move a whole level between allow, needs-auth and denial; change
|
|
126
|
+
`risk_to_level` to reclassify a risk class; add entries to `approved_workspaces`,
|
|
127
|
+
`personal_data_roots`, `system_read_only_roots` or `denied_roots` to move a path between
|
|
128
|
+
classes.
|
|
129
|
+
4. Test one decision without executing it:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
node tools/runtime-engine.mjs evaluate --risk R2 --target README.md --json
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
5. Confirm what the runtime actually loaded:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
node tools/runtime-engine.mjs policy --json
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
A `denied_roots` entry outranks an approved workspace, so it is the switch to use when a
|
|
142
|
+
path must be off limits regardless of any other rule.
|
|
143
|
+
|
|
144
|
+
See [INTEGRATION.md](INTEGRATION.md) for wiring these commands into another system, and
|
|
145
|
+
[LEVEL4.md](LEVEL4.md) for the runtime verbs as a whole.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Provenance
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Provenance
|
|
6
|
+
|
|
7
|
+
## What this document covers
|
|
8
|
+
|
|
9
|
+
Every handoff carries a hash chain that ties its rendered files to the transcript it was built from.
|
|
10
|
+
This document states what is hashed, how to re-verify it, and what the check cannot prove.
|
|
11
|
+
|
|
12
|
+
## The chain
|
|
13
|
+
|
|
14
|
+
| Artifact | Field | Definition |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Source transcript | `raw_sha256` | SHA-256 of the whole source file, read as UTF-8 at build time |
|
|
17
|
+
| `manifest.json` | `manifest_sha256` | SHA-256 of `JSON.stringify(manifest)` with `manifest_sha256` removed |
|
|
18
|
+
| `HANDOFF.summary.json` | `provenance.raw_sha256`, `provenance.manifest_sha256` | Copies of the two hashes above |
|
|
19
|
+
| `HANDOFF.llm.json` | `provenance.manifest_sha256` | Copy of the manifest hash |
|
|
20
|
+
| `HANDOFF.md` | *Provenance* section | Both hashes and the revision, rendered as text |
|
|
21
|
+
|
|
22
|
+
`manifest.json` also records `source_paths` (every transcript ever merged into the session),
|
|
23
|
+
`watermark` (the highest turn sequence emitted), `revisions` and `turn_count`.
|
|
24
|
+
|
|
25
|
+
## Verification
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
node tools/handoff.mjs verify <id-prefix>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`verify` recomputes `manifest_sha256` from the stored manifest and compares it, then requires
|
|
32
|
+
`timeline.jsonl` to exist and to hold exactly `turn_count` lines, then parses `HANDOFF.llm.json`.
|
|
33
|
+
It prints one `PASS` line, or `FAIL` with the reason.
|
|
34
|
+
|
|
35
|
+
| Exit | Meaning |
|
|
36
|
+
|---|---|
|
|
37
|
+
| 0 | All three checks passed |
|
|
38
|
+
| 1 | Manifest mismatch, timeline missing, turn-count drift, or unparsable payload |
|
|
39
|
+
| 2 | No prefix given |
|
|
40
|
+
| 3 | Prefix matched more than one session |
|
|
41
|
+
| 4 | No session matched, or no handoffs exist |
|
|
42
|
+
|
|
43
|
+
## What the check detects
|
|
44
|
+
|
|
45
|
+
- A stored manifest whose fields have been edited or added.
|
|
46
|
+
- A truncated or padded `timeline.jsonl`, because its line count must equal `turn_count`.
|
|
47
|
+
- A missing timeline.
|
|
48
|
+
- A `HANDOFF.llm.json` that is no longer valid JSON.
|
|
49
|
+
|
|
50
|
+
## What the check cannot detect
|
|
51
|
+
|
|
52
|
+
- **Edits to unhashed renders.** `HANDOFF.md`, `HANDOFF.summary.json` and `TOOLS.md` are not hashed.
|
|
53
|
+
Their bytes can be changed freely and `verify` still passes.
|
|
54
|
+
- **Timeline content edits that preserve the line count.** A rewritten turn keeps the count.
|
|
55
|
+
- **Re-hashing by the editor.** The hashes are self-consistent, not signed. Anyone who edits the
|
|
56
|
+
manifest can recompute `manifest_sha256` and produce a folder that verifies clean. There is no key
|
|
57
|
+
material, no signature and no external trust anchor.
|
|
58
|
+
- **Anything outside the session folder.** `PROJECT.md`, `INDEX.json` and `links/*.md` carry no
|
|
59
|
+
hashes.
|
|
60
|
+
|
|
61
|
+
Treat the chain as damage detection, not as authentication.
|
|
62
|
+
|
|
63
|
+
## Update instead of recreate
|
|
64
|
+
|
|
65
|
+
Re-running `build` on the same session id merges rather than duplicates: turns with a sequence above
|
|
66
|
+
`watermark` are appended to `timeline.jsonl`, `revisions` increments, and the manifest hash is
|
|
67
|
+
recomputed. If the source is unchanged and no new turns exist, the run reports `up-to-date` and
|
|
68
|
+
bumps nothing. The rendered summary, payload and `HANDOFF.md` are regenerated from the full
|
|
69
|
+
timeline, so a revision never mixes stale and fresh text.
|
|
70
|
+
|
|
71
|
+
## Privacy note on `source_paths`
|
|
72
|
+
|
|
73
|
+
A manifest records the absolute path of every transcript that fed the session. A handoff folder
|
|
74
|
+
therefore contains the local paths of the machine it was built on. Review that field before sharing
|
|
75
|
+
a folder outside the machine.
|
|
76
|
+
|
|
77
|
+
## Repository provenance
|
|
78
|
+
|
|
79
|
+
- License: MIT, see [../LICENSE](https://github.com/Alot1z/agent-handoff/blob/main/LICENSE).
|
|
80
|
+
- The engine and runtime use only `node:` built-ins. No third-party source is bundled and
|
|
81
|
+
`package.json` declares no dependencies.
|
|
82
|
+
- Development notes, internal plans and research material are not part of this repository and are
|
|
83
|
+
not published with it.
|
package/docs/SECURITY.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Security
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Security
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
agent-handoff reads session transcripts you point it at and writes a handoff folder to disk. It has
|
|
10
|
+
no network code, no third-party dependencies, and no privileged operations. This document states
|
|
11
|
+
what the tool does with data, what it refuses to do, and which guarantees it does not make.
|
|
12
|
+
|
|
13
|
+
## What the tool reads
|
|
14
|
+
|
|
15
|
+
| Input | How it is used |
|
|
16
|
+
|---|---|
|
|
17
|
+
| The file passed to `build --source` | Read as UTF-8 text and parsed into turns. A `.jsonl` file is parsed line by line; anything else is parsed with role markers. |
|
|
18
|
+
| `handoff.config.json` | Walked up from the current directory and validated against `handoff.config.schema.json`. A present-but-invalid file is fatal (`exit 2`). |
|
|
19
|
+
|
|
20
|
+
Adapters that read a harness session store are external to the engine. They read stores and never
|
|
21
|
+
write to them. See [../refs/ADAPTERS.md](https://github.com/Alot1z/agent-handoff/blob/main/refs/ADAPTERS.md).
|
|
22
|
+
|
|
23
|
+
## What the tool writes
|
|
24
|
+
|
|
25
|
+
Under the resolved root (see [FORMAT.md](FORMAT.md)): `HANDOFF.md`, `HANDOFF.summary.json`,
|
|
26
|
+
`HANDOFF.llm.json`, `timeline.jsonl`, `TOOLS.md`, `manifest.json`, and per project `PROJECT.md`,
|
|
27
|
+
plus `INDEX.json` and `links/*.md`. Nothing else is written and nothing outside the root is
|
|
28
|
+
modified.
|
|
29
|
+
|
|
30
|
+
Writes go straight to the destination path with `fs.writeFileSync`. They are not atomic and are not
|
|
31
|
+
staged through a temporary file, so an interrupted run can leave a partial file. `verify` detects
|
|
32
|
+
that only indirectly — see [PROVENANCE.md](PROVENANCE.md).
|
|
33
|
+
|
|
34
|
+
## Secrets
|
|
35
|
+
|
|
36
|
+
The engine performs **no secret detection and no redaction**. A transcript containing a token or a
|
|
37
|
+
key produces a handoff containing that token or key. There is no scanning pass, no allowlist and no
|
|
38
|
+
masking.
|
|
39
|
+
|
|
40
|
+
`handoff.config.example.json` lists an `exclude_from_handoff` array. Treat that as intent, not as an
|
|
41
|
+
enforced control: the engine reads `storage.path`, `handoff_dir`, `project_name` and `linking` from a
|
|
42
|
+
config, and does not read that array.
|
|
43
|
+
|
|
44
|
+
Only hand off transcripts you have reviewed. Keep `.env` files and key material out of version
|
|
45
|
+
control, and do not commit a handoff folder that quotes them.
|
|
46
|
+
|
|
47
|
+
## Malicious input
|
|
48
|
+
|
|
49
|
+
A transcript is data. It is never evaluated, never executed, and never interpreted as configuration
|
|
50
|
+
or as a request. Concretely:
|
|
51
|
+
|
|
52
|
+
- A tool call recorded in a transcript is copied verbatim as text into `TOOLS.md`. The engine does not run it.
|
|
53
|
+
- A transcript cannot change the engine's arguments, the store root, or the exit code beyond a parse failure.
|
|
54
|
+
- A transcript with no parsable turns stops the run (`exit 4`). Individual malformed JSONL lines are skipped.
|
|
55
|
+
|
|
56
|
+
The realistic risk is not code execution but content. A handoff is a readable document that a later
|
|
57
|
+
human or agent may treat as instructions, and it inherits whatever instructions the transcript
|
|
58
|
+
contained. Review a handoff before handing it to another agent.
|
|
59
|
+
|
|
60
|
+
## Paths and the workspace boundary
|
|
61
|
+
|
|
62
|
+
The engine resolves `--source` against the current directory and writes under the resolved root. It
|
|
63
|
+
performs no workspace-boundary check of its own: a path you pass is a path it uses.
|
|
64
|
+
|
|
65
|
+
`permission-policy.json` declares the boundary for the runtime layer — approved workspaces,
|
|
66
|
+
read-only system roots, personal-data roots, per-level grants (`READ_ONLY` and `WORKSPACE_WRITE`
|
|
67
|
+
allowed; `EXTERNAL_EFFECT`, `DESTRUCTIVE` and `IRREVERSIBLE` need authorization) and the R0–R4 risk
|
|
68
|
+
mapping. See [PERMISSIONS.md](PERMISSIONS.md). The handoff engine itself does not consult that file.
|
|
69
|
+
|
|
70
|
+
## Guarantees that are not made
|
|
71
|
+
|
|
72
|
+
- No sandboxing. No process isolation, no privilege separation. The tool runs with the permissions of the user who invoked it.
|
|
73
|
+
- No encryption. Handoff files are plain text and JSON.
|
|
74
|
+
- No authentication. Hashes are self-consistent, not signed — see [PROVENANCE.md](PROVENANCE.md).
|
|
75
|
+
- No audit log and no system logging.
|
|
76
|
+
- No secret scanning, no PII detection, no redaction.
|
|
77
|
+
|
|
78
|
+
## Dependencies
|
|
79
|
+
|
|
80
|
+
None. The engine and the runtime use only `node:` built-ins and require Node 18 or newer. The
|
|
81
|
+
installer is a single script with no package dependencies.
|
|
82
|
+
|
|
83
|
+
## Reporting a vulnerability
|
|
84
|
+
|
|
85
|
+
Report it privately to the maintainer, not in a public issue. Use the repository's private
|
|
86
|
+
vulnerability reporting if it is enabled, otherwise contact the maintainer directly. Include the
|
|
87
|
+
command, the input, and the observed result.
|
|
88
|
+
|
|
89
|
+
## Checklist before publishing a handoff folder
|
|
90
|
+
|
|
91
|
+
- [ ] The transcript was reviewed and contains no credentials, tokens or personal data.
|
|
92
|
+
- [ ] The handoff quotes no private path and no machine identifier.
|
|
93
|
+
- [ ] `node tools/handoff.mjs verify <id-prefix>` passes for every session in the folder.
|