@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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/cordis.patch.yml +3 -0
- package/lib/audit-archive.d.ts +52 -0
- package/lib/audit-archive.d.ts.map +1 -0
- package/lib/audit-archive.js +775 -0
- package/lib/audit-archive.js.map +1 -0
- package/lib/cleanup-receipt.d.ts +26 -0
- package/lib/cleanup-receipt.d.ts.map +1 -0
- package/lib/cleanup-receipt.js +131 -0
- package/lib/cleanup-receipt.js.map +1 -0
- package/lib/cli.d.ts +16 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +183 -0
- package/lib/cli.js.map +1 -0
- package/lib/config.d.ts +20 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +61 -0
- package/lib/config.js.map +1 -0
- package/lib/daemon-binding.d.ts +13 -0
- package/lib/daemon-binding.d.ts.map +1 -0
- package/lib/daemon-binding.js +120 -0
- package/lib/daemon-binding.js.map +1 -0
- package/lib/diagnostics.d.ts +19 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +122 -0
- package/lib/diagnostics.js.map +1 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -0
- package/lib/ledger.d.ts +88 -0
- package/lib/ledger.d.ts.map +1 -0
- package/lib/ledger.js +623 -0
- package/lib/ledger.js.map +1 -0
- package/lib/probe.d.ts +5 -0
- package/lib/probe.d.ts.map +1 -0
- package/lib/probe.js +45 -0
- package/lib/probe.js.map +1 -0
- package/lib/reconcile.d.ts +5 -0
- package/lib/reconcile.d.ts.map +1 -0
- package/lib/reconcile.js +29 -0
- package/lib/reconcile.js.map +1 -0
- package/lib/runner.d.ts +12 -0
- package/lib/runner.d.ts.map +1 -0
- package/lib/runner.js +201 -0
- package/lib/runner.js.map +1 -0
- package/lib/runtime-witness.d.ts +32 -0
- package/lib/runtime-witness.d.ts.map +1 -0
- package/lib/runtime-witness.js +225 -0
- package/lib/runtime-witness.js.map +1 -0
- package/lib/service.d.ts +47 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +361 -0
- package/lib/service.js.map +1 -0
- package/lib/storage-policy.d.ts +7 -0
- package/lib/storage-policy.d.ts.map +1 -0
- package/lib/storage-policy.js +82 -0
- package/lib/storage-policy.js.map +1 -0
- package/lib/storage.d.ts +18 -0
- package/lib/storage.d.ts.map +1 -0
- package/lib/storage.js +102 -0
- package/lib/storage.js.map +1 -0
- package/lib/tools.d.ts +4 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +49 -0
- package/lib/tools.js.map +1 -0
- package/lib/types.d.ts +143 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/verifier-runner.d.ts +21 -0
- package/lib/verifier-runner.d.ts.map +1 -0
- package/lib/verifier-runner.js +192 -0
- package/lib/verifier-runner.js.map +1 -0
- package/lib/version.d.ts +2 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +2 -0
- package/lib/version.js.map +1 -0
- package/lib/workspace.d.ts +6 -0
- package/lib/workspace.d.ts.map +1 -0
- package/lib/workspace.js +140 -0
- package/lib/workspace.js.map +1 -0
- package/package.json +105 -0
- package/runtime/supervisor.mjs +290 -0
package/lib/workspace.js
ADDED
|
@@ -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":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAG/C,MAAM,OAAO,GAAG,MAAM,CAAA;AACtB,MAAM,OAAO,GAAG,WAAW,CAAA;AAC3B,MAAM,IAAI,GAAG,6EAA6E,CAAA;AAC1F,SAAS,IAAI,CAAC,OAAe,IAAW,MAAM,IAAI,KAAK,CAAC,gCAAgC,OAAO,EAAE,CAAC,CAAA,CAAC,CAAC;AACpG,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAmB,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAK,KAAgB,GAAG,CAAC,CAAA;AAC1G,MAAM,KAAK,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;AACzE,MAAM,SAAS,GAAG,CAAC,IAAY,EAAE,KAAa,EAAU,EAAE,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAEnG,SAAS,WAAW,CAAC,KAAsB;IACzC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,EAAE;WACtE,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,gBAAgB,CAAC;WACxI,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,CAAC;QAAE,IAAI,CAAC,gBAAgB,CAAC,CAAA;IAC1O,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,MAAM,CAAC,KAAc,EAAE,IAAY,EAAE,OAAO,GAAG,OAAO;IAC7D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAA;IAC7H,OAAO,KAAK,CAAA;AACd,CAAC;AAED,sFAAsF;AACtF,SAAS,SAAS,CAAC,KAAc;IAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;IACxC,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,oBAAoB,CAAC,CAAA;IAC7I,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC7B,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI,CAAC;QAAE,IAAI,CAAC,aAAa,CAAC,CAAA;IAC/F,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,cAAc,CAAC,MAAe,EAAE,IAAY,EAAE,OAAe;IACpE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,OAAO;QAAE,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAA;IAC9E,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACnD,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,KAAK,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAAE,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAA;IAClG,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,0BAA0B,CAAC,KAAsB;IACxD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC;QAAE,IAAI,KAAK,CAAC,KAAK,CAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAE,CAAC,IAAI,GAAG,CAAC;YAAE,IAAI,CAAC,sBAAsB,CAAC,CAAA;AACrJ,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,gBAAgB,CAAC,OAAyB,EAAE,MAAuB;IACjF,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,CAAA;IACtC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,gBAAgB,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;WACvK,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,iBAAiB,EAAE,GAAG,CAAC;WACpG,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,iBAAiB,CAAC,CAAA;IACnI,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAA;IAChG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,UAAU,CAAC,aAAa;QAAE,IAAI,CAAC,iBAAiB,CAAC,CAAA;IACzF,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAA;IACjE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,MAAM,GAAG,UAAU,CAAC,QAAQ;QAAE,IAAI,CAAC,eAAe,CAAC,CAAA;IAC5F,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QAChC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,SAAS,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ;YAAE,IAAI,CAAC,cAAc,CAAC,CAAA;QACzM,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAA;IAC9D,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IAC1D,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,KAAK,GAAG,CAAC,CAAE,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,iBAAiB,CAAC,CAAA;IAC3G,0BAA0B,CAAC,KAAK,CAAC,CAAA;IACjC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAA;IAC5H,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;IACxL,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,UAAU,CAAC,aAAa;QAAE,IAAI,CAAC,qBAAqB,CAAC,CAAA;IAC3G,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAA;IACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAClC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE;YAAE,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IACvG,CAAC;IACD,IAAI,YAAY,CAAC,IAAI,GAAG,CAAC,GAAG,UAAU,CAAC,eAAe;QAAE,IAAI,CAAC,qCAAqC,CAAC,CAAA;IACnG,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,GAAG,UAAU,CAAC,YAAY,GAAG,SAAS;QAAE,IAAI,CAAC,oCAAoC,CAAC,CAAA;IACnJ,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,CAAA;AACzP,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,IAAY,EAAE,MAAe;IACpD,IAAI,MAAM;QAAE,MAAM,KAAK,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAC/D,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,CAAA;IAC/B,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC,cAAc,EAAE;QAAE,IAAI,CAAC,0BAA0B,CAAC,CAAA;IACpF,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;AAC1B,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,YAAoB;IAC9D,IAAI,OAAO,GAAG,IAAI,CAAA;IAClB,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAAC,KAAK,CAAC,GAAG,EAAE,CAAA;IAClD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAAC,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAAC,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,KAAK,CAAA,CAAC,CAAC,CAAC,CAAC;QAAC,MAAM,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IAAC,CAAC;IAC9N,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,4FAA4F;AAC5F,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAiB,EAAE,KAAa,EAAE,OAAyB;IAC9F,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,IAAI,CAAC,gCAAgC,CAAC,CAAA;IACvF,MAAM,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;IAChC,MAAM,UAAU,GAAG,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAAC,MAAM,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,CAAA;IACtF,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;IAC1C,IAAI,CAAC;QAAC,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAAC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAAC,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,IAAI,CAAC,qBAAqB,CAAC,CAAC;QAAC,MAAM,KAAK,CAAA;IAAC,CAAC;IAChK,MAAM,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IAC/B,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAAC,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAAC,MAAM,SAAS,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;IAC3H,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAA;IACjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAAC,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ;YAAE,IAAI,CAAC,qBAAqB,CAAC,CAAA;QACpG,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAC3H,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;QAC9C,IAAI,CAAC;YAAC,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAAC,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAAC,CAAC;gBAAS,CAAC;YAAC,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QAAC,CAAC;IAClH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC"}
|
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
|
+
})
|