cognitive-modules-cli 2.2.7 → 2.2.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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this package are documented in this file.
4
4
 
5
+ ## 2.2.9 - 2026-02-07
6
+
7
+ - Fix: `cog core run` now prints the error envelope (instead of `Error: undefined`) when execution fails.
8
+ - Packaging: add stable `bin.js` entrypoint so publish/install doesn't depend on prebuilt `dist/` existing at publish-time.
9
+ - Core: align core template placeholders with runtime substitution (`${query}` / `${code}`); missing fields are treated as empty for single-file modules.
10
+
11
+ ## 2.2.8 - 2026-02-07
12
+
13
+ - Fix: `npx cogn` alias compatibility on newer Node (exports + ESM). (Alias fix ships in `cogn@2.2.8`.)
14
+ - Hardening: configurable registry index fetch limits (`--registry-timeout-ms`, `--registry-max-bytes`).
15
+ - Hardening: remote registry verification supports bounded concurrency (`--concurrency`).
16
+
5
17
  ## 2.2.7 - 2026-02-06
6
18
 
7
19
  - Standardized v2.2 runtime behavior and cross-surface error envelope consistency (CLI/HTTP/MCP).
package/README.md CHANGED
@@ -10,12 +10,12 @@ Node.js/TypeScript 版本的 Cognitive Modules CLI,提供 `cog` 命令。
10
10
 
11
11
  ```bash
12
12
  # 全局安装(推荐)
13
- npm install -g cogn@2.2.7
13
+ npm install -g cogn@2.2.9
14
14
  # 或使用完整包名(同样提供 `cog` 命令)
15
- # npm install -g cognitive-modules-cli@2.2.7
15
+ # npm install -g cognitive-modules-cli@2.2.9
16
16
 
17
17
  # 或使用 npx 零安装
18
- npx cogn@2.2.7 --help
18
+ npx cogn@2.2.9 --help
19
19
  ```
20
20
 
21
21
  ## 快速开始
@@ -50,6 +50,18 @@ echo "review this code" | cog pipe --module code-reviewer
50
50
  ## 命令
51
51
 
52
52
  ```bash
53
+ # Core(单文件极简路径)
54
+ cog core new # 生成 demo.md
55
+ cog core run demo.md --args "..." # 运行单文件模块
56
+ cog core promote demo.md # 升级为 v2 模块目录
57
+
58
+ # 渐进复杂度(Profiles)
59
+ cog run code-reviewer --args "..." --profile core # 极简:跳过校验
60
+ cog run code-reviewer --args "..." --profile default # 默认:开启校验
61
+ cog run code-reviewer --args "..." --profile strict # 更严格:开启校验(更强门禁)
62
+ cog run code-reviewer --args "..." --profile certified # 最严格:v2.2 + 审计 + registry provenance/完整性门禁
63
+ # 覆盖开关:--validate auto|on|off,--audit(写入 ~/.cognitive/audit/)
64
+
53
65
  # 模块操作
54
66
  cog list # 列出模块
55
67
  cog run <module> --args "..." # 运行模块
@@ -76,6 +88,16 @@ cog mcp # 启动 MCP 服务(Claude Code / Cursor)
76
88
 
77
89
  # 环境检查
78
90
  cog doctor
91
+
92
+ # Registry(索引与分发)
93
+ # 默认 registry index(latest):
94
+ # https://github.com/Cognary/cognitive/releases/latest/download/cognitive-registry.v2.json
95
+ # 可通过环境变量或全局参数覆盖:
96
+ COGNITIVE_REGISTRY_URL=... cog search
97
+ COGNITIVE_REGISTRY_TIMEOUT_MS=15000 COGNITIVE_REGISTRY_MAX_BYTES=2097152 cog search
98
+ cog search --registry https://github.com/Cognary/cognitive/releases/download/v2.2.9/cognitive-registry.v2.json
99
+ cog registry verify --remote --index https://github.com/Cognary/cognitive/releases/latest/download/cognitive-registry.v2.json
100
+ cog registry verify --remote --concurrency 2
79
101
  ```
80
102
 
81
103
  ## 开发
