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.
Files changed (54) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/LICENSE +21 -0
  3. package/README.md +110 -2
  4. package/SKILL.md +147 -0
  5. package/capability-registry.json +27 -0
  6. package/docs/ARCHITECTURE.md +164 -0
  7. package/docs/CHANGELOG.md +151 -0
  8. package/docs/CLI.md +196 -0
  9. package/docs/COMPATIBILITY.md +124 -0
  10. package/docs/CONTRIBUTING.md +134 -0
  11. package/docs/FORMAT.md +157 -0
  12. package/docs/INSTALL.md +179 -0
  13. package/docs/INTEGRATION.md +188 -0
  14. package/docs/LEVEL4.md +202 -0
  15. package/docs/LEVEL5.md +96 -0
  16. package/docs/PERMISSIONS.md +145 -0
  17. package/docs/PROVENANCE.md +83 -0
  18. package/docs/SECURITY.md +93 -0
  19. package/docs/SESSIONS.md +66 -0
  20. package/docs/TROUBLESHOOTING.md +158 -0
  21. package/docs/UNINSTALL.md +122 -0
  22. package/docs/UPGRADE.md +139 -0
  23. package/docs/_config.yml +16 -0
  24. package/docs/_data/nav.yml +36 -0
  25. package/docs/_layouts/default.html +31 -0
  26. package/docs/assets/style.css +88 -0
  27. package/docs/index.md +83 -0
  28. package/handoff.config.example.json +35 -0
  29. package/handoff.config.schema.json +117 -0
  30. package/install/CHANGELOG.md +48 -0
  31. package/install/README.md +76 -0
  32. package/install/install.mjs +856 -0
  33. package/install/package.json +39 -0
  34. package/package.json +66 -4
  35. package/permission-policy.json +33 -0
  36. package/refs/ADAPTERS.md +33 -0
  37. package/refs/bootstrap.md +59 -0
  38. package/refs/brief-checklist.md +79 -0
  39. package/refs/handbook.md +58 -0
  40. package/refs/protocol.md +117 -0
  41. package/refs/roles.md +75 -0
  42. package/refs/validator.md +73 -0
  43. package/schemas/handoff.schema.json +275 -0
  44. package/skill.json +147 -0
  45. package/templates/HANDOFF.llm.schema.json +144 -0
  46. package/templates/HANDOFF.template.md +40 -0
  47. package/tests/acceptance/acceptance.yaml +209 -0
  48. package/tests/fixtures/minimal-transcript.jsonl +2 -0
  49. package/tools/agent-handoff.mjs +410 -0
  50. package/tools/capability-registry.mjs +120 -0
  51. package/tools/handoff.mjs +398 -0
  52. package/tools/handoff.test.mjs +465 -0
  53. package/tools/lib/handoff-root.mjs +161 -0
  54. 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.
@@ -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.