From fa4d5d6d972216278536c85368b5dd5de6042095 Mon Sep 17 00:00:00 2001 From: oliver Date: Sun, 11 Oct 2026 17:03:40 +0800 Subject: [PATCH] fix(uds-auth): isolate trusted Host calls from browser identity --- uds-auth/README.md | 2 + uds-auth/README.zh.md | 2 + uds-auth/lib/context.js | 11 ++++++ uds-auth/lib/index.js | 2 + uds-auth/package-lock.json | 4 +- uds-auth/package.json | 2 +- uds-auth/test/host-context.test.js | 63 ++++++++++++++++++++++++++++++ 7 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 uds-auth/test/host-context.test.js diff --git a/uds-auth/README.md b/uds-auth/README.md index 807b52d2..f2e37561 100644 --- a/uds-auth/README.md +++ b/uds-auth/README.md @@ -10,6 +10,8 @@ dsh plugin --profile web add -w "D:/project/chatgpt/oclaw/uds-auth" Restart Harness after install. The badge mounts on `sidebar.footer.action` (root scope). +Version `0.2.21` provides `ctx.get('udsAuth').runAsHost(callback)` for trusted Host plugins. It isolates UDS browser identity during synchronous and asynchronous background operations and restores the caller afterward. Use with `dsh-im-ops >= 4.9.1-ops.38` to fix WhatsApp background Session creation rejected as signed out; update both plugins and restart the Host. The callback adds no remote endpoint and preserves browser login and workspace ACL checks. + ## Config ```yaml diff --git a/uds-auth/README.zh.md b/uds-auth/README.zh.md index 6ecc668c..5703ac07 100644 --- a/uds-auth/README.zh.md +++ b/uds-auth/README.zh.md @@ -10,6 +10,8 @@ dsh plugin --profile web add -w "D:/project/chatgpt/oclaw/uds-auth" 安装后重启 Harness。登录徽章在 `shell.overlay`(右上角);用户管理/部署配置在 **设置 → UDS 认证**(`settings.section`)。 +`0.2.21` 提供仅供 Host 插件使用的 `ctx.get('udsAuth').runAsHost(callback)`:在回调及其异步操作内使用 Host 后台上下文,并恢复调用者的网页身份。配合 `dsh-im-ops >= 4.9.1-ops.38` 修复 WhatsApp 后台建会话误报“登录后才能创建会话”;更新两者后重启 Host。该方法不增加远程端点,网页登录及工作区 ACL 保持生效。 + ## 配置 在 **设置 → UDS 认证** 或 `cordis.patch.yml` 中修改: diff --git a/uds-auth/lib/context.js b/uds-auth/lib/context.js index 91e785c6..02d3fcce 100644 --- a/uds-auth/lib/context.js +++ b/uds-auth/lib/context.js @@ -33,6 +33,17 @@ export function runWithUserContext(userContext, fn) { return userContextStorage.run(userContext, fn) } +/** + * Run a trusted in-process Host operation without a browser identity. + * Only Host plugins receive this callback API; it is not a remote endpoint. + * @param {Function} fn + * @returns {*} The operation's result, preserving synchronous and async calls. + */ +export function runAsHost(fn) { + if (typeof fn !== 'function') throw new TypeError('Host operation must be a function') + return userContextStorage.run(undefined, fn) +} + /** * Persist ALS for the rest of this async execution chain. * Needed for Gateway Remote stream mux: message handlers are sync but start diff --git a/uds-auth/lib/index.js b/uds-auth/lib/index.js index ded5c869..813ae6fd 100644 --- a/uds-auth/lib/index.js +++ b/uds-auth/lib/index.js @@ -21,6 +21,7 @@ import { LOCAL_ADMIN_BOX_ENV, } from './local-admin.js' import { SessionBridgeStore } from './session-bridge.js' +import { runAsHost } from './context.js' const __dirname = dirname(fileURLToPath(import.meta.url)) const require = createRequire(import.meta.url) @@ -1267,6 +1268,7 @@ async function initServices(ctx, config) { } const udsAuth = { + runAsHost, async resolveRequestIdentity(req) { const identity = await resolveIdentity(req) if (!identity?.empNo) return null diff --git a/uds-auth/package-lock.json b/uds-auth/package-lock.json index 9fbcec30..26da796e 100644 --- a/uds-auth/package-lock.json +++ b/uds-auth/package-lock.json @@ -1,12 +1,12 @@ { "name": "uds-auth", - "version": "0.2.20", + "version": "0.2.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "uds-auth", - "version": "0.2.20", + "version": "0.2.21", "license": "MIT", "devDependencies": { "qrcode": "1.5.4" diff --git a/uds-auth/package.json b/uds-auth/package.json index b593b044..a0b5009c 100644 --- a/uds-auth/package.json +++ b/uds-auth/package.json @@ -1,6 +1,6 @@ { "name": "uds-auth", - "version": "0.2.20", + "version": "0.2.21", "description": "UDS authentication plugin for DeepSeek Harness with multi-tenant session isolation", "type": "module", "main": "lib/index.js", diff --git a/uds-auth/test/host-context.test.js b/uds-auth/test/host-context.test.js new file mode 100644 index 00000000..4c4d5d57 --- /dev/null +++ b/uds-auth/test/host-context.test.js @@ -0,0 +1,63 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { getUserContext, runAsHost, withUserContext } from '../lib/context.js' +import { installDshAcl } from '../lib/dsh-acl.js' + +test('Host callback context is isolated across concurrent browser identities and exceptions', async () => { + const identities = [null, { empNo: 'u1' }, { empNo: 'u2' }] + await Promise.all(identities.map(identity => withUserContext(identity, async () => { + assert.equal(await runAsHost(async () => { + await Promise.resolve() + assert.equal(getUserContext(), undefined) + return 'host-result' + }), 'host-result') + assert.equal(getUserContext(), identity) + assert.throws(() => runAsHost(() => { throw new Error('failed') }), /failed/) + assert.equal(getUserContext(), identity) + }))) + assert.equal(runAsHost(() => 42), 42) + assert.throws(() => runAsHost(null), TypeError) +}) + +test('trusted Host creation works from browser ALS while unauthenticated browser creation stays denied', async () => { + let creations = 0 + const owners = [] + const controller = { + async list() { return { items: [] } }, + async search() { return { items: [] } }, + async create(request) { + creations += 1 + return { sessionId: `session-${creations}`, request } + }, + } + installDshAcl({ + on() { return () => {} }, + get() { return undefined }, + inject(names, callback) { + if (names[0] === 'sessionController') callback({ sessionController: controller }) + }, + }, { + sessionAcl: { getOwner() { return null }, setOwner: (...args) => owners.push(args) }, + userWorkspaces: { isUserPath: (empNo, path) => path === `/ws/${empNo}`, get() { return null } }, + getWorkspaceRoot: () => '/ws', + getWorkspaceRegistry: () => undefined, + }) + await withUserContext(null, async () => { + await assert.rejects(controller.create({ cwd: '/bot' }), error => ( + error.details.udsError === 'login_required_create_session' + )) + const result = await runAsHost(() => controller.create({ cwd: '/bot', agentPreset: 'netxops' })) + assert.deepEqual(result.request, { cwd: '/bot', agentPreset: 'netxops' }) + assert.equal(getUserContext(), null) + await assert.rejects(controller.create({ cwd: '/bot' })) + }) + assert.equal(creations, 1) + assert.deepEqual(owners, [], 'background sessions must not inherit a browser owner') + await withUserContext({ empNo: 'u1', permissions: {} }, async () => { + await assert.rejects(controller.create({ cwd: '/other' }), error => ( + error.details.udsError === 'session_workspace_only' + )) + await controller.create({ cwd: '/ws/u1' }) + }) + assert.deepEqual(owners, [['session-2', 'u1']]) +})