@link-assistant/hive-mind 1.35.11 → 1.35.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.35.12
4
+
5
+ ### Patch Changes
6
+
7
+ - 05a72c3: fix: reject URLs and invalid git branch names used as --base-branch (Issue #1482)
8
+ - Add `validateBranchName()` function to `solve.branch.lib.mjs` that validates branch names against git-check-ref-format rules
9
+ - Reject URLs (https://, http://, git@, ssh://) passed as --base-branch with clear error message
10
+ - Reject invalid git ref characters (spaces, ~, ^, :, ?, \*, [, ], \, control chars, .., @{)
11
+ - Add validation in `solve.config.lib.mjs` parseArguments (early catch), `solve.branch.lib.mjs` createOrCheckoutBranch (defense-in-depth), and `hive.mjs` (before forwarding to solve)
12
+ - Add 19 test cases in `tests/test-base-branch-validation.mjs`
13
+ - Add case study documentation in `docs/case-studies/issue-1482/`
14
+
3
15
  ## 1.35.11
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.35.11",
3
+ "version": "1.35.12",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
package/src/hive.mjs CHANGED
@@ -766,8 +766,16 @@ if (isDirectExecution) {
766
766
  const kebabToCamel = str => str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
767
767
  const args = [issueUrl, '--model', argv.model];
768
768
  // Special handling for options with different semantics in hive vs solve
769
- if (argv.baseBranch) args.push('--base-branch', argv.baseBranch);
770
- else if (argv.targetBranch) args.push('--base-branch', argv.targetBranch);
769
+ // Validate branch name before forwarding (issue #1482: reject URLs used as branch names)
770
+ const branchValue = argv.baseBranch || argv.targetBranch;
771
+ if (branchValue) {
772
+ const { validateBranchName } = await import('./solve.branch.lib.mjs');
773
+ const branchValidation = validateBranchName(branchValue);
774
+ if (!branchValidation.valid) {
775
+ throw new Error(`Invalid branch name for --base-branch/--target-branch: ${branchValidation.reason}`);
776
+ }
777
+ args.push('--base-branch', branchValue);
778
+ }
771
779
  if (argv.skipToolConnectionCheck || argv.toolConnectionCheck === false) args.push('--skip-tool-connection-check');
772
780
  if (argv.dryRun) args.push('--dry-run');
773
781
  if (argv.autoCleanup) args.push('--auto-cleanup'); // hive default differs from solve's auto-detect default
@@ -100,6 +100,93 @@ export function detectBranchFormat(branchName) {
100
100
  return null;
101
101
  }
102
102
 
103
+ /**
104
+ * Validates a branch name for use as --base-branch.
105
+ * Rejects URLs, invalid git ref characters, and enforces safe naming conventions.
106
+ * Based on git-check-ref-format rules: https://git-scm.com/docs/git-check-ref-format
107
+ *
108
+ * @param {string} branchName - The branch name to validate
109
+ * @returns {{ valid: boolean, reason?: string }} Validation result
110
+ */
111
+ export function validateBranchName(branchName) {
112
+ if (!branchName || typeof branchName !== 'string') {
113
+ return { valid: false, reason: 'Branch name must be a non-empty string' };
114
+ }
115
+
116
+ const trimmed = branchName.trim();
117
+ if (trimmed !== branchName) {
118
+ return { valid: false, reason: 'Branch name must not have leading or trailing whitespace' };
119
+ }
120
+
121
+ // Reject URLs (the primary use case from issue #1482)
122
+ if (/^https?:\/\//i.test(branchName) || /^git@/i.test(branchName) || /^ssh:\/\//i.test(branchName)) {
123
+ return { valid: false, reason: `"${branchName}" looks like a URL, not a branch name. Use just the branch name (e.g. "main", "develop")` };
124
+ }
125
+
126
+ // Reject if it contains :// anywhere (catches other protocol-like URLs)
127
+ if (branchName.includes('://')) {
128
+ return { valid: false, reason: `"${branchName}" contains "://" which is not valid in a branch name` };
129
+ }
130
+
131
+ // Git ref format rules:
132
+ // Cannot contain ASCII control characters (bytes < 0x20) or DEL (0x7F)
133
+ // eslint-disable-next-line no-control-regex
134
+ if (/[\x00-\x1f\x7f]/.test(branchName)) {
135
+ return { valid: false, reason: 'Branch name must not contain control characters' };
136
+ }
137
+
138
+ // Cannot contain space, ~, ^, :, ?, *, [, or backslash
139
+ if (/[ ~^:?*[\]\\]/.test(branchName)) {
140
+ return { valid: false, reason: 'Branch name contains invalid characters (spaces, ~, ^, :, ?, *, [, ] or \\ are not allowed)' };
141
+ }
142
+
143
+ // Cannot contain ..
144
+ if (branchName.includes('..')) {
145
+ return { valid: false, reason: 'Branch name must not contain ".."' };
146
+ }
147
+
148
+ // Cannot start with . or -
149
+ if (branchName.startsWith('.') || branchName.startsWith('-')) {
150
+ return { valid: false, reason: 'Branch name must not start with "." or "-"' };
151
+ }
152
+
153
+ // Cannot end with . or .lock
154
+ if (branchName.endsWith('.') || branchName.endsWith('.lock')) {
155
+ return { valid: false, reason: 'Branch name must not end with "." or ".lock"' };
156
+ }
157
+
158
+ // Cannot contain @{
159
+ if (branchName.includes('@{')) {
160
+ return { valid: false, reason: 'Branch name must not contain "@{"' };
161
+ }
162
+
163
+ // Cannot be exactly @
164
+ if (branchName === '@') {
165
+ return { valid: false, reason: 'Branch name must not be "@"' };
166
+ }
167
+
168
+ // Component-level checks: no component can start with . or end with .lock
169
+ const components = branchName.split('/');
170
+ for (const component of components) {
171
+ if (component === '') {
172
+ return { valid: false, reason: 'Branch name must not contain consecutive slashes or start/end with "/"' };
173
+ }
174
+ if (component.startsWith('.')) {
175
+ return { valid: false, reason: `Branch name component "${component}" must not start with "."` };
176
+ }
177
+ if (component.endsWith('.lock')) {
178
+ return { valid: false, reason: `Branch name component "${component}" must not end with ".lock"` };
179
+ }
180
+ }
181
+
182
+ // Reasonable length limit
183
+ if (branchName.length > 255) {
184
+ return { valid: false, reason: 'Branch name must not exceed 255 characters' };
185
+ }
186
+
187
+ return { valid: true };
188
+ }
189
+
103
190
  export async function createOrCheckoutBranch({ isContinueMode, prBranch, issueNumber, tempDir, defaultBranch, argv, log, formatAligned, $, crypto, owner, repo, prNumber }) {
104
191
  // Create a branch for the issue or checkout existing PR branch
105
192
  let branchName;
@@ -120,6 +207,13 @@ export async function createOrCheckoutBranch({ isContinueMode, prBranch, issueNu
120
207
  // Use user-specified base branch if provided, otherwise use repository default
121
208
  const baseBranch = argv.baseBranch || defaultBranch;
122
209
  const branchSource = argv.baseBranch ? 'custom' : 'default';
210
+
211
+ // Defense-in-depth: validate base branch name even if already validated at CLI parsing (issue #1482)
212
+ const baseBranchValidation = validateBranchName(baseBranch);
213
+ if (!baseBranchValidation.valid) {
214
+ throw new Error(`Invalid base branch "${baseBranch}": ${baseBranchValidation.reason}`);
215
+ }
216
+
123
217
  await log(`\n${formatAligned('🌿', 'Creating branch:', `${branchName} from ${baseBranch} (${branchSource})`)}`);
124
218
 
125
219
  // IMPORTANT: Don't use 2>&1 here as it can interfere with exit codes
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { enhanceErrorMessage, detectMalformedFlags } from './option-suggestions.lib.mjs';
11
11
  import { defaultModels, buildModelOptionDescription } from './models/index.mjs';
12
+ import { validateBranchName } from './solve.branch.lib.mjs';
12
13
 
13
14
  // Re-export for use by telegram-bot.mjs (avoids extra import lines there)
14
15
  export { detectMalformedFlags };
@@ -563,6 +564,14 @@ export const parseArguments = async (yargs, hideBin) => {
563
564
  }
564
565
  }
565
566
 
567
+ // Validate --base-branch value (issue #1482: reject URLs and invalid git branch names)
568
+ if (argv.baseBranch) {
569
+ const branchValidation = validateBranchName(argv.baseBranch);
570
+ if (!branchValidation.valid) {
571
+ throw new Error(`Invalid --base-branch value: ${branchValidation.reason}`);
572
+ }
573
+ }
574
+
566
575
  if (argv.tool && !modelExplicitlyProvided && defaultModels[argv.tool]) {
567
576
  // User did not explicitly provide --model, so use the correct default for the tool
568
577
  // (Issue #1473: centralized in models/index.mjs)