@awebai/oats 0.23.2 → 0.24.1

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.
Files changed (172) hide show
  1. package/README.md +224 -391
  2. package/bin/oats-pi-sdk-host.mjs +17 -0
  3. package/bin/oats.mjs +470 -51
  4. package/capabilities/oats-okf/bin/oats-okf-binding.mjs +14 -0
  5. package/capabilities/oats-okf/bin/oats-okf.mjs +79 -11
  6. package/capabilities/oats-okf/lib/binding-wire.mjs +268 -0
  7. package/capabilities/oats-okf/lib/captured-worker.mjs +109 -0
  8. package/capabilities/oats-okf/lib/config.mjs +2 -1
  9. package/capabilities/oats-okf/lib/inspection.mjs +16 -1
  10. package/capabilities/oats-okf/lib/invocation-context.mjs +111 -0
  11. package/capabilities/oats-okf/lib/invocation-shape.mjs +135 -0
  12. package/capabilities/oats-okf/lib/io.mjs +1 -1
  13. package/capabilities/oats-okf/lib/portable-binding.mjs +199 -0
  14. package/capabilities/oats-okf/lib/source-contract.mjs +46 -0
  15. package/capabilities/oats-okf/lib/sources.mjs +123 -3
  16. package/capabilities/oats-okf/lib/stores.mjs +104 -25
  17. package/capabilities/oats-okf/lib/worker.mjs +69 -10
  18. package/capabilities/oats-okf/oats.json +35 -7
  19. package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +87 -0
  20. package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +113 -0
  21. package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +23 -5
  22. package/capabilities/oats-okf/skills/okf/SKILL.md +42 -1
  23. package/docs/artifact-approvals.schema.json +7 -0
  24. package/docs/capabilities.md +4 -0
  25. package/docs/capability-manifest.schema.json +37 -66
  26. package/docs/captured-invocation-context.schema.json +7 -0
  27. package/docs/captured-resolution.schema.json +7 -0
  28. package/docs/design/2026-09-14-artifact-retention-contract.md +190 -0
  29. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +708 -0
  30. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +85 -0
  31. package/docs/design/2026-09-14-portable-souls-explainer.md +750 -0
  32. package/docs/design/2026-09-15-captured-dispatch.md +127 -0
  33. package/docs/design/2026-09-15-captured-resolution-records.md +143 -0
  34. package/docs/design/2026-09-15-package-preparation.md +100 -0
  35. package/docs/design/2026-09-15-portable-data-contract.md +121 -0
  36. package/docs/design/2026-09-15-portable-declarations.md +189 -0
  37. package/docs/design/2026-09-15-portable-souls-handoff.md +150 -0
  38. package/docs/design/2026-09-15-portable-souls-implementation.md +417 -0
  39. package/docs/design/2026-09-15-selection-lock-and-approval.md +122 -0
  40. package/docs/design/2026-09-15-source-observation.md +119 -0
  41. package/docs/design/2026-09-16-captured-admission.md +77 -0
  42. package/docs/design/2026-09-16-captured-helper-dispatch.md +105 -0
  43. package/docs/design/2026-09-16-captured-launch-inputs.md +42 -0
  44. package/docs/design/2026-09-16-command-profile-preparation.md +86 -0
  45. package/docs/design/2026-09-16-fresh-install-first-rollout.md +47 -0
  46. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +277 -0
  47. package/docs/design/2026-09-16-knowledge-capability-contract.md +61 -0
  48. package/docs/design/2026-09-16-messaging-capability-contract.md +59 -0
  49. package/docs/design/2026-09-16-portable-migration-evidence.md +156 -0
  50. package/docs/design/2026-09-16-portable-onboarding.md +177 -0
  51. package/docs/design/2026-09-16-prepare-request-transport.md +26 -0
  52. package/docs/design/2026-09-16-provider-binding-codecs.md +98 -0
  53. package/docs/design/2026-09-16-provider-binding-wire.md +247 -0
  54. package/docs/design/2026-09-17-capability-helper-input-contract.md +95 -0
  55. package/docs/design/2026-09-17-captured-backend-parity.md +53 -0
  56. package/docs/design/2026-09-17-captured-native-start.md +58 -0
  57. package/docs/design/2026-09-17-portable-boundary-hookup.md +19 -0
  58. package/docs/design/2026-09-17-portable-boundary-resources.md +52 -0
  59. package/docs/design/2026-09-17-public-captured-start.md +108 -0
  60. package/docs/design/2026-09-17-public-prepare-request.md +90 -0
  61. package/docs/design/2026-09-18-captured-pi-host.md +205 -0
  62. package/docs/design/2026-09-18-first-cut-release-checklist.md +131 -0
  63. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +60 -0
  64. package/docs/design/2026-09-20-redesign-program-board.md +70 -0
  65. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +287 -0
  66. package/docs/design/2026-09-20-workspace-onboarding-public.md +124 -0
  67. package/docs/design/README.md +42 -0
  68. package/docs/desktop-cli-api.md +5 -2
  69. package/docs/execution-capsule.schema.json +108 -0
  70. package/docs/execution-targets.md +20 -7
  71. package/docs/first-team.md +1 -1
  72. package/docs/knowledge-theory.md +353 -111
  73. package/docs/knowledge.md +10 -1
  74. package/docs/layers.md +89 -354
  75. package/docs/oats-lock-v3.schema.json +7 -0
  76. package/docs/oats-member.schema.json +38 -0
  77. package/docs/oats-workspace.schema.json +68 -0
  78. package/docs/official-marketplace.md +79 -0
  79. package/docs/packages.md +4 -0
  80. package/docs/portable.schema.json +2512 -0
  81. package/docs/provider-check-input.schema.json +7 -0
  82. package/docs/release-notes/v0.24.0.md +104 -0
  83. package/docs/release-notes/v0.24.1.md +17 -0
  84. package/docs/schedules.md +126 -14
  85. package/docs/soul.schema.json +82 -0
  86. package/docs/souls-and-instances.md +20 -7
  87. package/docs/workspace-adoption.md +285 -0
  88. package/docs/workspaces.md +154 -0
  89. package/injects/oats-portable.md +16 -0
  90. package/injects/portable-instance-boundary.md +39 -0
  91. package/injects/portable-work-directory.md +29 -0
  92. package/lib/artifact-approvals.mjs +120 -0
  93. package/lib/artifact-tree.mjs +141 -0
  94. package/lib/capability-artifacts.mjs +179 -0
  95. package/lib/capability-execution.mjs +15 -0
  96. package/lib/capability-inputs.mjs +39 -0
  97. package/lib/capability-provenance.mjs +231 -0
  98. package/lib/captured-action-shape.mjs +21 -0
  99. package/lib/captured-admission-shape.mjs +20 -0
  100. package/lib/captured-binding-file.mjs +36 -0
  101. package/lib/captured-dispatch.mjs +66 -0
  102. package/lib/captured-instance-index.mjs +277 -0
  103. package/lib/captured-invocation-context.mjs +130 -0
  104. package/lib/captured-launch-request.mjs +46 -0
  105. package/lib/captured-operation-process.mjs +15 -0
  106. package/lib/captured-pi-custody.mjs +29 -0
  107. package/lib/captured-pi-host.mjs +167 -0
  108. package/lib/captured-pi-outcome.mjs +172 -0
  109. package/lib/captured-resolutions.mjs +275 -0
  110. package/lib/captured-scaffold.mjs +87 -0
  111. package/lib/captured-selector.mjs +28 -0
  112. package/lib/captured-session-backend.mjs +52 -0
  113. package/lib/captured-source-receipt-file.mjs +72 -0
  114. package/lib/config-data.mjs +104 -0
  115. package/lib/core.mjs +961 -566
  116. package/lib/errors.mjs +7 -0
  117. package/lib/helper-injection-policy.mjs +98 -0
  118. package/lib/herdr.mjs +18 -7
  119. package/lib/instruction-composition.mjs +31 -0
  120. package/lib/legacy-lock-codec.mjs +106 -0
  121. package/lib/manifest-settings.mjs +84 -0
  122. package/lib/package-closure.mjs +48 -0
  123. package/lib/package-materialization.mjs +83 -0
  124. package/lib/pi-sdk-host.mjs +229 -0
  125. package/lib/portable-artifacts.mjs +115 -0
  126. package/lib/portable-choices.mjs +82 -0
  127. package/lib/portable-composition.mjs +136 -0
  128. package/lib/portable-digest.mjs +105 -0
  129. package/lib/portable-files.mjs +26 -0
  130. package/lib/portable-identity.mjs +40 -0
  131. package/lib/portable-lock.mjs +117 -0
  132. package/lib/portable-migration-artifacts.mjs +135 -0
  133. package/lib/portable-migration-evidence.mjs +305 -0
  134. package/lib/portable-migration-store.mjs +199 -0
  135. package/lib/portable-migration.mjs +104 -0
  136. package/lib/portable-onboarding-acceptance.mjs +66 -0
  137. package/lib/portable-onboarding-request.mjs +49 -0
  138. package/lib/portable-onboarding.mjs +249 -0
  139. package/lib/portable-package-preparation.mjs +188 -0
  140. package/lib/portable-policy.mjs +44 -0
  141. package/lib/portable-shape.mjs +35 -0
  142. package/lib/portable-soul.mjs +38 -0
  143. package/lib/portable-state.mjs +80 -0
  144. package/lib/portable-values.mjs +181 -0
  145. package/lib/prepare-composition.mjs +151 -0
  146. package/lib/prepared-bindings.mjs +78 -0
  147. package/lib/prepared-resources.mjs +127 -0
  148. package/lib/provider-binding-broker.mjs +59 -0
  149. package/lib/provider-binding-wire.mjs +110 -0
  150. package/lib/provider-binding.mjs +22 -0
  151. package/lib/repository-observation.mjs +226 -0
  152. package/lib/resolution-shape.mjs +393 -0
  153. package/lib/schedule-capsule.mjs +206 -0
  154. package/lib/schedule.mjs +259 -38
  155. package/lib/servers.mjs +15 -0
  156. package/lib/soul-constraints.mjs +40 -0
  157. package/lib/source-projection.mjs +84 -0
  158. package/lib/source-spec.mjs +189 -0
  159. package/lib/workspace-definition.mjs +126 -0
  160. package/lib/workspace-discovery.mjs +146 -0
  161. package/package-catalog.json +2 -1
  162. package/package.json +3 -2
  163. package/packages/record/lib/capture-cc.mjs +14 -6
  164. package/packages/record/lib/formats.mjs +14 -3
  165. package/packages/record/lib/native-history.mjs +277 -7
  166. package/packages/record/lib/session-snapshot.mjs +25 -5
  167. package/packages/record/lib/sessions-for-home.mjs +30 -13
  168. package/skills/oats/SKILL.md +12 -7
  169. package/skills/oats-config/SKILL.md +11 -9
  170. package/skills/oats-packages/SKILL.md +12 -8
  171. package/skills/oats-portable/SKILL.md +115 -0
  172. package/skills/oats-portable-artifacts/SKILL.md +63 -0
