@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.
- package/PROTOCOL.md +33 -6
- package/README.en.md +134 -13
- package/README.md +38 -10
- package/THIRD_PARTY_NOTICES.md +38 -0
- package/dist/client.js +721 -172
- package/dist/codex-pro-bridge.js +4 -2
- package/dist/control.js +36 -10
- package/dist/index.js +17 -3
- package/dist/policy.js +10 -8
- package/dist/protocol.js +6 -4
- package/package.json +10 -4
package/dist/codex-pro-bridge.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
throw new Error('
|
|
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
|
-
|
|
65
|
-
|
|
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 => {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
10
|
+
throw new Error('Flywheel requires a sandbox-capable DSH shell and explicit permission policy');
|
|
10
11
|
}
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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 ||
|
|
20
|
-
|
|
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:
|
|
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
|
|
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 !==
|
|
157
|
-
|
|
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.
|
|
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-
|
|
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
|
},
|