@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-mix/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Harness Mix native Codex UI bridge for Codex, Pi, Claude Code, DeepSeek Harness, Antigravity, CodeBuddy, Kiro CLI, Cursor CLI and other native coding harnesses.",
6
6
  "keywords": [
@@ -30,7 +30,8 @@
30
30
  "harness-mix": "scripts/launch-codex.cjs"
31
31
  },
32
32
  "publishConfig": {
33
- "access": "public"
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org"
34
35
  },
35
36
  "postinstall": "node scripts/install-native.cjs",
36
37
  "files": [
@@ -69,6 +70,9 @@
69
70
  "node": ">=22.19.0 <23 || >=24 <25"
70
71
  },
71
72
  "scripts": {
73
+ "test:integrations": "node scripts/integrations-test.cjs",
74
+ "smoke:integrations-ui": "electron scripts/integrations-ui-smoke.cjs",
75
+ "e2e:integrations": "node scripts/e2e-integrations.cjs",
72
76
  "health:harnesses": "node scripts/harness-health-check.cjs",
73
77
  "test:dsh-collaboration-acp": "node scripts/dsh-collaboration-acp-test.cjs",
74
78
  "test:codebuddy-migration": "node scripts/codebuddy-migration-test.cjs",
@@ -82,6 +86,7 @@
82
86
  "test:collaboration": "node scripts/collaboration-test.cjs",
83
87
  "test:switch-harness": "node scripts/switch-harness-test.cjs",
84
88
  "smoke:collaboration-ui": "electron scripts/collaboration-ui-smoke.cjs",
89
+ "smoke:chatgpt-context-ui": "electron scripts/chatgpt-context-ui-smoke.cjs",
85
90
  "e2e:collaboration": "node scripts/e2e-collaboration.cjs",
86
91
  "test:native-rollback": "node scripts/native-rollback-test.cjs",
87
92
  "test:native-images": "node scripts/native-images-test.cjs",
@@ -95,6 +100,7 @@
95
100
  "test:native-config": "node scripts/native-config-test.cjs",
96
101
  "test:native-compatibility": "node scripts/native-compatibility-test.cjs",
97
102
  "e2e:native": "node scripts/e2e-native-host.cjs",
103
+ "e2e:native:codex": "node scripts/e2e-native-host.cjs --live-codex",
98
104
  "e2e:native:pi": "node scripts/e2e-native-host.cjs --live --harness=pi",
99
105
  "e2e:native:dsh": "node scripts/e2e-native-host.cjs --live --harness=deepseek-harness",
100
106
  "e2e:native:claude": "node scripts/e2e-native-host.cjs --live --harness=claude-code",
@@ -114,7 +120,10 @@
114
120
  "test:openclaw-adapter": "node scripts/openclaw-adapter-test.cjs",
115
121
  "test:core-concurrency": "node scripts/core-concurrency-test.cjs",
116
122
  "test:thread-title": "node scripts/thread-title-test.cjs",
117
- "test:core-all": "node scripts/contracts-test.cjs && node scripts/projector-test.cjs && node scripts/turn-manager-test.cjs && node scripts/sequence-test.cjs && node scripts/shadow-test.cjs && node scripts/architecture-test.cjs && node scripts/adapters-test.cjs && node scripts/dsh-adapter-test.cjs && node scripts/codex-adapter-test.cjs && node scripts/antigravity-adapter-test.cjs && node scripts/openclaw-adapter-test.cjs && node scripts/core-replay-test.cjs && node scripts/determinism-test.cjs && node scripts/core-runtime-test.cjs && node scripts/core-services-test.cjs && node scripts/native-file-change-test.cjs && node scripts/native-updater-test.cjs && node scripts/native-update-apply-test.cjs && node scripts/native-vendor-adapters-test.cjs && node scripts/core-concurrency-test.cjs && node scripts/concurrency-layers-test.cjs && node scripts/tool-concurrency-regression-test.cjs && node scripts/switch-harness-test.cjs && node scripts/native-acp-depth-test.cjs && node scripts/thread-title-test.cjs && node scripts/native-job-object-test.cjs",
123
+ "test:handoff": "node scripts/handoff-checkpoint-test.cjs && node scripts/switch-harness-test.cjs",
124
+ "test:core-all": "node scripts/contracts-test.cjs && node scripts/projector-test.cjs && node scripts/turn-manager-test.cjs && node scripts/sequence-test.cjs && node scripts/shadow-test.cjs && node scripts/architecture-test.cjs && node scripts/adapters-test.cjs && node scripts/dsh-adapter-test.cjs && node scripts/codex-adapter-test.cjs && node scripts/codex-accounts-test.cjs && node scripts/antigravity-adapter-test.cjs && node scripts/openclaw-adapter-test.cjs && node scripts/core-replay-test.cjs && node scripts/determinism-test.cjs && node scripts/core-runtime-test.cjs && node scripts/core-services-test.cjs && node scripts/native-file-change-test.cjs && node scripts/native-updater-test.cjs && node scripts/native-update-apply-test.cjs && node scripts/native-vendor-adapters-test.cjs && node scripts/core-concurrency-test.cjs && node scripts/concurrency-layers-test.cjs && node scripts/tool-concurrency-regression-test.cjs && node scripts/handoff-checkpoint-test.cjs && node scripts/switch-harness-test.cjs && node scripts/native-acp-depth-test.cjs && node scripts/thread-title-test.cjs && node scripts/native-job-object-test.cjs && node scripts/integrations-test.cjs",
125
+ "test:codex-accounts": "node scripts/codex-accounts-test.cjs",
126
+ "smoke:codex-accounts-ui": "electron scripts/codex-accounts-ui-smoke.cjs",
118
127
  "test:transcript": "node scripts/transcript-test.cjs",
119
128
  "test:workspace": "node scripts/workspace-test.cjs",
120
129
  "e2e:pi": "node scripts/e2e-pi.cjs",
@@ -151,7 +160,8 @@
151
160
  "test:collaboration-recovery": "node scripts/collaboration-recovery-test.cjs",
152
161
  "test:tool-concurrency": "node scripts/tool-concurrency-regression-test.cjs",
153
162
  "test:platform": "node scripts/platform-test.cjs",
154
- "test:shim": "node scripts/native-shim-test.cjs"
163
+ "test:shim": "node scripts/native-shim-test.cjs",
164
+ "publish:native": "node scripts/publish-native-package.cjs"
155
165
  },