package/bin.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stable CLI entrypoint.
4
+ *
5
+ * Motivation:
6
+ * - Avoid `npm publish` warnings when `bin` points into `dist/` before build runs.
7
+ * - Keep runtime entrypoint stable even if build output paths change.
8
+ *
9
+ * This file is shipped in the published package and forwards to the compiled CLI.
10
+ */
11
+
12
+ import { spawnSync } from 'node:child_process';
13
+ import { fileURLToPath } from 'node:url';
14
+ import path from 'node:path';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+
19
+ const cliPath = path.join(__dirname, 'dist', 'cli.js');
20
+ const args = process.argv.slice(2);
21
+
22
+ const res = spawnSync(process.execPath, [cliPath, ...args], { stdio: 'inherit' });
23
+
24
+ if (res.error) {
25
+ console.error(res.error);
26
+ process.exit(1);
27
+ }
28
+
29
+ process.exit(res.status ?? 1);
30
+
@@ -0,0 +1,13 @@
1
+ export interface AuditRecord {
2
+ ts: string;
3
+ kind: 'run' | 'pipe' | 'compose';
4
+ policy?: unknown;
5
+ provider?: string;
6
+ module?: unknown;
7
+ input?: unknown;
8
+ result?: unknown;
9
+ notes?: string[];
10
+ }
11
+ export declare function writeAuditRecord(record: AuditRecord): Promise<{
12
+ path: string;
13
+ } | null>;
package/dist/audit.js ADDED
@@ -0,0 +1,25 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ function safeSlug(s) {
5
+ const out = s.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9._-]/g, '-');
6
+ return out.length > 0 ? out.slice(0, 80) : 'unknown';
7
+ }
8
+ export async function writeAuditRecord(record) {
9
+ // Keep it simple and predictable: write under ~/.cognitive/audit/
10
+ const dir = path.join(os.homedir(), '.cognitive', 'audit');
11
+ const ts = record.ts.replace(/[:.]/g, '-');
12
+ const kind = record.kind;
13
+ const moduleName = safeSlug(typeof record.module?.name === 'string' ? record.module.name : 'module');
14
+ const filename = `${ts}-${kind}-${moduleName}-${Math.random().toString(16).slice(2, 10)}.json`;
15
+ try {
16
+ await fs.mkdir(dir, { recursive: true });
17
+ const outPath = path.join(dir, filename);
18
+ await fs.writeFile(outPath, JSON.stringify(record, null, 2) + '\n', 'utf-8');
19
+ return { path: outPath };
20
+ }
21
+ catch {
22
+ // Audit must never break execution.
23
+ return null;
24
+ }
25
+ }
package/dist/cli.js CHANGED
@@ -15,9 +15,12 @@
15
15
  */
16
16
  import { parseArgs } from 'node:util';
17
17
  import { getProvider, listProviders } from './providers/index.js';
18
- import { run, list, pipe, init, add, update, remove, versions, compose, composeInfo, validate, validateAll, migrate, migrateAll, test, testAll, search, listCategories, info } from './commands/index.js';
18
+ import { run, list, pipe, init, add, update, remove, versions, compose, composeInfo, validate, validateAll, migrate, migrateAll, test, testAll, search, listCategories, info, core } from './commands/index.js';
19
19
  import { listModules, getDefaultSearchPaths } from './modules/loader.js';
20
20
  import { VERSION } from './version.js';
