wyrm-mcp 7.3.2 → 7.3.3
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/README.md +8 -14
- package/dist/activation.js +59 -1
- package/dist/agent-daemon.js +281 -4
- package/dist/agent-loop.js +332 -7
- package/dist/analytics.js +236 -13
- package/dist/attribution.js +49 -1
- package/dist/audit.js +457 -2
- package/dist/auto-capture.js +138 -3
- package/dist/auto-orchestrator.js +325 -1
- package/dist/autoconfig.d.ts +50 -0
- package/dist/autoconfig.d.ts.map +1 -1
- package/dist/autoconfig.js +1115 -39
- package/dist/autoconfig.js.map +1 -1
- package/dist/buddy-runner.js +109 -1
- package/dist/buddy.js +564 -14
- package/dist/build-flags.js +15 -1
- package/dist/capabilities.js +183 -3
- package/dist/capture.js +56 -1
- package/dist/causality.js +148 -8
- package/dist/cli.js +281 -20
- package/dist/cloud/cli.js +541 -5
- package/dist/cloud/client.js +221 -1
- package/dist/cloud/crypto.js +85 -1
- package/dist/cloud/machine-id.js +113 -2
- package/dist/cloud/recovery.js +60 -1
- package/dist/cloud/sync-engine.js +543 -7
- package/dist/cloud-backup.js +579 -5
- package/dist/cloud-profile.js +138 -1
- package/dist/cloud-sync-entrypoint.js +47 -1
- package/dist/cloud-sync.js +309 -2
- package/dist/connectors/bridge-source.d.ts +46 -0
- package/dist/connectors/bridge-source.d.ts.map +1 -0
- package/dist/connectors/bridge-source.js +77 -0
- package/dist/connectors/bridge-source.js.map +1 -0
- package/dist/connectors/index.d.ts +24 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +69 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/ingest.d.ts +16 -0
- package/dist/connectors/ingest.d.ts.map +1 -0
- package/dist/connectors/ingest.js +116 -0
- package/dist/connectors/ingest.js.map +1 -0
- package/dist/connectors/types.d.ts +99 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +17 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/constellation.js +168 -12
- package/dist/content-signature.js +45 -1
- package/dist/context-build-budgeted.js +144 -4
- package/dist/context-ranking.js +69 -1
- package/dist/crypto.js +179 -1
- package/dist/daemon-write-endpoint.js +290 -1
- package/dist/daemon-writer.js +406 -2
- package/dist/database.js +1278 -53
- package/dist/deprecations.js +162 -2
- package/dist/design.js +141 -13
- package/dist/event-replication.js +112 -1
- package/dist/events-sse.js +43 -7
- package/dist/events.js +238 -6
- package/dist/failure-patterns.d.ts +107 -0
- package/dist/failure-patterns.d.ts.map +1 -1
- package/dist/failure-patterns.js +924 -43
- package/dist/failure-patterns.js.map +1 -1
- package/dist/federation.js +236 -12
- package/dist/goals.js +101 -13
- package/dist/golden.js +355 -3
- package/dist/handlers/agent.js +165 -4
- package/dist/handlers/alias-adapters.js +129 -1
- package/dist/handlers/aliases.js +171 -1
- package/dist/handlers/audit.js +87 -1
- package/dist/handlers/boundary.js +221 -1
- package/dist/handlers/capture.js +1114 -73
- package/dist/handlers/causality.js +119 -9
- package/dist/handlers/cloud.js +382 -85
- package/dist/handlers/companion.js +459 -28
- package/dist/handlers/datalake.js +187 -7
- package/dist/handlers/dispatch-context.js +22 -0
- package/dist/handlers/entity.js +256 -25
- package/dist/handlers/events.js +335 -16
- package/dist/handlers/failure.d.ts.map +1 -1
- package/dist/handlers/failure.js +408 -13
- package/dist/handlers/failure.js.map +1 -1
- package/dist/handlers/goals.js +296 -4
- package/dist/handlers/intelligence.js +681 -126
- package/dist/handlers/invoicing.js +70 -1
- package/dist/handlers/mcpclient.js +137 -6
- package/dist/handlers/orchestration.js +125 -40
- package/dist/handlers/output-schemas.js +24 -1
- package/dist/handlers/presence.js +99 -3
- package/dist/handlers/project.js +182 -28
- package/dist/handlers/prompts.js +157 -6
- package/dist/handlers/quest.js +224 -4
- package/dist/handlers/recall.js +237 -13
- package/dist/handlers/registry.js +167 -1
- package/dist/handlers/resources.js +288 -1
- package/dist/handlers/review.js +74 -11
- package/dist/handlers/run.js +498 -16
- package/dist/handlers/search.js +338 -15
- package/dist/handlers/session.js +643 -31
- package/dist/handlers/share.js +184 -8
- package/dist/handlers/shims.js +464 -1
- package/dist/handlers/skill.js +449 -67
- package/dist/handlers/survivors.js +120 -1
- package/dist/handlers/symbols.js +109 -8
- package/dist/handlers/syncops.js +302 -4
- package/dist/handlers/types.js +27 -1
- package/dist/harvest.js +191 -5
- package/dist/hours.js +156 -7
- package/dist/http-auth.js +321 -3
- package/dist/http-fast.js +1302 -22
- package/dist/icons.js +47 -1
- package/dist/importers.js +268 -1
- package/dist/index.js +840 -2
- package/dist/indexer.js +145 -4
- package/dist/intelligence.js +261 -31
- package/dist/internal-dispatch.js +212 -3
- package/dist/keyset.js +110 -1
- package/dist/knowledge-graph.js +176 -12
- package/dist/license.js +441 -2
- package/dist/logger.js +199 -2
- package/dist/maintenance.js +148 -2
- package/dist/mcp-client.js +262 -6
- package/dist/memory-artifacts.js +596 -32
- package/dist/migrate-prompt.js +124 -2
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +799 -42
- package/dist/migrations.js.map +1 -1
- package/dist/performance.js +228 -1
- package/dist/presence.js +140 -11
- package/dist/priority-embed.js +164 -5
- package/dist/providers/embedding-provider.js +196 -1
- package/dist/readonly-gate.js +29 -1
- package/dist/receipt.js +43 -1
- package/dist/rehydration.js +157 -9
- package/dist/reindex.js +88 -1
- package/dist/render-target.js +544 -21
- package/dist/render.js +280 -4
- package/dist/repl-guard.js +173 -1
- package/dist/replication-daemon-entrypoint.js +31 -1
- package/dist/replication-daemon.js +262 -2
- package/dist/rerank.js +142 -1
- package/dist/resilience.js +591 -1
- package/dist/reverse-bridge.js +360 -5
- package/dist/security.js +244 -1
- package/dist/session-seen.js +51 -3
- package/dist/setup.js +260 -1
- package/dist/skill-author.js +168 -5
- package/dist/spec-kit.js +191 -1
- package/dist/sqlite-busy.js +154 -1
- package/dist/statusline.js +315 -11
- package/dist/sub-agent.js +262 -13
- package/dist/summarizer.js +139 -13
- package/dist/symbols.js +283 -7
- package/dist/sync.js +359 -5
- package/dist/tasks-dispatch.js +84 -1
- package/dist/tasks.js +282 -1
- package/dist/token-budget.js +143 -1
- package/dist/tool-analytics.js +129 -7
- package/dist/tool-annotations.js +365 -1
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +75 -1
- package/dist/trace-harvest.js +244 -6
- package/dist/types.js +30 -1
- package/dist/ui-dashboard.js +50 -41
- package/dist/ulid.js +81 -1
- package/dist/usage-tracker.js +66 -1
- package/dist/validate.js +129 -1
- package/dist/vault.js +534 -1
- package/dist/vector-init.js +67 -1
- package/dist/vectors.js +184 -3
- package/dist/version-check.js +136 -4
- package/dist/visibility.js +155 -19
- package/dist/wyrm-cli.js +2845 -101
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.d.ts.map +1 -1
- package/dist/wyrm-guard.js +475 -14
- package/dist/wyrm-guard.js.map +1 -1
- package/dist/wyrm-loop.js +150 -3
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +11 -1
- package/dist/wyrm-statusline.js +56 -4
- package/dist/wyrm-ui.js +77 -9
- package/package.json +1 -1
package/dist/golden.js
CHANGED
|
@@ -1,3 +1,355 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Golden-transcript capture — Wyrm 7.0 BROOD, Phase F1 (T003b).
|
|
3
|
+
*
|
|
4
|
+
* Opt-in (`WYRM_GOLDEN_CAPTURE=1`, default OFF — Article VII) full-fidelity
|
|
5
|
+
* request/response JSONL capture at the single CallTool dispatch choke point
|
|
6
|
+
* (src/index.ts), covering BOTH dispatch families (handlerRegistry + the
|
|
7
|
+
* switch) plus the cache-hit early return and both catch returns. This is the
|
|
8
|
+
* fidelity `tool_call_log` deliberately lacks (its args_summary is truncated +
|
|
9
|
+
* value-stripped by design — frequency/coverage ranking only, NOT replayable).
|
|
10
|
+
* The corpus feeds the T021 alias-spine replay gate.
|
|
11
|
+
*
|
|
12
|
+
* Design spec: specs/wyrm-v7-brood/research/golden-capture-spec.md
|
|
13
|
+
*
|
|
14
|
+
* Contract (the `emitEvent` contract — src/events.ts):
|
|
15
|
+
* - NEVER throws and never blocks the tool path; entire body try/catch,
|
|
16
|
+
* warn to stderr only (stdout is the MCP transport).
|
|
17
|
+
* - Circuit breaker: 5 consecutive write failures disable capture for the
|
|
18
|
+
* process lifetime with a single warning (no warn-per-call storm on a
|
|
19
|
+
* full disk).
|
|
20
|
+
* - Lazy init: dir creation attempted on first capture, not at startup.
|
|
21
|
+
* - Article III: pure local fs append — no LLM, no network, no new deps.
|
|
22
|
+
* - Secrets redacted BY KEY (recursive, request AND response, on a deep
|
|
23
|
+
* clone — the live result object is still on its way to the client and is
|
|
24
|
+
* never mutated), plus a value-pattern safety net for secrets smuggled
|
|
25
|
+
* through benign fields. Redacted paths are recorded in `redactions[]` so
|
|
26
|
+
* replay can wildcard them.
|
|
27
|
+
*/
|
|
28
|
+
import { appendFileSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
29
|
+
import { homedir } from "os";
|
|
30
|
+
import { join, dirname } from "path";
|
|
31
|
+
import { fileURLToPath } from "url";
|
|
32
|
+
/**
|
|
33
|
+
* Failure warnings go DIRECTLY to stderr (the deprecations.ts pattern) — never
|
|
34
|
+
* through the logger. The header contract ("warn to stderr only") must hold by
|
|
35
|
+
* construction on the stdio CallTool path, independent of any logger console
|
|
36
|
+
* configuration. Failure-isolated: never throws.
|
|
37
|
+
*/
|
|
38
|
+
function warnStderr(message, err) {
|
|
39
|
+
try {
|
|
40
|
+
const detail = err === undefined ? "" : ` (${err instanceof Error ? err.message : String(err)})`;
|
|
41
|
+
process.stderr.write(`[wyrm] ${message}${detail}\n`);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* warnings must never break a tool call */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Redaction rules (golden-capture-spec.md §3)
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
/** Rule 1 — substring match on key names (case-insensitive): always redact. */
|
|
51
|
+
const SUBSTRING_REDACT = [
|
|
52
|
+
"token",
|
|
53
|
+
"secret",
|
|
54
|
+
"password",
|
|
55
|
+
"passwd",
|
|
56
|
+
"authorization",
|
|
57
|
+
"passphrase",
|
|
58
|
+
"bearer",
|
|
59
|
+
"credential",
|
|
60
|
+
"apikey",
|
|
61
|
+
"api_key",
|
|
62
|
+
// wyrm_activate's `license` arg is a signed bearer-style entitlement
|
|
63
|
+
// credential; no VALUE_PATTERN recognizes signed-license JSON, so the key
|
|
64
|
+
// rule must catch it (golden-capture-spec.md §3 amendment).
|
|
65
|
+
"license",
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* Rule 3 — per-tool overrides: keys whose ENTIRE value subtree has every value
|
|
69
|
+
* redacted regardless of key names (arbitrary env names can carry secrets that
|
|
70
|
+
* no key rule catches).
|
|
71
|
+
*/
|
|
72
|
+
const TOOL_REDACT_ALL_VALUES = {
|
|
73
|
+
wyrm_mcp_register: new Set(["env"]),
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Rule 4 — value-pattern safety net on every string value regardless of key
|
|
77
|
+
* name. Catches secrets smuggled through benign fields (value, notes,
|
|
78
|
+
* response text).
|
|
79
|
+
*/
|
|
80
|
+
const VALUE_PATTERNS = [
|
|
81
|
+
{ label: "jwt", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]*/ },
|
|
82
|
+
{ label: "github-token", re: /\bgh[posu]_[A-Za-z0-9]{16,}\b/ },
|
|
83
|
+
{ label: "npm-token", re: /\bnpm_[A-Za-z0-9]{16,}\b/ },
|
|
84
|
+
{ label: "api-key", re: /\b[sp]k-[A-Za-z0-9_-]{16,}\b/ },
|
|
85
|
+
{ label: "aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
86
|
+
{ label: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
87
|
+
{ label: "pem-private-key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
88
|
+
// Generic bearer credentials smuggled under benign keys (plausible inside
|
|
89
|
+
// wyrm_call_external proxied args). The {16,} run keeps prose like
|
|
90
|
+
// "Bearer token authentication" unmatched.
|
|
91
|
+
{ label: "bearer", re: /\bBearer\s+[A-Za-z0-9._~+\/-]{16,}/ },
|
|
92
|
+
];
|
|
93
|
+
/** Operator escape hatch: extra key names (exact, case-insensitive) to redact. */
|
|
94
|
+
function extraRedactKeys() {
|
|
95
|
+
const raw = process.env.WYRM_GOLDEN_REDACT_EXTRA;
|
|
96
|
+
if (!raw)
|
|
97
|
+
return new Set();
|
|
98
|
+
return new Set(raw
|
|
99
|
+
.split(",")
|
|
100
|
+
.map((s) => s.trim().toLowerCase())
|
|
101
|
+
.filter(Boolean));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Should this key's value be redacted outright?
|
|
105
|
+
* - Rule 1: substring match against SUBSTRING_REDACT (+ exact extras).
|
|
106
|
+
* - Rule 2: compound-suffix `*key` (privateKey, ssh_key, access-key,
|
|
107
|
+
* signingKey) — EXCEPT the exact bare name `key`, which in Wyrm is a
|
|
108
|
+
* required benign identifier on 6+ core tools (data store, set_global,
|
|
109
|
+
* references, design tokens — incl. the #3 most-called tool). Redacting it
|
|
110
|
+
* would make those write transcripts unreplayable; the value-pattern net
|
|
111
|
+
* backstops the residual risk. Deliberate, documented deviation
|
|
112
|
+
* (golden-capture-spec.md §6.1).
|
|
113
|
+
*/
|
|
114
|
+
function keyIsSecret(key, extras) {
|
|
115
|
+
const lower = key.toLowerCase();
|
|
116
|
+
if (extras.has(lower))
|
|
117
|
+
return true;
|
|
118
|
+
for (const s of SUBSTRING_REDACT) {
|
|
119
|
+
if (lower.includes(s))
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (lower !== "key" && lower.endsWith("key"))
|
|
123
|
+
return true;
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Deep-clone-and-redact in one pass. Walks the ORIGINAL (never mutates it) and
|
|
128
|
+
* builds the redacted copy. Cycles and non-serializable leaves degrade to
|
|
129
|
+
* `{"$unserializable": true}` — the record still captures rather than being
|
|
130
|
+
* dropped whole (mirrors emitEvent's payload-stringify isolation).
|
|
131
|
+
*/
|
|
132
|
+
function redactWalk(value, path, ctx, redactAll) {
|
|
133
|
+
// Primitives
|
|
134
|
+
if (value === null || value === undefined)
|
|
135
|
+
return value;
|
|
136
|
+
const t = typeof value;
|
|
137
|
+
if (t === "string") {
|
|
138
|
+
if (redactAll) {
|
|
139
|
+
ctx.redactions.push(path);
|
|
140
|
+
return "[REDACTED]";
|
|
141
|
+
}
|
|
142
|
+
for (const { label, re } of VALUE_PATTERNS) {
|
|
143
|
+
if (re.test(value)) {
|
|
144
|
+
ctx.redactions.push(path);
|
|
145
|
+
return `[REDACTED:${label}]`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
if (t === "number" || t === "boolean") {
|
|
151
|
+
if (redactAll) {
|
|
152
|
+
ctx.redactions.push(path);
|
|
153
|
+
return "[REDACTED]";
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
if (t === "bigint" || t === "function" || t === "symbol") {
|
|
158
|
+
return { $unserializable: true };
|
|
159
|
+
}
|
|
160
|
+
// Binary views (Buffer, TypedArray, DataView, ArrayBuffer) must NOT fall
|
|
161
|
+
// through to Object.entries(): a Buffer would serialize as numeric byte
|
|
162
|
+
// values ({"0":115,"1":101,…} — trivially reconstructible) and the
|
|
163
|
+
// string-only VALUE_PATTERNS net would never see the content. Degrade like
|
|
164
|
+
// every other non-serializable leaf (golden-capture-spec.md §3 amendment).
|
|
165
|
+
if (Buffer.isBuffer(value) || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
166
|
+
return { $unserializable: true };
|
|
167
|
+
}
|
|
168
|
+
// Objects / arrays
|
|
169
|
+
const obj = value;
|
|
170
|
+
if (ctx.seen.has(obj))
|
|
171
|
+
return { $unserializable: true };
|
|
172
|
+
ctx.seen.add(obj);
|
|
173
|
+
try {
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
return value.map((item, i) => redactWalk(item, `${path}.${i}`, ctx, redactAll));
|
|
176
|
+
}
|
|
177
|
+
if (value instanceof Date)
|
|
178
|
+
return value.toISOString();
|
|
179
|
+
const out = {};
|
|
180
|
+
const overrides = TOOL_REDACT_ALL_VALUES[ctx.tool];
|
|
181
|
+
for (const [k, v] of Object.entries(value)) {
|
|
182
|
+
const childPath = `${path}.${k}`;
|
|
183
|
+
if (redactAll) {
|
|
184
|
+
// Inside a redact-all subtree: every leaf value is redacted.
|
|
185
|
+
out[k] = redactWalk(v, childPath, ctx, true);
|
|
186
|
+
}
|
|
187
|
+
else if (keyIsSecret(k, ctx.extras)) {
|
|
188
|
+
ctx.redactions.push(childPath);
|
|
189
|
+
out[k] = "[REDACTED]";
|
|
190
|
+
}
|
|
191
|
+
else if (overrides?.has(k)) {
|
|
192
|
+
out[k] = redactWalk(v, childPath, ctx, true);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
out[k] = redactWalk(v, childPath, ctx, false);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
ctx.seen.delete(obj);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Redact a value tree for a given tool, recording redacted paths into
|
|
206
|
+
* `redactions`. Exported for direct unit testing.
|
|
207
|
+
*/
|
|
208
|
+
export function redactForGolden(tool, value, rootPath, redactions) {
|
|
209
|
+
const ctx = { tool, extras: extraRedactKeys(), redactions, seen: new WeakSet() };
|
|
210
|
+
return redactWalk(value, rootPath, ctx, false);
|
|
211
|
+
}
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Capture state (per process)
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
const MAX_CONSECUTIVE_WRITE_FAILURES = 5;
|
|
216
|
+
const state = {
|
|
217
|
+
seq: 0,
|
|
218
|
+
writeFailures: 0,
|
|
219
|
+
disabled: false,
|
|
220
|
+
warned: false,
|
|
221
|
+
};
|
|
222
|
+
const SESSION_ID = `${process.pid}-${Math.floor(Date.now() / 1000)}`;
|
|
223
|
+
let _version = null;
|
|
224
|
+
/** Lazy wyrm-mcp version (same package.json walk as index.ts; cached). */
|
|
225
|
+
function wyrmVersion() {
|
|
226
|
+
if (_version)
|
|
227
|
+
return _version;
|
|
228
|
+
try {
|
|
229
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
230
|
+
for (const p of [join(here, "..", "package.json"), join(here, "..", "..", "package.json")]) {
|
|
231
|
+
if (existsSync(p)) {
|
|
232
|
+
const v = JSON.parse(readFileSync(p, "utf-8")).version;
|
|
233
|
+
if (typeof v === "string") {
|
|
234
|
+
_version = v;
|
|
235
|
+
return v;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
/* fall through */
|
|
242
|
+
}
|
|
243
|
+
_version = "unknown";
|
|
244
|
+
return _version;
|
|
245
|
+
}
|
|
246
|
+
/** Flag check — lazy per call (cheap string compare), default OFF. */
|
|
247
|
+
export function isGoldenCaptureEnabled() {
|
|
248
|
+
const v = process.env.WYRM_GOLDEN_CAPTURE;
|
|
249
|
+
return v === "1" || v === "true";
|
|
250
|
+
}
|
|
251
|
+
function goldenDir() {
|
|
252
|
+
const d = process.env.WYRM_GOLDEN_DIR?.trim();
|
|
253
|
+
return d && d.length > 0 ? d : join(homedir(), ".wyrm", "golden");
|
|
254
|
+
}
|
|
255
|
+
function goldenFilePath(dir) {
|
|
256
|
+
const now = new Date();
|
|
257
|
+
const ymd = `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}${String(now.getUTCDate()).padStart(2, "0")}`;
|
|
258
|
+
return join(dir, `golden-${ymd}-${process.pid}.jsonl`);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Append one pre-serialized line. Failure feeds the circuit breaker — after
|
|
262
|
+
* MAX_CONSECUTIVE_WRITE_FAILURES consecutive failures, capture is disabled for
|
|
263
|
+
* the rest of the process lifetime (warned once).
|
|
264
|
+
*/
|
|
265
|
+
function writeLine(line) {
|
|
266
|
+
try {
|
|
267
|
+
const dir = goldenDir();
|
|
268
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
269
|
+
appendFileSync(goldenFilePath(dir), line, { mode: 0o600 });
|
|
270
|
+
state.writeFailures = 0;
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
state.writeFailures++;
|
|
274
|
+
if (state.writeFailures >= MAX_CONSECUTIVE_WRITE_FAILURES) {
|
|
275
|
+
state.disabled = true;
|
|
276
|
+
if (!state.warned) {
|
|
277
|
+
state.warned = true;
|
|
278
|
+
warnStderr(`golden: capture disabled for this process after ${state.writeFailures} consecutive write failures`, err);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
warnStderr("golden: capture dropped (write failed)", err);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** Derive outcome from a response when the call site didn't pin one. */
|
|
287
|
+
function deriveOutcome(response) {
|
|
288
|
+
const r = response;
|
|
289
|
+
if (r && typeof r === "object" && r.isError) {
|
|
290
|
+
const text = Array.isArray(r.content) ? r.content[0]?.text : undefined;
|
|
291
|
+
if (typeof text === "string" && text.startsWith("Unknown tool:"))
|
|
292
|
+
return "unknown_tool";
|
|
293
|
+
return "soft_error";
|
|
294
|
+
}
|
|
295
|
+
return "ok";
|
|
296
|
+
}
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// The capture entry point — called at all four response-producing sites
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
/**
|
|
301
|
+
* Capture one CallTool round-trip as a JSONL golden record. NEVER throws,
|
|
302
|
+
* never blocks; no-op unless WYRM_GOLDEN_CAPTURE=1 (Article VII: default OFF).
|
|
303
|
+
*/
|
|
304
|
+
export function goldenCapture(input) {
|
|
305
|
+
try {
|
|
306
|
+
if (state.disabled)
|
|
307
|
+
return;
|
|
308
|
+
if (!isGoldenCaptureEnabled())
|
|
309
|
+
return;
|
|
310
|
+
const redactions = [];
|
|
311
|
+
const args = redactForGolden(input.tool, input.args ?? {}, "request.arguments", redactions);
|
|
312
|
+
const meta = input.meta !== undefined ? redactForGolden(input.tool, input.meta, "request._meta", redactions) : undefined;
|
|
313
|
+
const response = redactForGolden(input.tool, input.response, "response", redactions);
|
|
314
|
+
const record = {
|
|
315
|
+
v: 1,
|
|
316
|
+
ts: new Date().toISOString(),
|
|
317
|
+
seq: ++state.seq,
|
|
318
|
+
session: SESSION_ID,
|
|
319
|
+
wyrm_version: wyrmVersion(),
|
|
320
|
+
tool: input.tool,
|
|
321
|
+
dispatch: input.dispatch,
|
|
322
|
+
request: {
|
|
323
|
+
name: input.tool,
|
|
324
|
+
arguments: args,
|
|
325
|
+
...(meta !== undefined ? { _meta: meta } : {}),
|
|
326
|
+
},
|
|
327
|
+
response,
|
|
328
|
+
outcome: input.outcome ?? deriveOutcome(input.response),
|
|
329
|
+
cached: input.cached === true,
|
|
330
|
+
ms: input.ms,
|
|
331
|
+
redactions,
|
|
332
|
+
};
|
|
333
|
+
writeLine(JSON.stringify(record) + "\n");
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
// log-and-drop: the tool result is already on its way to the client; the
|
|
337
|
+
// golden record is best-effort (the emitEvent contract).
|
|
338
|
+
warnStderr("golden: capture dropped", err);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Test hooks (additive; no production caller)
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
/** Reset breaker/sequence state between tests. */
|
|
345
|
+
export function __resetGoldenForTests() {
|
|
346
|
+
state.seq = 0;
|
|
347
|
+
state.writeFailures = 0;
|
|
348
|
+
state.disabled = false;
|
|
349
|
+
state.warned = false;
|
|
350
|
+
}
|
|
351
|
+
/** Read-only view of the capture state for tests. */
|
|
352
|
+
export function __goldenStateForTests() {
|
|
353
|
+
return { ...state };
|
|
354
|
+
}
|
|
355
|
+
//# sourceMappingURL=golden.js.map
|
package/dist/handlers/agent.js
CHANGED
|
@@ -1,4 +1,165 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
3
|
+
* @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
|
|
4
|
+
*/
|
|
5
|
+
import { TOOL_ANNOTATIONS } from "../tool-annotations.js";
|
|
6
|
+
export const agentToolSpecs = [
|
|
7
|
+
{
|
|
8
|
+
name: "wyrm_agent_init",
|
|
9
|
+
description: "Bootstrap Wyrm's autonomous agent loop in one call. Starts the wyrm-loop daemon as a detached background process so subsequent ticks happen automatically every interval_seconds. Optionally seeds a first goal in the same call. Idempotent — if already running, returns status without spawning a second instance.",
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
interval_seconds: { type: "number", description: "Seconds between OODA ticks (default 600 = 10 min, min 10)" },
|
|
14
|
+
max_steps: { type: "number", description: "Max iterations per goal per tick (default 3, max 20)" },
|
|
15
|
+
project_path: { type: "string", description: "Scope the daemon to a project root (optional)" },
|
|
16
|
+
verbose: { type: "boolean", description: "Log every tick to ~/.wyrm/wyrm-loop.log" },
|
|
17
|
+
seed_goal: {
|
|
18
|
+
type: "object",
|
|
19
|
+
description: "Optional first goal to set in the same call",
|
|
20
|
+
properties: {
|
|
21
|
+
title: { type: "string" },
|
|
22
|
+
description: { type: "string" },
|
|
23
|
+
success_criteria: { type: "string" },
|
|
24
|
+
priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
25
|
+
deadline: { type: "string" },
|
|
26
|
+
max_iterations: { type: "number" },
|
|
27
|
+
},
|
|
28
|
+
required: ["title"],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
annotations: TOOL_ANNOTATIONS["wyrm_agent_init"],
|
|
33
|
+
aliases: [],
|
|
34
|
+
handler: async (args, ctx) => {
|
|
35
|
+
const { agentDaemon, db, goals } = ctx;
|
|
36
|
+
const a = args;
|
|
37
|
+
const interval = Math.max(10, Math.min(a.interval_seconds ?? 600, 86400));
|
|
38
|
+
const r = agentDaemon.start({
|
|
39
|
+
interval_seconds: interval,
|
|
40
|
+
max_steps: a.max_steps,
|
|
41
|
+
project_path: a.project_path,
|
|
42
|
+
verbose: a.verbose ?? false,
|
|
43
|
+
});
|
|
44
|
+
if (!r.ok) {
|
|
45
|
+
return { content: [{ type: "text", text: ` Agent init failed: ${r.error}` }], isError: true };
|
|
46
|
+
}
|
|
47
|
+
let seedNote = '';
|
|
48
|
+
if (a.seed_goal) {
|
|
49
|
+
const proj = a.project_path ? db.getProject(a.project_path) : null;
|
|
50
|
+
const goal = goals.set({
|
|
51
|
+
project_id: proj?.id ?? null,
|
|
52
|
+
title: a.seed_goal.title,
|
|
53
|
+
description: a.seed_goal.description,
|
|
54
|
+
success_criteria: a.seed_goal.success_criteria,
|
|
55
|
+
deadline: a.seed_goal.deadline,
|
|
56
|
+
priority: a.seed_goal.priority,
|
|
57
|
+
max_iterations: a.seed_goal.max_iterations,
|
|
58
|
+
});
|
|
59
|
+
seedNote = `\n Seeded goal #${goal.id}: ${goal.title}`;
|
|
60
|
+
}
|
|
61
|
+
const status = r.status;
|
|
62
|
+
const wasAlready = status.pid != null && status.pid !== r.pid;
|
|
63
|
+
const text = [
|
|
64
|
+
` Agent ${wasAlready ? 'already running' : 'started'} — pid ${status.pid}`,
|
|
65
|
+
` Interval: ${interval}s · Active goals: ${status.active_goals} · Total iterations: ${status.total_iterations}`,
|
|
66
|
+
` Log: ${status.log_file}` + seedNote,
|
|
67
|
+
].join('\n');
|
|
68
|
+
return { content: [{ type: "text", text }] };
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "wyrm_agent_status",
|
|
73
|
+
description: "Inspect the autonomous agent: is the wyrm-loop daemon running, what was the last action, how many active goals + total iterations. Use to confirm bootstrap worked.",
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
include_log: { type: "boolean", description: "Include last ~40 lines of the daemon log" },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
annotations: TOOL_ANNOTATIONS["wyrm_agent_status"],
|
|
81
|
+
aliases: [],
|
|
82
|
+
handler: async (args, ctx) => {
|
|
83
|
+
const { agentDaemon, goals } = ctx;
|
|
84
|
+
const a = args;
|
|
85
|
+
const status = agentDaemon.status();
|
|
86
|
+
const lines = [];
|
|
87
|
+
if (status.running) {
|
|
88
|
+
lines.push(` Agent RUNNING — pid ${status.pid}${status.started_at ? ` (since ${status.started_at})` : ''}`);
|
|
89
|
+
if (status.uptime_seconds != null) {
|
|
90
|
+
const m = Math.floor(status.uptime_seconds / 60);
|
|
91
|
+
const s = status.uptime_seconds % 60;
|
|
92
|
+
lines.push(` Uptime: ${m}m ${s}s`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
lines.push(` Agent NOT RUNNING. Call wyrm_agent_init to start it.`);
|
|
97
|
+
}
|
|
98
|
+
lines.push(` Active goals: ${status.active_goals} · Total iterations: ${status.total_iterations}`);
|
|
99
|
+
if (status.last_action) {
|
|
100
|
+
lines.push(` Last action (${status.last_action.ran_at}): ${status.last_action.summary} [${status.last_action.result_status ?? '?'}]`);
|
|
101
|
+
}
|
|
102
|
+
if (a.include_log) {
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push('=== Recent log ===');
|
|
105
|
+
lines.push(agentDaemon.recentLog(40));
|
|
106
|
+
}
|
|
107
|
+
return { content: [{ type: "text", text: lines.join('\n') }] };
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "wyrm_agent_stop",
|
|
112
|
+
description: "Stop the autonomous agent daemon (SIGTERM with grace, then SIGKILL). Goals remain in DB and can be resumed by calling wyrm_agent_init again.",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
grace_ms: { type: "number", description: "Wait time before SIGKILL (default 3000, max 30000)" },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
annotations: TOOL_ANNOTATIONS["wyrm_agent_stop"],
|
|
120
|
+
aliases: [],
|
|
121
|
+
handler: async (args, ctx) => {
|
|
122
|
+
const { agentDaemon } = ctx;
|
|
123
|
+
const a = args;
|
|
124
|
+
const r = await agentDaemon.stop({ grace_ms: a.grace_ms });
|
|
125
|
+
if (!r.ok) {
|
|
126
|
+
return { content: [{ type: "text", text: ` Stop failed: ${r.error}` }], isError: true };
|
|
127
|
+
}
|
|
128
|
+
const text = r.was_running
|
|
129
|
+
? ` Agent stopped (was pid ${r.pid}). Goals remain in DB — wyrm_agent_init resumes.`
|
|
130
|
+
: ` Agent was not running. (No-op.)`;
|
|
131
|
+
return { content: [{ type: "text", text }] };
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "wyrm_agent_restart",
|
|
136
|
+
description: "Stop the daemon and start it again with optional new settings. Useful when changing interval_seconds or max_steps without manually stopping first.",
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
interval_seconds: { type: "number" },
|
|
141
|
+
max_steps: { type: "number" },
|
|
142
|
+
project_path: { type: "string" },
|
|
143
|
+
verbose: { type: "boolean" },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
annotations: TOOL_ANNOTATIONS["wyrm_agent_restart"],
|
|
147
|
+
aliases: [],
|
|
148
|
+
handler: async (args, ctx) => {
|
|
149
|
+
const { agentDaemon, goals } = ctx;
|
|
150
|
+
const a = args;
|
|
151
|
+
const r = await agentDaemon.restart({
|
|
152
|
+
interval_seconds: a.interval_seconds,
|
|
153
|
+
max_steps: a.max_steps,
|
|
154
|
+
project_path: a.project_path,
|
|
155
|
+
verbose: a.verbose,
|
|
156
|
+
});
|
|
157
|
+
if (!r.ok) {
|
|
158
|
+
return { content: [{ type: "text", text: ` Restart failed: ${r.error}` }], isError: true };
|
|
159
|
+
}
|
|
160
|
+
const s = r.status;
|
|
161
|
+
return { content: [{ type: "text", text: ` Agent restarted — pid ${s.pid}. Active goals: ${s.active_goals}.` }] };
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
//# sourceMappingURL=agent.js.map
|