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.
Files changed (156) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.d.ts.map +1 -1
  4. package/dist/activation.js +1 -44
  5. package/dist/activation.js.map +1 -1
  6. package/dist/agent-daemon.js +4 -281
  7. package/dist/agent-loop.js +7 -332
  8. package/dist/analytics.js +13 -236
  9. package/dist/attribution.js +1 -49
  10. package/dist/audit.js +2 -457
  11. package/dist/auto-capture.js +3 -138
  12. package/dist/auto-orchestrator.js +1 -325
  13. package/dist/autoconfig.js +39 -840
  14. package/dist/buddy-runner.js +1 -109
  15. package/dist/buddy.js +14 -564
  16. package/dist/build-flags.js +1 -17
  17. package/dist/capabilities.js +3 -183
  18. package/dist/capture.js +1 -56
  19. package/dist/causality.js +6 -107
  20. package/dist/cli.js +20 -281
  21. package/dist/cloud/cli.js +5 -541
  22. package/dist/cloud/client.js +1 -221
  23. package/dist/cloud/crypto.js +1 -85
  24. package/dist/cloud/machine-id.js +2 -113
  25. package/dist/cloud/recovery.js +1 -60
  26. package/dist/cloud/sync-engine.js +7 -543
  27. package/dist/cloud-backup.js +5 -579
  28. package/dist/cloud-profile.js +1 -138
  29. package/dist/cloud-sync-entrypoint.js +1 -47
  30. package/dist/cloud-sync.js +2 -309
  31. package/dist/constellation.js +12 -168
  32. package/dist/context-build-budgeted.js +4 -144
  33. package/dist/context-ranking.js +1 -69
  34. package/dist/crypto.js +1 -179
  35. package/dist/daemon-write-endpoint.js +1 -290
  36. package/dist/daemon-writer.js +2 -406
  37. package/dist/database.js +43 -1110
  38. package/dist/deprecations.js +2 -162
  39. package/dist/design.js +13 -141
  40. package/dist/event-replication.js +1 -112
  41. package/dist/events-sse.js +7 -43
  42. package/dist/events.js +6 -238
  43. package/dist/failure-patterns.js +42 -659
  44. package/dist/federation.js +12 -236
  45. package/dist/goals.js +13 -101
  46. package/dist/golden.js +3 -355
  47. package/dist/handlers/agent.js +4 -165
  48. package/dist/handlers/alias-adapters.js +1 -129
  49. package/dist/handlers/aliases.js +1 -171
  50. package/dist/handlers/audit.js +1 -87
  51. package/dist/handlers/boundary.js +1 -221
  52. package/dist/handlers/capture.js +73 -1109
  53. package/dist/handlers/causality.js +7 -114
  54. package/dist/handlers/cloud.js +85 -382
  55. package/dist/handlers/companion.js +28 -459
  56. package/dist/handlers/datalake.js +7 -187
  57. package/dist/handlers/dispatch-context.js +0 -22
  58. package/dist/handlers/entity.js +25 -256
  59. package/dist/handlers/events.js +16 -335
  60. package/dist/handlers/failure.js +13 -340
  61. package/dist/handlers/goals.js +4 -296
  62. package/dist/handlers/intelligence.js +126 -674
  63. package/dist/handlers/invoicing.js +1 -70
  64. package/dist/handlers/mcpclient.js +6 -137
  65. package/dist/handlers/orchestration.js +40 -125
  66. package/dist/handlers/output-schemas.js +1 -24
  67. package/dist/handlers/presence.js +3 -99
  68. package/dist/handlers/project.js +28 -182
  69. package/dist/handlers/prompts.js +6 -157
  70. package/dist/handlers/quest.js +4 -224
  71. package/dist/handlers/recall.js +11 -218
  72. package/dist/handlers/registry.js +1 -167
  73. package/dist/handlers/resources.js +1 -288
  74. package/dist/handlers/review.js +11 -74
  75. package/dist/handlers/run.js +17 -487
  76. package/dist/handlers/search.js +15 -326
  77. package/dist/handlers/session.js +28 -615
  78. package/dist/handlers/share.js +8 -184
  79. package/dist/handlers/shims.js +1 -464
  80. package/dist/handlers/skill.js +67 -449
  81. package/dist/handlers/survivors.js +1 -120
  82. package/dist/handlers/symbols.js +8 -109
  83. package/dist/handlers/syncops.js +4 -302
  84. package/dist/handlers/types.js +1 -27
  85. package/dist/harvest.js +5 -191
  86. package/dist/hours.js +7 -156
  87. package/dist/http-auth.js +3 -321
  88. package/dist/http-fast.js +21 -1137
  89. package/dist/icons.js +1 -47
  90. package/dist/index.js +2 -924
  91. package/dist/indexer.js +4 -145
  92. package/dist/intelligence.js +31 -261
  93. package/dist/internal-dispatch.js +3 -212
  94. package/dist/keyset.js +1 -110
  95. package/dist/knowledge-graph.js +12 -176
  96. package/dist/license.d.ts +11 -0
  97. package/dist/license.d.ts.map +1 -1
  98. package/dist/license.js +2 -414
  99. package/dist/license.js.map +1 -1
  100. package/dist/logger.js +2 -199
  101. package/dist/maintenance.js +2 -148
  102. package/dist/mcp-client.js +6 -262
  103. package/dist/memory-artifacts.js +30 -449
  104. package/dist/migrate-prompt.js +2 -124
  105. package/dist/migrations.js +40 -655
  106. package/dist/performance.js +1 -228
  107. package/dist/presence.js +11 -140
  108. package/dist/priority-embed.js +5 -164
  109. package/dist/providers/embedding-provider.js +1 -196
  110. package/dist/readonly-gate.js +1 -29
  111. package/dist/rehydration.js +9 -157
  112. package/dist/reindex.js +1 -88
  113. package/dist/render-target.js +21 -514
  114. package/dist/render.js +4 -280
  115. package/dist/repl-guard.js +1 -173
  116. package/dist/replication-daemon-entrypoint.js +1 -31
  117. package/dist/replication-daemon.js +2 -262
  118. package/dist/resilience.js +1 -591
  119. package/dist/reverse-bridge.js +5 -360
  120. package/dist/security.js +1 -244
  121. package/dist/session-seen.js +3 -51
  122. package/dist/setup.js +1 -260
  123. package/dist/skill-author.js +5 -168
  124. package/dist/spec-kit.js +1 -191
  125. package/dist/sqlite-busy.js +1 -154
  126. package/dist/statusline.js +11 -315
  127. package/dist/sub-agent.js +13 -262
  128. package/dist/summarizer.js +13 -139
  129. package/dist/symbols.js +7 -283
  130. package/dist/sync.js +5 -359
  131. package/dist/tasks-dispatch.js +1 -84
  132. package/dist/tasks.js +1 -282
  133. package/dist/token-budget.js +1 -143
  134. package/dist/tool-analytics.js +7 -129
  135. package/dist/tool-annotations.js +1 -365
  136. package/dist/tool-manifest-v2.json +1 -1
  137. package/dist/tool-manifest.json +1 -1
  138. package/dist/tool-profiles.js +1 -75
  139. package/dist/trace-harvest.js +6 -244
  140. package/dist/types.js +1 -30
  141. package/dist/ui-dashboard.js +41 -50
  142. package/dist/ulid.js +1 -81
  143. package/dist/validate.js +1 -129
  144. package/dist/vault.js +1 -534
  145. package/dist/vectors.js +3 -184
  146. package/dist/version-check.js +4 -136
  147. package/dist/visibility.js +19 -155
  148. package/dist/wyrm-cli.js +98 -2451
  149. package/dist/wyrm-cli.js.map +1 -1
  150. package/dist/wyrm-guard.js +14 -424
  151. package/dist/wyrm-loop.js +3 -150
  152. package/dist/wyrm-manifest.json +1 -1
  153. package/dist/wyrm-statusline-daemon.js +1 -11
  154. package/dist/wyrm-statusline.js +4 -56
  155. package/dist/wyrm-ui.js +9 -77
  156. package/package.json +4 -2
