@awebai/oats 0.25.8 → 0.26.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.
Files changed (185) hide show
  1. package/README.md +8 -6
  2. package/bin/oats.mjs +576 -1714
  3. package/capabilities/oats-authoring/oats-package.json +2 -2
  4. package/capabilities/oats-authoring/oats.json +2 -2
  5. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
  6. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
  7. package/capabilities/oats-aweb/bin/oats-aweb.mjs +334 -72
  8. package/capabilities/oats-aweb/injects/aweb.md +7 -2
  9. package/capabilities/oats-aweb/lib/binding-wire.mjs +107 -5
  10. package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
  11. package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
  12. package/capabilities/oats-aweb/oats.json +14 -4
  13. package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
  14. package/capabilities/oats-jira/oats.json +2 -2
  15. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
  16. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
  17. package/capabilities/oats-linear/oats.json +2 -2
  18. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
  19. package/capabilities/oats-okf/bin/oats-okf.mjs +1 -1
  20. package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
  21. package/capabilities/oats-okf/lib/migration.mjs +2 -2
  22. package/capabilities/oats-okf/lib/sources.mjs +5 -4
  23. package/capabilities/oats-okf/lib/stores.mjs +40 -9
  24. package/capabilities/oats-okf/lib/worker.mjs +3 -3
  25. package/capabilities/oats-okf/oats.json +1 -1
  26. package/capabilities/oats-review/oats.json +3 -2
  27. package/docs/capabilities.md +218 -47
  28. package/docs/capability-manifest.schema.json +13 -4
  29. package/docs/configuration.md +17 -5
  30. package/docs/conventions.md +16 -26
  31. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
  32. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
  33. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
  34. package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
  35. package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
  36. package/docs/design/2026-09-20-redesign-program-board.md +2 -2
  37. package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
  38. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
  39. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +54 -10
  40. package/docs/design/2026-09-24-phase-d-plan.md +77 -0
  41. package/docs/design/2026-09-25-teams-contract.md +226 -0
  42. package/docs/design/README.md +3 -3
  43. package/docs/design/launch-configurations.md +20 -16
  44. package/docs/design/operations-contract.md +27 -10
  45. package/docs/desktop-cli-api.md +546 -264
  46. package/docs/desktop-instance-start.md +1 -1
  47. package/docs/desktop.md +7 -13
  48. package/docs/execution-targets.md +16 -18
  49. package/docs/first-team.md +14 -17
  50. package/docs/implementation.md +28 -59
  51. package/docs/integrations.md +88 -32
  52. package/docs/knowledge-capability-authoring.md +1 -1
  53. package/docs/knowledge-reference/package-craft.md +10 -8
  54. package/docs/knowledge-theory.md +1 -1
  55. package/docs/knowledge.md +10 -11
  56. package/docs/layers.md +16 -17
  57. package/docs/oats-local.schema.json +29 -1
  58. package/docs/oats-membership.schema.json +5 -3
  59. package/docs/oats-package.schema.json +2 -2
  60. package/docs/oats-workspace.schema.json +1 -1
  61. package/docs/{official-marketplace.md → official-catalog.md} +15 -16
  62. package/docs/packages.md +75 -52
  63. package/docs/release-notes/v0.22.0.md +1 -1
  64. package/docs/release-notes/v0.23.1.md +1 -1
  65. package/docs/release-notes/v0.25.9.md +23 -0
  66. package/docs/release-notes/v0.26.0.md +670 -0
  67. package/docs/schedules.md +48 -126
  68. package/docs/soul.schema.json +11 -4
  69. package/docs/souls-and-instances.md +56 -43
  70. package/docs/workspaces.md +80 -58
  71. package/injects/instance-boundary.md +1 -1
  72. package/injects/work-attached.md +1 -1
  73. package/injects/work-workspace.md +2 -2
  74. package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
  75. package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
  76. package/lib/capability-contract.mjs +110 -0
  77. package/lib/config-data.mjs +2 -2
  78. package/lib/core.mjs +700 -4824
  79. package/lib/digest.mjs +12 -0
  80. package/lib/instance-inspect.mjs +396 -0
  81. package/lib/instance-lifecycle.mjs +3 -4
  82. package/lib/instance-resolution.mjs +212 -26
  83. package/lib/instruction-composition.mjs +0 -20
  84. package/lib/materialize.mjs +6 -4
  85. package/lib/operator-dispatch.mjs +33 -13
  86. package/lib/packages.mjs +25 -190
  87. package/lib/provider-binding.mjs +4 -2
  88. package/lib/provider-reasons.mjs +3 -68
  89. package/lib/resolve.mjs +204 -68
  90. package/lib/schedule.mjs +97 -272
  91. package/lib/servers.mjs +13 -13
  92. package/lib/{portable-shape.mjs → shape.mjs} +4 -3
  93. package/lib/tree-copy.mjs +44 -0
  94. package/lib/workspace.mjs +125 -20
  95. package/package-catalog.json +7 -7
  96. package/package.json +1 -1
  97. package/skills/integration-authoring/SKILL.md +48 -40
  98. package/skills/oats-getting-started/SKILL.md +105 -110
  99. package/skills/oats-support/SKILL.md +2 -2
  100. package/skills/soul-craft/SKILL.md +13 -6
  101. package/bin/oats-pi-sdk-host.mjs +0 -17
  102. package/docs/2026-09-03-architecture-proposal.md +0 -642
  103. package/docs/artifact-approvals.schema.json +0 -7
  104. package/docs/captured-invocation-context.schema.json +0 -7
  105. package/docs/captured-resolution.schema.json +0 -7
  106. package/docs/design/package-engine-contract.md +0 -813
  107. package/docs/design/package-runtime-api.md +0 -588
  108. package/docs/desktop-succession.md +0 -57
  109. package/docs/execution-capsule.schema.json +0 -108
  110. package/docs/first-team-demo.md +0 -92
  111. package/docs/knowledge-migration.md +0 -147
  112. package/docs/migration-from-oas.md +0 -103
  113. package/docs/oats-config.schema.json +0 -172
  114. package/docs/oats-lock-v3.schema.json +0 -7
  115. package/docs/oats-lock.schema.json +0 -175
  116. package/docs/operating-team-migration.md +0 -470
  117. package/docs/portable.schema.json +0 -2512
  118. package/docs/provider-check-input.schema.json +0 -7
  119. package/docs/rebuild-to-v2.md +0 -511
  120. package/docs/workspace-adoption.md +0 -74
  121. package/injects/framework-workspace.md +0 -7
  122. package/injects/local-soul.md +0 -19
  123. package/injects/oats-portable.md +0 -20
  124. package/injects/oats.md +0 -11
  125. package/injects/portable-instance-boundary.md +0 -39
  126. package/injects/portable-work-directory.md +0 -29
  127. package/lib/artifact-approvals.mjs +0 -120
  128. package/lib/artifact-tree.mjs +0 -141
  129. package/lib/capability-artifacts.mjs +0 -179
  130. package/lib/capability-execution.mjs +0 -15
  131. package/lib/capability-inputs.mjs +0 -39
  132. package/lib/capability-provenance.mjs +0 -231
  133. package/lib/captured-action-shape.mjs +0 -21
  134. package/lib/captured-admission-shape.mjs +0 -20
  135. package/lib/captured-binding-file.mjs +0 -36
  136. package/lib/captured-dispatch.mjs +0 -66
  137. package/lib/captured-instance-index.mjs +0 -277
  138. package/lib/captured-invocation-context.mjs +0 -130
  139. package/lib/captured-launch-request.mjs +0 -66
  140. package/lib/captured-operation-process.mjs +0 -15
  141. package/lib/captured-pi-custody.mjs +0 -29
  142. package/lib/captured-pi-host.mjs +0 -167
  143. package/lib/captured-pi-outcome.mjs +0 -172
  144. package/lib/captured-resolutions.mjs +0 -275
  145. package/lib/captured-scaffold.mjs +0 -87
  146. package/lib/captured-selector.mjs +0 -28
  147. package/lib/captured-session-backend.mjs +0 -52
  148. package/lib/captured-source-receipt-file.mjs +0 -72
  149. package/lib/helper-injection-policy.mjs +0 -104
  150. package/lib/legacy-lock-codec.mjs +0 -106
  151. package/lib/manifest-settings.mjs +0 -84
  152. package/lib/package-closure.mjs +0 -48
  153. package/lib/package-materialization.mjs +0 -83
  154. package/lib/pi-sdk-host.mjs +0 -229
  155. package/lib/portable-artifacts.mjs +0 -115
  156. package/lib/portable-choices.mjs +0 -82
  157. package/lib/portable-composition.mjs +0 -136
  158. package/lib/portable-digest.mjs +0 -105
  159. package/lib/portable-identity.mjs +0 -40
  160. package/lib/portable-lock.mjs +0 -117
  161. package/lib/portable-onboarding-request.mjs +0 -49
  162. package/lib/portable-onboarding.mjs +0 -256
  163. package/lib/portable-package-preparation.mjs +0 -188
  164. package/lib/portable-policy.mjs +0 -44
  165. package/lib/portable-soul.mjs +0 -42
  166. package/lib/portable-state.mjs +0 -80
  167. package/lib/prepare-composition.mjs +0 -170
  168. package/lib/prepared-bindings.mjs +0 -92
  169. package/lib/prepared-resources.mjs +0 -127
  170. package/lib/provider-binding-broker.mjs +0 -65
  171. package/lib/provider-binding-wire.mjs +0 -116
  172. package/lib/readiness.mjs +0 -225
  173. package/lib/repository-observation.mjs +0 -226
  174. package/lib/resolution-shape.mjs +0 -393
  175. package/lib/schedule-capsule.mjs +0 -206
  176. package/lib/soul-constraints.mjs +0 -40
  177. package/lib/source-projection.mjs +0 -84
  178. package/lib/source-spec.mjs +0 -189
  179. package/lib/workspace-definition.mjs +0 -126
  180. package/lib/workspace-discovery.mjs +0 -146
  181. package/skills/oats/SKILL.md +0 -162
  182. package/skills/oats-config/SKILL.md +0 -164
  183. package/skills/oats-packages/SKILL.md +0 -184
  184. package/skills/oats-portable/SKILL.md +0 -115
  185. package/skills/oats-portable-artifacts/SKILL.md +0 -63
