mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-17 07:23:30 +08:00
fix(uds-auth): isolate trusted Host calls from browser identity
This commit is contained in:
parent
3ba4461566
commit
fa4d5d6d97
7 changed files with 83 additions and 3 deletions
|
|
@ -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).
|
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
|
## Config
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ dsh plugin --profile web add -w "D:/project/chatgpt/oclaw/uds-auth"
|
||||||
|
|
||||||
安装后重启 Harness。登录徽章在 `shell.overlay`(右上角);用户管理/部署配置在 **设置 → UDS 认证**(`settings.section`)。
|
安装后重启 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` 中修改:
|
在 **设置 → UDS 认证** 或 `cordis.patch.yml` 中修改:
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,17 @@ export function runWithUserContext(userContext, fn) {
|
||||||
return userContextStorage.run(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.
|
* Persist ALS for the rest of this async execution chain.
|
||||||
* Needed for Gateway Remote stream mux: message handlers are sync but start
|
* Needed for Gateway Remote stream mux: message handlers are sync but start
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
LOCAL_ADMIN_BOX_ENV,
|
LOCAL_ADMIN_BOX_ENV,
|
||||||
} from './local-admin.js'
|
} from './local-admin.js'
|
||||||
import { SessionBridgeStore } from './session-bridge.js'
|
import { SessionBridgeStore } from './session-bridge.js'
|
||||||
|
import { runAsHost } from './context.js'
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
const require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
|
|
@ -1267,6 +1268,7 @@ async function initServices(ctx, config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const udsAuth = {
|
const udsAuth = {
|
||||||
|
runAsHost,
|
||||||
async resolveRequestIdentity(req) {
|
async resolveRequestIdentity(req) {
|
||||||
const identity = await resolveIdentity(req)
|
const identity = await resolveIdentity(req)
|
||||||
if (!identity?.empNo) return null
|
if (!identity?.empNo) return null
|
||||||
|
|
|
||||||
4
uds-auth/package-lock.json
generated
4
uds-auth/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "uds-auth",
|
"name": "uds-auth",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "uds-auth",
|
"name": "uds-auth",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"qrcode": "1.5.4"
|
"qrcode": "1.5.4"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "uds-auth",
|
"name": "uds-auth",
|
||||||
"version": "0.2.20",
|
"version": "0.2.21",
|
||||||
"description": "UDS authentication plugin for DeepSeek Harness with multi-tenant session isolation",
|
"description": "UDS authentication plugin for DeepSeek Harness with multi-tenant session isolation",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
|
|
|
||||||
63
uds-auth/test/host-context.test.js
Normal file
63
uds-auth/test/host-context.test.js
Normal 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']])
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue