@awebai/oats 0.23.2 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/README.md +224 -391
  2. package/bin/oats-pi-sdk-host.mjs +17 -0
  3. package/bin/oats.mjs +470 -51
  4. package/capabilities/oats-okf/bin/oats-okf-binding.mjs +14 -0
  5. package/capabilities/oats-okf/bin/oats-okf.mjs +79 -11
  6. package/capabilities/oats-okf/lib/binding-wire.mjs +268 -0
  7. package/capabilities/oats-okf/lib/captured-worker.mjs +109 -0
  8. package/capabilities/oats-okf/lib/config.mjs +2 -1
  9. package/capabilities/oats-okf/lib/inspection.mjs +16 -1
  10. package/capabilities/oats-okf/lib/invocation-context.mjs +111 -0
  11. package/capabilities/oats-okf/lib/invocation-shape.mjs +135 -0
  12. package/capabilities/oats-okf/lib/io.mjs +1 -1
  13. package/capabilities/oats-okf/lib/portable-binding.mjs +199 -0
  14. package/capabilities/oats-okf/lib/source-contract.mjs +46 -0
  15. package/capabilities/oats-okf/lib/sources.mjs +123 -3
  16. package/capabilities/oats-okf/lib/stores.mjs +104 -25
  17. package/capabilities/oats-okf/lib/worker.mjs +69 -10
  18. package/capabilities/oats-okf/oats.json +35 -7
  19. package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +87 -0
  20. package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +113 -0
  21. package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +23 -5
  22. package/capabilities/oats-okf/skills/okf/SKILL.md +42 -1
  23. package/docs/artifact-approvals.schema.json +7 -0
  24. package/docs/capabilities.md +4 -0
  25. package/docs/capability-manifest.schema.json +37 -66
  26. package/docs/captured-invocation-context.schema.json +7 -0
  27. package/docs/captured-resolution.schema.json +7 -0
  28. package/docs/design/2026-09-14-artifact-retention-contract.md +190 -0
  29. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +708 -0
  30. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +85 -0
  31. package/docs/design/2026-09-14-portable-souls-explainer.md +750 -0
  32. package/docs/design/2026-09-15-captured-dispatch.md +127 -0
  33. package/docs/design/2026-09-15-captured-resolution-records.md +143 -0
  34. package/docs/design/2026-09-15-package-preparation.md +100 -0
  35. package/docs/design/2026-09-15-portable-data-contract.md +121 -0
  36. package/docs/design/2026-09-15-portable-declarations.md +189 -0
  37. package/docs/design/2026-09-15-portable-souls-handoff.md +150 -0
  38. package/docs/design/2026-09-15-portable-souls-implementation.md +417 -0
  39. package/docs/design/2026-09-15-selection-lock-and-approval.md +122 -0
  40. package/docs/design/2026-09-15-source-observation.md +119 -0
  41. package/docs/design/2026-09-16-captured-admission.md +77 -0
  42. package/docs/design/2026-09-16-captured-helper-dispatch.md +105 -0
  43. package/docs/design/2026-09-16-captured-launch-inputs.md +42 -0
  44. package/docs/design/2026-09-16-command-profile-preparation.md +86 -0
  45. package/docs/design/2026-09-16-fresh-install-first-rollout.md +47 -0
  46. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +277 -0
  47. package/docs/design/2026-09-16-knowledge-capability-contract.md +61 -0
  48. package/docs/design/2026-09-16-messaging-capability-contract.md +59 -0
  49. package/docs/design/2026-09-16-portable-migration-evidence.md +156 -0
  50. package/docs/design/2026-09-16-portable-onboarding.md +177 -0
  51. package/docs/design/2026-09-16-prepare-request-transport.md +26 -0
  52. package/docs/design/2026-09-16-provider-binding-codecs.md +98 -0
  53. package/docs/design/2026-09-16-provider-binding-wire.md +247 -0
  54. package/docs/design/2026-09-17-capability-helper-input-contract.md +95 -0
  55. package/docs/design/2026-09-17-captured-backend-parity.md +53 -0
  56. package/docs/design/2026-09-17-captured-native-start.md +58 -0
  57. package/docs/design/2026-09-17-portable-boundary-hookup.md +19 -0
  58. package/docs/design/2026-09-17-portable-boundary-resources.md +52 -0
  59. package/docs/design/2026-09-17-public-captured-start.md +108 -0
  60. package/docs/design/2026-09-17-public-prepare-request.md +90 -0
  61. package/docs/design/2026-09-18-captured-pi-host.md +205 -0
  62. package/docs/design/2026-09-18-first-cut-release-checklist.md +131 -0
  63. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +60 -0
  64. package/docs/design/2026-09-20-redesign-program-board.md +70 -0
  65. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +287 -0
  66. package/docs/design/2026-09-20-workspace-onboarding-public.md +124 -0
  67. package/docs/design/README.md +42 -0
  68. package/docs/desktop-cli-api.md +5 -2
  69. package/docs/execution-capsule.schema.json +108 -0
  70. package/docs/execution-targets.md +20 -7
  71. package/docs/first-team.md +1 -1
  72. package/docs/knowledge-theory.md +353 -111
  73. package/docs/knowledge.md +10 -1
  74. package/docs/layers.md +89 -354
  75. package/docs/oats-lock-v3.schema.json +7 -0
  76. package/docs/oats-member.schema.json +38 -0
  77. package/docs/oats-workspace.schema.json +68 -0
  78. package/docs/official-marketplace.md +79 -0
  79. package/docs/packages.md +4 -0
  80. package/docs/portable.schema.json +2512 -0
  81. package/docs/provider-check-input.schema.json +7 -0
  82. package/docs/release-notes/v0.24.0.md +104 -0
  83. package/docs/release-notes/v0.24.1.md +17 -0
  84. package/docs/schedules.md +126 -14
  85. package/docs/soul.schema.json +82 -0
  86. package/docs/souls-and-instances.md +20 -7
  87. package/docs/workspace-adoption.md +285 -0
  88. package/docs/workspaces.md +154 -0
  89. package/injects/oats-portable.md +16 -0
  90. package/injects/portable-instance-boundary.md +39 -0
  91. package/injects/portable-work-directory.md +29 -0
  92. package/lib/artifact-approvals.mjs +120 -0
  93. package/lib/artifact-tree.mjs +141 -0
  94. package/lib/capability-artifacts.mjs +179 -0
  95. package/lib/capability-execution.mjs +15 -0
  96. package/lib/capability-inputs.mjs +39 -0
  97. package/lib/capability-provenance.mjs +231 -0
  98. package/lib/captured-action-shape.mjs +21 -0
  99. package/lib/captured-admission-shape.mjs +20 -0
  100. package/lib/captured-binding-file.mjs +36 -0
  101. package/lib/captured-dispatch.mjs +66 -0
  102. package/lib/captured-instance-index.mjs +277 -0
  103. package/lib/captured-invocation-context.mjs +130 -0
  104. package/lib/captured-launch-request.mjs +46 -0
  105. package/lib/captured-operation-process.mjs +15 -0
  106. package/lib/captured-pi-custody.mjs +29 -0
  107. package/lib/captured-pi-host.mjs +167 -0
  108. package/lib/captured-pi-outcome.mjs +172 -0
  109. package/lib/captured-resolutions.mjs +275 -0
  110. package/lib/captured-scaffold.mjs +87 -0
  111. package/lib/captured-selector.mjs +28 -0
  112. package/lib/captured-session-backend.mjs +52 -0
  113. package/lib/captured-source-receipt-file.mjs +72 -0
  114. package/lib/config-data.mjs +104 -0
  115. package/lib/core.mjs +961 -566
  116. package/lib/errors.mjs +7 -0
  117. package/lib/helper-injection-policy.mjs +98 -0
  118. package/lib/herdr.mjs +18 -7
  119. package/lib/instruction-composition.mjs +31 -0
  120. package/lib/legacy-lock-codec.mjs +106 -0
  121. package/lib/manifest-settings.mjs +84 -0
  122. package/lib/package-closure.mjs +48 -0
  123. package/lib/package-materialization.mjs +83 -0
  124. package/lib/pi-sdk-host.mjs +229 -0
  125. package/lib/portable-artifacts.mjs +115 -0
  126. package/lib/portable-choices.mjs +82 -0
  127. package/lib/portable-composition.mjs +136 -0
  128. package/lib/portable-digest.mjs +105 -0
  129. package/lib/portable-files.mjs +26 -0
  130. package/lib/portable-identity.mjs +40 -0
  131. package/lib/portable-lock.mjs +117 -0
  132. package/lib/portable-migration-artifacts.mjs +135 -0
  133. package/lib/portable-migration-evidence.mjs +305 -0
  134. package/lib/portable-migration-store.mjs +199 -0
  135. package/lib/portable-migration.mjs +104 -0
  136. package/lib/portable-onboarding-acceptance.mjs +66 -0
  137. package/lib/portable-onboarding-request.mjs +49 -0
  138. package/lib/portable-onboarding.mjs +249 -0
  139. package/lib/portable-package-preparation.mjs +188 -0
  140. package/lib/portable-policy.mjs +44 -0
  141. package/lib/portable-shape.mjs +35 -0
  142. package/lib/portable-soul.mjs +38 -0
  143. package/lib/portable-state.mjs +80 -0
  144. package/lib/portable-values.mjs +181 -0
  145. package/lib/prepare-composition.mjs +151 -0
  146. package/lib/prepared-bindings.mjs +78 -0
  147. package/lib/prepared-resources.mjs +127 -0
  148. package/lib/provider-binding-broker.mjs +59 -0
  149. package/lib/provider-binding-wire.mjs +110 -0
  150. package/lib/provider-binding.mjs +22 -0
  151. package/lib/repository-observation.mjs +226 -0
  152. package/lib/resolution-shape.mjs +393 -0
  153. package/lib/schedule-capsule.mjs +206 -0
  154. package/lib/schedule.mjs +259 -38
  155. package/lib/servers.mjs +15 -0
  156. package/lib/soul-constraints.mjs +40 -0
  157. package/lib/source-projection.mjs +84 -0
  158. package/lib/source-spec.mjs +189 -0
  159. package/lib/workspace-definition.mjs +126 -0
  160. package/lib/workspace-discovery.mjs +146 -0
  161. package/package-catalog.json +2 -1
  162. package/package.json +3 -2
  163. package/packages/record/lib/capture-cc.mjs +14 -6
  164. package/packages/record/lib/formats.mjs +14 -3
  165. package/packages/record/lib/native-history.mjs +277 -7
  166. package/packages/record/lib/session-snapshot.mjs +25 -5
  167. package/packages/record/lib/sessions-for-home.mjs +30 -13
  168. package/skills/oats/SKILL.md +12 -7
  169. package/skills/oats-config/SKILL.md +11 -9
  170. package/skills/oats-packages/SKILL.md +12 -8
  171. package/skills/oats-portable/SKILL.md +115 -0
  172. package/skills/oats-portable-artifacts/SKILL.md +63 -0
