Remove legacy client-manager workflows

This commit is contained in:
npc0-hue
2026-09-03 13:08:08 +08:00
parent bf3c382d15
commit fe09d21a56
56 changed files with 304 additions and 4121 deletions
+1 -61
View File
@@ -122,7 +122,6 @@ const runtimeActions = {
{ key: "generate-run", label: "Generate run", available: true },
{ key: "download-run", label: "Download run", available: true },
{ key: "push-run-update", label: "Push run update", available: true },
{ key: "generate-client-manager", label: "Generate client manager", available: true },
{ key: "dependencies-check", label: "Check dependencies", available: true },
{ key: "historical-logs", label: "Historical logs", available: true }
]
@@ -476,53 +475,6 @@ describe("PlatformApiClient AI providers", () => {
count: 1
});
}
if (url.endsWith("/api/v1/server-instances/server-1/client-managers/generate") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({
profileKey: "example-client-manager",
targetOs: "windows",
targetArch: "amd64",
repositoryUrl: "https://example.test/client-manager.git",
sourceRevision: "main",
idempotencyKey: "idem-client-generate"
});
return jsonResponse({
id: "client-dist-1",
serverInstanceId: server.id,
pluginId: plugin.id,
profileKey: "example-client-manager",
targetOs: "windows",
targetArch: "amd64",
repositoryUrl: "https://example.test/client-manager.git",
sourceRevision: "main",
buildJobId: "client-build-1",
artifactId: "artifact-client-1",
checksum: "sha256:clientchecksum",
keyGeneration: 1,
secretRef: "secret://runtime-keys/server-1/client-manager/example-client-manager/current",
status: "available",
createdAt: "2026-07-03T00:00:00Z",
updatedAt: "2026-07-03T00:00:00Z"
});
}
if (url.endsWith("/api/v1/server-instances/server-1/client-managers/download") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ profileKey: "example-client-manager" });
return jsonResponse({ ...runtimeDownload, artifactId: "artifact-client-1", filename: "example-client-manager.exe" });
}
if (url.endsWith("/api/v1/server-instances/server-1/client-managers/key/reset") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ componentKind: "client-manager", componentKey: "example-client-manager" });
return jsonResponse({
id: "runtime-key-server-1-client-2",
serverInstanceId: server.id,
componentKind: "client-manager",
componentKey: "example-client-manager",
secretRef: "secret://runtime-keys/server-1/client-manager/example-client-manager/current",
fingerprint: "def456abc123",
generation: 2,
status: "active",
createdAt: "2026-07-03T00:00:00Z",
updatedAt: "2026-07-03T00:00:00Z"
});
}
if (url.endsWith("/api/v1/server-instances/server-1/dependencies/check") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ probeKey: "java-21", idempotencyKey: "idem-dep-check" });
return jsonResponse({ ...job, id: "job-dep-check", capability: "dependencies.check", targetKey: "dependencies/java-21" });
@@ -652,18 +604,6 @@ describe("PlatformApiClient AI providers", () => {
status: "queued"
});
await expect(client.listRunUpdates(server.id)).resolves.toMatchObject({ count: 1, items: [{ phase: "restart-requested", rollback: false }] });
await expect(
client.generateClientManager(server.id, {
profileKey: "example-client-manager",
targetOs: "windows",
targetArch: "amd64",
repositoryUrl: "https://example.test/client-manager.git",
sourceRevision: "main",
idempotencyKey: "idem-client-generate"
})
).resolves.toMatchObject({ artifactId: "artifact-client-1", profileKey: "example-client-manager" });
await expect(client.downloadLatestClientManager(server.id, { profileKey: "example-client-manager" })).resolves.toMatchObject({ artifactId: "artifact-client-1" });
await expect(client.resetClientManagerKey(server.id, { componentKind: "client-manager", componentKey: "example-client-manager" })).resolves.toMatchObject({ generation: 2 });
await expect(client.checkDependencies(server.id, { probeKey: "java-21", idempotencyKey: "idem-dep-check" })).resolves.toMatchObject({ capability: "dependencies.check" });
await expect(client.getDependencyCatalog(server.id)).resolves.toMatchObject({ targetOs: "linux", plans: [{ digest: runtimeDigest }] });
await expect(client.installDependencies(server.id, { probeKey: "java-21", installPlanKey: "install-java-linux", planDigest: runtimeDigest, idempotencyKey: "idem-dep-install" })).resolves.toMatchObject({
@@ -673,7 +613,7 @@ describe("PlatformApiClient AI providers", () => {
client.invokeAI({ requestId: "ai-1", serverInstanceId: server.id, purpose: "config.suggest", prompt: "Tune PVP safely", currentConfig: "server.name=Example Survival #1\n" })
).resolves.toMatchObject({ status: "ok", usage: { mocked: true }, configRecommendation: { diffSummary: "review required" } });
expect(fetchMock).toHaveBeenCalledTimes(46);
expect(fetchMock).toHaveBeenCalledTimes(43);
});
it("normalizes server file workspace null arrays from older platform responses", async () => {
-66
View File
@@ -16,19 +16,7 @@ import type {
ArtifactFilterRequest,
ArtifactListResponse,
AuthSessionResponse,
ClientManagerBuildRequest,
ClientManagerControlRequest,
ClientManagerDeployRequest,
ClientManagerDistributionResponse,
ClientManagerDownloadRequest,
ClientManagerInstallationListResponse,
ClientManagerInstallationResponse,
ClientManagerRetryRequest,
ClientManagerRevokeSessionRequest,
ClientManagerUninstallRequest,
ClientManagerUpdateRequest,
ComponentKeyResponse,
ComponentKeyResetRequest,
CurrentUserResponse,
DependencyCatalogResponse,
DependencyJobRequest,
@@ -116,7 +104,6 @@ import type {
} from "./types";
import { readWebRuntimeEnv } from "../schemas/env";
import { parseSafeDependencyCatalog, parseSafeRunUpdate, parseSafeRunUpdateList } from "../schemas/runtimeUpdates";
import { parseSafeClientManagerLifecycle, parseSafeClientManagerLifecycleList } from "../schemas/clientManagerLifecycle";
import {
parseSafeGameClientBridgeCancellation,
parseSafeGameClientBridgeCommand,
@@ -352,59 +339,6 @@ export class PlatformApiClient {
return parseSafeRunUpdateList(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/run/update`));
}
async generateClientManager(id: string, request: ClientManagerBuildRequest): Promise<ClientManagerDistributionResponse> {
return this.request<ClientManagerDistributionResponse>(`/server-instances/${encodeURIComponent(id)}/client-managers/generate`, {
method: "POST",
body: request
});
}
async downloadLatestClientManager(id: string, request: ClientManagerDownloadRequest = {}): Promise<ArtifactDownloadReferenceResponse> {
return this.request<ArtifactDownloadReferenceResponse>(`/server-instances/${encodeURIComponent(id)}/client-managers/download`, {
method: "POST",
body: request
});
}
async resetClientManagerKey(id: string, request: ComponentKeyResetRequest): Promise<ComponentKeyResponse> {
return this.request<ComponentKeyResponse>(`/server-instances/${encodeURIComponent(id)}/client-managers/key/reset`, {
method: "POST",
body: request
});
}
async listClientManagerLifecycles(id: string): Promise<ClientManagerInstallationListResponse> {
return parseSafeClientManagerLifecycleList(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers`));
}
async getClientManagerLifecycle(id: string, profileKey: string): Promise<ClientManagerInstallationResponse> {
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/${encodeURIComponent(profileKey)}`));
}
async deployClientManager(id: string, request: ClientManagerDeployRequest): Promise<ClientManagerInstallationResponse> {
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/deploy`, { method: "POST", body: request }));
}
async controlClientManager(id: string, request: ClientManagerControlRequest): Promise<ClientManagerInstallationResponse> {
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/control`, { method: "POST", body: request }));
}
async updateClientManager(id: string, request: ClientManagerUpdateRequest): Promise<ClientManagerInstallationResponse> {
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/update`, { method: "POST", body: request }));
}
async retryClientManagerLifecycle(id: string, request: ClientManagerRetryRequest): Promise<ClientManagerInstallationResponse> {
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/retry`, { method: "POST", body: request }));
}
async revokeClientManagerSession(id: string, request: ClientManagerRevokeSessionRequest): Promise<ClientManagerInstallationResponse> {
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/revoke-session`, { method: "POST", body: request }));
}
async uninstallClientManager(id: string, request: ClientManagerUninstallRequest): Promise<ClientManagerInstallationResponse> {
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/uninstall`, { method: "POST", body: request }));
}
async getGameClientBridgeStatus(id: string): Promise<GameClientBridgeStatusResponse> {
return parseSafeGameClientBridgeStatus(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge`));
}
@@ -1,47 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { PlatformApiClient } from "./client";
const lifecycle = {
id: "installation-1", serverInstanceId: "server-1", pluginId: "game.example", profileKey: "example-client-manager", targetOs: "windows", targetArch: "amd64",
status: "available", phase: "artifact available", desiredVersion: "1.0.0", desiredRevision: "rev-1", desiredArtifactId: "artifact-1", keyGeneration: 1,
deploymentGeneration: 0, health: "unknown", healthReason: "component is not installed", retryable: false, requiresRedeploy: false, updatedAt: "2026-07-18T08:00:00Z",
distribution: { id: "distribution-1", artifactId: "artifact-1", sourceRevision: "rev-1", targetOs: "windows", targetArch: "amd64", checksum: `sha256:${"a".repeat(64)}`, keyGeneration: 1, status: "available" },
actions: [{ operation: "deploy", available: true }, { operation: "uninstall", available: false, reason: "not installed" }]
};
describe("PlatformApiClient Client Manager lifecycle", () => {
afterEach(() => vi.unstubAllGlobals());
it("uses typed Platform lifecycle routes and preserves action bodies", async () => {
const calls: Array<{ url: string; method: string; body?: unknown }> = [];
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
calls.push({ url, method: init?.method ?? "GET", body: init?.body ? JSON.parse(String(init.body)) : undefined });
return new Response(JSON.stringify(url.endsWith("/client-managers") && (init?.method ?? "GET") === "GET" ? { items: [lifecycle], count: 1 } : lifecycle), { status: 200, headers: { "Content-Type": "application/json" } });
}));
const client = new PlatformApiClient("/api/v1", () => "session-token");
await expect(client.listClientManagerLifecycles("server-1")).resolves.toMatchObject({ count: 1 });
await expect(client.getClientManagerLifecycle("server-1", "example-client-manager")).resolves.toMatchObject({ profileKey: "example-client-manager" });
await client.deployClientManager("server-1", { profileKey: "example-client-manager", distributionId: "distribution-1", expectedDeploymentGeneration: 0, idempotencyKey: "deploy-1" });
await client.controlClientManager("server-1", { profileKey: "example-client-manager", operation: "start", expectedDeploymentGeneration: 1, idempotencyKey: "start-1" });
await client.updateClientManager("server-1", { profileKey: "example-client-manager", distributionId: "distribution-2", expectedDeploymentGeneration: 1, approved: true, idempotencyKey: "update-1" });
await client.retryClientManagerLifecycle("server-1", { profileKey: "example-client-manager", expectedDeploymentGeneration: 2, idempotencyKey: "retry-1" });
await client.revokeClientManagerSession("server-1", { profileKey: "example-client-manager", reason: "operator revoked component session" });
await client.uninstallClientManager("server-1", { profileKey: "example-client-manager", expectedDeploymentGeneration: 2, confirmed: true, idempotencyKey: "uninstall-1" });
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
"GET /api/v1/server-instances/server-1/client-managers",
"GET /api/v1/server-instances/server-1/client-managers/example-client-manager",
"POST /api/v1/server-instances/server-1/client-managers/deploy",
"POST /api/v1/server-instances/server-1/client-managers/control",
"POST /api/v1/server-instances/server-1/client-managers/update",
"POST /api/v1/server-instances/server-1/client-managers/retry",
"POST /api/v1/server-instances/server-1/client-managers/revoke-session",
"POST /api/v1/server-instances/server-1/client-managers/uninstall"
]);
expect(calls[4]?.body).toMatchObject({ approved: true, distributionId: "distribution-2" });
expect(calls[7]?.body).toMatchObject({ confirmed: true });
});
});
-3
View File
@@ -61,6 +61,3 @@ Existing platform APIs already cover server lifecycle, jobs, log stream metadata
Browser Job contracts explicitly exclude raw or hashed lease tokens, Run session tokens/generations, secret refs, host paths, sockets, and credentials. The safe schema rejects those keys, and existing API client 401/403 behavior remains authoritative for expired sessions and cross-owner access.
- Server-scoped terminal log streaming (`GET /api/v1/server-instances/{id}/logs/events`) is used by the server detail terminal drawer for platform-accepted live SSE output only. It does not replay retained log entries, and accepted batches whose source entry timestamps predate the current SSE connection after a small clock-skew allowance stay out of the terminal stream; raw log list/backfill routes (`GET .../logs/live`, `POST .../logs/backfill`) and direct management-terminal/RCON input routes remain removed from product clients, and internal log ingest and cursor query remain available to platform services and maintenance/debug flows.
# Client Manager API projection
`PlatformApiClient` exposes list/detail and typed deploy, control, update, retry, revoke-session, and uninstall methods. `schemas/clientManagerLifecycle.ts` validates status/action/job/health fields and rejects forbidden machine or credential fields before rendering. Lifecycle commands carry profile, distribution, expected deployment generation, approval/confirmation, and idempotency only. Artifact bytes remain in the platform-owned artifact transfer client.
+4 -19
View File
@@ -10,12 +10,12 @@ const status = {
serverInstanceId: "server-1",
pluginId: "game.scum",
available: false,
reason: "compatible companion is offline",
reason: "plugin-owned bridge runtime is unavailable",
profiles: [{
pluginId: "game.scum",
profileKey: "scum-client",
available: false,
reason: "component heartbeat is unavailable",
reason: "declared runtime transport is unavailable",
commandTypes: ["scum.diagnostic.ping"],
snapshotTypes: ["scum.players"],
queryTemplateKeys: ["scum.player.search"]
@@ -98,22 +98,7 @@ const manifestDeclaration: GameClientBridgeManifestResponse = {
}],
commandRetentionSeconds: 86400,
maxCommands: 1000,
pages: [{ pageKey: "operations", commandTypes: ["scum.diagnostic.ping"], snapshotTypes: ["scum.players"], queryTemplateKeys: ["scum.player.search"] }],
companion: {
profileKey: "scum-client-manager",
configTemplateKey: "client-config",
configSchemaRef: "schemas/companion/config.schema.json",
configFormat: "yaml",
platformBaseUrlSource: "run-control",
registrationProof: "hmac-sha256",
proofMaterialSource: "component-package",
proofMaterialEnv: "SCUM_COMPONENT_PROOF",
sessionMode: "component-session",
tlsPolicy: "verify-system-roots",
heartbeatIntervalSeconds: 30,
commandPollIntervalSeconds: 5,
requestTimeoutSeconds: 15
}
pages: [{ pageKey: "operations", commandTypes: ["scum.diagnostic.ping"], snapshotTypes: ["scum.players"], queryTemplateKeys: ["scum.player.search"] }]
};
const pluginBridgeProjection: Pick<GamePluginResponse, "gameClientBridge"> & Pick<MarketplacePluginResponse, "gameClientBridge"> = {
@@ -124,7 +109,7 @@ describe("PlatformApiClient Game Client Bridge operator API", () => {
afterEach(() => vi.unstubAllGlobals());
it("types plugin and marketplace manifest declarations", () => {
expect(pluginBridgeProjection.gameClientBridge).toMatchObject({ commands: [{ type: "scum.diagnostic.ping" }], queryTemplates: [{ engine: "sqlite" }], companion: { tlsPolicy: "verify-system-roots", sessionMode: "component-session" } });
expect(pluginBridgeProjection.gameClientBridge).toMatchObject({ commands: [{ type: "scum.diagnostic.ping" }], queryTemplates: [{ engine: "sqlite" }] });
expect(JSON.stringify(pluginBridgeProjection)).not.toMatch(/authKey|componentKey|sessionToken|credential|secretRef/i);
});
+1 -194
View File
@@ -79,22 +79,6 @@ export interface GameClientBridgePageContractResponse {
export interface GameClientBridgeFeatureDeclarationResponse { key: string; title: string; permission: string; requiredHandlers?: string[]; requiredEventProducers?: string[]; }
export interface GameClientBridgeCompanionDeclarationResponse {
profileKey: string;
configTemplateKey: string;
configSchemaRef: string;
configFormat: "yaml";
platformBaseUrlSource: "run-control";
registrationProof: "hmac-sha256";
proofMaterialSource: "component-package";
proofMaterialEnv: string;
sessionMode: "component-session";
tlsPolicy: "verify-system-roots";
heartbeatIntervalSeconds: number;
commandPollIntervalSeconds: number;
requestTimeoutSeconds: number;
}
export interface GameClientBridgeManifestResponse {
commands: GameClientBridgeCommandDeclarationResponse[];
snapshots: GameClientBridgeSnapshotDeclarationResponse[];
@@ -104,7 +88,6 @@ export interface GameClientBridgeManifestResponse {
maxCommands: number;
pages?: GameClientBridgePageContractResponse[];
features?: GameClientBridgeFeatureDeclarationResponse[];
companion?: GameClientBridgeCompanionDeclarationResponse;
}
export interface GameClientBridgeProfileDeclarationResponse {
@@ -262,7 +245,6 @@ export interface RuntimeLifecycleProfileResponse {
capabilities: string[];
actionRefs?: Record<string, string>;
transportKeys?: string[];
clientManagerRef?: string;
dllExtensionRefs?: string[];
platforms?: string[];
}
@@ -318,23 +300,6 @@ export interface RuntimeTransportProfileResponse {
capabilities: string[];
}
export interface RuntimeClientManagerProfileResponse {
key: string;
displayName?: string;
version?: string;
revision?: string;
repository: { url: string; revisionPolicy: string; branch?: string; tag?: string; revision?: string };
supportedTargets: Array<{ os: string; arch: string }>;
build: { system: string; workspaceRef?: string; entryRef?: string };
configTemplates?: Array<{ key: string; templateRef: string; outputRef: string }>;
outputArtifacts: string[];
deployment?: { mode: string; executableRef: string; arguments: string[]; autoStart: boolean; requiredRunCapabilities: string[] };
lifecycle?: { actions: string[]; startupTimeoutSeconds: number; stopTimeoutSeconds: number };
health?: { mode: string; intervalSeconds: number; degradedAfterSeconds: number; offlineAfterSeconds: number; requiredCapabilities: string[] };
compatibility?: { minimumVersion?: string; maximumVersion?: string; allowDowngrade: boolean };
updatePolicy?: { strategy: string; requireApproval: boolean; healthConfirmationSeconds: number; retainPrevious: boolean };
}
export interface RuntimeDLLExtensionProfileResponse {
key: string;
displayName: string;
@@ -360,7 +325,6 @@ export interface GamePluginRuntimeProfilesResponse {
serverDeployments?: RuntimeServerDeploymentProfileResponse[];
logSources?: RuntimeLogSourceResponse[];
transportProfiles?: RuntimeTransportProfileResponse[];
clientManagers?: RuntimeClientManagerProfileResponse[];
dllExtensions?: RuntimeDLLExtensionProfileResponse[];
}
@@ -821,165 +785,8 @@ export interface RunUpdateJobListResponse {
count: number;
}
export interface ClientManagerBuildRequest {
profileKey: string;
targetOs: string;
targetArch: string;
repositoryUrl: string;
sourceRevision?: string;
idempotencyKey?: string;
}
export interface ClientManagerDistributionResponse {
id: string;
serverInstanceId: string;
pluginId: string;
profileKey: string;
targetOs: string;
targetArch: string;
repositoryUrl: string;
sourceRevision: string;
buildJobId: string;
artifactId: string;
checksum: string;
keyGeneration: number;
secretRef: string;
status: string;
createdAt: string;
updatedAt: string;
}
export interface ClientManagerDownloadRequest {
profileKey?: string;
}
export type ClientManagerLifecycleStatus =
| "requested"
| "building"
| "available"
| "deploying"
| "installed"
| "registering"
| "online"
| "degraded"
| "offline"
| "updating"
| "rolling_back"
| "stopping"
| "uninstalled"
| "failed";
export type ClientManagerLifecycleOperation = "deploy" | "start" | "stop" | "restart" | "status" | "update" | "rollback" | "uninstall";
export interface ClientManagerLifecycleActionResponse {
operation: ClientManagerLifecycleOperation;
available: boolean;
reason?: string;
}
export interface ClientManagerLifecycleJobResponse {
id: string;
state: JobState;
progress: JobProgressBody;
attempt: number;
createdAt: string;
updatedAt: string;
}
export interface ClientManagerDistributionSummaryResponse {
id: string;
artifactId: string;
sourceRevision: string;
targetOs: string;
targetArch: string;
checksum: string;
keyGeneration: number;
status: string;
}
export interface ClientManagerInstallationResponse {
id: string;
serverInstanceId: string;
pluginId: string;
profileKey: string;
targetOs: string;
targetArch: string;
status: ClientManagerLifecycleStatus;
phase: string;
desiredVersion?: string;
activeVersion?: string;
previousVersion?: string;
desiredRevision?: string;
activeRevision?: string;
previousRevision?: string;
desiredArtifactId?: string;
activeArtifactId?: string;
previousArtifactId?: string;
keyGeneration: number;
deploymentGeneration: number;
currentJobId?: string;
lastSuccessfulJobId?: string;
lastOperation?: ClientManagerLifecycleOperation;
health: "unknown" | "healthy" | "degraded" | "unhealthy" | "offline";
healthReason?: string;
lastSeenAt?: string;
retryable: boolean;
requiresRedeploy: boolean;
installedAt?: string;
uninstalledAt?: string;
updatedAt: string;
distribution?: ClientManagerDistributionSummaryResponse;
job?: ClientManagerLifecycleJobResponse;
actions: ClientManagerLifecycleActionResponse[];
}
export interface ClientManagerInstallationListResponse {
items: ClientManagerInstallationResponse[];
count: number;
}
export interface ClientManagerDeployRequest {
profileKey: string;
distributionId: string;
expectedDeploymentGeneration?: number;
idempotencyKey: string;
}
export interface ClientManagerControlRequest {
profileKey: string;
operation: "start" | "stop" | "restart" | "status" | "rollback";
expectedDeploymentGeneration: number;
idempotencyKey: string;
}
export interface ClientManagerUpdateRequest {
profileKey: string;
distributionId: string;
expectedDeploymentGeneration: number;
approved: boolean;
idempotencyKey: string;
}
export interface ClientManagerRetryRequest {
profileKey: string;
expectedDeploymentGeneration: number;
idempotencyKey: string;
}
export interface ClientManagerRevokeSessionRequest {
profileKey: string;
reason: string;
}
export interface ClientManagerUninstallRequest {
profileKey: string;
expectedDeploymentGeneration: number;
confirmed: boolean;
idempotencyKey: string;
}
export interface ComponentKeyResetRequest {
componentKind: "run" | "client-manager" | string;
componentKind: "run" | string;
componentKey?: string;
}