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/render.js
CHANGED
|
@@ -1,280 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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};
|
package/dist/repl-guard.js
CHANGED
|
@@ -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)});
|