From 70bde46efffaa05d22074c63162877e2baab91c3 Mon Sep 17 00:00:00 2001 From: oliver Date: Mon, 12 Oct 2026 10:36:33 +0800 Subject: [PATCH] Expose raw token access setting for skills --- uds-auth/docs/upgrade-0.3.10.zh.md | 2 +- uds-auth/lib/client.js | 15 ++++++++++++ uds-auth/lib/i18n.js | 4 ++++ uds-auth/lib/index.js | 34 ++++++++++++++++++++++------ uds-auth/lib/session/memory-store.js | 1 + uds-auth/lib/task-environment.js | 9 +++++--- 6 files changed, 54 insertions(+), 11 deletions(-) diff --git a/uds-auth/docs/upgrade-0.3.10.zh.md b/uds-auth/docs/upgrade-0.3.10.zh.md index 5ec7d248..9871973f 100644 --- a/uds-auth/docs/upgrade-0.3.10.zh.md +++ b/uds-auth/docs/upgrade-0.3.10.zh.md @@ -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` 开关控制。 diff --git a/uds-auth/lib/client.js b/uds-auth/lib/client.js index 2c0e2f53..8310e8c8 100644 --- a/uds-auth/lib/client.js +++ b/uds-auth/lib/client.js @@ -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', diff --git a/uds-auth/lib/i18n.js b/uds-auth/lib/i18n.js index 0c91b1d4..e08fa8b5 100644 --- a/uds-auth/lib/i18n.js +++ b/uds-auth/lib/i18n.js @@ -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', diff --git a/uds-auth/lib/index.js b/uds-auth/lib/index.js index 671793d4..6331e44c 100644 --- a/uds-auth/lib/index.js +++ b/uds-auth/lib/index.js @@ -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) diff --git a/uds-auth/lib/session/memory-store.js b/uds-auth/lib/session/memory-store.js index 121505d7..54c69d7c 100644 --- a/uds-auth/lib/session/memory-store.js +++ b/uds-auth/lib/session/memory-store.js @@ -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 diff --git a/uds-auth/lib/task-environment.js b/uds-auth/lib/task-environment.js index 9babb3ca..807fcffa 100644 --- a/uds-auth/lib/task-environment.js +++ b/uds-auth/lib/task-environment.js @@ -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)