@ai-workbench/connect 0.0.0-stage → 0.7.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 (131) hide show
  1. package/NOTICE.md +18 -0
  2. package/README.md +26 -2
  3. package/client/index.cjs +124 -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 +32 -0
  21. package/lib/component-download.js +76 -0
  22. package/lib/component-source.d.ts +52 -0
  23. package/lib/component-source.js +146 -0
  24. package/lib/components.d.ts +10 -0
  25. package/lib/components.js +154 -0
  26. package/lib/connect.d.ts +10 -0
  27. package/lib/connect.js +246 -0
  28. package/lib/cordis.d.ts +15 -0
  29. package/lib/cordis.js +1 -0
  30. package/lib/defaults.d.ts +30 -0
  31. package/lib/defaults.js +4 -0
  32. package/lib/electron.d.ts +8 -0
  33. package/lib/electron.js +83 -0
  34. package/lib/enrollment.d.ts +3 -0
  35. package/lib/enrollment.js +78 -0
  36. package/lib/index.d.ts +42 -0
  37. package/lib/index.js +55 -0
  38. package/lib/integration.d.ts +6 -0
  39. package/lib/integration.js +56 -0
  40. package/lib/login.d.ts +35 -0
  41. package/lib/login.js +81 -0
  42. package/lib/native-import-picker.d.ts +11 -0
  43. package/lib/native-import-picker.js +49 -0
  44. package/lib/needs.d.ts +9 -0
  45. package/lib/needs.js +52 -0
  46. package/lib/network.d.ts +26 -0
  47. package/lib/network.js +226 -0
  48. package/lib/physical-fs.d.ts +3 -0
  49. package/lib/physical-fs.js +7 -0
  50. package/lib/ports.d.ts +43 -0
  51. package/lib/ports.js +1 -0
  52. package/lib/private-files.d.ts +3 -0
  53. package/lib/private-files.js +41 -0
  54. package/lib/processes.d.ts +5 -0
  55. package/lib/processes.js +147 -0
  56. package/lib/profile-activation.d.ts +32 -0
  57. package/lib/profile-activation.js +118 -0
  58. package/lib/profile-enrollment.d.ts +21 -0
  59. package/lib/profile-enrollment.js +275 -0
  60. package/lib/profile-runtime.d.ts +6 -0
  61. package/lib/profile-runtime.js +47 -0
  62. package/lib/profile-settings.d.ts +2 -0
  63. package/lib/profile-settings.js +13 -0
  64. package/lib/projects.d.ts +4 -0
  65. package/lib/projects.js +41 -0
  66. package/lib/provider.d.ts +73 -0
  67. package/lib/provider.js +39 -0
  68. package/lib/remote-login.d.ts +2 -0
  69. package/lib/remote-login.js +9 -0
  70. package/lib/ssh.d.ts +10 -0
  71. package/lib/ssh.js +78 -0
  72. package/lib/startup.d.ts +18 -0
  73. package/lib/startup.js +211 -0
  74. package/lib/workspace-bindings.d.ts +32 -0
  75. package/lib/workspace-bindings.js +328 -0
  76. package/locale/en.json +6 -0
  77. package/locale/zh.json +6 -0
  78. package/node_modules/argparse/LICENSE +254 -0
  79. package/node_modules/argparse/README.md +84 -0
  80. package/node_modules/argparse/argparse.js +3707 -0
  81. package/node_modules/argparse/lib/sub.js +67 -0
  82. package/node_modules/argparse/lib/textwrap.js +440 -0
  83. package/node_modules/argparse/package.json +31 -0
  84. package/node_modules/js-yaml/LICENSE +21 -0
  85. package/node_modules/js-yaml/README.md +233 -0
  86. package/node_modules/js-yaml/bin/js-yaml.js +117 -0
  87. package/node_modules/js-yaml/dist/js-yaml.js +3022 -0
  88. package/node_modules/js-yaml/dist/js-yaml.js.map +1 -0
  89. package/node_modules/js-yaml/dist/js-yaml.min.js +2 -0
  90. package/node_modules/js-yaml/dist/js-yaml.min.js.map +1 -0
  91. package/node_modules/js-yaml/dist/js-yaml.mjs +3082 -0
  92. package/node_modules/js-yaml/dist/js-yaml.mjs.map +1 -0
  93. package/node_modules/js-yaml/index.js +44 -0
  94. package/node_modules/js-yaml/lib/common.js +50 -0
  95. package/node_modules/js-yaml/lib/dumper.js +937 -0
  96. package/node_modules/js-yaml/lib/exception.js +51 -0
  97. package/node_modules/js-yaml/lib/index_vite_proxy.tmp.mjs +37 -0
  98. package/node_modules/js-yaml/lib/loader.js +1803 -0
  99. package/node_modules/js-yaml/lib/schema/core.js +9 -0
  100. package/node_modules/js-yaml/lib/schema/default.js +20 -0
  101. package/node_modules/js-yaml/lib/schema/failsafe.js +14 -0
  102. package/node_modules/js-yaml/lib/schema/json.js +17 -0
  103. package/node_modules/js-yaml/lib/schema.js +109 -0
  104. package/node_modules/js-yaml/lib/snippet.js +96 -0
  105. package/node_modules/js-yaml/lib/type/binary.js +122 -0
  106. package/node_modules/js-yaml/lib/type/bool.js +35 -0
  107. package/node_modules/js-yaml/lib/type/float.js +99 -0
  108. package/node_modules/js-yaml/lib/type/int.js +142 -0
  109. package/node_modules/js-yaml/lib/type/map.js +8 -0
  110. package/node_modules/js-yaml/lib/type/merge.js +12 -0
  111. package/node_modules/js-yaml/lib/type/null.js +35 -0
  112. package/node_modules/js-yaml/lib/type/omap.js +45 -0
  113. package/node_modules/js-yaml/lib/type/pairs.js +50 -0
  114. package/node_modules/js-yaml/lib/type/seq.js +8 -0
  115. package/node_modules/js-yaml/lib/type/set.js +29 -0
  116. package/node_modules/js-yaml/lib/type/str.js +8 -0
  117. package/node_modules/js-yaml/lib/type/timestamp.js +88 -0
  118. package/node_modules/js-yaml/lib/type.js +66 -0
  119. package/node_modules/js-yaml/package.json +83 -0
  120. package/node_modules/pend/LICENSE +23 -0
  121. package/node_modules/pend/README.md +41 -0
  122. package/node_modules/pend/index.js +55 -0
  123. package/node_modules/pend/package.json +18 -0
  124. package/node_modules/pend/test.js +137 -0
  125. package/node_modules/yauzl/LICENSE +21 -0
  126. package/node_modules/yauzl/README.md +1015 -0
  127. package/node_modules/yauzl/crc32.js +45 -0
  128. package/node_modules/yauzl/fd-slicer.js +188 -0
  129. package/node_modules/yauzl/index.js +1096 -0
  130. package/node_modules/yauzl/package.json +40 -0
  131. package/package.json +89 -4
