@ran-sh/dsh-crew 1.0.2 → 1.1.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.
Files changed (40) hide show
  1. package/README.md +4 -0
  2. package/README.zh.md +4 -0
  3. package/codex/AGENTS.md +101 -92
  4. package/docs/ui-surfaces.md +48 -21
  5. package/lib/client.js +235 -34
  6. package/official-web-bridge/lib/client.js +368 -4532
  7. package/official-web-bridge/package.json +1 -2
  8. package/package.json +130 -67
  9. package/scripts/build-client.mjs +30 -15
  10. package/scripts/remove-legacy-official-bridge.ps1 +89 -0
  11. package/scripts/setup.mjs +152 -140
  12. package/scripts/verify-npm-install.mjs +311 -310
  13. package/scripts/verify-official-bridge-e2e.mjs +12 -1
  14. package/src/client/host-readiness.mjs +62 -59
  15. package/src/client/index.tsx +117 -24
  16. package/src/client/quick-entry.tsx +10 -0
  17. package/src/client/quick-panel.tsx +295 -0
  18. package/src/client/surface-detection.mjs +43 -30
  19. package/src/credential-reference.mjs +38 -0
  20. package/src/dsh-cli-runtime.mjs +500 -40
  21. package/src/dsh-cohort.mjs +20 -0
  22. package/src/hub/index.mjs +1602 -1016
  23. package/src/install/npx-lifecycle.mjs +1983 -468
  24. package/src/install/official-web.mjs +36 -73
  25. package/src/jobs.mjs +20 -24
  26. package/src/model-catalog.mjs +8 -1
  27. package/src/official-web-bridge.mjs +329 -249
  28. package/src/provider-delete-adapters.mjs +165 -17
  29. package/src/provider-inventory.mjs +17 -3
  30. package/src/provider-layer-migration-adapters.mjs +759 -0
  31. package/src/provider-layer-migration.mjs +198 -0
  32. package/src/provider-lifecycle-state.mjs +4 -1
  33. package/src/provider-profile-store.mjs +193 -4
  34. package/src/provider-settings-store.mjs +82 -6
  35. package/src/provider-store-lock.mjs +67 -0
  36. package/src/runtime-identity.mjs +24 -1
  37. package/src/supervisor/restart-request.mjs +202 -0
  38. package/windows/start-dsh-crew.ps1 +783 -370
  39. package/worker.cordis.yml +32 -46
  40. package/zcode/AGENTS.md +35 -26
@@ -1,5 +1,5 @@
1
- import { spawn } from 'node:child_process';
2
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { crewDshHome } from './install/install.mjs';
@@ -16,11 +16,11 @@ const HOP_BY_HOP = new Set([
16
16
  'te', 'trailer', 'transfer-encoding', 'upgrade', 'host', 'content-length',
17
17
  ]);
18
18
 
19
- export function resolveCrewBridgeTarget() {
20
- // Production bridge routing is an invariant, not an environment setting.
21
- // Tests can inject dependencies directly into createCrewSidecarSupervisor.
22
- return CREW_BRIDGE_TARGET;
23
- }
19
+ export function resolveCrewBridgeTarget() {
20
+ // Production bridge routing is an invariant, not an environment setting.
21
+ // Tests can inject dependencies directly into createCrewSidecarSupervisor.
22
+ return CREW_BRIDGE_TARGET;
23
+ }
24
24
 
25
25
  export function isTrustedLocalRequest(req) {
26
26
  if (!localRequestCore(req)) return false;
@@ -65,132 +65,142 @@ async function readBoundedBody(req, limit = MAX_BODY_BYTES) {
65
65
  return Buffer.concat(chunks);
66
66
  }
67
67
 
68
- export async function defaultRuntimeIdentity(fetchImpl = globalThis.fetch, bridgeTarget = CREW_BRIDGE_TARGET) {
69
- try {
70
- const response = await fetchImpl(`${bridgeTarget}${CREW_BRIDGE_PREFIX}/runtime`, {
71
- signal: AbortSignal.timeout(1_500),
72
- headers: { accept: 'application/json' },
73
- });
74
- if (!response.ok) return null;
75
- const body = await response.json();
76
- if (!(body?.ok === true
77
- && body.execution_plane === 'hub-3210'
78
- && body.profile === 'dsh-crew'
79
- && Number(body.listen_port) === 3210
80
- && typeof body.runtime_id === 'string'
81
- && body.runtime_id.trim().length > 0)) return null;
82
- return { execution_plane: body.execution_plane, profile: body.profile, listen_port: Number(body.listen_port), runtime_id: body.runtime_id.trim() };
83
- } catch { return null; }
84
- }
85
-
86
- export async function defaultHealthCheck(fetchImpl = globalThis.fetch, bridgeTarget = CREW_BRIDGE_TARGET) {
87
- return (await defaultRuntimeIdentity(fetchImpl, bridgeTarget)) !== null;
88
- }
68
+ export async function defaultRuntimeIdentity(fetchImpl = globalThis.fetch, bridgeTarget = CREW_BRIDGE_TARGET) {
69
+ try {
70
+ const response = await fetchImpl(`${bridgeTarget}${CREW_BRIDGE_PREFIX}/runtime`, {
71
+ signal: AbortSignal.timeout(1_500),
72
+ headers: { accept: 'application/json' },
73
+ });
74
+ if (!response.ok) return null;
75
+ const body = await response.json();
76
+ if (!(body?.ok === true
77
+ && body.execution_plane === 'hub-3210'
78
+ && body.profile === 'dsh-crew'
79
+ && Number(body.listen_port) === 3210
80
+ && typeof body.runtime_id === 'string'
81
+ && body.runtime_id.trim().length > 0)) return null;
82
+ return { execution_plane: body.execution_plane, profile: body.profile, listen_port: Number(body.listen_port), runtime_id: body.runtime_id.trim() };
83
+ } catch { return null; }
84
+ }
85
+
86
+ export async function defaultHealthCheck(fetchImpl = globalThis.fetch, bridgeTarget = CREW_BRIDGE_TARGET) {
87
+ return (await defaultRuntimeIdentity(fetchImpl, bridgeTarget)) !== null;
88
+ }
89
89
 
90
90
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
91
91
 
92
- export function createCrewSidecarSupervisor({
93
- home = homedir(),
94
- exists = existsSync,
95
- bridgeTarget = resolveCrewBridgeTarget(),
96
- healthCheck = () => defaultHealthCheck(globalThis.fetch, bridgeTarget),
97
- spawnImpl = spawn,
98
- killImpl = (pid) => process.kill(pid),
99
- runtimeIdentity = () => defaultRuntimeIdentity(globalThis.fetch, bridgeTarget),
100
- wait = delay,
101
- maxAttempts = 120,
102
- pollInterval = 250,
103
- ownershipFile = join(crewDshHome({ home }), 'supervisor-ownership.json'),
104
- } = {}) {
105
- if (bridgeTarget !== CREW_BRIDGE_TARGET) throw new Error('Crew supervisor is fixed to the isolated 3210 Crew Hub');
106
- let starting = null;
107
- let restarting = null;
108
- let runningChild = null;
92
+ export function createCrewSidecarSupervisor({
93
+ home = homedir(),
94
+ exists = existsSync,
95
+ bridgeTarget = resolveCrewBridgeTarget(),
96
+ healthCheck = () => defaultHealthCheck(globalThis.fetch, bridgeTarget),
97
+ spawnImpl = spawn,
98
+ killImpl = (pid) => process.kill(pid),
99
+ runtimeIdentity = () => defaultRuntimeIdentity(globalThis.fetch, bridgeTarget),
100
+ wait = delay,
101
+ maxAttempts = 120,
102
+ pollInterval = 250,
103
+ ownershipFile = join(crewDshHome({ home }), 'supervisor-ownership.json'),
104
+ } = {}) {
105
+ if (bridgeTarget !== CREW_BRIDGE_TARGET) throw new Error('Crew supervisor is fixed to the isolated 3210 Crew Hub');
106
+ let starting = null;
107
+ let restarting = null;
108
+ let runningChild = null;
109
109
  const runtime = crewDshRuntimeModule({ home });
110
- const dshHome = crewDshHome({ home });
111
- const bridgePort = new URL(bridgeTarget).port;
112
-
113
- function readOwnership() {
114
- try {
115
- const record = JSON.parse(readFileSync(ownershipFile, 'utf8'));
116
- if (record?.schema_version !== 1
117
- || !Number.isInteger(record.pid) || record.pid < 1
118
- || record.profile !== 'dsh-crew'
119
- || record.execution_plane !== 'hub-3210'
120
- || Number(record.port) !== Number(bridgePort)) return null;
121
- return record;
122
- } catch { return null; }
123
- }
124
-
125
- function clearOwnership(expectedPid) {
126
- try {
127
- const current = readOwnership();
128
- if (expectedPid !== undefined && current?.pid !== expectedPid) return;
129
- rmSync(ownershipFile, { force: true });
130
- } catch {}
131
- }
132
-
133
- function persistOwnership(pid, identity = null) {
134
- if (!Number.isInteger(pid) || pid < 1) return false;
135
- try {
136
- mkdirSync(dshHome, { recursive: true });
137
- const temp = `${ownershipFile}.${process.pid}.${Date.now()}.tmp`;
138
- writeFileSync(temp, JSON.stringify({
139
- schema_version: 1,
140
- pid,
141
- profile: 'dsh-crew',
142
- execution_plane: 'hub-3210',
143
- port: Number(bridgePort),
144
- runtime_id: typeof identity?.runtime_id === 'string' ? identity.runtime_id : null,
145
- }) + '\n');
146
- renameSync(temp, ownershipFile);
147
- return true;
148
- } catch { return false; }
149
- }
150
-
151
- function adoptedChild(pid) {
152
- return {
153
- pid,
154
- killed: false,
155
- exitCode: null,
156
- kill() {
157
- try {
158
- killImpl(pid);
159
- this.killed = true;
160
- this.exitCode = 0;
161
- clearOwnership(pid);
162
- return true;
163
- } catch { return false; }
164
- },
165
- unref() {},
166
- };
167
- }
168
-
169
- async function adoptPersistedChild() {
170
- if (runningChild && runningChild.killed !== true && runningChild.exitCode == null) return true;
171
- const record = readOwnership();
172
- if (!record || !(await healthCheck())) {
173
- if (record) clearOwnership(record.pid);
174
- return false;
175
- }
176
- const identity = await runtimeIdentity();
177
- if (!identity || (record.runtime_id && identity.runtime_id !== record.runtime_id)) {
178
- clearOwnership(record.pid);
179
- return false;
180
- }
181
- runningChild = adoptedChild(record.pid);
182
- return true;
183
- }
184
-
185
- async function start() {
186
- const childAlive = runningChild && runningChild.killed !== true && runningChild.exitCode == null;
187
- if (await healthCheck()) {
188
- if (childAlive) return { ok: true, started: false, owned: true };
189
- if (await adoptPersistedChild()) return { ok: true, started: false, adopted: true, owned: true };
190
- return { ok: false, code: 'PORT_OWNERSHIP_CONFLICT' };
191
- }
192
- if (!exists(runtime)) return { ok: false, code: 'CREW_RUNTIME_NOT_INSTALLED' };
193
- if (!childAlive) {
110
+ const dshHome = crewDshHome({ home });
111
+ const bridgePort = new URL(bridgeTarget).port;
112
+
113
+ function readOwnership() {
114
+ try {
115
+ const record = JSON.parse(readFileSync(ownershipFile, 'utf8'));
116
+ if (record?.schema_version !== 1
117
+ || !Number.isInteger(record.pid) || record.pid < 1
118
+ || record.profile !== 'dsh-crew'
119
+ || record.execution_plane !== 'hub-3210'
120
+ || Number(record.port) !== Number(bridgePort)) return null;
121
+ return record;
122
+ } catch { return null; }
123
+ }
124
+
125
+ function clearOwnership(expectedPid) {
126
+ try {
127
+ const current = readOwnership();
128
+ if (expectedPid !== undefined && current?.pid !== expectedPid) return;
129
+ rmSync(ownershipFile, { force: true });
130
+ } catch {}
131
+ }
132
+
133
+ function persistOwnership(pid, identity = null, { state = 'owned' } = {}) {
134
+ if (!Number.isInteger(pid) || pid < 1) return false;
135
+ if (state === 'owned' && !(typeof identity?.runtime_id === 'string' && identity.runtime_id.length > 0)) return false;
136
+ try {
137
+ mkdirSync(dshHome, { recursive: true });
138
+ const temp = `${ownershipFile}.${process.pid}.${Date.now()}.tmp`;
139
+ writeFileSync(temp, JSON.stringify({
140
+ schema_version: 1,
141
+ state,
142
+ pid,
143
+ profile: 'dsh-crew',
144
+ execution_plane: 'hub-3210',
145
+ port: Number(bridgePort),
146
+ runtime_id: typeof identity?.runtime_id === 'string' ? identity.runtime_id : null,
147
+ }) + '\n');
148
+ renameSync(temp, ownershipFile);
149
+ return true;
150
+ } catch { return false; }
151
+ }
152
+
153
+ function adoptedChild(pid) {
154
+ return {
155
+ pid,
156
+ killed: false,
157
+ exitCode: null,
158
+ kill() {
159
+ try {
160
+ killImpl(pid);
161
+ this.killed = true;
162
+ this.exitCode = 0;
163
+ clearOwnership(pid);
164
+ return true;
165
+ } catch { return false; }
166
+ },
167
+ unref() {},
168
+ };
169
+ }
170
+
171
+ async function adoptPersistedChild() {
172
+ if (runningChild && runningChild.killed !== true && runningChild.exitCode == null) return true;
173
+ const record = readOwnership();
174
+ if (!record || !(await healthCheck())) {
175
+ if (record) clearOwnership(record.pid);
176
+ return false;
177
+ }
178
+ // Kill authority requires a persisted non-empty runtime_id AND an exact
179
+ // match with the live identity. A record without runtime_id (crash
180
+ // between spawn and identity persistence, or PID reuse afterwards)
181
+ // can never authorize a kill.
182
+ if (typeof record.runtime_id !== 'string' || record.runtime_id.length === 0) {
183
+ clearOwnership(record.pid);
184
+ return false;
185
+ }
186
+ const identity = await runtimeIdentity();
187
+ if (!identity || identity.runtime_id !== record.runtime_id) {
188
+ clearOwnership(record.pid);
189
+ return false;
190
+ }
191
+ runningChild = adoptedChild(record.pid);
192
+ return true;
193
+ }
194
+
195
+ async function start() {
196
+ const childAlive = runningChild && runningChild.killed !== true && runningChild.exitCode == null;
197
+ if (await healthCheck()) {
198
+ if (childAlive) return { ok: true, started: false, owned: true };
199
+ if (await adoptPersistedChild()) return { ok: true, started: false, adopted: true, owned: true };
200
+ return { ok: false, code: 'PORT_OWNERSHIP_CONFLICT' };
201
+ }
202
+ if (!exists(runtime)) return { ok: false, code: 'CREW_RUNTIME_NOT_INSTALLED' };
203
+ if (!childAlive) {
194
204
  runningChild = spawnImpl(process.execPath, [
195
205
  runtime, '--profile', 'dsh-crew', '--host', '127.0.0.1', '--port', bridgePort,
196
206
  ], {
@@ -200,84 +210,142 @@ export function createCrewSidecarSupervisor({
200
210
  stdio: 'ignore',
201
211
  windowsHide: true,
202
212
  });
203
- const ownedChild = runningChild;
204
- if (Number.isInteger(ownedChild?.pid) && !persistOwnership(ownedChild.pid)) {
205
- try { ownedChild.kill?.(); } catch {}
206
- runningChild = null;
207
- return { ok: false, code: 'SUPERVISOR_OWNERSHIP_PERSIST_FAILED' };
208
- }
209
- ownedChild.once?.('exit', () => { if (runningChild === ownedChild) runningChild = null; });
213
+ const ownedChild = runningChild;
214
+ if (Number.isInteger(ownedChild?.pid) && !persistOwnership(ownedChild.pid, null, { state: 'starting' })) {
215
+ try { ownedChild.kill?.(); } catch {}
216
+ runningChild = null;
217
+ return { ok: false, code: 'SUPERVISOR_OWNERSHIP_PERSIST_FAILED' };
218
+ }
219
+ ownedChild.once?.('exit', () => { if (runningChild === ownedChild) runningChild = null; });
210
220
  ownedChild.unref?.();
211
221
  }
212
222
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
213
- if (await healthCheck()) {
214
- if (Number.isInteger(runningChild?.pid)) {
215
- const identity = await runtimeIdentity();
216
- if (!identity) {
217
- try { runningChild?.kill?.(); } catch {}
218
- runningChild = null;
219
- return { ok: false, code: 'SUPERVISOR_RUNTIME_IDENTITY_UNAVAILABLE' };
220
- }
221
- if (!persistOwnership(runningChild.pid, identity)) {
222
- try { runningChild.kill?.(); } catch {}
223
- runningChild = null;
224
- return { ok: false, code: 'SUPERVISOR_OWNERSHIP_PERSIST_FAILED' };
225
- }
226
- }
227
- return { ok: true, started: true, owned: true };
228
- }
229
- await wait(pollInterval);
223
+ if (await healthCheck()) {
224
+ if (Number.isInteger(runningChild?.pid)) {
225
+ const identity = await runtimeIdentity();
226
+ if (!identity) {
227
+ try { runningChild?.kill?.(); } catch {}
228
+ runningChild = null;
229
+ return { ok: false, code: 'SUPERVISOR_RUNTIME_IDENTITY_UNAVAILABLE' };
230
+ }
231
+ if (!persistOwnership(runningChild.pid, identity)) {
232
+ try { runningChild.kill?.(); } catch {}
233
+ runningChild = null;
234
+ return { ok: false, code: 'SUPERVISOR_OWNERSHIP_PERSIST_FAILED' };
235
+ }
236
+ }
237
+ return { ok: true, started: true, owned: true };
238
+ }
239
+ await wait(pollInterval);
230
240
  }
231
241
  return { ok: false, code: 'CREW_BACKEND_START_TIMEOUT' };
232
- }
233
-
234
- async function restartOwnedBackend() {
235
- let owned = runningChild;
236
- let childAlive = owned && owned.killed !== true && owned.exitCode == null;
237
- if (!childAlive && await healthCheck()) {
238
- if (await adoptPersistedChild()) {
239
- owned = runningChild;
240
- childAlive = true;
241
- } else {
242
- return { ok: false, code: 'PORT_OWNERSHIP_CONFLICT' };
243
- }
244
- }
245
- if (!childAlive) {
246
- const started = await start();
247
- return started.ok ? { ...started, restarted: started.started === true } : started;
248
- }
249
- if (typeof owned.kill !== 'function' || owned.kill() !== true) {
250
- return { ok: false, code: 'CREW_BACKEND_RESTART_UNAVAILABLE' };
251
- }
252
- for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
253
- if (!await healthCheck()) break;
254
- await wait(pollInterval);
255
- }
256
- if (await healthCheck()) return { ok: false, code: 'CREW_BACKEND_STOP_TIMEOUT' };
257
- const started = await start();
258
- return started.ok ? { ...started, restarted: true } : started;
259
- }
260
-
261
- return {
262
- execution_plane: 'hub-3210',
263
- profile: 'dsh-crew',
264
- listen_port: Number(bridgePort),
265
- ensure() {
266
- if (!starting) starting = start().finally(() => { starting = null; });
267
- return starting;
268
- },
269
- restartOwnedBackend() {
270
- if (!restarting) restarting = restartOwnedBackend().finally(() => { restarting = null; });
271
- return restarting;
272
- },
273
- };
274
- }
242
+ }
243
+
244
+ async function restartOwnedBackend() {
245
+ let owned = runningChild;
246
+ let childAlive = owned && owned.killed !== true && owned.exitCode == null;
247
+ if (!childAlive && await healthCheck()) {
248
+ if (await adoptPersistedChild()) {
249
+ owned = runningChild;
250
+ childAlive = true;
251
+ } else {
252
+ return { ok: false, code: 'PORT_OWNERSHIP_CONFLICT' };
253
+ }
254
+ }
255
+ if (!childAlive) {
256
+ const started = await start();
257
+ return started.ok ? { ...started, restarted: started.started === true } : started;
258
+ }
259
+ if (typeof owned.kill !== 'function' || owned.kill() !== true) {
260
+ return { ok: false, code: 'CREW_BACKEND_RESTART_UNAVAILABLE' };
261
+ }
262
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
263
+ if (!await healthCheck()) break;
264
+ await wait(pollInterval);
265
+ }
266
+ if (await healthCheck()) return { ok: false, code: 'CREW_BACKEND_STOP_TIMEOUT' };
267
+ const started = await start();
268
+ return started.ok ? { ...started, restarted: true } : started;
269
+ }
275
270
 
276
- const processSupervisor = createCrewSidecarSupervisor();
271
+ // Stop-only primitive for cohort migration: kills the owned 3210 child
272
+ // (PID-identity verified) and waits until health disappears. Never
273
+ // restarts: the caller swaps the runtime tree first, then calls
274
+ // startOwnedBackend. Returns the stopped PID for audit.
275
+ async function stopOwnedBackend() {
276
+ let owned = runningChild;
277
+ let childAlive = owned && owned.killed !== true && owned.exitCode == null;
278
+ if (!childAlive && await healthCheck()) {
279
+ if (await adoptPersistedChild()) {
280
+ owned = runningChild;
281
+ childAlive = true;
282
+ } else {
283
+ return { ok: false, code: 'PORT_OWNERSHIP_CONFLICT' };
284
+ }
285
+ }
286
+ if (!childAlive) return { ok: true, stopped: false, pid: null };
287
+ const pid = owned.pid;
288
+ if (typeof owned.kill !== 'function' || owned.kill() !== true) {
289
+ return { ok: false, code: 'CREW_BACKEND_STOP_UNAVAILABLE' };
290
+ }
291
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
292
+ if (!await healthCheck()) return { ok: true, stopped: true, pid };
293
+ await wait(pollInterval);
294
+ }
295
+ return { ok: false, code: 'CREW_BACKEND_STOP_TIMEOUT' };
296
+ }
297
+
298
+ // Start-only primitive for cohort migration: boots the Crew-owned 3210
299
+ // from the CURRENT live runtime tree and waits for health + identity.
300
+ // Never stops anything first: the caller stops before swapping.
301
+ async function startOwnedBackend() {
302
+ const started = await start();
303
+ return started.ok ? { ...started, started: true } : started;
304
+ }
305
+
306
+ return {
307
+ execution_plane: 'hub-3210',
308
+ profile: 'dsh-crew',
309
+ listen_port: Number(bridgePort),
310
+ ensure() {
311
+ if (!starting) starting = start().finally(() => { starting = null; });
312
+ return starting;
313
+ },
314
+ restartOwnedBackend() {
315
+ if (!restarting) restarting = restartOwnedBackend().finally(() => { restarting = null; });
316
+ return restarting;
317
+ },
318
+ stopOwnedBackend,
319
+ startOwnedBackend,
320
+ };
321
+ }
322
+
323
+ // NOTE: createCrewSidecarSupervisor (above) remains exported for unit
324
+ // tests, but NO production path may spawn/kill the 3210 through it. The
325
+ // Windows launcher supervisor (start-dsh-crew.ps1 watch mode) is the only
326
+ // process authority; npx lifecycle reaches it through the durable
327
+ // maintenance-request protocol (maintenance-stop/maintenance-start).
328
+
329
+ // The 3080 quick surface must NEVER spawn or own the 3210 process: recovery
330
+ // authority belongs exclusively to the Windows launcher supervisor. The
331
+ // default backend gate therefore only PROBES 3210 health and fails closed.
332
+ async function probeCrewBackend(fetchImpl = globalThis.fetch, bridgeTarget = resolveCrewBridgeTarget()) {
333
+ try {
334
+ const response = await fetchImpl(`${bridgeTarget}${CREW_BRIDGE_PREFIX}/runtime`, {
335
+ headers: { accept: 'application/json' },
336
+ signal: AbortSignal.timeout(3_000),
337
+ });
338
+ if (!response.ok) return { ok: false, code: 'CREW_BACKEND_UNREACHABLE' };
339
+ const body = await response.json();
340
+ return body?.ok === true ? { ok: true } : { ok: false, code: 'CREW_BACKEND_UNREADY' };
341
+ } catch (error) {
342
+ return { ok: false, code: 'CREW_BACKEND_UNREACHABLE', error: String(error?.message ?? error) };
343
+ }
344
+ }
277
345
 