package/lib/packages.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OATS packages — versions, lock v3, approval (workspace model v2).
2
+ * OATS packages — versions, lock v3 (workspace model v2).
3
3
  *
4
4
  * Contract: docs/design/2026-09-23-workspace-module-contracts.md §4.
5
5
  * Decision: agents/oats-expert/soul/knowledge/decisions/workspace-model-v2.md.
@@ -7,9 +7,10 @@
7
7
  * A package is a place to fetch from WITH a version attached. Nothing is
8
8
  * installed: `resolvePackages` turns each `workspace.packages` entry into an
9
9
  * exact (commit, integrity) pair recorded in `oats-lock.json`
10
- * (lockfileVersion 3), and the one-time executable approval per version lives
11
- * next to the commit it approved. A moved tag (same version string, different
12
- * commit) fails integrity and asks again.
10
+ * (lockfileVersion 3). There is no approval step (human decision 2026-09-24):
11
+ * declaring a package in the workspace's `packages:` IS the trust decision. The
12
+ * lock is reproducibility — a moved tag (same version string, different commit)
13
+ * or drifted content fails integrity (E_PACKAGE_INTEGRITY).
13
14
  *
14
15
  * This module is synchronous except `resolvePackages`, shells out to nothing,
15
16
  * and depends only on `node:*`. Remote access is INJECTED (`remote` option) so
