@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
package/lib/errors.mjs ADDED
@@ -0,0 +1,7 @@
1
+ /** Kernel error with a stable machine-readable code (contract §4) and optional provenance. */
2
+ export function oatsError(code, message, provenance) {
3
+ const e = new Error(message);
4
+ e.code = code;
5
+ if (provenance) e.provenance = provenance;
6
+ return e;
7
+ }
@@ -0,0 +1,98 @@
1
+ /** Exact capability-local helper instruction facts. This is a caller of the
2
+ * existing resolver, never a second authority/order or provider-policy engine. */
3
+ import { canonicalJson, parseStrictJson } from './portable-values.mjs';
4
+ import { bytesIntegrity } from './portable-digest.mjs';
5
+ import { pointerKey } from './portable-shape.mjs';
6
+ import { normalizePackagePath } from './capability-provenance.mjs';
7
+ import { resolveChoices } from './portable-choices.mjs';
8
+ import { validateCapabilityInputDeclarations } from './capability-inputs.mjs';
9
+ import { oatsError } from './errors.mjs';
10
+
11
+ const same = (a, b) => canonicalJson(a) === canonicalJson(b);
12
+ const prefix = '/helpers/injections/';
13
+ export const helperInjectionChoiceKey = id => `${prefix}${pointerKey(id)}`;
14
+
15
+ /** artifact/bytes are exact selected manifest inputs, not annotated objects. */
16
+ export function helperInjectionFact({ artifact, bytes }) {
17
+ const manifest = validateCapabilityInputDeclarations(parseStrictJson(bytes));
18
+ if (artifact.kind !== 'capability' || manifest.capability !== artifact.capability) throw oatsError('invalid-resolution', 'helper policy manifest owner differs from selected artifact');
19
+ if (!Object.hasOwn(manifest, 'helperInjection')) return { manifest, fact: null, resource: null };
20
+ const policy = manifest.helperInjection, source = `capability:${artifact.capability}`, key = helperInjectionChoiceKey(artifact.capability);
21
+ let path = null;
22
+ if (policy.mode === 'inherit') {
23
+ path = normalizePackagePath(manifest.inject);
24
+ if (typeof manifest.inject !== 'string' || path === undefined || path === '.') throw oatsError('needs-configuration', 'helper inherit needs its own declared injection file');
25
+ } else if (policy.mode === 'file') path = policy.path;
26
+ const resourceKey = `instruction:${source}`;
27
+ const origin = { kind: 'source-export', document: { kind: 'artifact', owner: artifact, path: 'oats.json', integrity: bytesIntegrity(bytes) }, pointer: '/helperInjection' };
28
+ return { manifest, source, resourceKey, resource: path === null ? null : { owner: artifact, path, kind: 'file' },
29
+ fact: { key, kind: 'equals', value: path === null ? null : resourceKey, origin } };
30
+ }
31
+
32
+ export function captureHelperInjectionChoices(plan, definitions) {
33
+ const policies = new Map(), requirements = [...plan.requirements];
34
+ for (const definition of definitions) {
35
+ const policy = helperInjectionFact(definition), id = definition.artifact.capability;
36
+ if (policies.has(id)) throw oatsError('invalid-resolution', 'duplicate helper policy owner');
37
+ policies.set(id, policy);
38
+ if (!policy.fact && policy.manifest.inject) throw oatsError('needs-configuration', 'new helper injection requires an explicit capability policy');
39
+ if (policy.fact) requirements.push(policy.fact);
40
+ }
41
+ const resolved = resolveChoices({ requirements, candidates: plan.candidates });
42
+ if (resolved.status !== 'resolved') throw oatsError(resolved.status === 'conflict' ? 'requirement-conflict' : 'needs-configuration', 'helper policy conflicts with captured instruction choices', resolved.problems);
43
+ return { choices: resolved.choices, policies };
44
+ }
45
+
46
+ /** Verify BOTH directions. Read-only verification permits literal legacy
47
+ * evidence; publication must explicitly disallow new legacy/missing-policy mint.
48
+ * Existing resource verification still proves physical containment/kind. */
49
+ export function verifyHelperInjectionPolicies(record, definitions, { publishing = false } = {}) {
50
+ const helper = record.subject.kind === 'helper', composition = record.dispatch.composition;
51
+ const expected = new Map(), selected = new Set();
52
+ for (const definition of definitions) {
53
+ const policy = helperInjectionFact(definition), id = definition.artifact.capability;
54
+ if (selected.has(id) || !record.artifacts.capabilities[id] || !same(record.artifacts.capabilities[id].artifact, definition.artifact)) throw oatsError('invalid-resolution', 'helper policy inputs differ from selected artifacts');
55
+ selected.add(id);
56
+ if (!helper) continue;
57
+ if (publishing && !policy.fact && policy.manifest.inject) throw oatsError('needs-configuration', 'new helper record lacks an explicit injection policy');
58
+ if (!policy.fact) continue;
59
+ const { fact, source, resource, resourceKey } = policy, choice = record.choices[fact.key];
60
+ expected.set(fact.key, fact);
61
+ if (!choice || !same(choice.value, fact.value) || !choice.constraints.some(entry => entry.kind === fact.kind && same(entry.value, fact.value) && same(entry.origin, fact.origin))) throw oatsError('resolution-incomplete', 'helper injection lacks its exact retained manifest witness');
62
+ const block = composition?.blocks.find(entry => entry.source === source), omission = composition?.omissions.find(entry => entry.source === source);
63
+ if (resource) {
64
+ if (omission || !block || block.choice !== fact.key || block.resource !== resourceKey || !record.resources[resourceKey] || !same(record.resources[resourceKey], resource)) throw oatsError('invalid-resolution', 'helper instruction block differs from declared owner/file');
65
+ } else if (block || !omission || omission.reason !== 'helper-policy' || omission.choice !== fact.key) throw oatsError('invalid-resolution', 'helper omission differs from explicit capability policy');
66
+ }
67
+ if (selected.size !== Object.keys(record.artifacts.capabilities).length) throw oatsError('resolution-incomplete', 'helper policy manifest inputs are incomplete');
68
+ const isPolicyOrigin = origin => origin?.kind === 'source-export' && (origin.pointer === '/helperInjection' || origin.pointer?.startsWith('/helperInjection/'));
69
+ for (const [key, choice] of Object.entries(record.choices)) {
70
+ if (key.startsWith(prefix) && !expected.has(key)) throw oatsError('invalid-resolution', 'helper policy choice lacks an applicable declaration');
71
+ for (const entry of [...choice.constraints, ...choice.considered]) if (isPolicyOrigin(entry.origin)) {
72
+ const fact = expected.get(key);
73
+ if (!fact || entry.kind !== 'equals' || !same(entry.value, fact.value) || !same(entry.origin, fact.origin)) throw oatsError('invalid-resolution', 'helper manifest witness cannot justify another choice or owner');
74
+ }
75
+ if (isPolicyOrigin(choice.selectedBy) && (!expected.has(key) || !same(choice.selectedBy, expected.get(key).origin))) throw oatsError('invalid-resolution', 'helper selected origin differs from its declaration');
76
+ }
77
+ const ownSource = fact => `capability:${fact.origin.document.owner.capability}`;
78
+ for (const block of composition?.blocks ?? []) if (block.choice?.startsWith(prefix)) {
79
+ const fact = expected.get(block.choice);
80
+ if (!fact || fact.value === null || block.source !== ownSource(fact) || block.resource !== fact.value) throw oatsError('invalid-resolution', 'helper instruction witness cannot control another contribution');
81
+ }
82
+ for (const omission of composition?.omissions ?? []) {
83
+ if (publishing && omission.reason === 'helper-knowledge') throw oatsError('needs-configuration', 'new publication cannot mint legacy slot-based helper omissions');
84
+ if (omission.reason === 'helper-policy') {
85
+ const fact = expected.get(omission.choice);
86
+ if (!helper || !fact || fact.value !== null || omission.source !== ownSource(fact)) throw oatsError('invalid-resolution', 'helper omission lacks its declared own policy');
87
+ }
88
+ }
89
+ // A missing/non-policy choice is not a way around capability ownership.
90
+ // Above we prove each declared contribution's exact file/omission; here every
91
+ // new capability contribution must point back to that owner's proved fact.
92
+ // Literal historical read/reuse and primary composition stay unchanged.
93
+ if (helper && publishing) for (const entry of [...(composition?.blocks ?? []), ...(composition?.omissions ?? [])]) {
94
+ if (!entry.source.startsWith('capability:')) continue;
95
+ const fact = expected.get(helperInjectionChoiceKey(entry.source.slice('capability:'.length)));
96
+ if (!fact || entry.choice !== fact.key) throw oatsError('invalid-resolution', 'new helper capability contribution lacks its declared own policy');
97
+ }
98
+ }
package/lib/herdr.mjs CHANGED
@@ -1,10 +1,18 @@
1
- /** Herdr 0.8.x host-local session backend. SSH belongs to the CLI router. */
1
+ /** Herdr 0.8.x/0.9.x host-local backend (explicit protocols 20/22).
2
+ * SSH belongs to the CLI router; protocol selection is never negotiation. */
2
3
  import { execFileSync, spawn } from "node:child_process";
