hedera-curb 0.1.0 → 0.4.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # hedera-curb
2
2
 
3
- **Verifiable spend-control for Hedera AI agents.** Drop-in [Hedera Agent Kit](https://github.com/hashgraph/hedera-agent-kit-js) hooks that govern every payment your agent makes — per-task & daily budgets, a counterparty allowlist, and single-use human approval — and write every decision immutably to the Hedera Consensus Service.
3
+ **Verifiable spend-control for Hedera AI agents.** Drop-in [Hedera Agent Kit](https://github.com/hashgraph/hedera-agent-kit-js) hooks that govern every payment your agent makes — per-task & rolling-24h budgets, a counterparty allowlist, and single-use human approval — and write every decision immutably to the Hedera Consensus Service.
4
4
 
5
5
  > ProveAI proves the model; Curb proves the spend.
6
6
 
@@ -10,50 +10,68 @@
10
10
  npm i hedera-curb @hashgraph/hedera-agent-kit @hiero-ledger/sdk
11
11
  ```
12
12
 
13
- ## Use (≈12 lines)
13
+ ## Quickstart two lines
14
14
 
15
15
  ```ts
16
- import { buildCurbHooks, InMemoryCurbStore } from 'hedera-curb';
17
- import { AgentMode } from '@hashgraph/hedera-agent-kit';
18
- import { coreAccountPlugin } from '@hashgraph/hedera-agent-kit/plugins';
19
- import { HederaAIToolkit } from '@hashgraph/hedera-agent-kit-ai-sdk';
16
+ import { createCurb } from 'hedera-curb';
20
17
 
21
- const store = new InMemoryCurbStore();
22
- store.allowAccount(AGENT_ID, PROVIDER_ID); // who the agent may pay
18
+ const curb = await createCurb({ client, agentAccountId }); // auto-creates audit + HCS-2 policy topics
19
+ new HederaAIToolkit({ client, configuration: { context: { hooks: curb.hooks } } });
20
+ ```
21
+
22
+ `createCurb` defaults the store to in-memory, auto-provisions the audit topic and an HCS-2 policy registry (owner-only writes), publishes the starting policy, and returns `{ hooks, config, store, auditTopicId, policyTopicId }`. Override anything:
23
+
24
+ ```ts
25
+ await createCurb({ client, agentAccountId, perDay: 25, store: myRedisStore, auditTopicId, allowlist });
26
+ ```
27
+
28
+ ### Or provision from the terminal (no code)
23
29
 
24
- const cfg = { agentAccountId: AGENT_ID, auditTopicId: TOPIC_ID,
25
- currency: 'HBAR' as const, perTask: 10, perDay: 25, autoUnder: 3, approveUnder: 10 };
30
+ ```bash
31
+ npx hedera-curb init --network testnet
32
+ # ✓ Audit topic: 0.0.x ✓ Policy topic: 0.0.y (HCS-2, owner-only)
33
+ ```
26
34
 
27
- const toolkit = new HederaAIToolkit({ client, configuration: {
28
- plugins: [coreAccountPlugin],
29
- context: { mode: AgentMode.AUTONOMOUS, accountId: AGENT_ID,
30
- hooks: buildCurbHooks({ cfg, store }) }, // ← that's the whole integration
31
- }});
35
+ ### Advanced: wire it yourself
36
+
37
+ ```ts
38
+ import { buildCurbHooks, InMemoryCurbStore } from 'hedera-curb';
39
+ const store = new InMemoryCurbStore();
40
+ store.allowAccount(AGENT_ID, PROVIDER_ID);
41
+ const cfg = { agentAccountId: AGENT_ID, auditTopicId: TOPIC_ID, currency: 'HBAR' as const, perTask: 10, perDay: 25, autoUnder: 3, approveUnder: 10 };
42
+ // context: { hooks: buildCurbHooks({ cfg, store }) }
32
43
  ```
33
44
 
34
- Now every `transfer_hbar` the agent attempts passes the budget, allowlist, and approval policies **before** it executes, and every decision lands on your HCS audit topic.
45
+ Now every governed transfer passes the budget, allowlist, and approval policies **before** it executes, and every decision lands on your HCS audit topic.
35
46
 
36
47
  ## Production storage
37
48
 
38
- `InMemoryCurbStore` is for tests / single-instance apps. For production, implement the 6-method `CurbStore` interface against Redis, Postgres, etc. the policies stay durable and complete (never derived from a windowed read):
49
+ `InMemoryCurbStore` is for tests / single-instance apps. For production, implement the `CurbStore` interface against Redis, Postgres, etc. Spend is a rolling-24h window split into **committed** (executed) and short-lived **holds** (atomic reservations) so two concurrent payments can't both slip under the cap:
39
50
 
40
51
  ```ts
41
52
  interface CurbStore {
42
53
  isAllowed(agent, account): Promise<boolean>;
43
- getDailySpend(agent): Promise<number>;
54
+ getDailySpend(agent): Promise<number>; // committed (executed) spend
44
55
  incrDailySpend(agent, amount): Promise<void>;
56
+ tryReserveSpend(agent, key, amount, cap): Promise<boolean>; // atomic hold; idempotent per key
57
+ commitSpend(agent, key, amount): Promise<void>; // hold → committed on execution
45
58
  getApproval(requestId): Promise<'pending' | 'approved' | 'rejected' | null>;
46
59
  setApproval(requestId, status, meta?): Promise<void>;
47
60
  consumeApproval(requestId): Promise<void>;
48
61
  }
49
62
  ```
50
63
 
64
+ ## Beyond budgets
65
+
66
+ The [full project](https://github.com/Madhav-Gupta-28/Curb) is a trust ladder: off-chain hooks (this package) → HCS audit + an HCS-2 **versioned policy registry** → an on-chain **`CurbVault`** contract that enforces caps + allowlist *in Hedera consensus* → non-custodial allowance / scheduled-transaction approvals. Pick how much to trust the server.
67
+
51
68
  ## Exports
52
69
 
53
- - `SpendLimitPolicy`, `CounterpartyAllowlistPolicy`, `ApprovalTierPolicy` composable `AbstractPolicy`s.
54
- - `CurbAuditHook` — an immutable HCS record per settled payment.
70
+ - `createCurb(opts)` — one-call setup (topics, store, hooks).
55
71
  - `buildCurbHooks(deps)` — the ordered policy + audit stack for `context.hooks`.
56
- - `CurbStore` + `InMemoryCurbStore`, `extractPayment`, `writeRecord`, and the `CurbConfig` / `CurbRecord` types.
72
+ - `SpendLimitPolicy`, `CounterpartyAllowlistPolicy`, `ApprovalTierPolicy`, `CurbAuditHook`.
73
+ - `createAuditTopic`, `createPolicyRegistry`, `publishPolicyVersion`, `readPolicyVersions`, `currentPolicy` — HCS-2 policy versioning.
74
+ - `CurbStore` + `InMemoryCurbStore`, `extractPayment`, `paymentKey`, `writeRecord`, and the `CurbConfig` / `CurbRecord` types.
57
75
 
58
76
  ## License
59
77
 
package/dist/audit.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { Client } from '@hiero-ledger/sdk';
2
- import type { CurbConfig } from './config';
3
- import type { CurbRecord } from './records';
2
+ import type { CurbConfig } from './config.js';
3
+ import type { CurbRecord } from './records.js';
4
+ /**
5
+ * Create an immutable HCS audit topic. No admin key (config can't change) and no submit key
6
+ * (records come from whichever client runs the hooks; junk writes are filtered on read by decode).
7
+ */
8
+ export declare function createAuditTopic(client: Client, memo?: string): Promise<string>;
4
9
  /** Append an immutable record to the Curb audit topic on HCS. */
5
10
  export declare function writeRecord(client: Client, cfg: CurbConfig, partial: Omit<CurbRecord, 'v' | 'agent' | 'ts'>): Promise<CurbRecord>;
package/dist/audit.js CHANGED
@@ -1,10 +1,24 @@
1
- import { TopicMessageSubmitTransaction } from '@hiero-ledger/sdk';
1
+ import { TopicCreateTransaction, TopicMessageSubmitTransaction } from '@hiero-ledger/sdk';
2
+ /**
3
+ * Create an immutable HCS audit topic. No admin key (config can't change) and no submit key
4
+ * (records come from whichever client runs the hooks; junk writes are filtered on read by decode).
5
+ */
6
+ export async function createAuditTopic(client, memo = 'curb-audit') {
7
+ const receipt = await (await new TopicCreateTransaction().setTopicMemo(memo).execute(client)).getReceipt(client);
8
+ return receipt.topicId.toString();
9
+ }
2
10
  /** Append an immutable record to the Curb audit topic on HCS. */
3
11
  export async function writeRecord(client, cfg, partial) {
4
12
  const rec = { v: 1, agent: cfg.agentAccountId, ts: Date.now(), ...partial };
5
- await new TopicMessageSubmitTransaction({
6
- topicId: cfg.auditTopicId,
7
- message: JSON.stringify(rec),
8
- }).execute(client);
13
+ try {
14
+ await new TopicMessageSubmitTransaction({
15
+ topicId: cfg.auditTopicId,
16
+ message: JSON.stringify(rec),
17
+ }).execute(client);
18
+ }
19
+ catch (e) {
20
+ // never let a transient HCS hiccup break enforcement — but surface it, don't drop silently.
21
+ console.error(`[curb] audit write failed (${rec.type}/${rec.reason ?? ''}):`, e.message);
22
+ }
9
23
  return rec;
10
24
  }
@@ -1,12 +1,13 @@
1
1
  import { RejectToolPolicy } from '@hashgraph/hedera-agent-kit/policies';
2
2
  import { HcsAuditTrailHook } from '@hashgraph/hedera-agent-kit/hooks';
3
- import { SpendLimitPolicy } from './policies/spend-limit';
4
- import { CounterpartyAllowlistPolicy } from './policies/counterparty-allowlist';
5
- import { ApprovalTierPolicy } from './policies/approval-tier';
6
- import { CurbAuditHook } from './hooks/curb-audit-hook';
7
- import type { CurbDeps } from './deps';
3
+ import { SpendLimitPolicy } from './policies/spend-limit.js';
4
+ import { CounterpartyAllowlistPolicy } from './policies/counterparty-allowlist.js';
5
+ import { ApprovalTierPolicy } from './policies/approval-tier.js';
6
+ import { CurbAuditHook } from './hooks/curb-audit-hook.js';
7
+ import type { CurbDeps } from './deps.js';
8
8
  /**
9
9
  * The ordered Curb policy + audit stack, ready to drop into `configuration.context.hooks`.
10
- * Hard-disables every account-mutating / allowance tool, then governs `transfer_hbar`.
10
+ * Hard-disables account-mutating / allowance-granting tools, then governs every transfer
11
+ * (direct and allowance-based) through the budget, allowlist, and approval policies.
11
12
  */
12
13
  export declare function buildCurbHooks(d: CurbDeps): (SpendLimitPolicy | CounterpartyAllowlistPolicy | ApprovalTierPolicy | CurbAuditHook | RejectToolPolicy | HcsAuditTrailHook)[];
@@ -1,13 +1,15 @@
1
1
  import { RejectToolPolicy } from '@hashgraph/hedera-agent-kit/policies';
2
2
  import { HcsAuditTrailHook } from '@hashgraph/hedera-agent-kit/hooks';
3
3
  import { coreAccountPluginToolNames } from '@hashgraph/hedera-agent-kit/plugins';
4
- import { SpendLimitPolicy } from './policies/spend-limit';
5
- import { CounterpartyAllowlistPolicy } from './policies/counterparty-allowlist';
6
- import { ApprovalTierPolicy } from './policies/approval-tier';
7
- import { CurbAuditHook } from './hooks/curb-audit-hook';
4
+ import { GOVERNED_TRANSFER_TOOLS } from './tools.js';
5
+ import { SpendLimitPolicy } from './policies/spend-limit.js';
6
+ import { CounterpartyAllowlistPolicy } from './policies/counterparty-allowlist.js';
7
+ import { ApprovalTierPolicy } from './policies/approval-tier.js';
8
+ import { CurbAuditHook } from './hooks/curb-audit-hook.js';
8
9
  /**
9
10
  * The ordered Curb policy + audit stack, ready to drop into `configuration.context.hooks`.
10
- * Hard-disables every account-mutating / allowance tool, then governs `transfer_hbar`.
11
+ * Hard-disables account-mutating / allowance-granting tools, then governs every transfer
12
+ * (direct and allowance-based) through the budget, allowlist, and approval policies.
11
13
  */
12
14
  export function buildCurbHooks(d) {
13
15
  return [
@@ -15,14 +17,13 @@ export function buildCurbHooks(d) {
15
17
  coreAccountPluginToolNames.CREATE_ACCOUNT_TOOL,
16
18
  coreAccountPluginToolNames.DELETE_ACCOUNT_TOOL,
17
19
  coreAccountPluginToolNames.UPDATE_ACCOUNT_TOOL,
18
- coreAccountPluginToolNames.APPROVE_HBAR_ALLOWANCE_TOOL,
20
+ coreAccountPluginToolNames.APPROVE_HBAR_ALLOWANCE_TOOL, // only the user grants allowances, never the agent
19
21
  coreAccountPluginToolNames.DELETE_HBAR_ALLOWANCE_TOOL,
20
- coreAccountPluginToolNames.TRANSFER_HBAR_WITH_ALLOWANCE_TOOL,
21
22
  ]),
22
23
  new CounterpartyAllowlistPolicy(d),
23
24
  new SpendLimitPolicy(d),
24
25
  new ApprovalTierPolicy(d),
25
26
  new CurbAuditHook(d),
26
- new HcsAuditTrailHook([coreAccountPluginToolNames.TRANSFER_HBAR_TOOL], d.cfg.auditTopicId),
27
+ new HcsAuditTrailHook(GOVERNED_TRANSFER_TOOLS, d.cfg.auditTopicId),
27
28
  ];
28
29
  }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // Provisions Curb's on-chain topics without touching the Agent Kit, so it runs anywhere the SDK is present.
3
+ import { Client, PrivateKey } from '@hiero-ledger/sdk';
4
+ // explicit .js extensions so the compiled CLI runs under pure Node ESM (the bin entrypoint)
5
+ import { createAuditTopic } from './audit.js';
6
+ import { createPolicyRegistry, publishPolicyVersion } from './policy-registry.js';
7
+ import { DEFAULT_CONFIG } from './config.js';
8
+ const flag = (name) => {
9
+ const i = process.argv.indexOf(`--${name}`);
10
+ return i >= 0 ? process.argv[i + 1] : undefined;
11
+ };
12
+ function parseKey(s) {
13
+ for (const parse of [PrivateKey.fromStringECDSA, PrivateKey.fromStringED25519, PrivateKey.fromStringDer]) {
14
+ try {
15
+ return parse(s);
16
+ }
17
+ catch {
18
+ /* try next encoding */
19
+ }
20
+ }
21
+ throw new Error('could not parse the private key (expected ECDSA / ED25519 / DER hex)');
22
+ }
23
+ async function init() {
24
+ const network = (flag('network') ?? process.env.HEDERA_NETWORK ?? 'testnet').toLowerCase();
25
+ const account = flag('account') ?? process.env.HEDERA_OPERATOR_ID ?? process.env.ACCOUNT_ID;
26
+ const key = flag('key') ?? process.env.HEDERA_OPERATOR_KEY ?? process.env.PRIVATE_KEY;
27
+ const agent = flag('agent') ?? process.env.AGENT_ACCOUNT_ID ?? account;
28
+ if (!account || !key) {
29
+ console.error('Missing credentials. Pass --account and --key, or set HEDERA_OPERATOR_ID / HEDERA_OPERATOR_KEY.');
30
+ process.exit(1);
31
+ }
32
+ console.log(`Provisioning Curb on ${network}…`);
33
+ const client = Client.forName(network).setOperator(account, parseKey(key));
34
+ const auditTopicId = await createAuditTopic(client);
35
+ const policyTopicId = await createPolicyRegistry(client);
36
+ const config = {
37
+ agentAccountId: agent,
38
+ auditTopicId,
39
+ ...DEFAULT_CONFIG,
40
+ perTask: Number(flag('per-task') ?? DEFAULT_CONFIG.perTask),
41
+ perDay: Number(flag('per-day') ?? DEFAULT_CONFIG.perDay),
42
+ };
43
+ await publishPolicyVersion(client, config, policyTopicId, 'initial policy').catch(() => { });
44
+ console.log(`\n✓ Audit topic: ${auditTopicId}`);
45
+ console.log(`✓ Policy topic: ${policyTopicId} (HCS-2, owner-only writes)`);
46
+ console.log('\nAdd these to your .env:\n');
47
+ console.log(`CURB_AUDIT_TOPIC_ID=${auditTopicId}`);
48
+ console.log(`CURB_POLICY_TOPIC_ID=${policyTopicId}`);
49
+ console.log('\nDone — drop `createCurb({ client, agentAccountId }).hooks` into the Agent Kit and you are governed.');
50
+ process.exit(0);
51
+ }
52
+ const cmd = process.argv[2];
53
+ if (cmd === 'init') {
54
+ init().catch((e) => {
55
+ console.error(String(e?.message ?? e));
56
+ process.exit(1);
57
+ });
58
+ }
59
+ else {
60
+ console.log('Usage: hedera-curb init [--network testnet] [--account 0.0.x] [--key <priv>] [--agent 0.0.y] [--per-task 10] [--per-day 25]');
61
+ process.exit(cmd ? 1 : 0);
62
+ }
@@ -0,0 +1,38 @@
1
+ import type { Client } from '@hiero-ledger/sdk';
2
+ import { buildCurbHooks } from './build-hooks.js';
3
+ import { type CurbStore } from './store.js';
4
+ import { type CurbConfig, type Currency } from './config.js';
5
+ export interface CreateCurbOptions {
6
+ /** Operator client — used to create topics and write the audit trail. */
7
+ client: Client;
8
+ /** The agent's Hedera account id (the governed / bounded-allowance account). */
9
+ agentAccountId: string;
10
+ /** Bring your own store (Redis, Postgres, …). Defaults to an in-memory store. */
11
+ store?: CurbStore;
12
+ /** Reuse an existing audit topic; omit to auto-create one. */
13
+ auditTopicId?: string;
14
+ /** Reuse an existing HCS-2 policy registry; omit to auto-create one; pass `false` to skip versioning. */
15
+ policyTopicId?: string | false;
16
+ currency?: Currency;
17
+ perTask?: number;
18
+ perDay?: number;
19
+ autoUnder?: number;
20
+ approveUnder?: number;
21
+ }
22
+ export interface Curb {
23
+ /** Drop straight into the Agent Kit's `configuration.context.hooks`. */
24
+ hooks: ReturnType<typeof buildCurbHooks>;
25
+ config: CurbConfig;
26
+ store: CurbStore;
27
+ auditTopicId: string;
28
+ policyTopicId?: string;
29
+ }
30
+ /**
31
+ * One-call setup for Curb. Auto-creates the audit topic (and an HCS-2 policy registry), defaults the
32
+ * store to in-memory, publishes the initial policy version, and returns ready-to-use Agent Kit hooks.
33
+ *
34
+ * @example
35
+ * const curb = await createCurb({ client, agentAccountId, perDay: 25 });
36
+ * new HederaAIToolkit({ client, configuration: { context: { hooks: curb.hooks } } });
37
+ */
38
+ export declare function createCurb(opts: CreateCurbOptions): Promise<Curb>;
@@ -0,0 +1,38 @@
1
+ import { buildCurbHooks } from './build-hooks.js';
2
+ import { InMemoryCurbStore } from './store.js';
3
+ import { createAuditTopic } from './audit.js';
4
+ import { createPolicyRegistry, publishPolicyVersion } from './policy-registry.js';
5
+ import { DEFAULT_CONFIG } from './config.js';
6
+ /**
7
+ * One-call setup for Curb. Auto-creates the audit topic (and an HCS-2 policy registry), defaults the
8
+ * store to in-memory, publishes the initial policy version, and returns ready-to-use Agent Kit hooks.
9
+ *
10
+ * @example
11
+ * const curb = await createCurb({ client, agentAccountId, perDay: 25 });
12
+ * new HederaAIToolkit({ client, configuration: { context: { hooks: curb.hooks } } });
13
+ */
14
+ export async function createCurb(opts) {
15
+ const store = opts.store ?? new InMemoryCurbStore();
16
+ const auditTopicId = opts.auditTopicId ?? (await createAuditTopic(opts.client));
17
+ const autoPolicy = opts.policyTopicId === undefined;
18
+ const policyTopicId = opts.policyTopicId === false ? undefined : (opts.policyTopicId ?? (await createPolicyRegistry(opts.client)));
19
+ const config = {
20
+ agentAccountId: opts.agentAccountId,
21
+ auditTopicId,
22
+ currency: opts.currency ?? DEFAULT_CONFIG.currency,
23
+ perTask: opts.perTask ?? DEFAULT_CONFIG.perTask,
24
+ perDay: opts.perDay ?? DEFAULT_CONFIG.perDay,
25
+ autoUnder: opts.autoUnder ?? DEFAULT_CONFIG.autoUnder,
26
+ approveUnder: opts.approveUnder ?? DEFAULT_CONFIG.approveUnder,
27
+ };
28
+ // seed the registry with the live caps so the on-chain policy is authoritative from day one
29
+ if (policyTopicId && autoPolicy) {
30
+ try {
31
+ await publishPolicyVersion(opts.client, config, policyTopicId, 'initial policy');
32
+ }
33
+ catch {
34
+ /* non-fatal: the registry exists; versions can be published later */
35
+ }
36
+ }
37
+ return { hooks: buildCurbHooks({ cfg: config, store }), config, store, auditTopicId, policyTopicId };
38
+ }
package/dist/deps.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { CurbConfig } from './config';
2
- import type { CurbStore } from './store';
1
+ import type { CurbConfig } from './config.js';
2
+ import type { CurbStore } from './store.js';
3
3
  /** Everything every Curb policy and hook needs at construction time. */
4
4
  export interface CurbDeps {
5
5
  cfg: CurbConfig;
package/dist/extract.d.ts CHANGED
@@ -1,11 +1,14 @@
1
+ /** Deterministic key for a payment (agent + recipient + amount) — used for spend holds and approvals. */
2
+ export declare function paymentKey(agent: string, recipient: string, amount: number): string;
1
3
  export interface PaymentIntent {
2
4
  amount: number;
3
5
  currency: 'HBAR' | 'USDC';
4
6
  recipients: string[];
5
7
  }
6
8
  /**
7
- * Pull the payment amount + recipients out of a tool's normalised params.
8
- * HBAR transfers normalise to `{ hbarTransfers: [{ accountId, amount }] }` and include the
9
- * sender as a negative entry we keep only the positive credits.
9
+ * Pull the payment amount + recipients out of a governed transfer's normalised params.
10
+ * Both `transfer_hbar` and `transfer_hbar_with_allowance` normalise recipients to
11
+ * `{ hbarTransfers: [{ accountId, amount }] }` (the sender/owner is a separate negative entry).
12
+ * We keep only the positive credits, which equal the amount actually spent.
10
13
  */
11
14
  export declare function extractPayment(method: string, normalised: any): PaymentIntent | null;
package/dist/extract.js CHANGED
@@ -1,5 +1,10 @@
1
+ import crypto from 'node:crypto';
1
2
  import { Hbar } from '@hiero-ledger/sdk';
2
- import { coreAccountPluginToolNames } from '@hashgraph/hedera-agent-kit/plugins';
3
+ import { GOVERNED_TRANSFER_TOOLS } from './tools.js';
4
+ /** Deterministic key for a payment (agent + recipient + amount) — used for spend holds and approvals. */
5
+ export function paymentKey(agent, recipient, amount) {
6
+ return crypto.createHash('sha256').update(`${agent}:${recipient}:${amount}`).digest('hex').slice(0, 16);
7
+ }
3
8
  function hbarToNumber(a) {
4
9
  if (a instanceof Hbar)
5
10
  return a.toBigNumber().toNumber();
@@ -10,12 +15,13 @@ function hbarToNumber(a) {
10
15
  return Number(a?.toString?.() ?? 0);
11
16
  }
12
17
  /**
13
- * Pull the payment amount + recipients out of a tool's normalised params.
14
- * HBAR transfers normalise to `{ hbarTransfers: [{ accountId, amount }] }` and include the
15
- * sender as a negative entry we keep only the positive credits.
18
+ * Pull the payment amount + recipients out of a governed transfer's normalised params.
19
+ * Both `transfer_hbar` and `transfer_hbar_with_allowance` normalise recipients to
20
+ * `{ hbarTransfers: [{ accountId, amount }] }` (the sender/owner is a separate negative entry).
21
+ * We keep only the positive credits, which equal the amount actually spent.
16
22
  */
17
23
  export function extractPayment(method, normalised) {
18
- if (method === coreAccountPluginToolNames.TRANSFER_HBAR_TOOL) {
24
+ if (GOVERNED_TRANSFER_TOOLS.includes(method)) {
19
25
  const transfers = normalised?.hbarTransfers ?? [];
20
26
  const credits = transfers.filter((t) => hbarToNumber(t.amount) > 0);
21
27
  return {
@@ -1,5 +1,5 @@
1
1
  import { AbstractHook, type PostSecondaryActionParams } from '@hashgraph/hedera-agent-kit';
2
- import type { CurbDeps } from '../deps';
2
+ import type { CurbDeps } from '../deps.js';
3
3
  /** Records every settled payment on HCS and advances the durable daily-spend counter. */
4
4
  export declare class CurbAuditHook extends AbstractHook {
5
5
  private d;
@@ -1,7 +1,7 @@
1
1
  import { AbstractHook } from '@hashgraph/hedera-agent-kit';
2
- import { coreAccountPluginToolNames } from '@hashgraph/hedera-agent-kit/plugins';
3
- import { extractPayment } from '../extract';
4
- import { writeRecord } from '../audit';
2
+ import { GOVERNED_TRANSFER_TOOLS } from '../tools.js';
3
+ import { extractPayment, paymentKey } from '../extract.js';
4
+ import { writeRecord } from '../audit.js';
5
5
  // `toolResult` is typed `any` in the kit; these are the observed shapes for tx id.
6
6
  function extractTxId(toolResult) {
7
7
  return (toolResult?.raw?.transactionId ??
@@ -15,7 +15,7 @@ export class CurbAuditHook extends AbstractHook {
15
15
  d;
16
16
  name = 'curb.audit';
17
17
  description = 'Records every settled payment on HCS';
18
- relevantTools = [coreAccountPluginToolNames.TRANSFER_HBAR_TOOL];
18
+ relevantTools = GOVERNED_TRANSFER_TOOLS;
19
19
  constructor(d) {
20
20
  super();
21
21
  this.d = d;
@@ -34,7 +34,9 @@ export class CurbAuditHook extends AbstractHook {
34
34
  allowed: true,
35
35
  reason: 'settled',
36
36
  });
37
- if (intent?.amount)
38
- await this.d.store.incrDailySpend(this.d.cfg.agentAccountId, intent.amount);
37
+ if (intent?.amount) {
38
+ const key = paymentKey(this.d.cfg.agentAccountId, intent.recipients[0], intent.amount);
39
+ await this.d.store.commitSpend(this.d.cfg.agentAccountId, key, intent.amount);
40
+ }
39
41
  }
40
42
  }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,14 @@
1
- export * from './config';
2
- export * from './records';
3
- export * from './store';
4
- export * from './deps';
5
- export * from './extract';
6
- export * from './audit';
7
- export * from './policies/spend-limit';
8
- export * from './policies/counterparty-allowlist';
9
- export * from './policies/approval-tier';
10
- export * from './hooks/curb-audit-hook';
11
- export * from './build-hooks';
1
+ export * from './config.js';
2
+ export * from './records.js';
3
+ export * from './store.js';
4
+ export * from './deps.js';
5
+ export * from './tools.js';
6
+ export * from './extract.js';
7
+ export * from './audit.js';
8
+ export * from './policy-registry.js';
9
+ export * from './policies/spend-limit.js';
10
+ export * from './policies/counterparty-allowlist.js';
11
+ export * from './policies/approval-tier.js';
12
+ export * from './hooks/curb-audit-hook.js';
13
+ export * from './build-hooks.js';
14
+ export * from './create-curb.js';
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
- export * from './config';
2
- export * from './records';
3
- export * from './store';
4
- export * from './deps';
5
- export * from './extract';
6
- export * from './audit';
7
- export * from './policies/spend-limit';
8
- export * from './policies/counterparty-allowlist';
9
- export * from './policies/approval-tier';
10
- export * from './hooks/curb-audit-hook';
11
- export * from './build-hooks';
1
+ export * from './config.js';
2
+ export * from './records.js';
3
+ export * from './store.js';
4
+ export * from './deps.js';
5
+ export * from './tools.js';
6
+ export * from './extract.js';
7
+ export * from './audit.js';
8
+ export * from './policy-registry.js';
9
+ export * from './policies/spend-limit.js';
10
+ export * from './policies/counterparty-allowlist.js';
11
+ export * from './policies/approval-tier.js';
12
+ export * from './hooks/curb-audit-hook.js';
13
+ export * from './build-hooks.js';
14
+ export * from './create-curb.js';
@@ -1,5 +1,5 @@
1
1
  import { AbstractPolicy, type PostParamsNormalizationParams } from '@hashgraph/hedera-agent-kit';
2
- import type { CurbDeps } from '../deps';
2
+ import type { CurbDeps } from '../deps.js';
3
3
  /**
4
4
  * Tiered human-in-the-loop:
5
5
  * - below `autoUnder` → auto-approve
@@ -1,8 +1,8 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { AbstractPolicy } from '@hashgraph/hedera-agent-kit';
3
- import { coreAccountPluginToolNames } from '@hashgraph/hedera-agent-kit/plugins';
4
- import { extractPayment } from '../extract';
5
- import { writeRecord } from '../audit';
3
+ import { GOVERNED_TRANSFER_TOOLS } from '../tools.js';
4
+ import { extractPayment } from '../extract.js';
5
+ import { writeRecord } from '../audit.js';
6
6
  /**
7
7
  * Tiered human-in-the-loop:
8
8
  * - below `autoUnder` → auto-approve
@@ -13,7 +13,7 @@ export class ApprovalTierPolicy extends AbstractPolicy {
13
13
  d;
14
14
  name = 'curb.approval-tier';
15
15
  description = 'Requires single-use human approval for larger payments';
16
- relevantTools = [coreAccountPluginToolNames.TRANSFER_HBAR_TOOL];
16
+ relevantTools = GOVERNED_TRANSFER_TOOLS;
17
17
  constructor(d) {
18
18
  super();
19
19
  this.d = d;
@@ -1,5 +1,5 @@
1
1
  import { AbstractPolicy, type PostParamsNormalizationParams } from '@hashgraph/hedera-agent-kit';
2
- import type { CurbDeps } from '../deps';
2
+ import type { CurbDeps } from '../deps.js';
3
3
  /** Blocks payments to any account not on the allowlist. */
4
4
  export declare class CounterpartyAllowlistPolicy extends AbstractPolicy {
5
5
  private d;
@@ -1,13 +1,13 @@
1
1
  import { AbstractPolicy } from '@hashgraph/hedera-agent-kit';
2
- import { coreAccountPluginToolNames } from '@hashgraph/hedera-agent-kit/plugins';
3
- import { extractPayment } from '../extract';
4
- import { writeRecord } from '../audit';
2
+ import { GOVERNED_TRANSFER_TOOLS } from '../tools.js';
3
+ import { extractPayment } from '../extract.js';
4
+ import { writeRecord } from '../audit.js';
5
5
  /** Blocks payments to any account not on the allowlist. */
6
6
  export class CounterpartyAllowlistPolicy extends AbstractPolicy {
7
7
  d;
8
8
  name = 'curb.allowlist';
9
9
  description = 'Blocks payments to accounts not on the allowlist';
10
- relevantTools = [coreAccountPluginToolNames.TRANSFER_HBAR_TOOL];
10
+ relevantTools = GOVERNED_TRANSFER_TOOLS;
11
11
  constructor(d) {
12
12
  super();
13
13
  this.d = d;
@@ -1,6 +1,10 @@
1
1
  import { AbstractPolicy, type PostParamsNormalizationParams } from '@hashgraph/hedera-agent-kit';
2
- import type { CurbDeps } from '../deps';
3
- /** Blocks a payment that would breach the per-task or rolling daily budget (durable store counter). */
2
+ import type { CurbDeps } from '../deps.js';
3
+ /**
4
+ * Blocks a payment that would breach the per-task or rolling daily budget.
5
+ * The daily check is an **atomic reserve** (a short-lived hold) so two concurrent payments can't both
6
+ * slip under the cap; the hold is committed on execution by {@link CurbAuditHook}.
7
+ */
4
8
  export declare class SpendLimitPolicy extends AbstractPolicy {
5
9
  private d;
6
10
  name: string;
@@ -1,13 +1,17 @@
1
1
  import { AbstractPolicy } from '@hashgraph/hedera-agent-kit';
2
- import { coreAccountPluginToolNames } from '@hashgraph/hedera-agent-kit/plugins';
3
- import { extractPayment } from '../extract';
4
- import { writeRecord } from '../audit';
5
- /** Blocks a payment that would breach the per-task or rolling daily budget (durable store counter). */
2
+ import { GOVERNED_TRANSFER_TOOLS } from '../tools.js';
3
+ import { extractPayment, paymentKey } from '../extract.js';
4
+ import { writeRecord } from '../audit.js';
5
+ /**
6
+ * Blocks a payment that would breach the per-task or rolling daily budget.
7
+ * The daily check is an **atomic reserve** (a short-lived hold) so two concurrent payments can't both
8
+ * slip under the cap; the hold is committed on execution by {@link CurbAuditHook}.
9
+ */
6
10
  export class SpendLimitPolicy extends AbstractPolicy {
7
11
  d;
8
12
  name = 'curb.spend-limit';
9
13
  description = 'Blocks a payment that would exceed the per-task or rolling daily budget';
10
- relevantTools = [coreAccountPluginToolNames.TRANSFER_HBAR_TOOL];
14
+ relevantTools = GOVERNED_TRANSFER_TOOLS;
11
15
  constructor(d) {
12
16
  super();
13
17
  this.d = d;
@@ -17,10 +21,21 @@ export class SpendLimitPolicy extends AbstractPolicy {
17
21
  if (!intent)
18
22
  return false;
19
23
  const { cfg, store } = this.d;
20
- const already = await store.getDailySpend(cfg.agentAccountId);
21
- const overTask = intent.amount > cfg.perTask;
22
- const overDay = already + intent.amount > cfg.perDay;
23
- const block = overTask || overDay;
24
+ if (intent.amount > cfg.perTask) {
25
+ await writeRecord(params.client, cfg, {
26
+ type: 'decision',
27
+ policy: this.name,
28
+ method,
29
+ amount: intent.amount,
30
+ currency: intent.currency,
31
+ counterparty: intent.recipients[0],
32
+ allowed: false,
33
+ reason: 'per_task_exceeded',
34
+ });
35
+ return true;
36
+ }
37
+ const key = paymentKey(cfg.agentAccountId, intent.recipients[0], intent.amount);
38
+ const reserved = await store.tryReserveSpend(cfg.agentAccountId, key, intent.amount, cfg.perDay);
24
39
  await writeRecord(params.client, cfg, {
25
40
  type: 'decision',
26
41
  policy: this.name,
@@ -28,9 +43,9 @@ export class SpendLimitPolicy extends AbstractPolicy {
28
43
  amount: intent.amount,
29
44
  currency: intent.currency,
30
45
  counterparty: intent.recipients[0],
31
- allowed: !block,
32
- reason: block ? (overTask ? 'per_task_exceeded' : 'per_day_exceeded') : 'within_budget',
46
+ allowed: reserved,
47
+ reason: reserved ? 'within_budget' : 'per_day_exceeded',
33
48
  });
34
- return block;
49
+ return !reserved;
35
50
  }
36
51
  }
@@ -0,0 +1,35 @@
1
+ import { Client } from '@hiero-ledger/sdk';
2
+ import type { CurbConfig } from './config.js';
3
+ /** The policy fields that are versioned on-chain. */
4
+ export interface PolicySnapshot {
5
+ currency: string;
6
+ perTask: number;
7
+ perDay: number;
8
+ autoUnder: number;
9
+ approveUnder: number;
10
+ }
11
+ export interface PolicyVersion extends PolicySnapshot {
12
+ /** 1-based version number, in consensus order. */
13
+ version: number;
14
+ reason: string;
15
+ /** Publisher wall-clock (ms) at publish time. */
16
+ ts: number;
17
+ sequenceNumber: number;
18
+ consensusTimestamp: string;
19
+ }
20
+ /**
21
+ * Create an HCS-2 indexed registry topic for Curb policy versions. The submit key is set to the
22
+ * operator's key so ONLY the owner can publish policy versions — otherwise anyone could submit a
23
+ * fake policy (e.g. huge caps) that `resolveConfig` would adopt. Returns the topic id.
24
+ */
25
+ export declare function createPolicyRegistry(client: Client, ttl?: number): Promise<string>;
26
+ /** Publish the current config as a new immutable policy version on the registry. */
27
+ export declare function publishPolicyVersion(client: Client, cfg: CurbConfig, registryTopicId: string, reason: string): Promise<PolicySnapshot>;
28
+ /**
29
+ * Read the full policy version history from the HCS-2 registry via the mirror node (consensus order).
30
+ * Pass `trustedPayer` (the owner's account id) to only trust versions the owner published — a
31
+ * defense-in-depth against policy injection even on a topic without a submit key.
32
+ */
33
+ export declare function readPolicyVersions(registryTopicId: string, mirrorBaseUrl: string, trustedPayer?: string): Promise<PolicyVersion[]>;
34
+ /** The current (latest) policy version on the registry, or null if empty. */
35
+ export declare function currentPolicy(registryTopicId: string, mirrorBaseUrl: string, trustedPayer?: string): Promise<PolicyVersion | null>;
@@ -0,0 +1,96 @@
1
+ import { TopicCreateTransaction, TopicMessageSubmitTransaction } from '@hiero-ledger/sdk';
2
+ /**
3
+ * HCS-2 policy versioning for Curb.
4
+ *
5
+ * Curb's spending policy (caps + tiers) is published to an HCS-2 *indexed* registry topic, so every
6
+ * change is an immutable, consensus-timestamped version. Anyone can replay the topic to audit exactly
7
+ * what the limits were at any point and why they changed — the policy itself becomes verifiable, not
8
+ * just the payments it governs.
9
+ *
10
+ * Per HCS-2: topic memo `hcs-2:{registryType}:{ttl}` (0 = indexed, keep all records); each version is a
11
+ * `register` op whose `metadata` carries the policy inline as a data URI and `t_id` references the
12
+ * audit topic the policy governs.
13
+ */
14
+ const HCS2 = 'hcs-2';
15
+ const snapshot = (cfg) => ({
16
+ currency: cfg.currency,
17
+ perTask: cfg.perTask,
18
+ perDay: cfg.perDay,
19
+ autoUnder: cfg.autoUnder,
20
+ approveUnder: cfg.approveUnder,
21
+ });
22
+ /**
23
+ * Create an HCS-2 indexed registry topic for Curb policy versions. The submit key is set to the
24
+ * operator's key so ONLY the owner can publish policy versions — otherwise anyone could submit a
25
+ * fake policy (e.g. huge caps) that `resolveConfig` would adopt. Returns the topic id.
26
+ */
27
+ export async function createPolicyRegistry(client, ttl = 86400) {
28
+ const tx = new TopicCreateTransaction().setTopicMemo(`${HCS2}:0:${ttl}`);
29
+ const submitKey = client.operatorPublicKey;
30
+ if (submitKey)
31
+ tx.setSubmitKey(submitKey);
32
+ const receipt = await (await tx.execute(client)).getReceipt(client);
33
+ return receipt.topicId.toString();
34
+ }
35
+ /** Publish the current config as a new immutable policy version on the registry. */
36
+ export async function publishPolicyVersion(client, cfg, registryTopicId, reason) {
37
+ const snap = snapshot(cfg);
38
+ const payload = { ...snap, reason, ts: Date.now() };
39
+ const metadata = `data:application/json;base64,${Buffer.from(JSON.stringify(payload)).toString('base64')}`;
40
+ const message = { p: HCS2, op: 'register', t_id: cfg.auditTopicId, metadata, m: reason };
41
+ await new TopicMessageSubmitTransaction({
42
+ topicId: registryTopicId,
43
+ message: JSON.stringify(message),
44
+ }).execute(client);
45
+ return snap;
46
+ }
47
+ /**
48
+ * Read the full policy version history from the HCS-2 registry via the mirror node (consensus order).
49
+ * Pass `trustedPayer` (the owner's account id) to only trust versions the owner published — a
50
+ * defense-in-depth against policy injection even on a topic without a submit key.
51
+ */
52
+ export async function readPolicyVersions(registryTopicId, mirrorBaseUrl, trustedPayer) {
53
+ const versions = [];
54
+ let url = `${mirrorBaseUrl}/topics/${registryTopicId}/messages?order=asc&limit=100`;
55
+ let n = 0;
56
+ while (url) {
57
+ const res = await fetch(url);
58
+ if (!res.ok)
59
+ break;
60
+ const json = await res.json();
61
+ for (const m of (json.messages ?? [])) {
62
+ if (trustedPayer && m.payer_account_id !== trustedPayer)
63
+ continue; // ignore non-owner versions
64
+ try {
65
+ const msg = JSON.parse(Buffer.from(m.message, 'base64').toString('utf8'));
66
+ if (msg.p !== HCS2 || msg.op !== 'register' || typeof msg.metadata !== 'string')
67
+ continue;
68
+ const b64 = msg.metadata.startsWith('data:') ? msg.metadata.split(',')[1] : msg.metadata;
69
+ const p = JSON.parse(Buffer.from(b64, 'base64').toString('utf8'));
70
+ versions.push({
71
+ version: ++n,
72
+ currency: p.currency,
73
+ perTask: p.perTask,
74
+ perDay: p.perDay,
75
+ autoUnder: p.autoUnder,
76
+ approveUnder: p.approveUnder,
77
+ reason: p.reason ?? msg.m ?? '',
78
+ ts: p.ts ?? 0,
79
+ sequenceNumber: m.sequence_number,
80
+ consensusTimestamp: m.consensus_timestamp,
81
+ });
82
+ }
83
+ catch {
84
+ /* skip malformed entries */
85
+ }
86
+ }
87
+ const next = json.links?.next;
88
+ url = next ? new URL(next, mirrorBaseUrl).toString() : null;
89
+ }
90
+ return versions;
91
+ }
92
+ /** The current (latest) policy version on the registry, or null if empty. */
93
+ export async function currentPolicy(registryTopicId, mirrorBaseUrl, trustedPayer) {
94
+ const all = await readPolicyVersions(registryTopicId, mirrorBaseUrl, trustedPayer);
95
+ return all.length ? all[all.length - 1] : null;
96
+ }
package/dist/store.d.ts CHANGED
@@ -7,12 +7,24 @@ export interface ApprovalMeta {
7
7
  /**
8
8
  * The operational state Curb needs at runtime. Bring your own implementation
9
9
  * (Redis, Postgres, …) or use {@link InMemoryCurbStore} for tests / single-process apps.
10
- * Everything here is durable and complete — never derived from a windowed read.
10
+ *
11
+ * Spend is a rolling 24h window (matching the on-chain CurbVault) split into two parts:
12
+ * - **committed** — actually-executed payments; this is what {@link getDailySpend} returns and what
13
+ * `/verify` reconciles against the HCS audit log.
14
+ * - **holds** — short-lived in-flight reservations placed atomically by the policy so two concurrent
15
+ * payments can't both pass the cap check (the TOCTOU race). A hold becomes committed on execution,
16
+ * or expires on its own if the payment never lands.
11
17
  */
12
18
  export interface CurbStore {
13
19
  isAllowed(agent: string, account: string): Promise<boolean>;
20
+ /** Committed (executed) spend in the rolling window. */
14
21
  getDailySpend(agent: string): Promise<number>;
22
+ /** Add to committed spend directly (e.g. seeding/migration). */
15
23
  incrDailySpend(agent: string, amount: number): Promise<void>;
24
+ /** Atomically reserve a hold if committed + holds + amount ≤ cap. Idempotent per `key`. */
25
+ tryReserveSpend(agent: string, key: string, amount: number, cap: number): Promise<boolean>;
26
+ /** Convert a hold into committed spend once the payment executes. */
27
+ commitSpend(agent: string, key: string, amount: number): Promise<void>;
16
28
  getApproval(requestId: string): Promise<ApprovalStatus | null>;
17
29
  setApproval(requestId: string, status: ApprovalStatus, meta?: ApprovalMeta): Promise<void>;
18
30
  consumeApproval(requestId: string): Promise<void>;
@@ -20,14 +32,18 @@ export interface CurbStore {
20
32
  /** Zero-dependency in-memory store — great for tests and single-instance deployments. */
21
33
  export declare class InMemoryCurbStore implements CurbStore {
22
34
  private allow;
23
- private spend;
35
+ private committed;
36
+ private holds;
24
37
  private approvals;
25
- private day;
26
38
  /** Seed an allowed counterparty. */
27
39
  allowAccount(agent: string, account: string): void;
40
+ private pruneCommitted;
41
+ private pruneHolds;
28
42
  isAllowed(agent: string, account: string): Promise<boolean>;
29
43
  getDailySpend(agent: string): Promise<number>;
30
44
  incrDailySpend(agent: string, amount: number): Promise<void>;
45
+ tryReserveSpend(agent: string, key: string, amount: number, cap: number): Promise<boolean>;
46
+ commitSpend(agent: string, key: string, amount: number): Promise<void>;
31
47
  getApproval(requestId: string): Promise<ApprovalStatus | null>;
32
48
  setApproval(requestId: string, status: ApprovalStatus): Promise<void>;
33
49
  consumeApproval(requestId: string): Promise<void>;
package/dist/store.js CHANGED
@@ -1,22 +1,50 @@
1
+ const WINDOW_MS = 24 * 60 * 60 * 1000;
2
+ const HOLD_TTL_MS = 2 * 60 * 1000; // an in-flight payment resolves well within 2 min
1
3
  /** Zero-dependency in-memory store — great for tests and single-instance deployments. */
2
4
  export class InMemoryCurbStore {
3
5
  allow = new Set();
4
- spend = new Map();
6
+ committed = new Map();
7
+ holds = new Map();
5
8
  approvals = new Map();
6
- day = () => new Date().toISOString().slice(0, 10);
7
9
  /** Seed an allowed counterparty. */
8
10
  allowAccount(agent, account) {
9
11
  this.allow.add(`${agent}:${account}`);
10
12
  }
13
+ pruneCommitted(agent) {
14
+ const cutoff = Date.now() - WINDOW_MS;
15
+ const arr = (this.committed.get(agent) ?? []).filter((e) => e.ts > cutoff);
16
+ this.committed.set(agent, arr);
17
+ return arr;
18
+ }
19
+ pruneHolds(agent) {
20
+ const cutoff = Date.now() - HOLD_TTL_MS;
21
+ const arr = (this.holds.get(agent) ?? []).filter((e) => e.ts > cutoff);
22
+ this.holds.set(agent, arr);
23
+ return arr;
24
+ }
11
25
  async isAllowed(agent, account) {
12
26
  return this.allow.has(`${agent}:${account}`);
13
27
  }
14
28
  async getDailySpend(agent) {
15
- return this.spend.get(`${agent}:${this.day()}`) ?? 0;
29
+ return this.pruneCommitted(agent).reduce((s, e) => s + e.amount, 0);
16
30
  }
17
31
  async incrDailySpend(agent, amount) {
18
- const k = `${agent}:${this.day()}`;
19
- this.spend.set(k, (this.spend.get(k) ?? 0) + amount);
32
+ this.pruneCommitted(agent).push({ ts: Date.now(), amount });
33
+ }
34
+ async tryReserveSpend(agent, key, amount, cap) {
35
+ const committed = this.pruneCommitted(agent).reduce((s, e) => s + e.amount, 0);
36
+ const holds = this.pruneHolds(agent);
37
+ if (holds.some((h) => h.key === key))
38
+ return true; // idempotent — same payment re-checked
39
+ const held = holds.reduce((s, h) => s + h.amount, 0);
40
+ if (committed + held + amount > cap)
41
+ return false;
42
+ holds.push({ ts: Date.now(), key, amount });
43
+ return true;
44
+ }
45
+ async commitSpend(agent, key, amount) {
46
+ this.pruneCommitted(agent).push({ ts: Date.now(), amount });
47
+ this.holds.set(agent, (this.holds.get(agent) ?? []).filter((h) => h.key !== key));
20
48
  }
21
49
  async getApproval(requestId) {
22
50
  return this.approvals.get(requestId) ?? null;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The value-moving tools Curb governs:
3
+ * - `transfer_hbar` (custodial: agent spends its own bounded account)
4
+ * - `transfer_hbar_with_allowance` (non-custodial: agent spends the user's account, capped by an allowance)
5
+ */
6
+ export declare const GOVERNED_TRANSFER_TOOLS: string[];
package/dist/tools.js ADDED
@@ -0,0 +1,10 @@
1
+ import { coreAccountPluginToolNames } from '@hashgraph/hedera-agent-kit/plugins';
2
+ /**
3
+ * The value-moving tools Curb governs:
4
+ * - `transfer_hbar` (custodial: agent spends its own bounded account)
5
+ * - `transfer_hbar_with_allowance` (non-custodial: agent spends the user's account, capped by an allowance)
6
+ */
7
+ export const GOVERNED_TRANSFER_TOOLS = [
8
+ coreAccountPluginToolNames.TRANSFER_HBAR_TOOL,
9
+ coreAccountPluginToolNames.TRANSFER_HBAR_WITH_ALLOWANCE_TOOL,
10
+ ];
package/package.json CHANGED
@@ -1,17 +1,21 @@
1
1
  {
2
2
  "name": "hedera-curb",
3
- "version": "0.1.0",
3
+ "version": "0.4.1",
4
4
  "description": "Verifiable spend-control policies for Hedera AI agents — drop-in Hedera Agent Kit hooks for budgets, allowlists, human approval, and an immutable HCS audit trail.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
8
  "module": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
+ "bin": {
11
+ "hedera-curb": "./dist/cli.js"
12
+ },
10
13
  "exports": {
11
14
  ".": {
12
15
  "types": "./dist/index.d.ts",
13
16
  "import": "./dist/index.js"
14
- }
17
+ },
18
+ "./package.json": "./package.json"
15
19
  },
16
20
  "files": ["dist", "README.md"],
17
21
  "sideEffects": false,