wowbagger 0.1.0-alpha.2 → 0.1.0-alpha.4

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.
@@ -0,0 +1,1280 @@
1
+ # Harness-neutral adapter contract
2
+
3
+ Status: version 1 remains accepted and frozen; version 2 is implemented by the
4
+ shipped adapters and independent reference oracle.
5
+
6
+ This is the public contract for a thin adapter between a coding-agent harness
7
+ and the existing Wowbagger core CLI. It supplements [SPEC.md](../SPEC.md),
8
+ [the mutation contract](mutation-contract.md), and
9
+ [ADR 0005](adr/0005-harness-neutral-adapter-contract.md). The core contracts
10
+ remain authoritative for ledger meaning, command JSON, exit codes, and mutation
11
+ limits.
12
+
13
+ The words MUST, MUST NOT, SHOULD, and MAY are normative.
14
+
15
+ ## 1. Scope and terms
16
+
17
+ An **adapter** is a small integration that translates a configured harness
18
+ request into a documented Wowbagger core command. It is not a second core.
19
+
20
+ | Term | Meaning |
21
+ |---|---|
22
+ | Core | The standalone `wowbagger` CLI and its published JSON contracts. |
23
+ | Harness | A host that may provide a workspace, filesystem, command runner, instruction inputs, and model interaction. |
24
+ | Model transport | A protocol that sends model requests and responses. It can be OpenAI-compatible without being a usable coding harness. |
25
+ | Consumer | The repository owner or authorized operator who configures the adapter and grants authority. |
26
+ | Workspace | A consumer-approved repository root known to a harness under an opaque identifier. |
27
+ | Instruction input | Bounded text that the harness explicitly supplies for a session. It is not discovered by guessing a filename. |
28
+
29
+ Version 1 covers only these core commands: `validate`, `ready`, `capabilities`,
30
+ `inspect`, `create`, and `transition`. An adapter MUST use their documented
31
+ `--json` forms. It MUST NOT create an alternate interpretation of lifecycle,
32
+ readiness, revisions, locks, error codes, or mutation state.
33
+
34
+ Sections 1 through 11 define version 1 unchanged. Section 12 defines version 2
35
+ only as explicit deltas from that base; an unmodified rule applies to both
36
+ versions.
37
+
38
+ The core contract version and this adapter-contract version are different
39
+ numbers. `contract_version` in core JSON is defined by the core. This document
40
+ uses `adapter_contract_version`.
41
+
42
+ Every adapter manifest, describe result, invocation, result, instruction input,
43
+ and handoff is one UTF-8 JSON object with no duplicate member names at any
44
+ depth. A receiver rejects an invalid or duplicate-member object rather than
45
+ choosing a last member.
46
+
47
+ ## 2. Boundary
48
+
49
+ The adapter boundary is deliberately narrow.
50
+
51
+ | Concern | Core | Adapter | Harness or consumer |
52
+ |---|---|---|---|
53
+ | Ledger validation and readiness | Owns | Forwards | May request |
54
+ | Local mutation scope and revisions | Owns | Probes and forwards | May approve a request |
55
+ | Command process and byte streams | Produces | Launches and preserves | Supplies a safe runner |
56
+ | Workspace selection and path guard | Rejects unsafe ledger entries | Verifies its own input boundary | Configures approved roots |
57
+ | Instruction discovery | Does not do it | Carries declared inputs | Supplies or configures inputs |
58
+ | Model API | Does not do it | May describe it | Provides it when applicable |
59
+ | Session state | Does not retain it | Carries an explicit handoff only | Stores or delivers it deliberately |
60
+ | Commit, push, install, or setup | Does not do it | Disabled by default | Requires separate approval |
61
+
62
+ An OpenAI-compatible API is model transport only. It MUST NOT cause an adapter
63
+ to report filesystem access, no-follow path handling, command execution,
64
+ standard-stream forwarding, instruction discovery, or mutation authority. A
65
+ host that has transport but lacks the required tools is API-only and cannot
66
+ invoke the core.
67
+
68
+ ## 3. Version and capability negotiation
69
+
70
+ An adapter package has a static manifest and a dynamic `describe` operation.
71
+ Discovery is passive: parsing a manifest MUST NOT install software, run a
72
+ command, start a daemon, or contact a network service.
73
+
74
+ The consumer selects an installed package or host registration. No adapter may
75
+ scan a repository for a vendor file, infer a host from a model name, or select
76
+ a package merely because an API endpoint is reachable.
77
+
78
+ Before an invocation, a client MUST:
79
+
80
+ 1. use fixed `bootstrap_wire_version` 1 to call `describe`;
81
+ 2. select adapter contract version 1 when both peers include it, or refuse;
82
+ 3. obtain `describe` and reject a required capability that is missing or
83
+ `false`;
84
+ 4. invoke core `capabilities --json` before assuming a core mutation,
85
+ coordination, or durability capability; and
86
+ 5. treat an unknown field, missing field, unsupported version, or failed probe
87
+ as unavailable.
88
+
89
+ The describe request is the exact object `bootstrap_wire_version`,
90
+ `supported_adapter_contract_versions`, and `request_id`, with no other members.
91
+ The request ID uses the safe opaque-ID syntax. Request version arrays are
92
+ nonempty, unique, sorted-ascending arrays of positive safe integers. The
93
+ version 1 manifest array is exactly `[1]`, and the successful dynamic result
94
+ selects exactly `1`; advertising a shared future version does not activate an
95
+ undocumented schema or handler. A future version requires a separately
96
+ registered manifest, request, result, and invoke-schema handler.
97
+ Malformed request, manifest, and dynamic objects are refused as
98
+ `invalid-describe-request`, `invalid-adapter-manifest`, and
99
+ `invalid-describe-result`, respectively; validation never depends on host
100
+ array methods or property access before the containing schema has passed.
101
+
102
+ The bootstrap wire version 1 refusal for a malformed `describe` request is the
103
+ exact envelope below. `message` is stable. For a parsed request that fails the
104
+ describe schema, `details.member` names the first failing member check. For an
105
+ input read or parse failure, `details` preserves the reader's complete
106
+ diagnostic object.
107
+
108
+ ```json
109
+ {
110
+ "ok": false,
111
+ "bootstrap_wire_version": 1,
112
+ "error": {
113
+ "code": "invalid-describe-request",
114
+ "message": "The adapter describe request is invalid.",
115
+ "details": {
116
+ "member": "request_id"
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ Bootstrap is deliberately non-circular. Wire version 1 is fixed independently
123
+ of the adapter versions being negotiated. The client sends its supported
124
+ adapter versions; `describe` selects one or refuses. An implementation MUST
125
+ compare the static ID, adapter version, platform declaration, and required core
126
+ contract version with the dynamic result, then compare the required core
127
+ contract version with the actual `capabilities --json` probe. Any mismatch or
128
+ unsupported platform is a refusal before the requested core command launches.
129
+ For each invocation, it MUST determine the active runtime platform and launch
130
+ only when the matching static and dynamic platform value is exactly
131
+ `supported`. `unsupported`, `unverified`, and an unlisted active platform are
132
+ `adapter-platform-mismatch` refusals with `platform`, `status`, and
133
+ `required: "supported"` details before core launch.
134
+
135
+ The adapter MUST preserve the core `capabilities` result as core output. The
136
+ probe is the exact successful version 1 core `capabilities --json` envelope:
137
+ its root, backend, operations (`inspect`, `create`, `transition`, and
138
+ `work_claim`), durability, and limits objects accept no missing or additional
139
+ members and every fixed value must match the published core contract. A
140
+ missing, extra, malformed, wrong-command, or wrong-contract probe is refused.
141
+ The adapter's complete ordered version 1 command list must match the probed
142
+ core contract. `optional_features.claims` is derived only from
143
+ `operations.work_claim.supported`; `optional_features.policy` remains false
144
+ because core version 1 advertises no policy feature. Static or dynamic claims
145
+ cannot elevate either value. The adapter MUST NOT turn a local mutation
146
+ capability into cross-worktree, cross-clone, or
147
+ work-claim support.
148
+
149
+ ### 3.1 Static package manifest
150
+
151
+ An installed package MAY expose a UTF-8 JSON file named
152
+ `wowbagger-adapter.json`. A host registration may carry the same object without
153
+ a file. This packaging name is not an instruction-discovery convention.
154
+
155
+ The manifest has these required members:
156
+
157
+ ```json
158
+ {
159
+ "adapter_manifest_version": 1,
160
+ "adapter_id": "org.example.wowbagger.adapter",
161
+ "adapter_version": "1.0.0",
162
+ "adapter_contract_versions": [1],
163
+ "bootstrap_wire_version": 1,
164
+ "required_core_contract_version": 1,
165
+ "entrypoints": {
166
+ "describe": {
167
+ "kind": "command",
168
+ "executable": "bin/wowbagger-adapter",
169
+ "fixed_args": ["describe"]
170
+ },
171
+ "invoke": {
172
+ "kind": "command",
173
+ "executable": "bin/wowbagger-adapter",
174
+ "fixed_args": ["invoke"]
175
+ }
176
+ },
177
+ "platforms": {
178
+ "darwin": "unverified",
179
+ "linux": "unverified",
180
+ "win32": "unverified"
181
+ }
182
+ }
183
+ ```
184
+
185
+ `adapter_id` is a stable reverse-domain-style identifier. `adapter_version` is
186
+ the package version. Version arrays are unique and sorted ascending. An
187
+ entrypoint is either a consumer-registered `host-tool` or the exact command
188
+ schema shown above. `executable` is a nonempty forward-slash relative path
189
+ anchored at the installed package root. Absolute, drive, UNC, device, volume,
190
+ backslash, control-character, empty-segment, `.`, and `..` forms are invalid on
191
+ every platform. The package root, every parent component, and the final regular
192
+ file are resolved no-follow and their stable identities are rechecked
193
+ immediately before launch. `fixed_args` is an array of UTF-8 strings without
194
+ NUL or control characters; arguments are passed directly and never through a
195
+ shell. Consumer
196
+ registration may replace the entire entrypoint, but a request may replace
197
+ neither field nor append arguments. The runner launches it directly without a
198
+ shell. Host-tool registrations have equivalent consumer-granted authority and
199
+ MUST identify the registered tool by a fixed name.
200
+
201
+ The manifest root and each entrypoint object are exact. Every displayed root
202
+ member is required, unknown members are refused, and a `host-tool` entrypoint
203
+ contains exactly `kind: "host-tool"` and a nonempty string `name`.
204
+
205
+ After manifest validation, a command entrypoint is joined to the approved
206
+ package root only after its relative syntax passes. A missing, link, junction,
207
+ reparse point, special file, escaping component, or identity replacement is a
208
+ refusal before the adapter process launches.
209
+
210
+ Platform values are `supported`, `unsupported`, or `unverified`. `supported`
211
+ requires native evidence from the common adapter vectors. A manifest MUST use
212
+ `unverified` until it has that evidence.
213
+
214
+ ### 3.2 Dynamic describe result
215
+
216
+ `describe` returns exactly one UTF-8 JSON object. Its successful envelope has
217
+ these members and no undocumented root members:
218
+
219
+ ```json
220
+ {
221
+ "ok": true,
222
+ "bootstrap_wire_version": 1,
223
+ "selected_adapter_contract_version": 1,
224
+ "adapter_id": "org.example.wowbagger.adapter",
225
+ "adapter_version": "1.0.0",
226
+ "core": {
227
+ "required_core_contract_version": 1,
228
+ "commands": ["capabilities", "create", "inspect", "ready", "transition", "validate"]
229
+ },
230
+ "host": {
231
+ "command_execution": {
232
+ "supported": true,
233
+ "arguments_array": true,
234
+ "shell": false,
235
+ "stdio": true,
236
+ "process_tree_containment": true,
237
+ "orphan_detection": true,
238
+ "timeout_enforcement": true,
239
+ "stdout_limit": true,
240
+ "stderr_limit": true
241
+ },
242
+ "filesystem": {
243
+ "workspace_selection": "guarded-relative",
244
+ "no_follow_resolution": true,
245
+ "stable_identity": true,
246
+ "component_walk": true
247
+ },
248
+ "model_transport": {
249
+ "available": true,
250
+ "protocol": "openai-compatible"
251
+ },
252
+ "instruction_input": {
253
+ "mode": "host-provided",
254
+ "max_sources": 8,
255
+ "max_bytes": 65536
256
+ },
257
+ "handoff": {
258
+ "supported": true,
259
+ "persistence": "explicit-only"
260
+ },
261
+ "trusted_approval": {
262
+ "supported": true,
263
+ "sources": ["consumer"]
264
+ },
265
+ "integration_mechanisms": {
266
+ "hooks": false,
267
+ "slash_commands": false,
268
+ "mcp": false,
269
+ "daemon": false
270
+ }
271
+ },
272
+ "optional_features": {
273
+ "claims": false,
274
+ "policy": false
275
+ },
276
+ "limits": {
277
+ "max_request_bytes": 65536,
278
+ "max_context_bytes": 65536,
279
+ "max_stdout_bytes": 1048576,
280
+ "max_stderr_bytes": 65536,
281
+ "max_timeout_ms": 30000
282
+ },
283
+ "platforms": {
284
+ "darwin": "unverified",
285
+ "linux": "unverified",
286
+ "win32": "unverified"
287
+ }
288
+ }
289
+ ```
290
+
291
+ The example does not assert that such a host exists. It shows the required
292
+ separation of capabilities. `model_transport` is descriptive. It does not
293
+ change `command_execution` or `filesystem`.
294
+
295
+ The root and every nested object shown above are exact, except that
296
+ `host.trusted_approval` MAY be absent to declare mutations unavailable. Every
297
+ other displayed member is required and no additional member is accepted.
298
+ Command arrays are unique,
299
+ ordered subsets of the version 1 core command list; capability flags are
300
+ booleans; enumerated strings and bounded integers use the domains described
301
+ below. A missing member, extra member, wrong type, unknown enumeration, or
302
+ invalid bound is `invalid-describe-result`.
303
+
304
+ When present, version 1 trusted approval has exactly one authority label:
305
+ `trusted_approval.sources` MUST equal `["consumer"]`. Model, agent, system,
306
+ tool, harness, and additional source labels are invalid describe results; they
307
+ cannot become trusted through configuration. `trusted_approval.supported` MUST
308
+ be `true` for `create` or `transition`. A false or absent member makes those
309
+ commands unavailable before an approval is validated or redeemed. Read-only
310
+ commands remain available.
311
+
312
+ A non-null `handoff_carrier` requires `host.handoff.supported: true` before
313
+ the carrier is parsed. A supported value of `false` is a
314
+ `capability-unavailable` refusal with `missing: ["handoff"]`; an absent or
315
+ malformed handoff capability is `invalid-describe-result`. Neither case can
316
+ launch the core.
317
+
318
+ `command_execution.supported` is true only when the adapter can launch the
319
+ configured core executable with an argument array, without a shell, and capture
320
+ both byte streams. `filesystem.workspace_selection` is `guarded-relative` only
321
+ when the adapter can apply section 4. `instruction_input.mode` is `none`,
322
+ `host-provided`, or `configured-relative-paths`. `handoff.persistence` is
323
+ always `explicit-only` in version 1.
324
+
325
+ Capability fields obey these cross-field invariants; a contradictory describe
326
+ result is `invalid-describe-result`.
327
+
328
+ | Mode | Required dependent values |
329
+ | --- | --- |
330
+ | `command_execution.supported: true` | `arguments_array`, `stdio`, `process_tree_containment`, `orphan_detection`, `timeout_enforcement`, `stdout_limit`, and `stderr_limit` are `true`; `shell` is `false`; every advertised byte/time limit is a positive safe integer. |
331
+ | `command_execution.supported: false` | Every dependent execution Boolean is `false`, including `shell`; `core.commands` is empty, so the adapter does not advertise invoke capability. |
332
+ | `filesystem.workspace_selection: "guarded-relative"` | `no_follow_resolution`, `stable_identity`, and `component_walk` are all `true`. |
333
+ | `filesystem.workspace_selection: "none"` | `no_follow_resolution`, `stable_identity`, and `component_walk` are all `false`. |
334
+ | `instruction_input.mode: "none"` | `max_sources` and `max_bytes` are zero. |
335
+ | Other instruction-input modes | `max_sources` and `max_bytes` are positive safe integers. |
336
+
337
+ Byte limits are finite nonnegative safe integers; zero permits no content.
338
+ `max_timeout_ms` is a finite positive safe integer. Byte limits apply to raw
339
+ bytes before base64 expansion. An adapter MAY advertise smaller limits than
340
+ the example. It MUST NOT imply an unbounded context, output, or execution time.
341
+
342
+ `optional_features.claims` and `optional_features.policy` default to `false`.
343
+ Claims become true only when the independently probed version 1 core reports
344
+ `work_claim.supported: true`; policy remains false because version 1 advertises
345
+ no policy feature. The mutation capability probe is advisory and cannot imply
346
+ publication fencing or safe exclusive dispatch. Ledger-specific callers must
347
+ use `claim capabilities` to distinguish an unprovisioned advisory ledger from
348
+ a provisioned merge-coordinated ledger. A mutation lock remains a short local
349
+ mutation lock, not a claim.
350
+
351
+ ### 3.3 Bootstrap command wire
352
+
353
+ Command entrypoints use the same bootstrap transport for `describe` and
354
+ `invoke`:
355
+
356
+ - The runner writes exactly one strict UTF-8 JSON object to stdin and closes
357
+ stdin. Duplicate members, trailing bytes, and invalid UTF-8 are refused.
358
+ - The entrypoint writes exactly one strict JSON object followed by one LF to
359
+ stdout. No prefix, progress record, or second object is allowed. Diagnostic
360
+ text is bounded and may use stderr; it is never parsed as a result.
361
+ - Exit 0 means a complete bootstrap response was written, including an
362
+ `ok:false` response. A nonzero exit, signal, timeout, malformed response, or
363
+ incomplete bounded stream is transport failure.
364
+ - The installed package root is the entrypoint working directory. The
365
+ invocation's separately guarded `cwd` applies only to the core child. The
366
+ adapter environment is a consumer-configured allowlist; inherited secrets,
367
+ Git variables, shell startup, and request-supplied environment entries are
368
+ excluded.
369
+ - The runner applies finite stdin, stdout, stderr, and wall-clock limits. It
370
+ starts the adapter and core in a containable process-tree unit and, on limit,
371
+ timeout, or cancellation, terminates that whole unit and verifies that no
372
+ descendant remains. A runner unable to provide or verify containment MUST
373
+ advertise command execution unavailable.
374
+
375
+ The describe request contains only `bootstrap_wire_version`,
376
+ `supported_adapter_contract_versions`, and an opaque `request_id`. The invoke
377
+ request uses the selected version. A describe result advertises
378
+ `trusted_approval`; absence means mutations are unavailable. Static and dynamic
379
+ adapter ID, adapter version, selected contract, complete three-platform map,
380
+ and `core.required_core_contract_version` MUST match exactly. The independently
381
+ launched core probe must then match that required core version. Error
382
+ precedence is describe-request schema, static-manifest schema, bootstrap wire
383
+ compatibility, common-version selection, dynamic-result schema, static/dynamic
384
+ identity and version, selected contract, required core, platform map, core
385
+ probe, capabilities, path/input validation, approval, then launch.
386
+
387
+ ## 4. Workspace and path selection
388
+
389
+ The model never supplies an absolute workspace root. A consumer preconfigures
390
+ an opaque `workspace_id` that maps to one approved root. The adapter accepts
391
+ only logical paths relative to that root.
392
+
393
+ A logical path is `.` or one or more forward-slash-separated segments. It MUST
394
+ NOT be absolute, empty, contain a backslash, NUL, drive prefix, volume prefix,
395
+ `.` segment, or `..` segment. Windows drive-relative forms such as `C:repo`,
396
+ drive-rooted forms using either separator, UNC forms using either separator,
397
+ device namespaces such as `\\?\C:` and `\\.\COM1`, and `Volume{...}` roots are all
398
+ invalid on every host platform. The same logical syntax is used on macOS,
399
+ Linux, and Windows; the adapter rejects platform prefixes before converting a
400
+ logical path to a host path.
401
+
402
+ For a core request with a workspace, the adapter MUST:
403
+
404
+ 1. resolve `workspace_id` to a consumer-approved real directory without
405
+ following a link or reparse point at the root;
406
+ 2. resolve `cwd` and `ledger` from that root with a no-follow check on every
407
+ existing path component, including Windows reparse points;
408
+ 3. reject a missing, symbolic-link, junction, reparse-point, special, or
409
+ escaping component before launching the core; and
410
+ 4. re-check stable platform file identities immediately before launch, refuse
411
+ a replacement, and launch the core with the resolved `cwd` plus an absolute
412
+ ledger argument anchored at the workspace root.
413
+
414
+ `cwd` never changes the base for `ledger`. For example, `cwd: "nested"` and
415
+ `ledger: "ledger"` select `<root>/nested` as the child working directory and
416
+ `<root>/ledger` as the ledger; `<root>/nested/ledger` is a decoy and MUST NOT
417
+ be selected. Root, cwd, ledger, and every component are directories with
418
+ no-follow `lstat` identity snapshots. The adapter checks the root and each
419
+ cumulative component (for example `nested`, then `nested/deep`) before and
420
+ immediately before launch. A component replacement between validation and
421
+ launch is `path-replaced`, not permission to retry through the new component.
422
+
423
+ Every before/after snapshot is the exact object `{ "kind": ..., "identity":
424
+ ... }`. `kind` is the required portable kind for that position (`directory`
425
+ for package/workspace roots and parents, `regular-file` for a command
426
+ executable). `identity` is either a nonempty control-free opaque stable token,
427
+ an exact POSIX `{ "dev": ..., "ino": ... }` object, or an exact Windows
428
+ `{ "volume_id": ..., "file_id": ... }` object. Explicit identity members are
429
+ nonempty control-free strings or nonnegative safe integers. Missing, malformed,
430
+ or extra snapshot members on either side are `path-rejected`; two valid
431
+ same-kind snapshots with unequal identities are `path-replaced`.
432
+
433
+ The core separately rejects a symbolic-link ledger root and symbolic-link
434
+ entries below it. The adapter boundary does not claim to eliminate privileged
435
+ filesystem races. It prevents a caller from selecting an arbitrary path or
436
+ silently traversing a link before the core gets its own fail-closed check.
437
+
438
+ An adapter that cannot make this no-follow determination MUST report
439
+ `no_follow_resolution: false` and MUST NOT accept local workspace or ledger
440
+ selection. An API-only host therefore cannot claim a local core invocation.
441
+
442
+ ## 5. Invocation
443
+
444
+ An adapter accepts a structured request. It MUST NOT accept raw shell source,
445
+ an arbitrary executable, an arbitrary CLI argument list, or an arbitrary input
446
+ file path. It constructs the documented core argument vector itself.
447
+
448
+ ```json
449
+ {
450
+ "adapter_contract_version": 1,
451
+ "request_id": "ready-forwarding-0001",
452
+ "workspace": {
453
+ "workspace_id": "example-workspace",
454
+ "cwd": "."
455
+ },
456
+ "core_request": {
457
+ "command": "ready",
458
+ "ledger": "ledger",
459
+ "as_of": "2030-01-15"
460
+ },
461
+ "instruction_input": {
462
+ "required": false,
463
+ "instruction_input_version": 1,
464
+ "sources": []
465
+ },
466
+ "handoff_carrier": null,
467
+ "limits": {
468
+ "context_bytes": 4096,
469
+ "stdout_bytes": 65536,
470
+ "stderr_bytes": 4096,
471
+ "timeout_ms": 30000
472
+ }
473
+ }
474
+ ```
475
+
476
+ `request_id` is an opaque ASCII identifier of 1 through 128 characters using
477
+ letters, digits, period, underscore, and hyphen. The adapter returns it
478
+ unchanged. Requested byte limits MUST be finite nonnegative safe integers no
479
+ greater than the advertised values. `timeout_ms` is required, positive, and no
480
+ greater than `max_timeout_ms`; it bounds the complete contained adapter/core
481
+ process observation.
482
+
483
+ The runner applies its finite local `max_request_bytes` ceiling before parsing
484
+ the raw invocation. After describe succeeds, the same raw bytes MUST also be
485
+ at most `described.limits.max_request_bytes`; that advertised cap is
486
+ authoritative for the invocation. A describe result that advertises a request
487
+ cap above the local ceiling is inconsistent and is refused as
488
+ `invalid-describe-result`. The adapter never enlarges either cap.
489
+
490
+ `workspace` is required for every command except `capabilities`. `cwd` defaults
491
+ to `.` when omitted. `ledger` is required for every workspace command and uses
492
+ the logical path rules in section 4.
493
+
494
+ The tagged `core_request` members are exact:
495
+
496
+ | Command | Required members | Core argument vector |
497
+ |---|---|---|
498
+ | `capabilities` | `command` | `capabilities --json` |
499
+ | `validate` | `command`, `ledger` | `validate --ledger <ledger> --json` |
500
+ | `ready` | `command`, `ledger`, `as_of` | `ready --ledger <ledger> --as-of <date> --json` |
501
+ | `inspect` | `command`, `ledger`, `id` | `inspect --ledger <ledger> --id <id> --json` |
502
+ | `create` | `command`, `ledger`, `input_base64` | `create --ledger <ledger> --input - --json` |
503
+ | `transition` | `command`, `ledger`, `input_base64` | `transition --ledger <ledger> --input - --json` |
504
+
505
+ `input_base64` is RFC 4648 base64 without line breaks. Its decoded bytes are
506
+ the exact UTF-8 JSON request sent to standard input. The adapter MUST check the
507
+ decoded byte limit before launching the core. It MUST use `--input -`; it MUST
508
+ NOT write a caller-selected request file.
509
+
510
+ `as_of`, `id`, and decoded mutation requests retain their core validation
511
+ rules. The adapter does not normalize dates, IDs, JSON whitespace, YAML, or
512
+ request bytes.
513
+
514
+ The version 1 request root always carries `instruction_input` and
515
+ `handoff_carrier`: an optional instruction set is represented by the exact
516
+ empty carrier `{ "instruction_input_version": 1, "required": false,
517
+ "sources": [] }`, and no handoff is represented by `null`. This keeps the root
518
+ schema exact instead of making validation depend on omitted fields. The
519
+ carriers are validated as sections 7 and 8 specify before core launch. A
520
+ required instruction set that is
521
+ missing or invalid produces a named diagnostic refusal; it cannot be silently
522
+ replaced by guessed files or prior-session memory.
523
+
524
+ ### 5.1 Consumer authority
525
+
526
+ `create` and `transition` require an explicit consumer approval event for that
527
+ invocation and `trusted_approval.supported: true` in describe. The event may be
528
+ represented to the adapter by a trusted host mechanism, but a model-supplied
529
+ Boolean is not approval. If approval is absent, the adapter returns
530
+ `consumer-approval-required` without launching the core. If trusted approval
531
+ is false or absent, it returns `capability-unavailable` with
532
+ `missing: ["trusted-approval"]` before validating or redeeming any approval.
533
+
534
+ Read operations do not grant mutation authority. No version 1 request grants
535
+ authority to commit, push, modify remote configuration, install dependencies,
536
+ start a setup script, edit a harness configuration, or bypass repository
537
+ instructions or safety gates. These actions are outside this interface.
538
+
539
+ Trusted command-entrypoint approval uses this exact object:
540
+
541
+ ```json
542
+ {
543
+ "approval_version": 1,
544
+ "source": "consumer",
545
+ "nonce": "single-use-opaque-value",
546
+ "issued_at": "2030-01-15T12:00:00Z",
547
+ "expires_at": "2030-01-15T12:05:00Z",
548
+ "invocation_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
549
+ }
550
+ ```
551
+
552
+ The digest covers this exact binding object; no member is optional and no
553
+ extra member is accepted:
554
+
555
+ ```json
556
+ {
557
+ "request_id": "mutation-approval-0002",
558
+ "adapter": {
559
+ "id": "org.example.wowbagger.adapter",
560
+ "version": "1.0.0",
561
+ "contract_version": 1
562
+ },
563
+ "core": {
564
+ "executable_identity": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
565
+ "contract_version": 1,
566
+ "argv": ["transition", "--ledger", "/approved/workspace/ledger", "--input", "-", "--json"],
567
+ "input_base64": "e30K"
568
+ },
569
+ "workspace": {
570
+ "id": "example-workspace",
571
+ "root": "/approved/workspace",
572
+ "cwd": "/approved/workspace",
573
+ "ledger": "/approved/workspace/ledger"
574
+ },
575
+ "limits": {
576
+ "context_bytes": 4096,
577
+ "stdout_bytes": 65536,
578
+ "stderr_bytes": 4096,
579
+ "timeout_ms": 30000
580
+ },
581
+ "instruction_set_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
582
+ "handoff_digest": null
583
+ }
584
+ ```
585
+
586
+ The consumer authority signs or delivers this record through the configured
587
+ trusted channel; the model and instruction inputs are never trusted sources.
588
+ The configured trusted-source set MUST be exactly `{consumer}`. A missing
589
+ consumer or any additional model, agent, system, tool, harness, or other label
590
+ is `approval-source-untrusted`, even when the approval object itself says
591
+ `source: "consumer"`.
592
+ The adapter canonicalizes a binding as UTF-8 JSON with object keys sorted
593
+ lexicographically, no insignificant whitespace, arrays retained in order, and
594
+ ordinary JSON scalar encoding. The SHA-256 binding includes request ID;
595
+ workspace ID, root, cwd, and absolute ledger; exact core executable identity,
596
+ contract version, argv, and stdin bytes; adapter ID/version/selected contract;
597
+ all byte/time limits; instruction-set digest; and handoff digest. Any change
598
+ requires new approval. Approval and binding objects use exact-member schemas.
599
+ Version must be 1; nonce is 16–128 ASCII letters, digits, periods, underscores,
600
+ or hyphens; digests use lowercase `sha256:` plus 64 hex characters. Issued,
601
+ expiry, and current time use canonical whole-second RFC 3339 UTC; invalid time,
602
+ `issued_at >= expires_at`, or current time before issue fails closed. The nonce
603
+ is redeemed atomically once, expires at `now >= expires_at`, and is scoped to
604
+ this consumer/adapter instance. Replay, expiry, unknown source, and binding
605
+ mismatch refuse before launch. More
606
+ restrictive repository, consumer, or harness policy wins; approval never
607
+ overrides an instruction or safety refusal.
608
+
609
+ ## 6. Result, streams, and exit status
610
+
611
+ When the core process starts and both streams fit the requested bounds, the
612
+ adapter returns exactly one JSON result envelope:
613
+
614
+ ```json
615
+ {
616
+ "ok": true,
617
+ "adapter_contract_version": 1,
618
+ "request_id": "ready-forwarding-0001",
619
+ "result": {
620
+ "core_command": "ready",
621
+ "core_exit_code": 0,
622
+ "stdout": {
623
+ "encoding": "base64",
624
+ "data": "eyJhc19vZiI6IjIwMzAtMDEtMTUiLCJ2YWxpZCI6dHJ1ZSwicmVhZHkiOltdfQo=",
625
+ "sha256": "sha256:73b7879d83e6047beee2720b38f4e882b98dc351452112d5a9e5f85cac7e8eda",
626
+ "byte_length": 47
627
+ },
628
+ "stderr": {
629
+ "encoding": "base64",
630
+ "data": "",
631
+ "sha256": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
632
+ "byte_length": 0
633
+ }
634
+ }
635
+ }
636
+ ```
637
+
638
+ The result envelope's `ok` means that the adapter completed transport. It does
639
+ not mean that the core command succeeded. `core_exit_code` MUST equal the
640
+ actual child-process exit code, including a nonzero validation, request,
641
+ conflict, capability, or operation result. The base64 values decode to the
642
+ exact child stream bytes. Their SHA-256 values cover those decoded bytes.
643
+
644
+ An adapter MUST NOT trim, reformat, parse-and-reserialize, prefix, suffix, or
645
+ replace a core stream. It MAY provide a separately named parsed JSON view only
646
+ when it proves that it is derived from the complete decoded standard output;
647
+ that view is never the authoritative core result.
648
+
649
+ For every `--json` core command, decoded standard output is complete only when
650
+ it is one compact JSON object followed by exactly one LF. Whitespace outside
651
+ JSON strings, a missing final LF, or a CR or second LF before that final LF is
652
+ a core protocol failure. The adapter still preserves those exact captured bytes
653
+ in its process observation; it does not normalize them into a valid result.
654
+
655
+ For `inspect`, the decoded core standard output is complete only when its
656
+ exact command and contract version match, exactly one of `result` or `error`
657
+ is present, and its exit is consistent with that success or documented error.
658
+ On success, `result` contains exactly `item`. The item has the exact documented
659
+ lossless core shape: canonical ID, safe ledger-relative Markdown path, source
660
+ encoding and media type, canonical source bytes, matching source digest, and a
661
+ normalized core/body view that agrees with those decoded UTF-8 source bytes.
662
+ The adapter validates core version 1 field domains and rejects an extra,
663
+ missing, source-inconsistent, or semantically invalid item as a protocol
664
+ failure.
665
+ For `create` and `transition`, the same checks also require the exact mutation
666
+ state: success is `state: "committed"` with `result` and exit 0; an error has
667
+ `error` and its documented `unchanged`, `committed`, or `unknown` state and
668
+ exit. A declared `unknown` state is an adapter
669
+ `mutation-outcome-unknown`, not a completed mutation result. Any malformed,
670
+ incomplete, mismatched-command, mismatched-version, mismatched-state,
671
+ result/error, or exit-inconsistent mutation envelope is likewise
672
+ `mutation-outcome-unknown` with section 6 recovery.
673
+
674
+ Before forwarding a complete core envelope, the adapter also binds every
675
+ request-derived response member to the exact canonical request it launched.
676
+ `ready` success repeats the requested `as_of`; an `inspect` item or
677
+ `item-not-found` detail repeats the requested ID. Every mutation response with
678
+ a target ID repeats the caller's ID. A transition `revision-conflict` repeats
679
+ the requested expected revision and has an actual revision that differs from
680
+ it. A committed-recovery transition reports a new revision rather than the
681
+ expected pre-transition revision. Create success uses the requested default
682
+ path and the exact canonical candidate source bytes (including permitted
683
+ extension data), whose digest and normalized view already have to match the
684
+ returned item. A committed create recovery reports the SHA-256 digest of those
685
+ same exact candidate bytes for that caller ID and default path; a merely
686
+ well-formed digest is not enough. Transition success repeats the target status,
687
+ date, and required decision evidence. Error operation and lock-owner
688
+ identifiers retain their documented meanings and are validated against their
689
+ target item rather than being treated as opaque strings. A response that is
690
+ valid in isolation but cannot be bound to this request is a protocol failure
691
+ (and therefore an unknown mutation outcome for a mutation). In particular, a
692
+ core `invalid-request` mutation response is acceptable only when the supplied
693
+ mutation bytes did not form a canonical valid request. The adapter retains the
694
+ exact decoded request bytes for this response check, including malformed JSON
695
+ and duplicate-key observations; it does not promote an invalid request into a
696
+ valid mutation merely to correlate its error.
697
+
698
+ The adapter also rejects self-contradictory or nondeterministic mutation
699
+ details. An `atomic-scope-required` envelope has a nonempty, unique blocker
700
+ array sorted by code, item ID, then field, and it retains its possibly empty
701
+ but ordered precondition issue array. Invalid-request issues sort by path,
702
+ code, then message. Transition issues sort by code, field, then related-ID
703
+ sequence, and each related-ID sequence is unique and ascending. Recovery
704
+ artifacts are unique by path, sorted by path then kind, contain at most 16 entries,
705
+ and use the documented truncation rule. These checks preserve the core's
706
+ deterministic response semantics instead of accepting an equivalent-looking
707
+ but impossible envelope. Blocker and precondition-issue fields also retain the
708
+ fixed meanings of their codes: dependency codes use `depends_on`, child codes
709
+ use `parent`, date checks use `date`, and an invalid edge uses `to_status`.
710
+
711
+ The adapter parses every returned item source, checks its source digest,
712
+ lossless body, and normalized view, then applies the authoritative ledger
713
+ validator to the returned item. It supplies only valid synthetic relation
714
+ targets needed to evaluate invariants that a one-item response can establish;
715
+ any validator error attributed to the returned item is a protocol failure.
716
+ This covers the version 1 kind and status domains, epic `in-progress`
717
+ prohibition, ID-created-date agreement, timestamp ordering, terminal dates and
718
+ decisions, dependency and relation rules, and terminal epic rollup evidence.
719
+ The core remains responsible for complete-ledger facts that cannot be observed
720
+ from one returned item. A valid validation result has an empty error array.
721
+ Every invalid-ledger, candidate-invalid, or ledger-invalid response has a
722
+ nonempty validation-error array in the deterministic `path`, `field`, `code`,
723
+ then `message` order.
724
+
725
+ `ready` has two exact complete core forms. A valid ready result has exactly
726
+ `as_of`, `valid: true`, and `ready`, with exit 0. A documented invalid-ledger
727
+ result has exactly `valid: false` and a nonempty deterministically ordered
728
+ `errors` array, with exit 1. The adapter forwards the latter's nonzero exit and
729
+ exact bytes. Other hybrid, extra, empty-error, or inconsistent ready shapes
730
+ are `core-protocol-error`.
731
+
732
+ If a wrapper also offers a stream passthrough mode, it MUST write the exact core
733
+ standard output and standard error and use the same exit code. It MUST NOT add
734
+ the outer envelope to either core stream.
735
+
736
+ When the adapter cannot provide a complete core observation, it returns this
737
+ error envelope:
738
+
739
+ ```json
740
+ {
741
+ "ok": false,
742
+ "adapter_contract_version": 1,
743
+ "request_id": "ready-forwarding-0001",
744
+ "error": {
745
+ "code": "capability-unavailable",
746
+ "message": "The configured host cannot invoke the Wowbagger core.",
747
+ "details": {
748
+ "missing": ["command-execution"]
749
+ }
750
+ }
751
+ }
752
+ ```
753
+
754
+ The following operation classes are exhaustive and disjoint. Their sorted
755
+ union is the complete public version 1 registry. Every code is emitted by the
756
+ reference model and exercised as an expected vector result; adding or removing
757
+ a code requires changing the classes, registry, executable evidence, and drift
758
+ test together.
759
+
760
+ <!-- adapter-error-code-classes:start -->
761
+ ```json
762
+ {
763
+ "approval": [
764
+ "approval-binding-mismatch",
765
+ "approval-expired",
766
+ "approval-not-yet-valid",
767
+ "approval-replayed",
768
+ "approval-source-untrusted",
769
+ "consumer-approval-required",
770
+ "invalid-approval",
771
+ "invalid-approval-binding",
772
+ "invalid-approval-time",
773
+ "invalid-approval-time-order"
774
+ ],
775
+ "handoff": [
776
+ "handoff-digest-mismatch",
777
+ "handoff-instruction-set-mismatch",
778
+ "handoff-item-mismatch",
779
+ "handoff-limit-exceeded",
780
+ "handoff-resume-binding-mismatch",
781
+ "handoff-stale-item-revision",
782
+ "handoff-workspace-mismatch",
783
+ "invalid-handoff-bytes",
784
+ "invalid-handoff-carrier",
785
+ "invalid-handoff-json",
786
+ "invalid-handoff-object",
787
+ "invalid-handoff-resume-request"
788
+ ],
789
+ "instruction": [
790
+ "duplicate-instruction-source-id",
791
+ "instruction-byte-limit-exceeded",
792
+ "instruction-source-limit-exceeded",
793
+ "invalid-instruction-input",
794
+ "invalid-instruction-source",
795
+ "required-instruction-input-missing"
796
+ ],
797
+ "invocation": [
798
+ "capability-unavailable",
799
+ "context-limit-exceeded",
800
+ "core-launch-failed",
801
+ "core-observation-incomplete",
802
+ "core-protocol-error",
803
+ "core-signaled",
804
+ "core-timeout",
805
+ "invalid-invocation",
806
+ "mutation-outcome-unknown",
807
+ "output-limit-exceeded",
808
+ "path-rejected",
809
+ "path-replaced",
810
+ "timeout-limit-exceeded"
811
+ ],
812
+ "negotiation": [
813
+ "adapter-contract-selection-mismatch",
814
+ "adapter-identity-mismatch",
815
+ "adapter-platform-mismatch",
816
+ "adapter-version-mismatch",
817
+ "core-contract-version-mismatch",
818
+ "invalid-adapter-manifest",
819
+ "invalid-describe-request",
820
+ "invalid-describe-result",
821
+ "required-core-contract-version-mismatch",
822
+ "unsupported-adapter-contract-version",
823
+ "unsupported-bootstrap-wire-version"
824
+ ]
825
+ }
826
+ ```
827
+ <!-- adapter-error-code-classes:end -->
828
+
829
+ The flattened registry is:
830
+
831
+ <!-- adapter-error-codes:start -->
832
+ ```text
833
+ adapter-contract-selection-mismatch
834
+ adapter-identity-mismatch
835
+ adapter-platform-mismatch
836
+ adapter-version-mismatch
837
+ approval-binding-mismatch
838
+ approval-expired
839
+ approval-not-yet-valid
840
+ approval-replayed
841
+ approval-source-untrusted
842
+ capability-unavailable
843
+ consumer-approval-required
844
+ context-limit-exceeded
845
+ core-contract-version-mismatch
846
+ core-launch-failed
847
+ core-observation-incomplete
848
+ core-protocol-error
849
+ core-signaled
850
+ core-timeout
851
+ duplicate-instruction-source-id
852
+ handoff-digest-mismatch
853
+ handoff-instruction-set-mismatch
854
+ handoff-item-mismatch
855
+ handoff-limit-exceeded
856
+ handoff-resume-binding-mismatch
857
+ handoff-stale-item-revision
858
+ handoff-workspace-mismatch
859
+ instruction-byte-limit-exceeded
860
+ instruction-source-limit-exceeded
861
+ invalid-adapter-manifest
862
+ invalid-approval
863
+ invalid-approval-binding
864
+ invalid-approval-time
865
+ invalid-approval-time-order
866
+ invalid-describe-request
867
+ invalid-describe-result
868
+ invalid-handoff-bytes
869
+ invalid-handoff-carrier
870
+ invalid-handoff-json
871
+ invalid-handoff-object
872
+ invalid-handoff-resume-request
873
+ invalid-instruction-input
874
+ invalid-instruction-source
875
+ invalid-invocation
876
+ mutation-outcome-unknown
877
+ output-limit-exceeded
878
+ path-rejected
879
+ path-replaced
880
+ required-core-contract-version-mismatch
881
+ required-instruction-input-missing
882
+ timeout-limit-exceeded
883
+ unsupported-adapter-contract-version
884
+ unsupported-bootstrap-wire-version
885
+ ```
886
+ <!-- adapter-error-codes:end -->
887
+
888
+ Error details are bounded JSON. They MUST NOT expose
889
+ credentials, environment values, raw instruction contents, arbitrary absolute
890
+ paths, or platform exception text.
891
+
892
+ If a stream exceeds its requested bound, the adapter MUST stop or contain the
893
+ core process according to the host's safe runner rule. It returns
894
+ `output-limit-exceeded`; it MUST NOT present a partial core JSON result as
895
+ valid. It may report only bounded byte counts and stream names needed for
896
+ recovery. It must not silently enlarge a limit.
897
+
898
+ The runner supplies the exact process observation object `started`,
899
+ `process_tree_contained`, `orphaned`, `exit_code`, `signal` (a portable runner
900
+ label, not necessarily a POSIX signal), `timed_out`, `stdout_complete`,
901
+ `stderr_complete`, `stdout_base64`, and `stderr_base64`. Booleans, canonical
902
+ base64 streams, a null or nonnegative integer exit code, and a null or
903
+ nonempty control-free signal label are type-checked before classification; no
904
+ unknown members are accepted. A normally terminated child tree has an integer
905
+ exit code and no timeout or signal. A null exit without another incomplete
906
+ observation condition, a missing or malformed exit, or an integer exit paired
907
+ with timeout/signal is an incomplete observation. The outer process summary
908
+ derives, rather than accepts, `core_envelope_present` and
909
+ `core_envelope_valid` from the captured stdout bytes. A result is complete only
910
+ after the child tree has ended, both streams ended within bounds, no descendant
911
+ remains, and stdout contains the strict complete core envelope expected for
912
+ that command.
913
+
914
+ Launch classification is tri-state for mutations. A normal
915
+ `core-launch-failed` is allowed only for a complete, internally consistent
916
+ observation that proves `started: false`: no exit, signal, timeout, output,
917
+ orphan, or missing stream/containment evidence. A missing, malformed, or
918
+ contradicted `started` member leaves launch unknown, as does every other
919
+ ambiguous observation, and therefore produces `mutation-outcome-unknown`.
920
+ Read-only commands retain their ordinary transport errors and never acquire a
921
+ mutation outcome.
922
+
923
+ The adapter independently decodes each captured base64 stream and compares its
924
+ decoded byte length with the invocation's requested stdout or stderr limit.
925
+ An over-limit capture is `output-limit-exceeded` for a read and
926
+ `mutation-outcome-unknown` for a mutation, even when the runner reports the
927
+ stream complete. Runner completion flags cannot enlarge a byte bound.
928
+
929
+ For read-only commands precedence is: a proven not-started observation →
930
+ `core-launch-failed`; timeout → `core-timeout`; signal → `core-signaled`;
931
+ containment/orphan doubt or malformed observation →
932
+ `core-observation-incomplete`; stream truncation → `output-limit-exceeded`;
933
+ then missing or invalid complete envelope → `core-protocol-error`. Exit code is
934
+ recorded but never overrides an earlier transport failure. No partial core JSON
935
+ is valid.
936
+
937
+ Mutation commands are stricter: unless the observation proves that launch did
938
+ not occur, any timeout or signal produces `mutation-outcome-unknown` even when
939
+ both buffered streams and a nominal success envelope appear complete.
940
+ Containment uncertainty, incomplete stdout, incomplete stderr, a missing or
941
+ invalid `started` observation, or a missing/invalid complete core envelope also
942
+ produces `mutation-outcome-unknown`—even if exit was zero or captured bytes
943
+ look like a success. The adapter MUST NOT label it failed, roll it back, or
944
+ retry it.
945
+
946
+ Every create request therefore carries a caller-generated item ID. Recovery is
947
+ bounded and explicit: validate the ledger, inspect that known ID, and retry
948
+ only after `item-not-found` plus review/recovery of any audited publication
949
+ artifact. For transition, validate, inspect the known ID, and compare the
950
+ caller-known expected revision with the current revision and state; never
951
+ retry until that observation is reviewed. This distinction prevents both a
952
+ hidden committed mutation and an orphaned duplicate mutation.
953
+
954
+ ## 7. Instruction inputs
955
+
956
+ The contract does not assume `CLAUDE.md`, `AGENTS.md`, any other filename,
957
+ slash command, hook, MCP server, daemon, or model vendor. A harness may supply
958
+ instruction inputs through this bounded envelope:
959
+
960
+ ```json
961
+ {
962
+ "instruction_input_version": 1,
963
+ "required": true,
964
+ "sources": [
965
+ {
966
+ "source_id": "repository-rules",
967
+ "origin": "repository",
968
+ "content_encoding": "base64",
969
+ "content_base64": "VXNlIHRoZSBhcHByb3ZlZCBsZWRnZXIgb25seS4K",
970
+ "sha256": "sha256:f42a9b7fa06a5825cc6c5faf5a5ed2b217ecf65672aee9e40572feddb36e2578",
971
+ "byte_length": 30,
972
+ "logical_path": "config/repository-rules.txt"
973
+ }
974
+ ]
975
+ }
976
+ ```
977
+
978
+ Sources are ordered by the harness. Each array member must first be an exact
979
+ JSON object; null, primitive, array, missing-member, and extra-member values
980
+ are deterministic `invalid-instruction-source` refusals and never cause
981
+ property-access exceptions. `origin` is `repository`, `consumer`,
982
+ `harness`, `user`, or `adapter`. A source MAY have an optional logical relative
983
+ path for diagnostics, but the path has no built-in semantic meaning. A host
984
+ with `configured-relative-paths` can read only consumer-configured paths using
985
+ section 4. It MUST NOT fall back to a guessed name.
986
+
987
+ The carrier has exactly `instruction_input_version`, `required`, and `sources`.
988
+ Version is 1 and `required` is Boolean. Each source has exactly `source_id`,
989
+ `origin`, `content_encoding`, `content_base64`, `sha256`, and `byte_length`,
990
+ plus optional `logical_path`. Source IDs are unique safe ASCII identifiers;
991
+ origin uses the stated enum; encoding is `base64`; base64 is canonical; and
992
+ logical paths use section 4 syntax. Unknown members or versions are refused.
993
+
994
+ The adapter verifies base64, byte length, SHA-256, source count, and total byte
995
+ limit before presenting input to a model. Instruction contents are data, not
996
+ executable configuration. Missing inputs do not waive repository instructions,
997
+ consumer policy, safety gates, or approval requirements.
998
+
999
+ The adapter preserves supplied order and exposes a bounded diagnostic record
1000
+ for each source: zero-based ordinal, `source_id`, origin, numeric precedence,
1001
+ optional logical source path, byte length, and digest. Content is never echoed
1002
+ in diagnostics. The
1003
+ `instruction_set_digest` is SHA-256 over canonical ordered records containing
1004
+ source ID, origin, byte length, and content digest; changing order changes the
1005
+ set digest. Source precedence is consumer, repository, harness, user, then
1006
+ adapter defaults, but a lower-precedence source cannot weaken a higher one and
1007
+ the stricter safety rule wins. A configured required source that is absent,
1008
+ invalid, over limit, or unreadable produces
1009
+ `required-instruction-input-missing` or `invalid-instruction-source` before
1010
+ model or core invocation.
1011
+
1012
+ Instruction refusals are `invalid-instruction-input`,
1013
+ `required-instruction-input-missing`, `invalid-instruction-source`,
1014
+ `duplicate-instruction-source-id`, `instruction-source-limit-exceeded`, and
1015
+ `instruction-byte-limit-exceeded`.
1016
+
1017
+ ## 8. Explicit handoff and resume
1018
+
1019
+ An adapter MAY carry an explicit handoff carrier. It has no hidden retrieval
1020
+ mechanism and no authority by itself. The decoded handoff bytes contain this
1021
+ exact strict JSON object:
1022
+
1023
+ ```json
1024
+ {
1025
+ "handoff_version": 1,
1026
+ "workspace_id": "example-workspace",
1027
+ "instruction_set_digest": "sha256:f42a9b7fa06a5825cc6c5faf5a5ed2b217ecf65672aee9e40572feddb36e2578",
1028
+ "item": {
1029
+ "id": "wb_01KDWPVNG00000000000000000",
1030
+ "revision": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
1031
+ }
1032
+ }
1033
+ ```
1034
+
1035
+ The invoke envelope carries this exact carrier schema:
1036
+
1037
+ ```json
1038
+ {
1039
+ "handoff_carrier_version": 1,
1040
+ "workspace_id": "example-workspace",
1041
+ "content_encoding": "base64",
1042
+ "content_base64": "eyJoYW5kb2ZmX3ZlcnNpb24iOjEsIndvcmtzcGFjZV9pZCI6ImV4YW1wbGUtd29ya3NwYWNlIiwiaW5zdHJ1Y3Rpb25fc2V0X2RpZ2VzdCI6InNoYTI1NjpmNDJhOWI3ZmEwNmE1ODI1Y2M2YzVmYWY1YTVlZDJiMjE3ZWNmNjU2NzJhZWU5ZTQwNTcyZmVkZGIzNmUyNTc4IiwiaXRlbSI6eyJpZCI6IndiXzAxS0RXUFZORzAwMDAwMDAwMDAwMDAwMDAwIiwicmV2aXNpb24iOiJzaGEyNTY6MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJ9fQo=",
1043
+ "byte_length": 287,
1044
+ "sha256": "sha256:31a1d0490913a7117eb4ff50937ff265af41d385fc80e17593ba0a75550b260b",
1045
+ "resume_request": {
1046
+ "item_id": "wb_01KDWPVNG00000000000000000",
1047
+ "expected_revision": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
1048
+ "instruction_set_digest": "sha256:f42a9b7fa06a5825cc6c5faf5a5ed2b217ecf65672aee9e40572feddb36e2578"
1049
+ }
1050
+ }
1051
+ ```
1052
+
1053
+ No carrier, resume-request, handoff-object, or item member may be missing or
1054
+ extra. The adapter validates version 1, `base64`, canonical base64 bytes,
1055
+ length, digest, strict UTF-8 JSON including duplicate-member rejection,
1056
+ workspace binding, item ID/revision, and instruction-set digest. Item IDs in
1057
+ both `resume_request.item_id` and decoded `item.id` MUST match the canonical
1058
+ Wowbagger grammar `^wb_[0-7][0-9A-HJKMNP-TV-Z]{25}$`; a merely generic safe
1059
+ identifier is invalid. Carrier,
1060
+ decoded handoff, configured workspace, resume request, current inspected item,
1061
+ and current instruction set must agree. Handoff text cannot grant approval or
1062
+ enlarge limits.
1063
+
1064
+ Instruction decoded bytes plus decoded handoff bytes share the invocation's
1065
+ single `context_bytes` bound; their individual bounds do not create extra
1066
+ capacity. The handoff MUST be explicitly stored or
1067
+ delivered by the consumer or harness. The adapter MUST NOT create a hidden
1068
+ database, infer a prior session, or silently reload memory.
1069
+
1070
+ Handoff refusals are `handoff-digest-mismatch`, `invalid-handoff-carrier`,
1071
+ `invalid-handoff-bytes`, `handoff-limit-exceeded`, `invalid-handoff-json`, `invalid-handoff-object`,
1072
+ `invalid-handoff-resume-request`, `handoff-workspace-mismatch`,
1073
+ `handoff-resume-binding-mismatch`, `handoff-instruction-set-mismatch`,
1074
+ `handoff-item-mismatch`, and `handoff-stale-item-revision`.
1075
+
1076
+ On resume, an adapter re-negotiates capabilities and validates the ledger. If a
1077
+ handoff names an item or revision, it re-inspects and compares current bytes
1078
+ before a mutation. It MUST NOT auto-transition an item, renew a claim, commit,
1079
+ push, or treat a stale revision as permission to overwrite.
1080
+
1081
+ The current instruction-set digest and item revision MUST exactly match the
1082
+ resume request. A mismatch is a bounded refusal naming only the mismatched
1083
+ member and digests or revisions. The adapter retains no unlisted memory
1084
+ between invocations and does not search for handoff records.
1085
+
1086
+ ## 9. Installation and portability
1087
+
1088
+ The manifest lets a consumer register a package in a native harness, a local
1089
+ tool registry, or a generic command runner. The packaging contract does not
1090
+ require a network service, MCP, a daemon, Gastown, Beads, a particular package
1091
+ manager, a shell, or a model API.
1092
+
1093
+ Portable adapters use UTF-8, JSON, argument arrays, bounded standard streams,
1094
+ and logical forward-slash paths. They do not assume POSIX shell syntax,
1095
+ Unix-only signals, hard links, a global install location, or a particular
1096
+ working-directory convention. The core itself reports filesystem-dependent
1097
+ mutation capabilities per operation.
1098
+
1099
+ macOS, Linux, and Windows claims are separate. An adapter marks a platform
1100
+ `supported` only after the conformance suite runs successfully on that native
1101
+ platform with its real path and command runner. A current absence of a platform
1102
+ implementation is `unverified`, not `unsupported` and not a compatibility
1103
+ promise.
1104
+
1105
+ ## 10. Conformance
1106
+
1107
+ The synthetic vectors in [spec/fixtures/adapters](../spec/fixtures/adapters/)
1108
+ are normative. They contain no consumer source, policy, people, or ledger data.
1109
+ Each manifest is strict JSON and hashes every test artifact. The fixture test
1110
+ checks JSON duplicate-member rejection, hashes, safe relative paths, and the
1111
+ applicable target list. `direct-core` appears only on equivalence cases; path,
1112
+ authority, instructions, handoff, process containment, and adapter negotiation
1113
+ are adapter concerns and do not pretend that the core implements them.
1114
+
1115
+ The direct core is the baseline. A future adapter does not pass by emitting an
1116
+ equivalent-looking object; it passes by preserving the baseline core exit code
1117
+ and exact standard-output and standard-error bytes for each compatible case.
1118
+
1119
+ The implementation runner selects the runtime scenario only from each
1120
+ manifest's declared `mode`:
1121
+
1122
+ - `equivalence` launches a real core child for both the direct baseline and the
1123
+ shipped adapter transaction. It compares the exit code and exact stdout and
1124
+ stderr bytes; reconstructed output is not equivalent.
1125
+ - `negative-capability` supplies the declared capability profile, process
1126
+ observation, or refusal input. A process-level audit loads before the shipped
1127
+ entrypoint's modules and records asynchronous child resources plus successful
1128
+ calls to Node's public synchronous child-process APIs. The runner fails the
1129
+ case if that audit records a child, including a launch that bypasses the
1130
+ injected launcher or whose result is discarded.
1131
+ - `protocol` supplies the declared contract input, observation, or handoff to
1132
+ the applicable shipped contract implementation. Its bootstrap transaction
1133
+ uses a supplied core capability snapshot and does not launch a core child.
1134
+
1135
+ Every mode still starts the shipped adapter entrypoint as a real process and
1136
+ exercises the strict bootstrap wire for every case. Only `equivalence` uses a
1137
+ live core child because only that mode has a direct-core baseline to preserve.
1138
+ The `06-bounded-output` classification still uses its declared process
1139
+ observation. Its `output-bound` assertion also starts a runner-owned writer
1140
+ child through the production core launcher, writes one byte past the request's
1141
+ stdout limit, and requires prompt termination, an incomplete stdout marker,
1142
+ and retained output truncated to exactly that limit. Real stdout-limit
1143
+ enforcement is therefore part of the implementation-vector result rather than
1144
+ inferred only from the unit suite.
1145
+
1146
+ `node spec/run-adapter-vectors.js` is the executable reference-model runner.
1147
+ Forwarding and negative invocation cases enter through the strict raw-byte
1148
+ reference `invoke` function. The real core run is a baseline comparison inside
1149
+ that invoke path, never a substitute for the adapter boundary. The invoke
1150
+ function enforces `max_request_bytes` before JSON parsing or request-member
1151
+ access, then applies exact request/carrier schemas, version negotiation, the
1152
+ exact core capability probe, host capabilities, guarded paths, approval,
1153
+ bounded process observation, and complete outer result/refusal envelopes. The
1154
+ runner also evaluates deterministic protocol,
1155
+ path, authority, limit, mutation-recovery, and version models for adapter-only
1156
+ assertions. The standalone runner itself rejects duplicate-member JSON, verifies
1157
+ every artifact hash, fails on an unknown mode or assertion type, rejects any
1158
+ hashed artifact that no assertion consumes, and reports a reference
1159
+ function/evidence label for every executed assertion ID.
1160
+ It validates `adapter_vector_version` as exactly `1` before it evaluates an
1161
+ artifact. The test suite fails unless those IDs exactly equal every manifest
1162
+ assertion; it re-hashes wrong expectation artifacts and proves that every
1163
+ expected result is semantically rejected rather than merely read. Its status is
1164
+ `reference-pass`. It does not run a real Claude Code, Codex, Kimi, or generic
1165
+ adapter, so its implementation statuses remain `unverified`.
1166
+ `node spec/run-adapter-implementation.js` accepts the same fixture directory,
1167
+ evaluates transactions through the shipped Claude Code entrypoint and emits the
1168
+ same result shape with an evidence platform. Its current native Darwin run is
1169
+ `pass`: 183 of 183 assertions and 15 of 15 cases pass. That native common-vector
1170
+ evidence earns the Claude Code manifest's Darwin `supported` declaration.
1171
+ Codex, Kimi, and generic adapter implementations remain `unverified`.
1172
+
1173
+ The supported manifest assertion types are `core-baseline`, `capability`,
1174
+ `instruction-order`, `path-refusal`, `output-bound`, `approval-gate`,
1175
+ `resume-plan`, `platform-status`, `process-outcome`, `path-race`,
1176
+ `path-syntax`, `snapshot-identity`, `entrypoint-path`, `invoke-version`,
1177
+ `core-probe`, `negotiation`, `context-validation`, and `approval-schema`.
1178
+ The runner fails closed on any other type.
1179
+
1180
+ | Requirement | Direct core baseline | Claude Code adapter | Codex adapter | Kimi adapter | Generic OpenAI-compatible harness adapter |
1181
+ |---|---|---|---|---|---|
1182
+ | Core JSON, standard streams, and exit | Reference-pass | Implementation-pass (darwin) | Unverified | Unverified | Unverified |
1183
+ | Capability negotiation | Reference-pass for core probe | Implementation-pass (darwin) | Unverified | Unverified | Unverified |
1184
+ | Instruction input | Not a core concern | Implementation-pass (darwin) | Unverified | Unverified | Unverified |
1185
+ | Safe workspace and ledger selection | Core ledger checks only | Implementation-pass (darwin) | Unverified | Unverified | Unverified |
1186
+ | Bounded context and output | Reference bytes only | Implementation-pass (darwin) | Unverified | Unverified | Unverified |
1187
+ | Mutation authority and recovery | Core mutation contract only | Implementation-pass (darwin) | Unverified | Unverified | Unverified |
1188
+ | API-only transport refusal | Not applicable | Implementation-pass (darwin) | Unverified | Unverified | Unverified |
1189
+ | Resume and handoff | Not a core concern | Implementation-pass (darwin) | Unverified | Unverified | Unverified |
1190
+
1191
+ The API-only negative vector is intentionally different: it must refuse core
1192
+ invocation because model transport alone is not a coding harness. A generic
1193
+ OpenAI-compatible *harness* that advertises the required filesystem and command
1194
+ capabilities must meet the same forwarding vectors as the other adapters.
1195
+
1196
+ ## 11. Non-goals
1197
+
1198
+ This contract does not:
1199
+
1200
+ - adopt Wowbagger in PropertyCompass or any other consumer repository;
1201
+ - add vendor-specific lifecycle logic to the core;
1202
+ - require an MCP server, daemon, hook, slash command, Gastown, or Beads;
1203
+ - define work claims, policy ranking, or hidden persistent agent memory;
1204
+ - grant automatic Git commit, push, setup, installation, or configuration
1205
+ authority; or
1206
+ - implement a Claude Code, Codex, Kimi, or generic adapter.
1207
+
1208
+ ## 11.1 Concurrency
1209
+
1210
+ The adapter is a **one-shot CLI process**: each invocation runs to completion and
1211
+ exits. There is no daemon, no server socket, and no shared mutable state between
1212
+ invocations. This has three consequences:
1213
+
1214
+ 1. **One adapter process cannot serve overlapping invokes.** It reads one request,
1215
+ processes it, writes one response, and exits. If a host needs concurrent
1216
+ execution, it must spawn multiple adapter processes.
1217
+
1218
+ 2. **Concurrent adapter processes against the same ledger are independent.** The
1219
+ adapter has no concurrency control; serialization happens at the core level
1220
+ through the lock file. Two simultaneous mutations may both pass approval and
1221
+ launch, but the core's lock serializes writes.
1222
+
1223
+ 3. **Each invocation enforces its own stdout/stderr limits independently.**
1224
+ Buffer state is per-process; one invocation hitting its limit does not affect
1225
+ another's limit enforcement.
1226
+
1227
+ This behavior is verified by the concurrent-invocation test suite. The contract
1228
+ does not require the host to serialize invokes; it simply provides no coordination
1229
+ beyond what the core offers.
1230
+
1231
+ ## 12. Adapter contract version 2
1232
+
1233
+ Version 2 retains sections 1 through 11, including the strict bootstrap wire,
1234
+ path guards, exact stream forwarding, bounded process observation, consumer
1235
+ approval, instruction input, explicit handoff, recovery precedence, and public
1236
+ error registry. It changes only this versioned surface:
1237
+
1238
+ | Path or behavior | Version 2 requirement |
1239
+ |---|---|
1240
+ | Manifest `adapter_contract_versions` | Exactly `[2]` |
1241
+ | Manifest and describe required core contract | Exactly `2` |
1242
+ | Describe `selected_adapter_contract_version` | Exactly `2` |
1243
+ | Describe `core.commands` order | `capabilities`, `create`, `inspect`, `patch`, `ready`, `transition`, `validate` |
1244
+ | Invoke and result `adapter_contract_version` | Exactly `2` |
1245
+ | Independently probed core `contract_version` | Exactly `2` |
1246
+
1247
+ The version 2 core capability probe adds exactly
1248
+ `operations.patch: {"supported":true,"write_scope":"single-item","cas_scope":"exact-byte-sha256"}`.
1249
+ Its mutation backend scope is always
1250
+ `same-working-copy-cooperative-writers`,
1251
+ `limits.cross_worktree_coordination` is always `false`, and
1252
+ `limits.multi_item_atomicity` remains `false`. The separately probed
1253
+ `operations.work_claim.supported` may still be `true` when claims are visible
1254
+ through the Git common directory. That visibility may set
1255
+ `optional_features.claims: true`; it MUST NOT elevate mutation coordination or
1256
+ safe exclusive dispatch. Provisioned ledger-specific claim capabilities may
1257
+ separately advertise merge-coordinated claim-protected publication.
1258
+
1259
+ Version 2 accepts a `patch` core request with exactly `command`, `ledger`, and
1260
+ `input_base64`. It launches the argument vector
1261
+ `["patch","--ledger","<resolved>","--input","-","--json"]`, sends the exact
1262
+ decoded bytes to standard input, and treats
1263
+ patch as a mutation for trusted consumer approval, process doubt, and
1264
+ revision-based recovery. It does not accept an arbitrary input-file path from
1265
+ the invocation.
1266
+
1267
+ Bootstrap wire version 1 and the manifest, approval, instruction-input,
1268
+ handoff, and adapter-vector format versions remain 1; they are separate
1269
+ version domains. The version 1 adapter and core contracts remain defined by
1270
+ the preceding sections and are not widened to include patch.
1271
+
1272
+ A v1-only consumer sending `supported_adapter_contract_versions: [1]` to a
1273
+ shipped v2 adapter exits successfully at the bootstrap process level but
1274
+ receives exactly the compact refusal
1275
+ `{"ok":false,"error":{"code":"unsupported-adapter-contract-version"}}` plus
1276
+ one LF. It receives no v2 describe result and no requested core child is
1277
+ launched. Conversely, a v1 adapter probing a v2 core observes
1278
+ `contract_version: 2` and refuses the pairing as
1279
+ `core-contract-version-mismatch`; neither direction silently receives the
1280
+ other version's behavior.