@awebai/oats 0.24.2 → 0.24.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.
package/bin/oats.mjs CHANGED
@@ -49,6 +49,10 @@ import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_
49
49
  import { capturedSelector } from "../lib/captured-selector.mjs";
50
50
  import { inspectCapturedPiOutcome } from "../lib/captured-pi-host.mjs";
51
51
  import { readCapturedResolution } from "../lib/captured-resolutions.mjs";
52
+ import { readLock3 } from "../lib/portable-lock.mjs";
53
+ import { oatsError } from "../lib/errors.mjs";
54
+ import { readPortableBytes } from "../lib/portable-files.mjs";
55
+ import { canonicalJson, parseStrictJson } from "../lib/portable-values.mjs";
52
56
  import { readPortablePreparationRequest } from "../lib/portable-onboarding-request.mjs";
53
57
  import { portableScope } from "../lib/portable-state.mjs";
54
58
  import { CAPTURED_OPERATION_TIMEOUT_MS, runCapturedOperationProcess } from "../lib/captured-operation-process.mjs";
@@ -99,15 +103,40 @@ const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok:
99
103
 
100
104
  function inspectOnboardingCmd() {
101
105
  const fail = (code, message) => JSON_MODE ? jsonFail(code, message) : die(message);
102
- let file;
106
+ const values = new Map();
103
107
  for (let index = 1; index < args.length; index++) {
104
108
  if (args[index] === "--json") continue;
105
- if (args[index] !== "--request" || file !== undefined || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "source inspection accepts one --request <absolute-json> and --json only");
106
- file = args[++index];
109
+ const key = args[index];
110
+ if (!["--request", "--emit-prepare-request"].includes(key) || values.has(key) || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "source inspection accepts one --request <absolute-json>, optional --emit-prepare-request <new-absolute-json>, and --json");
111
+ values.set(key, args[++index]);
107
112
  }
108
113
  try {
109
- const input = readPortablePreparationRequest({ file });
110
- const result = inspectPortableOnboarding(input);
114
+ const output = values.get("--emit-prepare-request");
115
+ let parent;
116
+ const checkOutput = () => {
117
+ if (!isAbsolute(output) || resolve(output) !== output || output.includes("\0")) throw oatsError("E_BAD_ARGS", "prepare-request output needs a normalized absolute path");
118
+ let stat;
119
+ try {
120
+ stat = lstatSync(dirname(output));
121
+ if (!stat.isDirectory() || realpathSync(dirname(output)) !== dirname(output)) throw new Error();
122
+ } catch { throw oatsError("E_BAD_ARGS", "prepare-request output parent must be an existing real directory"); }
123
+ if (parent && (parent.dev !== stat.dev || parent.ino !== stat.ino)) throw oatsError("selection-changed", "prepare-request output parent changed during inspection");
124
+ try { lstatSync(output); }
125
+ catch (error) { if (error.code === "ENOENT") return stat; throw oatsError("E_BAD_ARGS", "prepare-request output could not be checked"); }
126
+ throw oatsError("E_BAD_ARGS", "prepare-request output already exists; choose a new file (nothing overwritten)");
127
+ };
128
+ if (output !== undefined) parent = checkOutput();
129
+ const input = readPortablePreparationRequest({ file: values.get("--request") });
130
+ const { prepareRequest, ...view } = inspectPortableOnboarding(input, { includePrepareRequest: output !== undefined });
131
+ let result = view;
132
+ if (output !== undefined) {
133
+ checkOutput();
134
+ try { writeFileSync(output, canonicalJson(prepareRequest) + "\n", { flag: "wx", mode: 0o600 }); }
135
+ catch { throw oatsError("E_BAD_ARGS", "prepare-request output could not be created exclusively; no existing file was overwritten"); }
136
+ const deployment = view.deployment.deployment.path;
137
+ result = { ...view, prepareRequestFile: output, effects: { ...view.effects, requestFileWrite: true,
138
+ deploymentWrites: output === deployment || output.startsWith(deployment + sep) } };
139
+ }
111
140
  if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
112
141
  } catch (error) { fail(error.code || "E_INSPECT_FAILED", error.message); }
113
142
  }
@@ -142,7 +171,12 @@ function prepareCmd() {
142
171
  // Pass the whole request to the one public validator/resolver. Unknown
143
172
  // fields are refused there, never filtered or filled from ambient state.
144
173
  const result = prepareCapturedComposition(input);
145
- if (!result.resolution) fail("needs-configuration", "preparation is incomplete; no executable resolution was published", result);
174
+ if (!result.resolution) {
175
+ const summary = "preparation is incomplete; no executable resolution was published";
176
+ const reasons = (result.problems ?? []).filter(p => p.key !== undefined || (p.slot && p.capability))
177
+ .map(p => `[${p.slot && p.capability ? `${p.capability}/${p.slot}` : p.code}] ${p.key === undefined ? "" : `${JSON.stringify(p.key)}: `}${p.message}`);
178
+ fail("needs-configuration", JSON_MODE ? summary : [summary, ...reasons].join("\n"), result);
179
+ }
146
180
  if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
147
181
  } catch (error) { fail(error.code || "E_PREPARE_FAILED", error.message); }
148
182
  }
@@ -2446,6 +2480,28 @@ function trust() {
2446
2480
  if (!id || id.startsWith("--")) { cmdFail("E_USAGE", "usage: oats trust <capability> [--dir <dir>] | oats trust <package> --all-capabilities [--dir <dir>]"); return; }
2447
2481
  const dir = dirFlag();
2448
2482
  const all = args.includes("--all-capabilities");
2483
+ // A v3 deployment needs an EXPLICIT immutable artifact-set selection. Never
2484
+ // guess one or reinterpret its lock through the classic approval engine.
2485
+ try {
2486
+ for (const scope of lockLevelsUp(dir).reverse()) {
2487
+ // Discriminate with the shared bounded decoder only. Classic readers
2488
+ // retain all v1/v2 validation, including implicit (versionless) v1 locks.
2489
+ let version;
2490
+ try { version = parseStrictJson(readPortableBytes(join(scope, OATS_LOCK_FILE)))?.lockfileVersion; }
2491
+ catch { continue; } // Let the existing classic reader diagnose its input.
2492
+ if (version !== 3) continue;
2493
+ const portable = readLock3(scope).lock;
2494
+ if (!portable) throw oatsError("selection-changed", "selection lock disappeared; repeat explicit trust selection");
2495
+ const sets = [...new Set(Object.values(portable.selections).map(row => row.available).filter(key => key && Object.hasOwn(portable.artifactSets[key].capabilities, id)))].sort();
2496
+ const commands = sets.map(artifactSet => ({ artifactSet,
2497
+ command: `oats trust ${shellQuote(id)} --deployment ${shellQuote(scope)} --artifact-set ${shellQuote(artifactSet)}` }));
2498
+ const message = commands.length && !all
2499
+ ? `lock v3 requires exact artifact-set approval; run ${commands.map(item => item.command).join(" or ")}`
2500
+ : `lock v3 approvals are per capability; use prepare's selections[].artifactSet for each capability in approvalRequired[]: oats trust <capability> --deployment ${shellQuote(scope)} --artifact-set <sha256-artifact-set>`;
2501
+ if (JSON_MODE) jsonFail("needs-configuration", message, { deployment: scope, commands });
2502
+ die(message);
2503
+ }
2504
+ } catch (error) { cmdFail(error.code || "invalid-lock", error.message); return; }
2449
2505
  // Package-backed approval path (per-capability, or explicit bulk on a package id).
2450
2506
  let pkgs, locks;
2451
2507
  try { pkgs = listInstalledPackages(dir); locks = readPackageLocks(dir); } catch (e) { cmdFail(e.code || "invalid-lock", e.message || e); return; }
@@ -5361,8 +5417,10 @@ The turn record (core — every conversation captured, searchable, replicated):
5361
5417
  packages/experimental/README.md
5362
5418
 
5363
5419
  oats inspect --request <absolute-json-file> [--json]
5364
- read-only fresh source/workspace/member metadata;
5365
- no current config, provider execution or preparation authority
5420
+ [--emit-prepare-request <new-absolute-json-file>]
5421
+ fresh source/workspace/member metadata; optional private
5422
+ request export uses the existing fresh-request builder;
5423
+ no provider execution or preparation authority
5366
5424
  oats prepare --request <absolute-json-file> [--json]
5367
5425
  complete public preparation input; no mixed flags,
5368
5426
  inherited binding, implicit setup or launch authority
@@ -5374,6 +5432,10 @@ The turn record (core — every conversation captured, searchable, replicated):
5374
5432
  same preparation through workspace imports
5375
5433
  oats inspect --deployment <abs> --resolution <id> [--composition] [--json]
5376
5434
  [--helper <exact-map-key>] inspect retained source/helper inputs, not today's configuration
5435
+ oats trust <capability> --deployment <abs> --artifact-set <sha256-…> [--json]
5436
+ approve one exact prepared artifact; use each
5437
+ selections[].artifactSet for capability ids
5438
+ in prepare's approvalRequired[] at that selection
5377
5439
  oats trust <capability> --deployment <abs> --resolution <id> [--json]
5378
5440
  explicitly approve that exact captured artifact
5379
5441
  oats <namespace> <command> --deployment <abs> --resolution <id> -- [args…]
@@ -178,7 +178,21 @@
178
178
  "version": { "const": 1 },
179
179
  "normalize": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
180
180
  "bind": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
181
- "check": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }
181
+ "check": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
182
+ "reasons": {
183
+ "description": "Fixed nonsecret diagnostic strings allowed across the binding wire by exact match only. 1-64 unique printable ASCII strings of 1-200 characters, no braces/interpolation, operator values or paths. Omission uses the kernel's per-capability compatibility list when available; an invalid declaration never falls back.",
184
+ "type": "array",
185
+ "minItems": 1,
186
+ "maxItems": 64,
187
+ "uniqueItems": true,
188
+ "items": { "type": "string", "minLength": 1, "maxLength": 200, "not": { "pattern": "[{}]|[^\\x20-\\x7e]" } }
189
+ },
190
+ "keys": {
191
+ "description": "Owned operator-key declarations: exact names or trailing-dot namespaces, no other pattern syntax. Kernel 0.24.4 validates shape only; filtering, overlap checks and ownership attribution are deferred to 0.25. Providers must ignore foreign keys themselves.",
192
+ "type": "array",
193
+ "uniqueItems": true,
194
+ "items": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_-]*\\.?$(?![\\s\\S])" }
195
+ }
182
196
  }
