@ai-workbench/connect 0.7.7 → 0.7.11
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.
- package/README.md +62 -5
- package/client/index.cjs +33 -5
- package/helper/auth-status.cjs +6 -6
- package/helper/device-panel.cjs +4 -0
- package/helper/device-panel.css +1 -0
- package/helper/device-panel.html +1 -0
- package/helper/device-preload.cjs +5 -0
- package/helper/device-window.cjs +60 -0
- package/helper/remote-command.cjs +3 -2
- package/helper/remote-login.cjs +1 -1
- package/helper/window.cjs +8 -14
- package/lib/api.d.ts +98 -1
- package/lib/auth-diagnostics.d.ts +22 -0
- package/lib/auth-diagnostics.js +45 -0
- package/lib/auth-status.js +21 -8
- package/lib/components.d.ts +1 -1
- package/lib/components.js +72 -4
- package/lib/connect.js +93 -27
- package/lib/conversation-detail-settings.d.ts +32 -0
- package/lib/conversation-detail-settings.js +119 -0
- package/lib/conversation-details.d.ts +24 -0
- package/lib/conversation-details.js +382 -0
- package/lib/cordis.d.ts +3 -1
- package/lib/defaults.d.ts +4 -0
- package/lib/defaults.js +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +23 -15
- package/lib/integration.js +21 -1
- package/lib/login-diagnostics.d.ts +26 -0
- package/lib/login-diagnostics.js +42 -0
- package/lib/login.d.ts +8 -4
- package/lib/login.js +16 -25
- package/lib/needs.js +8 -0
- package/lib/processes.js +34 -7
- package/lib/profile-runtime.js +51 -11
- package/lib/profile-settings.js +3 -3
- package/lib/ssh.js +37 -14
- package/lib/startup.d.ts +22 -1
- package/lib/startup.js +124 -33
- package/lib/workspace-bindings.d.ts +4 -1
- package/lib/workspace-bindings.js +51 -1
- package/package.json +1 -1
package/lib/login.d.ts
CHANGED
|
@@ -6,11 +6,14 @@ export interface ProxyLease {
|
|
|
6
6
|
closed?: Promise<void>;
|
|
7
7
|
release(): Promise<void>;
|
|
8
8
|
}
|
|
9
|
-
export
|
|
10
|
-
provider:
|
|
9
|
+
export type LoginBoot = {
|
|
10
|
+
provider: 'claude';
|
|
11
11
|
url: string;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
} | {
|
|
13
|
+
provider: 'codex';
|
|
14
|
+
url: string;
|
|
15
|
+
deviceCode: string;
|
|
16
|
+
};
|
|
14
17
|
export interface RemoteLogin {
|
|
15
18
|
ownership?: Promise<{
|
|
16
19
|
pid: number;
|
|
@@ -18,6 +21,7 @@ export interface RemoteLogin {
|
|
|
18
21
|
provider: string;
|
|
19
22
|
}>;
|
|
20
23
|
url: Promise<Result<string>>;
|
|
24
|
+
deviceCode?: Promise<Result<string>>;
|
|
21
25
|
completion: Promise<Result<void>>;
|
|
22
26
|
writeCode(code: string): Promise<void>;
|
|
23
27
|
close(): Promise<void>;
|
package/lib/login.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
const { isClaudeAuthorizeUrl, validateClaudePaste } = createRequire(import.meta.url)('../helper/claude-callback.cjs');
|
|
3
|
-
import { fail, NeedError
|
|
3
|
+
import { fail, NeedError } from "./needs.js";
|
|
4
4
|
export function extractLoginUrl(provider, text) {
|
|
5
|
-
for (const match of text.matchAll(/https:\/\/[^\s<>"']+/g)) {
|
|
5
|
+
for (const match of text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').matchAll(/https:\/\/[^\s<>"']+/g)) {
|
|
6
6
|
try {
|
|
7
7
|
const u = new URL(match[0]);
|
|
8
|
-
if (!u.username && !u.password && u.protocol === 'https:' && ((provider === 'codex' && u.
|
|
8
|
+
if (!u.username && !u.password && u.protocol === 'https:' && ((provider === 'codex' && u.href === 'https://auth.openai.com/codex/device') || (provider === 'claude' && isClaudeAuthorizeUrl(u))))
|
|
9
9
|
return u.href;
|
|
10
10
|
}
|
|
11
11
|
catch { /* not a complete URL yet */ }
|
|
@@ -18,8 +18,8 @@ export async function runLogin(t, p, ports, parentSignal) {
|
|
|
18
18
|
parentSignal.addEventListener('abort', parentAbort, { once: true });
|
|
19
19
|
if (parentSignal.aborted)
|
|
20
20
|
controller.abort();
|
|
21
|
-
let remote,
|
|
22
|
-
let result = fail('LOGIN_FAILED')
|
|
21
|
+
let remote, window, remove = () => { };
|
|
22
|
+
let result = fail('LOGIN_FAILED');
|
|
23
23
|
try {
|
|
24
24
|
if (signal.aborted)
|
|
25
25
|
return result;
|
|
@@ -30,17 +30,19 @@ export async function runLogin(t, p, ports, parentSignal) {
|
|
|
30
30
|
return url;
|
|
31
31
|
if (typeof url.value !== 'string' || extractLoginUrl(p, url.value) !== url.value)
|
|
32
32
|
return result;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
let deviceCode;
|
|
34
|
+
if (p === 'codex') {
|
|
35
|
+
if (!remote.deviceCode)
|
|
36
|
+
return result;
|
|
37
|
+
const device = await Promise.race([remote.deviceCode, cancelled]);
|
|
38
|
+
if (!device.ok || typeof device.value !== 'string' || !device.value || /[\x00-\x1f\x7f]/.test(device.value))
|
|
39
|
+
return result;
|
|
40
|
+
deviceCode = device.value;
|
|
41
|
+
}
|
|
41
42
|
let codeSent = false, codeError = () => { };
|
|
42
43
|
const failedCode = new Promise(r => codeError = r);
|
|
43
|
-
|
|
44
|
+
const boot = p === 'claude' ? { provider: p, url: url.value } : { provider: p, url: url.value, deviceCode: deviceCode };
|
|
45
|
+
window = await ports.open(boot, async (code) => {
|
|
44
46
|
if (p !== 'claude' || codeSent || signal.aborted || !validateClaudePaste(url.value, code)) {
|
|
45
47
|
codeError(fail('LOGIN_FAILED'));
|
|
46
48
|
return;
|
|
@@ -65,17 +67,6 @@ export async function runLogin(t, p, ports, parentSignal) {
|
|
|
65
67
|
const cleanup = await Promise.allSettled([window?.close(), remote?.close()]);
|
|
66
68
|
if (cleanup.some(r => r.status === 'rejected'))
|
|
67
69
|
result = fail('LOGIN_FAILED');
|
|
68
|
-
releasing = true;
|
|
69
|
-
if (lease) {
|
|
70
|
-
try {
|
|
71
|
-
await lease.release();
|
|
72
|
-
}
|
|
73
|
-
catch (error) {
|
|
74
|
-
result = error instanceof NeedError ? { ok: false, error: error.need } : fail('LOGIN_FAILED');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (leaseLost)
|
|
78
|
-
result = fail('CONNECTION_LOST');
|
|
79
70
|
}
|
|
80
71
|
return result;
|
|
81
72
|
}
|
package/lib/needs.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
const entries = {
|
|
2
|
+
REMOTE_DETAIL_INVALID: { layer: 'integration', message: '诊断内容格式无效或含未脱敏认证材料,已拒绝保存。', requires: '有效且已脱敏的诊断与可信连接', actor: 'plugin', action: { id: 'inspect-remote-details', label: '查看技术详情', available: false } },
|
|
3
|
+
REMOTE_DETAIL_SCOPE_INVALID: { layer: 'integration', message: '诊断内容与当前连接、原生会话或已核验品牌关联不符,已拒绝保存。', requires: '有效且已脱敏的诊断与可信连接', actor: 'plugin', action: { id: 'inspect-remote-details', label: '查看技术详情', available: false } },
|
|
4
|
+
REMOTE_DETAIL_STORAGE_FAILED: { layer: 'integration', message: '诊断记录无法持久保存或读取,结果无法确认,请联系管理员。', requires: '有效且已脱敏的诊断与可信连接', actor: 'plugin', action: { id: 'inspect-remote-details', label: '查看技术详情', available: false } },
|
|
5
|
+
REMOTE_DETAIL_TOO_LARGE: { layer: 'integration', message: '诊断事件超过 conversationDetails.maxEventBytes,已拒绝保存,未裁剪。', requires: '有效且已脱敏的诊断与可信连接', actor: 'plugin', action: { id: 'inspect-remote-details', label: '查看技术详情', available: false } },
|
|
6
|
+
REMOTE_DETAIL_SEQUENCE_INVALID: { layer: 'integration', message: '诊断事件序号没有严格递增,已拒绝保存。', requires: '有效且已脱敏的诊断与可信连接', actor: 'plugin', action: { id: 'inspect-remote-details', label: '查看技术详情', available: false } },
|
|
7
|
+
REMOTE_DETAIL_CONFLICT: { layer: 'integration', message: '此诊断事件编号已有不同内容,已拒绝保存。', requires: '有效且已脱敏的诊断与可信连接', actor: 'plugin', action: { id: 'inspect-remote-details', label: '查看技术详情', available: false } },
|
|
8
|
+
REMOTE_DETAIL_CURSOR_INVALID: { layer: 'integration', message: '详情分页位置与当前主机用户、会话、品牌或快照不符,请刷新详情。', requires: '有效且已脱敏的诊断与可信连接', actor: 'plugin', action: { id: 'inspect-remote-details', label: '查看技术详情', available: false } },
|
|
9
|
+
MODEL_CATALOG_CONTEXT_INVALID: { layer: 'integration', message: '模型目录查询的品牌无效。', requires: '有效且已脱敏的诊断与可信连接', actor: 'plugin', action: { id: 'inspect-remote-details', label: '查看技术详情', available: false } },
|
|
2
10
|
ACTIVATION_CONFLICT: { layer: 'network', message: '已有另一项开通正在进行,已停止重复开通,请联系管理员。', requires: '唯一的开通请求', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
|
|
3
11
|
ACTIVATION_CANCELLED: { layer: 'network', message: '本次网络开通已取消。', requires: '完成明确的开通操作', actor: 'plugin', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
|
|
4
12
|
ENROLLMENT_CLEANUP_UNBOUND: { layer: 'network', message: '连接资料与已保存的入网身份不匹配,已停止清理,请联系管理员。', requires: '匹配的入网身份与原始连接资料', actor: 'administrator', action: { id: 'inspect-network-enrollment', label: '联系管理员', available: false } },
|
package/lib/processes.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { StringDecoder } from 'node:string_decoder';
|
|
3
3
|
import { extractLoginUrl } from "./login.js";
|
|
4
4
|
import { fail, ok } from "./needs.js";
|
|
5
|
+
import { AuthDiagnosticError, exceptionSummary, outputSummary } from "./auth-diagnostics.js";
|
|
5
6
|
function limit(n) { if (!Number.isSafeInteger(n) || n < 1)
|
|
6
7
|
throw new Error('OUTPUT_SETTING_REQUIRED'); }
|
|
7
8
|
function start(command, args, signal) {
|
|
@@ -18,15 +19,15 @@ export function createRunner(maxBytes) {
|
|
|
18
19
|
limit(maxBytes);
|
|
19
20
|
return { run: async (command, args, signal, input) => {
|
|
20
21
|
const child = start(command, args, signal);
|
|
21
|
-
let stdout = '', stderr = '', size = 0,
|
|
22
|
-
child.stdin.on('error',
|
|
22
|
+
let stdout = '', stderr = '', size = 0, failure;
|
|
23
|
+
child.stdin.on('error', error => { failure ??= { stage: 'process-stdin', reason: 'STDIN_FAILED', ...exceptionSummary(error) }; child.kill('SIGTERM'); });
|
|
23
24
|
child.stdin.end(input);
|
|
24
25
|
const finished = new Promise((resolve, reject) => {
|
|
25
|
-
child.once('error',
|
|
26
|
+
child.once('error', error => { failure = { stage: 'process-spawn', reason: 'SPAWN_FAILED', ...exceptionSummary(error) }; });
|
|
26
27
|
for (const [stream, channel] of [[child.stdout, 'out'], [child.stderr, 'err']]) {
|
|
27
28
|
const decoder = new StringDecoder('utf8');
|
|
28
29
|
stream.on('data', (chunk) => { size += chunk.length; if (size > maxBytes) {
|
|
29
|
-
|
|
30
|
+
failure ??= { stage: 'process-output', reason: 'OUTPUT_LIMIT_EXCEEDED', maxOutputBytes: maxBytes, observedBytes: size };
|
|
30
31
|
child.kill('SIGTERM');
|
|
31
32
|
return;
|
|
32
33
|
} if (channel === 'out')
|
|
@@ -34,7 +35,7 @@ export function createRunner(maxBytes) {
|
|
|
34
35
|
else
|
|
35
36
|
stderr += decoder.write(chunk); });
|
|
36
37
|
}
|
|
37
|
-
child.once('close', code =>
|
|
38
|
+
child.once('close', (code, processSignal) => failure || signal.aborted || processSignal !== null ? reject(new AuthDiagnosticError(failure ?? { stage: 'process-runner', reason: signal.aborted ? 'CANCELLED' : 'PROCESS_SIGNAL', exitCode: code, signal: ['SIGTERM', 'SIGKILL', 'SIGABRT', 'SIGHUP', 'SIGINT', 'SIGSEGV'].includes(processSignal ?? '') ? processSignal : 'OTHER', stderr: outputSummary(stderr) }, 'COMMAND_FAILED')) : resolve({ exitCode: code, stdout, stderr }));
|
|
38
39
|
});
|
|
39
40
|
const abort = () => child.kill('SIGTERM');
|
|
40
41
|
signal.addEventListener('abort', abort, { once: true });
|
|
@@ -58,6 +59,29 @@ export async function createRemoteLogin(command, args, provider, maxBytes, signa
|
|
|
58
59
|
void ownership.catch(() => { });
|
|
59
60
|
let found = () => { };
|
|
60
61
|
const url = new Promise(r => found = r);
|
|
62
|
+
let foundCode = () => { }, deviceUrlSeen = false, awaitingCode = false, deviceSent = false;
|
|
63
|
+
const deviceCode = new Promise(r => foundCode = r);
|
|
64
|
+
const observeDevice = (line, stdout) => {
|
|
65
|
+
if (provider !== 'codex' || !stdout || deviceSent)
|
|
66
|
+
return;
|
|
67
|
+
const text = line.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').trim();
|
|
68
|
+
if (!deviceUrlSeen) {
|
|
69
|
+
if (extractLoginUrl('codex', text) === text)
|
|
70
|
+
deviceUrlSeen = true;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (text === '2. Enter this one-time code (expires in 15 minutes)') {
|
|
74
|
+
awaitingCode = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (awaitingCode && text) {
|
|
78
|
+
awaitingCode = false;
|
|
79
|
+
if (!/[\x00-\x1f\x7f]/.test(text) && !text.startsWith('AW_LOGIN_')) {
|
|
80
|
+
deviceSent = true;
|
|
81
|
+
foundCode(ok(text));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
61
85
|
const pending = new Map();
|
|
62
86
|
for (const stream of [child.stdout, child.stderr]) {
|
|
63
87
|
const record = { decoder: new StringDecoder('utf8'), line: '' };
|
|
@@ -73,6 +97,7 @@ export async function createRemoteLogin(command, args, provider, maxBytes, signa
|
|
|
73
97
|
const lines = record.line.split(/\r?\n/);
|
|
74
98
|
record.line = lines.pop();
|
|
75
99
|
for (const line of lines) {
|
|
100
|
+
observeDevice(line, stream === child.stdout);
|
|
76
101
|
if (protocol && line.startsWith('AW_LOGIN_PROCESS ')) {
|
|
77
102
|
try {
|
|
78
103
|
const v = JSON.parse(line.slice(17));
|
|
@@ -123,6 +148,8 @@ export async function createRemoteLogin(command, args, provider, maxBytes, signa
|
|
|
123
148
|
ownReject();
|
|
124
149
|
if (!urlSent)
|
|
125
150
|
found(fail('LOGIN_FAILED'));
|
|
151
|
+
if (!deviceSent)
|
|
152
|
+
foundCode(fail('LOGIN_FAILED'));
|
|
126
153
|
signal.removeEventListener('abort', abort);
|
|
127
154
|
resolve(!invalid && !signal.aborted && code === 0 && (!protocol || (owned && terminal?.exitCode === 0 && !terminal.stopping)) ? ok(undefined) : fail('LOGIN_FAILED'));
|
|
128
155
|
}));
|
|
@@ -131,9 +158,9 @@ export async function createRemoteLogin(command, args, provider, maxBytes, signa
|
|
|
131
158
|
if (signal.aborted)
|
|
132
159
|
abort();
|
|
133
160
|
child.stdin.on('error', () => { invalid = true; });
|
|
134
|
-
return { url, completion, ...(protocol ? { ownership } : {}),
|
|
161
|
+
return { url, completion, ...(provider === 'codex' ? { deviceCode } : {}), ...(protocol ? { ownership } : {}),
|
|
135
162
|
writeCode: async (code) => {
|
|
136
|
-
if (signal.aborted || child.exitCode !== null || child.signalCode !== null || child.stdin.writableEnded || /[\x00-\x20\x7f]/.test(code) || !code)
|
|
163
|
+
if (provider !== 'claude' || signal.aborted || child.exitCode !== null || child.signalCode !== null || child.stdin.writableEnded || /[\x00-\x20\x7f]/.test(code) || !code)
|
|
137
164
|
throw new Error('LOGIN_CODE_REJECTED');
|
|
138
165
|
await new Promise((yes, no) => { const failed = () => no(new Error('LOGIN_STDIN_FAILED')); child.stdin.once('error', failed); const done = () => { child.stdin.removeListener('error', failed); yes(); }; if (protocol)
|
|
139
166
|
child.stdin.write(JSON.stringify({ type: 'code', value: code }) + '\n', done);
|
package/lib/profile-runtime.js
CHANGED
|
@@ -3,7 +3,11 @@ import { createProfileEnrollment } from "./profile-enrollment.js";
|
|
|
3
3
|
import { createWorkspaceBindings } from "./workspace-bindings.js";
|
|
4
4
|
import { NeedError, need, fail } from "./needs.js";
|
|
5
5
|
import { createNativeImportPicker } from "./native-import-picker.js";
|
|
6
|
-
import {
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { readPrivate } from "./private-files.js";
|
|
8
|
+
import { createConversationDetails } from "./conversation-details.js";
|
|
9
|
+
import { createLoginDiagnostics } from "./login-diagnostics.js";
|
|
10
|
+
import { CONVERSATION_DETAIL_DEFAULTS, L_DEFAULTS } from "./defaults.js";
|
|
7
11
|
import { createProfileActivation } from "./profile-activation.js";
|
|
8
12
|
// Composition only: consume official services; never provide replacements or alter default entries.
|
|
9
13
|
export function attachProfileRuntime(ctx, api, ports, config, network) {
|
|
@@ -26,17 +30,53 @@ export function attachProfileRuntime(ctx, api, ports, config, network) {
|
|
|
26
30
|
ctx.provide('aiWorkbenchWorkspaceDiagnostics', { getLatest: () => lastWorkspaceError });
|
|
27
31
|
ctx.inject(['workspaceRegistry', 'sessions', 'sessionPersistence'], native => {
|
|
28
32
|
const registry = native.get('workspaceRegistry');
|
|
33
|
+
const readHeader = async (id) => {
|
|
34
|
+
const live = native.get('sessions');
|
|
35
|
+
const session = live?.get(id);
|
|
36
|
+
if (session)
|
|
37
|
+
return session.header;
|
|
38
|
+
const persistence = native.get('sessionPersistence');
|
|
39
|
+
if (!persistence)
|
|
40
|
+
throw new NeedError(need('DEPENDENCY_REQUIRED'));
|
|
41
|
+
return (await persistence.list()).find(s => s.header.id === id)?.header;
|
|
42
|
+
};
|
|
29
43
|
const bindings = createWorkspaceBindings({ profileDir: profile.dir, metadataMaxBytes: config.enrollmentMetadataMaxBytes ?? L_DEFAULTS.enrollmentMetadataMaxBytes, api, registry,
|
|
30
|
-
readHeader:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
readHeader, nextRevision: () => ++workspaceRevision, onState: state => ctx.emit('ai-workbench/workspace-binding-state', state), diagnostic: error => { lastWorkspaceError = error; ctx.emit('ai-workbench/workspace-diagnostic', error); } });
|
|
45
|
+
const settings = ctx.get('aiWorkbenchConversationDetailSettings');
|
|
46
|
+
const context = async () => { const current = api.getModelCatalogTarget('codex'); if (!current.ok)
|
|
47
|
+
return current; const t = current.value.target; try {
|
|
48
|
+
const trust = await readPrivate(t.knownHostsFile, config.enrollmentMetadataMaxBytes ?? L_DEFAULTS.enrollmentMetadataMaxBytes);
|
|
49
|
+
return { ok: true, value: { identity: createHash('sha256').update(JSON.stringify([t.host, t.username, t.port, trust])).digest('hex') } };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return fail('REMOTE_DETAIL_SCOPE_INVALID');
|
|
53
|
+
} };
|
|
54
|
+
const details = createConversationDetails({ profileDir: profile.dir, get maxEventBytes() { return settings ? settings.values()?.maxEventBytes ?? 0 : config.conversationDetails?.maxEventBytes ?? CONVERSATION_DETAIL_DEFAULTS.maxEventBytes; }, get pageSize() { return settings ? settings.values()?.pageSize ?? 0 : config.conversationDetails?.pageSize ?? CONVERSATION_DETAIL_DEFAULTS.pageSize; }, getConnectionState: api.getConnectionState,
|
|
55
|
+
context,
|
|
56
|
+
scope: async (sessionId, provider) => { const current = api.getModelCatalogTarget(provider); if (!current.ok)
|
|
57
|
+
return current; return bindings.verifyRemoteConversationScope(sessionId, provider, current.value.target); },
|
|
58
|
+
onState: state => ctx.emit('ai-workbench/remote-conversation-detail-state', state) });
|
|
59
|
+
api.appendRemoteConversationDetail = details.appendRemoteConversationDetail;
|
|
60
|
+
api.getRemoteConversationDetailReceipt = details.getRemoteConversationDetailReceipt;
|
|
61
|
+
api.getRemoteConversationDetailState = details.getRemoteConversationDetailState;
|
|
62
|
+
api.onRemoteConversationDetailState = details.onRemoteConversationDetailState;
|
|
63
|
+
api.listRemoteConversationDetails = details.listRemoteConversationDetails;
|
|
64
|
+
const loginDiagnostics = createLoginDiagnostics({ profileDir: profile.dir, get maxEventBytes() { return settings ? settings.values()?.maxEventBytes ?? 0 : config.conversationDetails?.maxEventBytes ?? CONVERSATION_DETAIL_DEFAULTS.maxEventBytes; }, get pageSize() { return settings ? settings.values()?.pageSize ?? 0 : config.conversationDetails?.pageSize ?? CONVERSATION_DETAIL_DEFAULTS.pageSize; }, getConnectionState: api.getConnectionState, context,
|
|
65
|
+
scope: async (sessionId, provider) => { const current = api.getModelCatalogTarget(provider); if (!current.ok)
|
|
66
|
+
return current; const header = await readHeader(sessionId); if (!header || header.id !== sessionId)
|
|
67
|
+
return fail('REMOTE_DETAIL_SCOPE_INVALID'); return context(); },
|
|
68
|
+
onState: state => ctx.emit('ai-workbench/login-diagnostic-state', state) });
|
|
69
|
+
api.appendLoginDiagnostic = loginDiagnostics.appendLoginDiagnostic;
|
|
70
|
+
api.getLoginDiagnosticReceipt = loginDiagnostics.getLoginDiagnosticReceipt;
|
|
71
|
+
api.listLoginDiagnostics = loginDiagnostics.listLoginDiagnostics;
|
|
72
|
+
api.getLoginDiagnosticState = loginDiagnostics.getLoginDiagnosticState;
|
|
73
|
+
api.onLoginDiagnosticState = loginDiagnostics.onLoginDiagnosticState;
|
|
74
|
+
const previousContractInfo = api.getContractInfo, detailContractInfo = () => ({ ...previousContractInfo(), remoteConversationDetails: 1, loginDiagnostics: 1 });
|
|
75
|
+
api.getContractInfo = detailContractInfo;
|
|
76
|
+
native.effect(() => { details.observeConnection(api.getConnectionState()); return api.onConnectionState(details.observeConnection); });
|
|
77
|
+
native.effect(() => { loginDiagnostics.observeConnection(api.getConnectionState()); return api.onConnectionState(loginDiagnostics.observeConnection); });
|
|
78
|
+
native.effect(() => () => { if (api.getContractInfo === detailContractInfo)
|
|
79
|
+
api.getContractInfo = previousContractInfo; return Promise.all([details.dispose(), loginDiagnostics.dispose()]); });
|
|
40
80
|
native.effect(() => api.onConnectionState(bindings.observeConnection));
|
|
41
81
|
api.getWorkspaceBindingState = bindings.getWorkspaceBindingState;
|
|
42
82
|
api.onWorkspaceBindingState = bindings.onWorkspaceBindingState;
|
package/lib/profile-settings.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { isAbsolute, join } from 'node:path';
|
|
2
2
|
import { COMPONENT_SOURCES, preset } from "./component-source.js";
|
|
3
|
-
import { L_DEFAULTS, ELECTRON_PIN } from "./defaults.js";
|
|
3
|
+
import { L_DEFAULTS, ELECTRON_PIN, CONVERSATION_DETAIL_DEFAULTS } from "./defaults.js";
|
|
4
4
|
// Owner approved technical values, 2026-10-20 settings decision, part two.
|
|
5
|
-
//
|
|
5
|
+
// 组件直接下载;Claude网页遵循员工系统网络,Codex暂保留N主机代理。
|
|
6
6
|
export function profileSettings(config, profileDir, platform = process.platform, arch = process.arch) {
|
|
7
7
|
if (typeof profileDir !== 'string' || !isAbsolute(profileDir))
|
|
8
8
|
return config;
|
|
9
9
|
const runtime = platform === 'darwin' && arch === 'arm64' ? { ...ELECTRON_PIN, source: COMPONENT_SOURCES.npmmirror.source } : {};
|
|
10
10
|
const chosen = config.helper?.componentDownloadSource;
|
|
11
11
|
const selected = chosen === undefined ? {} : { source: preset(chosen) ? COMPONENT_SOURCES[chosen].source : '' };
|
|
12
|
-
return { ...config, commandOutputMaxBytes: config.commandOutputMaxBytes === undefined ? L_DEFAULTS.commandOutputMaxBytes : config.commandOutputMaxBytes, helper: { ...runtime, componentDownloadMaxRedirects: L_DEFAULTS.componentDownloadMaxRedirects, componentObservationIntervalMs: L_DEFAULTS.componentObservationIntervalMs, maxDownloadBytes: L_DEFAULTS.maxDownloadBytes, maxExtractedBytes: L_DEFAULTS.maxExtractedBytes, loginOutputMaxBytes: L_DEFAULTS.loginOutputMaxBytes, ipcMaxBytes: L_DEFAULTS.ipcMaxBytes, remoteKillGraceMs: L_DEFAULTS.remoteKillGraceMs, ...config.helper, ...selected, cacheDir: config.helper?.cacheDir === undefined ? join(profileDir, 'ai-workbench-connect', 'login-component-cache') : config.helper.cacheDir } };
|
|
12
|
+
return { ...config, conversationDetails: { ...CONVERSATION_DETAIL_DEFAULTS, ...config.conversationDetails }, commandOutputMaxBytes: config.commandOutputMaxBytes === undefined ? L_DEFAULTS.commandOutputMaxBytes : config.commandOutputMaxBytes, helper: { ...runtime, componentDownloadMaxRedirects: L_DEFAULTS.componentDownloadMaxRedirects, componentObservationIntervalMs: L_DEFAULTS.componentObservationIntervalMs, maxDownloadBytes: L_DEFAULTS.maxDownloadBytes, maxExtractedBytes: L_DEFAULTS.maxExtractedBytes, loginOutputMaxBytes: L_DEFAULTS.loginOutputMaxBytes, ipcMaxBytes: L_DEFAULTS.ipcMaxBytes, remoteKillGraceMs: L_DEFAULTS.remoteKillGraceMs, ...config.helper, ...selected, cacheDir: config.helper?.cacheDir === undefined ? join(profileDir, 'ai-workbench-connect', 'login-component-cache') : config.helper.cacheDir } };
|
|
13
13
|
}
|
package/lib/ssh.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { fail, ok } from "./needs.js";
|
|
4
|
+
import { AuthDiagnosticError, authFailure, outputSummary } from "./auth-diagnostics.js";
|
|
4
5
|
export function shellQuote(s) {
|
|
5
6
|
if (/[\x00-\x1f\x7f%]/.test(s))
|
|
6
7
|
throw new Error('INVALID_PROXY_PATH');
|
|
@@ -18,28 +19,46 @@ export async function runRemote(runner, t, command, signal, input) {
|
|
|
18
19
|
const nonce = randomUUID(), source = await readFile(new URL('../helper/remote-command.cjs', import.meta.url), 'utf8');
|
|
19
20
|
const remote = 'node -e ' + "'" + source.replaceAll("'", "'\\''") + "'";
|
|
20
21
|
const raw = await runner.run('ssh', [...sshArgs(t), remote], signal, JSON.stringify({ protocolVersion: 1, nonce, command, ...input === undefined ? {} : { input } }));
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
const bad = (stage, reason, details = {}) => { throw new AuthDiagnosticError({ stage, reason, ...details }); };
|
|
23
|
+
if (signal.aborted)
|
|
24
|
+
bad('ssh-result', 'CANCELLED');
|
|
25
|
+
if (raw.exitCode !== 0 || raw.stderr !== '')
|
|
26
|
+
bad('ssh-result', raw.exitCode !== 0 ? 'SSH_EXIT_NONZERO' : 'SSH_STDERR_NONEMPTY', { exitCode: raw.exitCode, stderr: outputSummary(raw.stderr) });
|
|
23
27
|
const lines = raw.stdout.split('\n');
|
|
24
28
|
if (lines.pop() !== '')
|
|
25
|
-
|
|
29
|
+
bad('remote-frame', 'FRAME_TRUNCATED');
|
|
26
30
|
let started = false, ended = false, exitCode = null;
|
|
27
31
|
const out = [], err = [];
|
|
28
|
-
for (const line of lines) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
for (const [index, line] of lines.entries()) {
|
|
33
|
+
let v;
|
|
34
|
+
try {
|
|
35
|
+
v = JSON.parse(line);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
bad('remote-frame', 'FRAME_JSON_INVALID', { index, errorName: 'SyntaxError', output: outputSummary(line) });
|
|
39
|
+
}
|
|
40
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
41
|
+
bad('remote-frame', 'FRAME_OBJECT_INVALID', { index });
|
|
42
|
+
if (v.protocolVersion !== 1)
|
|
43
|
+
bad('remote-frame', 'FRAME_VERSION_INVALID', { index });
|
|
44
|
+
if (v.nonce !== nonce)
|
|
45
|
+
bad('remote-frame', 'FRAME_NONCE_INVALID', { index });
|
|
46
|
+
if (ended)
|
|
47
|
+
bad('remote-frame', 'FRAME_AFTER_END', { index });
|
|
32
48
|
const keys = Object.keys(v).sort().join(',');
|
|
33
49
|
if (v.type === 'start' && !started && keys === 'nonce,protocolVersion,type') {
|
|
34
50
|
started = true;
|
|
35
51
|
continue;
|
|
36
52
|
}
|
|
53
|
+
const expected = v.type === 'start' ? 'nonce,protocolVersion,type' : v.type === 'end' ? 'exitCode,nonce,protocolVersion,signal,type' : v.type === 'stdout' || v.type === 'stderr' ? 'data,nonce,protocolVersion,type' : undefined;
|
|
54
|
+
if (expected && keys !== expected)
|
|
55
|
+
bad('remote-frame', 'FRAME_KEYS_INVALID', { index, missing: expected.split(',').filter(key => !Object.hasOwn(v, key)), unexpectedCount: Object.keys(v).filter(key => !expected.split(',').includes(key)).length });
|
|
37
56
|
if (!started)
|
|
38
|
-
|
|
57
|
+
bad('remote-frame', 'FRAME_START_MISSING', { index });
|
|
39
58
|
if ((v.type === 'stdout' || v.type === 'stderr') && keys === 'data,nonce,protocolVersion,type' && typeof v.data === 'string') {
|
|
40
59
|
const bytes = Buffer.from(v.data, 'base64');
|
|
41
60
|
if (bytes.toString('base64') !== v.data)
|
|
42
|
-
|
|
61
|
+
bad('remote-frame', 'FRAME_BASE64_INVALID', { index });
|
|
43
62
|
(v.type === 'stdout' ? out : err).push(bytes);
|
|
44
63
|
continue;
|
|
45
64
|
}
|
|
@@ -48,10 +67,14 @@ export async function runRemote(runner, t, command, signal, input) {
|
|
|
48
67
|
exitCode = v.exitCode;
|
|
49
68
|
continue;
|
|
50
69
|
}
|
|
51
|
-
|
|
70
|
+
if (v.type === 'end' && v.signal !== null)
|
|
71
|
+
bad('remote-frame', 'REMOTE_COMMAND_SIGNAL', { index, signal: ['SIGTERM', 'SIGKILL', 'SIGABRT', 'SIGHUP', 'SIGINT', 'SIGSEGV'].includes(v.signal) ? v.signal : 'OTHER' });
|
|
72
|
+
if (v.type === 'end')
|
|
73
|
+
bad('remote-frame', 'FRAME_END_EXIT_INVALID', { index, exitCodeType: typeof v.exitCode, ...(typeof v.exitCode === 'number' && Number.isFinite(v.exitCode) && Number.isInteger(v.exitCode) ? { exitCode: v.exitCode } : {}) });
|
|
74
|
+
bad('remote-frame', 'FRAME_SEQUENCE_OR_VALUE_INVALID', { index });
|
|
52
75
|
}
|
|
53
76
|
if (!ended || exitCode === null)
|
|
54
|
-
|
|
77
|
+
bad('remote-frame', 'FRAME_END_MISSING');
|
|
55
78
|
return { exitCode, stdout: Buffer.concat(out).toString('utf8'), stderr: Buffer.concat(err).toString('utf8') };
|
|
56
79
|
}
|
|
57
80
|
export function createSsh(runner, maxOutputBytes) {
|
|
@@ -66,12 +89,12 @@ export function createSsh(runner, maxOutputBytes) {
|
|
|
66
89
|
status: async (t, p, signal) => {
|
|
67
90
|
try {
|
|
68
91
|
if (!['codex', 'claude'].includes(p) || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1)
|
|
69
|
-
return
|
|
92
|
+
return authFailure('ssh-status', 'STATUS_ARGUMENT_INVALID');
|
|
70
93
|
const source = await readFile(new URL('../helper/auth-status.cjs', import.meta.url), 'utf8');
|
|
71
94
|
return ok(await runRemote(runner, t, 'node -e ' + "'" + source.replaceAll("'", "'\\''") + "' -- " + p + ' ' + maxOutputBytes, signal));
|
|
72
95
|
}
|
|
73
|
-
catch {
|
|
74
|
-
return
|
|
96
|
+
catch (error) {
|
|
97
|
+
return authFailure('ssh-status', 'STATUS_EXECUTION_FAILED', {}, error);
|
|
75
98
|
}
|
|
76
99
|
},
|
|
77
100
|
};
|
package/lib/startup.d.ts
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
|
-
import type { ConnectApi, ConnectionState, LoginPromptState, LoginPromptResult, Provider, NeedCode, Result } from './api.ts';
|
|
1
|
+
import type { ConnectApi, ConnectionState, LoginPromptState, LoginPromptResult, LoginState, Provider, NeedCode, Result } from './api.ts';
|
|
2
2
|
export type StartupState = LoginPromptState;
|
|
3
3
|
export declare function employeeMessage(code?: NeedCode): string;
|
|
4
4
|
type Operations = Pick<ConnectApi, 'connect' | 'checkLogin' | 'login' | 'getState'>;
|
|
5
5
|
export declare function createStartup(api: Operations, providers: Provider[], emit?: (s: LoginPromptState) => void, result?: (r: LoginPromptResult) => void, instanceId?: string): {
|
|
6
6
|
getState: () => LoginPromptState;
|
|
7
7
|
check: () => Promise<void>;
|
|
8
|
+
beginCheck: (provider: Provider) => {
|
|
9
|
+
provider: Provider;
|
|
10
|
+
generation: number;
|
|
11
|
+
serial: number;
|
|
12
|
+
};
|
|
13
|
+
finishCheck: (ticket: ReturnType<(provider: Provider) => {
|
|
14
|
+
provider: Provider;
|
|
15
|
+
generation: number;
|
|
16
|
+
serial: number;
|
|
17
|
+
}>, status: Result<LoginState>) => boolean;
|
|
18
|
+
currentTicket: (ticket: ReturnType<(provider: Provider) => {
|
|
19
|
+
provider: Provider;
|
|
20
|
+
generation: number;
|
|
21
|
+
serial: number;
|
|
22
|
+
}>) => boolean;
|
|
23
|
+
captureCheck: (provider: Provider) => {
|
|
24
|
+
provider: Provider;
|
|
25
|
+
generation: number;
|
|
26
|
+
serial: number;
|
|
27
|
+
};
|
|
28
|
+
inheritCheck(source: Result<LoginState>, derived: Result<LoginState>): void;
|
|
8
29
|
subscribe(listener: (s: LoginPromptState) => void): () => void;
|
|
9
30
|
request(provider: Provider, intent: "automatic" | "user"): Promise<Result<LoginPromptState>>;
|
|
10
31
|
respond: (requestId: string, action: "authorize" | "defer") => Promise<Result<LoginPromptState>>;
|