@ckanner/dsh-plugin-flywheel 0.28.0 → 0.28.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.
@@ -38,7 +38,7 @@ export function machineRequest(value, api, env) {
38
38
  const keys = new Set(['type', 'protocol', 'requestId', 'cwd', 'model', 'reasoningEffort', 'permissionMode', 'prompt', 'timeoutMs']);
39
39
  if (value === null || typeof value !== 'object' || Array.isArray(value) || value.type !== 'start' || value.protocol !== 1 ||
40
40
  Object.keys(value).some(key => !keys.has(key)) || typeof value.requestId !== 'string' || value.requestId.length > 256 ||
41
- !['read-only', 'workspace-write'].includes(value.permissionMode)) {
41
+ !['read-only', 'workspace-write', 'danger-full-access'].includes(value.permissionMode)) {
42
42
  throw new WorkerError('INVALID_REQUEST', 'Expected one protocol=1 start request with an explicit supported permission mode');
43
43
  }
44
44
  if (value.timeoutMs !== undefined && (!Number.isSafeInteger(value.timeoutMs) || value.timeoutMs < 1 || value.timeoutMs > 2147483647)) {
@@ -46,7 +46,9 @@ export function machineRequest(value, api, env) {
46
46
  }
47
47
  try {
48
48
  return api.parseMachineRequest({ protocol: 1, requestId: value.requestId, cwd: value.cwd, model: value.model,
49
- effort: value.reasoningEffort, permissionMode: value.permissionMode, prompt: value.prompt,
49
+ effort: value.reasoningEffort,
50
+ permissionMode: value.permissionMode === 'danger-full-access' ? 'dangerously-bypass-approvals-and-sandbox' : value.permissionMode,
51
+ prompt: value.prompt,
50
52
  ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }),
51
53
  command: env.FLYWHEEL_CODEX_BIN || 'codex', threadMode: 'ephemeral', maxOutputBytes: LIMITS.machineOutputBytes });
52
54
  } catch (error) {
package/dist/control.js CHANGED
@@ -4,6 +4,15 @@ import { directory, nonempty, record } from './protocol.js';
4
4
  const actions = ['start', 'status', 'stop', 'config', 'doctor'];
5
5
  const checkStatuses = ['pass', 'fail', 'warn', 'unknown', 'skipped'];
6
6
 
7
+ // This marker is issued only where the host can prove that no control process
8
+ // started. An engine/transport error after spawn must never acquire it.
9
+ export class ControlNotDispatchedError extends Error {
10
+ constructor(cause) {
11
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
12
+ this.name = 'ControlNotDispatchedError';
13
+ }
14
+ }
15
+
7
16
  export async function validateControl(input, defaultCwd) {
8
17
  if (!record(input) || !actions.includes(input.action)) {
9
18
  throw new Error('action must be start, status, stop, config, or doctor');
@@ -46,13 +55,18 @@ function validDiagnostic(value, request) {
46
55
 
47
56
  /** Only the engine executable can mutate engine state. No shell interpolation. */
48
57
  export async function runControl(input, { binary = 'flywheel', defaultCwd, signal, timeoutMs = 60000 } = {}) {
49
- nonempty(binary, 'flywheelBinary', 4096);
50
- if (binary.startsWith('-')) throw new Error('flywheelBinary must name an executable');
51
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 120000) {
52
- throw new Error('controlTimeoutMs must be between 100 and 120000');
58
+ let request;
59
+ try {
60
+ nonempty(binary, 'flywheelBinary', 4096);
61
+ if (binary.startsWith('-')) throw new Error('flywheelBinary must name an executable');
62
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 120000) {
63
+ throw new Error('controlTimeoutMs must be between 100 and 120000');
64
+ }
65
+ request = await validateControl(input, defaultCwd);
66
+ if (signal?.aborted) throw new Error('Flywheel control request cancelled');
67
+ } catch (error) {
68
+ throw new ControlNotDispatchedError(error);
53
69
  }
54
- const request = await validateControl(input, defaultCwd);
55
- if (signal?.aborted) throw new Error('Flywheel control request cancelled');
56
70
  // Diagnostic commands own watchdog child groups. Its TERM grace, pipe
57
71
  // WaitDelay, and quiescence confirmation may each take 5s; keep the Go owner
58
72
  // alive long enough to finish all three instead of orphaning its workers.
@@ -61,9 +75,17 @@ export async function runControl(input, { binary = 'flywheel', defaultCwd, signa
61
75
  if (request.taskDir) args.push('--task-dir', request.taskDir);
62
76
  if (request.probe) args.push('--probe');
63
77
  return new Promise((resolve, reject) => {
64
- const child = spawn(binary, args, {
65
- cwd: request.cwd, detached: false, stdio: ['ignore', 'pipe', 'pipe'],
66
- });
78
+ let child;
79
+ try {
80
+ child = spawn(binary, args, {
81
+ cwd: request.cwd, detached: false, stdio: ['ignore', 'pipe', 'pipe'],
82
+ });
83
+ } catch (error) {
84
+ reject(new ControlNotDispatchedError(error));
85
+ return;
86
+ }
87
+ let spawned = false;
88
+ child.once('spawn', () => { spawned = true; });
67
89
  let stdout = '';
68
90
  let stderr = '';
69
91
  let failure;
@@ -88,7 +110,11 @@ export async function runControl(input, { binary = 'flywheel', defaultCwd, signa
88
110
  if (Buffer.byteLength(stdout) > 1024 * 1024) stop(new Error('Flywheel control response exceeds 1 MiB'));
89
111
  });
90
112
  child.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-8192); });
91
- child.on('error', error => { failure = error; });
113
+ child.on('error', error => {
114
+ // Node emits spawn before other events after successful process creation.
115
+ // Require both the missing spawn event and missing PID for positive proof.
116
+ failure = !spawned && child.pid === undefined ? new ControlNotDispatchedError(error) : error;
117
+ });
92
118
  child.on('close', (code, childSignal) => {
93
119
  clearTimeout(timer);
94
120
  clearTimeout(killTimer);
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { runControl } from './control.js';
1
+ import { ControlNotDispatchedError, runControl } from './control.js';
2
2
  import { resolveFlywheelBinary } from './installation.js';
3
3
 
4
4
  export const inject = ['commands', 'tools'];
@@ -33,11 +33,25 @@ export function apply(ctx, config = {}) {
33
33
  description: '通过 Go 引擎控制 Flywheel 任务、查看实际模型配置或执行无推理环境检查。',
34
34
  input: { hint: '{"action":"doctor"} 或 {"action":"status","taskDir":"/absolute/docs/task"}' },
35
35
  async handler({ rawInput, agent, signal }) {
36
+ let input;
36
37
  try {
37
- const value = await control(JSON.parse(rawInput), agent, signal);
38
+ try {
39
+ input = JSON.parse(rawInput);
40
+ } catch (error) {
41
+ throw new ControlNotDispatchedError(error);
42
+ }
43
+ const value = await control(input, agent, signal);
38
44
  return { kind: 'success', text: JSON.stringify(value, null, 2) };
39
45
  } catch (error) {
40
- return { kind: 'error', text: error.message };
46
+ // CommandResult preserves only kind/text. Wrap every failure, including
47
+ // unknown outcomes, so a child error's text cannot impersonate host proof.
48
+ return { kind: 'error', text: JSON.stringify({
49
+ protocol: 1,
50
+ type: 'flywheel-control-error',
51
+ action: typeof input?.action === 'string' ? input.action : null,
52
+ dispatch: error instanceof ControlNotDispatchedError ? 'not-dispatched' : 'unknown',
53
+ message: error instanceof Error ? error.message : String(error),
54
+ }, null, 2) };
41
55
  }
42
56
  },
43
57
  });
package/dist/policy.js CHANGED
@@ -4,20 +4,22 @@ export const inject = ['sandboxPolicy', 'approval', 'shell'];
4
4
 
5
5
  export async function apply(ctx, config) {
6
6
  const { permissionMode, cwd } = config;
7
- if (!['read-only', 'workspace-write'].includes(permissionMode) || typeof cwd !== 'string' ||
7
+ const fullAccess = permissionMode === 'danger-full-access';
8
+ if (!['read-only', 'workspace-write', 'danger-full-access'].includes(permissionMode) || typeof cwd !== 'string' ||
8
9
  ctx.shell.sandboxMode === undefined) {
9
- throw new Error('Flywheel requires a confined DSH shell and explicit workspace policy');
10
+ throw new Error('Flywheel requires a sandbox-capable DSH shell and explicit permission policy');
10
11
  }
11
- // This local no-op proves that the actual runner starts and reports full
12
- // enforcement. Merely selecting a policy value is not readiness evidence.
12
+ // Probe the official runner. Restricted modes must prove full enforcement;
13
+ // full access must truthfully report that no confinement was applied.
13
14
  const probe = await ctx.shell.execute(ctx.shell.resolve({
14
15
  command: ':', workdir: cwd, timeoutMs: 5000,
15
16
  sandboxPolicy: { mode: permissionMode, workspaceRoot: cwd },
16
17
  }));
17
18
  const result = await probe.result();
18
19
  if (result.exitCode !== 0 || result.timedOut || result.aborted ||
19
- result.sandbox?.mode !== permissionMode || result.sandbox?.enforcement !== 'full') {
20
- throw new Error('Flywheel sandbox readiness probe did not prove full enforcement');
20
+ result.sandbox?.mode !== permissionMode ||
21
+ (fullAccess ? result.sandbox.denied !== false || result.sandbox.enforcement !== undefined : result.sandbox.enforcement !== 'full')) {
22
+ throw new Error(fullAccess ? 'Flywheel readiness probe did not confirm explicit full access' : 'Flywheel sandbox readiness probe did not prove full enforcement');
21
23
  }
22
24
  ctx.on('session/created', session => {
23
25
  // A fresh SDK session has no inherited permission facts. Pin both canonical
@@ -30,8 +32,8 @@ export async function apply(ctx, config) {
30
32
  cwd: policy.workspaceRoot,
31
33
  permissionMode: policy.mode,
32
34
  approvalPolicy: ctx.approval.effectivePolicy(session),
33
- confinedShell: ctx.shell.sandboxMode !== undefined,
34
- enforcement: result.sandbox.enforcement,
35
+ confinedShell: !fullAccess,
36
+ ...(fullAccess ? {} : { enforcement: result.sandbox.enforcement }),
35
37
  });
36
38
  });
37
39
  }
package/dist/protocol.js CHANGED
@@ -49,8 +49,8 @@ export async function validateStart(value) {
49
49
  const provider = nonempty(value.provider, 'provider');
50
50
  const model = nonempty(value.model, 'model');
51
51
  const prompt = nonempty(value.prompt, 'prompt', MAX_FRAME_BYTES);
52
- if (!['read-only', 'workspace-write'].includes(value.permissionMode)) {
53
- throw new BridgeError('INVALID_REQUEST', 'permissionMode must be read-only or workspace-write');
52
+ if (!['read-only', 'workspace-write', 'danger-full-access'].includes(value.permissionMode)) {
53
+ throw new BridgeError('INVALID_REQUEST', 'permissionMode must be read-only, workspace-write or danger-full-access');
54
54
  }
55
55
  if (value.reasoningEffort !== undefined) nonempty(value.reasoningEffort, 'reasoningEffort', 64);
56
56
  if (value.maxTokens !== undefined && (!Number.isSafeInteger(value.maxTokens) || value.maxTokens < 1)) {
@@ -151,10 +151,12 @@ export class SessionFold {
151
151
 
152
152
  verifiedPolicy(request) {
153
153
  const policy = this.policy;
154
+ const fullAccess = request.permissionMode === 'danger-full-access';
154
155
  if (!policy || policy.protocol !== PROTOCOL || policy.permissionMode !== request.permissionMode ||
155
156
  policy.cwd !== request.cwd || policy.approvalPolicy !== 'never' ||
156
- policy.confinedShell !== true || policy.enforcement !== 'full') {
157
- throw new BridgeError('INVALID_REQUEST', 'DSH did not attest the requested cwd and confined permission policy; prompt withheld');
157
+ policy.confinedShell !== !fullAccess ||
158
+ (fullAccess ? policy.enforcement !== undefined : policy.enforcement !== 'full')) {
159
+ throw new BridgeError('INVALID_REQUEST', 'DSH did not attest the requested cwd and permission policy; prompt withheld');
158
160
  }
159
161
  }
160
162
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckanner/dsh-plugin-flywheel",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
4
4
  "description": "Flywheel engine controls with owned DSH SDK and Codex Pro machine workers",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -25,7 +25,8 @@
25
25
  "cordis.patch.yml",
26
26
  "README.md",
27
27
  "README.en.md",
28
- "PROTOCOL.md"
28
+ "PROTOCOL.md",
29
+ "THIRD_PARTY_NOTICES.md"
29
30
  ],
30
31
  "dsh": {
31
32
  "bundle": {
@@ -33,9 +34,10 @@
33
34
  },
34
35
  "client": {
35
36
  "inject": [
36
- "@deepseek-ai/dsh-client-ui-chat",
37
+ "@deepseek-ai/dsh-client-ui-conversation",
37
38
  "@deepseek-ai/dsh-client-ui-commands",
38
- "@deepseek-ai/dsh-api-remotes"
39
+ "@deepseek-ai/dsh-api-remotes",
40
+ "@deepseek-ai/dsh-client-locale"
39
41
  ],
40
42
  "platform": "web"
41
43
  }
@@ -66,7 +68,11 @@
66
68
  "dsh-subagent-codex-pro": "0.8.0"
67
69
  },
68
70
  "devDependencies": {
71
+ "@deepseek-ai/cordis": "4.0.5",
72
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
73
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.5",
69
74
  "jsdom": "26.1.0",
75
+ "lightningcss": "1.33.0",
70
76
  "react": "18.3.1",
71
77
  "react-dom": "18.3.1"
72
78
  },