@orangecheck/agent-core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canonical.d.mts +5 -2
- package/dist/canonical.d.ts +5 -2
- package/dist/canonical.js +22 -0
- package/dist/canonical.js.map +1 -1
- package/dist/canonical.mjs +20 -1
- package/dist/canonical.mjs.map +1 -1
- package/dist/index.d.mts +15 -5
- package/dist/index.d.ts +15 -5
- package/dist/index.js +180 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +176 -22
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +29 -3
- package/dist/types.d.ts +29 -3
- package/dist/types.js.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +72 -69
- package/src/canonical.ts +23 -0
- package/src/index.ts +6 -0
- package/src/test-vectors.test.ts +358 -12
- package/src/types.ts +53 -2
- package/src/verify.ts +263 -32
package/src/test-vectors.test.ts
CHANGED
|
@@ -12,27 +12,38 @@ import {
|
|
|
12
12
|
computeActionId,
|
|
13
13
|
computeDelegationId,
|
|
14
14
|
computeRevocationId,
|
|
15
|
+
computeSubdelegationId,
|
|
15
16
|
delegationCanonicalMessage,
|
|
16
17
|
revocationCanonicalMessage,
|
|
18
|
+
subdelegationCanonicalMessage,
|
|
17
19
|
} from './canonical.js';
|
|
18
|
-
import {
|
|
20
|
+
import { parseScope, ScopeParseError } from './scope.js';
|
|
21
|
+
import { verifyAction, verifyDelegation, verifyRevocation, verifySubdelegation } from './verify.js';
|
|
19
22
|
import type {
|
|
20
23
|
ActionEnvelope,
|
|
24
|
+
AgentErrorCode,
|
|
21
25
|
DelegationEnvelope,
|
|
22
26
|
RevocationEnvelope,
|
|
27
|
+
SubdelegationEnvelope,
|
|
23
28
|
} from './types.js';
|
|
24
29
|
|
|
25
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
|
-
const VECTORS_DIR =
|
|
31
|
+
const VECTORS_DIR =
|
|
32
|
+
process.env.OC_AGENT_VECTORS_DIR ??
|
|
33
|
+
resolve(__dirname, '..', '..', '..', 'oc-agent-protocol', 'test-vectors');
|
|
27
34
|
|
|
28
35
|
interface BaseVector {
|
|
29
36
|
description: string;
|
|
30
|
-
kind: 'delegation' | 'action' | 'revocation';
|
|
37
|
+
kind: 'delegation' | 'action' | 'revocation' | 'subdelegation';
|
|
31
38
|
expected: {
|
|
32
39
|
canonical_message: string;
|
|
33
40
|
canonical_message_bytes_len: number;
|
|
34
41
|
id: string;
|
|
35
|
-
envelope:
|
|
42
|
+
envelope:
|
|
43
|
+
| DelegationEnvelope
|
|
44
|
+
| ActionEnvelope
|
|
45
|
+
| RevocationEnvelope
|
|
46
|
+
| SubdelegationEnvelope;
|
|
36
47
|
};
|
|
37
48
|
}
|
|
38
49
|
|
|
@@ -75,7 +86,106 @@ interface RevocationVector extends BaseVector {
|
|
|
75
86
|
expected: BaseVector['expected'] & { envelope: RevocationEnvelope };
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
|
|
89
|
+
interface SubdelegationVector extends BaseVector {
|
|
90
|
+
kind: 'subdelegation';
|
|
91
|
+
inputs: {
|
|
92
|
+
parent_id: string;
|
|
93
|
+
principal: string;
|
|
94
|
+
agent: string;
|
|
95
|
+
scopes: string[];
|
|
96
|
+
issued_at: string;
|
|
97
|
+
expires_at: string;
|
|
98
|
+
nonce: string;
|
|
99
|
+
};
|
|
100
|
+
expected: BaseVector['expected'] & { envelope: SubdelegationEnvelope };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Negative vectors carry "negative": true at the top level and document an
|
|
104
|
+
// expected verifier rejection (SPEC §11 error code) instead of a canonical-
|
|
105
|
+
// message round-trip.
|
|
106
|
+
interface NegativeVectorBase {
|
|
107
|
+
description: string;
|
|
108
|
+
kind: 'delegation' | 'action' | 'revocation';
|
|
109
|
+
negative: true;
|
|
110
|
+
expected: {
|
|
111
|
+
verification_outcome: 'REJECT';
|
|
112
|
+
error_code: AgentErrorCode;
|
|
113
|
+
spec_reference: string;
|
|
114
|
+
rejection_reason: string;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface NegativeActionVector extends NegativeVectorBase {
|
|
119
|
+
kind: 'action';
|
|
120
|
+
inputs: {
|
|
121
|
+
address: string;
|
|
122
|
+
content_hash: string;
|
|
123
|
+
content_length: number;
|
|
124
|
+
content_mime: string;
|
|
125
|
+
signed_at: string;
|
|
126
|
+
delegation_id: string;
|
|
127
|
+
scope_exercised: string;
|
|
128
|
+
content_ref?: string | null;
|
|
129
|
+
ots?: unknown;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface NegativeRevocationVector extends NegativeVectorBase {
|
|
134
|
+
kind: 'revocation';
|
|
135
|
+
inputs: {
|
|
136
|
+
address: string;
|
|
137
|
+
delegation_id: string;
|
|
138
|
+
reason: string;
|
|
139
|
+
signed_at: string;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface NegativeDelegationVector extends NegativeVectorBase {
|
|
144
|
+
kind: 'delegation';
|
|
145
|
+
inputs: {
|
|
146
|
+
principal: string;
|
|
147
|
+
agent: string;
|
|
148
|
+
scopes: string[];
|
|
149
|
+
bond: { sats: number; attestation_id: string } | null;
|
|
150
|
+
issued_at: string;
|
|
151
|
+
expires_at: string;
|
|
152
|
+
nonce: string;
|
|
153
|
+
};
|
|
154
|
+
additional_malformed_examples_for_implementer_smoke_tests?: {
|
|
155
|
+
scope: string;
|
|
156
|
+
why: string;
|
|
157
|
+
}[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface NegativeSubdelegationVector extends NegativeVectorBase {
|
|
161
|
+
kind: 'subdelegation';
|
|
162
|
+
inputs: {
|
|
163
|
+
parent_id: string;
|
|
164
|
+
principal: string;
|
|
165
|
+
agent: string;
|
|
166
|
+
scopes: string[];
|
|
167
|
+
issued_at: string;
|
|
168
|
+
expires_at: string;
|
|
169
|
+
nonce: string;
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
type NegativeVector =
|
|
174
|
+
| NegativeActionVector
|
|
175
|
+
| NegativeRevocationVector
|
|
176
|
+
| NegativeDelegationVector
|
|
177
|
+
| NegativeSubdelegationVector;
|
|
178
|
+
|
|
179
|
+
type Vector =
|
|
180
|
+
| DelegationVector
|
|
181
|
+
| ActionVector
|
|
182
|
+
| RevocationVector
|
|
183
|
+
| SubdelegationVector
|
|
184
|
+
| NegativeVector;
|
|
185
|
+
|
|
186
|
+
function isNegative(v: Vector): v is NegativeVector {
|
|
187
|
+
return 'negative' in v && v.negative === true;
|
|
188
|
+
}
|
|
79
189
|
|
|
80
190
|
async function loadVectors(): Promise<{ name: string; data: Vector }[]> {
|
|
81
191
|
try {
|
|
@@ -100,16 +210,22 @@ describe('oc-agent-protocol test vectors', () => {
|
|
|
100
210
|
return;
|
|
101
211
|
}
|
|
102
212
|
|
|
103
|
-
// First pass: the vectors can cross-reference each other (action cites delegation id
|
|
104
|
-
//
|
|
213
|
+
// First pass: the vectors can cross-reference each other (action cites delegation id;
|
|
214
|
+
// subdelegation cites parent id which may be a root or another sub).
|
|
215
|
+
// Key both kinds so chain walking works.
|
|
105
216
|
const delegationEnvelopes = new Map<string, DelegationEnvelope>();
|
|
217
|
+
const subdelegationEnvelopes = new Map<string, SubdelegationEnvelope>();
|
|
106
218
|
for (const { data } of vectors) {
|
|
219
|
+
if (isNegative(data)) continue;
|
|
107
220
|
if (data.kind === 'delegation') {
|
|
108
221
|
delegationEnvelopes.set(data.expected.id, data.expected.envelope);
|
|
222
|
+
} else if (data.kind === 'subdelegation') {
|
|
223
|
+
subdelegationEnvelopes.set(data.expected.id, data.expected.envelope);
|
|
109
224
|
}
|
|
110
225
|
}
|
|
111
226
|
|
|
112
227
|
for (const { name, data } of vectors) {
|
|
228
|
+
if (isNegative(data)) continue;
|
|
113
229
|
it(`${name} — canonical message reconstructs byte-identical`, () => {
|
|
114
230
|
const msg = reconstructCanonical(data);
|
|
115
231
|
expect(msg).toBe(data.expected.canonical_message);
|
|
@@ -143,7 +259,7 @@ describe('oc-agent-protocol test vectors', () => {
|
|
|
143
259
|
skipSignatureVerification: true,
|
|
144
260
|
});
|
|
145
261
|
expect(r.ok).toBe(true);
|
|
146
|
-
} else {
|
|
262
|
+
} else if (data.kind === 'revocation') {
|
|
147
263
|
const delegation = delegationEnvelopes.get(data.inputs.delegation_id);
|
|
148
264
|
if (!delegation) {
|
|
149
265
|
throw new Error(`revocation vector ${name} references missing delegation ${data.inputs.delegation_id}`);
|
|
@@ -154,12 +270,36 @@ describe('oc-agent-protocol test vectors', () => {
|
|
|
154
270
|
skipSignatureVerification: true,
|
|
155
271
|
});
|
|
156
272
|
expect(r.ok).toBe(true);
|
|
273
|
+
} else {
|
|
274
|
+
// subdelegation — parent may be a root delegation or another subdelegation.
|
|
275
|
+
const parentId = data.inputs.parent_id;
|
|
276
|
+
const parent =
|
|
277
|
+
delegationEnvelopes.get(parentId) ??
|
|
278
|
+
subdelegationEnvelopes.get(parentId);
|
|
279
|
+
if (!parent) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`subdelegation vector ${name} references missing parent ${parentId}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const r = await verifySubdelegation({
|
|
285
|
+
envelope: data.expected.envelope,
|
|
286
|
+
parent,
|
|
287
|
+
skipSignatureVerification: true,
|
|
288
|
+
skipTemporalCheck: true,
|
|
289
|
+
});
|
|
290
|
+
expect(r.ok).toBe(true);
|
|
157
291
|
}
|
|
158
292
|
});
|
|
159
293
|
}
|
|
160
294
|
});
|
|
161
295
|
|
|
162
|
-
|
|
296
|
+
type PositiveVector =
|
|
297
|
+
| DelegationVector
|
|
298
|
+
| ActionVector
|
|
299
|
+
| RevocationVector
|
|
300
|
+
| SubdelegationVector;
|
|
301
|
+
|
|
302
|
+
function reconstructCanonical(v: PositiveVector): string {
|
|
163
303
|
if (v.kind === 'delegation') {
|
|
164
304
|
// Canonicalize each scope (constraints sorted by key) then sort the list.
|
|
165
305
|
const canonical = canonicalizeScopes(v.inputs.scopes);
|
|
@@ -177,10 +317,23 @@ function reconstructCanonical(v: Vector): string {
|
|
|
177
317
|
if (v.kind === 'action') {
|
|
178
318
|
return actionCanonicalMessage(v.inputs);
|
|
179
319
|
}
|
|
180
|
-
|
|
320
|
+
if (v.kind === 'revocation') {
|
|
321
|
+
return revocationCanonicalMessage(v.inputs);
|
|
322
|
+
}
|
|
323
|
+
// subdelegation
|
|
324
|
+
const canonical = canonicalizeScopes(v.inputs.scopes);
|
|
325
|
+
return subdelegationCanonicalMessage({
|
|
326
|
+
parent_id: v.inputs.parent_id,
|
|
327
|
+
principal: v.inputs.principal,
|
|
328
|
+
agent: v.inputs.agent,
|
|
329
|
+
scopes: canonical,
|
|
330
|
+
issued_at: v.inputs.issued_at,
|
|
331
|
+
expires_at: v.inputs.expires_at,
|
|
332
|
+
nonce: v.inputs.nonce,
|
|
333
|
+
});
|
|
181
334
|
}
|
|
182
335
|
|
|
183
|
-
function reconstructId(v:
|
|
336
|
+
function reconstructId(v: PositiveVector): string {
|
|
184
337
|
if (v.kind === 'delegation') {
|
|
185
338
|
const canonical = canonicalizeScopes(v.inputs.scopes);
|
|
186
339
|
return computeDelegationId({
|
|
@@ -195,5 +348,198 @@ function reconstructId(v: Vector): string {
|
|
|
195
348
|
});
|
|
196
349
|
}
|
|
197
350
|
if (v.kind === 'action') return computeActionId(v.inputs);
|
|
198
|
-
return computeRevocationId(v.inputs);
|
|
351
|
+
if (v.kind === 'revocation') return computeRevocationId(v.inputs);
|
|
352
|
+
// subdelegation
|
|
353
|
+
const canonical = canonicalizeScopes(v.inputs.scopes);
|
|
354
|
+
return computeSubdelegationId({
|
|
355
|
+
parent_id: v.inputs.parent_id,
|
|
356
|
+
principal: v.inputs.principal,
|
|
357
|
+
agent: v.inputs.agent,
|
|
358
|
+
scopes: canonical,
|
|
359
|
+
issued_at: v.inputs.issued_at,
|
|
360
|
+
expires_at: v.inputs.expires_at,
|
|
361
|
+
nonce: v.inputs.nonce,
|
|
362
|
+
});
|
|
199
363
|
}
|
|
364
|
+
|
|
365
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
366
|
+
// Negative vectors — verifier MUST reject with the declared error code.
|
|
367
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
describe('oc-agent-protocol negative test vectors', () => {
|
|
370
|
+
const negatives = vectors.filter(({ data }) => isNegative(data)) as {
|
|
371
|
+
name: string;
|
|
372
|
+
data: NegativeVector;
|
|
373
|
+
}[];
|
|
374
|
+
|
|
375
|
+
if (negatives.length === 0) {
|
|
376
|
+
it.skip('(no negative vectors found)', () => {});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Need positive delegation envelopes to feed verifyAction / verifyRevocation
|
|
381
|
+
// for the cross-referenced negative vectors.
|
|
382
|
+
const delegationEnvelopes = new Map<string, DelegationEnvelope>();
|
|
383
|
+
for (const { data } of vectors) {
|
|
384
|
+
if (!isNegative(data) && data.kind === 'delegation') {
|
|
385
|
+
delegationEnvelopes.set(data.expected.id, data.expected.envelope);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const { name, data } of negatives) {
|
|
390
|
+
it(`${name} — verifier rejects with ${data.expected.error_code}`, async () => {
|
|
391
|
+
if (data.kind === 'action') {
|
|
392
|
+
const v = data as NegativeActionVector;
|
|
393
|
+
const delegation = delegationEnvelopes.get(v.inputs.delegation_id);
|
|
394
|
+
if (!delegation) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`negative action vector ${name} cites missing delegation ${v.inputs.delegation_id}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
// Build a syntactically well-formed action envelope from the
|
|
400
|
+
// inputs (the rejection here is semantic — scope / window —
|
|
401
|
+
// not structural). The id MUST be the real sha256(canonical)
|
|
402
|
+
// or the verifier will short-circuit with E_BAD_ID before
|
|
403
|
+
// reaching the rule we're actually testing.
|
|
404
|
+
const realId = computeActionId({
|
|
405
|
+
address: v.inputs.address,
|
|
406
|
+
content_hash: v.inputs.content_hash,
|
|
407
|
+
content_length: v.inputs.content_length,
|
|
408
|
+
content_mime: v.inputs.content_mime,
|
|
409
|
+
signed_at: v.inputs.signed_at,
|
|
410
|
+
delegation_id: v.inputs.delegation_id,
|
|
411
|
+
scope_exercised: v.inputs.scope_exercised,
|
|
412
|
+
});
|
|
413
|
+
const action: ActionEnvelope = {
|
|
414
|
+
v: 1,
|
|
415
|
+
kind: 'agent-action',
|
|
416
|
+
id: realId,
|
|
417
|
+
content: {
|
|
418
|
+
hash: v.inputs.content_hash,
|
|
419
|
+
length: v.inputs.content_length,
|
|
420
|
+
mime: v.inputs.content_mime,
|
|
421
|
+
ref: v.inputs.content_ref ?? null,
|
|
422
|
+
},
|
|
423
|
+
signer: { address: v.inputs.address, alg: 'bip322' },
|
|
424
|
+
signed_at: v.inputs.signed_at,
|
|
425
|
+
delegation_id: v.inputs.delegation_id,
|
|
426
|
+
scope_exercised: v.inputs.scope_exercised,
|
|
427
|
+
ots: null,
|
|
428
|
+
sig: { alg: 'bip322', pubkey: v.inputs.address, value: 'AAAA' },
|
|
429
|
+
};
|
|
430
|
+
const r = await verifyAction({
|
|
431
|
+
action,
|
|
432
|
+
delegation,
|
|
433
|
+
skipSignatureVerification: true,
|
|
434
|
+
});
|
|
435
|
+
expect(r.ok).toBe(false);
|
|
436
|
+
if (!r.ok) {
|
|
437
|
+
// v07 specifically allows either E_OUT_OF_WINDOW or E_EXPIRED
|
|
438
|
+
// since both fire for an action signed past the delegation's
|
|
439
|
+
// expires_at; harness_assertion documents this.
|
|
440
|
+
if (
|
|
441
|
+
v.expected.error_code === 'E_OUT_OF_WINDOW' &&
|
|
442
|
+
r.code === 'E_EXPIRED'
|
|
443
|
+
) {
|
|
444
|
+
// accept the alternate code
|
|
445
|
+
} else {
|
|
446
|
+
expect(r.code).toBe(v.expected.error_code);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} else if (data.kind === 'revocation') {
|
|
450
|
+
const v = data as NegativeRevocationVector;
|
|
451
|
+
const delegation = delegationEnvelopes.get(v.inputs.delegation_id);
|
|
452
|
+
if (!delegation) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`negative revocation vector ${name} cites missing delegation ${v.inputs.delegation_id}`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const realRevId = computeRevocationId({
|
|
458
|
+
address: v.inputs.address,
|
|
459
|
+
delegation_id: v.inputs.delegation_id,
|
|
460
|
+
reason: v.inputs.reason,
|
|
461
|
+
signed_at: v.inputs.signed_at,
|
|
462
|
+
});
|
|
463
|
+
const revocation: RevocationEnvelope = {
|
|
464
|
+
v: 1,
|
|
465
|
+
kind: 'agent-revocation',
|
|
466
|
+
id: realRevId,
|
|
467
|
+
delegation_id: v.inputs.delegation_id,
|
|
468
|
+
signer: { address: v.inputs.address, alg: 'bip322' },
|
|
469
|
+
reason: v.inputs.reason,
|
|
470
|
+
signed_at: v.inputs.signed_at,
|
|
471
|
+
ots: null,
|
|
472
|
+
sig: { alg: 'bip322', pubkey: v.inputs.address, value: 'AAAA' },
|
|
473
|
+
};
|
|
474
|
+
const r = await verifyRevocation({
|
|
475
|
+
envelope: revocation,
|
|
476
|
+
delegation,
|
|
477
|
+
skipSignatureVerification: true,
|
|
478
|
+
});
|
|
479
|
+
expect(r.ok).toBe(false);
|
|
480
|
+
if (!r.ok) expect(r.code).toBe(v.expected.error_code);
|
|
481
|
+
} else if (data.kind === 'delegation') {
|
|
482
|
+
// Delegation negative vectors target the §7.1 grammar parser
|
|
483
|
+
// directly — that's where E_BAD_SCOPE_GRAMMAR fires per
|
|
484
|
+
// SPEC §8.1 step 4. Asserting parseScope throws is the
|
|
485
|
+
// right level of rigor.
|
|
486
|
+
const v = data as NegativeDelegationVector;
|
|
487
|
+
expect(v.expected.error_code).toBe('E_BAD_SCOPE_GRAMMAR');
|
|
488
|
+
for (const s of v.inputs.scopes) {
|
|
489
|
+
expect(() => parseScope(s)).toThrow(ScopeParseError);
|
|
490
|
+
}
|
|
491
|
+
for (const ex of v.additional_malformed_examples_for_implementer_smoke_tests ??
|
|
492
|
+
[]) {
|
|
493
|
+
expect(
|
|
494
|
+
() => parseScope(ex.scope),
|
|
495
|
+
`additional malformed example: ${ex.why}`
|
|
496
|
+
).toThrow(ScopeParseError);
|
|
497
|
+
}
|
|
498
|
+
} else if (data.kind === 'subdelegation') {
|
|
499
|
+
// v12, v13, v14 — chain-link checks via verifySubdelegation.
|
|
500
|
+
// Build a structurally valid subdelegation envelope from inputs
|
|
501
|
+
// (the rejection here is semantic — scope / window / linkage —
|
|
502
|
+
// not structural) and feed it to verifySubdelegation against the
|
|
503
|
+
// root delegation vector.
|
|
504
|
+
const v = data as NegativeSubdelegationVector;
|
|
505
|
+
const parent = delegationEnvelopes.get(v.inputs.parent_id);
|
|
506
|
+
if (!parent) {
|
|
507
|
+
throw new Error(
|
|
508
|
+
`negative subdelegation vector ${name} references missing parent ${v.inputs.parent_id}`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
const realId = computeSubdelegationId({
|
|
512
|
+
parent_id: v.inputs.parent_id,
|
|
513
|
+
principal: v.inputs.principal,
|
|
514
|
+
agent: v.inputs.agent,
|
|
515
|
+
scopes: canonicalizeScopes(v.inputs.scopes),
|
|
516
|
+
issued_at: v.inputs.issued_at,
|
|
517
|
+
expires_at: v.inputs.expires_at,
|
|
518
|
+
nonce: v.inputs.nonce,
|
|
519
|
+
});
|
|
520
|
+
const sub: SubdelegationEnvelope = {
|
|
521
|
+
v: 1,
|
|
522
|
+
kind: 'agent-subdelegation',
|
|
523
|
+
id: realId,
|
|
524
|
+
parent_id: v.inputs.parent_id,
|
|
525
|
+
principal: { address: v.inputs.principal, alg: 'bip322' },
|
|
526
|
+
agent: { address: v.inputs.agent, alg: 'bip322' },
|
|
527
|
+
scopes: v.inputs.scopes,
|
|
528
|
+
issued_at: v.inputs.issued_at,
|
|
529
|
+
expires_at: v.inputs.expires_at,
|
|
530
|
+
nonce: v.inputs.nonce,
|
|
531
|
+
revocation: { holders: ['principal'], ref: null },
|
|
532
|
+
sig: { alg: 'bip322', pubkey: v.inputs.principal, value: 'AAAA' },
|
|
533
|
+
};
|
|
534
|
+
const r = await verifySubdelegation({
|
|
535
|
+
envelope: sub,
|
|
536
|
+
parent,
|
|
537
|
+
skipSignatureVerification: true,
|
|
538
|
+
skipTemporalCheck: true,
|
|
539
|
+
});
|
|
540
|
+
expect(r.ok).toBe(false);
|
|
541
|
+
if (!r.ok) expect(r.code).toBe(v.expected.error_code);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
export const ENVELOPE_VERSION = 1 as const;
|
|
4
4
|
|
|
5
|
-
export type EnvelopeKind =
|
|
5
|
+
export type EnvelopeKind =
|
|
6
|
+
| 'agent-delegation'
|
|
7
|
+
| 'agent-action'
|
|
8
|
+
| 'agent-revocation'
|
|
9
|
+
| 'agent-subdelegation';
|
|
6
10
|
|
|
7
11
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
12
|
// Shared building blocks
|
|
@@ -111,6 +115,41 @@ export interface ActionCanonicalInput {
|
|
|
111
115
|
scope_exercised: string;
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
// Sub-delegation (SUB-DELEGATION.md, v1.1)
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export interface SubdelegationEnvelope {
|
|
123
|
+
v: typeof ENVELOPE_VERSION;
|
|
124
|
+
kind: 'agent-subdelegation';
|
|
125
|
+
id: string; // 64-hex sha256(canonical_message)
|
|
126
|
+
parent_id: string; // 64-hex; the immediate parent envelope's id
|
|
127
|
+
/** The sub-principal — equal to parent.agent.address. */
|
|
128
|
+
principal: ActorRef;
|
|
129
|
+
/** The recipient sub-agent. */
|
|
130
|
+
agent: ActorRef;
|
|
131
|
+
/** Each scope MUST be a sub-scope of some scope on the parent. */
|
|
132
|
+
scopes: string[];
|
|
133
|
+
issued_at: string; // ISO 8601 UTC; >= parent.issued_at
|
|
134
|
+
expires_at: string; // ISO 8601 UTC; <= parent.expires_at
|
|
135
|
+
nonce: string; // 32-hex random
|
|
136
|
+
revocation: DelegationRevocationRef;
|
|
137
|
+
sig: Signature;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface SubdelegationCanonicalInput {
|
|
141
|
+
parent_id: string;
|
|
142
|
+
principal: string;
|
|
143
|
+
agent: string;
|
|
144
|
+
scopes: string[]; // pre-canonicalized, pre-sorted
|
|
145
|
+
issued_at: string;
|
|
146
|
+
expires_at: string;
|
|
147
|
+
nonce: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Either a root or a sub envelope; chain links walk up to a root delegation. */
|
|
151
|
+
export type ChainLink = DelegationEnvelope | SubdelegationEnvelope;
|
|
152
|
+
|
|
114
153
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
154
|
// Revocation (SPEC §9)
|
|
116
155
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -157,7 +196,11 @@ export type AgentErrorCode =
|
|
|
157
196
|
| 'E_BOND_UNMET'
|
|
158
197
|
| 'E_BOND_UNVERIFIED'
|
|
159
198
|
| 'E_REVOKER_UNAUTHORIZED'
|
|
160
|
-
| 'E_CALENDAR_UNREACHABLE'
|
|
199
|
+
| 'E_CALENDAR_UNREACHABLE'
|
|
200
|
+
| 'E_SUBDELEGATION_DEPTH_EXCEEDED'
|
|
201
|
+
| 'E_SUBDELEGATION_PRINCIPAL_MISMATCH'
|
|
202
|
+
| 'E_SUBDELEGATION_EXPIRES_EXTENDED'
|
|
203
|
+
| 'E_SUBDELEGATION_SCOPE_ESCALATED';
|
|
161
204
|
|
|
162
205
|
export interface VerifyOk<T> {
|
|
163
206
|
ok: true;
|
|
@@ -174,9 +217,17 @@ export interface VerifyErr {
|
|
|
174
217
|
|
|
175
218
|
export type VerifyDelegationResult = VerifyOk<DelegationEnvelope> | VerifyErr;
|
|
176
219
|
export type VerifyRevocationResult = VerifyOk<RevocationEnvelope> | VerifyErr;
|
|
220
|
+
export type VerifySubdelegationResult = VerifyOk<SubdelegationEnvelope> | VerifyErr;
|
|
177
221
|
|
|
178
222
|
export interface VerifyActionOkExtra {
|
|
223
|
+
/** The ROOT delegation rooting the authority chain. */
|
|
179
224
|
delegation: DelegationEnvelope;
|
|
225
|
+
/**
|
|
226
|
+
* The sub-delegation chain `[S_1, …, S_leaf]`. Empty when the action cites
|
|
227
|
+
* the root directly. The action's authority leaf is
|
|
228
|
+
* `chain[chain.length - 1] ?? delegation`.
|
|
229
|
+
*/
|
|
230
|
+
chain: SubdelegationEnvelope[];
|
|
180
231
|
scopeExercised: string;
|
|
181
232
|
anchor:
|
|
182
233
|
| { status: 'none' }
|