@aikdna/kdna-cli 0.16.10 → 0.18.0
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/README.md +158 -75
- package/package.json +5 -5
- package/skills/kdna-loader/SKILL.md +5 -6
- package/src/agent.js +489 -79
- package/src/cli.js +112 -62
- package/src/cmds/_common.js +32 -16
- package/src/cmds/badge.js +7 -7
- package/src/cmds/changelog.js +1 -1
- package/src/cmds/cluster.js +16 -48
- package/src/cmds/doctor.js +10 -27
- package/src/cmds/domain.js +213 -443
- package/src/cmds/explain.js +122 -0
- package/src/cmds/legacy.js +8 -8
- package/src/cmds/license.js +483 -26
- package/src/cmds/quality.js +14 -2
- package/src/cmds/registry.js +15 -67
- package/src/cmds/studio.js +4 -5
- package/src/cmds/test.js +4 -4
- package/src/cmds/trace.js +11 -7
- package/src/compare.js +28 -22
- package/src/diff.js +11 -13
- package/src/init.js +2 -2
- package/src/install.js +138 -460
- package/src/loader.js +10 -10
- package/src/package-store.js +229 -0
- package/src/paths.js +44 -0
- package/src/publish.js +184 -22
- package/src/registry.js +76 -9
- package/src/setup.js +19 -20
- package/src/verify.js +275 -121
- package/templates/standard-domain/kdna.json +2 -1
- package/validators/kdna-lint.js +37 -3
- package/validators/kdna-validate.js +3 -2
- package/src/cmds/encrypt.js +0 -199
package/validators/kdna-lint.js
CHANGED
|
@@ -10,6 +10,15 @@ const fs = require('fs');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const { lintDomain } = require('@aikdna/kdna-core');
|
|
12
12
|
|
|
13
|
+
const KDNA_DOMAIN_FILES = new Set([
|
|
14
|
+
'KDNA_Core.json',
|
|
15
|
+
'KDNA_Patterns.json',
|
|
16
|
+
'KDNA_Scenarios.json',
|
|
17
|
+
'KDNA_Cases.json',
|
|
18
|
+
'KDNA_Reasoning.json',
|
|
19
|
+
'KDNA_Evolution.json',
|
|
20
|
+
]);
|
|
21
|
+
|
|
13
22
|
const domainDir = process.argv[2];
|
|
14
23
|
if (!domainDir || domainDir === '--help' || domainDir === '-h') {
|
|
15
24
|
console.log(`kdna-lint — Structural and content validation for KDNA domains.
|
|
@@ -28,19 +37,44 @@ if (!fs.existsSync(domainDir) || !fs.statSync(domainDir).isDirectory()) {
|
|
|
28
37
|
process.exit(2);
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
// Read
|
|
32
|
-
|
|
40
|
+
// Read only canonical domain content files. Governance metadata such as
|
|
41
|
+
// KDNA_CARD.json is part of the package, but not one of the 6 KDNA JSON files.
|
|
42
|
+
const files = fs.readdirSync(domainDir).filter((f) => KDNA_DOMAIN_FILES.has(f));
|
|
33
43
|
const dataMap = {};
|
|
34
44
|
for (const f of files) {
|
|
35
45
|
try {
|
|
36
46
|
dataMap[f] = JSON.parse(fs.readFileSync(path.join(domainDir, f), 'utf8'));
|
|
37
|
-
} catch
|
|
47
|
+
} catch {
|
|
38
48
|
// lintDomain will report missing required files; skip unparseable here
|
|
39
49
|
}
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
const result = lintDomain(dataMap);
|
|
43
53
|
|
|
54
|
+
// Also validate kdna.json manifest if present and validateManifest is available
|
|
55
|
+
let manifestPath;
|
|
56
|
+
try {
|
|
57
|
+
manifestPath = path.join(domainDir, 'kdna.json');
|
|
58
|
+
if (fs.existsSync(manifestPath)) {
|
|
59
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
60
|
+
let validateManifestFn;
|
|
61
|
+
try {
|
|
62
|
+
validateManifestFn = require('@aikdna/kdna-core').validateManifest;
|
|
63
|
+
} catch {
|
|
64
|
+
// validateManifest not yet available in installed kdna-core — skip manifest check
|
|
65
|
+
}
|
|
66
|
+
if (validateManifestFn) {
|
|
67
|
+
const mResult = validateManifestFn(manifest);
|
|
68
|
+
for (const e of mResult.errors) result.errors.push(`kdna.json: ${e}`);
|
|
69
|
+
for (const w of mResult.warnings) result.warnings.push(`kdna.json: ${w}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e.code !== 'ENOENT') {
|
|
74
|
+
result.errors.push(`kdna.json: failed to parse — ${e.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
44
78
|
if (result.warnings.length) {
|
|
45
79
|
console.log('Warnings:');
|
|
46
80
|
result.warnings.forEach((w) => console.log(` - ${w}`));
|
|
@@ -42,7 +42,8 @@ const FILE_MAP = {
|
|
|
42
42
|
'KDNA_Evolution.json': 'KDNA_Evolution.schema.json',
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
// Read
|
|
45
|
+
// Read only canonical domain content files. Governance metadata such as
|
|
46
|
+
// KDNA_CARD.json is valid package metadata, but not part of the 6-file domain set.
|
|
46
47
|
const dataMap = {};
|
|
47
48
|
for (const [file] of Object.entries(FILE_MAP)) {
|
|
48
49
|
const filePath = path.join(domainDir, file);
|
|
@@ -57,7 +58,7 @@ for (const [file] of Object.entries(FILE_MAP)) {
|
|
|
57
58
|
|
|
58
59
|
// Read schemas
|
|
59
60
|
const schemaMap = {};
|
|
60
|
-
for (const
|
|
61
|
+
for (const schemaFile of Object.values(FILE_MAP)) {
|
|
61
62
|
const schemaPath = path.join(SCHEMA_DIR, schemaFile);
|
|
62
63
|
if (fs.existsSync(schemaPath)) {
|
|
63
64
|
try {
|
package/src/cmds/encrypt.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* KDNA Encryption — Encrypted container format (.kdnae) for licensed domains.
|
|
3
|
-
*
|
|
4
|
-
* .kdnae extends .kdna with AES-256-GCM encryption on KDNA JSON files.
|
|
5
|
-
* The kdna.json manifest and license.json stay in plaintext for discovery.
|
|
6
|
-
*
|
|
7
|
-
* Encryption key is derived via PBKDF2 from:
|
|
8
|
-
* license_key + machine_fingerprint
|
|
9
|
-
*
|
|
10
|
-
* This module provides pure-crypto functions (no filesystem I/O).
|
|
11
|
-
* File operations are in domain.js (pack/unpack) and install.js.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const crypto = require('crypto');
|
|
15
|
-
|
|
16
|
-
const ALGORITHM = 'aes-256-gcm';
|
|
17
|
-
const IV_LENGTH = 16; // 96-bit IV for GCM
|
|
18
|
-
const TAG_LENGTH = 16; // 128-bit auth tag
|
|
19
|
-
const SALT_LENGTH = 32;
|
|
20
|
-
const PBKDF2_ITERATIONS = 600000;
|
|
21
|
-
const KEY_LENGTH = 32; // AES-256
|
|
22
|
-
|
|
23
|
-
// Files in a .kdna container that get encrypted (JSON content only)
|
|
24
|
-
const ENCRYPTED_FILES = [
|
|
25
|
-
'KDNA_Core.json',
|
|
26
|
-
'KDNA_Patterns.json',
|
|
27
|
-
'KDNA_Scenarios.json',
|
|
28
|
-
'KDNA_Cases.json',
|
|
29
|
-
'KDNA_Reasoning.json',
|
|
30
|
-
'KDNA_Evolution.json',
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
// Files that always stay in plaintext
|
|
34
|
-
const PLAINTEXT_FILES = ['kdna.json', 'license.json', 'README.md', 'LICENSE', 'signature.json'];
|
|
35
|
-
|
|
36
|
-
function isEncryptable(filename) {
|
|
37
|
-
return ENCRYPTED_FILES.includes(filename);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ─── Machine Fingerprint ────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
function machineFingerprint() {
|
|
43
|
-
const os = require('os');
|
|
44
|
-
const parts = [os.hostname(), os.userInfo().uid.toString(), os.platform(), os.arch()];
|
|
45
|
-
// Try to get hardware UUID on macOS
|
|
46
|
-
try {
|
|
47
|
-
const { execSync } = require('child_process');
|
|
48
|
-
if (os.platform() === 'darwin') {
|
|
49
|
-
const uuid = execSync('ioreg -d2 -c IOPlatformExpertDevice | grep IOPlatformUUID', {
|
|
50
|
-
encoding: 'utf8',
|
|
51
|
-
timeout: 3000,
|
|
52
|
-
})
|
|
53
|
-
.match(/"[A-F0-9-]{36}"/)?.[0]
|
|
54
|
-
?.replace(/"/g, '');
|
|
55
|
-
if (uuid) parts.push(uuid);
|
|
56
|
-
}
|
|
57
|
-
if (os.platform() === 'linux') {
|
|
58
|
-
try {
|
|
59
|
-
const mid = require('fs').readFileSync('/etc/machine-id', 'utf8').trim();
|
|
60
|
-
if (mid) parts.push(mid);
|
|
61
|
-
} catch {
|
|
62
|
-
/* ignore */
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
} catch {
|
|
66
|
-
/* non-critical */
|
|
67
|
-
}
|
|
68
|
-
return crypto.createHash('sha256').update(parts.join('|')).digest('hex');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ─── Key Derivation ─────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
function deriveKey(licenseKey, fingerprint) {
|
|
74
|
-
const salt = crypto
|
|
75
|
-
.createHash('sha256')
|
|
76
|
-
.update(fingerprint || machineFingerprint())
|
|
77
|
-
.digest();
|
|
78
|
-
return crypto.pbkdf2Sync(licenseKey, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha512');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ─── Encrypt / Decrypt ──────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
function encrypt(plaintext, key) {
|
|
84
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
85
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
86
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
87
|
-
const tag = cipher.getAuthTag();
|
|
88
|
-
// Format: iv (16) + tag (16) + ciphertext
|
|
89
|
-
return Buffer.concat([iv, tag, encrypted]);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function decrypt(encryptedData, key) {
|
|
93
|
-
if (encryptedData.length < IV_LENGTH + TAG_LENGTH) {
|
|
94
|
-
throw new Error('Invalid encrypted data: too short');
|
|
95
|
-
}
|
|
96
|
-
const iv = encryptedData.subarray(0, IV_LENGTH);
|
|
97
|
-
const tag = encryptedData.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
98
|
-
const ciphertext = encryptedData.subarray(IV_LENGTH + TAG_LENGTH);
|
|
99
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
100
|
-
decipher.setAuthTag(tag);
|
|
101
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ─── Encrypted Container Detection ──────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
function isEncryptedContainer(filePath) {
|
|
107
|
-
return filePath.endsWith('.kdnae');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ─── License File ──────────────────────────────────────────────────────
|
|
111
|
-
|
|
112
|
-
function createLicense(domain, options = {}) {
|
|
113
|
-
const {
|
|
114
|
-
issuedTo = 'licensee@example.com',
|
|
115
|
-
expiresAt = null, // ISO date or null for perpetual
|
|
116
|
-
maxAgents = 1,
|
|
117
|
-
requireMachineBinding = true,
|
|
118
|
-
requireOnlineCheck = false,
|
|
119
|
-
offlineGraceDays = 7,
|
|
120
|
-
} = options;
|
|
121
|
-
|
|
122
|
-
const license = {
|
|
123
|
-
version: '1.0',
|
|
124
|
-
license_id: `lic_${crypto.randomBytes(8).toString('hex')}`,
|
|
125
|
-
domain,
|
|
126
|
-
issued_to: issuedTo,
|
|
127
|
-
issued_at: new Date().toISOString(),
|
|
128
|
-
expires_at: expiresAt,
|
|
129
|
-
max_agents: maxAgents,
|
|
130
|
-
require_machine_binding: requireMachineBinding,
|
|
131
|
-
require_online_check: requireOnlineCheck,
|
|
132
|
-
offline_grace_days: offlineGraceDays,
|
|
133
|
-
allowed_agents: options.allowedAgents || ['claude_code', 'codex', 'opencode'],
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
return license;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function verifyLicense(license, scopePubkey, fingerprint) {
|
|
140
|
-
const issues = [];
|
|
141
|
-
|
|
142
|
-
// Check expiration
|
|
143
|
-
if (license.expires_at) {
|
|
144
|
-
if (new Date(license.expires_at) < new Date()) {
|
|
145
|
-
issues.push('License has expired');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Check machine binding
|
|
150
|
-
if (license.require_machine_binding) {
|
|
151
|
-
if (license.machine_fingerprint && license.machine_fingerprint !== fingerprint) {
|
|
152
|
-
issues.push('Machine fingerprint mismatch');
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
valid: issues.length === 0,
|
|
158
|
-
issues,
|
|
159
|
-
domain: license.domain,
|
|
160
|
-
license_id: license.license_id,
|
|
161
|
-
issued_to: license.issued_to,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function signLicense(license, privateKeyPem) {
|
|
166
|
-
const { signature: _, ...payload } = license;
|
|
167
|
-
const data = JSON.stringify(payload, Object.keys(payload).sort());
|
|
168
|
-
// Use crypto.sign() for Ed25519 (supported via PEM keys)
|
|
169
|
-
const sig = crypto.sign(null, Buffer.from(data), privateKeyPem);
|
|
170
|
-
license.signature = `ed25519:${sig.toString('hex')}`;
|
|
171
|
-
return license;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function verifyLicenseSignature(license, publicKeyPem) {
|
|
175
|
-
const signature = license.signature?.replace('ed25519:', '') || '';
|
|
176
|
-
if (!signature) return false;
|
|
177
|
-
const { signature: _, ...payload } = license;
|
|
178
|
-
const data = JSON.stringify(payload, Object.keys(payload).sort());
|
|
179
|
-
try {
|
|
180
|
-
return crypto.verify(null, Buffer.from(data), publicKeyPem, Buffer.from(signature, 'hex'));
|
|
181
|
-
} catch {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
module.exports = {
|
|
187
|
-
encrypt,
|
|
188
|
-
decrypt,
|
|
189
|
-
deriveKey,
|
|
190
|
-
machineFingerprint,
|
|
191
|
-
isEncryptable,
|
|
192
|
-
isEncryptedContainer,
|
|
193
|
-
createLicense,
|
|
194
|
-
verifyLicense,
|
|
195
|
-
signLicense,
|
|
196
|
-
verifyLicenseSignature,
|
|
197
|
-
ENCRYPTED_FILES,
|
|
198
|
-
PLAINTEXT_FILES,
|
|
199
|
-
};
|