@dsh-enhanced/assistant-isolation 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/cordis.patch.yml +3 -0
  4. package/lib/audit-archive.d.ts +52 -0
  5. package/lib/audit-archive.d.ts.map +1 -0
  6. package/lib/audit-archive.js +775 -0
  7. package/lib/audit-archive.js.map +1 -0
  8. package/lib/cleanup-receipt.d.ts +26 -0
  9. package/lib/cleanup-receipt.d.ts.map +1 -0
  10. package/lib/cleanup-receipt.js +131 -0
  11. package/lib/cleanup-receipt.js.map +1 -0
  12. package/lib/cli.d.ts +16 -0
  13. package/lib/cli.d.ts.map +1 -0
  14. package/lib/cli.js +183 -0
  15. package/lib/cli.js.map +1 -0
  16. package/lib/config.d.ts +20 -0
  17. package/lib/config.d.ts.map +1 -0
  18. package/lib/config.js +61 -0
  19. package/lib/config.js.map +1 -0
  20. package/lib/daemon-binding.d.ts +13 -0
  21. package/lib/daemon-binding.d.ts.map +1 -0
  22. package/lib/daemon-binding.js +120 -0
  23. package/lib/daemon-binding.js.map +1 -0
  24. package/lib/diagnostics.d.ts +19 -0
  25. package/lib/diagnostics.d.ts.map +1 -0
  26. package/lib/diagnostics.js +122 -0
  27. package/lib/diagnostics.js.map +1 -0
  28. package/lib/index.d.ts +19 -0
  29. package/lib/index.d.ts.map +1 -0
  30. package/lib/index.js +13 -0
  31. package/lib/index.js.map +1 -0
  32. package/lib/ledger.d.ts +88 -0
  33. package/lib/ledger.d.ts.map +1 -0
  34. package/lib/ledger.js +623 -0
  35. package/lib/ledger.js.map +1 -0
  36. package/lib/probe.d.ts +5 -0
  37. package/lib/probe.d.ts.map +1 -0
  38. package/lib/probe.js +45 -0
  39. package/lib/probe.js.map +1 -0
  40. package/lib/reconcile.d.ts +5 -0
  41. package/lib/reconcile.d.ts.map +1 -0
  42. package/lib/reconcile.js +29 -0
  43. package/lib/reconcile.js.map +1 -0
  44. package/lib/runner.d.ts +12 -0
  45. package/lib/runner.d.ts.map +1 -0
  46. package/lib/runner.js +201 -0
  47. package/lib/runner.js.map +1 -0
  48. package/lib/runtime-witness.d.ts +32 -0
  49. package/lib/runtime-witness.d.ts.map +1 -0
  50. package/lib/runtime-witness.js +225 -0
  51. package/lib/runtime-witness.js.map +1 -0
  52. package/lib/service.d.ts +47 -0
  53. package/lib/service.d.ts.map +1 -0
  54. package/lib/service.js +361 -0
  55. package/lib/service.js.map +1 -0
  56. package/lib/storage-policy.d.ts +7 -0
  57. package/lib/storage-policy.d.ts.map +1 -0
  58. package/lib/storage-policy.js +82 -0
  59. package/lib/storage-policy.js.map +1 -0
  60. package/lib/storage.d.ts +18 -0
  61. package/lib/storage.d.ts.map +1 -0
  62. package/lib/storage.js +102 -0
  63. package/lib/storage.js.map +1 -0
  64. package/lib/tools.d.ts +4 -0
  65. package/lib/tools.d.ts.map +1 -0
  66. package/lib/tools.js +49 -0
  67. package/lib/tools.js.map +1 -0
  68. package/lib/types.d.ts +143 -0
  69. package/lib/types.d.ts.map +1 -0
  70. package/lib/types.js +2 -0
  71. package/lib/types.js.map +1 -0
  72. package/lib/verifier-runner.d.ts +21 -0
  73. package/lib/verifier-runner.d.ts.map +1 -0
  74. package/lib/verifier-runner.js +192 -0
  75. package/lib/verifier-runner.js.map +1 -0
  76. package/lib/version.d.ts +2 -0
  77. package/lib/version.d.ts.map +1 -0
  78. package/lib/version.js +2 -0
  79. package/lib/version.js.map +1 -0
  80. package/lib/workspace.d.ts +6 -0
  81. package/lib/workspace.d.ts.map +1 -0
  82. package/lib/workspace.js +140 -0
  83. package/lib/workspace.js.map +1 -0
  84. package/package.json +105 -0
  85. package/runtime/supervisor.mjs +290 -0
