hedera-curb 0.1.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/README.md +60 -0
- package/dist/audit.d.ts +5 -0
- package/dist/audit.js +10 -0
- package/dist/build-hooks.d.ts +12 -0
- package/dist/build-hooks.js +28 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +7 -0
- package/dist/deps.d.ts +7 -0
- package/dist/deps.js +1 -0
- package/dist/extract.d.ts +11 -0
- package/dist/extract.js +28 -0
- package/dist/hooks/curb-audit-hook.d.ts +11 -0
- package/dist/hooks/curb-audit-hook.js +40 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/policies/approval-tier.d.ts +16 -0
- package/dist/policies/approval-tier.js +58 -0
- package/dist/policies/counterparty-allowlist.d.ts +11 -0
- package/dist/policies/counterparty-allowlist.js +37 -0
- package/dist/policies/spend-limit.d.ts +11 -0
- package/dist/policies/spend-limit.js +36 -0
- package/dist/records.d.ts +17 -0
- package/dist/records.js +1 -0
- package/dist/store.d.ts +34 -0
- package/dist/store.js +30 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# hedera-curb
|
|
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.
|
|
4
|
+
|
|
5
|
+
> ProveAI proves the model; Curb proves the spend.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i hedera-curb @hashgraph/hedera-agent-kit @hiero-ledger/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use (≈12 lines)
|
|
14
|
+
|
|
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';
|
|
20
|
+
|
|
21
|
+
const store = new InMemoryCurbStore();
|
|
22
|
+
store.allowAccount(AGENT_ID, PROVIDER_ID); // who the agent may pay
|
|
23
|
+
|
|
24
|
+
const cfg = { agentAccountId: AGENT_ID, auditTopicId: TOPIC_ID,
|
|
25
|
+
currency: 'HBAR' as const, perTask: 10, perDay: 25, autoUnder: 3, approveUnder: 10 };
|
|
26
|
+
|
|
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
|
+
}});
|
|
32
|
+
```
|
|
33
|
+
|
|
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.
|
|
35
|
+
|
|
36
|
+
## Production storage
|
|
37
|
+
|
|
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):
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
interface CurbStore {
|
|
42
|
+
isAllowed(agent, account): Promise<boolean>;
|
|
43
|
+
getDailySpend(agent): Promise<number>;
|
|
44
|
+
incrDailySpend(agent, amount): Promise<void>;
|
|
45
|
+
getApproval(requestId): Promise<'pending' | 'approved' | 'rejected' | null>;
|
|
46
|
+
setApproval(requestId, status, meta?): Promise<void>;
|
|
47
|
+
consumeApproval(requestId): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Exports
|
|
52
|
+
|
|
53
|
+
- `SpendLimitPolicy`, `CounterpartyAllowlistPolicy`, `ApprovalTierPolicy` — composable `AbstractPolicy`s.
|
|
54
|
+
- `CurbAuditHook` — an immutable HCS record per settled payment.
|
|
55
|
+
- `buildCurbHooks(deps)` — the ordered policy + audit stack for `context.hooks`.
|
|
56
|
+
- `CurbStore` + `InMemoryCurbStore`, `extractPayment`, `writeRecord`, and the `CurbConfig` / `CurbRecord` types.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Client } from '@hiero-ledger/sdk';
|
|
2
|
+
import type { CurbConfig } from './config';
|
|
3
|
+
import type { CurbRecord } from './records';
|
|
4
|
+
/** Append an immutable record to the Curb audit topic on HCS. */
|
|
5
|
+
export declare function writeRecord(client: Client, cfg: CurbConfig, partial: Omit<CurbRecord, 'v' | 'agent' | 'ts'>): Promise<CurbRecord>;
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TopicMessageSubmitTransaction } from '@hiero-ledger/sdk';
|
|
2
|
+
/** Append an immutable record to the Curb audit topic on HCS. */
|
|
3
|
+
export async function writeRecord(client, cfg, partial) {
|
|
4
|
+
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);
|
|
9
|
+
return rec;
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RejectToolPolicy } from '@hashgraph/hedera-agent-kit/policies';
|
|
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';
|
|
8
|
+
/**
|
|
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`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildCurbHooks(d: CurbDeps): (SpendLimitPolicy | CounterpartyAllowlistPolicy | ApprovalTierPolicy | CurbAuditHook | RejectToolPolicy | HcsAuditTrailHook)[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RejectToolPolicy } from '@hashgraph/hedera-agent-kit/policies';
|
|
2
|
+
import { HcsAuditTrailHook } from '@hashgraph/hedera-agent-kit/hooks';
|
|
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';
|
|
8
|
+
/**
|
|
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`.
|
|
11
|
+
*/
|
|
12
|
+
export function buildCurbHooks(d) {
|
|
13
|
+
return [
|
|
14
|
+
new RejectToolPolicy([
|
|
15
|
+
coreAccountPluginToolNames.CREATE_ACCOUNT_TOOL,
|
|
16
|
+
coreAccountPluginToolNames.DELETE_ACCOUNT_TOOL,
|
|
17
|
+
coreAccountPluginToolNames.UPDATE_ACCOUNT_TOOL,
|
|
18
|
+
coreAccountPluginToolNames.APPROVE_HBAR_ALLOWANCE_TOOL,
|
|
19
|
+
coreAccountPluginToolNames.DELETE_HBAR_ALLOWANCE_TOOL,
|
|
20
|
+
coreAccountPluginToolNames.TRANSFER_HBAR_WITH_ALLOWANCE_TOOL,
|
|
21
|
+
]),
|
|
22
|
+
new CounterpartyAllowlistPolicy(d),
|
|
23
|
+
new SpendLimitPolicy(d),
|
|
24
|
+
new ApprovalTierPolicy(d),
|
|
25
|
+
new CurbAuditHook(d),
|
|
26
|
+
new HcsAuditTrailHook([coreAccountPluginToolNames.TRANSFER_HBAR_TOOL], d.cfg.auditTopicId),
|
|
27
|
+
];
|
|
28
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type Currency = 'HBAR' | 'USDC';
|
|
2
|
+
export interface CurbConfig {
|
|
3
|
+
/** The agent's Hedera account id (the bounded-allowance account). */
|
|
4
|
+
agentAccountId: string;
|
|
5
|
+
/** HCS topic id every policy decision is written to. */
|
|
6
|
+
auditTopicId: string;
|
|
7
|
+
currency: Currency;
|
|
8
|
+
/** Max value of a single payment (display units). */
|
|
9
|
+
perTask: number;
|
|
10
|
+
/** Rolling daily cap (display units). */
|
|
11
|
+
perDay: number;
|
|
12
|
+
/** Below this, payments auto-approve. */
|
|
13
|
+
autoUnder: number;
|
|
14
|
+
/** At/above this, approvals are flagged high-value. */
|
|
15
|
+
approveUnder: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const DEFAULT_CONFIG: Omit<CurbConfig, 'agentAccountId' | 'auditTopicId'>;
|
package/dist/config.js
ADDED
package/dist/deps.d.ts
ADDED
package/dist/deps.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface PaymentIntent {
|
|
2
|
+
amount: number;
|
|
3
|
+
currency: 'HBAR' | 'USDC';
|
|
4
|
+
recipients: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractPayment(method: string, normalised: any): PaymentIntent | null;
|
package/dist/extract.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Hbar } from '@hiero-ledger/sdk';
|
|
2
|
+
import { coreAccountPluginToolNames } from '@hashgraph/hedera-agent-kit/plugins';
|
|
3
|
+
function hbarToNumber(a) {
|
|
4
|
+
if (a instanceof Hbar)
|
|
5
|
+
return a.toBigNumber().toNumber();
|
|
6
|
+
if (typeof a === 'number')
|
|
7
|
+
return a;
|
|
8
|
+
if (typeof a === 'string')
|
|
9
|
+
return Number(a);
|
|
10
|
+
return Number(a?.toString?.() ?? 0);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
export function extractPayment(method, normalised) {
|
|
18
|
+
if (method === coreAccountPluginToolNames.TRANSFER_HBAR_TOOL) {
|
|
19
|
+
const transfers = normalised?.hbarTransfers ?? [];
|
|
20
|
+
const credits = transfers.filter((t) => hbarToNumber(t.amount) > 0);
|
|
21
|
+
return {
|
|
22
|
+
amount: credits.reduce((sum, t) => sum + hbarToNumber(t.amount), 0),
|
|
23
|
+
currency: 'HBAR',
|
|
24
|
+
recipients: credits.map((t) => String(t.accountId)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AbstractHook, type PostSecondaryActionParams } from '@hashgraph/hedera-agent-kit';
|
|
2
|
+
import type { CurbDeps } from '../deps';
|
|
3
|
+
/** Records every settled payment on HCS and advances the durable daily-spend counter. */
|
|
4
|
+
export declare class CurbAuditHook extends AbstractHook {
|
|
5
|
+
private d;
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
relevantTools: string[];
|
|
9
|
+
constructor(d: CurbDeps);
|
|
10
|
+
postToolExecutionHook(params: PostSecondaryActionParams, method: string): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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';
|
|
5
|
+
// `toolResult` is typed `any` in the kit; these are the observed shapes for tx id.
|
|
6
|
+
function extractTxId(toolResult) {
|
|
7
|
+
return (toolResult?.raw?.transactionId ??
|
|
8
|
+
toolResult?.transactionId ??
|
|
9
|
+
toolResult?.txId ??
|
|
10
|
+
toolResult?.receipt?.transactionId ??
|
|
11
|
+
undefined);
|
|
12
|
+
}
|
|
13
|
+
/** Records every settled payment on HCS and advances the durable daily-spend counter. */
|
|
14
|
+
export class CurbAuditHook extends AbstractHook {
|
|
15
|
+
d;
|
|
16
|
+
name = 'curb.audit';
|
|
17
|
+
description = 'Records every settled payment on HCS';
|
|
18
|
+
relevantTools = [coreAccountPluginToolNames.TRANSFER_HBAR_TOOL];
|
|
19
|
+
constructor(d) {
|
|
20
|
+
super();
|
|
21
|
+
this.d = d;
|
|
22
|
+
}
|
|
23
|
+
async postToolExecutionHook(params, method) {
|
|
24
|
+
if (!this.relevantTools.includes(method))
|
|
25
|
+
return;
|
|
26
|
+
const intent = extractPayment(method, params.normalisedParams);
|
|
27
|
+
await writeRecord(params.client, this.d.cfg, {
|
|
28
|
+
type: 'executed',
|
|
29
|
+
method,
|
|
30
|
+
amount: intent?.amount,
|
|
31
|
+
currency: this.d.cfg.currency,
|
|
32
|
+
counterparty: intent?.recipients[0],
|
|
33
|
+
txId: extractTxId(params.toolResult),
|
|
34
|
+
allowed: true,
|
|
35
|
+
reason: 'settled',
|
|
36
|
+
});
|
|
37
|
+
if (intent?.amount)
|
|
38
|
+
await this.d.store.incrDailySpend(this.d.cfg.agentAccountId, intent.amount);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
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';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
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';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AbstractPolicy, type PostParamsNormalizationParams } from '@hashgraph/hedera-agent-kit';
|
|
2
|
+
import type { CurbDeps } from '../deps';
|
|
3
|
+
/**
|
|
4
|
+
* Tiered human-in-the-loop:
|
|
5
|
+
* - below `autoUnder` → auto-approve
|
|
6
|
+
* - at/above → block until a human approves (payments ≥ `approveUnder` are flagged
|
|
7
|
+
* high-value). Each approval is SINGLE-USE.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ApprovalTierPolicy extends AbstractPolicy {
|
|
10
|
+
private d;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
relevantTools: string[];
|
|
14
|
+
constructor(d: CurbDeps);
|
|
15
|
+
protected shouldBlockPostParamsNormalization(params: PostParamsNormalizationParams, method: string): Promise<boolean>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
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';
|
|
6
|
+
/**
|
|
7
|
+
* Tiered human-in-the-loop:
|
|
8
|
+
* - below `autoUnder` → auto-approve
|
|
9
|
+
* - at/above → block until a human approves (payments ≥ `approveUnder` are flagged
|
|
10
|
+
* high-value). Each approval is SINGLE-USE.
|
|
11
|
+
*/
|
|
12
|
+
export class ApprovalTierPolicy extends AbstractPolicy {
|
|
13
|
+
d;
|
|
14
|
+
name = 'curb.approval-tier';
|
|
15
|
+
description = 'Requires single-use human approval for larger payments';
|
|
16
|
+
relevantTools = [coreAccountPluginToolNames.TRANSFER_HBAR_TOOL];
|
|
17
|
+
constructor(d) {
|
|
18
|
+
super();
|
|
19
|
+
this.d = d;
|
|
20
|
+
}
|
|
21
|
+
async shouldBlockPostParamsNormalization(params, method) {
|
|
22
|
+
const intent = extractPayment(method, params.normalisedParams);
|
|
23
|
+
if (!intent)
|
|
24
|
+
return false;
|
|
25
|
+
const { cfg, store } = this.d;
|
|
26
|
+
if (intent.amount < cfg.autoUnder)
|
|
27
|
+
return false; // tier 1: auto-approve
|
|
28
|
+
const requestId = crypto
|
|
29
|
+
.createHash('sha256')
|
|
30
|
+
.update(`${cfg.agentAccountId}:${intent.recipients[0]}:${intent.amount}`)
|
|
31
|
+
.digest('hex')
|
|
32
|
+
.slice(0, 16);
|
|
33
|
+
const status = await store.getApproval(requestId);
|
|
34
|
+
if (status === 'approved') {
|
|
35
|
+
await store.consumeApproval(requestId); // single-use
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (status !== 'pending') {
|
|
39
|
+
const highValue = intent.amount >= cfg.approveUnder;
|
|
40
|
+
await store.setApproval(requestId, 'pending', {
|
|
41
|
+
amount: intent.amount,
|
|
42
|
+
counterparty: intent.recipients[0],
|
|
43
|
+
method,
|
|
44
|
+
});
|
|
45
|
+
await writeRecord(params.client, cfg, {
|
|
46
|
+
type: 'approval_request',
|
|
47
|
+
policy: this.name,
|
|
48
|
+
method,
|
|
49
|
+
amount: intent.amount,
|
|
50
|
+
counterparty: intent.recipients[0],
|
|
51
|
+
allowed: false,
|
|
52
|
+
reason: highValue ? 'approval_required_high_value' : 'approval_required',
|
|
53
|
+
requestId,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AbstractPolicy, type PostParamsNormalizationParams } from '@hashgraph/hedera-agent-kit';
|
|
2
|
+
import type { CurbDeps } from '../deps';
|
|
3
|
+
/** Blocks payments to any account not on the allowlist. */
|
|
4
|
+
export declare class CounterpartyAllowlistPolicy extends AbstractPolicy {
|
|
5
|
+
private d;
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
relevantTools: string[];
|
|
9
|
+
constructor(d: CurbDeps);
|
|
10
|
+
protected shouldBlockPostParamsNormalization(params: PostParamsNormalizationParams, method: string): Promise<boolean>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 payments to any account not on the allowlist. */
|
|
6
|
+
export class CounterpartyAllowlistPolicy extends AbstractPolicy {
|
|
7
|
+
d;
|
|
8
|
+
name = 'curb.allowlist';
|
|
9
|
+
description = 'Blocks payments to accounts not on the allowlist';
|
|
10
|
+
relevantTools = [coreAccountPluginToolNames.TRANSFER_HBAR_TOOL];
|
|
11
|
+
constructor(d) {
|
|
12
|
+
super();
|
|
13
|
+
this.d = d;
|
|
14
|
+
}
|
|
15
|
+
async shouldBlockPostParamsNormalization(params, method) {
|
|
16
|
+
const intent = extractPayment(method, params.normalisedParams);
|
|
17
|
+
if (!intent)
|
|
18
|
+
return false;
|
|
19
|
+
const { cfg, store } = this.d;
|
|
20
|
+
const bad = [];
|
|
21
|
+
for (const acc of intent.recipients) {
|
|
22
|
+
if (!(await store.isAllowed(cfg.agentAccountId, acc)))
|
|
23
|
+
bad.push(acc);
|
|
24
|
+
}
|
|
25
|
+
const block = bad.length > 0;
|
|
26
|
+
await writeRecord(params.client, cfg, {
|
|
27
|
+
type: 'decision',
|
|
28
|
+
policy: this.name,
|
|
29
|
+
method,
|
|
30
|
+
amount: intent.amount,
|
|
31
|
+
counterparty: intent.recipients[0],
|
|
32
|
+
allowed: !block,
|
|
33
|
+
reason: block ? `not_allowlisted:${bad.join(',')}` : 'allowlisted',
|
|
34
|
+
});
|
|
35
|
+
return block;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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). */
|
|
4
|
+
export declare class SpendLimitPolicy extends AbstractPolicy {
|
|
5
|
+
private d;
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
relevantTools: string[];
|
|
9
|
+
constructor(d: CurbDeps);
|
|
10
|
+
protected shouldBlockPostParamsNormalization(params: PostParamsNormalizationParams, method: string): Promise<boolean>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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). */
|
|
6
|
+
export class SpendLimitPolicy extends AbstractPolicy {
|
|
7
|
+
d;
|
|
8
|
+
name = 'curb.spend-limit';
|
|
9
|
+
description = 'Blocks a payment that would exceed the per-task or rolling daily budget';
|
|
10
|
+
relevantTools = [coreAccountPluginToolNames.TRANSFER_HBAR_TOOL];
|
|
11
|
+
constructor(d) {
|
|
12
|
+
super();
|
|
13
|
+
this.d = d;
|
|
14
|
+
}
|
|
15
|
+
async shouldBlockPostParamsNormalization(params, method) {
|
|
16
|
+
const intent = extractPayment(method, params.normalisedParams);
|
|
17
|
+
if (!intent)
|
|
18
|
+
return false;
|
|
19
|
+
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
|
+
await writeRecord(params.client, cfg, {
|
|
25
|
+
type: 'decision',
|
|
26
|
+
policy: this.name,
|
|
27
|
+
method,
|
|
28
|
+
amount: intent.amount,
|
|
29
|
+
currency: intent.currency,
|
|
30
|
+
counterparty: intent.recipients[0],
|
|
31
|
+
allowed: !block,
|
|
32
|
+
reason: block ? (overTask ? 'per_task_exceeded' : 'per_day_exceeded') : 'within_budget',
|
|
33
|
+
});
|
|
34
|
+
return block;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type RecordType = 'decision' | 'executed' | 'approval_request' | 'approved' | 'rejected' | 'config' | 'allowlist';
|
|
2
|
+
/** One immutable line in the Curb audit trail (written to HCS). */
|
|
3
|
+
export interface CurbRecord {
|
|
4
|
+
v: 1;
|
|
5
|
+
type: RecordType;
|
|
6
|
+
agent: string;
|
|
7
|
+
policy?: string;
|
|
8
|
+
method?: string;
|
|
9
|
+
amount?: number;
|
|
10
|
+
currency?: string;
|
|
11
|
+
counterparty?: string;
|
|
12
|
+
allowed?: boolean;
|
|
13
|
+
reason?: string;
|
|
14
|
+
requestId?: string;
|
|
15
|
+
txId?: string;
|
|
16
|
+
ts: number;
|
|
17
|
+
}
|
package/dist/records.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ApprovalStatus = 'pending' | 'approved' | 'rejected';
|
|
2
|
+
export interface ApprovalMeta {
|
|
3
|
+
amount?: number;
|
|
4
|
+
counterparty?: string;
|
|
5
|
+
method?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The operational state Curb needs at runtime. Bring your own implementation
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
export interface CurbStore {
|
|
13
|
+
isAllowed(agent: string, account: string): Promise<boolean>;
|
|
14
|
+
getDailySpend(agent: string): Promise<number>;
|
|
15
|
+
incrDailySpend(agent: string, amount: number): Promise<void>;
|
|
16
|
+
getApproval(requestId: string): Promise<ApprovalStatus | null>;
|
|
17
|
+
setApproval(requestId: string, status: ApprovalStatus, meta?: ApprovalMeta): Promise<void>;
|
|
18
|
+
consumeApproval(requestId: string): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** Zero-dependency in-memory store — great for tests and single-instance deployments. */
|
|
21
|
+
export declare class InMemoryCurbStore implements CurbStore {
|
|
22
|
+
private allow;
|
|
23
|
+
private spend;
|
|
24
|
+
private approvals;
|
|
25
|
+
private day;
|
|
26
|
+
/** Seed an allowed counterparty. */
|
|
27
|
+
allowAccount(agent: string, account: string): void;
|
|
28
|
+
isAllowed(agent: string, account: string): Promise<boolean>;
|
|
29
|
+
getDailySpend(agent: string): Promise<number>;
|
|
30
|
+
incrDailySpend(agent: string, amount: number): Promise<void>;
|
|
31
|
+
getApproval(requestId: string): Promise<ApprovalStatus | null>;
|
|
32
|
+
setApproval(requestId: string, status: ApprovalStatus): Promise<void>;
|
|
33
|
+
consumeApproval(requestId: string): Promise<void>;
|
|
34
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Zero-dependency in-memory store — great for tests and single-instance deployments. */
|
|
2
|
+
export class InMemoryCurbStore {
|
|
3
|
+
allow = new Set();
|
|
4
|
+
spend = new Map();
|
|
5
|
+
approvals = new Map();
|
|
6
|
+
day = () => new Date().toISOString().slice(0, 10);
|
|
7
|
+
/** Seed an allowed counterparty. */
|
|
8
|
+
allowAccount(agent, account) {
|
|
9
|
+
this.allow.add(`${agent}:${account}`);
|
|
10
|
+
}
|
|
11
|
+
async isAllowed(agent, account) {
|
|
12
|
+
return this.allow.has(`${agent}:${account}`);
|
|
13
|
+
}
|
|
14
|
+
async getDailySpend(agent) {
|
|
15
|
+
return this.spend.get(`${agent}:${this.day()}`) ?? 0;
|
|
16
|
+
}
|
|
17
|
+
async incrDailySpend(agent, amount) {
|
|
18
|
+
const k = `${agent}:${this.day()}`;
|
|
19
|
+
this.spend.set(k, (this.spend.get(k) ?? 0) + amount);
|
|
20
|
+
}
|
|
21
|
+
async getApproval(requestId) {
|
|
22
|
+
return this.approvals.get(requestId) ?? null;
|
|
23
|
+
}
|
|
24
|
+
async setApproval(requestId, status) {
|
|
25
|
+
this.approvals.set(requestId, status);
|
|
26
|
+
}
|
|
27
|
+
async consumeApproval(requestId) {
|
|
28
|
+
this.approvals.delete(requestId);
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hedera-curb",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist", "README.md"],
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"keywords": ["hedera", "ai-agent", "agent-kit", "x402", "policy", "guardrails", "hcs"],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@hashgraph/hedera-agent-kit": ">=4",
|
|
26
|
+
"@hiero-ledger/sdk": ">=2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@hashgraph/hedera-agent-kit": "4.0.0",
|
|
30
|
+
"@hiero-ledger/sdk": "^2.85.0",
|
|
31
|
+
"@types/node": "^22",
|
|
32
|
+
"typescript": "^5.6.0",
|
|
33
|
+
"vitest": "^4.1.9"
|
|
34
|
+
}
|
|
35
|
+
}
|