wyrm-mcp 7.2.0 → 7.2.2
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 +26 -667
- package/NOTICE +14 -33
- package/dist/activation.d.ts.map +1 -1
- package/dist/activation.js +1 -44
- package/dist/activation.js.map +1 -1
- package/dist/agent-daemon.js +4 -281
- package/dist/agent-loop.js +7 -332
- package/dist/analytics.js +13 -236
- package/dist/attribution.js +1 -49
- package/dist/audit.js +2 -457
- package/dist/auto-capture.js +3 -138
- package/dist/auto-orchestrator.js +1 -325
- package/dist/autoconfig.js +39 -840
- package/dist/buddy-runner.js +1 -109
- package/dist/buddy.js +14 -564
- package/dist/build-flags.js +1 -17
- package/dist/capabilities.js +3 -183
- package/dist/capture.js +1 -56
- package/dist/causality.js +6 -107
- package/dist/cli.js +20 -281
- package/dist/cloud/cli.js +5 -541
- package/dist/cloud/client.js +1 -221
- package/dist/cloud/crypto.js +1 -85
- package/dist/cloud/machine-id.js +2 -113
- package/dist/cloud/recovery.js +1 -60
- package/dist/cloud/sync-engine.js +7 -543
- package/dist/cloud-backup.js +5 -579
- package/dist/cloud-profile.js +1 -138
- package/dist/cloud-sync-entrypoint.js +1 -47
- package/dist/cloud-sync.js +2 -309
- package/dist/constellation.js +12 -168
- package/dist/context-build-budgeted.js +4 -144
- package/dist/context-ranking.js +1 -69
- package/dist/crypto.js +1 -179
- package/dist/daemon-write-endpoint.js +1 -290
- package/dist/daemon-writer.js +2 -406
- package/dist/database.js +43 -1110
- package/dist/deprecations.js +2 -162
- package/dist/design.js +13 -141
- package/dist/event-replication.js +1 -112
- package/dist/events-sse.js +7 -43
- package/dist/events.js +6 -238
- package/dist/failure-patterns.js +42 -659
- package/dist/federation.js +12 -236
- package/dist/goals.js +13 -101
- package/dist/golden.js +3 -355
- package/dist/handlers/agent.js +4 -165
- package/dist/handlers/alias-adapters.js +1 -129
- package/dist/handlers/aliases.js +1 -171
- package/dist/handlers/audit.js +1 -87
- package/dist/handlers/boundary.js +1 -221
- package/dist/handlers/capture.js +73 -1109
- package/dist/handlers/causality.js +7 -114
- package/dist/handlers/cloud.js +85 -382
- package/dist/handlers/companion.js +28 -459
- package/dist/handlers/datalake.js +7 -187
- package/dist/handlers/dispatch-context.js +0 -22
- package/dist/handlers/entity.js +25 -256
- package/dist/handlers/events.js +16 -335
- package/dist/handlers/failure.js +13 -340
- package/dist/handlers/goals.js +4 -296
- package/dist/handlers/intelligence.js +126 -674
- package/dist/handlers/invoicing.js +1 -70
- package/dist/handlers/mcpclient.js +6 -137
- package/dist/handlers/orchestration.js +40 -125
- package/dist/handlers/output-schemas.js +1 -24
- package/dist/handlers/presence.js +3 -99
- package/dist/handlers/project.js +28 -182
- package/dist/handlers/prompts.js +6 -157
- package/dist/handlers/quest.js +4 -224
- package/dist/handlers/recall.js +11 -218
- package/dist/handlers/registry.js +1 -167
- package/dist/handlers/resources.js +1 -288
- package/dist/handlers/review.js +11 -74
- package/dist/handlers/run.js +17 -487
- package/dist/handlers/search.js +15 -326
- package/dist/handlers/session.js +28 -615
- package/dist/handlers/share.js +8 -184
- package/dist/handlers/shims.js +1 -464
- package/dist/handlers/skill.js +67 -449
- package/dist/handlers/survivors.js +1 -120
- package/dist/handlers/symbols.js +8 -109
- package/dist/handlers/syncops.js +4 -302
- package/dist/handlers/types.js +1 -27
- package/dist/harvest.js +5 -191
- package/dist/hours.js +7 -156
- package/dist/http-auth.js +3 -321
- package/dist/http-fast.js +21 -1137
- package/dist/icons.js +1 -47
- package/dist/index.js +2 -924
- package/dist/indexer.js +4 -145
- package/dist/intelligence.js +31 -261
- package/dist/internal-dispatch.js +3 -212
- package/dist/keyset.js +1 -110
- package/dist/knowledge-graph.js +12 -176
- package/dist/license.d.ts +11 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +2 -414
- package/dist/license.js.map +1 -1
- package/dist/logger.js +2 -199
- package/dist/maintenance.js +2 -148
- package/dist/mcp-client.js +6 -262
- package/dist/memory-artifacts.js +30 -449
- package/dist/migrate-prompt.js +2 -124
- package/dist/migrations.js +40 -655
- package/dist/performance.js +1 -228
- package/dist/presence.js +11 -140
- package/dist/priority-embed.js +5 -164
- package/dist/providers/embedding-provider.js +1 -196
- package/dist/readonly-gate.js +1 -29
- package/dist/rehydration.js +9 -157
- package/dist/reindex.js +1 -88
- package/dist/render-target.js +21 -514
- package/dist/render.js +4 -280
- package/dist/repl-guard.js +1 -173
- package/dist/replication-daemon-entrypoint.js +1 -31
- package/dist/replication-daemon.js +2 -262
- package/dist/resilience.js +1 -591
- package/dist/reverse-bridge.js +5 -360
- package/dist/security.js +1 -244
- package/dist/session-seen.js +3 -51
- package/dist/setup.js +1 -260
- package/dist/skill-author.js +5 -168
- package/dist/spec-kit.js +1 -191
- package/dist/sqlite-busy.js +1 -154
- package/dist/statusline.js +11 -315
- package/dist/sub-agent.js +13 -262
- package/dist/summarizer.js +13 -139
- package/dist/symbols.js +7 -283
- package/dist/sync.js +5 -359
- package/dist/tasks-dispatch.js +1 -84
- package/dist/tasks.js +1 -282
- package/dist/token-budget.js +1 -143
- package/dist/tool-analytics.js +7 -129
- package/dist/tool-annotations.js +1 -365
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +1 -75
- package/dist/trace-harvest.js +6 -244
- package/dist/types.js +1 -30
- package/dist/ui-dashboard.js +41 -50
- package/dist/ulid.js +1 -81
- package/dist/validate.js +1 -129
- package/dist/vault.js +1 -534
- package/dist/vectors.js +3 -184
- package/dist/version-check.js +4 -136
- package/dist/visibility.js +19 -155
- package/dist/wyrm-cli.js +98 -2451
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.js +14 -424
- package/dist/wyrm-loop.js +3 -150
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +1 -11
- package/dist/wyrm-statusline.js +4 -56
- package/dist/wyrm-ui.js +9 -77
- package/package.json +4 -2
package/dist/audit.js
CHANGED
|
@@ -1,460 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
* Compliance audit trail (Tier 3.9) — per-agent attribution + versioned
|
|
3
|
-
* Ed25519 signing (v7 F2, T010).
|
|
4
|
-
*
|
|
5
|
-
* Hash-chained event log: each row records sha256(prev_hash || payload ||
|
|
6
|
-
* timestamp). Tampering with any historical entry breaks the chain.
|
|
7
|
-
* Optional Ed25519 signing layered on top provides a SOC2 / HIPAA-grade
|
|
8
|
-
* audit surface — every AI decision, prompt, tool call, code edit, with
|
|
9
|
-
* the prompt that caused it.
|
|
10
|
-
*
|
|
11
|
-
* `wyrm_audit_export(range)` produces a tamper-evident JSON bundle that
|
|
12
|
-
* an external auditor can verify offline:
|
|
13
|
-
* 1. Recompute the hash chain from genesis through the range.
|
|
14
|
-
* 2. Verify Ed25519 signatures against the operator's public key(s).
|
|
15
|
-
* 3. Confirm no gaps in IDs.
|
|
16
|
-
*
|
|
17
|
-
* ## Signed-payload versioning (v7 F2, T010)
|
|
18
|
-
*
|
|
19
|
-
* Migration 20's `agent_id`/`run_id` columns are NOT part of the hash-chain
|
|
20
|
-
* input — the chain rule is byte-identical to 6.x:
|
|
21
|
-
*
|
|
22
|
-
* row_hash = sha256(`${prev_hash}|${event_kind}|${payload_json}|${logged_at}`)
|
|
23
|
-
*
|
|
24
|
-
* so a 6.x DB opened under v7 keeps verifying and the v1→v2 transition can
|
|
25
|
-
* never break the chain (old rows verify as v1, new rows as v2, one chain).
|
|
26
|
-
* Attribution is instead bound TAMPER-EVIDENTLY into the Ed25519 signature
|
|
27
|
-
* via a payload-version byte:
|
|
28
|
-
*
|
|
29
|
-
* v1 (legacy 6.x): signed message = utf8(row_hash) — no attribution
|
|
30
|
-
* stored signature = bare base64 (unchanged on old rows)
|
|
31
|
-
* v2 (v7): signed message = 0x02 ‖ utf8(JSON.stringify(
|
|
32
|
-
* { row_hash, agent_id, run_id })) — attribution bound
|
|
33
|
-
* stored signature = `v2:<key_fp16>:<base64>`
|
|
34
|
-
*
|
|
35
|
-
* The leading 0x02 version byte domain-separates the payloads (a v1 message
|
|
36
|
-
* is 64 ASCII-hex bytes and can never begin with 0x02), and the canonical
|
|
37
|
-
* JSON body (fixed key order, NULLs preserved) makes the agent_id/run_id
|
|
38
|
-
* field boundaries unambiguous — no delimiter-shifting forgeries.
|
|
39
|
-
* `key_fp16` = first 16 hex chars of sha256(SPKI DER of the signing public
|
|
40
|
-
* key): the "which key" answer, claimed in the row and CONFIRMED whenever
|
|
41
|
-
* public keys are handed to `verify()`.
|
|
42
|
-
*
|
|
43
|
-
* ## The rule for 6.x verifiers (explicit — locked by tests/audit-attribution.test.ts)
|
|
44
|
-
*
|
|
45
|
-
* The verify structure 6.x actually shipped (`verify()` / `verifyBundle()`)
|
|
46
|
-
* checks ONLY the hash chain and never parses the `signature` column.
|
|
47
|
-
* Therefore:
|
|
48
|
-
* (a) a 6.x verifier verifies BOTH v1 and v2 rows exactly as today — the
|
|
49
|
-
* chain rule is unchanged, mixed v1/v2 chains pass, and exported
|
|
50
|
-
* bundles remain version-1 bundles that 6.x `verifyBundle()` accepts;
|
|
51
|
-
* (b) a 6.x auditor's out-of-band Ed25519 step ("signature of the
|
|
52
|
-
* row_hash") FAILS CLOSED on a v2 row: the stored `v2:`-prefixed value
|
|
53
|
-
* is not the base64 of an Ed25519 signature over the row_hash, so the
|
|
54
|
-
* 6.x signature check reports INVALID — it can never silently accept a
|
|
55
|
-
* v7 signature (and so can never silently accept forged attribution).
|
|
56
|
-
* The chain walk itself stays green.
|
|
57
|
-
*
|
|
58
|
-
* Signing is OPT-IN (Articles I & VII): no key configured → rows are written
|
|
59
|
-
* unsigned, byte-identical to 6.x behavior. `WYRM_AUDIT_SIGNING_KEY` holds a
|
|
60
|
-
* PEM-encoded (pkcs8) Ed25519 private key, or a path to one.
|
|
61
|
-
*
|
|
62
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
63
|
-
* @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
|
|
64
|
-
*/
|
|
65
|
-
import { createHash, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify, } from 'crypto';
|
|
66
|
-
import { readFileSync } from 'fs';
|
|
67
|
-
import { getActor } from './handlers/boundary.js';
|
|
68
|
-
import { resolveAttribution } from './attribution.js';
|
|
69
|
-
const GENESIS_HASH = 'genesis';
|
|
70
|
-
// ==================== v7 F2 (T010) — versioned signed payload ====================
|
|
71
|
-
/** Stored-signature prefix marking a v2 (attribution-binding) signature. */
|
|
72
|
-
export const AUDIT_SIG_V2_PREFIX = 'v2:';
|
|
73
|
-
/** The payload-version byte prepended to the v2 signed message. */
|
|
74
|
-
const V2_VERSION_BYTE = 0x02;
|
|
75
|
-
/** Hex chars of sha256(SPKI DER) used as the key fingerprint. */
|
|
76
|
-
const KEY_FP_LEN = 16;
|
|
77
|
-
const KEY_FP_RE = new RegExp(`^[0-9a-f]{${KEY_FP_LEN}}$`);
|
|
78
|
-
/**
|
|
79
|
-
* Classify a stored signature string by payload version.
|
|
80
|
-
* Bare base64 (anything without the `v2:` prefix) = v1 legacy; `v2:<fp>:<b64>`
|
|
81
|
-
* = v2. Returns null for unsigned (NULL/empty). Never throws.
|
|
82
|
-
*/
|
|
83
|
-
export function parseAuditSignature(signature) {
|
|
84
|
-
if (typeof signature !== 'string' || signature.length === 0)
|
|
85
|
-
return null;
|
|
86
|
-
if (signature.startsWith(AUDIT_SIG_V2_PREFIX)) {
|
|
87
|
-
const rest = signature.slice(AUDIT_SIG_V2_PREFIX.length);
|
|
88
|
-
const sep = rest.indexOf(':');
|
|
89
|
-
const fp = sep >= 0 ? rest.slice(0, sep) : '';
|
|
90
|
-
const b64 = sep >= 0 ? rest.slice(sep + 1) : '';
|
|
91
|
-
return { version: 2, key_fp: KEY_FP_RE.test(fp) ? fp : null, sig_b64: b64 };
|
|
92
|
-
}
|
|
93
|
-
return { version: 1, key_fp: null, sig_b64: signature };
|
|
94
|
-
}
|
|
95
|
-
/** Fingerprint of an Ed25519 public key: sha256 over the SPKI DER, first 16 hex. */
|
|
96
|
-
export function auditKeyFingerprint(publicKey) {
|
|
97
|
-
const key = typeof publicKey === 'string' ? createPublicKey(publicKey) : publicKey;
|
|
98
|
-
const der = key.export({ type: 'spki', format: 'der' });
|
|
99
|
-
return createHash('sha256').update(der).digest('hex').slice(0, KEY_FP_LEN);
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Build the exact byte string an audit Ed25519 signature covers.
|
|
103
|
-
* v1: utf8(row_hash) — the legacy 6.x payload, no attribution.
|
|
104
|
-
* v2: 0x02 version byte ‖ utf8(JSON.stringify({row_hash, agent_id, run_id})).
|
|
105
|
-
*/
|
|
106
|
-
export function buildSignedPayload(version, fields) {
|
|
107
|
-
if (version === 1)
|
|
108
|
-
return Buffer.from(fields.row_hash, 'utf-8');
|
|
109
|
-
const body = JSON.stringify({
|
|
110
|
-
row_hash: fields.row_hash,
|
|
111
|
-
agent_id: fields.agent_id ?? null,
|
|
112
|
-
run_id: fields.run_id ?? null,
|
|
113
|
-
});
|
|
114
|
-
return Buffer.concat([Buffer.from([V2_VERSION_BYTE]), Buffer.from(body, 'utf-8')]);
|
|
115
|
-
}
|
|
116
|
-
/** Materialize a signer from a PEM (pkcs8) Ed25519 private key; the public
|
|
117
|
-
* half and fingerprint are derived. Throws on non-Ed25519 keys. */
|
|
118
|
-
export function createAuditSigner(privateKeyPem) {
|
|
119
|
-
const privateKey = createPrivateKey(privateKeyPem);
|
|
120
|
-
if (privateKey.asymmetricKeyType !== 'ed25519') {
|
|
121
|
-
throw new Error(`audit signing key must be Ed25519 (got ${privateKey.asymmetricKeyType})`);
|
|
122
|
-
}
|
|
123
|
-
const pub = createPublicKey(privateKey);
|
|
124
|
-
return {
|
|
125
|
-
privateKey,
|
|
126
|
-
publicKeyPem: pub.export({ type: 'spki', format: 'pem' }).toString(),
|
|
127
|
-
key_fp: auditKeyFingerprint(pub),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
/** Sign the v2 payload for a row. Returns the stored form `v2:<fp>:<base64>`. */
|
|
131
|
-
export function signAuditEntry(signer, fields) {
|
|
132
|
-
const sig = edSign(null, buildSignedPayload(2, fields), signer.privateKey);
|
|
133
|
-
return `${AUDIT_SIG_V2_PREFIX}${signer.key_fp}:${sig.toString('base64')}`;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Opt-in signer bootstrap (Articles I & VII): `WYRM_AUDIT_SIGNING_KEY` is a
|
|
137
|
-
* PEM string or a path to a PEM file. Absent → null (rows stay unsigned —
|
|
138
|
-
* exactly the 6.x behavior). Unusable → warn on STDERR (stdout is the MCP
|
|
139
|
-
* wire) and run unsigned; never throws.
|
|
140
|
-
*/
|
|
141
|
-
export function loadAuditSignerFromEnv(env = process.env) {
|
|
142
|
-
const raw = env.WYRM_AUDIT_SIGNING_KEY?.trim();
|
|
143
|
-
if (!raw)
|
|
144
|
-
return null;
|
|
145
|
-
try {
|
|
146
|
-
const pem = raw.includes('-----BEGIN') ? raw : readFileSync(raw, 'utf-8');
|
|
147
|
-
return createAuditSigner(pem);
|
|
148
|
-
}
|
|
149
|
-
catch (e) {
|
|
150
|
-
console.error(`[wyrm] WYRM_AUDIT_SIGNING_KEY unusable (${e.message}) — audit rows will be unsigned`);
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Verify ONE row's signature against a set of public keys (PEM or KeyObject).
|
|
156
|
-
* Fail-closed: malformed signatures, unknown fingerprints, undecodable
|
|
157
|
-
* base64, and crypto-layer rejections all report ok=false. Never throws.
|
|
158
|
-
*/
|
|
159
|
-
export function verifyAuditEntrySignature(row, publicKeys) {
|
|
160
|
-
const parsed = parseAuditSignature(row.signature);
|
|
161
|
-
if (!parsed)
|
|
162
|
-
return { ok: true, version: null, key_fp: null };
|
|
163
|
-
const keys = [];
|
|
164
|
-
for (const k of publicKeys) {
|
|
165
|
-
try {
|
|
166
|
-
const key = typeof k === 'string' ? createPublicKey(k) : k;
|
|
167
|
-
keys.push({ key, fp: auditKeyFingerprint(key) });
|
|
168
|
-
}
|
|
169
|
-
catch { /* unusable provided key — skipped (signed rows then fail closed) */ }
|
|
170
|
-
}
|
|
171
|
-
const sig = Buffer.from(parsed.sig_b64, 'base64');
|
|
172
|
-
if (parsed.version === 2) {
|
|
173
|
-
if (!parsed.key_fp) {
|
|
174
|
-
return { ok: false, version: 2, key_fp: null, reason: 'malformed v2 signature (missing key fingerprint)' };
|
|
175
|
-
}
|
|
176
|
-
const candidates = keys.filter((k) => k.fp === parsed.key_fp);
|
|
177
|
-
if (candidates.length === 0) {
|
|
178
|
-
return { ok: false, version: 2, key_fp: parsed.key_fp, reason: `no provided key matches fingerprint ${parsed.key_fp}` };
|
|
179
|
-
}
|
|
180
|
-
const payload = buildSignedPayload(2, row);
|
|
181
|
-
for (const c of candidates) {
|
|
182
|
-
try {
|
|
183
|
-
if (edVerify(null, payload, c.key, sig))
|
|
184
|
-
return { ok: true, version: 2, key_fp: parsed.key_fp };
|
|
185
|
-
}
|
|
186
|
-
catch { /* fail closed */ }
|
|
187
|
-
}
|
|
188
|
-
return {
|
|
189
|
-
ok: false, version: 2, key_fp: parsed.key_fp,
|
|
190
|
-
reason: 'v2 signature does not verify (row_hash/agent_id/run_id tampered or wrong key)',
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
// v1: legacy signature over the bare row_hash; no fingerprint — try every key.
|
|
194
|
-
const payload = buildSignedPayload(1, row);
|
|
195
|
-
for (const c of keys) {
|
|
196
|
-
try {
|
|
197
|
-
if (edVerify(null, payload, c.key, sig))
|
|
198
|
-
return { ok: true, version: 1, key_fp: c.fp };
|
|
199
|
-
}
|
|
200
|
-
catch { /* fail closed */ }
|
|
201
|
-
}
|
|
202
|
-
return { ok: false, version: 1, key_fp: null, reason: 'v1 signature does not verify under any provided key' };
|
|
203
|
-
}
|
|
204
|
-
/** Shared by verify()/verifyBundle(): version counts, who/which-run/which-key,
|
|
205
|
-
* and (when keys are provided) the signature-layer check. */
|
|
206
|
-
function summarizeAttribution(rows, publicKeys) {
|
|
207
|
-
const versions = { v1: 0, v2: 0, unsigned: 0 };
|
|
208
|
-
const agents = new Set();
|
|
209
|
-
const runs = new Set();
|
|
210
|
-
const keysSeen = new Set();
|
|
211
|
-
const checkSigs = Array.isArray(publicKeys) && publicKeys.length > 0;
|
|
212
|
-
// Materialize keys ONCE (not per row).
|
|
213
|
-
const materialized = [];
|
|
214
|
-
if (checkSigs) {
|
|
215
|
-
for (const k of publicKeys) {
|
|
216
|
-
try {
|
|
217
|
-
materialized.push(createPublicKey(k));
|
|
218
|
-
}
|
|
219
|
-
catch { /* skipped — fail closed below */ }
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
const report = { checked: 0, valid: 0, invalid: 0 };
|
|
223
|
-
for (const row of rows) {
|
|
224
|
-
const att = resolveAttribution(row);
|
|
225
|
-
agents.add(att.actor);
|
|
226
|
-
if (att.run_id)
|
|
227
|
-
runs.add(att.run_id);
|
|
228
|
-
const parsed = parseAuditSignature(row.signature);
|
|
229
|
-
if (!parsed)
|
|
230
|
-
versions.unsigned++;
|
|
231
|
-
else if (parsed.version === 2) {
|
|
232
|
-
versions.v2++;
|
|
233
|
-
if (parsed.key_fp)
|
|
234
|
-
keysSeen.add(parsed.key_fp);
|
|
235
|
-
}
|
|
236
|
-
else
|
|
237
|
-
versions.v1++;
|
|
238
|
-
if (checkSigs && parsed) {
|
|
239
|
-
report.checked++;
|
|
240
|
-
const res = verifyAuditEntrySignature(row, materialized);
|
|
241
|
-
if (res.ok) {
|
|
242
|
-
report.valid++;
|
|
243
|
-
if (res.key_fp)
|
|
244
|
-
keysSeen.add(res.key_fp);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
report.invalid++;
|
|
248
|
-
if (report.first_invalid_id === undefined) {
|
|
249
|
-
report.first_invalid_id = row.id;
|
|
250
|
-
report.reason = res.reason;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
payload_versions: versions,
|
|
257
|
-
attribution: { agents: [...agents].sort(), runs: [...runs].sort(), keys: [...keysSeen].sort() },
|
|
258
|
-
signatures: checkSigs ? report : undefined,
|
|
259
|
-
sigOk: !checkSigs || report.invalid === 0,
|
|
260
|
-
sigReason: report.first_invalid_id !== undefined
|
|
261
|
-
? `signature invalid at id ${report.first_invalid_id}: ${report.reason}`
|
|
262
|
-
: undefined,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
export class AuditLog {
|
|
266
|
-
db;
|
|
267
|
-
signer;
|
|
268
|
-
constructor(db, signer) {
|
|
269
|
-
this.db = db;
|
|
270
|
-
this.signer = signer ?? null;
|
|
271
|
-
}
|
|
272
|
-
/** Swap or clear the signing identity (key rotation, tests). */
|
|
273
|
-
setSigner(signer) {
|
|
274
|
-
this.signer = signer;
|
|
275
|
-
}
|
|
276
|
-
/** Get the hash of the most recent entry (or 'genesis' if empty). */
|
|
277
|
-
latestHash() {
|
|
278
|
-
const row = this.db.prepare('SELECT row_hash FROM audit_log ORDER BY id DESC LIMIT 1').get();
|
|
279
|
-
return row?.row_hash ?? GENESIS_HASH;
|
|
280
|
-
}
|
|
281
|
-
/** Append a hash-chained event. Returns the inserted row.
|
|
282
|
-
*
|
|
283
|
-
* v7 F2 review fix: the head-read + INSERT pair runs in ONE transaction
|
|
284
|
-
* declared IMMEDIATE, so BEGIN takes the cross-process write lock BEFORE
|
|
285
|
-
* latestHash() reads the chain head. Two concurrent OS processes appending
|
|
286
|
-
* (the T010 per-agent-key fleet shape — audit ops are not in
|
|
287
|
-
* DAEMON_WRITE_OPS, so fleet agents write audit_log directly) could
|
|
288
|
-
* otherwise both read head H and both insert prev_hash=H, FORKING the
|
|
289
|
-
* chain — after which verify() reports the compliance log as tampered
|
|
290
|
-
* forever on legitimate writes. */
|
|
291
|
-
log(input) {
|
|
292
|
-
const payloadJson = JSON.stringify(input.payload);
|
|
293
|
-
const loggedAt = new Date().toISOString();
|
|
294
|
-
// v7 F2 (T009): attribution columns ride beside the chained fields. The
|
|
295
|
-
// hash input is UNCHANGED (6.x verifiers keep verifying); the v2 signed
|
|
296
|
-
// payload (T010) is what binds agent_id/run_id tamper-evidently.
|
|
297
|
-
const ambient = getActor();
|
|
298
|
-
const agentId = input.agent_id !== undefined ? input.agent_id : ambient.agent_id;
|
|
299
|
-
const runId = input.run_id !== undefined ? input.run_id : ambient.run_id;
|
|
300
|
-
const append = this.db.transaction(() => {
|
|
301
|
-
const prevHash = this.latestHash();
|
|
302
|
-
const hashInput = `${prevHash}|${input.event_kind}|${payloadJson}|${loggedAt}`;
|
|
303
|
-
const rowHash = createHash('sha256').update(hashInput).digest('hex');
|
|
304
|
-
// v7 F2 (T010): explicit caller-supplied signature wins (legacy v1 path,
|
|
305
|
-
// stored as-is); otherwise a configured signer auto-signs the v2 payload.
|
|
306
|
-
const signature = input.signature != null
|
|
307
|
-
? input.signature
|
|
308
|
-
: this.signer
|
|
309
|
-
? signAuditEntry(this.signer, { row_hash: rowHash, agent_id: agentId, run_id: runId })
|
|
310
|
-
: null;
|
|
311
|
-
const info = this.db.prepare(`
|
|
1
|
+
import{createHash as g,createPrivateKey as w,createPublicKey as c,sign as $,verify as y}from"crypto";import{readFileSync as R}from"fs";import{getActor as I}from"./handlers/boundary.js";import{resolveAttribution as O}from"./attribution.js";const h="genesis",p="v2:",x=2,k=16,b=new RegExp(`^[0-9a-f]{${k}}$`);function m(s){if(typeof s!="string"||s.length===0)return null;if(s.startsWith(p)){const e=s.slice(p.length),t=e.indexOf(":"),o=t>=0?e.slice(0,t):"",r=t>=0?e.slice(t+1):"";return{version:2,key_fp:b.test(o)?o:null,sig_b64:r}}return{version:1,key_fp:null,sig_b64:s}}function E(s){const t=(typeof s=="string"?c(s):s).export({type:"spki",format:"der"});return g("sha256").update(t).digest("hex").slice(0,k)}function v(s,e){if(s===1)return Buffer.from(e.row_hash,"utf-8");const t=JSON.stringify({row_hash:e.row_hash,agent_id:e.agent_id??null,run_id:e.run_id??null});return Buffer.concat([Buffer.from([x]),Buffer.from(t,"utf-8")])}function A(s){const e=w(s);if(e.asymmetricKeyType!=="ed25519")throw new Error(`audit signing key must be Ed25519 (got ${e.asymmetricKeyType})`);const t=c(e);return{privateKey:e,publicKeyPem:t.export({type:"spki",format:"pem"}).toString(),key_fp:E(t)}}function D(s,e){const t=$(null,v(2,e),s.privateKey);return`${p}${s.key_fp}:${t.toString("base64")}`}function Y(s=process.env){const e=s.WYRM_AUDIT_SIGNING_KEY?.trim();if(!e)return null;try{const t=e.includes("-----BEGIN")?e:R(e,"utf-8");return A(t)}catch(t){return console.error(`[wyrm] WYRM_AUDIT_SIGNING_KEY unusable (${t.message}) \u2014 audit rows will be unsigned`),null}}function T(s,e){const t=m(s.signature);if(!t)return{ok:!0,version:null,key_fp:null};const o=[];for(const n of e)try{const d=typeof n=="string"?c(n):n;o.push({key:d,fp:E(d)})}catch{}const r=Buffer.from(t.sig_b64,"base64");if(t.version===2){if(!t.key_fp)return{ok:!1,version:2,key_fp:null,reason:"malformed v2 signature (missing key fingerprint)"};const n=o.filter(a=>a.fp===t.key_fp);if(n.length===0)return{ok:!1,version:2,key_fp:t.key_fp,reason:`no provided key matches fingerprint ${t.key_fp}`};const d=v(2,s);for(const a of n)try{if(y(null,d,a.key,r))return{ok:!0,version:2,key_fp:t.key_fp}}catch{}return{ok:!1,version:2,key_fp:t.key_fp,reason:"v2 signature does not verify (row_hash/agent_id/run_id tampered or wrong key)"}}const i=v(1,s);for(const n of o)try{if(y(null,i,n.key,r))return{ok:!0,version:1,key_fp:n.fp}}catch{}return{ok:!1,version:1,key_fp:null,reason:"v1 signature does not verify under any provided key"}}function S(s,e){const t={v1:0,v2:0,unsigned:0},o=new Set,r=new Set,i=new Set,n=Array.isArray(e)&&e.length>0,d=[];if(n)for(const l of e)try{d.push(c(l))}catch{}const a={checked:0,valid:0,invalid:0};for(const l of s){const f=O(l);o.add(f.actor),f.run_id&&r.add(f.run_id);const _=m(l.signature);if(_?_.version===2?(t.v2++,_.key_fp&&i.add(_.key_fp)):t.v1++:t.unsigned++,n&&_){a.checked++;const u=T(l,d);u.ok?(a.valid++,u.key_fp&&i.add(u.key_fp)):(a.invalid++,a.first_invalid_id===void 0&&(a.first_invalid_id=l.id,a.reason=u.reason))}}return{payload_versions:t,attribution:{agents:[...o].sort(),runs:[...r].sort(),keys:[...i].sort()},signatures:n?a:void 0,sigOk:!n||a.invalid===0,sigReason:a.first_invalid_id!==void 0?`signature invalid at id ${a.first_invalid_id}: ${a.reason}`:void 0}}class C{db;signer;constructor(e,t){this.db=e,this.signer=t??null}setSigner(e){this.signer=e}latestHash(){return this.db.prepare("SELECT row_hash FROM audit_log ORDER BY id DESC LIMIT 1").get()?.row_hash??h}log(e){const t=JSON.stringify(e.payload),o=new Date().toISOString(),r=I(),i=e.agent_id!==void 0?e.agent_id:r.agent_id,n=e.run_id!==void 0?e.run_id:r.run_id,d=this.db.transaction(()=>{const a=this.latestHash(),l=`${a}|${e.event_kind}|${t}|${o}`,f=g("sha256").update(l).digest("hex"),_=e.signature!=null?e.signature:this.signer?D(this.signer,{row_hash:f,agent_id:i,run_id:n}):null;return this.db.prepare(`
|
|
312
2
|
INSERT INTO audit_log
|
|
313
3
|
(event_kind, actor, project_id, payload_json, prev_hash, row_hash, signature, logged_at, agent_id, run_id)
|
|
314
4
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
315
|
-
`).run(
|
|
316
|
-
return info.lastInsertRowid;
|
|
317
|
-
});
|
|
318
|
-
return this.get(append.immediate());
|
|
319
|
-
}
|
|
320
|
-
get(id) {
|
|
321
|
-
return this.db.prepare('SELECT * FROM audit_log WHERE id = ?').get(id) ?? null;
|
|
322
|
-
}
|
|
323
|
-
/** Verify the entire chain from the first row through the latest.
|
|
324
|
-
* Useful for periodic self-audits.
|
|
325
|
-
*
|
|
326
|
-
* v7 F2 (T010): pass `publicKeys` (PEM strings) to additionally verify the
|
|
327
|
-
* Ed25519 signature layer; the report then answers who / which-run /
|
|
328
|
-
* which-key. Without keys the result is the 6.x chain-only semantics plus
|
|
329
|
-
* the attribution summary. */
|
|
330
|
-
verify(opts) {
|
|
331
|
-
const rows = this.db.prepare('SELECT * FROM audit_log ORDER BY id ASC').all();
|
|
332
|
-
if (rows.length === 0) {
|
|
333
|
-
return {
|
|
334
|
-
ok: true, total: 0, verified: 0,
|
|
335
|
-
payload_versions: { v1: 0, v2: 0, unsigned: 0 },
|
|
336
|
-
attribution: { agents: [], runs: [], keys: [] },
|
|
337
|
-
...(opts?.publicKeys?.length ? { signatures: { checked: 0, valid: 0, invalid: 0 } } : {}),
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
let expectedPrev = GENESIS_HASH;
|
|
341
|
-
for (const row of rows) {
|
|
342
|
-
if (row.prev_hash !== expectedPrev) {
|
|
343
|
-
return {
|
|
344
|
-
ok: false, total: rows.length, verified: row.id - 1,
|
|
345
|
-
first_invalid_id: row.id,
|
|
346
|
-
reason: `prev_hash mismatch: expected ${expectedPrev}, got ${row.prev_hash}`,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
const recomputed = createHash('sha256')
|
|
350
|
-
.update(`${row.prev_hash}|${row.event_kind}|${row.payload_json}|${row.logged_at}`)
|
|
351
|
-
.digest('hex');
|
|
352
|
-
if (recomputed !== row.row_hash) {
|
|
353
|
-
return {
|
|
354
|
-
ok: false, total: rows.length, verified: row.id - 1,
|
|
355
|
-
first_invalid_id: row.id,
|
|
356
|
-
reason: `row_hash mismatch at id ${row.id}: recomputed ${recomputed}, stored ${row.row_hash}`,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
expectedPrev = row.row_hash;
|
|
360
|
-
}
|
|
361
|
-
const s = summarizeAttribution(rows, opts?.publicKeys);
|
|
362
|
-
return {
|
|
363
|
-
ok: s.sigOk,
|
|
364
|
-
total: rows.length,
|
|
365
|
-
verified: rows.length,
|
|
366
|
-
...(s.sigOk ? {} : { reason: s.sigReason }),
|
|
367
|
-
payload_versions: s.payload_versions,
|
|
368
|
-
attribution: s.attribution,
|
|
369
|
-
...(s.signatures ? { signatures: s.signatures } : {}),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
/** Export a date-ranged tamper-evident bundle. Range is inclusive on
|
|
373
|
-
* both ends. Verification still walks from genesis to ensure no gaps. */
|
|
374
|
-
export(opts) {
|
|
375
|
-
const params = [];
|
|
376
|
-
let where = '';
|
|
377
|
-
if (opts.range_start) {
|
|
378
|
-
where += ' AND logged_at >= ?';
|
|
379
|
-
params.push(opts.range_start);
|
|
380
|
-
}
|
|
381
|
-
if (opts.range_end) {
|
|
382
|
-
where += ' AND logged_at <= ?';
|
|
383
|
-
params.push(opts.range_end);
|
|
384
|
-
}
|
|
385
|
-
const entries = this.db.prepare(`SELECT * FROM audit_log WHERE 1=1 ${where} ORDER BY id ASC`).all(...params);
|
|
386
|
-
return {
|
|
387
|
-
version: 1,
|
|
388
|
-
range: {
|
|
389
|
-
start: opts.range_start ?? entries[0]?.logged_at ?? new Date(0).toISOString(),
|
|
390
|
-
end: opts.range_end ?? entries[entries.length - 1]?.logged_at ?? new Date().toISOString(),
|
|
391
|
-
},
|
|
392
|
-
exported_at: new Date().toISOString(),
|
|
393
|
-
entry_count: entries.length,
|
|
394
|
-
first_id: entries[0]?.id ?? null,
|
|
395
|
-
last_id: entries[entries.length - 1]?.id ?? null,
|
|
396
|
-
genesis_prev_hash: entries[0]?.prev_hash ?? GENESIS_HASH,
|
|
397
|
-
final_row_hash: entries[entries.length - 1]?.row_hash ?? GENESIS_HASH,
|
|
398
|
-
entries,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
/** Statically verify a previously-exported bundle (no DB required).
|
|
402
|
-
* Useful for the external auditor who only has the JSON file.
|
|
403
|
-
*
|
|
404
|
-
* The chain walk is the unchanged 6.x structure — it never parses
|
|
405
|
-
* signatures, so mixed v1/v2 bundles verify exactly as they did under 6.x.
|
|
406
|
-
* v7 F2 (T010): optional `publicKeys` adds the signature-layer check, same
|
|
407
|
-
* as the instance `verify()`. */
|
|
408
|
-
static verifyBundle(bundle, opts) {
|
|
409
|
-
if (bundle.version !== 1) {
|
|
410
|
-
return { ok: false, total: bundle.entry_count, verified: 0, reason: `Unknown bundle version ${bundle.version}` };
|
|
411
|
-
}
|
|
412
|
-
let expectedPrev = bundle.genesis_prev_hash;
|
|
413
|
-
let verified = 0;
|
|
414
|
-
for (const row of bundle.entries) {
|
|
415
|
-
if (row.prev_hash !== expectedPrev) {
|
|
416
|
-
return {
|
|
417
|
-
ok: false, total: bundle.entries.length, verified,
|
|
418
|
-
first_invalid_id: row.id,
|
|
419
|
-
reason: `prev_hash mismatch: expected ${expectedPrev}, got ${row.prev_hash}`,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
const recomputed = createHash('sha256')
|
|
423
|
-
.update(`${row.prev_hash}|${row.event_kind}|${row.payload_json}|${row.logged_at}`)
|
|
424
|
-
.digest('hex');
|
|
425
|
-
if (recomputed !== row.row_hash) {
|
|
426
|
-
return {
|
|
427
|
-
ok: false, total: bundle.entries.length, verified,
|
|
428
|
-
first_invalid_id: row.id,
|
|
429
|
-
reason: `row_hash mismatch at id ${row.id}`,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
expectedPrev = row.row_hash;
|
|
433
|
-
verified++;
|
|
434
|
-
}
|
|
435
|
-
if (bundle.entries.length > 0 && bundle.final_row_hash !== expectedPrev) {
|
|
436
|
-
return {
|
|
437
|
-
ok: false, total: bundle.entries.length, verified,
|
|
438
|
-
reason: `final_row_hash mismatch`,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
const s = summarizeAttribution(bundle.entries, opts?.publicKeys);
|
|
442
|
-
return {
|
|
443
|
-
ok: s.sigOk,
|
|
444
|
-
total: bundle.entries.length,
|
|
445
|
-
verified,
|
|
446
|
-
...(s.sigOk ? {} : { reason: s.sigReason }),
|
|
447
|
-
payload_versions: s.payload_versions,
|
|
448
|
-
attribution: s.attribution,
|
|
449
|
-
...(s.signatures ? { signatures: s.signatures } : {}),
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
/** Slim query for recent entries — UI / dashboards. */
|
|
453
|
-
recent(limit = 100, kind) {
|
|
454
|
-
if (kind) {
|
|
455
|
-
return this.db.prepare('SELECT * FROM audit_log WHERE event_kind = ? ORDER BY id DESC LIMIT ?').all(kind, limit);
|
|
456
|
-
}
|
|
457
|
-
return this.db.prepare('SELECT * FROM audit_log ORDER BY id DESC LIMIT ?').all(limit);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
//# sourceMappingURL=audit.js.map
|
|
5
|
+
`).run(e.event_kind,e.actor??null,e.project_id??null,t,a,f,_,o,i,n).lastInsertRowid});return this.get(d.immediate())}get(e){return this.db.prepare("SELECT * FROM audit_log WHERE id = ?").get(e)??null}verify(e){const t=this.db.prepare("SELECT * FROM audit_log ORDER BY id ASC").all();if(t.length===0)return{ok:!0,total:0,verified:0,payload_versions:{v1:0,v2:0,unsigned:0},attribution:{agents:[],runs:[],keys:[]},...e?.publicKeys?.length?{signatures:{checked:0,valid:0,invalid:0}}:{}};let o=h;for(const i of t){if(i.prev_hash!==o)return{ok:!1,total:t.length,verified:i.id-1,first_invalid_id:i.id,reason:`prev_hash mismatch: expected ${o}, got ${i.prev_hash}`};const n=g("sha256").update(`${i.prev_hash}|${i.event_kind}|${i.payload_json}|${i.logged_at}`).digest("hex");if(n!==i.row_hash)return{ok:!1,total:t.length,verified:i.id-1,first_invalid_id:i.id,reason:`row_hash mismatch at id ${i.id}: recomputed ${n}, stored ${i.row_hash}`};o=i.row_hash}const r=S(t,e?.publicKeys);return{ok:r.sigOk,total:t.length,verified:t.length,...r.sigOk?{}:{reason:r.sigReason},payload_versions:r.payload_versions,attribution:r.attribution,...r.signatures?{signatures:r.signatures}:{}}}export(e){const t=[];let o="";e.range_start&&(o+=" AND logged_at >= ?",t.push(e.range_start)),e.range_end&&(o+=" AND logged_at <= ?",t.push(e.range_end));const r=this.db.prepare(`SELECT * FROM audit_log WHERE 1=1 ${o} ORDER BY id ASC`).all(...t);return{version:1,range:{start:e.range_start??r[0]?.logged_at??new Date(0).toISOString(),end:e.range_end??r[r.length-1]?.logged_at??new Date().toISOString()},exported_at:new Date().toISOString(),entry_count:r.length,first_id:r[0]?.id??null,last_id:r[r.length-1]?.id??null,genesis_prev_hash:r[0]?.prev_hash??h,final_row_hash:r[r.length-1]?.row_hash??h,entries:r}}static verifyBundle(e,t){if(e.version!==1)return{ok:!1,total:e.entry_count,verified:0,reason:`Unknown bundle version ${e.version}`};let o=e.genesis_prev_hash,r=0;for(const n of e.entries){if(n.prev_hash!==o)return{ok:!1,total:e.entries.length,verified:r,first_invalid_id:n.id,reason:`prev_hash mismatch: expected ${o}, got ${n.prev_hash}`};if(g("sha256").update(`${n.prev_hash}|${n.event_kind}|${n.payload_json}|${n.logged_at}`).digest("hex")!==n.row_hash)return{ok:!1,total:e.entries.length,verified:r,first_invalid_id:n.id,reason:`row_hash mismatch at id ${n.id}`};o=n.row_hash,r++}if(e.entries.length>0&&e.final_row_hash!==o)return{ok:!1,total:e.entries.length,verified:r,reason:"final_row_hash mismatch"};const i=S(e.entries,t?.publicKeys);return{ok:i.sigOk,total:e.entries.length,verified:r,...i.sigOk?{}:{reason:i.sigReason},payload_versions:i.payload_versions,attribution:i.attribution,...i.signatures?{signatures:i.signatures}:{}}}recent(e=100,t){return t?this.db.prepare("SELECT * FROM audit_log WHERE event_kind = ? ORDER BY id DESC LIMIT ?").all(t,e):this.db.prepare("SELECT * FROM audit_log ORDER BY id DESC LIMIT ?").all(e)}}export{p as AUDIT_SIG_V2_PREFIX,C as AuditLog,E as auditKeyFingerprint,v as buildSignedPayload,A as createAuditSigner,Y as loadAuditSignerFromEnv,m as parseAuditSignature,D as signAuditEntry,T as verifyAuditEntrySignature};
|
package/dist/auto-capture.js
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Auto-extraction (bet #2) — turn freeform text (a session, notes, a transcript)
|
|
3
|
-
* into candidate memories that land in the REVIEW QUEUE for operator approval.
|
|
4
|
-
*
|
|
5
|
-
* Closes the "just talk, it remembers" gap WITHOUT an expensive cloud LLM, via a
|
|
6
|
-
* pluggable LOCAL extractor:
|
|
7
|
-
* - LLM: any Ollama model (`WYRM_EXTRACT_MODEL`) — the slot the future DragonSpark
|
|
8
|
-
* nano-LLM drops into. Structured JSON out, robustly parsed.
|
|
9
|
-
* - Deterministic fallback: sentence segmentation + signal markers + classifyCapture,
|
|
10
|
-
* used when no model is configured or the LLM is unreachable.
|
|
11
|
-
* Always produces candidates, never blocks a write, never throws. Candidates are
|
|
12
|
-
* stored with needs_review=1 so the operator vets them (wyrm_review) — nothing
|
|
13
|
-
* auto-trusted, matching Wyrm's distillation-queue discipline.
|
|
14
|
-
*
|
|
15
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
16
|
-
* @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
|
|
17
|
-
*/
|
|
18
|
-
import { classifyCapture } from './capture.js';
|
|
19
|
-
const KINDS = ['truth', 'failure', 'decision', 'pattern', 'lesson'];
|
|
20
|
-
const EXTRACT_PROMPT = (text) => `You extract DURABLE, REUSABLE memories from a developer's working notes/transcript.
|
|
1
|
+
import{classifyCapture as l}from"./capture.js";const d=["truth","failure","decision","pattern","lesson"],p=t=>`You extract DURABLE, REUSABLE memories from a developer's working notes/transcript.
|
|
21
2
|
Output ONLY a JSON array, no prose. Each item: {"kind": one of ["truth","failure","decision","pattern","lesson"], "text": "<one self-contained sentence>"}.
|
|
22
3
|
- truth: a validated fact/constraint ("X uses Y", "Z is required").
|
|
23
4
|
- failure: an approach that FAILED and why (so it is not repeated).
|
|
@@ -28,122 +9,6 @@ Skip chit-chat, questions, and ephemeral status. Extract 0-8 items; fewer + high
|
|
|
28
9
|
|
|
29
10
|
NOTES:
|
|
30
11
|
"""
|
|
31
|
-
${
|
|
12
|
+
${t.slice(0,8e3)}
|
|
32
13
|
"""
|
|
33
|
-
JSON:`;
|
|
34
|
-
/** Robustly pull a candidate array out of an LLM response (small models wrap JSON in prose/markdown). */
|
|
35
|
-
export function parseCandidates(raw) {
|
|
36
|
-
const m = raw.match(/\[[\s\S]*\]/);
|
|
37
|
-
if (!m)
|
|
38
|
-
return [];
|
|
39
|
-
let arr;
|
|
40
|
-
try {
|
|
41
|
-
arr = JSON.parse(m[0]);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return [];
|
|
45
|
-
}
|
|
46
|
-
if (!Array.isArray(arr))
|
|
47
|
-
return [];
|
|
48
|
-
const out = [];
|
|
49
|
-
const seen = new Set();
|
|
50
|
-
for (const it of arr) {
|
|
51
|
-
if (!it || typeof it !== 'object')
|
|
52
|
-
continue;
|
|
53
|
-
const kind = it.kind;
|
|
54
|
-
const text = it.text;
|
|
55
|
-
if (typeof text !== 'string' || text.trim().length < 8)
|
|
56
|
-
continue;
|
|
57
|
-
const t = text.trim().slice(0, 1000);
|
|
58
|
-
const key = t.toLowerCase().replace(/\s+/g, ' ').slice(0, 80);
|
|
59
|
-
if (seen.has(key))
|
|
60
|
-
continue;
|
|
61
|
-
seen.add(key);
|
|
62
|
-
out.push({ kind: KINDS.includes(kind) ? kind : 'pattern', text: t, confidence: 0.6 });
|
|
63
|
-
if (out.length >= 12)
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
async function extractViaLLM(text, model, url) {
|
|
69
|
-
try {
|
|
70
|
-
const res = await fetch(`${url.replace(/\/$/, '')}/api/generate`, {
|
|
71
|
-
method: 'POST',
|
|
72
|
-
headers: { 'Content-Type': 'application/json' },
|
|
73
|
-
body: JSON.stringify({ model, prompt: EXTRACT_PROMPT(text), stream: false, options: { temperature: 0 } }),
|
|
74
|
-
signal: AbortSignal.timeout(120_000),
|
|
75
|
-
});
|
|
76
|
-
if (!res.ok)
|
|
77
|
-
return null;
|
|
78
|
-
const data = await res.json();
|
|
79
|
-
return data.response ? parseCandidates(data.response) : null;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/** No-LLM extractor: segment into statements, keep durable-looking ones, classify. */
|
|
86
|
-
export function extractDeterministic(text) {
|
|
87
|
-
const segs = text.split(/(?<=[.!?])\s+|\n+/).map((s) => s.trim()).filter((s) => s.length >= 20 && s.length <= 400);
|
|
88
|
-
const out = [];
|
|
89
|
-
const seen = new Set();
|
|
90
|
-
for (const s of segs) {
|
|
91
|
-
if (!/\b(use[sd]?|decided|because|fail(s|ed)?|error|always|never|must|should|learned|lesson|turned out|prefer|avoid|root cause|fixed|broke|requires?)\b/i.test(s))
|
|
92
|
-
continue;
|
|
93
|
-
const key = s.toLowerCase().replace(/\s+/g, ' ').slice(0, 80);
|
|
94
|
-
if (seen.has(key))
|
|
95
|
-
continue;
|
|
96
|
-
seen.add(key);
|
|
97
|
-
const c = classifyCapture(s);
|
|
98
|
-
const kind = /\b(fail(s|ed)?|error|broke|bug|mistake)\b/i.test(s) ? 'failure'
|
|
99
|
-
: c.type === 'truth' ? 'truth'
|
|
100
|
-
: /\bbecause\b/i.test(s) ? 'decision'
|
|
101
|
-
: c.subtype === 'lesson' ? 'lesson'
|
|
102
|
-
: 'pattern';
|
|
103
|
-
out.push({ kind, text: s, confidence: (c.confidence ?? 60) / 100 });
|
|
104
|
-
if (out.length >= 8)
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
return out;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Extract candidate memories from text. Tries the configured local LLM first
|
|
111
|
-
* (if any), falls back to deterministic. Never throws.
|
|
112
|
-
*/
|
|
113
|
-
export async function extractCandidates(text, opts = {}) {
|
|
114
|
-
const url = opts.ollamaUrl || process.env.WYRM_OLLAMA_URL || 'http://localhost:11434';
|
|
115
|
-
const model = opts.model || process.env.WYRM_EXTRACT_MODEL || '';
|
|
116
|
-
if (model) {
|
|
117
|
-
const llm = await extractViaLLM(text, model, url);
|
|
118
|
-
if (llm && llm.length)
|
|
119
|
-
return { candidates: llm, method: 'llm', model };
|
|
120
|
-
}
|
|
121
|
-
return { candidates: extractDeterministic(text), method: 'deterministic' };
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Escape SQL LIKE metacharacters (`%`, `_`, and the escape char itself) so a
|
|
125
|
-
* CONTENT-DERIVED dedup signature can be bound into `LIKE ? ESCAPE '\'`
|
|
126
|
-
* without acting as a wildcard (F3 security pass #1, confirmed finding: the
|
|
127
|
-
* ax:/harvest sig is derived from caller text, so a learning containing `%`
|
|
128
|
-
* previously turned the dedup probe into a broad-match pattern that silently
|
|
129
|
-
* suppressed legitimate capture — counted as `skipped`, never surfaced).
|
|
130
|
-
* Every sig-dedup probe (run debrief, wyrm_auto_capture, harvest MCP + CLI)
|
|
131
|
-
* routes through this one helper.
|
|
132
|
-
*/
|
|
133
|
-
export function escapeLikePattern(s) {
|
|
134
|
-
return s.replace(/[\\%_]/g, (ch) => '\\' + ch);
|
|
135
|
-
}
|
|
136
|
-
/** Map a candidate to its review-queue memory-artifact shape (needs_review=1). */
|
|
137
|
-
export function candidateToArtifact(c) {
|
|
138
|
-
const map = {
|
|
139
|
-
failure: 'anti_pattern',
|
|
140
|
-
decision: 'reasoning_trace',
|
|
141
|
-
lesson: 'lesson',
|
|
142
|
-
pattern: 'pattern',
|
|
143
|
-
truth: 'heuristic',
|
|
144
|
-
};
|
|
145
|
-
// A stable signature for dedup against re-extraction of the same text.
|
|
146
|
-
const sig = 'ax:' + c.text.toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 64);
|
|
147
|
-
return { kind: map[c.kind], problem: c.text, tags: ['auto-extract', `intent:${c.kind}`, sig], confidence: c.confidence };
|
|
148
|
-
}
|
|
149
|
-
//# sourceMappingURL=auto-capture.js.map
|
|
14
|
+
JSON:`;function f(t){const n=t.match(/\[[\s\S]*\]/);if(!n)return[];let r;try{r=JSON.parse(n[0])}catch{return[]}if(!Array.isArray(r))return[];const s=[],e=new Set;for(const a of r){if(!a||typeof a!="object")continue;const o=a.kind,i=a.text;if(typeof i!="string"||i.trim().length<8)continue;const c=i.trim().slice(0,1e3),u=c.toLowerCase().replace(/\s+/g," ").slice(0,80);if(!e.has(u)&&(e.add(u),s.push({kind:d.includes(o)?o:"pattern",text:c,confidence:.6}),s.length>=12))break}return s}async function h(t,n,r){try{const s=await fetch(`${r.replace(/\/$/,"")}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:n,prompt:p(t),stream:!1,options:{temperature:0}}),signal:AbortSignal.timeout(12e4)});if(!s.ok)return null;const e=await s.json();return e.response?f(e.response):null}catch{return null}}function m(t){const n=t.split(/(?<=[.!?])\s+|\n+/).map(e=>e.trim()).filter(e=>e.length>=20&&e.length<=400),r=[],s=new Set;for(const e of n){if(!/\b(use[sd]?|decided|because|fail(s|ed)?|error|always|never|must|should|learned|lesson|turned out|prefer|avoid|root cause|fixed|broke|requires?)\b/i.test(e))continue;const a=e.toLowerCase().replace(/\s+/g," ").slice(0,80);if(s.has(a))continue;s.add(a);const o=l(e),i=/\b(fail(s|ed)?|error|broke|bug|mistake)\b/i.test(e)?"failure":o.type==="truth"?"truth":/\bbecause\b/i.test(e)?"decision":o.subtype==="lesson"?"lesson":"pattern";if(r.push({kind:i,text:e,confidence:(o.confidence??60)/100}),r.length>=8)break}return r}async function b(t,n={}){const r=n.ollamaUrl||process.env.WYRM_OLLAMA_URL||"http://localhost:11434",s=n.model||process.env.WYRM_EXTRACT_MODEL||"";if(s){const e=await h(t,s,r);if(e&&e.length)return{candidates:e,method:"llm",model:s}}return{candidates:m(t),method:"deterministic"}}function k(t){return t.replace(/[\\%_]/g,n=>"\\"+n)}function y(t){const n={failure:"anti_pattern",decision:"reasoning_trace",lesson:"lesson",pattern:"pattern",truth:"heuristic"},r="ax:"+t.text.toLowerCase().replace(/\s+/g," ").trim().slice(0,64);return{kind:n[t.kind],problem:t.text,tags:["auto-extract",`intent:${t.kind}`,r],confidence:t.confidence}}export{y as candidateToArtifact,k as escapeLikePattern,b as extractCandidates,m as extractDeterministic,f as parseCandidates};
|