278
346
  export async function proxyCrewRequest(req, res, {
279
347
  fetchImpl = globalThis.fetch,
280
- ensureBackend = () => processSupervisor.ensure(),
348
+ ensureBackend = () => probeCrewBackend(fetchImpl, resolveCrewBridgeTarget()),
281
349
  bridgeTarget = resolveCrewBridgeTarget(),
282
350
  } = {}) {
283
351
  if (!isTrustedLocalRequest(req)) {
@@ -293,29 +361,29 @@ export async function proxyCrewRequest(req, res, {
293
361
  try {
294
362
  const backend = await ensureBackend();
295
363
  if (backend?.ok === false) throw new Error('backend unavailable');
296
- const method = String(req.method ?? 'GET').toUpperCase();
297
- const bodyBuffer = method === 'GET' || method === 'HEAD' ? null : await readBoundedBody(req);
298
- const requestHeaders = safeHeaders(req.headers);
299
- for (const name of Object.keys(requestHeaders)) {
300
- if (name.startsWith('x-dsh-crew-')) delete requestHeaders[name];
301
- }
302
- requestHeaders['x-dsh-crew-bridge'] = '3080-to-3210';
303
- requestHeaders['x-dsh-crew-execution-plane'] = 'hub-3210';
304
- requestHeaders['x-dsh-crew-ingress'] = 'official-3080';
305
- const response = await fetchImpl(`${bridgeTarget}${req.url}`, {
306
- method,
307
- headers: requestHeaders,
364
+ const method = String(req.method ?? 'GET').toUpperCase();
365
+ const bodyBuffer = method === 'GET' || method === 'HEAD' ? null : await readBoundedBody(req);
366
+ const requestHeaders = safeHeaders(req.headers);
367
+ for (const name of Object.keys(requestHeaders)) {
368
+ if (name.startsWith('x-dsh-crew-')) delete requestHeaders[name];
369
+ }
370
+ requestHeaders['x-dsh-crew-bridge'] = '3080-to-3210';
371
+ requestHeaders['x-dsh-crew-execution-plane'] = 'hub-3210';
372
+ requestHeaders['x-dsh-crew-ingress'] = 'official-3080';
373
+ const response = await fetchImpl(`${bridgeTarget}${req.url}`, {
374
+ method,
375
+ headers: requestHeaders,
308
376
  body: bodyBuffer === null ? undefined : new Blob([bodyBuffer]),
309
377
  signal: AbortSignal.timeout(120_000),
310
378
  });
311
- const responseBody = Buffer.from(await response.arrayBuffer());
312
- const headers = safeHeaders(Object.fromEntries(response.headers.entries()));
313
- for (const name of Object.keys(headers)) {
314
- if (name.startsWith('x-dsh-crew-')) delete headers[name];
315
- }
316
- headers['content-length'] = String(responseBody.length);
317
- headers['x-dsh-crew-bridge'] = '3080-to-3210';
318
- headers['x-dsh-crew-ingress'] = 'official-3080';
379
+ const responseBody = Buffer.from(await response.arrayBuffer());
380
+ const headers = safeHeaders(Object.fromEntries(response.headers.entries()));
381
+ for (const name of Object.keys(headers)) {
382
+ if (name.startsWith('x-dsh-crew-')) delete headers[name];
383
+ }
384
+ headers['content-length'] = String(responseBody.length);
385
+ headers['x-dsh-crew-bridge'] = '3080-to-3210';
386
+ headers['x-dsh-crew-ingress'] = 'official-3080';
319
387
  res.writeHead(response.status, headers);
320
388
  res.end(responseBody);
321
389
  } catch (error) {
@@ -324,45 +392,57 @@ export async function proxyCrewRequest(req, res, {
324
392
  }
325
393
  }
326
394
 
327
- export function registerOfficialWebBridge(ctx, options = {}) {
328
- return ctx.inject(['webServer'], (webCtx) => {
329
- const supervisor = options.supervisor ?? processSupervisor;
330
- const disposeStatus = webCtx.webServer.register({
395
+ export function registerOfficialWebBridge(ctx, options = {}) {
396
+ return ctx.inject(['webServer'], (webCtx) => {
397
+ const disposeStatus = webCtx.webServer.register({
331
398
  kind: 'exact',
332
399
  path: `${CREW_BRIDGE_PREFIX}/bridge-status`,
333
400
  handler: (req, res) => {
334
401
  if (!isTrustedLocalRequest(req)) return sendJson(res, 403, { ok: false, code: 'LOCAL_SAME_ORIGIN_ONLY' });
335
402
  return sendJson(res, 200, {
336
403
  ok: true,
337
- mode: 'official-3080-isolated-3210',
338
- surface: 'official-bridge',
339
- ui_role: 'control-plane',
340
- execution_plane: 'hub-3210',
341
- listen_port: 3210,
342
- });
404
+ mode: 'official-3080-quick-controls',
405
+ surface: 'official-bridge',
406
+ ui_role: 'quick-controls',
407
+ execution_plane: 'hub-3210',
408
+ listen_port: 3210,
409
+ full_control_plane_url: 'http://127.0.0.1:3210/',
410
+ });
411
+ },
412
+ });
413
+ // The legacy supervisor endpoint moved to the durable 3210 restart-
414
+ // request channel executed by the Windows launcher. Report 410 Gone so
415
+ // stale callers learn the new contract instead of failing opaquely.
416
+ const disposeRestart = webCtx.webServer.register({
417
+ kind: 'exact',
418
+ path: `${CREW_BRIDGE_PREFIX}/supervisor/restart`,
419
+ handler: (req, res) => {
420
+ if (!isTrustedLocalRequest(req)) return sendJson(res, 403, { ok: false, code: 'LOCAL_SAME_ORIGIN_ONLY' });
421
+ return sendJson(res, 410, { ok: false, code: 'SUPERVISOR_ENDPOINT_MOVED', control_plane: 'http://127.0.0.1:3210/' });
343
422
  },
344
- });
345
- const disposeRestart = webCtx.webServer.register({
346
- kind: 'exact',
347
- path: `${CREW_BRIDGE_PREFIX}/supervisor/restart`,
348
- handler: async (req, res) => {
349
- if (!isTrustedLocalRequest(req)) return sendJson(res, 403, { ok: false, code: 'LOCAL_SAME_ORIGIN_ONLY' });
350
- if (String(req.method ?? 'GET').toUpperCase() !== 'POST') return sendJson(res, 405, { ok: false, code: 'POST_ONLY' });
351
- let body;
352
- try { body = JSON.parse((await readBoundedBody(req, 8 * 1024)).toString('utf8') || '{}'); } catch { return sendJson(res, 400, { ok: false, code: 'SUPERVISOR_REQUEST_INVALID' }); }
353
- if (body?.confirm !== true) return sendJson(res, 400, { ok: false, code: 'SUPERVISOR_RESTART_CONFIRM_REQUIRED' });
354
- const result = await supervisor.restartOwnedBackend();
355
- return sendJson(res, result?.ok === true ? 200 : 409, { ...result });
356
- },
357
- });
358
- const disposeProxy = webCtx.webServer.register({
359
- kind: 'prefix',
360
- path: CREW_BRIDGE_PREFIX,
361
- handler: (req, res) => proxyCrewRequest(req, res, options),
362
423
  });
363
- return () => { disposeProxy?.(); disposeRestart?.(); disposeStatus?.(); };
364
- });
365
- }
424
+ // Least-privilege allowlist: the 3080 quick-controls surface may reach
425
+ // ONLY the narrow quick endpoints on 3210. Full Crew API (providers,
426
+ // credentials, install, workspaces, migration, rollback, quarantine)
427
+ // is no longer proxied here — the native 3210 page is the full control
428
+ // plane.
429
+ const QUICK_ALLOWLIST = new Set([
430
+ '/quick-config',
431
+ '/quick-status',
432
+ '/runtime/restart-request',
433
+ '/runtime/restart-status',
434
+ ]);
435
+ const disposers = [];
436
+ for (const path of QUICK_ALLOWLIST) {
437
+ disposers.push(webCtx.webServer.register({
438
+ kind: 'exact',
439
+ path: `${CREW_BRIDGE_PREFIX}${path}`,
440
+ handler: (req, res) => proxyCrewRequest(req, res, options),
441
+ }));
442
+ }
443
+ return () => { for (const dispose of disposers) dispose?.(); disposeRestart?.(); disposeStatus?.(); };
444
+ });
445
+ }
366
446
 
367
447
  export async function apply(ctx) {
368
448
  registerOfficialWebBridge(ctx);