@absol-labs/agent 0.4.0 → 0.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.
Files changed (49) hide show
  1. package/README.md +164 -0
  2. package/dist/capability/invocation-capability.d.ts +184 -0
  3. package/dist/capability/invocation-capability.d.ts.map +1 -0
  4. package/dist/capability/invocation-capability.js +183 -0
  5. package/dist/capability/invocation-capability.js.map +1 -0
  6. package/dist/frameworks/agentkit.js +2 -2
  7. package/dist/frameworks/agentkit.js.map +1 -1
  8. package/dist/frameworks/eliza.js +2 -2
  9. package/dist/frameworks/eliza.js.map +1 -1
  10. package/dist/frameworks/langchain.js +2 -2
  11. package/dist/frameworks/langchain.js.map +1 -1
  12. package/dist/gateway/caller-auth-gateway.d.ts +106 -0
  13. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -0
  14. package/dist/gateway/caller-auth-gateway.js +189 -0
  15. package/dist/gateway/caller-auth-gateway.js.map +1 -0
  16. package/dist/gateway/http-server.d.ts +49 -0
  17. package/dist/gateway/http-server.d.ts.map +1 -0
  18. package/dist/gateway/http-server.js +227 -0
  19. package/dist/gateway/http-server.js.map +1 -0
  20. package/dist/gateway/server-entry.d.ts +2 -0
  21. package/dist/gateway/server-entry.d.ts.map +1 -0
  22. package/dist/gateway/server-entry.js +30 -0
  23. package/dist/gateway/server-entry.js.map +1 -0
  24. package/dist/index.d.ts +5 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +5 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/mcp/server.js +2 -2
  29. package/dist/mcp/server.js.map +1 -1
  30. package/dist/sdk/invoke.d.ts +122 -0
  31. package/dist/sdk/invoke.d.ts.map +1 -0
  32. package/dist/sdk/invoke.js +158 -0
  33. package/dist/sdk/invoke.js.map +1 -0
  34. package/dist/wallet/lifecycle.d.ts +101 -0
  35. package/dist/wallet/lifecycle.d.ts.map +1 -0
  36. package/dist/wallet/lifecycle.js +57 -0
  37. package/dist/wallet/lifecycle.js.map +1 -0
  38. package/package.json +5 -2
  39. package/src/capability/invocation-capability.ts +255 -0
  40. package/src/frameworks/agentkit.ts +2 -2
  41. package/src/frameworks/eliza.ts +2 -2
  42. package/src/frameworks/langchain.ts +2 -2
  43. package/src/gateway/caller-auth-gateway.ts +328 -0
  44. package/src/gateway/http-server.ts +325 -0
  45. package/src/gateway/server-entry.ts +38 -0
  46. package/src/index.ts +81 -0
  47. package/src/mcp/server.ts +2 -2
  48. package/src/sdk/invoke.ts +313 -0
  49. package/src/wallet/lifecycle.ts +158 -0
