@ran-sh/dsh-crew 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -16
- package/.claude-plugin/plugin.json +14 -14
- package/LICENSE +21 -21
- package/README.de.md +359 -359
- package/README.es.md +359 -359
- package/README.fr.md +359 -359
- package/README.hi.md +359 -359
- package/README.id.md +359 -359
- package/README.ja.md +359 -359
- package/README.ko.md +359 -359
- package/README.md +126 -126
- package/README.pt.md +359 -359
- package/README.ru.md +359 -359
- package/README.th.md +359 -359
- package/README.tr.md +359 -359
- package/README.vi.md +359 -359
- package/README.zh-TW.md +359 -359
- package/README.zh.md +116 -116
- package/agents/ds-flash.md +24 -24
- package/agents/ds-pro.md +24 -24
- package/agents/ds-reviewer.md +23 -23
- package/agents/ds-worker.md +23 -23
- package/bin/dsh-crew.mjs +16 -16
- package/codex/agents/ds-flash.toml +34 -34
- package/codex/agents/ds-pro.toml +34 -34
- package/codex/agents/ds-reviewer.toml +32 -32
- package/codex/agents/ds-worker.toml +32 -32
- package/codex/prompts/dsh-config.md +19 -19
- package/codex/prompts/dsh-status.md +5 -5
- package/commands/config.md +23 -23
- package/commands/off.md +5 -5
- package/commands/on.md +5 -5
- package/commands/status.md +9 -9
- package/cordis.patch.yml +4 -4
- package/docs/gpt-relay-extension.md +103 -103
- package/docs/installation.md +138 -138
- package/docs/job-contracts.md +103 -103
- package/docs/readiness-matrix.md +85 -85
- package/docs/ui-surfaces.md +107 -107
- package/official-web-bridge/cordis.patch.yml +4 -4
- package/official-web-bridge/entry.mjs +1 -1
- package/official-web-bridge/overlay-entry.mjs +59 -59
- package/official-web-bridge/package.json +25 -25
- package/package.json +3 -2
- package/scripts/build-client.mjs +49 -49
- package/scripts/live-crew-smoke.mjs +39 -39
- package/scripts/live-policy-matrix.mjs +177 -177
- package/scripts/policy-probe.mjs +101 -101
- package/scripts/remove-legacy-official-bridge.ps1 +89 -89
- package/scripts/setup.mjs +393 -393
- package/scripts/smoke-real.mjs +110 -110
- package/scripts/smoke.mjs +78 -78
- package/scripts/verify-crew-ui-polish.mjs +145 -145
- package/scripts/verify-history-ui.mjs +97 -97
- package/scripts/verify-installer-fix.mjs +26 -26
- package/scripts/verify-npm-install.mjs +311 -311
- package/scripts/verify-official-bridge-e2e.mjs +192 -192
- package/src/adaptive-routing.mjs +260 -260
- package/src/client/activation-summary.tsx +64 -64
- package/src/client/collapsible-sections.mjs +55 -55
- package/src/client/history-panel.tsx +108 -108
- package/src/client/host-readiness.mjs +71 -71
- package/src/client/index.tsx +1711 -1711
- package/src/client/model-callability-view.mjs +17 -17
- package/src/client/panel-chrome.tsx +40 -40
- package/src/client/quick-entry.tsx +10 -10
- package/src/client/quick-panel.tsx +275 -275
- package/src/client/readiness-envelope.mjs +34 -34
- package/src/client/surface-detection.mjs +43 -43
- package/src/config-readiness.mjs +226 -226
- package/src/credential-reference.mjs +38 -38
- package/src/delivery.mjs +205 -205
- package/src/dsh-cli-runtime.mjs +1021 -1021
- package/src/dsh-cohort.mjs +20 -20
- package/src/extension-contract.mjs +104 -104
- package/src/failure-classification.mjs +201 -201
- package/src/history/admission-gate.mjs +67 -67
- package/src/history/archive-store.mjs +272 -272
- package/src/history/cleanup-plan.mjs +89 -89
- package/src/history/http.mjs +32 -32
- package/src/history/operation.mjs +86 -86
- package/src/history/runner-detach.mjs +34 -34
- package/src/history/runner.mjs +52 -52
- package/src/history/runtime.mjs +36 -36
- package/src/history/service.mjs +177 -177
- package/src/history/state.mjs +27 -27
- package/src/hub/entry.mjs +104 -104
- package/src/hub/index.mjs +2694 -2694
- package/src/hub-client.mjs +154 -154
- package/src/hub-compatibility.mjs +42 -42
- package/src/i18n.mjs +19 -19
- package/src/information-flow.mjs +67 -67
- package/src/install/cli.mjs +28 -28
- package/src/install/install-legacy.mjs +711 -711
- package/src/install/install.mjs +483 -483
- package/src/install/npx-lifecycle.mjs +3456 -3456
- package/src/install/official-frontend-assets.mjs +78 -78
- package/src/install/official-web.mjs +95 -95
- package/src/install/payload-content.mjs +88 -88
- package/src/install/windows-startup.mjs +236 -236
- package/src/install/windows-supervisor-adapter.mjs +443 -443
- package/src/install/windows-supervisor-lifecycle.mjs +782 -782
- package/src/install/zcode.mjs +397 -397
- package/src/job-contracts.mjs +255 -255
- package/src/local-request-guard.mjs +60 -60
- package/src/mcp-runtime.mjs +340 -340
- package/src/model-callability-contract.mjs +79 -79
- package/src/model-catalog.mjs +180 -180
- package/src/model-routing.mjs +586 -586
- package/src/model-schedule.mjs +207 -207
- package/src/official-web-bridge.mjs +447 -447
- package/src/policy.mjs +235 -235
- package/src/provider-delete-adapters.mjs +1934 -1934
- package/src/provider-health.mjs +130 -130
- package/src/provider-inventory.mjs +182 -182
- package/src/provider-layer-migration-adapters.mjs +759 -759
- package/src/provider-layer-migration.mjs +198 -198
- package/src/provider-lifecycle-state.mjs +103 -103
- package/src/provider-lifecycle.mjs +252 -252
- package/src/provider-profile-store.mjs +390 -390
- package/src/provider-settings-store.mjs +633 -633
- package/src/provider-store-lock.mjs +67 -67
- package/src/readiness-matrix.mjs +181 -181
- package/src/removable-waiter.mjs +29 -29
- package/src/role-profiles.mjs +107 -107
- package/src/runtime-controls.mjs +84 -84
- package/src/runtime-identity-contract.mjs +34 -34
- package/src/runtime-identity.mjs +235 -235
- package/src/runtime-readiness-snapshot.mjs +285 -285
- package/src/server.mjs +581 -581
- package/src/session-origins.mjs +60 -60
- package/src/standalone-sdk.mjs +23 -23
- package/src/status-shard.mjs +63 -63
- package/src/structured-error-code.mjs +38 -38
- package/src/supervisor/restart-request.mjs +256 -256
- package/src/workflow-runtime.mjs +739 -739
- package/src/workflow.mjs +155 -155
- package/src/workspace-audit.mjs +231 -231
- package/src/workspace-context.mjs +146 -146
- package/src/workspace-isolation.mjs +463 -455
- package/src/workspace-readiness.mjs +32 -32
- package/statusline/statusline.sh +14 -14
- package/statusline/worker-segment.sh +35 -35
- package/windows/start-dsh-crew.cmd +57 -57
- package/windows/start-dsh-crew.ps1 +1302 -1302
- package/windows/supervisor-control.ps1 +467 -467
- package/worker.cordis.yml +67 -67
- package/zcode/agents/ds-reviewer.md +31 -31
- package/zcode/agents/ds-worker.md +31 -31
- package/zcode/commands/dsh-config.md +17 -17
- package/zcode/commands/dsh-status.md +5 -5
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
5
|
-
import { pathToFileURL } from 'node:url';
|
|
6
|
-
|
|
7
|
-
function plan(home, root) {
|
|
8
|
-
const bridge = join(root, 'official-web-bridge');
|
|
9
|
-
const metadata = JSON.parse(readFileSync(join(bridge, 'package.json'), 'utf8'));
|
|
10
|
-
if (metadata.name !== '@ran-sh/dsh-crew-web-bridge') throw new Error('invalid frontend package');
|
|
11
|
-
const files = new Map([
|
|
12
|
-
['official-web-bridge/entry.mjs', readFileSync(join(bridge, 'overlay-entry.mjs'))],
|
|
13
|
-
['official-web-bridge/lib/client.js', readFileSync(join(bridge, 'lib', 'client.js'))],
|
|
14
|
-
['src/local-request-guard.mjs', readFileSync(join(root, 'src', 'local-request-guard.mjs'))],
|
|
15
|
-
]);
|
|
16
|
-
const hash = createHash('sha256').update(JSON.stringify(metadata));
|
|
17
|
-
for (const [name, bytes] of files) hash.update(name).update(bytes);
|
|
18
|
-
const revision = hash.digest('hex');
|
|
19
|
-
files.set('official-web-bridge/package.json', Buffer.from(JSON.stringify({ ...metadata,
|
|
20
|
-
dshCrewManagedFrontend: true, dshCrewFrontendRevision: revision,
|
|
21
|
-
dsh: { ...metadata.dsh, bundle: undefined },
|
|
22
|
-
}, null, 2) + '\n'));
|
|
23
|
-
const frontendRoot = join(home, '.config', 'dsh-crew', 'frontend');
|
|
24
|
-
const snapshotRoot = join(frontendRoot, 'revisions', revision);
|
|
25
|
-
const overlayFile = join(frontendRoot, 'official-web.patch.json');
|
|
26
|
-
const overlay = JSON.stringify([{ insert: [{ id: 'dsh-crew-official-web-bridge',
|
|
27
|
-
name: pathToFileURL(join(snapshotRoot, 'official-web-bridge', 'entry.mjs')).href,
|
|
28
|
-
}] }], null, 2) + '\n';
|
|
29
|
-
return { frontendRoot, snapshotRoot, revision, overlayFile, overlay, files };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function matches(p) {
|
|
33
|
-
try { return [...p.files].every(([name, bytes]) => {
|
|
34
|
-
const file = join(p.snapshotRoot, name);
|
|
35
|
-
return lstatSync(file).isFile() && !lstatSync(file).isSymbolicLink() && readFileSync(file).equals(bytes);
|
|
36
|
-
}); } catch { return false; }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function officialFrontendAssetsReady({ home = homedir(), root } = {}) {
|
|
40
|
-
try {
|
|
41
|
-
const p = plan(home, root);
|
|
42
|
-
return matches(p) && readFileSync(p.overlayFile, 'utf8') === p.overlay;
|
|
43
|
-
} catch { return false; }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function installOfficialFrontendAssets({ home = homedir(), root } = {}) {
|
|
47
|
-
let stage;
|
|
48
|
-
let temp;
|
|
49
|
-
try {
|
|
50
|
-
const p = plan(home, root);
|
|
51
|
-
for (const path of [p.frontendRoot, join(p.frontendRoot, 'revisions'), p.snapshotRoot, p.overlayFile]) {
|
|
52
|
-
if (existsSync(path) && lstatSync(path).isSymbolicLink()) throw new Error('linked frontend destination');
|
|
53
|
-
}
|
|
54
|
-
const already = officialFrontendAssetsReady({ home, root });
|
|
55
|
-
if (already) return { ok: true, changed: false, revision: p.revision, snapshotRoot: p.snapshotRoot, overlayFile: p.overlayFile };
|
|
56
|
-
mkdirSync(join(p.frontendRoot, 'revisions'), { recursive: true });
|
|
57
|
-
if (!existsSync(p.snapshotRoot)) {
|
|
58
|
-
stage = mkdtempSync(join(p.frontendRoot, '.stage-'));
|
|
59
|
-
for (const [name, bytes] of p.files) {
|
|
60
|
-
const file = join(stage, name);
|
|
61
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
62
|
-
writeFileSync(file, bytes);
|
|
63
|
-
}
|
|
64
|
-
try { renameSync(stage, p.snapshotRoot); stage = null; }
|
|
65
|
-
catch (error) { if (!matches(p)) throw error; }
|
|
66
|
-
}
|
|
67
|
-
if (!matches(p)) throw new Error('frontend snapshot conflicts with its revision');
|
|
68
|
-
temp = `${p.overlayFile}.${randomUUID()}.tmp`;
|
|
69
|
-
writeFileSync(temp, p.overlay, { flag: 'wx', mode: 0o600 });
|
|
70
|
-
renameSync(temp, p.overlayFile); temp = null;
|
|
71
|
-
return { ok: true, changed: true, revision: p.revision, snapshotRoot: p.snapshotRoot, overlayFile: p.overlayFile };
|
|
72
|
-
} catch (error) {
|
|
73
|
-
return { ok: false, code: 'OFFICIAL_FRONTEND_ASSETS_FAILED', error: error.message };
|
|
74
|
-
} finally {
|
|
75
|
-
if (stage) rmSync(stage, { recursive: true, force: true });
|
|
76
|
-
if (temp) rmSync(temp, { force: true });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
function plan(home, root) {
|
|
8
|
+
const bridge = join(root, 'official-web-bridge');
|
|
9
|
+
const metadata = JSON.parse(readFileSync(join(bridge, 'package.json'), 'utf8'));
|
|
10
|
+
if (metadata.name !== '@ran-sh/dsh-crew-web-bridge') throw new Error('invalid frontend package');
|
|
11
|
+
const files = new Map([
|
|
12
|
+
['official-web-bridge/entry.mjs', readFileSync(join(bridge, 'overlay-entry.mjs'))],
|
|
13
|
+
['official-web-bridge/lib/client.js', readFileSync(join(bridge, 'lib', 'client.js'))],
|
|
14
|
+
['src/local-request-guard.mjs', readFileSync(join(root, 'src', 'local-request-guard.mjs'))],
|
|
15
|
+
]);
|
|
16
|
+
const hash = createHash('sha256').update(JSON.stringify(metadata));
|
|
17
|
+
for (const [name, bytes] of files) hash.update(name).update(bytes);
|
|
18
|
+
const revision = hash.digest('hex');
|
|
19
|
+
files.set('official-web-bridge/package.json', Buffer.from(JSON.stringify({ ...metadata,
|
|
20
|
+
dshCrewManagedFrontend: true, dshCrewFrontendRevision: revision,
|
|
21
|
+
dsh: { ...metadata.dsh, bundle: undefined },
|
|
22
|
+
}, null, 2) + '\n'));
|
|
23
|
+
const frontendRoot = join(home, '.config', 'dsh-crew', 'frontend');
|
|
24
|
+
const snapshotRoot = join(frontendRoot, 'revisions', revision);
|
|
25
|
+
const overlayFile = join(frontendRoot, 'official-web.patch.json');
|
|
26
|
+
const overlay = JSON.stringify([{ insert: [{ id: 'dsh-crew-official-web-bridge',
|
|
27
|
+
name: pathToFileURL(join(snapshotRoot, 'official-web-bridge', 'entry.mjs')).href,
|
|
28
|
+
}] }], null, 2) + '\n';
|
|
29
|
+
return { frontendRoot, snapshotRoot, revision, overlayFile, overlay, files };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function matches(p) {
|
|
33
|
+
try { return [...p.files].every(([name, bytes]) => {
|
|
34
|
+
const file = join(p.snapshotRoot, name);
|
|
35
|
+
return lstatSync(file).isFile() && !lstatSync(file).isSymbolicLink() && readFileSync(file).equals(bytes);
|
|
36
|
+
}); } catch { return false; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function officialFrontendAssetsReady({ home = homedir(), root } = {}) {
|
|
40
|
+
try {
|
|
41
|
+
const p = plan(home, root);
|
|
42
|
+
return matches(p) && readFileSync(p.overlayFile, 'utf8') === p.overlay;
|
|
43
|
+
} catch { return false; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function installOfficialFrontendAssets({ home = homedir(), root } = {}) {
|
|
47
|
+
let stage;
|
|
48
|
+
let temp;
|
|
49
|
+
try {
|
|
50
|
+
const p = plan(home, root);
|
|
51
|
+
for (const path of [p.frontendRoot, join(p.frontendRoot, 'revisions'), p.snapshotRoot, p.overlayFile]) {
|
|
52
|
+
if (existsSync(path) && lstatSync(path).isSymbolicLink()) throw new Error('linked frontend destination');
|
|
53
|
+
}
|
|
54
|
+
const already = officialFrontendAssetsReady({ home, root });
|
|
55
|
+
if (already) return { ok: true, changed: false, revision: p.revision, snapshotRoot: p.snapshotRoot, overlayFile: p.overlayFile };
|
|
56
|
+
mkdirSync(join(p.frontendRoot, 'revisions'), { recursive: true });
|
|
57
|
+
if (!existsSync(p.snapshotRoot)) {
|
|
58
|
+
stage = mkdtempSync(join(p.frontendRoot, '.stage-'));
|
|
59
|
+
for (const [name, bytes] of p.files) {
|
|
60
|
+
const file = join(stage, name);
|
|
61
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
62
|
+
writeFileSync(file, bytes);
|
|
63
|
+
}
|
|
64
|
+
try { renameSync(stage, p.snapshotRoot); stage = null; }
|
|
65
|
+
catch (error) { if (!matches(p)) throw error; }
|
|
66
|
+
}
|
|
67
|
+
if (!matches(p)) throw new Error('frontend snapshot conflicts with its revision');
|
|
68
|
+
temp = `${p.overlayFile}.${randomUUID()}.tmp`;
|
|
69
|
+
writeFileSync(temp, p.overlay, { flag: 'wx', mode: 0o600 });
|
|
70
|
+
renameSync(temp, p.overlayFile); temp = null;
|
|
71
|
+
return { ok: true, changed: true, revision: p.revision, snapshotRoot: p.snapshotRoot, overlayFile: p.overlayFile };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return { ok: false, code: 'OFFICIAL_FRONTEND_ASSETS_FAILED', error: error.message };
|
|
74
|
+
} finally {
|
|
75
|
+
if (stage) rmSync(stage, { recursive: true, force: true });
|
|
76
|
+
if (temp) rmSync(temp, { force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
lstatSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
realpathSync,
|
|
6
|
-
} from 'node:fs';
|
|
7
|
-
import { homedir } from 'node:os';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
|
|
10
|
-
export const OFFICIAL_BRIDGE_PACKAGE = '@ran-sh/dsh-crew-web-bridge';
|
|
11
|
-
const STATE_FILENAME = 'official-web.json';
|
|
12
|
-
|
|
13
|
-
export function officialWebProfileDir({ home = homedir() } = {}) {
|
|
14
|
-
return join(home, '.dsh', 'profiles', 'web');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function officialWebIntegrationStateFile({ home = homedir() } = {}) {
|
|
18
|
-
return join(home, '.config', 'dsh-crew', STATE_FILENAME);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function readState(home) {
|
|
22
|
-
try {
|
|
23
|
-
const value = JSON.parse(readFileSync(officialWebIntegrationStateFile({ home }), 'utf8'));
|
|
24
|
-
return value && typeof value === 'object' ? value : null;
|
|
25
|
-
} catch { return null; }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function validOfficialManifest(file) {
|
|
29
|
-
if (!existsSync(file)) return { ok: false, code: 'OFFICIAL_WEB_PROFILE_NOT_FOUND' };
|
|
30
|
-
try {
|
|
31
|
-
const raw = readFileSync(file, 'utf8');
|
|
32
|
-
const manifest = JSON.parse(raw);
|
|
33
|
-
if (!manifest || typeof manifest !== 'object'
|
|
34
|
-
|| (manifest.dependencies !== undefined && (!manifest.dependencies || typeof manifest.dependencies !== 'object' || Array.isArray(manifest.dependencies)))
|
|
35
|
-
|| !Array.isArray(manifest.dsh?.profile?.bundles)) {
|
|
36
|
-
return { ok: false, code: 'OFFICIAL_WEB_PROFILE_INVALID' };
|
|
37
|
-
}
|
|
38
|
-
return { ok: true, raw, manifest };
|
|
39
|
-
} catch { return { ok: false, code: 'OFFICIAL_WEB_PROFILE_INVALID' }; }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export const OFFICIAL_WEB_READ_ONLY_CODE = 'OFFICIAL_WEB_PROFILE_READ_ONLY';
|
|
43
|
-
|
|
44
|
-
// The official ~/.dsh/profiles/web tree is a read-only boundary. Crew must
|
|
45
|
-
// never install, update, register, unlink, repair, or mutate anything under
|
|
46
|
-
// ~/.dsh; the isolated 3210 Crew backend is the only runtime Crew owns.
|
|
47
|
-
// ensure/remove therefore fail closed instead of writing the official
|
|
48
|
-
// profile. Status probing stays read-only.
|
|
49
|
-
export function ensureOfficialWebIntegration({ home = homedir(), releaseDir } = {}) {
|
|
50
|
-
return { ok: false, code: OFFICIAL_WEB_READ_ONLY_CODE };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Project the legacy full-bridge state file. The old `enabled: true` shape
|
|
54
|
-
// is DEPRECATED: dsh-crew no longer desires any control of the 3080 official
|
|
55
|
-
// web profile (the native 3210 page is the full control plane, and the 3080
|
|
56
|
-
// quick surface is optional). Status probing stays read-only; the projection
|
|
57
|
-
// makes the legacy record diagnostic instead of an active intent.
|
|
58
|
-
function projectLegacyState(state, home, releaseDir) {
|
|
59
|
-
if (!state || state?.enabled !== true) {
|
|
60
|
-
return { enabled: false, legacy_present: false, desired: false, healthy: false, state };
|
|
61
|
-
}
|
|
62
|
-
const expectedRelease = releaseDir ?? state.release_dir;
|
|
63
|
-
let expectedBridge;
|
|
64
|
-
try { expectedBridge = realpathSync(join(expectedRelease, 'official-web-bridge')); } catch {
|
|
65
|
-
return { enabled: false, legacy_present: true, desired: false, healthy: false, code: 'BRIDGE_RELEASE_MISSING', state };
|
|
66
|
-
}
|
|
67
|
-
const profileRoot = officialWebProfileDir({ home });
|
|
68
|
-
const profile = validOfficialManifest(join(profileRoot, 'package.json'));
|
|
69
|
-
if (!profile.ok) {
|
|
70
|
-
return { enabled: false, legacy_present: true, desired: false, healthy: false, code: profile.code, state };
|
|
71
|
-
}
|
|
72
|
-
const dependency = profile.manifest.dependencies?.[OFFICIAL_BRIDGE_PACKAGE];
|
|
73
|
-
const bundled = profile.manifest.dsh.profile.bundles.includes(OFFICIAL_BRIDGE_PACKAGE);
|
|
74
|
-
const linkPath = join(profileRoot, 'node_modules', ...OFFICIAL_BRIDGE_PACKAGE.split('/'));
|
|
75
|
-
let linked = false;
|
|
76
|
-
try { linked = lstatSync(linkPath).isSymbolicLink() && realpathSync(linkPath) === expectedBridge; } catch {}
|
|
77
|
-
return {
|
|
78
|
-
enabled: false,
|
|
79
|
-
legacy_present: true,
|
|
80
|
-
desired: false,
|
|
81
|
-
healthy: Boolean(dependency && bundled && linked),
|
|
82
|
-
removal_requires_manual_official_profile_action: true,
|
|
83
|
-
state,
|
|
84
|
-
linkPath,
|
|
85
|
-
expectedBridge,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function officialWebIntegrationStatus({ home = homedir(), releaseDir } = {}) {
|
|
90
|
-
return projectLegacyState(readState(home), home, releaseDir);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function removeOfficialWebIntegration({ home = homedir(), remember = true, preserveIntent = false } = {}) {
|
|
94
|
-
return { ok: false, code: OFFICIAL_WEB_READ_ONLY_CODE };
|
|
95
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
realpathSync,
|
|
6
|
+
} from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
export const OFFICIAL_BRIDGE_PACKAGE = '@ran-sh/dsh-crew-web-bridge';
|
|
11
|
+
const STATE_FILENAME = 'official-web.json';
|
|
12
|
+
|
|
13
|
+
export function officialWebProfileDir({ home = homedir() } = {}) {
|
|
14
|
+
return join(home, '.dsh', 'profiles', 'web');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function officialWebIntegrationStateFile({ home = homedir() } = {}) {
|
|
18
|
+
return join(home, '.config', 'dsh-crew', STATE_FILENAME);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readState(home) {
|
|
22
|
+
try {
|
|
23
|
+
const value = JSON.parse(readFileSync(officialWebIntegrationStateFile({ home }), 'utf8'));
|
|
24
|
+
return value && typeof value === 'object' ? value : null;
|
|
25
|
+
} catch { return null; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validOfficialManifest(file) {
|
|
29
|
+
if (!existsSync(file)) return { ok: false, code: 'OFFICIAL_WEB_PROFILE_NOT_FOUND' };
|
|
30
|
+
try {
|
|
31
|
+
const raw = readFileSync(file, 'utf8');
|
|
32
|
+
const manifest = JSON.parse(raw);
|
|
33
|
+
if (!manifest || typeof manifest !== 'object'
|
|
34
|
+
|| (manifest.dependencies !== undefined && (!manifest.dependencies || typeof manifest.dependencies !== 'object' || Array.isArray(manifest.dependencies)))
|
|
35
|
+
|| !Array.isArray(manifest.dsh?.profile?.bundles)) {
|
|
36
|
+
return { ok: false, code: 'OFFICIAL_WEB_PROFILE_INVALID' };
|
|
37
|
+
}
|
|
38
|
+
return { ok: true, raw, manifest };
|
|
39
|
+
} catch { return { ok: false, code: 'OFFICIAL_WEB_PROFILE_INVALID' }; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const OFFICIAL_WEB_READ_ONLY_CODE = 'OFFICIAL_WEB_PROFILE_READ_ONLY';
|
|
43
|
+
|
|
44
|
+
// The official ~/.dsh/profiles/web tree is a read-only boundary. Crew must
|
|
45
|
+
// never install, update, register, unlink, repair, or mutate anything under
|
|
46
|
+
// ~/.dsh; the isolated 3210 Crew backend is the only runtime Crew owns.
|
|
47
|
+
// ensure/remove therefore fail closed instead of writing the official
|
|
48
|
+
// profile. Status probing stays read-only.
|
|
49
|
+
export function ensureOfficialWebIntegration({ home = homedir(), releaseDir } = {}) {
|
|
50
|
+
return { ok: false, code: OFFICIAL_WEB_READ_ONLY_CODE };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Project the legacy full-bridge state file. The old `enabled: true` shape
|
|
54
|
+
// is DEPRECATED: dsh-crew no longer desires any control of the 3080 official
|
|
55
|
+
// web profile (the native 3210 page is the full control plane, and the 3080
|
|
56
|
+
// quick surface is optional). Status probing stays read-only; the projection
|
|
57
|
+
// makes the legacy record diagnostic instead of an active intent.
|
|
58
|
+
function projectLegacyState(state, home, releaseDir) {
|
|
59
|
+
if (!state || state?.enabled !== true) {
|
|
60
|
+
return { enabled: false, legacy_present: false, desired: false, healthy: false, state };
|
|
61
|
+
}
|
|
62
|
+
const expectedRelease = releaseDir ?? state.release_dir;
|
|
63
|
+
let expectedBridge;
|
|
64
|
+
try { expectedBridge = realpathSync(join(expectedRelease, 'official-web-bridge')); } catch {
|
|
65
|
+
return { enabled: false, legacy_present: true, desired: false, healthy: false, code: 'BRIDGE_RELEASE_MISSING', state };
|
|
66
|
+
}
|
|
67
|
+
const profileRoot = officialWebProfileDir({ home });
|
|
68
|
+
const profile = validOfficialManifest(join(profileRoot, 'package.json'));
|
|
69
|
+
if (!profile.ok) {
|
|
70
|
+
return { enabled: false, legacy_present: true, desired: false, healthy: false, code: profile.code, state };
|
|
71
|
+
}
|
|
72
|
+
const dependency = profile.manifest.dependencies?.[OFFICIAL_BRIDGE_PACKAGE];
|
|
73
|
+
const bundled = profile.manifest.dsh.profile.bundles.includes(OFFICIAL_BRIDGE_PACKAGE);
|
|
74
|
+
const linkPath = join(profileRoot, 'node_modules', ...OFFICIAL_BRIDGE_PACKAGE.split('/'));
|
|
75
|
+
let linked = false;
|
|
76
|
+
try { linked = lstatSync(linkPath).isSymbolicLink() && realpathSync(linkPath) === expectedBridge; } catch {}
|
|
77
|
+
return {
|
|
78
|
+
enabled: false,
|
|
79
|
+
legacy_present: true,
|
|
80
|
+
desired: false,
|
|
81
|
+
healthy: Boolean(dependency && bundled && linked),
|
|
82
|
+
removal_requires_manual_official_profile_action: true,
|
|
83
|
+
state,
|
|
84
|
+
linkPath,
|
|
85
|
+
expectedBridge,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function officialWebIntegrationStatus({ home = homedir(), releaseDir } = {}) {
|
|
90
|
+
return projectLegacyState(readState(home), home, releaseDir);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function removeOfficialWebIntegration({ home = homedir(), remember = true, preserveIntent = false } = {}) {
|
|
94
|
+
return { ok: false, code: OFFICIAL_WEB_READ_ONLY_CODE };
|
|
95
|
+
}
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { isAbsolute, join, posix } from 'node:path';
|
|
4
|
-
|
|
5
|
-
function canonical(value) {
|
|
6
|
-
if (Array.isArray(value)) return value.map(canonical);
|
|
7
|
-
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
|
|
8
|
-
return value;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function runtimeManifest(manifest) {
|
|
12
|
-
const normalized = { ...manifest, dependencies: { ...manifest.peerDependencies, ...manifest.dependencies } };
|
|
13
|
-
delete normalized.devDependencies;
|
|
14
|
-
delete normalized.peerDependencies;
|
|
15
|
-
delete normalized.peerDependenciesMeta;
|
|
16
|
-
return canonical(normalized);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Capture bounded first-party package bytes once for both validation and copying. */
|
|
20
|
-
export function capturePayloadContent(root, suppliedManifest) {
|
|
21
|
-
try {
|
|
22
|
-
const manifest = suppliedManifest ?? JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
23
|
-
if (!Array.isArray(manifest.files)) return null;
|
|
24
|
-
const files = new Map();
|
|
25
|
-
const modes = new Map();
|
|
26
|
-
const directories = new Set();
|
|
27
|
-
let bytes = 0;
|
|
28
|
-
const visit = relative => {
|
|
29
|
-
let parent = root;
|
|
30
|
-
for (const segment of posix.dirname(relative).split('/').filter(part => part && part !== '.')) {
|
|
31
|
-
parent = join(parent, segment);
|
|
32
|
-
const info = lstatSync(parent);
|
|
33
|
-
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('unsupported payload parent');
|
|
34
|
-
}
|
|
35
|
-
const file = join(root, relative);
|
|
36
|
-
const stat = lstatSync(file);
|
|
37
|
-
if (stat.isSymbolicLink()) throw new Error('unsupported payload symlink');
|
|
38
|
-
if (stat.isDirectory()) {
|
|
39
|
-
if (directories.size >= 4096) throw new Error('payload directory limit exceeded');
|
|
40
|
-
directories.add(relative);
|
|
41
|
-
for (const name of readdirSync(file)) visit(posix.join(relative, name));
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (!stat.isFile() || files.size >= 4096) throw new Error('unsupported payload entry');
|
|
45
|
-
if (files.has(relative)) return;
|
|
46
|
-
bytes += stat.size;
|
|
47
|
-
if (bytes > 64 * 1024 * 1024) throw new Error('payload content limit exceeded');
|
|
48
|
-
const content = relative === 'package.json'
|
|
49
|
-
? Buffer.from(JSON.stringify(runtimeManifest(manifest))) : readFileSync(file);
|
|
50
|
-
if (content.length > stat.size && relative !== 'package.json') throw new Error('payload changed while being read');
|
|
51
|
-
files.set(relative, content);
|
|
52
|
-
modes.set(relative, stat.mode & 0o777);
|
|
53
|
-
};
|
|
54
|
-
visit('package.json');
|
|
55
|
-
for (const entry of manifest.files ?? []) {
|
|
56
|
-
if (typeof entry !== 'string') throw new Error('invalid payload pattern');
|
|
57
|
-
const pattern = entry.replaceAll('\\', '/');
|
|
58
|
-
const segments = pattern.split('/');
|
|
59
|
-
if (!pattern.trim() || entry.includes('\\') || posix.normalize(pattern) === '.'
|
|
60
|
-
|| isAbsolute(pattern) || pattern.includes(':')
|
|
61
|
-
|| segments.some(segment => ['..', '.git', 'node_modules'].includes(segment.toLowerCase()))) throw new Error('invalid payload path');
|
|
62
|
-
if (!pattern.includes('*')) {
|
|
63
|
-
if (existsSync(join(root, pattern))) visit(pattern);
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
const base = posix.dirname(pattern);
|
|
67
|
-
if (base.includes('*')) throw new Error('unsupported payload wildcard');
|
|
68
|
-
const expression = new RegExp('^' + posix.basename(pattern).split('*').map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*') + '$');
|
|
69
|
-
if (existsSync(join(root, base))) {
|
|
70
|
-
for (const name of readdirSync(join(root, base))) if (expression.test(name)) visit(posix.join(base, name));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return { files, directories, modes };
|
|
74
|
-
} catch { return null; }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** This digest covers first-party shipped files, not dependency tamper attestation. */
|
|
78
|
-
export function payloadContentDigest(root) {
|
|
79
|
-
const snapshot = capturePayloadContent(root);
|
|
80
|
-
if (!snapshot) return null;
|
|
81
|
-
const hashes = [...snapshot.files].map(([name, bytes]) => [name, createHash('sha256').update(bytes).digest('hex')]);
|
|
82
|
-
return createHash('sha256').update(JSON.stringify(hashes.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))).digest('hex');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function samePayloadContent(sourceRoot, installedRoot) {
|
|
86
|
-
const source = payloadContentDigest(sourceRoot);
|
|
87
|
-
return source !== null && source === payloadContentDigest(installedRoot);
|
|
88
|
-
}
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, posix } from 'node:path';
|
|
4
|
+
|
|
5
|
+
function canonical(value) {
|
|
6
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
7
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function runtimeManifest(manifest) {
|
|
12
|
+
const normalized = { ...manifest, dependencies: { ...manifest.peerDependencies, ...manifest.dependencies } };
|
|
13
|
+
delete normalized.devDependencies;
|
|
14
|
+
delete normalized.peerDependencies;
|
|
15
|
+
delete normalized.peerDependenciesMeta;
|
|
16
|
+
return canonical(normalized);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Capture bounded first-party package bytes once for both validation and copying. */
|
|
20
|
+
export function capturePayloadContent(root, suppliedManifest) {
|
|
21
|
+
try {
|
|
22
|
+
const manifest = suppliedManifest ?? JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
23
|
+
if (!Array.isArray(manifest.files)) return null;
|
|
24
|
+
const files = new Map();
|
|
25
|
+
const modes = new Map();
|
|
26
|
+
const directories = new Set();
|
|
27
|
+
let bytes = 0;
|
|
28
|
+
const visit = relative => {
|
|
29
|
+
let parent = root;
|
|
30
|
+
for (const segment of posix.dirname(relative).split('/').filter(part => part && part !== '.')) {
|
|
31
|
+
parent = join(parent, segment);
|
|
32
|
+
const info = lstatSync(parent);
|
|
33
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('unsupported payload parent');
|
|
34
|
+
}
|
|
35
|
+
const file = join(root, relative);
|
|
36
|
+
const stat = lstatSync(file);
|
|
37
|
+
if (stat.isSymbolicLink()) throw new Error('unsupported payload symlink');
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
if (directories.size >= 4096) throw new Error('payload directory limit exceeded');
|
|
40
|
+
directories.add(relative);
|
|
41
|
+
for (const name of readdirSync(file)) visit(posix.join(relative, name));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!stat.isFile() || files.size >= 4096) throw new Error('unsupported payload entry');
|
|
45
|
+
if (files.has(relative)) return;
|
|
46
|
+
bytes += stat.size;
|
|
47
|
+
if (bytes > 64 * 1024 * 1024) throw new Error('payload content limit exceeded');
|
|
48
|
+
const content = relative === 'package.json'
|
|
49
|
+
? Buffer.from(JSON.stringify(runtimeManifest(manifest))) : readFileSync(file);
|
|
50
|
+
if (content.length > stat.size && relative !== 'package.json') throw new Error('payload changed while being read');
|
|
51
|
+
files.set(relative, content);
|
|
52
|
+
modes.set(relative, stat.mode & 0o777);
|
|
53
|
+
};
|
|
54
|
+
visit('package.json');
|
|
55
|
+
for (const entry of manifest.files ?? []) {
|
|
56
|
+
if (typeof entry !== 'string') throw new Error('invalid payload pattern');
|
|
57
|
+
const pattern = entry.replaceAll('\\', '/');
|
|
58
|
+
const segments = pattern.split('/');
|
|
59
|
+
if (!pattern.trim() || entry.includes('\\') || posix.normalize(pattern) === '.'
|
|
60
|
+
|| isAbsolute(pattern) || pattern.includes(':')
|
|
61
|
+
|| segments.some(segment => ['..', '.git', 'node_modules'].includes(segment.toLowerCase()))) throw new Error('invalid payload path');
|
|
62
|
+
if (!pattern.includes('*')) {
|
|
63
|
+
if (existsSync(join(root, pattern))) visit(pattern);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const base = posix.dirname(pattern);
|
|
67
|
+
if (base.includes('*')) throw new Error('unsupported payload wildcard');
|
|
68
|
+
const expression = new RegExp('^' + posix.basename(pattern).split('*').map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*') + '$');
|
|
69
|
+
if (existsSync(join(root, base))) {
|
|
70
|
+
for (const name of readdirSync(join(root, base))) if (expression.test(name)) visit(posix.join(base, name));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { files, directories, modes };
|
|
74
|
+
} catch { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** This digest covers first-party shipped files, not dependency tamper attestation. */
|
|
78
|
+
export function payloadContentDigest(root) {
|
|
79
|
+
const snapshot = capturePayloadContent(root);
|
|
80
|
+
if (!snapshot) return null;
|
|
81
|
+
const hashes = [...snapshot.files].map(([name, bytes]) => [name, createHash('sha256').update(bytes).digest('hex')]);
|
|
82
|
+
return createHash('sha256').update(JSON.stringify(hashes.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))).digest('hex');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function samePayloadContent(sourceRoot, installedRoot) {
|
|
86
|
+
const source = payloadContentDigest(sourceRoot);
|
|
87
|
+
return source !== null && source === payloadContentDigest(installedRoot);
|
|
88
|
+
}
|