@awebai/oats 0.25.9 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +8 -6
  2. package/bin/oats.mjs +648 -1755
  3. package/capabilities/oats-authoring/oats-package.json +2 -2
  4. package/capabilities/oats-authoring/oats.json +2 -2
  5. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
  6. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
  7. package/capabilities/oats-aweb/bin/oats-aweb.mjs +279 -93
  8. package/capabilities/oats-aweb/injects/aweb.md +7 -2
  9. package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
  10. package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
  11. package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
  12. package/capabilities/oats-aweb/oats.json +8 -4
  13. package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
  14. package/capabilities/oats-jira/oats.json +2 -2
  15. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
  16. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
  17. package/capabilities/oats-linear/oats.json +2 -2
  18. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
  19. package/capabilities/oats-review/oats.json +3 -2
  20. package/docs/capabilities.md +229 -58
  21. package/docs/capability-manifest.schema.json +29 -9
  22. package/docs/configuration.md +17 -5
  23. package/docs/conventions.md +18 -28
  24. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
  25. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
  26. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
  27. package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
  28. package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
  29. package/docs/design/2026-09-20-redesign-program-board.md +2 -2
  30. package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
  31. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
  32. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +34 -1
  33. package/docs/design/2026-09-24-phase-d-plan.md +57 -0
  34. package/docs/design/2026-09-25-teams-contract.md +226 -0
  35. package/docs/design/README.md +3 -3
  36. package/docs/design/launch-configurations.md +20 -16
  37. package/docs/design/operations-contract.md +27 -10
  38. package/docs/desktop-cli-api.md +604 -271
  39. package/docs/desktop-instance-start.md +3 -3
  40. package/docs/desktop.md +7 -13
  41. package/docs/execution-targets.md +16 -18
  42. package/docs/first-team.md +15 -18
  43. package/docs/implementation.md +31 -62
  44. package/docs/integrations.md +64 -33
  45. package/docs/knowledge-capability-authoring.md +1 -1
  46. package/docs/knowledge-reference/package-craft.md +10 -8
  47. package/docs/knowledge-theory.md +1 -1
  48. package/docs/knowledge.md +10 -11
  49. package/docs/layers.md +16 -17
  50. package/docs/oats-local.schema.json +30 -1
  51. package/docs/oats-membership.schema.json +5 -3
  52. package/docs/oats-package.schema.json +2 -2
  53. package/docs/oats-workspace.schema.json +1 -1
  54. package/docs/{official-marketplace.md → official-catalog.md} +15 -16
  55. package/docs/packages.md +76 -53
  56. package/docs/release-notes/v0.22.0.md +1 -1
  57. package/docs/release-notes/v0.23.1.md +1 -1
  58. package/docs/release-notes/v0.26.0.md +670 -0
  59. package/docs/release-notes/v0.27.0.md +100 -0
  60. package/docs/schedules.md +54 -132
  61. package/docs/servers.md +4 -4
  62. package/docs/soul.schema.json +11 -4
  63. package/docs/souls-and-instances.md +60 -47
  64. package/docs/workspaces.md +80 -58
  65. package/injects/instance-boundary.md +2 -2
  66. package/injects/work-attached.md +1 -1
  67. package/injects/work-workspace.md +2 -2
  68. package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
  69. package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
  70. package/lib/capability-contract.mjs +110 -0
  71. package/lib/config-data.mjs +2 -2
  72. package/lib/core.mjs +947 -5023
  73. package/lib/deprecation.mjs +24 -0
  74. package/lib/digest.mjs +12 -0
  75. package/lib/instance-inspect.mjs +397 -0
  76. package/lib/instance-lifecycle.mjs +3 -4
  77. package/lib/instance-resolution.mjs +212 -26
  78. package/lib/instruction-composition.mjs +0 -20
  79. package/lib/materialize.mjs +6 -4
  80. package/lib/operator-dispatch.mjs +33 -13
  81. package/lib/packages.mjs +25 -190
  82. package/lib/process-group.mjs +1 -1
  83. package/lib/provider-binding.mjs +4 -2
  84. package/lib/provider-reasons.mjs +3 -68
  85. package/lib/remote.mjs +1 -1
  86. package/lib/resolve.mjs +204 -68
  87. package/lib/schedule.mjs +136 -292
  88. package/lib/servers.mjs +70 -38
  89. package/lib/{portable-shape.mjs → shape.mjs} +4 -3
  90. package/lib/tree-copy.mjs +44 -0
  91. package/lib/workspace.mjs +132 -20
  92. package/package-catalog.json +6 -6
  93. package/package.json +1 -1
  94. package/packages/record/lib/session-roots.mjs +8 -6
  95. package/skills/integration-authoring/SKILL.md +48 -40
  96. package/skills/oats-getting-started/SKILL.md +105 -110
  97. package/skills/oats-support/SKILL.md +2 -2
  98. package/skills/soul-craft/SKILL.md +13 -6
  99. package/bin/oats-pi-sdk-host.mjs +0 -17
  100. package/docs/2026-09-03-architecture-proposal.md +0 -642
  101. package/docs/artifact-approvals.schema.json +0 -7
  102. package/docs/captured-invocation-context.schema.json +0 -7
  103. package/docs/captured-resolution.schema.json +0 -7
  104. package/docs/design/package-engine-contract.md +0 -813
  105. package/docs/design/package-runtime-api.md +0 -588
  106. package/docs/desktop-succession.md +0 -57
  107. package/docs/execution-capsule.schema.json +0 -108
  108. package/docs/first-team-demo.md +0 -92
  109. package/docs/knowledge-migration.md +0 -147
  110. package/docs/migration-from-oas.md +0 -103
  111. package/docs/oats-config.schema.json +0 -172
  112. package/docs/oats-lock-v3.schema.json +0 -7
  113. package/docs/oats-lock.schema.json +0 -175
  114. package/docs/operating-team-migration.md +0 -470
  115. package/docs/portable.schema.json +0 -2512
  116. package/docs/provider-check-input.schema.json +0 -7
  117. package/docs/rebuild-to-v2.md +0 -511
  118. package/docs/workspace-adoption.md +0 -74
  119. package/injects/framework-workspace.md +0 -7
  120. package/injects/local-soul.md +0 -19
  121. package/injects/oats-portable.md +0 -20
  122. package/injects/oats.md +0 -11
  123. package/injects/portable-instance-boundary.md +0 -39
  124. package/injects/portable-work-directory.md +0 -29
  125. package/lib/artifact-approvals.mjs +0 -120
  126. package/lib/artifact-tree.mjs +0 -141
  127. package/lib/capability-artifacts.mjs +0 -179
  128. package/lib/capability-execution.mjs +0 -15
  129. package/lib/capability-inputs.mjs +0 -39
  130. package/lib/capability-provenance.mjs +0 -231
  131. package/lib/captured-action-shape.mjs +0 -21
  132. package/lib/captured-admission-shape.mjs +0 -20
  133. package/lib/captured-binding-file.mjs +0 -36
  134. package/lib/captured-dispatch.mjs +0 -66
  135. package/lib/captured-instance-index.mjs +0 -277
  136. package/lib/captured-invocation-context.mjs +0 -130
  137. package/lib/captured-launch-request.mjs +0 -66
  138. package/lib/captured-operation-process.mjs +0 -15
  139. package/lib/captured-pi-custody.mjs +0 -29
  140. package/lib/captured-pi-host.mjs +0 -167
  141. package/lib/captured-pi-outcome.mjs +0 -172
  142. package/lib/captured-resolutions.mjs +0 -275
  143. package/lib/captured-scaffold.mjs +0 -87
  144. package/lib/captured-selector.mjs +0 -28
  145. package/lib/captured-session-backend.mjs +0 -52
  146. package/lib/captured-source-receipt-file.mjs +0 -72
  147. package/lib/helper-injection-policy.mjs +0 -104
  148. package/lib/legacy-lock-codec.mjs +0 -106
  149. package/lib/manifest-settings.mjs +0 -84
  150. package/lib/package-closure.mjs +0 -48
  151. package/lib/package-materialization.mjs +0 -83
  152. package/lib/pi-sdk-host.mjs +0 -229
  153. package/lib/portable-artifacts.mjs +0 -115
  154. package/lib/portable-choices.mjs +0 -82
  155. package/lib/portable-composition.mjs +0 -136
  156. package/lib/portable-digest.mjs +0 -105
  157. package/lib/portable-identity.mjs +0 -40
  158. package/lib/portable-lock.mjs +0 -117
  159. package/lib/portable-onboarding-request.mjs +0 -49
  160. package/lib/portable-onboarding.mjs +0 -256
  161. package/lib/portable-package-preparation.mjs +0 -188
  162. package/lib/portable-policy.mjs +0 -44
  163. package/lib/portable-soul.mjs +0 -42
  164. package/lib/portable-state.mjs +0 -80
  165. package/lib/prepare-composition.mjs +0 -170
  166. package/lib/prepared-bindings.mjs +0 -92
  167. package/lib/prepared-resources.mjs +0 -127
  168. package/lib/provider-binding-broker.mjs +0 -65
  169. package/lib/provider-binding-wire.mjs +0 -116
  170. package/lib/readiness.mjs +0 -225
  171. package/lib/repository-observation.mjs +0 -226
  172. package/lib/resolution-shape.mjs +0 -393
  173. package/lib/schedule-capsule.mjs +0 -206
  174. package/lib/soul-constraints.mjs +0 -40
  175. package/lib/source-projection.mjs +0 -84
  176. package/lib/source-spec.mjs +0 -189
  177. package/lib/workspace-definition.mjs +0 -126
  178. package/lib/workspace-discovery.mjs +0 -146
  179. package/skills/oats/SKILL.md +0 -162
  180. package/skills/oats-config/SKILL.md +0 -164
  181. package/skills/oats-packages/SKILL.md +0 -184
  182. package/skills/oats-portable/SKILL.md +0 -115
  183. package/skills/oats-portable-artifacts/SKILL.md +0 -63