@@ -0,0 +1,158 @@
1
+ import { MetrikClient } from "@absol-labs/sdk";
2
+ import { buildT2DeliveryProofSubmission, } from "../zktls/t2-delivery-proof.js";
3
+ import { CAPABILITY_HEADER_NAME, encodeCapabilityHeader, generateCapabilityNonce, hashInvocationPath, signInvocationCapability, DEFAULT_CAPABILITY_TTL_SECONDS, } from "../capability/invocation-capability.js";
4
+ /** Default `InvokeStreamReader`: a real, read-only `MetrikClient` (no wallet, no writes). */
5
+ export function createSdkInvokeStreamReader(options) {
6
+ const metrik = MetrikClient.baseSepolia({
7
+ escrow: options.escrowAddress,
8
+ rpcUrl: options.rpcUrl,
9
+ });
10
+ return {
11
+ async getStreamV2(streamId) {
12
+ const stream = await metrik.getStreamV2(streamId);
13
+ return {
14
+ buyer: stream.buyer,
15
+ operator: stream.operator,
16
+ serviceRef: stream.serviceRef,
17
+ status: stream.status,
18
+ expiresAt: stream.expiresAt,
19
+ };
20
+ },
21
+ };
22
+ }
23
+ export class InvokeStreamNotActiveError extends Error {
24
+ constructor(streamId, status) {
25
+ super(`stream ${streamId} is not active (status=${status})`);
26
+ this.name = "InvokeStreamNotActiveError";
27
+ }
28
+ }
29
+ export class InvokeStreamExpiredError extends Error {
30
+ constructor(streamId, expiresAt) {
31
+ super(`stream ${streamId} expired at ${expiresAt}`);
32
+ this.name = "InvokeStreamExpiredError";
33
+ }
34
+ }
35
+ export class InvokeBuyerMismatchError extends Error {
36
+ constructor(streamId, streamBuyer, signer) {
37
+ super(`stream ${streamId} buyer ${streamBuyer} does not match the signing account ${signer}`);
38
+ this.name = "InvokeBuyerMismatchError";
39
+ }
40
+ }
41
+ /**
42
+ * Builds and signs a minimal-scope `InvocationCapability` for one
43
+ * `method`+`path` call against `streamId`, WITHOUT touching the network.
44
+ */
45
+ export async function capabilityFor(streamId, request, options) {
46
+ const nowSeconds = options.nowSeconds ?? Math.floor(Date.now() / 1000);
47
+ const capability = {
48
+ streamId,
49
+ buyer: options.buyer.address,
50
+ serviceRef: options.serviceRef,
51
+ method: request.method,
52
+ pathHash: hashInvocationPath(request.path),
53
+ nonce: options.nonce ?? generateCapabilityNonce(),
54
+ expiry: nowSeconds + (options.ttlSeconds ?? DEFAULT_CAPABILITY_TTL_SECONDS),
55
+ };
56
+ const signature = await signInvocationCapability(capability, options.domain, options.buyer);
57
+ return { ...capability, signature };
58
+ }
59
+ /**
60
+ * `open()` -> `invoke()`: loads the stream, builds+signs a capability scoped
61
+ * to exactly this `method`+`path`, and calls the gateway-fronted service.
62
+ * Fails closed BEFORE any network call to the service if the stream is not
63
+ * active, is expired, or does not belong to the signing account.
64
+ */
65
+ export async function invoke(streamId, request, options) {
66
+ const stream = await options.streamReader.getStreamV2(streamId);
67
+ const nowSeconds = options.nowSeconds ?? Math.floor(Date.now() / 1000);
68
+ if (stream.buyer.toLowerCase() !== options.buyer.address.toLowerCase()) {
69
+ throw new InvokeBuyerMismatchError(streamId, stream.buyer, options.buyer.address);
70
+ }
71
+ if (stream.status !== "active") {
72
+ throw new InvokeStreamNotActiveError(streamId, stream.status);
73
+ }
74
+ if (nowSeconds >= stream.expiresAt) {
75
+ throw new InvokeStreamExpiredError(streamId, stream.expiresAt);
76
+ }
77
+ const capability = await capabilityFor(streamId, request, {
78
+ buyer: options.buyer,
79
+ domain: options.domain,
80
+ serviceRef: stream.serviceRef,
81
+ ...(options.ttlSeconds === undefined
82
+ ? {}
83
+ : { ttlSeconds: options.ttlSeconds }),
84
+ ...(options.nonce === undefined ? {} : { nonce: options.nonce }),
85
+ nowSeconds,
86
+ });
87
+ const header = encodeCapabilityHeader(capability);
88
+ const url = new URL(request.path, options.serviceBaseUrl).toString();
89
+ const fetchImpl = options.fetchImpl ?? fetch;
90
+ const response = await fetchImpl(url, {
91
+ method: request.method,
92
+ headers: {
93
+ ...(request.headers ?? {}),
94
+ [CAPABILITY_HEADER_NAME]: header,
95
+ },
96
+ ...(request.body === undefined ? {} : { body: request.body }),
97
+ });
98
+ return { response, capability, stream };
99
+ }
100
+ /**
101
+ * The T2 variant of `invoke()`: the SAME capability-authorized call the
102
+ * gateway serves is what the buyer's Reclaim attestor proves, so usage and
103
+ * delivery proof are one action (issue #63's "the invocation IS the
104
+ * delivery"). Attaches the `InvocationCapability` header to the exact request
105
+ * `buildT2DeliveryProofSubmission` proves via zkTLS, then signs the resulting
106
+ * `DeliveryReceipt` - identical evidence chain as the plain T2 flow, just
107
+ * capability-gated at the gateway.
108
+ */
109
+ export async function invokeWithT2DeliveryProof(streamId, options) {
110
+ const stream = await options.streamReader.getStreamV2(streamId);
111
+ const nowSeconds = options.nowSeconds ?? Math.floor(Date.now() / 1000);
112
+ if (stream.buyer.toLowerCase() !== options.buyer.address.toLowerCase()) {
113
+ throw new InvokeBuyerMismatchError(streamId, stream.buyer, options.buyer.address);
114
+ }
115
+ if (stream.status !== "active") {
116
+ throw new InvokeStreamNotActiveError(streamId, stream.status);
117
+ }
118
+ if (nowSeconds >= stream.expiresAt) {
119
+ throw new InvokeStreamExpiredError(streamId, stream.expiresAt);
120
+ }
121
+ const path = new URL(options.request.url).pathname;
122
+ const capability = await capabilityFor(streamId, { method: options.request.method ?? "GET", path }, {
123
+ buyer: options.buyer,
124
+ domain: options.domain,
125
+ serviceRef: stream.serviceRef,
126
+ ...(options.capabilityTtlSeconds === undefined
127
+ ? {}
128
+ : { ttlSeconds: options.capabilityTtlSeconds }),
129
+ ...(options.capabilityNonce === undefined
130
+ ? {}
131
+ : { nonce: options.capabilityNonce }),
132
+ nowSeconds,
133
+ });
134
+ const requestWithCapability = {
135
+ ...options.request,
136
+ headers: {
137
+ ...(options.request.headers ?? {}),
138
+ [CAPABILITY_HEADER_NAME]: encodeCapabilityHeader(capability),
139
+ },
140
+ };
141
+ const t2 = await buildT2DeliveryProofSubmission({
142
+ streamId,
143
+ operator: stream.operator,
144
+ serviceRef: stream.serviceRef,
145
+ intervalIndex: options.intervalIndex,
146
+ nonce: options.nonce,
147
+ buyer: options.buyer,
148
+ domain: options.domain,
149
+ request: requestWithCapability,
150
+ ...(options.issuedAt === undefined ? {} : { issuedAt: options.issuedAt }),
151
+ ...(options.mandateId === undefined
152
+ ? {}
153
+ : { mandateId: options.mandateId }),
154
+ ...(options.attestor === undefined ? {} : { attestor: options.attestor }),
155
+ });
156
+ return { capability, stream, t2 };
157
+ }
158
+ //# sourceMappingURL=invoke.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoke.js","sourceRoot":"","sources":["../../src/sdk/invoke.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ import { erc20Abi, type Address } from "viem";
2
+ /**
3
+ * Agent wallet lifecycle helpers (metrik-agent#65).
4
+ *
5
+ * `../wallet/provider.ts` already resolves a wallet (injected key or a
6
+ * Coinbase CDP Server Wallet v2 account) and hands back a viem `Account` that
7
+ * can sign directly — `resolveAgentWallet()`'s CDP path is create-OR-restore
8
+ * by construction (`cdp.evm.getOrCreateAccount({ name })` is idempotent: the
9
+ * same `ownerName` always resolves to the same on-chain address, so a second
10
+ * run with the same env "restores" rather than creates a new wallet).
11
+ *
12
+ * This module adds the remaining thin, non-custodial lifecycle pieces the
13
+ * issue asks for: reading balances, a testnet faucet hint, and requesting
14
+ * funds from the CDP faucet. It deliberately does NOT wrap signing — a
15
+ * `ResolvedAgentWallet.account` is already a plain viem `Account`
16
+ * (`signMessage` / `signTypedData` / `signTransaction` all work as-is), and
17
+ * adding a second signing surface here would be exactly the "second wallet
18
+ * system" AGENTS.md warns against.
19
+ */
20
+ /** Native gas balance + USDC balance for one address. */
21
+ export interface WalletBalances {
22
+ /** Native token balance (ETH on Base/Base Sepolia), in wei. */
23
+ readonly nativeWei: bigint;
24
+ /** USDC balance, in atomic units (6 decimals). */
25
+ readonly usdcAtomic: bigint;
26
+ }
27
+ /** The subset of a viem `PublicClient` balance reads need. Duck-typed so any real client satisfies it. */
28
+ export interface BalanceReader {
29
+ getBalance(args: {
30
+ readonly address: Address;
31
+ }): Promise<bigint>;
32
+ readContract(args: {
33
+ readonly address: Address;
34
+ readonly abi: typeof erc20Abi;
35
+ readonly functionName: "balanceOf";
36
+ readonly args: readonly [Address];
37
+ }): Promise<unknown>;
38
+ }
39
+ export interface GetWalletBalancesOptions {
40
+ readonly publicClient: BalanceReader;
41
+ readonly address: Address;
42
+ /** The USDC token contract to read the ERC-20 balance from. */
43
+ readonly usdc: Address;
44
+ }
45
+ /** Reads a wallet's native gas balance and USDC balance in one call. Read-only — no signing, no writes. */
46
+ export declare function getWalletBalances(options: GetWalletBalancesOptions): Promise<WalletBalances>;
47
+ /** Human-facing pointers to fund a freshly created/restored testnet wallet. */
48
+ export interface FaucetHint {
49
+ readonly network: string;
50
+ /** Coinbase CDP faucet — funds native ETH (and, on some networks, USDC) for a CDP-managed address. */
51
+ readonly cdpFaucetUrl: string;
52
+ /** Circle's own Base Sepolia USDC faucet — an alternative USDC source that doesn't require a CDP account. */
53
+ readonly usdcFaucetUrl: string;
54
+ /** How to request the same funds programmatically from a CDP wallet — see {@link requestCdpFaucet}. */
55
+ readonly programmaticHint: string;
56
+ }
57
+ export declare class UnsupportedFaucetChainError extends Error {
58
+ constructor(chainId: number);
59
+ }
60
+ /** A faucet hint for the given chain. Only Base Sepolia (84532) is supported — Metrik is testnet-only, single-chain. */
61
+ export declare function faucetHint(chainId: number): FaucetHint;
62
+ export type FaucetToken = "eth" | "usdc";
63
+ /** The subset of the Coinbase CDP client the faucet request needs. Duck-typed for testability. */
64
+ export interface CdpFaucetClientLike {
65
+ readonly evm: {
66
+ requestFaucet(options: {
67
+ readonly address: Address;
68
+ readonly network: string;
69
+ readonly token: FaucetToken;
70
+ }): Promise<{
71
+ readonly transactionHash: string;
72
+ }>;
73
+ };
74
+ }
75
+ export interface RequestCdpFaucetOptions {
76
+ readonly cdp: CdpFaucetClientLike;
77
+ readonly address: Address;
78
+ readonly token: FaucetToken;
79
+ /** Default `"base-sepolia"`. */
80
+ readonly network?: string;
81
+ }
82
+ export interface FaucetRequestResult {
83
+ /** `true` iff the faucet accepted the request (a tx hash was returned). */
84
+ readonly requested: boolean;
85
+ readonly txHash?: string;
86
+ /**
87
+ * Set when `requested` is `false`. The CDP faucet enforces a 24h per-address
88
+ * rate limit, so a failure here is an EXPECTED, non-fatal outcome — never
89
+ * thrown. Callers should log this and fall back to checking whether the
90
+ * balance is already sufficient.
91
+ */
92
+ readonly error?: string;
93
+ }
94
+ /**
95
+ * Requests testnet funds from the Coinbase CDP faucet for a CDP-managed
96
+ * address. Tolerates the faucet's 24h rate limit and any other request
97
+ * failure by returning `{ requested: false, error }` — it never throws, so a
98
+ * caller can always fall through to reading the current balance instead.
99
+ */
100
+ export declare function requestCdpFaucet(options: RequestCdpFaucetOptions): Promise<FaucetRequestResult>;
101
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../src/wallet/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,OAAO,EAAE,MAAM,MAAM,CAAC;AAE9C;;;;;;;;;;;;;;;;;GAiBG;AAEH,yDAAyD;AACzD,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,kDAAkD;IAClD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,0GAA0G;AAC1G,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjE,YAAY,CAAC,IAAI,EAAE;QACjB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;QAC1B,QAAQ,CAAC,GAAG,EAAE,OAAO,QAAQ,CAAC;QAC9B,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC;QACnC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;KACnC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,2GAA2G;AAC3G,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,cAAc,CAAC,CAWzB;AAED,+EAA+E;AAC/E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sGAAsG;IACtG,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,6GAA6G;IAC7G,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,uGAAuG;IACvG,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAM5B;AAYD,wHAAwH;AACxH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAKtD;AAED,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,CAAC;AAEzC,kGAAkG;AAClG,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,GAAG,EAAE;QACZ,aAAa,CAAC,OAAO,EAAE;YACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;YAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;YACzB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;SAC7B,GAAG,OAAO,CAAC;YAAE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACnD,CAAC;CACH;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,GAAG,EAAE,mBAAmB,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,gCAAgC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IAClC,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CAc9B"}
@@ -0,0 +1,57 @@
1
+ import { erc20Abi } from "viem";
2
+ /** Reads a wallet's native gas balance and USDC balance in one call. Read-only — no signing, no writes. */
3
+ export async function getWalletBalances(options) {
4
+ const [nativeWei, usdcAtomic] = await Promise.all([
5
+ options.publicClient.getBalance({ address: options.address }),
6
+ options.publicClient.readContract({
7
+ address: options.usdc,
8
+ abi: erc20Abi,
9
+ functionName: "balanceOf",
10
+ args: [options.address],
11
+ }),
12
+ ]);
13
+ return { nativeWei, usdcAtomic: usdcAtomic };
14
+ }
15
+ export class UnsupportedFaucetChainError extends Error {
16
+ constructor(chainId) {
17
+ super(`no known testnet faucet for chainId ${chainId} (Metrik is Base Sepolia (84532) only)`);
18
+ this.name = "UnsupportedFaucetChainError";
19
+ }
20
+ }
21
+ const BASE_SEPOLIA_CHAIN_ID = 84532;
22
+ const BASE_SEPOLIA_FAUCET_HINT = {
23
+ network: "base-sepolia",
24
+ cdpFaucetUrl: "https://portal.cdp.coinbase.com/products/faucet",
25
+ usdcFaucetUrl: "https://faucet.circle.com",
26
+ programmaticHint: "requestCdpFaucet({ cdp, address, token: 'eth' | 'usdc' }) — subject to a 24h per-address rate limit",
27
+ };
28
+ /** A faucet hint for the given chain. Only Base Sepolia (84532) is supported — Metrik is testnet-only, single-chain. */
29
+ export function faucetHint(chainId) {
30
+ if (chainId !== BASE_SEPOLIA_CHAIN_ID) {
31
+ throw new UnsupportedFaucetChainError(chainId);
32
+ }
33
+ return BASE_SEPOLIA_FAUCET_HINT;
34
+ }
35
+ /**
36
+ * Requests testnet funds from the Coinbase CDP faucet for a CDP-managed
37
+ * address. Tolerates the faucet's 24h rate limit and any other request
38
+ * failure by returning `{ requested: false, error }` — it never throws, so a
39
+ * caller can always fall through to reading the current balance instead.
40
+ */
41
+ export async function requestCdpFaucet(options) {
42
+ try {
43
+ const result = await options.cdp.evm.requestFaucet({
44
+ address: options.address,
45
+ network: options.network ?? "base-sepolia",
46
+ token: options.token,
47
+ });
48
+ return { requested: true, txHash: result.transactionHash };
49
+ }
50
+ catch (error) {
51
+ return {
52
+ requested: false,
53
+ error: error instanceof Error ? error.message : String(error),
54
+ };
55
+ }
56
+ }
57
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../src/wallet/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAgB,MAAM,MAAM,CAAC;AA+C9C,2GAA2G;AAC3G,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAiC;IAEjC,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAChD,OAAO,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;QAC7D,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC;YAChC,OAAO,EAAE,OAAO,CAAC,IAAI;YACrB,GAAG,EAAE,QAAQ;YACb,YAAY,EAAE,WAAW;YACzB,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;SACxB,CAAC;KACH,CAAC,CAAC;IACH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,UAAoB,EAAE,CAAC;AACzD,CAAC;AAaD,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IACpD,YAAY,OAAe;QACzB,KAAK,CACH,uCAAuC,OAAO,wCAAwC,CACvF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;IAC5C,CAAC;CACF;AAED,MAAM,qBAAqB,GAAG,KAAK,CAAC;AAEpC,MAAM,wBAAwB,GAAe;IAC3C,OAAO,EAAE,cAAc;IACvB,YAAY,EAAE,iDAAiD;IAC/D,aAAa,EAAE,2BAA2B;IAC1C,gBAAgB,EACd,qGAAqG;CACxG,CAAC;AAEF,wHAAwH;AACxH,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,IAAI,OAAO,KAAK,qBAAqB,EAAE,CAAC;QACtC,MAAM,IAAI,2BAA2B,CAAC,OAAO,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,wBAAwB,CAAC;AAClC,CAAC;AAoCD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAgC;IAEhC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC;YACjD,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,cAAc;YAC1C,KAAK,EAAE,OAAO,CAAC,KAAK;SACrB,CAAC,CAAC;QACH,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,eAAe,EAAE,CAAC;IAC7D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;SAC9D,CAAC;IACJ,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absol-labs/agent",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Metrik agent layer: x402 verified-streaming payments, an MCP server, framework tools, and spend mandates so AI agents can hire and pay verified services safely.",
5
5
  "license": "MIT",
