@awebai/oats 0.30.0 → 0.30.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.
Files changed (128) hide show
  1. package/bin/oats.mjs +1 -1
  2. package/docs/capabilities.md +3 -3
  3. package/docs/design/2026-09-23-workspace-module-contracts.md +2 -1
  4. package/docs/desktop-cli-api.md +23 -11
  5. package/docs/first-team.md +1 -1
  6. package/docs/implementation.md +2 -1
  7. package/docs/integrations.md +1 -1
  8. package/docs/knowledge-capability-authoring.md +1 -1
  9. package/docs/knowledge.md +4 -4
  10. package/docs/official-catalog.md +4 -4
  11. package/docs/packages.md +13 -13
  12. package/docs/plans/0.30-close-out.md +24 -2
  13. package/docs/release-lane.md +7 -2
  14. package/docs/release-notes/v0.30.1.md +123 -0
  15. package/docs/release-notes/v0.30.2.md +85 -0
  16. package/docs/souls-and-instances.md +6 -5
  17. package/docs/workspaces.md +7 -2
  18. package/lib/core.mjs +42 -28
  19. package/lib/instance-inspect.mjs +1 -1
  20. package/lib/instance-resolution.mjs +15 -8
  21. package/lib/materialize.mjs +33 -19
  22. package/lib/packages.mjs +1 -1
  23. package/lib/resolve.mjs +1 -1
  24. package/package-catalog.json +3 -3
  25. package/package.json +1 -3
  26. package/skills/oats-getting-started/SKILL.md +2 -2
  27. package/capabilities/oats-authoring/LICENSE +0 -21
  28. package/capabilities/oats-authoring/oats-package.json +0 -11
  29. package/capabilities/oats-authoring/oats.json +0 -12
  30. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +0 -84
  31. package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +0 -109
  32. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +0 -116
  33. package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +0 -11
  34. package/capabilities/oats-aweb/bin/oats-aweb.mjs +0 -1672
  35. package/capabilities/oats-aweb/injects/aweb.md +0 -47
  36. package/capabilities/oats-aweb/lib/binding-wire.mjs +0 -365
  37. package/capabilities/oats-aweb/lib/captured-execution.mjs +0 -91
  38. package/capabilities/oats-aweb/lib/captured-native.mjs +0 -91
  39. package/capabilities/oats-aweb/lib/grant-custody.mjs +0 -38
  40. package/capabilities/oats-aweb/lib/invocation-shape.mjs +0 -135
  41. package/capabilities/oats-aweb/lib/portable-binding.mjs +0 -146
  42. package/capabilities/oats-aweb/lib/session-readiness.mjs +0 -56
  43. package/capabilities/oats-aweb/lib/wake-receive.mjs +0 -56
  44. package/capabilities/oats-aweb/oats.json +0 -201
  45. package/capabilities/oats-aweb/skills/LICENSE +0 -21
  46. package/capabilities/oats-aweb/skills/VENDORED.md +0 -31
  47. package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +0 -201
  48. package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +0 -161
  49. package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +0 -61
  50. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +0 -116
  51. package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +0 -74
  52. package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +0 -286
  53. package/capabilities/oats-code-review/injects/reviewer.md +0 -26
  54. package/capabilities/oats-code-review/oats.json +0 -16
  55. package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +0 -66
  56. package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +0 -30
  57. package/capabilities/oats-code-review/skills/security-review/SKILL.md +0 -56
  58. package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +0 -34
  59. package/capabilities/oats-developer/injects/developer.md +0 -38
  60. package/capabilities/oats-developer/oats.json +0 -17
  61. package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +0 -43
  62. package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +0 -47
  63. package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +0 -65
  64. package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +0 -37
  65. package/capabilities/oats-developer/skills/worktrees/SKILL.md +0 -36
  66. package/capabilities/oats-engineering-expert/injects/expert.md +0 -37
  67. package/capabilities/oats-engineering-expert/oats.json +0 -17
  68. package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +0 -37
  69. package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +0 -52
  70. package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +0 -50
  71. package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +0 -53
  72. package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +0 -49
  73. package/capabilities/oats-jira/bin/oats-jira.mjs +0 -40
  74. package/capabilities/oats-jira/injects/jira.md +0 -10
  75. package/capabilities/oats-jira/oats.json +0 -22
  76. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +0 -179
  77. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +0 -34
  78. package/capabilities/oats-linear/bin/oats-linear.mjs +0 -344
  79. package/capabilities/oats-linear/injects/linear.md +0 -8
  80. package/capabilities/oats-linear/oats.json +0 -24
  81. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +0 -223
  82. package/capabilities/oats-okf/bin/oats-okf-binding.mjs +0 -14
  83. package/capabilities/oats-okf/bin/oats-okf.mjs +0 -213
  84. package/capabilities/oats-okf/injects/okf.md +0 -42
  85. package/capabilities/oats-okf/lib/binding-wire.mjs +0 -380
  86. package/capabilities/oats-okf/lib/captured-worker.mjs +0 -109
  87. package/capabilities/oats-okf/lib/config.mjs +0 -124
  88. package/capabilities/oats-okf/lib/consult.mjs +0 -518
  89. package/capabilities/oats-okf/lib/harvest-status.mjs +0 -88
  90. package/capabilities/oats-okf/lib/harvest-switch.mjs +0 -94
  91. package/capabilities/oats-okf/lib/inspection.mjs +0 -138
  92. package/capabilities/oats-okf/lib/invocation-context.mjs +0 -111
  93. package/capabilities/oats-okf/lib/invocation-shape.mjs +0 -135
  94. package/capabilities/oats-okf/lib/io.mjs +0 -118
  95. package/capabilities/oats-okf/lib/migration.mjs +0 -137
  96. package/capabilities/oats-okf/lib/okf-validate.mjs +0 -123
  97. package/capabilities/oats-okf/lib/portable-binding.mjs +0 -199
  98. package/capabilities/oats-okf/lib/source-contract.mjs +0 -46
  99. package/capabilities/oats-okf/lib/sources.mjs +0 -438
  100. package/capabilities/oats-okf/lib/stores.mjs +0 -473
  101. package/capabilities/oats-okf/lib/worker.mjs +0 -486
  102. package/capabilities/oats-okf/oats.json +0 -151
  103. package/capabilities/oats-okf/schemas/okf-base.schema.json +0 -46
  104. package/capabilities/oats-okf/schemas/okf-bindings.schema.json +0 -112
  105. package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +0 -87
  106. package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +0 -113
  107. package/capabilities/oats-okf/schemas/okf-soul.schema.json +0 -37
  108. package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +0 -144
  109. package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +0 -86
  110. package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +0 -104
  111. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +0 -140
  112. package/capabilities/oats-okf-harvest/injects/harvester.md +0 -12
  113. package/capabilities/oats-okf-harvest/oats.json +0 -26
  114. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +0 -168
  115. package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +0 -192
  116. package/capabilities/oats-okf-harvest/skills/okf-authoring/SKILL.md +0 -151
  117. package/capabilities/oats-okf-harvest/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  118. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +0 -170
  119. package/capabilities/oats-okf-maintenance/injects/maintainer.md +0 -12
  120. package/capabilities/oats-okf-maintenance/lib/provenance.mjs +0 -50
  121. package/capabilities/oats-okf-maintenance/oats.json +0 -21
  122. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +0 -159
  123. package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +0 -192
  124. package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +0 -151
  125. package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  126. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +0 -134
  127. package/capabilities/oats-workspace-experts/injects/oats-experts.md +0 -26
  128. package/capabilities/oats-workspace-experts/oats.json +0 -9
