@awebai/oats 0.23.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +7 -4
  2. package/bin/oats-pi-sdk-host.mjs +17 -0
  3. package/bin/oats.mjs +442 -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 +101 -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/capability-manifest.schema.json +37 -66
  25. package/docs/captured-invocation-context.schema.json +7 -0
  26. package/docs/captured-resolution.schema.json +7 -0
  27. package/docs/design/2026-09-14-artifact-retention-contract.md +190 -0
  28. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +708 -0
  29. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +85 -0
  30. package/docs/design/2026-09-14-portable-souls-explainer.md +750 -0
  31. package/docs/design/2026-09-15-captured-dispatch.md +127 -0
  32. package/docs/design/2026-09-15-captured-resolution-records.md +143 -0
  33. package/docs/design/2026-09-15-package-preparation.md +100 -0
  34. package/docs/design/2026-09-15-portable-data-contract.md +121 -0
  35. package/docs/design/2026-09-15-portable-declarations.md +189 -0
  36. package/docs/design/2026-09-15-portable-souls-handoff.md +150 -0
  37. package/docs/design/2026-09-15-portable-souls-implementation.md +417 -0
  38. package/docs/design/2026-09-15-selection-lock-and-approval.md +122 -0
  39. package/docs/design/2026-09-15-source-observation.md +119 -0
  40. package/docs/design/2026-09-16-captured-admission.md +77 -0
  41. package/docs/design/2026-09-16-captured-helper-dispatch.md +105 -0
  42. package/docs/design/2026-09-16-captured-launch-inputs.md +42 -0
  43. package/docs/design/2026-09-16-command-profile-preparation.md +86 -0
  44. package/docs/design/2026-09-16-fresh-install-first-rollout.md +47 -0
  45. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +277 -0
  46. package/docs/design/2026-09-16-knowledge-capability-contract.md +61 -0
  47. package/docs/design/2026-09-16-messaging-capability-contract.md +59 -0
  48. package/docs/design/2026-09-16-portable-migration-evidence.md +156 -0
  49. package/docs/design/2026-09-16-portable-onboarding.md +177 -0
  50. package/docs/design/2026-09-16-prepare-request-transport.md +26 -0
  51. package/docs/design/2026-09-16-provider-binding-codecs.md +98 -0
  52. package/docs/design/2026-09-16-provider-binding-wire.md +247 -0
  53. package/docs/design/2026-09-17-capability-helper-input-contract.md +95 -0
  54. package/docs/design/2026-09-17-captured-backend-parity.md +53 -0
  55. package/docs/design/2026-09-17-captured-native-start.md +58 -0
  56. package/docs/design/2026-09-17-portable-boundary-hookup.md +19 -0
  57. package/docs/design/2026-09-17-portable-boundary-resources.md +52 -0
  58. package/docs/design/2026-09-17-public-captured-start.md +108 -0
  59. package/docs/design/2026-09-17-public-prepare-request.md +90 -0
  60. package/docs/design/2026-09-18-captured-pi-host.md +205 -0
  61. package/docs/design/2026-09-18-first-cut-release-checklist.md +131 -0
  62. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +60 -0
  63. package/docs/desktop-cli-api.md +5 -2
  64. package/docs/execution-capsule.schema.json +108 -0
  65. package/docs/execution-targets.md +20 -7
  66. package/docs/oats-lock-v3.schema.json +7 -0
  67. package/docs/oats-member.schema.json +38 -0
  68. package/docs/oats-workspace.schema.json +68 -0
  69. package/docs/portable.schema.json +2512 -0
  70. package/docs/provider-check-input.schema.json +7 -0
  71. package/docs/release-notes/v0.23.2.md +49 -0
  72. package/docs/release-notes/v0.24.0.md +104 -0
  73. package/docs/schedules.md +126 -14
  74. package/docs/soul.schema.json +82 -0
  75. package/injects/oats-portable.md +17 -0
  76. package/injects/portable-instance-boundary.md +39 -0
  77. package/injects/portable-work-directory.md +29 -0
  78. package/lib/artifact-approvals.mjs +120 -0
  79. package/lib/artifact-tree.mjs +141 -0
  80. package/lib/capability-artifacts.mjs +179 -0
  81. package/lib/capability-execution.mjs +15 -0
  82. package/lib/capability-inputs.mjs +39 -0
  83. package/lib/capability-provenance.mjs +231 -0
  84. package/lib/captured-action-shape.mjs +21 -0
  85. package/lib/captured-admission-shape.mjs +20 -0
  86. package/lib/captured-binding-file.mjs +36 -0
  87. package/lib/captured-dispatch.mjs +66 -0
  88. package/lib/captured-instance-index.mjs +277 -0
  89. package/lib/captured-invocation-context.mjs +130 -0
  90. package/lib/captured-launch-request.mjs +46 -0
  91. package/lib/captured-operation-process.mjs +15 -0
  92. package/lib/captured-pi-custody.mjs +29 -0
  93. package/lib/captured-pi-host.mjs +167 -0
  94. package/lib/captured-pi-outcome.mjs +172 -0
  95. package/lib/captured-resolutions.mjs +275 -0
  96. package/lib/captured-scaffold.mjs +87 -0
  97. package/lib/captured-selector.mjs +28 -0
  98. package/lib/captured-session-backend.mjs +52 -0
  99. package/lib/captured-source-receipt-file.mjs +72 -0
  100. package/lib/config-data.mjs +104 -0
  101. package/lib/core.mjs +918 -562
  102. package/lib/errors.mjs +7 -0
  103. package/lib/helper-injection-policy.mjs +98 -0
  104. package/lib/herdr.mjs +18 -7
  105. package/lib/instruction-composition.mjs +31 -0
  106. package/lib/legacy-lock-codec.mjs +106 -0
  107. package/lib/manifest-settings.mjs +84 -0
  108. package/lib/package-closure.mjs +48 -0
  109. package/lib/package-materialization.mjs +83 -0
  110. package/lib/pi-sdk-host.mjs +229 -0
  111. package/lib/portable-artifacts.mjs +115 -0
  112. package/lib/portable-choices.mjs +82 -0
  113. package/lib/portable-composition.mjs +136 -0
  114. package/lib/portable-digest.mjs +105 -0
  115. package/lib/portable-files.mjs +26 -0
  116. package/lib/portable-identity.mjs +40 -0
  117. package/lib/portable-lock.mjs +117 -0
  118. package/lib/portable-migration-artifacts.mjs +135 -0
  119. package/lib/portable-migration-evidence.mjs +305 -0
  120. package/lib/portable-migration-store.mjs +199 -0
  121. package/lib/portable-migration.mjs +104 -0
  122. package/lib/portable-onboarding-acceptance.mjs +66 -0
  123. package/lib/portable-onboarding-request.mjs +49 -0
  124. package/lib/portable-onboarding.mjs +230 -0
  125. package/lib/portable-package-preparation.mjs +188 -0
  126. package/lib/portable-policy.mjs +44 -0
  127. package/lib/portable-shape.mjs +35 -0
  128. package/lib/portable-soul.mjs +38 -0
  129. package/lib/portable-state.mjs +80 -0
  130. package/lib/portable-values.mjs +181 -0
  131. package/lib/prepare-composition.mjs +151 -0
  132. package/lib/prepared-bindings.mjs +78 -0
  133. package/lib/prepared-resources.mjs +127 -0
  134. package/lib/provider-binding-broker.mjs +59 -0
  135. package/lib/provider-binding-wire.mjs +110 -0
  136. package/lib/provider-binding.mjs +22 -0
  137. package/lib/repository-observation.mjs +226 -0
  138. package/lib/resolution-shape.mjs +393 -0
  139. package/lib/schedule-capsule.mjs +206 -0
  140. package/lib/schedule.mjs +259 -38
  141. package/lib/servers.mjs +15 -0
  142. package/lib/soul-constraints.mjs +40 -0
  143. package/lib/source-projection.mjs +84 -0
  144. package/lib/source-spec.mjs +189 -0
  145. package/lib/workspace-definition.mjs +126 -0
  146. package/lib/workspace-discovery.mjs +146 -0
  147. package/package-catalog.json +1 -1
  148. package/package.json +3 -2
  149. package/packages/record/lib/capture-cc.mjs +14 -6
  150. package/packages/record/lib/formats.mjs +14 -3
  151. package/packages/record/lib/native-history.mjs +277 -7
  152. package/packages/record/lib/session-snapshot.mjs +25 -5
  153. package/packages/record/lib/sessions-for-home.mjs +30 -13
  154. package/skills/oats/SKILL.md +12 -7
  155. package/skills/oats-config/SKILL.md +12 -9
  156. package/skills/oats-packages/SKILL.md +12 -8
  157. package/skills/oats-portable/SKILL.md +116 -0
  158. package/skills/oats-portable-artifacts/SKILL.md +63 -0
  159. package/skills/oats-portable-setup/SKILL.md +69 -0
