@awebai/oats 0.25.9 → 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 +279 -93
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
- 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 +8 -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-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 +34 -1
- package/docs/design/2026-09-24-phase-d-plan.md +57 -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 +537 -261
- 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 +64 -33
- 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.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 +6 -6
- 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
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/** Existing materialized-capability data contract: layout/identity, lock-row
|
|
2
|
-
* validation and installation provenance. No scope discovery, lock writes,
|
|
3
|
-
* acquisition, approval, resolver or lifecycle dispatch. */
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import { isAbsolute, join } from "node:path";
|
|
6
|
-
import { oatsError } from "./errors.mjs";
|
|
7
|
-
|
|
8
|
-
/** Scope-relative capability store subtrees. */
|
|
9
|
-
export const CAPABILITIES_DIRNAME = join(".agents", "capabilities");
|
|
10
|
-
export const INSTALLED_SUBDIR = "installed";
|
|
11
|
-
|
|
12
|
-
/** THE identity grammar for a MATERIALIZED (revised-v2) capability.
|
|
13
|
-
*
|
|
14
|
-
* A materialized capability id is not merely a label: it becomes a DIRECTORY
|
|
15
|
-
* NAME directly under `installed/`, so anything that can steer a filesystem
|
|
16
|
-
* join must be impossible before the join happens. The grammar is deliberately
|
|
17
|
-
* the package-id grammar — namespaced dots are fine, and `/`, `\`, `..`,
|
|
18
|
-
* absolute forms, `@`, and percent-encoded spellings are all outside it.
|
|
19
|
-
*
|
|
20
|
-
* The LEGACY v1 / owned / `from: path:` grammar (loadManifestAt) stays looser
|
|
21
|
-
* on purpose: those artifacts are named by `basename()` of their source, never
|
|
22
|
-
* by the declared id, so the id never reaches a path there. Tightening it would
|
|
23
|
-
* strand already-published standalone capabilities. */
|
|
24
|
-
export const CAPABILITY_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
25
|
-
export const isMaterializedCapabilityId = (id) => typeof id === "string" && CAPABILITY_ID_RE.test(id);
|
|
26
|
-
/** Why a capability id was refused, in one sentence, for every caller's own
|
|
27
|
-
* typed error (the lock parser raises invalid-lock, manifest validation raises
|
|
28
|
-
* invalid-package-manifest — the code each consumer already branches on). */
|
|
29
|
-
export const capabilityIdViolation = (id) =>
|
|
30
|
-
`${JSON.stringify(id)} is not a valid capability identity — expected ${CAPABILITY_ID_RE.source} (a namespaced id such as "oats.okf"; path separators, "..", absolute paths, "@" and encoded forms are refused because the id names a directory under ${INSTALLED_SUBDIR}/)`;
|
|
31
|
-
|
|
32
|
-
/** Generated provenance file inside every materialized artifact. It is INSIDE
|
|
33
|
-
* the hashed tree, so tampering with it is integrity drift; the lock stays
|
|
34
|
-
* authoritative. */
|
|
35
|
-
export const CAPABILITY_INSTALLATION_FILE = ".oats-installation.json";
|
|
36
|
-
|
|
37
|
-
export const PACKAGE_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
38
|
-
/** Own-property PRESENCE of any of these on a package row is forbidden
|
|
39
|
-
* transitional evidence (contract §4.1) — never truthiness and never array
|
|
40
|
-
* length, so an empty `capabilities: []` or a dependency-free old row still
|
|
41
|
-
* classifies. Package-row `path`/`dependencies` are NEVER tells: the current
|
|
42
|
-
* shape retains both. */
|
|
43
|
-
export const TRANSITIONAL_ROW_FIELDS = ["capabilities", "trustedCapabilities", "depsIntegrity"];
|
|
44
|
-
|
|
45
|
-
/** Normalize a configured package path to its canonical form, or throw.
|
|
46
|
-
*
|
|
47
|
-
* Canonical form is a POSIX-relative path with no redundant or trailing
|
|
48
|
-
* separators; every spelling of the repository root ("", ".", "./", "./.")
|
|
49
|
-
* normalizes to the single canonical "." so a root selection round-trips
|
|
50
|
-
* identically through spec → lock → JSON → doctor/list/update (contract §4).
|
|
51
|
-
*
|
|
52
|
-
* Fail-closed: absolute paths, Windows drive paths, host-ambient "~" spellings,
|
|
53
|
-
* backslash separators (ambiguous — a backslash is a legal POSIX filename
|
|
54
|
-
* character, so accepting it as a separator would make containment checks
|
|
55
|
-
* disagree with the filesystem) and NUL are rejected as invalid-source; ".."
|
|
56
|
-
* traversal is path-escape. Returns undefined ONLY for an absent value, so the
|
|
57
|
-
* caller can apply the source-appropriate default. */
|
|
58
|
-
export function normalizePackagePath(raw, { where = "package path", code = "invalid-source" } = {}) {
|
|
59
|
-
// ABSENT means absent. A present `null` (JSON's way of spelling a malformed
|
|
60
|
-
// value) is a violation, not a fall-through to the caller's default — a
|
|
61
|
-
// catalog entry that says `"path": null` must fail, not silently install
|
|
62
|
-
// DEFAULT_PACKAGE_PATH.
|
|
63
|
-
if (raw === undefined) return undefined;
|
|
64
|
-
if (typeof raw !== "string") throw oatsError(code, `${where} must be a string (got ${Array.isArray(raw) ? "array" : raw === null ? "null" : typeof raw})`);
|
|
65
|
-
const s = raw.trim();
|
|
66
|
-
if (s.includes("\0")) throw oatsError(code, `${where} contains a NUL byte`);
|
|
67
|
-
if (s.startsWith("~")) throw oatsError(code, `${where} "${s}" is a host-ambient path — package paths are repository-relative`);
|
|
68
|
-
if (s.includes("\\")) throw oatsError(code, `${where} "${s}" uses backslashes — package paths are POSIX-relative (use "/")`);
|
|
69
|
-
if (/^[A-Za-z]:[/\\]/.test(s)) throw oatsError(code, `${where} "${s}" is an absolute drive path — package paths are repository-relative`);
|
|
70
|
-
if (isAbsolute(s)) throw oatsError(code, `${where} "${s}" is absolute — package paths are repository-relative`);
|
|
71
|
-
const segments = s.split("/").filter((seg) => seg !== "" && seg !== ".");
|
|
72
|
-
if (segments.includes("..")) throw oatsError("path-escape", `${where} "${s}" escapes the source root with ".."`);
|
|
73
|
-
return segments.length ? segments.join("/") : ".";
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Parse a lock entry's `source` against the EXACT normalized grammar the
|
|
77
|
-
* writer produces. Strict on purpose: `updatePackage` turns this back into a
|
|
78
|
-
* source spec, so a payload that merely "starts with catalog:" but is not a
|
|
79
|
-
* valid catalog id gets RECLASSIFIED downstream — `catalog:../evil` would be
|
|
80
|
-
* re-parsed as a host-relative local path and acquired from the operator's
|
|
81
|
-
* filesystem. A lock also never carries a `#<path>` fragment: the selected
|
|
82
|
-
* root is the entry's own `path` field, and a fragment here would produce a
|
|
83
|
-
* double-fragment spec on update. */
|
|
84
|
-
export function parseLockSource(src) {
|
|
85
|
-
const s = String(src || "");
|
|
86
|
-
const bad = (why) => oatsError("invalid-source", `unknown lock source "${src}" — ${why}`);
|
|
87
|
-
if (s.includes("#")) throw bad(`lock sources carry no "#<path>" fragment; the selected package root is the entry's "path" field`);
|
|
88
|
-
if (s.startsWith("path:")) {
|
|
89
|
-
const p = s.slice(5);
|
|
90
|
-
if (!p) throw bad("empty path source");
|
|
91
|
-
if (!isAbsolute(p)) throw bad("path source must be an absolute directory (the writer always resolves it)");
|
|
92
|
-
return { kind: "path", path: p, normalized: s };
|
|
93
|
-
}
|
|
94
|
-
if (s.startsWith("catalog:")) {
|
|
95
|
-
const body = s.slice(8);
|
|
96
|
-
// Split at the FIRST "@", mirroring the public parser's regex: the catalog
|
|
97
|
-
// id grammar cannot contain "@", so everything after the first one is the
|
|
98
|
-
// selector. Splitting at the LAST "@" misreads a legitimate ref spelling
|
|
99
|
-
// such as `oats.okf@release@candidate` — which the writer does produce —
|
|
100
|
-
// as the id `oats.okf@release`.
|
|
101
|
-
const at = body.indexOf("@");
|
|
102
|
-
const id = at > 0 ? body.slice(0, at) : body;
|
|
103
|
-
const selector = at > 0 ? body.slice(at + 1) : undefined;
|
|
104
|
-
if (!PACKAGE_ID_RE.test(id)) throw bad(`"${id}" is not a valid official catalog id`);
|
|
105
|
-
if (at > 0 && !selector) throw bad("empty catalog selector");
|
|
106
|
-
return { kind: "catalog", id, selector, normalized: s };
|
|
107
|
-
}
|
|
108
|
-
if (s.startsWith("git:")) {
|
|
109
|
-
const body = s.slice(4);
|
|
110
|
-
const at = body.lastIndexOf("@") > body.lastIndexOf("/") ? body.lastIndexOf("@") : -1;
|
|
111
|
-
const url = at > 0 ? body.slice(0, at) : body;
|
|
112
|
-
const ref = at > 0 ? body.slice(at + 1) : undefined;
|
|
113
|
-
if (!url) throw bad("empty git url");
|
|
114
|
-
if (at > 0 && !ref) throw bad("empty git ref");
|
|
115
|
-
if (!/^(https?:\/\/|file:\/\/|git@|ssh:\/\/|git:\/\/)/.test(url)) throw bad(`"${url}" is not an http(s)/ssh/file/git URL`);
|
|
116
|
-
return { kind: "git", url, ref, normalized: s };
|
|
117
|
-
}
|
|
118
|
-
throw oatsError("invalid-source", `unknown lock source "${src}"`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Semantic lock-entry validation for a PACKAGE row (runtime API addendum §4):
|
|
122
|
-
* source/commit pairing, canonical path, dependency references (incl. self and
|
|
123
|
-
* cycle over the locked graph), digest shapes, uniqueness. Run BEFORE restore,
|
|
124
|
-
* trust/approval, update/remove/migration planning, the locked-template reader,
|
|
125
|
-
* and doctor/list consumption. Fails closed with code "invalid-lock" carrying
|
|
126
|
-
* file/package provenance; never normalizes or auto-repairs on read. */
|
|
127
|
-
export function validateLockEntry(packageId, entry, allPackages = {}, opts = {}) {
|
|
128
|
-
const where = opts.file ? ` (${opts.file})` : "";
|
|
129
|
-
const bad = (msg) => oatsError("invalid-lock", `lock entry for package "${packageId}"${where} is invalid: ${msg}`, [{ package: packageId, file: opts.file, violation: msg }]);
|
|
130
|
-
if (!entry || typeof entry !== "object") throw bad("not an object");
|
|
131
|
-
for (const k of ["source", "path", "version", "commit", "integrity"]) if (!entry[k] || typeof entry[k] !== "string") throw bad(`missing ${k}`);
|
|
132
|
-
// The selected package root is a STRICT separate field (contract §1.1) stored
|
|
133
|
-
// in canonical form only — a lock is never normalized or repaired on read, so
|
|
134
|
-
// a non-canonical spelling ("./sub", "sub/", "") is invalid, not silently
|
|
135
|
-
// accepted. That is what makes the root representation round-trip.
|
|
136
|
-
{
|
|
137
|
-
let canonical;
|
|
138
|
-
try { canonical = normalizePackagePath(entry.path, { where: "path", code: "invalid-lock" }); }
|
|
139
|
-
catch (e) { throw bad(`invalid path ${JSON.stringify(entry.path)} — ${e.message}`); }
|
|
140
|
-
if (canonical !== entry.path) throw bad(`path ${JSON.stringify(entry.path)} is not in canonical form (expected ${JSON.stringify(canonical)})`);
|
|
141
|
-
}
|
|
142
|
-
if (!/^sha256-[0-9a-f]{64}$/.test(entry.integrity)) throw bad(`malformed integrity "${entry.integrity}"`);
|
|
143
|
-
// Present-but-wrong-typed optional fields are invalid — default ONLY when absent.
|
|
144
|
-
// `dependencies` is ALWAYS recorded (empty array when none), so a reader never
|
|
145
|
-
// has to distinguish absent from empty.
|
|
146
|
-
if (!Array.isArray(entry.dependencies)) throw bad("dependencies must be an array (empty when the package has none)");
|
|
147
|
-
for (const d of entry.dependencies) if (typeof d !== "string" || !PACKAGE_ID_RE.test(d)) throw bad(`dependencies contains an invalid package id ${JSON.stringify(d)}`);
|
|
148
|
-
if (new Set(entry.dependencies).size !== entry.dependencies.length) throw bad("dependencies contains duplicates");
|
|
149
|
-
// Package rows lock transport only. A capability list, a trust list or a
|
|
150
|
-
// dependency-closure digest here is the unsupported transitional shape — the
|
|
151
|
-
// central parser rejects those documents outright (contract §4.1); this is
|
|
152
|
-
// the entry-level backstop for a row reaching validation another way.
|
|
153
|
-
for (const gone of TRANSITIONAL_ROW_FIELDS) {
|
|
154
|
-
if (Object.hasOwn(entry, gone)) throw bad(`"${gone}" is a transitional package-root field — package rows lock transport only (capabilities and trust live on capability rows)`);
|
|
155
|
-
}
|
|
156
|
-
let src;
|
|
157
|
-
try { src = parseLockSource(entry.source); } catch { throw bad(`unrecognized source "${entry.source}"`); }
|
|
158
|
-
if (src.kind === "path" && !src.path) throw bad("empty path source");
|
|
159
|
-
if (src.kind === "git" && !src.url) throw bad("empty git source");
|
|
160
|
-
if (src.kind === "catalog" && !src.id) throw bad("empty catalog source");
|
|
161
|
-
if (src.kind === "path") {
|
|
162
|
-
if (entry.commit !== "local") throw bad(`path source requires commit "local", got "${entry.commit}"`);
|
|
163
|
-
// Local acquisition is exact-directory: the source string already names the
|
|
164
|
-
// package root, so the only valid contained path is the root itself.
|
|
165
|
-
if (entry.path !== ".") throw bad(`path source requires path "." (local sources are exact directories), got ${JSON.stringify(entry.path)}`);
|
|
166
|
-
}
|
|
167
|
-
else if (!/^[0-9a-f]{40}$/.test(entry.commit)) throw bad(`${src.kind} source requires an exact 40-hex commit, got "${entry.commit}"`);
|
|
168
|
-
for (const d of entry.dependencies || []) {
|
|
169
|
-
if (d === packageId) throw bad(`self-dependency "${d}"`);
|
|
170
|
-
// Object.hasOwn: a dependency literally named "constructor"/"__proto__"
|
|
171
|
-
// must not pass via Object.prototype.
|
|
172
|
-
if (!Object.hasOwn(allPackages, d)) throw bad(`dependency "${d}" is not locked in the same packages map`);
|
|
173
|
-
}
|
|
174
|
-
// Cycle over the locked dependency graph reachable from this entry.
|
|
175
|
-
const visiting = new Set();
|
|
176
|
-
const visited = new Set();
|
|
177
|
-
const walk = (id, chain) => {
|
|
178
|
-
if (visited.has(id)) return;
|
|
179
|
-
if (visiting.has(id)) throw bad(`dependency cycle in the locked graph: ${[...chain, id].join(" → ")}`);
|
|
180
|
-
visiting.add(id);
|
|
181
|
-
const deps = Object.hasOwn(allPackages, id) && Array.isArray(allPackages[id]?.dependencies) ? allPackages[id].dependencies : [];
|
|
182
|
-
for (const d of deps) if (Object.hasOwn(allPackages, d) || d === packageId) walk(d, [...chain, id]);
|
|
183
|
-
visiting.delete(id); visited.add(id);
|
|
184
|
-
};
|
|
185
|
-
walk(packageId, []);
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Semantic validation of one CAPABILITY row against the whole document
|
|
190
|
-
* (contract §4). The `package` back-reference must name a locked package: it is
|
|
191
|
-
* the single provider truth, so a dangling reference would leave a materialized
|
|
192
|
-
* artifact with no provenance to restore or verify it from. */
|
|
193
|
-
export function validateCapabilityLockEntry(capabilityId, entry, allPackages = {}, opts = {}) {
|
|
194
|
-
const where = opts.file ? ` (${opts.file})` : "";
|
|
195
|
-
const bad = (msg) => oatsError("invalid-lock", `capability lock entry for "${capabilityId}"${where} is invalid: ${msg}`, [{ package: capabilityId, file: opts.file, violation: msg }]);
|
|
196
|
-
if (typeof capabilityId !== "string" || !capabilityId) throw bad("empty capability id");
|
|
197
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw bad("not an object");
|
|
198
|
-
for (const k of ["version", "package", "path", "integrity"]) if (!entry[k] || typeof entry[k] !== "string") throw bad(`missing ${k}`);
|
|
199
|
-
if (!PACKAGE_ID_RE.test(entry.package)) throw bad(`provider package ${JSON.stringify(entry.package)} is not a valid package identity`);
|
|
200
|
-
if (!Object.hasOwn(allPackages, entry.package)) throw bad(`provider package "${entry.package}" is not locked in the same packages map`);
|
|
201
|
-
{
|
|
202
|
-
let canonical;
|
|
203
|
-
try { canonical = normalizePackagePath(entry.path, { where: "path", code: "invalid-lock" }); }
|
|
204
|
-
catch (e) { throw bad(`invalid path ${JSON.stringify(entry.path)} — ${e.message}`); }
|
|
205
|
-
if (canonical !== entry.path) throw bad(`path ${JSON.stringify(entry.path)} is not in canonical form (expected ${JSON.stringify(canonical)})`);
|
|
206
|
-
}
|
|
207
|
-
if (!/^sha256-[0-9a-f]{64}$/.test(entry.integrity)) throw bad(`malformed integrity "${entry.integrity}"`);
|
|
208
|
-
if (typeof entry.trusted !== "boolean") throw bad(`"trusted" must be a boolean (got ${JSON.stringify(entry.trusted)})`);
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Read an artifact's provenance and check it AGREES with the lock rows it was
|
|
213
|
-
* projected from. Disagreement is invalid-lock: the artifact and the lock claim
|
|
214
|
-
* different origins, and neither may silently win. (A modified file also fails
|
|
215
|
-
* integrity, but this gives the precise diagnosis.) */
|
|
216
|
-
export function verifyCapabilityInstallation(dir, capabilityId, capRow, pkgRow) {
|
|
217
|
-
const file = join(dir, CAPABILITY_INSTALLATION_FILE);
|
|
218
|
-
if (!existsSync(file)) throw oatsError("invalid-lock", `materialized capability ${capabilityId} has no ${CAPABILITY_INSTALLATION_FILE} provenance — reproject it with \`oats install\``, [{ package: capabilityId, file }]);
|
|
219
|
-
let doc;
|
|
220
|
-
try { doc = JSON.parse(readFileSync(file, "utf8")); }
|
|
221
|
-
catch (e) { throw oatsError("invalid-lock", `materialized capability ${capabilityId} has malformed ${CAPABILITY_INSTALLATION_FILE}: ${e.message}`, [{ package: capabilityId, file }]); }
|
|
222
|
-
const expected = {
|
|
223
|
-
schemaVersion: 1, capability: capabilityId, version: capRow.version, package: capRow.package,
|
|
224
|
-
packageVersion: pkgRow.version, source: pkgRow.source, commit: pkgRow.commit,
|
|
225
|
-
packagePath: pkgRow.path, capabilityPath: capRow.path,
|
|
226
|
-
};
|
|
227
|
-
for (const [k, want] of Object.entries(expected)) {
|
|
228
|
-
if (doc?.[k] !== want) throw oatsError("invalid-lock", `materialized capability ${capabilityId}: ${CAPABILITY_INSTALLATION_FILE} "${k}" is ${JSON.stringify(doc?.[k])} but the lock records ${JSON.stringify(want)}`, [{ package: capabilityId, file, violation: k }]);
|
|
229
|
-
}
|
|
230
|
-
return doc;
|
|
231
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/** The existing captured loader action wire, shared with provider invocation
|
|
2
|
-
* validation. This is a data codec, not a dispatch table or provider policy. */
|
|
3
|
-
import { canonicalJson } from "./portable-values.mjs";
|
|
4
|
-
import { objectAt, stringAt } from "./portable-shape.mjs";
|
|
5
|
-
import { oatsError } from "./errors.mjs";
|
|
6
|
-
|
|
7
|
-
export function validateCapturedAction(action) {
|
|
8
|
-
canonicalJson(action);
|
|
9
|
-
if (!["inspect", "compose", "command", "operation", "hook"].includes(action?.kind)) throw oatsError("unsupported-action", "captured action is not supported by this loader");
|
|
10
|
-
if (["inspect", "compose"].includes(action.kind)) objectAt(action, ["kind"], ["kind"]);
|
|
11
|
-
else if (action.kind === "command") {
|
|
12
|
-
objectAt(action, ["kind", "capability", "namespace", "name"], ["kind", "name"]);
|
|
13
|
-
if ((action.capability === undefined) === (action.namespace === undefined)) throw oatsError("invalid-declaration", "command needs exactly one capability or namespace");
|
|
14
|
-
} else {
|
|
15
|
-
const fields = action.kind === "hook" ? ["kind", "capability", "name"] : ["kind", "slot", "name"];
|
|
16
|
-
objectAt(action, fields, fields);
|
|
17
|
-
}
|
|
18
|
-
for (const key of ["name", "namespace", "capability", "slot"]) if (action[key] !== undefined) stringAt(action[key], `/action/${key}`);
|
|
19
|
-
if (action.kind === "operation" && !["knowledge", "messaging", "tasks"].includes(action.slot)) throw oatsError("invalid-declaration", "captured operation slot is invalid");
|
|
20
|
-
return action;
|
|
21
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/** Generic incarnation/admitted-intent identities. Composition references are
|
|
2
|
-
* deliberately not accepted as incarnation or logical-request identities. */
|
|
3
|
-
import { canonicalJson } from "./portable-values.mjs";
|
|
4
|
-
import { objectAt, stringAt } from "./portable-shape.mjs";
|
|
5
|
-
import { oatsError } from "./errors.mjs";
|
|
6
|
-
|
|
7
|
-
export function validateIncarnationId(value) {
|
|
8
|
-
stringAt(value, "/incarnationId", { pattern: /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/ });
|
|
9
|
-
return value;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function validateIntentRef(value) {
|
|
13
|
-
canonicalJson(value, { maxBytes: 4096, maxDepth: 4, maxEntries: 16 });
|
|
14
|
-
objectAt(value, ["schemaVersion", "executionId", "incarnationId", "attempt"], ["schemaVersion", "executionId", "incarnationId", "attempt"]);
|
|
15
|
-
if (value.schemaVersion !== 1) throw oatsError("unsupported-wire-version", "unsupported captured intent version");
|
|
16
|
-
stringAt(value.executionId, "/executionId", { pattern: /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/ });
|
|
17
|
-
validateIncarnationId(value.incarnationId);
|
|
18
|
-
if (!Number.isSafeInteger(value.attempt) || value.attempt < 1) throw oatsError("invalid-declaration", "intent attempt must be a positive integer");
|
|
19
|
-
return value;
|
|
20
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/** Private invocation projection, not a new binding store or action permit.
|
|
2
|
-
* The synchronous caller has already loaded/qualified its exact captured action. */
|
|
3
|
-
import { lstatSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { portableStateDirectory } from './portable-state.mjs';
|
|
6
|
-
import { canonicalJson } from './portable-values.mjs';
|
|
7
|
-
import { validateProviderBinding } from './resolution-shape.mjs';
|
|
8
|
-
import { BINDING_LIMITS } from './provider-binding-wire.mjs';
|
|
9
|
-
import { oatsError } from './errors.mjs';
|
|
10
|
-
|
|
11
|
-
export function withCapturedBindingFile({deployment,record,capability},run) {
|
|
12
|
-
const bindings=Object.values(record.bindings).filter(binding=>binding.capability===capability.id);
|
|
13
|
-
if (!bindings.length) return run({});
|
|
14
|
-
if (bindings.length !== 1) throw oatsError('invalid-resolution','one capability cannot consume contradictory slot bindings');
|
|
15
|
-
const binding=validateProviderBinding(bindings[0]),bytes=canonicalJson(binding,BINDING_LIMITS);
|
|
16
|
-
const directory=mkdtempSync(join(portableStateDirectory(deployment,true),'.binding-'));
|
|
17
|
-
const owned=lstatSync(directory),file=join(directory,'binding.json');let primary,result,completed=false;
|
|
18
|
-
try {
|
|
19
|
-
writeFileSync(file,bytes,{flag:'wx',mode:0o600});
|
|
20
|
-
result=run({OATS_BINDING_FILE:file});completed=true;return result;
|
|
21
|
-
} catch(error) {primary=error;throw error;}
|
|
22
|
-
finally {
|
|
23
|
-
try {
|
|
24
|
-
const current=lstatSync(directory);
|
|
25
|
-
if (!current.isDirectory() || current.dev!==owned.dev || current.ino!==owned.ino) throw oatsError('integrity-drift','binding invocation directory ownership changed');
|
|
26
|
-
rmSync(directory,{recursive:true});
|
|
27
|
-
} catch(cleanup) {
|
|
28
|
-
if (!primary) {
|
|
29
|
-
const error=new AggregateError([cleanup],'captured binding cleanup failed after invocation',{cause:cleanup});
|
|
30
|
-
error.code=cleanup.code;error.invocationCompleted=completed;error.invocationResult=result;throw error;
|
|
31
|
-
}
|
|
32
|
-
const error=new AggregateError([primary,cleanup],'captured command and binding cleanup failed',{cause:primary});
|
|
33
|
-
error.code=primary.code;if(primary.invocationCompleted){error.invocationCompleted=true;error.invocationResult=primary.invocationResult;}throw error;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/** One exact action-loading boundary. No current config, lock or marketplace
|
|
2
|
-
* lookup. Core supplies its complete codecs and native host checks. */
|
|
3
|
-
import { canonicalJson } from "./portable-values.mjs";
|
|
4
|
-
import { validateCapturedAction } from "./captured-action-shape.mjs";
|
|
5
|
-
import { verifyResolutionInputs } from "./captured-resolutions.mjs";
|
|
6
|
-
import { readApprovalLedger, evaluateCapturedApprovals } from "./artifact-approvals.mjs";
|
|
7
|
-
import { composeCapturedInstructions } from "./instruction-composition.mjs";
|
|
8
|
-
import { oatsError } from "./errors.mjs";
|
|
9
|
-
|
|
10
|
-
export function loadCapturedAction({ deployment, resolution, action }, codecs) {
|
|
11
|
-
validateCapturedAction(action);
|
|
12
|
-
const verified = verifyResolutionInputs(deployment, resolution), { record } = verified;
|
|
13
|
-
if (record.capture !== "prepared") throw oatsError("migration-required", "reconstructed dispatch requires the dedicated migration evidence verifier");
|
|
14
|
-
const manifests = new Map(), capabilities = new Map();
|
|
15
|
-
for (const [id, row] of Object.entries(record.artifacts.capabilities)) {
|
|
16
|
-
const root = verified.roots.get(canonicalJson(row.artifact));
|
|
17
|
-
const manifest = codecs.manifest(root);
|
|
18
|
-
if (!manifest || manifest.capability !== id || manifest.version !== row.version) throw oatsError("invalid-resolution", "compiled manifest differs from captured identity/version");
|
|
19
|
-
const settings = Object.fromEntries(Object.entries(record.dispatch.settingsChoices[id] ?? {}).map(([name, key]) => [name, record.choices[key].value]));
|
|
20
|
-
codecs.settings(manifest, settings);
|
|
21
|
-
manifests.set(id, manifest); capabilities.set(id, { id, manifest, settings });
|
|
22
|
-
}
|
|
23
|
-
// Complete static validation applies even to inspection; neither inspection
|
|
24
|
-
// nor input verification is an excuse to accept an unsafe launch recipe.
|
|
25
|
-
if (record.dispatch.launch !== null) codecs.launch(record.dispatch.launch);
|
|
26
|
-
const approvals = evaluateCapturedApprovals({ ...verified, manifests }, readApprovalLedger(deployment).ledger);
|
|
27
|
-
const base = { ...verified, manifests, capabilities, approvals, resolution, deployment };
|
|
28
|
-
if (action.kind === "inspect") return base;
|
|
29
|
-
if (action.kind === "compose") return { ...base, composition: composeCapturedInstructions(verified) };
|
|
30
|
-
let id = action.capability;
|
|
31
|
-
if (action.kind === "operation") id = record.bindings[action.slot]?.capability;
|
|
32
|
-
if (action.namespace !== undefined) {
|
|
33
|
-
const matches = [...manifests].filter(([, manifest]) => manifest.command === action.namespace);
|
|
34
|
-
if (matches.length > 1) throw oatsError("duplicate-namespace", "captured command namespace is ambiguous");
|
|
35
|
-
id = matches[0]?.[0];
|
|
36
|
-
}
|
|
37
|
-
if (typeof id !== "string" || !capabilities.has(id)) throw oatsError("capability-not-selected", "action capability is not selected by this resolution");
|
|
38
|
-
const capability = capabilities.get(id), manifest = capability.manifest;
|
|
39
|
-
let name = action.name, operation;
|
|
40
|
-
if (action.kind === "operation") {
|
|
41
|
-
operation = codecs.operations(manifest).find((entry) => entry.name === name);
|
|
42
|
-
if (!operation) throw oatsError("operation-not-found", "captured provider does not declare this operation");
|
|
43
|
-
name = operation.command;
|
|
44
|
-
}
|
|
45
|
-
const declarations = action.kind === "hook" ? codecs.hooks(manifest) : manifest.commands;
|
|
46
|
-
if (!declarations || !Object.hasOwn(declarations, name)) throw oatsError("command-not-found", "captured capability does not declare this action");
|
|
47
|
-
const approval = approvals.find((entry) => entry.artifact.capability === id);
|
|
48
|
-
if (approval.status !== "approved") throw oatsError("approval-required", "action needs current approval for its exact capability artifact");
|
|
49
|
-
// Provider-owned payload/credential/member qualification is a separate ABI.
|
|
50
|
-
// Until its adapter is supplied, report the missing gate rather than execute
|
|
51
|
-
// using an ambient binding file or treating captured choices as enrollment.
|
|
52
|
-
const invocation = typeof codecs.invocation === "function" ? codecs.invocation(base, action, capability) : undefined;
|
|
53
|
-
const bindings = Object.entries(record.bindings).filter(([, binding]) => binding.capability === id);
|
|
54
|
-
if (bindings.length) {
|
|
55
|
-
if (typeof codecs.bindings !== "function") throw oatsError("provider-not-qualified", "captured provider binding qualification is unavailable");
|
|
56
|
-
codecs.bindings(bindings, capability, record, invocation);
|
|
57
|
-
}
|
|
58
|
-
const missing = codecs.host(capability);
|
|
59
|
-
if (missing.length) throw oatsError("host-requirement-missing", "captured action has unmet host command requirements", missing);
|
|
60
|
-
const spec = declarations[name];
|
|
61
|
-
if (typeof spec !== "string" || !spec.trim() || spec.includes("\0")) throw oatsError("invalid-resolution", "captured executable declaration is not valid text");
|
|
62
|
-
const [script, ...args] = spec.trim().split(/\s+/), file = codecs.executable(manifest, script);
|
|
63
|
-
if (!file) throw oatsError("resource-not-found", "captured executable is absent");
|
|
64
|
-
if (![...verified.resources.values()].includes(file)) throw oatsError("resolution-incomplete", "captured executable is absent from the resource inventory");
|
|
65
|
-
return { ...base, action, capability, operation, executable: { file, args }, ...(invocation ? { invocation } : {}) };
|
|
66
|
-
}
|