21
+ import { resolveExecutionPolicy } from './profile.js';
22
+ import { buildRegistryAssets, verifyRegistryAssets } from './registry/assets.js';
23
+ import { DEFAULT_REGISTRY_URL } from './registry/client.js';
21
24
  async function main() {
22
25
  const args = process.argv.slice(2);
23
26
  const command = args[0];
@@ -33,6 +36,12 @@ async function main() {
33
36
  const { values, positionals } = parseArgs({
34
37
  args: args.slice(1),
35
38
  options: {
39
+ help: { type: 'boolean', short: 'h', default: false },
40
+ stdin: { type: 'boolean', default: false }, // core: read module prompt from stdin
41
+ force: { type: 'boolean', default: false }, // core promote: overwrite existing target dir
42
+ profile: { type: 'string' }, // progressive complexity profile
43
+ validate: { type: 'string' }, // auto|on|off (overrides --no-validate)
44
+ audit: { type: 'boolean', default: false }, // write audit record to ~/.cognitive/audit/
36
45
  args: { type: 'string', short: 'a' },
37
46
  input: { type: 'string', short: 'i' },
38
47
  module: { type: 'string', short: 'm' },
@@ -62,9 +71,59 @@ async function main() {
62
71
  format: { type: 'string', short: 'f' },
63
72
  // Search options
64
73
  category: { type: 'string', short: 'c' },
74
+ registry: { type: 'string' }, // override registry index URL (or use env COGNITIVE_REGISTRY_URL)
75
+ 'registry-timeout-ms': { type: 'string' },
76
+ 'registry-max-bytes': { type: 'string' },
77
+ // Registry build/verify options
78
+ 'modules-dir': { type: 'string' },
79
+ 'v1-registry': { type: 'string' },
80
+ 'out-dir': { type: 'string' },
81
+ 'registry-out': { type: 'string' },
82
+ namespace: { type: 'string' },
83
+ 'runtime-min': { type: 'string' },
84
+ repository: { type: 'string' },
85
+ homepage: { type: 'string' },
86
+ license: { type: 'string' },
87
+ timestamp: { type: 'string' },
88
+ only: { type: 'string', multiple: true },
89
+ index: { type: 'string' },
90
+ 'assets-dir': { type: 'string' },
91
+ 'tarball-base-url': { type: 'string' },
92
+ remote: { type: 'boolean', default: false }, // registry verify: fetch index + tarballs over network
93
+ 'fetch-timeout-ms': { type: 'string' },
94
+ 'max-index-bytes': { type: 'string' },
95
+ 'max-tarball-bytes': { type: 'string' },
96
+ concurrency: { type: 'string' },
65
97
  },
66
98
  allowPositionals: true,
67
99
  });
100
+ if (values.help) {
101
+ if (command === 'core') {
102
+ console.log(JSON.stringify({
103
+ usage: [
104
+ 'cog core new [file.md] [--dry-run]',
105
+ 'cog core schema <file.md> [--pretty]',
106
+ 'cog core run <file.md> [--args \"...\"] [--pretty] [--stream] [--no-validate]',
107
+ 'cog core run --stdin [--args \"...\"] [--pretty] [--stream] [--no-validate]',
108
+ 'cog core promote <file.md> [outDir] [--dry-run] [--force]',
109
+ ],
110
+ }, null, values.pretty ? 2 : 0));
111
+ process.exit(0);
112
+ }
113
+ printHelp();
114
+ process.exit(0);
115
+ }
116
+ // Guard Core-only flags so we don't silently ignore them on other commands.
117
+ if (command !== 'core') {
118
+ if (values.stdin) {
119
+ console.error('Error: --stdin is only supported for `cog core run --stdin`');
120
+ process.exit(1);
121
+ }
122
+ if (values.force) {
123
+ console.error('Error: --force is only supported for `cog core promote --force`');
124
+ process.exit(1);
125
+ }
126
+ }
68
127
  // Get provider
69
128
  let provider;
70
129
  try {
@@ -74,13 +133,72 @@ async function main() {
74
133
  console.error(`Error: ${e instanceof Error ? e.message : e}`);
75
134
  process.exit(1);
76
135
  }
136
+ let policy;
137
+ try {
138
+ policy = resolveExecutionPolicy({
139
+ profile: values.profile,
140
+ validate: values.validate,
141
+ noValidate: values['no-validate'],
142
+ audit: values.audit,
143
+ });
144
+ }
145
+ catch (e) {
146
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
147
+ process.exit(1);
148
+ }
149
+ const parsePositive = (key, raw) => {
150
+ if (raw === undefined || raw === null || raw === '')
151
+ return undefined;
152
+ const n = Number(raw);
153
+ if (!Number.isFinite(n) || n <= 0) {
154
+ throw new Error(`Invalid --${key}: ${String(raw)} (expected a positive number)`);
155
+ }
156
+ return Math.floor(n);
157
+ };
77
158
  const ctx = {
78
159
  cwd: process.cwd(),
79
160
  provider,
80
161
  verbose: values.verbose,
162
+ policy,
163
+ registryUrl: values.registry ?? undefined,
164
+ registryTimeoutMs: parsePositive('registry-timeout-ms', values['registry-timeout-ms']),
165
+ registryMaxBytes: parsePositive('registry-max-bytes', values['registry-max-bytes']),
81
166
  };
82
167
  try {
83
168
  switch (command) {
169
+ case 'core': {
170
+ const sub = positionals[0];
171
+ const target = positionals[1];
172
+ const rest = positionals.slice(2);
173
+ const result = await core(sub, target, ctx, {
174
+ args: values.args,
175
+ input: values.input,
176
+ noValidate: values['no-validate'],
177
+ pretty: values.pretty,
178
+ verbose: values.verbose,
179
+ stream: values.stream,
180
+ dryRun: values['dry-run'],
181
+ stdin: values.stdin,
182
+ force: values.force,
183
+ }, rest);
184
+ if (!result.success) {
185
+ // Keep parity with `cog run`: if an error envelope exists, print it.
186
+ // This avoids confusing "Error: undefined" when the command returns a valid envelope
187
+ // but `error` string is not set.
188
+ if (result.data) {
189
+ console.log(JSON.stringify(result.data, null, values.pretty ? 2 : 0));
190
+ }
191
+ else {
192
+ console.error(`Error: ${result.error ?? 'Unknown error'}`);
193
+ }
194
+ process.exit(1);
195
+ }
196
+ // Stream mode prints events as NDJSON already (via `cog run`).
197
+ if (!(values.stream && sub === 'run')) {
198
+ console.log(JSON.stringify(result.data, null, values.pretty ? 2 : 0));
199
+ }
200
+ break;
201
+ }
84
202
  case 'run': {
85
203
  const moduleName = args[1];
86
204
  if (!moduleName || moduleName.startsWith('-')) {
@@ -90,6 +208,7 @@ async function main() {
90
208
  const result = await run(moduleName, ctx, {
91
209
  args: values.args,
92
210
  input: values.input,
211
+ // policy.validate is resolved in run(); keep legacy flag compatibility here.
93
212
  noValidate: values['no-validate'],
94
213
  pretty: values.pretty,
95
214
  verbose: values.verbose,
@@ -388,6 +507,7 @@ async function main() {
388
507
  const result = await compose(moduleName, ctx, {
389
508
  args: values.args,
390
509
  input: values.input,
510
+ noValidate: values['no-validate'],
391
511
  maxDepth: values['max-depth'] ? parseInt(values['max-depth'], 10) : undefined,
392
512
  timeout: values.timeout ? parseInt(values.timeout, 10) : undefined,
393
513
  trace: values.trace,
@@ -770,6 +890,52 @@ async function main() {
770
890
  await client.fetchRegistry(true);
771
891
  console.log('✓ Registry cache refreshed');
772
892
  }
893
+ else if (subCommand === 'build') {
894
+ const result = await buildRegistryAssets({
895
+ tag: values.tag ?? null,
896
+ tarballBaseUrl: values['tarball-base-url'] ?? null,
897
+ modulesDir: values['modules-dir'] ?? 'cognitive/modules',
898
+ v1RegistryPath: values['v1-registry'] ?? 'cognitive-registry.json',
899
+ outDir: values['out-dir'] ?? 'dist/registry-assets',
900
+ registryOut: values['registry-out'] ?? 'cognitive-registry.v2.json',
901
+ namespace: values.namespace ?? 'official',
902
+ runtimeMin: values['runtime-min'] ?? '2.2.0',
903
+ repository: values.repository ?? 'https://github.com/Cognary/cognitive',
904
+ homepage: values.homepage ?? 'https://cognary.github.io/cognitive/',
905
+ license: values.license ?? 'MIT',
906
+ timestamp: values.timestamp ?? null,
907
+ only: Array.isArray(values.only) ? values.only : (values.only ? [String(values.only)] : []),
908
+ });
909
+ console.log(JSON.stringify({ ok: true, ...result }, null, values.pretty ? 2 : 0));
910
+ }
911
+ else if (subCommand === 'verify') {
912
+ const remote = Boolean(values.remote);
913
+ const defaultIndex = (typeof process.env.COGNITIVE_REGISTRY_URL === 'string' && process.env.COGNITIVE_REGISTRY_URL.trim()
914
+ ? process.env.COGNITIVE_REGISTRY_URL.trim()
915
+ : undefined) ??
916
+ ctx.registryUrl ??
917
+ DEFAULT_REGISTRY_URL;
918
+ const indexPath = values.index ??
919
+ (remote ? defaultIndex : 'cognitive-registry.v2.json');
920
+ const assetsDir = values['assets-dir'] ?? (remote ? undefined : 'dist/registry-assets');
921
+ const fetchTimeoutMs = parsePositive('fetch-timeout-ms', values['fetch-timeout-ms']);
922
+ const maxIndexBytes = parsePositive('max-index-bytes', values['max-index-bytes']);
923
+ const maxTarballBytes = parsePositive('max-tarball-bytes', values['max-tarball-bytes']);
924
+ const concurrency = parsePositive('concurrency', values.concurrency);
925
+ const verified = await verifyRegistryAssets({
926
+ registryIndexPath: indexPath,
927
+ assetsDir,
928
+ remote,
929
+ fetchTimeoutMs,
930
+ maxIndexBytes,
931
+ maxTarballBytes,
932
+ concurrency,
933
+ });
934
+ console.log(JSON.stringify(verified, null, values.pretty ? 2 : 0));
935
+ if (!verified.ok) {
936
+ process.exit(1);
937
+ }
938
+ }
773
939
  else {
774
940
  console.error(`Unknown registry subcommand: ${subCommand}`);
775
941
  console.error('');
@@ -778,6 +944,9 @@ async function main() {
778
944
  console.error(' cog registry categories List categories');
779
945
  console.error(' cog registry info <mod> Show module details');
780
946
  console.error(' cog registry refresh Refresh cache');
947
+ console.error(' cog registry build Build registry tarballs + v2 index (local)');
948
+ console.error(' cog registry verify Verify local tarballs against v2 index');
949
+ console.error(' cog registry verify --remote --index <url> Verify remote index+tarballs');
781
950
  process.exit(1);
782
951
  }
783
952
  break;
@@ -805,6 +974,7 @@ USAGE:
805
974
  cog <command> [options]
806
975
 
807
976
  COMMANDS:
977
+ core <cmd> Minimal "one-file" workflow (new, schema, run)
808
978
  run <module> Run a Cognitive Module
809
979
  test <module> Run golden tests for a module
810
980
  compose <module> Execute a composed module workflow
@@ -815,7 +985,7 @@ COMMANDS:
815
985
  remove <module> Remove installed module
816
986
  versions <url> List available versions
817
987
  search [query] Search modules in registry
818
- registry <cmd> Registry commands (list, categories, info, refresh)
988
+ registry <cmd> Registry commands (list, categories, info, refresh, build, verify)
819
989
  validate <module> Validate module structure
820
990
  migrate <module> Migrate module to v2.2 format
821
991
  pipe Pipe mode (stdin/stdout)
@@ -825,6 +995,9 @@ COMMANDS:
825
995
  doctor Check environment and configuration
826
996
 
827
997
  OPTIONS:
998
+ --profile <name> Progressive complexity profile: core|default|strict|certified
999
+ --validate <mode> Validation mode: auto|on|off (overrides --no-validate)
1000
+ --audit Write an audit record to ~/.cognitive/audit/ (stderr prints path)
828
1001
  -a, --args <str> Arguments to pass to module
829
1002
  -i, --input <json> JSON input for module
830
1003
  -m, --module <name> Module path within repo (for add)
@@ -836,6 +1009,8 @@ OPTIONS:
836
1009
  --pretty Pretty-print JSON output
837
1010
  -V, --verbose Verbose output
838
1011
  --no-validate Skip schema validation
1012
+ --stdin Read module prompt from stdin (for core run)
1013
+ --force Overwrite target directory (for core promote)
839
1014
  -H, --host <host> Server host (default: 0.0.0.0)
840
1015
  -P, --port <port> Server port (default: 8000)
841
1016
  -d, --max-depth <n> Max composition depth (default: 5)
@@ -847,11 +1022,21 @@ OPTIONS:
847
1022
  --all Process all modules (for validate/migrate)
848
1023
  -f, --format <fmt> Output format: text or json (for validate)
849
1024
  -c, --category <cat> Filter by category (for search)
1025
+ --registry <url> Override registry index URL (or set env COGNITIVE_REGISTRY_URL)
1026
+ --registry-timeout-ms <ms> Registry index fetch timeout (overrides env COGNITIVE_REGISTRY_TIMEOUT_MS)
1027
+ --registry-max-bytes <n> Registry index max bytes (overrides env COGNITIVE_REGISTRY_MAX_BYTES)
850
1028
  -l, --limit <n> Limit results (for search, versions)
851
1029
  -v, --version Show version
852
1030
  -h, --help Show this help
853
1031
 
854
1032
  EXAMPLES:
1033
+ # One-file Core (no registry required)
1034
+ cog core new demo.md
1035
+ cog core run demo.md --args "hello" --pretty
1036
+ cog core schema demo.md --pretty
1037
+ cog core promote demo.md
1038
+ cog core promote demo.md ./cognitive/modules/demo
1039
+
855
1040
  # Search and discover modules
856
1041
  cog search code review # Search by keywords
857
1042
  cog search # List all available modules
@@ -910,6 +1095,9 @@ ENVIRONMENT:
910
1095
  DASHSCOPE_API_KEY Alibaba Qwen (通义千问)
911
1096
  OLLAMA_HOST Ollama local (default: localhost:11434)
912
1097
  COG_MODEL Override default model for any provider
1098
+ COGNITIVE_REGISTRY_URL Override registry index URL
1099
+ COGNITIVE_REGISTRY_TIMEOUT_MS Registry index fetch timeout (ms)
1100
+ COGNITIVE_REGISTRY_MAX_BYTES Registry index max bytes
913
1101
  `);
914
1102
  }
915
1103
  main().catch(e => {
@@ -18,6 +18,7 @@ import { homedir, tmpdir } from 'node:os';
18
18
  import { createHash } from 'node:crypto';
19
19
  import { RegistryClient } from '../registry/client.js';
20
20
  import { extractTarGzFile } from '../registry/tar.js';
21
+ import { PROVENANCE_SPEC, computeModuleIntegrity, writeModuleProvenance } from '../provenance.js';
21
22
  // Module storage paths
22
23
  const USER_MODULES_DIR = join(homedir(), '.cognitive', 'modules');
23
24
  const INSTALLED_MANIFEST = join(homedir(), '.cognitive', 'installed.json');
@@ -340,9 +341,13 @@ function parseModuleSpec(spec) {
340
341
  export async function addFromRegistry(moduleSpec, ctx, options = {}) {
341
342
  const { name: moduleName, version: requestedVersion } = parseModuleSpec(moduleSpec);
342
343
  const { name: customName, registry: registryUrl } = options;
344
+ const policy = ctx.policy;
343
345
  let tempDir;
344
346
  try {
345
- const client = new RegistryClient(registryUrl);
347
+ const client = new RegistryClient(registryUrl, {
348
+ timeoutMs: ctx.registryTimeoutMs,
349
+ maxBytes: ctx.registryMaxBytes,
350
+ });
346
351
  const moduleInfo = await client.getModule(moduleName);
347
352
  if (!moduleInfo) {
348
353
  return {
@@ -357,6 +362,14 @@ export async function addFromRegistry(moduleSpec, ctx, options = {}) {
357
362
  // Get download info
358
363
  const downloadInfo = await client.getDownloadUrl(moduleName);
359
364
  if (downloadInfo.isGitHub && downloadInfo.githubInfo) {
365
+ if (policy?.profile === 'certified') {
366
+ return {
367
+ success: false,
368
+ error: `Certified profile requires registry tarball provenance.\n` +
369
+ `Registry entry for '${moduleName}' resolves to a GitHub source, which is not allowed in --profile certified.\n` +
370
+ `Use a tarball-based registry entry (distribution.tarball + checksum), or run with --profile strict/default.`,
371
+ };
372
+ }
360
373
  const { org, repo, path, ref } = downloadInfo.githubInfo;
361
374
  // Use addFromGitHub() directly (not add() to avoid recursion)
362
375
  const result = await addFromGitHub(`${org}/${repo}`, ctx, {
@@ -458,6 +471,35 @@ export async function addFromRegistry(moduleSpec, ctx, options = {}) {
458
471
  await mkdir(USER_MODULES_DIR, { recursive: true });
459
472
  copyDir(sourcePath, targetPath);
460
473
  const version = await getModuleVersion(sourcePath);
474
+ // Write provenance + integrity (enables certified profile gating).
475
+ try {
476
+ const integrity = await computeModuleIntegrity(targetPath);
477
+ await writeModuleProvenance(targetPath, {
478
+ spec: PROVENANCE_SPEC,
479
+ createdAt: new Date().toISOString(),
480
+ source: {
481
+ type: 'registry',
482
+ registryUrl: registryUrl ?? null,
483
+ moduleName,
484
+ requestedVersion: requestedVersion ?? null,
485
+ resolvedVersion: version ?? null,
486
+ tarballUrl: downloadInfo.url,
487
+ checksum: downloadInfo.checksum,
488
+ sha256: actualSha256,
489
+ quality: {
490
+ // moduleInfo is typed loosely (v1/v2); best-effort extract for v2.
491
+ verified: moduleInfo?.quality?.verified,
492
+ conformance_level: moduleInfo?.quality?.conformance_level,
493
+ spec_version: moduleInfo?.identity?.spec_version,
494
+ },
495
+ },
496
+ integrity,
497
+ });
498
+ }
499
+ catch (e) {
500
+ // If provenance fails, keep install but warn loudly (non-certified users can still run).
501
+ console.error(`Warning: failed to write provenance for ${safeInstallName}: ${e.message}`);
502
+ }
461
503
  await recordInstall(safeInstallName, {
462
504
  source: downloadInfo.url,
463
505
  githubUrl: downloadInfo.url,
@@ -502,6 +544,14 @@ export async function addFromRegistry(moduleSpec, ctx, options = {}) {
502
544
  export async function addFromGitHub(url, ctx, options = {}) {
503
545
  const { org, repo, fullUrl } = parseGitHubUrl(url);
504
546
  const { module: modulePath, name, branch = 'main', tag } = options;
547
+ const policy = ctx.policy;
548
+ if (policy?.profile === 'certified') {
549
+ return {
550
+ success: false,
551
+ error: `Certified profile requires registry tarball provenance; GitHub installs are not allowed.\n` +
552
+ `Use 'cog add <module>' against a tarball-based registry entry, or run with --profile strict/default.`,
553
+ };
554
+ }
505
555
  // Determine ref (tag takes priority)
506
556
  const ref = tag || branch;
507
557
  const isTag = !!tag;
@@ -546,6 +596,19 @@ export async function addFromGitHub(url, ctx, options = {}) {
546
596
  installedAt: targetPath,
547
597
  installedTime: new Date().toISOString(),
548
598
  });
599
+ // Best-effort provenance for non-certified installs (useful for audit/debug).
600
+ try {
601
+ const integrity = await computeModuleIntegrity(targetPath);
602
+ await writeModuleProvenance(targetPath, {
603
+ spec: PROVENANCE_SPEC,
604
+ createdAt: new Date().toISOString(),
605
+ source: { type: 'github', repoUrl: fullUrl, ref, modulePath: modulePath ?? null },
606
+ integrity,
607
+ });
608
+ }
609
+ catch {
610
+ // ignore
611
+ }
549
612
  // Cleanup temp directory
550
613
  const tempDir = dirname(repoRoot);
551
614
  if (tempDir && tempDir !== '/' && tempDir !== '.' && tempDir !== USER_MODULES_DIR) {
@@ -585,6 +648,10 @@ export async function addFromGitHub(url, ctx, options = {}) {
585
648
  * Add a module from GitHub or Registry (auto-detect source)
586
649
  */
587
650
  export async function add(source, ctx, options = {}) {
651
+ // Allow a global registry override via ctx.registryUrl unless explicitly set.
652
+ if (!options.registry && ctx.registryUrl) {
653
+ options = { ...options, registry: ctx.registryUrl };
654
+ }
588
655
  // Determine source type
589
656
  if (isGitHubSource(source)) {
590
657
  return addFromGitHub(source, ctx, options);
@@ -13,6 +13,8 @@ export interface ComposeOptions {
13
13
  args?: string;
14
14
  /** JSON input data */
15
15
  input?: string;
16
+ /** Disable validation */
17
+ noValidate?: boolean;
16
18
  /** Maximum composition depth */
17
19
  maxDepth?: number;
18
20
  /** Timeout in milliseconds */
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { findModule, getDefaultSearchPaths, executeComposition } from '../modules/index.js';
11
11
  import { ErrorCodes, attachContext, makeErrorEnvelope } from '../errors/index.js';
12
+ import { writeAuditRecord } from '../audit.js';
12
13
  function looksLikeCode(str) {
13
14
  const codeIndicators = [
14
15
  /^(def|function|class|const|let|var|import|export|public|private)\s/,
@@ -34,7 +35,19 @@ export async function compose(moduleName, ctx, options = {}) {
34
35
  data: errorEnvelope,
35
36
  };
36
37
  }
38
+ if (ctx.policy?.requireV22) {
39
+ if (module.formatVersion !== 'v2.2') {
40
+ const errorEnvelope = attachContext(makeErrorEnvelope({
41
+ code: ErrorCodes.INVALID_INPUT,
42
+ message: `Certified profile requires v2.2 modules; got: ${module.formatVersion ?? 'unknown'} (${module.format})`,
43
+ suggestion: "Migrate the module to v2.2, or run with `--profile strict` / `--profile default`",
44
+ }), { module: moduleName, provider: ctx.provider.name });
45
+ return { success: false, error: errorEnvelope.error.message, data: errorEnvelope };
46
+ }
47
+ }
37
48
  try {
49
+ const policy = ctx.policy;
50
+ const startedAt = Date.now();
38
51
  // Parse input if provided as JSON
39
52
  let inputData = {};
40
53
  if (options.input) {
@@ -67,7 +80,31 @@ export async function compose(moduleName, ctx, options = {}) {
67
80
  const result = await executeComposition(moduleName, inputData, ctx.provider, {
68
81
  cwd: ctx.cwd,
69
82
  maxDepth: options.maxDepth,
70
- timeoutMs: options.timeout
83
+ timeoutMs: options.timeout,
84
+ policy,
85
+ validateInput: (() => {
86
+ if (options.noValidate)
87
+ return false;
88
+ if (!policy)
89
+ return true;
90
+ if (policy.validate === 'off')
91
+ return false;
92
+ if (policy.validate === 'on')
93
+ return true;
94
+ return policy.profile !== 'core';
95
+ })(),
96
+ validateOutput: (() => {
97
+ if (options.noValidate)
98
+ return false;
99
+ if (!policy)
100
+ return true;
101
+ if (policy.validate === 'off')
102
+ return false;
103
+ if (policy.validate === 'on')
104
+ return true;
105
+ return policy.profile !== 'core';
106
+ })(),
107
+ enableRepair: policy?.enableRepair ?? true,
71
108
  });
72
109
  if (options.verbose) {
73
110
  console.error('--- Composition Trace ---');
@@ -98,6 +135,28 @@ export async function compose(moduleName, ctx, options = {}) {
98
135
  data: errorEnvelope,
99
136
  };
100
137
  }
138
+ if (policy?.audit) {
139
+ const rec = await writeAuditRecord({
140
+ ts: new Date().toISOString(),
141
+ kind: 'compose',
142
+ policy,
143
+ provider: ctx.provider.name,
144
+ module: { name: module.name, version: module.version, location: module.location, formatVersion: module.formatVersion },
145
+ input: inputData,
146
+ result: {
147
+ ok: result.ok,
148
+ result: result.result,
149
+ moduleResults: result.moduleResults,
150
+ trace: result.trace,
151
+ totalTimeMs: result.totalTimeMs,
152
+ error: result.error,
153
+ },
154
+ notes: [`duration_ms=${Date.now() - startedAt}`],
155
+ });
156
+ if (rec) {
157
+ console.error(`Audit: ${rec.path}`);
158
+ }
159
+ }
101
160
  // Return result
102
161
  if (options.trace) {
103
162
  // Include full result with trace