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.
Files changed (178) hide show
  1. package/README.md +592 -505
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +138 -13
  4. package/dist/commands/add-step.js +1 -1
  5. package/dist/commands/bootstrap.js +2 -26
  6. package/dist/commands/check-security-mcp.js +50 -33
  7. package/dist/commands/check-security.js +86 -43
  8. package/dist/commands/claim.js +22 -21
  9. package/dist/commands/confirm.js +26 -0
  10. package/dist/commands/context-diff.js +1 -1
  11. package/dist/commands/dispatch-watch.js +142 -0
  12. package/dist/commands/doctor.js +113 -2
  13. package/dist/commands/estimation-report.js +115 -16
  14. package/dist/commands/harvest.js +286 -23
  15. package/dist/commands/hooks.js +73 -73
  16. package/dist/commands/init.js +124 -22
  17. package/dist/commands/install-hooks.js +78 -78
  18. package/dist/commands/loops-handlers.js +4 -0
  19. package/dist/commands/mcp-read-handlers.js +253 -41
  20. package/dist/commands/mcp.js +664 -102
  21. package/dist/commands/memory.js +21 -17
  22. package/dist/commands/migrate.js +81 -17
  23. package/dist/commands/prune.js +78 -4
  24. package/dist/commands/reflect.js +26 -20
  25. package/dist/commands/register-agent.js +57 -1
  26. package/dist/commands/repair.js +20 -0
  27. package/dist/commands/session-end.js +15 -6
  28. package/dist/commands/session-start.js +18 -1
  29. package/dist/commands/setup-security.js +39 -18
  30. package/dist/commands/setup.js +26 -27
  31. package/dist/commands/stale.js +16 -2
  32. package/dist/commands/switch.js +26 -5
  33. package/dist/commands/uninstall.js +126 -34
  34. package/dist/commands/update-step.js +6 -0
  35. package/dist/commands/version.js +1 -1
  36. package/dist/commands/worktree.js +60 -0
  37. package/dist/core/actions.js +12 -3
  38. package/dist/core/agent-capability.js +30 -17
  39. package/dist/core/agent-files.js +963 -666
  40. package/dist/core/agent-integrations.js +0 -3
  41. package/dist/core/agent-inventory.js +67 -0
  42. package/dist/core/agent-registry.js +163 -29
  43. package/dist/core/agentrun-reconciler.js +33 -2
  44. package/dist/core/agentruns.js +7 -1
  45. package/dist/core/ai-agent-detection.js +31 -44
  46. package/dist/core/archival.js +15 -9
  47. package/dist/core/assignment-reconciler.js +56 -0
  48. package/dist/core/assignment-sweeper.js +127 -4
  49. package/dist/core/assignments.js +69 -11
  50. package/dist/core/bootstrap.js +233 -67
  51. package/dist/core/brainclaw-version.js +22 -0
  52. package/dist/core/candidates.js +21 -1
  53. package/dist/core/claims.js +313 -150
  54. package/dist/core/codev-prompts.js +38 -38
  55. package/dist/core/config.js +6 -1
  56. package/dist/core/context-diff.js +148 -20
  57. package/dist/core/context.js +129 -8
  58. package/dist/core/coordination.js +22 -3
  59. package/dist/core/default-profiles/doctor.yaml +11 -11
  60. package/dist/core/default-profiles/janitor.yaml +11 -11
  61. package/dist/core/default-profiles/onboarder.yaml +11 -11
  62. package/dist/core/default-profiles/reviewer.yaml +13 -13
  63. package/dist/core/dispatch-status.js +79 -5
  64. package/dist/core/dispatcher.js +65 -12
  65. package/dist/core/entity-operations.js +74 -27
  66. package/dist/core/entity-registry.js +31 -5
  67. package/dist/core/event-log.js +138 -21
  68. package/dist/core/events/checkpoint.js +258 -0
  69. package/dist/core/events/genesis.js +220 -0
  70. package/dist/core/events/journal.js +507 -0
  71. package/dist/core/events/materialize.js +126 -0
  72. package/dist/core/events/registry-post-image.js +110 -0
  73. package/dist/core/events/verify.js +109 -0
  74. package/dist/core/execution-adapters.js +23 -0
  75. package/dist/core/execution.js +1 -1
  76. package/dist/core/facade-schema.js +38 -0
  77. package/dist/core/gc-semantic.js +130 -5
  78. package/dist/core/handoff-snapshot.js +68 -0
  79. package/dist/core/ids.js +19 -8
  80. package/dist/core/instruction-templates.js +34 -115
  81. package/dist/core/io.js +39 -3
  82. package/dist/core/json-store.js +10 -1
  83. package/dist/core/lock.js +153 -28
  84. package/dist/core/loops/bootstrap-acquire.js +25 -1
  85. package/dist/core/loops/facade-schema.js +2 -0
  86. package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
  87. package/dist/core/loops/index.js +1 -0
  88. package/dist/core/loops/presets/bootstrap.js +7 -0
  89. package/dist/core/loops/store.js +17 -0
  90. package/dist/core/loops/verbs.js +24 -2
  91. package/dist/core/markdown.js +8 -76
  92. package/dist/core/mcp-command-resolution.js +245 -0
  93. package/dist/core/memory-compactor.js +5 -3
  94. package/dist/core/memory-lifecycle.js +282 -0
  95. package/dist/core/merge-risk.js +150 -0
  96. package/dist/core/messaging.js +10 -3
  97. package/dist/core/migration.js +11 -1
  98. package/dist/core/observer-mode.js +26 -0
  99. package/dist/core/operations/memory-mutation.js +90 -65
  100. package/dist/core/operations/plan.js +27 -1
  101. package/dist/core/protocol-skills.js +210 -0
  102. package/dist/core/reflection-safety.js +6 -7
  103. package/dist/core/reputation.js +84 -2
  104. package/dist/core/runtime-signals.js +72 -10
  105. package/dist/core/runtime.js +84 -1
  106. package/dist/core/schema.js +114 -0
  107. package/dist/core/search.js +19 -2
  108. package/dist/core/security-detectors.js +125 -0
  109. package/dist/core/security-extract.js +189 -0
  110. package/dist/core/security-guard.js +217 -139
  111. package/dist/core/security-packages.js +121 -0
  112. package/dist/core/security-scoring.js +76 -9
  113. package/dist/core/security.js +34 -2
  114. package/dist/core/sequence.js +11 -2
  115. package/dist/core/setup-flow.js +141 -13
  116. package/dist/core/spawn-check.js +16 -2
  117. package/dist/core/staleness.js +73 -2
  118. package/dist/core/state.js +250 -54
  119. package/dist/core/store-resolution.js +45 -12
  120. package/dist/core/worktree.js +90 -26
  121. package/dist/facts.js +8 -8
  122. package/dist/facts.json +7 -7
  123. package/docs/PROTOCOL.md +223 -0
  124. package/docs/adapters/openclaw.md +43 -43
  125. package/docs/architecture/project-refs.md +328 -328
  126. package/docs/cli.md +2097 -2096
  127. package/docs/concepts/coordination.md +52 -52
  128. package/docs/concepts/coordinator-runbook.md +129 -0
  129. package/docs/concepts/dispatch-lifecycle.md +245 -245
  130. package/docs/concepts/event-log-store.md +928 -0
  131. package/docs/concepts/ideation-loop.md +317 -317
  132. package/docs/concepts/loop-engine.md +520 -511
  133. package/docs/concepts/mcp-governance.md +268 -268
  134. package/docs/concepts/memory.md +89 -88
  135. package/docs/concepts/multi-agent-workflows.md +167 -167
  136. package/docs/concepts/observer-protocol.md +361 -0
  137. package/docs/concepts/parallel-merge-protocol.md +71 -0
  138. package/docs/concepts/plans-and-claims.md +217 -174
  139. package/docs/concepts/project-md-convention.md +35 -35
  140. package/docs/concepts/runtime-notes.md +38 -38
  141. package/docs/concepts/skills.md +78 -0
  142. package/docs/concepts/troubleshooting.md +254 -254
  143. package/docs/concepts/workspace-bootstrapping.md +142 -81
  144. package/docs/context-format-changelog.md +35 -35
  145. package/docs/context-format.md +48 -48
  146. package/docs/index.md +65 -65
  147. package/docs/integrations/agents.md +162 -162
  148. package/docs/integrations/claude-code.md +23 -23
  149. package/docs/integrations/cline.md +87 -88
  150. package/docs/integrations/codex.md +2 -2
  151. package/docs/integrations/continue.md +60 -60
  152. package/docs/integrations/copilot.md +82 -80
  153. package/docs/integrations/cursor.md +23 -23
  154. package/docs/integrations/kilocode.md +72 -72
  155. package/docs/integrations/mcp.md +377 -377
  156. package/docs/integrations/mistral-vibe.md +122 -122
  157. package/docs/integrations/openclaw.md +99 -98
  158. package/docs/integrations/opencode.md +84 -84
  159. package/docs/integrations/overview.md +122 -122
  160. package/docs/integrations/roo.md +74 -74
  161. package/docs/integrations/windsurf.md +83 -83
  162. package/docs/mcp-schema-changelog.md +360 -329
  163. package/docs/playbooks/integration/index.md +121 -121
  164. package/docs/playbooks/orchestration.md +37 -0
  165. package/docs/playbooks/productivity/index.md +99 -99
  166. package/docs/playbooks/team/index.md +117 -117
  167. package/docs/product/agent-first-model.md +184 -184
  168. package/docs/product/entity-model-audit.md +462 -462
  169. package/docs/product/positioning.md +86 -86
  170. package/docs/quickstart-existing-project.md +107 -107
  171. package/docs/quickstart.md +148 -147
  172. package/docs/release-maintenance.md +79 -79
  173. package/docs/reputation.md +52 -52
  174. package/docs/review.md +45 -45
  175. package/docs/security.md +212 -53
  176. package/docs/server-operations.md +118 -118
  177. package/docs/storage.md +110 -108
  178. 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 brainclaw check-security.
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 --security
9
- # Do not edit manually — regenerate with brainclaw setup --security
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) return 0 ;;
18
- -*) continue ;;
19
- *) break ;;
20
- esac
21
- done
22
- return 1
23
- }
24
-
25
- extract_packages() {
26
- local packages=""
27
- local skip_next=false
28
- local past_command=false
29
- for arg in "$@"; do
30
- if [ "$skip_next" = true ]; then
31
- skip_next=false
32
- continue
33
- fi
34
- case "$arg" in
35
- install|add|i)
36
- past_command=true
37
- continue
38
- ;;
39
- --save-dev|--save-peer|--save-optional|-D|-P|-O|-g|--global)
40
- continue
41
- ;;
42
- --registry|--prefix)
43
- skip_next=true
44
- continue
45
- ;;
46
- -*)
47
- continue
48
- ;;
49
- *)
50
- if [ "$past_command" = true ]; then
51
- if [ -n "$packages" ]; then
52
- packages="$packages,$arg"
53
- else
54
- packages="$arg"
55
- fi
56
- fi
57
- ;;
58
- esac
59
- done
60
- echo "$packages"
61
- }
62
-
63
- # Main logic
64
- if is_install_command "$@"; then
65
- packages=$(extract_packages "$@")
66
- if [ -n "$packages" ]; then
67
- ecosystem="npm"
68
- if [ "$ORIGINAL_CMD" = "pip" ] || [ "$ORIGINAL_CMD" = "pip3" ]; then
69
- ecosystem="pypi"
70
- fi
71
-
72
- result=$("$BRAINCLAW_BIN" check-security --packages "$packages" --ecosystem "$ecosystem" --json 2>/dev/null)
73
- exit_code=$?
74
-
75
- if [ $exit_code -eq 2 ]; then
76
- echo "[brainclaw-guard] BLOCKED supply chain risk detected" >&2
77
- echo "[brainclaw-guard] Run: brainclaw check-security --packages \\"$packages\\" for details" >&2
78
- exit 1
79
- elif [ $exit_code -eq 1 ]; then
80
- echo "[brainclaw-guard] WARNING — potential supply chain risk" >&2
81
- echo "[brainclaw-guard] Run: brainclaw check-security --packages \\"$packages\\" for details" >&2
82
- fi
83
- # exit_code 0 = pass, continue silently
84
- fi
85
- fi
86
-
87
- exec "$ORIGINAL_CMD" "$@"
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 --security
93
- # Do not edit manually — regenerate with brainclaw setup --security
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 Extract-Packages($arguments) {
109
- $packages = @()
110
- $pastCommand = $false
111
- $skipNext = $false
112
- foreach ($arg in $arguments) {
113
- if ($skipNext) { $skipNext = $false; continue }
114
- if ($arg -match "^(install|add|i)$") { $pastCommand = $true; continue }
115
- if ($arg -match "^--(registry|prefix)$") { $skipNext = $true; continue }
116
- if ($arg -match "^-") { continue }
117
- if ($pastCommand) { $packages += $arg }
118
- }
119
- return ($packages -join ",")
120
- }
121
-
122
- if (Is-InstallCommand $Args) {
123
- $packages = Extract-Packages $Args
124
- if ($packages) {
125
- $ecosystem = "npm"
126
- if ($OriginalCmd -match "^pip") { $ecosystem = "pypi" }
127
-
128
- try {
129
- & $BrainclawBin check-security --packages $packages --ecosystem $ecosystem --json 2>$null | Out-Null
130
- $exitCode = $LASTEXITCODE
131
- } catch {
132
- $exitCode = 0
133
- }
134
-
135
- if ($exitCode -eq 2) {
136
- Write-Error "[brainclaw-guard] BLOCKED - supply chain risk detected"
137
- Write-Error "[brainclaw-guard] Run: brainclaw check-security --packages \`"$packages\`" for details"
138
- exit 1
139
- } elseif ($exitCode -eq 1) {
140
- Write-Warning "[brainclaw-guard] WARNING - potential supply chain risk"
141
- Write-Warning "[brainclaw-guard] Run: brainclaw check-security --packages \`"$packages\`" for details"
142
- }
143
- }
144
- }
145
-
146
- & $OriginalCmd @Args
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