brainclaw 1.7.4 → 1.7.5
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 +9 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/release-claims.js +3 -2
- package/dist/commands/release-notes.js +4 -3
- package/dist/commands/session-end.js +27 -9
- package/dist/commands/sync.js +7 -4
- package/dist/facts.js +3 -3
- package/dist/facts.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -345,6 +345,15 @@ npm run test:coverage # with coverage report
|
|
|
345
345
|
|
|
346
346
|
For older releases (v0.x and the early v1.0 launch series), `git log` on `master` is the source of truth — every release commit follows the `chore(release): bump version to <semver>` convention, and the matching feature/fix commits reference their plan id (e.g. `feat(mcp): self-heal ... (pln#478)`).
|
|
347
347
|
|
|
348
|
+
### v1.7.5
|
|
349
|
+
|
|
350
|
+
- **Security patch (recommended upgrade)** — fixes a git command-injection / RCE
|
|
351
|
+
vector flagged by Socket AI: several commands interpolated a git ref (notably
|
|
352
|
+
one derived from the persisted session `git_sha`) into `execSync` shell
|
|
353
|
+
strings. All git calls now use `execFileSync` (no shell) and `git_sha` is
|
|
354
|
+
validated as a hex SHA. No functional change. (session-end, release-claims,
|
|
355
|
+
release-notes, sync)
|
|
356
|
+
|
|
348
357
|
### v1.7.4
|
|
349
358
|
|
|
350
359
|
- **Dispatch observability + worker DX hardening** (from a real cross-project
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { memoryExists } from '../core/io.js';
|
|
3
3
|
import { mutate } from '../core/mutation-pipeline.js';
|
|
4
4
|
import { listClaims, releaseClaim } from '../core/claims.js';
|
|
@@ -23,7 +23,8 @@ export function runReleaseClaims(options = {}) {
|
|
|
23
23
|
const ref1 = options.ref1 ?? 'ORIG_HEAD';
|
|
24
24
|
const ref2 = options.ref2 ?? 'HEAD';
|
|
25
25
|
try {
|
|
26
|
-
|
|
26
|
+
// Security: execFileSync (no shell) so ref1/ref2 cannot inject (Socket 2026-06-08 class).
|
|
27
|
+
const output = execFileSync('git', ['diff', '--name-only', ref1, ref2], { encoding: 'utf-8' });
|
|
27
28
|
changedFiles = output.split('\n').map((f) => f.trim()).filter(Boolean);
|
|
28
29
|
}
|
|
29
30
|
catch {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { loadConfig } from '../core/config.js';
|
|
3
3
|
import { memoryExists } from '../core/io.js';
|
|
4
4
|
import { checkBrainclawInstallableUpdate, getInstalledBrainclawVersion, } from '../core/brainclaw-version.js';
|
|
@@ -11,7 +11,8 @@ export function generateAgentReleaseNotes(cwd, since) {
|
|
|
11
11
|
const baseRef = since ?? findLastVersionTag(cwd) ?? 'HEAD~20';
|
|
12
12
|
let commits;
|
|
13
13
|
try {
|
|
14
|
-
|
|
14
|
+
// Security: execFileSync (no shell) so baseRef cannot inject (Socket 2026-06-08 class).
|
|
15
|
+
const raw = execFileSync('git', ['log', `${baseRef}..HEAD`, '--oneline', '--no-decorate'], {
|
|
15
16
|
cwd,
|
|
16
17
|
encoding: 'utf-8',
|
|
17
18
|
timeout: 10000,
|
|
@@ -58,7 +59,7 @@ export function generateAgentReleaseNotes(cwd, since) {
|
|
|
58
59
|
}
|
|
59
60
|
function findLastVersionTag(cwd) {
|
|
60
61
|
try {
|
|
61
|
-
const tag =
|
|
62
|
+
const tag = execFileSync('git', ['describe', '--tags', '--abbrev=0', 'HEAD'], {
|
|
62
63
|
cwd,
|
|
63
64
|
encoding: 'utf-8',
|
|
64
65
|
timeout: 5000,
|
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Security (Socket alert 2026-06-08, medium): a session snapshot's `git_sha` is
|
|
5
|
+
* persisted state and must be treated as untrusted — it must NEVER be
|
|
6
|
+
* interpolated into a shell command. The fix has two layers: (1) every git call
|
|
7
|
+
* below uses execFileSync (NO shell, args passed literally → no metacharacter
|
|
8
|
+
* interpretation), and (2) git_sha is validated as a plain hex SHA before it can
|
|
9
|
+
* reach a git ref. Either layer alone closes the command-injection vector; both
|
|
10
|
+
* are kept as defense in depth.
|
|
11
|
+
*/
|
|
12
|
+
export const GIT_SHA_RE = /^[0-9a-f]{7,40}$/i;
|
|
13
|
+
export function isValidGitSha(gitSha) {
|
|
14
|
+
return typeof gitSha === 'string' && GIT_SHA_RE.test(gitSha);
|
|
15
|
+
}
|
|
16
|
+
/** A trusted start ref: the snapshot SHA only if it is a valid hex SHA, else a safe literal. */
|
|
17
|
+
export function safeStartRef(gitSha) {
|
|
18
|
+
return isValidGitSha(gitSha) ? gitSha : 'HEAD~10';
|
|
19
|
+
}
|
|
3
20
|
import { memoryExists } from '../core/io.js';
|
|
4
21
|
import { buildOperationalIdentity, clearCurrentSession } from '../core/identity.js';
|
|
5
22
|
import { buildContextDiff } from '../core/context-diff.js';
|
|
@@ -166,10 +183,10 @@ export async function endSession(options = {}) {
|
|
|
166
183
|
try {
|
|
167
184
|
const snapshot = loadSessionSnapshot(sessionId, options.cwd);
|
|
168
185
|
const startSha = snapshot?.git_sha;
|
|
169
|
-
const ref = startSha
|
|
186
|
+
const ref = safeStartRef(startSha);
|
|
170
187
|
const cwd = options.cwd ?? process.cwd();
|
|
171
|
-
const commits =
|
|
172
|
-
const diffStat =
|
|
188
|
+
const commits = execFileSync('git', ['log', '--oneline', `${ref}..HEAD`], { encoding: 'utf-8', cwd }).trim();
|
|
189
|
+
const diffStat = execFileSync('git', ['diff', '--stat', `${ref}..HEAD`], { encoding: 'utf-8', cwd }).trim();
|
|
173
190
|
if (commits) {
|
|
174
191
|
const releasedClaims = listClaims(options.cwd)
|
|
175
192
|
.filter((c) => c.status === 'released' && c.agent === registered.agent_name);
|
|
@@ -178,7 +195,7 @@ export async function endSession(options = {}) {
|
|
|
178
195
|
let filesTouched = [];
|
|
179
196
|
let fullDiff;
|
|
180
197
|
try {
|
|
181
|
-
fullDiff =
|
|
198
|
+
fullDiff = execFileSync('git', ['diff', `${ref}..HEAD`], { encoding: 'utf-8', cwd, maxBuffer: 10 * 1024 * 1024 }).trim();
|
|
182
199
|
filesTouched = extractFilesFromDiff(fullDiff);
|
|
183
200
|
}
|
|
184
201
|
catch { /* fall back to empty */ }
|
|
@@ -662,8 +679,9 @@ function countSessionEditedFiles(sessionId, cwd) {
|
|
|
662
679
|
const repoCwd = cwd ?? process.cwd();
|
|
663
680
|
try {
|
|
664
681
|
const touched = new Set();
|
|
665
|
-
|
|
666
|
-
|
|
682
|
+
const sha = snapshot?.git_sha;
|
|
683
|
+
if (isValidGitSha(sha)) {
|
|
684
|
+
for (const pathEntry of execFileSync('git', ['diff', '--name-only', `${sha}..HEAD`], {
|
|
667
685
|
cwd: repoCwd,
|
|
668
686
|
encoding: 'utf-8',
|
|
669
687
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -671,14 +689,14 @@ function countSessionEditedFiles(sessionId, cwd) {
|
|
|
671
689
|
touched.add(pathEntry);
|
|
672
690
|
}
|
|
673
691
|
}
|
|
674
|
-
for (const pathEntry of
|
|
692
|
+
for (const pathEntry of execFileSync('git', ['diff', '--name-only', 'HEAD'], {
|
|
675
693
|
cwd: repoCwd,
|
|
676
694
|
encoding: 'utf-8',
|
|
677
695
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
678
696
|
}).split(/\r?\n/).filter((entry) => Boolean(entry) && shouldCountEditedPath(entry))) {
|
|
679
697
|
touched.add(pathEntry);
|
|
680
698
|
}
|
|
681
|
-
for (const pathEntry of
|
|
699
|
+
for (const pathEntry of execFileSync('git', ['ls-files', '--others', '--exclude-standard'], {
|
|
682
700
|
cwd: repoCwd,
|
|
683
701
|
encoding: 'utf-8',
|
|
684
702
|
stdio: ['ignore', 'pipe', 'ignore'],
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { memoryExists } from '../core/io.js';
|
|
5
5
|
import { loadState } from '../core/state.js';
|
|
6
6
|
import { listCandidates } from '../core/candidates.js';
|
|
@@ -63,7 +63,9 @@ export function runSync(options = {}) {
|
|
|
63
63
|
// Check git status of .brainclaw/
|
|
64
64
|
let gitStatus = '';
|
|
65
65
|
try {
|
|
66
|
-
|
|
66
|
+
// Security: execFileSync (no shell) + scopePaths spread as separate args so
|
|
67
|
+
// path specs cannot inject (Socket 2026-06-08 class).
|
|
68
|
+
gitStatus = execFileSync('git', ['status', '--porcelain', ...scopePaths], {
|
|
67
69
|
encoding: 'utf-8',
|
|
68
70
|
cwd: options.cwd ?? process.cwd(),
|
|
69
71
|
timeout: 5000,
|
|
@@ -85,11 +87,12 @@ export function runSync(options = {}) {
|
|
|
85
87
|
if (options.commit) {
|
|
86
88
|
const msg = options.message ?? `chore: sync brainclaw (${new Date().toISOString().slice(0, 10)})`;
|
|
87
89
|
try {
|
|
88
|
-
|
|
90
|
+
execFileSync('git', ['add', ...scopePaths], {
|
|
89
91
|
cwd: options.cwd ?? process.cwd(),
|
|
90
92
|
timeout: 5000,
|
|
91
93
|
});
|
|
92
|
-
|
|
94
|
+
// commit message passed as a literal arg — no shell, no escaping needed.
|
|
95
|
+
execFileSync('git', ['commit', '-m', msg], {
|
|
93
96
|
encoding: 'utf-8',
|
|
94
97
|
cwd: options.cwd ?? process.cwd(),
|
|
95
98
|
timeout: 10000,
|
package/dist/facts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
|
|
2
|
-
// Source: brainclaw v1.7.
|
|
2
|
+
// Source: brainclaw v1.7.5 on 2026-06-09T05:31:24.417Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.7.
|
|
5
|
-
"generated_at": "2026-06-
|
|
4
|
+
"version": "1.7.5",
|
|
5
|
+
"generated_at": "2026-06-09T05:31:24.417Z",
|
|
6
6
|
"tools": {
|
|
7
7
|
"count": 62,
|
|
8
8
|
"published_count": 61,
|
package/dist/facts.json
CHANGED