@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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention Management (KINDLING-016)
|
|
3
|
+
*
|
|
4
|
+
* Handles pruning of old observations and storage statistics.
|
|
5
|
+
* Works against the abstract IKindlingStore interface.
|
|
6
|
+
*
|
|
7
|
+
* Since the abstract store does not expose a direct "delete older than" method,
|
|
8
|
+
* retention is implemented via a dedicated IRetentionCapableStore interface
|
|
9
|
+
* that concrete stores can optionally implement.
|
|
10
|
+
*/
|
|
11
|
+
import type { KindlingConfig } from './config.js';
|
|
12
|
+
/**
|
|
13
|
+
* Extended store interface for stores that support retention operations.
|
|
14
|
+
*
|
|
15
|
+
* Not all stores need to implement this. The NoOpKindlingStore does not.
|
|
16
|
+
* Concrete SQLite or database-backed stores should implement this interface.
|
|
17
|
+
*/
|
|
18
|
+
export interface IRetentionCapableStore {
|
|
19
|
+
/**
|
|
20
|
+
* Delete all observations with timestamps older than the given ISO8601 date.
|
|
21
|
+
*
|
|
22
|
+
* @param olderThan - ISO8601 datetime cutoff
|
|
23
|
+
* @returns Number of observations deleted
|
|
24
|
+
*/
|
|
25
|
+
deleteObservationsOlderThan(olderThan: string): Promise<number>;
|
|
26
|
+
/**
|
|
27
|
+
* Get storage statistics.
|
|
28
|
+
*
|
|
29
|
+
* @returns Observation count and estimated storage size in bytes
|
|
30
|
+
*/
|
|
31
|
+
getStats(): Promise<StorageStats>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Storage statistics
|
|
35
|
+
*/
|
|
36
|
+
export interface StorageStats {
|
|
37
|
+
/** Total number of observations in the store */
|
|
38
|
+
observation_count: number;
|
|
39
|
+
/** Estimated storage size in bytes */
|
|
40
|
+
estimated_size_bytes: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if a store supports retention operations.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isRetentionCapable(store: unknown): store is IRetentionCapableStore;
|
|
46
|
+
/**
|
|
47
|
+
* Result of a pruning operation
|
|
48
|
+
*/
|
|
49
|
+
export interface PruneResult {
|
|
50
|
+
/** Whether pruning was actually performed */
|
|
51
|
+
pruned: boolean;
|
|
52
|
+
/** Number of observations deleted (0 if not pruned) */
|
|
53
|
+
deleted_count: number;
|
|
54
|
+
/** The cutoff date used */
|
|
55
|
+
cutoff_date: string;
|
|
56
|
+
/** Reason if pruning was skipped */
|
|
57
|
+
skip_reason?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Prune observations older than the configured retention period.
|
|
61
|
+
*
|
|
62
|
+
* If the store does not implement IRetentionCapableStore, this is a no-op
|
|
63
|
+
* that returns a skip result.
|
|
64
|
+
*
|
|
65
|
+
* @param store - The store to prune (must implement IRetentionCapableStore)
|
|
66
|
+
* @param config - Kindling configuration with retention settings
|
|
67
|
+
* @returns Prune result
|
|
68
|
+
*/
|
|
69
|
+
export declare function pruneOldObservations(store: unknown, config: KindlingConfig): Promise<PruneResult>;
|
|
70
|
+
/**
|
|
71
|
+
* Get storage statistics from the store.
|
|
72
|
+
*
|
|
73
|
+
* If the store does not implement IRetentionCapableStore, returns zero values.
|
|
74
|
+
*
|
|
75
|
+
* @param store - The store to query
|
|
76
|
+
* @returns Storage statistics
|
|
77
|
+
*/
|
|
78
|
+
export declare function getStorageStats(store: unknown): Promise<StorageStats>;
|
|
79
|
+
//# sourceMappingURL=retention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../src/retention.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAMlD;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEhE;;;;OAIG;IACH,QAAQ,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gDAAgD;IAChD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,sCAAsC;IACtC,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAMD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,sBAAsB,CASlF;AAMD;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,MAAM,EAAE,OAAO,CAAC;IAChB,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAC;IACtB,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,WAAW,CAAC,CA8BtB;AAMD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAS3E"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention Management (KINDLING-016)
|
|
3
|
+
*
|
|
4
|
+
* Handles pruning of old observations and storage statistics.
|
|
5
|
+
* Works against the abstract IKindlingStore interface.
|
|
6
|
+
*
|
|
7
|
+
* Since the abstract store does not expose a direct "delete older than" method,
|
|
8
|
+
* retention is implemented via a dedicated IRetentionCapableStore interface
|
|
9
|
+
* that concrete stores can optionally implement.
|
|
10
|
+
*/
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Type Guard
|
|
13
|
+
// =============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Check if a store supports retention operations.
|
|
16
|
+
*/
|
|
17
|
+
export function isRetentionCapable(store) {
|
|
18
|
+
if (store === null || typeof store !== 'object') {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const candidate = store;
|
|
22
|
+
return (typeof candidate['deleteObservationsOlderThan'] === 'function' &&
|
|
23
|
+
typeof candidate['getStats'] === 'function');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Prune observations older than the configured retention period.
|
|
27
|
+
*
|
|
28
|
+
* If the store does not implement IRetentionCapableStore, this is a no-op
|
|
29
|
+
* that returns a skip result.
|
|
30
|
+
*
|
|
31
|
+
* @param store - The store to prune (must implement IRetentionCapableStore)
|
|
32
|
+
* @param config - Kindling configuration with retention settings
|
|
33
|
+
* @returns Prune result
|
|
34
|
+
*/
|
|
35
|
+
export async function pruneOldObservations(store, config) {
|
|
36
|
+
if (!config.enabled) {
|
|
37
|
+
return {
|
|
38
|
+
pruned: false,
|
|
39
|
+
deleted_count: 0,
|
|
40
|
+
cutoff_date: '',
|
|
41
|
+
skip_reason: 'Kindling is disabled',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (!isRetentionCapable(store)) {
|
|
45
|
+
return {
|
|
46
|
+
pruned: false,
|
|
47
|
+
deleted_count: 0,
|
|
48
|
+
cutoff_date: '',
|
|
49
|
+
skip_reason: 'Store does not support retention operations',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const cutoffDate = new Date();
|
|
53
|
+
cutoffDate.setDate(cutoffDate.getDate() - config.retention.days);
|
|
54
|
+
const cutoffIso = cutoffDate.toISOString();
|
|
55
|
+
const deletedCount = await store.deleteObservationsOlderThan(cutoffIso);
|
|
56
|
+
return {
|
|
57
|
+
pruned: true,
|
|
58
|
+
deleted_count: deletedCount,
|
|
59
|
+
cutoff_date: cutoffIso,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Statistics
|
|
64
|
+
// =============================================================================
|
|
65
|
+
/**
|
|
66
|
+
* Get storage statistics from the store.
|
|
67
|
+
*
|
|
68
|
+
* If the store does not implement IRetentionCapableStore, returns zero values.
|
|
69
|
+
*
|
|
70
|
+
* @param store - The store to query
|
|
71
|
+
* @returns Storage statistics
|
|
72
|
+
*/
|
|
73
|
+
export async function getStorageStats(store) {
|
|
74
|
+
if (!isRetentionCapable(store)) {
|
|
75
|
+
return {
|
|
76
|
+
observation_count: 0,
|
|
77
|
+
estimated_size_bytes: 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return store.getStats();
|
|
81
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sensitive Data Validation (KINDLING-015)
|
|
3
|
+
*
|
|
4
|
+
* Validates and redacts sensitive data from observations before they
|
|
5
|
+
* are persisted to Kindling. This is a defense-in-depth layer on top of
|
|
6
|
+
* the `containsSensitiveData` check in observation-contract.ts.
|
|
7
|
+
*
|
|
8
|
+
* Patterns detected:
|
|
9
|
+
* - API keys (sk-*, ghp_*, AKIA*)
|
|
10
|
+
* - Long hex tokens (40+ characters)
|
|
11
|
+
* - Email addresses
|
|
12
|
+
* - Password-like values
|
|
13
|
+
*
|
|
14
|
+
* @see observation-contract.ts for the base containsSensitiveData utility
|
|
15
|
+
*/
|
|
16
|
+
import { type Observation } from './observation-contract.js';
|
|
17
|
+
/**
|
|
18
|
+
* Result of sensitive data validation
|
|
19
|
+
*/
|
|
20
|
+
export interface SensitiveDataValidationResult {
|
|
21
|
+
/** Whether any sensitive data was detected */
|
|
22
|
+
hasSensitiveData: boolean;
|
|
23
|
+
/** Detailed issues found */
|
|
24
|
+
issues: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate that an observation does not contain sensitive data.
|
|
28
|
+
*
|
|
29
|
+
* Uses the contract-level `containsSensitiveData` check plus additional
|
|
30
|
+
* pattern matching for known credential formats.
|
|
31
|
+
*
|
|
32
|
+
* @param observation - The observation to validate
|
|
33
|
+
* @returns Validation result with issues if sensitive data found
|
|
34
|
+
*/
|
|
35
|
+
export declare function validateNoSensitiveData(observation: Observation): SensitiveDataValidationResult;
|
|
36
|
+
/**
|
|
37
|
+
* Deep-clone an observation and redact all known sensitive patterns from
|
|
38
|
+
* string values. This operates on the JSON serialization to catch values
|
|
39
|
+
* regardless of nesting depth.
|
|
40
|
+
*
|
|
41
|
+
* The returned observation is safe to persist.
|
|
42
|
+
*
|
|
43
|
+
* @param observation - The observation to redact
|
|
44
|
+
* @returns A new observation with sensitive values replaced
|
|
45
|
+
*/
|
|
46
|
+
export declare function redactSensitiveFields(observation: Observation): Observation;
|
|
47
|
+
//# sourceMappingURL=sensitive-data-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensitive-data-validator.d.ts","sourceRoot":"","sources":["../src/sensitive-data-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAyB,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAyEpF;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,8CAA8C;IAC9C,gBAAgB,EAAE,OAAO,CAAC;IAC1B,4BAA4B;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,WAAW,GAAG,6BAA6B,CA+B/F;AAMD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,WAAW,CAW3E"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sensitive Data Validation (KINDLING-015)
|
|
3
|
+
*
|
|
4
|
+
* Validates and redacts sensitive data from observations before they
|
|
5
|
+
* are persisted to Kindling. This is a defense-in-depth layer on top of
|
|
6
|
+
* the `containsSensitiveData` check in observation-contract.ts.
|
|
7
|
+
*
|
|
8
|
+
* Patterns detected:
|
|
9
|
+
* - API keys (sk-*, ghp_*, AKIA*)
|
|
10
|
+
* - Long hex tokens (40+ characters)
|
|
11
|
+
* - Email addresses
|
|
12
|
+
* - Password-like values
|
|
13
|
+
*
|
|
14
|
+
* @see observation-contract.ts for the base containsSensitiveData utility
|
|
15
|
+
*/
|
|
16
|
+
import { containsSensitiveData } from './observation-contract.js';
|
|
17
|
+
import { createDebugger } from './utils/debug.js';
|
|
18
|
+
const debug = createDebugger('kindling');
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Sensitive Patterns
|
|
21
|
+
// =============================================================================
|
|
22
|
+
/**
|
|
23
|
+
* Known sensitive value patterns and their replacement strings
|
|
24
|
+
*/
|
|
25
|
+
const SENSITIVE_PATTERNS = [
|
|
26
|
+
{
|
|
27
|
+
name: 'openai-api-key',
|
|
28
|
+
pattern: /sk-[a-zA-Z0-9]{20,}/g,
|
|
29
|
+
replacement: '[REDACTED:api-key]',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'github-pat',
|
|
33
|
+
pattern: /ghp_[a-zA-Z0-9]{36,}/g,
|
|
34
|
+
replacement: '[REDACTED:github-token]',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'github-fine-grained-pat',
|
|
38
|
+
pattern: /github_pat_[a-zA-Z0-9_]{36,}/g,
|
|
39
|
+
replacement: '[REDACTED:github-token]',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'aws-access-key',
|
|
43
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
44
|
+
replacement: '[REDACTED:aws-key]',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'aws-secret-key',
|
|
48
|
+
pattern: /(?<=aws_secret_access_key\s*[=:]\s*)[^\s"',]+/gi,
|
|
49
|
+
replacement: '[REDACTED:aws-secret]',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'hex-token',
|
|
53
|
+
pattern: /\b[0-9a-fA-F]{40,}\b/g,
|
|
54
|
+
replacement: '[REDACTED:token]',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'email',
|
|
58
|
+
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
59
|
+
replacement: '[REDACTED:email]',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'password-value',
|
|
63
|
+
pattern: /(?<=(?:password|passwd|pwd)\s*[=:]\s*["']?)[^\s"',]+/gi,
|
|
64
|
+
replacement: '[REDACTED:password]',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'bearer-token',
|
|
68
|
+
pattern: /(?<=Bearer\s+)[a-zA-Z0-9._~+/=-]{20,}/gi,
|
|
69
|
+
replacement: '[REDACTED:bearer-token]',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'npm-token',
|
|
73
|
+
pattern: /npm_[a-zA-Z0-9]{36,}/g,
|
|
74
|
+
replacement: '[REDACTED:npm-token]',
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
/**
|
|
78
|
+
* Validate that an observation does not contain sensitive data.
|
|
79
|
+
*
|
|
80
|
+
* Uses the contract-level `containsSensitiveData` check plus additional
|
|
81
|
+
* pattern matching for known credential formats.
|
|
82
|
+
*
|
|
83
|
+
* @param observation - The observation to validate
|
|
84
|
+
* @returns Validation result with issues if sensitive data found
|
|
85
|
+
*/
|
|
86
|
+
export function validateNoSensitiveData(observation) {
|
|
87
|
+
const issues = [];
|
|
88
|
+
// Run the contract-level check first
|
|
89
|
+
const contractCheck = containsSensitiveData(observation);
|
|
90
|
+
if (contractCheck.hasSensitiveData) {
|
|
91
|
+
issues.push(...contractCheck.issues);
|
|
92
|
+
}
|
|
93
|
+
// Run additional pattern checks against the serialized payload
|
|
94
|
+
const serialized = JSON.stringify(observation);
|
|
95
|
+
for (const { name, pattern } of SENSITIVE_PATTERNS) {
|
|
96
|
+
// Reset lastIndex for global regex patterns
|
|
97
|
+
pattern.lastIndex = 0;
|
|
98
|
+
if (pattern.test(serialized)) {
|
|
99
|
+
issues.push(`Sensitive pattern detected: ${name}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (issues.length > 0) {
|
|
103
|
+
debug('sensitive data detected in observation', {
|
|
104
|
+
issueCount: issues.length,
|
|
105
|
+
patterns: issues,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
hasSensitiveData: issues.length > 0,
|
|
110
|
+
issues,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// =============================================================================
|
|
114
|
+
// Redaction
|
|
115
|
+
// =============================================================================
|
|
116
|
+
/**
|
|
117
|
+
* Deep-clone an observation and redact all known sensitive patterns from
|
|
118
|
+
* string values. This operates on the JSON serialization to catch values
|
|
119
|
+
* regardless of nesting depth.
|
|
120
|
+
*
|
|
121
|
+
* The returned observation is safe to persist.
|
|
122
|
+
*
|
|
123
|
+
* @param observation - The observation to redact
|
|
124
|
+
* @returns A new observation with sensitive values replaced
|
|
125
|
+
*/
|
|
126
|
+
export function redactSensitiveFields(observation) {
|
|
127
|
+
debug('redacting sensitive fields from observation', { kind: observation.kind });
|
|
128
|
+
let serialized = JSON.stringify(observation);
|
|
129
|
+
for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
|
|
130
|
+
// Reset lastIndex for global regex patterns
|
|
131
|
+
pattern.lastIndex = 0;
|
|
132
|
+
serialized = serialized.replace(pattern, replacement);
|
|
133
|
+
}
|
|
134
|
+
return JSON.parse(serialized);
|
|
135
|
+
}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kindling Status Utility (KINDLING-014)
|
|
3
|
+
*
|
|
4
|
+
* Provides a decoupled status summary for Kindling integration.
|
|
5
|
+
* Returns observation counts, database size, and retention config
|
|
6
|
+
* without coupling to any specific CLI framework.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const status = await getKindlingStatus(config, store);
|
|
10
|
+
* // => { enabled: true, observationCount: 42, dbSizeBytes: 81920, ... }
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Kindling status summary returned by getKindlingStatus()
|
|
14
|
+
*/
|
|
15
|
+
export interface KindlingStatus {
|
|
16
|
+
/** Whether Kindling is enabled in config */
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
/** Total number of observations stored (undefined if disabled) */
|
|
19
|
+
observationCount?: number;
|
|
20
|
+
/** Database file size in bytes (undefined if disabled or no DB) */
|
|
21
|
+
dbSizeBytes?: number;
|
|
22
|
+
/** Configured retention period in days (undefined if disabled) */
|
|
23
|
+
retentionDays?: number;
|
|
24
|
+
/** ISO8601 timestamp of the most recent observation (undefined if none) */
|
|
25
|
+
lastObservationAt?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Minimal config shape needed for status checks.
|
|
29
|
+
* This avoids importing from config.ts (built by other agent).
|
|
30
|
+
*/
|
|
31
|
+
export interface KindlingStatusConfig {
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
database?: string;
|
|
34
|
+
retention?: {
|
|
35
|
+
days?: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Minimal store interface for status queries.
|
|
40
|
+
* Implementations can be KindlingService, a direct SQLite handle, or a mock.
|
|
41
|
+
*/
|
|
42
|
+
export interface KindlingStatusStore {
|
|
43
|
+
/**
|
|
44
|
+
* Count all observations in the store.
|
|
45
|
+
* Returns 0 if the store is empty.
|
|
46
|
+
*/
|
|
47
|
+
countObservations(): Promise<number>;
|
|
48
|
+
/**
|
|
49
|
+
* Get the database file size in bytes.
|
|
50
|
+
* Returns 0 if the database does not exist or cannot be read.
|
|
51
|
+
*/
|
|
52
|
+
getDatabaseSizeBytes(): Promise<number>;
|
|
53
|
+
/**
|
|
54
|
+
* Get the timestamp of the most recent observation.
|
|
55
|
+
* Returns undefined if no observations exist.
|
|
56
|
+
*/
|
|
57
|
+
getLastObservationTimestamp(): Promise<string | undefined>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get Kindling integration status.
|
|
61
|
+
*
|
|
62
|
+
* If store is not provided or config is disabled, returns a minimal
|
|
63
|
+
* disabled status. This function never throws -- it handles errors
|
|
64
|
+
* gracefully and returns partial information.
|
|
65
|
+
*
|
|
66
|
+
* @param config - Kindling configuration (at minimum, { enabled: boolean })
|
|
67
|
+
* @param store - Optional store interface for querying observation data
|
|
68
|
+
* @returns KindlingStatus summary
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* import { getKindlingStatus } from './status.js';
|
|
73
|
+
*
|
|
74
|
+
* // Disabled (no store needed)
|
|
75
|
+
* const status = await getKindlingStatus({ enabled: false });
|
|
76
|
+
* // => { enabled: false }
|
|
77
|
+
*
|
|
78
|
+
* // Enabled with store
|
|
79
|
+
* const status = await getKindlingStatus(
|
|
80
|
+
* { enabled: true, retention: { days: 90 } },
|
|
81
|
+
* myStore,
|
|
82
|
+
* );
|
|
83
|
+
* // => { enabled: true, observationCount: 42, dbSizeBytes: 81920,
|
|
84
|
+
* // retentionDays: 90, lastObservationAt: '2026-02-15T...' }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare function getKindlingStatus(config: KindlingStatusConfig, store?: KindlingStatusStore): Promise<KindlingStatus>;
|
|
88
|
+
/**
|
|
89
|
+
* Format a KindlingStatus as a human-readable string.
|
|
90
|
+
* Useful for CLI output without coupling to a specific formatter.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const status = await getKindlingStatus(config, store);
|
|
95
|
+
* console.log(formatKindlingStatus(status));
|
|
96
|
+
* // Kindling: enabled
|
|
97
|
+
* // Observations: 42
|
|
98
|
+
* // Database size: 80 KB
|
|
99
|
+
* // Retention: 90 days
|
|
100
|
+
* // Last observation: 2026-02-15T10:30:00.000Z
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function formatKindlingStatus(status: KindlingStatus): string;
|
|
104
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IAEjB,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAMD;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE;QACV,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAMD;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAErC;;;OAGG;IACH,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAExC;;;OAGG;IACH,2BAA2B,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC5D;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,KAAK,CAAC,EAAE,mBAAmB,GAC1B,OAAO,CAAC,cAAc,CAAC,CA4BzB;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA4BnE"}
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kindling Status Utility (KINDLING-014)
|
|
3
|
+
*
|
|
4
|
+
* Provides a decoupled status summary for Kindling integration.
|
|
5
|
+
* Returns observation counts, database size, and retention config
|
|
6
|
+
* without coupling to any specific CLI framework.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const status = await getKindlingStatus(config, store);
|
|
10
|
+
* // => { enabled: true, observationCount: 42, dbSizeBytes: 81920, ... }
|
|
11
|
+
*/
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Status Function
|
|
14
|
+
// =============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Get Kindling integration status.
|
|
17
|
+
*
|
|
18
|
+
* If store is not provided or config is disabled, returns a minimal
|
|
19
|
+
* disabled status. This function never throws -- it handles errors
|
|
20
|
+
* gracefully and returns partial information.
|
|
21
|
+
*
|
|
22
|
+
* @param config - Kindling configuration (at minimum, { enabled: boolean })
|
|
23
|
+
* @param store - Optional store interface for querying observation data
|
|
24
|
+
* @returns KindlingStatus summary
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { getKindlingStatus } from './status.js';
|
|
29
|
+
*
|
|
30
|
+
* // Disabled (no store needed)
|
|
31
|
+
* const status = await getKindlingStatus({ enabled: false });
|
|
32
|
+
* // => { enabled: false }
|
|
33
|
+
*
|
|
34
|
+
* // Enabled with store
|
|
35
|
+
* const status = await getKindlingStatus(
|
|
36
|
+
* { enabled: true, retention: { days: 90 } },
|
|
37
|
+
* myStore,
|
|
38
|
+
* );
|
|
39
|
+
* // => { enabled: true, observationCount: 42, dbSizeBytes: 81920,
|
|
40
|
+
* // retentionDays: 90, lastObservationAt: '2026-02-15T...' }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export async function getKindlingStatus(config, store) {
|
|
44
|
+
// Fast path: disabled
|
|
45
|
+
if (!config.enabled) {
|
|
46
|
+
return { enabled: false };
|
|
47
|
+
}
|
|
48
|
+
// Enabled but no store available
|
|
49
|
+
if (!store) {
|
|
50
|
+
return {
|
|
51
|
+
enabled: true,
|
|
52
|
+
retentionDays: config.retention?.days,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Enabled with store -- query for status data
|
|
56
|
+
const [observationCount, dbSizeBytes, lastObservationAt] = await Promise.all([
|
|
57
|
+
safeCall(() => store.countObservations(), 0),
|
|
58
|
+
safeCall(() => store.getDatabaseSizeBytes(), 0),
|
|
59
|
+
safeCall(() => store.getLastObservationTimestamp(), undefined),
|
|
60
|
+
]);
|
|
61
|
+
return {
|
|
62
|
+
enabled: true,
|
|
63
|
+
observationCount,
|
|
64
|
+
dbSizeBytes,
|
|
65
|
+
retentionDays: config.retention?.days,
|
|
66
|
+
lastObservationAt,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// =============================================================================
|
|
70
|
+
// Formatting Utilities
|
|
71
|
+
// =============================================================================
|
|
72
|
+
/**
|
|
73
|
+
* Format a KindlingStatus as a human-readable string.
|
|
74
|
+
* Useful for CLI output without coupling to a specific formatter.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const status = await getKindlingStatus(config, store);
|
|
79
|
+
* console.log(formatKindlingStatus(status));
|
|
80
|
+
* // Kindling: enabled
|
|
81
|
+
* // Observations: 42
|
|
82
|
+
* // Database size: 80 KB
|
|
83
|
+
* // Retention: 90 days
|
|
84
|
+
* // Last observation: 2026-02-15T10:30:00.000Z
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function formatKindlingStatus(status) {
|
|
88
|
+
const lines = [];
|
|
89
|
+
lines.push(`Kindling: ${status.enabled ? 'enabled' : 'disabled'}`);
|
|
90
|
+
if (!status.enabled) {
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
if (status.observationCount !== undefined) {
|
|
94
|
+
lines.push(`Observations: ${status.observationCount.toLocaleString()}`);
|
|
95
|
+
}
|
|
96
|
+
if (status.dbSizeBytes !== undefined) {
|
|
97
|
+
lines.push(`Database size: ${formatBytes(status.dbSizeBytes)}`);
|
|
98
|
+
}
|
|
99
|
+
if (status.retentionDays !== undefined) {
|
|
100
|
+
lines.push(`Retention: ${status.retentionDays} days`);
|
|
101
|
+
}
|
|
102
|
+
if (status.lastObservationAt) {
|
|
103
|
+
lines.push(`Last observation: ${status.lastObservationAt}`);
|
|
104
|
+
}
|
|
105
|
+
else if (status.observationCount === 0) {
|
|
106
|
+
lines.push('Last observation: none');
|
|
107
|
+
}
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
// =============================================================================
|
|
111
|
+
// Internal Helpers
|
|
112
|
+
// =============================================================================
|
|
113
|
+
/**
|
|
114
|
+
* Call an async function and return a fallback value on error.
|
|
115
|
+
* Ensures getKindlingStatus never throws.
|
|
116
|
+
*/
|
|
117
|
+
async function safeCall(fn, fallback) {
|
|
118
|
+
try {
|
|
119
|
+
return await fn();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return fallback;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Format bytes into a human-readable string.
|
|
127
|
+
*/
|
|
128
|
+
function formatBytes(bytes) {
|
|
129
|
+
if (bytes === 0)
|
|
130
|
+
return '0 B';
|
|
131
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
132
|
+
const k = 1024;
|
|
133
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), units.length - 1);
|
|
134
|
+
const value = bytes / Math.pow(k, i);
|
|
135
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[i]}`;
|
|
136
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal debug logging utility for kindling-integration package
|
|
3
|
+
*
|
|
4
|
+
* Self-contained to avoid dependency on @eddacraft/anvil-core
|
|
5
|
+
*/
|
|
6
|
+
type DebugNamespace = 'kindling';
|
|
7
|
+
export declare function createDebugger(_namespace: DebugNamespace): (message: string, data?: unknown) => void;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=debug.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug.d.ts","sourceRoot":"","sources":["../../src/utils/debug.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,KAAK,cAAc,GAAG,UAAU,CAAC;AAsDjC,wBAAgB,cAAc,CAC5B,UAAU,EAAE,cAAc,GACzB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAE3C"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal debug logging utility for kindling-integration package
|
|
3
|
+
*
|
|
4
|
+
* Self-contained to avoid dependency on @eddacraft/anvil-core
|
|
5
|
+
*/
|
|
6
|
+
function isDebugEnabled() {
|
|
7
|
+
const anvilDebug = process.env.ANVIL_DEBUG;
|
|
8
|
+
const debug = process.env.DEBUG;
|
|
9
|
+
if (anvilDebug === '1' || anvilDebug === 'true') {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (debug) {
|
|
13
|
+
if (debug.includes('anvil:*') || debug.includes('anvil:kindling')) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
function sanitizeForLog(value) {
|
|
20
|
+
let sanitized = value.replace(/\b(sk-|ghp_|ghu_)[A-Za-z0-9_-]+/g, '[REDACTED]');
|
|
21
|
+
sanitized = sanitized.replace(/Bearer\s+[A-Za-z0-9_.+/=-]+/g, 'Bearer [REDACTED]');
|
|
22
|
+
sanitized = sanitized.replace(/\b[0-9a-fA-F]{40,}\b/g, '[REDACTED]');
|
|
23
|
+
sanitized = sanitized.replace(/\b[A-Za-z0-9+/]{20,}={0,3}\b/g, '[REDACTED]');
|
|
24
|
+
return sanitized;
|
|
25
|
+
}
|
|
26
|
+
function debugLog(namespace, message, data) {
|
|
27
|
+
if (!isDebugEnabled()) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const timestamp = new Date().toISOString();
|
|
31
|
+
const prefix = `[${timestamp}] [anvil:${namespace}]`;
|
|
32
|
+
const sanitizedMessage = sanitizeForLog(message);
|
|
33
|
+
/* eslint-disable no-console -- debug utility */
|
|
34
|
+
if (data !== undefined) {
|
|
35
|
+
if (data instanceof Error) {
|
|
36
|
+
console.debug(`${prefix} ${sanitizedMessage}:`, sanitizeForLog(data.message));
|
|
37
|
+
if (data.stack) {
|
|
38
|
+
console.debug(`${prefix} Stack:`, sanitizeForLog(data.stack));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (typeof data === 'string') {
|
|
42
|
+
console.debug(`${prefix} ${sanitizedMessage}:`, sanitizeForLog(data));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.debug(`${prefix} ${sanitizedMessage}:`, data);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.debug(`${prefix} ${sanitizedMessage}`);
|
|
50
|
+
}
|
|
51
|
+
/* eslint-enable no-console */
|
|
52
|
+
}
|
|
53
|
+
export function createDebugger(_namespace) {
|
|
54
|
+
return (message, data) => debugLog('kindling', message, data);
|
|
55
|
+
}
|