@ckanner/flywheel 0.33.1 → 0.34.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 (148) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -0
  3. package/README.zh-CN.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +5 -1
  5. package/bin/flywheel-kimi-native-worker.js +5 -0
  6. package/bin/flywheel.js +8 -1
  7. package/bin/runtime.js +5 -1
  8. package/dist/bridge.mjs +89 -7
  9. package/dist/claude-code-pro-bridge.mjs +55 -6
  10. package/dist/client.js +40 -5
  11. package/dist/codex-pro-bridge.mjs +12 -5
  12. package/dist/control.mjs +1 -1
  13. package/dist/kimi-native-bridge.mjs +429 -0
  14. package/dist/kimi-native-runtime.mjs +233 -0
  15. package/dist/kimi-native-worker.mjs +77 -0
  16. package/dist/kimi-tower-candidate.mjs +227 -0
  17. package/dist/machine-worker.mjs +39 -5
  18. package/dist/managed-lcu.mjs +223 -0
  19. package/dist/native-doctor.mjs +36 -0
  20. package/dist/native-jobs.mjs +72 -0
  21. package/dist/native-policy.mjs +371 -0
  22. package/dist/native-resources.mjs +144 -0
  23. package/dist/native.mjs +251 -0
  24. package/dist/policy.mjs +13 -3
  25. package/dist/prewarm.mjs +4 -1
  26. package/dist/protocol.mjs +8 -1
  27. package/dist/worker.mjs +16 -9
  28. package/docs/README.md +9 -0
  29. package/docs/README.zh-CN.md +3 -0
  30. package/docs/design.md +33 -11
  31. package/docs/en/claude-code-pro.md +2 -2
  32. package/docs/en/configuration.md +17 -5
  33. package/docs/en/contributing.md +5 -3
  34. package/docs/en/getting-started.md +2 -2
  35. package/docs/en/model-policy.md +2 -2
  36. package/docs/en/native-capabilities.md +185 -0
  37. package/docs/en/releasing.md +2 -2
  38. package/docs/en/session-recovery.md +2 -2
  39. package/docs/plans/2026-11-13-native-harness-capabilities.en.md +258 -0
  40. package/docs/plans/2026-11-13-native-harness-capabilities.md +276 -0
  41. package/docs/plans/README.md +1 -0
  42. package/docs/validation/2026-11-14-desktop-native-configuration.md +128 -0
  43. package/docs/validation/2026-11-14-native-harness-implementation.md +215 -0
  44. package/docs/validation/2026-11-14-published-companions.md +15 -0
  45. package/docs/validation/2026-11-14-release-0.33.1.md +78 -0
  46. package/docs/validation/README.md +3 -0
  47. package/docs/validation/evidence/2026-11-14-native-harness-candidate.json +255 -0
  48. package/docs/validation/evidence/2026-11-14-native-harness-sessions.json +6009 -0
  49. package/docs/validation/evidence/2026-11-14-native-harness-tower-candidate.json +192 -0
  50. package/docs/zh-CN/claude-code-pro.md +2 -2
  51. package/docs/zh-CN/configuration.md +21 -8
  52. package/docs/zh-CN/contributing.md +5 -3
  53. package/docs/zh-CN/getting-started.md +2 -2
  54. package/docs/zh-CN/model-policy.md +2 -2
  55. package/docs/zh-CN/native-capabilities.md +184 -0
  56. package/docs/zh-CN/releasing.md +2 -2
  57. package/docs/zh-CN/session-recovery.md +1 -1
  58. package/native/darwin-amd64/flywheel +0 -0
  59. package/native/darwin-arm64/flywheel +0 -0
  60. package/native/linux-amd64/flywheel +0 -0
  61. package/native/linux-arm64/flywheel +0 -0
  62. package/native/manifest.json +9 -9
  63. package/node_modules/dsh-preset-generator/CHANGELOG.md +77 -0
  64. package/node_modules/dsh-preset-generator/LICENSE +21 -0
  65. package/node_modules/dsh-preset-generator/README.md +149 -0
  66. package/node_modules/dsh-preset-generator/docs/README.zh.md +95 -0
  67. package/node_modules/dsh-preset-generator/package.json +43 -0
  68. package/node_modules/dsh-preset-generator/src/composition.mjs +150 -0
  69. package/node_modules/dsh-preset-generator/src/gen-presets.mjs +367 -0
  70. package/node_modules/dsh-preset-generator/src/policy.mjs +17 -0
  71. package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +16 -1
  72. package/node_modules/dsh-subagent-codex-pro/README.md +57 -4
  73. package/node_modules/dsh-subagent-codex-pro/assets/native-client.js +118 -0
  74. package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +6 -0
  75. package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +49 -2
  76. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +1 -1
  77. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +1 -1
  78. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +1 -1
  79. package/node_modules/dsh-subagent-codex-pro/docs/machine.md +2 -2
  80. package/node_modules/dsh-subagent-codex-pro/docs/native-capabilities.md +254 -0
  81. package/node_modules/dsh-subagent-codex-pro/docs/native-capabilities.zh.md +180 -0
  82. package/node_modules/dsh-subagent-codex-pro/lib/config.js +56 -0
  83. package/node_modules/dsh-subagent-codex-pro/lib/config.js.map +1 -0
  84. package/node_modules/dsh-subagent-codex-pro/lib/desktop-native.js +109 -0
  85. package/node_modules/dsh-subagent-codex-pro/lib/desktop-native.js.map +1 -0
  86. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +3 -4
  87. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -1
  88. package/node_modules/dsh-subagent-codex-pro/lib/index.js +64 -15
  89. package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -1
  90. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +6 -2
  91. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -1
  92. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +2 -2
  93. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -1
  94. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +5 -2
  95. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -1
  96. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +11 -0
  97. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -1
  98. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +1 -0
  99. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -1
  100. package/node_modules/dsh-subagent-codex-pro/lib/machine.js +20 -3
  101. package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -1
  102. package/node_modules/dsh-subagent-codex-pro/lib/native.js +297 -0
  103. package/node_modules/dsh-subagent-codex-pro/lib/native.js.map +1 -0
  104. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +11 -1
  105. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -1
  106. package/node_modules/dsh-subagent-codex-pro/lib/run.js +19 -1
  107. package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -1
  108. package/node_modules/dsh-subagent-codex-pro/lib/tool.js +123 -57
  109. package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -1
  110. package/node_modules/dsh-subagent-codex-pro/lib/types/config.d.ts +31 -0
  111. package/node_modules/dsh-subagent-codex-pro/lib/types/config.d.ts.map +1 -0
  112. package/node_modules/dsh-subagent-codex-pro/lib/types/desktop-native.d.ts +30 -0
  113. package/node_modules/dsh-subagent-codex-pro/lib/types/desktop-native.d.ts.map +1 -0
  114. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +7 -5
  115. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -1
  116. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +31 -0
  117. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -1
  118. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +2 -0
  119. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -1
  120. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -1
  121. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -1
  122. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +7 -0
  123. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -1
  124. package/node_modules/dsh-subagent-codex-pro/lib/types/native.d.ts +120 -0
  125. package/node_modules/dsh-subagent-codex-pro/lib/types/native.d.ts.map +1 -0
  126. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +11 -0
  127. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -1
  128. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +11 -0
  129. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -1
  130. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +7 -1
  131. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -1
  132. package/node_modules/dsh-subagent-codex-pro/lib/wire.js +34 -6
  133. package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -1
  134. package/node_modules/dsh-subagent-codex-pro/package.json +28 -4
  135. package/npm/install.js +2 -2
  136. package/package.json +25 -10
  137. package/plugins/dsh/KIMI_NATIVE_PROTOCOL.en.md +76 -0
  138. package/plugins/dsh/KIMI_NATIVE_PROTOCOL.md +76 -0
  139. package/plugins/dsh/PROTOCOL.md +149 -17
  140. package/plugins/dsh/README.md +15 -12
  141. package/plugins/dsh/README.zh-CN.md +8 -8
  142. package/plugins/dsh/THIRD_PARTY_NOTICES.md +5 -1
  143. package/plugins/dsh/vendor/README.md +40 -11
  144. package/plugins/dsh/vendor/chatgpt.json +21 -0
  145. package/plugins/dsh/vendor/claude-code-pro.json +21 -0
  146. package/plugins/dsh/vendor/codex-pro.json +13 -8
  147. package/plugins/dsh/vendor/lcu.json +21 -0
  148. package/plugins/dsh/vendor/preset-generator.json +16 -0
