功能修改
This commit is contained in:
@@ -14,8 +14,10 @@ Plugins use the platform bridge for every privileged action.
|
||||
- `dependencies.request`: request typed dependency checks or approved install plans declared by the plugin runtime profile.
|
||||
- `logs.backfill.request`: request historical log backfill for a declared log source.
|
||||
- `client-manager.request`: request generation/download/key reset or a typed status, deploy, start, stop, restart, update, rollback, session-revoke, retry, or uninstall operation for a plugin-declared companion client manager.
|
||||
- `plugin-lifecycle.request`: request a server-bound install, enable, disable, upgrade, rollback, retire, or dependency-check operation through Platform capacity and compatibility gates.
|
||||
- `ai.invoke`: request platform-mediated AI assistance.
|
||||
- `theme.tokens`: read safe platform theme tokens.
|
||||
- `game-client-bridge`: query declared companion health and snapshots, and queue or cancel only manifest-declared typed commands through Platform.
|
||||
|
||||
## Execution Envelopes
|
||||
|
||||
@@ -23,16 +25,22 @@ Plugin pages build execution requests with `createBridgeExecutionRequest` and ha
|
||||
|
||||
Execution responses use `requestId`, plugin/page/server scope, action, status, optional result refs, and optional safe errors. Use `parseBridgeExecutionResponse` before reading results so plugin code handles denied, deferred, and failed states uniformly.
|
||||
|
||||
AI requests use `createAIInvocationRequest` with an explicit purpose, prompt, scoped context refs, and optional current config. Use `parseAIInvocationResponse` to consume recommendations and safe errors. Plugin code must not include provider API keys, provider base URLs, bearer tokens, or direct transport details in AI request payloads.
|
||||
AI requests use `createAIInvocationRequest` with an explicit manifest-declared purpose, prompt, and scoped context refs. Use `parseAIInvocationResponse` to consume recommendations, reviewable `diffId` metadata, and safe errors. Plugin code must not choose or receive provider API keys, provider base URLs, bearer tokens, or direct transport details. Config writes require a separate Platform operator approval.
|
||||
|
||||
Artifact open requests use `createArtifactOpenRequest` with an artifact ID that belongs to the current server/job scope. Use `parseArtifactReference` to consume the bridge result. Parsed references contain platform-owned download URLs, filename, content type, size, checksum, expiry, range support, and chunk size; they do not contain bytes or raw storage adapter locations.
|
||||
|
||||
Remote access requests use `createRemoteAccessRequest` with a plugin-declared `remote.*` capability, logical target key, optional scoped `input://` or `artifact://` ref, and idempotency key. The SDK never accepts FTP passwords, rsync endpoints, database DSNs, RCON passwords, run sockets, or raw host paths in these envelopes.
|
||||
|
||||
SQLite reads use manifest-declared `gameClientBridge.queryTemplates`. Plugin pages send only a declared template key plus typed inputs; Platform verifies the page contract, permission, SQLite transport/target, timeout, and row limit before dispatch. Query declarations and browser envelopes never contain SQL text, DSNs, credentials, sockets, or host paths.
|
||||
|
||||
Run distribution, dependency, log backfill, and client-manager requests use `createRunDistributionRequest`, `createDependencyActionRequest`, `createLogBackfillRequest`, and `createClientManagerRequest`. Client-manager lifecycle envelopes carry only operation names, logical profile/installation IDs, target OS/architecture, artifact IDs, expected deployment generations, and idempotency keys. `parseClientManagerLifecycleStatus` whitelists safe state, version, health, job, artifact, and action fields. Raw run/client-manager keys, component sessions, secret refs, host paths, PIDs, sockets, credentials, and direct Run endpoint details are never plugin bridge fields.
|
||||
|
||||
Client-manager lifecycle requests remain Platform-mediated. A plugin declaration does not grant access by itself: Platform rechecks the installed plugin, server owner/administrator scope, runtime binding, assigned Run endpoint capabilities, current distribution target/revision/key generation, and durable installation state before dispatching a typed job.
|
||||
|
||||
Game-client plugin pages receive a host-provided `GameClientBridgePageClient`. The SDK defines status, command, result, snapshot, approval, and manifest declaration types but never creates its own HTTP client. Queue requests carry only a declared command type, logical profile key, bounded typed payload, expiry, priority, and idempotency key. Browser-facing types intentionally have no component session, component key, installation fence, host path, DSN, Run endpoint, socket, or storage credential fields.
|
||||
|
||||
Production plugin lifecycle requests use `createProductionPluginLifecycleRequest`. Envelopes contain only plugin/server scope, enumerated operation, optional target version, confirmation, and idempotency key. Platform rechecks the manifest `productionLifecycle` declaration, dependency policy, disruptive approval, endpoint capacity, compatibility, and prior idempotency inputs before dispatch.
|
||||
|
||||
## Forbidden Data
|
||||
|
||||
The bridge must not expose:
|
||||
@@ -45,3 +53,6 @@ The bridge must not expose:
|
||||
- unrestricted artifact storage credentials.
|
||||
- direct storage URLs or presigned backend URLs.
|
||||
- FTP, rsync, database, or RCON credentials.
|
||||
# Client Manager lifecycle bridge
|
||||
|
||||
The bridge may request typed `deploy`, `start`, `stop`, `restart`, `status`, `update`, `rollback`, `revoke`, `retry`, or `uninstall` intents when Platform action gating says they are available. Results are safe logical projections with real job phase/progress and redacted recovery guidance. The bridge is not a transport for Run sessions, component keys, artifact bytes, machine paths, process IDs, sockets, or credentials; component registration and heartbeat remain component-to-Platform contracts outside the plugin page.
|
||||
|
||||
+238
-8
@@ -11,6 +11,9 @@ export type PluginPermission =
|
||||
| "server.run.distribution"
|
||||
| "server.dependencies.manage"
|
||||
| "server.client-manager.manage"
|
||||
| "server.game-client.read"
|
||||
| "server.game-client.command"
|
||||
| "server.game-client.maintenance"
|
||||
| "ai.invoke";
|
||||
|
||||
export type RunCapability =
|
||||
@@ -58,6 +61,7 @@ export type PluginBridgeAction =
|
||||
| "dependencies.request"
|
||||
| "logs.backfill.request"
|
||||
| "client-manager.request"
|
||||
| "plugin-lifecycle.request"
|
||||
| "ai.invoke";
|
||||
|
||||
export type PluginBridgeRequestPayload = Record<string, unknown>;
|
||||
@@ -115,7 +119,7 @@ export interface PluginAIInvocationResponse {
|
||||
purpose: AIPurpose;
|
||||
status: "ok" | "denied" | "error" | string;
|
||||
recommendation?: string;
|
||||
suggestedConfig?: string;
|
||||
configRecommendation?: { diffId: string; key: string; suggestedConfig: string; diffSummary: string; expiresAt: string };
|
||||
usage?: { model?: string; mocked?: boolean; inputTokens?: number; outputTokens?: number };
|
||||
error?: PluginBridgeError;
|
||||
}
|
||||
@@ -131,6 +135,15 @@ export type PluginLifecycleDispatchPayload = Record<string, string> & {
|
||||
idempotencyKey: string;
|
||||
};
|
||||
|
||||
export type ProductionPluginLifecycleOperation = "install" | "enable" | "disable" | "upgrade" | "rollback" | "retire" | "dependency-check";
|
||||
|
||||
export type PluginProductionLifecyclePayload = Record<string, string> & {
|
||||
operation: ProductionPluginLifecycleOperation;
|
||||
targetVersion: string;
|
||||
idempotencyKey: string;
|
||||
confirmed: "true" | "false";
|
||||
};
|
||||
|
||||
export type PluginRemoteAccessPayload = Record<string, string> & {
|
||||
capability: Extract<RunCapability, `remote.${string}`>;
|
||||
targetKey?: string;
|
||||
@@ -196,6 +209,165 @@ export interface GamePluginRemoteAccess {
|
||||
logTransfer?: boolean;
|
||||
}
|
||||
|
||||
export type GameClientBridgeApprovalLevel = "none" | "operator" | "platform-admin";
|
||||
export type GameClientBridgeApprovalState = "not_required" | "pending" | "approved" | "rejected";
|
||||
export type GameClientBridgeCommandState = "pending" | "claimed" | "succeeded" | "failed" | "cancelled" | "expired";
|
||||
|
||||
export interface GameClientBridgeCommandDeclaration {
|
||||
type: string;
|
||||
title: string;
|
||||
permission: PluginPermission;
|
||||
approvalLevel: GameClientBridgeApprovalLevel;
|
||||
payloadSchemaRef: string;
|
||||
resultSchemaRef?: string;
|
||||
timeoutSeconds: number;
|
||||
maxPayloadBytes: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeSnapshotDeclaration {
|
||||
type: string;
|
||||
schemaVersion: string;
|
||||
schemaRef: string;
|
||||
keepForSeconds: number;
|
||||
maxRecords: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeQueryTemplateDeclaration {
|
||||
key: string;
|
||||
title: string;
|
||||
permission: PluginPermission;
|
||||
engine: "sqlite";
|
||||
transportKey: string;
|
||||
targetKey: string;
|
||||
parameterSchemaRef: string;
|
||||
resultSchemaRef: string;
|
||||
maxRows: number;
|
||||
timeoutSeconds: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgePageContract {
|
||||
pageKey: string;
|
||||
commandTypes?: string[];
|
||||
snapshotTypes?: string[];
|
||||
queryTemplateKeys?: string[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCompanionDeclaration {
|
||||
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 GameClientBridgeManifest {
|
||||
commands: GameClientBridgeCommandDeclaration[];
|
||||
snapshots: GameClientBridgeSnapshotDeclaration[];
|
||||
queryTemplates?: GameClientBridgeQueryTemplateDeclaration[];
|
||||
commandRetentionSeconds: number;
|
||||
maxCommands: number;
|
||||
pages?: GameClientBridgePageContract[];
|
||||
companion?: GameClientBridgeCompanionDeclaration;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeProfileStatus {
|
||||
pluginId: string;
|
||||
profileKey: string;
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
commandTypes: string[];
|
||||
snapshotTypes: string[];
|
||||
queryTemplateKeys: string[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeStatus {
|
||||
serverInstanceId: string;
|
||||
pluginId: string;
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
profiles: GameClientBridgeProfileStatus[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCommandResult {
|
||||
status: "succeeded" | "failed" | "cancelled";
|
||||
summary?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
completedAt: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCommand {
|
||||
id: string;
|
||||
serverInstanceId: string;
|
||||
pluginId: string;
|
||||
profileKey: string;
|
||||
commandType: string;
|
||||
priority: number;
|
||||
state: GameClientBridgeCommandState;
|
||||
approvalState: GameClientBridgeApprovalState;
|
||||
result?: GameClientBridgeCommandResult;
|
||||
auditReferences?: string[];
|
||||
expiresAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeSnapshot<TPayload extends Record<string, unknown> = Record<string, unknown>> {
|
||||
id: string;
|
||||
serverInstanceId: string;
|
||||
pluginId: string;
|
||||
profileKey: string;
|
||||
type: string;
|
||||
schemaVersion: string;
|
||||
streamKey: string;
|
||||
sequence: number;
|
||||
observedAt: string;
|
||||
payload: TPayload;
|
||||
auditReferences?: string[];
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeQueueRequest<TPayload extends Record<string, unknown> = Record<string, unknown>> {
|
||||
profileKey: string;
|
||||
commandType: string;
|
||||
payload: TPayload;
|
||||
idempotencyKey: string;
|
||||
priority?: number;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeSnapshotQuery {
|
||||
profileKey?: string;
|
||||
type?: string;
|
||||
streamKey?: string;
|
||||
observedAfter?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgePageClient {
|
||||
getStatus(serverInstanceId: string): Promise<GameClientBridgeStatus>;
|
||||
listCommands(serverInstanceId: string): Promise<GameClientBridgeCommand[]>;
|
||||
queueCommand<TPayload extends Record<string, unknown>>(serverInstanceId: string, request: GameClientBridgeQueueRequest<TPayload>): Promise<GameClientBridgeCommand>;
|
||||
cancelCommand(serverInstanceId: string, commandId: string, reason?: string): Promise<GameClientBridgeCommand>;
|
||||
querySnapshots<TPayload extends Record<string, unknown> = Record<string, unknown>>(serverInstanceId: string, query?: GameClientBridgeSnapshotQuery): Promise<Array<GameClientBridgeSnapshot<TPayload>>>;
|
||||
}
|
||||
|
||||
export function createGameClientBridgeQueueRequest<TPayload extends Record<string, unknown>>(input: GameClientBridgeQueueRequest<TPayload>): GameClientBridgeQueueRequest<TPayload> {
|
||||
if (!input.profileKey || !input.commandType || !input.idempotencyKey || !input.expiresAt) {
|
||||
throw new Error("profileKey, commandType, idempotencyKey, and expiresAt are required");
|
||||
}
|
||||
return { profileKey: input.profileKey, commandType: input.commandType, payload: { ...input.payload }, idempotencyKey: input.idempotencyKey, priority: input.priority, expiresAt: input.expiresAt };
|
||||
}
|
||||
|
||||
export type RuntimePlatform = "windows" | "linux" | "darwin";
|
||||
export type RuntimeArch = "amd64" | "arm64";
|
||||
export type RuntimeTarget = { os: RuntimePlatform; arch: RuntimeArch };
|
||||
@@ -254,6 +426,19 @@ export interface RuntimeLogSource {
|
||||
retentionDays?: number;
|
||||
}
|
||||
|
||||
export type RuntimeLogEventSeverity = "info" | "notice" | "warning" | "critical";
|
||||
|
||||
export interface RuntimeLogEventDeclaration {
|
||||
key: string;
|
||||
title: string;
|
||||
sourceKey: string;
|
||||
eventType: string;
|
||||
permission: PluginPermission;
|
||||
schemaRef: string;
|
||||
retentionDays: number;
|
||||
severity: RuntimeLogEventSeverity;
|
||||
}
|
||||
|
||||
export interface RuntimeTransportProfile {
|
||||
key: string;
|
||||
kind: "file" | "ftp" | "rsync" | "mysql" | "sqlite" | "rcon";
|
||||
@@ -318,6 +503,7 @@ export interface GamePluginRuntimeProfiles {
|
||||
dependencyProbes?: RuntimeDependencyProbe[];
|
||||
installPlans?: RuntimeInstallPlan[];
|
||||
logSources?: RuntimeLogSource[];
|
||||
logEvents?: RuntimeLogEventDeclaration[];
|
||||
transportProfiles?: RuntimeTransportProfile[];
|
||||
clientManagers?: RuntimeClientManagerProfile[];
|
||||
}
|
||||
@@ -380,6 +566,7 @@ export const pluginBridgeActionPolicies: Record<PluginBridgeAction, PluginBridge
|
||||
"dependencies.request": { permissions: ["server.dependencies.manage"] },
|
||||
"logs.backfill.request": { permissions: ["server.logs.read"] },
|
||||
"client-manager.request": { permissions: ["server.client-manager.manage"] },
|
||||
"plugin-lifecycle.request": { permissions: ["server.lifecycle"] },
|
||||
"ai.invoke": { permissions: ["ai.invoke"], aiPurposeRequired: true }
|
||||
};
|
||||
|
||||
@@ -432,10 +619,18 @@ export interface GamePluginManifest {
|
||||
permissions: PluginPermission[];
|
||||
remoteAccess?: GamePluginRemoteAccess;
|
||||
runtimeProfiles?: GamePluginRuntimeProfiles;
|
||||
gameClientBridge?: GameClientBridgeManifest;
|
||||
actions?: GamePluginActions;
|
||||
productionLifecycle: {
|
||||
operations: ProductionPluginLifecycleOperation[];
|
||||
dependencyPolicy: "required" | "optional";
|
||||
approvalRequired: Array<"disable" | "rollback" | "retire">;
|
||||
};
|
||||
pages?: GamePluginPage[];
|
||||
ai?: {
|
||||
purposes?: AIPurpose[];
|
||||
mediation: "platform";
|
||||
configWritePolicy: "review-required";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -533,24 +728,59 @@ export function createLifecycleDispatchRequest(input: {
|
||||
});
|
||||
}
|
||||
|
||||
export function createProductionPluginLifecycleRequest(input: {
|
||||
requestId: string;
|
||||
context: PluginBridgeContext;
|
||||
operation: ProductionPluginLifecycleOperation;
|
||||
targetVersion?: string;
|
||||
idempotencyKey: string;
|
||||
confirmed?: boolean;
|
||||
}): PluginBridgeExecutionRequest<PluginProductionLifecyclePayload> {
|
||||
if (!input.context.serverInstanceId) {
|
||||
throw new Error("serverInstanceId is required for plugin lifecycle requests");
|
||||
}
|
||||
if (["disable", "rollback", "retire"].includes(input.operation) && !input.confirmed) {
|
||||
throw new Error("disruptive plugin lifecycle requests require confirmation");
|
||||
}
|
||||
return createBridgeExecutionRequest({
|
||||
requestId: input.requestId,
|
||||
context: input.context,
|
||||
action: "plugin-lifecycle.request",
|
||||
payload: {
|
||||
operation: input.operation,
|
||||
targetVersion: input.targetVersion ?? "",
|
||||
idempotencyKey: input.idempotencyKey,
|
||||
confirmed: input.confirmed ? "true" : "false"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function createRemoteAccessRequest(input: {
|
||||
requestId: string;
|
||||
context: PluginBridgeContext;
|
||||
capability: PluginRemoteAccessPayload["capability"];
|
||||
targetKey?: string;
|
||||
inputRef?: string;
|
||||
inputs?: Record<string, string>;
|
||||
idempotencyKey: string;
|
||||
}): PluginBridgeExecutionRequest<PluginRemoteAccessPayload> {
|
||||
const payload: PluginRemoteAccessPayload = {
|
||||
capability: input.capability,
|
||||
targetKey: input.targetKey ?? "",
|
||||
inputRef: input.inputRef ?? "",
|
||||
idempotencyKey: input.idempotencyKey
|
||||
};
|
||||
for (const [key, value] of Object.entries(input.inputs ?? {})) {
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/.test(key) || /(sql|query|shell|script|password|secret|token|credential|dsn|path)/i.test(key)) {
|
||||
throw new Error(`remote adapter input key is unsafe: ${key}`);
|
||||
}
|
||||
payload[`input.${key}`] = value;
|
||||
}
|
||||
return createBridgeExecutionRequest({
|
||||
requestId: input.requestId,
|
||||
context: input.context,
|
||||
action: "remote.access.request",
|
||||
payload: {
|
||||
capability: input.capability,
|
||||
targetKey: input.targetKey ?? "",
|
||||
inputRef: input.inputRef ?? "",
|
||||
idempotencyKey: input.idempotencyKey
|
||||
}
|
||||
payload
|
||||
});
|
||||
}
|
||||
|
||||
@@ -781,7 +1011,7 @@ export function parseAIInvocationResponse(response: PluginAIInvocationResponse):
|
||||
purpose: response.purpose,
|
||||
status: response.status,
|
||||
recommendation: response.recommendation,
|
||||
suggestedConfig: response.suggestedConfig,
|
||||
configRecommendation: response.configRecommendation ? { ...response.configRecommendation } : undefined,
|
||||
usage: response.usage ? { ...response.usage } : undefined,
|
||||
error: response.error ? bridgeError(response.error.code, response.error.message, response.error.details ?? []) : undefined
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user