156
166
  "devDependencies": {
157
167
  "@types/node": "24.13.3",
@@ -167,11 +177,11 @@
167
177
  "zod": "4.4.3"
168
178
  },
169
179
  "optionalDependencies": {
170
- "@harness-mix/native-win32-x64": "0.1.4",
171
- "@harness-mix/native-win32-arm64": "0.1.4",
172
- "@harness-mix/native-darwin-x64": "0.1.4",
173
- "@harness-mix/native-darwin-arm64": "0.1.4",
174
- "@harness-mix/native-linux-x64": "0.1.4",
175
- "@harness-mix/native-linux-arm64": "0.1.4"
180
+ "@harness-mix/native-win32-x64": "0.1.6",
181
+ "@harness-mix/native-win32-arm64": "0.1.6",
182
+ "@harness-mix/native-darwin-x64": "0.1.6",
183
+ "@harness-mix/native-darwin-arm64": "0.1.6",
184
+ "@harness-mix/native-linux-x64": "0.1.6",
185
+ "@harness-mix/native-linux-arm64": "0.1.6"
176
186
  }
177
187
  }
@@ -0,0 +1,16 @@
1
+ const { app, BrowserWindow } = require('electron');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ app.disableHardwareAcceleration();
6
+ app.whenReady().then(async () => {
7
+ const bundle = await require('esbuild').build({ stdin: { contents: "export * from './src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts';", resolveDir: process.cwd() }, bundle: true, platform: 'browser', format: 'iife', globalName: 'ChatGptContext', write: false });
8
+ const win = new BrowserWindow({ show: false, width: 1100, height: 720, webPreferences: { sandbox: true, contextIsolation: true, offscreen: true } });
9
+ try {
10
+ const html = `<!doctype html><meta charset="utf-8"><style>body{margin:0;font:14px system-ui}.composer{position:fixed;left:40px;bottom:36px;width:52%;padding:14px;border:1px solid #ccc;border-radius:14px}.composer textarea{width:100%;height:110px}.quick{position:fixed;right:12px;top:60px;bottom:12px;width:38%;border:1px solid #ddd;border-radius:22px;background:white;overflow:hidden}.quick header{height:54px;display:flex;align-items:center;padding:0 18px;border-bottom:1px solid #eee}.turns{padding:24px}.turn{margin:16px 0;padding:12px;background:#f3f3f0}.assistant{background:#eef5ff}.chat-composer{position:absolute;left:16px;right:16px;bottom:16px}</style><main><div class="composer"><textarea id="draft" placeholder="发送消息"></textarea></div></main><aside class="quick" data-chatgpt-quick-chat-panel><header><span>聊天</span></header><a data-chat-title aria-current="page" href="https://chatgpt.com/c/design?private=1">额度设计讨论</a><section class="turns"><div class="turn" data-message-author-role="user">帮我设计多账号</div><div class="turn assistant" data-message-author-role="assistant">账号应绑定原生运行时。sk-abcdefghijklmnopqrstuvwxyz123456</div></section><textarea class="chat-composer" placeholder="给 ChatGPT 发消息"></textarea></aside>`;
11
+ await win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html)); await win.webContents.executeJavaScript(bundle.outputFiles[0].text);
12
+ const result = await win.webContents.executeJavaScript(`(() => { const api=ChatGptContext.installChatGptContextBridge({locale:()=> 'zh-CN',resolveTarget:()=>({editor:document.querySelector('#draft'),kind:'codex',label:'Codex',accountLabel:'work@example.com'})}); document.querySelector('[data-harness-mix-chatgpt-context-action]').click(); const value=document.querySelector('#draft').value; const status=document.querySelector('[data-harness-mix-chatgpt-context-status]').textContent; document.querySelector('[data-harness-mix-chatgpt-context-action]').click(); const duplicate=document.querySelector('[data-harness-mix-chatgpt-context-status]').textContent; globalThis.chatGptContextApi=api; return {value,status,duplicate}; })()`);
13
+ assert.match(result.value, /不可信历史资料/); assert.match(result.value, /User: 帮我设计多账号/); assert.match(result.value, /Assistant: 账号应绑定原生运行时/); assert.doesNotMatch(result.value, /sk-abcdefghijklmnopqrstuvwxyz/); assert.match(result.status, /Codex(work@example.com)/); assert.match(result.duplicate, /已经包含/);
14
+ const out = path.resolve('output/chatgpt-context-ui'); await fs.mkdir(out, { recursive: true }); await fs.writeFile(path.join(out, 'add-to-task.png'), (await win.webContents.capturePage()).toPNG()); await win.webContents.executeJavaScript('chatGptContextApi.dispose()'); assert.equal(await win.webContents.executeJavaScript('document.querySelectorAll("[data-harness-mix-chatgpt-context-action]").length'), 0); console.log('PASS: Quick chat context button, target account, redaction, draft insertion, duplicate guard and cleanup');
15
+ } finally { win.destroy(); app.quit(); }
16
+ }).catch((error) => { console.error(error); app.exit(1); });
@@ -0,0 +1,81 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const { CodexAccountManager } = require('../src/main/native/codex-accounts');
6
+
7
+ async function main() {
8
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-mix-codex-accounts-'));
9
+ const servers = new Map();
10
+ const emitted = [];
11
+ const acquireServer = async (_diagnostic, codexHome) => {
12
+ let server = servers.get(codexHome);
13
+ if (!server) {
14
+ const listeners = new Set();
15
+ server = {
16
+ refs: 0,
17
+ account: null,
18
+ requests: [],
19
+ async request(method, params) {
20
+ this.requests.push({ method, params });
21
+ if (method === 'account/read') return { account: this.account };
22
+ if (method === 'account/rateLimits/read') return { rateLimits: { primary: { usedPercent: 24, windowDurationMins: 300, resetsAt: 1_800_000_000 }, secondary: { usedPercent: 41, windowDurationMins: 10080, resetsAt: 1_800_604_800 } } };
23
+ if (method === 'account/login/start') return { type: 'chatgptDeviceCode', loginId: 'login-isolated', verificationUrl: 'https://auth.example/device', userCode: 'WXYZ-1234' };
24
+ if (method === 'account/login/cancel') return { status: 'cancelled' };
25
+ throw new Error(`Unexpected fake Account request: ${method}`);
26
+ },
27
+ onNotification(listener) { listeners.add(listener); return () => listeners.delete(listener); },
28
+ notify(message) { for (const listener of listeners) listener(message); },
29
+ release() { this.refs--; },
30
+ };
31
+ servers.set(codexHome, server);
32
+ }
33
+ server.refs++;
34
+ return server;
35
+ };
36
+ const official = async (method) => {
37
+ if (method === 'account/read') return { account: { type: 'chatgpt', email: 'owner@example.com', planType: 'plus' } };
38
+ throw new Error(`Unexpected official request: ${method}`);
39
+ };
40
+ const manager = new CodexAccountManager({ dataDirectory: root, requestOfficial: official, emit: message => emitted.push(message), acquireServer });
41
+ try {
42
+ const initial = await manager.list();
43
+ assert.equal(initial.accounts.length, 1);
44
+ assert.equal(initial.accounts[0].email, 'owner@example.com');
45
+ assert.equal(initial.accounts[0].management, 'native');
46
+
47
+ const created = await manager.create('工作账号');
48
+ assert.equal(created.account.management, 'isolated');
49
+ assert.equal(created.account.authenticated, false);
50
+ const context = manager.executionContext(created.account.accountId);
51
+ assert.ok(context.codexHome.startsWith(path.join(root, 'codex-accounts', 'profiles') + path.sep));
52
+ const server = servers.get(context.codexHome);
53
+ server.account = { type: 'chatgpt', email: 'work@example.com', planType: 'pro' };
54
+
55
+ const activated = await manager.activate(created.account.accountId);
56
+ assert.equal(activated.account.active, true);
57
+ const accounts = await manager.list(true);
58
+ assert.equal(accounts.accounts.find(account => account.isDefault).active, false);
59
+ assert.equal(accounts.accounts.find(account => account.accountId === created.account.accountId).email, 'work@example.com');
60
+ const usage = await manager.usage(created.account.accountId);
61
+ assert.equal(usage.usage.planFiveHourUsedPercent, 24);
62
+ assert.equal(usage.accountCredits.productUsage[0].usagePercent, 41);
63
+
64
+ const login = await manager.startLogin(created.account.accountId);
65
+ assert.equal(login.userCode, 'WXYZ-1234');
66
+ server.notify({ method: 'account/login/completed', params: { loginId: login.loginId, success: true, error: null } });
67
+ assert.deepEqual(emitted.at(-1), { method: 'codexhost/account/login/completed', params: { accountId: created.account.accountId, loginId: login.loginId, success: true, error: null } });
68
+
69
+ const registry = fs.readFileSync(path.join(root, 'codex-accounts', 'accounts.json'), 'utf8');
70
+ assert.equal(registry.includes('owner@example.com'), false, 'Registry must not persist native identity or credentials');
71
+ assert.equal(registry.includes('work@example.com'), false, 'Registry must not persist isolated identity or credentials');
72
+ await manager.delete(created.account.accountId);
73
+ assert.equal(fs.existsSync(context.codexHome), false);
74
+ } finally {
75
+ manager.close();
76
+ fs.rmSync(root, { recursive: true, force: true });
77
+ }
78
+ console.log('Codex multi-account isolation, switching, usage and login lifecycle passed');
79
+ }
80
+
81
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,70 @@
1
+ const { app, BrowserWindow } = require('electron');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const esbuild = require('esbuild');
6
+
7
+ app.disableHardwareAcceleration();
8
+
9
+ app.whenReady().then(async () => {
10
+ const output = path.resolve('output/codex-accounts-ui-smoke');
11
+ fs.mkdirSync(output, { recursive: true });
12
+ const bundle = await esbuild.build({
13
+ stdin: {
14
+ contents: `export { createAccountsSettingsPage } from './src/native-ui/renderer-extension/src/settings/accounts-page.ts'; export { rendererSettingsMessages } from './src/native-ui/renderer-extension/src/settings/localization.ts'; export { installRendererSettingsShell } from './src/native-ui/renderer-extension/src/settings/shell.ts';`,
15
+ resolveDir: process.cwd(),
16
+ },
17
+ bundle: true,
18
+ alias: { '@codexhost/shared-contracts': path.resolve('src/native-ui/shared-contracts/src/index.ts') },
19
+ platform: 'browser',
20
+ format: 'iife',
21
+ globalName: 'AccountUI',
22
+ write: false,
23
+ loader: { '.svg': 'dataurl', '.png': 'dataurl', '.css': 'text' },
24
+ logLevel: 'silent',
25
+ });
26
+ const win = new BrowserWindow({ show: false, width: 1080, height: 720, webPreferences: { contextIsolation: true, sandbox: true, offscreen: true, backgroundThrottling: false } });
27
+ await win.loadURL('data:text/html,<html><body></body></html>');
28
+ await win.webContents.executeJavaScript(bundle.outputFiles[0].text);
29
+ const result = await win.webContents.executeJavaScript(`(async () => {
30
+ const accounts = [
31
+ { accountId:'official-codex', label:'owner@example.com', email:'owner@example.com', planType:'plus', codexHome:'C:/Users/test/.codex', active:false, isDefault:true, authenticated:true, management:'native' },
32
+ { accountId:'account-work', label:'work@example.com', email:'work@example.com', planType:'pro', codexHome:'C:/Profiles/work', active:true, isDefault:false, authenticated:true, management:'isolated' },
33
+ ];
34
+ const usage = {
35
+ 'official-codex': { usedPercent:31, periodType:'five_hour', resetsAt:'2026-09-14T03:00:00.000Z', productUsage:[{product:'7-day window',usagePercent:22,resetsAt:'2026-09-20T03:00:00.000Z'}] },
36
+ 'account-work': { usedPercent:68, periodType:'five_hour', resetsAt:'2026-09-14T05:00:00.000Z', productUsage:[{product:'7-day window',usagePercent:47,resetsAt:'2026-09-19T05:00:00.000Z'}] },
37
+ };
38
+ const client = {
39
+ listCodexAccounts: async () => ({accounts}), refreshCodexAccounts: async () => ({accounts}),
40
+ inspectCodexAccountUsage: async ({accountId}) => ({accountId,usage:null,accountCredits:usage[accountId]}),
41
+ createCodexAccount: async () => ({account:accounts[1]}), deleteCodexAccount: async ({accountId}) => ({deletedAccountId:accountId}),
42
+ activateCodexAccount: async ({accountId}) => ({account:accounts.find(account => account.accountId===accountId)}),
43
+ startCodexAccountLogin: async () => { throw new Error('not expected'); }, cancelCodexAccountLogin: async () => ({cancelled:true}),
44
+ logoutCodexAccount: async () => ({account:{...accounts[0],email:undefined,authenticated:false}}),
45
+ };
46
+ const messages=AccountUI.rendererSettingsMessages('zh-CN');
47
+ const shell=AccountUI.installRendererSettingsShell([AccountUI.createAccountsSettingsPage(messages,()=>client)],messages,document);
48
+ shell.openSettings(undefined,'accounts');
49
+ await new Promise(resolve=>setTimeout(resolve,250));
50
+ const shadow=shell.root.shadowRoot;
51
+ const content=shadow.querySelector('.settings-page__content');
52
+ const text=content.textContent;
53
+ const add=[...shadow.querySelectorAll('button')].find(button=>button.textContent.includes('添加账号'));
54
+ const rows=[...content.querySelectorAll('[data-account-id]')].map(row=>row.getAttribute('data-account-id'));
55
+ return {text,addVisible:add && !add.hidden,rows,pathsVisible:text.includes('C:/Profiles')||text.includes('.codex')};
56
+ })()`);
57
+ assert.equal(result.addVisible, true);
58
+ assert.deepEqual(result.rows, ['official-codex', 'account-work']);
59
+ assert.match(result.text, /owner.*example\.com/s);
60
+ assert.match(result.text, /work.*example\.com/s);
61
+ assert.match(result.text, /Plus/);
62
+ assert.match(result.text, /Pro/);
63
+ assert.match(result.text, /5 小时|5小时/);
64
+ assert.match(result.text, /7 天|7天/);
65
+ assert.equal(result.pathsVisible, false, result.text);
66
+ fs.writeFileSync(path.join(output, 'codex-multi-account.png'), (await win.webContents.capturePage()).toPNG());
67
+ win.destroy();
68
+ console.log('PASS: Codex multi-account settings, quota windows, refresh times and hidden CODEX_HOME render in Electron');
69
+ app.quit();
70
+ }).catch(error => { console.error(error); app.exit(1); });
@@ -29,10 +29,11 @@ async function main() {
29
29
  const call = (name, args) => rt.collaboration.call(parent.id, name, args);
30
30
  const finish = (id, answer) => { const { s } = pending.get(id); pending.delete(id); active--; s.emit({ kind: 'text-delta', text: answer }); s.emit({ kind: 'completed', finalAnswer: true }); };
31
31
  try {
32
- assert.deepEqual(mentionedAgents('ask @w and [Worker](harness-mix://agent/worker) `@lead` mail@lead.com @worker/foo', rt), ['worker']);
32
+ assert.deepEqual(mentionedAgents('ask #w and [Worker](harness-mix://agent/worker) `#lead` issue#lead #worker/foo', rt), ['worker']);
33
+ assert.deepEqual(mentionedAgents('leave @w to native Codex mentions', rt), []);
33
34
  await assert.rejects(call('list_agents', {}), /no longer active/);
34
35
  rt.history.context = async ({ nativeSessionId }) => { assert.equal(nativeSessionId, 'c2Vzc2lvbg'); return { harnessId: 'pi', title: 'Old session', cwd: root, transcript: 'User: prior question\nAssistant: prior answer' }; };
35
- await rt.send(parent.id, '@worker review files @[Old session](harness-mix://session/c2Vzc2lvbg)');
36
+ await rt.send(parent.id, '#worker review files #[Old session](harness-mix://session/c2Vzc2lvbg)');
36
37
  assert.ok(rt.execution.isRunning(parent.id));
37
38
  assert.match(leadPrompt, /untrusted historical data[\s\S]*prior answer/);
38
39
  assert.match(leadPrompt, /Recovery checkpoint:[\s\S]*interrupted-fixture \(worker\)[\s\S]*call list_delegations now/);
@@ -88,7 +89,11 @@ async function main() {
88
89
  assert.equal((await call('get_delegation_status', { task_ids: [first.task_id], wait_ms: 3000 }))[0].result, 'follow-up-result');
89
90
  await assert.rejects(call('get_delegation_status', { task_ids: ['foreign-task'] }), /Unknown task/);
90
91
  await assert.rejects(rt.collaboration.call(jobs[0].childId, 'delegate_to_agent', { agent_type: 'worker', task: 'recursive' }), /Only lead/);
91
- await assert.rejects(call('delegate_to_agent', { agent_type: 'worker', task: 'x', unexpected: true }));
92
+ await assert.rejects(call('delegate_to_agent', { agent_type: 'worker', task: 'x', unexpected: true }));
93
+ const savedMentions = parent.activeMentions;
94
+ delete parent.activeMentions;
95
+ await assert.rejects(call('delegate_to_agent', { agent_type: 'worker', task: 'unauthorized' }), /用户本轮未显式委派/);
96
+ parent.activeMentions = savedMentions;
92
97
  const denied = await fetch(ownerConnection.env.HARNESS_MIX_COLLAB_URL, { method: 'POST', headers: { Authorization: 'Bearer invalid' }, body: '{}' });
93
98
  assert.equal(denied.status, 403);
94
99
  const outsider = await rt.createThread({ harnessId: 'worker', cwd: root });
@@ -5,53 +5,67 @@ const path = require('node:path');
5
5
  app.disableHardwareAcceleration();
6
6
  app.whenReady().then(async () => {
7
7
  const bundle = await require('esbuild').build({ stdin: { contents: `export * from './src/native-ui/renderer-extension/src/renderer-harness-mentions.ts';`, resolveDir: process.cwd() }, bundle: true, platform: 'browser', format: 'iife', globalName: 'Mentions', write: false });
8
- const win = new BrowserWindow({ show: false, width: 900, height: 650, webPreferences: { sandbox: true, contextIsolation: true, offscreen: true } });
8
+ const win = new BrowserWindow({ show: false, width: 900, height: 650, webPreferences: { sandbox: true, contextIsolation: true, offscreen: true, backgroundThrottling: false } });
9
9
  try {
10
- await win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent('<body style="font:16px system-ui;padding:32px"><h2>Harness Mix · 多 Agent 协作</h2><p>主任务负责分工、收集结果与验证。</p><div data-codex-composer-root style="position:absolute;bottom:50px;width:750px"><textarea style="width:100%;height:90px;font:16px system-ui" placeholder="输入 @ 选择协作 Harness"></textarea><div contenteditable="true" style="border:1px solid gray;padding:16px">你负责后端,</div></div></body>'));
10
+ await win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent('<body style="font:16px system-ui;padding:32px"><h2>Harness Mix · 多 Agent 协作</h2><p>主任务负责分工、收集结果与验证。</p><div data-codex-composer-root data-composer-draft-id="textarea-smoke" style="position:absolute;left:40px;bottom:50px;width:750px;box-sizing:border-box;padding:14px;border:2px solid #aaa;border-radius:16px"><textarea style="width:100%;height:90px;border:0;outline:0;resize:none;font:16px system-ui" placeholder="输入 # 选择协作 Harness"></textarea></div><div data-codex-composer-root data-composer-draft-id="rich-smoke" style="position:fixed;left:-1200px;width:400px"><div contenteditable="true" style="border:1px solid gray;padding:16px">你负责后端,</div></div></body>'));
11
11
  await win.webContents.executeJavaScript(`globalThis.__HARNESS_MIX_ICONS__ = ${JSON.stringify(require('../src/main/native/icons').getAllIconsDictionary())}`);
12
12
  await win.webContents.executeJavaScript(bundle.outputFiles[0].text);
13
13
  const result = await win.webContents.executeJavaScript(`(async () => {
14
- const api = Mentions.installHarnessMentions(async () => ({agents:[{id:'pi',name:'Pi',available:true,lead:true},{id:'claude',name:'Claude Code',available:true,lead:true},{id:'dsh',name:'DeepSeek Harness',available:false,lead:false}],sessions:[{id:'c2Vzc2lvbg',title:'旧版登录审查',harnessId:'claude',cwd:'E:/project',running:false}]}));
15
- const editor = document.querySelector('textarea');
16
- editor.focus(); editor.value = '你负责后端, @cl'; editor.setSelectionRange(editor.value.length,editor.value.length); editor.dispatchEvent(new Event('input',{bubbles:true}));
14
+ const api = Mentions.installHarnessMentions(async () => ({agents:[{id:'pi',name:'Pi',available:true,lead:true},{id:'claude',name:'Claude Code',available:true,lead:true},{id:'dsh',name:'DeepSeek Harness',available:false,lead:false}],sessions:[{id:'c2Vzc2lvbg',title:'旧版登录审查',harnessId:'claude',cwd:'E:/project',running:false}]}));
15
+ const editor = document.querySelector('textarea');
16
+ editor.focus(); editor.value = '@cl'; editor.setSelectionRange(editor.value.length,editor.value.length); editor.dispatchEvent(new Event('input',{bubbles:true}));
17
+ await new Promise(r=>setTimeout(r,30));
18
+ const atTriggerStayedNative = document.querySelector('[data-harness-mix-mentions]').hidden;
19
+ editor.focus(); editor.value = '你负责后端, #cl'; editor.setSelectionRange(editor.value.length,editor.value.length); editor.dispatchEvent(new Event('input',{bubbles:true}));
17
20
  await new Promise(r=>setTimeout(r,30));
18
21
  const filtered = document.querySelectorAll('[data-harness-mix-mentions] [role="option"]').length;
19
22
  editor.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,cancelable:true}));
20
- const text = editor.value;
21
- const badge = document.querySelector('[data-harness-mix-selected-mentions] img')?.dataset.harnessMixIcon;
23
+ const text = editor.value;
24
+ const badge = document.querySelector('[data-harness-mix-selected-mentions] img')?.dataset.harnessMixIcon;
25
+ const badgeButton = document.querySelector('[data-harness-mix-selected-mentions] button');
26
+ const inline = badgeButton?.parentElement?.nextElementSibling === editor && editor.parentElement?.dataset.harnessMixHasMentions === 'true';
27
+ editor.value = '你负责后端, #pi'; editor.setSelectionRange(editor.value.length,editor.value.length); editor.dispatchEvent(new Event('input',{bubbles:true}));
28
+ await new Promise(r=>setTimeout(r,30));
29
+ editor.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true,cancelable:true}));
30
+ const selectedAgentCount = editor.parentElement.querySelectorAll('[data-harness-mix-mention-badge^="agent:"]').length;
22
31
  const rich = document.querySelector('[contenteditable]'); rich.focus();
23
32
  const selection = getSelection(), range = document.createRange(); range.selectNodeContents(rich); range.collapse(false); selection.removeAllRanges(); selection.addRange(range);
24
- document.execCommand('insertText',false,' @pi');
33
+ document.execCommand('insertText',false,' #pi');
25
34
  await new Promise(r=>setTimeout(r,30));
26
35
  rich.textContent = rich.textContent; // Native editor redraw replaces the original Range nodes.
27
36
  document.querySelector('[data-harness-mix-mentions] [role="option"]').click();
28
37
  const richText = rich.textContent;
29
- editor.focus(); editor.value='@'; editor.setSelectionRange(1,1); editor.dispatchEvent(new Event('input',{bubbles:true}));
38
+ editor.focus(); editor.value='#'; editor.setSelectionRange(1,1); editor.dispatchEvent(new Event('input',{bubbles:true}));
30
39
  await new Promise(r=>setTimeout(r,30));
31
40
  globalThis.mentionSmoke = api;
32
41
  const tabs = document.querySelectorAll('[data-harness-mix-mentions] [role=tab]').length;
33
42
  if (tabs !== 2) throw new Error('Missing Agents/session tabs');
34
43
  const icons = document.querySelectorAll('[data-harness-mix-mentions] img').length;
35
44
  const disabled = document.querySelectorAll('[data-harness-mix-mentions] [role="option"]:disabled').length;
36
- editor.value='@旧';editor.setSelectionRange(2,2);editor.dispatchEvent(new Event('input',{bubbles:true}));await new Promise(r=>setTimeout(r,30));
37
- document.querySelector('[data-harness-mix-mentions] [role="option"]').click();
38
- const sessionBadge=[...document.querySelectorAll('[data-harness-mix-selected-mentions] span')].some(b=>b.title.startsWith('历史会话'));
39
- const root = editor.closest('[data-codex-composer-root]');
40
- root.dispatchEvent(new Event('submit', { bubbles: true }));
41
- const submittedText = editor.value;
42
- return {filtered,text,submittedText,richText,badge,icons,disabled,sessionText:editor.value,sessionBadge};
43
- })()`);
44
- assert.equal(result.filtered, 1);
45
+ editor.value='#旧';editor.setSelectionRange(2,2);editor.dispatchEvent(new Event('input',{bubbles:true}));await new Promise(r=>setTimeout(r,30));
46
+ document.querySelector('[data-harness-mix-mentions] [role="option"]').click();
47
+ const sessionBadge=[...document.querySelectorAll('[data-harness-mix-selected-mentions] [data-harness-mix-mention-badge]')].some(b=>b.title.startsWith('移除历史会话'));
48
+ editor.value='请分别实现并审查这个任务。'; editor.setSelectionRange(editor.value.length,editor.value.length); editor.dispatchEvent(new Event('input',{bubbles:true}));
49
+ globalThis.mentionSmokeEditor = editor;
50
+ return {atTriggerStayedNative,filtered,text,richText,badge,badgeLabel:badgeButton?.getAttribute('aria-label'),inline,selectedAgentCount,icons,disabled,sessionBadge};
51
+ })()`);
52
+ assert.equal(result.atTriggerStayedNative, true);
53
+ assert.equal(result.filtered, 1);
45
54
  assert.equal(result.text, '你负责后端, ');
46
- assert.match(result.submittedText, /@claude/);
47
- assert.equal(result.disabled, 1);
48
- assert.equal(result.badge, 'harnesses/claude'); assert.equal(result.icons, 3);
49
- assert.equal(result.sessionBadge, true);
50
- const out = path.resolve('output/collaboration-ui'); await fs.mkdir(out, { recursive: true });
51
- await new Promise(r => setTimeout(r,100));
52
- await fs.writeFile(path.join(out, 'mentions.png'), (await win.webContents.capturePage()).toPNG());
55
+ assert.equal(result.disabled, 1);
56
+ assert.equal(result.badge, 'harnesses/claude'); assert.equal(result.icons, 3);
57
+ assert.match(result.badgeLabel, /^移除协作 Agent /);
58
+ assert.equal(result.inline, true);
59
+ assert.equal(result.selectedAgentCount, 2);
60
+ assert.equal(result.sessionBadge, true);
61
+ const out = path.resolve('output/collaboration-ui'); await fs.mkdir(out, { recursive: true });
62
+ await new Promise(r => setTimeout(r,100));
63
+ await fs.writeFile(path.join(out, 'mentions.png'), (await win.webContents.capturePage()).toPNG());
64
+ const submittedText = await win.webContents.executeJavaScript(`(() => { const root=mentionSmokeEditor.closest('[data-codex-composer-root]'); root.dispatchEvent(new Event('submit',{bubbles:true})); return mentionSmokeEditor.value; })()`);
65
+ assert.match(submittedText, /#claude/);
66
+ assert.match(submittedText, /#pi/);
53
67
  await win.webContents.executeJavaScript('mentionSmoke.dispose()');
54
68
  assert.equal(await win.webContents.executeJavaScript('document.querySelectorAll("[data-harness-mix-mentions]").length'), 0);
55
- console.log('PASS: Electron native composer @ Harness/session icons, badges, keyboard selection, textarea/rich text and cleanup');
69
+ console.log('PASS: Electron native composer # Harness/session icons, badges, keyboard selection, textarea/rich text and cleanup');
56
70
  } finally { win.destroy(); app.quit(); }
57
71
  }).catch(error => { console.error(error); app.exit(1); });
@@ -0,0 +1,52 @@
1
+ const fs = require('node:fs/promises');
2
+ const path = require('node:path');
3
+ const assert = require('node:assert/strict');
4
+ const { randomUUID } = require('node:crypto');
5
+ const { Integrations } = require('../src/main/host/integrations');
6
+ const { buildAdapters } = require('../src/main/adapters');
7
+
8
+ async function main() {
9
+ const id = process.argv.find(a => a.startsWith('--harness='))?.split('=')[1] || 'claude';
10
+ const root = path.resolve('output/integrations-e2e', `${id}-${Date.now()}`), cwd = path.join(root, 'project');
11
+ await fs.mkdir(cwd, { recursive: true });
12
+ const adapters = new Map(buildAdapters(() => {}).map(a => [a.manifest.id, a]));
13
+ const adapter = adapters.get(id); assert.ok(adapter?.manifest.integrations?.mcp);
14
+ const runtime = { adapters, status: {}, sessions: new Map(), store: { directory: path.join(root, 'data') }, resolveHarnessId: id => id };
15
+ const manager = new Integrations(runtime), selected = { harnessId: id, scope: 'project', cwd };
16
+ const log = path.join(root, 'mcp-methods.log');
17
+ await manager.save({ ...selected, server: { name: 'acceptance', command: process.execPath, args: [path.resolve('scripts/support/integrations-mcp-fixture.cjs'), log], enabled: true } });
18
+ if (adapter.manifest.integrations.skills) {
19
+ const source = path.join(root, 'skill'); await fs.mkdir(source);
20
+ await fs.writeFile(path.join(source, 'SKILL.md'), '---\nname: hm-integration-acceptance\ndescription: Read-only Harness Mix integration acceptance fixture.\n---\nReturn HARNESS_MIX_SKILL_OK.\n');
21
+ await manager.skillChange({ ...selected, name: 'hm-integration-acceptance', source, action: 'install' });
22
+ }
23
+ const thread = { id: randomUUID(), nativeSessionId: randomUUID(), cwd, title: 'Integration acceptance', options: {} };
24
+ const injected = await manager.forSession(thread, adapter);
25
+ let session;
26
+ const deadline = setTimeout(() => { console.error('Native acceptance timed out'); process.exit(1); }, 55000);
27
+ try {
28
+ session = await adapter.open({ thread, managedMcp: injected.servers, emit: () => {}, diagnostic: () => {} });
29
+ session.adapter = adapter; session.integrationServers = injected.records; session.integrationCwd = injected.cwd;
30
+ runtime.sessions.set(thread.id, session);
31
+ let native = [], methods = '';
32
+ for (let attempt = 0; attempt < 15; attempt++) {
33
+ native = adapter.inspectIntegrations ? await adapter.inspectIntegrations(session) : [];
34
+ try { methods = await fs.readFile(log, 'utf8'); } catch (e) { if (e.code !== 'ENOENT') throw e; }
35
+ if (native.some(s => s.name === 'hm-user-acceptance' && s.status === 'connected')) break;
36
+ if (!adapter.inspectIntegrations && methods.includes('tools/list')) break;
37
+ await new Promise(resolve => setTimeout(resolve, 1000));
38
+ }
39
+ if (adapter.inspectIntegrations) assert.ok(native.some(s => s.name === 'hm-user-acceptance' && s.status === 'connected'), 'native Harness must report connected');
40
+ methods = await fs.readFile(log, 'utf8'); assert.match(methods, /initialize/); assert.match(methods, /tools\/list/);
41
+ let skillLoaded = null;
42
+ if (session.query?.supportedCommands) {
43
+ const commands = await session.query.supportedCommands();
44
+ skillLoaded = commands.some(c => c.name === 'hm-integration-acceptance');
45
+ assert.equal(skillLoaded, true, 'native Claude must discover the installed project skill');
46
+ }
47
+ const snapshot = await manager.list(selected); assert.equal(snapshot.servers[0].appliedSessions, 1);
48
+ const report = { harness: id, nativeMcpConnected: native.some(s => s.name === 'hm-user-acceptance' && s.status === 'connected') || null, nativeToolDiscovery: true, skillLoaded, modelTurnExecuted: false, automaticApproval: false };
49
+ await fs.writeFile(path.join(root, 'report.json'), JSON.stringify(report, null, 2)); console.log(JSON.stringify({ ...report, root }));
50
+ } finally { clearTimeout(deadline); if (session) await adapter.close(session); }
51
+ }
52
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -41,7 +41,9 @@ async function main() {
41
41
  const ownership = await request('harness-mix/runtime/inspect', {});
42
42
  assert.equal(ownership.owner, 'harness-mix');
43
43
  assert.equal(ownership.core, 'src/main/protocol-core/protocol-core.js');
44
+ assert.deepEqual(ownership.codex, { mode: 'official-passthrough', managedRoute: 'codex-harness' });
44
45
  report.checks.push('Harness Mix HostRuntime and ProtocolCore own external execution');
46
+ report.checks.push('official Codex route is separate from the managed Codex worker');
45
47
  const plugins = await request('codexhost/harness/plugins/list', {});
46
48
  assert.ok(plugins.plugins.some(plugin => (plugin.id || plugin.manifest?.id) === 'pi'), 'Pi plugin registered');
47
49
  report.checks.push('external harness plugins registered');
@@ -54,6 +56,43 @@ async function main() {
54
56
  const officialModels = await request('model/list', {});
55
57
  assert.ok(Array.isArray(officialModels.data), 'Official model/list passes through');
56
58
  report.checks.push('official model/list passthrough');
59
+ const codexAccounts = await request('codexhost/account/refresh', {});
60
+ const officialAccount = codexAccounts.accounts?.find(account => account.management === 'native');
61
+ assert.ok(officialAccount, 'Official Codex account is projected into Account management');
62
+ assert.equal(officialAccount.authenticated, true, 'Current official Codex login is detected');
63
+ assert.ok(officialAccount.email, 'Current ChatGPT account identity is available from account/read');
64
+ const officialAccountUsage = await request('codexhost/account/usage/inspect', { accountId: officialAccount.accountId });
65
+ assert.ok(officialAccountUsage.accountCredits, 'Official Codex rate limits are projected');
66
+ report.account = {
67
+ authenticated: officialAccount.authenticated,
68
+ planType: officialAccount.planType || null,
69
+ fiveHourUsedPercent: officialAccountUsage.usage?.planFiveHourUsedPercent ?? null,
70
+ sevenDayUsedPercent: officialAccountUsage.usage?.planSevenDayUsedPercent ?? null,
71
+ };
72
+ report.checks.push('real official Codex account and rate-limit projection');
73
+ if (process.argv.includes('--live-codex')) {
74
+ const native = await request('thread/start', { cwd: directory });
75
+ const nativeThreadId = native.thread?.id;
76
+ assert.ok(nativeThreadId, 'Official Codex thread/start returns a native Thread');
77
+ const nativeTurn = await request('turn/start', {
78
+ threadId: nativeThreadId,
79
+ input: [{ type: 'text', text: 'Reply with exactly HARNESS_MIX_CODEX_PASSTHROUGH_OK. Do not use tools.' }],
80
+ });
81
+ const nativeTurnId = nativeTurn.turn?.id;
82
+ assert.ok(nativeTurnId, 'Official Codex turn/start returns a native Turn');
83
+ const deadline = Date.now() + 120000;
84
+ while (Date.now() < deadline && !events.some(event => event.method === 'turn/completed' && event.params?.threadId === nativeThreadId && event.params?.turn?.id === nativeTurnId)) {
85
+ await new Promise(resolve => setTimeout(resolve, 250));
86
+ }
87
+ const completed = events.find(event => event.method === 'turn/completed' && event.params?.threadId === nativeThreadId && event.params?.turn?.id === nativeTurnId);
88
+ assert.equal(completed?.params?.turn?.status, 'completed', 'Official Codex Turn completes through the passthrough path');
89
+ const text = events.filter(event => event.params?.threadId === nativeThreadId && event.params?.turnId === nativeTurnId)
90
+ .map(event => event.params?.delta || event.params?.item?.text || '').join('');
91
+ assert.ok(text.includes('HARNESS_MIX_CODEX_PASSTHROUGH_OK'), 'Official Codex response reaches Desktop protocol unchanged');
92
+ await request('thread/delete', { threadId: nativeThreadId }).catch(() => {});
93
+ report.codex = { threadId: nativeThreadId, turnId: nativeTurnId, status: completed.params.turn.status };
94
+ report.checks.push('real official Codex thread/turn passthrough');
95
+ }
57
96
  // Real Host + stock-server section catalog, with test threads isolated in output/.
58
97
  const sidebarThread = await request('thread/start', { cwd: directory, model: 'codexhost/pi-native' });
59
98
  const sidebarId = sidebarThread.thread.id;
@@ -0,0 +1,83 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs/promises');
3
+ const path = require('node:path');
4
+ const { HandoffCheckpoints } = require('../src/main/host/handoff-checkpoints');
5
+ const { HandoffAccess } = require('../src/main/host/handoff-access');
6
+ const { dispatch } = require('../src/main/host/handoff-mcp.cjs');
7
+
8
+ async function main() {
9
+ await fs.mkdir('output', { recursive: true });
10
+ const root = await fs.mkdtemp(path.resolve('output/handoff-checkpoint-'));
11
+ const runtime = {
12
+ store: { directory: path.join(root, 'data') },
13
+ adapters: new Map([['target', { manifest: { integrations: { mcp: true } } }]]),
14
+ };
15
+ runtime.handoffs = new HandoffCheckpoints(runtime);
16
+ await runtime.handoffs.initialize();
17
+
18
+ const thread = {
19
+ id: 'thread-a',
20
+ harnessId: 'source',
21
+ title: '修复登录模块',
22
+ cwd: process.cwd(),
23
+ messages: [
24
+ { id: 'user-1', role: 'user', text: '继续实现,api_key=supersecretvalue123' },
25
+ {
26
+ id: 'assistant-1', role: 'assistant', text: '先跑测试再修复', coreItems: [
27
+ { type: 'plan', entries: [{ text: '定位失败', status: 'completed' }, { text: '实现修复', status: 'in_progress' }] },
28
+ { type: 'tool_call', toolCallId: 'native-replay-id', title: 'npm test', state: 'error', input: 'token=anothersecretvalue', output: 'authorization: Bearer abcdefghijklmnop\n1 test failed' },
29
+ { type: 'file_change', path: 'src/login.js', changeType: 'modified', before: 'a', after: 'b' },
30
+ ],
31
+ },
32
+ ],
33
+ };
34
+
35
+ const checkpoint = await runtime.handoffs.create(thread, 'target', { intent: 'execute-plan' });
36
+ assert.match(checkpoint.checkpointId, /^handoff_[a-f0-9]{24}$/);
37
+ assert.equal(checkpoint.onDemandAccess, 'mcp');
38
+ assert.equal(checkpoint.intent, 'execute-plan');
39
+ assert.equal(checkpoint.plan.inProgress[0], '实现修复');
40
+ assert.equal(checkpoint.fileState.files[0].path, 'src/login.js');
41
+ assert.match(checkpoint.fileState.diffDigest, /^[a-f0-9]{64}$/);
42
+ assert.ok(checkpoint.fileState.gitHead === null || /^[a-f0-9]{40,64}$/.test(checkpoint.fileState.gitHead));
43
+ const serialized = JSON.stringify(checkpoint);
44
+ assert.doesNotMatch(serialized, /supersecretvalue123|anothersecretvalue|abcdefghijklmnop|native-replay-id/);
45
+ assert.match(serialized, /\[redacted\]/);
46
+
47
+ thread.messages[0].text = 'mutated after handoff';
48
+ assert.doesNotMatch(JSON.stringify(runtime.handoffs.owned(thread.id, checkpoint.checkpointId)), /mutated after handoff/);
49
+
50
+ const access = new HandoffAccess(runtime);
51
+ const connection = await access.connection({ ...thread, harnessId: 'target', pendingHandoff: { checkpointId: checkpoint.checkpointId } });
52
+ assert.ok(connection);
53
+ const oldUrl = process.env.HARNESS_MIX_HANDOFF_URL;
54
+ const oldKey = process.env.HARNESS_MIX_HANDOFF_KEY;
55
+ process.env.HARNESS_MIX_HANDOFF_URL = connection.env.HARNESS_MIX_HANDOFF_URL;
56
+ process.env.HARNESS_MIX_HANDOFF_KEY = connection.env.HARNESS_MIX_HANDOFF_KEY;
57
+ try {
58
+ const catalog = await dispatch({ id: 1, method: 'tools/list' });
59
+ assert.deepEqual(catalog.tools.map(tool => tool.name), [
60
+ 'get_handoff_checkpoint', 'list_handoff_conversation', 'list_handoff_evidence',
61
+ 'read_handoff_evidence', 'list_handoff_files', 'read_handoff_plan',
62
+ ]);
63
+ const result = await dispatch({ id: 2, method: 'tools/call', params: { name: 'get_handoff_checkpoint', arguments: { checkpoint_id: checkpoint.checkpointId } } });
64
+ const summary = JSON.parse(result.content[0].text);
65
+ assert.equal(summary.checkpointId, checkpoint.checkpointId);
66
+ assert.equal(summary.status, 'checkpoint-created');
67
+ const denied = await dispatch({ id: 3, method: 'tools/call', params: { name: 'get_handoff_checkpoint', arguments: { checkpoint_id: 'handoff_outside_scope' } } });
68
+ assert.equal(denied.isError, true);
69
+ assert.match(denied.content[0].text, /outside this native session scope/);
70
+ } finally {
71
+ if (oldUrl === undefined) delete process.env.HARNESS_MIX_HANDOFF_URL; else process.env.HARNESS_MIX_HANDOFF_URL = oldUrl;
72
+ if (oldKey === undefined) delete process.env.HARNESS_MIX_HANDOFF_KEY; else process.env.HARNESS_MIX_HANDOFF_KEY = oldKey;
73
+ await access.close();
74
+ }
75
+
76
+ const reloaded = new HandoffCheckpoints(runtime);
77
+ await reloaded.initialize();
78
+ assert.equal(reloaded.get(thread.id, checkpoint.checkpointId).contentDigest, checkpoint.contentDigest);
79
+ await reloaded.close();
80
+ console.log('PASS: immutable handoff checkpoint, redaction, persistence and scoped read-only MCP access');
81
+ }
82
+
83
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -18,7 +18,10 @@ if (!fs.existsSync(packageRoot)) {
18
18
  fs.mkdirSync(destination, { recursive: true });
19
19
  for (const name of files) {
20
20
  const source = path.join(packageRoot, 'bin', name);
21
- if (!fs.existsSync(source)) throw new Error(`${packageName} is missing bin/${name}`);
21
+ if (!fs.existsSync(source)) {
22
+ console.warn(`[Harness Mix] ${packageName} contains source-only platform metadata; run npm run build:native on this machine.`);
23
+ process.exit(0);
24
+ }
22
25
  const target = path.join(destination, name);
23
26
  fs.copyFileSync(source, target);
24
27
  if (platform !== 'win32') fs.chmodSync(target, 0o755);