@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
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observation Contract (v1)
|
|
3
|
+
*
|
|
4
|
+
* Defines the 9 observation kinds that Anvil must emit to Kindling.
|
|
5
|
+
* This is the write-only contract - what gets recorded, not how it's queried.
|
|
6
|
+
*
|
|
7
|
+
* Based on: "What Kindling is used for in Anvil v1" specification
|
|
8
|
+
*
|
|
9
|
+
* @see query-contract.ts for the read-only query surface
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Schema Version
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
export const OBSERVATION_CONTRACT_VERSION = '1.0.0';
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// 1. Session Recording (The Spine)
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Session Start: Every Anvil run opens a session capsule
|
|
26
|
+
*/
|
|
27
|
+
export const SessionStartObservationSchema = z.object({
|
|
28
|
+
kind: z.literal('session_start'),
|
|
29
|
+
session_id: z.string().uuid().describe('Unique session identifier'),
|
|
30
|
+
timestamp: z.string().datetime().describe('When session started'),
|
|
31
|
+
|
|
32
|
+
// Context at session start
|
|
33
|
+
context: z.object({
|
|
34
|
+
working_directory: z.string().describe('Workspace root'),
|
|
35
|
+
git_ref: z.string().optional().describe('Current git commit/branch'),
|
|
36
|
+
git_dirty: z.boolean().optional().describe('Whether working tree has changes'),
|
|
37
|
+
anvil_version: z.string().describe('Anvil CLI version'),
|
|
38
|
+
command: z.string().describe('CLI command invoked (e.g., "anvil check")'),
|
|
39
|
+
args: z.array(z.string()).describe('Command arguments'),
|
|
40
|
+
environment: z
|
|
41
|
+
.enum(['development', 'ci', 'production', 'unknown'])
|
|
42
|
+
.describe('Execution context'),
|
|
43
|
+
}),
|
|
44
|
+
|
|
45
|
+
// Plan linkage (if this session is executing a plan)
|
|
46
|
+
plan_id: z.string().optional().describe('Plan being executed (if any)'),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type SessionStartObservation = z.infer<typeof SessionStartObservationSchema>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Session End: Closes the capsule with outcome
|
|
53
|
+
*/
|
|
54
|
+
export const SessionEndObservationSchema = z.object({
|
|
55
|
+
kind: z.literal('session_end'),
|
|
56
|
+
session_id: z.string().uuid().describe('Session being closed'),
|
|
57
|
+
timestamp: z.string().datetime().describe('When session ended'),
|
|
58
|
+
|
|
59
|
+
// Outcome
|
|
60
|
+
outcome: z.enum(['success', 'failure', 'partial', 'cancelled']).describe('Session result'),
|
|
61
|
+
exit_code: z.number().int().describe('Process exit code'),
|
|
62
|
+
duration_ms: z.number().int().nonnegative().describe('Session duration in milliseconds'),
|
|
63
|
+
|
|
64
|
+
// Summary counts (for quick reference)
|
|
65
|
+
summary: z.object({
|
|
66
|
+
gates_evaluated: z.number().int().nonnegative(),
|
|
67
|
+
gates_passed: z.number().int().nonnegative(),
|
|
68
|
+
gates_failed: z.number().int().nonnegative(),
|
|
69
|
+
actions_executed: z.number().int().nonnegative(),
|
|
70
|
+
errors_encountered: z.number().int().nonnegative(),
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export type SessionEndObservation = z.infer<typeof SessionEndObservationSchema>;
|
|
75
|
+
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// 2. PlanSpec Lifecycle Tracking
|
|
78
|
+
// =============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Plan Created: New plan authored
|
|
82
|
+
*/
|
|
83
|
+
export const PlanCreatedObservationSchema = z.object({
|
|
84
|
+
kind: z.literal('plan_created'),
|
|
85
|
+
session_id: z.string().uuid(),
|
|
86
|
+
timestamp: z.string().datetime(),
|
|
87
|
+
|
|
88
|
+
plan_id: z.string().describe('Unique plan identifier'),
|
|
89
|
+
plan_version: z.string().describe('Initial version (e.g., "1.0")'),
|
|
90
|
+
plan_path: z.string().describe('Path to plan file'),
|
|
91
|
+
plan_hash: z.string().describe('Content hash (for version tracking)'),
|
|
92
|
+
|
|
93
|
+
// Context
|
|
94
|
+
created_by: z.enum(['human', 'ai', 'system']).describe('Who created the plan'),
|
|
95
|
+
source: z.string().optional().describe('Source context (e.g., "github-issue-123")'),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export type PlanCreatedObservation = z.infer<typeof PlanCreatedObservationSchema>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Plan Edited: Plan modified
|
|
102
|
+
*/
|
|
103
|
+
export const PlanEditedObservationSchema = z.object({
|
|
104
|
+
kind: z.literal('plan_edited'),
|
|
105
|
+
session_id: z.string().uuid(),
|
|
106
|
+
timestamp: z.string().datetime(),
|
|
107
|
+
|
|
108
|
+
plan_id: z.string(),
|
|
109
|
+
previous_version: z.string(),
|
|
110
|
+
new_version: z.string(),
|
|
111
|
+
previous_hash: z.string(),
|
|
112
|
+
new_hash: z.string(),
|
|
113
|
+
|
|
114
|
+
edited_by: z.enum(['human', 'ai', 'system']),
|
|
115
|
+
change_summary: z.string().optional().describe('Brief description of changes'),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export type PlanEditedObservation = z.infer<typeof PlanEditedObservationSchema>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Plan Approved: Human approval recorded
|
|
122
|
+
*/
|
|
123
|
+
export const PlanApprovedObservationSchema = z.object({
|
|
124
|
+
kind: z.literal('plan_approved'),
|
|
125
|
+
session_id: z.string().uuid(),
|
|
126
|
+
timestamp: z.string().datetime(),
|
|
127
|
+
|
|
128
|
+
plan_id: z.string(),
|
|
129
|
+
plan_version: z.string(),
|
|
130
|
+
approved_by: z.string().describe('Human identifier (e.g., username, email)'),
|
|
131
|
+
approval_method: z.enum(['cli_confirm', 'explicit_flag', 'ci_gate']).describe('How approved'),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export type PlanApprovedObservation = z.infer<typeof PlanApprovedObservationSchema>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Plan Rejected: Human rejection recorded
|
|
138
|
+
*/
|
|
139
|
+
export const PlanRejectedObservationSchema = z.object({
|
|
140
|
+
kind: z.literal('plan_rejected'),
|
|
141
|
+
session_id: z.string().uuid(),
|
|
142
|
+
timestamp: z.string().datetime(),
|
|
143
|
+
|
|
144
|
+
plan_id: z.string(),
|
|
145
|
+
plan_version: z.string(),
|
|
146
|
+
rejected_by: z.string().describe('Human identifier'),
|
|
147
|
+
rejection_reason: z.string().optional().describe('Why rejected (if provided)'),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
export type PlanRejectedObservation = z.infer<typeof PlanRejectedObservationSchema>;
|
|
151
|
+
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// 3. Action Provenance (What Actually Happened)
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Action Executed: Observable action taken
|
|
158
|
+
*/
|
|
159
|
+
export const ActionExecutedObservationSchema = z.object({
|
|
160
|
+
kind: z.literal('action_executed'),
|
|
161
|
+
session_id: z.string().uuid(),
|
|
162
|
+
timestamp: z.string().datetime(),
|
|
163
|
+
|
|
164
|
+
action_id: z.string().describe('Unique action identifier'),
|
|
165
|
+
action_type: z
|
|
166
|
+
.enum(['command', 'tool_invocation', 'file_write', 'file_delete', 'diff_apply'])
|
|
167
|
+
.describe('Type of action'),
|
|
168
|
+
|
|
169
|
+
// What happened (redacted for security)
|
|
170
|
+
details: z.object({
|
|
171
|
+
command: z.string().optional().describe('Command executed (redacted)'),
|
|
172
|
+
tool_name: z.string().optional().describe('Tool invoked'),
|
|
173
|
+
file_paths: z.array(z.string()).optional().describe('Files touched'),
|
|
174
|
+
diff_summary: z
|
|
175
|
+
.object({
|
|
176
|
+
additions: z.number().int().nonnegative(),
|
|
177
|
+
deletions: z.number().int().nonnegative(),
|
|
178
|
+
files_changed: z.number().int().nonnegative(),
|
|
179
|
+
})
|
|
180
|
+
.optional()
|
|
181
|
+
.describe('Summary of changes (NOT full diff)'),
|
|
182
|
+
working_directory: z.string().describe('Where action executed'),
|
|
183
|
+
environment_target: z.string().optional().describe('Environment (e.g., "dev", "staging")'),
|
|
184
|
+
}),
|
|
185
|
+
|
|
186
|
+
// Governance linkage
|
|
187
|
+
governed_by_gate_id: z.string().optional().describe('Gate evaluation that allowed this'),
|
|
188
|
+
governed_by_plan_id: z.string().optional().describe('Plan that authorized this'),
|
|
189
|
+
|
|
190
|
+
// Outcome
|
|
191
|
+
outcome: z.enum(['success', 'failure', 'partial']),
|
|
192
|
+
exit_code: z.number().int().optional(),
|
|
193
|
+
duration_ms: z.number().int().nonnegative(),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
export type ActionExecutedObservation = z.infer<typeof ActionExecutedObservationSchema>;
|
|
197
|
+
|
|
198
|
+
// =============================================================================
|
|
199
|
+
// 4. Gate Evaluation Records
|
|
200
|
+
// =============================================================================
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Gate Evaluated: Structured gate check result
|
|
204
|
+
*/
|
|
205
|
+
export const GateEvaluatedObservationSchema = z.object({
|
|
206
|
+
kind: z.literal('gate_evaluated'),
|
|
207
|
+
session_id: z.string().uuid(),
|
|
208
|
+
timestamp: z.string().datetime(),
|
|
209
|
+
|
|
210
|
+
gate_eval_id: z.string().describe('Unique gate evaluation identifier'),
|
|
211
|
+
gate_id: z.string().describe('Gate identifier (e.g., "architecture", "coverage")'),
|
|
212
|
+
gate_version: z.string().optional().describe('Gate definition version'),
|
|
213
|
+
|
|
214
|
+
// Inputs (sanitised)
|
|
215
|
+
inputs: z
|
|
216
|
+
.object({
|
|
217
|
+
file_count: z.number().int().nonnegative().optional(),
|
|
218
|
+
changed_files: z.array(z.string()).optional().describe('Files evaluated (paths only)'),
|
|
219
|
+
baseline_hash: z.string().optional().describe('Architecture baseline used'),
|
|
220
|
+
})
|
|
221
|
+
.describe('What was evaluated (no sensitive data)'),
|
|
222
|
+
|
|
223
|
+
// Outcome
|
|
224
|
+
outcome: z.enum(['pass', 'fail', 'error', 'skipped']),
|
|
225
|
+
|
|
226
|
+
// Reasons (rule IDs, not prose)
|
|
227
|
+
rules_evaluated: z.array(z.string()).describe('Rule identifiers checked'),
|
|
228
|
+
rules_violated: z.array(z.string()).optional().describe('Rule identifiers that failed'),
|
|
229
|
+
|
|
230
|
+
// Enforcement
|
|
231
|
+
enforcement: z.enum(['blocking', 'warning', 'informational']).describe('Action taken on failure'),
|
|
232
|
+
|
|
233
|
+
// Metrics
|
|
234
|
+
duration_ms: z.number().int().nonnegative(),
|
|
235
|
+
violation_count: z.number().int().nonnegative().optional(),
|
|
236
|
+
warning_count: z.number().int().nonnegative().optional(),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
export type GateEvaluatedObservation = z.infer<typeof GateEvaluatedObservationSchema>;
|
|
240
|
+
|
|
241
|
+
// =============================================================================
|
|
242
|
+
// 5. Decision Constraints (Why Options Were Removed)
|
|
243
|
+
// =============================================================================
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Constraint Applied: When Anvil prevents an action
|
|
247
|
+
*/
|
|
248
|
+
export const ConstraintAppliedObservationSchema = z.object({
|
|
249
|
+
kind: z.literal('constraint_applied'),
|
|
250
|
+
session_id: z.string().uuid(),
|
|
251
|
+
timestamp: z.string().datetime(),
|
|
252
|
+
|
|
253
|
+
constraint_id: z.string().describe('Constraint identifier'),
|
|
254
|
+
constraint_type: z
|
|
255
|
+
.enum(['policy', 'rule', 'scope', 'environment', 'approval_required'])
|
|
256
|
+
.describe('Type of constraint'),
|
|
257
|
+
|
|
258
|
+
// What was prevented
|
|
259
|
+
prevented_action: z.object({
|
|
260
|
+
action_type: z.string().describe('What was attempted'),
|
|
261
|
+
action_target: z.string().optional().describe('Target (e.g., file path, command)'),
|
|
262
|
+
}),
|
|
263
|
+
|
|
264
|
+
// Why prevented
|
|
265
|
+
reason: z.string().describe('Rule ID or policy name that prevented action'),
|
|
266
|
+
scope: z.string().optional().describe('Scope constraint (e.g., "src/ only")'),
|
|
267
|
+
environment: z.string().optional().describe('Environment constraint (e.g., "not in production")'),
|
|
268
|
+
|
|
269
|
+
// What was available vs what was allowed
|
|
270
|
+
options_available: z.array(z.string()).optional().describe('All possible actions'),
|
|
271
|
+
options_allowed: z.array(z.string()).optional().describe('Actions that passed constraints'),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
export type ConstraintAppliedObservation = z.infer<typeof ConstraintAppliedObservationSchema>;
|
|
275
|
+
|
|
276
|
+
// =============================================================================
|
|
277
|
+
// 6. Human Inputs (First-Class Events)
|
|
278
|
+
// =============================================================================
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Human Input: User action recorded
|
|
282
|
+
*/
|
|
283
|
+
export const HumanInputObservationSchema = z.object({
|
|
284
|
+
kind: z.literal('human_input'),
|
|
285
|
+
session_id: z.string().uuid(),
|
|
286
|
+
timestamp: z.string().datetime(),
|
|
287
|
+
|
|
288
|
+
input_type: z
|
|
289
|
+
.enum(['approval', 'override', 'rejection', 'manual_edit', 'confirmation', 'cancellation'])
|
|
290
|
+
.describe('Type of human action'),
|
|
291
|
+
|
|
292
|
+
// Context
|
|
293
|
+
context: z.object({
|
|
294
|
+
prompt: z.string().optional().describe('What user was asked to decide'),
|
|
295
|
+
target: z.string().optional().describe('What the input was about (e.g., plan_id, gate_id)'),
|
|
296
|
+
}),
|
|
297
|
+
|
|
298
|
+
// Decision
|
|
299
|
+
decision: z.string().describe('What user chose'),
|
|
300
|
+
reason: z.string().optional().describe('User-provided reason (if any)'),
|
|
301
|
+
|
|
302
|
+
// Identity (for accountability)
|
|
303
|
+
user_identifier: z.string().describe('User identifier (username, email, etc.)'),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
export type HumanInputObservation = z.infer<typeof HumanInputObservationSchema>;
|
|
307
|
+
|
|
308
|
+
// =============================================================================
|
|
309
|
+
// 7. Error and Interruption History
|
|
310
|
+
// =============================================================================
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Error: Failure recorded (not noise, data)
|
|
314
|
+
*/
|
|
315
|
+
export const ErrorObservationSchema = z.object({
|
|
316
|
+
kind: z.literal('error'),
|
|
317
|
+
session_id: z.string().uuid(),
|
|
318
|
+
timestamp: z.string().datetime(),
|
|
319
|
+
|
|
320
|
+
error_id: z.string().describe('Unique error identifier'),
|
|
321
|
+
error_type: z
|
|
322
|
+
.enum([
|
|
323
|
+
'command_failure',
|
|
324
|
+
'tool_error',
|
|
325
|
+
'aborted_execution',
|
|
326
|
+
'partial_state',
|
|
327
|
+
'validation_failure',
|
|
328
|
+
])
|
|
329
|
+
.describe('Error category'),
|
|
330
|
+
|
|
331
|
+
// What failed
|
|
332
|
+
context: z.object({
|
|
333
|
+
component: z.string().describe('What was running (e.g., "gate:architecture")'),
|
|
334
|
+
action_id: z.string().optional().describe('Action that failed (if applicable)'),
|
|
335
|
+
gate_id: z.string().optional().describe('Gate that errored (if applicable)'),
|
|
336
|
+
}),
|
|
337
|
+
|
|
338
|
+
// Error details (sanitised)
|
|
339
|
+
error_message: z.string().describe('Error message (redacted if sensitive)'),
|
|
340
|
+
error_code: z.string().optional().describe('Error code (e.g., "ENOENT")'),
|
|
341
|
+
exit_code: z.number().int().optional(),
|
|
342
|
+
|
|
343
|
+
// State
|
|
344
|
+
recoverable: z.boolean().describe('Whether error is recoverable'),
|
|
345
|
+
partial_state_description: z
|
|
346
|
+
.string()
|
|
347
|
+
.optional()
|
|
348
|
+
.describe('Description of partial state if interrupted'),
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
export type ErrorObservation = z.infer<typeof ErrorObservationSchema>;
|
|
352
|
+
|
|
353
|
+
// =============================================================================
|
|
354
|
+
// Observation (Union Type)
|
|
355
|
+
// =============================================================================
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* All observation kinds (discriminated union by 'kind')
|
|
359
|
+
*/
|
|
360
|
+
export const ObservationSchema = z.discriminatedUnion('kind', [
|
|
361
|
+
SessionStartObservationSchema,
|
|
362
|
+
SessionEndObservationSchema,
|
|
363
|
+
PlanCreatedObservationSchema,
|
|
364
|
+
PlanEditedObservationSchema,
|
|
365
|
+
PlanApprovedObservationSchema,
|
|
366
|
+
PlanRejectedObservationSchema,
|
|
367
|
+
ActionExecutedObservationSchema,
|
|
368
|
+
GateEvaluatedObservationSchema,
|
|
369
|
+
ConstraintAppliedObservationSchema,
|
|
370
|
+
HumanInputObservationSchema,
|
|
371
|
+
ErrorObservationSchema,
|
|
372
|
+
]);
|
|
373
|
+
|
|
374
|
+
export type Observation =
|
|
375
|
+
| SessionStartObservation
|
|
376
|
+
| SessionEndObservation
|
|
377
|
+
| PlanCreatedObservation
|
|
378
|
+
| PlanEditedObservation
|
|
379
|
+
| PlanApprovedObservation
|
|
380
|
+
| PlanRejectedObservation
|
|
381
|
+
| ActionExecutedObservation
|
|
382
|
+
| GateEvaluatedObservation
|
|
383
|
+
| ConstraintAppliedObservation
|
|
384
|
+
| HumanInputObservation
|
|
385
|
+
| ErrorObservation;
|
|
386
|
+
|
|
387
|
+
// =============================================================================
|
|
388
|
+
// Observation Emission Contract
|
|
389
|
+
// =============================================================================
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* What Anvil must emit to be "Kindling-complete"
|
|
393
|
+
*
|
|
394
|
+
* Every Anvil execution must:
|
|
395
|
+
* 1. Emit SessionStartObservation when any command starts
|
|
396
|
+
* 2. Emit SessionEndObservation when command completes (success or failure)
|
|
397
|
+
* 3. Emit GateEvaluatedObservation for every gate check
|
|
398
|
+
* 4. Emit ActionExecutedObservation for every observable action
|
|
399
|
+
* 5. Emit ErrorObservation for every failure (even recoverable)
|
|
400
|
+
* 6. Emit HumanInputObservation for every approval/override/rejection
|
|
401
|
+
* 7. Emit ConstraintAppliedObservation when actions are prevented
|
|
402
|
+
* 8. Emit Plan* observations for all plan lifecycle events
|
|
403
|
+
*
|
|
404
|
+
* Observations are:
|
|
405
|
+
* - Immutable (write-once)
|
|
406
|
+
* - Timestamped (ISO8601)
|
|
407
|
+
* - Linked (session_id, plan_id, gate_id, action_id)
|
|
408
|
+
* - Sanitised (no secrets, redacted commands)
|
|
409
|
+
* - Facts only (no interpretation, no inference)
|
|
410
|
+
*/
|
|
411
|
+
|
|
412
|
+
// =============================================================================
|
|
413
|
+
// Integration Points (Where to Emit)
|
|
414
|
+
// =============================================================================
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Anvil codebase integration points:
|
|
418
|
+
*
|
|
419
|
+
* SessionStart/End:
|
|
420
|
+
* - cli/src/commands/*.ts (every command entry/exit)
|
|
421
|
+
* - cli/src/commands/watch.ts (each watch cycle)
|
|
422
|
+
*
|
|
423
|
+
* GateEvaluated:
|
|
424
|
+
* - core/src/gate/gate-runner.ts (GateRunner.run completion)
|
|
425
|
+
* - Each check implementation (architecture, coverage, secrets, etc.)
|
|
426
|
+
*
|
|
427
|
+
* ActionExecuted:
|
|
428
|
+
* - Anywhere Anvil executes commands (via child_process)
|
|
429
|
+
* - File write/delete operations
|
|
430
|
+
* - Diff application
|
|
431
|
+
*
|
|
432
|
+
* Plan*:
|
|
433
|
+
* - core/src/aps/ (plan parsing, validation, execution)
|
|
434
|
+
* - cli/src/commands/plan.ts (plan management commands)
|
|
435
|
+
*
|
|
436
|
+
* HumanInput:
|
|
437
|
+
* - cli/src/tui/ (TUI confirmation prompts)
|
|
438
|
+
* - cli/src/commands/ (CLI --approve flags)
|
|
439
|
+
*
|
|
440
|
+
* ConstraintApplied:
|
|
441
|
+
* - core/src/gate/ (when gate blocks action)
|
|
442
|
+
* - Policy evaluation layers
|
|
443
|
+
*
|
|
444
|
+
* Error:
|
|
445
|
+
* - All try/catch blocks that handle failures
|
|
446
|
+
* - Process error handlers
|
|
447
|
+
* - Validation failures
|
|
448
|
+
*/
|
|
449
|
+
|
|
450
|
+
// =============================================================================
|
|
451
|
+
// Validation Utilities
|
|
452
|
+
// =============================================================================
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Validate an observation before emission
|
|
456
|
+
*/
|
|
457
|
+
export function validateObservation(data: unknown): {
|
|
458
|
+
success: boolean;
|
|
459
|
+
data?: Observation;
|
|
460
|
+
error?: string;
|
|
461
|
+
} {
|
|
462
|
+
const result = ObservationSchema.safeParse(data);
|
|
463
|
+
if (result.success) {
|
|
464
|
+
return { success: true, data: result.data };
|
|
465
|
+
}
|
|
466
|
+
return { success: false, error: result.error.format()._errors.join(', ') };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Check if observation contains sensitive data (should never pass validation)
|
|
471
|
+
*/
|
|
472
|
+
export function containsSensitiveData(obs: Observation): {
|
|
473
|
+
hasSensitiveData: boolean;
|
|
474
|
+
issues: string[];
|
|
475
|
+
} {
|
|
476
|
+
const issues: string[] = [];
|
|
477
|
+
|
|
478
|
+
// Check for common sensitive patterns
|
|
479
|
+
const payloadStr = JSON.stringify(obs);
|
|
480
|
+
|
|
481
|
+
// Passwords, tokens, keys
|
|
482
|
+
if (/password|token|secret|api[_-]?key|private[_-]?key/i.test(payloadStr)) {
|
|
483
|
+
issues.push('Possible password/token/key detected');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// AWS credentials
|
|
487
|
+
if (/AKIA[0-9A-Z]{16}|aws_access_key_id|aws_secret_access_key/i.test(payloadStr)) {
|
|
488
|
+
issues.push('Possible AWS credentials detected');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Email addresses (may be sensitive depending on context)
|
|
492
|
+
if (/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g.test(payloadStr)) {
|
|
493
|
+
issues.push('Email addresses detected (may be sensitive)');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
hasSensitiveData: issues.length > 0,
|
|
498
|
+
issues,
|
|
499
|
+
};
|
|
500
|
+
}
|