@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 +70 -8
- package/docs/capability-manifest.schema.json +15 -1
- package/docs/design/2026-09-16-provider-binding-wire.md +28 -1
- package/docs/design/2026-09-20-redesign-program-board.md +23 -4
- package/docs/design/2026-09-20-workspace-onboarding-public.md +90 -7
- package/docs/first-team.md +2 -3
- package/docs/release-notes/v0.24.3.md +13 -0
- package/docs/release-notes/v0.24.4.md +10 -0
- package/docs/workspace-adoption.md +74 -42
- package/lib/core.mjs +19 -6
- package/lib/portable-onboarding.mjs +14 -7
- package/lib/prepared-bindings.mjs +22 -8
- package/lib/provider-binding-broker.mjs +3 -2
- package/lib/provider-binding-wire.mjs +11 -5
- package/lib/provider-binding.mjs +7 -2
- package/lib/provider-reasons.mjs +77 -0
- package/package.json +1 -1
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
|
-
|
|
106
|
+
const values = new Map();
|
|
103
107
|
for (let index = 1; index < args.length; index++) {
|
|
104
108
|
if (args[index] === "--json") continue;
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
110
|
-
|
|
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)
|
|
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
|
-
|
|
5365
|
-
|
|
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
|
|
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
|
|
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 | ✅
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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`
|
|
81
|
-
OR standalone context, operator policy/bindings,
|
|
82
|
-
launch and helperLaunches.
|
|
83
|
-
|
|
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
|
package/docs/first-team.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
74
|
-
|
|
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
|
|
81
|
-
|
|
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
|
-
-
|
|
86
|
-
|
|
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
|
|
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.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
125
|
-
|
|
126
|
-
exist before their
|
|
127
|
-
branch or an unreviewed local candidate
|
|
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:
|
|
184
|
+
revision: 906b1558633766cf489451f9b68016216acaa63b
|
|
153
185
|
alias: oats-expert
|
|
154
186
|
```
|
|
155
187
|
|
|
156
|
-
The [actual workspace](../oats-workspace.yaml) contains all
|
|
157
|
-
same revision; this excerpt is not
|
|
158
|
-
test
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
150
|
-
const
|
|
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
|
-
...(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(); }
|
package/lib/provider-binding.mjs
CHANGED
|
@@ -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.
|
|
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",
|