@dsh-electron/dsh-plugin-multi-root-workspace 0.1.14 → 0.1.16

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 (73) hide show
  1. package/CHANGELOG.en.md +32 -1
  2. package/CHANGELOG.md +32 -1
  3. package/README.en.md +25 -8
  4. package/README.md +25 -8
  5. package/cordis.patch.yml +5 -1
  6. package/lib/{llm-message-fYORacvh.js → agent-instructions-BXuKhOzr.js} +1 -84
  7. package/lib/auto-review.js +2 -2
  8. package/lib/client.js +969 -100
  9. package/lib/client.js.map +1 -1
  10. package/lib/{command-Dcz6nULo.js → command-DaVJ-mK3.js} +76 -11
  11. package/lib/command.js +1 -1
  12. package/lib/compat-CTJbpCxu.js +267 -0
  13. package/lib/compat.js +1 -1
  14. package/lib/{dialects-Dr4rDR-H.js → dialects-CdlG3RF9.js} +1 -1
  15. package/lib/{compat-veDqCOoa.js → dsh-version-DCwF7REk.js} +2 -264
  16. package/lib/index.js +10 -8
  17. package/lib/{instructions-KsUwUFE0.js → instructions-Ca80SkZq.js} +696 -27
  18. package/lib/instructions.js +1 -1
  19. package/lib/llm-message-CepyzEBQ.js +84 -0
  20. package/lib/lsp.js +4 -4
  21. package/lib/{metadata-DgqSdVX_.js → metadata-99jgViHQ.js} +1 -1
  22. package/lib/{method-wrapper-CFY3p3j2.js → method-wrapper-D4oUCsEF.js} +22 -2
  23. package/lib/{native-win32-CRNXr11U.js → native-win32-C5oMtt7f.js} +1 -1
  24. package/lib/ops-compatibility.js +58 -0
  25. package/lib/ops-settings-UN2oOKEp.js +40 -0
  26. package/lib/ops-tools-k4JYaOrM.js +152 -0
  27. package/lib/process-fence.js +193 -34
  28. package/lib/{protocol-CnR1KTXy.js → protocol-CwDnFEyK.js} +8 -4
  29. package/lib/{registry-DR_sMuvS.js → registry-BibiukWk.js} +236 -80
  30. package/lib/{registry-lease-win32-DDO-c7ig.js → registry-lease-win32-r4puac9m.js} +1 -1
  31. package/lib/{registry-operation-C2ZV3Jhw.js → registry-operation-CIlmt6_c.js} +5 -1
  32. package/lib/registry.js +2 -2
  33. package/lib/{review-context-NDpKeMCc.js → review-context-OxO-fMWR.js} +24 -7
  34. package/lib/review-context.js +1 -1
  35. package/lib/{roots-Ca_W1S9n.js → roots-CmaWxRVy.js} +112 -22
  36. package/lib/{sandbox-C-vCJCp3.js → sandbox-aobdyQGi.js} +7 -4
  37. package/lib/sandbox.js +1 -1
  38. package/lib/scope.js +162 -12
  39. package/lib/skills-utyuvmwr.js +104 -0
  40. package/lib/skills.js +5 -106
  41. package/lib/{tool-context-CpgIFcCB.js → tool-context-CCeY3mrK.js} +1 -1
  42. package/lib/types/authority/protocol.d.ts +5 -1
  43. package/lib/types/client/files-tree/FilesTree.d.ts +49 -6
  44. package/lib/types/client/files-tree/adapter.d.ts +69 -16
  45. package/lib/types/client/files-tree/face.d.ts +31 -2
  46. package/lib/types/client/files-tree/locate.d.ts +74 -0
  47. package/lib/types/client/files-tree/navigator.d.ts +98 -0
  48. package/lib/types/client/index.d.ts +7 -0
  49. package/lib/types/client/locales.d.ts +30 -0
  50. package/lib/types/client/ops-settings.d.ts +6 -0
  51. package/lib/types/client/skills-settings.d.ts +3 -2
  52. package/lib/types/client/styles.d.ts +1 -1
  53. package/lib/types/command.d.ts +3 -3
  54. package/lib/types/compat/files-tree.d.ts +8 -1
  55. package/lib/types/compat/forge-entrypoints.d.ts +44 -0
  56. package/lib/types/compat/instruction-activity.d.ts +32 -1
  57. package/lib/types/compat/ops-confirmation.d.ts +17 -0
  58. package/lib/types/compat/ops-settings.d.ts +11 -0
  59. package/lib/types/compat/ops-tools.d.ts +25 -0
  60. package/lib/types/compat/session-existence.d.ts +41 -0
  61. package/lib/types/compat/skills-client.d.ts +1 -0
  62. package/lib/types/compat/tool-context.d.ts +4 -0
  63. package/lib/types/compat.d.ts +2 -2
  64. package/lib/types/instructions.d.ts +6 -1
  65. package/lib/types/ops-compatibility.d.ts +34 -0
  66. package/lib/types/registry-operation.d.ts +7 -0
  67. package/lib/types/registry.d.ts +67 -8
  68. package/lib/types/review-context.d.ts +2 -0
  69. package/lib/types/roots.d.ts +116 -20
  70. package/lib/types/scope.d.ts +95 -3
  71. package/lib/workspace-files.js +4 -4
  72. package/lib/{workspace-routing-YPxaQ4V3.js → workspace-routing-D_qwMUh-.js} +4 -4
  73. package/package.json +5 -1
