@ran-sh/dsh-crew 2.2.10 → 2.2.11
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/package.json
CHANGED
package/src/crew-workspaces.mjs
CHANGED
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
defaultWorktreeRoot,
|
|
28
28
|
inspectRepository,
|
|
29
29
|
resetWorktreeTo,
|
|
30
|
+
sameRepoPath,
|
|
31
|
+
withSafeDirectory,
|
|
30
32
|
} from './workspace-isolation.mjs';
|
|
31
33
|
import { acquireWorkspaceLock, DEFAULT_LOCK_WAIT_MS } from './workspace-lock.mjs';
|
|
32
34
|
|
|
@@ -81,6 +83,14 @@ export function crewWorkspaceLockPath({ repoRoot, role, root = defaultWorktreeRo
|
|
|
81
83
|
*/
|
|
82
84
|
export async function ensureCrewWorkspace({
|
|
83
85
|
cwd,
|
|
86
|
+
// The repository root the REQUEST named, when Crew may trust it for this job's git
|
|
87
|
+
// subprocesses. A shared checkout can be owned by another account, and Git refuses
|
|
88
|
+
// to read it without a `safe.directory` exception; the exception is scoped to this
|
|
89
|
+
// one root, and the repository Git resolves must be exactly that root — anything
|
|
90
|
+
// else (a subdirectory cwd, a different repo) fails closed rather than widening the
|
|
91
|
+
// trust. Worktree-local operations do not get the exception: the worktree Crew
|
|
92
|
+
// creates is owned by the account Crew runs as.
|
|
93
|
+
trustedRepoRoot = null,
|
|
84
94
|
role,
|
|
85
95
|
baseRevision,
|
|
86
96
|
at,
|
|
@@ -92,8 +102,16 @@ export async function ensureCrewWorkspace({
|
|
|
92
102
|
isAlive,
|
|
93
103
|
pid,
|
|
94
104
|
} = {}) {
|
|
95
|
-
const
|
|
105
|
+
const run = trustedRepoRoot ? withSafeDirectory(git, trustedRepoRoot) : git;
|
|
106
|
+
const repo = await inspectRepository({ cwd, git: run });
|
|
96
107
|
if (!repo.ok) return { ok: false, reason: repo.reason, error: repo.error };
|
|
108
|
+
if (trustedRepoRoot && !sameRepoPath(repo.repoRoot, trustedRepoRoot)) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
reason: 'WORKSPACE_TRUST_ANCHOR_MISMATCH',
|
|
112
|
+
error: `git resolved ${repo.repoRoot}, not the trusted root ${trustedRepoRoot}; run the job at the repository root or register a workspace context`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
97
115
|
const repoRoot = repo.repoRoot;
|
|
98
116
|
const revision = baseRevision ?? repo.baseRevision;
|
|
99
117
|
const name = crewWorkspaceName(role);
|
|
@@ -109,7 +127,7 @@ export async function ensureCrewWorkspace({
|
|
|
109
127
|
return { ok: false, reason, error };
|
|
110
128
|
};
|
|
111
129
|
|
|
112
|
-
const reused = await crewOwnsWorktree({ worktreePath, repoRoot, git });
|
|
130
|
+
const reused = await crewOwnsWorktree({ worktreePath, repoRoot, git: run });
|
|
113
131
|
if (!reused) {
|
|
114
132
|
if (existsSync(worktreePath)) {
|
|
115
133
|
// Something is there that Crew did not create. Deleting it would be a
|
|
@@ -117,7 +135,7 @@ export async function ensureCrewWorkspace({
|
|
|
117
135
|
// say which path is in the way.
|
|
118
136
|
return fail(WORKSPACE_CONFLICT, `workspace path exists but is not a Crew worktree of this repository: ${worktreePath}`);
|
|
119
137
|
}
|
|
120
|
-
const created = await createWorktreeAt({ dir: worktreePath, repoRoot, revision, purpose: name, at, git });
|
|
138
|
+
const created = await createWorktreeAt({ dir: worktreePath, repoRoot, revision, purpose: name, at, git: run });
|
|
121
139
|
if (!created.ok) return fail(created.reason, created.error);
|
|
122
140
|
}
|
|
123
141
|
|
package/src/mcp-runtime.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
cleanupIsolatedWorkspace,
|
|
15
15
|
inspectRepository,
|
|
16
16
|
captureCandidate as captureIsolationCandidate,
|
|
17
|
+
withSafeDirectory,
|
|
17
18
|
} from './workspace-isolation.mjs';
|
|
18
19
|
import { startJob, waitJob, jobView, cancelJob } from './jobs.mjs';
|
|
19
20
|
import { hub } from './hub-client.mjs';
|
|
@@ -297,7 +298,13 @@ export function buildMcpWorkflowRuntime(deps) {
|
|
|
297
298
|
}
|
|
298
299
|
// Isolated roles fail closed when the workspace
|
|
299
300
|
// is not a git repo — never silently fall back to sharing the working tree.
|
|
300
|
-
|
|
301
|
+
// The pre-check and the workspace entry share one scoped exception: the
|
|
302
|
+
// requested cwd is the only root this dispatch may trust, and only when Git
|
|
303
|
+
// resolves it as the repository root (a shared checkout owned by another
|
|
304
|
+
// account is the case this exists for).
|
|
305
|
+
const trustedRoot = job.requested_cwd;
|
|
306
|
+
const scopedGit = withSafeDirectory(null, trustedRoot);
|
|
307
|
+
const repo = await inspectRepository({ cwd: job.requested_cwd, git: scopedGit });
|
|
301
308
|
if (!repo.ok) {
|
|
302
309
|
return { ok: false, reason: repo.reason ?? 'ISOLATION_UNAVAILABLE', error: `${job.role ?? 'worker'} needs an isolated git worktree: ${repo.error ?? repo.reason}` };
|
|
303
310
|
}
|
|
@@ -309,6 +316,7 @@ export function buildMcpWorkflowRuntime(deps) {
|
|
|
309
316
|
// cannot share it.
|
|
310
317
|
const workspace = await ensureCrewWorkspace({
|
|
311
318
|
cwd: job.requested_cwd,
|
|
319
|
+
trustedRepoRoot: trustedRoot,
|
|
312
320
|
role: job.role ?? 'worker',
|
|
313
321
|
baseRevision: job.workspace_branch ?? repo.baseRevision,
|
|
314
322
|
});
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -36,7 +36,7 @@ export {
|
|
|
36
36
|
// included in the identity contract.
|
|
37
37
|
const RUNTIME_ID = randomUUID();
|
|
38
38
|
|
|
39
|
-
export const RUNTIME_VERSION = '2.2.
|
|
39
|
+
export const RUNTIME_VERSION = '2.2.11';
|
|
40
40
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
41
41
|
|
|
42
42
|
export const HUB_CAPABILITIES = Object.freeze([
|
|
@@ -77,6 +77,33 @@ async function runGit(runner, args, opts) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function repoPathKey(value) {
|
|
81
|
+
try {
|
|
82
|
+
const p = resolve(String(value ?? ''));
|
|
83
|
+
return process.platform === 'win32' ? p.toLowerCase() : p;
|
|
84
|
+
} catch { return String(value ?? ''); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Whether two paths name the same repository root on this platform. */
|
|
88
|
+
export function sameRepoPath(left, right) {
|
|
89
|
+
const a = repoPathKey(left);
|
|
90
|
+
const b = repoPathKey(right);
|
|
91
|
+
return a.length > 0 && a === b;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A shared checkout can be owned by another Windows account, and Git refuses even to
|
|
95
|
+
// read it ("dubious ownership"); the operator's usual remedy, a global
|
|
96
|
+
// `safe.directory` entry, is one Crew must never write. `-c safe.directory=<root>` is
|
|
97
|
+
// the scoped form: it lives and dies with the subprocess, persists nothing, and names
|
|
98
|
+
// exactly one repository rather than a wildcard. Callers pass the root the request
|
|
99
|
+
// itself named, and the workspace entry fails closed when Git resolves any other
|
|
100
|
+
// repository.
|
|
101
|
+
export function withSafeDirectory(git, trustedRepoRoot) {
|
|
102
|
+
const run = git ?? defaultRunner;
|
|
103
|
+
const safe = resolve(String(trustedRepoRoot ?? ''));
|
|
104
|
+
return (args, opts) => run(['-c', `safe.directory=${safe}`, ...args], opts);
|
|
105
|
+
}
|
|
106
|
+
|
|
80
107
|
const PURPOSE_MAX = 32;
|
|
81
108
|
|
|
82
109
|
/** Local wall-clock stamp: readable, and what an operator expects to see. */
|