@@ -0,0 +1,154 @@
1
+ import { ComponentDownloadError } from "./component-download.js";
2
+ import { L_DEFAULTS } from "./defaults.js";
3
+ import { fail } from "./needs.js";
4
+ import { employeeMessage } from "./startup.js";
5
+ export function registerComponents(ctx, inventory, temporaryDataParent, component, sources, observationIntervalMs = L_DEFAULTS.componentObservationIntervalMs) {
6
+ const api = () => ctx.get('aiWorkbenchConnect');
7
+ const getState = () => {
8
+ const network = ctx.get('aiWorkbenchNetwork'), connect = api(), state = connect?.getState(), networkState = network?.getState();
9
+ return { workspaceBindings: connect?.getWorkspaceBindingState?.() ?? null, loginComponent: { temporaryDataParent: temporaryDataParent ?? null, download: component?.getState() ?? null, source: sources?.getState() ?? null, observationIntervalMs }, startup: connect?.getLoginPromptState?.() ?? null, inventory: inventory ?? null, network: { serviceProvided: !!network, phase: networkState?.phase ?? 'missing', needCode: networkState?.error?.code ?? networkState?.need?.code }, enrollment: connect?.getEnrollmentState?.() ?? null, connection: connect?.getConnectionState?.() ?? null, connect: { serviceProvided: !!connect, phase: state?.phase ?? 'missing', message: state?.need ? employeeMessage(state.need.code) : '连接服务等待就绪' }, codex: inventory ? { installed: !!inventory.codex, message: inventory.codex ? 'Codex 组件已安装' : 'Codex 组件未安装' } : { installed: null, message: '组件清单未装配' } };
10
+ };
11
+ ctx.provide('aiWorkbenchComponents', { getState });
12
+ ctx.inject(['webServer', 'connection'], host => {
13
+ const web = host.get('webServer'), connection = host.get('connection');
14
+ host.effect(() => {
15
+ const streams = new Set();
16
+ const off = web.register({ kind: 'prefix', path: '/ai-workbench-connect', handler: async (req, res) => {
17
+ const rejected = connection.requestRejection(req);
18
+ res.setHeader('Cache-Control', 'no-store');
19
+ if (rejected !== undefined) {
20
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
21
+ res.statusCode = rejected;
22
+ res.end(JSON.stringify({ error: 'forbidden' }));
23
+ return;
24
+ }
25
+ const path = new URL(req.url ?? '/', 'http://dsh').pathname, service = api();
26
+ if (req.method === 'GET' && path === '/ai-workbench-connect/events' && service) {
27
+ res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
28
+ res.statusCode = 200;
29
+ let closed = false;
30
+ let unprompt = () => { }, unconnection = () => { }, unenrollment = () => { }, unnative = () => { }, undownload = () => { }, unsource = () => { };
31
+ const close = () => { if (closed)
32
+ return; closed = true; unprompt(); unconnection(); unenrollment(); unnative(); undownload(); unsource(); streams.delete(close); res.end(); };
33
+ const send = () => { if (closed)
34
+ return; try {
35
+ if (!res.write('data: ' + JSON.stringify(getState()) + '\n\n'))
36
+ close();
37
+ }
38
+ catch {
39
+ close();
40
+ } };
41
+ unprompt = service.onLoginPromptState(send);
42
+ unconnection = service.onConnectionState(send);
43
+ unenrollment = service.onEnrollmentState?.(send) ?? (() => { });
44
+ unnative = service.onWorkspaceBindingState?.(send) ?? (() => { });
45
+ undownload = component?.onState(send) ?? (() => { });
46
+ unsource = sources?.onState(send) ?? (() => { });
47
+ streams.add(close);
48
+ res.once('close', close);
49
+ send();
50
+ return;
51
+ }
52
+ if (req.method === 'POST' && /^\/ai-workbench-connect\/component-download\/cancel\/[a-zA-Z0-9-]+$/.test(path) && component) {
53
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
54
+ if (Number(req.headers?.['content-length'] ?? 0) > 0 || req.headers?.['transfer-encoding']) {
55
+ res.statusCode = 400;
56
+ res.end(JSON.stringify({ error: 'no-input-accepted' }));
57
+ return;
58
+ }
59
+ const cancelled = component.cancel(path.split('/').at(-1));
60
+ res.statusCode = cancelled ? 200 : 409;
61
+ res.end(JSON.stringify({ ...getState(), ...(!cancelled ? { actionErrorCode: 'COMPONENT_DOWNLOAD_NOT_ACTIVE', actionError: '该组件下载已经结束或更新,请使用当前状态。' } : {}) }));
62
+ return;
63
+ }
64
+ if (req.method === 'POST' && /^\/ai-workbench-connect\/component-source\/(github|npmmirror)$/.test(path) && sources) {
65
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
66
+ if (Number(req.headers?.['content-length'] ?? 0) > 0 || req.headers?.['transfer-encoding']) {
67
+ res.statusCode = 400;
68
+ res.end(JSON.stringify({ error: 'no-input-accepted' }));
69
+ return;
70
+ }
71
+ try {
72
+ await sources.select(path.split('/').at(-1));
73
+ res.statusCode = 200;
74
+ res.end(JSON.stringify(getState()));
75
+ }
76
+ catch (e) {
77
+ res.statusCode = 409;
78
+ res.end(JSON.stringify({ ...getState(), actionErrorCode: e instanceof ComponentDownloadError ? e.message : 'COMPONENT_SOURCE_STATE_INVALID', actionError: '下载源未更改;请结束当前下载或授权后重选,或检查技术详情。' }));
79
+ }
80
+ return;
81
+ }
82
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
83
+ let actionError, actionErrorCode;
84
+ if (req.method === 'POST' && service) {
85
+ let result;
86
+ if (path === '/ai-workbench-connect/enrollment/import') {
87
+ if (Number(req.headers?.['content-length'] ?? 0) > 0 || req.headers?.['transfer-encoding']) {
88
+ res.statusCode = 400;
89
+ res.end(JSON.stringify({ error: 'no-input-accepted' }));
90
+ return;
91
+ }
92
+ result = await service.importEnrollment();
93
+ }
94
+ else if (/^\/ai-workbench-connect\/workspace-notice\/[a-zA-Z0-9-]+\/displayed$/.test(path)) {
95
+ if (Number(req.headers?.['content-length'] ?? 0) > 0 || req.headers?.['transfer-encoding']) {
96
+ res.statusCode = 400;
97
+ res.end(JSON.stringify({ error: 'no-input-accepted' }));
98
+ return;
99
+ }
100
+ const id = /\/workspace-notice\/([a-zA-Z0-9-]+)\/displayed$/.exec(path)[1];
101
+ result = await service.acknowledgeWorkspaceNotice({ noticeId: id, surface: 'settings', outcome: 'displayed' });
102
+ }
103
+ else if (path === '/ai-workbench-connect/check')
104
+ result = await service.refreshLoginPrompts();
105
+ else if (/^\/ai-workbench-connect\/start-login\/(claude|codex)\/[a-zA-Z0-9-]+\/\d+$/.test(path)) {
106
+ const [, provider, instance, revision] = /\/start-login\/(claude|codex)\/([a-zA-Z0-9-]+)\/(\d+)$/.exec(path);
107
+ const brand = provider, shown = service.getLoginPromptState(), before = service.getConnectionState();
108
+ if (before.phase !== 'connected' || shown.instanceId !== instance || shown.revision !== Number(revision) || shown.phase === 'authorizing' || shown.logins[brand] !== false || shown.checkingProviders.includes(brand) || shown.providerNeeds[brand])
109
+ result = fail('LOGIN_PROMPT_INVALID');
110
+ else {
111
+ const requested = await service.requestLoginPrompt(brand, 'user'), after = service.getConnectionState();
112
+ if (after.phase !== 'connected' || after.instanceId !== before.instanceId || after.epoch !== before.epoch)
113
+ result = fail('LOGIN_PROMPT_INVALID');
114
+ else if (!requested.ok)
115
+ result = requested;
116
+ else if (requested.value.phase !== 'login-required' || requested.value.provider !== brand || !requested.value.requestId)
117
+ result = fail('LOGIN_PROMPT_INVALID');
118
+ else
119
+ result = await service.respondLoginPrompt(requested.value.requestId, 'authorize');
120
+ }
121
+ }
122
+ else if (/^\/ai-workbench-connect\/(request|authorize)\/(claude|codex)$/.test(path)) {
123
+ const provider = /\/(claude|codex)$/.exec(path)[1];
124
+ result = await service.requestLoginPrompt(provider, 'user');
125
+ }
126
+ else if (/^\/ai-workbench-connect\/respond\/[a-zA-Z0-9-]+\/(authorize|defer)$/.test(path)) {
127
+ const [, id, action] = /\/respond\/([a-zA-Z0-9-]+)\/(authorize|defer)$/.exec(path);
128
+ result = await service.respondLoginPrompt(id, action);
129
+ }
130
+ else {
131
+ res.statusCode = 404;
132
+ res.end(JSON.stringify({ error: 'not-found' }));
133
+ return;
134
+ }
135
+ if (!result.ok) {
136
+ res.statusCode = 409;
137
+ actionErrorCode = result.error.code;
138
+ actionError = result.error.code === 'LOGIN_PROMPT_INVALID' ? result.error.message : employeeMessage(result.error.code);
139
+ }
140
+ }
141
+ else if (req.method !== 'GET' || path !== '/ai-workbench-connect/status') {
142
+ res.statusCode = 404;
143
+ res.end(JSON.stringify({ error: 'not-found' }));
144
+ return;
145
+ }
146
+ if (!actionError)
147
+ res.statusCode = 200;
148
+ res.end(JSON.stringify({ ...getState(), ...(actionError ? { actionError, actionErrorCode } : {}) }));
149
+ } });
150
+ return () => { off(); for (const close of [...streams])
151
+ close(); };
152
+ });
153
+ });
154
+ }
@@ -0,0 +1,10 @@
1
+ import type { ConnectApi, ConnectState, Provider, ConnectionState, LoginPromptState, LoginPromptResult } from './api.ts';
2
+ import type { Enrollment, Ports } from './ports.ts';
3
+ export declare function validEnrollment(e: Enrollment): boolean;
4
+ export interface ConnectOptions {
5
+ providers?: Provider[];
6
+ onConnectionState?: (s: ConnectionState) => void;
7
+ onPromptState?: (s: LoginPromptState) => void;
8
+ onPromptResult?: (r: LoginPromptResult) => void;
9
+ }
10
+ export declare function createConnect(ports: Ports, emit: (s: ConnectState) => void, options?: ConnectOptions): ConnectApi;
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,30 @@
1
+ export declare const L_DEFAULTS: Readonly<{
2
+ enrollmentMetadataMaxBytes: 1048576;
3
+ commandOutputMaxBytes: 1048576;
4
+ remoteKillGraceMs: 1000;
5
+ componentDownloadMaxRedirects: 3;
6
+ componentObservationIntervalMs: 1000;
7
+ maxDownloadBytes: 200000000;
8
+ maxExtractedBytes: 600000000;
9
+ loginOutputMaxBytes: 1048576;
10
+ ipcMaxBytes: 1048576;
11
+ }>;
12
+ export declare const ELECTRON_PIN: Readonly<{
13
+ source: "https://github.com/electron/electron/releases/download/v44.0.0/electron-v44.0.0-darwin-arm64.zip";
14
+ sha256: "076d79742986e1b100b69ebecc691cb07368045e54c9087cef631b8622b76a80";
15
+ executable: "Electron.app/Contents/MacOS/Electron";
16
+ }>;
17
+ export declare const NETWORK_DEFAULTS: Readonly<{
18
+ proxyListen: "127.0.0.1:0";
19
+ logUpload: false;
20
+ statusRefreshIntervalSeconds: 2;
21
+ dialTimeoutSeconds: 20;
22
+ headerTimeoutSeconds: 10;
23
+ enrollmentObservationSeconds: 30;
24
+ httpMaxHeaderBytes: 1048576;
25
+ httpMaxResponseHeaderBytes: 1048576;
26
+ startupObservationSeconds: 35;
27
+ shutdownObservationSeconds: 10;
28
+ controlResponseMaxBytes: 65536;
29
+ downloadMaxRedirects: 3;
30
+ }>;
@@ -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, componentObservationIntervalMs: 1000, 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
+ }