@ai-workbench/connect 0.0.0-stage → 0.7.5

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 (129) hide show
  1. package/NOTICE.md +18 -0
  2. package/README.md +24 -2
  3. package/client/index.cjs +123 -0
  4. package/cordis.patch.yml +3 -0
  5. package/helper/auth-status.cjs +18 -0
  6. package/helper/claude-callback.cjs +29 -0
  7. package/helper/main.cjs +19 -0
  8. package/helper/projects.cjs +57 -0
  9. package/helper/remote-command.cjs +15 -0
  10. package/helper/remote-login.cjs +25 -0
  11. package/helper/window.cjs +52 -0
  12. package/lib/api.d.ts +196 -0
  13. package/lib/api.js +1 -0
  14. package/lib/archive.d.ts +1 -0
  15. package/lib/archive.js +63 -0
  16. package/lib/auth-status.d.ts +3 -0
  17. package/lib/auth-status.js +18 -0
  18. package/lib/cache.d.ts +21 -0
  19. package/lib/cache.js +156 -0
  20. package/lib/component-download.d.ts +25 -0
  21. package/lib/component-download.js +73 -0
  22. package/lib/components.d.ts +9 -0
  23. package/lib/components.js +121 -0
  24. package/lib/connect.d.ts +10 -0
  25. package/lib/connect.js +246 -0
  26. package/lib/cordis.d.ts +15 -0
  27. package/lib/cordis.js +1 -0
  28. package/lib/defaults.d.ts +29 -0
  29. package/lib/defaults.js +4 -0
  30. package/lib/electron.d.ts +8 -0
  31. package/lib/electron.js +83 -0
  32. package/lib/enrollment.d.ts +3 -0
  33. package/lib/enrollment.js +78 -0
  34. package/lib/index.d.ts +40 -0
  35. package/lib/index.js +48 -0
  36. package/lib/integration.d.ts +5 -0
  37. package/lib/integration.js +47 -0
  38. package/lib/login.d.ts +35 -0
  39. package/lib/login.js +81 -0
  40. package/lib/native-import-picker.d.ts +11 -0
  41. package/lib/native-import-picker.js +49 -0
  42. package/lib/needs.d.ts +9 -0
  43. package/lib/needs.js +52 -0
  44. package/lib/network.d.ts +26 -0
  45. package/lib/network.js +226 -0
  46. package/lib/physical-fs.d.ts +3 -0
  47. package/lib/physical-fs.js +7 -0
  48. package/lib/ports.d.ts +43 -0
  49. package/lib/ports.js +1 -0
  50. package/lib/private-files.d.ts +3 -0
  51. package/lib/private-files.js +41 -0
  52. package/lib/processes.d.ts +5 -0
  53. package/lib/processes.js +147 -0
  54. package/lib/profile-activation.d.ts +32 -0
  55. package/lib/profile-activation.js +118 -0
  56. package/lib/profile-enrollment.d.ts +21 -0
  57. package/lib/profile-enrollment.js +275 -0
  58. package/lib/profile-runtime.d.ts +6 -0
  59. package/lib/profile-runtime.js +47 -0
  60. package/lib/profile-settings.d.ts +2 -0
  61. package/lib/profile-settings.js +10 -0
  62. package/lib/projects.d.ts +4 -0
  63. package/lib/projects.js +41 -0
  64. package/lib/provider.d.ts +73 -0
  65. package/lib/provider.js +39 -0
  66. package/lib/remote-login.d.ts +2 -0
  67. package/lib/remote-login.js +9 -0
  68. package/lib/ssh.d.ts +10 -0
  69. package/lib/ssh.js +78 -0
  70. package/lib/startup.d.ts +18 -0
  71. package/lib/startup.js +211 -0
  72. package/lib/workspace-bindings.d.ts +32 -0
  73. package/lib/workspace-bindings.js +328 -0
  74. package/locale/en.json +6 -0
  75. package/locale/zh.json +6 -0
  76. package/node_modules/argparse/LICENSE +254 -0
  77. package/node_modules/argparse/README.md +84 -0
  78. package/node_modules/argparse/argparse.js +3707 -0
  79. package/node_modules/argparse/lib/sub.js +67 -0
  80. package/node_modules/argparse/lib/textwrap.js +440 -0
  81. package/node_modules/argparse/package.json +31 -0
  82. package/node_modules/js-yaml/LICENSE +21 -0
  83. package/node_modules/js-yaml/README.md +233 -0
  84. package/node_modules/js-yaml/bin/js-yaml.js +117 -0
  85. package/node_modules/js-yaml/dist/js-yaml.js +3022 -0
  86. package/node_modules/js-yaml/dist/js-yaml.js.map +1 -0
  87. package/node_modules/js-yaml/dist/js-yaml.min.js +2 -0
  88. package/node_modules/js-yaml/dist/js-yaml.min.js.map +1 -0
  89. package/node_modules/js-yaml/dist/js-yaml.mjs +3082 -0
  90. package/node_modules/js-yaml/dist/js-yaml.mjs.map +1 -0
  91. package/node_modules/js-yaml/index.js +44 -0
  92. package/node_modules/js-yaml/lib/common.js +50 -0
  93. package/node_modules/js-yaml/lib/dumper.js +937 -0
  94. package/node_modules/js-yaml/lib/exception.js +51 -0
  95. package/node_modules/js-yaml/lib/index_vite_proxy.tmp.mjs +37 -0
  96. package/node_modules/js-yaml/lib/loader.js +1803 -0
  97. package/node_modules/js-yaml/lib/schema/core.js +9 -0
  98. package/node_modules/js-yaml/lib/schema/default.js +20 -0
  99. package/node_modules/js-yaml/lib/schema/failsafe.js +14 -0
  100. package/node_modules/js-yaml/lib/schema/json.js +17 -0
  101. package/node_modules/js-yaml/lib/schema.js +109 -0
  102. package/node_modules/js-yaml/lib/snippet.js +96 -0
  103. package/node_modules/js-yaml/lib/type/binary.js +122 -0
  104. package/node_modules/js-yaml/lib/type/bool.js +35 -0
  105. package/node_modules/js-yaml/lib/type/float.js +99 -0
  106. package/node_modules/js-yaml/lib/type/int.js +142 -0
  107. package/node_modules/js-yaml/lib/type/map.js +8 -0
  108. package/node_modules/js-yaml/lib/type/merge.js +12 -0
  109. package/node_modules/js-yaml/lib/type/null.js +35 -0
  110. package/node_modules/js-yaml/lib/type/omap.js +45 -0
  111. package/node_modules/js-yaml/lib/type/pairs.js +50 -0
  112. package/node_modules/js-yaml/lib/type/seq.js +8 -0
  113. package/node_modules/js-yaml/lib/type/set.js +29 -0
  114. package/node_modules/js-yaml/lib/type/str.js +8 -0
  115. package/node_modules/js-yaml/lib/type/timestamp.js +88 -0
  116. package/node_modules/js-yaml/lib/type.js +66 -0
  117. package/node_modules/js-yaml/package.json +83 -0
  118. package/node_modules/pend/LICENSE +23 -0
  119. package/node_modules/pend/README.md +41 -0
  120. package/node_modules/pend/index.js +55 -0
  121. package/node_modules/pend/package.json +18 -0
  122. package/node_modules/pend/test.js +137 -0
  123. package/node_modules/yauzl/LICENSE +21 -0
  124. package/node_modules/yauzl/README.md +1015 -0
  125. package/node_modules/yauzl/crc32.js +45 -0
  126. package/node_modules/yauzl/fd-slicer.js +188 -0
  127. package/node_modules/yauzl/index.js +1096 -0
  128. package/node_modules/yauzl/package.json +40 -0
  129. package/package.json +89 -4
