@ran-sh/dsh-crew 2.2.11 → 2.2.13

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crew",
3
- "version": "2.2.11",
3
+ "version": "2.2.13",
4
4
  "description": "Dispatch subtasks to DeepSeek Harness (DSH) agents as native subagents with live progress",
5
5
  "author": {
6
6
  "name": "ZSeven-W"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ran-sh/dsh-crew",
3
- "version": "2.2.11",
3
+ "version": "2.2.13",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
package/src/hub/index.mjs CHANGED
@@ -28,6 +28,7 @@ import { createCanonicalJobEvent, projectWorkflowView } from '../job-contracts.m
28
28
  import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
29
29
  import { loadCiEvidence } from '../ci-evidence.mjs';
30
30
  import { ensureCrewWorkspace, releaseCrewWorkspace } from '../crew-workspaces.mjs';
31
+ import { sameRepoPath } from '../workspace-isolation.mjs';
31
32
  import { loadRoleProfiles, resolveRoleProfile, saveRoleProfiles } from '../role-profiles.mjs';
32
33
  import { addContextReferences, buildWorkspaceTask, isSafeBranchName, loadWorkspaceContexts, resolveWorkspaceContext, saveWorkspaceContexts } from '../workspace-context.mjs';
33
34
  import { buildExtensionContract } from '../extension-contract.mjs';
@@ -897,7 +898,12 @@ export class WorkerRegistry { constructor(ctx) {
897
898
  // This project's stable worker/reviewer workspace, reset to the job's base
898
899
  // revision and held for the job's duration, so the Harness groups these
899
900
  // sessions into two entries instead of one per job.
900
- const created = await ensureCrewWorkspace({ cwd, role: jobRole, baseRevision: workspace_branch });
901
+ // The cross-SID git exception is granted only for a root the OPERATOR listed
902
+ // in `trusted_workspace_roots`: this API authenticates a loopback socket, not
903
+ // a Windows account, so an API caller's path can never grant itself the trust.
904
+ const approvedRoots = Array.isArray(cfg.trusted_workspace_roots) ? cfg.trusted_workspace_roots : [];
905
+ const trustedRepoRoot = approvedRoots.find((root) => typeof root === 'string' && sameRepoPath(root, cwd)) ?? null;
906
+ const created = await ensureCrewWorkspace({ cwd, trustedRepoRoot, role: jobRole, baseRevision: workspace_branch });
901
907
  if (!created.ok) throw Object.assign(new Error(created.error ?? created.reason), { code: created.reason });
902
908
  executionCwd = created.worktreePath;
903
909
  isolatedWorkspace = { worktreePath: created.worktreePath, repoRoot: created.repoRoot, stable: true, release: created.release };
@@ -396,6 +396,13 @@ export const GLOBAL_CONFIG_DEFAULTS = {
396
396
  // default: the interactive desktop entry is what opens a session, and an extra
397
397
  // always-on harness process is the operator's choice to make.
398
398
  frontend_autostart: false,
399
+ // Opt-in trust: repository roots whose ownership differs from this account's, for
400
+ // which the hub's worktree dispatch may carry a request-scoped git
401
+ // `safe.directory` exception. The hub API authenticates a loopback socket, not a
402
+ // Windows account, so this file — edited by the operator, not by an API caller —
403
+ // is what turns a foreign-owned checkout into a legal dispatch target. Exact
404
+ // roots only; wildcards are refused.
405
+ trusted_workspace_roots: [],
399
406
  // Worker provider routing for HUB workers: which DSH provider backs each
400
407
  // worker session. Fresh installs stay on the built-in DeepSeek provider;
401
408
  // follow-dsh is an explicit opt-in that uses the provider selected in DSH
@@ -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.11';
39
+ export const RUNTIME_VERSION = '2.2.13';
40
40
  export const HUB_PROTOCOL_VERSION = 1;
41
41
 
42
42
  export const HUB_CAPABILITIES = Object.freeze([
@@ -25,6 +25,8 @@ export const NOT_GIT_REPOSITORY = 'NOT_GIT_REPOSITORY';
25
25
  export const GIT_NOT_FOUND = 'GIT_NOT_FOUND';
26
26
  export const GIT_TIMEOUT = 'GIT_TIMEOUT';
27
27
  export const GIT_ERROR = 'GIT_ERROR';
28
+ /** Git refused to read a repository owned by another account; the trust decision is the operator's. */
29
+ export const WORKTREE_GIT_OWNERSHIP_UNSAFE = 'WORKTREE_GIT_OWNERSHIP_UNSAFE';
28
30
  /** A valid repository whose HEAD does not resolve because nothing is committed. */
29
31
  export const REPOSITORY_HAS_NO_COMMITS = 'REPOSITORY_HAS_NO_COMMITS';
30
32
  export const WORKTREE_LOCKED = 'WORKTREE_LOCKED';
@@ -67,6 +69,12 @@ async function runGit(runner, args, opts) {
67
69
  const r = await runner(args, opts);
68
70
  const stderr = r.stderr ?? '';
69
71
  if (/not a git repository/i.test(stderr)) return { ok: false, reason: NOT_GIT_REPOSITORY, error: stderr.trim() };
72
+ // Git's ownership refusal is its own condition, distinct from a broken
73
+ // repository: the tree exists, Crew can see it, and the remedy is a trust
74
+ // decision that belongs to the operator, not a repair step. Naming it keeps
75
+ // "the worktree looks unowned" (WORKSPACE_CONFLICT) and generic GIT_ERROR
76
+ // from misdescribing a shared checkout.
77
+ if (/dubious ownership/i.test(stderr)) return { ok: false, reason: WORKTREE_GIT_OWNERSHIP_UNSAFE, code: r.code, error: stderr.trim() };
70
78
  if (r.code != null && r.code !== 0) return { ok: false, reason: r.code === -1 ? GIT_NOT_FOUND : GIT_ERROR, code: r.code, error: stderr.trim() || 'git exited non-zero' };
71
79
  return { ok: true, code: r.code, stdout: r.stdout ?? '', stderr };
72
80
  } catch (err) {
@@ -101,6 +109,13 @@ export function sameRepoPath(left, right) {
101
109
  export function withSafeDirectory(git, trustedRepoRoot) {
102
110
  const run = git ?? defaultRunner;
103
111
  const safe = resolve(String(trustedRepoRoot ?? ''));
112
+ // Git reads a trailing `/*` as a prefix wildcard, and on POSIX `*` is a legal
113
+ // filename, so a value that could outlive "exactly one repository" is refused
114
+ // here rather than handed to Git and discovered later.
115
+ if (safe.includes('*')) {
116
+ const error = `Crew trust refusal: the safe.directory anchor must be one exact path without wildcards, got ${safe}`;
117
+ return async () => ({ code: 1, stdout: '', stderr: error });
118
+ }
104
119
  return (args, opts) => run(['-c', `safe.directory=${safe}`, ...args], opts);
105
120
  }
106
121