@ran-sh/dsh-crew 2.2.0 → 2.2.2
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/.claude-plugin/plugin.json +1 -1
- package/official-web-bridge/lib/client.js +1 -1
- package/official-web-bridge/overlay-entry.mjs +5 -1
- package/official-web-bridge/package.json +1 -1
- package/package.json +1 -1
- package/scripts/setup.mjs +39 -33
- package/scripts/verify-npm-install.mjs +2 -2
- package/src/adaptive-routing.mjs +2 -2
- package/src/ci-evidence.mjs +1 -1
- package/src/client/quick-panel.tsx +6 -2
- package/src/credential-lifecycle.mjs +0 -3
- package/src/credential-purge-state.mjs +1 -5
- package/src/credential-reference.mjs +0 -8
- package/src/crew-workspaces.mjs +1 -1
- package/src/delivery.mjs +2 -2
- package/src/dsh-cli-runtime.mjs +227 -30
- package/src/extension-contract.mjs +1 -1
- package/src/failure-classification.mjs +0 -10
- package/src/hub/entry.mjs +1 -1
- package/src/hub/index.mjs +57 -7
- package/src/i18n.mjs +0 -2
- package/src/install/crew-paths.mjs +16 -11
- package/src/install/crew-skill.mjs +1 -1
- package/src/install/desktop-profile.mjs +168 -0
- package/src/install/harness-config-import.mjs +204 -0
- package/src/install/install-legacy.mjs +3 -3
- package/src/install/npx-lifecycle.mjs +210 -125
- package/src/install/official-web.mjs +1 -1
- package/src/install/windows-startup.mjs +5 -5
- package/src/install/windows-supervisor-adapter.mjs +3 -2
- package/src/install/windows-supervisor-lifecycle.mjs +2 -2
- package/src/install/zcode.mjs +0 -2
- package/src/job-contracts.mjs +1 -1
- package/src/job-identity.mjs +2 -2
- package/src/jobs.mjs +3 -4
- package/src/model-routing.mjs +3 -4
- package/src/model-schedule.mjs +0 -1
- package/src/official-web-bridge.mjs +3 -1
- package/src/policy-legacy.mjs +10 -12
- package/src/provider-inventory.mjs +1 -1
- package/src/provider-layer-migration.mjs +0 -2
- package/src/provider-settings-store.mjs +19 -1
- package/src/readiness-matrix.mjs +1 -1
- package/src/release-in-use.mjs +1 -1
- package/src/role-profiles.mjs +2 -2
- package/src/runtime-controls.mjs +1 -1
- package/src/runtime-identity.mjs +1 -1
- package/src/status-shard.mjs +1 -1
- package/src/supervisor/restart-request.mjs +4 -4
- package/src/workflow-runtime.mjs +1 -2
- package/src/workspace-context.mjs +2 -2
- package/src/workspace-isolation.mjs +4 -4
- package/src/workspace-lock.mjs +2 -3
- package/scripts/policy-probe.mjs +0 -101
- package/scripts/verify-crew-ui-polish.mjs +0 -145
- package/scripts/verify-history-ui.mjs +0 -98
- package/scripts/verify-installer-fix.mjs +0 -26
package/src/dsh-cli-runtime.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
// Re-exported so existing importers keep working while the cohort value now
|
|
36
36
|
// lives in exactly one place (src/dsh-cohort.mjs).
|
|
37
37
|
export { DSH_CLI_PACKAGE, TARGET_DSH_VERSION, TARGET_DSH_SPEC };
|
|
38
|
-
|
|
38
|
+
const CREW_DSH_RUNTIME_DIRNAME = 'runtime';
|
|
39
39
|
const CREW_PROFILE_DEFAULT_BUNDLES = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'];
|
|
40
40
|
const PROFILE_PATCH_TEMPLATE = '[]\n';
|
|
41
41
|
const PROFILE_PNPM_WORKSPACE = 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n';
|
|
@@ -73,11 +73,6 @@ export function crewDshRuntimeVersionDir({ home = homedir(), version = TARGET_DS
|
|
|
73
73
|
return join(crewDshHome({ home }), `runtime-${version}`);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export function crewDshRuntimeEntry({ home = homedir(), platform = process.platform } = {}) {
|
|
77
|
-
const suffix = platform === 'win32' ? '.cmd' : '';
|
|
78
|
-
return join(crewDshRuntimeRoot({ home }), 'node_modules', '.bin', `dsh${suffix}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
76
|
export function crewDshRuntimeModule({ home = homedir() } = {}) {
|
|
82
77
|
return join(crewDshRuntimeRoot({ home }), 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js');
|
|
83
78
|
}
|
|
@@ -225,14 +220,29 @@ export function ensureCrewDshRuntime({
|
|
|
225
220
|
platform = process.platform,
|
|
226
221
|
comspec = process.env.ComSpec ?? 'cmd.exe',
|
|
227
222
|
env = process.env,
|
|
223
|
+
read = readFileSync,
|
|
224
|
+
write = writeFileSync,
|
|
225
|
+
log = () => {},
|
|
228
226
|
} = {}) {
|
|
229
|
-
const existing = resolveDshCli({ home, env, platform, exists, findCommand, includeCompatibility: false });
|
|
227
|
+
const existing = resolveDshCli({ home, env, platform, exists, findCommand, includeCompatibility: false, read });
|
|
230
228
|
// Reuse only when the installed runtime already matches the requested
|
|
231
229
|
// cohort. A stale cohort (e.g. an online 0.1.1-rc.2 runtime) must never be
|
|
232
230
|
// mistaken for the target, and it must never be upgraded in place under a
|
|
233
231
|
// live hub.
|
|
234
232
|
if (existing?.kind === 'crew-runtime' && existing?.version === version) {
|
|
235
|
-
|
|
233
|
+
// The core matching is not enough: a tree whose family drifted boots that
|
|
234
|
+
// core against siblings from another cohort and dies on the first import
|
|
235
|
+
// the newer ones dropped. Report it as the cohort mismatch it is so the
|
|
236
|
+
// caller runs the staged (re)install instead of handing it to the launcher.
|
|
237
|
+
const cohort = verifyCrewRuntimeCohort({ root: crewDshRuntimeRoot({ home }), version, read });
|
|
238
|
+
if (cohort.ok) return { ok: true, cli: existing, reused: true };
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
code: 'DSH_RUNTIME_COHORT_MISMATCH',
|
|
242
|
+
error: `Crew runtime family drifted from ${version}: ${describeCohortDrift(cohort.drift)}`,
|
|
243
|
+
installed: existing.version ?? null,
|
|
244
|
+
target: version,
|
|
245
|
+
};
|
|
236
246
|
}
|
|
237
247
|
if (existing?.kind === 'crew-runtime') {
|
|
238
248
|
return {
|
|
@@ -256,13 +266,14 @@ export function ensureCrewDshRuntime({
|
|
|
256
266
|
? ['add', '--dir', runtimeRoot, '--ignore-scripts', packageSpec]
|
|
257
267
|
: ['install', '--prefix', runtimeRoot, '--no-package-lock', '--ignore-scripts', '--omit=dev', packageSpec];
|
|
258
268
|
const invocation = buildDshInvocation(packageManager, packageArgs, { platform, comspec });
|
|
259
|
-
const
|
|
269
|
+
const runInstall = () => runner(invocation.command, invocation.args, {
|
|
260
270
|
encoding: 'utf8',
|
|
261
271
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
262
272
|
shell: invocation.shell,
|
|
263
273
|
env: { ...env },
|
|
264
274
|
});
|
|
265
|
-
const
|
|
275
|
+
const result = runInstall();
|
|
276
|
+
const cli = resolveDshCli({ home, env, platform, exists, findCommand, includeCompatibility: false, read });
|
|
266
277
|
if (result.status !== 0 || !cli || cli.kind !== 'crew-runtime') {
|
|
267
278
|
return {
|
|
268
279
|
ok: false,
|
|
@@ -283,7 +294,30 @@ export function ensureCrewDshRuntime({
|
|
|
283
294
|
target: version,
|
|
284
295
|
};
|
|
285
296
|
}
|
|
286
|
-
|
|
297
|
+
// The core is pinned by the install spec, but npm resolves its siblings from
|
|
298
|
+
// the core's caret ranges: a registry that published a newer prerelease of the
|
|
299
|
+
// family produces a mixed tree here, so lock and re-verify the family before
|
|
300
|
+
// this runtime can be reported reusable.
|
|
301
|
+
const locked = enforceCohortLock({ root: runtimeRoot, version, runInstall, read, write, log });
|
|
302
|
+
if (locked.failed) {
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
code: 'DSH_RUNTIME_INSTALL_FAILED',
|
|
306
|
+
error: 'Crew-owned DSH runtime install failed',
|
|
307
|
+
status: locked.failed.status ?? -1,
|
|
308
|
+
stderrTail: String(locked.failed.stderr || locked.failed.stdout || '').trim().split(/\r?\n/).slice(-3).join(' | ').slice(0, 300),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (!locked.ok) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
code: 'DSH_RUNTIME_COHORT_INCONSISTENT',
|
|
315
|
+
error: `installed Crew runtime family does not match ${version}: ${describeCohortDrift(locked.drift)}`,
|
|
316
|
+
cohort: { drift: locked.drift },
|
|
317
|
+
target: version,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return { ok: true, cli, reused: false, version: cli.version, runtimeRoot, ...(locked.repairs ? { cohortRepaired: true } : {}) };
|
|
287
321
|
}
|
|
288
322
|
|
|
289
323
|
export function describeDshCli(cli) {
|
|
@@ -292,6 +326,104 @@ export function describeDshCli(cli) {
|
|
|
292
326
|
return `${cli.kind}${version}`;
|
|
293
327
|
}
|
|
294
328
|
|
|
329
|
+
// ---- installed cohort identity ---------------------------------------------
|
|
330
|
+
// The runtime tree is a COHORT, not a single package: @deepseek-ai/dsh plus the
|
|
331
|
+
// @deepseek-ai/dsh-* family it loads. The install pins the core exactly, but npm
|
|
332
|
+
// still resolves the family from the core's own caret ranges, so a registry that
|
|
333
|
+
// has published a newer prerelease of the family yields a MIXED tree — core
|
|
334
|
+
// alpha.1 with alpha.2 siblings — that installs cleanly and then dies at boot:
|
|
335
|
+
// the alpha.1 core imports exports the alpha.2 siblings no longer provide. Both
|
|
336
|
+
// the reuse gate and the install therefore judge every family member, and a tree
|
|
337
|
+
// that drifted is locked with overrides and reinstalled once before anything is
|
|
338
|
+
// reported reusable.
|
|
339
|
+
|
|
340
|
+
// A family package may carry its own nested copy of a sibling, and node
|
|
341
|
+
// resolves that copy FIRST for its parent — so a nested copy at the wrong
|
|
342
|
+
// version breaks the parent exactly like a top-level one. npm does produce
|
|
343
|
+
// these (a reconcile against a mixed tree nested whole subtrees), so the walk
|
|
344
|
+
// follows family packages down through their own node_modules and stops before
|
|
345
|
+
// it can descend into unrelated trees.
|
|
346
|
+
const COHORT_NESTED_DEPTH = 3;
|
|
347
|
+
|
|
348
|
+
function installedCohortEntries(root, read = readFileSync) {
|
|
349
|
+
const entries = [];
|
|
350
|
+
const visit = (scope, prefix, depth) => {
|
|
351
|
+
let names;
|
|
352
|
+
try { names = readdirSync(scope); } catch { return; }
|
|
353
|
+
for (const name of names) {
|
|
354
|
+
if (name !== 'dsh' && !name.startsWith('dsh-')) continue;
|
|
355
|
+
const dir = join(scope, name);
|
|
356
|
+
entries.push({ name: `@deepseek-ai/${name}`, version: readPackageVersionAt(dir, read), scope: prefix || 'node_modules' });
|
|
357
|
+
if (depth >= COHORT_NESTED_DEPTH) continue;
|
|
358
|
+
const nested = join(dir, 'node_modules', '@deepseek-ai');
|
|
359
|
+
if (existsSync(nested)) visit(nested, `${prefix ? `${prefix}/` : ''}${name}/node_modules`, depth + 1);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
visit(join(root, 'node_modules', '@deepseek-ai'), '', 0);
|
|
363
|
+
return entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Judge every installed copy of the family against the pinned cohort version.
|
|
368
|
+
* `drift` names each copy whose on-disk version differs (an unreadable manifest
|
|
369
|
+
* reads as null), so callers can report the exact packages instead of a bare
|
|
370
|
+
* mismatch.
|
|
371
|
+
*/
|
|
372
|
+
export function verifyCrewRuntimeCohort({ root, version, read = readFileSync } = {}) {
|
|
373
|
+
const entries = installedCohortEntries(root, read);
|
|
374
|
+
const drift = entries.filter((entry) => entry.version !== version);
|
|
375
|
+
return { ok: entries.length > 0 && drift.length === 0, entries, drift };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function describeCohortDrift(drift) {
|
|
379
|
+
return drift
|
|
380
|
+
.map((entry) => (entry.scope && entry.scope !== 'node_modules'
|
|
381
|
+
? `${entry.name}@${entry.version ?? 'missing'} (nested in ${entry.scope})`
|
|
382
|
+
: `${entry.name}@${entry.version ?? 'missing'}`))
|
|
383
|
+
.join(', ');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Lock the whole family to the cohort version. npm v11 rejects an override that
|
|
387
|
+
// shares a name with a direct dependency unless the spec repeats it verbatim, so
|
|
388
|
+
// the core keeps its own exact dependency entry and is left out of `overrides`;
|
|
389
|
+
// pnpm reads overrides from its own field, so both are written.
|
|
390
|
+
function writeCohortOverrides({ root, version, names, read = readFileSync, write = writeFileSync }) {
|
|
391
|
+
const overrides = {};
|
|
392
|
+
for (const name of names) {
|
|
393
|
+
if (name === DSH_CLI_PACKAGE) continue;
|
|
394
|
+
overrides[name] = version;
|
|
395
|
+
}
|
|
396
|
+
if (Object.keys(overrides).length === 0) return false;
|
|
397
|
+
const manifestFile = join(root, 'package.json');
|
|
398
|
+
let manifest = null;
|
|
399
|
+
try { manifest = JSON.parse(read(manifestFile, 'utf8')); } catch { manifest = null; }
|
|
400
|
+
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) manifest = {};
|
|
401
|
+
const next = {
|
|
402
|
+
...manifest,
|
|
403
|
+
dependencies: { ...(manifest.dependencies ?? {}), [DSH_CLI_PACKAGE]: version },
|
|
404
|
+
overrides,
|
|
405
|
+
pnpm: { ...(manifest.pnpm ?? {}), overrides },
|
|
406
|
+
};
|
|
407
|
+
write(manifestFile, `${JSON.stringify(next, null, 2)}\n`);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Prove the freshly installed tree is a coherent cohort. A drifting family is
|
|
412
|
+
// locked with overrides and reinstalled exactly ONCE; a tree that still drifts is
|
|
413
|
+
// reported to the caller, never handed to the launcher as reusable.
|
|
414
|
+
function enforceCohortLock({ root, version, runInstall, read = readFileSync, write = writeFileSync, log = () => {} }) {
|
|
415
|
+
const observed = verifyCrewRuntimeCohort({ root, version, read });
|
|
416
|
+
if (observed.ok) return { ok: true, repairs: 0 };
|
|
417
|
+
if (!writeCohortOverrides({ root, version, names: observed.entries.map((entry) => entry.name), read, write })) {
|
|
418
|
+
return { ok: false, drift: observed.drift };
|
|
419
|
+
}
|
|
420
|
+
log(` DSH cohort drifted (${describeCohortDrift(observed.drift)}); locking the family with overrides`);
|
|
421
|
+
const result = runInstall();
|
|
422
|
+
if (result?.status !== 0) return { ok: false, failed: result };
|
|
423
|
+
const after = verifyCrewRuntimeCohort({ root, version, read });
|
|
424
|
+
return after.ok ? { ok: true, repairs: 1 } : { ok: false, drift: after.drift };
|
|
425
|
+
}
|
|
426
|
+
|
|
295
427
|
// Shared pnpm/npm install of the pinned DSH cohort into a specific root
|
|
296
428
|
// directory. pnpm materializes ABSOLUTE-path .cmd shims and junctions that
|
|
297
429
|
// point at the install root's own .pnpm store, so the tree must be installed
|
|
@@ -310,6 +442,9 @@ export function installDshInto({
|
|
|
310
442
|
platform = process.platform,
|
|
311
443
|
comspec = process.env.ComSpec ?? 'cmd.exe',
|
|
312
444
|
env = process.env,
|
|
445
|
+
read = readFileSync,
|
|
446
|
+
write = writeFileSync,
|
|
447
|
+
log = () => {},
|
|
313
448
|
}) {
|
|
314
449
|
const pnpm = pnpmCommand ?? findCommand('pnpm');
|
|
315
450
|
const npm = npmCommand ?? findCommand('npm');
|
|
@@ -322,12 +457,13 @@ export function installDshInto({
|
|
|
322
457
|
? ['add', '--dir', root, '--ignore-scripts', packageSpec]
|
|
323
458
|
: ['install', '--prefix', root, '--no-package-lock', '--ignore-scripts', '--omit=dev', packageSpec];
|
|
324
459
|
const invocation = buildDshInvocation(packageManager, packageArgs, { platform, comspec });
|
|
325
|
-
const
|
|
460
|
+
const runInstall = () => runner(invocation.command, invocation.args, {
|
|
326
461
|
encoding: 'utf8',
|
|
327
462
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
328
463
|
shell: invocation.shell,
|
|
329
464
|
env: { ...env },
|
|
330
465
|
});
|
|
466
|
+
const result = runInstall();
|
|
331
467
|
if (result.status !== 0) {
|
|
332
468
|
return {
|
|
333
469
|
ok: false,
|
|
@@ -341,7 +477,7 @@ export function installDshInto({
|
|
|
341
477
|
if (!exists(moduleEntry)) {
|
|
342
478
|
return { ok: false, code: 'DSH_RUNTIME_INSTALL_INCOMPLETE', error: 'runtime entry missing after install' };
|
|
343
479
|
}
|
|
344
|
-
const installedVersion = packageVersion(moduleEntry);
|
|
480
|
+
const installedVersion = packageVersion(moduleEntry, read);
|
|
345
481
|
if (installedVersion !== version) {
|
|
346
482
|
return {
|
|
347
483
|
ok: false,
|
|
@@ -351,7 +487,26 @@ export function installDshInto({
|
|
|
351
487
|
target: version,
|
|
352
488
|
};
|
|
353
489
|
}
|
|
354
|
-
|
|
490
|
+
const locked = enforceCohortLock({ root, version, runInstall, read, write, log });
|
|
491
|
+
if (locked.failed) {
|
|
492
|
+
return {
|
|
493
|
+
ok: false,
|
|
494
|
+
code: 'DSH_RUNTIME_INSTALL_FAILED',
|
|
495
|
+
error: 'Crew runtime install failed',
|
|
496
|
+
status: locked.failed.status ?? -1,
|
|
497
|
+
stderrTail: String(locked.failed.stderr || locked.failed.stdout || '').trim().split(/\r?\n/).slice(-3).join(' | ').slice(0, 300),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (!locked.ok) {
|
|
501
|
+
return {
|
|
502
|
+
ok: false,
|
|
503
|
+
code: 'DSH_RUNTIME_COHORT_INCONSISTENT',
|
|
504
|
+
error: `installed Crew runtime family does not match ${version}: ${describeCohortDrift(locked.drift)}`,
|
|
505
|
+
cohort: { drift: locked.drift },
|
|
506
|
+
target: version,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return { ok: true, root, version: installedVersion, ...(locked.repairs ? { cohortRepaired: true } : {}) };
|
|
355
510
|
}
|
|
356
511
|
|
|
357
512
|
// Staged cohort migration: install the target cohort into a versioned
|
|
@@ -736,35 +891,36 @@ export async function restoreRetainedRuntime({
|
|
|
736
891
|
}
|
|
737
892
|
|
|
738
893
|
// GC retained runtimes that no release pins. A retained cohort is needed only
|
|
739
|
-
// while some managed release
|
|
740
|
-
//
|
|
741
|
-
|
|
894
|
+
// while some managed release resolves to it, so the caller passes the resolved
|
|
895
|
+
// cohort versions (see resolveReleaseCohort: manifest pin, then the
|
|
896
|
+
// release-cohort.json sidecar). Best-effort; never throws — a root that is
|
|
897
|
+
// missing, unreadable or not a directory prunes nothing rather than failing
|
|
898
|
+
// the update that just committed.
|
|
899
|
+
export function gcRetainedRuntimes({ home = homedir(), cohorts = [], log = () => {} } = {}) {
|
|
742
900
|
const root = retainedRuntimesRoot({ home });
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const spec = payloadDshSpec(release);
|
|
747
|
-
if (spec && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(spec)) needed.add(spec);
|
|
748
|
-
}
|
|
901
|
+
const needed = new Set(cohorts.filter((spec) => typeof spec === 'string' && EXACT_COHORT_RE.test(spec)));
|
|
902
|
+
let names;
|
|
903
|
+
try { names = readdirSync(root); } catch { return []; }
|
|
749
904
|
const removed = [];
|
|
750
|
-
for (const name of
|
|
905
|
+
for (const name of names) {
|
|
906
|
+
if (needed.has(name)) continue;
|
|
751
907
|
const dir = join(root, name);
|
|
752
|
-
|
|
753
|
-
try { rmSync(dir, { recursive: true, force: true }); removed.push(name); } catch { /* best effort */ }
|
|
754
|
-
}
|
|
908
|
+
try { rmSync(dir, { recursive: true, force: true }); removed.push(name); } catch { /* best effort */ }
|
|
755
909
|
}
|
|
756
910
|
return removed;
|
|
757
911
|
}
|
|
758
912
|
|
|
913
|
+
const EXACT_COHORT_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
914
|
+
|
|
759
915
|
// Read the exact @deepseek-ai/dsh pin from a payload manifest (dependencies
|
|
760
916
|
// first, then peerDependencies). Exact pins only: a range or absence yields
|
|
761
917
|
// null so callers fail closed instead of guessing a cohort.
|
|
762
918
|
function payloadDshSpec(manifest) {
|
|
763
919
|
if (!manifest || typeof manifest !== 'object') return null;
|
|
764
920
|
const direct = manifest.dependencies?.['@deepseek-ai/dsh'];
|
|
765
|
-
if (typeof direct === 'string' &&
|
|
921
|
+
if (typeof direct === 'string' && EXACT_COHORT_RE.test(direct)) return direct;
|
|
766
922
|
const peer = manifest.peerDependencies?.['@deepseek-ai/dsh'];
|
|
767
|
-
if (typeof peer === 'string' &&
|
|
923
|
+
if (typeof peer === 'string' && EXACT_COHORT_RE.test(peer)) return peer;
|
|
768
924
|
return null;
|
|
769
925
|
}
|
|
770
926
|
|
|
@@ -964,7 +1120,7 @@ function ensureDirectoryLink(linkPath, targetRoot) {
|
|
|
964
1120
|
* command. The profile manifest and one loader-visible directory link are the
|
|
965
1121
|
* only state changed; no dependency resolution or policy bypass is attempted.
|
|
966
1122
|
*/
|
|
967
|
-
|
|
1123
|
+
function ensurePluginRegistration({
|
|
968
1124
|
profileRoot,
|
|
969
1125
|
root,
|
|
970
1126
|
name,
|
|
@@ -1088,3 +1244,44 @@ export function removeCrewPluginRegistration({ home = homedir(), name, profileRo
|
|
|
1088
1244
|
if (linkStat) unlinkSync(linkPath);
|
|
1089
1245
|
return { ok: true, removed: true };
|
|
1090
1246
|
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Idempotently materialize the Crew-owned Harness `web` profile scaffold.
|
|
1250
|
+
*
|
|
1251
|
+
* `ensureCrewDshRuntime` supplies the CLI but nothing else a managed 3210/3080
|
|
1252
|
+
* boot needs: the Windows launcher preflight and the supervisor both require
|
|
1253
|
+
* `<crew DSH_HOME>/profiles/web/package.json`, and a fresh install that never
|
|
1254
|
+
* created it deadlocked (supervisor heartbeat timeout -> pending handoff ->
|
|
1255
|
+
* every later update refused). The scaffold is the same shape the plugin
|
|
1256
|
+
* registration writes for the dsh-crew profile: an existing valid manifest is
|
|
1257
|
+
* left untouched, a missing one is created from CREW_PROFILE_DEFAULT_BUNDLES,
|
|
1258
|
+
* and a corrupt one fails closed instead of being overwritten.
|
|
1259
|
+
*/
|
|
1260
|
+
export function ensureCrewWebProfile({
|
|
1261
|
+
home = homedir(),
|
|
1262
|
+
profileName = 'web',
|
|
1263
|
+
defaultBundles = CREW_PROFILE_DEFAULT_BUNDLES,
|
|
1264
|
+
} = {}) {
|
|
1265
|
+
if (typeof profileName !== 'string' || !/^[A-Za-z0-9._-]+$/u.test(profileName)) {
|
|
1266
|
+
return { ok: false, code: 'INVALID_PROFILE_NAME' };
|
|
1267
|
+
}
|
|
1268
|
+
const profileRoot = join(crewDshHome({ home }), 'profiles', profileName);
|
|
1269
|
+
const profile = readProfileManifestForRegistration(profileRoot, { create: true, defaultBundles });
|
|
1270
|
+
if (profile.ok === false) return { ok: false, code: profile.code, profileRoot };
|
|
1271
|
+
let scaffoldChanged = false;
|
|
1272
|
+
try {
|
|
1273
|
+
scaffoldChanged = ensureProfileScaffold(profileRoot);
|
|
1274
|
+
if (profile.created) {
|
|
1275
|
+
writeFileSync(profile.profileManifest, JSON.stringify(profile.manifest, null, 2) + '\n');
|
|
1276
|
+
}
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
return { ok: false, code: 'CREW_WEB_PROFILE_SCAFFOLD_FAILED', error: String(error?.message ?? error), profileRoot };
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
ok: true,
|
|
1282
|
+
profileRoot,
|
|
1283
|
+
profileManifest: profile.profileManifest,
|
|
1284
|
+
created: profile.created === true,
|
|
1285
|
+
changed: profile.created === true || scaffoldChanged,
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { validateModelCallabilityV2 } from './model-callability-contract.mjs';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const EXTENSION_CONTRACT_SCHEMA_VERSION = 1;
|
|
8
8
|
|
|
9
9
|
function row(matrix, id) {
|
|
10
10
|
return Array.isArray(matrix?.rows) ? matrix.rows.find((entry) => entry?.id === id) : undefined;
|
|
@@ -3,16 +3,6 @@
|
|
|
3
3
|
// Classification consumes structured status/codes/outcomes only. It never
|
|
4
4
|
// parses raw exception text, provider responses, credentials, quotas or logs.
|
|
5
5
|
|
|
6
|
-
export const FAILURE_CATEGORIES = Object.freeze([
|
|
7
|
-
'none',
|
|
8
|
-
'policy',
|
|
9
|
-
'compatibility',
|
|
10
|
-
'provider',
|
|
11
|
-
'runtime',
|
|
12
|
-
'verification',
|
|
13
|
-
'cancelled',
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
6
|
export const FAILURE_REASON_CODES = Object.freeze({
|
|
17
7
|
NONE: 'NONE',
|
|
18
8
|
CANCELLED: 'CANCELLED',
|
package/src/hub/entry.mjs
CHANGED
|
@@ -68,7 +68,7 @@ export function recordAdaptiveJobOutcome(job, store = getProcessAdaptiveHealthSt
|
|
|
68
68
|
* a chained promise preserves all legacy completion/waiter behavior and runs
|
|
69
69
|
* this observer only after the Hub's own finalizer populated endedAt/status.
|
|
70
70
|
*/
|
|
71
|
-
|
|
71
|
+
function installAdaptiveHealthObserver() {
|
|
72
72
|
if (WorkerRegistry.prototype[ADAPTIVE_OBSERVER_INSTALLED] === true) return false;
|
|
73
73
|
const originalSpawn = WorkerRegistry.prototype.spawn;
|
|
74
74
|
Object.defineProperty(WorkerRegistry.prototype, ADAPTIVE_OBSERVER_INSTALLED, {
|
package/src/hub/index.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { raceWaiters } from '../removable-waiter.mjs';
|
|
|
40
40
|
import { buildProviderInventory } from '../provider-inventory.mjs';
|
|
41
41
|
import { buildProviderLayerMigrationPlan, executeProviderLayerMigration } from '../provider-layer-migration.mjs';
|
|
42
42
|
import { inspectProviderProfile, readProviderDeclarations, readProviderMaterialization } from '../provider-profile-store.mjs';
|
|
43
|
-
import { inspectProviderSettings, readHarnessDefault, readProviderSettingsDeclarations, readProviderSettingsMaterialization } from '../provider-settings-store.mjs';
|
|
43
|
+
import { hasHarnessDefaultBlock, inspectProviderSettings, readHarnessDefault, readProviderSettingsDeclarations, readProviderSettingsMaterialization } from '../provider-settings-store.mjs';
|
|
44
44
|
import { normalizeProviderLifecycleState } from '../provider-lifecycle-state.mjs';
|
|
45
45
|
import { createProviderHealthStore } from '../provider-health.mjs';
|
|
46
46
|
import { planProviderDelete } from '../provider-lifecycle.mjs';
|
|
@@ -119,7 +119,6 @@ export const QUICK_CONFIG_KEYS = Object.freeze([
|
|
|
119
119
|
const LEGACY_TIER_MODELS = { flash: 'deepseek-v4-flash', pro: 'deepseek-v4-pro' };
|
|
120
120
|
// Local copy (the hub must not import jobs.mjs, which pulls the DSH SDK into
|
|
121
121
|
// the profile realm): a valid dispatch role set.
|
|
122
|
-
const ROLES = { worker: true, reviewer: true };
|
|
123
122
|
const CONFIG_DIR = join(homedir(), '.config', 'dsh-crew');
|
|
124
123
|
const CREDENTIAL_PURGE_STATE_FILE = join(CONFIG_DIR, 'credential-purge-lifecycle.json');
|
|
125
124
|
const RECOVERY_ACTION_PATTERN = /^[a-f0-9]{32}$/u;
|
|
@@ -252,6 +251,26 @@ export function hasCompleteProviderDeclarationEvidence(evidence) {
|
|
|
252
251
|
&& Object.values(evidence.sources ?? {}).every((source) => source?.present !== true || !source.code);
|
|
253
252
|
}
|
|
254
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Decide what the Harness default authority evidence means. A persisted
|
|
256
|
+
* `agent-default-model` that disagrees with the live default is a real
|
|
257
|
+
* inconsistency and fails closed; a live default with nothing persisted is the
|
|
258
|
+
* normal state of a home whose model comes from configuration (the profile and
|
|
259
|
+
* base layers) — and the state every settings mutation materializes FROM, so
|
|
260
|
+
* demanding a persisted copy first deadlocked the very migration that writes one.
|
|
261
|
+
*/
|
|
262
|
+
export function projectHarnessDefaultEvidence({ liveDefault = null, persistedDefault = null, persistedDefaultDeclared = false } = {}) {
|
|
263
|
+
if (!liveDefault && !persistedDefault) return { ok: true, source: 'none' };
|
|
264
|
+
if (liveDefault && persistedDefault) {
|
|
265
|
+
return liveDefault.provider === persistedDefault.provider && liveDefault.model === persistedDefault.model
|
|
266
|
+
? { ok: true, source: 'harness-settings' }
|
|
267
|
+
: { ok: false, code: 'PROVIDER_DEFAULT_AUTHORITY_MISMATCH' };
|
|
268
|
+
}
|
|
269
|
+
return persistedDefaultDeclared
|
|
270
|
+
? { ok: false, code: 'PROVIDER_DEFAULT_AUTHORITY_MISMATCH' }
|
|
271
|
+
: { ok: true, source: 'harness-config' };
|
|
272
|
+
}
|
|
273
|
+
|
|
255
274
|
export function hasAvailableProviderLifecycleEvidence(evidence) {
|
|
256
275
|
return evidence?.ok === true;
|
|
257
276
|
}
|
|
@@ -341,6 +360,7 @@ async function readProviderInventorySnapshot(hub, ctx, config) {
|
|
|
341
360
|
} catch { declarationEvidence.ok = false; declarationEvidence.sources.profile = { present: true, code: 'PROVIDER_SOURCE_UNAVAILABLE' }; }
|
|
342
361
|
const settingsFile = join(CONFIG_DIR, 'harness', 'settings.yaml');
|
|
343
362
|
let settingsDefault = null;
|
|
363
|
+
let persistedDefaultDeclared = false;
|
|
344
364
|
let defaultEvidence = { ok: true, source: 'none' };
|
|
345
365
|
try {
|
|
346
366
|
declarationEvidence.sources.settings = { present: existsSync(settingsFile) };
|
|
@@ -353,8 +373,15 @@ async function readProviderInventorySnapshot(hub, ctx, config) {
|
|
|
353
373
|
}
|
|
354
374
|
else { declarationEvidence.ok = false; declarationEvidence.sources.settings.code = parsed.code; }
|
|
355
375
|
const parsedDefault = readHarnessDefault(source);
|
|
356
|
-
if (parsedDefault.ok)
|
|
357
|
-
|
|
376
|
+
if (parsedDefault.ok) {
|
|
377
|
+
settingsDefault = parsedDefault;
|
|
378
|
+
persistedDefaultDeclared = true;
|
|
379
|
+
} else if (hasHarnessDefaultBlock(source)) {
|
|
380
|
+
// The block exists but cannot be read: fail closed on the mutation
|
|
381
|
+
// authority rather than treating the layer as absent.
|
|
382
|
+
defaultEvidence = { ok: false, code: 'PROVIDER_DEFAULT_AUTHORITY_UNAVAILABLE' };
|
|
383
|
+
persistedDefaultDeclared = true;
|
|
384
|
+
}
|
|
358
385
|
}
|
|
359
386
|
} catch {
|
|
360
387
|
declarationEvidence.ok = false;
|
|
@@ -364,9 +391,8 @@ async function readProviderInventorySnapshot(hub, ctx, config) {
|
|
|
364
391
|
const liveDefault = catalog.harness_default && typeof catalog.harness_default.provider === 'string' && typeof catalog.harness_default.model === 'string'
|
|
365
392
|
? { provider: catalog.harness_default.provider, model: catalog.harness_default.model } : null;
|
|
366
393
|
const persistedDefault = settingsDefault ? { provider: settingsDefault.provider, model: settingsDefault.model } : null;
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
else if (liveDefault.provider !== persistedDefault.provider || liveDefault.model !== persistedDefault.model) defaultEvidence = { ok: false, code: 'PROVIDER_DEFAULT_AUTHORITY_MISMATCH' };
|
|
394
|
+
if (defaultEvidence.ok) {
|
|
395
|
+
defaultEvidence = projectHarnessDefaultEvidence({ liveDefault, persistedDefault, persistedDefaultDeclared });
|
|
370
396
|
}
|
|
371
397
|
// The persisted agent-default-model is the mutation authority. Keep it in
|
|
372
398
|
// the inventory even when the live selection is stale; the evidence gate
|
|
@@ -1439,6 +1465,30 @@ export async function apply(ctx) {
|
|
|
1439
1465
|
handler: (req, res) => sendJson(res, 200, { ok: true, service: 'dsh-crew-hub' }),
|
|
1440
1466
|
}));
|
|
1441
1467
|
|
|
1468
|
+
// Operator entry into the full control plane. Its session token is minted per
|
|
1469
|
+
// process, and every surface that links here (the desktop app's panel, the
|
|
1470
|
+
// 3080 panel) runs in a different harness process whose connection service
|
|
1471
|
+
// cannot mint one for this port — linking the bare port landed the operator on
|
|
1472
|
+
// "dsh web authentication required". Only the hub itself can sign its own
|
|
1473
|
+
// session, so it redirects; no token is written to a page, a file or a log.
|
|
1474
|
+
// Loopback-only, because the Location carries a session credential.
|
|
1475
|
+
disposers.push(webServer.register({
|
|
1476
|
+
kind: 'exact', path: `${ROUTE_BASE}/control-plane`,
|
|
1477
|
+
handler: (req, res) => {
|
|
1478
|
+
if (!isLoopbackRequest(req)) return sendJson(res, 403, { ok: false, error: 'loopback only' });
|
|
1479
|
+
const runtime = getHubRuntimeIdentity();
|
|
1480
|
+
const base = `http://127.0.0.1:${Number(runtime?.listen_port) > 0 ? runtime.listen_port : 3210}/`;
|
|
1481
|
+
let target = base;
|
|
1482
|
+
try {
|
|
1483
|
+
const connection = webCtx.get?.('connection');
|
|
1484
|
+
const authenticated = connection?.authenticatedUrl?.(base);
|
|
1485
|
+
if (typeof authenticated === 'string' && authenticated.startsWith('http://')) target = authenticated;
|
|
1486
|
+
} catch { /* the bare control plane stays the honest fallback */ }
|
|
1487
|
+
res.writeHead(302, { location: target, 'cache-control': 'no-store' });
|
|
1488
|
+
res.end();
|
|
1489
|
+
},
|
|
1490
|
+
}));
|
|
1491
|
+
|
|
1442
1492
|
disposers.push(webServer.register({
|
|
1443
1493
|
kind: 'prefix', path: `${ROUTE_BASE}/jobs`,
|
|
1444
1494
|
handler: async (req, res) => {
|
package/src/i18n.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// the check disagreed about what "the installed server" is, and only the
|
|
10
10
|
// installer was right.
|
|
11
11
|
|
|
12
|
-
import { existsSync,
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
|
|
@@ -60,21 +60,26 @@ export function loaderLinkPath({ home = homedir(), name } = {}) {
|
|
|
60
60
|
return join(crewProfileDir({ home }), 'node_modules', ...name.split('/'));
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function sameDirectory(left, right) {
|
|
64
|
-
try { return realpathSync(left) === realpathSync(right); } catch { return false; }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
63
|
/**
|
|
68
64
|
* The root the host integrations were installed from.
|
|
69
65
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* and
|
|
73
|
-
*
|
|
74
|
-
* the
|
|
66
|
+
* The integrations embed the loader link path itself, and the link is what the
|
|
67
|
+
* host loads through: whichever payload it currently resolves to IS the live
|
|
68
|
+
* one, and an upgrade re-points the link so the same embedded path keeps
|
|
69
|
+
* resolving to the new release. Readiness therefore judges the integrations
|
|
70
|
+
* against the link whenever it resolves to a readable payload of this package,
|
|
71
|
+
* and falls back to the caller's root only when the link is broken or carries
|
|
72
|
+
* a different package. Gating on whether the link happens to realpath to the
|
|
73
|
+
* caller's root made the writer (link-rooted) and the validator
|
|
74
|
+
* (release-rooted) disagree about the same machine, and every clean install
|
|
75
|
+
* read back as "needs repair".
|
|
75
76
|
*/
|
|
76
77
|
export function integrationRoot({ home = homedir(), root, name } = {}) {
|
|
77
78
|
const link = loaderLinkPath({ home, name });
|
|
78
79
|
if (!link || !root || !existsSync(link)) return root;
|
|
79
|
-
|
|
80
|
+
try {
|
|
81
|
+
const payload = JSON.parse(readFileSync(join(link, 'package.json'), 'utf8'));
|
|
82
|
+
if (payload?.name === name) return link;
|
|
83
|
+
} catch { /* broken or foreign link payload */ }
|
|
84
|
+
return root;
|
|
80
85
|
}
|
|
@@ -42,7 +42,7 @@ export function crewSkillFiles({ home = homedir(), env = process.env } = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** The skill template shipped in the payload. */
|
|
45
|
-
|
|
45
|
+
function crewSkillSource({ root }) {
|
|
46
46
|
return join(root, 'skills', CREW_SKILL_NAME, 'SKILL.md');
|
|
47
47
|
}
|
|
48
48
|
|