@@ -0,0 +1,140 @@
1
+ import { chmod, lstat, mkdir, open } from 'node:fs/promises';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ const maxText = 16_384;
4
+ const control = /[\p{Cc}]/u;
5
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
6
+ function fail(message) { throw new Error(`invalid isolation workspace: ${message}`); }
7
+ const positive = (value) => Number.isSafeInteger(value) && value > 0;
8
+ const bytes = (value) => Buffer.byteLength(value, 'utf8');
9
+ const pathOrder = (left, right) => left < right ? -1 : left > right ? 1 : 0;
10
+ function limitsInput(value) {
11
+ if (!value || typeof value !== 'object' || Object.keys(value).length !== 10
12
+ || !positive(value.maxDurationMs) || !positive(value.maxInputBytes) || !positive(value.maxOutputBytes) || !positive(value.maxArtifactBytes)
13
+ || !positive(value.workspaceMiB) || !positive(value.workspaceInodes) || !positive(value.maxFiles) || !positive(value.memoryMiB) || !positive(value.pidsLimit) || !Number.isFinite(value.cpus) || value.cpus <= 0)
14
+ fail('invalid limits');
15
+ return value;
16
+ }
17
+ function scalar(value, name, maximum = maxText) {
18
+ if (typeof value !== 'string' || value.length === 0 || value.length > maximum || control.test(value))
19
+ fail(`invalid ${name}`);
20
+ return value;
21
+ }
22
+ /** Canonical relative path syntax shared by input staging and artifact collection. */
23
+ function pathInput(value) {
24
+ const path = scalar(value, 'path', 4096);
25
+ if (isAbsolute(path) || path.includes('\\') || path.startsWith('./') || path.endsWith('/') || path.includes('//'))
26
+ fail('non-canonical path');
27
+ const parts = path.split('/');
28
+ if (parts.some(part => part.length === 0 || part === '.' || part === '..'))
29
+ fail('unsafe path');
30
+ return path;
31
+ }
32
+ function canonicalPaths(values, name, maximum) {
33
+ if (!Array.isArray(values) || values.length > maximum)
34
+ fail(`invalid ${name}`);
35
+ const paths = values.map(pathInput).sort(pathOrder);
36
+ if (paths.some((path, index) => index > 0 && path === paths[index - 1]))
37
+ fail(`duplicate ${name}`);
38
+ return paths;
39
+ }
40
+ function assertNoFileParentConflict(files) {
41
+ for (let index = 1; index < files.length; index += 1)
42
+ if (files[index].path.startsWith(`${files[index - 1].path}/`))
43
+ fail('file parent conflict');
44
+ }
45
+ /** Rejects model-controlled ambiguity before hashing, auditing, or staging a request. */
46
+ export function normalizeRequest(request, limits) {
47
+ const configured = limitsInput(limits);
48
+ if (!request || typeof request !== 'object' || !Object.keys(request).every(key => ['grantId', 'idempotencyKey', 'command', 'files', 'artifacts', 'timeoutMs'].includes(key))
49
+ || !scalar(request.grantId, 'grant id', 256) || !scalar(request.idempotencyKey, 'idempotency key', 256)
50
+ || typeof request.command !== 'string' || request.command.length === 0 || request.command.includes('\0'))
51
+ fail('invalid request');
52
+ const timeoutMs = request.timeoutMs === undefined ? configured.maxDurationMs : request.timeoutMs;
53
+ if (!positive(timeoutMs) || timeoutMs > configured.maxDurationMs)
54
+ fail('invalid timeout');
55
+ const rawFiles = request.files === undefined ? [] : request.files;
56
+ if (!Array.isArray(rawFiles) || rawFiles.length > configured.maxFiles)
57
+ fail('invalid files');
58
+ const files = rawFiles.map(file => {
59
+ if (!file || typeof file !== 'object' || !Object.keys(file).every(key => key === 'path' || key === 'content') || Object.keys(file).length !== 2 || typeof file.content !== 'string')
60
+ fail('invalid file');
61
+ return { path: pathInput(file.path), content: file.content };
62
+ }).sort((left, right) => pathOrder(left.path, right.path));
63
+ if (files.some((file, index) => index > 0 && file.path === files[index - 1].path))
64
+ fail('duplicate files');
65
+ assertNoFileParentConflict(files);
66
+ const artifacts = request.artifacts === undefined ? [] : canonicalPaths(request.artifacts, 'artifacts', configured.maxFiles);
67
+ const inputBytes = bytes(request.command) + files.reduce((total, file) => total + bytes(file.path) + bytes(file.content), 0) + artifacts.reduce((total, path) => total + bytes(path), 0);
68
+ if (!Number.isSafeInteger(inputBytes) || inputBytes > configured.maxInputBytes)
69
+ fail('input exceeds limit');
70
+ const inputEntries = new Set();
71
+ for (const file of files) {
72
+ const parts = file.path.split('/');
73
+ for (let index = 1; index <= parts.length; index++)
74
+ inputEntries.add(parts.slice(0, index).join('/'));
75
+ }
76
+ if (inputEntries.size + 1 > configured.workspaceInodes)
77
+ fail('input exceeds workspace inode limit');
78
+ if (files.reduce((total, file) => total + bytes(file.content), 0) > configured.workspaceMiB * 1_048_576)
79
+ fail('input exceeds workspace byte limit');
80
+ return Object.freeze({ grantId: request.grantId, idempotencyKey: request.idempotencyKey, command: request.command, ...(files.length ? { files: files.map(file => Object.freeze(file)) } : {}), ...(artifacts.length ? { artifacts } : {}), timeoutMs });
81
+ }
82
+ async function directory(path, create) {
83
+ if (create)
84
+ await mkdir(path, { recursive: true, mode: 0o700 });
85
+ const entry = await lstat(path);
86
+ if (!entry.isDirectory() || entry.isSymbolicLink())
87
+ fail('directory is not private');
88
+ await chmod(path, 0o700);
89
+ }
90
+ async function privateParents(root, relativePath) {
91
+ let current = root;
92
+ const parts = relativePath.split('/');
93
+ parts.pop();
94
+ for (const part of parts) {
95
+ current = resolve(current, part);
96
+ await mkdir(current, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST')
97
+ throw error; });
98
+ await directory(current, false);
99
+ }
100
+ return current;
101
+ }
102
+ /** Creates a fresh, job-only workspace. It deliberately never copies a project checkout. */
103
+ export async function stageWorkspace(stateRoot, jobId, request) {
104
+ if (!isAbsolute(stateRoot) || !uuid.test(jobId))
105
+ fail('invalid staging root or job id');
106
+ await directory(stateRoot, true);
107
+ const workspaces = resolve(stateRoot, 'workspaces');
108
+ await directory(workspaces, true);
109
+ const jobRoot = resolve(workspaces, jobId);
110
+ try {
111
+ await mkdir(jobRoot, { mode: 0o700 });
112
+ }
113
+ catch (error) {
114
+ if (error.code === 'EEXIST')
115
+ fail('workspace collision');
116
+ throw error;
117
+ }
118
+ await directory(jobRoot, false);
119
+ const workspace = resolve(jobRoot, 'workspace');
120
+ await mkdir(workspace, { mode: 0o700 });
121
+ await directory(workspace, false);
122
+ const files = request.files ?? [];
123
+ for (const file of files) {
124
+ const path = pathInput(file.path);
125
+ if (typeof file.content !== 'string')
126
+ fail('invalid staged file');
127
+ const parent = await privateParents(workspace, path);
128
+ const target = resolve(parent, path.slice(path.lastIndexOf('/') + 1));
129
+ const handle = await open(target, 'wx', 0o600);
130
+ try {
131
+ await handle.writeFile(file.content, 'utf8');
132
+ await handle.chmod(0o600);
133
+ }
134
+ finally {
135
+ await handle.close();
136
+ }
137
+ }
138
+ return workspace;
139
+ }
140
+ //# sourceMappingURL=workspace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "@dsh-enhanced/assistant-isolation",
3
+ "version": "0.1.30",
4
+ "description": "Finite owner-authorized offline Linux Docker jobs with durable audit and external revocation.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./lib/index.js",
8
+ "types": "./lib/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/index.d.ts",
12
+ "default": "./lib/index.js"
13
+ },
14
+ "./cordis.patch.yml": "./cordis.patch.yml",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "lib",
19
+ "runtime",
20
+ "cordis.patch.yml",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "engines": {
25
+ "node": "^22.19.0 || >=24.0.0"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/22-ai-00/dsh-enhanced.git",
33
+ "directory": "plugins/assistant-isolation"
34
+ },
35
+ "keywords": [
36
+ "deepseek",
37
+ "deepseek-harness",
38
+ "dsh-plugin"
39
+ ],
40
+ "dsh": {
41
+ "bundle": {
42
+ "patch": "./cordis.patch.yml"
43
+ }
44
+ },
45
+ "peerDependencies": {
46
+ "@deepseek-ai/cordis": "^4.0.1",
47
+ "@deepseek-ai/dsh-agent": ">=0.1.2-rc.1 <0.2.0",
48
+ "@deepseek-ai/dsh-session": ">=0.1.2-rc.1 <0.2.0",
49
+ "@deepseek-ai/dsh-tools": ">=0.1.2-rc.1 <0.2.0",
50
+ "@dsh-enhanced/assistant-delivery": ">0.1.7 <0.2.0",
51
+ "@dsh-enhanced/assistant-policy": ">0.1.7 <0.2.0",
52
+ "@deepseek-ai/dsh-system-prompt": ">=0.1.2-rc.1 <0.2.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "@deepseek-ai/cordis": {
56
+ "optional": true
57
+ },
58
+ "@deepseek-ai/dsh-agent": {
59
+ "optional": true
60
+ },
61
+ "@deepseek-ai/dsh-session": {
62
+ "optional": true
63
+ },
64
+ "@deepseek-ai/dsh-tools": {
65
+ "optional": true
66
+ },
67
+ "@dsh-enhanced/assistant-delivery": {
68
+ "optional": true
69
+ },
70
+ "@dsh-enhanced/assistant-policy": {
71
+ "optional": true
72
+ },
73
+ "@deepseek-ai/dsh-system-prompt": {
74
+ "optional": true
75
+ }
76
+ },
77
+ "devDependencies": {
78
+ "@deepseek-ai/cordis": "4.0.2",
79
+ "@types/node": "22.20.0",
80
+ "typescript": "6.0.3",
81
+ "vitest": "4.1.8",
82
+ "@deepseek-ai/dsh-agent": "0.1.2-rc.1",
83
+ "@deepseek-ai/dsh-session": "0.1.2-rc.1",
84
+ "@deepseek-ai/dsh-tools": "0.1.2-rc.1",
85
+ "@deepseek-ai/dsh-agent-loop": "0.1.2-rc.1",
86
+ "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
87
+ "@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
88
+ "@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
89
+ "@deepseek-ai/dsh-user-approval": "0.1.2-rc.1",
90
+ "@dsh-enhanced/assistant-policy": "0.1.30",
91
+ "@dsh-enhanced/assistant-delivery": "0.1.30"
92
+ },
93
+ "dependencies": {
94
+ "@deepseek-ai/schemastery": "3.18.2",
95
+ "@dsh-enhanced/task-acceptance-contract": "0.1.30"
96
+ },
97
+ "bin": {
98
+ "dsh-isolation": "./lib/cli.js"
99
+ },
100
+ "scripts": {
101
+ "build": "tsc -p tsconfig.build.json",
102
+ "test": "vitest run",
103
+ "typecheck": "tsc -p tsconfig.json --noEmit"
104
+ }
105
+ }
@@ -0,0 +1,290 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { chmod, mkdtemp, rm } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+
6
+ const SOCKET = 'unix:///var/run/docker.sock'
7
+ const OPERATION_TIMEOUT_MS = 10_000
8
+ const NAME = /^dsh-isolation-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/
9
+ const IMAGE = /^sha256:[0-9a-f]{64}$/
10
+ let config
11
+ let workerOwned = false
12
+ let keeperOwned = false
13
+ let volumeOwned = false
14
+ let started = false
15
+ let finalizing = false
16
+ let complete = false
17
+ let deadlineTimer
18
+ let stdout = ''
19
+ let stderr = ''
20
+ let dockerDirectory
21
+ let provisioning = Promise.resolve()
22
+ let cancelReason
23
+ let creationAmbiguous = false
24
+ let mutationAmbiguous = false
25
+ let requestsSettled = true
26
+ const activeCommands = new Set()
27
+ const workerName = () => config.containerName
28
+ const keeperName = () => `${workerName()}-keeper`
29
+ const volumeName = () => `${workerName()}-workspace`
30
+
31
+ function environment() {
32
+ return { PATH: process.env.PATH || '/usr/bin:/bin', LANG: 'C', LC_ALL: 'C' }
33
+ }
34
+ function send(message) {
35
+ if (process.connected) { try { process.send(message) } catch {} }
36
+ }
37
+ function decode(buffer) {
38
+ try { return new TextDecoder('utf-8', { fatal: true }).decode(buffer) }
39
+ catch { return undefined }
40
+ }
41
+ function succeeded(result) {
42
+ return result.code === 0 && !result.timeout && !result.overflow
43
+ }
44
+ function docker(args, { outputLimit = 65_536, timeoutMs = OPERATION_TIMEOUT_MS } = {}) {
45
+ return new Promise(resolve => {
46
+ let child
47
+ let out = Buffer.alloc(0)
48
+ let err = Buffer.alloc(0)
49
+ let overflow = false
50
+ let timeout = false
51
+ let settled = false
52
+ let timer
53
+ const finish = result => {
54
+ if (settled) return
55
+ settled = true
56
+ if (timer !== undefined) clearTimeout(timer)
57
+ const mutating = ['create', 'start', 'cp', 'exec'].includes(args[0]) || (args[0] === 'volume' && args[1] === 'create')
58
+ if (mutating && (timeout || overflow || result.code === null)) mutationAmbiguous = true
59
+ if (mutating && (timeout || overflow || result.code !== 0 || result.signal || result.error)) requestsSettled = false
60
+ const decoded = decode(out)
61
+ resolve({ ...result, stdout: decoded ?? '', stderr: decode(err) ?? '', stdoutBytes: out.length,
62
+ stdoutUtf8: decoded !== undefined, overflow, timeout })
63
+ }
64
+ try {
65
+ child = spawn(config.dockerPath, ['--config', dockerDirectory, '-H', SOCKET, ...args], {
66
+ shell: false, env: environment(), stdio: ['ignore', 'pipe', 'pipe'],
67
+ })
68
+ } catch (error) { finish({ code: null, error: String(error) }); return }
69
+ activeCommands.add(child)
70
+ child.once('close', () => activeCommands.delete(child))
71
+ const collect = (target, chunk) => {
72
+ const kept = chunk.subarray(0, Math.max(0, outputLimit - out.length - err.length))
73
+ if (target === 'out') out = Buffer.concat([out, kept])
74
+ else err = Buffer.concat([err, kept])
75
+ if (kept.length !== chunk.length && !overflow) { overflow = true; child.kill('SIGTERM') }
76
+ }
77
+ child.stdout.on('data', chunk => collect('out', chunk))
78
+ child.stderr.on('data', chunk => collect('err', chunk))
79
+ child.once('error', error => finish({ code: null, error: error.message }))
80
+ // close, not exit: collect the final pipe bytes before accepting an artifact.
81
+ child.once('close', (code, signal) => finish({ code, signal }))
82
+ timer = setTimeout(() => { timeout = true; child.kill('SIGKILL') }, timeoutMs)
83
+ timer.unref()
84
+ })
85
+ }
86
+ async function absent(type, name) {
87
+ const result = await docker(['inspect', '--type', type, name])
88
+ return !result.timeout && !result.overflow && result.code !== 0
89
+ && /no such (object|container|volume)/i.test(result.stderr)
90
+ }
91
+ async function erase(type, name) {
92
+ for (let attempt = 0; attempt < 3; attempt++) {
93
+ const result = await docker(type === 'volume' ? ['volume', 'rm', name] : ['rm', '-f', name])
94
+ if (succeeded(result) || await absent(type, name)) return true
95
+ if (attempt < 2) await new Promise(resolve => setTimeout(resolve, 100))
96
+ }
97
+ return false
98
+ }
99
+ async function removeWorker() {
100
+ if (!workerOwned) return true
101
+ await docker(['kill', workerName()])
102
+ return erase('container', workerName())
103
+ }
104
+ async function removeStorage() {
105
+ const keeperRemoved = !keeperOwned || await erase('container', keeperName())
106
+ const volumeRemoved = !volumeOwned || await erase('volume', volumeName())
107
+ return keeperRemoved && volumeRemoved
108
+ }
109
+ function artifactPath(path) {
110
+ return typeof path === 'string' && path.length > 0 && path.length <= 4096 && !path.startsWith('/')
111
+ && !/[\p{Cc}\\]/u.test(path) && path.split('/').every(part => part && part !== '.' && part !== '..')
112
+ }
113
+ function exportInterruption() {
114
+ if (cancelReason !== undefined) return cancelReason
115
+ return Date.now() >= config.deadline ? 'deadline-expired' : undefined
116
+ }
117
+ function exportTimeout() {
118
+ return Math.max(1, Math.min(OPERATION_TIMEOUT_MS, config.deadline - Date.now()))
119
+ }
120
+ async function stat(path, regular) {
121
+ if (exportInterruption()) return undefined
122
+ const result = await docker(['exec', keeperName(), '/bin/busybox', 'stat', '-c', '%f:%h:%s', '--', path], { timeoutMs: exportTimeout() })
123
+ if (!succeeded(result) || exportInterruption() || !/^[0-9a-fA-F]+:\d+:\d+\n?$/.test(result.stdout)) return undefined
124
+ const fields = result.stdout.trim().split(':')
125
+ const mode = Number.parseInt(fields[0], 16)
126
+ const links = Number(fields[1])
127
+ const size = Number(fields[2])
128
+ if (![mode, links, size].every(Number.isSafeInteger)) return undefined
129
+ // BusyBox stat without -L examines the link itself. Each parent is examined
130
+ // separately after worker removal, so no worker can race path resolution.
131
+ if (regular ? (mode & 0o170000) !== 0o100000 || links !== 1 : (mode & 0o170000) !== 0o40000) return undefined
132
+ return { size }
133
+ }
134
+ async function exportArtifacts() {
135
+ const files = []
136
+ let total = 0
137
+ for (const path of config.artifacts) {
138
+ if (exportInterruption() || !artifactPath(path)) return undefined
139
+ let parent = '/workspace'
140
+ if (!await stat(parent, false)) return undefined
141
+ const parts = path.split('/')
142
+ for (const part of parts.slice(0, -1)) {
143
+ parent += `/${part}`
144
+ if (!await stat(parent, false)) return undefined
145
+ }
146
+ const target = `/workspace/${path}`
147
+ const entry = await stat(target, true)
148
+ if (!entry || entry.size > config.limits.maxArtifactBytes - total || exportInterruption()) return undefined
149
+ const result = await docker(['exec', keeperName(), '/bin/busybox', 'cat', '--', target], {
150
+ outputLimit: entry.size, timeoutMs: exportTimeout(),
151
+ })
152
+ if (!succeeded(result) || !result.stdoutUtf8 || result.stdoutBytes !== entry.size || exportInterruption()) return undefined
153
+ total += entry.size
154
+ files.push({ path, content: result.stdout })
155
+ }
156
+ return files
157
+ }
158
+ async function finish(status, reason, exitCode) {
159
+ if (finalizing) return
160
+ finalizing = true
161
+ if (deadlineTimer !== undefined) clearTimeout(deadlineTimer)
162
+ // Cleanup cannot overtake a create/copy request still owned by this process.
163
+ await provisioning
164
+ const workerRemoved = await removeWorker()
165
+ let artifacts = []
166
+ if (status === 'succeeded' && workerRemoved && keeperOwned) {
167
+ const exported = await exportArtifacts()
168
+ const interrupted = exportInterruption()
169
+ if (interrupted !== undefined) {
170
+ status = interrupted === 'deadline-expired' ? 'timed-out' : 'cancelled'
171
+ reason = interrupted
172
+ } else if (exported === undefined) {
173
+ status = 'failed'
174
+ reason = 'artifact-export-rejected'
175
+ } else artifacts = exported
176
+ }
177
+ const storageRemoved = await removeStorage()
178
+ // A final receipt must not outrun an attached start or other CLI still alive.
179
+ // No new create can follow provisioning; begin() stops when finalizing is set.
180
+ const outstanding = [...activeCommands]
181
+ await Promise.all(outstanding.map(child => new Promise(resolve => child.once('close', resolve))))
182
+ // A timed-out CLI can leave an in-flight daemon create request. A momentary
183
+ // absence is not a release receipt for such a request; retain its reservation.
184
+ const quiescent = workerRemoved && storageRemoved && !mutationAmbiguous
185
+ if (creationAmbiguous) reason = 'docker-creation-unconfirmed'
186
+ else if (mutationAmbiguous) reason = 'docker-mutation-unconfirmed'
187
+ if (!quiescent || status !== 'succeeded') artifacts = []
188
+ if (dockerDirectory !== undefined) await rm(dockerDirectory, { recursive: true, force: true }).catch(() => undefined)
189
+ complete = true
190
+ send({ type: 'result', settlementProtocol: 'all-cli-closed/v1', requestsSettled: requestsSettled && !mutationAmbiguous, result: { status: quiescent ? status : 'unknown', quiescent,
191
+ ...(exitCode === undefined ? {} : { exitCode }), stdout, stderr, artifacts,
192
+ ...(reason === undefined ? {} : { reason }) } })
193
+ if (process.connected) process.disconnect()
194
+ }
195
+ async function begin() {
196
+ const attached = await docker(['start', '--attach', workerName()], {
197
+ outputLimit: config.limits.maxOutputBytes, timeoutMs: Math.max(1, config.deadline - Date.now()),
198
+ })
199
+ stdout = attached.stdout
200
+ stderr = attached.stderr
201
+ if (finalizing) return
202
+ if (attached.overflow) { await finish('failed', 'output-limit-exceeded'); return }
203
+ if (attached.timeout) { await finish('unknown', 'docker-start-timeout'); return }
204
+ const inspected = await docker(['inspect', '--type', 'container', '--format', '{{json .State}}', workerName()])
205
+ let state
206
+ try { state = JSON.parse(inspected.stdout) } catch {}
207
+ if (!succeeded(inspected) || typeof state?.Running !== 'boolean' || state.Running || !Number.isSafeInteger(state.ExitCode)) {
208
+ await finish('unknown', 'container-state-unconfirmed')
209
+ return
210
+ }
211
+ await finish(state.ExitCode === 0 ? 'succeeded' : 'failed', state.ExitCode === 0 ? undefined : 'container-exited-nonzero', state.ExitCode)
212
+ }
213
+ function valid(input) {
214
+ return input && NAME.test(input.containerName) && IMAGE.test(input.image)
215
+ && typeof input.dockerPath === 'string' && input.dockerPath.startsWith('/')
216
+ && typeof input.workspacePath === 'string' && input.workspacePath.startsWith('/') && !/[\p{Cc},]/u.test(input.workspacePath)
217
+ && typeof input.command === 'string' && Number.isSafeInteger(input.deadline)
218
+ && input.limits && Object.values(input.limits).every(value => typeof value === 'number' && Number.isFinite(value) && value > 0)
219
+ && Array.isArray(input.artifacts) && input.artifacts.length <= input.limits.maxFiles && input.artifacts.every(artifactPath)
220
+ }
221
+ function sandbox(keeper = false) {
222
+ const memory = keeper ? '32m' : `${Math.floor(config.limits.memoryMiB)}m`
223
+ const flags = ['--network', 'none', '--read-only', '--log-driver', 'none', '--cap-drop', 'ALL',
224
+ '--security-opt', 'no-new-privileges', '--user', `${process.getuid()}:${process.getgid()}`,
225
+ '--pids-limit', keeper ? '16' : String(Math.floor(config.limits.pidsLimit)), '--memory', memory, '--memory-swap', memory,
226
+ '--shm-size', '1m', '--cpus', String(config.limits.cpus)]
227
+ if (!keeper) {
228
+ const tmpMiB = Math.max(1, Math.min(64, Math.floor(config.limits.memoryMiB / 4)))
229
+ flags.push('--tmpfs', `/tmp:rw,nosuid,nodev,size=${tmpMiB}m,nr_inodes=${Math.floor(config.limits.workspaceInodes)}`)
230
+ }
231
+ return flags
232
+ }
233
+ function creationResult(result) {
234
+ if (result.timeout || result.overflow || result.code === null) creationAmbiguous = true
235
+ return !finalizing && succeeded(result)
236
+ }
237
+ async function provision() {
238
+ const inspected = await docker(['image', 'inspect', '--format', '{{json .Config.Volumes}}', config.image])
239
+ if (finalizing || !succeeded(inspected) || !['null', '{}'].includes(inspected.stdout.trim())) return false
240
+ // Mark each unique resource owned before sending its create request, including
241
+ // the timeout / lost-ack window. No resource name comes from the model.
242
+ volumeOwned = true
243
+ const options = `o=size=${Math.floor(config.limits.workspaceMiB)}m,nr_inodes=${Math.floor(config.limits.workspaceInodes)},uid=${process.getuid()},gid=${process.getgid()},mode=0700,nosuid,nodev`
244
+ if (!creationResult(await docker(['volume', 'create', '--driver', 'local', '--opt', 'type=tmpfs', '--opt', 'device=tmpfs', '--opt', options, volumeName()]))) return false
245
+ keeperOwned = true
246
+ if (!creationResult(await docker(['create', '--pull', 'never', '--name', keeperName(), ...sandbox(true),
247
+ '--mount', `type=volume,src=${volumeName()},dst=/workspace,volume-nocopy`, '--entrypoint', '/bin/busybox', config.image, 'sleep', '3600']))) return false
248
+ if (!succeeded(await docker(['start', keeperName()])) || finalizing) return false
249
+ if (!succeeded(await docker(['cp', '-a', `${config.workspacePath}/.`, `${keeperName()}:/workspace`])) || finalizing) return false
250
+ workerOwned = true
251
+ return creationResult(await docker(['create', '--pull', 'never', '--name', workerName(), ...sandbox(),
252
+ '--mount', `type=volume,src=${volumeName()},dst=/workspace,volume-nocopy`, '--workdir', '/workspace',
253
+ '--entrypoint', '/bin/sh', config.image, '-c', config.command]))
254
+ }
255
+ async function configure(input) {
256
+ if (config !== undefined || !valid(input)) { send({ type: 'error', reason: 'invalid-supervisor-config' }); return }
257
+ if (process.platform !== 'linux' || typeof process.getuid !== 'function' || typeof process.getgid !== 'function' || process.getuid() === 0) {
258
+ send({ type: 'error', reason: 'non-root-linux-host-required' })
259
+ return
260
+ }
261
+ config = input
262
+ try {
263
+ dockerDirectory = await mkdtemp(join(tmpdir(), 'dsh-isolation-docker-'))
264
+ await chmod(dockerDirectory, 0o700)
265
+ } catch { await finish('failed', 'docker-config-directory-unavailable'); return }
266
+ if (finalizing) { await rm(dockerDirectory, { recursive: true, force: true }).catch(() => undefined); return }
267
+ if (config.deadline <= Date.now()) { await finish('timed-out', 'deadline-expired-before-create'); return }
268
+ deadlineTimer = setTimeout(() => { void finish('timed-out', 'deadline-expired') }, config.deadline - Date.now())
269
+ deadlineTimer.unref()
270
+ let markSettled
271
+ provisioning = new Promise(resolve => { markSettled = resolve })
272
+ let prepared = false
273
+ try { prepared = await provision() } finally { markSettled() }
274
+ if (finalizing) return
275
+ if (!prepared) { await finish('failed', 'docker-create-failed'); return }
276
+ if (!process.connected) { await finish('unknown', 'parent-disconnected-before-authorization'); return }
277
+ send({ type: 'ready' })
278
+ }
279
+ process.on('message', message => {
280
+ if (!message || typeof message !== 'object') return
281
+ if (message.type === 'configure') void configure(message.config)
282
+ else if (message.type === 'start' && config !== undefined && !finalizing && !started) { started = true; void begin() }
283
+ else if (message.type === 'cancel' && config !== undefined) {
284
+ cancelReason = typeof message.reason === 'string' ? message.reason : 'cancelled'
285
+ void finish(cancelReason === 'deadline-expired' ? 'timed-out' : 'cancelled', cancelReason)
286
+ }
287
+ })
288
+ process.on('disconnect', () => {
289
+ if (config !== undefined && !complete) { cancelReason = 'parent-ipc-disconnected'; void finish('unknown', cancelReason) }
290
+ })