3
4
  import { existsSync, mkdirSync } from "node:fs";
4
5
  import { homedir } from "node:os";
5
6
  import { dirname, join, resolve } from "node:path";
6
7
 
8
+ // Keep the existing legacy/default protocol literal; captured callers select one.
7
9
  export const HERDR_PROTOCOL = 20;
10
+ export const HERDR_SUPPORTED_PROTOCOLS = Object.freeze([20, 22]);
11
+ export const isSupportedHerdrProtocol = (value) => HERDR_SUPPORTED_PROTOCOLS.includes(value);
12
+ function selectedHerdrProtocol(target) {
13
+ if (!isSupportedHerdrProtocol(target?.protocol)) throw new Error("Herdr endpoint requires explicit supported protocol 20 or 22");
14
+ return target.protocol;
15
+ }
8
16
  const quote = (s) => `'${String(s).replaceAll("'", "'\\''")}'`;
9
17
  const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
10
18
 
@@ -27,10 +35,10 @@ export function herdrCommand(target, args, { timeout = 10000, exec = execFileSyn
27
35
  }
28
36
 
29
37
  export function herdrSnapshot(target, io) {
38
+ const protocol = selectedHerdrProtocol(target);
30
39
  const snapshot = herdrCommand(target, ["api", "snapshot"], io).snapshot;
31
- if (snapshot?.protocol !== HERDR_PROTOCOL || !Array.isArray(snapshot.panes)) {
32
- throw new Error(`unsupported Herdr protocol ${snapshot?.protocol}; OATS requires ${HERDR_PROTOCOL}`);
33
- }
40
+ if (snapshot?.protocol !== protocol) throw new Error(`Herdr snapshot does not match selected protocol ${protocol}`);
41
+ if (!Array.isArray(snapshot.panes)) throw new Error("Herdr snapshot has no pane inventory");
34
42
  return snapshot;
35
43
  }
36
44
 
@@ -57,15 +65,18 @@ export function ensureHerdr({ binary = "herdr", socket, session = "oats" } = {})
57
65
  }
