@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,170 +0,0 @@
|
|
|
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
|
-
/** Origins of the choices that selected a capability — the same derivation preparation
|
|
35
|
-
* bindings use, so completion refusals render beside binding problems. */
|
|
36
|
-
function originsOf(plan, capability) {
|
|
37
|
-
const seen = new Map();
|
|
38
|
-
for (const key of plan.capabilities?.[capability]?.choiceKeys ?? []) {
|
|
39
|
-
const origin = plan.choices?.[key]?.selectedBy;
|
|
40
|
-
if (origin) seen.set(canonicalJson(origin), origin);
|
|
41
|
-
}
|
|
42
|
-
return [...seen.values()];
|
|
43
|
-
}
|
|
44
|
-
export function prepareComposition(input, { repositories, kernel, previous: suppliedPrevious }) {
|
|
45
|
-
canonicalJson(input);
|
|
46
|
-
objectAt(input, ["deployment", "directory", "source", "origin", "workspace", "member", "operator", "mode", "allowLocalPaths", "standaloneContextKey", "launch", "helperLaunches"], ["deployment", "directory", "source", "origin"]);
|
|
47
|
-
const { deployment, directory, source: requested, origin, workspace: workspaceRequest, member, operator, allowLocalPaths = false } = input;
|
|
48
|
-
const hasWorkspace = Object.hasOwn(input, "workspace");
|
|
49
|
-
const hasStandaloneContext = Object.hasOwn(input, "standaloneContextKey"), standaloneContextKey = input.standaloneContextKey;
|
|
50
|
-
if (hasWorkspace && hasStandaloneContext) throw oatsError("invalid-declaration", "workspace and standaloneContextKey are mutually exclusive");
|
|
51
|
-
if (hasStandaloneContext && standaloneContextKey !== null
|
|
52
|
-
&& (typeof standaloneContextKey !== "string" || !standaloneContextKey || standaloneContextKey.includes("\0") || Buffer.byteLength(standaloneContextKey, "utf8") > 256)) {
|
|
53
|
-
throw oatsError("invalid-declaration", "standaloneContextKey must be explicit null or non-empty opaque text up to 256 bytes");
|
|
54
|
-
}
|
|
55
|
-
if (typeof directory !== "string" || !isAbsolute(directory) || typeof allowLocalPaths !== "boolean") throw oatsError("invalid-declaration", "preparation needs an explicit scratch directory and local-input policy");
|
|
56
|
-
validateOrigin(origin);
|
|
57
|
-
const previous = suppliedPrevious ?? readLock3(deployment); // Public wrapper reads once BEFORE creating scratch or fetching.
|
|
58
|
-
const discovery = createWorkspaceDiscovery(repositories);
|
|
59
|
-
const workspace = hasWorkspace ? discovery.readWorkspace(workspaceRequest) : null;
|
|
60
|
-
let reference = requested, sourceOrigin = origin;
|
|
61
|
-
if (typeof requested === "string") {
|
|
62
|
-
const index = workspace?.parsed.imports.findIndex((entry) => entry.alias === requested) ?? -1;
|
|
63
|
-
if (index < 0) throw oatsError("export-not-found", "requested alias is not advertised by this workspace");
|
|
64
|
-
reference = workspace.parsed.imports[index];
|
|
65
|
-
sourceOrigin = { ...workspace.parsed.origins[`/imports/${index}`], kind: "import-adoption" };
|
|
66
|
-
}
|
|
67
|
-
if (member) {
|
|
68
|
-
if (!workspace) throw oatsError("needs-configuration", "member work needs an explicit workspace");
|
|
69
|
-
const membership = discovery.checkMember(workspace, member);
|
|
70
|
-
if (membership.status !== "eligible") return { status: "needs-configuration", resolution: null, problems: membership.problems };
|
|
71
|
-
}
|
|
72
|
-
const imported = discovery.importSoul(reference, { origin: sourceOrigin });
|
|
73
|
-
const mode = input.mode ?? imported.soul.declaration.work ?? "directory";
|
|
74
|
-
const adoptions = typeof requested !== "string" && imported.adoption ? [imported.adoption] : [];
|
|
75
|
-
// Adoption is keyed to qualified source identity, not whichever alias the
|
|
76
|
-
// caller typed. Conflicting aliases must not become a way to evade policy.
|
|
77
|
-
for (const [index, entry] of (workspace?.parsed.imports ?? []).entries()) {
|
|
78
|
-
if (!entry.adoption || entry.soul !== imported.identity.exportPath) continue;
|
|
79
|
-
const candidate = repositories.identify(entry.source);
|
|
80
|
-
if (!sameIdentity(candidate.identity, imported.identity.repository)) continue;
|
|
81
|
-
const pointer = `/imports/${index}`, origins = workspace.parsed.origins;
|
|
82
|
-
adoptions.push({ identity: imported.identity, pointer, origins,
|
|
83
|
-
parsed: parseSoulImport(entry, { pointer, origins, origin: origins[pointer].document }) });
|
|
84
|
-
}
|
|
85
|
-
let plan = planSoftwareChoices({ identity: imported.identity, soul: imported.soul,
|
|
86
|
-
...(workspace ? { workspace: workspace.parsed } : {}), adoptions, ...(operator ? { operator } : {}) });
|
|
87
|
-
if (plan.status !== "resolved") return { status: plan.status, resolution: null, problems: plan.problems };
|
|
88
|
-
const requests = Object.entries(plan.capabilities).map(([capability, selected]) => ({ capability, source: selected.source, origin: plan.choices[selected.choiceKeys[0]].selectedBy }));
|
|
89
|
-
let prepared, primary;
|
|
90
|
-
try {
|
|
91
|
-
prepared = requests.length ? preparePackageArtifacts({ requests, deployment, directory, repositories, kernel,
|
|
92
|
-
observations: [imported.observation], allowLocalPaths, catalog: kernel.catalog })
|
|
93
|
-
: { artifactSet: { schemaVersion: 1, packages: {}, capabilities: {} }, roots: [], cleanup() {} };
|
|
94
|
-
const allArtifacts = prepared.artifactSet, manifests = new Map(), definitions = [];
|
|
95
|
-
for (const [id, row] of Object.entries(allArtifacts.capabilities)) {
|
|
96
|
-
const root = verifyPortableArtifact(deployment, row.artifact).dir;
|
|
97
|
-
manifests.set(id, kernel.manifest(root));
|
|
98
|
-
if (Object.hasOwn(plan.capabilities, id)) definitions.push({ artifact: row.artifact, bytes: readPortableBytes(join(root, "oats.json")) });
|
|
99
|
-
}
|
|
100
|
-
plan = captureManifestSettings(plan, definitions);
|
|
101
|
-
const artifacts = { ...allArtifacts, capabilities: Object.fromEntries(Object.entries(allArtifacts.capabilities).filter(([id]) => Object.hasOwn(plan.capabilities, id))) };
|
|
102
|
-
const roots = new Set(imported.roots);
|
|
103
|
-
for (const pkg of Object.values(allArtifacts.packages)) {
|
|
104
|
-
const selected = parseLockedSource3(pkg.source, pkg.path);
|
|
105
|
-
if (selected.kind === "git" && pkg.commit === imported.observation.source.commit && parseRepositorySource(`git:${selected.url}`).normalized === imported.observation.source.remote) roots.add(pkg.path);
|
|
106
|
-
}
|
|
107
|
-
const projectionRoots = [...roots].sort(compareUtf8), sourceDirectory = join(directory, "soul-projection");
|
|
108
|
-
repositories.materialize(imported.observation, projectionRoots, sourceDirectory);
|
|
109
|
-
const sourceArtifact = { kind: "soul", identity: imported.identity, integrity: treeIntegrity(sourceDirectory) };
|
|
110
|
-
retainPortableArtifact(deployment, sourceDirectory, sourceArtifact);
|
|
111
|
-
const soul = { identity: imported.identity, alias: imported.reference.alias, revision: imported.observation.source,
|
|
112
|
-
sourceArtifact, definition: imported.definition, projection: { roots: projectionRoots } };
|
|
113
|
-
const context = workspace ? { kind: "workspace", identity: workspace.identity,
|
|
114
|
-
observation: { ...workspace.parsed.origins[""], kind: "workspace-default" } }
|
|
115
|
-
: { kind: "standalone", key: hasStandaloneContext ? standaloneContextKey : null };
|
|
116
|
-
let seed = { schemaVersion: 1, capture: "prepared", subject: { kind: "persistent", soul }, context, artifacts,
|
|
117
|
-
choices: plan.choices, evidence: imported.provenance };
|
|
118
|
-
const declarations = [bindingDeclaration("soul", imported.soul.declaration, imported.soul.origins)];
|
|
119
|
-
if (workspace) declarations.push(bindingDeclaration("workspace", workspace.parsed.declaration, workspace.parsed.origins));
|
|
120
|
-
for (const adoption of adoptions) {
|
|
121
|
-
const value = adoption.parsed.reference.adoption;
|
|
122
|
-
if (value) declarations.push(bindingDeclaration("adoption", value, adoption.origins, `${adoption.pointer}/adoption`));
|
|
123
|
-
}
|
|
124
|
-
if (operator) declarations.push(operatorBindingDeclaration(operator));
|
|
125
|
-
const bound = prepareProviderBindings({ seed, plan, manifests, declarations }, options => kernel.binding({ ...options, deployment }));
|
|
126
|
-
plan = bound.plan; seed = bound.seed;
|
|
127
|
-
let completion;
|
|
128
|
-
if (bound.problems.length) completion = { record: null, problems: bound.problems };
|
|
129
|
-
else {
|
|
130
|
-
try { completion = kernel.complete({ seed, plan, manifests, mode, deployment, directory, launch: input.launch, helperLaunches: input.helperLaunches }); }
|
|
131
|
-
catch (error) {
|
|
132
|
-
// A typed completion refusal that names its capability is a preparation problem like any
|
|
133
|
-
// other (slot/capability/origins), never a bare top-level error. Anything else propagates.
|
|
134
|
-
if (!Array.isArray(error?.problems) || !["needs-configuration", "requirement-conflict"].includes(error.code)) throw error;
|
|
135
|
-
completion = { record: null, problems: error.problems.map(problem => ({ ...problem, origins: problem.origins ?? originsOf(plan, problem.capability) })) };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
let resolution = null;
|
|
139
|
-
if (completion.record) resolution = commitCapturedResolution(deployment, completion.record);
|
|
140
|
-
// Publishing valid immutable inputs/records need not roll back on a later
|
|
141
|
-
// selection CAS loss. Never delete another transaction's retained objects.
|
|
142
|
-
const next = previous.lock ? structuredClone(previous.lock) : emptyLock3();
|
|
143
|
-
const { ledger } = readApprovalLedger(deployment), selections = [];
|
|
144
|
-
for (const root of new Set(prepared.roots)) {
|
|
145
|
-
const set = packageSubset(allArtifacts, root), key = artifactSetKey3(set), pkg = set.packages[root];
|
|
146
|
-
const request = { source: pkg.source, path: pkg.path }, requestKey = requestKey3(request);
|
|
147
|
-
const approval = evaluateCapturedApprovals({ record: { artifacts: set }, manifests }, ledger);
|
|
148
|
-
const usable = approval.every((entry) => entry.status !== "approval-required");
|
|
149
|
-
next.artifactSets[key] = set;
|
|
150
|
-
next.selections[requestKey] = { request, current: usable ? key : next.selections[requestKey]?.current ?? null, available: key,
|
|
151
|
-
freshness: { state: "refreshed", observedAt: new Date().toISOString(), problems: [] } };
|
|
152
|
-
selections.push({ request, artifactSet: key, approvalRequired: approval.filter((entry) => entry.status === "approval-required").map((entry) => entry.artifact.capability) });
|
|
153
|
-
}
|
|
154
|
-
writeLock3(deployment, previous.integrity, next);
|
|
155
|
-
const approvals = evaluateCapturedApprovals({ record: { artifacts }, manifests }, ledger);
|
|
156
|
-
return { status: resolution ? (approvals.some((entry) => entry.status === "approval-required") ? "approval-required" : "prepared") : completion.problems?.some(problem => problem.code === "requirement-conflict") ? "conflict" : "needs-configuration",
|
|
157
|
-
resolution, executionBinding: resolution ? { schemaVersion: 1, deployment, resolution } : null,
|
|
158
|
-
...(resolution ? { responsibleHuman: completion.record.messagingChoice.enabled ? completion.record.messagingChoice.privateKey.human : null } : {}),
|
|
159
|
-
source: { source: imported.reference.source, soul: imported.reference.soul, revision: imported.observation.source.commit, alias: imported.reference.alias },
|
|
160
|
-
selections, problems: completion.problems ?? [] };
|
|
161
|
-
} catch (error) { primary = error; throw error; }
|
|
162
|
-
finally {
|
|
163
|
-
try { prepared?.cleanup(); }
|
|
164
|
-
catch (cleanup) {
|
|
165
|
-
if (!primary) throw cleanup;
|
|
166
|
-
const error = new AggregateError([primary, cleanup], "preparation and package cleanup failed", { cause: primary });
|
|
167
|
-
error.code = primary.code; throw error;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
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
|
-
import { providerReasons, safeProviderReason } from './provider-reasons.mjs';
|
|
10
|
-
|
|
11
|
-
/** Remap dictionary keys for an adoption subtree, but retain original document
|
|
12
|
-
* pointers/spans inside each witness. No file/config is read here. */
|
|
13
|
-
export function bindingDeclaration(kind,value,origins,pointer='') {
|
|
14
|
-
const role={soul:'soul-requirement',workspace:'workspace-default',adoption:'import-adoption',operator:'operator'}[kind];
|
|
15
|
-
if (!role || !origins?.[pointer]) throw oatsError('invalid-declaration','binding input has no original declaration witness');
|
|
16
|
-
const mapped=Object.create(null);
|
|
17
|
-
for (const [key,origin] of Object.entries(origins)) {
|
|
18
|
-
if (key !== pointer && !key.startsWith(`${pointer}/`)) continue;
|
|
19
|
-
const witnessed={...origin,kind:role};validateOrigin(witnessed);
|
|
20
|
-
mapped[key.slice(pointer.length)]=witnessed;
|
|
21
|
-
}
|
|
22
|
-
return {kind,value,origin:mapped[''],origins:mapped};
|
|
23
|
-
}
|
|
24
|
-
export function operatorBindingDeclaration(operator) {
|
|
25
|
-
canonicalJson(operator);
|
|
26
|
-
const origins=Object.create(null);
|
|
27
|
-
const mark=(value,pointer)=>{
|
|
28
|
-
origins[pointer]={kind:'operator',document:operator.document,pointer};
|
|
29
|
-
if (value && typeof value === 'object') for (const [key,child] of Object.entries(value)) mark(child,`${pointer}/${pointerKey(key)}`);
|
|
30
|
-
};
|
|
31
|
-
mark(operator,'');
|
|
32
|
-
return bindingDeclaration('operator',operator,origins);
|
|
33
|
-
}
|
|
34
|
-
const same=(a,b)=>canonicalJson(a)===canonicalJson(b);
|
|
35
|
-
const safeCodes=new Set(['approval-required','needs-configuration','requirement-conflict','invalid-binding','invalid-binding-output',
|
|
36
|
-
'authorization-required','host-requirement-missing','provider-unavailable','provider-not-qualified','resource-not-found','integrity-drift']);
|
|
37
|
-
export function prepareProviderBindings({seed,plan,manifests,declarations},invoke) {
|
|
38
|
-
const normalized=new Map(),problems=[],requirements=[...plan.requirements],candidates=[...plan.candidates];
|
|
39
|
-
const settingsFor=id=>Object.fromEntries(Object.entries(plan.settings[id] ?? {}).map(([name,key])=>[name,plan.choices[key].value]));
|
|
40
|
-
const run=(id,phase,input)=>invoke({artifacts:seed.artifacts,capability:id,phase,settings:settingsFor(id),input});
|
|
41
|
-
const originsFor=id=>[...new Map((plan.capabilities[id]?.choiceKeys ?? []).flatMap(key=>{
|
|
42
|
-
const choice=plan.choices[key];return choice?.selectedBy?[choice.selectedBy]:[];
|
|
43
|
-
}).map(origin=>[canonicalJson(origin),origin])).values()];
|
|
44
|
-
const problem=(slot,id,code,message,origins=originsFor(id))=>({code,message,origins,slot,capability:id});
|
|
45
|
-
const refuse=(slot,id,phase,error)=>problem(slot,id,safeCodes.has(error?.code)?error.code:'provider-not-qualified',
|
|
46
|
-
safeProviderReason(error?.message,providerReasons(manifests.get(id))) ?? `${id} ${slot} ${phase} binding could not be prepared`); // Recheck fixed text; never arbitrary exceptions.
|
|
47
|
-
for (const [id,manifest] of manifests) {
|
|
48
|
-
if (!Object.hasOwn(seed.artifacts.capabilities,id) || !manifest.layer) continue;
|
|
49
|
-
if (plan.providers[manifest.layer] !== id) problems.push(problem(manifest.layer,id,'needs-configuration','fundamental capability must be selected in its own layer'));
|
|
50
|
-
}
|
|
51
|
-
for (const [slot,id] of Object.entries(plan.providers)) {
|
|
52
|
-
if (id === null) continue;
|
|
53
|
-
const manifest=manifests.get(id);
|
|
54
|
-
if (manifest?.layer !== slot) { problems.push(problem(slot,id,'provider-not-qualified',`${id} does not declare the selected ${slot} layer`));continue; }
|
|
55
|
-
if (!manifest.binding) { problems.push(problem(slot,id,'provider-not-qualified',`${id}@${manifest.version} declares no binding interface; ${slot} cannot be prepared`));continue; }
|
|
56
|
-
try {
|
|
57
|
-
const value=run(id,'normalize',{declarations,context:seed.context});
|
|
58
|
-
normalized.set(slot,{id,value}); requirements.push(...value.requirements);candidates.push(...value.candidates);
|
|
59
|
-
} catch(error) { problems.push(refuse(slot,id,'normalize',error)); }
|
|
60
|
-
}
|
|
61
|
-
// One unsupported slot must not mask another slot's normalized requirements
|
|
62
|
-
// or invalid choices. The same resolver still decides every value.
|
|
63
|
-
const resolved=resolveChoices({requirements,candidates});
|
|
64
|
-
for (const item of resolved.problems) {
|
|
65
|
-
const slot=Object.keys(plan.providers).find(slot=>bindingField(item.key,slot));
|
|
66
|
-
if (!slot) throw oatsError('invalid-binding-output','provider choice problem has no owned binding slot');
|
|
67
|
-
problems.push({...item,slot,capability:plan.providers[slot]});
|
|
68
|
-
}
|
|
69
|
-
const nextPlan={...plan,...resolved,requirements,candidates},bindings=Object.create(null);
|
|
70
|
-
let messagingChoice={schemaVersion:1,enabled:false};
|
|
71
|
-
const known=[];
|
|
72
|
-
for (const choice of Object.values(resolved.choices)) {
|
|
73
|
-
if (choice.selectedBy) known.push(choice.selectedBy);
|
|
74
|
-
for (const item of [...choice.constraints,...choice.considered]) known.push(item.origin);
|
|
75
|
-
}
|
|
76
|
-
const witnessed=origin=>bindingOriginWitnessed(origin,declarations) || known.some(candidate=>same(candidate,origin));
|
|
77
|
-
for (const [slot,{id,value}] of normalized) {
|
|
78
|
-
if (resolved.problems.some(item=>bindingField(item.key,slot))) continue;
|
|
79
|
-
try {
|
|
80
|
-
const choices=Object.fromEntries(Object.entries(resolved.choices).filter(([key])=>bindingField(key,slot)));
|
|
81
|
-
const bound=run(id,'bind',{model:value.model,choices,context:seed.context});
|
|
82
|
-
for (const origin of [...bound.binding.provenance,...(bound.messagingChoice?.provenance ?? [])]) {
|
|
83
|
-
if (!witnessed(origin)) throw oatsError('invalid-binding-output','binding provenance is not witnessed by this preparation');
|
|
84
|
-
}
|
|
85
|
-
bindings[slot]=bound.binding;
|
|
86
|
-
if (slot === 'messaging') messagingChoice=bound.messagingChoice;
|
|
87
|
-
} catch(error) { problems.push(refuse(slot,id,'bind',error)); }
|
|
88
|
-
}
|
|
89
|
-
nextPlan.problems=problems;
|
|
90
|
-
nextPlan.status=problems.some(item=>item.code==='requirement-conflict')?'conflict':problems.length?'needs-configuration':'resolved';
|
|
91
|
-
return {plan:nextPlan,seed:{...seed,choices:resolved.choices,bindings,messagingChoice},problems};
|
|
92
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/** Build retained command/curriculum records from already selected software.
|
|
2
|
-
* Kernel callbacks own existing skill discovery and manifest command semantics. */
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
4
|
-
import { dirname, join, relative, sep } from "node:path";
|
|
5
|
-
import { copyTreeSafe } from "./artifact-tree.mjs";
|
|
6
|
-
import { treeIntegrity, jsonIntegrity } from "./portable-digest.mjs";
|
|
7
|
-
import { retainPortableArtifact, verifyPortableArtifact } from "./portable-artifacts.mjs";
|
|
8
|
-
import { commitCapturedResolution, verifyResolutionInputs } from "./captured-resolutions.mjs";
|
|
9
|
-
import { readPortableBytes } from "./portable-files.mjs";
|
|
10
|
-
import { captureHelperInjectionChoices } from "./helper-injection-policy.mjs";
|
|
11
|
-
import { parseConfigData } from "./config-data.mjs";
|
|
12
|
-
import { oatsError } from "./errors.mjs";
|
|
13
|
-
import { normalizePackagePath } from "./capability-provenance.mjs";
|
|
14
|
-
import { compileCapturedLaunchRequest } from "./captured-launch-request.mjs";
|
|
15
|
-
|
|
16
|
-
export function completePreparedResources({ seed, plan, manifests, mode, deployment, directory, launch, helperLaunches = {} }, kernel) {
|
|
17
|
-
const problems = [];
|
|
18
|
-
for (const [id, manifest] of manifests) {
|
|
19
|
-
if (!Object.hasOwn(seed.artifacts.capabilities, id)) continue;
|
|
20
|
-
if (manifest.layer && seed.bindings?.[manifest.layer]?.capability !== id) problems.push({ code: "provider-not-qualified", message: `provider ${id} needs its binding adapter before complete preparation`, origins: [] });
|
|
21
|
-
const settings = Object.fromEntries(Object.entries(plan.settings[id] ?? {}).map(([name, key]) => [name, plan.choices[key].value]));
|
|
22
|
-
kernel.settings(manifest, settings);
|
|
23
|
-
}
|
|
24
|
-
if (problems.length) return { record: null, problems };
|
|
25
|
-
const snapshot = join(directory, "kernel-resources"); mkdirSync(snapshot); mkdirSync(join(snapshot, "injects"));
|
|
26
|
-
const kernelSkills = ["oats-portable", "oats-portable-artifacts"];
|
|
27
|
-
const workInjection = (workMode) => workMode === "directory" ? "portable-work-directory.md" : `work-${workMode}.md`;
|
|
28
|
-
const injectionFiles = ["oats-portable.md", "portable-instance-boundary.md", ...kernel.workModes.map(workInjection)];
|
|
29
|
-
// Only known kernel resources, never a sweep of the checkout/node_modules,
|
|
30
|
-
// author directories, deployment config or credentials.
|
|
31
|
-
for (const name of kernelSkills) copyTreeSafe(join(kernel.root, "skills", name), join(snapshot, "skills", name));
|
|
32
|
-
for (const file of injectionFiles) copyTreeSafe(join(kernel.root, "injects", file), join(snapshot, "injects", file));
|
|
33
|
-
const bundle = { kind: "resource", integrity: treeIntegrity(snapshot) };
|
|
34
|
-
retainPortableArtifact(deployment, snapshot, bundle);
|
|
35
|
-
const portable = (root, path) => relative(root, path).split(sep).join("/") || ".";
|
|
36
|
-
const effectiveSettings = Object.fromEntries(Object.entries(plan.settings).map(([id, values]) => [id,
|
|
37
|
-
Object.fromEntries(Object.entries(values).map(([name, key]) => [name, plan.choices[key].value]))]));
|
|
38
|
-
const definitions = Object.values(seed.artifacts.capabilities).map(row => ({ artifact: row.artifact,
|
|
39
|
-
bytes: readPortableBytes(join(verifyPortableArtifact(deployment, row.artifact).dir, "oats.json")) }));
|
|
40
|
-
const build = (subject, workMode, helpers, launchRequest) => {
|
|
41
|
-
const helperPolicy = subject.kind === "helper" ? captureHelperInjectionChoices(plan, definitions) : null;
|
|
42
|
-
const resources = Object.create(null), blocks = [], skills = [], names = new Set(), omissions = [];
|
|
43
|
-
const owner = subject.kind === "persistent" ? subject.soul.sourceArtifact : subject.provider;
|
|
44
|
-
const sourceRoot = verifyPortableArtifact(deployment, owner).dir;
|
|
45
|
-
const definition = subject.kind === "persistent" ? subject.soul.definition : subject.definition.path;
|
|
46
|
-
const soulRoot = join(sourceRoot, dirname(definition));
|
|
47
|
-
resources.body = { owner, path: portable(sourceRoot, join(soulRoot, "AGENTS.md")), kind: "file" };
|
|
48
|
-
const inject = (source, owner, path) => {
|
|
49
|
-
const key = `instruction:${source}`; resources[key] = { owner, path, kind: "file" }; blocks.push({ source, resource: key });
|
|
50
|
-
};
|
|
51
|
-
inject("kernel:oats-portable", bundle, "injects/oats-portable.md");
|
|
52
|
-
inject("kernel:instance-boundary", bundle, "injects/portable-instance-boundary.md");
|
|
53
|
-
inject(`work-mode:${workMode}`, bundle, `injects/${workInjection(workMode)}`);
|
|
54
|
-
const skillTree = (owner, root, tree, label, required = true) => {
|
|
55
|
-
const entries = kernel.skills(tree);
|
|
56
|
-
if (required && !entries.length) throw oatsError("resource-not-found", "a declared skill tree contains no readable skill");
|
|
57
|
-
for (const entry of entries) {
|
|
58
|
-
if (names.has(entry.name)) throw oatsError("needs-configuration", `skill ${entry.name} has multiple sources; an explicit override is required`);
|
|
59
|
-
names.add(entry.name);
|
|
60
|
-
const key = `skill:${label}:${entry.name}`; resources[key] = { owner, path: portable(root, entry.src), kind: "skill" };
|
|
61
|
-
skills.push({ name: entry.name, resource: key });
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
const kernelRoot = verifyPortableArtifact(deployment, bundle).dir;
|
|
65
|
-
for (const name of kernelSkills) skillTree(bundle, kernelRoot, join(kernelRoot, "skills", name), "kernel");
|
|
66
|
-
if (existsSync(join(soulRoot, "skills"))) skillTree(owner, sourceRoot, join(soulRoot, "skills"), "soul", false);
|
|
67
|
-
const providerManifests = Object.create(null);
|
|
68
|
-
for (const [id, row] of Object.entries(seed.artifacts.capabilities)) {
|
|
69
|
-
const manifest = manifests.get(id), root = verifyPortableArtifact(deployment, row.artifact).dir;
|
|
70
|
-
const key = `manifest:${id}`; resources[key] = { owner: row.artifact, path: "oats.json", kind: "manifest" }; providerManifests[id] = key;
|
|
71
|
-
for (const path of manifest.skills ?? []) skillTree(row.artifact, root, join(root, path), id);
|
|
72
|
-
if (helperPolicy) {
|
|
73
|
-
const policy = helperPolicy.policies.get(id);
|
|
74
|
-
if (policy.fact) {
|
|
75
|
-
if (policy.resource) {
|
|
76
|
-
resources[policy.resourceKey] = policy.resource;
|
|
77
|
-
blocks.push({ source: policy.source, resource: policy.resourceKey, choice: policy.fact.key });
|
|
78
|
-
} else omissions.push({ source: policy.source, reason: "helper-policy", choice: policy.fact.key });
|
|
79
|
-
}
|
|
80
|
-
} else if (manifest.inject) inject(`capability:${id}`, row.artifact, portable(root, join(root, manifest.inject)));
|
|
81
|
-
for (const [kind, declarations] of [["command", manifest.commands ?? {}], ["hook", kernel.hooks(manifest)]]) {
|
|
82
|
-
for (const [name, value] of Object.entries(declarations)) {
|
|
83
|
-
if (typeof value !== "string" || !value.trim() || value.includes("\0")) throw oatsError("invalid-resolution", "declared executable is not valid text");
|
|
84
|
-
resources[`executable:${id}:${kind}:${name}`] = { owner: row.artifact, path: portable(root, join(root, value.trim().split(/\s+/)[0])), kind: "file" };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const capturedLaunch = compileCapturedLaunchRequest(launchRequest, { artifacts: seed.artifacts, manifests, settings: effectiveSettings, resources }, kernel);
|
|
89
|
-
return { ...seed, subject, choices: helperPolicy?.choices ?? seed.choices, bindings: seed.bindings ?? {}, messagingChoice: seed.messagingChoice ?? { schemaVersion: 1, enabled: false }, resources, resourceBundles: [bundle], helpers,
|
|
90
|
-
dispatch: { schemaVersion: 1, providerManifests, settingsChoices: plan.settings, launch: capturedLaunch, runtimePackages: [], hostRequirements: [], workTargetInputs: {},
|
|
91
|
-
composition: { schemaVersion: 1, mode: workMode, body: "body", blocks, skills, omissions } } };
|
|
92
|
-
};
|
|
93
|
-
// Preflight EVERY helper before publishing any helper record. Until helpers
|
|
94
|
-
// run their own complete provider/software preparation, inheriting a parent's
|
|
95
|
-
// binding over helper-authored policy would create a usable contradiction.
|
|
96
|
-
const helperPlans = [], helperKeys = new Set();
|
|
97
|
-
for (const [id, row] of Object.entries(seed.artifacts.capabilities)) {
|
|
98
|
-
const root = verifyPortableArtifact(deployment, row.artifact).dir;
|
|
99
|
-
for (const path of manifests.get(id).agents ?? []) {
|
|
100
|
-
const normalized = normalizePackagePath(path);
|
|
101
|
-
if (normalized === undefined) throw oatsError("invalid-declaration", "invalid helper export path");
|
|
102
|
-
const definitionPath = `${normalized === "." ? "" : `${normalized}/`}soul.yaml`;
|
|
103
|
-
const definition = parseConfigData(readFileSync(join(root, definitionPath))).value;
|
|
104
|
-
const authoredPolicy = ["requires", "defaults"].filter((field) => definition[field] && Object.keys(definition[field]).length);
|
|
105
|
-
for (const field of ["knowledge", "teams", "resources"]) if (Object.hasOwn(definition, field)) authoredPolicy.push(field);
|
|
106
|
-
if (authoredPolicy.length) return { record: null, problems: [{ code: "needs-configuration",
|
|
107
|
-
message: `helper has authored ${authoredPolicy.join(", ")} policy requiring dedicated preparation; parent bindings were not inherited`, origins: [] }] };
|
|
108
|
-
const helperMode = definition.work ?? "directory";
|
|
109
|
-
if (!kernel.workModes.includes(helperMode)) throw oatsError("invalid-declaration", "helper work mode is invalid");
|
|
110
|
-
const subject = { kind: "helper", provider: row.artifact, name: definition.name, definition: { owner: row.artifact, path: definitionPath, kind: "file" } };
|
|
111
|
-
const key = `${id}:${definition.name}`;
|
|
112
|
-
if (helperKeys.has(key)) throw oatsError("invalid-declaration", "duplicate helper export name");
|
|
113
|
-
helperKeys.add(key); helperPlans.push({ key, subject, helperMode });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
for (const key of Object.keys(helperLaunches)) if (!helperKeys.has(key)) throw oatsError("helper-not-selected", "helper launch request must name an exact selected helper-map key");
|
|
117
|
-
// Compile EVERY launch request before publishing any helper record. No partial
|
|
118
|
-
// usable helper graph when a later helper's runtime inputs are unsupported.
|
|
119
|
-
const records = helperPlans.map(({ key, subject, helperMode }) => ({ key, record: build(subject, helperMode, {}, helperLaunches[key]) }));
|
|
120
|
-
const record = build(seed.subject, mode, {}, launch), helpers = Object.create(null);
|
|
121
|
-
// Verify every prospective file/owner/witness before publishing ANY member
|
|
122
|
-
// of this graph, including a primary file omitted only for helpers.
|
|
123
|
-
for (const candidate of [...records.map(item => item.record), record]) verifyResolutionInputs(deployment,
|
|
124
|
-
{ schemaVersion: 1, id: jsonIntegrity(candidate).value }, { draft: candidate });
|
|
125
|
-
for (const item of records) helpers[item.key] = commitCapturedResolution(deployment, item.record);
|
|
126
|
-
return { record: { ...record, helpers }, problems: [] };
|
|
127
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/** Execute only an approved retained provider command. Preparation does not need
|
|
2
|
-
* a fabricated complete resolution merely to obtain its binding. */
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { realpathSync } from 'node:fs';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { canonicalJson } from './portable-values.mjs';
|
|
7
|
-
import { objectAt } from './portable-shape.mjs';
|
|
8
|
-
import { validateArtifactSet } from './resolution-shape.mjs';
|
|
9
|
-
import { verifyPortableArtifact } from './portable-artifacts.mjs';
|
|
10
|
-
import { verifyRetainedCapability, verifyResolutionInputs } from './captured-resolutions.mjs';
|
|
11
|
-
import { buildCapturedInvocationContext } from './captured-invocation-context.mjs';
|
|
12
|
-
import { readApprovalLedger, evaluateCapturedApprovals } from './artifact-approvals.mjs';
|
|
13
|
-
import { validateBindingInterface } from './provider-binding.mjs';
|
|
14
|
-
import { BINDING_LIMITS, validateBindingRequest, decodeBindingResponse } from './provider-binding-wire.mjs';
|
|
15
|
-
import { oatsError } from './errors.mjs';
|
|
16
|
-
import { providerReasons } from './provider-reasons.mjs';
|
|
17
|
-
|
|
18
|
-
// Same kernel-owned CLI locator as lifecycle hooks; never caller env or PATH.
|
|
19
|
-
const CLI_BIN=fileURLToPath(new URL('../bin/oats.mjs',import.meta.url));
|
|
20
|
-
|
|
21
|
-
export function invokeProviderBinding(options,codecs) {
|
|
22
|
-
canonicalJson(options);
|
|
23
|
-
objectAt(options,['deployment','artifacts','capability','phase','settings','input','timeoutMs'],['deployment','artifacts','capability','phase','settings','input']);
|
|
24
|
-
const {deployment,artifacts,capability,phase,settings,input,timeoutMs=30000}=options;
|
|
25
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000) throw oatsError('invalid-declaration','binding timeout must be between 1 and 30000 milliseconds');
|
|
26
|
-
validateArtifactSet(artifacts);
|
|
27
|
-
if (!Object.hasOwn(artifacts.capabilities,capability)) throw oatsError('capability-not-selected','binding provider is not selected');
|
|
28
|
-
const row=artifacts.capabilities[capability],root=verifyPortableArtifact(deployment,row.artifact).dir;
|
|
29
|
-
const manifest=verifyRetainedCapability(root,artifacts,capability,codecs.manifest(root));
|
|
30
|
-
const binding=validateBindingInterface(manifest);
|
|
31
|
-
if (!binding) throw oatsError('provider-not-qualified','provider has no binding interface');
|
|
32
|
-
const request=validateBindingRequest({schemaVersion:1,phase,slot:manifest.layer,capability,settings,input});
|
|
33
|
-
if (phase === 'check' && input.invocation !== undefined) {
|
|
34
|
-
// Public check callers cannot turn a merely shape-valid projection into
|
|
35
|
-
// authority. Re-derive it from the referenced retained record/current target.
|
|
36
|
-
const verified=verifyResolutionInputs(deployment,input.invocation.executionBinding.resolution),record=verified.record;
|
|
37
|
-
if (record.capture !== 'prepared') throw oatsError('migration-required','captured check cannot use unverified reconstructed authority');
|
|
38
|
-
const actual=buildCapturedInvocationContext({loaded:{...verified,deployment,resolution:input.invocation.executionBinding.resolution,capability:{id:capability}},
|
|
39
|
-
action:input.action,instance:input.invocation.instance === null ? null : Object.fromEntries(['home','work','name','agent'].map(key=>[key,input.invocation.instance[key]])),
|
|
40
|
-
intent:input.invocation.intent,priorReceipt:input.invocation.priorReceipt});
|
|
41
|
-
const effective=Object.fromEntries(Object.entries(record.dispatch.settingsChoices[capability] ?? {}).map(([name,key])=>[name,record.choices[key].value]));
|
|
42
|
-
if (canonicalJson(actual)!==canonicalJson(input.invocation) || canonicalJson(record.artifacts)!==canonicalJson(artifacts)
|
|
43
|
-
|| !record.bindings[manifest.layer] || canonicalJson(record.bindings[manifest.layer])!==canonicalJson(input.binding)
|
|
44
|
-
|| canonicalJson(effective)!==canonicalJson(settings)) throw oatsError('invalid-resolution','provider check invocation differs from retained authority');
|
|
45
|
-
}
|
|
46
|
-
codecs.settings(manifest,settings);
|
|
47
|
-
const selected={record:{artifacts:{...artifacts,capabilities:{[capability]:row}}},manifests:new Map([[capability,manifest]])};
|
|
48
|
-
const approval=evaluateCapturedApprovals(selected,readApprovalLedger(deployment).ledger)[0];
|
|
49
|
-
if (approval.status !== 'approved') throw oatsError('approval-required','provider codec requires current exact-artifact approval');
|
|
50
|
-
if (codecs.host({manifest}).length) throw oatsError('host-requirement-missing','provider codec has unmet host requirements');
|
|
51
|
-
const spec=manifest.commands[binding[phase]];
|
|
52
|
-
const [script,...args]=spec.trim().split(/\s+/),file=codecs.executable(manifest,script);
|
|
53
|
-
if (!file) throw oatsError('resource-not-found','provider codec executable is unavailable');
|
|
54
|
-
const environment=Object.fromEntries(Object.entries(process.env).filter(([key])=>! /^(OATS_|OAS_|PI_)/.test(key)));
|
|
55
|
-
Object.assign(environment,{OATS_CAPABILITY:capability,OATS_CAPABILITY_ROOT:root,OATS_SETTINGS:canonicalJson(settings,BINDING_LIMITS),OATS_CLI_BIN:realpathSync(CLI_BIN)});
|
|
56
|
-
let result;
|
|
57
|
-
try {
|
|
58
|
-
result=spawnSync(process.execPath,[file,...args],{cwd:root,env:environment,input:canonicalJson(request,BINDING_LIMITS),
|
|
59
|
-
timeout:timeoutMs,killSignal:'SIGKILL',maxBuffer:BINDING_LIMITS.maxBytes,stdio:['pipe','pipe','pipe'],shell:false});
|
|
60
|
-
} catch { throw oatsError('provider-unavailable','provider codec could not execute'); }
|
|
61
|
-
if (result.error || result.signal || result.status !== 0) throw oatsError('provider-unavailable','provider codec did not complete successfully');
|
|
62
|
-
const response=decodeBindingResponse(result.stdout,request,{reasons:providerReasons(manifest)});
|
|
63
|
-
if (!response.ok) throw oatsError(response.error.code,response.error.message ?? 'provider binding phase refused');
|
|
64
|
-
return response.result;
|
|
65
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/** Bounded provider wire data. No provider model, precedence or native execution. */
|
|
2
|
-
import { canonicalJson, parseStrictJson } from './portable-values.mjs';
|
|
3
|
-
import { objectAt, stringAt, versionAt } from './portable-shape.mjs';
|
|
4
|
-
import { validateOrigin, validateProviderBinding, validateMessagingChoice, validateCapturedChoices, validateCapturedContext } from './resolution-shape.mjs';
|
|
5
|
-
import { validateCapturedInvocationContext } from './captured-invocation-context.mjs';
|
|
6
|
-
import { FUNDAMENTAL_SLOTS } from './portable-policy.mjs';
|
|
7
|
-
import { isMaterializedCapabilityId } from './capability-provenance.mjs';
|
|
8
|
-
import { BINDING_PHASES } from './provider-binding.mjs';
|
|
9
|
-
import { resolveChoices } from './portable-choices.mjs';
|
|
10
|
-
import { oatsError } from './errors.mjs';
|
|
11
|
-
import { providerReasons, safeProviderReason, validateBindingReasons } from './provider-reasons.mjs';
|
|
12
|
-
|
|
13
|
-
export const BINDING_LIMITS = Object.freeze({maxBytes:1024*1024,maxDepth:32,maxEntries:16384});
|
|
14
|
-
export const BINDING_PROBLEMS = Object.freeze(['needs-configuration','requirement-conflict','invalid-binding','authorization-required','host-requirement-missing','provider-unavailable','provider-not-qualified']);
|
|
15
|
-
const fail = () => { throw oatsError('invalid-binding-output','provider returned invalid binding data'); };
|
|
16
|
-
const same = (a,b) => canonicalJson(a) === canonicalJson(b);
|
|
17
|
-
const roleKinds = {soul:['soul-requirement','soul-default'],workspace:['workspace-default'],adoption:['import-adoption'],operator:['operator']};
|
|
18
|
-
const pointer = key => typeof key === 'string' && /^(?:\/(?:[^~]|~[01])*)+$/.test(key);
|
|
19
|
-
export const bindingField = (key,slot) => pointer(key) && key.startsWith(`/bindings/${slot}/`) && key.length > `/bindings/${slot}/`.length;
|
|
20
|
-
function problem(value,reasons) {
|
|
21
|
-
objectAt(value,['code','message'],['code']);
|
|
22
|
-
if (!BINDING_PROBLEMS.includes(value.code)) fail();
|
|
23
|
-
if (value.message !== undefined) stringAt(value.message,'/message',{empty:true});
|
|
24
|
-
const message=safeProviderReason(value.message,reasons);
|
|
25
|
-
return {code:value.code,...(message === undefined ? {} : {message})}; // Only declared fixed text crosses.
|
|
26
|
-
}
|
|
27
|
-
export function validateBindingRequest(request) {
|
|
28
|
-
canonicalJson(request,BINDING_LIMITS);
|
|
29
|
-
objectAt(request,['schemaVersion','phase','slot','capability','settings','input'],['schemaVersion','phase','slot','capability','settings','input']);
|
|
30
|
-
versionAt(request.schemaVersion);
|
|
31
|
-
if (!BINDING_PHASES.includes(request.phase) || !FUNDAMENTAL_SLOTS.includes(request.slot) || !isMaterializedCapabilityId(request.capability)) fail();
|
|
32
|
-
objectAt(request.settings,null,[]); const input=request.input;
|
|
33
|
-
if (request.phase === 'normalize') {
|
|
34
|
-
objectAt(input,['declarations','context'],['declarations','context']);
|
|
35
|
-
if (!Array.isArray(input.declarations)) fail();
|
|
36
|
-
for (const declaration of input.declarations) {
|
|
37
|
-
objectAt(declaration,['kind','value','origin','origins'],['kind','value','origin','origins']);
|
|
38
|
-
if (!Object.hasOwn(roleKinds,declaration.kind)) fail();
|
|
39
|
-
validateOrigin(declaration.origin); objectAt(declaration.origins,null,[]);
|
|
40
|
-
if (!roleKinds[declaration.kind].includes(declaration.origin.kind)) fail();
|
|
41
|
-
for (const [key,origin] of Object.entries(declaration.origins)) {
|
|
42
|
-
if (key !== '' && !pointer(key)) fail();
|
|
43
|
-
validateOrigin(origin);
|
|
44
|
-
if (!roleKinds[declaration.kind].includes(origin.kind) || !same(origin.document,declaration.origin.document)) fail();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} else if (request.phase === 'bind') {
|
|
48
|
-
objectAt(input,['model','choices','context'],['model','choices','context']);
|
|
49
|
-
validateCapturedChoices(input.choices);
|
|
50
|
-
for (const key of Object.keys(input.choices)) if (!bindingField(key,request.slot)) fail();
|
|
51
|
-
} else {
|
|
52
|
-
objectAt(input,['binding','context','action','invocation'],['binding','context','action']);
|
|
53
|
-
validateProviderBinding(input.binding); objectAt(input.action,null,['kind']);
|
|
54
|
-
if (input.binding.capability !== request.capability) fail();
|
|
55
|
-
if (Object.hasOwn(input,'invocation')) {
|
|
56
|
-
const invocation=validateCapturedInvocationContext(input.invocation);
|
|
57
|
-
if (invocation.capability !== request.capability || !same(invocation.context,input.context) || !same(invocation.action,input.action)) fail();
|
|
58
|
-
if (request.slot === 'messaging' && (!invocation.messagingChoice.enabled || invocation.messagingChoice.privateKey.provider !== request.capability)) fail();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
validateCapturedContext(input.context);
|
|
62
|
-
return request;
|
|
63
|
-
}
|
|
64
|
-
function witnessed(origin,declarations) {
|
|
65
|
-
validateOrigin(origin);
|
|
66
|
-
return declarations.some(declaration => roleKinds[declaration.kind].includes(origin.kind)
|
|
67
|
-
&& [declaration.origin,...Object.values(declaration.origins)].some(candidate => {
|
|
68
|
-
const {kind:_,...a}=origin, {kind:__,...b}=candidate;
|
|
69
|
-
return same(a,b);
|
|
70
|
-
}));
|
|
71
|
-
}
|
|
72
|
-
export function decodeBindingResponse(bytes,request,{reasons}={}) {
|
|
73
|
-
let response;
|
|
74
|
-
try {
|
|
75
|
-
validateBindingRequest(request);
|
|
76
|
-
// Out-of-band trusted caller input, NEVER a field supplied in the response.
|
|
77
|
-
// Unknown providers legitimately have no compatibility reasons; an explicit
|
|
78
|
-
// manifest declaration was already validated with the stricter nonempty bound.
|
|
79
|
-
reasons=validateBindingReasons(reasons === undefined ? providerReasons({capability:request.capability}) : reasons,{allowEmpty:true});
|
|
80
|
-
response=parseStrictJson(bytes,BINDING_LIMITS);
|
|
81
|
-
objectAt(response,response?.ok === true ? ['schemaVersion','phase','slot','capability','ok','result'] : ['schemaVersion','phase','slot','capability','ok','error'],
|
|
82
|
-
response?.ok === true ? ['schemaVersion','phase','slot','capability','ok','result'] : ['schemaVersion','phase','slot','capability','ok','error']);
|
|
83
|
-
for (const key of ['schemaVersion','phase','slot','capability']) if (response[key] !== request[key]) fail();
|
|
84
|
-
if (response.ok === false) return {ok:false,error:problem(response.error,reasons)};
|
|
85
|
-
if (response.ok !== true) fail();
|
|
86
|
-
const result=response.result;
|
|
87
|
-
if (request.phase === 'normalize') {
|
|
88
|
-
objectAt(result,['requirements','candidates','model'],['requirements','candidates','model']);
|
|
89
|
-
if (!Array.isArray(result.requirements) || !Array.isArray(result.candidates)) fail();
|
|
90
|
-
for (const entry of [...result.requirements,...result.candidates]) {
|
|
91
|
-
if (!bindingField(entry.key,request.slot) || !witnessed(entry.origin,request.input.declarations)) fail();
|
|
92
|
-
}
|
|
93
|
-
for (const requirement of result.requirements) if (requirement.origin.kind !== 'soul-requirement') fail();
|
|
94
|
-
for (const candidate of result.candidates) if (candidate.kind !== candidate.origin.kind) fail();
|
|
95
|
-
// Reuse the sole entry codec/solver; conflicts are data for the combined plan.
|
|
96
|
-
resolveChoices(result);
|
|
97
|
-
return {ok:true,result};
|
|
98
|
-
}
|
|
99
|
-
if (request.phase === 'bind') {
|
|
100
|
-
const keys=['payloadContract','payloadVersion','payload','credentialRefs','provenance'];
|
|
101
|
-
objectAt(result,request.slot === 'messaging' ? [...keys,'messagingChoice'] : keys,request.slot === 'messaging' ? [...keys,'messagingChoice'] : keys);
|
|
102
|
-
const {messagingChoice,...fields}=result;
|
|
103
|
-
const binding=validateProviderBinding({schemaVersion:1,capability:request.capability,...fields});
|
|
104
|
-
// Source read/ownership provenance may live in the opaque normalized model;
|
|
105
|
-
// preparation validates that additional provenance against its original input.
|
|
106
|
-
if (request.slot === 'messaging') validateMessagingChoice(messagingChoice,{bindings:{messaging:binding},context:request.input.context});
|
|
107
|
-
return {ok:true,result:{binding,...(request.slot === 'messaging' ? {messagingChoice} : {})}};
|
|
108
|
-
}
|
|
109
|
-
objectAt(result,['status','problems'],['status','problems']);
|
|
110
|
-
if (!['ready','needs-configuration','authorization-required','unavailable'].includes(result.status) || !Array.isArray(result.problems)) fail();
|
|
111
|
-
const problems=result.problems.map(value=>problem(value,reasons));
|
|
112
|
-
if (result.status === 'ready' && problems.length) fail();
|
|
113
|
-
return {ok:true,result:{status:result.status,problems}};
|
|
114
|
-
} catch { fail(); }
|
|
115
|
-
}
|
|
116
|
-
export function bindingOriginWitnessed(origin,declarations) { return witnessed(origin,declarations); }
|