feat(auth): validate agent context upstream

This commit is contained in:
2026-06-12 10:18:41 +08:00
parent 4b572d9264
commit 8857b18dc9
13 changed files with 247 additions and 57 deletions
+60 -6
View File
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
import cors from "cors";
import express from "express";
import { requireAgentAuth } from "./auth/agentAuth.js";
import { SessionTranscriptStore } from "./sessions/transcriptStore.js";
import { ChatSessionBridge } from "./chat/sessionBridge.js";
import { config } from "./config.js";
@@ -18,7 +19,11 @@ import {
} from "./results/store.js";
import { buildChatRouter } from "./routes/chat.js";
import { opencodeRuntime } from "./runtime/opencode.js";
import { getRuntimeSessionContext } from "./runtime/sessionContext.js";
import {
getRuntimeSessionContext,
markRuntimeSessionAuthExpired,
type RuntimeSessionContext,
} from "./runtime/sessionContext.js";
const app = express();
@@ -78,6 +83,14 @@ app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
});
return;
}
if (isRuntimeAuthExpired(context)) {
markAuthExpired(context, "access_token_expired");
res.status(401).json({
message: "access token expired; refresh chat context",
detail: sessionId,
});
return;
}
const command = typeof req.body?.command === "string" ? req.body.command.trim() : "";
if (!command) {
@@ -252,9 +265,20 @@ app.post("/internal/tools/session-search", async (req, res) => {
const callBackendJson = async (
path: string,
accessToken: string | undefined,
context: RuntimeSessionContext,
payload: unknown,
) => {
if (isRuntimeAuthExpired(context)) {
markAuthExpired(context, "access_token_expired");
return {
ok: false,
status: 401,
text: JSON.stringify({
message: "access token expired; refresh chat context",
}),
};
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), config.TJWATER_API_TIMEOUT_MS);
try {
@@ -262,8 +286,8 @@ const callBackendJson = async (
Accept: "application/json",
"Content-Type": "application/json",
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
if (context.accessToken) {
headers.Authorization = `Bearer ${context.accessToken}`;
}
const response = await fetch(new URL(path, config.TJWATER_API_BASE_URL), {
method: "POST",
@@ -272,6 +296,9 @@ const callBackendJson = async (
signal: controller.signal,
});
const text = await response.text();
if (response.status === 401) {
markAuthExpired(context, "access_token_rejected");
}
return {
ok: response.ok,
status: response.status,
@@ -287,6 +314,32 @@ const parseStringArray = (value: unknown) =>
? value.filter((item): item is string => typeof item === "string")
: undefined;
const AUTH_EXPIRY_SKEW_MS = 30_000;
function isRuntimeAuthExpired(context: RuntimeSessionContext) {
if (!context.tokenExpiresAt) {
return false;
}
const expiresAt = Date.parse(context.tokenExpiresAt);
if (!Number.isFinite(expiresAt)) {
return false;
}
return Date.now() >= expiresAt - AUTH_EXPIRY_SKEW_MS;
}
function markAuthExpired(
context: RuntimeSessionContext,
reason: NonNullable<RuntimeSessionContext["authExpired"]>["reason"],
) {
markRuntimeSessionAuthExpired(context.sessionId, reason);
void opencodeRuntime.abortSession(context.sessionId).catch((error) => {
logger.warn(
{ err: error, sessionId: context.sessionId },
"failed to abort runtime after auth expired",
);
});
}
app.post("/internal/tools/web-search", async (req, res) => {
if (req.header("x-agent-internal-token") !== internalToken) {
res.status(403).json({ message: "forbidden" });
@@ -328,7 +381,7 @@ app.post("/internal/tools/web-search", async (req, res) => {
try {
const response = await callBackendJson(
"/api/v1/web-search",
context.accessToken,
context,
payload,
);
res
@@ -371,7 +424,7 @@ app.post("/internal/tools/geocode", async (req, res) => {
try {
const response = await callBackendJson(
"/api/v1/tianditu/geocode",
context.accessToken,
context,
{ keyword },
);
res
@@ -389,6 +442,7 @@ app.post("/internal/tools/geocode", async (req, res) => {
app.use(
"/api/v1/agent/chat",
requireAgentAuth,
buildChatRouter(
sessionBridge,
opencodeRuntime,