@awebai/oats 0.24.3 → 0.24.5
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 +25 -6
- package/capabilities/oats-aweb/oats.json +44 -3
- package/capabilities/oats-okf/lib/binding-wire.mjs +57 -7
- package/capabilities/oats-okf/oats.json +12 -3
- 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 +60 -5
- package/docs/design/2026-09-20-workspace-onboarding-public.md +18 -1
- package/docs/first-team.md +10 -4
- package/docs/layers.md +1 -1
- package/docs/official-marketplace.md +1 -1
- package/docs/packages.md +2 -2
- package/docs/release-notes/oats-framework-v1.1.3.md +11 -0
- package/docs/release-notes/v0.24.3.md +1 -1
- package/docs/release-notes/v0.24.4.md +10 -0
- package/docs/release-notes/v0.24.5.md +12 -0
- package/docs/workspace-adoption.md +77 -45
- package/docs/workspaces.md +1 -1
- package/lib/captured-launch-request.mjs +22 -2
- package/lib/core.mjs +25 -3
- package/lib/helper-injection-policy.mjs +7 -1
- package/lib/portable-onboarding.mjs +11 -4
- package/lib/prepare-composition.mjs +21 -2
- package/lib/prepared-bindings.mjs +2 -1
- 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/lib/setup-expert-source.mjs +25 -1
- package/package-catalog.json +3 -3
- package/package.json +1 -1
package/docs/packages.md
CHANGED
|
@@ -402,7 +402,7 @@ sources; installing a kernel does not advance existing package locks:
|
|
|
402
402
|
{
|
|
403
403
|
"packages": {
|
|
404
404
|
"oats.okf": { "url": "https://github.com/awebai/oats-okf.git", "ref": "v2.0.0", "path": "oats-package" },
|
|
405
|
-
"oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.1.
|
|
405
|
+
"oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.1.3", "path": "oats-package" },
|
|
406
406
|
"oats.dev": { "url": "https://github.com/awebai/oats-dev.git", "ref": "v1.0.0", "path": "oats-package" }
|
|
407
407
|
},
|
|
408
408
|
"capabilities": { "oats.review": "oats.dev" }
|
|
@@ -429,7 +429,7 @@ integrity checks. Git transport preserves the canonical source alias.
|
|
|
429
429
|
|
|
430
430
|
The `oats.framework` distribution package is a separate Git payload in this
|
|
431
431
|
repository's `oats-package/`, excluded from the kernel npm tarball. The catalog
|
|
432
|
-
entry selects the published `oats-framework/v1.1.
|
|
432
|
+
entry selects the published `oats-framework/v1.1.3` tag, which exports three
|
|
433
433
|
capabilities: `oats.core` (day-to-day operation: `oats-operate`, `oats-souls`
|
|
434
434
|
and the "you run on OATS" briefing — declared explicitly on every soul by
|
|
435
435
|
default at creation and removable), `oats.setup` (OATS Soul Setup: `oats-config`,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# oats.framework 1.1.3 · aweb 1.11.2 — helper composition for every edition
|
|
2
|
+
|
|
3
|
+
Packaging fix found by the independent second operator on the 0.24.4 wave: behind the operator's `responsibleHuman` requirement, preparation refused with `needs-configuration: new helper injection requires an explicit capability policy`. `oats.okf` had adopted the helper-injection contract (`omit`); its siblings **`oats.core`** and **`oats.aweb`** shipped an `inject` without a `helperInjection` policy, so no edition's OKF harvest helper could compose and **no edition could publish a resolution**. Both remaining blockers were packaging, not operator input.
|
|
4
|
+
|
|
5
|
+
- **`oats.core` 1.0.1** (in oats.framework 1.1.3): `helperInjection: {version: 1, mode: inherit}` — a helper is still an OATS instance and keeps the "you run on OATS" briefing.
|
|
6
|
+
- **aweb 1.11.2**: `helperInjection: {version: 1, mode: omit}` — a harvest helper has no messaging identity. Manifest-only; code identical to 1.11.0.
|
|
7
|
+
- **Release check**: `test/release-packaging.test.mjs` now asserts every framework-shipped capability with an `inject` declares a `helperInjection` policy; the theory-package check pins core's `inherit`. The framework's own packages must pass the contracts the kernel imposes.
|
|
8
|
+
- Catalog: `oats.aweb` → `v1.11.2`, `oats.framework` → `oats-framework/v1.1.3`; six editions and workspace imports repinned.
|
|
9
|
+
- Still open (kernel, 0.25 unless a 0.24.5 is cut): this early refusal is a bare top-level error with no `details`/attribution — it must route through the same problem shape as every other preparation problem. Also noted: `responsibleHuman` is a captured accountability claim the code validates only by shape.
|
|
10
|
+
|
|
11
|
+
Decision: `agents/oats-expert/soul/knowledge/decisions/helper-injection-policy-on-every-injecting-capability.md`.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
- **Attributed provider problems.** Every preparation problem now carries `slot`, `capability` and its `origins`, with kernel-fixed text
|
|
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
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
7
|
- **Help** documents `--artifact-set` and how `prepare`'s `selections[].artifactSet` / `approvalRequired[]` pair with it.
|
|
8
8
|
- **`prepare` on an absent deployment path** returns a typed explicit-provisioning hold, not a raw `ENOENT` with a host path.
|
|
@@ -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.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# OATS v0.24.5 — four small fixes from the second-operator wave
|
|
2
|
+
|
|
3
|
+
Kernel/Pi/Desktop **0.24.5**. No contract, schema or authority change. Every item was found while an independent operator (and then the lead) drove the published 0.24.4 wave to its first end-to-end resolution.
|
|
4
|
+
|
|
5
|
+
- **Retirement recovers a worktree on the branch it actually has.** `oats retire` derived the recovery clone's branch from `instance.json` (the spawn-time branch); a worktree that had legitimately switched branches during its task could never pass recovery verification and became unretirable by the normal path, even with nothing unpreserved. Recovery now derives the branch from the worktree while it exists (detached HEADs recover at their exact commit), falls back to the recorded branch only when the worktree is gone, and records the drift in `recovery.json` (`branchDrift`). Fail-closed behaviour is unchanged; the source of truth moved to the object.
|
|
6
|
+
- **Captured launch: `ifInstalled` runtime rows are not hard blocks, and refusals are attributed.** A provider's `requires` row marked `ifInstalled: true` (a version floor for an ambient package, if present) was treated as a hard requirement, so a Pi launch with aweb `delivery: session` could never publish a resolution — the same request without a `launch` block published fine. Such rows are now satisfied by absence in captured preparation. Remaining hard rows refuse through the ordinary preparation problem shape (`slot`, `capability`, `runtime`, `package`, the manifest's own `install` text) instead of one bare `needs-configuration`.
|
|
7
|
+
- **Helper-injection refusal is attributed.** A capability that ships an `inject` without a `helperInjection` policy now yields a preparation problem naming that capability (with `origins`), so the operator learns *which* sibling has not adopted the contract rather than only that one has.
|
|
8
|
+
- **`oats onboard --workspace` acquires the framework the workspace's catalog names.** The kernel tarball ships `package-catalog.json` as a snapshot at the kernel's tag, so it lags every `oats.framework` release cut afterwards; onboarding a current workspace edition against it refused as `integrity-drift` — correct, but a fresh machine has no reviewed list to point `OATS_PACKAGE_CATALOG` at. Onboarding now reads the workspace repository's `package-catalog.json` at the observed revision and uses its `oats.framework` entry; the bundled entry is the fallback, `OATS_PACKAGE_CATALOG` still overrides. The result reports `catalog.origin`, and a drift refusal names the lag and both integrities.
|
|
9
|
+
|
|
10
|
+
Rule going forward (recorded in the maintainer's knowledge): **`prepare` never ends in a bare `needs-configuration` after selection** — every refusal is a problem with a slot/capability, or it is a kernel defect. Remaining bare sites are pre-selection input errors, record-shape guards and the skill-name collision, none reachable from a published edition.
|
|
11
|
+
|
|
12
|
+
Not in this release: `binding.keys` enforcement and the dotted-exact-key grammar (0.25); OKF `check` per-cause reasons (OKF 2.1.3, deferred with harvest).
|
|
@@ -44,64 +44,88 @@ 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.2** and
|
|
110
|
+
messaging **oats.aweb@1.11.2** (both OATS >=0.24.4), 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
|
-
- Published
|
|
117
|
+
- Published OKF 2.1.2 supports `inherit: stores.oats`, normalized to
|
|
94
118
|
`/bindings/knowledge/stores/oats`. The explicit `destination: oats` preserves
|
|
95
119
|
routing; omitting it would instead require `write.default`. No new schema,
|
|
96
120
|
owner or production locator is needed for this declaration.
|
|
97
121
|
- aweb 1.10.3 (`24efa6f9`) has no portable binding interface; **aweb 1.11.0**
|
|
98
|
-
(`v1.11.0`, OATS >=0.24.2) adds it and the five editions
|
|
122
|
+
(`v1.11.0`, OATS >=0.24.2) adds it; **1.11.1/1.11.2** (OATS >=0.24.4) are code-identical and declare its fixed reasons, owned operator keys and `helperInjection: omit` (a harvest helper has no messaging identity); the five editions pin 1.11.2. Its `check`
|
|
99
123
|
qualifies only an input-capable Claude/Codex primary with an explicit private team
|
|
100
124
|
and `delivery: session`; strict-Pi print reports `needs-configuration`. Status is on
|
|
101
125
|
the [program board](design/2026-09-20-redesign-program-board.md). Qualification
|
|
102
126
|
is HOME-route operational custody only: not human/native-principal delegation,
|
|
103
127
|
private grants, broker delivery or model consumption.
|
|
104
|
-
- Published
|
|
128
|
+
- Published OKF 2.1.2 accepts retained Claude/Codex helpers with the complete approved
|
|
105
129
|
capability closure and native-default model intent. Strict Pi still requires an
|
|
106
130
|
explicit model and the sole-OKF profile; Pi plus messaging remains unqualified.
|
|
107
131
|
This provider release alone is not combined-profile acceptance. Do not silently
|
|
@@ -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
|
+
OKF 2.1.2 and aweb 1.11.2 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/docs/workspaces.md
CHANGED
|
@@ -71,7 +71,7 @@ name: domain-expert
|
|
|
71
71
|
requires:
|
|
72
72
|
knowledge:
|
|
73
73
|
capability: oats.okf
|
|
74
|
-
source: git:github.com/awebai/oats-okf@v2.1.
|
|
74
|
+
source: git:github.com/awebai/oats-okf@v2.1.2#oats-package
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
This illustrates software selection, not complete OKF provisioning: the chosen capability also needs its own valid knowledge declaration, bindings and accepted base.
|
|
@@ -32,8 +32,28 @@ export function validateCapturedLaunchRequest(value, validateConfig) {
|
|
|
32
32
|
export function compileCapturedLaunchRequest(request,{artifacts,manifests,settings,resources},kernel) {
|
|
33
33
|
if(request===undefined)return null;
|
|
34
34
|
validateCapturedLaunchRequest(request,kernel.validateLaunchConfig);
|
|
35
|
-
const
|
|
36
|
-
|
|
35
|
+
const providers=[...manifests].filter(([id])=>Object.hasOwn(artifacts.capabilities,id)).map(([id,manifest])=>({id,manifest,settings:settings[id]}));
|
|
36
|
+
// `ifInstalled: true` rows constrain a package that may legitimately be absent (a version floor
|
|
37
|
+
// for an ambient extension, if any). Captured preparation retains no ambient packages, so such a
|
|
38
|
+
// row is satisfied by absence — treating it as a hard requirement made a Pi launch with aweb
|
|
39
|
+
// `delivery: session` unpublishable (second-operator finding, 2026-09-21). Hard rows refuse
|
|
40
|
+
// through the preparation problem shape, naming the capability and package; the remedy text is
|
|
41
|
+
// the manifest's own `install` string — declared data, never free text.
|
|
42
|
+
// Resolve each applicable row back to its declaration, honouring the same `when` predicate the
|
|
43
|
+
// kernel's requirement selection uses, so two rows for one package (channel vs session) stay distinct.
|
|
44
|
+
const holds=(provider,row)=>!row.when || Object.entries(row.when).every(([k,v])=>String(provider.settings?.[k] ?? '')===String(v));
|
|
45
|
+
const declarationOf=(row)=>{const provider=providers.find(p=>p.id===row.capability);
|
|
46
|
+
return {provider,declared:provider?.manifest?.requires?.find(r=>r && typeof r==='object' && r.runtime===request.runtime && r.package===row.package && holds(provider,r))};};
|
|
47
|
+
const required=kernel.runtimeRequirements(request.runtime,providers).filter(row=>declarationOf(row).declared?.ifInstalled!==true);
|
|
48
|
+
if(required.length){
|
|
49
|
+
const error=oatsError('needs-configuration','runtime package requirements need retained runtime roots and a qualified loader; no ambient package discovery was used');
|
|
50
|
+
error.problems=required.map(row=>{
|
|
51
|
+
const {provider,declared}=declarationOf(row);
|
|
52
|
+
return {code:'needs-configuration',message:`${request.runtime} launch requires runtime package ${row.package}, which captured preparation has not retained`,capability:row.capability,
|
|
53
|
+
...(provider?.manifest?.layer?{slot:provider.manifest.layer}:{}),runtime:request.runtime,package:row.package,...(typeof declared?.install==='string'?{install:declared.install}:{})};
|
|
54
|
+
});
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
37
57
|
const base={version:1,runtime:request.runtime,args:request.args,env:request.env,model:request.model,yolo:request.yolo,
|
|
38
58
|
hooks:{launch:{},env:{},contributions:[],pending:true},prompt:{kind:'task-file',file:'TASK.md'}};
|
|
39
59
|
if(typeof request.executable==='string')return {...base,executable:request.executable,executableResolvedFrom:'explicit-host'};
|
package/lib/core.mjs
CHANGED
|
@@ -7080,6 +7080,15 @@ function fingerprintTree(root, { excludeRoot = new Set(), excludeGitMetadata = f
|
|
|
7080
7080
|
return `sha256:${hash.digest("hex")}`;
|
|
7081
7081
|
}
|
|
7082
7082
|
|
|
7083
|
+
/** The ref a worktree actually has checked out: `{branch, oid}` with
|
|
7084
|
+
* `branch === null` when HEAD is detached. Recovery derives truth from the
|
|
7085
|
+
* object, never from spawn-time metadata. */
|
|
7086
|
+
function worktreeRef(work) {
|
|
7087
|
+
const oid = execFileSync("git", ["-C", work, "rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER }).trim();
|
|
7088
|
+
let branch = null;
|
|
7089
|
+
try { branch = execFileSync("git", ["-C", work, "symbolic-ref", "--quiet", "--short", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER }).trim() || null; } catch { branch = null; }
|
|
7090
|
+
return { branch, oid };
|
|
7091
|
+
}
|
|
7083
7092
|
function worktreeStatus(repo) {
|
|
7084
7093
|
try {
|
|
7085
7094
|
return execFileSync("git", ["-C", repo, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignored=matching", "--ignore-submodules=none"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER });
|
|
@@ -8304,9 +8313,21 @@ function preserveRetirementWork(observation, meta, instance) {
|
|
|
8304
8313
|
if (fingerprintTree(observation.home, { excludeRoot: new Set(["work"]) }) !== fingerprintTree(recoveredHome)) {
|
|
8305
8314
|
throw new Error("home recovery verification disagreed with the source");
|
|
8306
8315
|
}
|
|
8316
|
+
let branchDrift;
|
|
8307
8317
|
if (!homeOnly && meta.work === "worktree" && meta.repo && meta.branch && (existsSync(observation.work) || observation.branchExists)) {
|
|
8308
8318
|
const recoveredRepo = join(staging, "repo");
|
|
8309
|
-
|
|
8319
|
+
// The branch is derived from the worktree while it exists: an instance
|
|
8320
|
+
// that legitimately switched branches during its task must still be
|
|
8321
|
+
// recoverable, and the recorded spawn-time branch is only the fallback
|
|
8322
|
+
// when the worktree is gone. Detached HEADs recover at their exact OID.
|
|
8323
|
+
const ref = existsSync(observation.work) ? worktreeRef(observation.work) : { branch: meta.branch, oid: null };
|
|
8324
|
+
if (ref.branch !== meta.branch) branchDrift = { recordedBranch: meta.branch, worktreeBranch: ref.branch, detachedAt: ref.branch === null ? ref.oid : null };
|
|
8325
|
+
if (ref.branch !== null) execFileSync("git", ["clone", "--no-local", "--quiet", "--branch", ref.branch, meta.repo, recoveredRepo], { stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER });
|
|
8326
|
+
else {
|
|
8327
|
+
execFileSync("git", ["clone", "--no-local", "--quiet", "--no-checkout", meta.repo, recoveredRepo], { stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER });
|
|
8328
|
+
execFileSync("git", ["-C", recoveredRepo, "fetch", "--quiet", observation.work, ref.oid], { stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER });
|
|
8329
|
+
execFileSync("git", ["-C", recoveredRepo, "checkout", "--quiet", "--detach", ref.oid], { stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER });
|
|
8330
|
+
}
|
|
8310
8331
|
const sourceGitContext = existsSync(observation.work) ? observation.work : meta.repo;
|
|
8311
8332
|
detachRecoveryClone(sourceGitContext, recoveredRepo);
|
|
8312
8333
|
if (existsSync(observation.work)) {
|
|
@@ -8325,7 +8346,8 @@ function preserveRetirementWork(observation, meta, instance) {
|
|
|
8325
8346
|
if (worktreeStatus(observation.work) !== worktreeStatus(recoveredRepo)) throw new Error("recovered Git index/status disagreed with the source");
|
|
8326
8347
|
}
|
|
8327
8348
|
const recoveredHead = execFileSync("git", ["-C", recoveredRepo, "rev-parse", "HEAD"], { encoding: "utf8" , maxBuffer: GIT_MAX_BUFFER }).trim();
|
|
8328
|
-
const sourceHead =
|
|
8349
|
+
const sourceHead = ref.branch === null ? ref.oid
|
|
8350
|
+
: execFileSync("git", ["-C", meta.repo, "rev-parse", `refs/heads/${ref.branch}`], { encoding: "utf8" , maxBuffer: GIT_MAX_BUFFER }).trim();
|
|
8329
8351
|
if (recoveredHead !== sourceHead) throw new Error("recovery clone does not retain the instance branch tip");
|
|
8330
8352
|
}
|
|
8331
8353
|
if (observation.directory && observation.directoryFingerprint) {
|
|
@@ -8336,7 +8358,7 @@ function preserveRetirementWork(observation, meta, instance) {
|
|
|
8336
8358
|
}
|
|
8337
8359
|
}
|
|
8338
8360
|
const repoCopy = homeOnly ? { copied: false, reason: "Only instance-home bytes changed; no work state requires a repository copy", source: meta.repo, branch: meta.branch } : undefined;
|
|
8339
|
-
writeFileSync(join(staging, "recovery.json"), JSON.stringify({ version: 1, instance, classes: observation.classes, sourceHome: observation.home, createdAt: new Date().toISOString(), ...(repoCopy ? { repoCopy } : {}) }, null, 2) + "\n", { mode: 0o600 });
|
|
8361
|
+
writeFileSync(join(staging, "recovery.json"), JSON.stringify({ version: 1, instance, classes: observation.classes, sourceHome: observation.home, createdAt: new Date().toISOString(), ...(repoCopy ? { repoCopy } : {}), ...(branchDrift ? { branchDrift } : {}) }, null, 2) + "\n", { mode: 0o600 });
|
|
8340
8362
|
mkdirSync(dirname(recovery), { recursive: true });
|
|
8341
8363
|
renameSync(staging, recovery);
|
|
8342
8364
|
return { path: recovery, classes: observation.classes, ...(repoCopy ? { repoCopy } : {}) };
|
|
@@ -35,7 +35,13 @@ export function captureHelperInjectionChoices(plan, definitions) {
|
|
|
35
35
|
const policy = helperInjectionFact(definition), id = definition.artifact.capability;
|
|
36
36
|
if (policies.has(id)) throw oatsError('invalid-resolution', 'duplicate helper policy owner');
|
|
37
37
|
policies.set(id, policy);
|
|
38
|
-
if (!policy.fact && policy.manifest.inject)
|
|
38
|
+
if (!policy.fact && policy.manifest.inject) {
|
|
39
|
+
// Attributed like every other preparation problem: the operator learns WHICH
|
|
40
|
+
// capability lacks the declaration, not only that one does (second-operator finding).
|
|
41
|
+
const error = oatsError('needs-configuration', 'new helper injection requires an explicit capability policy');
|
|
42
|
+
error.problems = [{ code: 'needs-configuration', message: 'capability ships an inject without a helperInjection policy', capability: id, ...(policy.manifest.layer ? { slot: policy.manifest.layer } : {}) }];
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
39
45
|
if (policy.fact) requirements.push(policy.fact);
|
|
40
46
|
}
|
|
41
47
|
const resolved = resolveChoices({ requirements, candidates: plan.candidates });
|
|
@@ -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([
|
|
@@ -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
150
|
const target = inspectWorkTarget(input.workTarget), discovery = createWorkspaceDiscovery(repositories);
|
|
150
|
-
const
|
|
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 },
|
|
@@ -241,7 +246,9 @@ export function buildFreshPreparationRequest(inspection, options = {}) {
|
|
|
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,
|
|
@@ -31,6 +31,16 @@ function packageSubset(artifacts, root) {
|
|
|
31
31
|
|
|
32
32
|
/** Repositories and kernel callbacks belong to this operation; the caller owns
|
|
33
33
|
* their lifetime/scratch cleanup. No hook, launch, enrollment or approval here. */
|
|
34
|
+
/** Origins of the choices that selected a capability — the same derivation preparation
|
|
35
|
+
* bindings use, so completion refusals render beside binding problems. */
|
|
36
|
+
function originsOf(plan, capability) {
|
|
37
|
+
const seen = new Map();
|
|
38
|
+
for (const key of plan.capabilities?.[capability]?.choiceKeys ?? []) {
|
|
39
|
+
const origin = plan.choices?.[key]?.selectedBy;
|
|
40
|
+
if (origin) seen.set(canonicalJson(origin), origin);
|
|
41
|
+
}
|
|
42
|
+
return [...seen.values()];
|
|
43
|
+
}
|
|
34
44
|
export function prepareComposition(input, { repositories, kernel, previous: suppliedPrevious }) {
|
|
35
45
|
canonicalJson(input);
|
|
36
46
|
objectAt(input, ["deployment", "directory", "source", "origin", "workspace", "member", "operator", "mode", "allowLocalPaths", "standaloneContextKey", "launch", "helperLaunches"], ["deployment", "directory", "source", "origin"]);
|
|
@@ -114,8 +124,17 @@ export function prepareComposition(input, { repositories, kernel, previous: supp
|
|
|
114
124
|
if (operator) declarations.push(operatorBindingDeclaration(operator));
|
|
115
125
|
const bound = prepareProviderBindings({ seed, plan, manifests, declarations }, options => kernel.binding({ ...options, deployment }));
|
|
116
126
|
plan = bound.plan; seed = bound.seed;
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
let completion;
|
|
128
|
+
if (bound.problems.length) completion = { record: null, problems: bound.problems };
|
|
129
|
+
else {
|
|
130
|
+
try { completion = kernel.complete({ seed, plan, manifests, mode, deployment, directory, launch: input.launch, helperLaunches: input.helperLaunches }); }
|
|
131
|
+
catch (error) {
|
|
132
|
+
// A typed completion refusal that names its capability is a preparation problem like any
|
|
133
|
+
// other (slot/capability/origins), never a bare top-level error. Anything else propagates.
|
|
134
|
+
if (!Array.isArray(error?.problems) || !["needs-configuration", "requirement-conflict"].includes(error.code)) throw error;
|
|
135
|
+
completion = { record: null, problems: error.problems.map(problem => ({ ...problem, origins: problem.origins ?? originsOf(plan, problem.capability) })) };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
119
138
|
let resolution = null;
|
|
120
139
|
if (completion.record) resolution = commitCapturedResolution(deployment, completion.record);
|
|
121
140
|
// Publishing valid immutable inputs/records need not roll back on a later
|
|
@@ -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. */
|
|
@@ -42,7 +43,7 @@ export function prepareProviderBindings({seed,plan,manifests,declarations},invok
|
|
|
42
43
|
}).map(origin=>[canonicalJson(origin),origin])).values()];
|
|
43
44
|
const problem=(slot,id,code,message,origins=originsFor(id))=>({code,message,origins,slot,capability:id});
|
|
44
45
|
const refuse=(slot,id,phase,error)=>problem(slot,id,safeCodes.has(error?.code)?error.code:'provider-not-qualified',
|
|
45
|
-
`${id} ${slot} ${phase} binding could not be prepared`); //
|
|
46
|
+
safeProviderReason(error?.message,providerReasons(manifests.get(id))) ?? `${id} ${slot} ${phase} binding could not be prepared`); // Recheck fixed text; never arbitrary exceptions.
|
|
46
47
|
for (const [id,manifest] of manifests) {
|
|
47
48
|
if (!Object.hasOwn(seed.artifacts.capabilities,id) || !manifest.layer) continue;
|
|
48
49
|
if (plan.providers[manifest.layer] !== id) problems.push(problem(manifest.layer,id,'needs-configuration','fundamental capability must be selected in its own layer'));
|
|
@@ -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-]*$/ });
|