cognitive-modules-cli 2.2.7 → 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 +6 -0
- package/README.md +25 -3
- package/dist/audit.d.ts +13 -0
- package/dist/audit.js +25 -0
- package/dist/cli.js +182 -2
- package/dist/commands/add.js +68 -1
- 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.js +99 -17
- package/dist/commands/search.js +13 -3
- package/dist/commands/update.js +4 -1
- 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 +3 -1
- package/dist/modules/runner.js +113 -2
- 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 +8 -1
- package/dist/registry/client.js +83 -29
- package/dist/types.d.ts +31 -0
- package/package.json +1 -1
|
@@ -577,10 +577,18 @@ export class CompositionOrchestrator {
|
|
|
577
577
|
provider;
|
|
578
578
|
cwd;
|
|
579
579
|
searchPaths;
|
|
580
|
-
|
|
580
|
+
validateInput;
|
|
581
|
+
validateOutput;
|
|
582
|
+
enableRepair;
|
|
583
|
+
policy;
|
|
584
|
+
constructor(provider, cwd = process.cwd(), enforcement = {}) {
|
|
581
585
|
this.provider = provider;
|
|
582
586
|
this.cwd = cwd;
|
|
583
587
|
this.searchPaths = getDefaultSearchPaths(cwd);
|
|
588
|
+
this.validateInput = enforcement.validateInput ?? true;
|
|
589
|
+
this.validateOutput = enforcement.validateOutput ?? true;
|
|
590
|
+
this.enableRepair = enforcement.enableRepair ?? true;
|
|
591
|
+
this.policy = enforcement.policy;
|
|
584
592
|
}
|
|
585
593
|
/**
|
|
586
594
|
* Execute a composed module workflow
|
|
@@ -1160,9 +1168,11 @@ export class CompositionOrchestrator {
|
|
|
1160
1168
|
try {
|
|
1161
1169
|
const result = await runModule(module, this.provider, {
|
|
1162
1170
|
input,
|
|
1163
|
-
validateInput:
|
|
1164
|
-
validateOutput:
|
|
1165
|
-
useV22: true
|
|
1171
|
+
validateInput: this.validateInput,
|
|
1172
|
+
validateOutput: this.validateOutput,
|
|
1173
|
+
useV22: true,
|
|
1174
|
+
enableRepair: this.enableRepair,
|
|
1175
|
+
policy: this.policy,
|
|
1166
1176
|
});
|
|
1167
1177
|
const endTime = Date.now();
|
|
1168
1178
|
trace.push({
|
|
@@ -1278,8 +1288,8 @@ export class CompositionOrchestrator {
|
|
|
1278
1288
|
* Execute a composed module workflow
|
|
1279
1289
|
*/
|
|
1280
1290
|
export async function executeComposition(moduleName, input, provider, options = {}) {
|
|
1281
|
-
const { cwd = process.cwd(), ...execOptions } = options;
|
|
1282
|
-
const orchestrator = new CompositionOrchestrator(provider, cwd);
|
|
1291
|
+
const { cwd = process.cwd(), validateInput, validateOutput, enableRepair, policy, ...execOptions } = options;
|
|
1292
|
+
const orchestrator = new CompositionOrchestrator(provider, cwd, { validateInput, validateOutput, enableRepair, policy });
|
|
1283
1293
|
return orchestrator.execute(moduleName, input, execOptions);
|
|
1284
1294
|
}
|
|
1285
1295
|
/**
|
package/dist/modules/loader.d.ts
CHANGED
|
@@ -7,6 +7,16 @@ import type { CognitiveModule, ModuleTier, SchemaStrictness } from '../types.js'
|
|
|
7
7
|
* Load a Cognitive Module (auto-detects format)
|
|
8
8
|
*/
|
|
9
9
|
export declare function loadModule(modulePath: string): Promise<CognitiveModule>;
|
|
10
|
+
/**
|
|
11
|
+
* Load a single-file module (Markdown) with optional YAML frontmatter.
|
|
12
|
+
*
|
|
13
|
+
* This enables the "5-minute" path: one file runs, and the runtime generates a loose schema/envelope.
|
|
14
|
+
*
|
|
15
|
+
* File format:
|
|
16
|
+
* - Optional YAML frontmatter between --- ... --- (same as v1 MODULE.md)
|
|
17
|
+
* - Body is treated as prompt
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadSingleFileModule(filePath: string): Promise<CognitiveModule>;
|
|
10
20
|
export declare function findModule(name: string, searchPaths: string[]): Promise<CognitiveModule | null>;
|
|
11
21
|
export declare function listModules(searchPaths: string[]): Promise<CognitiveModule[]>;
|
|
12
22
|
export declare function getDefaultSearchPaths(cwd: string): string[];
|
package/dist/modules/loader.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as path from 'node:path';
|
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import yaml from 'js-yaml';
|
|
9
9
|
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?/;
|
|
10
|
+
const SAFE_MODULE_NAME_REGEX = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
10
11
|
/**
|
|
11
12
|
* Detect module format version
|
|
12
13
|
*/
|
|
@@ -333,6 +334,173 @@ export async function loadModule(modulePath) {
|
|
|
333
334
|
return loadModuleV0(modulePath);
|
|
334
335
|
}
|
|
335
336
|
}
|
|
337
|
+
// =============================================================================
|
|
338
|
+
// Single-File Modules (Ad-hoc)
|
|
339
|
+
// =============================================================================
|
|
340
|
+
function coerceModuleName(name, fallback) {
|
|
341
|
+
const raw = (typeof name === 'string' && name.trim().length > 0) ? name.trim() : fallback;
|
|
342
|
+
const normalized = raw
|
|
343
|
+
.replace(/\s+/g, '-')
|
|
344
|
+
.replace(/[^a-zA-Z0-9._-]/g, '-')
|
|
345
|
+
.replace(/-+/g, '-')
|
|
346
|
+
.replace(/^-|-$/g, '');
|
|
347
|
+
const lower = normalized.toLowerCase();
|
|
348
|
+
if (SAFE_MODULE_NAME_REGEX.test(lower))
|
|
349
|
+
return lower;
|
|
350
|
+
return fallback;
|
|
351
|
+
}
|
|
352
|
+
function defaultMetaSchema() {
|
|
353
|
+
return {
|
|
354
|
+
type: 'object',
|
|
355
|
+
additionalProperties: true,
|
|
356
|
+
required: ['confidence', 'risk', 'explain'],
|
|
357
|
+
properties: {
|
|
358
|
+
confidence: { type: 'number', minimum: 0, maximum: 1 },
|
|
359
|
+
risk: { type: 'string', enum: ['none', 'low', 'medium', 'high'] },
|
|
360
|
+
explain: { type: 'string', maxLength: 280 },
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function defaultDataSchema() {
|
|
365
|
+
// Intentionally loose: the "5-minute" path should not fail on business fields.
|
|
366
|
+
return {
|
|
367
|
+
type: 'object',
|
|
368
|
+
additionalProperties: true,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function defaultInputSchema() {
|
|
372
|
+
// Matches runtime behavior: args are mapped to either query or code.
|
|
373
|
+
return {
|
|
374
|
+
type: 'object',
|
|
375
|
+
additionalProperties: true,
|
|
376
|
+
properties: {
|
|
377
|
+
query: { type: 'string' },
|
|
378
|
+
code: { type: 'string' },
|
|
379
|
+
args: { type: 'string' },
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function defaultEnvelopeSchema(metaSchema) {
|
|
384
|
+
// Used as a hint for structured output (provider jsonSchema). Validation uses metaSchema/dataSchema separately.
|
|
385
|
+
return {
|
|
386
|
+
type: 'object',
|
|
387
|
+
additionalProperties: true,
|
|
388
|
+
oneOf: [
|
|
389
|
+
{
|
|
390
|
+
type: 'object',
|
|
391
|
+
required: ['ok', 'meta', 'data'],
|
|
392
|
+
properties: {
|
|
393
|
+
ok: { const: true },
|
|
394
|
+
version: { type: 'string' },
|
|
395
|
+
meta: metaSchema,
|
|
396
|
+
data: { type: 'object', additionalProperties: true },
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
type: 'object',
|
|
401
|
+
required: ['ok', 'meta', 'error'],
|
|
402
|
+
properties: {
|
|
403
|
+
ok: { const: false },
|
|
404
|
+
version: { type: 'string' },
|
|
405
|
+
meta: metaSchema,
|
|
406
|
+
error: {
|
|
407
|
+
type: 'object',
|
|
408
|
+
additionalProperties: true,
|
|
409
|
+
required: ['code', 'message'],
|
|
410
|
+
properties: {
|
|
411
|
+
code: { type: 'string' },
|
|
412
|
+
message: { type: 'string' },
|
|
413
|
+
recoverable: { type: 'boolean' },
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
partial_data: { type: 'object', additionalProperties: true },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function looksLikeMarkdownModule(filePath) {
|
|
423
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
424
|
+
return ext === '.md' || ext === '.markdown';
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Load a single-file module (Markdown) with optional YAML frontmatter.
|
|
428
|
+
*
|
|
429
|
+
* This enables the "5-minute" path: one file runs, and the runtime generates a loose schema/envelope.
|
|
430
|
+
*
|
|
431
|
+
* File format:
|
|
432
|
+
* - Optional YAML frontmatter between --- ... --- (same as v1 MODULE.md)
|
|
433
|
+
* - Body is treated as prompt
|
|
434
|
+
*/
|
|
435
|
+
export async function loadSingleFileModule(filePath) {
|
|
436
|
+
const absPath = path.resolve(filePath);
|
|
437
|
+
if (!looksLikeMarkdownModule(absPath)) {
|
|
438
|
+
throw new Error(`Single-file modules currently support Markdown (.md) only: ${absPath}`);
|
|
439
|
+
}
|
|
440
|
+
const content = await fs.readFile(absPath, 'utf-8');
|
|
441
|
+
let manifest = {};
|
|
442
|
+
let prompt = content;
|
|
443
|
+
const match = content.match(FRONTMATTER_REGEX);
|
|
444
|
+
if (match) {
|
|
445
|
+
try {
|
|
446
|
+
const loaded = yaml.load(match[1]);
|
|
447
|
+
manifest = (loaded && typeof loaded === 'object') ? loaded : {};
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
manifest = {};
|
|
451
|
+
}
|
|
452
|
+
prompt = (match[2] ?? '').trim();
|
|
453
|
+
}
|
|
454
|
+
const base = path.basename(absPath, path.extname(absPath));
|
|
455
|
+
const name = coerceModuleName(manifest.name, coerceModuleName(base, 'single-file-module'));
|
|
456
|
+
const tier = manifest.tier ?? 'decision';
|
|
457
|
+
const responsibility = manifest.responsibility
|
|
458
|
+
?? `Ad-hoc single-file module loaded from ${path.basename(absPath)}`;
|
|
459
|
+
const excludes = Array.isArray(manifest.excludes) ? manifest.excludes : [];
|
|
460
|
+
const metaSchema = defaultMetaSchema();
|
|
461
|
+
const dataSchema = defaultDataSchema();
|
|
462
|
+
const inputSchema = defaultInputSchema();
|
|
463
|
+
const envelopeSchema = defaultEnvelopeSchema(metaSchema);
|
|
464
|
+
// Keep validation permissive by default, but still enforce envelope meta shape.
|
|
465
|
+
const schemaStrictness = manifest.schema_strictness ?? 'low';
|
|
466
|
+
return {
|
|
467
|
+
name,
|
|
468
|
+
version: manifest.version ?? '0.1.0',
|
|
469
|
+
responsibility,
|
|
470
|
+
excludes,
|
|
471
|
+
constraints: manifest.constraints,
|
|
472
|
+
policies: manifest.policies,
|
|
473
|
+
tools: manifest.tools,
|
|
474
|
+
output: manifest.output ?? { envelope: true, format: 'json_lenient' },
|
|
475
|
+
failure: manifest.failure,
|
|
476
|
+
runtimeRequirements: manifest.runtime_requirements,
|
|
477
|
+
tier,
|
|
478
|
+
schemaStrictness,
|
|
479
|
+
overflow: {
|
|
480
|
+
enabled: true,
|
|
481
|
+
recoverable: true,
|
|
482
|
+
max_items: 20,
|
|
483
|
+
require_suggested_mapping: false,
|
|
484
|
+
},
|
|
485
|
+
enums: { strategy: 'extensible', unknown_tag: 'custom' },
|
|
486
|
+
compat: { accepts_v21_payload: true, runtime_auto_wrap: true, schema_output_alias: 'data' },
|
|
487
|
+
metaConfig: { risk_rule: 'explicit' },
|
|
488
|
+
composition: undefined,
|
|
489
|
+
context: manifest.context,
|
|
490
|
+
prompt: prompt.length > 0 ? prompt : content.trim(),
|
|
491
|
+
// Schemas:
|
|
492
|
+
// - outputSchema is used as provider jsonSchema hint (we want the full envelope)
|
|
493
|
+
// - metaSchema/dataSchema are used for runtime validation after wrapping/repair
|
|
494
|
+
inputSchema,
|
|
495
|
+
outputSchema: envelopeSchema,
|
|
496
|
+
dataSchema,
|
|
497
|
+
metaSchema,
|
|
498
|
+
errorSchema: undefined,
|
|
499
|
+
location: absPath,
|
|
500
|
+
format: 'v2',
|
|
501
|
+
formatVersion: 'v2.2',
|
|
502
|
+
};
|
|
503
|
+
}
|
|
336
504
|
/**
|
|
337
505
|
* Check if a directory contains a valid module
|
|
338
506
|
*/
|
package/dist/modules/runner.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* v2.2: Envelope format with meta/data separation, risk_rule, repair pass
|
|
4
4
|
* v2.2.1: Version field, enhanced error taxonomy, observability hooks, streaming
|
|
5
5
|
*/
|
|
6
|
-
import type { Provider, CognitiveModule, ModuleResult, ModuleInput, EnvelopeResponseV22, EnvelopeMeta, RiskLevel } from '../types.js';
|
|
6
|
+
import type { Provider, CognitiveModule, ModuleResult, ModuleInput, EnvelopeResponseV22, EnvelopeMeta, RiskLevel, ExecutionPolicy } from '../types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Validate data against JSON schema. Returns list of errors.
|
|
9
9
|
*/
|
|
@@ -366,6 +366,7 @@ export interface RunOptions {
|
|
|
366
366
|
enableRepair?: boolean;
|
|
367
367
|
traceId?: string;
|
|
368
368
|
model?: string;
|
|
369
|
+
policy?: ExecutionPolicy;
|
|
369
370
|
}
|
|
370
371
|
export declare function runModule(module: CognitiveModule, provider: Provider, options?: RunOptions): Promise<ModuleResult>;
|
|
371
372
|
/** Event types emitted during streaming execution */
|
|
@@ -394,6 +395,7 @@ export interface StreamOptions {
|
|
|
394
395
|
enableRepair?: boolean;
|
|
395
396
|
traceId?: string;
|
|
396
397
|
model?: string;
|
|
398
|
+
policy?: ExecutionPolicy;
|
|
397
399
|
}
|
|
398
400
|
/**
|
|
399
401
|
* Run a cognitive module with streaming output.
|
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
|
|
@@ -1339,7 +1443,7 @@ 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;
|
|
1345
1449
|
const providerName = provider?.name;
|
|
@@ -1356,6 +1460,13 @@ export async function* runModuleStream(module, provider, options = {}) {
|
|
|
1356
1460
|
try {
|
|
1357
1461
|
// Emit start event
|
|
1358
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
|
+
}
|
|
1359
1470
|
// Build input data
|
|
1360
1471
|
const inputData = input || {};
|
|
1361
1472
|
if (args && !inputData.code && !inputData.query) {
|
|
@@ -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
|
+
}
|