@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/package.json
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eddacraft/anvil-kindling-integration",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kindling memory integration contracts for Anvil v1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./query": {
|
|
14
|
+
"types": "./dist/query-contract.d.ts",
|
|
15
|
+
"import": "./dist/query-contract.js"
|
|
16
|
+
},
|
|
17
|
+
"./observation": {
|
|
18
|
+
"types": "./dist/observation-contract.d.ts",
|
|
19
|
+
"import": "./dist/observation-contract.js"
|
|
20
|
+
},
|
|
21
|
+
"./config": {
|
|
22
|
+
"types": "./dist/config.d.ts",
|
|
23
|
+
"import": "./dist/config.js"
|
|
24
|
+
},
|
|
25
|
+
"./service": {
|
|
26
|
+
"types": "./dist/kindling-service.d.ts",
|
|
27
|
+
"import": "./dist/kindling-service.js"
|
|
28
|
+
},
|
|
29
|
+
"./emitters": {
|
|
30
|
+
"types": "./dist/emitters/index.d.ts",
|
|
31
|
+
"import": "./dist/emitters/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./emitters/session": {
|
|
34
|
+
"types": "./dist/emitters/session-emitter.d.ts",
|
|
35
|
+
"import": "./dist/emitters/session-emitter.js"
|
|
36
|
+
},
|
|
37
|
+
"./emitters/gate": {
|
|
38
|
+
"types": "./dist/emitters/gate-emitter.d.ts",
|
|
39
|
+
"import": "./dist/emitters/gate-emitter.js"
|
|
40
|
+
},
|
|
41
|
+
"./emitters/action": {
|
|
42
|
+
"types": "./dist/emitters/action-emitter.d.ts",
|
|
43
|
+
"import": "./dist/emitters/action-emitter.js"
|
|
44
|
+
},
|
|
45
|
+
"./emitters/plan": {
|
|
46
|
+
"types": "./dist/emitters/plan-emitter.d.ts",
|
|
47
|
+
"import": "./dist/emitters/plan-emitter.js"
|
|
48
|
+
},
|
|
49
|
+
"./emitters/human-input": {
|
|
50
|
+
"types": "./dist/emitters/human-input-emitter.d.ts",
|
|
51
|
+
"import": "./dist/emitters/human-input-emitter.js"
|
|
52
|
+
},
|
|
53
|
+
"./emitters/constraint": {
|
|
54
|
+
"types": "./dist/emitters/constraint-emitter.d.ts",
|
|
55
|
+
"import": "./dist/emitters/constraint-emitter.js"
|
|
56
|
+
},
|
|
57
|
+
"./emitters/error": {
|
|
58
|
+
"types": "./dist/emitters/error-emitter.d.ts",
|
|
59
|
+
"import": "./dist/emitters/error-emitter.js"
|
|
60
|
+
},
|
|
61
|
+
"./query-service": {
|
|
62
|
+
"types": "./dist/query-service.d.ts",
|
|
63
|
+
"import": "./dist/query-service.js"
|
|
64
|
+
},
|
|
65
|
+
"./query-limits": {
|
|
66
|
+
"types": "./dist/query-limits.d.ts",
|
|
67
|
+
"import": "./dist/query-limits.js"
|
|
68
|
+
},
|
|
69
|
+
"./sensitive-data": {
|
|
70
|
+
"types": "./dist/sensitive-data-validator.d.ts",
|
|
71
|
+
"import": "./dist/sensitive-data-validator.js"
|
|
72
|
+
},
|
|
73
|
+
"./retention": {
|
|
74
|
+
"types": "./dist/retention.d.ts",
|
|
75
|
+
"import": "./dist/retention.js"
|
|
76
|
+
},
|
|
77
|
+
"./status": {
|
|
78
|
+
"types": "./dist/status.d.ts",
|
|
79
|
+
"import": "./dist/status.js"
|
|
80
|
+
},
|
|
81
|
+
"./adapter": {
|
|
82
|
+
"types": "./dist/adapter.d.ts",
|
|
83
|
+
"import": "./dist/adapter.js"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"files": [
|
|
87
|
+
"dist",
|
|
88
|
+
"src"
|
|
89
|
+
],
|
|
90
|
+
"dependencies": {
|
|
91
|
+
"@eddacraft/kindling-core": "0.1.1",
|
|
92
|
+
"zod": "^4.3.6"
|
|
93
|
+
},
|
|
94
|
+
"devDependencies": {
|
|
95
|
+
"@types/node": "^25.2.3",
|
|
96
|
+
"typescript": "^5.9.3",
|
|
97
|
+
"vitest": "^4.0.18"
|
|
98
|
+
},
|
|
99
|
+
"keywords": [
|
|
100
|
+
"anvil",
|
|
101
|
+
"kindling",
|
|
102
|
+
"memory",
|
|
103
|
+
"observability",
|
|
104
|
+
"governance"
|
|
105
|
+
],
|
|
106
|
+
"license": "PROPRIETARY",
|
|
107
|
+
"scripts": {
|
|
108
|
+
"build": "tsc",
|
|
109
|
+
"test": "vitest",
|
|
110
|
+
"bench": "vitest bench",
|
|
111
|
+
"generate:openapi": "tsx scripts/generate-openapi.ts",
|
|
112
|
+
"lint": "eslint src --ext .ts"
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import type { KindlingService, Capsule, ID } from '@eddacraft/kindling-core';
|
|
13
|
+
import type { Observation as AnvilObservation } from './observation-contract.js';
|
|
14
|
+
import { OBSERVATION_CONTRACT_VERSION } from './observation-contract.js';
|
|
15
|
+
import { createDebugger } from './utils/debug.js';
|
|
16
|
+
|
|
17
|
+
const debug = createDebugger('kindling');
|
|
18
|
+
|
|
19
|
+
/** Map Anvil observation kinds to Kindling's generic observation kinds */
|
|
20
|
+
const KIND_MAP: Record<AnvilObservation['kind'], string> = {
|
|
21
|
+
session_start: 'message',
|
|
22
|
+
session_end: 'message',
|
|
23
|
+
plan_created: 'message',
|
|
24
|
+
plan_edited: 'message',
|
|
25
|
+
plan_approved: 'message',
|
|
26
|
+
plan_rejected: 'message',
|
|
27
|
+
action_executed: 'command',
|
|
28
|
+
gate_evaluated: 'command',
|
|
29
|
+
constraint_applied: 'message',
|
|
30
|
+
human_input: 'message',
|
|
31
|
+
error: 'error',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface AnvilKindlingAdapterConfig {
|
|
35
|
+
service: KindlingService;
|
|
36
|
+
/** Repo path for scope isolation */
|
|
37
|
+
repoId?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Bridges Anvil observation emission to Kindling storage.
|
|
42
|
+
*
|
|
43
|
+
* Usage:
|
|
44
|
+
* ```ts
|
|
45
|
+
* const adapter = new AnvilKindlingAdapter({ service });
|
|
46
|
+
* const capsule = adapter.startSession(sessionId, scopeIds);
|
|
47
|
+
* adapter.emit(observation);
|
|
48
|
+
* adapter.endSession(capsule.id);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export class AnvilKindlingAdapter {
|
|
52
|
+
private service: KindlingService;
|
|
53
|
+
private repoId: string | undefined;
|
|
54
|
+
|
|
55
|
+
constructor(config: AnvilKindlingAdapterConfig) {
|
|
56
|
+
this.service = config.service;
|
|
57
|
+
this.repoId = config.repoId;
|
|
58
|
+
debug('AnvilKindlingAdapter created', { repoId: config.repoId });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Open a Kindling capsule for an Anvil session.
|
|
63
|
+
* Call this when a CLI command starts.
|
|
64
|
+
*/
|
|
65
|
+
startSession(sessionId: string, intent: string): Capsule {
|
|
66
|
+
debug('starting session', { sessionId, intent });
|
|
67
|
+
return this.service.openCapsule({
|
|
68
|
+
type: 'session',
|
|
69
|
+
intent,
|
|
70
|
+
scopeIds: {
|
|
71
|
+
sessionId,
|
|
72
|
+
repoId: this.repoId,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Close the capsule for a session.
|
|
79
|
+
* Call this when a CLI command ends.
|
|
80
|
+
*/
|
|
81
|
+
endSession(capsuleId: ID, summaryContent?: string): Capsule {
|
|
82
|
+
debug('ending session', { capsuleId });
|
|
83
|
+
return this.service.closeCapsule(capsuleId, {
|
|
84
|
+
generateSummary: !!summaryContent,
|
|
85
|
+
summaryContent,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Emit an Anvil observation to Kindling.
|
|
91
|
+
* The rich Anvil schema is serialized to content; the original kind
|
|
92
|
+
* is preserved in provenance for filtering.
|
|
93
|
+
*/
|
|
94
|
+
emit(observation: AnvilObservation, capsuleId?: ID): void {
|
|
95
|
+
debug('emitting observation via adapter', { kind: observation.kind, capsuleId });
|
|
96
|
+
const kindlingObs = {
|
|
97
|
+
id: randomUUID(),
|
|
98
|
+
kind: KIND_MAP[observation.kind] as 'message' | 'command' | 'error',
|
|
99
|
+
content: JSON.stringify(observation),
|
|
100
|
+
provenance: {
|
|
101
|
+
anvil_kind: observation.kind,
|
|
102
|
+
anvil_contract_version: OBSERVATION_CONTRACT_VERSION,
|
|
103
|
+
},
|
|
104
|
+
ts: Date.now(),
|
|
105
|
+
scopeIds: {
|
|
106
|
+
sessionId: observation.session_id,
|
|
107
|
+
repoId: this.repoId,
|
|
108
|
+
},
|
|
109
|
+
redacted: false,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
this.service.appendObservation(kindlingObs, {
|
|
113
|
+
capsuleId,
|
|
114
|
+
validate: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kindling Configuration Schema (KINDLING-002)
|
|
3
|
+
*
|
|
4
|
+
* Defines configuration for the Kindling integration layer.
|
|
5
|
+
* Configuration is read from `.anvilrc` or `anvil.config.json` in the project root.
|
|
6
|
+
*
|
|
7
|
+
* @see kindling-service.ts for how config is consumed
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Configuration Schema
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Capture flags: which observation kinds to record
|
|
20
|
+
*/
|
|
21
|
+
export const CaptureConfigSchema = z.object({
|
|
22
|
+
sessions: z.boolean().default(true).describe('Record session start/end'),
|
|
23
|
+
gates: z.boolean().default(true).describe('Record gate evaluations'),
|
|
24
|
+
actions: z.boolean().default(true).describe('Record action executions'),
|
|
25
|
+
plans: z.boolean().default(true).describe('Record plan lifecycle events'),
|
|
26
|
+
human_inputs: z.boolean().default(true).describe('Record human inputs'),
|
|
27
|
+
constraints: z.boolean().default(true).describe('Record constraint applications'),
|
|
28
|
+
errors: z.boolean().default(true).describe('Record errors'),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type CaptureConfig = z.infer<typeof CaptureConfigSchema>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Retention policy
|
|
35
|
+
*/
|
|
36
|
+
export const RetentionConfigSchema = z.object({
|
|
37
|
+
days: z.number().int().positive().default(90).describe('Days to retain observations'),
|
|
38
|
+
auto_prune: z.boolean().default(false).describe('Automatically prune on session start'),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export type RetentionConfig = z.infer<typeof RetentionConfigSchema>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Query limit defaults
|
|
45
|
+
*/
|
|
46
|
+
export const QueryLimitConfigSchema = z.object({
|
|
47
|
+
max_results: z
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.positive()
|
|
51
|
+
.max(1000)
|
|
52
|
+
.default(100)
|
|
53
|
+
.describe('Default max results per query'),
|
|
54
|
+
max_payload_bytes: z
|
|
55
|
+
.number()
|
|
56
|
+
.int()
|
|
57
|
+
.positive()
|
|
58
|
+
.max(10 * 1024 * 1024)
|
|
59
|
+
.default(1024 * 1024)
|
|
60
|
+
.describe('Default max payload size in bytes'),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export type QueryLimitConfig = z.infer<typeof QueryLimitConfigSchema>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Full Kindling configuration
|
|
67
|
+
*/
|
|
68
|
+
export const KindlingConfigSchema = z.object({
|
|
69
|
+
enabled: z.boolean().default(false).describe('Whether Kindling recording is active'),
|
|
70
|
+
database_path: z
|
|
71
|
+
.string()
|
|
72
|
+
.default('.anvil/kindling.db')
|
|
73
|
+
.describe('Path to the SQLite database (relative to project root)'),
|
|
74
|
+
retention: RetentionConfigSchema.default(() => ({
|
|
75
|
+
days: 90,
|
|
76
|
+
auto_prune: false,
|
|
77
|
+
})),
|
|
78
|
+
capture: CaptureConfigSchema.default(() => ({
|
|
79
|
+
sessions: true,
|
|
80
|
+
gates: true,
|
|
81
|
+
actions: true,
|
|
82
|
+
plans: true,
|
|
83
|
+
human_inputs: true,
|
|
84
|
+
constraints: true,
|
|
85
|
+
errors: true,
|
|
86
|
+
})),
|
|
87
|
+
query_limits: QueryLimitConfigSchema.default(() => ({
|
|
88
|
+
max_results: 100,
|
|
89
|
+
max_payload_bytes: 1024 * 1024,
|
|
90
|
+
})),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export type KindlingConfig = z.infer<typeof KindlingConfigSchema>;
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// Default Configuration
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Default configuration: disabled, sensible defaults for everything else
|
|
101
|
+
*/
|
|
102
|
+
export const DEFAULT_KINDLING_CONFIG: KindlingConfig = KindlingConfigSchema.parse({});
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Configuration Loading
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Known configuration file names, checked in order
|
|
110
|
+
*/
|
|
111
|
+
const CONFIG_FILE_NAMES = ['.anvilrc', 'anvil.config.json'] as const;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Load Kindling configuration from the project root.
|
|
115
|
+
*
|
|
116
|
+
* Searches for `.anvilrc` or `anvil.config.json` in the given directory.
|
|
117
|
+
* Expects a JSON file with an optional `kindling` key containing the config.
|
|
118
|
+
*
|
|
119
|
+
* Returns the default (disabled) config if no file is found or if the
|
|
120
|
+
* `kindling` key is absent.
|
|
121
|
+
*
|
|
122
|
+
* @param projectRoot - Absolute path to the project root directory
|
|
123
|
+
* @returns Parsed and validated KindlingConfig
|
|
124
|
+
*/
|
|
125
|
+
export function loadKindlingConfig(projectRoot: string): KindlingConfig {
|
|
126
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
127
|
+
const filePath = join(projectRoot, fileName);
|
|
128
|
+
|
|
129
|
+
if (!existsSync(filePath)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
135
|
+
const parsed: unknown = JSON.parse(raw);
|
|
136
|
+
|
|
137
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const record = parsed as Record<string, unknown>;
|
|
142
|
+
|
|
143
|
+
if (!('kindling' in record) || record['kindling'] === undefined) {
|
|
144
|
+
// Config file exists but has no kindling section
|
|
145
|
+
return DEFAULT_KINDLING_CONFIG;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const result = KindlingConfigSchema.safeParse(record['kindling']);
|
|
149
|
+
|
|
150
|
+
if (result.success) {
|
|
151
|
+
return result.data;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Invalid config shape -- fall back to defaults rather than crashing
|
|
155
|
+
// The caller (service layer) will operate in disabled mode
|
|
156
|
+
return DEFAULT_KINDLING_CONFIG;
|
|
157
|
+
} catch {
|
|
158
|
+
// JSON parse error or read error -- fall back to defaults
|
|
159
|
+
return DEFAULT_KINDLING_CONFIG;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// No config file found
|
|
164
|
+
return DEFAULT_KINDLING_CONFIG;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check whether a specific observation kind should be captured based on config.
|
|
169
|
+
*
|
|
170
|
+
* @param config - Kindling configuration
|
|
171
|
+
* @param kind - Observation kind string
|
|
172
|
+
* @returns true if the observation should be captured
|
|
173
|
+
*/
|
|
174
|
+
export function shouldCapture(config: KindlingConfig, kind: string): boolean {
|
|
175
|
+
if (!config.enabled) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
switch (kind) {
|
|
180
|
+
case 'session_start':
|
|
181
|
+
case 'session_end':
|
|
182
|
+
return config.capture.sessions;
|
|
183
|
+
case 'gate_evaluated':
|
|
184
|
+
return config.capture.gates;
|
|
185
|
+
case 'action_executed':
|
|
186
|
+
return config.capture.actions;
|
|
187
|
+
case 'plan_created':
|
|
188
|
+
case 'plan_edited':
|
|
189
|
+
case 'plan_approved':
|
|
190
|
+
case 'plan_rejected':
|
|
191
|
+
return config.capture.plans;
|
|
192
|
+
case 'human_input':
|
|
193
|
+
return config.capture.human_inputs;
|
|
194
|
+
case 'constraint_applied':
|
|
195
|
+
return config.capture.constraints;
|
|
196
|
+
case 'error':
|
|
197
|
+
return config.capture.errors;
|
|
198
|
+
default:
|
|
199
|
+
// Unknown kinds are captured by default when enabled
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Emitter (KINDLING-005)
|
|
3
|
+
*
|
|
4
|
+
* Emits action_executed observations when commands, tool invocations,
|
|
5
|
+
* file operations, or diff applications occur.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import type { KindlingService } from '../kindling-service.js';
|
|
10
|
+
import type { ActionExecutedObservation } from '../observation-contract.js';
|
|
11
|
+
import { createDebugger } from '../utils/debug.js';
|
|
12
|
+
|
|
13
|
+
const debug = createDebugger('kindling');
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Input Types
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Action execution details to be recorded
|
|
21
|
+
*/
|
|
22
|
+
export interface ActionDetails {
|
|
23
|
+
session_id: string;
|
|
24
|
+
action_type: 'command' | 'tool_invocation' | 'file_write' | 'file_delete' | 'diff_apply';
|
|
25
|
+
details: {
|
|
26
|
+
command?: string;
|
|
27
|
+
tool_name?: string;
|
|
28
|
+
file_paths?: string[];
|
|
29
|
+
diff_summary?: {
|
|
30
|
+
additions: number;
|
|
31
|
+
deletions: number;
|
|
32
|
+
files_changed: number;
|
|
33
|
+
};
|
|
34
|
+
working_directory: string;
|
|
35
|
+
environment_target?: string;
|
|
36
|
+
};
|
|
37
|
+
governed_by_gate_id?: string;
|
|
38
|
+
governed_by_plan_id?: string;
|
|
39
|
+
outcome: 'success' | 'failure' | 'partial';
|
|
40
|
+
exit_code?: number;
|
|
41
|
+
duration_ms: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Emitter
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Emit an action_executed observation.
|
|
50
|
+
*
|
|
51
|
+
* @param service - KindlingService instance
|
|
52
|
+
* @param actionDetails - Action execution details
|
|
53
|
+
* @returns The generated action_id
|
|
54
|
+
*/
|
|
55
|
+
export function emitActionExecuted(service: KindlingService, actionDetails: ActionDetails): string {
|
|
56
|
+
const actionId = randomUUID();
|
|
57
|
+
debug('emitting action_executed', {
|
|
58
|
+
actionId,
|
|
59
|
+
actionType: actionDetails.action_type,
|
|
60
|
+
outcome: actionDetails.outcome,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const observation: ActionExecutedObservation = {
|
|
64
|
+
kind: 'action_executed',
|
|
65
|
+
session_id: actionDetails.session_id,
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
action_id: actionId,
|
|
68
|
+
action_type: actionDetails.action_type,
|
|
69
|
+
details: {
|
|
70
|
+
command: actionDetails.details.command,
|
|
71
|
+
tool_name: actionDetails.details.tool_name,
|
|
72
|
+
file_paths: actionDetails.details.file_paths,
|
|
73
|
+
diff_summary: actionDetails.details.diff_summary,
|
|
74
|
+
working_directory: actionDetails.details.working_directory,
|
|
75
|
+
environment_target: actionDetails.details.environment_target,
|
|
76
|
+
},
|
|
77
|
+
governed_by_gate_id: actionDetails.governed_by_gate_id,
|
|
78
|
+
governed_by_plan_id: actionDetails.governed_by_plan_id,
|
|
79
|
+
outcome: actionDetails.outcome,
|
|
80
|
+
exit_code: actionDetails.exit_code,
|
|
81
|
+
duration_ms: actionDetails.duration_ms,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Fire-and-forget
|
|
85
|
+
service.emit(observation).catch(() => {
|
|
86
|
+
// Silently swallow
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return actionId;
|
|
90
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constraint Emitter (KINDLING-007b)
|
|
3
|
+
*
|
|
4
|
+
* Emits constraint_applied observations when Anvil prevents an action
|
|
5
|
+
* due to policy, scope, environment, or approval requirements.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import type { KindlingService } from '../kindling-service.js';
|
|
10
|
+
import type { ConstraintAppliedObservation } from '../observation-contract.js';
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Input Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Constraint application details to be recorded
|
|
18
|
+
*/
|
|
19
|
+
export interface ConstraintDetails {
|
|
20
|
+
session_id: string;
|
|
21
|
+
constraint_type: 'policy' | 'rule' | 'scope' | 'environment' | 'approval_required';
|
|
22
|
+
prevented_action: {
|
|
23
|
+
action_type: string;
|
|
24
|
+
action_target?: string;
|
|
25
|
+
};
|
|
26
|
+
reason: string;
|
|
27
|
+
scope?: string;
|
|
28
|
+
environment?: string;
|
|
29
|
+
options_available?: string[];
|
|
30
|
+
options_allowed?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Emitter
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Emit a constraint_applied observation.
|
|
39
|
+
*
|
|
40
|
+
* @param service - KindlingService instance
|
|
41
|
+
* @param constraint - Constraint application details
|
|
42
|
+
* @returns The generated constraint_id
|
|
43
|
+
*/
|
|
44
|
+
export function emitConstraintApplied(
|
|
45
|
+
service: KindlingService,
|
|
46
|
+
constraint: ConstraintDetails
|
|
47
|
+
): string {
|
|
48
|
+
const constraintId = randomUUID();
|
|
49
|
+
|
|
50
|
+
const observation: ConstraintAppliedObservation = {
|
|
51
|
+
kind: 'constraint_applied',
|
|
52
|
+
session_id: constraint.session_id,
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
constraint_id: constraintId,
|
|
55
|
+
constraint_type: constraint.constraint_type,
|
|
56
|
+
prevented_action: {
|
|
57
|
+
action_type: constraint.prevented_action.action_type,
|
|
58
|
+
action_target: constraint.prevented_action.action_target,
|
|
59
|
+
},
|
|
60
|
+
reason: constraint.reason,
|
|
61
|
+
scope: constraint.scope,
|
|
62
|
+
environment: constraint.environment,
|
|
63
|
+
options_available: constraint.options_available,
|
|
64
|
+
options_allowed: constraint.options_allowed,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Fire-and-forget
|
|
68
|
+
service.emit(observation).catch(() => {
|
|
69
|
+
// Silently swallow
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return constraintId;
|
|
73
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Emitter (KINDLING-008)
|
|
3
|
+
*
|
|
4
|
+
* Emits error observations when failures occur.
|
|
5
|
+
* "Errors are not noise, they are data."
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import type { KindlingService } from '../kindling-service.js';
|
|
10
|
+
import type { ErrorObservation } from '../observation-contract.js';
|
|
11
|
+
import { createDebugger } from '../utils/debug.js';
|
|
12
|
+
|
|
13
|
+
const debug = createDebugger('kindling');
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Input Types
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Error details to be recorded
|
|
21
|
+
*/
|
|
22
|
+
export interface ErrorDetails {
|
|
23
|
+
session_id: string;
|
|
24
|
+
error_type:
|
|
25
|
+
| 'command_failure'
|
|
26
|
+
| 'tool_error'
|
|
27
|
+
| 'aborted_execution'
|
|
28
|
+
| 'partial_state'
|
|
29
|
+
| 'validation_failure';
|
|
30
|
+
context: {
|
|
31
|
+
component: string;
|
|
32
|
+
action_id?: string;
|
|
33
|
+
gate_id?: string;
|
|
34
|
+
};
|
|
35
|
+
error_message: string;
|
|
36
|
+
error_code?: string;
|
|
37
|
+
exit_code?: number;
|
|
38
|
+
recoverable: boolean;
|
|
39
|
+
partial_state_description?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// Emitter
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Emit an error observation.
|
|
48
|
+
*
|
|
49
|
+
* @param service - KindlingService instance
|
|
50
|
+
* @param errorDetails - Error details
|
|
51
|
+
* @returns The generated error_id
|
|
52
|
+
*/
|
|
53
|
+
export function emitError(service: KindlingService, errorDetails: ErrorDetails): string {
|
|
54
|
+
const errorId = randomUUID();
|
|
55
|
+
debug('emitting error observation', {
|
|
56
|
+
errorId,
|
|
57
|
+
errorType: errorDetails.error_type,
|
|
58
|
+
component: errorDetails.context.component,
|
|
59
|
+
recoverable: errorDetails.recoverable,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const observation: ErrorObservation = {
|
|
63
|
+
kind: 'error',
|
|
64
|
+
session_id: errorDetails.session_id,
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
error_id: errorId,
|
|
67
|
+
error_type: errorDetails.error_type,
|
|
68
|
+
context: {
|
|
69
|
+
component: errorDetails.context.component,
|
|
70
|
+
action_id: errorDetails.context.action_id,
|
|
71
|
+
gate_id: errorDetails.context.gate_id,
|
|
72
|
+
},
|
|
73
|
+
error_message: errorDetails.error_message,
|
|
74
|
+
error_code: errorDetails.error_code,
|
|
75
|
+
exit_code: errorDetails.exit_code,
|
|
76
|
+
recoverable: errorDetails.recoverable,
|
|
77
|
+
partial_state_description: errorDetails.partial_state_description,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Fire-and-forget
|
|
81
|
+
service.emit(observation).catch(() => {
|
|
82
|
+
// Silently swallow
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return errorId;
|
|
86
|
+
}
|