@@ -0,0 +1,127 @@
1
+ /** Build retained command/curriculum records from already selected software.
2
+ * Kernel callbacks own existing skill discovery and manifest command semantics. */
3
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
4
+ import { dirname, join, relative, sep } from "node:path";
5
+ import { copyTreeSafe } from "./artifact-tree.mjs";
6
+ import { treeIntegrity, jsonIntegrity } from "./portable-digest.mjs";
7
+ import { retainPortableArtifact, verifyPortableArtifact } from "./portable-artifacts.mjs";
8
+ import { commitCapturedResolution, verifyResolutionInputs } from "./captured-resolutions.mjs";
9
+ import { readPortableBytes } from "./portable-files.mjs";
10
+ import { captureHelperInjectionChoices } from "./helper-injection-policy.mjs";
11
+ import { parseConfigData } from "./config-data.mjs";
12
+ import { oatsError } from "./errors.mjs";
13
+ import { normalizePackagePath } from "./capability-provenance.mjs";
14
+ import { compileCapturedLaunchRequest } from "./captured-launch-request.mjs";
15
+
16
+ export function completePreparedResources({ seed, plan, manifests, mode, deployment, directory, launch, helperLaunches = {} }, kernel) {
17
+ const problems = [];
18
+ for (const [id, manifest] of manifests) {
19
+ if (!Object.hasOwn(seed.artifacts.capabilities, id)) continue;
20
+ if (manifest.layer && seed.bindings?.[manifest.layer]?.capability !== id) problems.push({ code: "provider-not-qualified", message: `provider ${id} needs its binding adapter before complete preparation`, origins: [] });
21
+ const settings = Object.fromEntries(Object.entries(plan.settings[id] ?? {}).map(([name, key]) => [name, plan.choices[key].value]));
22
+ kernel.settings(manifest, settings);
23
+ }
24
+ if (problems.length) return { record: null, problems };
25
+ const snapshot = join(directory, "kernel-resources"); mkdirSync(snapshot); mkdirSync(join(snapshot, "injects"));
26
+ const kernelSkills = ["oats-portable", "oats-portable-artifacts"];
27
+ const workInjection = (workMode) => workMode === "directory" ? "portable-work-directory.md" : `work-${workMode}.md`;
28
+ const injectionFiles = ["oats-portable.md", "portable-instance-boundary.md", ...kernel.workModes.map(workInjection)];
29
+ // Only known kernel resources, never a sweep of the checkout/node_modules,
30
+ // author directories, deployment config or credentials.
31
+ for (const name of kernelSkills) copyTreeSafe(join(kernel.root, "skills", name), join(snapshot, "skills", name));
32
+ for (const file of injectionFiles) copyTreeSafe(join(kernel.root, "injects", file), join(snapshot, "injects", file));
33
+ const bundle = { kind: "resource", integrity: treeIntegrity(snapshot) };
34
+ retainPortableArtifact(deployment, snapshot, bundle);
35
+ const portable = (root, path) => relative(root, path).split(sep).join("/") || ".";
36
+ const effectiveSettings = Object.fromEntries(Object.entries(plan.settings).map(([id, values]) => [id,
37
+ Object.fromEntries(Object.entries(values).map(([name, key]) => [name, plan.choices[key].value]))]));
38
+ const definitions = Object.values(seed.artifacts.capabilities).map(row => ({ artifact: row.artifact,
39
+ bytes: readPortableBytes(join(verifyPortableArtifact(deployment, row.artifact).dir, "oats.json")) }));
40
+ const build = (subject, workMode, helpers, launchRequest) => {
41
+ const helperPolicy = subject.kind === "helper" ? captureHelperInjectionChoices(plan, definitions) : null;
42
+ const resources = Object.create(null), blocks = [], skills = [], names = new Set(), omissions = [];
43
+ const owner = subject.kind === "persistent" ? subject.soul.sourceArtifact : subject.provider;
44
+ const sourceRoot = verifyPortableArtifact(deployment, owner).dir;
45
+ const definition = subject.kind === "persistent" ? subject.soul.definition : subject.definition.path;
46
+ const soulRoot = join(sourceRoot, dirname(definition));
47
+ resources.body = { owner, path: portable(sourceRoot, join(soulRoot, "AGENTS.md")), kind: "file" };
48
+ const inject = (source, owner, path) => {
49
+ const key = `instruction:${source}`; resources[key] = { owner, path, kind: "file" }; blocks.push({ source, resource: key });
50
+ };
51
+ inject("kernel:oats-portable", bundle, "injects/oats-portable.md");
52
+ inject("kernel:instance-boundary", bundle, "injects/portable-instance-boundary.md");
53
+ inject(`work-mode:${workMode}`, bundle, `injects/${workInjection(workMode)}`);
54
+ const skillTree = (owner, root, tree, label, required = true) => {
55
+ const entries = kernel.skills(tree);
56
+ if (required && !entries.length) throw oatsError("resource-not-found", "a declared skill tree contains no readable skill");
57
+ for (const entry of entries) {
58
+ if (names.has(entry.name)) throw oatsError("needs-configuration", `skill ${entry.name} has multiple sources; an explicit override is required`);
59
+ names.add(entry.name);
60
+ const key = `skill:${label}:${entry.name}`; resources[key] = { owner, path: portable(root, entry.src), kind: "skill" };
61
+ skills.push({ name: entry.name, resource: key });
62
+ }
63
+ };
64
+ const kernelRoot = verifyPortableArtifact(deployment, bundle).dir;
65
+ for (const name of kernelSkills) skillTree(bundle, kernelRoot, join(kernelRoot, "skills", name), "kernel");
66
+ if (existsSync(join(soulRoot, "skills"))) skillTree(owner, sourceRoot, join(soulRoot, "skills"), "soul", false);
67
+ const providerManifests = Object.create(null);
68
+ for (const [id, row] of Object.entries(seed.artifacts.capabilities)) {
69
+ const manifest = manifests.get(id), root = verifyPortableArtifact(deployment, row.artifact).dir;
70
+ const key = `manifest:${id}`; resources[key] = { owner: row.artifact, path: "oats.json", kind: "manifest" }; providerManifests[id] = key;
71
+ for (const path of manifest.skills ?? []) skillTree(row.artifact, root, join(root, path), id);
72
+ if (helperPolicy) {
73
+ const policy = helperPolicy.policies.get(id);
74
+ if (policy.fact) {
75
+ if (policy.resource) {
76
+ resources[policy.resourceKey] = policy.resource;
77
+ blocks.push({ source: policy.source, resource: policy.resourceKey, choice: policy.fact.key });
78
+ } else omissions.push({ source: policy.source, reason: "helper-policy", choice: policy.fact.key });
79
+ }
80
+ } else if (manifest.inject) inject(`capability:${id}`, row.artifact, portable(root, join(root, manifest.inject)));
81
+ for (const [kind, declarations] of [["command", manifest.commands ?? {}], ["hook", kernel.hooks(manifest)]]) {
82
+ for (const [name, value] of Object.entries(declarations)) {
83
+ if (typeof value !== "string" || !value.trim() || value.includes("\0")) throw oatsError("invalid-resolution", "declared executable is not valid text");
84
+ resources[`executable:${id}:${kind}:${name}`] = { owner: row.artifact, path: portable(root, join(root, value.trim().split(/\s+/)[0])), kind: "file" };
85
+ }
86
+ }
87
+ }
88
+ const capturedLaunch = compileCapturedLaunchRequest(launchRequest, { artifacts: seed.artifacts, manifests, settings: effectiveSettings, resources }, kernel);
89
+ return { ...seed, subject, choices: helperPolicy?.choices ?? seed.choices, bindings: seed.bindings ?? {}, messagingChoice: seed.messagingChoice ?? { schemaVersion: 1, enabled: false }, resources, resourceBundles: [bundle], helpers,
90
+ dispatch: { schemaVersion: 1, providerManifests, settingsChoices: plan.settings, launch: capturedLaunch, runtimePackages: [], hostRequirements: [], workTargetInputs: {},
91
+ composition: { schemaVersion: 1, mode: workMode, body: "body", blocks, skills, omissions } } };
92
+ };
93
+ // Preflight EVERY helper before publishing any helper record. Until helpers
94
+ // run their own complete provider/software preparation, inheriting a parent's
95
+ // binding over helper-authored policy would create a usable contradiction.
96
+ const helperPlans = [], helperKeys = new Set();
97
+ for (const [id, row] of Object.entries(seed.artifacts.capabilities)) {
98
+ const root = verifyPortableArtifact(deployment, row.artifact).dir;
99
+ for (const path of manifests.get(id).agents ?? []) {
100
+ const normalized = normalizePackagePath(path);
101
+ if (normalized === undefined) throw oatsError("invalid-declaration", "invalid helper export path");
102
+ const definitionPath = `${normalized === "." ? "" : `${normalized}/`}soul.yaml`;
103
+ const definition = parseConfigData(readFileSync(join(root, definitionPath))).value;
104
+ const authoredPolicy = ["requires", "defaults"].filter((field) => definition[field] && Object.keys(definition[field]).length);
105
+ for (const field of ["knowledge", "teams", "resources"]) if (Object.hasOwn(definition, field)) authoredPolicy.push(field);
106
+ if (authoredPolicy.length) return { record: null, problems: [{ code: "needs-configuration",
107
+ message: `helper has authored ${authoredPolicy.join(", ")} policy requiring dedicated preparation; parent bindings were not inherited`, origins: [] }] };
108
+ const helperMode = definition.work ?? "directory";
109
+ if (!kernel.workModes.includes(helperMode)) throw oatsError("invalid-declaration", "helper work mode is invalid");
110
+ const subject = { kind: "helper", provider: row.artifact, name: definition.name, definition: { owner: row.artifact, path: definitionPath, kind: "file" } };
111
+ const key = `${id}:${definition.name}`;
112
+ if (helperKeys.has(key)) throw oatsError("invalid-declaration", "duplicate helper export name");
113
+ helperKeys.add(key); helperPlans.push({ key, subject, helperMode });
114
+ }
115
+ }
116
+ for (const key of Object.keys(helperLaunches)) if (!helperKeys.has(key)) throw oatsError("helper-not-selected", "helper launch request must name an exact selected helper-map key");
117
+ // Compile EVERY launch request before publishing any helper record. No partial
118
+ // usable helper graph when a later helper's runtime inputs are unsupported.
119
+ const records = helperPlans.map(({ key, subject, helperMode }) => ({ key, record: build(subject, helperMode, {}, helperLaunches[key]) }));
120
+ const record = build(seed.subject, mode, {}, launch), helpers = Object.create(null);
121
+ // Verify every prospective file/owner/witness before publishing ANY member
122
+ // of this graph, including a primary file omitted only for helpers.
123
+ for (const candidate of [...records.map(item => item.record), record]) verifyResolutionInputs(deployment,
124
+ { schemaVersion: 1, id: jsonIntegrity(candidate).value }, { draft: candidate });
125
+ for (const item of records) helpers[item.key] = commitCapturedResolution(deployment, item.record);
126
+ return { record: { ...record, helpers }, problems: [] };
127
+ }
@@ -0,0 +1,59 @@
1
+ /** Execute only an approved retained provider command. Preparation does not need
2
+ * a fabricated complete resolution merely to obtain its binding. */
3
+ import { spawnSync } from 'node:child_process';
4
+ import { canonicalJson } from './portable-values.mjs';
5
+ import { objectAt } from './portable-shape.mjs';
6
+ import { validateArtifactSet } from './resolution-shape.mjs';
7
+ import { verifyPortableArtifact } from './portable-artifacts.mjs';
8
+ import { verifyRetainedCapability, verifyResolutionInputs } from './captured-resolutions.mjs';
9
+ import { buildCapturedInvocationContext } from './captured-invocation-context.mjs';
10
+ import { readApprovalLedger, evaluateCapturedApprovals } from './artifact-approvals.mjs';
11
+ import { validateBindingInterface } from './provider-binding.mjs';
12
+ import { BINDING_LIMITS, validateBindingRequest, decodeBindingResponse } from './provider-binding-wire.mjs';
13
+ import { oatsError } from './errors.mjs';
14
+
15
+ export function invokeProviderBinding(options,codecs) {
16
+ canonicalJson(options);
17
+ objectAt(options,['deployment','artifacts','capability','phase','settings','input','timeoutMs'],['deployment','artifacts','capability','phase','settings','input']);
18
+ const {deployment,artifacts,capability,phase,settings,input,timeoutMs=30000}=options;
19
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000) throw oatsError('invalid-declaration','binding timeout must be between 1 and 30000 milliseconds');
20
+ validateArtifactSet(artifacts);
21
+ if (!Object.hasOwn(artifacts.capabilities,capability)) throw oatsError('capability-not-selected','binding provider is not selected');
22
+ const row=artifacts.capabilities[capability],root=verifyPortableArtifact(deployment,row.artifact).dir;
23
+ const manifest=verifyRetainedCapability(root,artifacts,capability,codecs.manifest(root));
24
+ const binding=validateBindingInterface(manifest);
25
+ if (!binding) throw oatsError('provider-not-qualified','provider has no binding interface');
26
+ const request=validateBindingRequest({schemaVersion:1,phase,slot:manifest.layer,capability,settings,input});
27
+ if (phase === 'check' && input.invocation !== undefined) {
28
+ // Public check callers cannot turn a merely shape-valid projection into
29
+ // authority. Re-derive it from the referenced retained record/current target.
30
+ const verified=verifyResolutionInputs(deployment,input.invocation.executionBinding.resolution),record=verified.record;
31
+ if (record.capture !== 'prepared') throw oatsError('migration-required','captured check cannot use unverified reconstructed authority');
32
+ const actual=buildCapturedInvocationContext({loaded:{...verified,deployment,resolution:input.invocation.executionBinding.resolution,capability:{id:capability}},
33
+ action:input.action,instance:input.invocation.instance === null ? null : Object.fromEntries(['home','work','name','agent'].map(key=>[key,input.invocation.instance[key]])),
34
+ intent:input.invocation.intent,priorReceipt:input.invocation.priorReceipt});
35
+ const effective=Object.fromEntries(Object.entries(record.dispatch.settingsChoices[capability] ?? {}).map(([name,key])=>[name,record.choices[key].value]));
36
+ if (canonicalJson(actual)!==canonicalJson(input.invocation) || canonicalJson(record.artifacts)!==canonicalJson(artifacts)
37
+ || !record.bindings[manifest.layer] || canonicalJson(record.bindings[manifest.layer])!==canonicalJson(input.binding)
38
+ || canonicalJson(effective)!==canonicalJson(settings)) throw oatsError('invalid-resolution','provider check invocation differs from retained authority');
39
+ }
40
+ codecs.settings(manifest,settings);
41
+ const selected={record:{artifacts:{...artifacts,capabilities:{[capability]:row}}},manifests:new Map([[capability,manifest]])};
42
+ const approval=evaluateCapturedApprovals(selected,readApprovalLedger(deployment).ledger)[0];
43
+ if (approval.status !== 'approved') throw oatsError('approval-required','provider codec requires current exact-artifact approval');
44
+ if (codecs.host({manifest}).length) throw oatsError('host-requirement-missing','provider codec has unmet host requirements');
45
+ const spec=manifest.commands[binding[phase]];
46
+ const [script,...args]=spec.trim().split(/\s+/),file=codecs.executable(manifest,script);
47
+ if (!file) throw oatsError('resource-not-found','provider codec executable is unavailable');
48
+ const environment=Object.fromEntries(Object.entries(process.env).filter(([key])=>! /^(OATS_|OAS_|PI_)/.test(key)));
49
+ Object.assign(environment,{OATS_CAPABILITY:capability,OATS_CAPABILITY_ROOT:root,OATS_SETTINGS:canonicalJson(settings,BINDING_LIMITS)});
50
+ let result;
51
+ try {
52
+ result=spawnSync(process.execPath,[file,...args],{cwd:root,env:environment,input:canonicalJson(request,BINDING_LIMITS),
53
+ timeout:timeoutMs,killSignal:'SIGKILL',maxBuffer:BINDING_LIMITS.maxBytes,stdio:['pipe','pipe','pipe'],shell:false});
54
+ } catch { throw oatsError('provider-unavailable','provider codec could not execute'); }
55
+ if (result.error || result.signal || result.status !== 0) throw oatsError('provider-unavailable','provider codec did not complete successfully');
56
+ const response=decodeBindingResponse(result.stdout,request);
57
+ if (!response.ok) throw oatsError(response.error.code,'provider binding phase refused');
58
+ return response.result;
59
+ }
@@ -0,0 +1,110 @@
1
+ /** Bounded provider wire data. No provider model, precedence or native execution. */
2
+ import { canonicalJson, parseStrictJson } from './portable-values.mjs';
3
+ import { objectAt, stringAt, versionAt } from './portable-shape.mjs';
4
+ import { validateOrigin, validateProviderBinding, validateMessagingChoice, validateCapturedChoices, validateCapturedContext } from './resolution-shape.mjs';
5
+ import { validateCapturedInvocationContext } from './captured-invocation-context.mjs';
6
+ import { FUNDAMENTAL_SLOTS } from './portable-policy.mjs';
7
+ import { isMaterializedCapabilityId } from './capability-provenance.mjs';
8
+ import { BINDING_PHASES } from './provider-binding.mjs';
9
+ import { resolveChoices } from './portable-choices.mjs';
10
+ import { oatsError } from './errors.mjs';
11
+
12
+ export const BINDING_LIMITS = Object.freeze({maxBytes:1024*1024,maxDepth:32,maxEntries:16384});
13
+ export const BINDING_PROBLEMS = Object.freeze(['needs-configuration','requirement-conflict','invalid-binding','authorization-required','host-requirement-missing','provider-unavailable','provider-not-qualified']);
14
+ const fail = () => { throw oatsError('invalid-binding-output','provider returned invalid binding data'); };
15
+ const same = (a,b) => canonicalJson(a) === canonicalJson(b);
16
+ const roleKinds = {soul:['soul-requirement','soul-default'],workspace:['workspace-default'],adoption:['import-adoption'],operator:['operator']};
17
+ const pointer = key => typeof key === 'string' && /^(?:\/(?:[^~]|~[01])*)+$/.test(key);
18
+ export const bindingField = (key,slot) => pointer(key) && key.startsWith(`/bindings/${slot}/`) && key.length > `/bindings/${slot}/`.length;
19
+ function problem(value) {
20
+ objectAt(value,['code','message'],['code']);
21
+ if (!BINDING_PROBLEMS.includes(value.code)) fail();
22
+ if (value.message !== undefined) stringAt(value.message,'/message',{empty:true});
23
+ return {code:value.code}; // Free text never leaves the provider boundary.
24
+ }
25
+ export function validateBindingRequest(request) {
26
+ canonicalJson(request,BINDING_LIMITS);
27
+ objectAt(request,['schemaVersion','phase','slot','capability','settings','input'],['schemaVersion','phase','slot','capability','settings','input']);
28
+ versionAt(request.schemaVersion);
29
+ if (!BINDING_PHASES.includes(request.phase) || !FUNDAMENTAL_SLOTS.includes(request.slot) || !isMaterializedCapabilityId(request.capability)) fail();
30
+ objectAt(request.settings,null,[]); const input=request.input;
31
+ if (request.phase === 'normalize') {
32
+ objectAt(input,['declarations','context'],['declarations','context']);
33
+ if (!Array.isArray(input.declarations)) fail();
34
+ for (const declaration of input.declarations) {
35
+ objectAt(declaration,['kind','value','origin','origins'],['kind','value','origin','origins']);
36
+ if (!Object.hasOwn(roleKinds,declaration.kind)) fail();
37
+ validateOrigin(declaration.origin); objectAt(declaration.origins,null,[]);
38
+ if (!roleKinds[declaration.kind].includes(declaration.origin.kind)) fail();
39
+ for (const [key,origin] of Object.entries(declaration.origins)) {
40
+ if (key !== '' && !pointer(key)) fail();
41
+ validateOrigin(origin);
42
+ if (!roleKinds[declaration.kind].includes(origin.kind) || !same(origin.document,declaration.origin.document)) fail();
43
+ }
44
+ }
45
+ } else if (request.phase === 'bind') {
46
+ objectAt(input,['model','choices','context'],['model','choices','context']);
47
+ validateCapturedChoices(input.choices);
48
+ for (const key of Object.keys(input.choices)) if (!bindingField(key,request.slot)) fail();
49
+ } else {
50
+ objectAt(input,['binding','context','action','invocation'],['binding','context','action']);
51
+ validateProviderBinding(input.binding); objectAt(input.action,null,['kind']);
52
+ if (input.binding.capability !== request.capability) fail();
53
+ if (Object.hasOwn(input,'invocation')) {
54
+ const invocation=validateCapturedInvocationContext(input.invocation);
55
+ if (invocation.capability !== request.capability || !same(invocation.context,input.context) || !same(invocation.action,input.action)) fail();
56
+ if (request.slot === 'messaging' && (!invocation.messagingChoice.enabled || invocation.messagingChoice.privateKey.provider !== request.capability)) fail();
57
+ }
58
+ }
59
+ validateCapturedContext(input.context);
60
+ return request;
61
+ }
62
+ function witnessed(origin,declarations) {
63
+ validateOrigin(origin);
64
+ return declarations.some(declaration => roleKinds[declaration.kind].includes(origin.kind)
65
+ && [declaration.origin,...Object.values(declaration.origins)].some(candidate => {
66
+ const {kind:_,...a}=origin, {kind:__,...b}=candidate;
67
+ return same(a,b);
68
+ }));
69
+ }
70
+ export function decodeBindingResponse(bytes,request) {
71
+ let response;
72
+ try {
73
+ validateBindingRequest(request);
74
+ response=parseStrictJson(bytes,BINDING_LIMITS);
75
+ objectAt(response,response?.ok === true ? ['schemaVersion','phase','slot','capability','ok','result'] : ['schemaVersion','phase','slot','capability','ok','error'],
76
+ response?.ok === true ? ['schemaVersion','phase','slot','capability','ok','result'] : ['schemaVersion','phase','slot','capability','ok','error']);
77
+ for (const key of ['schemaVersion','phase','slot','capability']) if (response[key] !== request[key]) fail();
78
+ if (response.ok === false) return {ok:false,error:problem(response.error)};
79
+ if (response.ok !== true) fail();
80
+ const result=response.result;
81
+ if (request.phase === 'normalize') {
82
+ objectAt(result,['requirements','candidates','model'],['requirements','candidates','model']);
83
+ if (!Array.isArray(result.requirements) || !Array.isArray(result.candidates)) fail();
84
+ for (const entry of [...result.requirements,...result.candidates]) {
85
+ if (!bindingField(entry.key,request.slot) || !witnessed(entry.origin,request.input.declarations)) fail();
86
+ }
87
+ for (const requirement of result.requirements) if (requirement.origin.kind !== 'soul-requirement') fail();
88
+ for (const candidate of result.candidates) if (candidate.kind !== candidate.origin.kind) fail();
89
+ // Reuse the sole entry codec/solver; conflicts are data for the combined plan.
90
+ resolveChoices(result);
91
+ return {ok:true,result};
92
+ }
93
+ if (request.phase === 'bind') {
94
+ const keys=['payloadContract','payloadVersion','payload','credentialRefs','provenance'];
95
+ objectAt(result,request.slot === 'messaging' ? [...keys,'messagingChoice'] : keys,request.slot === 'messaging' ? [...keys,'messagingChoice'] : keys);
96
+ const {messagingChoice,...fields}=result;
97
+ const binding=validateProviderBinding({schemaVersion:1,capability:request.capability,...fields});
98
+ // Source read/ownership provenance may live in the opaque normalized model;
99
+ // preparation validates that additional provenance against its original input.
100
+ if (request.slot === 'messaging') validateMessagingChoice(messagingChoice,{bindings:{messaging:binding},context:request.input.context});
101
+ return {ok:true,result:{binding,...(request.slot === 'messaging' ? {messagingChoice} : {})}};
102
+ }
103
+ objectAt(result,['status','problems'],['status','problems']);
104
+ if (!['ready','needs-configuration','authorization-required','unavailable'].includes(result.status) || !Array.isArray(result.problems)) fail();
105
+ const problems=result.problems.map(problem);
106
+ if (result.status === 'ready' && problems.length) fail();
107
+ return {ok:true,result:{status:result.status,problems}};
108
+ } catch { fail(); }
109
+ }
110
+ export function bindingOriginWitnessed(origin,declarations) { return witnessed(origin,declarations); }
@@ -0,0 +1,22 @@
1
+ /** Provider-owned binding protocol. This codec declares no provider model and
2
+ * grants no execution authority; commands remain in the sole manifest table. */
3
+ import { objectAt, stringAt, versionAt } from './portable-shape.mjs';
4
+ import { FUNDAMENTAL_SLOTS } from './portable-policy.mjs';
5
+ import { oatsError } from './errors.mjs';
6
+
7
+ export const BINDING_PHASES = Object.freeze(['normalize', 'bind', 'check']);
8
+ export function validateBindingInterface(manifest) {
9
+ if (manifest.binding === undefined) return null;
10
+ if (!FUNDAMENTAL_SLOTS.includes(manifest.layer)) throw oatsError('invalid-binding-interface', 'binding codecs belong to a fundamental provider slot');
11
+ const binding = manifest.binding;
12
+ objectAt(binding, ['version', ...BINDING_PHASES], ['version', ...BINDING_PHASES], '/binding');
13
+ versionAt(binding.version);
14
+ for (const phase of BINDING_PHASES) {
15
+ const name = binding[phase];
16
+ stringAt(name, `/binding/${phase}`, { pattern: /^[a-z0-9][a-z0-9-]*$/ });
17
+ if (!manifest.commands || !Object.hasOwn(manifest.commands, name) || typeof manifest.commands[name] !== 'string' || !manifest.commands[name].trim()) {
18
+ throw oatsError('invalid-binding-interface', 'binding phase must reference a command owned by this manifest');
19
+ }
20
+ }
21
+ return binding;
22
+ }
@@ -0,0 +1,226 @@
1
+ /** Exact, transaction-scoped repository observations. Bare Git reads never check
2
+ * out or execute source content. Cache lifetime/auth context is one transaction. */
3
+ import { execFileSync } from "node:child_process";
4
+ import { lstatSync, mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
5
+ import { isAbsolute, join } from "node:path";
6
+ import { canonicalJson, decodeUtf8, freezeJson, parseStrictJson } from "./portable-values.mjs";
7
+ import { bytesIntegrity } from "./portable-digest.mjs";
8
+ import { parseRepositorySource, portablePath, revisionSelector } from "./source-spec.mjs";
9
+ import { sameIdentity, validateRepositoryIdentity } from "./portable-identity.mjs";
10
+ import { validateOrigin } from "./resolution-shape.mjs";
11
+ import { createSourceProjection, validateProjectionPath } from "./source-projection.mjs";
12
+ export { validateProjectionPath } from "./source-projection.mjs";
13
+ import { oatsError } from "./errors.mjs";
14
+
15
+ function githubCoordinates(url) {
16
+ let host, path;
17
+ if (url.includes("://")) { const parsed = new URL(url); host = parsed.hostname; path = parsed.pathname.slice(1); }
18
+ else { const match = /^[^@]+@([^:]+):(.+)$/.exec(url); if (!match) return null; [, host, path] = match; }
19
+ if (host.toLowerCase() !== "github.com") return null;
20
+ const parts = path.replace(/\.git$/, "").split("/");
21
+ if (parts.length !== 2 || parts.some((part) => !/^[A-Za-z0-9_.-]+$/.test(part))) throw oatsError("source-identity-unresolved", "GitHub repository locator cannot be qualified");
22
+ return { host: "github.com", owner: parts[0], name: parts[1] };
23
+ }
24
+
25
+ /** Native host credentials only; no token extraction, copying or fallback after
26
+ * a GitHub identity lookup fails. Unsupported hosts use explicit remote identity. */
27
+ export function readHostingIdentity(repository, { environment = process.env, expectedIdentity } = {}) {
28
+ if (expectedIdentity?.kind === "canonical-remote") return { identity: { kind: "canonical-remote", remote: repository.normalized } };
29
+ const coordinates = githubCoordinates(repository.url);
30
+ if (!coordinates) return { identity: { kind: "canonical-remote", remote: repository.normalized } };
31
+ let metadata;
32
+ try {
33
+ const output = execFileSync("gh", ["api", "--hostname", coordinates.host, `repos/${coordinates.owner}/${coordinates.name}`,
34
+ "--jq", "{id, full_name, default_branch}"], { env: environment, timeout: 30_000, maxBuffer: 64 * 1024, stdio: ["ignore", "pipe", "pipe"] });
35
+ metadata = parseStrictJson(output);
36
+ } catch { throw oatsError("source-identity-unresolved", "repository hosting identity is unavailable in the selected access context"); }
37
+ if (!Number.isSafeInteger(metadata.id) || metadata.id < 1 || typeof metadata.full_name !== "string") throw oatsError("source-identity-unresolved", "hosting response lacks a stable repository identity");
38
+ if (metadata.full_name.toLowerCase() !== `${coordinates.owner}/${coordinates.name}`.toLowerCase()) throw oatsError("source-identity-change", "repository redirect or rename requires an explicit identity mapping");
39
+ revisionSelector(metadata.default_branch);
40
+ return { identity: { kind: "provider-repository", provider: "github", host: coordinates.host, id: String(metadata.id) }, defaultBranch: metadata.default_branch };
41
+ }
42
+
43
+ function gitEnvironment(environment) {
44
+ const out = { ...environment };
45
+ // Keep native credential/SSH facilities, but never an invoking repository's
46
+ // object/index/config injection. Host HOME/config credentials remain host-owned.
47
+ const retained = new Set(["GIT_ASKPASS", "GIT_SSH", "GIT_SSH_COMMAND", "GIT_SSH_VARIANT", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_CONFIG_NOSYSTEM"]);
48
+ for (const key of Object.keys(out)) if (key.startsWith("GIT_") && !retained.has(key)) delete out[key];
49
+ out.GIT_TERMINAL_PROMPT = "0"; out.GIT_LITERAL_PATHSPECS = "1"; out.GIT_NO_REPLACE_OBJECTS = "1";
50
+ return out;
51
+ }
52
+
53
+ export function createRepositoryTransaction({ directory, accessContextKey, environment = process.env, identityReader = readHostingIdentity, allowLocalGit = false,
54
+ maxRepositories = 32, maxEntries = 10_000, maxBytes = 256 * 1024 * 1024 } = {}) {
55
+ if (typeof directory !== "string" || !isAbsolute(directory) || typeof accessContextKey !== "string" || !accessContextKey) throw oatsError("invalid-source", "repository observation requires an explicit scratch directory and access context");
56
+ canonicalJson(accessContextKey, { maxBytes: 8192 });
57
+ for (const value of [maxRepositories, maxEntries, maxBytes]) if (!Number.isSafeInteger(value) || value < 1) throw oatsError("resource-limit", "invalid repository observation budget");
58
+ const parent = realpathSync(directory);
59
+ if (!lstatSync(parent).isDirectory()) throw oatsError("invalid-source", "repository observation scratch root is not a directory");
60
+ const root = mkdtempSync(join(parent, ".repository-transaction-")), owned = lstatSync(root), hooks = join(root, "empty-hooks");
61
+ mkdirSync(hooks, { mode: 0o700 });
62
+ const env = gitEnvironment(environment), memo = new Map(), snapshots = new Map(), hosts = new Map(), defaults = new Map(), handles = new WeakMap(), blobs = new Map();
63
+ let closed = false, readBytes = 0, entriesRead = 0;
64
+ const active = () => { if (closed) throw oatsError("source-unavailable", "repository transaction is closed"); };
65
+ const git = (args, limit = 1024 * 1024) => {
66
+ active();
67
+ try { return execFileSync("git", ["-c", `core.hooksPath=${hooks}`, "-c", "protocol.ext.allow=never", "-c", `protocol.file.allow=${allowLocalGit ? "always" : "never"}`,
68
+ "-c", "http.followRedirects=false", ...args], { cwd: root, env, timeout: 60_000, maxBuffer: limit, stdio: ["ignore", "pipe", "pipe"] }); }
69
+ catch (error) { throw oatsError(error.code === "ENOBUFS" ? "resource-limit" : "source-unavailable", "repository read failed in the selected access context"); }
70
+ };
71
+ const hostFor = (repository, expectedIdentity) => {
72
+ if (!hosts.has(repository.normalized)) {
73
+ if (hosts.size >= maxRepositories * 4) throw oatsError("resource-limit", "repository identity count exceeded");
74
+ try {
75
+ const response = identityReader(repository, { environment: env, expectedIdentity });
76
+ validateRepositoryIdentity(response.identity);
77
+ if (response.defaultBranch !== undefined) revisionSelector(response.defaultBranch);
78
+ hosts.set(repository.normalized, { value: freezeJson({ identity: structuredClone(response.identity),
79
+ ...(response.defaultBranch === undefined ? {} : { defaultBranch: response.defaultBranch }) }) });
80
+ } catch (error) { hosts.set(repository.normalized, { error }); }
81
+ }
82
+ const hosted = hosts.get(repository.normalized);
83
+ if (hosted.error) throw hosted.error;
84
+ const host = hosted.value;
85
+ if (host.identity.kind === "canonical-remote" && host.identity.remote !== repository.normalized) throw oatsError("source-identity-change", "canonical repository identity differs from its source");
86
+ if (expectedIdentity && !sameIdentity(host.identity, expectedIdentity)) throw oatsError("source-identity-change", "repository hosting identity changed");
87
+ const identityKey = canonicalJson(host.identity);
88
+ if (host.defaultBranch !== undefined && !defaults.has(identityKey)) defaults.set(identityKey, { selector: host.defaultBranch });
89
+ return host;
90
+ };
91
+ const dataFor = (observation) => {
92
+ active(); const data = handles.get(observation);
93
+ if (!data) throw oatsError("invalid-source", "repository observation was not issued by this transaction");
94
+ return data;
95
+ };
96
+ const list = (data, paths, recursive) => {
97
+ const args = ["-C", data.directory, "ls-tree", "-z", "--full-tree", ...(recursive ? ["-r"] : []), data.commit];
98
+ if (!paths.includes(".")) args.push("--", ...paths);
99
+ const rows = decodeUtf8(git(args, 16 * 1024 * 1024)).split("\0").filter(Boolean);
100
+ if ((entriesRead += rows.length) > maxEntries) throw oatsError("resource-limit", "repository entry budget exceeded");
101
+ return rows.map((row) => {
102
+ const at = row.indexOf("\t"), header = /^([0-7]{6}) (blob|tree|commit) ([a-f0-9]{40})$/.exec(row.slice(0, at)), path = row.slice(at + 1);
103
+ if (!header || at < 0) throw oatsError("invalid-source", "repository returned a malformed tree entry");
104
+ portablePath(path);
105
+ if (path.split("/").some((part) => part.toLowerCase() === ".git")) throw oatsError("resource-not-contained", "source projection cannot include Git administrative files");
106
+ if (!paths.some((root) => root === "." || path === root || path.startsWith(`${root}/`))) throw oatsError("resource-not-contained", "repository returned an unselected source path");
107
+ return { mode: header[1], type: header[2], object: header[3], path };
108
+ });
109
+ };
110
+ const blob = (data, entry, limit) => {
111
+ if (entry.type !== "blob") throw oatsError("source-incomplete", "selected source contains an unsupported submodule or object kind");
112
+ const key = `${data.directory}:${entry.object}`;
113
+ if (!blobs.has(key)) {
114
+ const sizeText = decodeUtf8(git(["-C", data.directory, "cat-file", "-s", entry.object], 1024)).trim();
115
+ const size = Number(sizeText);
116
+ if (!/^[0-9]+$/.test(sizeText) || !Number.isSafeInteger(size) || size > limit || readBytes + size > maxBytes) throw oatsError("resource-limit", "repository content byte budget exceeded");
117
+ const bytes = git(["-C", data.directory, "cat-file", "blob", entry.object], size + 1024);
118
+ if (bytes.length !== size) throw oatsError("integrity-drift", "repository blob changed during reading");
119
+ readBytes += size; blobs.set(key, bytes);
120
+ }
121
+ const bytes = blobs.get(key);
122
+ if (bytes.length > limit) throw oatsError("resource-limit", "repository descriptor byte limit exceeded");
123
+ return Buffer.from(bytes);
124
+ };
125
+
126
+ return {
127
+ identify(source, { expectedIdentity } = {}) {
128
+ active();
129
+ if (expectedIdentity !== undefined) validateRepositoryIdentity(expectedIdentity);
130
+ const repository = parseRepositorySource(source), host = hostFor(repository, expectedIdentity);
131
+ return Object.freeze({ identity: host.identity, source: repository.normalized, accessContextKey });
132
+ },
133
+ observe(source, { revision, origin, expectedIdentity } = {}) {
134
+ active(); validateOrigin(origin);
135
+ if (expectedIdentity !== undefined) validateRepositoryIdentity(expectedIdentity);
136
+ // Local working snapshots use path: custody, not portable file: identities.
137
+ // allowLocalGit only enables an explicit host/test transport mapping.
138
+ const repository = parseRepositorySource(source);
139
+ if (revision !== undefined) revisionSelector(revision);
140
+ const key = canonicalJson({ source: repository.normalized, selector: revision ?? null });
141
+ if (!memo.has(key)) {
142
+ if (memo.size >= maxRepositories * 4) throw oatsError("resource-limit", "repository request count exceeded");
143
+ // Cache failures too: a single transaction never re-observes a moving
144
+ // request because its first attempt was inconvenient or inaccessible.
145
+ const slot = {}; memo.set(key, slot);
146
+ try {
147
+ const host = hostFor(repository, expectedIdentity), identityKey = canonicalJson(host.identity);
148
+ let selector = revision ?? defaults.get(identityKey)?.selector, exactHead = revision === undefined ? defaults.get(identityKey)?.exactHead : undefined;
149
+ if (selector === undefined) {
150
+ const head = decodeUtf8(git(["ls-remote", "--symref", "--", repository.url, "HEAD"], 64 * 1024));
151
+ const symbolic = /^ref: refs\/heads\/(.+)\tHEAD$/m.exec(head), resolved = /^([a-f0-9]{40})\tHEAD$/m.exec(head);
152
+ if (!symbolic || !resolved) throw oatsError("source-identity-unresolved", "repository default branch is unavailable");
153
+ selector = symbolic[1]; exactHead = resolved[1];
154
+ defaults.set(identityKey, { selector, exactHead });
155
+ }
156
+ revisionSelector(selector);
157
+ const snapshotKey = canonicalJson({ identity: host.identity, selector });
158
+ if (snapshots.has(snapshotKey)) {
159
+ const existing = snapshots.get(snapshotKey);
160
+ if (existing.error) throw existing.error;
161
+ slot.data = existing.data;
162
+ } else {
163
+ if (snapshots.size >= maxRepositories) throw oatsError("resource-limit", "repository snapshot count exceeded");
164
+ const directory = join(root, `repository-${snapshots.size}.git`);
165
+ snapshots.set(snapshotKey, slot);
166
+ // One selected shallow snapshot, not all repository branches/history.
167
+ // Servers may decline filtering; budgets below bound selected reads,
168
+ // not the transport's pack size. Git/host resource limits still apply.
169
+ git(["init", "--quiet", "--bare", directory]);
170
+ git(["-C", directory, "remote", "add", "origin", repository.url]);
171
+ git(["-C", directory, "fetch", "--quiet", "--depth=1", "--filter=blob:none", "--no-tags", "--", "origin", exactHead ?? selector], 4 * 1024 * 1024);
172
+ const commit = decodeUtf8(git(["-C", directory, "rev-parse", "--verify", "--quiet", "--end-of-options", "FETCH_HEAD^{commit}"], 1024)).trim();
173
+ if (exactHead && commit !== exactHead) throw oatsError("integrity-drift", "fetched default branch differs from its exact observation");
174
+ if (!/^[a-f0-9]{40}$/.test(commit)) throw oatsError("source-identity-unresolved", "repository selector did not resolve to an exact commit");
175
+ slot.data = { directory, identity: Object.freeze({ ...host.identity }), selector, commit };
176
+ }
177
+ } catch (error) { slot.error = error; throw error; }
178
+ }
179
+ const slot = memo.get(key);
180
+ if (slot.error) throw slot.error;
181
+ const data = { ...slot.data, remote: repository.normalized };
182
+ if (expectedIdentity && !sameIdentity(data.identity, expectedIdentity)) throw oatsError("source-identity-change", "cached repository identity differs from the request");
183
+ const observation = Object.freeze({ schemaVersion: 1, source: Object.freeze({ identity: data.identity, remote: data.remote,
184
+ selector: data.selector, commit: data.commit, provenance: freezeJson([structuredClone(origin)]) }), accessContextKey });
185
+ handles.set(observation, data);
186
+ return observation;
187
+ },
188
+ readFile(observation, path, { optional = false } = {}) {
189
+ portablePath(path); const data = dataFor(observation), rows = list(data, [path], false);
190
+ if (!rows.length && optional) return null;
191
+ if (rows.length !== 1 || rows[0].path !== path || !["100644", "100755"].includes(rows[0].mode)) throw oatsError("export-not-found", "repository descriptor is absent or is not a regular file");
192
+ const bytes = blob(data, rows[0], 1024 * 1024);
193
+ return { bytes, origin: { kind: "source", source: data.remote, revision: data.commit, path, integrity: bytesIntegrity(bytes) } };
194
+ },
195
+ materialize(observation, roots, destination) {
196
+ const data = dataFor(observation);
197
+ canonicalJson(roots);
198
+ if (!Array.isArray(roots) || !roots.length || roots.length > 1024 || new Set(roots).size !== roots.length) throw oatsError("invalid-source", "source projection needs a bounded unique root set");
199
+ roots.forEach((path) => portablePath(path, { allowRoot: true }));
200
+ if (typeof destination !== "string" || !isAbsolute(destination)) throw oatsError("invalid-source", "source projection destination must be explicit");
201
+ const rows = list(data, roots, true);
202
+ for (const path of roots) if (!rows.some((row) => path === "." || row.path === path || row.path.startsWith(`${path}/`))) throw oatsError("export-not-found", "required source projection root is absent");
203
+ for (const entry of rows) validateProjectionPath(entry.path);
204
+ const expected = new Map(rows.map((entry) => [entry.path, entry]));
205
+ if (expected.size !== rows.length) throw oatsError("invalid-source", "source tree has duplicate paths");
206
+ const projection = createSourceProjection(destination);
207
+ try {
208
+ for (const entry of rows) {
209
+ const bytes = blob(data, entry, maxBytes);
210
+ if (entry.mode === "120000") projection.link(entry.path, decodeUtf8(bytes, "source link target"));
211
+ else if (["100644", "100755"].includes(entry.mode)) projection.file(entry.path, bytes, entry.mode === "100755" ? 0o755 : 0o644);
212
+ else throw oatsError("source-incomplete", "source object mode cannot be retained");
213
+ }
214
+ projection.finish();
215
+ return { directory: destination, roots: [...roots], observation };
216
+ } catch (error) { error.stagingPath = destination; throw error; }
217
+ },
218
+ close() {
219
+ if (closed) return;
220
+ const current = lstatSync(root);
221
+ if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError("source-unavailable", "repository transaction cleanup lost ownership");
222
+ closed = true;
223
+ rmSync(root, { recursive: true });
224
+ },
225
+ };
226
+ }