@arcfold/dsh 0.0.0-stage → 0.4.0
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 +8 -2
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/cordis.patch.yml +138 -0
- package/package.json +70 -4
- package/patches/deepseek-0.2.2-rc.1-transport.json +65 -0
- package/patches/deepseek-0.2.2-rc.1-transport.patch +61 -0
- package/presets/arcfold.patch.yml +110 -0
- package/runtime/README.md +19 -0
- package/runtime/bootstrap.json +15 -0
- package/runtime/desktop-python.tar.gz +0 -0
- package/runtime/host/package.json +11 -0
- package/runtime/host/pnpm-workspace.yaml +9 -0
- package/runtime/initial-policy.json +94 -0
- package/runtime/profile/pnpm-workspace.yaml +7 -0
- package/scripts/exact-package-patch.mjs +106 -0
- package/scripts/patch-deepseek-transport.mjs +14 -0
- package/src/agent.mjs +56 -0
- package/src/browse-scope.mjs +14 -0
- package/src/budget.mjs +369 -0
- package/src/business.mjs +334 -0
- package/src/context-material.mjs +119 -0
- package/src/deepseek-budget-plugin.mjs +58 -0
- package/src/deepseek-provider.mjs +57 -0
- package/src/deepseek-receipt.mjs +97 -0
- package/src/deepseek-transport.mjs +97 -0
- package/src/desktop/adapter.mjs +16 -0
- package/src/desktop/bootstrap.mjs +84 -0
- package/src/desktop/configuration.mjs +117 -0
- package/src/desktop/provider.mjs +46 -0
- package/src/desktop/python-runtime.mjs +105 -0
- package/src/desktop/setup-service.mjs +81 -0
- package/src/desktop/vendor/LICENSE.deepseek +21 -0
- package/src/desktop/vendor/NOTICE.md +5 -0
- package/src/desktop/vendor/deepseek-0.2.2-rc.1.mjs +2919 -0
- package/src/evidence-ownership.mjs +51 -0
- package/src/evidence.mjs +35 -0
- package/src/goal-context.mjs +52 -0
- package/src/goal-execution-tool.mjs +74 -0
- package/src/goal-execution.mjs +333 -0
- package/src/goal-tool.mjs +56 -0
- package/src/goal-work-control.mjs +63 -0
- package/src/goal-work.mjs +257 -0
- package/src/goals.mjs +578 -0
- package/src/importer.mjs +298 -0
- package/src/index.mjs +53 -0
- package/src/learning-contracts.mjs +220 -0
- package/src/learning-defaults.mjs +98 -0
- package/src/learning-goals.mjs +165 -0
- package/src/learning-plugin.mjs +28 -0
- package/src/learning-runner.mjs +275 -0
- package/src/learning.mjs +534 -0
- package/src/legacy-export.mjs +332 -0
- package/src/legacy-import-source.mjs +356 -0
- package/src/metered-provider-host.mjs +101 -0
- package/src/metered-transport.mjs +435 -0
- package/src/method-context.mjs +57 -0
- package/src/method-lifecycle.mjs +34 -0
- package/src/method-tool.mjs +61 -0
- package/src/methods.mjs +425 -0
- package/src/model-selection.mjs +41 -0
- package/src/native-context-call.mjs +88 -0
- package/src/native-continuation.mjs +217 -0
- package/src/native-request-authority.mjs +90 -0
- package/src/openai-models.mjs +4 -0
- package/src/openai-receipt.mjs +98 -0
- package/src/openai-transport.mjs +167 -0
- package/src/openai-usage.mjs +194 -0
- package/src/opportunities.mjs +295 -0
- package/src/preparation-artifacts.mjs +213 -0
- package/src/preparation-plugin.mjs +290 -0
- package/src/runtime-command.mjs +264 -0
- package/src/runtime-config.mjs +417 -0
- package/src/runtime-guard.mjs +284 -0
- package/src/runtime-host.mjs +212 -0
- package/src/runtime-init.mjs +278 -0
- package/src/runtime-installation.mjs +518 -0
- package/src/runtime-lifecycle.mjs +227 -0
- package/src/runtime-model-settings.mjs +111 -0
- package/src/runtime-readiness.mjs +285 -0
- package/src/runtime-workspace.mjs +18 -0
- package/src/session-bridge.mjs +365 -0
- package/src/store.mjs +394 -0
- package/src/tree/LICENSE +21 -0
- package/src/tree/attribution.mjs +120 -0
- package/src/tree/clusters.mjs +61 -0
- package/src/tree/consolidation.mjs +103 -0
- package/src/tree/context.mjs +211 -0
- package/src/tree/contracts.mjs +142 -0
- package/src/tree/experience.mjs +140 -0
- package/src/tree/experiment-budget.mjs +84 -0
- package/src/tree/hooks.mjs +254 -0
- package/src/tree/host-context.mjs +107 -0
- package/src/tree/host-process.mjs +288 -0
- package/src/tree/host.mjs +37 -0
- package/src/tree/improvement.mjs +106 -0
- package/src/tree/items.mjs +294 -0
- package/src/tree/jobs.mjs +184 -0
- package/src/tree/learning.mjs +202 -0
- package/src/tree/metrics.mjs +203 -0
- package/src/tree/model-owner.mjs +254 -0
- package/src/tree/observations.mjs +73 -0
- package/src/tree/prompts.mjs +500 -0
- package/src/tree/reports.mjs +67 -0
- package/src/tree/repository.mjs +236 -0
- package/src/tree/runtime.mjs +745 -0
- package/src/tree/script-writer.mjs +149 -0
- package/src/tree/scripts.mjs +208 -0
- package/src/tree/signals.mjs +118 -0
- package/src/tree/snapshots.mjs +379 -0
- package/src/tree/token-counter.mjs +75 -0
- package/src/tree/token-counter.py +37 -0
- package/src/tree/tokenizer-resources/LICENSE +21 -0
- package/src/tree/tokenizer-resources/README.md +31 -0
- package/src/tree/tokenizer-resources/deepseek_recipe-0.2.0-cp310-abi3-macosx_11_0_arm64.whl +0 -0
- package/src/tree/tokenizer-resources/manifest.json +61 -0
- package/src/tree/tokenizer-resources/native.pyi +1042 -0
- package/src/tree/tokenizer-resources/tokenizer-NOTICE.md +26 -0
- package/src/tree/tokenizer-resources/tokenizer-v4-NOTICE.md +27 -0
- package/src/tree/tokenizer-resources/tokenizer-v4.json +267359 -0
- package/src/tree/tokenizer-resources/tokenizer.json +267359 -0
- package/src/tree/tools.mjs +99 -0
- package/src/tree/tree.mjs +202 -0
- package/src/tree/ui.mjs +285 -0
- package/src/tree/verification.mjs +143 -0
- package/src/understandings.mjs +309 -0
- package/src/web-budget-recovery.mjs +181 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const error = (code, message) => Object.assign(new Error(message), { code });
|
|
8
|
+
export const contentHash = value => createHash('sha256').update(value).digest('hex');
|
|
9
|
+
export const beneath = (path, root) => path === root || path.startsWith(root + sep);
|
|
10
|
+
const quoted = value => JSON.stringify(value);
|
|
11
|
+
const pathFilter = value => `(subpath ${quoted(value)})`;
|
|
12
|
+
function anchorDirectories(policy) {
|
|
13
|
+
const paths = new Set();
|
|
14
|
+
for (const root of [...policy.protectedRoots, policy.runtimeRoot, ...(policy.immutableRoots ?? [])]) {
|
|
15
|
+
let path = dirname(root);
|
|
16
|
+
while (path !== dirname(path)) { paths.add(path); path = dirname(path); }
|
|
17
|
+
paths.add(path);
|
|
18
|
+
}
|
|
19
|
+
return `(deny file-write* ${[...paths].map(p => `(literal ${quoted(p)})`).join(' ')})`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function canonicalPath(path) {
|
|
23
|
+
if (typeof path !== 'string' || !isAbsolute(path) || path.includes('\0')) throw error('PATH_INVALID', 'An absolute path is required');
|
|
24
|
+
let ancestor = resolve(path), suffix = [];
|
|
25
|
+
while (!existsSync(ancestor)) {
|
|
26
|
+
if (dirname(ancestor) === ancestor) throw error('PATH_INVALID', 'No existing ancestor');
|
|
27
|
+
suffix.unshift(ancestor.slice(dirname(ancestor).length + 1)); ancestor = dirname(ancestor);
|
|
28
|
+
}
|
|
29
|
+
return join(realpathSync(ancestor), ...suffix);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Intersect a declared ceiling with actual session writes; extra roots never widen it. */
|
|
33
|
+
export function processPolicy({ workspace, protectedRoots, runtimeRoot, immutableRoots = [], sessionPolicy, authorizedWriteRoots = [] }) {
|
|
34
|
+
workspace = realpathSync(workspace); runtimeRoot = realpathSync(runtimeRoot);
|
|
35
|
+
const protectedPaths = [...new Set(protectedRoots.map(canonicalPath))];
|
|
36
|
+
if (!protectedPaths.length || protectedPaths.some(p => beneath(runtimeRoot, p)))
|
|
37
|
+
throw error('PROTECTION_INVALID', 'Separate private artifacts/data from the ephemeral execution directory');
|
|
38
|
+
if (!['read-only', 'workspace-write', 'danger-full-access'].includes(sessionPolicy?.mode))
|
|
39
|
+
throw error('POLICY_INVALID', 'An explicit live session policy is required');
|
|
40
|
+
const sessionRoot = realpathSync(sessionPolicy.workspaceRoot);
|
|
41
|
+
const writeRoots = sessionPolicy.mode === 'read-only' ? [] : authorizedWriteRoots.map(canonicalPath).map(p => {
|
|
42
|
+
if (!beneath(p, workspace) || !beneath(p, sessionRoot)) throw error('WRITE_ROOT_DENIED', 'Write grant exceeds workspace/session ceiling');
|
|
43
|
+
if (protectedPaths.some(root => beneath(p, root)) || beneath(p, runtimeRoot)) throw error('WRITE_ROOT_DENIED', 'Protected write target');
|
|
44
|
+
return p;
|
|
45
|
+
});
|
|
46
|
+
return Object.freeze({ workspace, runtimeRoot, protectedRoots: protectedPaths, immutableRoots: immutableRoots.map(canonicalPath), writeRoots });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Kernel file denials are mandatory even when the workspace contains private data. */
|
|
50
|
+
export function macScriptProfile(policy, runDirectory, node = process.execPath) {
|
|
51
|
+
const run = realpathSync(runDirectory), executable = realpathSync(node);
|
|
52
|
+
if (!beneath(run, policy.runtimeRoot) || run === policy.runtimeRoot) throw error('RUN_ROOT_INVALID', 'Owned per-run directory required');
|
|
53
|
+
const exclusions = [...policy.protectedRoots, policy.runtimeRoot].map(p => `(require-not ${pathFilter(p)})`).join(' ');
|
|
54
|
+
const within = root => `(require-all ${pathFilter(root)} ${exclusions})`;
|
|
55
|
+
const writable = root => `(require-all ${within(root)} ${(policy.immutableRoots ?? []).map(p => `(require-not ${pathFilter(p)})`).join(' ')})`;
|
|
56
|
+
return [
|
|
57
|
+
'(version 1)', '(deny default)', '(import "dyld-support.sb")', '(allow syscall*)',
|
|
58
|
+
// No import of system.sb: it broadly grants XPC lookup. Node only needs its
|
|
59
|
+
// fixed interpreter/libraries, JIT, metadata and a small read-only sysctl set.
|
|
60
|
+
'(allow sysctl-read)', '(allow dynamic-code-generation)',
|
|
61
|
+
'(allow process-exec (literal ' + quoted(executable) + '))',
|
|
62
|
+
'(allow file-read-metadata)',
|
|
63
|
+
`(allow file-read* file-map-executable (literal ${quoted(executable)}) (subpath "/System") (subpath "/usr/lib") (subpath "/usr/share") (subpath "/private/var/db/timezone"))`,
|
|
64
|
+
`(allow file-read* ${within(policy.workspace)} ${pathFilter(run)} (literal "/dev/null") (literal "/dev/urandom"))`,
|
|
65
|
+
`(allow file-write* ${pathFilter(run)} (literal "/dev/null") ${policy.writeRoots.map(writable).join(' ')})`,
|
|
66
|
+
anchorDirectories(policy),
|
|
67
|
+
// Deny-by-default covers network (including Unix sockets), task inspection,
|
|
68
|
+
// signal and fork. Generated scripts have no ambient child-process grant.
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Ordinary command processes retain their session grants but cannot see private artifacts. */
|
|
73
|
+
export function macMainProfile(policy) {
|
|
74
|
+
const hidden = [...policy.protectedRoots, policy.runtimeRoot];
|
|
75
|
+
const exclusions = [...hidden, ...(policy.immutableRoots ?? [])].map(p => `(require-not ${pathFilter(p)})`).join(' ');
|
|
76
|
+
return ['(version 1)', '(allow default)',
|
|
77
|
+
`(deny file-read* file-write* file-map-executable ${hidden.map(pathFilter).join(' ')})`,
|
|
78
|
+
'(deny file-write*)',
|
|
79
|
+
`(allow file-write* (literal "/dev/null") ${policy.writeRoots.map(p => `(require-all ${pathFilter(p)} ${exclusions})`).join(' ')})`,
|
|
80
|
+
'(deny process-info* mach-task-name mach-lookup)', '(allow process-info* (target self))', '(deny signal)',
|
|
81
|
+
anchorDirectories(policy),
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Closed boundary, not a fallback to the original argv. Linux remains unverified. */
|
|
86
|
+
export function confinedArgv({ policy, runDirectory, argv, kind = 'script', platform = process.platform }) {
|
|
87
|
+
if (platform !== 'darwin') throw error('SANDBOX_UNAVAILABLE', 'This platform has no validated tree confinement backend');
|
|
88
|
+
if (!Array.isArray(argv) || !argv.length || argv.some(v => typeof v !== 'string' || v.includes('\0')))
|
|
89
|
+
throw error('ARGV_INVALID', 'Exact argv required');
|
|
90
|
+
if (!['script', 'main'].includes(kind)) throw error('POLICY_INVALID', 'Unknown process owner');
|
|
91
|
+
if (kind === 'script' && realpathSync(argv[0]) !== realpathSync(process.execPath)) throw error('INTERPRETER_DENIED', 'Only fixed Node is supported');
|
|
92
|
+
// Desktop's signed Electron executable supplies Node for its own workers.
|
|
93
|
+
const runAsNode = argv[0] === process.execPath && Boolean(process.versions.electron) &&
|
|
94
|
+
process.env.ELECTRON_RUN_AS_NODE === '1';
|
|
95
|
+
return { argv: ['/usr/bin/sandbox-exec', '-p', kind === 'script' ? macScriptProfile(policy, runDirectory) : macMainProfile(policy), '--', ...argv],
|
|
96
|
+
enforcement: 'full', platform, kind, runAsNode };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function spawnConfined(wrapped, { cwd, runDirectory, signal, input, timeoutMs = 30000, outputLimit = 1048576, assertCurrent = () => {} }) {
|
|
100
|
+
if (wrapped?.enforcement !== 'full') throw error('SANDBOX_UNAVAILABLE', 'Full enforcement required');
|
|
101
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120000 ||
|
|
102
|
+
!Number.isInteger(outputLimit) || outputLimit < 1 || outputLimit > 1048576) throw error('LIMIT_INVALID', 'Execution limits exceed contract');
|
|
103
|
+
signal?.throwIfAborted(); await assertCurrent(); signal?.throwIfAborted();
|
|
104
|
+
return new Promise((resolveResult, reject) => {
|
|
105
|
+
let size = 0, truncated = false, reason = null, spawnError, groupKilled = false; const captured = { stdout: [], stderr: [] };
|
|
106
|
+
const child = spawn(wrapped.argv[0], wrapped.argv.slice(1), { cwd, detached: true,
|
|
107
|
+
env: { PATH: '/usr/bin:/bin', HOME: runDirectory, TMPDIR: runDirectory, LANG: 'en_US.UTF-8', TZ: 'UTC',
|
|
108
|
+
...(wrapped.runAsNode ? { ELECTRON_RUN_AS_NODE: '1' } : {}) },
|
|
109
|
+
stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'] });
|
|
110
|
+
if (child.stdin) { child.stdin.on('error', () => {}); child.stdin.end(input); }
|
|
111
|
+
const killGroup = () => {
|
|
112
|
+
if (!child.pid || groupKilled) return;
|
|
113
|
+
try { process.kill(-child.pid, 'SIGKILL'); groupKilled = true; }
|
|
114
|
+
catch (e) { if (e.code === 'ESRCH') groupKilled = true; else spawnError = e; }
|
|
115
|
+
};
|
|
116
|
+
// Several already-buffered output chunks can arrive after the first kill.
|
|
117
|
+
// Do not signal the same terminating group repeatedly (macOS may report
|
|
118
|
+
// EPERM for a group containing only unreaped zombies).
|
|
119
|
+
const stop = why => { reason ??= why; killGroup(); };
|
|
120
|
+
const timer = setTimeout(() => stop('timeout'), timeoutMs);
|
|
121
|
+
const abort = () => stop('cancelled'); signal?.addEventListener('abort', abort, { once: true });
|
|
122
|
+
if (signal?.aborted) abort();
|
|
123
|
+
for (const [stream, which] of [[child.stdout, 'stdout'], [child.stderr, 'stderr']]) stream.on('data', bytes => {
|
|
124
|
+
const retained = bytes.subarray(0, Math.max(0, outputLimit - size));
|
|
125
|
+
if (retained.length) captured[which].push(Buffer.from(retained));
|
|
126
|
+
size += bytes.length;
|
|
127
|
+
if (size > outputLimit) { truncated = true; stop('output-limit'); }
|
|
128
|
+
});
|
|
129
|
+
child.on('error', e => { spawnError = e; });
|
|
130
|
+
child.on('close', (code, exitSignal) => {
|
|
131
|
+
clearTimeout(timer); signal?.removeEventListener('abort', abort);
|
|
132
|
+
// Also close any child that remained in this owned group after its leader.
|
|
133
|
+
killGroup();
|
|
134
|
+
if (spawnError) reject(spawnError);
|
|
135
|
+
else {
|
|
136
|
+
// Decode after joining chunks so a split UTF-8 character is preserved.
|
|
137
|
+
// Invalid byte replacement can expand UTF-8 length; cap that final
|
|
138
|
+
// representation too, without ending on a partial character.
|
|
139
|
+
let remaining = outputLimit;
|
|
140
|
+
const decode = parts => {
|
|
141
|
+
const encoded = Buffer.from(Buffer.concat(parts).toString('utf8'));
|
|
142
|
+
if (encoded.length > remaining) { truncated = true; reason ??= 'output-limit'; }
|
|
143
|
+
const value = new TextDecoder().decode(encoded.subarray(0, remaining), { stream: true }); remaining -= Buffer.byteLength(value); return value;
|
|
144
|
+
};
|
|
145
|
+
const stdout = decode(captured.stdout), stderr = decode(captured.stderr);
|
|
146
|
+
resolveResult({ code, signal: exitSignal, stdout, stderr, truncated, reason });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Clamp the host's explicit danger-mode bypass before invoking its real executor. */
|
|
153
|
+
export function protectedBashClass(Base, readPolicy) {
|
|
154
|
+
return class ProtectedBash extends Base {
|
|
155
|
+
execute(spec) {
|
|
156
|
+
if (!spec.sandboxPolicy) throw error('POLICY_INVALID', 'No live session policy');
|
|
157
|
+
return super.execute({ ...spec, sandboxPolicy: { ...spec.sandboxPolicy,
|
|
158
|
+
mode: spec.sandboxPolicy.mode === 'danger-full-access' ? 'workspace-write' : spec.sandboxPolicy.mode } });
|
|
159
|
+
}
|
|
160
|
+
async confine(command, sessionPolicy, signal) {
|
|
161
|
+
signal?.throwIfAborted();
|
|
162
|
+
const policy = await readPolicy(sessionPolicy);
|
|
163
|
+
return { ...confinedArgv({ policy, kind: 'main', argv: ['/bin/bash', '-c', command] }),
|
|
164
|
+
denialSignatures: ['Operation not permitted', 'Permission denied'] };
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const fsMethods = new Set(['resolve', 'stat', 'lstat', 'readText', 'readBytes', 'readByteRange', 'listDir', 'writeText', 'editText', 'patchText', 'deleteFile', 'moveFile']);
|
|
170
|
+
let mutationTail = Promise.resolve();
|
|
171
|
+
/**
|
|
172
|
+
* Actual DSH filesystem methods run behind the same kernel fence as commands.
|
|
173
|
+
* A pathname check alone is insufficient: a concurrent shell can swap symlinks
|
|
174
|
+
* after the check. The host provider's original atomic write/CAS code is reused.
|
|
175
|
+
*/
|
|
176
|
+
export function protectedFileSystemClass(Base, { readPolicy, cordisUrl, fileSystemUrl }) {
|
|
177
|
+
const remote = async (method, args, signal, sessionPolicy) => {
|
|
178
|
+
signal?.throwIfAborted();
|
|
179
|
+
const policy = await readPolicy(sessionPolicy);
|
|
180
|
+
const worker = fileURLToPath(import.meta.url);
|
|
181
|
+
const immutableRoots = [...new Set([...(policy.immutableRoots ?? []), dirname(worker), dirname(fileURLToPath(fileSystemUrl))])];
|
|
182
|
+
const wrapped = confinedArgv({ policy: { ...policy, immutableRoots }, kind: 'main', argv: [process.execPath, worker, '--tree-filesystem-worker'] });
|
|
183
|
+
const result = await spawnConfined(wrapped, { cwd: policy.workspace, runDirectory: policy.workspace, signal,
|
|
184
|
+
input: JSON.stringify({ method, args, cordisUrl, fileSystemUrl, cwd: policy.workspace }) });
|
|
185
|
+
if (result.code !== 0 || result.truncated) throw error('FS_SANDBOX_DENIED', result.stderr || 'Filesystem worker failed or exceeded output limit');
|
|
186
|
+
let response;
|
|
187
|
+
try { response = JSON.parse(result.stdout); } catch { throw error('FS_SANDBOX_DENIED', 'No complete filesystem worker receipt'); }
|
|
188
|
+
if (response.error) throw error(response.error.code, response.error.message);
|
|
189
|
+
return response.bytes ? Uint8Array.from(Buffer.from(response.bytes, 'base64')) : response.value;
|
|
190
|
+
};
|
|
191
|
+
const mutate = (method, args, signal, policy) => {
|
|
192
|
+
const result = mutationTail.then(() => remote(method, args, signal, policy));
|
|
193
|
+
mutationTail = result.then(() => {}, () => {}); return result;
|
|
194
|
+
};
|
|
195
|
+
return class ProtectedFileSystem extends Base {
|
|
196
|
+
get sandboxMode() { return 'workspace-write'; }
|
|
197
|
+
resolve(path, opts) { return remote('resolve', [path, { cwd: opts?.cwd }], opts?.signal); }
|
|
198
|
+
stat(target, signal) { return remote('stat', [target], signal); }
|
|
199
|
+
lstat(path, opts, signal) { return remote('lstat', [path, opts], signal); }
|
|
200
|
+
readText(target, signal) { return remote('readText', [target], signal); }
|
|
201
|
+
readBytes(target, signal, maxBytes) { return remote('readBytes', [target, null, maxBytes], signal); }
|
|
202
|
+
readByteRange(target, range, signal) { return remote('readByteRange', [target, range], signal); }
|
|
203
|
+
async streamText(target, signal) {
|
|
204
|
+
// Streaming retains the provider's exact UTF-8/binary contract; errors at
|
|
205
|
+
// this bounded transport are explicit, never a successful truncation.
|
|
206
|
+
const text = await this.readText(target, signal);
|
|
207
|
+
return (async function* () { yield text; })();
|
|
208
|
+
}
|
|
209
|
+
listDir(target, signal) { return remote('listDir', [target], signal); }
|
|
210
|
+
writeText(target, content, expected, signal, policy) { return mutate('writeText', [target, content, expected], signal, policy); }
|
|
211
|
+
editText(target, edit, expected, signal, policy) { return mutate('editText', [target, edit, expected], signal, policy); }
|
|
212
|
+
patchText(target, hunks, expected, signal, policy) { return mutate('patchText', [target, hunks, expected], signal, policy); }
|
|
213
|
+
deleteFile(target, expected, signal, policy) { return mutate('deleteFile', [target, expected], signal, policy); }
|
|
214
|
+
moveFile(source, destination, options, signal, policy) { return mutate('moveFile', [source, destination, options], signal, policy); }
|
|
215
|
+
watch() { throw error('FS_WATCH_UNAVAILABLE', 'Protected watcher is not implemented; never use an unrestricted watcher'); }
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (process.argv[2] === '--tree-filesystem-worker' && resolve(process.argv[1] ?? '') === fileURLToPath(import.meta.url)) {
|
|
220
|
+
let body = '';
|
|
221
|
+
for await (const chunk of process.stdin) { body += chunk; if (Buffer.byteLength(body) > 1048576) throw error('INPUT_LIMIT', 'Filesystem request exceeds limit'); }
|
|
222
|
+
let ctx;
|
|
223
|
+
try {
|
|
224
|
+
const request = JSON.parse(body);
|
|
225
|
+
if (!fsMethods.has(request.method)) throw error('METHOD_INVALID', 'Unsupported filesystem operation');
|
|
226
|
+
const [{ Context }, { default: Local }] = await Promise.all([import(request.cordisUrl), import(request.fileSystemUrl)]);
|
|
227
|
+
ctx = new Context();
|
|
228
|
+
const fileSystem = new Local(ctx, { cwd: request.cwd, diffBasisMaxBytes: 65536 });
|
|
229
|
+
const args = request.args.map(value => value === null ? undefined : value);
|
|
230
|
+
const value = await fileSystem[request.method](...args);
|
|
231
|
+
process.stdout.write(JSON.stringify(value instanceof Uint8Array ? { bytes: Buffer.from(value).toString('base64') } : { value }));
|
|
232
|
+
} catch (e) { process.stdout.write(JSON.stringify({ error: { code: e.code ?? 'FS_WORKER_ERROR', message: e.message } })); }
|
|
233
|
+
finally { if (ctx) await ctx.fiber.dispose(); }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function denyPrivatePath(path, policy) {
|
|
237
|
+
const target = canonicalPath(path);
|
|
238
|
+
if ([...policy.protectedRoots, policy.runtimeRoot].some(root => beneath(target, root)))
|
|
239
|
+
throw error('PRIVATE_ARTIFACT_DENIED', 'Ordinary tools cannot access private artifacts or runtime data');
|
|
240
|
+
return target;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** A minimal capability owner; B4 adds snapshot and durable run-intent services. */
|
|
244
|
+
export class ScriptBoundary {
|
|
245
|
+
#runs = new Map();
|
|
246
|
+
constructor({ policy, resolveArtifact, assertCurrent, record }) {
|
|
247
|
+
if ([resolveArtifact, assertCurrent, record].some(fn => typeof fn !== 'function')) throw error('BROKER_INVALID', 'Trusted artifact/authorization/audit services required');
|
|
248
|
+
this.policy = policy; this.resolveArtifact = resolveArtifact; this.assertCurrent = assertCurrent; this.record = record;
|
|
249
|
+
mkdirSync(policy.runtimeRoot, { recursive: true, mode: 0o700 });
|
|
250
|
+
}
|
|
251
|
+
revoke(predicate) { for (const run of this.#runs.values()) if (predicate(run.ticket)) run.abort.abort(); }
|
|
252
|
+
async run(ticket, args, { signal, readOnly = false, timeoutMs = 30000, beforeSpawn = async () => {} } = {}) {
|
|
253
|
+
const frozen = structuredClone(ticket);
|
|
254
|
+
await this.assertCurrent(frozen);
|
|
255
|
+
const artifact = await this.resolveArtifact(frozen);
|
|
256
|
+
if (!artifact || !Array.isArray(artifact.files) || artifact.files.length < 1 || artifact.files.length > 16) throw error('ARTIFACT_INVALID', 'Bounded immutable artifact required');
|
|
257
|
+
const root = mkdtempSync(join(this.policy.runtimeRoot, 'run-'));
|
|
258
|
+
const abort = new AbortController(), id = randomUUID();
|
|
259
|
+
this.#runs.set(id, { ticket: frozen, abort });
|
|
260
|
+
const combined = signal ? AbortSignal.any([signal, abort.signal]) : abort.signal;
|
|
261
|
+
try {
|
|
262
|
+
let bytes = 0; const names = new Set();
|
|
263
|
+
for (const file of artifact.files) {
|
|
264
|
+
if (typeof file.path !== 'string' || !file.path.endsWith('.mjs') || isAbsolute(file.path) || file.path.includes('\\') || file.path.includes('\0') ||
|
|
265
|
+
file.path.split('/').some(p => !p || p === '.' || p === '..') || names.has(file.path)) throw error('ARTIFACT_INVALID', 'Unique relative ESM files required');
|
|
266
|
+
names.add(file.path);
|
|
267
|
+
const source = canonicalPath(file.sourcePath);
|
|
268
|
+
if (!this.policy.protectedRoots.some(p => beneath(source, p)) || !statSync(source).isFile() || lstatSync(file.sourcePath).isSymbolicLink())
|
|
269
|
+
throw error('ARTIFACT_INVALID', 'Private regular artifact file required');
|
|
270
|
+
const content = readFileSync(source); bytes += content.length;
|
|
271
|
+
if (bytes > 262144 || contentHash(content) !== file.sha256) throw error('ARTIFACT_HASH_MISMATCH', 'Artifact changed or exceeds size limit');
|
|
272
|
+
const target = join(root, file.path); mkdirSync(dirname(target), { recursive: true, mode: 0o700 }); writeFileSync(target, content, { flag: 'wx', mode: 0o400 });
|
|
273
|
+
}
|
|
274
|
+
if (!names.has(artifact.entry)) throw error('ARTIFACT_INVALID', 'Entry is not a verified artifact');
|
|
275
|
+
const input = JSON.stringify(args);
|
|
276
|
+
if (typeof input !== 'string' || Buffer.byteLength(input) > 65536) throw error('ARGS_INVALID', 'Bounded JSON args required');
|
|
277
|
+
const inputPath = join(root, 'input.json'); writeFileSync(inputPath, input, { flag: 'wx', mode: 0o400 });
|
|
278
|
+
const policy = readOnly ? { ...this.policy, writeRoots: [] } : this.policy;
|
|
279
|
+
const wrapped = confinedArgv({ policy, runDirectory: root, argv: [process.execPath, join(root, artifact.entry), '--input', inputPath, ...(readOnly ? ['--dry-run'] : [])] });
|
|
280
|
+
await this.record({ id, phase: 'prepared', ticket: frozen, artifactHash: contentHash(artifact.files.map(f => f.sha256).join(':')) });
|
|
281
|
+
await beforeSpawn();
|
|
282
|
+
const result = await spawnConfined(wrapped, { cwd: policy.workspace, runDirectory: root, signal: combined, timeoutMs,
|
|
283
|
+
...(readOnly ? { timeoutMs: Math.min(timeoutMs, 10000) } : {}),
|
|
284
|
+
outputLimit: readOnly ? 65536 : 1048576, assertCurrent: () => this.assertCurrent(frozen) });
|
|
285
|
+
await this.record({ id, phase: 'completed', result }); return result;
|
|
286
|
+
} finally { this.#runs.delete(id); rmSync(root, { recursive: true, force: true }); }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { protectedBashClass, protectedFileSystemClass } from './host-process.mjs';
|
|
2
|
+
import { ownedCompactionClass } from './host-context.mjs';
|
|
3
|
+
|
|
4
|
+
export const name = 'arcfold-tree-host';
|
|
5
|
+
export const inject = ['arcfoldTreeMode'];
|
|
6
|
+
|
|
7
|
+
// Preserve the actual legacy implementation when the flag is off. With the
|
|
8
|
+
// flag on, no ordinary file/process capability starts before its private fence.
|
|
9
|
+
export async function apply(ctx, config) {
|
|
10
|
+
const { component, ...options } = config;
|
|
11
|
+
const modules = { filesystem: '@deepseek-ai/dsh-fs-sandbox', shell: '@deepseek-ai/dsh-bash-sandbox', compaction: '@deepseek-ai/dsh-compaction-basic', goal: '@deepseek-ai/dsh-goal-round-driver' };
|
|
12
|
+
if (!Object.hasOwn(modules, component)) throw new Error('Unknown tree host component');
|
|
13
|
+
// The unchanged official entries wait for arcfoldLegacyMode. Do not install
|
|
14
|
+
// a second copy under this wrapper when the feature is off.
|
|
15
|
+
if (!ctx.arcfoldTreeMode.enabled) return;
|
|
16
|
+
const Base = await import(modules[component]);
|
|
17
|
+
if (component === 'goal') return; // v3 has only bounded same-turn hook steps.
|
|
18
|
+
ctx.inject(['arcfoldTreeRuntime'], async owner => {
|
|
19
|
+
const runtime = owner.arcfoldTreeRuntime;
|
|
20
|
+
if (component === 'filesystem') {
|
|
21
|
+
const { default: Local } = await import('@deepseek-ai/dsh-fs-local');
|
|
22
|
+
const Protected = protectedFileSystemClass(Local, { readPolicy: policy => runtime.hostPolicy(policy),
|
|
23
|
+
cordisUrl: import.meta.resolve('@deepseek-ai/cordis'), fileSystemUrl: import.meta.resolve('@deepseek-ai/dsh-fs-local') });
|
|
24
|
+
await owner.plugin(Protected, options); return;
|
|
25
|
+
}
|
|
26
|
+
if (component === 'shell') { await owner.plugin(protectedBashClass(Base.default, policy => runtime.hostPolicy(policy)), options); return; }
|
|
27
|
+
const { appendPluginRecord } = await import('@deepseek-ai/dsh-session');
|
|
28
|
+
const Compactor = ownedCompactionClass(Base.default, { appendPluginRecord, summarize: (input, agent, signal) => runtime.summarizeNative(input, agent, signal) });
|
|
29
|
+
// Auxiliary persistent learning uses its own 128K epoch replacement. Native
|
|
30
|
+
// pressure must not rewrite it behind the owner's frozen input contract.
|
|
31
|
+
class MainCompaction extends Compactor {
|
|
32
|
+
compactIfNeeded(agent, ...args) { return runtime.mainAgents.has(agent.session.id) ? super.compactIfNeeded(agent, ...args) : Promise.resolve(null); }
|
|
33
|
+
compactNow(agent, ...args) { if (!runtime.mainAgents.has(agent.session.id)) throw new Error('Only a main session supports native compaction'); return super.compactNow(agent, ...args); }
|
|
34
|
+
}
|
|
35
|
+
await owner.plugin(MainCompaction, options);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { CONTRACT, canonicalJson, digest, fail, oneOf } from './contracts.mjs';
|
|
2
|
+
import { compilePrompt, producerRuntimeSpecification, validateOutput } from './prompts.mjs';
|
|
3
|
+
import { itemInput } from './learning.mjs';
|
|
4
|
+
import { evidenceResolver } from './signals.mjs';
|
|
5
|
+
|
|
6
|
+
const unique = values => [...new Map(values.map(value => [canonicalJson(value), value])).values()];
|
|
7
|
+
const roles = ['attribution', 'learning', 'consolidation', 'verification', 'improvement'];
|
|
8
|
+
|
|
9
|
+
export class ImprovementService {
|
|
10
|
+
constructor({ repository, jobs, contextBuilder, modelConfig, reports }) {
|
|
11
|
+
Object.assign(this, { repo: repository, jobs, builder: contextBuilder, config: modelConfig, reports });
|
|
12
|
+
this.resolveSource = evidenceResolver(repository, contextBuilder.resolveSource);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
enqueue(producerRole) {
|
|
16
|
+
oneOf(producerRole, roles, 'producerRole');
|
|
17
|
+
const prior = this.repo.list('tree_jobs', job => job.role === 'improvement' && job.trigger.producerRole === producerRole);
|
|
18
|
+
const active = prior.find(job => !['committed', 'stale', 'invalid_output', 'failed', 'cancelled'].includes(job.status));
|
|
19
|
+
if (active) return active;
|
|
20
|
+
|
|
21
|
+
const processed = new Set(prior.filter(job => job.status === 'committed').flatMap(job => job.input.repairs.map(repair => repair.id)));
|
|
22
|
+
const repairs = this.repo.list('tree_repairs', repair =>
|
|
23
|
+
repair.producerRole === producerRole &&
|
|
24
|
+
(repair.countsAsError === true || repair.eligibleForDiagnosis === true && repair.countsAsError !== false) &&
|
|
25
|
+
!['changed', 'mark_changed'].includes(repair.type) && repair.attribution !== 'unresolved' && !processed.has(repair.id));
|
|
26
|
+
if (repairs.length < 2) return null;
|
|
27
|
+
const key = digest(['producer-repairs', producerRole, repairs.map(repair => [repair.id, repair.revision])]);
|
|
28
|
+
// A terminal diagnosis remains the owner of these exact repairs. Do not
|
|
29
|
+
// rebuild its frozen input or resend it when unrelated state later changes.
|
|
30
|
+
const existing = this.repo.get('tree_jobs', digest(['model_job', 'improvement', key]));
|
|
31
|
+
if (existing) return existing;
|
|
32
|
+
|
|
33
|
+
const nodeIds = [this.repo.rootId()];
|
|
34
|
+
const relatedNodeIds = [...new Set([...nodeIds, ...repairs.flatMap(repair => repair.nodeIds)])];
|
|
35
|
+
// A user can undo a mistakenly created node before diagnosis runs. Keep
|
|
36
|
+
// that historical node in affectedNodes/repairs, but do not traverse its
|
|
37
|
+
// deleted path as if it were still part of the active tree.
|
|
38
|
+
const liveNodeIds = relatedNodeIds.filter(id => { const node = this.repo.get('tree_nodes', id); return node && !node.deleted; });
|
|
39
|
+
const ancestors = liveNodeIds.map(id => this.repo.ancestors(id).map(node => node.id));
|
|
40
|
+
const ancestorIds = [...new Set(ancestors.flat())];
|
|
41
|
+
const rows = this.builder.activeItems(ancestorIds, producerRole).filter(row =>
|
|
42
|
+
row.item.kind === 'supplement' || row.item.kind === 'understanding' && row.item.nodeId === this.repo.rootId());
|
|
43
|
+
const sourceRefs = unique([...repairs.flatMap(repair => repair.evidenceRefs ?? []), ...rows.flatMap(row => row.version.sourceRefs)]);
|
|
44
|
+
const sources = sourceRefs.map(ref => {
|
|
45
|
+
const source = this.resolveSource(ref);
|
|
46
|
+
if (!source) fail('SOURCE_CHANGED', 'Repair evidence was withdrawn');
|
|
47
|
+
return { ...source, ref, id: source.id ?? digest(ref) };
|
|
48
|
+
});
|
|
49
|
+
if (!sources.length) fail('REPAIR_EVIDENCE_REQUIRED', 'No immutable evidence for a diagnosis');
|
|
50
|
+
|
|
51
|
+
const parameters = this.repo.list('tree_parameters', parameter => ancestorIds.includes(parameter.scopeNodeId));
|
|
52
|
+
const repairedItems = repairs.filter(repair => repair.itemId && repair.itemVersion).map(repair => {
|
|
53
|
+
const item = this.repo.get('tree_items', repair.itemId);
|
|
54
|
+
return {
|
|
55
|
+
repairId: repair.id,
|
|
56
|
+
item,
|
|
57
|
+
before: this.repo.get('tree_item_versions', digest(['item_version', repair.itemId, repair.itemVersion])),
|
|
58
|
+
after: item && this.repo.get('tree_item_versions', digest(['item_version', item.id, item.activeVersion ?? item.draftVersion])),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
const input = {
|
|
62
|
+
inputVersion: CONTRACT,
|
|
63
|
+
role: 'improvement',
|
|
64
|
+
scope: { nodeIds, ancestorPaths: ancestors, treeRevision: this.repo.meta().treeRevision },
|
|
65
|
+
sources,
|
|
66
|
+
knownItems: rows.map(itemInput),
|
|
67
|
+
allowedIds: { nodeIds: ancestorIds, subjectIds: rows.map(row => row.item.id), fileIds: [] },
|
|
68
|
+
changeReceipts: [],
|
|
69
|
+
repairs,
|
|
70
|
+
relatedNodeIds,
|
|
71
|
+
fixedProducerSpecification: compilePrompt(producerRole),
|
|
72
|
+
fixedProducerRuntime: producerRuntimeSpecification(producerRole),
|
|
73
|
+
parameters,
|
|
74
|
+
existingReports: this.repo.list('tree_reports', report => report.producerRole === producerRole),
|
|
75
|
+
repairedItems,
|
|
76
|
+
affectedNodes: relatedNodeIds.map(id => this.repo.get('tree_nodes', id)),
|
|
77
|
+
rootUnderstandingIds: rows.filter(row => row.item.kind === 'understanding').map(row => row.item.id),
|
|
78
|
+
};
|
|
79
|
+
const revisions = Object.fromEntries([
|
|
80
|
+
...unique([...ancestorIds, ...relatedNodeIds]).map(id => this.repo.get('tree_nodes', id)).filter(Boolean)
|
|
81
|
+
.map(node => ['tree_nodes:' + node.id, node.revision]),
|
|
82
|
+
...repairs.map(repair => ['tree_repairs:' + repair.id, repair.revision]),
|
|
83
|
+
...rows.map(row => ['tree_items:' + row.item.id, row.item.revision]),
|
|
84
|
+
...repairedItems.filter(row => row.item).map(row => ['tree_items:' + row.item.id, row.item.revision]),
|
|
85
|
+
...parameters.map(parameter => ['tree_parameters:' + parameter.id, parameter.revision]),
|
|
86
|
+
]);
|
|
87
|
+
const modelOrigin = this.config.source?.({ nodeIds: relatedNodeIds });
|
|
88
|
+
return this.jobs.enqueue({
|
|
89
|
+
role: 'improvement', key, nodeIds, input, revisionSet: revisions, config: this.config('improvement', { nodeIds: relatedNodeIds }),
|
|
90
|
+
trigger: { kind: 'repair_group', producerRole, ...(modelOrigin ? { modelSourceJobId: modelOrigin.id } : {}), explicitParameters: parameters.filter(parameter => parameter.actor.kind === 'user')
|
|
91
|
+
.map(parameter => ({ name: parameter.name, scopeNodeId: parameter.scopeNodeId })) },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
applyResult(result, { job, requestId }) {
|
|
96
|
+
const output = validateOutput('improvement', result, job.input);
|
|
97
|
+
return this.repo.transaction(() => {
|
|
98
|
+
this.jobs.assertCurrent(job);
|
|
99
|
+
const report = this.reports.recordReview(output, { job, requestId: digest([requestId, 'report']) });
|
|
100
|
+
return {
|
|
101
|
+
same: output.same, kind: output.kind, diagnosis: output.diagnosis,
|
|
102
|
+
reportId: report.reportId, supplements: [], parameters: [], reportOnly: true,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|