synabun 2026.4.20-beta.13 → 2026.4.20-beta.14
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
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Raw internal changelogs are archived in [changelog/](changelog/).
|
|
4
4
|
|
|
5
|
+
## 2026-04-21
|
|
6
|
+
|
|
7
|
+
### Added — Workspace Trust Flow For Windows Git "Dubious Ownership" Repos
|
|
8
|
+
- **New `POST /api/terminal/trust-workspace` endpoint** — In `neural-interface/server.js`, added `detectDubiousOwnership(dir)` helper that probes `git rev-parse --is-inside-work-tree` and matches stderr against `/dubious ownership|safe\.directory/i` (CVE-2022-24765 protection; fires on Windows when the repo is owned by a different SID — external drives, cross-user installs, copied repos). New `normalizeGitPath(dir)` converts backslashes to forward slashes and strips trailing slashes to match the format `git config --add safe.directory` expects. The endpoint validates `path` (non-empty string, no control chars, not `*`/`/`/`\\`, `existsSync` + `isDirectory`), runs `spawnGit(['config', '--global', '--add', 'safe.directory', normalizeGitPath(abs)])`, re-probes to confirm, logs `[git] trusted workspace: <path>`, and returns `{ ok, path, stillDubious }`.
|
|
9
|
+
- **`POST /api/claude-code/integrations` flags dubious workspaces on registration** — The `target === 'project'` branch now calls `detectDubiousOwnership(normalized)` after saving the project to the hook-projects list; when the error fires it adds `dubiousOwnership: true, trustPath: normalizeGitPath(normalized)` to the response JSON. Mac/Linux and healthy Windows installs see no change to response shape.
|
|
10
|
+
- **`promptTrustWorkspace(trustPath)` modal in the file-explorer add-project flow** — In `neural-interface/public/shared/ui-file-explorer.js`, `addProjectFlow()` now reads the integrations response and `await`s a new `promptTrustWorkspace()` modal when `dubiousOwnership` is true. The modal reuses the existing `.fe-modal-overlay` system with a `.fe-trust-modal` amber variant, shows the exact path in a `.fe-trust-path` monospace chip, and offers **Skip** (close, no git change) or **Trust workspace** (POSTs to `/api/terminal/trust-workspace`, toasts the result, closes). The promise resolves either way so the add flow never blocks.
|
|
11
|
+
- **Inline trust row in the onboarding wizard** — In `neural-interface/public/onboarding.html`, the project-add step on `$addBtn` now persists `dubiousOwnership` + `trustPath` fields onto each `wizardState.projects[i]` entry, and `renderProjectList()` appends an amber inline `⚠ Git ownership mismatch — [Trust workspace]` row under the card when flagged. Click POSTs to `/api/terminal/trust-workspace`; on success the row swaps to `✓ Workspace trusted`. Separate from the file-explorer modal because onboarding already has its own card-based flow and shouldn't launch overlay dialogs.
|
|
12
|
+
- **Amber modal styling** — In `neural-interface/public/shared/styles.css`, added `.fe-trust-modal` overrides of `.fe-modal-title` (amber `#ffb74d`) and `.fe-modal-confirm` (warm `rgba(255,183,77,…)`) so the trust dialog is visually distinct from the destructive-red delete modal. New `.fe-trust-path` class styles the monospace path chip with dark background and `word-break: break-all`.
|
|
13
|
+
|
|
14
|
+
### Fixed — Git UI Silently Disabled On Windows Across Shim / Version / PATH Variants
|
|
15
|
+
- **`git` invocation rewritten to resolve absolute path, then fall back to shell only when needed** — In `neural-interface/server.js`, added `resolveGitPath()` that runs `where git` on Windows / `command -v git` on Unix via `execSync` at first use, caches the result, and prefers `.exe` entries over `.cmd`/`.bat` shims (Scoop, winget) so `spawnSync` can execute git directly without cmd.exe involvement. New `spawnGit(args, opts)` uses the resolved absolute path with no shell on all platforms; only falls back to `shell: true` with per-arg metacharacter quoting when path resolution fails OR the resolved target is a `.cmd`/`.bat` shim. Logs `[git] resolved: <path>` at first use so Windows users can confirm which binary Node is running.
|
|
16
|
+
- **Chained branch-name fallback for old git installs** — `getGitInfo()` and `GET /api/terminal/branches` used `git branch --show-current` which requires git 2.22+ — older Git for Windows builds silently exited non-zero and the branch label disappeared. Added chained fallback: `branch --show-current` → `rev-parse --abbrev-ref HEAD` → `rev-parse --short HEAD`. The `--abbrev-ref` form works on every git version and handles both named branches and detached HEAD (returns literal `"HEAD"`, which the parser treats as "no branch" and escalates to the short SHA).
|
|
17
|
+
- **Plain `git branch` parse replacing `--format=%(refname:short)`** — The branch-list endpoint's `--format=%(refname:short)` arg was prone to cmd.exe variable-expansion mangling on Windows even with double-quoting. Replaced with bare `git branch` and a `line.replace(/^[\s*]+/, '').trim()` parser that strips the current-branch marker and filters out `(HEAD detached …)` lines and rename arrows. Removes the cmd.exe `%(…)` hazard entirely.
|
|
18
|
+
- **All git calls now route through `spawnGit` with unified error logging** — The call sites (`getGitInfo`, `/api/terminal/branches`, `/api/terminal/checkout`, the project-add integration probe) had inconsistent handling — some used raw `execSync`, some `spawnSync` directly. Every git call now goes through `spawnGit(['…'])`, and every failure routes through a new `logGitFailure(where, err)` helper that suppresses the expected "not a git repository" case, deduplicates the "git not found on PATH" warning via `_gitMissingLogged`, and logs everything else with one-line stderr.
|
|
19
|
+
- **Startup `git --version` probe** — Added a `setImmediate(() => spawnGit(['--version']))` at module init that always logs `[git] git version X.Y.Z — OK` or `[git] startup probe FAILED: <reason>. File explorer git UI will be hidden.` — the first diagnostic Windows users should check, since every prior git failure was silently swallowed by the catch blocks.
|
|
20
|
+
- **`windowsHide: true` on every git spawn** — Prevents a momentary cmd.exe console window flash on Windows whenever the file explorer refreshes git status.
|
|
21
|
+
|
|
5
22
|
## 2026-04-20
|
|
6
23
|
|
|
7
24
|
### Added — Install Toggles For Claude / Gemini / Codex / OpenCode In Onboarding Step 5
|
|
@@ -2154,15 +2154,58 @@ document.getElementById('buildBtn').addEventListener('click', async () => {
|
|
|
2154
2154
|
wizardState.projects.forEach((p, i) => {
|
|
2155
2155
|
const card = document.createElement('div');
|
|
2156
2156
|
card.className = 'onboard-project-card';
|
|
2157
|
-
card.style.cssText = 'display:flex;
|
|
2157
|
+
card.style.cssText = 'display:flex;flex-direction:column;gap:8px;padding:10px 12px;background:rgba(109,213,140,0.06);border:1px solid rgba(109,213,140,0.2);border-radius:8px;font-size:12px';
|
|
2158
|
+
const safePath = p.path.replace(/\\/g, '/');
|
|
2158
2159
|
card.innerHTML = `
|
|
2159
|
-
<
|
|
2160
|
-
|
|
2161
|
-
<div style="
|
|
2162
|
-
|
|
2160
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
2161
|
+
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:rgba(109,213,140,0.8);stroke-width:2;flex-shrink:0"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
2162
|
+
<div style="flex:1;min-width:0">
|
|
2163
|
+
<div style="color:var(--t-bright);font-weight:500">${p.label}</div>
|
|
2164
|
+
<div style="color:var(--t-muted);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${safePath}">${safePath}</div>
|
|
2165
|
+
</div>
|
|
2166
|
+
<button class="onboard-project-remove" style="background:none;border:none;color:var(--t-muted);cursor:pointer;font-size:16px;padding:2px 6px;line-height:1;opacity:0.5" title="Remove">×</button>
|
|
2163
2167
|
</div>
|
|
2164
|
-
<button class="onboard-project-remove" style="background:none;border:none;color:var(--t-muted);cursor:pointer;font-size:16px;padding:2px 6px;line-height:1;opacity:0.5" title="Remove">×</button>
|
|
2165
2168
|
`;
|
|
2169
|
+
if (p.dubiousOwnership && !p.trusted) {
|
|
2170
|
+
const trustRow = document.createElement('div');
|
|
2171
|
+
trustRow.className = 'onboard-trust-row';
|
|
2172
|
+
trustRow.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 8px;background:rgba(255,183,77,0.08);border:1px solid rgba(255,183,77,0.25);border-radius:6px;font-size:11px;color:rgba(255,204,128,0.9)';
|
|
2173
|
+
trustRow.innerHTML = `
|
|
2174
|
+
<span>⚠ Git ownership mismatch — workspace needs to be trusted</span>
|
|
2175
|
+
<button class="onboard-trust-btn" style="margin-left:auto;background:rgba(255,183,77,0.2);border:1px solid rgba(255,183,77,0.35);color:#ffe0b2;padding:4px 10px;border-radius:5px;font-size:11px;cursor:pointer">Trust workspace</button>
|
|
2176
|
+
`;
|
|
2177
|
+
const $btn = trustRow.querySelector('.onboard-trust-btn');
|
|
2178
|
+
$btn.addEventListener('click', async () => {
|
|
2179
|
+
$btn.disabled = true;
|
|
2180
|
+
$btn.textContent = 'Trusting...';
|
|
2181
|
+
try {
|
|
2182
|
+
const r = await fetch('/api/terminal/trust-workspace', {
|
|
2183
|
+
method: 'POST',
|
|
2184
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2185
|
+
body: JSON.stringify({ path: p.trustPath || safePath }),
|
|
2186
|
+
});
|
|
2187
|
+
const d = await r.json();
|
|
2188
|
+
if (d.ok) {
|
|
2189
|
+
p.trusted = true;
|
|
2190
|
+
renderProjectList();
|
|
2191
|
+
} else {
|
|
2192
|
+
$btn.disabled = false;
|
|
2193
|
+
$btn.textContent = 'Trust workspace';
|
|
2194
|
+
$error.textContent = d.error || 'Trust failed';
|
|
2195
|
+
}
|
|
2196
|
+
} catch {
|
|
2197
|
+
$btn.disabled = false;
|
|
2198
|
+
$btn.textContent = 'Trust workspace';
|
|
2199
|
+
$error.textContent = 'Trust failed';
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
2202
|
+
card.appendChild(trustRow);
|
|
2203
|
+
} else if (p.trusted) {
|
|
2204
|
+
const okRow = document.createElement('div');
|
|
2205
|
+
okRow.style.cssText = 'font-size:11px;color:rgba(109,213,140,0.8)';
|
|
2206
|
+
okRow.textContent = '✓ Workspace trusted';
|
|
2207
|
+
card.appendChild(okRow);
|
|
2208
|
+
}
|
|
2166
2209
|
card.querySelector('.onboard-project-remove').addEventListener('click', () => {
|
|
2167
2210
|
wizardState.projects.splice(i, 1);
|
|
2168
2211
|
renderProjectList();
|
|
@@ -2212,15 +2255,16 @@ document.getElementById('buildBtn').addEventListener('click', async () => {
|
|
|
2212
2255
|
|
|
2213
2256
|
// Register via API
|
|
2214
2257
|
$addBtn.innerHTML = '<div class="spinner" style="width:14px;height:14px"></div> Registering...';
|
|
2258
|
+
let integData = null;
|
|
2215
2259
|
try {
|
|
2216
2260
|
const res = await fetch('/api/claude-code/integrations', {
|
|
2217
2261
|
method: 'POST',
|
|
2218
2262
|
headers: { 'Content-Type': 'application/json' },
|
|
2219
2263
|
body: JSON.stringify({ target: 'project', projectPath: folderPath, label }),
|
|
2220
2264
|
});
|
|
2221
|
-
|
|
2222
|
-
if (!
|
|
2223
|
-
$error.textContent =
|
|
2265
|
+
integData = await res.json();
|
|
2266
|
+
if (!integData.ok) {
|
|
2267
|
+
$error.textContent = integData.error || 'Failed to register project.';
|
|
2224
2268
|
$addBtn.disabled = false;
|
|
2225
2269
|
$addBtn.innerHTML = origHTML;
|
|
2226
2270
|
return;
|
|
@@ -2232,7 +2276,12 @@ document.getElementById('buildBtn').addEventListener('click', async () => {
|
|
|
2232
2276
|
return;
|
|
2233
2277
|
}
|
|
2234
2278
|
|
|
2235
|
-
|
|
2279
|
+
const projectEntry = { path: folderPath, label };
|
|
2280
|
+
if (integData && integData.dubiousOwnership && integData.trustPath) {
|
|
2281
|
+
projectEntry.dubiousOwnership = true;
|
|
2282
|
+
projectEntry.trustPath = integData.trustPath;
|
|
2283
|
+
}
|
|
2284
|
+
wizardState.projects.push(projectEntry);
|
|
2236
2285
|
$addBtn.disabled = false;
|
|
2237
2286
|
$addBtn.innerHTML = origHTML;
|
|
2238
2287
|
renderProjectList();
|
|
@@ -13868,6 +13868,23 @@
|
|
|
13868
13868
|
}
|
|
13869
13869
|
.fe-modal-confirm:hover { background: rgba(229,115,115,0.35); color: #ffab91; }
|
|
13870
13870
|
|
|
13871
|
+
/* ── Trust-workspace modal (amber accent, different from destructive red) ── */
|
|
13872
|
+
.fe-trust-modal .fe-modal-title { color: #ffb74d; }
|
|
13873
|
+
.fe-trust-modal .fe-modal-confirm { background: rgba(255,183,77,0.18); color: #ffcc80; }
|
|
13874
|
+
.fe-trust-modal .fe-modal-confirm:hover { background: rgba(255,183,77,0.3); color: #ffe0b2; }
|
|
13875
|
+
.fe-trust-modal .fe-modal-confirm:disabled { opacity: 0.5; cursor: default; }
|
|
13876
|
+
.fe-trust-path {
|
|
13877
|
+
margin-top: 10px;
|
|
13878
|
+
padding: 8px 10px;
|
|
13879
|
+
background: rgba(0,0,0,0.3);
|
|
13880
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
13881
|
+
border-radius: 6px;
|
|
13882
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
13883
|
+
font-size: 11px;
|
|
13884
|
+
color: rgba(255,255,255,0.6);
|
|
13885
|
+
word-break: break-all;
|
|
13886
|
+
}
|
|
13887
|
+
|
|
13871
13888
|
/* ── Color palette popup ── */
|
|
13872
13889
|
.fe-color-palette {
|
|
13873
13890
|
position: absolute;
|
|
@@ -648,7 +648,13 @@ async function addProjectFlow() {
|
|
|
648
648
|
|
|
649
649
|
// Register as a project
|
|
650
650
|
const label = data.path.split(/[\\/]/).pop() || data.path;
|
|
651
|
-
await fetch('/api/claude-code/integrations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target: 'project', projectPath: data.path, label }) });
|
|
651
|
+
const integRes = await fetch('/api/claude-code/integrations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target: 'project', projectPath: data.path, label }) });
|
|
652
|
+
const integData = await integRes.json().catch(() => ({}));
|
|
653
|
+
|
|
654
|
+
// Windows git CVE-2022-24765 safe.directory prompt — only fires when server detected the specific error.
|
|
655
|
+
if (integData && integData.dubiousOwnership && integData.trustPath) {
|
|
656
|
+
await promptTrustWorkspace(integData.trustPath);
|
|
657
|
+
}
|
|
652
658
|
|
|
653
659
|
// Refresh projects list and select the new one
|
|
654
660
|
await fetchProjects();
|
|
@@ -665,6 +671,67 @@ async function addProjectFlow() {
|
|
|
665
671
|
}
|
|
666
672
|
}
|
|
667
673
|
|
|
674
|
+
function promptTrustWorkspace(trustPath) {
|
|
675
|
+
return new Promise((resolve) => {
|
|
676
|
+
const existing = document.getElementById('fe-trust-modal');
|
|
677
|
+
if (existing) existing.remove();
|
|
678
|
+
|
|
679
|
+
const overlay = document.createElement('div');
|
|
680
|
+
overlay.id = 'fe-trust-modal';
|
|
681
|
+
overlay.className = 'fe-modal-overlay';
|
|
682
|
+
|
|
683
|
+
const modal = document.createElement('div');
|
|
684
|
+
modal.className = 'fe-modal fe-trust-modal';
|
|
685
|
+
modal.innerHTML = `
|
|
686
|
+
<div class="fe-modal-title">Git ownership mismatch</div>
|
|
687
|
+
<div class="fe-modal-body">
|
|
688
|
+
Git is refusing to read this workspace because it's owned by a different user account. Trusting it adds the directory to your global <code>safe.directory</code> config so Synabun's git UI can show the branch and changes.
|
|
689
|
+
<div class="fe-trust-path" id="fe-trust-path"></div>
|
|
690
|
+
</div>
|
|
691
|
+
<div class="fe-modal-actions">
|
|
692
|
+
<button class="fe-modal-btn fe-modal-cancel" id="fe-trust-skip">Skip</button>
|
|
693
|
+
<button class="fe-modal-btn fe-modal-confirm" id="fe-trust-confirm">Trust workspace</button>
|
|
694
|
+
</div>
|
|
695
|
+
`;
|
|
696
|
+
overlay.appendChild(modal);
|
|
697
|
+
document.body.appendChild(overlay);
|
|
698
|
+
modal.querySelector('#fe-trust-path').textContent = trustPath;
|
|
699
|
+
|
|
700
|
+
requestAnimationFrame(() => overlay.classList.add('fe-modal-overlay--in'));
|
|
701
|
+
|
|
702
|
+
const done = () => {
|
|
703
|
+
overlay.classList.remove('fe-modal-overlay--in');
|
|
704
|
+
setTimeout(() => overlay.remove(), 150);
|
|
705
|
+
document.removeEventListener('keydown', onKey);
|
|
706
|
+
resolve();
|
|
707
|
+
};
|
|
708
|
+
const onKey = (ev) => { if (ev.key === 'Escape') done(); };
|
|
709
|
+
document.addEventListener('keydown', onKey);
|
|
710
|
+
|
|
711
|
+
modal.querySelector('#fe-trust-skip').addEventListener('click', done);
|
|
712
|
+
overlay.addEventListener('click', (ev) => { if (ev.target === overlay) done(); });
|
|
713
|
+
|
|
714
|
+
modal.querySelector('#fe-trust-confirm').addEventListener('click', async () => {
|
|
715
|
+
const btn = modal.querySelector('#fe-trust-confirm');
|
|
716
|
+
btn.disabled = true;
|
|
717
|
+
btn.textContent = 'Trusting...';
|
|
718
|
+
try {
|
|
719
|
+
const res = await fetch('/api/terminal/trust-workspace', {
|
|
720
|
+
method: 'POST',
|
|
721
|
+
headers: { 'Content-Type': 'application/json' },
|
|
722
|
+
body: JSON.stringify({ path: trustPath }),
|
|
723
|
+
});
|
|
724
|
+
const data = await res.json();
|
|
725
|
+
if (data.ok) showToast('Workspace trusted');
|
|
726
|
+
else showToast(data.error || 'Trust failed');
|
|
727
|
+
} catch {
|
|
728
|
+
showToast('Trust failed');
|
|
729
|
+
}
|
|
730
|
+
done();
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
668
735
|
function initProjectSelector() {
|
|
669
736
|
const btn = $('fe-project-btn');
|
|
670
737
|
const wrap = $('fe-project-selector');
|
|
@@ -1704,21 +1771,21 @@ function focusEditorLocation(lineNum, columnNum = 1) {
|
|
|
1704
1771
|
syncGutterScroll();
|
|
1705
1772
|
}
|
|
1706
1773
|
|
|
1707
|
-
async function openFileEditor(filePath, opts = {}) {
|
|
1708
|
-
try {
|
|
1709
|
-
const normalizedPath = String(filePath || '').replace(/\\/g, '/');
|
|
1710
|
-
const res = await fetch(`/api/file-content?path=${encodeURIComponent(filePath)}`);
|
|
1711
|
-
const data = await res.json();
|
|
1712
|
-
if (!res.ok) {
|
|
1713
|
-
const isOpencodeConfig = normalizedPath.endsWith('/.config/opencode/config.json');
|
|
1714
|
-
const isAllowlistMiss = res.status === 403 && data?.error === 'Path outside registered project roots';
|
|
1715
|
-
if (isOpencodeConfig && isAllowlistMiss) {
|
|
1716
|
-
showToast('Restart Synabun to apply OpenCode config editor support');
|
|
1717
|
-
return;
|
|
1718
|
-
}
|
|
1719
|
-
showToast(data.error || 'Cannot open file');
|
|
1720
|
-
return;
|
|
1721
|
-
}
|
|
1774
|
+
async function openFileEditor(filePath, opts = {}) {
|
|
1775
|
+
try {
|
|
1776
|
+
const normalizedPath = String(filePath || '').replace(/\\/g, '/');
|
|
1777
|
+
const res = await fetch(`/api/file-content?path=${encodeURIComponent(filePath)}`);
|
|
1778
|
+
const data = await res.json();
|
|
1779
|
+
if (!res.ok) {
|
|
1780
|
+
const isOpencodeConfig = normalizedPath.endsWith('/.config/opencode/config.json');
|
|
1781
|
+
const isAllowlistMiss = res.status === 403 && data?.error === 'Path outside registered project roots';
|
|
1782
|
+
if (isOpencodeConfig && isAllowlistMiss) {
|
|
1783
|
+
showToast('Restart Synabun to apply OpenCode config editor support');
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
showToast(data.error || 'Cannot open file');
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1722
1789
|
|
|
1723
1790
|
_editorOpen = true;
|
|
1724
1791
|
_editorFilePath = data.path;
|
|
@@ -12755,6 +12755,18 @@ app.post('/api/claude-code/integrations', (req, res) => {
|
|
|
12755
12755
|
// Create default category tree for the project (parent + children)
|
|
12756
12756
|
try { ensureProjectCategories(); } catch {}
|
|
12757
12757
|
|
|
12758
|
+
// Probe for git "dubious ownership" — signals the UI to show an explicit trust prompt.
|
|
12759
|
+
// Only relevant on Windows when the project dir is owned by a different SID than the
|
|
12760
|
+
// account running Node (external drives, copied repos, etc.). No-op on Mac/Linux.
|
|
12761
|
+
let dubiousOwnership = false;
|
|
12762
|
+
let trustPath = null;
|
|
12763
|
+
try {
|
|
12764
|
+
if (detectDubiousOwnership(normalized)) {
|
|
12765
|
+
dubiousOwnership = true;
|
|
12766
|
+
trustPath = normalizeGitPath(normalized);
|
|
12767
|
+
}
|
|
12768
|
+
} catch { /* non-critical */ }
|
|
12769
|
+
|
|
12758
12770
|
// Auto-create greeting config entry for the new project
|
|
12759
12771
|
try {
|
|
12760
12772
|
const greetingKey = projectLabel.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
@@ -12774,7 +12786,7 @@ app.post('/api/claude-code/integrations', (req, res) => {
|
|
|
12774
12786
|
}
|
|
12775
12787
|
} catch { /* non-critical */ }
|
|
12776
12788
|
|
|
12777
|
-
return res.json({ ok: true, message: `Hook enabled for ${basename(normalized)}
|
|
12789
|
+
return res.json({ ok: true, message: `Hook enabled for ${basename(normalized)}.`, dubiousOwnership, trustPath });
|
|
12778
12790
|
}
|
|
12779
12791
|
|
|
12780
12792
|
res.status(400).json({ error: 'Invalid target. Use "global" or "project".' });
|
|
@@ -17156,6 +17168,22 @@ setImmediate(() => {
|
|
|
17156
17168
|
}
|
|
17157
17169
|
});
|
|
17158
17170
|
|
|
17171
|
+
// Git "dubious ownership" detection — CVE-2022-24765 safety check refuses to operate on
|
|
17172
|
+
// repos owned by a different OS user/SID. Common on Windows external drives or repos
|
|
17173
|
+
// copied from another machine. Returns true only when git emits the specific error.
|
|
17174
|
+
function detectDubiousOwnership(dir) {
|
|
17175
|
+
const r = spawnGit(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
|
|
17176
|
+
if (r.status === 0) return false;
|
|
17177
|
+
const stderr = r.stderr?.toString?.() || '';
|
|
17178
|
+
return /dubious ownership|safe\.directory/i.test(stderr);
|
|
17179
|
+
}
|
|
17180
|
+
|
|
17181
|
+
// Normalize a filesystem path for `git config --add safe.directory` — forward-slashed
|
|
17182
|
+
// absolute path, no trailing slash.
|
|
17183
|
+
function normalizeGitPath(dir) {
|
|
17184
|
+
return String(dir).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
17185
|
+
}
|
|
17186
|
+
|
|
17159
17187
|
/** Try to get git status for a directory. Returns { branch, statuses } or null. */
|
|
17160
17188
|
function getGitInfo(dir) {
|
|
17161
17189
|
// Check if inside a git work tree (spawnGit for consistent path resolution)
|
|
@@ -17353,6 +17381,38 @@ app.get('/api/terminal/branches', (req, res) => {
|
|
|
17353
17381
|
}
|
|
17354
17382
|
});
|
|
17355
17383
|
|
|
17384
|
+
// Trust a workspace path with git — runs `git config --global --add safe.directory <path>`
|
|
17385
|
+
// so git stops refusing to operate on it (CVE-2022-24765 dubious-ownership protection).
|
|
17386
|
+
// Triggered by the add-project UI flows when the server detects the dubious-ownership error.
|
|
17387
|
+
app.post('/api/terminal/trust-workspace', (req, res) => {
|
|
17388
|
+
const { path: dir } = req.body || {};
|
|
17389
|
+
if (!dir || typeof dir !== 'string') return res.status(400).json({ ok: false, error: 'path required' });
|
|
17390
|
+
if (/[\x00-\x1f]/.test(dir) || dir === '*' || dir === '/' || dir === '\\') {
|
|
17391
|
+
return res.status(400).json({ ok: false, error: 'Invalid path' });
|
|
17392
|
+
}
|
|
17393
|
+
let abs;
|
|
17394
|
+
try {
|
|
17395
|
+
abs = resolve(dir);
|
|
17396
|
+
if (!existsSync(abs)) return res.status(400).json({ ok: false, error: `Directory not found: ${abs}` });
|
|
17397
|
+
if (!statSync(abs).isDirectory()) return res.status(400).json({ ok: false, error: 'Path is not a directory' });
|
|
17398
|
+
} catch (err) {
|
|
17399
|
+
return res.status(400).json({ ok: false, error: err.message });
|
|
17400
|
+
}
|
|
17401
|
+
|
|
17402
|
+
const gitPath = normalizeGitPath(abs);
|
|
17403
|
+
const r = spawnGit(['config', '--global', '--add', 'safe.directory', gitPath]);
|
|
17404
|
+
if (r.status !== 0) {
|
|
17405
|
+
const stderr = r.stderr?.toString?.()?.trim() || r.error?.message || 'git config failed';
|
|
17406
|
+
logGitFailure('config --add safe.directory', r.error || { stderr: r.stderr });
|
|
17407
|
+
return res.status(500).json({ ok: false, error: stderr });
|
|
17408
|
+
}
|
|
17409
|
+
|
|
17410
|
+
// Confirm by re-probing
|
|
17411
|
+
const stillDubious = detectDubiousOwnership(abs);
|
|
17412
|
+
console.log(`[git] trusted workspace: ${gitPath}${stillDubious ? ' (but still flagged — check permissions)' : ''}`);
|
|
17413
|
+
res.json({ ok: true, path: gitPath, stillDubious });
|
|
17414
|
+
});
|
|
17415
|
+
|
|
17356
17416
|
app.post('/api/terminal/checkout', (req, res) => {
|
|
17357
17417
|
const { path: dir, branch } = req.body;
|
|
17358
17418
|
if (!dir || !branch) return res.status(400).json({ error: 'path and branch required' });
|