@@ -27,27 +28,13 @@
27
28
  * version: "<version string, no leading v>",
28
29
  * commit: "<full 40-hex OID>",
29
30
  * integrity: "sha256-<hex>", // contentDigest of the package tree at <path>
30
- * capabilities: ["<cap name>", …], // sorted
31
- * approved: { executables: "sha256-<hex>", at: "<ISO-8601 UTC>" } | null
31
+ * capabilities: ["<cap name>", …] // sorted
32
32
  * }
33
33
  * }
34
34
  * }
35
35
  *
36
- * `packageTree` (input of `executablesDigest`):
37
- *
38
- * { manifests: [ { name: "<cap name>", manifest: <parsed oats.json>, files: Map<relpath, Buffer> } ] }
39
- *
40
- * `files` holds the bytes of the capability directory, keyed by POSIX
41
- * relative path from that directory (e.g. "bin/tool.mjs"). Each
42
- * `manifest.commands[<cmd>]` value is "<relpath> [args…]"; the FIRST token is
43
- * the executable target whose bytes are digested. Only command targets are
44
- * digested — skills, injects and other files are covered by `integrity`.
45
- *
46
- * `executablesDigestAt(remote, ref, commit, path, capabilities?)` is the ONE shared
47
- * computation of that digest over a remote tree at a commit: `oats sync` approves
48
- * what it returns, `resolveSoul` requires equality with `approved.executables` at
49
- * spawn (E_PACKAGE_UNAPPROVED reason "digest-mismatch"). An edited lock — same
50
- * id/version, different commit, copied approval — can therefore never materialize.
36
+ * A lock written before 0.26.0 may still carry an `approved` record per entry:
37
+ * it is read with the field ignored, and the next write drops it.
51
38
  *
52
39
  * Deprecated shims: every name the 0.24 kernel still imports from this module
53
40
  * is exported below as a thin function throwing E_REMOVED so nothing breaks at
@@ -56,9 +43,9 @@
56
43
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, lstatSync } from "node:fs";
57
44
  import { tmpdir } from "node:os";
58
45
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
59
- import { createHash } from "node:crypto";
60
46
  import { oatsError as baseOatsError } from "./errors.mjs";
61
47
  import * as defaultRemote from "./remote.mjs";
48
+ import { manifestContractProblems } from "./capability-contract.mjs";
62
49
 
63
50
  export const LOCK_FILE = "oats-lock.json";
64
51
  export const LOCK_VERSION = 3;
