@walkhi/code-relax 0.1.0-beta.81 → 0.1.0-beta.83

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.
@@ -1 +1 @@
1
- {"layout":"npm-platform-packages","version":"0.1.0-beta.81"}
1
+ {"layout":"npm-platform-packages","version":"0.1.0-beta.83"}
@@ -115,6 +115,38 @@
115
115
  } catch { return null; }
116
116
  }
117
117
  function elicitationResult(action, content = null) { return { action, content: action === 'accept' ? content : null, _meta: null }; }
118
+ function browserAuth(request) {
119
+ const p = request.params || {}, meta = p._meta || {};
120
+ if (request.method !== elicitation || p.mode !== 'form' || meta.codex_approval_kind !== 'browser_auth'
121
+ || p.requestedSchema?.type !== 'object' || !p.requestedSchema.properties || Object.keys(p.requestedSchema.properties).length
122
+ || !/^[A-Za-z0-9_-]{32,128}$/.test(meta.browser_auth_challenge_id || '')
123
+ || !Array.isArray(meta.fields) || !meta.fields.length || meta.fields.length > 6
124
+ || meta.options != null && (!Array.isArray(meta.options) || meta.options.length)
125
+ || meta.qrCode || meta.qrCodePayload || meta.qrCodeUrl || meta.webauthn != null) return null;
126
+ try {
127
+ const origin = new URL(meta.origin);
128
+ if (!['https:', 'http:'].includes(origin.protocol) || origin.origin !== meta.origin) return null;
129
+ } catch { return null; }
130
+ const ids = new Set();
131
+ for (const field of meta.fields) {
132
+ if (!field || typeof field.id !== 'string' || !/^[A-Za-z0-9_-]{1,48}$/.test(field.id) || ['__proto__', 'constructor', 'prototype'].includes(field.id)
133
+ || ids.has(field.id) || typeof field.label !== 'string' || typeof field.required !== 'boolean'
134
+ || !['email', 'number', 'password', 'search', 'tel', 'text', 'url'].includes(field.type)) return null;
135
+ ids.add(field.id);
136
+ }
137
+ return { origin: meta.origin, challengeId: meta.browser_auth_challenge_id, fields: meta.fields,
138
+ accountUsername: typeof meta.account_username === 'string' ? meta.account_username : null };
139
+ }
140
+ function validateBrowserAuthFields(auth, fields) {
141
+ if (!auth || !fields || typeof fields !== 'object' || Array.isArray(fields)
142
+ || !Object.keys(fields).length || Object.keys(fields).some(id => !auth.fields.some(field => field.id === id))) throw new Error('安全登录字段无效');
143
+ for (const field of auth.fields) {
144
+ const entry = fields[field.id];
145
+ if (entry === undefined && !field.required) continue;
146
+ if (!entry || Object.keys(entry).length !== 1 || typeof entry.value !== 'string'
147
+ || !entry.value.length) throw new Error('请填写有效的安全登录信息');
148
+ }
149
+ }
118
150
  function fieldOptions(field) {
119
151
  const schema = field.type === 'array' ? field.items : field;
120
152
  if (Array.isArray(schema?.enum)) return schema.enum.map((value, index) => ({ value, label: schema.enumNames?.[index] || value }));
@@ -191,6 +223,10 @@
191
223
  }
192
224
  function validate(request, result) {
193
225
  if (request.method === elicitation && equal(result, elicitationResult('cancel'))) return;
226
+ if (browserAuth(request)) {
227
+ if (!['decline', 'cancel'].some(action => equal(result, { action }))) throw new Error('安全登录必须通过专用输入入口提交');
228
+ return;
229
+ }
194
230
  if (externalAction(request)) {
195
231
  if (!['accept', 'decline'].some(action => equal(result, elicitationResult(action)))) throw new Error('外部操作结果无效');
196
232
  return;
@@ -307,5 +343,5 @@
307
343
  return { submitted: true };
308
344
  }
309
345
  }
