mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-13 05:50:45 +08:00
Expose raw token access setting for skills
This commit is contained in:
parent
b744483fcd
commit
70bde46eff
6 changed files with 54 additions and 11 deletions
|
|
@ -16,7 +16,7 @@ Desktop 只把 bridge 附到 `dsh-app://app` 或 Host 启动时提供的 `__DSH_
|
|||
|
||||
## Skill、cron 和后台 shell
|
||||
|
||||
Host 加载 `shellEnv` 时自动注册 uds-auth contributor。每次 shell 执行根据真实 `execution.agent.session.header.id` 和服务端 owner 签发仅有 `outbound` scope 的 capability,默认 10 分钟,注入受保护的 `DSH_UDS_TASK_CAPABILITY` / `DSH_UDS_AUTH_BASE`。Python helper 优先读取这两个变量;兼容的手工 `UDS_TASK_CAPABILITY` / `UDS_AUTH_BASE` 仅在没有 Host 管理变量时使用。所有 base,包括旧 `DSH_WEB_URL`,都必须是 loopback HTTP(S)。不再猜测 8787 端口,也不允许 remote override 携带凭据。
|
||||
Host 加载 `shellEnv` 时自动注册 uds-auth contributor。每次 shell 执行根据真实 `execution.agent.session.header.id` 和服务端 owner 签发 capability,默认只有 `outbound` scope;仅当管理员显式开启 `allowRawAgentToken` 时增加 `credentials` scope,以供受信 skill 调用 `/agent-credentials`。默认 10 分钟,注入受保护的 `DSH_UDS_TASK_CAPABILITY` / `DSH_UDS_AUTH_BASE`。Python helper 优先读取这两个变量;兼容的手工 `UDS_TASK_CAPABILITY` / `UDS_AUTH_BASE` 仅在没有 Host 管理变量时使用。所有 base,包括旧 `DSH_WEB_URL`,都必须是 loopback HTTP(S)。不再猜测 8787 端口,也不允许 remote override 携带凭据。
|
||||
|
||||
普通执行结束或取消即撤销。官方 shell 交给 jobs 的 background/promoted 执行绑定该 job,停止、结算、移除或 TTL 到期撤销;不跨任务复用。账号停用、owner 改变和 scope 不符均拒绝代理调用。长于 10 分钟的后台进程不能自动续期;应拆为新的受控调用。前台/后台都不会向 shell 注入 UAC 原始 token。需要 raw credentials 的受信 Host 调用必须显式申请 scope,且仍受 `allowRawAgentToken` 开关控制。
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ window.__ModuleLoader__.load({
|
|||
"ui.deployConfig": "部署配置",
|
||||
"ui.userSearchUrl": "用户搜索 URL(token 校验)",
|
||||
"ui.workspaceRoot": "工作区根目录(空=$DSH_HOME/user-workspaces)",
|
||||
"ui.allowRawAgentToken": "允许 Skill 获取原始 UDS token",
|
||||
"ui.allowRawAgentTokenHint": "开启后,具备当前任务 credentials scope 的本机 Skill 可获取工号和 token。只在业务确实需要原始 token 时开启。",
|
||||
"ui.saveConfig": "保存配置",
|
||||
"ui.saving": "保存中...",
|
||||
"ui.configSaved": "配置已保存",
|
||||
|
|
@ -182,6 +184,8 @@ window.__ModuleLoader__.load({
|
|||
"ui.deployConfig": "Deploy config",
|
||||
"ui.userSearchUrl": "User search URL (token verify)",
|
||||
"ui.workspaceRoot": "Workspace root (empty=$DSH_HOME/user-workspaces)",
|
||||
"ui.allowRawAgentToken": "Allow Skills to read the raw UDS token",
|
||||
"ui.allowRawAgentTokenHint": "Enables local Skills with the current task credentials scope to read the employee number and token. Turn this on only when a workflow needs the raw token.",
|
||||
"ui.saveConfig": "Save config",
|
||||
"ui.saving": "Saving...",
|
||||
"ui.configSaved": "Config saved",
|
||||
|
|
@ -1290,6 +1294,7 @@ function reloadAfterLogin() {
|
|||
loginSystemCode: '',
|
||||
originSystemCode: '',
|
||||
workspaceRoot: '',
|
||||
allowRawAgentToken: false,
|
||||
})
|
||||
const [fallbackEnabled, setFallbackEnabled] = useState(false)
|
||||
const [fallbackPwd, setFallbackPwd] = useState('')
|
||||
|
|
@ -1412,6 +1417,16 @@ function reloadAfterLogin() {
|
|||
field('loginSystemCode', 'loginSystemCode'),
|
||||
field('originSystemCode', 'originSystemCode'),
|
||||
field('workspaceRoot', t('ui.workspaceRoot')),
|
||||
h('label', { className: 'uds-auth-settings-toggle' },
|
||||
h('input', {
|
||||
type: 'checkbox',
|
||||
checked: form.allowRawAgentToken === true,
|
||||
disabled: !canSettings || busy,
|
||||
onChange: (e) => setForm((prev) => ({ ...prev, allowRawAgentToken: e.target.checked })),
|
||||
}),
|
||||
h('span', null, t('ui.allowRawAgentToken')),
|
||||
),
|
||||
h('p', { className: 'uds-auth-settings-intro' }, t('ui.allowRawAgentTokenHint')),
|
||||
h('div', { className: 'uds-auth-settings-actions' },
|
||||
h('button', {
|
||||
type: 'button',
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export const MESSAGES = {
|
|||
'ui.deployConfig': '部署配置',
|
||||
'ui.userSearchUrl': '用户搜索 URL(token 校验)',
|
||||
'ui.workspaceRoot': '工作区根目录(空=$DSH_HOME/user-workspaces)',
|
||||
'ui.allowRawAgentToken': '允许 Skill 获取原始 UDS token',
|
||||
'ui.allowRawAgentTokenHint': '开启后,具备当前任务 credentials scope 的本机 Skill 可获取工号和 token。只在业务确实需要原始 token 时开启。',
|
||||
'ui.saveConfig': '保存配置',
|
||||
'ui.saving': '保存中...',
|
||||
'ui.configSaved': '配置已保存',
|
||||
|
|
@ -215,6 +217,8 @@ export const MESSAGES = {
|
|||
'ui.deployConfig': 'Deploy config',
|
||||
'ui.userSearchUrl': 'User search URL (token verify)',
|
||||
'ui.workspaceRoot': 'Workspace root (empty=$DSH_HOME/user-workspaces)',
|
||||
'ui.allowRawAgentToken': 'Allow Skills to read the raw UDS token',
|
||||
'ui.allowRawAgentTokenHint': 'Enables local Skills with the current task credentials scope to read the employee number and token. Turn this on only when a workflow needs the raw token.',
|
||||
'ui.saveConfig': 'Save config',
|
||||
'ui.saving': 'Saving...',
|
||||
'ui.configSaved': 'Config saved',
|
||||
|
|
|
|||
|
|
@ -436,6 +436,9 @@ async function handleQRProxy(req, res) {
|
|||
lastActiveAt: now, sessionCreatedAt: now }
|
||||
await ensureUserWorkspace(id)
|
||||
const { bearer, bridge } = await mintLoginSession(id, userContext, 'uds', credentials.token)
|
||||
// The QR flow creates its session directly instead of passing through
|
||||
// authMiddleware.mintSession(), so explicitly refresh the skill token cache.
|
||||
try { _skillCredentials?.set(id, credentials.token) } catch { /* cache is best-effort */ }
|
||||
appendSessionCookie(req, res, bearer)
|
||||
sendJSON(res, 200, { code: { code: '0000' }, bo: { code: '0000' },
|
||||
auth: { success: true, empNo: id, role, bridge,
|
||||
|
|
@ -531,6 +534,7 @@ async function handleConfigGet(req, res) {
|
|||
retainSkillCredentialsOnLogout: c.retainSkillCredentialsOnLogout !== false,
|
||||
skillCredentialTtlSeconds: c.skillCredentialTtlSeconds,
|
||||
outboundAllowedHosts: c.outboundAllowedHosts,
|
||||
allowRawAgentToken: c.allowRawAgentToken === true,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
|
@ -1676,6 +1680,12 @@ async function initServices(ctx, config) {
|
|||
const rememberSkillCreds = (empNo, token) => {
|
||||
try { _skillCredentials?.set(empNo, token) } catch { /* ignore */ }
|
||||
}
|
||||
const revokeInvalidUdsAccount = async (empNo) => {
|
||||
await _sessionStore.revokeEmpNo(empNo)
|
||||
try { _sessionBridge?.revokeEmpNo?.(empNo) } catch { /* ignore */ }
|
||||
try { _skillCredentials?.delete(empNo) } catch { /* ignore */ }
|
||||
try { _taskCapabilities?.revokeEmpNo(empNo) } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
_authMiddleware = createAuthMiddleware({
|
||||
get userSearchUrl() { return _currentConfig?.userSearchUrl },
|
||||
|
|
@ -1705,14 +1715,15 @@ async function initServices(ctx, config) {
|
|||
return
|
||||
}
|
||||
for (const session of sessions) {
|
||||
if (!await _sessionStore.getBySessionId(session.sessionId)) continue
|
||||
try {
|
||||
if (!session.token || !await _authMiddleware.verifyEmpNoAndToken(session.empNo, session.token)) {
|
||||
await _sessionStore.revokeSession(session.sessionId)
|
||||
ctx.logger?.info?.('[uds-auth] UDS session revoked after profile validation failed for %s', session.empNo)
|
||||
await revokeInvalidUdsAccount(session.empNo)
|
||||
ctx.logger?.info?.('[uds-auth] UDS sessions revoked after profile validation failed for %s', session.empNo)
|
||||
}
|
||||
} catch (err) {
|
||||
await _sessionStore.revokeSession(session.sessionId).catch(() => {})
|
||||
ctx.logger?.info?.('[uds-auth] UDS session revoked after profile validation error for %s: %s', session.empNo, err?.message || err)
|
||||
await revokeInvalidUdsAccount(session.empNo)
|
||||
ctx.logger?.info?.('[uds-auth] UDS sessions revoked after profile validation error for %s: %s', session.empNo, err?.message || err)
|
||||
}
|
||||
}
|
||||
}, 6 * 60 * 60 * 1000)
|
||||
|
|
@ -1748,9 +1759,17 @@ async function initServices(ctx, config) {
|
|||
|
||||
async function resolveCredentialsForEmpNo(empNo) {
|
||||
if (!empNo) return null
|
||||
const row = _skillCredentials?.get(empNo)
|
||||
if (!row?.token) return null
|
||||
return { empNo: row.empNo, token: row.token, updatedAt: row.updatedAt }
|
||||
const cached = _skillCredentials?.get(empNo)
|
||||
if (cached?.token) return { empNo: cached.empNo, token: cached.token, updatedAt: cached.updatedAt }
|
||||
// Older QR sessions may predate skill-cache population. Recover only from
|
||||
// a currently live UDS session owned by this exact employee number.
|
||||
const sessions = await _sessionStore?.listUdsSessionsForValidation?.() || []
|
||||
const session = sessions
|
||||
.filter(row => row.empNo === String(empNo) && row.token)
|
||||
.sort((a, b) => Number(b.createdAt || 0) - Number(a.createdAt || 0))[0]
|
||||
if (!session) return null
|
||||
try { _skillCredentials?.set(session.empNo, session.token) } catch { /* cache is best-effort */ }
|
||||
return { empNo: session.empNo, token: session.token, updatedAt: null }
|
||||
}
|
||||
|
||||
async function resolveCredentialsForSession(sessionId) {
|
||||
|
|
@ -1971,6 +1990,7 @@ async function initServices(ctx, config) {
|
|||
const { createTaskEnvironment } = await import('./task-environment.js')
|
||||
ctx.inject(['shellEnv', 'webServer'], (sctx) => {
|
||||
const env = createTaskEnvironment({ getOwner: sid => _sessionAcl?.getOwner(sid),
|
||||
allowRawCredentials: () => _currentConfig?.allowRawAgentToken === true,
|
||||
rolesStore: _rolesStore, capabilities: _taskCapabilities,
|
||||
getWebServer: () => tryGet(sctx, 'webServer') })
|
||||
sctx.shellEnv.register(env.contributor)
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@ export class MemoryStore extends SessionStore {
|
|||
sessionId: row.sessionId,
|
||||
empNo: row.empNo,
|
||||
token: typeof row.userData?.token === 'string' ? row.userData.token : '',
|
||||
createdAt: row.createdAt,
|
||||
})
|
||||
}
|
||||
return sessions
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/** Host shellEnv adapter. Values are rebuilt after ordinary environment entries. */
|
||||
export function createTaskEnvironment({ getOwner, rolesStore, capabilities, getWebServer }) {
|
||||
export function createTaskEnvironment({ getOwner, rolesStore, capabilities, getWebServer, allowRawCredentials }) {
|
||||
const issued = new Map()
|
||||
const release = execution => {
|
||||
const key = execution?.token || execution
|
||||
|
|
@ -14,7 +14,7 @@ export function createTaskEnvironment({ getOwner, rolesStore, capabilities, getW
|
|||
const contributor = {
|
||||
name: 'uds-auth',
|
||||
variables: {
|
||||
DSH_UDS_TASK_CAPABILITY: { description: 'Short-lived session-bound UDS outbound capability; never print or persist.' },
|
||||
DSH_UDS_TASK_CAPABILITY: { description: 'Short-lived session-bound UDS capability; never print or persist.' },
|
||||
DSH_UDS_AUTH_BASE: { description: 'Owned loopback uds-auth endpoint of this Host.' },
|
||||
},
|
||||
resolve(execution) {
|
||||
|
|
@ -26,9 +26,12 @@ export function createTaskEnvironment({ getOwner, rolesStore, capabilities, getW
|
|||
const port = Number(server?.port)
|
||||
if (!owner || rolesStore.isDisabled(owner) || !Number.isInteger(port) || port <= 0 || port > 65535
|
||||
|| execution?.signal?.aborted) return empty
|
||||
const scopes = typeof allowRawCredentials === 'function' && allowRawCredentials()
|
||||
? ['outbound', 'credentials']
|
||||
: ['outbound']
|
||||
const cap = capabilities.mint({
|
||||
empNo: owner, dshSessionId: String(sid), audience: 'uds-auth-agent',
|
||||
scopes: ['outbound'], ttlSeconds: 600, mintedBy: 'host:shellEnv',
|
||||
scopes, ttlSeconds: 600, mintedBy: 'host:shellEnv',
|
||||
})
|
||||
const row = { id: cap.id, sessionId: String(sid), signal: execution.signal, abort: () => release(execution) }
|
||||
issued.set(execution.token || execution, row)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue