@awebai/oats 0.24.1 → 0.24.2

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.
@@ -0,0 +1,91 @@
1
+ // Execution-only consumption of the kernel's selected private snapshots.
2
+ // This establishes correspondence/custody, NOT native human/team authority.
3
+ // No kernel index, source checkout, ambient team/root, or credential is read.
4
+ import fs from 'node:fs';
5
+ import {dirname,isAbsolute,resolve} from 'node:path';
6
+ import {parseBindingJson,BINDING_WIRE_LIMITS} from './binding-wire.mjs';
7
+ import {INVOCATION_LIMITS,sameInvocationJson as same} from './invocation-shape.mjs';
8
+ import {validateAwebBinding,validateAwebInvocationContext} from './portable-binding.mjs';
9
+ const CAPABILITY='oats.aweb';
10
+ const RECEIPT_LIMITS={bytes:256*1024,depth:32,entries:16384};
11
+ const markers=['OATS_BINDING_FILE','OATS_INVOCATION_CONTEXT_FILE','OATS_SOURCE_RECEIPT_FILE'];
12
+ const invalid=()=>{throw Object.assign(new Error('invalid captured aweb execution input'),{code:'invalid-binding'});};
13
+ const absentAuthority=()=>{throw Object.assign(new Error('captured aweb action requires an admitted instance intent'),{code:'authorization-required'});};
14
+ const absolute=p=>typeof p==='string'&&!p.includes('\0')&&isAbsolute(p)&&resolve(p)===p;
15
+ const keys=(value,names)=>value!==null&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).length===names.length&&names.every(k=>Object.hasOwn(value,k));
16
+ const statKey=s=>['dev','ino','size','mtimeNs','ctimeNs'].map(k=>String(s[k])).join(':');
17
+ function physical(file){
18
+ if(!absolute(file))invalid();
19
+ for(let p=file;;p=dirname(p)){
20
+ const s=fs.lstatSync(p,{bigint:true});
21
+ if(s.isSymbolicLink()||(p!==file&&!s.isDirectory()))invalid();
22
+ if(dirname(p)===p)return s;
23
+ }
24
+ }
25
+ /** Pin the physical named file to its open descriptor BEFORE every bounded
26
+ * read, including when an ancestor was redirected only during open(). */
27
+ function snapshot(file,limits){
28
+ let fd;
29
+ try{
30
+ physical(file);
31
+ const uid=typeof process.getuid==='function'?BigInt(process.getuid()):null;
32
+ const privateFile=s=>s.isFile()&&s.nlink===1n&&s.size<=BigInt(limits.bytes)&&(s.mode&0o7777n)===0o600n&&(uid===null||s.uid===uid);
33
+ const before=fs.lstatSync(file,{bigint:true});
34
+ if(!privateFile(before)||typeof fs.constants.O_NOFOLLOW!=='number'||typeof fs.constants.O_NONBLOCK!=='number')invalid();
35
+ fd=fs.openSync(file,fs.constants.O_RDONLY|fs.constants.O_NOFOLLOW|fs.constants.O_NONBLOCK);
36
+ const check=()=>{
37
+ physical(file);const named=fs.lstatSync(file,{bigint:true}),opened=fs.fstatSync(fd,{bigint:true});
38
+ if(!privateFile(named)||!privateFile(opened)||statKey(named)!==statKey(before)||statKey(opened)!==statKey(named))invalid();
39
+ };
40
+ check();const bytes=Buffer.alloc(Number(before.size));
41
+ for(let n=0;n<bytes.length;){check();const count=fs.readSync(fd,bytes,n,Math.min(32768,bytes.length-n),n);if(!count)invalid();n+=count;}
42
+ check();return {value:parseBindingJson(bytes,limits),identity:statKey(before)};
43
+ }catch{invalid();}finally{if(fd!==undefined)fs.closeSync(fd);}
44
+ }
45
+ function freeze(value){if(value&&typeof value==='object'){for(const v of Object.values(value))freeze(v);Object.freeze(value);}return value;}
46
+ function validateSourceReceipt(receipt,context,binding){
47
+ if(!keys(receipt,['schemaVersion','kind','home','work','context','agent','instance','sourceIdentity','role','executionBinding','responsibleHuman','binding']))invalid();
48
+ const i=context.instance;
49
+ if(!i||receipt.schemaVersion!==1||receipt.kind!==context.subject.kind||receipt.home!==i.home||receipt.work!==i.work||receipt.context!==context.executionBinding.deployment||receipt.agent!==i.agent||receipt.instance!==i.name||typeof receipt.role!=='string'||!receipt.role.trim()||!same(receipt.executionBinding,context.executionBinding)||!same(receipt.responsibleHuman,context.responsibleHuman)||!same(receipt.binding,binding))invalid();
50
+ if(context.action.kind!=='hook'||!['spawn','retire'].includes(context.action.name)||!same(receipt.sourceIdentity,context.subject.kind==='persistent'?context.subject.soul.identity:null))invalid();
51
+ }
52
+ /** Absence is explicitly legacy. Any supplied input requires the full selected
53
+ * binding+invocation pair; invalid-present can never select legacy behavior.
54
+ * assertCurrent is for every native boundary, not a new admission mechanism. */
55
+ export function loadCapturedAwebExecution(env=process.env){
56
+ if(!markers.some(k=>Object.hasOwn(env,k)))return {kind:'legacy'};
57
+ if(!Object.hasOwn(env,markers[0])||!Object.hasOwn(env,markers[1]))invalid();
58
+ const files=markers.filter(k=>Object.hasOwn(env,k)).map(k=>env[k]);if(new Set(files).size!==files.length)invalid();
59
+ const inputs=files.map((p,i)=>snapshot(p,[BINDING_WIRE_LIMITS,INVOCATION_LIMITS,RECEIPT_LIMITS][i]));
60
+ let binding,context;
61
+ try{binding=validateAwebBinding(inputs[0].value);context=validateAwebInvocationContext(inputs[1].value,binding);}catch{invalid();}
62
+ if(inputs[2])validateSourceReceipt(inputs[2].value,context,binding);
63
+ const assertCurrent=()=>{
64
+ files.forEach((p,i)=>{const current=snapshot(p,[BINDING_WIRE_LIMITS,INVOCATION_LIMITS,RECEIPT_LIMITS][i]);if(current.identity!==inputs[i].identity||!same(current.value,inputs[i].value))invalid();});
65
+ };
66
+ assertCurrent();
67
+ return {kind:'captured',binding:freeze(binding),context:freeze(context),sourceReceipt:inputs[2]?freeze(inputs[2].value):null,assertCurrent};
68
+ }
69
+ /** Exact entrypoint and retained instance, not OATS_META/current config, choose
70
+ * the actor's target. The kernel still owns admission and replay. */
71
+ export function requireCapturedAwebAction(loaded,event,manifest,{env=process.env,cwd=process.cwd()}={}){
72
+ if(loaded.kind!=='captured'||manifest.capability!==CAPABILITY)invalid();
73
+ const {context}=loaded,a=context.action,i=context.instance;
74
+ if(a.kind==='hook'){
75
+ if(a.capability!==CAPABILITY||a.name!==event||!Object.hasOwn(manifest.hooks||{},event))invalid();
76
+ }else if(a.kind==='command'){
77
+ if(a.name!==event||!Object.hasOwn(manifest.commands||{},event)||(a.capability!==CAPABILITY&&a.namespace!==manifest.command))invalid();
78
+ }else invalid();
79
+ if(!i||!context.intent)absentAuthority();
80
+ if(cwd!==i.home)invalid();
81
+ for(const [key,value] of Object.entries({OATS_HOME:i.home,OATS_INSTANCE_HOME:i.home,OATS_INSTANCE:i.name,OATS_AGENT:i.agent,OATS_CONTEXT:context.executionBinding.deployment,OATS_DEPLOYMENT:context.executionBinding.deployment,OATS_RESOLUTION:context.executionBinding.resolution.id,OATS_CAPABILITY:CAPABILITY}))if(Object.hasOwn(env,key)&&env[key]!==value)invalid();
82
+ if(a.kind==='hook'&&Object.hasOwn(env,'OATS_EVENT')&&env.OATS_EVENT!==event)invalid();
83
+ const directories=[i.home,i.work,context.executionBinding.deployment].map(p=>{
84
+ try{physical(p);const s=fs.lstatSync(p,{bigint:true});if(!s.isDirectory())invalid();return {path:p,dev:s.dev,ino:s.ino};}catch{invalid();}
85
+ });
86
+ const assertCurrent=()=>{
87
+ loaded.assertCurrent();
88
+ for(const d of directories){try{physical(d.path);const s=fs.lstatSync(d.path,{bigint:true});if(!s.isDirectory()||s.dev!==d.dev||s.ino!==d.ino)invalid();}catch{invalid();}}
89
+ };
90
+ assertCurrent();return {binding:loaded.binding,context,sourceReceipt:loaded.sourceReceipt,assertCurrent};
91
+ }
@@ -0,0 +1,91 @@
1
+ // Selected-input adapter over EXISTING native aw commands. No account/grant
2
+ // protocol, credential copying, private kernel index or second admission store.
3
+ import fs from 'node:fs';
4
+ import {join} from 'node:path';
5
+ import {parseBindingJson,BINDING_WIRE_LIMITS} from './binding-wire.mjs';
6
+ const fail=(code,message)=>{throw Object.assign(new Error(message),{code});};
7
+ function identityDirectory(home,{required=true}={}){
8
+ const path=join(home,'.aw');
9
+ try{const s=fs.lstatSync(path);if(!s.isDirectory()||s.isSymbolicLink()||fs.realpathSync(path)!==path)fail('needs-configuration','native identity directory must be physical and local to its selected home');return path;}
10
+ catch(e){if(e.code==='ENOENT'&&!required)return null;if(e.code==='needs-configuration')throw e;fail('needs-configuration','the selected home needs an initialized native aw identity');}
11
+ }
12
+ /** `run` is the same bounded, argv-only, redacting native runner used by legacy
13
+ * hooks. The execution consumer and each call retain the original snapshots.
14
+ * This function is not callable as a substitute for kernel admission. */
15
+ export function runCapturedNative({selected,event,settings,run,env=process.env}){
16
+ const {context,binding,assertCurrent}=selected,{home,name}=context.instance,team=binding.payload.privateTeam?.id;
17
+ let meta=context.priorReceipt===null?null:JSON.parse(JSON.stringify(context.priorReceipt)),invitationToken=null;
18
+ const roots=new Map();
19
+ const nativeIdentity=(dir,options)=>{
20
+ const path=identityDirectory(dir,options);
21
+ if(!path){if(roots.has(join(dir,'.aw')))fail('invalid-binding','native identity directory disappeared during the captured action');return null;}
22
+ const s=fs.lstatSync(path),stamp=`${s.dev}:${s.ino}`,prior=roots.get(path);
23
+ if(prior!==undefined&&prior!==stamp)fail('invalid-binding','native identity directory changed during the captured action');
24
+ roots.set(path,stamp);return path;
25
+ };
26
+ const refusal=(code,message)=>({exitCode:1,output:{...(meta?{meta}:{}),status:'needs-configuration',problems:[{code,message}],warning:`oats-aweb: ${message}; retain the home and any recorded partial effects`}});
27
+ try{
28
+ assertCurrent();
29
+ if(!/^[a-z0-9][a-z0-9._-]{0,127}$/i.test(name)||!team)fail('needs-configuration','exact native alias and private-team selection are required');
30
+ if(settings.delivery!=='session')fail('needs-configuration','captured delivery must be session');
31
+ if(binding.payload.wider.length)fail('needs-configuration','selected wider memberships need their explicitly qualified native setup; they were not omitted');
32
+ // A global override may select a different actor even with an exact cwd.
33
+ // Do not clear/replace it or borrow that identity to make setup pass.
34
+ if(env.AWEB_IDENTITY_HOME)fail('needs-configuration','captured setup requires native per-directory identity context, not an external AWEB_IDENTITY_HOME override');
35
+ const call=(args,cwd=home,{secret=false,allowNativeRemoval=false}={})=>{
36
+ assertCurrent();nativeIdentity(cwd,{required:false});
37
+ const out=run(['aw',...args],cwd,45000,{secretSafe:true});
38
+ assertCurrent();
39
+ if(allowNativeRemoval){
40
+ try{fs.lstatSync(join(cwd,'.aw'));nativeIdentity(cwd,{required:false});}catch(e){if(e.code!=='ENOENT')throw e;}
41
+ }else nativeIdentity(cwd,{required:false});
42
+ try{return parseBindingJson(Buffer.from(out),BINDING_WIRE_LIMITS);}catch{if(secret)fail('provider-unavailable','native invitation response is unavailable');fail('provider-unavailable','native command returned no bounded JSON observation');}
43
+ };
44
+ const plain=(args)=>{assertCurrent();nativeIdentity(home);run(['aw',...args],home,60000,{secretSafe:true});assertCurrent();nativeIdentity(home);};
45
+ const observe=()=>{
46
+ nativeIdentity(home);
47
+ const who=call(['--team',team,'whoami','--json']);
48
+ const status=call(['--team',team,'workspace','status','--json']),ws=status.workspace;
49
+ if(who.alias!==name||typeof who.did!=='string'||!/^did:key:[A-Za-z0-9]+$/.test(who.did)||status.selected_team!==team||!ws||ws.alias!==name||ws.workspace_path!==home)fail('needs-configuration','native alias, team and workspace must match the selected instance home');
50
+ if(invitationToken&&who.did.includes(invitationToken))fail('provider-unavailable','native identity response cannot be safely recorded');
51
+ if(meta?.did!==undefined&&meta.did!==who.did)fail('needs-configuration','native identity changed from the recorded setup; explicit reconciliation is required');
52
+ return who.did;
53
+ };
54
+ if(event==='setup'){
55
+ if(!nativeIdentity(home,{required:false}))return{exitCode:0,output:{status:'needs-configuration',problems:[{code:'needs-configuration',message:'use explicit captured spawn with existing native authority in the selected deployment; setup does not provision accounts or teams'}]}};
56
+ observe();return{exitCode:0,output:{status:'ready',problems:[],scope:'native home association only; no account or private-grant attestation'}};
57
+ }
58
+ if(event==='roster'){
59
+ observe();const result=call(['id','team','members','--team-id',team,'--json']);return{exitCode:0,output:result};
60
+ }
61
+ if(event==='spawn'){
62
+ if(meta!==null||nativeIdentity(home,{required:false}))fail('needs-configuration','existing or uncertain native setup requires explicit reconciliation; no duplicate enrollment');
63
+ // Deployment is an explicit retained context, never an ambient root search.
64
+ // Native aw itself decides whether its existing identity may issue an invite.
65
+ const authority=context.executionBinding.deployment;nativeIdentity(authority);
66
+ meta={team,delivery:'session',pending:'invite'};
67
+ const invitation=call(['team','invite','--team-id',team,'--json'],authority,{secret:true});
68
+ if(typeof invitation?.token!=='string'||!invitation.token)fail('provider-unavailable','native invitation returned no token');
69
+ invitationToken=invitation.token;
70
+ meta.pending='join';
71
+ // Native aw mints the child's own identity; OATS never reads/copies a key.
72
+ const joined=call(['team','join',invitation.token,'--name',name,'--json'],home,{secret:true});
73
+ // Never echo server strings or a token disguised as an alias into metadata.
74
+ if(joined?.alias!==name||joined?.team_id!==team)fail('provider-unavailable','native join did not confirm the exact requested alias and team');
75
+ meta={team,alias:name,delivery:'session',pending:'connect'};
76
+ plain(['--team',team,'init','--do-not-touch-agents-md']);
77
+ const did=observe();meta={team,alias:name,delivery:'session',did,pending:'wake-register'};
78
+ plain(['wake','register','--home',home,'--identity-home',join(home,'.aw'),'--delivery','session']);
79
+ meta={team,alias:name,delivery:'session',did};
80
+ return{exitCode:0,output:{meta,env:{AWEB_DELIVERY:'session'},brief:`Comms: your own native aweb identity is ${name} on ${team}. Notification delivery: external (session); registration is durable, not proof of broker delivery or model consumption. Use aw mail/aw chat from this instance home.`}};
81
+ }
82
+ if(event==='retire'){
83
+ if(!meta||meta.alias!==name||meta.team!==team||meta.delivery!=='session'||typeof meta.did!=='string')fail('needs-configuration','recorded owned native identity is required for cleanup; absence is not proof of no effects');
84
+ observe();plain(['wake','deregister','--home',home]);
85
+ const result=call(['--team',team,'workspace','delete',name,'--json'],home,{allowNativeRemoval:true});
86
+ if(result?.alias!==name||typeof result.deleted_at!=='string')fail('provider-unavailable','native self-delete outcome is unconfirmed');
87
+ return{exitCode:0,output:{meta:{...meta,retired:true,aliasReusable:result.alias_released===true},...(result.alias_released===true?{}:{warning:'oats-aweb: workspace deleted; alias release was not confirmed'})}};
88
+ }
89
+ fail('needs-configuration','this captured native entrypoint is unsupported');
90
+ }catch(e){return refusal(['needs-configuration','invalid-binding','authorization-required'].includes(e?.code)?e.code:'provider-unavailable',['needs-configuration','invalid-binding','authorization-required'].includes(e?.code)?e.message:'native action failed or is uncertain');}
91
+ }
@@ -0,0 +1,135 @@
1
+ // Provider-side consumption checks for the kernel-owned CapturedInvocationContext1.
2
+ // Decoded JSON only: no source/config parsing, artifact lookup or authorization.
3
+ // The kernel must verify retained authority before invoking provider code.
4
+ import { isAbsolute, join, resolve } from 'node:path';
5
+ export const INVOCATION_LIMITS=Object.freeze({bytes:512*1024,depth:32,entries:16384});
6
+ export const PRIOR_RECEIPT_LIMITS=Object.freeze({bytes:128*1024,depth:24,entries:8192});
7
+ const fail=()=>{throw Object.assign(new Error('invalid captured invocation'),{code:'invalid-binding'});};
8
+ const obj=value=>value!==null && typeof value==='object' && !Array.isArray(value);
9
+ const canonical=value=>value===null || typeof value!=='object'?JSON.stringify(value):Array.isArray(value)?`[${value.map(canonical).join(',')}]`:`{${Object.keys(value).sort().map(key=>`${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
10
+ export const sameInvocationJson=(a,b)=>canonical(a)===canonical(b);
11
+ const same=sameInvocationJson;
12
+ function keys(value,allowed,required=allowed) {if(!obj(value)) fail();for(const key of Object.keys(value)) if(!allowed.includes(key)) fail();for(const key of required) if(!Object.hasOwn(value,key)) fail();}
13
+ function text(value,empty=false) {if(typeof value!=='string' || (!empty && !value.length) || value.includes('\0')) fail();}
14
+ function list(value) {if(!Array.isArray(value)) fail();return value;}
15
+ function absolute(value) {text(value);if(!isAbsolute(value) || resolve(value)!==value) fail();}
16
+ function path(value,root=false) {text(value);if(root && value==='.') return;if(value.startsWith('/') || value.includes('\\') || value.split('/').some(part=>!part || part==='.' || part==='..')) fail();}
17
+ function hash(value) {if(typeof value!=='string' || !/^sha256-[a-f0-9]{64}$/.test(value)) fail();}
18
+ function integrity(value,format) {keys(value,['format','value']);if(value.format!==format) fail();hash(value.value);}
19
+ function capability(value) {text(value);if(!/^(?:@?[a-z0-9][a-z0-9._-]*[./])[a-z0-9][a-z0-9._/-]*$/.test(value)) fail();}
20
+ function repository(value) {
21
+ if(value?.kind==='canonical-remote') {keys(value,['kind','remote']);text(value.remote);}
22
+ else if(value?.kind==='provider-repository') {keys(value,['kind','provider','host','id']);text(value.provider);text(value.host);text(value.id);}
23
+ else fail();
24
+ }
25
+ function soulIdentity(value) {
26
+ if(value?.kind==='git-soul') {keys(value,['kind','repository','exportPath']);repository(value.repository);path(value.exportPath);}
27
+ else if(value?.kind==='local-soul') {keys(value,['kind','source','exportPath']);text(value.source);if(!value.source.startsWith('path:')) fail();path(value.exportPath,true);}
28
+ else fail();
29
+ }
30
+ function artifact(value) {
31
+ if(value?.kind==='soul') {keys(value,['kind','identity','integrity']);soulIdentity(value.identity);}
32
+ else if(value?.kind==='capability') {keys(value,['kind','capability','integrity']);capability(value.capability);}
33
+ else if(value?.kind==='resource') keys(value,['kind','integrity']);
34
+ else fail();
35
+ integrity(value.integrity,'oats.tree-exec.v1');
36
+ }
37
+ function resolution(value) {keys(value,['schemaVersion','id']);if(value.schemaVersion!==1) fail();hash(value.id);}
38
+ function incarnation(value) {if(typeof value!=='string' || !/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(value)) fail();}
39
+ function intent(value,instance) {
40
+ if(value===null) return;
41
+ keys(value,['schemaVersion','executionId','incarnationId','attempt']);incarnation(value.incarnationId);
42
+ if(value.schemaVersion!==1 || typeof value.executionId!=='string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value.executionId) || !Number.isSafeInteger(value.attempt) || value.attempt<1) fail();
43
+ if(instance===null || value.incarnationId!==instance.incarnationId) fail();
44
+ }
45
+ function origin(value) {
46
+ keys(value,['kind','document','pointer','span'],['kind','document','pointer']);
47
+ if(!['soul-requirement','soul-default','workspace-default','import-adoption','operator','package-dependency','work-target','migration-evidence','provider-binding','workspace-admission','member-backlink','source-export','manifest-default'].includes(value.kind)) fail();
48
+ text(value.pointer,true);if(!/^(?:\/(?:[^~]|~[01])*)*$/.test(value.pointer)) fail();
49
+ const doc=value.document;
50
+ if(doc?.kind==='source') {keys(doc,['kind','source','revision','path','integrity']);text(doc.source);text(doc.revision);path(doc.path);integrity(doc.integrity,'oats.bytes.v1');}
51
+ else if(doc?.kind==='deployment') {keys(doc,['kind','path','integrity']);path(doc.path);integrity(doc.integrity,'oats.bytes.v1');}
52
+ else if(doc?.kind==='operator') {keys(doc,['kind','id']);text(doc.id);}
53
+ else if(doc?.kind==='record') {keys(doc,['kind','ref']);resolution(doc.ref);}
54
+ else if(doc?.kind==='artifact') {keys(doc,['kind','owner','path','integrity']);artifact(doc.owner);path(doc.path);integrity(doc.integrity,'oats.bytes.v1');}
55
+ else fail();
56
+ if(Object.hasOwn(value,'span')) {keys(value.span,['start','end']);if(!Number.isSafeInteger(value.span.start) || !Number.isSafeInteger(value.span.end) || value.span.start<0 || value.span.end<value.span.start) fail();}
57
+ }
58
+ function origins(value) {list(value).forEach(origin);}
59
+ function subject(value) {
60
+ if(value?.kind==='helper') {
61
+ keys(value,['kind','provider','definition','name']);artifact(value.provider);
62
+ keys(value.definition,['owner','path','kind']);artifact(value.definition.owner);path(value.definition.path,true);
63
+ if(value.provider.kind!=='capability' || !same(value.provider,value.definition.owner) || !['file','directory','manifest','skill','runtime-package'].includes(value.definition.kind)) fail();
64
+ if(typeof value.name!=='string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value.name)) fail();return value.name;
65
+ }
66
+ if(value?.kind!=='persistent') fail();keys(value,['kind','soul']);const s=value.soul;
67
+ keys(s,['identity','revision','alias','sourceArtifact','definition','projection']);soulIdentity(s.identity);artifact(s.sourceArtifact);
68
+ if(s.sourceArtifact.kind!=='soul' || !same(s.identity,s.sourceArtifact.identity)) fail();
69
+ if(typeof s.alias!=='string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(s.alias)) fail();path(s.definition);
70
+ keys(s.projection,['roots']);list(s.projection.roots).forEach(value=>path(value,true));
71
+ if(!s.projection.roots.length || !s.projection.roots.some(root=>root==='.' || s.definition===root || s.definition.startsWith(`${root}/`))) fail();
72
+ for(let i=1;i<s.projection.roots.length;i++) if(Buffer.compare(Buffer.from(s.projection.roots[i-1]),Buffer.from(s.projection.roots[i]))>=0) fail();
73
+ if(s.identity.exportPath!=='.' && !s.definition.startsWith(`${s.identity.exportPath}/`)) fail();
74
+ const r=s.revision;
75
+ if(r?.kind==='local') {
76
+ keys(r,['kind','source','integrity','provenance','repository'],['kind','source','integrity','provenance']);text(r.source);if(!r.source.startsWith('path:')) fail();integrity(r.integrity,'oats.tree-exec.v1');
77
+ if(!same(r.integrity,s.sourceArtifact.integrity)) fail();
78
+ if(s.identity.kind==='local-soul') {if(r.source!==s.identity.source || Object.hasOwn(r,'repository')) fail();}
79
+ else {repository(r.repository);if(!same(r.repository,s.identity.repository)) fail();}
80
+ } else {
81
+ keys(r,['identity','remote','selector','commit','provenance']);repository(r.identity);text(r.remote);text(r.selector);
82
+ if(s.identity.kind!=='git-soul' || !same(r.identity,s.identity.repository) || !/^[a-f0-9]{40}$/.test(r.commit)) fail();
83
+ if(r.identity.kind==='canonical-remote' && r.identity.remote!==r.remote) fail();
84
+ }
85
+ origins(r.provenance);if(!r.provenance.length) fail();return s.alias;
86
+ }
87
+ function context(value) {
88
+ if(value?.kind==='workspace') {keys(value,['kind','identity','observation']);keys(value.identity,['repository','path']);repository(value.identity.repository);if(value.identity.path!=='oats-workspace.yaml') fail();origin(value.observation);return {kind:'workspace',identity:value.identity};}
89
+ if(value?.kind==='standalone') {keys(value,['kind','key']);if(value.key!==null) text(value.key);return value;}
90
+ fail();
91
+ }
92
+ function human(value) {if(value===null) return;keys(value,['provider','id']);text(value.provider);text(value.id);}
93
+ function messaging(value,selectedContext,responsibleHuman) {
94
+ if(value?.enabled===false) {keys(value,['schemaVersion','enabled']);if(responsibleHuman!==null) fail();}
95
+ else {
96
+ keys(value,['schemaVersion','enabled','privateKey','wider','provenance']);if(value.enabled!==true) fail();
97
+ keys(value.privateKey,['provider','human','context']);capability(value.privateKey.provider);human(value.privateKey.human);
98
+ if(value.privateKey.human===null || !same(value.privateKey.human,responsibleHuman) || !same(value.privateKey.context,selectedContext) || (selectedContext.kind==='standalone' && selectedContext.key===null)) fail();
99
+ list(value.wider).forEach(row=>{keys(row,['provider','id']);text(row.id);if(row.provider!==value.privateKey.provider) fail();});
100
+ for(let i=1;i<value.wider.length;i++) if(Buffer.compare(Buffer.from(canonical(value.wider[i-1])),Buffer.from(canonical(value.wider[i])))>=0) fail();
101
+ origins(value.provenance);
102
+ }
103
+ if(value.schemaVersion!==1) fail();
104
+ }
105
+ function action(value) {
106
+ if(!obj(value)) fail();
107
+ if(value.kind==='command') {keys(value,['kind','capability','namespace','name'],['kind','name']);if(Object.hasOwn(value,'capability')===Object.hasOwn(value,'namespace')) fail();if(value.capability!==undefined) capability(value.capability);else text(value.namespace);text(value.name);}
108
+ else if(value.kind==='hook') {keys(value,['kind','capability','name']);capability(value.capability);text(value.name);}
109
+ else if(value.kind==='operation') {keys(value,['kind','slot','name']);if(!['knowledge','messaging','tasks'].includes(value.slot)) fail();text(value.name);}
110
+ else if(['inspect','compose'].includes(value.kind)) keys(value,['kind']);
111
+ else fail();
112
+ }
113
+ export function assertInvocationBounds(value,limits=INVOCATION_LIMITS) {
114
+ let entries=0;const active=new Set();
115
+ const visit=(v,depth)=>{
116
+ if(v===null || typeof v==='string' || typeof v==='boolean') return;
117
+ if(typeof v==='number') {if(!Number.isFinite(v)) fail();return;}
118
+ if(typeof v!=='object' || depth>limits.depth || active.has(v)) fail();
119
+ if(!Array.isArray(v) && Object.getPrototypeOf(v)!==Object.prototype && Object.getPrototypeOf(v)!==null) fail();
120
+ active.add(v);for(const child of Object.values(v)) {if(++entries>limits.entries) fail();visit(child,depth+1);}active.delete(v);
121
+ };
122
+ visit(value,1);if(Buffer.byteLength(JSON.stringify(value),'utf8')>limits.bytes) fail();
123
+ }
124
+ export function validateInvocationShape(value,expected={}) {
125
+ assertInvocationBounds(value);keys(value,['schemaVersion','executionBinding','subject','instance','intent','context','responsibleHuman','messagingChoice','capability','action','priorReceipt']);
126
+ if(value.schemaVersion!==1) fail();keys(value.executionBinding,['schemaVersion','deployment','resolution']);if(value.executionBinding.schemaVersion!==1) fail();absolute(value.executionBinding.deployment);resolution(value.executionBinding.resolution);
127
+ const alias=subject(value.subject);
128
+ if(value.instance!==null) {keys(value.instance,['home','work','name','agent','incarnationId']);incarnation(value.instance.incarnationId);absolute(value.instance.home);absolute(value.instance.work);text(value.instance.name);text(value.instance.agent);if(value.instance.agent!==alias || value.instance.work!==join(value.instance.home,'work')) fail();}
129
+ intent(value.intent,value.instance);
130
+ const selectedContext=context(value.context);human(value.responsibleHuman);messaging(value.messagingChoice,selectedContext,value.responsibleHuman);capability(value.capability);action(value.action);assertInvocationBounds(value.priorReceipt,PRIOR_RECEIPT_LIMITS);
131
+ if(value.action.capability!==undefined && value.action.capability!==value.capability) fail();
132
+ if(value.action.kind==='hook' && value.instance===null) fail();
133
+ for(const field of ['capability','context','action']) if(Object.hasOwn(expected,field) && !same(value[field],expected[field])) fail();
134
+ return value;
135
+ }
@@ -0,0 +1,146 @@
1
+ import { validateInvocationShape, sameInvocationJson } from './invocation-shape.mjs';
2
+ export const MESSAGING_CONTRACT='oats.aweb.messaging';
3
+ export const MESSAGING_CONTRACT_VERSION=1;
4
+ export const RESPONSIBLE_HUMAN_KEY='/bindings/messaging/responsibleHuman';
5
+ export const PRIVATE_TEAM_KEY='/bindings/messaging/privateTeam';
6
+ export const WIDER_KEY='/bindings/messaging/wider';
7
+ const CAPABILITY='oats.aweb';
8
+ const identifierPattern=/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,95}$/;
9
+ const obj=value=>value!==null && typeof value==='object' && !Array.isArray(value);
10
+ const clone=value=>JSON.parse(JSON.stringify(value));
11
+ const canonical=value=>value===null || typeof value!=='object'?JSON.stringify(value):Array.isArray(value)?`[${value.map(canonical).join(',')}]`:`{${Object.keys(value).sort().map(key=>`${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
12
+ const same=(a,b)=>canonical(a)===canonical(b);
13
+ const fail=(code,message)=>{throw Object.assign(new Error(message),{code});};
14
+ function keys(value,allowed,required,label) {
15
+ if(!obj(value)) fail('invalid-binding',`${label} must be an object`);
16
+ for(const key of Object.keys(value)) if(!allowed.includes(key)) fail('invalid-binding',`unknown ${label} property: ${key}`);
17
+ for(const key of required) if(!Object.hasOwn(value,key)) fail('invalid-binding',`${label} requires ${key}`);
18
+ return value;
19
+ }
20
+ function identifier(value,label='identity') {if(typeof value!=='string' || !identifierPattern.test(value) || ['constructor','__proto__','prototype','toString','valueOf'].includes(value)) fail('invalid-binding',`invalid ${label}`);return value;}
21
+ const pointerKey=value=>String(value).replace(/~/g,'~0').replace(/\//g,'~1');
22
+ export const teamChoiceKey=alias=>`/bindings/messaging/teams/${pointerKey(identifier(alias,'team alias'))}`;
23
+ function originAt(declaration,pointer,kind) {
24
+ const found=declaration.origins[pointer] ?? declaration.origin;
25
+ if(!obj(found)) fail('invalid-binding',`missing origin for ${pointer}`);
26
+ return clone({...found,kind});
27
+ }
28
+ function teamRef(value) {
29
+ keys(value,['provider','id'],['provider','id'],'aweb team reference');
30
+ if(value.provider!==CAPABILITY || typeof value.id!=='string' || !/^[^\s:]+:[^\s:]+$/.test(value.id)) fail('invalid-binding','aweb team reference requires provider oats.aweb and canonical team id');
31
+ return {provider:CAPABILITY,id:value.id};
32
+ }
33
+ function humanRef(value) {
34
+ keys(value,['provider','id'],['provider','id'],'responsible human');
35
+ if(value.provider!==CAPABILITY || typeof value.id!=='string' || !value.id.trim()) fail('invalid-binding','responsible human needs a provider-resolvable oats.aweb id');
36
+ return {provider:CAPABILITY,id:value.id.trim()};
37
+ }
38
+ function exactContext(value) {
39
+ if(value?.kind==='workspace') {keys(value,['kind','identity','observation'],['kind','identity','observation'],'workspace context');if(!obj(value.identity) || !obj(value.observation)) fail('invalid-binding','workspace context needs qualified identity and observation');return {kind:'workspace',identity:clone(value.identity)};}
40
+ if(value?.kind==='standalone') {keys(value,['kind','key'],['kind','key'],'standalone context');if(typeof value.key!=='string' || !value.key.trim()) fail('needs-configuration','messaging-enabled standalone preparation needs an explicit context key');return {kind:'standalone',key:value.key.trim()};}
41
+ fail('invalid-binding','unsupported messaging context');
42
+ }
43
+ function boundContext(value) {
44
+ if(value?.kind==='workspace') {keys(value,['kind','identity'],['kind','identity'],'bound workspace context');if(!obj(value.identity)) fail('invalid-binding','bound workspace context needs qualified identity');return clone(value);}
45
+ if(value?.kind==='standalone') {keys(value,['kind','key'],['kind','key'],'bound standalone context');if(typeof value.key!=='string' || !value.key.trim()) fail('invalid-binding','bound standalone context needs an explicit key');return {kind:'standalone',key:value.key.trim()};}
46
+ fail('invalid-binding','unsupported bound messaging context');
47
+ }
48
+ function declaration(value) {
49
+ keys(value,['kind','value','origin','origins'],['kind','value','origin','origins'],'messaging declaration');
50
+ if(!['soul','workspace','adoption','operator'].includes(value.kind) || !obj(value.value) || !obj(value.origin) || !obj(value.origins)) fail('invalid-binding','invalid messaging declaration');
51
+ return value;
52
+ }
53
+
54
+ /** Emit messaging provider fields for the kernel's single resolver. Team aliases
55
+ * are availability data; only the explicit operator wider list is consent. */
56
+ export function normalizeMessagingDeclarations({declarations,context}={}) {
57
+ if(!Array.isArray(declarations)) fail('invalid-binding','messaging declarations must be an array');
58
+ const exact=exactContext(context),contextOrigin=context?.kind==='workspace'?clone(context.observation):null,requirements=[],candidates=[],teams={},requested=[],requestedOrigins={},aliases={},aliasOrigins={};let soul=null,privatePolicy=false,privatePolicyOrigin=null;
59
+ for(const raw of declarations) {
60
+ const item=declaration(raw);
61
+ // The kernel supplies complete validated source/workspace/operator
62
+ // declarations, including other providers' fields. Consume only messaging
63
+ // fields here; do not impose a second closed schema on the whole document.
64
+ if(item.kind==='soul') {
65
+ if(soul) fail('requirement-conflict','multiple soul messaging declarations');soul=item;
66
+ const list=item.value.teams ?? [];
67
+ if(!Array.isArray(list) || new Set(list).size!==list.length) fail('invalid-binding','soul teams must be a unique array');
68
+ for(let index=0;index<list.length;index++) {const alias=identifier(list[index],'soul team alias');requested.push(alias);requestedOrigins[alias]=originAt(item,`/teams/${index}`,'soul-requirement');}
69
+ } else if(item.kind==='workspace') {
70
+ const value=item.value.teams;if(value===undefined) continue;if(!obj(value)) fail('invalid-binding','workspace teams must be an object');
71
+ for(const [alias,ref] of Object.entries(value)) {
72
+ if(alias==='private') {if(ref!=='per-human') fail('invalid-binding','workspace private team policy must be per-human');privatePolicy=true;privatePolicyOrigin=originAt(item,'/teams/private','workspace-default');continue;}
73
+ const key=teamChoiceKey(alias),origin=originAt(item,`/teams/${pointerKey(alias)}`,'workspace-default');
74
+ teams[alias]=key;candidates.push({key,kind:'workspace-default',value:teamRef(ref),origin});
75
+ }
76
+ } else if(item.kind==='adoption') {
77
+ const value=item.value.teamAliases;if(value===undefined) continue;if(!obj(value)) fail('invalid-binding','adoption teamAliases must be an object');
78
+ for(const [source,target] of Object.entries(value)) {
79
+ identifier(source,'source team alias');identifier(target,'workspace team alias');
80
+ if(Object.hasOwn(aliases,source) && aliases[source]!==target) fail('requirement-conflict','adoption team aliases have conflicting mappings');
81
+ aliases[source]=target;aliasOrigins[source]=originAt(item,`/teamAliases/${pointerKey(source)}`,'import-adoption');
82
+ }
83
+ } else {
84
+ const bindings=item.value.bindings;if(bindings===undefined) continue;
85
+ if(!obj(bindings)) fail('invalid-binding','operator bindings must be an object');
86
+ if(Object.hasOwn(bindings,'responsibleHuman')) candidates.push({key:RESPONSIBLE_HUMAN_KEY,kind:'operator',value:humanRef(bindings.responsibleHuman),origin:originAt(item,'/bindings/responsibleHuman','operator')});
87
+ if(Object.hasOwn(bindings,'privateTeam')) candidates.push({key:PRIVATE_TEAM_KEY,kind:'operator',value:teamRef(bindings.privateTeam),origin:originAt(item,'/bindings/privateTeam','operator')});
88
+ if(Object.hasOwn(bindings,'wider')) {
89
+ if(!Array.isArray(bindings.wider) || new Set(bindings.wider).size!==bindings.wider.length) fail('invalid-binding','operator wider consent must be a unique array');
90
+ candidates.push({key:WIDER_KEY,kind:'operator',value:bindings.wider.map(alias=>identifier(alias,'wider team alias')),origin:originAt(item,'/bindings/wider','operator')});
91
+ }
92
+ }
93
+ }
94
+ if(!soul) fail('needs-configuration','messaging binding needs one soul declaration');
95
+ if(exact.kind==='workspace' && !privatePolicy) fail('needs-configuration','messaging workspace must declare private: per-human');
96
+ const rootOrigin=originAt(soul,'','soul-requirement');
97
+ requirements.push({key:RESPONSIBLE_HUMAN_KEY,kind:'required',origin:rootOrigin},{key:WIDER_KEY,kind:'required',origin:rootOrigin});
98
+ return {requirements,candidates,model:{contract:MESSAGING_CONTRACT,version:MESSAGING_CONTRACT_VERSION,context:exact,contextOrigin,requested,requestedOrigins,aliases,aliasOrigins,teams,privatePolicy,privatePolicyOrigin}};
99
+ }
100
+ function selected(choices,key,{optional=false}={}) {
101
+ const choice=choices?.[key];if(!obj(choice) || !Object.hasOwn(choice,'value') || choice.value===null) {if(optional)return null;fail('needs-configuration',key===RESPONSIBLE_HUMAN_KEY?'an explicit responsible-human binding is required':key===WIDER_KEY?'an explicit wider-membership consent list is required':'a selected wider-team binding is required');}return choice;
102
+ }
103
+
104
+ export function bindMessagingDomain({model,choices}={}) {
105
+ if(!obj(model) || model.contract!==MESSAGING_CONTRACT || model.version!==MESSAGING_CONTRACT_VERSION) fail('invalid-binding','invalid normalized messaging model');
106
+ const humanChoice=selected(choices,RESPONSIBLE_HUMAN_KEY),widerChoice=selected(choices,WIDER_KEY),human=humanRef(humanChoice.value);
107
+ if(!Array.isArray(widerChoice.value) || new Set(widerChoice.value).size!==widerChoice.value.length) fail('invalid-binding','resolved wider consent must be a unique array');
108
+ const wider=[],provenance=[humanChoice.selectedBy,widerChoice.selectedBy,model.contextOrigin,model.privatePolicyOrigin].filter(obj).map(clone);
109
+ for(const sourceAlias of widerChoice.value) {
110
+ identifier(sourceAlias,'wider team alias');if(!model.requested.includes(sourceAlias)) fail('invalid-binding',`wider consent names undeclared soul alias: ${sourceAlias}`);
111
+ const workspaceAlias=model.aliases[sourceAlias] ?? sourceAlias,key=model.teams[workspaceAlias];if(!key) fail('needs-configuration','a selected wider alias needs an explicit workspace team mapping');
112
+ const choice=selected(choices,key),ref=teamRef(choice.value);wider.push(ref);if(obj(choice.selectedBy)) provenance.push(clone(choice.selectedBy));
113
+ if(obj(model.requestedOrigins[sourceAlias])) provenance.push(clone(model.requestedOrigins[sourceAlias]));
114
+ if(obj(model.aliasOrigins[sourceAlias])) provenance.push(clone(model.aliasOrigins[sourceAlias]));
115
+ }
116
+ wider.sort((a,b)=>Buffer.compare(Buffer.from(canonical(a)),Buffer.from(canonical(b))));
117
+ for(let index=1;index<wider.length;index++) if(same(wider[index-1],wider[index])) fail('invalid-binding','duplicate resolved wider team');
118
+ const privateChoice=selected(choices,PRIVATE_TEAM_KEY,{optional:true}),privateTeam=privateChoice?teamRef(privateChoice.value):null;if(privateTeam && wider.some(item=>same(item,privateTeam))) fail('invalid-binding','private team cannot also be a wider team');if(obj(privateChoice?.selectedBy)) provenance.push(clone(privateChoice.selectedBy));
119
+ const unique=[];for(const item of provenance) if(!unique.some(prior=>same(prior,item))) unique.push(item);
120
+ const privateKey={provider:CAPABILITY,human,context:clone(model.context)};
121
+ return {payloadContract:MESSAGING_CONTRACT,payloadVersion:MESSAGING_CONTRACT_VERSION,payload:{responsibleHuman:human,context:clone(model.context),privateTeam,wider},credentialRefs:{},provenance:unique,
122
+ messagingChoice:{schemaVersion:1,enabled:true,privateKey,wider:clone(wider),provenance:clone(unique)}};
123
+ }
124
+
125
+ export function validateAwebBinding(binding) {
126
+ keys(binding,['schemaVersion','capability','payloadContract','payloadVersion','payload','credentialRefs','provenance'],['schemaVersion','capability','payloadContract','payloadVersion','payload','credentialRefs','provenance'],'aweb provider binding');
127
+ if(binding.schemaVersion!==1 || binding.capability!==CAPABILITY || binding.payloadContract!==MESSAGING_CONTRACT || binding.payloadVersion!==MESSAGING_CONTRACT_VERSION || !obj(binding.payload) || !obj(binding.credentialRefs) || !Array.isArray(binding.provenance)) fail('invalid-binding','invalid aweb provider binding');
128
+ keys(binding.payload,['responsibleHuman','context','privateTeam','wider'],['responsibleHuman','context','privateTeam','wider'],'aweb binding payload');
129
+ humanRef(binding.payload.responsibleHuman);boundContext(binding.payload.context);if(!Array.isArray(binding.payload.wider)) fail('invalid-binding','aweb wider teams must be an array');
130
+ const wider=binding.payload.wider.map(teamRef);for(let index=1;index<wider.length;index++) {if(same(wider[index-1],wider[index])) fail('invalid-binding','duplicate aweb wider team');if(Buffer.compare(Buffer.from(canonical(wider[index-1])),Buffer.from(canonical(wider[index])))>0) fail('invalid-binding','aweb wider teams are not canonical');}
131
+ if(binding.payload.privateTeam!==null) {
132
+ const privateTeam=teamRef(binding.payload.privateTeam);
133
+ if(wider.some(team=>same(team,privateTeam))) fail('invalid-binding','private team cannot also be a wider team');
134
+ }
135
+ if(Object.keys(binding.credentialRefs).length) fail('invalid-binding','no credential reference shape is supported by this binding version');
136
+ return binding;
137
+ }
138
+
139
+ export function validateAwebInvocationContext(value,binding,expected={}) {
140
+ validateAwebBinding(binding);
141
+ validateInvocationShape(value,{...expected,capability:CAPABILITY});
142
+ const selected=value.context.kind==='workspace'?{kind:'workspace',identity:value.context.identity}:value.context;
143
+ if(value.messagingChoice.enabled!==true || value.messagingChoice.privateKey.provider!==CAPABILITY || !sameInvocationJson(selected,binding.payload.context)
144
+ || !sameInvocationJson(value.responsibleHuman,binding.payload.responsibleHuman) || !sameInvocationJson(value.messagingChoice.wider,binding.payload.wider)) fail('invalid-binding','invocation contradicts messaging binding');
145
+ return value;
146
+ }
@@ -0,0 +1,56 @@
1
+ // Capability-owned operational readiness for the existing HOME delivery route.
2
+ // This is not human-account delegation, private-grant proof or model completion.
3
+ import {execFileSync} from 'node:child_process';
4
+ import {isAbsolute,resolve} from 'node:path';
5
+ import {BINDING_WIRE_LIMITS,parseBindingJson} from './binding-wire.mjs';
6
+ import {sameInvocationJson as same} from './invocation-shape.mjs';
7
+ import {validateAwebBinding,validateAwebInvocationContext} from './portable-binding.mjs';
8
+ // 0.24.1 supplies HOME custody; 0.24.2 also ships the required public projection
9
+ // and same-kernel codec CLI locator. Source presence is not a released version.
10
+ export const HOME_ROUTE_KERNEL_FLOOR='0.24.2';
11
+ const obj=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
12
+ const missing=message=>({status:'needs-configuration',problems:[{code:'needs-configuration',message}]});
13
+ const error=()=>{throw Object.assign(new Error('selected kernel observation unavailable'),{code:'provider-unavailable'});};
14
+ /** Preserve native HOME/profile/auth/Git; remove only the invoking instance's
15
+ * OATS selectors so a read-only query cannot accidentally target its parent. */
16
+ export function kernelQueryEnvironment(inherited){
17
+ const env={...inherited};
18
+ for(const key of Object.keys(env))if(/^(OATS_(?!HOME_DIR$)|OAS_|PI_AGENT_)/.test(key)||key==='PI_AGENTS_ROOT')delete env[key];
19
+ return env;
20
+ }
21
+ export function kernelMeetsHomeRouteFloor(version){
22
+ if(typeof version!=='string'||!/^\d+\.\d+\.\d+$/.test(version))return false;
23
+ const parts=version.split('.').map(Number),floor=HOME_ROUTE_KERNEL_FLOOR.split('.').map(Number);if(!parts.every(Number.isSafeInteger))return false;
24
+ for(let i=0;i<3;i++)if(parts[i]!==floor[i])return parts[i]>floor[i];
25
+ return true;
26
+ }
27
+ /** Use the caller-owned CLI location, never search PATH or import private kernel
28
+ * indexes. Binding execution must receive the same OATS_CLI_BIN as hooks. */
29
+ export function querySelectedKernel(args,{env=process.env}={}){
30
+ const cli=env.OATS_CLI_BIN;
31
+ if(typeof cli!=='string'||!isAbsolute(cli)||resolve(cli)!==cli||cli.includes('\0'))error();
32
+ let bytes;
33
+ try{bytes=execFileSync(process.execPath,[cli,...args],{env:kernelQueryEnvironment(env),timeout:10000,maxBuffer:BINDING_WIRE_LIMITS.bytes,stdio:['ignore','pipe','pipe']});}catch{error();}
34
+ try{return parseBindingJson(bytes,BINDING_WIRE_LIMITS);}catch{error();}
35
+ }
36
+ /** Reads only public version/retained selection. A missing profile projection
37
+ * stays explicit; no source default, mutable home label or runtime guess. */
38
+ export function assessCapturedSessionReadiness({binding,invocation,settings},{query=querySelectedKernel,env=process.env}={}){
39
+ if(!binding||!invocation)return missing('selected binding and inline captured invocation are required');
40
+ validateAwebBinding(binding);validateAwebInvocationContext(invocation,binding);
41
+ if(!invocation.instance)return missing('an explicit captured instance home is required');
42
+ if(binding.payload.privateTeam===null)return missing('an explicit private-team binding is required');
43
+ if(settings.delivery!=='session')return missing('captured messaging requires explicit delivery: session');
44
+ if(binding.payload.wider.length)return missing('selected wider memberships need their explicitly qualified native setup; they were not omitted');
45
+ let version,observed;
46
+ try{version=query(['--version','--json'],{env});}catch{return missing('caller-owned OATS_CLI_BIN and readable kernel version are required');}
47
+ if(version?.schemaVersion!==1||version?.name!=='@awebai/oats'||!kernelMeetsHomeRouteFloor(version.version))return missing('oats >=0.24.2 is required for captured HOME custody and retained runtime inspection');
48
+ const b=invocation.executionBinding;
49
+ try{observed=query(['inspect','--deployment',b.deployment,'--resolution',b.resolution.id,'--json'],{env});}catch{return missing('the exact retained runtime profile must be readable');}
50
+ if(observed?.schemaVersion!==1||observed?.ok!==true||!same(observed.result?.resolution,b.resolution))return missing('the kernel must report the exact retained resolution');
51
+ const launch=observed.result.launchSelection;
52
+ if(!obj(launch)||Object.keys(launch).some(k=>!['runtime','model'].includes(k))||!Object.hasOwn(launch,'model')||(launch.model!==null&&(typeof launch.model!=='string'||!launch.model.trim())))return missing('a retained launchSelection runtime/model observation is required');
53
+ if(launch.runtime==='pi')return missing('Pi strict print does not support session input; retain messaging and configure an input-capable profile');
54
+ if(!['claude','codex'].includes(launch.runtime))return missing('a supported input-capable ordinary Claude/Codex profile is required');
55
+ return {status:'ready',problems:[]};
56
+ }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "capability": "oats.aweb",
3
3
  "command": "aweb",
4
- "version": "1.10.3",
4
+ "version": "1.11.0",
5
5
  "compatibility": {
6
- "oats": ">=0.22.3"
6
+ "oats": ">=0.24.2"
7
7
  },
8
8
  "layer": "messaging",
9
9
  "description": "Messaging layer via aweb: per-instance team identities + native aw mail/chat skills + cross-machine team roster.",
@@ -64,7 +64,16 @@
64
64
  "inject": "injects/aweb.md",
65
65
  "commands": {
66
66
  "roster": "bin/oats-aweb.mjs roster",
67
- "setup": "bin/oats-aweb.mjs setup"
67
+ "setup": "bin/oats-aweb.mjs setup",
68
+ "binding-normalize": "bin/oats-aweb-binding.mjs normalize",
69
+ "binding-bind": "bin/oats-aweb-binding.mjs bind",
70
+ "binding-check": "bin/oats-aweb-binding.mjs check"
71
+ },
72
+ "binding": {
73
+ "version": 1,
74
+ "normalize": "binding-normalize",
75
+ "bind": "binding-bind",
76
+ "check": "binding-check"
68
77
  },
69
78
  "hooks": {
70
79
  "spawn": {