fix(uds-auth): isolate trusted Host calls from browser identity
Some checks are pending
ci / test (push) Waiting to run
ci / test-postgresql (push) Waiting to run

This commit is contained in:
oliver 2026-10-11 17:03:40 +08:00
parent 3ba4461566
commit fa4d5d6d97
7 changed files with 83 additions and 3 deletions

View file

@ -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

View file

@ -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` 中修改:

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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",

View file

@ -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']])
})