/* * SPEC: PJ2026-010404 项目管理 draft-2026-06-25-p0-project-management-mdtodo; PJ2026-010401 Web工作台 draft-2026-06-18-r1; PJ2026-010403 API契约 draft-2026-06-18-r1. * 职责: Workbench 公共 launch API,用稳定 project/task 元数据创建 Workbench session 并写入项目管理 link 投影。 */ import { createHash, randomUUID } from "node:crypto"; import { getHeader, readBody, safeConversationId, safeOpaqueId, safeSessionId, safeTraceId, sendJson } from "./server-http-utils.ts"; import { emitHttpServerRequestSpan, newOtelSpanId } from "./otel-trace.ts"; import { writeWorkbenchSessionAdmissionFact } from "./workbench-projection-writer.ts"; const contractVersion = "workbench-launch-v1"; const defaultProjectId = "project_hwlab_v03"; export async function handleWorkbenchLaunchHttp(request, response, options = {}) { const env = options.env ?? process.env; attachWorkbenchLaunchOtel(request, { stage: "start", result: "started" }); if (request.method !== "POST") { attachWorkbenchLaunchOtel(request, { stage: "method", result: "failed", errorCode: "method_not_allowed", statusCode: 405 }); emitWorkbenchLaunchOtelSpan(request, env, "method", { result: "failed", errorCode: "method_not_allowed", statusCode: 405 }, { status: "error" }); return sendJson(response, 405, launchError("method_not_allowed", "POST required.")); } const auth = await options.accessController?.authenticate?.(request, { required: true }); if (!auth?.ok) { const statusCode = auth?.status ?? 401; const errorCode = auth?.error?.code ?? auth?.code ?? "auth_required"; attachWorkbenchLaunchOtel(request, { stage: "auth", result: "failed", errorCode, statusCode }); emitWorkbenchLaunchOtelSpan(request, env, "auth", { result: "failed", errorCode, statusCode }, { status: "error" }); return sendJson(response, statusCode, auth ?? launchError("auth_required", "Authentication is required.")); } const body = await readJsonObject(request, options.bodyLimitBytes); if (!body.ok) { attachWorkbenchLaunchOtel(request, { stage: "parse_body", result: "failed", errorCode: body.code, statusCode: 400 }); emitWorkbenchLaunchOtelSpan(request, env, "parse_body", { result: "failed", errorCode: body.code, statusCode: 400 }, { status: "error" }); return sendJson(response, 400, launchError(body.code, body.message, { reason: body.reason })); } const params = body.value; const projectId = safeToken(params.projectId, defaultProjectId); const taskRef = safeProjectOpaque(params.taskRef); if (!taskRef) { attachWorkbenchLaunchOtel(request, { stage: "validate", result: "failed", projectId, errorCode: "task_ref_required", statusCode: 400 }); emitWorkbenchLaunchOtelSpan(request, env, "validate", { result: "failed", projectId, errorCode: "task_ref_required", statusCode: 400 }, { status: "error" }); return sendJson(response, 400, launchError("task_ref_required", "Workbench launch requires taskRef.")); } const now = new Date().toISOString(); const conversationId = safeConversationId(params.conversationId) || `cnv_${randomUUID()}`; const sessionId = safeSessionId(params.sessionId) || `ses_${randomUUID()}`; const traceId = safeTraceId(params.traceId) || null; const providerProfile = safeToken(params.providerProfile ?? params.codeAgentProviderProfile, null); const contextStartedAt = Date.now(); attachWorkbenchLaunchOtel(request, { projectId, taskRef, sessionId, conversationId, traceId, stage: "launch_context", result: "started" }); const authoritativeContext = await readProjectManagementLaunchContext(request, { env, auth, projectId, taskRef }); if (!authoritativeContext.ok) { attachWorkbenchLaunchOtel(request, { projectId, taskRef, sessionId, conversationId, traceId, stage: "launch_context", result: "failed", errorCode: authoritativeContext.code, upstreamStatus: authoritativeContext.status, statusCode: authoritativeContext.status }); emitWorkbenchLaunchOtelSpan(request, env, "launch_context", { projectId, taskRef, sessionId, conversationId, traceId, result: "failed", errorCode: authoritativeContext.code, upstreamStatus: authoritativeContext.status, statusCode: authoritativeContext.status }, { startTimeMs: contextStartedAt, status: "error", statusMessage: authoritativeContext.code }); return sendJson(response, authoritativeContext.status, launchError(authoritativeContext.code, authoritativeContext.message, { retryable: authoritativeContext.retryable })); } const launchContext = normalizeLaunchContext(params.launchContext, { projectId, taskRef, now, authoritative: authoritativeContext.launchContext }); const linkId = safeToken(params.linkId, stableLinkId({ projectId, taskRef, sessionId })); const baseOtel = launchContextOtelFields({ projectId, taskRef, sessionId, conversationId, traceId, linkId, launchContext }); attachWorkbenchLaunchOtel(request, { ...baseOtel, stage: "launch_context", result: "ok", upstreamStatus: authoritativeContext.status, statusCode: 200 }); emitWorkbenchLaunchOtelSpan(request, env, "launch_context", { ...baseOtel, result: "ok", upstreamStatus: authoritativeContext.status, statusCode: 200 }, { startTimeMs: contextStartedAt }); attachWorkbenchLaunchOtel(request, { ...baseOtel, stage: "session_owner", result: "started" }); let session; const sessionOwnerStartedAt = Date.now(); try { session = await options.accessController.recordAgentSessionOwner({ ownerUserId: auth.actor?.id, ownerRole: auth.actor?.role, sessionId, projectId, agentId: "hwlab-code-agent", status: "idle", conversationId, threadId: safeOpaqueId(params.threadId), traceId, session: { source: "workbench-launch", providerProfile, sessionStatus: "idle", launchContext, createdAt: now, valuesRedacted: true, secretMaterialStored: false } }); attachWorkbenchLaunchOtel(request, { ...baseOtel, stage: "session_owner", result: "ok", ownerUserId: auth.actor?.id ?? null, sessionId: session?.id ?? sessionId }); emitWorkbenchLaunchOtelSpan(request, env, "session_owner", { ...baseOtel, sessionId: session?.id ?? sessionId, result: "ok", ownerUserId: auth.actor?.id ?? null, statusCode: 200 }, { startTimeMs: sessionOwnerStartedAt }); } catch (error) { attachWorkbenchLaunchOtel(request, { ...baseOtel, stage: "session_owner", result: "failed", errorCode: "workbench_launch_session_owner_failed", statusCode: 500 }); emitWorkbenchLaunchOtelSpan(request, env, "session_owner", { ...baseOtel, result: "failed", errorCode: "workbench_launch_session_owner_failed", statusCode: 500 }, { startTimeMs: sessionOwnerStartedAt, status: "error", error }); throw error; } const admittedSessionId = session?.id ?? sessionId; const admissionStartedAt = Date.now(); attachWorkbenchLaunchOtel(request, { ...baseOtel, sessionId: admittedSessionId, stage: "admission_fact", result: "started" }); try { const admissionWrite = await writeWorkbenchSessionAdmissionFact({ runtimeStore: options.runtimeStore, session, ownerUserId: auth.actor?.id, ownerRole: auth.actor?.role, projectId, conversationId, threadId: safeOpaqueId(params.threadId), status: "idle", now }); attachWorkbenchLaunchOtel(request, { ...baseOtel, sessionId: admittedSessionId, stage: "admission_fact", result: "ok", admissionFactWritten: Boolean(admissionWrite) }); emitWorkbenchLaunchOtelSpan(request, env, "admission_fact", { ...baseOtel, sessionId: admittedSessionId, result: "ok", admissionFactWritten: Boolean(admissionWrite), statusCode: 200 }, { startTimeMs: admissionStartedAt }); } catch (error) { attachWorkbenchLaunchOtel(request, { ...baseOtel, sessionId: admittedSessionId, stage: "admission_fact", result: "failed", errorCode: "workbench_launch_admission_fact_failed", statusCode: 500 }); emitWorkbenchLaunchOtelSpan(request, env, "admission_fact", { ...baseOtel, sessionId: admittedSessionId, result: "failed", errorCode: "workbench_launch_admission_fact_failed", statusCode: 500 }, { startTimeMs: admissionStartedAt, status: "error", error }); throw error; } const workbenchUrl = `/workbench/sessions/${encodeURIComponent(admittedSessionId)}`; const linkWriteStartedAt = Date.now(); attachWorkbenchLaunchOtel(request, { ...baseOtel, sessionId: admittedSessionId, stage: "project_link_write", result: "started" }); const linkWrite = await writeProjectManagementLink(request, { env, auth, link: { linkId, projectId, taskRef, sessionId: admittedSessionId, traceId, role: "launch", createdBy: auth.actor?.id ?? null, link: { launchContext, workbenchUrl, valuesRedacted: true } } }); if (!linkWrite.ok) { attachWorkbenchLaunchOtel(request, { ...baseOtel, sessionId: admittedSessionId, stage: "project_link_write", result: "failed", errorCode: linkWrite.code, upstreamStatus: linkWrite.status, statusCode: linkWrite.status }); emitWorkbenchLaunchOtelSpan(request, env, "project_link_write", { ...baseOtel, sessionId: admittedSessionId, result: "failed", errorCode: linkWrite.code, upstreamStatus: linkWrite.status, statusCode: linkWrite.status }, { startTimeMs: linkWriteStartedAt, status: "error", statusMessage: linkWrite.code }); return sendJson(response, linkWrite.status, launchError(linkWrite.code, linkWrite.message, { sessionId: admittedSessionId, retryable: true })); } attachWorkbenchLaunchOtel(request, { ...baseOtel, sessionId: admittedSessionId, linkId: linkWrite.link?.linkId ?? linkId, stage: "completed", result: "created", upstreamStatus: linkWrite.status, statusCode: 201 }); emitWorkbenchLaunchOtelSpan(request, env, "project_link_write", { ...baseOtel, sessionId: admittedSessionId, linkId: linkWrite.link?.linkId ?? linkId, result: "ok", upstreamStatus: linkWrite.status, statusCode: linkWrite.status }, { startTimeMs: linkWriteStartedAt }); emitWorkbenchLaunchOtelSpan(request, env, "completed", { ...baseOtel, sessionId: admittedSessionId, linkId: linkWrite.link?.linkId ?? linkId, result: "created", upstreamStatus: linkWrite.status, statusCode: 201 }); return sendJson(response, 201, { ok: true, status: "created", contractVersion, sessionId: admittedSessionId, conversationId, projectId, taskRef, workbenchUrl, linkId: linkWrite.link?.linkId ?? linkId, link: linkWrite.link ?? null, launchContext, valuesRedacted: true, secretMaterialStored: false }); } function attachWorkbenchLaunchOtel(request, fields = {}) { const context = request?.hwlabHttpRequestContext; if (!context) return; const errorCode = safeAttr(fields.errorCode, null); context.route = "/v1/workbench/launches"; if (errorCode) { context.lastHttpError = { code: errorCode, category: "workbench_launch", layer: `workbench.launch.${safeAttr(fields.stage, "unknown")}` }; } context.otelAttributes = { ...(context.otelAttributes ?? {}), "workbench.route": "launches", "workbench.launch.stage": safeAttr(fields.stage, null), "workbench.launch.result": safeAttr(fields.result, null), "workbench.launch.project_id": safeAttr(fields.projectId, null), "workbench.launch.task_ref": safeAttr(fields.taskRef, null), "workbench.launch.task_ref_hash": fields.taskRef ? stableHash(fields.taskRef) : null, "workbench.launch.source_id": safeAttr(fields.sourceId, null), "workbench.launch.hwpod_id": safeAttr(fields.hwpodId, null), "workbench.launch.workspace_root_hash": safeAttr(fields.workspaceRootHash, null), "workbench.launch.context_fingerprint": safeAttr(fields.contextFingerprint, null), "workbench.launch.session_id": safeAttr(fields.sessionId, null), "workbench.launch.conversation_id": safeAttr(fields.conversationId, null), "workbench.launch.link_id": safeAttr(fields.linkId, null), "workbench.launch.trace_id": safeAttr(fields.traceId, null), "workbench.launch.owner_user_id": safeAttr(fields.ownerUserId, null), "workbench.launch.admission_fact_written": typeof fields.admissionFactWritten === "boolean" ? fields.admissionFactWritten : null, "workbench.launch.upstream_status": Number.isFinite(Number(fields.upstreamStatus)) ? Math.trunc(Number(fields.upstreamStatus)) : null, "workbench.launch.values_redacted": true, ...(errorCode ? { "error.code": errorCode, "error.category": "workbench_launch", "error.layer": `workbench.launch.${safeAttr(fields.stage, "unknown")}` } : {}) }; } function emitWorkbenchLaunchOtelSpan(request, env, stage, fields = {}, options = {}) { const context = request?.hwlabHttpRequestContext; if (!context?.traceId) return; const statusCode = Number.isFinite(Number(fields.statusCode)) ? Math.trunc(Number(fields.statusCode)) : (options.status === "error" ? 500 : 200); void emitHttpServerRequestSpan(context, env, { name: `workbench.launch.${stage}`, spanId: newOtelSpanId(), parentSpanId: context.spanId, startTimeMs: options.startTimeMs, endTimeMs: Date.now(), statusCode, method: "POST", route: "/v1/workbench/launches", status: options.status ?? (statusCode >= 400 ? "error" : "ok"), statusMessage: options.statusMessage ?? safeAttr(fields.errorCode, null), error: options.error, errorCode: safeAttr(fields.errorCode, null), errorCategory: fields.errorCode ? "workbench_launch" : null, errorLayer: fields.errorCode ? `workbench.launch.${stage}` : null, attributes: { ...(context.otelAttributes ?? {}), "workbench.launch.span": stage, "workbench.launch.stage": stage, "workbench.launch.result": safeAttr(fields.result, null), "workbench.launch.project_id": safeAttr(fields.projectId, null), "workbench.launch.task_ref": safeAttr(fields.taskRef, null), "workbench.launch.task_ref_hash": fields.taskRef ? stableHash(fields.taskRef) : null, "workbench.launch.source_id": safeAttr(fields.sourceId, null), "workbench.launch.hwpod_id": safeAttr(fields.hwpodId, null), "workbench.launch.workspace_root_hash": safeAttr(fields.workspaceRootHash, null), "workbench.launch.context_fingerprint": safeAttr(fields.contextFingerprint, null), "workbench.launch.session_id": safeAttr(fields.sessionId, null), "workbench.launch.conversation_id": safeAttr(fields.conversationId, null), "workbench.launch.link_id": safeAttr(fields.linkId, null), "workbench.launch.trace_id": safeAttr(fields.traceId, null), "workbench.launch.owner_user_id": safeAttr(fields.ownerUserId, null), "workbench.launch.admission_fact_written": typeof fields.admissionFactWritten === "boolean" ? fields.admissionFactWritten : null, "workbench.launch.upstream_status": Number.isFinite(Number(fields.upstreamStatus)) ? Math.trunc(Number(fields.upstreamStatus)) : null, "workbench.launch.values_redacted": true } }).catch(() => undefined); } async function readJsonObject(request, bodyLimitBytes) { const body = await readBody(request, bodyLimitBytes); try { const value = body ? JSON.parse(body) : {}; if (!value || typeof value !== "object" || Array.isArray(value)) return { ok: false, code: "invalid_params", message: "body must be a JSON object" }; return { ok: true, value }; } catch (error) { return { ok: false, code: "parse_error", message: "Invalid JSON body", reason: error?.message ?? String(error) }; } } async function writeProjectManagementLink(request, { env, auth, link }) { const baseUrl = String(env.HWLAB_PROJECT_MANAGEMENT_URL ?? "").trim(); if (!baseUrl) return { ok: false, status: 503, code: "project_management_service_unconfigured", message: "HWLAB_PROJECT_MANAGEMENT_URL is required to write Workbench link projection." }; let upstreamUrl; try { upstreamUrl = new URL("/v1/project-management/workbench-links", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`); } catch { return { ok: false, status: 503, code: "project_management_service_url_invalid", message: "HWLAB_PROJECT_MANAGEMENT_URL is invalid." }; } const headers = new Headers({ accept: "application/json", "content-type": "application/json", "x-hwlab-bridge-service": "hwlab-cloud-api" }); for (const name of ["x-request-id", "traceparent", "tracestate"]) { const value = getHeader(request, name); if (value) headers.set(name, value); } if (auth.actor?.id) headers.set("x-hwlab-actor-id", auth.actor.id); if (auth.actor?.role) headers.set("x-hwlab-actor-role", auth.actor.role); headers.set("x-hwlab-auth-method", auth.authMethod ?? (auth.apiKey ? "api-key" : auth.session ? "web-session" : "unknown")); try { const upstream = await fetch(upstreamUrl, { method: "POST", headers, body: JSON.stringify(link), signal: AbortSignal.timeout(positiveInteger(env.HWLAB_PROJECT_MANAGEMENT_PROXY_TIMEOUT_MS, 30000)) }); const payload = await upstream.json().catch(() => null); if (!upstream.ok) return { ok: false, status: upstream.status, code: payload?.error?.code ?? "project_management_link_write_failed", message: payload?.error?.message ?? "Project management link write failed." }; return { ok: true, status: upstream.status, link: payload?.link ?? null }; } catch (error) { return { ok: false, status: 502, code: "project_management_link_proxy_failed", message: error?.message ?? "Project management link write failed." }; } } async function readProjectManagementLaunchContext(request, { env, auth, projectId, taskRef }) { const baseUrl = String(env.HWLAB_PROJECT_MANAGEMENT_URL ?? "").trim(); if (!baseUrl) return { ok: false, status: 503, code: "project_management_service_unconfigured", message: "HWLAB_PROJECT_MANAGEMENT_URL is required to resolve MDTODO launch context.", retryable: true }; let upstreamUrl; try { upstreamUrl = new URL("/v1/project-management/mdtodo/launch-context", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`); upstreamUrl.searchParams.set("taskRef", taskRef); upstreamUrl.searchParams.set("projectId", projectId); } catch { return { ok: false, status: 503, code: "project_management_service_url_invalid", message: "HWLAB_PROJECT_MANAGEMENT_URL is invalid.", retryable: true }; } const headers = new Headers({ accept: "application/json", "x-hwlab-bridge-service": "hwlab-cloud-api" }); for (const name of ["x-request-id", "traceparent", "tracestate"]) { const value = getHeader(request, name); if (value) headers.set(name, value); } if (auth.actor?.id) headers.set("x-hwlab-actor-id", auth.actor.id); if (auth.actor?.role) headers.set("x-hwlab-actor-role", auth.actor.role); headers.set("x-hwlab-auth-method", auth.authMethod ?? (auth.apiKey ? "api-key" : auth.session ? "web-session" : "unknown")); try { const upstream = await fetch(upstreamUrl, { method: "GET", headers, signal: AbortSignal.timeout(positiveInteger(env.HWLAB_PROJECT_MANAGEMENT_PROXY_TIMEOUT_MS, 30000)) }); const payload = await upstream.json().catch(() => null); if (!upstream.ok) { return { ok: false, status: upstream.status, code: payload?.error?.code ?? "project_management_launch_context_failed", message: payload?.error?.message ?? "Project management launch context resolution failed.", retryable: upstream.status >= 500 }; } const launchContext = payload?.launchContext && typeof payload.launchContext === "object" ? payload.launchContext : payload?.executionContext; if (!launchContext || typeof launchContext !== "object") return { ok: false, status: 502, code: "project_management_launch_context_empty", message: "Project management launch context response did not include launchContext.", retryable: true }; return { ok: true, status: upstream.status, launchContext }; } catch (error) { return { ok: false, status: 502, code: "project_management_launch_context_proxy_failed", message: error?.message ?? "Project management launch context resolution failed.", retryable: true }; } } function normalizeLaunchContext(value, { projectId, taskRef, now, authoritative = null }) { const input = value && typeof value === "object" && !Array.isArray(value) ? value : {}; const trusted = authoritative && typeof authoritative === "object" && !Array.isArray(authoritative) ? authoritative : {}; const executionContext = normalizeExecutionContext(trusted.executionContext ?? trusted.execution); return { source: "project-management", projectId, taskRef, sourceId: safeToken(trusted.sourceId ?? input.sourceId, null), sourceKind: safeToken(trusted.sourceKind ?? input.sourceKind, null), fileRef: safeToken(trusted.fileRef ?? input.fileRef, null), relativePath: safeRelativePath(trusted.relativePath ?? input.relativePath), taskId: safeToken(trusted.taskId ?? input.taskId, null), title: shortText(trusted.title ?? input.title, 220), status: safeToken(trusted.status ?? input.status, null), hwpodId: executionContext?.hwpodId ?? safeProjectOpaque(trusted.hwpodId ?? input.hwpodId), nodeId: executionContext?.nodeId ?? safeProjectOpaque(trusted.nodeId ?? input.nodeId), workspaceRootRef: executionContext?.workspaceRootRef ?? safeWorkspaceRef(trusted.workspaceRootRef ?? input.workspaceRootRef), workspaceRootHash: executionContext?.workspaceRootHash ?? safeProjectOpaque(trusted.workspaceRootHash ?? input.workspaceRootHash), workspaceRootLabel: executionContext?.workspaceRootLabel ?? shortText(trusted.workspaceRootLabel ?? input.workspaceRootLabel, 180), mdtodoRootRef: executionContext?.mdtodoRootRef ?? safeRelativePath(trusted.mdtodoRootRef ?? input.mdtodoRootRef), hwpodWorkspaceArgs: executionContext?.hwpodWorkspaceArgs ?? shortText(trusted.hwpodWorkspaceArgs ?? input.hwpodWorkspaceArgs, 500), contextFingerprint: executionContext?.contextFingerprint ?? safeProjectOpaque(trusted.contextFingerprint ?? input.contextFingerprint), capabilities: sanitizeCapabilities(trusted.capabilities ?? input.capabilities), executionContext, bodyPreview: shortText(input.bodyPreview, 800), reportLinks: sanitizeReportLinks(input.reportLinks), route: "/projects/mdtodo", launchedAt: now, valuesRedacted: true }; } function normalizeExecutionContext(value) { const input = value && typeof value === "object" && !Array.isArray(value) ? value : null; if (!input) return null; return { sourceId: safeToken(input.sourceId, null), sourceKind: safeToken(input.sourceKind, null), projectId: safeToken(input.projectId, null), taskRef: safeProjectOpaque(input.taskRef), fileRef: safeToken(input.fileRef, null), relativePath: safeRelativePath(input.relativePath), taskId: safeToken(input.taskId, null), hwpodId: safeProjectOpaque(input.hwpodId), nodeId: safeProjectOpaque(input.nodeId), workspaceRootRef: safeWorkspaceRef(input.workspaceRootRef), workspaceRootHash: safeProjectOpaque(input.workspaceRootHash), workspaceRootLabel: shortText(input.workspaceRootLabel, 180), mdtodoRootRef: safeRelativePath(input.mdtodoRootRef), hwpodWorkspaceArgs: shortText(input.hwpodWorkspaceArgs, 500), contextFingerprint: safeProjectOpaque(input.contextFingerprint), capabilities: sanitizeCapabilities(input.capabilities), valuesRedacted: true }; } function launchContextOtelFields({ projectId, taskRef, sessionId, conversationId, traceId, linkId, launchContext }) { const execution = launchContext?.executionContext && typeof launchContext.executionContext === "object" ? launchContext.executionContext : launchContext; return { projectId, taskRef, sessionId, conversationId, traceId, linkId, sourceId: launchContext?.sourceId ?? null, hwpodId: execution?.hwpodId ?? null, workspaceRootHash: execution?.workspaceRootHash ?? null, contextFingerprint: execution?.contextFingerprint ?? launchContext?.contextFingerprint ?? null }; } function sanitizeCapabilities(value) { const input = value && typeof value === "object" && !Array.isArray(value) ? value : {}; const out = {}; for (const [key, item] of Object.entries(input)) { if (!/^[A-Za-z0-9_.:-]{1,80}$/u.test(key)) continue; if (["string", "number", "boolean"].includes(typeof item) || item === null) out[key] = item; } return out; } function sanitizeReportLinks(value) { if (!Array.isArray(value)) return []; return value.slice(0, 12).map((item) => { const input = item && typeof item === "object" ? item : {}; return { linkId: safeToken(input.linkId, null), label: shortText(input.label, 160), href: shortText(input.href, 500), relativePath: safeRelativePath(input.relativePath) }; }); } function launchError(code, message, extra = {}) { return { ok: false, contractVersion, error: { code, message, status: extra.status ?? null, retryable: extra.retryable ?? false, sessionId: extra.sessionId ?? null, reason: extra.reason ?? null }, valuesRedacted: true }; } function stableLinkId({ projectId, taskRef, sessionId }) { return `lnk_${createHash("sha256").update(`${projectId}\n${taskRef}\n${sessionId}`).digest("hex").slice(0, 24)}`; } function stableHash(value) { return createHash("sha256").update(String(value ?? "")).digest("hex").slice(0, 24); } function safeAttr(value, fallback) { const text = String(value ?? "").trim(); return text ? text.slice(0, 360) : fallback; } function safeToken(value, fallback) { const text = String(value ?? "").trim(); return /^[A-Za-z0-9_.:-]{1,160}$/u.test(text) ? text : fallback; } function safeProjectOpaque(value) { const text = String(value ?? "").trim(); return /^[A-Za-z0-9_.:/#-]{1,360}$/u.test(text) ? text : null; } function safeRelativePath(value) { const text = String(value ?? "").replace(/\\/gu, "/").trim().replace(/^\/+|\/+$/gu, ""); if (!text) return null; if (text === ".") return "."; if (text.includes("..") || /(^|\/)\./u.test(text)) return null; return /^[\p{L}\p{N}_./ -]{1,360}$/u.test(text) ? text : null; } function safeWorkspaceRef(value) { const text = String(value ?? "").trim(); if (!text || text.includes("\0") || text.length > 500) return null; return /^[\p{L}\p{N}_:./\\ -]+$/u.test(text) ? text : null; } function shortText(value, max) { const text = String(value ?? "").replace(/\s+/gu, " ").trim(); return text ? text.slice(0, max) : null; } function positiveInteger(value, fallback) { const number = Number(value); return Number.isInteger(number) && number > 0 ? number : fallback; }