@awebai/oats 0.25.9 → 0.27.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 +648 -1755
- 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 +229 -58
- package/docs/capability-manifest.schema.json +29 -9
- package/docs/configuration.md +17 -5
- package/docs/conventions.md +18 -28
- 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 +604 -271
- package/docs/desktop-instance-start.md +3 -3
- package/docs/desktop.md +7 -13
- package/docs/execution-targets.md +16 -18
- package/docs/first-team.md +15 -18
- package/docs/implementation.md +31 -62
- 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 +30 -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 +76 -53
- 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/release-notes/v0.27.0.md +100 -0
- package/docs/schedules.md +54 -132
- package/docs/servers.md +4 -4
- package/docs/soul.schema.json +11 -4
- package/docs/souls-and-instances.md +60 -47
- package/docs/workspaces.md +80 -58
- package/injects/instance-boundary.md +2 -2
- 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 +947 -5023
- package/lib/deprecation.mjs +24 -0
- package/lib/digest.mjs +12 -0
- package/lib/instance-inspect.mjs +397 -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/process-group.mjs +1 -1
- package/lib/provider-binding.mjs +4 -2
- package/lib/provider-reasons.mjs +3 -68
- package/lib/remote.mjs +1 -1
- package/lib/resolve.mjs +204 -68
- package/lib/schedule.mjs +136 -292
- package/lib/servers.mjs +70 -38
- package/lib/{portable-shape.mjs → shape.mjs} +4 -3
- package/lib/tree-copy.mjs +44 -0
- package/lib/workspace.mjs +132 -20
- package/package-catalog.json +6 -6
- package/package.json +1 -1
- package/packages/record/lib/session-roots.mjs +8 -6
- package/skills/integration-authoring/SKILL.md +48 -40
- package/skills/oats-getting-started/SKILL.md +105 -110
- package/skills/oats-support/SKILL.md +2 -2
- package/skills/soul-craft/SKILL.md +13 -6
- package/bin/oats-pi-sdk-host.mjs +0 -17
- package/docs/2026-09-03-architecture-proposal.md +0 -642
- package/docs/artifact-approvals.schema.json +0 -7
- package/docs/captured-invocation-context.schema.json +0 -7
- package/docs/captured-resolution.schema.json +0 -7
- package/docs/design/package-engine-contract.md +0 -813
- package/docs/design/package-runtime-api.md +0 -588
- package/docs/desktop-succession.md +0 -57
- package/docs/execution-capsule.schema.json +0 -108
- package/docs/first-team-demo.md +0 -92
- package/docs/knowledge-migration.md +0 -147
- package/docs/migration-from-oas.md +0 -103
- package/docs/oats-config.schema.json +0 -172
- package/docs/oats-lock-v3.schema.json +0 -7
- package/docs/oats-lock.schema.json +0 -175
- package/docs/operating-team-migration.md +0 -470
- package/docs/portable.schema.json +0 -2512
- package/docs/provider-check-input.schema.json +0 -7
- package/docs/rebuild-to-v2.md +0 -511
- package/docs/workspace-adoption.md +0 -74
- package/injects/framework-workspace.md +0 -7
- package/injects/local-soul.md +0 -19
- package/injects/oats-portable.md +0 -20
- package/injects/oats.md +0 -11
- package/injects/portable-instance-boundary.md +0 -39
- package/injects/portable-work-directory.md +0 -29
- package/lib/artifact-approvals.mjs +0 -120
- package/lib/artifact-tree.mjs +0 -141
- package/lib/capability-artifacts.mjs +0 -179
- package/lib/capability-execution.mjs +0 -15
- package/lib/capability-inputs.mjs +0 -39
- package/lib/capability-provenance.mjs +0 -231
- package/lib/captured-action-shape.mjs +0 -21
- package/lib/captured-admission-shape.mjs +0 -20
- package/lib/captured-binding-file.mjs +0 -36
- package/lib/captured-dispatch.mjs +0 -66
- package/lib/captured-instance-index.mjs +0 -277
- package/lib/captured-invocation-context.mjs +0 -130
- package/lib/captured-launch-request.mjs +0 -66
- package/lib/captured-operation-process.mjs +0 -15
- package/lib/captured-pi-custody.mjs +0 -29
- package/lib/captured-pi-host.mjs +0 -167
- package/lib/captured-pi-outcome.mjs +0 -172
- package/lib/captured-resolutions.mjs +0 -275
- package/lib/captured-scaffold.mjs +0 -87
- package/lib/captured-selector.mjs +0 -28
- package/lib/captured-session-backend.mjs +0 -52
- package/lib/captured-source-receipt-file.mjs +0 -72
- package/lib/helper-injection-policy.mjs +0 -104
- package/lib/legacy-lock-codec.mjs +0 -106
- package/lib/manifest-settings.mjs +0 -84
- package/lib/package-closure.mjs +0 -48
- package/lib/package-materialization.mjs +0 -83
- package/lib/pi-sdk-host.mjs +0 -229
- package/lib/portable-artifacts.mjs +0 -115
- package/lib/portable-choices.mjs +0 -82
- package/lib/portable-composition.mjs +0 -136
- package/lib/portable-digest.mjs +0 -105
- package/lib/portable-identity.mjs +0 -40
- package/lib/portable-lock.mjs +0 -117
- package/lib/portable-onboarding-request.mjs +0 -49
- package/lib/portable-onboarding.mjs +0 -256
- package/lib/portable-package-preparation.mjs +0 -188
- package/lib/portable-policy.mjs +0 -44
- package/lib/portable-soul.mjs +0 -42
- package/lib/portable-state.mjs +0 -80
- package/lib/prepare-composition.mjs +0 -170
- package/lib/prepared-bindings.mjs +0 -92
- package/lib/prepared-resources.mjs +0 -127
- package/lib/provider-binding-broker.mjs +0 -65
- package/lib/provider-binding-wire.mjs +0 -116
- package/lib/readiness.mjs +0 -225
- package/lib/repository-observation.mjs +0 -226
- package/lib/resolution-shape.mjs +0 -393
- package/lib/schedule-capsule.mjs +0 -206
- package/lib/soul-constraints.mjs +0 -40
- package/lib/source-projection.mjs +0 -84
- package/lib/source-spec.mjs +0 -189
- package/lib/workspace-definition.mjs +0 -126
- package/lib/workspace-discovery.mjs +0 -146
- package/skills/oats/SKILL.md +0 -162
- package/skills/oats-config/SKILL.md +0 -164
- package/skills/oats-packages/SKILL.md +0 -184
- package/skills/oats-portable/SKILL.md +0 -115
- package/skills/oats-portable-artifacts/SKILL.md +0 -63
package/lib/resolution-shape.mjs
DELETED
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
/** Captured-resolution data shape. Filesystem/provenance verification and
|
|
2
|
-
* authorization are separate; passing this codec alone never permits dispatch. */
|
|
3
|
-
import { canonicalJson, compareUtf8 } from "./portable-values.mjs";
|
|
4
|
-
import { TREE_FORMAT, PACKAGE_FORMAT, BYTES_FORMAT, validateIntegrity } from "./portable-digest.mjs";
|
|
5
|
-
import { resolveChoices } from "./portable-choices.mjs";
|
|
6
|
-
import { validateRepositoryIdentity, validateSoulIdentity, validateWorkspaceIdentity, sameIdentity } from "./portable-identity.mjs";
|
|
7
|
-
import { portablePath, parseLockedSource3, parseRepositorySource, revisionSelector } from "./source-spec.mjs";
|
|
8
|
-
import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
|
|
9
|
-
import { invalidShape, objectAt, pointerKey, stringAt, stringSetAt, versionAt } from "./portable-shape.mjs";
|
|
10
|
-
|
|
11
|
-
export const RESOLUTION_FIELDS = Object.freeze(["schemaVersion", "capture", "subject", "context", "artifacts", "choices", "bindings", "messagingChoice", "resources", "resourceBundles", "dispatch", "helpers", "evidence"]);
|
|
12
|
-
const hash = (value, pointer) => stringAt(value, pointer, { pattern: /^sha256-[a-f0-9]{64}$/ });
|
|
13
|
-
const capability = (value, pointer) => { if (!isMaterializedCapabilityId(value)) invalidShape(pointer, "invalid capability identity"); };
|
|
14
|
-
const array = (value, pointer) => { if (!Array.isArray(value)) invalidShape(pointer, "expected array"); return value; };
|
|
15
|
-
const own = (value, key, pointer) => { stringAt(key, pointer); if (!Object.hasOwn(value, key)) invalidShape(pointer, "missing captured reference"); return value[key]; };
|
|
16
|
-
const orderedSet = (value, pointer, key = (item) => item) => {
|
|
17
|
-
for (let i = 1; i < value.length; i++) if (compareUtf8(key(value[i - 1]), key(value[i])) >= 0) invalidShape(pointer, "captured set must be unique and canonically ordered");
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function validateResolutionRef(value) {
|
|
21
|
-
canonicalJson(value, { maxBytes: 1024, maxDepth: 4, maxEntries: 8 });
|
|
22
|
-
objectAt(value, ["schemaVersion", "id"], ["schemaVersion", "id"]);
|
|
23
|
-
versionAt(value.schemaVersion); hash(value.id, "/id");
|
|
24
|
-
return value;
|
|
25
|
-
}
|
|
26
|
-
export function validateArtifactRef(value) {
|
|
27
|
-
canonicalJson(value, { maxBytes: 64 * 1024, maxDepth: 16, maxEntries: 1024 });
|
|
28
|
-
objectAt(value, value?.kind === "soul" ? ["kind", "identity", "integrity"]
|
|
29
|
-
: value?.kind === "capability" ? ["kind", "capability", "integrity"] : ["kind", "integrity"], ["kind", "integrity"]);
|
|
30
|
-
validateIntegrity(value.integrity, [TREE_FORMAT]);
|
|
31
|
-
if (value.kind === "soul") validateSoulIdentity(value.identity);
|
|
32
|
-
else if (value.kind === "capability") capability(value.capability, "/capability");
|
|
33
|
-
else if (value.kind !== "resource") invalidShape("/kind", "invalid artifact reference kind");
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
export function validateResourceRef(value) {
|
|
37
|
-
canonicalJson(value, { maxBytes: 64 * 1024, maxDepth: 20, maxEntries: 2048 });
|
|
38
|
-
objectAt(value, ["owner", "path", "kind"], ["owner", "path", "kind"]);
|
|
39
|
-
validateArtifactRef(value.owner); portablePath(value.path, { allowRoot: true });
|
|
40
|
-
if (!["file", "directory", "manifest", "skill", "runtime-package"].includes(value.kind)) invalidShape("/kind", "invalid captured resource kind");
|
|
41
|
-
return value;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const ORIGIN_KINDS = Object.freeze(["soul-requirement", "soul-default", "workspace-default", "import-adoption", "operator", "package-dependency", "work-target", "migration-evidence", "provider-binding", "workspace-admission", "member-backlink", "source-export", "manifest-default"]);
|
|
45
|
-
export function validateOrigin(value) {
|
|
46
|
-
canonicalJson(value, { maxBytes: 64 * 1024, maxDepth: 16, maxEntries: 1024 });
|
|
47
|
-
objectAt(value, ["kind", "document", "pointer", "span"], ["kind", "document", "pointer"]);
|
|
48
|
-
if (!ORIGIN_KINDS.includes(value.kind)) invalidShape("/kind", "invalid origin kind");
|
|
49
|
-
stringAt(value.pointer, "/pointer", { empty: true, pattern: /^(?:\/(?:[^~]|~[01])*)*$/ });
|
|
50
|
-
const doc = value.document;
|
|
51
|
-
if (doc?.kind === "source") {
|
|
52
|
-
objectAt(doc, ["kind", "source", "revision", "path", "integrity"], ["kind", "source", "revision", "path", "integrity"]);
|
|
53
|
-
portablePath(doc.path); validateIntegrity(doc.integrity, [BYTES_FORMAT]);
|
|
54
|
-
if (typeof doc.source === "string" && doc.source.startsWith("path:")) {
|
|
55
|
-
if (parseLockedSource3(doc.source, ".").kind !== "path" || doc.revision !== "local") invalidShape("/document", "invalid local document witness");
|
|
56
|
-
} else {
|
|
57
|
-
if (parseRepositorySource(doc.source).normalized !== doc.source || typeof doc.revision !== "string" || !/^[a-f0-9]{40}$/.test(doc.revision)) invalidShape("/document", "invalid exact document source");
|
|
58
|
-
}
|
|
59
|
-
} else if (doc?.kind === "deployment") {
|
|
60
|
-
objectAt(doc, ["kind", "path", "integrity"], ["kind", "path", "integrity"]);
|
|
61
|
-
portablePath(doc.path); validateIntegrity(doc.integrity, [BYTES_FORMAT]);
|
|
62
|
-
} else if (doc?.kind === "operator") {
|
|
63
|
-
objectAt(doc, ["kind", "id"], ["kind", "id"]); stringAt(doc.id, "/document/id");
|
|
64
|
-
} else if (doc?.kind === "record") {
|
|
65
|
-
objectAt(doc, ["kind", "ref"], ["kind", "ref"]); validateResolutionRef(doc.ref);
|
|
66
|
-
} else if (doc?.kind === "artifact") {
|
|
67
|
-
objectAt(doc, ["kind", "owner", "path", "integrity"], ["kind", "owner", "path", "integrity"]);
|
|
68
|
-
validateArtifactRef(doc.owner); portablePath(doc.path); validateIntegrity(doc.integrity, [BYTES_FORMAT]);
|
|
69
|
-
} else invalidShape("/document", "invalid origin document");
|
|
70
|
-
if (Object.hasOwn(value, "span")) {
|
|
71
|
-
objectAt(value.span, ["start", "end"], ["start", "end"]);
|
|
72
|
-
if (!Number.isSafeInteger(value.span.start) || !Number.isSafeInteger(value.span.end)
|
|
73
|
-
|| value.span.start < 0 || value.span.end < value.span.start) invalidShape("/span", "invalid source span");
|
|
74
|
-
}
|
|
75
|
-
return value;
|
|
76
|
-
}
|
|
77
|
-
const origins = (value, pointer) => array(value, pointer).forEach(validateOrigin);
|
|
78
|
-
|
|
79
|
-
export function validateCapturedChoices(value) {
|
|
80
|
-
canonicalJson(value); objectAt(value, null, []);
|
|
81
|
-
const requirements = [], candidates = [];
|
|
82
|
-
for (const [key, choice] of Object.entries(value)) {
|
|
83
|
-
objectAt(choice, ["value", "selectedBy", "constraints", "considered"], ["value", "selectedBy", "constraints", "considered"]);
|
|
84
|
-
if (choice.selectedBy !== null) validateOrigin(choice.selectedBy);
|
|
85
|
-
for (const constraint of array(choice.constraints, "/choices/constraints")) {
|
|
86
|
-
objectAt(constraint, ["kind", "value", "origin"], ["kind", "origin"]);
|
|
87
|
-
validateOrigin(constraint.origin); requirements.push({ key, ...constraint });
|
|
88
|
-
}
|
|
89
|
-
for (const considered of array(choice.considered, "/choices/considered")) {
|
|
90
|
-
objectAt(considered, ["kind", "value", "origin", "disposition"], ["kind", "value", "origin", "disposition"]);
|
|
91
|
-
validateOrigin(considered.origin);
|
|
92
|
-
if (!["selected", "overridden"].includes(considered.disposition)) invalidShape("/choices/considered", "invalid choice disposition");
|
|
93
|
-
candidates.push({ key, kind: considered.kind, value: considered.value, origin: considered.origin });
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const replay = resolveChoices({ requirements, candidates });
|
|
97
|
-
if (replay.status !== "resolved" || !sameIdentity(replay.choices, value)) invalidShape("/choices", "captured choices do not satisfy their recorded constraints", "resolution-incomplete");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Shared envelope codec for both preparation output and immutable records.
|
|
101
|
-
* Domain payload interpretation and mutable provider readiness remain separate. */
|
|
102
|
-
export function validateProviderBinding(binding) {
|
|
103
|
-
canonicalJson(binding);
|
|
104
|
-
objectAt(binding, ["schemaVersion", "capability", "payloadContract", "payloadVersion", "payload", "credentialRefs", "provenance"], ["schemaVersion", "capability", "payloadContract", "payloadVersion", "payload", "credentialRefs", "provenance"]);
|
|
105
|
-
versionAt(binding.schemaVersion); capability(binding.capability, "/bindings/capability");
|
|
106
|
-
stringAt(binding.payloadContract, "/bindings/payloadContract");
|
|
107
|
-
if (!Number.isSafeInteger(binding.payloadVersion) || binding.payloadVersion < 1) invalidShape("/bindings/payloadVersion", "invalid provider payload version");
|
|
108
|
-
objectAt(binding.credentialRefs, null, [], "/bindings/credentialRefs");
|
|
109
|
-
for (const credential of Object.values(binding.credentialRefs)) {
|
|
110
|
-
if (credential?.kind === "env") {
|
|
111
|
-
objectAt(credential, ["kind", "name"], ["kind", "name"]);
|
|
112
|
-
stringAt(credential.name, "/bindings/credentialRefs/name", { pattern: /^[A-Za-z_][A-Za-z0-9_]*$/ });
|
|
113
|
-
} else {
|
|
114
|
-
objectAt(credential, ["kind", "provider", "key"], ["kind", "provider", "key"]);
|
|
115
|
-
if (credential.kind !== "provider") invalidShape("/bindings/credentialRefs/kind", "invalid credential reference");
|
|
116
|
-
stringAt(credential.provider, "/bindings/credentialRefs/provider"); stringAt(credential.key, "/bindings/credentialRefs/key");
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
origins(binding.provenance, "/bindings/provenance");
|
|
120
|
-
return binding;
|
|
121
|
-
}
|
|
122
|
-
function bindings(value, record) {
|
|
123
|
-
for (const [slot, binding] of Object.entries(value)) {
|
|
124
|
-
if (!["knowledge", "messaging", "tasks"].includes(slot)) invalidShape("/bindings", "unknown fundamental slot");
|
|
125
|
-
validateProviderBinding(binding);
|
|
126
|
-
own(record.artifacts.capabilities, binding.capability, "/bindings/capability");
|
|
127
|
-
const selected = own(record.choices, `/layers/${slot}`, "/bindings/selection").value;
|
|
128
|
-
if (!selected || typeof selected !== "object" || selected.capability !== binding.capability) invalidShape("/bindings", "binding differs from selected provider");
|
|
129
|
-
}
|
|
130
|
-
for (const slot of ["knowledge", "messaging", "tasks"]) {
|
|
131
|
-
const selected = record.choices[`/layers/${slot}`]?.value;
|
|
132
|
-
if (selected !== undefined && selected !== null) own(value, slot, "/bindings");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function validateMessagingChoice(value, record) {
|
|
137
|
-
const selected = record.bindings.messaging;
|
|
138
|
-
if (value?.enabled === false) {
|
|
139
|
-
objectAt(value, ["schemaVersion", "enabled"], ["schemaVersion", "enabled"]);
|
|
140
|
-
if (selected) invalidShape("/messagingChoice", "selected messaging provider cannot disappear");
|
|
141
|
-
} else {
|
|
142
|
-
objectAt(value, ["schemaVersion", "enabled", "privateKey", "wider", "provenance"], ["schemaVersion", "enabled", "privateKey", "wider", "provenance"]);
|
|
143
|
-
if (value.enabled !== true || !selected) invalidShape("/messagingChoice", "invalid enabled messaging choice");
|
|
144
|
-
objectAt(value.privateKey, ["provider", "human", "context"], ["provider", "human", "context"]);
|
|
145
|
-
if (value.privateKey.provider !== selected.capability) invalidShape("/messagingChoice/privateKey", "private provider mismatch");
|
|
146
|
-
objectAt(value.privateKey.human, ["provider", "id"], ["provider", "id"]);
|
|
147
|
-
stringAt(value.privateKey.human.provider, "/messagingChoice/privateKey/human/provider");
|
|
148
|
-
stringAt(value.privateKey.human.id, "/messagingChoice/privateKey/human/id");
|
|
149
|
-
const expected = record.context.kind === "workspace" ? { kind: "workspace", identity: record.context.identity } : { kind: "standalone", key: record.context.key };
|
|
150
|
-
if ((record.context.kind === "standalone" && record.context.key === null) || !sameIdentity(value.privateKey.context, expected)) invalidShape("/messagingChoice/privateKey/context", "private context mismatch");
|
|
151
|
-
const seen = new Set();
|
|
152
|
-
for (const team of array(value.wider, "/messagingChoice/wider")) {
|
|
153
|
-
objectAt(team, ["provider", "id"], ["provider", "id"]); stringAt(team.id, "/messagingChoice/wider/id");
|
|
154
|
-
if (team.provider !== selected.capability) invalidShape("/messagingChoice/wider/provider", "wider team provider mismatch");
|
|
155
|
-
const key = canonicalJson(team); if (seen.has(key)) invalidShape("/messagingChoice/wider", "duplicate wider team"); seen.add(key);
|
|
156
|
-
}
|
|
157
|
-
orderedSet(value.wider, "/messagingChoice/wider", canonicalJson);
|
|
158
|
-
origins(value.provenance, "/messagingChoice/provenance");
|
|
159
|
-
}
|
|
160
|
-
versionAt(value.schemaVersion);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function sourceSelection(value) {
|
|
164
|
-
objectAt(value, ["identity", "revision", "alias", "sourceArtifact", "definition", "projection"], ["identity", "revision", "alias", "sourceArtifact", "definition", "projection"]);
|
|
165
|
-
validateSoulIdentity(value.identity); validateArtifactRef(value.sourceArtifact);
|
|
166
|
-
if (value.sourceArtifact.kind !== "soul" || !sameIdentity(value.identity, value.sourceArtifact.identity)) invalidShape("/subject/soul", "source artifact identity mismatch");
|
|
167
|
-
stringAt(value.alias, "/subject/soul/alias", { pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ });
|
|
168
|
-
portablePath(value.definition);
|
|
169
|
-
objectAt(value.projection, ["roots"], ["roots"]);
|
|
170
|
-
stringSetAt(value.projection.roots, "/subject/soul/projection/roots", (path) => portablePath(path, { allowRoot: true }));
|
|
171
|
-
orderedSet(value.projection.roots, "/subject/soul/projection/roots");
|
|
172
|
-
if (!value.projection.roots.length || !value.projection.roots.some((root) => root === "." || value.definition === root || value.definition.startsWith(`${root}/`))) {
|
|
173
|
-
invalidShape("/subject/soul/definition", "definition is not in the captured projection");
|
|
174
|
-
}
|
|
175
|
-
const exportPath = value.identity.exportPath;
|
|
176
|
-
if (exportPath !== "." && !value.definition.startsWith(`${exportPath}/`)) invalidShape("/subject/soul/definition", "definition is outside exported soul");
|
|
177
|
-
const revision = value.revision;
|
|
178
|
-
if (revision?.kind === "local") {
|
|
179
|
-
objectAt(revision, ["kind", "source", "integrity", "provenance", "repository"], ["kind", "source", "integrity", "provenance"]);
|
|
180
|
-
if (parseLockedSource3(revision.source, ".").kind !== "path") invalidShape("/subject/soul/revision", "local witness requires an explicit source");
|
|
181
|
-
validateIntegrity(revision.integrity, [TREE_FORMAT]);
|
|
182
|
-
if (!sameIdentity(revision.integrity, value.sourceArtifact.integrity)) invalidShape("/subject/soul/revision", "local projection witness mismatch");
|
|
183
|
-
if (value.identity.kind === "local-soul") {
|
|
184
|
-
if (value.identity.source !== revision.source || Object.hasOwn(revision, "repository")) invalidShape("/subject/soul/revision", "local source identity mismatch");
|
|
185
|
-
} else {
|
|
186
|
-
validateRepositoryIdentity(revision.repository);
|
|
187
|
-
if (!sameIdentity(revision.repository, value.identity.repository)) invalidShape("/subject/soul/revision", "local Git witness repository mismatch");
|
|
188
|
-
}
|
|
189
|
-
} else {
|
|
190
|
-
objectAt(revision, ["identity", "remote", "selector", "commit", "provenance"], ["identity", "remote", "selector", "commit", "provenance"]);
|
|
191
|
-
validateRepositoryIdentity(revision.identity);
|
|
192
|
-
if (value.identity.kind !== "git-soul" || !sameIdentity(revision.identity, value.identity.repository)) invalidShape("/subject/soul/revision", "Git observation identity mismatch");
|
|
193
|
-
if (parseRepositorySource(revision.remote).normalized !== revision.remote) invalidShape("/subject/soul/revision/remote", "noncanonical observed remote");
|
|
194
|
-
if (revision.identity.kind === "canonical-remote" && revision.identity.remote !== revision.remote) invalidShape("/subject/soul/revision/remote", "observed remote contradicts canonical source identity");
|
|
195
|
-
revisionSelector(revision.selector);
|
|
196
|
-
stringAt(revision.commit, "/subject/soul/revision/commit", { pattern: /^[a-f0-9]{40}$/ });
|
|
197
|
-
}
|
|
198
|
-
origins(revision.provenance, "/subject/soul/revision/provenance");
|
|
199
|
-
if (!revision.provenance.length) invalidShape("/subject/soul/revision/provenance", "source witness is missing", "resolution-incomplete");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function validateArtifactSet(value) {
|
|
203
|
-
canonicalJson(value);
|
|
204
|
-
objectAt(value, ["schemaVersion", "packages", "capabilities"], ["schemaVersion", "packages", "capabilities"]);
|
|
205
|
-
versionAt(value.schemaVersion);
|
|
206
|
-
objectAt(value.packages, null, []); objectAt(value.capabilities, null, []);
|
|
207
|
-
for (const [id, row] of Object.entries(value.packages)) {
|
|
208
|
-
capability(id, "/artifacts/packages");
|
|
209
|
-
objectAt(row, ["source", "path", "version", "commit", "integrity", "dependencies"], ["source", "path", "version", "commit", "integrity", "dependencies"]);
|
|
210
|
-
const source = parseLockedSource3(row.source, row.path);
|
|
211
|
-
stringAt(row.version, "/artifacts/packages/version");
|
|
212
|
-
validateIntegrity(row.integrity, [PACKAGE_FORMAT]);
|
|
213
|
-
if (typeof row.commit !== "string" || (source.kind === "path" ? row.commit !== "local" : !/^[a-f0-9]{40}$/.test(row.commit))) invalidShape("/artifacts/packages/commit", "invalid exact package revision");
|
|
214
|
-
stringSetAt(row.dependencies, "/artifacts/packages/dependencies", capability);
|
|
215
|
-
orderedSet(row.dependencies, "/artifacts/packages/dependencies");
|
|
216
|
-
for (const dependency of row.dependencies) own(value.packages, dependency, "/artifacts/packages/dependencies");
|
|
217
|
-
}
|
|
218
|
-
// Iterative bounded DFS: no recursive dependency traversal or prototype lookup.
|
|
219
|
-
const visited = new Set(), active = new Set();
|
|
220
|
-
for (const id of Object.keys(value.packages)) {
|
|
221
|
-
const stack = [{ id, exit: false }];
|
|
222
|
-
while (stack.length) {
|
|
223
|
-
const current = stack.pop();
|
|
224
|
-
if (current.exit) { active.delete(current.id); visited.add(current.id); continue; }
|
|
225
|
-
if (active.has(current.id)) invalidShape("/artifacts/packages", "package dependency cycle");
|
|
226
|
-
if (visited.has(current.id)) continue;
|
|
227
|
-
if (active.size > 256) invalidShape("/artifacts/packages", "package graph depth limit", "resource-limit");
|
|
228
|
-
active.add(current.id); stack.push({ id: current.id, exit: true });
|
|
229
|
-
for (const dependency of value.packages[current.id].dependencies) stack.push({ id: dependency, exit: false });
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
for (const [id, row] of Object.entries(value.capabilities)) {
|
|
233
|
-
capability(id, "/artifacts/capabilities");
|
|
234
|
-
objectAt(row, ["version", "artifact", "origin"], ["version", "artifact", "origin"]);
|
|
235
|
-
stringAt(row.version, "/artifacts/capabilities/version"); validateArtifactRef(row.artifact);
|
|
236
|
-
if (row.artifact.kind !== "capability" || row.artifact.capability !== id) invalidShape("/artifacts/capabilities", "artifact/map identity mismatch");
|
|
237
|
-
if (row.origin?.kind === "package") {
|
|
238
|
-
objectAt(row.origin, ["kind", "package", "path", "projectionVersion"], ["kind", "package", "path", "projectionVersion"]);
|
|
239
|
-
capability(row.origin.package, "/artifacts/capabilities/origin/package");
|
|
240
|
-
own(value.packages, row.origin.package, "/artifacts/capabilities/origin/package");
|
|
241
|
-
portablePath(row.origin.path, { allowRoot: true }); versionAt(row.origin.projectionVersion);
|
|
242
|
-
} else {
|
|
243
|
-
objectAt(row.origin, ["kind", "source", "authoredAs", "witness"], ["kind", "source", "authoredAs", "witness"]);
|
|
244
|
-
if (row.origin.kind !== "local-capability" || !["owned", "path"].includes(row.origin.authoredAs)
|
|
245
|
-
|| parseLockedSource3(row.origin.source, ".").kind !== "path") invalidShape("/artifacts/capabilities/origin", "invalid local provenance");
|
|
246
|
-
validateOrigin(row.origin.witness);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return value;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function validateComposition(composition, record) {
|
|
253
|
-
objectAt(composition, ["schemaVersion", "mode", "body", "blocks", "skills", "omissions"], ["schemaVersion", "mode", "body", "blocks", "skills", "omissions"]);
|
|
254
|
-
versionAt(composition.schemaVersion);
|
|
255
|
-
if (!["worktree", "checkout", "attached", "workspace", "directory"].includes(composition.mode)) invalidShape("/dispatch/composition/mode", "invalid work mode");
|
|
256
|
-
const body = own(record.resources, composition.body, "/dispatch/composition/body");
|
|
257
|
-
const subject = record.subject, definition = subject.kind === "persistent" ? subject.soul.definition : subject.definition.path;
|
|
258
|
-
const owner = subject.kind === "persistent" ? subject.soul.sourceArtifact : subject.provider;
|
|
259
|
-
const bodyPath = [...definition.split("/").slice(0, -1), "AGENTS.md"].join("/");
|
|
260
|
-
if (body.kind !== "file" || body.path !== bodyPath || !sameIdentity(body.owner, owner)) invalidShape("/dispatch/composition/body", "composition body is not the captured canonical soul instructions");
|
|
261
|
-
const sources = new Set(), names = new Set();
|
|
262
|
-
const source = (label) => {
|
|
263
|
-
stringAt(label, "/dispatch/composition/source", { pattern: /^(kernel|work-mode|capability|config):[A-Za-z0-9._:-]+$/ });
|
|
264
|
-
if (sources.has(label)) invalidShape("/dispatch/composition", "duplicate instruction source or contradictory omission");
|
|
265
|
-
sources.add(label);
|
|
266
|
-
};
|
|
267
|
-
for (const block of array(composition.blocks, "/dispatch/composition/blocks")) {
|
|
268
|
-
objectAt(block, ["source", "resource", "choice"], ["source", "resource"]); source(block.source);
|
|
269
|
-
const resource = own(record.resources, block.resource, "/dispatch/composition/blocks");
|
|
270
|
-
if (resource.kind !== "file" || block.resource === composition.body) invalidShape("/dispatch/composition/blocks", "instruction block is not an independent file");
|
|
271
|
-
if (block.choice !== undefined && own(record.choices, block.choice, "/choices").value !== block.resource) invalidShape("/dispatch/composition/blocks", "instruction override differs from captured choice");
|
|
272
|
-
}
|
|
273
|
-
for (const omission of array(composition.omissions, "/dispatch/composition/omissions")) {
|
|
274
|
-
objectAt(omission, ["source", "reason", "choice"], ["source", "reason"]); source(omission.source);
|
|
275
|
-
if (omission.reason === "disabled") {
|
|
276
|
-
if (own(record.choices, omission.choice, "/choices").value !== null) invalidShape("/dispatch/composition/omissions", "disabled injection lacks explicit none choice");
|
|
277
|
-
} else if (omission.reason === "helper-policy") {
|
|
278
|
-
if (subject.kind !== "helper" || !omission.source.startsWith("capability:")
|
|
279
|
-
|| omission.choice !== `/helpers/injections/${pointerKey(omission.source.slice("capability:".length))}`
|
|
280
|
-
|| own(record.choices, omission.choice, "/choices").value !== null) invalidShape("/dispatch/composition/omissions", "helper omission lacks its own explicit none choice");
|
|
281
|
-
} else if (omission.reason !== "helper-knowledge" || subject.kind !== "helper" || !omission.source.startsWith("capability:") || omission.choice !== undefined) {
|
|
282
|
-
invalidShape("/dispatch/composition/omissions", "invalid injection omission");
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
for (const skill of array(composition.skills, "/dispatch/composition/skills")) {
|
|
286
|
-
objectAt(skill, ["name", "resource"], ["name", "resource"]);
|
|
287
|
-
stringAt(skill.name, "/dispatch/composition/skills/name", { pattern: /^[A-Za-z0-9][A-Za-z0-9._-]*$/ });
|
|
288
|
-
if (names.has(skill.name)) invalidShape("/dispatch/composition/skills", "duplicate captured skill name");
|
|
289
|
-
names.add(skill.name);
|
|
290
|
-
if (own(record.resources, skill.resource, "/resources").kind !== "skill") invalidShape("/dispatch/composition/skills", "wrong captured skill kind");
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** Shared exact subject union for retained records and invocation projections. */
|
|
295
|
-
export function validateCapturedSubject(subject) {
|
|
296
|
-
canonicalJson(subject);
|
|
297
|
-
if (subject?.kind === "persistent") {
|
|
298
|
-
objectAt(subject, ["kind", "soul"], ["kind", "soul"]); sourceSelection(subject.soul);
|
|
299
|
-
} else {
|
|
300
|
-
objectAt(subject, ["kind", "provider", "definition", "name"], ["kind", "provider", "definition", "name"]);
|
|
301
|
-
if (subject.kind !== "helper") invalidShape("/subject/kind", "invalid resolution subject");
|
|
302
|
-
validateArtifactRef(subject.provider); validateResourceRef(subject.definition);
|
|
303
|
-
if (subject.provider.kind !== "capability" || !sameIdentity(subject.provider, subject.definition.owner)) invalidShape("/subject", "helper provider/definition mismatch");
|
|
304
|
-
stringAt(subject.name, "/subject/name", { pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ });
|
|
305
|
-
}
|
|
306
|
-
return subject;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export function validateCapturedContext(context) {
|
|
310
|
-
canonicalJson(context);
|
|
311
|
-
if (context?.kind === "workspace") {
|
|
312
|
-
objectAt(context, ["kind", "identity", "observation"], ["kind", "identity", "observation"]);
|
|
313
|
-
validateWorkspaceIdentity(context.identity); validateOrigin(context.observation);
|
|
314
|
-
} else {
|
|
315
|
-
objectAt(context, ["kind", "key"], ["kind", "key"]);
|
|
316
|
-
if (context.kind !== "standalone") invalidShape("/context/kind", "invalid resolution context");
|
|
317
|
-
if (context.key !== null) stringAt(context.key, "/context/key");
|
|
318
|
-
}
|
|
319
|
-
return context;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
export function validateResolutionShape(value) {
|
|
323
|
-
canonicalJson(value);
|
|
324
|
-
objectAt(value, RESOLUTION_FIELDS, RESOLUTION_FIELDS);
|
|
325
|
-
versionAt(value.schemaVersion);
|
|
326
|
-
if (!["prepared", "reconstructed"].includes(value.capture)) invalidShape("/capture", "incomplete evidence is not a captured resolution", "resolution-incomplete");
|
|
327
|
-
validateArtifactSet(value.artifacts);
|
|
328
|
-
validateCapturedSubject(value.subject);
|
|
329
|
-
validateCapturedContext(value.context);
|
|
330
|
-
for (const key of ["choices", "bindings", "resources", "helpers"]) objectAt(value[key], null, [], `/${key}`);
|
|
331
|
-
origins(value.evidence, "/evidence");
|
|
332
|
-
if (value.capture === "reconstructed" && !value.evidence.length) invalidShape("/evidence", "reconstruction requires evidence", "resolution-incomplete");
|
|
333
|
-
const owners = new Set();
|
|
334
|
-
const addOwner = (ref) => { validateArtifactRef(ref); owners.add(canonicalJson(ref)); };
|
|
335
|
-
for (const row of Object.values(value.artifacts.capabilities)) addOwner(row.artifact);
|
|
336
|
-
if (value.subject.kind === "persistent") addOwner(value.subject.soul.sourceArtifact);
|
|
337
|
-
for (const ref of array(value.resourceBundles, "/resourceBundles")) {
|
|
338
|
-
if (ref?.kind !== "resource") invalidShape("/resourceBundles", "expected managed resource bundle");
|
|
339
|
-
const key = canonicalJson(ref); if (owners.has(key)) invalidShape("/resourceBundles", "duplicate resource bundle"); addOwner(ref);
|
|
340
|
-
}
|
|
341
|
-
orderedSet(value.resourceBundles, "/resourceBundles", canonicalJson);
|
|
342
|
-
if (value.subject.kind === "helper" && !owners.has(canonicalJson(value.subject.provider))) invalidShape("/subject/provider", "helper provider is not selected");
|
|
343
|
-
for (const resource of Object.values(value.resources)) {
|
|
344
|
-
validateResourceRef(resource);
|
|
345
|
-
if (!owners.has(canonicalJson(resource.owner))) invalidShape("/resources", "resource owner is not selected");
|
|
346
|
-
}
|
|
347
|
-
for (const ref of Object.values(value.helpers)) validateResolutionRef(ref);
|
|
348
|
-
objectAt(value.dispatch, ["schemaVersion", "providerManifests", "settingsChoices", "launch", "runtimePackages", "hostRequirements", "workTargetInputs", "composition"], ["schemaVersion", "providerManifests", "settingsChoices", "launch", "runtimePackages", "hostRequirements", "workTargetInputs"]);
|
|
349
|
-
versionAt(value.dispatch.schemaVersion);
|
|
350
|
-
for (const key of ["providerManifests", "settingsChoices", "workTargetInputs"]) objectAt(value.dispatch[key], null, [], `/dispatch/${key}`);
|
|
351
|
-
const manifests = value.dispatch.providerManifests;
|
|
352
|
-
if (Object.keys(manifests).length !== Object.keys(value.artifacts.capabilities).length) invalidShape("/dispatch/providerManifests", "selected manifest set mismatch");
|
|
353
|
-
for (const [id, row] of Object.entries(value.artifacts.capabilities)) {
|
|
354
|
-
const resource = own(value.resources, own(manifests, id, "/dispatch/providerManifests"), "/resources");
|
|
355
|
-
if (!sameIdentity(resource.owner, row.artifact) || resource.kind !== "manifest" || resource.path !== "oats.json") invalidShape("/dispatch/providerManifests", "manifest owner/path mismatch");
|
|
356
|
-
}
|
|
357
|
-
for (const key of Object.values(value.dispatch.workTargetInputs)) own(value.resources, key, "/dispatch/workTargetInputs");
|
|
358
|
-
for (const [id, settings] of Object.entries(value.dispatch.settingsChoices)) {
|
|
359
|
-
own(value.artifacts.capabilities, id, "/dispatch/settingsChoices");
|
|
360
|
-
objectAt(settings, null, [], "/dispatch/settingsChoices");
|
|
361
|
-
for (const [name, key] of Object.entries(settings)) {
|
|
362
|
-
if (key !== `/settings/${pointerKey(id)}/${pointerKey(name)}`) invalidShape("/dispatch/settingsChoices", "setting reference is not its canonical choice key");
|
|
363
|
-
own(value.choices, key, "/choices");
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (value.dispatch.composition !== undefined) validateComposition(value.dispatch.composition, value);
|
|
367
|
-
if (value.dispatch.launch !== null) {
|
|
368
|
-
if (!value.dispatch.composition) invalidShape("/dispatch/composition", "launchable capture lacks instruction/resource composition", "resolution-incomplete");
|
|
369
|
-
objectAt(value.dispatch.launch, null, ["version", "runtime"], "/dispatch/launch");
|
|
370
|
-
versionAt(value.dispatch.launch.version);
|
|
371
|
-
if (!["pi", "claude", "codex"].includes(value.dispatch.launch.runtime)) invalidShape("/dispatch/launch/runtime", "invalid captured runtime");
|
|
372
|
-
if (value.dispatch.launch.executableResource !== undefined || value.dispatch.launch.executable === "captured-resource") {
|
|
373
|
-
const executable = own(value.resources, value.dispatch.launch.executableResource, "/dispatch/launch/executableResource");
|
|
374
|
-
const entrypoint = value.dispatch.launch.entrypoint;
|
|
375
|
-
objectAt(entrypoint, ["capability", "command"], ["capability", "command"], "/dispatch/launch/entrypoint");
|
|
376
|
-
stringAt(entrypoint.command, "/dispatch/launch/entrypoint/command");
|
|
377
|
-
const owner = own(value.artifacts.capabilities, entrypoint.capability, "/dispatch/launch/entrypoint/capability");
|
|
378
|
-
if (value.dispatch.launch.executable !== "captured-resource" || executable.kind !== "file"
|
|
379
|
-
|| !sameIdentity(executable.owner, owner.artifact)
|
|
380
|
-
|| value.dispatch.launch.executableResource !== `executable:${entrypoint.capability}:command:${entrypoint.command}`) invalidShape("/dispatch/launch/executableResource", "captured launch executable must be its exact declared retained command resource");
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
for (const runtime of array(value.dispatch.runtimePackages, "/dispatch/runtimePackages")) {
|
|
384
|
-
objectAt(runtime, ["runtime", "package", "resource", "requiredBy"], ["runtime", "package", "resource", "requiredBy"]);
|
|
385
|
-
stringAt(runtime.runtime, "/dispatch/runtimePackages/runtime"); stringAt(runtime.package, "/dispatch/runtimePackages/package");
|
|
386
|
-
const resource = own(value.resources, runtime.resource, "/dispatch/runtimePackages/resource");
|
|
387
|
-
if (resource.kind !== "runtime-package") invalidShape("/dispatch/runtimePackages/resource", "wrong runtime resource kind");
|
|
388
|
-
origins(runtime.requiredBy, "/dispatch/runtimePackages/requiredBy");
|
|
389
|
-
}
|
|
390
|
-
array(value.dispatch.hostRequirements, "/dispatch/hostRequirements");
|
|
391
|
-
validateCapturedChoices(value.choices); bindings(value.bindings, value); validateMessagingChoice(value.messagingChoice, value);
|
|
392
|
-
return value;
|
|
393
|
-
}
|
package/lib/schedule-capsule.mjs
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/** Immutable software authority for scheduled execution.
|
|
2
|
-
*
|
|
3
|
-
* A definition stores an execution template without an intent ID. Each
|
|
4
|
-
* admitted attempt receives a fresh opaque `executionId`. Two attempts of
|
|
5
|
-
* identical retained bytes are therefore distinct intents; a canonical digest
|
|
6
|
-
* of the template separately witnesses content identity. This module performs
|
|
7
|
-
* no preparation, approval, launch, or ambient resolution. */
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
9
|
-
import { isAbsolute, resolve } from "node:path";
|
|
10
|
-
import { capturedSelector } from "./captured-selector.mjs";
|
|
11
|
-
import { jsonIntegrity } from "./portable-digest.mjs";
|
|
12
|
-
import { canonicalJson } from "./portable-values.mjs";
|
|
13
|
-
import { validateOrigin, validateResolutionRef } from "./resolution-shape.mjs";
|
|
14
|
-
import { oatsError } from "./errors.mjs";
|
|
15
|
-
|
|
16
|
-
export const EXECUTION_CAPSULE_VERSION = 1;
|
|
17
|
-
export const SCHEDULE_ATTEMPT_VERSION = 1;
|
|
18
|
-
export const SCHEDULE_DEFINITION_VERSION = 2;
|
|
19
|
-
export const RECURRENCE_POLICIES = Object.freeze(["capture", "prepare-on-tick"]);
|
|
20
|
-
const TEMPLATE_FIELDS = Object.freeze(["schemaVersion", "resolution", "deployment", "action", "target", "inputRefs", "responsibleHuman"]);
|
|
21
|
-
const CAPSULE_FIELDS = Object.freeze([...TEMPLATE_FIELDS, "executionId"]);
|
|
22
|
-
const COMMAND_TARGET_FIELDS = Object.freeze(["cwd", "argv"]);
|
|
23
|
-
const WAKE_TARGET_FIELDS = Object.freeze(["home", "message"]);
|
|
24
|
-
const BINDING_FIELDS = Object.freeze(["schemaVersion", "deployment", "resolution"]);
|
|
25
|
-
const ACTION_FIELDS = Object.freeze(["kind", "name"]);
|
|
26
|
-
const PREPARATION_FIELDS = Object.freeze(["deployment", "source", "origin", "workspace", "member", "operator", "mode", "allowLocalPaths"]);
|
|
27
|
-
const SOURCE_FIELDS = Object.freeze(["source", "soul", "revision", "alias"]);
|
|
28
|
-
const EXECUTION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
29
|
-
|
|
30
|
-
function exactObject(value, fields, where) {
|
|
31
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw oatsError("invalid-declaration", `${where} must be an object`);
|
|
32
|
-
const keys = Object.keys(value).sort(), expected = [...fields].sort();
|
|
33
|
-
if (keys.length !== expected.length || keys.some((key, i) => key !== expected[i])) {
|
|
34
|
-
throw oatsError("invalid-declaration", `${where} has unexpected or missing fields`);
|
|
35
|
-
}
|
|
36
|
-
return value;
|
|
37
|
-
}
|
|
38
|
-
function same(a, b) { return canonicalJson(a) === canonicalJson(b); }
|
|
39
|
-
function copied(value) { return JSON.parse(canonicalJson(value)); }
|
|
40
|
-
|
|
41
|
-
/** Parse only the already-public captured capability-command form. Selectors
|
|
42
|
-
* after `--` are child data and cannot become scheduler authority. */
|
|
43
|
-
export function capturedCommandTarget(target) {
|
|
44
|
-
canonicalJson(target, { maxBytes: 1024 * 1024, maxDepth: 32, maxEntries: 20_000 });
|
|
45
|
-
exactObject(target, COMMAND_TARGET_FIELDS, "execution target");
|
|
46
|
-
if (typeof target.cwd !== "string" || !isAbsolute(target.cwd)) throw oatsError("invalid-declaration", "execution target cwd must be absolute");
|
|
47
|
-
if (!Array.isArray(target.argv) || target.argv.length < 3 || target.argv[0] !== "oats"
|
|
48
|
-
|| target.argv.some((arg) => typeof arg !== "string" || arg.includes("\0"))) {
|
|
49
|
-
throw oatsError("invalid-declaration", "execution target argv must be an oats command without NUL values");
|
|
50
|
-
}
|
|
51
|
-
if (!target.argv.includes("--json")) {
|
|
52
|
-
throw oatsError("invalid-declaration", "captured schedule argv must save --json explicitly; admission cannot mutate the capsule target");
|
|
53
|
-
}
|
|
54
|
-
const selector = capturedSelector(target.argv.slice(1), {});
|
|
55
|
-
if (!selector?.explicit || selector.artifactSet !== undefined) {
|
|
56
|
-
throw oatsError("invalid-declaration", "captured schedule command needs explicit --deployment and --resolution selectors in saved argv");
|
|
57
|
-
}
|
|
58
|
-
if (resolve(selector.deployment) !== selector.deployment) throw oatsError("invalid-declaration", "captured schedule deployment selector must be a normalized absolute path");
|
|
59
|
-
const end = selector.args.indexOf("--"), head = end < 0 ? selector.args : selector.args.slice(0, end);
|
|
60
|
-
const [namespace, command] = head;
|
|
61
|
-
if (typeof namespace !== "string" || !namespace || namespace.startsWith("-")
|
|
62
|
-
|| typeof command !== "string" || !command || command.startsWith("-")) {
|
|
63
|
-
throw oatsError("invalid-declaration", "captured schedule command needs a capability namespace and command");
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
deployment: resolve(selector.deployment),
|
|
67
|
-
resolution: copied(selector.resolution),
|
|
68
|
-
action: { kind: "command", name: `${namespace}:${command}` },
|
|
69
|
-
dispatchAction: { kind: "command", namespace, name: command },
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function validatePreparationInput(value) {
|
|
74
|
-
canonicalJson(value, { maxBytes: 1024 * 1024, maxDepth: 64, maxEntries: 20_000 });
|
|
75
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw oatsError("invalid-declaration", "preparation must be an object");
|
|
76
|
-
for (const key of Object.keys(value)) if (!PREPARATION_FIELDS.includes(key)) throw oatsError("invalid-declaration", `unsupported preparation field ${key}`);
|
|
77
|
-
if (!Object.hasOwn(value, "deployment") || !Object.hasOwn(value, "source")) throw oatsError("invalid-declaration", "preparation needs deployment and source");
|
|
78
|
-
if (typeof value.deployment !== "string" || !isAbsolute(value.deployment) || resolve(value.deployment) !== value.deployment) throw oatsError("invalid-declaration", "preparation deployment must be a normalized absolute path");
|
|
79
|
-
if (typeof value.source === "string") {
|
|
80
|
-
if (!value.source || !value.workspace) throw oatsError("invalid-declaration", "workspace alias preparation needs a non-empty source alias and workspace request");
|
|
81
|
-
} else {
|
|
82
|
-
exactObject(value.source, SOURCE_FIELDS, "preparation source");
|
|
83
|
-
for (const key of SOURCE_FIELDS) if (typeof value.source[key] !== "string" || !value.source[key]) throw oatsError("invalid-declaration", `preparation source ${key} must be non-empty text`);
|
|
84
|
-
}
|
|
85
|
-
if (value.origin !== undefined) validateOrigin(value.origin);
|
|
86
|
-
if (value.mode !== undefined && (typeof value.mode !== "string" || !value.mode.trim())) throw oatsError("invalid-declaration", "preparation mode must be a work-mode string");
|
|
87
|
-
if (value.allowLocalPaths !== undefined && typeof value.allowLocalPaths !== "boolean") throw oatsError("invalid-declaration", "preparation allowLocalPaths must be boolean");
|
|
88
|
-
return copied(value);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function commandArgvWithExecutionBinding(argv, binding) {
|
|
92
|
-
validateExecutionBinding(binding);
|
|
93
|
-
canonicalJson(argv, { maxBytes: 1024 * 1024, maxDepth: 8, maxEntries: 20_000 });
|
|
94
|
-
if (!Array.isArray(argv) || argv.length < 3 || argv[0] !== "oats" || argv.some((arg) => typeof arg !== "string" || arg.includes("\0"))) throw oatsError("invalid-declaration", "prepare-on-tick argv must be an oats command without NUL values");
|
|
95
|
-
let selected;
|
|
96
|
-
try { selected = capturedSelector(argv.slice(1), {}); } catch (error) { throw oatsError("invalid-declaration", `prepare-on-tick argv has invalid selectors: ${error.message}`); }
|
|
97
|
-
if (selected) throw oatsError("invalid-declaration", "prepare-on-tick argv cannot contain a captured selector before preparation");
|
|
98
|
-
if (!argv.includes("--json")) throw oatsError("invalid-declaration", "prepare-on-tick argv must save --json explicitly");
|
|
99
|
-
const end = argv.indexOf("--"), insertAt = end < 0 ? argv.length : end;
|
|
100
|
-
return [...argv.slice(0, insertAt), "--deployment", binding.deployment, "--resolution", binding.resolution.id, ...argv.slice(insertAt)];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function validateExecutionBinding(value) {
|
|
104
|
-
canonicalJson(value, { maxBytes: 64 * 1024, maxDepth: 8, maxEntries: 32 });
|
|
105
|
-
exactObject(value, BINDING_FIELDS, "execution binding");
|
|
106
|
-
if (value.schemaVersion !== 1) throw oatsError("unsupported-wire-version", "unsupported execution binding version");
|
|
107
|
-
if (typeof value.deployment !== "string" || !isAbsolute(value.deployment) || resolve(value.deployment) !== value.deployment) throw oatsError("invalid-declaration", "execution binding deployment must be a normalized absolute path");
|
|
108
|
-
validateResolutionRef(value.resolution);
|
|
109
|
-
return value;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function templateOf(value) {
|
|
113
|
-
return {
|
|
114
|
-
schemaVersion: value.schemaVersion,
|
|
115
|
-
resolution: value.resolution,
|
|
116
|
-
deployment: value.deployment,
|
|
117
|
-
action: value.action,
|
|
118
|
-
target: value.target,
|
|
119
|
-
inputRefs: value.inputRefs,
|
|
120
|
-
responsibleHuman: value.responsibleHuman,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function executionContentIntegrity(value) { return jsonIntegrity(templateOf(value)); }
|
|
125
|
-
|
|
126
|
-
export function buildCommandExecutionTemplate({ cwd, argv, inputRefs = {}, responsibleHuman = null }) {
|
|
127
|
-
canonicalJson({ cwd, argv }, { maxBytes: 1024 * 1024, maxDepth: 32, maxEntries: 20_000 });
|
|
128
|
-
if (typeof cwd !== "string" || !isAbsolute(cwd) || !Array.isArray(argv)) throw oatsError("invalid-declaration", "captured command needs an absolute cwd and argv array");
|
|
129
|
-
const target = copied({ cwd: resolve(cwd), argv });
|
|
130
|
-
const parsed = capturedCommandTarget(target);
|
|
131
|
-
if (!inputRefs || typeof inputRefs !== "object" || Array.isArray(inputRefs)) throw oatsError("invalid-declaration", "execution inputRefs must be an object");
|
|
132
|
-
if (responsibleHuman !== null && (!responsibleHuman || typeof responsibleHuman !== "object" || Array.isArray(responsibleHuman))) {
|
|
133
|
-
throw oatsError("invalid-declaration", "execution responsibleHuman must be an object or null");
|
|
134
|
-
}
|
|
135
|
-
return copied({
|
|
136
|
-
schemaVersion: EXECUTION_CAPSULE_VERSION,
|
|
137
|
-
resolution: parsed.resolution,
|
|
138
|
-
deployment: parsed.deployment,
|
|
139
|
-
action: parsed.action,
|
|
140
|
-
target,
|
|
141
|
-
inputRefs,
|
|
142
|
-
responsibleHuman,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function buildWakeExecutionTemplate({ home, message, executionBinding, inputRefs = {}, responsibleHuman }) {
|
|
147
|
-
validateExecutionBinding(executionBinding);
|
|
148
|
-
canonicalJson({ home, message, inputRefs, responsibleHuman }, { maxBytes: 1024 * 1024, maxDepth: 32, maxEntries: 20_000 });
|
|
149
|
-
if (typeof home !== "string" || !isAbsolute(home) || resolve(home) !== home) throw oatsError("invalid-declaration", "captured wake home must be a normalized absolute path");
|
|
150
|
-
if (typeof message !== "string" || !message || message.includes("\0")) throw oatsError("invalid-declaration", "captured wake message must be non-empty text without NUL");
|
|
151
|
-
if (!inputRefs || typeof inputRefs !== "object" || Array.isArray(inputRefs)) throw oatsError("invalid-declaration", "execution inputRefs must be an object");
|
|
152
|
-
if (responsibleHuman === undefined || (responsibleHuman !== null && (!responsibleHuman || typeof responsibleHuman !== "object" || Array.isArray(responsibleHuman)))) {
|
|
153
|
-
throw oatsError("invalid-declaration", "captured wake responsibleHuman must be an explicit object or null");
|
|
154
|
-
}
|
|
155
|
-
return copied({ schemaVersion: EXECUTION_CAPSULE_VERSION, resolution: executionBinding.resolution,
|
|
156
|
-
deployment: executionBinding.deployment, action: { kind: "wake", name: "session" },
|
|
157
|
-
target: { home, message }, inputRefs, responsibleHuman });
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function validateExecutionTemplate(value) {
|
|
161
|
-
canonicalJson(value, { maxBytes: 1024 * 1024, maxDepth: 64, maxEntries: 20_000 });
|
|
162
|
-
exactObject(value, TEMPLATE_FIELDS, "execution template");
|
|
163
|
-
if (value.schemaVersion !== EXECUTION_CAPSULE_VERSION) throw oatsError("unsupported-wire-version", "unsupported execution template version");
|
|
164
|
-
validateResolutionRef(value.resolution);
|
|
165
|
-
if (typeof value.deployment !== "string" || !isAbsolute(value.deployment) || resolve(value.deployment) !== value.deployment) throw oatsError("invalid-declaration", "execution deployment must be a normalized absolute path");
|
|
166
|
-
exactObject(value.action, ACTION_FIELDS, "execution action");
|
|
167
|
-
if (value.action.kind === "command") {
|
|
168
|
-
if (typeof value.action.name !== "string" || !/^[^:\s]+:[^:\s]+$/.test(value.action.name)) throw oatsError("invalid-declaration", "captured command action name must be namespace:command");
|
|
169
|
-
} else if (value.action.kind === "wake") {
|
|
170
|
-
if (value.action.name !== "session") throw oatsError("invalid-declaration", "captured wake action must name session");
|
|
171
|
-
} else throw oatsError("invalid-declaration", "unsupported scheduled execution action");
|
|
172
|
-
if (!value.inputRefs || typeof value.inputRefs !== "object" || Array.isArray(value.inputRefs)) throw oatsError("invalid-declaration", "execution inputRefs must be an object");
|
|
173
|
-
if (value.responsibleHuman !== null && (!value.responsibleHuman || typeof value.responsibleHuman !== "object" || Array.isArray(value.responsibleHuman))) {
|
|
174
|
-
throw oatsError("invalid-declaration", "execution responsibleHuman must be an object or null");
|
|
175
|
-
}
|
|
176
|
-
if (value.action.kind === "command") {
|
|
177
|
-
const parsed = capturedCommandTarget(value.target);
|
|
178
|
-
if (parsed.deployment !== value.deployment || !same(parsed.resolution, value.resolution)
|
|
179
|
-
|| !same(parsed.action, value.action)) throw oatsError("invalid-declaration", "execution template disagrees with its saved command selectors");
|
|
180
|
-
} else {
|
|
181
|
-
exactObject(value.target, WAKE_TARGET_FIELDS, "wake target");
|
|
182
|
-
if (typeof value.target.home !== "string" || !isAbsolute(value.target.home) || resolve(value.target.home) !== value.target.home) throw oatsError("invalid-declaration", "captured wake home must be a normalized absolute path");
|
|
183
|
-
if (typeof value.target.message !== "string" || !value.target.message || value.target.message.includes("\0")) throw oatsError("invalid-declaration", "captured wake message must be non-empty text without NUL");
|
|
184
|
-
}
|
|
185
|
-
return value;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function admitExecutionTemplate(template, { executionId = randomUUID() } = {}) {
|
|
189
|
-
validateExecutionTemplate(template);
|
|
190
|
-
if (typeof executionId !== "string" || !EXECUTION_ID_RE.test(executionId)) throw oatsError("invalid-declaration", "executionId must be a non-empty opaque identifier");
|
|
191
|
-
return validateExecutionCapsule({ ...copied(template), executionId });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export function validateExecutionCapsule(value) {
|
|
195
|
-
canonicalJson(value, { maxBytes: 1024 * 1024, maxDepth: 64, maxEntries: 20_000 });
|
|
196
|
-
exactObject(value, CAPSULE_FIELDS, "execution capsule");
|
|
197
|
-
if (typeof value.executionId !== "string" || !EXECUTION_ID_RE.test(value.executionId)) throw oatsError("invalid-declaration", "executionId must be a non-empty opaque identifier");
|
|
198
|
-
const { executionId: _executionId, ...template } = value;
|
|
199
|
-
validateExecutionTemplate(template);
|
|
200
|
-
return value;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export function capturedDispatchAction(value) {
|
|
204
|
-
const template = Object.hasOwn(value, "executionId") ? (() => { validateExecutionCapsule(value); const { executionId: _executionId, ...rest } = value; return rest; })() : validateExecutionTemplate(value);
|
|
205
|
-
return template.action.kind === "command" ? capturedCommandTarget(template.target).dispatchAction : template.action;
|
|
206
|
-
}
|