package/dist/render.js CHANGED
@@ -1,280 +1,4 @@
1
- /**
2
- * The single dual-emit renderer (v7 F3, T019 — spec FR-3).
3
- *
4
- * MCP responses ride two channels: `content[0].text` (the text wire every
5
- * 6.x client reads) and `structuredContent` (the machine wire, MCP
6
- * 2025-06-18). Before T019 each surface hand-built both and kept them in
7
- * sync by discipline — the WYRM_BUSY body (sqlite-busy.ts, T011) and the
8
- * failure_check verdict (T014) were the precedents. This module makes drift
9
- * IMPOSSIBLE BY CONSTRUCTION: the structured body is the ONLY input, and the
10
- * text is DERIVED from it by a pure template `(body, glyphs) => string` that
11
- * sees nothing else — there is no second source of truth to drift.
12
- *
13
- * Glyph policy (spec FR-3): plain ASCII by default — fleet subagents, CI
14
- * logs, and pipe consumers must never need a Nerd Font — with the Ghost
15
- * Protocol brand glyphs opt-in behind WYRM_FANCY=1. Toggling WYRM_FANCY
16
- * changes ONLY the text channel; `structuredContent` is byte-identical in
17
- * both modes (locked by tests/render-contract.test.ts).
18
- *
19
- * Rollout (spec FR-3, version-pinned): ToolSpec-resident domains render
20
- * through here from T019 on (goals first); switch-resident tools keep their
21
- * 6.x prose verbatim until their domain extracts (T026 hot paths, T036 full
22
- * drain) — the renderer is the path forward, not a big-bang rewrite.
23
- *
24
- * Article III: zero LLM, zero network, zero clock — rendering is a pure
25
- * function of the body and the WYRM_FANCY env read.
26
- *
27
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
28
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
29
- */
30
- import { ICON } from './icons.js';
31
- // ── Glyph sets ──────────────────────────────────────────────────────────────
32
- /** True when the operator opted into brand glyphs (WYRM_FANCY=1|true). Read
33
- * per render call (not cached at module load) so one long-lived server obeys
34
- * a harness-provided env and tests can toggle it deterministically. */
35
- export function isFancy() {
36
- const v = process.env.WYRM_FANCY;
37
- return v === '1' || v === 'true';
38
- }
39
- /** Plain ASCII (the default): decoration collapses to nothing, the prose
40
- * carries the semantics, lists keep a '-' bullet. */
41
- const ASCII_GLYPHS = {
42
- brand: '',
43
- ok: '',
44
- fail: '',
45
- warn: '',
46
- paused: '',
47
- resumed: '',
48
- bullet: '-',
49
- point: '>',
50
- };
51
- /** WYRM_FANCY=1 — the curated icons.ts vocabulary (ICON.brand honours the
52
- * WYRM_BRAND override; the pause/resume marks predate icons.ts and are kept
53
- * for 6.x visual continuity). */
54
- const FANCY_GLYPHS = {
55
- brand: ICON.brand,
56
- ok: ICON.ok,
57
- fail: ICON.blocked,
58
- warn: ICON.failure,
59
- paused: '⏸', // ⏸
60
- resumed: '▶', // ▶
61
- bullet: ICON.bullet,
62
- point: ICON.point,
63
- };
64
- /** The active glyph set for this render call. */
65
- export function glyphs() {
66
- return isFancy() ? FANCY_GLYPHS : ASCII_GLYPHS;
67
- }
68
- /** Prefix `text` with a glyph — or return it untouched when the glyph is
69
- * empty (ASCII mode), so default output never carries stray separators. */
70
- export function withGlyph(glyph, text) {
71
- return glyph ? `${glyph} ${text}` : text;
72
- }
73
- // ── Text-wire control-char floor (F3 security pass #1) ─────────────────────
74
- /** C0 control chars (minus \n 0x0a and \t 0x09) + DEL. ESC (0x1b) is the ANSI
75
- * introducer; CR/backspace enable line-spoofing in terminal consumers. */
76
- const TEXT_WIRE_CONTROLS = /[\x00-\x08\x0b-\x1f\x7f]/g;
77
- /**
78
- * Strip terminal-control characters from a derived text channel (security
79
- * pass #1, confirmed finding): boundary strings validated only for LENGTH
80
- * (asString — e.g. wyrm_run's `role`/`orchestrator`, stored prime sections)
81
- * may legally carry ESC/CR/backspace, and templates interpolate them raw.
82
- * `content[0].text` feeds terminals (wyrm watch, CLI, CI logs), so injected
83
- * ANSI could spoof those consumers. This is the ONE chokepoint every template
84
- * render passes through — the by-construction guarantee stays a renderer
85
- * property, not per-handler discipline. \n and \t survive (templates are
86
- * multi-line); `structuredContent` is untouched (the machine wire JSON-escapes
87
- * C0 on serialization, so it was never exposed).
88
- */
89
- export function stripWireControls(text) {
90
- return text.replace(TEXT_WIRE_CONTROLS, '');
91
- }
92
- export function channelPolicy() {
93
- const v = process.env.WYRM_CHANNEL;
94
- return v === 'text' || v === 'structured' ? v : 'both';
95
- }
96
- /**
97
- * The READ_ONLY_TOOLS read-cache key for a resolved (name, args) call.
98
- *
99
- * The cached value is a FULLY-RENDERED response whose channel shape
100
- * (content[0].text vs structuredContent presence) depends on channelPolicy()
101
- * — read per render call by design (see the channelPolicy header). The cache
102
- * must therefore be partitioned by that policy: otherwise a policy change
103
- * mid-process serves a stale-channel cache-hit until the entry expires (up to
104
- * its TTL), silently overriding the renderer's per-call freshness for every
105
- * READ_ONLY tool (wyrm_search, wyrm_session_prime, wyrm_context_build, ...).
106
- * In normal production WYRM_CHANNEL is process-static so the prefix is constant
107
- * (zero behavior change); the partition only matters when a harness/test
108
- * toggles the policy at runtime — exactly what the renderer was built to honour.
109
- *
110
- * Single source of truth for the key convention documented in handlers/types.ts
111
- * (ResponseCache): the dispatcher read+write path and every handler-side
112
- * cache.set MUST build keys through this so a publish and its later hit collide.
113
- */
114
- export function cacheKeyFor(name, argsStr) {
115
- return `${channelPolicy()}:${name}:${argsStr}`;
116
- }
117
- /** The pointer appended to a collapsed text channel in 'structured' mode, so a
118
- * text-reading consumer that DID end up on a structured-only payload is told
119
- * where the body went rather than seeing a bare summary line. */
120
- const STRUCTURED_POINTER = '\n\n_(full body in structuredContent)_';
121
- /** First non-empty line of a rendered text block — the implicit summary used
122
- * when a template declares no explicit `summary`. */
123
- function firstLine(text) {
124
- const nl = text.indexOf('\n');
125
- return nl === -1 ? text : text.slice(0, nl);
126
- }
127
- /**
128
- * THE dual-emit constructor for success responses: `structuredContent` is the
129
- * canonical body; `content[0].text` is derived from it — by the template when
130
- * the tool has prose, else the deterministic 2-space JSON serialization (the
131
- * sqlite-busy.ts precedent). Nothing else may write both channels.
132
- *
133
- * The WYRM_CHANNEL policy (see ChannelPolicy) decides which channel(s) ride
134
- * the wire. 'both' (default) reproduces the base @7.0.0 bytes exactly.
135
- */
136
- export function renderResult(body, template, opts) {
137
- // Template path: sanitize at THE chokepoint (stripWireControls above) so no
138
- // boundary string can ride ANSI/C0 controls onto the terminal-facing text
139
- // wire. The JSON path needs none: JSON.stringify escapes C0 to \uXXXX.
140
- const fullText = template ? stripWireControls(template(body, glyphs())) : JSON.stringify(body, null, 2);
141
- const policy = channelPolicy();
142
- if (policy === 'structured' && !opts?.exemptStructured) {
143
- // Collapse the text channel to a summary line + pointer; the body (the
144
- // canonical, information-complete payload) rides structuredContent
145
- // unchanged. The summary is the marked `summary` derivation, else the
146
- // template's first line, else the body JSON (no template ⇒ nothing to
147
- // summarize, keep the deterministic serialization).
148
- const summaryText = opts?.summary
149
- ? stripWireControls(opts.summary(body, glyphs()))
150
- : template
151
- ? firstLine(fullText)
152
- : fullText;
153
- const collapsed = summaryText + STRUCTURED_POINTER;
154
- // Never make the text channel LARGER: a one-line response (a join ack, a
155
- // clean failure verdict) is already minimal, and summary+pointer would
156
- // ADD bytes. Collapse only when it actually shrinks — so 'structured' is a
157
- // monotone-no-worse text wire for every template.
158
- const text = collapsed.length < fullText.length ? collapsed : fullText;
159
- return {
160
- content: [{ type: 'text', text }],
161
- structuredContent: body,
162
- };
163
- }
164
- if (policy === 'text') {
165
- // Single text channel: the human-complete derivation. structuredContent is
166
- // dropped — the consumer reads prose only. (No body channel means nothing
167
- // to shrink, and no information is lost: the text already carried it all.)
168
- return { content: [{ type: 'text', text: fullText }] };
169
- }
170
- // 'both' (default) — byte-identical to the base @7.0.0 wire.
171
- return {
172
- content: [{ type: 'text', text: fullText }],
173
- structuredContent: body,
174
- };
175
- }
176
- /**
177
- * Dual-emit constructor for `isError:true` responses: `content[0].text` is
178
- * the JSON serialization of the SAME body riding `structuredContent` —
179
- * text-parsing and structured clients see one truth. (Error bodies are JSON
180
- * on the text channel by design: their consumers are programs, and the 6.x
181
- * busy path already shipped these bytes.)
182
- */
183
- export function renderErrorResponse(body) {
184
- // Error bodies are JSON on the text channel BY DESIGN (their consumers are
185
- // programs; the 6.x busy path shipped these bytes), so 'structured' shrink
186
- // does not apply — the text already IS the body and collapsing it would drop
187
- // the machine-actionable {error,expected} contract. The only policy branch
188
- // that touches errors is 'text', which drops the redundant structuredContent
189
- // copy (the text channel carries the identical SEP-1303 envelope). 'both'
190
- // (default) and 'structured' keep the base @7.0.0 dual-emit bytes.
191
- if (channelPolicy() === 'text') {
192
- return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }], isError: true };
193
- }
194
- return {
195
- content: [{ type: 'text', text: JSON.stringify(body, null, 2) }],
196
- isError: true,
197
- structuredContent: body,
198
- };
199
- }
200
- // ── SEP-1303 boundary-validation errors (v7 F3, T020) ──────────────────────
201
- /** Machine-readable code for boundary-validation rejections. Deliberately
202
- * distinct from WYRM_BUSY: a busy write is retryable AS-IS; a validation
203
- * failure is only "retryable" after the caller CORRECTS the named argument. */
204
- export const WYRM_VALIDATION_CODE = 'WYRM_VALIDATION';
205
- /**
206
- * Build the validation body. `detail` is ValidationError.message — already
207
- * `'<field>' <reason>` (validate.ts), so the message reads
208
- * `wyrm_x: 'field' must be one of: …`. Deterministic (Article III/VIII: no
209
- * clock, no model — same inputs, same bytes).
210
- */
211
- export function validationErrorBody(tool, field, detail) {
212
- return {
213
- error: {
214
- code: WYRM_VALIDATION_CODE,
215
- message: `${tool}: ${detail}`,
216
- retryable: false,
217
- field,
218
- },
219
- expected: `Correct the '${field}' argument — it ${detail.replace(`'${field}' `, '')} — and call ${tool} again. ` +
220
- `This rejection is deterministic: the SAME arguments will fail the SAME way, so do not retry unchanged ` +
221
- `(unlike WYRM_BUSY, which is retryable as-is). Validators run at the argument-binding seam ` +
222
- `(validate.ts) before the handler's write path, so re-calling with corrected arguments is safe. ` +
223
- `The tool's inputSchema documents the accepted values.`,
224
- };
225
- }
226
- /** The full dual-emit MCP response for a boundary-validation rejection —
227
- * `isError:true`, text = JSON of the SAME body riding structuredContent. */
228
- export function validationErrorResponse(tool, field, detail) {
229
- return renderErrorResponse(validationErrorBody(tool, field, detail));
230
- }
231
- // ── outputSchema contract checking ──────────────────────────────────────────
232
- /**
233
- * Minimal hand-rolled JSON-Schema subset validator (the repo deliberately
234
- * carries no ajv/zod for core ops — Article III keeps the core dependency-
235
- * light). Covers exactly the constructs our outputSchemas use: `type` (incl.
236
- * `["x","null"]` unions), `properties`, `required`, `items`, `enum`,
237
- * `minimum`/`maximum`. Returns a list of human-readable violations (empty =
238
- * conforms). Used by the T019 contract tests to validate every ToolSpec-
239
- * routed response against its declared outputSchema; lifted here from
240
- * tests/toolspec-registry.test.ts so there is ONE validator to keep honest.
241
- */
242
- export function validateAgainstSchema(value, schema, path = '$') {
243
- const errors = [];
244
- const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
245
- const jsType = (v) => v === null ? 'null'
246
- : Array.isArray(v) ? 'array'
247
- : typeof v === 'number' ? (Number.isInteger(v) ? 'integer' : 'number')
248
- : typeof v;
249
- if (types.length > 0) {
250
- const actual = jsType(value);
251
- const ok = types.some((t) => t === actual || (t === 'number' && actual === 'integer'));
252
- if (!ok)
253
- errors.push(`${path}: expected ${types.join('|')}, got ${actual}`);
254
- }
255
- if (schema.enum && !schema.enum.includes(value)) {
256
- errors.push(`${path}: ${JSON.stringify(value)} not in enum`);
257
- }
258
- if (typeof value === 'number') {
259
- if (typeof schema.minimum === 'number' && value < schema.minimum)
260
- errors.push(`${path}: < minimum`);
261
- if (typeof schema.maximum === 'number' && value > schema.maximum)
262
- errors.push(`${path}: > maximum`);
263
- }
264
- if (value && typeof value === 'object' && !Array.isArray(value) && schema.properties) {
265
- const props = schema.properties;
266
- for (const req of schema.required ?? []) {
267
- if (!(req in value))
268
- errors.push(`${path}: missing required '${req}'`);
269
- }
270
- for (const [k, v] of Object.entries(value)) {
271
- if (props[k])
272
- errors.push(...validateAgainstSchema(v, props[k], `${path}.${k}`));
273
- }
274
- }
275
- if (Array.isArray(value) && schema.items) {
276
- value.forEach((v, i) => errors.push(...validateAgainstSchema(v, schema.items, `${path}[${i}]`)));
277
- }
278
- return errors;
279
- }
280
- //# sourceMappingURL=render.js.map
1
+ import{ICON as u}from"./icons.js";function a(){const t=process.env.WYRM_FANCY;return t==="1"||t==="true"}const x={brand:"",ok:"",fail:"",warn:"",paused:"",resumed:"",bullet:"-",point:">"},d={brand:u.brand,ok:u.ok,fail:u.blocked,warn:u.failure,paused:"\u23F8",resumed:"\u25B6",bullet:u.bullet,point:u.point};function l(){return a()?d:x}function N(t,r){return t?`${t} ${r}`:r}const $=/[\x00-\x08\x0b-\x1f\x7f]/g;function m(t){return t.replace($,"")}function p(){const t=process.env.WYRM_CHANNEL;return t==="text"||t==="structured"?t:"both"}function T(t,r){return`${p()}:${t}:${r}`}const g=`
2
+
3
+ _(full body in structuredContent)_`;function A(t){const r=t.indexOf(`
4
+ `);return r===-1?t:t.slice(0,r)}function O(t,r,n){const o=r?m(r(t,l())):JSON.stringify(t,null,2),s=p();if(s==="structured"&&!n?.exemptStructured){const e=(n?.summary?m(n.summary(t,l())):r?A(o):o)+g;return{content:[{type:"text",text:e.length<o.length?e:o}],structuredContent:t}}return s==="text"?{content:[{type:"text",text:o}]}:{content:[{type:"text",text:o}],structuredContent:t}}function b(t){return p()==="text"?{content:[{type:"text",text:JSON.stringify(t,null,2)}],isError:!0}:{content:[{type:"text",text:JSON.stringify(t,null,2)}],isError:!0,structuredContent:t}}const S="WYRM_VALIDATION";function C(t,r,n){return{error:{code:S,message:`${t}: ${n}`,retryable:!1,field:r},expected:`Correct the '${r}' argument \u2014 it ${n.replace(`'${r}' `,"")} \u2014 and call ${t} again. This rejection is deterministic: the SAME arguments will fail the SAME way, so do not retry unchanged (unlike WYRM_BUSY, which is retryable as-is). Validators run at the argument-binding seam (validate.ts) before the handler's write path, so re-calling with corrected arguments is safe. The tool's inputSchema documents the accepted values.`}}function R(t,r,n){return b(C(t,r,n))}function y(t,r,n="$"){const o=[],s=Array.isArray(r.type)?r.type:r.type?[r.type]:[],f=e=>e===null?"null":Array.isArray(e)?"array":typeof e=="number"?Number.isInteger(e)?"integer":"number":typeof e;if(s.length>0){const e=f(t);s.some(c=>c===e||c==="number"&&e==="integer")||o.push(`${n}: expected ${s.join("|")}, got ${e}`)}if(r.enum&&!r.enum.includes(t)&&o.push(`${n}: ${JSON.stringify(t)} not in enum`),typeof t=="number"&&(typeof r.minimum=="number"&&t<r.minimum&&o.push(`${n}: < minimum`),typeof r.maximum=="number"&&t>r.maximum&&o.push(`${n}: > maximum`)),t&&typeof t=="object"&&!Array.isArray(t)&&r.properties){const e=r.properties;for(const i of r.required??[])i in t||o.push(`${n}: missing required '${i}'`);for(const[i,c]of Object.entries(t))e[i]&&o.push(...y(c,e[i],`${n}.${i}`))}return Array.isArray(t)&&r.items&&t.forEach((e,i)=>o.push(...y(e,r.items,`${n}[${i}]`))),o}export{S as WYRM_VALIDATION_CODE,T as cacheKeyFor,p as channelPolicy,l as glyphs,a as isFancy,b as renderErrorResponse,O as renderResult,m as stripWireControls,y as validateAgainstSchema,C as validationErrorBody,R as validationErrorResponse,N as withGlyph};
@@ -1,173 +1 @@
1
- /**
2
- * Live Memory v6.4 — replication egress guard.
3
- *
4
- * The replication client makes OUTBOUND HTTP requests to a caller/registry-supplied
5
- * peer URL, with a bearer token attached. Left unguarded that's an SSRF + credential
6
- * exfiltration primitive (point it at cloud metadata / localhost / an attacker host).
7
- * This module is the chokepoint every outbound replication request goes through:
8
- *
9
- * - scheme allowlist (http/https only),
10
- * - block loopback / link-local / private / metadata hosts (unless
11
- * WYRM_REPL_ALLOW_PRIVATE=1, which is needed for same-host two-node testing),
12
- * - `redirect: 'manual'` so a peer can't 3xx the token to another origin,
13
- * - a hard request timeout (a hung peer must not wedge the daemon),
14
- * - a byte-bounded body read (a hostile peer can't OOM us with a huge response).
15
- *
16
- * Residual: DNS-rebinding (host resolves to a public IP at check time, a private one
17
- * at connect time) is NOT fully closed — fetch doesn't expose connect-time IP. The
18
- * literal-host checks below cover the realistic SSRF targets for an operator-driven
19
- * tool; treat cross-trust peers accordingly.
20
- *
21
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
22
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
23
- */
24
- import { isIP } from 'node:net';
25
- export const PEER_FETCH_TIMEOUT_MS = 15_000;
26
- export const MAX_PEER_RESPONSE_BYTES = 4 * 1024 * 1024; // 4 MB — matches the 500-event push batch
27
- /** Is this hostname/IP literal a loopback / link-local / private / metadata target? */
28
- export function isBlockedHost(hostname) {
29
- // strip IPv6 brackets + a trailing FQDN-root dot ("localhost." resolves to loopback)
30
- const h = hostname.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
31
- if (!h || h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.local') || h.endsWith('.internal'))
32
- return true;
33
- const kind = isIP(h);
34
- if (kind === 4) {
35
- const o = h.split('.').map(Number);
36
- if (o.length !== 4 || o.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
37
- return true; // malformed → block
38
- if (o[0] === 127)
39
- return true; // 127.0.0.0/8 loopback
40
- if (o[0] === 10)
41
- return true; // 10.0.0.0/8
42
- if (o[0] === 0)
43
- return true; // 0.0.0.0/8
44
- if (o[0] === 169 && o[1] === 254)
45
- return true; // 169.254.0.0/16 link-local + metadata
46
- if (o[0] === 172 && o[1] >= 16 && o[1] <= 31)
47
- return true; // 172.16.0.0/12
48
- if (o[0] === 192 && o[1] === 168)
49
- return true; // 192.168.0.0/16
50
- if (o[0] === 100 && o[1] >= 64 && o[1] <= 127)
51
- return true; // 100.64.0.0/10 CGNAT
52
- return false;
53
- }
54
- if (kind === 6) {
55
- if (h === '::1' || h === '::')
56
- return true; // loopback / unspecified
57
- if (h.startsWith('fe80') || h.startsWith('fe9') || h.startsWith('fea') || h.startsWith('feb'))
58
- return true; // fe80::/10 link-local
59
- if (h.startsWith('fc') || h.startsWith('fd'))
60
- return true; // fc00::/7 ULA
61
- // IPv4-mapped (::ffff:a.b.c.d). WHATWG URL normalizes this to the HEX form
62
- // (::ffff:7f00:1), so handle BOTH the dotted tail and the two-hex-group tail —
63
- // otherwise http://[::ffff:127.0.0.1]/ slips through to loopback.
64
- if (h.startsWith('::ffff:')) {
65
- const tail = h.slice('::ffff:'.length);
66
- if (isIP(tail) === 4)
67
- return isBlockedHost(tail);
68
- const m = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
69
- if (m) {
70
- const hi = parseInt(m[1], 16), lo = parseInt(m[2], 16);
71
- return isBlockedHost(`${(hi >> 8) & 255}.${hi & 255}.${(lo >> 8) & 255}.${lo & 255}`);
72
- }
73
- return true; // unrecognized ::ffff: form → block to be safe
74
- }
75
- if (h.startsWith('64:ff9b:'))
76
- return true; // NAT64 well-known prefix (can embed private IPv4)
77
- return false;
78
- }
79
- return false; // a non-IP hostname (public DNS name) — allowed
80
- }
81
- /** Validate a peer base URL for outbound replication. Returns the parsed URL or a reason. */
82
- export function checkPeerUrl(baseUrl) {
83
- let url;
84
- try {
85
- url = new URL(baseUrl);
86
- }
87
- catch {
88
- return { ok: false, reason: 'invalid URL' };
89
- }
90
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
91
- return { ok: false, reason: `scheme ${url.protocol} not allowed (http/https only)` };
92
- }
93
- const allowPrivate = process.env.WYRM_REPL_ALLOW_PRIVATE === '1';
94
- if (!allowPrivate && isBlockedHost(url.hostname)) {
95
- return { ok: false, reason: `host ${url.hostname} is loopback/private/link-local (set WYRM_REPL_ALLOW_PRIVATE=1 for same-host/LAN peers)` };
96
- }
97
- return { ok: true, url };
98
- }
99
- /**
100
- * Is a peer token env-var name allowed? Must be `WYRM_`-prefixed — this stops a
101
- * (possibly attacker-supplied) registry entry from pointing `tokenEnv` at an
102
- * arbitrary process secret like `OPENAI_API_KEY` / `AWS_SECRET_ACCESS_KEY` and
103
- * exfiltrating it to a peer. Set WYRM_REPL_ALLOW_ANY_TOKEN_ENV=1 to override.
104
- */
105
- export function isValidTokenEnvName(name) {
106
- if (process.env.WYRM_REPL_ALLOW_ANY_TOKEN_ENV === '1')
107
- return /^[A-Z][A-Z0-9_]{0,63}$/.test(name);
108
- return /^WYRM_[A-Z0-9_]{1,58}$/.test(name);
109
- }
110
- /**
111
- * Fetch a peer endpoint with the egress guard applied: validated URL, hard timeout,
112
- * no redirect following, and a byte-bounded body. Returns the parsed JSON (object).
113
- * Throws on any violation — callers are failure-isolated and treat throw as "skip".
114
- */
115
- export async function guardedFetchJson(fetchImpl, baseUrl, path, init = {}, maxBytes = MAX_PEER_RESPONSE_BYTES, timeoutMs = PEER_FETCH_TIMEOUT_MS) {
116
- const check = checkPeerUrl(baseUrl);
117
- if (!check.ok)
118
- throw new Error(`peer URL rejected: ${check.reason}`);
119
- const controller = new AbortController();
120
- const timer = setTimeout(() => controller.abort(), timeoutMs);
121
- try {
122
- const res = await fetchImpl(`${baseUrl}${path}`, {
123
- ...init,
124
- redirect: 'manual', // never follow a 3xx — it could leak the token cross-origin
125
- signal: controller.signal,
126
- });
127
- if (res.status >= 300 && res.status < 400)
128
- throw new Error(`peer redirected (${res.status}) — refused`);
129
- if (!res.ok)
130
- throw new Error(`peer HTTP ${res.status}`);
131
- const declared = Number(res.headers?.get?.('content-length') ?? '');
132
- if (Number.isFinite(declared) && declared > maxBytes)
133
- throw new Error('peer response too large (content-length)');
134
- // Real fetch: byte-bounded stream read. Test fakes / bodyless responses: fall back to json()/text().
135
- if (typeof res.body?.getReader === 'function') {
136
- const text = await readBounded(res, maxBytes);
137
- return text ? JSON.parse(text) : {};
138
- }
139
- if (typeof res.json === 'function')
140
- return await res.json();
141
- const t = typeof res.text === 'function' ? await res.text() : '';
142
- return t ? JSON.parse(t) : {};
143
- }
144
- finally {
145
- clearTimeout(timer);
146
- }
147
- }
148
- /** Read a fetch Response body, aborting if it exceeds maxBytes (defeats a no-content-length OOM). */
149
- async function readBounded(res, maxBytes) {
150
- const reader = res.body?.getReader?.();
151
- if (!reader)
152
- return typeof res.text === 'function' ? res.text() : '';
153
- let total = 0;
154
- const chunks = [];
155
- for (;;) {
156
- const { done, value } = await reader.read();
157
- if (done)
158
- break;
159
- if (value) {
160
- total += value.length;
161
- if (total > maxBytes) {
162
- try {
163
- await reader.cancel();
164
- }
165
- catch { /* */ }
166
- throw new Error('peer response too large');
167
- }
168
- chunks.push(value);
169
- }
170
- }
171
- return Buffer.concat(chunks).toString('utf8');
172
- }
173
- //# sourceMappingURL=repl-guard.js.map
1
+ import{isIP as p}from"node:net";const w=15e3,E=4*1024*1024;function f(o){const t=o.toLowerCase().replace(/^\[|\]$/g,"").replace(/\.$/,"");if(!t||t==="localhost"||t.endsWith(".localhost")||t.endsWith(".local")||t.endsWith(".internal"))return!0;const s=p(t);if(s===4){const e=t.split(".").map(Number);return!!(e.length!==4||e.some(r=>!Number.isInteger(r)||r<0||r>255)||e[0]===127||e[0]===10||e[0]===0||e[0]===169&&e[1]===254||e[0]===172&&e[1]>=16&&e[1]<=31||e[0]===192&&e[1]===168||e[0]===100&&e[1]>=64&&e[1]<=127)}if(s===6){if(t==="::1"||t==="::"||t.startsWith("fe80")||t.startsWith("fe9")||t.startsWith("fea")||t.startsWith("feb")||t.startsWith("fc")||t.startsWith("fd"))return!0;if(t.startsWith("::ffff:")){const e=t.slice(7);if(p(e)===4)return f(e);const r=e.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);if(r){const i=parseInt(r[1],16),a=parseInt(r[2],16);return f(`${i>>8&255}.${i&255}.${a>>8&255}.${a&255}`)}return!0}return!!t.startsWith("64:ff9b:")}return!1}function _(o){let t;try{t=new URL(o)}catch{return{ok:!1,reason:"invalid URL"}}return t.protocol!=="http:"&&t.protocol!=="https:"?{ok:!1,reason:`scheme ${t.protocol} not allowed (http/https only)`}:!(process.env.WYRM_REPL_ALLOW_PRIVATE==="1")&&f(t.hostname)?{ok:!1,reason:`host ${t.hostname} is loopback/private/link-local (set WYRM_REPL_ALLOW_PRIVATE=1 for same-host/LAN peers)`}:{ok:!0,url:t}}function R(o){return process.env.WYRM_REPL_ALLOW_ANY_TOKEN_ENV==="1"?/^[A-Z][A-Z0-9_]{0,63}$/.test(o):/^WYRM_[A-Z0-9_]{1,58}$/.test(o)}async function g(o,t,s,e={},r=E,i=w){const a=_(t);if(!a.ok)throw new Error(`peer URL rejected: ${a.reason}`);const u=new AbortController,d=setTimeout(()=>u.abort(),i);try{const n=await o(`${t}${s}`,{...e,redirect:"manual",signal:u.signal});if(n.status>=300&&n.status<400)throw new Error(`peer redirected (${n.status}) \u2014 refused`);if(!n.ok)throw new Error(`peer HTTP ${n.status}`);const c=Number(n.headers?.get?.("content-length")??"");if(Number.isFinite(c)&&c>r)throw new Error("peer response too large (content-length)");if(typeof n.body?.getReader=="function"){const h=await W(n,r);return h?JSON.parse(h):{}}if(typeof n.json=="function")return await n.json();const l=typeof n.text=="function"?await n.text():"";return l?JSON.parse(l):{}}finally{clearTimeout(d)}}async function W(o,t){const s=o.body?.getReader?.();if(!s)return typeof o.text=="function"?o.text():"";let e=0;const r=[];for(;;){const{done:i,value:a}=await s.read();if(i)break;if(a){if(e+=a.length,e>t){try{await s.cancel()}catch{}throw new Error("peer response too large")}r.push(a)}}return Buffer.concat(r).toString("utf8")}export{E as MAX_PEER_RESPONSE_BYTES,w as PEER_FETCH_TIMEOUT_MS,_ as checkPeerUrl,g as guardedFetchJson,f as isBlockedHost,R as isValidTokenEnvName};
@@ -1,31 +1 @@
1
- /**
2
- * Wyrm Live Memory — replication daemon entrypoint.
3
- *
4
- * Spawned detached by `ReplicationManager.start()`. Opens the same DB the MCP
5
- * server uses (`WYRM_DB_PATH` or `~/.wyrm/wyrm.db`) and runs the sync loop until
6
- * SIGTERM/SIGINT. Not meant to be invoked directly — go through the
7
- * `wyrm_replication` MCP tool, which manages the PID file.
8
- *
9
- * argv[2] = interval_ms
10
- *
11
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
12
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
13
- */
14
- import { WyrmDB } from './database.js';
15
- import { ReplicationDaemon, DEFAULT_INTERVAL_MS } from './replication-daemon.js';
16
- async function main() {
17
- const intervalMs = Number(process.argv[2]) || DEFAULT_INTERVAL_MS;
18
- const db = new WyrmDB(process.env.WYRM_DB_PATH);
19
- if (!db.liveMemoryEnabled()) {
20
- console.error('[replication-entrypoint] WYRM_LIVE_MEMORY is off — refusing to start');
21
- process.exit(2);
22
- }
23
- const daemon = new ReplicationDaemon(db, intervalMs);
24
- console.log(`[replication] started · interval=${intervalMs}ms · db=${process.env.WYRM_DB_PATH || '~/.wyrm/wyrm.db'}`);
25
- await daemon.run();
26
- }
27
- main().catch((e) => {
28
- console.error('[replication-entrypoint] fatal:', e);
29
- process.exit(1);
30
- });
31
- //# sourceMappingURL=replication-daemon-entrypoint.js.map
1
+ import{WyrmDB as n}from"./database.js";import{ReplicationDaemon as t,DEFAULT_INTERVAL_MS as i}from"./replication-daemon.js";async function s(){const o=Number(process.argv[2])||i,e=new n(process.env.WYRM_DB_PATH);e.liveMemoryEnabled()||(console.error("[replication-entrypoint] WYRM_LIVE_MEMORY is off \u2014 refusing to start"),process.exit(2));const r=new t(e,o);console.log(`[replication] started \xB7 interval=${o}ms \xB7 db=${process.env.WYRM_DB_PATH||"~/.wyrm/wyrm.db"}`),await r.run()}s().catch(o=>{console.error("[replication-entrypoint] fatal:",o),process.exit(1)});