@dsh-electron/dsh-plugin-multi-root-workspace 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +36 -1
- package/CHANGELOG.md +36 -1
- package/README.en.md +27 -10
- package/README.md +27 -10
- package/cordis.patch.yml +5 -1
- package/detail/DETAIL.en.md +1 -0
- package/detail/DETAIL.md +1 -0
- package/detail/preview.svg +1 -0
- package/detail/preview.zh.svg +1 -0
- package/lib/{llm-message-fYORacvh.js → agent-instructions-BXuKhOzr.js} +1 -84
- package/lib/auto-review.js +2 -2
- package/lib/client.js +584 -73
- package/lib/client.js.map +1 -1
- package/lib/{command-B9855H7y.js → command-y5tfcAeC.js} +45 -12
- package/lib/command.js +1 -1
- package/lib/compat-CTJbpCxu.js +267 -0
- package/lib/compat.js +1 -1
- package/lib/{dialects-Dr4rDR-H.js → dialects-CdlG3RF9.js} +1 -1
- package/lib/{compat-Bg2HFVMX.js → dsh-version-DCwF7REk.js} +5 -265
- package/lib/{fs-CfzJq2wd.js → fs-Uws0ZpmV.js} +38 -0
- package/lib/fs.js +1 -1
- package/lib/index.js +11 -9
- package/lib/{instructions-KsUwUFE0.js → instructions-Ck9wp4J4.js} +695 -26
- package/lib/instructions.js +1 -1
- package/lib/llm-message-CepyzEBQ.js +84 -0
- package/lib/lsp.js +3 -3
- package/lib/{metadata-DgqSdVX_.js → metadata-99jgViHQ.js} +1 -1
- package/lib/{method-wrapper-CFY3p3j2.js → method-wrapper-D4oUCsEF.js} +22 -2
- package/lib/{native-win32-2waCRkPI.js → native-win32-C5oMtt7f.js} +2 -1
- package/lib/ops-compatibility.js +58 -0
- package/lib/ops-settings-UN2oOKEp.js +40 -0
- package/lib/ops-tools-k4JYaOrM.js +152 -0
- package/lib/process-fence.js +100 -24
- package/lib/{protocol-CJOvAvOS.js → protocol-CwDnFEyK.js} +54 -8
- package/lib/{registry-Bv431TfO.js → registry-DyDNDSJZ.js} +235 -79
- package/lib/{registry-lease-win32-DVTq9d4D.js → registry-lease-win32-r4puac9m.js} +2 -1
- package/lib/registry.js +2 -2
- package/lib/{review-context-NDpKeMCc.js → review-context-B-oHkU6a.js} +24 -7
- package/lib/review-context.js +1 -1
- package/lib/{roots-Ca_W1S9n.js → roots-CmaWxRVy.js} +112 -22
- package/lib/{sandbox-C-vCJCp3.js → sandbox-CZElTNPz.js} +1 -1
- package/lib/sandbox.js +1 -1
- package/lib/scope.js +72 -9
- package/lib/skills-utyuvmwr.js +104 -0
- package/lib/skills.js +4 -105
- package/lib/{tool-context-CpgIFcCB.js → tool-context-CCeY3mrK.js} +1 -1
- package/lib/types/authority/protocol.d.ts +5 -1
- package/lib/types/client/RootSwitcher.d.ts +2 -0
- package/lib/types/client/auto-review-settings.d.ts +18 -0
- package/lib/types/client/files-tree/FilesTree.d.ts +4 -1
- package/lib/types/client/files-tree/locales.d.ts +1 -1
- package/lib/types/client/locales.d.ts +40 -0
- package/lib/types/client/open-in-app/locales.d.ts +1 -1
- package/lib/types/client/ops-settings.d.ts +6 -0
- package/lib/types/client/skills-settings.d.ts +13 -0
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/command.d.ts +3 -3
- package/lib/types/compat/auto-review-client.d.ts +58 -0
- package/lib/types/compat/connection-rpc.d.ts +12 -0
- package/lib/types/compat/dsh-version.d.ts +1 -1
- package/lib/types/compat/files-tree.d.ts +36 -0
- package/lib/types/compat/fs-patch.d.ts +6 -0
- package/lib/types/compat/instruction-activity.d.ts +32 -1
- package/lib/types/compat/koffi.d.ts +2 -0
- package/lib/types/compat/ops-confirmation.d.ts +17 -0
- package/lib/types/compat/ops-settings.d.ts +11 -0
- package/lib/types/compat/ops-tools.d.ts +25 -0
- package/lib/types/compat/skills-client.d.ts +1 -0
- package/lib/types/compat/tool-context.d.ts +4 -0
- package/lib/types/compat.d.ts +2 -2
- package/lib/types/fs.d.ts +3 -1
- package/lib/types/instructions.d.ts +6 -1
- package/lib/types/ops-compatibility.d.ts +34 -0
- package/lib/types/registry.d.ts +67 -8
- package/lib/types/review-context.d.ts +2 -0
- package/lib/types/roots.d.ts +114 -20
- package/lib/types/scope.d.ts +28 -2
- package/lib/workspace-files.js +3 -3
- package/lib/{workspace-routing-YPxaQ4V3.js → workspace-routing-D_qwMUh-.js} +4 -4
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +93 -82
- package/src/auto-review/README.en.md +1 -1
- package/src/auto-review/README.md +1 -1
package/lib/instructions.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as composeInstructionMessage, i as apply, n as DEFAULT_MAX_SOURCE_BYTES, o as inject, r as PLUGIN_SOURCE,
|
|
1
|
+
import { a as composeInstructionMessage, i as apply, l as touchedPathOfToolCall, n as DEFAULT_MAX_SOURCE_BYTES, o as inject, r as PLUGIN_SOURCE, t as DEFAULT_MAX_BYTES } from "./instructions-Ck9wp4J4.js";
|
|
2
2
|
export { DEFAULT_MAX_BYTES, DEFAULT_MAX_SOURCE_BYTES, PLUGIN_SOURCE, apply, composeInstructionMessage, inject, touchedPathOfToolCall };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//#region src/compat/llm-message.ts
|
|
2
|
+
/**
|
|
3
|
+
* This plugin's own producer kind.
|
|
4
|
+
*
|
|
5
|
+
* Session format 4 rejects the shared `kind: 'plugin'` (`format v4 message
|
|
6
|
+
* requires a producer-owned source kind`). The kind is ours, not
|
|
7
|
+
* `agent-instructions`: upstream treats that kind's `changes` array as its
|
|
8
|
+
* own reconciliation authority.
|
|
9
|
+
*/
|
|
10
|
+
const PRODUCER_SOURCE_KIND = "multi-root-workspace";
|
|
11
|
+
/**
|
|
12
|
+
* The first session format whose encoder refuses `kind: 'plugin'`.
|
|
13
|
+
* Earlier formats declare that kind on `MessageSourceMap` and require it.
|
|
14
|
+
*/
|
|
15
|
+
const PRODUCER_OWNED_FORMAT = 4;
|
|
16
|
+
/**
|
|
17
|
+
* Which source kind the installed session format will persist.
|
|
18
|
+
* @param formatVersion - `SESSION_FORMAT_VERSION`, when the session package is installed.
|
|
19
|
+
* @returns `plugin` through format 3, and this plugin's own kind from format 4 on.
|
|
20
|
+
*/
|
|
21
|
+
function instructionSourceKind(formatVersion) {
|
|
22
|
+
return formatVersion !== void 0 && formatVersion >= PRODUCER_OWNED_FORMAT ? PRODUCER_SOURCE_KIND : "plugin";
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The installed session format, when that package is present.
|
|
26
|
+
*
|
|
27
|
+
* A composition that never persists a session can omit the package; format 3
|
|
28
|
+
* is then the spelling that still declares `kind: 'plugin'`. Any other failure
|
|
29
|
+
* is a broken install and must surface.
|
|
30
|
+
* @returns the format version, or `undefined` when the package is not installed.
|
|
31
|
+
*/
|
|
32
|
+
async function installedFormatVersion() {
|
|
33
|
+
try {
|
|
34
|
+
const session = await import("@deepseek-ai/dsh-session");
|
|
35
|
+
return typeof session.SESSION_FORMAT_VERSION === "number" ? session.SESSION_FORMAT_VERSION : void 0;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const code = error.code;
|
|
38
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") return void 0;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build one user-role instruction message.
|
|
44
|
+
*
|
|
45
|
+
* Format 3 and earlier: `{ kind: 'plugin', plugin, form: 'instructions' }`.
|
|
46
|
+
* Format 4 and later: `{ kind: 'multi-root-workspace', plugin, form: 'instructions' }`.
|
|
47
|
+
* @param input - the body and the plugin attribution.
|
|
48
|
+
* @returns the frozen message upstream's helper produced.
|
|
49
|
+
* @throws {Error} when the optional peer is absent: a step that has instruction
|
|
50
|
+
* text to deliver cannot be served without it, and staying silent would drop
|
|
51
|
+
* user-visible context instead of reporting a misconfigured composition.
|
|
52
|
+
*/
|
|
53
|
+
async function createInstructionMessage(input) {
|
|
54
|
+
let module;
|
|
55
|
+
try {
|
|
56
|
+
module = await import("@deepseek-ai/dsh-llm");
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(`multi-root workspace: @deepseek-ai/dsh-llm is not installed, so additional roots' instruction files cannot be delivered as messages (${error instanceof Error ? error.message : String(error)}).`);
|
|
59
|
+
}
|
|
60
|
+
const create = module.createUserMessage;
|
|
61
|
+
return create({
|
|
62
|
+
content: [{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: input.text
|
|
65
|
+
}],
|
|
66
|
+
source: instructionSource(input.plugin, await installedFormatVersion())
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** The source object the installed format will persist. */
|
|
70
|
+
function instructionSource(plugin, formatVersion) {
|
|
71
|
+
const kind = instructionSourceKind(formatVersion);
|
|
72
|
+
if (kind === "plugin") return {
|
|
73
|
+
kind,
|
|
74
|
+
plugin,
|
|
75
|
+
form: "instructions"
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
kind,
|
|
79
|
+
plugin,
|
|
80
|
+
form: "instructions"
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { createInstructionMessage as t };
|
package/lib/lsp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { t as wrapMethod } from "./method-wrapper-
|
|
1
|
+
import { t as wrapMethod } from "./method-wrapper-D4oUCsEF.js";
|
|
2
2
|
import { c as withRegistryRead } from "./registry-operation-C2ZV3Jhw.js";
|
|
3
|
-
import { t as aroundTool } from "./tool-context-
|
|
4
|
-
import { t as routeWorkspacePath } from "./workspace-routing-
|
|
3
|
+
import { t as aroundTool } from "./tool-context-CCeY3mrK.js";
|
|
4
|
+
import { t as routeWorkspacePath } from "./workspace-routing-D_qwMUh-.js";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
//#region src/lsp.ts
|
|
7
7
|
const inject = [
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
//#region src/method-wrapper.ts
|
|
2
|
+
const layers = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
function liveDescriptor(target, key, descriptor) {
|
|
4
|
+
let current = descriptor;
|
|
5
|
+
while (typeof current?.value === "function") {
|
|
6
|
+
const layer = layers.get(current.value);
|
|
7
|
+
if (layer === void 0 || layer.active || layer.target !== target || layer.key !== key) break;
|
|
8
|
+
current = layer.own;
|
|
9
|
+
}
|
|
10
|
+
return current;
|
|
11
|
+
}
|
|
2
12
|
/** Wrap a public method on a Cordis service without capturing a caller-context proxy. */
|
|
3
13
|
function wrapMethod(target, key, wrap) {
|
|
4
14
|
const own = Object.getOwnPropertyDescriptor(target, key);
|
|
@@ -10,6 +20,14 @@ function wrapMethod(target, key, wrap) {
|
|
|
10
20
|
}
|
|
11
21
|
if (typeof descriptor?.value !== "function") throw new Error(`multi-root: ${String(key)} is not a public method`);
|
|
12
22
|
const value = wrap(descriptor.value);
|
|
23
|
+
if (value === descriptor.value) return () => {};
|
|
24
|
+
const layer = {
|
|
25
|
+
target,
|
|
26
|
+
key,
|
|
27
|
+
own,
|
|
28
|
+
active: true
|
|
29
|
+
};
|
|
30
|
+
layers.set(value, layer);
|
|
13
31
|
Object.defineProperty(target, key, {
|
|
14
32
|
configurable: true,
|
|
15
33
|
writable: true,
|
|
@@ -17,9 +35,11 @@ function wrapMethod(target, key, wrap) {
|
|
|
17
35
|
value
|
|
18
36
|
});
|
|
19
37
|
return () => {
|
|
38
|
+
layer.active = false;
|
|
20
39
|
if (Object.getOwnPropertyDescriptor(target, key)?.value !== value) return;
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
const previous = liveDescriptor(target, key, own);
|
|
41
|
+
if (previous === void 0) Reflect.deleteProperty(target, key);
|
|
42
|
+
else Object.defineProperty(target, key, previous);
|
|
23
43
|
};
|
|
24
44
|
}
|
|
25
45
|
//#endregion
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { c as loadHostKoffi } from "./protocol-CwDnFEyK.js";
|
|
1
2
|
import { EventEmitter } from "node:events";
|
|
2
3
|
//#region src/authority/native-win32.ts
|
|
3
4
|
/** Windows local pipes: tokens, never a caller-supplied PID or a bearer file. */
|
|
4
5
|
var RestrictedRegistryPeerError = class extends Error {};
|
|
5
6
|
let apiPromise;
|
|
6
7
|
async function load() {
|
|
7
|
-
const koffi =
|
|
8
|
+
const koffi = await loadHostKoffi();
|
|
8
9
|
const kernel = koffi.load("kernel32.dll"), advapi = koffi.load("advapi32.dll");
|
|
9
10
|
const k = (name, ret, args) => kernel.func("__stdcall", name, ret, args);
|
|
10
11
|
const a = (name, ret, args) => advapi.func("__stdcall", name, ret, args);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { n as opsCompatibilityField, t as installOpsCompatibilitySettings } from "./ops-settings-UN2oOKEp.js";
|
|
2
|
+
import { t as configValue } from "./skills-utyuvmwr.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
5
|
+
import z from "@deepseek-ai/schemastery";
|
|
6
|
+
//#region src/ops-compatibility.ts
|
|
7
|
+
const enabledField = opsCompatibilityField(z.boolean().default(false));
|
|
8
|
+
const experimental = enabledField.experimental;
|
|
9
|
+
const described = typeof experimental === "function" ? experimental.call(enabledField) : enabledField;
|
|
10
|
+
const Config = z.object({ enabled: described.description("OPS 兼容模式(实验性,仅限 DSH 0.2.*):可能存在功能、版本和平台限制。启用 OPS 写保护、实时活动与 Review 增强;受限模式可能拒绝附加根操作(包括读取),后台缓存可能残留;关闭时显式只读附加根可能被 OPS 写入。") });
|
|
11
|
+
var MultiRootOpsCompatibility = class extends Service {
|
|
12
|
+
settingsConfig;
|
|
13
|
+
static Config = Config;
|
|
14
|
+
static provide = "multiRootOpsCompatibility";
|
|
15
|
+
static inject = [
|
|
16
|
+
"multiRootCompat",
|
|
17
|
+
"multiRootProviders",
|
|
18
|
+
"multiRootScope"
|
|
19
|
+
];
|
|
20
|
+
constructor(ctx, settingsConfig = {}) {
|
|
21
|
+
super(ctx, "multiRootOpsCompatibility");
|
|
22
|
+
this.settingsConfig = settingsConfig;
|
|
23
|
+
const settings = installOpsCompatibilitySettings(ctx, () => this.settingsConfig.enabled, () => this.sync());
|
|
24
|
+
this.settingsRead = settings.read;
|
|
25
|
+
ctx.effect(() => () => {
|
|
26
|
+
this.closing = true;
|
|
27
|
+
this.enabledNow = false;
|
|
28
|
+
this.currentToken = void 0;
|
|
29
|
+
});
|
|
30
|
+
this.sync();
|
|
31
|
+
}
|
|
32
|
+
settingsRead;
|
|
33
|
+
currentToken;
|
|
34
|
+
enabledNow = false;
|
|
35
|
+
closing = false;
|
|
36
|
+
get token() {
|
|
37
|
+
this.sync();
|
|
38
|
+
return this.currentToken;
|
|
39
|
+
}
|
|
40
|
+
get enabled() {
|
|
41
|
+
this.sync();
|
|
42
|
+
return this.enabledNow;
|
|
43
|
+
}
|
|
44
|
+
sync() {
|
|
45
|
+
if (this.closing) {
|
|
46
|
+
this.enabledNow = false;
|
|
47
|
+
this.currentToken = void 0;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const raw = this.settingsRead?.();
|
|
51
|
+
const enabled = configValue(raw) === true;
|
|
52
|
+
if (enabled === this.enabledNow) return;
|
|
53
|
+
this.enabledNow = enabled;
|
|
54
|
+
this.currentToken = enabled ? Object.freeze({ id: randomUUID() }) : void 0;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
export { Config, MultiRootOpsCompatibility, MultiRootOpsCompatibility as default };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { d as readInstalledVersion } from "./dsh-version-DCwF7REk.js";
|
|
2
|
+
/** Product availability policy, independent of structural Settings adaptation. */
|
|
3
|
+
function opsCompatibilityRelease(release) {
|
|
4
|
+
return release !== void 0 && /^0\.2\.\d+(?:[-+].*)?$/.test(release);
|
|
5
|
+
}
|
|
6
|
+
/** Unsupported hosts retain the config input but expose no editable live field. */
|
|
7
|
+
function opsCompatibilityField(schema) {
|
|
8
|
+
if (!opsCompatibilityRelease(readInstalledVersion("@deepseek-ai/dsh-sandbox"))) return schema;
|
|
9
|
+
const method = schema.volatile;
|
|
10
|
+
return typeof method === "function" ? method.call(schema) : schema;
|
|
11
|
+
}
|
|
12
|
+
function installOpsCompatibilitySettings(ctx, initial, changed) {
|
|
13
|
+
let active = false;
|
|
14
|
+
return {
|
|
15
|
+
fiber: ctx.inject(["settings"], (scope) => {
|
|
16
|
+
if (!opsCompatibilityRelease(scope.get("multiRootCompat")?.release)) return;
|
|
17
|
+
const settings = scope.get("settings");
|
|
18
|
+
if (typeof settings.configure !== "function") return;
|
|
19
|
+
const stop = settings.configure({ auto: false }, ctx.fiber);
|
|
20
|
+
active = true;
|
|
21
|
+
const stopEvent = scope.on("settings/document-updated", (id) => {
|
|
22
|
+
if (id === "multi-root-ops-compatibility") changed();
|
|
23
|
+
});
|
|
24
|
+
changed();
|
|
25
|
+
return () => {
|
|
26
|
+
active = false;
|
|
27
|
+
stopEvent();
|
|
28
|
+
stop();
|
|
29
|
+
changed();
|
|
30
|
+
};
|
|
31
|
+
}),
|
|
32
|
+
read: () => active && opsCompatibilityRelease(ctx.get("multiRootCompat")?.release) ? initial() : void 0
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function opsCompatibilityToken(ctx) {
|
|
36
|
+
const service = ctx.get("multiRootOpsCompatibility");
|
|
37
|
+
return service === void 0 ? void 0 : service.token;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { opsCompatibilityField as n, opsCompatibilityToken as r, installOpsCompatibilitySettings as t };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { r as opsCompatibilityToken } from "./ops-settings-UN2oOKEp.js";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, relative } from "node:path";
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
//#region src/compat/ops-tools.ts
|
|
7
|
+
const OPS_TOOL_PREFIX = "ops_";
|
|
8
|
+
const OPS_EXEC_RAW_NAMES = [
|
|
9
|
+
"inspect_local_file",
|
|
10
|
+
"grep",
|
|
11
|
+
"glob",
|
|
12
|
+
"replace",
|
|
13
|
+
"write_file",
|
|
14
|
+
"edit_file",
|
|
15
|
+
"diff",
|
|
16
|
+
"stat",
|
|
17
|
+
"copy",
|
|
18
|
+
"move",
|
|
19
|
+
"remove",
|
|
20
|
+
"read_files",
|
|
21
|
+
"read_table",
|
|
22
|
+
"read_docx",
|
|
23
|
+
"read_pdf",
|
|
24
|
+
"batch",
|
|
25
|
+
"result",
|
|
26
|
+
"bash"
|
|
27
|
+
];
|
|
28
|
+
const OPS_PUBLISH_RAW_NAMES = ["discover_tools", "load_sdk"];
|
|
29
|
+
OPS_EXEC_RAW_NAMES.map((raw) => OPS_TOOL_PREFIX + raw);
|
|
30
|
+
function classifyOpsTool(name) {
|
|
31
|
+
if (typeof name !== "string" || !name.startsWith("ops_")) return "other";
|
|
32
|
+
const raw = name.slice(4);
|
|
33
|
+
if (OPS_EXEC_RAW_NAMES.includes(raw)) return "exec";
|
|
34
|
+
if (OPS_PUBLISH_RAW_NAMES.includes(raw)) return "publish";
|
|
35
|
+
return "unknown";
|
|
36
|
+
}
|
|
37
|
+
const spawnPermit = new AsyncLocalStorage();
|
|
38
|
+
function withOpsSpawnPermit(state, body) {
|
|
39
|
+
const permit = {
|
|
40
|
+
state,
|
|
41
|
+
active: true
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
return spawnPermit.run(permit, body);
|
|
45
|
+
} finally {
|
|
46
|
+
permit.active = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function opsSpawnPermitted(state) {
|
|
50
|
+
const permit = spawnPermit.getStore();
|
|
51
|
+
return permit?.state === state && permit.active;
|
|
52
|
+
}
|
|
53
|
+
function createOpsProtectionState() {
|
|
54
|
+
return {
|
|
55
|
+
owner: void 0,
|
|
56
|
+
entered: 0,
|
|
57
|
+
drainWaiters: /* @__PURE__ */ new Set()
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function claimOpsProtection(state) {
|
|
61
|
+
const owner = {};
|
|
62
|
+
state.owner = owner;
|
|
63
|
+
return owner;
|
|
64
|
+
}
|
|
65
|
+
function revokeOpsProtection(state, token) {
|
|
66
|
+
if (state.owner === token) state.owner = void 0;
|
|
67
|
+
}
|
|
68
|
+
function opsProtectionReady(state) {
|
|
69
|
+
return state.owner !== void 0;
|
|
70
|
+
}
|
|
71
|
+
function assertOpsProtectionOwner(state, token) {
|
|
72
|
+
if (state.owner !== token) throw new Error("multi-root workspace: the OPS execution protection generation was replaced during dispatch");
|
|
73
|
+
}
|
|
74
|
+
function enterOpsProtection(state) {
|
|
75
|
+
if (state.owner === void 0) throw new Error("multi-root workspace: the OPS execution protection is not installed on this scope");
|
|
76
|
+
state.entered += 1;
|
|
77
|
+
return state.owner;
|
|
78
|
+
}
|
|
79
|
+
function leaveOpsProtection(state) {
|
|
80
|
+
state.entered -= 1;
|
|
81
|
+
if (state.entered === 0) {
|
|
82
|
+
for (const resolve of state.drainWaiters) resolve();
|
|
83
|
+
state.drainWaiters.clear();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function drainOpsProtection(state) {
|
|
87
|
+
if (state.entered === 0) return;
|
|
88
|
+
await new Promise((resolve) => state.drainWaiters.add(resolve));
|
|
89
|
+
}
|
|
90
|
+
const OPS_RESULT_CACHE_LEAF = /^dsh-ops-results-[0-9a-f]{64}$/u;
|
|
91
|
+
function cacheRefusal(detail) {
|
|
92
|
+
return /* @__PURE__ */ new Error(`multi-root workspace: ${detail}; refusing to prepare the OPS execution`);
|
|
93
|
+
}
|
|
94
|
+
function containsPath(root, path) {
|
|
95
|
+
if (root === path) return true;
|
|
96
|
+
const rel = relative(root, path);
|
|
97
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
98
|
+
}
|
|
99
|
+
function assertOpsPosixCacheAdmission(deniedRoots) {
|
|
100
|
+
let canonicalTmp;
|
|
101
|
+
try {
|
|
102
|
+
canonicalTmp = realpathSync.native(tmpdir());
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw cacheRefusal(`the canonical temporary directory cannot be resolved (${error.message})`);
|
|
105
|
+
}
|
|
106
|
+
if (!isAbsolute(canonicalTmp)) throw cacheRefusal("the canonical temporary directory is not absolute");
|
|
107
|
+
const rawTmp = tmpdir();
|
|
108
|
+
const spellings = /* @__PURE__ */ new Set();
|
|
109
|
+
for (const root of deniedRoots) {
|
|
110
|
+
if (root === "") continue;
|
|
111
|
+
spellings.add(root);
|
|
112
|
+
try {
|
|
113
|
+
spellings.add(realpathSync.native(root));
|
|
114
|
+
} catch {}
|
|
115
|
+
if (containsPath(rawTmp, root) || root === rawTmp) {
|
|
116
|
+
const rel = relative(rawTmp, root);
|
|
117
|
+
spellings.add(rel === "" ? canonicalTmp : canonicalTmp + "/" + rel);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const deny of spellings) {
|
|
121
|
+
if (containsPath(deny, canonicalTmp)) throw cacheRefusal(`a deny boundary ${JSON.stringify(deny)} covers the temporary directory`);
|
|
122
|
+
const rel = relative(canonicalTmp, deny);
|
|
123
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) continue;
|
|
124
|
+
const first = rel.split("/")[0];
|
|
125
|
+
if (first !== void 0 && OPS_RESULT_CACHE_LEAF.test(first)) throw cacheRefusal(`a deny boundary ${JSON.stringify(deny)} overlaps the OPS result cache`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const object = (value) => typeof value === "object" && value !== null;
|
|
129
|
+
function opsDispatchBarrier(ctx, scope, exec) {
|
|
130
|
+
if (opsCompatibilityToken(ctx) === void 0) return void 0;
|
|
131
|
+
const kind = classifyOpsTool(exec.name);
|
|
132
|
+
if (kind === "other" || kind === "publish") return void 0;
|
|
133
|
+
if (ctx.get("multiRootCompat") === void 0 || ctx.get("multiRootProviders") === void 0) return void 0;
|
|
134
|
+
const session = exec.agent?.session;
|
|
135
|
+
const policyService = ctx.get("sandboxPolicy");
|
|
136
|
+
let policy;
|
|
137
|
+
if (session !== void 0 && policyService !== void 0) try {
|
|
138
|
+
policy = policyService.resolve({ session });
|
|
139
|
+
} catch {
|
|
140
|
+
policy = void 0;
|
|
141
|
+
}
|
|
142
|
+
if (policy === void 0) return `the OPS tool ${JSON.stringify(exec.name)} cannot be confirmed against a trusted session and policy`;
|
|
143
|
+
const preview = scope.previewForTool(policy, exec.name);
|
|
144
|
+
if (preview.authority === "unavailable") return `the multi-root registry authority is unavailable; the OPS call ${JSON.stringify(exec.name)} is refused before dispatch`;
|
|
145
|
+
if (preview.deniedRoots.length === 0) return void 0;
|
|
146
|
+
if (policy.mode !== "danger-full-access") return `the OPS call ${JSON.stringify(exec.name)} is refused in ${policy.mode}: private preparation and worker write boundaries cannot be proven without OPS integration`;
|
|
147
|
+
if (kind === "unknown") return `the unrecognized OPS capability ${JSON.stringify(exec.name)} is refused while write boundaries apply`;
|
|
148
|
+
if (!opsProtectionReady(scope.opsProtection)) return `the OPS execution protection is not installed; the call ${JSON.stringify(exec.name)} is refused`;
|
|
149
|
+
if (exec.name === `ops_bash` && object(exec.arguments) && exec.arguments.run_in_background === true) return "run_in_background is refused while danger-full-access write boundaries apply";
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { createOpsProtectionState as a, leaveOpsProtection as c, revokeOpsProtection as d, withOpsSpawnPermit as f, classifyOpsTool as i, opsDispatchBarrier as l, assertOpsProtectionOwner as n, drainOpsProtection as o, claimOpsProtection as r, enterOpsProtection as s, assertOpsPosixCacheAdmission as t, opsSpawnPermitted as u };
|
package/lib/process-fence.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { t as wrapMethod } from "./method-wrapper-
|
|
1
|
+
import { t as wrapMethod } from "./method-wrapper-D4oUCsEF.js";
|
|
2
2
|
import { s as withRegistryAdmission } from "./registry-operation-C2ZV3Jhw.js";
|
|
3
|
-
import { t as aroundTool } from "./tool-context-
|
|
4
|
-
import {
|
|
3
|
+
import { t as aroundTool } from "./tool-context-CCeY3mrK.js";
|
|
4
|
+
import { r as opsCompatibilityToken } from "./ops-settings-UN2oOKEp.js";
|
|
5
|
+
import { n as detectDialect, s as confineInstalled } from "./dialects-CdlG3RF9.js";
|
|
6
|
+
import { c as leaveOpsProtection, d as revokeOpsProtection, f as withOpsSpawnPermit, i as classifyOpsTool, n as assertOpsProtectionOwner, o as drainOpsProtection, r as claimOpsProtection, s as enterOpsProtection, t as assertOpsPosixCacheAdmission } from "./ops-tools-k4JYaOrM.js";
|
|
5
7
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
8
|
//#region src/compat/process-profile.ts
|
|
7
9
|
/** Structural runner recognition shared by the synchronous process seams. */
|
|
@@ -206,9 +208,59 @@ function isThenable(value) {
|
|
|
206
208
|
}
|
|
207
209
|
const object = (value) => typeof value === "object" && value !== null;
|
|
208
210
|
function apply(ctx) {
|
|
211
|
+
const opsScope = ctx.get("multiRootScope");
|
|
212
|
+
const opsSandbox = ctx.sandbox;
|
|
213
|
+
const runOpsDispatch = async (exec, policy, next) => {
|
|
214
|
+
const modeToken = opsCompatibilityToken(ctx);
|
|
215
|
+
const scope = opsScope;
|
|
216
|
+
const opsState = scope.opsProtection;
|
|
217
|
+
const token = enterOpsProtection(opsState);
|
|
218
|
+
try {
|
|
219
|
+
const dispatch = async () => {
|
|
220
|
+
const resolved = scope.resolve(policy);
|
|
221
|
+
if (resolved.authority === "unavailable") throw new Error("multi-root workspace: the registry authority is unavailable; refusing the OPS execution");
|
|
222
|
+
if (resolved.deniedRoots.length > 0) {
|
|
223
|
+
if (policy.mode !== "danger-full-access") throw new Error(`multi-root workspace: the OPS execution is refused in ${policy.mode}: private preparation and worker write boundaries cannot be proven without OPS integration`);
|
|
224
|
+
if (classifyOpsTool(exec.name) === "unknown") throw new Error(`multi-root workspace: the unrecognized OPS capability ${JSON.stringify(exec.name)} is refused while write boundaries apply`);
|
|
225
|
+
if (exec.name === "ops_bash" && typeof exec.arguments === "object" && exec.arguments !== null && exec.arguments.run_in_background === true) throw new Error("multi-root workspace: run_in_background is refused while danger-full-access write boundaries apply");
|
|
226
|
+
}
|
|
227
|
+
let facts;
|
|
228
|
+
if (resolved.deniedRoots.length > 0) {
|
|
229
|
+
if (process.platform === "win32") throw new Error("multi-root workspace: OPS worker preparation is not verified on this platform; refusing the call while write boundaries apply");
|
|
230
|
+
assertOpsPosixCacheAdmission(resolved.deniedRoots);
|
|
231
|
+
facts = await opsSandbox.restrictedExecution(policy, resolved.deniedRoots, exec.signal, resolved.protectedHome);
|
|
232
|
+
if (facts === void 0 || facts.argv.length === 0) throw new Error("multi-root workspace: the restricted runner could not be prepared; refusing the OPS execution");
|
|
233
|
+
}
|
|
234
|
+
assertOpsProtectionOwner(opsState, token);
|
|
235
|
+
if (opsCompatibilityToken(ctx) !== modeToken) throw new Error("multi-root workspace: the OPS compatibility mode changed during dispatch; refusing the execution");
|
|
236
|
+
const store = {
|
|
237
|
+
argvPrefix: facts?.argv ?? [],
|
|
238
|
+
ops: true,
|
|
239
|
+
opsState,
|
|
240
|
+
active: true
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
return await processDenyStore.run(store, () => facts === void 0 ? next() : decorateProcessOutcome(next(), facts, policy, { stderr: () => store.stderr?.() }));
|
|
244
|
+
} finally {
|
|
245
|
+
store.active = false;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
if (ctx.get("multiRootRegistry")?.shared) return await withRegistryAdmission(ctx, policy, "process", dispatch);
|
|
249
|
+
return await dispatch();
|
|
250
|
+
} finally {
|
|
251
|
+
leaveOpsProtection(opsState);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
209
254
|
aroundTool(ctx, async (exec, next) => {
|
|
210
|
-
|
|
211
|
-
|
|
255
|
+
const kind = classifyOpsTool(exec.name);
|
|
256
|
+
if (kind === "other" || kind === "publish" || opsCompatibilityToken(ctx) === void 0) {
|
|
257
|
+
if (!ctx.get("multiRootRegistry")?.shared || exec.agent === void 0) return next();
|
|
258
|
+
const policy = ctx.sandboxPolicy.resolve({ session: exec.agent.session });
|
|
259
|
+
return opsScope.withToolCap(exec.name, () => withRegistryAdmission(ctx, policy, "process", next));
|
|
260
|
+
}
|
|
261
|
+
if (exec.agent === void 0) throw new Error(`multi-root workspace: the OPS call ${JSON.stringify(exec.name)} cannot be confirmed against a trusted session`);
|
|
262
|
+
const policy = ctx.sandboxPolicy.resolve({ session: exec.agent.session });
|
|
263
|
+
return opsScope.withToolCap(exec.name, () => runOpsDispatch(exec, policy, next));
|
|
212
264
|
});
|
|
213
265
|
const policyOf = (spec) => spec?.sandboxPolicy ?? ctx.sandboxPolicy.resolve();
|
|
214
266
|
/**
|
|
@@ -240,27 +292,46 @@ function apply(ctx) {
|
|
|
240
292
|
return isThenable(prepared) ? prepared.then(run) : run(prepared);
|
|
241
293
|
};
|
|
242
294
|
ctx.inject(["subprocess"], (scope) => {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
argv
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
295
|
+
const opsState = opsScope.opsProtection;
|
|
296
|
+
scope.effect(() => {
|
|
297
|
+
const restore = wrapMethod(scope.get("subprocess"), "spawn", (original) => function(spec) {
|
|
298
|
+
const store = processDenyStore.getStore();
|
|
299
|
+
if (store?.ops === true && (store.active === false || store.opsState !== opsState)) throw new Error("multi-root workspace: the OPS execution domain has expired");
|
|
300
|
+
const admissionScope = store?.ops === true ? opsScope : scope.multiRootScope;
|
|
301
|
+
if (admissionScope.admissionRequired && admissionScope.hasAdmission() && !admissionScope.admissionPathsValid()) throw new Error("multi-root workspace: an admitted root was redirected before process launch");
|
|
302
|
+
let argv = spec.argv;
|
|
303
|
+
if (store?.ops === true) {
|
|
304
|
+
const prefix = store.argvPrefix;
|
|
305
|
+
if (hasConfinedProfile(spec.argv)) {
|
|
306
|
+
if (!(spec.argv.length > prefix.length && prefix.every((segment, index) => spec.argv[index] === segment))) throw new Error("multi-root workspace: a confined spawn inside an OPS execution domain must carry this domain's own profile");
|
|
307
|
+
} else argv = [...prefix, ...spec.argv];
|
|
308
|
+
if (opsScope.admissionRequired && (hasConfinedProfile(spec.argv) || prefix.length > 0) && !opsScope.permitsProfile(argv)) throw new Error("multi-root workspace: a confined spawn requires a profile from this live admission");
|
|
309
|
+
} else {
|
|
310
|
+
argv = store === void 0 ? spec.argv : [...store.argvPrefix, ...spec.argv];
|
|
311
|
+
if (scope.multiRootScope.admissionRequired && (store !== void 0 || hasConfinedProfile(argv)) && !scope.multiRootScope.permitsProfile(argv)) throw new Error("multi-root workspace: a confined spawn requires a profile from this live admission");
|
|
259
312
|
}
|
|
313
|
+
const launch = () => original.call(this, argv === spec.argv ? spec : {
|
|
314
|
+
...spec,
|
|
315
|
+
argv
|
|
316
|
+
});
|
|
317
|
+
const handle = store !== void 0 || admissionScope.admissionRequired && admissionScope.permitsProfile(argv) ? withOpsSpawnPermit(opsState, launch) : launch();
|
|
318
|
+
if (store === void 0) return handle;
|
|
319
|
+
const stderr = object(handle) && object(handle.collected) && object(handle.collected.stderr) && typeof handle.collected.stderr.readFrom === "function" ? handle.collected.stderr : void 0;
|
|
320
|
+
if (stderr !== void 0) store.stderr = () => {
|
|
321
|
+
try {
|
|
322
|
+
return stderr.readFrom(0).text;
|
|
323
|
+
} catch {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
return handle;
|
|
328
|
+
});
|
|
329
|
+
return async () => {
|
|
330
|
+
await drainOpsProtection(opsState);
|
|
331
|
+
restore();
|
|
260
332
|
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
scope.effect(() => wrapMethod(scope.subprocess, "spawnTerminal", (original) => async function(spec) {
|
|
333
|
+
});
|
|
334
|
+
scope.effect(() => wrapMethod(scope.get("subprocess"), "spawnTerminal", (original) => async function(spec) {
|
|
264
335
|
const ptySessionId = spec.env?.DSH_PTY_SESSION_ID;
|
|
265
336
|
if (ptySessionId === void 0 || ptySessionId === "") return await original.call(this, spec);
|
|
266
337
|
const sessionId = spec.env?.DSH_SESSION_ID;
|
|
@@ -293,6 +364,11 @@ function apply(ctx) {
|
|
|
293
364
|
});
|
|
294
365
|
});
|
|
295
366
|
}));
|
|
367
|
+
const token = claimOpsProtection(opsState);
|
|
368
|
+
scope.effect(() => async () => {
|
|
369
|
+
revokeOpsProtection(opsState, token);
|
|
370
|
+
await drainOpsProtection(opsState);
|
|
371
|
+
});
|
|
296
372
|
});
|
|
297
373
|
/**
|
|
298
374
|
* The wrapper every execute-style consumer entry shares: resolve the policy
|