fix(uds-auth): explain why skills get no credentials for a session (0.3.18)

Skill credentials follow the DSH session owner, so a chat created under
the emergency account stays owned by administrator after a QR scan and
skills cannot get a UAC token there. The generic "scan QR first" error
hid this.

- Return session_owner_no_uac when the owner is administrator / a local
  account, telling the user to start a new session after QR sign-in.
- Name the owner in no_skill_credentials for UAC owners.
- Audit denied outbound calls with session id and owner.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-12 18:52:31 +08:00
parent e61f9c6336
commit f0b4b6cd74
5 changed files with 82 additions and 10 deletions

View file

@ -158,7 +158,16 @@ export function createAgentAuthHandlers(deps) {
const creds = await deps.resolveCredentialsForSession(String(sessionId))
if (!creds || String(creds.empNo) !== String(caller.empNo)) {
return { error: 'no_skill_credentials', creds: null, sessionId: String(sessionId) }
// Credentials follow the session OWNER (stamped at creation), not whoever scanned last.
// Emergency / local-admin owners never hold UAC credentials, so name the owner.
const owner = String(caller.empNo)
const noUac = owner === 'administrator' || owner.startsWith('__')
return {
error: noUac ? 'session_owner_no_uac' : 'no_skill_credentials',
vars: { owner },
creds: null,
sessionId: String(sessionId),
}
}
return { error: null, creds, sessionId: String(sessionId), caller }
}
@ -185,7 +194,7 @@ export function createAgentAuthHandlers(deps) {
} catch (err) {
return sendErr(req, res, err.statusCode || 400, err.code || 'invalid_json')
}
const { error, creds, sessionId } = await resolveAuthorizedCreds(req, body, 'credentials')
const { error, creds, sessionId, vars } = await resolveAuthorizedCreds(req, body, 'credentials')
if (error || !creds) {
audit({
action: 'agent_credentials',
@ -194,7 +203,7 @@ export function createAgentAuthHandlers(deps) {
meta: { sessionId },
})
return sendJSON(res, 401, {
...apiError(error || 'no_skill_credentials', resolveLocale(req)),
...apiError(error || 'no_skill_credentials', resolveLocale(req), vars),
sessionId: sessionId || null,
})
}
@ -220,9 +229,15 @@ export function createAgentAuthHandlers(deps) {
return sendErr(req, res, 405, 'method_not_allowed')
}
const body = await readJsonBody(req)
const { error, creds } = await resolveAuthorizedCreds(req, body, 'outbound')
const { error, creds, vars, sessionId } = await resolveAuthorizedCreds(req, body, 'outbound')
if (error || !creds) {
return sendErr(req, res, 401, error || 'no_skill_credentials')
audit({
action: 'agent_outbound',
decision: 'deny',
reasonCode: error || 'no_skill_credentials',
meta: { sessionId, owner: vars?.owner },
})
return sendErr(req, res, 401, error || 'no_skill_credentials', vars)
}
const targetUrl = body.url

View file

@ -156,7 +156,8 @@ window.__ModuleLoader__.load({
"err.session_workspace_only": "只能在自己的工作区创建会话",
"err.workspace_path_only": "只能打开自己的工作区路径",
"err.workspace_create_forbidden": "只有管理员可以创建工作区",
"err.no_skill_credentials": "请先完成 UAC 扫码登录",
"err.session_owner_no_uac": "此会话归属 {owner}(应急 / 本机账号),没有 UAC 凭证;扫码登录不会改变已有会话的归属。请在扫码登录后新建会话再使用",
"err.no_skill_credentials": "会话归属 {owner} 的 UAC 登录已失效或未保存凭证,请该账号重新扫码登录后重试",
"err.loopback_only_credentials": "agent-credentials 仅允许本机访问",
"err.loopback_only_outbound": "outbound 仅允许本机访问",
"err.url_required": "缺少 url",
@ -313,7 +314,8 @@ window.__ModuleLoader__.load({
"err.session_workspace_only": "Sessions can only be created in your own workspace",
"err.workspace_path_only": "You can only open your own workspace path",
"err.workspace_create_forbidden": "Only admins can create workspaces",
"err.no_skill_credentials": "Complete UAC QR sign-in first",
"err.session_owner_no_uac": "This session is owned by {owner} (emergency / local account) which has no UAC credentials; scanning does not change who owns an existing session. Start a new session after QR sign-in",
"err.no_skill_credentials": "UAC sign-in for session owner {owner} is missing or expired; scan the QR code again as that account and retry",
"err.loopback_only_credentials": "agent-credentials is loopback-only",
"err.loopback_only_outbound": "outbound is loopback-only",
"err.url_required": "url required",

View file

@ -180,7 +180,8 @@ export const MESSAGES = {
'err.session_workspace_only': '只能在自己的工作区创建会话',
'err.workspace_path_only': '只能打开自己的工作区路径',
'err.workspace_create_forbidden': '只有管理员可以创建工作区',
'err.no_skill_credentials': '请先完成 UAC 扫码登录',
'err.session_owner_no_uac': '此会话归属 {owner}(应急 / 本机账号),没有 UAC 凭证;扫码登录不会改变已有会话的归属。请在扫码登录后新建会话再使用',
'err.no_skill_credentials': '会话归属 {owner} 的 UAC 登录已失效或未保存凭证,请该账号重新扫码登录后重试',
'err.loopback_only_credentials': 'agent-credentials 仅允许本机访问',
'err.loopback_only_outbound': 'outbound 仅允许本机访问',
'err.url_required': '缺少 url',
@ -372,7 +373,8 @@ export const MESSAGES = {
'err.session_workspace_only': 'Sessions can only be created in your own workspace',
'err.workspace_path_only': 'You can only open your own workspace path',
'err.workspace_create_forbidden': 'Only admins can create workspaces',
'err.no_skill_credentials': 'Complete UAC QR sign-in first',
'err.session_owner_no_uac': 'This session is owned by {owner} (emergency / local account) which has no UAC credentials; scanning does not change who owns an existing session. Start a new session after QR sign-in',
'err.no_skill_credentials': 'UAC sign-in for session owner {owner} is missing or expired; scan the QR code again as that account and retry',
'err.loopback_only_credentials': 'agent-credentials is loopback-only',
'err.loopback_only_outbound': 'outbound is loopback-only',
'err.url_required': 'url required',