@orangecheck/agent-core 2.2.0 → 2.4.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.js.map +1 -1
- package/dist/canonical.mjs.map +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +53 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +53 -3
- package/dist/index.mjs.map +1 -1
- package/dist/scope.js +4 -1
- package/dist/scope.js.map +1 -1
- package/dist/scope.mjs +4 -1
- package/dist/scope.mjs.map +1 -1
- package/dist/types.d.mts +6 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.js.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +1 -1
- package/src/federation.test.ts +97 -1
- package/src/federation.ts +75 -1
- package/src/revocation-authority.test.ts +25 -1
- package/src/scope.test.ts +25 -0
- package/src/scope.ts +3 -0
- package/src/types.ts +6 -1
- package/src/verify.ts +5 -2
package/src/federation.ts
CHANGED
|
@@ -25,7 +25,13 @@ import {
|
|
|
25
25
|
hexEncode,
|
|
26
26
|
revocationCanonicalMessage,
|
|
27
27
|
} from './canonical.js';
|
|
28
|
-
import type {
|
|
28
|
+
import type {
|
|
29
|
+
ActorRef,
|
|
30
|
+
AgentErrorCode,
|
|
31
|
+
DelegationBond,
|
|
32
|
+
DelegationRevocationRef,
|
|
33
|
+
RevocationEnvelope,
|
|
34
|
+
} from './types.js';
|
|
29
35
|
|
|
30
36
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
37
|
// Types (FEDERATION.md §2 / §3.2)
|
|
@@ -221,6 +227,13 @@ async function checkFederationQuorum(
|
|
|
221
227
|
|
|
222
228
|
export interface VerifyFederationDelegationInput extends VerifyFederationBase {
|
|
223
229
|
envelope: FederationDelegationEnvelope;
|
|
230
|
+
/**
|
|
231
|
+
* Revocations to check (kind-30085 by `#delegation`): federation revocations,
|
|
232
|
+
* and agent self-revocations when `revocation.holders` includes "agent".
|
|
233
|
+
* Required unless `skipRevocationCheck` states that revocation is not checked.
|
|
234
|
+
*/
|
|
235
|
+
revocations?: Array<FederationRevocationEnvelope | RevocationEnvelope>;
|
|
236
|
+
skipRevocationCheck?: boolean;
|
|
224
237
|
now?: Date;
|
|
225
238
|
skipTemporalCheck?: boolean;
|
|
226
239
|
}
|
|
@@ -273,6 +286,21 @@ export async function verifyFederationDelegation(
|
|
|
273
286
|
const quorum = await checkFederationQuorum(env.principal, env.sig, env.id, input);
|
|
274
287
|
if (!quorum.ok) return quorum;
|
|
275
288
|
|
|
289
|
+
// Revocation — the same rule as verifyDelegation: check unless told not to.
|
|
290
|
+
if (!input.skipRevocationCheck && !input.revocations) {
|
|
291
|
+
return fail(
|
|
292
|
+
'E_MALFORMED',
|
|
293
|
+
'verifyFederationDelegation requires `revocations` (fetch kind-30085 by #delegation) ' +
|
|
294
|
+
'or an explicit skipRevocationCheck:true to state that revocation is not being checked'
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
for (const rev of input.revocations ?? []) {
|
|
298
|
+
if (rev?.delegation_id !== env.id) continue;
|
|
299
|
+
const rr = await verifyRevocationOf(env, rev, input);
|
|
300
|
+
if (!rr.ok) continue; // a malformed or unauthorized revocation revokes nothing
|
|
301
|
+
return fail('E_REVOKED', `delegation ${env.id} was revoked by ${rev.id}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
276
304
|
if (!input.skipTemporalCheck) {
|
|
277
305
|
const now = input.now ?? new Date();
|
|
278
306
|
const issued = new Date(env.issued_at);
|
|
@@ -285,6 +313,52 @@ export async function verifyFederationDelegation(
|
|
|
285
313
|
return { ok: true, id: env.id, canonicalMessage: delegationCanonicalMessage(canonInput) };
|
|
286
314
|
}
|
|
287
315
|
|
|
316
|
+
/**
|
|
317
|
+
* FEDERATION.md §4: the federation (M-of-N) may always revoke; the agent may
|
|
318
|
+
* self-revoke with its own BIP-322 signature when `revocation.holders`
|
|
319
|
+
* includes "agent".
|
|
320
|
+
*/
|
|
321
|
+
async function verifyRevocationOf(
|
|
322
|
+
d: FederationDelegationEnvelope,
|
|
323
|
+
rev: FederationRevocationEnvelope | RevocationEnvelope,
|
|
324
|
+
input: VerifyFederationBase
|
|
325
|
+
): Promise<FederationVerifyResult> {
|
|
326
|
+
if ((rev.signer as FederationPrincipal | undefined)?.alg === 'federation') {
|
|
327
|
+
return verifyFederationRevocation({
|
|
328
|
+
envelope: rev as FederationRevocationEnvelope,
|
|
329
|
+
delegation: d,
|
|
330
|
+
verifyBip322: input.verifyBip322,
|
|
331
|
+
skipSignatureVerification: input.skipSignatureVerification,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const v1 = rev as RevocationEnvelope;
|
|
335
|
+
const holders = d.revocation?.holders ?? ['principal'];
|
|
336
|
+
if (!Array.isArray(holders) || !holders.includes('agent')) {
|
|
337
|
+
return fail('E_REVOKER_UNAUTHORIZED', 'only the federation may revoke this delegation');
|
|
338
|
+
}
|
|
339
|
+
if (v1.kind !== 'agent-revocation' || v1.signer?.address !== d.agent.address) {
|
|
340
|
+
return fail('E_REVOKER_UNAUTHORIZED', 'a single-address revocation must be signed by the agent');
|
|
341
|
+
}
|
|
342
|
+
if (typeof v1.reason !== 'string' || !ISO_UTC.test(v1.signed_at ?? '')) {
|
|
343
|
+
return fail('E_MALFORMED', 'revocation reason / signed_at invalid');
|
|
344
|
+
}
|
|
345
|
+
const canonInput = {
|
|
346
|
+
address: v1.signer.address,
|
|
347
|
+
delegation_id: v1.delegation_id,
|
|
348
|
+
reason: v1.reason,
|
|
349
|
+
signed_at: v1.signed_at,
|
|
350
|
+
};
|
|
351
|
+
const id = computeRevocationId(canonInput);
|
|
352
|
+
if (id !== v1.id) return fail('E_BAD_ID', `reconstructed id (${id}) != revocation.id (${v1.id})`);
|
|
353
|
+
if (!input.skipSignatureVerification) {
|
|
354
|
+
if (!input.verifyBip322) return fail('E_BAD_SIG', 'no BIP-322 verifier supplied');
|
|
355
|
+
if (typeof v1.sig?.value !== 'string' || !(await input.verifyBip322(v1.id, v1.sig.value, v1.signer.address))) {
|
|
356
|
+
return fail('E_BAD_SIG', 'revocation BIP-322 signature did not verify');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return { ok: true, id: v1.id, canonicalMessage: revocationCanonicalMessage(canonInput) };
|
|
360
|
+
}
|
|
361
|
+
|
|
288
362
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
289
363
|
// Revocation under a federation principal (FEDERATION.md §4)
|
|
290
364
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -176,13 +176,37 @@ describe('revocation priority uses only verified anchors', () => {
|
|
|
176
176
|
verifyOtsAnchor: async () => false,
|
|
177
177
|
});
|
|
178
178
|
expect(clean.ok && clean.anchor).toEqual({
|
|
179
|
-
status: '
|
|
179
|
+
status: 'claimed',
|
|
180
180
|
blockHeight: 1,
|
|
181
181
|
blockHash: 'ab'.repeat(32),
|
|
182
182
|
verified: false,
|
|
183
183
|
});
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
// The ots block is outside the signature: its block is a claim until walked.
|
|
187
|
+
it('reports an unwalked anchor as claimed, and a walked one as confirmed', async () => {
|
|
188
|
+
const unwalked = await verifyAction({
|
|
189
|
+
action: action(d, later, confirmed(1)),
|
|
190
|
+
delegation: d,
|
|
191
|
+
revocations: [],
|
|
192
|
+
skipSignatureVerification: true,
|
|
193
|
+
});
|
|
194
|
+
expect(unwalked.ok && unwalked.anchor.status).toBe('claimed');
|
|
195
|
+
const walked = await verifyAction({
|
|
196
|
+
action: action(d, later, confirmed(1)),
|
|
197
|
+
delegation: d,
|
|
198
|
+
revocations: [],
|
|
199
|
+
skipSignatureVerification: true,
|
|
200
|
+
verifyOtsAnchor: async () => true,
|
|
201
|
+
});
|
|
202
|
+
expect(walked.ok && walked.anchor).toEqual({
|
|
203
|
+
status: 'confirmed',
|
|
204
|
+
blockHeight: 1,
|
|
205
|
+
blockHash: 'ab'.repeat(32),
|
|
206
|
+
verified: true,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
186
210
|
it('the anchor verifier is asked about the envelope it belongs to', async () => {
|
|
187
211
|
const a = action(d, later, confirmed(100));
|
|
188
212
|
const revAnchored = revocation(d, PRINCIPAL, '2026-04-23T00:00:00Z', confirmed(200));
|
package/src/scope.test.ts
CHANGED
|
@@ -204,3 +204,28 @@ describe('ordered-op values must be decimal integers (SPEC §7.4)', () => {
|
|
|
204
204
|
expect(isSubScope(exercised, parseScope('ln:send(max_sats<=0)'))).toBe(false);
|
|
205
205
|
});
|
|
206
206
|
});
|
|
207
|
+
|
|
208
|
+
// oc-pledge-protocol SPEC §7.3 defines pledge:create and its constraint keys.
|
|
209
|
+
describe('pledge:create is a registered scope', () => {
|
|
210
|
+
for (const s of [
|
|
211
|
+
'pledge:create',
|
|
212
|
+
'pledge:create(max_bond_sats=2000000)',
|
|
213
|
+
'pledge:create(mechanism=vote_resolves)',
|
|
214
|
+
'pledge:create(counterparty=bc1qalice000000000000000000000000000000000)',
|
|
215
|
+
'pledge:create(max_bond_sats=2000000,mechanism=vote_resolves,counterparty=bc1qalice000000000000000000000000000000000)',
|
|
216
|
+
]) {
|
|
217
|
+
it(`strict mode accepts ${s}`, () => {
|
|
218
|
+
expect(() => validateScope(parseScope(s), { mode: 'strict' })).not.toThrow();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
it('strict mode still rejects an unregistered pledge:create key', () => {
|
|
223
|
+
expect(() => validateScope(parseScope('pledge:create(foo=1)'), { mode: 'strict' })).toThrow(ScopeParseError);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('max_bond_sats is an ordered numeric bound', () => {
|
|
227
|
+
const granted = parseScope('pledge:create(max_bond_sats<=2000000)');
|
|
228
|
+
expect(isSubScope(parseScope('pledge:create(max_bond_sats=500000)'), granted)).toBe(true);
|
|
229
|
+
expect(isSubScope(parseScope('pledge:create(max_bond_sats=3000000)'), granted)).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
package/src/scope.ts
CHANGED
|
@@ -34,6 +34,8 @@ export const REGISTERED_SCOPES: Record<string, { keys: string[] }> = {
|
|
|
34
34
|
'http:request': { keys: ['origin', 'method', 'max_rps', 'max_bytes_out'] },
|
|
35
35
|
'ln:send': { keys: ['max_sats', 'node', 'max_fee_sats'] },
|
|
36
36
|
'mcp:invoke': { keys: ['server', 'tool', 'max_invocations'] },
|
|
37
|
+
// oc-pledge-protocol SPEC §7.3.
|
|
38
|
+
'pledge:create': { keys: ['max_bond_sats', 'mechanism', 'counterparty'] },
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
/** Keys whose values are compared numerically for sub-scope ordering. */
|
|
@@ -46,6 +48,7 @@ const NUMERIC_KEYS = new Set<string>([
|
|
|
46
48
|
'max_sats',
|
|
47
49
|
'max_fee_sats',
|
|
48
50
|
'max_invocations',
|
|
51
|
+
'max_bond_sats',
|
|
49
52
|
'kind',
|
|
50
53
|
]);
|
|
51
54
|
|
package/src/types.ts
CHANGED
|
@@ -289,10 +289,15 @@ export interface VerifyActionOkExtra {
|
|
|
289
289
|
*/
|
|
290
290
|
chain: SubdelegationEnvelope[];
|
|
291
291
|
scopeExercised: string;
|
|
292
|
+
/**
|
|
293
|
+
* `claimed` is a confirmed `ots` block no verifier walked: the block is the
|
|
294
|
+
* envelope's say-so. Only a passing `verifyOtsAnchor` yields `confirmed`.
|
|
295
|
+
*/
|
|
292
296
|
anchor:
|
|
293
297
|
| { status: 'none' }
|
|
294
298
|
| { status: 'pending' }
|
|
295
|
-
| { status: '
|
|
299
|
+
| { status: 'claimed'; blockHeight: number; blockHash: string; verified: false }
|
|
300
|
+
| { status: 'confirmed'; blockHeight: number; blockHash: string; verified: true };
|
|
296
301
|
}
|
|
297
302
|
|
|
298
303
|
export type VerifyActionResult =
|
package/src/verify.ts
CHANGED
|
@@ -512,8 +512,11 @@ export async function verifyAction(input: VerifyActionInput): Promise<VerifyActi
|
|
|
512
512
|
if (h === null || hash === null) {
|
|
513
513
|
return err('E_MALFORMED', 'confirmed OTS proof missing block_height or block_hash');
|
|
514
514
|
}
|
|
515
|
-
|
|
516
|
-
|
|
515
|
+
anchor = (
|
|
516
|
+
actionAnchor !== null
|
|
517
|
+
? { status: 'confirmed', blockHeight: h, blockHash: hash, verified: true }
|
|
518
|
+
: { status: 'claimed', blockHeight: h, blockHash: hash, verified: false }
|
|
519
|
+
) as typeof anchor;
|
|
517
520
|
}
|
|
518
521
|
|
|
519
522
|
return {
|