@awebai/oats 0.23.2 → 0.24.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 +7 -4
- package/bin/oats-pi-sdk-host.mjs +17 -0
- package/bin/oats.mjs +442 -51
- package/capabilities/oats-okf/bin/oats-okf-binding.mjs +14 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +79 -11
- package/capabilities/oats-okf/lib/binding-wire.mjs +268 -0
- package/capabilities/oats-okf/lib/captured-worker.mjs +101 -0
- package/capabilities/oats-okf/lib/config.mjs +2 -1
- package/capabilities/oats-okf/lib/inspection.mjs +16 -1
- package/capabilities/oats-okf/lib/invocation-context.mjs +111 -0
- package/capabilities/oats-okf/lib/invocation-shape.mjs +135 -0
- package/capabilities/oats-okf/lib/io.mjs +1 -1
- package/capabilities/oats-okf/lib/portable-binding.mjs +199 -0
- package/capabilities/oats-okf/lib/source-contract.mjs +46 -0
- package/capabilities/oats-okf/lib/sources.mjs +123 -3
- package/capabilities/oats-okf/lib/stores.mjs +104 -25
- package/capabilities/oats-okf/lib/worker.mjs +69 -10
- package/capabilities/oats-okf/oats.json +35 -7
- package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +87 -0
- package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +113 -0
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +23 -5
- package/capabilities/oats-okf/skills/okf/SKILL.md +42 -1
- package/docs/artifact-approvals.schema.json +7 -0
- package/docs/capability-manifest.schema.json +37 -66
- package/docs/captured-invocation-context.schema.json +7 -0
- package/docs/captured-resolution.schema.json +7 -0
- package/docs/design/2026-09-14-artifact-retention-contract.md +190 -0
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +708 -0
- package/docs/design/2026-09-14-portable-souls-contract-amendments.md +85 -0
- package/docs/design/2026-09-14-portable-souls-explainer.md +750 -0
- package/docs/design/2026-09-15-captured-dispatch.md +127 -0
- package/docs/design/2026-09-15-captured-resolution-records.md +143 -0
- package/docs/design/2026-09-15-package-preparation.md +100 -0
- package/docs/design/2026-09-15-portable-data-contract.md +121 -0
- package/docs/design/2026-09-15-portable-declarations.md +189 -0
- package/docs/design/2026-09-15-portable-souls-handoff.md +150 -0
- package/docs/design/2026-09-15-portable-souls-implementation.md +417 -0
- package/docs/design/2026-09-15-selection-lock-and-approval.md +122 -0
- package/docs/design/2026-09-15-source-observation.md +119 -0
- package/docs/design/2026-09-16-captured-admission.md +77 -0
- package/docs/design/2026-09-16-captured-helper-dispatch.md +105 -0
- package/docs/design/2026-09-16-captured-launch-inputs.md +42 -0
- package/docs/design/2026-09-16-command-profile-preparation.md +86 -0
- package/docs/design/2026-09-16-fresh-install-first-rollout.md +47 -0
- package/docs/design/2026-09-16-fresh-operator-walkthrough.md +277 -0
- package/docs/design/2026-09-16-knowledge-capability-contract.md +61 -0
- package/docs/design/2026-09-16-messaging-capability-contract.md +59 -0
- package/docs/design/2026-09-16-portable-migration-evidence.md +156 -0
- package/docs/design/2026-09-16-portable-onboarding.md +177 -0
- package/docs/design/2026-09-16-prepare-request-transport.md +26 -0
- package/docs/design/2026-09-16-provider-binding-codecs.md +98 -0
- package/docs/design/2026-09-16-provider-binding-wire.md +247 -0
- package/docs/design/2026-09-17-capability-helper-input-contract.md +95 -0
- package/docs/design/2026-09-17-captured-backend-parity.md +53 -0
- package/docs/design/2026-09-17-captured-native-start.md +58 -0
- package/docs/design/2026-09-17-portable-boundary-hookup.md +19 -0
- package/docs/design/2026-09-17-portable-boundary-resources.md +52 -0
- package/docs/design/2026-09-17-public-captured-start.md +108 -0
- package/docs/design/2026-09-17-public-prepare-request.md +90 -0
- package/docs/design/2026-09-18-captured-pi-host.md +205 -0
- package/docs/design/2026-09-18-first-cut-release-checklist.md +131 -0
- package/docs/design/2026-09-18-herdr-protocol-compatibility.md +60 -0
- package/docs/desktop-cli-api.md +5 -2
- package/docs/execution-capsule.schema.json +108 -0
- package/docs/execution-targets.md +20 -7
- package/docs/oats-lock-v3.schema.json +7 -0
- package/docs/oats-member.schema.json +38 -0
- package/docs/oats-workspace.schema.json +68 -0
- package/docs/portable.schema.json +2512 -0
- package/docs/provider-check-input.schema.json +7 -0
- package/docs/release-notes/v0.24.0.md +104 -0
- package/docs/schedules.md +126 -14
- package/docs/soul.schema.json +82 -0
- package/injects/oats-portable.md +17 -0
- package/injects/portable-instance-boundary.md +39 -0
- package/injects/portable-work-directory.md +29 -0
- package/lib/artifact-approvals.mjs +120 -0
- package/lib/artifact-tree.mjs +141 -0
- package/lib/capability-artifacts.mjs +179 -0
- package/lib/capability-execution.mjs +15 -0
- package/lib/capability-inputs.mjs +39 -0
- package/lib/capability-provenance.mjs +231 -0
- package/lib/captured-action-shape.mjs +21 -0
- package/lib/captured-admission-shape.mjs +20 -0
- package/lib/captured-binding-file.mjs +36 -0
- package/lib/captured-dispatch.mjs +66 -0
- package/lib/captured-instance-index.mjs +277 -0
- package/lib/captured-invocation-context.mjs +130 -0
- package/lib/captured-launch-request.mjs +46 -0
- package/lib/captured-operation-process.mjs +15 -0
- package/lib/captured-pi-custody.mjs +29 -0
- package/lib/captured-pi-host.mjs +167 -0
- package/lib/captured-pi-outcome.mjs +172 -0
- package/lib/captured-resolutions.mjs +275 -0
- package/lib/captured-scaffold.mjs +87 -0
- package/lib/captured-selector.mjs +28 -0
- package/lib/captured-session-backend.mjs +52 -0
- package/lib/captured-source-receipt-file.mjs +72 -0
- package/lib/config-data.mjs +104 -0
- package/lib/core.mjs +918 -562
- package/lib/errors.mjs +7 -0
- package/lib/helper-injection-policy.mjs +98 -0
- package/lib/herdr.mjs +18 -7
- package/lib/instruction-composition.mjs +31 -0
- package/lib/legacy-lock-codec.mjs +106 -0
- package/lib/manifest-settings.mjs +84 -0
- package/lib/package-closure.mjs +48 -0
- package/lib/package-materialization.mjs +83 -0
- package/lib/pi-sdk-host.mjs +229 -0
- package/lib/portable-artifacts.mjs +115 -0
- package/lib/portable-choices.mjs +82 -0
- package/lib/portable-composition.mjs +136 -0
- package/lib/portable-digest.mjs +105 -0
- package/lib/portable-files.mjs +26 -0
- package/lib/portable-identity.mjs +40 -0
- package/lib/portable-lock.mjs +117 -0
- package/lib/portable-migration-artifacts.mjs +135 -0
- package/lib/portable-migration-evidence.mjs +305 -0
- package/lib/portable-migration-store.mjs +199 -0
- package/lib/portable-migration.mjs +104 -0
- package/lib/portable-onboarding-acceptance.mjs +66 -0
- package/lib/portable-onboarding-request.mjs +49 -0
- package/lib/portable-onboarding.mjs +230 -0
- package/lib/portable-package-preparation.mjs +188 -0
- package/lib/portable-policy.mjs +44 -0
- package/lib/portable-shape.mjs +35 -0
- package/lib/portable-soul.mjs +38 -0
- package/lib/portable-state.mjs +80 -0
- package/lib/portable-values.mjs +181 -0
- package/lib/prepare-composition.mjs +151 -0
- package/lib/prepared-bindings.mjs +78 -0
- package/lib/prepared-resources.mjs +127 -0
- package/lib/provider-binding-broker.mjs +59 -0
- package/lib/provider-binding-wire.mjs +110 -0
- package/lib/provider-binding.mjs +22 -0
- package/lib/repository-observation.mjs +226 -0
- package/lib/resolution-shape.mjs +393 -0
- package/lib/schedule-capsule.mjs +206 -0
- package/lib/schedule.mjs +259 -38
- package/lib/servers.mjs +15 -0
- package/lib/soul-constraints.mjs +40 -0
- package/lib/source-projection.mjs +84 -0
- package/lib/source-spec.mjs +189 -0
- package/lib/workspace-definition.mjs +126 -0
- package/lib/workspace-discovery.mjs +146 -0
- package/package-catalog.json +1 -1
- package/package.json +3 -2
- package/packages/record/lib/capture-cc.mjs +14 -6
- package/packages/record/lib/formats.mjs +14 -3
- package/packages/record/lib/native-history.mjs +277 -7
- package/packages/record/lib/session-snapshot.mjs +25 -5
- package/packages/record/lib/sessions-for-home.mjs +30 -13
- package/skills/oats/SKILL.md +12 -7
- package/skills/oats-config/SKILL.md +12 -9
- package/skills/oats-packages/SKILL.md +12 -8
- package/skills/oats-portable/SKILL.md +116 -0
- package/skills/oats-portable-artifacts/SKILL.md +63 -0
- package/skills/oats-portable-setup/SKILL.md +69 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Versioned soul declarations: parsing and origins, not composition or execution.
|
|
2
|
+
* Requirements constrain the later single resolver; defaults never erase them. */
|
|
3
|
+
import { parseConfigData } from "./config-data.mjs";
|
|
4
|
+
import { portablePath } from "./source-spec.mjs";
|
|
5
|
+
import { invalidShape, objectAt, stringAt, stringSetAt, versionAt } from "./portable-shape.mjs";
|
|
6
|
+
import { validatePolicyChoices, validateProviderDeclaration } from "./portable-policy.mjs";
|
|
7
|
+
|
|
8
|
+
export { FUNDAMENTAL_SLOTS } from "./portable-policy.mjs";
|
|
9
|
+
const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const ALIAS = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
11
|
+
const LAUNCH_CONFIG_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
12
|
+
const ROOT_FIELDS = ["schemaVersion", "name", "description", "requires", "defaults", "knowledge", "teams", "resources", "work", "runtime", "model", "yolo", "launch-config", "backend"];
|
|
13
|
+
|
|
14
|
+
export function parsePortableSoul(input, { origin = null, localBase, allowLocalPaths = false, limits } = {}) {
|
|
15
|
+
const parsed = parseConfigData(input, { origin, limits });
|
|
16
|
+
const value = objectAt(parsed.value, ROOT_FIELDS, ["schemaVersion", "name"]);
|
|
17
|
+
versionAt(value.schemaVersion);
|
|
18
|
+
stringAt(value.name, "/name", { pattern: NAME });
|
|
19
|
+
if (Object.hasOwn(value, "description")) stringAt(value.description, "/description", { empty: true });
|
|
20
|
+
const sources = Object.create(null);
|
|
21
|
+
for (const section of ["requires", "defaults"]) {
|
|
22
|
+
if (!Object.hasOwn(value, section)) continue;
|
|
23
|
+
Object.assign(sources, validatePolicyChoices(value[section], {
|
|
24
|
+
pointer: `/${section}`, required: section === "requires", origins: parsed.origins,
|
|
25
|
+
origin, localBase, allowLocalPaths,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
if (Object.hasOwn(value, "knowledge")) validateProviderDeclaration(value.knowledge, "/knowledge");
|
|
29
|
+
if (Object.hasOwn(value, "teams")) stringSetAt(value.teams, "/teams", (item, pointer) => stringAt(item, pointer, { pattern: ALIAS }));
|
|
30
|
+
if (Object.hasOwn(value, "resources")) stringSetAt(value.resources, "/resources", (item) => portablePath(item, { allowRoot: true }));
|
|
31
|
+
if (Object.hasOwn(value, "work") && !["worktree", "checkout", "attached", "workspace", "directory"].includes(value.work)) invalidShape("/work", "unsupported work mode");
|
|
32
|
+
if (Object.hasOwn(value, "runtime") && !["pi", "claude", "codex"].includes(value.runtime)) invalidShape("/runtime", "unsupported runtime");
|
|
33
|
+
if (Object.hasOwn(value, "model")) stringAt(value.model, "/model");
|
|
34
|
+
if (Object.hasOwn(value, "launch-config")) stringAt(value["launch-config"], "/launch-config", { pattern: LAUNCH_CONFIG_NAME });
|
|
35
|
+
if (Object.hasOwn(value, "backend") && !["tmux", "herdr"].includes(value.backend)) invalidShape("/backend", "unsupported session backend");
|
|
36
|
+
if (Object.hasOwn(value, "yolo") && typeof value.yolo !== "boolean") invalidShape("/yolo", "expected boolean");
|
|
37
|
+
return { declaration: value, sources, origins: parsed.origins, integrity: parsed.integrity };
|
|
38
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Cooperative serialization for new portable-state writers, not legacy cutover
|
|
2
|
+
* or hostile-host locking. A crashed guard is held for explicit recovery. */
|
|
3
|
+
import { linkSync, lstatSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { canonicalJson } from "./portable-values.mjs";
|
|
6
|
+
import { ensureStoreIgnore } from "./capability-artifacts.mjs";
|
|
7
|
+
import { oatsError } from "./errors.mjs";
|
|
8
|
+
|
|
9
|
+
export function portableScope(scope) {
|
|
10
|
+
if (typeof scope !== "string" || !isAbsolute(scope)) throw oatsError("invalid-artifact-store", "portable deployment must be explicit and absolute");
|
|
11
|
+
const actual = realpathSync(scope);
|
|
12
|
+
if (!lstatSync(actual).isDirectory()) throw oatsError("invalid-artifact-store", "portable deployment must be a directory");
|
|
13
|
+
return actual;
|
|
14
|
+
}
|
|
15
|
+
export function portableStateDirectory(scope, create = false) {
|
|
16
|
+
let root = portableScope(scope);
|
|
17
|
+
for (const name of [".agents", "portable"]) {
|
|
18
|
+
root = join(root, name);
|
|
19
|
+
if (create) { try { mkdirSync(root, { mode: 0o700 }); } catch (error) { if (error.code !== "EEXIST") throw error; } }
|
|
20
|
+
let stat;
|
|
21
|
+
try { stat = lstatSync(root); } catch (error) { if (!create && error.code === "ENOENT") return null; throw error; }
|
|
22
|
+
if (!stat.isDirectory()) throw oatsError("invalid-artifact-store", "managed portable-state component is not a real directory");
|
|
23
|
+
}
|
|
24
|
+
if (create) ensureStoreIgnore(root);
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
const activeWrites = new WeakSet();
|
|
28
|
+
|
|
29
|
+
/** Only an active synchronous write can publish a mutable metadata document. */
|
|
30
|
+
export function writeGuardedPortableDocument(context, target, value, { absent = false } = {}) {
|
|
31
|
+
if (!activeWrites.has(context)) throw oatsError("selection-changed", "portable write context is not active");
|
|
32
|
+
const { root, deployment } = context;
|
|
33
|
+
if (dirname(target) !== root && target !== join(deployment, "oats-lock.json")) throw oatsError("invalid-artifact-store", "portable write target is outside its owned metadata namespace");
|
|
34
|
+
const bytes = canonicalJson(value), staging = mkdtempSync(join(root, ".document-")), candidate = join(staging, "document.json");
|
|
35
|
+
let failed = false, primary;
|
|
36
|
+
try {
|
|
37
|
+
writeFileSync(candidate, bytes, { flag: "wx", mode: 0o600 });
|
|
38
|
+
if (absent) {
|
|
39
|
+
try { linkSync(candidate, target); }
|
|
40
|
+
catch (error) { if (error.code === "EEXIST") throw oatsError("selection-changed", "portable metadata appeared during first publication"); throw error; }
|
|
41
|
+
} else renameSync(candidate, target);
|
|
42
|
+
} catch (error) { failed = true; primary = error; throw error; }
|
|
43
|
+
finally {
|
|
44
|
+
try { rmSync(staging, { recursive: true, force: true }); }
|
|
45
|
+
catch (cleanup) {
|
|
46
|
+
const error = failed ? new AggregateError([primary, cleanup], "portable metadata write and staging cleanup failed", { cause: primary })
|
|
47
|
+
: new Error("portable metadata staging cleanup failed", { cause: cleanup });
|
|
48
|
+
error.code = failed ? primary.code : cleanup.code; error.stagingPath = staging; throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function withPortableStateWrite(scope, write) {
|
|
54
|
+
const deployment = portableScope(scope), root = portableStateDirectory(deployment, true), guard = join(root, ".write-guard");
|
|
55
|
+
try { mkdirSync(guard, { mode: 0o700 }); }
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code === "EEXIST") throw oatsError("selection-changed", "portable state has another writer or a held recovery guard");
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const owned = lstatSync(guard), context = Object.freeze({ deployment, root });
|
|
61
|
+
activeWrites.add(context);
|
|
62
|
+
let failed = false, primary;
|
|
63
|
+
try { return write(context); }
|
|
64
|
+
catch (error) { failed = true; primary = error; throw error; }
|
|
65
|
+
finally {
|
|
66
|
+
activeWrites.delete(context);
|
|
67
|
+
try {
|
|
68
|
+
const current = lstatSync(guard);
|
|
69
|
+
if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError("selection-changed", "portable write guard ownership changed");
|
|
70
|
+
// Empty owned guard only; never force-remove a foreign/stale guard tree.
|
|
71
|
+
rmdirSync(guard);
|
|
72
|
+
} catch (cleanup) {
|
|
73
|
+
const error = failed ? new AggregateError([primary, cleanup], "portable state write and guard cleanup failed", { cause: primary })
|
|
74
|
+
: new Error("portable write guard cleanup failed", { cause: cleanup });
|
|
75
|
+
error.code = failed ? primary.code : cleanup.code;
|
|
76
|
+
error.guardPath = guard;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/** Bounded data codecs shared by portable declarations and retained records.
|
|
2
|
+
* No source resolution, filesystem access, getters, or caller serialization hooks. */
|
|
3
|
+
import { oatsError } from "./errors.mjs";
|
|
4
|
+
|
|
5
|
+
const DEFAULTS = Object.freeze({ maxBytes: 8 * 1024 * 1024, maxDepth: 64, maxEntries: 100_000 });
|
|
6
|
+
export function dataLimits(options = {}) {
|
|
7
|
+
const limits = { ...DEFAULTS, ...options };
|
|
8
|
+
for (const key of Object.keys(DEFAULTS)) {
|
|
9
|
+
if (!Number.isSafeInteger(limits[key]) || limits[key] < 1) {
|
|
10
|
+
throw oatsError("invalid-declaration", `invalid data limit: ${key}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (limits.maxDepth > 256) throw oatsError("invalid-declaration", "maximum supported data depth is 256");
|
|
14
|
+
return limits;
|
|
15
|
+
}
|
|
16
|
+
export function scalarString(value, where = "value") {
|
|
17
|
+
if (typeof value !== "string" || Buffer.from(value, "utf8").toString("utf8") !== value) {
|
|
18
|
+
throw oatsError("invalid-declaration", `expected Unicode-scalar string at ${where}`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
const typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
|
|
23
|
+
const byteLengthOf = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength").get;
|
|
24
|
+
const byteOffsetOf = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteOffset").get;
|
|
25
|
+
const bufferOf = Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer").get;
|
|
26
|
+
|
|
27
|
+
/** Intrinsic getters bypass caller-owned byteLength/buffer accessors. Pass an
|
|
28
|
+
* undecorated view to platform codecs, rather than the caller's decorated object. */
|
|
29
|
+
export function byteView(input, where = "input") {
|
|
30
|
+
if (!(input instanceof Uint8Array)) throw oatsError("invalid-declaration", `expected UTF-8 bytes at ${where}`);
|
|
31
|
+
try { return new Uint8Array(bufferOf.call(input), byteOffsetOf.call(input), byteLengthOf.call(input)); }
|
|
32
|
+
catch { throw oatsError("invalid-declaration", `invalid byte view at ${where}`); }
|
|
33
|
+
}
|
|
34
|
+
export function decodeUtf8(input, where = "input") {
|
|
35
|
+
if (typeof input === "string") return scalarString(input, where);
|
|
36
|
+
const bytes = byteView(input, where);
|
|
37
|
+
try { return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); }
|
|
38
|
+
catch { throw oatsError("invalid-declaration", `invalid UTF-8 at ${where}`); }
|
|
39
|
+
}
|
|
40
|
+
export const compareUtf8 = (a, b) => Buffer.compare(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
41
|
+
|
|
42
|
+
/** Freeze a bounded JSON observation, never caller accessors or cyclic objects. */
|
|
43
|
+
export function freezeJson(value) {
|
|
44
|
+
canonicalJson(value);
|
|
45
|
+
const seen = new WeakSet();
|
|
46
|
+
const freeze = (item) => {
|
|
47
|
+
if (!item || typeof item !== "object" || seen.has(item)) return;
|
|
48
|
+
seen.add(item); for (const child of Object.values(item)) freeze(child); Object.freeze(item);
|
|
49
|
+
};
|
|
50
|
+
freeze(value); return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Canonical JSON includes exactly one final LF. Emit sorted keys directly:
|
|
54
|
+
* JSON.stringify on a rebuilt object would reorder integer-looking keys. */
|
|
55
|
+
export function canonicalJson(value, options) {
|
|
56
|
+
const limits = dataLimits(options), chunks = [], active = new WeakSet();
|
|
57
|
+
let bytes = 0, entries = 0;
|
|
58
|
+
const emit = (text) => {
|
|
59
|
+
bytes += Buffer.byteLength(text, "utf8");
|
|
60
|
+
if (bytes > limits.maxBytes) throw oatsError("resource-limit", "canonical JSON byte limit exceeded");
|
|
61
|
+
chunks.push(text);
|
|
62
|
+
};
|
|
63
|
+
const visit = (item, depth) => {
|
|
64
|
+
if (depth > limits.maxDepth || ++entries > limits.maxEntries) {
|
|
65
|
+
throw oatsError("resource-limit", "canonical JSON depth/entry limit exceeded");
|
|
66
|
+
}
|
|
67
|
+
if (item === null || typeof item === "boolean") { emit(JSON.stringify(item)); return; }
|
|
68
|
+
if (typeof item === "string") { emit(JSON.stringify(scalarString(item))); return; }
|
|
69
|
+
if (typeof item === "number" && Number.isFinite(item)) { emit(JSON.stringify(item)); return; }
|
|
70
|
+
if (typeof item !== "object") throw oatsError("invalid-declaration", "value is not finite JSON data");
|
|
71
|
+
if (active.has(item)) throw oatsError("invalid-declaration", "cyclic JSON data");
|
|
72
|
+
const array = Array.isArray(item), proto = Object.getPrototypeOf(item);
|
|
73
|
+
if (proto !== (array ? Array.prototype : Object.prototype) && !(proto === null && !array)) {
|
|
74
|
+
throw oatsError("invalid-declaration", "JSON data must use plain objects and arrays");
|
|
75
|
+
}
|
|
76
|
+
const descriptors = Object.getOwnPropertyDescriptors(item), keys = Reflect.ownKeys(descriptors);
|
|
77
|
+
if (keys.length > limits.maxEntries - entries + (array ? 1 : 0)) {
|
|
78
|
+
throw oatsError("resource-limit", "canonical JSON entry limit exceeded");
|
|
79
|
+
}
|
|
80
|
+
for (const key of keys) {
|
|
81
|
+
if (typeof key !== "string") throw oatsError("invalid-declaration", "symbol keys are not JSON data");
|
|
82
|
+
scalarString(key, "object key");
|
|
83
|
+
const descriptor = descriptors[key];
|
|
84
|
+
if (!Object.hasOwn(descriptor, "value") || (!descriptor.enumerable && !(array && key === "length"))) {
|
|
85
|
+
throw oatsError("invalid-declaration", "accessor/non-enumerable properties are not JSON data");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
active.add(item);
|
|
89
|
+
try {
|
|
90
|
+
if (array) {
|
|
91
|
+
const length = descriptors.length.value;
|
|
92
|
+
if (length > limits.maxEntries - entries) throw oatsError("resource-limit", "canonical JSON array limit exceeded");
|
|
93
|
+
if (keys.length !== length + 1) throw oatsError("invalid-declaration", "sparse or extended arrays are not JSON data");
|
|
94
|
+
emit("[");
|
|
95
|
+
for (let i = 0; i < length; i++) {
|
|
96
|
+
if (!Object.hasOwn(descriptors, String(i))) throw oatsError("invalid-declaration", "sparse or extended arrays are not JSON data");
|
|
97
|
+
if (i) emit(",");
|
|
98
|
+
visit(descriptors[i].value, depth + 1);
|
|
99
|
+
}
|
|
100
|
+
emit("]");
|
|
101
|
+
} else {
|
|
102
|
+
emit("{");
|
|
103
|
+
keys.sort(compareUtf8).forEach((key, index) => {
|
|
104
|
+
if (index) emit(",");
|
|
105
|
+
emit(JSON.stringify(key)); emit(":"); visit(descriptors[key].value, depth + 1);
|
|
106
|
+
});
|
|
107
|
+
emit("}");
|
|
108
|
+
}
|
|
109
|
+
} finally { active.delete(item); }
|
|
110
|
+
};
|
|
111
|
+
visit(value, 0); emit("\n");
|
|
112
|
+
return chunks.join("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** JSON.parse alone cannot detect duplicate decoded keys. This small JSON
|
|
116
|
+
* decoder preserves that check and budgets; YAML must use it for JSON input,
|
|
117
|
+
* not maintain another permissive JSON reader. Objects have null prototypes. */
|
|
118
|
+
export function parseStrictJson(input, options) {
|
|
119
|
+
const limits = dataLimits(options);
|
|
120
|
+
const data = typeof input === "string" ? input : byteView(input);
|
|
121
|
+
if ((typeof data === "string" ? Buffer.byteLength(data) : byteLengthOf.call(data)) > limits.maxBytes) {
|
|
122
|
+
throw oatsError("resource-limit", "JSON input byte limit exceeded");
|
|
123
|
+
}
|
|
124
|
+
const text = decodeUtf8(data);
|
|
125
|
+
let at = 0, entries = 0;
|
|
126
|
+
const bad = () => { throw oatsError("invalid-declaration", `invalid JSON at character ${at}`); };
|
|
127
|
+
const space = () => { while (at < text.length && /[\x20\t\r\n]/.test(text[at])) at++; };
|
|
128
|
+
const string = () => {
|
|
129
|
+
const start = at++;
|
|
130
|
+
while (at < text.length) {
|
|
131
|
+
const char = text[at++];
|
|
132
|
+
if (char === "\\") { at++; continue; }
|
|
133
|
+
if (char === '"') {
|
|
134
|
+
let value;
|
|
135
|
+
try { value = JSON.parse(text.slice(start, at)); } catch { bad(); }
|
|
136
|
+
return scalarString(value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
bad();
|
|
140
|
+
};
|
|
141
|
+
const value = (depth) => {
|
|
142
|
+
if (depth > limits.maxDepth || ++entries > limits.maxEntries) {
|
|
143
|
+
throw oatsError("resource-limit", "JSON depth/entry limit exceeded");
|
|
144
|
+
}
|
|
145
|
+
space();
|
|
146
|
+
const char = text[at];
|
|
147
|
+
if (char === '"') return string();
|
|
148
|
+
if (char === "{" || char === "[") {
|
|
149
|
+
const array = char === "[", result = array ? [] : Object.create(null), close = array ? "]" : "}";
|
|
150
|
+
at++; space();
|
|
151
|
+
if (text[at] === close) { at++; return result; }
|
|
152
|
+
for (;;) {
|
|
153
|
+
if (array) result.push(value(depth + 1));
|
|
154
|
+
else {
|
|
155
|
+
if (text[at] !== '"') bad();
|
|
156
|
+
const key = string(); space();
|
|
157
|
+
if (Object.hasOwn(result, key)) throw oatsError("invalid-declaration", `duplicate JSON key at character ${at}`);
|
|
158
|
+
if (text[at++] !== ":") bad();
|
|
159
|
+
result[key] = value(depth + 1);
|
|
160
|
+
}
|
|
161
|
+
space();
|
|
162
|
+
if (text[at] === close) { at++; return result; }
|
|
163
|
+
if (text[at++] !== ",") bad();
|
|
164
|
+
space();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const [token, decoded] of [["true", true], ["false", false], ["null", null]]) {
|
|
168
|
+
if (text.startsWith(token, at)) { at += token.length; return decoded; }
|
|
169
|
+
}
|
|
170
|
+
const number = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(text.slice(at));
|
|
171
|
+
if (number) {
|
|
172
|
+
at += number[0].length;
|
|
173
|
+
const result = Number(number[0]);
|
|
174
|
+
if (Number.isFinite(result)) return result;
|
|
175
|
+
}
|
|
176
|
+
bad();
|
|
177
|
+
};
|
|
178
|
+
const result = value(0); space();
|
|
179
|
+
if (at !== text.length) bad();
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** Explicit source-to-record preparation. Current locks are inputs to NEW work
|
|
2
|
+
* only; source/package observations are frozen once and never dispatch authority. */
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { objectAt } from "./portable-shape.mjs";
|
|
5
|
+
import { canonicalJson, compareUtf8 } from "./portable-values.mjs";
|
|
6
|
+
import { readPortableBytes } from "./portable-files.mjs";
|
|
7
|
+
import { validateOrigin } from "./resolution-shape.mjs";
|
|
8
|
+
import { createWorkspaceDiscovery } from "./workspace-discovery.mjs";
|
|
9
|
+
import { parseSoulImport } from "./workspace-definition.mjs";
|
|
10
|
+
import { sameIdentity } from "./portable-identity.mjs";
|
|
11
|
+
import { planSoftwareChoices } from "./portable-composition.mjs";
|
|
12
|
+
import { captureManifestSettings } from "./manifest-settings.mjs";
|
|
13
|
+
import { bindingDeclaration, operatorBindingDeclaration, prepareProviderBindings } from "./prepared-bindings.mjs";
|
|
14
|
+
import { preparePackageArtifacts } from "./portable-package-preparation.mjs";
|
|
15
|
+
import { parseLockedSource3, parseRepositorySource } from "./source-spec.mjs";
|
|
16
|
+
import { retainPortableArtifact, verifyPortableArtifact } from "./portable-artifacts.mjs";
|
|
17
|
+
import { treeIntegrity } from "./portable-digest.mjs";
|
|
18
|
+
import { readLock3, writeLock3, emptyLock3, artifactSetKey3, requestKey3 } from "./portable-lock.mjs";
|
|
19
|
+
import { readApprovalLedger, evaluateCapturedApprovals } from "./artifact-approvals.mjs";
|
|
20
|
+
import { commitCapturedResolution } from "./captured-resolutions.mjs";
|
|
21
|
+
import { oatsError } from "./errors.mjs";
|
|
22
|
+
|
|
23
|
+
// Project an ALREADY RESOLVED package graph for lock-v3's per-root set. This
|
|
24
|
+
// performs no dependency parsing, source lookup, acquisition or version choice.
|
|
25
|
+
function packageSubset(artifacts, root) {
|
|
26
|
+
const ids = new Set([root]), pending = [root];
|
|
27
|
+
while (pending.length) for (const id of artifacts.packages[pending.pop()].dependencies) if (!ids.has(id)) { ids.add(id); pending.push(id); }
|
|
28
|
+
return { schemaVersion: 1, packages: Object.fromEntries([...ids].sort(compareUtf8).map((id) => [id, artifacts.packages[id]])),
|
|
29
|
+
capabilities: Object.fromEntries(Object.entries(artifacts.capabilities).filter(([, row]) => row.origin.kind === "package" && ids.has(row.origin.package))) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Repositories and kernel callbacks belong to this operation; the caller owns
|
|
33
|
+
* their lifetime/scratch cleanup. No hook, launch, enrollment or approval here. */
|
|
34
|
+
export function prepareComposition(input, { repositories, kernel, previous: suppliedPrevious }) {
|
|
35
|
+
canonicalJson(input);
|
|
36
|
+
objectAt(input, ["deployment", "directory", "source", "origin", "workspace", "member", "operator", "mode", "allowLocalPaths", "standaloneContextKey", "launch", "helperLaunches"], ["deployment", "directory", "source", "origin"]);
|
|
37
|
+
const { deployment, directory, source: requested, origin, workspace: workspaceRequest, member, operator, allowLocalPaths = false } = input;
|
|
38
|
+
const hasWorkspace = Object.hasOwn(input, "workspace");
|
|
39
|
+
const hasStandaloneContext = Object.hasOwn(input, "standaloneContextKey"), standaloneContextKey = input.standaloneContextKey;
|
|
40
|
+
if (hasWorkspace && hasStandaloneContext) throw oatsError("invalid-declaration", "workspace and standaloneContextKey are mutually exclusive");
|
|
41
|
+
if (hasStandaloneContext && standaloneContextKey !== null
|
|
42
|
+
&& (typeof standaloneContextKey !== "string" || !standaloneContextKey || standaloneContextKey.includes("\0") || Buffer.byteLength(standaloneContextKey, "utf8") > 256)) {
|
|
43
|
+
throw oatsError("invalid-declaration", "standaloneContextKey must be explicit null or non-empty opaque text up to 256 bytes");
|
|
44
|
+
}
|
|
45
|
+
if (typeof directory !== "string" || !isAbsolute(directory) || typeof allowLocalPaths !== "boolean") throw oatsError("invalid-declaration", "preparation needs an explicit scratch directory and local-input policy");
|
|
46
|
+
validateOrigin(origin);
|
|
47
|
+
const previous = suppliedPrevious ?? readLock3(deployment); // Public wrapper reads once BEFORE creating scratch or fetching.
|
|
48
|
+
const discovery = createWorkspaceDiscovery(repositories);
|
|
49
|
+
const workspace = hasWorkspace ? discovery.readWorkspace(workspaceRequest) : null;
|
|
50
|
+
let reference = requested, sourceOrigin = origin;
|
|
51
|
+
if (typeof requested === "string") {
|
|
52
|
+
const index = workspace?.parsed.imports.findIndex((entry) => entry.alias === requested) ?? -1;
|
|
53
|
+
if (index < 0) throw oatsError("export-not-found", "requested alias is not advertised by this workspace");
|
|
54
|
+
reference = workspace.parsed.imports[index];
|
|
55
|
+
sourceOrigin = { ...workspace.parsed.origins[`/imports/${index}`], kind: "import-adoption" };
|
|
56
|
+
}
|
|
57
|
+
if (member) {
|
|
58
|
+
if (!workspace) throw oatsError("needs-configuration", "member work needs an explicit workspace");
|
|
59
|
+
const membership = discovery.checkMember(workspace, member);
|
|
60
|
+
if (membership.status !== "eligible") return { status: "needs-configuration", resolution: null, problems: membership.problems };
|
|
61
|
+
}
|
|
62
|
+
const imported = discovery.importSoul(reference, { origin: sourceOrigin });
|
|
63
|
+
const mode = input.mode ?? imported.soul.declaration.work ?? "directory";
|
|
64
|
+
const adoptions = typeof requested !== "string" && imported.adoption ? [imported.adoption] : [];
|
|
65
|
+
// Adoption is keyed to qualified source identity, not whichever alias the
|
|
66
|
+
// caller typed. Conflicting aliases must not become a way to evade policy.
|
|
67
|
+
for (const [index, entry] of (workspace?.parsed.imports ?? []).entries()) {
|
|
68
|
+
if (!entry.adoption || entry.soul !== imported.identity.exportPath) continue;
|
|
69
|
+
const candidate = repositories.identify(entry.source);
|
|
70
|
+
if (!sameIdentity(candidate.identity, imported.identity.repository)) continue;
|
|
71
|
+
const pointer = `/imports/${index}`, origins = workspace.parsed.origins;
|
|
72
|
+
adoptions.push({ identity: imported.identity, pointer, origins,
|
|
73
|
+
parsed: parseSoulImport(entry, { pointer, origins, origin: origins[pointer].document }) });
|
|
74
|
+
}
|
|
75
|
+
let plan = planSoftwareChoices({ identity: imported.identity, soul: imported.soul,
|
|
76
|
+
...(workspace ? { workspace: workspace.parsed } : {}), adoptions, ...(operator ? { operator } : {}) });
|
|
77
|
+
if (plan.status !== "resolved") return { status: plan.status, resolution: null, problems: plan.problems };
|
|
78
|
+
const requests = Object.entries(plan.capabilities).map(([capability, selected]) => ({ capability, source: selected.source, origin: plan.choices[selected.choiceKeys[0]].selectedBy }));
|
|
79
|
+
let prepared, primary;
|
|
80
|
+
try {
|
|
81
|
+
prepared = requests.length ? preparePackageArtifacts({ requests, deployment, directory, repositories, kernel,
|
|
82
|
+
observations: [imported.observation], allowLocalPaths, catalog: kernel.catalog })
|
|
83
|
+
: { artifactSet: { schemaVersion: 1, packages: {}, capabilities: {} }, roots: [], cleanup() {} };
|
|
84
|
+
const allArtifacts = prepared.artifactSet, manifests = new Map(), definitions = [];
|
|
85
|
+
for (const [id, row] of Object.entries(allArtifacts.capabilities)) {
|
|
86
|
+
const root = verifyPortableArtifact(deployment, row.artifact).dir;
|
|
87
|
+
manifests.set(id, kernel.manifest(root));
|
|
88
|
+
if (Object.hasOwn(plan.capabilities, id)) definitions.push({ artifact: row.artifact, bytes: readPortableBytes(join(root, "oats.json")) });
|
|
89
|
+
}
|
|
90
|
+
plan = captureManifestSettings(plan, definitions);
|
|
91
|
+
const artifacts = { ...allArtifacts, capabilities: Object.fromEntries(Object.entries(allArtifacts.capabilities).filter(([id]) => Object.hasOwn(plan.capabilities, id))) };
|
|
92
|
+
const roots = new Set(imported.roots);
|
|
93
|
+
for (const pkg of Object.values(allArtifacts.packages)) {
|
|
94
|
+
const selected = parseLockedSource3(pkg.source, pkg.path);
|
|
95
|
+
if (selected.kind === "git" && pkg.commit === imported.observation.source.commit && parseRepositorySource(`git:${selected.url}`).normalized === imported.observation.source.remote) roots.add(pkg.path);
|
|
96
|
+
}
|
|
97
|
+
const projectionRoots = [...roots].sort(compareUtf8), sourceDirectory = join(directory, "soul-projection");
|
|
98
|
+
repositories.materialize(imported.observation, projectionRoots, sourceDirectory);
|
|
99
|
+
const sourceArtifact = { kind: "soul", identity: imported.identity, integrity: treeIntegrity(sourceDirectory) };
|
|
100
|
+
retainPortableArtifact(deployment, sourceDirectory, sourceArtifact);
|
|
101
|
+
const soul = { identity: imported.identity, alias: imported.reference.alias, revision: imported.observation.source,
|
|
102
|
+
sourceArtifact, definition: imported.definition, projection: { roots: projectionRoots } };
|
|
103
|
+
const context = workspace ? { kind: "workspace", identity: workspace.identity,
|
|
104
|
+
observation: { ...workspace.parsed.origins[""], kind: "workspace-default" } }
|
|
105
|
+
: { kind: "standalone", key: hasStandaloneContext ? standaloneContextKey : null };
|
|
106
|
+
let seed = { schemaVersion: 1, capture: "prepared", subject: { kind: "persistent", soul }, context, artifacts,
|
|
107
|
+
choices: plan.choices, evidence: imported.provenance };
|
|
108
|
+
const declarations = [bindingDeclaration("soul", imported.soul.declaration, imported.soul.origins)];
|
|
109
|
+
if (workspace) declarations.push(bindingDeclaration("workspace", workspace.parsed.declaration, workspace.parsed.origins));
|
|
110
|
+
for (const adoption of adoptions) {
|
|
111
|
+
const value = adoption.parsed.reference.adoption;
|
|
112
|
+
if (value) declarations.push(bindingDeclaration("adoption", value, adoption.origins, `${adoption.pointer}/adoption`));
|
|
113
|
+
}
|
|
114
|
+
if (operator) declarations.push(operatorBindingDeclaration(operator));
|
|
115
|
+
const bound = prepareProviderBindings({ seed, plan, manifests, declarations }, options => kernel.binding({ ...options, deployment }));
|
|
116
|
+
plan = bound.plan; seed = bound.seed;
|
|
117
|
+
const completion = bound.problems.length ? { record: null, problems: bound.problems }
|
|
118
|
+
: kernel.complete({ seed, plan, manifests, mode, deployment, directory, launch: input.launch, helperLaunches: input.helperLaunches });
|
|
119
|
+
let resolution = null;
|
|
120
|
+
if (completion.record) resolution = commitCapturedResolution(deployment, completion.record);
|
|
121
|
+
// Publishing valid immutable inputs/records need not roll back on a later
|
|
122
|
+
// selection CAS loss. Never delete another transaction's retained objects.
|
|
123
|
+
const next = previous.lock ? structuredClone(previous.lock) : emptyLock3();
|
|
124
|
+
const { ledger } = readApprovalLedger(deployment), selections = [];
|
|
125
|
+
for (const root of new Set(prepared.roots)) {
|
|
126
|
+
const set = packageSubset(allArtifacts, root), key = artifactSetKey3(set), pkg = set.packages[root];
|
|
127
|
+
const request = { source: pkg.source, path: pkg.path }, requestKey = requestKey3(request);
|
|
128
|
+
const approval = evaluateCapturedApprovals({ record: { artifacts: set }, manifests }, ledger);
|
|
129
|
+
const usable = approval.every((entry) => entry.status !== "approval-required");
|
|
130
|
+
next.artifactSets[key] = set;
|
|
131
|
+
next.selections[requestKey] = { request, current: usable ? key : next.selections[requestKey]?.current ?? null, available: key,
|
|
132
|
+
freshness: { state: "refreshed", observedAt: new Date().toISOString(), problems: [] } };
|
|
133
|
+
selections.push({ request, artifactSet: key, approvalRequired: approval.filter((entry) => entry.status === "approval-required").map((entry) => entry.artifact.capability) });
|
|
134
|
+
}
|
|
135
|
+
writeLock3(deployment, previous.integrity, next);
|
|
136
|
+
const approvals = evaluateCapturedApprovals({ record: { artifacts }, manifests }, ledger);
|
|
137
|
+
return { status: resolution ? (approvals.some((entry) => entry.status === "approval-required") ? "approval-required" : "prepared") : completion.problems?.some(problem => problem.code === "requirement-conflict") ? "conflict" : "needs-configuration",
|
|
138
|
+
resolution, executionBinding: resolution ? { schemaVersion: 1, deployment, resolution } : null,
|
|
139
|
+
...(resolution ? { responsibleHuman: completion.record.messagingChoice.enabled ? completion.record.messagingChoice.privateKey.human : null } : {}),
|
|
140
|
+
source: { source: imported.reference.source, soul: imported.reference.soul, revision: imported.observation.source.commit, alias: imported.reference.alias },
|
|
141
|
+
selections, problems: completion.problems ?? [] };
|
|
142
|
+
} catch (error) { primary = error; throw error; }
|
|
143
|
+
finally {
|
|
144
|
+
try { prepared?.cleanup(); }
|
|
145
|
+
catch (cleanup) {
|
|
146
|
+
if (!primary) throw cleanup;
|
|
147
|
+
const error = new AggregateError([primary, cleanup], "preparation and package cleanup failed", { cause: primary });
|
|
148
|
+
error.code = primary.code; throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Provider composition over one frozen source/workspace observation. Provider
|
|
2
|
+
* codecs normalize fields; only resolveChoices selects their values. */
|
|
3
|
+
import { canonicalJson } from './portable-values.mjs';
|
|
4
|
+
import { resolveChoices } from './portable-choices.mjs';
|
|
5
|
+
import { validateOrigin } from './resolution-shape.mjs';
|
|
6
|
+
import { pointerKey } from './portable-shape.mjs';
|
|
7
|
+
import { bindingField, bindingOriginWitnessed } from './provider-binding-wire.mjs';
|
|
8
|
+
import { oatsError } from './errors.mjs';
|
|
9
|
+
|
|
10
|
+
/** Remap dictionary keys for an adoption subtree, but retain original document
|
|
11
|
+
* pointers/spans inside each witness. No file/config is read here. */
|
|
12
|
+
export function bindingDeclaration(kind,value,origins,pointer='') {
|
|
13
|
+
const role={soul:'soul-requirement',workspace:'workspace-default',adoption:'import-adoption',operator:'operator'}[kind];
|
|
14
|
+
if (!role || !origins?.[pointer]) throw oatsError('invalid-declaration','binding input has no original declaration witness');
|
|
15
|
+
const mapped=Object.create(null);
|
|
16
|
+
for (const [key,origin] of Object.entries(origins)) {
|
|
17
|
+
if (key !== pointer && !key.startsWith(`${pointer}/`)) continue;
|
|
18
|
+
const witnessed={...origin,kind:role};validateOrigin(witnessed);
|
|
19
|
+
mapped[key.slice(pointer.length)]=witnessed;
|
|
20
|
+
}
|
|
21
|
+
return {kind,value,origin:mapped[''],origins:mapped};
|
|
22
|
+
}
|
|
23
|
+
export function operatorBindingDeclaration(operator) {
|
|
24
|
+
canonicalJson(operator);
|
|
25
|
+
const origins=Object.create(null);
|
|
26
|
+
const mark=(value,pointer)=>{
|
|
27
|
+
origins[pointer]={kind:'operator',document:operator.document,pointer};
|
|
28
|
+
if (value && typeof value === 'object') for (const [key,child] of Object.entries(value)) mark(child,`${pointer}/${pointerKey(key)}`);
|
|
29
|
+
};
|
|
30
|
+
mark(operator,'');
|
|
31
|
+
return bindingDeclaration('operator',operator,origins);
|
|
32
|
+
}
|
|
33
|
+
const same=(a,b)=>canonicalJson(a)===canonicalJson(b);
|
|
34
|
+
const safeCodes=new Set(['approval-required','needs-configuration','requirement-conflict','invalid-binding','invalid-binding-output',
|
|
35
|
+
'authorization-required','host-requirement-missing','provider-unavailable','provider-not-qualified','resource-not-found','integrity-drift']);
|
|
36
|
+
export function prepareProviderBindings({seed,plan,manifests,declarations},invoke) {
|
|
37
|
+
const normalized=new Map(),problems=[],requirements=[...plan.requirements],candidates=[...plan.candidates];
|
|
38
|
+
const settingsFor=id=>Object.fromEntries(Object.entries(plan.settings[id] ?? {}).map(([name,key])=>[name,plan.choices[key].value]));
|
|
39
|
+
const run=(id,phase,input)=>invoke({artifacts:seed.artifacts,capability:id,phase,settings:settingsFor(id),input});
|
|
40
|
+
const refuse=error=>({code:safeCodes.has(error?.code)?error.code:'provider-not-qualified',message:'provider binding could not be prepared',origins:[]});
|
|
41
|
+
for (const [id,manifest] of manifests) {
|
|
42
|
+
if (!Object.hasOwn(seed.artifacts.capabilities,id) || !manifest.layer) continue;
|
|
43
|
+
if (plan.providers[manifest.layer] !== id) problems.push({code:'needs-configuration',message:'fundamental capability must be selected in its own layer',origins:[]});
|
|
44
|
+
}
|
|
45
|
+
if (problems.length) return {plan,seed,problems};
|
|
46
|
+
for (const [slot,id] of Object.entries(plan.providers)) {
|
|
47
|
+
if (id === null) continue;
|
|
48
|
+
const manifest=manifests.get(id);
|
|
49
|
+
if (manifest?.layer !== slot || !manifest.binding) { problems.push({code:'provider-not-qualified',message:'selected provider has no matching binding interface',origins:[]});continue; }
|
|
50
|
+
try {
|
|
51
|
+
const value=run(id,'normalize',{declarations,context:seed.context});
|
|
52
|
+
normalized.set(slot,{id,value}); requirements.push(...value.requirements);candidates.push(...value.candidates);
|
|
53
|
+
} catch(error) { problems.push(refuse(error)); }
|
|
54
|
+
}
|
|
55
|
+
if (problems.length) return {plan,seed,problems};
|
|
56
|
+
const resolved=resolveChoices({requirements,candidates});
|
|
57
|
+
if (resolved.status !== 'resolved') return {plan:{...plan,...resolved},seed,problems:resolved.problems};
|
|
58
|
+
const nextPlan={...plan,...resolved,requirements,candidates},bindings=Object.create(null);
|
|
59
|
+
let messagingChoice={schemaVersion:1,enabled:false};
|
|
60
|
+
const known=[];
|
|
61
|
+
for (const choice of Object.values(resolved.choices)) {
|
|
62
|
+
if (choice.selectedBy) known.push(choice.selectedBy);
|
|
63
|
+
for (const item of [...choice.constraints,...choice.considered]) known.push(item.origin);
|
|
64
|
+
}
|
|
65
|
+
const witnessed=origin=>bindingOriginWitnessed(origin,declarations) || known.some(candidate=>same(candidate,origin));
|
|
66
|
+
for (const [slot,{id,value}] of normalized) {
|
|
67
|
+
try {
|
|
68
|
+
const choices=Object.fromEntries(Object.entries(resolved.choices).filter(([key])=>bindingField(key,slot)));
|
|
69
|
+
const bound=run(id,'bind',{model:value.model,choices,context:seed.context});
|
|
70
|
+
for (const origin of [...bound.binding.provenance,...(bound.messagingChoice?.provenance ?? [])]) {
|
|
71
|
+
if (!witnessed(origin)) throw oatsError('invalid-binding-output','binding provenance is not witnessed by this preparation');
|
|
72
|
+
}
|
|
73
|
+
bindings[slot]=bound.binding;
|
|
74
|
+
if (slot === 'messaging') messagingChoice=bound.messagingChoice;
|
|
75
|
+
} catch(error) { problems.push(refuse(error)); }
|
|
76
|
+
}
|
|
77
|
+
return {plan:nextPlan,seed:{...seed,choices:resolved.choices,bindings,messagingChoice},problems};
|
|
78
|
+
}
|