@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,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kindling Query Contract (v1)
|
|
3
|
+
*
|
|
4
|
+
* Defines the read-only, bounded query surface for Kindling observations.
|
|
5
|
+
* This is a system of record, not a reasoning engine.
|
|
6
|
+
*
|
|
7
|
+
* GOVERNING RULE:
|
|
8
|
+
* Queries may retrieve facts; interpretation is the caller's responsibility.
|
|
9
|
+
* User-supplied AI may read, but may not mutate, infer, or generalise via Kindling.
|
|
10
|
+
*
|
|
11
|
+
* @see plans/modules/kindling-integration.aps.md for integration plan
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Schema Version
|
|
16
|
+
// =============================================================================
|
|
17
|
+
export const KINDLING_QUERY_CONTRACT_VERSION = '1.0.0';
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Query Scopes (Mandatory Boundary)
|
|
20
|
+
// =============================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Every query must specify exactly one scope.
|
|
23
|
+
* No free-text search. No global scans. No cross-project reads.
|
|
24
|
+
*/
|
|
25
|
+
export const QueryScopeSchema = z.enum([
|
|
26
|
+
'session', // "What happened in this run?"
|
|
27
|
+
'plan', // "What happened because of this plan?"
|
|
28
|
+
'gate', // "Why did this gate pass/fail?"
|
|
29
|
+
'action', // "What exactly did this action do?"
|
|
30
|
+
]);
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Result Shape
|
|
33
|
+
// =============================================================================
|
|
34
|
+
/**
|
|
35
|
+
* How results should be structured
|
|
36
|
+
*/
|
|
37
|
+
export const ResultShapeSchema = z.enum([
|
|
38
|
+
'timeline', // Ordered observations grouped by phase
|
|
39
|
+
'list', // Flat list of observations
|
|
40
|
+
'entity', // Single entity with metadata
|
|
41
|
+
]);
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// Output Format
|
|
44
|
+
// =============================================================================
|
|
45
|
+
/**
|
|
46
|
+
* Result serialisation format
|
|
47
|
+
*/
|
|
48
|
+
export const OutputFormatSchema = z.enum([
|
|
49
|
+
'json', // Machine-readable
|
|
50
|
+
'text', // Human-readable (for CLI)
|
|
51
|
+
]);
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Query Request (Base)
|
|
54
|
+
// =============================================================================
|
|
55
|
+
/**
|
|
56
|
+
* Base query request with mandatory constraints
|
|
57
|
+
*/
|
|
58
|
+
export const QueryRequestBaseSchema = z.object({
|
|
59
|
+
scope: QueryScopeSchema.describe('Query scope (mandatory)'),
|
|
60
|
+
shape: ResultShapeSchema.describe('Result structure (mandatory)'),
|
|
61
|
+
format: OutputFormatSchema.default('json').describe('Output format'),
|
|
62
|
+
// Time bounds (optional but encouraged)
|
|
63
|
+
time_after: z.string().datetime().optional().describe('Include observations after this time'),
|
|
64
|
+
time_before: z.string().datetime().optional().describe('Include observations before this time'),
|
|
65
|
+
// Result limits (anti-vacuum-cleaner)
|
|
66
|
+
max_results: z
|
|
67
|
+
.number()
|
|
68
|
+
.int()
|
|
69
|
+
.positive()
|
|
70
|
+
.max(1000)
|
|
71
|
+
.default(100)
|
|
72
|
+
.describe('Maximum observations to return'),
|
|
73
|
+
max_payload_bytes: z
|
|
74
|
+
.number()
|
|
75
|
+
.int()
|
|
76
|
+
.positive()
|
|
77
|
+
.max(10 * 1024 * 1024) // 10MB
|
|
78
|
+
.default(1024 * 1024) // 1MB
|
|
79
|
+
.describe('Maximum total payload size'),
|
|
80
|
+
});
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// Session Query
|
|
83
|
+
// =============================================================================
|
|
84
|
+
/**
|
|
85
|
+
* Query: "What happened in this run?"
|
|
86
|
+
*
|
|
87
|
+
* Returns ordered observations grouped by phase (plan / gate / action / outcome).
|
|
88
|
+
* Raw payloads only, no summaries.
|
|
89
|
+
*/
|
|
90
|
+
export const SessionQuerySchema = QueryRequestBaseSchema.extend({
|
|
91
|
+
scope: z.literal('session'),
|
|
92
|
+
session_id: z.string().uuid().describe('Session/run ID (mandatory)'),
|
|
93
|
+
include_phases: z
|
|
94
|
+
.array(z.enum(['plan', 'gate', 'action', 'outcome', 'error']))
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('Filter to specific phases'),
|
|
97
|
+
});
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// Plan Query
|
|
100
|
+
// =============================================================================
|
|
101
|
+
/**
|
|
102
|
+
* Query: "What happened because of this plan?"
|
|
103
|
+
*
|
|
104
|
+
* Returns plan metadata, versions, and linked executions.
|
|
105
|
+
* This is the ONLY cross-session read allowed, via explicit plan_id.
|
|
106
|
+
*/
|
|
107
|
+
export const PlanQuerySchema = QueryRequestBaseSchema.extend({
|
|
108
|
+
scope: z.literal('plan'),
|
|
109
|
+
plan_id: z.string().describe('Plan ID (mandatory)'),
|
|
110
|
+
include_executions: z.boolean().default(true).describe('Include linked execution run IDs'),
|
|
111
|
+
include_versions: z.boolean().default(true).describe('Include plan version history'),
|
|
112
|
+
});
|
|
113
|
+
// =============================================================================
|
|
114
|
+
// Gate Query
|
|
115
|
+
// =============================================================================
|
|
116
|
+
/**
|
|
117
|
+
* Query: "Why did this gate pass/fail?"
|
|
118
|
+
*
|
|
119
|
+
* Returns gate evaluation details with rule IDs, inputs (sanitised), and outcomes.
|
|
120
|
+
* No prose. No explanation layer.
|
|
121
|
+
*/
|
|
122
|
+
export const GateQuerySchema = QueryRequestBaseSchema.extend({
|
|
123
|
+
scope: z.literal('gate'),
|
|
124
|
+
gate_eval_id: z.string().describe('Gate evaluation ID (mandatory)'),
|
|
125
|
+
});
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// Action Query
|
|
128
|
+
// =============================================================================
|
|
129
|
+
/**
|
|
130
|
+
* Query: "What exactly did this action do?"
|
|
131
|
+
*
|
|
132
|
+
* Returns action details with redacted command, environment, and linked governance.
|
|
133
|
+
* This is the atomic unit of accountability.
|
|
134
|
+
*/
|
|
135
|
+
export const ActionQuerySchema = QueryRequestBaseSchema.extend({
|
|
136
|
+
scope: z.literal('action'),
|
|
137
|
+
action_id: z.string().describe('Action ID (mandatory)'),
|
|
138
|
+
include_approval_chain: z
|
|
139
|
+
.boolean()
|
|
140
|
+
.default(true)
|
|
141
|
+
.describe('Include approval requirements and state'),
|
|
142
|
+
});
|
|
143
|
+
// =============================================================================
|
|
144
|
+
// Query Request (Union)
|
|
145
|
+
// =============================================================================
|
|
146
|
+
/**
|
|
147
|
+
* All query types (discriminated union)
|
|
148
|
+
*/
|
|
149
|
+
export const QueryRequestSchema = z.discriminatedUnion('scope', [
|
|
150
|
+
SessionQuerySchema,
|
|
151
|
+
PlanQuerySchema,
|
|
152
|
+
GateQuerySchema,
|
|
153
|
+
ActionQuerySchema,
|
|
154
|
+
]);
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// Query Response (Output Guarantees)
|
|
157
|
+
// =============================================================================
|
|
158
|
+
/**
|
|
159
|
+
* Standard metadata for all responses
|
|
160
|
+
*/
|
|
161
|
+
export const QueryResponseMetadataSchema = z.object({
|
|
162
|
+
query_id: z.string().uuid().describe('Unique query identifier (for debugging)'),
|
|
163
|
+
executed_at: z.string().datetime().describe('When query was executed'),
|
|
164
|
+
contract_version: z.string().describe('Query contract version used'),
|
|
165
|
+
result_count: z.number().int().nonnegative().describe('Number of observations returned'),
|
|
166
|
+
truncated: z.boolean().describe('Whether results were truncated (hit limits)'),
|
|
167
|
+
truncation_reason: z
|
|
168
|
+
.enum(['max_results', 'max_payload_bytes', 'none'])
|
|
169
|
+
.optional()
|
|
170
|
+
.describe('Why truncation occurred'),
|
|
171
|
+
});
|
|
172
|
+
/**
|
|
173
|
+
* Standard provenance link
|
|
174
|
+
*/
|
|
175
|
+
export const ProvenanceLinkSchema = z.object({
|
|
176
|
+
type: z.enum(['caused_by', 'governed_by', 'approved_by', 'linked_to']).describe('Link type'),
|
|
177
|
+
entity_type: z
|
|
178
|
+
.enum(['session', 'plan', 'gate', 'action', 'human'])
|
|
179
|
+
.describe('Target entity type'),
|
|
180
|
+
entity_id: z.string().describe('Target entity ID'),
|
|
181
|
+
timestamp: z.string().datetime().describe('When link was created'),
|
|
182
|
+
});
|
|
183
|
+
/**
|
|
184
|
+
* Observation (base type for all returned data)
|
|
185
|
+
*/
|
|
186
|
+
export const ObservationSchema = z.object({
|
|
187
|
+
id: z.string().uuid().describe('Observation ID'),
|
|
188
|
+
kind: z
|
|
189
|
+
.enum([
|
|
190
|
+
'session_start',
|
|
191
|
+
'session_end',
|
|
192
|
+
'plan_created',
|
|
193
|
+
'plan_edited',
|
|
194
|
+
'plan_approved',
|
|
195
|
+
'plan_rejected',
|
|
196
|
+
'gate_evaluated',
|
|
197
|
+
'action_executed',
|
|
198
|
+
'constraint_applied',
|
|
199
|
+
'human_input',
|
|
200
|
+
'error',
|
|
201
|
+
])
|
|
202
|
+
.describe('Observation kind'),
|
|
203
|
+
timestamp: z.string().datetime().describe('When observation was recorded'),
|
|
204
|
+
session_id: z.string().uuid().describe('Session this observation belongs to'),
|
|
205
|
+
// Provenance (explicit links)
|
|
206
|
+
provenance: z.array(ProvenanceLinkSchema).describe('Explicit links to other entities'),
|
|
207
|
+
// Payload (fact data, no inference)
|
|
208
|
+
payload: z.record(z.string(), z.unknown()).describe('Observation-specific data (raw facts only)'),
|
|
209
|
+
});
|
|
210
|
+
/**
|
|
211
|
+
* Query response
|
|
212
|
+
*/
|
|
213
|
+
export const QueryResponseSchema = z.object({
|
|
214
|
+
metadata: QueryResponseMetadataSchema.describe('Query execution metadata'),
|
|
215
|
+
observations: z.array(ObservationSchema).describe('Ordered observations (facts only)'),
|
|
216
|
+
});
|
|
217
|
+
// =============================================================================
|
|
218
|
+
// Output Guarantees (Documented Requirements)
|
|
219
|
+
// =============================================================================
|
|
220
|
+
/**
|
|
221
|
+
* Every Kindling response guarantees:
|
|
222
|
+
*
|
|
223
|
+
* 1. Stable field names - No field names change between queries
|
|
224
|
+
* 2. Explicit timestamps - Every observation has ISO8601 timestamp
|
|
225
|
+
* 3. Explicit links - Provenance via typed links (caused_by, governed_by, approved_by)
|
|
226
|
+
* 4. No hidden inference - Payload contains only raw facts
|
|
227
|
+
* 5. No reordered history - Observations returned in recorded order
|
|
228
|
+
*
|
|
229
|
+
* This makes Kindling LLM-safe by construction.
|
|
230
|
+
*
|
|
231
|
+
* AI can:
|
|
232
|
+
* - Narrate events
|
|
233
|
+
* - Summarise outcomes
|
|
234
|
+
* - Explain facts
|
|
235
|
+
*
|
|
236
|
+
* But AI will always be explaining facts, not ghosts.
|
|
237
|
+
*/
|
|
238
|
+
// =============================================================================
|
|
239
|
+
// Read-Only Enforcement (Anti-Pattern Markers)
|
|
240
|
+
// =============================================================================
|
|
241
|
+
/**
|
|
242
|
+
* Operations that MUST NOT exist in the query API:
|
|
243
|
+
*
|
|
244
|
+
* ❌ write()
|
|
245
|
+
* ❌ update()
|
|
246
|
+
* ❌ delete()
|
|
247
|
+
* ❌ annotate()
|
|
248
|
+
* ❌ tag()
|
|
249
|
+
* ❌ learn()
|
|
250
|
+
* ❌ embed()
|
|
251
|
+
* ❌ infer()
|
|
252
|
+
*
|
|
253
|
+
* If user AI wants memory, it must bring its own store.
|
|
254
|
+
*/
|
|
255
|
+
// =============================================================================
|
|
256
|
+
// Explicit Non-Goals (v1 Boundary)
|
|
257
|
+
// =============================================================================
|
|
258
|
+
/**
|
|
259
|
+
* The following are explicitly OUT OF SCOPE for v1:
|
|
260
|
+
*
|
|
261
|
+
* ❌ Semantic search
|
|
262
|
+
* ❌ Similarity queries
|
|
263
|
+
* ❌ Embeddings
|
|
264
|
+
* ❌ Cross-plan discovery
|
|
265
|
+
* ❌ Learned relevance
|
|
266
|
+
* ❌ Auto-summaries (stored in Kindling)
|
|
267
|
+
* ❌ AI-generated annotations stored in Kindling
|
|
268
|
+
*
|
|
269
|
+
* These belong to Edda / Ember, not Kindling v1.
|
|
270
|
+
*/
|
|
271
|
+
// =============================================================================
|
|
272
|
+
// Validation Utilities
|
|
273
|
+
// =============================================================================
|
|
274
|
+
/**
|
|
275
|
+
* Validate a query request
|
|
276
|
+
*/
|
|
277
|
+
export function validateQueryRequest(data) {
|
|
278
|
+
const result = QueryRequestSchema.safeParse(data);
|
|
279
|
+
if (result.success) {
|
|
280
|
+
return { success: true, data: result.data };
|
|
281
|
+
}
|
|
282
|
+
return { success: false, error: result.error.format()._errors.join(', ') };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Validate a query response
|
|
286
|
+
*/
|
|
287
|
+
export function validateQueryResponse(data) {
|
|
288
|
+
const result = QueryResponseSchema.safeParse(data);
|
|
289
|
+
if (result.success) {
|
|
290
|
+
return { success: true, data: result.data };
|
|
291
|
+
}
|
|
292
|
+
return { success: false, error: result.error.format()._errors.join(', ') };
|
|
293
|
+
}
|
|
294
|
+
// =============================================================================
|
|
295
|
+
// CLI Command Mapping (Human-First, AI-Compatible)
|
|
296
|
+
// =============================================================================
|
|
297
|
+
/**
|
|
298
|
+
* All queries have CLI equivalents:
|
|
299
|
+
*
|
|
300
|
+
* anvil run show <run_id> --json
|
|
301
|
+
* → SessionQuery { scope: 'session', session_id: run_id, shape: 'timeline' }
|
|
302
|
+
*
|
|
303
|
+
* anvil plan trace <plan_id> --json
|
|
304
|
+
* → PlanQuery { scope: 'plan', plan_id: plan_id, shape: 'entity' }
|
|
305
|
+
*
|
|
306
|
+
* anvil gate show <gate_eval_id> --json
|
|
307
|
+
* → GateQuery { scope: 'gate', gate_eval_id: gate_eval_id, shape: 'entity' }
|
|
308
|
+
*
|
|
309
|
+
* anvil action show <action_id> --json
|
|
310
|
+
* → ActionQuery { scope: 'action', action_id: action_id, shape: 'entity' }
|
|
311
|
+
*
|
|
312
|
+
* The CLI is a thin wrapper over this query surface.
|
|
313
|
+
* That symmetry is intentional.
|
|
314
|
+
*/
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Limits Enforcement (KINDLING-010)
|
|
3
|
+
*
|
|
4
|
+
* Provides post-query truncation and metadata flagging.
|
|
5
|
+
* Used by KindlingQueryService before returning results to callers,
|
|
6
|
+
* as a defense-in-depth layer on top of store-level limits.
|
|
7
|
+
*/
|
|
8
|
+
import type { QueryResponse } from './query-contract.js';
|
|
9
|
+
import type { QueryLimitConfig } from './config.js';
|
|
10
|
+
/**
|
|
11
|
+
* Limits to enforce on a query response
|
|
12
|
+
*/
|
|
13
|
+
export interface QueryLimits {
|
|
14
|
+
/** Maximum number of observations to return */
|
|
15
|
+
max_results: number;
|
|
16
|
+
/** Maximum total payload size in bytes */
|
|
17
|
+
max_payload_bytes: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Enforce query limits on a response by truncating results if necessary.
|
|
21
|
+
*
|
|
22
|
+
* This is applied after the store returns results, as a safety net.
|
|
23
|
+
* If the store already respects limits, this is a no-op.
|
|
24
|
+
*
|
|
25
|
+
* Sets the `truncated` and `truncation_reason` metadata flags when
|
|
26
|
+
* results are truncated.
|
|
27
|
+
*
|
|
28
|
+
* @param response - The query response from the store
|
|
29
|
+
* @param limits - The limits to enforce
|
|
30
|
+
* @returns A new QueryResponse with limits enforced
|
|
31
|
+
*/
|
|
32
|
+
export declare function enforceQueryLimits(response: QueryResponse, limits: QueryLimits): QueryResponse;
|
|
33
|
+
/**
|
|
34
|
+
* Create QueryLimits from a QueryLimitConfig.
|
|
35
|
+
*
|
|
36
|
+
* @param config - Query limit configuration
|
|
37
|
+
* @returns QueryLimits object
|
|
38
|
+
*/
|
|
39
|
+
export declare function limitsFromConfig(config: QueryLimitConfig): QueryLimits;
|
|
40
|
+
//# sourceMappingURL=query-limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-limits.d.ts","sourceRoot":"","sources":["../src/query-limits.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAyB,MAAM,qBAAqB,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAMpD;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,WAAW,GAAG,aAAa,CAkD9F;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,WAAW,CAKtE"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Limits Enforcement (KINDLING-010)
|
|
3
|
+
*
|
|
4
|
+
* Provides post-query truncation and metadata flagging.
|
|
5
|
+
* Used by KindlingQueryService before returning results to callers,
|
|
6
|
+
* as a defense-in-depth layer on top of store-level limits.
|
|
7
|
+
*/
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Enforcement
|
|
10
|
+
// =============================================================================
|
|
11
|
+
/**
|
|
12
|
+
* Enforce query limits on a response by truncating results if necessary.
|
|
13
|
+
*
|
|
14
|
+
* This is applied after the store returns results, as a safety net.
|
|
15
|
+
* If the store already respects limits, this is a no-op.
|
|
16
|
+
*
|
|
17
|
+
* Sets the `truncated` and `truncation_reason` metadata flags when
|
|
18
|
+
* results are truncated.
|
|
19
|
+
*
|
|
20
|
+
* @param response - The query response from the store
|
|
21
|
+
* @param limits - The limits to enforce
|
|
22
|
+
* @returns A new QueryResponse with limits enforced
|
|
23
|
+
*/
|
|
24
|
+
export function enforceQueryLimits(response, limits) {
|
|
25
|
+
let observations = response.observations;
|
|
26
|
+
let truncated = response.metadata.truncated;
|
|
27
|
+
let truncationReason = response.metadata.truncation_reason;
|
|
28
|
+
// Check max_results
|
|
29
|
+
if (observations.length > limits.max_results) {
|
|
30
|
+
observations = observations.slice(0, limits.max_results);
|
|
31
|
+
truncated = true;
|
|
32
|
+
truncationReason = 'max_results';
|
|
33
|
+
}
|
|
34
|
+
// Check max_payload_bytes
|
|
35
|
+
const serialized = JSON.stringify(observations);
|
|
36
|
+
const payloadBytes = new TextEncoder().encode(serialized).byteLength;
|
|
37
|
+
if (payloadBytes > limits.max_payload_bytes) {
|
|
38
|
+
// Binary search for the maximum number of observations that fit
|
|
39
|
+
let lo = 0;
|
|
40
|
+
let hi = observations.length;
|
|
41
|
+
while (lo < hi) {
|
|
42
|
+
const mid = Math.floor((lo + hi + 1) / 2);
|
|
43
|
+
const slice = observations.slice(0, mid);
|
|
44
|
+
const sliceBytes = new TextEncoder().encode(JSON.stringify(slice)).byteLength;
|
|
45
|
+
if (sliceBytes <= limits.max_payload_bytes) {
|
|
46
|
+
lo = mid;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
hi = mid - 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
observations = observations.slice(0, lo);
|
|
53
|
+
truncated = true;
|
|
54
|
+
// Only override reason if not already set (max_results takes precedence)
|
|
55
|
+
truncationReason = truncationReason === 'max_results' ? 'max_results' : 'max_payload_bytes';
|
|
56
|
+
}
|
|
57
|
+
const metadata = {
|
|
58
|
+
...response.metadata,
|
|
59
|
+
result_count: observations.length,
|
|
60
|
+
truncated,
|
|
61
|
+
truncation_reason: truncated ? truncationReason : 'none',
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
metadata,
|
|
65
|
+
observations,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Create QueryLimits from a QueryLimitConfig.
|
|
70
|
+
*
|
|
71
|
+
* @param config - Query limit configuration
|
|
72
|
+
* @returns QueryLimits object
|
|
73
|
+
*/
|
|
74
|
+
export function limitsFromConfig(config) {
|
|
75
|
+
return {
|
|
76
|
+
max_results: config.max_results,
|
|
77
|
+
max_payload_bytes: config.max_payload_bytes,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kindling Query Service (KINDLING-009)
|
|
3
|
+
*
|
|
4
|
+
* High-level query interface that wraps KindlingService.query() with
|
|
5
|
+
* convenience methods for each query scope. Applies query limit enforcement
|
|
6
|
+
* before returning results.
|
|
7
|
+
*
|
|
8
|
+
* This is the primary read API for Anvil CLI commands and tooling.
|
|
9
|
+
*/
|
|
10
|
+
import type { KindlingService } from './kindling-service.js';
|
|
11
|
+
import type { QueryResponse, ResultShape, OutputFormat } from './query-contract.js';
|
|
12
|
+
/**
|
|
13
|
+
* Common options for all query methods
|
|
14
|
+
*/
|
|
15
|
+
export interface QueryOptions {
|
|
16
|
+
/** Result structure (default: 'list') */
|
|
17
|
+
shape?: ResultShape;
|
|
18
|
+
/** Output format (default: 'json') */
|
|
19
|
+
format?: OutputFormat;
|
|
20
|
+
/** Include observations after this time */
|
|
21
|
+
time_after?: string;
|
|
22
|
+
/** Include observations before this time */
|
|
23
|
+
time_before?: string;
|
|
24
|
+
/** Maximum observations to return (capped by config) */
|
|
25
|
+
max_results?: number;
|
|
26
|
+
/** Maximum total payload size in bytes (capped by config) */
|
|
27
|
+
max_payload_bytes?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Options specific to session queries
|
|
31
|
+
*/
|
|
32
|
+
export interface SessionQueryOptions extends QueryOptions {
|
|
33
|
+
/** Filter to specific phases */
|
|
34
|
+
include_phases?: Array<'plan' | 'gate' | 'action' | 'outcome' | 'error'>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Options specific to plan queries
|
|
38
|
+
*/
|
|
39
|
+
export interface PlanQueryOptions extends QueryOptions {
|
|
40
|
+
/** Include linked execution run IDs (default: true) */
|
|
41
|
+
include_executions?: boolean;
|
|
42
|
+
/** Include plan version history (default: true) */
|
|
43
|
+
include_versions?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Options specific to action queries
|
|
47
|
+
*/
|
|
48
|
+
export interface ActionQueryOptions extends QueryOptions {
|
|
49
|
+
/** Include approval chain (default: true) */
|
|
50
|
+
include_approval_chain?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* High-level query service for Kindling observations.
|
|
54
|
+
*
|
|
55
|
+
* Provides typed convenience methods for each query scope and enforces
|
|
56
|
+
* query limits from the service configuration.
|
|
57
|
+
*/
|
|
58
|
+
export declare class KindlingQueryService {
|
|
59
|
+
private readonly service;
|
|
60
|
+
constructor(service: KindlingService);
|
|
61
|
+
/**
|
|
62
|
+
* Query: "What happened in this run?"
|
|
63
|
+
*
|
|
64
|
+
* Returns ordered observations for a specific session, optionally
|
|
65
|
+
* filtered by phase.
|
|
66
|
+
*
|
|
67
|
+
* @param sessionId - Session/run ID
|
|
68
|
+
* @param options - Query options
|
|
69
|
+
* @returns Query response with session observations
|
|
70
|
+
*/
|
|
71
|
+
querySession(sessionId: string, options?: SessionQueryOptions): Promise<QueryResponse>;
|
|
72
|
+
/**
|
|
73
|
+
* Query: "What happened because of this plan?"
|
|
74
|
+
*
|
|
75
|
+
* Returns plan metadata, versions, and linked executions.
|
|
76
|
+
* This is the only cross-session read allowed.
|
|
77
|
+
*
|
|
78
|
+
* @param planId - Plan ID
|
|
79
|
+
* @param options - Query options
|
|
80
|
+
* @returns Query response with plan observations
|
|
81
|
+
*/
|
|
82
|
+
queryPlan(planId: string, options?: PlanQueryOptions): Promise<QueryResponse>;
|
|
83
|
+
/**
|
|
84
|
+
* Query: "Why did this gate pass/fail?"
|
|
85
|
+
*
|
|
86
|
+
* Returns gate evaluation details with rule IDs, inputs, and outcomes.
|
|
87
|
+
*
|
|
88
|
+
* @param gateEvalId - Gate evaluation ID
|
|
89
|
+
* @param options - Query options
|
|
90
|
+
* @returns Query response with gate observations
|
|
91
|
+
*/
|
|
92
|
+
queryGate(gateEvalId: string, options?: QueryOptions): Promise<QueryResponse>;
|
|
93
|
+
/**
|
|
94
|
+
* Query: "What exactly did this action do?"
|
|
95
|
+
*
|
|
96
|
+
* Returns action execution details with redacted command, environment,
|
|
97
|
+
* and linked governance.
|
|
98
|
+
*
|
|
99
|
+
* @param actionId - Action ID
|
|
100
|
+
* @param options - Query options
|
|
101
|
+
* @returns Query response with action observations
|
|
102
|
+
*/
|
|
103
|
+
queryAction(actionId: string, options?: ActionQueryOptions): Promise<QueryResponse>;
|
|
104
|
+
/**
|
|
105
|
+
* Apply query limits from the service configuration as a defense-in-depth layer.
|
|
106
|
+
*/
|
|
107
|
+
private applyLimits;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=query-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-service.d.ts","sourceRoot":"","sources":["../src/query-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EACV,aAAa,EAKb,WAAW,EACX,YAAY,EACb,MAAM,qBAAqB,CAAC;AAU7B;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,sCAAsC;IACtC,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,YAAY;IACvD,gCAAgC;IAChC,cAAc,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC;CAC1E;AAED;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,uDAAuD;IACvD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,6CAA6C;IAC7C,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAMD;;;;;GAKG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;gBAE9B,OAAO,EAAE,eAAe;IAIpC;;;;;;;;;OASG;IACG,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,aAAa,CAAC;IAmBhG;;;;;;;;;OASG;IACG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC;IAoBvF;;;;;;;;OAQG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,aAAa,CAAC;IAkBvF;;;;;;;;;OASG;IACG,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,aAAa,CAAC;IAuB7F;;OAEG;IACH,OAAO,CAAC,WAAW;CAIpB"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kindling Query Service (KINDLING-009)
|
|
3
|
+
*
|
|
4
|
+
* High-level query interface that wraps KindlingService.query() with
|
|
5
|
+
* convenience methods for each query scope. Applies query limit enforcement
|
|
6
|
+
* before returning results.
|
|
7
|
+
*
|
|
8
|
+
* This is the primary read API for Anvil CLI commands and tooling.
|
|
9
|
+
*/
|
|
10
|
+
import { enforceQueryLimits, limitsFromConfig } from './query-limits.js';
|
|
11
|
+
import { createDebugger } from './utils/debug.js';
|
|
12
|
+
const debug = createDebugger('kindling');
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Query Service
|
|
15
|
+
// =============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* High-level query service for Kindling observations.
|
|
18
|
+
*
|
|
19
|
+
* Provides typed convenience methods for each query scope and enforces
|
|
20
|
+
* query limits from the service configuration.
|
|
21
|
+
*/
|
|
22
|
+
export class KindlingQueryService {
|
|
23
|
+
service;
|
|
24
|
+
constructor(service) {
|
|
25
|
+
this.service = service;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Query: "What happened in this run?"
|
|
29
|
+
*
|
|
30
|
+
* Returns ordered observations for a specific session, optionally
|
|
31
|
+
* filtered by phase.
|
|
32
|
+
*
|
|
33
|
+
* @param sessionId - Session/run ID
|
|
34
|
+
* @param options - Query options
|
|
35
|
+
* @returns Query response with session observations
|
|
36
|
+
*/
|
|
37
|
+
async querySession(sessionId, options = {}) {
|
|
38
|
+
debug('querySession', { sessionId, shape: options.shape });
|
|
39
|
+
const request = {
|
|
40
|
+
scope: 'session',
|
|
41
|
+
session_id: sessionId,
|
|
42
|
+
shape: options.shape ?? 'timeline',
|
|
43
|
+
format: options.format ?? 'json',
|
|
44
|
+
time_after: options.time_after,
|
|
45
|
+
time_before: options.time_before,
|
|
46
|
+
max_results: options.max_results ?? this.service.configuration.query_limits.max_results,
|
|
47
|
+
max_payload_bytes: options.max_payload_bytes ?? this.service.configuration.query_limits.max_payload_bytes,
|
|
48
|
+
include_phases: options.include_phases,
|
|
49
|
+
};
|
|
50
|
+
const response = await this.service.query(request);
|
|
51
|
+
return this.applyLimits(response);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Query: "What happened because of this plan?"
|
|
55
|
+
*
|
|
56
|
+
* Returns plan metadata, versions, and linked executions.
|
|
57
|
+
* This is the only cross-session read allowed.
|
|
58
|
+
*
|
|
59
|
+
* @param planId - Plan ID
|
|
60
|
+
* @param options - Query options
|
|
61
|
+
* @returns Query response with plan observations
|
|
62
|
+
*/
|
|
63
|
+
async queryPlan(planId, options = {}) {
|
|
64
|
+
debug('queryPlan', { planId, shape: options.shape });
|
|
65
|
+
const request = {
|
|
66
|
+
scope: 'plan',
|
|
67
|
+
plan_id: planId,
|
|
68
|
+
shape: options.shape ?? 'entity',
|
|
69
|
+
format: options.format ?? 'json',
|
|
70
|
+
time_after: options.time_after,
|
|
71
|
+
time_before: options.time_before,
|
|
72
|
+
max_results: options.max_results ?? this.service.configuration.query_limits.max_results,
|
|
73
|
+
max_payload_bytes: options.max_payload_bytes ?? this.service.configuration.query_limits.max_payload_bytes,
|
|
74
|
+
include_executions: options.include_executions ?? true,
|
|
75
|
+
include_versions: options.include_versions ?? true,
|
|
76
|
+
};
|
|
77
|
+
const response = await this.service.query(request);
|
|
78
|
+
return this.applyLimits(response);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Query: "Why did this gate pass/fail?"
|
|
82
|
+
*
|
|
83
|
+
* Returns gate evaluation details with rule IDs, inputs, and outcomes.
|
|
84
|
+
*
|
|
85
|
+
* @param gateEvalId - Gate evaluation ID
|
|
86
|
+
* @param options - Query options
|
|
87
|
+
* @returns Query response with gate observations
|
|
88
|
+
*/
|
|
89
|
+
async queryGate(gateEvalId, options = {}) {
|
|
90
|
+
debug('queryGate', { gateEvalId });
|
|
91
|
+
const request = {
|
|
92
|
+
scope: 'gate',
|
|
93
|
+
gate_eval_id: gateEvalId,
|
|
94
|
+
shape: options.shape ?? 'entity',
|
|
95
|
+
format: options.format ?? 'json',
|
|
96
|
+
time_after: options.time_after,
|
|
97
|
+
time_before: options.time_before,
|
|
98
|
+
max_results: options.max_results ?? this.service.configuration.query_limits.max_results,
|
|
99
|
+
max_payload_bytes: options.max_payload_bytes ?? this.service.configuration.query_limits.max_payload_bytes,
|
|
100
|
+
};
|
|
101
|
+
const response = await this.service.query(request);
|
|
102
|
+
return this.applyLimits(response);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Query: "What exactly did this action do?"
|
|
106
|
+
*
|
|
107
|
+
* Returns action execution details with redacted command, environment,
|
|
108
|
+
* and linked governance.
|
|
109
|
+
*
|
|
110
|
+
* @param actionId - Action ID
|
|
111
|
+
* @param options - Query options
|
|
112
|
+
* @returns Query response with action observations
|
|
113
|
+
*/
|
|
114
|
+
async queryAction(actionId, options = {}) {
|
|
115
|
+
debug('queryAction', { actionId });
|
|
116
|
+
const request = {
|
|
117
|
+
scope: 'action',
|
|
118
|
+
action_id: actionId,
|
|
119
|
+
shape: options.shape ?? 'entity',
|
|
120
|
+
format: options.format ?? 'json',
|
|
121
|
+
time_after: options.time_after,
|
|
122
|
+
time_before: options.time_before,
|
|
123
|
+
max_results: options.max_results ?? this.service.configuration.query_limits.max_results,
|
|
124
|
+
max_payload_bytes: options.max_payload_bytes ?? this.service.configuration.query_limits.max_payload_bytes,
|
|
125
|
+
include_approval_chain: options.include_approval_chain ?? true,
|
|
126
|
+
};
|
|
127
|
+
const response = await this.service.query(request);
|
|
128
|
+
return this.applyLimits(response);
|
|
129
|
+
}
|
|
130
|
+
// ===========================================================================
|
|
131
|
+
// Private
|
|
132
|
+
// ===========================================================================
|
|
133
|
+
/**
|
|
134
|
+
* Apply query limits from the service configuration as a defense-in-depth layer.
|
|
135
|
+
*/
|
|
136
|
+
applyLimits(response) {
|
|
137
|
+
const limits = limitsFromConfig(this.service.configuration.query_limits);
|
|
138
|
+
return enforceQueryLimits(response, limits);
|
|
139
|
+
}
|
|
140
|
+
}
|