@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
|
|
1285
|
-
|
|
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.
|
|
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 (
|
|
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 });
|