6
6
  "author": "Absol Labs",
@@ -66,7 +66,7 @@
66
66
  "pnpm": "9.15.x"
67
67
  },
68
68
  "dependencies": {
69
- "@absol-labs/sdk": "^0.4.0",
69
+ "@absol-labs/sdk": "^0.7.0",
70
70
  "@absol-labs/shared": "^0.11.0",
71
71
  "@coinbase/cdp-sdk": "^1.51.2",
72
72
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -108,9 +108,12 @@
108
108
  "build:e2e": "tsc -p tsconfig.e2e.build.json",
109
109
  "mcp:stdio": "tsx src/mcp/stdio.ts",
110
110
  "mcp:http": "tsx src/mcp/http-server.ts",
111
+ "gateway": "tsx src/gateway/server-entry.ts",
111
112
  "e2e:cdp": "node dist-e2e/scripts/e2e-cdp.js",
113
+ "e2e:hire": "tsx scripts/e2e-hire.ts",
112
114
  "l2:proof": "node scripts/l2-proof.mjs",
113
115
  "test": "vitest run",
116
+ "test:package": "node scripts/test-packed-package.mjs",
114
117
  "typecheck": "tsc -p tsconfig.json --noEmit",
115
118
  "typecheck:e2e": "tsc -p tsconfig.e2e.json --noEmit",
116
119
  "lint": "tsc -p tsconfig.json --noEmit",
@@ -0,0 +1,255 @@
1
+ import { randomBytes } from "node:crypto";
2
+
3
+ import {
4
+ getAddress,
5
+ isAddress,
6
+ isHex,
7
+ keccak256,
8
+ recoverTypedDataAddress,
9
+ stringToBytes,
10
+ type Address,
11
+ type Hex,
12
+ type LocalAccount,
13
+ } from "viem";
14
+ import { z } from "zod";
15
+
16
+ /**
17
+ * `InvocationCapability` — the stream-bound EIP-712 credential that closes the
18
+ * "pay -> use" loop (metrik-protocol#62/#63/#64).
19
+ *
20
+ * A buyer who has opened an active, funded stream signs one of these per
21
+ * service call: "I, `buyer`, authorize exactly `method` `pathHash` against
22
+ * `serviceRef` under stream `streamId`, once, before `expiry`." The seller's
23
+ * gateway (`../gateway/caller-auth-gateway.ts`) recovers the signer, cross
24
+ * checks it against the on-chain stream, and enforces the nonce/expiry/route
25
+ * binding before proxying the call upstream.
26
+ *
27
+ * Deliberately kept LOCAL to `@absol-labs/agent` for this MVP slice (per
28
+ * metrik-agent#64) rather than added to `@absol-labs/shared` - it is never
29
+ * read on-chain (no contract parses it), so there is no cross-repo byte
30
+ * compatibility requirement the way there is for `DeliveryReceipt`.
31
+ */
32
+
33
+ const addressSchema = z
34
+ .string()
35
+ .refine((value) => isAddress(value), "must be an EVM address");
36
+ const bytes32Schema = z
37
+ .string()
38
+ .refine(
39
+ (value) => isHex(value, { strict: true }) && value.length === 66,
40
+ "must be bytes32 hex",
41
+ );
42
+ const signatureSchema = z
43
+ .string()
44
+ .refine(
45
+ (value) => isHex(value, { strict: true }) && value.length === 132,
46
+ "must be a 65-byte signature",
47
+ );
48
+
49
+ /** HTTP methods a capability may authorize. */
50
+ export const httpMethodSchema = z.enum([
51
+ "GET",
52
+ "POST",
53
+ "PUT",
54
+ "PATCH",
55
+ "DELETE",
56
+ "HEAD",
57
+ "OPTIONS",
58
+ ]);
59
+ export type HttpMethod = z.infer<typeof httpMethodSchema>;
60
+
61
+ export const invocationCapabilitySchema = z.object({
62
+ /** The on-chain `StreamEscrowV2` stream this capability draws authority from. */
63
+ streamId: bytes32Schema,
64
+ /** Must equal `stream.buyer` and the EIP-712 signer. */
65
+ buyer: addressSchema,
66
+ /** Must equal the target service's `serviceRef` (and the stream's). */
67
+ serviceRef: bytes32Schema,
68
+ /** The single HTTP method this capability authorizes. */
69
+ method: httpMethodSchema,
70
+ /** `keccak256` of the exact request path this capability authorizes. */
71
+ pathHash: bytes32Schema,
72
+ /** Single-use anti-replay nonce. */
73
+ nonce: bytes32Schema,
74
+ /** Unix seconds after which the capability is no longer valid. */
75
+ expiry: z.number().int().positive(),
76
+ });
77
+ export type InvocationCapability = z.infer<typeof invocationCapabilitySchema>;
78
+
79
+ export const signedInvocationCapabilitySchema =
80
+ invocationCapabilitySchema.extend({
81
+ signature: signatureSchema,
82
+ });
83
+ export type SignedInvocationCapability = z.infer<
84
+ typeof signedInvocationCapabilitySchema
85
+ >;
86
+
87
+ /** Chain-bound domain: a capability for one chain/escrow can never be replayed against another. */
88
+ export interface InvocationCapabilityDomainInput {
89
+ readonly chainId: number;
90
+ /** The `StreamEscrowV2` the stream settles on. */
91
+ readonly verifyingContract: Address;
92
+ }
93
+
94
+ export const invocationCapabilityDomainName =
95
+ "Metrik Invocation Capability" as const;
96
+ export const invocationCapabilityDomainVersion = "1" as const;
97
+
98
+ export const invocationCapabilityTypedData = {
99
+ InvocationCapability: [
100
+ { name: "streamId", type: "bytes32" },
101
+ { name: "buyer", type: "address" },
102
+ { name: "serviceRef", type: "bytes32" },
103
+ { name: "method", type: "string" },
104
+ { name: "pathHash", type: "bytes32" },
105
+ { name: "nonce", type: "bytes32" },
106
+ { name: "expiry", type: "uint64" },
107
+ ],
108
+ } as const;
109
+
110
+ /** Default single-use lifetime for a freshly-issued capability. Deliberately short. */
111
+ export const DEFAULT_CAPABILITY_TTL_SECONDS = 60;
112
+
113
+ /** Header a buyer's request carries the signed capability in. */
114
+ export const CAPABILITY_HEADER_NAME = "x-metrik-capability";
115
+
116
+ export function createInvocationCapabilityEip712Domain(
117
+ domain: InvocationCapabilityDomainInput,
118
+ ) {
119
+ return {
120
+ name: invocationCapabilityDomainName,
121
+ version: invocationCapabilityDomainVersion,
122
+ chainId: domain.chainId,
123
+ verifyingContract: getAddress(domain.verifyingContract),
124
+ } as const;
125
+ }
126
+
127
+ /**
128
+ * `keccak256` of the exact request path this capability authorizes (e.g.
129
+ * `/v1/infer`). Path only - no scheme/host/query/fragment - so the signer and
130
+ * the gateway hash the identical canonical string regardless of how each side
131
+ * received the full URL.
132
+ */
133
+ export function hashInvocationPath(path: string): Hex {
134
+ return keccak256(stringToBytes(normalizeInvocationPath(path)));
135
+ }
136
+
137
+ /** Strips query/fragment and any trailing slash (except the root) for a stable canonical form. */
138
+ export function normalizeInvocationPath(path: string): string {
139
+ const withoutFragment = path.split("#")[0] ?? "";
140
+ const withoutQuery = withoutFragment.split("?")[0] ?? "";
141
+ if (withoutQuery.length === 0) {
142
+ return "/";
143
+ }
144
+ if (withoutQuery.length > 1 && withoutQuery.endsWith("/")) {
145
+ return withoutQuery.slice(0, -1);
146
+ }
147
+ return withoutQuery;
148
+ }
149
+
150
+ function invocationCapabilityMessage(capability: InvocationCapability) {
151
+ const parsed = invocationCapabilitySchema.parse(capability);
152
+ return {
153
+ streamId: parsed.streamId as Hex,
154
+ buyer: getAddress(parsed.buyer),
155
+ serviceRef: parsed.serviceRef as Hex,
156
+ method: parsed.method,
157
+ pathHash: parsed.pathHash as Hex,
158
+ nonce: parsed.nonce as Hex,
159
+ expiry: BigInt(parsed.expiry),
160
+ };
161
+ }
162
+
163
+ export function buildInvocationCapabilityTypedData(
164
+ capability: InvocationCapability,
165
+ domain: InvocationCapabilityDomainInput,
166
+ ) {
167
+ return {
168
+ domain: createInvocationCapabilityEip712Domain(domain),
169
+ types: invocationCapabilityTypedData,
170
+ primaryType: "InvocationCapability" as const,
171
+ message: invocationCapabilityMessage(capability),
172
+ };
173
+ }
174
+
175
+ /** Signs an `InvocationCapability` with the buyer's own wallet. */
176
+ export async function signInvocationCapability(
177
+ capability: InvocationCapability,
178
+ domain: InvocationCapabilityDomainInput,
179
+ account: LocalAccount,
180
+ ): Promise<Hex> {
181
+ const typedData = buildInvocationCapabilityTypedData(capability, domain);
182
+ return account.signTypedData(typedData);
183
+ }
184
+
185
+ /** Recovers the EIP-712 signer of a capability. Throws on a malformed signature. */
186
+ export async function recoverInvocationCapabilitySigner(
187
+ capability: InvocationCapability,
188
+ domain: InvocationCapabilityDomainInput,
189
+ signature: Hex,
190
+ ): Promise<Address> {
191
+ const typedData = buildInvocationCapabilityTypedData(capability, domain);
192
+ return recoverTypedDataAddress({ ...typedData, signature });
193
+ }
194
+
195
+ /** Cryptographically random 32-byte single-use nonce. */
196
+ export function generateCapabilityNonce(): Hex {
197
+ return `0x${randomBytes(32).toString("hex")}` as Hex;
198
+ }
199
+
200
+ export class InvalidCapabilityHeaderError extends Error {
201
+ constructor(message: string, options?: { readonly cause?: unknown }) {
202
+ super(message, options);
203
+ this.name = "InvalidCapabilityHeaderError";
204
+ }
205
+ }
206
+
207
+ /** Encodes a signed capability as an opaque, header-safe (base64url) string. */
208
+ export function encodeCapabilityHeader(
209
+ signed: SignedInvocationCapability,
210
+ ): string {
211
+ const parsed = signedInvocationCapabilitySchema.parse(signed);
212
+ const json = JSON.stringify({
213
+ streamId: parsed.streamId,
214
+ buyer: parsed.buyer,
215
+ serviceRef: parsed.serviceRef,
216
+ method: parsed.method,
217
+ pathHash: parsed.pathHash,
218
+ nonce: parsed.nonce,
219
+ expiry: parsed.expiry,
220
+ signature: parsed.signature,
221
+ });
222
+ return Buffer.from(json, "utf8").toString("base64url");
223
+ }
224
+
225
+ /** Decodes and schema-validates a capability header value. Fails closed on any malformation. */
226
+ export function decodeCapabilityHeader(
227
+ headerValue: string,
228
+ ): SignedInvocationCapability {
229
+ let json: string;
230
+ try {
231
+ json = Buffer.from(headerValue, "base64url").toString("utf8");
232
+ } catch (cause) {
233
+ throw new InvalidCapabilityHeaderError(
234
+ "malformed base64 capability header",
235
+ { cause },
236
+ );
237
+ }
238
+
239
+ let raw: unknown;
240
+ try {
241
+ raw = JSON.parse(json);
242
+ } catch (cause) {
243
+ throw new InvalidCapabilityHeaderError("malformed JSON capability header", {
244
+ cause,
245
+ });
246
+ }
247
+
248
+ const result = signedInvocationCapabilitySchema.safeParse(raw);
249
+ if (!result.success) {
250
+ throw new InvalidCapabilityHeaderError(
251
+ `invalid capability shape: ${result.error.message}`,
252
+ );
253
+ }
254
+ return result.data;
255
+ }
@@ -365,7 +365,7 @@ export class MetrikActionProvider extends ActionProvider<WalletProvider> {
365
365
  @CreateAction({
366
366
  name: "check_stream_status",
367
367
  description:
368
- "Read a Metrik stream's status: verified accrued amount, whether it is active or paused (two failed checks auto-pause), claimable-by-seller, and reclaimable-by-buyer USDC.",
368
+ "Read a Metrik stream's status, verified accrued amount, claimable-by-seller, and reclaimable-by-buyer USDC. In V2, failed or unproven intervals do not advance cumulative entitlement; the stream remains active until buyer close or expiry.",
369
369
  schema: checkStreamStatusSchema,
370
370
  })
371
371
  async checkStreamStatus(
@@ -390,7 +390,7 @@ export class MetrikActionProvider extends ActionProvider<WalletProvider> {
390
390
  @CreateAction({
391
391
  name: "reclaim_unspent",
392
392
  description:
393
- "Reclaim unspent escrowed USDC from a Metrik stream back to the buyer. Set closeFirst to close an active stream before reclaiming. Runs under the same signed mandate controls.",
393
+ "Reclaim unspent escrowed USDC from a Metrik stream back to the buyer. Set closeFirst to close an active stream before reclaiming. V2 reclaim follows checkpoint finalization or escape-window rules. Runs under the same signed mandate controls.",
394
394
  schema: reclaimUnspentSchema,
395
395
  })
396
396
  async reclaimUnspent(
@@ -368,7 +368,7 @@ function createStatusAction(agentClient: MetrikElizaClient): Action {
368
368
  "is the service still delivering",
369
369
  ],
370
370
  description:
371
- "Read the current Metrik stream status verified accrual, active/paused state (two failed checks auto-pause), and claimable/reclaimable USDC plus whether the agent should stop work.",
371
+ "Read the current Metrik stream status, verified accrual, and claimable/reclaimable USDC, plus whether the agent should stop work. In V2, failed or unproven intervals do not advance cumulative entitlement; buyer close or expiry ends the session.",
372
372
  examples: createExamples("CHECK_STREAM_STATUS"),
373
373
  validate: async (_runtime, message) =>
374
374
  canParseElizaActionInput(message.content, "CHECK_STREAM_STATUS"),
@@ -408,7 +408,7 @@ function createReclaimAction(
408
408
  "refund the remaining balance",
409
409
  ],
410
410
  description:
411
- "Reclaim unspent escrowed USDC from a verified stream back to the buyer under the original signed mandate. Set closeFirst to close an active stream before reclaiming.",
411
+ "Reclaim unspent escrowed USDC from a verified stream back to the buyer under the original signed mandate. Set closeFirst to close an active stream before reclaiming. V2 reclaim follows checkpoint finalization or escape-window rules.",
412
412
  examples: createExamples("RECLAIM_UNSPENT"),
413
413
  validate: async (_runtime, message) =>
414
414
  canParseElizaActionInput(message.content, "RECLAIM_UNSPENT"),