@wipcomputer/wip-ldm-os 0.4.85-alpha.7 → 0.4.85-alpha.9

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/lib/deploy.mjs CHANGED
@@ -1258,6 +1258,58 @@ function copySkillTree(skillDir, dest, copyFullFolder = false) {
1258
1258
  }
1259
1259
  }
1260
1260
 
1261
+ const SKILL_COMPANION_DIRS = ['references', 'agents', 'scripts', 'assets'];
1262
+
1263
+ function listSkillCopyEntries(skillDir, copyFullFolder = false) {
1264
+ if (copyFullFolder) {
1265
+ try {
1266
+ return readdirSync(skillDir)
1267
+ .filter((entry) => entry !== '.DS_Store')
1268
+ .sort();
1269
+ } catch {
1270
+ return ['SKILL.md'];
1271
+ }
1272
+ }
1273
+
1274
+ const entries = ['SKILL.md'];
1275
+ for (const child of SKILL_COMPANION_DIRS) {
1276
+ if (existsSync(join(skillDir, child))) entries.push(`${child}/`);
1277
+ }
1278
+ return entries;
1279
+ }
1280
+
1281
+ function printSkillDryRunPlan({ sourceSkillDir, refsSrc, toolName, harnesses, workspace, entries }) {
1282
+ const formatTargetEntries = (base) => entries.map((entry) => join(base, entry));
1283
+ const harnessTargets = Object.entries(harnesses)
1284
+ .filter(([, h]) => h.detected && h.skills)
1285
+ .map(([name, h]) => ({ name, base: join(h.skills, toolName) }));
1286
+
1287
+ ok(`Skill: ${toolName}`);
1288
+ log(`Source: ${sourceSkillDir}`);
1289
+ log('Would copy:');
1290
+ for (const entry of entries) log(`- ${entry}`);
1291
+
1292
+ const permanentBase = join(LDM_EXTENSIONS, toolName);
1293
+ log('Permanent copy:');
1294
+ for (const target of formatTargetEntries(permanentBase)) log(`- ${target}`);
1295
+
1296
+ if (harnessTargets.length > 0) {
1297
+ log('Agent skill targets:');
1298
+ for (const target of harnessTargets) {
1299
+ log(`- ${target.name}: ${target.base}`);
1300
+ for (const entryTarget of formatTargetEntries(target.base)) log(` - ${entryTarget}`);
1301
+ }
1302
+ } else {
1303
+ log('Agent skill targets: no detected skill harnesses');
1304
+ }
1305
+
1306
+ if (existsSync(refsSrc) && workspace && existsSync(workspace)) {
1307
+ const homeRefsDest = join(workspace, 'settings', 'docs', 'skills', toolName);
1308
+ log('Workspace docs target:');
1309
+ log(`- ${homeRefsDest} (references/ only)`);
1310
+ }
1311
+ }
1312
+
1261
1313
  function installSkillFolder(skillDir, toolName, opts = {}) {
1262
1314
  const { harnesses, workspace } = getHarnesses();
1263
1315
 
@@ -1281,17 +1333,8 @@ function installSkillFolder(skillDir, toolName, opts = {}) {
1281
1333
  if (!existsSync(refsSrc) && existsSync(permanentRefs)) refsSrc = permanentRefs;
1282
1334
 
1283
1335
  if (DRY_RUN) {
1284
- const targets = Object.entries(harnesses)
1285
- .filter(([,h]) => h.detected && h.skills)
1286
- .map(([,h]) => join(h.skills, toolName));
1287
- ok(`Skill: ${toolName}`);
1288
- log(`Source: ${sourceSkillDir}`);
1289
- if (targets.length > 0) {
1290
- log(`Targets:`);
1291
- for (const target of targets) log(`- ${target}`);
1292
- } else {
1293
- log(`Targets: no detected skill harnesses`);
1294
- }
1336
+ const entries = listSkillCopyEntries(sourceSkillDir, opts.copyFullFolder);
1337
+ printSkillDryRunPlan({ sourceSkillDir, refsSrc, toolName, harnesses, workspace, entries });
1295
1338
  return true;
1296
1339
  }
1297
1340
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.85-alpha.7",
3
+ "version": "0.4.85-alpha.9",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {
@@ -24,11 +24,13 @@
24
24
  "test:installer-update-tracks": "node scripts/test-installer-update-tracks.mjs",
25
25
  "test:installer-hook-toolname": "node scripts/test-installer-hook-toolname.mjs",
26
26
  "test:installer-skill-directory": "node scripts/test-installer-skill-directory.mjs",
27
+ "test:installer-skill-dry-run-destinations": "node scripts/test-installer-skill-dry-run-destinations.mjs",
27
28
  "test:ldm-install-bin-shim": "node scripts/test-ldm-install-preserves-foreign-bin.mjs",
28
29
  "test:doctor-cron-target": "node scripts/test-doctor-cron-target.mjs",
29
30
  "test:bin-manifest": "node scripts/test-bin-manifest.mjs",
30
31
  "test:crc-agentid-tenant-boundary": "node scripts/test-crc-agentid-tenant-boundary.mjs",
31
32
  "test:crc-pair-login-flow": "node scripts/test-crc-pair-login-flow.mjs",
33
+ "test:crc-pair-status-poll-token": "node scripts/test-crc-pair-status-poll-token.mjs",
32
34
  "test:crc-e2ee-session-route": "node scripts/test-crc-e2ee-session-route.mjs",
33
35
  "fmt": "npx prettier --write 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'",
34
36
  "fmt:check": "npx prettier --check 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'"
@@ -0,0 +1,73 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const server = readFileSync("src/hosted-mcp/server.mjs", "utf8");
4
+
5
+ function assertContains(needle, label) {
6
+ if (!server.includes(needle)) {
7
+ throw new Error(`${label} missing expected text: ${needle}`);
8
+ }
9
+ }
10
+
11
+ function assertNotContains(needle, label) {
12
+ if (server.includes(needle)) {
13
+ throw new Error(`${label} still contains forbidden text: ${needle}`);
14
+ }
15
+ }
16
+
17
+ assertContains("function generateCodexPairPollToken()", "pair poll token generator");
18
+ assertContains('return "ppt_" + randomBytes(32).toString("base64url");', "pair poll token entropy");
19
+ assertContains("function getBearerToken(req)", "bearer token helper");
20
+ assertContains("const pollToken = generateCodexPairPollToken();", "pair-init mints poll token");
21
+ assertContains("poll_token: pollToken,", "pair state stores poll token");
22
+ assertContains("poll_token_used: false,", "pair state tracks token consumption");
23
+ assertContains("pair_poll_token: pollToken,", "pair-init returns poll token to daemon");
24
+ assertContains('json(res, 401, { error: "pair_poll_token_expired" });', "expired token rejected");
25
+ assertContains('json(res, 401, { error: "invalid_pair_poll_token" });', "missing or wrong token rejected");
26
+ assertContains("if (!pollToken || pollToken !== p.poll_token || p.poll_token_used)", "pair-status validates token");
27
+ assertContains("p.poll_token_used = true;", "completed credential response consumes token");
28
+
29
+ function pairStatusModel(pair, bearer, now) {
30
+ if (now > pair.expires) return { code: 401, body: { error: "pair_poll_token_expired" } };
31
+ if (!bearer || bearer !== pair.poll_token || pair.poll_token_used) {
32
+ return { code: 401, body: { error: "invalid_pair_poll_token" } };
33
+ }
34
+ if (pair.status === "completed") {
35
+ pair.poll_token_used = true;
36
+ return { code: 200, body: { status: "completed", api_key: pair.apiKey, handle: pair.handle } };
37
+ }
38
+ return { code: 200, body: { status: pair.status } };
39
+ }
40
+
41
+ const pair = {
42
+ status: "pending",
43
+ expires: 10_000,
44
+ poll_token: "ppt_good",
45
+ poll_token_used: false,
46
+ apiKey: "ck_secret",
47
+ handle: "Parker",
48
+ };
49
+
50
+ if (pairStatusModel({ ...pair }, null, 1).code !== 401) {
51
+ throw new Error("missing poll token should fail");
52
+ }
53
+ if (pairStatusModel({ ...pair }, "ppt_wrong", 1).code !== 401) {
54
+ throw new Error("wrong poll token should fail");
55
+ }
56
+ if (pairStatusModel({ ...pair }, "ppt_good", 20_000).code !== 401) {
57
+ throw new Error("expired poll token should fail");
58
+ }
59
+ if (pairStatusModel({ ...pair }, "ppt_good", 1).body.status !== "pending") {
60
+ throw new Error("correct poll token should return pending before completion");
61
+ }
62
+
63
+ const completedPair = { ...pair, status: "completed" };
64
+ const completed = pairStatusModel(completedPair, "ppt_good", 1);
65
+ if (completed.code !== 200 || completed.body.api_key !== "ck_secret") {
66
+ throw new Error("correct poll token should return completed credential once");
67
+ }
68
+ const replay = pairStatusModel(completedPair, "ppt_good", 1);
69
+ if (replay.code !== 401) {
70
+ throw new Error("reused completed poll token should fail");
71
+ }
72
+
73
+ console.log("crc pair-status poll token checks passed");
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+
6
+ const home = mkdtempSync(join(tmpdir(), 'ldm-skill-dry-run-home-'));
7
+ const source = mkdtempSync(join(tmpdir(), 'ldm-skill-dry-run-source-'));
8
+
9
+ function assert(condition, message) {
10
+ if (!condition) throw new Error(message);
11
+ }
12
+
13
+ try {
14
+ process.env.HOME = home;
15
+
16
+ for (const dir of ['.claude', '.openclaw', '.codex', '.agents']) {
17
+ mkdirSync(join(home, dir), { recursive: true });
18
+ }
19
+
20
+ const workspace = join(home, 'workspace');
21
+ mkdirSync(workspace, { recursive: true });
22
+ mkdirSync(join(home, '.ldm'), { recursive: true });
23
+ writeFileSync(join(home, '.ldm', 'config.json'), JSON.stringify({
24
+ workspace,
25
+ harnesses: {
26
+ 'claude-code': {
27
+ detected: true,
28
+ home: join(home, '.claude'),
29
+ skills: join(home, '.claude', 'skills'),
30
+ },
31
+ openclaw: {
32
+ detected: true,
33
+ home: join(home, '.openclaw'),
34
+ skills: join(home, '.openclaw', 'skills'),
35
+ },
36
+ codex: {
37
+ detected: true,
38
+ home: join(home, '.codex'),
39
+ skills: join(home, '.codex', 'skills'),
40
+ },
41
+ 'wip-agents': {
42
+ detected: true,
43
+ home: join(home, '.agents'),
44
+ skills: join(home, '.agents', 'skills'),
45
+ },
46
+ },
47
+ }, null, 2));
48
+
49
+ mkdirSync(join(source, 'references'), { recursive: true });
50
+ mkdirSync(join(source, 'agents'), { recursive: true });
51
+ writeFileSync(join(source, 'package.json'), JSON.stringify({
52
+ name: '@wipcomputer/wip-ai-chat-ui',
53
+ version: '0.1.1',
54
+ }, null, 2));
55
+ writeFileSync(join(source, 'SKILL.md'), '---\nname: wip-ai-chat-ui\ndescription: "test skill"\n---\n\n# Test Skill\n');
56
+ writeFileSync(join(source, 'references', 'stack.md'), '# Stack\n');
57
+ writeFileSync(join(source, 'agents', 'openai.yaml'), 'display_name: "WIP AI Chat UI"\n');
58
+
59
+ const { setFlags, installFromPath } = await import('../lib/deploy.mjs');
60
+
61
+ const lines = [];
62
+ const originalLog = console.log;
63
+ console.log = (...args) => lines.push(args.join(' '));
64
+
65
+ try {
66
+ setFlags({ dryRun: true, jsonOutput: false });
67
+ const result = await installFromPath(source);
68
+ assert(result.interfaces === 1, 'dry run should process one skill interface');
69
+ } finally {
70
+ console.log = originalLog;
71
+ }
72
+
73
+ const output = lines.join('\n');
74
+
75
+ for (const expected of [
76
+ 'Would copy:',
77
+ '- SKILL.md',
78
+ '- references/',
79
+ '- agents/',
80
+ 'Permanent copy:',
81
+ join(home, '.ldm', 'extensions', 'wip-ai-chat-ui', 'SKILL.md'),
82
+ join(home, '.ldm', 'extensions', 'wip-ai-chat-ui', 'references/'),
83
+ 'Agent skill targets:',
84
+ `claude-code: ${join(home, '.claude', 'skills', 'wip-ai-chat-ui')}`,
85
+ join(home, '.claude', 'skills', 'wip-ai-chat-ui', 'SKILL.md'),
86
+ `openclaw: ${join(home, '.openclaw', 'skills', 'wip-ai-chat-ui')}`,
87
+ join(home, '.openclaw', 'skills', 'wip-ai-chat-ui', 'references/'),
88
+ `codex: ${join(home, '.codex', 'skills', 'wip-ai-chat-ui')}`,
89
+ `wip-agents: ${join(home, '.agents', 'skills', 'wip-ai-chat-ui')}`,
90
+ 'Workspace docs target:',
91
+ `${join(workspace, 'settings', 'docs', 'skills', 'wip-ai-chat-ui')} (references/ only)`,
92
+ ]) {
93
+ assert(output.includes(expected), `dry-run output should include ${expected}\n\n${output}`);
94
+ }
95
+
96
+ console.log('installer skill dry-run destinations regression passed');
97
+ } finally {
98
+ rmSync(home, { recursive: true, force: true });
99
+ rmSync(source, { recursive: true, force: true });
100
+ }
@@ -2599,7 +2599,7 @@ const httpServer = createServer(async (req, res) => {
2599
2599
 
2600
2600
  const CODEX_PAIR_EXPIRY_MS = 5 * 60 * 1000;
2601
2601
  const CODEX_PAIR_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
2602
- const codexPairings = {}; // pairing_id -> { code, status, expires, daemon_info, apiKey?, agentId?, handle?, daemon_public_key?, crypto_versions? }
2602
+ const codexPairings = {}; // pairing_id -> { code, status, expires, poll_token, poll_token_used?, daemon_info, apiKey?, agentId?, handle?, daemon_public_key?, crypto_versions? }
2603
2603
  const codexPairingByCode = {}; // code -> pairing_id (only while pending)
2604
2604
  const codexDaemons = new Map(); // agentId -> ws
2605
2605
  const codexWebClients = new Map(); // `${agentId}:${threadId}` -> Set<ws>
@@ -2689,16 +2689,30 @@ function generateCodexPairingCode() {
2689
2689
  throw new Error("Could not generate unique codex-relay pairing code");
2690
2690
  }
2691
2691
 
2692
+ function generateCodexPairPollToken() {
2693
+ return "ppt_" + randomBytes(32).toString("base64url");
2694
+ }
2695
+
2696
+ function getBearerToken(req) {
2697
+ const auth = req.headers["authorization"];
2698
+ if (typeof auth !== "string" || !auth.startsWith("Bearer ")) return null;
2699
+ const token = auth.slice(7).trim();
2700
+ return token || null;
2701
+ }
2702
+
2692
2703
  async function handleCodexPairInit(req, res) {
2693
2704
  let body = {};
2694
2705
  try { body = (await readBody(req)) || {}; } catch {}
2695
2706
  const code = generateCodexPairingCode();
2696
2707
  const pairingId = randomUUID();
2708
+ const pollToken = generateCodexPairPollToken();
2697
2709
  const expires = Date.now() + CODEX_PAIR_EXPIRY_MS;
2698
2710
  codexPairings[pairingId] = {
2699
2711
  code,
2700
2712
  status: "pending",
2701
2713
  expires,
2714
+ poll_token: pollToken,
2715
+ poll_token_used: false,
2702
2716
  daemon_info: {
2703
2717
  hostname: typeof body.hostname === "string" ? body.hostname.slice(0, 64) : null,
2704
2718
  platform: typeof body.platform === "string" ? body.platform.slice(0, 32) : null,
@@ -2721,6 +2735,7 @@ async function handleCodexPairInit(req, res) {
2721
2735
  json(res, 200, {
2722
2736
  code,
2723
2737
  pairing_id: pairingId,
2738
+ pair_poll_token: pollToken,
2724
2739
  web_url: ISSUER_URL + "/login?next=" + encodeURIComponent("/pair/" + code),
2725
2740
  expires_at: new Date(expires).toISOString(),
2726
2741
  });
@@ -2729,11 +2744,19 @@ async function handleCodexPairInit(req, res) {
2729
2744
  function handleCodexPairStatus(req, res, pairingId) {
2730
2745
  const p = codexPairings[pairingId];
2731
2746
  if (!p) { json(res, 404, { error: "pairing not found" }); return; }
2732
- if (p.status === "pending" && Date.now() > p.expires) {
2747
+ if (Date.now() > p.expires) {
2733
2748
  p.status = "expired";
2734
2749
  if (codexPairingByCode[p.code] === pairingId) delete codexPairingByCode[p.code];
2750
+ json(res, 401, { error: "pair_poll_token_expired" });
2751
+ return;
2752
+ }
2753
+ const pollToken = getBearerToken(req);
2754
+ if (!pollToken || pollToken !== p.poll_token || p.poll_token_used) {
2755
+ json(res, 401, { error: "invalid_pair_poll_token" });
2756
+ return;
2735
2757
  }
2736
2758
  if (p.status === "completed") {
2759
+ p.poll_token_used = true;
2737
2760
  json(res, 200, { status: "completed", api_key: p.apiKey, handle: p.handle || p.agentId });
2738
2761
  } else {
2739
2762
  json(res, 200, { status: p.status });