@harness-mix/cli 0.1.4 → 0.1.6

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 (146) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +26 -96
  3. package/docs/chatgpt-web-sidebar.md +7 -1
  4. package/docs/cross-platform.md +15 -0
  5. package/docs/harness-handoff-design.md +377 -0
  6. package/docs/harness-management.md +28 -1
  7. package/docs/multi-agent-collaboration.md +9 -9
  8. package/docs/native-codex.md +21 -10
  9. package/output/native-build/harness-mix-shim.exe +0 -0
  10. package/output/native-build/renderer-extension.js +2596 -135
  11. package/package.json +20 -10
  12. package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
  13. package/scripts/codex-accounts-test.cjs +81 -0
  14. package/scripts/codex-accounts-ui-smoke.cjs +70 -0
  15. package/scripts/collaboration-test.cjs +8 -3
  16. package/scripts/collaboration-ui-smoke.cjs +40 -26
  17. package/scripts/e2e-integrations.cjs +52 -0
  18. package/scripts/e2e-native-host.cjs +39 -0
  19. package/scripts/handoff-checkpoint-test.cjs +83 -0
  20. package/scripts/install-native.cjs +4 -1
  21. package/scripts/integrations-test.cjs +123 -0
  22. package/scripts/integrations-ui-smoke.cjs +124 -0
  23. package/scripts/native-protocol-test.cjs +180 -5
  24. package/scripts/native-secret-test.cjs +2 -2
  25. package/scripts/native-shim-test.cjs +20 -4
  26. package/scripts/native-sidebar-test.cjs +7 -0
  27. package/scripts/native-skill-roots-test.cjs +90 -0
  28. package/scripts/native-ui-smoke.cjs +58 -2
  29. package/scripts/publish-native-package.cjs +8 -5
  30. package/scripts/support/integrations-mcp-fixture.cjs +15 -0
  31. package/scripts/switch-harness-test.cjs +27 -1
  32. package/scripts/test-live-mentions.cjs +152 -0
  33. package/src/main/adapters/acp.js +3 -3
  34. package/src/main/adapters/antigravity.js +17 -17
  35. package/src/main/adapters/claude.js +50 -10
  36. package/src/main/adapters/codebuddy.js +1 -0
  37. package/src/main/adapters/codex-app-server.js +20 -13
  38. package/src/main/adapters/codex.js +25 -5
  39. package/src/main/adapters/cursor.js +5 -1
  40. package/src/main/adapters/dsh.js +9 -1
  41. package/src/main/adapters/grok.js +7 -3
  42. package/src/main/adapters/hermes.js +8 -0
  43. package/src/main/adapters/kiro.js +2 -1
  44. package/src/main/adapters/managed-mcp.js +25 -0
  45. package/src/main/adapters/native-acp.js +4 -4
  46. package/src/main/adapters/omp.js +11 -0
  47. package/src/main/adapters/openclaw.js +6 -0
  48. package/src/main/adapters/opencode.js +13 -3
  49. package/src/main/adapters/pi.js +5 -0
  50. package/src/main/adapters/qoder.js +1 -0
  51. package/src/main/adapters/trae.js +4 -0
  52. package/src/main/adapters/zcode.js +3 -0
  53. package/src/main/harness-adapter/manifest.js +17 -2
  54. package/src/main/host/collaboration.js +57 -11
  55. package/src/main/host/handoff-access.js +53 -0
  56. package/src/main/host/handoff-checkpoints.js +206 -0
  57. package/src/main/host/handoff-mcp.cjs +38 -0
  58. package/src/main/host/handoff-tools.js +13 -0
  59. package/src/main/host/handoff.js +28 -6
  60. package/src/main/host/integrations.js +339 -0
  61. package/src/main/host/runtime.js +95 -15
  62. package/src/main/host/store.js +4 -4
  63. package/src/main/native/codex-accounts.js +260 -0
  64. package/src/main/native/host.js +1 -1
  65. package/src/main/native/launcher.js +2 -2
  66. package/src/main/native/protocol.js +176 -14
  67. package/src/main/native/redact.js +1 -1
  68. package/src/main/native/rs/crates/shim/src/main.rs +155 -12
  69. package/src/main/native/thread-list.js +3 -0
  70. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  71. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
  72. package/src/native-ui/renderer-extension/dist/types/index.d.ts +4 -0
  73. package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
  74. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
  75. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
  76. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  77. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts +36 -0
  78. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
  79. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
  80. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
  81. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
  82. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
  83. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
  84. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
  85. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts +11 -0
  86. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
  87. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +105 -0
  88. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
  89. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
  90. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  91. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
  92. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  93. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
  94. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
  95. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
  96. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
  97. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +6 -0
  98. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
  99. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -0
  100. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  101. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +8 -4
  102. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  103. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  104. package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +4 -1
  105. package/src/native-ui/renderer-extension/src/index.ts +28 -0
  106. package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
  107. package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
  108. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +115 -18
  109. package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +87 -0
  110. package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
  111. package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
  112. package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
  113. package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +556 -123
  114. package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +59 -0
  115. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
  116. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  117. package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
  118. package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
  119. package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +1474 -0
  120. package/src/native-ui/renderer-extension/src/settings/localization.ts +32 -13
  121. package/src/native-ui/renderer-extension/src/settings/pages.ts +49 -18
  122. package/src/native-ui/renderer-extension/src/settings/shell.css +103 -0
  123. package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
  124. package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +21 -0
  125. package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
  126. package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
  127. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
  128. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
  129. package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
  130. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +6 -0
  131. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
  132. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
  133. package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
  134. package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
  135. package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
  136. package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
  137. package/src/native-ui/shared-contracts/dist/index.js +1 -1
  138. package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
  139. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
  140. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
  141. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
  142. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
  143. package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
  144. package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
  145. package/src/native-ui/shared-contracts/src/index.ts +8 -1
  146. package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +17 -0