@@ -1,47 +0,0 @@
1
- ## Messaging: aweb
2
-
3
- Your messaging layer is **aweb**. Your identity's alias is your instance name,
4
- and it lives in the workspace's default team (the `Comms:` line of your TASK.md
5
- names it). Wider workspace teams are joined explicitly, each with its own
6
- identity home.
7
-
8
- **Run /oats-aweb before your first `aw mail`/`aw chat` of a session,
9
- whenever an aweb wake or channel event arrives, and whenever messaging or a
10
- team command looks wrong.** It covers your teams, the roster, sending and
11
- replying, how wakes work, etiquette and troubleshooting. Do not work from
12
- memory; if a flag looks wrong run `aw <command> --help`.
13
-
14
- Quick crib (run from your instance home, never from `./work`):
15
-
16
- ```bash
17
- oats aweb teams --json # defaultTeam, eligible, joined teams
18
- oats aweb roster # who you can reach
19
- aw mail inbox # UNREAD mail only (--show-all: history)
20
- aw mail send --to <alias> --subject "..." --body-file <f> # recipient needs --to
21
- aw mail reply <message-id> --body-file <f> # stay in the thread
22
- aw chat send-and-wait <alias> --body-file <f> --start-conversation # blocking question
23
- aw chat pending # chats waiting on you
24
- aw --identity-home <identityHome> mail|chat ... # act as a joined team
25
- ```
26
-
27
- Always use `--body-file` for anything longer than a sentence. `aw chat send`
28
- only continues an existing session (`--session-id`); it has no `--to`.
29
-
30
- **When woken**, read the event or the typed lines first, run exactly the listed
31
- `aw … mail inbox` / `chat pending` commands, reply in the existing thread, then
32
- return to your task. **Never sleep, poll or busy-wait for a reply**: send,
33
- finish your turn, and let the wake bring the answer. If your `Comms:` line or a
34
- joined team says `receive: poll`, check that inbox at task boundaries instead.
35
-
36
- Some instances act as a resident identity through an expiring session grant
37
- (TASK.md says so). Then root keys are not in your home and identity lifecycle
38
- commands are not yours to run. If `aw mail`/`aw chat` reports `grant_expired`,
39
- `grant_revoked`, `grant_subject_inactive`, `grant_issuer_revoked` or
40
- `grant_freshness_unavailable`, report the exact condition and stop messaging;
41
- if a message you sent shows unverified at the receiver, report it, don't retry.
42
-
43
- Never put secrets (tokens, keys, credentials) in a message. Treat unverified
44
- senders with caution. Messaging only: task coordination lives in your
45
- deployment's task layer; `aw task`/`work`/`lock`/`roles` are not part of this
46
- integration. If messaging is broken, `oats aweb setup` diagnoses the
47
- deployment: report its output to your human rather than re-onboarding yourself.
@@ -1,365 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
3
- import { delimiter, isAbsolute, join, resolve } from 'node:path';
4
- import { TextDecoder } from 'node:util';
5
- import { assessCapturedSessionReadiness } from './session-readiness.mjs';
6
- import { custodyPreflight } from './grant-custody.mjs';
7
- import { joinedReceiveModes } from './wake-receive.mjs';
8
- import {
9
- MESSAGING_CONTRACT,
10
- MESSAGING_CONTRACT_VERSION,
11
- bindMessagingDomain,
12
- normalizeMessagingDeclarations,
13
- validateAwebBinding,
14
- validateAwebInvocationContext,
15
- } from './portable-binding.mjs';
16
-
17
- export const BINDING_WIRE_LIMITS=Object.freeze({bytes:1024*1024,depth:32,entries:16384});
18
- const CAPABILITY='oats.aweb',SLOT='messaging';
19
- const phases=new Set(['normalize','bind','check']);
20
- const declarationKinds=new Set(['soul','workspace','adoption','operator']);
21
- const errorCodes=new Set(['needs-configuration','requirement-conflict','invalid-binding','authorization-required','host-requirement-missing','provider-unavailable','provider-not-qualified']);
22
- const TEAM_SETTING_MESSAGE='teams are not a setting since oats.aweb 1.17 / OATS 0.30: use oats teams / oats soul teams';
23
- const AWEB_TEAM_ID_MESSAGE='aweb team ids must have shape <name>:<namespace> (name matches ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$; namespace is a hostname)';
24
- const obj=value=>value!==null && typeof value==='object' && !Array.isArray(value);
25
- const wireError=(code,message=code)=>{throw Object.assign(new Error(message),{wireCode:code});};
26
- 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(',')}}`;
27
- const same=(a,b)=>canonical(a)===canonical(b);
28
- function keys(value,allowed,required) {
29
- if(!obj(value)) wireError('invalid-binding');
30
- for(const key of Object.keys(value)) if(!allowed.includes(key)) wireError('invalid-binding');
31
- for(const key of required) if(!Object.hasOwn(value,key)) wireError('invalid-binding');
32
- return value;
33
- }
34
- function pointer(value) {return typeof value==='string' && /^(?:\/(?:[^~]|~[01])*)+$/.test(value);}
35
- function bindingKey(value) {return pointer(value) && value.startsWith('/bindings/messaging/') && value.length>'/bindings/messaging/'.length;}
36
-
37
- class StrictJsonParser {
38
- constructor(text,{depth,entries}) {this.text=text;this.maxDepth=depth;this.maxEntries=entries;this.at=0;this.entries=0;}
39
- whitespace() {while(/[\u0009\u000a\u000d\u0020]/.test(this.text[this.at] || '')) this.at++;}
40
- count() {if(++this.entries>this.maxEntries) wireError('invalid-binding');}
41
- string() {
42
- if(this.text[this.at]!=='"') wireError('invalid-binding');
43
- const start=this.at++;
44
- while(this.at<this.text.length) {
45
- const code=this.text.charCodeAt(this.at++);
46
- if(code===0x22) {try{return JSON.parse(this.text.slice(start,this.at));}catch{wireError('invalid-binding');}}
47
- if(code<0x20) wireError('invalid-binding');
48
- if(code===0x5c) {
49
- const escaped=this.text[this.at++];
50
- if(escaped==='u') {if(!/^[0-9a-fA-F]{4}$/.test(this.text.slice(this.at,this.at+4))) wireError('invalid-binding');this.at+=4;}
51
- else if(!'"\\/bfnrt'.includes(escaped || '')) wireError('invalid-binding');
52
- }
53
- }
54
- wireError('invalid-binding');
55
- }
56
- value(depth=1) {
57
- this.whitespace();const char=this.text[this.at];
58
- if(char==='"') return this.string();
59
- if(char==='{') {
60
- if(depth>this.maxDepth) wireError('invalid-binding');this.at++;this.whitespace();
61
- const result=Object.create(null),seen=new Set();if(this.text[this.at]==='}') {this.at++;return result;}
62
- while(true) {
63
- this.whitespace();const key=this.string();if(seen.has(key)) wireError('invalid-binding');seen.add(key);this.count();
64
- this.whitespace();if(this.text[this.at++]!==':') wireError('invalid-binding');result[key]=this.value(depth+1);this.whitespace();
65
- const next=this.text[this.at++];if(next==='}') return result;if(next!==',') wireError('invalid-binding');
66
- }
67
- }
68
- if(char==='[') {
69
- if(depth>this.maxDepth) wireError('invalid-binding');this.at++;this.whitespace();
70
- const result=[];if(this.text[this.at]===']') {this.at++;return result;}
71
- while(true) {this.count();result.push(this.value(depth+1));this.whitespace();const next=this.text[this.at++];if(next===']') return result;if(next!==',') wireError('invalid-binding');}
72
- }
73
- for(const [token,value] of [['true',true],['false',false],['null',null]]) if(this.text.startsWith(token,this.at)) {this.at+=token.length;return value;}
74
- const match=/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(this.text.slice(this.at));
75
- if(!match) wireError('invalid-binding');this.at+=match[0].length;const number=Number(match[0]);if(!Number.isFinite(number)) wireError('invalid-binding');return number;
76
- }
77
- parse() {this.whitespace();const value=this.value();this.whitespace();if(this.at!==this.text.length) wireError('invalid-binding');return value;}
78
- }
79
-
80
- export function parseBindingJson(bytes,limits=BINDING_WIRE_LIMITS) {
81
- if(!Buffer.isBuffer(bytes)) bytes=Buffer.from(bytes);
82
- if(bytes.length>limits.bytes) wireError('invalid-binding');
83
- let text;try{text=new TextDecoder('utf-8',{fatal:true}).decode(bytes);}catch{wireError('invalid-binding');}
84
- return new StrictJsonParser(text,limits).parse();
85
- }
86
- function settings(value,{phase}={}) {
87
- if(!obj(value)) wireError('invalid-binding');
88
- if(Object.hasOwn(value,'team')) wireError('needs-configuration',TEAM_SETTING_MESSAGE);
89
- if(phase!=='check' && Object.hasOwn(value,'identity')) wireError('provider-not-qualified');
90
- keys(value,phase==='check'?['delivery','root','roots','identity','residents','join']:['delivery','root','roots','join'],[]);
91
- if(value.delivery!==undefined && !['channel','session'].includes(value.delivery)) wireError('needs-configuration');
92
- if(value.root!==undefined && (typeof value.root!=='string' || !value.root.trim())) wireError('needs-configuration');
93
- if(value.join!==undefined && typeof value.join!=='string') wireError('needs-configuration');
94
- if(value.roots!==undefined && !obj(value.roots)) wireError('needs-configuration');
95
- if(obj(value.roots)) for(const [team,root] of Object.entries(value.roots)) if(!team || typeof root!=='string' || !root.trim()) wireError('needs-configuration');
96
- if(value.identity!==undefined && !obj(value.identity)) wireError('needs-configuration');
97
- if(value.residents!==undefined && !obj(value.residents)) wireError('needs-configuration');
98
- if(obj(value.residents)) for(const [name,custody] of Object.entries(value.residents)) if(!name || typeof custody!=='string' || !custody.trim()) wireError('needs-configuration');
99
- return value;
100
- }
101
- function request(value,phase) {
102
- keys(value,['schemaVersion','phase','slot','capability','settings','input'],['schemaVersion','phase','slot','capability','settings','input']);
103
- if(value.schemaVersion!==1 || value.phase!==phase || value.slot!==SLOT || value.capability!==CAPABILITY) wireError('invalid-binding');
104
- settings(value.settings,{phase});return value;
105
- }
106
- function declaration(value) {
107
- keys(value,['kind','value','origin','origins'],['kind','value','origin','origins']);
108
- if(!declarationKinds.has(value.kind) || !obj(value.value) || !obj(value.origin) || !obj(value.origins)) wireError('invalid-binding');
109
- return value;
110
- }
111
- function context(value,{request=false}={}) {
112
- if(value?.kind==='workspace') {
113
- keys(value,request?['kind','identity','observation']:['kind','identity'],request?['kind','identity','observation']:['kind','identity']);
114
- if(!obj(value.identity) || (request && !obj(value.observation))) wireError('invalid-binding');
115
- return {kind:'workspace',identity:value.identity};
116
- }
117
- if(value?.kind==='standalone') {
118
- keys(value,['kind','key'],['kind','key']);
119
- if(value.key!==null && (typeof value.key!=='string' || !value.key.trim())) wireError('invalid-binding');
120
- return {kind:'standalone',key:value.key};
121
- }
122
- wireError('invalid-binding');
123
- }
124
- function normalizePhase(req) {
125
- keys(req.input,['declarations','context'],['declarations','context']);
126
- if(!Array.isArray(req.input.declarations)) wireError('invalid-binding');
127
- const declarations=req.input.declarations.map(declaration);context(req.input.context,{request:true});
128
- return normalizeMessagingDeclarations({declarations,context:req.input.context});
129
- }
130
- function choiceMap(value,model) {
131
- if(!obj(value)) wireError('invalid-binding');
132
- const allowed=new Set(['/bindings/messaging/responsibleHuman','/bindings/messaging/privateTeam','/bindings/messaging/wider',...Object.values(model.teams)]);
133
- for(const [key,choice] of Object.entries(value)) {
134
- if(!bindingKey(key) || !allowed.has(key)) wireError('invalid-binding');
135
- keys(choice,['value','selectedBy','constraints','considered'],['value','selectedBy','constraints','considered']);
136
- if(!Array.isArray(choice.constraints) || !Array.isArray(choice.considered) || (choice.selectedBy!==null && !obj(choice.selectedBy))) wireError('invalid-binding');
137
- }
138
- return value;
139
- }
140
- function model(value) {
141
- keys(value,['contract','version','context','contextOrigin','requested','requestedOrigins','aliases','aliasOrigins','teams','privatePolicy','privatePolicyOrigin'],['contract','version','context','contextOrigin','requested','requestedOrigins','aliases','aliasOrigins','teams','privatePolicy','privatePolicyOrigin']);
142
- if(value.contract!==MESSAGING_CONTRACT || value.version!==MESSAGING_CONTRACT_VERSION || !Array.isArray(value.requested) || !obj(value.requestedOrigins) || !obj(value.aliases) || !obj(value.aliasOrigins) || !obj(value.teams) || typeof value.privatePolicy!=='boolean') wireError('invalid-binding');
143
- context(value.context);if(value.contextOrigin!==null && !obj(value.contextOrigin)) wireError('invalid-binding');if(value.privatePolicyOrigin!==null && !obj(value.privatePolicyOrigin)) wireError('invalid-binding');
144
- for(const key of Object.values(value.teams)) if(!bindingKey(key)) wireError('invalid-binding');
145
- return value;
146
- }
147
- function bindPhase(req) {
148
- keys(req.input,['model','choices','context'],['model','choices','context']);
149
- const normalized=model(req.input.model),selectedContext=context(req.input.context,{request:true});
150
- if(!same(selectedContext,normalized.context) || (selectedContext.kind==='workspace' && !same(req.input.context.observation,normalized.contextOrigin))) wireError('invalid-binding');
151
- return bindMessagingDomain({model:normalized,choices:choiceMap(req.input.choices,normalized)});
152
- }
153
- function binding(value) {
154
- keys(value,['schemaVersion','capability','payloadContract','payloadVersion','payload','credentialRefs','provenance'],['schemaVersion','capability','payloadContract','payloadVersion','payload','credentialRefs','provenance']);
155
- if(value.schemaVersion!==1 || value.capability!==CAPABILITY || value.payloadContract!==MESSAGING_CONTRACT || value.payloadVersion!==MESSAGING_CONTRACT_VERSION || !obj(value.payload) || !obj(value.credentialRefs) || Object.keys(value.credentialRefs).length || !Array.isArray(value.provenance)) wireError('invalid-binding');
156
- for(const origin of value.provenance) if(!obj(origin)) wireError('invalid-binding');
157
- return value;
158
- }
159
- function checkResult(message) {return {status:'needs-configuration',problems:[{code:'needs-configuration',message}]};}
160
- function checkProblems(problems) {return problems.length?{status:'needs-configuration',problems}:null;}
161
- function workspaceReadinessContext(value) {
162
- keys(value,['kind','workspace','deployment','soul','team','instance','home'],['kind','workspace','deployment','soul']);
163
- if(value.kind!=='workspace' || typeof value.workspace!=='string' || !value.workspace.trim() || typeof value.deployment!=='string' || !value.deployment.trim() || typeof value.soul!=='string' || !value.soul.trim()) wireError('invalid-binding');
164
- if(value.team!==null && value.team!==undefined && (typeof value.team!=='string' || !value.team.trim())) wireError('invalid-binding');
165
- if(value.instance!==null && value.instance!==undefined && (typeof value.instance!=='string' || !value.instance.trim())) wireError('invalid-binding');
166
- if(value.home!==null && value.home!==undefined && (typeof value.home!=='string' || !value.home.trim())) wireError('invalid-binding');
167
- return value;
168
- }
169
- function yamlScalar(text,key){const m=String(text).match(new RegExp(`^${key}:\\s*["']?([^"'\\n#]+)["']?\\s*$`,'m'));return m?m[1].trim():undefined;}
170
- export const AW_MIN = '1.36.13';
171
- const CLASSIC_REFUSAL = 'oats.aweb 1.14 needs OATS 0.26.0 or newer (workspace model); on an older kernel pin oats.aweb v1.13.x';
172
- function classicEnv(env=process.env) {return !!env.OATS_TEAM_SCOPE && !(env.OATS_WORKSPACE_KEY || env.OATS_WORKSPACE_NAME || env.OATS_DEFAULT_TEAM);}
173
- export function grantYamlCustodySocket(text) {
174
- const lines=String(text??'').split(/\r?\n/);let inCustody=false,baseIndent=0;
175
- for(const line of lines) {
176
- const custody=/^(\s*)custody:\s*(?:#.*)?$/.exec(line);if(custody){inCustody=true;baseIndent=custody[1].length;continue;}
177
- if(inCustody){const ind=/^(\s*)/.exec(line)?.[1].length||0;if(line.trim()&&ind<=baseIndent)inCustody=false;const socket=/^\s*socket_path:\s*["']?([^"'\n#]+)["']?\s*$/.exec(line);if(socket)return socket[1].trim();}
178
- }
179
- return undefined;
180
- }
181
- function newestGrantHome(home) {
182
- if(typeof home!=='string'||!home.trim()) return null;
183
- try {
184
- const dirs=readdirSync(home).filter((name)=>name==='.aweb-identity'||/^\.aweb-identity-\d+$/.test(name)).map((name)=>join(home,name)).filter((p)=>{try{return statSync(p).isDirectory();}catch{return false;}}).map((p)=>{try{return {path:p,mtime:statSync(p).mtimeMs};}catch{return {path:p,mtime:0};}}).sort((a,b)=>b.mtime-a.mtime||b.path.localeCompare(a.path));
185
- return dirs[0]?.path||null;
186
- } catch {return null;}
187
- }
188
- function grantAttachmentProblem(home) {
189
- const grantHome=newestGrantHome(home);
190
- if(!grantHome) return null;
191
- const grantYaml=join(grantHome,'grant.yaml');
192
- if(!existsSync(grantYaml)) return null;
193
- let text;try{text=readFileSync(grantYaml,'utf8');}catch{return null;}
194
- if(grantYamlCustodySocket(text)) return null;
195
- const id=yamlScalar(text,'grant_id')||'<unknown>';
196
- return {code:'custody',message:`grant ${id} is not attached to custody; retire and respawn on aw >= ${AW_MIN}`};
197
- }
198
- function activeTeamAt(root){try{const text=readFileSync(join(resolve(root),'.aw','teams.yaml'),'utf8');return yamlScalar(text,'active_team')||yamlScalar(text,'active');}catch{return undefined;}}
199
- function residentCustodyRoot(settings){const identity=obj(settings.identity)?settings.identity:{},residents=obj(settings.residents)?settings.residents:{};const name=typeof identity.resident==='string'?identity.resident:'';const root=name&&typeof residents[name]==='string'?residents[name]:undefined;return identity.mode==='global'&&root&&isAbsolute(root)?root:undefined;}
200
- function teamFromSettings(settings,candidate,{env=process.env}={}) {
201
- return typeof env.OATS_DEFAULT_TEAM_ID==='string' && env.OATS_DEFAULT_TEAM_ID.trim()?env.OATS_DEFAULT_TEAM_ID.trim():undefined;
202
- }
203
- function rootCandidate(settings,team,{deployment,env=process.env}={}) {
204
- const roots=obj(settings.roots)?settings.roots:{};
205
- if(team && typeof roots[team]==='string' && roots[team].trim()) return {root:roots[team].trim(),key:`settings.oats.aweb.roots[${JSON.stringify(team)}]`,declared:true};
206
- if(typeof settings.root==='string' && settings.root.trim()) return {root:settings.root.trim(),key:'settings.oats.aweb.root',declared:true};
207
- const candidates=[env.OATS_WORKSPACE || deployment || process.cwd()];
208
- for(const root of candidates) if(isAbsolute(root) && existsSync(join(resolve(root),'.aw'))) return {root,key:'settings.oats.aweb.root',declared:false};
209
- return {root:candidates[0] || process.cwd(),key:'settings.oats.aweb.root',declared:false};
210
- }
211
- function validHostname(value){const s=String(value||'');return s.length<=253&&s.split('.').every(label=>/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(label));}
212
- function validAwebTeamId(value){const m=/^([A-Za-z0-9][A-Za-z0-9._-]{0,127}):([^:]+)$/.exec(String(value||''));return !!m&&validHostname(m[2]);}
213
- function invalidAwebTeamId(env=process.env){const id=typeof env.OATS_DEFAULT_TEAM_ID==='string'&&env.OATS_DEFAULT_TEAM_ID.trim()?env.OATS_DEFAULT_TEAM_ID.trim():null;if(id&&!validAwebTeamId(id))return id;try{const rows=JSON.parse(env.OATS_TEAMS||'[]');if(Array.isArray(rows))for(const r of rows)if(r&&typeof r.team==='string'&&r.team.trim()&&!validAwebTeamId(r.team.trim()))return r.team.trim();}catch{}return null;}
214
- function parseOatsTeams(env=process.env){try{const rows=JSON.parse(env.OATS_TEAMS||'[]');return Array.isArray(rows)?rows.filter(r=>r&&typeof r==='object').map(r=>({label:String(r.label||''),team:typeof r.team==='string'?r.team:null,default:r.default===true,from:r.from==='shared'?'shared':'local'})):[];}catch{return [];}}
215
- function primaryTeamLabel(env=process.env){return typeof env.OATS_DEFAULT_TEAM==='string'&&env.OATS_DEFAULT_TEAM.trim()?env.OATS_DEFAULT_TEAM.trim():null;}
216
- function unmappedPrimary(env=process.env){return undefined;}
217
- function joinedTeams(home){if(!home)return[];try{const doc=JSON.parse(readFileSync(join(home,'.oats-aweb','teams.json'),'utf8'));return Array.isArray(doc.joinedTeams)?doc.joinedTeams.filter(j=>j&&typeof j==='object'&&j.label&&j.team&&j.identityHome):[];}catch{return[];}}
218
- function readinessDetails(settings,{deployment,env=process.env}={}) {
219
- if(classicEnv(env)) return {team:undefined,candidate:null,warnings:[],result:{status:'needs-configuration',problems:[{code:'needs-configuration',message:CLASSIC_REFUSAL}]}};
220
- const invalidTeam=invalidAwebTeamId(env);if(invalidTeam)return {team:undefined,candidate:null,warnings:[],result:{status:'needs-configuration',problems:[{code:'needs-configuration',message:AWEB_TEAM_ID_MESSAGE}]}};
221
- const team=teamFromSettings(settings,null,{env}),candidate=rootCandidate(settings,team,{deployment,env}),problems=[],warnings=[];
222
- const awProblem=awFloorProblem();if(awProblem) problems.push(awProblem);
223
- if(!team) {
224
- const label=typeof env.OATS_DEFAULT_TEAM==='string'&&env.OATS_DEFAULT_TEAM.trim()?env.OATS_DEFAULT_TEAM.trim():'';
225
- problems.push({code:'needs-configuration',message:label?`the default team ${label} has no provider id yet: its owner runs oats aweb setup, then commits the id, or choose another default with oats teams default`:'no teams configured: run `oats aweb setup`'});
226
- }
227
- if(!candidate.root || !isAbsolute(candidate.root) || !existsSync(join(resolve(candidate.root),'.aw'))) problems.push({code:'needs-configuration',message:`no messaging root at ${candidate.root?resolve(candidate.root):process.cwd()}: run oats aweb setup there or set ${candidate.key}`});
228
- return {team,candidate,warnings,result:checkProblems(problems) || {status:'ready',problems:[]}};
229
- }
230
- function readinessFromSettings(settings,options) {return readinessDetails(settings,options).result;}
231
- function runAw(argv,cwd,{unsetEnv=[],timeout=60000}={}) {
232
- // An inherited AWEB_IDENTITY_HOME is the caller's identity, never this check's.
233
- const env={...process.env};delete env.AWEB_IDENTITY_HOME;for(const name of unsetEnv) delete env[name];
234
- try {return execFileSync(argv[0],argv.slice(1),{cwd,env,encoding:'utf8',stdio:['ignore','pipe','pipe'],timeout}).trim();}
235
- catch(e) {throw new Error(`${argv.slice(0,3).join(' ')} failed${e.status===undefined?'':` (exit ${e.status})`}`);}
236
- }
237
- function semverLt(a,b) {const A=String(a||'0.0.0').split('.').map(n=>Number(n)||0),B=String(b).split('.').map(n=>Number(n)||0);for(let i=0;i<3;i++){if((A[i]||0)!==(B[i]||0)) return (A[i]||0)<(B[i]||0);}return false;}
238
- function onPath(cmd,env=process.env){for(const dir of String(env.PATH||'').split(delimiter)){if(!dir)continue;try{const st=statSync(join(dir,cmd));if(st.isFile()&&(st.mode&0o111))return true;}catch{}}return false;}
239
- function awVersionLabel(){try{const text=runAw(['aw','version'],process.cwd(),{timeout:10000});const m=/aw\s+v?(\d+\.\d+\.\d+)/.exec(text);return m?m[1]:undefined;}catch{return undefined;}}
240
- function awFloorProblem(){if(!onPath('aw'))return{code:'needs-configuration',message:`aw CLI not on PATH; install aw >= ${AW_MIN}`};const installed=awVersionLabel();if(!installed)return{code:'needs-configuration',message:`aw version could not be read; install aw >= ${AW_MIN}`};return !semverLt(installed,AW_MIN)?null:{code:'needs-configuration',message:`aw ${installed} is older than required ${AW_MIN}; install aw >= ${AW_MIN}`};}
241
- function wakeReadiness(home,{reliedOn=false}={}) {
242
- if(!home || !reliedOn) return {problems:[],warnings:[]};
243
- try {
244
- const doc=JSON.parse(runAw(['aw','wake','status','--json'],home,{timeout:10000}));
245
- const state=doc.daemon_version_state || (doc.daemon_running===false?'not_running':doc.daemon_version?'reported':'unknown');
246
- if(state==='reported') {
247
- const running=String(doc.daemon_version||'unknown');
248
- if(semverLt(running,AW_MIN)) return {problems:[{code:'wake-daemon-outdated',message:`host wake daemon is running ${running}; required ${AW_MIN}; upgrade aw, then restart the host wake daemon`}],warnings:[]};
249
- return {problems:[],warnings:[]};
250
- }
251
- if(state==='not_running') return {problems:[{code:'wake-daemon-not-running',message:'host wake daemon is not running; session delivery relies on it'}],warnings:[]};
252
- return {problems:[],warnings:[{code:'wake-daemon-version-unknown',message:`host wake daemon version is unknown; compatibility unproven; required ${AW_MIN}; upgrade aw, then restart the host wake daemon`}]};
253
- } catch {return {problems:[],warnings:[{code:'wake-daemon-version-unknown',message:`host wake daemon version is unknown; compatibility unproven; required ${AW_MIN}; upgrade aw, then restart the host wake daemon`}]};}
254
- }
255
- function workspaceReadinessPhase(req) {
256
- const ctx=workspaceReadinessContext(req.input.context);
257
- if(!obj(req.input.action) || req.input.action.kind!=='readiness') wireError('invalid-binding');
258
- const details=readinessDetails(req.settings,{deployment:ctx.deployment}),problems=[...details.result.problems],warnings=[...details.warnings];
259
- const identity=obj(req.settings.identity)?req.settings.identity:{},mode=identity.mode===undefined || identity.mode===null || identity.mode===''?'local':String(identity.mode);
260
- if(mode==='global') {
261
- const grantProblem=grantAttachmentProblem(ctx.home);if(grantProblem) problems.push(grantProblem);
262
- const resident=typeof identity.resident==='string' && identity.resident.trim()?identity.resident.trim():undefined;
263
- const residents=obj(req.settings.residents)?req.settings.residents:{};
264
- const custody=resident?residents[resident]:undefined;
265
- if(!resident) problems.push({code:'custody',message:'identity.mode "global" requires identity.resident; set oats-local.yaml settings.oats.aweb.residents.<name> to the absolute custody directory for that resident identity'});
266
- else if(typeof custody!=='string' || !isAbsolute(custody) || !existsSync(join(custody,'.aw','identity.yaml'))) problems.push({code:'custody',message:`identity.mode "global" resident ${JSON.stringify(resident)} is not resolvable; set oats-local.yaml settings.oats.aweb.residents.${resident} to an absolute custody directory whose .aw/identity.yaml exists`});
267
- else if(details.team) {
268
- try {
269
- const preflight=custodyPreflight({custody,resident,team:details.team,e2eeRequired:identity.e2ee!==false,fatalOnError:false,runAw:(argv,cwd,options={})=>runAw(argv,cwd,{...options,timeout:20000})});
270
- for(const message of preflight.warnings) warnings.push({code:'e2ee-disabled',message});
271
- }
272
- catch(e) {problems.push({code:'custody',message:e.message});}
273
- }
274
- }
275
- const joined=joinedTeams(ctx.home);
276
- if(joined.length) {
277
- let status;try{status=JSON.parse(runAw(['aw','wake','status','--json'],ctx.home,{timeout:10000}));}catch{status=undefined;}
278
- const why={'home-not-registered':'this home is not registered with the host wake broker','not-registered-with-broker':'its identity home is not registered with the host wake broker','wake-daemon-not-running':'the host wake daemon is not running','stream-not-admitted':'the host wake broker has not admitted its stream'};
279
- for(const mode of joinedReceiveModes(status,{home:ctx.home,joined})) {
280
- const row=joined.find(j=>j.label===mode.label);
281
- if(mode.receive==='native') warnings.push({code:'joined-team-receive',message:`joined team ${mode.label} receives native through the host wake broker (stream ${mode.phase})`});
282
- else warnings.push({code:'joined-team-poll-only',message:`joined team ${mode.label} receives by polling: ${status?why[mode.reason]||mode.reason:'aw wake status is unavailable'}${mode.detail?` (${mode.detail})`:''}; check aw --identity-home ${row.identityHome} mail inbox and chat pending at task boundaries`});
283
- }
284
- }
285
- const wake=String(req.settings.delivery||'channel')==='session'?wakeReadiness(ctx.home,{reliedOn:true}):{problems:[],warnings:[]};
286
- problems.push(...wake.problems);warnings.push(...wake.warnings);
287
- const result=checkProblems(problems) || {status:'ready',problems:[]};
288
- return {...result,warnings};
289
- }
290
- function checkPhase(req) {
291
- keys(req.input,['binding','context','action','invocation'],['context','action']);
292
- if(!obj(req.input.action) || typeof req.input.action.kind!=='string') wireError('invalid-binding');
293
- if(!Object.hasOwn(req.input,'binding')) return workspaceReadinessPhase(req);
294
- const current=validateAwebBinding(binding(req.input.binding)),selectedContext=context(req.input.context,{request:true});
295
- if(!same(selectedContext,current.payload.context)) wireError('invalid-binding');
296
- const invocation=Object.hasOwn(req.input,'invocation')?validateAwebInvocationContext(req.input.invocation,current,{context:req.input.context,action:req.input.action}):null;
297
- if(['command','hook','operation'].includes(req.input.action.kind) && (!invocation || invocation.instance===null || invocation.intent===null)) return checkResult('an admitted captured instance intent is required for execution');
298
- if(current.payload.privateTeam===null) return checkResult('an explicit private-team binding is required');
299
- const hostReady=readinessFromSettings(req.settings);
300
- if(hostReady.status!=='ready') return hostReady;
301
- if(!invocation) return hostReady;
302
- // Read-only public kernel observations, not an account/grant attestation.
303
- // Native setup is performed only by the separately admitted execution path.
304
- return assessCapturedSessionReadiness({binding:current,invocation,settings:req.settings});
305
- }
306
-
307
- export function handleBindingRequest(phase,value) {
308
- if(!phases.has(phase)) wireError('invalid-binding');
309
- const req=request(value,phase);
310
- if(phase==='normalize') return normalizePhase(req);
311
- if(phase==='bind') return bindPhase(req);
312
- return checkPhase(req);
313
- }
314
- function response(phase,body) {return {schemaVersion:1,phase,slot:SLOT,capability:CAPABILITY,...body};}
315
- function errorCode(error) {if(errorCodes.has(error?.wireCode)) return error.wireCode;if(errorCodes.has(error?.code)) return error.code;return 'invalid-binding';}
316
- // Only these literal domain diagnostics may cross the wire. Never reflect a
317
- // caught exception's dynamic alias/key/path, native stderr or credential text.
318
- const safeReasons=new Map([
319
- ['needs-configuration',[
320
- TEAM_SETTING_MESSAGE,
321
- AWEB_TEAM_ID_MESSAGE,
322
- 'messaging-enabled standalone preparation needs an explicit context key',
323
- 'messaging binding needs one soul declaration',
324
- 'messaging workspace must declare private: per-human',
325
- 'an explicit responsible-human binding is required',
326
- 'an explicit wider-membership consent list is required',
327
- 'a selected wider-team binding is required',
328
- 'a selected wider alias needs an explicit workspace team mapping',
329
- ]],
330
- ['requirement-conflict',['multiple soul messaging declarations','adoption team aliases have conflicting mappings']],
331
- ]);
332
- const fallbackReasons=Object.freeze({
333
- 'needs-configuration':'messaging settings and explicit binding selections are required',
334
- 'requirement-conflict':'messaging declarations contain incompatible requirements',
335
- 'invalid-binding':'messaging input must match the supported binding contract',
336
- 'authorization-required':'explicit native messaging authorization is required',
337
- 'host-requirement-missing':'a required native messaging host resource is unavailable',
338
- 'provider-unavailable':'the selected messaging provider is unavailable',
339
- 'provider-not-qualified':'the requested messaging configuration is not qualified',
340
- });
341
- function errorProblem(error) {
342
- const code=errorCode(error),message=safeReasons.get(code)?.find(literal=>literal===error?.message)??fallbackReasons[code];
343
- return {code,message};
344
- }
345
- function enforceOutputLimits(value,depth=1,state={entries:0}) {
346
- if(depth>BINDING_WIRE_LIMITS.depth) wireError('provider-not-qualified');
347
- if(value===null || typeof value!=='object') return;
348
- for(const child of Array.isArray(value)?value:Object.values(value)) {
349
- if(++state.entries>BINDING_WIRE_LIMITS.entries) wireError('provider-not-qualified');
350
- enforceOutputLimits(child,depth+1,state);
351
- }
352
- }
353
- export async function runBindingWire(phase,input=process.stdin,output=process.stdout) {
354
- let answer;
355
- try {
356
- const chunks=[];let length=0;
357
- for await(const chunk of input) {const bytes=Buffer.from(chunk);length+=bytes.length;if(length>BINDING_WIRE_LIMITS.bytes) wireError('invalid-binding');chunks.push(bytes);}
358
- const result=handleBindingRequest(phase,parseBindingJson(Buffer.concat(chunks,length)));
359
- answer=response(phase,{ok:true,result});
360
- } catch(error) {answer=response(phases.has(phase)?phase:'check',{ok:false,error:errorProblem(error)});}
361
- let bytes;
362
- try {enforceOutputLimits(answer);bytes=Buffer.from(JSON.stringify(answer)+'\n');if(bytes.length>BINDING_WIRE_LIMITS.bytes) wireError('provider-not-qualified');}
363
- catch {bytes=Buffer.from(JSON.stringify(response(phases.has(phase)?phase:'check',{ok:false,error:{code:'provider-not-qualified',message:'messaging response exceeds the supported wire limits'}}))+'\n');}
364
- output.write(bytes);return answer.ok;
365
- }
@@ -1,91 +0,0 @@
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
- }
@@ -1,91 +0,0 @@
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,63}$/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
- }