@@ -156,11 +143,6 @@ export function validateLock(lock, { file } = {}) {
156
143
  if (typeof entry.commit !== "string" || !OID_RE.test(entry.commit)) throw bad("commit", "must be a full 40-hex OID");
157
144
  if (typeof entry.integrity !== "string" || !DIGEST_RE.test(entry.integrity)) throw bad("integrity", "must be sha256-<hex>");
158
145
  if (!Array.isArray(entry.capabilities) || entry.capabilities.some((c) => typeof c !== "string" || !c)) throw bad("capabilities", "must be an array of capability names");
159
- if (entry.approved !== null) {
160
- if (!plainObject(entry.approved)) throw bad("approved", "must be null or { executables, at }");
161
- if (typeof entry.approved.executables !== "string" || !DIGEST_RE.test(entry.approved.executables)) throw bad("approved/executables", "must be sha256-<hex>");
162
- if (typeof entry.approved.at !== "string" || Number.isNaN(Date.parse(entry.approved.at))) throw bad("approved/at", "must be an ISO-8601 timestamp");
163
- }
164
146
  }
165
147
  return lock;
166
148
  }
@@ -186,7 +168,6 @@ export function canonicalLock(lock) {
186
168
  packages[id] = {
187
169
  source: e.source, ...(typeof e.url === "string" && e.url ? { url: e.url } : {}), path: e.path, version: e.version, commit: e.commit, integrity: e.integrity,
188
170
  capabilities: [...e.capabilities].sort(),
189
- approved: e.approved ? { executables: e.approved.executables, at: e.approved.at } : null,
190
171
  };
191
172
  }
192
173
  return { lockfileVersion: LOCK_VERSION, packages };
@@ -332,6 +313,8 @@ export async function readPackageManifests(remote, remoteRef, commit, path, deta
332
313
  if (seen.has(capManifest.capability)) {
333
314
  throw oatsError("E_PACKAGE_MANIFEST", `package ${manifest.package} declares capability ${JSON.stringify(capManifest.capability)} twice (${seen.get(capManifest.capability)} and ${dir})`, { ...details, path: dir, duplicate: capManifest.capability, other: seen.get(capManifest.capability) });
334
315
  }
316
+ const [problem] = manifestContractProblems(capManifest);
317
+ if (problem) throw oatsError("E_PACKAGE_MANIFEST", `${pjoin(dir, CAPABILITY_MANIFEST)}#${problem.pointer}: ${problem.message}`, { ...details, path: dir, capability: capManifest.capability, pointer: problem.pointer });
335
318
  seen.set(capManifest.capability, dir);
336
319
  capabilities.push({ name: capManifest.capability, dir, manifest: capManifest });
337
320
  }
@@ -354,83 +337,6 @@ async function packageIntegrity(remote, remoteRef, commit, path) {
354
337
  } finally { rmSync(scratch, { recursive: true, force: true }); }
355
338
  }
356
339
 
