@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +51 -1
- package/CHANGELOG.md +51 -1
- package/README.en.md +155 -92
- package/README.md +156 -91
- package/cordis.patch.yml +69 -4
- package/lib/client.js +2678 -276
- package/lib/client.js.map +1 -1
- package/lib/command-CESuwlVt.js +1306 -0
- package/lib/command.js +2 -2
- package/lib/compat-DR2LvmYZ.js +600 -0
- package/lib/compat.js +1 -1
- package/lib/contract-DZ4zczDT.js +238 -0
- package/lib/{fs-D_Sw3o5z.js → fs-Nlp2AiRX.js} +56 -24
- package/lib/fs.js +2 -1
- package/lib/index.js +10 -8
- package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
- package/lib/instructions.js +1 -1
- package/lib/lsp.js +7 -2
- package/lib/method-wrapper-CFY3p3j2.js +26 -0
- package/lib/process-fence.js +280 -0
- package/lib/providers.js +87 -0
- package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
- package/lib/registry.js +2 -2
- package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
- package/lib/sandbox-CVAcT_gy.js +743 -0
- package/lib/sandbox.js +2 -1
- package/lib/scope.js +229 -20
- package/lib/types/client/RootSwitcher.d.ts +63 -0
- package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
- package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
- package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
- package/lib/types/client/files-tree/adapter.d.ts +94 -0
- package/lib/types/client/files-tree/directory-node.d.ts +62 -0
- package/lib/types/client/files-tree/face.d.ts +107 -0
- package/lib/types/client/files-tree/locales.d.ts +10 -0
- package/lib/types/client/files-tree/primitives.d.ts +33 -0
- package/lib/types/client/files-tree/store.d.ts +81 -0
- package/lib/types/client/locales.d.ts +56 -10
- package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
- package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
- package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
- package/lib/types/client/open-in-app/adapter.d.ts +46 -0
- package/lib/types/client/open-in-app/applications.d.ts +4 -0
- package/lib/types/client/open-in-app/controller.d.ts +53 -0
- package/lib/types/client/open-in-app/locales.d.ts +10 -0
- package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
- package/lib/types/client/open-in-app/open-path.d.ts +13 -0
- package/lib/types/client/panel-client.d.ts +2 -1
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/command.d.ts +16 -3
- package/lib/types/compat/client-icons.d.ts +18 -0
- package/lib/types/compat/dsh-version.d.ts +57 -9
- package/lib/types/compat/open-in-app-routes.d.ts +38 -0
- package/lib/types/compat/process-entrypoints.d.ts +31 -0
- package/lib/types/compat/process-result.d.ts +14 -0
- package/lib/types/compat/pty-marker.d.ts +83 -0
- package/lib/types/compat/sandbox-confine.d.ts +2 -0
- package/lib/types/compat/storage-zod.d.ts +65 -0
- package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
- package/lib/types/compat/windows-native.d.ts +68 -0
- package/lib/types/compat/windows-path-guard.d.ts +56 -0
- package/lib/types/compat/workspace-files.d.ts +2 -0
- package/lib/types/compat.d.ts +15 -5
- package/lib/types/contract.d.ts +31 -19
- package/lib/types/dialects.d.ts +99 -3
- package/lib/types/fs.d.ts +32 -10
- package/lib/types/index.d.ts +8 -3
- package/lib/types/process-fence.d.ts +54 -0
- package/lib/types/profile-isolation.d.ts +178 -0
- package/lib/types/providers.d.ts +70 -0
- package/lib/types/registry.d.ts +132 -31
- package/lib/types/roots.d.ts +100 -13
- package/lib/types/sandbox.d.ts +59 -3
- package/lib/types/scope.d.ts +200 -14
- package/lib/types/windows-broker.d.ts +17 -0
- package/lib/types/windows-deny.d.ts +1 -0
- package/lib/types/windows-profile.d.ts +10 -13
- package/lib/types/windows-runner-contract.d.ts +97 -0
- package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
- package/lib/windows-runner.js +1256 -13
- package/lib/workspace-files.js +138 -3
- package/lib/workspace-routing-YPxaQ4V3.js +26 -0
- package/package.json +84 -62
- package/scripts/windows-broker/install.ps1 +89 -0
- package/scripts/windows-broker/rights.cs +58 -0
- package/scripts/windows-broker/service.cs +323 -0
- package/scripts/windows-broker/verify-install.ps1 +80 -0
- package/lib/command-CTdNvwt1.js +0 -791
- package/lib/compat-B6ZqbQBy.js +0 -295
- package/lib/method-wrapper-Zc-JlDP3.js +0 -51
- package/lib/sandbox-BRXl_FpF.js +0 -132
- package/lib/types/client/RootFileTree.d.ts +0 -12
- package/lib/types/panel-files.d.ts +0 -3
- package/lib/windows-profile-CiFsFYIV.js +0 -216
- package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile pnpm import isolation — the host-side maintenance behind
|
|
3
|
+
* `/workspace-folders isolate-profiles` (branch review Phase C2, finding R4).
|
|
4
|
+
*
|
|
5
|
+
* Why it exists: a Windows deny ACE applies to the file OBJECT, not to one
|
|
6
|
+
* hardlink name. The pnpm profile defaults upstream writes (`initProfile` in
|
|
7
|
+
* dsh-app-boot) never set `packageImportMethod`, so `dsh plugin add` imports
|
|
8
|
+
* the store into `<home>/profiles/<name>/node_modules` BY HARDLINK. Denying
|
|
9
|
+
* `$DSH_HOME` then rewrites security descriptors shared with every other name
|
|
10
|
+
* of the same objects — including the global pnpm store and other projects
|
|
11
|
+
* linking it. The C1 preflight therefore refuses such a home before any ACL
|
|
12
|
+
* write; this module is the remediation it points at: it detects the confirmed
|
|
13
|
+
* profiles, writes `packageImportMethod: copy` conservatively, and scans the
|
|
14
|
+
* whole home for the multi-link files that still need a reinstall.
|
|
15
|
+
*
|
|
16
|
+
* Boundaries of the subset it touches:
|
|
17
|
+
* - profile identification is conservative — only a directory under
|
|
18
|
+
* `profiles/` whose `package.json` parses to an object carrying
|
|
19
|
+
* `dsh.profile` as an object counts; everything else is enumerated but
|
|
20
|
+
* never written;
|
|
21
|
+
* - the YAML handling is a strict SUBSET scanner, not a parser substitute —
|
|
22
|
+
* the runtime cannot depend on a YAML package, so anything ambiguous is
|
|
23
|
+
* reported as a conflict and refused instead of guessed;
|
|
24
|
+
* - every write is atomic (temp sibling + rename) and never crosses a reparse
|
|
25
|
+
* point;
|
|
26
|
+
* - the whole-home scan never descends a reparse point: with plain `lstat` a
|
|
27
|
+
* symlink/junction reports as a link, and other reparse flavours still fall
|
|
28
|
+
* under the runner's own Win32-attribute preflight, which remains the
|
|
29
|
+
* authoritative gate;
|
|
30
|
+
* - the command sets configuration — it never deletes `node_modules`, never
|
|
31
|
+
* runs pnpm, and never repairs ACLs or ownership written in the past.
|
|
32
|
+
*
|
|
33
|
+
* @module profile-isolation
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Mirrors `PROFILE_PNPM_WORKSPACE` written by `initProfile` in
|
|
37
|
+
* `@deepseek-ai/dsh-app-boot`; that constant is not exported, so the template
|
|
38
|
+
* is duplicated here and must stay in sync with upstream.
|
|
39
|
+
*/
|
|
40
|
+
export declare const PROFILE_PNPM_WORKSPACE = "packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n";
|
|
41
|
+
/** One accepted top-level `key: value` line, located in the source text. */
|
|
42
|
+
export interface WorkspaceYamlEntry {
|
|
43
|
+
/** The unquoted scalar when the value is one; `undefined` for block/complex. */
|
|
44
|
+
readonly value: string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* `scalar` — a same-line plain or quoted scalar; `block` — the value is
|
|
47
|
+
* empty, so a nested block (or an explicit comment) follows; `complex` —
|
|
48
|
+
* the value opens with a YAML node property or collection marker
|
|
49
|
+
* (`&`, `*`, `!`, `|`, `>`, `{`, `[`) or is a malformed scalar the subset
|
|
50
|
+
* refuses to interpret.
|
|
51
|
+
*/
|
|
52
|
+
readonly kind: 'scalar' | 'block' | 'complex';
|
|
53
|
+
/** 1-based line number the key sits on. */
|
|
54
|
+
readonly line: number;
|
|
55
|
+
/** UTF-16 code-unit offset where the value token begins. */
|
|
56
|
+
readonly valueStart: number;
|
|
57
|
+
/** UTF-16 code-unit offset just past the value token (before ` #comment` or EOL). */
|
|
58
|
+
readonly valueEnd: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read the top-level `key: value` lines of a `pnpm-workspace.yaml` text.
|
|
62
|
+
*
|
|
63
|
+
* This is a strict subset scanner, deliberately NOT a YAML parser: it accepts
|
|
64
|
+
* only top-level `key:` lines (plain or quoted keys) plus indented nested
|
|
65
|
+
* content it never interprets, and throws — naming the file and the 1-based
|
|
66
|
+
* line — on an indented first entry, tabs in indentation, `%` / `---` / `...` directives, duplicate
|
|
67
|
+
* top-level keys, or any other non-indented content line outside the key
|
|
68
|
+
* form. A `#` preceded by whitespace ends a plain scalar; quoted scalars are
|
|
69
|
+
* unquoted for the reported value. `valueStart`/`valueEnd` are the UTF-16
|
|
70
|
+
* offsets of the value token itself, so a caller can replace exactly the
|
|
71
|
+
* scalar while comments and line endings survive untouched.
|
|
72
|
+
*
|
|
73
|
+
* @param text - the yaml text to scan.
|
|
74
|
+
* @param path - the file name used in rejection messages.
|
|
75
|
+
* @returns the top-level entries keyed by their unquoted names.
|
|
76
|
+
*/
|
|
77
|
+
export declare function parseWorkspaceYamlKeys(text: string, path?: string): ReadonlyMap<string, WorkspaceYamlEntry>;
|
|
78
|
+
/** What one confirmed profile's `pnpm-workspace.yaml` says about imports. */
|
|
79
|
+
export interface ProfileYamlReport {
|
|
80
|
+
/**
|
|
81
|
+
* `absent` — `packageImportMethod` is not configured (the file may still
|
|
82
|
+
* exist); `copy` — it is a same-line scalar `copy`; `conflict` — it exists
|
|
83
|
+
* but is not a plain `copy` scalar, or the yaml exists but is a reparse
|
|
84
|
+
* point / non-file; `unreadable` — the file cannot be read or the subset
|
|
85
|
+
* scanner rejected it.
|
|
86
|
+
*/
|
|
87
|
+
readonly state: 'absent' | 'copy' | 'conflict' | 'unreadable';
|
|
88
|
+
/** Human-readable reason for `conflict`/`unreadable`. */
|
|
89
|
+
readonly detail?: string;
|
|
90
|
+
}
|
|
91
|
+
/** One confirmed profile and its yaml verdict. */
|
|
92
|
+
export interface ProfileReport {
|
|
93
|
+
/** The profile directory name under `profiles/`. */
|
|
94
|
+
readonly name: string;
|
|
95
|
+
/** The profile's absolute path under the canonical home. */
|
|
96
|
+
readonly path: string;
|
|
97
|
+
/** The `packageImportMethod` verdict for its workspace yaml. */
|
|
98
|
+
readonly yaml: ProfileYamlReport;
|
|
99
|
+
}
|
|
100
|
+
/** The read-only whole-home scan accompanying every report. */
|
|
101
|
+
export interface HomeScan {
|
|
102
|
+
/** Directories successfully listed (including the home itself). */
|
|
103
|
+
readonly directories: number;
|
|
104
|
+
/** Non-directory entries inspected — plain files and reparse leaves alike. */
|
|
105
|
+
readonly files: number;
|
|
106
|
+
/** Plain files carrying more than one name (`st_nlink > 1`). */
|
|
107
|
+
readonly multiLinkTotal: number;
|
|
108
|
+
/** Up to {@link SAMPLE_CAP} absolute paths of multi-link files. */
|
|
109
|
+
readonly multiLinkPaths: readonly string[];
|
|
110
|
+
/** Entries `lstat`/`readdir` failed on (first {@link SAMPLE_CAP}). */
|
|
111
|
+
readonly uninspectable: readonly {
|
|
112
|
+
path: string;
|
|
113
|
+
reason: string;
|
|
114
|
+
}[];
|
|
115
|
+
/** Total uninspectable entries — may exceed the retained paths. */
|
|
116
|
+
readonly uninspectableTotal: number;
|
|
117
|
+
}
|
|
118
|
+
/** The read-only verdict for one canonical home. */
|
|
119
|
+
export interface IsolateReport {
|
|
120
|
+
/** The canonical DSH home the report covers. */
|
|
121
|
+
readonly home: string;
|
|
122
|
+
/** Confirmed profiles only; non-profile entries are listed separately. */
|
|
123
|
+
readonly profiles: readonly ProfileReport[];
|
|
124
|
+
/** `profiles/` entries that are not confirmed profiles — never written. */
|
|
125
|
+
readonly nonProfileEntries: readonly string[];
|
|
126
|
+
/** The whole-home link/uninspectable counts. */
|
|
127
|
+
readonly scan: HomeScan;
|
|
128
|
+
/** True whenever anything could not be inspected — never a clean result. */
|
|
129
|
+
readonly incomplete: boolean;
|
|
130
|
+
}
|
|
131
|
+
/** The per-profile outcome of one `--apply` run. */
|
|
132
|
+
export interface ProfileAction {
|
|
133
|
+
readonly name: string;
|
|
134
|
+
/**
|
|
135
|
+
* `written` — the yaml was created or gained the key; `replaced` — a
|
|
136
|
+
* same-line scalar was swapped in place; `noop` — already `copy`;
|
|
137
|
+
* `refused` — a conflict/unreadable file the command will not guess at.
|
|
138
|
+
*/
|
|
139
|
+
readonly action: 'written' | 'replaced' | 'noop' | 'refused';
|
|
140
|
+
readonly detail?: string;
|
|
141
|
+
}
|
|
142
|
+
/** The report of one `--apply` run: the read verdict plus the actions. */
|
|
143
|
+
export interface ApplyReport extends IsolateReport {
|
|
144
|
+
readonly actions: readonly ProfileAction[];
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read-only inspection of a DSH home: confirmed profiles under `profiles/`,
|
|
148
|
+
* each one's `packageImportMethod` verdict, and a whole-home `lstat` scan
|
|
149
|
+
* counting directories, files, multi-link files, and uninspectable entries.
|
|
150
|
+
* Nothing is written. The report is `incomplete` — never clean — whenever any
|
|
151
|
+
* entry could not be inspected.
|
|
152
|
+
* @param home - the DSH home to inspect (canonicalized).
|
|
153
|
+
* @returns the isolation report.
|
|
154
|
+
*/
|
|
155
|
+
export declare function isolateReport(home: string): IsolateReport;
|
|
156
|
+
/**
|
|
157
|
+
* Write `packageImportMethod: copy` into every fixable confirmed profile.
|
|
158
|
+
* A missing file receives the upstream profile template plus the copy line;
|
|
159
|
+
* a missing key is appended; a same-line non-`copy` scalar is replaced by
|
|
160
|
+
* byte range so comments and CRLF survive; a block/complex/duplicate-key or
|
|
161
|
+
* unreadable file is refused untouched. Every write lands atomically through
|
|
162
|
+
* a sibling temp file + rename, and the command never descends or writes
|
|
163
|
+
* through a reparse point.
|
|
164
|
+
* @param home - the DSH home to migrate (canonicalized).
|
|
165
|
+
* @returns the read verdict plus one action per confirmed profile.
|
|
166
|
+
*/
|
|
167
|
+
export declare function applyIsolateConfig(home: string): ApplyReport;
|
|
168
|
+
/**
|
|
169
|
+
* The remediation steps the user must run by hand — this command writes
|
|
170
|
+
* configuration only. For every confirmed profile whose dependencies still
|
|
171
|
+
* need re-installing as copies (config changed under it, or links were seen
|
|
172
|
+
* inside it), the steps say: quit the hosts using it, remove its
|
|
173
|
+
* `node_modules` yourself, reinstall from its lockfile. Refused profiles get
|
|
174
|
+
* a manual-resolution line instead of automated steps.
|
|
175
|
+
* @param report - the {@link ApplyReport} (or a check report) to explain.
|
|
176
|
+
* @returns the guidance lines, in English.
|
|
177
|
+
*/
|
|
178
|
+
export declare function remediationSteps(report: IsolateReport): string[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MultiRootProviders`: the fail-loud guard that proves the services mounted as
|
|
3
|
+
* `ctx.fs` and `ctx.sandbox` are THIS bundle's providers — the only ones whose
|
|
4
|
+
* widening the registry, the instruction row, LSP routing and workspace-files
|
|
5
|
+
* are allowed to attach to.
|
|
6
|
+
*
|
|
7
|
+
* Why a brand and not `instanceof`: the plugin may be installed twice (a
|
|
8
|
+
* duplicated install forks the class identity), and cordis private fields are
|
|
9
|
+
* not a public contract. A well-known symbol survives both. The brand is
|
|
10
|
+
* non-enumerable so it never leaks into spreads, JSON, or surface payloads.
|
|
11
|
+
*
|
|
12
|
+
* Deterministic outcome under a provider conflict: cordis refuses a second
|
|
13
|
+
* `ctx.provide` of an already-registered key, so whoever mounts the colliding
|
|
14
|
+
* provider second loses loudly — but a conflicting plugin can also DISABLE this
|
|
15
|
+
* bundle's rows (the same mechanism this bundle uses on upstream), leaving a
|
|
16
|
+
* foreign `ctx.fs`/`ctx.sandbox` in place. In that case this service's
|
|
17
|
+
* `[Service.init]` logs which service is foreign and throws: the row never
|
|
18
|
+
* activates, so every row injecting `multiRootProviders` — the registry above
|
|
19
|
+
* all — never starts, the scope stays empty, and nothing is ever granted,
|
|
20
|
+
* regardless of patch order. Cordis re-evaluates an injecting fiber when an
|
|
21
|
+
* injected service is unprovided and re-provided, so a LATE replacement re-runs
|
|
22
|
+
* the same check.
|
|
23
|
+
*
|
|
24
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/providers
|
|
25
|
+
*/
|
|
26
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
27
|
+
/**
|
|
28
|
+
* The well-known brand key both providers carry. `Symbol.for` makes it
|
|
29
|
+
* identical across duplicated installs of this package.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MULTI_ROOT_PROVIDER_BRAND: unique symbol;
|
|
32
|
+
/** The two service keys this bundle takes over. */
|
|
33
|
+
export type ProviderKind = 'fs' | 'sandbox';
|
|
34
|
+
/**
|
|
35
|
+
* Stamp the multi-root provider brand on a service value. Non-enumerable:
|
|
36
|
+
* the mark must not travel into structural copies.
|
|
37
|
+
* @param value - the service instance being mounted.
|
|
38
|
+
* @param kind - which provider key it claims.
|
|
39
|
+
* @returns the same value, branded.
|
|
40
|
+
*/
|
|
41
|
+
export declare function brandMultiRootProvider<T>(value: T, kind: ProviderKind): T;
|
|
42
|
+
/**
|
|
43
|
+
* Whether `value` is this bundle's provider of `kind`. Structural on purpose:
|
|
44
|
+
* no `instanceof`, so a duplicated install still recognizes the brand.
|
|
45
|
+
* @param value - the service mounted under the key.
|
|
46
|
+
* @param kind - the provider key being checked.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isMultiRootProvider(value: unknown, kind: ProviderKind): boolean;
|
|
49
|
+
declare module '@deepseek-ai/cordis' {
|
|
50
|
+
interface Context {
|
|
51
|
+
multiRootProviders: MultiRootProviders;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The conflict gate: `ctx.multiRootProviders` exists only while `ctx.fs` and
|
|
56
|
+
* `ctx.sandbox` are this bundle's branded providers. Every row that widens an
|
|
57
|
+
* authority surface injects it, so a foreign provider composition loses the
|
|
58
|
+
* whole gated set instead of silently widening an unverified one.
|
|
59
|
+
*/
|
|
60
|
+
export declare class MultiRootProviders extends Service {
|
|
61
|
+
static provide: string;
|
|
62
|
+
static inject: string[];
|
|
63
|
+
/**
|
|
64
|
+
* Verify the provider identities once per activation. A foreign service is
|
|
65
|
+
* named in the log and the throw leaves this row — and every dependent —
|
|
66
|
+
* unstarted: fail loud, never a widened grant.
|
|
67
|
+
*/
|
|
68
|
+
protected [Service.init](): void;
|
|
69
|
+
}
|
|
70
|
+
export default MultiRootProviders;
|
package/lib/types/registry.d.ts
CHANGED
|
@@ -37,14 +37,15 @@
|
|
|
37
37
|
* memory-authoritative after open, so two DSH processes sharing a storage
|
|
38
38
|
* root cannot each keep a live domain snapshot. A store-wide kernel lease
|
|
39
39
|
* (ADR-0007) elects one authority; a contended process publishes an empty
|
|
40
|
-
* scope
|
|
40
|
+
* scope marked `authority: 'unavailable'` — under which Full-access is
|
|
41
|
+
* limited to primary-root-only writes — rejects mutations, and retries the lease
|
|
42
|
+
* from {@link MultiRootRegistry.refresh}.
|
|
41
43
|
*
|
|
42
44
|
* @module @dsh-electron/dsh-plugin-multi-root-workspace/registry
|
|
43
45
|
*/
|
|
44
46
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
45
47
|
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox';
|
|
46
|
-
import {
|
|
47
|
-
import { type AdditionalRootId, type RegisteredRoot, type RootRef, type RootStatus } from './roots.ts';
|
|
48
|
+
import { type AdditionalRootId, type RegisteredRoot, type RootAccess, type RootRef, type RootStatus } from './roots.ts';
|
|
48
49
|
/** The domain name; also the store file stem (`$DSH_HOME/storages/<name>.json`). */
|
|
49
50
|
export declare const DOMAIN_NAME = "multi_root_workspace";
|
|
50
51
|
/** The single table name inside the domain. */
|
|
@@ -56,10 +57,20 @@ export declare const MAX_ALIAS_LENGTH = 120;
|
|
|
56
57
|
* locale; the panel also shows this string verbatim via `RootsView.unavailable`.
|
|
57
58
|
*/
|
|
58
59
|
export declare const REGISTRY_CONTENDED_MESSAGE = "the root registry is owned by another DSH process; additional roots are not granted here. Close the other process, then retry.";
|
|
60
|
+
/**
|
|
61
|
+
* Default cap on stored additional-root records per primary root. The bench
|
|
62
|
+
* evidence for this default lives in `docs/reference/bench-root-scale-*.json`
|
|
63
|
+
* (`pnpm bench:roots`): classification and argv widening stay flat far beyond
|
|
64
|
+
* it, so the cap exists to bound the operator-facing list and the widened
|
|
65
|
+
* profile size, not to protect the classifier.
|
|
66
|
+
*/
|
|
67
|
+
export declare const DEFAULT_MAX_ADDITIONAL_ROOTS = 16;
|
|
68
|
+
/** Hard ceiling for `maxAdditionalRoots`: values above it are config-invalid. */
|
|
69
|
+
export declare const MAX_ADDITIONAL_ROOTS_CEILING = 64;
|
|
59
70
|
/**
|
|
60
71
|
* Whether this process is the Registry Authority for the store-wide domain
|
|
61
72
|
* medium. Contended and storage-failed are both fail-closed: the scope stays
|
|
62
|
-
* empty and mutations reject.
|
|
73
|
+
* empty, is marked `authority: 'unavailable'`, and mutations reject.
|
|
63
74
|
*/
|
|
64
75
|
export type RegistryAuthorityState = {
|
|
65
76
|
readonly kind: 'active';
|
|
@@ -68,6 +79,9 @@ export type RegistryAuthorityState = {
|
|
|
68
79
|
} | {
|
|
69
80
|
readonly kind: 'storage-failed';
|
|
70
81
|
readonly reason: string;
|
|
82
|
+
} | {
|
|
83
|
+
readonly kind: 'config-invalid';
|
|
84
|
+
readonly reason: string;
|
|
71
85
|
};
|
|
72
86
|
/**
|
|
73
87
|
* Plugin configuration for the registry row. `leasePath` must sit beside the
|
|
@@ -78,6 +92,16 @@ export type RegistryAuthorityState = {
|
|
|
78
92
|
export interface Config {
|
|
79
93
|
/** Absolute path of the store-wide kernel lock file. */
|
|
80
94
|
readonly leasePath?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Cap on STORED additional-root records per primary root (every state
|
|
97
|
+
* counts, including withheld and unusable ones, so the count is stable and
|
|
98
|
+
* independent of the filesystem). Must be an integer in
|
|
99
|
+
* 1..{@link MAX_ADDITIONAL_ROOTS_CEILING}; an invalid value puts the
|
|
100
|
+
* registry in the fail-closed `config-invalid` state instead of throwing.
|
|
101
|
+
* A store that already exceeds the limit keeps every record and grants
|
|
102
|
+
* exactly what classification grants — only new adds are refused.
|
|
103
|
+
*/
|
|
104
|
+
readonly maxAdditionalRoots?: number;
|
|
81
105
|
}
|
|
82
106
|
declare module '@deepseek-ai/cordis' {
|
|
83
107
|
interface Context {
|
|
@@ -85,21 +109,54 @@ declare module '@deepseek-ai/cordis' {
|
|
|
85
109
|
}
|
|
86
110
|
}
|
|
87
111
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* is
|
|
112
|
+
* Stored shape of one root record.
|
|
113
|
+
*
|
|
114
|
+
* The access level is expressed by WHICH grant key is present, never by an
|
|
115
|
+
* `access` field on the same `recordedPath` (ADR-0017):
|
|
116
|
+
*
|
|
117
|
+
* - `recordedPath` means `inherit` — the historical writable grant, which
|
|
118
|
+
* under B9 keeps following the session mode. Upgrading a legacy record to
|
|
119
|
+
* `inherit` changes nothing about its effective authority.
|
|
120
|
+
* - `readOnlyRecordedPath` means `read-only`.
|
|
121
|
+
* - `readWriteRecordedPath` means fixed `read-write` (B9).
|
|
122
|
+
*
|
|
123
|
+
* That split is what makes this single layout downgrade-safe at domain
|
|
124
|
+
* version 1 (the JSON backend checks the version with strict equality, so a
|
|
125
|
+
* bump would break both upgrade and downgrade):
|
|
126
|
+
*
|
|
127
|
+
* - an OLDER plugin version parses this record with a schema that strips
|
|
128
|
+
* unknown keys, so a read-only or fixed read-write record loses its grant
|
|
129
|
+
* field and arrives with NO `recordedPath`; the old classification reports
|
|
130
|
+
* it `invalid` and grants nothing — a downgrade can never escalate either
|
|
131
|
+
* restricted level into a write grant. A fixed read-write record must
|
|
132
|
+
* therefore NEVER also write `recordedPath`: an older plugin would see the
|
|
133
|
+
* legacy field and grant it as plain read-write;
|
|
134
|
+
* - a record with `recordedPath` only is the writable grant it always was,
|
|
135
|
+
* so upgrading changes nothing for legacy records.
|
|
91
136
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
type PersistedRoot = {
|
|
138
|
+
id: string;
|
|
139
|
+
path: string;
|
|
140
|
+
/**
|
|
141
|
+
* Absent only in records written before the field existed (or by hand),
|
|
142
|
+
* and in non-inherit records. Such a record is reported `invalid` rather
|
|
143
|
+
* than granted on a guess — see classifyStoredRoots.
|
|
144
|
+
*/
|
|
145
|
+
recordedPath?: string | undefined;
|
|
146
|
+
readOnlyRecordedPath?: string | undefined;
|
|
147
|
+
readWriteRecordedPath?: string | undefined;
|
|
148
|
+
alias?: string | undefined;
|
|
149
|
+
addedAt: string;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Stored shape of one primary root's records: the whole registration list.
|
|
153
|
+
* The domain's `single` layout stores the entire unit as one document, so
|
|
154
|
+
* keys stay opaque strings — which is exactly what lets the canonical
|
|
155
|
+
* primary root itself be the key.
|
|
156
|
+
*/
|
|
157
|
+
export type PersistedPrimaryRoot = {
|
|
158
|
+
roots: PersistedRoot[];
|
|
159
|
+
};
|
|
103
160
|
/**
|
|
104
161
|
* The domain declaration: version 1, `single` layout, one `roots` table keyed
|
|
105
162
|
* by the canonical primary root. Records that fail validation are backed up and
|
|
@@ -111,15 +168,7 @@ export declare const multiRootDomainSpec: {
|
|
|
111
168
|
version: number;
|
|
112
169
|
invalidRecords: "backup-and-skip";
|
|
113
170
|
tables: {
|
|
114
|
-
roots: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<string,
|
|
115
|
-
roots: {
|
|
116
|
-
id: string;
|
|
117
|
-
path: string;
|
|
118
|
-
addedAt: string;
|
|
119
|
-
recordedPath?: string | undefined;
|
|
120
|
-
alias?: string | undefined;
|
|
121
|
-
}[];
|
|
122
|
-
}>;
|
|
171
|
+
roots: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<string, PersistedPrimaryRoot>;
|
|
123
172
|
};
|
|
124
173
|
};
|
|
125
174
|
/** Registration request for one new additional root. */
|
|
@@ -128,6 +177,12 @@ export interface AddRootInput {
|
|
|
128
177
|
readonly path: string;
|
|
129
178
|
/** Optional display alias. */
|
|
130
179
|
readonly alias?: string;
|
|
180
|
+
/**
|
|
181
|
+
* The write authority to register; defaults to `inherit` (the historical
|
|
182
|
+
* session-following grant). On a REVIVE the existing record keeps its
|
|
183
|
+
* access unless this field is given explicitly.
|
|
184
|
+
*/
|
|
185
|
+
readonly access?: RootAccess;
|
|
131
186
|
}
|
|
132
187
|
/** The registry service: `ctx.multiRootRegistry`. */
|
|
133
188
|
export declare class MultiRootRegistry extends Service {
|
|
@@ -156,9 +211,13 @@ export declare class MultiRootRegistry extends Service {
|
|
|
156
211
|
private loggedContended;
|
|
157
212
|
/** True once the fiber disposer has run: no further acquisition may happen. */
|
|
158
213
|
private disposed;
|
|
214
|
+
/** The validated per-primary-root cap (the default when the row omits it). */
|
|
215
|
+
private readonly maxRoots;
|
|
216
|
+
/** Keys already warned about a historical over-limit store (one warning each). */
|
|
217
|
+
private readonly warnedOverLimit;
|
|
159
218
|
/**
|
|
160
219
|
* @param ctx - the host context; `storageDomain` and `multiRootScope` are injected.
|
|
161
|
-
* @param config - the patch row's `leasePath`.
|
|
220
|
+
* @param config - the patch row's `leasePath` and `maxAdditionalRoots`.
|
|
162
221
|
*/
|
|
163
222
|
constructor(ctx: Context, config?: Config);
|
|
164
223
|
/**
|
|
@@ -185,6 +244,17 @@ export declare class MultiRootRegistry extends Service {
|
|
|
185
244
|
* Tests and diagnostics read this; surfaces use {@link unavailable}.
|
|
186
245
|
*/
|
|
187
246
|
get authority(): RegistryAuthorityState;
|
|
247
|
+
/**
|
|
248
|
+
* The configured cap on stored additional-root records per primary root.
|
|
249
|
+
* Surfaces show it next to the count so "N of LIMIT" is always explainable.
|
|
250
|
+
*/
|
|
251
|
+
get limit(): number;
|
|
252
|
+
/**
|
|
253
|
+
* Whether one primary root's stored record count exceeds {@link limit} — a
|
|
254
|
+
* historical over-limit store, which stays fully classified and granted.
|
|
255
|
+
* @param primaryRoot - the session workspace root (any spelling).
|
|
256
|
+
*/
|
|
257
|
+
overLimit(primaryRoot: string): boolean;
|
|
188
258
|
/**
|
|
189
259
|
* Every registration of one primary root, in registry order, as last
|
|
190
260
|
* classified. A READ of the in-memory state: it never touches the filesystem
|
|
@@ -225,6 +295,29 @@ export declare class MultiRootRegistry extends Service {
|
|
|
225
295
|
* @throws {RootValidationError} when the candidate violates a root rule.
|
|
226
296
|
*/
|
|
227
297
|
add(primaryRoot: string, input: AddRootInput): Promise<readonly RootStatus[]>;
|
|
298
|
+
/**
|
|
299
|
+
* Change one registration's write authority WITHOUT re-recording its path.
|
|
300
|
+
*
|
|
301
|
+
* Re-resolving is not re-authorizing: the grant keeps the canonical
|
|
302
|
+
* directory it was recorded for; only which persisted field carries it
|
|
303
|
+
* changes. The three levels are not a total order — `inherit` is weaker
|
|
304
|
+
* than `read-write` under a read-only session but stronger under
|
|
305
|
+
* Full-access — so "upgrade" cannot be a single direction. The gate is
|
|
306
|
+
* therefore: pinning to `read-only` is always allowed (a withheld root may
|
|
307
|
+
* be restricted while it is withheld), while ANY other target requires the
|
|
308
|
+
* root to be `available` under the FRESH classification — every
|
|
309
|
+
* non-read-only level can widen authority under some session mode, and a
|
|
310
|
+
* root that is not currently granted must not be granted anything by a
|
|
311
|
+
* metadata change.
|
|
312
|
+
* @param primaryRoot - the session workspace root the root belongs to.
|
|
313
|
+
* @param ref - which registered root to change.
|
|
314
|
+
* @param access - the new write authority.
|
|
315
|
+
* @returns the updated status list.
|
|
316
|
+
* @throws {RootValidationError} `not-found` when the reference matches
|
|
317
|
+
* nothing, `root-unavailable` when the target is not `available` and the
|
|
318
|
+
* new level is not `read-only`.
|
|
319
|
+
*/
|
|
320
|
+
setAccess(primaryRoot: string, ref: RootRef, access: RootAccess): Promise<readonly RootStatus[]>;
|
|
228
321
|
/**
|
|
229
322
|
* Remove one registration.
|
|
230
323
|
* @param primaryRoot - the session workspace root the root belongs to.
|
|
@@ -329,13 +422,21 @@ export declare class MultiRootRegistry extends Service {
|
|
|
329
422
|
/**
|
|
330
423
|
* Push one status list into the scope and notify listeners.
|
|
331
424
|
*
|
|
332
|
-
* What is published is the effective GRANT —
|
|
333
|
-
* withheld registration can never reach a provider
|
|
334
|
-
*
|
|
425
|
+
* What is published is the effective GRANT — membership requires
|
|
426
|
+
* `available`, so a withheld registration can never reach a provider as a
|
|
427
|
+
* grant. Withheld entries still publish for their DENY boundary alone;
|
|
428
|
+
* that includes `membership-conflict` records, whose recorded grant is
|
|
429
|
+
* intact and trustworthy even though their membership cannot stand. When
|
|
430
|
+
* the grant and the withheld set are unchanged (the common case for a
|
|
431
|
+
* read-path refresh on a
|
|
335
432
|
* healthy workspace) the scope is left alone and no listener fires: a refresh
|
|
336
433
|
* exists to keep the two in sync, not to churn on every listing — and, since
|
|
337
434
|
* the scope rebuilds its root table on each call, skipping the no-op write is
|
|
338
435
|
* also what keeps the read path cheap.
|
|
436
|
+
*
|
|
437
|
+
* Available roots keep registry order: instruction discovery consumes them
|
|
438
|
+
* under a shared byte budget, so a move must update the live scope immediately.
|
|
439
|
+
* Aliases and no-op moves still leave the signature unchanged.
|
|
339
440
|
*/
|
|
340
441
|
private publish;
|
|
341
442
|
/**
|