@orangecheck/agent-core 0.1.0 → 0.3.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 +60 -5
- package/dist/index.d.ts +60 -5
- package/dist/index.js +356 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +347 -30
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +65 -4
- package/dist/types.d.ts +65 -4
- package/dist/types.js.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +73 -69
- package/src/canonical.ts +23 -0
- package/src/index.ts +18 -0
- package/src/private-scope.test.ts +223 -0
- package/src/private-scope.ts +122 -0
- package/src/test-vectors.test.ts +358 -12
- package/src/types.ts +109 -4
- package/src/verify.ts +487 -40
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
|
|
@@ -40,14 +44,59 @@ export interface DelegationRevocationRef {
|
|
|
40
44
|
ref: string | null;
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
/**
|
|
48
|
+
* v1.2 private-scope mode: a wholesale OC Lock v2 LockEnvelope wrapping the
|
|
49
|
+
* canonical scope list as its payload. We re-import the LockEnvelope type
|
|
50
|
+
* structurally rather than depending on the @orangecheck/lock-core type — agent-
|
|
51
|
+
* core's type surface stays loose so verifiers can be authored in either an
|
|
52
|
+
* agent-only or full-family setup.
|
|
53
|
+
*/
|
|
54
|
+
export interface ScopesEncryptedEnvelope {
|
|
55
|
+
v: 2;
|
|
56
|
+
kind: 'identity';
|
|
57
|
+
id: string;
|
|
58
|
+
alg: { kem: 'x25519'; aead: 'aes-256-gcm'; kdf: 'hkdf-sha256' };
|
|
59
|
+
from: { address: string; attestation_id?: string };
|
|
60
|
+
recipients: Array<{
|
|
61
|
+
address: string;
|
|
62
|
+
device_id: string;
|
|
63
|
+
device_pk: string;
|
|
64
|
+
eph_pk: string;
|
|
65
|
+
wrapped_key: string;
|
|
66
|
+
nonce_kek: string;
|
|
67
|
+
}>;
|
|
68
|
+
ciphertext: string;
|
|
69
|
+
nonce_ct: string;
|
|
70
|
+
hint?: string;
|
|
71
|
+
created_at: string;
|
|
72
|
+
expires_at: string | null;
|
|
73
|
+
payment: unknown | null;
|
|
74
|
+
sig: { alg: 'bip322'; pubkey: string; value: string };
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
export interface DelegationEnvelope {
|
|
44
78
|
v: typeof ENVELOPE_VERSION;
|
|
45
79
|
kind: 'agent-delegation';
|
|
46
80
|
id: string; // 64-hex sha256(canonical_message)
|
|
47
81
|
principal: ActorRef;
|
|
48
82
|
agent: ActorRef;
|
|
49
|
-
/**
|
|
50
|
-
|
|
83
|
+
/**
|
|
84
|
+
* v1.0 / v1.1 public mode: sorted lexicographically in the canonical
|
|
85
|
+
* message; stored in sorted order on the envelope too.
|
|
86
|
+
*
|
|
87
|
+
* v1.2 private mode: this field is OMITTED from the envelope JSON;
|
|
88
|
+
* `scopes_encrypted` is set instead. After decryption, the recovered
|
|
89
|
+
* scope list takes this field's place in the in-memory envelope object
|
|
90
|
+
* for the remainder of verification.
|
|
91
|
+
*/
|
|
92
|
+
scopes?: string[];
|
|
93
|
+
/**
|
|
94
|
+
* v1.2 private mode (PRIVATE-SCOPE.md §1.1): an OC Lock v2 envelope
|
|
95
|
+
* wrapping the canonical scope-list bytes as its payload, sealed to one
|
|
96
|
+
* or more recipients (typically the agent ± named verifiers). MUTUALLY
|
|
97
|
+
* EXCLUSIVE with `scopes`.
|
|
98
|
+
*/
|
|
99
|
+
scopes_encrypted?: ScopesEncryptedEnvelope;
|
|
51
100
|
bond: DelegationBond | null;
|
|
52
101
|
issued_at: string; // ISO 8601 UTC
|
|
53
102
|
expires_at: string; // ISO 8601 UTC
|
|
@@ -111,6 +160,46 @@ export interface ActionCanonicalInput {
|
|
|
111
160
|
scope_exercised: string;
|
|
112
161
|
}
|
|
113
162
|
|
|
163
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
164
|
+
// Sub-delegation (SUB-DELEGATION.md, v1.1)
|
|
165
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
export interface SubdelegationEnvelope {
|
|
168
|
+
v: typeof ENVELOPE_VERSION;
|
|
169
|
+
kind: 'agent-subdelegation';
|
|
170
|
+
id: string; // 64-hex sha256(canonical_message)
|
|
171
|
+
parent_id: string; // 64-hex; the immediate parent envelope's id
|
|
172
|
+
/** The sub-principal — equal to parent.agent.address. */
|
|
173
|
+
principal: ActorRef;
|
|
174
|
+
/** The recipient sub-agent. */
|
|
175
|
+
agent: ActorRef;
|
|
176
|
+
/**
|
|
177
|
+
* v1.0 / v1.1 public mode: each scope MUST be a sub-scope of some scope
|
|
178
|
+
* on the parent. v1.2 private mode: omitted; `scopes_encrypted` set.
|
|
179
|
+
*/
|
|
180
|
+
scopes?: string[];
|
|
181
|
+
/** v1.2 private mode — same shape as the root delegation field. */
|
|
182
|
+
scopes_encrypted?: ScopesEncryptedEnvelope;
|
|
183
|
+
issued_at: string; // ISO 8601 UTC; >= parent.issued_at
|
|
184
|
+
expires_at: string; // ISO 8601 UTC; <= parent.expires_at
|
|
185
|
+
nonce: string; // 32-hex random
|
|
186
|
+
revocation: DelegationRevocationRef;
|
|
187
|
+
sig: Signature;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface SubdelegationCanonicalInput {
|
|
191
|
+
parent_id: string;
|
|
192
|
+
principal: string;
|
|
193
|
+
agent: string;
|
|
194
|
+
scopes: string[]; // pre-canonicalized, pre-sorted
|
|
195
|
+
issued_at: string;
|
|
196
|
+
expires_at: string;
|
|
197
|
+
nonce: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Either a root or a sub envelope; chain links walk up to a root delegation. */
|
|
201
|
+
export type ChainLink = DelegationEnvelope | SubdelegationEnvelope;
|
|
202
|
+
|
|
114
203
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
204
|
// Revocation (SPEC §9)
|
|
116
205
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -157,7 +246,15 @@ export type AgentErrorCode =
|
|
|
157
246
|
| 'E_BOND_UNMET'
|
|
158
247
|
| 'E_BOND_UNVERIFIED'
|
|
159
248
|
| 'E_REVOKER_UNAUTHORIZED'
|
|
160
|
-
| 'E_CALENDAR_UNREACHABLE'
|
|
249
|
+
| 'E_CALENDAR_UNREACHABLE'
|
|
250
|
+
| 'E_SUBDELEGATION_DEPTH_EXCEEDED'
|
|
251
|
+
| 'E_SUBDELEGATION_PRINCIPAL_MISMATCH'
|
|
252
|
+
| 'E_SUBDELEGATION_EXPIRES_EXTENDED'
|
|
253
|
+
| 'E_SUBDELEGATION_SCOPE_ESCALATED'
|
|
254
|
+
| 'E_SCOPES_BOTH_PROVIDED'
|
|
255
|
+
| 'E_SCOPES_NEITHER_PROVIDED'
|
|
256
|
+
| 'E_SCOPES_UNREADABLE'
|
|
257
|
+
| 'E_BAD_LOCK_ENVELOPE';
|
|
161
258
|
|
|
162
259
|
export interface VerifyOk<T> {
|
|
163
260
|
ok: true;
|
|
@@ -174,9 +271,17 @@ export interface VerifyErr {
|
|
|
174
271
|
|
|
175
272
|
export type VerifyDelegationResult = VerifyOk<DelegationEnvelope> | VerifyErr;
|
|
176
273
|
export type VerifyRevocationResult = VerifyOk<RevocationEnvelope> | VerifyErr;
|
|
274
|
+
export type VerifySubdelegationResult = VerifyOk<SubdelegationEnvelope> | VerifyErr;
|
|
177
275
|
|
|
178
276
|
export interface VerifyActionOkExtra {
|
|
277
|
+
/** The ROOT delegation rooting the authority chain. */
|
|
179
278
|
delegation: DelegationEnvelope;
|
|
279
|
+
/**
|
|
280
|
+
* The sub-delegation chain `[S_1, …, S_leaf]`. Empty when the action cites
|
|
281
|
+
* the root directly. The action's authority leaf is
|
|
282
|
+
* `chain[chain.length - 1] ?? delegation`.
|
|
283
|
+
*/
|
|
284
|
+
chain: SubdelegationEnvelope[];
|
|
180
285
|
scopeExercised: string;
|
|
181
286
|
anchor:
|
|
182
287
|
| { status: 'none' }
|