feat(context): verify product session scope

This commit is contained in:
2026-09-22 10:43:36 +08:00
parent 88b41ba04c
commit 9faed59f23
12 changed files with 358 additions and 56 deletions
+32 -1
View File
@@ -13,6 +13,11 @@ import { type SessionMetadataStore } from "../sessions/metadataStore.js";
import { type SessionTranscriptStore } from "../sessions/transcriptStore.js";
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
import { uiRegistryResponse } from "../uiEnvelope/registry.js";
import {
attachTrustedAgentContext,
getLocalAgentContext,
ProductContextError,
} from "../context/localContext.js";
import { registerChatAuxiliaryRoutes } from "./chatAuxiliaryRoutes.js";
import { registerChatInteractionRoutes } from "./chatInteractionRoutes.js";
import { createChatRunState } from "./chatRunState.js";
@@ -48,6 +53,18 @@ export const buildChatRouter = (options: BuildChatRouterOptions) => {
const chatRouter = Router();
const { activeRuns, lastRunStatuses } = runState;
chatRouter.use((req, res, next) => {
void attachTrustedAgentContext(req)
.then(() => next())
.catch((error) => {
const status = error instanceof ProductContextError ? error.status : 503;
res.status(status).json({
code: "PRODUCT_CONTEXT_UNAVAILABLE",
message: error instanceof Error ? error.message : "product identity verification failed",
});
});
});
chatRouter.get("/models", (_req, res) => {
res.json({
default_model: resolveDefaultModel(config.OPENCODE_MODEL),
@@ -63,13 +80,27 @@ export const buildChatRouter = (options: BuildChatRouterOptions) => {
res.json(frontendActionRegistryResponse);
});
chatRouter.post("/frontend-actions/:action_id/result", (req, res) => {
chatRouter.post("/frontend-actions/:action_id/result", async (req, res) => {
const sessionId = req.header("x-agent-session-id")?.trim() ?? "";
if (!sessionId || req.params.action_id !== req.body?.actionId) {
res.status(400).json({ message: "invalid frontend action result" });
return;
}
try {
const context = getLocalAgentContext(req);
const sessionRecord = await sessionMetadataStore.get(
{
actorKey: context.actorKey,
projectId: context.projectId,
projectKey: context.projectKey,
userId: context.userId,
},
sessionId,
);
if (!sessionRecord) {
res.status(404).json({ message: "session not found" });
return;
}
const outcome = frontendActionCoordinator.submitResult(sessionId, req.body);
res.json({
accepted: true,
+21 -26
View File
@@ -59,7 +59,7 @@ export const registerChatAuxiliaryRoutes = (
return;
}
const result = await resultReferenceResolver.getFullAuthorized(resultRef, {
actorKey: toActorKey(context.userId), clientSessionId, projectId: context.projectId,
actorKey: context.actorKey, clientSessionId, projectId: context.projectId,
}, { expectedKind: RESULT_REFERENCE_KIND.serverApiPayload });
if (!result) {
res.status(404).json({ message: "result_ref not found" });
@@ -84,23 +84,25 @@ export const registerChatAuxiliaryRoutes = (
}
try {
frontendActionCoordinator.cancelSession(parsed.data.session_id);
const authContext = getLocalAgentContext(req);
const projectId = authContext.projectId;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
const sessionRecord = await sessionMetadataStore.get(
{ actorKey, projectId, projectKey, userId },
parsed.data.session_id,
);
const binding = sessionRecord
? await sessionBridge.abort({
clientSessionId: sessionRecord.sessionId,
sessionId: sessionRecord.sessionId,
})
: null;
const run = activeRuns.get(parsed.data.session_id);
if (!sessionRecord) {
res.status(204).end();
return;
}
frontendActionCoordinator.cancelSession(sessionRecord.sessionId);
const binding = await sessionBridge.abort({
clientSessionId: sessionRecord.sessionId,
sessionId: sessionRecord.sessionId,
});
const run = activeRuns.get(sessionRecord.sessionId);
if (run && run.status === "running") {
run.status = "aborted";
lastRunStatuses.set(parsed.data.session_id, "aborted");
@@ -115,16 +117,14 @@ export const registerChatAuxiliaryRoutes = (
progress: completeBackendProgress(message.progress),
todos: cancelBackendTodos(message.todos),
}));
if (sessionRecord) {
const currentState = await sessionUiStateStore.read(
toSessionUiStateContext(sessionRecord.sessionId),
);
await sessionUiStateStore.write(toSessionUiStateContext(sessionRecord.sessionId), {
sessionId: sessionRecord.sessionId,
isTitleManuallyEdited: currentState?.isTitleManuallyEdited ?? false,
messages: run.messages,
});
}
const currentState = await sessionUiStateStore.read(
toSessionUiStateContext(sessionRecord.sessionId),
);
await sessionUiStateStore.write(toSessionUiStateContext(sessionRecord.sessionId), {
sessionId: sessionRecord.sessionId,
isTitleManuallyEdited: currentState?.isTitleManuallyEdited ?? false,
messages: run.messages,
});
for (const subscriber of run.subscribers) {
subscriber.write("error", {
session_id: parsed.data.session_id,
@@ -135,11 +135,6 @@ export const registerChatAuxiliaryRoutes = (
run.subscribers.clear();
}
if (!binding && !run) {
res.status(204).end();
return;
}
logger.info(
{
clientSessionId: parsed.data.session_id,
+6 -6
View File
@@ -91,8 +91,8 @@ export const registerChatInteractionRoutes = (
const authContext = getLocalAgentContext(req);
const projectId = authContext.projectId;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
const sessionRecord = await sessionMetadataStore.get(
{ actorKey, projectId, projectKey, userId },
parsed.data.session_id,
@@ -216,8 +216,8 @@ export const registerChatInteractionRoutes = (
const authContext = getLocalAgentContext(req);
const projectId = authContext.projectId;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
const sessionRecord = await sessionMetadataStore.get(
{ actorKey, projectId, projectKey, userId },
parsed.data.session_id,
@@ -348,8 +348,8 @@ export const registerChatInteractionRoutes = (
const authContext = getLocalAgentContext(req);
const projectId = authContext.projectId;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
const sessionRecord = await sessionMetadataStore.get(
{ actorKey, projectId, projectKey, userId },
parsed.data.session_id,
+15 -15
View File
@@ -73,8 +73,8 @@ export const registerChatSessionRoutes = (
const authContext = getLocalAgentContext(req);
const projectId = authContext.projectId;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
const requestedSessionId = parsed.data.session_id?.trim();
let sessionId = requestedSessionId;
if (!sessionId) {
@@ -112,8 +112,8 @@ export const registerChatSessionRoutes = (
const authContext = getLocalAgentContext(req);
const projectId = authContext.projectId;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
const records = await sessionMetadataStore.list({
actorKey,
projectId,
@@ -139,8 +139,8 @@ export const registerChatSessionRoutes = (
const authContext = getLocalAgentContext(req);
const projectId = authContext.projectId;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
if (!sessionId) {
res.status(400).json({ message: "session_id is required" });
return;
@@ -183,8 +183,8 @@ export const registerChatSessionRoutes = (
const authContext = getLocalAgentContext(req);
const projectId = authContext.projectId;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
if (!sessionId) {
res.status(400).json({ message: "session_id is required" });
return;
@@ -213,7 +213,7 @@ export const registerChatSessionRoutes = (
session_id: sessionRecord.sessionId,
messages: state?.messages ?? run?.messages ?? [],
is_streaming: run?.status === "running",
run_status: getSessionRunStatus(sessionRecord.sessionId) ?? "completed",
run_status: getSessionRunStatus(sessionRecord.sessionId),
}),
);
@@ -254,8 +254,8 @@ export const registerChatSessionRoutes = (
const authContext = getLocalAgentContext(req);
const projectId = authContext.projectId;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
if (!sessionId || !title) {
res.status(400).json({ message: "session_id and title are required" });
return;
@@ -296,8 +296,8 @@ export const registerChatSessionRoutes = (
const authContext = getLocalAgentContext(req);
const projectId = authContext.projectId;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
if (!sessionId) {
res.status(400).json({ message: "session_id is required" });
return;
@@ -338,8 +338,8 @@ export const registerChatSessionRoutes = (
const traceId = req.header("x-trace-id") ?? undefined;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
const sourceSessionId = parsed.data.session_id?.trim();
const sourceSessionRecord = sourceSessionId
? await sessionMetadataStore.get(
+8 -2
View File
@@ -197,8 +197,8 @@ export const registerChatStreamRoute = (
const projectId = authContext.projectId;
const traceId = req.header("x-trace-id") ?? undefined;
const userId = authContext.userId;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const actorKey = authContext.actorKey;
const projectKey = authContext.projectKey;
const requestUiMessages = toUiMessages(parsed.data.messages);
const promptText =
parsed.data.message?.trim() || readLatestUserText(requestUiMessages);
@@ -219,6 +219,10 @@ export const registerChatStreamRoute = (
requestedSessionId,
)
: null;
if (requestedSessionId && !existingSessionRecord) {
res.status(404).json({ message: "session not found" });
return;
}
const hadExistingRuntimeSession = Boolean(existingSessionRecord);
const { binding, requestContext, created } = await sessionBridge.resolve({
@@ -227,6 +231,8 @@ export const registerChatStreamRoute = (
projectId,
traceId,
userId,
actorKey,
projectKey,
});
setRuntimeSessionContext({
...(getRuntimeSessionContext(binding.sessionId)!),