@@ -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;
@@ -2,6 +2,13 @@ import type { SandboxMode } from '@deepseek-ai/dsh-sandbox';
2
2
  import type { FilesystemScope } from './scope.ts';
3
3
  export type RegistryOperationKind = 'fs-write' | 'fs-edit' | 'process' | 'pty' | 'lsp' | 'files' | 'instructions' | 'skills';
4
4
  export declare function currentRegistryOperation(proof: object): RegistryOperation | undefined;
5
+ /**
6
+ * The enclosing admission frame's mint disposition for this proof. `expired`
7
+ * frames still occupy the async context of their descendants; an expired
8
+ * admission must never bequeath a launchable profile.
9
+ */
10
+ export type OperationFrameState = 'active' | 'expired' | 'absent';
11
+ export declare function operationFrameState(proof: object): OperationFrameState;
5
12
  export interface RegistryOperationIdentity {
6
13
  readonly operationId: string;
7
14
  readonly kind: RegistryOperationKind;
@@ -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,9 @@ 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'
206
+ /** Synthesized at the panel dispatch catch — the session existed but could not resolve a root during the request (writer-held, mid-resume, or a race resolved after the throw); no RootValidationError is constructed with it. */
207
+ | 'session-not-live' | 'unsupported-path' | 'root-unavailable' | 'protected-path' | 'protected-boundary-unknown' | 'revive-target-changed';
176
208
  /** A rejected root operation; `code` is the stable contract, `message` the fallback prose. */
177
209
  export declare class RootValidationError extends Error {
178
210
  readonly code: RootValidationCode;
@@ -200,6 +232,21 @@ export declare class RootValidationError extends Error {
200
232
  * @returns the expanded path (still unvalidated).
201
233
  */
202
234
  export declare function expandRootInput(raw: string, home?: string): string;
235
+ /**
236
+ * The Registry/scope lookup key of one primary root: the UPSTREAM spelling,
237
+ * normalized lexically by `path.resolve` and case-folded on Windows — never
238
+ * re-resolved. Upstream `dsh-sandbox-policy` sets `policy.workspaceRoot` to
239
+ * `session.header.cwd` verbatim, so the spelling IS the identity: a symlinked
240
+ * workspace and its realpath target are different sessions and different key
241
+ * spaces. Re-resolving here would let a replaced workspace directory
242
+ * silently re-key the record set onto another identity — the exact failure
243
+ * the redirect rules exist to prevent. This is the ONLY function allowed to
244
+ * turn a primary root into a Registry key; path geometry keeps using
245
+ * {@link canonicalRoot}.
246
+ */
247
+ export declare function primaryKey(path: string, platform?: NodeJS.Platform): string;
248
+ /** Whether two canonical paths name the same directory on this platform. */
249
+ export declare function samePath(left: string, right: string): boolean;
203
250
  /**
204
251
  * Whether `candidate` is `root` itself or lies beneath it. Both sides must
205
252
  * already be canonical: symlinks are resolved by then, so the lexical test is
@@ -242,6 +289,15 @@ export interface RootCandidateContext {
242
289
  * revived exempts only itself.
243
290
  */
244
291
  readonly existing: readonly string[];
292
+ /**
293
+ * The protected DSH directories — the home, the registry's storage medium,
294
+ * and the lease directory — that no candidate may equal, lie inside, or
295
+ * contain, at any access level and under any mode. Derived from the trusted
296
+ * host configuration by the registry; absent means "no protected set was
297
+ * provided" and is never a substitute for the boundary being unprovable
298
+ * (the caller refuses that case itself with `protected-boundary-unknown`).
299
+ */
300
+ readonly protectedPaths?: readonly ProtectedPath[];
245
301
  }
246
302
  /**
247
303
  * Judge one candidate root and return its canonical spelling.
@@ -251,6 +307,16 @@ export interface RootCandidateContext {
251
307
  * @throws {RootValidationError} with the first rule that failed.
252
308
  */
253
309
  export declare function validateRootCandidate(raw: string, context: RootCandidateContext): string;
310
+ /**
311
+ * The first protected directory `candidate` overlaps — equals, lies inside,
312
+ * or contains. Both sides must already be canonical (or absolute boundary
313
+ * spellings): the comparison is the same lexical three-way test the other
314
+ * boundary rules use, so a symlink alias can never route around it.
315
+ * @param candidate - the canonical candidate or recorded grant to test.
316
+ * @param protectedPaths - the protected DSH directories.
317
+ * @returns the overlapping protected path, or `undefined`.
318
+ */
319
+ export declare function protectedOverlap(candidate: string, protectedPaths: readonly ProtectedPath[]): ProtectedPath | undefined;
254
320
  /**
255
321
  * Re-judge every stored record of one primary root — the startup and refresh
256
322
  * path, which must never throw: a registration that stopped being usable is
@@ -262,26 +328,56 @@ export declare function validateRootCandidate(raw: string, context: RootCandidat
262
328
  * @param primaryRoot - the session workspace root the records belong to.
263
329
  * @param records - the stored records, in registry order.
264
330
  * @param platform - the platform the rules run for; defaults to `process.platform`.
331
+ * @param protectedPaths - the protected DSH directories a stored grant may not
332
+ * overlap; a record that does is reported `invalid` with
333
+ * `invalidReason: 'protected-path'` — withheld, never deleted, never re-granted.
265
334
  * @returns one status per record, in the same order.
266
335
  */
267
- export declare function classifyStoredRoots(primaryRoot: string, records: readonly RegisteredRoot[], platform?: NodeJS.Platform): RootStatus[];
336
+ export declare function classifyStoredRoots(primaryRoot: string, records: readonly RegisteredRoot[], platform?: NodeJS.Platform, protectedPaths?: readonly ProtectedPath[]): RootStatus[];
337
+ /**
338
+ * The single recorded grant a record or status carries — `undefined` unless
339
+ * EXACTLY one grant field holds a non-empty string. Shared by the conflict
340
+ * pass and {@link reservedRootBoundary}; the stored decoder is the source of
341
+ * the access level, so this never cross-checks the field against `access`.
342
+ */
343
+ export declare function singleGrantOf(record: {
344
+ readonly recordedPath?: string;
345
+ readonly readOnlyRecordedPath?: string;
346
+ readonly readWriteRecordedPath?: string;
347
+ }): string | undefined;
268
348
  /**
269
349
  * The canonical roots a status list grants, in registry order.
270
350
  * @param statuses - the statuses to project.
271
351
  * @returns the canonical paths of every `available` root.
272
352
  */
273
353
  export declare function availableRoots(statuses: readonly RootStatus[]): string[];
354
+ /**
355
+ * Apply the protected-path verdict to an already-classified list: a status
356
+ * whose recorded grant overlaps a protected directory becomes `invalid` with
357
+ * `invalidReason: 'protected-path'` — the same verdict the pass-one stored
358
+ * check produces, here applied by a consumer that classified elsewhere (a
359
+ * shared-registry follower re-judging an owner snapshot against ITS OWN
360
+ * protected set, which may differ from the owner's). Statuses already
361
+ * `invalid` keep their own verdict — a structural failure is never healed
362
+ * into a trustworthy boundary.
363
+ * @param statuses - the classified statuses to re-mark.
364
+ * @param protectedPaths - the caller's own protected DSH directories.
365
+ * @returns a new list with the verdict applied.
366
+ */
367
+ export declare function markProtectedPaths(statuses: readonly RootStatus[], protectedPaths: readonly ProtectedPath[]): RootStatus[];
274
368
  /**
275
369
  * The recorded boundary one status still RESERVES: the grant it was
276
370
  * registered for, which a new or revived registration may not overlap merely
277
371
  * because the root is currently unusable.
278
372
  *
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
373
+ * `available`, `missing`, `redirected`, and the trustworthy-boundary
374
+ * `invalid` verdicts (`membership-conflict`, `protected-path`, and
375
+ * `primary-overlap`) all carry
376
+ * an intact recorded grant; every other `invalid` verdict means the grant
377
+ * could not be parsed at all, so it reserves — and denies — nothing. This is
378
+ * not an independent authorization table: it derives the boundary from the
379
+ * status list {@link classifyStoredRoots} produced, with the same
380
+ * grant-extraction semantics, so its answer can never disagree with
285
381
  * classification.
286
382
  * @param status - one classified registration.
287
383
  * @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;
@@ -199,6 +200,24 @@ export interface Config {
199
200
  */
200
201
  readonly dshHome?: string;
201
202
  }
203
+ /**
204
+ * The baseline a mint resolved to. Only primary-bounded forms may join the
205
+ * unadmitted ledger: `clamped` (primary-root clamp) and `read-only` (the
206
+ * upstream read-only skeleton — never writable). `ambient` mints —
207
+ * `restrictedExecution`'s ambient-minus-denies profile, and any mint whose
208
+ * scope could not be proven unavailable — may only live bound to an
209
+ * admission frame, and otherwise die.
210
+ */
211
+ export type MintForm = 'clamped' | 'read-only' | 'ambient';
212
+ /** The authorization basis a mint carried at its synchronous entry, stamped onto its ledger entry. */
213
+ export interface MintStamp {
214
+ /** Publication generation the mint's authorization snapshot was resolved under. */
215
+ readonly version: number;
216
+ /** Tool-cap basis at the same instant. */
217
+ readonly capped: boolean;
218
+ /** The profile form the mint produced; gates ledger admission. */
219
+ readonly form: MintForm;
220
+ }
202
221
  /**
203
222
  * The `ctx.multiRootScope` service. Providers ask it — never the filesystem —
204
223
  * what the current scope is, which is what keeps the fs fence and every kernel
@@ -210,9 +229,18 @@ export declare class MultiRootScopeService extends Service {
210
229
  private readonly listeners;
211
230
  /** Enclosing {@link MultiRootScopeService.batch} depth; inner calls nest. */
212
231
  private batchDepth;
232
+ readonly opsProtection: OpsProtectionState;
213
233
  /** True when a batched write coalesced its notification into the flush. */
214
234
  private batchDirty;
215
235
  private admissionProof;
236
+ /**
237
+ * The publication generation the mint ledger is keyed on: bumped when a
238
+ * root-table write or an authority transition changes the authorization
239
+ * basis (a repeat publish of identical content is not a publication), so a
240
+ * minted profile dies with the state it was computed from.
241
+ */
242
+ private publicationVersion;
243
+ private mintedProfiles;
216
244
  /**
217
245
  * The registry-authority fact the registry row publishes. Defaults to
218
246
  * `available`: a composition without a registry row can never have
@@ -233,6 +261,13 @@ export declare class MultiRootScopeService extends Service {
233
261
  * treated as evidence.
234
262
  */
235
263
  private readonly dshHome;
264
+ /**
265
+ * Whether the row carried a `dshHome` value at all — even an invalid one.
266
+ * The registry's protected-boundary set must tell "no home configured"
267
+ * (absent: nothing to protect) from "a home was supplied and rejected"
268
+ * (invalid: the boundary cannot be proven).
269
+ */
270
+ private readonly dshHomeSupplied;
236
271
  /** Observe registry publications; consumers still resolve authority from scopeOf(). */
237
272
  subscribe(listener: () => void): () => void;
238
273
  /**
@@ -249,6 +284,8 @@ export declare class MultiRootScopeService extends Service {
249
284
  batch<T>(body: () => T): T;
250
285
  /** Notify subscribers now, or coalesce into the enclosing batch's flush. */
251
286
  private notifySubscribers;
287
+ /** A publication superseded the authorization basis: every outstanding minted profile dies. */
288
+ private bumpPublication;
252
289
  /**
253
290
  * The sorted names of the tools `toolAccess` caps — read-only for the
254
291
  * prompt contribution. An invalid config is not enumerated (it caps every
@@ -257,6 +294,21 @@ export declare class MultiRootScopeService extends Service {
257
294
  toolCaps(): readonly string[];
258
295
  /** Whether malformed toolAccess caps every call, including unknown tools. */
259
296
  get toolAccessInvalid(): boolean;
297
+ /**
298
+ * The validated `dshHome` row config, classified for the registry's
299
+ * protected-boundary set: `absent` when the row carried no value (nothing
300
+ * to protect), `invalid` when it carried a value the constructor rejected
301
+ * (the boundary cannot be proven), `configured` with the accepted absolute
302
+ * path. Read-only — this changes no scope or `protectedHome` semantics.
303
+ */
304
+ get dshHomeConfig(): {
305
+ readonly kind: 'absent';
306
+ } | {
307
+ readonly kind: 'invalid';
308
+ } | {
309
+ readonly kind: 'configured';
310
+ readonly path: string;
311
+ };
260
312
  constructor(ctx: Context, config?: Config);
261
313
  /**
262
314
  * Resolve the scope of one confined call.
@@ -264,8 +316,46 @@ export declare class MultiRootScopeService extends Service {
264
316
  * @returns the canonical scope; `additionalRoots` is empty when none are registered.
265
317
  */
266
318
  requireAdmission(proof: object): void;
267
- rememberProfile(argv: readonly string[]): void;
319
+ /**
320
+ * The stamp an asynchronously completing mint must carry: the publication
321
+ * generation and tool-cap basis at the mint's synchronous entry, before any
322
+ * provider probe could run. An in-flight mint that crosses a real
323
+ * publication keeps its entry generation, so the ledger refuses it rather
324
+ * than re-stamping stale authorization under the current publication.
325
+ * @param form - the profile form the mint will produce (`clamped` and
326
+ * `read-only` are the only ledger-eligible forms).
327
+ */
328
+ captureMint(form: MintForm): MintStamp;
329
+ /**
330
+ * Record a confined profile this host's provider just minted. Only
331
+ * meaningful under the shared Registry (`admissionProof` set; without it
332
+ * `permitsProfile` never refuses): a live admission binds the profile to
333
+ * the operation and it dies with it; an expired admission never bequeaths
334
+ * one — a mint that outlived its operation is dropped, not demoted into
335
+ * the ledger; only with NO admission frame at all does the profile join
336
+ * the publication-keyed ledger stamped with `mint`, and only when the
337
+ * mint's form is primary-bounded — an `ambient` mint that arrives without
338
+ * a frame is dropped the same way.
339
+ * @param argv - the confined argv just minted.
340
+ * @param mint - the {@link captureMint} stamp taken at the mint's
341
+ * synchronous entry.
342
+ */
343
+ rememberProfile(argv: readonly string[], mint: MintStamp): void;
344
+ /**
345
+ * Whether a confined argv may launch now: a profile minted inside this live
346
+ * admission, or — for a `confine`→`spawn` composition with no wrapped entry
347
+ * to admit it — a profile this host minted while its authorization
348
+ * publication is still current. Cached argv, argv minted under a superseded
349
+ * publication, and argv minted elsewhere stay refused.
350
+ */
268
351
  permitsProfile(argv: readonly string[]): boolean;
352
+ /**
353
+ * The strict form: only profiles minted inside this live admission. The OPS
354
+ * execution domain owns its profile exclusively and never honors the
355
+ * unadmitted mint ledger.
356
+ */
357
+ permitsOperationProfile(argv: readonly string[]): boolean;
358
+ private permitsMintedProfile;
269
359
  get admissionRequired(): boolean;
270
360
  admissionPathsValid(): boolean;
271
361
  hasAdmission(): boolean;
@@ -275,6 +365,7 @@ export declare class MultiRootScopeService extends Service {
275
365
  resolve(policy: SandboxExecutionPolicy): FilesystemScope;
276
366
  /** Read-only pending-dispatch projection. Does not mint an ALS execution grant. */
277
367
  previewForTool(policy: SandboxExecutionPolicy, toolName: string): FilesystemScope;
368
+ withToolCap<T>(name: string, fn: () => T): T;
278
369
  /** Execution and review use exactly the same parent-narrowing rule. */
279
370
  private capForTool;
280
371
  private project;
@@ -324,7 +415,8 @@ export declare class MultiRootScopeService extends Service {
324
415
  * M1 uses this from tests and smoke scripts only; M3 feeds it from plugin
325
416
  * storage. It is a plain setter so the transition swaps the data source
326
417
  * without touching any provider.
327
- * @param primaryRoot - canonical primary root to register roots under.
418
+ * @param primaryRoot - primary root the roots belong to; the Registry key
419
+ * is its upstream spelling via {@link primaryKey}, never re-resolved.
328
420
  * @param roots - the additional roots, in display order.
329
421
  */
330
422
  setAdditionalRoots(primaryRoot: string, roots: readonly AdditionalWorkspaceRoot[]): void;
@@ -1,7 +1,7 @@
1
- import { t as wrapMethod } from "./method-wrapper-CFY3p3j2.js";
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";
1
+ import { t as wrapMethod } from "./method-wrapper-D4oUCsEF.js";
2
+ import { l as withRegistryRead } from "./registry-operation-CIlmt6_c.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) {
@@ -4,7 +4,7 @@ import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
4
4
  async function routeWorkspacePath(ctx, primaryRoot, path, signal, cwd = primaryRoot) {
5
5
  signal?.throwIfAborted();
6
6
  const primary = canonicalPath(primaryRoot);
7
- const roots = [primary, ...ctx.multiRootScope.scopeOf(primary)];
7
+ const roots = [primary, ...ctx.multiRootScope.scopeOf(primaryRoot)];
8
8
  const target = await ctx.fs.resolve(path, {
9
9
  cwd,
10
10
  ...signal === void 0 ? {} : { signal }
@@ -12,11 +12,11 @@ async function routeWorkspacePath(ctx, primaryRoot, path, signal, cwd = primaryR
12
12
  for (const root of roots) {
13
13
  const rootTarget = await ctx.fs.resolve(root, signal === void 0 ? {} : { signal });
14
14
  if (!ctx.fs.contains(rootTarget, target)) continue;
15
- if (root !== primary && !ctx.multiRootScope.scopeOf(primary).includes(root)) return void 0;
15
+ if (root !== primary && !ctx.multiRootScope.scopeOf(primaryRoot).includes(root)) return void 0;
16
16
  signal?.throwIfAborted();
17
17
  return {
18
- primaryRoot: primary,
19
- workspaceRoot: root,
18
+ primaryRoot,
19
+ workspaceRoot: root === primary ? primaryRoot : root,
20
20
  target,
21
21
  absolutePath: ctx.fs.processPath(target)
22
22
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dsh-electron/dsh-plugin-multi-root-workspace",
3
3
  "description": "Multi-root Workspace for DeepSeek Harness: adds N additional roots to the workspace sandbox scope as an out-of-tree bundle",
4
- "version": "0.1.14",
4
+ "version": "0.1.16",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -75,6 +75,10 @@
75
75
  "./skills": {
76
76
  "types": "./lib/types/skills.d.ts",
77
77
  "default": "./lib/skills.js"
78
+ },
79
+ "./ops-compatibility": {
80
+ "types": "./lib/types/ops-compatibility.d.ts",
81
+ "default": "./lib/ops-compatibility.js"
78
82
  }
79
83
  },
80
84
  "files": [