dsh-bots 0.0.1 → 0.2.11

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/lib/version.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Minimal semver caret-range matching for the prerelease patterns this plugin
3
+ * actually uses (e.g. `^4.0.1`, `^0.1.1-rc.2`).
4
+ *
5
+ * Ported from the community dsh-plugin-template (MIT) and kept
6
+ * dependency-free: it implements the subset of node-semver semantics needed to
7
+ * turn a silent peer mismatch into a loud, actionable error.
8
+ * See tests/version.spec.ts for the behavior matrix.
9
+ * @module dsh-plugin-bots/version
10
+ */
11
+ /** Parse `X.Y.Z` or `X.Y.Z-pre` into a comparable structure, or null. */
12
+ export function parseVersion(input) {
13
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(input.trim());
14
+ if (match === null)
15
+ return null;
16
+ const prerelease = match[4] !== undefined && match[4] !== '' ? match[4].split('.') : null;
17
+ return {
18
+ major: Number(match[1]),
19
+ minor: Number(match[2]),
20
+ patch: Number(match[3]),
21
+ prerelease,
22
+ };
23
+ }
24
+ function compareIdentifier(a, b) {
25
+ const aNumeric = /^\d+$/.test(a);
26
+ const bNumeric = /^\d+$/.test(b);
27
+ if (aNumeric && bNumeric) {
28
+ const diff = BigInt(a) - BigInt(b);
29
+ return diff === 0n ? 0 : diff > 0n ? 1 : -1;
30
+ }
31
+ if (aNumeric)
32
+ return -1; // numeric identifiers always sort lower
33
+ if (bNumeric)
34
+ return 1;
35
+ return a < b ? -1 : a > b ? 1 : 0;
36
+ }
37
+ /** Compare two prerelease identifier lists; a stable version (null) is higher. */
38
+ export function comparePrerelease(a, b) {
39
+ if (a === null && b === null)
40
+ return 0;
41
+ if (a === null)
42
+ return 1;
43
+ if (b === null)
44
+ return -1;
45
+ const length = Math.max(a.length, b.length);
46
+ for (let i = 0; i < length; i++) {
47
+ const left = a[i];
48
+ const right = b[i];
49
+ if (left === undefined)
50
+ return -1; // shorter prerelease sorts lower
51
+ if (right === undefined)
52
+ return 1;
53
+ const diff = compareIdentifier(left, right);
54
+ if (diff !== 0)
55
+ return diff;
56
+ }
57
+ return 0;
58
+ }
59
+ function tupleOf(version) {
60
+ return [version.major, version.minor, version.patch];
61
+ }
62
+ function compareTuple(a, b) {
63
+ for (let i = 0; i < 3; i++) {
64
+ const diff = a[i] - b[i];
65
+ if (diff !== 0)
66
+ return diff > 0 ? 1 : -1;
67
+ }
68
+ return 0;
69
+ }
70
+ /** Upper exclusive bound of a caret range: ^0.1.0 → 0.2.0, ^1.2.3 → 2.0.0. */
71
+ export function caretUpperBound(version) {
72
+ if (version.major > 0)
73
+ return [version.major + 1, 0, 0];
74
+ if (version.minor > 0)
75
+ return [0, version.minor + 1, 0];
76
+ return [0, 0, version.patch + 1];
77
+ }
78
+ /**
79
+ * Whether `version` satisfies the caret range `^X.Y.Z` or `^X.Y.Z-pre`.
80
+ * Mirrors node-semver for the subset this plugin declares.
81
+ */
82
+ export function satisfiesCaret(version, range) {
83
+ const match = /^\^([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?)$/.exec(range.trim());
84
+ if (match === null)
85
+ return false;
86
+ const parsed = parseVersion(version);
87
+ const parsedRange = parseVersion(match[1] ?? '');
88
+ if (parsed === null || parsedRange === null)
89
+ return false;
90
+ const versionTuple = tupleOf(parsed);
91
+ const rangeTuple = tupleOf(parsedRange);
92
+ const tupleDiff = compareTuple(versionTuple, rangeTuple);
93
+ // Lower bound.
94
+ if (tupleDiff < 0)
95
+ return false;
96
+ if (tupleDiff === 0) {
97
+ if (parsedRange.prerelease === null) {
98
+ // A range without a prerelease does not match prereleases of the same tuple.
99
+ if (parsed.prerelease !== null)
100
+ return false;
101
+ }
102
+ else if (comparePrerelease(parsed.prerelease, parsedRange.prerelease) < 0) {
103
+ return false;
104
+ }
105
+ }
106
+ // Upper bound (exclusive).
107
+ const upper = caretUpperBound(parsedRange);
108
+ return compareTuple(versionTuple, upper) < 0;
109
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Per-agent workspace jail configuration bridge (plugin side).
3
+ *
4
+ * The ENGINE owns the isolation mechanism (`sdk-bots src/host/runner/
5
+ * agent-workspace-jail.ts`): when an agent's settings.json declares
6
+ * `workspaceRoot`, every shell that agent runs is wrapped in a generated
7
+ * macOS Seatbelt profile that denies file WRITES outside the agent's own
8
+ * workspace directory (+ allowPaths + OS temp). Reads stay unrestricted so
9
+ * shared blackboards remain readable. The jail resolves lazily per turn —
10
+ * editing settings.json takes effect on the agent's next turn, no restart.
11
+ *
12
+ * This module only mirrors the engine's config contract so the plugin can
13
+ * read and write it safely:
14
+ * `<dataDir>/agents/<agentId>/settings.json` →
15
+ * { workspaceRoot?: "/workspace/<slug>", workspaceAllowPaths?: string[] }
16
+ *
17
+ * Validation mirrors the engine exactly (SLUG_PATTERN, agentId pattern,
18
+ * virtual-prefix form) because the engine FAILS CLOSED on malformed config —
19
+ * a bad write would break the agent's turns, not silently run unjailed.
20
+ * @module dsh-bots/workspace
21
+ */
22
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ /** Bot-facing virtual root prefix; the daemon maps it under the box root. */
25
+ export const WORKSPACE_VIRTUAL_PREFIX = '/workspace/';
26
+ /** Same shape as the engine's `AGENT_ID_FILENAME_PATTERN`. */
27
+ const AGENT_ID_PATTERN = /^[\w-]{1,128}$/;
28
+ /** Same shape as the engine's `SLUG_PATTERN` (unicode letters allowed: 录音师). */
29
+ const SLUG_PATTERN = /^[\p{L}\p{N}][\p{L}\p{N}._-]{0,78}$/u;
30
+ /** Read one agent's settings.json jail keys, preserving unknown fields. */
31
+ function readRawSettings(agentDir) {
32
+ const path = join(agentDir, 'settings.json');
33
+ if (!existsSync(path))
34
+ return {};
35
+ try {
36
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
37
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
38
+ ? parsed
39
+ : {};
40
+ }
41
+ catch {
42
+ return {};
43
+ }
44
+ }
45
+ /** Validate the `/workspace/<slug>` virtual form exactly like the engine. */
46
+ export function validateVirtualRoot(requested) {
47
+ if (typeof requested !== 'string' || !requested.startsWith(WORKSPACE_VIRTUAL_PREFIX)) {
48
+ throw new Error(`workspaceRoot 必须是 "${WORKSPACE_VIRTUAL_PREFIX}<slug>" 形式(如 /workspace/录音师)`);
49
+ }
50
+ const slug = requested.slice(WORKSPACE_VIRTUAL_PREFIX.length);
51
+ if (slug.includes('..') || slug.includes('/') || !SLUG_PATTERN.test(slug)) {
52
+ throw new Error(`workspaceRoot 的 slug 不合法(字母/数字/点/横线/下划线,1-79 位):"${slug}"`);
53
+ }
54
+ return requested;
55
+ }
56
+ /** Read one agent's jail config; null when the agent dir does not exist. */
57
+ export function readAgentWorkspace(dataDir, agentId) {
58
+ if (typeof agentId !== 'string' || !AGENT_ID_PATTERN.test(agentId))
59
+ return null;
60
+ const agentDir = join(dataDir, 'agents', agentId);
61
+ if (!existsSync(agentDir))
62
+ return null;
63
+ const raw = readRawSettings(agentDir);
64
+ const allowPaths = Array.isArray(raw.workspaceAllowPaths)
65
+ ? raw.workspaceAllowPaths.filter((v) => typeof v === 'string' && v.trim() !== '')
66
+ : [];
67
+ return {
68
+ agentId,
69
+ workspaceRoot: typeof raw.workspaceRoot === 'string' && raw.workspaceRoot.trim() !== ''
70
+ ? raw.workspaceRoot.trim()
71
+ : null,
72
+ allowPaths,
73
+ };
74
+ }
75
+ /** Scan every agent directory for its jail config (missing settings → unjailed). */
76
+ export function listAgentWorkspaces(dataDir) {
77
+ const agentsDir = join(dataDir, 'agents');
78
+ let entries = [];
79
+ try {
80
+ entries = readdirSync(agentsDir);
81
+ }
82
+ catch {
83
+ return [];
84
+ }
85
+ return entries
86
+ .filter((id) => AGENT_ID_PATTERN.test(id))
87
+ .sort()
88
+ .map((id) => readAgentWorkspace(dataDir, id) ?? { agentId: id, workspaceRoot: null, allowPaths: [] });
89
+ }
90
+ /**
91
+ * Write one agent's jail keys into its settings.json, preserving every other
92
+ * field. Creates the agent dir when missing (a freshly created bot may not
93
+ * have its settings.json yet).
94
+ */
95
+ export function setAgentWorkspace(dataDir, agentId, request) {
96
+ if (typeof agentId !== 'string' || !AGENT_ID_PATTERN.test(agentId)) {
97
+ throw new Error(`agentId 不合法:"${agentId}"`);
98
+ }
99
+ const agentDir = join(dataDir, 'agents', agentId);
100
+ const raw = readRawSettings(agentDir);
101
+ if (request.workspaceRoot === null || request.workspaceRoot === undefined || request.workspaceRoot === '') {
102
+ delete raw.workspaceRoot;
103
+ }
104
+ else {
105
+ raw.workspaceRoot = validateVirtualRoot(request.workspaceRoot);
106
+ }
107
+ if (request.allowPaths === undefined) {
108
+ // Leave existing allowPaths untouched unless explicitly replaced.
109
+ }
110
+ else if (Array.isArray(request.allowPaths)) {
111
+ const cleaned = request.allowPaths.filter((v) => typeof v === 'string' && v.trim() !== '');
112
+ if (cleaned.length === 0)
113
+ delete raw.workspaceAllowPaths;
114
+ else
115
+ raw.workspaceAllowPaths = cleaned;
116
+ }
117
+ else {
118
+ throw new Error('allowPaths 必须是字符串数组');
119
+ }
120
+ mkdirSync(agentDir, { recursive: true });
121
+ writeFileSync(join(agentDir, 'settings.json'), JSON.stringify(raw, null, 2) + '\n', 'utf8');
122
+ return {
123
+ agentId,
124
+ workspaceRoot: typeof raw.workspaceRoot === 'string' ? raw.workspaceRoot : null,
125
+ allowPaths: Array.isArray(raw.workspaceAllowPaths)
126
+ ? raw.workspaceAllowPaths
127
+ : [],
128
+ };
129
+ }
package/package.json CHANGED
@@ -1,21 +1,57 @@
1
1
  {
2
2
  "name": "dsh-bots",
3
- "version": "0.0.1",
4
- "description": "The plural form of the dsh bot framework - name reserved; first release in development.",
5
- "license": "MIT",
6
- "author": "dushaobindoudou",
3
+ "version": "0.2.11",
4
+ "description": "Multi-bot workbench for DeepSeek Harness: bridges the sdk-bots orchestration gateway (group chats, single bots) into the dsh web shell with official-styled UI.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./client": "./lib/client.js",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
24
+ "prepare": "pnpm run build",
25
+ "typecheck": "tsc -p tsconfig.json --noEmit",
26
+ "test": "vitest run",
27
+ "test:integration": "node scripts/integration-test.mjs",
28
+ "test:dsh-smoke": "bash scripts/dsh-smoke.sh"
29
+ },
7
30
  "keywords": [
8
- "dsh",
9
- "dsh-plugin",
10
31
  "deepseek-harness",
11
- "cordis-plugin",
12
- "bot",
13
- "bots"
32
+ "dsh-plugin",
33
+ "bots",
34
+ "multibot",
35
+ "cordis"
14
36
  ],
15
- "repository": {
16
- "type": "git",
17
- "url": "https://github.com/dushaobindoudou/dsh-plugin.git",
18
- "directory": "packages/dsh-bots"
37
+ "license": "MIT",
38
+ "peerDependencies": {
39
+ "@deepseek-ai/cordis": "^4.0.1",
40
+ "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.6"
41
+ },
42
+ "devDependencies": {
43
+ "@deepseek-ai/cordis": "^4.0.1",
44
+ "@deepseek-ai/dsh-typert-protocol": "0.1.1-rc.2",
45
+ "@types/node": "^22.20.0",
46
+ "typescript": "^6.0.3",
47
+ "vitest": "^4.1.8"
19
48
  },
20
- "homepage": "https://github.com/dushaobindoudou/dsh-plugin/tree/main/packages/dsh-bots#readme"
49
+ "dsh": {
50
+ "bundle": {
51
+ "patch": "./cordis.patch.yml"
52
+ },
53
+ "client": {
54
+ "platform": "web"
55
+ }
56
+ }
21
57
  }