310
- return { Store, decisions, validate, command, file, input, elicitation, permissions, externalAction, elicitationResult, formSchema, fieldOptions, validateForm, permissionResult, blocksComposer, browserAccess, browserAction, browserActionResult, computerAppAccess, computerAppResult, mcpToolApproval, mcpToolResult, confirmation, confirmationResult };
346
+ return { Store, decisions, validate, command, file, input, elicitation, permissions, externalAction, elicitationResult, formSchema, fieldOptions, validateForm, permissionResult, blocksComposer, browserAccess, browserAction, browserActionResult, browserAuth, validateBrowserAuthFields, computerAppAccess, computerAppResult, mcpToolApproval, mcpToolResult, confirmation, confirmationResult };
311
347
  });
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+
5
+ // Only submission markers are durable. Credentials never enter this store.
6
+ export class BrowserAuthOperations {
7
+ constructor(stateRoot) { this.directory = path.join(stateRoot, 'browser-auth-operations'); }
8
+ file(key) { return path.join(this.directory, createHash('sha256').update(JSON.stringify(key)).digest('hex') + '.json'); }
9
+ async read(key) {
10
+ try { return JSON.parse(await fs.readFile(this.file(key), 'utf8')).status; }
11
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
12
+ }
13
+ async claim(key) {
14
+ await fs.mkdir(this.directory, { recursive: true });
15
+ let handle;
16
+ try { handle = await fs.open(this.file(key), 'wx'); }
17
+ catch (error) { if (error.code === 'EEXIST') throw new Error('此安全登录已提交或结果待核对,请勿重复提交'); throw error; }
18
+ try { await handle.writeFile(JSON.stringify({ status: 'unknown' })); await handle.sync(); }
19
+ finally { await handle.close(); }
20
+ }
21
+ async complete(key, status) {
22
+ const file = this.file(key), temporary = file + '.' + randomUUID() + '.tmp';
23
+ await fs.writeFile(temporary, JSON.stringify({ status }));
24
+ await fs.rename(temporary, file);
25
+ }
26
+ }
@@ -1,71 +1,73 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { setTimeout as delay } from 'node:timers/promises';
4
- import { readRecord, lifecycle } from './service-lifecycle.mjs';
5
- import * as serviceHost from './platform/service-host.mjs';
6
- import { managedPortAvailable as portAvailable } from './managed-app-server.mjs';
7
- import { readManagedEndpoint } from './managed-endpoint.mjs';
8
- export { portAvailable };
9
- function desktopEnvironment(stateRoot, environment) {
10
- const childEnv = { ...environment };
11
- for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnv[key];
12
- const runtime = readRecord(path.join(stateRoot, 'codex-runtime.json'));
13
- if (runtime?.codexHome) childEnv.CODEX_HOME = runtime.codexHome;
14
- childEnv.CODEX_APP_SERVER_WS_URL = readManagedEndpoint(stateRoot);
15
- return childEnv;
16
- }
17
-
18
- // Opening the optional UI does not start, restart or supervise the kernel.
19
- export async function startDesktop({ stateRoot, host, environment = process.env, restart = false }) {
20
- let stage = 'inspect';
21
- try {
22
- const info = await host.inspect();
23
- if (!info.desktop) throw new Error('未安装 Codex 桌面端。');
24
- if (info.running && !restart) return { status: 'running', message: 'Codex 桌面端已启动。' };
25
- if (info.running) { stage = 'close'; await host.closeDesktop(info.desktop); }
26
- stage = 'activate';
27
- await host.startDesktop(info.desktop, desktopEnvironment(stateRoot, environment));
28
- return { status: 'accepted', message: restart ? '已提交 Codex 桌面端重启请求。' : '已提交 Codex 桌面端启动请求。' };
29
- } catch (error) {
30
- console.error(JSON.stringify({ time: new Date().toISOString(), event: 'desktop-operation-failed', restart, stage,
31
- message: error.message, code: error.code, stderr: error.stderr }));
32
- throw new Error(stage === 'close' ? '桌面端关闭失败,请查看工作站诊断日志。'
33
- : stage === 'activate' ? '桌面端启动失败,请查看工作站诊断日志。' : '无法检查桌面端,请查看工作站诊断日志。', { cause: error });
34
- }
35
- }
36
-
37
- export async function launchDesktop({ stateRoot, host, environment = process.env, wait = delay, timeout = 30000, recoveryId, progress = () => {}, service = lifecycle({ stateRoot, host: serviceHost }) }) {
38
- fs.mkdirSync(stateRoot, { recursive: true });
39
- const recovery = readRecord(path.join(stateRoot, 'shared-recovery.lock'));
40
- if (recovery && recovery.operationId !== recoveryId) throw new Error('Codex 内核正在启动,请用 relax doctor 查看恢复状态。');
41
- const lock = path.join(stateRoot, 'desktop-launch.lock');
42
- let descriptor;
43
- try { descriptor = fs.openSync(lock, 'wx'); }
44
- catch (error) {
45
- if (error.code === 'EEXIST') throw new Error('另一个 Desktop 启动操作正在进行;若上次异常退出,请确认没有启动器运行后移除 desktop-launch.lock。');
46
- throw error;
47
- }
48
- try {
49
- const info = await host.inspect();
50
- if (!info.desktop) throw new Error('未安装 Codex Desktop,请使用 relax start 启动工作站。');
51
- const connected = await host.sharedServiceUrl();
52
- if (info.running) {
53
- if (connected) return { reused: true };
54
- throw new Error('请先保存任务并完全退出 Codex Desktop,再接入 Codex 内核;不会自动结束正在运行的任务。');
55
- }
56
- progress('starting-app-server');
57
- const health = await service.status();
58
- if (!health.running || !health.managedAppServer?.ready) throw new Error(health.managedAppServer?.error || '工作站 Codex 内核未就绪。');
59
- const childEnv = desktopEnvironment(stateRoot, environment);
60
- progress('starting-desktop');
61
- await host.startDesktop(info.desktop, childEnv);
62
- const deadline = Date.now() + timeout;
63
- while (true) {
64
- const ready = await host.desktopReady();
65
- if (ready) break;
66
- if (Date.now() >= deadline) throw new Error('Desktop 尚未接入 Codex 内核。请查看 Desktop 窗口后重试。');
67
- await wait(500);
68
- }
69
- return { reused: false };
70
- } finally { fs.closeSync(descriptor); fs.rmSync(lock, { force: true }); }
71
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import { readRecord, lifecycle } from './service-lifecycle.mjs';
5
+ import * as serviceHost from './platform/service-host.mjs';
6
+ import { managedPortAvailable as portAvailable } from './managed-app-server.mjs';
7
+ import { readManagedEndpoint } from './managed-endpoint.mjs';
8
+ export { portAvailable };
9
+ function desktopEnvironment(stateRoot, environment) {
10
+ const childEnv = { ...environment };
11
+ for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnv[key];
12
+ const runtime = readRecord(path.join(stateRoot, 'codex-runtime.json'));
13
+ if (runtime?.codexHome) childEnv.CODEX_HOME = runtime.codexHome;
14
+ childEnv.CODEX_APP_SERVER_WS_URL = readManagedEndpoint(stateRoot);
15
+ return childEnv;
16
+ }
17
+
18
+ // Opening the optional UI does not start, restart or supervise the kernel.
19
+ export async function startDesktop({ stateRoot, host, environment = process.env, restart = false, progress = () => {} }) {
20
+ let stage = 'inspect';
21
+ progress(stage);
22
+ try {
23
+ const info = await host.inspect();
24
+ if (!info.desktop) throw new Error('未安装 Codex 桌面端。');
25
+ if (info.running && !restart) return { status: 'running', message: 'Codex 桌面端已启动。' };
26
+ if (info.running) { stage = 'close'; progress(stage); await host.closeDesktop(info.desktop); }
27
+ stage = 'activate'; progress(stage);
28
+ await host.startDesktop(info.desktop, desktopEnvironment(stateRoot, environment));
29
+ return { status: 'accepted', message: restart ? '已提交 Codex 桌面端重启请求。' : '已提交 Codex 桌面端启动请求。' };
30
+ } catch (error) {
31
+ console.error(JSON.stringify({ time: new Date().toISOString(), event: 'desktop-operation-failed', restart, stage,
32
+ message: error.message, code: error.code, stderr: error.stderr }));
33
+ throw new Error(stage === 'close' && ['MAC_DESKTOP_CLOSE_TIMEOUT', 'MAC_DESKTOP_CLOSE_CHANGED'].includes(error.code) ? error.message
34
+ : stage === 'close' ? '桌面端关闭失败,请查看工作站诊断日志。'
35
+ : stage === 'activate' ? '桌面端启动失败,请查看工作站诊断日志。' : '无法检查桌面端,请查看工作站诊断日志。', { cause: error });
36
+ }
37
+ }
38
+
39
+ export async function launchDesktop({ stateRoot, host, environment = process.env, wait = delay, timeout = 30000, recoveryId, progress = () => {}, service = lifecycle({ stateRoot, host: serviceHost }) }) {
40
+ fs.mkdirSync(stateRoot, { recursive: true });
41
+ const recovery = readRecord(path.join(stateRoot, 'shared-recovery.lock'));
42
+ if (recovery && recovery.operationId !== recoveryId) throw new Error('Codex 内核正在启动,请用 relax doctor 查看恢复状态。');
43
+ const lock = path.join(stateRoot, 'desktop-launch.lock');
44
+ let descriptor;
45
+ try { descriptor = fs.openSync(lock, 'wx'); }
46
+ catch (error) {
47
+ if (error.code === 'EEXIST') throw new Error('另一个 Desktop 启动操作正在进行;若上次异常退出,请确认没有启动器运行后移除 desktop-launch.lock。');
48
+ throw error;
49
+ }
50
+ try {
51
+ const info = await host.inspect();
52
+ if (!info.desktop) throw new Error('未安装 Codex Desktop,请使用 relax start 启动工作站。');
53
+ const connected = await host.sharedServiceUrl();
54
+ if (info.running) {
55
+ if (connected) return { reused: true };
56
+ throw new Error('请先保存任务并完全退出 Codex Desktop,再接入 Codex 内核;不会自动结束正在运行的任务。');
57
+ }
58
+ progress('starting-app-server');
59
+ const health = await service.status();
60
+ if (!health.running || !health.managedAppServer?.ready) throw new Error(health.managedAppServer?.error || '工作站 Codex 内核未就绪。');
61
+ const childEnv = desktopEnvironment(stateRoot, environment);
62
+ progress('starting-desktop');
63
+ await host.startDesktop(info.desktop, childEnv);
64
+ const deadline = Date.now() + timeout;
65
+ while (true) {
66
+ const ready = await host.desktopReady();
67
+ if (ready) break;
68
+ if (Date.now() >= deadline) throw new Error('Desktop 尚未接入 Codex 内核。请查看 Desktop 窗口后重试。');
69
+ await wait(500);
70
+ }
71
+ return { reused: false };
72
+ } finally { fs.closeSync(descriptor); fs.rmSync(lock, { force: true }); }
73
+ }
@@ -1,41 +1,46 @@
1
- import { setTimeout as delay } from 'node:timers/promises';
2
- import { startDesktop } from './desktop-launcher.mjs';
3
-
4
- // The request is acknowledged before native close/start can outlive the phone's
5
- // request deadline. Health carries the same receipt; a lost reply is not a retry.
6
- export class DesktopOperation {
7
- constructor({ stateRoot, host, sample, launch = startDesktop, wait = delay, now = Date.now, timeout = 30000 }) {
8
- Object.assign(this, { stateRoot, host, sample, launch, wait, now, timeout });
9
- this.record = null;
10
- }
11
- snapshot() { return this.record ? { ...this.record } : null; }
12
- request({ operationId, restart }) {
13
- if (this.record?.operationId === operationId) return this.snapshot();
14
- if (this.record?.status === 'running') {
15
- throw Object.assign(new Error('桌面端操作仍在进行,请刷新状态核对结果。'), { statusCode: 409 });
16
- }
17
- this.record = { operationId, restart, status: 'running', startedAt: new Date(this.now()).toISOString() };
18
- this.completion = this.run(this.record);
19
- return this.snapshot();
20
- }
21
- async run(record) {
22
- let failure;
23
- try {
24
- await this.launch({ stateRoot: this.stateRoot, host: this.host, restart: record.restart });
25
- const deadline = this.now() + this.timeout;
26
- while (!await this.host.desktopReady()) {
27
- if (this.now() >= deadline) throw new Error('桌面端启动请求已执行,但未确认接入共享内核,请查看工作站诊断日志。');
28
- await this.wait(500);
29
- }
30
- } catch (error) { failure = error; }
31
- // Publish the final receipt with a fresh desktop sample, including failures.
32
- try { await this.sample(); }
33
- catch (error) { failure ||= error; }
34
- Object.assign(record, { status: failure ? 'failed' : 'completed', finishedAt: new Date(this.now()).toISOString() });
35
- if (failure) {
36
- record.error = failure.message;
37
- console.warn(JSON.stringify({ event: 'desktop-operation-result', operationId: record.operationId,
38
- status: record.status, message: failure.message }));
39
- }
40
- }
41
- }
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { startDesktop } from './desktop-launcher.mjs';
3
+
4
+ // The request is acknowledged before native close/start can outlive the phone's
5
+ // request deadline. Health carries the same receipt; a lost reply is not a retry.
6
+ export class DesktopOperation {
7
+ constructor({ stateRoot, host, sample, launch = startDesktop, wait = delay, now = Date.now, timeout = 30000 }) {
8
+ Object.assign(this, { stateRoot, host, sample, launch, wait, now, timeout });
9
+ this.record = null;
10
+ }
11
+ snapshot() { return this.record ? { ...this.record } : null; }
12
+ request({ operationId, restart }) {
13
+ if (this.record?.operationId === operationId) return this.snapshot();
14
+ if (this.record?.status === 'running') {
15
+ throw Object.assign(new Error('桌面端操作仍在进行,请刷新状态核对结果。'), { statusCode: 409 });
16
+ }
17
+ this.record = { operationId, restart, status: 'running', startedAt: new Date(this.now()).toISOString() };
18
+ this.completion = this.run(this.record);
19
+ return this.snapshot();
20
+ }
21
+ async run(record) {
22
+ let failure;
23
+ const progress = stage => {
24
+ Object.assign(record, { stage, updatedAt: new Date(this.now()).toISOString() });
25
+ console.info(JSON.stringify({ event: 'desktop-operation-stage', operationId: record.operationId,
26
+ restart: record.restart, stage, time: record.updatedAt }));
27
+ };
28
+ try {
29
+ await this.launch({ stateRoot: this.stateRoot, host: this.host, restart: record.restart, progress });
30
+ progress('connecting');
31
+ const deadline = this.now() + this.timeout;
32
+ while (!await this.host.desktopReady()) {
33
+ if (this.now() >= deadline) throw new Error('桌面端启动请求已执行,但未确认接入共享内核,请查看工作站诊断日志。');
34
+ await this.wait(500);
35
+ }
36
+ } catch (error) { failure = error; }
37
+ // Publish the final receipt with a fresh desktop sample, including failures.
38
+ if (!failure) progress('verifying');
39
+ try { await this.sample(); }
40
+ catch (error) { failure ||= error; }
41
+ Object.assign(record, { status: failure ? 'failed' : 'completed', finishedAt: new Date(this.now()).toISOString() });
42
+ if (failure) record.error = failure.message;
43
+ console.info(JSON.stringify({ event: 'desktop-operation-result', operationId: record.operationId,
44
+ time: record.finishedAt, stage: record.stage, status: record.status, message: record.error }));
45
+ }
46
+ }