@@ -1,87 +0,0 @@
1
- /** Materialize one already-verified captured composition into a new private home.
2
- * Home placement is supplied by the lifecycle owner; this module never discovers
3
- * config, souls, work targets, launch software or instance roots. */
4
- import { existsSync, lstatSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
6
- import { randomUUID } from "node:crypto";
7
- import { capturedDirectoryIdentity, registerCapturedInstance, syncCapturedMetadata } from "./captured-instance-index.mjs";
8
- import { copyTreeSafe } from "./artifact-tree.mjs";
9
- import { canonicalJson } from "./portable-values.mjs";
10
- import { portableScope } from "./portable-state.mjs";
11
- import { oatsError } from "./errors.mjs";
12
-
13
- const INSTANCE_RE = /^[a-z0-9][a-z0-9-]{0,127}$/;
14
-
15
- function sameDirectory(path, witness) {
16
- try {
17
- const current = lstatSync(path);
18
- return current.isDirectory() && current.dev === witness.dev && current.ino === witness.ino;
19
- } catch { return false; }
20
- }
21
-
22
- export function validateCapturedDirectoryTarget(home, instance) {
23
- if (typeof home !== "string" || !isAbsolute(home) || resolve(home) !== home || basename(home) !== instance || !INSTANCE_RE.test(instance)) {
24
- throw oatsError("invalid-declaration", "captured scaffold needs a normalized absolute home whose basename is its instance name");
25
- }
26
- }
27
-
28
- export function materializeCapturedDirectoryScaffold({ home, instance, loaded }) {
29
- validateCapturedDirectoryTarget(home, instance);
30
- if (!loaded?.record || !loaded?.composition || !loaded?.resolution || typeof loaded.deployment !== "string") throw oatsError("invalid-resolution", "captured scaffold requires a verified composition load");
31
- const { record, composition } = loaded;
32
- if (record.dispatch.composition?.mode !== "directory") throw oatsError("needs-configuration", "captured scaffold currently requires retained directory work mode");
33
- if (existsSync(home)) throw oatsError("E_INSTANCE_EXISTS", `instance home already exists: ${home}`);
34
- const parent = dirname(home), parentStat = lstatSync(parent);
35
- if (!parentStat.isDirectory() || realpathSync(parent) !== parent) throw oatsError("E_NO_CANONICAL_ROOT", "captured scaffold parent must be a physical directory");
36
- const subject = record.subject, agent = subject.kind === "persistent" ? subject.soul.alias : subject.name;
37
- const responsibleHuman = record.messagingChoice.enabled ? record.messagingChoice.privateKey.human : null;
38
- const executionBinding = { schemaVersion: 1, deployment: portableScope(loaded.deployment), resolution: loaded.resolution };
39
- const bodyFile = loaded.resources.get(record.dispatch.composition.body);
40
- if (!bodyFile) throw oatsError("invalid-resolution", "captured scaffold body is absent from verified resources");
41
- const sourceRoot = dirname(bodyFile);
42
- const metadata = {
43
- instance, agent, kind: subject.kind, home, work: "directory", launched: false, incarnationId: randomUUID(),
44
- executionBinding, responsibleHuman,
45
- captured: { schemaVersion: 1, resolution: loaded.resolution, lifecycle: "scaffolded-hooks-pending" },
46
- capabilities: [...loaded.capabilities.values()].map(({ id, manifest, settings }) => ({ id, version: manifest.version, settings })),
47
- };
48
- let owned, primary, indexAttempted = false;
49
- try {
50
- mkdirSync(home, { mode: 0o700 }); owned = lstatSync(home);
51
- symlinkSync(sourceRoot, join(home, "soul"));
52
- writeFileSync(join(home, "AGENTS.md"), composition.text, { mode: 0o600 });
53
- symlinkSync("AGENTS.md", join(home, "CLAUDE.md"));
54
- mkdirSync(join(home, ".agents", "skills"), { recursive: true, mode: 0o700 });
55
- mkdirSync(join(home, ".claude"), { mode: 0o700 });
56
- for (const skill of composition.skills) copyTreeSafe(skill.path, join(home, ".agents", "skills", skill.name));
57
- symlinkSync(join("..", ".agents", "skills"), join(home, ".claude", "skills"));
58
- mkdirSync(join(home, "work"), { mode: 0o700 });
59
- for (const skill of composition.skills) {
60
- const file = join(home, ".agents", "skills", skill.name, "SKILL.md");
61
- if (!existsSync(file) || !lstatSync(file).isFile()) throw oatsError("resolution-incomplete", `captured skill ${skill.name} did not materialize`);
62
- }
63
- metadata.captured.custody = { home: capturedDirectoryIdentity(home), work: capturedDirectoryIdentity(join(home, "work")) };
64
- writeFileSync(join(home, "instance.json"), canonicalJson(metadata), { flag: "wx", mode: 0o600 });
65
- for (const path of [join(home, "instance.json"), join(home, "work"), home, parent]) syncCapturedMetadata(path);
66
- indexAttempted = true;
67
- registerCapturedInstance(loaded.deployment, home);
68
- return { home, instance, agent, work: "directory", incarnationId: metadata.incarnationId, executionBinding, responsibleHuman, launched: false, hooksPending: true };
69
- } catch (error) {
70
- primary = error;
71
- // Publication/guard cleanup may fail AFTER index publication. Retain the
72
- // home rather than erase evidence just because the call did not return.
73
- if (indexAttempted) { error.home = home; error.cleanupRequired = true; }
74
- throw error;
75
- }
76
- finally {
77
- if (primary && owned && !indexAttempted) {
78
- try {
79
- if (!sameDirectory(home, owned)) throw oatsError("integrity-drift", "captured scaffold home ownership changed during rollback");
80
- rmSync(home, { recursive: true });
81
- } catch (cleanup) {
82
- const error = new AggregateError([primary, cleanup], "captured scaffold and rollback failed", { cause: primary });
83
- error.code = primary.code; throw error;
84
- }
85
- }
86
- }
87
- }
@@ -1,28 +0,0 @@
1
- /** Explicit independent-dispatch selectors, with the captured command's own
2
- * environment as a propagation channel. Never infer a record from cwd/locks. */
3
- import { isAbsolute } from "node:path";
4
- import { validateResolutionRef } from "./resolution-shape.mjs";
5
- import { oatsError } from "./errors.mjs";
6
-
7
- export function capturedSelector(argv, env = process.env) {
8
- const values = new Map(), args = [];
9
- for (let i = 0; i < argv.length; i++) {
10
- const arg = argv[i];
11
- if (arg === "--") { args.push(...argv.slice(i)); break; }
12
- const match = /^(--deployment|--resolution|--artifact-set)(?:=(.*))?$/.exec(arg);
13
- if (!match) { args.push(arg); continue; }
14
- const name = match[1].slice(2), value = match[2] ?? argv[++i];
15
- if (values.has(name) || !value || value.startsWith("--")) throw oatsError("E_BAD_ARGS", `--${name} needs one unambiguous value`);
16
- values.set(name, value);
17
- }
18
- const explicit = values.size > 0;
19
- const deployment = explicit ? values.get("deployment") : env.OATS_DEPLOYMENT;
20
- if (values.has("resolution") && values.has("artifact-set")) throw oatsError("E_BAD_ARGS", "choose a resolution or an artifact set, never both");
21
- const artifactSet = values.get("artifact-set");
22
- const id = explicit ? artifactSet ?? values.get("resolution") : env.OATS_RESOLUTION;
23
- if (!explicit && deployment === undefined && id === undefined) return null;
24
- if (typeof deployment !== "string" || !isAbsolute(deployment) || typeof id !== "string") throw oatsError("E_BAD_ARGS", "portable selectors need --deployment <absolute path> and one --resolution or --artifact-set <sha256-id>");
25
- const resolution = { schemaVersion: 1, id };
26
- validateResolutionRef(resolution);
27
- return { deployment, ...(artifactSet === undefined ? { resolution } : { artifactSet }), args, explicit };
28
- }
@@ -1,52 +0,0 @@
1
- /** Closed inputs for the existing native session adapters. No config lookup,
2
- * endpoint allocation, backend registry or runtime/provider readiness probe. */
3
- import { isAbsolute, resolve } from 'node:path';
4
- import { canonicalJson } from './portable-values.mjs';
5
- import { objectAt } from './portable-shape.mjs';
6
- import { HERDR_SUPPORTED_PROTOCOLS, isSupportedHerdrProtocol, validHerdrTarget } from './herdr.mjs';
7
- import { oatsError } from './errors.mjs';
8
-
9
- const same = (a,b) => canonicalJson(a) === canonicalJson(b);
10
- const absolute = value => typeof value === 'string' && isAbsolute(value) && resolve(value) === value && !value.includes('\0');
11
- export function validateCapturedSessionBackend(value) {
12
- canonicalJson(value);
13
- const fields=value?.backend==='herdr'?['backend','binary','socket','protocol']:['backend','binary','socket','session'];
14
- objectAt(value,fields,fields);
15
- if(!['tmux','herdr'].includes(value.backend)||!absolute(value.binary)||!absolute(value.socket))throw oatsError('needs-configuration','captured native backend needs explicit normalized host binary/socket');
16
- if(value.backend==='herdr'){
17
- if(!isSupportedHerdrProtocol(value.protocol))throw oatsError('needs-configuration',`captured Herdr requires an explicitly selected protocol in ${HERDR_SUPPORTED_PROTOCOLS.join(', ')}`);
18
- }else if(typeof value.session!=='string'||!/^[A-Za-z0-9_-]+$/.test(value.session))throw oatsError('needs-configuration','captured tmux needs an explicit supported session name');
19
- return value;
20
- }
21
-
22
- export function validateCapturedSessionTarget(value,backend,instance) {
23
- validateCapturedSessionBackend(backend);canonicalJson(value);
24
- if(backend.backend==='tmux'){
25
- objectAt(value,['backend','session','window','socket'],['backend','session','window','socket']);
26
- if(!same(value,{backend:'tmux',session:backend.session,window:instance,socket:backend.socket}))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','captured tmux target differs from admitted endpoint');
27
- }else{
28
- objectAt(value,['backend','binary','socket','protocol','workspaceId','paneId','terminalId'],['backend','binary','socket','protocol','workspaceId','paneId','terminalId']);
29
- if(!validHerdrTarget(value)||value.binary!==backend.binary||value.socket!==backend.socket||value.protocol!==backend.protocol
30
- ||[value.workspaceId,value.paneId,value.terminalId].some(id=>id.includes('\0')))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','captured Herdr target differs from admitted endpoint/identifier contract');
31
- }
32
- return value;
33
- }
34
-
35
- /** First placement admits a server endpoint, not caller-chosen Herdr IDs.
36
- * Subsequent IDs must come from independently retained native observations. */
37
- export function assertCapturedSessionPlacement(metadata,backend,knownTargets=[]) {
38
- validateCapturedSessionBackend(backend);
39
- if(typeof metadata.launched!=='boolean')throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','captured metadata lacks native launch state');
40
- if(metadata.launched&&!Object.hasOwn(metadata,backend.backend==='herdr'?'sessionTarget':'tmux'))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','launched metadata lacks its native target');
41
- if(Object.hasOwn(metadata,'backend')&&metadata.backend!==backend.backend)throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','captured metadata selects a different backend');
42
- if(backend.backend==='tmux'){
43
- if(Object.hasOwn(metadata,'sessionTarget'))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','Herdr target cannot redirect captured tmux');
44
- if(Object.hasOwn(metadata,'tmux'))validateCapturedSessionTarget({backend:'tmux',...metadata.tmux},backend,metadata.instance);
45
- }else{
46
- if(Object.hasOwn(metadata,'tmux'))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','tmux metadata cannot redirect captured Herdr');
47
- if(Object.hasOwn(metadata,'sessionTarget')){
48
- validateCapturedSessionTarget(metadata.sessionTarget,backend,metadata.instance);
49
- if(!knownTargets.some(target=>same(target,metadata.sessionTarget)))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','Herdr metadata target lacks independent indexed custody');
50
- }
51
- }
52
- }
@@ -1,72 +0,0 @@
1
- /** Private captured-source registration input. This is a synchronous hook
2
- * snapshot, never durable worker authority or an ambient configuration file. */
3
- import { lstatSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
- import { isAbsolute, join, resolve } from "node:path";
5
- import { validateSoulIdentity } from "./portable-identity.mjs";
6
- import { canonicalJson } from "./portable-values.mjs";
7
- import { objectAt, stringAt } from "./portable-shape.mjs";
8
- import { portableStateDirectory } from "./portable-state.mjs";
9
- import { validateProviderBinding } from "./resolution-shape.mjs";
10
- import { validateExecutionBinding } from "./schedule-capsule.mjs";
11
- import { oatsError } from "./errors.mjs";
12
-
13
- export const CAPTURED_SOURCE_RECEIPT_LIMITS = Object.freeze({ maxBytes: 256 * 1024, maxDepth: 32, maxEntries: 16_384 });
14
- const RECEIPT_FIELDS = ["schemaVersion", "kind", "home", "work", "context", "agent", "instance", "sourceIdentity", "role", "executionBinding", "responsibleHuman", "binding"];
15
-
16
- function absolutePath(value, pointer) {
17
- stringAt(value, pointer);
18
- if (!isAbsolute(value) || resolve(value) !== value || value.includes("\0")) throw oatsError("invalid-declaration", `${pointer} must be a normalized absolute path`);
19
- }
20
-
21
- function human(value) {
22
- if (value === null) return;
23
- objectAt(value, ["provider", "id"], ["provider", "id"], "/responsibleHuman");
24
- stringAt(value.provider, "/responsibleHuman/provider");
25
- stringAt(value.id, "/responsibleHuman/id");
26
- }
27
-
28
- export function validateCapturedSourceReceipt(value) {
29
- canonicalJson(value, CAPTURED_SOURCE_RECEIPT_LIMITS);
30
- objectAt(value, RECEIPT_FIELDS, RECEIPT_FIELDS);
31
- if (value.schemaVersion !== 1) throw oatsError("unsupported-wire-version", "captured source receipt schemaVersion must be 1");
32
- if (!["persistent", "helper"].includes(value.kind)) throw oatsError("invalid-declaration", "captured source receipt kind must be persistent or helper");
33
- for (const [key, field] of [["home", value.home], ["work", value.work], ["context", value.context]]) absolutePath(field, `/${key}`);
34
- if (value.context !== value.executionBinding?.deployment) throw oatsError("invalid-declaration", "captured source receipt context must equal its execution deployment");
35
- stringAt(value.agent, "/agent"); stringAt(value.instance, "/instance"); stringAt(value.role, "/role");
36
- for (const [pointer, text] of [["/agent", value.agent], ["/instance", value.instance], ["/role", value.role]]) if (text.includes("\0")) throw oatsError("invalid-declaration", `${pointer} contains NUL`);
37
- if (!value.agent || !value.instance || Buffer.byteLength(value.role, "utf8") > 128 * 1024) throw oatsError("invalid-declaration", "captured source receipt identity/role is empty or exceeds its bound");
38
- if (value.kind === "persistent") {
39
- if (value.sourceIdentity === null) throw oatsError("invalid-declaration", "persistent captured source receipt needs a qualified soul identity");
40
- validateSoulIdentity(value.sourceIdentity);
41
- } else if (value.sourceIdentity !== null) throw oatsError("invalid-declaration", "helper captured source receipt must not claim a persistent soul identity");
42
- validateExecutionBinding(value.executionBinding); human(value.responsibleHuman); validateProviderBinding(value.binding);
43
- return value;
44
- }
45
-
46
- export function withCapturedSourceReceiptFile(home, receipt, run) {
47
- validateCapturedSourceReceipt(receipt);
48
- if (typeof run !== "function") throw oatsError("invalid-declaration", "captured source receipt consumer must be a function");
49
- if (resolve(home) !== receipt.home) throw oatsError("invalid-declaration", "captured source receipt home differs from its invocation home");
50
- const directory = mkdtempSync(join(portableStateDirectory(receipt.executionBinding.deployment, true), ".source-receipt-"));
51
- const owned = lstatSync(directory), file = join(directory, "receipt.json"); let primary, result, completed = false;
52
- try {
53
- writeFileSync(file, canonicalJson(receipt, CAPTURED_SOURCE_RECEIPT_LIMITS), { flag: "wx", mode: 0o600 });
54
- result = run({ OATS_SOURCE_RECEIPT_FILE: file }); completed = true; return result;
55
- } catch (error) { primary = error; throw error; }
56
- finally {
57
- try {
58
- const current = lstatSync(directory);
59
- if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError("integrity-drift", "source receipt invocation directory ownership changed");
60
- rmSync(directory, { recursive: true });
61
- } catch (cleanup) {
62
- if (!primary) {
63
- const error = new AggregateError([cleanup], "captured source receipt cleanup failed after invocation", { cause: cleanup });
64
- error.code = cleanup.code; error.invocationCompleted = completed; error.invocationResult = result; throw error;
65
- }
66
- const error = new AggregateError([primary, cleanup], "captured source receipt invocation and cleanup failed", { cause: primary });
67
- error.code = primary.code;
68
- if (primary.invocationCompleted) { error.invocationCompleted = true; error.invocationResult = primary.invocationResult; }
69
- throw error;
70
- }
71
- }
72
- }
@@ -1,104 +0,0 @@
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) {
39
- // Attributed like every other preparation problem: the operator learns WHICH
40
- // capability lacks the declaration, not only that one does (second-operator finding).
41
- const error = oatsError('needs-configuration', 'new helper injection requires an explicit capability policy');
42
- error.problems = [{ code: 'needs-configuration', message: 'capability ships an inject without a helperInjection policy', capability: id, ...(policy.manifest.layer ? { slot: policy.manifest.layer } : {}) }];
43
- throw error;
44
- }
45
- if (policy.fact) requirements.push(policy.fact);
46
- }
47
- const resolved = resolveChoices({ requirements, candidates: plan.candidates });
48
- if (resolved.status !== 'resolved') throw oatsError(resolved.status === 'conflict' ? 'requirement-conflict' : 'needs-configuration', 'helper policy conflicts with captured instruction choices', resolved.problems);
49
- return { choices: resolved.choices, policies };
50
- }
51
-
52
- /** Verify BOTH directions. Read-only verification permits literal legacy
53
- * evidence; publication must explicitly disallow new legacy/missing-policy mint.
54
- * Existing resource verification still proves physical containment/kind. */
55
- export function verifyHelperInjectionPolicies(record, definitions, { publishing = false } = {}) {
56
- const helper = record.subject.kind === 'helper', composition = record.dispatch.composition;
57
- const expected = new Map(), selected = new Set();
58
- for (const definition of definitions) {
59
- const policy = helperInjectionFact(definition), id = definition.artifact.capability;
60
- 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');
61
- selected.add(id);
62
- if (!helper) continue;
63
- if (publishing && !policy.fact && policy.manifest.inject) throw oatsError('needs-configuration', 'new helper record lacks an explicit injection policy');
64
- if (!policy.fact) continue;
65
- const { fact, source, resource, resourceKey } = policy, choice = record.choices[fact.key];
66
- expected.set(fact.key, fact);
67
- 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');
68
- const block = composition?.blocks.find(entry => entry.source === source), omission = composition?.omissions.find(entry => entry.source === source);
69
- if (resource) {
70
- 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');
71
- } else if (block || !omission || omission.reason !== 'helper-policy' || omission.choice !== fact.key) throw oatsError('invalid-resolution', 'helper omission differs from explicit capability policy');
72
- }
73
- if (selected.size !== Object.keys(record.artifacts.capabilities).length) throw oatsError('resolution-incomplete', 'helper policy manifest inputs are incomplete');
74
- const isPolicyOrigin = origin => origin?.kind === 'source-export' && (origin.pointer === '/helperInjection' || origin.pointer?.startsWith('/helperInjection/'));
75
- for (const [key, choice] of Object.entries(record.choices)) {
76
- if (key.startsWith(prefix) && !expected.has(key)) throw oatsError('invalid-resolution', 'helper policy choice lacks an applicable declaration');
77
- for (const entry of [...choice.constraints, ...choice.considered]) if (isPolicyOrigin(entry.origin)) {
78
- const fact = expected.get(key);
79
- 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');
80
- }
81
- 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');
82
- }
83
- const ownSource = fact => `capability:${fact.origin.document.owner.capability}`;
84
- for (const block of composition?.blocks ?? []) if (block.choice?.startsWith(prefix)) {
85
- const fact = expected.get(block.choice);
86
- if (!fact || fact.value === null || block.source !== ownSource(fact) || block.resource !== fact.value) throw oatsError('invalid-resolution', 'helper instruction witness cannot control another contribution');
87
- }
88
- for (const omission of composition?.omissions ?? []) {
89
- if (publishing && omission.reason === 'helper-knowledge') throw oatsError('needs-configuration', 'new publication cannot mint legacy slot-based helper omissions');
90
- if (omission.reason === 'helper-policy') {
91
- const fact = expected.get(omission.choice);
92
- if (!helper || !fact || fact.value !== null || omission.source !== ownSource(fact)) throw oatsError('invalid-resolution', 'helper omission lacks its declared own policy');
93
- }
94
- }
95
- // A missing/non-policy choice is not a way around capability ownership.
96
- // Above we prove each declared contribution's exact file/omission; here every
97
- // new capability contribution must point back to that owner's proved fact.
98
- // Literal historical read/reuse and primary composition stay unchanged.
99
- if (helper && publishing) for (const entry of [...(composition?.blocks ?? []), ...(composition?.omissions ?? [])]) {
100
- if (!entry.source.startsWith('capability:')) continue;
101
- const fact = expected.get(helperInjectionChoiceKey(entry.source.slice('capability:'.length)));
102
- if (!fact || entry.choice !== fact.key) throw oatsError('invalid-resolution', 'new helper capability contribution lacks its declared own policy');
103
- }
104
- }
@@ -1,106 +0,0 @@
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
- }
@@ -1,84 +0,0 @@
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
- }
@@ -1,48 +0,0 @@
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
- }