@ckanner/flywheel 0.32.1 → 0.33.1

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 (134) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +5 -0
  4. package/THIRD_PARTY_NOTICES.md +42 -0
  5. package/bin/runtime.js +25 -31
  6. package/cordis.patch.yml +5 -0
  7. package/dist/bridge.mjs +421 -0
  8. package/dist/claude-code-pro-bridge.mjs +82 -0
  9. package/dist/claude-code-pro-worker.mjs +5 -0
  10. package/dist/client.js +4140 -0
  11. package/dist/codex-pro-bridge.mjs +75 -0
  12. package/dist/codex-pro-worker.mjs +5 -0
  13. package/dist/console-read.mjs +51 -0
  14. package/dist/control.mjs +218 -0
  15. package/dist/index.mjs +185 -0
  16. package/dist/installation.mjs +56 -0
  17. package/dist/machine-worker.mjs +241 -0
  18. package/dist/policy.mjs +91 -0
  19. package/dist/prewarm.mjs +136 -0
  20. package/dist/protocol.mjs +260 -0
  21. package/dist/task-association.mjs +127 -0
  22. package/dist/worker.mjs +52 -0
  23. package/dist/workspaces.mjs +42 -0
  24. package/docs/README.md +1 -1
  25. package/docs/README.zh-CN.md +1 -1
  26. package/docs/design.md +8 -5
  27. package/docs/en/configuration.md +1 -1
  28. package/docs/en/contributing.md +5 -6
  29. package/docs/en/flow-console.md +2 -2
  30. package/docs/en/getting-started.md +9 -2
  31. package/docs/en/releasing.md +132 -174
  32. package/docs/validation/2026-11-13-release-0.32.1.md +83 -0
  33. package/docs/validation/2026-11-14-release-0.33.0.md +49 -0
  34. package/docs/validation/2026-11-14-single-package.md +60 -0
  35. package/docs/validation/README.md +3 -0
  36. package/docs/zh-CN/configuration.md +1 -1
  37. package/docs/zh-CN/contributing.md +4 -5
  38. package/docs/zh-CN/flow-console.md +2 -2
  39. package/docs/zh-CN/getting-started.md +7 -3
  40. package/docs/zh-CN/releasing.md +97 -81
  41. package/native/darwin-amd64/flywheel +0 -0
  42. package/native/darwin-arm64/flywheel +0 -0
  43. package/native/linux-amd64/flywheel +0 -0
  44. package/native/linux-arm64/flywheel +0 -0
  45. package/native/manifest.json +22 -0
  46. package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +403 -0
  47. package/node_modules/dsh-subagent-codex-pro/LICENSE +21 -0
  48. package/node_modules/dsh-subagent-codex-pro/README.md +557 -0
  49. package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +30 -0
  50. package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +355 -0
  51. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +65 -0
  52. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +144 -0
  53. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +98 -0
  54. package/node_modules/dsh-subagent-codex-pro/docs/machine.md +194 -0
  55. package/node_modules/dsh-subagent-codex-pro/docs/orchestration.zh.md +86 -0
  56. package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js +68 -0
  57. package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js.map +1 -0
  58. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +15 -0
  59. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -0
  60. package/node_modules/dsh-subagent-codex-pro/lib/index.js +830 -0
  61. package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -0
  62. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +35 -0
  63. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -0
  64. package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js +229 -0
  65. package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js.map +1 -0
  66. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +101 -0
  67. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -0
  68. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +122 -0
  69. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -0
  70. package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js +167 -0
  71. package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js.map +1 -0
  72. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +141 -0
  73. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -0
  74. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +128 -0
  75. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -0
  76. package/node_modules/dsh-subagent-codex-pro/lib/machine.js +104 -0
  77. package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -0
  78. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +156 -0
  79. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -0
  80. package/node_modules/dsh-subagent-codex-pro/lib/run.js +842 -0
  81. package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -0
  82. package/node_modules/dsh-subagent-codex-pro/lib/structured.js +329 -0
  83. package/node_modules/dsh-subagent-codex-pro/lib/structured.js.map +1 -0
  84. package/node_modules/dsh-subagent-codex-pro/lib/tool.js +295 -0
  85. package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -0
  86. package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts +88 -0
  87. package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts.map +1 -0
  88. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +179 -0
  89. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -0
  90. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +266 -0
  91. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -0
  92. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +34 -0
  93. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -0
  94. package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts +113 -0
  95. package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts.map +1 -0
  96. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts +3 -0
  97. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts.map +1 -0
  98. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts +4 -0
  99. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -0
  100. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts +53 -0
  101. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts.map +1 -0
  102. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts +8 -0
  103. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -0
  104. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts +2 -0
  105. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts.map +1 -0
  106. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +70 -0
  107. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -0
  108. package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts +22 -0
  109. package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts.map +1 -0
  110. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +386 -0
  111. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -0
  112. package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts +35 -0
  113. package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts.map +1 -0
  114. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +107 -0
  115. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -0
  116. package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts +131 -0
  117. package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts.map +1 -0
  118. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +321 -0
  119. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -0
  120. package/node_modules/dsh-subagent-codex-pro/lib/usage.js +356 -0
  121. package/node_modules/dsh-subagent-codex-pro/lib/usage.js.map +1 -0
  122. package/node_modules/dsh-subagent-codex-pro/lib/wire.js +986 -0
  123. package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -0
  124. package/node_modules/dsh-subagent-codex-pro/package.json +76 -0
  125. package/npm/install.js +59 -6
  126. package/package.json +74 -12
  127. package/plugins/dsh/LICENSE +21 -0
  128. package/plugins/dsh/PROTOCOL.md +609 -0
  129. package/plugins/dsh/README.en.md +4 -0
  130. package/plugins/dsh/README.md +342 -0
  131. package/plugins/dsh/README.zh-CN.md +279 -0
  132. package/plugins/dsh/THIRD_PARTY_NOTICES.md +42 -0
  133. package/plugins/dsh/vendor/README.md +17 -0
  134. package/plugins/dsh/vendor/codex-pro.json +16 -0
