@eddacraft/anvil-kindling-integration 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 +14 -0
- package/README.md +542 -0
- package/dist/adapter.d.ts +49 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +100 -0
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +173 -0
- package/dist/emitters/action-emitter.d.ts +40 -0
- package/dist/emitters/action-emitter.d.ts.map +1 -0
- package/dist/emitters/action-emitter.js +52 -0
- package/dist/emitters/constraint-emitter.d.ts +32 -0
- package/dist/emitters/constraint-emitter.d.ts.map +1 -0
- package/dist/emitters/constraint-emitter.js +41 -0
- package/dist/emitters/error-emitter.d.ts +33 -0
- package/dist/emitters/error-emitter.d.ts.map +1 -0
- package/dist/emitters/error-emitter.js +50 -0
- package/dist/emitters/gate-emitter.d.ts +37 -0
- package/dist/emitters/gate-emitter.d.ts.map +1 -0
- package/dist/emitters/gate-emitter.js +53 -0
- package/dist/emitters/human-input-emitter.d.ts +30 -0
- package/dist/emitters/human-input-emitter.d.ts.map +1 -0
- package/dist/emitters/human-input-emitter.js +38 -0
- package/dist/emitters/index.d.ts +13 -0
- package/dist/emitters/index.d.ts.map +1 -0
- package/dist/emitters/index.js +19 -0
- package/dist/emitters/plan-emitter.d.ts +75 -0
- package/dist/emitters/plan-emitter.d.ts.map +1 -0
- package/dist/emitters/plan-emitter.js +116 -0
- package/dist/emitters/session-emitter.d.ts +57 -0
- package/dist/emitters/session-emitter.d.ts.map +1 -0
- package/dist/emitters/session-emitter.js +80 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +111 -0
- package/dist/kindling-service.d.ts +122 -0
- package/dist/kindling-service.d.ts.map +1 -0
- package/dist/kindling-service.js +203 -0
- package/dist/observation-contract.d.ts +561 -0
- package/dist/observation-contract.d.ts.map +1 -0
- package/dist/observation-contract.js +391 -0
- package/dist/query-contract.d.ts +463 -0
- package/dist/query-contract.d.ts.map +1 -0
- package/dist/query-contract.js +314 -0
- package/dist/query-limits.d.ts +40 -0
- package/dist/query-limits.d.ts.map +1 -0
- package/dist/query-limits.js +79 -0
- package/dist/query-service.d.ts +109 -0
- package/dist/query-service.d.ts.map +1 -0
- package/dist/query-service.js +140 -0
- package/dist/retention.d.ts +79 -0
- package/dist/retention.d.ts.map +1 -0
- package/dist/retention.js +81 -0
- package/dist/sensitive-data-validator.d.ts +47 -0
- package/dist/sensitive-data-validator.d.ts.map +1 -0
- package/dist/sensitive-data-validator.js +135 -0
- package/dist/status.d.ts +104 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +136 -0
- package/dist/utils/debug.d.ts +9 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +55 -0
- package/package.json +114 -0
- package/src/adapter.ts +117 -0
- package/src/config.ts +202 -0
- package/src/emitters/action-emitter.ts +90 -0
- package/src/emitters/constraint-emitter.ts +73 -0
- package/src/emitters/error-emitter.ts +86 -0
- package/src/emitters/gate-emitter.ts +87 -0
- package/src/emitters/human-input-emitter.ts +71 -0
- package/src/emitters/index.ts +40 -0
- package/src/emitters/plan-emitter.ts +183 -0
- package/src/emitters/session-emitter.ts +131 -0
- package/src/index.ts +254 -0
- package/src/kindling-service.ts +272 -0
- package/src/malicious-ai.test.ts +949 -0
- package/src/observation-contract.ts +500 -0
- package/src/query-contract.ts +389 -0
- package/src/query-limits.ts +106 -0
- package/src/query-service.ts +217 -0
- package/src/retention.ts +153 -0
- package/src/sensitive-data-validator.ts +167 -0
- package/src/status.ts +221 -0
- package/src/utils/debug.ts +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Copyright (c) 2026 EddaCraft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
PROPRIETARY AND CONFIDENTIAL
|
|
4
|
+
|
|
5
|
+
This software and associated documentation files (the "Software") are the
|
|
6
|
+
exclusive property of EddaCraft. Unauthorised copying, modification,
|
|
7
|
+
distribution, or use of this Software, via any medium, is strictly prohibited
|
|
8
|
+
without the express written permission of EddaCraft.
|
|
9
|
+
|
|
10
|
+
The Software is provided for evaluation and testing purposes only to authorised
|
|
11
|
+
beta testers. No licence is granted to use, copy, modify, or distribute the
|
|
12
|
+
Software for any other purpose.
|
|
13
|
+
|
|
14
|
+
For licensing enquiries, contact: legal@eddacraft.com
|
package/README.md
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
# @eddacraft/anvil-kindling-integration
|
|
2
|
+
|
|
3
|
+
> Mechanical contracts and integration layer for Kindling memory in Anvil v1
|
|
4
|
+
|
|
5
|
+
This package provides the **read-only, queryable memory contract** between Anvil
|
|
6
|
+
and Kindling. It defines what Anvil records (observations), how to retrieve them
|
|
7
|
+
(queries), and enforces that user-supplied AI can read but never mutate the
|
|
8
|
+
system of record.
|
|
9
|
+
|
|
10
|
+
No embedded AI. No magic. Just mechanics.
|
|
11
|
+
|
|
12
|
+
## Governing Rule
|
|
13
|
+
|
|
14
|
+
> **Kindling is a system of record, not a reasoning engine.** Queries may
|
|
15
|
+
> retrieve facts; interpretation is the caller's responsibility.
|
|
16
|
+
|
|
17
|
+
Anvil enforces this mechanically:
|
|
18
|
+
|
|
19
|
+
- User-supplied AI may **read**, but may not **mutate, infer, or generalise**
|
|
20
|
+
via Kindling
|
|
21
|
+
- All queries are **bounded, explicit, and evidence-preserving**
|
|
22
|
+
- No free-text search. No global scans. No cross-project reads.
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
+----------------+
|
|
28
|
+
| Kindling |
|
|
29
|
+
| (SQLite DB) |
|
|
30
|
+
+----------------+
|
|
31
|
+
^ |
|
|
32
|
+
Write | | Read
|
|
33
|
+
Only | | Only
|
|
34
|
+
| v
|
|
35
|
+
+-----------------+-------------------+
|
|
36
|
+
| |
|
|
37
|
+
+----v------+ +-------v--------+
|
|
38
|
+
| Anvil | | User AI |
|
|
39
|
+
| Emits | | (BYO-AI) |
|
|
40
|
+
| Facts | | Queries + |
|
|
41
|
+
+----------+ | Interprets |
|
|
42
|
+
+----------------+
|
|
43
|
+
|
|
44
|
+
Truth flows one way: Anvil -> Kindling -> AI
|
|
45
|
+
AI never writes back to Kindling
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Package Structure
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
packages/kindling-integration/
|
|
52
|
+
src/
|
|
53
|
+
observation-contract.ts # Write-only (11 observation kinds)
|
|
54
|
+
query-contract.ts # Read-only (4 query scopes)
|
|
55
|
+
index.ts # Public exports
|
|
56
|
+
kindling-service.ts # Core service (emit + query)
|
|
57
|
+
config.ts # Configuration schema
|
|
58
|
+
query-service.ts # Read-only query API
|
|
59
|
+
query-limits.ts # Anti-vacuum-cleaner enforcement
|
|
60
|
+
sensitive-data-validator.ts # Secret detection
|
|
61
|
+
retention.ts # Auto-pruning
|
|
62
|
+
status.ts # Status utility (decoupled from CLI)
|
|
63
|
+
malicious-ai.test.ts # Read-only enforcement tests
|
|
64
|
+
emitters/
|
|
65
|
+
session-emitter.ts # session_start / session_end
|
|
66
|
+
gate-emitter.ts # gate_evaluated
|
|
67
|
+
action-emitter.ts # action_executed
|
|
68
|
+
plan-emitter.ts # plan_created / edited / approved / rejected
|
|
69
|
+
human-input-emitter.ts # human_input
|
|
70
|
+
constraint-emitter.ts # constraint_applied
|
|
71
|
+
error-emitter.ts # error
|
|
72
|
+
benchmarks/
|
|
73
|
+
emission-overhead.bench.ts # Performance validation
|
|
74
|
+
scripts/
|
|
75
|
+
generate-openapi.ts # OpenAPI 3.1 spec generator
|
|
76
|
+
CONTRACTS.md # Contract summary
|
|
77
|
+
README.md # This file
|
|
78
|
+
openapi.json # Generated OpenAPI spec
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Contracts Overview
|
|
82
|
+
|
|
83
|
+
### 1. Observation Contract (Write-Only)
|
|
84
|
+
|
|
85
|
+
**File:** [`src/observation-contract.ts`](./src/observation-contract.ts)
|
|
86
|
+
|
|
87
|
+
Defines the 11 observation kinds Anvil must emit to be "Kindling-complete":
|
|
88
|
+
|
|
89
|
+
| Kind | Purpose | When Emitted |
|
|
90
|
+
| -------------------- | ------------------------- | ------------------------------- |
|
|
91
|
+
| `session_start` | Session recording spine | Every Anvil run starts |
|
|
92
|
+
| `session_end` | Session outcome + summary | Every Anvil run completes |
|
|
93
|
+
| `plan_created` | Plan lifecycle tracking | New plan authored |
|
|
94
|
+
| `plan_edited` | Plan version history | Plan modified |
|
|
95
|
+
| `plan_approved` | Human approval | User approves plan |
|
|
96
|
+
| `plan_rejected` | Human rejection | User rejects plan |
|
|
97
|
+
| `action_executed` | Action provenance | Command/tool/file operation |
|
|
98
|
+
| `gate_evaluated` | Gate check result | Every gate evaluation |
|
|
99
|
+
| `constraint_applied` | Decision constraint | Action prevented by rule/policy |
|
|
100
|
+
| `human_input` | Human decision | Approval/override/rejection |
|
|
101
|
+
| `error` | Failure history | Command/tool/execution error |
|
|
102
|
+
|
|
103
|
+
**All observations are:**
|
|
104
|
+
|
|
105
|
+
- Immutable (write-once)
|
|
106
|
+
- Timestamped (ISO8601)
|
|
107
|
+
- Linked (session_id, plan_id, gate_id, action_id)
|
|
108
|
+
- Sanitised (no secrets, redacted commands)
|
|
109
|
+
- Facts only (no interpretation, no inference)
|
|
110
|
+
|
|
111
|
+
### 2. Query Contract (Read-Only)
|
|
112
|
+
|
|
113
|
+
**File:** [`src/query-contract.ts`](./src/query-contract.ts)
|
|
114
|
+
|
|
115
|
+
Defines 4 bounded query scopes:
|
|
116
|
+
|
|
117
|
+
| Scope | Question | Required ID |
|
|
118
|
+
| --------- | ------------------------------------- | -------------- |
|
|
119
|
+
| `session` | "What happened in this run?" | `session_id` |
|
|
120
|
+
| `plan` | "What happened because of this plan?" | `plan_id` |
|
|
121
|
+
| `gate` | "Why did this gate pass/fail?" | `gate_eval_id` |
|
|
122
|
+
| `action` | "What exactly did this action do?" | `action_id` |
|
|
123
|
+
|
|
124
|
+
## Service Setup and Configuration
|
|
125
|
+
|
|
126
|
+
### Configuration
|
|
127
|
+
|
|
128
|
+
Kindling is opt-in and disabled by default:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// In .anvilrc
|
|
132
|
+
{
|
|
133
|
+
"kindling": {
|
|
134
|
+
"enabled": true,
|
|
135
|
+
"database_path": ".anvil/kindling.db",
|
|
136
|
+
"retention": {
|
|
137
|
+
"days": 90,
|
|
138
|
+
"auto_prune": false
|
|
139
|
+
},
|
|
140
|
+
"capture": {
|
|
141
|
+
"sessions": true,
|
|
142
|
+
"plans": true,
|
|
143
|
+
"gates": true,
|
|
144
|
+
"actions": true,
|
|
145
|
+
"constraints": true,
|
|
146
|
+
"human_inputs": true,
|
|
147
|
+
"errors": true
|
|
148
|
+
},
|
|
149
|
+
"query_limits": {
|
|
150
|
+
"max_results": 100,
|
|
151
|
+
"max_payload_bytes": 1048576
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Service Initialisation
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { KindlingService } from '@eddacraft/anvil-kindling-integration';
|
|
161
|
+
|
|
162
|
+
const kindling = new KindlingService(store, config);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Emitter Usage
|
|
166
|
+
|
|
167
|
+
Emitters are specialised helpers for each observation kind. They construct
|
|
168
|
+
properly-typed observations and emit them through the service.
|
|
169
|
+
|
|
170
|
+
### Session Emitter
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { SessionStartObservation } from '@eddacraft/anvil-kindling-integration/observation';
|
|
174
|
+
|
|
175
|
+
// Emit at command entry
|
|
176
|
+
const obs: SessionStartObservation = {
|
|
177
|
+
kind: 'session_start',
|
|
178
|
+
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
179
|
+
timestamp: new Date().toISOString(),
|
|
180
|
+
context: {
|
|
181
|
+
working_directory: '/home/user/project',
|
|
182
|
+
anvil_version: '1.0.0',
|
|
183
|
+
command: 'anvil check',
|
|
184
|
+
args: ['--watch'],
|
|
185
|
+
environment: 'development',
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
await kindling.emit(obs);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Gate Emitter
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { GateEvaluatedObservation } from '@eddacraft/anvil-kindling-integration/observation';
|
|
195
|
+
|
|
196
|
+
const obs: GateEvaluatedObservation = {
|
|
197
|
+
kind: 'gate_evaluated',
|
|
198
|
+
session_id: sessionId,
|
|
199
|
+
timestamp: new Date().toISOString(),
|
|
200
|
+
gate_eval_id: 'gate-eval-001',
|
|
201
|
+
gate_id: 'architecture',
|
|
202
|
+
inputs: {
|
|
203
|
+
file_count: 12,
|
|
204
|
+
changed_files: ['src/index.ts', 'src/config.ts'],
|
|
205
|
+
},
|
|
206
|
+
outcome: 'pass',
|
|
207
|
+
rules_evaluated: ['no-circular-deps', 'layer-boundaries'],
|
|
208
|
+
enforcement: 'blocking',
|
|
209
|
+
duration_ms: 250,
|
|
210
|
+
};
|
|
211
|
+
await kindling.emit(obs);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Validation Before Emission
|
|
215
|
+
|
|
216
|
+
Always validate observations before emitting:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import {
|
|
220
|
+
validateObservation,
|
|
221
|
+
containsSensitiveData,
|
|
222
|
+
} from '@eddacraft/anvil-kindling-integration';
|
|
223
|
+
|
|
224
|
+
const validation = validateObservation(obs);
|
|
225
|
+
if (!validation.success) {
|
|
226
|
+
console.error('Invalid observation:', validation.error);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const sensitiveCheck = containsSensitiveData(validation.data);
|
|
231
|
+
if (sensitiveCheck.hasSensitiveData) {
|
|
232
|
+
console.error('Sensitive data detected:', sensitiveCheck.issues);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await kindling.emit(validation.data);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Query API Usage
|
|
240
|
+
|
|
241
|
+
### Session Query
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import {
|
|
245
|
+
SessionQuery,
|
|
246
|
+
validateQueryRequest,
|
|
247
|
+
} from '@eddacraft/anvil-kindling-integration';
|
|
248
|
+
|
|
249
|
+
const query: SessionQuery = {
|
|
250
|
+
scope: 'session',
|
|
251
|
+
session_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
252
|
+
shape: 'timeline',
|
|
253
|
+
format: 'json',
|
|
254
|
+
max_results: 100,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const result = validateQueryRequest(query);
|
|
258
|
+
if (result.success) {
|
|
259
|
+
const response = await kindling.query(result.data);
|
|
260
|
+
console.log(`${response.metadata.result_count} observations`);
|
|
261
|
+
console.log(`Truncated: ${response.metadata.truncated}`);
|
|
262
|
+
for (const obs of response.observations) {
|
|
263
|
+
console.log(` [${obs.timestamp}] ${obs.kind}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Plan Query (Cross-Session)
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { PlanQuery } from '@eddacraft/anvil-kindling-integration';
|
|
272
|
+
|
|
273
|
+
// This is the ONLY cross-session read allowed
|
|
274
|
+
const query: PlanQuery = {
|
|
275
|
+
scope: 'plan',
|
|
276
|
+
plan_id: 'plan-001',
|
|
277
|
+
shape: 'entity',
|
|
278
|
+
include_executions: true,
|
|
279
|
+
include_versions: true,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const response = await kindling.query(query);
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Gate Query
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import { GateQuery } from '@eddacraft/anvil-kindling-integration';
|
|
289
|
+
|
|
290
|
+
const query: GateQuery = {
|
|
291
|
+
scope: 'gate',
|
|
292
|
+
gate_eval_id: 'gate-eval-456',
|
|
293
|
+
shape: 'entity',
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const response = await kindling.query(query);
|
|
297
|
+
// Returns: gate evaluation with rule IDs, inputs (sanitised), outcome
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Action Query
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { ActionQuery } from '@eddacraft/anvil-kindling-integration';
|
|
304
|
+
|
|
305
|
+
const query: ActionQuery = {
|
|
306
|
+
scope: 'action',
|
|
307
|
+
action_id: 'action-789',
|
|
308
|
+
shape: 'entity',
|
|
309
|
+
include_approval_chain: true,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const response = await kindling.query(query);
|
|
313
|
+
// Returns: action details, redacted command, governance links
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## CLI Command Mapping
|
|
317
|
+
|
|
318
|
+
All queries have CLI equivalents. The CLI is a **thin wrapper** over the same
|
|
319
|
+
query surface.
|
|
320
|
+
|
|
321
|
+
| CLI Command | Query Scope | Query Type |
|
|
322
|
+
| ------------------------ | ----------- | -------------- |
|
|
323
|
+
| `anvil run show <id>` | session | `SessionQuery` |
|
|
324
|
+
| `anvil plan trace <id>` | plan | `PlanQuery` |
|
|
325
|
+
| `anvil gate show <id>` | gate | `GateQuery` |
|
|
326
|
+
| `anvil action show <id>` | action | `ActionQuery` |
|
|
327
|
+
|
|
328
|
+
**Examples:**
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
# Session timeline (JSON)
|
|
332
|
+
anvil run show 550e8400-e29b-41d4-a716-446655440000 --json
|
|
333
|
+
|
|
334
|
+
# Plan trace with linked executions
|
|
335
|
+
anvil plan trace plan-001 --json
|
|
336
|
+
|
|
337
|
+
# Gate evaluation details
|
|
338
|
+
anvil gate show gate-eval-456 --json
|
|
339
|
+
|
|
340
|
+
# Action with approval chain
|
|
341
|
+
anvil action show action-789 --json
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Security Model
|
|
345
|
+
|
|
346
|
+
### Read-Only Enforcement
|
|
347
|
+
|
|
348
|
+
Operations that **MUST NOT** exist in the query API:
|
|
349
|
+
|
|
350
|
+
- `write()` / `update()` / `delete()`
|
|
351
|
+
- `annotate()` / `tag()`
|
|
352
|
+
- `learn()` / `embed()` / `infer()`
|
|
353
|
+
|
|
354
|
+
**If user AI wants memory, it must bring its own store.** The malicious AI test
|
|
355
|
+
suite (`src/malicious-ai.test.ts`) proves these boundaries hold.
|
|
356
|
+
|
|
357
|
+
### Sensitive Data Detection
|
|
358
|
+
|
|
359
|
+
Observations are validated before emission to catch:
|
|
360
|
+
|
|
361
|
+
- Passwords, tokens, API keys
|
|
362
|
+
- AWS credentials
|
|
363
|
+
- Private keys
|
|
364
|
+
- Email addresses (flagged as potentially sensitive)
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
const check = containsSensitiveData(obs);
|
|
368
|
+
if (check.hasSensitiveData) {
|
|
369
|
+
// Reject observation, log issues
|
|
370
|
+
console.error(check.issues);
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Query Limits (Anti-Vacuum-Cleaner)
|
|
375
|
+
|
|
376
|
+
Every query enforces:
|
|
377
|
+
|
|
378
|
+
- `max_results`: Default 100, max 1000
|
|
379
|
+
- `max_payload_bytes`: Default 1MB, max 10MB
|
|
380
|
+
- Mandatory scoping (scope + explicit ID)
|
|
381
|
+
- No free-text search, no global scans
|
|
382
|
+
|
|
383
|
+
### Output Guarantees
|
|
384
|
+
|
|
385
|
+
Every Kindling response guarantees:
|
|
386
|
+
|
|
387
|
+
1. **Stable field names** -- no field names change between queries
|
|
388
|
+
2. **Explicit timestamps** -- every observation has ISO8601 timestamp
|
|
389
|
+
3. **Explicit links** -- provenance via typed links (caused_by, governed_by,
|
|
390
|
+
approved_by)
|
|
391
|
+
4. **No hidden inference** -- payload contains only raw facts
|
|
392
|
+
5. **No reordered history** -- observations returned in recorded order
|
|
393
|
+
|
|
394
|
+
## Status Utility
|
|
395
|
+
|
|
396
|
+
Check Kindling integration status without coupling to any CLI framework:
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
import {
|
|
400
|
+
getKindlingStatus,
|
|
401
|
+
formatKindlingStatus,
|
|
402
|
+
} from '@eddacraft/anvil-kindling-integration';
|
|
403
|
+
|
|
404
|
+
// Quick check (no store needed)
|
|
405
|
+
const status = await getKindlingStatus({ enabled: false });
|
|
406
|
+
// => { enabled: false }
|
|
407
|
+
|
|
408
|
+
// Full status with store
|
|
409
|
+
const status = await getKindlingStatus(
|
|
410
|
+
{ enabled: true, retention: { days: 90 } },
|
|
411
|
+
myStore
|
|
412
|
+
);
|
|
413
|
+
console.log(formatKindlingStatus(status));
|
|
414
|
+
// Kindling: enabled
|
|
415
|
+
// Observations: 42
|
|
416
|
+
// Database size: 80 KB
|
|
417
|
+
// Retention: 90 days
|
|
418
|
+
// Last observation: 2026-02-15T10:30:00.000Z
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## BYO-AI Integration Example
|
|
422
|
+
|
|
423
|
+
User brings their own AI (e.g., Claude via API):
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
import {
|
|
427
|
+
QueryRequest,
|
|
428
|
+
QueryResponse,
|
|
429
|
+
} from '@eddacraft/anvil-kindling-integration';
|
|
430
|
+
|
|
431
|
+
async function explainGateFailure(gateEvalId: string): Promise<string> {
|
|
432
|
+
// 1. Query Kindling (read-only, bounded)
|
|
433
|
+
const query: QueryRequest = {
|
|
434
|
+
scope: 'gate',
|
|
435
|
+
gate_eval_id: gateEvalId,
|
|
436
|
+
shape: 'entity',
|
|
437
|
+
format: 'json',
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const response: QueryResponse = await kindling.query(query);
|
|
441
|
+
|
|
442
|
+
// 2. AI interprets facts (using external AI service)
|
|
443
|
+
const prompt = `
|
|
444
|
+
Explain why this gate failed based on these facts:
|
|
445
|
+
${JSON.stringify(response.observations, null, 2)}
|
|
446
|
+
`;
|
|
447
|
+
|
|
448
|
+
const explanation = await callExternalAI(prompt);
|
|
449
|
+
|
|
450
|
+
// 3. AI stores interpretation in its own memory (NOT in Kindling)
|
|
451
|
+
await userAI.memory.store({
|
|
452
|
+
type: 'gate_failure_explanation',
|
|
453
|
+
gate_eval_id: gateEvalId,
|
|
454
|
+
explanation,
|
|
455
|
+
generated_at: new Date().toISOString(),
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
return explanation;
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Key points:**
|
|
463
|
+
|
|
464
|
+
- AI reads from Kindling (bounded query)
|
|
465
|
+
- AI interprets facts (external reasoning)
|
|
466
|
+
- AI stores conclusions in **its own memory** (not Kindling)
|
|
467
|
+
- Kindling remains immutable system of record
|
|
468
|
+
|
|
469
|
+
## OpenAPI Spec Generation
|
|
470
|
+
|
|
471
|
+
Generate a machine-readable OpenAPI 3.1 spec from the query contracts:
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
npx tsx scripts/generate-openapi.ts
|
|
475
|
+
# Outputs: openapi.json
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Use the spec for:
|
|
479
|
+
|
|
480
|
+
- **Client library generation** (TypeScript, Python, Go, Rust, etc.)
|
|
481
|
+
- **API documentation** (Swagger UI, Redoc)
|
|
482
|
+
- **Contract testing** (validate implementations against the spec)
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
# Generate TypeScript client
|
|
486
|
+
npx openapi-generator-cli generate -i openapi.json -g typescript-fetch -o ./clients/ts
|
|
487
|
+
|
|
488
|
+
# Generate Python client
|
|
489
|
+
openapi-generator-cli generate -i openapi.json -g python -o ./clients/python
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Performance
|
|
493
|
+
|
|
494
|
+
Observation emission must add < 50ms overhead (async, non-blocking). The
|
|
495
|
+
benchmark suite validates this:
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
pnpm bench --filter kindling-integration
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
With a no-op store, emission overhead is typically < 1ms per observation
|
|
502
|
+
(validation + sensitive data check).
|
|
503
|
+
|
|
504
|
+
## Running Tests
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
# All tests
|
|
508
|
+
pnpm test --filter kindling-integration
|
|
509
|
+
|
|
510
|
+
# Malicious AI test suite only
|
|
511
|
+
pnpm test --filter kindling-integration -- --testNamePattern="malicious-ai"
|
|
512
|
+
|
|
513
|
+
# Benchmarks
|
|
514
|
+
pnpm bench --filter kindling-integration
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
## Explicit Non-Goals (v1)
|
|
518
|
+
|
|
519
|
+
The following are **OUT OF SCOPE** for v1:
|
|
520
|
+
|
|
521
|
+
- Semantic search / similarity queries / embeddings
|
|
522
|
+
- Cross-plan discovery (except explicit plan_id lookup)
|
|
523
|
+
- Learned relevance / pattern detection
|
|
524
|
+
- Auto-summaries stored in Kindling
|
|
525
|
+
- AI-generated annotations stored in Kindling
|
|
526
|
+
- Real-time streaming dashboard
|
|
527
|
+
- Team-level memory sharing
|
|
528
|
+
|
|
529
|
+
**These belong to Edda / Ember, not Kindling v1.**
|
|
530
|
+
|
|
531
|
+
## Licence
|
|
532
|
+
|
|
533
|
+
Copyright (c) 2026 EddaCraft. All rights reserved. See [LICENSE](../../LICENSE)
|
|
534
|
+
for details.
|
|
535
|
+
|
|
536
|
+
## See Also
|
|
537
|
+
|
|
538
|
+
- [CONTRACTS.md](./CONTRACTS.md) -- One-page contract summary
|
|
539
|
+
- [Kindling Integration Plan](../../plans/modules/kindling-integration.aps.md)
|
|
540
|
+
-- APS module specification
|
|
541
|
+
- [Kindling Repository](https://github.com/EddaCraft/kindling) -- Core Kindling
|
|
542
|
+
implementation
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anvil-Kindling Adapter
|
|
3
|
+
*
|
|
4
|
+
* Maps Anvil's 11 observation kinds (rich, domain-specific schemas)
|
|
5
|
+
* to Kindling core's generic observation model for storage and retrieval.
|
|
6
|
+
*
|
|
7
|
+
* Anvil observation data is serialized to `content` as JSON.
|
|
8
|
+
* The original Anvil kind is preserved in `provenance.anvil_kind`.
|
|
9
|
+
*/
|
|
10
|
+
import type { KindlingService, Capsule, ID } from '@eddacraft/kindling-core';
|
|
11
|
+
import type { Observation as AnvilObservation } from './observation-contract.js';
|
|
12
|
+
export interface AnvilKindlingAdapterConfig {
|
|
13
|
+
service: KindlingService;
|
|
14
|
+
/** Repo path for scope isolation */
|
|
15
|
+
repoId?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Bridges Anvil observation emission to Kindling storage.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* ```ts
|
|
22
|
+
* const adapter = new AnvilKindlingAdapter({ service });
|
|
23
|
+
* const capsule = adapter.startSession(sessionId, scopeIds);
|
|
24
|
+
* adapter.emit(observation);
|
|
25
|
+
* adapter.endSession(capsule.id);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare class AnvilKindlingAdapter {
|
|
29
|
+
private service;
|
|
30
|
+
private repoId;
|
|
31
|
+
constructor(config: AnvilKindlingAdapterConfig);
|
|
32
|
+
/**
|
|
33
|
+
* Open a Kindling capsule for an Anvil session.
|
|
34
|
+
* Call this when a CLI command starts.
|
|
35
|
+
*/
|
|
36
|
+
startSession(sessionId: string, intent: string): Capsule;
|
|
37
|
+
/**
|
|
38
|
+
* Close the capsule for a session.
|
|
39
|
+
* Call this when a CLI command ends.
|
|
40
|
+
*/
|
|
41
|
+
endSession(capsuleId: ID, summaryContent?: string): Capsule;
|
|
42
|
+
/**
|
|
43
|
+
* Emit an Anvil observation to Kindling.
|
|
44
|
+
* The rich Anvil schema is serialized to content; the original kind
|
|
45
|
+
* is preserved in provenance for filtering.
|
|
46
|
+
*/
|
|
47
|
+
emit(observation: AnvilObservation, capsuleId?: ID): void;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,0BAA0B,CAAC;AAC7E,OAAO,KAAK,EAAE,WAAW,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAqBjF,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,eAAe,CAAC;IACzB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,MAAM,CAAqB;gBAEvB,MAAM,EAAE,0BAA0B;IAM9C;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAYxD;;;OAGG;IACH,UAAU,CAAC,SAAS,EAAE,EAAE,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO;IAQ3D;;;;OAIG;IACH,IAAI,CAAC,WAAW,EAAE,gBAAgB,EAAE,SAAS,CAAC,EAAE,EAAE,GAAG,IAAI;CAuB1D"}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anvil-Kindling Adapter
|
|
3
|
+
*
|
|
4
|
+
* Maps Anvil's 11 observation kinds (rich, domain-specific schemas)
|
|
5
|
+
* to Kindling core's generic observation model for storage and retrieval.
|
|
6
|
+
*
|
|
7
|
+
* Anvil observation data is serialized to `content` as JSON.
|
|
8
|
+
* The original Anvil kind is preserved in `provenance.anvil_kind`.
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { OBSERVATION_CONTRACT_VERSION } from './observation-contract.js';
|
|
12
|
+
import { createDebugger } from './utils/debug.js';
|
|
13
|
+
const debug = createDebugger('kindling');
|
|
14
|
+
/** Map Anvil observation kinds to Kindling's generic observation kinds */
|
|
15
|
+
const KIND_MAP = {
|
|
16
|
+
session_start: 'message',
|
|
17
|
+
session_end: 'message',
|
|
18
|
+
plan_created: 'message',
|
|
19
|
+
plan_edited: 'message',
|
|
20
|
+
plan_approved: 'message',
|
|
21
|
+
plan_rejected: 'message',
|
|
22
|
+
action_executed: 'command',
|
|
23
|
+
gate_evaluated: 'command',
|
|
24
|
+
constraint_applied: 'message',
|
|
25
|
+
human_input: 'message',
|
|
26
|
+
error: 'error',
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Bridges Anvil observation emission to Kindling storage.
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* ```ts
|
|
33
|
+
* const adapter = new AnvilKindlingAdapter({ service });
|
|
34
|
+
* const capsule = adapter.startSession(sessionId, scopeIds);
|
|
35
|
+
* adapter.emit(observation);
|
|
36
|
+
* adapter.endSession(capsule.id);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class AnvilKindlingAdapter {
|
|
40
|
+
service;
|
|
41
|
+
repoId;
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.service = config.service;
|
|
44
|
+
this.repoId = config.repoId;
|
|
45
|
+
debug('AnvilKindlingAdapter created', { repoId: config.repoId });
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Open a Kindling capsule for an Anvil session.
|
|
49
|
+
* Call this when a CLI command starts.
|
|
50
|
+
*/
|
|
51
|
+
startSession(sessionId, intent) {
|
|
52
|
+
debug('starting session', { sessionId, intent });
|
|
53
|
+
return this.service.openCapsule({
|
|
54
|
+
type: 'session',
|
|
55
|
+
intent,
|
|
56
|
+
scopeIds: {
|
|
57
|
+
sessionId,
|
|
58
|
+
repoId: this.repoId,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Close the capsule for a session.
|
|
64
|
+
* Call this when a CLI command ends.
|
|
65
|
+
*/
|
|
66
|
+
endSession(capsuleId, summaryContent) {
|
|
67
|
+
debug('ending session', { capsuleId });
|
|
68
|
+
return this.service.closeCapsule(capsuleId, {
|
|
69
|
+
generateSummary: !!summaryContent,
|
|
70
|
+
summaryContent,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Emit an Anvil observation to Kindling.
|
|
75
|
+
* The rich Anvil schema is serialized to content; the original kind
|
|
76
|
+
* is preserved in provenance for filtering.
|
|
77
|
+
*/
|
|
78
|
+
emit(observation, capsuleId) {
|
|
79
|
+
debug('emitting observation via adapter', { kind: observation.kind, capsuleId });
|
|
80
|
+
const kindlingObs = {
|
|
81
|
+
id: randomUUID(),
|
|
82
|
+
kind: KIND_MAP[observation.kind],
|
|
83
|
+
content: JSON.stringify(observation),
|
|
84
|
+
provenance: {
|
|
85
|
+
anvil_kind: observation.kind,
|
|
86
|
+
anvil_contract_version: OBSERVATION_CONTRACT_VERSION,
|
|
87
|
+
},
|
|
88
|
+
ts: Date.now(),
|
|
89
|
+
scopeIds: {
|
|
90
|
+
sessionId: observation.session_id,
|
|
91
|
+
repoId: this.repoId,
|
|
92
|
+
},
|
|
93
|
+
redacted: false,
|
|
94
|
+
};
|
|
95
|
+
this.service.appendObservation(kindlingObs, {
|
|
96
|
+
capsuleId,
|
|
97
|
+
validate: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|