@orangecheck/agent-core 2.3.0 → 2.5.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 +55 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +55 -3
- package/dist/index.mjs.map +1 -1
- package/dist/scope.js +6 -1
- package/dist/scope.js.map +1 -1
- package/dist/scope.mjs +6 -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 +28 -0
- package/src/scope.ts +5 -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
|
@@ -229,3 +229,31 @@ describe('pledge:create is a registered scope', () => {
|
|
|
229
229
|
expect(isSubScope(parseScope('pledge:create(max_bond_sats=3000000)'), granted)).toBe(false);
|
|
230
230
|
});
|
|
231
231
|
});
|
|
232
|
+
|
|
233
|
+
// SPEC §7.3 / §7.6 (1.4.0): the scopes the agent-stack adapters exercise.
|
|
234
|
+
describe('the adapter scopes are registered', () => {
|
|
235
|
+
for (const s of [
|
|
236
|
+
'anthropic:tool(name=invoice.create)',
|
|
237
|
+
'openai:function(name=getWeather)',
|
|
238
|
+
'vercel:tool(verb=invoice.create)',
|
|
239
|
+
'langgraph:tool(verb=search)',
|
|
240
|
+
'anthropic:tool(name=*)',
|
|
241
|
+
]) {
|
|
242
|
+
it(`strict mode accepts ${s}`, () => {
|
|
243
|
+
expect(() => validateScope(parseScope(s), { mode: 'strict' })).not.toThrow();
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
it('strict mode rejects a key the adapter does not use', () => {
|
|
248
|
+
expect(() => validateScope(parseScope('vercel:tool(name=search)'), { mode: 'strict' })).toThrow(ScopeParseError);
|
|
249
|
+
expect(() => validateScope(parseScope('anthropic:tool(verb=search)'), { mode: 'strict' })).toThrow(ScopeParseError);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('a tool grant admits only that tool, case included', () => {
|
|
253
|
+
const granted = parseScope('openai:function(name=getWeather)');
|
|
254
|
+
expect(isSubScope(parseScope('openai:function(name=getWeather)'), granted)).toBe(true);
|
|
255
|
+
expect(isSubScope(parseScope('openai:function(name=getweather)'), granted)).toBe(false);
|
|
256
|
+
expect(isSubScope(parseScope('openai:function(name=deleteAll)'), granted)).toBe(false);
|
|
257
|
+
expect(isSubScope(parseScope('anthropic:tool(name=getWeather)'), granted)).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
});
|
package/src/scope.ts
CHANGED
|
@@ -36,6 +36,11 @@ export const REGISTERED_SCOPES: Record<string, { keys: string[] }> = {
|
|
|
36
36
|
'mcp:invoke': { keys: ['server', 'tool', 'max_invocations'] },
|
|
37
37
|
// oc-pledge-protocol SPEC §7.3.
|
|
38
38
|
'pledge:create': { keys: ['max_bond_sats', 'mechanism', 'counterparty'] },
|
|
39
|
+
// The agent-stack adapters (agent-anthropic, -openai, -vercel, -langgraph).
|
|
40
|
+
'anthropic:tool': { keys: ['name'] },
|
|
41
|
+
'openai:function': { keys: ['name'] },
|
|
42
|
+
'vercel:tool': { keys: ['verb'] },
|
|
43
|
+
'langgraph:tool': { keys: ['verb'] },
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
/** Keys whose values are compared numerically for sub-scope ordering. */
|
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 {
|