@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,227 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { mkdir, open, readFile, rename, stat } from 'node:fs/promises';
|
|
5
|
+
import { createConnection } from 'node:net';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
9
|
+
|
|
10
|
+
export const RUNTIME_PROTOCOL = 'arcfold-runtime-v1';
|
|
11
|
+
const hostEntry = fileURLToPath(new URL('./runtime-host.mjs', import.meta.url));
|
|
12
|
+
const busy = error => ['EAGAIN', 'EWOULDBLOCK'].includes(error.code) && error.syscall === 'flock';
|
|
13
|
+
const validRunId = value => typeof value === 'string' && /^[a-zA-Z0-9_-]{1,128}$/.test(value);
|
|
14
|
+
export const lifecycleError = (code, message, details) => Object.assign(new Error(message), { code, details });
|
|
15
|
+
export async function readRuntimeRecord(path) {
|
|
16
|
+
try { return JSON.parse(await readFile(path, 'utf8')); }
|
|
17
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
18
|
+
}
|
|
19
|
+
export async function writeRuntimeRecord(path, value, { exclusive = false } = {}) {
|
|
20
|
+
const temporary = exclusive ? path : join(dirname(path), '.runtime-' + randomUUID());
|
|
21
|
+
const file = await open(temporary, 'wx', 0o600);
|
|
22
|
+
try { await file.writeFile(JSON.stringify(value) + '\n'); await file.sync(); } finally { await file.close(); }
|
|
23
|
+
if (!exclusive) await rename(temporary, path);
|
|
24
|
+
const directory = await open(dirname(path), 'r');
|
|
25
|
+
try { await directory.sync(); } finally { await directory.close(); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The lock file is never deleted. PID records do not confer ownership. */
|
|
29
|
+
export async function acquireRuntimeLease(runtimeDirectory, { probe = false } = {}) {
|
|
30
|
+
if (!['darwin', 'linux'].includes(process.platform)) throw lifecycleError('unsupported_platform', 'The runtime owner requires POSIX flock.');
|
|
31
|
+
const { tryLockExclusive } = await import('@deepseek-ai/node-addon-system/flock');
|
|
32
|
+
if (!probe) await mkdir(runtimeDirectory, { recursive: true, mode: 0o700 });
|
|
33
|
+
const path = join(runtimeDirectory, 'owner.lock');
|
|
34
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
35
|
+
let handle;
|
|
36
|
+
try { handle = await open(path, constants.O_RDWR | constants.O_NOFOLLOW | (probe ? 0 : constants.O_CREAT), 0o600); }
|
|
37
|
+
catch (error) { if (probe && error.code === 'ENOENT') return null; throw error; }
|
|
38
|
+
try {
|
|
39
|
+
await tryLockExclusive(handle.fd);
|
|
40
|
+
const held = await handle.stat({ bigint: true }), current = await stat(path, { bigint: true }).catch(error => {
|
|
41
|
+
if (error.code === 'ENOENT') return null; throw error;
|
|
42
|
+
});
|
|
43
|
+
if (current && held.dev === current.dev && held.ino === current.ino) return handle;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
await handle.close();
|
|
46
|
+
if (busy(error)) {
|
|
47
|
+
// A read-only status probe briefly holds the same kernel lock. Give that
|
|
48
|
+
// probe a bounded chance to release; never infer ownership from a PID.
|
|
49
|
+
if (!probe && attempt < 2) { await delay(10); continue; }
|
|
50
|
+
throw lifecycleError('already_running', 'This data directory already has a runtime owner.');
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
await handle.close();
|
|
55
|
+
}
|
|
56
|
+
throw lifecycleError('owner_path_changed', 'The runtime lock changed while acquiring ownership.');
|
|
57
|
+
}
|
|
58
|
+
async function ownerHeld(runtimeDirectory) {
|
|
59
|
+
try { const lease = await acquireRuntimeLease(runtimeDirectory, { probe: true }); await lease?.close(); return false; }
|
|
60
|
+
catch (error) { if (error.code === 'already_running') return true; throw error; }
|
|
61
|
+
}
|
|
62
|
+
export async function runtimeControl(current, method, timeoutMs = 1000) {
|
|
63
|
+
if (!current || current.schema !== RUNTIME_PROTOCOL || !['status', 'stop', 'open'].includes(method) ||
|
|
64
|
+
!validRunId(current.runId) || current.controlAddress?.host !== '127.0.0.1' ||
|
|
65
|
+
!Number.isSafeInteger(current.controlAddress.port) || current.controlAddress.port < 1 || current.controlAddress.port > 65535)
|
|
66
|
+
throw lifecycleError('invalid_runtime_record', 'No valid current runtime control identity.');
|
|
67
|
+
const controlPath = join(current.dataRoot, 'runtime', 'runs', current.runId, 'control.json');
|
|
68
|
+
const file = await open(controlPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
69
|
+
let control;
|
|
70
|
+
try {
|
|
71
|
+
const info = await file.stat();
|
|
72
|
+
if (!info.isFile() || info.uid !== process.getuid() || (info.mode & 0o777) !== 0o600 || info.size > 4096)
|
|
73
|
+
throw lifecycleError('invalid_control_owner', 'The runtime control identity is not a private user-owned file.');
|
|
74
|
+
control = JSON.parse(await file.readFile('utf8'));
|
|
75
|
+
} finally { await file.close(); }
|
|
76
|
+
if (control.schema !== RUNTIME_PROTOCOL || control.runId !== current.runId || control.dataRoot !== current.dataRoot ||
|
|
77
|
+
control.address?.host !== current.controlAddress.host || control.address?.port !== current.controlAddress.port ||
|
|
78
|
+
typeof control.token !== 'string' || !/^[a-f0-9]{64}$/.test(control.token))
|
|
79
|
+
throw lifecycleError('invalid_control_owner', 'The private runtime control identity does not match its public record.');
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const socket = createConnection(current.controlAddress); let data = '', settled = false;
|
|
82
|
+
const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); socket.destroy(); error ? reject(error) : resolve(value); };
|
|
83
|
+
const timer = setTimeout(() => finish(lifecycleError('control_timeout', 'The owned runtime did not answer.')), timeoutMs);
|
|
84
|
+
socket.on('error', error => finish(error));
|
|
85
|
+
socket.on('connect', () => socket.write(JSON.stringify({ schema: RUNTIME_PROTOCOL, runId: current.runId, dataRoot: current.dataRoot, method, token: control.token }) + '\n'));
|
|
86
|
+
socket.on('data', chunk => {
|
|
87
|
+
data += chunk; if (Buffer.byteLength(data) > 256 * 1024) return finish(lifecycleError('invalid_control_response', 'Runtime response is too large.'));
|
|
88
|
+
if (!data.includes('\n')) return;
|
|
89
|
+
try { const value = JSON.parse(data.slice(0, data.indexOf('\n')));
|
|
90
|
+
if (value.schema !== RUNTIME_PROTOCOL || value.runId !== current.runId || value.dataRoot !== current.dataRoot)
|
|
91
|
+
throw lifecycleError('runtime_identity_changed', 'Runtime control identity did not match.');
|
|
92
|
+
if (value.controlError) {
|
|
93
|
+
const messages = { unsupported_platform:'Opening the authenticated page is currently supported only on macOS.',
|
|
94
|
+
runtime_not_ready:'The runtime is not ready to open its page.', browser_open_failed:'The authenticated page could not be opened.' };
|
|
95
|
+
const code = value.controlError.code;
|
|
96
|
+
throw lifecycleError(Object.hasOwn(messages, code) ? code : 'invalid_control_response', messages[code] ?? 'The runtime control request failed.');
|
|
97
|
+
}
|
|
98
|
+
if (method === 'open' && value.opened !== true) throw lifecycleError('invalid_control_response', 'The runtime did not confirm opening its page.');
|
|
99
|
+
finish(null, method === 'open' ? { opened:true } : value);
|
|
100
|
+
} catch (error) { finish(error); }
|
|
101
|
+
});
|
|
102
|
+
socket.on('end', () => { if (!settled) finish(lifecycleError('control_closed', 'Runtime control closed without a complete response.')); });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Status reads existing files only; it neither initializes data nor signals PIDs. */
|
|
107
|
+
export async function inspectOwnedRuntime(identity) {
|
|
108
|
+
const current = await readRuntimeRecord(join(identity.runtimeDirectory, 'current.json'));
|
|
109
|
+
if (current && (current.schema !== RUNTIME_PROTOCOL || current.dataRoot !== identity.dataRoot || !validRunId(current.runId)))
|
|
110
|
+
throw lifecycleError('invalid_runtime_record', 'Runtime metadata belongs to a different data directory.');
|
|
111
|
+
const held = await ownerHeld(identity.runtimeDirectory);
|
|
112
|
+
if (held) {
|
|
113
|
+
if (current) try { return { ...await runtimeControl(current, 'status'), ownerHeld: true }; } catch {}
|
|
114
|
+
return { schema: RUNTIME_PROTOCOL, dataRoot: identity.dataRoot, phase: 'unreachable', ownerHeld: true,
|
|
115
|
+
runId: null, lastKnownRunId: current?.runId ?? null, shutdownEvidence: 'unconfirmed' };
|
|
116
|
+
}
|
|
117
|
+
const receipt = current && await readRuntimeRecord(join(identity.runtimeDirectory, 'runs', current.runId, 'official-shutdown-returned.json'));
|
|
118
|
+
const failure = current && await readRuntimeRecord(join(identity.runtimeDirectory, 'runs', current.runId, 'failure.json'));
|
|
119
|
+
const confirmed = receipt?.runId === current?.runId && receipt?.dataRoot === identity.dataRoot && receipt?.kind === 'official-shutdown-returned';
|
|
120
|
+
return { schema: RUNTIME_PROTOCOL, dataRoot: identity.dataRoot, phase: 'stopped', ownerHeld: false,
|
|
121
|
+
runId: current?.runId ?? null, lastPhase: current?.phase ?? null, lastError: failure ?? current?.error ?? null,
|
|
122
|
+
shutdownEvidence: confirmed ? 'official-shutdown-returned' : current ? 'unconfirmed' : 'none',
|
|
123
|
+
shutdownReceipt: confirmed ? receipt : null };
|
|
124
|
+
}
|
|
125
|
+
const deadlineMs = value => {
|
|
126
|
+
const timeout = value ?? 60000;
|
|
127
|
+
if (!Number.isSafeInteger(timeout) || timeout < 100 || timeout > 600000) throw lifecycleError('invalid_timeout', 'Use a timeout between 100 and 600000 ms.');
|
|
128
|
+
return timeout;
|
|
129
|
+
};
|
|
130
|
+
export async function statusRuntime({ configPath }) {
|
|
131
|
+
const { loadRuntimeIdentity } = await import('./runtime-config.mjs');
|
|
132
|
+
return inspectOwnedRuntime(await loadRuntimeIdentity(configPath));
|
|
133
|
+
}
|
|
134
|
+
export async function openRuntime({ configPath, timeoutMs, signal }) {
|
|
135
|
+
if (process.platform !== 'darwin') throw lifecycleError('unsupported_platform', 'Opening the authenticated page is currently supported only on macOS.');
|
|
136
|
+
const timeout = deadlineMs(timeoutMs); signal?.throwIfAborted();
|
|
137
|
+
const { loadRuntimeIdentity } = await import('./runtime-config.mjs');
|
|
138
|
+
const identity = await loadRuntimeIdentity(configPath), before = await inspectOwnedRuntime(identity);
|
|
139
|
+
if (!before.ownerHeld) throw lifecycleError('runtime_not_running', 'Start Arcfold before opening its page.');
|
|
140
|
+
if (!before.runId || before.phase !== 'ready') throw lifecycleError('runtime_not_ready', 'The runtime is not ready to open its page.');
|
|
141
|
+
const current = await readRuntimeRecord(join(identity.runtimeDirectory, 'current.json'));
|
|
142
|
+
if (current?.runId !== before.runId) throw lifecycleError('runtime_identity_changed', 'The runtime owner changed before opening its page.');
|
|
143
|
+
signal?.throwIfAborted();
|
|
144
|
+
return runtimeControl(current, 'open', Math.min(timeout, 10000));
|
|
145
|
+
}
|
|
146
|
+
export async function startRuntime({ configPath, timeoutMs, signal }) {
|
|
147
|
+
const timeout = deadlineMs(timeoutMs); signal?.throwIfAborted();
|
|
148
|
+
const { loadRuntimePlan } = await import('./runtime-config.mjs');
|
|
149
|
+
const plan = await loadRuntimePlan(configPath), existing = await inspectOwnedRuntime(plan);
|
|
150
|
+
if (existing.ownerHeld) throw lifecycleError('already_running', 'This data directory already has a runtime owner.', existing);
|
|
151
|
+
const runId = randomUUID(), runDirectory = join(plan.runtimeDirectory, 'runs', runId);
|
|
152
|
+
await mkdir(runDirectory, { recursive: true, mode: 0o700 });
|
|
153
|
+
const launchFile = join(runDirectory, 'launch.json');
|
|
154
|
+
await writeRuntimeRecord(launchFile, { schema: RUNTIME_PROTOCOL, runId, runDirectory, plan }, { exclusive: true });
|
|
155
|
+
let output, errors, child, done, closed = false, exit, launchError;
|
|
156
|
+
try {
|
|
157
|
+
output = await open(join(runDirectory, 'stdout.log'), 'ax', 0o600);
|
|
158
|
+
errors = await open(join(runDirectory, 'stderr.log'), 'ax', 0o600);
|
|
159
|
+
child = spawn(process.execPath, [hostEntry, '--launch', launchFile], { cwd: plan.workspace, detached: true,
|
|
160
|
+
stdio: ['ignore', output.fd, errors.fd], env: { ...process.env, DSH_HOME: plan.nativeHome } });
|
|
161
|
+
child.once('error', error => { launchError = error; });
|
|
162
|
+
done = new Promise(resolveDone => child.once('close', (code, exitSignal) => { closed = true; exit = { code, signal: exitSignal }; resolveDone(exit); }));
|
|
163
|
+
} finally { await output?.close(); await errors?.close(); }
|
|
164
|
+
child.unref();
|
|
165
|
+
const ownStatePath = join(runDirectory, 'state.json');
|
|
166
|
+
const isOwnState = value => value?.schema === RUNTIME_PROTOCOL && value.runId === runId && value.dataRoot === plan.dataRoot &&
|
|
167
|
+
value.pid === child.pid && value.configurationHash === plan.configurationHash;
|
|
168
|
+
let childFailureObserved = false;
|
|
169
|
+
const deadline = Date.now() + timeout;
|
|
170
|
+
try {
|
|
171
|
+
for (;;) {
|
|
172
|
+
signal?.throwIfAborted(); if (launchError) throw launchError;
|
|
173
|
+
const own = await readRuntimeRecord(ownStatePath);
|
|
174
|
+
if (own && !isOwnState(own)) throw lifecycleError('invalid_runtime_record', 'The startup record does not belong to this child.');
|
|
175
|
+
if (own?.phase === 'failed') {
|
|
176
|
+
childFailureObserved = true;
|
|
177
|
+
throw lifecycleError(own.error?.code || 'startup_failed', own.error?.message || 'Runtime startup failed.', own);
|
|
178
|
+
}
|
|
179
|
+
if (own) {
|
|
180
|
+
const current = await inspectOwnedRuntime(plan);
|
|
181
|
+
if (current.ownerHeld && current.runId === runId && current.phase === 'ready') return current;
|
|
182
|
+
}
|
|
183
|
+
if (closed) throw lifecycleError('startup_exited', 'Runtime exited before validated readiness.', { exit, own });
|
|
184
|
+
if (Date.now() >= deadline) throw lifecycleError('startup_timeout', 'Runtime readiness was not confirmed before the deadline.', { runId });
|
|
185
|
+
await delay(50);
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (!closed && !childFailureObserved) {
|
|
189
|
+
const latest = await readRuntimeRecord(ownStatePath).catch(() => null);
|
|
190
|
+
childFailureObserved = isOwnState(latest) && latest.phase === 'failed';
|
|
191
|
+
}
|
|
192
|
+
// A ChildProcess handle from this call is the only PID signal authority.
|
|
193
|
+
// A verified failed child is already cleaning up. The official interrupt
|
|
194
|
+
// escalates an in-progress shutdown to immediate exit, so do not add one.
|
|
195
|
+
// Cancellation/timeout without that evidence still sends one SIGTERM only.
|
|
196
|
+
if (!closed && !childFailureObserved && child.pid) child.kill('SIGTERM');
|
|
197
|
+
let timer; try { await Promise.race([done, new Promise(resolveWait => { timer = setTimeout(resolveWait, 7000); })]); } finally { clearTimeout(timer); }
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export async function stopRuntime({ configPath, timeoutMs, signal }) {
|
|
202
|
+
const timeout = deadlineMs(timeoutMs); signal?.throwIfAborted();
|
|
203
|
+
const { loadRuntimeIdentity } = await import('./runtime-config.mjs');
|
|
204
|
+
const identity = await loadRuntimeIdentity(configPath), before = await inspectOwnedRuntime(identity);
|
|
205
|
+
if (!before.ownerHeld) return before;
|
|
206
|
+
if (!before.runId) throw lifecycleError('runtime_unreachable', 'An owner holds the data lock but its control identity is unavailable; no PID was signalled.', before);
|
|
207
|
+
const current = await readRuntimeRecord(join(identity.runtimeDirectory, 'current.json'));
|
|
208
|
+
if (current?.runId !== before.runId) throw lifecycleError('runtime_identity_changed', 'The runtime owner changed before stop.');
|
|
209
|
+
try { await runtimeControl(current, 'stop'); }
|
|
210
|
+
catch (error) {
|
|
211
|
+
const after = await inspectOwnedRuntime(identity);
|
|
212
|
+
if (!after.ownerHeld && after.runId === before.runId) return after;
|
|
213
|
+
throw lifecycleError('stop_unconfirmed', 'The single stop request was not acknowledged; no PID was signalled.', { runId: before.runId, cause: error.code });
|
|
214
|
+
}
|
|
215
|
+
const deadline = Date.now() + timeout;
|
|
216
|
+
for (;;) {
|
|
217
|
+
signal?.throwIfAborted(); const result = await inspectOwnedRuntime(identity);
|
|
218
|
+
if (result.runId && result.runId !== before.runId) throw lifecycleError('runtime_identity_changed', 'Another runtime owns this data after the stop request.');
|
|
219
|
+
if (!result.ownerHeld) return result;
|
|
220
|
+
if (Date.now() >= deadline) throw lifecycleError('stop_unconfirmed', 'Stop was requested once; release of ownership remains unconfirmed.', { runId: before.runId });
|
|
221
|
+
await delay(50);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export async function restartRuntime(options) {
|
|
225
|
+
const previous = await stopRuntime(options); options.signal?.throwIfAborted();
|
|
226
|
+
return { ...await startRuntime(options), previous };
|
|
227
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createModelSelectionHandler } from './model-selection.mjs';
|
|
2
|
+
import { getRuntimeGuard, serviceIdentity } from './runtime-guard.mjs';
|
|
3
|
+
import { runtimeTransport, runtimeRouteAvailability } from './runtime-config.mjs';
|
|
4
|
+
import { runtimeMainAvailability } from './runtime-readiness.mjs';
|
|
5
|
+
|
|
6
|
+
const choice = value => value ? { provider: value.provider, model: value.model,
|
|
7
|
+
reasoningEffort: value.reasoningEffort ?? null } : { provider: '', model: '', reasoningEffort: null };
|
|
8
|
+
const defaultView = value => value ? { revision: value.revision, ...choice(value) } : null;
|
|
9
|
+
const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
10
|
+
const unavailable = reason => ({ available: false, reason });
|
|
11
|
+
|
|
12
|
+
/** Read-only views and validated edits over existing native/business owners. */
|
|
13
|
+
export function createRuntimeModelSettings(host, config, guard) {
|
|
14
|
+
const routes = runtimeTransport(config).routes;
|
|
15
|
+
const assertReady = () => guard.assertReady();
|
|
16
|
+
const assertLearningSelection = selected => {
|
|
17
|
+
// Startup can prepare an existing pending item before the outer stream gate
|
|
18
|
+
// opens. This grants no send: that gate and the final dispatch still apply.
|
|
19
|
+
const phase = guard.snapshot().phase;
|
|
20
|
+
if (!['starting', 'ready'].includes(phase)) assertReady();
|
|
21
|
+
if (!config.learn || !runtimeRouteAvailability({ ...config, learn: selected }).learn.available)
|
|
22
|
+
fail('learning_configuration_unavailable', '当前部署不允许这项学习模型设置。');
|
|
23
|
+
};
|
|
24
|
+
const assertMainSelection = async (_agent, selected) => {
|
|
25
|
+
assertReady();
|
|
26
|
+
const available = await runtimeMainAvailability(host.llm, config, selected);
|
|
27
|
+
assertReady();
|
|
28
|
+
if (!available.available) fail('model_selection_unavailable', '当前部署无法使用这项主模型设置。');
|
|
29
|
+
};
|
|
30
|
+
const catalog = async () => {
|
|
31
|
+
const native = await host.sessionController.modelCatalog();
|
|
32
|
+
return Object.entries(routes).map(([provider, route]) => {
|
|
33
|
+
const group = native.groups.find(group => group.id === provider);
|
|
34
|
+
return { provider, label: provider === 'openai' ? 'OpenAI' : 'DeepSeek', apiKeyRef: route.apiKeyRef,
|
|
35
|
+
models: (group?.models ?? []).flatMap(model => {
|
|
36
|
+
const policy = route.modelPolicies[model.id];
|
|
37
|
+
if (!policy) return [];
|
|
38
|
+
const efforts = (model.reasoning?.efforts ?? []).map(value => value.id)
|
|
39
|
+
.filter(value => policy.reasoningEfforts.includes(value));
|
|
40
|
+
return efforts.length ? [{ id: model.id, label: model.name, reasoningEfforts: efforts }] : [];
|
|
41
|
+
}) };
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const usage = provider => {
|
|
45
|
+
const source = host.get('arcfoldMeteredTransport') ?? (provider === 'deepseek-official' ? host.get('arcfoldDeepSeekTransport') : null);
|
|
46
|
+
let value; try { value = source?.summary(provider); } catch { return null; }
|
|
47
|
+
if (!value) return null;
|
|
48
|
+
if (provider === 'openai') return { accounting: 'tokens_only', scope: value.scope,
|
|
49
|
+
requests: value.requests, unknownCount: value.unknownCount, pendingCount: value.pendingCount,
|
|
50
|
+
promptTokens: value.promptTokens, completionTokens: value.completionTokens, cachedTokens: value.cachedTokens };
|
|
51
|
+
return { accounting: 'cny', scope: value.scope, requests: value.requests, unknownCount: value.unknownCount,
|
|
52
|
+
spentCny: value.spentCny, reservedCny: value.reservedCny, ceilingCny: value.ceilingCny };
|
|
53
|
+
};
|
|
54
|
+
return Object.freeze({ assertLearningSelection, assertMainSelection,
|
|
55
|
+
async read() {
|
|
56
|
+
const defaults = host.get('arcfoldLearningDefaults');
|
|
57
|
+
const main = choice(host.agentDefaultModel.currentSelection()), learn = defaults?.read();
|
|
58
|
+
let list = [], mainAvailability, learnAvailability;
|
|
59
|
+
try { list = await catalog(); } catch { /* A stopped/failed catalog has no executable choices. */ }
|
|
60
|
+
try { mainAvailability = await runtimeMainAvailability(host.llm, config, main); }
|
|
61
|
+
catch { mainAvailability = unavailable('selected-model-controls-unavailable'); }
|
|
62
|
+
learnAvailability = config.learn && learn ? runtimeRouteAvailability({ ...config,
|
|
63
|
+
learn: { ...config.learn, ...choice(learn) } }).learn : unavailable('learn-not-configured');
|
|
64
|
+
const presence = {};
|
|
65
|
+
for (const [provider, route] of Object.entries(routes)) {
|
|
66
|
+
try { presence[provider] = (await host.credentials.describe(route.apiKeyRef)).configured === true; }
|
|
67
|
+
catch { presence[provider] = false; }
|
|
68
|
+
}
|
|
69
|
+
const withCredential = (available, selected) => available.available && !presence[selected?.provider]
|
|
70
|
+
? unavailable('native-credential-not-configured') : available;
|
|
71
|
+
mainAvailability = withCredential(mainAvailability, main); learnAvailability = withCredential(learnAvailability, learn);
|
|
72
|
+
const gate = guard.snapshot(), currentMain = choice(host.agentDefaultModel.currentSelection()), currentLearn = defaults?.read();
|
|
73
|
+
if (JSON.stringify(currentMain) !== JSON.stringify(main)) mainAvailability = unavailable('native-selection-changed-during-status');
|
|
74
|
+
if (currentLearn?.revision !== learn?.revision) learnAvailability = unavailable('learning-selection-changed-during-status');
|
|
75
|
+
if (gate.phase !== 'ready') mainAvailability = learnAvailability = unavailable('runtime-not-ready');
|
|
76
|
+
return { gate, main: { defaultSelection: currentMain, availability: mainAvailability },
|
|
77
|
+
learning: { enabled: Boolean(config.learn && defaults), defaults: defaultView(currentLearn), availability: learnAvailability },
|
|
78
|
+
routes: list.map(route => ({ ...route, usage: usage(route.provider) })) };
|
|
79
|
+
},
|
|
80
|
+
async saveLearning(request, assertCaller = () => {}) {
|
|
81
|
+
assertCaller(); assertReady();
|
|
82
|
+
const defaults = host.get('arcfoldLearningDefaults');
|
|
83
|
+
if (!config.learn || !defaults) fail('learning_configuration_unavailable', '当前部署尚未启用独立学习。');
|
|
84
|
+
// The default service owns exact field/revision checks. Catalog reads may
|
|
85
|
+
// await, so current authorization and revision are checked again at save.
|
|
86
|
+
const list = await catalog();
|
|
87
|
+
const model = list.find(route => route.provider === request?.provider)?.models.find(model => model.id === request?.model);
|
|
88
|
+
if (!model?.reasoningEfforts.includes(request?.reasoningEffort))
|
|
89
|
+
fail('learning_configuration_unavailable', '所选模型或思考强度不在当前可用目录中。');
|
|
90
|
+
assertCaller(); assertReady();
|
|
91
|
+
return defaultView(defaults.save(request));
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const name = 'arcfold-runtime-model-settings';
|
|
97
|
+
export const inject = ['arcfoldRuntimeGuard', 'arcfoldBusiness', 'sessions', 'sessionController',
|
|
98
|
+
'agentDefaultModel', 'llm', 'credentials', 'configEditor'];
|
|
99
|
+
export function apply(ctx, { runtime } = {}) {
|
|
100
|
+
const config = structuredClone(runtime), guard = getRuntimeGuard(ctx);
|
|
101
|
+
const service = createRuntimeModelSettings(ctx, config, guard);
|
|
102
|
+
const handler = createModelSelectionHandler({ sessions: ctx.sessions, defaults: ctx.agentDefaultModel,
|
|
103
|
+
assertSelection: service.assertMainSelection });
|
|
104
|
+
let active = true;
|
|
105
|
+
const dispose = ctx.on('session/select-model', payload => handler(payload));
|
|
106
|
+
ctx.effect(() => () => { active = false; dispose(); }, 'remove native idle model selection');
|
|
107
|
+
ctx.provide('arcfoldRuntimeModels', Object.freeze({ ...service,
|
|
108
|
+
identity: Object.freeze({ fiber: ctx.fiber, controller: serviceIdentity(ctx.sessionController),
|
|
109
|
+
defaults: serviceIdentity(ctx.agentDefaultModel), sessions: serviceIdentity(ctx.sessions),
|
|
110
|
+
hookActive: () => active }) }));
|
|
111
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { assertPreparationComposition } from './preparation-plugin.mjs';
|
|
2
|
+
import * as NativeGoalDriver from '@deepseek-ai/dsh-goal-round-driver';
|
|
3
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { canonicalJson } from './store.mjs';
|
|
8
|
+
import { assertOpenAIIdentity, serviceIdentity } from './runtime-guard.mjs';
|
|
9
|
+
import { canonicalRuntimePath, runtimeProviderDefaults, runtimeRouteAvailability, runtimeTransport } from './runtime-config.mjs';
|
|
10
|
+
|
|
11
|
+
const CORE = '@arcfold/dsh', UI = '@arcfold/dsh-method-ui';
|
|
12
|
+
const ACTIVE = 2;
|
|
13
|
+
const SERVICES = ['llm', 'sessions', 'sessionPersistence', 'arcfoldBusiness', 'arcfoldBridge', 'arcfoldWork',
|
|
14
|
+
'arcfoldOpportunities', 'arcfoldPreparation', 'arcfoldOpportunityWeb', 'arcfoldMethods', 'arcfoldGoals', 'arcfoldMethodWeb', 'arcfoldGoalWeb', 'agentPresets', 'pluginPackages',
|
|
15
|
+
'clientModules', 'webApp', 'connection', 'credentials', 'agentDefaultModel', 'sessionController', 'configEditor', 'settings',
|
|
16
|
+
'arcfoldRuntimeWorkspace', 'workspaceRegistry', 'workspaceController', 'arcfoldRuntimeModels', 'arcfoldModelWeb'];
|
|
17
|
+
const required = (condition, message) => { if (!condition) throw new Error(message); };
|
|
18
|
+
|
|
19
|
+
/** Loader keeps the thenable returned by registry.plugin; ownership belongs to its underlying Fiber. */
|
|
20
|
+
export function actualRuntimeFiber(entry, root) {
|
|
21
|
+
const wrapper = entry?.fiber, actual = wrapper?.ctx.fiber;
|
|
22
|
+
required(!entry?.disabled && actual?.state === ACTIVE && actual.ctx.root === root && actual.entry === entry &&
|
|
23
|
+
(wrapper === actual || Object.getPrototypeOf(wrapper) === actual),
|
|
24
|
+
`Required component is not active in this host: ${entry?.options.id ?? 'unknown'}`);
|
|
25
|
+
return actual;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The official driver is global on loading/teardown, even when mounted in a
|
|
29
|
+
// scoped preset. Require exactly its root entry and reject every preset copy.
|
|
30
|
+
export async function inspectNativeContinuationDriver(ctx, entries, compositions) {
|
|
31
|
+
const name = '@deepseek-ai/dsh-goal-round-driver';
|
|
32
|
+
const drivers = entries.filter(entry => entry.options.id === 'goal-round-driver' ||
|
|
33
|
+
entry.options.name === name || entry.moduleNamespace?.apply === NativeGoalDriver.apply);
|
|
34
|
+
required(drivers.length === 1, 'Expected one root native goal driver');
|
|
35
|
+
const entry = drivers[0];
|
|
36
|
+
required(entry.options.id === 'goal-round-driver' && entry.options.name === name &&
|
|
37
|
+
entry.moduleNamespace?.apply === NativeGoalDriver.apply,
|
|
38
|
+
'Native goal driver does not use the verified official module');
|
|
39
|
+
for (const composition of compositions) for (const module of composition.modules) {
|
|
40
|
+
required(module.moduleName !== name, 'A preset must not contain another native goal driver');
|
|
41
|
+
// Preset inspection exposes its original specifier/base, not its private
|
|
42
|
+
// Loader tree. Use the same public loader import/normalization for aliases.
|
|
43
|
+
const owner = module.baseUrl ? ctx.extend({ baseUrl: module.baseUrl }) : ctx;
|
|
44
|
+
const loaded = await owner.loader.import(module.moduleName);
|
|
45
|
+
const plugin = owner.loader.unwrapExports(loaded);
|
|
46
|
+
required(plugin !== NativeGoalDriver.apply && plugin?.apply !== NativeGoalDriver.apply,
|
|
47
|
+
'A preset must not contain another native goal driver through a module alias');
|
|
48
|
+
}
|
|
49
|
+
const actual = actualRuntimeFiber(entry, ctx.root);
|
|
50
|
+
for (const service of ['agents', 'goals', 'sessions'])
|
|
51
|
+
required(actual.committed?.[service]?.value === serviceIdentity(ctx.get(service)),
|
|
52
|
+
'Native goal driver is bound to a different host service');
|
|
53
|
+
return actual;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Resolve only the verified local adapter's defaults; unknown routes are never probed. */
|
|
57
|
+
export async function runtimeMainAvailability(llm, config, selection) {
|
|
58
|
+
if (!selection?.provider || !selection?.model) return { available: false, reason: 'main-not-configured' };
|
|
59
|
+
if (!Object.hasOwn(runtimeTransport(config).routes[selection.provider]?.modelPolicies ?? {}, selection.model))
|
|
60
|
+
return { available: false, reason: 'selected-route-not-supported' };
|
|
61
|
+
const defaults = runtimeProviderDefaults(config, selection.provider);
|
|
62
|
+
if (!defaults.available) return { available: false, reason: defaults.reason };
|
|
63
|
+
let resolved;
|
|
64
|
+
try { resolved = await llm.resolveCallConfig(selection); }
|
|
65
|
+
catch { return { available: false, reason: 'selected-model-controls-unavailable' }; }
|
|
66
|
+
required(resolved.provider === selection.provider && resolved.model === selection.model,
|
|
67
|
+
'The model capability lookup replaced the selected route');
|
|
68
|
+
if (config.main && resolved.maxTokens !== config.main.maxTokens)
|
|
69
|
+
return { available: false, reason: 'main-output-limit-unavailable',
|
|
70
|
+
requestedMaxTokens: config.main.maxTokens, resolvedMaxTokens: resolved.maxTokens ?? null };
|
|
71
|
+
return runtimeRouteAvailability({ ...config, main: resolved }).main;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Fresh status projection only: no arm, config write, retry, or secret resolution. */
|
|
75
|
+
export async function refreshRuntimeReadiness(ctx, plan) {
|
|
76
|
+
const unavailable = reason => ({ available: false, reason });
|
|
77
|
+
const gateView = guard => guard?.snapshot() ?? { phase: 'unavailable', reason: 'guard-unavailable' };
|
|
78
|
+
const blocked = (gate, reason = gate.phase === 'requires-restart' ? 'runtime-restart-required' : 'runtime-not-ready') => ({
|
|
79
|
+
mode: 'configuration-only', modelGate: gate,
|
|
80
|
+
routeAvailability: { main: unavailable(reason), learn: unavailable(reason), credentialConfigured: false,
|
|
81
|
+
providerCredentialValidated: false, mainSelectionScope: 'native-default' }, issues: [reason],
|
|
82
|
+
});
|
|
83
|
+
let guard;
|
|
84
|
+
try {
|
|
85
|
+
guard = ctx.get('arcfoldRuntimeGuard');
|
|
86
|
+
let gate = gateView(guard);
|
|
87
|
+
if (gate.phase !== 'ready') return blocked(gate);
|
|
88
|
+
const defaults = ctx.get('agentDefaultModel'), llm = ctx.get('llm'), credentials = ctx.get('credentials');
|
|
89
|
+
required(defaults && llm && credentials, 'Runtime model settings are unavailable');
|
|
90
|
+
const selection = defaults.currentSelection();
|
|
91
|
+
const selectionKey = value => JSON.stringify([value.provider, value.model, value.reasoningEffort]);
|
|
92
|
+
const before = selectionKey(selection);
|
|
93
|
+
let main = await runtimeMainAvailability(llm, plan.config, selection);
|
|
94
|
+
const learningDefaults = ctx.get('arcfoldLearningDefaults');
|
|
95
|
+
const savedLearn = learningDefaults?.read();
|
|
96
|
+
const learnSelection = savedLearn && plan.config.learn ? { ...plan.config.learn,
|
|
97
|
+
provider: savedLearn.provider, model: savedLearn.model, reasoningEffort: savedLearn.reasoningEffort } : null;
|
|
98
|
+
let learn = runtimeRouteAvailability({ ...plan.config, learn: learnSelection }).learn;
|
|
99
|
+
const routes = runtimeTransport(plan.config).routes;
|
|
100
|
+
const providers = {};
|
|
101
|
+
for (const [provider, route] of Object.entries(routes)) providers[provider] = {
|
|
102
|
+
credentialConfigured: (await credentials.describe(route.apiKeyRef)).configured === true, providerCredentialValidated: false,
|
|
103
|
+
};
|
|
104
|
+
const credentialConfigured = providers[selection.provider]?.credentialConfigured === true;
|
|
105
|
+
const withCredential = (value, selected) => value.available && !providers[selected?.provider]?.credentialConfigured
|
|
106
|
+
? { ...value, available: false, reason: 'native-credential-not-configured', credentialConfigured: false }
|
|
107
|
+
: { ...value, credentialConfigured: providers[selected?.provider]?.credentialConfigured === true };
|
|
108
|
+
main = withCredential(main, selection); learn = withCredential(learn, learnSelection);
|
|
109
|
+
// An asynchronous metadata read cannot advertise the earlier topology or
|
|
110
|
+
// selection after a live native edit. The next status call may read again.
|
|
111
|
+
gate = gateView(guard);
|
|
112
|
+
if (ctx.get('arcfoldRuntimeGuard') !== guard || gate.phase !== 'ready') return blocked(gate);
|
|
113
|
+
const latestSelection = ctx.get('agentDefaultModel')?.currentSelection();
|
|
114
|
+
if (!latestSelection || selectionKey(latestSelection) !== before)
|
|
115
|
+
main = unavailable('native-selection-changed-during-status');
|
|
116
|
+
if (learningDefaults !== ctx.get('arcfoldLearningDefaults') || learningDefaults?.read()?.revision !== savedLearn?.revision)
|
|
117
|
+
learn = unavailable('learning-selection-changed-during-status');
|
|
118
|
+
const issues = [!main.available && main.reason, !learn.available && learn.reason,
|
|
119
|
+
plan.config.transport && !credentialConfigured && 'native-credential-not-configured'].filter(Boolean);
|
|
120
|
+
return { mode: main.available && credentialConfigured ? 'execution-ready' : 'configuration-only', modelGate: gate,
|
|
121
|
+
routeAvailability: { main, learn, credentialConfigured, providerCredentialValidated: false,
|
|
122
|
+
mainSelectionScope: 'native-default', providers }, issues };
|
|
123
|
+
} catch {
|
|
124
|
+
return blocked(gateView(guard), 'runtime-readiness-unavailable');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Verify only fixed local modules and receipts, not model endpoints or secrets.
|
|
129
|
+
* Shared by full startup and the real-plugin focused ownership fixtures. */
|
|
130
|
+
export async function inspectRuntimeProviders(ctx, plan, criticalFibers) {
|
|
131
|
+
const routes = runtimeTransport(plan.config).routes;
|
|
132
|
+
const providers = Object.keys(routes).filter(provider => runtimeProviderDefaults(plan.config, provider).available).sort();
|
|
133
|
+
required(canonicalJson(ctx.llm.listProviders().map(value => value.id).sort()) === canonicalJson(providers),
|
|
134
|
+
'The running model routes differ from the installed metered routes');
|
|
135
|
+
const shared = plan.config.schema === 'arcfold-local-v2';
|
|
136
|
+
for (const provider of providers) {
|
|
137
|
+
const openai = provider === 'openai';
|
|
138
|
+
const proof = plan.installation.providers?.[provider] ?? (!openai ? plan.installation.provider : undefined);
|
|
139
|
+
required(proof?.path, `The installation has no actual consuming-copy proof for ${provider}`);
|
|
140
|
+
const importer = pathToFileURL(join(plan.installation.coreRoot, 'src', openai ? 'openai-provider.mjs' : 'deepseek-provider.mjs')).href;
|
|
141
|
+
const packageName = openai ? '@deepseek-ai/dsh-llm-pi-ai' : '@deepseek-ai/dsh-llm-deepseek';
|
|
142
|
+
const actual = ctx.pluginPackages.packageOf(packageName, importer);
|
|
143
|
+
required(actual && realpathSync(actual.dir) === realpathSync(proof.path), `The ${provider} wrapper resolves to an unverified copy`);
|
|
144
|
+
const moduleURL = pathToFileURL(join(actual.dir, 'lib/index.js')).href;
|
|
145
|
+
const loaded = await import(moduleURL);
|
|
146
|
+
if (openai) {
|
|
147
|
+
required(shared && typeof proof.receiptSHA256 === 'string' && /^[a-f0-9]{64}$/.test(proof.receiptSHA256) && proof.receipt,
|
|
148
|
+
'OpenAI requires a new exact installation patch receipt');
|
|
149
|
+
const receiptBytes = readFileSync(proof.receipt);
|
|
150
|
+
required(createHash('sha256').update(receiptBytes).digest('hex') === proof.receiptSHA256, 'The OpenAI installation receipt changed');
|
|
151
|
+
const inspector = await import(new URL('../scripts/patch-pi-ai-transport.mjs', import.meta.url));
|
|
152
|
+
required(inspector.inspectPiAiPatch(actual.dir).state === 'patched' && proof.patchId === inspector.patchSpec.id &&
|
|
153
|
+
loaded.PiAiAdapter.transportSeam === 'pi-ai-create-transport-v2', 'The actual OpenAI consuming copy lacks the exact loaded interface');
|
|
154
|
+
const wrapper = criticalFibers['arcfold-openai-provider'];
|
|
155
|
+
const llm = [...criticalFibers.llm.provisions].find(record => record.name === 'llm');
|
|
156
|
+
const metered = [...criticalFibers['arcfold-shared-metered-host'].provisions].find(record => record.name === 'arcfoldMeteredTransport');
|
|
157
|
+
const identity = assertOpenAIIdentity(ctx.root, wrapper, llm, metered);
|
|
158
|
+
required(realpathSync(fileURLToPath(identity.moduleURL)) === realpathSync(fileURLToPath(moduleURL)) &&
|
|
159
|
+
identity.nativeFiber.runtime.callback === loaded.apply,
|
|
160
|
+
'The running native OpenAI child is not the verified installed module');
|
|
161
|
+
criticalFibers['arcfold-openai-native'] = identity.nativeFiber;
|
|
162
|
+
} else {
|
|
163
|
+
const inspector = await import(new URL('../scripts/patch-deepseek-transport.mjs', import.meta.url));
|
|
164
|
+
required(inspector.inspectDeepSeekPatch(actual.dir).state === 'patched' && proof.patchId === inspector.patchSpec.id &&
|
|
165
|
+
loaded.registerDeepSeekProvider.transportSeam === 'deepseek-messages-create-transport-v1', 'The loaded DeepSeek provider lacks metered dispatch');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return providers;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
/** Check the actual official profile consumer and the one owned settings hook. */
|
|
173
|
+
export async function inspectRuntimeModelSelection(ctx, plan, criticalFibers) {
|
|
174
|
+
const proof = plan.installation.modelSelection;
|
|
175
|
+
required(proof?.path && proof.receipt && /^[a-f0-9]{64}$/.test(proof.receiptSHA256 ?? ''),
|
|
176
|
+
'This installed version has no native model-selection patch proof');
|
|
177
|
+
const parent = pathToFileURL(join(plan.installation.profileDir, 'cordis.yml')).href;
|
|
178
|
+
const actual = ctx.pluginPackages.packageOf('@deepseek-ai/dsh-api-session-controller', parent);
|
|
179
|
+
required(actual && realpathSync(actual.dir) === realpathSync(proof.path), 'The native controller uses an unverified consuming copy');
|
|
180
|
+
required(createHash('sha256').update(readFileSync(proof.receipt)).digest('hex') === proof.receiptSHA256,
|
|
181
|
+
'The native controller patch receipt changed');
|
|
182
|
+
const inspector = await import(new URL('../scripts/patch-model-selection.mjs', import.meta.url));
|
|
183
|
+
const loaded = await import(pathToFileURL(join(actual.dir, 'lib/index.js')).href);
|
|
184
|
+
required(inspector.inspectModelSelectionPatch(actual.dir).state === 'patched' && proof.patchId === inspector.patchSpec.id &&
|
|
185
|
+
loaded.SessionController.modelSelectionSeam === 'session-model-selection-commit-v1' &&
|
|
186
|
+
serviceIdentity(ctx.sessionController).constructor === loaded.SessionController,
|
|
187
|
+
'The running native controller lacks the verified model-selection interface');
|
|
188
|
+
const modelFiber = criticalFibers['arcfold-runtime-model-settings'];
|
|
189
|
+
const matches = [...modelFiber.provisions].filter(value => value.name === 'arcfoldRuntimeModels');
|
|
190
|
+
const models = serviceIdentity(ctx.arcfoldRuntimeModels), identity = models.identity;
|
|
191
|
+
required(matches.length === 1 && matches[0].value === models && Object.isFrozen(identity) &&
|
|
192
|
+
identity.fiber === modelFiber && identity.hookActive() === true &&
|
|
193
|
+
identity.controller === serviceIdentity(ctx.sessionController) && identity.defaults === serviceIdentity(ctx.agentDefaultModel) &&
|
|
194
|
+
identity.sessions === serviceIdentity(ctx.sessions), 'The required native model selection handler is not owned by this runtime');
|
|
195
|
+
const defaults = criticalFibers['agent-default-model'];
|
|
196
|
+
required(defaults.entry && ctx.configEditor.entries().includes(defaults.entry) &&
|
|
197
|
+
canonicalRuntimePath(ctx.configEditor.documentPath) === canonicalRuntimePath(join(plan.nativeHome, 'arcfold.patch.yml')),
|
|
198
|
+
'The native model default has no verified persistent profile editor');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Observe the actual official host; this does not send a model or resolve a secret. */
|
|
202
|
+
export async function inspectRuntimeReady(ctx, plan) {
|
|
203
|
+
const guard = ctx.get('arcfoldRuntimeGuard');
|
|
204
|
+
required(guard, 'The root-owned model guard is absent');
|
|
205
|
+
try {
|
|
206
|
+
const expectedAdapterUpdates = guard.snapshot().adapterUpdates;
|
|
207
|
+
const services = Object.fromEntries(SERVICES.map(name => [name, ctx.get(name)]));
|
|
208
|
+
for (const name of SERVICES) required(services[name], `Required runtime service is not active: ${name}`);
|
|
209
|
+
const loader = ctx.get('loader'); required(loader, 'Official loader is absent');
|
|
210
|
+
const entries = [...loader.entries()];
|
|
211
|
+
const entry = id => {
|
|
212
|
+
const matches = entries.filter(value => value.options.id === id);
|
|
213
|
+
required(matches.length === 1, `Expected one actual component: ${id}`);
|
|
214
|
+
return matches[0];
|
|
215
|
+
};
|
|
216
|
+
const fiber = id => actualRuntimeFiber(entry(id), ctx.root);
|
|
217
|
+
const profile = ctx.get('profileContext');
|
|
218
|
+
required(profile && realpathSync(profile.dir) === plan.installation.profileDir &&
|
|
219
|
+
realpathSync(profile.installAnchor) === plan.installation.installAnchor &&
|
|
220
|
+
realpathSync(profile.home) === plan.nativeHome && realpathSync(profile.cwd) === plan.workspace,
|
|
221
|
+
'The host is using a different profile, data home, installation or workspace');
|
|
222
|
+
const parent = pathToFileURL(join(plan.installation.profileDir, 'package.json')).href;
|
|
223
|
+
const packages = services.pluginPackages;
|
|
224
|
+
for (const [name, directory] of [[CORE, plan.installation.coreRoot], [UI, plan.installation.uiRoot]])
|
|
225
|
+
required(realpathSync(packages.packageOf(name, parent)?.dir ?? '') === directory, `Actual loaded package differs: ${name}`);
|
|
226
|
+
const uiPath = services.clientModules.clientPath(UI);
|
|
227
|
+
required(uiPath && realpathSync(uiPath).startsWith(plan.installation.uiRoot + '/'), 'The installed UI client is absent');
|
|
228
|
+
required(services.clientModules.graph().entries.filter(value => value.id === UI).length === 1, 'The UI module is absent or duplicated');
|
|
229
|
+
const typert = ctx.get('typert');
|
|
230
|
+
for (const endpoint of ['arcfoldMethods/list', 'arcfoldMethods/decide', 'arcfoldGoals/list', 'arcfoldGoals/decide',
|
|
231
|
+
'arcfoldOpportunities/list', 'arcfoldOpportunities/savePermission', 'arcfoldOpportunities/prepare', 'arcfoldOpportunities/continue',
|
|
232
|
+
'session/modelCatalog', 'session/selectModel', 'arcfoldModels/read', 'arcfoldModels/saveLearning'])
|
|
233
|
+
required(typert?.local.get(endpoint), `The actual UI endpoint is missing: ${endpoint}`);
|
|
234
|
+
const presets = await services.agentPresets.list();
|
|
235
|
+
for (const id of ['arcfold-main', 'arcfold-learning', 'arcfold-preparation'])
|
|
236
|
+
required(presets.filter(value => value.id === id && !value.broken).length === 1, `Required preset is missing: ${id}`);
|
|
237
|
+
const compositions = services.agentPresets.inspectCompositions();
|
|
238
|
+
required(compositions.find(value => value.id === 'arcfold-main')?.modules.some(value => value.moduleName === '@arcfold/dsh/agent'),
|
|
239
|
+
'The native main preset lacks Arcfold ownership');
|
|
240
|
+
required(!compositions.find(value => value.id === 'arcfold-main')?.modules.some(value =>
|
|
241
|
+
['@deepseek-ai/dsh-tool-goal', '@deepseek-ai/dsh-command-goal'].includes(value.moduleName)),
|
|
242
|
+
'Native goal mutations must not bypass Arcfold stage control');
|
|
243
|
+
required(ctx.get('arcfoldWork'), 'Arcfold execution-stage control is missing');
|
|
244
|
+
required(compositions.find(value => value.id === 'arcfold-learning')?.modules.length === 0, 'Learn acquired tool-bearing modules');
|
|
245
|
+
assertPreparationComposition(compositions.find(value => value.id === 'arcfold-preparation'),
|
|
246
|
+
entry('preset-arcfold-preparation').fiber.ctx.fiber.rawConfig.plugins);
|
|
247
|
+
const nativeDriver = await inspectNativeContinuationDriver(ctx, entries, compositions);
|
|
248
|
+
const configured = runtimeRouteAvailability(plan.config);
|
|
249
|
+
required(services.arcfoldRuntimeWorkspace.requestedPath === plan.workspace,
|
|
250
|
+
'The native workspace initialization used a different deployment directory');
|
|
251
|
+
const criticalFibers = Object.fromEntries(['llm', 'arcfold-business', 'preset-arcfold-main', 'arcfold-preparation', 'preset-arcfold-preparation',
|
|
252
|
+
'fs-observation-policy', 'fs-sandbox', 'tool-executor-native',
|
|
253
|
+
'arcfold-runtime-workspace', 'workspace-controller', 'arcfold-runtime-model-settings',
|
|
254
|
+
'session', 'session-controller', 'agent-default-model', 'config-editor', 'arcfold-ui'].map(id => [id, fiber(id)]));
|
|
255
|
+
criticalFibers['goal-round-driver'] = nativeDriver;
|
|
256
|
+
criticalFibers.goal = fiber('goal');
|
|
257
|
+
await inspectRuntimeModelSelection(ctx, plan, criticalFibers);
|
|
258
|
+
let identifyRequest;
|
|
259
|
+
const routes = runtimeTransport(plan.config).routes;
|
|
260
|
+
const enabled = Object.keys(routes).filter(provider => runtimeProviderDefaults(plan.config, provider).available);
|
|
261
|
+
if (enabled.length) {
|
|
262
|
+
for (const id of ['credentials', 'session']) criticalFibers[id] = fiber(id);
|
|
263
|
+
if (plan.config.schema === 'arcfold-local-v2') criticalFibers['arcfold-shared-metered-host'] = fiber('arcfold-shared-metered-host');
|
|
264
|
+
else criticalFibers['arcfold-metered-budget'] = fiber('arcfold-metered-budget');
|
|
265
|
+
if (enabled.includes('deepseek-official')) criticalFibers['arcfold-metered-provider'] = fiber('arcfold-metered-provider');
|
|
266
|
+
if (enabled.includes('openai')) criticalFibers['arcfold-openai-provider'] = fiber('arcfold-openai-provider');
|
|
267
|
+
const { nativeRequestIdentity } = await import(pathToFileURL(join(plan.installation.coreRoot, 'src/native-request-authority.mjs')).href);
|
|
268
|
+
identifyRequest = options => nativeRequestIdentity(services.arcfoldBusiness, services.sessions, options);
|
|
269
|
+
}
|
|
270
|
+
await inspectRuntimeProviders(ctx, plan, criticalFibers);
|
|
271
|
+
if (configured.learn.available) {
|
|
272
|
+
required(ctx.get('arcfoldLearning'), 'Configured independent learning is not active');
|
|
273
|
+
criticalFibers['arcfold-learning'] = fiber('arcfold-learning');
|
|
274
|
+
criticalFibers['preset-arcfold-learning'] = fiber('preset-arcfold-learning');
|
|
275
|
+
}
|
|
276
|
+
const rawUrl = new URL(services.webApp.getAdvertisedUrl());
|
|
277
|
+
required(rawUrl.hostname === '127.0.0.1' && rawUrl.protocol === 'http:', 'Unexpected local web listener');
|
|
278
|
+
rawUrl.username = ''; rawUrl.password = ''; rawUrl.search = ''; rawUrl.hash = '';
|
|
279
|
+
// No HTTP inference: each required service, actual module and native preset was inspected above.
|
|
280
|
+
guard.arm({ criticalFibers, identifyRequest, expectedAdapterUpdates });
|
|
281
|
+
return { url: rawUrl.href, identity: { sourceCommit: plan.installation.sourceCommit, profile: plan.installation.profileName,
|
|
282
|
+
core: plan.installation.coreRoot, ui: plan.installation.uiRoot },
|
|
283
|
+
...await refreshRuntimeReadiness(ctx, plan) };
|
|
284
|
+
} catch (error) { guard.fail(error); throw error; }
|
|
285
|
+
}
|