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/cloud/client.js
CHANGED
|
@@ -1,221 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Wyrm Cloud HTTP client — Bearer-auth wrapper around wyrm.ghosts.lk.
|
|
3
|
-
*
|
|
4
|
-
* Persists session to ~/.wyrm/cloud.json on successful login. The session
|
|
5
|
-
* value is the only secret stored locally for cloud access (the master
|
|
6
|
-
* encryption key is in a SEPARATE file — see crypto.ts).
|
|
7
|
-
*
|
|
8
|
-
* Endpoints used:
|
|
9
|
-
* POST /api/v1/auth/cli-init — start a login flow
|
|
10
|
-
* POST /api/v1/auth/cli-poll — exchange poll_token → session
|
|
11
|
-
* GET /api/v1/auth/me — current account info
|
|
12
|
-
* POST /api/v1/auth/logout — revoke this session
|
|
13
|
-
* POST /api/v1/devices — register this machine as a device
|
|
14
|
-
* GET /api/v1/devices — list devices
|
|
15
|
-
* DELETE /api/v1/devices/:id — revoke a device
|
|
16
|
-
* POST /api/v1/sync/push — upload encrypted deltas
|
|
17
|
-
* GET /api/v1/sync/pull — download peer deltas
|
|
18
|
-
* GET /api/v1/sync/status — tier + storage usage
|
|
19
|
-
*
|
|
20
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
21
|
-
*/
|
|
22
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, unlinkSync } from 'node:fs';
|
|
23
|
-
import { join } from 'node:path';
|
|
24
|
-
import { homedir } from 'node:os';
|
|
25
|
-
export const DEFAULT_BASE = process.env.WYRM_CLOUD_URL ?? 'https://wyrm.ghosts.lk';
|
|
26
|
-
/**
|
|
27
|
-
* Resolve the per-operator Wyrm config directory.
|
|
28
|
-
*
|
|
29
|
-
* Honors `WYRM_CLOUD_DIR` (an absolute path) so tests can sandbox every
|
|
30
|
-
* file we touch (session, cursor, key, machine-id) away from the real
|
|
31
|
-
* ~/.wyrm. Resolved at call time — NOT frozen at import — so a test that
|
|
32
|
-
* sets the env after importing this module still redirects correctly.
|
|
33
|
-
*/
|
|
34
|
-
export function resolveCloudDir() {
|
|
35
|
-
return process.env.WYRM_CLOUD_DIR ?? join(homedir(), '.wyrm');
|
|
36
|
-
}
|
|
37
|
-
export const CLOUD_DIR = resolveCloudDir();
|
|
38
|
-
export const SESSION_FILE = join(CLOUD_DIR, 'cloud.json');
|
|
39
|
-
/** Resolve the session file path at call time (honors WYRM_CLOUD_DIR). */
|
|
40
|
-
export function sessionFilePath() {
|
|
41
|
-
return join(resolveCloudDir(), 'cloud.json');
|
|
42
|
-
}
|
|
43
|
-
function ensureDir() {
|
|
44
|
-
const dir = resolveCloudDir();
|
|
45
|
-
if (!existsSync(dir))
|
|
46
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
47
|
-
}
|
|
48
|
-
export function loadSession() {
|
|
49
|
-
const file = sessionFilePath();
|
|
50
|
-
if (!existsSync(file))
|
|
51
|
-
return null;
|
|
52
|
-
try {
|
|
53
|
-
return JSON.parse(readFileSync(file, 'utf-8'));
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
export function saveSession(sess) {
|
|
60
|
-
ensureDir();
|
|
61
|
-
const file = sessionFilePath();
|
|
62
|
-
writeFileSync(file, JSON.stringify(sess, null, 2), { mode: 0o600 });
|
|
63
|
-
try {
|
|
64
|
-
chmodSync(file, 0o600);
|
|
65
|
-
}
|
|
66
|
-
catch { /* best-effort */ }
|
|
67
|
-
}
|
|
68
|
-
export function clearSession() {
|
|
69
|
-
try {
|
|
70
|
-
unlinkSync(sessionFilePath());
|
|
71
|
-
}
|
|
72
|
-
catch { /* fine */ }
|
|
73
|
-
}
|
|
74
|
-
export class CloudError extends Error {
|
|
75
|
-
status;
|
|
76
|
-
body;
|
|
77
|
-
constructor(status, body, message) {
|
|
78
|
-
super(message);
|
|
79
|
-
this.status = status;
|
|
80
|
-
this.body = body;
|
|
81
|
-
this.name = 'CloudError';
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
export class CloudClient {
|
|
85
|
-
base;
|
|
86
|
-
bearer;
|
|
87
|
-
constructor(base, bearer) {
|
|
88
|
-
this.base = base;
|
|
89
|
-
this.bearer = bearer;
|
|
90
|
-
}
|
|
91
|
-
static fromSession() {
|
|
92
|
-
const s = loadSession();
|
|
93
|
-
if (!s)
|
|
94
|
-
throw new CloudError(401, null, 'Not logged in. Run `wyrm cloud login` first.');
|
|
95
|
-
return new CloudClient(s.base, s.session);
|
|
96
|
-
}
|
|
97
|
-
async req(path, init = {}) {
|
|
98
|
-
const headers = {
|
|
99
|
-
'content-type': 'application/json',
|
|
100
|
-
'user-agent': 'wyrm-cli/cloud',
|
|
101
|
-
...(init.headers ?? {}),
|
|
102
|
-
};
|
|
103
|
-
if (this.bearer)
|
|
104
|
-
headers.authorization = `Bearer ${this.bearer}`;
|
|
105
|
-
// Resilient under heavy team sync: retry transient failures (429 + 5xx +
|
|
106
|
-
// network/timeout) with exponential backoff, honoring a server Retry-After.
|
|
107
|
-
// Cloud writes are idempotent (server INSERT-OR-IGNORE keyed on
|
|
108
|
-
// origin_device/seq), so replaying a push is safe. Hard 4xx (auth, bad
|
|
109
|
-
// request) are NOT retried — they won't get better.
|
|
110
|
-
const MAX_RETRIES = 4;
|
|
111
|
-
let lastErr = null;
|
|
112
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
113
|
-
// 60-second timeout via AbortSignal — defeats network-blackhole hangs.
|
|
114
|
-
const ctrl = new AbortController();
|
|
115
|
-
const timer = setTimeout(() => ctrl.abort(), 60_000);
|
|
116
|
-
let res;
|
|
117
|
-
try {
|
|
118
|
-
res = await fetch(`${this.base}${path}`, { ...init, headers, signal: ctrl.signal });
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
clearTimeout(timer);
|
|
122
|
-
lastErr = err.name === 'AbortError'
|
|
123
|
-
? new CloudError(0, null, `Timed out reaching ${this.base}${path} after 60s`)
|
|
124
|
-
: new CloudError(0, null, `Network error: ${err instanceof Error ? err.message : err}`);
|
|
125
|
-
if (attempt < MAX_RETRIES) {
|
|
126
|
-
await this.backoff(attempt, null);
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
throw lastErr;
|
|
130
|
-
}
|
|
131
|
-
clearTimeout(timer);
|
|
132
|
-
if ((res.status === 429 || res.status >= 500) && attempt < MAX_RETRIES) {
|
|
133
|
-
await this.backoff(attempt, res.headers.get('retry-after'));
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
const text = await res.text();
|
|
137
|
-
let body = null;
|
|
138
|
-
try {
|
|
139
|
-
body = text ? JSON.parse(text) : null;
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
body = text;
|
|
143
|
-
}
|
|
144
|
-
if (!res.ok) {
|
|
145
|
-
const msg = body?.message
|
|
146
|
-
?? body?.error
|
|
147
|
-
?? `HTTP ${res.status}`;
|
|
148
|
-
throw new CloudError(res.status, body, msg);
|
|
149
|
-
}
|
|
150
|
-
return body;
|
|
151
|
-
}
|
|
152
|
-
throw lastErr ?? new CloudError(0, null, `request to ${path} failed after ${MAX_RETRIES} retries`);
|
|
153
|
-
}
|
|
154
|
-
/** Backoff before a retry: honor numeric Retry-After (seconds), else
|
|
155
|
-
* exponential 250ms·2^n capped at 30s, with jitter. */
|
|
156
|
-
async backoff(attempt, retryAfter) {
|
|
157
|
-
let ms;
|
|
158
|
-
if (retryAfter && /^\d+$/.test(retryAfter)) {
|
|
159
|
-
ms = Math.min(30_000, parseInt(retryAfter, 10) * 1000);
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
ms = Math.min(30_000, 250 * Math.pow(2, attempt));
|
|
163
|
-
ms += Math.floor(Math.random() * (ms * 0.25)); // jitter to avoid thundering herd
|
|
164
|
-
}
|
|
165
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
166
|
-
}
|
|
167
|
-
// ── Auth ────────────────────────────────────────────────────────────────
|
|
168
|
-
async cliInit(pollToken) {
|
|
169
|
-
return this.req('/api/v1/auth/cli-init', { method: 'POST', body: JSON.stringify({ poll_token: pollToken }) });
|
|
170
|
-
}
|
|
171
|
-
async cliPoll(pollToken) {
|
|
172
|
-
return this.req('/api/v1/auth/cli-poll', { method: 'POST', body: JSON.stringify({ poll_token: pollToken }) });
|
|
173
|
-
}
|
|
174
|
-
async me() {
|
|
175
|
-
return this.req('/api/v1/auth/me');
|
|
176
|
-
}
|
|
177
|
-
async logout() {
|
|
178
|
-
return this.req('/api/v1/auth/logout', { method: 'POST' });
|
|
179
|
-
}
|
|
180
|
-
// ── Devices ─────────────────────────────────────────────────────────────
|
|
181
|
-
async registerDevice(name, pubkey) {
|
|
182
|
-
return this.req('/api/v1/devices', { method: 'POST', body: JSON.stringify({ name, pubkey }) });
|
|
183
|
-
}
|
|
184
|
-
async listDevices() {
|
|
185
|
-
return this.req('/api/v1/devices');
|
|
186
|
-
}
|
|
187
|
-
async revokeDevice(deviceId) {
|
|
188
|
-
return this.req(`/api/v1/devices/${encodeURIComponent(deviceId)}`, {
|
|
189
|
-
method: 'DELETE',
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
// ── Sync ────────────────────────────────────────────────────────────────
|
|
193
|
-
async syncStatus() {
|
|
194
|
-
return this.req('/api/v1/sync/status');
|
|
195
|
-
}
|
|
196
|
-
async syncPush(deviceId, deltas) {
|
|
197
|
-
return this.req('/api/v1/sync/push', {
|
|
198
|
-
method: 'POST',
|
|
199
|
-
body: JSON.stringify({ device_id: deviceId, deltas }),
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
// ── Account ─────────────────────────────────────────────────────────────
|
|
203
|
-
async accountExport() {
|
|
204
|
-
return this.req('/api/v1/account/export');
|
|
205
|
-
}
|
|
206
|
-
async accountDelete() {
|
|
207
|
-
return this.req('/api/v1/account', {
|
|
208
|
-
method: 'DELETE',
|
|
209
|
-
body: JSON.stringify({ confirm: 'DELETE my account' }),
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
async syncPull(deviceId, since, limit = 100) {
|
|
213
|
-
const qs = new URLSearchParams({
|
|
214
|
-
device_id: deviceId,
|
|
215
|
-
since: String(since),
|
|
216
|
-
limit: String(limit),
|
|
217
|
-
});
|
|
218
|
-
return this.req(`/api/v1/sync/pull?${qs.toString()}`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
//# sourceMappingURL=client.js.map
|
|
1
|
+
import{readFileSync as w,writeFileSync as g,existsSync as S,mkdirSync as b,chmodSync as E,unlinkSync as O}from"node:fs";import{join as y}from"node:path";import{homedir as T}from"node:os";const k=process.env.WYRM_CLOUD_URL??"https://wyrm.ghosts.lk";function d(){return process.env.WYRM_CLOUD_DIR??y(T(),".wyrm")}const x=d(),N=y(x,"cloud.json");function m(){return y(d(),"cloud.json")}function q(){const s=d();S(s)||b(s,{recursive:!0,mode:448})}function D(){const s=m();if(!S(s))return null;try{return JSON.parse(w(s,"utf-8"))}catch{return null}}function L(s){q();const t=m();g(t,JSON.stringify(s,null,2),{mode:384});try{E(t,384)}catch{}}function R(){try{O(m())}catch{}}class u extends Error{status;body;constructor(t,e,r){super(r),this.status=t,this.body=e,this.name="CloudError"}}class v{base;bearer;constructor(t,e){this.base=t,this.bearer=e}static fromSession(){const t=D();if(!t)throw new u(401,null,"Not logged in. Run `wyrm cloud login` first.");return new v(t.base,t.session)}async req(t,e={}){const r={"content-type":"application/json","user-agent":"wyrm-cli/cloud",...e.headers??{}};this.bearer&&(r.authorization=`Bearer ${this.bearer}`);const n=4;let l=null;for(let i=0;i<=n;i++){const p=new AbortController,f=setTimeout(()=>p.abort(),6e4);let o;try{o=await fetch(`${this.base}${t}`,{...e,headers:r,signal:p.signal})}catch(c){if(clearTimeout(f),l=c.name==="AbortError"?new u(0,null,`Timed out reaching ${this.base}${t} after 60s`):new u(0,null,`Network error: ${c instanceof Error?c.message:c}`),i<n){await this.backoff(i,null);continue}throw l}if(clearTimeout(f),(o.status===429||o.status>=500)&&i<n){await this.backoff(i,o.headers.get("retry-after"));continue}const h=await o.text();let a=null;try{a=h?JSON.parse(h):null}catch{a=h}if(!o.ok){const c=a?.message??a?.error??`HTTP ${o.status}`;throw new u(o.status,a,c)}return a}throw l??new u(0,null,`request to ${t} failed after ${n} retries`)}async backoff(t,e){let r;e&&/^\d+$/.test(e)?r=Math.min(3e4,parseInt(e,10)*1e3):(r=Math.min(3e4,250*Math.pow(2,t)),r+=Math.floor(Math.random()*(r*.25))),await new Promise(n=>setTimeout(n,r))}async cliInit(t){return this.req("/api/v1/auth/cli-init",{method:"POST",body:JSON.stringify({poll_token:t})})}async cliPoll(t){return this.req("/api/v1/auth/cli-poll",{method:"POST",body:JSON.stringify({poll_token:t})})}async me(){return this.req("/api/v1/auth/me")}async logout(){return this.req("/api/v1/auth/logout",{method:"POST"})}async registerDevice(t,e){return this.req("/api/v1/devices",{method:"POST",body:JSON.stringify({name:t,pubkey:e})})}async listDevices(){return this.req("/api/v1/devices")}async revokeDevice(t){return this.req(`/api/v1/devices/${encodeURIComponent(t)}`,{method:"DELETE"})}async syncStatus(){return this.req("/api/v1/sync/status")}async syncPush(t,e){return this.req("/api/v1/sync/push",{method:"POST",body:JSON.stringify({device_id:t,deltas:e})})}async accountExport(){return this.req("/api/v1/account/export")}async accountDelete(){return this.req("/api/v1/account",{method:"DELETE",body:JSON.stringify({confirm:"DELETE my account"})})}async syncPull(t,e,r=100){const n=new URLSearchParams({device_id:t,since:String(e),limit:String(r)});return this.req(`/api/v1/sync/pull?${n.toString()}`)}}export{x as CLOUD_DIR,v as CloudClient,u as CloudError,k as DEFAULT_BASE,N as SESSION_FILE,R as clearSession,D as loadSession,d as resolveCloudDir,L as saveSession,m as sessionFilePath};
|
package/dist/cloud/crypto.js
CHANGED
|
@@ -1,85 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Wyrm Cloud client-side encryption.
|
|
3
|
-
*
|
|
4
|
-
* AES-256-GCM with a randomly-generated master key held ONLY at
|
|
5
|
-
* ~/.wyrm/cloud.key (0600 perms). The cloud server stores ciphertext;
|
|
6
|
-
* losing the key means losing the data — by design, this is the
|
|
7
|
-
* operator-owns-data guarantee in constitution rule IV.
|
|
8
|
-
*
|
|
9
|
-
* Per-delta encryption envelope (32 bytes IV + 16 bytes tag minimum):
|
|
10
|
-
*
|
|
11
|
-
* [ version:1 ][ iv:12 ][ ciphertext+tag:N+16 ]
|
|
12
|
-
*
|
|
13
|
-
* The 1-byte version prefix lets us migrate the envelope format later
|
|
14
|
-
* without breaking previously-uploaded blobs.
|
|
15
|
-
*
|
|
16
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
17
|
-
*/
|
|
18
|
-
import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
|
|
19
|
-
import { readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs';
|
|
20
|
-
import { join } from 'node:path';
|
|
21
|
-
import { CLOUD_DIR } from './client.js';
|
|
22
|
-
const KEY_FILE = join(CLOUD_DIR, 'cloud.key');
|
|
23
|
-
const ENVELOPE_VERSION = 1;
|
|
24
|
-
const KEY_BYTES = 32; // AES-256
|
|
25
|
-
const IV_BYTES = 12; // GCM standard
|
|
26
|
-
/** Get or generate the per-operator master key. */
|
|
27
|
-
export function getMasterKey() {
|
|
28
|
-
if (existsSync(KEY_FILE)) {
|
|
29
|
-
const buf = readFileSync(KEY_FILE);
|
|
30
|
-
if (buf.length !== KEY_BYTES) {
|
|
31
|
-
throw new Error(`Wyrm Cloud key at ${KEY_FILE} has wrong length (${buf.length}, expected ${KEY_BYTES}). Refusing to proceed.`);
|
|
32
|
-
}
|
|
33
|
-
return buf;
|
|
34
|
-
}
|
|
35
|
-
const fresh = randomBytes(KEY_BYTES);
|
|
36
|
-
writeFileSync(KEY_FILE, fresh, { mode: 0o600 });
|
|
37
|
-
try {
|
|
38
|
-
chmodSync(KEY_FILE, 0o600);
|
|
39
|
-
}
|
|
40
|
-
catch { /* best-effort */ }
|
|
41
|
-
return fresh;
|
|
42
|
-
}
|
|
43
|
-
/** Encrypt a plaintext object → base64 string (envelope: version|iv|ct+tag). */
|
|
44
|
-
export function encrypt(plaintext) {
|
|
45
|
-
const key = getMasterKey();
|
|
46
|
-
const iv = randomBytes(IV_BYTES);
|
|
47
|
-
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
48
|
-
const ct = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
49
|
-
const tag = cipher.getAuthTag();
|
|
50
|
-
const envelope = Buffer.concat([
|
|
51
|
-
Buffer.from([ENVELOPE_VERSION]),
|
|
52
|
-
iv,
|
|
53
|
-
ct,
|
|
54
|
-
tag,
|
|
55
|
-
]);
|
|
56
|
-
return envelope.toString('base64');
|
|
57
|
-
}
|
|
58
|
-
/** Decrypt a base64 envelope back to plaintext. Throws on tag mismatch (tamper). */
|
|
59
|
-
export function decrypt(payloadB64) {
|
|
60
|
-
const key = getMasterKey();
|
|
61
|
-
const env = Buffer.from(payloadB64, 'base64');
|
|
62
|
-
if (env.length < 1 + IV_BYTES + 16) {
|
|
63
|
-
throw new Error(`Wyrm Cloud envelope too short (${env.length} bytes).`);
|
|
64
|
-
}
|
|
65
|
-
const version = env.readUInt8(0);
|
|
66
|
-
if (version !== ENVELOPE_VERSION) {
|
|
67
|
-
throw new Error(`Wyrm Cloud envelope version ${version} unsupported (this client speaks ${ENVELOPE_VERSION}). Upgrade wyrm-mcp.`);
|
|
68
|
-
}
|
|
69
|
-
const iv = env.subarray(1, 1 + IV_BYTES);
|
|
70
|
-
const tag = env.subarray(env.length - 16);
|
|
71
|
-
const ct = env.subarray(1 + IV_BYTES, env.length - 16);
|
|
72
|
-
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
73
|
-
decipher.setAuthTag(tag);
|
|
74
|
-
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
75
|
-
return pt.toString('utf-8');
|
|
76
|
-
}
|
|
77
|
-
/** Return path of the key file for status reporting (never the bytes). */
|
|
78
|
-
export function keyFilePath() {
|
|
79
|
-
return KEY_FILE;
|
|
80
|
-
}
|
|
81
|
-
/** Confirm a key exists locally without exposing it. */
|
|
82
|
-
export function keyExists() {
|
|
83
|
-
return existsSync(KEY_FILE);
|
|
84
|
-
}
|
|
85
|
-
//# sourceMappingURL=crypto.js.map
|
|
1
|
+
import{randomBytes as h,createCipheriv as g,createDecipheriv as m}from"node:crypto";import{readFileSync as d,writeFileSync as E,existsSync as y,chmodSync as v}from"node:fs";import{join as w}from"node:path";import{CLOUD_DIR as S}from"./client.js";const r=w(S,"cloud.key"),u=1,f=32,c=12;function l(){if(y(r)){const e=d(r);if(e.length!==f)throw new Error(`Wyrm Cloud key at ${r} has wrong length (${e.length}, expected ${f}). Refusing to proceed.`);return e}const o=h(f);E(r,o,{mode:384});try{v(r,384)}catch{}return o}function C(o){const e=l(),t=h(c),n=g("aes-256-gcm",e,t),s=Buffer.concat([n.update(o,"utf-8"),n.final()]),i=n.getAuthTag();return Buffer.concat([Buffer.from([u]),t,s,i]).toString("base64")}function I(o){const e=l(),t=Buffer.from(o,"base64");if(t.length<1+c+16)throw new Error(`Wyrm Cloud envelope too short (${t.length} bytes).`);const n=t.readUInt8(0);if(n!==u)throw new Error(`Wyrm Cloud envelope version ${n} unsupported (this client speaks ${u}). Upgrade wyrm-mcp.`);const s=t.subarray(1,1+c),i=t.subarray(t.length-16),p=t.subarray(1+c,t.length-16),a=m("aes-256-gcm",e,s);return a.setAuthTag(i),Buffer.concat([a.update(p),a.final()]).toString("utf-8")}function _(){return r}function F(){return y(r)}export{I as decrypt,C as encrypt,l as getMasterKey,F as keyExists,_ as keyFilePath};
|
package/dist/cloud/machine-id.js
CHANGED
|
@@ -1,113 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* THE PROBLEM (failure #40): Wyrm Cloud's per-device pull query is
|
|
5
|
-
* WHERE account_id=? AND device_id != <self> AND updated_at > cursor
|
|
6
|
-
* i.e. a device never re-pulls its OWN device_id's deltas — correct, a
|
|
7
|
-
* device already has its own writes. The `device_id` is minted once at
|
|
8
|
-
* `wyrm cloud login` and stored in ~/.wyrm/cloud.json. If an operator
|
|
9
|
-
* provisions a SECOND machine by COPYING ~/.wyrm/cloud.json (or the whole
|
|
10
|
-
* ~/.wyrm) instead of running its own `wyrm cloud login`, both machines
|
|
11
|
-
* share ONE device_id. Each then filters the OTHER's deltas out as "its
|
|
12
|
-
* own" → push works, pull returns 0, silently, with no error. Peer skills
|
|
13
|
-
* / truths appear MISSING on the copied machine.
|
|
14
|
-
*
|
|
15
|
-
* THE FIX (client-side): bind a stable machine fingerprint into the
|
|
16
|
-
* session at login. On sync, recompute it and compare. A mismatch means
|
|
17
|
-
* "this session was minted on a different machine" → device_id collision
|
|
18
|
-
* → warn LOUD (stderr) and stop, with a --force / env escape for the
|
|
19
|
-
* legitimate hostname-change case.
|
|
20
|
-
*
|
|
21
|
-
* FINGERPRINT INPUTS (deterministic, no new dependency):
|
|
22
|
-
* machine_fp = sha256( hostname "\n" install_id )[:32]
|
|
23
|
-
* - hostname = os.hostname() (the human-facing machine identity;
|
|
24
|
-
* a genuine hostname change is the one false-positive
|
|
25
|
-
* we accept and cover with --force)
|
|
26
|
-
* - install_id = a 32-hex-char random id generated ONCE and stored at
|
|
27
|
-
* ~/.wyrm/machine-id (0600). This is the load-bearing
|
|
28
|
-
* component: it is NOT copied by `cp ~/.wyrm/cloud.json`
|
|
29
|
-
* alone, and even a full `cp -r ~/.wyrm` to a new box
|
|
30
|
-
* yields the SAME install_id but a DIFFERENT hostname,
|
|
31
|
-
* so the fingerprint still diverges. A fresh
|
|
32
|
-
* `wyrm cloud login` on the new box regenerates nothing
|
|
33
|
-
* here (install_id is machine-scoped, not session-scoped)
|
|
34
|
-
* but re-mints a distinct device_id — which is the cure.
|
|
35
|
-
*
|
|
36
|
-
* The install_id file is intentionally machine-local config, NOT a secret:
|
|
37
|
-
* it carries no account data and only disambiguates one machine from
|
|
38
|
-
* another. The ONLY ~/.wyrm file meant to be shared between an operator's
|
|
39
|
-
* machines is ~/.wyrm/cloud.key (the E2E master key). cloud.json,
|
|
40
|
-
* cloud-cursor.json, and machine-id are all per-device.
|
|
41
|
-
*
|
|
42
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
43
|
-
* @license AGPL-3.0-or-later
|
|
44
|
-
*/
|
|
45
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
46
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs';
|
|
47
|
-
import { join } from 'node:path';
|
|
48
|
-
import { hostname } from 'node:os';
|
|
49
|
-
import { resolveCloudDir } from './client.js';
|
|
50
|
-
/** Resolve the install-id file path at call time (honors WYRM_CLOUD_DIR). */
|
|
51
|
-
export function machineIdFilePath() {
|
|
52
|
-
return join(resolveCloudDir(), 'machine-id');
|
|
53
|
-
}
|
|
54
|
-
const INSTALL_ID_RE = /^[0-9a-f]{32}$/;
|
|
55
|
-
function ensureDir() {
|
|
56
|
-
const dir = resolveCloudDir();
|
|
57
|
-
if (!existsSync(dir))
|
|
58
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Get or generate this machine's persistent install id (32 hex chars).
|
|
62
|
-
*
|
|
63
|
-
* Generated once and stored at ~/.wyrm/machine-id (0600). Stable across
|
|
64
|
-
* logins, sessions, and Wyrm upgrades on the same machine. A malformed or
|
|
65
|
-
* empty file is treated as missing and regenerated (fail-open: a fresh id
|
|
66
|
-
* is harmless — worst case is one spurious "looks copied" warning that
|
|
67
|
-
* the operator clears by re-login, which is the desired loud behavior).
|
|
68
|
-
*/
|
|
69
|
-
export function getInstallId() {
|
|
70
|
-
const file = machineIdFilePath();
|
|
71
|
-
if (existsSync(file)) {
|
|
72
|
-
try {
|
|
73
|
-
const raw = readFileSync(file, 'utf-8').trim();
|
|
74
|
-
if (INSTALL_ID_RE.test(raw))
|
|
75
|
-
return raw;
|
|
76
|
-
}
|
|
77
|
-
catch { /* fall through to regenerate */ }
|
|
78
|
-
}
|
|
79
|
-
ensureDir();
|
|
80
|
-
const fresh = randomBytes(16).toString('hex'); // 32 hex chars
|
|
81
|
-
writeFileSync(file, fresh, { mode: 0o600 });
|
|
82
|
-
try {
|
|
83
|
-
chmodSync(file, 0o600);
|
|
84
|
-
}
|
|
85
|
-
catch { /* best-effort */ }
|
|
86
|
-
return fresh;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Compute this machine's fingerprint: sha256(hostname \n install_id),
|
|
90
|
-
* truncated to 32 hex chars. Deterministic on a given machine; diverges
|
|
91
|
-
* across machines (different install_id and/or hostname).
|
|
92
|
-
*/
|
|
93
|
-
export function computeMachineFp() {
|
|
94
|
-
const h = hostname();
|
|
95
|
-
const id = getInstallId();
|
|
96
|
-
return createHash('sha256').update(`${h}\n${id}`).digest('hex').slice(0, 32);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Classify a session's stored machine_fp against this machine.
|
|
100
|
-
*
|
|
101
|
-
* - absent → 'adopt' (backward compat: pre-7.0.3 session, no false alarm)
|
|
102
|
-
* - equal → 'match' (normal, silent)
|
|
103
|
-
* - differs → 'mismatch'(looks copied from another machine — warn loud)
|
|
104
|
-
*/
|
|
105
|
-
export function classifySession(storedFp) {
|
|
106
|
-
if (!storedFp)
|
|
107
|
-
return { state: 'adopt' };
|
|
108
|
-
const current = computeMachineFp();
|
|
109
|
-
if (storedFp === current)
|
|
110
|
-
return { state: 'match' };
|
|
111
|
-
return { state: 'mismatch', stored: storedFp, current };
|
|
112
|
-
}
|
|
113
|
-
//# sourceMappingURL=machine-id.js.map
|
|
1
|
+
import{createHash as i,randomBytes as c}from"node:crypto";import{readFileSync as s,writeFileSync as a,existsSync as n,mkdirSync as m,chmodSync as f}from"node:fs";import{join as u}from"node:path";import{hostname as h}from"node:os";import{resolveCloudDir as o}from"./client.js";function d(){return u(o(),"machine-id")}const p=/^[0-9a-f]{32}$/;function l(){const t=o();n(t)||m(t,{recursive:!0,mode:448})}function y(){const t=d();if(n(t))try{const e=s(t,"utf-8").trim();if(p.test(e))return e}catch{}l();const r=c(16).toString("hex");a(t,r,{mode:384});try{f(t,384)}catch{}return r}function S(){const t=h(),r=y();return i("sha256").update(`${t}
|
|
2
|
+
${r}`).digest("hex").slice(0,32)}function v(t){if(!t)return{state:"adopt"};const r=S();return t===r?{state:"match"}:{state:"mismatch",stored:t,current:r}}export{v as classifySession,S as computeMachineFp,y as getInstallId,d as machineIdFilePath};
|
package/dist/cloud/recovery.js
CHANGED
|
@@ -1,60 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* BIP39 recovery for the Wyrm Cloud master key.
|
|
3
|
-
*
|
|
4
|
-
* The 32-byte key in ~/.wyrm/cloud.key is exactly the 256 bits of entropy
|
|
5
|
-
* a BIP39 24-word mnemonic encodes. So we can:
|
|
6
|
-
*
|
|
7
|
-
* 1. Print the current key as 24 words so the operator can write them
|
|
8
|
-
* down on paper / store them in a password manager.
|
|
9
|
-
* 2. Restore a key from those 24 words on a new machine.
|
|
10
|
-
*
|
|
11
|
-
* The cloud server never sees either form. This is purely a local
|
|
12
|
-
* operator-facing convenience layered on top of the existing key file —
|
|
13
|
-
* losing the key file with no recovery phrase still means losing access,
|
|
14
|
-
* which is the deliberate operator-owns-data tradeoff.
|
|
15
|
-
*
|
|
16
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
17
|
-
*/
|
|
18
|
-
import { entropyToMnemonic, mnemonicToEntropy, validateMnemonic } from '@scure/bip39';
|
|
19
|
-
import { wordlist } from '@scure/bip39/wordlists/english.js';
|
|
20
|
-
import { existsSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
21
|
-
import { keyFilePath } from './crypto.js';
|
|
22
|
-
const KEY_BYTES = 32;
|
|
23
|
-
/** Encode the current master key as a 24-word BIP39 phrase. */
|
|
24
|
-
export function keyToMnemonic() {
|
|
25
|
-
const path = keyFilePath();
|
|
26
|
-
if (!existsSync(path)) {
|
|
27
|
-
throw new Error(`Master key not found at ${path}. Run \`wyrm cloud login\` first to generate one.`);
|
|
28
|
-
}
|
|
29
|
-
const buf = readFileSync(path);
|
|
30
|
-
if (buf.length !== KEY_BYTES) {
|
|
31
|
-
throw new Error(`Master key at ${path} has unexpected length ${buf.length} (expected ${KEY_BYTES}).`);
|
|
32
|
-
}
|
|
33
|
-
return entropyToMnemonic(new Uint8Array(buf), wordlist);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Restore a master key from a 24-word phrase. Refuses to overwrite an
|
|
37
|
-
* existing key unless `overwrite` is true.
|
|
38
|
-
*/
|
|
39
|
-
export function mnemonicToKey(phrase, opts = {}) {
|
|
40
|
-
const normalised = phrase.trim().toLowerCase().split(/\s+/).join(' ');
|
|
41
|
-
if (!validateMnemonic(normalised, wordlist)) {
|
|
42
|
-
throw new Error('Recovery phrase failed BIP39 checksum. Re-check spelling and word order (24 words from the BIP39 English list).');
|
|
43
|
-
}
|
|
44
|
-
const entropy = mnemonicToEntropy(normalised, wordlist);
|
|
45
|
-
if (entropy.length !== KEY_BYTES) {
|
|
46
|
-
throw new Error(`Recovery phrase decoded to ${entropy.length} bytes (expected ${KEY_BYTES}). Use a 24-word phrase.`);
|
|
47
|
-
}
|
|
48
|
-
const path = keyFilePath();
|
|
49
|
-
const overwrote = existsSync(path);
|
|
50
|
-
if (overwrote && !opts.overwrite) {
|
|
51
|
-
throw new Error(`Master key already exists at ${path}. Pass --overwrite to replace it (this WILL invalidate decryption of any blobs encrypted with the old key).`);
|
|
52
|
-
}
|
|
53
|
-
writeFileSync(path, Buffer.from(entropy), { mode: 0o600 });
|
|
54
|
-
try {
|
|
55
|
-
chmodSync(path, 0o600);
|
|
56
|
-
}
|
|
57
|
-
catch { /* best-effort */ }
|
|
58
|
-
return { path, overwrote };
|
|
59
|
-
}
|
|
60
|
-
//# sourceMappingURL=recovery.js.map
|
|
1
|
+
import{entropyToMnemonic as d,mnemonicToEntropy as l,validateMnemonic as y}from"@scure/bip39";import{wordlist as i}from"@scure/bip39/wordlists/english.js";import{existsSync as a,readFileSync as m,writeFileSync as p,chmodSync as w}from"node:fs";import{keyFilePath as h}from"./crypto.js";const t=32;function k(){const e=h();if(!a(e))throw new Error(`Master key not found at ${e}. Run \`wyrm cloud login\` first to generate one.`);const o=m(e);if(o.length!==t)throw new Error(`Master key at ${e} has unexpected length ${o.length} (expected ${t}).`);return d(new Uint8Array(o),i)}function v(e,o={}){const c=e.trim().toLowerCase().split(/\s+/).join(" ");if(!y(c,i))throw new Error("Recovery phrase failed BIP39 checksum. Re-check spelling and word order (24 words from the BIP39 English list).");const n=l(c,i);if(n.length!==t)throw new Error(`Recovery phrase decoded to ${n.length} bytes (expected ${t}). Use a 24-word phrase.`);const r=h(),s=a(r);if(s&&!o.overwrite)throw new Error(`Master key already exists at ${r}. Pass --overwrite to replace it (this WILL invalidate decryption of any blobs encrypted with the old key).`);p(r,Buffer.from(n),{mode:384});try{w(r,384)}catch{}return{path:r,overwrote:s}}export{k as keyToMnemonic,v as mnemonicToKey};
|