@agentguard-run/spend 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/LICENSE +63 -0
- package/PATENTS.md +16 -0
- package/README.md +80 -0
- package/dist/cost-table.d.ts +53 -0
- package/dist/cost-table.d.ts.map +1 -0
- package/dist/cost-table.js +108 -0
- package/dist/cost-table.js.map +1 -0
- package/dist/decision-log.d.ts +98 -0
- package/dist/decision-log.d.ts.map +1 -0
- package/dist/decision-log.js +233 -0
- package/dist/decision-log.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +47 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +239 -0
- package/dist/policy.js.map +1 -0
- package/dist/spend-guard.d.ts +108 -0
- package/dist/spend-guard.d.ts.map +1 -0
- package/dist/spend-guard.js +154 -0
- package/dist/spend-guard.js.map +1 -0
- package/dist/store-memory.d.ts +28 -0
- package/dist/store-memory.d.ts.map +1 -0
- package/dist/store-memory.js +70 -0
- package/dist/store-memory.js.map +1 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
AgentGuard(TM) Spend SDK — Alpha License
|
|
2
|
+
Copyright (c) 2026 Dunecrest Ventures Inc.
|
|
3
|
+
|
|
4
|
+
1. SCOPE.
|
|
5
|
+
This software, including all files under packages/agentguard-spend/src/, is
|
|
6
|
+
licensed for internal evaluation use only by the recipient identified in a
|
|
7
|
+
separate written agreement with Dunecrest Ventures Inc. ("Licensor").
|
|
8
|
+
Production use, redistribution, sublicensing, public hosting, and republication
|
|
9
|
+
require a separate commercial agreement with Licensor.
|
|
10
|
+
|
|
11
|
+
2. NO PATENT LICENSE GRANTED.
|
|
12
|
+
Nothing in this License grants, expressly or by implication, any patent license
|
|
13
|
+
to any patent, patent application, or other intellectual property right of
|
|
14
|
+
Licensor. All patent rights are expressly reserved. The patent applications
|
|
15
|
+
identified in Section 7 are not licensed by this License.
|
|
16
|
+
|
|
17
|
+
3. SEPARATE GRANT FOR DEMONSTRATION ASSETS.
|
|
18
|
+
The following assets, and ONLY these assets, are released under the Apache
|
|
19
|
+
License, Version 2.0, the text of which is reproduced or available at
|
|
20
|
+
https://www.apache.org/licenses/LICENSE-2.0:
|
|
21
|
+
|
|
22
|
+
- The test vectors under packages/agentguard-spend/test-vectors/
|
|
23
|
+
- The documentation examples under packages/agentguard-spend/examples/
|
|
24
|
+
- The contents of packages/agentguard-spend/README.md
|
|
25
|
+
|
|
26
|
+
The source code under packages/agentguard-spend/src/ is NOT included in this
|
|
27
|
+
Apache License 2.0 grant. The TypeScript type definitions, policy engine,
|
|
28
|
+
decision log, store implementation, cost table, and wrapper code under src/
|
|
29
|
+
are licensed only under the alpha evaluation terms of Section 1 above.
|
|
30
|
+
|
|
31
|
+
4. WARRANTY DISCLAIMER.
|
|
32
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
33
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
34
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
35
|
+
DUNECREST VENTURES INC. BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY
|
|
36
|
+
ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
37
|
+
DEALINGS IN THE SOFTWARE.
|
|
38
|
+
|
|
39
|
+
5. SUCCESSORS AND ASSIGNS.
|
|
40
|
+
This License binds and benefits the parties' respective successors and assigns.
|
|
41
|
+
In the event of an asset sale, merger, change of control, or other transfer of
|
|
42
|
+
the Licensor's rights in this software, all rights and obligations under this
|
|
43
|
+
License inure to the benefit of and are binding upon Licensor's successor or
|
|
44
|
+
assignee. Outstanding evaluation grants survive change-of-control, but the
|
|
45
|
+
successor or assignee may, upon thirty (30) days' written notice, terminate
|
|
46
|
+
ongoing evaluation grants in favor of a commercial-license requirement.
|
|
47
|
+
|
|
48
|
+
6. TERMINATION.
|
|
49
|
+
Licensor may terminate this License with thirty (30) days' written notice for
|
|
50
|
+
any reason or no reason. Upon termination, Licensee shall cease all use of the
|
|
51
|
+
software under src/ and shall destroy all copies in Licensee's possession.
|
|
52
|
+
|
|
53
|
+
7. PATENT NOTICE (35 U.S.C. § 287).
|
|
54
|
+
Protected by U.S. patent-pending technology (Application Numbers 63/983,615;
|
|
55
|
+
63/983,621; 63/983,843; 63/984,626; and an additional application filed in
|
|
56
|
+
May 2026, application number to be added upon receipt of filing acknowledgment).
|
|
57
|
+
Additional patents pending.
|
|
58
|
+
|
|
59
|
+
AgentGuard(TM) is a trademark of Dunecrest Ventures Inc. (USPTO Serial No.
|
|
60
|
+
99462472, pending). MerchantGuard(TM) is a trademark of Dunecrest Ventures
|
|
61
|
+
Inc. (USPTO Serial No. 99051215, pending).
|
|
62
|
+
|
|
63
|
+
For commercial licensing: licensing@merchantguard.ai
|
package/PATENTS.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Patent Notice
|
|
2
|
+
|
|
3
|
+
This package is part of the AgentGuard cryptographic trust portfolio.
|
|
4
|
+
|
|
5
|
+
Protected by U.S. patent-pending technology:
|
|
6
|
+
|
|
7
|
+
- Application No. 63/983,615 (filed February 2026)
|
|
8
|
+
- Application No. 63/983,621 (filed February 2026)
|
|
9
|
+
- Application No. 63/983,843 (filed February 2026)
|
|
10
|
+
- Application No. 63/984,626 (filed February 2026)
|
|
11
|
+
- Application No. 64/071,781 (filed May 2026)
|
|
12
|
+
|
|
13
|
+
All rights reserved. Dunecrest Ventures Inc.
|
|
14
|
+
|
|
15
|
+
For licensing, OEM integration, or strategic partnership inquiries:
|
|
16
|
+
invest@agentguard.run
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @agentguard-run/spend
|
|
2
|
+
|
|
3
|
+
**Local-runtime spend caps and capability-gated model routing for AI agents.**
|
|
4
|
+
|
|
5
|
+
Every policy decision runs inside your process. Prompts, provider API keys, and signing keys never leave your runtime. Each enforcement decision produces a signed, hash-chained receipt suitable for audit and compliance review.
|
|
6
|
+
|
|
7
|
+
## Why no proxy
|
|
8
|
+
|
|
9
|
+
Every funded competitor in AI spend governance (Portkey, Helicone, LiteLLM, Cloudflare AI Gateway, Vercel AI Gateway) proxies your traffic. That means your prompts and provider keys flow through their infrastructure. AgentGuard Spend never sees any of that. The policy runs in your process. The signed log lives in your storage.
|
|
10
|
+
|
|
11
|
+
The procurement consequence: your security review covers this SDK like any other library, not like a vendor that handles your data.
|
|
12
|
+
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
Private preview. Designed for enterprise, OEM, and platform integration.
|
|
16
|
+
|
|
17
|
+
For evaluation access, OEM licensing, or strategic partnership inquiries:
|
|
18
|
+
**invest@agentguard.run**
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Your code
|
|
24
|
+
|
|
|
25
|
+
v
|
|
26
|
+
withSpendGuard(openai, { policy, scope })
|
|
27
|
+
├── estimate tokens
|
|
28
|
+
├── evaluatePolicy() ──► SpendStore (your storage)
|
|
29
|
+
├── signDecision() with Ed25519
|
|
30
|
+
├── append to DecisionLog (your storage)
|
|
31
|
+
└── pass-through / downgrade / block
|
|
32
|
+
|
|
|
33
|
+
v
|
|
34
|
+
Provider (OpenAI / Anthropic / Bedrock)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
No part of this picture sends data to AgentGuard infrastructure.
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @agentguard-run/spend
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import OpenAI from 'openai';
|
|
47
|
+
import { withSpendGuard } from '@agentguard-run/spend';
|
|
48
|
+
|
|
49
|
+
const openai = new OpenAI();
|
|
50
|
+
|
|
51
|
+
const guarded = withSpendGuard(openai, {
|
|
52
|
+
policy: {
|
|
53
|
+
id: 'my-policy',
|
|
54
|
+
version: 1,
|
|
55
|
+
mode: 'enforce',
|
|
56
|
+
caps: {
|
|
57
|
+
perCall: { limitCents: 50 },
|
|
58
|
+
daily: { limitCents: 500 },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
scope: { tenantId: 'acme', userId: 'u_123' },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Drop-in replacement — same API as openai.chat.completions.create
|
|
65
|
+
const completion = await guarded.chat.completions.create({
|
|
66
|
+
model: 'gpt-4o',
|
|
67
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If the policy blocks the call, an `AgentGuardBlockedError` is thrown with the full signed decision attached.
|
|
72
|
+
|
|
73
|
+
## Patent notice
|
|
74
|
+
|
|
75
|
+
See [PATENTS.md](./PATENTS.md). Protected by 5 U.S. patent-pending applications filed February and May 2026.
|
|
76
|
+
|
|
77
|
+
## Links
|
|
78
|
+
|
|
79
|
+
- [agentguard.run](https://agentguard.run)
|
|
80
|
+
- Contact: invest@agentguard.run
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentGuard(TM) Spend — Per-model cost table
|
|
3
|
+
*
|
|
4
|
+
* Costs are USD cents per 1,000 tokens (integer math).
|
|
5
|
+
* Values reflect publicly-listed pricing as of May 2026 and are intentionally
|
|
6
|
+
* conservative (rounded UP) so spend caps fire early rather than late.
|
|
7
|
+
*
|
|
8
|
+
* Customers can override this table via setCostOverride() when enterprise
|
|
9
|
+
* contracts have negotiated rates that differ from public list pricing.
|
|
10
|
+
*
|
|
11
|
+
* Licensed under the alpha evaluation license; see LICENSE in the package
|
|
12
|
+
* root. Patent notice: Protected by U.S. patent-pending technology
|
|
13
|
+
* (App. Nos. 63/983,615; 63/983,621; 63/983,843; 63/984,626;
|
|
14
|
+
* and an additional application filed May 2026).
|
|
15
|
+
*/
|
|
16
|
+
import type { Provider } from './types';
|
|
17
|
+
export interface ModelCost {
|
|
18
|
+
/** Cents per 1,000 input tokens */
|
|
19
|
+
inputCentsPerKtok: number;
|
|
20
|
+
/** Cents per 1,000 output tokens */
|
|
21
|
+
outputCentsPerKtok: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Override the cost for a specific model. Used when the customer has negotiated
|
|
25
|
+
* enterprise rates that differ from list pricing.
|
|
26
|
+
*/
|
|
27
|
+
export declare function setCostOverride(model: string, cost: ModelCost): void;
|
|
28
|
+
/**
|
|
29
|
+
* Clear all cost overrides. Mainly for testing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function clearCostOverrides(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Look up the cost for a model. Returns null if the model is unknown AND no
|
|
34
|
+
* override has been registered. Callers should treat unknown models defensively
|
|
35
|
+
* (e.g., by registering an override or by rejecting the call).
|
|
36
|
+
*/
|
|
37
|
+
export declare function getModelCost(model: string): ModelCost | null;
|
|
38
|
+
/**
|
|
39
|
+
* Compute the cents this call would cost given input + output token counts.
|
|
40
|
+
* Returns null if the model is unknown (caller must decide how to handle).
|
|
41
|
+
*
|
|
42
|
+
* Formula: cents = ceil( (cents_per_ktok * tokens) / 1000 )
|
|
43
|
+
*
|
|
44
|
+
* Rounds UP so caps fire conservatively (a call that would cost 19.4c
|
|
45
|
+
* counts as 20c against the budget).
|
|
46
|
+
*/
|
|
47
|
+
export declare function computeCallCents(model: string, inputTokens: number, outputTokens: number): number | null;
|
|
48
|
+
/**
|
|
49
|
+
* Infer the Provider enum from a model name. Used when callers do not pass
|
|
50
|
+
* provider explicitly. Returns 'unknown' if no pattern matches.
|
|
51
|
+
*/
|
|
52
|
+
export declare function inferProvider(model: string): Provider;
|
|
53
|
+
//# sourceMappingURL=cost-table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-table.d.ts","sourceRoot":"","sources":["../src/cost-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,MAAM,WAAW,SAAS;IACxB,mCAAmC;IACnC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oCAAoC;IACpC,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAmCD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAEpE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAG5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,MAAM,GAAG,IAAI,CAOf;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAOrD"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AgentGuard(TM) Spend — Per-model cost table
|
|
4
|
+
*
|
|
5
|
+
* Costs are USD cents per 1,000 tokens (integer math).
|
|
6
|
+
* Values reflect publicly-listed pricing as of May 2026 and are intentionally
|
|
7
|
+
* conservative (rounded UP) so spend caps fire early rather than late.
|
|
8
|
+
*
|
|
9
|
+
* Customers can override this table via setCostOverride() when enterprise
|
|
10
|
+
* contracts have negotiated rates that differ from public list pricing.
|
|
11
|
+
*
|
|
12
|
+
* Licensed under the alpha evaluation license; see LICENSE in the package
|
|
13
|
+
* root. Patent notice: Protected by U.S. patent-pending technology
|
|
14
|
+
* (App. Nos. 63/983,615; 63/983,621; 63/983,843; 63/984,626;
|
|
15
|
+
* and an additional application filed May 2026).
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.setCostOverride = setCostOverride;
|
|
19
|
+
exports.clearCostOverrides = clearCostOverrides;
|
|
20
|
+
exports.getModelCost = getModelCost;
|
|
21
|
+
exports.computeCallCents = computeCallCents;
|
|
22
|
+
exports.inferProvider = inferProvider;
|
|
23
|
+
/**
|
|
24
|
+
* Default cost table. Values are list-price ceilings as of May 2026.
|
|
25
|
+
* Update via setCostOverride() for negotiated enterprise rates.
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_COSTS = {
|
|
28
|
+
// OpenAI
|
|
29
|
+
'gpt-5': { inputCentsPerKtok: 0.5, outputCentsPerKtok: 1.5 },
|
|
30
|
+
'gpt-5-mini': { inputCentsPerKtok: 0.05, outputCentsPerKtok: 0.2 },
|
|
31
|
+
'gpt-4o': { inputCentsPerKtok: 0.25, outputCentsPerKtok: 1.0 },
|
|
32
|
+
'gpt-4o-mini': { inputCentsPerKtok: 0.015, outputCentsPerKtok: 0.06 },
|
|
33
|
+
// Anthropic
|
|
34
|
+
'claude-opus-4-7': { inputCentsPerKtok: 2.0, outputCentsPerKtok: 10.0 },
|
|
35
|
+
'claude-opus-4-6': { inputCentsPerKtok: 1.5, outputCentsPerKtok: 7.5 },
|
|
36
|
+
'claude-sonnet-4-6': { inputCentsPerKtok: 0.3, outputCentsPerKtok: 1.5 },
|
|
37
|
+
'claude-sonnet-4-5': { inputCentsPerKtok: 0.3, outputCentsPerKtok: 1.5 },
|
|
38
|
+
'claude-haiku-4-5': { inputCentsPerKtok: 0.1, outputCentsPerKtok: 0.5 },
|
|
39
|
+
// Google Gemini
|
|
40
|
+
'gemini-3.1-pro-preview': { inputCentsPerKtok: 0.2, outputCentsPerKtok: 1.0 },
|
|
41
|
+
'gemini-3-flash-preview': { inputCentsPerKtok: 0.02, outputCentsPerKtok: 0.1 },
|
|
42
|
+
'gemini-3-pro-image-preview': { inputCentsPerKtok: 0.4, outputCentsPerKtok: 2.0 },
|
|
43
|
+
// AWS Bedrock - common models (rates vary by region; these are us-east-1 ceilings)
|
|
44
|
+
'anthropic.claude-opus-4-v1:0': { inputCentsPerKtok: 1.5, outputCentsPerKtok: 7.5 },
|
|
45
|
+
'anthropic.claude-sonnet-4-v1:0': { inputCentsPerKtok: 0.3, outputCentsPerKtok: 1.5 },
|
|
46
|
+
'amazon.nova-pro-v1:0': { inputCentsPerKtok: 0.08, outputCentsPerKtok: 0.32 },
|
|
47
|
+
'amazon.nova-lite-v1:0': { inputCentsPerKtok: 0.006, outputCentsPerKtok: 0.024 },
|
|
48
|
+
};
|
|
49
|
+
/** Runtime override storage. Keyed by exact model name. */
|
|
50
|
+
const overrides = new Map();
|
|
51
|
+
/**
|
|
52
|
+
* Override the cost for a specific model. Used when the customer has negotiated
|
|
53
|
+
* enterprise rates that differ from list pricing.
|
|
54
|
+
*/
|
|
55
|
+
function setCostOverride(model, cost) {
|
|
56
|
+
overrides.set(model, cost);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Clear all cost overrides. Mainly for testing.
|
|
60
|
+
*/
|
|
61
|
+
function clearCostOverrides() {
|
|
62
|
+
overrides.clear();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Look up the cost for a model. Returns null if the model is unknown AND no
|
|
66
|
+
* override has been registered. Callers should treat unknown models defensively
|
|
67
|
+
* (e.g., by registering an override or by rejecting the call).
|
|
68
|
+
*/
|
|
69
|
+
function getModelCost(model) {
|
|
70
|
+
if (overrides.has(model))
|
|
71
|
+
return overrides.get(model) ?? null;
|
|
72
|
+
return DEFAULT_COSTS[model] ?? null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Compute the cents this call would cost given input + output token counts.
|
|
76
|
+
* Returns null if the model is unknown (caller must decide how to handle).
|
|
77
|
+
*
|
|
78
|
+
* Formula: cents = ceil( (cents_per_ktok * tokens) / 1000 )
|
|
79
|
+
*
|
|
80
|
+
* Rounds UP so caps fire conservatively (a call that would cost 19.4c
|
|
81
|
+
* counts as 20c against the budget).
|
|
82
|
+
*/
|
|
83
|
+
function computeCallCents(model, inputTokens, outputTokens) {
|
|
84
|
+
const cost = getModelCost(model);
|
|
85
|
+
if (!cost)
|
|
86
|
+
return null;
|
|
87
|
+
const inputCents = Math.ceil((cost.inputCentsPerKtok * inputTokens) / 1000);
|
|
88
|
+
const outputCents = Math.ceil((cost.outputCentsPerKtok * outputTokens) / 1000);
|
|
89
|
+
return inputCents + outputCents;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Infer the Provider enum from a model name. Used when callers do not pass
|
|
93
|
+
* provider explicitly. Returns 'unknown' if no pattern matches.
|
|
94
|
+
*/
|
|
95
|
+
function inferProvider(model) {
|
|
96
|
+
if (model.startsWith('gpt-') || model.startsWith('o1') || model.startsWith('o3'))
|
|
97
|
+
return 'openai';
|
|
98
|
+
if (model.startsWith('claude-'))
|
|
99
|
+
return 'anthropic';
|
|
100
|
+
if (model.startsWith('gemini-'))
|
|
101
|
+
return 'gemini';
|
|
102
|
+
if (model.includes('claude-') && model.includes('-v'))
|
|
103
|
+
return 'bedrock';
|
|
104
|
+
if (model.startsWith('amazon.') || model.startsWith('anthropic.') || model.startsWith('meta.'))
|
|
105
|
+
return 'bedrock';
|
|
106
|
+
return 'unknown';
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=cost-table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-table.js","sourceRoot":"","sources":["../src/cost-table.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentGuard(TM) Spend — Signed hash-chained decision log
|
|
3
|
+
*
|
|
4
|
+
* Each policy decision is appended to a tamper-evident log:
|
|
5
|
+
* entry_n.previousHash = SHA-256(canonical_json(entry_{n-1} minus signature))
|
|
6
|
+
* entry_n.entryHash = SHA-256(canonical_json(entry_n minus signature))
|
|
7
|
+
* entry_n.signature = Ed25519(privateKey, entryHash)
|
|
8
|
+
*
|
|
9
|
+
* The log lives entirely in the customer runtime. The signing key never leaves
|
|
10
|
+
* the customer environment. Public verification is performed by anyone with
|
|
11
|
+
* the corresponding public key.
|
|
12
|
+
*
|
|
13
|
+
* The log structure (sequence number, previousHash binding, canonical-JSON
|
|
14
|
+
* serialization, Ed25519 signature) is the subject of a U.S. provisional
|
|
15
|
+
* patent application filed by the applicant in May 2026 (application number
|
|
16
|
+
* pending). The log is also designed to interoperate with the DAG Trust
|
|
17
|
+
* Attestation Token framework of Patent D §7.3 (App. No. 63/984,626) when
|
|
18
|
+
* deployed in composition; such composition is not required for the
|
|
19
|
+
* spend-governance functions implemented in this package.
|
|
20
|
+
*
|
|
21
|
+
* Patent notice: Protected by U.S. patent-pending technology
|
|
22
|
+
* (App. Nos. 63/983,615; 63/983,621; 63/983,843; 63/984,626;
|
|
23
|
+
* and an additional application filed May 2026).
|
|
24
|
+
*/
|
|
25
|
+
import type { SignedDecisionLogEntry, SpendDecision, DecisionLogStore } from './types';
|
|
26
|
+
/** All-zero hex string used as the previousHash of the genesis entry. */
|
|
27
|
+
export declare const GENESIS_PREVIOUS_HASH: string;
|
|
28
|
+
/**
|
|
29
|
+
* Deterministic JSON serialization. Object keys are sorted, no trailing
|
|
30
|
+
* whitespace, no NaN / Infinity. Required for stable hashing across runtimes.
|
|
31
|
+
*/
|
|
32
|
+
export declare function canonicalJson(value: unknown): string;
|
|
33
|
+
/** SHA-256 of a string, returned as lowercase hex. */
|
|
34
|
+
export declare function sha256Hex(input: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Compute the entryHash for a decision and the previous hash.
|
|
37
|
+
* This is the value that gets signed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function computeEntryHash(args: {
|
|
40
|
+
sequence: number;
|
|
41
|
+
decision: SpendDecision;
|
|
42
|
+
previousHash: string;
|
|
43
|
+
}): string;
|
|
44
|
+
/**
|
|
45
|
+
* Compute the public key fingerprint: first 8 bytes of SHA-256(pubkey), hex.
|
|
46
|
+
* Used for short identification of which key signed an entry.
|
|
47
|
+
*/
|
|
48
|
+
export declare function computeSignerFingerprint(publicKey: Uint8Array): string;
|
|
49
|
+
/**
|
|
50
|
+
* Sign a decision and produce a SignedDecisionLogEntry.
|
|
51
|
+
*
|
|
52
|
+
* The privateKey is a 32-byte Ed25519 secret seed. It MUST be generated and
|
|
53
|
+
* stored by the customer; AgentGuard Spend never sees or transmits it.
|
|
54
|
+
*/
|
|
55
|
+
export declare function signDecision(args: {
|
|
56
|
+
sequence: number;
|
|
57
|
+
decision: SpendDecision;
|
|
58
|
+
previousHash: string;
|
|
59
|
+
privateKey: Uint8Array;
|
|
60
|
+
publicKey: Uint8Array;
|
|
61
|
+
}): Promise<SignedDecisionLogEntry>;
|
|
62
|
+
/**
|
|
63
|
+
* Verify a single entry's signature and hash. Returns true iff the entryHash
|
|
64
|
+
* recomputes correctly AND the signature is valid for the given public key.
|
|
65
|
+
*
|
|
66
|
+
* Does NOT verify chain linkage (that's verifyChain's job).
|
|
67
|
+
*/
|
|
68
|
+
export declare function verifyEntry(entry: SignedDecisionLogEntry, publicKey: Uint8Array): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* Verify an entire chain of decision log entries.
|
|
71
|
+
*
|
|
72
|
+
* Returns:
|
|
73
|
+
* { ok: true } if every entry is valid AND each entry's previousHash matches
|
|
74
|
+
* the prior entry's entryHash, AND the genesis entry has GENESIS_PREVIOUS_HASH,
|
|
75
|
+
* AND sequences are strictly increasing starting at 0.
|
|
76
|
+
* { ok: false, sequence, reason } on the first detected fault.
|
|
77
|
+
*/
|
|
78
|
+
export declare function verifyChain(entries: SignedDecisionLogEntry[], publicKey: Uint8Array): Promise<{
|
|
79
|
+
ok: true;
|
|
80
|
+
} | {
|
|
81
|
+
ok: false;
|
|
82
|
+
sequence: number;
|
|
83
|
+
reason: string;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* In-memory decision log store. Useful for tests, examples, and short-lived
|
|
87
|
+
* shadow deployments. Production callers will typically supply their own
|
|
88
|
+
* implementation that writes to a database table or append-only file.
|
|
89
|
+
*/
|
|
90
|
+
export declare class InMemoryDecisionLogStore implements DecisionLogStore {
|
|
91
|
+
private entries;
|
|
92
|
+
append(entry: SignedDecisionLogEntry): Promise<void>;
|
|
93
|
+
getLatest(): Promise<SignedDecisionLogEntry | null>;
|
|
94
|
+
read(fromSequence: number, limit: number): Promise<SignedDecisionLogEntry[]>;
|
|
95
|
+
/** Returns a defensive copy of all entries. Mainly for testing. */
|
|
96
|
+
snapshot(): SignedDecisionLogEntry[];
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=decision-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decision-log.d.ts","sourceRoot":"","sources":["../src/decision-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,KAAK,EACV,sBAAsB,EACtB,aAAa,EACb,gBAAgB,EACjB,MAAM,SAAS,CAAC;AAKjB,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,QAAiB,CAAC;AAEpD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcpD;AAED,sDAAsD;AACtD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,aAAa,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CAOT;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAGtE;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,aAAa,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,EAAE,UAAU,CAAC;CACvB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAgBlC;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,sBAAsB,EAC7B,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC,OAAO,CAAC,CAalB;AAED;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,sBAAsB,EAAE,EACjC,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAoCzE;AAED;;;;GAIG;AACH,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,OAAO,CAAgC;IAEzC,MAAM,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpD,SAAS,IAAI,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAKnD,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC;IAMlF,mEAAmE;IACnE,QAAQ,IAAI,sBAAsB,EAAE;CAGrC"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AgentGuard(TM) Spend — Signed hash-chained decision log
|
|
4
|
+
*
|
|
5
|
+
* Each policy decision is appended to a tamper-evident log:
|
|
6
|
+
* entry_n.previousHash = SHA-256(canonical_json(entry_{n-1} minus signature))
|
|
7
|
+
* entry_n.entryHash = SHA-256(canonical_json(entry_n minus signature))
|
|
8
|
+
* entry_n.signature = Ed25519(privateKey, entryHash)
|
|
9
|
+
*
|
|
10
|
+
* The log lives entirely in the customer runtime. The signing key never leaves
|
|
11
|
+
* the customer environment. Public verification is performed by anyone with
|
|
12
|
+
* the corresponding public key.
|
|
13
|
+
*
|
|
14
|
+
* The log structure (sequence number, previousHash binding, canonical-JSON
|
|
15
|
+
* serialization, Ed25519 signature) is the subject of a U.S. provisional
|
|
16
|
+
* patent application filed by the applicant in May 2026 (application number
|
|
17
|
+
* pending). The log is also designed to interoperate with the DAG Trust
|
|
18
|
+
* Attestation Token framework of Patent D §7.3 (App. No. 63/984,626) when
|
|
19
|
+
* deployed in composition; such composition is not required for the
|
|
20
|
+
* spend-governance functions implemented in this package.
|
|
21
|
+
*
|
|
22
|
+
* Patent notice: Protected by U.S. patent-pending technology
|
|
23
|
+
* (App. Nos. 63/983,615; 63/983,621; 63/983,843; 63/984,626;
|
|
24
|
+
* and an additional application filed May 2026).
|
|
25
|
+
*/
|
|
26
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
29
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
30
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
31
|
+
}
|
|
32
|
+
Object.defineProperty(o, k2, desc);
|
|
33
|
+
}) : (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
o[k2] = m[k];
|
|
36
|
+
}));
|
|
37
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
38
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
39
|
+
}) : function(o, v) {
|
|
40
|
+
o["default"] = v;
|
|
41
|
+
});
|
|
42
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
43
|
+
var ownKeys = function(o) {
|
|
44
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
45
|
+
var ar = [];
|
|
46
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
47
|
+
return ar;
|
|
48
|
+
};
|
|
49
|
+
return ownKeys(o);
|
|
50
|
+
};
|
|
51
|
+
return function (mod) {
|
|
52
|
+
if (mod && mod.__esModule) return mod;
|
|
53
|
+
var result = {};
|
|
54
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
55
|
+
__setModuleDefault(result, mod);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.InMemoryDecisionLogStore = exports.GENESIS_PREVIOUS_HASH = void 0;
|
|
61
|
+
exports.canonicalJson = canonicalJson;
|
|
62
|
+
exports.sha256Hex = sha256Hex;
|
|
63
|
+
exports.computeEntryHash = computeEntryHash;
|
|
64
|
+
exports.computeSignerFingerprint = computeSignerFingerprint;
|
|
65
|
+
exports.signDecision = signDecision;
|
|
66
|
+
exports.verifyEntry = verifyEntry;
|
|
67
|
+
exports.verifyChain = verifyChain;
|
|
68
|
+
const crypto_1 = require("crypto");
|
|
69
|
+
const ed = __importStar(require("@noble/ed25519"));
|
|
70
|
+
// @noble/ed25519 v2.3+ has sha512Async wired internally. The signAsync /
|
|
71
|
+
// verifyAsync code paths used below do not require any module-level setup.
|
|
72
|
+
/** All-zero hex string used as the previousHash of the genesis entry. */
|
|
73
|
+
exports.GENESIS_PREVIOUS_HASH = '0'.repeat(64);
|
|
74
|
+
/**
|
|
75
|
+
* Deterministic JSON serialization. Object keys are sorted, no trailing
|
|
76
|
+
* whitespace, no NaN / Infinity. Required for stable hashing across runtimes.
|
|
77
|
+
*/
|
|
78
|
+
function canonicalJson(value) {
|
|
79
|
+
if (value === null || typeof value !== 'object') {
|
|
80
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
81
|
+
throw new Error('canonicalJson: non-finite numbers are not permitted');
|
|
82
|
+
}
|
|
83
|
+
return JSON.stringify(value);
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(value)) {
|
|
86
|
+
return '[' + value.map(canonicalJson).join(',') + ']';
|
|
87
|
+
}
|
|
88
|
+
const obj = value;
|
|
89
|
+
const keys = Object.keys(obj).sort();
|
|
90
|
+
const parts = keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(obj[k]));
|
|
91
|
+
return '{' + parts.join(',') + '}';
|
|
92
|
+
}
|
|
93
|
+
/** SHA-256 of a string, returned as lowercase hex. */
|
|
94
|
+
function sha256Hex(input) {
|
|
95
|
+
return (0, crypto_1.createHash)('sha256').update(input, 'utf8').digest('hex');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Compute the entryHash for a decision and the previous hash.
|
|
99
|
+
* This is the value that gets signed.
|
|
100
|
+
*/
|
|
101
|
+
function computeEntryHash(args) {
|
|
102
|
+
const payload = canonicalJson({
|
|
103
|
+
sequence: args.sequence,
|
|
104
|
+
decision: args.decision,
|
|
105
|
+
previousHash: args.previousHash,
|
|
106
|
+
});
|
|
107
|
+
return sha256Hex(payload);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Compute the public key fingerprint: first 8 bytes of SHA-256(pubkey), hex.
|
|
111
|
+
* Used for short identification of which key signed an entry.
|
|
112
|
+
*/
|
|
113
|
+
function computeSignerFingerprint(publicKey) {
|
|
114
|
+
const hash = (0, crypto_1.createHash)('sha256').update(publicKey).digest('hex');
|
|
115
|
+
return hash.slice(0, 16);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Sign a decision and produce a SignedDecisionLogEntry.
|
|
119
|
+
*
|
|
120
|
+
* The privateKey is a 32-byte Ed25519 secret seed. It MUST be generated and
|
|
121
|
+
* stored by the customer; AgentGuard Spend never sees or transmits it.
|
|
122
|
+
*/
|
|
123
|
+
async function signDecision(args) {
|
|
124
|
+
const entryHash = computeEntryHash({
|
|
125
|
+
sequence: args.sequence,
|
|
126
|
+
decision: args.decision,
|
|
127
|
+
previousHash: args.previousHash,
|
|
128
|
+
});
|
|
129
|
+
const sigBytes = await ed.signAsync(entryHash, args.privateKey);
|
|
130
|
+
const signature = Buffer.from(sigBytes).toString('hex');
|
|
131
|
+
return {
|
|
132
|
+
sequence: args.sequence,
|
|
133
|
+
decision: args.decision,
|
|
134
|
+
previousHash: args.previousHash,
|
|
135
|
+
entryHash,
|
|
136
|
+
signature,
|
|
137
|
+
signerFingerprint: computeSignerFingerprint(args.publicKey),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Verify a single entry's signature and hash. Returns true iff the entryHash
|
|
142
|
+
* recomputes correctly AND the signature is valid for the given public key.
|
|
143
|
+
*
|
|
144
|
+
* Does NOT verify chain linkage (that's verifyChain's job).
|
|
145
|
+
*/
|
|
146
|
+
async function verifyEntry(entry, publicKey) {
|
|
147
|
+
const expectedHash = computeEntryHash({
|
|
148
|
+
sequence: entry.sequence,
|
|
149
|
+
decision: entry.decision,
|
|
150
|
+
previousHash: entry.previousHash,
|
|
151
|
+
});
|
|
152
|
+
if (expectedHash !== entry.entryHash)
|
|
153
|
+
return false;
|
|
154
|
+
const sigBytes = Buffer.from(entry.signature, 'hex');
|
|
155
|
+
try {
|
|
156
|
+
return await ed.verifyAsync(sigBytes, entry.entryHash, publicKey);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Verify an entire chain of decision log entries.
|
|
164
|
+
*
|
|
165
|
+
* Returns:
|
|
166
|
+
* { ok: true } if every entry is valid AND each entry's previousHash matches
|
|
167
|
+
* the prior entry's entryHash, AND the genesis entry has GENESIS_PREVIOUS_HASH,
|
|
168
|
+
* AND sequences are strictly increasing starting at 0.
|
|
169
|
+
* { ok: false, sequence, reason } on the first detected fault.
|
|
170
|
+
*/
|
|
171
|
+
async function verifyChain(entries, publicKey) {
|
|
172
|
+
if (entries.length === 0)
|
|
173
|
+
return { ok: true };
|
|
174
|
+
for (let i = 0; i < entries.length; i++) {
|
|
175
|
+
const entry = entries[i];
|
|
176
|
+
if (!entry) {
|
|
177
|
+
return { ok: false, sequence: i, reason: 'Missing entry at index ' + i };
|
|
178
|
+
}
|
|
179
|
+
const expectedSeq = i === 0 ? entries[0].sequence : (entries[i - 1].sequence + 1);
|
|
180
|
+
if (entry.sequence !== expectedSeq) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
sequence: entry.sequence,
|
|
184
|
+
reason: `Sequence gap: expected ${expectedSeq}, got ${entry.sequence}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const expectedPrev = i === 0 ? exports.GENESIS_PREVIOUS_HASH : entries[i - 1].entryHash;
|
|
188
|
+
if (entry.previousHash !== expectedPrev) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
sequence: entry.sequence,
|
|
192
|
+
reason: `previousHash mismatch at sequence ${entry.sequence}: expected ${expectedPrev}, got ${entry.previousHash}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const ok = await verifyEntry(entry, publicKey);
|
|
196
|
+
if (!ok) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
sequence: entry.sequence,
|
|
200
|
+
reason: `Signature or hash invalid at sequence ${entry.sequence}`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { ok: true };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* In-memory decision log store. Useful for tests, examples, and short-lived
|
|
208
|
+
* shadow deployments. Production callers will typically supply their own
|
|
209
|
+
* implementation that writes to a database table or append-only file.
|
|
210
|
+
*/
|
|
211
|
+
class InMemoryDecisionLogStore {
|
|
212
|
+
entries = [];
|
|
213
|
+
async append(entry) {
|
|
214
|
+
this.entries.push(entry);
|
|
215
|
+
}
|
|
216
|
+
async getLatest() {
|
|
217
|
+
if (this.entries.length === 0)
|
|
218
|
+
return null;
|
|
219
|
+
return this.entries[this.entries.length - 1] ?? null;
|
|
220
|
+
}
|
|
221
|
+
async read(fromSequence, limit) {
|
|
222
|
+
const start = this.entries.findIndex((e) => e.sequence >= fromSequence);
|
|
223
|
+
if (start === -1)
|
|
224
|
+
return [];
|
|
225
|
+
return this.entries.slice(start, start + limit);
|
|
226
|
+
}
|
|
227
|
+
/** Returns a defensive copy of all entries. Mainly for testing. */
|
|
228
|
+
snapshot() {
|
|
229
|
+
return [...this.entries];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
exports.InMemoryDecisionLogStore = InMemoryDecisionLogStore;
|
|
233
|
+
//# sourceMappingURL=decision-log.js.map
|