dsh-openwrite 0.2.27 → 0.2.33
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 +55 -6
- package/THIRD_PARTY_NOTICES.md +4 -3
- package/companions.mjs +508 -0
- package/cordis.patch.yml +0 -2
- package/docs/CLOSED_ISSUE_FOLLOWUPS.md +5 -3
- package/docs/COMPANION_PLUGINS.md +142 -0
- package/docs/CONTEXT_SLOTS.md +60 -0
- 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 +17 -0
- package/docs/INSTALL.md +112 -4
- package/docs/ISSUES_121_123.md +97 -0
- package/docs/ISSUES_140_142.md +58 -0
- package/docs/ISSUES_149_150_151.md +77 -0
- package/docs/ISSUES_98_100.md +2 -0
- package/docs/ISSUE_123_FOLLOWUP.md +55 -0
- 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 +90 -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 +16 -0
- package/docs/TOOLS.md +933 -0
- package/docs/WORKFLOWS.md +5 -5
- package/package.json +18 -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 +10 -4
- 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 +179 -35
- package/packages/openwrite-bridge/lib/tool-policy.js +39 -12
- package/packages/openwrite-bridge/lib/tools.js +1238 -1219
- package/packages/openwrite-bridge/package.json +7 -5
- package/packages/studio-panel/lib/client.js +116667 -114773
- 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 +8 -2
- 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/manuscript-annotations.d.ts +2 -1
- package/packages/studio-panel/lib/types/client/manuscript-overlay.d.ts +2 -1
- package/packages/studio-panel/lib/types/client/manuscript-selection.d.ts +18 -5
- package/packages/studio-panel/lib/types/client/selection-coordinates.d.ts +15 -0
- 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.16-py3-none-any.whl → openwrite-5.8.30-py3-none-any.whl} +0 -0
- package/release/runtime-manifest.json +4 -12
- 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 +11 -2
- package/scripts/safe-upgrade.mjs +141 -13
- package/dog-tools.mjs +0 -11
- 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-BAlgZpLq.js +0 -2074
- 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 -1279
- 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;
|
|
@@ -191,7 +201,9 @@ export class ManagedRuntime {
|
|
|
191
201
|
}
|
|
192
202
|
status() { return { ...this.state }; }
|
|
193
203
|
update(phase, message, extra = {}) {
|
|
194
|
-
this.
|
|
204
|
+
const recoveryError = this.recovered && ['recovering', 'waiting', 'downloading', 'installing'].includes(phase)
|
|
205
|
+
? this.state.error : undefined;
|
|
206
|
+
this.state = { phase, message, ...(recoveryError ? { error: recoveryError } : {}), ...extra };
|
|
195
207
|
}
|
|
196
208
|
ensure() {
|
|
197
209
|
if (this.closed)
|
|
@@ -239,7 +251,12 @@ export class ManagedRuntime {
|
|
|
239
251
|
* forcefully and pushed through the normal recovery path.
|
|
240
252
|
*/
|
|
241
253
|
async verifyConnection(connection) {
|
|
242
|
-
|
|
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') {
|
|
243
260
|
if (this.closed)
|
|
244
261
|
throw new Error('OpenWrite was removed');
|
|
245
262
|
const current = this.connection;
|
|
@@ -247,6 +264,14 @@ export class ManagedRuntime {
|
|
|
247
264
|
return current;
|
|
248
265
|
return this.queueRestart();
|
|
249
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
|
+
}
|
|
250
275
|
if (this.connection !== connection)
|
|
251
276
|
return this.queueRestart();
|
|
252
277
|
this.connection = undefined;
|
|
@@ -261,22 +286,76 @@ export class ManagedRuntime {
|
|
|
261
286
|
this.update('recovering', 'The writing backend is unresponsive; recovering');
|
|
262
287
|
return this.queueRestart();
|
|
263
288
|
}
|
|
264
|
-
/**
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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;
|
|
269
299
|
try {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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 };
|
|
275
308
|
}
|
|
276
309
|
catch {
|
|
277
|
-
return
|
|
310
|
+
return undefined;
|
|
278
311
|
}
|
|
279
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
|
+
}
|
|
280
359
|
async cancel() {
|
|
281
360
|
this.cancelEpoch += 1;
|
|
282
361
|
this.allowRevive = false;
|
|
@@ -528,7 +607,7 @@ export class ManagedRuntime {
|
|
|
528
607
|
async command(executable, args, signal) {
|
|
529
608
|
signal.throwIfAborted();
|
|
530
609
|
const child = this.spawnFn(executable, args, { cwd: this.root, detached: process.platform !== 'win32', windowsHide: true,
|
|
531
|
-
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' }),
|
|
532
611
|
stdio: ['ignore', 'ignore', 'pipe'] });
|
|
533
612
|
let failure = '';
|
|
534
613
|
child.stderr?.on('data', bytes => { failure = (failure + String(bytes)).slice(-2000); });
|
|
@@ -545,6 +624,10 @@ export class ManagedRuntime {
|
|
|
545
624
|
signal.removeEventListener('abort', abort);
|
|
546
625
|
}
|
|
547
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
|
+
}
|
|
548
631
|
async lock(signal) {
|
|
549
632
|
for (;;) {
|
|
550
633
|
signal.throwIfAborted();
|
|
@@ -565,7 +648,13 @@ export class ManagedRuntime {
|
|
|
565
648
|
async prepare(signal) {
|
|
566
649
|
const manifest = JSON.parse(await readFile(join(this.artifacts, 'runtime-manifest.json'), 'utf8'));
|
|
567
650
|
const platform = manifest.platforms[`${process.platform}-${process.arch}`];
|
|
568
|
-
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)
|
|
569
658
|
throw new UnsupportedRuntimePlatformError(`${process.platform}/${process.arch}`);
|
|
570
659
|
if (!manifest.wheel || !manifest.requirements)
|
|
571
660
|
throw new Error('The package is missing Core or dependency checksums');
|
|
@@ -582,30 +671,73 @@ export class ManagedRuntime {
|
|
|
582
671
|
const unlock = await this.lock(signal);
|
|
583
672
|
let python;
|
|
584
673
|
try {
|
|
585
|
-
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);
|
|
586
676
|
const destination = join(this.root, 'environments', generation);
|
|
587
|
-
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
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;
|
|
593
682
|
try {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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);
|
|
600
707
|
}
|
|
601
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');
|
|
602
723
|
await rm(destination, { recursive: true, force: true });
|
|
603
|
-
|
|
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
|
+
}
|
|
604
736
|
}
|
|
605
737
|
}
|
|
606
738
|
signal.throwIfAborted();
|
|
607
739
|
this.update(this.recovered ? 'recovering' : 'starting', this.recovered ? 'Recovering the writing environment' : 'Starting the writing environment');
|
|
608
|
-
const connection = await this.start(python, manifest, signal);
|
|
740
|
+
const connection = await this.start(python, manifest, signal, coreRoot);
|
|
609
741
|
const pending = join(this.root, 'active.' + randomUUID() + '.json');
|
|
610
742
|
try {
|
|
611
743
|
await writeFile(pending, JSON.stringify({ generation, coreVersion: manifest.core_version }));
|
|
@@ -626,16 +758,19 @@ export class ManagedRuntime {
|
|
|
626
758
|
await unlock();
|
|
627
759
|
}
|
|
628
760
|
}
|
|
629
|
-
async start(python, manifest, signal) {
|
|
761
|
+
async start(python, manifest, signal, coreRoot) {
|
|
630
762
|
signal.throwIfAborted();
|
|
631
763
|
const token = randomBytes(32).toString('hex');
|
|
632
764
|
const generation = ++this.generation;
|
|
633
765
|
// Windows defaults redirected stdout to its legacy locale encoding. Core
|
|
634
766
|
// initialization logs and manuscript paths are Unicode; -I ignores Python
|
|
635
767
|
// environment options, so set UTF-8 explicitly on the interpreter as well.
|
|
636
|
-
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], {
|
|
637
772
|
cwd: this.root, detached: process.platform !== 'win32', windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
638
|
-
env: buildChildEnv(process.env, { PYTHONNOUSERSITE: '1', PYTHONUTF8: '1' }),
|
|
773
|
+
env: buildChildEnv(process.env, { ...this.pythonLibraryEnv(), PYTHONNOUSERSITE: '1', PYTHONUTF8: '1' }),
|
|
639
774
|
});
|
|
640
775
|
this.child = child;
|
|
641
776
|
// Mirror stderr to state/logs/backend.log (last 64 KiB): a stuck backend
|
|
@@ -678,7 +813,16 @@ export class ManagedRuntime {
|
|
|
678
813
|
signal.addEventListener('abort', abort, { once: true });
|
|
679
814
|
let port;
|
|
680
815
|
try {
|
|
681
|
-
|
|
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');
|
|
682
826
|
port = await ready;
|
|
683
827
|
}
|
|
684
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,16 +26,42 @@ 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) {
|
|
31
54
|
if (reads.has(name))
|
|
32
55
|
return 'read';
|
|
33
56
|
const values = args && typeof args === 'object' ? args : {};
|
|
57
|
+
if (name === 'novel_write_chapter' && values.dry_run === true)
|
|
58
|
+
return 'read';
|
|
34
59
|
if (readActions[name]?.includes(String(values.action)))
|
|
35
60
|
return 'read';
|
|
36
61
|
if (name === 'novel_model_benchmark' && values.action === 'run')
|
|
37
62
|
return 'generate';
|
|
63
|
+
if (generateActions[name]?.includes(String(values.action)))
|
|
64
|
+
return 'generate';
|
|
38
65
|
return generates.has(name) ? 'generate' : 'write';
|
|
39
66
|
}
|
|
40
67
|
export function novelPolicyDenial(ctx, execution) {
|