@a2a-exposed/encryption 0.0.0-stage → 0.8.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPENDENCIES.md +31 -0
- package/LICENSE +21 -0
- package/LOCAL-CUSTODY-DRAFT.md +263 -0
- package/README.md +135 -2
- package/assignment.mjs +68 -0
- package/backup.mjs +63 -0
- package/carriers.mjs +45 -0
- package/content.mjs +82 -0
- package/index.mjs +230 -0
- package/keyring-worker.mjs +19 -0
- package/ledger.mjs +330 -0
- package/local-state.mjs +809 -0
- package/native-store.mjs +55 -0
- package/package.json +57 -4
- package/schemas/assigned-carrier.schema.json +271 -0
- package/schemas/assignment.schema.json +73 -0
- package/schemas/capability.schema.json +84 -0
- package/schemas/content.schema.json +546 -0
- package/schemas/envelope.schema.json +188 -0
- package/schemas/message-carrier.schema.json +195 -0
- package/schemas/receipt-carrier.schema.json +197 -0
- package/schemas/task-carrier.schema.json +247 -0
- package/tasks.mjs +55 -0
- package/transport.mjs +96 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://a2a.exposed/ext/sealed/v1/task-carrier.schema.json",
|
|
4
|
+
"description": "Structural Task wrappers only. Runtime additionally checks raw duplicate keys, size, calendar validity, context/task and status/header agreement, conflicting duplicate IDs and immutable receipt equality. Schema and extraction do not authenticate content, array ordering or routing state.",
|
|
5
|
+
"oneOf": [
|
|
6
|
+
{
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"id": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"pattern": "^[!-~]{1,200}$"
|
|
12
|
+
},
|
|
13
|
+
"contextId": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"pattern": "^[!-~]{1,200}$"
|
|
16
|
+
},
|
|
17
|
+
"status": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"state": {
|
|
21
|
+
"enum": [
|
|
22
|
+
"submitted",
|
|
23
|
+
"working",
|
|
24
|
+
"input-required",
|
|
25
|
+
"completed",
|
|
26
|
+
"failed",
|
|
27
|
+
"canceled",
|
|
28
|
+
"rejected",
|
|
29
|
+
"auth-required"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"message": {
|
|
33
|
+
"allOf": [
|
|
34
|
+
{
|
|
35
|
+
"oneOf": [
|
|
36
|
+
{
|
|
37
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/message-carrier.schema.json#/oneOf/0"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/receipt-carrier.schema.json#/oneOf/0"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/assigned-carrier.schema.json#/oneOf/0"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"properties": {
|
|
49
|
+
"role": {
|
|
50
|
+
"const": "agent"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"timestamp": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"pattern": "^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d(?:\\.\\d{1,9})?Z$"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"required": [
|
|
62
|
+
"state"
|
|
63
|
+
],
|
|
64
|
+
"additionalProperties": false
|
|
65
|
+
},
|
|
66
|
+
"artifacts": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"maxItems": 0
|
|
69
|
+
},
|
|
70
|
+
"history": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"maxItems": 1000,
|
|
73
|
+
"items": {
|
|
74
|
+
"oneOf": [
|
|
75
|
+
{
|
|
76
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/message-carrier.schema.json#/oneOf/0"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/receipt-carrier.schema.json#/oneOf/0"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/assigned-carrier.schema.json#/oneOf/0"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"metadata": {
|
|
88
|
+
"oneOf": [
|
|
89
|
+
{
|
|
90
|
+
"type": "object",
|
|
91
|
+
"maxProperties": 0
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"type": "object",
|
|
95
|
+
"properties": {
|
|
96
|
+
"https://a2a.exposed/ext/sealed/v1": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"v": {
|
|
100
|
+
"const": 1
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"required": [
|
|
104
|
+
"v"
|
|
105
|
+
],
|
|
106
|
+
"additionalProperties": false
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"required": [
|
|
110
|
+
"https://a2a.exposed/ext/sealed/v1"
|
|
111
|
+
],
|
|
112
|
+
"additionalProperties": false
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
"kind": {
|
|
117
|
+
"const": "task"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"required": [
|
|
121
|
+
"id",
|
|
122
|
+
"contextId",
|
|
123
|
+
"status",
|
|
124
|
+
"kind"
|
|
125
|
+
],
|
|
126
|
+
"additionalProperties": false
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"id": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"pattern": "^[!-~]{1,200}$"
|
|
134
|
+
},
|
|
135
|
+
"contextId": {
|
|
136
|
+
"type": "string",
|
|
137
|
+
"pattern": "^[!-~]{1,200}$"
|
|
138
|
+
},
|
|
139
|
+
"status": {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"properties": {
|
|
142
|
+
"state": {
|
|
143
|
+
"enum": [
|
|
144
|
+
"TASK_STATE_SUBMITTED",
|
|
145
|
+
"TASK_STATE_WORKING",
|
|
146
|
+
"TASK_STATE_INPUT_REQUIRED",
|
|
147
|
+
"TASK_STATE_COMPLETED",
|
|
148
|
+
"TASK_STATE_FAILED",
|
|
149
|
+
"TASK_STATE_CANCELED",
|
|
150
|
+
"TASK_STATE_REJECTED",
|
|
151
|
+
"TASK_STATE_AUTH_REQUIRED"
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
"message": {
|
|
155
|
+
"allOf": [
|
|
156
|
+
{
|
|
157
|
+
"oneOf": [
|
|
158
|
+
{
|
|
159
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/message-carrier.schema.json#/oneOf/1"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/receipt-carrier.schema.json#/oneOf/1"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/assigned-carrier.schema.json#/oneOf/1"
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"properties": {
|
|
171
|
+
"role": {
|
|
172
|
+
"const": "ROLE_AGENT"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
"timestamp": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"pattern": "^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d(?:\\.\\d{1,9})?Z$"
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"required": [
|
|
184
|
+
"state"
|
|
185
|
+
],
|
|
186
|
+
"additionalProperties": false
|
|
187
|
+
},
|
|
188
|
+
"artifacts": {
|
|
189
|
+
"type": "array",
|
|
190
|
+
"maxItems": 0
|
|
191
|
+
},
|
|
192
|
+
"history": {
|
|
193
|
+
"type": "array",
|
|
194
|
+
"maxItems": 1000,
|
|
195
|
+
"items": {
|
|
196
|
+
"oneOf": [
|
|
197
|
+
{
|
|
198
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/message-carrier.schema.json#/oneOf/1"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/receipt-carrier.schema.json#/oneOf/1"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"$ref": "https://a2a.exposed/ext/sealed/v1/assigned-carrier.schema.json#/oneOf/1"
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
"metadata": {
|
|
210
|
+
"oneOf": [
|
|
211
|
+
{
|
|
212
|
+
"type": "object",
|
|
213
|
+
"maxProperties": 0
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"type": "object",
|
|
217
|
+
"properties": {
|
|
218
|
+
"https://a2a.exposed/ext/sealed/v1": {
|
|
219
|
+
"type": "object",
|
|
220
|
+
"properties": {
|
|
221
|
+
"v": {
|
|
222
|
+
"const": 1
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
"required": [
|
|
226
|
+
"v"
|
|
227
|
+
],
|
|
228
|
+
"additionalProperties": false
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
"required": [
|
|
232
|
+
"https://a2a.exposed/ext/sealed/v1"
|
|
233
|
+
],
|
|
234
|
+
"additionalProperties": false
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
"required": [
|
|
240
|
+
"id",
|
|
241
|
+
"contextId",
|
|
242
|
+
"status"
|
|
243
|
+
],
|
|
244
|
+
"additionalProperties": false
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
}
|
package/tasks.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Structural Task extraction only: routing state and array order never authenticate content.
|
|
2
|
+
import {EXTENSION,EncryptionError,canonicalJSON,parseJSON}from'./index.mjs';
|
|
3
|
+
import {readMessageCarrier,readReceiptCarrier,readAssignedCarrier}from'./carriers.mjs';
|
|
4
|
+
const fail=()=>{throw new EncryptionError('task_carrier_invalid');};
|
|
5
|
+
const object=value=>value&&typeof value==='object'&&!Array.isArray(value);
|
|
6
|
+
const fields=(value,required,optional=[])=>object(value)&&required.every(key=>Object.hasOwn(value,key))&&Object.keys(value).every(key=>required.includes(key)||optional.includes(key));
|
|
7
|
+
const id=value=>typeof value==='string'&&/^[\x21-\x7e]{1,200}$/.test(value);
|
|
8
|
+
const states=['submitted','working','input-required','completed','failed','canceled','rejected','auth-required'];
|
|
9
|
+
const wireState=(state,version)=>version==='0.3'?state:'TASK_STATE_'+state.toUpperCase().replaceAll('-','_');
|
|
10
|
+
function marker(value){return fields(value,[EXTENSION])&&fields(value[EXTENSION],['v'])&&value[EXTENSION].v===1;}
|
|
11
|
+
function event(value,version,taskId,contextId,current,routingState){
|
|
12
|
+
if(!object(value))fail();const raw=canonicalJSON(value);let envelope=null,receipt=null;
|
|
13
|
+
if(Object.hasOwn(value,'metadata')){
|
|
14
|
+
if(value.parts?.[0]?.data!==undefined)({envelope,receipt}=readAssignedCarrier(raw,version));
|
|
15
|
+
else receipt=readReceiptCarrier(raw,version);
|
|
16
|
+
}else envelope=readMessageCarrier(raw,version);
|
|
17
|
+
if(receipt&&(receipt.contextId!==contextId||receipt.taskId!==taskId))fail();
|
|
18
|
+
if(envelope){const h=envelope.header;
|
|
19
|
+
if(h.contextId!==contextId||(h.taskId===null?(current||h.role!=='user'||h.kind!=='message'):h.taskId!==taskId))fail();
|
|
20
|
+
if((current&&h.role!=='agent')||(h.role==='user'&&h.kind!=='message'))fail();
|
|
21
|
+
if(current&&h.kind==='status'&&h.state!==routingState)fail();
|
|
22
|
+
}
|
|
23
|
+
return {type:envelope?'envelope':'receipt',envelope,receipt};
|
|
24
|
+
}
|
|
25
|
+
export function readTaskCarrier(text,version){
|
|
26
|
+
try{
|
|
27
|
+
if(typeof text!=='string'||Buffer.byteLength(text)>1024*1024||!['0.3','1.0'].includes(version))fail();
|
|
28
|
+
const task=parseJSON(text);
|
|
29
|
+
if(!fields(task,['id','contextId','status',...(version==='0.3'?['kind']:[])],['artifacts','history','metadata'])||(version==='0.3'&&task.kind!=='task')||!id(task.id)||!id(task.contextId)||(Object.hasOwn(task,'artifacts')&&(!Array.isArray(task.artifacts)||task.artifacts.length!==0)))fail();
|
|
30
|
+
if(Object.hasOwn(task,'metadata')&&(!object(task.metadata)||(Object.keys(task.metadata).length!==0&&!marker(task.metadata))))fail();
|
|
31
|
+
if(!fields(task.status,['state'],['message','timestamp']))fail();
|
|
32
|
+
const routingState=states.find(state=>wireState(state,version)===task.status.state);if(!routingState)fail();
|
|
33
|
+
if(Object.hasOwn(task.status,'timestamp')){const value=task.status.timestamp;if(typeof value!=='string'||!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?Z$/.test(value)||(!Number.isFinite(Date.parse(value))||new Date(value).toISOString().slice(0,19)!==value.slice(0,19)))fail();}
|
|
34
|
+
if(Object.hasOwn(task,'history')&&(!Array.isArray(task.history)||task.history.length>1000))fail();
|
|
35
|
+
const history=(task.history||[]).map(value=>event(value,version,task.id,task.contextId,false,routingState));
|
|
36
|
+
const status=Object.hasOwn(task.status,'message')?event(task.status.message,version,task.id,task.contextId,true,routingState):null;
|
|
37
|
+
// Reject conflicting duplicates and immutable-receipt changes, without trusting array ordering.
|
|
38
|
+
const seen=new Map();let assignment=null;
|
|
39
|
+
for(const item of [...history,...(status?[status]:[])]){
|
|
40
|
+
const key=item.envelope?item.envelope.header.role+'\n'+item.envelope.header.messageId:'agent\n'+item.receipt.ackId,value=canonicalJSON(item.envelope||item.receipt);
|
|
41
|
+
if(seen.has(key)&&seen.get(key)!==value)fail();seen.set(key,value);
|
|
42
|
+
if(item.receipt){const encoded=canonicalJSON(item.receipt);if(assignment!==null&&assignment!==encoded)fail();assignment=encoded;}
|
|
43
|
+
}
|
|
44
|
+
return {taskId:task.id,contextId:task.contextId,routingState,status,history,authentication:'unverified',routingOnly:status?.envelope==null,historyOrder:'untrusted'};
|
|
45
|
+
}catch(error){if(error instanceof EncryptionError&&error.code==='task_carrier_invalid')throw error;fail();}
|
|
46
|
+
}
|
|
47
|
+
export function makeTaskCarrier(value,version){
|
|
48
|
+
try{
|
|
49
|
+
value=parseJSON(canonicalJSON(value));if(!fields(value,['taskId','contextId','routingState'],['statusMessage','history','metadata','timestamp']))fail();
|
|
50
|
+
const {taskId,contextId,routingState,statusMessage,history=[],metadata,timestamp}=value;
|
|
51
|
+
if(!states.includes(routingState)||!['0.3','1.0'].includes(version))fail();
|
|
52
|
+
const task={...(version==='0.3'?{kind:'task'}:{}),id:taskId,contextId,status:{state:wireState(routingState,version),...(statusMessage===undefined?{}:{message:statusMessage}),...(timestamp===undefined?{}:{timestamp})},artifacts:[],history,...(metadata===undefined?{}:{metadata})};
|
|
53
|
+
const raw=canonicalJSON(task);readTaskCarrier(raw,version);return parseJSON(raw);
|
|
54
|
+
}catch{fail();}
|
|
55
|
+
}
|
package/transport.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Privileged sealed-only JSON-RPC adapter. No CLI route or Worker capability is enabled here.
|
|
2
|
+
import{randomUUID}from'node:crypto';
|
|
3
|
+
import{DeliveryLedger}from'./ledger.mjs';import{LocalStateError}from'./local-state.mjs';
|
|
4
|
+
import{EXTENSION,EncryptionError,canonicalJSON,parseJSON}from'./index.mjs';import{makeMessageCarrier}from'./carriers.mjs';import{readTaskCarrier}from'./tasks.mjs';
|
|
5
|
+
class TransportError extends Error{constructor(code){super(code);this.code=code;}}
|
|
6
|
+
const fail=code=>{throw new TransportError(code);};
|
|
7
|
+
const clock=v=>Number.isSafeInteger(v)&&v>=0&&v<=8640000000000000;
|
|
8
|
+
const id=v=>typeof v==='string'&&/^[\x21-\x7e]{1,200}$/.test(v);
|
|
9
|
+
const exact=(v,keys)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).sort().join('\0')===[...keys].sort().join('\0');
|
|
10
|
+
const sealedSet=value=>[...new Set((value||'').split(',').map(v=>v.trim()).filter(v=>v.startsWith('https://a2a.exposed/ext/sealed/')))].sort();
|
|
11
|
+
function echo(headers,protocol){
|
|
12
|
+
const standard=headers.get('A2A-Extensions'),legacy=headers.get('X-A2A-Extensions'),wanted=canonicalJSON([EXTENSION]);
|
|
13
|
+
if(standard===null||(protocol==='0.3'&&legacy===null))fail('extension_not_confirmed');
|
|
14
|
+
if(standard!==null&&canonicalJSON(sealedSet(standard))!==wanted)fail('extension_not_confirmed');
|
|
15
|
+
if(legacy!==null&&canonicalJSON(sealedSet(legacy))!==wanted)fail('extension_not_confirmed');
|
|
16
|
+
}
|
|
17
|
+
async function text(response){
|
|
18
|
+
const length=response.headers.get('content-length');if(length!==null&&(!/^\d+$/.test(length)||Number(length)>1024*1024))fail('response_too_large');
|
|
19
|
+
const reader=response.body?.getReader();if(!reader)fail('response_invalid');let size=0;const bytes=new Uint8Array(1024*1024);
|
|
20
|
+
try{for(;;){const {done,value}=await reader.read();if(done)break;if(!(value instanceof Uint8Array))fail('response_invalid');if(size+value.byteLength>bytes.byteLength)fail('response_too_large');bytes.set(value,size);size+=value.byteLength;}return new TextDecoder('utf-8',{fatal:true}).decode(bytes.subarray(0,size));}
|
|
21
|
+
finally{try{await reader.cancel();}catch{}reader.releaseLock();}
|
|
22
|
+
}
|
|
23
|
+
export class SealedJsonRpcClient{
|
|
24
|
+
constructor({ledger,localId,peer,protocol,card,getPairing,withPairingLease,fetchImpl=globalThis.fetch,timeoutMs=15000}={}){
|
|
25
|
+
if(!(ledger instanceof DeliveryLedger)||typeof localId!=='string'||!(/^[a-f0-9-]{36}$/).test(localId)||typeof peer!=='string'||!(/^[A-Za-z0-9_-]{1,80}$/).test(peer)||!['0.3','1.0'].includes(protocol)||typeof getPairing!=='function'||typeof withPairingLease!=='function'||typeof fetchImpl!=='function'||!Number.isSafeInteger(timeoutMs)||timeoutMs<1||timeoutMs>60000)fail('transport_options');
|
|
26
|
+
this.ledger=ledger;this.localId=localId;this.peer=peer;this.protocol=protocol;this.card=parseJSON(canonicalJSON(card));this.getPairing=getPairing;this.withPairingLease=withPairingLease;this.fetch=fetchImpl;this.timeoutMs=timeoutMs;const freeze=value=>{if(value&&typeof value==='object'){for(const child of Object.values(value))freeze(child);Object.freeze(value);}};freeze(this.card);Object.freeze(this);
|
|
27
|
+
}
|
|
28
|
+
#binding(){
|
|
29
|
+
// Only the trusted local host may supply this fresh saved-grant/credential snapshot. Never take it from a peer body.
|
|
30
|
+
const pairing=parseJSON(canonicalJSON(this.getPairing()));
|
|
31
|
+
if(!exact(pairing,['identity','endpoint','token','verified','revision'])||!Number.isSafeInteger(pairing.revision)||pairing.revision<0||pairing.verified!==true||typeof pairing.token!=='string'||!(/^[\x21-\x7e]{1,4096}$/).test(pairing.token))fail('pairing_unverified');
|
|
32
|
+
const scope={localId:this.localId,peer:this.peer,pairingIdentity:pairing.identity,pairingVerified:true},pin=this.ledger.resolve(this.localId,this.peer,scope);
|
|
33
|
+
let url;try{url=new URL(pairing.endpoint);}catch{fail('endpoint_binding_changed');}
|
|
34
|
+
if(url.protocol!=='https:'||url.href!==pairing.endpoint||url.username||url.password||url.search||url.hash||url.origin!==new URL(pin.remote.cardUrl).origin||pairing.endpoint!==pin.record.peerEndpoint)fail('endpoint_binding_changed');
|
|
35
|
+
const extensions=this.card?.capabilities?.extensions,sealed=Array.isArray(extensions)?extensions.filter(e=>typeof e?.uri==='string'&&e.uri.startsWith('https://a2a.exposed/ext/sealed/')):[];
|
|
36
|
+
if(sealed.length!==1||!exact(sealed[0],['uri','required','params',...(Object.hasOwn(sealed[0],'description')?['description']:[])])||(Object.hasOwn(sealed[0],'description')&&(typeof sealed[0].description!=='string'||sealed[0].description.length>4096))||sealed[0].uri!==EXTENSION||sealed[0].required!==false||!exact(sealed[0].params,['bundle','responseKind'])||sealed[0].params.responseKind!=='task'||canonicalJSON(sealed[0].params.bundle)!==canonicalJSON(pin.remote))fail('capability_unconfirmed');
|
|
37
|
+
const interfaces=this.card.supportedInterfaces;
|
|
38
|
+
if(interfaces!==undefined&&!Array.isArray(interfaces))fail('interface_routing_unsupported');
|
|
39
|
+
const selected=(interfaces||[]).filter(i=>i?.url===pairing.endpoint&&i.protocolVersion===this.protocol),rpc=selected.filter(i=>i.protocolBinding==='JSONRPC');
|
|
40
|
+
if(rpc.length){for(const i of rpc)if(!exact(i,['url','protocolVersion','protocolBinding',...(Object.hasOwn(i,'tenant')?['tenant']:[])])||(Object.hasOwn(i,'tenant')&&i.tenant!==''))fail('interface_routing_unsupported');}
|
|
41
|
+
else{
|
|
42
|
+
if(selected.length||this.protocol!=='0.3'||(this.card.protocolVersion!==undefined&&!['0.3','0.3.0'].includes(this.card.protocolVersion))||((interfaces||[]).length&&this.card.protocolVersion===undefined))fail('interface_routing_unsupported');
|
|
43
|
+
const additional=this.card.additionalInterfaces;if(additional!==undefined&&!Array.isArray(additional))fail('interface_routing_unsupported');
|
|
44
|
+
const extra=(additional||[]).filter(i=>i?.url===pairing.endpoint&&i.transport==='JSONRPC');
|
|
45
|
+
if(extra.some(i=>!exact(i,['url','transport'])))fail('interface_routing_unsupported');
|
|
46
|
+
const main=this.card.url===pairing.endpoint&&(this.card.preferredTransport===undefined||this.card.preferredTransport==='JSONRPC');
|
|
47
|
+
if(!extra.length&&!main)fail('interface_routing_unsupported');
|
|
48
|
+
if(Object.hasOwn(this.card,'tenant')&&this.card.tenant!=='')fail('interface_routing_unsupported');
|
|
49
|
+
}
|
|
50
|
+
return{scope,pin,pairing};
|
|
51
|
+
}
|
|
52
|
+
#recheck(original){const current=this.#binding();if(current.pin.digest!==original.pin.digest||canonicalJSON(current.pairing)!==canonicalJSON(original.pairing))fail('pairing_changed');return current;}
|
|
53
|
+
#commitGuard(binding){try{this.#recheck(binding);return true;}catch{throw new LocalStateError('authorization_changed','The current local pairing, endpoint or effective credential changed; no new delivery was committed.');}}
|
|
54
|
+
#attempted(messageId){return this.ledger.state.db.prepare('SELECT state FROM outbound WHERE local_key_id=? AND peer=? AND message_id=?').all(this.localId,this.peer,messageId).some(row=>row.state!=='prepared');}
|
|
55
|
+
async #rpc(binding,method,params,onSubmit=()=>{}){
|
|
56
|
+
const id=randomUUID(),headers={'content-type':'application/json',accept:'application/json','user-agent':'a2a-exposed-sealed-client','A2A-Version':this.protocol,'A2A-Extensions':EXTENSION,authorization:'Bearer '+binding.pairing.token};if(this.protocol==='0.3')headers['X-A2A-Extensions']=EXTENSION;
|
|
57
|
+
const request={jsonrpc:'2.0',id,method,params},body=canonicalJSON(request);if(Buffer.byteLength(body)>1024*1024)fail('request_too_large');
|
|
58
|
+
const init={method:'POST',headers,body,redirect:'error',signal:AbortSignal.timeout(this.timeoutMs)};onSubmit();const response=await this.fetch(binding.pairing.endpoint,init);
|
|
59
|
+
try{
|
|
60
|
+
if(response.redirected||(response.url&&response.url!==binding.pairing.endpoint)||response.status>=300&&response.status<400)fail('transport_redirect_refused');
|
|
61
|
+
if(response.status!==200)fail('transport_rejected');if(response.headers.get('content-type')?.split(';')[0].trim().toLowerCase()!=='application/json')fail('response_invalid');echo(response.headers,this.protocol);
|
|
62
|
+
const data=parseJSON(await text(response));if(!exact(data,['jsonrpc','id','result'])||data.jsonrpc!=='2.0'||data.id!==id)fail('rpc_response_invalid');return data.result;
|
|
63
|
+
}catch(error){try{await response.body?.cancel();}catch{}throw error;}
|
|
64
|
+
}
|
|
65
|
+
async send({messageId,now=Date.now()}={}){
|
|
66
|
+
let submitted=false,prior=false,binding;
|
|
67
|
+
try{
|
|
68
|
+
if(!id(messageId))fail('message_id_required');
|
|
69
|
+
prior=this.#attempted(messageId);if(!clock(now))fail('clock_invalid');
|
|
70
|
+
binding=this.#binding();const saved=this.ledger.outboundRow(binding.pin,messageId);const envelope=parseJSON(saved.envelope);if(saved.protocol!==this.protocol||envelope.header.role!=='user'||envelope.header.kind!=='message'||saved.reference_digest!==null)fail('request_binding_mismatch');
|
|
71
|
+
const started=await this.ledger.beginTransmission({...binding.scope,messageId,now});this.#recheck(binding);const message=makeMessageCarrier(started.envelope,this.protocol);
|
|
72
|
+
// Construct/validate bytes before marking submission; all later failures retain known sealed readability.
|
|
73
|
+
const params={message,configuration:this.protocol==='1.0'?{returnImmediately:true}:{blocking:false}};canonicalJSON(params);
|
|
74
|
+
const result=await this.#rpc(binding,this.protocol==='1.0'?'SendMessage':'message/send',params,()=>{submitted=true;}),task=this.protocol==='1.0'?(exact(result,['task'])?result.task:fail('task_response_required')):result;
|
|
75
|
+
const rawTask=canonicalJSON(task),parsed=readTaskCarrier(rawTask,this.protocol);this.#recheck(binding);
|
|
76
|
+
await this.ledger.recordTransport({...binding.scope,messageId,outcome:'transport-accepted',provisionalTaskId:parsed.taskId,extensionEcho:EXTENSION});
|
|
77
|
+
// A follow-up uses the initial locally signed assignment, never the provisional Task ID.
|
|
78
|
+
const initial=saved.task_id===null?messageId:this.ledger.state.db.prepare("SELECT request_message_id FROM assignments WHERE local_identity=? AND peer_identity=? AND context=? AND task_id=? AND direction='outbound'").get(binding.pin.localIdentity,binding.pin.peerIdentity,saved.context,saved.task_id)?.request_message_id;if(!initial)fail('assignment_required');
|
|
79
|
+
const accepted=await this.ledger.acceptTask({...binding.scope,requestMessageId:initial,rawTask,protocol:this.protocol,activatedExtension:EXTENSION,authorizationGuard:()=>this.#commitGuard(binding),withAuthorizationLease:this.withPairingLease,now});
|
|
80
|
+
return{ok:true,code:'sealed_task_received',operation:'send',deliveryStatus:accepted.deliveries.length?'locally-opened':'transport-accepted',next:'Inspect stable local delivery IDs. No model consumption or exactly-once external effect is claimed.',details:{messageId,taskId:accepted.taskId,assignmentAuthenticated:accepted.assignmentAuthenticated,deliveries:accepted.deliveries,currentPeerState:accepted.currentPeerState,privacy:{serverReadable:false,contentMode:'sealed'}}};
|
|
81
|
+
}catch(error){if(!submitted&&id(messageId)){try{prior=prior||this.#attempted(messageId);}catch{prior=true;}if(error?.code==='outbound_already_attempted')prior=true;}if(submitted&&binding){try{await this.ledger.recordTransport({...binding.scope,messageId,outcome:'unknown'});}catch{}}
|
|
82
|
+
return this.#failure(error,'send',submitted||prior?'unknown':'not-sent',{...(id(messageId)?{messageId}:{}),privacy:submitted?{serverReadable:false,contentMode:'sealed'}:prior?{serverReadable:'unknown',contentMode:'unknown'}:{serverReadable:'unknown',contentMode:'not-sent'}});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async poll({requestMessageId,now=Date.now()}={}){
|
|
86
|
+
try{
|
|
87
|
+
if(!id(requestMessageId))fail('request_message_id_required');if(!clock(now))fail('clock_invalid');
|
|
88
|
+
const binding=this.#binding(),row=this.ledger.outboundRow(binding.pin,requestMessageId),assignment=this.ledger.state.db.prepare("SELECT * FROM assignments WHERE local_identity=? AND peer_identity=? AND request_message_id=? AND direction='outbound'").get(binding.pin.localIdentity,binding.pin.peerIdentity,requestMessageId);
|
|
89
|
+
if(!assignment||assignment.trust_digest!==binding.pin.digest||assignment.protocol!==this.protocol||row.reference_digest!==null||row.task_id!==null||row.protocol!==this.protocol)fail('assignment_required');
|
|
90
|
+
const result=await this.#rpc(binding,this.protocol==='1.0'?'GetTask':'tasks/get',{id:assignment.task_id}),rawTask=canonicalJSON(result);this.#recheck(binding);
|
|
91
|
+
const accepted=await this.ledger.acceptTask({...binding.scope,requestMessageId,rawTask,protocol:this.protocol,activatedExtension:EXTENSION,authorizationGuard:()=>this.#commitGuard(binding),withAuthorizationLease:this.withPairingLease,now});
|
|
92
|
+
return{ok:true,code:'sealed_task_received',operation:'read',deliveryStatus:accepted.deliveries.length?'locally-opened':'transport-accepted',next:'Read stable local delivery IDs explicitly; transport polling did not consume model work.',details:{taskId:accepted.taskId,deliveries:accepted.deliveries,currentPeerState:accepted.currentPeerState,transportRoutingState:accepted.transportRoutingState,transportStateAuthenticated:false}};
|
|
93
|
+
}catch(error){return this.#failure(error,'read','unknown',{});}
|
|
94
|
+
}
|
|
95
|
+
#failure(error,operation,deliveryStatus,details){return{ok:false,code:error instanceof TransportError||error instanceof EncryptionError||error instanceof LocalStateError?error.code:'transport_failed',operation,deliveryStatus,retryable:false,next:'Preserve the original local message/assignment IDs and inspect their state. Never automatically resend with a new ID, alternate endpoint, protocol version or plaintext.',details};}
|
|
96
|
+
}
|