@@ -0,0 +1,260 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const os = require('node:os');
4
+ const { randomUUID } = require('node:crypto');
5
+ const { CodexAppServer } = require('../adapters/codex-app-server');
6
+
7
+ const OFFICIAL_ACCOUNT_ID = 'official-codex';
8
+ const ACCOUNT_ID = /^[A-Za-z0-9._~-]{1,256}$/;
9
+ const PLAN_TYPES = new Set(['free', 'go', 'plus', 'pro', 'prolite', 'team', 'self_serve_business_prolite', 'self_serve_business_usage_based', 'business', 'ent26', 'enterprise_cbp_automation', 'enterprise_cbp_usage_based', 'enterprise', 'edu', 'edu_plus', 'edu_pro', 'unknown']);
10
+
11
+ function isoFromUnixSeconds(value) {
12
+ return Number.isSafeInteger(value) && value >= 0 ? new Date(value * 1000).toISOString() : undefined;
13
+ }
14
+
15
+ function rateLimitView(result) {
16
+ const snapshot = result?.rateLimits;
17
+ const primary = snapshot?.primary;
18
+ const secondary = snapshot?.secondary;
19
+ const usage = {};
20
+ if (typeof primary?.usedPercent === 'number' && primary.usedPercent >= 0 && primary.usedPercent <= 100) {
21
+ usage.planFiveHourUsedPercent = primary.usedPercent;
22
+ if (Number.isSafeInteger(primary.resetsAt) && primary.resetsAt >= 0) usage.planFiveHourResetsAtUnix = primary.resetsAt;
23
+ }
24
+ if (typeof secondary?.usedPercent === 'number' && secondary.usedPercent >= 0 && secondary.usedPercent <= 100) {
25
+ usage.planSevenDayUsedPercent = secondary.usedPercent;
26
+ if (Number.isSafeInteger(secondary.resetsAt) && secondary.resetsAt >= 0) usage.planSevenDayResetsAtUnix = secondary.resetsAt;
27
+ }
28
+ let accountCredits;
29
+ if (typeof primary?.usedPercent === 'number' && primary.usedPercent >= 0 && primary.usedPercent <= 100) {
30
+ accountCredits = {
31
+ usedPercent: primary.usedPercent,
32
+ periodType: primary.windowDurationMins === 300 ? 'five_hour' : 'unknown',
33
+ ...(isoFromUnixSeconds(primary.resetsAt) ? { resetsAt: isoFromUnixSeconds(primary.resetsAt) } : {}),
34
+ };
35
+ if (typeof secondary?.usedPercent === 'number' && secondary.usedPercent >= 0 && secondary.usedPercent <= 100) {
36
+ accountCredits.productUsage = [{
37
+ product: secondary.windowDurationMins === 10080 ? '7-day window' : (snapshot.limitName || 'Secondary limit'),
38
+ usagePercent: secondary.usedPercent,
39
+ ...(isoFromUnixSeconds(secondary.resetsAt) ? { resetsAt: isoFromUnixSeconds(secondary.resetsAt) } : {}),
40
+ }];
41
+ }
42
+ const reset = result?.rateLimitResetCredits;
43
+ if (Number.isSafeInteger(reset?.availableCount) && reset.availableCount > 0) {
44
+ const expiresAt = Array.isArray(reset.credits)
45
+ ? reset.credits.map(credit => isoFromUnixSeconds(credit?.expiresAt)).filter(Boolean).slice(0, 32)
46
+ : [];
47
+ accountCredits.resetCredits = {
48
+ availableCount: reset.availableCount,
49
+ ...(expiresAt.length ? { nextExpiresAt: expiresAt[0], expiresAt } : {}),
50
+ };
51
+ }
52
+ }
53
+ return { usage: Object.keys(usage).length ? usage : null, ...(accountCredits ? { accountCredits } : {}) };
54
+ }
55
+
56
+ function accountIdentity(account) {
57
+ const email = account?.type === 'chatgpt' && typeof account.email === 'string' && account.email.trim()
58
+ ? account.email.trim() : undefined;
59
+ const planType = account?.type === 'chatgpt' && PLAN_TYPES.has(account.planType) ? account.planType : undefined;
60
+ return { email, planType, authenticated: !!account };
61
+ }
62
+
63
+ class CodexAccountManager {
64
+ constructor({ dataDirectory, requestOfficial, emit, acquireServer = CodexAppServer.acquire }) {
65
+ this.root = path.resolve(dataDirectory, 'codex-accounts');
66
+ this.profilesRoot = path.join(this.root, 'profiles');
67
+ this.registryFile = path.join(this.root, 'accounts.json');
68
+ this.requestOfficial = requestOfficial;
69
+ this.emit = emit;
70
+ this.acquireServer = acquireServer;
71
+ this.logins = new Map();
72
+ this.registry = this.#load();
73
+ }
74
+
75
+ #load() {
76
+ try {
77
+ const value = JSON.parse(fs.readFileSync(this.registryFile, 'utf8'));
78
+ const seen = new Set();
79
+ const accounts = (Array.isArray(value.accounts) ? value.accounts : []).filter(entry => {
80
+ if (!entry || !ACCOUNT_ID.test(entry.accountId) || typeof entry.label !== 'string' || !entry.label.trim() || entry.accountId === OFFICIAL_ACCOUNT_ID || seen.has(entry.accountId)) return false;
81
+ seen.add(entry.accountId);
82
+ entry.label = entry.label.trim().slice(0, 256);
83
+ return true;
84
+ }).slice(0, 127);
85
+ const activeAccountId = value.activeAccountId === OFFICIAL_ACCOUNT_ID || accounts.some(entry => entry.accountId === value.activeAccountId)
86
+ ? value.activeAccountId : OFFICIAL_ACCOUNT_ID;
87
+ return { version: 1, activeAccountId, accounts };
88
+ } catch (error) {
89
+ if (error.code !== 'ENOENT') throw error;
90
+ return { version: 1, activeAccountId: OFFICIAL_ACCOUNT_ID, accounts: [] };
91
+ }
92
+ }
93
+
94
+ #save() {
95
+ fs.mkdirSync(this.root, { recursive: true });
96
+ const temp = `${this.registryFile}.${process.pid}.tmp`;
97
+ fs.writeFileSync(temp, JSON.stringify(this.registry, null, 2));
98
+ fs.renameSync(temp, this.registryFile);
99
+ }
100
+
101
+ #entry(accountId) {
102
+ if (accountId === OFFICIAL_ACCOUNT_ID) return { accountId, label: 'Codex 官方账号', codexHome: process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), native: true };
103
+ const entry = this.registry.accounts.find(candidate => candidate.accountId === accountId);
104
+ if (!entry) throw new Error('Unknown Codex account');
105
+ const codexHome = path.resolve(this.profilesRoot, entry.accountId);
106
+ if (!codexHome.startsWith(`${path.resolve(this.profilesRoot)}${path.sep}`)) throw new Error('Invalid Codex account directory');
107
+ return { ...entry, codexHome, native: false };
108
+ }
109
+
110
+ async #request(entry, method, params) {
111
+ if (entry.native) {
112
+ if (!this.requestOfficial) throw new Error('Official Codex account service is unavailable');
113
+ return this.requestOfficial(method, params);
114
+ }
115
+ const server = await this.acquireServer(undefined, entry.codexHome);
116
+ try { return await server.request(method, params); }
117
+ finally { server.release(); }
118
+ }
119
+
120
+ async #summary(entry, refreshToken = false) {
121
+ let raw = null;
122
+ try { raw = await this.#request(entry, 'account/read', { refreshToken }); }
123
+ catch { /* A signed-out profile is still a useful selectable Account slot. */ }
124
+ const identity = accountIdentity(raw?.account);
125
+ return {
126
+ accountId: entry.accountId,
127
+ label: identity.email || entry.label,
128
+ ...(identity.email ? { email: identity.email } : {}),
129
+ ...(identity.planType ? { planType: identity.planType } : {}),
130
+ codexHome: entry.codexHome,
131
+ active: this.registry.activeAccountId === entry.accountId,
132
+ isDefault: entry.native,
133
+ authenticated: identity.authenticated,
134
+ management: entry.native ? 'native' : 'isolated',
135
+ };
136
+ }
137
+
138
+ async list(refresh = false) {
139
+ const entries = [this.#entry(OFFICIAL_ACCOUNT_ID), ...this.registry.accounts.map(entry => this.#entry(entry.accountId))];
140
+ return { accounts: await Promise.all(entries.map(entry => this.#summary(entry, refresh))) };
141
+ }
142
+
143
+ async create(label) {
144
+ if (this.registry.accounts.length >= 127) throw new Error('Codex Account limit reached (128 including the official account)');
145
+ const accountId = `account-${randomUUID().slice(0, 8)}`;
146
+ const entry = { accountId, label: typeof label === 'string' && label.trim() ? label.trim().slice(0, 256) : `Codex 账号 ${this.registry.accounts.length + 2}`, createdAt: Date.now() };
147
+ fs.mkdirSync(path.join(this.profilesRoot, accountId), { recursive: true });
148
+ this.registry.accounts.push(entry);
149
+ this.#save();
150
+ return { account: await this.#summary(this.#entry(accountId)) };
151
+ }
152
+
153
+ async activate(accountId) {
154
+ const entry = this.#entry(accountId);
155
+ this.registry.activeAccountId = entry.accountId;
156
+ this.#save();
157
+ return { account: await this.#summary(entry) };
158
+ }
159
+
160
+ async delete(accountId) {
161
+ const entry = this.#entry(accountId);
162
+ if (entry.native) throw new Error('The official Codex account cannot be deleted');
163
+ if ([...this.logins.values()].some(login => login.accountId === accountId)) throw new Error('Cancel account sign-in before deleting it');
164
+ const resolved = path.resolve(entry.codexHome);
165
+ const root = path.resolve(this.profilesRoot);
166
+ if (!resolved.startsWith(`${root}${path.sep}`)) throw new Error('Refusing to delete an account outside the managed profile directory');
167
+ fs.rmSync(resolved, { recursive: true, force: true });
168
+ this.registry.accounts = this.registry.accounts.filter(candidate => candidate.accountId !== accountId);
169
+ if (this.registry.activeAccountId === accountId) this.registry.activeAccountId = OFFICIAL_ACCOUNT_ID;
170
+ this.#save();
171
+ return { deletedAccountId: accountId };
172
+ }
173
+
174
+ async startLogin(accountId) {
175
+ const entry = this.#entry(accountId);
176
+ if (entry.native) {
177
+ const result = await this.#request(entry, 'account/login/start', { type: 'chatgptDeviceCode' });
178
+ return { accountId, loginId: result.loginId, verificationUrl: result.verificationUrl, userCode: result.userCode };
179
+ }
180
+ const server = await this.acquireServer(undefined, entry.codexHome);
181
+ let unwatch;
182
+ try {
183
+ unwatch = server.onNotification(message => {
184
+ if (message?.method !== 'account/login/completed') return;
185
+ const pending = this.logins.get(message.params?.loginId);
186
+ if (!pending || pending.server !== server) return;
187
+ this.logins.delete(message.params.loginId);
188
+ pending.unwatch?.();
189
+ pending.server.release();
190
+ this.emit?.({ method: 'codexhost/account/login/completed', params: {
191
+ accountId: pending.accountId,
192
+ loginId: message.params.loginId,
193
+ success: message.params.success === true,
194
+ error: typeof message.params.error === 'string' ? message.params.error : null,
195
+ } });
196
+ });
197
+ const result = await server.request('account/login/start', { type: 'chatgptDeviceCode' });
198
+ this.logins.set(result.loginId, { accountId, server, unwatch });
199
+ return { accountId, loginId: result.loginId, verificationUrl: result.verificationUrl, userCode: result.userCode };
200
+ } catch (error) {
201
+ unwatch?.();
202
+ server.release();
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ async cancelLogin(loginId) {
208
+ const pending = this.logins.get(loginId);
209
+ if (!pending) {
210
+ if (!this.requestOfficial) return { cancelled: false };
211
+ const result = await this.requestOfficial('account/login/cancel', { loginId });
212
+ return { cancelled: ['canceled', 'cancelled'].includes(result?.status) };
213
+ }
214
+ const result = await pending.server.request('account/login/cancel', { loginId });
215
+ this.logins.delete(loginId);
216
+ pending.unwatch?.();
217
+ pending.server.release();
218
+ return { cancelled: ['canceled', 'cancelled'].includes(result?.status) };
219
+ }
220
+
221
+ async usage(accountId) {
222
+ const entry = this.#entry(accountId);
223
+ return { accountId, ...rateLimitView(await this.#request(entry, 'account/rateLimits/read', {})) };
224
+ }
225
+
226
+ async consumeReset(accountId, idempotencyKey) {
227
+ const entry = this.#entry(accountId);
228
+ const result = await this.#request(entry, 'account/rateLimitResetCredit/consume', { idempotencyKey: idempotencyKey || randomUUID() });
229
+ const refreshed = rateLimitView(await this.#request(entry, 'account/rateLimits/read', {}));
230
+ return { accountId, outcome: result.outcome, ...(refreshed.accountCredits ? { accountCredits: refreshed.accountCredits } : {}) };
231
+ }
232
+
233
+ signedOutOfficial() {
234
+ const entry = this.#entry(OFFICIAL_ACCOUNT_ID);
235
+ return {
236
+ account: {
237
+ accountId: entry.accountId,
238
+ label: entry.label,
239
+ codexHome: entry.codexHome,
240
+ active: this.registry.activeAccountId === entry.accountId,
241
+ isDefault: true,
242
+ authenticated: false,
243
+ management: 'native',
244
+ },
245
+ };
246
+ }
247
+
248
+ executionContext(accountId) {
249
+ const entry = this.#entry(accountId);
250
+ if (entry.native) throw new Error('The official account stays on the native Codex route');
251
+ return { accountId: entry.accountId, codexHome: entry.codexHome };
252
+ }
253
+
254
+ close() {
255
+ for (const pending of this.logins.values()) { pending.unwatch?.(); pending.server.release(); }
256
+ this.logins.clear();
257
+ }
258
+ }
259
+
260
+ module.exports = { CodexAccountManager, OFFICIAL_ACCOUNT_ID, rateLimitView };
@@ -148,6 +148,6 @@ async function runNativeHost() {
148
148
  official.on('error', error => { console.error(error); void close(); });
149
149
  official.on('exit', () => { void close(); });
150
150
  await ready;
151
- console.error('[Harness Mix] native protocol -> local HostRuntime -> local ProtocolCore');
151
+ console.error('[Harness Mix] official Codex passthrough + external Harness routes via HostRuntime/ProtocolCore');
152
152
  }
153
153
  module.exports = { runNativeHost };
@@ -120,7 +120,7 @@ async function launch(args = []) {
120
120
  const paths = nativePaths();
121
121
  for (const file of Object.values(paths)) if (!fs.existsSync(file)) throw new Error(`Missing ${file}; run npm run build:native`);
122
122
  if (flags.has('--check')) {
123
- console.log(`platform=${process.platform}\narchitecture=${process.arch}\ndesktop_version=${installation.version}\ndesktop_compatibility=${compatibility.state}\ndesktop_evidence=${compatibility.evidence?.level || 'none'}\nexecutable_codex_cli=${installation.stock}\nlauncher=${paths.cli}\nshim=${paths.shim}\nruntime=${paths.runtime}\nrenderer=${paths.renderer}\ncore=src/main/protocol-core`);
123
+ console.log(`platform=${process.platform}\narchitecture=${process.arch}\ndesktop_version=${installation.version}\ndesktop_compatibility=${compatibility.state}\ndesktop_evidence=${compatibility.evidence?.level || 'none'}\nexecutable_codex_cli=${installation.stock}\ncodex_mode=official-passthrough\ncodex_managed_route=codex-harness\nlauncher=${paths.cli}\nshim=${paths.shim}\nruntime=${paths.runtime}\nrenderer=${paths.renderer}\ncore=src/main/protocol-core`);
124
124
  return;
125
125
  }
126
126
  const skipUpdate = flags.has('--no-update') || process.env.HARNESS_MIX_AUTO_UPDATE === '0';
@@ -151,7 +151,7 @@ async function launch(args = []) {
151
151
  CODEXHOST_DATA_DIR: env.CODEXHOST_DATA_DIR, HARNESS_MIX_NODE_PATH: process.execPath,
152
152
  CODEXHOST_DEFAULT_AGENT: 'codex' };
153
153
  const block = Buffer.from(Object.entries(overrides).map(([k, v]) => `${k}=${v}`).join('\0') + '\0\0', 'utf16le').toString('base64');
154
- console.log('[Harness Mix] Restarting Codex Desktop with the local Shim and ProtocolCore.');
154
+ console.log('[Harness Mix] Restarting Codex Desktop: official Codex passthrough + Harness Mix routes.');
155
155
  stopDesktopProcesses(installation);
156
156
  await new Promise(resolve => setTimeout(resolve, 1000));
157
157
  sweepLeftovers(root, dataDir);
@@ -21,6 +21,7 @@ const {
21
21
  readState,
22
22
  } = require('./update-state');
23
23
  const { nativeEnvironment } = require('./config');
24
+ const { CodexAccountManager } = require('./codex-accounts');
24
25
 
25
26
  const REPO_ROOT = path.resolve(__dirname, '../../..');
26
27
  const PACKAGE_JSON_PATH = path.join(REPO_ROOT, 'package.json');
@@ -33,7 +34,6 @@ const routeModel = harnessId => ['pi', 'claude-code', 'deepseek-harness', 'antig
33
34
  : `codexhost/plugin-v1@${Buffer.from(JSON.stringify({ harnessId })).toString('hex')}`;
34
35
  const terminal = status => ['completed', 'cancelled', 'error', 'failed'].includes(status);
35
36
  const turnStatus = status => ({ cancelled: 'interrupted', error: 'failed', failed: 'failed', completed: 'completed' }[status] || 'inProgress');
36
-
37
37
  const HARNESS_INSTALL_COMMANDS = {
38
38
  qoder: { win32: 'npm install -g @qoder-ai/qodercli', default: 'npm install -g @qoder-ai/qodercli' },
39
39
  codex: { win32: 'npm install -g @openai/codex', default: 'npm install -g @openai/codex' },
@@ -193,10 +193,16 @@ class NativeProtocol {
193
193
  this.runtime = runtime;
194
194
  this.emit = emit;
195
195
  this.requestOfficial = requestOfficial;
196
+ this.codexAccounts = new CodexAccountManager({
197
+ dataDirectory: nativeEnvironment().CODEXHOST_DATA_DIR,
198
+ requestOfficial,
199
+ emit,
200
+ });
196
201
  this.approvals = new Map();
197
202
  this.published = new Map();
198
203
  this.steering = new Map(); // threadId -> in-flight steer promise
199
204
  this.steerReceipts = new Map(); // `${threadId}\0${clientUserMessageId}` -> bounded delivery receipt
205
+ this.queues = new Map(); // threadId -> Array<{ id, input, clientUserMessageId, createdAt }>
200
206
  this.unsubscribe = runtime.core.subscribe(({ event, projected }) => this.onCore(event, projected));
201
207
  // Host 侧新建的线程(协作子任务等)也要通知 Desktop 侧栏,与 thread/start 同一契约
202
208
  this.unsubscribeRuntime = runtime.subscribe(event => {
@@ -206,6 +212,13 @@ class NativeProtocol {
206
212
  }
207
213
  });
208
214
  }
215
+ getQueue(threadId) {
216
+ if (!this.queues.has(threadId)) this.queues.set(threadId, []);
217
+ return this.queues.get(threadId);
218
+ }
219
+ emitQueueChanged(threadId) {
220
+ this.emit({ method: 'thread/queue/changed', params: { threadId } });
221
+ }
209
222
  thread(id) { return this.runtime.threads.find(t => t.id === id); }
210
223
  owns(id) { return Boolean(this.thread(id)); }
211
224
  turn(turn) { return { id: turn.id, status: turnStatus(turn.status), error: turn.error ? { message: String(turn.error), codexErrorInfo: null, additionalDetails: null } : null,
@@ -318,12 +331,33 @@ class NativeProtocol {
318
331
  return model;
319
332
  }
320
333
  async request(method, params = {}) {
334
+ if (method === 'codexhost/integrations/catalog') return this.runtime.integrations.catalog();
335
+ if (method === 'codexhost/integrations/list') return this.runtime.integrations.list(params);
336
+ if (method === 'codexhost/integrations/mcp/save') return this.runtime.integrations.save(params);
337
+ if (method === 'codexhost/integrations/mcp/remove') return this.runtime.integrations.remove(params);
338
+ if (method === 'codexhost/integrations/skill/change') return this.runtime.integrations.skillChange(params);
321
339
  if (method === 'codexhost/harness/session-import/sources') return this.runtime.history.sources();
322
340
  if (method === 'codexhost/harness/session-import/list') return this.runtime.history.list(params);
323
341
  if (method === 'codexhost/harness/session-import/import') return this.runtime.history.import(params);
324
342
  if (method === 'codexhost/collaboration/agents') return [...this.runtime.adapters.values()].map(a => ({ id: externalId(a.manifest.id), name: a.manifest.name, available: !!this.runtime.status[a.manifest.id]?.available, lead: a.manifest.capabilities?.collaborationTools === true }));
325
343
  const thread = this.thread(params.threadId);
326
- if (method === 'harness-mix/runtime/inspect') return { owner: 'harness-mix', runtime: 'src/main/host/runtime.js', core: 'src/main/protocol-core/protocol-core.js', threads: this.runtime.threads.length };
344
+ if (method === 'harness-mix/runtime/inspect') return {
345
+ owner: 'harness-mix',
346
+ runtime: 'src/main/host/runtime.js',
347
+ core: 'src/main/protocol-core/protocol-core.js',
348
+ threads: this.runtime.threads.length,
349
+ codex: {
350
+ mode: 'official-passthrough',
351
+ // `codex` remains owned by the stock app-server. Only the explicit
352
+ // `codex-harness` route creates a HostRuntime-managed Codex worker.
353
+ managedRoute: 'codex-harness',
354
+ },
355
+ };
356
+ if (method === 'harness-mix/runtime/version') {
357
+ let version = '0.0.0';
358
+ try { version = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8')).version || version; } catch {}
359
+ return { version };
360
+ }
327
361
  // Updates
328
362
  if (method === 'codexhost/update/check') {
329
363
  const dataDir = nativeEnvironment().CODEXHOST_DATA_DIR;
@@ -429,8 +463,22 @@ class NativeProtocol {
429
463
  return { status: null };
430
464
  }
431
465
 
432
- // No additional managed accounts: native Codex keeps its own signed-in account.
433
- if (method === 'codexhost/account/list' || method === 'codexhost/account/refresh') return { accounts: [] };
466
+ // Each extra Account is an isolated native CODEX_HOME. Harness Mix stores
467
+ // only the profile label/path; auth.json and token refresh remain owned by Codex.
468
+ if (method === 'codexhost/account/list') return this.codexAccounts.list(false);
469
+ if (method === 'codexhost/account/refresh') return this.codexAccounts.list(true);
470
+ if (method === 'codexhost/account/create') return this.codexAccounts.create(params.label);
471
+ if (method === 'codexhost/account/delete') return this.codexAccounts.delete(params.accountId);
472
+ if (method === 'codexhost/account/activate') return this.codexAccounts.activate(params.accountId);
473
+ if (method === 'codexhost/account/usage/inspect') return this.codexAccounts.usage(params.accountId);
474
+ if (method === 'codexhost/account/login/start') return this.codexAccounts.startLogin(params.accountId);
475
+ if (method === 'codexhost/account/login/cancel') return this.codexAccounts.cancelLogin(params.loginId);
476
+ if (method === 'codexhost/account/logout') {
477
+ if (!this.requestOfficial) throw new Error('Official Codex logout is unavailable');
478
+ await this.requestOfficial('account/logout', undefined);
479
+ return this.codexAccounts.signedOutOfficial();
480
+ }
481
+ if (method === 'codexhost/account/rate-limit-reset/consume') return this.codexAccounts.consumeReset(params.accountId, params.idempotencyKey);
434
482
  if (method === 'codexhost/harness/account/login') {
435
483
  const extId = params.harnessId;
436
484
  const meta = HARNESS_AUTH_INFO[extId] || HARNESS_AUTH_INFO[ALIASES[extId]];
@@ -571,15 +619,22 @@ class NativeProtocol {
571
619
  }
572
620
  if (method === 'thread/start') {
573
621
  const route = decodeRoute(params.model);
574
- if (!route) return undefined;
575
- const id = ALIASES[route.harnessId] || route.harnessId;
622
+ const accountContext = typeof params.__codexhostAccountId === 'string'
623
+ ? this.codexAccounts.executionContext(params.__codexhostAccountId) : null;
624
+ if (!route && !accountContext) return undefined;
625
+ const effectiveRoute = route || { harnessId: 'codex-harness', ...(typeof params.model === 'string' ? { model: { id: params.model } } : {}) };
626
+ const id = ALIASES[effectiveRoute.harnessId] || effectiveRoute.harnessId;
576
627
  if (!this.runtime.adapters.has(id)) throw new Error(`Unsupported Harness: ${id}`);
577
628
  if (params.ephemeral && params.threadSource) throw new Error('Ephemeral background thread is not supported');
578
629
  const isWorktree = params.worktree === true || params.options?.worktree === true;
630
+ const selectedModel = accountContext && typeof params.model === 'string'
631
+ ? (await this.runtime.describe(id)).models.find(model => model.id === params.model)
632
+ : await this.resolveModel(id, effectiveRoute.model);
579
633
  const created = await this.runtime.createThread({ harnessId: id, cwd: params.cwd, ephemeral: params.ephemeral === true,
580
634
  worktree: isWorktree,
581
635
  options: {
582
- model: await this.resolveModel(id, route.model), thinking: route.thinkingOptionId, permissionMode: route.permissionModeId,
636
+ model: selectedModel, thinking: effectiveRoute.thinkingOptionId, permissionMode: effectiveRoute.permissionModeId,
637
+ ...(accountContext || {}),
583
638
  ...(isWorktree ? { worktree: true } : {}),
584
639
  } });
585
640
  if (created.error) throw new Error(created.error);
@@ -597,9 +652,83 @@ class NativeProtocol {
597
652
  return undefined;
598
653
  }
599
654
  if (method === 'thread/read') return { thread: this.projectThread(thread, params.includeTurns !== false) };
600
- // External sessions currently accept immediate turns only, so their Desktop
601
- // submission queue is empty. Do not forward their IDs to the stock server.
602
- if (method === 'thread/queue/list') return { data: [], nextCursor: null };
655
+ if (method === 'thread/queue/list') {
656
+ const queue = this.getQueue(thread.id);
657
+ return {
658
+ data: queue.map(item => ({
659
+ id: item.id,
660
+ input: item.input,
661
+ clientUserMessageId: item.clientUserMessageId,
662
+ createdAt: item.createdAt,
663
+ })),
664
+ nextCursor: null,
665
+ };
666
+ }
667
+ if (method === 'thread/queue/add') {
668
+ const input = Array.isArray(params.input) ? params.input : [];
669
+ if (!input.length) throw new Error('Queue submission requires non-empty input');
670
+ const queue = this.getQueue(thread.id);
671
+ const submission = {
672
+ id: randomUUID(),
673
+ input,
674
+ clientUserMessageId: typeof params.clientUserMessageId === 'string' && params.clientUserMessageId ? params.clientUserMessageId : null,
675
+ createdAt: Date.now(),
676
+ };
677
+ queue.push(submission);
678
+ this.emitQueueChanged(thread.id);
679
+ return { queuedSubmission: submission };
680
+ }
681
+ if (method === 'thread/queue/delete') {
682
+ const queue = this.getQueue(thread.id);
683
+ const idx = queue.findIndex(item => item.id === params.queuedSubmissionId);
684
+ if (idx !== -1) {
685
+ queue.splice(idx, 1);
686
+ this.emitQueueChanged(thread.id);
687
+ return { deleted: true };
688
+ }
689
+ return { deleted: false };
690
+ }
691
+ if (method === 'thread/queue/update') {
692
+ const queue = this.getQueue(thread.id);
693
+ const item = queue.find(it => it.id === params.queuedSubmissionId);
694
+ if (!item) throw new Error('Queued submission not found');
695
+ if (Array.isArray(params.input)) item.input = params.input;
696
+ this.emitQueueChanged(thread.id);
697
+ return { queuedSubmission: item };
698
+ }
699
+ if (method === 'thread/queue/reorder') {
700
+ const queue = this.getQueue(thread.id);
701
+ const ids = Array.isArray(params.queuedSubmissionIds) ? params.queuedSubmissionIds : [];
702
+ const map = new Map(queue.map(item => [item.id, item]));
703
+ const reordered = ids.map(id => map.get(id)).filter(Boolean);
704
+ for (const item of queue) {
705
+ if (!ids.includes(item.id)) reordered.push(item);
706
+ }
707
+ this.queues.set(thread.id, reordered);
708
+ this.emitQueueChanged(thread.id);
709
+ return {};
710
+ }
711
+ if (method === 'thread/queue/start') {
712
+ const queue = this.getQueue(thread.id);
713
+ const idx = params.queuedSubmissionId
714
+ ? queue.findIndex(item => item.id === params.queuedSubmissionId)
715
+ : 0;
716
+ if (idx === -1 || !queue[idx]) throw new Error('Queued submission not found');
717
+ const [submission] = queue.splice(idx, 1);
718
+ this.emitQueueChanged(thread.id);
719
+ const { text, attachments } = await prepareInput(submission.input, thread.cwd);
720
+ if (attachments.length && !this.runtime.getCapabilities(thread.harnessId).conversation.attachments) {
721
+ throw new Error('当前 Harness 不支持图片附件');
722
+ }
723
+ if (this.runtime.execution.isRunning(thread.id)) {
724
+ // 正在执行中:打断当前回合并立即执行该排队消息
725
+ const turnId = await this.steerExclusive(thread, thread.currentTurn?.id, text, attachments);
726
+ const started = this.runtime.core.getTurn(turnId) || thread.currentTurn;
727
+ return { turn: this.turn(started) };
728
+ }
729
+ const turn = await this.startNativeTurn(thread, text, attachments);
730
+ return { turn: this.turn(turn) };
731
+ }
603
732
  if (method === 'thread/metadata/update') return { thread: this.projectThread(await this.runtime.updateThreadMetadata(thread.id, params.gitInfo)) };
604
733
  if (method === 'thread/section/move') {
605
734
  if (params.sectionId !== null && (typeof params.sectionId !== 'string' || !params.sectionId)) throw new Error('Invalid sectionId');
@@ -660,7 +789,17 @@ class NativeProtocol {
660
789
  }
661
790
  if (method === 'thread/rollback') return { thread: this.projectThread(await this.runtime.rollbackThread(thread.id, params.numTurns)) };
662
791
  if (method === 'thread/name/set') { await this.runtime.renameThread(thread.id, params.name); return {}; }
663
- if (method === 'thread/archive' || method === 'thread/unarchive') { await this.runtime.setThreadArchived(thread.id, method === 'thread/archive'); return method === 'thread/archive' ? {} : { thread: this.projectThread(thread) }; }
792
+ if (method === 'thread/archive' || method === 'thread/unarchive') {
793
+ const isArchive = method === 'thread/archive';
794
+ await this.runtime.setThreadArchived(thread.id, isArchive);
795
+ if (isArchive) {
796
+ this.emit({ method: 'thread/archived', params: { threadId: thread.id } });
797
+ return {};
798
+ }
799
+ const projected = this.projectThread(thread);
800
+ this.emit({ method: 'thread/unarchived', params: { thread: projected } });
801
+ return { thread: projected };
802
+ }
664
803
  if (method === 'thread/loaded/list') return { data: [...this.runtime.sessions.keys()] };
665
804
  if (method === 'codexhost/thread/usage/inspect') {
666
805
  const usage = params.refresh === 'exact' ? await this.runtime.refreshUsage(thread.id) : this.runtime.core.getThread(thread.id)?.usage;
@@ -697,8 +836,12 @@ class NativeProtocol {
697
836
  }
698
837
  // 原地切换 Harness:会话历史保留,下条消息携带一次性上下文信封(/switch 指令的 RPC 等价物)
699
838
  if (method === 'codexhost/thread/harness/switch') {
700
- await this.runtime.switchHarness(thread.id, ALIASES[params.harnessId] || params.harnessId, { note: typeof params.note === 'string' ? params.note : undefined });
701
- return { threadId: thread.id };
839
+ await this.runtime.switchHarness(thread.id, ALIASES[params.harnessId] || params.harnessId, {
840
+ note: typeof params.note === 'string' ? params.note : undefined,
841
+ intent: params.intent,
842
+ includes: params.includes,
843
+ });
844
+ return { threadId: thread.id, checkpointId: thread.pendingHandoff.checkpointId };
702
845
  }
703
846
  if (method === 'thread/fork' || method === 'codexhost/thread/fork') {
704
847
  if (params.ephemeral || params.threadSource || params.excludeTurns) {
@@ -760,6 +903,25 @@ class NativeProtocol {
760
903
  if (Array.isArray(projected.turn.itemIds)) {
761
904
  for (const id of projected.turn.itemIds) this.published.delete(id);
762
905
  }
906
+ for (const [id, app] of this.approvals.entries()) {
907
+ if (app.threadId === threadId) this.approvals.delete(id);
908
+ }
909
+ if (projected.turn.status === 'completed') {
910
+ setTimeout(async () => {
911
+ const queue = this.getQueue(threadId);
912
+ const currentThread = this.thread(threadId);
913
+ if (queue.length > 0 && currentThread && !this.runtime.execution.isRunning(threadId) && !this.steering.has(threadId) && !this.runtime.sending?.has(threadId)) {
914
+ const [submission] = queue.splice(0, 1);
915
+ this.emitQueueChanged(threadId);
916
+ try {
917
+ const { text, attachments } = await prepareInput(submission.input, currentThread.cwd);
918
+ await this.startNativeTurn(currentThread, text, attachments);
919
+ } catch (err) {
920
+ console.error(`Failed to auto-drain queued submission for thread ${threadId}:`, err);
921
+ }
922
+ }
923
+ }, 2500);
924
+ }
763
925
  }
764
926
  }
765
927
  async startNativeTurn(thread, text, attachments, commandId) {
@@ -831,6 +993,6 @@ class NativeProtocol {
831
993
  this.approvals.delete(message.id);
832
994
  return true;
833
995
  }
834
- close() { this.unsubscribe(); this.unsubscribeRuntime(); }
996
+ close() { this.codexAccounts.close(); this.unsubscribe(); this.unsubscribeRuntime(); }
835
997
  }
836
998
  module.exports = { NativeProtocol, decodeRoute, projectItem, externalId, routeModel };
@@ -8,7 +8,7 @@ const PATTERNS = [
8
8
  /xox[abpsr]-[A-Za-z0-9-]{10,}/g,
9
9
  /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, // JWT
10
10
  ];
11
- const SECRET_KEY = /(token|secret|password|passwd|api[_-]?key|authorization|credential|cookie)/i;
11
+ const SECRET_KEY = /(token|secret|password|passwd|api[_-]?key|authorization|credential|cookie|userCode|loginId|verificationUrl)/i;
12
12
  const REDACTED = '[redacted]';
13
13
 
14
14
  function redactText(value) {