@@ -0,0 +1,66 @@
1
+ /** Fresh setup acceptance boundary. Read-only comparison and explicit mutation
2
+ * handoff only; all source discovery and request construction remain owned by
3
+ * portable-onboarding. */
4
+ import { canonicalJson, freezeJson } from "./portable-values.mjs";
5
+ import { sameIdentity } from "./portable-identity.mjs";
6
+ import { buildFreshPreparationRequest, recheckFreshOnboarding } from "./portable-onboarding.mjs";
7
+ import { oatsError } from "./errors.mjs";
8
+
9
+ export const PORTABLE_ONBOARDING_ACCEPTANCE_VERSION = 1;
10
+
11
+ /** Prove two issued/ready inspections name one unchanged source while retaining
12
+ * distinct organization and standalone work/context facts. */
13
+ export function compareFreshSourceAcceptance({ organization, standalone }) {
14
+ const organizationRequest = buildFreshPreparationRequest(organization), standaloneRequest = buildFreshPreparationRequest(standalone);
15
+ if (!organization.workspace || organization.context.kind !== "workspace") throw oatsError("invalid-declaration", "organization acceptance requires an explicit workspace context");
16
+ if (standalone.workspace !== null || standalone.context.kind !== "standalone") throw oatsError("invalid-declaration", "standalone acceptance requires an explicit standalone context");
17
+ if (standalone.workTarget.git.present) throw oatsError("invalid-declaration", "standalone acceptance work target must be explicitly non-Git");
18
+ if (!sameIdentity(organization.source.identity, standalone.source.identity)) throw oatsError("source-identity-change", "acceptance inspections name different source identities");
19
+ if (organization.source.revision.commit !== standalone.source.revision.commit
20
+ || organization.source.exportPath !== standalone.source.exportPath
21
+ || organization.source.definition !== standalone.source.definition) {
22
+ throw oatsError("integrity-drift", "acceptance inspections do not name the same unchanged source export");
23
+ }
24
+ return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_ACCEPTANCE_VERSION, status: "ready",
25
+ source: { identity: organization.source.identity, commit: organization.source.revision.commit,
26
+ exportPath: organization.source.exportPath, definition: organization.source.definition },
27
+ organization: { context: organization.context, workTarget: organization.workTarget,
28
+ preparation: organizationRequest.preparation },
29
+ standalone: { context: standalone.context, workTarget: standalone.workTarget,
30
+ preparation: standaloneRequest.preparation },
31
+ effects: { writes: false, preparation: false, approval: false, enrollment: false } });
32
+ }
33
+
34
+ function approvalRequests(result) {
35
+ const requests = [];
36
+ for (const selection of Array.isArray(result?.selections) ? result.selections : []) {
37
+ for (const capability of Array.isArray(selection.approvalRequired) ? selection.approvalRequired : []) {
38
+ requests.push({ capability, artifactSet: selection.artifactSet, request: selection.request });
39
+ }
40
+ }
41
+ return requests;
42
+ }
43
+
44
+ /** Recheck fresh deployment state immediately before an explicitly supplied
45
+ * public preparation function. Missing integration/provisioning remains pending;
46
+ * no stale inspection is permission to overwrite managed state. */
47
+ export function prepareFreshOnboarding(inspection, options = {}, { prepareCapturedComposition } = {}) {
48
+ canonicalJson(options);
49
+ const built = buildFreshPreparationRequest(inspection, options);
50
+ const preflight = recheckFreshOnboarding(inspection);
51
+ const requestedBindings = built.preparation.operator?.bindings ?? null;
52
+ if (preflight.deployment.state === "absent") return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_ACCEPTANCE_VERSION,
53
+ status: "pending", code: "fresh-deployment-provisioning-required", mutationAttempted: false,
54
+ preparation: built.preparation, workTarget: built.workTarget, requestedBindings,
55
+ resolution: null, approvalRequests: [], problems: [{ code: "fresh-deployment-provisioning-required", message: "provision the explicit fresh deployment path before preparation" }] });
56
+ if (typeof prepareCapturedComposition !== "function") return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_ACCEPTANCE_VERSION,
57
+ status: "pending", code: "onboarding-integration-required", mutationAttempted: false,
58
+ preparation: built.preparation, workTarget: built.workTarget, requestedBindings,
59
+ resolution: null, approvalRequests: [], problems: [{ code: "onboarding-integration-required", message: "the public captured preparation adapter is not available" }] });
60
+ const result = prepareCapturedComposition(built.preparation);
61
+ if (!result || typeof result !== "object" || Array.isArray(result)) throw oatsError("invalid-resolution", "public preparation returned no structured result");
62
+ return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_ACCEPTANCE_VERSION,
63
+ status: typeof result.status === "string" ? result.status : "pending", mutationAttempted: true,
64
+ preparation: built.preparation, workTarget: built.workTarget, requestedBindings,
65
+ resolution: result.resolution ?? null, approvalRequests: approvalRequests(result), problems: Array.isArray(result.problems) ? result.problems : [] });
66
+ }
@@ -0,0 +1,49 @@
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
+ }
@@ -0,0 +1,249 @@
1
+ /** Read-only fresh setup and source discovery facade.
2
+ *
3
+ * Existing project content is allowed. Relevant OATS-managed state produces a
4
+ * separate-path hold; nothing is deleted, rewritten, installed, enrolled or
5
+ * prepared here. Repository reads are delegated to the existing transaction and
6
+ * workspace discovery/parser stack. */
7
+ import { lstatSync, opendirSync, realpathSync } from "node:fs";
8
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
+ import { createWorkspaceDiscovery } from "./workspace-discovery.mjs";
10
+ import { parseMemberExports } from "./workspace-definition.mjs";
11
+ import { canonicalJson, compareUtf8, freezeJson } from "./portable-values.mjs";
12
+ import { validateOrigin } from "./resolution-shape.mjs";
13
+ import { oatsError } from "./errors.mjs";
14
+
15
+ export const PORTABLE_ONBOARDING_VERSION = 1;
16
+ const issued = new WeakMap();
17
+ const preflightWitnesses = new WeakMap();
18
+ const MANAGED_PATHS = Object.freeze([
19
+ ["deployment-config", "oats-config.yaml"],
20
+ ["selection-lock", "oats-lock.json"],
21
+ ["schedules", "oats-schedules.json"],
22
+ ["portable-state", ".agents/portable"],
23
+ ["captured-resolutions", ".agents/resolutions"],
24
+ ["migration-evidence", ".agents/resolution-evidence"],
25
+ ["installed-capabilities", ".agents/capabilities/installed"],
26
+ ["retained-capabilities", ".agents/capabilities/artifacts"],
27
+ ["retained-souls", ".agents/soul-artifacts"],
28
+ ["retained-resources", ".agents/resource-artifacts"],
29
+ ["schedule-state", ".agents/schedules"],
30
+ ["installed-packages", ".agents/packages/installed"],
31
+ ["native-history", ".oats-native-record"],
32
+ ]);
33
+
34
+ const present = (path) => { try { return lstatSync(path); } catch (error) { if (error.code === "ENOENT") return null; throw error; } };
35
+ function exact(value, allowed, required, where) {
36
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw oatsError("invalid-declaration", `${where} must be an object`);
37
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) throw oatsError("invalid-declaration", `${where} has unsupported field ${key}`);
38
+ for (const key of required) if (!Object.hasOwn(value, key)) throw oatsError("invalid-declaration", `${where} is missing ${key}`);
39
+ return value;
40
+ }
41
+ function canonicalExistingDirectory(path, label) {
42
+ if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path) throw oatsError("invalid-declaration", `${label} must be a normalized absolute path`);
43
+ let actual;
44
+ try { actual = realpathSync(path); } catch (error) { if (error.code === "ENOENT") throw oatsError("source-unavailable", `${label} is absent`); throw error; }
45
+ if (actual !== path || !lstatSync(actual).isDirectory()) throw oatsError("invalid-declaration", `${label} must be a real non-symlinked directory`);
46
+ return actual;
47
+ }
48
+ function directoryIdentity(path) {
49
+ const stat = lstatSync(path);
50
+ if (!stat.isDirectory()) throw oatsError("selection-changed", "fresh setup directory is no longer a real directory");
51
+ return { dev: stat.dev, ino: stat.ino };
52
+ }
53
+ function freshDeploymentPath(path) {
54
+ if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path) throw oatsError("invalid-declaration", "fresh deployment must be a normalized absolute path");
55
+ const stat = present(path);
56
+ if (stat) {
57
+ if (!stat.isDirectory() || realpathSync(path) !== path) throw oatsError("invalid-declaration", "fresh deployment path must be a real non-symlinked directory");
58
+ return { path, state: "existing", identity: { dev: stat.dev, ino: stat.ino } };
59
+ }
60
+ let parent;
61
+ try { parent = realpathSync(dirname(path)); }
62
+ catch (error) { if (error.code === "ENOENT") throw oatsError("source-unavailable", "fresh deployment parent is absent"); throw error; }
63
+ if (parent !== dirname(path) || !lstatSync(parent).isDirectory()) throw oatsError("invalid-declaration", "fresh deployment parent must be a real non-symlinked directory");
64
+ return { path, state: "absent", parent: directoryIdentity(parent) };
65
+ }
66
+ function relativePath(root, path) { return relative(root, path).split(sep).join("/"); }
67
+ function scanInstanceState(root, base, budget) {
68
+ const found = [], path = join(root, base), stat = present(path);
69
+ if (!stat) return found;
70
+ if (!stat.isDirectory() || realpathSync(path) !== path) return [{ kind: "managed-agents-root", path: base }];
71
+ const directory = opendirSync(path);
72
+ try {
73
+ for (;;) {
74
+ const entry = directory.readSync();
75
+ if (entry === null) break;
76
+ if (++budget.visited > budget.maxEntries) throw oatsError("resource-limit", "fresh deployment instance scan entry limit exceeded");
77
+ if (!entry.isDirectory()) continue;
78
+ for (const [kind, child] of [["instance-homes", "instances"], ["retirement-state", ".oats-retirement"]]) {
79
+ const target = join(path, entry.name, child);
80
+ if (present(target)) found.push({ kind, path: relativePath(root, target) });
81
+ }
82
+ }
83
+ } finally { directory.closeSync(); }
84
+ return found;
85
+ }
86
+
87
+ /** Detect only known deployment-owned state. Ordinary files, Git metadata,
88
+ * authored souls/capabilities and source repositories do not make a path dirty. */
89
+ export function preflightFreshDeployment({ deployment, maxEntries = 4096 }) {
90
+ canonicalJson({ deployment, maxEntries });
91
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 1 || maxEntries > 100_000) throw oatsError("resource-limit", "invalid fresh deployment scan budget");
92
+ const selected = freshDeploymentPath(deployment), managedState = [];
93
+ if (selected.state === "existing") {
94
+ for (const [kind, relative] of MANAGED_PATHS) if (present(join(selected.path, relative))) managedState.push({ kind, path: relative });
95
+ const budget = { visited: 0, maxEntries };
96
+ managedState.push(...scanInstanceState(selected.path, "agents", budget), ...scanInstanceState(selected.path, "local-agents", budget));
97
+ }
98
+ managedState.sort((a, b) => compareUtf8(`${a.path}:${a.kind}`, `${b.path}:${b.kind}`));
99
+ if (canonicalJson(freshDeploymentPath(deployment)) !== canonicalJson(selected)) {
100
+ throw oatsError("selection-changed", "fresh deployment changed during preflight; inspect it again");
101
+ }
102
+ const status = managedState.length ? "separate-deployment-required" : "ready";
103
+ const result = freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, status,
104
+ deployment: { path: selected.path, state: selected.state }, managedState,
105
+ advice: managedState.length ? ["Preserve this deployment and choose a separate fresh deployment path; no existing state was changed."] : [],
106
+ effects: { writes: false, deletes: false, installs: false, enrollment: false } });
107
+ preflightWitnesses.set(result, selected);
108
+ return result;
109
+ }
110
+
111
+ function workTarget(path) {
112
+ const canonical = canonicalExistingDirectory(path, "work target"), marker = present(join(canonical, ".git"));
113
+ return { path: canonical, state: "existing", git: marker ? { present: true, kind: marker.isDirectory() ? "directory" : marker.isFile() ? "file" : "other" } : { present: false, kind: null } };
114
+ }
115
+ function sourceOrigin(workspace, alias) {
116
+ const index = workspace?.parsed.imports.findIndex((entry) => entry.alias === alias) ?? -1;
117
+ if (index < 0) throw oatsError("export-not-found", "requested alias is not advertised by the explicit workspace");
118
+ return { reference: workspace.parsed.imports[index], origin: { ...workspace.parsed.origins[`/imports/${index}`], kind: "import-adoption" } };
119
+ }
120
+ function explicitContext(input) {
121
+ const hasWorkspace = Object.hasOwn(input, "workspace");
122
+ const hasStandalone = Object.hasOwn(input, "standaloneContextKey");
123
+ if (hasWorkspace && hasStandalone) throw oatsError("invalid-declaration", "workspace and standaloneContextKey are mutually exclusive contexts");
124
+ if (hasWorkspace) return null;
125
+ if (!hasStandalone) throw oatsError("needs-configuration", "standalone onboarding requires an explicit standaloneContextKey (null explicitly means no private messaging context)");
126
+ const key = input.standaloneContextKey;
127
+ if (key !== null && (typeof key !== "string" || !key || Buffer.byteLength(key, "utf8") > 256 || key.trim() !== key || /[\x00-\x1f\x7f]/.test(key))) {
128
+ throw oatsError("invalid-declaration", "standaloneContextKey must be explicit null or non-empty text up to 256 UTF-8 bytes");
129
+ }
130
+ return { kind: "standalone", key };
131
+ }
132
+ function catalogSelection(value, count) {
133
+ if (value === undefined) return [];
134
+ if (!Array.isArray(value) || value.length > count || value.some((index) => !Number.isSafeInteger(index) || index < 0 || index >= count)
135
+ || new Set(value).size !== value.length) throw oatsError("invalid-declaration", "catalogIndexes must be unique declared workspace catalog indexes");
136
+ return [...value].sort((a, b) => a - b);
137
+ }
138
+
139
+ /** Inspect one explicit source and optional explicit workspace/member/catalogs.
140
+ * The caller owns the repository transaction lifetime. */
141
+ export function inspectPortableOnboarding(input, { repositories } = {}) {
142
+ canonicalJson(input);
143
+ exact(input, ["deployment", "workTarget", "source", "origin", "workspace", "member", "catalogIndexes", "standaloneContextKey"],
144
+ ["deployment", "workTarget", "source", "origin"], "portable onboarding inspection");
145
+ if (!repositories || typeof repositories !== "object") throw oatsError("invalid-source", "portable onboarding needs an explicit repository transaction");
146
+ validateOrigin(input.origin);
147
+ const standalone = explicitContext(input);
148
+ const deployment = preflightFreshDeployment({ deployment: input.deployment });
149
+ const target = workTarget(input.workTarget), discovery = createWorkspaceDiscovery(repositories);
150
+ const witness = { deployment: preflightWitnesses.get(deployment), work: directoryIdentity(target.path) };
151
+ const workspace = Object.hasOwn(input, "workspace") ? discovery.readWorkspace(input.workspace) : null;
152
+ const selected = typeof input.source === "string" ? sourceOrigin(workspace, input.source) : { reference: input.source, origin: input.origin };
153
+ const imported = discovery.importSoul(selected.reference, { origin: selected.origin });
154
+ const indexDocument = repositories.readFile(imported.observation, "oats.yaml");
155
+ const exports = parseMemberExports(indexDocument.bytes, { origin: indexDocument.origin });
156
+ const membership = input.member ? (() => {
157
+ if (!workspace) throw oatsError("needs-configuration", "member inspection needs an explicit workspace");
158
+ return discovery.checkMember(workspace, input.member);
159
+ })() : { schemaVersion: 1, status: "not-requested", evidence: [], problems: [] };
160
+ const catalogs = [];
161
+ const selectedCatalogs = catalogSelection(input.catalogIndexes, workspace?.parsed.catalogs.length ?? 0);
162
+ if (selectedCatalogs.length && !workspace) throw oatsError("needs-configuration", "catalog inspection needs an explicit workspace");
163
+ for (const index of selectedCatalogs) {
164
+ const declared = workspace.parsed.catalogs[index], base = workspace.parsed.origins[`/catalogs/${index}`];
165
+ if (!declared.path) throw oatsError("needs-configuration", "catalog inspection needs an explicit descriptor path");
166
+ const origin = { ...base, kind: "workspace-default" }; validateOrigin(origin);
167
+ const observation = repositories.observe(declared.source, { ...(declared.revision ? { revision: declared.revision } : {}), origin });
168
+ const document = repositories.readFile(observation, declared.path);
169
+ catalogs.push({ index, declaration: declared, source: observation.source, document: document.origin });
170
+ }
171
+ const declaredTeams = workspace?.parsed.declaration.teams ?? {};
172
+ const status = deployment.status !== "ready" ? deployment.status
173
+ : input.member && membership.status !== "eligible" ? "needs-configuration" : "ready-for-preparation";
174
+ const result = freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, status, deployment, workTarget: target,
175
+ source: { location: imported.reference.source, reference: imported.reference, identity: imported.identity,
176
+ revision: imported.observation.source, alias: imported.reference.alias, exportPath: imported.reference.soul,
177
+ definition: imported.definition, roots: imported.roots, exports: exports.exports, provenance: imported.provenance },
178
+ workspace: workspace ? { request: structuredClone(input.workspace), identity: workspace.identity, source: workspace.source,
179
+ name: workspace.parsed.declaration.name, members: workspace.parsed.members,
180
+ imports: workspace.parsed.imports, catalogs: workspace.parsed.catalogs } : null,
181
+ context: workspace ? { kind: "workspace", identity: workspace.identity } : standalone,
182
+ repositoryMembership: { request: input.member ? structuredClone(input.member) : null, result: membership },
183
+ teams: { declared: declaredTeams, enrollment: "not-performed", privateTeamQualification: "not-evaluated" },
184
+ catalogs, problems: membership.problems ?? [],
185
+ effects: { deploymentWrites: false, repositoryReads: true, repositoryScratch: "caller-owned",
186
+ installs: false, activation: false, credentials: false, teams: false, jobs: false } });
187
+ issued.set(result, witness);
188
+ if (status === "ready-for-preparation") recheckFreshOnboarding(result);
189
+ return result;
190
+ }
191
+
192
+ /** Metadata-only public view, NOT a serialized fresh-inspection witness or a
193
+ * preparation request. Provider payloads/adoption values have not gone through
194
+ * their owner's nonsecret classification and must not leak through inspection. */
195
+ export function describePortableOnboarding(inspection) {
196
+ if (!issued.has(inspection)) throw oatsError("invalid-declaration", "inspection summary requires an issued onboarding view");
197
+ const reference = value => ({ source: value.source, soul: value.soul, revision: value.revision, alias: value.alias,
198
+ adoptionPresent: Object.hasOwn(value, "adoption") });
199
+ const { reference: selected, exports, ...source } = inspection.source;
200
+ return freezeJson({ ...inspection,
201
+ source: { ...source, adoptionPresent: Object.hasOwn(selected, "adoption"), exports: {
202
+ souls: (exports.souls ?? []).map(({ path, definition, description }) => ({ path, definition, ...(description === undefined ? {} : { description }) })),
203
+ packages: (exports.packages ?? []).map(({ path, description }) => ({ path, ...(description === undefined ? {} : { description }) })),
204
+ knowledge: (exports.knowledge ?? []).map(({ contract, version }) => ({ contract, version, payloadOmitted: true })),
205
+ } },
206
+ workspace: inspection.workspace ? { ...inspection.workspace, imports: inspection.workspace.imports.map(reference) } : null,
207
+ omitted: { providerPayloads: true, adoptionValues: true },
208
+ });
209
+ }
210
+
211
+ /** Ephemeral local root custody, not a new persistent authority/schema. Ordinary
212
+ * project content changes are allowed; replacing the inspected directories or
213
+ * provisioning a previously absent deployment requires a fresh inspection. */
214
+ export function recheckFreshOnboarding(inspection) {
215
+ const witness = issued.get(inspection);
216
+ if (!witness) throw oatsError("invalid-declaration", "fresh target recheck requires an issued inspection");
217
+ const current = preflightFreshDeployment({ deployment: inspection.deployment.deployment.path });
218
+ if (current.status !== "ready") throw oatsError("fresh-deployment-required", "fresh deployment acquired managed state after inspection; preserve it and choose another path");
219
+ if (canonicalJson(preflightWitnesses.get(current)) !== canonicalJson(witness.deployment)) {
220
+ throw oatsError("selection-changed", "inspected deployment or its absent-path parent changed; inspect it again");
221
+ }
222
+ let work;
223
+ try { work = directoryIdentity(canonicalExistingDirectory(inspection.workTarget.path, "work target")); }
224
+ catch { throw oatsError("selection-changed", "inspected work target is no longer available at its owned path"); }
225
+ if (canonicalJson(work) !== canonicalJson(witness.work)) throw oatsError("selection-changed", "inspected work target directory changed; inspect it again");
226
+ return current;
227
+ }
228
+
229
+ /** Build data for the existing preparation API. This function never invokes it.
230
+ * Work target remains adjacent and distinct from deployment/source inputs. */
231
+ export function buildFreshPreparationRequest(inspection, options = {}) {
232
+ canonicalJson(options);
233
+ exact(options, ["operator", "mode", "allowLocalPaths"], [], "fresh preparation options");
234
+ if (!issued.has(inspection)) throw oatsError("invalid-declaration", "fresh preparation requires an inspection issued by this module");
235
+ if (inspection.status !== "ready-for-preparation") throw oatsError("fresh-deployment-required", "fresh preparation is held by deployment or membership preflight");
236
+ const { operator, mode, allowLocalPaths = false } = options;
237
+ if (typeof allowLocalPaths !== "boolean") throw oatsError("invalid-declaration", "allowLocalPaths must be boolean");
238
+ if (mode !== undefined && (typeof mode !== "string" || !mode.trim())) throw oatsError("invalid-declaration", "preparation mode must be non-empty text");
239
+ const input = { deployment: inspection.deployment.deployment.path,
240
+ source: inspection.source.reference,
241
+ origin: inspection.source.revision.provenance[0], allowLocalPaths,
242
+ ...(inspection.workspace ? { workspace: inspection.workspace.request } : { standaloneContextKey: inspection.context.key }),
243
+ ...(inspection.repositoryMembership.request ? { member: inspection.repositoryMembership.request } : {}),
244
+ ...(operator === undefined ? {} : { operator }), ...(mode === undefined ? {} : { mode }) };
245
+ canonicalJson(input);
246
+ return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, operation: "prepare", persisted: false,
247
+ preparation: input, workTarget: inspection.workTarget,
248
+ effects: { writes: false, installs: false, activation: false, enrollment: false } });
249
+ }
@@ -0,0 +1,188 @@
1
+ /** Package-side portable preparation over frozen observations. No current lock,
2
+ * config cascade, approval or activation lookup; caller owns final composition/CAS. */
3
+ import { chmodSync, lstatSync, mkdtempSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
4
+ import { isAbsolute, join, posix, relative } from "node:path";
5
+ import { canonicalJson, compareUtf8, decodeUtf8, parseStrictJson } from "./portable-values.mjs";
6
+ import { PACKAGE_FORMAT, bytesIntegrity, treeIntegrity } from "./portable-digest.mjs";
7
+ import { readPortableBytes } from "./portable-files.mjs";
8
+ import { removeOwnedStaging } from "./artifact-tree.mjs";
9
+ import { createSourceProjection, validateProjectionPath } from "./source-projection.mjs";
10
+ import { resolvePackageClosure } from "./package-closure.mjs";
11
+ import { parseLockedSource3, parsePackageDependency3, parseRepositorySource, portablePath, validateRepoAnchor, validateSelectionSource } from "./source-spec.mjs";
12
+ import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
13
+ import { objectAt } from "./portable-shape.mjs";
14
+ import { validateArtifactSet, validateOrigin } from "./resolution-shape.mjs";
15
+ import { retainPortableArtifact } from "./portable-artifacts.mjs";
16
+ import { oatsError } from "./errors.mjs";
17
+
18
+ const outside = (root, target) => { const path = relative(root, target); return path === ".." || path.startsWith("../") || path.startsWith("..\\") || isAbsolute(path); };
19
+ function copyLocalPayload(source, destination) {
20
+ const root = realpathSync(source), parent = realpathSync(join(destination, "..")), rootStat = lstatSync(root);
21
+ if (!rootStat.isDirectory() || !outside(root, parent)) throw oatsError("invalid-source", "package input must be a directory outside its staging tree");
22
+ const entries = []; let count = 0, bytes = 0;
23
+ const inspect = (directory, parts) => {
24
+ if (parts.length > 64) throw oatsError("resource-limit", "local package source depth exceeded");
25
+ for (const raw of readdirSync(directory, { encoding: "buffer" })) {
26
+ if (++count > 10_000) throw oatsError("resource-limit", "local package source entry budget exceeded");
27
+ const name = decodeUtf8(raw), lowered = name.toLowerCase();
28
+ if (lowered === ".git") continue; // Never read/copy Git administrative state, at any depth.
29
+ if ([".aw", ".agents", ".oats-state"].includes(lowered) || (lowered === "instances" && parts.at(-2)?.toLowerCase() === "agents")) {
30
+ throw oatsError("source-incomplete", "local package subtree contains private deployment/auth/instance state");
31
+ }
32
+ const path = [...parts, name].join("/"), file = join(directory, name), stat = lstatSync(file);
33
+ validateProjectionPath(path);
34
+ if (!stat.isDirectory() && !stat.isFile() && !stat.isSymbolicLink()) throw oatsError("invalid-source", "local package source contains a special file");
35
+ if (stat.isFile() && (bytes += stat.size) > 256 * 1024 * 1024) throw oatsError("resource-limit", "local package source byte budget exceeded");
36
+ entries.push({ path, file, stat });
37
+ if (stat.isDirectory()) inspect(file, [...parts, name]);
38
+ }
39
+ };
40
+ inspect(root, []); // Complete no-follow privacy/size preflight before any copy.
41
+ const projection = createSourceProjection(destination);
42
+ const unchanged = (entry) => {
43
+ const actual = lstatSync(entry.file), before = entry.stat;
44
+ if (actual.dev !== before.dev || actual.ino !== before.ino || actual.mode !== before.mode || actual.size !== before.size
45
+ || actual.mtimeMs !== before.mtimeMs || actual.ctimeMs !== before.ctimeMs) throw oatsError("integrity-drift", "local package source changed during snapshot");
46
+ };
47
+ for (const entry of entries) {
48
+ unchanged(entry);
49
+ if (entry.stat.isDirectory()) projection.directory(entry.path, entry.stat.mode & 0o7777);
50
+ else if (entry.stat.isSymbolicLink()) projection.link(entry.path, readlinkSync(entry.file));
51
+ else projection.copyFile(entry.path, entry.file, entry.stat.mode & 0o7777);
52
+ unchanged(entry);
53
+ }
54
+ projection.finish(); chmodSync(destination, rootStat.mode & 0o7777);
55
+ }
56
+
57
+ export function preparePackageArtifacts({ requests, deployment, directory, repositories, kernel, observations = [], catalog, allowLocalPaths = false }) {
58
+ canonicalJson(requests);
59
+ if (!Array.isArray(requests) || !requests.length || typeof directory !== "string" || !isAbsolute(directory)
60
+ || typeof deployment !== "string" || !isAbsolute(deployment) || typeof allowLocalPaths !== "boolean") throw oatsError("invalid-source", "package preparation needs explicit requests, scope and scratch directory");
61
+ for (const name of ["loadPackageManifestAt", "capabilityCompatibility", "assertPlatformInvariantLocks", "materializeCapability"]) {
62
+ if (typeof kernel?.[name] !== "function") throw new TypeError(`package preparation requires kernel.${name}`);
63
+ }
64
+ for (const request of requests) {
65
+ objectAt(request, ["source", "origin", "capability"], ["source", "origin"]); validateOrigin(request.origin);
66
+ if (request.capability !== undefined && !isMaterializedCapabilityId(request.capability)) throw oatsError("invalid-source", "invalid requested capability identity");
67
+ }
68
+ const staging = mkdtempSync(join(realpathSync(directory), ".package-preparation-")), owned = lstatSync(staging);
69
+ const cached = new Map(), sourceObservations = [...observations]; let counter = 0, closed = false;
70
+ const cleanup = () => {
71
+ if (closed) return;
72
+ const current = lstatSync(staging);
73
+ if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError("source-incomplete", "package staging cleanup lost ownership");
74
+ removeOwnedStaging(staging); closed = true;
75
+ };
76
+ const observe = (source, selector, origin) => {
77
+ const existing = sourceObservations.find((value) => value.source.remote === source && (value.source.commit === selector || value.source.selector === selector));
78
+ if (existing) return existing;
79
+ const observation = repositories.observe(source, { ...(selector === undefined ? {} : { revision: selector }), origin });
80
+ sourceObservations.push(observation);
81
+ return observation;
82
+ };
83
+ const readPackage = (request) => {
84
+ validateOrigin(request.origin);
85
+ let selected = request.source;
86
+ if (selected?.kind === "catalog") {
87
+ const normalized = parsePackageDependency3(selected.source);
88
+ if (canonicalJson(normalized) !== canonicalJson(selected)) throw oatsError("invalid-source", "catalog request fields disagree");
89
+ } else validateSelectionSource(selected);
90
+ const key = canonicalJson(selected);
91
+ if (cached.has(key)) return cached.get(key);
92
+ let originalSource, packagePath, commit, payload, sourceDocument, repoAnchor = null, localBase;
93
+ const destination = join(staging, `package-${counter++}`);
94
+ let repository, selector;
95
+ if (selected.kind === "repo") {
96
+ repoAnchor = selected.anchor;
97
+ if (repoAnchor.source.startsWith("path:")) {
98
+ const root = parseLockedSource3(repoAnchor.source, ".").localPath;
99
+ const localPath = join(root, selected.path);
100
+ selected = { kind: "path", source: `path:${localPath}`, path: ".", localPath, portable: false };
101
+ } else {
102
+ repository = repoAnchor.source; selector = repoAnchor.revision; packagePath = selected.path;
103
+ originalSource = `${repository}@${selector}`;
104
+ }
105
+ }
106
+ if (selected.kind === "catalog") {
107
+ if (typeof catalog !== "function") throw oatsError("needs-configuration", "package dependency requires an explicit catalog resolver");
108
+ const entry = catalog(selected.id, selected.selector);
109
+ if (!entry || typeof entry.url !== "string") throw oatsError("invalid-source", "catalog did not resolve a repository");
110
+ repository = parseRepositorySource(`git:${entry.url}`).normalized;
111
+ selector = entry.ref; packagePath = entry.path ?? "oats-package"; originalSource = selected.source;
112
+ } else if (selected.kind === "git") {
113
+ repository = parseRepositorySource(`git:${selected.url}`).normalized;
114
+ selector = selected.selector; packagePath = selected.path; originalSource = selected.source;
115
+ }
116
+ if (selected.kind === "path") {
117
+ if (!allowLocalPaths) throw oatsError("needs-configuration", "local package input requires explicit authorization");
118
+ localBase = selected.localPath; originalSource = selected.source; packagePath = "."; commit = "local";
119
+ copyLocalPayload(localBase, destination); payload = destination;
120
+ sourceDocument = { kind: "source", source: originalSource, revision: "local", path: "oats-package.json" };
121
+ } else {
122
+ portablePath(packagePath, { allowRoot: true });
123
+ const observation = observe(repository, selector, request.origin);
124
+ repositories.materialize(observation, [packagePath], destination);
125
+ payload = join(destination, packagePath); commit = observation.source.commit;
126
+ repoAnchor = { source: observation.source.remote, revision: commit };
127
+ sourceDocument = { kind: "source", source: observation.source.remote, revision: commit, path: posix.join(packagePath, "oats-package.json") };
128
+ }
129
+ // Strict JSON ingress precedes the sole existing full package/manifest codec.
130
+ const bytes = readPortableBytes(join(payload, "oats-package.json")); parseStrictJson(bytes);
131
+ sourceDocument.integrity = bytesIntegrity(bytes);
132
+ const manifest = kernel.loadPackageManifestAt(payload, { strict: true });
133
+ const dependencyRequests = (manifest.dependencies ?? []).map((spec, index) => {
134
+ // Local siblings are authoring input. A remote manifest never gains access
135
+ // to adopter files merely because another root request was local.
136
+ let source = parsePackageDependency3(spec, { localBase, allowLocalPaths: allowLocalPaths && localBase !== undefined });
137
+ if (source.kind === "repo") {
138
+ if (!repoAnchor) throw oatsError("needs-configuration", "repo dependency has no explicit repository snapshot anchor");
139
+ source = { ...source, anchor: validateRepoAnchor(repoAnchor) };
140
+ }
141
+ return { source, origin: { kind: "package-dependency", document: sourceDocument, pointer: `/dependencies/${index}` } };
142
+ });
143
+ const record = { package: manifest.package, version: manifest.version, source: originalSource, path: packagePath, commit,
144
+ sourceKey: canonicalJson({ source: originalSource, path: packagePath, commit }), dir: payload, manifest,
145
+ capabilities: manifest._capabilities, dependencyRequests, origins: [request.origin] };
146
+ cached.set(key, record); return record;
147
+ };
148
+ try {
149
+ const graph = resolvePackageClosure({ requests, readPackage,
150
+ validatePackage(record) {
151
+ const compatibility = kernel.capabilityCompatibility(record.manifest);
152
+ if (!compatibility.compatible) throw oatsError("incompatible-oats", `package ${record.package} requires OATS ${compatibility.range}`);
153
+ },
154
+ finalizePackage: (record, deps) => ({ ...record, deps, integrity: treeIntegrity(record.dir, { format: PACKAGE_FORMAT }) }),
155
+ discardPackage: () => {}, // All duplicate payloads belong to outer owned staging.
156
+ });
157
+ const owners = new Map();
158
+ for (const record of graph.packages.values()) for (const cap of record.capabilities) {
159
+ if (owners.has(cap.id) && owners.get(cap.id) !== record.package) throw oatsError("duplicate-capability-id", "selected package closures export conflicting capability identities");
160
+ owners.set(cap.id, record.package);
161
+ }
162
+ requests.forEach((request, index) => {
163
+ if (request.capability && !graph.packages.get(graph.roots[index]).capabilities.some((cap) => cap.id === request.capability)) throw oatsError("capability-list-mismatch", "selected source does not export the required capability");
164
+ });
165
+ kernel.assertPlatformInvariantLocks([...graph.packages.values()].flatMap((record) => record.capabilities.map((cap) => cap.dir)));
166
+ const artifactSet = { schemaVersion: 1, packages: Object.create(null), capabilities: Object.create(null) };
167
+ const artifacts = new Map();
168
+ for (const record of graph.packages.values()) {
169
+ artifactSet.packages[record.package] = { source: record.source, path: record.path, version: record.version, commit: record.commit,
170
+ integrity: record.integrity, dependencies: [...new Set(record.deps)].sort(compareUtf8) };
171
+ for (const cap of record.capabilities) {
172
+ const projected = kernel.materializeCapability({ cap, pkg: record, artifactsDir: join(staging, "artifacts") });
173
+ const artifact = { kind: "capability", capability: cap.id, integrity: treeIntegrity(projected.dir) };
174
+ artifactSet.capabilities[cap.id] = { version: projected.version, artifact, origin: { kind: "package", package: record.package, path: cap.rel, projectionVersion: 1 } };
175
+ artifacts.set(cap.id, projected.dir);
176
+ }
177
+ }
178
+ validateArtifactSet(artifactSet);
179
+ for (const [id, source] of artifacts) retainPortableArtifact(deployment, source, artifactSet.capabilities[id].artifact);
180
+ return { artifactSet, roots: graph.roots, cleanup };
181
+ } catch (error) {
182
+ try { cleanup(); } catch (failure) {
183
+ const combined = new AggregateError([error, failure], "package preparation and cleanup failed", { cause: error });
184
+ combined.code = error.code; combined.stagingPath = staging; throw combined;
185
+ }
186
+ throw error;
187
+ }
188
+ }
@@ -0,0 +1,44 @@
1
+ /** Shared authored selection shapes for the two policy authorities. Validation
2
+ * only: no precedence, provider selection, acquisition, enrollment or execution. */
3
+ import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
4
+ import { parsePortableSource } from "./source-spec.mjs";
5
+ import { invalidShape, objectAt, pointerKey, stringAt } from "./portable-shape.mjs";
6
+
7
+ export const FUNDAMENTAL_SLOTS = Object.freeze(["knowledge", "messaging", "tasks"]);
8
+ export function validatePolicyChoices(value, { pointer, required = false, origins = {}, origin = null, localBase, allowLocalPaths = false }) {
9
+ const rules = objectAt(value, ["capabilities", ...FUNDAMENTAL_SLOTS], [], pointer);
10
+ const sources = Object.create(null);
11
+ const capabilityAt = (id, at) => { if (!isMaterializedCapabilityId(id)) invalidShape(at, "invalid capability identity"); };
12
+ const selection = (item, at, capability) => {
13
+ objectAt(item, capability === undefined ? ["capability", "source", "settings"] : ["source", "settings"],
14
+ capability === undefined ? ["capability", "source"] : ["source"], at);
15
+ capabilityAt(capability ?? item.capability, capability === undefined ? `${at}/capability` : at);
16
+ stringAt(item.source, `${at}/source`);
17
+ try { sources[`${at}/source`] = parsePortableSource(item.source, { localBase, allowLocalPaths }); }
18
+ catch (error) { error.provenance = [origins[`${at}/source`] ?? { document: origin, pointer: `${at}/source` }]; throw error; }
19
+ if (Object.hasOwn(item, "settings")) objectAt(item.settings, null, [], `${at}/settings`);
20
+ };
21
+ if (Object.hasOwn(rules, "capabilities")) {
22
+ objectAt(rules.capabilities, null, [], `${pointer}/capabilities`);
23
+ for (const [id, item] of Object.entries(rules.capabilities)) {
24
+ const at = `${pointer}/capabilities/${pointerKey(id)}`;
25
+ capabilityAt(id, at);
26
+ if (!required && item === false) continue;
27
+ selection(item, at, id);
28
+ }
29
+ }
30
+ for (const slot of FUNDAMENTAL_SLOTS) {
31
+ if (!Object.hasOwn(rules, slot)) continue;
32
+ const item = rules[slot], at = `${pointer}/${slot}`;
33
+ if ((required && item === "any") || (!required && item === "none")) continue;
34
+ selection(item, at);
35
+ }
36
+ return sources;
37
+ }
38
+
39
+ export function validateProviderDeclaration(value, pointer) {
40
+ const envelope = objectAt(value, ["contract", "version", "payload"], ["contract", "version", "payload"], pointer);
41
+ stringAt(envelope.contract, `${pointer}/contract`, { pattern: /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/ });
42
+ if (!Number.isSafeInteger(envelope.version) || envelope.version < 1) invalidShape(`${pointer}/version`, "invalid provider contract version");
43
+ return envelope;
44
+ }
@@ -0,0 +1,35 @@
1
+ /** Small shape checks shared by portable document/record codecs. Call only on
2
+ * decoded or canonical-data-validated values; these helpers do not resolve policy. */
3
+ import { oatsError } from "./errors.mjs";
4
+ import { scalarString } from "./portable-values.mjs";
5
+
6
+ export const pointerKey = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
7
+ export function invalidShape(pointer, message, code = "invalid-declaration") {
8
+ throw oatsError(code, `${message} at ${pointer || "/"}`);
9
+ }
10
+ export function objectAt(value, allowed, required, pointer = "") {
11
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalidShape(pointer, "expected object");
12
+ if (allowed) for (const key of Object.keys(value)) {
13
+ if (!allowed.includes(key)) invalidShape(`${pointer}/${pointerKey(key)}`, "unknown field");
14
+ }
15
+ for (const key of required ?? []) if (!Object.hasOwn(value, key)) invalidShape(`${pointer}/${pointerKey(key)}`, "missing field");
16
+ return value;
17
+ }
18
+ export function stringAt(value, pointer, { pattern, empty = false } = {}) {
19
+ scalarString(value, pointer);
20
+ if ((!empty && !value) || (pattern && !pattern.test(value))) invalidShape(pointer, "invalid string");
21
+ return value;
22
+ }
23
+ export function versionAt(value, pointer = "/schemaVersion") {
24
+ if (value !== 1) invalidShape(pointer, "unsupported wire version", "unsupported-wire-version");
25
+ }
26
+ export function stringSetAt(value, pointer, validate = stringAt) {
27
+ if (!Array.isArray(value)) invalidShape(pointer, "expected array");
28
+ const seen = new Set();
29
+ value.forEach((item, index) => {
30
+ validate(item, `${pointer}/${index}`);
31
+ if (seen.has(item)) invalidShape(`${pointer}/${index}`, "duplicate set entry");
32
+ seen.add(item);
33
+ });
34
+ return value;
35
+ }