@dsh-electron/dsh-plugin-multi-root-workspace 0.1.10 → 0.1.12
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 +25 -1
- package/CHANGELOG.md +25 -1
- package/README.en.md +11 -9
- package/README.md +11 -9
- package/cordis.patch.yml +3 -2
- package/lib/auto-review.js +1 -1
- package/lib/client.js +252 -129
- package/lib/client.js.map +1 -1
- package/lib/{command-BGdz7VBH.js → command-CmLekG1M.js} +3 -2
- package/lib/command.js +1 -1
- package/lib/{compat-D-MljsPV.js → compat-Bg2HFVMX.js} +6 -1
- package/lib/compat.js +1 -1
- package/lib/{sandbox-CXk5h-ct.js → dialects-Dr4rDR-H.js} +6 -264
- package/lib/{fs-BTXySwSG.js → fs-CfzJq2wd.js} +23 -8
- package/lib/fs.js +1 -1
- package/lib/index.js +7 -6
- package/lib/{instructions-MePoRigy.js → instructions-KsUwUFE0.js} +53 -37
- package/lib/instructions.js +1 -1
- package/lib/lsp.js +19 -16
- package/lib/native-win32-2waCRkPI.js +312 -0
- package/lib/process-fence.js +62 -12
- package/lib/protocol-4zdwV7u4.js +641 -0
- package/lib/{registry-KSsmU393.js → registry-YTUOjmVu.js} +368 -25
- package/lib/registry-operation-C2ZV3Jhw.js +116 -0
- package/lib/registry.js +3 -2
- package/lib/{review-context-AH-SXHmj.js → review-context-NDpKeMCc.js} +1 -1
- package/lib/review-context.js +1 -1
- package/lib/sandbox-C-vCJCp3.js +278 -0
- package/lib/sandbox.js +1 -1
- package/lib/scope.js +70 -10
- package/lib/skills.js +48 -29
- package/lib/types/authority/native-win32.d.ts +7 -0
- package/lib/types/authority/native.d.ts +23 -0
- package/lib/types/authority/protocol.d.ts +70 -0
- package/lib/types/client/open-in-app/OpenInAppAction.d.ts +5 -3
- package/lib/types/client/open-in-app/OpenTargetButton.d.ts +16 -10
- package/lib/types/client/open-in-app/adapter.d.ts +5 -1
- package/lib/types/client/open-in-app/controller.d.ts +5 -2
- package/lib/types/client/open-in-app/device.d.ts +11 -0
- package/lib/types/client/open-in-app/locales.d.ts +1 -1
- package/lib/types/client/open-in-app/open-path.d.ts +6 -5
- package/lib/types/client/open-in-app/open-toast.d.ts +39 -0
- package/lib/types/client/open-in-app/outcome.d.ts +23 -0
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/compat/authority-storage.d.ts +6 -0
- package/lib/types/compat/dsh-version.d.ts +4 -4
- package/lib/types/compat/open-in-app-device.d.ts +23 -0
- package/lib/types/compat/process-entrypoints.d.ts +2 -0
- package/lib/types/compat/process-profile.d.ts +3 -0
- package/lib/types/compat/sandbox-confine.d.ts +2 -0
- package/lib/types/fs.d.ts +4 -1
- package/lib/types/instructions.d.ts +0 -47
- package/lib/types/registry-operation.d.ts +31 -0
- package/lib/types/registry.d.ts +47 -6
- package/lib/types/scope.d.ts +12 -0
- package/lib/types/skills/catalog.d.ts +3 -0
- package/lib/types/skills/pool.d.ts +1 -1
- package/lib/workspace-files.js +63 -7
- package/package.json +87 -80
- package/lib/types/client/open-in-app/open-failure-toast.d.ts +0 -11
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
//#region src/registry-operation.ts
|
|
4
|
+
/**
|
|
5
|
+
* Host-local operation context after ordered Registry authority admission.
|
|
6
|
+
* This is Host-only state, never a wire permit or a reusable cached grant.
|
|
7
|
+
*/
|
|
8
|
+
const operations = new AsyncLocalStorage();
|
|
9
|
+
function currentRegistryOperation(proof) {
|
|
10
|
+
const frame = operations.getStore();
|
|
11
|
+
return frame?.active === true && frame.proof === proof ? frame.operation : void 0;
|
|
12
|
+
}
|
|
13
|
+
/** Capture by value: neither later publication nor a caller can mutate admission. */
|
|
14
|
+
function freezeOperationScope(scope) {
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
...scope,
|
|
17
|
+
additionalRoots: Object.freeze([...scope.additionalRoots]),
|
|
18
|
+
roots: Object.freeze(scope.roots.map((root) => Object.freeze({ ...root }))),
|
|
19
|
+
writableRoots: Object.freeze([...scope.writableRoots]),
|
|
20
|
+
deniedRoots: Object.freeze([...scope.deniedRoots])
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Execute outside the authority queue; completion also expires async descendants. */
|
|
24
|
+
async function runRegistryOperation(scope, kind, authorityEpoch, revision, body, proof, mode) {
|
|
25
|
+
let active = true;
|
|
26
|
+
const operation = Object.freeze({
|
|
27
|
+
identity: Object.freeze({
|
|
28
|
+
operationId: randomUUID(),
|
|
29
|
+
kind,
|
|
30
|
+
authorityEpoch,
|
|
31
|
+
revision,
|
|
32
|
+
mode,
|
|
33
|
+
primaryRoot: scope.primaryRoot
|
|
34
|
+
}),
|
|
35
|
+
get scope() {
|
|
36
|
+
if (!active) throw new Error("multi-root registry: operation admission has expired");
|
|
37
|
+
return scope;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
const frame = {
|
|
41
|
+
proof,
|
|
42
|
+
operation,
|
|
43
|
+
active: true,
|
|
44
|
+
profiles: []
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
return await operations.run(frame, () => body(operation));
|
|
48
|
+
} finally {
|
|
49
|
+
active = false;
|
|
50
|
+
frame.active = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Optional Registry seam for public consumers; the legacy composition stays exact. */
|
|
54
|
+
async function withRegistryRead(ctx, primary, kind, body) {
|
|
55
|
+
const registry = ctx.get("multiRootRegistry");
|
|
56
|
+
return registry?.shared ? registry.withOperation({
|
|
57
|
+
workspaceRoot: primary,
|
|
58
|
+
mode: "read-only"
|
|
59
|
+
}, kind, () => body()) : body();
|
|
60
|
+
}
|
|
61
|
+
async function withRegistryAdmission(ctx, policy, kind, body) {
|
|
62
|
+
const registry = ctx.get("multiRootRegistry");
|
|
63
|
+
return registry?.shared ? registry.withOperation(policy, kind, () => body()) : body();
|
|
64
|
+
}
|
|
65
|
+
/** Preserve a legacy synchronous entry while granting only the safe primary-root fallback. */
|
|
66
|
+
function runPrimaryOperation(scope, mode, proof, body) {
|
|
67
|
+
let active = true;
|
|
68
|
+
const frame = {
|
|
69
|
+
proof,
|
|
70
|
+
operation: Object.freeze({
|
|
71
|
+
identity: Object.freeze({
|
|
72
|
+
operationId: randomUUID(),
|
|
73
|
+
kind: "process",
|
|
74
|
+
authorityEpoch: void 0,
|
|
75
|
+
revision: 0,
|
|
76
|
+
mode,
|
|
77
|
+
primaryRoot: scope.primaryRoot
|
|
78
|
+
}),
|
|
79
|
+
get scope() {
|
|
80
|
+
if (!active) throw new Error("operation admission has expired");
|
|
81
|
+
return scope;
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
active: true,
|
|
85
|
+
profiles: []
|
|
86
|
+
};
|
|
87
|
+
const finish = () => {
|
|
88
|
+
active = false;
|
|
89
|
+
frame.active = false;
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
const result = operations.run(frame, body);
|
|
93
|
+
if (typeof result?.then === "function") return Promise.resolve(result).finally(finish);
|
|
94
|
+
finish();
|
|
95
|
+
return result;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
finish();
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Only profiles produced within this exact admission can reach a sync spawn. */
|
|
102
|
+
function rememberOperationProfile(proof, argv, capped) {
|
|
103
|
+
const frame = operations.getStore();
|
|
104
|
+
if (frame?.active !== true || frame.proof !== proof) return;
|
|
105
|
+
if (frame.profiles.length >= 128) throw new Error("too many confined profiles in one operation");
|
|
106
|
+
frame.profiles.push({
|
|
107
|
+
argv: [...argv],
|
|
108
|
+
capped
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function operationPermitsProfile(proof, argv, capped) {
|
|
112
|
+
const frame = operations.getStore();
|
|
113
|
+
return frame?.active === true && frame.proof === proof && frame.profiles.some((profile) => profile.capped === capped && profile.argv.length <= argv.length && profile.argv.every((arg, i) => argv[i] === arg));
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { runPrimaryOperation as a, withRegistryRead as c, rememberOperationProfile as i, freezeOperationScope as n, runRegistryOperation as o, operationPermitsProfile as r, withRegistryAdmission as s, currentRegistryOperation as t };
|
package/lib/registry.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import { a as MultiRootRegistry, c as multiRootDomainSpec, i as MAX_ALIAS_LENGTH, n as DOMAIN_NAME, o as REGISTRY_CONTENDED_MESSAGE, r as MAX_ADDITIONAL_ROOTS_CEILING, s as TABLE_NAME, t as DEFAULT_MAX_ADDITIONAL_ROOTS } from "./registry-
|
|
2
|
-
|
|
1
|
+
import { a as MultiRootRegistry, c as multiRootDomainSpec, i as MAX_ALIAS_LENGTH, n as DOMAIN_NAME, o as REGISTRY_CONTENDED_MESSAGE, r as MAX_ADDITIONAL_ROOTS_CEILING, s as TABLE_NAME, t as DEFAULT_MAX_ADDITIONAL_ROOTS } from "./registry-YTUOjmVu.js";
|
|
2
|
+
import { o as nativeHostAvailability } from "./protocol-4zdwV7u4.js";
|
|
3
|
+
export { DEFAULT_MAX_ADDITIONAL_ROOTS, DOMAIN_NAME, MAX_ADDITIONAL_ROOTS_CEILING, MAX_ALIAS_LENGTH, MultiRootRegistry, MultiRootRegistry as default, REGISTRY_CONTENDED_MESSAGE, TABLE_NAME, multiRootDomainSpec, nativeHostAvailability };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as effectiveDirectory } from "./session-directory-BLOstut1.js";
|
|
2
|
-
import { s as touchedPathOfToolCall } from "./instructions-
|
|
2
|
+
import { s as touchedPathOfToolCall } from "./instructions-KsUwUFE0.js";
|
|
3
3
|
import { l as isCanonicallyUnder } from "./roots-Ca_W1S9n.js";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { resolve } from "node:path";
|
package/lib/review-context.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as MultiRootReviewContext } from "./review-context-
|
|
1
|
+
import { t as MultiRootReviewContext } from "./review-context-NDpKeMCc.js";
|
|
2
2
|
export { MultiRootReviewContext, MultiRootReviewContext as default };
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { a as parseWindowsProfile, i as isWindowsNetworkOrDevicePath, n as assertWindowsDenyCapacity, t as DialectUnrecognizedError } from "./windows-runner-contract-B5R2K3mM.js";
|
|
2
|
+
import { a as splitConfined, c as mapConfined, i as restrictProfileArgs, l as widenConfined, o as widenProfileArgs, r as graftProfileArgs, t as denyProfileArgs } from "./dialects-Dr4rDR-H.js";
|
|
3
|
+
import { brandMultiRootProvider } from "./providers.js";
|
|
4
|
+
import { SandboxUnavailableError, canonicalPath } from "@deepseek-ai/dsh-sandbox";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { LocalSandboxProvider } from "@deepseek-ai/dsh-sandbox-local";
|
|
7
|
+
//#region src/windows-profile.ts
|
|
8
|
+
/** Windows runner argv and capability identity. No Win32 calls at module load. */
|
|
9
|
+
/** Recognize the upstream runner, then select our ambient deny-token path.
|
|
10
|
+
*
|
|
11
|
+
* `protectedHome` is a diagnostics label only: when it names one of the deny
|
|
12
|
+
* roots it is forwarded as `--protected-home` so the runner can word its
|
|
13
|
+
* hardlink/scan remediation for the protected `$DSH_HOME`. It never changes
|
|
14
|
+
* which roots are denied or how.
|
|
15
|
+
*/
|
|
16
|
+
function restrictWindowsProfile(profile, deniedRoots, protectedHome) {
|
|
17
|
+
assertWindowsDenyCapacity(deniedRoots.length);
|
|
18
|
+
if (profile.length < 2 || profile[0] === void 0 || profile[1]?.startsWith("--")) throw new DialectUnrecognizedError("Windows ACL runner invocation is not [node, runner, ...flags]");
|
|
19
|
+
const parsed = parseWindowsProfile(profile.slice(2));
|
|
20
|
+
if (deniedRoots.length === 0) throw new DialectUnrecognizedError("Windows ambient restriction requires at least one deny root");
|
|
21
|
+
for (const root of [parsed.workspace, ...deniedRoots]) if (isWindowsNetworkOrDevicePath(root)) throw new DialectUnrecognizedError(`Windows ACL confinement cannot cover network or device paths: ${root}`);
|
|
22
|
+
const label = protectedHome !== void 0 && deniedRoots.some((root) => root.toLowerCase() === protectedHome.toLowerCase()) ? ["--protected-home", protectedHome] : [];
|
|
23
|
+
return [
|
|
24
|
+
profile[0],
|
|
25
|
+
fileURLToPath(new URL("./windows-runner.js", import.meta.url)),
|
|
26
|
+
"--upstream-runner",
|
|
27
|
+
profile[1],
|
|
28
|
+
"--workspace",
|
|
29
|
+
parsed.workspace,
|
|
30
|
+
"--temp",
|
|
31
|
+
parsed.temp,
|
|
32
|
+
"--mode",
|
|
33
|
+
"danger-full-access",
|
|
34
|
+
...deniedRoots.flatMap((root) => ["--deny-root", root]),
|
|
35
|
+
...label
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
function widenWindowsProfile(profile, roots, deniedRoots, primaryRoot) {
|
|
39
|
+
if (profile.length < 2 || profile[0] === void 0 || profile[1]?.startsWith("--")) throw new DialectUnrecognizedError("Windows ACL runner invocation is not [node, runner, ...flags]");
|
|
40
|
+
const parsed = parseWindowsProfile(profile.slice(2));
|
|
41
|
+
for (const root of [parsed.workspace, ...roots]) if (isWindowsNetworkOrDevicePath(root)) throw new DialectUnrecognizedError(`Windows ACL confinement cannot cover network or device paths: ${root}`);
|
|
42
|
+
if (primaryRoot !== void 0 && canonicalPath(parsed.workspace) !== primaryRoot) throw new DialectUnrecognizedError("Windows ACL profile workspace does not match the policy root");
|
|
43
|
+
{
|
|
44
|
+
const granted = parsed.mode === "workspace-write" ? [
|
|
45
|
+
parsed.workspace,
|
|
46
|
+
parsed.temp,
|
|
47
|
+
...roots
|
|
48
|
+
] : roots;
|
|
49
|
+
const covered = deniedRoots.filter((root) => granted.some((target) => windowsContained(root, target) || windowsContained(target, root)));
|
|
50
|
+
if (covered.length > 0) throw new DialectUnrecognizedError(`Windows ACL cannot deny [${covered.join(", ")}] inside a writable grant — the restricted-token model only grants`);
|
|
51
|
+
}
|
|
52
|
+
if (roots.length === 0) return [...profile];
|
|
53
|
+
return [
|
|
54
|
+
profile[0],
|
|
55
|
+
fileURLToPath(new URL("./windows-runner.js", import.meta.url)),
|
|
56
|
+
"--upstream-runner",
|
|
57
|
+
profile[1],
|
|
58
|
+
...profile.slice(2),
|
|
59
|
+
...roots.flatMap((root) => ["--additional-root", root])
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Containment over Windows SPELLINGS, not the host OS: the profile this
|
|
64
|
+
* function rewrites may be produced for a Windows host while the check runs
|
|
65
|
+
* in tests on POSIX, so the comparison cannot borrow the host's separator or
|
|
66
|
+
* case rules.
|
|
67
|
+
*/
|
|
68
|
+
function windowsContained(candidate, root) {
|
|
69
|
+
const normalize = (path) => path.replaceAll("/", "\\").replace(/\\+$/, "").toLowerCase();
|
|
70
|
+
const target = normalize(candidate);
|
|
71
|
+
const base = normalize(root);
|
|
72
|
+
return target === base || target.startsWith(`${base}\\`);
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/sandbox.ts
|
|
76
|
+
/**
|
|
77
|
+
* Local process-sandbox provider over the multi-root scope. Registers as
|
|
78
|
+
* `ctx.sandbox`; the runner chain, probing, denial dialects, and Windows ACL
|
|
79
|
+
* grant mechanics stay the upstream implementation's.
|
|
80
|
+
*/
|
|
81
|
+
var MultiRootSandboxProvider = class extends LocalSandboxProvider {
|
|
82
|
+
static inject = ["sandboxPolicy", "multiRootScope"];
|
|
83
|
+
constructor(ctx, config) {
|
|
84
|
+
super(ctx, config);
|
|
85
|
+
brandMultiRootProvider(this, "sandbox");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Wrap `argv` so it executes confined under `policy` on this host, granting
|
|
89
|
+
* the scope's additional roots inside the SAME dialect profile.
|
|
90
|
+
*
|
|
91
|
+
* Under `read-only` nothing is added (the mode denies every write, additional
|
|
92
|
+
* roots included), and `danger-full-access` never reaches here at all: the
|
|
93
|
+
* bash executor and the PTY backend both hand their argv straight to the local
|
|
94
|
+
* runtime in that mode. Widening therefore only ever ADDS the scope's
|
|
95
|
+
* additional roots to an upstream `workspace-write` profile.
|
|
96
|
+
*
|
|
97
|
+
* The return shape is the INSTALLED upstream provider's: synchronous on a
|
|
98
|
+
* release with a synchronous `confine`, a promise on a release with an
|
|
99
|
+
* asynchronous one. `widenConfined` preserves whichever it is — see
|
|
100
|
+
* `./compat/sandbox-confine.ts`.
|
|
101
|
+
* @param argv - the exact argv the caller is about to spawn.
|
|
102
|
+
* @param policy - the file-effect policy this execution runs under.
|
|
103
|
+
* @param signal - cancellation, forwarded to releases that accept it.
|
|
104
|
+
* @returns the argv to spawn instead, plus the selected backend's facts.
|
|
105
|
+
*/
|
|
106
|
+
confine(argv, policy, signal) {
|
|
107
|
+
const scope = this.ctx.multiRootScope.resolve(policy);
|
|
108
|
+
const remember = (facts) => {
|
|
109
|
+
this.ctx.multiRootScope.rememberProfile(facts.argv);
|
|
110
|
+
return facts;
|
|
111
|
+
};
|
|
112
|
+
if (policy.mode === "workspace-write" && scope.authority === "unavailable") {
|
|
113
|
+
const prefix = this.clampedExecution(policy, scope.deniedRoots, signal);
|
|
114
|
+
return widenConfined(() => prefix, (facts) => remember({
|
|
115
|
+
...facts,
|
|
116
|
+
argv: [...facts.argv, ...argv]
|
|
117
|
+
}), argv, policy, signal);
|
|
118
|
+
}
|
|
119
|
+
const base = super.confine;
|
|
120
|
+
return widenConfined(base.bind(this), (confined) => remember(this.widenForScope(confined, argv, policy, scope)), argv, policy, signal);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Graft the scope's effective grants and denies onto one upstream `confine`
|
|
124
|
+
* result.
|
|
125
|
+
*
|
|
126
|
+
* Pure with respect to the upstream call: it only ever reads the scope and
|
|
127
|
+
* rewrites `argv`. `enforcement`, `denialSignatures` and `runnerFailureRules`
|
|
128
|
+
* are passed through untouched, because the bash executor derives its denial
|
|
129
|
+
* and enforcement reporting from them.
|
|
130
|
+
*
|
|
131
|
+
* Mode behavior (B9, R2): `workspace-write` widens the grant with the
|
|
132
|
+
* effective writable roots AND denies any restricted root the upstream
|
|
133
|
+
* grants could reach (a `read-only` root registered under the platform
|
|
134
|
+
* temp). `read-only` grafts the fixed `read-write` roots back onto the
|
|
135
|
+
* deny-all baseline — the three-state matrix's standout cell — and then
|
|
136
|
+
* applies the SAME deny pass: a grafted writable parent must not silently
|
|
137
|
+
* swallow a denied child (Seatbelt appends the deny after the allow, bwrap
|
|
138
|
+
* adds a mandatory `--ro-bind`, Landlock / windows-acl refuse the
|
|
139
|
+
* unrepresentable subtraction). `danger-full-access` never reaches
|
|
140
|
+
* here (consumers bypass `confine`); its deny semantics live in
|
|
141
|
+
* {@link restrictedArgvPrefix}.
|
|
142
|
+
* @param confined - the upstream result for this call.
|
|
143
|
+
* @param argv - the caller's argv, needed to re-split the wrapped form.
|
|
144
|
+
* @param policy - the per-call policy.
|
|
145
|
+
* @param scope - the authorization snapshot taken at dispatch.
|
|
146
|
+
* @returns the upstream result element for element, or one with a rewritten argv.
|
|
147
|
+
*/
|
|
148
|
+
widenForScope(confined, argv, policy, scope) {
|
|
149
|
+
if (scope.authority === "unavailable" || policy.mode !== "workspace-write" && policy.mode !== "read-only") return confined;
|
|
150
|
+
const writable = scope.writableRoots;
|
|
151
|
+
const denied = scope.deniedRoots;
|
|
152
|
+
if (writable.length === 0 && (denied.length === 0 || policy.mode === "read-only")) return confined;
|
|
153
|
+
try {
|
|
154
|
+
const shape = splitConfined(confined.argv, argv);
|
|
155
|
+
if (shape.dialect === "windows-acl") return {
|
|
156
|
+
...confined,
|
|
157
|
+
argv: [
|
|
158
|
+
...widenWindowsProfile(shape.profileArgs, writable, denied, scope.primaryRoot),
|
|
159
|
+
"--",
|
|
160
|
+
...argv
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
const widened = policy.mode === "workspace-write" ? widenProfileArgs(shape.dialect, shape.profileArgs, policy, writable) : graftProfileArgs(shape.dialect, shape.profileArgs, writable);
|
|
164
|
+
const profileArgs = denyProfileArgs(shape.dialect, widened, denied);
|
|
165
|
+
return {
|
|
166
|
+
...confined,
|
|
167
|
+
argv: [
|
|
168
|
+
...profileArgs,
|
|
169
|
+
"--",
|
|
170
|
+
...argv
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (!(error instanceof DialectUnrecognizedError)) throw error;
|
|
175
|
+
throw new SandboxUnavailableError(policy.mode, `multi-root workspace: cannot apply the additional workspace roots (writable: [${writable.join(", ")}], denied: [${denied.join(", ")}]) under the primary root ${scope.primaryRoot} — ${error.message}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Build the runner-prefix that confines an ambient-writable execution to
|
|
180
|
+
* everything EXCEPT `deniedRoots` — the `danger-full-access` + explicit
|
|
181
|
+
* `read-only` cell, which the shell, PTY and PTC consumers never route
|
|
182
|
+
* through `confine()` at all.
|
|
183
|
+
*
|
|
184
|
+
* The restricted profile does not depend on the callee's argv, so the prefix
|
|
185
|
+
* is computed once per call here and the synchronous `ctx.subprocess.spawn`
|
|
186
|
+
* seam only has to prepend it. The upstream profile builders stay private:
|
|
187
|
+
* this probes `confine` under `read-only` for the runner's argument skeleton
|
|
188
|
+
* and rewrites the file-effect part per dialect. Windows selects the
|
|
189
|
+
* standalone ambient deny-token runner. Landlock cannot express
|
|
190
|
+
* "everything except", so this combination selects bubblewrap; unusable
|
|
191
|
+
* runners fail without spawning an unconfined command.
|
|
192
|
+
*
|
|
193
|
+
* The return preserves the installed `confine` shape: a synchronous result
|
|
194
|
+
* on a synchronous release, a promise on an asynchronous one.
|
|
195
|
+
* @param policy - the caller's policy (mode must be `danger-full-access`).
|
|
196
|
+
* @param deniedRoots - canonical roots this execution must not write.
|
|
197
|
+
* @param signal - cancellation, forwarded to releases that accept it.
|
|
198
|
+
* @param protectedHome - diagnostics label: which deny root is the
|
|
199
|
+
* protected `$DSH_HOME`. Forwarded to the Windows runner only when it is
|
|
200
|
+
* a member of `deniedRoots`; it never changes authorization.
|
|
201
|
+
* @returns the argv prefix, ending at the `--` separator.
|
|
202
|
+
*/
|
|
203
|
+
restrictedArgvPrefix(policy, deniedRoots, signal, protectedHome) {
|
|
204
|
+
const result = this.restrictedExecution(policy, deniedRoots, signal, protectedHome);
|
|
205
|
+
return mapConfined(result, (confined) => confined.argv);
|
|
206
|
+
}
|
|
207
|
+
/** Full-access restriction and the actual selected backend's diagnostics. */
|
|
208
|
+
restrictedExecution(policy, deniedRoots, signal, protectedHome) {
|
|
209
|
+
const probePolicy = {
|
|
210
|
+
mode: "read-only",
|
|
211
|
+
workspaceRoot: policy.workspaceRoot,
|
|
212
|
+
...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
|
|
213
|
+
};
|
|
214
|
+
const probe = super.confine.call(this, ["true"], probePolicy, signal);
|
|
215
|
+
const restrict = (confined) => {
|
|
216
|
+
try {
|
|
217
|
+
const shape = splitConfined(confined.argv, ["true"]);
|
|
218
|
+
const argv = [...shape.dialect === "windows-acl" ? restrictWindowsProfile(shape.profileArgs, deniedRoots, protectedHome) : restrictProfileArgs(shape.dialect, shape.profileArgs, deniedRoots), "--"];
|
|
219
|
+
return shape.dialect === "landlock" ? {
|
|
220
|
+
argv,
|
|
221
|
+
enforcement: "full",
|
|
222
|
+
denialSignatures: ["read-only file system"],
|
|
223
|
+
runnerFailureRules: [{ fatalSignatures: ["bwrap: "] }]
|
|
224
|
+
} : {
|
|
225
|
+
...confined,
|
|
226
|
+
argv
|
|
227
|
+
};
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (!(error instanceof DialectUnrecognizedError)) throw error;
|
|
230
|
+
throw new SandboxUnavailableError(policy.mode, `multi-root workspace: cannot confine an ambient execution away from the denied roots [${deniedRoots.join(", ")}] — ${error.message}`);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
return widenConfined(() => probe, (facts) => {
|
|
234
|
+
const result = restrict(facts);
|
|
235
|
+
this.ctx.multiRootScope.rememberProfile(result.argv);
|
|
236
|
+
return result;
|
|
237
|
+
}, ["true"], probePolicy, signal);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Limit an execution with unknown registry authority to the primary root.
|
|
241
|
+
* Shared temp grants are unsafe: another process may have registered a
|
|
242
|
+
* read-only root beneath them. Harvest the upstream read-only skeleton and
|
|
243
|
+
* graft only the primary root, retaining diagnostics and the sync/async shape.
|
|
244
|
+
* Explicit denies still win; an unrepresentable overlap refuses launch.
|
|
245
|
+
* @param policy - the caller's execution policy.
|
|
246
|
+
* @param deniedRoots - known negative boundaries, including protected home.
|
|
247
|
+
* @param signal - cancellation forwarded to upstream.
|
|
248
|
+
* @returns the confined prefix ending at `--`, without the caller's command.
|
|
249
|
+
*/
|
|
250
|
+
clampedExecution(policy, deniedRoots, signal) {
|
|
251
|
+
const probePolicy = {
|
|
252
|
+
mode: "read-only",
|
|
253
|
+
workspaceRoot: policy.workspaceRoot,
|
|
254
|
+
...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
|
|
255
|
+
};
|
|
256
|
+
const probe = super.confine.call(this, ["true"], probePolicy, signal);
|
|
257
|
+
const clamp = (confined) => {
|
|
258
|
+
try {
|
|
259
|
+
const shape = splitConfined(confined.argv, ["true"]);
|
|
260
|
+
const profileArgs = shape.dialect === "windows-acl" ? widenWindowsProfile(shape.profileArgs, [canonicalPath(policy.workspaceRoot)], deniedRoots, canonicalPath(policy.workspaceRoot)) : denyProfileArgs(shape.dialect, graftProfileArgs(shape.dialect, shape.profileArgs, [canonicalPath(policy.workspaceRoot)]), deniedRoots);
|
|
261
|
+
return {
|
|
262
|
+
...confined,
|
|
263
|
+
argv: [...profileArgs, "--"]
|
|
264
|
+
};
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (!(error instanceof DialectUnrecognizedError)) throw error;
|
|
267
|
+
throw new SandboxUnavailableError(policy.mode, `multi-root workspace: cannot limit an execution to the primary root while the registry authority is unavailable — ${error.message}`);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
return widenConfined(() => probe, (facts) => {
|
|
271
|
+
const result = clamp(facts);
|
|
272
|
+
this.ctx.multiRootScope.rememberProfile(result.argv);
|
|
273
|
+
return result;
|
|
274
|
+
}, ["true"], probePolicy, signal);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
//#endregion
|
|
278
|
+
export { MultiRootSandboxProvider as t };
|
package/lib/sandbox.js
CHANGED
package/lib/scope.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { i as rememberOperationProfile, r as operationPermitsProfile, t as currentRegistryOperation } from "./registry-operation-C2ZV3Jhw.js";
|
|
1
2
|
import { t as aroundTool } from "./tool-context-CpgIFcCB.js";
|
|
2
3
|
import { r as effectiveRootAccess } from "./contract-CStSEOcK.js";
|
|
3
4
|
import { statSync } from "node:fs";
|
|
@@ -153,6 +154,7 @@ var MultiRootScopeService = class extends Service {
|
|
|
153
154
|
/** Additional roots keyed by canonical primary root; empty until roots are registered. */
|
|
154
155
|
rootsByPrimary = /* @__PURE__ */ new Map();
|
|
155
156
|
listeners = /* @__PURE__ */ new Set();
|
|
157
|
+
admissionProof;
|
|
156
158
|
/**
|
|
157
159
|
* The registry-authority fact the registry row publishes. Defaults to
|
|
158
160
|
* `available`: a composition without a registry row can never have
|
|
@@ -220,7 +222,7 @@ var MultiRootScopeService = class extends Service {
|
|
|
220
222
|
const session = assembly.agent?.session;
|
|
221
223
|
if (session === void 0) return "";
|
|
222
224
|
const policy = scope.sandboxPolicy.resolve({ session });
|
|
223
|
-
const resolved = this.
|
|
225
|
+
const resolved = this.snapshot(policy);
|
|
224
226
|
if (resolved.roots.length === 0 && resolved.authority !== "unavailable" && resolved.protectedHome === void 0) return "";
|
|
225
227
|
return renderWorkspaceRootsContext({
|
|
226
228
|
platform: process.platform,
|
|
@@ -241,9 +243,52 @@ var MultiRootScopeService = class extends Service {
|
|
|
241
243
|
* @param policy - the upstream policy whose workspace root is the primary root.
|
|
242
244
|
* @returns the canonical scope; `additionalRoots` is empty when none are registered.
|
|
243
245
|
*/
|
|
244
|
-
|
|
246
|
+
requireAdmission(proof) {
|
|
247
|
+
this.admissionProof = proof;
|
|
248
|
+
}
|
|
249
|
+
rememberProfile(argv) {
|
|
250
|
+
if (this.admissionProof !== void 0) rememberOperationProfile(this.admissionProof, argv, this.capped());
|
|
251
|
+
}
|
|
252
|
+
permitsProfile(argv) {
|
|
253
|
+
return this.admissionProof === void 0 || operationPermitsProfile(this.admissionProof, argv, this.capped());
|
|
254
|
+
}
|
|
255
|
+
get admissionRequired() {
|
|
256
|
+
return this.admissionProof !== void 0;
|
|
257
|
+
}
|
|
258
|
+
admissionPathsValid() {
|
|
259
|
+
if (this.admissionProof === void 0) return true;
|
|
260
|
+
const operation = currentRegistryOperation(this.admissionProof);
|
|
261
|
+
return operation !== void 0 && operation.scope.additionalRoots.every((root) => {
|
|
262
|
+
try {
|
|
263
|
+
return canonicalPath(root) === root && statSync(root).isDirectory();
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
hasAdmission() {
|
|
270
|
+
return this.admissionProof === void 0 || currentRegistryOperation(this.admissionProof) !== void 0;
|
|
271
|
+
}
|
|
272
|
+
/** Trusted display/review projection, never an execution permit. */
|
|
273
|
+
snapshot(policy) {
|
|
245
274
|
return this.project(policy, this.capped());
|
|
246
275
|
}
|
|
276
|
+
resolveSnapshot(policy, roots, available) {
|
|
277
|
+
return this.project(policy, this.capped(), roots, available ? "available" : "unavailable");
|
|
278
|
+
}
|
|
279
|
+
resolve(policy) {
|
|
280
|
+
if (this.admissionProof === void 0) return this.project(policy, this.capped());
|
|
281
|
+
const operation = currentRegistryOperation(this.admissionProof);
|
|
282
|
+
if (operation !== void 0 && operation.identity.mode === policy.mode && operation.scope.primaryRoot === canonicalPath(policy.workspaceRoot)) {
|
|
283
|
+
const snapshot = operation.scope;
|
|
284
|
+
return this.capped() ? {
|
|
285
|
+
...snapshot,
|
|
286
|
+
writableRoots: [],
|
|
287
|
+
deniedRoots: [.../* @__PURE__ */ new Set([...snapshot.deniedRoots, ...snapshot.additionalRoots])]
|
|
288
|
+
} : snapshot;
|
|
289
|
+
}
|
|
290
|
+
return this.project(policy, true, void 0, "unavailable");
|
|
291
|
+
}
|
|
247
292
|
/** Read-only pending-dispatch projection. Does not mint an ALS execution grant. */
|
|
248
293
|
previewForTool(policy, toolName) {
|
|
249
294
|
return this.project(policy, this.capForTool(toolName));
|
|
@@ -252,14 +297,15 @@ var MultiRootScopeService = class extends Service {
|
|
|
252
297
|
capForTool(toolName) {
|
|
253
298
|
return this.capsInvalid || toolCapStore.getStore()?.capped === true || this.caps[toolName] === "read-only";
|
|
254
299
|
}
|
|
255
|
-
project(policy, capped) {
|
|
300
|
+
project(policy, capped, recorded, authority = this.registryAuthority) {
|
|
256
301
|
const primaryRoot = canonicalPath(policy.workspaceRoot);
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
const
|
|
302
|
+
const registered = recorded ?? this.rootsByPrimary.get(primaryRoot) ?? [];
|
|
303
|
+
const roots = authority === "unavailable" ? [] : sanitizeMembers(primaryRoot, registered);
|
|
304
|
+
const writable = capped || authority === "unavailable" ? [] : roots.filter((root) => effectivelyWritable(policy.mode, root.access)).map((root) => root.path);
|
|
305
|
+
const denied = registered.filter((root) => typeof root.recordedPath === "string" && root.recordedPath !== "" && root.recordedPath !== primaryRoot && (capped || !effectivelyWritable(policy.mode, root.access))).map((root) => root.recordedPath);
|
|
260
306
|
const deniedRoots = [...new Set(denied)];
|
|
261
307
|
let protectedHome;
|
|
262
|
-
if (this.dshHome !== void 0 && policy.mode === "danger-full-access" && (deniedRoots.length > 0 ||
|
|
308
|
+
if (this.dshHome !== void 0 && policy.mode === "danger-full-access" && (deniedRoots.length > 0 || authority === "unavailable")) {
|
|
263
309
|
const home = canonicalPath(this.dshHome);
|
|
264
310
|
if (!deniedRoots.includes(home)) {
|
|
265
311
|
deniedRoots.push(home);
|
|
@@ -272,7 +318,7 @@ var MultiRootScopeService = class extends Service {
|
|
|
272
318
|
roots,
|
|
273
319
|
writableRoots: writable,
|
|
274
320
|
deniedRoots,
|
|
275
|
-
authority
|
|
321
|
+
authority,
|
|
276
322
|
...protectedHome === void 0 ? {} : { protectedHome }
|
|
277
323
|
};
|
|
278
324
|
}
|
|
@@ -298,6 +344,16 @@ var MultiRootScopeService = class extends Service {
|
|
|
298
344
|
* @returns the canonical additional roots; empty when none are registered.
|
|
299
345
|
*/
|
|
300
346
|
scopeOf(primaryRoot) {
|
|
347
|
+
if (this.admissionProof !== void 0) {
|
|
348
|
+
const operation = currentRegistryOperation(this.admissionProof);
|
|
349
|
+
return operation?.scope.primaryRoot === canonicalPath(primaryRoot) ? operation.scope.additionalRoots.filter((root) => {
|
|
350
|
+
try {
|
|
351
|
+
return canonicalPath(root) === root && statSync(root).isDirectory();
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}) : [];
|
|
356
|
+
}
|
|
301
357
|
return this.membersOf(primaryRoot).map((root) => root.path);
|
|
302
358
|
}
|
|
303
359
|
/**
|
|
@@ -323,7 +379,7 @@ var MultiRootScopeService = class extends Service {
|
|
|
323
379
|
* registry cannot prove none does.
|
|
324
380
|
*/
|
|
325
381
|
hasActiveDenyRoots() {
|
|
326
|
-
if (this.registryAuthority === "unavailable") return true;
|
|
382
|
+
if (this.registryAuthority === "unavailable" || !this.hasAdmission()) return true;
|
|
327
383
|
for (const members of this.rootsByPrimary.values()) if (members.some((root) => root.access === "read-only") || members.length > 0 && this.capped()) return true;
|
|
328
384
|
return false;
|
|
329
385
|
}
|
|
@@ -356,10 +412,14 @@ var MultiRootScopeService = class extends Service {
|
|
|
356
412
|
* (before acquisition, on contention or storage failure, at teardown) and
|
|
357
413
|
* `available` only once its stored records have been published. Nothing
|
|
358
414
|
* else may call it, and it never touches the root table.
|
|
415
|
+
* Only transitions notify: a failed refresh may republish `unavailable`,
|
|
416
|
+
* and notifying on that no-op would re-arm Skills clients' refresh loop.
|
|
359
417
|
* @param available - whether this process holds the registry authority.
|
|
360
418
|
*/
|
|
361
419
|
setRegistryAuthority(available) {
|
|
362
|
-
|
|
420
|
+
const next = available ? "available" : "unavailable";
|
|
421
|
+
if (this.registryAuthority === next) return;
|
|
422
|
+
this.registryAuthority = next;
|
|
363
423
|
for (const listener of this.listeners) listener();
|
|
364
424
|
}
|
|
365
425
|
};
|