@dsh-electron/dsh-plugin-multi-root-workspace 0.1.13 → 0.1.15

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.
Files changed (84) hide show
  1. package/CHANGELOG.en.md +36 -1
  2. package/CHANGELOG.md +36 -1
  3. package/README.en.md +27 -10
  4. package/README.md +27 -10
  5. package/cordis.patch.yml +5 -1
  6. package/detail/DETAIL.en.md +1 -0
  7. package/detail/DETAIL.md +1 -0
  8. package/detail/preview.svg +1 -0
  9. package/detail/preview.zh.svg +1 -0
  10. package/lib/{llm-message-fYORacvh.js → agent-instructions-BXuKhOzr.js} +1 -84
  11. package/lib/auto-review.js +2 -2
  12. package/lib/client.js +584 -73
  13. package/lib/client.js.map +1 -1
  14. package/lib/{command-B9855H7y.js → command-y5tfcAeC.js} +45 -12
  15. package/lib/command.js +1 -1
  16. package/lib/compat-CTJbpCxu.js +267 -0
  17. package/lib/compat.js +1 -1
  18. package/lib/{dialects-Dr4rDR-H.js → dialects-CdlG3RF9.js} +1 -1
  19. package/lib/{compat-Bg2HFVMX.js → dsh-version-DCwF7REk.js} +5 -265
  20. package/lib/{fs-CfzJq2wd.js → fs-Uws0ZpmV.js} +38 -0
  21. package/lib/fs.js +1 -1
  22. package/lib/index.js +11 -9
  23. package/lib/{instructions-KsUwUFE0.js → instructions-Ck9wp4J4.js} +695 -26
  24. package/lib/instructions.js +1 -1
  25. package/lib/llm-message-CepyzEBQ.js +84 -0
  26. package/lib/lsp.js +3 -3
  27. package/lib/{metadata-DgqSdVX_.js → metadata-99jgViHQ.js} +1 -1
  28. package/lib/{method-wrapper-CFY3p3j2.js → method-wrapper-D4oUCsEF.js} +22 -2
  29. package/lib/{native-win32-2waCRkPI.js → native-win32-C5oMtt7f.js} +2 -1
  30. package/lib/ops-compatibility.js +58 -0
  31. package/lib/ops-settings-UN2oOKEp.js +40 -0
  32. package/lib/ops-tools-k4JYaOrM.js +152 -0
  33. package/lib/process-fence.js +100 -24
  34. package/lib/{protocol-CJOvAvOS.js → protocol-CwDnFEyK.js} +54 -8
  35. package/lib/{registry-Bv431TfO.js → registry-DyDNDSJZ.js} +235 -79
  36. package/lib/{registry-lease-win32-DVTq9d4D.js → registry-lease-win32-r4puac9m.js} +2 -1
  37. package/lib/registry.js +2 -2
  38. package/lib/{review-context-NDpKeMCc.js → review-context-B-oHkU6a.js} +24 -7
  39. package/lib/review-context.js +1 -1
  40. package/lib/{roots-Ca_W1S9n.js → roots-CmaWxRVy.js} +112 -22
  41. package/lib/{sandbox-C-vCJCp3.js → sandbox-CZElTNPz.js} +1 -1
  42. package/lib/sandbox.js +1 -1
  43. package/lib/scope.js +72 -9
  44. package/lib/skills-utyuvmwr.js +104 -0
  45. package/lib/skills.js +4 -105
  46. package/lib/{tool-context-CpgIFcCB.js → tool-context-CCeY3mrK.js} +1 -1
  47. package/lib/types/authority/protocol.d.ts +5 -1
  48. package/lib/types/client/RootSwitcher.d.ts +2 -0
  49. package/lib/types/client/auto-review-settings.d.ts +18 -0
  50. package/lib/types/client/files-tree/FilesTree.d.ts +4 -1
  51. package/lib/types/client/files-tree/locales.d.ts +1 -1
  52. package/lib/types/client/locales.d.ts +40 -0
  53. package/lib/types/client/open-in-app/locales.d.ts +1 -1
  54. package/lib/types/client/ops-settings.d.ts +6 -0
  55. package/lib/types/client/skills-settings.d.ts +13 -0
  56. package/lib/types/client/styles.d.ts +1 -1
  57. package/lib/types/command.d.ts +3 -3
  58. package/lib/types/compat/auto-review-client.d.ts +58 -0
  59. package/lib/types/compat/connection-rpc.d.ts +12 -0
  60. package/lib/types/compat/dsh-version.d.ts +1 -1
  61. package/lib/types/compat/files-tree.d.ts +36 -0
  62. package/lib/types/compat/fs-patch.d.ts +6 -0
  63. package/lib/types/compat/instruction-activity.d.ts +32 -1
  64. package/lib/types/compat/koffi.d.ts +2 -0
  65. package/lib/types/compat/ops-confirmation.d.ts +17 -0
  66. package/lib/types/compat/ops-settings.d.ts +11 -0
  67. package/lib/types/compat/ops-tools.d.ts +25 -0
  68. package/lib/types/compat/skills-client.d.ts +1 -0
  69. package/lib/types/compat/tool-context.d.ts +4 -0
  70. package/lib/types/compat.d.ts +2 -2
  71. package/lib/types/fs.d.ts +3 -1
  72. package/lib/types/instructions.d.ts +6 -1
  73. package/lib/types/ops-compatibility.d.ts +34 -0
  74. package/lib/types/registry.d.ts +67 -8
  75. package/lib/types/review-context.d.ts +2 -0
  76. package/lib/types/roots.d.ts +114 -20
  77. package/lib/types/scope.d.ts +28 -2
  78. package/lib/workspace-files.js +3 -3
  79. package/lib/{workspace-routing-YPxaQ4V3.js → workspace-routing-D_qwMUh-.js} +4 -4
  80. package/locale/en.json +7 -0
  81. package/locale/zh.json +7 -0
  82. package/package.json +93 -82
  83. package/src/auto-review/README.en.md +1 -1
  84. package/src/auto-review/README.md +1 -1
