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
@@ -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};
@@ -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};
@@ -1,113 +1,2 @@
1
- /**
2
- * Per-machine fingerprint for Wyrm Cloud session integrity.
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};
@@ -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};