@milpa/comal 0.1.7 → 0.2.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 (134) hide show
  1. package/README.md +19 -1
  2. package/backend/contracts/remote-commands-v1.json +12 -0
  3. package/governance/chrome-task-v10.json +323 -0
  4. package/governance/chrome-task-v11.json +355 -0
  5. package/governance/chrome-task-v12.json +387 -0
  6. package/governance/chrome-task-v13.json +419 -0
  7. package/governance/chrome-task-v3.json +99 -0
  8. package/governance/chrome-task-v4.json +131 -0
  9. package/governance/chrome-task-v5.json +163 -0
  10. package/governance/chrome-task-v6.json +195 -0
  11. package/governance/chrome-task-v7.json +227 -0
  12. package/governance/chrome-task-v8.json +259 -0
  13. package/governance/chrome-task-v9.json +291 -0
  14. package/governance/source-task-v1.json +1 -0
  15. package/governance/source-task-v2.json +1 -0
  16. package/package.json +14 -1
  17. package/src/agent-recovery-proof.mjs +12 -1
  18. package/src/agent.mjs +94 -30
  19. package/src/audit.mjs +21 -6
  20. package/src/auto-guard.mjs +48 -1
  21. package/src/chrome-candidate-recovery-proof.mjs +47 -0
  22. package/src/chrome-recovery-proof.mjs +6 -2
  23. package/src/chrome-source-syntax.mjs +93 -0
  24. package/src/cli.mjs +158 -54
  25. package/src/codex-provider.mjs +3 -2
  26. package/src/connection-settings.mjs +204 -0
  27. package/src/context-pressure.mjs +2 -1
  28. package/src/durable-agent-runs.mjs +5 -11
  29. package/src/event-channels.mjs +8 -0
  30. package/src/file-recovery-proof.mjs +11 -3
  31. package/src/file-recovery.mjs +24 -0
  32. package/src/gemini-provider.mjs +6 -1
  33. package/src/git-agent-revision.mjs +12 -0
  34. package/src/git-agent-workspaces.mjs +143 -0
  35. package/src/git-integration-lineage.mjs +20 -0
  36. package/src/git-integration-operations.mjs +200 -0
  37. package/src/git-integration-recovery-proof.mjs +70 -0
  38. package/src/git-page-cli-bridge.mjs +61 -0
  39. package/src/git-page-evidence.mjs +30 -0
  40. package/src/git-page-identity.mjs +10 -0
  41. package/src/git-page-proof.mjs +96 -0
  42. package/src/git-page-settlement.mjs +21 -0
  43. package/src/git-page-verification.mjs +134 -0
  44. package/src/git-plumbing.mjs +212 -0
  45. package/src/git-process-worker.mjs +50 -0
  46. package/src/git-source.mjs +110 -0
  47. package/src/git-workspaces.mjs +519 -0
  48. package/src/goals.mjs +13 -7
  49. package/src/house.mjs +246 -20
  50. package/src/i18n/cli-es.mjs +4 -0
  51. package/src/i18n/es.mjs +35 -0
  52. package/src/judges.mjs +27 -11
  53. package/src/mcp-runtime.mjs +15 -3
  54. package/src/model-controls.mjs +11 -1
  55. package/src/model-deadline.mjs +4 -3
  56. package/src/native-refusal.mjs +38 -0
  57. package/src/onboarding.mjs +88 -24
  58. package/src/openai-provider.mjs +14 -7
  59. package/src/operations.mjs +9 -2
  60. package/src/output-recovery.mjs +48 -0
  61. package/src/peer-tools.mjs +18 -8
  62. package/src/permission-controls.mjs +232 -0
  63. package/src/permission-rules.mjs +312 -0
  64. package/src/permissions.mjs +113 -30
  65. package/src/plugin-catalog.mjs +84 -0
  66. package/src/plugin-config.mjs +48 -6
  67. package/src/plugin-controls.mjs +332 -0
  68. package/src/plugin-notices.mjs +88 -0
  69. package/src/plugin-trust.mjs +104 -0
  70. package/src/plugins/chrome-lab/bridge.mjs +38 -12
  71. package/src/plugins/chrome-lab/client.mjs +103 -25
  72. package/src/plugins/chrome-lab/diagnostics.mjs +19 -0
  73. package/src/plugins/chrome-lab/index.mjs +116 -26
  74. package/src/plugins/chrome-lab/project.mjs +30 -2
  75. package/src/plugins/chrome-lab/scoped-input.mjs +135 -0
  76. package/src/plugins/chrome-lab/worker-diagnostics.mjs +51 -0
  77. package/src/plugins/open-relay/actions.mjs +205 -0
  78. package/src/plugins/open-relay/bound.mjs +47 -0
  79. package/src/plugins/open-relay/client.mjs +158 -0
  80. package/src/plugins/open-relay/dossier.mjs +119 -0
  81. package/src/plugins/open-relay/index.mjs +153 -0
  82. package/src/plugins/open-relay/lookup.mjs +45 -0
  83. package/src/plugins/open-relay/notices.mjs +79 -0
  84. package/src/plugins/open-relay/watch.mjs +127 -0
  85. package/src/plugins.mjs +72 -29
  86. package/src/private-fs.mjs +40 -0
  87. package/src/process-lock.mjs +24 -13
  88. package/src/provider-auth.mjs +14 -3
  89. package/src/provider.mjs +11 -2
  90. package/src/read-only-check.mjs +7 -3
  91. package/src/scope-ceiling.mjs +56 -0
  92. package/src/scoped-policy.mjs +39 -0
  93. package/src/secret-login.mjs +68 -0
  94. package/src/session-bus.mjs +41 -7
  95. package/src/session-projections-candidate.mjs +119 -0
  96. package/src/session-projections.mjs +119 -0
  97. package/src/session-read-cache.mjs +23 -3
  98. package/src/sessions.mjs +35 -11
  99. package/src/source-editor-role.mjs +13 -0
  100. package/src/source-ownership.mjs +73 -0
  101. package/src/source-proposal-proof.mjs +55 -0
  102. package/src/source-proposals.mjs +118 -0
  103. package/src/source-requests.mjs +148 -0
  104. package/src/source-task-proof.mjs +24 -0
  105. package/src/source-task-revision-proof.mjs +44 -0
  106. package/src/subagents.mjs +187 -87
  107. package/src/tool-packs.mjs +5 -5
  108. package/src/tools.mjs +82 -15
  109. package/src/trusted-context.mjs +5 -1
  110. package/src/tui/connection-transition.mjs +1 -1
  111. package/src/tui/controller.mjs +8 -4
  112. package/src/tui/discovery.mjs +24 -12
  113. package/src/tui/inbound-blocks.mjs +55 -0
  114. package/src/tui/present.mjs +504 -0
  115. package/src/tui/screen.mjs +49 -13
  116. package/src/windows-private.ps1 +61 -0
  117. package/src/work-assertions.mjs +6 -2
  118. package/src/work-plan.mjs +34 -8
  119. package/src/work-state.mjs +15 -9
  120. package/src/workflows/chrome-page-cases.mjs +156 -0
  121. package/src/workflows/chrome-page-checkpoint-reader.mjs +10 -0
  122. package/src/workflows/chrome-page-diagnostics.mjs +113 -0
  123. package/src/workflows/chrome-page-law.mjs +48 -0
  124. package/src/workflows/chrome-page-recovery-host-v13.mjs +14 -0
  125. package/src/workflows/chrome-page-recovery-v13.mjs +104 -0
  126. package/src/workflows/chrome-page-task.mjs +429 -0
  127. package/src/workflows/chrome-page-verifier.mjs +123 -0
  128. package/src/workflows/chrome-source.mjs +73 -0
  129. package/src/workflows/chrome-supplied-inputs.mjs +43 -0
  130. package/src/workflows/chrome-task-adoption.mjs +122 -0
  131. package/src/workflows/chrome-task.mjs +379 -74
  132. package/src/workflows/source-task-v1.mjs +124 -0
  133. package/src/workflows/source-task-v2.mjs +227 -0
  134. package/src/workflows/source-task.mjs +13 -0