@@ -0,0 +1,241 @@
1
+ import { isAbsolute } from 'node:path';
2
+
3
+ const LIFECYCLE_EVENTS = new Set(['thread-ready', 'dispatch-intent', 'dispatch-possible', 'turn-accepted', 'terminal', 'cleanup-confirmed']);
4
+ const ACK_BYTES = 4096;
5
+ const MAX_LIFECYCLE_EVENTS = 64;
6
+
7
+ function identifier(value) {
8
+ return typeof value === 'string' && value.trim() !== '' && value.length <= 256 && !value.includes('\0');
9
+ }
10
+
11
+ /** Recovery carries only the old non-secret binding, never a prompt. */
12
+ export function recoveryRequest(value, WorkerError) {
13
+ const keys = new Set(['type', 'protocol', 'requestId', 'runId', 'recoveryDirectory', 'original']);
14
+ const originalKeys = new Set(['cwd', 'model', 'reasoningEffort', 'permissionMode', 'threadMode', 'threadId']);
15
+ const o = value?.original;
16
+ if (!value || typeof value !== 'object' || Array.isArray(value) || value.type !== 'recover' || value.protocol !== 3 ||
17
+ Object.keys(value).some(key => !keys.has(key)) || !identifier(value.requestId) || !identifier(value.runId) ||
18
+ typeof value.recoveryDirectory !== 'string' || !isAbsolute(value.recoveryDirectory) || value.recoveryDirectory.includes('\0') || value.recoveryDirectory.length > 4096 ||
19
+ !o || typeof o !== 'object' || Array.isArray(o) || Object.keys(o).some(key => !originalKeys.has(key)) ||
20
+ typeof o.cwd !== 'string' || !isAbsolute(o.cwd) || o.cwd.includes('\0') || !identifier(o.model) ||
21
+ typeof o.reasoningEffort !== 'string' || !['read-only', 'workspace-write', 'danger-full-access'].includes(o.permissionMode) ||
22
+ !['ephemeral', 'persistent'].includes(o.threadMode) ||
23
+ (o.threadId !== undefined && (o.threadMode !== 'persistent' || !identifier(o.threadId)))) {
24
+ throw new WorkerError('INVALID_REQUEST', 'Expected a protocol 3 read-only recovery request with the original non-secret binding');
25
+ }
26
+ // No cwd resolution, login, runMachine, thread/resume or model request here.
27
+ return { protocol: 1, requestId: value.requestId, runId: value.runId, recoveryDirectory: value.recoveryDirectory };
28
+ }
29
+
30
+ // Parent lifeline and bounded transport; adapters own backend semantics.
31
+ export async function runMachineWorker({ ADAPTER, LIMITS, WorkerError, loadAdapter, machineRequest, doctorResult,
32
+ label, commandName, attestAdmission = false, runtimeOptions = () => ({}) }) {
33
+ const controller = new AbortController();
34
+ let cancelledBy, inputFailure, outputFailure, requestId = 'invalid-request', protocol = 1, finished = false, adapterInvoked = false;
35
+ let recovery = false, pendingAck, lifecycleSequence = 0, lifecycleRunId, lifecycleBytes = 0;
36
+ const cancel = signal => { cancelledBy ??= signal; controller.abort(); };
37
+ const handlers = new Map(['SIGTERM', 'SIGINT', 'SIGHUP'].map(signal => [signal, () => cancel(signal)]));
38
+ for (const [signal, handler] of handlers) process.on(signal, handler);
39
+ process.stdout.on('error', () => {
40
+ outputFailure ??= new WorkerError('OUTPUT_ERROR', 'The result consumer closed or failed');
41
+ process.exitCode = 1;
42
+ controller.abort();
43
+ });
44
+
45
+ function write(frame) {
46
+ if (outputFailure) return false;
47
+ const line = JSON.stringify(frame) + '\n';
48
+ if (Buffer.byteLength(line) > LIMITS.receiptFrameBytes) throw new WorkerError('OUTPUT_LIMIT', label + ' result exceeded the complete receipt frame limit');
49
+ process.stdout.write(line);
50
+ return true;
51
+ }
52
+
53
+ function settleAck(error) {
54
+ if (!pendingAck) return;
55
+ const pending = pendingAck;
56
+ pendingAck = undefined;
57
+ clearTimeout(pending.timer);
58
+ if (error) pending.reject(error); else pending.resolve();
59
+ }
60
+
61
+ function acceptAck(value) {
62
+ const keys = new Set(['type', 'protocol', 'requestId', 'runId', 'sequence']);
63
+ if (protocol !== 3 || !pendingAck || !value || typeof value !== 'object' || Array.isArray(value) ||
64
+ Object.keys(value).some(key => !keys.has(key)) || value.type !== 'ack' || value.protocol !== 3 ||
65
+ value.requestId !== requestId || value.runId !== lifecycleRunId || value.sequence !== lifecycleSequence) {
66
+ throw new WorkerError('INVALID_ACK', 'A lifecycle acknowledgment must match the pending request, run and sequence');
67
+ }
68
+ settleAck();
69
+ }
70
+
71
+ /** Keep stdin open: protocol 3 uses durable ACKs, all versions use EOF as a lifeline. */
72
+ function input() {
73
+ let buffer = Buffer.alloc(0), admitted = false, inputProtocol = 1, ackBytes = 0, resolveInput, rejectInput;
74
+ const promise = new Promise((resolve, reject) => { resolveInput = resolve; rejectInput = reject; });
75
+ const fail = (code, message) => {
76
+ if (finished) return;
77
+ inputFailure ??= new WorkerError(code, message);
78
+ clearTimeout(timer);
79
+ rejectInput(inputFailure);
80
+ settleAck(inputFailure);
81
+ controller.abort();
82
+ };
83
+ const timer = setTimeout(() => fail('INPUT_TIMEOUT', 'No complete start request arrived within 10 seconds'), LIMITS.admissionMs);
84
+ const onData = chunk => {
85
+ if (finished || inputFailure) return;
86
+ buffer = Buffer.concat([buffer, chunk]);
87
+ while (buffer.length) {
88
+ if (admitted && buffer.toString('utf8').trim() !== '' && (inputProtocol !== 3 || !pendingAck)) {
89
+ fail(inputProtocol === 3 ? 'INVALID_ACK' : 'INVALID_REQUEST', 'Only the pending lifecycle checkpoint may be acknowledged');
90
+ return;
91
+ }
92
+ const newline = buffer.indexOf(10);
93
+ const limit = admitted ? ACK_BYTES : LIMITS.requestBytes;
94
+ if ((newline < 0 ? buffer.length : newline + 1) > limit) { fail('INPUT_LIMIT', 'A request or acknowledgment exceeded its frame limit'); return; }
95
+ if (newline < 0) return;
96
+ const bytes = buffer.subarray(0, newline);
97
+ buffer = buffer.subarray(newline + 1);
98
+ try {
99
+ const line = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
100
+ if (admitted) {
101
+ ackBytes += bytes.length + 1;
102
+ if (ackBytes > MAX_LIFECYCLE_EVENTS * ACK_BYTES) throw new WorkerError('INPUT_LIMIT', 'Lifecycle acknowledgments exceeded their total limit');
103
+ if (line.trim() === '') continue;
104
+ }
105
+ const value = JSON.parse(line);
106
+ if (!admitted) {
107
+ admitted = true;
108
+ inputProtocol = value?.protocol;
109
+ clearTimeout(timer);
110
+ resolveInput(value);
111
+ } else {
112
+ acceptAck(value);
113
+ }
114
+ } catch (error) {
115
+ fail(error instanceof WorkerError ? error.code : 'INVALID_REQUEST',
116
+ error instanceof WorkerError ? error.message : 'Every frame must be one valid UTF-8 JSON document');
117
+ return;
118
+ }
119
+ }
120
+ };
121
+ const onEnd = () => fail('PARENT_DISCONNECTED', 'The parent stdin lifeline closed before completion');
122
+ const onError = () => fail('INPUT_ERROR', 'The parent stdin lifeline failed');
123
+ const onAbort = () => {
124
+ clearTimeout(timer);
125
+ const error = inputFailure || outputFailure || new WorkerError('CANCELLED', label + ' worker was cancelled');
126
+ rejectInput(error);
127
+ settleAck(error);
128
+ };
129
+ process.stdin.on('data', onData);
130
+ process.stdin.once('end', onEnd);
131
+ process.stdin.once('error', onError);
132
+ controller.signal.addEventListener('abort', onAbort, { once: true });
133
+ if (controller.signal.aborted) onAbort();
134
+ return { promise, stop() {
135
+ clearTimeout(timer);
136
+ settleAck(new WorkerError('CANCELLED', 'The worker has settled'));
137
+ process.stdin.off('data', onData); process.stdin.off('end', onEnd); process.stdin.off('error', onError);
138
+ controller.signal.removeEventListener('abort', onAbort);
139
+ process.stdin.pause();
140
+ process.stdin.destroy();
141
+ } };
142
+ }
143
+
144
+ async function onLifecycle(event) {
145
+ if (controller.signal.aborted) throw inputFailure || outputFailure || new WorkerError('CANCELLED', 'Lifecycle consumer was cancelled');
146
+ if (pendingAck || !event || typeof event !== 'object' || event.type !== 'lifecycle' || event.lifecycleVersion !== 1 ||
147
+ event.requestId !== requestId || !identifier(event.runId) || (lifecycleRunId && lifecycleRunId !== event.runId) ||
148
+ !Number.isSafeInteger(event.sequence) || event.sequence !== lifecycleSequence + 1 || event.sequence > MAX_LIFECYCLE_EVENTS ||
149
+ !LIFECYCLE_EVENTS.has(event.event)) {
150
+ throw new WorkerError('INVALID_LIFECYCLE', 'The adapter emitted an uncorrelated or out-of-order lifecycle event');
151
+ }
152
+ lifecycleRunId = event.runId;
153
+ lifecycleSequence = event.sequence;
154
+ const frame = { protocol: 3, type: 'lifecycle', requestId, adapter: ADAPTER, lifecycle: event };
155
+ lifecycleBytes += Buffer.byteLength(JSON.stringify(frame) + '\n');
156
+ if (lifecycleBytes > LIMITS.progressBytes) throw new WorkerError('OUTPUT_LIMIT', 'Lifecycle evidence exceeded its stream limit');
157
+ // Install the waiter before writing so a fast durable consumer cannot race it.
158
+ await new Promise((resolve, reject) => {
159
+ const timer = setTimeout(() => {
160
+ inputFailure ??= new WorkerError('ACK_TIMEOUT', 'The parent did not durably acknowledge the lifecycle checkpoint in time');
161
+ settleAck(inputFailure);
162
+ controller.abort();
163
+ }, LIMITS.lifecycleAckMs ?? 10000);
164
+ pendingAck = { resolve, reject, timer };
165
+ try {
166
+ if (!write(frame)) settleAck(outputFailure);
167
+ } catch (error) { settleAck(error); controller.abort(); }
168
+ });
169
+ }
170
+
171
+ let lifeline;
172
+ try {
173
+ const args = process.argv.slice(2);
174
+ if (args.length > 1 || (args.length === 1 && args[0] !== '--doctor')) throw new WorkerError('INVALID_REQUEST', 'Use ' + commandName + ' [--doctor]');
175
+ if (args[0] === '--doctor') {
176
+ await loadAdapter();
177
+ write(doctorResult());
178
+ process.exitCode = 0;
179
+ } else {
180
+ lifeline = input();
181
+ const value = await lifeline.promise;
182
+ protocol = [2, 3].includes(value?.protocol) ? value.protocol : 1;
183
+ recovery = protocol === 3 && value?.type === 'recover';
184
+ if (identifier(value?.requestId)) requestId = value.requestId;
185
+ const api = await loadAdapter();
186
+ if (controller.signal.aborted) throw inputFailure || new WorkerError('CANCELLED', label + ' worker was cancelled before the run');
187
+ if (protocol === 3 && (api.MACHINE_LIFECYCLE_VERSION !== 1 || typeof api.recoverMachine !== 'function' ||
188
+ api.MACHINE_LIFECYCLE_CAPABILITIES?.durableJournal !== true || api.MACHINE_LIFECYCLE_CAPABILITIES?.readOnlyRecovery !== true)) {
189
+ throw new WorkerError('ADAPTER_MISMATCH', 'The selected adapter does not expose lifecycle 1 and read-only durable recovery');
190
+ }
191
+ if (recovery) {
192
+ const query = recoveryRequest(value, WorkerError);
193
+ const evidence = await api.recoverMachine(query);
194
+ finished = true;
195
+ lifeline.stop();
196
+ const error = inputFailure || outputFailure;
197
+ write({ protocol: 3, type: 'recovery', requestId, adapter: ADAPTER, recovery: evidence,
198
+ error: error ? { code: error.code, message: error.message } : null });
199
+ process.exitCode = error || controller.signal.aborted ? 1 : 0;
200
+ } else {
201
+ const request = machineRequest(value, api, process.env);
202
+ requestId = request.requestId;
203
+ let progressBytes = 0;
204
+ const options = { signal: controller.signal, env: process.env, ...runtimeOptions(process.env),
205
+ ...(protocol === 3 ? { recoveryDirectory: value.recoveryDirectory, onLifecycle } : {}),
206
+ onProgress(text) {
207
+ if (controller.signal.aborted) return;
208
+ const frame = { protocol, type: 'progress', requestId, text };
209
+ progressBytes += Buffer.byteLength(JSON.stringify(frame) + '\n');
210
+ if (progressBytes > LIMITS.progressBytes) {
211
+ outputFailure ??= new WorkerError('OUTPUT_LIMIT', label + ' progress exceeded the 1 MiB stream limit');
212
+ controller.abort();
213
+ } else write(frame);
214
+ } };
215
+ adapterInvoked = true;
216
+ const receipt = await api.runMachine(request, options);
217
+ finished = true;
218
+ lifeline.stop();
219
+ const error = inputFailure || outputFailure;
220
+ if (outputFailure?.code === 'OUTPUT_LIMIT') outputFailure = undefined;
221
+ write({ protocol, type: 'result', requestId, adapter: ADAPTER, receipt,
222
+ error: error ? { code: error.code, message: error.message } : null });
223
+ process.exitCode = receipt.ok && !error && !controller.signal.aborted ? 0 : 1;
224
+ }
225
+ }
226
+ } catch (error) {
227
+ finished = true;
228
+ lifeline?.stop();
229
+ const known = error instanceof WorkerError ? error : new WorkerError('WORKER_ERROR', label + ' worker failed before a complete receipt was available');
230
+ write({ protocol, type: recovery ? 'recovery' : 'result', requestId, adapter: ADAPTER,
231
+ ...(recovery ? { recovery: null } : { receipt: null }),
232
+ ...(attestAdmission && !adapterInvoked && !recovery ? { admission: { dispatched: false, cleanupConfirmed: true } } : {}),
233
+ error: { code: known.code, message: known.message } });
234
+ process.exitCode = 1;
235
+ } finally {
236
+ finished = true;
237
+ lifeline?.stop();
238
+ if (cancelledBy) process.exitCode = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 }[cancelledBy];
239
+ for (const [signal, handler] of handlers) process.off(signal, handler);
240
+ }
241
+ }
@@ -0,0 +1,91 @@
1
+ // Loaded only in the independently owned SDK process, never in the desktop host.
2
+ // Public rc.5/rc.1 Cordis services; the worker verifies this acknowledgement before prompt().
3
+ import { link, realpath, stat, unlink, writeFile } from 'node:fs/promises';
4
+ import { dirname, isAbsolute } from 'node:path';
5
+ import { readPolicyReceipt, nonempty } from './protocol.mjs';
6
+
7
+ export const inject = ['sandboxPolicy', 'approval', 'shell', 'workingDirectory'];
8
+
9
+ export async function apply(ctx, config) {
10
+ const { permissionMode, cwd, workspaceCwd, receiptPath, requestId, nonce, deferredAdmissionPath } = config;
11
+ let { sessionTitle, sessionId } = config;
12
+ const fullAccess = permissionMode === 'danger-full-access';
13
+ if (!['read-only', 'workspace-write', 'danger-full-access'].includes(permissionMode) || typeof cwd !== 'string' ||
14
+ ctx.shell.sandboxMode === undefined) {
15
+ throw new Error('Flywheel requires a sandbox-capable DSH shell and explicit permission policy');
16
+ }
17
+ if (typeof receiptPath !== 'string' || !isAbsolute(receiptPath) ||
18
+ [requestId, ...(deferredAdmissionPath === undefined ? [sessionId] : []), nonce].some(value => typeof value !== 'string' || value.trim() === '' || value.includes('\0'))) {
19
+ throw new Error('Flywheel policy requires an owned, correlated admission receipt');
20
+ }
21
+ if (deferredAdmissionPath !== undefined && (typeof deferredAdmissionPath !== 'string' || !isAbsolute(deferredAdmissionPath) ||
22
+ dirname(deferredAdmissionPath) !== dirname(receiptPath) || sessionId !== undefined)) {
23
+ throw new Error('Flywheel deferred admission requires the same owned receipt directory');
24
+ }
25
+ const parent = dirname(receiptPath);
26
+ const owner = await stat(parent);
27
+ if (await realpath(parent) !== parent || !owner.isDirectory() || (owner.mode & 0o077) !== 0) {
28
+ throw new Error('Flywheel policy receipt must be inside its private physical directory');
29
+ }
30
+ // Probe the official runner. Restricted modes must prove full enforcement;
31
+ // full access must truthfully report that no confinement was applied.
32
+ const probe = await ctx.shell.execute(ctx.shell.resolve({
33
+ command: ':', workdir: cwd, timeoutMs: 5000,
34
+ sandboxPolicy: { mode: permissionMode, workspaceRoot: cwd },
35
+ }));
36
+ const result = await probe.result();
37
+ if (result.exitCode !== 0 || result.timedOut || result.aborted ||
38
+ result.sandbox?.mode !== permissionMode ||
39
+ (fullAccess ? result.sandbox.denied !== false || result.sandbox.enforcement !== undefined : result.sandbox.enforcement !== 'full')) {
40
+ throw new Error(fullAccess ? 'Flywheel readiness probe did not confirm explicit full access' : 'Flywheel sandbox readiness probe did not prove full enforcement');
41
+ }
42
+ // Session creation is read-only in rc.1. Agent creation is awaited after
43
+ // that announcement and before the factory admits any model work in both hosts.
44
+ ctx.on('agent/created', async ({ agent }) => {
45
+ const session = agent.session;
46
+ if (deferredAdmissionPath !== undefined) {
47
+ const identity = await readPolicyReceipt(deferredAdmissionPath, { requestId, nonce });
48
+ if (Object.keys(identity).some(key => !['requestId', 'nonce', 'sessionId', 'sessionTitle'].includes(key))) {
49
+ throw new Error('Flywheel deferred admission has unsupported fields');
50
+ }
51
+ sessionId = nonempty(identity.sessionId, 'sessionId', 512);
52
+ sessionTitle = identity.sessionTitle === undefined ? undefined : nonempty(identity.sessionTitle, 'sessionTitle', 512);
53
+ }
54
+ if (String(session.id) !== sessionId) throw new Error('Flywheel policy refuses an unrelated SDK session');
55
+ if (workspaceCwd !== undefined && session.header.cwd !== workspaceCwd) {
56
+ throw new Error('Flywheel refuses to relabel a session from a different project workspace');
57
+ }
58
+ if (workspaceCwd !== undefined) {
59
+ // Public service accepts an unpublished Agent. Switch before attesting
60
+ // readiness. The native sandbox root remains the immutable project;
61
+ // unsupported split workspace-write requests are rejected at admission.
62
+ await ctx.workingDirectory.set(agent, cwd);
63
+ }
64
+ if (sessionTitle && ctx.get('sessionTitle')) {
65
+ ctx.get('sessionTitle').rename(session, [...sessionTitle].slice(0, 35).join(''));
66
+ }
67
+ // Official events remain readable by a later SDK. An ad-hoc Session event
68
+ // would make rc.1 reject the entire persisted log on resume.
69
+ session.append('sandbox/mode', { mode: permissionMode });
70
+ session.append('approval/policy', { policy: 'never' });
71
+ const policy = ctx.sandboxPolicy.resolve({ session });
72
+ const receipt = {
73
+ protocol: 1,
74
+ requestId, sessionId, nonce,
75
+ ...(workspaceCwd === undefined ? {} : { workspaceCwd: session.header.cwd }),
76
+ cwd: policy.workspaceRoot,
77
+ permissionMode: policy.mode,
78
+ // Read the explicit logged override through the public API.
79
+ approvalPolicy: ctx.approval.overrideOf(session),
80
+ confinedShell: !fullAccess,
81
+ ...(fullAccess ? {} : { enforcement: result.sandbox.enforcement }),
82
+ };
83
+ // The awaited creation hook publishes once into an owned 0700 directory.
84
+ // Exclusive creation + atomic link rejects existing files/symlinks. This
85
+ // transient control receipt is read before model dispatch, never on replay.
86
+ const pending = receiptPath + '.pending';
87
+ await writeFile(pending, JSON.stringify(receipt), { mode: 0o600, flag: 'wx' });
88
+ await link(pending, receiptPath);
89
+ await unlink(pending);
90
+ });
91
+ }
@@ -0,0 +1,136 @@
1
+ // One-use, no-prompt SDK startup. No model Session exists until the owned start.
2
+ import { open, realpath, stat, link, unlink, readdir } from 'node:fs/promises';
3
+ import { join, isAbsolute } from 'node:path';
4
+ import { BridgeError, MAX_FRAME_BYTES, directory, nonempty, record } from './protocol.mjs';
5
+
6
+ export const PREWARM_MS = 60000;
7
+ const MAX_RECEIPT_BYTES = 8192;
8
+ const TRUNCATED_DIAGNOSTIC = ' [truncated]';
9
+ const BINDING_FIELDS = ['cwd', 'workspaceCwd', 'provider', 'model', 'reasoningEffort', 'permissionMode'];
10
+
11
+ export async function validatePrepare(value) {
12
+ const keys = new Set(['type', 'protocol', 'warmId', 'binding', 'receiptDirectory', 'ttlMs', ...BINDING_FIELDS]);
13
+ if (!record(value) || value.type !== 'prepare' || value.protocol !== 1 ||
14
+ Object.keys(value).some(key => !keys.has(key)) || typeof value.warmId !== 'string' || typeof value.binding !== 'string' ||
15
+ !/^[a-zA-Z0-9_-]{8,128}$/.test(value.warmId) ||
16
+ !/^[a-f0-9]{64}$/.test(value.binding ?? '') || !Number.isSafeInteger(value.ttlMs) || value.ttlMs < 1 || value.ttlMs > PREWARM_MS ||
17
+ !['read-only', 'workspace-write', 'danger-full-access'].includes(value.permissionMode)) {
18
+ throw new BridgeError('INVALID_REQUEST', 'expected a bounded, identified SDK prepare request without a prompt');
19
+ }
20
+ const request = { ...value, cwd: await directory(value.cwd, 'cwd'),
21
+ ...(value.workspaceCwd === undefined ? {} : { workspaceCwd: await directory(value.workspaceCwd, 'workspaceCwd') }) };
22
+ for (const key of ['provider', 'model', 'reasoningEffort']) nonempty(value[key], key);
23
+ if (request.workspaceCwd && request.workspaceCwd !== request.cwd && request.permissionMode === 'workspace-write') {
24
+ throw new BridgeError('INVALID_REQUEST', 'separate execution roots require an explicitly compatible permission mode');
25
+ }
26
+ const path = value.receiptDirectory;
27
+ if (typeof path !== 'string' || !isAbsolute(path) || await realpath(path) !== path) {
28
+ throw new BridgeError('INVALID_REQUEST', 'prewarm receipt directory must be physical and absolute');
29
+ }
30
+ const owner = await stat(path);
31
+ if (!owner.isDirectory() || (owner.mode & 0o077) !== 0) throw new BridgeError('INVALID_REQUEST', 'prewarm receipt directory must be private');
32
+ if ((await readdir(path)).length) throw new BridgeError('INVALID_REQUEST', 'prewarm receipt directory must be new and empty');
33
+ return request;
34
+ }
35
+
36
+ export function matchWarmStart(prepared, request) {
37
+ if (request.maxTokens !== undefined) throw new BridgeError('INVALID_REQUEST', 'maxTokens cannot be introduced after SDK initialization');
38
+ if (BINDING_FIELDS.some(key => (prepared[key] ?? '') !== (request[key] ?? ''))) {
39
+ throw new BridgeError('INVALID_REQUEST', 'formal request differs from the initialized SDK binding');
40
+ }
41
+ }
42
+
43
+ export async function writeWarmReceipt(prepared, settled, error) {
44
+ // A parent may recover this receipt only after proving this worker has exited.
45
+ // Exclusive publication prevents overwriting evidence from another lifetime.
46
+ const pending = join(prepared.receiptDirectory, 'terminal.pending');
47
+ const diagnostic = typeof error?.message === 'string' ? error.message : undefined;
48
+ const receipt = { protocol: 1, warmId: prepared.warmId, binding: prepared.binding,
49
+ workerPid: process.pid, dispatched: false, cleanupConfirmed: settled,
50
+ ...(diagnostic === undefined ? {} : { error: '' }) };
51
+ let bytes = Buffer.byteLength(JSON.stringify(receipt) + '\n');
52
+ // Only diagnostic text is expendable. Never publish oversized or shortened
53
+ // proof fields; a validated prepare leaves ample room for the marker.
54
+ if (bytes > MAX_RECEIPT_BYTES || (diagnostic !== undefined && bytes + TRUNCATED_DIAGNOSTIC.length > MAX_RECEIPT_BYTES)) {
55
+ throw new BridgeError('INVALID_REQUEST', 'prewarm cleanup identity exceeds the private receipt limit');
56
+ }
57
+ if (diagnostic !== undefined) {
58
+ const parts = [], sizes = [];
59
+ // Read only the prefix that can fit. Per-code-point JSON size accounts for
60
+ // UTF-8, escapes and lone surrogates without splitting a surrogate pair.
61
+ for (const character of diagnostic) {
62
+ const size = Buffer.byteLength(JSON.stringify(character)) - 2;
63
+ if (bytes + size > MAX_RECEIPT_BYTES) {
64
+ while (bytes + TRUNCATED_DIAGNOSTIC.length > MAX_RECEIPT_BYTES) {
65
+ bytes -= sizes.pop(); parts.pop();
66
+ }
67
+ parts.push(TRUNCATED_DIAGNOSTIC);
68
+ break;
69
+ }
70
+ parts.push(character); sizes.push(size); bytes += size;
71
+ }
72
+ receipt.error = parts.join('');
73
+ }
74
+ const file = await open(pending, 'wx', 0o600);
75
+ try { await file.writeFile(JSON.stringify(receipt) + '\n'); await file.sync(); }
76
+ finally { await file.close(); }
77
+ await link(pending, join(prepared.receiptDirectory, 'terminal.json'));
78
+ await unlink(pending);
79
+ const directory = await open(prepared.receiptDirectory, 'r');
80
+ try { await directory.sync(); } finally { await directory.close(); }
81
+ }
82
+
83
+ /** Both phases keep stdin as the parent lifeline; EOF is never a start signal. */
84
+ export function prewarmInput(controller, input = process.stdin) {
85
+ let buffer = Buffer.alloc(0), first, phase = 0, ready = false, finished = false;
86
+ let admit, rejectAdmission, next, rejectNext;
87
+ const admission = new Promise((resolve, reject) => { admit = resolve; rejectAdmission = reject; });
88
+ const dispatch = new Promise((resolve, reject) => { next = resolve; rejectNext = reject; });
89
+ dispatch.catch(() => {}); // Initialization may fail before a dispatch is awaited.
90
+ const fail = error => {
91
+ if (finished) return;
92
+ clearTimeout(timer);
93
+ rejectAdmission(error); rejectNext(error); controller.abort(error);
94
+ };
95
+ const timer = setTimeout(() => fail(new BridgeError('TIMEOUT', 'no complete prepare request within 10 seconds')), 10000);
96
+ const onData = chunk => {
97
+ if (finished || controller.signal.aborted) return;
98
+ buffer = Buffer.concat([buffer, chunk]);
99
+ while (buffer.length) {
100
+ const newline = buffer.indexOf(10);
101
+ if ((newline < 0 ? buffer.length : newline + 1) > MAX_FRAME_BYTES) {
102
+ fail(new BridgeError('INVALID_REQUEST', 'prewarm frame exceeds 8 MiB')); return;
103
+ }
104
+ if (newline < 0) return;
105
+ const bytes = buffer.subarray(0, newline); buffer = buffer.subarray(newline + 1);
106
+ try {
107
+ const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
108
+ if (phase === 0) { first = value; phase = 1; clearTimeout(timer); admit(value); }
109
+ else {
110
+ const close = value?.type === 'close';
111
+ if (phase !== 1 || !record(value) || value.protocol !== 1 || value.warmId !== first?.warmId ||
112
+ Object.keys(value).some(key => !['type', 'protocol', 'warmId', ...(close ? [] : ['request'])].includes(key)) ||
113
+ (!close && (value.type !== 'start' || !ready || !record(value.request)))) {
114
+ throw new BridgeError('INVALID_REQUEST', 'expected one matching ready start or close frame');
115
+ }
116
+ phase = 2;
117
+ next(close ? null : value.request);
118
+ if (close && !ready) controller.abort(new BridgeError('TRANSPORT', 'prewarm closed before readiness'));
119
+ }
120
+ } catch (error) {
121
+ fail(error instanceof BridgeError ? error : new BridgeError('INVALID_REQUEST', 'prewarm frames must be complete UTF-8 JSON'));
122
+ return;
123
+ }
124
+ }
125
+ };
126
+ const onEnd = () => fail(new BridgeError('TRANSPORT', 'prewarm parent lifeline closed'));
127
+ const onError = () => fail(new BridgeError('TRANSPORT', 'prewarm parent lifeline failed'));
128
+ const onAbort = () => { clearTimeout(timer); rejectAdmission(controller.signal.reason); rejectNext(controller.signal.reason); };
129
+ controller.signal.addEventListener('abort', onAbort, { once: true });
130
+ if (controller.signal.aborted) onAbort();
131
+ input.on('data', onData); input.once('end', onEnd); input.once('error', onError);
132
+ return { admission, dispatch, ready() { ready = true; }, stop() {
133
+ finished = true; clearTimeout(timer); controller.signal.removeEventListener('abort', onAbort);
134
+ input.off('data', onData); input.off('end', onEnd); input.off('error', onError); input.pause();
135
+ } };
136
+ }