oclaw/uds-auth/lib/task-environment.js
oliver df97c5e042
Some checks are pending
ci / test (push) Waiting to run
ci / test-postgresql (push) Waiting to run
fix(uds-auth): emergency login workspaces, Desktop key prompts, persistent sessions
Emergency (fallback / sealed-box) admins lost the workspace sidebar and saw
"sign-in failed / add API key" on DSH Desktop because the remote.mux opens
anonymously at boot and every early call was rejected terminally.

- gateway: anonymous workspace/follow reaches dsh-acl's empty baseline;
  anonymous streams are parked until the carrier aborts instead of failing;
  anonymous workspace/initializeDefault answers "nothing created"
- request-auth: a dead UDS_SESSION cookie no longer shadows a valid WS ticket
  or bridge (sync + async variants used by all callers)
- dsh-acl: /api/* waits (bounded 15s) for the gateway ACL during Host startup
  instead of 403, so the Desktop welcome read no longer fails
- desktop-bootstrap: key/account projection is best-effort per reference
- client: emit connection/reset after login so boot-time caches (settings
  describe mirror -> Settings > Models) re-read under the real principal
- sessions: absolute lifetime fixed at 7 days (was min(ttl, 7d) = 30 min);
  bridge expiry slides with its session; MemoryStore persisted to
  sessions.json (bearer hash only, userData AES-256-GCM sealed)
- README: session lifetime/persistence and DSH compatibility notes

Also includes previously uncommitted uds-auth 0.3.x work in this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:11:09 +08:00

60 lines
2.7 KiB
JavaScript

/** Host shellEnv adapter. Values are rebuilt after ordinary environment entries. */
export function createTaskEnvironment({ getOwner, rolesStore, capabilities, getWebServer }) {
const issued = new Map()
const release = execution => {
const key = execution?.token || execution
const row = issued.get(key)
if (!row) return
capabilities.revoke(row.id)
row.signal?.removeEventListener('abort', row.abort)
clearTimeout(row.timer)
row.off?.()
issued.delete(key)
}
const contributor = {
name: 'uds-auth',
variables: {
DSH_UDS_TASK_CAPABILITY: { description: 'Short-lived session-bound UDS outbound capability; never print or persist.' },
DSH_UDS_AUTH_BASE: { description: 'Owned loopback uds-auth endpoint of this Host.' },
},
resolve(execution) {
release(execution)
const empty = { DSH_UDS_TASK_CAPABILITY: '', DSH_UDS_AUTH_BASE: '' }
const sid = execution?.agent?.session?.header?.id
const owner = sid ? getOwner(String(sid)) : null
const server = getWebServer()
const port = Number(server?.port)
if (!owner || rolesStore.isDisabled(owner) || !Number.isInteger(port) || port <= 0 || port > 65535
|| execution?.signal?.aborted) return empty
const cap = capabilities.mint({
empNo: owner, dshSessionId: String(sid), audience: 'uds-auth-agent',
scopes: ['outbound'], 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)
row.signal?.addEventListener('abort', row.abort, { once: true })
row.timer = setTimeout(() => release(execution), cap.expiresAt - Date.now())
row.timer.unref?.()
return { DSH_UDS_TASK_CAPABILITY: cap.token, DSH_UDS_AUTH_BASE: `http://127.0.0.1:${port}/uds-auth` }
},
}
return { contributor, release, finish(execution, result, jobs) {
const row = issued.get(execution?.token || execution)
const value = result?.value
if (row && !result?.isError && ['background', 'promoted'].includes(value?.kind)
&& typeof value.jobId === 'string' && jobs?.events?.subscribe) {
const job = jobs.list(row.sessionId).find(job => job.id === value.jobId)
if (job && ['running', 'stopping'].includes(job.status)) {
row.signal?.removeEventListener('abort', row.abort)
row.signal = null
row.off = jobs.events.subscribe({ owner: row.sessionId }, event => {
if (event.job?.id === value.jobId && ['settled', 'stopping', 'removed'].includes(event.type)) release(execution)
})
return
}
}
release(execution)
}, dispose() {
for (const key of [...issued.keys()]) release({ token: key })
} }
}