@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,136 +0,0 @@
|
|
|
1
|
-
/** Source-aware software choice planning. Consumes validated parser results;
|
|
2
|
-
* qualifies no hosting identity and performs no acquisition, approval or enrollment.
|
|
3
|
-
* All precedence/conflicts still go through the single resolveChoices engine. */
|
|
4
|
-
import { canonicalJson, compareUtf8 } from "./portable-values.mjs";
|
|
5
|
-
import { invalidShape, objectAt, pointerKey } from "./portable-shape.mjs";
|
|
6
|
-
import { validateOrigin } from "./resolution-shape.mjs";
|
|
7
|
-
import { sameIdentity, validateSoulIdentity } from "./portable-identity.mjs";
|
|
8
|
-
import { FUNDAMENTAL_SLOTS, validatePolicyChoices } from "./portable-policy.mjs";
|
|
9
|
-
import { capabilityChoiceKey, layerChoiceKey, settingChoiceKey, soulConstraints, sourceSelectionValue } from "./soul-constraints.mjs";
|
|
10
|
-
import { resolveChoices } from "./portable-choices.mjs";
|
|
11
|
-
import { validateRepoAnchor, validateSelectionSource } from "./source-spec.mjs";
|
|
12
|
-
import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
|
|
13
|
-
|
|
14
|
-
function originAt(parsed, pointer, kind) {
|
|
15
|
-
const locator = parsed.origins[pointer];
|
|
16
|
-
if (!locator) invalidShape(pointer, "planning requires the original declaration origin");
|
|
17
|
-
const origin = { ...locator, kind };
|
|
18
|
-
validateOrigin(origin);
|
|
19
|
-
return origin;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function planSoftwareChoices(input) {
|
|
23
|
-
canonicalJson(input);
|
|
24
|
-
objectAt(input, ["identity", "soul", "workspace", "adoptions", "operator", "providerRequirements"], ["identity", "soul"]);
|
|
25
|
-
const { identity, soul, workspace = null, adoptions = [], operator = null, providerRequirements = [] } = input;
|
|
26
|
-
validateSoulIdentity(identity);
|
|
27
|
-
const requirements = soulConstraints(soul), candidates = [], settingsCandidates = [];
|
|
28
|
-
if (!Array.isArray(adoptions) || !Array.isArray(providerRequirements)) invalidShape("", "adoptions and provider constraints must be arrays");
|
|
29
|
-
const add = (parsed, key, value, pointer, kind) => candidates.push({ key, value, kind, origin: originAt(parsed, pointer, kind) });
|
|
30
|
-
const collect = (parsed, policy, pointer, kind, sourceContext) => {
|
|
31
|
-
const selection = (id, item, at, key) => {
|
|
32
|
-
const selectedSource = item === false || item === "none" ? null : sourceSelectionValue(id, parsed, at, sourceContext);
|
|
33
|
-
add(parsed, key, selectedSource, at, kind);
|
|
34
|
-
if (item && typeof item === "object") for (const [name, value] of Object.entries(item.settings ?? {})) {
|
|
35
|
-
settingsCandidates.push({ owner: { key, value: selectedSource }, candidate: { key: settingChoiceKey(id, name), value, kind,
|
|
36
|
-
origin: originAt(parsed, `${at}/settings/${pointerKey(name)}`, kind) } });
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
for (const [id, item] of Object.entries(policy.capabilities ?? {})) selection(id, item, `${pointer}/capabilities/${pointerKey(id)}`, capabilityChoiceKey(id));
|
|
40
|
-
for (const slot of FUNDAMENTAL_SLOTS) if (Object.hasOwn(policy, slot)) {
|
|
41
|
-
const item = policy[slot]; selection(item === "none" ? null : item.capability, item, `${pointer}/${slot}`, layerChoiceKey(slot));
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
if (workspace?.declaration.defaults) collect(workspace, workspace.declaration.defaults, "/defaults", "workspace-default");
|
|
45
|
-
if (soul.declaration.defaults) collect(soul, soul.declaration.defaults, "/defaults", "soul-default");
|
|
46
|
-
|
|
47
|
-
// Discovery supplies qualified identity. An alias never performs this lookup.
|
|
48
|
-
// Several aliases may contribute compatible fields; conflicting equal-authority
|
|
49
|
-
// fields are reported by resolveChoices with both original document pointers.
|
|
50
|
-
for (const adoption of adoptions) {
|
|
51
|
-
objectAt(adoption, ["identity", "parsed", "origins", "pointer"], ["identity", "parsed", "origins", "pointer"]);
|
|
52
|
-
validateSoulIdentity(adoption.identity);
|
|
53
|
-
if (!sameIdentity(adoption.identity, identity)) continue;
|
|
54
|
-
const imported = adoption.parsed.reference;
|
|
55
|
-
if (identity.kind !== "git-soul" || imported.soul !== identity.exportPath) invalidShape("/adoptions", "qualified adoption does not match its exported source path");
|
|
56
|
-
if (identity.repository.kind === "canonical-remote" && imported.source !== identity.repository.remote) invalidShape("/adoptions", "import source contradicts its qualified canonical identity");
|
|
57
|
-
const parsed = { sources: adoption.parsed.sources, origins: adoption.origins };
|
|
58
|
-
const root = `${adoption.pointer}/adoption`, value = imported.adoption ?? {};
|
|
59
|
-
if (value.providers) collect(parsed, value.providers, `${root}/providers`, "import-adoption");
|
|
60
|
-
for (const field of ["bindings", "teamAliases"]) for (const [key, item] of Object.entries(value[field] ?? {})) {
|
|
61
|
-
add(parsed, `/adoption/${field}/${pointerKey(key)}`, item, `${root}/${field}/${pointerKey(key)}`, "import-adoption");
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (operator) {
|
|
65
|
-
objectAt(operator, ["policy", "document", "localBase", "allowLocalPaths", "sourceContext", "bindings"], ["policy", "document"]);
|
|
66
|
-
if (operator.bindings !== undefined) objectAt(operator.bindings, null, [], "/operator/bindings");
|
|
67
|
-
if (operator.sourceContext !== undefined) validateRepoAnchor(operator.sourceContext);
|
|
68
|
-
if (operator.allowLocalPaths !== undefined && typeof operator.allowLocalPaths !== "boolean") invalidShape("/operator/allowLocalPaths", "expected explicit boolean local-source authorization");
|
|
69
|
-
validateOrigin({ kind: "operator", document: operator.document, pointer: "/policy" });
|
|
70
|
-
if (operator.document.kind !== "operator") invalidShape("/operator/document", "operator choices need an operator input locator");
|
|
71
|
-
const origins = Object.create(null);
|
|
72
|
-
const mark = (value, pointer) => {
|
|
73
|
-
origins[pointer] = { document: operator.document, pointer };
|
|
74
|
-
if (value && typeof value === "object") for (const [key, child] of Object.entries(value)) mark(child, `${pointer}/${pointerKey(key)}`);
|
|
75
|
-
};
|
|
76
|
-
mark(operator.policy, "/policy");
|
|
77
|
-
const sources = validatePolicyChoices(operator.policy, { pointer: "/policy", origins,
|
|
78
|
-
localBase: operator.localBase, allowLocalPaths: operator.allowLocalPaths === true });
|
|
79
|
-
collect({ sources, origins }, operator.policy, "/policy", "operator", operator.sourceContext);
|
|
80
|
-
}
|
|
81
|
-
for (const requirement of [...requirements, ...providerRequirements]) validateOrigin(requirement.origin);
|
|
82
|
-
// Provider-owned fixed fields join the SAME constraint pipeline. This function
|
|
83
|
-
// neither interprets a payload nor executes a provider to obtain these inputs.
|
|
84
|
-
const allRequirements = [...requirements, ...providerRequirements];
|
|
85
|
-
// Eligibility follows the selected owning field AND source. Reuse the same
|
|
86
|
-
// engine for this deterministic source pass, then final field resolution;
|
|
87
|
-
// nothing is fetched/re-observed and no second precedence policy is introduced.
|
|
88
|
-
const sourceChoices = resolveChoices({ requirements: allRequirements, candidates }).choices;
|
|
89
|
-
const excludedSettings = [];
|
|
90
|
-
for (const { owner, candidate } of settingsCandidates) {
|
|
91
|
-
if (sameIdentity(sourceChoices[owner.key]?.value ?? null, owner.value)) candidates.push(candidate);
|
|
92
|
-
else excludedSettings.push({ ...candidate, owner, reason: "owning-source-overridden" });
|
|
93
|
-
}
|
|
94
|
-
const result = resolveChoices({ requirements: allRequirements, candidates });
|
|
95
|
-
const capabilities = Object.create(null), providers = Object.create(null), settings = Object.create(null);
|
|
96
|
-
const problems = [...result.problems];
|
|
97
|
-
const select = (key) => {
|
|
98
|
-
const choice = result.choices[key];
|
|
99
|
-
if (!choice || choice.value === null) return null;
|
|
100
|
-
objectAt(choice.value, ["capability", "source"], ["capability", "source"], key);
|
|
101
|
-
const { capability, source } = choice.value;
|
|
102
|
-
if (!isMaterializedCapabilityId(capability)) invalidShape(key, "invalid selected capability identity");
|
|
103
|
-
if (key.startsWith("/capabilities/") && key !== capabilityChoiceKey(capability)) invalidShape(key, "capability choice key differs from its selected identity");
|
|
104
|
-
validateSelectionSource(source);
|
|
105
|
-
if (Object.hasOwn(capabilities, capability)) {
|
|
106
|
-
const prior = capabilities[capability];
|
|
107
|
-
if (!sameIdentity(prior.source, source)) problems.push({ code: "requirement-conflict", key,
|
|
108
|
-
message: "one capability identity has incompatible selected sources", origins: [result.choices[prior.choiceKeys[0]].selectedBy, choice.selectedBy] });
|
|
109
|
-
prior.choiceKeys.push(key);
|
|
110
|
-
} else capabilities[capability] = { source, choiceKeys: [key] };
|
|
111
|
-
return capability;
|
|
112
|
-
};
|
|
113
|
-
for (const key of Object.keys(result.choices).sort(compareUtf8)) if (key.startsWith("/capabilities/")) select(key);
|
|
114
|
-
for (const slot of FUNDAMENTAL_SLOTS) {
|
|
115
|
-
const key = layerChoiceKey(slot);
|
|
116
|
-
if (Object.hasOwn(result.choices, key)) providers[slot] = select(key);
|
|
117
|
-
}
|
|
118
|
-
// Only selected software receives settings; alternative/default history remains
|
|
119
|
-
// in choices for explanation, not an ambient configuration to execute later.
|
|
120
|
-
for (const id of Object.keys(capabilities)) settings[id] = Object.create(null);
|
|
121
|
-
const unescape = (part) => part.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
122
|
-
for (const key of Object.keys(result.choices)) {
|
|
123
|
-
if (key.startsWith("/layers/") && !FUNDAMENTAL_SLOTS.some((slot) => key === layerChoiceKey(slot))) invalidShape(key, "unknown fundamental slot choice");
|
|
124
|
-
if (!key.startsWith("/settings/")) continue;
|
|
125
|
-
const separator = key.indexOf("/", "/settings/".length);
|
|
126
|
-
if (separator < 0) invalidShape(key, "setting choice must name a capability and setting");
|
|
127
|
-
const id = unescape(key.slice("/settings/".length, separator)), name = unescape(key.slice(separator + 1));
|
|
128
|
-
if (!isMaterializedCapabilityId(id) || settingChoiceKey(id, name) !== key) invalidShape(key, "noncanonical setting choice key");
|
|
129
|
-
if (Object.hasOwn(settings, id)) settings[id][name] = key;
|
|
130
|
-
}
|
|
131
|
-
return { ...result, status: problems.some((problem) => problem.code === "requirement-conflict") ? "conflict" : result.status,
|
|
132
|
-
requirements: allRequirements, candidates, capabilities, providers, settings, excludedSettings, problems,
|
|
133
|
-
providerInputs: { knowledge: soul.declaration.knowledge ?? null, stores: workspace?.declaration.knowledge?.stores ?? [] },
|
|
134
|
-
// Advertisements/adoption aliases are data, never wider-team consent.
|
|
135
|
-
advertisedTeams: soul.declaration.teams ?? [] };
|
|
136
|
-
}
|
package/lib/portable-digest.mjs
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/** Explicit new digest domains. Existing v1 package/artifact functions remain
|
|
2
|
-
* literal in their owning modules; this module never reinterprets legacy hashes. */
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync, readlinkSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { oatsError } from "./errors.mjs";
|
|
7
|
-
import { canonicalJson, compareUtf8, dataLimits, decodeUtf8 } from "./portable-values.mjs";
|
|
8
|
-
|
|
9
|
-
export const TREE_FORMAT = "oats.tree-exec.v1";
|
|
10
|
-
export const PACKAGE_FORMAT = "oats.package-payload-exec.v1";
|
|
11
|
-
export const JSON_FORMAT = "oats.json.v1";
|
|
12
|
-
export const BYTES_FORMAT = "oats.bytes.v1";
|
|
13
|
-
const digest = (format, data) => ({ format, value: `sha256-${createHash("sha256").update(data).digest("hex")}` });
|
|
14
|
-
|
|
15
|
-
export function jsonIntegrity(value, options) { return digest(JSON_FORMAT, canonicalJson(value, options)); }
|
|
16
|
-
export function bytesIntegrity(value) {
|
|
17
|
-
if (!Buffer.isBuffer(value) && !(value instanceof Uint8Array)) {
|
|
18
|
-
throw oatsError("invalid-declaration", "raw-byte integrity requires bytes");
|
|
19
|
-
}
|
|
20
|
-
return digest(BYTES_FORMAT, value);
|
|
21
|
-
}
|
|
22
|
-
export function validateIntegrity(value, formats = [TREE_FORMAT, PACKAGE_FORMAT, JSON_FORMAT, BYTES_FORMAT]) {
|
|
23
|
-
const invalid = () => { throw oatsError("invalid-artifact-reference", "expected explicit supported digest format and full SHA-256"); };
|
|
24
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) invalid();
|
|
25
|
-
const prototype = Object.getPrototypeOf(value), fields = Object.getOwnPropertyDescriptors(value);
|
|
26
|
-
if ((prototype !== null && prototype !== Object.prototype) || Reflect.ownKeys(fields).length !== 2) invalid();
|
|
27
|
-
for (const key of ["format", "value"]) {
|
|
28
|
-
if (!Object.hasOwn(fields, key) || !Object.hasOwn(fields[key], "value") || !fields[key].enumerable) invalid();
|
|
29
|
-
}
|
|
30
|
-
if (!formats.includes(fields.format.value) || typeof fields.value.value !== "string"
|
|
31
|
-
|| !/^sha256-[a-f0-9]{64}$/.test(fields.value.value)) invalid();
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size
|
|
36
|
-
&& a.mode === b.mode && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
37
|
-
|
|
38
|
-
/** Domain NUL; then UTF-8-path-sorted records:
|
|
39
|
-
* F || uint64(path bytes) || path || owner-exec byte || uint64(content bytes) || content
|
|
40
|
-
* S || uint64(path bytes) || path || uint64(target bytes) || literal target
|
|
41
|
-
* Length framing prevents the legacy binary record-boundary ambiguity.
|
|
42
|
-
* Empty directories and non-owner-execute mode bits do not enter identity.
|
|
43
|
-
* Containment remains the retention caller's responsibility; links aren't followed. */
|
|
44
|
-
export function treeIntegrity(root, { format = TREE_FORMAT, ...options } = {}) {
|
|
45
|
-
if (![TREE_FORMAT, PACKAGE_FORMAT].includes(format)) {
|
|
46
|
-
throw oatsError("unsupported-wire-version", "unsupported portable tree digest format");
|
|
47
|
-
}
|
|
48
|
-
const limits = dataLimits({ maxBytes: 256 * 1024 * 1024, ...options });
|
|
49
|
-
const entries = [], hash = createHash("sha256");
|
|
50
|
-
let visited = 0, bytes = 0;
|
|
51
|
-
const account = (length) => {
|
|
52
|
-
bytes += length;
|
|
53
|
-
if (bytes > limits.maxBytes) throw oatsError("resource-limit", "artifact digest byte limit exceeded");
|
|
54
|
-
};
|
|
55
|
-
const length = (size) => {
|
|
56
|
-
const value = Buffer.alloc(8);
|
|
57
|
-
value.writeBigUInt64BE(BigInt(size));
|
|
58
|
-
return value;
|
|
59
|
-
};
|
|
60
|
-
const field = (data) => { account(8 + data.length); hash.update(length(data.length)); hash.update(data); };
|
|
61
|
-
const walk = (dir, parts) => {
|
|
62
|
-
if (parts.length > limits.maxDepth) throw oatsError("resource-limit", "artifact tree depth limit exceeded");
|
|
63
|
-
const names = readdirSync(dir, { encoding: "buffer" });
|
|
64
|
-
for (const raw of names) {
|
|
65
|
-
if (++visited > limits.maxEntries) throw oatsError("resource-limit", "artifact tree entry limit exceeded");
|
|
66
|
-
const name = decodeUtf8(raw, "artifact filename");
|
|
67
|
-
if (format === PACKAGE_FORMAT && (name === "node_modules" || (!parts.length && name === "oats-lock.json"))) continue;
|
|
68
|
-
const path = join(dir, name), relative = [...parts, name].join("/");
|
|
69
|
-
const stat = lstatSync(path);
|
|
70
|
-
if (stat.isSymbolicLink()) entries.push({ path, relative, stat, kind: "S" });
|
|
71
|
-
else if (stat.isDirectory()) walk(path, [...parts, name]);
|
|
72
|
-
else if (stat.isFile()) entries.push({ path, relative, stat, kind: "F" });
|
|
73
|
-
else throw oatsError("invalid-artifact", "unsupported artifact tree entry");
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
const rootStat = lstatSync(root);
|
|
77
|
-
if (!rootStat.isDirectory()) throw oatsError("invalid-artifact", "artifact digest root must be a real directory");
|
|
78
|
-
walk(root, []);
|
|
79
|
-
hash.update(`${format}\0`);
|
|
80
|
-
for (const entry of entries.sort((a, b) => compareUtf8(a.relative, b.relative))) {
|
|
81
|
-
account(1); hash.update(entry.kind); field(Buffer.from(entry.relative, "utf8"));
|
|
82
|
-
if (entry.kind === "S") {
|
|
83
|
-
const target = readlinkSync(entry.path, { encoding: "buffer" });
|
|
84
|
-
decodeUtf8(target, "artifact symlink target");
|
|
85
|
-
field(target);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const fd = openSync(entry.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
89
|
-
try {
|
|
90
|
-
const before = fstatSync(fd);
|
|
91
|
-
if (!before.isFile() || !sameFile(entry.stat, before)) {
|
|
92
|
-
throw oatsError("integrity-drift", "artifact input changed during digest");
|
|
93
|
-
}
|
|
94
|
-
account(1 + 8 + before.size);
|
|
95
|
-
hash.update(Buffer.from([(before.mode & 0o100) !== 0 ? 1 : 0]));
|
|
96
|
-
hash.update(length(before.size));
|
|
97
|
-
const content = readFileSync(fd);
|
|
98
|
-
if (content.length !== before.size || !sameFile(before, fstatSync(fd))) {
|
|
99
|
-
throw oatsError("integrity-drift", "artifact input changed during digest");
|
|
100
|
-
}
|
|
101
|
-
hash.update(content);
|
|
102
|
-
} finally { closeSync(fd); }
|
|
103
|
-
}
|
|
104
|
-
return { format, value: `sha256-${hash.digest("hex")}` };
|
|
105
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/** Qualified identity value types, not a proof of hosting/provider ownership.
|
|
2
|
-
* Discovery obtains facts from the authenticated host adapter, never repo YAML. */
|
|
3
|
-
import { canonicalJson } from "./portable-values.mjs";
|
|
4
|
-
import { jsonIntegrity } from "./portable-digest.mjs";
|
|
5
|
-
import { parseLockedSource3, parseRepositorySource, portablePath } from "./source-spec.mjs";
|
|
6
|
-
import { invalidShape, objectAt, stringAt } from "./portable-shape.mjs";
|
|
7
|
-
|
|
8
|
-
export function validateRepositoryIdentity(value) {
|
|
9
|
-
canonicalJson(value, { maxBytes: 64 * 1024, maxDepth: 8, maxEntries: 64 });
|
|
10
|
-
objectAt(value, value?.kind === "provider-repository" ? ["kind", "provider", "host", "id"] : ["kind", "remote"], ["kind"]);
|
|
11
|
-
if (value.kind === "provider-repository") {
|
|
12
|
-
for (const name of ["provider", "host", "id"]) stringAt(value[name], `/${name}`);
|
|
13
|
-
if (!/^[a-z][a-z0-9.-]*$/.test(value.provider) || !/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(value.host)) {
|
|
14
|
-
invalidShape("", "provider and host identity must be canonical");
|
|
15
|
-
}
|
|
16
|
-
} else if (value.kind === "canonical-remote") {
|
|
17
|
-
stringAt(value.remote, "/remote");
|
|
18
|
-
if (parseRepositorySource(value.remote).normalized !== value.remote) invalidShape("/remote", "repository identity remote is not canonical");
|
|
19
|
-
} else invalidShape("/kind", "unsupported repository identity");
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
export function validateSoulIdentity(value) {
|
|
23
|
-
canonicalJson(value, { maxBytes: 64 * 1024, maxDepth: 12, maxEntries: 128 });
|
|
24
|
-
objectAt(value, value?.kind === "git-soul" ? ["kind", "repository", "exportPath"] : ["kind", "source", "exportPath"], ["kind", "exportPath"]);
|
|
25
|
-
if (value.kind === "git-soul") validateRepositoryIdentity(value.repository);
|
|
26
|
-
else if (value.kind === "local-soul") {
|
|
27
|
-
if (parseLockedSource3(value.source, ".").kind !== "path") invalidShape("/source", "local soul identity requires an explicit local source");
|
|
28
|
-
} else invalidShape("/kind", "unsupported soul identity");
|
|
29
|
-
portablePath(value.exportPath, { allowRoot: value.kind === "local-soul" });
|
|
30
|
-
return value;
|
|
31
|
-
}
|
|
32
|
-
export function validateWorkspaceIdentity(value) {
|
|
33
|
-
canonicalJson(value, { maxBytes: 64 * 1024, maxDepth: 12, maxEntries: 128 });
|
|
34
|
-
objectAt(value, ["repository", "path"], ["repository", "path"]);
|
|
35
|
-
validateRepositoryIdentity(value.repository);
|
|
36
|
-
if (value.path !== "oats-workspace.yaml") invalidShape("/path", "workspace identity names oats-workspace.yaml");
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
export function sameIdentity(a, b) { return canonicalJson(a) === canonicalJson(b); }
|
|
40
|
-
export function soulIdentityKey(value) { validateSoulIdentity(value); return jsonIntegrity(value).value; }
|
package/lib/portable-lock.mjs
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/** Lock v3 is mutable input to future preparation, NEVER historical dispatch
|
|
2
|
-
* authority. Existing v1/v2 readers and stores are not migrated by this module. */
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { canonicalJson, parseStrictJson } from "./portable-values.mjs";
|
|
5
|
-
import { JSON_FORMAT, jsonIntegrity, validateIntegrity } from "./portable-digest.mjs";
|
|
6
|
-
import { validateArtifactSet, validateOrigin } from "./resolution-shape.mjs";
|
|
7
|
-
import { objectAt, stringAt } from "./portable-shape.mjs";
|
|
8
|
-
import { parseLockedSource3 } from "./source-spec.mjs";
|
|
9
|
-
import { readPortableBytes } from "./portable-files.mjs";
|
|
10
|
-
import { portableScope, withPortableStateWrite, writeGuardedPortableDocument } from "./portable-state.mjs";
|
|
11
|
-
import { oatsError } from "./errors.mjs";
|
|
12
|
-
|
|
13
|
-
const invalid = (message) => { throw oatsError("invalid-lock", message); };
|
|
14
|
-
export const emptyLock3 = () => ({ lockfileVersion: 3, artifactSets: {}, selections: {} });
|
|
15
|
-
export function requestKey3(request) {
|
|
16
|
-
canonicalJson(request);
|
|
17
|
-
objectAt(request, ["source", "path"], ["source", "path"]);
|
|
18
|
-
parseLockedSource3(request.source, request.path);
|
|
19
|
-
return jsonIntegrity(request).value;
|
|
20
|
-
}
|
|
21
|
-
export function artifactSetKey3(set) { validateArtifactSet(set); return jsonIntegrity(set).value; }
|
|
22
|
-
function validateProblem(problem) {
|
|
23
|
-
objectAt(problem, ["code", "message", "origins", "target", "details"], ["code", "message", "origins"]);
|
|
24
|
-
stringAt(problem.code, "/freshness/problems/code"); stringAt(problem.message, "/freshness/problems/message");
|
|
25
|
-
if (!Array.isArray(problem.origins)) invalid("refresh problem origins must be an array");
|
|
26
|
-
problem.origins.forEach(validateOrigin);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function validateLock3(lock) {
|
|
30
|
-
try {
|
|
31
|
-
canonicalJson(lock);
|
|
32
|
-
if (lock?.lockfileVersion === 1 || lock?.lockfileVersion === 2) {
|
|
33
|
-
throw oatsError("migration-required", "legacy lock requires its explicit evidence/migration reader; it is not lock v3");
|
|
34
|
-
}
|
|
35
|
-
objectAt(lock, ["lockfileVersion", "artifactSets", "selections"], ["lockfileVersion", "artifactSets", "selections"]);
|
|
36
|
-
if (lock.lockfileVersion !== 3) throw oatsError("unsupported-wire-version", "unsupported selection-lock version");
|
|
37
|
-
objectAt(lock.artifactSets, null, []); objectAt(lock.selections, null, []);
|
|
38
|
-
const indexes = new Map(), checked = new Set();
|
|
39
|
-
for (const [key, set] of Object.entries(lock.artifactSets)) {
|
|
40
|
-
if (artifactSetKey3(set) !== key) invalid("artifact-set key differs from its canonical content");
|
|
41
|
-
const requests = new Map();
|
|
42
|
-
for (const [id, pkg] of Object.entries(set.packages)) {
|
|
43
|
-
const request = jsonIntegrity({ source: pkg.source, path: pkg.path }).value;
|
|
44
|
-
if (!requests.has(request)) requests.set(request, []);
|
|
45
|
-
requests.get(request).push(id);
|
|
46
|
-
}
|
|
47
|
-
indexes.set(key, requests);
|
|
48
|
-
}
|
|
49
|
-
for (const [key, selection] of Object.entries(lock.selections)) {
|
|
50
|
-
objectAt(selection, ["request", "current", "available", "freshness"], ["request", "current", "available", "freshness"]);
|
|
51
|
-
if (requestKey3(selection.request) !== key) invalid("selection key differs from its normalized source request");
|
|
52
|
-
for (const name of ["current", "available"]) {
|
|
53
|
-
const selected = selection[name];
|
|
54
|
-
if (selected === null) continue;
|
|
55
|
-
stringAt(selected, `/selections/${name}`);
|
|
56
|
-
if (!Object.hasOwn(lock.artifactSets, selected)) invalid("selection references an absent artifact set");
|
|
57
|
-
const roots = indexes.get(selected).get(key);
|
|
58
|
-
if (roots?.length !== 1) invalid("selection has no unique root package matching its request");
|
|
59
|
-
if (checked.has(`${selected}:${key}`)) continue;
|
|
60
|
-
const set = lock.artifactSets[selected], reached = new Set([roots[0]]), pending = [roots[0]];
|
|
61
|
-
while (pending.length) {
|
|
62
|
-
for (const dependency of set.packages[pending.pop()].dependencies) {
|
|
63
|
-
if (!reached.has(dependency)) { reached.add(dependency); pending.push(dependency); }
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (reached.size !== Object.keys(set.packages).length) invalid("selected set includes packages outside the requested dependency closure");
|
|
67
|
-
for (const row of Object.values(set.capabilities)) if (row.origin.kind !== "package") invalid("package selection cannot smuggle a direct local capability");
|
|
68
|
-
checked.add(`${selected}:${key}`);
|
|
69
|
-
}
|
|
70
|
-
const freshness = selection.freshness;
|
|
71
|
-
objectAt(freshness, ["state", "observedAt", "problems"], ["state", "observedAt", "problems"]);
|
|
72
|
-
if (!["refreshed", "offline", "failed", "not-checked"].includes(freshness.state)) invalid("invalid freshness state");
|
|
73
|
-
if (freshness.observedAt !== null) {
|
|
74
|
-
stringAt(freshness.observedAt, "/freshness/observedAt");
|
|
75
|
-
const date = new Date(freshness.observedAt);
|
|
76
|
-
if (!Number.isFinite(date.getTime()) || date.toISOString() !== freshness.observedAt) invalid("observation timestamp must be canonical UTC ISO time");
|
|
77
|
-
}
|
|
78
|
-
if (freshness.state === "refreshed" && (freshness.observedAt === null || selection.available === null)) invalid("successful refresh needs an observed available set and timestamp");
|
|
79
|
-
if (!Array.isArray(freshness.problems)) invalid("refresh problems must be an array");
|
|
80
|
-
freshness.problems.forEach(validateProblem);
|
|
81
|
-
}
|
|
82
|
-
return lock;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
if (["invalid-declaration", "invalid-source", "invalid-artifact-reference"].includes(error.code)) {
|
|
85
|
-
const failure = oatsError("invalid-lock", error.message); failure.cause = error; throw failure;
|
|
86
|
-
}
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Explicit scope only. Absent differs from empty-v1, unsupported or damaged.
|
|
92
|
-
* Integrity is the CAS token, not software verification or executable consent. */
|
|
93
|
-
export function readLock3(scope) {
|
|
94
|
-
const path = join(portableScope(scope), "oats-lock.json");
|
|
95
|
-
const bytes = readPortableBytes(path, { allowMissing: true, invalidCode: "invalid-lock" });
|
|
96
|
-
if (bytes === null) return { lock: null, integrity: null };
|
|
97
|
-
const lock = parseStrictJson(bytes);
|
|
98
|
-
validateLock3(lock);
|
|
99
|
-
if (!bytes.equals(Buffer.from(canonicalJson(lock)))) invalid("selection lock must use canonical JSON bytes");
|
|
100
|
-
return { lock, integrity: jsonIntegrity(lock) };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** All new writers share a cooperative guard and compare the complete snapshot.
|
|
104
|
-
* No implicit legacy migration, row merging, approval, tree cleanup or live cutover. */
|
|
105
|
-
export function writeLock3(scope, expectedPreviousIntegrity, next) {
|
|
106
|
-
if (expectedPreviousIntegrity !== null) validateIntegrity(expectedPreviousIntegrity, [JSON_FORMAT]);
|
|
107
|
-
validateLock3(next);
|
|
108
|
-
return withPortableStateWrite(scope, (context) => {
|
|
109
|
-
const { deployment } = context;
|
|
110
|
-
const previous = readLock3(deployment);
|
|
111
|
-
if (canonicalJson(previous.integrity) !== canonicalJson(expectedPreviousIntegrity)) throw oatsError("selection-changed", "selection lock changed; repeat explicit preparation");
|
|
112
|
-
const integrity = jsonIntegrity(next);
|
|
113
|
-
if (canonicalJson(previous.integrity) === canonicalJson(integrity)) return { lock: next, integrity, status: "kept" };
|
|
114
|
-
writeGuardedPortableDocument(context, join(deployment, "oats-lock.json"), next, { absent: previous.lock === null });
|
|
115
|
-
return { lock: next, integrity, status: "written" };
|
|
116
|
-
});
|
|
117
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/** Thin request-file transport for explicit new-work preparation.
|
|
2
|
-
* The CLI owns flag parsing through its existing router; public core owns the
|
|
3
|
-
* preparation schema/resolver. This module reads no ambient authority and never
|
|
4
|
-
* prepares, installs, approves, activates or dispatches a command. */
|
|
5
|
-
import { isAbsolute, resolve } from "node:path";
|
|
6
|
-
import { readPortableBytes } from "./portable-files.mjs";
|
|
7
|
-
import { canonicalJson, freezeJson, parseStrictJson } from "./portable-values.mjs";
|
|
8
|
-
import { objectAt } from "./portable-shape.mjs";
|
|
9
|
-
import { oatsError } from "./errors.mjs";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* file: explicit normalized absolute JSON file.
|
|
13
|
-
* inputFlags: OTHER prepare value flags already parsed by the existing router;
|
|
14
|
-
* request/json are consumed by that router and must not occur here. Even a
|
|
15
|
-
* false/null/empty-valued competing flag is a conflict by own-key presence.
|
|
16
|
-
* explicitSelector: existing capturedSelector(argv, {}) result, or null. No
|
|
17
|
-
* second global selector parser, inherited resolution or environment lookup.
|
|
18
|
-
*
|
|
19
|
-
* Return the decoded request itself, not a wrapper/private prepare input. Unknown
|
|
20
|
-
* request fields survive unchanged so the sole public core validator can refuse
|
|
21
|
-
* them; never strip them to produce an apparently compatible request.
|
|
22
|
-
*/
|
|
23
|
-
export function readPortablePreparationRequest(options) {
|
|
24
|
-
canonicalJson(options);
|
|
25
|
-
objectAt(options, ["file", "inputFlags", "explicitSelector"], ["file"]);
|
|
26
|
-
const { file, inputFlags = {}, explicitSelector = null } = options;
|
|
27
|
-
objectAt(inputFlags, null, []);
|
|
28
|
-
if (Object.keys(inputFlags).length) throw oatsError("E_BAD_ARGS", "--request cannot be combined with other preparation input flags");
|
|
29
|
-
if (explicitSelector !== null) throw oatsError("E_BAD_ARGS", "new-work preparation cannot use explicit captured selectors");
|
|
30
|
-
if (typeof file !== "string" || !isAbsolute(file) || resolve(file) !== file || file.includes("\0")) {
|
|
31
|
-
throw oatsError("E_BAD_ARGS", "--request needs a normalized absolute JSON file path");
|
|
32
|
-
}
|
|
33
|
-
let bytes;
|
|
34
|
-
try { bytes = readPortableBytes(file, { missingCode: "E_BAD_ARGS", invalidCode: "E_BAD_ARGS" }); }
|
|
35
|
-
catch (error) {
|
|
36
|
-
// Do not echo request content, source/settings or OS diagnostic payloads.
|
|
37
|
-
throw oatsError(error.code === "resource-limit" ? "resource-limit"
|
|
38
|
-
: error.code === "integrity-drift" ? "integrity-drift" : "E_BAD_ARGS", "preparation request file could not be read as bounded regular bytes");
|
|
39
|
-
}
|
|
40
|
-
let input;
|
|
41
|
-
try { input = parseStrictJson(bytes); }
|
|
42
|
-
catch (error) {
|
|
43
|
-
throw oatsError(error.code === "resource-limit" ? "resource-limit" : "invalid-declaration", "preparation request file must contain bounded strict UTF-8 JSON");
|
|
44
|
-
}
|
|
45
|
-
// Only the transport's JSON-object boundary is checked here, not a copy of the
|
|
46
|
-
// public preparation field/choice/provider validation contract.
|
|
47
|
-
objectAt(input, null, []);
|
|
48
|
-
return freezeJson(input);
|
|
49
|
-
}
|