@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.
- package/README.md +8 -6
- package/bin/oats.mjs +576 -1714
- package/capabilities/oats-authoring/oats-package.json +2 -2
- package/capabilities/oats-authoring/oats.json +2 -2
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +334 -72
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +107 -5
- package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
- package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
- package/capabilities/oats-aweb/oats.json +14 -4
- package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
- package/capabilities/oats-jira/oats.json +2 -2
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
- package/capabilities/oats-linear/oats.json +2 -2
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +1 -1
- package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
- package/capabilities/oats-okf/lib/migration.mjs +2 -2
- package/capabilities/oats-okf/lib/sources.mjs +5 -4
- package/capabilities/oats-okf/lib/stores.mjs +40 -9
- package/capabilities/oats-okf/lib/worker.mjs +3 -3
- package/capabilities/oats-okf/oats.json +1 -1
- package/capabilities/oats-review/oats.json +3 -2
- package/docs/capabilities.md +218 -47
- package/docs/capability-manifest.schema.json +13 -4
- package/docs/configuration.md +17 -5
- package/docs/conventions.md +16 -26
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
- package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
- package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
- package/docs/design/2026-09-20-redesign-program-board.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +54 -10
- package/docs/design/2026-09-24-phase-d-plan.md +77 -0
- package/docs/design/2026-09-25-teams-contract.md +226 -0
- package/docs/design/README.md +3 -3
- package/docs/design/launch-configurations.md +20 -16
- package/docs/design/operations-contract.md +27 -10
- package/docs/desktop-cli-api.md +546 -264
- package/docs/desktop-instance-start.md +1 -1
- package/docs/desktop.md +7 -13
- package/docs/execution-targets.md +16 -18
- package/docs/first-team.md +14 -17
- package/docs/implementation.md +28 -59
- package/docs/integrations.md +88 -32
- package/docs/knowledge-capability-authoring.md +1 -1
- package/docs/knowledge-reference/package-craft.md +10 -8
- package/docs/knowledge-theory.md +1 -1
- package/docs/knowledge.md +10 -11
- package/docs/layers.md +16 -17
- package/docs/oats-local.schema.json +29 -1
- package/docs/oats-membership.schema.json +5 -3
- package/docs/oats-package.schema.json +2 -2
- package/docs/oats-workspace.schema.json +1 -1
- package/docs/{official-marketplace.md → official-catalog.md} +15 -16
- package/docs/packages.md +75 -52
- package/docs/release-notes/v0.22.0.md +1 -1
- package/docs/release-notes/v0.23.1.md +1 -1
- package/docs/release-notes/v0.25.9.md +23 -0
- package/docs/release-notes/v0.26.0.md +670 -0
- package/docs/schedules.md +48 -126
- package/docs/soul.schema.json +11 -4
- package/docs/souls-and-instances.md +56 -43
- package/docs/workspaces.md +80 -58
- package/injects/instance-boundary.md +1 -1
- package/injects/work-attached.md +1 -1
- package/injects/work-workspace.md +2 -2
- package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
- package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
- package/lib/capability-contract.mjs +110 -0
- package/lib/config-data.mjs +2 -2
- package/lib/core.mjs +700 -4824
- package/lib/digest.mjs +12 -0
- package/lib/instance-inspect.mjs +396 -0
- package/lib/instance-lifecycle.mjs +3 -4
- package/lib/instance-resolution.mjs +212 -26
- package/lib/instruction-composition.mjs +0 -20
- package/lib/materialize.mjs +6 -4
- package/lib/operator-dispatch.mjs +33 -13
- package/lib/packages.mjs +25 -190
- package/lib/provider-binding.mjs +4 -2
- package/lib/provider-reasons.mjs +3 -68
- package/lib/resolve.mjs +204 -68
- package/lib/schedule.mjs +97 -272
- package/lib/servers.mjs +13 -13
- package/lib/{portable-shape.mjs → shape.mjs} +4 -3
- package/lib/tree-copy.mjs +44 -0
- package/lib/workspace.mjs +125 -20
- package/package-catalog.json +7 -7
- package/package.json +1 -1
- package/skills/integration-authoring/SKILL.md +48 -40
- package/skills/oats-getting-started/SKILL.md +105 -110
- package/skills/oats-support/SKILL.md +2 -2
- package/skills/soul-craft/SKILL.md +13 -6
- package/bin/oats-pi-sdk-host.mjs +0 -17
- package/docs/2026-09-03-architecture-proposal.md +0 -642
- package/docs/artifact-approvals.schema.json +0 -7
- package/docs/captured-invocation-context.schema.json +0 -7
- package/docs/captured-resolution.schema.json +0 -7
- package/docs/design/package-engine-contract.md +0 -813
- package/docs/design/package-runtime-api.md +0 -588
- package/docs/desktop-succession.md +0 -57
- package/docs/execution-capsule.schema.json +0 -108
- package/docs/first-team-demo.md +0 -92
- package/docs/knowledge-migration.md +0 -147
- package/docs/migration-from-oas.md +0 -103
- package/docs/oats-config.schema.json +0 -172
- package/docs/oats-lock-v3.schema.json +0 -7
- package/docs/oats-lock.schema.json +0 -175
- package/docs/operating-team-migration.md +0 -470
- package/docs/portable.schema.json +0 -2512
- package/docs/provider-check-input.schema.json +0 -7
- package/docs/rebuild-to-v2.md +0 -511
- package/docs/workspace-adoption.md +0 -74
- package/injects/framework-workspace.md +0 -7
- package/injects/local-soul.md +0 -19
- package/injects/oats-portable.md +0 -20
- package/injects/oats.md +0 -11
- package/injects/portable-instance-boundary.md +0 -39
- package/injects/portable-work-directory.md +0 -29
- package/lib/artifact-approvals.mjs +0 -120
- package/lib/artifact-tree.mjs +0 -141
- package/lib/capability-artifacts.mjs +0 -179
- package/lib/capability-execution.mjs +0 -15
- package/lib/capability-inputs.mjs +0 -39
- package/lib/capability-provenance.mjs +0 -231
- package/lib/captured-action-shape.mjs +0 -21
- package/lib/captured-admission-shape.mjs +0 -20
- package/lib/captured-binding-file.mjs +0 -36
- package/lib/captured-dispatch.mjs +0 -66
- package/lib/captured-instance-index.mjs +0 -277
- package/lib/captured-invocation-context.mjs +0 -130
- package/lib/captured-launch-request.mjs +0 -66
- package/lib/captured-operation-process.mjs +0 -15
- package/lib/captured-pi-custody.mjs +0 -29
- package/lib/captured-pi-host.mjs +0 -167
- package/lib/captured-pi-outcome.mjs +0 -172
- package/lib/captured-resolutions.mjs +0 -275
- package/lib/captured-scaffold.mjs +0 -87
- package/lib/captured-selector.mjs +0 -28
- package/lib/captured-session-backend.mjs +0 -52
- package/lib/captured-source-receipt-file.mjs +0 -72
- package/lib/helper-injection-policy.mjs +0 -104
- package/lib/legacy-lock-codec.mjs +0 -106
- package/lib/manifest-settings.mjs +0 -84
- package/lib/package-closure.mjs +0 -48
- package/lib/package-materialization.mjs +0 -83
- package/lib/pi-sdk-host.mjs +0 -229
- package/lib/portable-artifacts.mjs +0 -115
- package/lib/portable-choices.mjs +0 -82
- package/lib/portable-composition.mjs +0 -136
- package/lib/portable-digest.mjs +0 -105
- package/lib/portable-identity.mjs +0 -40
- package/lib/portable-lock.mjs +0 -117
- package/lib/portable-onboarding-request.mjs +0 -49
- package/lib/portable-onboarding.mjs +0 -256
- package/lib/portable-package-preparation.mjs +0 -188
- package/lib/portable-policy.mjs +0 -44
- package/lib/portable-soul.mjs +0 -42
- package/lib/portable-state.mjs +0 -80
- package/lib/prepare-composition.mjs +0 -170
- package/lib/prepared-bindings.mjs +0 -92
- package/lib/prepared-resources.mjs +0 -127
- package/lib/provider-binding-broker.mjs +0 -65
- package/lib/provider-binding-wire.mjs +0 -116
- package/lib/readiness.mjs +0 -225
- package/lib/repository-observation.mjs +0 -226
- package/lib/resolution-shape.mjs +0 -393
- package/lib/schedule-capsule.mjs +0 -206
- package/lib/soul-constraints.mjs +0 -40
- package/lib/source-projection.mjs +0 -84
- package/lib/source-spec.mjs +0 -189
- package/lib/workspace-definition.mjs +0 -126
- package/lib/workspace-discovery.mjs +0 -146
- package/skills/oats/SKILL.md +0 -162
- package/skills/oats-config/SKILL.md +0 -164
- package/skills/oats-packages/SKILL.md +0 -184
- package/skills/oats-portable/SKILL.md +0 -115
- 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
|
|
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)
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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>", …]
|
|
31
|
-
* approved: { executables: "sha256-<hex>", at: "<ISO-8601 UTC>" } | null
|
|
31
|
+
* capabilities: ["<cap name>", …] // sorted
|
|
32
32
|
* }
|
|
33
33
|
* }
|
|
34
34
|
* }
|
|
35
35
|
*
|
|
36
|
-
* `
|
|
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
|
|
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
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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. */
|
package/lib/provider-binding.mjs
CHANGED
|
@@ -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 './
|
|
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;
|
package/lib/provider-reasons.mjs
CHANGED
|
@@ -1,60 +1,6 @@
|
|
|
1
|
-
/** Fixed provider-declared diagnostics
|
|
2
|
-
*
|
|
3
|
-
|
|
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
|
-
}
|