@dorigjo/besa 0.1.0-alpha.2 → 0.1.0-beta.5

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,345 +1,312 @@
1
- # Besa
2
-
3
- Signed trust infrastructure for AI-agent tools.
4
-
5
- > **Alpha / developer preview — not production-ready.**
6
- >
7
- > Besa is currently an early alpha (`0.1.0-alpha.0`). APIs, file formats, receipt formats, and behavior may change without notice.
8
- >
9
- > Do not use Besa to protect production systems, production secrets, customer data, or real signing keys yet.
10
- >
11
- > The key under `.besa/` is a local demo key.
12
-
13
- Besa signs MCP-style tool manifests, verifies them before use, admits or denies tool calls against policy, and issues signed tamper-evident receipts.
14
-
15
- Besa is the trust layer for AI-agent tools.
16
-
17
- ## What it does
18
-
19
- * Signs tool manifests with Ed25519.
20
- * Verifies signed manifests before runtime use.
21
- * Allows or denies tool calls with reason codes.
22
- * Blocks destructive high-risk tools by default.
23
- * Tracks local per-tool usage with a mini ActionMeter.
24
- * Creates signed receipts for admission decisions.
25
-
26
- Flow:
27
-
28
- ```text
29
- manifest.yaml -> sign -> verify -> admit -> receipt
30
- ```
31
-
32
- ## Why it matters
33
-
34
- AI agents increasingly call external tools, APIs, MCP servers, and internal systems.
35
-
36
- The important question is not only whether an agent can call a tool.
37
-
38
- The important questions are:
39
-
40
- * Which tool is the agent allowed to call?
41
- * Who signed the declared capability?
42
- * Has the manifest changed?
43
- * Was the call allowed or denied?
44
- * Is there a receipt proving the decision?
45
-
46
- Besa turns those answers into signed artifacts.
47
-
48
- ## Quickstart
49
-
50
- Install dependencies:
51
-
52
- ```bash
53
- npm install
54
- ```
55
-
56
- Build:
57
-
58
- ```bash
59
- npm run build
60
- ```
61
-
62
- Run tests:
63
-
64
- ```bash
65
- npm test
66
- ```
67
-
68
- Run the smoke test:
69
-
70
- ```bash
71
- npm run smoke
72
- ```
73
-
74
- The smoke test runs the full CLI flow: build, load, sign, verify, admit allow, admit deny, and receipt creation.
75
-
76
- ## CLI commands
77
-
78
- Load a manifest:
79
-
80
- ```bash
81
- node dist/index.js load examples/manifest.yaml
82
- ```
83
-
84
- Sign a manifest:
85
-
86
- ```bash
87
- node dist/index.js sign examples/manifest.yaml
88
- ```
89
-
90
- Verify a signed manifest:
91
-
92
- ```bash
93
- node dist/index.js verify examples/manifest.signed.json
94
- ```
95
-
96
- Admit a safe tool:
97
-
98
- ```bash
99
- node dist/index.js admit examples/manifest.signed.json crm.lookup
100
- ```
101
-
102
- Deny a dangerous tool:
103
-
104
- ```bash
105
- node dist/index.js admit examples/manifest.signed.json crm.delete
106
- ```
107
-
108
- Create a signed receipt:
109
-
110
- ```bash
111
- node dist/index.js receipt crm.lookup
112
- ```
113
-
114
- Expected behavior:
115
-
116
- * `crm.lookup` -> allow / `ALLOWED`
117
- * `crm.delete` -> deny / `RISK_BLOCKED`
118
-
119
- ### Grant-aware admission (optional)
120
-
121
- Besa can scope a tool call to a specific agent. Add a `grants.yaml` listing which `agentId` may use which tools:
1
+ <p align="center">
2
+ <img src="site/logo.svg" alt="" width="44" height="40" />
3
+ </p>
4
+
5
+ <h1 align="center">Besa</h1>
6
+
7
+ <p align="center"><strong>Agent Action Receipts</strong></p>
8
+
9
+ <p align="center">
10
+ The signed-receipt layer for AI-agent tool calls.
11
+ </p>
12
+
13
+ <p align="center">
14
+ <a href="https://github.com/dorigjo/besa/actions/workflows/ci.yml"><img src="https://github.com/dorigjo/besa/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
15
+ <a href="https://www.npmjs.com/package/@dorigjo/besa"><img src="https://img.shields.io/npm/v/@dorigjo/besa" alt="npm" /></a>
16
+ </p>
17
+
18
+ ---
19
+
20
+ Besa creates cryptographic execution evidence for AI-agent tool calls. Every
21
+ admission decision is signed. Every signed receipt is tamper-evident and
22
+ independently verifiable.
23
+
24
+ > **Beta.** `0.1.0-beta.5` is a public developer beta. Not yet production-ready.
25
+ > Feedback and issues: [github.com/dorigjo/besa/issues](https://github.com/dorigjo/besa/issues).
26
+
27
+ ---
28
+
29
+ ## Trust flow
122
30
 
123
31
  ```
124
- grants:
125
- - agentId: agent-alpha
126
- tools:
127
- - crm.lookup
32
+ manifest.yaml
33
+ besa sign # sign the declared tools, capabilities, risks, scopes
34
+ → manifest.signed.json # Ed25519-signed artifact
35
+ besa trust add # pin the publisher's public key
36
+ → besa verify # verify signature against pinned trust anchor
37
+ → besa admit <tool> # dry-run: check policy, capabilities, budget
38
+ → besa receipt <tool> # enforce budget, issue signed receipt
39
+ → besa verify-receipt # verify the receipt chain end-to-end
128
40
  ```
129
41
 
130
- Then pass `--agent` and `--grants` to `admit` or `receipt`:
42
+ Every step produces a durable, verifiable artifact. The signed manifest, public
43
+ key ID, manifest hash, admission decision, request hash, and signed receipt are
44
+ all tamper-evident. Changing any field causes verification to fail.
45
+
46
+ ---
47
+
48
+ ## What works today
131
49
 
50
+ - YAML + JSON manifest loading with strict schema validation
51
+ - Ed25519 key generation; AES-256-GCM encrypted key storage with scrypt KDF
52
+ - Whole-envelope Ed25519 signatures covering manifest, hash, key, algorithm, and timestamp
53
+ - Explicit public-key trust anchors (active / retired / revoked)
54
+ - Signed key-rotation proofs preserving forward trust continuity
55
+ - Allow / deny decisions with machine-readable reason codes
56
+ - Destructive high-risk tool blocking by default policy
57
+ - ASCII-validated tool names (prevents Unicode homograph attacks)
58
+ - Manifest-scoped call budgets with cross-process atomic file locking
59
+ - Optional per-agent grant scoping
60
+ - Signed, tamper-evident Agent Action Receipts
61
+ - Receipt trust-chain verification
62
+ - TypeScript SDK exports
63
+
64
+ ---
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ npm install @dorigjo/besa
70
+ ```
71
+
72
+ Pin the beta channel explicitly:
73
+
74
+ ```bash
75
+ npm install @dorigjo/besa@beta
132
76
  ```
133
- node dist/index.js admit examples/manifest.signed.json crm.lookup --agent agent-alpha --grants examples/grants.yaml
77
+
78
+ Set the key passphrase before any signing operation:
79
+
80
+ ```bash
81
+ export BESA_KEY_PASSPHRASE="your-passphrase-at-least-16-bytes"
82
+ ```
83
+
84
+ ### Build from source
85
+
86
+ ```bash
87
+ git clone https://github.com/dorigjo/besa
88
+ cd besa
89
+ npm ci
90
+ npm run build
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Quickstart
96
+
97
+ ```bash
98
+ # Show available commands
99
+ npx besa --help
100
+
101
+ # Generate or load the local signing key
102
+ npx besa keys
103
+
104
+ # Validate the manifest (dry-run, no signing)
105
+ npx besa load examples/manifest.yaml
106
+
107
+ # Sign the manifest
108
+ npx besa sign examples/manifest.yaml
109
+
110
+ # Verify the signature
111
+ npx besa verify examples/manifest.signed.json
112
+
113
+ # Admission dry-run (does not consume budget)
114
+ npx besa admit examples/manifest.signed.json crm.lookup # → allow
115
+ npx besa admit examples/manifest.signed.json crm.delete # → deny RISK_BLOCKED
116
+
117
+ # Issue a signed receipt (consumes budget)
118
+ npx besa receipt crm.lookup examples/manifest.signed.json \
119
+ --request examples/request.json
120
+
121
+ # Verify the receipt chain
122
+ npx besa verify-receipt .besa/receipts/<id>.json \
123
+ examples/manifest.signed.json
124
+ ```
125
+
126
+ ### PowerShell
127
+
128
+ ```powershell
129
+ $env:BESA_KEY_PASSPHRASE = "your-passphrase-at-least-16-bytes"
130
+ npx besa keys
131
+ npx besa sign examples/manifest.yaml
132
+ npx besa verify examples/manifest.signed.json
133
+ npx besa admit examples/manifest.signed.json crm.lookup
134
+ npx besa receipt crm.lookup examples/manifest.signed.json `
135
+ --request examples/request.json
136
+
137
+ $receipt = Get-ChildItem .\.besa\receipts\*.json |
138
+ Sort-Object LastWriteTime -Descending | Select-Object -First 1
139
+ npx besa verify-receipt $receipt.FullName examples/manifest.signed.json
140
+ ```
141
+
142
+ ### Consumer trust (separate system)
143
+
144
+ ```bash
145
+ # Pin the publisher's public key
146
+ npx besa trust add examples/manifest.signed.json \
147
+ --trust consumer-trust.json
148
+
149
+ # Verify against a pinned trust anchor (fails without it)
150
+ npx besa verify examples/manifest.signed.json \
151
+ --trust consumer-trust.json
134
152
  ```
135
153
 
136
- - `--agent <id>`: the id of the calling agent.
137
- - `--grants <file>`: the grants file to check against.
138
- - If the agent is not granted the tool, admission is denied (`TOOL_NOT_GRANTED`, or `AGENT_NOT_FOUND` for an unknown agent), and the receipt records `agentId` and `grantReasonCode`.
139
-
140
- Grants are **optional and backward-compatible**: without `--grants`, admission behaves exactly as before.
141
-
142
- ## Core concepts
143
-
144
- ### Tool Manifest
145
-
146
- A YAML or JSON file that declares a tool server and its tools.
147
-
148
- Each tool has:
149
-
150
- * name
151
- * description
152
- * capability
153
- * risk
154
- * scopes
155
- * budgetLimit
156
- * inputSchema
157
-
158
- Capabilities:
159
-
160
- * read
161
- * write
162
- * destructive
163
-
164
- Risk levels:
165
-
166
- * low
167
- * medium
168
- * high
169
-
170
- ### Signed Manifest
171
-
172
- A manifest signed with Ed25519.
173
-
174
- The signed manifest includes:
175
-
176
- * manifest
177
- * manifestHash
178
- * algorithm
179
- * publicKey
180
- * publicKeyId
181
- * signature
182
- * signedAt
183
-
184
- ### Admission Decision
185
-
186
- Besa evaluates whether a tool call should be allowed or denied.
187
-
188
- Reason codes include:
189
-
190
- * `ALLOWED`
191
- * `TOOL_NOT_FOUND`
192
- * `RISK_BLOCKED`
193
- * `BUDGET_EXCEEDED`
194
-
195
- ### Mini ActionMeter
196
-
197
- Besa tracks local call counts per tool.
198
-
199
- This allows simple budget enforcement through `budgetLimit`.
200
-
201
- ### Signed Receipt
202
-
203
- A receipt proves what decision was made.
204
-
205
- A receipt includes:
206
-
207
- * receiptId
208
- * manifestHash
209
- * toolName
210
- * decision
211
- * reasonCode
212
- * timestamp
213
- * requestHash
214
- * publicKeyId
215
- * algorithm
216
- * signature
217
-
218
- ## SDK usage
219
-
220
- Import Besa from the SDK:
221
-
222
- ```ts
223
- import {
224
- loadManifest,
225
- generateKeyPair,
226
- signManifest,
227
- verifySignedManifest,
228
- admit,
229
- createReceipt,
230
- verifyReceipt,
231
- } from "besa";
232
- ```
233
-
234
- Basic flow:
235
-
236
- ```ts
237
- const manifest = loadManifest("examples/manifest.yaml");
238
-
239
- const keypair = generateKeyPair();
240
-
241
- const signed = signManifest(manifest, keypair);
242
-
243
- const verified = verifySignedManifest(signed);
244
-
245
- if (!verified.valid) {
246
- throw new Error(verified.reasonCode);
247
- }
248
-
249
- const decision = admit(signed, "crm.lookup");
250
-
251
- const receipt = createReceipt(signed, decision, keypair);
252
-
253
- const receiptResult = verifyReceipt(receipt);
254
-
255
- if (!receiptResult.valid) {
256
- throw new Error(receiptResult.reasonCode);
257
- }
258
- ```
259
-
260
- ## Security
261
-
262
- Never commit `.besa/`.
263
-
264
- The `.besa/` folder contains local trust artifacts, including the Ed25519 private key.
265
-
266
- Ignored local artifacts:
267
-
268
- * `.besa/`
269
- * `.besa/key.json`
270
- * `.besa/meter.json`
271
- * `.besa/receipts/`
272
- * `examples/manifest.signed.json`
273
-
274
- The local key generated by this MVP is a demo key. Rotate keys before real usage.
275
-
276
- See:
277
-
278
- * [SECURITY.md](SECURITY.md)
279
- * [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md)
280
-
281
- ## MVP limitations
282
-
283
- This is an MVP and alpha developer preview.
284
-
285
- Current limitations:
286
-
287
- * local key storage only
288
- * local JSON meter only
289
- * no hosted registry
290
- * no SaaS backend
291
- * no dashboard
292
- * no remote verifier API
293
- * no hosted receipts API
294
- * no distributed replay protection
295
- * no key rotation
296
- * no key revocation
297
- * one default policy
298
-
299
- Default policy:
300
-
301
- * destructive + high risk = denied
302
-
303
- ## What Besa is not
304
-
305
- Besa is currently an alpha trust layer for AI-agent tool control and evidence.
306
-
307
- It is not:
308
-
309
- * a hosted SaaS
310
- * a dashboard or UI
311
- * a full MCP gateway
312
- * production key management
313
- * a compliance certification product
314
- * a replacement for identity, authorization, audit storage, or security monitoring
315
- * ready for production secrets or production systems
316
-
317
- ## Release docs
318
-
319
- * [SECURITY.md](SECURITY.md) — security policy, key handling, and vulnerability reporting
320
- * [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) — assets, threats, mitigations, and current MVP limitations
321
- * [docs/RELEASE_CHECKLIST.md](docs/RELEASE_CHECKLIST.md) — pre-release gates before tagging or publishing
322
- * [CHANGELOG.md](CHANGELOG.md) — notable changes by version
323
-
324
- ## Roadmap
325
-
326
- Planned next layers:
327
-
328
- * hosted key management
329
- * remote verifier API
330
- * policy packs
331
- * MCP gateway integration
332
- * enterprise audit export
333
- * receipts API
334
- * usage-based ActionMeter
335
- * organization-level trust registry
336
-
337
- ## Positioning
338
-
339
- Besa is signed trust infrastructure for AI-agent tools.
340
-
341
- It is not another chatbot.
342
-
343
- It is not another dashboard.
344
-
345
- It is a trust layer for agentic execution.
154
+ ### Key rotation
155
+
156
+ ```bash
157
+ npx besa keys rotate
158
+
159
+ npx besa trust apply .besa/rotations/<rotation>.json \
160
+ --trust consumer-trust.json
161
+
162
+ npx besa sign examples/manifest.yaml # re-sign under the new key
163
+ ```
164
+
165
+ The previous key becomes `retired`: artifacts signed before rotation remain
166
+ verifiable, but new admissions under that key are denied. `trust revoke`
167
+ invalidates a key for all artifacts, current and historical.
168
+
169
+ ---
170
+
171
+ ## Commands
172
+
173
+ | Command | Description |
174
+ |---|---|
175
+ | `besa keys` | Generate or display the local signing key |
176
+ | `besa keys rotate` | Rotate to a new key, archive the previous |
177
+ | `besa trust add <manifest>` | Pin the manifest's public key as a trust anchor |
178
+ | `besa trust apply <rotation>` | Apply a signed rotation proof |
179
+ | `besa trust revoke <key-id>` | Revoke a trust anchor |
180
+ | `besa trust list` | List all trust anchors and their status |
181
+ | `besa load <manifest>` | Validate a manifest without signing |
182
+ | `besa sign <manifest>` | Sign a manifest |
183
+ | `besa verify <manifest>` | Verify a signed manifest against the trust store |
184
+ | `besa admit <manifest> <tool>` | Dry-run: check policy + budget |
185
+ | `besa receipt <tool> <manifest>` | Enforce budget and issue a signed receipt |
186
+ | `besa verify-receipt <receipt> <manifest>` | Verify the receipt trust chain |
187
+
188
+ All commands accept `--trust <trust.json>` to use a consumer-side trust store.
189
+ `admit` and `receipt` also accept `--agent <id> --grants <grants.yaml>`.
190
+
191
+ ---
192
+
193
+ ## Receipt artifact
194
+
195
+ ```json
196
+ {
197
+ "receiptId": "rcpt_2d7942c7-8f70-4984-9c3f-24876acfd860",
198
+ "manifestHash": "ea7e9ca22d199f40281cdf9e5d6145440c6c7d6bfbe94157c4b1da5527054410",
199
+ "toolName": "crm.lookup",
200
+ "decision": "allow",
201
+ "reasonCode": "ALLOWED",
202
+ "timestamp": "2026-06-19T10:00:00.000Z",
203
+ "requestHash": "b27b80d1227c167a6fca199778645daa77d20a8087782fc48802d11d6281c920",
204
+ "publicKeyId": "f68668614543c4896cf8cee418492f1a4df1f1acdba8850f94728b8a94cf90fe",
205
+ "algorithm": "ed25519",
206
+ "signature": "<base64-ed25519-signature>"
207
+ }
208
+ ```
209
+
210
+ `publicKeyId` is the full SHA-256 fingerprint of the Ed25519 public key DER bytes.
211
+ Changing any field causes `verify-receipt` to fail closed.
212
+
213
+ ---
214
+
215
+ ## Reason codes
216
+
217
+ | Code | Meaning |
218
+ |---|---|
219
+ | `ALLOWED` | Tool call admitted |
220
+ | `TOOL_NOT_FOUND` | Tool not declared in the signed manifest |
221
+ | `RISK_BLOCKED` | Destructive high-risk tool blocked by policy |
222
+ | `BUDGET_EXCEEDED` | Call count reached the manifest budget limit |
223
+ | `TOOL_NOT_GRANTED` | Agent not granted access to this tool |
224
+ | `AGENT_NOT_FOUND` | Agent ID not listed in the grant set |
225
+ | `E_KEY_UNTRUSTED` | Signing key not in the trust store |
226
+ | `E_KEY_RETIRED` | Key retired; new admissions under it are denied |
227
+ | `E_KEY_REVOKED` | Key revoked; all operations denied |
228
+
229
+ ---
230
+
231
+ ## SDK
232
+
233
+ ```typescript
234
+ import {
235
+ admit,
236
+ addTrustAnchor,
237
+ applyKeyRotation,
238
+ canonicalize,
239
+ checkTrustedKey,
240
+ createKeyRotation,
241
+ createReceipt,
242
+ generateKeyPair,
243
+ hashRequest,
244
+ loadManifest,
245
+ signManifest,
246
+ validateManifest,
247
+ validateReceipt,
248
+ verifyReceiptDetailed,
249
+ verifySignedManifest,
250
+ verifyTrustedSignedManifest,
251
+ } from "@dorigjo/besa";
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Security model
257
+
258
+ Besa provides **tamper-evidence**, not secrecy.
259
+
260
+ A signed manifest proves that the declared tool capabilities, scopes, risks, and
261
+ metadata have not changed since signing. A signed receipt creates a
262
+ tamper-evident record that a specific admission decision was made at a specific
263
+ time under a specific key.
264
+
265
+ **Cryptography:**
266
+
267
+ - Ed25519 signatures (256-bit security) on the complete artifact envelope
268
+ - AES-256-GCM key encryption at rest with scrypt KDF (N=32768, r=8, p=1)
269
+ - SHA-256 manifest hashing and full 256-bit (64-hex-character) SHA-256 public key fingerprints
270
+ - Domain-separated signature messages (`besa:<domain>:v1\0<canonical-json>`)
271
+ - Timing-safe public key comparison via `crypto.timingSafeEqual`
272
+
273
+ **Fail-closed behavior:**
274
+
275
+ - Verification fails on any signature, hash, or key mismatch
276
+ - Admission fails closed on invalid policy, manifest, or call count
277
+ - Trust store rejects symlinks, unknown fields, and duplicate key IDs
278
+ - Trust store paths must end in `.json`
279
+ - Tool names are restricted to ASCII printable characters
280
+
281
+ See [SECURITY.md](SECURITY.md) and [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md).
282
+
283
+ ---
284
+
285
+ ## Release gates
286
+
287
+ ```bash
288
+ npm ci
289
+ npm run build
290
+ npm test # 56 tests
291
+ npm run smoke # end-to-end trust flow
292
+ npm run test:package
293
+ npm pack --dry-run
294
+ ```
295
+
296
+ ---
297
+
298
+ ## Beta limitations
299
+
300
+ - Local key storage only; no hosted key management or HSM integration
301
+ - File-based meter and trust state; intended for single-host use
302
+ - No distributed replay protection across machines or environments
303
+ - No external trusted timestamp authority
304
+ - No hosted verifier, receipt retention, or SIEM export
305
+ - No production identity or multi-user authorization
306
+ - No formal compliance certification (SOC 2, ISO 27001, EU AI Act)
307
+
308
+ ---
309
+
310
+ ## License
311
+
312
+ [MIT](LICENSE)
package/dist/admit.d.ts CHANGED
@@ -4,15 +4,26 @@ export declare const REASON: {
4
4
  readonly TOOL_NOT_FOUND: "TOOL_NOT_FOUND";
5
5
  readonly RISK_BLOCKED: "RISK_BLOCKED";
6
6
  readonly BUDGET_EXCEEDED: "BUDGET_EXCEEDED";
7
+ readonly INVALID_MANIFEST: "INVALID_MANIFEST";
8
+ readonly INVALID_TOOL_NAME: "INVALID_TOOL_NAME";
9
+ readonly INVALID_CALL_COUNT: "INVALID_CALL_COUNT";
10
+ readonly INVALID_POLICY: "INVALID_POLICY";
7
11
  };
8
12
  export interface AdmissionPolicy {
9
13
  denyDestructiveHighRisk: boolean;
10
14
  }
11
15
  export declare const DEFAULT_POLICY: AdmissionPolicy;
12
16
  export type MeterState = Record<string, number>;
17
+ export interface MeterLockOptions {
18
+ timeoutMs?: number;
19
+ staleMs?: number;
20
+ retryMs?: number;
21
+ }
13
22
  export declare function findTool(manifest: Manifest, toolName: string): ToolDefinition | undefined;
14
23
  export declare function admit(manifest: Manifest, toolName: string, callCount: number, policy?: AdmissionPolicy): AdmissionDecision;
24
+ export declare function meterKey(manifestHash: string, toolName: string): string;
15
25
  export declare function loadMeter(path: string): MeterState;
16
26
  export declare function saveMeter(path: string, state: MeterState): void;
17
- export declare function getCount(state: MeterState, toolName: string): number;
18
- export declare function increment(state: MeterState, toolName: string): MeterState;
27
+ export declare function getCount(state: MeterState, key: string): number;
28
+ export declare function increment(state: MeterState, key: string): MeterState;
29
+ export declare function admitAndConsume(path: string, manifestHash: string, manifest: Manifest, toolName: string, policy?: AdmissionPolicy, lockOptions?: MeterLockOptions): AdmissionDecision;