Tighten app-server boundary contracts

This commit is contained in:
murashit 2026-07-12 08:48:16 +09:00
parent 2364664c2e
commit 859ccb2ac1
9 changed files with 33 additions and 32 deletions

View file

@ -72,6 +72,7 @@
"**/src/**/*.{ts,tsx}",
"!**/src/app-server/connection/**",
"!**/src/app-server/protocol/server-requests.ts",
"!**/src/app-server/protocol/thread.ts",
"!**/src/app-server/protocol/turn.ts"
]
},

View file

@ -1,14 +1,15 @@
import type { Thread, ThreadProvenance } from "../../domain/threads/model";
import type { Thread as GeneratedThread } from "../../generated/app-server/v2/Thread";
export interface ThreadRecord {
id: string;
preview: string;
name: string | null;
createdAt: number;
updatedAt: number;
recencyAt?: number | null;
[key: string]: unknown;
}
type RequiredThreadRecordFields = "id" | "preview" | "name" | "createdAt" | "updatedAt";
export type ThreadRecord = Pick<GeneratedThread, RequiredThreadRecordFields> &
Partial<Omit<GeneratedThread, RequiredThreadRecordFields | "source" | "status" | "turns">> & {
/** Kept unknown at the protocol edge so a newer SessionSource variant degrades to `other` instead of breaking thread lists. */
source?: unknown;
status?: unknown;
turns?: readonly GeneratedThread["turns"][number][];
};
export function threadFromThreadRecord(thread: ThreadRecord, options: { archived?: boolean } = {}): Thread {
const hasRecencyAt = Object.hasOwn(thread, "recencyAt");

View file

@ -3,7 +3,7 @@ import type { ThreadTokenUsage, TokenUsageBreakdown } from "../../domain/runtime
const ROLLOUT_TOKEN_USAGE_READ_TIMEOUT_MS = 2_000;
const ROLLOUT_TOKEN_USAGE_MAX_BASE64_BYTES = 12 * 1024 * 1024;
export type RolloutReadFileBase64 = (path: string, options: { timeoutMs: number }) => Promise<string>;
export type RolloutReadFileBase64 = (path: string, options: { timeoutMs: number }) => Promise<string | null>;
export async function recoverRolloutTokenUsage(
path: string | null,
@ -11,12 +11,13 @@ export async function recoverRolloutTokenUsage(
): Promise<ThreadTokenUsage | null> {
if (!path || !isAbsolutePath(path)) return null;
let dataBase64: string;
let dataBase64: string | null;
try {
dataBase64 = await readFileBase64(path, { timeoutMs: ROLLOUT_TOKEN_USAGE_READ_TIMEOUT_MS });
} catch {
return null;
}
if (dataBase64 === null) return null;
if (dataBase64.length > ROLLOUT_TOKEN_USAGE_MAX_BASE64_BYTES) return null;
const text = decodeBase64Text(dataBase64);

View file

@ -25,10 +25,6 @@ import type { AppServerRequestClient } from "./request-client";
const THREAD_LIST_PAGE_LIMIT = 100;
export type ThreadTurnSortDirection = "asc" | "desc";
export type TurnTranscriptSummaryClient = AppServerRequestClient;
export type ThreadForkClient = AppServerRequestClient;
export type ThreadRollbackClient = AppServerRequestClient;
export type ThreadCompactionClient = AppServerRequestClient;
interface TurnTranscriptSummaryPage {
summaries: TurnTranscriptSummary[];
@ -165,7 +161,7 @@ export async function readThreadForArchiveExport(client: AppServerRequestClient,
}
export async function readCompletedTurnTranscriptSummariesPage(
client: TurnTranscriptSummaryClient,
client: AppServerRequestClient,
threadId: string,
cursor: string | null,
limit: number,
@ -179,7 +175,7 @@ export async function readCompletedTurnTranscriptSummariesPage(
}
export async function readReferencedThreadTurnTranscriptSummaries(
client: TurnTranscriptSummaryClient,
client: AppServerRequestClient,
threadId: string,
limit = REFERENCED_THREAD_TURN_LIMIT,
): Promise<TurnTranscriptSummary[]> {
@ -208,13 +204,13 @@ function threadRollbackSnapshotFromAppServerResponse(response: ThreadRollbackRes
};
}
export async function rollbackThread(client: ThreadRollbackClient, threadId: string, numTurns?: number): Promise<ThreadRollbackSnapshot> {
export async function rollbackThread(client: AppServerRequestClient, threadId: string, numTurns?: number): Promise<ThreadRollbackSnapshot> {
const response = await client.request("thread/rollback", { threadId, numTurns: numTurns ?? 1 });
return threadRollbackSnapshotFromAppServerResponse(response);
}
export async function forkThread(
client: ThreadForkClient,
client: AppServerRequestClient,
threadId: string,
cwd: string,
lastTurnId: string | null = null,
@ -234,7 +230,7 @@ export interface EphemeralThreadForkSnapshot {
}
export async function forkEphemeralThread(
client: ThreadForkClient,
client: AppServerRequestClient,
sourceThreadId: string,
cwd: string,
): Promise<EphemeralThreadForkSnapshot> {
@ -252,7 +248,7 @@ export async function forkEphemeralThread(
};
}
export async function compactThread(client: ThreadCompactionClient, threadId: string): Promise<void> {
export async function compactThread(client: AppServerRequestClient, threadId: string): Promise<void> {
await client.request("thread/compact/start", { threadId });
}

View file

@ -38,8 +38,11 @@ export interface RuntimePermissionState {
}
export interface RuntimePermissionKnownState {
/** True when the active-thread layer is authoritative, including an authoritative null meaning the detail was not reported. */
readonly approvalPolicyKnown: boolean;
/** True prevents fallback to a different config profile's sandbox; null still displays as not reported. */
readonly sandboxPolicyKnown: boolean;
/** True when the active profile identity, including an authoritative null, supersedes config fallback. */
readonly permissionProfileKnown: boolean;
}

View file

@ -23,7 +23,7 @@ interface ChatThreadReferenceResolverOptions {
export interface ChatAppServerGateway extends ChatSessionTransports, ChatMetadataTransports {
clientAccess: AppServerClientAccess;
connectionAvailable(): boolean;
readFileBase64(path: string, options?: { timeoutMs?: number }): Promise<string>;
readFileBase64(path: string, options?: { timeoutMs?: number }): Promise<string | null>;
threadReferences(options: ChatThreadReferenceResolverOptions): ThreadReferenceResolver;
}
@ -70,9 +70,9 @@ async function readCurrentClientFileBase64(
host: ChatAppServerGatewayHost,
path: string,
options: { timeoutMs?: number } = {},
): Promise<string> {
): Promise<string | null> {
const client = host.currentClient();
if (!client) return "";
if (!client) return null;
const response = await client.request("fs/readFile", { path }, options);
return host.currentClient() === client ? response.dataBase64 : "";
return host.currentClient() === client ? response.dataBase64 : null;
}

View file

@ -1,4 +1,5 @@
import { readReferencedThreadTurnTranscriptSummaries, type TurnTranscriptSummaryClient } from "../../../app-server/services/threads";
import type { AppServerRequestClient } from "../../../app-server/services/request-client";
import { readReferencedThreadTurnTranscriptSummaries } from "../../../app-server/services/threads";
import { type CodexInput, codexTextInputWithAttachments } from "../../../domain/chat/input";
import { shortThreadId } from "../../../domain/threads/id";
import type { Thread } from "../../../domain/threads/model";
@ -7,7 +8,7 @@ import type { ComposerInputSnapshot } from "../application/composer/input-snapsh
import type { ThreadReferenceInput } from "../application/turns/slash-command-execution";
interface ThreadReferenceResolverHost {
currentClient(): TurnTranscriptSummaryClient | null;
currentClient(): AppServerRequestClient | null;
prepareInput(text: string, snapshot: ComposerInputSnapshot): { text: string; input: CodexInput };
addSystemMessage(text: string): void;
setStatus(status: string): void;

View file

@ -45,9 +45,6 @@ interface ChatAppServerTransportHost extends ConnectedChatAppServerClientHost {
vaultPath: string;
}
type ChatThreadHistoryClient = AppServerRequestClient;
type ChatThreadResumeClient = AppServerRequestClient;
interface AppServerThreadTurnsPage {
readonly data: ThreadTurnsPage["turns"];
readonly nextCursor: string | null;
@ -244,7 +241,7 @@ async function withCurrentChatAppServerClient<T>(
}
async function readChatThreadHistoryPage(
client: ChatThreadHistoryClient,
client: AppServerRequestClient,
threadId: string,
cursor: string | null,
limit = 20,
@ -260,7 +257,7 @@ function chatThreadHistoryPageFromTurnsPage(page: ThreadTurnsPage): ThreadHistor
};
}
async function resumeChatThread(client: ChatThreadResumeClient, threadId: string, cwd: string): Promise<ThreadResumeSnapshot> {
async function resumeChatThread(client: AppServerRequestClient, threadId: string, cwd: string): Promise<ThreadResumeSnapshot> {
const response = await resumeThread(client, threadId, cwd);
return {
activation: threadActivationSnapshotFromAppServerResponse(response),

View file

@ -96,6 +96,7 @@ describe("Biome Grit plugin wiring", () => {
"**/src/**/*.{ts,tsx}",
"!**/src/app-server/connection/**",
"!**/src/app-server/protocol/server-requests.ts",
"!**/src/app-server/protocol/thread.ts",
"!**/src/app-server/protocol/turn.ts",
]);
expect(projectPluginIncludes("no-restricted-css-policy.grit")).toEqual(["**/src/styles/**/*.css"]);