@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.
- package/README.md +19 -1
- package/backend/contracts/remote-commands-v1.json +12 -0
- package/governance/chrome-task-v10.json +323 -0
- package/governance/chrome-task-v11.json +355 -0
- package/governance/chrome-task-v12.json +387 -0
- package/governance/chrome-task-v13.json +419 -0
- package/governance/chrome-task-v3.json +99 -0
- package/governance/chrome-task-v4.json +131 -0
- package/governance/chrome-task-v5.json +163 -0
- package/governance/chrome-task-v6.json +195 -0
- package/governance/chrome-task-v7.json +227 -0
- package/governance/chrome-task-v8.json +259 -0
- package/governance/chrome-task-v9.json +291 -0
- package/governance/source-task-v1.json +1 -0
- package/governance/source-task-v2.json +1 -0
- package/package.json +14 -1
- package/src/agent-recovery-proof.mjs +12 -1
- package/src/agent.mjs +94 -30
- package/src/audit.mjs +21 -6
- package/src/auto-guard.mjs +48 -1
- package/src/chrome-candidate-recovery-proof.mjs +47 -0
- package/src/chrome-recovery-proof.mjs +6 -2
- package/src/chrome-source-syntax.mjs +93 -0
- package/src/cli.mjs +158 -54
- package/src/codex-provider.mjs +3 -2
- package/src/connection-settings.mjs +204 -0
- package/src/context-pressure.mjs +2 -1
- package/src/durable-agent-runs.mjs +5 -11
- package/src/event-channels.mjs +8 -0
- package/src/file-recovery-proof.mjs +11 -3
- package/src/file-recovery.mjs +24 -0
- package/src/gemini-provider.mjs +6 -1
- package/src/git-agent-revision.mjs +12 -0
- package/src/git-agent-workspaces.mjs +143 -0
- package/src/git-integration-lineage.mjs +20 -0
- package/src/git-integration-operations.mjs +200 -0
- package/src/git-integration-recovery-proof.mjs +70 -0
- package/src/git-page-cli-bridge.mjs +61 -0
- package/src/git-page-evidence.mjs +30 -0
- package/src/git-page-identity.mjs +10 -0
- package/src/git-page-proof.mjs +96 -0
- package/src/git-page-settlement.mjs +21 -0
- package/src/git-page-verification.mjs +134 -0
- package/src/git-plumbing.mjs +212 -0
- package/src/git-process-worker.mjs +50 -0
- package/src/git-source.mjs +110 -0
- package/src/git-workspaces.mjs +519 -0
- package/src/goals.mjs +13 -7
- package/src/house.mjs +246 -20
- package/src/i18n/cli-es.mjs +4 -0
- package/src/i18n/es.mjs +35 -0
- package/src/judges.mjs +27 -11
- package/src/mcp-runtime.mjs +15 -3
- package/src/model-controls.mjs +11 -1
- package/src/model-deadline.mjs +4 -3
- package/src/native-refusal.mjs +38 -0
- package/src/onboarding.mjs +88 -24
- package/src/openai-provider.mjs +14 -7
- package/src/operations.mjs +9 -2
- package/src/output-recovery.mjs +48 -0
- package/src/peer-tools.mjs +18 -8
- package/src/permission-controls.mjs +232 -0
- package/src/permission-rules.mjs +312 -0
- package/src/permissions.mjs +113 -30
- package/src/plugin-catalog.mjs +84 -0
- package/src/plugin-config.mjs +48 -6
- package/src/plugin-controls.mjs +332 -0
- package/src/plugin-notices.mjs +88 -0
- package/src/plugin-trust.mjs +104 -0
- package/src/plugins/chrome-lab/bridge.mjs +38 -12
- package/src/plugins/chrome-lab/client.mjs +103 -25
- package/src/plugins/chrome-lab/diagnostics.mjs +19 -0
- package/src/plugins/chrome-lab/index.mjs +116 -26
- package/src/plugins/chrome-lab/project.mjs +30 -2
- package/src/plugins/chrome-lab/scoped-input.mjs +135 -0
- package/src/plugins/chrome-lab/worker-diagnostics.mjs +51 -0
- package/src/plugins/open-relay/actions.mjs +205 -0
- package/src/plugins/open-relay/bound.mjs +47 -0
- package/src/plugins/open-relay/client.mjs +158 -0
- package/src/plugins/open-relay/dossier.mjs +119 -0
- package/src/plugins/open-relay/index.mjs +153 -0
- package/src/plugins/open-relay/lookup.mjs +45 -0
- package/src/plugins/open-relay/notices.mjs +79 -0
- package/src/plugins/open-relay/watch.mjs +127 -0
- package/src/plugins.mjs +72 -29
- package/src/private-fs.mjs +40 -0
- package/src/process-lock.mjs +24 -13
- package/src/provider-auth.mjs +14 -3
- package/src/provider.mjs +11 -2
- package/src/read-only-check.mjs +7 -3
- package/src/scope-ceiling.mjs +56 -0
- package/src/scoped-policy.mjs +39 -0
- package/src/secret-login.mjs +68 -0
- package/src/session-bus.mjs +41 -7
- package/src/session-projections-candidate.mjs +119 -0
- package/src/session-projections.mjs +119 -0
- package/src/session-read-cache.mjs +23 -3
- package/src/sessions.mjs +35 -11
- package/src/source-editor-role.mjs +13 -0
- package/src/source-ownership.mjs +73 -0
- package/src/source-proposal-proof.mjs +55 -0
- package/src/source-proposals.mjs +118 -0
- package/src/source-requests.mjs +148 -0
- package/src/source-task-proof.mjs +24 -0
- package/src/source-task-revision-proof.mjs +44 -0
- package/src/subagents.mjs +187 -87
- package/src/tool-packs.mjs +5 -5
- package/src/tools.mjs +82 -15
- package/src/trusted-context.mjs +5 -1
- package/src/tui/connection-transition.mjs +1 -1
- package/src/tui/controller.mjs +8 -4
- package/src/tui/discovery.mjs +24 -12
- package/src/tui/inbound-blocks.mjs +55 -0
- package/src/tui/present.mjs +504 -0
- package/src/tui/screen.mjs +49 -13
- package/src/windows-private.ps1 +61 -0
- package/src/work-assertions.mjs +6 -2
- package/src/work-plan.mjs +34 -8
- package/src/work-state.mjs +15 -9
- package/src/workflows/chrome-page-cases.mjs +156 -0
- package/src/workflows/chrome-page-checkpoint-reader.mjs +10 -0
- package/src/workflows/chrome-page-diagnostics.mjs +113 -0
- package/src/workflows/chrome-page-law.mjs +48 -0
- package/src/workflows/chrome-page-recovery-host-v13.mjs +14 -0
- package/src/workflows/chrome-page-recovery-v13.mjs +104 -0
- package/src/workflows/chrome-page-task.mjs +429 -0
- package/src/workflows/chrome-page-verifier.mjs +123 -0
- package/src/workflows/chrome-source.mjs +73 -0
- package/src/workflows/chrome-supplied-inputs.mjs +43 -0
- package/src/workflows/chrome-task-adoption.mjs +122 -0
- package/src/workflows/chrome-task.mjs +379 -74
- package/src/workflows/source-task-v1.mjs +124 -0
- package/src/workflows/source-task-v2.mjs +227 -0
- package/src/workflows/source-task.mjs +13 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import {mkdir,open,readFile,readlink,realpath,lstat,link,unlink,symlink,rm} from 'node:fs/promises';
|
|
2
|
+
import {constants} from 'node:fs';
|
|
3
|
+
import {createHash,randomUUID} from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {inspectGitSource} from './git-source.mjs';
|
|
6
|
+
import {SessionStore} from './sessions.mjs';
|
|
7
|
+
import {sealRecord,auditExecution,auditJournal} from './audit.mjs';
|
|
8
|
+
import {assertOutput,validateAssertions,assertionSchema} from './work-assertions.mjs';
|
|
9
|
+
import Ajv from 'ajv';
|
|
10
|
+
import {readOnlyBashCheck} from './read-only-check.mjs';
|
|
11
|
+
import {digest,immutable} from './operations.mjs';
|
|
12
|
+
import {acquireProcessLock} from './process-lock.mjs';
|
|
13
|
+
import {GitPlumbing,mutateGit,mutateGitWithChildWitness,GIT_INPUT_GATE_PROTOCOL,assertGitPreparedInputCommand,assertGitChildWitness,assertGitChildJournal,readGit,gitFailure,gitWorkerGone,privateGitDirectory} from './git-plumbing.mjs';
|
|
14
|
+
import {isPageVerification,pageVerificationSchema,assertPageChain,assertNativePrefix,nativePrefix} from './git-page-proof.mjs';
|
|
15
|
+
import {hostCodeIdentity} from './git-page-identity.mjs';
|
|
16
|
+
import {reservePageCheck} from './git-page-verification.mjs';
|
|
17
|
+
|
|
18
|
+
const sha=bytes=>createHash('sha256').update(bytes).digest('hex'),line=result=>result.stdout.toString('utf8').replace(/\n$/,'');
|
|
19
|
+
const sourcePath=name=>{
|
|
20
|
+
if(typeof name!=='string'||path.isAbsolute(name)||name.includes('\0')||name.split('/').some(part=>!part||part==='..'||part==='.'||part.toLowerCase()==='.git')||/^(?:\.comal|\.harness|\.analysis)(?:\/|$)/.test(name))throw gitFailure('Unsupported source path.','GIT_SOURCE_UNSUPPORTED');return name;
|
|
21
|
+
};
|
|
22
|
+
const ownerDigest=owner=>{
|
|
23
|
+
if(!owner?.actor?.id||!['agent','subagent','human'].includes(owner.actor.kind)||!owner.session_id||Buffer.byteLength(JSON.stringify(owner))>4096)throw gitFailure('A host-bound source owner is required.','GIT_OWNER_REQUIRED');return digest(owner);
|
|
24
|
+
};
|
|
25
|
+
export function gitWorkspaceResourceId(cwd,owner,request){const hex=digest({mechanism:'comal-git-workspace-v1',cwd,owner:ownerDigest(owner),request}).slice(0,32);return`${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;}
|
|
26
|
+
const resourceId=gitWorkspaceResourceId;
|
|
27
|
+
const publicWork=work=>({...work,scope:'isolated_source_not_verified_delivery',asked:'unjudged'});
|
|
28
|
+
const checkContract=new Ajv({strict:false}).compile({type:'object',additionalProperties:false,required:['criterion','checks'],properties:{criterion:{type:'string',minLength:1,maxLength:4000},checks:{type:'array',minItems:1,maxItems:8,items:{type:'object',additionalProperties:false,required:['id','command'],properties:{id:{type:'string',pattern:'^[a-zA-Z0-9_.:-]{1,80}$'},command:{type:'string',minLength:1,maxLength:2000},timeout:{type:'integer',minimum:1,maximum:120000},assertions:{type:'array',maxItems:8,items:assertionSchema}}}}}});
|
|
29
|
+
const checkPageContract=new Ajv({strict:false}).compile(pageVerificationSchema);
|
|
30
|
+
|
|
31
|
+
async function readCaptured(root,file,budget,signal){
|
|
32
|
+
const name=sourcePath(file.path),target=path.join(root,name),parent=await realpath(path.dirname(target)),relative=path.relative(root,parent);
|
|
33
|
+
if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative))throw gitFailure('Source parent escaped its workspace.','GIT_SOURCE_CHANGED');
|
|
34
|
+
let bytes;
|
|
35
|
+
if(file.kind==='symlink')bytes=await readlink(target,{encoding:'buffer'});
|
|
36
|
+
else{
|
|
37
|
+
const handle=await open(target,constants.O_RDONLY|(constants.O_NOFOLLOW||0));
|
|
38
|
+
try{const info=await handle.stat();if(!info.isFile()||info.size>budget.bytes)throw gitFailure('Source capture exceeds its bound.','GIT_SOURCE_BOUND');const chunks=[];let count=0;
|
|
39
|
+
for(;;){signal?.throwIfAborted();const chunk=Buffer.alloc(128*1024),read=await handle.read(chunk,0,chunk.length,null);if(!read.bytesRead)break;count+=read.bytesRead;if(count>budget.bytes)throw gitFailure('Source capture exceeds its bound.','GIT_SOURCE_BOUND');chunks.push(chunk.subarray(0,read.bytesRead));}bytes=Buffer.concat(chunks);
|
|
40
|
+
}finally{await handle.close();}
|
|
41
|
+
}
|
|
42
|
+
budget.bytes-=bytes.length;if(budget.bytes<0)throw gitFailure('Source capture exceeds its bound.','GIT_SOURCE_BOUND');
|
|
43
|
+
if(sha(bytes)!==file.sha256)throw gitFailure('Inspected source bytes changed before capture.','GIT_SOURCE_CHANGED');
|
|
44
|
+
return{path:name,mode:file.kind==='symlink'?'120000':file.executable?'100755':'100644',oid:file.blob_oid,bytes};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Kernel first: the public operation/delegation adapter must bind owner and
|
|
48
|
+
// verifier authority in host code. Neither is accepted from model JSON here.
|
|
49
|
+
export class GitWorkspaces {
|
|
50
|
+
constructor({cwd,directory,fault=async()=>{},verifierFactory,verifierIdentity,beforeEffect=async()=>{}}){this.cwd=path.resolve(cwd);this.directory=directory;this.fault=fault;this.verifierFactory=verifierFactory;this.verifierIdentity=verifierIdentity;this.beforeEffect=beforeEffect;}
|
|
51
|
+
pageKernelIdentity(){if(!/^[a-f0-9]{64}$/.test(this.verifierIdentity||''))throw gitFailure('Actual host integration implementation identity is unavailable.','GIT_VERIFIER_REQUIRED');return digest({format:'comal-git-page-kernel/1',kernel:hostCodeIdentity(this,GitWorkspaces),integration_adapter:this.verifierIdentity});}
|
|
52
|
+
async initialize(){
|
|
53
|
+
this.cwd=await realpath(this.cwd);this.directory=await privateGitDirectory(this.directory||path.join(this.cwd,'.comal/git'));
|
|
54
|
+
this.worktrees=await privateGitDirectory(path.join(this.directory,'worktrees'));this.resources=await privateGitDirectory(path.join(this.directory,'resources'));
|
|
55
|
+
}
|
|
56
|
+
async context(id,create=false){
|
|
57
|
+
if(!/^[a-f0-9-]{36}$/.test(id))throw gitFailure('Invalid Git resource identity.','GIT_RESOURCE_ID');
|
|
58
|
+
const directory=path.join(this.resources,id);if(create)await privateGitDirectory(directory);const store=new SessionStore({cwd:this.cwd,stateDir:path.join(directory,'ledger')});if(create)await mkdir(store.dir,{recursive:true,mode:0o700});
|
|
59
|
+
return{id,directory,store,file:store.file(id),append:event=>store.append(id,{...event,source:'git_host',mechanism:'comal-git-workspace-v1'})};
|
|
60
|
+
}
|
|
61
|
+
async load(c,owner,observed){
|
|
62
|
+
const book=observed||await c.store.load(c.id);if(!book.journalAudit.verified)throw gitFailure('Git resource journal does not verify.');
|
|
63
|
+
const roots=book.rows.filter(row=>row.type==='git_resource_created'),root=roots[0];
|
|
64
|
+
if(!roots.length&&book.rows.length===1)throw gitFailure('Git resource identity was not yet initialized.','GIT_RESOURCE_UNINITIALIZED');
|
|
65
|
+
if(book.id!==c.id||book.cwd!==this.cwd||roots.length!==1||root.source!=='git_host'||root.mechanism!=='comal-git-workspace-v1'||root.owner_digest!==ownerDigest(owner)||digest(root.owner)!==root.owner_digest||root.root!==this.cwd||root.resource_id!==c.id||root.spec_digest!==digest(root.spec)||root.spec.repository?.root!==this.cwd)throw gitFailure('Git resource belongs to another host-bound owner or has an invalid binding.','GIT_OWNER_MISMATCH');
|
|
66
|
+
const prepared=new Map(),phases=new Set(),endings=new Set();
|
|
67
|
+
for(const row of book.rows){
|
|
68
|
+
if(!row.type?.startsWith('git_'))continue;
|
|
69
|
+
if(row.source!=='git_host'||row.mechanism!=='comal-git-workspace-v1')throw gitFailure('Git journal contains an unbound host event.');
|
|
70
|
+
const key=JSON.stringify([row.action,row.attempt]);
|
|
71
|
+
if(row.type==='git_action_prepared'){if(prepared.has(key)||row.input_digest!==digest(row.input))throw gitFailure('Git action preparation changed.');prepared.set(key,row);}
|
|
72
|
+
if(['git_action_phase','git_action_returned','git_action_rejected'].includes(row.type)&&!prepared.has(key))throw gitFailure('Git event has no original action preparation.');
|
|
73
|
+
if(row.type==='git_action_phase'){const phaseKey=key+':'+row.phase;if(phases.has(phaseKey)||row.value_digest!==digest(row.value))throw gitFailure('Git phase is duplicated or changed.');phases.add(phaseKey);}
|
|
74
|
+
if(['git_action_returned','git_action_rejected'].includes(row.type)){if(endings.has(key)||row.type==='git_action_returned'&&row.output_digest!==digest(row.output))throw gitFailure('Git action outcome is duplicated or changed.');endings.add(key);}
|
|
75
|
+
}
|
|
76
|
+
// v1 rows are unchanged; only the explicit new native grammar opts in.
|
|
77
|
+
try{assertGitChildJournal(book.rows);}catch(error){throw gitFailure('Native Git child journal does not verify. '+error.message);}
|
|
78
|
+
return{book,root};
|
|
79
|
+
}
|
|
80
|
+
async lock(c){
|
|
81
|
+
return acquireProcessLock(c.file+'.resource.lock',{resource:{kind:'comal-git-workspace-v1',id:c.id},validate:async()=>{
|
|
82
|
+
try{const book=await c.store.load(c.id);if(!book.journalAudit.verified)throw gitFailure('Git resource journal is damaged.');}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
83
|
+
}});
|
|
84
|
+
}
|
|
85
|
+
async create(c,owner,spec){
|
|
86
|
+
let handle;try{handle=await open(c.file,'wx',0o600);}catch(error){if(error.code!=='EEXIST')throw error;}
|
|
87
|
+
if(handle)try{await handle.writeFile(JSON.stringify(sealRecord({type:'session',id:c.id,cwd:this.cwd,model:'house-git-resource',provider:'host',created_at:new Date().toISOString()},0))+'\n');await handle.sync();}finally{await handle.close();}
|
|
88
|
+
const book=await c.store.load(c.id);
|
|
89
|
+
if(!book.rows.some(row=>row.type==='git_resource_created'))await c.append({type:'git_resource_created',resource_id:c.id,root:this.cwd,owner,owner_digest:ownerDigest(owner),spec,spec_digest:digest(spec)});
|
|
90
|
+
const loaded=await this.load(c,owner);if(loaded.root.spec_digest!==digest(spec)||digest(loaded.root.spec)!==digest(spec))throw gitFailure('Request ID already names another source intent. Use a new request ID.','GIT_REQUEST_CONFLICT');return loaded;
|
|
91
|
+
}
|
|
92
|
+
async action(c,owner,action,attempt,input,run,signal){
|
|
93
|
+
let{book,root}=await this.load(c,owner);const key={action,attempt},prepared=book.rows.find(row=>row.type==='git_action_prepared'&&row.action===action&&row.attempt===attempt);
|
|
94
|
+
if(book.rows.some(row=>row.type==='git_resource_retired'))throw gitFailure('This source resource is retired. Its files, history and unfinished observations were preserved.','GIT_RESOURCE_RETIRED');
|
|
95
|
+
if(prepared&&prepared.input_digest!==digest(input))throw gitFailure('This Git attempt already names different arguments.','GIT_REQUEST_CONFLICT');
|
|
96
|
+
const terminal=book.rows.findLast(row=>row.type==='git_action_returned'&&row.action===action&&row.attempt===attempt);
|
|
97
|
+
if(terminal){if(terminal.output_digest!==digest(terminal.output))throw gitFailure('Git resource outcome does not verify.');return terminal.output;}
|
|
98
|
+
const rejected=book.rows.findLast(row=>row.type==='git_action_rejected'&&row.action===action&&row.attempt===attempt);
|
|
99
|
+
if(rejected)throw gitFailure('This intent was rejected before source/ref dispatch. Inspect source and use a NEW request/attempt ID. '+rejected.error,rejected.code);
|
|
100
|
+
const pending=book.rows.filter(row=>row.type==='git_action_prepared'&&!book.rows.some(end=>['git_action_returned','git_action_rejected'].includes(end.type)&&end.action===row.action&&end.attempt===row.attempt));
|
|
101
|
+
if(pending.some(row=>row.action!==action||row.attempt!==attempt))throw gitFailure('Another Git action has an unsettled outcome. Inspect/reconcile its original attempt.');
|
|
102
|
+
if(!prepared)await c.append({type:'git_action_prepared',...key,input,input_digest:digest(input)});
|
|
103
|
+
book=await c.store.load(c.id);
|
|
104
|
+
for(const worker of book.rows.filter(row=>row.type==='git_command_prepared')){
|
|
105
|
+
const done=book.rows.some(row=>row.type==='git_command_returned'&&row.command_id===worker.command_id);
|
|
106
|
+
if(!done&&!gitWorkerGone(worker.worker))throw gitFailure('A recorded Git worker or its descendants may still be active. No command was restarted.','GIT_PROCESS_ACTIVE');
|
|
107
|
+
}
|
|
108
|
+
const stamp=book.rows.find(row=>row.type==='git_action_prepared'&&row.action===action&&row.attempt===attempt);
|
|
109
|
+
const phase=async(name,value)=>{const rows=(await c.store.load(c.id)).rows,prior=rows.find(row=>row.type==='git_action_phase'&&row.action===action&&row.attempt===attempt&&row.phase===name);
|
|
110
|
+
if(prior){if(prior.value_digest!==digest(prior.value)||value!==undefined&&digest(value)!==prior.value_digest)throw gitFailure('Prepared Git phase changed.');return prior.value;}
|
|
111
|
+
if(value===undefined)return null;await c.append({type:'git_action_phase',...key,phase:name,value,value_digest:digest(value)});await this.fault(name,{id:c.id,action,attempt,value});return value;
|
|
112
|
+
};
|
|
113
|
+
const mutate=async(args,options={})=>{
|
|
114
|
+
const command_id=randomUUID();let result;
|
|
115
|
+
try{const observed=process.platform==='linux';result=await (observed?mutateGitWithChildWitness:mutateGit)(this.cwd,args,{...options,...(observed?{commandId:command_id}:{}),recordWorker:async(worker,dispatch)=>{await c.append({type:'git_command_prepared',...key,command_id,args,input_sha256:dispatch?dispatch.input_sha256:sha(options.input||Buffer.alloc(0)),worker,...(dispatch?{dispatch_protocol:dispatch.format,native_args:dispatch.native_args,input_gate:dispatch.input_gate}:{})});await this.fault('command_gate',{id:c.id,args,worker});await this.beforeEffect({resource_id:c.id,action,attempt,kind:'git_dispatch',args});if(options.beforeRelease){await options.beforeRelease();await this.beforeEffect({resource_id:c.id,action,attempt,kind:'git_dispatch',args});}},...(observed?{recordChild:async witness=>{const rows=(await c.store.load(c.id)).rows,p=rows.find(r=>r.type==='git_command_prepared'&&r.command_id===command_id);if(!p)throw gitFailure('Native child has no durable command');assertGitPreparedInputCommand(p);assertGitChildWitness(witness,p);await c.append({type:'git_command_spawned',...key,command_id,prepared_sha256:p.audit.sha256,witness});await this.fault('command_child_gate',{id:c.id,args,worker:witness.worker,witness,command_id});await this.beforeEffect({resource_id:c.id,action,attempt,kind:'git_input_release',args});}}:{})});}
|
|
116
|
+
catch(error){if(error.git_dispatch==='not_released')await c.append({type:'git_command_returned',...key,command_id,code:null,dispatch:'not_released',scope:'no_git_process_started'});else if(error.git_exit)await c.append({type:'git_command_returned',...key,command_id,code:error.git_exit.code,output_sha256:sha(error.git_exit.stdout),scope:'native_git_exit_observed'});throw error;}
|
|
117
|
+
await c.append({type:'git_command_returned',...key,command_id,code:result.code,output_sha256:sha(result.stdout)});return result;
|
|
118
|
+
};
|
|
119
|
+
try{
|
|
120
|
+
const plumbing=new GitPlumbing({cwd:this.cwd,directory:path.join(c.directory,'objects'),repository:root.spec.repository,mutate});await plumbing.prepare(signal);
|
|
121
|
+
const output=await run({book,root,stamp,phase,mutate,plumbing});
|
|
122
|
+
await c.append({type:'git_action_returned',...key,output,output_digest:digest(output)});return output;
|
|
123
|
+
}catch(error){
|
|
124
|
+
const cutoff={open:'snapshot',checkpoint:'checkpoint',integration_prepare:'candidate',integration_apply:'integration_admitted',integration_verify:'verification_session'}[action],rows=(await c.store.load(c.id)).rows;
|
|
125
|
+
if(error.git_ref_precondition)await c.append({type:'git_action_rejected',...key,code:error.code,error:error.message,scope:error.git_ref_precondition.dispatched?'native_cas_rejected_or_ref_changed':'ref_precondition_refused_before_dispatch',evidence:error.git_ref_precondition,retry_authorized:false});
|
|
126
|
+
else if(error.git_verification_failure)await c.append({type:'git_action_rejected',...key,code:error.code,error:error.message,scope:'failed_native_verification_not_acceptance',evidence:error.git_verification_failure,partial_effects:'preserved_unjudged',retry_authorized:false});
|
|
127
|
+
else if(cutoff&&!rows.some(row=>row.type==='git_action_phase'&&row.action===action&&row.attempt===attempt&&row.phase===cutoff))await c.append({type:'git_action_rejected',...key,code:error.code||'GIT_PREFLIGHT_FAILED',error:error.message,scope:'no_source_or_ref_dispatch',possible_auxiliary_effects:'private_metadata_and_content_addressed_objects',retry_authorized:false});
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async treeEntries(plumbing,commit,signal){
|
|
132
|
+
const raw=(await plumbing.read(['ls-tree','-r','-z','--full-tree',commit],{signal})).stdout.toString('utf8'),entries=raw.split('\0').filter(Boolean).map(record=>{
|
|
133
|
+
const match=/^(100644|100755|120000) blob ([a-f0-9]{40}|[a-f0-9]{64})\t([\s\S]+)$/.exec(record);if(!match)throw gitFailure('Unsupported Git tree resource.','GIT_SOURCE_UNSUPPORTED');return{mode:match[1],oid:match[2],path:sourcePath(match[3])};
|
|
134
|
+
});
|
|
135
|
+
if(entries.length>2000)throw gitFailure('Git tree exceeds its path bound.','GIT_SOURCE_BOUND');return entries;
|
|
136
|
+
}
|
|
137
|
+
async snapshot(c,plumbing,identity,stamp,signal,message,parentCommit=identity.head){
|
|
138
|
+
const files=[],budget={bytes:64*1024*1024};
|
|
139
|
+
for(const file of identity.files)if(file.kind!=='absent')files.push(await readCaptured(identity.repository.root,file,budget,signal));
|
|
140
|
+
if((await inspectGitSource({cwd:identity.repository.root,signal})).source_identity!==identity.source_identity)throw gitFailure('Source changed during capture.','GIT_SOURCE_CHANGED');
|
|
141
|
+
for(const file of files){const oid=line(await plumbing.write(['hash-object','--no-filters','-w','--stdin'],{input:file.bytes,signal}));if(oid!==file.oid)throw gitFailure('Stored raw blob differs from the inspected source.');}
|
|
142
|
+
const index=path.join(c.directory,'index-'+randomUUID()),env={GIT_INDEX_FILE:index};let tree;
|
|
143
|
+
try{const input=Buffer.from(files.map(file=>`${file.mode} ${file.oid}\t${file.path}\0`).join(''));
|
|
144
|
+
await plumbing.write(['update-index','-z','--index-info'],{input,env,signal});tree=line(await plumbing.write(['write-tree'],{env,signal}));
|
|
145
|
+
}finally{await rm(index,{force:true});}
|
|
146
|
+
const commit=await this.commitObject(c,plumbing,stamp,tree,parentCommit?[parentCommit]:[],`${message}\n\nSource identity: ${identity.source_identity}`,signal);
|
|
147
|
+
return{commit,tree,source_identity:identity.source_identity,parent:parentCommit,entries:files.map(({bytes,...file})=>file)};
|
|
148
|
+
}
|
|
149
|
+
async commitObject(c,plumbing,stamp,tree,parents,message,signal){
|
|
150
|
+
const date=new Date(stamp.timestamp).toISOString(),env={GIT_AUTHOR_NAME:'Comal',GIT_AUTHOR_EMAIL:'comal@localhost',GIT_COMMITTER_NAME:'Comal',GIT_COMMITTER_EMAIL:'comal@localhost',GIT_AUTHOR_DATE:date,GIT_COMMITTER_DATE:date};
|
|
151
|
+
return line(await plumbing.write(['commit-tree',tree,...parents.flatMap(parent=>['-p',parent])],{env,input:Buffer.from(`${message}\n\nComal resource: ${c.id}\nPreparation: ${stamp.audit.sha256}\n`),signal}));
|
|
152
|
+
}
|
|
153
|
+
async rootRef(ref,signal){const result=await readGit(this.cwd,['rev-parse','--verify','--quiet',ref],{signal,allowed:[1]});return result.code===1?null:line(result);}
|
|
154
|
+
async validateIntegrationTarget(target,signal){
|
|
155
|
+
if(!/^[a-z0-9][a-z0-9_.-]{0,79}$/.test(target||''))throw gitFailure('Use a bounded logical integration target name.','GIT_INPUT');
|
|
156
|
+
const ref='refs/comal/integrations/'+target,result=await readGit(this.cwd,['check-ref-format',ref],{signal,allowed:[1]});
|
|
157
|
+
if(result.code!==0)throw gitFailure('The logical target is not a valid Git ref name. Choose another target before preparing source.','GIT_INPUT');return ref;
|
|
158
|
+
}
|
|
159
|
+
async cas(mutate,ref,commit,expected,format,signal){
|
|
160
|
+
const stale=(actual,dispatched)=>{const error=gitFailure('Git ref changed; prepare new work against its observed version. '+JSON.stringify({ref,expected:expected||null,observed:actual}),'GIT_REF_STALE');error.git_ref_precondition={ref,expected:expected||null,observed:actual,desired:commit,dispatched};return error;};
|
|
161
|
+
const actual=await this.rootRef(ref,signal);if(actual===commit)return;if(actual!==(expected||null))throw stale(actual,false);
|
|
162
|
+
try{await mutate(['update-ref','--create-reflog','-m','Comal governed source',ref,commit,expected||'0'.repeat(format==='sha256'?64:40)],{signal});}
|
|
163
|
+
catch(error){if(error.git_exit){const observed=await this.rootRef(ref,signal);if(observed!==commit&&observed!==(expected||null))throw stale(observed,true);}throw error;}
|
|
164
|
+
}
|
|
165
|
+
async worktreeList(signal){
|
|
166
|
+
const records=(await readGit(this.cwd,['worktree','list','--porcelain','-z'],{signal})).stdout.toString('utf8').split('\0'),items=[];let item;
|
|
167
|
+
for(const record of records){if(record.startsWith('worktree ')){if(item)items.push(item);item={path:record.slice(9)};}else if(record.startsWith('HEAD '))item.head=record.slice(5);else if(record.startsWith('branch '))item.branch=record.slice(7);}
|
|
168
|
+
if(item)items.push(item);return items;
|
|
169
|
+
}
|
|
170
|
+
async materialize(plumbing,worktree,entries,signal){
|
|
171
|
+
const budget={bytes:64*1024*1024};
|
|
172
|
+
for(const file of entries){
|
|
173
|
+
signal?.throwIfAborted();const length=Number(line(await plumbing.read(['cat-file','-s',file.oid],{signal})));if(!Number.isSafeInteger(length)||length<0||length>budget.bytes)throw gitFailure('Materialization exceeds its byte bound.','GIT_SOURCE_BOUND');budget.bytes-=length;
|
|
174
|
+
const bytes=(await plumbing.read(['cat-file','blob',file.oid],{signal,maxOutput:Math.max(1,length+1)})).stdout;if(bytes.length!==length)throw gitFailure('Git blob length changed.');
|
|
175
|
+
const target=path.join(worktree,sourcePath(file.path));await this.beforeEffect({kind:'source_directory',worktree});await mkdir(path.dirname(target),{recursive:true});const parent=await realpath(path.dirname(target)),relative=path.relative(worktree,parent);if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative))throw gitFailure('Materialization parent escaped the owned worktree.');
|
|
176
|
+
const existing=await lstat(target).catch(error=>{if(error.code==='ENOENT')return null;throw error;});
|
|
177
|
+
if(existing){const current=existing.isSymbolicLink()?await readlink(target,{encoding:'buffer'}):existing.isFile()?await readFile(target):null;
|
|
178
|
+
if(!current||!current.equals(bytes)||existing.isSymbolicLink()!==(file.mode==='120000')||existing.isFile()&&Boolean(existing.mode&0o111)!==(file.mode==='100755'))throw gitFailure('An existing owned-worktree path has different bytes/mode; it was preserved.','GIT_WORKSPACE_DIVERGED');continue;
|
|
179
|
+
}
|
|
180
|
+
if(file.mode==='120000'){await this.beforeEffect({kind:'source_publish',worktree});await symlink(bytes.toString('utf8'),target);}
|
|
181
|
+
else{const temp=path.join(this.directory,'materialize-'+randomUUID()),handle=await open(temp,'wx',file.mode==='100755'?0o755:0o644);
|
|
182
|
+
try{await handle.writeFile(bytes);await handle.sync();}finally{await handle.close();}
|
|
183
|
+
try{await this.beforeEffect({kind:'source_publish',worktree});await link(temp,target);}finally{await unlink(temp).catch(()=>{});}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async ensureWorktree(c,desired,plumbing,mutate,signal){
|
|
188
|
+
const worktree=path.join(this.worktrees,'work-'+c.id),branch='refs/heads/comal/work/'+c.id,items=await this.worktreeList(signal),known=items.find(item=>path.resolve(item.path)===worktree);
|
|
189
|
+
if(known){if(known.branch!==branch||known.head!==desired.commit)throw gitFailure('Registered worktree changed identity.','GIT_WORKSPACE_DIVERGED');}
|
|
190
|
+
else{
|
|
191
|
+
if(await lstat(worktree).catch(error=>{if(error.code==='ENOENT')return null;throw error;}))throw gitFailure('Worktree path exists without the expected Git registration.');
|
|
192
|
+
const ref=await this.rootRef(branch,signal);if(ref&&ref!==desired.commit)throw gitFailure('Owned branch points to different source.','GIT_REF_STALE');
|
|
193
|
+
await mutate(['worktree','add','--no-checkout','--lock','--reason','Comal '+c.id,...ref?[]:['-b',branch.slice(11)],worktree,ref?branch:desired.commit],{signal});
|
|
194
|
+
}
|
|
195
|
+
if(await realpath(worktree)!==worktree)throw gitFailure('Owned worktree path is noncanonical.');
|
|
196
|
+
const entries=desired.entries||await this.treeEntries(plumbing,desired.commit,signal);await this.materialize(plumbing,worktree,entries,signal);
|
|
197
|
+
const current=(await readGit(worktree,['ls-files','--stage','-z'],{signal})).stdout.toString('utf8'),expected=[...entries].sort((a,b)=>Buffer.compare(Buffer.from(a.path),Buffer.from(b.path))).map(file=>`${file.mode} ${file.oid} 0\t${file.path}\0`).join('');
|
|
198
|
+
if(current&¤t!==expected)throw gitFailure('Owned worktree index has unrelated staged changes; it was preserved.','GIT_WORKSPACE_DIVERGED');
|
|
199
|
+
if(!current&&entries.length)await mutate(['read-tree',desired.commit],{signal,env:{GIT_INDEX_FILE:line(await readGit(worktree,['rev-parse','--path-format=absolute','--git-path','index'],{signal}))}});
|
|
200
|
+
const observed=await inspectGitSource({cwd:worktree,signal});if(observed.head!==desired.commit||observed.branch!==branch||observed.dirty)throw gitFailure('Owned worktree source does not match its prepared commit.','GIT_WORKSPACE_DIVERGED');
|
|
201
|
+
return publicWork({workspace_id:c.id,path:worktree,branch,head:desired.commit,tree:desired.tree,source_identity:observed.source_identity,original_source_identity:desired.source_identity,base_commit:desired.parent});
|
|
202
|
+
}
|
|
203
|
+
async open({request_id,expected_source_identity,source}, {owner,signal}={}){
|
|
204
|
+
signal?.throwIfAborted();
|
|
205
|
+
ownerDigest(owner);if(typeof request_id!=='string'||!request_id||request_id.length>120||!['working','head'].includes(source)||!/^[a-f0-9]{64}$/.test(expected_source_identity||''))throw gitFailure('Open requires request_id, expected_source_identity and explicit working/head source mode.','GIT_INPUT');
|
|
206
|
+
return this.#allocateSource({request_id,expected_source_identity,source}, {owner,signal},async({c,plumbing,stamp})=>{
|
|
207
|
+
const identity=await inspectGitSource({cwd:this.cwd,signal});if(identity.source_identity!==expected_source_identity)throw gitFailure('Original source identity changed; no worktree source was selected.','GIT_SOURCE_CHANGED');
|
|
208
|
+
if(source==='head'){if(!identity.head)throw gitFailure('HEAD mode requires a committed base.','GIT_SOURCE_UNSUPPORTED');return{commit:identity.head,tree:line(await plumbing.read(['rev-parse',identity.head+'^{tree}'],{signal})),source_identity:identity.source_identity,parent:identity.head};}
|
|
209
|
+
return this.snapshot(c,plumbing,identity,stamp,signal,'Comal working-source snapshot');
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async #allocateSource(input,{owner,signal},resolve){
|
|
213
|
+
const{request_id}=input;await this.initialize();const id=resourceId(this.cwd,owner,request_id),c=await this.context(id,true),lock=await this.lock(c);
|
|
214
|
+
try{
|
|
215
|
+
let repository;try{repository=(await this.load(c,owner)).root.spec.repository;}catch(error){if(!['ENOENT','GIT_RESOURCE_UNINITIALIZED'].includes(error.code))throw error;repository=(await inspectGitSource({cwd:this.cwd,signal})).repository;}
|
|
216
|
+
const spec={...input,repository};await this.create(c,owner,spec);
|
|
217
|
+
return await this.action(c,owner,'open',request_id,spec,async({stamp,phase,mutate,plumbing})=>{
|
|
218
|
+
let desired=await phase('snapshot');
|
|
219
|
+
if(!desired){desired=await resolve({c,plumbing,stamp});await phase('snapshot',desired);}
|
|
220
|
+
await this.cas(mutate,'refs/comal/snapshots/'+id,desired.commit,null,repository.object_format,signal);
|
|
221
|
+
await this.fault('snapshot_ref',{id,desired});const result=await this.ensureWorktree(c,desired,plumbing,mutate,signal);await phase('worktree_observed',result);return result;
|
|
222
|
+
},signal);
|
|
223
|
+
}finally{await lock.release();}
|
|
224
|
+
}
|
|
225
|
+
// The caller is a trusted native adapter. Source handles are verified data,
|
|
226
|
+
// never authority: the adapter must bind both owners/current effect frames.
|
|
227
|
+
async #revisionContext(id,owner,source,signal){
|
|
228
|
+
await this.initialize();const c=await this.context(id),{book,root}=await this.load(c,owner);
|
|
229
|
+
if(root.spec.kind!=='integration')throw gitFailure('A revision requires an owned native integration.','GIT_REVISION_SOURCE_INVALID');
|
|
230
|
+
if(book.rows.some(r=>r.type==='git_resource_retired'))throw gitFailure('The original source is retired.','GIT_RESOURCE_RETIRED');
|
|
231
|
+
if(book.rows.some(r=>r.type==='git_action_prepared'&&!book.rows.some(end=>['git_action_returned','git_action_rejected'].includes(end.type)&&end.action===r.action&&end.attempt===r.attempt)))throw gitFailure('The original integration has an unsettled action. No correction source was selected.','GIT_REVISION_UNCERTAIN');
|
|
232
|
+
if(book.rows.some(r=>r.type==='git_action_returned'&&r.action==='integration_apply'))throw gitFailure('An applied integration is not an open correction.','GIT_REVISION_NOT_REQUIRED');
|
|
233
|
+
let prefix=book.rows;
|
|
234
|
+
if(source){const end=prefix.findIndex(r=>r.audit.sha256===source.failure_ref);if(end<0)throw gitFailure('The frozen revision failure no longer verifies.','GIT_REVISION_SOURCE_INVALID');prefix=prefix.slice(0,end+1);}
|
|
235
|
+
const prepared=prefix.find(r=>r.type==='git_action_returned'&&r.action==='integration_prepare'),failed=prefix.findLast(r=>['git_action_returned','git_action_rejected'].includes(r.type)&&r.action===(prepared?'integration_verify':'integration_prepare'));
|
|
236
|
+
if(!failed||failed.type!=='git_action_rejected'||prepared&&(!['native_process_exit_observed','native_output_assertion_failed',...(isPageVerification(root.spec.verification)?['native_page_needs_correction']:[])].includes(failed.evidence?.scope))||!prepared&&failed.code!=='GIT_MERGE_CONFLICT')throw gitFailure('Correction requires a known source/check failure; denial, missing evidence and uncertainty are not correction work.','GIT_REVISION_NOT_REQUIRED');
|
|
237
|
+
if(prepared&&failed.evidence?.scope==='native_page_needs_correction'){
|
|
238
|
+
const session=prefix.find(r=>r.type==='git_action_phase'&&r.action==='integration_verify'&&r.attempt===failed.attempt&&r.phase==='verification_session')?.value,reservation=prefix.find(r=>r.type==='git_action_phase'&&r.action==='integration_verify'&&r.attempt===failed.attempt&&r.phase==='page_reservation')?.value;
|
|
239
|
+
if(!session||!reservation)throw gitFailure('Original failed page reservation is unavailable.','GIT_REVISION_SOURCE_INVALID');
|
|
240
|
+
const native=await this.nativeVerificationSession(session,prepared.output.worktree);assertNativePrefix(native,failed.evidence.prefix);
|
|
241
|
+
const proof=assertPageChain(native,{reservation,task:{...root.spec.page_recipes[0].task,project:'git-'+id},sourceDigest:reservation.source_digest,callId:reservation.test_call_id,knownFailure:true});
|
|
242
|
+
if(proof.invocation_id!==failed.evidence.invocation_id||proof.receipt_digest!==failed.evidence.receipt_digest)throw gitFailure('Original semantic failure no longer matches its native owners.','GIT_REVISION_SOURCE_INVALID');
|
|
243
|
+
}
|
|
244
|
+
const conflict=!prepared&&prefix.findLast(r=>r.type==='git_action_phase'&&r.action==='integration_prepare'&&r.attempt===failed.attempt&&r.phase.startsWith('conflict:')),record=prepared||conflict;
|
|
245
|
+
const tree=prepared?.output.tree||conflict?.value.tree;
|
|
246
|
+
if(!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(tree||''))throw gitFailure('This historical conflict has no native tree identity. No source was guessed from prose.','GIT_REVISION_SOURCE_INVALID');
|
|
247
|
+
const descriptor=immutable({schema:'comal-source-revision-v1',integration_id:id,owner_digest:root.owner_digest,spec_digest:root.spec_digest,source_ref:record.audit.sha256,failure_ref:failed.audit.sha256,kind:prepared?'failed_candidate':'merge_conflict',tree,parents:prepared?[prepared.output.candidate_commit]:[conflict.value.base,conflict.value.source],target:root.spec.target,expected_target:root.spec.expected_target,base_commit:root.spec.base_commit,verification_digest:root.spec.verification_digest,remaining_offers:prepared?[]:root.spec.offers.slice(Number(conflict.phase.split(':')[1])+1),scope:'frozen_native_source_not_verified_delivery_or_authority'});
|
|
248
|
+
if(source&&digest(source)!==digest(descriptor))throw gitFailure('The revision source differs from its original native record.','GIT_REVISION_SOURCE_INVALID');return{source:descriptor,spec:root.spec,book};
|
|
249
|
+
}
|
|
250
|
+
async revisionSource(id,{owner,signal}={}){return(await this.#revisionContext(id,immutable(structuredClone(owner)),null,signal)).source;}
|
|
251
|
+
async validateRevisionSource(source,{owner,signal}={}){source=immutable(structuredClone(source));return(await this.#revisionContext(source?.integration_id,immutable(structuredClone(owner)),source,signal)).source;}
|
|
252
|
+
async openRevision({request_id,source},{owner,source_owner,signal}={}){
|
|
253
|
+
signal?.throwIfAborted();source=immutable(structuredClone(source));owner=immutable(structuredClone(owner));source_owner=immutable(structuredClone(source_owner));ownerDigest(owner);ownerDigest(source_owner);
|
|
254
|
+
if(typeof request_id!=='string'||!request_id||request_id.length>120||!source||owner.actor.kind!=='subagent'||owner.actor.id!==owner.session_id||owner.actor.parent_session_id!==source_owner.session_id)throw gitFailure('A revision requires a fresh editor owned by the original integration parent.','GIT_REVISION_OWNER');
|
|
255
|
+
await this.#revisionContext(source.integration_id,source_owner,source,signal);const input={request_id,source:'revision',expected_source_identity:digest(source),revision:source};
|
|
256
|
+
return this.#allocateSource(input,{owner,signal},async({c,plumbing,stamp})=>{
|
|
257
|
+
if(await this.rootRef('refs/comal/integrations/'+source.target,signal)!==(source.expected_target==='absent'?null:source.expected_target))throw gitFailure('The original revision target changed; its expected version was not refreshed.','GIT_REF_STALE');
|
|
258
|
+
await this.#revisionContext(source.integration_id,source_owner,source,signal);
|
|
259
|
+
await this.treeEntries(plumbing,source.tree,signal);for(const parent of source.parents)await plumbing.read(['cat-file','-e',parent+'^{commit}'],{signal});
|
|
260
|
+
if(source.kind==='failed_candidate'&&line(await plumbing.read(['rev-parse',source.parents[0]+'^{tree}'],{signal}))!==source.tree)throw gitFailure('Candidate tree differs from its original commit.','GIT_REVISION_SOURCE_INVALID');
|
|
261
|
+
const commit=source.kind==='failed_candidate'?source.parents[0]:await this.commitObject(c,plumbing,stamp,source.tree,source.parents,'Comal unresolved native source revision',signal);
|
|
262
|
+
return{commit,tree:source.tree,source_identity:digest(source),parent:commit,revision_digest:digest(source)};
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async prepareRevision(input,{owner,signal,pageRecipes}={}){
|
|
266
|
+
input=immutable(structuredClone(input));owner=immutable(structuredClone(owner));
|
|
267
|
+
if(!input||Object.keys(input).some(k=>!['request_id','source','offer'].includes(k)))throw gitFailure('A revision cannot replace the original target, base or verification contract.','GIT_INPUT');
|
|
268
|
+
const{request_id,source,offer}=input;
|
|
269
|
+
if(!source||!offer)throw gitFailure('A revision needs its frozen source and native offer.','GIT_REVISION_SOURCE_INVALID');
|
|
270
|
+
const original=await this.#revisionContext(source.integration_id,owner,source,signal),published=await this.offered(offer,signal),c=await this.context(published.workspace_id),rows=(await c.store.load(c.id)).rows,created=rows.find(r=>r.type==='git_resource_created');
|
|
271
|
+
if(!created)throw gitFailure('The revision offer has no original resource.','GIT_REVISION_SOURCE_INVALID');
|
|
272
|
+
const resource=await this.load(c,created.owner),editor=resource.root.owner;
|
|
273
|
+
if(editor.actor.kind!=='subagent'||editor.actor.parent_session_id!==owner.session_id||editor.actor.id!==editor.session_id||resource.root.spec.source!=='revision'||digest(resource.root.spec.revision)!==digest(source))throw gitFailure('The offered checkpoint belongs to another correction/source owner.','GIT_REVISION_OWNER');
|
|
274
|
+
const{target,expected_target,base_commit,verification}=original.spec;
|
|
275
|
+
return this.#prepareIntegration({request_id,target,expected_target,base_commit,offers:[{workspace_id:published.workspace_id,offer_id:published.offer_id},...source.remaining_offers],verification},{owner,signal,pageRecipes,revision:{source,offer_id:published.offer_id,workspace_id:published.workspace_id}});
|
|
276
|
+
}
|
|
277
|
+
async status(id,{owner}={}){
|
|
278
|
+
await this.initialize();const c=await this.context(id),{book,root}=await this.load(c,owner),completed=book.rows.filter(row=>row.type==='git_action_returned');
|
|
279
|
+
return{workspace_id:id,lifecycle:book.rows.some(row=>row.type==='git_resource_retired')?'retired_preserved':'retained',owner:root.owner,spec:root.spec,completed:completed.map(row=>({action:row.action,attempt:row.attempt,output:row.output})),pending:book.rows.filter(row=>row.type==='git_action_prepared'&&!book.rows.some(end=>['git_action_returned','git_action_rejected'].includes(end.type)&&end.action===row.action&&end.attempt===row.attempt)).map(row=>({action:row.action,attempt:row.attempt,input_digest:row.input_digest})),rejected:book.rows.filter(row=>row.type==='git_action_rejected').map(({action,attempt,code,scope})=>({action,attempt,code,scope})),journal:c.file,journal_tip:book.journalAudit.tip_sha256};
|
|
280
|
+
}
|
|
281
|
+
async checkpoint(id,{attempt_id,expected_source_identity,message='Comal source checkpoint'},{owner,signal}={}){
|
|
282
|
+
signal?.throwIfAborted();ownerDigest(owner);if(typeof attempt_id!=='string'||!attempt_id||attempt_id.length>120||!/^[a-f0-9]{64}$/.test(expected_source_identity||'')||typeof message!=='string'||message.length>1000)throw gitFailure('Checkpoint requires a new attempt_id and inspected source identity.','GIT_INPUT');
|
|
283
|
+
await this.initialize();const c=await this.context(id),lock=await this.lock(c);
|
|
284
|
+
try{return await this.action(c,owner,'checkpoint',attempt_id,{expected_source_identity,message},async({book,root,stamp,phase,plumbing,mutate})=>{
|
|
285
|
+
const opened=book.rows.find(row=>row.type==='git_action_returned'&&row.action==='open')?.output;if(!opened)throw gitFailure('Worktree allocation has not settled.');
|
|
286
|
+
const previous=book.rows.findLast(row=>row.type==='git_action_returned'&&row.action==='checkpoint')?.output,ref='refs/comal/checkpoints/'+id,expectedRef=previous?.commit||null;
|
|
287
|
+
let desired=await phase('checkpoint');
|
|
288
|
+
if(!desired){
|
|
289
|
+
const identity=await inspectGitSource({cwd:opened.path,signal});if(identity.head!==opened.head||identity.branch!==opened.branch||identity.repository.common_directory!==root.spec.repository.common_directory)throw gitFailure('Agent worktree identity changed; its index/source was preserved.','GIT_WORKSPACE_DIVERGED');
|
|
290
|
+
if(identity.source_identity!==expected_source_identity)throw gitFailure('Agent source changed since inspection.','GIT_SOURCE_CHANGED');
|
|
291
|
+
desired=await this.snapshot(c,plumbing,identity,stamp,signal,message,expectedRef||opened.head);await phase('checkpoint',desired);
|
|
292
|
+
}
|
|
293
|
+
// Checkpoints have a dedicated non-checkout ref. Neither the user's nor the
|
|
294
|
+
// editing agent's index/HEAD/working bytes are rewritten by a checkpoint.
|
|
295
|
+
if((await this.worktreeList(signal)).some(item=>item.branch===ref))throw gitFailure('A checkpoint ref is unexpectedly attached to a checkout.','GIT_REF_CHECKED_OUT');
|
|
296
|
+
await this.cas(mutate,ref,desired.commit,expectedRef,root.spec.repository.object_format,signal);await this.fault('checkpoint_ref',{id,desired});
|
|
297
|
+
return{workspace_id:id,ref,commit:desired.commit,tree:desired.tree,parent:desired.parent,source_identity:desired.source_identity,worktree_head:opened.head,worktree_index:'preserved',scope:'recorded_source_not_verification',asked:'unjudged'};
|
|
298
|
+
},signal);}finally{await lock.release();}
|
|
299
|
+
}
|
|
300
|
+
async offer(id,{attempt_id,expected_commit},{owner,signal}={}){
|
|
301
|
+
ownerDigest(owner);if(!attempt_id||typeof attempt_id!=='string'||attempt_id.length>120||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(expected_commit||''))throw gitFailure('Offer requires attempt_id and an exact recorded commit.','GIT_INPUT');
|
|
302
|
+
await this.initialize();const c=await this.context(id),lock=await this.lock(c);
|
|
303
|
+
try{return await this.action(c,owner,'offer',attempt_id,{expected_commit},async({book,root,mutate})=>{
|
|
304
|
+
const source=book.rows.find(row=>row.type==='git_action_returned'&&(['checkpoint'].includes(row.action)&&row.output.commit===expected_commit||row.action==='open'&&row.output.head===expected_commit));if(!source)throw gitFailure('Only this resource\'s recorded checkpoint/base can be offered.','GIT_OFFER_INVALID');
|
|
305
|
+
const ref='refs/comal/offers/'+id+'/'+expected_commit,metadata={workspace_id:id,commit:expected_commit,source_event:source.audit.sha256,owner_digest:root.owner_digest,repository:root.spec.repository},offer_id=digest(metadata);
|
|
306
|
+
await this.cas(mutate,ref,expected_commit,null,root.spec.repository.object_format,signal);
|
|
307
|
+
return{workspace_id:id,offer_id,ref,commit:expected_commit,metadata,scope:'source_offer_not_verification_or_edit_authority'};
|
|
308
|
+
},signal);}finally{await lock.release();}
|
|
309
|
+
}
|
|
310
|
+
async offered(reference,signal){
|
|
311
|
+
if(!reference?.workspace_id||!/^[a-f0-9]{64}$/.test(reference.offer_id||''))throw gitFailure('Invalid source offer.','GIT_OFFER_INVALID');
|
|
312
|
+
const c=await this.context(reference.workspace_id),rows=[],handle=await open(c.file,constants.O_RDONLY|(constants.O_NOFOLLOW||0));let carry=Buffer.alloc(0),total=0,found=false;
|
|
313
|
+
// The caller holds a published offer handle. Verify only its exact historical
|
|
314
|
+
// prefix, so a later/incomplete append cannot invalidate immutable offered
|
|
315
|
+
// source or make this reader adopt a newer checkpoint implicitly.
|
|
316
|
+
try{for(;;){signal?.throwIfAborted();if(total>=64*1024*1024)throw gitFailure('Offer prefix exceeds its inspection bound.','GIT_SOURCE_BOUND');const chunk=Buffer.alloc(Math.min(128*1024,64*1024*1024-total)),read=await handle.read(chunk,0,chunk.length,null);if(!read.bytesRead)break;total+=read.bytesRead;carry=Buffer.concat([carry,chunk.subarray(0,read.bytesRead)]);
|
|
317
|
+
let newline;while((newline=carry.indexOf(10))>=0){const row=JSON.parse(carry.subarray(0,newline).toString('utf8'));rows.push(row);carry=carry.subarray(newline+1);if(row.type==='git_action_returned'&&row.action==='offer'&&row.output?.offer_id===reference.offer_id){found=true;break;}}if(found)break;
|
|
318
|
+
}}finally{await handle.close();}
|
|
319
|
+
if(!found)throw gitFailure('The published offer is not in the verified prefix.','GIT_OFFER_INVALID');
|
|
320
|
+
const book={id:rows[0]?.id,cwd:rows[0]?.cwd,rows,journalAudit:auditJournal(rows)},created=rows.find(row=>row.type==='git_resource_created');if(!created)throw gitFailure('Source offer has no owner.','GIT_OFFER_INVALID');await this.load(c,created.owner,book);
|
|
321
|
+
const receipt=book.rows.find(row=>row.type==='git_action_returned'&&row.action==='offer'&&row.output.offer_id===reference.offer_id),offer=receipt?.output;
|
|
322
|
+
if(!offer||receipt.output_digest!==digest(offer)||offer.offer_id!==digest(offer.metadata)||offer.metadata.owner_digest!==created.owner_digest||offer.metadata.workspace_id!==c.id||offer.metadata.commit!==offer.commit||digest(offer.metadata.repository)!==digest(created.spec.repository)||offer.ref!=='refs/comal/offers/'+c.id+'/'+offer.commit||!book.rows.some(row=>row.audit.sha256===offer.metadata.source_event&&row.type==='git_action_returned'&&(row.output.commit||row.output.head)===offer.commit)||await this.rootRef(offer.ref,signal)!==offer.commit)throw gitFailure('The exact published source offer no longer verifies.','GIT_OFFER_INVALID');return offer;
|
|
323
|
+
}
|
|
324
|
+
verificationContract(contract){
|
|
325
|
+
if(!checkContract(contract)&&!checkPageContract(contract))throw gitFailure('Integration requires a bounded declared verification contract.','GIT_VERIFICATION_CONTRACT');
|
|
326
|
+
const ids=new Set();for(const check of contract.checks){if(ids.has(check.id))throw gitFailure('Duplicate integration verification check.','GIT_VERIFICATION_CONTRACT');ids.add(check.id);validateAssertions(check.assertions||[]);}
|
|
327
|
+
return immutable(structuredClone(contract));
|
|
328
|
+
}
|
|
329
|
+
async nativeVerificationSession(descriptor,cwd){
|
|
330
|
+
const file=descriptor?.journal;if(typeof file!=='string'||!descriptor.session_id||!descriptor.binding||typeof descriptor.binding!=='object')throw gitFailure('Native verifier session is unbound.','GIT_NATIVE_VERIFICATION_REQUIRED');
|
|
331
|
+
const canonical=await realpath(file),relative=path.relative(cwd,canonical);if(relative.startsWith('..')||path.isAbsolute(relative)||!['.comal','.harness'].includes(relative.split(path.sep)[0]))throw gitFailure('Verifier journal is outside its owned worktree.','GIT_NATIVE_VERIFICATION_REQUIRED');
|
|
332
|
+
const info=await lstat(file);if(!info.isFile()||info.isSymbolicLink()||info.size>32*1024*1024)throw gitFailure('Native verifier journal is not a bounded regular file.');
|
|
333
|
+
const bytes=await readFile(file),rows=bytes.toString('utf8').trim().split('\n').map(JSON.parse);
|
|
334
|
+
if(rows[0]?.type!=='session'||rows[0].id!==descriptor.session_id||rows[0].cwd!==cwd||digest(rows[0].git_verification??null)!==digest(descriptor.binding))throw gitFailure('Native verifier identity or resource binding differs.','GIT_NATIVE_VERIFICATION_REQUIRED');return{id:descriptor.session_id,cwd,rows,bytes};
|
|
335
|
+
}
|
|
336
|
+
async ensureVerificationSession(store,descriptor){
|
|
337
|
+
await privateGitDirectory(path.join(store.cwd,'.comal'));await mkdir(store.dir,{recursive:true,mode:0o700});
|
|
338
|
+
if(store.file(descriptor.session_id)!==descriptor.journal)throw gitFailure('Native verifier session path changed.','GIT_NATIVE_VERIFICATION_REQUIRED');
|
|
339
|
+
const exists=await lstat(descriptor.journal).catch(error=>{if(error.code==='ENOENT')return null;throw error;});
|
|
340
|
+
if(!exists){const temp=path.join(store.dir,'.init-'+randomUUID()),handle=await open(temp,'wx',0o600);
|
|
341
|
+
try{await handle.writeFile(JSON.stringify(sealRecord({type:'session',id:descriptor.session_id,cwd:store.cwd,model:'house-git-verifier',provider:'host',created_at:new Date().toISOString(),git_verification:descriptor.binding},0))+'\n');await handle.sync();}finally{await handle.close();}
|
|
342
|
+
try{await link(temp,descriptor.journal);}finally{await unlink(temp).catch(()=>{});}
|
|
343
|
+
}
|
|
344
|
+
return this.nativeVerificationSession(descriptor,store.cwd);
|
|
345
|
+
}
|
|
346
|
+
nativeCheckProof(native,owner,check,args,callId){
|
|
347
|
+
const audit=auditExecution(native),matches=audit.receipts.filter(row=>row.call_id===callId),receipt=matches[0],contract=native.rows.find(row=>row.type==='house_contract'&&row.contract_digest===receipt?.contract_digest)?.contract;
|
|
348
|
+
if(!audit.journal.verified||audit.violations.length||audit.unverified.length||matches.length!==1||receipt.operation!=='Bash'||receipt.arguments_digest!==digest(args)||digest(receipt.actor)!==digest(owner.actor)||contract?.origin!==null||contract.permission!=='Bash'||contract.name!=='Bash'||contract.steps||contract.effects.process!=='unknown'||contract.effects.authority!=='none')throw gitFailure('A tool result or boolean is not a verified native check receipt.','GIT_NATIVE_VERIFICATION_REQUIRED');
|
|
349
|
+
const exits=receipt.observations?.filter(fact=>fact.kind==='process_exit')||[],knownExit=exits.length===1&&!exits[0].interrupted&&!exits[0].termination_signal&&Number.isInteger(exits[0].exit_code);
|
|
350
|
+
if(receipt.status!=='returned'){
|
|
351
|
+
const error=gitFailure(receipt.error||'Native verification failed.','GIT_NATIVE_CHECK_FAILED');
|
|
352
|
+
if(knownExit||receipt.status==='refused'&&receipt.effects_observed==='none')error.git_verification_failure={check_id:check.id,session_id:native.id,invocation_id:receipt.invocation_id,row:receipt.evidence_row,receipt_digest:receipt.audit.sha256,scope:knownExit?'native_process_exit_observed':'native_check_refused'};throw error;
|
|
353
|
+
}
|
|
354
|
+
const processGood=knownExit&&exits[0].exit_code===0||readOnlyBashCheck(args.command)&&receipt.observations?.some(fact=>fact.kind==='file_read');
|
|
355
|
+
if(!processGood||receipt.output_digest!==digest(receipt.output))throw gitFailure('Native process completion has no verified observation.','GIT_NATIVE_VERIFICATION_REQUIRED');
|
|
356
|
+
try{assertOutput(receipt.output,check.assertions||[]);}catch(error){error.git_verification_failure={check_id:check.id,session_id:native.id,invocation_id:receipt.invocation_id,scope:'native_output_assertion_failed'};throw error;}
|
|
357
|
+
return receipt;
|
|
358
|
+
}
|
|
359
|
+
async prepareIntegration(input,{owner,signal,pageRecipes}={}){return this.#prepareIntegration(input,{owner,signal,pageRecipes});}
|
|
360
|
+
async #prepareIntegration(input,{owner,signal,revision,pageRecipes}={}){
|
|
361
|
+
ownerDigest(owner);const{request_id,target,expected_target,base_commit,offers}=input,verification=this.verificationContract(input.verification);
|
|
362
|
+
if(typeof request_id!=='string'||!request_id||request_id.length>120||!/^[a-z0-9][a-z0-9_.-]{0,79}$/.test(target||'')||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(base_commit||'')||expected_target!=='absent'&&!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(expected_target||'')||!Array.isArray(offers)||!offers.length||offers.length>8||expected_target!=='absent'&&base_commit!==expected_target)throw gitFailure('Integration needs an exact target/base and 1–8 source offers.','GIT_INPUT');
|
|
363
|
+
await this.validateIntegrationTarget(target,signal);await this.initialize();const id=resourceId(this.cwd,owner,{kind:'integration',request_id}),c=await this.context(id,true),lock=await this.lock(c);
|
|
364
|
+
try{
|
|
365
|
+
let repository;try{repository=(await this.load(c,owner)).root.spec.repository;}catch(error){if(!['ENOENT','GIT_RESOURCE_UNINITIALIZED'].includes(error.code))throw error;repository=(await inspectGitSource({cwd:this.cwd,signal})).repository;}
|
|
366
|
+
if(isPageVerification(verification)&&(!Array.isArray(pageRecipes)||pageRecipes.length!==1||pageRecipes[0].name!==verification.checks[0].recipe||!/^[a-f0-9]{64}$/.test(pageRecipes[0].digest||'')))throw gitFailure('A native page check requires its original host-bound recipe.','GIT_PAGE_RECIPE');
|
|
367
|
+
const spec={kind:'integration',request_id,target,expected_target,base_commit,offers,verification,verification_digest:digest(verification),repository,...(revision?{revision}: {}),...(isPageVerification(verification)?{page_recipes:immutable(structuredClone(pageRecipes))}: {})};await this.create(c,owner,spec);
|
|
368
|
+
return await this.action(c,owner,'integration_prepare',request_id,spec,async({stamp,phase,plumbing,mutate})=>{
|
|
369
|
+
const targetRef='refs/comal/integrations/'+target;
|
|
370
|
+
if(await this.rootRef(targetRef,signal)!==(expected_target==='absent'?null:expected_target))throw gitFailure('Integration target changed before preparation.','GIT_REF_STALE');
|
|
371
|
+
let desired=await phase('candidate');
|
|
372
|
+
if(!desired){
|
|
373
|
+
const published=[];for(const reference of offers)published.push(await this.offered(reference,signal));if(new Set(published.map(offer=>offer.offer_id)).size!==published.length)throw gitFailure('Duplicate source offer.','GIT_OFFER_INVALID');
|
|
374
|
+
await plumbing.read(['cat-file','-e',base_commit+'^{commit}'],{signal});let commit=base_commit,tree;
|
|
375
|
+
for(const[index,offer]of published.entries()){
|
|
376
|
+
const saved=await phase('merge:'+index);if(saved){commit=saved.commit;tree=saved.tree;continue;}
|
|
377
|
+
const merged=await plumbing.write(['merge-tree','--write-tree',commit,offer.commit],{signal,allowed:[1]}),text=merged.stdout.toString('utf8');
|
|
378
|
+
if(merged.code===1){await phase('conflict:'+index,{offer_id:offer.offer_id,base:commit,source:offer.commit,tree:text.split('\n')[0],output_sha256:sha(merged.stdout),details:text.slice(0,12000)});throw gitFailure('Combined source conflicts. Resolve it in an owned editing worktree and offer a new checkpoint; no target ref was updated.','GIT_MERGE_CONFLICT');}
|
|
379
|
+
tree=text.split('\n')[0];if(!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(tree))throw gitFailure('Merge did not return a tree identity.');
|
|
380
|
+
commit=await this.commitObject(c,plumbing,stamp,tree,[commit,offer.commit],`Comal combined candidate ${index+1}\nOffer: ${offer.offer_id}`,signal);await phase('merge:'+index,{commit,tree,offer_id:offer.offer_id});
|
|
381
|
+
}
|
|
382
|
+
desired={commit,tree,parent:base_commit,source_identity:digest({base_commit,offers:published.map(offer=>offer.offer_id)}),offers:published.map(({workspace_id,offer_id,commit})=>({workspace_id,offer_id,commit}))};await phase('candidate',desired);
|
|
383
|
+
}
|
|
384
|
+
await this.cas(mutate,'refs/comal/candidates/'+id,desired.commit,null,repository.object_format,signal);
|
|
385
|
+
const work=await this.ensureWorktree(c,desired,plumbing,mutate,signal);await phase('candidate_observed',work);
|
|
386
|
+
return{integration_id:id,target_ref:targetRef,expected_target,base_commit,candidate_commit:desired.commit,tree:desired.tree,worktree:work.path,source_identity:work.source_identity,offers:desired.offers,verification_digest:digest(verification),scope:'prepared_combined_source_not_verified_or_applied',asked:'unjudged'};
|
|
387
|
+
},signal);
|
|
388
|
+
}finally{await lock.release();}
|
|
389
|
+
}
|
|
390
|
+
async verifyIntegration(id,{attempt_id},{owner,signal}={}){
|
|
391
|
+
if(!attempt_id||typeof attempt_id!=='string'||attempt_id.length>120)throw gitFailure('Verification requires a unique attempt_id.','GIT_INPUT');
|
|
392
|
+
await this.initialize();const c=await this.context(id),lock=await this.lock(c);
|
|
393
|
+
try{
|
|
394
|
+
const initial=await this.load(c,owner),{book,root}=initial,prepared=book.rows.find(r=>r.type==='git_action_returned'&&r.action==='integration_prepare')?.output;
|
|
395
|
+
if(isPageVerification(root.spec.verification))for(const prior of book.rows.filter(r=>r.type==='git_action_phase'&&r.action==='integration_verify'&&r.phase==='verification_session'&&r.attempt!==attempt_id)){
|
|
396
|
+
const old=await this.nativeVerificationSession(prior.value,prepared.worktree);
|
|
397
|
+
if(old.rows.some(r=>r.type==='house_proposal'&&['ChromePublish','ChromeTest'].includes(r.operation))){
|
|
398
|
+
const ended=old.rows.findLast(r=>r.type==='house_receipt'&&r.operation==='ChromeTest'),known=ended?.status==='returned'&&JSON.parse(ended.output).status==='needs_correction';
|
|
399
|
+
throw gitFailure(known?'The original candidate has a known failed page case. Correct it through the original-contract revision route; a new attempt cannot replay its effects.':'The original page attempt already proposed an effect. Inspect/reconcile its original owners; a new attempt cannot replay it.',known?'GIT_PAGE_NEW_CANDIDATE_REQUIRED':'GIT_VERIFICATION_UNCERTAIN');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return await this.action(c,owner,'integration_verify',attempt_id,{},async({book,root,phase})=>{
|
|
403
|
+
const prepared=book.rows.find(row=>row.type==='git_action_returned'&&row.action==='integration_prepare')?.output;if(!prepared||root.spec.kind!=='integration')throw gitFailure('Combined candidate has not settled.');
|
|
404
|
+
const before=await inspectGitSource({cwd:prepared.worktree,signal});if(before.head!==prepared.candidate_commit||before.dirty||before.source_identity!==prepared.source_identity)throw gitFailure('Combined candidate changed before verification.','GIT_VERIFICATION_SOURCE_CHANGED');
|
|
405
|
+
let session=await phase('verification_session'),runner;const store=new SessionStore({cwd:prepared.worktree,stateDir:path.join(prepared.worktree,'.comal')});
|
|
406
|
+
const binding={resource_id:id,attempt_id,candidate_commit:prepared.candidate_commit,owner_digest:ownerDigest(owner),verification_digest:root.spec.verification_digest,...(isPageVerification(root.spec.verification)?{page_recipe_digest:root.spec.page_recipes[0].digest}: {})};
|
|
407
|
+
if(!session){
|
|
408
|
+
if(typeof this.verifierFactory!=='function')throw gitFailure('No host-bound native verification executor is configured.','GIT_VERIFIER_REQUIRED');
|
|
409
|
+
const session_id=randomUUID();session={cwd:prepared.worktree,session_id,journal:store.file(session_id),contract_digest:root.spec.verification_digest,binding};await phase('verification_session',session);
|
|
410
|
+
}
|
|
411
|
+
if(digest(session.binding)!==digest(binding))throw gitFailure('Native verifier birth binding changed.','GIT_NATIVE_VERIFICATION_REQUIRED');
|
|
412
|
+
await this.ensureVerificationSession(store,session);
|
|
413
|
+
if(isPageVerification(root.spec.verification)){
|
|
414
|
+
const check=root.spec.verification.checks[0],recipe=root.spec.page_recipes[0],context={cwd:prepared.worktree,store,owner:immutable(structuredClone(owner)),resource_id:id,attempt_id,session_id:session.session_id,signal,binding,kernel_identity:this.pageKernelIdentity()};
|
|
415
|
+
let reservation=await phase('page_reservation');if(!reservation)reservation=await reservePageCheck({context,prepared,recipe,check,phase});
|
|
416
|
+
if(reservation.kernel_identity!==context.kernel_identity)throw gitFailure('Original page kernel implementation changed.','GIT_PAGE_RECIPE_CHANGED');
|
|
417
|
+
let native=await this.nativeVerificationSession(session,prepared.worktree),reserved=native.rows.filter(r=>r.type==='git_page_check_reserved');
|
|
418
|
+
if(!reserved.length){if(native.rows.some(r=>r.type==='house_proposal'))throw gitFailure('A page reservation cannot be added after native effects.','GIT_PAGE_EVIDENCE');await store.append(session.session_id,{type:'git_page_check_reserved',source:'git_page_host',reservation,reservation_digest:digest(reservation)});}
|
|
419
|
+
else if(reserved.length!==1||digest(reserved[0].reservation)!==digest(reservation))throw gitFailure('The original page reservation changed.','GIT_PAGE_EVIDENCE');
|
|
420
|
+
native=await this.nativeVerificationSession(session,prepared.worktree);
|
|
421
|
+
if(!native.rows.some(r=>r.type==='house_receipt'&&r.call_id===reservation.test_call_id)){
|
|
422
|
+
if(native.rows.some(r=>r.type==='house_proposal'&&(r.call_id===reservation.publish_call_id||r.call_id===reservation.test_call_id)))throw gitFailure('The original page effect has no settled returned owner; it was not replayed.','GIT_VERIFICATION_UNCERTAIN');
|
|
423
|
+
if(typeof this.verifierFactory!=='function')throw gitFailure('The native page executor is unavailable.','GIT_VERIFIER_REQUIRED');
|
|
424
|
+
runner=await this.verifierFactory({...context,page:{prepared,recipe,reservation}});if(runner?.store!==store||runner.session_id!==session.session_id||typeof runner.page?.run!=='function')throw gitFailure('The page executor is not bound to its reserved native session.','GIT_PAGE_EVIDENCE');
|
|
425
|
+
await runner.page.run(signal);native=await this.nativeVerificationSession(session,prepared.worktree);
|
|
426
|
+
}
|
|
427
|
+
const ended=native.rows.find(r=>r.type==='house_receipt'&&r.call_id===reservation.test_call_id);
|
|
428
|
+
if(ended?.status==='returned'&&JSON.parse(ended.output).status==='needs_correction'){assertPageChain(native,{reservation,task:{...recipe.task,project:'git-'+id},sourceDigest:reservation.source_digest,callId:reservation.test_call_id,knownFailure:true});const error=gitFailure('The original native page check failed.','GIT_NATIVE_CHECK_FAILED');error.git_verification_failure={check_id:check.id,session_id:native.id,invocation_id:ended.invocation_id,row:native.rows.indexOf(ended),receipt_digest:ended.audit.sha256,public_result:JSON.parse(ended.output),prefix:nativePrefix(native),scope:'native_page_needs_correction'};throw error;}
|
|
429
|
+
const proof=assertPageChain(native,{reservation,task:recipe.task&&{...recipe.task,project:'git-'+id},sourceDigest:reservation.source_digest,callId:reservation.test_call_id});
|
|
430
|
+
const after=await inspectGitSource({cwd:prepared.worktree,signal});if(after.source_identity!==before.source_identity||after.head!==prepared.candidate_commit||after.dirty)throw gitFailure('Page verification changed the combined source.','GIT_VERIFICATION_SOURCE_CHANGED');
|
|
431
|
+
return {integration_id:id,candidate_commit:prepared.candidate_commit,verification_digest:root.spec.verification_digest,source_identity:after.source_identity,native_session_id:session.session_id,native_journal:session.journal,native_binding:binding,native_journal_sha256:sha(native.bytes),receipts:[{check_id:check.id,...proof}],page:{format:'comal-git-page-proof/1',reservation,recipe,public_result:JSON.parse(ended.output),task:{...recipe.task,project:'git-'+id},prefix:nativePrefix(native)},status:'passed_declared_checks',scope:'native_combined_page_checks_not_human_acceptance',asked:'unjudged'};
|
|
432
|
+
}
|
|
433
|
+
const executor=async()=>{
|
|
434
|
+
if(typeof this.verifierFactory!=='function')throw gitFailure('No host-bound native verification executor is configured.','GIT_VERIFIER_REQUIRED');
|
|
435
|
+
if(!runner)runner=await this.verifierFactory({cwd:prepared.worktree,store,owner:immutable(structuredClone(owner)),resource_id:id,attempt_id,session_id:session.session_id,signal});
|
|
436
|
+
if(runner?.store!==store||typeof runner.tools?.execute!=='function'||runner.session_id!==session.session_id)throw gitFailure('The verifier must use the reserved native journal/executor.','GIT_NATIVE_VERIFICATION_REQUIRED');return runner;
|
|
437
|
+
};
|
|
438
|
+
if(session.contract_digest!==root.spec.verification_digest||session.cwd!==prepared.worktree)throw gitFailure('Verifier contract binding changed.','GIT_NATIVE_VERIFICATION_REQUIRED');
|
|
439
|
+
const receipts=[];
|
|
440
|
+
for(const check of root.spec.verification.checks){
|
|
441
|
+
const args={command:check.command,timeout:check.timeout||30000};let call=await phase('check:'+check.id);
|
|
442
|
+
if(!call)call=await phase('check:'+check.id,{call_id:randomUUID(),arguments_digest:digest(args)});
|
|
443
|
+
if(call.arguments_digest!==digest(args))throw gitFailure('Prepared check arguments changed.','GIT_NATIVE_VERIFICATION_REQUIRED');
|
|
444
|
+
let native=await this.nativeVerificationSession(session,prepared.worktree),existing=native.rows.find(row=>row.type==='house_receipt'&&row.call_id===call.call_id);
|
|
445
|
+
if(!existing){
|
|
446
|
+
if(native.rows.some(row=>row.type==='house_proposal'&&row.call_id===call.call_id))throw gitFailure('The original native check was proposed but has no durable outcome. It was not replayed.','GIT_VERIFICATION_UNCERTAIN');
|
|
447
|
+
await executor();let returned,error;
|
|
448
|
+
try{returned=await runner.tools.execute('Bash',args,{signal,callId:call.call_id});}catch(cause){error=cause;}
|
|
449
|
+
native=await this.nativeVerificationSession(session,prepared.worktree);
|
|
450
|
+
const receipt=this.nativeCheckProof(native,owner,check,args,call.call_id);
|
|
451
|
+
if(error)throw gitFailure('The executor return failed despite its native receipt; inspect it before continuing.','GIT_VERIFICATION_UNCERTAIN');
|
|
452
|
+
if(receipt.output!==returned)throw gitFailure('Executor output differs from its native receipt.','GIT_NATIVE_VERIFICATION_REQUIRED');
|
|
453
|
+
}
|
|
454
|
+
const receipt=this.nativeCheckProof(native,owner,check,args,call.call_id);
|
|
455
|
+
receipts.push({check_id:check.id,call_id:call.call_id,invocation_id:receipt.invocation_id,row:receipt.evidence_row,receipt_digest:receipt.audit.sha256,output_digest:receipt.output_digest});
|
|
456
|
+
await phase('check_observed:'+check.id,receipts.at(-1));
|
|
457
|
+
}
|
|
458
|
+
const after=await inspectGitSource({cwd:prepared.worktree,signal});if(after.source_identity!==before.source_identity||after.head!==prepared.candidate_commit||after.dirty){const error=gitFailure('Verification modified the combined source; it cannot approve the prepared candidate.','GIT_VERIFICATION_SOURCE_CHANGED');error.git_verification_failure={scope:'verification_source_changed',receipts};throw error;}
|
|
459
|
+
return{integration_id:id,candidate_commit:prepared.candidate_commit,verification_digest:root.spec.verification_digest,source_identity:after.source_identity,native_session_id:session.session_id,native_journal:session.journal,native_binding:binding,native_journal_sha256:sha((await this.nativeVerificationSession(session,prepared.worktree)).bytes),receipts,status:'passed_declared_checks',scope:'native_combined_checks_not_human_acceptance',asked:'unjudged'};
|
|
460
|
+
},signal);}finally{await lock.release();}
|
|
461
|
+
}
|
|
462
|
+
async applyIntegration(id,{attempt_id,expected_candidate},{owner,signal}={}){
|
|
463
|
+
if(!attempt_id||typeof attempt_id!=='string'||attempt_id.length>120)throw gitFailure('Integration requires a unique attempt_id.','GIT_INPUT');
|
|
464
|
+
await this.initialize();const c=await this.context(id),lock=await this.lock(c);
|
|
465
|
+
try{return await this.action(c,owner,'integration_apply',attempt_id,{expected_candidate},async({book,root,mutate,phase})=>{
|
|
466
|
+
const prepared=book.rows.find(row=>row.type==='git_action_returned'&&row.action==='integration_prepare')?.output,lastCheck=book.rows.findLast(row=>['git_action_returned','git_action_rejected'].includes(row.type)&&row.action==='integration_verify'),verified=lastCheck?.type==='git_action_returned'?lastCheck.output:null;
|
|
467
|
+
if(prepared&&(root.spec.kind!=='integration'||prepared.integration_id!==id||prepared.target_ref!=='refs/comal/integrations/'+root.spec.target||prepared.expected_target!==root.spec.expected_target||prepared.base_commit!==root.spec.base_commit||root.spec.verification_digest!==digest(root.spec.verification)))throw gitFailure('Integration admission is bound to a different target or contract.','GIT_VERIFICATION_REQUIRED');
|
|
468
|
+
if(!prepared||!verified||verified.status!=='passed_declared_checks'||verified.candidate_commit!==prepared.candidate_commit||expected_candidate!==prepared.candidate_commit||verified.verification_digest!==root.spec.verification_digest||!verified.receipts.length)throw gitFailure('The exact combined candidate has no passed native verification.','GIT_VERIFICATION_REQUIRED');
|
|
469
|
+
const source=await inspectGitSource({cwd:prepared.worktree,signal});if(source.source_identity!==verified.source_identity||source.head!==prepared.candidate_commit||source.dirty)throw gitFailure('Verified combined source changed.','GIT_VERIFICATION_SOURCE_CHANGED');
|
|
470
|
+
const native=await this.nativeVerificationSession({journal:verified.native_journal,session_id:verified.native_session_id,binding:verified.native_binding},prepared.worktree);
|
|
471
|
+
if(verified.native_binding?.resource_id!==id||verified.native_binding.candidate_commit!==prepared.candidate_commit||verified.native_binding.owner_digest!==ownerDigest(owner)||verified.native_binding.verification_digest!==root.spec.verification_digest)throw gitFailure('Native verification belongs to another resource, owner or contract.','GIT_VERIFICATION_REQUIRED');
|
|
472
|
+
if(verified.receipts.length!==root.spec.verification.checks.length)throw gitFailure('Verification receipt count changed after admission.','GIT_VERIFICATION_REQUIRED');
|
|
473
|
+
let pageAdmission;
|
|
474
|
+
if(isPageVerification(root.spec.verification)){
|
|
475
|
+
const page=verified.page;if(page?.reservation?.kernel_identity!==this.pageKernelIdentity())throw gitFailure('Original page kernel implementation changed.','GIT_PAGE_RECIPE_CHANGED');if(page?.format!=='comal-git-page-proof/1'||digest(page.recipe)!==digest(root.spec.page_recipes[0])||digest(page.reservation.native_binding)!==digest(verified.native_binding))throw gitFailure('The reserved page proof changed.','GIT_PAGE_EVIDENCE');
|
|
476
|
+
assertNativePrefix(native,page.prefix);
|
|
477
|
+
const proof=assertPageChain(native,{reservation:page.reservation,task:page.task,sourceDigest:page.reservation.source_digest,callId:page.reservation.test_call_id});
|
|
478
|
+
if(proof.invocation_id!==verified.receipts[0].invocation_id||proof.receipt_digest!==verified.receipts[0].receipt_digest)throw gitFailure('Original page check receipt changed.','GIT_PAGE_EVIDENCE');
|
|
479
|
+
let final=await phase('page_final_reserved');
|
|
480
|
+
if(final)throw gitFailure('This Apply already reserved a read-only admission. Inspect its original outcome and use a new Apply attempt; no old final or effect is replayed.','GIT_PAGE_FINAL_EXPIRED');
|
|
481
|
+
final=await phase('page_final_reserved',{format:'comal-git-page-admission/1',call_id:randomUUID(),verification_prefix:page.prefix,before:nativePrefix(native),reservation_digest:digest(page.reservation)});
|
|
482
|
+
const store=new SessionStore({cwd:prepared.worktree,stateDir:path.join(prepared.worktree,'.comal')});
|
|
483
|
+
await store.append(verified.native_session_id,{type:'git_page_apply_reserved',source:'git_page_host',integration_id:id,attempt_id,admission:final,admission_digest:digest(final)});
|
|
484
|
+
if(typeof this.verifierFactory!=='function')throw gitFailure('Original page runtime is unavailable.','GIT_VERIFIER_REQUIRED');
|
|
485
|
+
const runner=await this.verifierFactory({cwd:prepared.worktree,store,owner:immutable(structuredClone(owner)),resource_id:id,attempt_id:page.reservation.attempt_id,session_id:verified.native_session_id,signal,binding:verified.native_binding,kernel_identity:this.pageKernelIdentity(),page:{prepared,recipe:page.recipe,reservation:page.reservation,admission:final}});
|
|
486
|
+
if(runner?.store!==store||runner.session_id!==verified.native_session_id||typeof runner.page?.final!=='function')throw gitFailure('Original read-only page executor is unavailable.','GIT_PAGE_EVIDENCE');
|
|
487
|
+
if(!runner.page.authority)throw gitFailure('Original Apply leaf authority is unavailable.','GIT_PAGE_EVIDENCE');
|
|
488
|
+
await phase('page_authority_reserved',runner.page.authority);
|
|
489
|
+
await runner.page.final(final.call_id,signal);
|
|
490
|
+
const current=await this.nativeVerificationSession({journal:verified.native_journal,session_id:verified.native_session_id,binding:verified.native_binding},prepared.worktree);
|
|
491
|
+
assertNativePrefix(current,page.prefix);assertNativePrefix(current,final.before);
|
|
492
|
+
pageAdmission=assertPageChain(current,{reservation:page.reservation,task:page.task,sourceDigest:page.reservation.source_digest,callId:final.call_id,final:true,afterRows:final.before.rows,authority:runner.page.authority});
|
|
493
|
+
const fresh=await inspectGitSource({cwd:prepared.worktree,signal});if(fresh.dirty||fresh.source_identity!==verified.source_identity||fresh.head!==prepared.candidate_commit)throw gitFailure('Candidate changed during read-only admission.','GIT_VERIFICATION_SOURCE_CHANGED');
|
|
494
|
+
await phase('page_final_observed',pageAdmission);
|
|
495
|
+
}else{
|
|
496
|
+
if(sha(native.bytes)!==verified.native_journal_sha256)throw gitFailure('Verification journal changed after admission.','GIT_VERIFICATION_REQUIRED');
|
|
497
|
+
for(const[index,check]of root.spec.verification.checks.entries()){
|
|
498
|
+
const proof=verified.receipts[index],receipt=this.nativeCheckProof(native,owner,check,{command:check.command,timeout:check.timeout||30000},proof.call_id);
|
|
499
|
+
if(proof.check_id!==check.id||receipt.invocation_id!==proof.invocation_id||receipt.audit.sha256!==proof.receipt_digest||receipt.evidence_row!==proof.row)throw gitFailure('Native verification reference differs from the prepared check.','GIT_VERIFICATION_REQUIRED');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if(!prepared.target_ref.startsWith('refs/comal/integrations/')||(await this.worktreeList(signal)).some(item=>item.branch===prepared.target_ref))throw gitFailure('Integration target is attached to a checkout or outside the reserved namespace.','GIT_REF_CHECKED_OUT');
|
|
503
|
+
await phase('integration_admitted',{candidate_commit:prepared.candidate_commit,target_ref:prepared.target_ref,expected_target:prepared.expected_target,verification_digest:verified.verification_digest,native_journal_sha256:verified.native_journal_sha256,...(pageAdmission?{page_admission:pageAdmission}: {})});
|
|
504
|
+
const admissionMutate=pageAdmission?(args,options={})=>mutate(args,{...options,beforeRelease:async()=>{
|
|
505
|
+
const current=await inspectGitSource({cwd:prepared.worktree,signal});
|
|
506
|
+
if(current.dirty||current.head!==prepared.candidate_commit||current.source_identity!==verified.source_identity)throw gitFailure('Candidate source changed after read-only admission; Git CAS was not released.','GIT_VERIFICATION_SOURCE_CHANGED');
|
|
507
|
+
}}):mutate;
|
|
508
|
+
await this.cas(admissionMutate,prepared.target_ref,prepared.candidate_commit,prepared.expected_target==='absent'?null:prepared.expected_target,root.spec.repository.object_format,signal);await this.fault('integration_ref',{id,prepared});
|
|
509
|
+
return{integration_id:id,target_ref:prepared.target_ref,applied_commit:prepared.candidate_commit,previous:prepared.expected_target,verification_digest:root.spec.verification_digest,scope:'recorded_target_ref_update_not_human_acceptance',human_checkout:'preserved',asked:'unjudged'};
|
|
510
|
+
},signal);}finally{await lock.release();}
|
|
511
|
+
}
|
|
512
|
+
async retire(id,{reason='Source retained for review'}={}, {owner}={}){
|
|
513
|
+
if(typeof reason!=='string'||reason.length>1000)throw gitFailure('Invalid resource retirement reason.','GIT_INPUT');await this.initialize();const c=await this.context(id),lock=await this.lock(c);
|
|
514
|
+
try{const{book}=await this.load(c,owner),previous=book.rows.find(row=>row.type==='git_resource_retired');if(previous)return previous.outcome;
|
|
515
|
+
const outcome={resource_id:id,lifecycle:'retired_preserved',files:'preserved',refs_and_history:'preserved',processes:'not_stopped_by_retirement',asked:'unjudged'};
|
|
516
|
+
await c.append({type:'git_resource_retired',reason,outcome});return outcome;
|
|
517
|
+
}finally{await lock.release();}
|
|
518
|
+
}
|
|
519
|
+
}
|