@@ -2,10 +2,12 @@
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { fs, join, dirname, resolve, readJSON, save, safePath, cliPath, oats, fail, unlock } from '../lib/io.mjs';
4
4
  import { loadBindings, declaration, splitRef } from '../lib/config.mjs';
5
- import { register, homeSource, loadSource, loadStatus, saveStatus, updateStatus, capture, scheduleSource, service, markerPath, views } from '../lib/sources.mjs';
6
- import { runSource, complete, retry, readRun } from '../lib/worker.mjs';
5
+ import { register, registerCaptured, loadInvocationSourceReceipt, homeSource, loadSource, loadStatus, saveStatus, updateStatus, capture, scheduleSource, service, markerPath, views } from '../lib/sources.mjs';
6
+ import { runSource, complete, retry, readRun, requireQualifiedHelper } from '../lib/worker.mjs';
7
7
  import { initBase, migrate, deliverMigration, cutoverMigration, migrateSource } from '../lib/migration.mjs';
8
8
  import { inspect } from '../lib/inspection.mjs';
9
+ import { loadInvocationKnowledgeBinding } from '../lib/binding-wire.mjs';
10
+ import { loadCapturedOkfInvocation, loadOkfSourceReceiptInput, assertOkfInvocationAction, requireOkfAdmittedAction, assertOkfSourceContext, assertOkfRegisteredSourceReplay } from '../lib/invocation-context.mjs';
9
11
  const HELP=`oats okf inspect [--home PATH | --source FILE] [--json]
10
12
  oats okf harvest [--home PATH] [--no-launch] [--json]
11
13
  oats okf run-source --source FILE [--manual] [--no-launch] [--json]
@@ -19,6 +21,10 @@ oats okf migrate --legacy PATH --base ALIAS --node NODE --output PATH [--json]
19
21
  oats okf migrate --deliver FILE | --cutover FILE --soul-dir PATH [--json]
20
22
  oats okf migrate --source-home PATH [--json]
21
23
  oats okf unlock --lock PATH --token TOKEN [--json]
24
+ Captured workers use oats operation run knowledge:harvest with SOURCE --deployment/--resolution/--home,
25
+ --arg native-request=ABS_BACKEND_ONLY_JSON and optional --arg worker-mode=prepare|launch.
26
+ Raw captured harvest/run-source/scheduler/rejudge remain held; completed runs use SOURCE complete/retry.
27
+ Unknown scaffold/native outcomes are retained, never automatically re-scaffolded or redispatched.
22
28
  Explicit retry --run ID requires --rejudge; add --launch only for operator-approved launch.
23
29
  Closed-PR recovery uses retained evidence and a fresh run; complete its returned ID.
24
30
  Settled destinations and old proposals/receipts are preserved; another active run blocks recovery.
@@ -40,7 +46,7 @@ else {
40
46
  }
41
47
  const accepted={
42
48
  spawn:[],retire:['home'], 'soul-scaffold':[],
43
- harvest:['home','no-launch'],inspect:['home','source'],
49
+ harvest:['home','no-launch','native-request','worker-mode'],inspect:['home','source'],
44
50
  'run-source':['source','manual','no-launch'],complete:['source','run','judgment'],
45
51
  retry:['source','run','rejudge','launch','adopt-home'],read:['home','source','base','path'],refresh:['home','source'],
46
52
  setup:['source','enable','disable','install-host'],init:['base','nodes','output','confirm'],
@@ -48,21 +54,75 @@ else {
48
54
  };
49
55
  for(const k of Object.keys(flags)) if(!['json','soul',...(accepted[event] || [])].includes(k)) fail('E_USAGE',`unknown flag --${k} for ${event}`);
50
56
  if(flags.source && flags.home) fail('E_USAGE','choose source descriptor OR home');
51
- const home=resolve(flags.home || process.env.OATS_INSTANCE_HOME || process.env.OATS_HOME || process.cwd());
52
- const src=()=>flags.source?loadSource(resolve(flags.source)):homeSource(home);
57
+ const execution=Object.hasOwn(process.env,'OATS_INVOCATION_CONTEXT_FILE')?loadCapturedOkfInvocation():null;
58
+ const invocation=execution?{kind:'captured',binding:execution.binding}:loadInvocationKnowledgeBinding(),captured=invocation.kind==='captured';
59
+ if(execution) assertOkfInvocationAction(execution.context,event,readJSON(new URL('../oats.json',import.meta.url)));
60
+ // Only an actual admitted, instance-scoped kernel operation can create a
61
+ // captured worker. Raw commands/null intents and legacy ingress stay closed.
62
+ const capturedHarvest=captured && execution?.context.action.kind==='operation' && execution.context.action.slot==='knowledge' && execution.context.action.name==='harvest';
63
+ if(captured && event==='harvest') {
64
+ if(!capturedHarvest)requireQualifiedHelper({providerBinding:invocation.binding});
65
+ requireOkfAdmittedAction(execution.context);
66
+ if(!flags['native-request']||!['prepare','launch'].includes(flags['worker-mode']||'launch'))fail('E_CAPTURED_HELPER','captured harvest needs explicit native-request and supported worker-mode');
67
+ }
68
+ const unsupportedCaptured=new Set(['setup','init','migrate','unlock']);
69
+ if(captured && unsupportedCaptured.has(event)) fail('E_MIGRATION',`captured ${event} is not supported; use an explicit operator administration path`);
70
+ const target=execution?.context.instance;
71
+ if(target && flags.home && resolve(flags.home)!==target.home) fail('E_INVOCATION','captured invocation target differs from requested home');
72
+ const home=target?.home || resolve(flags.home || (execution?process.cwd():process.env.OATS_INSTANCE_HOME || process.env.OATS_HOME || process.cwd()));
73
+ if(execution && ['spawn','retire'].includes(event)) requireOkfAdmittedAction(execution.context);
74
+ const sourceReceipt=execution?loadOkfSourceReceiptInput(execution):loadInvocationSourceReceipt(home);
75
+ if(sourceReceipt.mode==='captured' && !['spawn','retire'].includes(event)) fail('E_SOURCE','captured source receipt is valid only for lifecycle hooks');
76
+ const src=()=>{
77
+ try {
78
+ const source=flags.source?loadSource(resolve(flags.source)):homeSource(home);
79
+ if(execution) assertOkfSourceContext(source,execution.context,execution.binding);
80
+ else if(captured) assertOkfRegisteredSourceReplay(source,invocation.binding);
81
+ else if(source.providerBinding && event!=='inspect') fail('E_INVOCATION','captured source execution requires its selected binding');
82
+ return source;
83
+ } catch(error) {if(captured && ['ENOENT','ENOTDIR'].includes(error.code)) fail('E_SOURCE','captured command requires its durable registered source descriptor');throw error;}
84
+ };
85
+ // Deliberate old registered-source replay is a separate qualified contract,
86
+ // never a way to create a source or synthesize generic admission. A present
87
+ // invalid/unadmitted generic invocation cannot enter this compatibility path.
88
+ if(captured && !execution && Object.hasOwn(accepted,event) && event!=='soul-scaffold') {
89
+ if(!fs.existsSync(flags.source?resolve(flags.source):markerPath(home))) fail('E_ADMISSION','new captured registration requires generic admitted invocation inputs');
90
+ src();
91
+ }
92
+ if(['spawn','retire'].includes(event)) {
93
+ // A generic intent authorizes its action, not missing source/role input.
94
+ // Absence may replay an already qualified source, never create one or
95
+ // infer a helper skip from an ambient kind/name/knowledge-slot heuristic.
96
+ if(captured && sourceReceipt.mode!=='captured' && !fs.existsSync(markerPath(home))) fail('E_SOURCE','new captured registration requires SourceReceipt1 input authority');
97
+ if(execution && fs.existsSync(markerPath(home))) src();
98
+ if(!captured && fs.existsSync(join(home,'instance.json')) && Object.hasOwn(readJSON(safePath(join(home,'instance.json'))),'executionBinding')) fail('E_INVOCATION','captured home cannot use legacy lifecycle ingress');
99
+ }
100
+ // Scope commands have no kernel instance intent. They may finish only
101
+ // already retained runs under their exact source binding/descriptor, never
102
+ // allocate a new worker or infer an incarnation for a deleted source.
103
+ const retainedRun=s=>{
104
+ const id=event==='complete'?flags.run:loadStatus(s).activeRun;
105
+ if(!id) {if(event==='complete') fail('E_RUN','complete requires an existing --run');requireQualifiedHelper(s);}
106
+ readRun(s,id);return s;
107
+ };
53
108
  let result;
54
109
  if(event==='soul-scaffold') {
55
110
  // Souls are portable declarations, never an implicit knowledge store.
56
111
  result={meta:{scaffolded:false},brief:'OKF requires explicit external bindings and soul/okf.json before a working instance can spawn. Use init or migrate; no knowledge was created in this soul.'};
57
112
  } else if(event==='spawn') {
58
- const s=register(home);
113
+ const s=sourceReceipt.mode==='captured'?registerCaptured(home,sourceReceipt.receipt):register(home);
59
114
  if(s.skipped) result={meta:{memory:'none'},brief:'Service agent: follow your own task; no working-memory upkeep.'};
60
115
  else {
61
116
  const schedule=loadStatus(s).schedule.result;
62
117
  result={meta:{memory:'okf-v2',source:s.file,schedule},brief:`Knowledge is an immutable accepted snapshot at ./knowledge/. Read knowledge/view.json for base paths under knowledge/bases/<alias>/, then the indexes for ${[...new Set([...s.decl.owns,...s.decl.reads])].join(', ')}. Follow only relevant links. All configured bases are available. Use oats okf read for current accepted text; old views stay stable. Keep STATE.md/log.md/notes/ current; never edit knowledge.`};
63
118
  }
64
119
  } else if(event==='retire') {
65
- if(service(home)) result={meta:{retired:true}};
120
+ if(captured) {
121
+ let s;
122
+ if(sourceReceipt.mode==='captured') {s=registerCaptured(home,sourceReceipt.receipt);if(s.skipped) {result={meta:{retired:true,reason:'service'}};s=null;}}
123
+ else {if(!fs.existsSync(markerPath(home))) fail('E_MIGRATION','captured retire requires a durable registered source or explicit helper receipt');s=src();}
124
+ if(s) {scheduleSource(s);const r=capture(s,{final:true});result={meta:{retired:r.complete===true,source:s.file,capture:r},brief:'Final input is in durable custody. Delivery remains asynchronous.'};}
125
+ } else if(service(home)) result={meta:{retired:true}};
66
126
  else if(!fs.existsSync(markerPath(home))) {
67
127
  if(['STATE.md','log.md','notes','.okf-harvest-record.json','.okf-harvest-record.next.json'].some(p=>fs.existsSync(join(home,p)))) fail('E_MIGRATION','unregistered/legacy source has memory; explicitly migrate/register before retirement');
68
128
  result={meta:{retired:true,reason:'nothing-to-delete'}};
@@ -70,11 +130,19 @@ else {
70
130
  const s=src();scheduleSource(s);const r=capture(s,{final:true});result={meta:{retired:r.complete===true,source:s.file,capture:r},brief:'Final input is in durable custody. Delivery remains asynchronous.'};
71
131
  }
72
132
  } else if(event==='harvest') {
73
- const s=register(home);
74
- result=s.skipped?{status:'skipped',reason:'service'}:runSource(s,{manual:true,noLaunch:!!flags['no-launch']});
133
+ if(capturedHarvest) {
134
+ const s=src(); // existing registered descriptor ONLY; never new registration.
135
+ result=runSource(s,{manual:true,noLaunch:!!flags['no-launch']||flags['worker-mode']==='prepare',capturedInvocation:execution.context,nativeRequest:flags['native-request']});
136
+ } else {
137
+ if(flags['native-request']||flags['worker-mode'])fail('E_USAGE','native-request/worker-mode require an admitted captured operation');
138
+ // Snapshot absence does not turn a persisted captured source into legacy.
139
+ if(fs.existsSync(markerPath(home))) requireQualifiedHelper(src());
140
+ const s=register(home);
141
+ result=s.skipped?{status:'skipped',reason:'service'}:runSource(s,{manual:true,noLaunch:!!flags['no-launch']});
142
+ }
75
143
  } else if(event==='run-source') result=runSource(src(),{manual:!!flags.manual,noLaunch:!!flags['no-launch']});
76
- else if(event==='complete') result=complete(src(),flags.run,flags.judgment && resolve(flags.judgment));
77
- else if(event==='retry') result=retry(src(),{run:flags.run,rejudge:!!flags.rejudge,launch:!!flags.launch,adoptHome:flags['adopt-home']});
144
+ else if(event==='complete') {const s=src();if(captured) retainedRun(s);result=complete(s,flags.run,flags.judgment && resolve(flags.judgment));}
145
+ else if(event==='retry') {const s=src();if(captured && !flags.run && !flags.rejudge && !flags.launch && !flags['adopt-home']) retainedRun(s);result=retry(s,{run:flags.run,rejudge:!!flags.rejudge,launch:!!flags.launch,adoptHome:flags['adopt-home']});}
78
146
  else if(event==='inspect') result=inspect(src());
79
147
  else if(event==='setup') {
80
148
  const s=src();if(flags.enable && flags.disable) fail('E_USAGE','choose enable or disable');
@@ -0,0 +1,268 @@
1
+ import { TextDecoder } from 'node:util';
2
+ import { validateInvocationShape } from './invocation-shape.mjs';
3
+ import { tmpdir } from 'node:os';
4
+ import { isAbsolute, join, resolve } from 'node:path';
5
+ import { fs, safePath } from './io.mjs';
6
+ import {
7
+ KNOWLEDGE_CONTRACT,
8
+ KNOWLEDGE_CONTRACT_VERSION,
9
+ bindKnowledgeDomain,
10
+ checkKnowledgeRuntime,
11
+ normalizeKnowledgeBindingCandidates,
12
+ normalizeKnowledgeDeclaration,
13
+ renderKnowledgeRuntime,
14
+ sameJson,
15
+ } from './portable-binding.mjs';
16
+ import { validateBindings } from './config.mjs';
17
+ import { stageBase, validateBase } from './stores.mjs';
18
+
19
+ export const BINDING_WIRE_LIMITS=Object.freeze({bytes:1024*1024,depth:32,entries:16384});
20
+ const CAPABILITY='oats.okf',SLOT='knowledge';
21
+ const phases=new Set(['normalize','bind','check']);
22
+ const unsupportedCapturedCommands=new Set(['setup','init','migrate','unlock']);
23
+ const declarationKinds=new Set(['soul','workspace','adoption','operator']);
24
+ const errorCodes=new Set(['needs-configuration','requirement-conflict','invalid-binding','authorization-required','host-requirement-missing','provider-unavailable','provider-not-qualified']);
25
+ const obj=value=>value!==null && typeof value==='object' && !Array.isArray(value);
26
+ const wireError=code=>{throw Object.assign(new Error(code),{wireCode:code});};
27
+ function keys(value,allowed,required,label) {
28
+ if(!obj(value)) wireError('invalid-binding');
29
+ for(const key of Object.keys(value)) if(!allowed.includes(key)) wireError('invalid-binding');
30
+ for(const key of required) if(!Object.hasOwn(value,key)) wireError('invalid-binding');
31
+ return value;
32
+ }
33
+ function absolute(value) {return typeof value==='string' && isAbsolute(value) && resolve(value)===value;}
34
+
35
+ class StrictJsonParser {
36
+ constructor(text,{depth,entries}) {this.text=text;this.maxDepth=depth;this.maxEntries=entries;this.at=0;this.entries=0;}
37
+ whitespace() {while(/[\u0009\u000a\u000d\u0020]/.test(this.text[this.at] || '')) this.at++;}
38
+ count() {if(++this.entries>this.maxEntries) wireError('invalid-binding');}
39
+ string() {
40
+ if(this.text[this.at]!=='"') wireError('invalid-binding');
41
+ const start=this.at++;
42
+ while(this.at<this.text.length) {
43
+ const code=this.text.charCodeAt(this.at++);
44
+ if(code===0x22) {try{return JSON.parse(this.text.slice(start,this.at));}catch{wireError('invalid-binding');}}
45
+ if(code<0x20) wireError('invalid-binding');
46
+ if(code===0x5c) {
47
+ const escaped=this.text[this.at++];
48
+ if(escaped==='u') {if(!/^[0-9a-fA-F]{4}$/.test(this.text.slice(this.at,this.at+4))) wireError('invalid-binding');this.at+=4;}
49
+ else if(!'"\\/bfnrt'.includes(escaped || '')) wireError('invalid-binding');
50
+ }
51
+ }
52
+ wireError('invalid-binding');
53
+ }
54
+ value(depth=1) {
55
+ this.whitespace();const char=this.text[this.at];
56
+ if(char==='"') return this.string();
57
+ if(char==='{') {
58
+ if(depth>this.maxDepth) wireError('invalid-binding');this.at++;this.whitespace();
59
+ const result=Object.create(null),seen=new Set();if(this.text[this.at]==='}') {this.at++;return result;}
60
+ while(true) {
61
+ this.whitespace();const key=this.string();if(seen.has(key)) wireError('invalid-binding');seen.add(key);this.count();
62
+ this.whitespace();if(this.text[this.at++]!==':') wireError('invalid-binding');result[key]=this.value(depth+1);this.whitespace();
63
+ const next=this.text[this.at++];if(next==='}') return result;if(next!==',') wireError('invalid-binding');
64
+ }
65
+ }
66
+ if(char==='[') {
67
+ if(depth>this.maxDepth) wireError('invalid-binding');this.at++;this.whitespace();
68
+ const result=[];if(this.text[this.at]===']') {this.at++;return result;}
69
+ while(true) {this.count();result.push(this.value(depth+1));this.whitespace();const next=this.text[this.at++];if(next===']') return result;if(next!==',') wireError('invalid-binding');}
70
+ }
71
+ for(const [token,value] of [['true',true],['false',false],['null',null]]) if(this.text.startsWith(token,this.at)) {this.at+=token.length;return value;}
72
+ const match=/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(this.text.slice(this.at));
73
+ if(!match) wireError('invalid-binding');this.at+=match[0].length;const number=Number(match[0]);if(!Number.isFinite(number)) wireError('invalid-binding');return number;
74
+ }
75
+ parse() {this.whitespace();const value=this.value();this.whitespace();if(this.at!==this.text.length) wireError('invalid-binding');return value;}
76
+ }
77
+
78
+ export function parseBindingJson(bytes,limits=BINDING_WIRE_LIMITS) {
79
+ if(!Buffer.isBuffer(bytes)) bytes=Buffer.from(bytes);
80
+ if(bytes.length>limits.bytes) wireError('invalid-binding');
81
+ let text;try{text=new TextDecoder('utf-8',{fatal:true}).decode(bytes);}catch{wireError('invalid-binding');}
82
+ return new StrictJsonParser(text,limits).parse();
83
+ }
84
+
85
+ function request(value,phase) {
86
+ keys(value,['schemaVersion','phase','slot','capability','settings','input'],['schemaVersion','phase','slot','capability','settings','input'],'binding request');
87
+ if(value.schemaVersion!==1 || value.phase!==phase || value.slot!==SLOT || value.capability!==CAPABILITY || !obj(value.settings)) wireError('invalid-binding');
88
+ return value;
89
+ }
90
+ function declaration(value) {
91
+ keys(value,['kind','value','origin','origins'],['kind','value','origin','origins'],'provider declaration');
92
+ if(!declarationKinds.has(value.kind) || !obj(value.value) || !obj(value.origin) || !obj(value.origins)) wireError('invalid-binding');
93
+ return value;
94
+ }
95
+ function contract(value,{required=false}={}) {
96
+ if(value===undefined) {if(required) wireError('needs-configuration');return null;}
97
+ if(!obj(value) || value.contract!==KNOWLEDGE_CONTRACT || value.version!==KNOWLEDGE_CONTRACT_VERSION) wireError('invalid-binding');
98
+ return value;
99
+ }
100
+ function runtimeSettings(settings) {
101
+ keys(settings,['bindings-file','state-dir','harvest-runtime','harvest-model'],[], 'OKF settings');
102
+ const descriptorFile=settings['bindings-file'],stateDir=settings['state-dir'],runtime=settings['harvest-runtime'],model=settings['harvest-model'] ?? null;
103
+ if(!absolute(descriptorFile) || !absolute(stateDir) || !['pi','claude','codex'].includes(runtime) || (model!==null && (typeof model!=='string' || !model.trim()))) wireError('needs-configuration');
104
+ return {descriptorFile,stateDir,execution:{runtime,model}};
105
+ }
106
+ function normalizePhase(req) {
107
+ keys(req.input,['declarations','context'],['declarations','context'],'normalize input');
108
+ if(!Array.isArray(req.input.declarations) || !obj(req.input.context)) wireError('invalid-binding');
109
+ const requirements=[],candidates=[];let domain=null;
110
+ for(const raw of req.input.declarations) {
111
+ const item=declaration(raw);
112
+ if(item.kind==='soul') {
113
+ const envelope=contract(item.value.knowledge,{required:true});
114
+ if(domain) wireError('requirement-conflict');
115
+ domain=normalizeKnowledgeDeclaration(envelope,{origin:item.origin,origins:item.origins,pointer:'/knowledge/payload'});
116
+ continue;
117
+ }
118
+ if(item.kind==='workspace') {
119
+ const stores=item.value.knowledge?.stores;
120
+ if(stores===undefined) continue;
121
+ if(!Array.isArray(stores)) wireError('invalid-binding');
122
+ stores.forEach((rawStore,index)=>{
123
+ if(!obj(rawStore)) wireError('invalid-binding');
124
+ if(rawStore.contract!==KNOWLEDGE_CONTRACT) return;
125
+ const store=contract(rawStore);
126
+ keys(store.payload,['bindings'],['bindings'],'workspace OKF payload');
127
+ candidates.push(...normalizeKnowledgeBindingCandidates({bindings:store.payload.bindings,kind:'workspace-default',origin:item.origin,origins:item.origins,pointer:`/knowledge/stores/${index}/payload/bindings`}));
128
+ });
129
+ continue;
130
+ }
131
+ if(item.value.bindings===undefined) continue;
132
+ candidates.push(...normalizeKnowledgeBindingCandidates({bindings:item.value.bindings,kind:item.kind==='adoption'?'import-adoption':'operator',origin:item.origin,origins:item.origins,pointer:'/bindings'}));
133
+ }
134
+ if(!domain) wireError('needs-configuration');
135
+ requirements.push(...domain.requirements);candidates.unshift(...domain.candidates);
136
+ return {requirements,candidates,model:{domain,runtime:runtimeSettings(req.settings)}};
137
+ }
138
+ function choiceMap(value) {
139
+ if(!obj(value)) wireError('invalid-binding');
140
+ for(const [key,choice] of Object.entries(value)) {
141
+ if(!key.startsWith('/bindings/knowledge/') || key.includes('//') || /~(?![01])/.test(key)) wireError('invalid-binding');
142
+ keys(choice,['value','selectedBy','constraints','considered'],['value','selectedBy','constraints','considered'],'knowledge choice');
143
+ if(!Array.isArray(choice.constraints) || !Array.isArray(choice.considered) || (choice.selectedBy!==null && !obj(choice.selectedBy))) wireError('invalid-binding');
144
+ }
145
+ return value;
146
+ }
147
+ function bindPhase(req) {
148
+ keys(req.input,['model','choices','context'],['model','choices','context'],'bind input');if(!obj(req.input.context)) wireError('invalid-binding');
149
+ keys(req.input.model,['domain','runtime'],['domain','runtime'],'OKF binding model');
150
+ keys(req.input.model.runtime,['stateDir','descriptorFile','execution'],['stateDir','descriptorFile','execution'],'OKF runtime model');
151
+ let bound;try{bound=bindKnowledgeDomain({model:req.input.model.domain,choices:choiceMap(req.input.choices)});}catch(error){if(/unresolved knowledge binding/.test(error.message)) wireError('needs-configuration');throw error;}
152
+ const runtime=renderKnowledgeRuntime({domain:bound.payload,...req.input.model.runtime});
153
+ return {payloadContract:bound.contract,payloadVersion:bound.version,payload:{...bound.payload,runtime,execution:req.input.model.runtime.execution},credentialRefs:bound.credentialRefs,provenance:bound.provenance};
154
+ }
155
+ function bindingPayload(binding) {
156
+ keys(binding,['schemaVersion','capability','payloadContract','payloadVersion','payload','credentialRefs','provenance'],['schemaVersion','capability','payloadContract','payloadVersion','payload','credentialRefs','provenance'],'provider binding');
157
+ if(binding.schemaVersion!==1 || binding.capability!==CAPABILITY || binding.payloadContract!==KNOWLEDGE_CONTRACT || binding.payloadVersion!==KNOWLEDGE_CONTRACT_VERSION || !Array.isArray(binding.provenance)) wireError('invalid-binding');
158
+ if(!obj(binding.credentialRefs) || Object.keys(binding.credentialRefs).length) wireError('invalid-binding');
159
+ keys(binding.payload,['owner','stores','reads','owns','runtime','execution'],['owner','stores','reads','owns','runtime','execution'],'OKF binding payload');
160
+ const domain={owner:binding.payload.owner,stores:binding.payload.stores,reads:binding.payload.reads,owns:binding.payload.owns};
161
+ const runtime=renderKnowledgeRuntime({domain,stateDir:binding.payload.runtime?.bindings?.stateDir,descriptorFile:binding.payload.runtime?.descriptorFile});
162
+ if(!sameJson(runtime,binding.payload.runtime)) wireError('invalid-binding');
163
+ keys(binding.payload.execution,['runtime','model'],['runtime','model'],'OKF worker execution');
164
+ if(!['pi','claude','codex'].includes(binding.payload.execution.runtime) || (binding.payload.execution.model!==null && (typeof binding.payload.execution.model!=='string' || !binding.payload.execution.model.trim()))) wireError('invalid-binding');
165
+ return {domain,runtime,execution:binding.payload.execution};
166
+ }
167
+
168
+ /** Pure projection for durable source registration and later source-free work. */
169
+ export function sourceRuntimeFromKnowledgeBinding(binding) {
170
+ const {domain,runtime,execution}=bindingPayload(binding);
171
+ return {owner:domain.owner,bindings:{file:runtime.descriptorFile,...runtime.bindings},decl:runtime.declaration,execution:{...execution}};
172
+ }
173
+
174
+ const invocationError=()=>{throw Object.assign(new Error('invalid captured provider binding snapshot'),{code:'E_BINDING'});};
175
+ export function readPrivateInvocationJson(file) {
176
+ if(!absolute(file)) invocationError();
177
+ let fd;
178
+ try {
179
+ if(safePath(file)!==file) invocationError();
180
+ const before=fs.lstatSync(file),uid=typeof process.getuid==='function'?process.getuid():null;
181
+ if(!before.isFile() || before.nlink!==1 || before.size>BINDING_WIRE_LIMITS.bytes || (before.mode&0o077)!==0 || (uid!==null && before.uid!==uid)) invocationError();
182
+ fd=fs.openSync(file,fs.constants.O_RDONLY|fs.constants.O_NOFOLLOW|fs.constants.O_NONBLOCK);
183
+ const opened=fs.fstatSync(fd);
184
+ if(!opened.isFile() || opened.nlink!==1 || opened.dev!==before.dev || opened.ino!==before.ino || opened.size>BINDING_WIRE_LIMITS.bytes || (opened.mode&0o077)!==0 || (uid!==null && opened.uid!==uid)) invocationError();
185
+ const bytes=Buffer.alloc(Math.min(BINDING_WIRE_LIMITS.bytes+1,opened.size+1));let length=0;
186
+ while(length<bytes.length) {const count=fs.readSync(fd,bytes,length,bytes.length-length,null);if(!count) break;length+=count;}
187
+ const after=fs.fstatSync(fd);
188
+ if(length>BINDING_WIRE_LIMITS.bytes || after.size!==opened.size || after.mtimeMs!==opened.mtimeMs) invocationError();
189
+ return parseBindingJson(bytes.subarray(0,length));
190
+ } catch(error) {if(error?.code==='E_BINDING') throw error;invocationError();}
191
+ finally {if(fd!==undefined) fs.closeSync(fd);}
192
+ }
193
+
194
+ /** Load only the parent-owned transient ProviderBinding1 snapshot when present.
195
+ * Absence is an explicit legacy mode; every present-file defect fails closed. */
196
+ export function loadInvocationKnowledgeBinding(env=process.env) {
197
+ if(!Object.hasOwn(env,'OATS_BINDING_FILE')) return {kind:'legacy'};
198
+ const file=env.OATS_BINDING_FILE,binding=readPrivateInvocationJson(file);
199
+ let runtime;try{runtime=sourceRuntimeFromKnowledgeBinding(binding);}catch{invocationError();}
200
+ return {kind:'captured',file,binding,runtime};
201
+ }
202
+ function problem(code) {return {code};}
203
+ function providerActionName(action) {
204
+ if(action.kind==='operation' && action.slot===SLOT) return action.name;
205
+ if(action.kind!=='command') return null;
206
+ if(action.namespace==='okf' || action.capability===CAPABILITY) return action.name;
207
+ if(typeof action.name==='string' && action.name.startsWith('okf:')) return action.name.slice(4);
208
+ return null;
209
+ }
210
+ function checkPhase(req) {
211
+ keys(req.input,['binding','context','action','invocation'],['binding','context','action'],'check input');
212
+ if(!obj(req.input.context) || !obj(req.input.action)) wireError('invalid-binding');
213
+ if(Object.hasOwn(req.input,'invocation')) validateInvocationShape(req.input.invocation,{capability:CAPABILITY,context:req.input.context,action:req.input.action});
214
+ const {runtime}=bindingPayload(req.input.binding),action=req.input.action,name=providerActionName(action);
215
+ const harvestInvocation=req.input.invocation;
216
+ const admittedHarvest=name==='harvest' && action.kind==='operation' && action.slot==='knowledge' && action.name==='harvest'
217
+ && harvestInvocation?.subject.kind==='persistent' && harvestInvocation.instance!==null && !!harvestInvocation.intent;
218
+ if(name && (unsupportedCapturedCommands.has(name) || name==='run-source' || (name==='harvest' && !admittedHarvest))) return {status:'needs-configuration',problems:[problem('provider-not-qualified')]};
219
+ if(action.kind==='hook' && action.name==='soul-scaffold') return {status:'ready',problems:[]};
220
+ let bindings;try{bindings=validateBindings(runtime.bindings,runtime.descriptorFile);}catch{return {status:'needs-configuration',problems:[problem('needs-configuration')]};}
221
+ const accepted={},gitBases=Object.entries(bindings.bases).filter(([,base])=>base.kind==='git');
222
+ if(gitBases.length>64) return {status:'unavailable',problems:[problem('provider-not-qualified')]};
223
+ let scratch=null;
224
+ try {
225
+ if(gitBases.length) scratch=fs.mkdtempSync(join(fs.realpathSync(tmpdir()),'oats-okf-binding-check-'));
226
+ for(const [alias,base] of Object.entries(bindings.bases)) accepted[alias]=(base.kind==='directory'?validateBase(base.path,base):stageBase(base,join(scratch,alias))).meta;
227
+ checkKnowledgeRuntime({rendered:runtime,accepted});
228
+ } catch(error) {
229
+ if(error.code==='E_COMMAND') return {status:'unavailable',problems:[problem('provider-unavailable')]};
230
+ if(['E_OWNER','E_BASE','E_VALIDATION','E_DIRECTORY_GIT','E_CONFIRM'].includes(error.code)) return {status:'needs-configuration',problems:[problem('provider-not-qualified')]};
231
+ return {status:'unavailable',problems:[problem('provider-unavailable')]};
232
+ } finally {
233
+ if(scratch) fs.rmSync(scratch,{recursive:true,force:true});
234
+ }
235
+ return {status:'ready',problems:[]};
236
+ }
237
+
238
+ export function handleBindingRequest(phase,value) {
239
+ if(!phases.has(phase)) wireError('invalid-binding');
240
+ const req=request(value,phase);
241
+ if(phase==='normalize') return normalizePhase(req);
242
+ if(phase==='bind') return bindPhase(req);
243
+ return checkPhase(req);
244
+ }
245
+ function response(phase,body) {return {schemaVersion:1,phase,slot:SLOT,capability:CAPABILITY,...body};}
246
+ function errorCode(error) {if(errorCodes.has(error?.wireCode)) return error.wireCode;if(error?.code==='E_OWNER') return 'requirement-conflict';return 'invalid-binding';}
247
+ function enforceOutputLimits(value,depth=1,state={entries:0}) {
248
+ if(depth>BINDING_WIRE_LIMITS.depth) wireError('provider-not-qualified');
249
+ if(value===null || typeof value!=='object') return;
250
+ for(const child of Array.isArray(value)?value:Object.values(value)) {
251
+ if(++state.entries>BINDING_WIRE_LIMITS.entries) wireError('provider-not-qualified');
252
+ enforceOutputLimits(child,depth+1,state);
253
+ }
254
+ }
255
+ export async function runBindingWire(phase,input=process.stdin,output=process.stdout) {
256
+ let answer;
257
+ try {
258
+ const chunks=[];let length=0;
259
+ for await(const chunk of input) {const bytes=Buffer.from(chunk);length+=bytes.length;if(length>BINDING_WIRE_LIMITS.bytes) wireError('invalid-binding');chunks.push(bytes);}
260
+ const result=handleBindingRequest(phase,parseBindingJson(Buffer.concat(chunks,length)));
261
+ answer=response(phase,{ok:true,result});
262
+ } catch(error) {answer=response(phases.has(phase)?phase:'check',{ok:false,error:{code:errorCode(error)}});}
263
+ let bytes;
264
+ try {enforceOutputLimits(answer);bytes=Buffer.from(JSON.stringify(answer)+'\n');if(bytes.length>BINDING_WIRE_LIMITS.bytes) wireError('provider-not-qualified');}
265
+ catch {bytes=Buffer.from(JSON.stringify(response(phases.has(phase)?phase:'check',{ok:false,error:{code:'provider-not-qualified'}}))+'\n');}
266
+ output.write(bytes);
267
+ return answer.ok;
268
+ }
@@ -0,0 +1,101 @@
1
+ // Captured worker PUBLIC consumer. No kernel imports/index reads, legacy source
2
+ // discovery, fake admission, auth handling, backend defaults or history repair.
3
+ import {fs,join,resolve,safePath,exec,cliPath,hash,fail} from './io.mjs';
4
+ import {isAbsolute} from 'node:path';
5
+ import {parseBindingJson,BINDING_WIRE_LIMITS} from './binding-wire.mjs';
6
+ import {assertOkfSourceContext,requireOkfAdmittedAction} from './invocation-context.mjs';
7
+ import {sameInvocationJson as same} from './invocation-shape.mjs';
8
+ export const CAPTURED_WORKER_KEY='oats.okf:memory-harvest';
9
+ const obj=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
10
+ const abs=v=>typeof v==='string'&&isAbsolute(v)&&resolve(v)===v&&!v.includes('\0');
11
+ const uuid=v=>typeof v==='string'&&/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(v);
12
+ export const capturedSource=s=>['providerBinding','executionBinding','registration'].some(k=>Object.hasOwn(s,k));
13
+ function binding(v){if(!obj(v)||v.schemaVersion!==1||!abs(v.deployment)||v.resolution?.schemaVersion!==1||!/^sha256-[a-f0-9]{64}$/.test(v.resolution.id))fail('E_CAPTURED_HELPER','invalid public execution binding');return v;}
14
+ export function workerCallEnvironment(inherited){
15
+ // Preserve native HOME/profile/auth/Git helpers and the active OATS data
16
+ // scope, not invoking instance selectors (including legacy PI_AGENT aliases).
17
+ const env={...inherited};for(const key of Object.keys(env))if(/^(OATS_(?!HOME_DIR$)|OAS_|PI_AGENT_)/.test(key)||key==='PI_AGENTS_ROOT')delete env[key];
18
+ return env;
19
+ }
20
+ export function publicWorkerCall(source,args){
21
+ let text,status=0;
22
+ const env=workerCallEnvironment(process.env);
23
+ try{text=exec(cliPath(),args,{cwd:source.context,env,timeout:90000});}
24
+ catch(error){text=error.stdout;status=error.status??null;}
25
+ let envelope;try{envelope=parseBindingJson(Buffer.from(text||''),BINDING_WIRE_LIMITS);}catch{fail('E_CAPTURED_HELPER_UNKNOWN','public helper outcome is unknown; retain run/home, never re-scaffold');}
26
+ if(envelope.schemaVersion!==1||typeof envelope.ok!=='boolean')fail('E_CAPTURED_HELPER_UNKNOWN','invalid public helper response');
27
+ if(status!==0||!envelope.ok){const error=Object.assign(new Error('public captured helper action refused or is uncertain; retain custody'),{code:'E_CAPTURED_HELPER_UNKNOWN',publicObservation:{status,envelope}});throw error;}
28
+ if(!obj(envelope.result))fail('E_CAPTURED_HELPER_UNKNOWN','public helper result missing');return envelope.result;
29
+ }
30
+ export function readWorkerEndpoint(file){
31
+ if(!abs(file))fail('E_CAPTURED_HELPER','captured harvest needs an explicit absolute native-request');safePath(file);let fd;
32
+ try{
33
+ fd=fs.openSync(file,fs.constants.O_RDONLY|fs.constants.O_NOFOLLOW|fs.constants.O_NONBLOCK);
34
+ const before=fs.fstatSync(fd);if(!before.isFile()||before.size>BINDING_WIRE_LIMITS.bytes)fail('E_CAPTURED_HELPER','invalid bounded native request');
35
+ const buffer=Buffer.alloc(before.size+1);let n=0,k;while(n<buffer.length&&(k=fs.readSync(fd,buffer,n,buffer.length-n,null)))n+=k;
36
+ const after=fs.fstatSync(fd);if(n!==before.size||before.size!==after.size||before.mtimeMs!==after.mtimeMs||before.ctimeMs!==after.ctimeMs)fail('E_CAPTURED_HELPER','native request changed');
37
+ const value=parseBindingJson(buffer.subarray(0,n),BINDING_WIRE_LIMITS);
38
+ if(!obj(value)||value.schemaVersion!==1||!obj(value.backend)||Object.keys(value).some(k=>!['schemaVersion','backend','stopGraceMs'].includes(k)))fail('E_CAPTURED_HELPER','worker native-request allows backend/stopGraceMs only; provider owns task');
39
+ const b=value.backend,allowed=b.backend==='tmux'?['backend','binary','socket','session']:b.backend==='herdr'?['backend','binary','socket','protocol']:[];
40
+ if(!allowed.length||Object.keys(b).length!==allowed.length||!allowed.every(k=>Object.hasOwn(b,k))||!abs(b.binary)||!abs(b.socket))fail('E_CAPTURED_HELPER','unsupported exact backend request');
41
+ if(b.backend==='tmux'&&(typeof b.session!=='string'||!b.session||/[\r\n\0]/.test(b.session)))fail('E_CAPTURED_HELPER','invalid tmux endpoint');
42
+ if(b.backend==='herdr'&&![20,22].includes(b.protocol))fail('E_CAPTURED_HELPER','unknown Herdr protocol');
43
+ if(value.stopGraceMs!==undefined&&(!Number.isSafeInteger(value.stopGraceMs)||value.stopGraceMs<1||value.stopGraceMs>300000))fail('E_CAPTURED_HELPER','invalid stop grace');
44
+ return JSON.parse(JSON.stringify(value));
45
+ }finally{if(fd!==undefined)fs.closeSync(fd);}
46
+ }
47
+ export function qualifyCapturedWorker(source,{context,nativeRequest}={},call=publicWorkerCall){
48
+ if(!context||source.registration?.schemaVersion!==1||source.registration.kind!=='captured')fail('E_CAPTURED_HELPER','captured worker needs registered SOURCE and current admitted operation');
49
+ requireOkfAdmittedAction(context);assertOkfSourceContext(source,context,source.providerBinding);
50
+ if(context.action.kind!=='operation'||context.action.slot!=='knowledge'||context.action.name!=='harvest'||context.subject.kind!=='persistent')fail('E_CAPTURED_HELPER','only admitted SOURCE knowledge:harvest may create a captured worker');
51
+ const manifest=JSON.parse(fs.readFileSync(new URL('../oats.json',import.meta.url),'utf8'));
52
+ if((manifest.requires||[]).some(r=>obj(r)&&r.runtime==='pi')||manifest.hooks?.launch)fail('E_CAPTURED_HELPER','required Pi plugin/launch behavior is not qualified by this captured slice');
53
+ const request=readWorkerEndpoint(nativeRequest),sourceBinding=binding(source.executionBinding);
54
+ const inspected=call(source,['inspect','--deployment',sourceBinding.deployment,'--resolution',sourceBinding.resolution.id,'--helper',CAPTURED_WORKER_KEY,'--json']);
55
+ return validateWorkerSelection(source,context,request,inspected);
56
+ }
57
+ export function validateWorkerSelection(source,context,request,inspected){
58
+ const selected=inspected?.helperSelection;
59
+ if(selected?.schemaVersion!==1||!same(selected.sourceExecutionBinding,source.executionBinding)||selected.workMode!=='directory'||!same(selected.context,context.context)||!same(selected.responsibleHuman,source.responsibleHuman))fail('E_CAPTURED_HELPER','helper SOURCE/context/human/work correspondence is unqualified');
60
+ const h=selected.helper,subject=h?.subject;
61
+ if(h?.key!==CAPTURED_WORKER_KEY||h.name!=='memory-harvest'||subject?.kind!=='helper'||subject.name!==h.name||subject.provider?.kind!=='capability'||subject.provider.capability!=='oats.okf'||!same(subject.provider,subject.definition?.owner))fail('E_CAPTURED_HELPER','wrong qualified helper identity');
62
+ const api={schemaVersion:1,api:{contract:'oats.captured-session',version:2,available:true,backends:['tmux','herdr']},readiness:{status:'not-checked'}};
63
+ if(!same(selected.launch,api)||!same(inspected.nativeSession,api))fail('E_CAPTURED_HELPER','unknown captured native public API');
64
+ // Exact retained model/runtime data is required; API availability alone is
65
+ // never qualification. Missing public selection stays closed, no private read.
66
+ const launch=selected.launchSelection;
67
+ if(!obj(launch)||launch.runtime!==source.execution.runtime||launch.runtime!=='pi'||typeof launch.model!=='string'||!launch.model.trim()||(source.execution.model!==null&&launch.model!==source.execution.model))fail('E_CAPTURED_HELPER','retained helper launch runtime/model missing or mismatched');
68
+ if(!Array.isArray(inspected.capabilities)||inspected.capabilities.length!==1||inspected.capabilities[0].id!=='oats.okf'||inspected.capabilities[0].approval!=='approved')fail('E_CAPTURED_HELPER','only the exact approved default-OKF helper profile is qualified in this slice');
69
+ if(binding(selected.executionBinding).deployment!==source.executionBinding.deployment||!same(inspected.resolution,selected.executionBinding.resolution))fail('E_CAPTURED_HELPER','helper inspection resolution/deployment differs');
70
+ return {schemaVersion:1,sourceBinding:JSON.parse(JSON.stringify(source.executionBinding)),helperBinding:JSON.parse(JSON.stringify(selected.executionBinding)),helper:JSON.parse(JSON.stringify(h)),request,requestHash:hash(request),sourceIntent:JSON.parse(JSON.stringify(context.intent)),sourceIncarnationId:context.instance.incarnationId,launchSelection:JSON.parse(JSON.stringify(launch))};
71
+ }
72
+ export function assertCapturedRun(source,run,plan){
73
+ const prior=run.capturedWorker;
74
+ if(!prior||prior.schemaVersion!==1||!same(prior.sourceBinding,source.executionBinding)||!same(prior.helperBinding,plan.helperBinding)||!same(prior.helper,plan.helper)||prior.sourceIncarnationId!==plan.sourceIncarnationId||prior.requestHash!==plan.requestHash||hash(prior.request)!==prior.requestHash||!same(prior.request,plan.request)||!same(prior.launchSelection,plan.launchSelection))fail('E_CAPTURED_HELPER','existing run authority/endpoint changed; no replacement worker');
75
+ if(['spawn-intent','scaffold-unknown','scaffolded','launch-intent','launch-unknown'].includes(run.status))fail('E_CAPTURED_HELPER_UNKNOWN','existing worker outcome requires explicit custody recovery; no duplicate dispatch');
76
+ }
77
+ export function capturedScaffold(source,run,home,call=publicWorkerCall){
78
+ const p=run.capturedWorker,b=p.helperBinding;
79
+ const result=call(source,['spawn',p.helper.name,'--deployment',b.deployment,'--resolution',b.resolution.id,'--home',home,'--no-launch','--json']);
80
+ if(result.home!==home||!uuid(result.incarnationId)||!same(result.executionBinding,b)||result.launchPending!==true||result.hooksPending!==false||result.cleanupRequired!==false||result.agent!==p.helper.name||result.work!=='directory'||typeof result.instance!=='string')fail('E_CAPTURED_HELPER_UNKNOWN','scaffold receipt lacks exact completed helper custody');
81
+ return result;
82
+ }
83
+ function directoryCustody(home){
84
+ const result={};for(const [key,path] of [['home',home],['work',join(home,'work')]]){safePath(path);const s=fs.lstatSync(path);if(!s.isDirectory()||!Number.isSafeInteger(s.dev)||!Number.isSafeInteger(s.ino))fail('E_WORKER','original worker directories unavailable');result[key]={dev:s.dev,ino:s.ino};}return result;
85
+ }
86
+ export function retainCapturedWorkerCustody(run,meta){
87
+ // Observe the EXISTING kernel home/work proof after the real public scaffold;
88
+ // do not mint a new incarnation or read/modify its private kernel index.
89
+ const actual=directoryCustody(run.worker.home);
90
+ if(!same(meta.captured?.custody,actual)||meta.incarnationId!==run.worker.incarnationId||!same(meta.executionBinding,run.capturedWorker.helperBinding))fail('E_WORKER','scaffold metadata/directory proof mismatch');
91
+ return JSON.parse(JSON.stringify(meta.captured.custody));
92
+ }
93
+ export function assertCapturedWorkerHome(source,run,meta,home){
94
+ const p=run.capturedWorker;if(!p||home!==run.worker.home||!same(p.sourceBinding,source.executionBinding)||!same(meta.executionBinding,p.helperBinding)||meta.incarnationId!==run.worker.incarnationId||meta.kind!=='helper'||meta.agent!==p.helper.name||!same(meta.captured?.custody,p.workerDirectoryCustody)||!same(directoryCustody(home),p.workerDirectoryCustody))fail('E_WORKER','captured worker incarnation/binding/subject/directory mismatch');
95
+ }
96
+ export function capturedStart(source,run,requestFile,call=publicWorkerCall){
97
+ const p=run.capturedWorker,b=p.sourceBinding;
98
+ const result=call(source,['session','start','--deployment',b.deployment,'--resolution',b.resolution.id,'--helper',p.helper.key,'--home',run.worker.home,'--request',requestFile,'--json']);
99
+ if(result.home!==run.worker.home||result.incarnationId!==run.worker.incarnationId||!same(result.executionBinding,p.helperBinding)||!same(result.sourceExecutionBinding,b)||!same(result.helper,p.helper)||result.dispatchAccepted!==true||result.intent?.incarnationId!==run.worker.incarnationId||typeof result.intent?.executionId!=='string'||result.runtime!==p.launchSelection.runtime||result.model!==p.launchSelection.model)fail('E_CAPTURED_HELPER_UNKNOWN','native result lacks exact helper dispatch custody');
100
+ return result;
101
+ }
@@ -7,7 +7,8 @@ function keys(value, allowed, label, code='E_CONFIG') {
7
7
  }
8
8
  export function settings() {
9
9
  const s = JSON.parse(process.env.OATS_SETTINGS || '{}');
10
- keys(s,['bindings-file','harvest-runtime','harvest-model'],'OATS_SETTINGS');
10
+ keys(s,['bindings-file','state-dir','harvest-runtime','harvest-model'],'OATS_SETTINGS');
11
+ if(s['state-dir']!==undefined && (typeof s['state-dir']!=='string' || !isAbsolute(s['state-dir']) || resolve(s['state-dir'])!==s['state-dir'])) fail('E_CONFIG','state-dir must be a normalized absolute path');
11
12
  if(s['harvest-runtime']!==undefined && !['pi','claude','codex'].includes(s['harvest-runtime'])) fail('E_CONFIG','invalid harvest-runtime');
12
13
  if(s['harvest-model']!==undefined && (typeof s['harvest-model']!=='string' || !s['harvest-model'].trim())) fail('E_CONFIG','harvest-model must be a nonempty string');
13
14
  return s;
@@ -1,5 +1,6 @@
1
1
  import { fs, join, dirname, safePath, fail, oats } from './io.mjs';
2
2
  import { markerPath, loadStatus } from './sources.mjs';
3
+ import { qualifiedSoulIdentity } from './source-contract.mjs';
3
4
 
4
5
  // Keep the v1 labeled-document contract, including its explicit per-document
5
6
  // preview cap. The JSON envelope itself must drain in full through stdout.
@@ -84,13 +85,27 @@ export function workingDocuments(source,status=loadStatus(source)) {
84
85
  if(error) fail(error.code==='E_PATH'?'E_PATH':'E_INSPECT_FAILED',error.message);
85
86
  return {liveMemory:{available:true,reason:'live',observedAt},documents};
86
87
  }
88
+ export function capturedAuthority(source) {
89
+ const fields=['registration','providerBinding','sourceIdentity','executionBinding','responsibleHuman'],present=fields.filter(key=>Object.hasOwn(source,key));
90
+ const base={schemaVersion:1};
91
+ if(!present.length) return {...base,registration:'legacy',capture:'unknown',migrationRequired:true,responsibleHuman:{status:'unknown'}};
92
+ const binding=source.executionBinding,identity=source.sourceIdentity,complete=source.registration?.schemaVersion===1 && source.registration.kind==='captured'
93
+ && source.providerBinding && typeof source.providerBinding==='object' && identity && typeof identity==='object' && !Array.isArray(identity)
94
+ && binding?.schemaVersion===1 && typeof binding.deployment==='string' && binding.resolution?.schemaVersion===1 && typeof binding.resolution.id==='string'
95
+ && Object.hasOwn(source,'responsibleHuman');
96
+ const invalid={...base,registration:'invalid',capture:'invalid',migrationRequired:true,responsibleHuman:{status:'unknown'}};
97
+ if(!complete) return invalid;
98
+ try {if(Buffer.byteLength(JSON.stringify({identity,binding}))>64*1024) return invalid;qualifiedSoulIdentity(identity);} catch {return invalid;}
99
+ return {...base,registration:'captured',capture:'recorded',migrationRequired:false,sourceIdentity:JSON.parse(JSON.stringify(identity)),executionBinding:JSON.parse(JSON.stringify(binding)),
100
+ responsibleHuman:{status:source.responsibleHuman===null?'disabled':'specified'}};
101
+ }
87
102
  export function inspect(source) {
88
103
  const status=loadStatus(source),working=workingDocuments(source,status);
89
104
  let health;try {health=oats(['schedule','list','--dir',source.context,'--json'],source.context).scheduler;} catch(e) {health={active:false,error:e.message};}
90
105
  const documents=[...working.documents,{label:'Durable processing receipts',kind:'text',path:join(dirname(source.file),'status.json'),text:JSON.stringify(status,null,2)}];
91
106
  return {
92
107
  summary:`OKF ${source.id}: ${status.captured.inputs.length-status.processed.length} unprocessed inputs; ${status.retired?'source retired':'source not retired'}; ${working.liveMemory.available?`${working.documents.length} working-memory documents`:`live memory unavailable (${working.liveMemory.reason})`}`,
93
- source:source.file,owns:source.decl.owns,reads:source.decl.reads,bases:source.bindings.bases,
108
+ source:source.file,owns:source.decl.owns,reads:source.decl.reads,bases:source.bindings.bases,authority:capturedAuthority(source),
94
109
  acceptedView:source.acceptedView,status,scheduler:health,liveMemory:working.liveMemory,documents
95
110
  };
96
111
  }