357
- /** Depth bound for reading a capability directory: deep enough for any real layout. */
358
- export const PACKAGE_TREE_DEPTH = 64;
359
-
360
- /** Build a `packageTree` (see module header) for `executablesDigest` from the remote.
361
- * Reads every blob under each capability directory. */
362
- export async function readPackageTree(remote, remoteRef, commit, path, { depth = PACKAGE_TREE_DEPTH, remoteOptions } = {}) {
363
- remote = bindRemote(remote, remoteOptions);
364
- const { capabilities } = await readPackageManifests(remote, remoteRef, commit, path);
365
- const manifests = [];
366
- for (const cap of capabilities) {
367
- const listing = await remote.listRemoteTree(remoteRef, commit, cap.dir, { depth });
368
- const files = new Map();
369
- for (const item of listing) {
370
- if (item.type !== "blob") continue;
371
- const { bytes } = await remote.readRemoteFile(remoteRef, commit, pjoin(cap.dir, item.path));
372
- files.set(item.path, Buffer.from(bytes));
373
- }
374
- manifests.push({ name: cap.name, manifest: cap.manifest, files });
375
- }
376
- return { manifests };
377
- }
378
-
379
- /**
380
- * The executables digest of a locked package AT A COMMIT, read over the remote — the ONE definition
381
- * `oats sync` (approval) and `resolveSoul` (the gate at spawn) share, so what was approved is exactly what
382
- * is checked. `remote` is a contract-§1 remote (default lib/remote.mjs; `remoteOptions` bound here).
383
- *
384
- * executablesDigestAt(remote, ref, commit, path, capabilities?, { remoteOptions } = {})
385
- * → { digest: "sha256-…", executables: [{ capability, kind, name, target }], capabilities: [<names>] } (frozen)
386
- *
387
- * `capabilities` (optional, the lock entry's list) is checked against what `<path>/oats-package.json`
388
- * declares at `commit`: a mismatch is E_PACKAGE_INTEGRITY { why: "capabilities", listed, locked } — a lock
389
- * whose capability list drifted from the tree it names is not describing that tree.
390
- *
391
- * Cached per process by (remote identity, repo key, commit, path): a commit is content-addressed, so the
392
- * tree — and its digest — can never change under the same OID. The cache is keyed on the remote OBJECT
393
- * (WeakMap): a test's fake remote gets its own cache and never sees another test's tree; the kernel's
394
- * default remote keeps one cache for the process (sync → many spawns of the same lock).
395
- */
396
- const digestCache = new WeakMap();
397
- export async function executablesDigestAt(remote, ref, commit, path, capabilities = null, { remoteOptions } = {}) {
398
- if (!remote || typeof remote.readRemoteFile !== "function" || typeof remote.listRemoteTree !== "function") {
399
- throw new TypeError("executablesDigestAt: remote must provide readRemoteFile()/listRemoteTree() (module contract §1)");
400
- }
401
- if (typeof commit !== "string" || !OID_RE.test(commit)) throw oatsError("E_PACKAGE_INTEGRITY", `executablesDigestAt: commit must be a full 40-hex OID, got ${JSON.stringify(commit)}`, { commit, path });
402
- if (capabilities !== null && (!Array.isArray(capabilities) || capabilities.some((c) => typeof c !== "string"))) throw new TypeError("executablesDigestAt: capabilities must be an array of names or null");
403
- const parse = typeof remote.parseRepoRef === "function" ? remote.parseRepoRef : defaultRemote.parseRepoRef;
404
- let repoKey; try { repoKey = parse(ref).key; } catch { repoKey = String(ref); }
405
- const cacheKey = `${repoKey}\0${commit}\0${path}`;
406
- let perRemote = digestCache.get(remote);
407
- if (!perRemote) { perRemote = new Map(); digestCache.set(remote, perRemote); }
408
- let pending = perRemote.get(cacheKey);
409
- if (!pending) {
410
- pending = (async () => {
411
- const tree = await readPackageTree(remote, ref, commit, path, { remoteOptions });
412
- const digest = executablesDigest(tree);
413
- const executables = [];
414
- for (const m of [...tree.manifests].sort((a, b) => byCodepoint(String(a.name), String(b.name)))) {
415
- for (const x of manifestExecutables(m.manifest)) executables.push({ capability: m.name, kind: x.kind, name: x.name, target: x.target });
416
- }
417
- return Object.freeze({ digest, executables: Object.freeze(executables.map((x) => Object.freeze(x))), capabilities: Object.freeze(tree.manifests.map((m) => m.name).sort(byCodepoint)) });
418
- })();
419
- perRemote.set(cacheKey, pending);
420
- // A failed read is not cached: the next caller retries (a transient remote failure must not pin an error).
421
- pending.catch(() => { if (perRemote.get(cacheKey) === pending) perRemote.delete(cacheKey); });
422
- }
423
- const result = await pending;
424
- if (capabilities !== null) {
425
- const locked = [...capabilities].sort(byCodepoint);
426
- const listed = result.capabilities;
427
- if (locked.length !== listed.length || locked.some((c, i) => c !== listed[i])) {
428
- throw oatsError("E_PACKAGE_INTEGRITY", `the lock lists capabilities [${locked.join(", ")}] for ${path} at ${commit.slice(0, 12)}, but the package there declares [${listed.join(", ")}]`, { why: "capabilities", commit, path, listed: [...listed], locked });
429
- }
430
- }
431
- return result;
432
- }
433
-
434
340
  // ---------- resolvePackages ----------
435
341
 
