@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,111 @@
1
+ // Execution-only consumer of the kernel-owned generic snapshot. This module
2
+ // never reads a kernel index, mints identity, discovers source/config, or grants
3
+ // native authority from structural validation alone. Check remains stdin-only.
4
+ import fs from 'node:fs';
5
+ import { dirname, isAbsolute, resolve } from 'node:path';
6
+ import { BINDING_WIRE_LIMITS, parseBindingJson, sourceRuntimeFromKnowledgeBinding } from './binding-wire.mjs';
7
+ import { INVOCATION_LIMITS, validateInvocationShape, sameInvocationJson } from './invocation-shape.mjs';
8
+ import { validateCapturedReceipt } from './sources.mjs';
9
+
10
+ const CAPABILITY='oats.okf';
11
+ export const INVOCATION_CONTEXT_LIMITS=INVOCATION_LIMITS;
12
+ const SOURCE_RECEIPT_LIMITS=Object.freeze({bytes:256*1024,depth:32,entries:16384});
13
+ const invalid=()=>{throw Object.assign(new Error('invalid captured OKF invocation context'),{code:'E_INVOCATION'});};
14
+ function absolute(value) {return typeof value==='string' && !value.includes('\0') && isAbsolute(value) && resolve(value)===value;}
15
+ function noLinks(file) {
16
+ for(let path=file;;path=dirname(path)) {
17
+ const stat=fs.lstatSync(path);
18
+ if(stat.isSymbolicLink() || (path!==file && !stat.isDirectory())) invalid();
19
+ if(dirname(path)===path) return;
20
+ }
21
+ }
22
+ function readPrivateJson(file,limits) {
23
+ if(!absolute(file)) invalid();let fd;
24
+ try {
25
+ noLinks(file);
26
+ const uid=typeof process.getuid==='function'?process.getuid():null;
27
+ const privateFile=stat=>stat.isFile() && stat.nlink===1n && stat.size<=BigInt(limits.bytes) && (stat.mode&0o7777n)===0o600n && (uid===null || stat.uid===BigInt(uid));
28
+ const before=fs.lstatSync(file,{bigint:true});
29
+ const same=stat=>privateFile(stat) && ['dev','ino','size','mtimeNs','ctimeNs'].every(key=>stat[key]===before[key]);
30
+ if(!privateFile(before) || typeof fs.constants.O_NOFOLLOW!=='number' || typeof fs.constants.O_NONBLOCK!=='number') invalid();
31
+ fd=fs.openSync(file,fs.constants.O_RDONLY|fs.constants.O_NOFOLLOW|fs.constants.O_NONBLOCK);
32
+ noLinks(file);
33
+ if(!same(fs.fstatSync(fd,{bigint:true})) || !same(fs.lstatSync(file,{bigint:true}))) invalid();
34
+ const bytes=Buffer.alloc(Number(before.size));
35
+ for(let offset=0;offset<bytes.length;) {
36
+ const count=fs.readSync(fd,bytes,offset,bytes.length-offset,offset);
37
+ if(!count) invalid();offset+=count;
38
+ }
39
+ noLinks(file);
40
+ if(!same(fs.fstatSync(fd,{bigint:true})) || !same(fs.lstatSync(file,{bigint:true}))) invalid();
41
+ return parseBindingJson(bytes,limits);
42
+ } catch {invalid();}
43
+ finally {if(fd!==undefined) fs.closeSync(fd);}
44
+ }
45
+ export function validateOkfInvocationContext(value,binding) {
46
+ try {
47
+ sourceRuntimeFromKnowledgeBinding(binding);
48
+ return validateInvocationShape(value,{capability:CAPABILITY});
49
+ } catch {invalid();}
50
+ }
51
+ export function loadCapturedOkfInvocation(env=process.env) {
52
+ const hasContext=Object.hasOwn(env,'OATS_INVOCATION_CONTEXT_FILE'),hasBinding=Object.hasOwn(env,'OATS_BINDING_FILE');
53
+ if(!hasContext && !hasBinding && !Object.hasOwn(env,'OATS_SOURCE_RECEIPT_FILE')) return {kind:'legacy'};
54
+ if(!hasContext || !hasBinding || env.OATS_INVOCATION_CONTEXT_FILE===env.OATS_BINDING_FILE) invalid();
55
+ if(Object.hasOwn(env,'OATS_SOURCE_RECEIPT_FILE') && [env.OATS_INVOCATION_CONTEXT_FILE,env.OATS_BINDING_FILE].includes(env.OATS_SOURCE_RECEIPT_FILE)) invalid();
56
+ const binding=readPrivateJson(env.OATS_BINDING_FILE,BINDING_WIRE_LIMITS);
57
+ const context=validateOkfInvocationContext(readPrivateJson(env.OATS_INVOCATION_CONTEXT_FILE,INVOCATION_CONTEXT_LIMITS),binding);
58
+ return {kind:'captured',file:env.OATS_INVOCATION_CONTEXT_FILE,bindingFile:env.OATS_BINDING_FILE,binding,context};
59
+ }
60
+ export function loadOkfSourceReceiptInput(loaded,env=process.env) {
61
+ if(!Object.hasOwn(env,'OATS_SOURCE_RECEIPT_FILE')) return {mode:'absent'};
62
+ if([loaded.file,loaded.bindingFile].includes(env.OATS_SOURCE_RECEIPT_FILE)) invalid();
63
+ const receipt=readPrivateJson(env.OATS_SOURCE_RECEIPT_FILE,SOURCE_RECEIPT_LIMITS);
64
+ assertOkfSourceReceiptContext(receipt,loaded.context,loaded.binding);
65
+ return {mode:'captured',receipt};
66
+ }
67
+ // Explicit compatibility for a source already validated/loaded from durable
68
+ // storage. This cannot construct a source or supply generic admission authority.
69
+ export function assertOkfRegisteredSourceReplay(source,binding) {
70
+ try {sourceRuntimeFromKnowledgeBinding(binding);} catch {invalid();}
71
+ if(!source?.file || source.registration?.schemaVersion!==1 || source.registration.kind!=='captured' || !sameInvocationJson(source.providerBinding,binding)) invalid();
72
+ }
73
+ export function assertOkfInvocationAction(context,event,manifest) {
74
+ try {validateInvocationShape(context,{capability:CAPABILITY});} catch {invalid();}
75
+ const action=context.action;
76
+ if(manifest?.capability!==CAPABILITY) invalid();
77
+ if(action.kind==='hook') {
78
+ if(action.capability!==CAPABILITY || action.name!==event || !Object.hasOwn(manifest.hooks||{},event)) invalid();
79
+ } else if(action.kind==='command') {
80
+ if(action.name!==event || !Object.hasOwn(manifest.commands||{},event) || (action.capability!==CAPABILITY && action.namespace!==manifest.command)) invalid();
81
+ } else if(action.kind==='operation') {
82
+ if(action.slot!==manifest.layer || !Object.hasOwn(manifest.operations||{},action.name) || manifest.operations[action.name].command!==event) invalid();
83
+ } else invalid();
84
+ }
85
+ export function requireOkfAdmittedAction(context) {
86
+ try {validateInvocationShape(context,{capability:CAPABILITY});} catch {invalid();}
87
+ if(!context.instance || !context.intent) throw Object.assign(new Error('captured action requires an admitted instance intent'),{code:'E_ADMISSION'});
88
+ // The shared shape enforces incarnation equality; the kernel, not this
89
+ // provider, proves that the supplied attempt is currently admitted/running.
90
+ }
91
+ export function assertOkfSourceReceiptContext(receipt,context,binding) {
92
+ validateOkfInvocationContext(context,binding);
93
+ try {validateCapturedReceipt(context.instance?.home,receipt);} catch {invalid();}
94
+ const instance=context.instance;
95
+ if(!instance || context.action.kind!=='hook' || !['spawn','retire'].includes(context.action.name)
96
+ || receipt.kind!==context.subject.kind || receipt.home!==instance.home || receipt.work!==instance.work
97
+ || receipt.agent!==instance.agent || receipt.instance!==instance.name || receipt.context!==context.executionBinding.deployment
98
+ || !sameInvocationJson(receipt.executionBinding,context.executionBinding) || !sameInvocationJson(receipt.responsibleHuman,context.responsibleHuman)
99
+ || !sameInvocationJson(receipt.binding,binding)) invalid();
100
+ const identity=context.subject.kind==='persistent'?context.subject.soul.identity:null;
101
+ if(!sameInvocationJson(receipt.sourceIdentity,identity)) invalid();
102
+ }
103
+ export function assertOkfSourceContext(source,context,binding) {
104
+ validateOkfInvocationContext(context,binding);
105
+ if(!source) invalid();
106
+ if(context.subject.kind!=='persistent' || source.agent!==context.subject.soul.alias
107
+ || !sameInvocationJson(source.sourceIdentity,context.subject.soul.identity) || !sameInvocationJson(source.providerBinding,binding)
108
+ || !sameInvocationJson(source.executionBinding,context.executionBinding) || source.context!==context.executionBinding.deployment
109
+ || !sameInvocationJson(source.responsibleHuman,context.responsibleHuman)) invalid();
110
+ if(context.instance && (source.home!==context.instance.home || source.work!==context.instance.work || source.instance!==context.instance.name || source.agent!==context.instance.agent)) invalid();
111
+ }
@@ -0,0 +1,135 @@
1
+ // Provider-side consumption checks for the kernel-owned CapturedInvocationContext1.
2
+ // Decoded JSON only: no source/config parsing, artifact lookup or authorization.
3
+ // The kernel must verify retained authority before invoking provider code.
4
+ import { isAbsolute, join, resolve } from 'node:path';
5
+ export const INVOCATION_LIMITS=Object.freeze({bytes:512*1024,depth:32,entries:16384});
6
+ export const PRIOR_RECEIPT_LIMITS=Object.freeze({bytes:128*1024,depth:24,entries:8192});
7
+ const fail=()=>{throw Object.assign(new Error('invalid captured invocation'),{code:'invalid-binding'});};
8
+ const obj=value=>value!==null && typeof value==='object' && !Array.isArray(value);
9
+ const canonical=value=>value===null || typeof value!=='object'?JSON.stringify(value):Array.isArray(value)?`[${value.map(canonical).join(',')}]`:`{${Object.keys(value).sort().map(key=>`${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
10
+ export const sameInvocationJson=(a,b)=>canonical(a)===canonical(b);
11
+ const same=sameInvocationJson;
12
+ function keys(value,allowed,required=allowed) {if(!obj(value)) fail();for(const key of Object.keys(value)) if(!allowed.includes(key)) fail();for(const key of required) if(!Object.hasOwn(value,key)) fail();}
13
+ function text(value,empty=false) {if(typeof value!=='string' || (!empty && !value.length) || value.includes('\0')) fail();}
14
+ function list(value) {if(!Array.isArray(value)) fail();return value;}
15
+ function absolute(value) {text(value);if(!isAbsolute(value) || resolve(value)!==value) fail();}
16
+ function path(value,root=false) {text(value);if(root && value==='.') return;if(value.startsWith('/') || value.includes('\\') || value.split('/').some(part=>!part || part==='.' || part==='..')) fail();}
17
+ function hash(value) {if(typeof value!=='string' || !/^sha256-[a-f0-9]{64}$/.test(value)) fail();}
18
+ function integrity(value,format) {keys(value,['format','value']);if(value.format!==format) fail();hash(value.value);}
19
+ function capability(value) {text(value);if(!/^(?:@?[a-z0-9][a-z0-9._-]*[./])[a-z0-9][a-z0-9._/-]*$/.test(value)) fail();}
20
+ function repository(value) {
21
+ if(value?.kind==='canonical-remote') {keys(value,['kind','remote']);text(value.remote);}
22
+ else if(value?.kind==='provider-repository') {keys(value,['kind','provider','host','id']);text(value.provider);text(value.host);text(value.id);}
23
+ else fail();
24
+ }
25
+ function soulIdentity(value) {
26
+ if(value?.kind==='git-soul') {keys(value,['kind','repository','exportPath']);repository(value.repository);path(value.exportPath);}
27
+ else if(value?.kind==='local-soul') {keys(value,['kind','source','exportPath']);text(value.source);if(!value.source.startsWith('path:')) fail();path(value.exportPath,true);}
28
+ else fail();
29
+ }
30
+ function artifact(value) {
31
+ if(value?.kind==='soul') {keys(value,['kind','identity','integrity']);soulIdentity(value.identity);}
32
+ else if(value?.kind==='capability') {keys(value,['kind','capability','integrity']);capability(value.capability);}
33
+ else if(value?.kind==='resource') keys(value,['kind','integrity']);
34
+ else fail();
35
+ integrity(value.integrity,'oats.tree-exec.v1');
36
+ }
37
+ function resolution(value) {keys(value,['schemaVersion','id']);if(value.schemaVersion!==1) fail();hash(value.id);}
38
+ function incarnation(value) {if(typeof value!=='string' || !/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(value)) fail();}
39
+ function intent(value,instance) {
40
+ if(value===null) return;
41
+ keys(value,['schemaVersion','executionId','incarnationId','attempt']);incarnation(value.incarnationId);
42
+ if(value.schemaVersion!==1 || typeof value.executionId!=='string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value.executionId) || !Number.isSafeInteger(value.attempt) || value.attempt<1) fail();
43
+ if(instance===null || value.incarnationId!==instance.incarnationId) fail();
44
+ }
45
+ function origin(value) {
46
+ keys(value,['kind','document','pointer','span'],['kind','document','pointer']);
47
+ if(!['soul-requirement','soul-default','workspace-default','import-adoption','operator','package-dependency','work-target','migration-evidence','provider-binding','workspace-admission','member-backlink','source-export','manifest-default'].includes(value.kind)) fail();
48
+ text(value.pointer,true);if(!/^(?:\/(?:[^~]|~[01])*)*$/.test(value.pointer)) fail();
49
+ const doc=value.document;
50
+ if(doc?.kind==='source') {keys(doc,['kind','source','revision','path','integrity']);text(doc.source);text(doc.revision);path(doc.path);integrity(doc.integrity,'oats.bytes.v1');}
51
+ else if(doc?.kind==='deployment') {keys(doc,['kind','path','integrity']);path(doc.path);integrity(doc.integrity,'oats.bytes.v1');}
52
+ else if(doc?.kind==='operator') {keys(doc,['kind','id']);text(doc.id);}
53
+ else if(doc?.kind==='record') {keys(doc,['kind','ref']);resolution(doc.ref);}
54
+ else if(doc?.kind==='artifact') {keys(doc,['kind','owner','path','integrity']);artifact(doc.owner);path(doc.path);integrity(doc.integrity,'oats.bytes.v1');}
55
+ else fail();
56
+ if(Object.hasOwn(value,'span')) {keys(value.span,['start','end']);if(!Number.isSafeInteger(value.span.start) || !Number.isSafeInteger(value.span.end) || value.span.start<0 || value.span.end<value.span.start) fail();}
57
+ }
58
+ function origins(value) {list(value).forEach(origin);}
59
+ function subject(value) {
60
+ if(value?.kind==='helper') {
61
+ keys(value,['kind','provider','definition','name']);artifact(value.provider);
62
+ keys(value.definition,['owner','path','kind']);artifact(value.definition.owner);path(value.definition.path,true);
63
+ if(value.provider.kind!=='capability' || !same(value.provider,value.definition.owner) || !['file','directory','manifest','skill','runtime-package'].includes(value.definition.kind)) fail();
64
+ if(typeof value.name!=='string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value.name)) fail();return value.name;
65
+ }
66
+ if(value?.kind!=='persistent') fail();keys(value,['kind','soul']);const s=value.soul;
67
+ keys(s,['identity','revision','alias','sourceArtifact','definition','projection']);soulIdentity(s.identity);artifact(s.sourceArtifact);
68
+ if(s.sourceArtifact.kind!=='soul' || !same(s.identity,s.sourceArtifact.identity)) fail();
69
+ if(typeof s.alias!=='string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(s.alias)) fail();path(s.definition);
70
+ keys(s.projection,['roots']);list(s.projection.roots).forEach(value=>path(value,true));
71
+ if(!s.projection.roots.length || !s.projection.roots.some(root=>root==='.' || s.definition===root || s.definition.startsWith(`${root}/`))) fail();
72
+ for(let i=1;i<s.projection.roots.length;i++) if(Buffer.compare(Buffer.from(s.projection.roots[i-1]),Buffer.from(s.projection.roots[i]))>=0) fail();
73
+ if(s.identity.exportPath!=='.' && !s.definition.startsWith(`${s.identity.exportPath}/`)) fail();
74
+ const r=s.revision;
75
+ if(r?.kind==='local') {
76
+ keys(r,['kind','source','integrity','provenance','repository'],['kind','source','integrity','provenance']);text(r.source);if(!r.source.startsWith('path:')) fail();integrity(r.integrity,'oats.tree-exec.v1');
77
+ if(!same(r.integrity,s.sourceArtifact.integrity)) fail();
78
+ if(s.identity.kind==='local-soul') {if(r.source!==s.identity.source || Object.hasOwn(r,'repository')) fail();}
79
+ else {repository(r.repository);if(!same(r.repository,s.identity.repository)) fail();}
80
+ } else {
81
+ keys(r,['identity','remote','selector','commit','provenance']);repository(r.identity);text(r.remote);text(r.selector);
82
+ if(s.identity.kind!=='git-soul' || !same(r.identity,s.identity.repository) || !/^[a-f0-9]{40}$/.test(r.commit)) fail();
83
+ if(r.identity.kind==='canonical-remote' && r.identity.remote!==r.remote) fail();
84
+ }
85
+ origins(r.provenance);if(!r.provenance.length) fail();return s.alias;
86
+ }
87
+ function context(value) {
88
+ if(value?.kind==='workspace') {keys(value,['kind','identity','observation']);keys(value.identity,['repository','path']);repository(value.identity.repository);if(value.identity.path!=='oats-workspace.yaml') fail();origin(value.observation);return {kind:'workspace',identity:value.identity};}
89
+ if(value?.kind==='standalone') {keys(value,['kind','key']);if(value.key!==null) text(value.key);return value;}
90
+ fail();
91
+ }
92
+ function human(value) {if(value===null) return;keys(value,['provider','id']);text(value.provider);text(value.id);}
93
+ function messaging(value,selectedContext,responsibleHuman) {
94
+ if(value?.enabled===false) {keys(value,['schemaVersion','enabled']);if(responsibleHuman!==null) fail();}
95
+ else {
96
+ keys(value,['schemaVersion','enabled','privateKey','wider','provenance']);if(value.enabled!==true) fail();
97
+ keys(value.privateKey,['provider','human','context']);capability(value.privateKey.provider);human(value.privateKey.human);
98
+ if(value.privateKey.human===null || !same(value.privateKey.human,responsibleHuman) || !same(value.privateKey.context,selectedContext) || (selectedContext.kind==='standalone' && selectedContext.key===null)) fail();
99
+ list(value.wider).forEach(row=>{keys(row,['provider','id']);text(row.id);if(row.provider!==value.privateKey.provider) fail();});
100
+ for(let i=1;i<value.wider.length;i++) if(Buffer.compare(Buffer.from(canonical(value.wider[i-1])),Buffer.from(canonical(value.wider[i])))>=0) fail();
101
+ origins(value.provenance);
102
+ }
103
+ if(value.schemaVersion!==1) fail();
104
+ }
105
+ function action(value) {
106
+ if(!obj(value)) fail();
107
+ if(value.kind==='command') {keys(value,['kind','capability','namespace','name'],['kind','name']);if(Object.hasOwn(value,'capability')===Object.hasOwn(value,'namespace')) fail();if(value.capability!==undefined) capability(value.capability);else text(value.namespace);text(value.name);}
108
+ else if(value.kind==='hook') {keys(value,['kind','capability','name']);capability(value.capability);text(value.name);}
109
+ else if(value.kind==='operation') {keys(value,['kind','slot','name']);if(!['knowledge','messaging','tasks'].includes(value.slot)) fail();text(value.name);}
110
+ else if(['inspect','compose'].includes(value.kind)) keys(value,['kind']);
111
+ else fail();
112
+ }
113
+ export function assertInvocationBounds(value,limits=INVOCATION_LIMITS) {
114
+ let entries=0;const active=new Set();
115
+ const visit=(v,depth)=>{
116
+ if(v===null || typeof v==='string' || typeof v==='boolean') return;
117
+ if(typeof v==='number') {if(!Number.isFinite(v)) fail();return;}
118
+ if(typeof v!=='object' || depth>limits.depth || active.has(v)) fail();
119
+ if(!Array.isArray(v) && Object.getPrototypeOf(v)!==Object.prototype && Object.getPrototypeOf(v)!==null) fail();
120
+ active.add(v);for(const child of Object.values(v)) {if(++entries>limits.entries) fail();visit(child,depth+1);}active.delete(v);
121
+ };
122
+ visit(value,1);if(Buffer.byteLength(JSON.stringify(value),'utf8')>limits.bytes) fail();
123
+ }
124
+ export function validateInvocationShape(value,expected={}) {
125
+ assertInvocationBounds(value);keys(value,['schemaVersion','executionBinding','subject','instance','intent','context','responsibleHuman','messagingChoice','capability','action','priorReceipt']);
126
+ if(value.schemaVersion!==1) fail();keys(value.executionBinding,['schemaVersion','deployment','resolution']);if(value.executionBinding.schemaVersion!==1) fail();absolute(value.executionBinding.deployment);resolution(value.executionBinding.resolution);
127
+ const alias=subject(value.subject);
128
+ if(value.instance!==null) {keys(value.instance,['home','work','name','agent','incarnationId']);incarnation(value.instance.incarnationId);absolute(value.instance.home);absolute(value.instance.work);text(value.instance.name);text(value.instance.agent);if(value.instance.agent!==alias || value.instance.work!==join(value.instance.home,'work')) fail();}
129
+ intent(value.intent,value.instance);
130
+ const selectedContext=context(value.context);human(value.responsibleHuman);messaging(value.messagingChoice,selectedContext,value.responsibleHuman);capability(value.capability);action(value.action);assertInvocationBounds(value.priorReceipt,PRIOR_RECEIPT_LIMITS);
131
+ if(value.action.capability!==undefined && value.action.capability!==value.capability) fail();
132
+ if(value.action.kind==='hook' && value.instance===null) fail();
133
+ for(const field of ['capability','context','action']) if(Object.hasOwn(expected,field) && !same(value[field],expected[field])) fail();
134
+ return value;
135
+ }
@@ -70,7 +70,7 @@ export function unlock(path, token) {
70
70
  try { process.kill(o.pid, 0); fail('E_LOCKED', 'lock owner is still alive'); } catch (e) { if(e.code !== 'ESRCH') throw e; }
71
71
  fs.rmSync(path, { recursive: true }); syncDir(dirname(path)); return { unlocked: path };
72
72
  }
73
- export const identityKeys = ['OATS_INSTANCE','OATS_INSTANCE_HOME','OATS_HOME','PI_AGENT_HOME','PI_AGENT_NAME','PI_AGENT_INSTANCE','PI_AGENTS_ROOT','OATS_ROOT','OATS_SOUL','OATS_AGENT','OATS_KIND','OATS_EVENT','OATS_CONTEXT','OATS_REPO','OATS_WORK','OATS_BRANCH','OATS_META','OATS_SETTINGS'];
73
+ export const identityKeys = ['OATS_INSTANCE','OATS_INSTANCE_HOME','OATS_HOME','PI_AGENT_HOME','PI_AGENT_NAME','PI_AGENT_INSTANCE','PI_AGENTS_ROOT','OATS_ROOT','OATS_SOUL','OATS_AGENT','OATS_KIND','OATS_EVENT','OATS_CONTEXT','OATS_REPO','OATS_WORK','OATS_BRANCH','OATS_META','OATS_SETTINGS','OATS_BINDING_FILE','OATS_SOURCE_RECEIPT_FILE','OATS_INVOCATION_CONTEXT_FILE','OATS_DEPLOYMENT','OATS_RESOLUTION'];
74
74
  export function cleanEnv(env = process.env) {
75
75
  return Object.fromEntries(Object.entries(env).filter(([k]) => !/^(OATS_(?!HOME_DIR$|PACKAGE_CATALOG$)|PI_AGENT|GIT_)/.test(k)));
76
76
  }
@@ -0,0 +1,199 @@
1
+ import { isAbsolute, resolve } from 'node:path';
2
+ import { identifier, relPath, fail } from './io.mjs';
3
+ import { resolveNodes, validateBindings, validateDeclaration } from './config.mjs';
4
+ import { validateRepositoryLocator } from './source-contract.mjs';
5
+
6
+ export const KNOWLEDGE_CONTRACT='oats.okf.locations';
7
+ export const KNOWLEDGE_CONTRACT_VERSION=1;
8
+ const candidateKinds=new Set(['workspace-default','import-adoption','operator']);
9
+ const obj=value=>value && typeof value==='object' && !Array.isArray(value);
10
+ const pointerKey=value=>String(value).replace(/~/g,'~0').replace(/\//g,'~1');
11
+ const clone=value=>JSON.parse(JSON.stringify(value));
12
+ const canonicalJson=value=>value===null || typeof value!=='object'?JSON.stringify(value):Array.isArray(value)?`[${value.map(canonicalJson).join(',')}]`:`{${Object.keys(value).sort().map(key=>`${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
13
+ export const sameJson=(a,b)=>canonicalJson(a)===canonicalJson(b);
14
+ function keys(value,allowed,required,label) {
15
+ if(!obj(value)) fail('E_CONFIG',`${label} must be an object`);
16
+ for(const key of Object.keys(value)) if(!allowed.includes(key)) fail('E_CONFIG',`unknown ${label} property: ${key}`);
17
+ for(const key of required) if(!Object.hasOwn(value,key)) fail('E_CONFIG',`${label} requires ${key}`);
18
+ return value;
19
+ }
20
+ function bindingKey(value) {
21
+ if(typeof value!=='string') fail('E_CONFIG','invalid knowledge binding key');
22
+ // Reserved stores.<alias> addresses ONE legal alias; dots inside that suffix
23
+ // are literal identity characters, never another hierarchy or fuzzy match.
24
+ const parts=value.startsWith('stores.')?[value.slice(7)]:value.split('.');
25
+ if(parts.some(part=>{try{identifier(part);return false;}catch{return true;}})) fail('E_CONFIG','invalid knowledge binding key');
26
+ return value;
27
+ }
28
+ export const bindingChoiceKey=value=>{
29
+ bindingKey(value);
30
+ return value.startsWith('stores.')?storeChoiceKey(value.slice(7)):`/bindings/knowledge/${value.split('.').map(pointerKey).join('/')}`;
31
+ };
32
+ export const storeChoiceKey=alias=>`/bindings/knowledge/stores/${pointerKey(identifier(alias))}`;
33
+ function originAt({origins={},origin=null,pointer},suffix,kind) {
34
+ const at=`${pointer}${suffix}`,specific=origins[at],found=specific ?? origin;
35
+ if(!obj(found)) fail('E_CONFIG',`missing origin for ${at}`);
36
+ return clone({...found,kind});
37
+ }
38
+
39
+ /** Portable, non-secret store locator. Filesystem existence/custody and remote
40
+ * identity are checked later by bind/check; this function performs no I/O. */
41
+ export function validateStoreLocator(value) {
42
+ if(!obj(value)) fail('E_CONFIG','knowledge store locator must be an object');
43
+ identifier(value.id);
44
+ if(value.kind==='directory') {
45
+ keys(value,['id','kind','path'],['id','kind','path'],'directory store locator');
46
+ if(typeof value.path!=='string' || !value.path.startsWith('path:') || !isAbsolute(value.path.slice(5)) || resolve(value.path.slice(5))!==value.path.slice(5)) fail('E_CONFIG','portable directory store requires a normalized absolute path: locator');
47
+ return {id:value.id,kind:value.kind,path:value.path};
48
+ } else if(value.kind==='git') {
49
+ keys(value,['id','kind','repository','root','acceptedBranch','pr'],['id','kind','repository','root','acceptedBranch','pr'],'git store locator');
50
+ validateRepositoryLocator(value.repository);
51
+ relPath(value.root,true);
52
+ if(typeof value.acceptedBranch!=='string' || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(value.acceptedBranch) || value.acceptedBranch.includes('..') || value.acceptedBranch.endsWith('/') || value.acceptedBranch.endsWith('.lock')) fail('E_CONFIG','invalid acceptedBranch');
53
+ keys(value.pr,['repository'],['repository'],'git PR binding');if(typeof value.pr.repository!=='string' || !/^[\w.-]+\/[\w.-]+$/.test(value.pr.repository)) fail('E_CONFIG','git PR binding requires owner/repo');
54
+ return {id:value.id,kind:value.kind,repository:value.repository,root:value.root,acceptedBranch:value.acceptedBranch,pr:{repository:value.pr.repository}};
55
+ }
56
+ fail('E_CONFIG',`unsupported knowledge store kind: ${value.kind}`);
57
+ }
58
+ function validateEnvelope(value) {
59
+ keys(value,['contract','version','payload'],['contract','version','payload'],'knowledge declaration');
60
+ if(value.contract!==KNOWLEDGE_CONTRACT || value.version!==KNOWLEDGE_CONTRACT_VERSION) fail('E_CONFIG',`unsupported knowledge contract: ${value.contract}@${value.version}`);
61
+ return value.payload;
62
+ }
63
+
64
+ /** Validate the source-owned OKF payload and emit only field-level inputs for
65
+ * the kernel's one resolver. No winner is selected here. */
66
+ export function normalizeKnowledgeDeclaration(envelope,{origins={},origin=null,pointer='/knowledge/payload'}={}) {
67
+ const payload=validateEnvelope(envelope);
68
+ keys(payload,['owner','stores','reads','owns'],['owner','stores','reads','owns'],'OKF knowledge payload');
69
+ const owner=identifier(payload.owner),stores={},requirements=[],candidates=[];
70
+ if(!obj(payload.stores)) fail('E_CONFIG','knowledge stores must be an object');
71
+ for(const [alias,spec] of Object.entries(payload.stores)) {
72
+ identifier(alias);keys(spec,['fixed','default','inherit'],[],`knowledge store ${alias}`);
73
+ const modes=['fixed','default','inherit'].filter(key=>Object.hasOwn(spec,key));
74
+ if(modes.length!==1) fail('E_CONFIG',`knowledge store ${alias} needs exactly one of fixed, default or inherit`);
75
+ const mode=modes[0],at=`/stores/${pointerKey(alias)}`,choiceKey=mode==='inherit'?bindingChoiceKey(bindingKey(spec.inherit)):storeChoiceKey(alias);
76
+ const selectedOrigin=originAt({origins,origin,pointer},`${at}/${mode}`,mode==='default'?'soul-default':'soul-requirement');
77
+ if(mode==='fixed') requirements.push({key:choiceKey,kind:'equals',value:validateStoreLocator(spec.fixed),origin:selectedOrigin});
78
+ else if(mode==='default') candidates.push({key:choiceKey,kind:'soul-default',value:validateStoreLocator(spec.default),origin:selectedOrigin});
79
+ else requirements.push({key:choiceKey,kind:'required',origin:selectedOrigin});
80
+ stores[alias]={mode,choiceKey,origin:selectedOrigin};
81
+ }
82
+ if(!Array.isArray(payload.reads) || !Array.isArray(payload.owns)) fail('E_CONFIG','knowledge reads/owns must be arrays');
83
+ const reads=[],owns=[],seenReads=new Set(),seenOwns=new Set();
84
+ payload.reads.forEach((entry,index)=>{
85
+ keys(entry,['store','node'],['store','node'],'knowledge read');
86
+ const store=identifier(entry.store),node=identifier(entry.node),key=`${store}/${node}`;
87
+ if(!Object.hasOwn(stores,store)) fail('E_CONFIG',`read references undeclared store: ${store}`);
88
+ if(seenReads.has(key)) fail('E_CONFIG',`duplicate knowledge read: ${key}`);seenReads.add(key);
89
+ reads.push({store,node,origin:originAt({origins,origin,pointer},`/reads/${index}`,'soul-requirement')});
90
+ });
91
+ payload.owns.forEach((entry,index)=>{
92
+ keys(entry,['node','destination'],['node'],'knowledge ownership');
93
+ const node=identifier(entry.node),destination=entry.destination===undefined?null:identifier(entry.destination);
94
+ if(destination!==null && !Object.hasOwn(stores,destination)) fail('E_CONFIG',`owned node references undeclared destination: ${destination}`);
95
+ const key=`${destination ?? '<write.default>'}/${node}`;
96
+ if(seenOwns.has(key)) fail('E_CONFIG',`duplicate knowledge ownership: ${key}`);seenOwns.add(key);
97
+ const itemOrigin=originAt({origins,origin,pointer},`/owns/${index}`,'soul-requirement');
98
+ if(destination===null) requirements.push({key:bindingChoiceKey('write.default'),kind:'required',origin:itemOrigin});
99
+ owns.push({node,destination,choiceKey:destination===null?bindingChoiceKey('write.default'):stores[destination].choiceKey,origin:itemOrigin});
100
+ });
101
+ return {contract:KNOWLEDGE_CONTRACT,version:KNOWLEDGE_CONTRACT_VERSION,owner,stores,reads,owns,requirements,candidates};
102
+ }
103
+
104
+ /** Convert already-parsed workspace/adoption/operator values to candidates.
105
+ * This intentionally performs no precedence or recursive merging. */
106
+ export function normalizeKnowledgeBindingCandidates({bindings,kind,origins={},origin=null,pointer='/bindings'}={}) {
107
+ if(!candidateKinds.has(kind)) fail('E_CONFIG',`invalid knowledge candidate kind: ${kind}`);
108
+ if(!obj(bindings)) fail('E_CONFIG','knowledge bindings must be an object');
109
+ const candidates=[];
110
+ for(const [name,value] of Object.entries(bindings)) {
111
+ const key=bindingChoiceKey(name),at=`/${pointerKey(name)}`;
112
+ candidates.push({key,kind,value:validateStoreLocator(value),origin:originAt({origins,origin,pointer},at,kind)});
113
+ }
114
+ return candidates;
115
+ }
116
+
117
+ function selected(choices,key) {
118
+ if(!obj(choices) || !obj(choices[key]) || !Object.hasOwn(choices[key],'value') || choices[key].value===null) fail('E_CONFIG',`unresolved knowledge binding: ${key}`);
119
+ return validateStoreLocator(choices[key].value);
120
+ }
121
+ function runtimeStore(locator) {return locator.kind==='directory'?{...locator,path:locator.path.slice(5)}:locator;}
122
+
123
+ /** Render provider-owned nonsecret data after the kernel's shared resolver has
124
+ * selected every field. Reads never imply a write destination. */
125
+ export function bindKnowledgeDomain({model,choices}) {
126
+ if(!obj(model) || model.contract!==KNOWLEDGE_CONTRACT || model.version!==KNOWLEDGE_CONTRACT_VERSION) fail('E_CONFIG','invalid normalized OKF model');
127
+ const aliases={},stores={},provenance=[];
128
+ const add=(locator,choiceKey)=>{
129
+ const rendered=runtimeStore(locator);
130
+ if(Object.hasOwn(stores,locator.id) && !sameJson(stores[locator.id],rendered)) fail('E_CONFIG',`store identity ${locator.id} resolves to conflicting locations`);
131
+ stores[locator.id]=rendered;
132
+ const selectedBy=choices[choiceKey]?.selectedBy;if(obj(selectedBy)) provenance.push(clone(selectedBy));
133
+ return locator.id;
134
+ };
135
+ for(const [alias,plan] of Object.entries(model.stores)) aliases[alias]=add(selected(choices,plan.choiceKey),plan.choiceKey);
136
+ const reads=model.reads.map(entry=>({store:aliases[entry.store],node:entry.node}));
137
+ const readKeys=new Set();for(const entry of reads) {const key=`${entry.store}/${entry.node}`;if(readKeys.has(key)) fail('E_CONFIG',`duplicate resolved knowledge read: ${key}`);readKeys.add(key);}
138
+ const owns=model.owns.map(entry=>{
139
+ const locator=entry.destination===null?selected(choices,entry.choiceKey):selected(choices,model.stores[entry.destination].choiceKey);
140
+ const store=add(locator,entry.choiceKey);
141
+ return {store,node:entry.node,steward:model.owner};
142
+ });
143
+ const ownKeys=new Set();for(const entry of owns) {const key=`${entry.store}/${entry.node}`;if(ownKeys.has(key)) fail('E_OWNER',`duplicate resolved knowledge steward: ${key}`);ownKeys.add(key);}
144
+ for(const entry of [...model.reads,...model.owns]) provenance.push(clone(entry.origin));
145
+ const unique=[];for(const item of provenance) if(!unique.some(prior=>sameJson(prior,item))) unique.push(item);
146
+ return {contract:KNOWLEDGE_CONTRACT,version:KNOWLEDGE_CONTRACT_VERSION,payload:{owner:model.owner,stores,reads,owns},credentialRefs:{},provenance:unique};
147
+ }
148
+
149
+ function absolutePath(value,label) {
150
+ if(typeof value!=='string' || !isAbsolute(value) || resolve(value)!==value) fail('E_PATH',`${label} must be a normalized absolute path`);
151
+ return value;
152
+ }
153
+ function boundStore(value) {
154
+ if(!obj(value)) fail('E_CONFIG','bound knowledge store must be an object');
155
+ const portable=value.kind==='directory'?{...value,path:`path:${value.path}`} : value;
156
+ return runtimeStore(validateStoreLocator(portable));
157
+ }
158
+
159
+ /** Purely adapt a captured nonsecret provider domain to the existing OKF v1
160
+ * documents. The host supplies durable custody paths; neither is derived from
161
+ * a source home, and this function performs no filesystem access. */
162
+ export function renderKnowledgeRuntime({domain,stateDir,descriptorFile}={}) {
163
+ keys(domain,['owner','stores','reads','owns'],['owner','stores','reads','owns'],'bound knowledge domain');
164
+ const owner=identifier(domain.owner),bases={};
165
+ if(!obj(domain.stores) || !Object.keys(domain.stores).length) fail('E_CONFIG','bound knowledge domain requires stores');
166
+ for(const [alias,value] of Object.entries(domain.stores)) {
167
+ identifier(alias);const store=boundStore(value);
168
+ if(store.id!==alias) fail('E_ID',`runtime alias must equal stable store identity: ${alias}`);
169
+ bases[alias]=store;
170
+ }
171
+ const references=(entries,label,owned=false)=>{
172
+ if(!Array.isArray(entries)) fail('E_CONFIG',`bound knowledge ${label} must be an array`);
173
+ return entries.map(entry=>{
174
+ keys(entry,owned?['store','node','steward']:['store','node'],owned?['store','node','steward']:['store','node'],`bound knowledge ${label}`);
175
+ const store=identifier(entry.store),node=identifier(entry.node);
176
+ if(!Object.hasOwn(bases,store)) fail('E_CONFIG',`${label} references unbound store: ${store}`);
177
+ if(owned && identifier(entry.steward)!==owner) fail('E_OWNER',`bound knowledge steward mismatch: ${store}/${node}`);
178
+ return `${store}/${node}`;
179
+ });
180
+ };
181
+ const declaration=validateDeclaration({version:1,owner,reads:references(domain.reads,'read'),owns:references(domain.owns,'ownership',true)});
182
+ return {descriptorFile:absolutePath(descriptorFile,'OKF descriptor file'),bindings:{version:1,stateDir:absolutePath(stateDir,'OKF stateDir'),bases},declaration};
183
+ }
184
+
185
+ /** Read-only custody/accepted-base check for an already rendered capture.
186
+ * `accepted` must come from the existing base validation path. */
187
+ export function checkKnowledgeRuntime({rendered,accepted,sourceHome,sourceWork}={}) {
188
+ keys(rendered,['descriptorFile','bindings','declaration'],['descriptorFile','bindings','declaration'],'rendered OKF runtime');
189
+ keys(rendered.bindings,['version','stateDir','bases'],['version','stateDir','bases'],'rendered OKF bindings');
190
+ if(rendered.bindings.version!==1 || !obj(rendered.bindings.bases) || !Object.keys(rendered.bindings.bases).length) fail('E_CONFIG','invalid rendered OKF bindings');
191
+ absolutePath(rendered.bindings.stateDir,'OKF stateDir');
192
+ for(const [alias,value] of Object.entries(rendered.bindings.bases)) if(identifier(alias)!==boundStore(value).id) fail('E_ID',`runtime alias must equal stable store identity: ${alias}`);
193
+ if(!obj(accepted)) fail('E_CONFIG','accepted OKF base metadata is required');
194
+ const descriptorFile=absolutePath(rendered.descriptorFile,'OKF descriptor file');
195
+ const bindings=validateBindings(rendered.bindings,descriptorFile,{sourceHome,sourceWork});
196
+ const declaration=validateDeclaration(rendered.declaration);
197
+ resolveNodes(declaration,bindings,accepted);
198
+ return {descriptorFile,bindings,declaration};
199
+ }
@@ -0,0 +1,46 @@
1
+ import { isAbsolute, resolve } from 'node:path';
2
+ import { fail, relPath } from './io.mjs';
3
+ const obj=value=>value!==null && typeof value==='object' && !Array.isArray(value);
4
+ const invalid=()=>fail('E_SOURCE','invalid nonsecret qualified source identity');
5
+ function exact(value,fields) {if(!obj(value) || Object.keys(value).some(key=>!fields.includes(key)) || fields.some(key=>!Object.hasOwn(value,key))) invalid();}
6
+
7
+ /** Provider-owned nonsecret locator contract; no I/O, source resolver or secret
8
+ * lookup. Authentication remains in the native credential transport. */
9
+ export function validateRepositoryLocator(value) {
10
+ const bad=()=>fail('E_CONFIG','Git repository must not contain credentials, query, fragment or malformed paths');
11
+ if(typeof value!=='string' || /[\s\\\x00-\x1f\x7f?#]/.test(value)) bad();
12
+ const scp=/^(git)@([^/:]+):(.+)$/.exec(value);
13
+ const spelling=scp?`ssh://${scp[1]}@${scp[2]}${scp[3].startsWith('/')?'':'/'}${scp[3]}`:value;
14
+ if(!/^(https|ssh):\/\//.test(spelling)) bad();
15
+ const pathStart=spelling.indexOf('/',spelling.indexOf('://')+3);
16
+ if(pathStart<0) bad();
17
+ for(const part of spelling.slice(pathStart+1).split('/')) {
18
+ let decoded;try{decoded=decodeURIComponent(part);}catch{bad();}
19
+ if(!part || decoded==='.' || decoded==='..' || /[/\\\x00-\x1f\x7f]/.test(decoded)) bad();
20
+ }
21
+ let url;try{url=new URL(spelling);}catch{bad();}
22
+ if(!url.hostname || url.password || url.search || url.hash || (url.protocol==='https:' && url.username)) bad();
23
+ if(scp) {if(value!==`${scp[1]}@${url.hostname}:${scp[3]}`) bad();}
24
+ else if(url.href!==value) bad();
25
+ return value;
26
+ }
27
+ export function qualifiedSoulIdentity(value) {
28
+ try {
29
+ if(value?.kind==='git-soul') {
30
+ exact(value,['kind','repository','exportPath']);const r=value.repository;
31
+ if(r?.kind==='provider-repository') {
32
+ exact(r,['kind','provider','host','id']);
33
+ if(typeof r.provider!=='string' || !/^[a-z][a-z0-9.-]*$/.test(r.provider) || typeof r.host!=='string' || !/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(r.host) || typeof r.id!=='string' || !r.id || /[\x00-\x1f\x7f]/.test(r.id)) invalid();
34
+ } else {
35
+ exact(r,['kind','remote']);if(r.kind!=='canonical-remote' || typeof r.remote!=='string' || !r.remote.startsWith('git:')) invalid();
36
+ validateRepositoryLocator(r.remote.slice(4));
37
+ }
38
+ } else if(value?.kind==='local-soul') {
39
+ exact(value,['kind','source','exportPath']);
40
+ if(typeof value.source!=='string' || !value.source.startsWith('path:') || /[\x00-\x1f\x7f]/.test(value.source)) invalid();
41
+ const p=value.source.slice(5);if(!isAbsolute(p) || resolve(p)!==p) invalid();
42
+ } else invalid();
43
+ relPath(value.exportPath,value.kind==='local-soul');
44
+ return JSON.parse(JSON.stringify(value));
45
+ } catch {invalid();}
46
+ }