@awebai/oats 0.22.0
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/LICENSE +21 -0
- package/README.md +380 -0
- package/bin/oats.mjs +3294 -0
- package/capabilities/oats-authoring/oats.json +12 -0
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +297 -0
- package/capabilities/oats-aweb/injects/aweb.md +55 -0
- package/capabilities/oats-aweb/oats.json +47 -0
- package/capabilities/oats-aweb/package.json +20 -0
- package/capabilities/oats-jira/bin/oats-jira.mjs +40 -0
- package/capabilities/oats-jira/injects/jira.md +10 -0
- package/capabilities/oats-jira/oats.json +22 -0
- package/capabilities/oats-jira/package.json +25 -0
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +176 -0
- package/capabilities/oats-linear/README.md +234 -0
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +32 -0
- package/capabilities/oats-linear/bin/oats-linear.mjs +344 -0
- package/capabilities/oats-linear/injects/linear.md +8 -0
- package/capabilities/oats-linear/oats.json +24 -0
- package/capabilities/oats-linear/package.json +29 -0
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +217 -0
- package/capabilities/oats-linear/test/oats-linear.test.mjs +168 -0
- package/capabilities/oats-okf/agents/memory-harvest.md +25 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +283 -0
- package/capabilities/oats-okf/injects/okf.md +62 -0
- package/capabilities/oats-okf/oats.json +20 -0
- package/capabilities/oats-okf/package.json +22 -0
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +100 -0
- package/capabilities/oats-okf/skills/okf/SKILL.md +107 -0
- package/capabilities/oats-okf/skills/okf/scripts/okf-validate.mjs +123 -0
- package/capabilities/oats-review/agents/reviewer/AGENTS.md +53 -0
- package/capabilities/oats-review/agents/reviewer/soul.yaml +6 -0
- package/capabilities/oats-review/injects/review.md +68 -0
- package/capabilities/oats-review/oats.json +10 -0
- package/capabilities/oats-review/skills/code-review/SKILL.md +44 -0
- package/capabilities/oats-review/skills/security-review/SKILL.md +59 -0
- package/docs/capabilities.md +505 -0
- package/docs/capability-manifest.schema.json +223 -0
- package/docs/configuration.md +482 -0
- package/docs/conventions.md +73 -0
- package/docs/design/desktop-ux-plan.md +362 -0
- package/docs/design/package-engine-contract.md +813 -0
- package/docs/design/package-runtime-api.md +414 -0
- package/docs/desktop-cli-api.md +89 -0
- package/docs/desktop-succession.md +51 -0
- package/docs/desktop.md +187 -0
- package/docs/implementation.md +282 -0
- package/docs/integrations.md +123 -0
- package/docs/knowledge-theory.md +97 -0
- package/docs/knowledge.md +139 -0
- package/docs/layers.md +110 -0
- package/docs/migration-from-oas.md +122 -0
- package/docs/oats-config.schema.json +140 -0
- package/docs/oats-lock.schema.json +175 -0
- package/docs/oats-package.schema.json +129 -0
- package/docs/packages.md +442 -0
- package/docs/release-lane.md +132 -0
- package/docs/release-notes/v0.18.2.md +79 -0
- package/docs/release-notes/v0.18.3.md +63 -0
- package/docs/release-notes/v0.18.4.md +65 -0
- package/docs/release-notes/v0.18.5.md +66 -0
- package/docs/release-notes/v0.18.6.md +87 -0
- package/docs/release-notes/v0.19.0.md +186 -0
- package/docs/release-notes/v0.19.1.md +24 -0
- package/docs/release-notes/v0.19.2.md +32 -0
- package/docs/release-notes/v0.19.3.md +16 -0
- package/docs/release-notes/v0.19.4.md +12 -0
- package/docs/release-notes/v0.20.0.md +89 -0
- package/docs/release-notes/v0.22.0.md +180 -0
- package/docs/souls-and-instances.md +344 -0
- package/injects/framework-workspace.md +7 -0
- package/injects/instance-boundary.md +43 -0
- package/injects/local-soul.md +19 -0
- package/injects/oats.md +11 -0
- package/injects/work-attached.md +16 -0
- package/injects/work-checkout.md +12 -0
- package/injects/work-workspace.md +22 -0
- package/injects/work-worktree.md +13 -0
- package/lib/core.mjs +6792 -0
- package/lib/packages.mjs +1375 -0
- package/lib/tmux-config.mjs +35 -0
- package/package-catalog.json +44 -0
- package/package.json +55 -0
- package/packages/record/README.md +151 -0
- package/packages/record/bin/capture.mjs +256 -0
- package/packages/record/bin/recall.mjs +102 -0
- package/packages/record/bin/setup.mjs +281 -0
- package/packages/record/bin/turn-record.mjs +47 -0
- package/packages/record/docs/turn-record-sot.md +350 -0
- package/packages/record/lib/canonical.mjs +151 -0
- package/packages/record/lib/capture-aw.mjs +179 -0
- package/packages/record/lib/capture-cc.mjs +322 -0
- package/packages/record/lib/formats.mjs +296 -0
- package/packages/record/lib/ignore.mjs +155 -0
- package/packages/record/lib/index-db.mjs +524 -0
- package/packages/record/lib/project-aweb.mjs +251 -0
- package/packages/record/lib/segments.mjs +186 -0
- package/packages/record/lib/store.mjs +447 -0
- package/packages/record/lib/tags.mjs +114 -0
- package/packages/record/package.json +45 -0
- package/packages/record/test/vectors/README.md +34 -0
- package/packages/record/test/vectors/aweb-projection-v1.json +231 -0
- package/packages/record/test/vectors/journal-merge-v1.json +261 -0
- package/packages/record/test/vectors/turn-id-v1.json +152 -0
- package/packages/record/test/vectors/validate.mjs +391 -0
- package/skills/integration-authoring/SKILL.md +76 -0
- package/skills/oats/SKILL.md +151 -0
- package/skills/oats-config/SKILL.md +162 -0
- package/skills/oats-getting-started/SKILL.md +159 -0
- package/skills/oats-packages/SKILL.md +180 -0
- package/skills/oats-support/SKILL.md +79 -0
- package/skills/skill-craft/SKILL.md +109 -0
- package/skills/soul-craft/SKILL.md +109 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
# Package engine contract (capability materialization, lock v2)
|
|
2
|
+
|
|
3
|
+
Status: **FROZEN** for the capability-materialization delivery. This document is
|
|
4
|
+
the resolver / projection / lock API that the config-and-CLI lane builds
|
|
5
|
+
against. It implements the accepted Decision "Packages materialize capabilities
|
|
6
|
+
while config templates remain explicitly adopted local policy" (2026-07-29) as
|
|
7
|
+
amended by the founder's simplification ruling: the capability-materialization
|
|
8
|
+
`{ packages, capabilities }` lock **replaces** the earlier package-root
|
|
9
|
+
`lockfileVersion: 2` in place and **remains `lockfileVersion: 2`**. There is no
|
|
10
|
+
lock v3, and there is no compatibility path or migration for the earlier
|
|
11
|
+
transitional v2 shape — it is unsupported input, rejected rather than converted.
|
|
12
|
+
Contract changes go through the coordinator to the maintainer.
|
|
13
|
+
|
|
14
|
+
Companion machine-readable schemas:
|
|
15
|
+
|
|
16
|
+
- [`docs/oats-package.schema.json`](../oats-package.schema.json) — `oats-package.json`
|
|
17
|
+
- [`docs/oats-lock.schema.json`](../oats-lock.schema.json) — `oats-lock.json` v2 (+ readable, usable v1)
|
|
18
|
+
|
|
19
|
+
Addendum: [`package-runtime-api.md`](./package-runtime-api.md) — the public
|
|
20
|
+
package-runtime CLI boundary, npm runtime closure semantics, incremental
|
|
21
|
+
transaction guarantees, and runtime-validated schema invariants.
|
|
22
|
+
|
|
23
|
+
## 0. The model in one paragraph
|
|
24
|
+
|
|
25
|
+
A **package** is transport: the source, dependency, integrity, review and atomic
|
|
26
|
+
update unit. A **capability** is the installed entity: the versioned, targetable,
|
|
27
|
+
activatable, trustable thing a user actually runs. Acquisition stages a package
|
|
28
|
+
closure in a temporary transaction directory, validates the whole selected
|
|
29
|
+
payload, **materializes** each declared capability into a flat, self-contained
|
|
30
|
+
artifact under `.agents/capabilities/installed/<capability-id>/`, writes the
|
|
31
|
+
exact lock, and discards staging. There is **no persistent package root**.
|
|
32
|
+
Acquisition activates nothing and trusts nothing. Config templates are package
|
|
33
|
+
source material that installation never applies.
|
|
34
|
+
|
|
35
|
+
## 1. Package source grammar and normalized identity
|
|
36
|
+
|
|
37
|
+
*(Unchanged. Restated because the lock's `source` and `path` fields are parsed
|
|
38
|
+
against exactly this grammar.)*
|
|
39
|
+
|
|
40
|
+
A **package source spec** (CLI argument, or an entry in a manifest's
|
|
41
|
+
`dependencies[]`) takes one of four forms:
|
|
42
|
+
|
|
43
|
+
| Form | Examples | Notes |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| Shorthand git | `git:github.com/org/repo@v1.2.0#<path>`, `git:host/org/repo@<ref>` | `@ref` and `#<path>` both optional at the CLI; resolved once and exact-locked, never advanced on restore |
|
|
46
|
+
| Raw git URL | `https://host/org/repo.git@v1.2.0#dist/oats`, `git@host:org/repo.git@ref#.` | HTTPS or SSH; same `@ref` and `#<path>` rules |
|
|
47
|
+
| Local path | `./pkgs/mypkg`, `/abs/path`, `path:./pkgs/mypkg` | development escape hatch; locked with `commit: "local"` and tree integrity; **exact directory** — no `#<path>` and no default-path heuristic |
|
|
48
|
+
| Official catalog short ID | `oats.okf`, `oats.okf@v1.4.0` | pattern `^[a-z0-9][a-z0-9._-]*$` with optional `@selector`; resolved through the catalog to a git repo **and its `path`**; takes no `#<path>` |
|
|
49
|
+
|
|
50
|
+
Manifest `dependencies[]` entries must be pinnable: an official selector, a
|
|
51
|
+
pinned git tag/commit (`@ref` required), or a local path. There is **no
|
|
52
|
+
general semver solver**.
|
|
53
|
+
|
|
54
|
+
### 1.1 Contained package root (`path`)
|
|
55
|
+
|
|
56
|
+
A Git repository is not a package: it *contains* one. The **package root** is
|
|
57
|
+
the directory inside the fetched source that carries `oats-package.json`, and it
|
|
58
|
+
is selected by the source contract — never hardcoded at a use site:
|
|
59
|
+
|
|
60
|
+
- **Git specs** select it with a single `#<path>` fragment, split off *before*
|
|
61
|
+
`@ref` parsing so a path can never be mistaken for part of a ref. One
|
|
62
|
+
fragment maximum; a second `#` is `invalid-source`.
|
|
63
|
+
- **Catalog entries** carry it as data: `{ url, ref?, path? }`. The catalog
|
|
64
|
+
owns its packages' roots, so an entry may move one (see §5.5 `updatePackage`).
|
|
65
|
+
- **Omitted** on either: the default is **`oats-package`**.
|
|
66
|
+
- **Local paths** never take one. `oats install /repo/custom-root` treats that
|
|
67
|
+
exact directory as the package root whatever it is named, and locks `.`.
|
|
68
|
+
|
|
69
|
+
**Canonical form.** A path is POSIX-relative with no redundant or trailing
|
|
70
|
+
separators. Every spelling of the source root (`.`, `./`, `./.`, empty)
|
|
71
|
+
normalizes to the single canonical `"."`, so a root selection round-trips
|
|
72
|
+
identically through spec → lock → JSON → doctor/list/update. Absolute paths,
|
|
73
|
+
Windows drive paths, `~` spellings, backslash separators and NUL are
|
|
74
|
+
`invalid-source`; `..` traversal is `path-escape`.
|
|
75
|
+
|
|
76
|
+
**Resolution and containment.** One exact commit is cloned once; the configured
|
|
77
|
+
path is resolved *inside that checkout by realpath*; `oats-package.json` must be
|
|
78
|
+
there; and **only that subtree** is staged, hashed and projected. A path that
|
|
79
|
+
resolves outside the checkout — through a symlink at any depth — and a broken
|
|
80
|
+
link are `path-escape`, decided before any store or lock mutation. Staged
|
|
81
|
+
payload bytes therefore equal the selected subtree: repository docs, CI
|
|
82
|
+
configuration, owner souls and sibling packages never reach `integrity` and can
|
|
83
|
+
never reach an installed artifact. Root source-control metadata (`.git`) is
|
|
84
|
+
always stripped on staging, including direct local roots.
|
|
85
|
+
|
|
86
|
+
One repository may contain several packages selected by different paths. Because
|
|
87
|
+
the closure dedupe key is *source **and** selected path*, two contained roots
|
|
88
|
+
claiming the same OATS package identity still fail `duplicate-package-identity`.
|
|
89
|
+
|
|
90
|
+
**Normalized identity** (what dedupe and lock keys use):
|
|
91
|
+
|
|
92
|
+
- The **package identity** is the `package` field of the staged
|
|
93
|
+
`oats-package.json` — never derived from the source string.
|
|
94
|
+
- The **normalized source** recorded in the lock is one of
|
|
95
|
+
`git:<canonical-url>@<ref>`, `path:<dir>`, `catalog:<id>` for an originally
|
|
96
|
+
bare catalog request, or `catalog:<id>@<selector>` for an originally explicit
|
|
97
|
+
selector. The resolved catalog commit is recorded separately in `commit`.
|
|
98
|
+
- The **selected package root** is recorded in the lock as its own strict
|
|
99
|
+
`path` field — never folded into the source string, stored in canonical form
|
|
100
|
+
only, never normalized or repaired on read (`invalid-lock` otherwise).
|
|
101
|
+
- A lock's `source` is parsed against **exactly** that writer grammar, and
|
|
102
|
+
never carries a `#<path>` fragment. Strictness is load-bearing: `updatePackage`
|
|
103
|
+
and `readLockedConfigTemplates` re-derive a source spec from this string, so a
|
|
104
|
+
payload that merely starts with a known scheme but is invalid for its kind
|
|
105
|
+
(`catalog:../evil`, `path:relative/dir`) would be RECLASSIFIED. Such entries
|
|
106
|
+
are `invalid-lock` at parse, before anything can act on them.
|
|
107
|
+
|
|
108
|
+
**Catalog resolver boundary**: the catalog is a pure mapping *official short ID
|
|
109
|
+
→ git repository (+ optional selector → ref translation, + optional package
|
|
110
|
+
root path)*. It authenticates identity and discovery only. It performs **no lock
|
|
111
|
+
advancement** and grants **no executable trust**; after catalog resolution the
|
|
112
|
+
source behaves exactly like a pinned git source. The engine ships a fixture
|
|
113
|
+
catalog for tests.
|
|
114
|
+
|
|
115
|
+
## 2. Package manifest: what a package must declare
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"package": "example.engineering",
|
|
120
|
+
"version": "3.0.0",
|
|
121
|
+
"description": "Shared agent capabilities and workspace defaults.",
|
|
122
|
+
"compatibility": { "oats": ">=0.20.0" },
|
|
123
|
+
"capabilities": ["capabilities/example-review", "capabilities/example-delivery"],
|
|
124
|
+
"configTemplates": {
|
|
125
|
+
"default": { "path": "config-templates/default/oats-config.yaml", "default": true }
|
|
126
|
+
},
|
|
127
|
+
"dependencies": ["oats.okf@v1.4.0"]
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Binding rules (schema + runtime; JSON Schema alone is not complete — see the
|
|
132
|
+
addendum §4):
|
|
133
|
+
|
|
134
|
+
1. **`capabilities` is REQUIRED and non-empty.** Config-only and empty packages
|
|
135
|
+
are rejected: `invalid-package-manifest`. A package's reason to exist is the
|
|
136
|
+
capabilities it materializes.
|
|
137
|
+
2. **Dedicated capability roots.** Each entry names a directory carrying one
|
|
138
|
+
`oats.json`. Authoring never emits `"."`; conventional roots are
|
|
139
|
+
`capabilities/<slug>/`.
|
|
140
|
+
3. **`configTemplates` is the canonical spelling and is OPTIONAL.** A package
|
|
141
|
+
that ships no template is perfectly valid — installation is about
|
|
142
|
+
capabilities. `configs` is accepted as a deprecated read-only alias so
|
|
143
|
+
immutable published tags stay consumable; both spellings normalize to one
|
|
144
|
+
descriptor shape carrying a diagnostic `legacySpelling`. Carrying **both** is
|
|
145
|
+
`invalid-package-manifest`.
|
|
146
|
+
4. **Legacy `"."` capability roots are read compatibility, and the discriminator
|
|
147
|
+
is `configTemplates` — never `configs`.** Published packages exist with a
|
|
148
|
+
`"."` root and *no* template map at all (`oats.authoring@1.0.0` is
|
|
149
|
+
`capabilities: ["."]` with neither spelling), so keying acceptance on
|
|
150
|
+
`configs` would strand them. The rule is therefore:
|
|
151
|
+
- a manifest **without** `configTemplates` may declare `"."` — the
|
|
152
|
+
compatibility reader accepts it and projects it;
|
|
153
|
+
- a manifest **with** `configTemplates` is unambiguously new, and `"."` is
|
|
154
|
+
`invalid-package-manifest`;
|
|
155
|
+
- authoring tooling rejects and never emits `"."` regardless.
|
|
156
|
+
`"."` remains exclusive with any other capability path (it would nest one
|
|
157
|
+
capability inside another).
|
|
158
|
+
5. **Self-containment.** Everything a capability declares (`skills`, `inject`,
|
|
159
|
+
`commands`, `hooks`, `agents`) must exist and resolve **inside that
|
|
160
|
+
capability's own root** after symlink resolution. A capability reaching
|
|
161
|
+
package-only paths, sibling capabilities, or outside the package is
|
|
162
|
+
`capability-not-self-contained` — it cannot be materialized, and the engine
|
|
163
|
+
fails rather than silently installing a broken artifact.
|
|
164
|
+
6. At most one `configTemplates.*.default === true`; `compatibility.oats` is
|
|
165
|
+
required with exactly the grammar `>=x.y.z` / `^x.y.z` / `x.y.z`.
|
|
166
|
+
7. Two capability paths in one package exporting the same capability ID is
|
|
167
|
+
`duplicate-capability-id`; two packages at one scope exporting the same
|
|
168
|
+
capability ID is `duplicate-capability-id` with both packages as provenance.
|
|
169
|
+
|
|
170
|
+
## 3. Store layout and the materialized artifact
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
<scope>/oats-config.yaml zero or one active config
|
|
174
|
+
<scope>/oats-lock.json lock (v2)
|
|
175
|
+
<scope>/.agents/capabilities/.gitignore contains exactly `installed/`
|
|
176
|
+
<scope>/.agents/capabilities/owned/<id>/ authored; normally committed
|
|
177
|
+
<scope>/.agents/capabilities/installed/<id>/ MATERIALIZED artifact; ignored
|
|
178
|
+
<scope>/.agents/config-templates/adopted/<pkg>/<template>/
|
|
179
|
+
adopted base + adoption.json
|
|
180
|
+
(written by the config lane;
|
|
181
|
+
NEVER ignored)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- There is **no `<scope>/.agents/packages/`** in this model at all. A package
|
|
185
|
+
checkout exists only inside a transaction staging directory, which is created
|
|
186
|
+
under `.agents/capabilities/installed/.staging-<random>/` (same filesystem as
|
|
187
|
+
the destination, so the commit phase is a rename; already gitignored; skipped
|
|
188
|
+
by discovery because it is dot-prefixed) and removed unconditionally when the
|
|
189
|
+
transaction ends.
|
|
190
|
+
- The **materialized artifact** at `installed/<id>/` is the complete validated
|
|
191
|
+
capability root: its `oats.json`, skills, injections, commands, hooks,
|
|
192
|
+
capability-defined agents, and its materialized runtime closure
|
|
193
|
+
(`node_modules`, §6). It additionally carries a generated
|
|
194
|
+
**`.oats-installation.json`** provenance file.
|
|
195
|
+
|
|
196
|
+
### 3.1 `.oats-installation.json` — deterministic, replayable provenance
|
|
197
|
+
|
|
198
|
+
The file is **inside** the hashed tree, so tampering with it is integrity drift.
|
|
199
|
+
That is only sound if a future kernel reprojecting the same locked bytes
|
|
200
|
+
produces the same file, so it contains **nothing about the writing kernel** —
|
|
201
|
+
only lock-, source- and manifest-derived values:
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"schemaVersion": 1,
|
|
206
|
+
"capability": "example.review",
|
|
207
|
+
"version": "2.1.0",
|
|
208
|
+
"package": "example.engineering",
|
|
209
|
+
"packageVersion": "3.0.0",
|
|
210
|
+
"source": "catalog:example.engineering",
|
|
211
|
+
"commit": "0123456789abcdef0123456789abcdef01234567",
|
|
212
|
+
"packagePath": "oats-package",
|
|
213
|
+
"capabilityPath": "capabilities/example-review"
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Binding serialization: exactly these keys, in exactly this order,
|
|
218
|
+
`JSON.stringify(obj, null, 2)` plus a single trailing `"\n"`, written with mode
|
|
219
|
+
`0o644`. `schemaVersion` is a constant of the format, bumped only by an explicit
|
|
220
|
+
contract change (which is itself an integrity change, so it is visible). Every
|
|
221
|
+
field must agree with the lock rows it was projected from; a disagreement is
|
|
222
|
+
`invalid-lock` at read time, and a modified file is `integrity-drift`. Bare
|
|
223
|
+
restore under a newer kernel therefore reproduces the identical artifact hash.
|
|
224
|
+
|
|
225
|
+
### 3.2 Integrity boundaries
|
|
226
|
+
|
|
227
|
+
- **Capability artifact integrity** (`capabilityArtifactIntegrity`) hashes
|
|
228
|
+
**every byte** under the artifact root — no exclusions, including
|
|
229
|
+
`node_modules` and `.oats-installation.json`. It is the only thing executable
|
|
230
|
+
trust binds to; the runtime closure is *inside* the artifact, so there is no
|
|
231
|
+
separate dependency digest anywhere in this model.
|
|
232
|
+
- **Package payload integrity** (`packageIntegrity`) hashes the staged package
|
|
233
|
+
subtree excluding any `node_modules` and a root `oats-lock.json`. It proves the
|
|
234
|
+
distribution bytes and is what bare restore re-verifies before reprojecting.
|
|
235
|
+
- `.agents/capabilities/owned/<id>/` and `from: path:<dir>` keep their exact
|
|
236
|
+
current semantics, precedence, and structural trust. `from: installed` means
|
|
237
|
+
the flat installed-capability store regardless of which package supplied it.
|
|
238
|
+
|
|
239
|
+
### 3.3 Git ignore maintenance is part of the transaction
|
|
240
|
+
|
|
241
|
+
`ensureInstalledGitignore` is **not** post-commit convenience. At a Git-backed
|
|
242
|
+
scope the engine **preflights** `.agents/capabilities/.gitignore` before any
|
|
243
|
+
authoritative mutation: it snapshots the prior bytes (or the file's absence),
|
|
244
|
+
ensures the file contains `installed/`, and if it cannot do so **fails before
|
|
245
|
+
any artifact or lock mutation**. If the transaction later fails, the ignore
|
|
246
|
+
bytes are rolled back with everything else. It writes `installed/` and nothing
|
|
247
|
+
else: `owned/` holds authored capabilities and
|
|
248
|
+
`.agents/config-templates/adopted/` holds portable adopted bases, and neither is
|
|
249
|
+
ever ignored or touched. Outside version control it is a no-op: a non-Git scope
|
|
250
|
+
uses the same layout without pretending Git owns its durability.
|
|
251
|
+
|
|
252
|
+
The engine owns this for acquire, restore, update and v1 migration. The CLI lane
|
|
253
|
+
keeps its own outer rollback journal and snapshots the prior ignore state
|
|
254
|
+
alongside its other state; no engine transaction handle or callback is exchanged.
|
|
255
|
+
|
|
256
|
+
## 4. Lock v2 (capability materialization)
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"lockfileVersion": 2,
|
|
261
|
+
"packages": {
|
|
262
|
+
"example.engineering": {
|
|
263
|
+
"source": "git:https://example.invalid/engineering.git@v3.0.0",
|
|
264
|
+
"path": "oats-package",
|
|
265
|
+
"version": "3.0.0",
|
|
266
|
+
"commit": "0123456789abcdef0123456789abcdef01234567",
|
|
267
|
+
"integrity": "sha256-…",
|
|
268
|
+
"dependencies": ["oats.okf"]
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
"capabilities": {
|
|
272
|
+
"example.review": {
|
|
273
|
+
"version": "2.1.0",
|
|
274
|
+
"package": "example.engineering",
|
|
275
|
+
"path": "capabilities/example-review",
|
|
276
|
+
"integrity": "sha256-…",
|
|
277
|
+
"trusted": false
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
- Both top-level maps are **required**. `packages` rows exact-lock the transport
|
|
284
|
+
unit: `source`, selected package `path`, `version`, `commit`, package payload
|
|
285
|
+
`integrity`, and **package-identity** `dependencies` — always present, an
|
|
286
|
+
empty array when there are none. `capabilities` rows exact-lock the installed
|
|
287
|
+
entity: `version` (from the capability's own `oats.json`), provider `package`,
|
|
288
|
+
dedicated manifest `path` inside that package, materialized artifact
|
|
289
|
+
`integrity`, and boolean `trusted` bound **only** to that artifact integrity.
|
|
290
|
+
- A package row carries **no** capability list and **no** trust: the capability
|
|
291
|
+
rows' `package` back-reference is the single provider truth, so the two levels
|
|
292
|
+
cannot disagree.
|
|
293
|
+
- Semantic invariants validated before any consumption (`invalid-lock`):
|
|
294
|
+
every `capabilities.*.package` is a key of `packages`; every
|
|
295
|
+
`packages.*.dependencies[]` id is a key of `packages`, with no self-dependency
|
|
296
|
+
and no cycle; canonical `path` spellings; `path:` sources require
|
|
297
|
+
`commit: "local"` and `path: "."`; `git:`/`catalog:` require an exact 40-hex
|
|
298
|
+
commit; sha256 digest shapes; array uniqueness.
|
|
299
|
+
- **Prototype safety is global.** Every map is read into a null-prototype
|
|
300
|
+
object with identity-charset-validated keys, and *every* lookup or membership
|
|
301
|
+
check keyed by a package or capability ID uses `Object.hasOwn` or such a map —
|
|
302
|
+
central read, dependency graph, provider resolution, trust, update and remove
|
|
303
|
+
alike. Raw parsed JSON returns inherited `constructor`, `toString` or
|
|
304
|
+
`valueOf` for `map[id]` when no own entry exists, so a prototype-named or
|
|
305
|
+
hostile ID must never be able to impersonate a provider, a dependency or a
|
|
306
|
+
trust entry, or to bypass a membership check.
|
|
307
|
+
|
|
308
|
+
### 4.1 The unsupported transitional v2 shape
|
|
309
|
+
|
|
310
|
+
The earlier package-root spelling of `lockfileVersion: 2` is **unsupported
|
|
311
|
+
input**. It is detected centrally, **before any discovery or mutation**, and
|
|
312
|
+
rejected with typed `invalid-lock` and an actionable message naming the
|
|
313
|
+
unsupported transitional v2 shape and scope recreation. It is never converted
|
|
314
|
+
and never partially interpreted.
|
|
315
|
+
|
|
316
|
+
Detection uses **direct raw lock-scope reads** — walking ancestor directories
|
|
317
|
+
that own an `oats-lock.json`, *not* `configChain`, so lock-only scopes with no
|
|
318
|
+
config are visible — and this exact OR predicate:
|
|
319
|
+
|
|
320
|
+
1. a **nonempty** `lockfileVersion: 2` document with no top-level `capabilities`
|
|
321
|
+
map is unsupported transitional v2 **by itself**; **independently**
|
|
322
|
+
2. any package row for which
|
|
323
|
+
`Object.hasOwn(row, "capabilities") || Object.hasOwn(row, "trustedCapabilities") || Object.hasOwn(row, "depsIntegrity")`
|
|
324
|
+
is forbidden transitional evidence.
|
|
325
|
+
|
|
326
|
+
The row test is **own-property presence, never truthiness or array length**: an
|
|
327
|
+
empty `capabilities: []`, an empty `trustedCapabilities: []`, and a dependency-free
|
|
328
|
+
old row that carries `capabilities`/`trustedCapabilities` but no `depsIntegrity`
|
|
329
|
+
are all still transitional. Package-row `path` and `dependencies` are **never**
|
|
330
|
+
tells — the current shape retains both.
|
|
331
|
+
|
|
332
|
+
An **empty** transitional `{ "lockfileVersion": 2, "packages": {} }` carries no
|
|
333
|
+
state and is semantically identical to an empty current lock, so it normalizes
|
|
334
|
+
to `{ lockfileVersion: 2, packages: {}, capabilities: {} }`. An empty
|
|
335
|
+
`lockfileVersion: 1` file does **not** normalize: it remains pending explicit
|
|
336
|
+
format migration.
|
|
337
|
+
|
|
338
|
+
## 5. Exported kernel functions (`lib/core.mjs`)
|
|
339
|
+
|
|
340
|
+
All functions are runtime-neutral and dependency-free like the rest of the
|
|
341
|
+
kernel. Errors are thrown `Error`s carrying `code` (§8) and, where relevant,
|
|
342
|
+
`provenance`. Typed engine codes and messages pass through the CLI verbatim.
|
|
343
|
+
|
|
344
|
+
### 5.1 Source, manifest, integrity
|
|
345
|
+
|
|
346
|
+
```js
|
|
347
|
+
export function parsePackageSource(spec, { baseDir } = {})
|
|
348
|
+
export const DEFAULT_PACKAGE_PATH // "oats-package"
|
|
349
|
+
export function normalizePackagePath(raw, opts)
|
|
350
|
+
export function inspectGitSourceRoot(spec)
|
|
351
|
+
export function resolvePackageRoot(checkout, packagePath, spec)
|
|
352
|
+
export function gitCheckoutExactRef(dir, ref, spec)
|
|
353
|
+
export function packageIntegrity(dir) // payload hash; excludes node_modules
|
|
354
|
+
export function capabilityArtifactIntegrity(dir) // materialized artifact hash, no exclusions
|
|
355
|
+
export function capabilityIntegrity(dir) // v1 artifact hash (legacy capability store)
|
|
356
|
+
|
|
357
|
+
/** Load + validate an oats-package.json against §2. Returns the manifest plus
|
|
358
|
+
* _dir, _legacySpelling (true when the deprecated `configs` key was used),
|
|
359
|
+
* _configTemplates (normalized { name: { path, description?, default } } from
|
|
360
|
+
* either spelling), and _capabilities: [{ id, rel, dir, manifest }].
|
|
361
|
+
* @throws "invalid-package-manifest", "path-escape", "duplicate-capability-id",
|
|
362
|
+
* "retired-capability"
|
|
363
|
+
*/
|
|
364
|
+
export function loadPackageManifestAt(pdir)
|
|
365
|
+
|
|
366
|
+
/** Assert a capability root can be materialized self-contained: every declared
|
|
367
|
+
* resource exists and resolves (after symlink resolution, recursively through
|
|
368
|
+
* contained directory links) inside `capDir`.
|
|
369
|
+
* @throws "capability-not-self-contained", "path-escape"
|
|
370
|
+
*/
|
|
371
|
+
export function assertCapabilitySelfContained(capDir, manifest)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### 5.2 Locks
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
/** THE strict lock reader/validator for v1 and v2. Returns
|
|
378
|
+
* { version, packages, capabilities, legacyCapabilities } — all null-prototype
|
|
379
|
+
* and validated — or null when the file is absent. Rejects the unsupported
|
|
380
|
+
* transitional v2 shape (§4.1) centrally, with NO side effects. Old locks are
|
|
381
|
+
* read as they are: never normalized, repaired or rewritten (the one exception
|
|
382
|
+
* is the state-free empty transitional document of §4.1).
|
|
383
|
+
*/
|
|
384
|
+
export function parseLockFileStrict(file)
|
|
385
|
+
|
|
386
|
+
/** Read every lock visible from a directory — every ancestor owning an
|
|
387
|
+
* oats-lock.json, plus config-chain levels — closest scope wins per identity.
|
|
388
|
+
* SOLE strict reader; consumers never see an invalid lock as absent or usable.
|
|
389
|
+
* @returns {{
|
|
390
|
+
* packages: Record<pkgId, PackageRow & { _file, _level }>,
|
|
391
|
+
* capabilities: Record<capId, CapabilityRow & { _file, _level }>,
|
|
392
|
+
* legacy: Array<{ file, level, lockfileVersion, capabilities }>, // v1 files
|
|
393
|
+
* migration: Array<{ file, level, lockfileVersion, kind: "v1"|"v1-empty",
|
|
394
|
+
* capabilities: string[] }>
|
|
395
|
+
* }}
|
|
396
|
+
* // `migration` is provenance ONLY: which scopes still need an explicit
|
|
397
|
+
* // conversion and what they hold. Reading never converts anything.
|
|
398
|
+
*/
|
|
399
|
+
export function readPackageLocks(startDir)
|
|
400
|
+
|
|
401
|
+
/** Write/replace (entry) or delete (entry === null) one package row / one
|
|
402
|
+
* capability row. Each validates the COMPLETE prospective document before
|
|
403
|
+
* writing. An existing v1 lock — INCLUDING an empty one — is `legacy-lock`:
|
|
404
|
+
* conversion happens only through explicit migration, so only an ABSENT lock is
|
|
405
|
+
* a fresh document.
|
|
406
|
+
*/
|
|
407
|
+
export function writePackageLock(levelDir, packageId, entry)
|
|
408
|
+
export function writeCapabilityLockEntry(levelDir, capabilityId, entry)
|
|
409
|
+
|
|
410
|
+
/** Semantic validation of one row against the whole document (§4). */
|
|
411
|
+
export function validateLockEntry(packageId, entry, allPackages, opts)
|
|
412
|
+
export function validateCapabilityLockEntry(capabilityId, entry, allPackages, opts)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### 5.3 Acquisition and projection
|
|
416
|
+
|
|
417
|
+
```js
|
|
418
|
+
/** Stage → validate → materialize → lock → discard staging, at one scope.
|
|
419
|
+
*
|
|
420
|
+
* Fetches the root source and the whole dependency closure into a temporary
|
|
421
|
+
* staging directory; validates every manifest (§2), detects cycles, identity
|
|
422
|
+
* collisions and capability-ID collisions (within the closure, and against
|
|
423
|
+
* capabilities already locked at this scope by packages outside it); asserts
|
|
424
|
+
* every capability is self-contained; materializes each capability's runtime
|
|
425
|
+
* closure IN STAGING; calls `opts.assertCommittable` (below); preflights the
|
|
426
|
+
* scope's `.gitignore` (§3.3); then atomically swaps every projected artifact
|
|
427
|
+
* into `.agents/capabilities/installed/<id>/` and writes the lock. On ANY
|
|
428
|
+
* failure — before OR during the commit phase — every already-renamed artifact,
|
|
429
|
+
* the lock bytes and the ignore bytes are rolled back to the pre-operation
|
|
430
|
+
* state. Staging is always removed.
|
|
431
|
+
*
|
|
432
|
+
* ANCHOR DIRECTORIES: staging must live inside the store, so on a scope with no
|
|
433
|
+
* store this necessarily creates `.agents/`, `.agents/capabilities/` and
|
|
434
|
+
* `.agents/capabilities/installed/`. Every refusal and failure path removes
|
|
435
|
+
* exactly the anchors THIS operation created — deepest-first, only while empty,
|
|
436
|
+
* never a pre-existing one and never one holding owned/, adopted/ or any
|
|
437
|
+
* unrelated state. A refused or failed acquisition therefore leaves the scope's
|
|
438
|
+
* tree byte-for-byte and entry-for-entry as it found it, including on a
|
|
439
|
+
* completely clean scope.
|
|
440
|
+
*
|
|
441
|
+
* A v1 lock at the scope is refused BEFORE any source fetch, staging, ignore or
|
|
442
|
+
* artifact work — every v1, INCLUDING AN EMPTY ONE. An empty v1 is still an
|
|
443
|
+
* unconverted scope: converting it as a side effect of `oats install` is the
|
|
444
|
+
* implicit migration §7 forbids, and failing later would make the caller pay for
|
|
445
|
+
* a fetch to learn it.
|
|
446
|
+
*
|
|
447
|
+
* ACTIVATES NOTHING and TRUSTS NOTHING: `trusted` is false for every capability
|
|
448
|
+
* whose artifact integrity is not byte-identical to the one already locked.
|
|
449
|
+
*
|
|
450
|
+
* @param {string} levelDir scope directory owning the store + lock
|
|
451
|
+
* @param {string} spec package source spec
|
|
452
|
+
* @param {{ catalog?, replace?: boolean, expectPackage?: string, rootSnapshot?,
|
|
453
|
+
* assertCommittable?: (preview) => void }} [opts]
|
|
454
|
+
* // `assertCommittable` is the PRE-COMMIT GATE. It is called exactly once,
|
|
455
|
+
* // after the whole closure is projected in staging and BEFORE the ignore
|
|
456
|
+
* // preflight, the artifact swap and the lock write — so the scope is still
|
|
457
|
+
* // completely untouched. It receives the full staged outcome:
|
|
458
|
+
* //
|
|
459
|
+
* // { root, packages: [{ package, version, source, path, commit, integrity,
|
|
460
|
+
* // dependencies, capabilities }],
|
|
461
|
+
* // capabilities: [{ capability, version, package, path, integrity,
|
|
462
|
+
* // trusted, status, layer, executableSurface }],
|
|
463
|
+
* // configTemplates } // identical descriptors to the return value's,
|
|
464
|
+
* // // including `content` and `contentIntegrity`
|
|
465
|
+
* //
|
|
466
|
+
* // It is a PURE GATE: inspect and (optionally) throw, nothing else. A throw
|
|
467
|
+
* // propagates unchanged, staging is discarded, and NOTHING is mutated — no
|
|
468
|
+
* // ignore file, no artifact, no lock byte — so a refusal needs no rollback.
|
|
469
|
+
* // This is what lets guided `oats init --package` present and validate the
|
|
470
|
+
* // complete selected template plan before any engine mutation, and what lets
|
|
471
|
+
* // `oats update` refuse a config-referenced export drop byte-exactly (§5.5).
|
|
472
|
+
* // Staging paths are deliberately NOT exposed: the gate decides, it does not
|
|
473
|
+
* // reach into the transaction.
|
|
474
|
+
* //
|
|
475
|
+
* // `layer` is the capability's DECLARED fundamental layer, normalized to
|
|
476
|
+
* // null when it declares none. It is in the preview because a config
|
|
477
|
+
* // template may bind a fundamental slot to a capability the ROOT PACKAGE
|
|
478
|
+
* // ITSELF supplies: before the commit that capability is not materialized,
|
|
479
|
+
* // is not in the lock, and is not discoverable, so the preview is the only
|
|
480
|
+
* // place the binding can be validated. Validating it after the commit and
|
|
481
|
+
* // unwinding is strictly worse — the gate exists so that case never needs an
|
|
482
|
+
* // outer rollback. The field is the minimum needed for that check; it is not
|
|
483
|
+
* // a staging path and not the manifest.
|
|
484
|
+
* @returns {{
|
|
485
|
+
* root: string,
|
|
486
|
+
* lockFile: string,
|
|
487
|
+
* installed: Array<{ package, version, source, path, commit, integrity,
|
|
488
|
+
* dependencies: string[], capabilities: string[], kept: boolean }>,
|
|
489
|
+
* capabilities: Array<{ capability, version, package, path, integrity, dir,
|
|
490
|
+
* trusted: boolean, status: "installed"|"replaced"|"kept",
|
|
491
|
+
* layer: string|null,
|
|
492
|
+
* executableSurface: { commands: string[], hooks: string[],
|
|
493
|
+
* environment: string[] } }>,
|
|
494
|
+
* configTemplates: Array<{ package, template, path, description?, default: boolean,
|
|
495
|
+
* content: string, contentIntegrity: string,
|
|
496
|
+
* legacySpelling?: boolean }>
|
|
497
|
+
* }}
|
|
498
|
+
* // `configTemplates` carries VALIDATED descriptors AND payload bytes read
|
|
499
|
+
* // from staging before it is discarded, with digests identical to what
|
|
500
|
+
* // `readLockedConfigTemplates` would return, so the config lane can adopt a
|
|
501
|
+
* // template inside the same transaction without a second fetch. Acquisition
|
|
502
|
+
* // itself applies none of them.
|
|
503
|
+
* @throws "invalid-source", "invalid-package-manifest", "path-escape",
|
|
504
|
+
* "capability-not-self-contained", "dependency-cycle",
|
|
505
|
+
* "duplicate-package-identity", "duplicate-capability-id",
|
|
506
|
+
* "incompatible-oats", "integrity-drift", "legacy-lock", "invalid-lock"
|
|
507
|
+
*/
|
|
508
|
+
export function acquirePackage(levelDir, spec, opts)
|
|
509
|
+
|
|
510
|
+
/** Bare restore, for every visible lock-owning scope.
|
|
511
|
+
*
|
|
512
|
+
* Preflight parses and caches the COMPLETE visible chain before any fetch,
|
|
513
|
+
* staging or swap. Per capability: a present artifact whose integrity equals the
|
|
514
|
+
* locked integrity is `ok`. Otherwise the provider package's EXACT locked
|
|
515
|
+
* provenance (source + commit + path) is fetched once per package into staging,
|
|
516
|
+
* its payload integrity is verified against the package row, the capability is
|
|
517
|
+
* reprojected, its artifact integrity is verified against the capability row,
|
|
518
|
+
* and only then swapped in. NEVER advances source/version/commit/path, never
|
|
519
|
+
* changes `trusted`, never converts a lock. v1 scopes restore through the
|
|
520
|
+
* existing legacy capability path and are reported with their migration action.
|
|
521
|
+
*
|
|
522
|
+
* @returns {Array<{ package?, capability?, level, status:
|
|
523
|
+
* "ok"|"restored"|"failed"|"legacy", dir?, reason?, code? }>}
|
|
524
|
+
* @throws "invalid-lock" (preflight, before any mutation)
|
|
525
|
+
*/
|
|
526
|
+
export function restorePackages(startDir, opts)
|
|
527
|
+
|
|
528
|
+
/** Derive the package/provider view from the lock + the flat capability store —
|
|
529
|
+
* NOT from a package root (there is none). Closest scope wins per package
|
|
530
|
+
* identity; two same-scope packages claiming one capability ID is
|
|
531
|
+
* duplicate-capability-id.
|
|
532
|
+
* @returns {Array<{ package, version, level, source, path, commit, integrity,
|
|
533
|
+
* dependencies: string[],
|
|
534
|
+
* capabilities: Array<{ id, version, path, dir, integrity, trusted,
|
|
535
|
+
* installed: boolean, manifest? }> }>}
|
|
536
|
+
* // `installed:false` + absent `manifest` = locked but not materialized —
|
|
537
|
+
* // exactly what a bare `oats install` repairs. It is reported, never hidden.
|
|
538
|
+
*/
|
|
539
|
+
export function listInstalledPackages(startDir)
|
|
540
|
+
|
|
541
|
+
export function installedCapabilityDir(levelDir, capabilityId)
|
|
542
|
+
export const CAPABILITY_INSTALLATION_FILE // ".oats-installation.json"
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### 5.4 Trust
|
|
546
|
+
|
|
547
|
+
```js
|
|
548
|
+
/** Is this capability's executable surface approved at its CURRENT materialized
|
|
549
|
+
* artifact integrity? Two call shapes (unchanged):
|
|
550
|
+
* capabilityTrust(startDir, capabilityId) // contract shape
|
|
551
|
+
* capabilityTrust(manifest, startDir) // internal resolver/dispatch shape
|
|
552
|
+
* @returns {{ trusted, package, integrity,
|
|
553
|
+
* executableSurface: { commands, hooks, environment },
|
|
554
|
+
* reason? }}
|
|
555
|
+
*/
|
|
556
|
+
export function capabilityTrust(a, b)
|
|
557
|
+
|
|
558
|
+
/** Approve executable surfaces at exactly the current artifact integrity.
|
|
559
|
+
* Per-capability by default; `allCapabilities` treats `id` as a PACKAGE identity
|
|
560
|
+
* and approves every capability that package currently supplies (the caller must
|
|
561
|
+
* have displayed the full executable-surface summary first). Writes
|
|
562
|
+
* `trusted: true` on the capability rows. Non-executable capabilities need no
|
|
563
|
+
* approval (no-op, reported in `skipped`). Official identity grants nothing.
|
|
564
|
+
*
|
|
565
|
+
* Two preconditions per target, BOTH required before any flag is set:
|
|
566
|
+
* 1. the materialized artifact hashes to the capability row's `integrity`;
|
|
567
|
+
* 2. its `.oats-installation.json` agrees with that capability row and its
|
|
568
|
+
* provider package row.
|
|
569
|
+
* Integrity alone is not sufficient — a provenance file edited and then
|
|
570
|
+
* re-hashed into its row leaves every byte matching its recorded digest with
|
|
571
|
+
* only the ORIGIN disagreeing, and approval is what unlocks execution. Both are
|
|
572
|
+
* checked for every target before the single lock write, so a bulk approval
|
|
573
|
+
* commits nothing when one capability's origin is disputed.
|
|
574
|
+
* @throws "unknown-capability", "integrity-drift", "invalid-lock"
|
|
575
|
+
*/
|
|
576
|
+
export function approveCapability(levelDir, id, { allCapabilities } = {})
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### 5.5 Update and remove
|
|
580
|
+
|
|
581
|
+
```js
|
|
582
|
+
/** Transactional update of one package: re-resolve the closure from the row's
|
|
583
|
+
* ORIGINAL spec (or opts.spec), validate everything in staging, then replace
|
|
584
|
+
* ALL of that package's exported capability artifacts and lock rows together.
|
|
585
|
+
*
|
|
586
|
+
* - Every export is validated and replaced atomically — never a partial set.
|
|
587
|
+
* - Trust is preserved ONLY for capabilities whose new artifact integrity is
|
|
588
|
+
* byte-identical to the locked one; any change sets `trusted: false`.
|
|
589
|
+
* - Exports that no longer exist are removed ONLY when safe: no config in the
|
|
590
|
+
* chain references them. Otherwise the whole update fails `remove-blocked`
|
|
591
|
+
* and the pre-operation state is restored.
|
|
592
|
+
* @returns {{ package, level, changed, pathChanged, before, after, installed,
|
|
593
|
+
* capabilities, configTemplates, addedCapabilities, removedCapabilities,
|
|
594
|
+
* retiredArtifacts, invalidatedApprovals }}
|
|
595
|
+
*/
|
|
596
|
+
export function updatePackage(startDir, packageId, opts)
|
|
597
|
+
|
|
598
|
+
/** Remove one locked package and every capability artifact it supplied.
|
|
599
|
+
* Refuses (`remove-blocked`, with provenance) while another locked package in
|
|
600
|
+
* the TARGET ENTRY'S OWN scope map depends on it, or any config in the chain
|
|
601
|
+
* references one of its capabilities. Transactional: artifacts move to a
|
|
602
|
+
* backup, lock rows are removed, and both sides roll back on failure.
|
|
603
|
+
*/
|
|
604
|
+
export function removePackage(startDir, packageId)
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### 5.6 Config templates (read-only, exact-locked)
|
|
608
|
+
|
|
609
|
+
```js
|
|
610
|
+
/** Read config templates from the EXACT currently locked source of one package
|
|
611
|
+
* — the config lane's `oats config diff` / `oats config sync` / `oats config adopt`
|
|
612
|
+
* input.
|
|
613
|
+
*
|
|
614
|
+
* Stages the locked source (source + commit + path), validates the manifest and
|
|
615
|
+
* its resource containment, verifies the payload integrity against the package
|
|
616
|
+
* row, reads the requested template bytes, and removes staging. It NEVER
|
|
617
|
+
* persists a package root, never exposes a path into one, never mutates the lock
|
|
618
|
+
* or the capability store, and never advances anything. A plain list of what is
|
|
619
|
+
* installed must NOT call this — it is a network operation.
|
|
620
|
+
*
|
|
621
|
+
* @param {{ template?: string, catalog? }} [opts] template omitted = all of them
|
|
622
|
+
* @returns {{ package, source, version, commit, path, integrity, legacySpelling,
|
|
623
|
+
* templates: Array<{ template, path, description?, default, content,
|
|
624
|
+
* contentIntegrity, legacySpelling }> }}
|
|
625
|
+
* // `integrity` is the package PAYLOAD integrity, verified equal to the lock.
|
|
626
|
+
* // The CLI lane owns oats-config schema/policy validation.
|
|
627
|
+
* @throws "unknown-capability" (no such locked package), "invalid-lock",
|
|
628
|
+
* "integrity-drift", "invalid-package-manifest", "invalid-source",
|
|
629
|
+
* "unknown-config-template"
|
|
630
|
+
*/
|
|
631
|
+
export function readLockedConfigTemplates(startDir, packageId, opts)
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
**One template descriptor shape, two readers.** Acquisition (§5.3
|
|
635
|
+
`configTemplates`) and this locked reader produce the SAME descriptor, field for
|
|
636
|
+
field, and both carry `legacySpelling` on **every template item** — the root
|
|
637
|
+
`legacySpelling` here is a package-level convenience duplicate, not the only
|
|
638
|
+
place it appears. A consumer must never have to know which reader produced a
|
|
639
|
+
descriptor in order to read it.
|
|
640
|
+
|
|
641
|
+
**`contentIntegrity` digests the exact FILE BYTES**, not the decoded string:
|
|
642
|
+
`sha256-<64 lowercase hex>` over the bytes on disk. Digesting the decoded string
|
|
643
|
+
would hash U+FFFD replacement characters for any byte sequence that failed to
|
|
644
|
+
decode, yielding a digest nothing can reproduce from the file — and adoption
|
|
645
|
+
compares template bytes. Config templates are UTF-8 text by contract, so the
|
|
646
|
+
decode is **fail-closed**: undecodable bytes are `invalid-package-manifest`, a
|
|
647
|
+
malformed package, never silently repaired. Acquisition and the locked reader
|
|
648
|
+
therefore agree byte-for-byte and digest-for-digest on the same locked source.
|
|
649
|
+
|
|
650
|
+
### 5.7 Migration (v1 only)
|
|
651
|
+
|
|
652
|
+
```js
|
|
653
|
+
/** Plan the conversion of one scope's v1 lock. PURE — applies nothing.
|
|
654
|
+
*
|
|
655
|
+
* `marketplace:` entries map through the catalog (aliases first, then identity);
|
|
656
|
+
* `git:`/`path:` entries map to package specs when the source really is a
|
|
657
|
+
* package. Anything the scope cannot convert makes the WHOLE SCOPE
|
|
658
|
+
* unconvertible — the lock has no residue container, so an entry left behind
|
|
659
|
+
* would have nowhere to live and would simply be dropped. `hold`, `manual` AND
|
|
660
|
+
* `retain` therefore all clear `convertible`, and the scope stays v1 in full
|
|
661
|
+
* and keeps working. (`retain` is the guided mode's "keep this custom entry
|
|
662
|
+
* unchanged"; keeping it is only possible by keeping the entire scope.)
|
|
663
|
+
*
|
|
664
|
+
* @returns {{ from: 1|2, convertible: boolean,
|
|
665
|
+
* plan: Array<{ capabilityId?, v1?, package?, spec?,
|
|
666
|
+
* action: "acquire"|"convert-format"|"hold"|"manual"|"retain" ,
|
|
667
|
+
* reason? }>,
|
|
668
|
+
* warnings: string[] }}
|
|
669
|
+
*/
|
|
670
|
+
export function migrateLegacyLock(levelDir, opts)
|
|
671
|
+
|
|
672
|
+
/** Apply that plan, transactionally and all-or-nothing per scope. Executable
|
|
673
|
+
* approvals are never carried over — a v1 capability artifact and a materialized
|
|
674
|
+
* artifact are different bytes — and the returned `trust[]` names every surface
|
|
675
|
+
* to re-approve. Any failure restores the original v1 lock BYTE-IDENTICALLY,
|
|
676
|
+
* removes everything the conversion created, and leaves superseded v1 artifacts
|
|
677
|
+
* in place.
|
|
678
|
+
*
|
|
679
|
+
* A MIXED scope — official work beside `retain` entries — is refused
|
|
680
|
+
* `legacy-lock` BEFORE any lock, artifact or ignore mutation: not one official
|
|
681
|
+
* artifact is partially acquired, and the config, store and trust of that scope
|
|
682
|
+
* are byte-identical afterwards.
|
|
683
|
+
*
|
|
684
|
+
* There is NO residue result and no residue container. `retained` appears only
|
|
685
|
+
* on a `skipped` scope — one with no official work at all, left entirely on v1 —
|
|
686
|
+
* and lists the v1 capability ids that were left untouched.
|
|
687
|
+
* @returns {{ migrated, skipped?, retained?, formatConverted?, warnings, file, trust }}
|
|
688
|
+
* @throws "official-mapping-unavailable", "legacy-lock", "invalid-lock", …
|
|
689
|
+
*/
|
|
690
|
+
export function applyLegacyLockMigration(levelDir, opts)
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
## 6. Runtime closure in the materialized model
|
|
694
|
+
|
|
695
|
+
- **Materialization roots are the declared CAPABILITY roots**, each carrying
|
|
696
|
+
both `package.json` and `package-lock.json`. A package-root-only closure has
|
|
697
|
+
no durable home and is **not** materialized — it is package tooling. If a
|
|
698
|
+
capability actually needs it, self-containment (§2.5) fails and the package is
|
|
699
|
+
rejected. (For a legacy `"."` capability root, capability root == package root,
|
|
700
|
+
so the package-root closure *is* the capability closure.)
|
|
701
|
+
- `npm ci --omit=dev --omit=peer --ignore-scripts --no-audit --no-fund` only;
|
|
702
|
+
no npm lifecycle scripts ever run, at any phase.
|
|
703
|
+
- Transaction-wide platform-invariance preflight over EVERY materialization
|
|
704
|
+
root's lockfile **before** any `npm ci`; post-materialization `.node` native
|
|
705
|
+
binary scan and symlink containment (every link under every materialized
|
|
706
|
+
`node_modules` must realpath-resolve inside the **capability artifact root**)
|
|
707
|
+
**before** any digest or swap.
|
|
708
|
+
- Materialization happens **in staging**; a failure fails the whole transaction
|
|
709
|
+
with the store and lock unchanged.
|
|
710
|
+
- The closure is inside the artifact, so it is covered by the capability's
|
|
711
|
+
`integrity`. Tampering with a materialized dependency invalidates `trusted`
|
|
712
|
+
exactly like source drift, and bare restore reprojects it.
|
|
713
|
+
|
|
714
|
+
## 7. Compatibility
|
|
715
|
+
|
|
716
|
+
**v1 stays usable without any implicit conversion.** Runtime discovery, exact
|
|
717
|
+
restore, trust checks and approval updates, and `doctor`/`list` diagnosis all
|
|
718
|
+
continue to work against v1 locks and the existing
|
|
719
|
+
`.agents/capabilities/installed/` v1 artifacts. Ordinary use of an unconverted
|
|
720
|
+
deployment must not require migration; only lifecycle *mutation* through the new
|
|
721
|
+
package surface does (`legacy-lock`, naming the explicit command). Guided
|
|
722
|
+
official migration converts a v1 scope **directly** into this flat capability
|
|
723
|
+
materialization.
|
|
724
|
+
|
|
725
|
+
**Immutable published package manifests** using `configs`, or a `"."` capability
|
|
726
|
+
root, remain readable and projectable under §2.3–2.4.
|
|
727
|
+
|
|
728
|
+
**The earlier transitional package-root v2** is unsupported input (§4.1): it
|
|
729
|
+
fails clearly rather than being guessed at or converted. Existing local
|
|
730
|
+
pre-adoption state is recreated by reinstalling; there is no product migration
|
|
731
|
+
path, and there is no compatibility subsystem to maintain.
|
|
732
|
+
|
|
733
|
+
## 8. Error taxonomy
|
|
734
|
+
|
|
735
|
+
Stable `error.code` values (also the `--json` envelope codes):
|
|
736
|
+
|
|
737
|
+
| code | Meaning |
|
|
738
|
+
|---|---|
|
|
739
|
+
| `invalid-source` | source spec parses to none of the four grammar forms |
|
|
740
|
+
| `invalid-package-manifest` | `oats-package.json` missing/invalid against §2, or a declared path does not identify the expected resource kind |
|
|
741
|
+
| `path-escape` | a declared path resolves outside its containment root after symlink resolution (package root when staging, capability root when projecting, artifact root at runtime) |
|
|
742
|
+
| `capability-not-self-contained` | a declared capability cannot be materialized as a self-contained artifact (a declared resource is missing, or resolves outside its capability root) |
|
|
743
|
+
| `dependency-cycle` | package dependency graph contains a cycle (provenance: the cycle path) |
|
|
744
|
+
| `duplicate-package-identity` | two sources claim the same package identity at one scope (provenance: both sources) |
|
|
745
|
+
| `duplicate-capability-id` | two packages export the same capability ID at one scope, or one package exports it twice (provenance: both) |
|
|
746
|
+
| `integrity-drift` | staged/installed bytes ≠ locked integrity (package payload or capability artifact), or a trust operation against a drifted artifact |
|
|
747
|
+
| `capability-list-mismatch` | a locked capability's provider package no longer exports it at the locked path |
|
|
748
|
+
| `incompatible-oats` | `compatibility.oats` floor not met by the running kernel |
|
|
749
|
+
| `retired-capability` | a package exports / config references a capability the kernel has retired |
|
|
750
|
+
| `legacy-lock` | operation requires the current lock shape but the scope has v1 — run the explicit migration command |
|
|
751
|
+
| `invalid-lock` | lock violates the semantic invariants of §4, **or** is the unsupported transitional v2 shape of §4.1 — fail closed, no normalization, no auto-repair, no side effects |
|
|
752
|
+
| `unknown-capability` | trust/update/remove/template target is not present in the visible locks |
|
|
753
|
+
| `unknown-config-template` | the package has no config template by that name |
|
|
754
|
+
| `remove-blocked` | removal target is still referenced by config or by a dependent locked package (provenance: the blockers) |
|
|
755
|
+
| `official-mapping-unavailable` | guided official migration cannot map a legacy official capability yet; the scope was left unchanged |
|
|
756
|
+
| `unsafe-config-key` | a YAML document uses `__proto__` as a mapping key, or a command was asked to write a key that cannot be one — `__proto__` (assigning it rewrites the parsed object's prototype instead of becoming data, leaving the entry invisible to every key validator), or a `--settings` key / `--soul` / `--type` name carrying a control character, surrounding whitespace, a leading YAML structure indicator, or the `:`/`#` that end a key token. The reader names the mapping key; whoever holds the path re-raises it naming the file, and the CLI renders it as an ordinary typed failure (one `oats:` line, or one `--json` envelope) — never an uncaught stack |
|
|
757
|
+
| `unsafe-config-value` | a command was asked to WRITE config text that cannot survive as one YAML scalar on one line — a control character (the newline is the one that matters: a crafted `oats use --settings k=v` value injected a whole second capability entry), leading/trailing whitespace a read would strip, or a leading YAML structure indicator. Refused before anything is written; the message names the offending input (`--settings value for "mode"`, `--description`) |
|
|
758
|
+
| `E_NO_CONFIG` | the command needs an `oats-config.yaml` it cannot find. TWO emitters, with different conditions and different remedies — stated separately below, because no single sentence is true of both |
|
|
759
|
+
|
|
760
|
+
`E_NO_CONFIG` is shared by two commands that ask different questions. Both are
|
|
761
|
+
diagnosis only: neither writes anything.
|
|
762
|
+
|
|
763
|
+
| emitter | condition | remedy named |
|
|
764
|
+
|---|---|---|
|
|
765
|
+
| `oats use <capability>` | the capability IS present in this scope's own `installed/`/`owned/` store, **and** there is no `oats-config.yaml` at this scope or at any level above it — the chain is empty, so the chain walk never opens this scope's store and the capability would otherwise be reported as never acquired | `oats init --raw --dir <scope>` — offline, deterministic, writes only the minimal config — then the same `oats use` again |
|
|
766
|
+
| `oats config <diff\|sync\|adopt>` | there is no `oats-config.yaml` **at this exact directory**, whatever the chain above it holds: the three-way template lane compares THIS scope's file against its recorded adopted base, so an outer scope's config is not a substitute and the chain is not consulted | `oats init --package <source> --config <name>` — the adopting form, because this lane needs a config that carries a recorded template base |
|
|
767
|
+
|
|
768
|
+
Neither is `E_NO_ADOPTED_BASE`, the next check in the same lane: there the file
|
|
769
|
+
exists but was not adopted from a config template, so no recorded base exists to
|
|
770
|
+
compare against (remedy: `oats config adopt <package> --config <name>`).
|
|
771
|
+
|
|
772
|
+
Fail-closed enforcement points: `parseLockFileStrict`, `readPackageLocks` and
|
|
773
|
+
`listInstalledPackages` RAISE — consumers never see an invalid lock as absent or
|
|
774
|
+
usable data; the writers validate the complete prospective document before
|
|
775
|
+
writing; restore, trust queries, approval, update/remove/migration planning and
|
|
776
|
+
the template reader all validate before acting. Doctor is the only consumer that
|
|
777
|
+
continues past an invalid lock, and it never uses the invalid data.
|
|
778
|
+
|
|
779
|
+
## 9. Invariants (restated from the Decision — binding)
|
|
780
|
+
|
|
781
|
+
- Acquisition activates nothing, targets nothing and trusts nothing.
|
|
782
|
+
- Every exported capability stays independently addressable, targetable,
|
|
783
|
+
activatable, configurable, excludable and trustable by capability ID;
|
|
784
|
+
capability manifests still cannot carry deployment targets; packages cannot
|
|
785
|
+
make capabilities, family assignments or settings mandatory.
|
|
786
|
+
- Package installation applies **no** config template and creates **no** active
|
|
787
|
+
config. Templates are reported as optional follow-ups.
|
|
788
|
+
- A materialized capability is self-contained: its complete validated local
|
|
789
|
+
production closure and every declared artifact, with all paths and symlinks
|
|
790
|
+
inside the capability root after resolution.
|
|
791
|
+
- Trust binds to capability artifact integrity, never to package identity;
|
|
792
|
+
official catalog identity is not executable approval; any artifact change
|
|
793
|
+
resets `trusted` to false.
|
|
794
|
+
- No silent lock advancement anywhere: bare restore never changes
|
|
795
|
+
source/version/commit/path; only an explicit `oats update <package-id>` may.
|
|
796
|
+
The selected package ROOT advances only where the source owns it: a catalog
|
|
797
|
+
entry supplies `path`, so an update adopts a moved root and reports
|
|
798
|
+
`pathChanged`; a Git spec's `#<path>` is the operator's own selection and
|
|
799
|
+
stays sticky across updates. A path mismatch on acquire therefore names the
|
|
800
|
+
route that can resolve it — `oats update` for catalog sources, `oats remove`
|
|
801
|
+
followed by a re-install with the intended `#<path>` for Git sources (removal
|
|
802
|
+
still refuses while config or dependent packages reference the package).
|
|
803
|
+
- No npm lifecycle scripts, ever; production closure only; platform-invariant
|
|
804
|
+
closures required.
|
|
805
|
+
- Existing config targeting / layer / injection / override / scope-precedence
|
|
806
|
+
semantics are unchanged, including `from: installed`, `from: owned`,
|
|
807
|
+
`from: path:<dir>`, `none` for inherited layers, and injection ejection.
|
|
808
|
+
- `.agents/capabilities/owned/<id>` and `from: path:<dir>` capability development
|
|
809
|
+
are untouched by package paths and keep their existing structural/executable
|
|
810
|
+
trust, targeting, override and composition semantics.
|
|
811
|
+
- Git-backed scopes ignore `installed/` only, ensured transactionally; `owned/`
|
|
812
|
+
and adopted config-template data are never ignored. Non-Git scopes work
|
|
813
|
+
without fake Git state.
|