package/lib/connect.js ADDED
@@ -0,0 +1,246 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { dependency, fail, ok, need } from "./needs.js";
3
+ import { isProjectName } from "./projects.js";
4
+ import { createStartup } from "./startup.js";
5
+ import { parseLoginStatus } from "./auth-status.js";
6
+ export function validEnrollment(e) {
7
+ return typeof e === 'object' && e !== null && typeof e.host === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(e.host)
8
+ && typeof e.username === 'string' && /^emp-[a-z0-9][a-z0-9-]*$/.test(e.username)
9
+ && Number.isInteger(e.port) && e.port > 0 && e.port <= 65535
10
+ && e.session?.protocolVersion === 1 && [e.session.command, e.session.root, e.session.settingsPath].every(v => typeof v === 'string' && v.startsWith('/') && !/[\x00-\x1f\x7f]/.test(v))
11
+ && typeof e.knownHostsFile === 'string' && e.knownHostsFile.startsWith('/') && !/[\x00-\x1f\x7f]/.test(e.knownHostsFile)
12
+ && typeof e.projectRoot === 'string' && e.projectRoot.length > 0 && !/[\x00-\x1f\x7f]/.test(e.projectRoot);
13
+ }
14
+ export function createConnect(ports, emit, options = {}) {
15
+ let state = { phase: 'idle' }, generation = 0, disposed = false;
16
+ let flight;
17
+ const instanceId = randomUUID();
18
+ let connection = { instanceId, phase: 'unavailable', epoch: 0 };
19
+ const connectionListeners = new Set();
20
+ let startup;
21
+ const setConnection = (phase, need) => {
22
+ const next = { instanceId, phase, epoch: generation, ...(need ? { need } : {}) };
23
+ if (JSON.stringify(next) === JSON.stringify(connection))
24
+ return;
25
+ connection = structuredClone(next);
26
+ startup?.connectionChanged(next);
27
+ options.onConnectionState?.(structuredClone(next));
28
+ for (const listener of connectionListeners)
29
+ listener(structuredClone(next));
30
+ };
31
+ let controller = new AbortController();
32
+ const loginFlights = new Map();
33
+ const pending = new Set();
34
+ let disposal;
35
+ const track = (promise) => { pending.add(promise); void promise.then(() => pending.delete(promise), () => pending.delete(promise)); return promise; };
36
+ const publish = (s) => { state = structuredClone(s); emit(structuredClone(state)); };
37
+ const invalid = (g) => disposed ? fail('DISPOSED') : g !== generation ? fail('CONNECTION_LOST') : undefined;
38
+ const block = (r) => { if (!r.ok && !disposed) {
39
+ publish({ phase: 'blocked', need: r.error });
40
+ if (connection.phase !== 'connected')
41
+ setConnection('unavailable', r.error);
42
+ } return r; };
43
+ const transportBlock = (r) => { if (!r.ok && !disposed && connection.phase === 'connected') {
44
+ generation++;
45
+ controller.abort();
46
+ controller = new AbortController();
47
+ flight = undefined;
48
+ loginFlights.clear();
49
+ } const value = block(r); if (!r.ok && !disposed)
50
+ setConnection('unavailable', r.error); return value; };
51
+ const connect = () => {
52
+ if (disposed)
53
+ return Promise.resolve(fail('DISPOSED'));
54
+ if (state.target)
55
+ return Promise.resolve(ok(structuredClone(state.target)));
56
+ if (flight)
57
+ return flight;
58
+ const g = generation, signal = controller.signal;
59
+ if (connection.phase !== 'connected')
60
+ setConnection('connecting');
61
+ const run = async () => {
62
+ if (!ports.enrollment)
63
+ return transportBlock(dependency('S host-layout/enroll.json 适配器'));
64
+ if (!ports.network)
65
+ return transportBlock(dependency('N net-helper 生命周期/状态适配器'));
66
+ if (!ports.ssh)
67
+ return transportBlock(dependency('系统 SSH 适配器'));
68
+ try {
69
+ publish({ phase: 'enrolling' });
70
+ const e = await ports.enrollment.read(signal);
71
+ let stale = invalid(g);
72
+ if (stale)
73
+ return stale;
74
+ if (!e.ok)
75
+ return transportBlock(e);
76
+ if (!validEnrollment(e.value))
77
+ return transportBlock(fail('ENROLLMENT_INVALID'));
78
+ publish({ phase: 'network' });
79
+ const n = await ports.network.connect(e.value, signal);
80
+ stale = invalid(g);
81
+ if (stale)
82
+ return stale;
83
+ if (!n.ok)
84
+ return transportBlock(n);
85
+ if (typeof n.value.proxyCommand !== 'string' || n.value.proxyCommand.length === 0 || /[\r\n\x00]/.test(n.value.proxyCommand))
86
+ return transportBlock(dependency('有效的 N ProxyCommand'));
87
+ const { host, username, port, projectRoot, knownHostsFile, session, hostContext } = e.value;
88
+ const target = { host, username, port, projectRoot, knownHostsFile, session, ...(hostContext ? { hostContext } : {}), proxyCommand: n.value.proxyCommand };
89
+ publish({ phase: 'ssh' });
90
+ const verified = await ports.ssh.verify(target, signal);
91
+ stale = invalid(g);
92
+ if (stale)
93
+ return stale;
94
+ if (!verified.ok)
95
+ return transportBlock(verified);
96
+ publish({ phase: 'connected', target });
97
+ setConnection('connected');
98
+ return ok(structuredClone(target));
99
+ }
100
+ catch {
101
+ return invalid(g) ?? transportBlock(dependency('提供方调用失败,根因未查明;请检查 S/N/SSH 适配器'));
102
+ }
103
+ };
104
+ const promise = track(run());
105
+ flight = promise;
106
+ const clear = () => { if (flight === promise)
107
+ flight = undefined; };
108
+ void promise.then(clear, clear);
109
+ return promise;
110
+ };
111
+ const checkLogin = async (provider) => {
112
+ if (provider !== 'claude' && provider !== 'codex')
113
+ return fail('AUTH_STATUS_INVALID');
114
+ const g = generation;
115
+ const connected = await connect();
116
+ const staleConnection = invalid(g);
117
+ if (staleConnection)
118
+ return staleConnection;
119
+ if (!connected.ok)
120
+ return connected;
121
+ publish({ phase: 'checking-login', target: connected.value, provider });
122
+ try {
123
+ const raw = await ports.ssh.status(connected.value, provider, controller.signal);
124
+ const stale = invalid(g);
125
+ if (stale)
126
+ return stale;
127
+ const status = raw.ok ? parseLoginStatus(provider, raw.value) : raw;
128
+ if (!status.ok)
129
+ return ['network', 'ssh', 'enrollment'].includes(status.error.layer) ? transportBlock(status) : block(status);
130
+ publish(status.value.loggedIn ? { phase: 'ready', target: connected.value, provider } : { phase: 'connected', target: connected.value, provider });
131
+ return status;
132
+ }
133
+ catch {
134
+ return invalid(g) ?? block(fail('AUTH_STATUS_INVALID'));
135
+ }
136
+ };
137
+ const login = (provider) => {
138
+ if (provider !== 'claude' && provider !== 'codex')
139
+ return Promise.resolve(fail('LOGIN_FAILED'));
140
+ if (disposed)
141
+ return Promise.resolve(fail('DISPOSED'));
142
+ const prior = loginFlights.get(provider);
143
+ if (prior)
144
+ return prior;
145
+ const run = async () => {
146
+ const g = generation;
147
+ const connected = await connect();
148
+ const staleConnection = invalid(g);
149
+ if (staleConnection)
150
+ return staleConnection;
151
+ if (!connected.ok)
152
+ return connected;
153
+ if (!ports.login)
154
+ return block(dependency('Electron 登录助手及已确认设置'));
155
+ publish({ phase: 'login', target: connected.value, provider });
156
+ try {
157
+ const r = await ports.login.run(connected.value, provider, controller.signal);
158
+ const stale = invalid(g);
159
+ if (stale)
160
+ return stale;
161
+ if (!r.ok)
162
+ return ['network', 'ssh', 'enrollment'].includes(r.error.layer) ? transportBlock(r) : block(r);
163
+ const checked = await checkLogin(provider);
164
+ return checked.ok && !checked.value.loggedIn ? block(fail('LOGIN_FAILED')) : checked;
165
+ }
166
+ catch {
167
+ return invalid(g) ?? block(fail('LOGIN_FAILED'));
168
+ }
169
+ };
170
+ const promise = track(run());
171
+ loginFlights.set(provider, promise);
172
+ const clear = () => { if (loginFlights.get(provider) === promise)
173
+ loginFlights.delete(provider); };
174
+ void promise.then(clear, clear);
175
+ return promise;
176
+ };
177
+ const projectOperation = (operation, name) => track((async () => {
178
+ if (disposed)
179
+ return fail('DISPOSED');
180
+ if (operation === 'create' && !isProjectName(name))
181
+ return fail('PROJECT_NAME_INVALID');
182
+ const g = generation, connected = await connect();
183
+ const stale = invalid(g);
184
+ if (stale)
185
+ return stale;
186
+ if (!connected.ok)
187
+ return connected;
188
+ if (!ports.projects)
189
+ return dependency('L 主机项目接口适配器');
190
+ try {
191
+ const r = operation === 'list' ? await ports.projects.list(connected.value, controller.signal) : await ports.projects.create(connected.value, name, controller.signal);
192
+ return invalid(g) ?? r;
193
+ }
194
+ catch {
195
+ return invalid(g) ?? fail(operation === 'list' ? 'PROJECT_LIST_FAILED' : 'PROJECT_CREATE_FAILED');
196
+ }
197
+ })());
198
+ const unsubscribe = ports.network?.onState(event => {
199
+ if (disposed)
200
+ return;
201
+ if (event.kind === 'lost') {
202
+ generation++;
203
+ controller.abort();
204
+ controller = new AbortController();
205
+ flight = undefined;
206
+ loginFlights.clear();
207
+ block(fail('CONNECTION_LOST'));
208
+ setConnection('unavailable', need('CONNECTION_LOST'));
209
+ }
210
+ else {
211
+ void connect();
212
+ }
213
+ });
214
+ const operations = { getState: () => structuredClone(state), connect, checkLogin: provider => track(checkLogin(provider)), login };
215
+ startup = createStartup(operations, options.providers ?? ['codex', 'claude'], options.onPromptState, options.onPromptResult, instanceId);
216
+ return {
217
+ ...operations,
218
+ getContractInfo: () => ({ apiVersion: '0.7.0', status: 'implemented', workspaceBinding: 'native-local-v1' }),
219
+ getEnrollmentState: () => ({ instanceId, revision: 0, phase: 'blocked', need: need('DEPENDENCY_REQUIRED') }), onEnrollmentState: () => () => { },
220
+ importEnrollment: async () => dependency('官方profileContext与原生目录选择服务'),
221
+ getWorkspaceBindingState: () => ({ instanceId, revision: 0, items: [] }), onWorkspaceBindingState: () => () => { }, acknowledgeWorkspaceNotice: async () => dependency('官方工作区说明回执'), resolveSessionProject: async () => dependency('官方会话存储与L可信工作区关联'),
222
+ listProjects: () => projectOperation('list'), createProject: name => projectOperation('create', name), getSshTarget: connect,
223
+ getConnectionState: () => structuredClone(connection), onConnectionState: listener => { connectionListeners.add(listener); return () => { connectionListeners.delete(listener); }; },
224
+ getLoginPromptState: startup.getState, onLoginPromptState: startup.subscribe,
225
+ requestLoginPrompt: startup.request, respondLoginPrompt: startup.respond,
226
+ refreshLoginPrompts: async () => { await startup.check(); const s = startup.getState(); return s.phase === 'disposed' ? fail('DISPOSED') : s.phase === 'blocked' ? fail(s.technicalDetails.code ?? 'AUTH_STATUS_INVALID') : ok(s); },
227
+ login: async (provider) => { const request = await startup.request(provider, 'automatic'); if (!request.ok)
228
+ return request; return request.value.logins[provider] === true ? ok({ provider, loggedIn: true }) : fail('LOGIN_REQUIRED'); },
229
+ ensureReady: provider => track((async () => {
230
+ const g = generation;
231
+ const r = await checkLogin(provider);
232
+ const stale = invalid(g);
233
+ if (stale)
234
+ return stale;
235
+ if (!r.ok)
236
+ return r;
237
+ if (!r.value.loggedIn) {
238
+ const request = await startup.request(provider, 'automatic');
239
+ return invalid(g) ?? (!request.ok ? request : fail('LOGIN_REQUIRED'));
240
+ }
241
+ return connect();
242
+ })()),
243
+ dispose: () => { if (disposal)
244
+ return disposal; startup.stop(); disposed = true; generation++; controller.abort(); unsubscribe?.(); publish({ phase: 'disposed' }); setConnection('disposed'); connectionListeners.clear(); disposal = Promise.allSettled([...pending]).then(() => startup.dispose()); return disposal; },
245
+ };
246
+ }
@@ -0,0 +1,15 @@
1
+ import type { ConnectApi, ConnectState, ConnectionState, LoginPromptState, LoginPromptResult, EnrollmentState, WorkspaceBindingState } from './api.ts';
2
+ declare module '@deepseek-ai/cordis' {
3
+ interface Context {
4
+ aiWorkbenchConnect: ConnectApi;
5
+ }
6
+ interface Events {
7
+ 'ai-workbench/enrollment-state': (state: EnrollmentState) => void;
8
+ 'ai-workbench/workspace-binding-state': (state: WorkspaceBindingState) => void;
9
+ 'ai-workbench/workspace-diagnostic': (error: unknown) => void;
10
+ 'ai-workbench/connect-state': (state: ConnectState) => void;
11
+ 'ai-workbench/connection-state': (state: ConnectionState) => void;
12
+ 'ai-workbench/login-prompt-state': (state: LoginPromptState) => void;
13
+ 'ai-workbench/login-prompt-result': (result: LoginPromptResult) => void;
14
+ }
15
+ }
package/lib/cordis.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ export declare const L_DEFAULTS: Readonly<{
2
+ enrollmentMetadataMaxBytes: 1048576;
3
+ commandOutputMaxBytes: 1048576;
4
+ remoteKillGraceMs: 1000;
5
+ componentDownloadMaxRedirects: 3;
6
+ maxDownloadBytes: 200000000;
7
+ maxExtractedBytes: 600000000;
8
+ loginOutputMaxBytes: 1048576;
9
+ ipcMaxBytes: 1048576;
10
+ }>;
11
+ export declare const ELECTRON_PIN: Readonly<{
12
+ source: "https://github.com/electron/electron/releases/download/v44.0.0/electron-v44.0.0-darwin-arm64.zip";
13
+ sha256: "076d79742986e1b100b69ebecc691cb07368045e54c9087cef631b8622b76a80";
14
+ executable: "Electron.app/Contents/MacOS/Electron";
15
+ }>;
16
+ export declare const NETWORK_DEFAULTS: Readonly<{
17
+ proxyListen: "127.0.0.1:0";
18
+ logUpload: false;
19
+ statusRefreshIntervalSeconds: 2;
20
+ dialTimeoutSeconds: 20;
21
+ headerTimeoutSeconds: 10;
22
+ enrollmentObservationSeconds: 30;
23
+ httpMaxHeaderBytes: 1048576;
24
+ httpMaxResponseHeaderBytes: 1048576;
25
+ startupObservationSeconds: 35;
26
+ shutdownObservationSeconds: 10;
27
+ controlResponseMaxBytes: 65536;
28
+ downloadMaxRedirects: 3;
29
+ }>;
@@ -0,0 +1,4 @@
1
+ // Owner-approved technical settings (2026-10-20, part two). One source for schema and composition.
2
+ export const L_DEFAULTS = Object.freeze({ enrollmentMetadataMaxBytes: 1048576, commandOutputMaxBytes: 1048576, remoteKillGraceMs: 1000, componentDownloadMaxRedirects: 3, maxDownloadBytes: 200000000, maxExtractedBytes: 600000000, loginOutputMaxBytes: 1048576, ipcMaxBytes: 1048576 });
3
+ export const ELECTRON_PIN = Object.freeze({ source: 'https://github.com/electron/electron/releases/download/v44.0.0/electron-v44.0.0-darwin-arm64.zip', sha256: '076d79742986e1b100b69ebecc691cb07368045e54c9087cef631b8622b76a80', executable: 'Electron.app/Contents/MacOS/Electron' });
4
+ export const NETWORK_DEFAULTS = Object.freeze({ proxyListen: '127.0.0.1:0', logUpload: false, statusRefreshIntervalSeconds: 2, dialTimeoutSeconds: 20, headerTimeoutSeconds: 10, enrollmentObservationSeconds: 30, httpMaxHeaderBytes: 1048576, httpMaxResponseHeaderBytes: 1048576, startupObservationSeconds: 35, shutdownObservationSeconds: 10, controlResponseMaxBytes: 65536, downloadMaxRedirects: 3 });
@@ -0,0 +1,8 @@
1
+ import type { LoginBoot, LoginWindow } from './login.ts';
2
+ export interface ElectronLaunch {
3
+ executable: string;
4
+ entry: string;
5
+ cacheDir: string;
6
+ ipcMaxBytes: number;
7
+ }
8
+ export declare function launchElectron(settings: ElectronLaunch, boot: LoginBoot, onCode: (code: string) => Promise<void>, signal: AbortSignal): Promise<LoginWindow>;
@@ -0,0 +1,83 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { join, isAbsolute } from 'node:path';
4
+ import { StringDecoder } from 'node:string_decoder';
5
+ import { privateDirectory } from "./private-files.js";
6
+ export async function launchElectron(settings, boot, onCode, signal) {
7
+ if (signal.aborted || !isAbsolute(settings.executable) || !isAbsolute(settings.entry) || !Number.isSafeInteger(settings.ipcMaxBytes) || settings.ipcMaxBytes < 1)
8
+ throw new Error('HELPER_SETTINGS_REQUIRED');
9
+ await privateDirectory(settings.cacheDir);
10
+ if (signal.aborted)
11
+ throw new Error('HELPER_CANCELLED');
12
+ const userDataDir = await mkdtemp(join(settings.cacheDir, 'session-'));
13
+ if (signal.aborted) {
14
+ await rm(userDataDir, { recursive: true });
15
+ throw new Error('HELPER_CANCELLED');
16
+ }
17
+ const child = spawn(settings.executable, [settings.entry], { shell: false, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, ELECTRON_RUN_AS_NODE: undefined, ELECTRON_ENABLE_LOGGING: undefined } });
18
+ let resolveReady = () => { }, rejectReady = () => { };
19
+ const ready = new Promise((yes, no) => { resolveReady = yes; rejectReady = no; });
20
+ let failed = false, line = '', size = 0;
21
+ const decoder = new StringDecoder('utf8');
22
+ const abort = () => { failed = true; child.kill('SIGTERM'); };
23
+ const close = () => { if (child.exitCode === null && child.signalCode === null && !child.stdin.destroyed) {
24
+ child.stdin.end(JSON.stringify({ type: 'close' }) + '\n');
25
+ } return closed; };
26
+ child.stdout.on('data', (chunk) => {
27
+ size += chunk.length;
28
+ if (size > settings.ipcMaxBytes) {
29
+ abort();
30
+ return;
31
+ }
32
+ line += decoder.write(chunk);
33
+ const lines = line.split('\n');
34
+ line = lines.pop();
35
+ for (const value of lines) {
36
+ try {
37
+ const e = JSON.parse(value);
38
+ if (e.type === 'ready')
39
+ resolveReady();
40
+ else if (e.type === 'claude-code' && boot.provider === 'claude' && typeof e.code === 'string')
41
+ void onCode(e.code).catch(abort);
42
+ else if (e.type === 'closed') { /* process exit owns cleanup acknowledgement */ }
43
+ else
44
+ abort();
45
+ }
46
+ catch {
47
+ abort();
48
+ }
49
+ }
50
+ });
51
+ // Chromium diagnostics are deliberately not forwarded to logs; failures use fixed public codes.
52
+ child.stderr.resume();
53
+ child.stdin.on('error', abort);
54
+ child.once('error', () => { failed = true; });
55
+ const closed = new Promise((yes, no) => child.once('close', async (code) => {
56
+ signal.removeEventListener('abort', abort);
57
+ rejectReady(new Error('HELPER_CLOSED'));
58
+ try {
59
+ await rm(userDataDir, { recursive: true });
60
+ if (failed || code !== 0)
61
+ no(new Error('HELPER_FAILED'));
62
+ else
63
+ yes();
64
+ }
65
+ catch {
66
+ no(new Error('HELPER_CLEANUP_FAILED'));
67
+ }
68
+ }));
69
+ // The child may fail before the caller receives its handle.
70
+ void closed.catch(() => { });
71
+ signal.addEventListener('abort', abort, { once: true });
72
+ child.stdin.write(JSON.stringify({ ...boot, userDataDir }) + '\n');
73
+ if (signal.aborted)
74
+ abort();
75
+ try {
76
+ await ready;
77
+ return { closed, close };
78
+ }
79
+ catch {
80
+ await closed.catch(() => { });
81
+ throw new Error('HELPER_FAILED');
82
+ }
83
+ }
@@ -0,0 +1,3 @@
1
+ import type { Result } from './api.ts';
2
+ import type { Enrollment } from './ports.ts';
3
+ export declare function readEnrollment(packageDir: string, trustDir: string, maxMetadataBytes?: number): Promise<Result<Enrollment>>;
@@ -0,0 +1,78 @@
1
+ import { lstat, open, rename, rm } from 'node:fs/promises';
2
+ import { join, isAbsolute } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { fail, ok } from "./needs.js";
5
+ import { privateDirectory, privateFile, readPrivate } from "./private-files.js";
6
+ export async function readEnrollment(packageDir, trustDir, maxMetadataBytes) {
7
+ try {
8
+ if (!isAbsolute(packageDir) || !isAbsolute(trustDir))
9
+ return fail('ENROLLMENT_INVALID');
10
+ const dir = await lstat(packageDir);
11
+ if (!dir.isDirectory() || dir.isSymbolicLink() || dir.uid !== process.getuid() || (dir.mode & 0o077) !== 0)
12
+ return fail('ENROLLMENT_INVALID');
13
+ const wire = JSON.parse(await readPrivate(join(packageDir, 'enroll.json'), maxMetadataBytes));
14
+ if (wire.schemaVersion !== 1 || !/^emp-[a-z0-9][a-z0-9-]*$/.test(wire.employee?.username) || wire.employee.username.length > 32)
15
+ return fail('ENROLLMENT_INVALID');
16
+ const user = wire.employee.username, home = '/Users/' + user;
17
+ const rows = (v) => Array.isArray(v) && v.every(r => r && typeof r.id === 'string' && typeof r.status === 'string' && !/[\x00-\x1f\x7f]/.test(r.id + r.status));
18
+ if (!['production', 'test'].includes(wire.hostEnforcement) || wire.host?.enforcement !== wire.hostEnforcement || ![0, 1].includes(wire.checkExitCode) || !rows(wire.failedChecks) || !rows(wire.unverifiedChecks))
19
+ return fail('ENROLLMENT_INVALID');
20
+ const hasIssues = wire.failedChecks.length + wire.unverifiedChecks.length > 0;
21
+ if ((wire.checkExitCode === 0 && hasIssues) || (wire.checkExitCode === 1 && !hasIssues) || (wire.hostEnforcement === 'production' && (hasIssues || wire.checkExitCode !== 0)))
22
+ return fail('ENROLLMENT_INVALID');
23
+ const hostContext = { enforcement: wire.hostEnforcement, checkExitCode: wire.checkExitCode, failedChecks: wire.failedChecks.map(({ id, status }) => ({ id, status })), unverifiedChecks: wire.unverifiedChecks.map(({ id, status }) => ({ id, status })) };
24
+ if (wire.employee.home !== home || wire.employee.projectsRoot !== home + '/projects' || wire.employee.tag !== 'tag:aw-' + user || wire.host?.tag !== 'tag:aw-host' || !Number.isInteger(wire.host?.sshPort) || wire.host.sshPort < 1 || wire.host.sshPort > 65535 || !(/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/).test(wire.host?.name))
25
+ return fail('ENROLLMENT_INVALID');
26
+ if (wire.session?.protocolVersion !== 1 || typeof wire.session.command !== 'string' || !wire.session.command.startsWith('/') || typeof wire.session.settingsPath !== 'string' || !wire.session.settingsPath.startsWith('/') || wire.session.root !== home + '/.aw/s' || [wire.session.command, wire.session.settingsPath].some((v) => /[\x00-\x1f\x7f]/.test(v)) || wire.authKeyDelivery?.mode !== 'file' || wire.authKeyDelivery.source !== 'administrator-stdin' || typeof wire.authKeyDelivery.path !== 'string' || !(/^[a-zA-Z0-9._-]+$/).test(wire.authKeyDelivery.path) || ['.', '..'].includes(wire.authKeyDelivery.path))
27
+ return fail('ENROLLMENT_INVALID');
28
+ // Metadata only. The enrollment secret is opened exclusively by N.
29
+ let authKeyFile = join(packageDir, wire.authKeyDelivery.path);
30
+ try {
31
+ await privateFile(authKeyFile);
32
+ }
33
+ catch (error) {
34
+ if (error.code === 'ENOENT')
35
+ authKeyFile = undefined;
36
+ else
37
+ throw error;
38
+ }
39
+ const keys = wire.host.knownHosts;
40
+ if (!Array.isArray(keys) || keys.length === 0)
41
+ return fail('HOST_TRUST_REQUIRED');
42
+ if (keys.some((k) => typeof k !== 'string' || /[\r\n\x00]/.test(k) || !(/^(\S+) (ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp\d+|sk-ssh-ed25519@openssh.com) [A-Za-z0-9+/=]+(?: .*)?$/).test(k)))
43
+ return fail('HOST_TRUST_REQUIRED');
44
+ const expected = wire.host.sshPort === 22 ? wire.host.name : `[${wire.host.name}]:${wire.host.sshPort}`;
45
+ if (keys.some((k) => !k.split(' ')[0].split(',').includes(expected)))
46
+ return fail('HOST_TRUST_REQUIRED');
47
+ await privateDirectory(trustDir);
48
+ const destination = join(trustDir, 'known_hosts'), temp = join(trustDir, 'known_hosts-' + randomUUID());
49
+ // N holds this immutable handoff path; reopening must never replace its inode.
50
+ let exists = false;
51
+ try {
52
+ const cached = await readPrivate(destination, maxMetadataBytes);
53
+ if (cached !== keys.join('\n') + '\n')
54
+ return fail('HOST_KEY_MISMATCH');
55
+ exists = true;
56
+ }
57
+ catch (error) {
58
+ if (error.code !== 'ENOENT')
59
+ throw error;
60
+ }
61
+ if (!exists) {
62
+ const file = await open(temp, 'wx', 0o600);
63
+ try {
64
+ await file.writeFile(keys.join('\n') + '\n');
65
+ await file.close();
66
+ await rename(temp, destination);
67
+ }
68
+ finally {
69
+ await file.close();
70
+ await rm(temp, { force: true });
71
+ }
72
+ }
73
+ return ok({ authKeyFile, hostContext, host: wire.host.name, username: user, port: wire.host.sshPort, projectRoot: wire.employee.projectsRoot, knownHostsFile: destination, session: { command: wire.session.command, root: wire.session.root, settingsPath: wire.session.settingsPath, protocolVersion: 1 } });
74
+ }
75
+ catch {
76
+ return fail('ENROLLMENT_INVALID');
77
+ }
78
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import z from '@deepseek-ai/schemastery';
3
+ export type * from './cordis.ts';
4
+ import type { Ports } from './ports.ts';
5
+ import { type ComponentInventory } from './components.ts';
6
+ import type { NetworkSettings } from './network.ts';
7
+ import type { AssemblySettings } from './profile-activation.ts';
8
+ export { writeNetworkConfig, createNetworkClient } from './network.ts';
9
+ export type * from './api.ts';
10
+ export { createConnect } from './connect.ts';
11
+ export { sshArgs } from './ssh.ts';
12
+ export declare const name = "ai-workbench-connect";
13
+ export declare const inject: string[];
14
+ export interface Config {
15
+ inventory?: ComponentInventory;
16
+ enrollmentMetadataMaxBytes?: number;
17
+ enrollmentPackageDir?: string;
18
+ trustDir?: string;
19
+ commandOutputMaxBytes?: number;
20
+ network?: (NetworkSettings & AssemblySettings);
21
+ helper?: {
22
+ componentDownloadMaxRedirects?: number;
23
+ remoteKillGraceMs?: number;
24
+ maxRedirects?: number;
25
+ source?: string;
26
+ sha256?: string;
27
+ cacheDir?: string;
28
+ maxDownloadBytes?: number;
29
+ maxExtractedBytes?: number;
30
+ executable?: string;
31
+ loginOutputMaxBytes?: number;
32
+ ipcMaxBytes?: number;
33
+ };
34
+ }
35
+ export declare const Config: z<Config>;
36
+ export declare function register(ctx: Context, ports: Ports, inventory?: ComponentInventory, extend?: (api: import('./api.ts').ConnectApi) => void): void;
37
+ export declare function apply(ctx: Context, config?: Config): void;
38
+ export { createProfileEnrollment } from './profile-enrollment.ts';
39
+ export { createWorkspaceBindings } from './workspace-bindings.ts';
40
+ export { profileSettings } from './profile-settings.ts';
package/lib/index.js ADDED
@@ -0,0 +1,48 @@
1
+ import z from '@deepseek-ai/schemastery';
2
+ import { createConnect } from "./connect.js";
3
+ import { configuredPorts } from "./integration.js";
4
+ import { attachProfileRuntime } from "./profile-runtime.js";
5
+ import { profileSettings } from "./profile-settings.js";
6
+ import { createComponentProgress } from "./component-download.js";
7
+ import { L_DEFAULTS, NETWORK_DEFAULTS } from "./defaults.js";
8
+ import { registerComponents } from "./components.js";
9
+ export { writeNetworkConfig, createNetworkClient } from "./network.js";
10
+ export { createConnect } from "./connect.js";
11
+ export { sshArgs } from "./ssh.js";
12
+ export const name = 'ai-workbench-connect';
13
+ export const inject = ['aiWorkbenchNetwork'];
14
+ const positive = () => z.number().step(1).min(1);
15
+ // Administrator settings are optional in schema so the loaded service can explain missing prerequisites.
16
+ // aiWorkbenchNetwork itself is a required Cordis injection; missing N holds this module from loading.
17
+ // Own technical defaults use the approved settings record; no global DSH policy is changed.
18
+ export const Config = z.object({
19
+ enrollmentMetadataMaxBytes: positive().max(2147483647).default(L_DEFAULTS.enrollmentMetadataMaxBytes).description('开通元数据与工作区关联文件字节上限;默认1MiB,可由管理员修改;不读取key内容'),
20
+ inventory: z.object({ network: z.string(), connect: z.string(), claude: z.string(), codex: z.string() }).description('由bundle真实包版本自动生成,只读交付事实,不是策略默认'),
21
+ enrollmentPackageDir: z.string().description('开通包目录:由安装交付,员工不手填;owner已定稿,交付显式配置'),
22
+ trustDir: z.string().description('主机信任缓存目录;owner已定稿,交付显式配置'),
23
+ commandOutputMaxBytes: positive().default(L_DEFAULTS.commandOutputMaxBytes).description('CLI检查输出内存上限(字节);owner已定1048576,管理员可改'),
24
+ network: z.object({ executable: z.string(), runtimeDir: z.string(), stateDir: z.string(), configFile: z.string(), helperLogFile: z.string(), nodeName: z.string(), startupObservationSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.startupObservationSeconds), shutdownObservationSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.shutdownObservationSeconds), controlResponseMaxBytes: positive().default(NETWORK_DEFAULTS.controlResponseMaxBytes), downloadMaxRedirects: z.number().step(1).min(0).default(NETWORK_DEFAULTS.downloadMaxRedirects), proxyListen: z.string().default(NETWORK_DEFAULTS.proxyListen), logUpload: z.boolean().default(NETWORK_DEFAULTS.logUpload), statusRefreshIntervalSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.statusRefreshIntervalSeconds), dialTimeoutSeconds: z.number().min(1e-9).default(NETWORK_DEFAULTS.dialTimeoutSeconds), headerTimeoutSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.headerTimeoutSeconds), enrollmentObservationSeconds: z.number().min(1e-9).default(NETWORK_DEFAULTS.enrollmentObservationSeconds), httpMaxHeaderBytes: positive().default(NETWORK_DEFAULTS.httpMaxHeaderBytes), httpMaxResponseHeaderBytes: positive().default(NETWORK_DEFAULTS.httpMaxResponseHeaderBytes), controlMaxBytes: positive() }).description('N v1 交接设置;沿owner已定技术值自动装配,管理员可改;路径由当前用户/profile派生,员工不填'),
25
+ helper: z.object({
26
+ componentDownloadMaxRedirects: z.number().step(1).min(0).default(L_DEFAULTS.componentDownloadMaxRedirects).description('组件直接HTTPS下载的重定向次数上限;默认3,0拒绝重定向;管理员可改,不影响OAuth主机出口'),
27
+ remoteKillGraceMs: z.number().step(1).min(0).max(2147483647).default(L_DEFAULTS.remoteKillGraceMs).description('远端本轮登录进程组结束宽限毫秒;owner已定1000,0立即KILL,管理员可改'),
28
+ maxRedirects: z.number().step(1).min(0).description('兼容旧配置;0.5默认路径改由N插件downloadMaxRedirects控制,此项不再生效'),
29
+ source: z.string().description('darwin-arm64默认已定Electron44.0.0官方ZIP;其他平台无默认,管理员可改'), sha256: z.string().description('darwin-arm64默认已定44.0.0下载SHA256;管理员可改,必须校验'),
30
+ cacheDir: z.string().description('默认从当前profile自动派生ai-workbench-connect/login-component-cache;管理员可改,员工不填路径'), maxDownloadBytes: positive().default(L_DEFAULTS.maxDownloadBytes).description('下载字节上限;owner已定200000000,管理员可改'), maxExtractedBytes: positive().default(L_DEFAULTS.maxExtractedBytes).description('解压字节上限;owner已定600000000,管理员可改'),
31
+ executable: z.string().description('darwin-arm64默认Electron.app/Contents/MacOS/Electron;其他平台无默认,管理员可改'), loginOutputMaxBytes: positive().default(L_DEFAULTS.loginOutputMaxBytes).description('登录进程输出字节上限;owner已定1048576,管理员可改'), ipcMaxBytes: positive().default(L_DEFAULTS.ipcMaxBytes).description('私有IPC字节上限;owner已定1048576,管理员可改'),
32
+ }),
33
+ });
34
+ export function register(ctx, ports, inventory, extend) {
35
+ const api = createConnect(ports, state => ctx.emit('ai-workbench/connect-state', state), { providers: inventory && !inventory.codex ? ['claude'] : ['codex', 'claude'], onConnectionState: state => ctx.emit('ai-workbench/connection-state', state), onPromptState: state => ctx.emit('ai-workbench/login-prompt-state', state), onPromptResult: result => ctx.emit('ai-workbench/login-prompt-result', result) });
36
+ extend?.(api);
37
+ ctx.provide('aiWorkbenchConnect', api);
38
+ ctx.effect(() => { void api.refreshLoginPrompts(); return () => api.dispose(); });
39
+ }
40
+ export function apply(ctx, config = {}) {
41
+ config = profileSettings(config, ctx.get('profileContext')?.dir);
42
+ const network = ctx.get('aiWorkbenchNetwork'), component = createComponentProgress(), ports = configuredPorts(config, network, component);
43
+ register(ctx, ports, config.inventory, api => attachProfileRuntime(ctx, api, ports, config, network));
44
+ registerComponents(ctx, config.inventory, config.helper?.cacheDir, component);
45
+ }
46
+ export { createProfileEnrollment } from "./profile-enrollment.js";
47
+ export { createWorkspaceBindings } from "./workspace-bindings.js";
48
+ export { profileSettings } from "./profile-settings.js";
@@ -0,0 +1,5 @@
1
+ import type { Config } from './index.ts';
2
+ import type { Ports } from './ports.ts';
3
+ import { type NetworkProvider } from './provider.ts';
4
+ import { type ComponentProgressStore } from './component-download.ts';
5
+ export declare function configuredPorts(config: Config, provider?: NetworkProvider, component?: ComponentProgressStore): Ports;
@@ -0,0 +1,47 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { readEnrollment } from "./enrollment.js";
3
+ import { adaptNetwork } from "./provider.js";
4
+ import { createRunner, createRemoteLogin } from "./processes.js";
5
+ import { createSsh, sshArgs } from "./ssh.js";
6
+ import { createProjects } from "./projects.js";
7
+ import { remoteLoginCommand } from "./remote-login.js";
8
+ import { runLogin } from "./login.js";
9
+ import { ensureHelper } from "./cache.js";
10
+ import { launchElectron } from "./electron.js";
11
+ import { need, fail } from "./needs.js";
12
+ import { createComponentProgress, downloadComponent } from "./component-download.js";
13
+ export function configuredPorts(config, provider, component = createComponentProgress()) {
14
+ const ports = {};
15
+ const network = provider ? adaptNetwork(provider) : undefined;
16
+ if (config.enrollmentPackageDir && config.trustDir)
17
+ ports.enrollment = { read: () => readEnrollment(config.enrollmentPackageDir, config.trustDir) };
18
+ else if (provider)
19
+ ports.enrollment = { read: async () => fail('SETTINGS_REQUIRED') };
20
+ if (config.commandOutputMaxBytes !== undefined) {
21
+ const runner = createRunner(config.commandOutputMaxBytes);
22
+ ports.ssh = createSsh(runner, config.commandOutputMaxBytes);
23
+ ports.projects = createProjects(runner);
24
+ }
25
+ else if (provider)
26
+ ports.ssh = { verify: async () => fail('SETTINGS_REQUIRED'), status: async () => fail('SETTINGS_REQUIRED') };
27
+ if (network) {
28
+ ports.network = network;
29
+ ports.login = { run: async (target, provider, signal) => {
30
+ const h = config.helper;
31
+ if (!h || !Number.isSafeInteger(h.remoteKillGraceMs) || h.remoteKillGraceMs < 0 || h.remoteKillGraceMs > 2147483647 || ![h.source, h.sha256, h.cacheDir, h.executable].every(x => typeof x === 'string' && x) || !Number.isSafeInteger(h.componentDownloadMaxRedirects) || h.componentDownloadMaxRedirects < 0 || ![h.maxDownloadBytes, h.maxExtractedBytes, h.loginOutputMaxBytes, h.ipcMaxBytes].every(x => Number.isSafeInteger(x) && x > 0))
32
+ return { ok: false, error: need('SETTINGS_REQUIRED') };
33
+ const attempt = component.begin();
34
+ const result = await ensureHelper(h, { download: source => downloadComponent(source, signal, h.componentDownloadMaxRedirects), signal, onProgress: attempt.update });
35
+ if (!result.ok)
36
+ return result;
37
+ if (signal.aborted)
38
+ return fail('LOGIN_FAILED');
39
+ return runLogin(target, provider, {
40
+ start: (t, p, s) => createRemoteLogin('ssh', [...sshArgs(t), remoteLoginCommand(p, h.remoteKillGraceMs)], p, h.loginOutputMaxBytes, s, 'remote-control'),
41
+ acquireProxy: (_t, s) => network.acquireProxy(s),
42
+ open: (boot, onCode, s) => launchElectron({ executable: result.value, entry: fileURLToPath(new URL('../helper/main.cjs', import.meta.url)), cacheDir: h.cacheDir, ipcMaxBytes: h.ipcMaxBytes }, boot, onCode, s),
43
+ }, signal);
44
+ } };
45
+ }
46
+ return ports;
47
+ }