183
197
  },
184
198
  "operations": {
@@ -36,7 +36,34 @@ result. All structured responses exit 0: transport completed, while `ok` is the
36
36
  semantic outcome. Nonzero exit/signal/timeout is transport failure. Allowed error/problem codes are needs-configuration, requirement-conflict,
37
37
  invalid-binding, authorization-required, host-requirement-missing,
38
38
  provider-unavailable and provider-not-qualified. Optional provider error/problem
39
- `message` is permitted but never forwarded by the kernel.
39
+ `message` is permitted. The 0.24.4 follow-up retains it ONLY when it exactly
40
+ matches a fixed nonsecret reason in the VERIFIED selected capability manifest's
41
+ optional `binding.reasons` array: **1–64 unique strings**, each **1–200 printable
42
+ ASCII characters**, with no braces/interpolation markers. If the field is absent,
43
+ the kernel's reviewed per-capability compatibility list applies; a present invalid
44
+ or empty declaration refuses, never falls back. No trimming, Unicode normalization,
45
+ interpolation, prefix matching, operator values, paths, or unlisted provider output
46
+ cross this boundary. Code-only replies
47
+ and unknown/unlisted messages keep the existing kernel template fallback.
48
+
49
+ The allowlist is out-of-band kernel input, never declared by a provider response.
50
+ Exact artifact approval is still required BEFORE invoking the codec. The broker
51
+ preserves the vetted message and preparation rechecks it against the same selected
52
+ manifest, retaining slot/capability/origins. JSON and human CLI diagnostics surface
53
+ the reason; human output also shows an existing choice `key` when provided. This
54
+ changes no readiness status, launch authority, credential contract or wire version.
55
+ Older kernels reject the new optional manifest fields; providers declaring them
56
+ must floor on the reasons-capable **0.24.4** kernel. The compatibility list serves
57
+ older manifests, not a way around declaration validation. It includes the complete
58
+ 30-message aweb1.11.0 codec vocabulary (including non-ready check reasons) and the
59
+ seven OKF2.1.2 setting messages; it invents none for code-only OKF2.1.1.
60
+
61
+ `binding.keys` is also accepted with **shape validation only** in0.24.4: a unique
62
+ array of exact names or trailing-dot namespaces (`wider`, `stores.`), each matching
63
+ `^[A-Za-z][A-Za-z0-9_-]*\\.?$` over the WHOLE string (no trailing newline). There
64
+ is no filtering, overlapping-ownership check or owned/unowned-key attribution yet;
65
+ those are deferred to0.25. Providers still receive the complete map and MUST ignore
66
+ foreign keys themselves. Declaring keys does not authorize diagnostic text.
40
67
 
41
68
  Knowledge providers and harvesters follow the same separation: the
42
69
  [knowledge capability boundary](2026-09-16-knowledge-capability-contract.md) keeps
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Purpose:** the one accurate view of every work stream in the redesign, what is on main, what is in flight, who owns it, and what blocks it. Lead: `oats-expert` (redesign lead). Updated whenever anything merges, is returned, or reality changes. Older per-lane boards are superseded by this file.
4
4
 
5
- **Last update:** 2026-09-21 04:10Z · main `feef2a2f`+ · **OATS v0.24.2 tagged (CI)** · OKF v2.1.1 · oats-framework/v1.1.1 · aweb v1.11.0 · oats-knowledge 8d67eab4
5
+ **Last update:** 2026-09-21 15:15Z · main `ef211d3e`+ · OATS v0.24.4 cutting (PR41 merged) · OKF v2.1.1 · oats-framework/v1.1.1 · aweb v1.11.0 · oats-knowledge 8d67eab4
6
6
 
7
7
  Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
8
8
 
@@ -11,7 +11,7 @@ Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, n
11
11
  | # | Stream | State | Owner | Next action |
12
12
  |---|---|---|---|---|
13
13
  | S1 | Knowledge capability contract rework (kernel↔provider boundary, OKF 2.x) | ✅ OATS 0.24.1 / OKF 2.1.1 published | P | done for this phase |
14
- | S2 | Workspace/Portable Souls adoption of the OATS repos | ✅ workspace + seven indexes + **six imports** (five experts @caa341f3, setup expert @0aad753c) · ✅ second-operator gate run · 🔄 five seams → L, target 0.24.3 | lead, L, Antares | seams PR; Antares re-run on 0.24.2 (aweb 1.11.0) |
14
+ | S2 | Workspace/Portable Souls adoption of the OATS repos | ✅ 0.24.3 gate run: knowledge slot resolves via the workspace route; seams 1–4 fixed, 5 attributed · ✅ **messaging source defect found and fixed (906b1558: per-human policy + soul teams)**, imports repinned f3ee31e0 · 🔄 final re-run for a published resolution + OKF check probe | lead, Antares | re-run; then S2 closed |
15
15
  | S3 | Messaging capability readiness on the new infrastructure (aweb) | ✅ aweb 1.11.0 released · ✅ **catalog + six editions pin v1.11.0 (0.24.2)** · ⬜ second-operator re-run | P, lead | Antares re-run |
16
16
  | S4 | Official capabilities `oats.core` / `oats.setup` + explicit default + onboarding `oats-setup-expert` | ✅ D1, D2, **D3 merged (PR35)**: `oats onboard` verified live (acquire 1.1.1 → setup expert with both caps → scaffold composes the five capability skills, no legacy) · `oats.framework` 1.1.1 tagged | P, L | done; Desktop surfaces → S8 |
17
17
  | S5 | Official marketplace = reviewed list in oats repo | ✅ D4 merged · ✅ `oats.framework` 1.1.1 listed (`oats.core`, `oats.setup`, `oats.knowledge-theory` aliases) | M | Desktop view → S8 |
@@ -33,12 +33,31 @@ Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, n
33
33
  - ⬜ Fresh local deployment from the shared definition (P1.5) — the real acceptance gate.
34
34
 
35
35
  ## S2 — second-operator gate report (Antares, Juan's machine, 2026-09-20)
36
- Fresh dir, local `@awebai/oats@0.24.1`, no prior state. `inspect --request` → ready-for-preparation, membership eligible, source `souls/oats-kernel-expert@caa341f3`. `prepare` resolved and materialized `oats-package@caa341f3`, `oats.okf@v2.1.1`, `oats.aweb@v1.10.3`, `oats.core`, `oats.setup`, `oats.knowledge-theory` + soul; artifact-set approvals worked. Terminal: `needs-configuration` + `provider-not-qualified` (aweb 1.10.3 has no binding interface) — expected. Seams (assigned to L, one PR, priority):
36
+ Fresh dir, local `@awebai/oats@0.24.1`, no prior state. `inspect --request` → ready-for-preparation, membership eligible, source `souls/oats-kernel-expert@caa341f3`. `prepare` resolved and materialized `oats-package@caa341f3`, `oats.okf@v2.1.1`, `oats.aweb@v1.10.3`, `oats.core`, `oats.setup`, `oats.knowledge-theory` + soul; artifact-set approvals worked. Terminal: `needs-configuration` + `provider-not-qualified` (aweb 1.10.3 has no binding interface) — expected. Seams — **all fixed in PR36 (913c4f9e), shipped in 0.24.3**:
37
37
  1. `inspect --request` requires `workTarget`; `prepare --request` refuses it (`buildFreshPreparationRequest` exists but the CLI never uses it).
38
38
  2. `prepare` on the absent deployment inspect blessed → raw `ENOENT` + host path through the JSON envelope.
39
39
  3. `prepare` writes lock v3; `oats trust <cap> --dir` rejects it (`unsupported lockfileVersion 3`) → dead end from `--help`.
40
40
  4. The working `trust --deployment --artifact-set <sha256>` route is absent from `--help`.
41
- 5. Problems carry `origins: []` and no slot/capability; aweb's missing interface masks OKF diagnostics — a valid and a bogus `stores.oats` binding produce byte-identical output. **Fix first.**
41
+ 5. Problems carry `origins: []` and no slot/capability, so with more than one unresolved slot the operator cannot tell which slot a `needs-configuration` refers to (Antares needed three runs and an ablation table). **Root cause of the original identical pair (L's trace, confirmed by P and by Antares' settings-aware run):** both OKF `normalize` calls refused because the request had no OKF runtime settings (`bindings-file`, `state-dir`); the identical pair is correct output for that input. With settings, OKF normalize diagnostics DO reach the operator (a malformed `acceptedBranch` yields a distinct `invalid-binding`); a structurally valid locator to a nonexistent repo is correctly not distinguished until OKF `check`. Fix (0.24.3, PR36) = attribution by slot/capability/phase/origins + kernel-fixed messages. Precision: the kernel names the missing item only where it knows it (missing binding interface: id/version/slot); for a provider `needs-configuration` it cannot name `bindings-file`/`state-dir` because OKF 2.1.1's wire is code-only — OKF 2.1.2 (assigned to P) adds the fixed safe message naming the setting. Antares' earlier "no signal at all" framing is withdrawn by its author.
42
+
43
+ ## S2 — second-operator re-run on 0.24.2 (Antares, Juan's machine, 2026-09-21)
44
+ - Found the five expert imports still pinned at `caa341f3` (aweb 1.10.3) while the setup expert was at `0aad753c` — the one repinned soul was the one that never exercises aweb. Fixed: all six imports at v0.24.3 `3156e4de` (638206b9); new layout guard fails when a pin's provider requirements lag the current edition (3ce40aaa, verified to catch the miss).
45
+ - **aweb 1.11.0 qualifies**: via the direct source route on current main, `provider-not-qualified` disappeared → `approval-required` → after approval both slots on the settings hold. Confirms L's root cause.
46
+ - `oats onboard --workspace git:github.com/awebai/oats` from a fresh dir: clean (acquired 1.1.1, both caps, spawn printed not run); scaffold composed exactly the five capability skills, both trusted, no hooks.
47
+ - Seams 1–5 reproduced identically on 0.24.2 (baseline); fixed in 0.24.3. Gate decisions given: `harvest-model` arbitrary for the gate; aweb slot `delivery: session` without a private-team binding → expected typed `needs-configuration` naming the binding (Juan's team identity is never guessed).
48
+
49
+ ## S2 — settings-aware run (Antares, direct source route on main, aweb 1.11.0, 2026-09-21)
50
+ - With `bindings-file`/`state-dir`/`harvest-runtime: pi` and the public `oats-knowledge` bound as base `oats`: **the knowledge slot binds** (OKF problem gone). `harvest-model` is optional per manifest; aweb 1.11.0 declares exactly one setting (`delivery`).
51
+ - **aweb 1.11.0 is the only remaining hold** (`needs-configuration`, identical for `delivery: session` and `channel` on 0.24.2's unattributed output). No resolution publishes → OKF `check` probe not reachable yet. 0.24.3 run will show the attributed aweb message (expected: private-team binding / Pi session-input).
52
+
53
+ ## S2 — 0.24.3 gate run (Antares, Juan's machine, 2026-09-21) — verdict
54
+ - Seams 1, 3, 4 **fixed**; seam 2 fixed as typed (message does not echo the path — deliberate, consistent with no host diagnostics; expectation corrected); seam 5 **half**: attribution (slot/capability/full origins) fixed; naming the missing item only where the kernel knows it. The specificity half is provider-side: OKF 2.1.2 (assigned) and an aweb follow-up.
55
+ - Six imports resolve at the repinned revision; workspace route selects aweb 1.11.0; **the knowledge slot fully resolves through the workspace route** with `bindings-file`/`state-dir`/`harvest-runtime` (`harvest-model` confirmed optional).
56
+ - **Messaging was a SOURCE defect, not an operator gap**: aweb 1.11.0 normalize requires the workspace to declare `teams: {private: per-human}` and the soul a messaging declaration; neither was published, so the slot was unreachable by any input. Fixed on main **906b1558** (workspace policy + `teams: []` on the five messaging editions; verified against aweb v1.11.0 normalize) and imports repinned **f3ee31e0**. Remaining operator inputs after the fix: `responsibleHuman` (required) and `wider` (required, may be empty) — the private team is a later `check` outcome.
57
+ - **S2 exit-gate verdict (amended per the operator):** knowledge — an outside operator on 0.24.3 discovers, resolves, approves and binds from public sources, remaining item attributed: **PASS**. Messaging — on 0.24.3 as published the operator hit a source defect wearing a configuration error; fixed on main 906b1558 and to be re-proven on the repinned imports before it is written as PASS.
58
+ - **Re-run on 906b1558 (operator, b875669f):** messaging normalize passes; remaining problem is `responsibleHuman`, attributed. But end-to-end is **deadlocked**: `wider: []` (messaging requires) makes OKF 2.1.1 return `invalid-binding` on knowledge (it validates every `operator.bindings` key as a store locator); without it messaging needs `wider`. Operator's wording adopted for S2: *a second operator reaches a published, resolvable configuration boundary for knowledge; messaging is blocked by a kernel-level binding-namespace collision, not by operator input or identity.* Not Juan's to unblock; no identity requested. [Decision](https://github.com/awebai/oats/blob/main/agents/oats-expert/soul/knowledge/decisions/operator-bindings-ownership.md): flat map with declared ownership (`binding.keys`); providers ignore foreign keys (OKF 2.1.2, aweb 1.11.1); kernel attributes/refuses stray keys by name — **deferred to 0.25 by L's scope call (accepted): overlap refusal, filtered forwarding, owned/unowned attribution and the undeclared-provider path need their own regression matrix; the provider ignore rule is the floor and ships in 0.24.4's provider releases.**
59
+ - ✅ **PR41 merged `ef211d3e`** (L): inspect accepts prepare's superset (`ignored`), provider reasons cross the wire by exact match (manifest `binding.reasons` / bundled lists), `binding.keys` shape-only, CLI renders `key`. Full gate 1657/1653/0/4 twice (L + maintainer). → **v0.24.4**, kernel-first; OKF 2.1.2 / aweb 1.11.1 floor `>=0.24.4` (closed validator on ≤0.24.3 rejects the fields — P's finding).
60
+ - Two further kernel findings from the repeat (assigned to L, 0.24.4): (a) seam 1 converged one way only — `inspect --request` still refuses prepare's `operator`/`launch`; (b) **the kernel discards the adapter's reason** at the wire (`provider-binding-wire.mjs:23`) and templates it; aweb already sends whitelisted safe reasons. New [decision](https://github.com/awebai/oats/blob/main/agents/oats-expert/soul/knowledge/decisions/provider-problem-reasons-cross-the-wire.md): whitelisted fixed reasons cross the wire (`binding.reasons`), free text still refused.
42
61
 
43
62
  ## S3 — Messaging (aweb) on the new infrastructure
44
63
  - ✅ **aweb PR3 merged → v1.11.0 (93f8ab96)**: `binding {normalize,bind,check}` on the existing wire; `check` = HOME-route operational custody only (explicit private team, `delivery: session`, kernel ≥0.24.2 via caller-owned `OATS_CLI_BIN`, retained `launchSelection` must be input-capable Claude/Codex; strict-Pi print → `needs-configuration`, never downgraded). Native adapter over existing `aw` commands with physical identity-dir custody and redacted tokens. Standalone 30/0; coupling 14/0 vs kernel b92f0d07. PR33 (launchSelection projection, OATS_CLI_BIN in codec env) merged b92f0d07.
@@ -9,9 +9,18 @@ owned; production capability/profile readiness is not established by the fixture
9
9
 
10
10
  ```sh
11
11
  oats inspect --request /absolute/inspection.json --json
12
+ # Optional explicit request export (new private file; never overwrite):
13
+ oats inspect --request /absolute/inspection.json --emit-prepare-request /absolute/preparation.json --json
12
14
  ```
13
15
 
14
- This mode accepts only one request file and `--json`. Explicit captured selectors
16
+ This mode accepts one request file, optional `--emit-prepare-request`, and `--json`.
17
+ The 0.24.4 follow-up also accepts the complete preparation request's `operator`,
18
+ `launch`, `helperLaunches`, `mode`, and `allowLocalPaths` fields. Inspection ignores
19
+ their semantics: it does not validate provider payloads, select a runtime/model,
20
+ execute a codec, or authorize local acquisition. `ignored: [...]` lists only the
21
+ present field NAMES in a stable order; values stay out of the metadata view and
22
+ `omitted.*` remains true. Preparation still validates those fields normally.
23
+ Unknown fields remain errors. Explicit captured selectors
15
24
  or current-context flags conflict before file reads; inherited captured environment
16
25
  is not new-work input. Other existing inspect modes are unchanged. The shared
17
26
  bounded strict JSON request reader feeds the existing inspection validator intact:
@@ -66,21 +75,53 @@ have not been classified by their owner and are omitted. Import summaries expose
66
75
  `payloadOmitted`. Top-level `omitted:{providerPayloads:true,adoptionValues:true}`
67
76
  states that this is a metadata view, not a lossless request or a safe-payload claim.
68
77
  It is not an issued `buildFreshPreparationRequest` witness, even in the same
69
- process. Keep the original authored input for an explicit preparation request.
78
+ process. The opt-in `--emit-prepare-request` route calls that existing builder
79
+ on the real in-process inspection before dropping the private witness. It writes
80
+ only `.preparation` to a new mode-0600 file at an explicit normalized absolute
81
+ path with an existing real parent; existing files/symlinks are refused, never
82
+ overwritten. JSON output names `prepareRequestFile` and records the explicit
83
+ request-file write in `effects`; it does not echo the request contents. The
84
+ export can carry unclassified adoption declarations and must remain private;
85
+ requests must contain nonsecret values or credential references, never secrets.
86
+ A held inspection cannot emit a fresh preparation request. Core callers can
87
+ explicitly request this data via `{includePrepareRequest:true}`; the default
88
+ metadata projection and its omissions are unchanged.
89
+
90
+ The explicit export preserves authored prepare-only fields privately through the
91
+ existing builder, without interpreting them; it must not silently drop operator
92
+ bindings or launch/helper choices. They remain unvalidated until preparation.
93
+ This does not expose their values in normal metadata or turn ignored values into
94
+ inspection authority.
95
+
96
+ The file is reusable new-work input, NOT a stored resolution, approval, admission,
97
+ or serialized ready-inspection permission. Preparation performs fresh validation
98
+ and observations, including re-resolving any mutable source selectors. Provider
99
+ configuration still requires explicit operator choices; conversion invents none.
70
100
 
71
101
  Existing managed deployment state is preserved and reported, not repaired or
72
102
  migrated. An absent selected path requires explicit operator provisioning and
73
- reinspection. The serialized inspection does not lock the filesystem or authorize
103
+ reinspection. Prepare refuses it with typed `needs-configuration` and provisioning
104
+ guidance before fetching or writing managed state, not a raw ENOENT. Inspection's
105
+ `ready` means the path is eligible for fresh setup, not already provisioned.
106
+ The serialized inspection does not lock the filesystem or authorize
74
107
  later mutation; preparation retains its own existing validation/custody rules.
75
108
  The inspected work target does not become source identity or an implied placement
76
109
  choice. Supported captured directory scaffolds own their separate H/work.
77
110
 
78
111
  ## Existing preparation and retained execution
79
112
 
80
- `oats prepare --request` already accepts deployment/source/origin, workspace/member
81
- OR standalone context, operator policy/bindings, mode/local-input authorization,
82
- launch and helperLaunches. Do not pass the inspection result or workTarget/catalog
83
- wrapper. Exact executable approval is separate. A required provider whose binding
113
+ `oats prepare --request` accepts deployment/source/origin, optional `workTarget`,
114
+ workspace/member OR standalone context, operator policy/bindings,
115
+ mode/local-input authorization, launch and helperLaunches. The original minimal
116
+ inspection request (without inspection-only `catalogIndexes`) is also accepted;
117
+ use the converter rather than stripping fields from a metadata/result wrapper.
118
+ Explicit `workTarget` is validated with the same physical existing-directory
119
+ validator as inspection and returned as work-context metadata. It takes precedence
120
+ over any caller assumption about cwd: no cwd/config fallback selects its value.
121
+ Omitting it preserves prior preparation behavior without inventing a placement.
122
+ It does not change source identity, `operator.localBase`, work mode, or captured
123
+ H/work placement. Do not pass an inspection result/catalog wrapper or private
124
+ scratch `directory` to preparation. Exact executable approval is separate. A required provider whose binding
84
125
  code is unapproved may return `needs-configuration` with an `approval-required`
85
126
  problem and exact artifact-set/capability requests, before any record exists:
86
127
 
@@ -92,6 +133,48 @@ oats spawn <subject> --deployment <D> --resolution <R> --home <new-H> --no-launc
92
133
  oats session start --deployment <D> --resolution <R> --home <H> --request /absolute/native.json --json
93
134
  ```
94
135
 
136
+ Each `selections[]` row carries an `artifactSet` id and an `approvalRequired[]`
137
+ array of **capability ids**. Pair them: pass the capability to `trust` and that
138
+ row's artifact-set id to `--artifact-set`. These are not interchangeable ids.
139
+ `trust <capability> --dir <D>` against a v3 deployment now refuses with typed
140
+ `needs-configuration` and exact available-set commands; it never picks or approves
141
+ a set automatically. Classic v1/v2 trust remains the classic route.
142
+
143
+ Provider preparation problems carry `slot` and `capability`, plus original
144
+ provenance where available. A no-interface provider is identified with kernel-known
145
+ manifest/version facts. Other supported slots still normalize, resolve through
146
+ the same choice engine, and bind if their own choices are resolved; any required
147
+ slot problem still prevents publication. Provider free text is not passed through.
148
+ The 0.24.4 follow-up preserves only exact fixed reasons declared by the selected
149
+ manifest (or its reviewed kernel compatibility list when absent); see the
150
+ [binding wire](2026-09-16-provider-binding-wire.md). Human CLI output also shows a
151
+ problem's existing choice key, without inventing new key/provider semantics.
152
+ Different opaque inputs need not produce different public errors if both fail the
153
+ same provider prerequisite. In particular, missing OKF host runtime settings can
154
+ hold both syntactically valid Git locators; preparation does not test whether a
155
+ remote repository exists. Required readiness checks remain later, with the provider.
156
+
157
+ ### Operator input
158
+
159
+ When present, `operator` requires both `policy` (object; `{}` is valid) and
160
+ `document` (`{ "kind": "operator", "id": "setup-attempt" }`). Its ONLY optional
161
+ fields are `localBase`, `allowLocalPaths`, `sourceContext`, and `bindings`:
162
+
163
+ - `policy`: explicit provider/additive selections with their selected sources and
164
+ settings, using the existing policy grammar. It cannot erase soul requirements.
165
+ - `localBase`: explicit absolute base for relative local policy sources. Work
166
+ context/cwd is not a substitute.
167
+ - `allowLocalPaths`: explicit boolean authorization for those local policy sources.
168
+ Top-level local acquisition authorization remains a separate input.
169
+ - `sourceContext`: existing qualified repository anchor for `repo:` policy sources;
170
+ not a new repository inferred from workTarget.
171
+ - `bindings`: provider-owned map. Kernel preserves it and its document pointers;
172
+ it does not interpret store names, Git destinations, credentials or private teams.
173
+
174
+ Selecting an inherited store does not replace required provider runtime settings.
175
+ Use the selected provider's instructions for those settings; kernel must not guess
176
+ host-owned durable paths or copy native authentication.
177
+
95
178
  Repreparation after explicit approval is ordinary continuation in the selected,
96
179
  now-managed deployment; do not delete its state to make fresh preflight pass.
97
180
  Required hooks still run under their admitted custody with `--no-launch`; a parsed
@@ -44,14 +44,13 @@ create/spawn/retire. A team roster does not select a work repository for spawn.
44
44
 
45
45
  ## Onboarding with the setup expert
46
46
 
47
- For a kernel build that includes `oats onboard` (check `oats onboard --help`),
48
- start in an explicit empty deployment:
47
+ On **OATS 0.24.2 or later**, start in an explicit empty deployment:
49
48
 
50
49
  ```bash
51
50
  oats onboard --dir /absolute/new-deployment --json
52
51
  ```
53
52
 
54
- This command is **not present in the published 0.24.0/0.24.1 kernels**. It is a
53
+ `oats onboard` ships from 0.24.2 (earlier kernels refuse it). It is a
55
54
  classic local bootstrap, not captured preparation or workspace enrollment. It
56
55
  acquires `oats.framework` from the official catalog, exact-locks its artifacts,
57
56
  selects only `oats.core` and `oats.setup` for the new local `oats-setup-expert`,
@@ -0,0 +1,13 @@
1
+ # OATS v0.24.3 — second-operator `prepare` seams
2
+
3
+ Kernel/Pi/Desktop **0.24.3**. Fixes the five usability seams an independent second operator hit adopting the shared workspace definition on 0.24.1/0.24.2 (see the [program board](../design/2026-09-20-redesign-program-board.md)). No contract, schema or authority change.
4
+
5
+ - **Attributed provider problems.** Every preparation problem now carries `slot`, `capability` and its `origins`, with kernel-fixed text (for example `oats.okf knowledge normalize binding could not be prepared` / `oats.aweb@1.10.3 declares no binding interface; messaging cannot be prepared`). The kernel names the missing item where it knows it (a missing binding interface); for a provider's own `needs-configuration` it attributes the slot/capability/phase but cannot name the missing setting unless the provider sends it — OKF 2.1.1 sends code only, so `bindings-file`/`state-dir` are named by the setup guidance and by OKF 2.1.2. One unqualified slot no longer masks another slot's diagnostics: every slot that has a binding interface is normalized, and the single resolver reports each invalid choice under its slot. Provider free text still never crosses the wire.
6
+ - **`oats trust <capability> --dir <deployment>` on a lock v3 deployment** returns a typed problem with the exact working command (`oats trust <cap> --deployment <abs> --artifact-set <sha256-…>`) instead of `unsupported lockfileVersion 3`. No auto-approval.
7
+ - **Help** documents `--artifact-set` and how `prepare`'s `selections[].artifactSet` / `approvalRequired[]` pair with it.
8
+ - **`prepare` on an absent deployment path** returns a typed explicit-provisioning hold, not a raw `ENOENT` with a host path.
9
+ - **One request file for both commands.** `prepare --request` accepts the explicit `workTarget` that `inspect --request` requires (explicit beats cwd; it never implies captured placement or current config), and `oats inspect --request <f> --emit-prepare-request <out>` writes the converted preparation request through the existing builder.
10
+
11
+ The second operator's recorded requests and results are preserved verbatim as regression fixtures (`test/fixtures/second-operator/`). Their valid-vs-bogus `stores.oats` pair remains identical by design: both refused on missing OKF runtime settings (`bindings-file`, `state-dir`), which the messages now say.
12
+
13
+ Setup guidance for those settings lands in the next `oats.framework` payload.
@@ -0,0 +1,10 @@
1
+ # OATS v0.24.4 — provider reasons cross the wire; one request file for both commands
2
+
3
+ Kernel/Pi/Desktop **0.24.4**. Two findings from the independent second operator's repeat on 0.24.3 (see the [program board](../design/2026-09-20-redesign-program-board.md)), plus the first half of the binding-ownership rule. Kernel-first release: the provider releases that declare the new manifest fields (OKF 2.1.2, aweb 1.11.1) require this version.
4
+
5
+ - **The kernel no longer discards the provider's reason.** A provider's `error.message` (and a non-ready `check`'s `result.problems[].message`) now crosses the binding wire when it is byte-equal to a fixed reason the provider declares — manifest `binding.reasons`, or the kernel's reviewed compatibility list for aweb 1.11.0 and OKF 2.1.2 — and is surfaced as the problem's `message` beside `slot`, `capability` and `origins`. Anything else is dropped as before and the kernel template is used. No interpolation, no operator values, no paths, ever: the second operator sees `messaging workspace must declare private: per-human` or `setting bindings-file is required (absolute host path)` in one run instead of six. Decision: `agents/oats-expert/soul/knowledge/decisions/provider-problem-reasons-cross-the-wire.md`.
6
+ - **`binding.reasons` and `binding.keys` manifest fields** (optional). `reasons`: 1–64 unique printable-ASCII strings ≤200 chars, no braces; an invalid declaration refuses, it never falls back. `keys`: a provider's owned operator-binding keys, exact names or trailing-dot namespaces (`stores.`) — **shape-validated only in 0.24.4**; forwarding, overlap refusal and stray-key attribution land in 0.25. Providers must still ignore keys they do not own (decision `operator-bindings-ownership.md`). Every earlier kernel rejects manifests carrying these fields, so declaring providers floor on `>=0.24.4`.
7
+ - **`inspect --request` accepts `prepare`'s fields.** `operator`, `launch`, `helperLaunches`, `mode`, `allowLocalPaths` are accepted, not evaluated, and listed under `ignored`; `--emit-prepare-request` carries them through unchanged. The same complete request file is now valid for both commands — the 0.24.3 fix had converged only one way.
8
+ - **CLI renders the problem `key`** (for example `/bindings/messaging/responsibleHuman`) beside its message.
9
+
10
+ Not in this release: the operator-bindings deadlock between OKF 2.1.1 and aweb 1.11.0 (OKF validated messaging's `wider` as a store locator) is fixed provider-side in OKF 2.1.2, released next with `binding.reasons`/`binding.keys` declared and floor 0.24.4; then aweb 1.11.1 (manifest-only), the `oats.framework` 1.1.2 payload, and the workspace editions re-pinned.
@@ -44,51 +44,75 @@ owner registry belongs in the public workspace. Knowledge publication and accept
44
44
  (S7) remain separate; no ready knowledge export is advertised. Preserve the parked
45
45
  roster/curation and every old home, lock, source, pending job, history and worktree.
46
46
 
47
- ## Planned onboarding: OATS Soul Setup (D3)
48
-
49
- **The D3 onboarding flow remains pending.** It will create and instantiate
50
- `oats-setup-expert`, declaring both `oats.core` and `oats.setup` from the
51
- [official marketplace](official-marketplace.md). Those capabilities are now
52
- published in `oats.framework` 1.1.0 and listed; the separate setup-expert edition
53
- and onboarding entry point do not become available merely by listing the package.
54
- This flow was not shipped in the 0.24.0 or 0.24.1 kernel releases.
55
-
56
- - The setup expert will help the operator adopt repositories, select capabilities
57
- and carry out the normal prepare/approve/scaffold/start steps. It bypasses no
58
- executable approval, provider readiness, identity or permission boundary.
59
- - Every soul created by that flow will declare `requires.capabilities.oats.core`
60
- and its source explicitly. The operator can remove or replace that dependency
61
- by editing the authored definition, not a captured record; the kernel will not
62
- silently reinsert an absent one.
63
- - The CLI/Desktop entry point still requires separate implementation and review.
64
- Do not invent a workspace init/adopt command, create a setup soul from this
65
- sketch, or treat a listed package as installed. Existing instances and retained
66
- resources are not rewritten by the plan.
47
+ ## Onboard with OATS Soul Setup (0.24.2+)
48
+
49
+ With operator approval, [OATS 0.24.2](release-notes/v0.24.2.md) and later provide:
50
+
51
+ ```sh
52
+ oats onboard --dir /absolute/context --json
53
+ ```
54
+
55
+ This is **classic local bootstrap**, not captured preparation or workspace
56
+ enrollment; the command is absent from 0.24.0/0.24.1. Classic root resolution
57
+ selects an enclosing roster, otherwise the enclosing Git root/context. Supplying
58
+ `--dir` does **not** promise that a literal nested subdirectory becomes a new
59
+ physical deployment; choose an independent context when that is intended.
60
+
61
+ Onboarding acquires the catalog's official `oats.framework` package through the
62
+ ordinary acquisition/lock engine and creates a local `oats-setup-expert`, selecting
63
+ only `oats.core` and `oats.setup` for it. Both local capability requirements name
64
+ the **actually acquired immutable commit**, not orphan `repo:` paths in the new
65
+ deployment. Knowledge, messaging and tasks default to none, with no knowledge
66
+ owner or payload. Unexpected executable surfaces refuse rather than gaining trust
67
+ from catalog membership. Review the resolved deployment and returned
68
+ `result.next.command`: it uses the same kernel for the next spawn, and onboarding
69
+ **never executes it or launches a model**.
70
+
71
+ Optional `--workspace git:host/org/repository[@revision]` selects the workspace's
72
+ pinned setup-expert import, or that explicit repository's advertised setup edition
73
+ at its observed revision. Its source-package bytes must match the official
74
+ acquisition. Failed explicit inputs never fall back to the packaged default, and
75
+ workspace policy, teams and provider adoption values are not silently adopted.
76
+
77
+ An **existing roster** requires `--force-existing` (the guard is the agent list,
78
+ not merely any existing configuration). The flag cannot replace an existing or
79
+ incomplete setup soul or disable providers/additives for other souls; exclusions
80
+ apply only to the new setup expert. Failures report partial acquisition/creation,
81
+ not atomic captured preparation. Preserve that evidence before retrying.
82
+ **`oats setup` remains record capture setup**, unchanged. Native authentication
83
+ and permission boundaries remain.
84
+
85
+ The expert can then guide deliberate configuration and the normal retained
86
+ prepare/approve/scaffold/start stages. A created soul or printed spawn command is
87
+ not a running session, provider qualification or accepted learning. Desktop
88
+ onboarding and legacy roster/knowledge cutover remain separate.
67
89
 
68
90
  ## Stage two: published experts and pinned imports
69
91
 
70
92
  - `oats-workspace.yaml` explicitly admits `oats` and the six intended capability
71
93
  repositories: `oats-dev`, `oats-okf`, `oats-aweb`, `oats-authoring`, `oats-jira`
72
94
  and `oats-linear`. It activates no additional capability; tasks default to none.
73
- - `oats.yaml` exports all five `souls/<name>` editions above and the actual package
74
- roots `oats-package` (`oats.framework`) and `capabilities/oats-authoring`, not
95
+ - `oats.yaml` exports the five knowledge-owning editions above plus the separate
96
+ `souls/oats-setup-expert` bootstrap edition, and the actual package roots
97
+ `oats-package` (`oats.framework`) and `capabilities/oats-authoring`, not
75
98
  the npm root as a fictitious OATS distribution. Its workspace backlink names
76
99
  the same framework repository.
77
100
  - The editions are parallel to, not replacements for, the live `agents/` roster.
78
101
  Each contains canonical instructions, `CLAUDE.md -> AGENTS.md`, and its reviewed
79
102
  private procedures where applicable. No durable KB is copied into them; legacy
80
- roster cutover remains deferred until S7 knowledge publication.
81
- - Each edition preserves its knowledge owner, owned node and four cross-reads.
103
+ roster/knowledge cutover remains deferred until the fresh-reader proof against
104
+ the accepted public knowledge base.
105
+ - Each of the five expertise editions preserves its owner, node and four cross-reads.
82
106
  Store `oats` requires the explicit `stores.oats` binding; no publisher writer,
83
107
  production store or grants are supplied. An acceptance fixture is parent-owned
84
108
  and cannot be counted as production knowledge adoption.
85
- - Knowledge **oats.okf@2.1.1** and messaging **oats.aweb@1.11.0** are explicit hard
86
- requirements, not optional defaults. They are published starting revisions,
87
- **not proof that their combined bindings/runtime profile is ready**. The provider
109
+ - Current authored expert editions require knowledge **oats.okf@2.1.1** and
110
+ messaging **oats.aweb@1.11.0**, not optional defaults. These published revisions
111
+ are **not proof that their combined bindings/runtime profile is ready**. The provider
88
112
  owner supplies that evidence and any subsequently reviewed compatible revision.
89
113
  Do not replace either requirement with none or erase a read edge to launch.
90
114
 
91
- At these starting pins, the provider boundary is concrete:
115
+ At those authored revisions, the provider boundary is concrete:
92
116
 
93
117
  - Published OKF2.1.1 supports `inherit: stores.oats`, normalized to
94
118
  `/bindings/knowledge/stores/oats`. The explicit `destination: oats` preserves
@@ -112,19 +136,27 @@ select reviewed compatible provider revisions and update the source pin delibera
112
136
  before claiming an operational pilot; metadata-only repository indexes change none
113
137
  of these runtime facts.
114
138
 
115
- Stage one used an empty imports list until source publication. Stage two is now
116
- committed: all five imports pin **`caa341f34009e37006567419a983d5a743037a79`**, the
117
- published edition revision containing explicit core requirements and the package.
118
- The later workspace commit `375b9f42` added those imports. Live source inspection
119
- against published main resolved all five as `ready-for-preparation`; this is
120
- metadata readiness, not provider binding, approval, enrollment or a running pilot.
139
+ Stage one used an empty imports list until source publication. All six imports
140
+ now pin **`906b1558633766cf489451f9b68016216acaa63b`** (after
141
+ [OATS v0.24.3](release-notes/v0.24.3.md)), the reviewed revision at which the
142
+ workspace declares the per-human private team policy (`teams: {private: per-human}`)
143
+ and every messaging edition carries its `teams: []` declaration — without them the
144
+ aweb provider cannot normalize in workspace context, as the second operator found.
145
+ Workspace update `f3ee31e0`. The five knowledge-owning experts therefore select
146
+ OKF2.1.1 and aweb1.11.0 with explicit core; setup remains provider-independent,
147
+ requiring core/setup and defaulting all three fundamental layers to none. It is
148
+ not a sixth knowledge owner. This deliberate repin, not a catalog/kernel upgrade
149
+ alone, advances the selected source requirements. Successful source inspection,
150
+ including `ready-for-preparation`, is still metadata readiness—not binding,
151
+ approval, enrollment or a running pilot.
121
152
 
122
153
  ## Preserve source-before-import publication order
123
154
 
124
- 1. Publish complete, reviewed source editions before pinning them. The current
125
- source is `caa341f34009e37006567419a983d5a743037a79`; future revisions must likewise
126
- exist before their workspace import update. Never use an invented SHA, a mutable
127
- branch or an unreviewed local candidate as the accepted source.
155
+ 1. Publish complete, reviewed source editions before pinning them. All six imports
156
+ use `906b1558633766cf489451f9b68016216acaa63b`. Future revisions must
157
+ likewise exist before their import update.
158
+ Never use an invented SHA, a mutable branch or an unreviewed local candidate
159
+ as the accepted source.
128
160
  2. In each of the six repositories, review a root `oats.yaml` against its actual
129
161
  source head and actual `oats-package/oats-package.json`. The declaration is:
130
162
 
@@ -149,13 +181,13 @@ metadata readiness, not provider binding, approval, enrollment or a running pilo
149
181
  imports:
150
182
  - source: git:github.com/awebai/oats
151
183
  soul: souls/oats-expert
152
- revision: caa341f34009e37006567419a983d5a743037a79
184
+ revision: 906b1558633766cf489451f9b68016216acaa63b
153
185
  alias: oats-expert
154
186
  ```
155
187
 
156
- The [actual workspace](../oats-workspace.yaml) contains all five imports at that
157
- same revision; this excerpt is not a replacement for the full list. The layout
158
- test now checks stage-two imports and source-document declarations. Do not
188
+ The [actual workspace](../oats-workspace.yaml) contains all six imports at that
189
+ same revision; this excerpt is not the full list.
190
+ The layout test checks all six imports and source-document declarations. Do not
159
191
  change stable export paths or owners merely because the workspace advances.
160
192
  4. Qualify reciprocal admission at the now-published observations. A missing
161
193
  backlink, a fork's copied file or a stale workspace observation is not membership.
package/lib/core.mjs CHANGED
@@ -62,7 +62,7 @@ import { validateCapturedLaunchRequest } from "./captured-launch-request.mjs";
62
62
  import { validateCapabilityInputDeclarations } from "./capability-inputs.mjs";
63
63
  import { validateCapturedSessionBackend, validateCapturedSessionTarget, assertCapturedSessionPlacement } from "./captured-session-backend.mjs";
64
64
  import { createRepositoryTransaction } from "./repository-observation.mjs";
65
- import { inspectPortableOnboarding as inspectOnboarding, describePortableOnboarding } from "./portable-onboarding.mjs";
65
+ import { inspectPortableOnboarding as inspectOnboarding, describePortableOnboarding, buildFreshPreparationRequest, inspectWorkTarget } from "./portable-onboarding.mjs";
66
66
  import { readLock3 } from "./portable-lock.mjs";
67
67
  import { portableScope, portableStateDirectory } from "./portable-state.mjs";
68
68
  import { objectAt } from "./portable-shape.mjs";
@@ -1211,13 +1211,17 @@ export function approveAvailableCapability(deployment, artifactSet, capability,
1211
1211
  /** Public read-only source/workspace inspection through the existing facade.
1212
1212
  * Owns only transient repository scratch, never deployment state, provisioning,
1213
1213
  * provider code, approvals or a serialized mutation witness. */
1214
- export function inspectPortableOnboarding(input, { repositoryOptions = {} } = {}) {
1214
+ export function inspectPortableOnboarding(input, { repositoryOptions = {}, includePrepareRequest = false } = {}) {
1215
1215
  canonicalJson(input);
1216
+ if (typeof includePrepareRequest !== "boolean") throw oatsError("invalid-declaration", "request conversion requires an explicit boolean option");
1216
1217
  const directory = mkdtempSync(join(realpathSync(tmpdir()), "oats-source-inspection-")), owned = lstatSync(directory);
1217
1218
  let repositories, primary;
1218
1219
  try {
1219
1220
  repositories = createRepositoryTransaction({ ...repositoryOptions, directory, accessContextKey: repositoryOptions.accessContextKey ?? "native" });
1220
- return describePortableOnboarding(inspectOnboarding(input, { repositories }));
1221
+ const inspection = inspectOnboarding(input, { repositories }), result = describePortableOnboarding(inspection);
1222
+ // Opt-in data export, not a serialized custody witness or executable grant.
1223
+ // Default metadata continues to omit unclassified adoption/provider values.
1224
+ return includePrepareRequest ? { ...result, prepareRequest: buildFreshPreparationRequest(inspection).preparation } : result;
1221
1225
  } catch (error) { primary = error; throw error; }
1222
1226
  finally {
1223
1227
  try {
@@ -1239,21 +1243,28 @@ export function inspectPortableOnboarding(input, { repositoryOptions = {} } = {}
1239
1243
  * a fabricated complete record. No lifecycle side effects run here. */
1240
1244
  export function prepareCapturedComposition(input, { repositoryOptions = {} } = {}) {
1241
1245
  canonicalJson(input);
1242
- objectAt(input, ["deployment", "source", "origin", "workspace", "member", "operator", "mode", "allowLocalPaths", "standaloneContextKey", "launch", "helperLaunches"], ["deployment", "source"]);
1246
+ objectAt(input, ["deployment", "workTarget", "source", "origin", "workspace", "member", "operator", "mode", "allowLocalPaths", "standaloneContextKey", "launch", "helperLaunches"], ["deployment", "source"]);
1243
1247
  if (input.launch !== undefined) validateCapturedLaunchRequest(input.launch, validateLaunchConfig);
1244
1248
  if (input.helperLaunches !== undefined) {
1245
1249
  objectAt(input.helperLaunches, null, []);
1246
1250
  for (const request of Object.values(input.helperLaunches)) validateCapturedLaunchRequest(request, validateLaunchConfig);
1247
1251
  }
1248
1252
  if (input.mode !== undefined && !WORK_MODES.includes(input.mode)) throw oatsError("invalid-declaration", "invalid preparation work mode");
1249
- const deployment = portableScope(input.deployment);
1253
+ const { workTarget, ...preparationInput } = input;
1254
+ const target = Object.hasOwn(input, "workTarget") ? inspectWorkTarget(workTarget) : null;
1255
+ let deployment;
1256
+ try { deployment = portableScope(input.deployment); }
1257
+ catch (error) {
1258
+ if (error.code === "ENOENT") throw oatsError("needs-configuration", "deployment directory is absent; provision the explicitly selected directory and inspect again before preparation");
1259
+ throw error;
1260
+ }
1250
1261
  const previous = readLock3(deployment); // Old state refuses before scratch/fetch.
1251
1262
  const directory = mkdtempSync(join(portableStateDirectory(deployment, true), ".prepare-")), owned = lstatSync(directory);
1252
1263
  const origin = input.origin ?? { kind: "operator", document: { kind: "operator", id: "oats-prepare" }, pointer: "/source" };
1253
1264
  let repositories, primary, catalog;
1254
1265
  try {
1255
1266
  repositories = createRepositoryTransaction({ ...repositoryOptions, directory, accessContextKey: repositoryOptions.accessContextKey ?? "native" });
1256
- return prepareComposition({ ...input, deployment, origin, directory }, { repositories, previous, kernel: {
1267
+ const result = prepareComposition({ ...preparationInput, deployment, origin, directory }, { repositories, previous, kernel: {
1257
1268
  loadPackageManifestAt, capabilityCompatibility, assertPlatformInvariantLocks, materializeCapability,
1258
1269
  manifest: loadRetainedManifest,
1259
1270
  binding: runCapturedProviderBinding,
@@ -1266,6 +1277,8 @@ export function prepareCapturedComposition(input, { repositoryOptions = {} } = {
1266
1277
  settings: assertCapabilitySettingValues, skills: skillEntriesIn, hooks: manifestHookDeclarations,
1267
1278
  validateLaunchConfig, runtimeRequirements: applicableRequirements }),
1268
1279
  } });
1280
+ // Explicit work context never selects source/config, cwd, or H/work placement.
1281
+ return target ? { ...result, workTarget: target } : result;
1269
1282
  } catch (error) { primary = error; throw error; }
1270
1283
  finally {
1271
1284
  try {
@@ -13,6 +13,7 @@ import { validateOrigin } from "./resolution-shape.mjs";
13
13
  import { oatsError } from "./errors.mjs";
14
14
 
15
15
  export const PORTABLE_ONBOARDING_VERSION = 1;
16
+ const PREPARATION_ONLY_FIELDS = Object.freeze(["operator", "launch", "helperLaunches", "mode", "allowLocalPaths"]);
16
17
  const issued = new WeakMap();
17
18
  const preflightWitnesses = new WeakMap();
18
19
  const MANAGED_PATHS = Object.freeze([
@@ -108,7 +109,7 @@ export function preflightFreshDeployment({ deployment, maxEntries = 4096 }) {
108
109
  return result;
109
110
  }
110
111
 
111
- function workTarget(path) {
112
+ export function inspectWorkTarget(path) {
112
113
  const canonical = canonicalExistingDirectory(path, "work target"), marker = present(join(canonical, ".git"));
113
114
  return { path: canonical, state: "existing", git: marker ? { present: true, kind: marker.isDirectory() ? "directory" : marker.isFile() ? "file" : "other" } : { present: false, kind: null } };
114
115
  }
@@ -140,14 +141,18 @@ function catalogSelection(value, count) {
140
141
  * The caller owns the repository transaction lifetime. */
141
142
  export function inspectPortableOnboarding(input, { repositories } = {}) {
142
143
  canonicalJson(input);
143
- exact(input, ["deployment", "workTarget", "source", "origin", "workspace", "member", "catalogIndexes", "standaloneContextKey"],
144
+ exact(input, ["deployment", "workTarget", "source", "origin", "workspace", "member", "catalogIndexes", "standaloneContextKey", ...PREPARATION_ONLY_FIELDS],
144
145
  ["deployment", "workTarget", "source", "origin"], "portable onboarding inspection");
145
146
  if (!repositories || typeof repositories !== "object") throw oatsError("invalid-source", "portable onboarding needs an explicit repository transaction");
146
147
  validateOrigin(input.origin);
147
148
  const standalone = explicitContext(input);
148
149
  const deployment = preflightFreshDeployment({ deployment: input.deployment });
149
- const target = workTarget(input.workTarget), discovery = createWorkspaceDiscovery(repositories);
150
- const witness = { deployment: preflightWitnesses.get(deployment), work: directoryIdentity(target.path) };
150
+ const target = inspectWorkTarget(input.workTarget), discovery = createWorkspaceDiscovery(repositories);
151
+ const ignored = PREPARATION_ONLY_FIELDS.filter(field => Object.hasOwn(input, field));
152
+ // Retain authored data privately for explicit request export ONLY. Inspection
153
+ // neither validates provider/launch semantics nor exposes these values.
154
+ const witness = { deployment: preflightWitnesses.get(deployment), work: directoryIdentity(target.path),
155
+ preparationFields: Object.fromEntries(ignored.map(field => [field, structuredClone(input[field])])) };
151
156
  const workspace = Object.hasOwn(input, "workspace") ? discovery.readWorkspace(input.workspace) : null;
152
157
  const selected = typeof input.source === "string" ? sourceOrigin(workspace, input.source) : { reference: input.source, origin: input.origin };
153
158
  const imported = discovery.importSoul(selected.reference, { origin: selected.origin });
@@ -171,7 +176,7 @@ export function inspectPortableOnboarding(input, { repositories } = {}) {
171
176
  const declaredTeams = workspace?.parsed.declaration.teams ?? {};
172
177
  const status = deployment.status !== "ready" ? deployment.status
173
178
  : input.member && membership.status !== "eligible" ? "needs-configuration" : "ready-for-preparation";
174
- const result = freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, status, deployment, workTarget: target,
179
+ const result = freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, status, deployment, workTarget: target, ignored,
175
180
  source: { location: imported.reference.source, reference: imported.reference, identity: imported.identity,
176
181
  revision: imported.observation.source, alias: imported.reference.alias, exportPath: imported.reference.soul,
177
182
  definition: imported.definition, roots: imported.roots, exports: exports.exports, provenance: imported.provenance },
@@ -236,12 +241,14 @@ export function buildFreshPreparationRequest(inspection, options = {}) {
236
241
  const { operator, mode, allowLocalPaths = false } = options;
237
242
  if (typeof allowLocalPaths !== "boolean") throw oatsError("invalid-declaration", "allowLocalPaths must be boolean");
238
243
  if (mode !== undefined && (typeof mode !== "string" || !mode.trim())) throw oatsError("invalid-declaration", "preparation mode must be non-empty text");
239
- const input = { deployment: inspection.deployment.deployment.path,
244
+ const input = { deployment: inspection.deployment.deployment.path, workTarget: inspection.workTarget.path,
240
245
  source: inspection.source.reference,
241
246
  origin: inspection.source.revision.provenance[0], allowLocalPaths,
242
247
  ...(inspection.workspace ? { workspace: inspection.workspace.request } : { standaloneContextKey: inspection.context.key }),
243
248
  ...(inspection.repositoryMembership.request ? { member: inspection.repositoryMembership.request } : {}),
244
- ...(operator === undefined ? {} : { operator }), ...(mode === undefined ? {} : { mode }) };
249
+ ...issued.get(inspection).preparationFields,
250
+ ...(operator === undefined ? {} : { operator }), ...(mode === undefined ? {} : { mode }),
251
+ ...(Object.hasOwn(options, "allowLocalPaths") ? { allowLocalPaths } : {}) };
245
252
  canonicalJson(input);
246
253
  return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, operation: "prepare", persisted: false,
247
254
  preparation: input, workTarget: inspection.workTarget,
@@ -6,6 +6,7 @@ import { validateOrigin } from './resolution-shape.mjs';
6
6
  import { pointerKey } from './portable-shape.mjs';
7
7
  import { bindingField, bindingOriginWitnessed } from './provider-binding-wire.mjs';
8
8
  import { oatsError } from './errors.mjs';
9
+ import { providerReasons, safeProviderReason } from './provider-reasons.mjs';
9
10
 
10
11
  /** Remap dictionary keys for an adoption subtree, but retain original document
11
12
  * pointers/spans inside each witness. No file/config is read here. */
@@ -37,24 +38,34 @@ export function prepareProviderBindings({seed,plan,manifests,declarations},invok
37
38
  const normalized=new Map(),problems=[],requirements=[...plan.requirements],candidates=[...plan.candidates];
38
39
  const settingsFor=id=>Object.fromEntries(Object.entries(plan.settings[id] ?? {}).map(([name,key])=>[name,plan.choices[key].value]));
39
40
  const run=(id,phase,input)=>invoke({artifacts:seed.artifacts,capability:id,phase,settings:settingsFor(id),input});
40
- const refuse=error=>({code:safeCodes.has(error?.code)?error.code:'provider-not-qualified',message:'provider binding could not be prepared',origins:[]});
41
+ const originsFor=id=>[...new Map((plan.capabilities[id]?.choiceKeys ?? []).flatMap(key=>{
42
+ const choice=plan.choices[key];return choice?.selectedBy?[choice.selectedBy]:[];
43
+ }).map(origin=>[canonicalJson(origin),origin])).values()];
44
+ const problem=(slot,id,code,message,origins=originsFor(id))=>({code,message,origins,slot,capability:id});
45
+ const refuse=(slot,id,phase,error)=>problem(slot,id,safeCodes.has(error?.code)?error.code:'provider-not-qualified',
46
+ safeProviderReason(error?.message,providerReasons(manifests.get(id))) ?? `${id} ${slot} ${phase} binding could not be prepared`); // Recheck fixed text; never arbitrary exceptions.
41
47
  for (const [id,manifest] of manifests) {
42
48
  if (!Object.hasOwn(seed.artifacts.capabilities,id) || !manifest.layer) continue;
43
- if (plan.providers[manifest.layer] !== id) problems.push({code:'needs-configuration',message:'fundamental capability must be selected in its own layer',origins:[]});
49
+ if (plan.providers[manifest.layer] !== id) problems.push(problem(manifest.layer,id,'needs-configuration','fundamental capability must be selected in its own layer'));
44
50
  }
45
- if (problems.length) return {plan,seed,problems};
46
51
  for (const [slot,id] of Object.entries(plan.providers)) {
47
52
  if (id === null) continue;
48
53
  const manifest=manifests.get(id);
49
- if (manifest?.layer !== slot || !manifest.binding) { problems.push({code:'provider-not-qualified',message:'selected provider has no matching binding interface',origins:[]});continue; }
54
+ if (manifest?.layer !== slot) { problems.push(problem(slot,id,'provider-not-qualified',`${id} does not declare the selected ${slot} layer`));continue; }
55
+ if (!manifest.binding) { problems.push(problem(slot,id,'provider-not-qualified',`${id}@${manifest.version} declares no binding interface; ${slot} cannot be prepared`));continue; }
50
56
  try {
51
57
  const value=run(id,'normalize',{declarations,context:seed.context});
52
58
  normalized.set(slot,{id,value}); requirements.push(...value.requirements);candidates.push(...value.candidates);
53
- } catch(error) { problems.push(refuse(error)); }
59
+ } catch(error) { problems.push(refuse(slot,id,'normalize',error)); }
54
60
  }
55
- if (problems.length) return {plan,seed,problems};
61
+ // One unsupported slot must not mask another slot's normalized requirements
62
+ // or invalid choices. The same resolver still decides every value.
56
63
  const resolved=resolveChoices({requirements,candidates});
57
- if (resolved.status !== 'resolved') return {plan:{...plan,...resolved},seed,problems:resolved.problems};
64
+ for (const item of resolved.problems) {
65
+ const slot=Object.keys(plan.providers).find(slot=>bindingField(item.key,slot));
66
+ if (!slot) throw oatsError('invalid-binding-output','provider choice problem has no owned binding slot');
67
+ problems.push({...item,slot,capability:plan.providers[slot]});
68
+ }
58
69
  const nextPlan={...plan,...resolved,requirements,candidates},bindings=Object.create(null);
59
70
  let messagingChoice={schemaVersion:1,enabled:false};
60
71
  const known=[];
@@ -64,6 +75,7 @@ export function prepareProviderBindings({seed,plan,manifests,declarations},invok
64
75
  }
65
76
  const witnessed=origin=>bindingOriginWitnessed(origin,declarations) || known.some(candidate=>same(candidate,origin));
66
77
  for (const [slot,{id,value}] of normalized) {
78
+ if (resolved.problems.some(item=>bindingField(item.key,slot))) continue;
67
79
  try {
68
80
  const choices=Object.fromEntries(Object.entries(resolved.choices).filter(([key])=>bindingField(key,slot)));
69
81
  const bound=run(id,'bind',{model:value.model,choices,context:seed.context});
@@ -72,7 +84,9 @@ export function prepareProviderBindings({seed,plan,manifests,declarations},invok
72
84
  }
73
85
  bindings[slot]=bound.binding;
74
86
  if (slot === 'messaging') messagingChoice=bound.messagingChoice;
75
- } catch(error) { problems.push(refuse(error)); }
87
+ } catch(error) { problems.push(refuse(slot,id,'bind',error)); }
76
88
  }
89
+ nextPlan.problems=problems;
90
+ nextPlan.status=problems.some(item=>item.code==='requirement-conflict')?'conflict':problems.length?'needs-configuration':'resolved';
77
91
  return {plan:nextPlan,seed:{...seed,choices:resolved.choices,bindings,messagingChoice},problems};
78
92
  }
@@ -13,6 +13,7 @@ import { readApprovalLedger, evaluateCapturedApprovals } from './artifact-approv
13
13
  import { validateBindingInterface } from './provider-binding.mjs';
14
14
  import { BINDING_LIMITS, validateBindingRequest, decodeBindingResponse } from './provider-binding-wire.mjs';
15
15
  import { oatsError } from './errors.mjs';
16
+ import { providerReasons } from './provider-reasons.mjs';
16
17
 
17
18
  // Same kernel-owned CLI locator as lifecycle hooks; never caller env or PATH.
18
19
  const CLI_BIN=fileURLToPath(new URL('../bin/oats.mjs',import.meta.url));
@@ -58,7 +59,7 @@ export function invokeProviderBinding(options,codecs) {
58
59
  timeout:timeoutMs,killSignal:'SIGKILL',maxBuffer:BINDING_LIMITS.maxBytes,stdio:['pipe','pipe','pipe'],shell:false});
59
60
  } catch { throw oatsError('provider-unavailable','provider codec could not execute'); }
60
61
  if (result.error || result.signal || result.status !== 0) throw oatsError('provider-unavailable','provider codec did not complete successfully');
61
- const response=decodeBindingResponse(result.stdout,request);
62
- if (!response.ok) throw oatsError(response.error.code,'provider binding phase refused');
62
+ const response=decodeBindingResponse(result.stdout,request,{reasons:providerReasons(manifest)});
63
+ if (!response.ok) throw oatsError(response.error.code,response.error.message ?? 'provider binding phase refused');
63
64
  return response.result;
64
65
  }
@@ -8,6 +8,7 @@ import { isMaterializedCapabilityId } from './capability-provenance.mjs';
8
8
  import { BINDING_PHASES } from './provider-binding.mjs';
9
9
  import { resolveChoices } from './portable-choices.mjs';
10
10
  import { oatsError } from './errors.mjs';
11
+ import { providerReasons, safeProviderReason, validateBindingReasons } from './provider-reasons.mjs';
11
12
 
12
13
  export const BINDING_LIMITS = Object.freeze({maxBytes:1024*1024,maxDepth:32,maxEntries:16384});
13
14
  export const BINDING_PROBLEMS = Object.freeze(['needs-configuration','requirement-conflict','invalid-binding','authorization-required','host-requirement-missing','provider-unavailable','provider-not-qualified']);
@@ -16,11 +17,12 @@ const same = (a,b) => canonicalJson(a) === canonicalJson(b);
16
17
  const roleKinds = {soul:['soul-requirement','soul-default'],workspace:['workspace-default'],adoption:['import-adoption'],operator:['operator']};
17
18
  const pointer = key => typeof key === 'string' && /^(?:\/(?:[^~]|~[01])*)+$/.test(key);
18
19
  export const bindingField = (key,slot) => pointer(key) && key.startsWith(`/bindings/${slot}/`) && key.length > `/bindings/${slot}/`.length;
19
- function problem(value) {
20
+ function problem(value,reasons) {
20
21
  objectAt(value,['code','message'],['code']);
21
22
  if (!BINDING_PROBLEMS.includes(value.code)) fail();
22
23
  if (value.message !== undefined) stringAt(value.message,'/message',{empty:true});
23
- return {code:value.code}; // Free text never leaves the provider boundary.
24
+ const message=safeProviderReason(value.message,reasons);
25
+ return {code:value.code,...(message === undefined ? {} : {message})}; // Only declared fixed text crosses.
24
26
  }
25
27
  export function validateBindingRequest(request) {
26
28
  canonicalJson(request,BINDING_LIMITS);
@@ -67,15 +69,19 @@ function witnessed(origin,declarations) {
67
69
  return same(a,b);
68
70
  }));
69
71
  }
70
- export function decodeBindingResponse(bytes,request) {
72
+ export function decodeBindingResponse(bytes,request,{reasons}={}) {
71
73
  let response;
72
74
  try {
73
75
  validateBindingRequest(request);
76
+ // Out-of-band trusted caller input, NEVER a field supplied in the response.
77
+ // Unknown providers legitimately have no compatibility reasons; an explicit
78
+ // manifest declaration was already validated with the stricter nonempty bound.
79
+ reasons=validateBindingReasons(reasons === undefined ? providerReasons({capability:request.capability}) : reasons,{allowEmpty:true});
74
80
  response=parseStrictJson(bytes,BINDING_LIMITS);
75
81
  objectAt(response,response?.ok === true ? ['schemaVersion','phase','slot','capability','ok','result'] : ['schemaVersion','phase','slot','capability','ok','error'],
76
82
  response?.ok === true ? ['schemaVersion','phase','slot','capability','ok','result'] : ['schemaVersion','phase','slot','capability','ok','error']);
77
83
  for (const key of ['schemaVersion','phase','slot','capability']) if (response[key] !== request[key]) fail();
78
- if (response.ok === false) return {ok:false,error:problem(response.error)};
84
+ if (response.ok === false) return {ok:false,error:problem(response.error,reasons)};
79
85
  if (response.ok !== true) fail();
80
86
  const result=response.result;
81
87
  if (request.phase === 'normalize') {
@@ -102,7 +108,7 @@ export function decodeBindingResponse(bytes,request) {
102
108
  }
103
109
  objectAt(result,['status','problems'],['status','problems']);
104
110
  if (!['ready','needs-configuration','authorization-required','unavailable'].includes(result.status) || !Array.isArray(result.problems)) fail();
105
- const problems=result.problems.map(problem);
111
+ const problems=result.problems.map(value=>problem(value,reasons));
106
112
  if (result.status === 'ready' && problems.length) fail();
107
113
  return {ok:true,result:{status:result.status,problems}};
108
114
  } catch { fail(); }
@@ -1,16 +1,21 @@
1
1
  /** Provider-owned binding protocol. This codec declares no provider model and
2
2
  * grants no execution authority; commands remain in the sole manifest table. */
3
- import { objectAt, stringAt, versionAt } from './portable-shape.mjs';
3
+ import { objectAt, stringAt, stringSetAt, versionAt } from './portable-shape.mjs';
4
4
  import { FUNDAMENTAL_SLOTS } from './portable-policy.mjs';
5
5
  import { oatsError } from './errors.mjs';
6
+ import { validateBindingReasons } from './provider-reasons.mjs';
6
7
 
7
8
  export const BINDING_PHASES = Object.freeze(['normalize', 'bind', 'check']);
8
9
  export function validateBindingInterface(manifest) {
9
10
  if (manifest.binding === undefined) return null;
10
11
  if (!FUNDAMENTAL_SLOTS.includes(manifest.layer)) throw oatsError('invalid-binding-interface', 'binding codecs belong to a fundamental provider slot');
11
12
  const binding = manifest.binding;
12
- objectAt(binding, ['version', ...BINDING_PHASES], ['version', ...BINDING_PHASES], '/binding');
13
+ objectAt(binding, ['version', ...BINDING_PHASES, 'reasons', 'keys'], ['version', ...BINDING_PHASES], '/binding');
13
14
  versionAt(binding.version);
15
+ if (Object.hasOwn(binding, 'reasons')) validateBindingReasons(binding.reasons);
16
+ // 0.24.4 validates declarations only; ownership/routing enforcement is 0.25.
17
+ if (Object.hasOwn(binding, 'keys')) stringSetAt(binding.keys, '/binding/keys', (key, pointer) =>
18
+ stringAt(key, pointer, { pattern: /^[A-Za-z][A-Za-z0-9_-]*\.?$(?![\s\S])/ }));
14
19
  for (const phase of BINDING_PHASES) {
15
20
  const name = binding[phase];
16
21
  stringAt(name, `/binding/${phase}`, { pattern: /^[a-z0-9][a-z0-9-]*$/ });
@@ -0,0 +1,77 @@
1
+ /** Fixed provider-declared diagnostics, never a free-text transport. Values
2
+ * come from the verified selected manifest or reviewed kernel compatibility
3
+ * data, not from a codec response, operator input or today's configuration. */
4
+ import { invalidShape, stringAt, stringSetAt } from './portable-shape.mjs';
5
+
6
+ // Compatibility literals for manifests predating binding.reasons. These are
7
+ // data, never provider-name-dependent interpretation of payloads or settings.
8
+ // aweb: awebai/oats-aweb v1.11.0, 862f156c883ab39c69c9e83cdf3bab86be882867.
9
+ // oats-package/capabilities/oats-aweb/lib/{binding-wire,session-readiness}.mjs:
10
+ // complete safeReasons/fallback/overflow/check vocabulary (no hook warnings).
11
+ // OKF: awebai/oats-okf PR5, 0517a70a7158ebdf14ccb6e880b437c3d43cb1db,
12
+ // same capability-relative file, settingMessages (UNRELEASED 2.1.2 at selection).
13
+ // Released OKF 2.1.1 sends code-only; this table invents no reason for it.
14
+ const bundled = Object.freeze({
15
+ 'oats.aweb': Object.freeze([
16
+ 'messaging-enabled standalone preparation needs an explicit context key',
17
+ 'messaging binding needs one soul declaration',
18
+ 'messaging workspace must declare private: per-human',
19
+ 'an explicit responsible-human binding is required',
20
+ 'an explicit wider-membership consent list is required',
21
+ 'a selected wider-team binding is required',
22
+ 'a selected wider alias needs an explicit workspace team mapping',
23
+ 'multiple soul messaging declarations',
24
+ 'adoption team aliases have conflicting mappings',
25
+ 'messaging settings and explicit binding selections are required',
26
+ 'messaging declarations contain incompatible requirements',
27
+ 'messaging input must match the supported binding contract',
28
+ 'explicit native messaging authorization is required',
29
+ 'a required native messaging host resource is unavailable',
30
+ 'the selected messaging provider is unavailable',
31
+ 'the requested messaging configuration is not qualified',
32
+ 'messaging response exceeds the supported wire limits',
33
+ 'an admitted captured instance intent is required for execution',
34
+ 'an explicit private-team binding is required',
35
+ 'selected binding and inline captured invocation are required',
36
+ 'an explicit captured instance home is required',
37
+ 'captured messaging requires explicit delivery: session',
38
+ 'selected wider memberships need their explicitly qualified native setup; they were not omitted',
39
+ 'caller-owned OATS_CLI_BIN and readable kernel version are required',
40
+ 'oats >=0.24.2 is required for captured HOME custody and retained runtime inspection',
41
+ 'the exact retained runtime profile must be readable',
42
+ 'the kernel must report the exact retained resolution',
43
+ 'a retained launchSelection runtime/model observation is required',
44
+ 'Pi strict print does not support session input; retain messaging and configure an input-capable profile',
45
+ 'a supported input-capable ordinary Claude/Codex profile is required',
46
+ ]),
47
+ 'oats.okf': Object.freeze([
48
+ 'setting bindings-file is required (absolute host path)',
49
+ 'setting bindings-file must be a normalized absolute host path',
50
+ 'setting state-dir is required (absolute host path)',
51
+ 'setting state-dir must be a normalized absolute host path',
52
+ 'setting harvest-runtime is required (pi, claude or codex)',
53
+ 'setting harvest-runtime must be pi, claude or codex',
54
+ 'setting harvest-model must be null or a non-empty string',
55
+ ]),
56
+ });
57
+ const none = Object.freeze([]);
58
+
59
+ export function validateBindingReasons(reasons, { allowEmpty = false } = {}) {
60
+ stringSetAt(reasons, '/binding/reasons', (reason, pointer) => {
61
+ stringAt(reason, pointer);
62
+ if (reason.length > 200 || /[{}]|[^\x20-\x7e]/.test(reason)) invalidShape(pointer, 'reason must be printable ASCII of at most 200 characters without braces');
63
+ });
64
+ if (reasons.length > 64 || (!allowEmpty && reasons.length === 0)) invalidShape('/binding/reasons', 'expected 1 to 64 fixed reasons');
65
+ return reasons;
66
+ }
67
+
68
+ export function providerReasons(manifest) {
69
+ if (Object.hasOwn(manifest.binding ?? {}, 'reasons')) return validateBindingReasons(manifest.binding.reasons);
70
+ return Object.hasOwn(bundled, manifest.capability) ? bundled[manifest.capability] : none;
71
+ }
72
+
73
+ /** Exact Unicode scalar text is exact UTF-8 text; never trim, normalize,
74
+ * interpolate or accept a prefix/substring. Absence preserves template fallback. */
75
+ export function safeProviderReason(message, reasons) {
76
+ return typeof message === 'string' && reasons.includes(message) ? message : undefined;
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.24.2",
3
+ "version": "0.24.4",
4
4
  "description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
5
5
  "keywords": [
6
6
  "agents",