brainclaw 1.8.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +592 -505
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +138 -13
- package/dist/commands/add-step.js +1 -1
- package/dist/commands/bootstrap.js +2 -26
- package/dist/commands/check-security-mcp.js +50 -33
- package/dist/commands/check-security.js +86 -43
- package/dist/commands/claim.js +22 -21
- package/dist/commands/confirm.js +26 -0
- package/dist/commands/context-diff.js +1 -1
- package/dist/commands/dispatch-watch.js +142 -0
- package/dist/commands/doctor.js +113 -2
- package/dist/commands/estimation-report.js +115 -16
- package/dist/commands/harvest.js +286 -23
- package/dist/commands/hooks.js +73 -73
- package/dist/commands/init.js +124 -22
- package/dist/commands/install-hooks.js +78 -78
- package/dist/commands/loops-handlers.js +4 -0
- package/dist/commands/mcp-read-handlers.js +253 -41
- package/dist/commands/mcp.js +664 -102
- package/dist/commands/memory.js +21 -17
- package/dist/commands/migrate.js +81 -17
- package/dist/commands/prune.js +78 -4
- package/dist/commands/reflect.js +26 -20
- package/dist/commands/register-agent.js +57 -1
- package/dist/commands/repair.js +20 -0
- package/dist/commands/session-end.js +15 -6
- package/dist/commands/session-start.js +18 -1
- package/dist/commands/setup-security.js +39 -18
- package/dist/commands/setup.js +26 -27
- package/dist/commands/stale.js +16 -2
- package/dist/commands/switch.js +26 -5
- package/dist/commands/uninstall.js +126 -34
- package/dist/commands/update-step.js +6 -0
- package/dist/commands/version.js +1 -1
- package/dist/commands/worktree.js +60 -0
- package/dist/core/actions.js +12 -3
- package/dist/core/agent-capability.js +30 -17
- package/dist/core/agent-files.js +963 -666
- package/dist/core/agent-integrations.js +0 -3
- package/dist/core/agent-inventory.js +67 -0
- package/dist/core/agent-registry.js +163 -29
- package/dist/core/agentrun-reconciler.js +33 -2
- package/dist/core/agentruns.js +7 -1
- package/dist/core/ai-agent-detection.js +31 -44
- package/dist/core/archival.js +15 -9
- package/dist/core/assignment-reconciler.js +56 -0
- package/dist/core/assignment-sweeper.js +127 -4
- package/dist/core/assignments.js +69 -11
- package/dist/core/bootstrap.js +233 -67
- package/dist/core/brainclaw-version.js +22 -0
- package/dist/core/candidates.js +21 -1
- package/dist/core/claims.js +313 -150
- package/dist/core/codev-prompts.js +38 -38
- package/dist/core/config.js +6 -1
- package/dist/core/context-diff.js +148 -20
- package/dist/core/context.js +129 -8
- package/dist/core/coordination.js +22 -3
- package/dist/core/default-profiles/doctor.yaml +11 -11
- package/dist/core/default-profiles/janitor.yaml +11 -11
- package/dist/core/default-profiles/onboarder.yaml +11 -11
- package/dist/core/default-profiles/reviewer.yaml +13 -13
- package/dist/core/dispatch-status.js +79 -5
- package/dist/core/dispatcher.js +65 -12
- package/dist/core/entity-operations.js +74 -27
- package/dist/core/entity-registry.js +31 -5
- package/dist/core/event-log.js +138 -21
- package/dist/core/events/checkpoint.js +258 -0
- package/dist/core/events/genesis.js +220 -0
- package/dist/core/events/journal.js +507 -0
- package/dist/core/events/materialize.js +126 -0
- package/dist/core/events/registry-post-image.js +110 -0
- package/dist/core/events/verify.js +109 -0
- package/dist/core/execution-adapters.js +23 -0
- package/dist/core/execution.js +1 -1
- package/dist/core/facade-schema.js +38 -0
- package/dist/core/gc-semantic.js +130 -5
- package/dist/core/handoff-snapshot.js +68 -0
- package/dist/core/ids.js +19 -8
- package/dist/core/instruction-templates.js +34 -115
- package/dist/core/io.js +39 -3
- package/dist/core/json-store.js +10 -1
- package/dist/core/lock.js +153 -28
- package/dist/core/loops/bootstrap-acquire.js +25 -1
- package/dist/core/loops/facade-schema.js +2 -0
- package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
- package/dist/core/loops/index.js +1 -0
- package/dist/core/loops/presets/bootstrap.js +7 -0
- package/dist/core/loops/store.js +17 -0
- package/dist/core/loops/verbs.js +24 -2
- package/dist/core/markdown.js +8 -76
- package/dist/core/mcp-command-resolution.js +245 -0
- package/dist/core/memory-compactor.js +5 -3
- package/dist/core/memory-lifecycle.js +282 -0
- package/dist/core/merge-risk.js +150 -0
- package/dist/core/messaging.js +10 -3
- package/dist/core/migration.js +11 -1
- package/dist/core/observer-mode.js +26 -0
- package/dist/core/operations/memory-mutation.js +90 -65
- package/dist/core/operations/plan.js +27 -1
- package/dist/core/protocol-skills.js +210 -0
- package/dist/core/reflection-safety.js +6 -7
- package/dist/core/reputation.js +84 -2
- package/dist/core/runtime-signals.js +72 -10
- package/dist/core/runtime.js +84 -1
- package/dist/core/schema.js +114 -0
- package/dist/core/search.js +19 -2
- package/dist/core/security-detectors.js +125 -0
- package/dist/core/security-extract.js +189 -0
- package/dist/core/security-guard.js +217 -139
- package/dist/core/security-packages.js +121 -0
- package/dist/core/security-scoring.js +76 -9
- package/dist/core/security.js +34 -2
- package/dist/core/sequence.js +11 -2
- package/dist/core/setup-flow.js +141 -13
- package/dist/core/spawn-check.js +16 -2
- package/dist/core/staleness.js +73 -2
- package/dist/core/state.js +250 -54
- package/dist/core/store-resolution.js +45 -12
- package/dist/core/worktree.js +90 -26
- package/dist/facts.js +8 -8
- package/dist/facts.json +7 -7
- package/docs/PROTOCOL.md +223 -0
- package/docs/adapters/openclaw.md +43 -43
- package/docs/architecture/project-refs.md +328 -328
- package/docs/cli.md +2097 -2096
- package/docs/concepts/coordination.md +52 -52
- package/docs/concepts/coordinator-runbook.md +129 -0
- package/docs/concepts/dispatch-lifecycle.md +245 -245
- package/docs/concepts/event-log-store.md +928 -0
- package/docs/concepts/ideation-loop.md +317 -317
- package/docs/concepts/loop-engine.md +520 -511
- package/docs/concepts/mcp-governance.md +268 -268
- package/docs/concepts/memory.md +89 -88
- package/docs/concepts/multi-agent-workflows.md +167 -167
- package/docs/concepts/observer-protocol.md +361 -0
- package/docs/concepts/parallel-merge-protocol.md +71 -0
- package/docs/concepts/plans-and-claims.md +217 -174
- package/docs/concepts/project-md-convention.md +35 -35
- package/docs/concepts/runtime-notes.md +38 -38
- package/docs/concepts/skills.md +78 -0
- package/docs/concepts/troubleshooting.md +254 -254
- package/docs/concepts/workspace-bootstrapping.md +142 -81
- package/docs/context-format-changelog.md +35 -35
- package/docs/context-format.md +48 -48
- package/docs/index.md +65 -65
- package/docs/integrations/agents.md +162 -162
- package/docs/integrations/claude-code.md +23 -23
- package/docs/integrations/cline.md +87 -88
- package/docs/integrations/codex.md +2 -2
- package/docs/integrations/continue.md +60 -60
- package/docs/integrations/copilot.md +82 -80
- package/docs/integrations/cursor.md +23 -23
- package/docs/integrations/kilocode.md +72 -72
- package/docs/integrations/mcp.md +377 -377
- package/docs/integrations/mistral-vibe.md +122 -122
- package/docs/integrations/openclaw.md +99 -98
- package/docs/integrations/opencode.md +84 -84
- package/docs/integrations/overview.md +122 -122
- package/docs/integrations/roo.md +74 -74
- package/docs/integrations/windsurf.md +83 -83
- package/docs/mcp-schema-changelog.md +360 -329
- package/docs/playbooks/integration/index.md +121 -121
- package/docs/playbooks/orchestration.md +37 -0
- package/docs/playbooks/productivity/index.md +99 -99
- package/docs/playbooks/team/index.md +117 -117
- package/docs/product/agent-first-model.md +184 -184
- package/docs/product/entity-model-audit.md +462 -462
- package/docs/product/positioning.md +86 -86
- package/docs/quickstart-existing-project.md +107 -107
- package/docs/quickstart.md +148 -147
- package/docs/release-maintenance.md +79 -79
- package/docs/reputation.md +52 -52
- package/docs/review.md +45 -45
- package/docs/security.md +212 -53
- package/docs/server-operations.md +118 -118
- package/docs/storage.md +110 -108
- package/package.json +86 -69
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
export function collectPackages(opts) {
|
|
3
|
+
const seen = new Set();
|
|
4
|
+
const out = [];
|
|
5
|
+
const push = (spec) => {
|
|
6
|
+
const trimmed = spec.trim();
|
|
7
|
+
if (!trimmed)
|
|
8
|
+
return;
|
|
9
|
+
// Skip local paths and URLs — they aren't registry packages.
|
|
10
|
+
if (isLocalOrUrl(trimmed))
|
|
11
|
+
return;
|
|
12
|
+
if (seen.has(trimmed))
|
|
13
|
+
return;
|
|
14
|
+
seen.add(trimmed);
|
|
15
|
+
out.push(trimmed);
|
|
16
|
+
};
|
|
17
|
+
if (opts.packages) {
|
|
18
|
+
for (const item of opts.packages.split(','))
|
|
19
|
+
push(item);
|
|
20
|
+
}
|
|
21
|
+
if (opts.requirements) {
|
|
22
|
+
for (const item of parseRequirementsFile(opts.requirements))
|
|
23
|
+
push(item);
|
|
24
|
+
}
|
|
25
|
+
if (opts.lockfile) {
|
|
26
|
+
for (const item of parseLockfile(opts.lockfile))
|
|
27
|
+
push(item);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Return true for arguments that look like a filesystem path or URL
|
|
33
|
+
* rather than a registry package name. Mirrors the heuristic the
|
|
34
|
+
* wrapper scripts use when intercepting install commands.
|
|
35
|
+
*/
|
|
36
|
+
export function isLocalOrUrl(spec) {
|
|
37
|
+
if (!spec)
|
|
38
|
+
return false;
|
|
39
|
+
if (spec === '.' || spec === '..')
|
|
40
|
+
return true;
|
|
41
|
+
if (spec.startsWith('./') || spec.startsWith('../'))
|
|
42
|
+
return true;
|
|
43
|
+
if (spec.startsWith('/'))
|
|
44
|
+
return true; // absolute POSIX path
|
|
45
|
+
if (/^[A-Za-z]:[\\/]/.test(spec))
|
|
46
|
+
return true; // Windows drive path
|
|
47
|
+
if (/^[a-z]+:\/\//.test(spec))
|
|
48
|
+
return true; // http(s)/git/file URLs
|
|
49
|
+
if (spec.startsWith('git+') || spec.startsWith('git@'))
|
|
50
|
+
return true;
|
|
51
|
+
if (spec.endsWith('.tar.gz') || spec.endsWith('.tgz') || spec.endsWith('.whl'))
|
|
52
|
+
return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse a pip requirements.txt file. Supports the subset of syntax that
|
|
57
|
+
* actually shows up in install gates:
|
|
58
|
+
* - one spec per line; "name", "name==version", "name~=version"
|
|
59
|
+
* - comments (#) and blank lines
|
|
60
|
+
* - continuation lines with trailing backslash
|
|
61
|
+
* - line options (-r/--requirement, -e/--editable, -i/--index-url, etc.)
|
|
62
|
+
* are skipped, with -r/--requirement recursively included.
|
|
63
|
+
*/
|
|
64
|
+
export function parseRequirementsFile(filePath) {
|
|
65
|
+
const raw = readFileOrThrow(filePath, 'requirements file');
|
|
66
|
+
const out = [];
|
|
67
|
+
const lines = unfoldContinuations(raw).split(/\r?\n/);
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
const stripped = line.replace(/\s*#.*$/, '').trim();
|
|
70
|
+
if (!stripped)
|
|
71
|
+
continue;
|
|
72
|
+
if (stripped.startsWith('-r ') || stripped.startsWith('--requirement ')) {
|
|
73
|
+
// Recursive include — keep it bounded but useful.
|
|
74
|
+
const nested = stripped.replace(/^(-r|--requirement)\s+/, '').trim();
|
|
75
|
+
if (nested) {
|
|
76
|
+
try {
|
|
77
|
+
out.push(...parseRequirementsFile(resolveSibling(filePath, nested)));
|
|
78
|
+
}
|
|
79
|
+
catch { /* missing nested file is non-fatal */ }
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (stripped.startsWith('-'))
|
|
84
|
+
continue; // -e, -i, --index-url, etc.
|
|
85
|
+
// Drop env-marker portion: "pkg==1.0 ; python_version>'3.7'"
|
|
86
|
+
const noMarker = stripped.split(';')[0].trim();
|
|
87
|
+
if (!noMarker)
|
|
88
|
+
continue;
|
|
89
|
+
// Strip extras: "pkg[extra1,extra2]==1.0"
|
|
90
|
+
const noExtras = noMarker.replace(/\[[^\]]*\]/g, '');
|
|
91
|
+
// Keep only "name" or "name==version" forms; reject URL/path specs and
|
|
92
|
+
// ranges we cannot translate to an exact spec ("~=", ">=", "<=", "<", ">", "!=", "===").
|
|
93
|
+
const exact = noExtras.match(/^([A-Za-z0-9._-]+)\s*==\s*([^\s,;]+)\s*$/);
|
|
94
|
+
if (exact) {
|
|
95
|
+
out.push(`${exact[1]}==${exact[2]}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const bare = noExtras.match(/^([A-Za-z0-9._-]+)\s*$/);
|
|
99
|
+
if (bare) {
|
|
100
|
+
out.push(bare[1]);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// For range specs, keep the name (range resolution is outside our scope).
|
|
104
|
+
const nameOnly = noExtras.match(/^([A-Za-z0-9._-]+)/);
|
|
105
|
+
if (nameOnly) {
|
|
106
|
+
out.push(nameOnly[1]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Parse a lockfile to extract top-level direct-dependency package names.
|
|
113
|
+
* Supports:
|
|
114
|
+
* - npm package-lock.json (v1, v2, v3): uses `packages[""].dependencies`
|
|
115
|
+
* and `packages[""].devDependencies` when present (v2+), else falls
|
|
116
|
+
* back to top-level `dependencies` (v1).
|
|
117
|
+
* - npm shrinkwrap.json: same shape as package-lock.
|
|
118
|
+
*
|
|
119
|
+
* Transitive deps are deliberately excluded; the gate is for what the
|
|
120
|
+
* operator is asking to install, not the full resolved graph.
|
|
121
|
+
*/
|
|
122
|
+
export function parseLockfile(filePath) {
|
|
123
|
+
const raw = readFileOrThrow(filePath, 'lockfile');
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(raw);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
throw new Error(`Failed to parse lockfile ${filePath}: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
if (!parsed || typeof parsed !== 'object')
|
|
132
|
+
return [];
|
|
133
|
+
const obj = parsed;
|
|
134
|
+
const out = [];
|
|
135
|
+
// npm package-lock v2+
|
|
136
|
+
const packages = obj['packages'];
|
|
137
|
+
if (packages && typeof packages === 'object') {
|
|
138
|
+
const root = packages[''];
|
|
139
|
+
if (root && typeof root === 'object') {
|
|
140
|
+
const deps = root['dependencies'];
|
|
141
|
+
const devDeps = root['devDependencies'];
|
|
142
|
+
collectLockDeps(deps, out);
|
|
143
|
+
collectLockDeps(devDeps, out);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// npm package-lock v1 fallback
|
|
147
|
+
if (out.length === 0) {
|
|
148
|
+
const deps = obj['dependencies'];
|
|
149
|
+
if (deps && typeof deps === 'object') {
|
|
150
|
+
for (const [name, meta] of Object.entries(deps)) {
|
|
151
|
+
const v = meta && typeof meta === 'object' ? meta['version'] : undefined;
|
|
152
|
+
out.push(typeof v === 'string' ? `${name}@${v}` : name);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
function collectLockDeps(deps, out) {
|
|
159
|
+
if (!deps || typeof deps !== 'object')
|
|
160
|
+
return;
|
|
161
|
+
for (const [name, range] of Object.entries(deps)) {
|
|
162
|
+
if (typeof range === 'string' && /^\d/.test(range)) {
|
|
163
|
+
out.push(`${name}@${range}`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
out.push(name);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function unfoldContinuations(raw) {
|
|
171
|
+
return raw.replace(/\\\r?\n/g, ' ');
|
|
172
|
+
}
|
|
173
|
+
function readFileOrThrow(p, label) {
|
|
174
|
+
try {
|
|
175
|
+
return fs.readFileSync(p, 'utf-8');
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
throw new Error(`Could not read ${label} at ${p}: ${err.message}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function resolveSibling(parent, nested) {
|
|
182
|
+
// Tolerate both absolute and relative includes without pulling in `node:path`.
|
|
183
|
+
if (nested.startsWith('/') || /^[A-Za-z]:[\\/]/.test(nested))
|
|
184
|
+
return nested;
|
|
185
|
+
const sep = parent.includes('\\') ? '\\' : '/';
|
|
186
|
+
const dir = parent.replace(/[\\/][^\\/]*$/, '');
|
|
187
|
+
return dir + sep + nested;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=security-extract.js.map
|
|
@@ -1,149 +1,227 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generates brainclaw-guard wrapper scripts (bash + PowerShell) that intercept
|
|
3
|
-
* npm/pip install commands and check packages via
|
|
3
|
+
* npm/pip/pnpm/yarn install commands and check packages via
|
|
4
|
+
* brainclaw check-security.
|
|
5
|
+
*
|
|
6
|
+
* Mode is enforced by the CLI's exit code, not the wrapper:
|
|
7
|
+
* exit 0 — pass (silent)
|
|
8
|
+
* exit 1 — warn (printed; install continues)
|
|
9
|
+
* exit 2 — block (printed; install aborted)
|
|
10
|
+
* In advisory mode the CLI never emits exit 2, so the wrapper never aborts.
|
|
11
|
+
* The wrapper therefore stays mode-agnostic — flipping advisory↔enforced is
|
|
12
|
+
* a config change, no regeneration required.
|
|
4
13
|
*/
|
|
5
14
|
export function generateBashGuard(brainclawBin) {
|
|
6
|
-
return `#!/usr/bin/env bash
|
|
7
|
-
# brainclaw-guard — preinstall security gate
|
|
8
|
-
# Generated by: brainclaw setup
|
|
9
|
-
# Do not edit manually — regenerate with brainclaw setup
|
|
10
|
-
|
|
11
|
-
BRAINCLAW_BIN="${brainclawBin}"
|
|
12
|
-
ORIGINAL_CMD="\${BRAINCLAW_GUARD_ORIGINAL_CMD:-npm}"
|
|
13
|
-
|
|
14
|
-
is_install_command() {
|
|
15
|
-
for arg in "$@"; do
|
|
16
|
-
case "$arg" in
|
|
17
|
-
install|add|i)
|
|
18
|
-
-*)
|
|
19
|
-
*)
|
|
20
|
-
esac
|
|
21
|
-
done
|
|
22
|
-
return 1
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
;;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
15
|
+
return `#!/usr/bin/env bash
|
|
16
|
+
# brainclaw-guard — preinstall security gate
|
|
17
|
+
# Generated by: brainclaw setup-security
|
|
18
|
+
# Do not edit manually — regenerate with brainclaw setup-security
|
|
19
|
+
|
|
20
|
+
BRAINCLAW_BIN="${brainclawBin}"
|
|
21
|
+
ORIGINAL_CMD="\${BRAINCLAW_GUARD_ORIGINAL_CMD:-npm}"
|
|
22
|
+
|
|
23
|
+
is_install_command() {
|
|
24
|
+
for arg in "$@"; do
|
|
25
|
+
case "$arg" in
|
|
26
|
+
install|add|i) return 0 ;;
|
|
27
|
+
-*) continue ;;
|
|
28
|
+
*) break ;;
|
|
29
|
+
esac
|
|
30
|
+
done
|
|
31
|
+
return 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
is_local_or_url() {
|
|
35
|
+
case "$1" in
|
|
36
|
+
.|..|./*|../*|/*|*:/*|git+*|git@*|*.tgz|*.tar.gz|*.whl) return 0 ;;
|
|
37
|
+
esac
|
|
38
|
+
case "$1" in
|
|
39
|
+
[A-Za-z]:[\\\\/]*) return 0 ;;
|
|
40
|
+
esac
|
|
41
|
+
return 1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
extract_packages() {
|
|
45
|
+
local packages=""
|
|
46
|
+
local req_file=""
|
|
47
|
+
local skip_next=false
|
|
48
|
+
local past_command=false
|
|
49
|
+
for arg in "$@"; do
|
|
50
|
+
if [ "$skip_next" = true ]; then
|
|
51
|
+
# The flag's value lands here; capture it for -r/--requirement.
|
|
52
|
+
case "$prev_flag" in
|
|
53
|
+
-r|--requirement) req_file="$arg" ;;
|
|
54
|
+
esac
|
|
55
|
+
skip_next=false
|
|
56
|
+
prev_flag=""
|
|
57
|
+
continue
|
|
58
|
+
fi
|
|
59
|
+
case "$arg" in
|
|
60
|
+
install|add|i)
|
|
61
|
+
past_command=true
|
|
62
|
+
continue
|
|
63
|
+
;;
|
|
64
|
+
--save-dev|--save-peer|--save-optional|--save-exact|--save-prod|-D|-P|-O|-E|-S|-g|--global|--no-save|--user|--upgrade|-U|--break-system-packages|--no-deps|--pre|--force-reinstall|--ignore-installed)
|
|
65
|
+
continue
|
|
66
|
+
;;
|
|
67
|
+
--registry|--prefix|--tag|--registry-url|--cache|--index-url|--extra-index-url|--find-links|--proxy|--cert|--client-cert|--trusted-host|--target|--root|--python|--platform|--abi|--implementation)
|
|
68
|
+
prev_flag="$arg"
|
|
69
|
+
skip_next=true
|
|
70
|
+
continue
|
|
71
|
+
;;
|
|
72
|
+
-r|--requirement|-e|--editable|-c|--constraint)
|
|
73
|
+
prev_flag="$arg"
|
|
74
|
+
skip_next=true
|
|
75
|
+
continue
|
|
76
|
+
;;
|
|
77
|
+
-*)
|
|
78
|
+
continue
|
|
79
|
+
;;
|
|
80
|
+
*)
|
|
81
|
+
if [ "$past_command" = true ]; then
|
|
82
|
+
if is_local_or_url "$arg"; then
|
|
83
|
+
continue
|
|
84
|
+
fi
|
|
85
|
+
if [ -n "$packages" ]; then
|
|
86
|
+
packages="$packages,$arg"
|
|
87
|
+
else
|
|
88
|
+
packages="$arg"
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
;;
|
|
92
|
+
esac
|
|
93
|
+
done
|
|
94
|
+
printf '%s\\t%s' "$packages" "$req_file"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Main logic
|
|
98
|
+
if is_install_command "$@"; then
|
|
99
|
+
extracted=$(extract_packages "$@")
|
|
100
|
+
packages="\${extracted%% *}"
|
|
101
|
+
req_file="\${extracted##* }"
|
|
102
|
+
|
|
103
|
+
if [ -n "$packages" ] || [ -n "$req_file" ]; then
|
|
104
|
+
ecosystem="npm"
|
|
105
|
+
if [ "$ORIGINAL_CMD" = "pip" ] || [ "$ORIGINAL_CMD" = "pip3" ]; then
|
|
106
|
+
ecosystem="pypi"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
args=(check-security --ecosystem "$ecosystem" --json)
|
|
110
|
+
if [ -n "$packages" ]; then args+=(--packages "$packages"); fi
|
|
111
|
+
if [ -n "$req_file" ]; then args+=(--requirements "$req_file"); fi
|
|
112
|
+
|
|
113
|
+
"$BRAINCLAW_BIN" "\${args[@]}" >/dev/null 2>&1
|
|
114
|
+
exit_code=$?
|
|
115
|
+
|
|
116
|
+
if [ $exit_code -eq 2 ]; then
|
|
117
|
+
echo "[brainclaw-guard] BLOCKED — supply chain risk detected (enforced mode)" >&2
|
|
118
|
+
details_pkgs="\${packages:-$req_file}"
|
|
119
|
+
echo "[brainclaw-guard] Details: $BRAINCLAW_BIN check-security --ecosystem $ecosystem --packages \\"$details_pkgs\\"" >&2
|
|
120
|
+
exit 1
|
|
121
|
+
elif [ $exit_code -eq 1 ]; then
|
|
122
|
+
echo "[brainclaw-guard] WARNING — potential supply chain risk (advisory or warn-threshold verdict)" >&2
|
|
123
|
+
details_pkgs="\${packages:-$req_file}"
|
|
124
|
+
echo "[brainclaw-guard] Details: $BRAINCLAW_BIN check-security --ecosystem $ecosystem --packages \\"$details_pkgs\\"" >&2
|
|
125
|
+
fi
|
|
126
|
+
# exit_code 0 = pass, continue silently
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
exec "$ORIGINAL_CMD" "$@"
|
|
88
131
|
`;
|
|
89
132
|
}
|
|
90
133
|
export function generatePowerShellGuard(brainclawBin) {
|
|
91
|
-
return `# brainclaw-guard — preinstall security gate (PowerShell)
|
|
92
|
-
# Generated by: brainclaw setup
|
|
93
|
-
# Do not edit manually — regenerate with brainclaw setup
|
|
94
|
-
|
|
95
|
-
param([Parameter(ValueFromRemainingArguments)]$Args)
|
|
96
|
-
|
|
97
|
-
$BrainclawBin = "${brainclawBin}"
|
|
98
|
-
$OriginalCmd = if ($env:BRAINCLAW_GUARD_ORIGINAL_CMD) { $env:BRAINCLAW_GUARD_ORIGINAL_CMD } else { "npm" }
|
|
99
|
-
|
|
100
|
-
function Is-InstallCommand($arguments) {
|
|
101
|
-
foreach ($arg in $arguments) {
|
|
102
|
-
if ($arg -match "^(install|add|i)$") { return $true }
|
|
103
|
-
if ($arg -notmatch "^-") { break }
|
|
104
|
-
}
|
|
105
|
-
return $false
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function
|
|
109
|
-
$
|
|
110
|
-
$
|
|
111
|
-
$
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
$
|
|
124
|
-
|
|
125
|
-
$
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
134
|
+
return `# brainclaw-guard — preinstall security gate (PowerShell)
|
|
135
|
+
# Generated by: brainclaw setup-security
|
|
136
|
+
# Do not edit manually — regenerate with brainclaw setup-security
|
|
137
|
+
|
|
138
|
+
param([Parameter(ValueFromRemainingArguments)]$Args)
|
|
139
|
+
|
|
140
|
+
$BrainclawBin = "${brainclawBin}"
|
|
141
|
+
$OriginalCmd = if ($env:BRAINCLAW_GUARD_ORIGINAL_CMD) { $env:BRAINCLAW_GUARD_ORIGINAL_CMD } else { "npm" }
|
|
142
|
+
|
|
143
|
+
function Is-InstallCommand($arguments) {
|
|
144
|
+
foreach ($arg in $arguments) {
|
|
145
|
+
if ($arg -match "^(install|add|i)$") { return $true }
|
|
146
|
+
if ($arg -notmatch "^-") { break }
|
|
147
|
+
}
|
|
148
|
+
return $false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function Is-LocalOrUrl($arg) {
|
|
152
|
+
if ($arg -eq "." -or $arg -eq "..") { return $true }
|
|
153
|
+
if ($arg -match "^(\\./|\\.\\./|/)") { return $true }
|
|
154
|
+
if ($arg -match "^[A-Za-z]:[\\\\/]") { return $true }
|
|
155
|
+
if ($arg -match "^[a-z]+://") { return $true }
|
|
156
|
+
if ($arg -match "^(git\\+|git@)") { return $true }
|
|
157
|
+
if ($arg -match "\\.(tgz|tar\\.gz|whl)$") { return $true }
|
|
158
|
+
return $false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function Extract-Packages($arguments) {
|
|
162
|
+
$packages = @()
|
|
163
|
+
$reqFile = ""
|
|
164
|
+
$pastCommand = $false
|
|
165
|
+
$skipNext = $false
|
|
166
|
+
$prevFlag = ""
|
|
167
|
+
foreach ($arg in $arguments) {
|
|
168
|
+
if ($skipNext) {
|
|
169
|
+
if ($prevFlag -match "^(-r|--requirement)$") { $reqFile = $arg }
|
|
170
|
+
$skipNext = $false
|
|
171
|
+
$prevFlag = ""
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
if ($arg -match "^(install|add|i)$") { $pastCommand = $true; continue }
|
|
175
|
+
if ($arg -match "^(--save-dev|--save-peer|--save-optional|--save-exact|--save-prod|-D|-P|-O|-E|-S|-g|--global|--no-save|--user|--upgrade|-U|--break-system-packages|--no-deps|--pre|--force-reinstall|--ignore-installed)$") { continue }
|
|
176
|
+
if ($arg -match "^(--registry|--prefix|--tag|--registry-url|--cache|--index-url|--extra-index-url|--find-links|--proxy|--cert|--client-cert|--trusted-host|--target|--root|--python|--platform|--abi|--implementation)$") {
|
|
177
|
+
$prevFlag = $arg; $skipNext = $true; continue
|
|
178
|
+
}
|
|
179
|
+
if ($arg -match "^(-r|--requirement|-e|--editable|-c|--constraint)$") {
|
|
180
|
+
$prevFlag = $arg; $skipNext = $true; continue
|
|
181
|
+
}
|
|
182
|
+
if ($arg -match "^-") { continue }
|
|
183
|
+
if ($pastCommand) {
|
|
184
|
+
if (Is-LocalOrUrl $arg) { continue }
|
|
185
|
+
$packages += $arg
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return [PSCustomObject]@{ Packages = ($packages -join ","); RequirementsFile = $reqFile }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (Is-InstallCommand $Args) {
|
|
192
|
+
$extracted = Extract-Packages $Args
|
|
193
|
+
$packages = $extracted.Packages
|
|
194
|
+
$reqFile = $extracted.RequirementsFile
|
|
195
|
+
|
|
196
|
+
if ($packages -or $reqFile) {
|
|
197
|
+
$ecosystem = "npm"
|
|
198
|
+
if ($OriginalCmd -match "^pip") { $ecosystem = "pypi" }
|
|
199
|
+
|
|
200
|
+
$cliArgs = @("check-security", "--ecosystem", $ecosystem, "--json")
|
|
201
|
+
if ($packages) { $cliArgs += @("--packages", $packages) }
|
|
202
|
+
if ($reqFile) { $cliArgs += @("--requirements", $reqFile) }
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
& $BrainclawBin @cliArgs *> $null
|
|
206
|
+
$exitCode = $LASTEXITCODE
|
|
207
|
+
} catch {
|
|
208
|
+
$exitCode = 0
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
$detailsPkgs = if ($packages) { $packages } else { $reqFile }
|
|
212
|
+
if ($exitCode -eq 2) {
|
|
213
|
+
Write-Error "[brainclaw-guard] BLOCKED - supply chain risk detected (enforced mode)"
|
|
214
|
+
Write-Error "[brainclaw-guard] Details: $BrainclawBin check-security --ecosystem $ecosystem --packages \`"$detailsPkgs\`""
|
|
215
|
+
exit 1
|
|
216
|
+
} elseif ($exitCode -eq 1) {
|
|
217
|
+
Write-Warning "[brainclaw-guard] WARNING - potential supply chain risk (advisory or warn-threshold verdict)"
|
|
218
|
+
Write-Warning "[brainclaw-guard] Details: $BrainclawBin check-security --ecosystem $ecosystem --packages \`"$detailsPkgs\`""
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
& $OriginalCmd @Args
|
|
224
|
+
exit $LASTEXITCODE
|
|
147
225
|
`;
|
|
148
226
|
}
|
|
149
227
|
export function generatePipBashGuard(brainclawBin) {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical package matching for security allow/deny lists.
|
|
3
|
+
*
|
|
4
|
+
* Supports three entry forms:
|
|
5
|
+
* "name" — bare name; matches any ecosystem
|
|
6
|
+
* "ecosystem:name" — ecosystem-scoped, any version
|
|
7
|
+
* "ecosystem:name@version" — exact (ecosystem, name, version) pin
|
|
8
|
+
*
|
|
9
|
+
* Matching is exact on each component (no substring). Bare names match by
|
|
10
|
+
* package name only, which preserves backward compatibility with the MVP
|
|
11
|
+
* config while making the comparison precise instead of "purl.includes(d)".
|
|
12
|
+
*/
|
|
13
|
+
const ECOSYSTEMS = new Set(['npm', 'pypi']);
|
|
14
|
+
/**
|
|
15
|
+
* Parse a package spec like "pkg", "pkg@1.0.0", "@scope/pkg",
|
|
16
|
+
* "@scope/pkg@1.0.0", or pip-style "pkg==1.0.0".
|
|
17
|
+
*/
|
|
18
|
+
export function parsePackageSpec(spec) {
|
|
19
|
+
const trimmed = spec.trim();
|
|
20
|
+
// pip-style "name==version"
|
|
21
|
+
const pipMatch = trimmed.match(/^([A-Za-z0-9._-]+)\s*==\s*([^\s,;]+)$/);
|
|
22
|
+
if (pipMatch) {
|
|
23
|
+
return { depname: pipMatch[1], version: pipMatch[2] };
|
|
24
|
+
}
|
|
25
|
+
// npm scoped: @scope/pkg or @scope/pkg@version
|
|
26
|
+
if (trimmed.startsWith('@')) {
|
|
27
|
+
const lastAt = trimmed.lastIndexOf('@');
|
|
28
|
+
if (lastAt > 0) {
|
|
29
|
+
return { depname: trimmed.slice(0, lastAt), version: trimmed.slice(lastAt + 1) || 'latest' };
|
|
30
|
+
}
|
|
31
|
+
return { depname: trimmed, version: 'latest' };
|
|
32
|
+
}
|
|
33
|
+
// pkg@version
|
|
34
|
+
if (trimmed.includes('@')) {
|
|
35
|
+
const idx = trimmed.indexOf('@');
|
|
36
|
+
return { depname: trimmed.slice(0, idx), version: trimmed.slice(idx + 1) || 'latest' };
|
|
37
|
+
}
|
|
38
|
+
return { depname: trimmed, version: 'latest' };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse an allow/deny list entry. Accepts:
|
|
42
|
+
* "name"
|
|
43
|
+
* "ecosystem:name"
|
|
44
|
+
* "ecosystem:name@version"
|
|
45
|
+
* Whitespace tolerated. Unknown ecosystem prefixes are treated as bare
|
|
46
|
+
* names (so "lodash@1.0.0" still works as bare-name+version).
|
|
47
|
+
*/
|
|
48
|
+
export function parseListEntry(entry) {
|
|
49
|
+
const raw = entry;
|
|
50
|
+
const trimmed = entry.trim();
|
|
51
|
+
if (!trimmed) {
|
|
52
|
+
return { ecosystem: null, name: '', version: null, raw };
|
|
53
|
+
}
|
|
54
|
+
let ecosystem = null;
|
|
55
|
+
let remainder = trimmed;
|
|
56
|
+
// Only treat "<prefix>:" as ecosystem when <prefix> is a known ecosystem.
|
|
57
|
+
// This prevents misparsing of names that legitimately contain ":".
|
|
58
|
+
const colonIdx = trimmed.indexOf(':');
|
|
59
|
+
if (colonIdx > 0) {
|
|
60
|
+
const prefix = trimmed.slice(0, colonIdx).toLowerCase();
|
|
61
|
+
if (ECOSYSTEMS.has(prefix)) {
|
|
62
|
+
ecosystem = prefix;
|
|
63
|
+
remainder = trimmed.slice(colonIdx + 1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Now extract optional @version from remainder. Handle scoped names
|
|
67
|
+
// ("@scope/pkg") and pip "name==version" too.
|
|
68
|
+
const pipMatch = remainder.match(/^([A-Za-z0-9._-]+)\s*==\s*([^\s,;]+)$/);
|
|
69
|
+
if (pipMatch) {
|
|
70
|
+
return { ecosystem, name: pipMatch[1], version: pipMatch[2], raw };
|
|
71
|
+
}
|
|
72
|
+
if (remainder.startsWith('@')) {
|
|
73
|
+
const lastAt = remainder.lastIndexOf('@');
|
|
74
|
+
if (lastAt > 0) {
|
|
75
|
+
return {
|
|
76
|
+
ecosystem,
|
|
77
|
+
name: remainder.slice(0, lastAt),
|
|
78
|
+
version: remainder.slice(lastAt + 1) || null,
|
|
79
|
+
raw,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { ecosystem, name: remainder, version: null, raw };
|
|
83
|
+
}
|
|
84
|
+
if (remainder.includes('@')) {
|
|
85
|
+
const idx = remainder.indexOf('@');
|
|
86
|
+
return {
|
|
87
|
+
ecosystem,
|
|
88
|
+
name: remainder.slice(0, idx),
|
|
89
|
+
version: remainder.slice(idx + 1) || null,
|
|
90
|
+
raw,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { ecosystem, name: remainder, version: null, raw };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Returns true if (ecosystem, name, version) matches the parsed entry.
|
|
97
|
+
* Matching rules:
|
|
98
|
+
* - entry.ecosystem null → any ecosystem
|
|
99
|
+
* - entry.name must equal name exactly
|
|
100
|
+
* - entry.version null → any version; otherwise exact equality with
|
|
101
|
+
* "*" as a wildcard alias for "any version"
|
|
102
|
+
*/
|
|
103
|
+
export function matchesEntry(entry, ecosystem, name, version) {
|
|
104
|
+
if (entry.name === '')
|
|
105
|
+
return false;
|
|
106
|
+
if (entry.name !== name)
|
|
107
|
+
return false;
|
|
108
|
+
if (entry.ecosystem !== null && entry.ecosystem !== ecosystem)
|
|
109
|
+
return false;
|
|
110
|
+
if (entry.version !== null && entry.version !== '*' && entry.version !== version)
|
|
111
|
+
return false;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
export function matchesAnyEntry(entries, ecosystem, name, version) {
|
|
115
|
+
for (const e of entries) {
|
|
116
|
+
if (matchesEntry(e, ecosystem, name, version))
|
|
117
|
+
return e;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=security-packages.js.map
|