58
66
 
59
67
  export function allocateHerdr(target, { home, instance }, io) {
68
+ const protocol = selectedHerdrProtocol(target);
60
69
  const result = herdrCommand(target, ["workspace", "create", "--cwd", home, "--label", instance, "--no-focus"], io);
61
70
  const pane = result.root_pane;
62
71
  if (!pane?.pane_id || !pane?.terminal_id || !pane?.workspace_id) throw new Error("Herdr allocation returned no session identity");
63
- return { ...target, workspaceId: pane.workspace_id, paneId: pane.pane_id, terminalId: pane.terminal_id };
72
+ return { ...target, protocol, workspaceId: pane.workspace_id, paneId: pane.pane_id, terminalId: pane.terminal_id };
64
73
  }
65
74
 
66
75
  export function inspectHerdr(target, io) {
67
76
  const snapshot = herdrSnapshot(target, io);
68
- const pane = snapshot.panes.find((p) => p.pane_id === target.paneId && p.terminal_id === target.terminalId);
77
+ const matches = snapshot.panes.filter((p) => p.pane_id === target.paneId && p.terminal_id === target.terminalId);
78
+ if (io?.strictHerdrTarget && (matches.length > 1 || (matches.length === 1 && matches[0].workspace_id !== target.workspaceId))) throw new Error("Herdr target workspace/pane/terminal identity is ambiguous or changed");
79
+ const pane = matches[0];
69
80
  const agent = pane && snapshot.agents?.find((a) => a.terminal_id === target.terminalId);
70
81
  return { present: !!pane, pane, agent, status: agent?.agent_status || "unknown" };
71
82
  }
@@ -89,7 +100,7 @@ export function inputHerdr(target, text, io) {
89
100
  }
90
101
 
91
102
  export function validHerdrTarget(value) {
92
- return value?.backend === "herdr" && value.protocol === HERDR_PROTOCOL
103
+ return value?.backend === "herdr" && isSupportedHerdrProtocol(value.protocol)
93
104
  && [value.socket, value.binary, value.workspaceId, value.paneId, value.terminalId].every((v) => typeof v === "string" && v.length > 0)
94
105
  && value.socket === resolve(value.socket);
95
106
  }
@@ -0,0 +1,31 @@
1
+ /** Formatting shared by current preparation and retained instruction projection.
2
+ * Selection/order is supplied by the caller; this module never discovers config. */
3
+ import { readPortableBytes } from "./portable-files.mjs";
4
+ import { oatsError } from "./errors.mjs";
5
+
6
+ export function renderInstructionText(body, blocks) {
7
+ let text = body.replace(/\n*$/, "\n");
8
+ for (const { source, file, content } of blocks) {
9
+ text += `\n<!-- oats:${source} src=${file} -->\n${content.trim()}\n<!-- /oats:${source} -->\n`;
10
+ }
11
+ return text;
12
+ }
13
+
14
+ /** Input is the result of exact record verification, not a current config or
15
+ * a mutable soul directory. All paths come from its verified resource inventory. */
16
+ export function composeCapturedInstructions(verified) {
17
+ const composition = verified.record.dispatch.composition;
18
+ if (!composition) throw oatsError("resolution-incomplete", "captured instruction composition is absent");
19
+ const read = (key) => {
20
+ const file = verified.resources.get(key);
21
+ if (!file) throw oatsError("resolution-incomplete", "captured instruction resource is absent");
22
+ let content;
23
+ try { content = new TextDecoder("utf-8", { fatal: true }).decode(readPortableBytes(file)); }
24
+ catch (cause) { throw oatsError("invalid-resolution", "captured instructions are not readable UTF-8", { cause }); }
25
+ return { file, content };
26
+ };
27
+ const body = read(composition.body);
28
+ const blocks = composition.blocks.map(({ source, resource }) => ({ source, ...read(resource) }));
29
+ const skills = composition.skills.map(({ name, resource }) => ({ name, path: verified.resources.get(resource) }));
30
+ return { text: renderInstructionText(body.content, blocks), blocks, skills, omissions: composition.omissions };
31
+ }
@@ -0,0 +1,106 @@
1
+ /** Sole bounded structural decoder for historical lockfileVersion 1 and the
2
+ * capability-materialization lockfileVersion 2.
3
+ *
4
+ * Bytes in, validated null-prototype maps out. No filesystem/config reads,
5
+ * migration, normalization, repair or writes. Retired-capability policy is an
6
+ * injected pure predicate so this leaf does not import the kernel. */
7
+ import { byteView, parseStrictJson } from "./portable-values.mjs";
8
+ import {
9
+ PACKAGE_ID_RE, TRANSITIONAL_ROW_FIELDS, capabilityIdViolation,
10
+ isMaterializedCapabilityId, validateCapabilityLockEntry, validateLockEntry,
11
+ } from "./capability-provenance.mjs";
12
+ import { oatsError } from "./errors.mjs";
13
+
14
+ const LOCK_PACKAGE_KEYS = new Set(["source", "path", "version", "commit", "integrity", "dependencies"]);
15
+ const LOCK_CAPABILITY_KEYS = new Set(["version", "package", "path", "integrity", "trusted"]);
16
+ const noneRetired = () => undefined;
17
+ const nullProtoMap = (raw) => {
18
+ const out = Object.create(null);
19
+ for (const key of Object.keys(raw || {})) out[key] = raw[key];
20
+ return out;
21
+ };
22
+ const kind = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
23
+
24
+ /** Validate one literal v1 capability row. Unknown historical fields remain
25
+ * preserved, matching the existing reader; required fields and known optional
26
+ * field types remain strict. */
27
+ export function legacyCapabilityEntryViolation(entry) {
28
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return "not an object";
29
+ for (const key of ["source", "version", "integrity"]) if (typeof entry[key] !== "string" || !entry[key]) return `missing/invalid ${key}`;
30
+ if (!/^sha256-[0-9a-f]{64}$/.test(entry.integrity)) return `malformed integrity "${entry.integrity}"`;
31
+ if (entry.commit !== undefined && typeof entry.commit !== "string") return "invalid commit";
32
+ if (entry.trustedExecutables !== undefined && typeof entry.trustedExecutables !== "boolean") return "invalid trustedExecutables";
33
+ return null;
34
+ }
35
+
36
+ /** Decode one already-read historical lock. `file` is diagnostic provenance,
37
+ * not a path this codec reads. Strict JSON adds bounded/duplicate-key/UTF-8
38
+ * refusal before the unchanged v1/v2 semantic pass. */
39
+ export function decodeLegacyLockBytes(input, { file = "<legacy-lock>", retiredCapabilityReason = noneRetired, limits } = {}) {
40
+ const bytes = byteView(input, "legacy lock bytes");
41
+ if (typeof file !== "string" || !file) throw oatsError("invalid-lock", "legacy lock diagnostic file must be non-empty text");
42
+ if (typeof retiredCapabilityReason !== "function") throw oatsError("invalid-lock", `${file}: retired-capability classifier must be a function`);
43
+ const bad = (message, extra = {}) => oatsError("invalid-lock", `${file}: ${message}`, [{ file, violation: message, ...extra }]);
44
+ let parsed;
45
+ try { parsed = parseStrictJson(bytes, limits); }
46
+ catch (error) { const failure = bad(`malformed JSON — ${error.message}`); failure.cause = error; throw failure; }
47
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw bad(`lock root must be a JSON object (got ${kind(parsed)})`);
48
+ const rawVersion = parsed.lockfileVersion;
49
+ if (rawVersion !== undefined && typeof rawVersion !== "number") throw bad(`lockfileVersion must be a number (got ${JSON.stringify(rawVersion)})`);
50
+ if (rawVersion !== undefined && rawVersion !== 1 && rawVersion !== 2) throw bad(`unsupported lockfileVersion ${rawVersion}`);
51
+ const version = rawVersion ?? 1;
52
+ if (parsed.capabilities !== undefined && (!parsed.capabilities || typeof parsed.capabilities !== "object" || Array.isArray(parsed.capabilities))) {
53
+ throw bad(`"capabilities" must be an object map (got ${kind(parsed.capabilities)})`);
54
+ }
55
+ const out = { version, packages: Object.create(null), capabilities: Object.create(null), legacyCapabilities: Object.create(null) };
56
+ if (version === 1) {
57
+ if (parsed.packages !== undefined) throw bad(`lockfileVersion 1 must not carry a "packages" map`);
58
+ for (const id of Object.keys(parsed.capabilities || {})) {
59
+ const entry = parsed.capabilities[id];
60
+ if (!retiredCapabilityReason(id)) {
61
+ const violation = legacyCapabilityEntryViolation(entry);
62
+ if (violation) throw bad(`legacy entry "${id}" is malformed (${violation})`, { package: id });
63
+ }
64
+ out.legacyCapabilities[id] = entry;
65
+ }
66
+ return out;
67
+ }
68
+
69
+ const rawPackages = parsed.packages;
70
+ if (!rawPackages || typeof rawPackages !== "object" || Array.isArray(rawPackages)) {
71
+ throw bad(`lockfileVersion 2 requires a "packages" object map (got ${kind(rawPackages)})`);
72
+ }
73
+ const packageKeys = Object.keys(rawPackages), hasCapabilityMap = Object.hasOwn(parsed, "capabilities");
74
+ const stateFree = packageKeys.length === 0 && (!hasCapabilityMap || Object.keys(parsed.capabilities).length === 0);
75
+ const unsupported = (why) => bad(`unsupported transitional package-root lockfileVersion 2 (${why}). This is the superseded package-store lock shape; it is not converted or interpreted. Delete this lock (and any .agents/packages directory) and recreate the scope's state with \`oats install\`.`);
76
+ if (!stateFree) {
77
+ if (!hasCapabilityMap) throw unsupported(`no top-level "capabilities" map`);
78
+ for (const id of packageKeys) {
79
+ const row = rawPackages[id];
80
+ if (!row || typeof row !== "object" || Array.isArray(row)) continue;
81
+ const tells = TRANSITIONAL_ROW_FIELDS.filter((field) => Object.hasOwn(row, field));
82
+ if (tells.length) throw unsupported(`package row "${id}" carries ${tells.join(", ")}`);
83
+ }
84
+ }
85
+ for (const id of packageKeys) {
86
+ if (!PACKAGE_ID_RE.test(id)) throw bad(`packages map has an invalid package key ${JSON.stringify(id)}`, { package: id });
87
+ const entry = rawPackages[id];
88
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw bad(`lock entry for "${id}" is not an object`, { package: id });
89
+ const extra = Object.keys(entry).filter((key) => !LOCK_PACKAGE_KEYS.has(key));
90
+ if (extra.length) throw bad(`lock entry for "${id}" has unknown keys: ${extra.join(", ")}`, { package: id });
91
+ out.packages[id] = entry;
92
+ }
93
+ for (const id of Object.keys(out.packages)) validateLockEntry(id, out.packages[id], out.packages, { file });
94
+ if (hasCapabilityMap) {
95
+ for (const id of Object.keys(parsed.capabilities)) {
96
+ if (!isMaterializedCapabilityId(id)) throw bad(`capabilities map has an invalid capability key: ${capabilityIdViolation(id)}`);
97
+ const entry = parsed.capabilities[id];
98
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw bad(`capability lock entry for "${id}" is not an object`, { package: id });
99
+ const extra = Object.keys(entry).filter((key) => !LOCK_CAPABILITY_KEYS.has(key));
100
+ if (extra.length) throw bad(`capability lock entry for "${id}" has unknown keys: ${extra.join(", ")}`, { package: id });
101
+ out.capabilities[id] = entry;
102
+ }
103
+ }
104
+ for (const id of Object.keys(out.capabilities)) validateCapabilityLockEntry(id, out.capabilities[id], out.packages, { file });
105
+ return out;
106
+ }
@@ -0,0 +1,84 @@
1
+ /** Manifest-owned setting defaults are intrinsic field fallbacks, not another
2
+ * workspace/repository policy authority. Capture them before record publication. */
3
+ import { canonicalJson, parseStrictJson } from "./portable-values.mjs";
4
+ import { bytesIntegrity } from "./portable-digest.mjs";
5
+ import { objectAt, pointerKey } from "./portable-shape.mjs";
6
+ import { validateArtifactRef, validateOrigin } from "./resolution-shape.mjs";
7
+ import { settingChoiceKey } from "./soul-constraints.mjs";
8
+ import { resolveChoices } from "./portable-choices.mjs";
9
+ import { oatsError } from "./errors.mjs";
10
+
11
+ export function manifestSettingDefaults(artifact, bytes) {
12
+ validateArtifactRef(artifact);
13
+ if (artifact.kind !== "capability") throw oatsError("invalid-declaration", "manifest defaults need a capability artifact owner");
14
+ const manifest = parseStrictJson(bytes);
15
+ objectAt(manifest, null, ["capability", "version"]);
16
+ if (manifest.capability !== artifact.capability) throw oatsError("invalid-resolution", "manifest default owner differs from its artifact");
17
+ const defaults = Object.create(null);
18
+ if (manifest.settings !== undefined) objectAt(manifest.settings, null, [], "/settings");
19
+ const document = { kind: "artifact", owner: artifact, path: "oats.json", integrity: bytesIntegrity(bytes) };
20
+ for (const [name, declaration] of Object.entries(manifest.settings ?? {})) {
21
+ if (!declaration || typeof declaration !== "object" || Array.isArray(declaration) || !Object.hasOwn(declaration, "default")) continue;
22
+ const origin = { kind: "manifest-default", document, pointer: `/settings/${pointerKey(name)}/default` };
23
+ validateOrigin(origin);
24
+ defaults[name] = { key: settingChoiceKey(artifact.capability, name), kind: "manifest-default", value: declaration.default, origin };
25
+ }
26
+ return { manifest, defaults };
27
+ }
28
+
29
+ /** Definitions are the selected, verified manifest byte snapshots. No provider
30
+ * payload code runs here, and no source/lock/approval lookup is performed. */
31
+ export function captureManifestSettings(plan, definitions) {
32
+ canonicalJson(plan);
33
+ if (plan.status === "conflict") throw oatsError("requirement-conflict", "cannot capture defaults for a conflicting software plan");
34
+ if (!Array.isArray(definitions)) throw oatsError("invalid-declaration", "selected manifest definitions must be an array");
35
+ const candidates = [...plan.candidates], settings = Object.create(null), seen = new Set();
36
+ for (const id of Object.keys(plan.capabilities)) settings[id] = Object.assign(Object.create(null), plan.settings[id] ?? {});
37
+ for (const { artifact, bytes } of definitions) {
38
+ const { defaults } = manifestSettingDefaults(artifact, bytes), id = artifact.capability;
39
+ if (!Object.hasOwn(plan.capabilities, id) || seen.has(id)) throw oatsError("invalid-resolution", "manifest defaults do not match the selected capability set");
40
+ seen.add(id);
41
+ for (const [name, candidate] of Object.entries(defaults)) {
42
+ candidates.push(candidate); settings[id][name] = candidate.key;
43
+ }
44
+ }
45
+ if (seen.size !== Object.keys(plan.capabilities).length) throw oatsError("resolution-incomplete", "selected manifest defaults were not fully captured");
46
+ const resolved = resolveChoices({ requirements: plan.requirements, candidates });
47
+ return { ...plan, ...resolved, candidates, settings };
48
+ }
49
+
50
+ /** Check both directions over ALL choices, not only settings the dispatch map
51
+ * happens to enumerate. Neither omissions nor invented intrinsic facts are valid,
52
+ * including overridden facts. Read each selected manifest only once. */
53
+ export function verifyManifestSettings(record, definitions) {
54
+ const manifests = new Map(), expected = new Map();
55
+ const originKey = ({ kind, document, pointer }) => canonicalJson({ kind, document, pointer });
56
+ const matches = (entry, candidate) => candidate && entry.kind === "manifest-default"
57
+ && canonicalJson(entry.value) === canonicalJson(candidate.value) && originKey(entry.origin) === originKey(candidate.origin);
58
+ for (const { artifact, bytes } of definitions) {
59
+ const { manifest, defaults } = manifestSettingDefaults(artifact, bytes), id = artifact.capability;
60
+ const selected = record.artifacts.capabilities[id];
61
+ if (!selected || manifests.has(id) || canonicalJson(selected.artifact) !== canonicalJson(artifact)) {
62
+ throw oatsError("resolution-incomplete", "default verification does not match the selected capability set");
63
+ }
64
+ manifests.set(id, manifest);
65
+ for (const [name, candidate] of Object.entries(defaults)) {
66
+ expected.set(candidate.key, candidate);
67
+ const key = record.dispatch.settingsChoices[id]?.[name], choice = record.choices[candidate.key];
68
+ if (key !== candidate.key || !choice || !choice.considered.some((entry) => matches(entry, candidate))) {
69
+ throw oatsError("resolution-incomplete", "captured setting omits its exact manifest default or provenance");
70
+ }
71
+ }
72
+ }
73
+ if (manifests.size !== Object.keys(record.artifacts.capabilities).length) {
74
+ throw oatsError("resolution-incomplete", "selected manifest defaults were not fully verified");
75
+ }
76
+ for (const [key, choice] of Object.entries(record.choices)) {
77
+ for (const entry of choice.considered) {
78
+ if (entry.kind === "manifest-default" && !matches(entry, expected.get(key))) {
79
+ throw oatsError("resolution-incomplete", "captured intrinsic default is not witnessed by its selected manifest");
80
+ }
81
+ }
82
+ }
83
+ return manifests;
84
+ }
@@ -0,0 +1,48 @@
1
+ /** One bounded staged dependency walker. Adapters own source syntax/fetch,
2
+ * manifest compatibility and digest versions; this engine owns graph identity,
3
+ * cycle checks and ordering, never installation, selection or executable trust. */
4
+ import { oatsError } from "./errors.mjs";
5
+
6
+ export function resolvePackageClosure({ requests, readPackage, validatePackage, finalizePackage, discardPackage,
7
+ context = "package preparation", maxPackages = 256, maxDepth = 64, maxRequests = 1024 }) {
8
+ if (!Array.isArray(requests) || !requests.length) throw oatsError("invalid-source", "package closure requires explicit root requests");
9
+ for (const callback of [readPackage, validatePackage, finalizePackage, discardPackage]) {
10
+ if (typeof callback !== "function") throw new TypeError("package closure requires complete source/validation/ownership adapters");
11
+ }
12
+ for (const limit of [maxPackages, maxDepth, maxRequests]) {
13
+ if (!Number.isSafeInteger(limit) || limit < 1) throw oatsError("resource-limit", "invalid package graph budget");
14
+ }
15
+ if (maxDepth > 256) throw oatsError("resource-limit", "package graph depth budget exceeds supported traversal");
16
+ const resolved = new Map();
17
+ let reads = 0;
18
+ const visit = (request, parent, chain) => {
19
+ if (++reads > maxRequests || chain.length > maxDepth) {
20
+ throw oatsError("resource-limit", "package dependency graph budget exceeded");
21
+ }
22
+ const record = readPackage(request, parent, chain);
23
+ if (!record || typeof record.package !== "string" || !record.package || typeof record.sourceKey !== "string" || !record.sourceKey
24
+ || !Array.isArray(record.dependencyRequests)) throw oatsError("invalid-package-manifest", "package source adapter returned incomplete graph data");
25
+ const id = record.package;
26
+ if (chain.includes(id)) throw oatsError("dependency-cycle", `package dependency cycle: ${[...chain, id].join(" → ")}`, [...chain, id]);
27
+ if (resolved.has(id)) {
28
+ const previous = resolved.get(id);
29
+ if (previous.sourceKey !== record.sourceKey) {
30
+ throw oatsError("duplicate-package-identity", `two sources claim package "${id}" at ${context}: ${previous.sourceKey} and ${record.sourceKey}`,
31
+ [...(previous.origins ?? [previous.sourceKey]), ...(record.origins ?? [record.sourceKey])]);
32
+ }
33
+ // A source adapter may reuse an already staged root. Never discard the
34
+ // authoritative copy merely because another edge reached it again.
35
+ if (record.dir !== previous.dir) discardPackage(record, previous);
36
+ return id;
37
+ }
38
+ if (resolved.size + chain.length >= maxPackages) throw oatsError("resource-limit", "package identity budget exceeded");
39
+ validatePackage(record);
40
+ const deps = record.dependencyRequests.map((dependency) => visit(dependency, record, [...chain, id]));
41
+ const complete = finalizePackage(record, deps);
42
+ if (!complete || complete.package !== id || complete.sourceKey !== record.sourceKey) throw oatsError("invalid-package-manifest", "package finalizer changed graph identity");
43
+ resolved.set(id, complete);
44
+ return id;
45
+ };
46
+ const roots = requests.map((request) => visit(request, null, []));
47
+ return { roots, packages: resolved };
48
+ }
@@ -0,0 +1,83 @@
1
+ /** Shared materialization mechanics. Kernel manifest/runtime validators are
2
+ * supplied once by the caller; this module selects no source, lock or approval. */
3
+ import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { capabilityArtifactIntegrity, copyTreeSafe } from "./artifact-tree.mjs";
6
+ import { CAPABILITY_INSTALLATION_FILE } from "./capability-provenance.mjs";
7
+ import { executableSurfaceOf } from "./capability-execution.mjs";
8
+ import { oatsError } from "./errors.mjs";
9
+
10
+ /** Provenance format v1 is byte-stable across writer kernels and digest versions:
11
+ * exactly these keys/order, two-space JSON, one LF, mode0644, no writer version. */
12
+ function capabilityInstallationRecord(cap, pkg) {
13
+ return {
14
+ schemaVersion: 1,
15
+ capability: cap.id,
16
+ version: cap.manifest.version,
17
+ package: pkg.package,
18
+ packageVersion: pkg.version,
19
+ source: pkg.source,
20
+ commit: pkg.commit,
21
+ packagePath: pkg.path,
22
+ capabilityPath: cap.rel,
23
+ };
24
+ }
25
+ function assertMaterializationInput(root) {
26
+ if (!lstatSync(root).isDirectory()) throw oatsError("invalid-package-manifest", "a materialized capability root must be a real directory, not a source alias");
27
+ let metadata;
28
+ try { metadata = lstatSync(join(root, CAPABILITY_INSTALLATION_FILE)); }
29
+ catch (error) { if (error.code === "ENOENT") return; throw error; }
30
+ if (!metadata.isFile() || (metadata.mode & 0o100) !== 0 || !readdirSync(root).includes(CAPABILITY_INSTALLATION_FILE)) {
31
+ throw oatsError("invalid-package-manifest", "source provenance entry must not be a link, executable, directory or filesystem alias");
32
+ }
33
+ }
34
+ function writeCapabilityInstallation(dest, cap, pkg) {
35
+ assertMaterializationInput(dest);
36
+ const temporary = mkdtempSync(join(dest, ".installation-")), candidate = join(temporary, "record.json");
37
+ let failed = false, primary;
38
+ try {
39
+ writeFileSync(candidate, JSON.stringify(capabilityInstallationRecord(cap, pkg), null, 2) + "\n", { flag: "wx", mode: 0o644 });
40
+ chmodSync(candidate, 0o644);
41
+ // Replace the reserved entry itself, never follow a source-controlled link
42
+ // or mutate another file sharing its inode. Valid v1 serialization is unchanged.
43
+ renameSync(candidate, join(dest, CAPABILITY_INSTALLATION_FILE));
44
+ } catch (error) { failed = true; primary = error; throw error; }
45
+ finally {
46
+ try { rmSync(candidate, { force: true }); rmdirSync(temporary); }
47
+ catch (cleanup) {
48
+ const error = failed ? new AggregateError([primary, cleanup], "provenance write and staging cleanup failed", { cause: primary })
49
+ : new Error("provenance staging cleanup failed", { cause: cleanup });
50
+ error.code = failed ? primary.code : cleanup.code; error.stagingPath = temporary; throw error;
51
+ }
52
+ }
53
+ }
54
+
55
+ export function createCapabilityMaterializer({ materializeCapabilityDeps, assertCapabilitySelfContained, assertMaterializedDepsContained, assertNoNativeBinaries }) {
56
+ for (const validate of [materializeCapabilityDeps, assertCapabilitySelfContained, assertMaterializedDepsContained, assertNoNativeBinaries]) {
57
+ if (typeof validate !== "function") throw new TypeError("materializer requires the kernel's complete validation callbacks");
58
+ }
59
+ /** The returned legacy integrity remains literal compatibility evidence. New
60
+ * preparation computes the explicit new-format integrity over this same tree. */
61
+ return function materializeCapability({ cap, pkg, artifactsDir }) {
62
+ assertMaterializationInput(cap.dir);
63
+ const rep = materializeCapabilityDeps(cap.dir);
64
+ if (rep.error) throw oatsError("invalid-package-manifest", `runtime dependency materialization failed for capability "${cap.id}" of package "${pkg.package}": ${rep.error}`);
65
+ assertCapabilitySelfContained(cap.dir, cap.manifest);
66
+ assertMaterializedDepsContained(cap.dir);
67
+ assertNoNativeBinaries(cap.dir);
68
+ const dest = join(artifactsDir, cap.id);
69
+ mkdirSync(dirname(dest), { recursive: true });
70
+ if (cap.rel === ".") {
71
+ // Published flat packages need their original root for subsequent template
72
+ // reads; copy instead of moving, preserving literal links and real modes.
73
+ copyTreeSafe(cap.dir, dest);
74
+ } else renameSync(cap.dir, dest);
75
+ writeCapabilityInstallation(dest, cap, pkg);
76
+ return {
77
+ capability: cap.id, version: cap.manifest.version, package: pkg.package, path: cap.rel,
78
+ dir: dest, integrity: capabilityArtifactIntegrity(dest),
79
+ layer: cap.manifest.layer ?? null,
80
+ executableSurface: executableSurfaceOf(cap.manifest), manifest: cap.manifest,
81
+ };
82
+ };
83
+ }