@@ -9,9 +9,40 @@ export type FileActivity = {
9
9
  failed: boolean;
10
10
  };
11
11
  export declare function ptcFileActivity(event: unknown): FileActivity | undefined;
12
+ export declare function stableOpsJson(value: unknown): string;
13
+ export declare const opsActivityEvidenceKey: (kind: "native" | "ptc", id: string, name: string, args: unknown) => string;
14
+ export interface OpsActivityEvidence {
15
+ readonly targets: readonly OpsFileTarget[];
16
+ readonly complete: boolean;
17
+ }
12
18
  /** Rebuild read-only discovery candidates from successful durable activity after resume. */
13
- export declare function replayedInstructionTargets(events: readonly unknown[], origin: string | undefined): readonly string[];
19
+ export declare function replayedInstructionTargets(events: readonly unknown[], origin: string | undefined, liveOpsEvidence?: ReadonlyMap<string, OpsActivityEvidence>, pendingExec?: {
20
+ readonly callId: string;
21
+ readonly name: string;
22
+ readonly arguments: unknown;
23
+ readonly parent?: unknown;
24
+ }, options?: {
25
+ readonly opsCompatibility?: boolean;
26
+ }): readonly string[];
14
27
  /** fs tools whose success touches the file at their `file_path` argument. */
15
28
  export declare const FILE_TOUCH_TOOL_NAMES: Record<string, true>;
16
29
  /** One explicit fs-tool target, shared with live touch capture. */
17
30
  export declare function touchedPathOfToolCall(name: string, argumentsJson: string): string | undefined;
31
+ export interface OpsFileActivityInput {
32
+ name: string;
33
+ args: unknown;
34
+ cwd: string | undefined;
35
+ finalValue: unknown;
36
+ observedPaths: readonly string[];
37
+ isError?: boolean;
38
+ }
39
+ export interface OpsFileTarget {
40
+ path: string;
41
+ kind: 'read' | 'write' | 'remove';
42
+ }
43
+ export interface OpsFileActivity {
44
+ targets: readonly OpsFileTarget[];
45
+ complete: boolean;
46
+ }
47
+ export declare function normalizeOpsFileActivity(input: OpsFileActivityInput): OpsFileActivity;
48
+ export declare function pendingInstructionTargets(name: string, args: unknown): readonly string[];
@@ -0,0 +1,2 @@
1
+ export type Koffi = (typeof import('koffi'))['default'];
2
+ export declare function loadHostKoffi(): Promise<Koffi>;
@@ -0,0 +1,17 @@
1
+ /** Keep the optional host confirmation component behind a structural probe. */
2
+ import type { ComponentType } from 'react';
3
+ export interface OpsConfirmationProps {
4
+ open: boolean;
5
+ title: string;
6
+ description: string;
7
+ acknowledgeLabel: string;
8
+ cancelLabel: string;
9
+ closeLabel: string;
10
+ confirmLabel: string;
11
+ acknowledged: boolean;
12
+ disabled?: boolean;
13
+ onAcknowledgedChange(value: boolean): void;
14
+ onCancel(): void;
15
+ onConfirm(): void;
16
+ }
17
+ export declare function opsRiskConfirmation(): ComponentType<OpsConfirmationProps> | undefined;
@@ -0,0 +1,11 @@
1
+ import type { Context, Fiber } from '@deepseek-ai/cordis';
2
+ export declare const OPS_SETTINGS_ID = "multi-root-ops-compatibility";
3
+ /** Product availability policy, independent of structural Settings adaptation. */
4
+ export declare function opsCompatibilityRelease(release: string | undefined): boolean;
5
+ /** Unsupported hosts retain the config input but expose no editable live field. */
6
+ export declare function opsCompatibilityField<S>(schema: S): S;
7
+ export declare function installOpsCompatibilitySettings(ctx: Context, initial: () => unknown, changed: () => void): {
8
+ read(): unknown;
9
+ fiber: Fiber;
10
+ };
11
+ export declare function opsCompatibilityToken(ctx: Context): object | undefined;
@@ -0,0 +1,25 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { MultiRootScopeService } from '../scope.ts';
3
+ import type { ToolCaller } from './tool-context.ts';
4
+ export declare const OPS_TOOL_PREFIX = "ops_";
5
+ export declare const OPS_EXEC_NAMES: readonly string[];
6
+ export type OpsToolKind = 'exec' | 'publish' | 'unknown' | 'other';
7
+ export declare function classifyOpsTool(name: string): OpsToolKind;
8
+ export interface OpsProtectionState {
9
+ owner: object | undefined;
10
+ entered: number;
11
+ readonly drainWaiters: Set<() => void>;
12
+ }
13
+ export declare function withOpsSpawnPermit<T>(state: OpsProtectionState, body: () => T): T;
14
+ export declare function opsSpawnPermitted(state: OpsProtectionState): boolean;
15
+ export declare function createOpsProtectionState(): OpsProtectionState;
16
+ export declare function claimOpsProtection(state: OpsProtectionState): object;
17
+ export declare function revokeOpsProtection(state: OpsProtectionState, token: object): void;
18
+ export declare function opsProtectionReady(state: OpsProtectionState): boolean;
19
+ export declare function assertOpsProtectionReady(state: OpsProtectionState): void;
20
+ export declare function assertOpsProtectionOwner(state: OpsProtectionState, token: object): void;
21
+ export declare function enterOpsProtection(state: OpsProtectionState): object;
22
+ export declare function leaveOpsProtection(state: OpsProtectionState): void;
23
+ export declare function drainOpsProtection(state: OpsProtectionState): Promise<void>;
24
+ export declare function assertOpsPosixCacheAdmission(deniedRoots: readonly string[]): void;
25
+ export declare function opsDispatchBarrier(ctx: Context, scope: MultiRootScopeService, exec: ToolCaller): string | undefined;
@@ -16,6 +16,7 @@ export interface SkillsForm {
16
16
  value: boolean;
17
17
  }[], revision?: number): Promise<boolean>;
