dsh-openwrite 0.2.28 → 0.2.34
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 +1 -1
- package/README.md +49 -8
- package/THIRD_PARTY_NOTICES.md +4 -3
- package/companions.mjs +508 -0
- package/cordis.patch.yml +0 -2
- package/docs/CLOSED_ISSUE_FOLLOWUPS.md +1 -1
- package/docs/COMPANION_PLUGINS.md +142 -0
- package/docs/CONTEXT_SLOTS.md +1 -1
- package/docs/COST_MEASUREMENT.md +2 -2
- package/docs/DEVELOPMENT.md +6 -11
- package/docs/DSH_STORE.md +14 -2
- package/docs/EDITOR_SAFETY_REPAIRS.md +2 -2
- package/docs/INSTALL.md +112 -4
- package/docs/ISSUES_121_123.md +4 -4
- package/docs/ISSUES_140_142.md +58 -0
- package/docs/ISSUES_149_150_151.md +77 -0
- package/docs/ISSUE_123_FOLLOWUP.md +3 -2
- package/docs/ISSUE_146_REVIEW_EVIDENCE.md +53 -0
- package/docs/LEARNING.md +1 -1
- package/docs/MOBILE_READINESS.md +62 -0
- package/docs/PLUGIN_MAINTENANCE.md +1 -3
- package/docs/RELEASE_ACCEPTANCE.md +20 -3
- package/docs/REVIEW_ROUND_22.md +67 -0
- package/docs/REVIEW_ROUND_23.md +48 -0
- package/docs/REVIEW_ROUND_25.md +51 -0
- package/docs/SELECTION_UPGRADE_REPAIRS.md +2 -2
- package/docs/TOOLS.md +181 -604
- package/docs/WORKFLOWS.md +5 -5
- package/package.json +17 -21
- package/packages/openwrite-bridge/lib/client.d.ts +0 -1
- package/packages/openwrite-bridge/lib/client.js +90 -9
- package/packages/openwrite-bridge/lib/contracts-generated.js +2 -1
- package/packages/openwrite-bridge/lib/dog-delivery.d.ts +1 -0
- package/packages/openwrite-bridge/lib/dog-delivery.js +3 -1
- package/packages/openwrite-bridge/lib/dog-review.js +15 -3
- package/packages/openwrite-bridge/lib/domain.d.ts +27 -0
- package/packages/openwrite-bridge/lib/domain.js +250 -29
- package/packages/openwrite-bridge/lib/host-llm.d.ts +188 -0
- package/packages/openwrite-bridge/lib/host-llm.js +657 -0
- package/packages/openwrite-bridge/lib/host-web.d.ts +61 -0
- package/packages/openwrite-bridge/lib/host-web.js +166 -0
- package/packages/openwrite-bridge/lib/index.d.ts +4 -0
- package/packages/openwrite-bridge/lib/index.js +150 -6
- package/packages/openwrite-bridge/lib/managed-runtime.d.ts +47 -2
- package/packages/openwrite-bridge/lib/managed-runtime.js +176 -34
- package/packages/openwrite-bridge/lib/tool-policy.js +37 -12
- package/packages/openwrite-bridge/lib/tools.js +1223 -1227
- package/packages/openwrite-bridge/package.json +7 -5
- package/packages/studio-panel/lib/client.js +123207 -121534
- package/packages/studio-panel/lib/client.js.map +1 -1
- package/packages/studio-panel/lib/types/client/LaunchOpenWrite.d.ts +10 -0
- package/packages/studio-panel/lib/types/client/ModelView.d.ts +28 -4
- package/packages/studio-panel/lib/types/client/OpenWriteSettings.d.ts +64 -0
- package/packages/studio-panel/lib/types/client/SceneWorkbench.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/SearchSettings.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/SerialWritingView.d.ts +2 -1
- package/packages/studio-panel/lib/types/client/TurnMutationSummary.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/VditorBody.d.ts +1 -1
- package/packages/studio-panel/lib/types/client/api.d.ts +41 -2
- package/packages/studio-panel/lib/types/client/browser-download.d.ts +4 -0
- package/packages/studio-panel/lib/types/client/companions.d.ts +46 -0
- package/packages/studio-panel/lib/types/client/dto.d.ts +22 -0
- package/packages/studio-panel/lib/types/client/locales.d.ts +1 -1
- package/packages/studio-panel/lib/types/client/workspace-chrome.d.ts +41 -0
- package/packages/studio-panel/package.json +5 -3
- package/plugin.mjs +159 -20
- package/preset-definition.mjs +144 -4
- package/presets/openwrite/agent.cordis.yml +7 -9
- package/presets/openwrite/ops-integration.mjs +28 -0
- package/presets/openwrite/preset.yml +2 -1
- package/presets/openwrite/skills/canon/SKILL.md +3 -3
- package/presets/openwrite/skills/delivery-artifact-query/SKILL.md +41 -0
- package/presets/openwrite/skills/dialoguequality/SKILL.md +16 -17
- package/presets/openwrite/skills/dog-delivery-verification/SKILL.md +113 -0
- package/presets/openwrite/skills/export-book/SKILL.md +3 -3
- package/presets/openwrite/skills/foreshadow/SKILL.md +1 -1
- package/presets/openwrite/skills/foreshadowing-system/SKILL.md +16 -25
- package/presets/openwrite/skills/import-artifact-query/SKILL.md +30 -0
- package/presets/openwrite/skills/learn/SKILL.md +3 -3
- package/presets/openwrite/skills/novel-creator/SKILL.md +10 -10
- package/presets/openwrite/skills/novel-reviewer/SKILL.md +11 -11
- package/presets/openwrite/skills/progress/SKILL.md +1 -1
- package/presets/openwrite/skills/review-artifact-query/SKILL.md +41 -0
- package/presets/openwrite/skills/review-chapter/SKILL.md +2 -2
- package/presets/openwrite/skills/revise-span/SKILL.md +5 -5
- package/presets/openwrite/skills/style-system/SKILL.md +13 -6
- package/presets/openwrite/skills/truth-validation/SKILL.md +9 -12
- package/presets/openwrite/skills/workflow-manager/SKILL.md +13 -13
- package/presets/openwrite/skills/world-query/SKILL.md +16 -11
- package/presets/openwrite/skills/write-next/SKILL.md +1 -1
- package/release/{openwrite-5.8.20-py3-none-any.whl → openwrite-5.8.30-py3-none-any.whl} +0 -0
- package/release/runtime-manifest.json +4 -13
- package/release/source.json +21 -21
- package/release/wheels/licenses/OPENSSL-3.6.3-LICENSE.txt +225 -0
- package/scripts/maintenance.mjs +67 -7
- package/scripts/plugin-doctor.mjs +179 -0
- package/scripts/prepare.mjs +26 -4
- package/scripts/safe-upgrade.mjs +38 -5
- package/dog-tools.mjs +0 -13
- package/presets/openwrite/skills/dog-delivery-query/SKILL.md +0 -42
- package/presets/openwrite/skills/dog-import-query/SKILL.md +0 -29
- package/presets/openwrite/skills/dog-review-query/SKILL.md +0 -42
- package/scripts/dog/delivery-stage.js +0 -32
- package/scripts/dog/import-chapter.js +0 -20
- package/scripts/dog/import-record.js +0 -35
- package/scripts/dog/review-dimension.js +0 -41
- package/scripts/dog/review-record.js +0 -42
- package/vendor/dsh-dog/LICENSE +0 -29
- package/vendor/dsh-dog/README.md +0 -114
- package/vendor/dsh-dog/UPSTREAM.md +0 -30
- package/vendor/dsh-dog/cordis.patch.yml +0 -14
- package/vendor/dsh-dog/lib/client.js +0 -2960
- package/vendor/dsh-dog/lib/client.js.map +0 -1
- package/vendor/dsh-dog/lib/core--byUPJL8.js +0 -2191
- package/vendor/dsh-dog/lib/core-BiEBMLz3.d.ts +0 -495
- package/vendor/dsh-dog/lib/core.d.ts +0 -2
- package/vendor/dsh-dog/lib/core.js +0 -2
- package/vendor/dsh-dog/lib/index.d.ts +0 -143
- package/vendor/dsh-dog/lib/index.js +0 -1230
- package/vendor/dsh-dog/package.json +0 -110
- package/vendor/dsh-dog/schemas/schema-0.2/graph.schema.json +0 -139
- package/vendor/dsh-dog/schemas/schema-0.2/report.schema.json +0 -87
- package/vendor/dsh-dog/schemas/schema-0.2/run.schema.json +0 -259
- package/vendor/dsh-dog/schemas/schema-0.2/runtime-event.schema.json +0 -105
- package/vendor/dsh-dog/schemas/schema-0.2/verification.schema.json +0 -40
|
@@ -13,7 +13,7 @@ export class UnsupportedRuntimePlatformError extends Error {
|
|
|
13
13
|
code = 'RUNTIME_PLATFORM_UNSUPPORTED';
|
|
14
14
|
status = 503;
|
|
15
15
|
constructor(platform) {
|
|
16
|
-
super(`OpenWrite Studio managed runtime is unavailable on ${platform}.
|
|
16
|
+
super(`OpenWrite Studio managed runtime is unavailable on ${platform}. Set pythonPath or OPENWRITE_PYTHON to a prepared Python interpreter, or configure the bridge with mode: external and baseUrl pointing to a supported Studio server; see docs/INSTALL.md#external-studio. File tools that do not require Studio remain available.`);
|
|
17
17
|
this.platform = platform;
|
|
18
18
|
this.name = 'UnsupportedRuntimePlatformError';
|
|
19
19
|
}
|
|
@@ -22,6 +22,8 @@ export const DEFAULT_RESTART_LIMIT = 5;
|
|
|
22
22
|
export const DEFAULT_BACKOFF_MS = [1_000, 2_000, 4_000, 8_000, 16_000];
|
|
23
23
|
/** Budget for the per-request liveness probe of a previously-ready backend. */
|
|
24
24
|
const HEALTH_PROBE_TIMEOUT_MS = 1_500;
|
|
25
|
+
/** Second, longer budget for an ambiguous probe before calling it `unknown`. */
|
|
26
|
+
const HEALTH_PROBE_RETRY_MS = 8_000;
|
|
25
27
|
/** A download with no progress for this long is cut and retried on the next source. */
|
|
26
28
|
const DOWNLOAD_IDLE_TIMEOUT_MS = 30_000;
|
|
27
29
|
/** The backend stderr log on disk keeps only the newest chunk. */
|
|
@@ -181,9 +183,17 @@ export class ManagedRuntime {
|
|
|
181
183
|
wait;
|
|
182
184
|
restartLimit;
|
|
183
185
|
backoffMs;
|
|
186
|
+
pythonPath;
|
|
187
|
+
pythonModulePaths;
|
|
188
|
+
hostLlm;
|
|
189
|
+
hostWeb;
|
|
184
190
|
constructor(root, artifacts, options = {}) {
|
|
185
191
|
this.root = root;
|
|
186
192
|
this.artifacts = artifacts;
|
|
193
|
+
this.pythonPath = options.pythonPath?.trim() || process.env['OPENWRITE_PYTHON']?.trim() || undefined;
|
|
194
|
+
this.pythonModulePaths = [...(options.pythonModulePaths ?? [])];
|
|
195
|
+
this.hostLlm = options.hostLlm;
|
|
196
|
+
this.hostWeb = options.hostWeb;
|
|
187
197
|
this.spawnFn = options.spawn ?? spawn;
|
|
188
198
|
this.wait = options.delay ?? delay;
|
|
189
199
|
this.restartLimit = options.restartLimit ?? DEFAULT_RESTART_LIMIT;
|
|
@@ -241,7 +251,12 @@ export class ManagedRuntime {
|
|
|
241
251
|
* forcefully and pushed through the normal recovery path.
|
|
242
252
|
*/
|
|
243
253
|
async verifyConnection(connection) {
|
|
244
|
-
|
|
254
|
+
const liveness = await this.probeLiveness(connection);
|
|
255
|
+
// Only a *confirmed* dead backend justifies tearing it down. A probe that
|
|
256
|
+
// merely timed out is ambiguous — a backend busy running chapter_write or
|
|
257
|
+
// review answers /api/health late — and killing it destroys exactly the
|
|
258
|
+
// in-flight work this recovery path exists to protect.
|
|
259
|
+
if (liveness === 'alive') {
|
|
245
260
|
if (this.closed)
|
|
246
261
|
throw new Error('OpenWrite was removed');
|
|
247
262
|
const current = this.connection;
|
|
@@ -249,6 +264,14 @@ export class ManagedRuntime {
|
|
|
249
264
|
return current;
|
|
250
265
|
return this.queueRestart();
|
|
251
266
|
}
|
|
267
|
+
if (liveness === 'unknown') {
|
|
268
|
+
// Keep serving the cached connection and let the real request decide; a
|
|
269
|
+
// slow health answer is not a reason to invalidate a working backend.
|
|
270
|
+
const current = this.connection;
|
|
271
|
+
if (current)
|
|
272
|
+
return current;
|
|
273
|
+
return this.queueRestart();
|
|
274
|
+
}
|
|
252
275
|
if (this.connection !== connection)
|
|
253
276
|
return this.queueRestart();
|
|
254
277
|
this.connection = undefined;
|
|
@@ -263,22 +286,76 @@ export class ManagedRuntime {
|
|
|
263
286
|
this.update('recovering', 'The writing backend is unresponsive; recovering');
|
|
264
287
|
return this.queueRestart();
|
|
265
288
|
}
|
|
266
|
-
/**
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
289
|
+
/**
|
|
290
|
+
* The host relay coordinates, if the host offers one.
|
|
291
|
+
*
|
|
292
|
+
* A relay that cannot be reached is not a startup failure: Core still runs,
|
|
293
|
+
* it simply has no host models to offer. The failure is silent on purpose —
|
|
294
|
+
* the relay's own diagnostics belong to the host, not to the backend boot.
|
|
295
|
+
*/
|
|
296
|
+
async resolveHostLlm() {
|
|
297
|
+
if (this.hostLlm === undefined)
|
|
298
|
+
return undefined;
|
|
271
299
|
try {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
300
|
+
const resolved = await this.hostLlm();
|
|
301
|
+
if (resolved === undefined || resolved === null)
|
|
302
|
+
return undefined;
|
|
303
|
+
if (typeof resolved.url !== 'string' || resolved.url === '')
|
|
304
|
+
return undefined;
|
|
305
|
+
if (typeof resolved.token !== 'string' || resolved.token === '')
|
|
306
|
+
return undefined;
|
|
307
|
+
return { url: resolved.url, token: resolved.token };
|
|
277
308
|
}
|
|
278
309
|
catch {
|
|
279
|
-
return
|
|
310
|
+
return undefined;
|
|
280
311
|
}
|
|
281
312
|
}
|
|
313
|
+
async resolveHostWeb() {
|
|
314
|
+
if (this.hostWeb === undefined)
|
|
315
|
+
return undefined;
|
|
316
|
+
try {
|
|
317
|
+
const resolved = await this.hostWeb();
|
|
318
|
+
if (resolved === undefined)
|
|
319
|
+
return undefined;
|
|
320
|
+
return { url: resolved.url, token: resolved.token };
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Liveness with three outcomes. `dead` is only returned on positive evidence —
|
|
328
|
+
* the child exited, or the connection was refused, or health answered
|
|
329
|
+
* non-ok (auth drift). A probe that timed out returns `unknown`, because a
|
|
330
|
+
* busy backend is indistinguishable from a hung one at a 1.5 s deadline and
|
|
331
|
+
* killing the former destroys in-flight work.
|
|
332
|
+
*/
|
|
333
|
+
async probeLiveness(connection) {
|
|
334
|
+
const child = this.child;
|
|
335
|
+
if (!child || child.exitCode !== null || child.signalCode !== null)
|
|
336
|
+
return 'dead';
|
|
337
|
+
for (const budget of [HEALTH_PROBE_TIMEOUT_MS, HEALTH_PROBE_RETRY_MS]) {
|
|
338
|
+
try {
|
|
339
|
+
const response = await fetch(connection.baseUrl + '/api/health', {
|
|
340
|
+
headers: { Authorization: `Bearer ${connection.token}` },
|
|
341
|
+
signal: AbortSignal.timeout(budget),
|
|
342
|
+
});
|
|
343
|
+
return response.ok ? 'alive' : 'dead';
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
// A refusal or a dead socket is positive evidence; anything else (the
|
|
347
|
+
// timeout we just imposed, DNS, reset) is not.
|
|
348
|
+
const code = error.code
|
|
349
|
+
?? error.cause?.code;
|
|
350
|
+
if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'EAI_AGAIN')
|
|
351
|
+
return 'dead';
|
|
352
|
+
if (error instanceof Error && error.name === 'TimeoutError')
|
|
353
|
+
continue;
|
|
354
|
+
return 'dead';
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return 'unknown';
|
|
358
|
+
}
|
|
282
359
|
async cancel() {
|
|
283
360
|
this.cancelEpoch += 1;
|
|
284
361
|
this.allowRevive = false;
|
|
@@ -530,7 +607,7 @@ export class ManagedRuntime {
|
|
|
530
607
|
async command(executable, args, signal) {
|
|
531
608
|
signal.throwIfAborted();
|
|
532
609
|
const child = this.spawnFn(executable, args, { cwd: this.root, detached: process.platform !== 'win32', windowsHide: true,
|
|
533
|
-
env: buildChildEnv(process.env, { UV_CACHE_DIR: join(this.root, 'cache', 'uv'), UV_PYTHON_DOWNLOADS: 'never', UV_NO_CONFIG: '1' }),
|
|
610
|
+
env: buildChildEnv(process.env, { ...this.pythonLibraryEnv(), UV_CACHE_DIR: join(this.root, 'cache', 'uv'), UV_PYTHON_DOWNLOADS: 'never', UV_NO_CONFIG: '1' }),
|
|
534
611
|
stdio: ['ignore', 'ignore', 'pipe'] });
|
|
535
612
|
let failure = '';
|
|
536
613
|
child.stderr?.on('data', bytes => { failure = (failure + String(bytes)).slice(-2000); });
|
|
@@ -547,6 +624,10 @@ export class ManagedRuntime {
|
|
|
547
624
|
signal.removeEventListener('abort', abort);
|
|
548
625
|
}
|
|
549
626
|
}
|
|
627
|
+
/** Shared Android/embedded Python may need its host's native-library search paths. */
|
|
628
|
+
pythonLibraryEnv() {
|
|
629
|
+
return this.pythonPath ? Object.fromEntries(['LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH', 'DYLD_FALLBACK_LIBRARY_PATH'].map(key => [key, process.env[key]])) : {};
|
|
630
|
+
}
|
|
550
631
|
async lock(signal) {
|
|
551
632
|
for (;;) {
|
|
552
633
|
signal.throwIfAborted();
|
|
@@ -567,7 +648,13 @@ export class ManagedRuntime {
|
|
|
567
648
|
async prepare(signal) {
|
|
568
649
|
const manifest = JSON.parse(await readFile(join(this.artifacts, 'runtime-manifest.json'), 'utf8'));
|
|
569
650
|
const platform = manifest.platforms[`${process.platform}-${process.arch}`];
|
|
570
|
-
if (!
|
|
651
|
+
if (this.pythonPath && !isAbsolute(this.pythonPath))
|
|
652
|
+
throw new Error('pythonPath / OPENWRITE_PYTHON must be an absolute path to a Python executable, without command-line arguments');
|
|
653
|
+
if (this.pythonModulePaths.length && !this.pythonPath)
|
|
654
|
+
throw new Error('pythonModulePaths requires pythonPath or OPENWRITE_PYTHON');
|
|
655
|
+
if (this.pythonModulePaths.some(path => !isAbsolute(path)))
|
|
656
|
+
throw new Error('pythonModulePaths must contain absolute dependency directory paths');
|
|
657
|
+
if (!this.pythonPath && !platform)
|
|
571
658
|
throw new UnsupportedRuntimePlatformError(`${process.platform}/${process.arch}`);
|
|
572
659
|
if (!manifest.wheel || !manifest.requirements)
|
|
573
660
|
throw new Error('The package is missing Core or dependency checksums');
|
|
@@ -584,30 +671,73 @@ export class ManagedRuntime {
|
|
|
584
671
|
const unlock = await this.lock(signal);
|
|
585
672
|
let python;
|
|
586
673
|
try {
|
|
587
|
-
const generation = createHash('sha256').update(JSON.stringify(manifest))
|
|
674
|
+
const generation = createHash('sha256').update(JSON.stringify(manifest))
|
|
675
|
+
.update(this.pythonPath ? JSON.stringify({ pythonPath: this.pythonPath, pythonModulePaths: this.pythonModulePaths }) : '').digest('hex').slice(0, 24);
|
|
588
676
|
const destination = join(this.root, 'environments', generation);
|
|
589
|
-
|
|
590
|
-
if (
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
await rm(destination, { recursive: true, force: true });
|
|
677
|
+
let coreRoot;
|
|
678
|
+
if (this.pythonPath) {
|
|
679
|
+
python = this.pythonPath;
|
|
680
|
+
coreRoot = join(destination, 'core');
|
|
681
|
+
const complete = await readFile(join(destination, '.complete'), 'utf8').catch(() => '') === generation;
|
|
595
682
|
try {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
683
|
+
if (!complete) {
|
|
684
|
+
await rm(destination, { recursive: true, force: true });
|
|
685
|
+
await mkdir(coreRoot, { recursive: true });
|
|
686
|
+
// Core is a pure-Python wheel. Install only its verified contents in
|
|
687
|
+
// our directory: no pip/uv, dependency resolution, or author-site writes.
|
|
688
|
+
for (const [name, bytes] of Object.entries(unzipSync(await readFile(wheel)))) {
|
|
689
|
+
signal.throwIfAborted();
|
|
690
|
+
const target = resolve(coreRoot, name);
|
|
691
|
+
if (isAbsolute(name) || !target.startsWith(coreRoot + sep))
|
|
692
|
+
throw new Error('Invalid Core wheel path');
|
|
693
|
+
if (name.endsWith('/'))
|
|
694
|
+
await mkdir(target, { recursive: true });
|
|
695
|
+
else {
|
|
696
|
+
await mkdir(dirname(target), { recursive: true });
|
|
697
|
+
await writeFile(target, bytes);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
this.update('installing', 'Checking the supplied Python and its dependencies; no runtime downloads');
|
|
702
|
+
await this.command(python, ['-I', '-B', '-X', 'utf8', '-c',
|
|
703
|
+
'import json, sys; assert sys.version_info >= (3, 10), "Python >=3.10 is required"; sys.path[:0] = json.loads(sys.argv[1]); from tools.studio_http import health_payload; from tools.studio import create_server; h = health_payload(); assert h["core_version"] == sys.argv[2] and h["contract_version"] == int(sys.argv[3]), "Core version or API mismatch"',
|
|
704
|
+
JSON.stringify([coreRoot, ...this.pythonModulePaths]), manifest.core_version, String(manifest.contract_version)], AbortSignal.any([signal, AbortSignal.timeout(120_000)]));
|
|
705
|
+
if (!complete)
|
|
706
|
+
await writeFile(join(destination, '.complete'), generation);
|
|
602
707
|
}
|
|
603
708
|
catch (error) {
|
|
709
|
+
if (!complete)
|
|
710
|
+
await rm(destination, { recursive: true, force: true });
|
|
711
|
+
signal.throwIfAborted();
|
|
712
|
+
throw new Error(`Supplied Python is not ready: ${sanitizeDiagnostic(error instanceof Error ? error.message : String(error))}. Prepare its dependencies first; see docs/INSTALL.md#bring-your-own-python. No uv or Python download was attempted.`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
if (!platform)
|
|
717
|
+
throw new UnsupportedRuntimePlatformError(`${process.platform}/${process.arch}`);
|
|
718
|
+
python = join(destination, process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python');
|
|
719
|
+
if (await readFile(join(destination, '.complete'), 'utf8').catch(() => '') !== generation) {
|
|
720
|
+
const uv = await this.extract(platform.uv, signal);
|
|
721
|
+
const basePython = await this.extract(platform.python, signal);
|
|
722
|
+
this.update('installing', 'Installing isolated dependencies; first use may take several minutes');
|
|
604
723
|
await rm(destination, { recursive: true, force: true });
|
|
605
|
-
|
|
724
|
+
try {
|
|
725
|
+
await this.command(uv, ['venv', '--python', basePython, destination], signal);
|
|
726
|
+
await this.command(uv, ['pip', 'sync', '--python', python, '--require-hashes', '--only-binary', ':all:',
|
|
727
|
+
...(manifest.dependency_wheels?.length ? ['--find-links', join(this.artifacts, 'wheels')] : []), requirements], signal);
|
|
728
|
+
await this.command(uv, ['pip', 'install', '--python', python, '--no-deps', wheel], signal);
|
|
729
|
+
await this.command(python, ['-I', '-c', 'from tools.studio_http import health_payload; from tools.studio import create_server; assert health_payload()["contract_version"] == 1'], signal);
|
|
730
|
+
await writeFile(join(destination, '.complete'), generation);
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
await rm(destination, { recursive: true, force: true });
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
606
736
|
}
|
|
607
737
|
}
|
|
608
738
|
signal.throwIfAborted();
|
|
609
739
|
this.update(this.recovered ? 'recovering' : 'starting', this.recovered ? 'Recovering the writing environment' : 'Starting the writing environment');
|
|
610
|
-
const connection = await this.start(python, manifest, signal);
|
|
740
|
+
const connection = await this.start(python, manifest, signal, coreRoot);
|
|
611
741
|
const pending = join(this.root, 'active.' + randomUUID() + '.json');
|
|
612
742
|
try {
|
|
613
743
|
await writeFile(pending, JSON.stringify({ generation, coreVersion: manifest.core_version }));
|
|
@@ -628,16 +758,19 @@ export class ManagedRuntime {
|
|
|
628
758
|
await unlock();
|
|
629
759
|
}
|
|
630
760
|
}
|
|
631
|
-
async start(python, manifest, signal) {
|
|
761
|
+
async start(python, manifest, signal, coreRoot) {
|
|
632
762
|
signal.throwIfAborted();
|
|
633
763
|
const token = randomBytes(32).toString('hex');
|
|
634
764
|
const generation = ++this.generation;
|
|
635
765
|
// Windows defaults redirected stdout to its legacy locale encoding. Core
|
|
636
766
|
// initialization logs and manuscript paths are Unicode; -I ignores Python
|
|
637
767
|
// environment options, so set UTF-8 explicitly on the interpreter as well.
|
|
638
|
-
const
|
|
768
|
+
const launch = coreRoot
|
|
769
|
+
? ['-c', 'import json, runpy, sys; sys.path[:0] = json.loads(sys.argv[1]); sys.argv = sys.argv[2:]; runpy.run_module(sys.argv[0], run_name="__main__", alter_sys=True)', JSON.stringify([coreRoot, ...this.pythonModulePaths]), 'tools.managed_runtime']
|
|
770
|
+
: ['-m', 'tools.managed_runtime'];
|
|
771
|
+
const child = this.spawnFn(python, ['-I', ...(coreRoot ? ['-B'] : []), '-X', 'utf8', '-u', ...launch], {
|
|
639
772
|
cwd: this.root, detached: process.platform !== 'win32', windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
640
|
-
env: buildChildEnv(process.env, { PYTHONNOUSERSITE: '1', PYTHONUTF8: '1' }),
|
|
773
|
+
env: buildChildEnv(process.env, { ...this.pythonLibraryEnv(), PYTHONNOUSERSITE: '1', PYTHONUTF8: '1' }),
|
|
641
774
|
});
|
|
642
775
|
this.child = child;
|
|
643
776
|
// Mirror stderr to state/logs/backend.log (last 64 KiB): a stuck backend
|
|
@@ -680,7 +813,16 @@ export class ManagedRuntime {
|
|
|
680
813
|
signal.addEventListener('abort', abort, { once: true });
|
|
681
814
|
let port;
|
|
682
815
|
try {
|
|
683
|
-
|
|
816
|
+
const handshake = { token, state_dir: join(this.root, 'state') };
|
|
817
|
+
// The relay address is ephemeral, so it is resolved per start and
|
|
818
|
+
// never cached: a stale URL would point Core at a dead port.
|
|
819
|
+
const hostLlm = await this.resolveHostLlm();
|
|
820
|
+
if (hostLlm !== undefined)
|
|
821
|
+
handshake['host_llm'] = hostLlm;
|
|
822
|
+
const hostWeb = await this.resolveHostWeb();
|
|
823
|
+
if (hostWeb !== undefined)
|
|
824
|
+
handshake['host_web'] = hostWeb;
|
|
825
|
+
child.stdin.write(JSON.stringify(handshake) + '\n');
|
|
684
826
|
port = await ready;
|
|
685
827
|
}
|
|
686
828
|
finally {
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
+
// Only unconditionally read-only tools belong here. An action-dispatching tool
|
|
2
|
+
// must NOT be listed: `reads` short-circuits before `readActions`, so a merged
|
|
3
|
+
// family placed here has all of its mutating actions (delete, configure,
|
|
4
|
+
// save_routes) classified as reads and run under a read-only sandbox or plan
|
|
5
|
+
// mode. `novel_model_profiles` / `novel_embedding_profiles` were listed here by
|
|
6
|
+
// mistake after the 95→56 merge; their read-only actions are in `readActions`.
|
|
1
7
|
const reads = new Set([
|
|
2
|
-
'
|
|
3
|
-
'novel_context_preview', 'novel_outline_read', '
|
|
4
|
-
'novel_chapter_work', '
|
|
5
|
-
'
|
|
6
|
-
'
|
|
7
|
-
'novel_research_status', 'novel_research_report', 'novel_model_profiles',
|
|
8
|
-
'novel_manuscript_acceptance', 'novel_export_preflight',
|
|
9
|
-
'novel_manuscript_metrics', 'novel_outline_audit',
|
|
8
|
+
'novel_status', 'novel_review_framework', 'novel_trace_list',
|
|
9
|
+
'novel_context_preview', 'novel_outline_read', 'novel_reading_packet',
|
|
10
|
+
'novel_chapter_work', 'novel_chapter_scenes', 'novel_search', 'novel_continuity', 'novel_diagnostics',
|
|
11
|
+
'novel_research_status', 'novel_research_report',
|
|
12
|
+
'novel_manuscript_acceptance', 'novel_manuscript_metrics', 'novel_outline_audit',
|
|
10
13
|
]);
|
|
11
14
|
const generates = new Set([
|
|
12
|
-
'novel_write_chapter', 'novel_review_chapter', 'novel_multi_write',
|
|
13
|
-
'
|
|
14
|
-
'novel_revision_create_from_review', 'novel_revision_regenerate', 'novel_settle_backfill',
|
|
15
|
-
'novel_model_test', 'novel_model_embedding_test',
|
|
15
|
+
'novel_write_chapter', 'novel_review_chapter', 'novel_multi_write',
|
|
16
|
+
'novel_revision_regenerate', 'novel_settle_backfill',
|
|
16
17
|
]);
|
|
17
18
|
const readActions = {
|
|
18
19
|
novel_writing_assets: ['get'],
|
|
@@ -25,6 +26,28 @@ const readActions = {
|
|
|
25
26
|
novel_reference_library_action: ['list', 'status', 'profile'],
|
|
26
27
|
novel_runtime_skill_action: ['list', 'diagnose', 'resolve'],
|
|
27
28
|
novel_rule_action: ['status'],
|
|
29
|
+
// Merged tools: the read-only actions of each family.
|
|
30
|
+
novel_task: ['get', 'list'],
|
|
31
|
+
novel_revision: ['list', 'get'],
|
|
32
|
+
novel_assets: ['list', 'read'],
|
|
33
|
+
novel_assets_package: ['preview'],
|
|
34
|
+
novel_doc: ['read'],
|
|
35
|
+
novel_scene: ['structure'],
|
|
36
|
+
novel_scene_migration: ['preview'],
|
|
37
|
+
novel_reading_order: ['get'],
|
|
38
|
+
novel_export: ['preflight'],
|
|
39
|
+
// `download` is NOT here: it writes a local file (mkdir + writeFile under
|
|
40
|
+
// outputDir), so classifying it as a read would let a read-only sandbox or plan
|
|
41
|
+
// mode write bytes to disk — the same class of mistake as the assets-package
|
|
42
|
+
// export. Its archive_op read variants are covered by the write classification
|
|
43
|
+
// being fail-closed rather than permissive.
|
|
44
|
+
novel_model_profiles: ['list', 'test'],
|
|
45
|
+
novel_embedding_profiles: ['list', 'test'],
|
|
46
|
+
};
|
|
47
|
+
/** Merged tools whose creating/generating actions replace the old per-tool names. */
|
|
48
|
+
const generateActions = {
|
|
49
|
+
novel_task: ['create', 'retry', 'confirm'],
|
|
50
|
+
novel_revision: ['create'],
|
|
28
51
|
};
|
|
29
52
|
/** Unknown and preview operations fail closed: previews can persist confirmation records. */
|
|
30
53
|
export function novelEffect(name, args) {
|
|
@@ -37,6 +60,8 @@ export function novelEffect(name, args) {
|
|
|
37
60
|
return 'read';
|
|
38
61
|
if (name === 'novel_model_benchmark' && values.action === 'run')
|
|
39
62
|
return 'generate';
|
|
63
|
+
if (generateActions[name]?.includes(String(values.action)))
|
|
64
|
+
return 'generate';
|
|
40
65
|
return generates.has(name) ? 'generate' : 'write';
|
|
41
66
|
}
|
|
42
67
|
export function novelPolicyDenial(ctx, execution) {
|