436
342
  function assertDigest(what, value, details) {
@@ -463,14 +369,11 @@ export function bindRemote(remote, remoteOptions) {
463
369
  * lib/remote.mjs; tests pass an in-memory fake. options.remoteOptions (cacheDir, exec, …)
464
370
  * is threaded into every remote call.
465
371
  *
466
- * → { lock, changes: [{ id, from, to, commit, approvalNeeded }] }
372
+ * → { lock, changes: [{ id, from, to, commit }] }
467
373
  * - `from` is the previously locked version or null; `to` is the resolved version
468
374
  * (null when the package was removed from the workspace and dropped from the lock).
469
- * - `approvalNeeded` = the entry's `approved` is null.
470
375
  * - A locked entry whose version string is unchanged but whose commit or
471
376
  * integrity moved → E_PACKAGE_INTEGRITY { id, version, locked, observed }.
472
- * - Unchanged entries keep their approval ONLY when the recorded executables digest still
473
- * matches the tree (else E_PACKAGE_UNAPPROVED); a new version starts unapproved.
474
377
  * - A value resolving to a BRANCH → E_PACKAGE_INTEGRITY { why: "branch" }: versions are immutable.
475
378
  */
476
379
  export async function resolvePackages(workspace, { catalog = {}, lock = emptyLock(), remote = defaultRemote, remoteOptions } = {}) {
@@ -513,16 +416,18 @@ export async function resolvePackages(workspace, { catalog = {}, lock = emptyLoc
513
416
  `packages.${id} ${version} @ ${obs.commit}: content digest ${integrity} does not match the locked ${old.integrity}`,
514
417
  { ...details, version, locked: { commit: old.commit, integrity: old.integrity }, observed: { commit: obs.commit, integrity } });
515
418
  }
516
- // A recorded approval must describe THESE executables, not merely any well-formed digest.
517
- if (old.approved) {
518
- const { digest: executables } = await executablesDigestAt(remote, req.remoteRef, obs.commit, old.path);
519
- if (executables !== old.approved.executables) {
520
- throw oatsError("E_PACKAGE_UNAPPROVED",
521
- `packages.${id} ${version}: the recorded approval ${old.approved.executables} does not match the package's executables ${executables} — approve again`,
522
- { ...details, version, commit: obs.commit, approved: old.approved, executables });
523
- }
419
+ // The lock's capability list must still be what the package declares at that commit —
420
+ // spawn refuses a mismatch (E_PACKAGE_INTEGRITY why:capabilities), so sync must too, or
421
+ // a bad list would pass every sync and fail every spawn.
422
+ const { capabilities } = await readPackageManifests(remote, req.remoteRef, obs.commit, old.path, details);
423
+ const listed = capabilities.map((c) => c.name).sort(), locked = [...old.capabilities].sort();
424
+ if (listed.length !== locked.length || listed.some((c, i) => c !== locked[i])) {
425
+ throw oatsError("E_PACKAGE_INTEGRITY",
426
+ `packages.${id} ${version} @ ${obs.commit}: the lock lists capabilities [${locked.join(", ")}] but the package declares [${listed.join(", ")}] — the lock was edited; remove this entry from oats-lock.json and run \`oats sync\` again`,
427
+ { ...details, version, why: "capabilities", listed, locked });
524
428
  }
525
- nextPackages[id] = clone(old);
429
+ const { approved: _dropped, ...kept } = clone(old); // a pre-0.26 approval record is dropped on write
430
+ nextPackages[id] = kept;
526
431
  continue;
527
432
  }
528
433
 
@@ -531,87 +436,17 @@ export async function resolvePackages(workspace, { catalog = {}, lock = emptyLoc
531
436
  nextPackages[id] = {
532
437
  source, url: obs.url, path: req.path, version, commit: obs.commit, integrity,
533
438
  capabilities: capabilities.map((c) => c.name).sort(),
534
- approved: null,
535
439
  };
536
- changes.push({ id, from: old ? old.version : null, to: version, commit: obs.commit, approvalNeeded: true });
440
+ changes.push({ id, from: old ? old.version : null, to: version, commit: obs.commit });
537
441
  }
538
442
 
539
443
  for (const id of Object.keys(previous.packages).sort()) {
540
- if (!(id in requests)) changes.push({ id, from: previous.packages[id].version, to: null, commit: null, approvalNeeded: false });
444
+ if (!(id in requests)) changes.push({ id, from: previous.packages[id].version, to: null, commit: null });
541
445
  }
542
446
 
543
447
  return { lock: { lockfileVersion: LOCK_VERSION, packages: sortedObject(nextPackages) }, changes };
544
448
  }
545
449
 
546
- // ---------- executables digest + approval ----------
547
-
548
- /** First token of a `commands` value is the executable target (e.g. "bin/x.mjs cut" → "bin/x.mjs"). */
549
- export function commandTarget(specification) {
550
- return String(specification).trim().split(/\s+/)[0] || "";
551
- }
552
-
553
- /** Every executable a manifest can make the kernel run: `commands.*` targets AND `hooks.*.command`
554
- * targets (hooks run automatically at spawn/retire — the executables an approver most needs to see).
555
- * → [{ kind: "command"|"hook", name, target }] in canonical order. */
556
- export function manifestExecutables(manifest) {
557
- const out = [];
558
- const commands = plainObject(manifest?.commands) ? manifest.commands : {};
559
- for (const cmd of Object.keys(commands).sort(byCodepoint)) out.push({ kind: "command", name: cmd, target: commandTarget(commands[cmd]), spec: commands[cmd] });
560
- const hooks = plainObject(manifest?.hooks) ? manifest.hooks : {};
561
- for (const hook of Object.keys(hooks).sort(byCodepoint)) {
562
- // A hook is { command, ... } (schema: required command) or a bare string. A hook object WITHOUT
563
- // `command` is malformed — it enters the list with spec undefined so executablesDigest refuses it
564
- // (E_PACKAGE_MANIFEST), never an invisible no-op an approver does not see.
565
- const spec = plainObject(hooks[hook]) ? hooks[hook].command : hooks[hook];
566
- out.push({ kind: "hook", name: hook, target: spec === undefined || spec === null ? "" : commandTarget(spec), spec });
567
- }
568
- return out;
569
- }
570
-
571
- /**
572
- * sha256 over every capability manifest's executables' bytes — `commands` targets and
573
- * `hooks.*.command` targets — in canonical codepoint order (manifest name, kind, entry name).
574
- * Locale-independent: the same tree digests identically on every machine.
575
- * Input is a `packageTree` (module header). A target missing from `files` →
576
- * E_PACKAGE_MANIFEST { capability, command, target }. Empty set → the digest of nothing.
577
- */
578
- export function executablesDigest(packageTree) {
579
- if (!plainObject(packageTree) || !Array.isArray(packageTree.manifests)) {
580
- throw oatsError("E_PACKAGE_MANIFEST", "executablesDigest expects { manifests: [{ name, manifest, files }] }", { path: "/manifests" });
581
- }
582
- const hash = createHash("sha256");
583
- const manifests = [...packageTree.manifests].sort((a, b) => byCodepoint(String(a.name), String(b.name)));
584
- for (const { name, manifest, files } of manifests) {
585
- for (const { kind, name: entry, target, spec } of manifestExecutables(manifest)) {
586
- const label = kind === "hook" ? `hooks.${entry}.command` : `commands.${entry}`;
587
- const details = { capability: name, command: entry, kind, target };
588
- if (typeof spec !== "string") throw oatsError("E_PACKAGE_MANIFEST", `${name}: ${label} must be a string, got ${typeof spec}`, details);
589
- assertRelPath(`${name} ${label}`, target, details);
590
- const bytes = files instanceof Map ? files.get(target) : (plainObject(files) && Object.hasOwn(files, target) ? files[target] : undefined);
591
- if (bytes === undefined) throw oatsError("E_PACKAGE_MANIFEST", `${name}: ${label} targets ${target}, which is not in the package`, details);
592
- const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
593
- // Hooks enter the framing under "hooks.<name>" so a hook and a command of the same name never collide.
594
- hash.update(`${name}\0${kind === "hook" ? `hooks.${entry}` : entry}\0${target}\0${buf.length}\0`);
595
- hash.update(buf);
596
- hash.update("\0");
597
- }
598
- }
599
- return `sha256-${hash.digest("hex")}`;
600
- }
601
-
602
- /** Record the executable approval for package `id`. Returns a NEW lock; the input is untouched. */
603
- export function approve(lock, id, digest, at = new Date().toISOString()) {
604
- validateLock(lock);
605
- const entry = lock.packages[id];
606
- if (!entry) throw oatsError("E_PACKAGE_MISSING", `cannot approve ${JSON.stringify(id)}: not in the lock — run \`oats sync\` first`, { id });
607
- if (typeof digest !== "string" || !DIGEST_RE.test(digest)) throw oatsError("E_PACKAGE_INTEGRITY", `approval digest for ${id} must be sha256-<hex>`, { id, digest });
608
- const ms = Date.parse(at);
609
- if (Number.isNaN(ms)) throw oatsError("E_LOCK_SCHEMA", `approval timestamp for ${id} is not ISO-8601: ${JSON.stringify(at)}`, { id, at });
610
- const next = clone(lock);
611
- next.packages[id] = { ...next.packages[id], approved: { executables: digest, at: new Date(ms).toISOString() } };
612
- return next;
613
- }
614
-
615
450
  /** Which locked package provides capability `capName`? → { id, entry } | null.
616
451
  * Two packages providing the same name is ambiguous and fails closed:
617
452
  * E_PACKAGE_MISSING { capability, ambiguous: [ids] } — never a silent first-by-id pick. */
@@ -1,10 +1,12 @@
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, stringSetAt, versionAt } from './portable-shape.mjs';
4
- import { FUNDAMENTAL_SLOTS } from './portable-policy.mjs';
3
+ import { objectAt, stringAt, stringSetAt, versionAt } from './shape.mjs';
5
4
  import { oatsError } from './errors.mjs';
6
5
  import { validateBindingReasons } from './provider-reasons.mjs';
7
6
 
7
+ /** The layers a binding codec may belong to (the kernel's LAYERS). */
8
+ const FUNDAMENTAL_SLOTS = Object.freeze(['knowledge', 'messaging', 'tasks']);
9
+
8
10
  export const BINDING_PHASES = Object.freeze(['normalize', 'bind', 'check']);
9
11
  export function validateBindingInterface(manifest) {
10
12
  if (manifest.binding === undefined) return null;
@@ -1,60 +1,6 @@
1
- /** Fixed provider-declared diagnostics, never a free-text transport. Values
2
- * come from the verified selected manifest or reviewed kernel compatibility
3
- * data, not from a codec response, operator input or today's configuration. */
4
- import { invalidShape, stringAt, stringSetAt } from './portable-shape.mjs';
5
-
6
- // Compatibility literals for manifests predating binding.reasons. These are
7
- // data, never provider-name-dependent interpretation of payloads or settings.
8
- // aweb: awebai/oats-aweb v1.11.0, 862f156c883ab39c69c9e83cdf3bab86be882867.
9
- // oats-package/capabilities/oats-aweb/lib/{binding-wire,session-readiness}.mjs:
10
- // complete safeReasons/fallback/overflow/check vocabulary (no hook warnings).
11
- // OKF: awebai/oats-okf PR5, 0517a70a7158ebdf14ccb6e880b437c3d43cb1db,
12
- // same capability-relative file, settingMessages (UNRELEASED 2.1.2 at selection).
13
- // Released OKF 2.1.1 sends code-only; this table invents no reason for it.
14
- const bundled = Object.freeze({
15
- 'oats.aweb': Object.freeze([
16
- 'messaging-enabled standalone preparation needs an explicit context key',
17
- 'messaging binding needs one soul declaration',
18
- 'messaging workspace must declare private: per-human',
19
- 'an explicit responsible-human binding is required',
20
- 'an explicit wider-membership consent list is required',
21
- 'a selected wider-team binding is required',
22
- 'a selected wider alias needs an explicit workspace team mapping',
23
- 'multiple soul messaging declarations',
24
- 'adoption team aliases have conflicting mappings',
25
- 'messaging settings and explicit binding selections are required',
26
- 'messaging declarations contain incompatible requirements',
27
- 'messaging input must match the supported binding contract',
28
- 'explicit native messaging authorization is required',
29
- 'a required native messaging host resource is unavailable',
30
- 'the selected messaging provider is unavailable',
31
- 'the requested messaging configuration is not qualified',
32
- 'messaging response exceeds the supported wire limits',
33
- 'an admitted captured instance intent is required for execution',
34
- 'an explicit private-team binding is required',
35
- 'selected binding and inline captured invocation are required',
36
- 'an explicit captured instance home is required',
37
- 'captured messaging requires explicit delivery: session',
38
- 'selected wider memberships need their explicitly qualified native setup; they were not omitted',
39
- 'caller-owned OATS_CLI_BIN and readable kernel version are required',
40
- 'oats >=0.24.2 is required for captured HOME custody and retained runtime inspection',
41
- 'the exact retained runtime profile must be readable',
42
- 'the kernel must report the exact retained resolution',
43
- 'a retained launchSelection runtime/model observation is required',
44
- 'Pi strict print does not support session input; retain messaging and configure an input-capable profile',
45
- 'a supported input-capable ordinary Claude/Codex profile is required',
46
- ]),
47
- 'oats.okf': Object.freeze([
48
- 'setting bindings-file is required (absolute host path)',
49
- 'setting bindings-file must be a normalized absolute host path',
50
- 'setting state-dir is required (absolute host path)',
51
- 'setting state-dir must be a normalized absolute host path',
52
- 'setting harvest-runtime is required (pi, claude or codex)',
53
- 'setting harvest-runtime must be pi, claude or codex',
54
- 'setting harvest-model must be null or a non-empty string',
55
- ]),
56
- });
57
- const none = Object.freeze([]);
1
+ /** Fixed provider-declared diagnostics (a manifest's binding.reasons), never a
2
+ * free-text transport: their shape and bounds. */
3
+ import { invalidShape, stringAt, stringSetAt } from './shape.mjs';
58
4
 
59
5
  export function validateBindingReasons(reasons, { allowEmpty = false } = {}) {
60
6
  stringSetAt(reasons, '/binding/reasons', (reason, pointer) => {
@@ -64,14 +10,3 @@ export function validateBindingReasons(reasons, { allowEmpty = false } = {}) {
64
10
  if (reasons.length > 64 || (!allowEmpty && reasons.length === 0)) invalidShape('/binding/reasons', 'expected 1 to 64 fixed reasons');
65
11
  return reasons;
66
12
  }
67
-
68
- export function providerReasons(manifest) {
69
- if (Object.hasOwn(manifest.binding ?? {}, 'reasons')) return validateBindingReasons(manifest.binding.reasons);
70
- return Object.hasOwn(bundled, manifest.capability) ? bundled[manifest.capability] : none;
71
- }
72
-
73
- /** Exact Unicode scalar text is exact UTF-8 text; never trim, normalize,
74
- * interpolate or accept a prefix/substring. Absence preserves template fallback. */
75
- export function safeProviderReason(message, reasons) {
76
- return typeof message === 'string' && reasons.includes(message) ? message : undefined;
77
- }