@@ -0,0 +1,73 @@
1
+ import {mkdir,lstat,realpath,readFile,stat} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import {createHash} from 'node:crypto';
5
+ import {acquireProcessLock,processIdentity} from './process-lock.mjs';
6
+ import {auditExecution} from './audit.mjs';
7
+ import {digest} from './operations.mjs';
8
+ import {ensurePrivateDirectory,assertPrivateDirectory} from './private-fs.mjs';
9
+
10
+ const fail=(message,code='SOURCE_LOCK_UNRESOLVED')=>Object.assign(new Error(message),{code});
11
+ const normal=file=>process.platform==='win32'?file.toLowerCase():file;
12
+ export const defaultSourceLockDirectory=()=>path.join(os.homedir(),'.comal','source-locks');
13
+ export function sourceLockFile(file,directory=defaultSourceLockDirectory()){
14
+ return path.join(directory,createHash('sha256').update(normal(file)).digest('hex')+'.lock');
15
+ }
16
+
17
+ // Same OS user + shared registry + canonical path. This is cooperative native
18
+ // ownership, not exclusion of Bash, arbitrary editors or other OS accounts.
19
+ export class SourceOwnership {
20
+ constructor({cwd,store,directory=defaultSourceLockDirectory()}){
21
+ this.cwd=cwd;this.store=store;this.directory=path.resolve(directory);this.held=new Map();this.paths=new Map();
22
+ }
23
+ contains(file){const r=path.relative(this.directory,file);return r===''||r!=='..'&&!r.startsWith('..'+path.sep)&&!path.isAbsolute(r);}
24
+ async prepareDirectory(){
25
+ const parent=path.dirname(this.directory);
26
+ await mkdir(parent,{recursive:true,mode:0o700});
27
+ if(await realpath(parent)!==parent)throw fail('Native source lock registry has a noncanonical parent.');
28
+ try{await ensurePrivateDirectory(this.directory);}catch(error){if(error.code==='PRIVATE_STATE_UNSAFE')throw fail('Native source lock registry is not private.');throw error;}
29
+ const info=await lstat(this.directory);
30
+ try{await assertPrivateDirectory(this.directory,info);}catch{throw fail('Native source lock registry is not a private canonical directory.');}
31
+ }
32
+ async acquire(file,{invocation_id,sessionId,actor,operation,signal,recordWork}){
33
+ signal?.throwIfAborted();await this.prepareDirectory();
34
+ const resource={kind:'native-source-v1',file,cwd:this.cwd,session_id:sessionId??null,actor,invocation_id,operation,journal:this.store&&sessionId?this.store.file(sessionId):null};
35
+ let lock;
36
+ try{lock=await acquireProcessLock(sourceLockFile(file,this.directory),{resource,validate:owner=>this.validateSettledOwner(owner,file)});}
37
+ catch(error){if(error.code==='SESSION_IN_USE')throw fail('Another native source operation owns this file. No replacement was started. Propose again after it settles, preserving your expected source version.','SOURCE_BUSY');throw error;}
38
+ const state={file,lock,signal,beforeHash:undefined};this.held.set(invocation_id,state);this.paths.set(normal(file),state);
39
+ await recordWork({type:'source_ownership_acquired',source:'workspace_host',resource,resource_digest:digest(resource)});
40
+ signal?.throwIfAborted();return state;
41
+ }
42
+ transaction(file){return this.paths.get(normal(file));}
43
+ async settle({invocation_id,receiptRecorded}){
44
+ const state=this.held.get(invocation_id);if(!state)return;
45
+ if(!receiptRecorded){
46
+ // Keep the marker after a lost durable outcome. Even when the process
47
+ // later dies, reclamation still requires a verified settlement/recovery.
48
+ await state.lock.file.close();this.held.delete(invocation_id);this.paths.delete(normal(state.file));return;
49
+ }
50
+ await state.lock.release();this.held.delete(invocation_id);this.paths.delete(normal(state.file));
51
+ }
52
+ async validateSettledOwner(owner,file){
53
+ const r=owner.resource;
54
+ if(r?.kind!=='native-source-v1'||normal(r.file||'')!==normal(file)||!['Write','Edit'].includes(r.operation)||typeof r.journal!=='string'||!r.session_id||!r.invocation_id)throw fail('Dead source owner has no verifiable native journal; ownership was preserved.');
55
+ try{
56
+ const info=await stat(r.journal);if(!info.isFile()||info.size>64*1024*1024)throw fail('Source owner journal is unavailable or exceeds the recovery inspection bound.');
57
+ const bytes=await readFile(r.journal),rows=bytes.toString('utf8').trim().split('\n').map(JSON.parse);
58
+ if(rows[0]?.type!=='session'||rows[0].id!==r.session_id||rows[0].cwd!==r.cwd)throw fail('Source owner journal identity does not match.');
59
+ const audit=auditExecution({rows,id:r.session_id,cwd:r.cwd});
60
+ if(!audit.journal.verified||audit.violations.length||audit.unverified.some(x=>x.invocation_id===r.invocation_id))throw fail('Source owner journal does not verify.');
61
+ const proposal=rows.find(x=>x.type==='house_proposal'&&x.invocation_id===r.invocation_id),receipts=rows.filter(x=>x.type==='house_receipt'&&x.invocation_id===r.invocation_id),receipt=receipts[0];
62
+ if(!proposal||receipts.length!==1||!['returned','failed','refused'].includes(receipt?.status)||proposal.operation!==r.operation||proposal.house!==r.cwd||digest(proposal.actor)!==digest(r.actor)||proposal.session_id!==r.session_id)throw fail('Source invocation has no verified durable outcome; reconcile it without replay before writing.');
63
+ for(const key of Object.keys(processIdentity))if(digest(proposal.execution_process?.[key]??null)!==digest(owner[key]??null))throw fail('Source owner process does not match its native proposal.');
64
+ const acquired=rows.filter(x=>x.type==='source_ownership_acquired'&&x.invocation_id===r.invocation_id);
65
+ if(acquired.length!==1||acquired[0].source!=='workspace_host'||acquired[0].house!==r.cwd||acquired[0].session_id!==r.session_id||digest(acquired[0].actor)!==digest(r.actor)||acquired[0].resource_digest!==digest(r)||digest(acquired[0].resource)!==digest(r))throw fail('Source ownership is not bound to its native invocation journal.');
66
+ const contract=rows.find(x=>x.type==='house_contract'&&x.contract_digest===proposal.contract_digest)?.contract;
67
+ if(contract?.origin!==null||contract.name!==r.operation||contract.permission!==r.operation||contract.steps||contract.effects?.workspace!=='write'||contract.effects?.external!=='none'||contract.effects?.process!=='none')throw fail('Source owner is not a native file operation.');
68
+ if(audit.unresolved.some(x=>x.invocation_id===r.invocation_id))throw fail('Source owner remains unresolved.');
69
+ // A durable failed outcome still retains its unknown/partial attribution.
70
+ // Releasing exclusion never retries it or certifies its requested effect.
71
+ }catch(error){if(error.code==='SOURCE_LOCK_UNRESOLVED')throw error;throw fail('Source owner journal could not be verified; ownership was preserved.');}
72
+ }
73
+ }
@@ -0,0 +1,55 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {digest} from './operations.mjs';
3
+ export const sourceSHA=value=>createHash('sha256').update(value).digest('hex');
4
+ export const sourceContract=contract=>{const {revision,...semantic}=contract;return digest(semantic);};
5
+ export const sourceOwn=(a,b)=>['session_id','house','surface','actor'].every(k=>digest(a[k]??null)===digest(b[k]??null));
6
+ const contractFor=(rows,p)=>rows.findLast(r=>r.type==='house_contract'&&r.contract_digest===p?.contract_digest)?.contract;
7
+ export function nativeSourceParent(rows,p){const c=contractFor(rows,p);return ['SourceDraft','SourceRetain','SourcePublish'].includes(p?.operation)&&c?.name===p.operation&&c.origin===null&&c.permission===p.operation&&c.steps===true&&c.effects?.external==='none'&&c.effects?.process==='none'&&c.effects?.authority==='none'&&c.effects?.workspace===(p.operation==='SourcePublish'?'write':'read');}
8
+ export function sourceInput(rows,p){
9
+ const facts=rows.slice(0,rows.indexOf(p)).filter(r=>r.type==='message'&&r.message?.role==='assistant').flatMap(r=>r.message.content||[]).filter(c=>c.type==='tool_use'&&c.id===p.call_id&&c.name===p.operation&&digest(c.input)===p.arguments_digest);
10
+ return facts.length===1?facts[0].input:null;
11
+ }
12
+ export function sourcePartValid(rows,row){try{
13
+ if(row.type!=='source_part'||row.source!=='source_host'||!Number.isSafeInteger(row.index)||row.index<0)return false;
14
+ const parent=rows.find(p=>p.type==='house_proposal'&&p.invocation_id===row.invocation_id);
15
+ if(!nativeSourceParent(rows,parent)||!sourceOwn(parent,row))return false;
16
+ const data=row.data;if(digest(data)!==row.data_digest||data.version!==1||data.file_path!==row.file_path||typeof row.content!=='string')return false;
17
+ if(!rows.some(r=>r.type==='house_started'&&r.invocation_id===parent.invocation_id&&sourceOwn(r,parent))||rows.indexOf(parent)>=rows.indexOf(row))return false;
18
+ if(parent.operation==='SourceRetain'){
19
+ const original=rows.find(p=>p.type==='house_proposal'&&p.invocation_id===data.original_invocation),input=original&&sourceInput(rows,original),receipt=original&&rows.findLast(r=>r.type==='house_receipt'&&r.invocation_id===original.invocation_id),c=contractFor(rows,original);
20
+ if(!original||!sourceOwn(original,row)||!['Write','Edit'].includes(original.operation)||c?.origin!==null||c.permission!==original.operation||c.steps||c.effects?.workspace!=='write'||['external','process','authority'].some(k=>c.effects?.[k]!=='none')||!input||receipt?.status!=='refused'||receipt.code!=='MODEL_STEP_TOO_LARGE'||receipt.effects_observed!=='none'||!sourceOwn(receipt,row))return false;
21
+ if(data.operation!==original.operation||digest(row.retained_input)!==original.arguments_digest||digest(input)!==digest(row.retained_input)||row.content!=='')return false;
22
+ }else if(parent.operation==='SourceDraft'){
23
+ if(data.operation!=='Write'||row.retained_input!==undefined||Buffer.byteLength(row.content)>64000)return false;
24
+ if(parent.arguments_digest!==digest(row.draft_input)||row.draft_input.draft_id!==row.draft_id||row.draft_input.part_id!==row.part_id||row.draft_input.content!==row.content||Boolean(row.draft_input.final)!==row.final)return false;
25
+ }else return false;
26
+ const info=rows.findLast(r=>r.type==='house_receipt'&&r.invocation_id===data.inspection_id),ip=rows.find(r=>r.type==='house_proposal'&&r.invocation_id===data.inspection_id),ic=contractFor(rows,ip);
27
+ if(!info||!ip||!sourceOwn(info,row)||info.operation!=='FileInfo'||info.status!=='returned'||info.output_truncated||info.output_digest!==digest(info.output)||ic?.origin!==null||ic.permission!=='FileInfo'||ic.steps)return false;
28
+ if(rows.indexOf(info)>=rows.indexOf(row)||rows.indexOf(ip)>=rows.indexOf(info))return false;
29
+ const snapshot=JSON.parse(info.output);
30
+ const expected=parent.operation==='SourceRetain'?row.retained_input.expected_sha256??data.inspected_sha256:row.index===0?row.draft_input.expected_sha256??data.inspected_sha256:data.expected_sha256;
31
+ if(data.expected_sha256!==expected||ip.arguments_digest!==digest({file_path:data.requested_path,hash:true,allow_missing:true,max_bytes:1048576})||info.parent_invocation_id!==rows.find(r=>r.type==='source_part'&&r.draft_id===row.draft_id&&sourceOwn(r,row))?.invocation_id)return false;
32
+ return snapshot.file_path===row.file_path&&snapshot.type==='file'&&snapshot.sha256===data.inspected_sha256||snapshot.file_path===row.file_path&&snapshot.type==='absent'&&data.inspected_sha256==='absent';
33
+ }catch{return false;}}
34
+ export function sourceMaterial(rows,ref){
35
+ const last=rows.find(r=>r.type==='source_part'&&r.ref===ref);if(!last)return null;
36
+ const parts=rows.filter(r=>r.type==='source_part'&&r.draft_id===last.draft_id&&sourceOwn(r,last)&&digest(r.data)===digest(last.data)&&rows.indexOf(r)<=rows.indexOf(last));
37
+ if(!parts.length||parts.some((r,i)=>r.index!==i||i<parts.length-1&&r.final||!sourcePartValid(rows,r))||new Set(parts.map(p=>p.part_id)).size!==parts.length)return null;
38
+ const input=last.data.original_invocation?last.retained_input:{file_path:last.file_path,content:parts.map(r=>r.content).join('')};
39
+ const resolved={...input,file_path:last.file_path,expected_sha256:last.data.expected_sha256};
40
+ if(parts.some(r=>r.ref!=='source:'+digest({data:r.data,parts:parts.slice(0,r.index+1).map(p=>({part_id:p.part_id,content:p.content,final:p.final,...(p.retained_input?{retained_input:p.retained_input}:{})}))})))return null;
41
+ return {last,parts,input:resolved,complete:last.final,bytes:Buffer.byteLength(last.data.operation==='Write'?resolved.content:resolved.new_string),sha256:sourceSHA(last.data.operation==='Write'?resolved.content:resolved.new_string)};
42
+ }
43
+ export function sourcePublicationLeafValid(rows,p){try{
44
+ const parent=rows.find(r=>r.type==='house_proposal'&&r.invocation_id===p.parent_invocation_id),dispatch=rows.find(r=>r.type==='source_dispatch'&&r.invocation_id===parent?.invocation_id&&r.call_id===p.call_id),intent=dispatch&&rows.find(r=>r.type==='source_publication_intent'&&r.intent_id===dispatch.intent_id),material=intent&&sourceMaterial(rows,intent.ref);
45
+ const original=intent&&rows.find(r=>r.type==='house_proposal'&&r.invocation_id===intent.invocation_id),writer=contractFor(rows,p);
46
+ return Boolean(nativeSourceParent(rows,original)&&original.operation==='SourcePublish'&&original.arguments_digest===digest({ref:intent.ref,publication_id:intent.publication_id})&&sourceOwn(original,intent)&&writer&&sourceContract(writer)===material?.last.data.contract&&rows.indexOf(intent)<rows.indexOf(dispatch)&&rows.indexOf(dispatch)<rows.indexOf(p)&&nativeSourceParent(rows,parent)&&parent.operation==='SourcePublish'&&dispatch?.source==='source_host'&&intent?.source==='source_host'&&sourceOwn(parent,p)&&sourceOwn(dispatch,p)&&sourceOwn(intent,p)&&material?.complete&&sourceOwn(material.last,p)&&intent.operation===p.operation&&intent.arguments_digest===digest(material.input)&&p.arguments_digest===intent.arguments_digest&&intent.call_id===p.call_id&&parent.arguments_digest===digest({ref:intent.ref,publication_id:intent.publication_id}));
47
+ }catch{return false;}}
48
+ export function sourceRecoveryPlan(rows,p){
49
+ if(!nativeSourceParent(rows,p))return null;
50
+ const children=rows.filter(r=>r.type==='house_proposal'&&r.parent_invocation_id===p.invocation_id);
51
+ if(children.some(c=>!rows.some(r=>r.type==='house_receipt'&&r.invocation_id===c.invocation_id)))return null;
52
+ if(children.some(c=>!['FileInfo','Write','Edit'].includes(c.operation)||['Write','Edit'].includes(c.operation)&&!sourcePublicationLeafValid(rows,c)))return null;
53
+ return {kind:'source_recorded',code:'HOUSE_RECOVERY_SOURCE_CONTINUE',error:'The source operation was interrupted. SourceRead exposes retained data and recorded native outcomes. No publication was replayed; source effects do not establish task acceptance.',effects_observed:'recorded_source_children_not_delivery',child_refs:children.map(c=>rows.findIndex(r=>r.type==='house_receipt'&&r.invocation_id===c.invocation_id))};
54
+ }
55
+ export function sourceRecoveryValid(rows,index,row){const plan=sourceRecoveryPlan(rows.slice(0,index),rows[row.recovery.proposal_ref]);return Boolean(plan&&row.status==='refused'&&row.code===plan.code&&row.error===plan.error&&row.effects_observed===plan.effects_observed&&row.recovery.plan_digest===digest(plan));}
@@ -0,0 +1,118 @@
1
+ import path from 'node:path';
2
+ import {randomUUID} from 'node:crypto';
3
+ import {digest} from './operations.mjs';
4
+ import {workProjection} from './work-plan.mjs';
5
+ import {auditExecution} from './audit.mjs';
6
+ import {acquireProcessLock} from './process-lock.mjs';
7
+ import {ensurePrivateDirectory} from './private-fs.mjs';
8
+ import {sourceContract,sourceOwn,sourceInput,sourceMaterial,sourcePublicationLeafValid} from './source-proposal-proof.mjs';
9
+ const id={type:'string',pattern:'^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$'},ref={type:'string',pattern:'^source:[a-f0-9]{64}$'},version={type:'string',pattern:'^(?:absent|[a-f0-9]{64})$'};
10
+ const schema=(name,description,properties,required,write=false)=>({name,description,input_schema:{type:'object',properties,required,additionalProperties:false},steps:name!=='SourceRead',effects:{workspace:write?'write':'read',external:'none',process:'none',authority:'none'}});
11
+ export const sourceDefinitions=[
12
+ schema('SourceDraft','Append UTF-8 source data without a work plan. Stable part_id retries reuse identical bytes; host assigns order. First part freezes destination/version; final:true seals it. Does not write the source file or grant publication permission.',{draft_id:id,file_path:{type:'string',minLength:1,maxLength:4096},part_id:id,content:{type:'string',maxLength:64000},final:{type:'boolean'},expected_sha256:version},['draft_id','file_path','part_id','content']),
13
+ schema('SourceRetain','Reuse your own earlier MODEL_STEP_TOO_LARGE Write/Edit without regenerating its content. call_id identifies the refused tool call. This NEW retention intent inspects/fixes the current destination baseline unless the original supplied expected_sha256; that original is never refreshed. No file is written.',{call_id:{type:'string',minLength:1,maxLength:256}},['call_id']),
14
+ schema('SourceRead','Read own retained source status, exact publish calls, dated outcomes, or a bounded content page. No ref/draft_id lists the latest 12 own drafts. Retention and successful file effects do not establish functional or human acceptance.',{ref,draft_id:id,offset:{type:'integer',minimum:0},limit:{type:'integer',minimum:1,maximum:12000}},[]),
15
+ schema('SourcePublish','Publish sealed retained bytes via the ORIGINAL native Write/Edit and frozen expected_sha256 under CURRENT permissions, task/goal and contract. Stable publication_id never repeats a dispatched mutation; uncertain outcomes require inspection. Returns dated native evidence, not task acceptance.',{ref,publication_id:id},['ref','publication_id'],true),
16
+ ];
17
+ const fail=(message,code='SOURCE_PROPOSAL_INVALID',details)=>Object.assign(new Error(message),{code,...(details?{details}:{})});
18
+ export class SourceProposals {
19
+ constructor(workspace){this.workspace=workspace;this.locks=new Map();this.scopes=new Map();}
20
+ bindScope(sessionId,accept){
21
+ const binding={accept};this.scopes.set(sessionId,binding);
22
+ return {active:()=>this.scopes.get(sessionId)===binding,dispose:()=>{if(this.scopes.get(sessionId)===binding)this.scopes.delete(sessionId);}};
23
+ }
24
+ async access(data,o,s,binding=this.scopes.get(o.sessionId)){
25
+ if(this.scopes.get(o.sessionId)!==binding)return false;
26
+ if(!binding)return true;
27
+ const accepted=await binding.accept(data,o,s);
28
+ return accepted&&this.scopes.get(o.sessionId)===binding;
29
+ }
30
+ async requireAccess(data,o,s,binding=this.scopes.get(o.sessionId)){if(!await this.access(data,o,s,binding))throw fail('Retained source is outside the active native source scope.','SOURCE_SCOPE_CHANGED');}
31
+ async settle({invocation_id,receiptRecorded}){const lock=this.locks.get(invocation_id);if(!lock)return;this.locks.delete(invocation_id);if(receiptRecorded)await lock.release();else await lock.file.close();}
32
+ async session(o){const w=this.workspace;if(!w.store||!w.house.hasRecorder||!o.sessionId)throw fail('Retained source requires this native durable session.');const s=await w.store.load(o.sessionId),a=auditExecution(s,w.house.stateContext());if(!a.journal.consistent||a.violations.length)throw fail('The source journal does not verify.','AUDIT_INVALID');return s;}
33
+ scope(s){const work=workProjection(s,this.workspace.house.stateContext());return {reset_ref:s.rows.findLastIndex(r=>r.type==='reset'),goal_id:s.goal?.id??null,task_id:work?.current?.id??null,work_digest:work?.current?.work_digest??null};}
34
+ async lock(key,o){const store=this.workspace.store;await ensurePrivateDirectory(store.dir);const lock=await acquireProcessLock(path.join(store.dir,`.source-${digest([o.sessionId,o.actor,key])}.lock`),{resource:{session_id:o.sessionId,actor:o.actor,invocation_id:o.invocation_id},validate:async owner=>{
35
+ const s=await this.session(o),resource=owner.resource;
36
+ if(resource?.session_id!==o.sessionId||digest(resource.actor)!==digest(o.actor)||!s.rows.some(r=>r.type==='house_receipt'&&r.invocation_id===resource.invocation_id))throw fail('The earlier source owner has no settled native outcome. Recover the session before continuing.','SOURCE_PUBLICATION_UNCERTAIN');
37
+ }}).catch(error=>{if(error.code==='SESSION_IN_USE')throw fail('This source intent is already in progress; it was not replayed.','SOURCE_BUSY');throw error;});this.locks.set(o.invocation_id,lock);}
38
+ parent(s,o){const p=s.rows.findLast(r=>r.type==='house_proposal'&&r.invocation_id===o.invocation_id);if(!p||p.session_id!==o.sessionId||digest(p.actor)!==digest(o.actor))throw fail('Missing native source invocation identity.');return p;}
39
+ own(s,o){return this.parent(s,o);}
40
+ summary(m){return {ref:m.last.ref,draft_id:m.last.draft_id,file_path:m.last.file_path,operation:m.last.data.operation,bytes:m.bytes,content_sha256:m.sha256,expected_sha256:m.last.data.expected_sha256,complete:m.complete,next_index:m.parts.length,part_ids:m.parts.slice(-4).map(p=>p.part_id),scope:m.last.data.scope,...(m.complete?{publish_call:{name:'SourcePublish',input:{ref:m.last.ref,publication_id:'publish-'+m.last.ref.slice(7,23)}}}:{}),authority:'data_only',acceptance:'unjudged'};}
41
+ material(s,reference,owner){const m=sourceMaterial(s.rows,reference);if(!m||!sourceOwn(m.last,owner))throw fail('No valid retained source belongs to this actor/session/ref.','SOURCE_NOT_FOUND');return m;}
42
+ async frameState(m,o,binding){
43
+ if(await this.workspace.resolve(m.last.data.requested_path,{create:true})!==path.resolve(this.workspace.cwd,m.last.file_path))throw fail('The original source alias changed; its replacement was not targeted.','SOURCE_CHANGED');
44
+ const s=await this.session(o);await this.requireAccess(m.last.data,o,s,binding);
45
+ if(digest(this.scope(s))!==digest(m.last.data.scope)||s.goal&&['cleared','met','failed','budget'].includes(s.goal.status))throw fail('The original task, goal or reset scope changed. Start an explicit new draft; saved data remains readable.','SOURCE_SCOPE_CHANGED');
46
+ const op=this.workspace.house.registry.get(m.last.data.operation);if(!this.workspace.isNativeOperation(m.last.data.operation)||sourceContract(op.contract)!==m.last.data.contract)throw fail('The original native writer contract changed; retained data grants no replacement authority.','SOURCE_CONTRACT_CHANGED');
47
+ return s;
48
+ }
49
+ async frame(m,o){const binding=this.scopes.get(o.sessionId);await this.frameState(m,o,binding);await o.revalidate();return this.frameState(m,o,binding);}
50
+ async readFrame(materials,o,binding){
51
+ if(this.scopes.get(o.sessionId)!==binding)throw fail('The native source scope binding changed.','SOURCE_SCOPE_CHANGED');
52
+ if(!binding)return;
53
+ await o.revalidate();const s=await this.session(o);
54
+ if(this.scopes.get(o.sessionId)!==binding)throw fail('The native source scope binding changed.','SOURCE_SCOPE_CHANGED');
55
+ for(const m of materials)await this.requireAccess(m.last.data,o,s,binding);
56
+ }
57
+ async baseline(file,expected,o){
58
+ if(!this.workspace.isNativeOperation('FileInfo'))throw fail('Native bounded FileInfo is required to retain source.','SOURCE_CONTRACT_CHANGED');
59
+ const input={file_path:file,hash:true,allow_missing:true,max_bytes:1048576};
60
+ await o.recordWork({type:'source_inspection_intent',source:'source_host',input,input_digest:digest(input)});
61
+ const callId=randomUUID(),output=await o.invoke('FileInfo',input,{signal:o.signal,callId}),info=JSON.parse(output),s=await this.session(o),r=s.rows.findLast(r=>r.type==='house_receipt'&&r.call_id===callId&&r.operation==='FileInfo'&&r.status==='returned');
62
+ if(!r||!['file','absent'].includes(info.type))throw fail('A regular UTF-8 destination or absent path is required.');
63
+ const actual=info.type==='absent'?'absent':info.sha256;return {file_path:info.file_path,inspected_sha256:actual,expected_sha256:expected??actual,inspection_id:r.invocation_id};
64
+ }
65
+ async execute(name,input,o){
66
+ let s=await this.session(o);const owner=this.own(s,o);
67
+ if(name==='SourceRead'){
68
+ const binding=this.scopes.get(o.sessionId);
69
+ const items=s.rows.filter(r=>r.type==='source_part'&&sourceOwn(r,owner));let last=input.ref?items.find(r=>r.ref===input.ref):input.draft_id?items.findLast(r=>r.draft_id===input.draft_id):null;
70
+ if(!input.ref&&!input.draft_id){const latest=new Map();for(const row of items)latest.set(row.draft_id,row);const materials=[];for(const row of latest.values()){const m=this.material(s,row.ref,owner);if(await this.access(m.last.data,o,s,binding))materials.push(m);}const selected=materials.slice(-12);await this.readFrame(selected,o,binding);return JSON.stringify({drafts:selected.map(m=>this.summary(m)),authority:'data_only'});}
71
+ if(!last)throw fail('Retained source not found.','SOURCE_NOT_FOUND');const m=this.material(s,last.ref,owner);if(!await this.access(m.last.data,o,s,binding))throw fail('Retained source is outside the active native source scope.','SOURCE_SCOPE_CHANGED');const text=m.last.data.operation==='Write'?m.input.content:m.input.new_string;
72
+ const publications=s.rows.filter(r=>r.type==='source_publication_intent'&&sourceOwn(r,owner)&&r.ref===last.ref).map(r=>({publication_id:r.publication_id,call_id:r.call_id,attempts:s.rows.filter(p=>p.type==='house_proposal'&&p.call_id===r.call_id).map(p=>{const receipt=s.rows.findLast(e=>e.type==='house_receipt'&&e.invocation_id===p.invocation_id);return{receipt_id:p.invocation_id,status:receipt?.status??'uncertain',code:receipt?.code??null,attribution:receipt?.recovery?'observed_postcondition_not_handler_return':'dated_native_receipt'};})}));
73
+ await this.readFrame([m],o,binding);return JSON.stringify({...this.summary(m),publications,...(input.offset!==undefined?{offset:input.offset,content:text.slice(input.offset,input.offset+(input.limit??4000)),truncated:input.offset+(input.limit??4000)<text.length}:{}),historical:true});
74
+ }
75
+ if(name==='SourcePublish')return this.publish(input,o,s,owner);
76
+ const original=name==='SourceRetain'?s.rows.filter(p=>p.type==='house_proposal'&&p.call_id===input.call_id&&sourceOwn(p,owner)):null;
77
+ if(original&&(original.length!==1||!['Write','Edit'].includes(original[0].operation)))throw fail('Identify one exact native refused Write/Edit from your own session.','SOURCE_NOT_FOUND');
78
+ const old=original?.[0],retained=old&&sourceInput(s.rows,old),ending=old&&s.rows.findLast(r=>r.type==='house_receipt'&&r.invocation_id===old.invocation_id);
79
+ if(old&&(!retained||ending?.status!=='refused'||ending.code!=='MODEL_STEP_TOO_LARGE'||ending.effects_observed!=='none'))throw fail('The original bytes and noneffect oversize refusal do not verify.','SOURCE_NOT_FOUND');
80
+ const scope=this.scope(s),draft_id=old?'retained-'+digest([old.invocation_id,scope]).slice(0,32):input.draft_id;
81
+ await this.requireAccess({requested_path:old?retained.file_path:input.file_path,scope,...(old?{original_invocation:old.invocation_id}:{})},o,s);
82
+ await this.lock(['draft',draft_id],o);s=await this.session(o);
83
+ const existing=s.rows.filter(r=>r.type==='source_part'&&sourceOwn(r,owner)&&r.draft_id===draft_id),prior=existing.at(-1),part_id=old?'original':input.part_id,content=old?'':input.content,final=old?true:Boolean(input.final);
84
+ if(prior){const m=this.material(s,prior.ref,owner),same=existing.find(r=>r.part_id===part_id);await this.frame(m,o);
85
+ if(same){if(same.content!==content||same.final!==final||!old&&(same.draft_input.file_path!==input.file_path||same.draft_input.expected_sha256!==input.expected_sha256))throw fail('part_id already names different bytes or arguments.','SOURCE_PART_CONFLICT');return JSON.stringify({...this.summary(m),reused:true});}
86
+ if(m.complete)throw fail('The draft is sealed; use a new draft_id for a new proposal.','SOURCE_SEALED');
87
+ if(input.file_path!==m.last.data.requested_path||input.expected_sha256!==undefined&&input.expected_sha256!==m.last.data.expected_sha256)throw fail('A source draft cannot retarget or refresh its baseline.','SOURCE_PART_CONFLICT');
88
+ }
89
+ const operation=old?.operation??'Write',op=this.workspace.house.registry.get(operation);
90
+ if(!this.workspace.isNativeOperation(operation))throw fail('The original native writer is unavailable.','SOURCE_CONTRACT_CHANGED');
91
+ if(old){const c=s.rows.findLast(r=>r.type==='house_contract'&&r.contract_digest===old.contract_digest)?.contract;if(!c||sourceContract(c)!==sourceContract(op.contract))throw fail('The original refused writer contract changed.','SOURCE_CONTRACT_CHANGED');}
92
+ if(!prior&&s.rows.filter(r=>r.type==='source_part'&&sourceOwn(r,owner)&&r.index===0&&r.data.scope.reset_ref===scope.reset_ref).length>=32)throw fail('This reset scope already retains 32 source proposals.','SOURCE_LIMIT');
93
+ const snapshot=prior?.data??await this.baseline(old?retained.file_path:input.file_path,old?retained.expected_sha256:input.expected_sha256,o);
94
+ const data=prior?.data??{version:1,...snapshot,operation,requested_path:old?retained.file_path:input.file_path,scope,contract:sourceContract(op.contract),...(old?{original_invocation:old.invocation_id}:{})};
95
+ if(Buffer.byteLength(content)>64000&&!old||existing.length>=128||existing.reduce((n,r)=>n+Buffer.byteLength(r.content),0)+Buffer.byteLength(old?JSON.stringify(retained):content)>1048576)throw fail('Retained source exceeds its 64 KB part, 128 part or 1 MiB proposal bound.','SOURCE_LIMIT');
96
+ const parts=[...existing.map(r=>({part_id:r.part_id,content:r.content,final:r.final,...(r.retained_input?{retained_input:r.retained_input}:{})})),{part_id,content,final,...(old?{retained_input:retained}:{})}],reference='source:'+digest({data,parts});
97
+ await o.revalidate();const current=await this.session(o);await this.requireAccess(data,o,current);if(digest(this.scope(current))!==digest(scope))throw fail('Source scope changed during inspection.','SOURCE_SCOPE_CHANGED');
98
+ await o.recordWork({type:'source_part',source:'source_host',draft_id,part_id,index:existing.length,file_path:data.file_path,content,final,data,data_digest:digest(data),ref:reference,...(old?{retained_input:retained}:{draft_input:input})});
99
+ s=await this.session(o);return JSON.stringify(this.summary(this.material(s,reference,owner)));
100
+ }
101
+ async publish(input,o,s,owner){
102
+ const m=this.material(s,input.ref,owner);if(!m.complete)throw fail('The source draft is not final.','SOURCE_INCOMPLETE');
103
+ await this.lock(['publication',input.publication_id],o);s=await this.frame(m,o);
104
+ let intent=s.rows.find(r=>r.type==='source_publication_intent'&&sourceOwn(r,owner)&&r.publication_id===input.publication_id);
105
+ if(intent&&intent.ref!==input.ref)throw fail('publication_id already names another immutable source proposal.','SOURCE_PUBLICATION_CONFLICT');
106
+ if(!intent){intent={type:'source_publication_intent',source:'source_host',intent_id:randomUUID(),publication_id:input.publication_id,ref:input.ref,operation:m.last.data.operation,arguments_digest:digest(m.input),call_id:randomUUID()};await o.recordWork(intent);}
107
+ s=await this.session(o);const attempts=s.rows.filter(r=>r.type==='house_proposal'&&r.call_id===intent.call_id);
108
+ if(attempts.length){const p=attempts[0],r=s.rows.findLast(r=>r.type==='house_receipt'&&r.invocation_id===p.invocation_id);
109
+ if(attempts.length!==1||!r||!sourcePublicationLeafValid(s.rows,p)||r.arguments_digest!==p.arguments_digest||!sourceOwn(r,p)||r.status==='returned'&&(r.output_truncated||r.output_digest!==digest(r.output)))throw fail('A native publication was dispatched without a settled outcome. Recover/inspect it; no mutation was replayed.','SOURCE_PUBLICATION_UNCERTAIN');
110
+ if(r.status!=='returned')throw fail('This publication has a recorded refusal/failure. Inspect it and use a NEW publication_id for new corrective intent; the frozen baseline remains.','SOURCE_PUBLICATION_CLOSED',{receipt_id:r.invocation_id,code:r.code??null});
111
+ return JSON.stringify({ref:input.ref,publication_id:input.publication_id,receipt_id:r.invocation_id,reused:true,output:r.output,scope:'dated_native_source_outcome_not_current_file_or_acceptance',acceptance:'unjudged'});
112
+ }
113
+ await o.recordWork({type:'source_dispatch',source:'source_host',intent_id:intent.intent_id,call_id:intent.call_id});
114
+ const output=await o.invoke(intent.operation,m.input,{signal:o.signal,callId:intent.call_id,beforeEffect:()=>this.frame(m,o)});
115
+ s=await this.session(o);const receipt=s.rows.findLast(r=>r.type==='house_receipt'&&r.call_id===intent.call_id&&r.status==='returned');if(!receipt)throw fail('The native source outcome was not recorded.','SOURCE_PUBLICATION_UNCERTAIN');
116
+ return JSON.stringify({ref:input.ref,publication_id:input.publication_id,receipt_id:receipt.invocation_id,output,reused:false,scope:'dated_native_source_outcome_not_current_file_or_acceptance',acceptance:'unjudged'});
117
+ }
118
+ }
@@ -0,0 +1,148 @@
1
+ // Experimental native source-data admission. No public ports or permission grants.
2
+ import path from 'node:path';
3
+ import { lstat, realpath } from 'node:fs/promises';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { digest } from './operations.mjs';
6
+ import { auditExecution } from './audit.mjs';
7
+ import { sourceInput, sourceOwn, sourceContract } from './source-proposal-proof.mjs';
8
+ import { workProjection } from './work-plan.mjs';
9
+
10
+ const managers = new WeakMap(), tokens = new WeakMap();
11
+ export const SOURCE_REQUEST_LIMITS = Object.freeze({ version: 1, max_bytes: 1048576, max_path: 4096, max_call_id: 256, max_active: 8, max_ancestors: 128 });
12
+ const fail = (message, code = 'SOURCE_REQUEST_INVALID') => Object.assign(new Error(message), { code });
13
+ const scope = (book, context) => {
14
+ const work = workProjection(book, context);
15
+ return { reset_ref: book.rows.findLastIndex(row => row.type === 'reset'), goal_id: book.goal?.id ?? null, goal_status: book.goal?.status ?? null,
16
+ task_id: work?.current?.id ?? null, work_digest: work?.current?.work_digest ?? null, task_status: work?.current?.status ?? null,
17
+ chrome_task: book.rows.findLast(row => row.type === 'chrome_task_accepted' && row.source === 'workflow_host')?.task_digest ?? null,
18
+ source_task: book.rows.findLast(row => row.type === 'source_task_accepted' && row.source === 'source_task_host')?.task_digest ?? null };
19
+ };
20
+ const terminalGoal = book => book.goal && ['cleared', 'met', 'failed', 'budget'].includes(book.goal.status);
21
+ const nativeWriter = (contract, name) => contract?.name === name && contract.origin === null && contract.permission === name && !contract.steps && contract.effects?.workspace === 'write' && ['external', 'process', 'authority'].every(key => contract.effects?.[key] === 'none');
22
+ const same = (a, b) => digest(a) === digest(b);
23
+
24
+ function inspection(book, audit, owner, file, isNative, registry) {
25
+ const tip = audit.state.find(item => item.path === file);
26
+ if (!tip || tip.operation_status !== 'returned') return null;
27
+ const row = audit.receipts.findLast(row => sourceOwn(row, owner) && row.status === 'returned' && !row.output_truncated && ['Read', 'Write', 'Edit'].includes(row.operation) &&
28
+ !audit.unverified.some(item => item.invocation_id === row.invocation_id) && isNative(row.operation) &&
29
+ row.observations?.some(item => item.file_path === file && (item.kind === 'file_read' && row.operation === 'Read' && item.sha256 === tip.sha256 || item.kind === 'file_write' && row.operation !== 'Read' && item.matches_requested === true && item.after_sha256 === tip.sha256)));
30
+ if (!row) return null;
31
+ const contract = book.rows.findLast(item => item.type === 'house_contract' && item.contract_digest === row.contract_digest)?.contract;
32
+ if (!contract || contract.origin !== null || contract.permission !== row.operation || contract.steps || sourceContract(contract) !== sourceContract(registry.get(row.operation).contract)) return null;
33
+ return { receipt_id: row.invocation_id, receipt_row: book.rows.findIndex(item => item.type === 'house_receipt' && item.invocation_id === row.invocation_id), operation: row.operation,
34
+ file_path: file, sha256: tip.sha256, contract_digest: row.contract_digest, arguments_digest: row.arguments_digest, output_digest: row.output_digest };
35
+ }
36
+
37
+ async function canonical(workspace, requested, name) {
38
+ // Metadata only: no content read, absence claim, Read cache or FileInfo bypass.
39
+ let candidate = path.resolve(workspace.cwd, requested), depth = 0;
40
+ if (!workspace.inside(candidate)) throw fail('Source request is outside the workspace.', 'SOURCE_REQUEST_PATH');
41
+ for (let part = candidate; part !== workspace.cwd; part = path.dirname(part)) {
42
+ if (++depth > SOURCE_REQUEST_LIMITS.max_ancestors) throw fail('Source request path exceeds its metadata bound.', 'SOURCE_REQUEST_PATH');
43
+ try { if ((await lstat(part)).isSymbolicLink()) await realpath(part); }
44
+ catch (error) { if (error.code !== 'ENOENT') throw error; try { if ((await lstat(part)).isSymbolicLink()) throw fail('A dangling source alias is not a create target.', 'SOURCE_REQUEST_PATH'); } catch (again) { if (again.code !== 'ENOENT') throw again; } }
45
+ }
46
+ return workspace.resolve(requested, { create: name === 'Write' });
47
+ }
48
+
49
+ // Called only by WorkspaceTools construction; this is an in-process host boundary,
50
+ // not an OS sandbox against arbitrary trusted Node code importing host modules.
51
+ export function registerNativeSourceRequests(workspace, { enabled = false, isNative }) {
52
+ const active = new Map();
53
+ managers.set(workspace, { workspace, enabled: enabled === true, isNative, active });
54
+ }
55
+ export function nativeSourceRequestStatus(workspace) {
56
+ const manager = managers.get(workspace);
57
+ return { enabled: Boolean(manager?.enabled), active: manager?.active.size ?? 0, max_active: SOURCE_REQUEST_LIMITS.max_active };
58
+ }
59
+ export function nativeSourceRequestCapability(workspace, name, catalogue) {
60
+ const manager = managers.get(workspace), op = workspace.house.registry.get(name);
61
+ if (!manager?.enabled || typeof workspace.store?.load !== 'function' || typeof workspace.store?.file !== 'function' || !workspace.house.hasRecorder || !workspace.executor?.identity().session_id || !manager.isNative(name) || !nativeWriter(op?.contract, name) || catalogue && !catalogue.some(item => item.name === name && item.contract_digest === op.digest)) return null;
62
+ return Object.freeze({ protocol: 'comal-native-source-request/1', max_bytes: SOURCE_REQUEST_LIMITS.max_bytes, byte_basis: 'UTF-8 JSON arguments', max_path_utf16: SOURCE_REQUEST_LIMITS.max_path, max_active: SOURCE_REQUEST_LIMITS.max_active });
63
+ }
64
+ export function prepareNativeSourceRequest(workspace, frame) {
65
+ const manager = managers.get(workspace);
66
+ if (!manager?.enabled) return;
67
+ return prepare(manager, frame);
68
+ }
69
+ async function prepare(manager, frame) {
70
+ const { workspace: w, isNative, active } = manager, { house, context, op, input, proposal, signal, recordWork } = frame;
71
+ if (w.house !== house || !isNative(proposal.operation) || house.registry.get(proposal.operation) !== op || !nativeWriter(op.contract, proposal.operation)) return;
72
+ if (!w.store || !house.hasRecorder || !context.sessionId || typeof proposal.call_id !== 'string' || !proposal.call_id.length || proposal.call_id.length > SOURCE_REQUEST_LIMITS.max_call_id || typeof input.file_path !== 'string' || input.file_path.length > SOURCE_REQUEST_LIMITS.max_path || Buffer.byteLength(JSON.stringify(input)) > SOURCE_REQUEST_LIMITS.max_bytes) return;
73
+ if (active.size >= SOURCE_REQUEST_LIMITS.max_active) throw fail('Too many native source requests are active.', 'SOURCE_REQUEST_LIMIT');
74
+ const authority = house.authority(context);
75
+ const load = async () => {
76
+ signal?.throwIfAborted(); const book = await w.store.load(context.sessionId), audit = auditExecution(book, house.stateContext());
77
+ if (book.id !== context.sessionId || book.cwd !== w.cwd || !audit.journal.consistent || audit.violations.length || audit.unresolved.length) throw fail('The original source-request book is unavailable.', 'AUDIT_INVALID');
78
+ return { book, audit };
79
+ };
80
+ const { book, audit } = await load(), originals = book.rows.filter(row => row.type === 'house_proposal' && row.call_id === proposal.call_id && sourceOwn(row, { ...context, session_id: context.sessionId, house: w.cwd }));
81
+ if (originals.length !== 1 || originals[0].invocation_id !== proposal.invocation_id || book.rows.some(row => row.type === 'source_request_prepared' && row.call_id === proposal.call_id && sourceOwn(row, originals[0]))) throw fail('This original source call already has an invocation; it cannot replay.', 'SOURCE_REQUEST_REPLAY');
82
+ const original = originals[0], retained = sourceInput(book.rows, original);
83
+ if (!retained || !same(retained, input) || original.contract_digest !== op.digest || original.operation !== proposal.operation || !same(original.actor, context.actor) || original.surface !== context.surface) return;
84
+ const inputReference = book.rows.flatMap((row, rowIndex) => row.type === 'message' && row.message?.role === 'assistant' && Array.isArray(row.message.content) ? row.message.content.flatMap((block, blockIndex) => block.type === 'tool_use' && block.id === proposal.call_id && block.name === proposal.operation && same(block.input, input) ? [{ row: rowIndex, block: blockIndex }] : []) : []);
85
+ if (inputReference.length !== 1 || inputReference[0].row >= book.rows.indexOf(original) || terminalGoal(book)) return;
86
+ const file = await canonical(w, input.file_path, proposal.operation), relative = path.relative(w.cwd, file);
87
+ const proof = inspection(book, audit, original, relative, isNative, house.registry);
88
+ if (proposal.operation === 'Edit' && !proof) return;
89
+ const expected = input.expected_sha256 ?? proof?.sha256 ?? 'absent';
90
+ const data = { format: 'comal-native-source-request/1', request_id: randomUUID(), call_id: proposal.call_id, operation: proposal.operation,
91
+ arguments_digest: proposal.arguments_digest, contract_digest: op.digest, input_ref: inputReference[0], input_bytes: Buffer.byteLength(JSON.stringify(input)),
92
+ requested_path: input.file_path, canonical_path: relative, expected_sha256: expected,
93
+ baseline_basis: input.expected_sha256 !== undefined ? 'explicit_precondition' : proof ? 'own_dated_native_content_proof' : 'create_only_intent_not_observed_absence',
94
+ inspection: proof, scope: w.sourceProposals.scope(book), request_scope: scope(book, house.stateContext()), authority_stamp: authority, limits: SOURCE_REQUEST_LIMITS };
95
+ const binding = w.sourceProposals.scopes.get(context.sessionId);
96
+ await w.sourceProposals.requireAccess(data, { ...context, invocation_id: proposal.invocation_id, signal }, book, binding);
97
+ if (active.size >= SOURCE_REQUEST_LIMITS.max_active) throw fail('Too many native source requests are active.', 'SOURCE_REQUEST_LIMIT');
98
+ const token = Object.freeze({}), entry = { manager, frame, data, digest: digest(data), file, authority, scope: data.request_scope, binding, token, consumed: false, retired: false };
99
+ active.set(proposal.invocation_id, entry); tokens.set(token, entry);
100
+ await recordWork({ type: 'source_request_prepared', source: 'workspace_host', call_id: proposal.call_id, request: data, request_digest: entry.digest });
101
+ await verify(entry, false);
102
+ return token;
103
+ }
104
+ async function verify(entry, requireActive = true) {
105
+ const { manager, frame, data, file } = entry, { workspace: w, isNative } = manager, { house, context, op, input, proposal, signal } = frame;
106
+ signal?.throwIfAborted();
107
+ if (entry.retired || manager.active.get(proposal.invocation_id) !== entry || !isNative(proposal.operation) || house.registry.get(proposal.operation) !== op || house.authority(context) !== entry.authority || requireActive && !house.stateContext().active.has(proposal.invocation_id)) throw fail('The original source request or authority changed.', 'HOUSE_AUTHORITY_CHANGED');
108
+ if (await canonical(w, input.file_path, proposal.operation) !== file) throw fail('The source-request alias changed; no replacement target was adopted.', 'SOURCE_CHANGED');
109
+ const book = await w.store.load(context.sessionId), audit = auditExecution(book, house.stateContext());
110
+ if (book.id !== context.sessionId || book.cwd !== w.cwd || !audit.journal.consistent || audit.violations.length || audit.unresolved.length) throw fail('The native source request no longer verifies.', 'AUDIT_INVALID');
111
+ durable(book, entry);
112
+ if (!same(scope(book, house.stateContext()), entry.scope) || terminalGoal(book)) throw fail('The original source-request task, goal or reset scope changed.', 'SOURCE_SCOPE_CHANGED');
113
+ if (w.sourceProposals.scopes.get(context.sessionId) !== entry.binding) throw fail('The original native source binding changed.', 'SOURCE_SCOPE_CHANGED');
114
+ await w.sourceProposals.requireAccess(data, { ...context, invocation_id: proposal.invocation_id, signal }, book, entry.binding);
115
+ // The host scope callback may itself await or append. Its old book cannot
116
+ // certify the task/goal/reset after that boundary.
117
+ const latest = await w.store.load(context.sessionId), finalAudit = auditExecution(latest, house.stateContext());
118
+ if (latest.id !== context.sessionId || latest.cwd !== w.cwd || !finalAudit.journal.consistent || finalAudit.violations.length || finalAudit.unresolved.length) throw fail('The current source-request journal does not verify.', 'AUDIT_INVALID');
119
+ durable(latest, entry);
120
+ if (!same(scope(latest, house.stateContext()), entry.scope) || terminalGoal(latest) || w.sourceProposals.scopes.get(context.sessionId) !== entry.binding) throw fail('The original source-request scope changed during validation.', 'SOURCE_SCOPE_CHANGED');
121
+ if (await canonical(w, input.file_path, proposal.operation) !== file) throw fail('The source-request alias changed during validation.', 'SOURCE_CHANGED');
122
+ signal?.throwIfAborted();
123
+ if (house.authority(context) !== entry.authority || entry.retired || house.registry.get(proposal.operation) !== op) throw fail('The original source request changed during validation.', 'HOUSE_AUTHORITY_CHANGED');
124
+ }
125
+ function durable(book, entry) {
126
+ const { frame: { context, proposal, input }, manager: { workspace: w }, data } = entry;
127
+ const records = book.rows.filter(row => row.type === 'source_request_prepared' && row.invocation_id === proposal.invocation_id);
128
+ const originals = book.rows.filter(row => row.type === 'house_proposal' && row.invocation_id === proposal.invocation_id);
129
+ const original = originals[0], block = book.rows[data.input_ref.row]?.message?.content?.[data.input_ref.block];
130
+ if (records.length !== 1 || records[0].request_digest !== entry.digest || !same(records[0].request, data) || !sourceOwn(records[0], { ...context, session_id: context.sessionId, house: w.cwd }) || originals.length !== 1 || original.arguments_digest !== data.arguments_digest || original.contract_digest !== data.contract_digest || original.operation !== data.operation || original.call_id !== data.call_id || !sourceOwn(original, records[0]) || !same(sourceInput(book.rows, original), input) || block?.id !== data.call_id || block?.name !== data.operation || !same(block.input, input)) throw fail('The durable original source request changed.');
131
+ if (data.inspection) {
132
+ const proof = data.inspection, row = book.rows[proof.receipt_row];
133
+ if (row?.type !== 'house_receipt' || row.invocation_id !== proof.receipt_id || row.status !== 'returned' || row.operation !== proof.operation || row.contract_digest !== proof.contract_digest || row.arguments_digest !== proof.arguments_digest || row.output_digest !== proof.output_digest || !sourceOwn(row, original)) throw fail('The original source inspection changed.');
134
+ }
135
+ }
136
+ export function consumeNativeSourceRequest(workspace, token, { operation, input, invocation_id, actor, sessionId }) {
137
+ if (token === undefined) return null;
138
+ const entry = tokens.get(token), frame = entry?.frame;
139
+ if (!entry || entry.retired || entry.consumed || entry.manager.workspace !== workspace || frame.proposal.operation !== operation || frame.proposal.invocation_id !== invocation_id || frame.context.sessionId !== sessionId || !same(frame.context.actor, actor) || !same(frame.input, input) || !workspace.house.stateContext().active.has(invocation_id)) throw fail('No genuine active native source request belongs to this writer.');
140
+ entry.consumed = true;
141
+ return { expected_sha256: entry.data.expected_sha256, file: entry.file, request_digest: entry.digest, request_id: entry.data.request_id,
142
+ async validate(beforeHash) { await verify(entry); if (beforeHash !== undefined && entry.data.expected_sha256 !== (beforeHash ?? 'absent')) throw fail('The original source-request version changed; it was not refreshed.', 'SOURCE_CHANGED'); if (beforeHash !== undefined && beforeHash !== null && !entry.data.inspection) throw fail('The original source request has no durable own inspected-content proof.', 'SOURCE_REQUEST_INSPECTION'); },
143
+ revalidate: () => verify(entry) };
144
+ }
145
+ export function settleNativeSourceRequest(workspace, { invocation_id }) {
146
+ const manager = managers.get(workspace), entry = manager?.active.get(invocation_id);
147
+ if (entry) { entry.retired = true; manager.active.delete(invocation_id); tokens.delete(entry.token); }
148
+ }
@@ -0,0 +1,24 @@
1
+ import {digest} from './operations.mjs';
2
+ import {sourceTaskRevisions} from './source-task-revision-proof.mjs';
3
+ export const SOURCE_TASK_MECHANISM='comal-source-task-v1';
4
+ export const SOURCE_TASK_MECHANISM_V2='comal-source-task-v2';
5
+ const own=(a,b)=>['house','session_id','actor','surface'].every(k=>digest(a[k]??null)===digest(b[k]??null));
6
+ export const savedSourceTask=book=>book.rows.slice(book.rows.findLastIndex(r=>r.type==='reset')+1).findLast(r=>r.type==='source_task_accepted'&&r.source==='source_task_host');
7
+ export function sourceTaskFactValid(row){return row?.type==='source_task_accepted'&&row.source==='source_task_host'&&[SOURCE_TASK_MECHANISM,SOURCE_TASK_MECHANISM_V2].includes(row.mechanism)&&row.task_digest===digest(row.task)&&row.binding_digest===digest(row.binding)&&row.binding?.session_id===row.session_id&&row.binding?.cwd===row.house&&digest(row.binding?.actor)===digest(row.actor);}
8
+ export function nativeSourceTask(rows,p){const c=rows.findLast(r=>r.type==='house_contract'&&r.contract_digest===p?.contract_digest)?.contract;if(!c||c.origin!==null||c.name!==p.operation||c.permission!==(p.operation==='SourceEvidence'?'GitIntegrationEvidence':p.operation==='SourceStatus'||p.operation==='SourceVerify'?'GitIntegrationStatus':p.operation))return false;if(['SourceDeliver','SourceRevise','SourceStatus','SourceVerify','SourceEvidence'].includes(p.operation))return [SOURCE_TASK_MECHANISM,SOURCE_TASK_MECHANISM_V2].includes(c.revision)&&(p.operation!=='SourceRevise'||c.revision===SOURCE_TASK_MECHANISM_V2)&&c.steps===['SourceDeliver','SourceRevise'].includes(p.operation);
9
+ if(p.operation!=='WorkTaskUpdate')return false;const parent=rows.find(r=>r.type==='house_proposal'&&r.invocation_id===p.parent_invocation_id);return ['SourceDeliver','SourceRevise'].includes(parent?.operation)&&own(parent,p)&&nativeSourceTask(rows,parent);
10
+ }
11
+ export function sourceTaskRecoveryPlan(rows,p){
12
+ if(!nativeSourceTask(rows,p))return null;
13
+ const call=rows.find(r=>r.type==='source_task_call_prepared'&&r.source==='source_task_host'&&r.invocation_id===p.invocation_id&&r.operation===p.operation&&r.arguments_digest===p.arguments_digest&&own(r,p));
14
+ const close=p.operation==='WorkTaskUpdate',read=['SourceStatus','SourceVerify','SourceEvidence'].includes(p.operation),proposalIndex=rows.findIndex(r=>r.type==='house_proposal'&&r.invocation_id===p.invocation_id),accepted=rows.slice(0,proposalIndex).findLast(r=>r.type==='source_task_accepted'&&sourceTaskFactValid(r)&&own(r,p)&&(!call||r.task_digest===call.task_digest));if(!accepted)return null;
15
+ const root=close?rows.find(r=>r.type==='house_proposal'&&r.invocation_id===p.parent_invocation_id):p,contract=rows.findLast(r=>r.type==='house_contract'&&r.contract_digest===root.contract_digest)?.contract;if(accepted.mechanism!==contract?.revision)return null;
16
+ if(accepted.mechanism===SOURCE_TASK_MECHANISM_V2){try{sourceTaskRevisions(rows,accepted);}catch{return null;}}
17
+ const children=rows.filter(r=>r.type==='house_proposal'&&r.parent_invocation_id===p.invocation_id);
18
+ if(!call&&!close&&children.length)return null;
19
+ if(children.some(c=>!own(c,p)||!rows.some(r=>r.type==='house_receipt'&&r.invocation_id===c.invocation_id)))return null;
20
+ if(close){const prepared=rows.find(r=>r.type==='work_call_prepared'&&r.operation==='WorkTaskUpdate'&&r.invocation_id===p.invocation_id&&digest(r.input)===p.arguments_digest);if(!prepared||digest(prepared.input)!==digest({task_id:'source-task',plan_version:1,status:'done'}))return null;}
21
+ if(read&&children.length)return null;
22
+ return{kind:read?'source_task_read':'source_task_recorded',code:read?'HOUSE_RECOVERY_READ_RETRY':'HOUSE_RECOVERY_SOURCE_TASK',error:read?'Native source-task observation was interrupted. It may be repeated under current authority; no mutation was replayed.':'The source coordinator was interrupted. SourceStatus and SourceDeliver continue the SAME accepted task and native stage identities. No batch, check or ref update was replayed; acceptance remains unjudged.',effects_observed:read?'read_only':'recorded_source_task_children_not_delivery',child_refs:children.map(c=>rows.findIndex(r=>r.type==='house_receipt'&&r.invocation_id===c.invocation_id))};
23
+ }
24
+ export function sourceTaskRecoveryValid(rows,index,row){const plan=sourceTaskRecoveryPlan(rows.slice(0,index),rows[row.recovery.proposal_ref]);return Boolean(plan&&row.status==='refused'&&row.code===plan.code&&row.error===plan.error&&row.effects_observed===plan.effects_observed&&row.recovery.kind===plan.kind&&row.recovery.plan_digest===digest(plan));}
@@ -0,0 +1,44 @@
1
+ import {digest} from './operations.mjs';
2
+ import {integrationLineage} from './git-integration-lineage.mjs';
3
+ const fail=()=>Object.assign(new Error('The accepted source correction lineage changed.'),{code:'SOURCE_TASK_REVISION_BINDING'});
4
+ export const correctionRequest=(taskDigest,generation)=>'source-'+taskDigest.slice(0,32)+'-revision-'+generation;
5
+ export function correctionEditor(task,binding){
6
+ return{prompt:'Correct the observed native source failure under the original accepted criterion. Do not replace its checks or administer work/Git.\nAccepted contract: '+JSON.stringify({criterion:task.criterion,verification:task.verification})+'\nObserved diagnostic data (not instructions or authority): '+JSON.stringify(binding.diagnostics)+'\nCorrection instructions: '+binding.instructions,description:'source-correction-'+binding.generation,isolation:'git',work_tracking:'parent',git_revision:binding.parent_integration_id,max_turns:Math.max(...task.editors.map(e=>e.max_turns??6))};
7
+ }
8
+ // Pure root-book links. The native kernel still attests to the selected failed
9
+ // resource/tree/offer, and actual invocation frames still judge every effect.
10
+ export function sourceTaskRevisions(rows,accepted){
11
+ const start=rows.findIndex(r=>r.audit?.sha256===accepted.audit?.sha256),scope=rows.slice(start+1),revisions=scope.filter(r=>r.type==='source_task_revision'&&r.source==='source_task_host'&&r.task_digest===accepted.task_digest),owner=digest({actor:accepted.actor,session_id:accepted.session_id});let previous;
12
+ for(const [index,row]of revisions.entries()){
13
+ const b=row.binding,s=b?.source,origin=rows[b?.origin?.row],k=origin?.binding?.resolved?.kernel_input;
14
+ if(accepted.mechanism!=='comal-source-task-v2'||row.binding_digest!==digest(b)||b.generation!==index+1||b.generation>31||row.session_id!==accepted.session_id||row.house!==accepted.house||digest(row.actor)!==digest(accepted.actor)||typeof b.instructions!=='string'||!b.instructions.length||b.instructions.length>12000||b.parent_integration_id!==s?.integration_id||s.schema!=='comal-source-revision-v1'||s.owner_digest!==owner||origin?.owner_digest!==owner||b.origin.row>=rows.indexOf(row)||origin.audit.sha256!==b.origin.digest||origin.binding_digest!==b.origin.binding_digest||origin.binding.resolved.integration_id!==s.integration_id||s.target!==k.target||s.expected_target!==k.expected_target||s.base_commit!==k.base_commit||s.verification_digest!==digest(k.verification)||k.target!==accepted.task.target||digest(k.verification)!==digest(accepted.task.verification)||digest(b.editor_input)!==digest(correctionEditor(accepted.task,b)))throw fail();
15
+ integrationLineage(rows,origin);
16
+ const expected=previous?correctionRequest(accepted.task_digest,previous.binding.generation):'source-'+accepted.task_digest.slice(0,40);if(origin.input.request_id!==expected)throw fail();
17
+ const executions=scope.filter(r=>r.type==='source_task_execution_bound'&&r.source==='source_task_host'&&r.task_digest===accepted.task_digest);if(executions.length!==1||b.goal_owner!==executions[0].goal_owner)throw fail();previous=row;
18
+ }
19
+ for(const used of scope.filter(r=>r.type==='source_task_revision_used'&&r.source==='source_task_host'&&r.task_digest===accepted.task_digest)){
20
+ const revision=revisions.find(r=>r.binding.generation===used.generation),proposal=rows.find(r=>r.type==='house_proposal'&&r.invocation_id===used.invocation_id),contract=proposal&&rows.findLast(r=>r.type==='house_contract'&&r.contract_digest===proposal.contract_digest)?.contract;
21
+ if(!revision||used.revision_digest!==revision.audit.sha256||rows.indexOf(revision)>=rows.indexOf(used)||used.session_id!==accepted.session_id||used.house!==accepted.house||digest(used.actor)!==digest(accepted.actor)||!['SourceDeliver','SourceRevise'].includes(proposal?.operation)||contract?.origin!==null||contract.revision!==accepted.mechanism)throw fail();
22
+ }
23
+ return revisions;
24
+ }
25
+
26
+ export function auditSourceTaskRevisions(rows){
27
+ const violations=[];
28
+ for(let i=0;i<rows.length;i++){const accepted=rows[i];if(accepted.type!=='source_task_accepted'||accepted.source!=='source_task_host'||accepted.mechanism!=='comal-source-task-v2')continue;let end=rows.findIndex((r,n)=>n>i&&(r.type==='reset'||r.type==='source_task_accepted'&&r.source==='source_task_host'));if(end<0)end=rows.length;try{sourceTaskRevisions(rows.slice(0,end),accepted);}catch{violations.push({invocation_id:accepted.invocation_id,operation:'SourceRevise',reason:'source_task_revision_binding_invalid'});}}
29
+ for(let index=0;index<rows.length;index++){
30
+ const receipt=rows[index];if(receipt.type!=='house_receipt'||receipt.status!=='returned'||!['SourceDeliver','SourceRevise'].includes(receipt.operation))continue;
31
+ const prefix=rows.slice(0,index),contract=prefix.findLast(r=>r.type==='house_contract'&&r.contract_digest===receipt.contract_digest)?.contract;if(contract?.origin!==null||contract.revision!=='comal-source-task-v2')continue;
32
+ const fault=reason=>violations.push({invocation_id:receipt.invocation_id,operation:receipt.operation,reason});
33
+ try{
34
+ const accepted=prefix.findLast(r=>r.type==='source_task_accepted'&&r.source==='source_task_host'&&r.session_id===receipt.session_id),revisions=sourceTaskRevisions(prefix,accepted),requests=new Set(['source-'+accepted.task_digest.slice(0,40),...revisions.map(r=>correctionRequest(accepted.task_digest,r.binding.generation))]),original=prefix.findLast(r=>r.type==='git_integration_intent'&&r.operation==='GitIntegrationPrepare'&&requests.has(r.input.request_id)&&r.owner_digest===digest({actor:receipt.actor,session_id:receipt.session_id}));if(!original)continue;
35
+ const chain=integrationLineage(prefix,original),goal=prefix.findLast(r=>r.type==='goal')?.goal,owner=goal?.status==='active'?goal.id:null,decisions=prefix.filter(r=>r.type==='house_effect_constraint'&&r.source==='scope_host'&&r.invocation_id===receipt.invocation_id&&r.operation===receipt.operation&&r.arguments_digest===receipt.arguments_digest&&r.session_id===receipt.session_id&&r.house===receipt.house&&digest(r.actor)===digest(receipt.actor));
36
+ for(const intent of chain){
37
+ if(intent.binding.authority.denied.includes(receipt.operation))fault('source_task_original_controller_permission_refused');if(intent.binding.goal_owner!==owner)fault('source_task_original_controller_goal_changed');
38
+ const c=intent.binding.ceiling,expected=[...c.guards.map(identity=>({kind:'guard',identity})),...c.laws.flatMap(law=>law.plan.gates.filter(g=>g.tier==='enforced'&&(g.operations.includes('*')||g.operations.includes(receipt.operation))).map(g=>({kind:'law',identity:{house:law.house,session_id:law.session_id,activation_id:law.activation_id,plan_hash:law.plan.plan_hash,mechanism_stamp:law.mechanism_stamp,gate_id:g.id}})))];
39
+ for(const constraint of expected)if(!decisions.some(r=>r.ceiling_digest===c.digest&&r.kind===constraint.kind&&digest(r.identity)===digest(constraint.identity)&&r.allowed===true))fault('source_task_original_controller_constraint_missing');if(decisions.some(r=>r.ceiling_digest===c.digest&&r.allowed===false))fault('source_task_original_controller_constraint_refused');
40
+ }
41
+ }catch{fault('source_task_controller_binding_invalid');}
42
+ }
43
+ return{scope:'recorded_correction_lineage_and_controller_constraints_not_live_verification',violations};
44
+ }