@@ -0,0 +1,223 @@
1
+ // Host adapter for the public LCU attachment API. The scheduler owns the physical
2
+ // desktop lease; LCU owns each real Agent's runtime and awaited lifecycle hooks.
3
+ import { createHash } from 'node:crypto';
4
+ import { constants } from 'node:fs';
5
+ import { lstat, mkdir, open, realpath } from 'node:fs/promises';
6
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
7
+ import { attachManagedLcu, ManagedLcuError } from 'dsh-plugin-lcu/managed';
8
+
9
+ const MAX_IMAGE_BYTES = 64 * 1024 * 1024;
10
+ const MIME_EXTENSIONS = new Map([['image/png', 'png'], ['image/jpeg', 'jpg'], ['image/webp', 'webp'], ['image/gif', 'gif']]);
11
+ const identityKeys = ['runId', 'invocationId', 'rootSessionId', 'candidateId'];
12
+ const leaseKeys = ['leaseId', 'resourceId', 'leaseEpoch', 'ownerInvocationId'];
13
+ const hash = data => createHash('sha256').update(data).digest('hex');
14
+ const errorText = error => (error instanceof Error ? error.message : String(error)).slice(0, 2048);
15
+
16
+ function requireIdentity(value, label) {
17
+ if (typeof value !== 'string' || !value.trim() || value.length > 4096 || value.includes('\0')) throw new Error(`Invalid managed LCU ${label}`);
18
+ }
19
+
20
+ async function physicalDirectory(path, create = false) {
21
+ if (create) await mkdir(path, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
22
+ const stat = await lstat(path);
23
+ if (!stat.isDirectory() || (stat.mode & 0o022) !== 0 ||
24
+ (process.getuid && stat.uid !== process.getuid()) || await realpath(path) !== path) {
25
+ throw new Error('LCU evidence requires an owned physical directory without shared write permission');
26
+ }
27
+ return path;
28
+ }
29
+
30
+ async function evidenceDirectory(taskDir, invocationId) {
31
+ let path = await physicalDirectory(taskDir);
32
+ for (const component of ['reports', 'native', invocationId]) path = await physicalDirectory(join(path, component), true);
33
+ return path;
34
+ }
35
+
36
+ async function syncDirectory(path) {
37
+ const directory = await open(path, constants.O_RDONLY | constants.O_DIRECTORY);
38
+ try { await directory.sync(); } finally { await directory.close(); }
39
+ }
40
+
41
+ async function readImage(path, expected) {
42
+ if (!isAbsolute(path)) throw new Error('Attachment store returned a relative image path');
43
+ // The source comes from the attachment service. Pin its physical parent and
44
+ // refuse a final symlink/non-file before reading any bytes.
45
+ const physical = join(await realpath(dirname(path)), basename(path));
46
+ const file = await open(physical, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
47
+ try {
48
+ const stat = await file.stat();
49
+ if (!stat.isFile() || stat.size < 1 || stat.size > MAX_IMAGE_BYTES || stat.size !== expected.bytes) {
50
+ throw new Error('Attachment bytes are not a bounded regular image of the observed size');
51
+ }
52
+ const data = await file.readFile();
53
+ if (data.length !== expected.bytes || hash(data) !== expected.sha256) throw new Error('Attachment hash differs from the captured image');
54
+ return data;
55
+ } finally { await file.close(); }
56
+ }
57
+
58
+ async function writeVerified(path, data) {
59
+ let file;
60
+ try { file = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); }
61
+ catch (error) {
62
+ if (error.code !== 'EEXIST') throw error;
63
+ // A repeated content hash is reusable only after reading the real existing
64
+ // file. A symlink, old unrelated file or partial write must never be reused.
65
+ await readImage(path, { bytes: data.length, sha256: hash(data) });
66
+ return;
67
+ }
68
+ try { await file.writeFile(data); await file.sync(); } finally { await file.close(); }
69
+ await readImage(path, { bytes: data.length, sha256: hash(data) });
70
+ await syncDirectory(dirname(path));
71
+ }
72
+
73
+ async function attachMediaEvidence(ctx, taskDir, event) {
74
+ const images = Array.isArray(event.facts.images) ? event.facts.images : [];
75
+ const evidence = {
76
+ version: 1, identity: event.identity, lease: event.lease,
77
+ agentSessionId: event.agentSessionId, runtimeEpoch: event.runtimeEpoch,
78
+ turnId: event.turnId ?? null, callId: event.callId ?? null,
79
+ sourceSequence: event.sequence, capturedAt: event.facts.observedAt ?? null,
80
+ method: event.facts.method ?? null, target: event.facts.target ?? null,
81
+ modelRoute: event.facts.modelRoute ?? {},
82
+ screenshotCaptured: images.length > 0,
83
+ imageDeliveredToModel: 'unverified', evidenceAttachedToRun: false,
84
+ status: 'unavailable', images: [],
85
+ };
86
+ let directory;
87
+ try { directory = await evidenceDirectory(taskDir, event.identity.invocationId); }
88
+ catch (error) { return { ...evidence, error: errorText(error) }; }
89
+ const attachments = ctx.get('attachments');
90
+ for (const image of images) {
91
+ const entry = { capturedSha256: image.sha256, capturedMimeType: image.mimeType, capturedBytes: image.bytes, status: 'unavailable' };
92
+ try {
93
+ if (!/^[a-f0-9]{64}$/.test(image.sha256) || !Number.isSafeInteger(image.bytes) || image.bytes < 1 ||
94
+ image.bytes > MAX_IMAGE_BYTES || !MIME_EXTENSIONS.has(image.mimeType)) throw new Error('Image facts are not verifiable');
95
+ if (typeof attachments?.readImage !== 'function' || !image.attachment) throw new Error('No verifiable attachment-store reference was prepared');
96
+ // DSH may normalize/rescale a capture before publishing its reference.
97
+ // The public reader verifies that normalized object, whose hash must be
98
+ // recorded separately from the raw runtime capture's hash.
99
+ const stored = await attachments.readImage(image.attachment);
100
+ const data = stored?.data instanceof Uint8Array ? Buffer.from(stored.data) : undefined;
101
+ const ref = stored?.ref;
102
+ if (!data || data.length < 1 || data.length > MAX_IMAGE_BYTES || ref?.attachmentId !== image.attachment.attachmentId ||
103
+ data.length !== ref.bytes || ref.bytes !== image.attachment.bytes || !MIME_EXTENSIONS.has(ref.mediaType)) {
104
+ throw new Error('Attachment reader did not verify the referenced normalized image');
105
+ }
106
+ entry.sha256 = hash(data); entry.mimeType = ref.mediaType; entry.bytes = data.length;
107
+ entry.attachment = ref;
108
+ entry.normalized = entry.sha256 !== image.sha256 || entry.mimeType !== image.mimeType || entry.bytes !== image.bytes;
109
+ const path = join(directory, `${entry.sha256}.${MIME_EXTENSIONS.get(entry.mimeType)}`);
110
+ await physicalDirectory(directory);
111
+ await writeVerified(path, data);
112
+ entry.path = path;
113
+ entry.status = 'attached';
114
+ } catch (error) { entry.error = errorText(error); }
115
+ evidence.images.push(entry);
116
+ }
117
+ const attached = evidence.images.filter(image => image.status === 'attached').length;
118
+ evidence.status = attached === images.length && images.length > 0 ? 'attached' : attached > 0 ? 'partial' : 'unavailable';
119
+ evidence.evidenceAttachedToRun = evidence.status === 'attached';
120
+ const manifestName = hash(JSON.stringify([event.agentSessionId, event.runtimeEpoch, event.turnId, event.callId, event.sequence]));
121
+ const manifest = join(directory, `${manifestName}.json`);
122
+ try {
123
+ await physicalDirectory(directory);
124
+ await writeVerified(manifest, Buffer.from(JSON.stringify(evidence) + '\n'));
125
+ return { ...evidence, manifest };
126
+ } catch (error) {
127
+ // The bytes may exist, but no durable candidate-bound manifest was verified.
128
+ return { ...evidence, status: 'unavailable', evidenceAttachedToRun: false, error: errorText(error) };
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Mount the actual public LCU API after native-policy has admitted the Agent.
134
+ * identityOf is consulted again for every call and approval, so a removed or
135
+ * unrelated live Agent cannot keep using a previously copied grant.
136
+ */
137
+ export async function createManagedLcu(ctx, { options = {}, managed, rootSessionId, identityOf, verifyLease, onEvent }) {
138
+ if (managed?.version !== 1) throw new Error('Managed LCU requires invocation identity version 1');
139
+ const identity = Object.freeze({ runId: managed.runId, invocationId: managed.invocationId, rootSessionId,
140
+ ...(managed.candidateId === undefined ? {} : { candidateId: managed.candidateId }) });
141
+ for (const [key, value] of Object.entries(identity)) requireIdentity(value, key);
142
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(managed.invocationId)) throw new Error('Managed LCU invocation ID must be a safe evidence directory name');
143
+ if (typeof identityOf !== 'function' || typeof verifyLease !== 'function' || typeof onEvent !== 'function') throw new Error('Managed LCU requires lineage, lease and event callbacks');
144
+ const lease = Object.freeze(Object.fromEntries(leaseKeys.map(key => [key, managed.lease?.[key]])));
145
+ for (const [key, value] of Object.entries(lease)) requireIdentity(value, `lease.${key}`);
146
+ if (lease.ownerInvocationId !== identity.invocationId) throw new Error('Managed LCU lease belongs to another invocation');
147
+ if (typeof managed.taskDir !== 'string' || !isAbsolute(managed.taskDir) || resolve(managed.taskDir) !== managed.taskDir) throw new Error('Managed LCU requires a physical absolute task directory');
148
+ const taskDir = await physicalDirectory(managed.taskDir);
149
+ const host = Object.freeze({ role: managed.role, scope: managed.scope, channel: managed.channel });
150
+ const config = structuredClone(options);
151
+ const handles = new Map();
152
+ const pending = new Map();
153
+ const failedReceipts = [];
154
+ let accepting = true;
155
+ let closing;
156
+ let eventTail = Promise.resolve();
157
+
158
+ const verifyIdentity = agent => {
159
+ const actual = identityOf(agent);
160
+ if (!actual || identityKeys.some(key => actual[key] !== identity[key])) throw new Error('LCU Agent identity differs from the admitted invocation');
161
+ return identity;
162
+ };
163
+ const emit = event => {
164
+ const operation = eventTail.catch(() => {}).then(async () => {
165
+ let envelope = { ...event, host };
166
+ if (event.type === 'media') {
167
+ const evidence = await attachMediaEvidence(ctx, taskDir, event);
168
+ envelope = { ...envelope, facts: { ...event.facts, evidence,
169
+ evidenceAttachedToRun: evidence.evidenceAttachedToRun,
170
+ imageDeliveredToModel: 'unverified' } };
171
+ }
172
+ await onEvent(envelope);
173
+ });
174
+ eventTail = operation;
175
+ return operation;
176
+ };
177
+ const snapshot = () => [...failedReceipts, ...[...handles.values()].map(handle => handle.snapshot())];
178
+ return {
179
+ snapshot,
180
+ async attach(agent) {
181
+ if (!accepting) throw new Error('Managed LCU invocation is closing');
182
+ verifyIdentity(agent);
183
+ if (handles.has(agent)) return handles.get(agent).snapshot();
184
+ if (pending.has(agent)) return pending.get(agent);
185
+ const operation = (async () => {
186
+ try {
187
+ const handle = await attachManagedLcu(ctx, { agent, identity, lease, config,
188
+ verifyLease: async admission => { verifyIdentity(agent); await verifyLease(admission); }, onEvent: emit });
189
+ handles.set(agent, handle);
190
+ return handle.snapshot();
191
+ } catch (error) {
192
+ if (error instanceof ManagedLcuError && error.receipt) failedReceipts.push(error.receipt);
193
+ throw error;
194
+ }
195
+ })();
196
+ pending.set(agent, operation);
197
+ try { return await operation; } finally { pending.delete(agent); }
198
+ },
199
+ async close(reason = 'Interrupt') {
200
+ if (closing) return closing;
201
+ accepting = false;
202
+ const event = ['Stop', 'Interrupt', 'SubagentStop'].includes(reason) ? reason : 'Interrupt';
203
+ closing = (async () => {
204
+ await Promise.allSettled([...pending.values()]);
205
+ const outcomes = await Promise.allSettled([...handles.values()].map(handle => handle.close(event)));
206
+ // A public close normally returns a receipt. Unexpected rejection still
207
+ // keeps its real snapshot; append unknown instead of hiding the owner.
208
+ const owned = [...handles.values()];
209
+ outcomes.forEach((outcome, index) => {
210
+ if (outcome.status === 'rejected') {
211
+ const receipt = owned[index].snapshot();
212
+ failedReceipts.push({ ...receipt, cleanup: 'unknown', state: 'blocked', errors: [...receipt.errors, errorText(outcome.reason)] });
213
+ }
214
+ });
215
+ await eventTail.catch(() => {}); // LCU snapshots already retain failed event receipts.
216
+ return snapshot();
217
+ })();
218
+ const receipts = await closing;
219
+ if (receipts.some(receipt => receipt.cleanup !== 'confirmed')) closing = undefined;
220
+ return receipts;
221
+ },
222
+ };
223
+ }
@@ -0,0 +1,36 @@
1
+ // Import-only availability checks. Do not instantiate an SDK, an Agent, LCU,
2
+ // a desktop client, or a model. Actual admission is owned by each invocation.
3
+ const loadModule = specifier => import(specifier, specifier.endsWith('/package.json') ? { with: { type: 'json' } } : {});
4
+
5
+ export async function nativeDoctor({ load = loadModule } = {}) {
6
+ const composition = { package: 'dsh-preset-generator', version: null, protocol: null, available: false,
7
+ presets: [], executor: 'ptc', namedPresetMounted: false };
8
+ const lcu = { package: 'dsh-plugin-lcu', version: null, managedProtocol: null, available: false };
9
+ try {
10
+ const api = await load('dsh-preset-generator/composition');
11
+ const manifest = (await load('dsh-preset-generator/package.json')).default;
12
+ composition.version = manifest.version;
13
+ composition.protocol = api.COMPOSITION_PROTOCOL;
14
+ if (manifest.name !== composition.package || manifest.version !== '0.4.0' || api.COMPOSITION_PROTOCOL !== 1
15
+ || typeof api.validateSdkPtcAdmission !== 'function' || typeof api.inspectPtcBundle !== 'function') throw new Error();
16
+ for (const preset of ['daily', 'heavy']) {
17
+ const value = api.createSdkPtcComposition({ runtimeProfile: 'sdk', preset });
18
+ if (value.version !== 1 || value.runtimeProfile !== 'sdk' || value.requirements?.executor !== 'ptc'
19
+ || value.provenance?.namedPresetMounted !== false || !/^[a-f0-9]{64}$/.test(value.digest)) throw new Error();
20
+ composition.presets.push(preset);
21
+ }
22
+ composition.available = true;
23
+ } catch { composition.available = false; }
24
+ try {
25
+ const api = await load('dsh-plugin-lcu/managed');
26
+ const manifest = (await load('dsh-plugin-lcu/package.json')).default;
27
+ lcu.version = manifest.version;
28
+ if (manifest.name !== lcu.package || manifest.version !== '0.5.0'
29
+ || typeof api.attachManagedLcu !== 'function' || typeof api.ManagedLcuError !== 'function') throw new Error();
30
+ lcu.managedProtocol = 1;
31
+ lcu.available = true;
32
+ } catch { lcu.available = false; }
33
+ return { protocol: 1, type: 'native-doctor', harness: 'dsh', nativeVersion: 1,
34
+ available: composition.available, composition, lcu, inferenceTested: false, admissionTested: false,
35
+ nativeAdmission: 'requires-selected-sdk-runtime-and-effective-capability-probe' };
36
+ }
@@ -0,0 +1,72 @@
1
+ // Independent native jobs preserve their own receipt and recovery semantics.
2
+ // They never return a Flywheel Review/Judge verdict or mutate task state.
3
+ import { open } from 'node:fs/promises';
4
+ import { constants } from 'node:fs';
5
+ import { isAbsolute } from 'node:path';
6
+ import { loadAdapter as loadCodex } from './codex-pro-bridge.mjs';
7
+ import { loadAdapter as loadClaude } from './claude-code-pro-bridge.mjs';
8
+
9
+ const OPERATIONS = new Set(['codex-review', 'claude-review', 'claude-team', 'claude-recover', 'claude-control']);
10
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
11
+
12
+ export function nativeJobExitCode(receipt) {
13
+ const outcome = receipt.receipt ?? receipt;
14
+ return ['pending', 'unknown'].includes(receipt.status) || outcome.ok === false ||
15
+ outcome.cleanup?.confirmed === false || outcome.native?.cleanup?.confirmed === false ? 3 : 0;
16
+ }
17
+
18
+ /** Input is the public package request plus an explicit journal directory. */
19
+ export async function runNativeJob(operation, input, { signal, loaders = { codex: loadCodex, claude: loadClaude } } = {}) {
20
+ if (!OPERATIONS.has(operation) || !object(input)) throw new Error('Choose an independent native job and provide a JSON object');
21
+ if (typeof input.recoveryDirectory !== 'string' || !isAbsolute(input.recoveryDirectory) || input.recoveryDirectory.includes('\0')) {
22
+ throw new Error('A private absolute recoveryDirectory is required for every native job');
23
+ }
24
+ const api = await loaders[operation === 'codex-review' ? 'codex' : 'claude']();
25
+ signal?.throwIfAborted();
26
+ if (operation === 'claude-recover') return api.recoverNativeJob(input);
27
+ if (operation === 'claude-control') return api.controlInteractiveTeam(input);
28
+ const { recoveryDirectory, ...request } = input;
29
+ const runtime = { recoveryDirectory, signal };
30
+ if (operation === 'codex-review') {
31
+ if (!request.native?.review || request.threadMode !== 'ephemeral' || request.threadId !== undefined) {
32
+ throw new Error('Native Codex review requires its own fresh ephemeral request and explicit review target');
33
+ }
34
+ return api.runMachine(api.parseMachineRequest(request), runtime);
35
+ }
36
+ if (operation === 'claude-review') {
37
+ api.validateNativeReviewRequest(request);
38
+ return api.runNativeReview(request, runtime);
39
+ }
40
+ api.validateInteractiveTeamRequest(request);
41
+ return api.runInteractiveTeam(request, runtime);
42
+ }
43
+
44
+ export async function main(args, { stdout = process.stdout, stderr = process.stderr } = {}) {
45
+ if (args.length === 0 || (args.length === 1 && args[0] === '--help')) {
46
+ stdout.write('Usage: flywheel native <codex-review|claude-review|claude-team|claude-recover|claude-control> --request /absolute/request.json\n' +
47
+ 'Requests use the pinned plugin public API plus recoveryDirectory. These jobs produce native evidence, never a formal Flywheel verdict.\n');
48
+ return 0;
49
+ }
50
+ const controller = new AbortController();
51
+ const cancel = () => controller.abort(new Error('Native job cancelled'));
52
+ for (const name of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(name, cancel);
53
+ try {
54
+ if (args.length !== 3 || !OPERATIONS.has(args[0]) || args[1] !== '--request' || !isAbsolute(args[2])) throw new Error('Use flywheel native --help for the request-file interface');
55
+ const file = await open(args[2], constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
56
+ let input;
57
+ try {
58
+ const stat = await file.stat();
59
+ if (!stat.isFile() || stat.size < 2 || stat.size > 1024 * 1024) throw new Error('Native request must be a regular JSON file of at most 1 MiB');
60
+ input = JSON.parse(await file.readFile('utf8'));
61
+ } finally { await file.close(); }
62
+ const receipt = await runNativeJob(args[0], input, { signal: controller.signal });
63
+ stdout.write(JSON.stringify(receipt) + '\n');
64
+ if (controller.signal.aborted) return 130;
65
+ return nativeJobExitCode(receipt);
66
+ } catch (error) {
67
+ stderr.write(`flywheel native: ${error instanceof Error ? error.message : 'native operation failed'}\n`);
68
+ return controller.signal.aborted ? 130 : 1;
69
+ } finally {
70
+ for (const name of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.off(name, cancel);
71
+ }
72
+ }