18
18
  }
19
+ export declare function bindSettingsForm(ctx: Context, entryId: string): SkillsForm | undefined;
19
20
  export declare function bindSkillsForm(ctx: Context): SkillsForm | undefined;
20
21
  export declare function clientInject(ctx: Context, services: string[], callback: (scope: Context) => void | (() => void)): void;
21
22
  export interface NamespaceRule {
@@ -5,5 +5,9 @@ export interface ToolCaller {
5
5
  readonly agent?: Agent;
6
6
  readonly callId: string;
7
7
  readonly name: string;
8
+ readonly arguments?: unknown;
9
+ readonly signal?: AbortSignal;
10
+ readonly parent?: unknown;
11
+ readonly token?: object;
8
12
  }
9
13
  export declare function aroundTool(ctx: Context, listener: (exec: ToolCaller, next: () => Promise<unknown>) => Promise<unknown>): void;
@@ -57,8 +57,8 @@ export declare class DshCompatUnsupportedError extends Error {
57
57
  * `./compat/` detect the shape they need STRUCTURALLY — whether a return value
58
58
  * is a thenable, which export name a module carries — because a structural
59
59
  * probe also survives upstream reshaping something within one release, while a
60
- * version comparison does not. The release string here is for the gate and for
61
- * diagnostics.
60
+ * version comparison does not. The release string here is for installation and
61
+ * feature-availability gates (such as the OPS 0.2.* policy), and diagnostics.
62
62
  */
63
63
  export declare class MultiRootCompatService extends Service {
64
64
  /** The verdict this composition started under. */
package/lib/types/fs.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * It extends the local backend so every text-storage mechanic — resolve, stat,
5
5
  * read/stream, listing, the atomic write and the read-match-write edit critical
6
6
  * section — stays the upstream implementation's, verbatim; this class adds only
7
- * the per-call POLICY fence on the two mutations, exactly as the upstream
7
+ * the per-call POLICY fence on mutations, exactly as the upstream
8
8
  * `SandboxedFileSystem` does, widened from one workspace root to the scope's
9
9
  * root set.
10
10
  *
@@ -64,6 +64,8 @@ export declare class MultiRootFileSystem extends LocalFileSystem {
64
64
  editText(target: FsTarget, edit: FsEditRequest, expected?: {
65
65
  version: FsVersion;
66
66
  }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsEditOutcome>;
67
+ /** Fence every endpoint of a patch, delete, or move under one admission. */
68
+ private additionalMutation;
67
69
  /** The provider must also work without the gated registry (ADR-0009). */
68
70
  private withOperation;
69
71
  /**
@@ -4,7 +4,12 @@ declare module '@deepseek-ai/cordis' {
4
4
  interface Context {
5
5
  multiRootInstructions: {
6
6
  knownRoots(session: object): readonly string[];
7
- snapshot(agent: Agent, roots: readonly string[], target: string | undefined, signal: AbortSignal): Promise<readonly CurrentRootInstruction[]>;
7
+ snapshot(agent: Agent, roots: readonly string[], target: string | readonly string[] | undefined, signal: AbortSignal, pending?: {
8
+ readonly callId: string;
9
+ readonly name: string;
10
+ readonly arguments: unknown;
11
+ readonly parent?: unknown;
12
+ }): Promise<readonly CurrentRootInstruction[]>;
8
13
  };
9
14
  }
10
15
  }
@@ -0,0 +1,34 @@
1
+ import { Service, type Context } from '@deepseek-ai/cordis';
2
+ import z from '@deepseek-ai/schemastery';
3
+ export interface Config {
4
+ readonly enabled?: boolean;
5
+ }
6
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
7
+ enabled: z<boolean, boolean, "defined">;
8
+ }>>, Schemastery.ObjectT<NoInfer<{
9
+ enabled: z<boolean, boolean, "defined">;
10
+ }>>, "plain">;
11
+ declare module '@deepseek-ai/cordis' {
12
+ interface Context {
13
+ multiRootOpsCompatibility: MultiRootOpsCompatibility;
14
+ }
15
+ }
16
+ export declare class MultiRootOpsCompatibility extends Service {
17
+ private readonly settingsConfig;
18
+ static Config: z<Schemastery.ObjectS<NoInfer<{
19
+ enabled: z<boolean, boolean, "defined">;
20
+ }>>, Schemastery.ObjectT<NoInfer<{
21
+ enabled: z<boolean, boolean, "defined">;
22
+ }>>, "plain">;
23
+ static provide: string;
24
+ static inject: string[];
25
+ constructor(ctx: Context, settingsConfig?: Config);
26
+ private settingsRead;
27
+ private currentToken;
28
+ private enabledNow;
29
+ private closing;
30
+ get token(): object | undefined;
31
+ get enabled(): boolean;
32
+ private sync;
33
+ }
34
+ export default MultiRootOpsCompatibility;
@@ -121,7 +121,12 @@ export type RegistryAuthorityState = {
121
121
  export interface Config {
122
122
  /** Absolute path of the store-wide kernel lock file. */
123
123
  readonly leasePath?: string;
124
- /** Explicit medium binding for the multi-Host authority service. */
124
+ /**
125
+ * Medium binding for the multi-Host authority service — REQUIRED since the
126
+ * single-process mode was removed (ADR-0028). Absent or empty puts the
127
+ * registry in the fail-closed `config-invalid` state: no lease, no domain,
128
+ * no grants.
129
+ */
125
130
  readonly sharedStorageRoot?: string;
126
131
  /**
127
132
  * Cap on STORED additional-root records per primary root (every state
@@ -281,8 +286,15 @@ export declare class MultiRootRegistry extends Service {
281
286
  /** Keys already warned about a historical over-limit store (one warning each). */
282
287
  private readonly warnedOverLimit;
283
288
  /**
284
- * @param ctx - the host context; `storageDomain` and `multiRootScope` are injected.
285
- * @param config - the patch row's `leasePath` and `maxAdditionalRoots`.
289
+ * The canonical value each protected boundary resolved to the FIRST time it
290
+ * could be computed, keyed by boundary kind. Never re-pinned: a later
291
+ * rename or symlink swap of the DSH home (or of the lease directory) must
292
+ * not drop the boundary the registration rules were enforced under.
293
+ */
294
+ private readonly pinnedProtectedPaths;
295
+ /**
296
+ * @param ctx - the host context; `storage` and `multiRootScope` are injected.
297
+ * @param config - the patch row's `sharedStorageRoot`, `leasePath`, and `maxAdditionalRoots`.
286
298
  */
287
299
  constructor(ctx: Context, config?: Config);
288
300
  /**
@@ -379,6 +391,44 @@ export declare class MultiRootRegistry extends Service {
379
391
  */
380
392
  withOperation<T>(policy: SandboxExecutionPolicy, kind: RegistryOperationKind, body: (operation: RegistryOperation) => T | Promise<T>): Promise<T>;
381
393
  withPrimaryOperation<T>(policy: SandboxExecutionPolicy, body: () => T): T;
394
+ /**
395
+ * The spellings one protected boundary contributes: the configured path,
396
+ * its current canonical resolution, and the canonical value pinned the
397
+ * first time it resolved — never re-pinned, so a later rename or symlink
398
+ * swap of the protected directory cannot drop the boundary the
399
+ * registration rules were enforced under. The list is deduplicated by
400
+ * {@link samePath}.
401
+ */
402
+ private boundarySpellings;
403
+ /**
404
+ * The best-known protected-boundary set, plus whether it is COMPLETE.
405
+ *
406
+ * The set comes from trusted host configuration, never from a panel or
407
+ * shell claim: the scope row's `dshHome`, the physical medium
408
+ * `storeIdentity` bound, and the lease directory beside it.
409
+ * `complete` is false — and only NEW grants may then be refused — when a
410
+ * boundary cannot be proven: an invalid `dshHome` config, a configured
411
+ * home that is not a directory right now, or a medium that was never bound. Stored classification always consumes whatever IS known
412
+ * (pinned values included), so an unknown boundary never weakens the
413
+ * revalidation of existing records.
414
+ */
415
+ private protectedBoundarySet;
416
+ /**
417
+ * The protected DSH directories this Host can prove right now, or
418
+ * `undefined` when the set cannot be proven. `undefined` blocks every NEW
419
+ * grant; it never weakens the classification of stored records.
420
+ */
421
+ private protectedPaths;
422
+ /**
423
+ * Every protected entry this Host can prove — used wherever a partial set
424
+ * must still protect stored records and received snapshots.
425
+ */
426
+ private knownProtectedPaths;
427
+ /**
428
+ * The one refusal shared by the owner mutation path and the follower
429
+ * pre-check: the protected set cannot be proven, so no new grant is safe.
430
+ */
431
+ private static protectedBoundaryUnknown;
382
432
  /**
383
433
  * Registers one additional root.
384
434
  * @param primaryRoot - the session workspace root the root belongs to.
@@ -463,7 +513,7 @@ export declare class MultiRootRegistry extends Service {
463
513
  recheck(primaryRoot: string): Promise<readonly RootStatus[]>;
464
514
  /**
465
515
  * Observe registry changes.
466
- * @param listener - called with the canonical primary root after each durable change.
516
+ * @param listener - called with the Registry key (`primaryKey()`, case-folded on Windows) after each durable change.
467
517
  * @returns the disposer that stops observing.
468
518
  */
469
519
  onChange(listener: (primaryRoot: string) => void): () => void;
@@ -495,10 +545,17 @@ export declare class MultiRootRegistry extends Service {
495
545
  */
496
546
  private statusesOf;
497
547
  /**
498
- * The stored record set of one key, or `undefined` when there is none (or
499
- * when the store itself could not be read: an unusable store reads as
500
- * "nothing registered" so reads stay answerable and the surfaces can explain
501
- * the situation, while every write fails loudly in `requireTable()`).
548
+ * The stored record set of one key: `{ record, migrated: false }` when the
549
+ * spelling key holds it; `{ record, migrated: true }` when only the legacy
550
+ * CANONICAL key does — stores written before the upstream primary spelling
551
+ * became the key (v0.1.14 and earlier). The fallback follows the CURRENT
552
+ * resolution ONLY for such a not-yet-migrated store; the first serialized
553
+ * access copies the record forward under the spelling key (see
554
+ * {@link reclassify}), after which resolution can never re-key a grant.
555
+ * `undefined` when there is no record (or when the store itself could not
556
+ * be read: an unusable store reads as "nothing registered" so reads stay
557
+ * answerable and the surfaces can explain the situation, while every write
558
+ * fails loudly in `requireTable()`).
502
559
  */
503
560
  private lookupRecords;
504
561
  /**
@@ -562,6 +619,8 @@ export declare class MultiRootRegistry extends Service {
562
619
  private commit;
563
620
  /** Deliver one key's notifications now, or collect them inside a commit. */
564
621
  private deliverNotifications;
622
+ /** Narrow owner rows for this Host, identically for projection and admission. */
623
+ private localSnapshotRoots;
565
624
  private applySnapshot;
566
625
  private remoteUnavailable;
567
626
  /**
@@ -8,8 +8,10 @@ declare module '@deepseek-ai/cordis' {
8
8
  }
9
9
  export interface ReviewExecution {
10
10
  readonly agent?: Agent;
11
+ readonly callId?: string;
11
12
  readonly name: string;
12
13
  readonly arguments: unknown;
14
+ readonly parent?: unknown;
13
15
  }
14
16
  export interface ReviewContext {
15
17
  readonly schemaVersion: 1;
@@ -14,15 +14,21 @@
14
14
  * misconfiguration, not a path to guess at.
15
15
  * 2. `missing` — the directory does not exist.
16
16
  * 3. `not-a-directory` — it exists but is not a directory.
17
- * 4. `equals-primary` — the canonical candidate IS the session's workspace
17
+ * 4. `protected-path` — the canonical candidate equals, lies inside, or
18
+ * contains a protected DSH directory (the DSH home, the registry's
19
+ * physical storage medium, or the lease directory, supplied through
20
+ * {@link RootCandidateContext.protectedPaths}): those hold the store,
21
+ * the lease, profiles and user credentials, so no access level may
22
+ * register them. Applies to every mode; no remote exemption exists.
23
+ * 5. `equals-primary` — the canonical candidate IS the session's workspace
18
24
  * root, which is already granted by the upstream policy.
19
- * 5. `primary-overlap` — the candidate lies under the workspace root, or
25
+ * 6. `primary-overlap` — the candidate lies under the workspace root, or
20
26
  * contains it. The union grants nothing new, and the displayed list would
21
27
  * stop matching the enforced root set.
22
- * 6. `duplicate` — another registered root canonicalizes to the same path.
23
- * 7. `nested` — the candidate lies under a registered root, or contains one.
24
- * Same reasoning as rule 5, applied to the additional roots.
25
- * 8. `unsupported-path` — on Windows, a network or device path (`\\server`,
28
+ * 7. `duplicate` — another registered root canonicalizes to the same path.
29
+ * 8. `nested` — the candidate lies under a registered root, or contains one.
30
+ * Same reasoning as rule 6, applied to the additional roots.
31
+ * 9. `unsupported-path` — on Windows, a network or device path (`\\server`,
26
32
  * `\\?\…`, `\\.\…`): the ACL confinement cannot be shown to cover it, so it
27
33
  * is rejected lexically before any filesystem touch.
28
34
  *
@@ -67,6 +73,23 @@ export declare function additionalRootId(value: string): AdditionalRootId;
67
73
  * `read-only` session; it does not widen anything beyond its own directory.
68
74
  */
69
75
  export type RootAccess = 'inherit' | 'read-write' | 'read-only';
76
+ /**
77
+ * Which protected DSH directory one boundary belongs to: the harness home
78
+ * (`$DSH_HOME`), the registry's physical storage medium, or the directory the
79
+ * store-wide lease occupies.
80
+ */
81
+ export type ProtectedPathKind = 'dsh-home' | 'registry-medium' | 'registry-lease';
82
+ /**
83
+ * One directory no additional root may equal, lie inside, or contain. `path`
84
+ * is an absolute spelling — the configured one, the current canonical one, or
85
+ * a canonical value pinned the first time the boundary resolved — and is
86
+ * compared with {@link samePath}/{@link isCanonicallyUnder} exactly like
87
+ * every other boundary in this module.
88
+ */
89
+ export interface ProtectedPath {
90
+ readonly kind: ProtectedPathKind;
91
+ readonly path: string;
92
+ }
70
93
  /** One registered additional root exactly as it is persisted. */
71
94
  export interface RegisteredRoot {
72
95
  /** Stable registry identity. */
@@ -139,13 +162,20 @@ export interface RootStatus extends RegisteredRoot {
139
162
  *
140
163
  * `'membership-conflict'` means the record passed every structural rule —
141
164
  * including an intact, absolute recorded grant — but its recorded boundary
142
- * overlaps another record's. Neither membership may be granted while the
143
- * conflict stands, yet the boundary remains a trustworthy negative
144
- * restriction, so {@link reservedRootBoundary} keeps it reserved and the
145
- * registry publishes the root as a withheld, deny-only entry. Every other
146
- * `invalid` verdict left the grant unverifiable, so it contributes nothing.
165
+ * overlaps another record's. `'protected-path'` means that same intact
166
+ * grant overlaps a protected DSH directory (the home, the storage medium,
167
+ * or the lease directory) — typically a store written before the boundary
168
+ * existed or on another layout. `'primary-overlap'` means the intact grant
169
+ * equals or overlaps the CURRENT canonical primary root — e.g. a primary
170
+ * spelling retargeted onto (or above) a read-only root — which withholds
171
+ * membership but must never drop the deny. All three reasons mean the same
172
+ * thing: the boundary is intact and trustworthy, so
173
+ * {@link reservedRootBoundary} keeps it reserved and the registry
174
+ * publishes the root as a withheld, deny-only entry — only the membership
175
+ * is withheld. Every other `invalid` verdict left the grant unverifiable,
176
+ * so it contributes nothing.
147
177
  */
148
- readonly invalidReason?: 'membership-conflict';
178
+ readonly invalidReason?: 'membership-conflict' | 'protected-path' | 'primary-overlap';
149
179
  }
150
180
  /** One status together with the 1-based ordinal the surfaces accept for it. */
151
181
  export interface IndexedRootStatus {
@@ -172,7 +202,7 @@ export type RootRef = {
172
202
  readonly addedAt: string;
173
203
  };
174
204
  /** Every way root handling can fail, as a stable code the surfaces localize. */
175
- export type RootValidationCode = 'not-absolute' | 'missing' | 'not-a-directory' | 'equals-primary' | 'primary-overlap' | 'duplicate' | 'nested' | 'invalid-alias' | 'invalid-alias-id' | 'not-found' | 'invalid-ref' | 'storage-unavailable' | 'registry-contended' | 'root-limit-reached' | 'config-invalid' | 'reveal-unavailable' | 'session-not-found' | 'unsupported-path' | 'root-unavailable';
205
+ export type RootValidationCode = 'not-absolute' | 'missing' | 'not-a-directory' | 'equals-primary' | 'primary-overlap' | 'duplicate' | 'nested' | 'invalid-alias' | 'invalid-alias-id' | 'not-found' | 'invalid-ref' | 'storage-unavailable' | 'registry-contended' | 'root-limit-reached' | 'config-invalid' | 'reveal-unavailable' | 'session-not-found' | 'unsupported-path' | 'root-unavailable' | 'protected-path' | 'protected-boundary-unknown' | 'revive-target-changed';
176
206
  /** A rejected root operation; `code` is the stable contract, `message` the fallback prose. */
177
207
  export declare class RootValidationError extends Error {
178
208
  readonly code: RootValidationCode;
@@ -200,6 +230,21 @@ export declare class RootValidationError extends Error {
200
230
  * @returns the expanded path (still unvalidated).
201
231
  */
202
232
  export declare function expandRootInput(raw: string, home?: string): string;
233
+ /**
234
+ * The Registry/scope lookup key of one primary root: the UPSTREAM spelling,
235
+ * normalized lexically by `path.resolve` and case-folded on Windows — never
236
+ * re-resolved. Upstream `dsh-sandbox-policy` sets `policy.workspaceRoot` to
237
+ * `session.header.cwd` verbatim, so the spelling IS the identity: a symlinked
238
+ * workspace and its realpath target are different sessions and different key
239
+ * spaces. Re-resolving here would let a replaced workspace directory
240
+ * silently re-key the record set onto another identity — the exact failure
241
+ * the redirect rules exist to prevent. This is the ONLY function allowed to
242
+ * turn a primary root into a Registry key; path geometry keeps using
243
+ * {@link canonicalRoot}.
244
+ */
245
+ export declare function primaryKey(path: string, platform?: NodeJS.Platform): string;
246
+ /** Whether two canonical paths name the same directory on this platform. */
247
+ export declare function samePath(left: string, right: string): boolean;
203
248
  /**
204
249
  * Whether `candidate` is `root` itself or lies beneath it. Both sides must
205
250
  * already be canonical: symlinks are resolved by then, so the lexical test is
@@ -242,6 +287,15 @@ export interface RootCandidateContext {
242
287
  * revived exempts only itself.
243
288
  */
244
289
  readonly existing: readonly string[];
290
+ /**
291
+ * The protected DSH directories — the home, the registry's storage medium,
292
+ * and the lease directory — that no candidate may equal, lie inside, or
293
+ * contain, at any access level and under any mode. Derived from the trusted
294
+ * host configuration by the registry; absent means "no protected set was
295
+ * provided" and is never a substitute for the boundary being unprovable
296
+ * (the caller refuses that case itself with `protected-boundary-unknown`).
297
+ */
298
+ readonly protectedPaths?: readonly ProtectedPath[];
245
299
  }
246
300
  /**
247
301
  * Judge one candidate root and return its canonical spelling.
@@ -251,6 +305,16 @@ export interface RootCandidateContext {
251
305
  * @throws {RootValidationError} with the first rule that failed.
252
306
  */
253
307
  export declare function validateRootCandidate(raw: string, context: RootCandidateContext): string;
308
+ /**
309
+ * The first protected directory `candidate` overlaps — equals, lies inside,
310
+ * or contains. Both sides must already be canonical (or absolute boundary
311
+ * spellings): the comparison is the same lexical three-way test the other
312
+ * boundary rules use, so a symlink alias can never route around it.
313
+ * @param candidate - the canonical candidate or recorded grant to test.
314
+ * @param protectedPaths - the protected DSH directories.
315
+ * @returns the overlapping protected path, or `undefined`.
316
+ */
317
+ export declare function protectedOverlap(candidate: string, protectedPaths: readonly ProtectedPath[]): ProtectedPath | undefined;
254
318
  /**
255
319
  * Re-judge every stored record of one primary root — the startup and refresh
256
320
  * path, which must never throw: a registration that stopped being usable is
@@ -262,26 +326,56 @@ export declare function validateRootCandidate(raw: string, context: RootCandidat
262
326
  * @param primaryRoot - the session workspace root the records belong to.
263
327
  * @param records - the stored records, in registry order.
264
328
  * @param platform - the platform the rules run for; defaults to `process.platform`.
329
+ * @param protectedPaths - the protected DSH directories a stored grant may not
330
+ * overlap; a record that does is reported `invalid` with
331
+ * `invalidReason: 'protected-path'` — withheld, never deleted, never re-granted.
265
332
  * @returns one status per record, in the same order.
266
333
  */
267
- export declare function classifyStoredRoots(primaryRoot: string, records: readonly RegisteredRoot[], platform?: NodeJS.Platform): RootStatus[];
334
+ export declare function classifyStoredRoots(primaryRoot: string, records: readonly RegisteredRoot[], platform?: NodeJS.Platform, protectedPaths?: readonly ProtectedPath[]): RootStatus[];
335
+ /**
336
+ * The single recorded grant a record or status carries — `undefined` unless
337
+ * EXACTLY one grant field holds a non-empty string. Shared by the conflict
338
+ * pass and {@link reservedRootBoundary}; the stored decoder is the source of
339
+ * the access level, so this never cross-checks the field against `access`.
340
+ */
341
+ export declare function singleGrantOf(record: {
342
+ readonly recordedPath?: string;
343
+ readonly readOnlyRecordedPath?: string;
344
+ readonly readWriteRecordedPath?: string;
345
+ }): string | undefined;
268
346
  /**
269
347
  * The canonical roots a status list grants, in registry order.
270
348
  * @param statuses - the statuses to project.
271
349
  * @returns the canonical paths of every `available` root.
272
350
  */
273
351
  export declare function availableRoots(statuses: readonly RootStatus[]): string[];
352
+ /**
353
+ * Apply the protected-path verdict to an already-classified list: a status
354
+ * whose recorded grant overlaps a protected directory becomes `invalid` with
355
+ * `invalidReason: 'protected-path'` — the same verdict the pass-one stored
356
+ * check produces, here applied by a consumer that classified elsewhere (a
357
+ * shared-registry follower re-judging an owner snapshot against ITS OWN
358
+ * protected set, which may differ from the owner's). Statuses already
359
+ * `invalid` keep their own verdict — a structural failure is never healed
360
+ * into a trustworthy boundary.
361
+ * @param statuses - the classified statuses to re-mark.
362
+ * @param protectedPaths - the caller's own protected DSH directories.
363
+ * @returns a new list with the verdict applied.
364
+ */
365
+ export declare function markProtectedPaths(statuses: readonly RootStatus[], protectedPaths: readonly ProtectedPath[]): RootStatus[];
274
366
  /**
275
367
  * The recorded boundary one status still RESERVES: the grant it was
276
368
  * registered for, which a new or revived registration may not overlap merely
277
369
  * because the root is currently unusable.
278
370
  *
279
- * `available`, `missing`, `redirected`, and membership-conflict statuses all
280
- * carry an intact, trustworthy recorded grant; every other `invalid` verdict
281
- * means the grant could not be parsed at all, so it reserves — and denies —
282
- * nothing. This is not an independent authorization table: it derives the
283
- * boundary from the status list {@link classifyStoredRoots} produced, with
284
- * the same grant-extraction semantics, so its answer can never disagree with
371
+ * `available`, `missing`, `redirected`, and the trustworthy-boundary
372
+ * `invalid` verdicts (`membership-conflict`, `protected-path`, and
373
+ * `primary-overlap`) all carry
374
+ * an intact recorded grant; every other `invalid` verdict means the grant
375
+ * could not be parsed at all, so it reserves — and denies — nothing. This is
376
+ * not an independent authorization table: it derives the boundary from the
377
+ * status list {@link classifyStoredRoots} produced, with the same
378
+ * grant-extraction semantics, so its answer can never disagree with
285
379
  * classification.
286
380
  * @param status - one classified registration.
287
381
  * @returns the absolute recorded boundary, or `undefined` when there is none.
@@ -17,7 +17,8 @@
17
17
  */
18
18
  import { Context, Service } from '@deepseek-ai/cordis';
19
19
  import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox';
20
- import type { RootAccess } from './roots.ts';
20
+ import { type RootAccess } from './roots.ts';
21
+ import type { OpsProtectionState } from './compat/ops-tools.ts';
21
22
  declare module '@deepseek-ai/cordis' {
22
23
  interface Context {
23
24
  multiRootScope: MultiRootScopeService;
@@ -210,6 +211,7 @@ export declare class MultiRootScopeService extends Service {
210
211
  private readonly listeners;
211
212
  /** Enclosing {@link MultiRootScopeService.batch} depth; inner calls nest. */
212
213
  private batchDepth;
214
+ readonly opsProtection: OpsProtectionState;
213
215
  /** True when a batched write coalesced its notification into the flush. */
214
216
  private batchDirty;
215
217
  private admissionProof;
@@ -233,6 +235,13 @@ export declare class MultiRootScopeService extends Service {
233
235
  * treated as evidence.
234
236
  */
235
237
  private readonly dshHome;
238
+ /**
239
+ * Whether the row carried a `dshHome` value at all — even an invalid one.
240
+ * The registry's protected-boundary set must tell "no home configured"
241
+ * (absent: nothing to protect) from "a home was supplied and rejected"
242
+ * (invalid: the boundary cannot be proven).
243
+ */
244
+ private readonly dshHomeSupplied;
236
245
  /** Observe registry publications; consumers still resolve authority from scopeOf(). */
237
246
  subscribe(listener: () => void): () => void;
238
247
  /**
@@ -257,6 +266,21 @@ export declare class MultiRootScopeService extends Service {
257
266
  toolCaps(): readonly string[];
258
267
  /** Whether malformed toolAccess caps every call, including unknown tools. */
259
268
  get toolAccessInvalid(): boolean;
269
+ /**
270
+ * The validated `dshHome` row config, classified for the registry's
271
+ * protected-boundary set: `absent` when the row carried no value (nothing
272
+ * to protect), `invalid` when it carried a value the constructor rejected
273
+ * (the boundary cannot be proven), `configured` with the accepted absolute
274
+ * path. Read-only — this changes no scope or `protectedHome` semantics.
275
+ */
276
+ get dshHomeConfig(): {
277
+ readonly kind: 'absent';
278
+ } | {
279
+ readonly kind: 'invalid';
280
+ } | {
281
+ readonly kind: 'configured';
282
+ readonly path: string;
283
+ };
260
284
  constructor(ctx: Context, config?: Config);
261
285
  /**
262
286
  * Resolve the scope of one confined call.
@@ -275,6 +299,7 @@ export declare class MultiRootScopeService extends Service {
275
299
  resolve(policy: SandboxExecutionPolicy): FilesystemScope;
276
300
  /** Read-only pending-dispatch projection. Does not mint an ALS execution grant. */
277
301
  previewForTool(policy: SandboxExecutionPolicy, toolName: string): FilesystemScope;
302
+ withToolCap<T>(name: string, fn: () => T): T;
278
303
  /** Execution and review use exactly the same parent-narrowing rule. */
279
304
  private capForTool;
280
305
  private project;
@@ -324,7 +349,8 @@ export declare class MultiRootScopeService extends Service {
324
349
  * M1 uses this from tests and smoke scripts only; M3 feeds it from plugin
325
350
  * storage. It is a plain setter so the transition swaps the data source
326
351
  * without touching any provider.
327
- * @param primaryRoot - canonical primary root to register roots under.
352
+ * @param primaryRoot - primary root the roots belong to; the Registry key
353
+ * is its upstream spelling via {@link primaryKey}, never re-resolved.
328
354
  * @param roots - the additional roots, in display order.
329
355
  */
330
356
  setAdditionalRoots(primaryRoot: string, roots: readonly AdditionalWorkspaceRoot[]): void;
@@ -1,7 +1,7 @@
1
- import { t as wrapMethod } from "./method-wrapper-CFY3p3j2.js";
1
+ import { t as wrapMethod } from "./method-wrapper-D4oUCsEF.js";
2
2
  import { c as withRegistryRead } from "./registry-operation-C2ZV3Jhw.js";
3
- import { l as isCanonicallyUnder } from "./roots-Ca_W1S9n.js";
4
- import { t as routeWorkspacePath } from "./workspace-routing-YPxaQ4V3.js";
3
+ import { l as isCanonicallyUnder } from "./roots-CmaWxRVy.js";
4
+ import { t as routeWorkspacePath } from "./workspace-routing-D_qwMUh-.js";
5
5
  import { posix, resolve, win32 } from "node:path";
6
6
  //#region src/compat/workspace-files.ts
7
7
  function adaptPathWatch(service, wrap) {