@shomra/agent 0.3.17 → 0.3.18
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/NOTICE +1 -1
- package/README.md +57 -57
- package/package.json +3 -9
- package/shomra.mjs +9 -7168
- package/src/agents/hook-command.mjs +19 -0
- package/src/agents/hook-files.mjs +41 -0
- package/src/agents/installers.mjs +203 -0
- package/src/artifacts/matchers.mjs +59 -0
- package/src/artifacts/report.mjs +50 -0
- package/src/cli/flags.mjs +68 -0
- package/src/cli/help-sections.mjs +309 -0
- package/src/cli/help.mjs +27 -0
- package/src/cli/main.mjs +55 -0
- package/src/cli/registry.mjs +80 -0
- package/src/cli/suggestions.mjs +33 -0
- package/src/commands/add.mjs +149 -0
- package/src/commands/agent-identity.mjs +46 -0
- package/src/commands/check.mjs +194 -0
- package/src/commands/corpus.mjs +126 -0
- package/src/commands/design.mjs +168 -0
- package/src/commands/doctor.mjs +209 -0
- package/src/commands/fix.mjs +115 -0
- package/src/commands/gate.mjs +154 -0
- package/src/commands/git-hooks.mjs +163 -0
- package/src/commands/init.mjs +36 -0
- package/src/commands/install-hook.mjs +51 -0
- package/src/commands/llm-proxy.mjs +153 -0
- package/src/commands/mcp-add.mjs +185 -0
- package/src/commands/mcp.mjs +143 -0
- package/src/commands/memory-scan.mjs +181 -0
- package/src/commands/model-scan.mjs +99 -0
- package/src/commands/models.mjs +145 -0
- package/src/commands/new.mjs +64 -0
- package/src/commands/plan.mjs +87 -0
- package/src/commands/pr.mjs +249 -0
- package/src/commands/protect.mjs +38 -0
- package/src/commands/provenance.mjs +91 -0
- package/src/commands/redteam.mjs +166 -0
- package/src/commands/rules.mjs +220 -0
- package/src/commands/run.mjs +128 -0
- package/src/commands/scan-zip.mjs +118 -0
- package/src/commands/scan.mjs +102 -0
- package/src/commands/secrets.mjs +99 -0
- package/src/commands/status.mjs +50 -0
- package/src/commands/why.mjs +88 -0
- package/src/core/api-client.mjs +66 -0
- package/src/core/api-key.mjs +6 -0
- package/src/core/circuit-breaker.mjs +42 -0
- package/src/core/config.mjs +37 -0
- package/src/core/exit-codes.mjs +9 -0
- package/src/core/json-file.mjs +13 -0
- package/src/core/numbers.mjs +4 -0
- package/src/core/package-root.mjs +10 -0
- package/src/core/terminal.mjs +16 -0
- package/src/core/version.mjs +14 -0
- package/src/core/wire-limits.mjs +53 -0
- package/src/corpus/screening.mjs +127 -0
- package/{ai-usage.mjs → src/detect/ai-usage.mjs} +0 -27
- package/src/detect/code-sast.mjs +2 -0
- package/{design.mjs → src/detect/design.mjs} +17 -106
- package/src/detect/guard-signals.mjs +18 -0
- package/{model-refs.mjs → src/detect/model-refs.mjs} +18 -77
- package/src/detect/sast/chains.mjs +30 -0
- package/src/detect/sast/path-expressions.mjs +76 -0
- package/src/detect/sast/rules-chains.mjs +33 -0
- package/src/detect/sast/rules-config.mjs +51 -0
- package/src/detect/sast/rules-javascript.mjs +109 -0
- package/src/detect/sast/rules-python.mjs +292 -0
- package/src/detect/sast/scanner.mjs +104 -0
- package/src/detect/sast/source-lines.mjs +115 -0
- package/src/detect/sast/taint.mjs +71 -0
- package/src/detect/signals/artifacts.mjs +113 -0
- package/src/detect/signals/autonomy.mjs +55 -0
- package/src/detect/signals/config-markers.mjs +28 -0
- package/src/detect/signals/credential-harvest.mjs +64 -0
- package/src/detect/signals/durable-claims.mjs +73 -0
- package/src/detect/signals/egress.mjs +56 -0
- package/src/detect/signals/execution-hijack.mjs +128 -0
- package/src/detect/signals/gate.mjs +91 -0
- package/src/detect/signals/injection.mjs +55 -0
- package/src/detect/signals/lines.mjs +42 -0
- package/src/detect/signals/masking.mjs +99 -0
- package/src/detect/signals/memory.mjs +357 -0
- package/src/detect/signals/packages.mjs +45 -0
- package/src/detect/signals/propagation.mjs +86 -0
- package/src/detect/signals/prose-context.mjs +82 -0
- package/src/detect/signals/scan.mjs +91 -0
- package/src/detect/signals/secrets.mjs +85 -0
- package/src/detect/signals/sensitive.mjs +9 -0
- package/src/detect/signals/severity.mjs +10 -0
- package/src/detect/signals/shell.mjs +96 -0
- package/src/detect/signals/staged-fetch.mjs +66 -0
- package/src/detect/signals/text-match.mjs +35 -0
- package/src/gate/batch.mjs +157 -0
- package/src/gate/environment.mjs +122 -0
- package/src/gate/repo-policy.mjs +65 -0
- package/src/gate/result.mjs +53 -0
- package/src/gate/sarif.mjs +33 -0
- package/src/gate/sast.mjs +64 -0
- package/src/gate/suppressions.mjs +0 -0
- package/src/guard/classify.mjs +50 -0
- package/src/guard/emit.mjs +51 -0
- package/src/guard/ignore.mjs +24 -0
- package/src/guard/ledger.mjs +112 -0
- package/src/guard/model-load.mjs +50 -0
- package/src/guard/normalize.mjs +77 -0
- package/src/guard/options.mjs +10 -0
- package/src/guard/prompt-guard.mjs +184 -0
- package/src/guard/report.mjs +35 -0
- package/src/guard/result-guard.mjs +140 -0
- package/src/guard/tool-guard.mjs +166 -0
- package/src/inventory/agent-artifacts.mjs +5 -0
- package/src/inventory/agent-posture.mjs +249 -0
- package/src/inventory/artifacts/classify.mjs +27 -0
- package/src/inventory/artifacts/discover.mjs +187 -0
- package/src/inventory/artifacts/file-read.mjs +42 -0
- package/src/inventory/artifacts/hooks.mjs +14 -0
- package/src/inventory/artifacts/limits.mjs +37 -0
- package/src/inventory/artifacts/marketplaces.mjs +45 -0
- package/src/inventory/artifacts/roots.mjs +20 -0
- package/src/inventory/artifacts/walk.mjs +36 -0
- package/src/inventory/discovery/ai-dependencies.mjs +161 -0
- package/src/inventory/discovery/ai-tools.mjs +23 -0
- package/src/inventory/discovery/all.mjs +40 -0
- package/src/inventory/discovery/coding-agents.mjs +77 -0
- package/src/inventory/discovery/fs-read.mjs +36 -0
- package/src/inventory/discovery/local-runtimes.mjs +53 -0
- package/src/inventory/discovery/mcp-clients.mjs +67 -0
- package/src/inventory/discovery/mcp-servers.mjs +78 -0
- package/src/inventory/discovery/model-keys.mjs +97 -0
- package/src/inventory/discovery/platform.mjs +16 -0
- package/src/inventory/discovery/rules-files.mjs +25 -0
- package/src/inventory/discovery/vector-stores.mjs +176 -0
- package/src/inventory/discovery/workspace.mjs +124 -0
- package/src/inventory/discovery.mjs +10 -0
- package/src/mcp/child-process.mjs +50 -0
- package/src/mcp/config-wrapping.mjs +75 -0
- package/src/mcp/connect-gate.mjs +45 -0
- package/src/mcp/hosts.mjs +16 -0
- package/src/mcp/jsonrpc.mjs +48 -0
- package/src/mcp/lookup.mjs +50 -0
- package/src/mcp/screening.mjs +103 -0
- package/src/mcp/server-tools.mjs +97 -0
- package/src/mcp/server.mjs +102 -0
- package/src/mcp/shim.mjs +205 -0
- package/src/models/lookup.mjs +79 -0
- package/src/models/references.mjs +103 -0
- package/src/rules/context.mjs +98 -0
- package/src/rules/generate.mjs +103 -0
- package/src/rules/sections.mjs +145 -0
- package/src/scaffold/agent-project.mjs +185 -0
- package/src/scaffold/artifact-templates.mjs +35 -0
- package/code-sast.mjs +0 -1063
- package/discovery.mjs +0 -977
- package/guard-ledger.mjs +0 -239
- package/guard-signals.mjs +0 -2055
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { SELF_PATH } from '../agents/hook-command.mjs';
|
|
4
|
+
import { EXIT_USAGE } from '../core/exit-codes.mjs';
|
|
5
|
+
import { bold, dim, green, red, yellow } from '../core/terminal.mjs';
|
|
6
|
+
import { MCP_HOST_CONFIGS, MCP_HOST_KEYS, shomraMcpEntry } from '../mcp/hosts.mjs';
|
|
7
|
+
import { runMcpServer } from '../mcp/server.mjs';
|
|
8
|
+
import { cmdMcpAdd, cmdMcpList } from './mcp-add.mjs';
|
|
9
|
+
|
|
10
|
+
function cmdMcpInstall(flags) {
|
|
11
|
+
const requested = flags.agent
|
|
12
|
+
? String(flags.agent).toLowerCase().split(',').map((s) => s.trim()).filter(Boolean)
|
|
13
|
+
: MCP_HOST_KEYS;
|
|
14
|
+
if (requested.includes('all')) requested.splice(0, requested.length, ...MCP_HOST_KEYS);
|
|
15
|
+
const bad = requested.filter((a) => !MCP_HOST_CONFIGS[a]);
|
|
16
|
+
if (bad.length) {
|
|
17
|
+
console.error(red('✗') + ` No MCP config is known for: ${bad.join(', ')}. Supported: ${MCP_HOST_KEYS.join(', ')}, all.`);
|
|
18
|
+
process.exit(EXIT_USAGE);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const global = !!flags.global;
|
|
22
|
+
const entry = shomraMcpEntry();
|
|
23
|
+
const out = [];
|
|
24
|
+
|
|
25
|
+
for (const key of requested) {
|
|
26
|
+
const host = MCP_HOST_CONFIGS[key];
|
|
27
|
+
const file = global ? host.global() : host.local();
|
|
28
|
+
let cfg = {};
|
|
29
|
+
if (fs.existsSync(file)) {
|
|
30
|
+
try { cfg = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {
|
|
31
|
+
console.log(` ${red('✗')} ${host.label} ${dim('- ' + file + ' is not valid JSON; fix or move it first.')}`);
|
|
32
|
+
out.push({ agent: key, file, changed: false, error: 'invalid json' });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
37
|
+
const before = JSON.stringify(cfg.mcpServers.shomra || null);
|
|
38
|
+
cfg.mcpServers.shomra = entry;
|
|
39
|
+
const changed = before !== JSON.stringify(entry);
|
|
40
|
+
if (changed) {
|
|
41
|
+
try {
|
|
42
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
43
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.log(` ${red('✗')} ${host.label} ${dim('- ' + e.message)}`);
|
|
46
|
+
out.push({ agent: key, file, changed: false, error: e.message });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
out.push({ agent: key, file, changed });
|
|
51
|
+
if (!flags.json) {
|
|
52
|
+
if (changed) console.log(` ${green('✓')} Registered the Shomra MCP server for ${bold(host.label)} ${dim('→ ' + file)}`);
|
|
53
|
+
else console.log(` ${yellow('•')} ${host.label} ${dim('already registered (' + file + ')')}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (flags.json) { console.log(JSON.stringify({ scope: global ? 'global' : 'project', installed: out }, null, 2)); return; }
|
|
58
|
+
console.log(dim('\n The agent can now call Shomra in its own loop: ') + bold('shomra_review_change') + dim(' (gate content BEFORE writing it),'));
|
|
59
|
+
console.log(dim(' ') + bold('shomra_rules') + dim(' (what will be refused here), plus check / explain / fix / scan_models.'));
|
|
60
|
+
console.log(dim(' Restart the agent to pick up the new server.'));
|
|
61
|
+
if (!global) {
|
|
62
|
+
|
|
63
|
+
console.log(dim(' Note: the entry holds absolute paths for THIS machine. If you commit it, teammates should'));
|
|
64
|
+
console.log(dim(' run ') + bold('shomra mcp install') + dim(' themselves rather than rely on the committed path.'));
|
|
65
|
+
}
|
|
66
|
+
console.log('');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function cmdMcpGuard(flags, positional) {
|
|
70
|
+
const { runMcpShim } = await import('../mcp/shim.mjs');
|
|
71
|
+
return runMcpShim(flags, positional);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function cmdMcpGuardInstall(flags) {
|
|
75
|
+
const { wrapMcpConfig, unwrapMcpConfig, mcpConfigCandidates } = await import('../mcp/shim.mjs');
|
|
76
|
+
const undo = !!flags.uninstall;
|
|
77
|
+
const files = flags.config
|
|
78
|
+
? [{ label: 'config', file: path.resolve(String(flags.config)) }]
|
|
79
|
+
: mcpConfigCandidates();
|
|
80
|
+
|
|
81
|
+
if (!files.length) {
|
|
82
|
+
console.log(dim('\n No MCP client config found on this machine. Nothing to guard.\n'));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const results = [];
|
|
87
|
+
for (const { label, file } of files) {
|
|
88
|
+
let cfg;
|
|
89
|
+
try {
|
|
90
|
+
cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.log(` ${red('✗')} ${label} ${dim('- ' + file + ' is not valid JSON; fix or move it first.')}`);
|
|
93
|
+
results.push({ file, error: 'invalid json' });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const before = JSON.stringify(cfg);
|
|
97
|
+
const out = undo ? unwrapMcpConfig(cfg, SELF_PATH) : wrapMcpConfig(cfg, SELF_PATH, process.execPath);
|
|
98
|
+
const changed = JSON.stringify(cfg) !== before;
|
|
99
|
+
if (changed) {
|
|
100
|
+
try {
|
|
101
|
+
|
|
102
|
+
const bak = file + '.shomra-backup';
|
|
103
|
+
if (!undo && !fs.existsSync(bak)) fs.writeFileSync(bak, before);
|
|
104
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.log(` ${red('✗')} ${label} ${dim('- ' + e.message)}`);
|
|
107
|
+
results.push({ file, error: e.message });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
results.push({ file, label, ...out, changed });
|
|
112
|
+
if (!flags.json) {
|
|
113
|
+
const names = undo ? out.restored : out.wrapped;
|
|
114
|
+
if (names.length) console.log(` ${green('✓')} ${bold(label)} ${dim('- ' + (undo ? 'restored ' : 'guarded ') + names.join(', '))}`);
|
|
115
|
+
else console.log(` ${yellow('•')} ${label} ${dim('- nothing to ' + (undo ? 'restore' : 'guard'))}`);
|
|
116
|
+
for (const s of out.skipped ?? []) console.log(` ${dim('· ' + s.name + ' - ' + s.why)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (flags.json) { console.log(JSON.stringify({ mode: undo ? 'uninstall' : 'install', results }, null, 2)); return; }
|
|
121
|
+
if (!undo) {
|
|
122
|
+
console.log(dim('\n Every stdio MCP server now starts through Shomra: a DENIED / REVOKED / QUARANTINED'));
|
|
123
|
+
console.log(dim(' server is refused before its process exists, and poisoned tool descriptions are'));
|
|
124
|
+
console.log(dim(' withheld from the model at tools/list rather than at the first call.'));
|
|
125
|
+
console.log(dim(' Restart the agent to pick up the change. Undo: ') + bold('shomra mcp guard --uninstall') + '\n');
|
|
126
|
+
} else {
|
|
127
|
+
console.log(dim('\n Original launch lines restored. Restart the agent.\n'));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function cmdMcp(flags, positional) {
|
|
132
|
+
const sub = String(positional[0] || '').toLowerCase();
|
|
133
|
+
if (sub === 'guard') return cmdMcpGuardInstall(flags);
|
|
134
|
+
if (sub === 'serve') return runMcpServer(flags);
|
|
135
|
+
if (sub === 'install') return cmdMcpInstall(flags);
|
|
136
|
+
|
|
137
|
+
const configFile = path.resolve(flags.config ? String(flags.config) : '.mcp.json');
|
|
138
|
+
if (sub === 'list') return cmdMcpList(flags, configFile);
|
|
139
|
+
if (sub === 'add') return cmdMcpAdd(flags, positional, configFile);
|
|
140
|
+
|
|
141
|
+
console.error(red('✗') + ` Usage: ${bold('shomra mcp add <name> <command…> | --url <url>')} ${dim('|')} ${bold('shomra mcp list')}`);
|
|
142
|
+
process.exit(EXIT_USAGE);
|
|
143
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { MAX_ARTIFACT_BYTES, SKIP_DIRS } from '../artifacts/matchers.mjs';
|
|
5
|
+
import { api } from '../core/api-client.mjs';
|
|
6
|
+
import { guardTimeoutMs } from '../core/circuit-breaker.mjs';
|
|
7
|
+
import { loadConfig, resolveSettings } from '../core/config.mjs';
|
|
8
|
+
import { EXIT_USAGE, exitNotConfigured } from '../core/exit-codes.mjs';
|
|
9
|
+
import { SEV_COLOR, VERDICT_COLOR, bold, cyan, dim, gray, green, red, yellow } from '../core/terminal.mjs';
|
|
10
|
+
|
|
11
|
+
const MEMORY_MATCHERS = [
|
|
12
|
+
/(^|\/)MEMOR(Y|IES)\.(md|json|jsonl|txt)$/i,
|
|
13
|
+
/(^|\/)memor(y|ies)\/[^/]+\.(md|mdx|json|jsonl|txt|ya?ml)$/i,
|
|
14
|
+
/(^|\/)\.(mem0|letta|memgpt)\/[^/]+\.(md|json|jsonl|txt)$/i,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const INSTRUCTION_MATCHERS = [
|
|
18
|
+
/(^|\/)(CLAUDE|AGENTS?|GEMINI|CONVENTIONS)\.md$/i,
|
|
19
|
+
/(^|\/)LLMS(-FULL)?\.txt$/i,
|
|
20
|
+
/(^|\/)\.(cursorrules|windsurfrules|clinerules|aiderrules|continuerules|goosehints)$/i,
|
|
21
|
+
/(^|\/)\.github\/copilot-instructions\.md$/i,
|
|
22
|
+
/(^|\/)copilot-instructions\.md$/i,
|
|
23
|
+
/(^|\/)\.cursor\/rules\/.+\.mdc$/i,
|
|
24
|
+
/(^|\/)\.clinerules\/.+\.md$/i,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function isMemoryPath(p) {
|
|
28
|
+
const rel = String(p || '').split(path.sep).join('/');
|
|
29
|
+
return MEMORY_MATCHERS.some((re) => re.test(rel)) || INSTRUCTION_MATCHERS.some((re) => re.test(rel));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function walkMemoryFiles(root) {
|
|
33
|
+
const found = [];
|
|
34
|
+
const stack = [root];
|
|
35
|
+
while (stack.length) {
|
|
36
|
+
const dir = stack.pop();
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
} catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (const ent of entries) {
|
|
44
|
+
const full = path.join(dir, ent.name);
|
|
45
|
+
if (ent.isDirectory()) {
|
|
46
|
+
if (!SKIP_DIRS.has(ent.name)) stack.push(full);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const rel = path.relative(root, full).split(path.sep).join('/');
|
|
50
|
+
if (isMemoryPath(rel) || isMemoryPath(full)) found.push({ full, rel });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function reportMemoryWrite(url, apiKey, body) {
|
|
57
|
+
try {
|
|
58
|
+
const ctrl = new AbortController();
|
|
59
|
+
const timer = setTimeout(() => ctrl.abort(), guardTimeoutMs());
|
|
60
|
+
await fetch(`${url}/memory/ingest`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json', 'X-Shomra-Key': apiKey, Connection: 'close' },
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
signal: ctrl.signal,
|
|
65
|
+
});
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
} catch {
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const NO_STORES_HINT = '(Looked for MEMORY.md, memory/ dirs, .mem0/.letta stores, and rules files: CLAUDE.md, AGENTS.md, .cursorrules, copilot-instructions.md, …)';
|
|
73
|
+
|
|
74
|
+
function resolveMemoryTarget(positional) {
|
|
75
|
+
const argument = positional[0] || '.';
|
|
76
|
+
const target = path.resolve(String(argument));
|
|
77
|
+
if (!fs.existsSync(target)) {
|
|
78
|
+
console.error(`${red('✗')} Not found: ${argument}`);
|
|
79
|
+
process.exit(EXIT_USAGE);
|
|
80
|
+
}
|
|
81
|
+
return fs.statSync(target).isDirectory()
|
|
82
|
+
? { target, files: walkMemoryFiles(target) }
|
|
83
|
+
: { target, files: [{ full: target, rel: path.basename(target) }] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readStoreContent(file, flags) {
|
|
87
|
+
try {
|
|
88
|
+
if (fs.statSync(file.full).size > MAX_ARTIFACT_BYTES) {
|
|
89
|
+
if (!flags.json) console.log(` ${gray('•')} ${dim(file.rel)} ${yellow('skipped (too large)')}`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return fs.readFileSync(file.full, 'utf8');
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function worseVerdict(current, next) {
|
|
99
|
+
if (current === 'FAIL' || next === 'FAIL') return 'FAIL';
|
|
100
|
+
if (current === 'REVIEW' || next === 'REVIEW') return 'REVIEW';
|
|
101
|
+
return 'PASS';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function printStore(file, report) {
|
|
105
|
+
const verdict = report?.store?.verdict || 'PASS';
|
|
106
|
+
const colour = VERDICT_COLOR[verdict] || gray;
|
|
107
|
+
const poisonScore = report?.store?.poisonScore ?? 0;
|
|
108
|
+
const anomalous = report?.provenance?.anomalous;
|
|
109
|
+
|
|
110
|
+
const quarantineNote = report?.quarantined ? ` ${red('QUARANTINED')}` : '';
|
|
111
|
+
const provenanceNote = anomalous ? ` ${red('OUT-OF-BAND WRITE')}` : '';
|
|
112
|
+
console.log(`\n ${colour('●')} ${bold(path.basename(file.rel))} ${dim(file.rel)} ${colour(verdict)} ${dim(`poison ${poisonScore}/100`)}${quarantineNote}${provenanceNote}`);
|
|
113
|
+
|
|
114
|
+
for (const finding of (report?.analysis?.findings || []).filter((f) => f.severity !== 'INFO')) {
|
|
115
|
+
console.log(` ${SEV_COLOR[finding.severity](String(finding.severity).padEnd(8))} ${finding.title}`);
|
|
116
|
+
}
|
|
117
|
+
if (anomalous) console.log(` ${red('provenance:')} ${dim(report.provenance.reason)}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function summaryLine(worst) {
|
|
121
|
+
if (worst === 'FAIL') return red('✗ Memory poisoning detected - roll back the affected stores.');
|
|
122
|
+
if (worst === 'REVIEW') return yellow('⚠ Review the flagged memory before the agent reloads it.');
|
|
123
|
+
return green('✓ No memory poisoning found.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function cmdMemoryScan(flags, positional) {
|
|
127
|
+
const { apiKey, url } = resolveSettings(loadConfig());
|
|
128
|
+
if (!apiKey) exitNotConfigured();
|
|
129
|
+
|
|
130
|
+
const { target, files } = resolveMemoryTarget(positional);
|
|
131
|
+
if (!files.length) {
|
|
132
|
+
if (flags.json) console.log(JSON.stringify({ scanned: 0, stores: [] }, null, 2));
|
|
133
|
+
else console.log(dim(`\n No memory or rules files found under ${target}.\n ${NO_STORES_HINT}\n`));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const ingestFields = {
|
|
138
|
+
scope: flags.scope ? String(flags.scope).toLowerCase() : undefined,
|
|
139
|
+
writer: flags.writer ? String(flags.writer).toUpperCase() : 'AGENT',
|
|
140
|
+
actor: `${os.hostname()}/${os.userInfo().username}`,
|
|
141
|
+
source: 'shomra memory-scan',
|
|
142
|
+
...(flags.project ? { projectId: String(flags.project) } : {}),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (!flags.json) {
|
|
146
|
+
console.log(bold(cyan('\n Shomra Memory Integrity')) + dim(` - scanning ${files.length} store${files.length > 1 ? 's' : ''}`));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let worst = 'PASS';
|
|
150
|
+
const stores = [];
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
const content = readStoreContent(file, flags);
|
|
153
|
+
if (content == null) continue;
|
|
154
|
+
|
|
155
|
+
let report;
|
|
156
|
+
try {
|
|
157
|
+
report = await api(url, apiKey, '/memory/ingest', {
|
|
158
|
+
...ingestFields,
|
|
159
|
+
path: file.rel,
|
|
160
|
+
name: path.basename(file.rel),
|
|
161
|
+
content,
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(` ${red('✗')} ${file.rel} ${red(`ingest error: ${error.message}`)}`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
worst = worseVerdict(worst, report?.store?.verdict || 'PASS');
|
|
169
|
+
stores.push({ path: file.rel, ...report });
|
|
170
|
+
if (!flags.json) printStore(file, report);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (flags.json) {
|
|
174
|
+
console.log(JSON.stringify({ scanned: stores.length, worst, stores }, null, 2));
|
|
175
|
+
} else {
|
|
176
|
+
console.log(`\n ${summaryLine(worst)}${dim(' Full timeline + rollback in the Shomra dashboard → Memory.\n')}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (worst === 'FAIL') process.exitCode = 1;
|
|
180
|
+
else if (worst === 'REVIEW' && flags.strict) process.exitCode = 2;
|
|
181
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { api } from '../core/api-client.mjs';
|
|
3
|
+
import { loadConfig, resolveSettings } from '../core/config.mjs';
|
|
4
|
+
import { EXIT_USAGE, exitNotConfigured } from '../core/exit-codes.mjs';
|
|
5
|
+
import { SEV_COLOR, VERDICT_COLOR, bold, dim, gray, green, red, yellow } from '../core/terminal.mjs';
|
|
6
|
+
import { modelLookup, printAlternatives } from '../models/lookup.mjs';
|
|
7
|
+
|
|
8
|
+
const USAGE = 'shomra model-scan <hf-url | owner/model | github-url> [--project <id>] [--json]';
|
|
9
|
+
|
|
10
|
+
async function requestModelScan({ url, apiKey, target, flags }) {
|
|
11
|
+
if (!flags.json) process.stdout.write(dim(`\n Scanning ${target}… `));
|
|
12
|
+
try {
|
|
13
|
+
const report = await api(url, apiKey, '/projects/agent-model-scan', {
|
|
14
|
+
target: String(target),
|
|
15
|
+
actor: `${os.hostname()}/${os.userInfo().username}`,
|
|
16
|
+
...(flags.project ? { projectId: String(flags.project) } : {}),
|
|
17
|
+
});
|
|
18
|
+
if (!flags.json) console.log(green('done'));
|
|
19
|
+
return report;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (!flags.json) console.log(red('failed'));
|
|
22
|
+
console.error(` ${red('✗')} ${error.message}\n`);
|
|
23
|
+
return process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function printAssetVulnerabilities(report) {
|
|
28
|
+
for (const asset of report.assets || []) {
|
|
29
|
+
const vulnerabilities = (asset.vulnerabilities || []).filter((v) => v.severity !== 'INFO');
|
|
30
|
+
if (!vulnerabilities.length) continue;
|
|
31
|
+
|
|
32
|
+
console.log(`\n ${bold(asset.name)} ${dim(`(${asset.assetType})`)}`);
|
|
33
|
+
for (const vulnerability of vulnerabilities) {
|
|
34
|
+
console.log(` ${SEV_COLOR[vulnerability.severity](String(vulnerability.severity).padEnd(8))} ${vulnerability.title}`);
|
|
35
|
+
const evidence = vulnerability.evidence?.analysis === 'sast' ? vulnerability.evidence : null;
|
|
36
|
+
if (!evidence) continue;
|
|
37
|
+
const source = evidence.source ? ` · source ${evidence.source}` : '';
|
|
38
|
+
console.log(` ${dim(`${evidence.ruleId} · ${evidence.file}:${evidence.line} · sink ${evidence.sink}${source}`)}`);
|
|
39
|
+
if (evidence.snippet) console.log(` ${gray(evidence.snippet)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function printSaferAlternatives(url, report, target) {
|
|
45
|
+
if (report.verdict !== 'FAIL' && report.verdict !== 'REVIEW') return;
|
|
46
|
+
const modelId = hfModelIdFromTarget(String(report.target || target));
|
|
47
|
+
if (!modelId) return;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const lookup = await modelLookup(url, modelId);
|
|
51
|
+
if (!Array.isArray(lookup?.alternatives) || !lookup.alternatives.length) return;
|
|
52
|
+
console.log(`\n ${bold('Safer alternatives')}${dim(' (same category, lower risk):')}`);
|
|
53
|
+
printAlternatives(lookup.alternatives, 'model', ' ');
|
|
54
|
+
} catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function summaryLine(verdict) {
|
|
60
|
+
if (verdict === 'FAIL') return red('✗ Do not load this model unreviewed.');
|
|
61
|
+
if (verdict === 'REVIEW') return yellow('⚠ Review the findings above before trusting this model.');
|
|
62
|
+
return green('✓ No high-severity issues found.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function cmdModelScan(flags, positional) {
|
|
66
|
+
const { apiKey, url } = resolveSettings(loadConfig());
|
|
67
|
+
if (!apiKey) exitNotConfigured();
|
|
68
|
+
|
|
69
|
+
const target = positional[0];
|
|
70
|
+
if (!target) {
|
|
71
|
+
console.error(`${red('✗')} Usage: ${bold(USAGE)}`);
|
|
72
|
+
process.exit(EXIT_USAGE);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const report = await requestModelScan({ url, apiKey, target, flags });
|
|
76
|
+
|
|
77
|
+
if (flags.json) {
|
|
78
|
+
console.log(JSON.stringify(report, null, 2));
|
|
79
|
+
} else {
|
|
80
|
+
const colour = VERDICT_COLOR[report.verdict] || gray;
|
|
81
|
+
console.log(`\n ${bold(report.target || target)} ${dim(`· ${report.scanType} scan`)}`);
|
|
82
|
+
console.log(` ${colour('●')} ${colour(bold(report.verdict))} ${dim(`risk ${report.riskScore}/100 · ${report.vulnCount} finding(s) · ${report.criticalCount} critical · ${report.highCount} high`)}`);
|
|
83
|
+
|
|
84
|
+
printAssetVulnerabilities(report);
|
|
85
|
+
await printSaferAlternatives(url, report, target);
|
|
86
|
+
console.log(`\n ${summaryLine(report.verdict)}${dim(' Full report in the Shomra dashboard → Projects.\n')}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (report.verdict === 'FAIL') process.exitCode = 1;
|
|
90
|
+
else if (report.verdict === 'REVIEW' && flags.strict) process.exitCode = 2;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hfModelIdFromTarget(target) {
|
|
94
|
+
const t = String(target || '').trim();
|
|
95
|
+
const hf = t.match(/huggingface\.co\/([^/\s?#]+\/[^/\s?#]+)/i);
|
|
96
|
+
if (hf) return hf[1];
|
|
97
|
+
if (/^[\w.-]+\/[\w.-]+$/.test(t) && !/github\.com/i.test(t)) return t;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadConfig, resolveSettings } from '../core/config.mjs';
|
|
3
|
+
import { clampInt } from '../core/numbers.mjs';
|
|
4
|
+
import { SEV_COLOR, bold, cyan, dim, gray, green, red, yellow } from '../core/terminal.mjs';
|
|
5
|
+
import { printAlternatives } from '../models/lookup.mjs';
|
|
6
|
+
import { assessModel, collectModelReferences, lookupModels, resolveScanScope } from '../models/references.mjs';
|
|
7
|
+
import { walkFiles } from './secrets.mjs';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_CONCURRENCY = 6;
|
|
10
|
+
const MIN_CONCURRENCY = 1;
|
|
11
|
+
const MAX_CONCURRENCY = 16;
|
|
12
|
+
const MAX_LOCATIONS_SHOWN = 3;
|
|
13
|
+
const MAX_FINDINGS_SHOWN = 3;
|
|
14
|
+
|
|
15
|
+
function lookupUrl(url, reference) {
|
|
16
|
+
const query = `id=${encodeURIComponent(reference.id)}${reference.sha ? `&sha=${reference.sha}` : ''}`;
|
|
17
|
+
return `${url}/models/lookup?${query}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printNothingFound(flags, root) {
|
|
21
|
+
if (flags.json) {
|
|
22
|
+
console.log(JSON.stringify({ detected: 0, models: [] }, null, 2));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const where = path.relative(process.cwd(), root).split(path.sep).join('/') || '.';
|
|
26
|
+
console.log(`${green('\n ✓ No AI model references found in the code.')}${dim(` (looked under ${where})`)}\n`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printDryRun(flags, references, url) {
|
|
30
|
+
if (flags.json) {
|
|
31
|
+
console.log(JSON.stringify({
|
|
32
|
+
detected: references.length,
|
|
33
|
+
models: references.map((reference) => ({
|
|
34
|
+
id: reference.id,
|
|
35
|
+
sha: reference.sha,
|
|
36
|
+
source: reference.source,
|
|
37
|
+
lookup: lookupUrl(url, reference),
|
|
38
|
+
locations: reference.locations,
|
|
39
|
+
})),
|
|
40
|
+
}, null, 2));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log(bold(cyan('\n Shomra models')) + dim(` - ${references.length} reference(s) detected (dry run - no lookup)`));
|
|
44
|
+
for (const reference of references) {
|
|
45
|
+
const revision = reference.sha ? dim(`@${reference.sha}`) : '';
|
|
46
|
+
console.log(` ${gray('•')} ${bold(reference.id)}${revision} ${dim(`(${reference.source})`)} ${dim(`→ ${url}/models/lookup?id=${reference.id}`)}`);
|
|
47
|
+
}
|
|
48
|
+
console.log('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function statusLabel(model) {
|
|
52
|
+
if (model.error) return yellow('lookup failed');
|
|
53
|
+
if (model.notIndexed) {
|
|
54
|
+
return dim(model.source === 'ollama' ? 'local runtime - not in the index' : 'not in the index yet');
|
|
55
|
+
}
|
|
56
|
+
const verdictColour = model.verdict === 'FAIL' ? red : model.verdict === 'REVIEW' ? yellow : green;
|
|
57
|
+
return `${verdictColour(model.verdict)} ${dim(`risk ${model.riskScore} · ${model.findingCount} vuln(s)`)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function alertMark(model) {
|
|
61
|
+
if (model.alert === 'BLOCK') return red('●');
|
|
62
|
+
if (model.alert === 'FLAG') return yellow('●');
|
|
63
|
+
return model.notIndexed ? gray('○') : green('●');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function printModel(model) {
|
|
67
|
+
const revision = model.sha ? dim(`@${String(model.sha).slice(0, 12)}`) : '';
|
|
68
|
+
console.log(` ${alertMark(model)} ${bold(model.id)}${revision} ${dim(`(${model.source})`)} ${statusLabel(model)}`);
|
|
69
|
+
|
|
70
|
+
const shown = model.locations.slice(0, MAX_LOCATIONS_SHOWN).map((l) => `${l.file}:${l.line}`).join(', ');
|
|
71
|
+
const more = model.locations.length > MAX_LOCATIONS_SHOWN ? ` (+${model.locations.length - MAX_LOCATIONS_SHOWN})` : '';
|
|
72
|
+
console.log(` ${dim(`used in ${shown}${more}`)}`);
|
|
73
|
+
|
|
74
|
+
for (const finding of model.findings.slice(0, MAX_FINDINGS_SHOWN)) {
|
|
75
|
+
console.log(` ${(SEV_COLOR[finding.severity] || dim)(String(finding.severity).padEnd(8))} ${finding.title}`);
|
|
76
|
+
}
|
|
77
|
+
printAlternatives(model.alternatives, 'model');
|
|
78
|
+
|
|
79
|
+
if (model.notIndexed && model.source !== 'ollama') {
|
|
80
|
+
console.log(` ${dim('→ scan it now:')} ${bold(`shomra model-scan ${model.id}`)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function uncheckedNotice(models, url) {
|
|
85
|
+
const failed = models.filter((model) => model.error).length;
|
|
86
|
+
if (!failed) return null;
|
|
87
|
+
const why = url ? ' (model index unreachable)' : ' (no backend configured - set SHOMRA_URL)';
|
|
88
|
+
return yellow(`⚠ ${failed} model reference(s) could not be checked`) + dim(why);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function summaryLine({ models, blocked, flagged, url }) {
|
|
92
|
+
const unchecked = uncheckedNotice(models, url);
|
|
93
|
+
const suffix = unchecked ? ` · ${unchecked}` : '';
|
|
94
|
+
if (blocked) return red(`✗ ${blocked} vulnerable`) + dim(` · ${flagged} to review`) + suffix;
|
|
95
|
+
if (flagged) return yellow(`⚠ ${flagged} to review`) + suffix;
|
|
96
|
+
return unchecked || green('✓ No known-vulnerable models');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function printReport({ models, blocked, flagged, url, indexUnreachable }) {
|
|
100
|
+
const indexNote = url ? ` · index at ${url}` : ' · local detection only';
|
|
101
|
+
console.log(bold(cyan('\n Shomra models')) + dim(` - ${models.length} model reference(s)${indexNote}`));
|
|
102
|
+
|
|
103
|
+
if (indexUnreachable) {
|
|
104
|
+
const why = url
|
|
105
|
+
? 'Model index unreachable - could not fetch vulnerability info.'
|
|
106
|
+
: 'Model index not configured - set SHOMRA_URL to check models against the Shomra Model Index.';
|
|
107
|
+
console.log(` ${yellow('⚠')} ${dim(why)}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const model of models) printModel(model);
|
|
111
|
+
console.log(`\n ${summaryLine({ models, blocked, flagged, url })}\n`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function cmdModels(flags, positional) {
|
|
115
|
+
const { url } = resolveSettings(loadConfig());
|
|
116
|
+
const target = path.resolve(positional[0] || flags.path || '.');
|
|
117
|
+
|
|
118
|
+
const { root, entries } = resolveScanScope(target, walkFiles);
|
|
119
|
+
const references = collectModelReferences(root, entries);
|
|
120
|
+
|
|
121
|
+
if (!references.length) {
|
|
122
|
+
printNothingFound(flags, root);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (flags['dry-run']) {
|
|
126
|
+
printDryRun(flags, references, url);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const concurrency = clampInt(process.env.SHOMRA_GATE_CONCURRENCY, DEFAULT_CONCURRENCY, MIN_CONCURRENCY, MAX_CONCURRENCY);
|
|
131
|
+
const { results, indexUnreachable } = await lookupModels(url, references, concurrency);
|
|
132
|
+
const models = references.map((reference, index) => assessModel(reference, results[index]));
|
|
133
|
+
|
|
134
|
+
const blocked = models.filter((model) => model.alert === 'BLOCK').length;
|
|
135
|
+
const flagged = models.filter((model) => model.alert === 'FLAG').length;
|
|
136
|
+
|
|
137
|
+
if (flags.json) {
|
|
138
|
+
console.log(JSON.stringify({ detected: models.length, blocked, flagged, apiDown: indexUnreachable, url, models }, null, 2));
|
|
139
|
+
} else {
|
|
140
|
+
printReport({ models, blocked, flagged, url, indexUnreachable });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (blocked) process.exitCode = 1;
|
|
144
|
+
else if (flagged && flags.strict) process.exitCode = 2;
|
|
145
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { EXIT_USAGE } from '../core/exit-codes.mjs';
|
|
4
|
+
import { bold, dim, green, red, yellow } from '../core/terminal.mjs';
|
|
5
|
+
import { localGate } from '../detect/guard-signals.mjs';
|
|
6
|
+
import { AGENT_FRAMEWORKS, agentProjectFiles } from '../scaffold/agent-project.mjs';
|
|
7
|
+
import { NEW_TEMPLATES } from '../scaffold/artifact-templates.mjs';
|
|
8
|
+
|
|
9
|
+
function cmdNewAgent(flags, positional) {
|
|
10
|
+
const framework = String(flags.framework || AGENT_FRAMEWORKS[0]).toLowerCase();
|
|
11
|
+
if (!AGENT_FRAMEWORKS.includes(framework)) {
|
|
12
|
+
console.error(red('✗') + ` Unknown --framework: ${framework}. Supported: ${AGENT_FRAMEWORKS.join(', ')}.`);
|
|
13
|
+
process.exit(EXIT_USAGE);
|
|
14
|
+
}
|
|
15
|
+
const name = (positional[0] || 'my-agent').replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
16
|
+
const dir = path.resolve(name);
|
|
17
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length && !flags.force) {
|
|
18
|
+
console.error(red('✗') + ` ${name}/ already exists and is not empty. Use ${bold('--force')} to write into it anyway.`);
|
|
19
|
+
process.exit(EXIT_USAGE);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const files = agentProjectFiles(name);
|
|
23
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
24
|
+
const abs = path.join(dir, rel);
|
|
25
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
26
|
+
fs.writeFileSync(abs, content);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (flags.json) {
|
|
30
|
+
console.log(JSON.stringify({ created: name, framework, files: Object.keys(files) }, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log(`\n ${green('✓ Created')} ${bold(name + '/')} ${dim('· ' + framework + ' · ' + Object.keys(files).length + ' files')}`);
|
|
34
|
+
for (const rel of Object.keys(files)) console.log(` ${dim('+')} ${rel}`);
|
|
35
|
+
console.log(`\n ${bold('Next')}`);
|
|
36
|
+
console.log(` cd ${name} && cp .env.example .env && npm install`);
|
|
37
|
+
console.log(` ${bold('shomra rules --write')} ${dim('- write the agent rules block into CLAUDE.md')}`);
|
|
38
|
+
console.log(` ${bold('shomra check')} ${dim('- confirm it starts clean')}`);
|
|
39
|
+
console.log(dim('\n Guard enforcing, egress allowlisted, secrets in env, gate in CI - from commit zero.\n'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function cmdNew(flags, positional) {
|
|
43
|
+
const kind = String(positional[0] || '').toLowerCase();
|
|
44
|
+
|
|
45
|
+
if (kind === 'agent') return cmdNewAgent(flags, positional.slice(1));
|
|
46
|
+
const tmpl = NEW_TEMPLATES[kind];
|
|
47
|
+
if (!tmpl) {
|
|
48
|
+
console.error(red('✗') + ` Usage: ${bold('shomra new ' + Object.keys(NEW_TEMPLATES).join('|') + '|agent [name]')}`);
|
|
49
|
+
process.exit(EXIT_USAGE);
|
|
50
|
+
}
|
|
51
|
+
const name = (positional[1] || (kind === 'rules' ? 'rules' : `my-${kind}`)).replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
52
|
+
const { file, content } = tmpl(name);
|
|
53
|
+
const target = path.resolve(file);
|
|
54
|
+
if (fs.existsSync(target) && !flags.force) {
|
|
55
|
+
console.error(red('✗') + ` ${file} already exists. Use ${bold('--force')} to overwrite.`);
|
|
56
|
+
process.exit(EXIT_USAGE);
|
|
57
|
+
}
|
|
58
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
59
|
+
fs.writeFileSync(target, content);
|
|
60
|
+
const g = localGate(content, { kind: kind === 'agent-card' ? 'agent-card' : kind === 'mcp' ? 'mcp' : kind === 'rules' ? 'rules' : kind, path: file });
|
|
61
|
+
if (flags.json) { console.log(JSON.stringify({ created: file, kind, verdict: g.verdict }, null, 2)); return; }
|
|
62
|
+
console.log(`\n ${green('✓ Created')} ${bold(file)} ${dim(`(${kind})`)}`);
|
|
63
|
+
console.log(` ${g.verdict === 'ALLOW' ? green('✓ gate: clean') : yellow('gate: ' + g.verdict)} ${dim('- secure-by-default template. Edit, then')} ${bold('shomra gate ' + file)}${dim('.')}\n`);
|
|
64
|
+
}
|