cognitive-modules-cli 2.2.5 → 2.2.8
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/CHANGELOG.md +7 -1
- package/README.md +25 -3
- package/dist/audit.d.ts +13 -0
- package/dist/audit.js +25 -0
- package/dist/cli.js +188 -3
- package/dist/commands/add.js +232 -7
- package/dist/commands/compose.d.ts +2 -0
- package/dist/commands/compose.js +60 -1
- package/dist/commands/core.d.ts +31 -0
- package/dist/commands/core.js +338 -0
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.js +1 -0
- package/dist/commands/pipe.js +45 -2
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +136 -31
- package/dist/commands/search.js +13 -3
- package/dist/commands/update.js +4 -1
- package/dist/errors/index.d.ts +7 -0
- package/dist/errors/index.js +48 -40
- package/dist/modules/composition.d.ts +15 -2
- package/dist/modules/composition.js +16 -6
- package/dist/modules/loader.d.ts +10 -0
- package/dist/modules/loader.js +168 -0
- package/dist/modules/runner.d.ts +10 -6
- package/dist/modules/runner.js +130 -16
- package/dist/profile.d.ts +8 -0
- package/dist/profile.js +59 -0
- package/dist/provenance.d.ts +50 -0
- package/dist/provenance.js +137 -0
- package/dist/registry/assets.d.ts +48 -0
- package/dist/registry/assets.js +723 -0
- package/dist/registry/client.d.ts +20 -5
- package/dist/registry/client.js +87 -30
- package/dist/registry/tar.d.ts +8 -0
- package/dist/registry/tar.js +353 -0
- package/dist/server/http.js +167 -42
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/sse.d.ts +13 -0
- package/dist/server/sse.js +22 -0
- package/dist/types.d.ts +31 -0
- package/package.json +1 -1
package/dist/modules/runner.js
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import _Ajv from 'ajv';
|
|
7
7
|
const Ajv = _Ajv.default || _Ajv;
|
|
8
|
+
import * as fs from 'node:fs/promises';
|
|
9
|
+
import * as path from 'node:path';
|
|
8
10
|
import { aggregateRisk, isV22Envelope } from '../types.js';
|
|
11
|
+
import { readModuleProvenance, verifyModuleIntegrity } from '../provenance.js';
|
|
9
12
|
// =============================================================================
|
|
10
13
|
// Schema Validation
|
|
11
14
|
// =============================================================================
|
|
@@ -1046,12 +1049,113 @@ function convertLegacyToEnvelope(data, isError = false) {
|
|
|
1046
1049
|
};
|
|
1047
1050
|
}
|
|
1048
1051
|
}
|
|
1052
|
+
async function enforcePolicyGates(module, policy) {
|
|
1053
|
+
if (!policy)
|
|
1054
|
+
return null;
|
|
1055
|
+
if (policy.requireV22) {
|
|
1056
|
+
const fv = module.formatVersion ?? 'unknown';
|
|
1057
|
+
if (fv !== 'v2.2') {
|
|
1058
|
+
return makeErrorResponse({
|
|
1059
|
+
code: 'E4007', // PERMISSION_DENIED
|
|
1060
|
+
message: `Certified policy requires v2.2 modules; got: ${fv} (${module.format})`,
|
|
1061
|
+
explain: 'Refused by execution policy.',
|
|
1062
|
+
confidence: 1.0,
|
|
1063
|
+
risk: 'none',
|
|
1064
|
+
suggestion: 'Migrate the module to v2.2, or run with --profile strict/default.',
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (policy.profile !== 'certified')
|
|
1069
|
+
return null;
|
|
1070
|
+
const loc = module.location;
|
|
1071
|
+
if (typeof loc !== 'string' || loc.trim().length === 0) {
|
|
1072
|
+
return makeErrorResponse({
|
|
1073
|
+
code: 'E4007', // PERMISSION_DENIED
|
|
1074
|
+
message: 'Certified policy requires an installed module with provenance; module location is missing.',
|
|
1075
|
+
explain: 'Refused by execution policy.',
|
|
1076
|
+
confidence: 1.0,
|
|
1077
|
+
risk: 'none',
|
|
1078
|
+
suggestion: 'Reinstall the module from a registry tarball that writes provenance.json.',
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
// Single-file modules (5-minute path) are intentionally not allowed in certified flows.
|
|
1082
|
+
try {
|
|
1083
|
+
const st = await fs.stat(loc);
|
|
1084
|
+
if (!st.isDirectory()) {
|
|
1085
|
+
return makeErrorResponse({
|
|
1086
|
+
code: 'E4007', // PERMISSION_DENIED
|
|
1087
|
+
message: `Certified policy requires module directory provenance; got a non-directory location: ${loc}`,
|
|
1088
|
+
explain: 'Refused by execution policy.',
|
|
1089
|
+
confidence: 1.0,
|
|
1090
|
+
risk: 'none',
|
|
1091
|
+
suggestion: 'Install the module via `cog add <name>` (registry tarball) or use --profile strict/default.',
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
catch {
|
|
1096
|
+
return makeErrorResponse({
|
|
1097
|
+
code: 'E4007',
|
|
1098
|
+
message: `Certified policy requires module directory provenance, but location does not exist: ${loc}`,
|
|
1099
|
+
explain: 'Refused by execution policy.',
|
|
1100
|
+
confidence: 1.0,
|
|
1101
|
+
risk: 'none',
|
|
1102
|
+
suggestion: 'Reinstall the module from a registry tarball and retry.',
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
const prov = await readModuleProvenance(loc);
|
|
1106
|
+
if (!prov) {
|
|
1107
|
+
return makeErrorResponse({
|
|
1108
|
+
code: 'E4007', // PERMISSION_DENIED
|
|
1109
|
+
message: `Certified policy requires provenance.json in the module directory: ${loc}`,
|
|
1110
|
+
explain: 'Refused by execution policy.',
|
|
1111
|
+
confidence: 1.0,
|
|
1112
|
+
risk: 'none',
|
|
1113
|
+
suggestion: 'Reinstall the module from a registry tarball (distribution.tarball + checksum), then retry.',
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
if (prov.source.type !== 'registry') {
|
|
1117
|
+
return makeErrorResponse({
|
|
1118
|
+
code: 'E4007', // PERMISSION_DENIED
|
|
1119
|
+
message: `Certified policy requires registry provenance; module provenance is type=${prov.source.type}`,
|
|
1120
|
+
explain: 'Refused by execution policy.',
|
|
1121
|
+
confidence: 1.0,
|
|
1122
|
+
risk: 'none',
|
|
1123
|
+
suggestion: 'Reinstall the module from a registry tarball and retry, or use --profile strict/default.',
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
// Integrity check (tamper detection).
|
|
1127
|
+
const ok = await verifyModuleIntegrity(loc, prov);
|
|
1128
|
+
if (!ok.ok) {
|
|
1129
|
+
return makeErrorResponse({
|
|
1130
|
+
code: 'E4007', // PERMISSION_DENIED
|
|
1131
|
+
message: `Certified policy integrity check failed: ${ok.reason}`,
|
|
1132
|
+
explain: 'Module contents appear to have been modified after install.',
|
|
1133
|
+
confidence: 1.0,
|
|
1134
|
+
risk: 'none',
|
|
1135
|
+
suggestion: 'Reinstall the module from the registry tarball to restore integrity.',
|
|
1136
|
+
details: { location: loc, reason: ok.reason },
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
// Optional: enforce that the module directory remains within itself (defense-in-depth for weird paths).
|
|
1140
|
+
const resolved = path.resolve(loc);
|
|
1141
|
+
if (!resolved)
|
|
1142
|
+
return null;
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1049
1145
|
// =============================================================================
|
|
1050
1146
|
// Main Runner
|
|
1051
1147
|
// =============================================================================
|
|
1052
1148
|
export async function runModule(module, provider, options = {}) {
|
|
1053
|
-
const { args, input, verbose = false, validateInput = true, validateOutput = true, useEnvelope, useV22, enableRepair = true, traceId, model: modelOverride } = options;
|
|
1149
|
+
const { args, input, verbose = false, validateInput = true, validateOutput = true, useEnvelope, useV22, enableRepair = true, traceId, model: modelOverride, policy } = options;
|
|
1054
1150
|
const startTime = Date.now();
|
|
1151
|
+
const gate = await enforcePolicyGates(module, policy);
|
|
1152
|
+
if (gate) {
|
|
1153
|
+
const msg = gate.ok === false && 'error' in gate && gate.error?.message
|
|
1154
|
+
? String(gate.error.message)
|
|
1155
|
+
: 'Refused by execution policy';
|
|
1156
|
+
_invokeErrorHooks(module.name, new Error(msg), null);
|
|
1157
|
+
return gate;
|
|
1158
|
+
}
|
|
1055
1159
|
// Determine if we should use envelope format
|
|
1056
1160
|
const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
|
|
1057
1161
|
// Determine if we should use v2.2 format
|
|
@@ -1324,9 +1428,9 @@ export async function runModule(module, provider, options = {}) {
|
|
|
1324
1428
|
*
|
|
1325
1429
|
* Yields StreamEvent objects as the module executes:
|
|
1326
1430
|
* - type="start": Module execution started
|
|
1327
|
-
* - type="
|
|
1431
|
+
* - type="delta": Incremental output delta (provider streaming chunk)
|
|
1328
1432
|
* - type="meta": Meta information available early
|
|
1329
|
-
* - type="
|
|
1433
|
+
* - type="end": Final result envelope (always emitted)
|
|
1330
1434
|
* - type="error": Error occurred
|
|
1331
1435
|
*
|
|
1332
1436
|
* @example
|
|
@@ -1339,20 +1443,30 @@ export async function runModule(module, provider, options = {}) {
|
|
|
1339
1443
|
* }
|
|
1340
1444
|
*/
|
|
1341
1445
|
export async function* runModuleStream(module, provider, options = {}) {
|
|
1342
|
-
const { input, args, validateInput = true, validateOutput = true, useV22 = true, enableRepair = true, traceId, model } = options;
|
|
1446
|
+
const { input, args, validateInput = true, validateOutput = true, useV22 = true, enableRepair = true, traceId, model, policy } = options;
|
|
1343
1447
|
const startTime = Date.now();
|
|
1344
1448
|
const moduleName = module.name;
|
|
1449
|
+
const providerName = provider?.name;
|
|
1345
1450
|
function makeEvent(type, extra = {}) {
|
|
1346
1451
|
return {
|
|
1347
1452
|
type,
|
|
1453
|
+
version: ENVELOPE_VERSION,
|
|
1348
1454
|
timestamp_ms: Date.now() - startTime,
|
|
1349
|
-
|
|
1455
|
+
module: moduleName,
|
|
1456
|
+
...(providerName ? { provider: providerName } : {}),
|
|
1350
1457
|
...extra,
|
|
1351
1458
|
};
|
|
1352
1459
|
}
|
|
1353
1460
|
try {
|
|
1354
1461
|
// Emit start event
|
|
1355
1462
|
yield makeEvent('start');
|
|
1463
|
+
const gate = await enforcePolicyGates(module, policy);
|
|
1464
|
+
if (gate) {
|
|
1465
|
+
const errorObj = gate?.error ?? { code: 'E4007', message: 'Refused by execution policy' };
|
|
1466
|
+
yield makeEvent('error', { error: { code: errorObj.code, message: errorObj.message } });
|
|
1467
|
+
yield makeEvent('end', { result: gate });
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1356
1470
|
// Build input data
|
|
1357
1471
|
const inputData = input || {};
|
|
1358
1472
|
if (args && !inputData.code && !inputData.query) {
|
|
@@ -1378,7 +1492,7 @@ export async function* runModuleStream(module, provider, options = {}) {
|
|
|
1378
1492
|
_invokeErrorHooks(module.name, new Error(inputErrors.join('; ')), null);
|
|
1379
1493
|
const errorObj = errorResult.error;
|
|
1380
1494
|
yield makeEvent('error', { error: errorObj });
|
|
1381
|
-
yield makeEvent('
|
|
1495
|
+
yield makeEvent('end', { result: errorResult });
|
|
1382
1496
|
return;
|
|
1383
1497
|
}
|
|
1384
1498
|
}
|
|
@@ -1415,7 +1529,7 @@ export async function* runModuleStream(module, provider, options = {}) {
|
|
|
1415
1529
|
let streamResult;
|
|
1416
1530
|
while (!(streamResult = await stream.next()).done) {
|
|
1417
1531
|
const chunk = streamResult.value;
|
|
1418
|
-
yield makeEvent('
|
|
1532
|
+
yield makeEvent('delta', { delta: chunk });
|
|
1419
1533
|
}
|
|
1420
1534
|
// Get the final result (returned from the generator)
|
|
1421
1535
|
fullContent = streamResult.value.content;
|
|
@@ -1429,7 +1543,7 @@ export async function* runModuleStream(module, provider, options = {}) {
|
|
|
1429
1543
|
});
|
|
1430
1544
|
fullContent = result.content;
|
|
1431
1545
|
// Emit chunk event with full response
|
|
1432
|
-
yield makeEvent('
|
|
1546
|
+
yield makeEvent('delta', { delta: result.content });
|
|
1433
1547
|
}
|
|
1434
1548
|
// Parse response
|
|
1435
1549
|
let parsed;
|
|
@@ -1448,7 +1562,7 @@ export async function* runModuleStream(module, provider, options = {}) {
|
|
|
1448
1562
|
// errorResult is always an error response from makeErrorResponse
|
|
1449
1563
|
const errorObj = errorResult.error;
|
|
1450
1564
|
yield makeEvent('error', { error: errorObj });
|
|
1451
|
-
yield makeEvent('
|
|
1565
|
+
yield makeEvent('end', { result: errorResult });
|
|
1452
1566
|
return;
|
|
1453
1567
|
}
|
|
1454
1568
|
// Convert to v2.2 envelope
|
|
@@ -1502,7 +1616,7 @@ export async function* runModuleStream(module, provider, options = {}) {
|
|
|
1502
1616
|
_invokeErrorHooks(module.name, new Error(dataErrors.join('; ')), response.data);
|
|
1503
1617
|
const errorObj = errorResult.error;
|
|
1504
1618
|
yield makeEvent('error', { error: errorObj });
|
|
1505
|
-
yield makeEvent('
|
|
1619
|
+
yield makeEvent('end', { result: errorResult });
|
|
1506
1620
|
return;
|
|
1507
1621
|
}
|
|
1508
1622
|
const overflowErrors = validateOverflowLimits(dataToValidate, module);
|
|
@@ -1517,7 +1631,7 @@ export async function* runModuleStream(module, provider, options = {}) {
|
|
|
1517
1631
|
_invokeErrorHooks(module.name, new Error(overflowErrors.join('; ')), dataToValidate);
|
|
1518
1632
|
const errorObj = errorResult.error;
|
|
1519
1633
|
yield makeEvent('error', { error: errorObj });
|
|
1520
|
-
yield makeEvent('
|
|
1634
|
+
yield makeEvent('end', { result: errorResult });
|
|
1521
1635
|
return;
|
|
1522
1636
|
}
|
|
1523
1637
|
const enumErrors = validateEnumStrategy(dataToValidate, module);
|
|
@@ -1532,7 +1646,7 @@ export async function* runModuleStream(module, provider, options = {}) {
|
|
|
1532
1646
|
_invokeErrorHooks(module.name, new Error(enumErrors.join('; ')), dataToValidate);
|
|
1533
1647
|
const errorObj = errorResult.error;
|
|
1534
1648
|
yield makeEvent('error', { error: errorObj });
|
|
1535
|
-
yield makeEvent('
|
|
1649
|
+
yield makeEvent('end', { result: errorResult });
|
|
1536
1650
|
return;
|
|
1537
1651
|
}
|
|
1538
1652
|
}
|
|
@@ -1554,7 +1668,7 @@ export async function* runModuleStream(module, provider, options = {}) {
|
|
|
1554
1668
|
_invokeErrorHooks(module.name, new Error(metaErrors.join('; ')), response.data);
|
|
1555
1669
|
const errorObj = errorResult.error;
|
|
1556
1670
|
yield makeEvent('error', { error: errorObj });
|
|
1557
|
-
yield makeEvent('
|
|
1671
|
+
yield makeEvent('end', { result: errorResult });
|
|
1558
1672
|
return;
|
|
1559
1673
|
}
|
|
1560
1674
|
}
|
|
@@ -1566,8 +1680,8 @@ export async function* runModuleStream(module, provider, options = {}) {
|
|
|
1566
1680
|
}
|
|
1567
1681
|
const finalLatencyMs = Date.now() - startTime;
|
|
1568
1682
|
_invokeAfterHooks(module.name, response, finalLatencyMs);
|
|
1569
|
-
// Emit
|
|
1570
|
-
yield makeEvent('
|
|
1683
|
+
// Emit end event
|
|
1684
|
+
yield makeEvent('end', { result: response });
|
|
1571
1685
|
}
|
|
1572
1686
|
catch (e) {
|
|
1573
1687
|
_invokeErrorHooks(module.name, e, null);
|
|
@@ -1579,7 +1693,7 @@ export async function* runModuleStream(module, provider, options = {}) {
|
|
|
1579
1693
|
// errorResult is always an error response from makeErrorResponse
|
|
1580
1694
|
const errorObj = errorResult.error;
|
|
1581
1695
|
yield makeEvent('error', { error: errorObj });
|
|
1582
|
-
yield makeEvent('
|
|
1696
|
+
yield makeEvent('end', { result: errorResult });
|
|
1583
1697
|
}
|
|
1584
1698
|
}
|
|
1585
1699
|
// =============================================================================
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExecutionPolicy } from './types.js';
|
|
2
|
+
export interface ResolvePolicyInput {
|
|
3
|
+
profile?: string | null;
|
|
4
|
+
validate?: string | null;
|
|
5
|
+
noValidate?: boolean;
|
|
6
|
+
audit?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveExecutionPolicy(input: ResolvePolicyInput): ExecutionPolicy;
|
package/dist/profile.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
function normalizeProfile(raw) {
|
|
2
|
+
const v = (raw ?? '').trim().toLowerCase();
|
|
3
|
+
if (v === 'core')
|
|
4
|
+
return 'core';
|
|
5
|
+
if (v === 'default' || v === '')
|
|
6
|
+
return 'default';
|
|
7
|
+
if (v === 'strict')
|
|
8
|
+
return 'strict';
|
|
9
|
+
if (v === 'certified' || v === 'cert')
|
|
10
|
+
return 'certified';
|
|
11
|
+
throw new Error(`Invalid --profile: ${raw}. Expected one of: core|default|strict|certified`);
|
|
12
|
+
}
|
|
13
|
+
function normalizeValidate(raw) {
|
|
14
|
+
const v = (raw ?? '').trim().toLowerCase();
|
|
15
|
+
if (v === '' || v === 'auto')
|
|
16
|
+
return 'auto';
|
|
17
|
+
if (v === 'on' || v === 'true' || v === '1')
|
|
18
|
+
return 'on';
|
|
19
|
+
if (v === 'off' || v === 'false' || v === '0')
|
|
20
|
+
return 'off';
|
|
21
|
+
throw new Error(`Invalid --validate: ${raw}. Expected one of: auto|on|off`);
|
|
22
|
+
}
|
|
23
|
+
export function resolveExecutionPolicy(input) {
|
|
24
|
+
const profile = normalizeProfile(input.profile);
|
|
25
|
+
// Base defaults per profile.
|
|
26
|
+
let validate = profile === 'core' ? 'off' : 'on';
|
|
27
|
+
let audit = false;
|
|
28
|
+
let enableRepair = true;
|
|
29
|
+
let requireV22 = false;
|
|
30
|
+
if (profile === 'strict') {
|
|
31
|
+
validate = 'on';
|
|
32
|
+
audit = false;
|
|
33
|
+
enableRepair = true;
|
|
34
|
+
requireV22 = false;
|
|
35
|
+
}
|
|
36
|
+
if (profile === 'certified') {
|
|
37
|
+
validate = 'on';
|
|
38
|
+
audit = true;
|
|
39
|
+
enableRepair = false; // certification prefers fail-fast over runtime repair
|
|
40
|
+
requireV22 = true;
|
|
41
|
+
}
|
|
42
|
+
// CLI overrides.
|
|
43
|
+
const validateExplicit = input.validate != null || Boolean(input.noValidate);
|
|
44
|
+
if (input.validate != null) {
|
|
45
|
+
validate = normalizeValidate(input.validate);
|
|
46
|
+
}
|
|
47
|
+
if (input.noValidate) {
|
|
48
|
+
validate = 'off';
|
|
49
|
+
}
|
|
50
|
+
if (typeof input.audit === 'boolean') {
|
|
51
|
+
audit = input.audit;
|
|
52
|
+
}
|
|
53
|
+
// Trigger rule: if audit is enabled and validate wasn't explicitly turned off,
|
|
54
|
+
// force validation on (auditing without validation is usually not meaningful).
|
|
55
|
+
if (audit && !(validateExplicit && validate === 'off')) {
|
|
56
|
+
validate = 'on';
|
|
57
|
+
}
|
|
58
|
+
return { profile, validate, audit, enableRepair, requireV22 };
|
|
59
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const PROVENANCE_FILENAME = "provenance.json";
|
|
2
|
+
export declare const PROVENANCE_SPEC = "cognitive.module.provenance/v1";
|
|
3
|
+
export type ProvenanceSource = {
|
|
4
|
+
type: 'registry';
|
|
5
|
+
registryUrl?: string | null;
|
|
6
|
+
moduleName: string;
|
|
7
|
+
requestedVersion?: string | null;
|
|
8
|
+
resolvedVersion?: string | null;
|
|
9
|
+
tarballUrl: string;
|
|
10
|
+
checksum: string;
|
|
11
|
+
sha256: string;
|
|
12
|
+
quality?: {
|
|
13
|
+
verified?: boolean;
|
|
14
|
+
conformance_level?: number;
|
|
15
|
+
spec_version?: string;
|
|
16
|
+
};
|
|
17
|
+
} | {
|
|
18
|
+
type: 'github';
|
|
19
|
+
repoUrl: string;
|
|
20
|
+
ref?: string | null;
|
|
21
|
+
modulePath?: string | null;
|
|
22
|
+
};
|
|
23
|
+
export interface ModuleIntegrity {
|
|
24
|
+
algorithm: 'sha256';
|
|
25
|
+
maxFiles: number;
|
|
26
|
+
maxTotalBytes: number;
|
|
27
|
+
maxSingleFileBytes: number;
|
|
28
|
+
totalBytes: number;
|
|
29
|
+
files: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
export interface ModuleProvenance {
|
|
32
|
+
spec: typeof PROVENANCE_SPEC;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
source: ProvenanceSource;
|
|
35
|
+
integrity: ModuleIntegrity;
|
|
36
|
+
}
|
|
37
|
+
export interface IntegrityOptions {
|
|
38
|
+
maxFiles: number;
|
|
39
|
+
maxTotalBytes: number;
|
|
40
|
+
maxSingleFileBytes: number;
|
|
41
|
+
}
|
|
42
|
+
export declare function computeModuleIntegrity(moduleDir: string, options?: Partial<IntegrityOptions>): Promise<ModuleIntegrity>;
|
|
43
|
+
export declare function writeModuleProvenance(moduleDir: string, prov: ModuleProvenance): Promise<void>;
|
|
44
|
+
export declare function readModuleProvenance(moduleDir: string): Promise<ModuleProvenance | null>;
|
|
45
|
+
export declare function verifyModuleIntegrity(moduleDir: string, prov: ModuleProvenance): Promise<{
|
|
46
|
+
ok: true;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: string;
|
|
50
|
+
}>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
export const PROVENANCE_FILENAME = 'provenance.json';
|
|
6
|
+
export const PROVENANCE_SPEC = 'cognitive.module.provenance/v1';
|
|
7
|
+
const DEFAULT_INTEGRITY_LIMITS = {
|
|
8
|
+
maxFiles: 5_000,
|
|
9
|
+
maxTotalBytes: 50 * 1024 * 1024, // 50MB
|
|
10
|
+
maxSingleFileBytes: 20 * 1024 * 1024, // 20MB
|
|
11
|
+
};
|
|
12
|
+
async function hashFileSha256(filePath, size) {
|
|
13
|
+
const h = createHash('sha256');
|
|
14
|
+
await new Promise((resolve, reject) => {
|
|
15
|
+
const rs = createReadStream(filePath);
|
|
16
|
+
rs.on('data', (chunk) => h.update(chunk));
|
|
17
|
+
rs.on('error', reject);
|
|
18
|
+
rs.on('end', resolve);
|
|
19
|
+
});
|
|
20
|
+
// Include size in the hash domain separation to make accidental truncation obvious.
|
|
21
|
+
h.update(`\nsize:${size}\n`);
|
|
22
|
+
return h.digest('hex');
|
|
23
|
+
}
|
|
24
|
+
async function walkFiles(rootDir, relDir) {
|
|
25
|
+
const absDir = path.join(rootDir, relDir);
|
|
26
|
+
const entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const ent of entries) {
|
|
29
|
+
const rel = relDir ? path.posix.join(relDir.replace(/\\/g, '/'), ent.name) : ent.name;
|
|
30
|
+
const abs = path.join(rootDir, rel);
|
|
31
|
+
// Never hash our own provenance.
|
|
32
|
+
if (rel === PROVENANCE_FILENAME)
|
|
33
|
+
continue;
|
|
34
|
+
if (ent.name === '.DS_Store' || ent.name === '__MACOSX')
|
|
35
|
+
continue;
|
|
36
|
+
if (ent.isSymbolicLink()) {
|
|
37
|
+
// Refuse to hash symlinks. Tar extraction also rejects them.
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (ent.isDirectory()) {
|
|
41
|
+
out.push(...await walkFiles(rootDir, rel));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (ent.isFile()) {
|
|
45
|
+
out.push(rel);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
export async function computeModuleIntegrity(moduleDir, options = {}) {
|
|
51
|
+
const limits = {
|
|
52
|
+
...DEFAULT_INTEGRITY_LIMITS,
|
|
53
|
+
...options,
|
|
54
|
+
};
|
|
55
|
+
const relFiles = (await walkFiles(moduleDir, '')).sort((a, b) => a.localeCompare(b));
|
|
56
|
+
if (relFiles.length > limits.maxFiles) {
|
|
57
|
+
throw new Error(`Module has too many files to hash (max ${limits.maxFiles}): ${relFiles.length}`);
|
|
58
|
+
}
|
|
59
|
+
const files = {};
|
|
60
|
+
let totalBytes = 0;
|
|
61
|
+
for (const rel of relFiles) {
|
|
62
|
+
const abs = path.join(moduleDir, rel);
|
|
63
|
+
const st = await fs.stat(abs);
|
|
64
|
+
if (!st.isFile())
|
|
65
|
+
continue;
|
|
66
|
+
if (st.size > limits.maxSingleFileBytes) {
|
|
67
|
+
throw new Error(`Module file too large for integrity hashing (max ${limits.maxSingleFileBytes} bytes): ${rel}`);
|
|
68
|
+
}
|
|
69
|
+
totalBytes += st.size;
|
|
70
|
+
if (totalBytes > limits.maxTotalBytes) {
|
|
71
|
+
throw new Error(`Module too large for integrity hashing (max ${limits.maxTotalBytes} bytes)`);
|
|
72
|
+
}
|
|
73
|
+
const sha256 = await hashFileSha256(abs, st.size);
|
|
74
|
+
files[rel] = sha256;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
algorithm: 'sha256',
|
|
78
|
+
maxFiles: limits.maxFiles,
|
|
79
|
+
maxTotalBytes: limits.maxTotalBytes,
|
|
80
|
+
maxSingleFileBytes: limits.maxSingleFileBytes,
|
|
81
|
+
totalBytes,
|
|
82
|
+
files,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function writeModuleProvenance(moduleDir, prov) {
|
|
86
|
+
const filePath = path.join(moduleDir, PROVENANCE_FILENAME);
|
|
87
|
+
const json = JSON.stringify(prov, null, 2) + '\n';
|
|
88
|
+
await fs.writeFile(filePath, json, 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
export async function readModuleProvenance(moduleDir) {
|
|
91
|
+
const filePath = path.join(moduleDir, PROVENANCE_FILENAME);
|
|
92
|
+
try {
|
|
93
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
95
|
+
if (!parsed || typeof parsed !== 'object')
|
|
96
|
+
return null;
|
|
97
|
+
if (parsed.spec !== PROVENANCE_SPEC)
|
|
98
|
+
return null;
|
|
99
|
+
if (!parsed.source || typeof parsed.source !== 'object')
|
|
100
|
+
return null;
|
|
101
|
+
if (!parsed.integrity || typeof parsed.integrity !== 'object')
|
|
102
|
+
return null;
|
|
103
|
+
return parsed;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function verifyModuleIntegrity(moduleDir, prov) {
|
|
110
|
+
const expected = prov.integrity?.files ?? {};
|
|
111
|
+
const expectedKeys = Object.keys(expected).sort();
|
|
112
|
+
if (expectedKeys.length === 0) {
|
|
113
|
+
return { ok: false, reason: 'Provenance integrity.files is empty' };
|
|
114
|
+
}
|
|
115
|
+
const computed = await computeModuleIntegrity(moduleDir, {
|
|
116
|
+
maxFiles: prov.integrity.maxFiles,
|
|
117
|
+
maxTotalBytes: prov.integrity.maxTotalBytes,
|
|
118
|
+
maxSingleFileBytes: prov.integrity.maxSingleFileBytes,
|
|
119
|
+
});
|
|
120
|
+
const computedKeys = Object.keys(computed.files).sort();
|
|
121
|
+
if (expectedKeys.length !== computedKeys.length) {
|
|
122
|
+
return { ok: false, reason: 'Integrity file list changed (file count mismatch)' };
|
|
123
|
+
}
|
|
124
|
+
for (let i = 0; i < expectedKeys.length; i++) {
|
|
125
|
+
if (expectedKeys[i] !== computedKeys[i]) {
|
|
126
|
+
return { ok: false, reason: `Integrity file list changed (mismatch at ${i}: ${expectedKeys[i]} vs ${computedKeys[i]})` };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const rel of expectedKeys) {
|
|
130
|
+
const a = expected[rel];
|
|
131
|
+
const b = computed.files[rel];
|
|
132
|
+
if (a !== b) {
|
|
133
|
+
return { ok: false, reason: `Integrity mismatch for ${rel}` };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { ok: true };
|
|
137
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface BuildRegistryOptions {
|
|
2
|
+
tag?: string | null;
|
|
3
|
+
tarballBaseUrl?: string | null;
|
|
4
|
+
modulesDir: string;
|
|
5
|
+
v1RegistryPath: string;
|
|
6
|
+
outDir: string;
|
|
7
|
+
registryOut: string;
|
|
8
|
+
namespace: string;
|
|
9
|
+
runtimeMin: string;
|
|
10
|
+
repository: string;
|
|
11
|
+
homepage: string;
|
|
12
|
+
license: string;
|
|
13
|
+
timestamp?: string | null;
|
|
14
|
+
only?: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface VerifyRegistryOptions {
|
|
17
|
+
registryIndexPath: string;
|
|
18
|
+
assetsDir?: string;
|
|
19
|
+
maxTarballBytes?: number;
|
|
20
|
+
remote?: boolean;
|
|
21
|
+
fetchTimeoutMs?: number;
|
|
22
|
+
maxIndexBytes?: number;
|
|
23
|
+
concurrency?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface RegistryBuildResult {
|
|
26
|
+
registryOut: string;
|
|
27
|
+
outDir: string;
|
|
28
|
+
updated: string;
|
|
29
|
+
modules: Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
version: string;
|
|
32
|
+
file: string;
|
|
33
|
+
sha256: string;
|
|
34
|
+
size_bytes: number;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export interface RegistryVerifyResult {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
checked: number;
|
|
40
|
+
passed: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
failures: Array<{
|
|
43
|
+
module: string;
|
|
44
|
+
reason: string;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
export declare function buildRegistryAssets(opts: BuildRegistryOptions): Promise<RegistryBuildResult>;
|
|
48
|
+
export declare function verifyRegistryAssets(opts: VerifyRegistryOptions): Promise<RegistryVerifyResult>;
|