Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
74
packages/gateway-protocol/src/channels.schema.test.ts
Normal file
74
packages/gateway-protocol/src/channels.schema.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// Gateway Protocol tests cover channels.schema behavior.
|
||||
import { Compile } from "typebox/compile";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ChannelsStatusResultSchema, WebLoginWaitParamsSchema } from "./schema/channels.js";
|
||||
|
||||
/**
|
||||
* Channel schema regressions for browser login and status diagnostics.
|
||||
* These payloads are consumed by dashboard/operator UI, so QR payload bounds
|
||||
* and event-loop diagnostic shape are part of the public gateway contract.
|
||||
*/
|
||||
|
||||
describe("WebLoginWaitParamsSchema", () => {
|
||||
/** Compiled validator reused across QR bounds cases. */
|
||||
const validate = Compile(WebLoginWaitParamsSchema);
|
||||
|
||||
it("bounds caller-provided QR data URLs", () => {
|
||||
expect(
|
||||
validate.Check({
|
||||
currentQrDataUrl: "data:image/png;base64,qr",
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
validate.Check({
|
||||
currentQrDataUrl: "x".repeat(16_385),
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
validate.Check({
|
||||
currentQrDataUrl: "https://example.com/qr.png",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChannelsStatusResultSchema", () => {
|
||||
/** Compiled status validator for channel docking diagnostics. */
|
||||
const validate = Compile(ChannelsStatusResultSchema);
|
||||
|
||||
it("accepts gateway event-loop diagnostics emitted by channels.status", () => {
|
||||
expect(
|
||||
validate.Check({
|
||||
ts: Date.now(),
|
||||
channelOrder: ["discord"],
|
||||
channelLabels: { discord: "Discord" },
|
||||
channels: { discord: { configured: true } },
|
||||
channelAccounts: {
|
||||
discord: [
|
||||
{
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
running: true,
|
||||
connected: false,
|
||||
healthState: "stale-socket",
|
||||
},
|
||||
],
|
||||
},
|
||||
channelDefaultAccountId: { discord: "default" },
|
||||
partial: true,
|
||||
warnings: ["discord:default probe timed out after 1000ms"],
|
||||
eventLoop: {
|
||||
degraded: true,
|
||||
reasons: ["event_loop_delay", "cpu"],
|
||||
intervalMs: 62_000,
|
||||
delayP99Ms: 1_250.5,
|
||||
delayMaxMs: 62_000,
|
||||
utilization: 0.98,
|
||||
cpuCoreRatio: 1.2,
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
66
packages/gateway-protocol/src/clawhub-trust-error-details.ts
Normal file
66
packages/gateway-protocol/src/clawhub-trust-error-details.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/** Structured ClawHub trust details carried in gateway error payloads. */
|
||||
export const ClawHubTrustErrorCodes = {
|
||||
SECURITY_UNAVAILABLE: "clawhub_security_unavailable",
|
||||
RISK_ACKNOWLEDGEMENT_REQUIRED: "clawhub_risk_acknowledgement_required",
|
||||
DOWNLOAD_BLOCKED: "clawhub_download_blocked",
|
||||
} as const;
|
||||
|
||||
export type ClawHubTrustErrorCode =
|
||||
(typeof ClawHubTrustErrorCodes)[keyof typeof ClawHubTrustErrorCodes];
|
||||
|
||||
export type ClawHubTrustErrorDetails = {
|
||||
clawhubTrustCode?: ClawHubTrustErrorCode;
|
||||
version?: string;
|
||||
warning?: string;
|
||||
};
|
||||
|
||||
function normalizeNonEmptyString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
export function isClawHubTrustErrorCode(value: unknown): value is ClawHubTrustErrorCode {
|
||||
return (
|
||||
value === ClawHubTrustErrorCodes.SECURITY_UNAVAILABLE ||
|
||||
value === ClawHubTrustErrorCodes.RISK_ACKNOWLEDGEMENT_REQUIRED ||
|
||||
value === ClawHubTrustErrorCodes.DOWNLOAD_BLOCKED
|
||||
);
|
||||
}
|
||||
|
||||
export function buildClawHubTrustErrorDetails(params: {
|
||||
code?: ClawHubTrustErrorCode;
|
||||
version?: string;
|
||||
warning?: string;
|
||||
}): ClawHubTrustErrorDetails | undefined {
|
||||
if (!params.code && !params.version && !params.warning) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(params.code ? { clawhubTrustCode: params.code } : {}),
|
||||
...(params.version ? { version: params.version } : {}),
|
||||
...(params.warning ? { warning: params.warning } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function readClawHubTrustErrorDetails(
|
||||
details: unknown,
|
||||
): ClawHubTrustErrorDetails | undefined {
|
||||
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
||||
return undefined;
|
||||
}
|
||||
const raw = details as {
|
||||
clawhubTrustCode?: unknown;
|
||||
version?: unknown;
|
||||
warning?: unknown;
|
||||
};
|
||||
const code = isClawHubTrustErrorCode(raw.clawhubTrustCode) ? raw.clawhubTrustCode : undefined;
|
||||
const version = normalizeNonEmptyString(raw.version);
|
||||
const warning = normalizeNonEmptyString(raw.warning);
|
||||
if (!code && !version && !warning) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(code ? { clawhubTrustCode: code } : {}),
|
||||
...(version ? { version } : {}),
|
||||
...(warning ? { warning } : {}),
|
||||
};
|
||||
}
|
||||
123
packages/gateway-protocol/src/client-info.ts
Normal file
123
packages/gateway-protocol/src/client-info.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Shared gateway client identity contract.
|
||||
*
|
||||
* These values cross the WebSocket handshake boundary, so additions must stay
|
||||
* aligned with protocol schemas and server policy checks.
|
||||
*/
|
||||
function normalizeOptionalLowercaseString(raw?: string | null): string | undefined {
|
||||
if (typeof raw !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
/** Canonical client ids accepted in gateway hello/connect payloads. */
|
||||
export const GATEWAY_CLIENT_IDS = {
|
||||
WEBCHAT_UI: "webchat-ui",
|
||||
CONTROL_UI: "openclaw-control-ui",
|
||||
TUI: "openclaw-tui",
|
||||
WEBCHAT: "webchat",
|
||||
CLI: "cli",
|
||||
GATEWAY_CLIENT: "gateway-client",
|
||||
MACOS_APP: "openclaw-macos",
|
||||
IOS_APP: "openclaw-ios",
|
||||
ANDROID_APP: "openclaw-android",
|
||||
NODE_HOST: "node-host",
|
||||
TEST: "test",
|
||||
FINGERPRINT: "fingerprint",
|
||||
PROBE: "openclaw-probe",
|
||||
} as const;
|
||||
|
||||
/** Stable gateway client ids used on the wire during hello/connect handshakes. */
|
||||
export type GatewayClientId = (typeof GATEWAY_CLIENT_IDS)[keyof typeof GATEWAY_CLIENT_IDS];
|
||||
|
||||
// Back-compat naming (internal): these values are IDs, not display names.
|
||||
export const GATEWAY_CLIENT_NAMES = GATEWAY_CLIENT_IDS;
|
||||
/** Compatibility alias for internal callers that still use "name" terminology. */
|
||||
export type GatewayClientName = GatewayClientId;
|
||||
|
||||
/** Coarse modes let policy group clients without matching every product id. */
|
||||
export const GATEWAY_CLIENT_MODES = {
|
||||
WEBCHAT: "webchat",
|
||||
CLI: "cli",
|
||||
UI: "ui",
|
||||
BACKEND: "backend",
|
||||
NODE: "node",
|
||||
PROBE: "probe",
|
||||
TEST: "test",
|
||||
} as const;
|
||||
|
||||
/** Coarse client category used for gateway policy and diagnostics. */
|
||||
export type GatewayClientMode = (typeof GATEWAY_CLIENT_MODES)[keyof typeof GATEWAY_CLIENT_MODES];
|
||||
|
||||
/** Client metadata sent during gateway connection setup. */
|
||||
export type GatewayClientInfo = {
|
||||
/** Stable product/client identifier from `GATEWAY_CLIENT_IDS`. */
|
||||
id: GatewayClientId;
|
||||
/** Human-readable label for diagnostics; not used for policy decisions. */
|
||||
displayName?: string;
|
||||
/** Client app or package version reported by the connecting process. */
|
||||
version: string;
|
||||
/** Runtime platform string, such as `darwin`, `ios`, `android`, or `web`. */
|
||||
platform: string;
|
||||
/** Optional device family used by native clients for display and routing hints. */
|
||||
deviceFamily?: string;
|
||||
/** Native hardware/model identifier when available. */
|
||||
modelIdentifier?: string;
|
||||
/** Coarse category from `GATEWAY_CLIENT_MODES` for policy and diagnostics. */
|
||||
mode: GatewayClientMode;
|
||||
/** Per-installation or per-process id used to distinguish same-product clients. */
|
||||
instanceId?: string;
|
||||
};
|
||||
|
||||
/** Capability flags a client may advertise during the gateway handshake. */
|
||||
export const GATEWAY_CLIENT_CAPS = {
|
||||
TOOL_EVENTS: "tool-events",
|
||||
} as const;
|
||||
|
||||
/** Optional capability advertised by clients during gateway handshake. */
|
||||
export type GatewayClientCap = (typeof GATEWAY_CLIENT_CAPS)[keyof typeof GATEWAY_CLIENT_CAPS];
|
||||
|
||||
const GATEWAY_CLIENT_ID_SET = new Set<GatewayClientId>(Object.values(GATEWAY_CLIENT_IDS));
|
||||
const GATEWAY_CLIENT_MODE_SET = new Set<GatewayClientMode>(Object.values(GATEWAY_CLIENT_MODES));
|
||||
|
||||
/** Normalizes untrusted client ids and rejects unknown values. */
|
||||
export function normalizeGatewayClientId(raw?: string | null): GatewayClientId | undefined {
|
||||
// Handshake input is intentionally case-insensitive, but policy decisions use
|
||||
// the canonical lowercase ids from the closed registry above.
|
||||
const normalized = normalizeOptionalLowercaseString(raw);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return GATEWAY_CLIENT_ID_SET.has(normalized as GatewayClientId)
|
||||
? (normalized as GatewayClientId)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Normalizes legacy client-name fields through the canonical client-id registry. */
|
||||
export function normalizeGatewayClientName(raw?: string | null): GatewayClientName | undefined {
|
||||
return normalizeGatewayClientId(raw);
|
||||
}
|
||||
|
||||
/** Normalizes untrusted client modes and rejects unknown values. */
|
||||
export function normalizeGatewayClientMode(raw?: string | null): GatewayClientMode | undefined {
|
||||
const normalized = normalizeOptionalLowercaseString(raw);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return GATEWAY_CLIENT_MODE_SET.has(normalized as GatewayClientMode)
|
||||
? (normalized as GatewayClientMode)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Checks a client-advertised capability list without treating missing caps as errors. */
|
||||
export function hasGatewayClientCap(
|
||||
caps: string[] | null | undefined,
|
||||
cap: GatewayClientCap,
|
||||
): boolean {
|
||||
if (!Array.isArray(caps)) {
|
||||
return false;
|
||||
}
|
||||
return caps.includes(cap);
|
||||
}
|
||||
195
packages/gateway-protocol/src/connect-error-details.test.ts
Normal file
195
packages/gateway-protocol/src/connect-error-details.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
// Gateway Protocol tests cover connect error details behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildPairingConnectCloseReason,
|
||||
buildPairingConnectErrorDetails,
|
||||
buildPairingConnectErrorMessage,
|
||||
ConnectPairingRequiredReasons,
|
||||
describePairingConnectRequirement,
|
||||
formatConnectErrorMessage,
|
||||
formatConnectPairingRequiredMessage,
|
||||
normalizePairingConnectRequestId,
|
||||
readConnectErrorDetailCode,
|
||||
readConnectErrorRecoveryAdvice,
|
||||
readConnectPairingRequiredMessage,
|
||||
readPairingConnectErrorDetails,
|
||||
resolveAuthConnectErrorDetailCode,
|
||||
} from "./connect-error-details.js";
|
||||
|
||||
/**
|
||||
* Connect error detail regressions for Gateway/WebSocket clients.
|
||||
*
|
||||
* These tests pin structured auth/pairing details, human-readable fallback
|
||||
* formatting, and request-id sanitization because these strings surface in
|
||||
* control UI reconnect flows and device pairing diagnostics.
|
||||
*/
|
||||
|
||||
describe("readConnectErrorDetailCode", () => {
|
||||
it("reads structured detail codes", () => {
|
||||
expect(readConnectErrorDetailCode({ code: "AUTH_TOKEN_MISMATCH" })).toBe("AUTH_TOKEN_MISMATCH");
|
||||
});
|
||||
|
||||
it("returns null for invalid detail payloads", () => {
|
||||
expect(readConnectErrorDetailCode(null)).toBeNull();
|
||||
expect(readConnectErrorDetailCode("AUTH_TOKEN_MISMATCH")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("readConnectErrorRecoveryAdvice", () => {
|
||||
it("reads retry advice fields when present", () => {
|
||||
expect(
|
||||
readConnectErrorRecoveryAdvice({
|
||||
canRetryWithDeviceToken: true,
|
||||
recommendedNextStep: "retry_with_device_token",
|
||||
}),
|
||||
).toEqual({
|
||||
canRetryWithDeviceToken: true,
|
||||
recommendedNextStep: "retry_with_device_token",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns empty advice for invalid payloads", () => {
|
||||
expect(readConnectErrorRecoveryAdvice(null)).toStrictEqual({});
|
||||
expect(readConnectErrorRecoveryAdvice("x")).toStrictEqual({});
|
||||
expect(readConnectErrorRecoveryAdvice({ canRetryWithDeviceToken: "yes" })).toEqual({});
|
||||
expect(
|
||||
readConnectErrorRecoveryAdvice({
|
||||
canRetryWithDeviceToken: true,
|
||||
recommendedNextStep: "retry_with_magic",
|
||||
}),
|
||||
).toEqual({ canRetryWithDeviceToken: true, recommendedNextStep: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveAuthConnectErrorDetailCode", () => {
|
||||
it("maps device token scope mismatches to a dedicated auth detail", () => {
|
||||
expect(resolveAuthConnectErrorDetailCode("scope_mismatch")).toBe("AUTH_SCOPE_MISMATCH");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pairing connect details", () => {
|
||||
it("builds reason-specific pairing messages", () => {
|
||||
expect(buildPairingConnectErrorMessage(ConnectPairingRequiredReasons.SCOPE_UPGRADE)).toBe(
|
||||
"pairing required: device is asking for more scopes than currently approved",
|
||||
);
|
||||
expect(describePairingConnectRequirement(ConnectPairingRequiredReasons.NOT_PAIRED)).toBe(
|
||||
"device is not approved yet",
|
||||
);
|
||||
});
|
||||
|
||||
it("builds structured pairing details with remediation", () => {
|
||||
expect(
|
||||
buildPairingConnectErrorDetails({
|
||||
reason: ConnectPairingRequiredReasons.NOT_PAIRED,
|
||||
requestId: "req-123",
|
||||
recommendedNextStep: "wait_then_retry",
|
||||
retryable: true,
|
||||
pauseReconnect: false,
|
||||
}),
|
||||
).toEqual({
|
||||
code: "PAIRING_REQUIRED",
|
||||
reason: "not-paired",
|
||||
requestId: "req-123",
|
||||
remediationHint: "Approve this device from the pending pairing requests.",
|
||||
recommendedNextStep: "wait_then_retry",
|
||||
retryable: true,
|
||||
pauseReconnect: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("reads pairing details and backfills missing remediation hints", () => {
|
||||
expect(
|
||||
readPairingConnectErrorDetails({
|
||||
code: "PAIRING_REQUIRED",
|
||||
reason: "scope-upgrade",
|
||||
requestId: "req-456",
|
||||
}),
|
||||
).toEqual({
|
||||
code: "PAIRING_REQUIRED",
|
||||
reason: "scope-upgrade",
|
||||
requestId: "req-456",
|
||||
remediationHint: "Review the requested scopes, then approve the pending upgrade.",
|
||||
});
|
||||
});
|
||||
|
||||
it("includes request ids in close reasons when available", () => {
|
||||
expect(
|
||||
buildPairingConnectCloseReason({
|
||||
reason: ConnectPairingRequiredReasons.ROLE_UPGRADE,
|
||||
requestId: "req-789",
|
||||
}),
|
||||
).toBe(
|
||||
"pairing required: device is asking for a higher role than currently approved (requestId: req-789)",
|
||||
);
|
||||
});
|
||||
|
||||
it("drops request ids that do not match the allowlist", () => {
|
||||
expect(normalizePairingConnectRequestId("req-123")).toBe("req-123");
|
||||
expect(normalizePairingConnectRequestId("req-123;rm -rf /")).toBeUndefined();
|
||||
expect(
|
||||
readPairingConnectErrorDetails({
|
||||
code: "PAIRING_REQUIRED",
|
||||
reason: "scope-upgrade",
|
||||
requestId: "req-123;rm -rf /",
|
||||
}),
|
||||
).toEqual({
|
||||
code: "PAIRING_REQUIRED",
|
||||
reason: "scope-upgrade",
|
||||
remediationHint: "Review the requested scopes, then approve the pending upgrade.",
|
||||
});
|
||||
});
|
||||
|
||||
it("formats upgrade rejections with the request id", () => {
|
||||
expect(
|
||||
formatConnectPairingRequiredMessage({
|
||||
code: "PAIRING_REQUIRED",
|
||||
requestId: "req-123",
|
||||
reason: "scope-upgrade",
|
||||
}),
|
||||
).toBe("scope upgrade pending approval (requestId: req-123)");
|
||||
});
|
||||
|
||||
it("parses surfaced pairing-required messages", () => {
|
||||
expect(
|
||||
readConnectPairingRequiredMessage("scope upgrade pending approval (requestId: req-123)"),
|
||||
).toEqual({
|
||||
requestId: "req-123",
|
||||
reason: "scope-upgrade",
|
||||
});
|
||||
expect(
|
||||
readConnectPairingRequiredMessage(
|
||||
"scope upgrade pending approval (requestId: req-123;rm -rf /)",
|
||||
),
|
||||
).toEqual({
|
||||
reason: "scope-upgrade",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers pairing detail formatting over the generic message", () => {
|
||||
expect(
|
||||
formatConnectErrorMessage({
|
||||
message: "pairing required",
|
||||
details: {
|
||||
code: "PAIRING_REQUIRED",
|
||||
requestId: "req-123",
|
||||
reason: "scope-upgrade",
|
||||
},
|
||||
}),
|
||||
).toBe("scope upgrade pending approval (requestId: req-123)");
|
||||
});
|
||||
|
||||
it("formats protocol mismatch details with both client and gateway versions", () => {
|
||||
expect(
|
||||
formatConnectErrorMessage({
|
||||
message: "protocol mismatch",
|
||||
details: {
|
||||
code: "PROTOCOL_MISMATCH",
|
||||
clientMinProtocol: 5,
|
||||
clientMaxProtocol: 5,
|
||||
expectedProtocol: 4,
|
||||
minimumProbeProtocol: 4,
|
||||
},
|
||||
}),
|
||||
).toBe("protocol mismatch: Control UI v5, Gateway v4, probe min v4");
|
||||
});
|
||||
});
|
||||
528
packages/gateway-protocol/src/connect-error-details.ts
Normal file
528
packages/gateway-protocol/src/connect-error-details.ts
Normal file
@@ -0,0 +1,528 @@
|
||||
/**
|
||||
* Shared gateway connect-error detail helpers.
|
||||
*
|
||||
* These details cross client/server boundaries, so readers normalize untrusted
|
||||
* payloads before using them in reconnect decisions or user-facing messages.
|
||||
*/
|
||||
function normalizeOptionalString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function normalizeArrayBackedTrimmedStringList(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const values = value
|
||||
.map((entry) => normalizeOptionalString(entry))
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
// Pairing details omit absent lists. Emitting empty arrays makes clients think
|
||||
// the gateway intentionally supplied scope/role context when it did not.
|
||||
return values.length > 0 ? values : undefined;
|
||||
}
|
||||
|
||||
/** Structured connect-error codes carried in gateway error `details.code`. */
|
||||
export const ConnectErrorDetailCodes = {
|
||||
AUTH_REQUIRED: "AUTH_REQUIRED",
|
||||
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
||||
AUTH_TOKEN_MISSING: "AUTH_TOKEN_MISSING",
|
||||
AUTH_TOKEN_MISMATCH: "AUTH_TOKEN_MISMATCH",
|
||||
AUTH_TOKEN_NOT_CONFIGURED: "AUTH_TOKEN_NOT_CONFIGURED",
|
||||
AUTH_PASSWORD_MISSING: "AUTH_PASSWORD_MISSING", // pragma: allowlist secret
|
||||
AUTH_PASSWORD_MISMATCH: "AUTH_PASSWORD_MISMATCH", // pragma: allowlist secret
|
||||
AUTH_PASSWORD_NOT_CONFIGURED: "AUTH_PASSWORD_NOT_CONFIGURED", // pragma: allowlist secret
|
||||
AUTH_BOOTSTRAP_TOKEN_INVALID: "AUTH_BOOTSTRAP_TOKEN_INVALID",
|
||||
AUTH_DEVICE_TOKEN_MISMATCH: "AUTH_DEVICE_TOKEN_MISMATCH",
|
||||
AUTH_SCOPE_MISMATCH: "AUTH_SCOPE_MISMATCH",
|
||||
AUTH_RATE_LIMITED: "AUTH_RATE_LIMITED",
|
||||
AUTH_TAILSCALE_IDENTITY_MISSING: "AUTH_TAILSCALE_IDENTITY_MISSING",
|
||||
AUTH_TAILSCALE_PROXY_MISSING: "AUTH_TAILSCALE_PROXY_MISSING",
|
||||
AUTH_TAILSCALE_WHOIS_FAILED: "AUTH_TAILSCALE_WHOIS_FAILED",
|
||||
AUTH_TAILSCALE_IDENTITY_MISMATCH: "AUTH_TAILSCALE_IDENTITY_MISMATCH",
|
||||
CONTROL_UI_ORIGIN_NOT_ALLOWED: "CONTROL_UI_ORIGIN_NOT_ALLOWED",
|
||||
PROTOCOL_MISMATCH: "PROTOCOL_MISMATCH",
|
||||
CONTROL_UI_DEVICE_IDENTITY_REQUIRED: "CONTROL_UI_DEVICE_IDENTITY_REQUIRED",
|
||||
DEVICE_IDENTITY_REQUIRED: "DEVICE_IDENTITY_REQUIRED",
|
||||
DEVICE_AUTH_INVALID: "DEVICE_AUTH_INVALID",
|
||||
DEVICE_AUTH_DEVICE_ID_MISMATCH: "DEVICE_AUTH_DEVICE_ID_MISMATCH",
|
||||
DEVICE_AUTH_SIGNATURE_EXPIRED: "DEVICE_AUTH_SIGNATURE_EXPIRED",
|
||||
DEVICE_AUTH_NONCE_REQUIRED: "DEVICE_AUTH_NONCE_REQUIRED",
|
||||
DEVICE_AUTH_NONCE_MISMATCH: "DEVICE_AUTH_NONCE_MISMATCH",
|
||||
DEVICE_AUTH_SIGNATURE_INVALID: "DEVICE_AUTH_SIGNATURE_INVALID",
|
||||
DEVICE_AUTH_PUBLIC_KEY_INVALID: "DEVICE_AUTH_PUBLIC_KEY_INVALID",
|
||||
PAIRING_REQUIRED: "PAIRING_REQUIRED",
|
||||
CLIENT_VERSION_MISMATCH: "CLIENT_VERSION_MISMATCH",
|
||||
} as const;
|
||||
|
||||
export type ConnectErrorDetailCode =
|
||||
(typeof ConnectErrorDetailCodes)[keyof typeof ConnectErrorDetailCodes];
|
||||
|
||||
/** Pairing-specific reasons clients can display and use for reconnect policy. */
|
||||
export const ConnectPairingRequiredReasons = {
|
||||
NOT_PAIRED: "not-paired",
|
||||
ROLE_UPGRADE: "role-upgrade",
|
||||
SCOPE_UPGRADE: "scope-upgrade",
|
||||
METADATA_UPGRADE: "metadata-upgrade",
|
||||
} as const;
|
||||
|
||||
export type ConnectPairingRequiredReason =
|
||||
(typeof ConnectPairingRequiredReasons)[keyof typeof ConnectPairingRequiredReasons];
|
||||
|
||||
/** Suggested client-side recovery action for structured connect errors. */
|
||||
export type ConnectRecoveryNextStep =
|
||||
| "retry_with_device_token"
|
||||
| "update_auth_configuration"
|
||||
| "update_auth_credentials"
|
||||
| "wait_then_retry"
|
||||
| "review_auth_configuration";
|
||||
|
||||
/** Optional retry guidance extracted from gateway connect-error details. */
|
||||
export type ConnectErrorRecoveryAdvice = {
|
||||
canRetryWithDeviceToken?: boolean;
|
||||
recommendedNextStep?: ConnectRecoveryNextStep;
|
||||
};
|
||||
|
||||
/** Full structured details for pairing-required connect failures. */
|
||||
export type PairingConnectErrorDetails = {
|
||||
code: typeof ConnectErrorDetailCodes.PAIRING_REQUIRED;
|
||||
reason?: ConnectPairingRequiredReason;
|
||||
requestId?: string;
|
||||
remediationHint?: string;
|
||||
recommendedNextStep?: ConnectRecoveryNextStep;
|
||||
retryable?: boolean;
|
||||
pauseReconnect?: boolean;
|
||||
deviceId?: string;
|
||||
requestedRole?: string;
|
||||
requestedScopes?: string[];
|
||||
approvedRoles?: string[];
|
||||
approvedScopes?: string[];
|
||||
};
|
||||
|
||||
/** Compact pairing-required subset used by reconnect/status surfaces. */
|
||||
export type ConnectPairingRequiredDetails = Pick<
|
||||
PairingConnectErrorDetails,
|
||||
"reason" | "requestId"
|
||||
>;
|
||||
|
||||
const CONNECT_RECOVERY_NEXT_STEP_VALUES: ReadonlySet<ConnectRecoveryNextStep> = new Set([
|
||||
"retry_with_device_token",
|
||||
"update_auth_configuration",
|
||||
"update_auth_credentials",
|
||||
"wait_then_retry",
|
||||
"review_auth_configuration",
|
||||
]);
|
||||
|
||||
const CONNECT_PAIRING_REQUIRED_REASON_VALUES: ReadonlySet<ConnectPairingRequiredReason> = new Set([
|
||||
"not-paired",
|
||||
"role-upgrade",
|
||||
"scope-upgrade",
|
||||
"metadata-upgrade",
|
||||
]);
|
||||
const PAIRING_CONNECT_REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
|
||||
const PAIRING_CONNECT_REASON_METADATA: Readonly<
|
||||
Record<
|
||||
ConnectPairingRequiredReason,
|
||||
{
|
||||
requirement: string;
|
||||
remediationHint: string;
|
||||
recoveryTitle: string;
|
||||
}
|
||||
>
|
||||
> = {
|
||||
"not-paired": {
|
||||
requirement: "device is not approved yet",
|
||||
remediationHint: "Approve this device from the pending pairing requests.",
|
||||
recoveryTitle: "Gateway pairing approval required.",
|
||||
},
|
||||
"role-upgrade": {
|
||||
requirement: "device is asking for a higher role than currently approved",
|
||||
remediationHint: "Review the requested role upgrade, then approve the pending request.",
|
||||
recoveryTitle: "Gateway role upgrade approval required.",
|
||||
},
|
||||
"scope-upgrade": {
|
||||
requirement: "device is asking for more scopes than currently approved",
|
||||
remediationHint: "Review the requested scopes, then approve the pending upgrade.",
|
||||
recoveryTitle: "Gateway scope upgrade approval required.",
|
||||
},
|
||||
"metadata-upgrade": {
|
||||
requirement: "device identity changed and must be re-approved",
|
||||
remediationHint: "Review the refreshed device details, then approve the pending request.",
|
||||
recoveryTitle: "Gateway device refresh approval required.",
|
||||
},
|
||||
};
|
||||
|
||||
const CONNECT_PAIRING_REQUIRED_MESSAGE_BY_REASON: Readonly<
|
||||
Record<ConnectPairingRequiredReason, string>
|
||||
> = {
|
||||
"not-paired": "device pairing required",
|
||||
"role-upgrade": "role upgrade pending approval",
|
||||
"scope-upgrade": "scope upgrade pending approval",
|
||||
"metadata-upgrade": "device metadata change pending approval",
|
||||
};
|
||||
|
||||
/** Maps internal auth failure reasons to public connect-error detail codes. */
|
||||
export function resolveAuthConnectErrorDetailCode(
|
||||
reason: string | undefined,
|
||||
): ConnectErrorDetailCode {
|
||||
switch (reason) {
|
||||
case "token_missing":
|
||||
return ConnectErrorDetailCodes.AUTH_TOKEN_MISSING;
|
||||
case "token_mismatch":
|
||||
return ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH;
|
||||
case "token_missing_config":
|
||||
return ConnectErrorDetailCodes.AUTH_TOKEN_NOT_CONFIGURED;
|
||||
case "password_missing":
|
||||
return ConnectErrorDetailCodes.AUTH_PASSWORD_MISSING;
|
||||
case "password_mismatch":
|
||||
return ConnectErrorDetailCodes.AUTH_PASSWORD_MISMATCH;
|
||||
case "password_missing_config":
|
||||
return ConnectErrorDetailCodes.AUTH_PASSWORD_NOT_CONFIGURED;
|
||||
case "bootstrap_token_invalid":
|
||||
return ConnectErrorDetailCodes.AUTH_BOOTSTRAP_TOKEN_INVALID;
|
||||
case "tailscale_user_missing":
|
||||
return ConnectErrorDetailCodes.AUTH_TAILSCALE_IDENTITY_MISSING;
|
||||
case "tailscale_proxy_missing":
|
||||
return ConnectErrorDetailCodes.AUTH_TAILSCALE_PROXY_MISSING;
|
||||
case "tailscale_whois_failed":
|
||||
return ConnectErrorDetailCodes.AUTH_TAILSCALE_WHOIS_FAILED;
|
||||
case "tailscale_user_mismatch":
|
||||
return ConnectErrorDetailCodes.AUTH_TAILSCALE_IDENTITY_MISMATCH;
|
||||
case "rate_limited":
|
||||
return ConnectErrorDetailCodes.AUTH_RATE_LIMITED;
|
||||
case "device_token_mismatch":
|
||||
return ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH;
|
||||
case "scope_mismatch":
|
||||
return ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH;
|
||||
case undefined:
|
||||
return ConnectErrorDetailCodes.AUTH_REQUIRED;
|
||||
default:
|
||||
return ConnectErrorDetailCodes.AUTH_UNAUTHORIZED;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps device-auth verifier reasons to public connect-error detail codes. */
|
||||
export function resolveDeviceAuthConnectErrorDetailCode(
|
||||
reason: string | undefined,
|
||||
): ConnectErrorDetailCode {
|
||||
switch (reason) {
|
||||
case "device-id-mismatch":
|
||||
return ConnectErrorDetailCodes.DEVICE_AUTH_DEVICE_ID_MISMATCH;
|
||||
case "device-signature-stale":
|
||||
return ConnectErrorDetailCodes.DEVICE_AUTH_SIGNATURE_EXPIRED;
|
||||
case "device-nonce-missing":
|
||||
return ConnectErrorDetailCodes.DEVICE_AUTH_NONCE_REQUIRED;
|
||||
case "device-nonce-mismatch":
|
||||
return ConnectErrorDetailCodes.DEVICE_AUTH_NONCE_MISMATCH;
|
||||
case "device-signature":
|
||||
return ConnectErrorDetailCodes.DEVICE_AUTH_SIGNATURE_INVALID;
|
||||
case "device-public-key":
|
||||
return ConnectErrorDetailCodes.DEVICE_AUTH_PUBLIC_KEY_INVALID;
|
||||
default:
|
||||
return ConnectErrorDetailCodes.DEVICE_AUTH_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads a non-empty detail code from an untrusted error details payload. */
|
||||
export function readConnectErrorDetailCode(details: unknown): string | null {
|
||||
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
||||
return null;
|
||||
}
|
||||
const code = (details as { code?: unknown }).code;
|
||||
return typeof code === "string" && code.trim().length > 0 ? code : null;
|
||||
}
|
||||
|
||||
/** Extracts normalized retry advice from untrusted connect-error details. */
|
||||
export function readConnectErrorRecoveryAdvice(details: unknown): ConnectErrorRecoveryAdvice {
|
||||
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
||||
return {};
|
||||
}
|
||||
const raw = details as {
|
||||
canRetryWithDeviceToken?: unknown;
|
||||
recommendedNextStep?: unknown;
|
||||
};
|
||||
const canRetryWithDeviceToken =
|
||||
typeof raw.canRetryWithDeviceToken === "boolean" ? raw.canRetryWithDeviceToken : undefined;
|
||||
const normalizedNextStep = normalizeOptionalString(raw.recommendedNextStep) ?? "";
|
||||
const recommendedNextStep = CONNECT_RECOVERY_NEXT_STEP_VALUES.has(
|
||||
normalizedNextStep as ConnectRecoveryNextStep,
|
||||
)
|
||||
? (normalizedNextStep as ConnectRecoveryNextStep)
|
||||
: undefined;
|
||||
return {
|
||||
canRetryWithDeviceToken,
|
||||
recommendedNextStep,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePairingConnectReason(value: unknown): ConnectPairingRequiredReason | undefined {
|
||||
const normalized = normalizeOptionalString(value) ?? "";
|
||||
return CONNECT_PAIRING_REQUIRED_REASON_VALUES.has(normalized as ConnectPairingRequiredReason)
|
||||
? (normalized as ConnectPairingRequiredReason)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Normalizes pairing request ids before echoing them in close reasons or UI text. */
|
||||
export function normalizePairingConnectRequestId(value: unknown): string | undefined {
|
||||
const normalized = normalizeOptionalString(value);
|
||||
return normalized && PAIRING_CONNECT_REQUEST_ID_PATTERN.test(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeStringArray(value: unknown): string[] | undefined {
|
||||
return normalizeArrayBackedTrimmedStringList(value);
|
||||
}
|
||||
|
||||
function createPairingConnectErrorDetails(params: {
|
||||
reason?: ConnectPairingRequiredReason;
|
||||
requestId?: string;
|
||||
remediationHint?: string;
|
||||
recommendedNextStep?: ConnectRecoveryNextStep;
|
||||
retryable?: boolean;
|
||||
pauseReconnect?: boolean;
|
||||
deviceId?: string;
|
||||
requestedRole?: string;
|
||||
requestedScopes?: string[];
|
||||
approvedRoles?: string[];
|
||||
approvedScopes?: string[];
|
||||
}): PairingConnectErrorDetails {
|
||||
return {
|
||||
code: ConnectErrorDetailCodes.PAIRING_REQUIRED,
|
||||
...(params.reason ? { reason: params.reason } : {}),
|
||||
...(params.requestId ? { requestId: params.requestId } : {}),
|
||||
...(params.remediationHint ? { remediationHint: params.remediationHint } : {}),
|
||||
...(params.recommendedNextStep ? { recommendedNextStep: params.recommendedNextStep } : {}),
|
||||
...(params.retryable !== undefined ? { retryable: params.retryable } : {}),
|
||||
...(params.pauseReconnect !== undefined ? { pauseReconnect: params.pauseReconnect } : {}),
|
||||
...(params.deviceId ? { deviceId: params.deviceId } : {}),
|
||||
...(params.requestedRole ? { requestedRole: params.requestedRole } : {}),
|
||||
...(params.requestedScopes ? { requestedScopes: params.requestedScopes } : {}),
|
||||
...(params.approvedRoles ? { approvedRoles: params.approvedRoles } : {}),
|
||||
...(params.approvedScopes ? { approvedScopes: params.approvedScopes } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Human-readable requirement summary for a pairing-required reason. */
|
||||
export function describePairingConnectRequirement(
|
||||
reason: ConnectPairingRequiredReason | undefined,
|
||||
): string {
|
||||
return reason
|
||||
? PAIRING_CONNECT_REASON_METADATA[reason].requirement
|
||||
: "device approval is required";
|
||||
}
|
||||
|
||||
/** Builds the gateway close/error message for a pairing-required connect failure. */
|
||||
export function buildPairingConnectErrorMessage(
|
||||
reason: ConnectPairingRequiredReason | undefined,
|
||||
): string {
|
||||
return reason
|
||||
? `pairing required: ${describePairingConnectRequirement(reason)}`
|
||||
: "pairing required";
|
||||
}
|
||||
|
||||
function buildPairingConnectRemediationHint(
|
||||
reason: ConnectPairingRequiredReason | undefined,
|
||||
): string {
|
||||
return reason
|
||||
? PAIRING_CONNECT_REASON_METADATA[reason].remediationHint
|
||||
: "Approve the pending device request before retrying.";
|
||||
}
|
||||
|
||||
/** Short user-facing recovery title for pairing-required connect failures. */
|
||||
export function buildPairingConnectRecoveryTitle(
|
||||
reason: ConnectPairingRequiredReason | undefined,
|
||||
): string {
|
||||
return reason
|
||||
? PAIRING_CONNECT_REASON_METADATA[reason].recoveryTitle
|
||||
: "Gateway pairing approval required.";
|
||||
}
|
||||
|
||||
/** Builds sanitized structured details for a pairing-required connect failure. */
|
||||
export function buildPairingConnectErrorDetails(params: {
|
||||
reason: ConnectPairingRequiredReason | undefined;
|
||||
requestId?: string;
|
||||
remediationHint?: string;
|
||||
recommendedNextStep?: ConnectRecoveryNextStep;
|
||||
retryable?: boolean;
|
||||
pauseReconnect?: boolean;
|
||||
deviceId?: string;
|
||||
requestedRole?: string;
|
||||
requestedScopes?: string[];
|
||||
approvedRoles?: string[];
|
||||
approvedScopes?: string[];
|
||||
}): PairingConnectErrorDetails {
|
||||
const requestId = normalizePairingConnectRequestId(params.requestId);
|
||||
const remediationHint =
|
||||
normalizeOptionalString(params.remediationHint) ??
|
||||
buildPairingConnectRemediationHint(params.reason);
|
||||
const deviceId = normalizeOptionalString(params.deviceId);
|
||||
const requestedRole = normalizeOptionalString(params.requestedRole);
|
||||
const requestedScopes = normalizeStringArray(params.requestedScopes);
|
||||
const approvedRoles = normalizeStringArray(params.approvedRoles);
|
||||
const approvedScopes = normalizeStringArray(params.approvedScopes);
|
||||
return createPairingConnectErrorDetails({
|
||||
reason: params.reason,
|
||||
requestId,
|
||||
remediationHint,
|
||||
recommendedNextStep: params.recommendedNextStep,
|
||||
retryable: params.retryable,
|
||||
pauseReconnect: params.pauseReconnect,
|
||||
deviceId,
|
||||
requestedRole,
|
||||
requestedScopes,
|
||||
approvedRoles,
|
||||
approvedScopes,
|
||||
});
|
||||
}
|
||||
|
||||
/** Builds a sanitized close reason string for WebSocket pairing rejections. */
|
||||
export function buildPairingConnectCloseReason(params: {
|
||||
reason: ConnectPairingRequiredReason | undefined;
|
||||
requestId?: string;
|
||||
}): string {
|
||||
const requestId = normalizePairingConnectRequestId(params.requestId);
|
||||
const message = buildPairingConnectErrorMessage(params.reason);
|
||||
return requestId ? `${message} (requestId: ${requestId})` : message;
|
||||
}
|
||||
|
||||
/** Reads and backfills pairing-required details from an untrusted details object. */
|
||||
export function readPairingConnectErrorDetails(
|
||||
details: unknown,
|
||||
): PairingConnectErrorDetails | null {
|
||||
if (readConnectErrorDetailCode(details) !== ConnectErrorDetailCodes.PAIRING_REQUIRED) {
|
||||
return null;
|
||||
}
|
||||
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
||||
return null;
|
||||
}
|
||||
const raw = details as {
|
||||
reason?: unknown;
|
||||
requestId?: unknown;
|
||||
remediationHint?: unknown;
|
||||
recommendedNextStep?: unknown;
|
||||
retryable?: unknown;
|
||||
pauseReconnect?: unknown;
|
||||
deviceId?: unknown;
|
||||
requestedRole?: unknown;
|
||||
requestedScopes?: unknown;
|
||||
approvedRoles?: unknown;
|
||||
approvedScopes?: unknown;
|
||||
};
|
||||
const reason = normalizePairingConnectReason(raw.reason);
|
||||
const requestId = normalizePairingConnectRequestId(raw.requestId);
|
||||
const remediationHint =
|
||||
normalizeOptionalString(raw.remediationHint) ?? buildPairingConnectRemediationHint(reason);
|
||||
const normalizedNextStep = normalizeOptionalString(raw.recommendedNextStep) ?? "";
|
||||
const recommendedNextStep = CONNECT_RECOVERY_NEXT_STEP_VALUES.has(
|
||||
normalizedNextStep as ConnectRecoveryNextStep,
|
||||
)
|
||||
? (normalizedNextStep as ConnectRecoveryNextStep)
|
||||
: undefined;
|
||||
const deviceId = normalizeOptionalString(raw.deviceId);
|
||||
const requestedRole = normalizeOptionalString(raw.requestedRole);
|
||||
const requestedScopes = normalizeStringArray(raw.requestedScopes);
|
||||
const approvedRoles = normalizeStringArray(raw.approvedRoles);
|
||||
const approvedScopes = normalizeStringArray(raw.approvedScopes);
|
||||
return createPairingConnectErrorDetails({
|
||||
reason,
|
||||
requestId,
|
||||
remediationHint,
|
||||
recommendedNextStep,
|
||||
retryable: typeof raw.retryable === "boolean" ? raw.retryable : undefined,
|
||||
pauseReconnect: typeof raw.pauseReconnect === "boolean" ? raw.pauseReconnect : undefined,
|
||||
deviceId,
|
||||
requestedRole,
|
||||
requestedScopes,
|
||||
approvedRoles,
|
||||
approvedScopes,
|
||||
});
|
||||
}
|
||||
|
||||
/** Parses legacy/string-only pairing-required messages into structured details. */
|
||||
export function readConnectPairingRequiredMessage(
|
||||
message: string | null | undefined,
|
||||
): ConnectPairingRequiredDetails | null {
|
||||
const normalizedMessage = normalizeOptionalString(message);
|
||||
if (!normalizedMessage) {
|
||||
return null;
|
||||
}
|
||||
const normalized = normalizedMessage.trim().toLowerCase();
|
||||
let reason: ConnectPairingRequiredReason | undefined;
|
||||
for (const [candidate, prefix] of Object.entries(
|
||||
CONNECT_PAIRING_REQUIRED_MESSAGE_BY_REASON,
|
||||
) as Array<[ConnectPairingRequiredReason, string]>) {
|
||||
if (normalized.includes(prefix)) {
|
||||
reason = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!reason && normalized.includes("pairing required")) {
|
||||
reason = ConnectPairingRequiredReasons.NOT_PAIRED;
|
||||
}
|
||||
if (!reason) {
|
||||
return null;
|
||||
}
|
||||
const requestId = normalizePairingConnectRequestId(
|
||||
normalizedMessage.match(/\(requestId:\s*([^\s)]+)\)/i)?.[1],
|
||||
);
|
||||
return {
|
||||
...(requestId ? { requestId } : {}),
|
||||
reason,
|
||||
};
|
||||
}
|
||||
|
||||
/** Formats pairing-required details into the canonical user-facing message. */
|
||||
export function formatConnectPairingRequiredMessage(details: unknown): string {
|
||||
const pairing = readPairingConnectErrorDetails(details);
|
||||
const base =
|
||||
CONNECT_PAIRING_REQUIRED_MESSAGE_BY_REASON[
|
||||
pairing?.reason ?? ConnectPairingRequiredReasons.NOT_PAIRED
|
||||
];
|
||||
return pairing?.requestId ? `${base} (requestId: ${pairing.requestId})` : base;
|
||||
}
|
||||
|
||||
/** Formats connect errors using structured details before falling back to raw messages. */
|
||||
export function formatConnectErrorMessage(params: { message?: string; details?: unknown }): string {
|
||||
if (readConnectErrorDetailCode(params.details) === ConnectErrorDetailCodes.PAIRING_REQUIRED) {
|
||||
return formatConnectPairingRequiredMessage(params.details);
|
||||
}
|
||||
if (readConnectErrorDetailCode(params.details) === ConnectErrorDetailCodes.PROTOCOL_MISMATCH) {
|
||||
return formatProtocolMismatchMessage(params.message, params.details);
|
||||
}
|
||||
return normalizeOptionalString(params.message) ?? "gateway request failed";
|
||||
}
|
||||
|
||||
function formatProtocolMismatchMessage(message: string | undefined, details: unknown): string {
|
||||
const raw = details as {
|
||||
clientMinProtocol?: unknown;
|
||||
clientMaxProtocol?: unknown;
|
||||
expectedProtocol?: unknown;
|
||||
minimumProbeProtocol?: unknown;
|
||||
};
|
||||
const clientMin = normalizeProtocolNumber(raw.clientMinProtocol);
|
||||
const clientMax = normalizeProtocolNumber(raw.clientMaxProtocol);
|
||||
const expected = normalizeProtocolNumber(raw.expectedProtocol);
|
||||
const probeMin = normalizeProtocolNumber(raw.minimumProbeProtocol);
|
||||
const parts: string[] = [];
|
||||
if (clientMin !== undefined && clientMax !== undefined) {
|
||||
parts.push(
|
||||
clientMin === clientMax
|
||||
? `Control UI v${clientMin}`
|
||||
: `Control UI v${clientMin}-v${clientMax}`,
|
||||
);
|
||||
}
|
||||
if (expected !== undefined) {
|
||||
parts.push(`Gateway v${expected}`);
|
||||
}
|
||||
if (probeMin !== undefined) {
|
||||
parts.push(`probe min v${probeMin}`);
|
||||
}
|
||||
const normalized = normalizeOptionalString(message) ?? "protocol mismatch";
|
||||
return parts.length > 0 ? `${normalized}: ${parts.join(", ")}` : normalized;
|
||||
}
|
||||
|
||||
function normalizeProtocolNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
353
packages/gateway-protocol/src/cron-validators.test.ts
Normal file
353
packages/gateway-protocol/src/cron-validators.test.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
// Gateway Protocol tests cover cron validators behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
validateCronAddParams,
|
||||
validateCronGetParams,
|
||||
validateCronListParams,
|
||||
validateCronRemoveParams,
|
||||
validateCronRunParams,
|
||||
validateCronRunsParams,
|
||||
validateCronUpdateParams,
|
||||
} from "./index.js";
|
||||
|
||||
/**
|
||||
* Cron validator regressions for public scheduler RPC payloads.
|
||||
*
|
||||
* The cases cover both canonical `id` selectors and legacy `jobId` aliases,
|
||||
* delivery routing, update clears, and run-log path traversal guards.
|
||||
*/
|
||||
|
||||
/** Smallest valid cron job create payload shared by add/update variations. */
|
||||
const minimalAddParams = {
|
||||
name: "daily-summary",
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
sessionTarget: "main",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "systemEvent", text: "tick" },
|
||||
} as const;
|
||||
|
||||
const agentToolCallerScope = {
|
||||
kind: "agentTool",
|
||||
agentId: "ops",
|
||||
} as const;
|
||||
|
||||
describe("cron protocol validators", () => {
|
||||
it("accepts minimal add params", () => {
|
||||
expect(validateCronAddParams(minimalAddParams)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects public caller scope on cron admin params", () => {
|
||||
expect(validateCronListParams({ callerScope: agentToolCallerScope })).toBe(false);
|
||||
expect(validateCronGetParams({ id: "job-1", callerScope: agentToolCallerScope })).toBe(false);
|
||||
expect(validateCronAddParams({ ...minimalAddParams, callerScope: agentToolCallerScope })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: { enabled: false },
|
||||
callerScope: agentToolCallerScope,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(validateCronRemoveParams({ jobId: "job-1", callerScope: agentToolCallerScope })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(validateCronRunParams({ id: "job-1", callerScope: agentToolCallerScope })).toBe(false);
|
||||
expect(validateCronRunsParams({ id: "job-1", callerScope: agentToolCallerScope })).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts current and custom session targets", () => {
|
||||
expect(
|
||||
validateCronAddParams({
|
||||
...minimalAddParams,
|
||||
sessionTarget: "current",
|
||||
payload: { kind: "agentTurn", message: "tick" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateCronAddParams({
|
||||
...minimalAddParams,
|
||||
sessionTarget: "session:project-alpha",
|
||||
payload: { kind: "agentTurn", message: "tick" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: { sessionTarget: "session:project-alpha" },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts command cron payloads", () => {
|
||||
expect(
|
||||
validateCronAddParams({
|
||||
...minimalAddParams,
|
||||
sessionTarget: "isolated",
|
||||
payload: {
|
||||
kind: "command",
|
||||
argv: ["sh", "-lc", "echo ok"],
|
||||
cwd: "/srv/example",
|
||||
env: { FOO: "bar" },
|
||||
input: "stdin",
|
||||
timeoutSeconds: 30,
|
||||
noOutputTimeoutSeconds: 5,
|
||||
outputMaxBytes: 4096,
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: {
|
||||
payload: {
|
||||
kind: "command",
|
||||
argv: ["sh", "-lc", "echo updated"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects add params when required scheduling fields are missing", () => {
|
||||
const { wakeMode: _wakeMode, ...withoutWakeMode } = minimalAddParams;
|
||||
expect(validateCronAddParams(withoutWakeMode)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts update params for id and jobId selectors", () => {
|
||||
expect(validateCronUpdateParams({ id: "job-1", patch: { enabled: false } })).toBe(true);
|
||||
expect(validateCronUpdateParams({ jobId: "job-2", patch: { enabled: true } })).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts nullable model clears only on update payload patches", () => {
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: {
|
||||
payload: {
|
||||
kind: "agentTurn",
|
||||
model: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateCronAddParams({
|
||||
...minimalAddParams,
|
||||
payload: {
|
||||
kind: "agentTurn",
|
||||
message: "tick",
|
||||
model: null,
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts get params for id and jobId selectors", () => {
|
||||
expect(validateCronGetParams({ id: "job-1" })).toBe(true);
|
||||
expect(validateCronGetParams({ jobId: "job-2" })).toBe(true);
|
||||
expect(validateCronGetParams({})).toBe(false);
|
||||
expect(validateCronGetParams({ id: "" })).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts delivery threadId on add and update params", () => {
|
||||
expect(
|
||||
validateCronAddParams({
|
||||
...minimalAddParams,
|
||||
delivery: {
|
||||
mode: "announce",
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
threadId: 42,
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: {
|
||||
delivery: {
|
||||
mode: "announce",
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
threadId: "topic-42",
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: {
|
||||
delivery: {
|
||||
threadId: 42,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts nullable delivery clears on update params", () => {
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: {
|
||||
delivery: {
|
||||
channel: null,
|
||||
to: null,
|
||||
threadId: null,
|
||||
accountId: null,
|
||||
failureDestination: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: {
|
||||
delivery: {
|
||||
failureDestination: {
|
||||
channel: null,
|
||||
to: null,
|
||||
accountId: null,
|
||||
mode: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects blank cron delivery target strings", () => {
|
||||
expect(
|
||||
validateCronAddParams({
|
||||
...minimalAddParams,
|
||||
delivery: {
|
||||
mode: "announce",
|
||||
channel: "telegram",
|
||||
to: " ",
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: {
|
||||
delivery: {
|
||||
channel: "\t",
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: {
|
||||
delivery: {
|
||||
failureDestination: {
|
||||
channel: null,
|
||||
to: " ",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
validateCronUpdateParams({
|
||||
id: "job-1",
|
||||
patch: {
|
||||
failureAlert: {
|
||||
channel: "last",
|
||||
to: "\n\t",
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts remove params for id and jobId selectors", () => {
|
||||
expect(validateCronRemoveParams({ id: "job-1" })).toBe(true);
|
||||
expect(validateCronRemoveParams({ jobId: "job-2" })).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts run params mode for id and jobId selectors", () => {
|
||||
expect(validateCronRunParams({ id: "job-1", mode: "force" })).toBe(true);
|
||||
expect(validateCronRunParams({ jobId: "job-2", mode: "due" })).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts list paging/filter/sort params", () => {
|
||||
expect(
|
||||
validateCronListParams({
|
||||
includeDisabled: true,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
query: "daily",
|
||||
enabled: "all",
|
||||
scheduleKind: "cron",
|
||||
lastRunStatus: "unknown",
|
||||
sortBy: "nextRunAtMs",
|
||||
sortDir: "asc",
|
||||
agentId: "ops",
|
||||
compact: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(validateCronListParams({ offset: -1 })).toBe(false);
|
||||
expect(validateCronListParams({ agentId: "" })).toBe(false);
|
||||
expect(validateCronListParams({ scheduleKind: "yearly" })).toBe(false);
|
||||
expect(validateCronListParams({ lastRunStatus: "pending" })).toBe(false);
|
||||
});
|
||||
|
||||
it("enforces runs limit minimum for id and jobId selectors", () => {
|
||||
expect(validateCronRunsParams({ id: "job-1", limit: 1 })).toBe(true);
|
||||
expect(validateCronRunsParams({ jobId: "job-2", limit: 1 })).toBe(true);
|
||||
expect(validateCronRunsParams({ id: "job-1", limit: 0 })).toBe(false);
|
||||
expect(validateCronRunsParams({ jobId: "job-2", limit: 0 })).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects cron.runs path traversal ids", () => {
|
||||
expect(validateCronRunsParams({ id: "../job-1" })).toBe(false);
|
||||
expect(validateCronRunsParams({ id: "nested/job-1" })).toBe(false);
|
||||
expect(validateCronRunsParams({ jobId: "..\\job-2" })).toBe(false);
|
||||
expect(validateCronRunsParams({ jobId: "nested\\job-2" })).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts runs paging/filter/sort params", () => {
|
||||
expect(
|
||||
validateCronRunsParams({
|
||||
id: "job-1",
|
||||
runId: "manual:job-1:123:0",
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
status: "error",
|
||||
query: "timeout",
|
||||
sortDir: "desc",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(validateCronRunsParams({ id: "job-1", offset: -1 })).toBe(false);
|
||||
expect(validateCronRunsParams({ id: "job-1", runId: "" })).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts all-scope runs with multi-select filters", () => {
|
||||
expect(
|
||||
validateCronRunsParams({
|
||||
scope: "all",
|
||||
limit: 25,
|
||||
statuses: ["ok", "error"],
|
||||
deliveryStatuses: ["delivered", "not-requested"],
|
||||
query: "fail",
|
||||
sortDir: "desc",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateCronRunsParams({
|
||||
scope: "job",
|
||||
statuses: [],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
132
packages/gateway-protocol/src/exec-approvals-validators.test.ts
Normal file
132
packages/gateway-protocol/src/exec-approvals-validators.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
// Gateway Protocol tests cover exec approvals validators behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
validateExecApprovalRequestParams,
|
||||
validateExecApprovalsNodeSetParams,
|
||||
validateExecApprovalsSetParams,
|
||||
} from "./index.js";
|
||||
|
||||
/**
|
||||
* Exec approval validator regressions for gateway and node-scoped policy
|
||||
* writes. The fixtures pin runtime-owned allowlist metadata and command-span
|
||||
* bounds because those contracts are consumed by approval UI and replay logic.
|
||||
*/
|
||||
|
||||
describe("exec approvals protocol validators", () => {
|
||||
it("accepts runtime-owned allowlist metadata on gateway and node set payloads", () => {
|
||||
const file = {
|
||||
version: 1 as const,
|
||||
agents: {
|
||||
main: {
|
||||
allowlist: [
|
||||
{
|
||||
id: "entry-1",
|
||||
pattern: "cmd:allow-always:abcdef",
|
||||
source: "allow-always" as const,
|
||||
commandText: "python3 -c 'print(123)'",
|
||||
argPattern: "-c *",
|
||||
lastUsedAt: 1775154056736,
|
||||
lastUsedCommand: "python3 -c 'print(123)'",
|
||||
lastResolvedPath: "/usr/bin/python3",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(validateExecApprovalsSetParams({ file, baseHash: "abc123" })).toBe(true);
|
||||
expect(
|
||||
validateExecApprovalsNodeSetParams({
|
||||
nodeId: "node-1",
|
||||
file,
|
||||
baseHash: "abc123",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unknown allowlist metadata", () => {
|
||||
expect(
|
||||
validateExecApprovalsSetParams({
|
||||
file: {
|
||||
version: 1,
|
||||
agents: {
|
||||
main: {
|
||||
allowlist: [
|
||||
{
|
||||
pattern: "/usr/bin/python3",
|
||||
source: "unknown-source",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
baseHash: "abc123",
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
validateExecApprovalsSetParams({
|
||||
file: {
|
||||
version: 1,
|
||||
agents: {
|
||||
main: {
|
||||
allowlist: [
|
||||
{
|
||||
pattern: "/usr/bin/python3",
|
||||
randomMetadata: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
baseHash: "abc123",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("requires command spans to have non-negative starts and positive exclusive ends", () => {
|
||||
expect(
|
||||
validateExecApprovalRequestParams({
|
||||
command: "echo hi",
|
||||
commandSpans: [{ startIndex: 0, endIndex: 4 }],
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
validateExecApprovalRequestParams({
|
||||
command: "echo hi",
|
||||
commandSpans: [{ startIndex: 0, endIndex: 0 }],
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
validateExecApprovalRequestParams({
|
||||
command: "echo hi",
|
||||
commandSpans: [{ startIndex: -1, endIndex: 4 }],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts only optional unavailable approval decisions", () => {
|
||||
expect(
|
||||
validateExecApprovalRequestParams({
|
||||
command: "echo hi",
|
||||
unavailableDecisions: ["allow-always"],
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
for (const unavailableDecisions of [
|
||||
[],
|
||||
["allow-always", "allow-always"],
|
||||
["allow-once"],
|
||||
["deny"],
|
||||
]) {
|
||||
expect(
|
||||
validateExecApprovalRequestParams({
|
||||
command: "echo hi",
|
||||
unavailableDecisions,
|
||||
}),
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
877
packages/gateway-protocol/src/index.test.ts
Normal file
877
packages/gateway-protocol/src/index.test.ts
Normal file
@@ -0,0 +1,877 @@
|
||||
// Gateway Protocol tests cover index behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TALK_TEST_PROVIDER_ID } from "../../../src/test-utils/talk-test-provider.js";
|
||||
import * as protocol from "./index.js";
|
||||
import {
|
||||
formatValidationErrors,
|
||||
validateChatAbortParams,
|
||||
validateChatHistoryParams,
|
||||
validateChatMetadataParams,
|
||||
validateChatSendParams,
|
||||
validateChatEvent,
|
||||
validateCommandsListParams,
|
||||
validateConnectParams,
|
||||
validateModelsListParams,
|
||||
validateNodeEventResult,
|
||||
validateNodePairRequestParams,
|
||||
validateNodePresenceAlivePayload,
|
||||
validateTasksCancelParams,
|
||||
validateTasksListParams,
|
||||
validateTalkCatalogResult,
|
||||
validateTalkConfigResult,
|
||||
validateTalkEvent,
|
||||
validateTalkClientCreateParams,
|
||||
validateTalkClientSteerParams,
|
||||
validateTalkClientToolCallParams,
|
||||
validateTalkAgentControlResult,
|
||||
validateTalkSessionAppendAudioParams,
|
||||
validateTalkSessionCancelOutputParams,
|
||||
validateTalkSessionCancelTurnParams,
|
||||
validateTalkSessionCreateParams,
|
||||
validateTalkSessionJoinParams,
|
||||
validateTalkSessionJoinResult,
|
||||
validateTalkSessionSubmitToolResultParams,
|
||||
validateTalkSessionSteerParams,
|
||||
validateTalkSessionTurnParams,
|
||||
validateTalkSessionTurnResult,
|
||||
validateWakeParams,
|
||||
type ValidationError,
|
||||
} from "./index.js";
|
||||
|
||||
/**
|
||||
* Broad protocol validator smoke tests.
|
||||
*
|
||||
* This file exercises exported lazy validators, readable validation errors, and
|
||||
* representative cross-surface payloads so schema registry changes fail before
|
||||
* they reach CLI, Gateway, channel, or dashboard consumers.
|
||||
*/
|
||||
|
||||
/** Builds a validation error fixture while keeping only the field under test noisy. */
|
||||
const makeError = (overrides: Partial<ValidationError>): ValidationError => ({
|
||||
keyword: "type",
|
||||
instancePath: "",
|
||||
schemaPath: "#/",
|
||||
params: {},
|
||||
message: "validation error",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
/** Runtime shape shared by all exported lazy protocol validator functions. */
|
||||
type ProtocolValidator = (value: unknown) => boolean;
|
||||
|
||||
describe("lazy protocol validators", () => {
|
||||
it("validates through exported lazy validators", () => {
|
||||
expect(validateCommandsListParams({})).toBe(true);
|
||||
expect(validateCommandsListParams({ includeArgs: true })).toBe(true);
|
||||
expect(validateCommandsListParams({ includeArgs: "yes" })).toBe(false);
|
||||
expect(formatValidationErrors(validateCommandsListParams.errors)).toContain("must be boolean");
|
||||
});
|
||||
|
||||
it("keeps validation errors readable on the exported validator", () => {
|
||||
expect(validateConnectParams({})).toBe(false);
|
||||
expect(formatValidationErrors(validateConnectParams.errors)).toContain("must have required");
|
||||
|
||||
expect(
|
||||
validateConnectParams({
|
||||
minProtocol: 1,
|
||||
maxProtocol: 1,
|
||||
client: {
|
||||
id: "test",
|
||||
version: "1.0.0",
|
||||
platform: "test",
|
||||
mode: "test",
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(validateConnectParams.errors).toBeNull();
|
||||
});
|
||||
|
||||
it("accepts selected-agent scope on chat send, history, and abort params", () => {
|
||||
expect(
|
||||
validateChatHistoryParams({
|
||||
sessionKey: "global",
|
||||
agentId: "work",
|
||||
limit: 50,
|
||||
offset: 100,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateChatSendParams({
|
||||
sessionKey: "global",
|
||||
agentId: "work",
|
||||
sessionId: "session-work",
|
||||
message: "hello",
|
||||
idempotencyKey: "run-global-work",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateChatSendParams({
|
||||
sessionKey: "global",
|
||||
sessionId: "session-work",
|
||||
resumeSession: true,
|
||||
message: "hello",
|
||||
idempotencyKey: "run-global-work",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
validateChatAbortParams({
|
||||
sessionKey: "global",
|
||||
agentId: "work",
|
||||
runId: "run-global-work",
|
||||
preserveSideRuns: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
protocol.validateSessionsCompactParams({
|
||||
key: "global",
|
||||
agentId: "work",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts selected-agent scope on chat metadata params", () => {
|
||||
expect(validateChatMetadataParams({})).toBe(true);
|
||||
expect(validateChatMetadataParams({ agentId: "work" })).toBe(true);
|
||||
expect(validateChatMetadataParams({ agentId: "" })).toBe(false);
|
||||
expect(validateChatMetadataParams({ agentId: "work", view: "configured" })).toBe(false);
|
||||
});
|
||||
|
||||
it("validates chat sends that suppress command interpretation", () => {
|
||||
expect(
|
||||
validateChatSendParams({
|
||||
sessionKey: "agent:main",
|
||||
message: "/reset examples",
|
||||
suppressCommandInterpretation: true,
|
||||
idempotencyKey: "chat-run-1",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("validates Skill Workshop revision request params", () => {
|
||||
expect(
|
||||
protocol.validateSkillsProposalRequestRevisionParams({
|
||||
proposalId: "support-file-sampler-20260531-68207b7b7f",
|
||||
targetAgentId: "writer",
|
||||
instructions: "Make the support files 5",
|
||||
sessionKey: "agent:main:session:skill-workshop",
|
||||
idempotencyKey: "revision-run-1",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
protocol.validateSkillsProposalRequestRevisionParams({
|
||||
proposalId: "support-file-sampler-20260531-68207b7b7f",
|
||||
instructions: "",
|
||||
sessionKey: "agent:main:session:skill-workshop",
|
||||
idempotencyKey: "revision-run-1",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
protocol.validateSkillsProposalRequestRevisionParams({
|
||||
proposalId: "support-file-sampler-20260531-68207b7b7f",
|
||||
instructions: "Make the support files 5",
|
||||
sessionKey: "agent:main:session:skill-workshop",
|
||||
idempotencyKey: "revision-run-1",
|
||||
hiddenPrompt: "do not accept caller-provided hidden prompts",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("can still compile every exported protocol validator", () => {
|
||||
const failures: string[] = [];
|
||||
const validators: Array<[string, ProtocolValidator]> = [];
|
||||
for (const [name, value] of Object.entries(protocol)) {
|
||||
if (name.startsWith("validate") && typeof value === "function") {
|
||||
validators.push([name, value as ProtocolValidator]);
|
||||
}
|
||||
}
|
||||
|
||||
expect(validators.length).toBeGreaterThan(150);
|
||||
for (const [name, validate] of validators) {
|
||||
try {
|
||||
validate(undefined);
|
||||
} catch (err) {
|
||||
failures.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
expect(failures).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatValidationErrors", () => {
|
||||
it("returns unknown validation error when missing errors", () => {
|
||||
expect(formatValidationErrors(undefined)).toBe("unknown validation error");
|
||||
expect(formatValidationErrors(null)).toBe("unknown validation error");
|
||||
});
|
||||
|
||||
it("returns unknown validation error when errors list is empty", () => {
|
||||
expect(formatValidationErrors([])).toBe("unknown validation error");
|
||||
});
|
||||
|
||||
it("formats additionalProperties at root", () => {
|
||||
const err = makeError({
|
||||
keyword: "additionalProperties",
|
||||
params: { additionalProperty: "token" },
|
||||
});
|
||||
|
||||
expect(formatValidationErrors([err])).toBe("at root: unexpected property 'token'");
|
||||
});
|
||||
|
||||
it("formats additionalProperties with instancePath", () => {
|
||||
const err = makeError({
|
||||
keyword: "additionalProperties",
|
||||
instancePath: "/auth",
|
||||
params: { additionalProperty: "token" },
|
||||
});
|
||||
|
||||
expect(formatValidationErrors([err])).toBe("at /auth: unexpected property 'token'");
|
||||
});
|
||||
|
||||
it("formats message with path for other errors", () => {
|
||||
const err = makeError({
|
||||
keyword: "required",
|
||||
instancePath: "/auth",
|
||||
message: "must have required property 'token'",
|
||||
});
|
||||
|
||||
expect(formatValidationErrors([err])).toBe("at /auth: must have required property 'token'");
|
||||
});
|
||||
|
||||
it("de-dupes repeated entries", () => {
|
||||
const err = makeError({
|
||||
keyword: "required",
|
||||
instancePath: "/auth",
|
||||
message: "must have required property 'token'",
|
||||
});
|
||||
|
||||
expect(formatValidationErrors([err, err])).toBe(
|
||||
"at /auth: must have required property 'token'",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateTalkConfigResult", () => {
|
||||
it("accepts Talk SecretRef payloads", () => {
|
||||
expect(
|
||||
validateTalkConfigResult({
|
||||
config: {
|
||||
talk: {
|
||||
provider: TALK_TEST_PROVIDER_ID,
|
||||
providers: {
|
||||
[TALK_TEST_PROVIDER_ID]: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "ELEVENLABS_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
resolved: {
|
||||
provider: TALK_TEST_PROVIDER_ID,
|
||||
config: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "ELEVENLABS_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts normalized talk payloads without resolved provider materialization", () => {
|
||||
expect(
|
||||
validateTalkConfigResult({
|
||||
config: {
|
||||
talk: {
|
||||
provider: TALK_TEST_PROVIDER_ID,
|
||||
providers: {
|
||||
[TALK_TEST_PROVIDER_ID]: {
|
||||
voiceId: "voice-normalized",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts realtime Talk defaults without requiring a speech provider", () => {
|
||||
expect(
|
||||
validateTalkConfigResult({
|
||||
config: {
|
||||
talk: {
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OPENAI_API_KEY",
|
||||
},
|
||||
model: "gpt-realtime",
|
||||
},
|
||||
},
|
||||
model: "gpt-realtime",
|
||||
speakerVoice: "alloy",
|
||||
speakerVoiceId: "voice-123",
|
||||
voice: "alloy",
|
||||
instructions: "Speak with crisp diction.",
|
||||
mode: "realtime",
|
||||
transport: "gateway-relay",
|
||||
brain: "agent-consult",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateTalkCatalogResult", () => {
|
||||
it("accepts provider registry aliases", () => {
|
||||
expect(
|
||||
validateTalkCatalogResult({
|
||||
modes: ["realtime"],
|
||||
transports: ["gateway-relay"],
|
||||
brains: ["agent-consult"],
|
||||
speech: { providers: [] },
|
||||
transcription: { providers: [] },
|
||||
realtime: {
|
||||
ready: true,
|
||||
activeProvider: "google",
|
||||
providers: [
|
||||
{
|
||||
id: "google",
|
||||
aliases: ["gemini-live"],
|
||||
label: "Google Live Voice",
|
||||
configured: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateTalkClientCreateParams", () => {
|
||||
it("accepts provider, model, voice, mode, transport, and brain overrides", () => {
|
||||
expect(
|
||||
validateTalkClientCreateParams({
|
||||
sessionKey: "agent:main:main",
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
voice: "alloy",
|
||||
mode: "realtime",
|
||||
transport: "webrtc",
|
||||
brain: "agent-consult",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects request-time instruction overrides for Talk client creation", () => {
|
||||
expect(
|
||||
validateTalkClientCreateParams({
|
||||
sessionKey: "agent:main:main",
|
||||
instructions: "Ignore the configured realtime prompt.",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(formatValidationErrors(validateTalkClientCreateParams.errors)).toContain(
|
||||
"unexpected property 'instructions'",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateTalkEvent", () => {
|
||||
it("pins the common Talk event envelope used by relay and surface adapters", () => {
|
||||
expect(
|
||||
validateTalkEvent({
|
||||
id: "talk-session:1",
|
||||
type: "capture.started",
|
||||
sessionId: "talk-session",
|
||||
turnId: "turn-1",
|
||||
captureId: "capture-1",
|
||||
seq: 1,
|
||||
timestamp: "2026-05-05T12:00:00.000Z",
|
||||
mode: "stt-tts",
|
||||
transport: "managed-room",
|
||||
brain: "agent-consult",
|
||||
provider: "openai",
|
||||
final: false,
|
||||
callId: "call-1",
|
||||
itemId: "item-1",
|
||||
parentId: "parent-1",
|
||||
payload: { source: "ptt" },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects stale or vendor-shaped event payloads without required correlation", () => {
|
||||
expect(
|
||||
validateTalkEvent({
|
||||
type: "output.audio.delta",
|
||||
sessionId: "talk-session",
|
||||
seq: 0,
|
||||
timestamp: "2026-05-05T12:00:00.000Z",
|
||||
mode: "realtime-duplex",
|
||||
transport: "webrtc-sdp",
|
||||
brain: "agent-consult",
|
||||
payload: { byteLength: 12 },
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(formatValidationErrors(validateTalkEvent.errors)).toContain("must have required");
|
||||
});
|
||||
|
||||
it("requires turnId and captureId for scoped Talk events", () => {
|
||||
expect(
|
||||
validateTalkEvent({
|
||||
id: "talk-session:1",
|
||||
type: "turn.started",
|
||||
sessionId: "talk-session",
|
||||
seq: 1,
|
||||
timestamp: "2026-05-05T12:00:00.000Z",
|
||||
mode: "stt-tts",
|
||||
transport: "managed-room",
|
||||
brain: "agent-consult",
|
||||
payload: {},
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(formatValidationErrors(validateTalkEvent.errors)).toContain("must have required");
|
||||
|
||||
expect(
|
||||
validateTalkEvent({
|
||||
id: "talk-session:2",
|
||||
type: "capture.started",
|
||||
sessionId: "talk-session",
|
||||
turnId: "turn-1",
|
||||
seq: 2,
|
||||
timestamp: "2026-05-05T12:00:01.000Z",
|
||||
mode: "stt-tts",
|
||||
transport: "managed-room",
|
||||
brain: "agent-consult",
|
||||
payload: {},
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(formatValidationErrors(validateTalkEvent.errors)).toContain("must have required");
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateTalkSession", () => {
|
||||
it("accepts session-scoped provider, model, and voice selection", () => {
|
||||
expect(
|
||||
validateTalkSessionCreateParams({
|
||||
sessionKey: "agent:main:main",
|
||||
spawnedBy: "agent:main:parent",
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
voice: "alloy",
|
||||
mode: "realtime",
|
||||
transport: "managed-room",
|
||||
brain: "agent-consult",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateTalkSessionJoinResult({
|
||||
id: "session-1",
|
||||
roomId: "talk_room-1",
|
||||
roomUrl: "/talk/rooms/talk_handoff-1",
|
||||
sessionKey: "agent:main:main",
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
voice: "alloy",
|
||||
mode: "realtime",
|
||||
transport: "managed-room",
|
||||
brain: "agent-consult",
|
||||
createdAt: 1,
|
||||
expiresAt: 2,
|
||||
room: {
|
||||
activeClientId: "conn-1",
|
||||
recentTalkEvents: [
|
||||
{
|
||||
id: "talk_handoff-1:1",
|
||||
type: "session.ready",
|
||||
sessionId: "talk_handoff-1",
|
||||
seq: 1,
|
||||
timestamp: "2026-05-05T12:00:00.000Z",
|
||||
mode: "realtime",
|
||||
transport: "managed-room",
|
||||
brain: "agent-consult",
|
||||
payload: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects request-time instruction overrides for Talk session creation", () => {
|
||||
expect(
|
||||
validateTalkSessionCreateParams({
|
||||
sessionKey: "agent:main:main",
|
||||
instructionsOverride: "Ignore configured policy.",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(formatValidationErrors(validateTalkSessionCreateParams.errors)).toContain(
|
||||
"unexpected property 'instructionsOverride'",
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts managed-room join, turn lifecycle params, and results", () => {
|
||||
expect(
|
||||
validateTalkSessionJoinParams({
|
||||
sessionId: "session-1",
|
||||
token: "token-1",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateTalkSessionTurnParams({
|
||||
sessionId: "session-1",
|
||||
turnId: "turn-1",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateTalkSessionCancelTurnParams({
|
||||
sessionId: "session-1",
|
||||
turnId: "turn-1",
|
||||
reason: "barge-in",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateTalkSessionTurnResult({
|
||||
ok: true,
|
||||
turnId: "turn-1",
|
||||
events: [
|
||||
{
|
||||
id: "talk_handoff-1:2",
|
||||
type: "turn.started",
|
||||
sessionId: "talk_handoff-1",
|
||||
turnId: "turn-1",
|
||||
seq: 2,
|
||||
timestamp: "2026-05-05T12:00:00.000Z",
|
||||
mode: "realtime",
|
||||
transport: "managed-room",
|
||||
brain: "agent-consult",
|
||||
payload: {},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateTalkClientToolCallParams", () => {
|
||||
it("accepts optional relay session correlation", () => {
|
||||
expect(
|
||||
validateTalkClientToolCallParams({
|
||||
sessionKey: "agent:main:main",
|
||||
relaySessionId: "relay-1",
|
||||
callId: "call-1",
|
||||
name: "openclaw_agent_consult",
|
||||
args: { question: "what now" },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateTalkAgentControlParams", () => {
|
||||
it("accepts client and session steering params plus structured outcomes", () => {
|
||||
expect(
|
||||
validateTalkClientSteerParams({
|
||||
sessionKey: "agent:main:main",
|
||||
text: "use the safer path",
|
||||
mode: "steer",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateTalkSessionSteerParams({
|
||||
sessionId: "talk-1",
|
||||
sessionKey: "agent:main:main",
|
||||
text: "status",
|
||||
mode: "status",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateTalkAgentControlResult({
|
||||
ok: true,
|
||||
mode: "cancel",
|
||||
sessionKey: "agent:main:main",
|
||||
sessionId: "session-1",
|
||||
active: true,
|
||||
aborted: true,
|
||||
message: "Cancelled the active OpenClaw run.",
|
||||
speak: true,
|
||||
show: true,
|
||||
suppress: false,
|
||||
providerResult: {
|
||||
status: "cancelled",
|
||||
message: "Cancelled the active OpenClaw run.",
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateTalkSessionRelayParams", () => {
|
||||
it("accepts session audio, cancel, output cancel, and tool result params", () => {
|
||||
expect(
|
||||
validateTalkSessionAppendAudioParams({
|
||||
sessionId: "session-1",
|
||||
audioBase64: "aGVsbG8=",
|
||||
timestamp: 123,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateTalkSessionCancelTurnParams({
|
||||
sessionId: "session-1",
|
||||
reason: "barge-in",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateTalkSessionCancelOutputParams({
|
||||
sessionId: "session-1",
|
||||
reason: "barge-in",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateTalkSessionSubmitToolResultParams({
|
||||
sessionId: "session-1",
|
||||
callId: "call-1",
|
||||
result: { ok: true },
|
||||
options: { suppressResponse: true, willContinue: true },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateWakeParams", () => {
|
||||
it("accepts valid wake params", () => {
|
||||
expect(validateWakeParams({ mode: "now", text: "hello" })).toBe(true);
|
||||
expect(validateWakeParams({ mode: "next-heartbeat", text: "remind me" })).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects missing required fields", () => {
|
||||
expect(validateWakeParams({ mode: "now" })).toBe(false);
|
||||
expect(validateWakeParams({ text: "hello" })).toBe(false);
|
||||
expect(validateWakeParams({})).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts unknown properties for forward compatibility", () => {
|
||||
expect(
|
||||
validateWakeParams({
|
||||
mode: "now",
|
||||
text: "hello",
|
||||
paperclip: { version: "2026.416.0", source: "wake" },
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
validateWakeParams({
|
||||
mode: "next-heartbeat",
|
||||
text: "check back",
|
||||
unknownFutureField: 42,
|
||||
anotherExtra: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts optional sessionKey and agentId so per-session wakes can be routed", () => {
|
||||
// Origin-capture fix for #46886 / #64556 — wakes that name an explicit
|
||||
// session/agent must validate so the gateway handler can forward them
|
||||
// through to the cron service.
|
||||
expect(
|
||||
validateWakeParams({
|
||||
mode: "now",
|
||||
text: "follow up on the report",
|
||||
sessionKey: "agent:main:telegram:8661849123:topic:4052",
|
||||
agentId: "main",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateWakeParams({
|
||||
mode: "next-heartbeat",
|
||||
text: "tick",
|
||||
sessionKey: "agent:main:discord:guild123:thread456",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects sessionKey or agentId when they are present but empty strings", () => {
|
||||
// NonEmptyString — caller must omit the field entirely to fall back to
|
||||
// the default routing. Explicit empties are an error rather than a
|
||||
// silent no-op.
|
||||
expect(validateWakeParams({ mode: "now", text: "x", sessionKey: "" })).toBe(false);
|
||||
expect(validateWakeParams({ mode: "now", text: "x", agentId: "" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateChatEvent", () => {
|
||||
it("accepts v4 chat delta text and replacement markers", () => {
|
||||
expect(
|
||||
validateChatEvent({
|
||||
runId: "run-chat",
|
||||
sessionKey: "agent:main:main",
|
||||
seq: 1,
|
||||
state: "delta",
|
||||
deltaText: "hello",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateChatEvent({
|
||||
runId: "run-chat",
|
||||
sessionKey: "agent:main:main",
|
||||
seq: 2,
|
||||
state: "delta",
|
||||
deltaText: "replacement",
|
||||
replace: true,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "replacement" }],
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts selected-agent chat events", () => {
|
||||
expect(
|
||||
validateChatEvent({
|
||||
runId: "run-chat",
|
||||
sessionKey: "global",
|
||||
agentId: "work",
|
||||
seq: 1,
|
||||
state: "delta",
|
||||
deltaText: "hello",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects v3-style chat deltas without deltaText", () => {
|
||||
expect(
|
||||
validateChatEvent({
|
||||
runId: "run-chat",
|
||||
sessionKey: "agent:main:main",
|
||||
seq: 1,
|
||||
state: "delta",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateChatSendParams", () => {
|
||||
it("accepts one-turn fast:auto cutoff seconds", () => {
|
||||
const base = {
|
||||
sessionKey: "agent:main:main",
|
||||
message: "hello",
|
||||
fastMode: "auto",
|
||||
idempotencyKey: "run-1",
|
||||
};
|
||||
|
||||
expect(validateChatSendParams(base)).toBe(true);
|
||||
expect(validateChatSendParams({ ...base, fastAutoOnSeconds: 2 })).toBe(true);
|
||||
expect(validateChatSendParams({ ...base, fastAutoOnSeconds: 0 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateModelsListParams", () => {
|
||||
it("accepts the supported model catalog views", () => {
|
||||
expect(validateModelsListParams({})).toBe(true);
|
||||
expect(validateModelsListParams({ view: "default" })).toBe(true);
|
||||
expect(validateModelsListParams({ view: "configured" })).toBe(true);
|
||||
expect(validateModelsListParams({ view: "all" })).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unknown model catalog views and extra fields", () => {
|
||||
expect(validateModelsListParams({ view: "available" })).toBe(false);
|
||||
expect(validateModelsListParams({ view: "configured", provider: "minimax" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateTasksListParams", () => {
|
||||
it("accepts SDK task ledger filters", () => {
|
||||
expect(
|
||||
validateTasksListParams({
|
||||
status: ["running", "completed"],
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
limit: 50,
|
||||
cursor: "100",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects internal task statuses and unknown fields", () => {
|
||||
expect(validateTasksListParams({ status: "succeeded" })).toBe(false);
|
||||
expect(validateTasksCancelParams({ taskId: "task-1", force: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateNodePresenceAlivePayload", () => {
|
||||
it("accepts a closed trigger and known metadata fields", () => {
|
||||
expect(
|
||||
validateNodePresenceAlivePayload({
|
||||
trigger: "silent_push",
|
||||
sentAtMs: 123,
|
||||
displayName: "Peter's iPhone",
|
||||
version: "2026.4.28",
|
||||
platform: "iOS 18.4.0",
|
||||
deviceFamily: "iPhone",
|
||||
modelIdentifier: "iPhone17,1",
|
||||
pushTransport: "relay",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unknown triggers and extra fields", () => {
|
||||
expect(validateNodePresenceAlivePayload({ trigger: "push", sentAtMs: 123 })).toBe(false);
|
||||
expect(
|
||||
validateNodePresenceAlivePayload({
|
||||
trigger: "silent_push",
|
||||
arbitrary: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateNodePairRequestParams", () => {
|
||||
it("accepts node pairing permissions", () => {
|
||||
expect(
|
||||
validateNodePairRequestParams({
|
||||
nodeId: "ios-node-1",
|
||||
commands: ["canvas.snapshot"],
|
||||
permissions: { camera: true, notifications: false },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects non-boolean node pairing permissions", () => {
|
||||
expect(
|
||||
validateNodePairRequestParams({
|
||||
nodeId: "ios-node-1",
|
||||
permissions: { camera: "yes" },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateNodeEventResult", () => {
|
||||
it("accepts structured handled results", () => {
|
||||
expect(
|
||||
validateNodeEventResult({
|
||||
ok: true,
|
||||
event: "node.presence.alive",
|
||||
handled: true,
|
||||
reason: "persisted",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
1536
packages/gateway-protocol/src/index.ts
Normal file
1536
packages/gateway-protocol/src/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,235 @@
|
||||
// Gateway Protocol tests cover native protocol levels.guard behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, it } from "vitest";
|
||||
import { ProtocolSchemas } from "./schema/protocol-schemas.js";
|
||||
import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION } from "./version.js";
|
||||
|
||||
/**
|
||||
* Cross-language guard for Gateway protocol version constants.
|
||||
*
|
||||
* Native Swift/Kotlin clients and dev smoke scripts cannot derive these values
|
||||
* from TypeScript at runtime, so this test keeps checked-in generated constants
|
||||
* and connect payloads aligned with the package source of truth.
|
||||
*/
|
||||
|
||||
/** Min/max protocol pair expected in every native client surface. */
|
||||
type ProtocolLevels = {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
|
||||
const expectedLevels: ProtocolLevels = {
|
||||
min: MIN_CLIENT_PROTOCOL_VERSION,
|
||||
max: PROTOCOL_VERSION,
|
||||
};
|
||||
|
||||
/** Reads a repo-relative source file used by a native protocol guard. */
|
||||
async function readRepoFile(relativePath: string): Promise<string> {
|
||||
return fs.readFile(path.join(process.cwd(), relativePath), "utf8");
|
||||
}
|
||||
|
||||
/** Extracts one integer constant and reports the owning file on drift. */
|
||||
function extractInteger(
|
||||
content: string,
|
||||
pattern: RegExp,
|
||||
relativePath: string,
|
||||
label: string,
|
||||
): number {
|
||||
const match = pattern.exec(content);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`${relativePath}: missing ${label}; keep native Gateway protocol levels in sync with packages/gateway-protocol/src/version.ts.`,
|
||||
);
|
||||
}
|
||||
return Number.parseInt(match[1], 10);
|
||||
}
|
||||
|
||||
/** Compares native min/max values to the TypeScript version constants. */
|
||||
function assertLevelsMatch(relativePath: string, actual: ProtocolLevels): void {
|
||||
if (actual.min === expectedLevels.min && actual.max === expectedLevels.max) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`${relativePath}: Gateway protocol level mismatch: expected min=${expectedLevels.min} max=${expectedLevels.max} from packages/gateway-protocol/src/version.ts, got min=${actual.min} max=${actual.max}. Update the native constants/generated artifacts before shipping.`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Asserts a compatibility pattern exists in generated/native source text. */
|
||||
function assertPattern(
|
||||
content: string,
|
||||
relativePath: string,
|
||||
pattern: RegExp,
|
||||
message: string,
|
||||
): void {
|
||||
if (pattern.test(content)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`${relativePath}: ${message}`);
|
||||
}
|
||||
|
||||
function stringLiteralUnionValues(schema: unknown): string[] | undefined {
|
||||
if (!schema || typeof schema !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const candidate = schema as { anyOf?: unknown; oneOf?: unknown };
|
||||
const branches = candidate.oneOf ?? candidate.anyOf;
|
||||
if (!Array.isArray(branches) || branches.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const values: string[] = [];
|
||||
for (const branch of branches) {
|
||||
if (!branch || typeof branch !== "object" || !("const" in branch)) {
|
||||
return undefined;
|
||||
}
|
||||
const value = branch.const;
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
values.push(value);
|
||||
}
|
||||
return new Set(values).size === values.length ? values : undefined;
|
||||
}
|
||||
|
||||
describe("native Gateway protocol levels", () => {
|
||||
it("match the TypeScript source of truth", async () => {
|
||||
if (MIN_CLIENT_PROTOCOL_VERSION > PROTOCOL_VERSION) {
|
||||
throw new Error(
|
||||
`packages/gateway-protocol/src/version.ts: MIN_CLIENT_PROTOCOL_VERSION (${MIN_CLIENT_PROTOCOL_VERSION}) must not exceed PROTOCOL_VERSION (${PROTOCOL_VERSION}).`,
|
||||
);
|
||||
}
|
||||
|
||||
const swiftGeneratedPath =
|
||||
"apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift";
|
||||
const swiftGenerated = await readRepoFile(swiftGeneratedPath);
|
||||
assertLevelsMatch(swiftGeneratedPath, {
|
||||
min: extractInteger(
|
||||
swiftGenerated,
|
||||
/public let GATEWAY_MIN_PROTOCOL_VERSION = (\d+)/,
|
||||
swiftGeneratedPath,
|
||||
"GATEWAY_MIN_PROTOCOL_VERSION",
|
||||
),
|
||||
max: extractInteger(
|
||||
swiftGenerated,
|
||||
/public let GATEWAY_PROTOCOL_VERSION = (\d+)/,
|
||||
swiftGeneratedPath,
|
||||
"GATEWAY_PROTOCOL_VERSION",
|
||||
),
|
||||
});
|
||||
|
||||
const androidPath = "apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt";
|
||||
const android = await readRepoFile(androidPath);
|
||||
assertLevelsMatch(androidPath, {
|
||||
min: extractInteger(
|
||||
android,
|
||||
/const val GATEWAY_MIN_PROTOCOL_VERSION = (\d+)/,
|
||||
androidPath,
|
||||
"GATEWAY_MIN_PROTOCOL_VERSION",
|
||||
),
|
||||
max: extractInteger(
|
||||
android,
|
||||
/const val GATEWAY_PROTOCOL_VERSION = (\d+)/,
|
||||
androidPath,
|
||||
"GATEWAY_PROTOCOL_VERSION",
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the min constant for native connect compatibility ranges", async () => {
|
||||
const swiftConnectFiles = [
|
||||
"apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift",
|
||||
"apps/macos/Sources/OpenClawMacCLI/WizardCommand.swift",
|
||||
];
|
||||
for (const relativePath of swiftConnectFiles) {
|
||||
const content = await readRepoFile(relativePath);
|
||||
assertPattern(
|
||||
content,
|
||||
relativePath,
|
||||
/"minProtocol": ProtoAnyCodable\(GATEWAY_MIN_PROTOCOL_VERSION\)/,
|
||||
"connect params must advertise GATEWAY_MIN_PROTOCOL_VERSION as minProtocol.",
|
||||
);
|
||||
assertPattern(
|
||||
content,
|
||||
relativePath,
|
||||
/"maxProtocol": ProtoAnyCodable\(GATEWAY_PROTOCOL_VERSION\)/,
|
||||
"connect params must advertise GATEWAY_PROTOCOL_VERSION as maxProtocol.",
|
||||
);
|
||||
}
|
||||
|
||||
const androidPath = "apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt";
|
||||
const android = await readRepoFile(androidPath);
|
||||
assertPattern(
|
||||
android,
|
||||
androidPath,
|
||||
/put\("minProtocol", JsonPrimitive\(GATEWAY_MIN_PROTOCOL_VERSION\)\)/,
|
||||
"connect params must advertise GATEWAY_MIN_PROTOCOL_VERSION as minProtocol.",
|
||||
);
|
||||
assertPattern(
|
||||
android,
|
||||
androidPath,
|
||||
/put\("maxProtocol", JsonPrimitive\(GATEWAY_PROTOCOL_VERSION\)\)/,
|
||||
"connect params must advertise GATEWAY_PROTOCOL_VERSION as maxProtocol.",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the TypeScript source of truth for dev Gateway smoke scripts", async () => {
|
||||
const devScripts = ["scripts/dev/gateway-smoke.ts", "scripts/dev/ios-node-e2e.ts"];
|
||||
for (const relativePath of devScripts) {
|
||||
const content = await readRepoFile(relativePath);
|
||||
assertPattern(
|
||||
content,
|
||||
relativePath,
|
||||
/MIN_CLIENT_PROTOCOL_VERSION/,
|
||||
"connect params must import/use MIN_CLIENT_PROTOCOL_VERSION as minProtocol.",
|
||||
);
|
||||
assertPattern(
|
||||
content,
|
||||
relativePath,
|
||||
/PROTOCOL_VERSION/,
|
||||
"connect params must import/use PROTOCOL_VERSION as maxProtocol.",
|
||||
);
|
||||
assertPattern(
|
||||
content,
|
||||
relativePath,
|
||||
/minProtocol:\s*MIN_CLIENT_PROTOCOL_VERSION/,
|
||||
"connect params must advertise MIN_CLIENT_PROTOCOL_VERSION as minProtocol.",
|
||||
);
|
||||
assertPattern(
|
||||
content,
|
||||
relativePath,
|
||||
/maxProtocol:\s*PROTOCOL_VERSION/,
|
||||
"connect params must advertise PROTOCOL_VERSION as maxProtocol.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("emits named string-literal unions as Swift enums", async () => {
|
||||
const swiftGeneratedPath =
|
||||
"apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift";
|
||||
const swiftGenerated = await readRepoFile(swiftGeneratedPath);
|
||||
|
||||
for (const [name, schema] of Object.entries(ProtocolSchemas)) {
|
||||
const values = stringLiteralUnionValues(schema);
|
||||
if (!values) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const enumStart = `public enum ${name}: String, Codable, Sendable {`;
|
||||
const start = swiftGenerated.indexOf(enumStart);
|
||||
if (start < 0) {
|
||||
throw new Error(`${swiftGeneratedPath}: missing Swift enum for ${name}.`);
|
||||
}
|
||||
const end = swiftGenerated.indexOf("\n}\n", start);
|
||||
const enumSource = swiftGenerated.slice(start, end);
|
||||
for (const value of values) {
|
||||
assertPattern(
|
||||
enumSource,
|
||||
swiftGeneratedPath,
|
||||
new RegExp(`= ${JSON.stringify(value)}$`, "m"),
|
||||
`${name} must include the ${JSON.stringify(value)} literal.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
42
packages/gateway-protocol/src/primitives.secretref.test.ts
Normal file
42
packages/gateway-protocol/src/primitives.secretref.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// Gateway Protocol tests cover primitives.secretref behavior.
|
||||
import { Compile } from "typebox/compile";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
INVALID_EXEC_SECRET_REF_IDS,
|
||||
VALID_EXEC_SECRET_REF_IDS,
|
||||
} from "../../../src/test-utils/secret-ref-test-vectors.js";
|
||||
import { SecretInputSchema, SecretRefSchema } from "./schema/primitives.js";
|
||||
|
||||
/**
|
||||
* SecretRef schema regressions shared with core secret-ref test vectors.
|
||||
* Exec-backed ids have stricter character rules than env/file refs, so these
|
||||
* checks keep provider config payloads aligned with runtime secret resolution.
|
||||
*/
|
||||
describe("gateway protocol SecretRef schema", () => {
|
||||
const validateSecretRef = Compile(SecretRefSchema);
|
||||
const validateSecretInput = Compile(SecretInputSchema);
|
||||
|
||||
it("accepts valid source-specific refs", () => {
|
||||
expect(
|
||||
validateSecretRef.Check({ source: "env", provider: "default", id: "OPENAI_API_KEY" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateSecretRef.Check({
|
||||
source: "file",
|
||||
provider: "filemain",
|
||||
id: "/providers/openai/apiKey",
|
||||
}),
|
||||
).toBe(true);
|
||||
for (const id of VALID_EXEC_SECRET_REF_IDS) {
|
||||
expect(validateSecretRef.Check({ source: "exec", provider: "vault", id }), id).toBe(true);
|
||||
expect(validateSecretInput.Check({ source: "exec", provider: "vault", id }), id).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects invalid exec refs", () => {
|
||||
for (const id of INVALID_EXEC_SECRET_REF_IDS) {
|
||||
expect(validateSecretRef.Check({ source: "exec", provider: "vault", id }), id).toBe(false);
|
||||
expect(validateSecretInput.Check({ source: "exec", provider: "vault", id }), id).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
26
packages/gateway-protocol/src/push.test.ts
Normal file
26
packages/gateway-protocol/src/push.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// Gateway Protocol tests cover push behavior.
|
||||
import { Compile } from "typebox/compile";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PushTestResultSchema } from "./schema/push.js";
|
||||
|
||||
/**
|
||||
* Push protocol schema regression for APNS test results.
|
||||
* The transport field tells operators whether delivery used direct APNS or the
|
||||
* relay path, so it is part of the public result contract.
|
||||
*/
|
||||
describe("gateway protocol push schema", () => {
|
||||
const validatePushTestResult = Compile(PushTestResultSchema);
|
||||
|
||||
it("accepts push.test results with a transport", () => {
|
||||
expect(
|
||||
validatePushTestResult.Check({
|
||||
ok: true,
|
||||
status: 200,
|
||||
tokenSuffix: "abcd1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
transport: "relay",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
33
packages/gateway-protocol/src/schema.ts
Normal file
33
packages/gateway-protocol/src/schema.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Public schema barrel for the gateway protocol package.
|
||||
*
|
||||
* Runtime validators import canonical TypeBox schemas from their owning modules;
|
||||
* this barrel gives package consumers one stable path for schema-level imports.
|
||||
*/
|
||||
export * from "./schema/primitives.js";
|
||||
export * from "./schema/agent.js";
|
||||
export * from "./schema/agents-models-skills.js";
|
||||
export * from "./schema/artifacts.js";
|
||||
export * from "./schema/channels.js";
|
||||
export * from "./schema/commands.js";
|
||||
export * from "./schema/config.js";
|
||||
export * from "./schema/crestodian.js";
|
||||
export * from "./schema/cron.js";
|
||||
export * from "./schema/error-codes.js";
|
||||
export * from "./schema/environments.js";
|
||||
export * from "./schema/exec-approvals.js";
|
||||
export * from "./schema/devices.js";
|
||||
export * from "./schema/frames.js";
|
||||
export * from "./schema/logs-chat.js";
|
||||
export * from "./schema/nodes.js";
|
||||
export * from "./schema/protocol-schemas.js";
|
||||
export * from "./schema/push.js";
|
||||
export * from "./schema/secrets.js";
|
||||
export * from "./schema/sessions.js";
|
||||
export * from "./schema/snapshot.js";
|
||||
export * from "./schema/tasks.js";
|
||||
export * from "./schema/terminal.js";
|
||||
export * from "./schema/types.js";
|
||||
export * from "./schema/plugin-approvals.js";
|
||||
export * from "./schema/plugins.js";
|
||||
export * from "./schema/wizard.js";
|
||||
83
packages/gateway-protocol/src/schema/agent.test.ts
Normal file
83
packages/gateway-protocol/src/schema/agent.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// Gateway Protocol tests cover agent behavior.
|
||||
import { Value } from "typebox/value";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AgentParamsSchema } from "./agent.js";
|
||||
|
||||
/**
|
||||
* Regression coverage for agent-run schema payloads that carry internal
|
||||
* completion events. These events are produced by child automation and consumed
|
||||
* by parent agent runs, so the fixture mirrors the cross-runtime boundary.
|
||||
*/
|
||||
type AgentInternalEvent = {
|
||||
type: "task_completion";
|
||||
source: string;
|
||||
childSessionKey: string;
|
||||
childSessionId: string;
|
||||
announceType: string;
|
||||
taskLabel: string;
|
||||
status: "ok" | "error";
|
||||
statusLabel: string;
|
||||
result: string;
|
||||
attachments?: unknown[];
|
||||
mediaUrls?: string[];
|
||||
replyInstruction?: string;
|
||||
};
|
||||
|
||||
/** Builds the smallest valid agent request that embeds one internal event. */
|
||||
function makeAgentParamsWithInternalEvent(event: AgentInternalEvent) {
|
||||
return {
|
||||
message: "A music generation task finished. Process the completion update now.",
|
||||
sessionKey: "agent:main:discord:channel:1456744319972282449",
|
||||
internalEvents: [event],
|
||||
idempotencyKey: "music_generate:task-123:ok",
|
||||
};
|
||||
}
|
||||
|
||||
/** Representative generated-media completion event from a child task. */
|
||||
const musicCompletionEvent: AgentInternalEvent = {
|
||||
type: "task_completion",
|
||||
source: "music_generation",
|
||||
childSessionKey: "music_generate:task-123",
|
||||
childSessionId: "task-123",
|
||||
announceType: "music generation task",
|
||||
taskLabel: "OpenClaw release anthem",
|
||||
status: "ok",
|
||||
statusLabel: "completed successfully",
|
||||
result: "Generated 1 track.",
|
||||
attachments: [
|
||||
{
|
||||
type: "audio",
|
||||
path: "/tmp/openclaw/generated-release-anthem.mp3",
|
||||
mimeType: "audio/mpeg",
|
||||
name: "generated-release-anthem.mp3",
|
||||
},
|
||||
],
|
||||
mediaUrls: ["/tmp/openclaw/generated-release-anthem.mp3"],
|
||||
replyInstruction: "Deliver the generated music.",
|
||||
};
|
||||
|
||||
describe("AgentParamsSchema", () => {
|
||||
it("accepts generated music attachments on internal completion events", () => {
|
||||
const params = makeAgentParamsWithInternalEvent(musicCompletionEvent);
|
||||
|
||||
expect(Value.Check(AgentParamsSchema, params)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps task completion internal events strict", () => {
|
||||
const params = makeAgentParamsWithInternalEvent({
|
||||
...musicCompletionEvent,
|
||||
unexpected: true,
|
||||
} as AgentInternalEvent);
|
||||
|
||||
expect(Value.Check(AgentParamsSchema, params)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects malformed generated attachment entries on internal events", () => {
|
||||
const params = makeAgentParamsWithInternalEvent({
|
||||
...musicCompletionEvent,
|
||||
attachments: [null],
|
||||
} as unknown as AgentInternalEvent);
|
||||
|
||||
expect(Value.Check(AgentParamsSchema, params)).toBe(false);
|
||||
});
|
||||
});
|
||||
290
packages/gateway-protocol/src/schema/agent.ts
Normal file
290
packages/gateway-protocol/src/schema/agent.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { InputProvenanceSchema, NonEmptyString, SessionLabelString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Agent and channel-action gateway schemas.
|
||||
*
|
||||
* These payloads sit on the boundary between external channel adapters, gateway
|
||||
* RPC callers, and the agent runtime. Keep public request fields documented
|
||||
* because older CLI/channel clients may continue sending them across releases.
|
||||
*/
|
||||
const AGENT_INTERNAL_EVENT_TYPE_TASK_COMPLETION = "task_completion";
|
||||
const AGENT_INTERNAL_EVENT_SOURCES = [
|
||||
"subagent",
|
||||
"cron",
|
||||
"image_generation",
|
||||
"video_generation",
|
||||
"music_generation",
|
||||
] as const;
|
||||
const AGENT_INTERNAL_EVENT_STATUSES = ["ok", "timeout", "error", "unknown"] as const;
|
||||
|
||||
/** Generated media/file attachment metadata carried by internal agent events. */
|
||||
export const AgentGeneratedAttachmentSchema = Type.Object(
|
||||
{
|
||||
type: Type.Optional(Type.String({ enum: ["image", "audio", "video", "file"] })),
|
||||
path: Type.Optional(Type.String()),
|
||||
url: Type.Optional(Type.String()),
|
||||
mediaUrl: Type.Optional(Type.String()),
|
||||
filePath: Type.Optional(Type.String()),
|
||||
mimeType: Type.Optional(Type.String()),
|
||||
name: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Internal completion event surfaced when child automation reports back to a parent run. */
|
||||
export const AgentInternalEventSchema = Type.Object(
|
||||
{
|
||||
type: Type.Literal(AGENT_INTERNAL_EVENT_TYPE_TASK_COMPLETION),
|
||||
source: Type.String({ enum: [...AGENT_INTERNAL_EVENT_SOURCES] }),
|
||||
childSessionKey: Type.String(),
|
||||
childSessionId: Type.Optional(Type.String()),
|
||||
announceType: Type.String(),
|
||||
taskLabel: Type.String(),
|
||||
status: Type.String({ enum: [...AGENT_INTERNAL_EVENT_STATUSES] }),
|
||||
statusLabel: Type.String(),
|
||||
result: Type.String(),
|
||||
attachments: Type.Optional(Type.Array(AgentGeneratedAttachmentSchema)),
|
||||
mediaUrls: Type.Optional(Type.Array(Type.String())),
|
||||
statsLine: Type.Optional(Type.String()),
|
||||
replyInstruction: Type.String(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Stream event emitted by the agent runtime over the gateway protocol. */
|
||||
export const AgentEventSchema = Type.Object(
|
||||
{
|
||||
runId: NonEmptyString,
|
||||
seq: Type.Integer({ minimum: 0 }),
|
||||
stream: NonEmptyString,
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
spawnedBy: Type.Optional(NonEmptyString),
|
||||
isHeartbeat: Type.Optional(Type.Boolean()),
|
||||
data: Type.Record(Type.String(), Type.Unknown()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Channel context injected into message actions so tools can reply in-place. */
|
||||
export const MessageActionToolContextSchema = Type.Object(
|
||||
{
|
||||
currentChannelId: Type.Optional(Type.String()),
|
||||
currentMessagingTarget: Type.Optional(Type.String()),
|
||||
currentGraphChannelId: Type.Optional(Type.String()),
|
||||
currentChannelProvider: Type.Optional(Type.String()),
|
||||
currentThreadTs: Type.Optional(Type.String()),
|
||||
currentMessageId: Type.Optional(Type.Union([Type.String(), Type.Number()])),
|
||||
replyToMode: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("off"),
|
||||
Type.Literal("first"),
|
||||
Type.Literal("all"),
|
||||
Type.Literal("batched"),
|
||||
]),
|
||||
),
|
||||
hasRepliedRef: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
value: Type.Boolean(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
sameChannelThreadRequired: Type.Optional(Type.Boolean()),
|
||||
skipCrossContextDecoration: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Request to execute a channel message action through a configured adapter. */
|
||||
export const MessageActionParamsSchema = Type.Object(
|
||||
{
|
||||
channel: NonEmptyString,
|
||||
action: NonEmptyString,
|
||||
params: Type.Record(Type.String(), Type.Unknown()),
|
||||
accountId: Type.Optional(Type.String()),
|
||||
requesterAccountId: Type.Optional(Type.String()),
|
||||
requesterSenderId: Type.Optional(Type.String()),
|
||||
// Honored only when the RPC caller has the full operator scope set
|
||||
// (shared-secret bearer or `operator.admin`). For narrowly-scoped
|
||||
// callers (e.g. `operator.write`-only) the gateway forces this to
|
||||
// `false` regardless of the value sent here.
|
||||
senderIsOwner: Type.Optional(Type.Boolean()),
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
sessionId: Type.Optional(Type.String()),
|
||||
inboundTurnKind: Type.Optional(Type.String({ enum: ["user_request", "room_event"] })),
|
||||
agentId: Type.Optional(Type.String()),
|
||||
toolContext: Type.Optional(MessageActionToolContextSchema),
|
||||
idempotencyKey: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Outbound send request shared by channel adapters. */
|
||||
export const SendParamsSchema = Type.Object(
|
||||
{
|
||||
to: NonEmptyString,
|
||||
message: Type.Optional(Type.String()),
|
||||
mediaUrl: Type.Optional(Type.String()),
|
||||
mediaUrls: Type.Optional(Type.Array(Type.String())),
|
||||
/** Base64 attachment payload for gateway-local media materialization. */
|
||||
buffer: Type.Optional(Type.String()),
|
||||
/** Optional filename for a base64 attachment payload. */
|
||||
filename: Type.Optional(Type.String()),
|
||||
/** Optional MIME type for a base64 attachment payload. */
|
||||
contentType: Type.Optional(Type.String()),
|
||||
asVoice: Type.Optional(Type.Boolean()),
|
||||
gifPlayback: Type.Optional(Type.Boolean()),
|
||||
channel: Type.Optional(Type.String()),
|
||||
accountId: Type.Optional(Type.String()),
|
||||
/** Optional agent id for per-agent media root resolution on gateway sends. */
|
||||
agentId: Type.Optional(Type.String()),
|
||||
/** Reply target message id for native quoted/threaded sends where supported. */
|
||||
replyToId: Type.Optional(Type.String()),
|
||||
/** Thread id (channel-specific meaning, e.g. Telegram forum topic id). */
|
||||
threadId: Type.Optional(Type.String()),
|
||||
/** Force document-style media sends where supported. */
|
||||
forceDocument: Type.Optional(Type.Boolean()),
|
||||
/** Send silently (no notification) where supported. */
|
||||
silent: Type.Optional(Type.Boolean()),
|
||||
/** Channel-specific parse mode for formatted text. */
|
||||
parseMode: Type.Optional(Type.Literal("HTML")),
|
||||
/** Optional session key for mirroring delivered output back into the transcript. */
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
idempotencyKey: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Poll creation request for adapters that support native polls. */
|
||||
export const PollParamsSchema = Type.Object(
|
||||
{
|
||||
to: NonEmptyString,
|
||||
question: NonEmptyString,
|
||||
options: Type.Array(NonEmptyString, { minItems: 2, maxItems: 12 }),
|
||||
maxSelections: Type.Optional(Type.Integer({ minimum: 1, maximum: 12 })),
|
||||
/** Poll duration in seconds (channel-specific limits may apply). */
|
||||
durationSeconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 604_800 })),
|
||||
durationHours: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
/** Send silently (no notification) where supported. */
|
||||
silent: Type.Optional(Type.Boolean()),
|
||||
/** Poll anonymity where supported (e.g. Telegram polls default to anonymous). */
|
||||
isAnonymous: Type.Optional(Type.Boolean()),
|
||||
/** Thread id (channel-specific meaning, e.g. Telegram forum topic id). */
|
||||
threadId: Type.Optional(Type.String()),
|
||||
channel: Type.Optional(Type.String()),
|
||||
accountId: Type.Optional(Type.String()),
|
||||
idempotencyKey: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Main agent-run request accepted by the gateway. */
|
||||
export const AgentParamsSchema = Type.Object(
|
||||
{
|
||||
message: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
provider: Type.Optional(Type.String()),
|
||||
model: Type.Optional(Type.String()),
|
||||
to: Type.Optional(Type.String()),
|
||||
replyTo: Type.Optional(Type.String()),
|
||||
sessionId: Type.Optional(Type.String()),
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
thinking: Type.Optional(Type.String()),
|
||||
deliver: Type.Optional(Type.Boolean()),
|
||||
attachments: Type.Optional(Type.Array(Type.Unknown())),
|
||||
channel: Type.Optional(Type.String()),
|
||||
replyChannel: Type.Optional(Type.String()),
|
||||
accountId: Type.Optional(Type.String()),
|
||||
replyAccountId: Type.Optional(Type.String()),
|
||||
threadId: Type.Optional(Type.String()),
|
||||
groupId: Type.Optional(Type.String()),
|
||||
groupChannel: Type.Optional(Type.String()),
|
||||
groupSpace: Type.Optional(Type.String()),
|
||||
timeout: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
bestEffortDeliver: Type.Optional(Type.Boolean()),
|
||||
lane: Type.Optional(Type.String()),
|
||||
// One-shot CLI gateway requests can ask the gateway to close process-wide
|
||||
// bundle MCP resources after the run instead of keeping them warm.
|
||||
cleanupBundleMcpOnRunEnd: Type.Optional(Type.Boolean()),
|
||||
modelRun: Type.Optional(Type.Boolean()),
|
||||
promptMode: Type.Optional(
|
||||
Type.Union([Type.Literal("full"), Type.Literal("minimal"), Type.Literal("none")]),
|
||||
),
|
||||
extraSystemPrompt: Type.Optional(Type.String()),
|
||||
bootstrapContextMode: Type.Optional(
|
||||
Type.Union([Type.Literal("full"), Type.Literal("lightweight")]),
|
||||
),
|
||||
// Commitment fan-out scope is scheduler-internal and cannot be selected over Gateway RPC.
|
||||
bootstrapContextRunKind: Type.Optional(
|
||||
Type.Union([Type.Literal("default"), Type.Literal("heartbeat"), Type.Literal("cron")]),
|
||||
),
|
||||
acpTurnSource: Type.Optional(Type.Literal("manual_spawn")),
|
||||
internalRuntimeHandoffId: Type.Optional(NonEmptyString),
|
||||
execApprovalFollowupExpectedSessionId: Type.Optional(NonEmptyString),
|
||||
internalEvents: Type.Optional(Type.Array(AgentInternalEventSchema)),
|
||||
inputProvenance: Type.Optional(InputProvenanceSchema),
|
||||
suppressPromptPersistence: Type.Optional(Type.Boolean()),
|
||||
sessionEffects: Type.Optional(Type.Union([Type.Literal("visible"), Type.Literal("internal")])),
|
||||
sourceReplyDeliveryMode: Type.Optional(
|
||||
Type.Union([Type.Literal("automatic"), Type.Literal("message_tool_only")]),
|
||||
),
|
||||
disableMessageTool: Type.Optional(Type.Boolean()),
|
||||
voiceWakeTrigger: Type.Optional(Type.String()),
|
||||
idempotencyKey: NonEmptyString,
|
||||
label: Type.Optional(SessionLabelString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Identity lookup request for the current or selected agent/session. */
|
||||
export const AgentIdentityParamsSchema = Type.Object(
|
||||
{
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Public display identity returned for an agent. */
|
||||
export const AgentIdentityResultSchema = Type.Object(
|
||||
{
|
||||
agentId: NonEmptyString,
|
||||
name: Type.Optional(NonEmptyString),
|
||||
avatar: Type.Optional(NonEmptyString),
|
||||
avatarSource: Type.Optional(NonEmptyString),
|
||||
avatarStatus: Type.Optional(Type.String({ enum: ["none", "local", "remote", "data"] })),
|
||||
avatarReason: Type.Optional(NonEmptyString),
|
||||
emoji: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Waits for a submitted agent run to complete or time out. */
|
||||
export const AgentWaitParamsSchema = Type.Object(
|
||||
{
|
||||
runId: NonEmptyString,
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Wake request from external schedulers or devices into an agent session. */
|
||||
export const WakeParamsSchema = Type.Object(
|
||||
{
|
||||
mode: Type.Union([Type.Literal("now"), Type.Literal("next-heartbeat")]),
|
||||
text: NonEmptyString,
|
||||
// Typed field; misspelled variants remain opaque metadata because wake
|
||||
// senders already rely on additionalProperties.
|
||||
sessionKey: Type.Optional(NonEmptyString),
|
||||
/**
|
||||
* Optional agent id paired with `sessionKey`. Routes multi-agent setups
|
||||
* to the agent that owns the targeted session — closes the related half
|
||||
* of #46886 ("always routes to default agent").
|
||||
*/
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: true }, // external wake senders may attach opaque metadata
|
||||
);
|
||||
@@ -0,0 +1,207 @@
|
||||
// Gateway Protocol tests cover agents models skills behavior.
|
||||
import { Value } from "typebox/value";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
AgentsListResultSchema,
|
||||
SkillsDetailResultSchema,
|
||||
SkillsProposalInspectResultSchema,
|
||||
SkillsProposalRequestRevisionResultSchema,
|
||||
ToolsEffectiveResultSchema,
|
||||
} from "./agents-models-skills.js";
|
||||
|
||||
/**
|
||||
* Schema regression tests for agent metadata, skill proposals, and effective
|
||||
* tool catalogs. These payloads are UI-facing but also consumed by runtime
|
||||
* guards, so the fixtures exercise strictness at the public gateway boundary.
|
||||
*/
|
||||
|
||||
/** Minimal effective-tools result used by strict notice tests. */
|
||||
function toolsEffectiveResult() {
|
||||
return {
|
||||
agentId: "main",
|
||||
profile: "full",
|
||||
groups: [
|
||||
{
|
||||
id: "core",
|
||||
label: "Built-in tools",
|
||||
source: "core",
|
||||
tools: [
|
||||
{
|
||||
id: "exec",
|
||||
label: "Exec",
|
||||
description: "Run shell commands",
|
||||
rawDescription: "Run shell commands",
|
||||
source: "core",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("AgentsListResultSchema", () => {
|
||||
it("accepts resolved per-agent thinking metadata", () => {
|
||||
const result = {
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
agents: [
|
||||
{
|
||||
id: "investment-master",
|
||||
name: "Investment Master",
|
||||
model: { primary: "deepseek/deepseek-v4-flash" },
|
||||
thinkingLevels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "xhigh", label: "xhigh" },
|
||||
],
|
||||
thinkingOptions: ["off", "xhigh"],
|
||||
thinkingDefault: "xhigh",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(Value.Check(AgentsListResultSchema, result)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ToolsEffectiveResultSchema", () => {
|
||||
it("accepts runtime tool quarantine notices", () => {
|
||||
const result = {
|
||||
...toolsEffectiveResult(),
|
||||
notices: [
|
||||
{
|
||||
id: "unsupported-tool-schema:fuzzplugin_move_angles",
|
||||
severity: "warning",
|
||||
message:
|
||||
'Tool "fuzzplugin_move_angles" from plugin "fuzzplugin" has an unsupported runtime input schema and was quarantined before model projection.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(Value.Check(ToolsEffectiveResultSchema, result)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps tool quarantine notices strict", () => {
|
||||
const result = {
|
||||
...toolsEffectiveResult(),
|
||||
notices: [
|
||||
{
|
||||
id: "unsupported-tool-schema:fuzzplugin_move_angles",
|
||||
severity: "warning",
|
||||
message: "Unsupported schema.",
|
||||
extra: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(Value.Check(ToolsEffectiveResultSchema, result)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SkillsProposalInspectResultSchema", () => {
|
||||
it("accepts update proposal support file target metadata", () => {
|
||||
const result = {
|
||||
record: {
|
||||
id: "proposal-1",
|
||||
kind: "update",
|
||||
status: "pending",
|
||||
title: "weather-helper",
|
||||
description: "Improve weather checks",
|
||||
schema: "openclaw.skill-workshop.proposal.v1",
|
||||
createdAt: "2026-05-30T00:00:00.000Z",
|
||||
updatedAt: "2026-05-30T00:00:00.000Z",
|
||||
createdBy: "skill-workshop",
|
||||
proposedVersion: "v1",
|
||||
draftFile: "PROPOSAL.md",
|
||||
target: {
|
||||
skillName: "weather-helper",
|
||||
skillDir: "/tmp/workspace/skills/weather-helper",
|
||||
skillFile: "/tmp/workspace/skills/weather-helper/SKILL.md",
|
||||
skillKey: "weather-helper",
|
||||
currentContentHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
},
|
||||
draftHash: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
||||
scan: {
|
||||
state: "clean",
|
||||
scannedAt: "2026-05-30T00:00:00.000Z",
|
||||
critical: 0,
|
||||
warn: 0,
|
||||
info: 0,
|
||||
findings: [],
|
||||
},
|
||||
supportFiles: [
|
||||
{
|
||||
path: "references/weather.md",
|
||||
sizeBytes: 42,
|
||||
hash: "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
|
||||
targetExisted: true,
|
||||
targetContentHash: "123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0",
|
||||
},
|
||||
],
|
||||
},
|
||||
content: "# Weather Helper\n",
|
||||
supportFiles: [
|
||||
{
|
||||
path: "references/weather.md",
|
||||
content: "Use current weather before recommendations.\n",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(Value.Check(SkillsProposalInspectResultSchema, result)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SkillsProposalRequestRevisionResultSchema", () => {
|
||||
it.each(["started", "in_flight", "ok", "timeout", "error"])(
|
||||
"accepts forwarded chat.send ack status %s",
|
||||
(status) => {
|
||||
expect(
|
||||
Value.Check(SkillsProposalRequestRevisionResultSchema, {
|
||||
runId: "run-revision",
|
||||
status,
|
||||
}),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects unknown forwarded chat.send ack statuses", () => {
|
||||
expect(
|
||||
Value.Check(SkillsProposalRequestRevisionResultSchema, {
|
||||
runId: "run-revision",
|
||||
status: "queued",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SkillsDetailResultSchema", () => {
|
||||
it("accepts official ClawHub skill publisher metadata", () => {
|
||||
const result = {
|
||||
skill: {
|
||||
slug: "tao-setup-nvidia-gpu-host",
|
||||
displayName: "TAO Setup NVIDIA GPU Host",
|
||||
summary: "Prepare an NVIDIA GPU host for TAO workflows.",
|
||||
tags: { gpu: "GPU" },
|
||||
channel: "official",
|
||||
isOfficial: true,
|
||||
createdAt: 1_700_000_000,
|
||||
updatedAt: 1_700_010_000,
|
||||
},
|
||||
latestVersion: {
|
||||
version: "1.0.0",
|
||||
createdAt: 1_700_010_000,
|
||||
},
|
||||
owner: {
|
||||
handle: "nvidia",
|
||||
displayName: "NVIDIA",
|
||||
image: "https://example.test/nvidia.png",
|
||||
official: true,
|
||||
channel: "official",
|
||||
isOfficial: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(Value.Check(SkillsDetailResultSchema, result)).toBe(true);
|
||||
});
|
||||
});
|
||||
1017
packages/gateway-protocol/src/schema/agents-models-skills.ts
Normal file
1017
packages/gateway-protocol/src/schema/agents-models-skills.ts
Normal file
File diff suppressed because it is too large
Load Diff
89
packages/gateway-protocol/src/schema/artifacts.ts
Normal file
89
packages/gateway-protocol/src/schema/artifacts.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Artifact lookup and download protocol schemas.
|
||||
*
|
||||
* Artifacts are files or payloads produced by sessions, runs, tasks, or agents;
|
||||
* these schemas keep lookup filters explicit and download results transport-safe.
|
||||
*/
|
||||
const ArtifactQueryParamsProperties = {
|
||||
sessionKey: Type.Optional(NonEmptyString),
|
||||
runId: Type.Optional(NonEmptyString),
|
||||
taskId: Type.Optional(NonEmptyString),
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
};
|
||||
|
||||
/** Shared artifact filter payload used by list-style requests. */
|
||||
export const ArtifactQueryParamsSchema = Type.Object(ArtifactQueryParamsProperties, {
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
/** Artifact lookup payload with a required artifact id plus optional scope filters. */
|
||||
export const ArtifactGetParamsSchema = Type.Object(
|
||||
{
|
||||
...ArtifactQueryParamsProperties,
|
||||
artifactId: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Public artifact metadata returned before or alongside download data. */
|
||||
export const ArtifactSummarySchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
type: NonEmptyString,
|
||||
title: NonEmptyString,
|
||||
mimeType: Type.Optional(NonEmptyString),
|
||||
sizeBytes: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
sessionKey: Type.Optional(NonEmptyString),
|
||||
runId: Type.Optional(NonEmptyString),
|
||||
taskId: Type.Optional(NonEmptyString),
|
||||
messageSeq: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
source: Type.Optional(NonEmptyString),
|
||||
download: Type.Object(
|
||||
{
|
||||
mode: Type.Union([Type.Literal("bytes"), Type.Literal("url"), Type.Literal("unsupported")]),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** List request payload for artifacts visible in the selected scope. */
|
||||
export const ArtifactsListParamsSchema = ArtifactQueryParamsSchema;
|
||||
|
||||
/** List response containing artifact summaries only. */
|
||||
export const ArtifactsListResultSchema = Type.Object(
|
||||
{
|
||||
artifacts: Type.Array(ArtifactSummarySchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Get request payload for one artifact summary. */
|
||||
export const ArtifactsGetParamsSchema = ArtifactGetParamsSchema;
|
||||
|
||||
/** Get response containing one artifact summary. */
|
||||
export const ArtifactsGetResultSchema = Type.Object(
|
||||
{
|
||||
artifact: ArtifactSummarySchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Download request payload for one artifact. */
|
||||
export const ArtifactsDownloadParamsSchema = ArtifactGetParamsSchema;
|
||||
|
||||
/** Download response, either inline base64 bytes, URL, or metadata for unsupported modes. */
|
||||
export const ArtifactsDownloadResultSchema = Type.Object(
|
||||
{
|
||||
artifact: ArtifactSummarySchema,
|
||||
encoding: Type.Optional(Type.Literal("base64")),
|
||||
data: Type.Optional(Type.String()),
|
||||
url: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
852
packages/gateway-protocol/src/schema/channels.ts
Normal file
852
packages/gateway-protocol/src/schema/channels.ts
Normal file
@@ -0,0 +1,852 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString, SecretInputSchema } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Channel and Talk protocol schemas.
|
||||
*
|
||||
* Talk schemas are consumed by browser realtime clients, gateway relay sessions,
|
||||
* and channel adapters, so the mode/transport/brain unions below are shared
|
||||
* API vocabulary rather than provider-local implementation details.
|
||||
*/
|
||||
|
||||
/** Toggles Talk mode for the gateway, with an optional rollout phase marker. */
|
||||
export const TalkModeParamsSchema = Type.Object(
|
||||
{
|
||||
enabled: Type.Boolean(),
|
||||
phase: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Reads Talk configuration; secrets are included only for trusted callers. */
|
||||
export const TalkConfigParamsSchema = Type.Object(
|
||||
{
|
||||
includeSecrets: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** One-shot text-to-speech request with provider-specific voice tuning knobs. */
|
||||
export const TalkSpeakParamsSchema = Type.Object(
|
||||
{
|
||||
text: NonEmptyString,
|
||||
voiceId: Type.Optional(Type.String()),
|
||||
modelId: Type.Optional(Type.String()),
|
||||
outputFormat: Type.Optional(Type.String()),
|
||||
speed: Type.Optional(Type.Number()),
|
||||
rateWpm: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
stability: Type.Optional(Type.Number()),
|
||||
similarity: Type.Optional(Type.Number()),
|
||||
style: Type.Optional(Type.Number()),
|
||||
speakerBoost: Type.Optional(Type.Boolean()),
|
||||
seed: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
normalize: Type.Optional(Type.String()),
|
||||
language: Type.Optional(Type.String()),
|
||||
latencyTier: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Supported Talk session shapes exposed to clients and providers. */
|
||||
const TalkModeSchema = Type.Union([
|
||||
Type.Literal("realtime"),
|
||||
Type.Literal("stt-tts"),
|
||||
Type.Literal("transcription"),
|
||||
]);
|
||||
|
||||
/** Transport families; browser clients branch on this value to choose setup flow. */
|
||||
const TalkTransportSchema = Type.Union([
|
||||
Type.Literal("webrtc"),
|
||||
Type.Literal("provider-websocket"),
|
||||
Type.Literal("gateway-relay"),
|
||||
Type.Literal("managed-room"),
|
||||
]);
|
||||
|
||||
/** How a Talk session delegates reasoning/tool use to the agent runtime. */
|
||||
const TalkBrainSchema = Type.Union([
|
||||
Type.Literal("agent-consult"),
|
||||
Type.Literal("direct-tools"),
|
||||
Type.Literal("none"),
|
||||
]);
|
||||
|
||||
/** Agent control actions accepted from Talk clients and managed rooms. */
|
||||
const TalkAgentControlModeSchema = Type.Union([
|
||||
Type.Literal("status"),
|
||||
Type.Literal("steer"),
|
||||
Type.Literal("cancel"),
|
||||
Type.Literal("followup"),
|
||||
]);
|
||||
|
||||
/** Stable event names emitted by Talk sessions across providers/transports. */
|
||||
const TalkEventTypeSchema = Type.Union([
|
||||
Type.Literal("session.started"),
|
||||
Type.Literal("session.ready"),
|
||||
Type.Literal("session.closed"),
|
||||
Type.Literal("session.error"),
|
||||
Type.Literal("session.replaced"),
|
||||
Type.Literal("turn.started"),
|
||||
Type.Literal("turn.ended"),
|
||||
Type.Literal("turn.cancelled"),
|
||||
Type.Literal("capture.started"),
|
||||
Type.Literal("capture.stopped"),
|
||||
Type.Literal("capture.cancelled"),
|
||||
Type.Literal("capture.once"),
|
||||
Type.Literal("input.audio.delta"),
|
||||
Type.Literal("input.audio.committed"),
|
||||
Type.Literal("transcript.delta"),
|
||||
Type.Literal("transcript.done"),
|
||||
Type.Literal("output.text.delta"),
|
||||
Type.Literal("output.text.done"),
|
||||
Type.Literal("output.audio.started"),
|
||||
Type.Literal("output.audio.delta"),
|
||||
Type.Literal("output.audio.done"),
|
||||
Type.Literal("tool.call"),
|
||||
Type.Literal("tool.progress"),
|
||||
Type.Literal("tool.result"),
|
||||
Type.Literal("tool.error"),
|
||||
Type.Literal("usage.metrics"),
|
||||
Type.Literal("latency.metrics"),
|
||||
Type.Literal("health.changed"),
|
||||
]);
|
||||
|
||||
/** Event types that must carry a turn id for client-side stream correlation. */
|
||||
const TURN_SCOPED_TALK_EVENT_TYPES = [
|
||||
"turn.started",
|
||||
"turn.ended",
|
||||
"turn.cancelled",
|
||||
"input.audio.delta",
|
||||
"input.audio.committed",
|
||||
"transcript.delta",
|
||||
"transcript.done",
|
||||
"output.text.delta",
|
||||
"output.text.done",
|
||||
"output.audio.started",
|
||||
"output.audio.delta",
|
||||
"output.audio.done",
|
||||
"tool.call",
|
||||
"tool.progress",
|
||||
"tool.result",
|
||||
"tool.error",
|
||||
];
|
||||
|
||||
/** Capture lifecycle events must include capture id to avoid cross-turn ambiguity. */
|
||||
const CAPTURE_SCOPED_TALK_EVENT_TYPES = [
|
||||
"capture.started",
|
||||
"capture.stopped",
|
||||
"capture.cancelled",
|
||||
"capture.once",
|
||||
];
|
||||
|
||||
/** Builds JSON Schema conditional requirements while avoiding reserved word syntax. */
|
||||
function requireJsonSchemaProperties(properties: string[]): Record<string, { required: string[] }> {
|
||||
const conditionalRequirementKey = ["th", "en"].join("");
|
||||
return Object.fromEntries([[conditionalRequirementKey, { required: properties }]]);
|
||||
}
|
||||
|
||||
/** Canonical Talk event envelope emitted to browser, relay, and channel consumers. */
|
||||
export const TalkEventSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
type: TalkEventTypeSchema,
|
||||
sessionId: NonEmptyString,
|
||||
turnId: Type.Optional(Type.String()),
|
||||
captureId: Type.Optional(Type.String()),
|
||||
seq: Type.Integer({ minimum: 1 }),
|
||||
timestamp: NonEmptyString,
|
||||
mode: TalkModeSchema,
|
||||
transport: TalkTransportSchema,
|
||||
brain: TalkBrainSchema,
|
||||
provider: Type.Optional(Type.String()),
|
||||
final: Type.Optional(Type.Boolean()),
|
||||
callId: Type.Optional(Type.String()),
|
||||
itemId: Type.Optional(Type.String()),
|
||||
parentId: Type.Optional(Type.String()),
|
||||
payload: Type.Unknown(),
|
||||
},
|
||||
{
|
||||
additionalProperties: false,
|
||||
allOf: [
|
||||
{
|
||||
if: {
|
||||
properties: { type: { enum: TURN_SCOPED_TALK_EVENT_TYPES } },
|
||||
required: ["type"],
|
||||
},
|
||||
...requireJsonSchemaProperties(["turnId"]),
|
||||
},
|
||||
{
|
||||
if: {
|
||||
properties: { type: { enum: CAPTURE_SCOPED_TALK_EVENT_TYPES } },
|
||||
required: ["type"],
|
||||
},
|
||||
...requireJsonSchemaProperties(["captureId"]),
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
/** Creates a browser-facing Talk client session. */
|
||||
export const TalkClientCreateParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
provider: Type.Optional(Type.String()),
|
||||
model: Type.Optional(Type.String()),
|
||||
voice: Type.Optional(Type.String()),
|
||||
vadThreshold: Type.Optional(Type.Number()),
|
||||
silenceDurationMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
prefixPaddingMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
reasoningEffort: Type.Optional(Type.String()),
|
||||
mode: Type.Optional(TalkModeSchema),
|
||||
transport: Type.Optional(TalkTransportSchema),
|
||||
brain: Type.Optional(TalkBrainSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Tool-call request from a browser/client session back into the agent runtime. */
|
||||
export const TalkClientToolCallParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
callId: NonEmptyString,
|
||||
name: NonEmptyString,
|
||||
args: Type.Optional(Type.Unknown()),
|
||||
relaySessionId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Agent run identity returned after accepting a Talk client tool call. */
|
||||
export const TalkClientToolCallResultSchema = Type.Object(
|
||||
{
|
||||
runId: NonEmptyString,
|
||||
idempotencyKey: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Text steering request for a Talk session bound to an agent turn. */
|
||||
export const TalkClientSteerParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
text: NonEmptyString,
|
||||
mode: Type.Optional(TalkAgentControlModeSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Result of applying agent control to an embedded or reply-backed Talk run. */
|
||||
export const TalkAgentControlResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Boolean(),
|
||||
mode: TalkAgentControlModeSchema,
|
||||
sessionKey: NonEmptyString,
|
||||
sessionId: Type.Optional(NonEmptyString),
|
||||
active: Type.Boolean(),
|
||||
queued: Type.Optional(Type.Boolean()),
|
||||
aborted: Type.Optional(Type.Boolean()),
|
||||
target: Type.Optional(Type.Union([Type.Literal("embedded_run"), Type.Literal("reply_run")])),
|
||||
reason: Type.Optional(Type.String()),
|
||||
message: Type.String(),
|
||||
speak: Type.Boolean(),
|
||||
show: Type.Boolean(),
|
||||
suppress: Type.Boolean(),
|
||||
providerResult: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
status: Type.Literal("cancelled"),
|
||||
message: Type.String(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
enqueuedAtMs: Type.Optional(Type.Number()),
|
||||
deliveredAtMs: Type.Optional(Type.Number()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Joins an existing managed-room Talk session. */
|
||||
export const TalkSessionJoinParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
token: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Creates a gateway-managed Talk session for realtime, transcription, or relay use. */
|
||||
export const TalkSessionCreateParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
spawnedBy: Type.Optional(NonEmptyString),
|
||||
provider: Type.Optional(Type.String()),
|
||||
model: Type.Optional(Type.String()),
|
||||
voice: Type.Optional(Type.String()),
|
||||
vadThreshold: Type.Optional(Type.Number()),
|
||||
silenceDurationMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
prefixPaddingMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
reasoningEffort: Type.Optional(Type.String()),
|
||||
mode: Type.Optional(TalkModeSchema),
|
||||
transport: Type.Optional(TalkTransportSchema),
|
||||
brain: Type.Optional(TalkBrainSchema),
|
||||
ttlMs: Type.Optional(Type.Integer({ minimum: 1000, maximum: 3600000 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Appends base64 audio to an active Talk session. */
|
||||
export const TalkSessionAppendAudioParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
audioBase64: NonEmptyString,
|
||||
timestamp: Type.Optional(Type.Number()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Starts or advances a Talk turn within a session. */
|
||||
export const TalkSessionTurnParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
turnId: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Cancels the active or named Talk turn. */
|
||||
export const TalkSessionCancelTurnParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
turnId: Type.Optional(Type.String()),
|
||||
reason: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Cancels currently streaming Talk output without necessarily ending the turn. */
|
||||
export const TalkSessionCancelOutputParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
turnId: Type.Optional(Type.String()),
|
||||
reason: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Submits a tool result back to a Talk provider session. */
|
||||
export const TalkSessionSubmitToolResultParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
callId: NonEmptyString,
|
||||
result: Type.Unknown(),
|
||||
options: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
suppressResponse: Type.Optional(Type.Boolean()),
|
||||
willContinue: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Steers a managed Talk session by session id rather than transcript key. */
|
||||
export const TalkSessionSteerParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
sessionKey: Type.Optional(NonEmptyString),
|
||||
text: NonEmptyString,
|
||||
mode: Type.Optional(TalkAgentControlModeSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Closes a gateway-managed Talk session. */
|
||||
export const TalkSessionCloseParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Mutable room state returned when a client joins a managed Talk room. */
|
||||
const TalkSessionManagedRoomStateSchema = Type.Object(
|
||||
{
|
||||
activeClientId: Type.Optional(Type.String()),
|
||||
activeTurnId: Type.Optional(Type.String()),
|
||||
recentTalkEvents: Type.Array(TalkEventSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Managed-room session record shared with browser clients. */
|
||||
const TalkSessionManagedRoomRecordSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
roomId: NonEmptyString,
|
||||
roomUrl: NonEmptyString,
|
||||
sessionKey: NonEmptyString,
|
||||
sessionId: Type.Optional(Type.String()),
|
||||
channel: Type.Optional(Type.String()),
|
||||
target: Type.Optional(Type.String()),
|
||||
provider: Type.Optional(Type.String()),
|
||||
model: Type.Optional(Type.String()),
|
||||
voice: Type.Optional(Type.String()),
|
||||
mode: TalkModeSchema,
|
||||
transport: TalkTransportSchema,
|
||||
brain: TalkBrainSchema,
|
||||
createdAt: Type.Number(),
|
||||
expiresAt: Type.Number(),
|
||||
room: TalkSessionManagedRoomStateSchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Empty request payload for reading configured Talk provider capabilities. */
|
||||
export const TalkCatalogParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** One provider entry in the Talk capability catalog. */
|
||||
const TalkCatalogProviderSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
label: NonEmptyString,
|
||||
configured: Type.Boolean(),
|
||||
aliases: Type.Optional(Type.Array(NonEmptyString)),
|
||||
models: Type.Optional(Type.Array(Type.String())),
|
||||
voices: Type.Optional(Type.Array(Type.String())),
|
||||
defaultModel: Type.Optional(Type.String()),
|
||||
modes: Type.Optional(Type.Array(TalkModeSchema)),
|
||||
transports: Type.Optional(Type.Array(TalkTransportSchema)),
|
||||
brains: Type.Optional(Type.Array(TalkBrainSchema)),
|
||||
inputAudioFormats: Type.Optional(
|
||||
Type.Array(
|
||||
Type.Object(
|
||||
{
|
||||
encoding: Type.Union([Type.Literal("pcm16"), Type.Literal("g711_ulaw")]),
|
||||
sampleRateHz: Type.Integer({ minimum: 1 }),
|
||||
channels: Type.Integer({ minimum: 1 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
),
|
||||
outputAudioFormats: Type.Optional(
|
||||
Type.Array(
|
||||
Type.Object(
|
||||
{
|
||||
encoding: Type.Union([Type.Literal("pcm16"), Type.Literal("g711_ulaw")]),
|
||||
sampleRateHz: Type.Integer({ minimum: 1 }),
|
||||
channels: Type.Integer({ minimum: 1 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
),
|
||||
supportsBrowserSession: Type.Optional(Type.Boolean()),
|
||||
supportsBargeIn: Type.Optional(Type.Boolean()),
|
||||
supportsToolCalls: Type.Optional(Type.Boolean()),
|
||||
supportsVideoFrames: Type.Optional(Type.Boolean()),
|
||||
supportsSessionResumption: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Active provider plus all candidates for a Talk capability family. */
|
||||
const TalkCatalogProviderGroupSchema = Type.Object(
|
||||
{
|
||||
ready: Type.Optional(Type.Boolean()),
|
||||
activeProvider: Type.Optional(Type.String()),
|
||||
providers: Type.Array(TalkCatalogProviderSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Provider, mode, transport, and audio-format catalog returned to clients. */
|
||||
export const TalkCatalogResultSchema = Type.Object(
|
||||
{
|
||||
modes: Type.Array(TalkModeSchema),
|
||||
transports: Type.Array(TalkTransportSchema),
|
||||
brains: Type.Array(TalkBrainSchema),
|
||||
speech: TalkCatalogProviderGroupSchema,
|
||||
transcription: TalkCatalogProviderGroupSchema,
|
||||
realtime: TalkCatalogProviderGroupSchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Audio format contract for realtime browser sessions. */
|
||||
const BrowserRealtimeAudioContractSchema = Type.Object(
|
||||
{
|
||||
inputEncoding: Type.Union([Type.Literal("pcm16"), Type.Literal("g711_ulaw")]),
|
||||
inputSampleRateHz: Type.Integer({ minimum: 1 }),
|
||||
outputEncoding: Type.Union([Type.Literal("pcm16"), Type.Literal("g711_ulaw")]),
|
||||
outputSampleRateHz: Type.Integer({ minimum: 1 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Session creation result with transport-specific ids and credentials. */
|
||||
export const TalkSessionCreateResultSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
provider: Type.Optional(Type.String()),
|
||||
mode: TalkModeSchema,
|
||||
transport: TalkTransportSchema,
|
||||
brain: TalkBrainSchema,
|
||||
relaySessionId: Type.Optional(NonEmptyString),
|
||||
transcriptionSessionId: Type.Optional(NonEmptyString),
|
||||
handoffId: Type.Optional(NonEmptyString),
|
||||
roomId: Type.Optional(NonEmptyString),
|
||||
roomUrl: Type.Optional(NonEmptyString),
|
||||
token: Type.Optional(NonEmptyString),
|
||||
audio: Type.Optional(Type.Unknown()),
|
||||
model: Type.Optional(Type.String()),
|
||||
voice: Type.Optional(Type.String()),
|
||||
expiresAt: Type.Optional(Type.Number()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Result for a Talk turn request, optionally including emitted events. */
|
||||
export const TalkSessionTurnResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Boolean(),
|
||||
turnId: Type.Optional(Type.String()),
|
||||
events: Type.Optional(Type.Array(TalkEventSchema)),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Managed-room record returned to clients after joining an existing Talk session. */
|
||||
export const TalkSessionJoinResultSchema = TalkSessionManagedRoomRecordSchema;
|
||||
|
||||
/** Generic success result for Talk session lifecycle calls. */
|
||||
export const TalkSessionOkResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Boolean(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Browser WebRTC setup payload using provider SDP exchange. */
|
||||
const BrowserRealtimeWebRtcSdpSessionSchema = Type.Object(
|
||||
{
|
||||
provider: NonEmptyString,
|
||||
transport: Type.Literal("webrtc"),
|
||||
clientSecret: NonEmptyString,
|
||||
offerUrl: Type.Optional(Type.String()),
|
||||
offerHeaders: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||
model: Type.Optional(Type.String()),
|
||||
voice: Type.Optional(Type.String()),
|
||||
expiresAt: Type.Optional(Type.Number()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Browser websocket setup payload with JSON/PCM audio contract. */
|
||||
const BrowserRealtimeJsonPcmWebSocketSessionSchema = Type.Object(
|
||||
{
|
||||
provider: NonEmptyString,
|
||||
transport: Type.Literal("provider-websocket"),
|
||||
protocol: NonEmptyString,
|
||||
clientSecret: NonEmptyString,
|
||||
websocketUrl: NonEmptyString,
|
||||
audio: BrowserRealtimeAudioContractSchema,
|
||||
initialMessage: Type.Optional(Type.Unknown()),
|
||||
model: Type.Optional(Type.String()),
|
||||
voice: Type.Optional(Type.String()),
|
||||
expiresAt: Type.Optional(Type.Number()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Browser setup payload for gateway-relayed realtime audio. */
|
||||
const BrowserRealtimeGatewayRelaySessionSchema = Type.Object(
|
||||
{
|
||||
provider: NonEmptyString,
|
||||
transport: Type.Literal("gateway-relay"),
|
||||
relaySessionId: NonEmptyString,
|
||||
audio: BrowserRealtimeAudioContractSchema,
|
||||
model: Type.Optional(Type.String()),
|
||||
voice: Type.Optional(Type.String()),
|
||||
expiresAt: Type.Optional(Type.Number()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Browser setup payload for managed-room Talk sessions. */
|
||||
const BrowserRealtimeManagedRoomSessionSchema = Type.Object(
|
||||
{
|
||||
provider: NonEmptyString,
|
||||
transport: Type.Literal("managed-room"),
|
||||
roomUrl: NonEmptyString,
|
||||
token: Type.Optional(Type.String()),
|
||||
model: Type.Optional(Type.String()),
|
||||
voice: Type.Optional(Type.String()),
|
||||
expiresAt: Type.Optional(Type.Number()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Union of all browser Talk session setup payloads. */
|
||||
export const TalkClientCreateResultSchema = Type.Union([
|
||||
BrowserRealtimeWebRtcSdpSessionSchema,
|
||||
BrowserRealtimeJsonPcmWebSocketSessionSchema,
|
||||
BrowserRealtimeGatewayRelaySessionSchema,
|
||||
BrowserRealtimeManagedRoomSessionSchema,
|
||||
]);
|
||||
|
||||
/** Secret-bearing provider fields; extra provider options remain provider-owned. */
|
||||
const talkProviderFieldSchemas = {
|
||||
apiKey: Type.Optional(SecretInputSchema),
|
||||
};
|
||||
|
||||
/** Per-provider Talk config bag. */
|
||||
const TalkProviderConfigSchema = Type.Object(talkProviderFieldSchemas, {
|
||||
additionalProperties: true,
|
||||
});
|
||||
|
||||
/** Realtime Talk defaults and provider selection stored in config. */
|
||||
const TalkRealtimeConfigSchema = Type.Object(
|
||||
{
|
||||
provider: Type.Optional(Type.String()),
|
||||
providers: Type.Optional(Type.Record(Type.String(), TalkProviderConfigSchema)),
|
||||
model: Type.Optional(Type.String()),
|
||||
speakerVoice: Type.Optional(Type.String()),
|
||||
speakerVoiceId: Type.Optional(Type.String()),
|
||||
voice: Type.Optional(Type.String()),
|
||||
instructions: Type.Optional(Type.String()),
|
||||
mode: Type.Optional(TalkModeSchema),
|
||||
transport: Type.Optional(TalkTransportSchema),
|
||||
brain: Type.Optional(TalkBrainSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Resolved active Talk provider plus its normalized provider config. */
|
||||
const ResolvedTalkConfigSchema = Type.Object(
|
||||
{
|
||||
provider: Type.String(),
|
||||
config: TalkProviderConfigSchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Talk config subtree returned through gateway config APIs. */
|
||||
const TalkConfigSchema = Type.Object(
|
||||
{
|
||||
provider: Type.Optional(Type.String()),
|
||||
providers: Type.Optional(Type.Record(Type.String(), TalkProviderConfigSchema)),
|
||||
realtime: Type.Optional(TalkRealtimeConfigSchema),
|
||||
resolved: Type.Optional(ResolvedTalkConfigSchema),
|
||||
consultThinkingLevel: Type.Optional(Type.String()),
|
||||
consultFastMode: Type.Optional(Type.Boolean()),
|
||||
speechLocale: Type.Optional(Type.String()),
|
||||
interruptOnSpeech: Type.Optional(Type.Boolean()),
|
||||
silenceTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Full Talk config read result, including related session/UI context. */
|
||||
export const TalkConfigResultSchema = Type.Object(
|
||||
{
|
||||
config: Type.Object(
|
||||
{
|
||||
talk: Type.Optional(TalkConfigSchema),
|
||||
session: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
mainKey: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
ui: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
seamColor: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Text-to-speech result with encoded audio and provider output metadata. */
|
||||
export const TalkSpeakResultSchema = Type.Object(
|
||||
{
|
||||
audioBase64: NonEmptyString,
|
||||
provider: NonEmptyString,
|
||||
outputFormat: Type.Optional(Type.String()),
|
||||
voiceCompatible: Type.Optional(Type.Boolean()),
|
||||
mimeType: Type.Optional(Type.String()),
|
||||
fileExtension: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Channel status request, optionally probing one channel before returning. */
|
||||
export const ChannelsStatusParamsSchema = Type.Object(
|
||||
{
|
||||
probe: Type.Optional(Type.Boolean()),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
channel: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/**
|
||||
* Per-account status snapshot for channel docking.
|
||||
*
|
||||
* This is intentionally schema-light so new channel-specific metadata can ship
|
||||
* without a gateway protocol update; known fields stay documented for UI use.
|
||||
*/
|
||||
export const ChannelAccountSnapshotSchema = Type.Object(
|
||||
{
|
||||
accountId: NonEmptyString,
|
||||
name: Type.Optional(Type.String()),
|
||||
enabled: Type.Optional(Type.Boolean()),
|
||||
configured: Type.Optional(Type.Boolean()),
|
||||
linked: Type.Optional(Type.Boolean()),
|
||||
running: Type.Optional(Type.Boolean()),
|
||||
connected: Type.Optional(Type.Boolean()),
|
||||
reconnectAttempts: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastConnectedAt: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastError: Type.Optional(Type.String()),
|
||||
healthState: Type.Optional(Type.String()),
|
||||
lastStartAt: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastStopAt: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastInboundAt: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastOutboundAt: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastTransportActivityAt: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
busy: Type.Optional(Type.Boolean()),
|
||||
activeRuns: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastRunActivityAt: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastProbeAt: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
mode: Type.Optional(Type.String()),
|
||||
dmPolicy: Type.Optional(Type.String()),
|
||||
allowFrom: Type.Optional(Type.Array(Type.String())),
|
||||
tokenSource: Type.Optional(Type.String()),
|
||||
botTokenSource: Type.Optional(Type.String()),
|
||||
appTokenSource: Type.Optional(Type.String()),
|
||||
baseUrl: Type.Optional(Type.String()),
|
||||
allowUnmentionedGroups: Type.Optional(Type.Boolean()),
|
||||
cliPath: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
dbPath: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
port: Type.Optional(Type.Union([Type.Integer({ minimum: 0 }), Type.Null()])),
|
||||
probe: Type.Optional(Type.Unknown()),
|
||||
audit: Type.Optional(Type.Unknown()),
|
||||
application: Type.Optional(Type.Unknown()),
|
||||
},
|
||||
{ additionalProperties: true },
|
||||
);
|
||||
|
||||
/** UI label and icon metadata for one channel. */
|
||||
export const ChannelUiMetaSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
label: NonEmptyString,
|
||||
detailLabel: NonEmptyString,
|
||||
systemImage: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Event-loop health snapshot included with channel status responses. */
|
||||
export const ChannelEventLoopHealthSchema = Type.Object(
|
||||
{
|
||||
degraded: Type.Boolean(),
|
||||
reasons: Type.Array(
|
||||
Type.Union([
|
||||
Type.Literal("event_loop_delay"),
|
||||
Type.Literal("event_loop_utilization"),
|
||||
Type.Literal("cpu"),
|
||||
]),
|
||||
),
|
||||
intervalMs: Type.Integer({ minimum: 0 }),
|
||||
delayP99Ms: Type.Number({ minimum: 0 }),
|
||||
delayMaxMs: Type.Number({ minimum: 0 }),
|
||||
utilization: Type.Number({ minimum: 0 }),
|
||||
cpuCoreRatio: Type.Number({ minimum: 0 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Full channel status result for dashboard and operator diagnostics. */
|
||||
export const ChannelsStatusResultSchema = Type.Object(
|
||||
{
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
channelOrder: Type.Array(NonEmptyString),
|
||||
channelLabels: Type.Record(NonEmptyString, NonEmptyString),
|
||||
channelDetailLabels: Type.Optional(Type.Record(NonEmptyString, NonEmptyString)),
|
||||
channelSystemImages: Type.Optional(Type.Record(NonEmptyString, NonEmptyString)),
|
||||
channelMeta: Type.Optional(Type.Array(ChannelUiMetaSchema)),
|
||||
channels: Type.Record(NonEmptyString, Type.Unknown()),
|
||||
channelAccounts: Type.Record(NonEmptyString, Type.Array(ChannelAccountSnapshotSchema)),
|
||||
channelDefaultAccountId: Type.Record(NonEmptyString, NonEmptyString),
|
||||
eventLoop: Type.Optional(ChannelEventLoopHealthSchema),
|
||||
partial: Type.Optional(Type.Boolean()),
|
||||
warnings: Type.Optional(Type.Array(Type.String())),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Logs out one channel account. */
|
||||
export const ChannelsLogoutParamsSchema = Type.Object(
|
||||
{
|
||||
channel: NonEmptyString,
|
||||
accountId: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Stops one channel account runtime. */
|
||||
export const ChannelsStopParamsSchema = Type.Object(
|
||||
{
|
||||
channel: NonEmptyString,
|
||||
accountId: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Starts one channel account runtime. */
|
||||
export const ChannelsStartParamsSchema = Type.Object(
|
||||
{
|
||||
channel: NonEmptyString,
|
||||
accountId: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Starts browser/web login for a channel account. */
|
||||
export const WebLoginStartParamsSchema = Type.Object(
|
||||
{
|
||||
force: Type.Optional(Type.Boolean()),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
verbose: Type.Optional(Type.Boolean()),
|
||||
accountId: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
const QrDataUrlSchema = Type.String({
|
||||
maxLength: 16_384,
|
||||
pattern: "^data:image/png;base64,",
|
||||
});
|
||||
|
||||
/** Waits for web login completion or the next QR code. */
|
||||
export const WebLoginWaitParamsSchema = Type.Object(
|
||||
{
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
accountId: Type.Optional(Type.String()),
|
||||
currentQrDataUrl: Type.Optional(QrDataUrlSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
120
packages/gateway-protocol/src/schema/commands.ts
Normal file
120
packages/gateway-protocol/src/schema/commands.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Command catalog protocol schemas.
|
||||
*
|
||||
* Command entries describe native, skill, and plugin commands that clients can
|
||||
* render or route; limits keep command catalogs bounded for UI and transport.
|
||||
*/
|
||||
/** Maximum command display/name length accepted in catalog entries. */
|
||||
export const COMMAND_NAME_MAX_LENGTH = 200;
|
||||
/** Maximum command description length accepted in catalog entries. */
|
||||
export const COMMAND_DESCRIPTION_MAX_LENGTH = 2_000;
|
||||
/** Maximum text aliases advertised for one command. */
|
||||
export const COMMAND_ALIAS_MAX_ITEMS = 20;
|
||||
/** Maximum declared arguments advertised for one command. */
|
||||
export const COMMAND_ARGS_MAX_ITEMS = 20;
|
||||
/** Maximum argument name length accepted in catalog entries. */
|
||||
export const COMMAND_ARG_NAME_MAX_LENGTH = 200;
|
||||
/** Maximum argument description length accepted in catalog entries. */
|
||||
export const COMMAND_ARG_DESCRIPTION_MAX_LENGTH = 500;
|
||||
/** Maximum static choices advertised for one argument. */
|
||||
export const COMMAND_ARG_CHOICES_MAX_ITEMS = 50;
|
||||
/** Maximum machine-readable choice value length. */
|
||||
export const COMMAND_CHOICE_VALUE_MAX_LENGTH = 200;
|
||||
/** Maximum user-facing choice label length. */
|
||||
export const COMMAND_CHOICE_LABEL_MAX_LENGTH = 200;
|
||||
/** Maximum commands returned by one catalog response. */
|
||||
export const COMMAND_LIST_MAX_ITEMS = 500;
|
||||
|
||||
const BoundedNonEmptyString = (maxLength: number) => Type.String({ minLength: 1, maxLength });
|
||||
|
||||
/** Source system that contributed a command. */
|
||||
export const CommandSourceSchema = Type.Union([
|
||||
Type.Literal("native"),
|
||||
Type.Literal("skill"),
|
||||
Type.Literal("plugin"),
|
||||
]);
|
||||
|
||||
/** Surfaces where a command may be invoked. */
|
||||
export const CommandScopeSchema = Type.Union([
|
||||
Type.Literal("text"),
|
||||
Type.Literal("native"),
|
||||
Type.Literal("both"),
|
||||
]);
|
||||
|
||||
/** Coarse UI grouping for command catalog display. */
|
||||
export const CommandCategorySchema = Type.Union([
|
||||
Type.Literal("session"),
|
||||
Type.Literal("options"),
|
||||
Type.Literal("status"),
|
||||
Type.Literal("management"),
|
||||
Type.Literal("media"),
|
||||
Type.Literal("tools"),
|
||||
Type.Literal("docks"),
|
||||
]);
|
||||
|
||||
/** Static argument choice shown to clients. */
|
||||
export const CommandArgChoiceSchema = Type.Object(
|
||||
{
|
||||
value: Type.String({ maxLength: COMMAND_CHOICE_VALUE_MAX_LENGTH }),
|
||||
label: Type.String({ maxLength: COMMAND_CHOICE_LABEL_MAX_LENGTH }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** One typed argument advertised for a command. */
|
||||
export const CommandArgSchema = Type.Object(
|
||||
{
|
||||
name: BoundedNonEmptyString(COMMAND_ARG_NAME_MAX_LENGTH),
|
||||
description: Type.String({ maxLength: COMMAND_ARG_DESCRIPTION_MAX_LENGTH }),
|
||||
type: Type.Union([Type.Literal("string"), Type.Literal("number"), Type.Literal("boolean")]),
|
||||
required: Type.Optional(Type.Boolean()),
|
||||
choices: Type.Optional(
|
||||
Type.Array(CommandArgChoiceSchema, { maxItems: COMMAND_ARG_CHOICES_MAX_ITEMS }),
|
||||
),
|
||||
dynamic: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** One command catalog entry visible to clients. */
|
||||
export const CommandEntrySchema = Type.Object(
|
||||
{
|
||||
name: BoundedNonEmptyString(COMMAND_NAME_MAX_LENGTH),
|
||||
nativeName: Type.Optional(BoundedNonEmptyString(COMMAND_NAME_MAX_LENGTH)),
|
||||
textAliases: Type.Optional(
|
||||
Type.Array(BoundedNonEmptyString(COMMAND_NAME_MAX_LENGTH), {
|
||||
maxItems: COMMAND_ALIAS_MAX_ITEMS,
|
||||
}),
|
||||
),
|
||||
description: Type.String({ maxLength: COMMAND_DESCRIPTION_MAX_LENGTH }),
|
||||
category: Type.Optional(CommandCategorySchema),
|
||||
source: CommandSourceSchema,
|
||||
scope: CommandScopeSchema,
|
||||
acceptsArgs: Type.Boolean(),
|
||||
args: Type.Optional(Type.Array(CommandArgSchema, { maxItems: COMMAND_ARGS_MAX_ITEMS })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Command catalog request filters. */
|
||||
export const CommandsListParamsSchema = Type.Object(
|
||||
{
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
provider: Type.Optional(NonEmptyString),
|
||||
scope: Type.Optional(CommandScopeSchema),
|
||||
includeArgs: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Bounded command catalog response. */
|
||||
export const CommandsListResultSchema = Type.Object(
|
||||
{
|
||||
commands: Type.Array(CommandEntrySchema, { maxItems: COMMAND_LIST_MAX_ITEMS }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
148
packages/gateway-protocol/src/schema/config.ts
Normal file
148
packages/gateway-protocol/src/schema/config.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Gateway config and update protocol schemas.
|
||||
*
|
||||
* These payloads carry raw config text plus optional delivery context so the
|
||||
* gateway can report edits/restarts back to the originating channel.
|
||||
*/
|
||||
const ConfigSchemaLookupPathString = Type.String({
|
||||
minLength: 1,
|
||||
maxLength: 1024,
|
||||
pattern: "^[A-Za-z0-9_./\\[\\]\\-*]+$",
|
||||
});
|
||||
|
||||
const ConfigDeliveryContextSchema = Type.Object(
|
||||
{
|
||||
channel: Type.Optional(Type.String()),
|
||||
to: Type.Optional(Type.String()),
|
||||
accountId: Type.Optional(Type.String()),
|
||||
threadId: Type.Optional(Type.Union([Type.String(), Type.Number()])),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Empty request payload for reading the current raw config. */
|
||||
export const ConfigGetParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** Full raw config replacement request with optional base hash guard. */
|
||||
export const ConfigSetParamsSchema = Type.Object(
|
||||
{
|
||||
raw: NonEmptyString,
|
||||
baseHash: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Shared config apply/patch payload with optional restart notification context. */
|
||||
const ConfigApplyLikeParamProperties = {
|
||||
raw: NonEmptyString,
|
||||
baseHash: Type.Optional(NonEmptyString),
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
deliveryContext: Type.Optional(ConfigDeliveryContextSchema),
|
||||
note: Type.Optional(Type.String()),
|
||||
restartDelayMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
} as const;
|
||||
|
||||
const ConfigApplyLikeParamsSchema = Type.Object(ConfigApplyLikeParamProperties, {
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
/** Raw config apply request that may schedule a restart. */
|
||||
export const ConfigApplyParamsSchema = ConfigApplyLikeParamsSchema;
|
||||
/** Raw config patch request that may schedule a restart. */
|
||||
export const ConfigPatchParamsSchema = Type.Object(
|
||||
{
|
||||
...ConfigApplyLikeParamProperties,
|
||||
replacePaths: Type.Optional(Type.Array(NonEmptyString, { maxItems: 256 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Empty request payload for fetching the generated config schema. */
|
||||
export const ConfigSchemaParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** Schema lookup request for one config path. */
|
||||
export const ConfigSchemaLookupParamsSchema = Type.Object(
|
||||
{
|
||||
path: ConfigSchemaLookupPathString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Empty request payload for checking update/restart status. */
|
||||
export const UpdateStatusParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** Request payload for running an update/restart flow with optional channel delivery context. */
|
||||
export const UpdateRunParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
deliveryContext: Type.Optional(ConfigDeliveryContextSchema),
|
||||
note: Type.Optional(Type.String()),
|
||||
continuationMessage: Type.Optional(Type.String()),
|
||||
restartDelayMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** UI metadata attached to config schema paths. */
|
||||
export const ConfigUiHintSchema = Type.Object(
|
||||
{
|
||||
label: Type.Optional(Type.String()),
|
||||
help: Type.Optional(Type.String()),
|
||||
tags: Type.Optional(Type.Array(Type.String())),
|
||||
group: Type.Optional(Type.String()),
|
||||
order: Type.Optional(Type.Integer()),
|
||||
advanced: Type.Optional(Type.Boolean()),
|
||||
sensitive: Type.Optional(Type.Boolean()),
|
||||
placeholder: Type.Optional(Type.String()),
|
||||
itemTemplate: Type.Optional(Type.Unknown()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Full generated config schema response. */
|
||||
export const ConfigSchemaResponseSchema = Type.Object(
|
||||
{
|
||||
schema: Type.Unknown(),
|
||||
uiHints: Type.Record(Type.String(), ConfigUiHintSchema),
|
||||
version: NonEmptyString,
|
||||
generatedAt: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Child entry returned when looking up a config schema path. */
|
||||
export const ConfigSchemaLookupChildSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
path: NonEmptyString,
|
||||
type: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
|
||||
required: Type.Boolean(),
|
||||
hasChildren: Type.Boolean(),
|
||||
reloadKind: Type.Optional(
|
||||
Type.Union([Type.Literal("restart"), Type.Literal("hot"), Type.Literal("none")]),
|
||||
),
|
||||
hint: Type.Optional(ConfigUiHintSchema),
|
||||
hintPath: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Schema lookup response for one config path and its immediate children. */
|
||||
export const ConfigSchemaLookupResultSchema = Type.Object(
|
||||
{
|
||||
path: NonEmptyString,
|
||||
schema: Type.Unknown(),
|
||||
reloadKind: Type.Optional(
|
||||
Type.Union([Type.Literal("restart"), Type.Literal("hot"), Type.Literal("none")]),
|
||||
),
|
||||
hint: Type.Optional(ConfigUiHintSchema),
|
||||
hintPath: Type.Optional(Type.String()),
|
||||
children: Type.Array(ConfigSchemaLookupChildSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
39
packages/gateway-protocol/src/schema/crestodian.ts
Normal file
39
packages/gateway-protocol/src/schema/crestodian.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Gateway Protocol schema module defines Crestodian chat payloads.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Crestodian chat lets clients (macOS app onboarding, future UIs) hold the
|
||||
* setup/repair conversation over the gateway. It is configless-safe: the
|
||||
* engine answers deterministically before any model is configured. Omitting
|
||||
* `message` returns the welcome/greeting for a fresh session without input.
|
||||
*/
|
||||
export const CrestodianChatParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
message: Type.Optional(Type.String()),
|
||||
/** "onboarding" seeds the first-run setup proposal in the greeting. */
|
||||
welcomeVariant: Type.Optional(Type.Union([Type.Literal("onboarding")])),
|
||||
/** Drop any in-flight approval/wizard state and start the session over. */
|
||||
reset: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** One Crestodian reply; `action` tells clients about conversation handoffs. */
|
||||
export const CrestodianChatResultSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
reply: NonEmptyString,
|
||||
/** The next reply is a hosted-wizard secret and clients must mask its input/echo. */
|
||||
sensitive: Type.Optional(Type.Boolean()),
|
||||
action: Type.Union([
|
||||
Type.Literal("none"),
|
||||
// The user asked to talk to their agent; clients should move to their
|
||||
// normal agent chat surface.
|
||||
Type.Literal("open-agent"),
|
||||
Type.Literal("exit"),
|
||||
]),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
602
packages/gateway-protocol/src/schema/cron.ts
Normal file
602
packages/gateway-protocol/src/schema/cron.ts
Normal file
@@ -0,0 +1,602 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type, type TSchema } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Cron scheduler protocol schemas.
|
||||
*
|
||||
* These contracts describe scheduled agent turns, system events, delivery
|
||||
* routing, run history, and mutable job state shared by gateway RPC clients.
|
||||
*/
|
||||
|
||||
/** Builds create/patch payload variants while preserving per-call field optionality. */
|
||||
function cronAgentTurnPayloadSchema(params: {
|
||||
message: TSchema;
|
||||
model: TSchema;
|
||||
fallbacks: TSchema;
|
||||
toolsAllow: TSchema;
|
||||
thinking: TSchema;
|
||||
}) {
|
||||
return Type.Object(
|
||||
{
|
||||
kind: Type.Literal("agentTurn"),
|
||||
message: params.message,
|
||||
model: Type.Optional(params.model),
|
||||
fallbacks: Type.Optional(params.fallbacks),
|
||||
thinking: Type.Optional(params.thinking),
|
||||
timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
allowUnsafeExternalContent: Type.Optional(Type.Boolean()),
|
||||
lightContext: Type.Optional(Type.Boolean()),
|
||||
toolsAllow: Type.Optional(params.toolsAllow),
|
||||
// Server-managed marker for auto-stamped defaults; persisted so CLI cron
|
||||
// runs can drop only the cap that was never user-explicit.
|
||||
toolsAllowIsDefault: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
}
|
||||
|
||||
/** Builds command payload variants while preserving create/patch argv optionality. */
|
||||
function cronCommandPayloadSchema(params: { argv: TSchema }) {
|
||||
return Type.Object(
|
||||
{
|
||||
kind: Type.Literal("command"),
|
||||
argv: params.argv,
|
||||
cwd: Type.Optional(Type.String({ minLength: 1 })),
|
||||
env: Type.Optional(Type.Record(Type.String({ minLength: 1 }), Type.String())),
|
||||
input: Type.Optional(Type.String()),
|
||||
timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
noOutputTimeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
outputMaxBytes: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
}
|
||||
|
||||
/** Session target accepted by cron jobs. */
|
||||
const CronSessionTargetSchema = Type.Union([
|
||||
Type.Literal("main"),
|
||||
Type.Literal("isolated"),
|
||||
Type.Literal("current"),
|
||||
Type.String({ pattern: "^session:.+" }),
|
||||
]);
|
||||
/** Whether a cron job waits for heartbeat processing or wakes immediately. */
|
||||
const CronWakeModeSchema = Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]);
|
||||
/** Run status factory reused for the active field and deprecated alias metadata. */
|
||||
function cronRunStatusSchema(options: Record<string, unknown> = {}) {
|
||||
return Type.Union([Type.Literal("ok"), Type.Literal("error"), Type.Literal("skipped")], options);
|
||||
}
|
||||
|
||||
const CronRunStatusSchema = cronRunStatusSchema();
|
||||
const DeprecatedCronRunStatusSchema = cronRunStatusSchema({
|
||||
deprecated: true,
|
||||
description: "Deprecated alias for lastRunStatus.",
|
||||
});
|
||||
const CronSortDirSchema = Type.Union([Type.Literal("asc"), Type.Literal("desc")]);
|
||||
const CronJobsEnabledFilterSchema = Type.Union([
|
||||
Type.Literal("all"),
|
||||
Type.Literal("enabled"),
|
||||
Type.Literal("disabled"),
|
||||
]);
|
||||
const CronJobsScheduleKindFilterSchema = Type.Union([
|
||||
Type.Literal("all"),
|
||||
Type.Literal("at"),
|
||||
Type.Literal("every"),
|
||||
Type.Literal("cron"),
|
||||
Type.Literal("on-exit"),
|
||||
]);
|
||||
const CronJobsLastRunStatusFilterSchema = Type.Union([
|
||||
Type.Literal("all"),
|
||||
Type.Literal("ok"),
|
||||
Type.Literal("error"),
|
||||
Type.Literal("skipped"),
|
||||
Type.Literal("unknown"),
|
||||
]);
|
||||
const CronJobsSortBySchema = Type.Union([
|
||||
Type.Literal("nextRunAtMs"),
|
||||
Type.Literal("updatedAtMs"),
|
||||
Type.Literal("name"),
|
||||
]);
|
||||
const CronRunsStatusFilterSchema = Type.Union([
|
||||
Type.Literal("all"),
|
||||
Type.Literal("ok"),
|
||||
Type.Literal("error"),
|
||||
Type.Literal("skipped"),
|
||||
]);
|
||||
const CronRunsStatusValueSchema = Type.Union([
|
||||
Type.Literal("ok"),
|
||||
Type.Literal("error"),
|
||||
Type.Literal("skipped"),
|
||||
]);
|
||||
const CronDeliveryStatusSchema = Type.Union([
|
||||
Type.Literal("delivered"),
|
||||
Type.Literal("not-delivered"),
|
||||
Type.Literal("unknown"),
|
||||
Type.Literal("not-requested"),
|
||||
]);
|
||||
const NonBlankString = Type.String({ minLength: 1, pattern: "\\S" });
|
||||
const CronAnnounceChannelSchema = Type.Union([Type.Literal("last"), NonBlankString]);
|
||||
const CronFailoverReasonSchema = Type.Union([
|
||||
Type.Literal("auth"),
|
||||
Type.Literal("auth_permanent"),
|
||||
Type.Literal("format"),
|
||||
Type.Literal("rate_limit"),
|
||||
Type.Literal("overloaded"),
|
||||
Type.Literal("billing"),
|
||||
Type.Literal("server_error"),
|
||||
Type.Literal("timeout"),
|
||||
Type.Literal("context_overflow"),
|
||||
Type.Literal("model_not_found"),
|
||||
Type.Literal("session_expired"),
|
||||
Type.Literal("empty_response"),
|
||||
Type.Literal("no_error_details"),
|
||||
Type.Literal("unclassified"),
|
||||
Type.Literal("unknown"),
|
||||
]);
|
||||
const CronRunDiagnosticSeveritySchema = Type.Union([
|
||||
Type.Literal("info"),
|
||||
Type.Literal("warn"),
|
||||
Type.Literal("error"),
|
||||
]);
|
||||
const CronRunDiagnosticSourceSchema = Type.Union([
|
||||
Type.Literal("cron-preflight"),
|
||||
Type.Literal("cron-setup"),
|
||||
Type.Literal("model-preflight"),
|
||||
Type.Literal("agent-run"),
|
||||
Type.Literal("tool"),
|
||||
Type.Literal("exec"),
|
||||
Type.Literal("delivery"),
|
||||
]);
|
||||
const CronRunDiagnosticSchema = Type.Object(
|
||||
{
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
source: CronRunDiagnosticSourceSchema,
|
||||
severity: CronRunDiagnosticSeveritySchema,
|
||||
message: Type.String(),
|
||||
toolName: Type.Optional(Type.String()),
|
||||
exitCode: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
||||
truncated: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
const CronRunDiagnosticsSchema = Type.Object(
|
||||
{
|
||||
summary: Type.Optional(Type.String()),
|
||||
entries: Type.Array(CronRunDiagnosticSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
const CronCommonOptionalFields = {
|
||||
agentId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
sessionKey: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
description: Type.Optional(Type.String()),
|
||||
enabled: Type.Optional(Type.Boolean()),
|
||||
deleteAfterRun: Type.Optional(Type.Boolean()),
|
||||
};
|
||||
|
||||
function cronIdOrJobIdParams(extraFields: Record<string, TSchema>) {
|
||||
return Type.Union([
|
||||
Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
...extraFields,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
Type.Object(
|
||||
{
|
||||
jobId: NonEmptyString,
|
||||
...extraFields,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
const CronRunLogJobIdSchema = Type.String({
|
||||
minLength: 1,
|
||||
// Prevent path traversal via separators in cron.runs id/jobId.
|
||||
pattern: "^[^/\\\\]+$",
|
||||
});
|
||||
|
||||
/** Schedule expression for one-time, interval, or cron-expression jobs. */
|
||||
export const CronScheduleSchema = Type.Union([
|
||||
Type.Object(
|
||||
{
|
||||
kind: Type.Literal("at"),
|
||||
at: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
Type.Object(
|
||||
{
|
||||
kind: Type.Literal("every"),
|
||||
everyMs: Type.Integer({ minimum: 1 }),
|
||||
anchorMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
Type.Object(
|
||||
{
|
||||
kind: Type.Literal("cron"),
|
||||
expr: NonEmptyString,
|
||||
tz: Type.Optional(Type.String()),
|
||||
staggerMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
Type.Object(
|
||||
{
|
||||
// Event-driven trigger: fires once when the gateway-owned watcher running
|
||||
// `command` exits. Survives per-turn CLI teardown (runs under the gateway
|
||||
// ProcessSupervisor, not the turn process tree).
|
||||
kind: Type.Literal("on-exit"),
|
||||
command: NonEmptyString,
|
||||
cwd: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
]);
|
||||
|
||||
/** Full cron payload for new jobs. */
|
||||
export const CronPayloadSchema = Type.Union([
|
||||
Type.Object(
|
||||
{
|
||||
kind: Type.Literal("systemEvent"),
|
||||
text: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
cronAgentTurnPayloadSchema({
|
||||
message: NonEmptyString,
|
||||
model: Type.String(),
|
||||
fallbacks: Type.Array(Type.String()),
|
||||
toolsAllow: Type.Array(Type.String()),
|
||||
thinking: Type.String(),
|
||||
}),
|
||||
cronCommandPayloadSchema({
|
||||
argv: Type.Array(NonEmptyString, { minItems: 1 }),
|
||||
}),
|
||||
]);
|
||||
|
||||
/** Partial cron payload for job updates. */
|
||||
export const CronPayloadPatchSchema = Type.Union([
|
||||
Type.Object(
|
||||
{
|
||||
kind: Type.Literal("systemEvent"),
|
||||
text: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
cronAgentTurnPayloadSchema({
|
||||
message: Type.Optional(NonEmptyString),
|
||||
model: Type.Union([Type.String(), Type.Null()]),
|
||||
fallbacks: Type.Union([Type.Array(Type.String()), Type.Null()]),
|
||||
toolsAllow: Type.Union([Type.Array(Type.String()), Type.Null()]),
|
||||
thinking: Type.Union([Type.String(), Type.Null()]),
|
||||
}),
|
||||
cronCommandPayloadSchema({
|
||||
argv: Type.Optional(Type.Array(NonEmptyString, { minItems: 1 })),
|
||||
}),
|
||||
]);
|
||||
|
||||
/** Failure alert policy for repeated cron run failures. */
|
||||
export const CronFailureAlertSchema = Type.Object(
|
||||
{
|
||||
after: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
channel: Type.Optional(CronAnnounceChannelSchema),
|
||||
to: Type.Optional(NonBlankString),
|
||||
cooldownMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
includeSkipped: Type.Optional(Type.Boolean()),
|
||||
mode: Type.Optional(Type.Union([Type.Literal("announce"), Type.Literal("webhook")])),
|
||||
accountId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Delivery destination used when failure alerts need a separate target. */
|
||||
export const CronFailureDestinationSchema = Type.Object(
|
||||
{
|
||||
channel: Type.Optional(CronAnnounceChannelSchema),
|
||||
to: Type.Optional(NonBlankString),
|
||||
accountId: Type.Optional(NonEmptyString),
|
||||
mode: Type.Optional(Type.Union([Type.Literal("announce"), Type.Literal("webhook")])),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
const CronFailureDestinationPatchSchema = Type.Object(
|
||||
{
|
||||
channel: Type.Optional(Type.Union([CronAnnounceChannelSchema, Type.Null()])),
|
||||
to: Type.Optional(Type.Union([NonBlankString, Type.Null()])),
|
||||
accountId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
mode: Type.Optional(
|
||||
Type.Union([Type.Literal("announce"), Type.Literal("webhook"), Type.Null()]),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const CronCompletionDestinationSchema = Type.Object(
|
||||
{
|
||||
mode: Type.Literal("webhook"),
|
||||
to: NonBlankString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
const CronDeliverySharedProperties = {
|
||||
channel: Type.Optional(CronAnnounceChannelSchema),
|
||||
threadId: Type.Optional(Type.Union([Type.String(), Type.Number()])),
|
||||
accountId: Type.Optional(NonEmptyString),
|
||||
bestEffort: Type.Optional(Type.Boolean()),
|
||||
failureDestination: Type.Optional(CronFailureDestinationSchema),
|
||||
};
|
||||
|
||||
const CronDeliveryPatchSharedProperties = {
|
||||
channel: Type.Optional(Type.Union([CronAnnounceChannelSchema, Type.Null()])),
|
||||
threadId: Type.Optional(Type.Union([Type.String(), Type.Number(), Type.Null()])),
|
||||
accountId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
bestEffort: Type.Optional(Type.Boolean()),
|
||||
failureDestination: Type.Optional(Type.Union([CronFailureDestinationPatchSchema, Type.Null()])),
|
||||
};
|
||||
|
||||
const CronDeliveryNoopSchema = Type.Object(
|
||||
{
|
||||
mode: Type.Literal("none"),
|
||||
...CronDeliverySharedProperties,
|
||||
to: Type.Optional(NonBlankString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
const CronDeliveryAnnounceSchema = Type.Object(
|
||||
{
|
||||
mode: Type.Literal("announce"),
|
||||
...CronDeliverySharedProperties,
|
||||
completionDestination: Type.Optional(CronCompletionDestinationSchema),
|
||||
to: Type.Optional(NonBlankString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
const CronDeliveryWebhookSchema = Type.Object(
|
||||
{
|
||||
mode: Type.Literal("webhook"),
|
||||
...CronDeliverySharedProperties,
|
||||
to: NonBlankString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Delivery policy for cron run output. */
|
||||
export const CronDeliverySchema = Type.Union([
|
||||
CronDeliveryNoopSchema,
|
||||
CronDeliveryAnnounceSchema,
|
||||
CronDeliveryWebhookSchema,
|
||||
]);
|
||||
|
||||
/** Patch shape for cron delivery policy updates. */
|
||||
export const CronDeliveryPatchSchema = Type.Object(
|
||||
{
|
||||
mode: Type.Optional(
|
||||
Type.Union([Type.Literal("none"), Type.Literal("announce"), Type.Literal("webhook")]),
|
||||
),
|
||||
...CronDeliveryPatchSharedProperties,
|
||||
completionDestination: Type.Optional(
|
||||
Type.Union([CronCompletionDestinationSchema, Type.Null()]),
|
||||
),
|
||||
to: Type.Optional(Type.Union([NonBlankString, Type.Null()])),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
const CronFailureNotificationDeliverySchema = Type.Object(
|
||||
{
|
||||
delivered: Type.Optional(Type.Boolean()),
|
||||
status: CronDeliveryStatusSchema,
|
||||
error: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Scheduler-maintained state for the latest run/delivery outcome. */
|
||||
export const CronJobStateSchema = Type.Object(
|
||||
{
|
||||
nextRunAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
runningAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastRunAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastRunStatus: Type.Optional(CronRunStatusSchema),
|
||||
lastStatus: Type.Optional(DeprecatedCronRunStatusSchema),
|
||||
lastError: Type.Optional(Type.String()),
|
||||
lastDiagnostics: Type.Optional(CronRunDiagnosticsSchema),
|
||||
lastDiagnosticSummary: Type.Optional(Type.String()),
|
||||
lastErrorReason: Type.Optional(CronFailoverReasonSchema),
|
||||
lastDurationMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
consecutiveErrors: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
consecutiveSkipped: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastDelivered: Type.Optional(Type.Boolean()),
|
||||
lastDeliveryStatus: Type.Optional(CronDeliveryStatusSchema),
|
||||
lastDeliveryError: Type.Optional(Type.String()),
|
||||
lastFailureNotificationDelivered: Type.Optional(Type.Boolean()),
|
||||
lastFailureNotificationDeliveryStatus: Type.Optional(CronDeliveryStatusSchema),
|
||||
lastFailureNotificationDeliveryError: Type.Optional(Type.String()),
|
||||
lastFailureAlertAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
const CronJobStatePatchSchema = Type.Object(
|
||||
{
|
||||
nextRunAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
runningAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastRunAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastRunStatus: Type.Optional(CronRunStatusSchema),
|
||||
lastStatus: Type.Optional(DeprecatedCronRunStatusSchema),
|
||||
lastError: Type.Optional(Type.String()),
|
||||
lastErrorReason: Type.Optional(CronFailoverReasonSchema),
|
||||
lastDurationMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
consecutiveErrors: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
consecutiveSkipped: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastDelivered: Type.Optional(Type.Boolean()),
|
||||
lastDeliveryStatus: Type.Optional(CronDeliveryStatusSchema),
|
||||
lastDeliveryError: Type.Optional(Type.String()),
|
||||
lastFailureNotificationDelivered: Type.Optional(Type.Boolean()),
|
||||
lastFailureNotificationDeliveryStatus: Type.Optional(CronDeliveryStatusSchema),
|
||||
lastFailureNotificationDeliveryError: Type.Optional(Type.String()),
|
||||
lastFailureAlertAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Persisted cron job definition returned by scheduler list/get APIs. */
|
||||
export const CronJobSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
sessionKey: Type.Optional(NonEmptyString),
|
||||
name: NonEmptyString,
|
||||
description: Type.Optional(Type.String()),
|
||||
enabled: Type.Boolean(),
|
||||
deleteAfterRun: Type.Optional(Type.Boolean()),
|
||||
createdAtMs: Type.Integer({ minimum: 0 }),
|
||||
updatedAtMs: Type.Integer({ minimum: 0 }),
|
||||
schedule: CronScheduleSchema,
|
||||
sessionTarget: CronSessionTargetSchema,
|
||||
wakeMode: CronWakeModeSchema,
|
||||
payload: CronPayloadSchema,
|
||||
delivery: Type.Optional(CronDeliverySchema),
|
||||
failureAlert: Type.Optional(Type.Union([Type.Literal(false), CronFailureAlertSchema])),
|
||||
state: CronJobStateSchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Query params for listing cron jobs with filters and pagination. */
|
||||
export const CronListParamsSchema = Type.Object(
|
||||
{
|
||||
includeDisabled: Type.Optional(Type.Boolean()),
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200 })),
|
||||
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
query: Type.Optional(Type.String()),
|
||||
enabled: Type.Optional(CronJobsEnabledFilterSchema),
|
||||
scheduleKind: Type.Optional(CronJobsScheduleKindFilterSchema),
|
||||
lastRunStatus: Type.Optional(CronJobsLastRunStatusFilterSchema),
|
||||
sortBy: Type.Optional(CronJobsSortBySchema),
|
||||
sortDir: Type.Optional(CronSortDirSchema),
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
compact: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Empty request payload for scheduler status. */
|
||||
export const CronStatusParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** Looks up a job by stable id or legacy jobId alias. */
|
||||
export const CronGetParamsSchema = cronIdOrJobIdParams({});
|
||||
|
||||
/** Creates a scheduled job with schedule, target, payload, and delivery policy. */
|
||||
export const CronAddParamsSchema = Type.Object(
|
||||
{
|
||||
name: NonEmptyString,
|
||||
...CronCommonOptionalFields,
|
||||
schedule: CronScheduleSchema,
|
||||
sessionTarget: CronSessionTargetSchema,
|
||||
wakeMode: CronWakeModeSchema,
|
||||
payload: CronPayloadSchema,
|
||||
delivery: Type.Optional(CronDeliverySchema),
|
||||
failureAlert: Type.Optional(Type.Union([Type.Literal(false), CronFailureAlertSchema])),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Mutable cron job fields accepted by update APIs. */
|
||||
export const CronJobPatchSchema = Type.Object(
|
||||
{
|
||||
name: Type.Optional(NonEmptyString),
|
||||
...CronCommonOptionalFields,
|
||||
schedule: Type.Optional(CronScheduleSchema),
|
||||
sessionTarget: Type.Optional(CronSessionTargetSchema),
|
||||
wakeMode: Type.Optional(CronWakeModeSchema),
|
||||
payload: Type.Optional(CronPayloadPatchSchema),
|
||||
delivery: Type.Optional(CronDeliveryPatchSchema),
|
||||
failureAlert: Type.Optional(Type.Union([Type.Literal(false), CronFailureAlertSchema])),
|
||||
state: Type.Optional(CronJobStatePatchSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Updates a cron job by id or legacy jobId alias. */
|
||||
export const CronUpdateParamsSchema = cronIdOrJobIdParams({
|
||||
patch: CronJobPatchSchema,
|
||||
});
|
||||
|
||||
/** Removes a cron job by id or legacy jobId alias. */
|
||||
export const CronRemoveParamsSchema = cronIdOrJobIdParams({});
|
||||
|
||||
/** Runs a cron job immediately or only if due. */
|
||||
export const CronRunParamsSchema = cronIdOrJobIdParams({
|
||||
mode: Type.Optional(Type.Union([Type.Literal("due"), Type.Literal("force")])),
|
||||
});
|
||||
|
||||
/** Query params for cron run history. */
|
||||
export const CronRunsParamsSchema = Type.Object(
|
||||
{
|
||||
scope: Type.Optional(Type.Union([Type.Literal("job"), Type.Literal("all")])),
|
||||
id: Type.Optional(CronRunLogJobIdSchema),
|
||||
jobId: Type.Optional(CronRunLogJobIdSchema),
|
||||
runId: Type.Optional(NonEmptyString),
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200 })),
|
||||
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
statuses: Type.Optional(Type.Array(CronRunsStatusValueSchema, { minItems: 1, maxItems: 3 })),
|
||||
status: Type.Optional(CronRunsStatusFilterSchema),
|
||||
deliveryStatuses: Type.Optional(
|
||||
Type.Array(CronDeliveryStatusSchema, { minItems: 1, maxItems: 4 }),
|
||||
),
|
||||
deliveryStatus: Type.Optional(CronDeliveryStatusSchema),
|
||||
query: Type.Optional(Type.String()),
|
||||
sortDir: Type.Optional(CronSortDirSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** One persisted cron run history entry. */
|
||||
export const CronRunLogEntrySchema = Type.Object(
|
||||
{
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
jobId: NonEmptyString,
|
||||
action: Type.Literal("finished"),
|
||||
status: Type.Optional(CronRunStatusSchema),
|
||||
error: Type.Optional(Type.String()),
|
||||
errorReason: Type.Optional(CronFailoverReasonSchema),
|
||||
summary: Type.Optional(Type.String()),
|
||||
diagnostics: Type.Optional(CronRunDiagnosticsSchema),
|
||||
delivered: Type.Optional(Type.Boolean()),
|
||||
deliveryStatus: Type.Optional(CronDeliveryStatusSchema),
|
||||
deliveryError: Type.Optional(Type.String()),
|
||||
failureNotificationDelivery: Type.Optional(CronFailureNotificationDeliverySchema),
|
||||
sessionId: Type.Optional(NonEmptyString),
|
||||
sessionKey: Type.Optional(NonEmptyString),
|
||||
runId: Type.Optional(NonEmptyString),
|
||||
runAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
durationMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
nextRunAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
model: Type.Optional(Type.String()),
|
||||
provider: Type.Optional(Type.String()),
|
||||
usage: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
input_tokens: Type.Optional(Type.Number()),
|
||||
output_tokens: Type.Optional(Type.Number()),
|
||||
total_tokens: Type.Optional(Type.Number()),
|
||||
cache_read_tokens: Type.Optional(Type.Number()),
|
||||
cache_write_tokens: Type.Optional(Type.Number()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
jobName: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
119
packages/gateway-protocol/src/schema/devices.ts
Normal file
119
packages/gateway-protocol/src/schema/devices.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Device pairing and token-management protocol schemas.
|
||||
*
|
||||
* These payloads cross the gateway approval boundary, so request ids and device
|
||||
* ids stay explicit and feature handlers own the authorization checks.
|
||||
*/
|
||||
/** Lists pending and approved device pairing records. */
|
||||
export const DevicePairListParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** Approves a pending pairing request by request id. */
|
||||
export const DevicePairApproveParamsSchema = Type.Object(
|
||||
{ requestId: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Rejects a pending pairing request by request id. */
|
||||
export const DevicePairRejectParamsSchema = Type.Object(
|
||||
{ requestId: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Removes an approved or remembered device by device id. */
|
||||
export const DevicePairRemoveParamsSchema = Type.Object(
|
||||
{ deviceId: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Rotates or issues a device token for a specific role/scope grant. */
|
||||
export const DeviceTokenRotateParamsSchema = Type.Object(
|
||||
{
|
||||
deviceId: NonEmptyString,
|
||||
role: NonEmptyString,
|
||||
scopes: Type.Optional(Type.Array(NonEmptyString)),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Revokes one role-bound device token grant. */
|
||||
export const DeviceTokenRevokeParamsSchema = Type.Object(
|
||||
{
|
||||
deviceId: NonEmptyString,
|
||||
role: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Event emitted when a client opens or refreshes a pairing request. */
|
||||
export const DevicePairRequestedEventSchema = Type.Object(
|
||||
{
|
||||
requestId: NonEmptyString,
|
||||
deviceId: NonEmptyString,
|
||||
publicKey: NonEmptyString,
|
||||
displayName: Type.Optional(NonEmptyString),
|
||||
platform: Type.Optional(NonEmptyString),
|
||||
deviceFamily: Type.Optional(NonEmptyString),
|
||||
clientId: Type.Optional(NonEmptyString),
|
||||
clientMode: Type.Optional(NonEmptyString),
|
||||
role: Type.Optional(NonEmptyString),
|
||||
roles: Type.Optional(Type.Array(NonEmptyString)),
|
||||
scopes: Type.Optional(Type.Array(NonEmptyString)),
|
||||
remoteIp: Type.Optional(NonEmptyString),
|
||||
silent: Type.Optional(Type.Boolean()),
|
||||
isRepair: Type.Optional(Type.Boolean()),
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Event emitted after a pairing request is approved, rejected, or otherwise resolved. */
|
||||
export const DevicePairResolvedEventSchema = Type.Object(
|
||||
{
|
||||
requestId: NonEmptyString,
|
||||
deviceId: NonEmptyString,
|
||||
decision: NonEmptyString,
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
const SetupCodeQrDataUrlSchema = Type.String({
|
||||
maxLength: 16_384,
|
||||
pattern: "^data:image/png;base64,",
|
||||
});
|
||||
|
||||
/**
|
||||
* Generates a device-pairing setup code (and optional QR) so a mobile/companion
|
||||
* client can scan it and connect to this gateway. The embedded setup code mints
|
||||
* a short-lived bootstrap token that hands off broad operator scopes
|
||||
* (read/write/approvals/talk.secrets), so this method requires operator.admin
|
||||
* (enforced by the core method descriptor's method-scope policy, not the handler)
|
||||
* and is not advertised.
|
||||
*/
|
||||
export const DevicePairSetupCodeParamsSchema = Type.Object(
|
||||
{
|
||||
publicUrl: Type.Optional(NonEmptyString),
|
||||
preferRemoteUrl: Type.Optional(Type.Boolean()),
|
||||
includeQr: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/**
|
||||
* Setup code plus non-secret connection metadata. `auth` is a label only
|
||||
* ("token" | "password"); the gateway credential itself is never returned.
|
||||
*/
|
||||
export const DevicePairSetupCodeResultSchema = Type.Object(
|
||||
{
|
||||
setupCode: NonEmptyString,
|
||||
qrDataUrl: Type.Optional(SetupCodeQrDataUrlSchema),
|
||||
gatewayUrl: NonEmptyString,
|
||||
auth: Type.Union([Type.Literal("token"), Type.Literal("password")]),
|
||||
urlSource: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
50
packages/gateway-protocol/src/schema/environments.ts
Normal file
50
packages/gateway-protocol/src/schema/environments.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Environment inventory protocol schemas.
|
||||
*
|
||||
* Environments are runtime targets such as local hosts, VMs, or remote workers;
|
||||
* this schema layer only describes their gateway-visible status summary.
|
||||
*/
|
||||
/** Runtime availability state for an environment target. */
|
||||
export const EnvironmentStatusSchema = Type.String({
|
||||
enum: ["available", "unavailable", "starting", "stopping", "error"],
|
||||
});
|
||||
|
||||
function createEnvironmentSummarySchema() {
|
||||
return Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
type: NonEmptyString,
|
||||
label: Type.Optional(NonEmptyString),
|
||||
status: EnvironmentStatusSchema,
|
||||
capabilities: Type.Optional(Type.Array(NonEmptyString)),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
}
|
||||
|
||||
/** Public environment summary shown in listings and status responses. */
|
||||
export const EnvironmentSummarySchema = createEnvironmentSummarySchema();
|
||||
|
||||
/** Empty request payload for listing known environments. */
|
||||
export const EnvironmentsListParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** List response containing all gateway-visible environment summaries. */
|
||||
export const EnvironmentsListResultSchema = Type.Object(
|
||||
{
|
||||
environments: Type.Array(EnvironmentSummarySchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Status lookup request for one environment id. */
|
||||
export const EnvironmentsStatusParamsSchema = Type.Object(
|
||||
{ environmentId: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Status lookup result for one environment id. */
|
||||
export const EnvironmentsStatusResultSchema = createEnvironmentSummarySchema();
|
||||
34
packages/gateway-protocol/src/schema/error-codes.ts
Normal file
34
packages/gateway-protocol/src/schema/error-codes.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import type { ErrorShape } from "./types.js";
|
||||
|
||||
/** Gateway JSON-RPC style error codes shared by clients and server handlers. */
|
||||
export const ErrorCodes = {
|
||||
/** Client has not completed account/device linking for this gateway. */
|
||||
NOT_LINKED: "NOT_LINKED",
|
||||
/** Device exists but still needs an explicit pairing approval. */
|
||||
NOT_PAIRED: "NOT_PAIRED",
|
||||
/** Agent turn exceeded the gateway wait window. */
|
||||
AGENT_TIMEOUT: "AGENT_TIMEOUT",
|
||||
/** Request payload failed protocol validation or method preconditions. */
|
||||
INVALID_REQUEST: "INVALID_REQUEST",
|
||||
/** Approval resolution referenced a missing or expired approval request. */
|
||||
APPROVAL_NOT_FOUND: "APPROVAL_NOT_FOUND",
|
||||
/** Gateway service or required backend is temporarily unavailable. */
|
||||
UNAVAILABLE: "UNAVAILABLE",
|
||||
} as const;
|
||||
|
||||
/** Closed set of canonical gateway error code strings. */
|
||||
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
||||
|
||||
/** Builds the canonical gateway error payload while preserving optional retry metadata. */
|
||||
export function errorShape(
|
||||
code: ErrorCode,
|
||||
message: string,
|
||||
opts?: { details?: unknown; retryable?: boolean; retryAfterMs?: number },
|
||||
): ErrorShape {
|
||||
return {
|
||||
code,
|
||||
message,
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
206
packages/gateway-protocol/src/schema/exec-approvals.ts
Normal file
206
packages/gateway-protocol/src/schema/exec-approvals.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Exec approval protocol schemas.
|
||||
*
|
||||
* These payloads cross the security-review boundary for command execution, so
|
||||
* persisted policy, request snapshots, and resolve decisions stay explicit.
|
||||
*/
|
||||
/** One persisted allowlist entry for a command pattern or resolved executable. */
|
||||
export const ExecApprovalsAllowlistEntrySchema = Type.Object(
|
||||
{
|
||||
id: Type.Optional(NonEmptyString),
|
||||
pattern: Type.String(),
|
||||
source: Type.Optional(Type.Literal("allow-always")),
|
||||
commandText: Type.Optional(Type.String()),
|
||||
argPattern: Type.Optional(Type.String()),
|
||||
lastUsedAt: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastUsedCommand: Type.Optional(Type.String()),
|
||||
lastResolvedPath: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
const ExecApprovalsPolicyFields = {
|
||||
security: Type.Optional(Type.String()),
|
||||
ask: Type.Optional(Type.String()),
|
||||
askFallback: Type.Optional(Type.String()),
|
||||
autoAllowSkills: Type.Optional(Type.Boolean()),
|
||||
};
|
||||
|
||||
/** Default exec approval policy shared by all agents unless overridden. */
|
||||
export const ExecApprovalsDefaultsSchema = Type.Object(ExecApprovalsPolicyFields, {
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
/** Agent-specific exec approval policy and allowlist. */
|
||||
export const ExecApprovalsAgentSchema = Type.Object(
|
||||
{
|
||||
...ExecApprovalsPolicyFields,
|
||||
allowlist: Type.Optional(Type.Array(ExecApprovalsAllowlistEntrySchema)),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Versioned exec approvals config file edited through gateway APIs. */
|
||||
export const ExecApprovalsFileSchema = Type.Object(
|
||||
{
|
||||
version: Type.Literal(1),
|
||||
socket: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
path: Type.Optional(Type.String()),
|
||||
token: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
defaults: Type.Optional(ExecApprovalsDefaultsSchema),
|
||||
agents: Type.Optional(Type.Record(Type.String(), ExecApprovalsAgentSchema)),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Read snapshot with path/hash metadata for optimistic writes. */
|
||||
export const ExecApprovalsSnapshotSchema = Type.Object(
|
||||
{
|
||||
path: NonEmptyString,
|
||||
exists: Type.Boolean(),
|
||||
hash: NonEmptyString,
|
||||
file: ExecApprovalsFileSchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Empty request payload for reading local exec approval policy. */
|
||||
export const ExecApprovalsGetParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** Local exec approval policy write request with optional base hash guard. */
|
||||
export const ExecApprovalsSetParamsSchema = Type.Object(
|
||||
{
|
||||
file: ExecApprovalsFileSchema,
|
||||
baseHash: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Node-scoped request payload for reading exec approval policy. */
|
||||
export const ExecApprovalsNodeGetParamsSchema = Type.Object(
|
||||
{
|
||||
nodeId: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Node-scoped exec approval policy write request with optional base hash guard. */
|
||||
export const ExecApprovalsNodeSetParamsSchema = Type.Object(
|
||||
{
|
||||
nodeId: NonEmptyString,
|
||||
file: ExecApprovalsFileSchema,
|
||||
baseHash: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Lookup request for one pending exec approval by id. */
|
||||
export const ExecApprovalGetParamsSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Pending command execution approval request shown to reviewers. */
|
||||
export const ExecApprovalRequestParamsSchema = Type.Object(
|
||||
{
|
||||
id: Type.Optional(NonEmptyString),
|
||||
command: Type.Optional(NonEmptyString),
|
||||
commandArgv: Type.Optional(Type.Array(Type.String())),
|
||||
systemRunPlan: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
argv: Type.Array(Type.String()),
|
||||
cwd: Type.Union([Type.String(), Type.Null()]),
|
||||
commandText: Type.String(),
|
||||
commandPreview: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
agentId: Type.Union([Type.String(), Type.Null()]),
|
||||
sessionKey: Type.Union([Type.String(), Type.Null()]),
|
||||
mutableFileOperand: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Object(
|
||||
{
|
||||
argvIndex: Type.Integer({ minimum: 0 }),
|
||||
path: Type.String(),
|
||||
sha256: Type.String(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
Type.Null(),
|
||||
]),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
env: Type.Optional(Type.Record(NonEmptyString, Type.String())),
|
||||
cwd: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
nodeId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
host: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
security: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
ask: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
warningText: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
unavailableDecisions: Type.Optional(
|
||||
Type.Array(Type.String({ enum: ["allow-always"] }), {
|
||||
minItems: 1,
|
||||
maxItems: 1,
|
||||
}),
|
||||
),
|
||||
commandSpans: Type.Optional(
|
||||
Type.Array(
|
||||
Type.Object(
|
||||
{
|
||||
startIndex: Type.Integer({
|
||||
minimum: 0,
|
||||
description: "Inclusive UTF-16 code unit offset into command.",
|
||||
}),
|
||||
endIndex: Type.Integer({
|
||||
minimum: 1,
|
||||
description:
|
||||
"Exclusive UTF-16 code unit offset into command; must be greater than startIndex and no greater than command.length.",
|
||||
}),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
),
|
||||
agentId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
resolvedPath: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
sessionKey: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
turnSourceChannel: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
turnSourceTo: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
turnSourceAccountId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||
turnSourceThreadId: Type.Optional(Type.Union([Type.String(), Type.Number(), Type.Null()])),
|
||||
approvalReviewerDeviceIds: Type.Optional(
|
||||
Type.Array(NonEmptyString, {
|
||||
description:
|
||||
"Trusted approval-runtime metadata naming operator devices that may review this approval; ordinary Gateway clients may send the field, but the Gateway only binds it for internal approval-runtime requests.",
|
||||
}),
|
||||
),
|
||||
requireDeliveryRoute: Type.Optional(Type.Boolean()),
|
||||
suppressDelivery: Type.Optional(Type.Boolean()),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
twoPhase: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Reviewer decision payload for one pending exec approval. */
|
||||
export const ExecApprovalResolveParamsSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
decision: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
192
packages/gateway-protocol/src/schema/frames.ts
Normal file
192
packages/gateway-protocol/src/schema/frames.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { GatewayClientIdSchema, GatewayClientModeSchema, NonEmptyString } from "./primitives.js";
|
||||
import { SnapshotSchema, StateVersionSchema } from "./snapshot.js";
|
||||
|
||||
/**
|
||||
* Top-level gateway frame schemas.
|
||||
*
|
||||
* These are the WebSocket envelope contracts; method/event payload schemas live
|
||||
* in feature-specific modules and are referenced by runtime validators.
|
||||
*/
|
||||
/** Periodic server heartbeat event payload. */
|
||||
export const TickEventSchema = Type.Object(
|
||||
{
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Server shutdown notice event payload. */
|
||||
export const ShutdownEventSchema = Type.Object(
|
||||
{
|
||||
reason: NonEmptyString,
|
||||
restartExpectedMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Initial client hello/connect payload sent before the gateway accepts frames. */
|
||||
export const ConnectParamsSchema = Type.Object(
|
||||
{
|
||||
minProtocol: Type.Integer({ minimum: 1 }),
|
||||
maxProtocol: Type.Integer({ minimum: 1 }),
|
||||
client: Type.Object(
|
||||
{
|
||||
id: GatewayClientIdSchema,
|
||||
displayName: Type.Optional(NonEmptyString),
|
||||
version: NonEmptyString,
|
||||
platform: NonEmptyString,
|
||||
deviceFamily: Type.Optional(NonEmptyString),
|
||||
modelIdentifier: Type.Optional(NonEmptyString),
|
||||
mode: GatewayClientModeSchema,
|
||||
instanceId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
caps: Type.Optional(Type.Array(NonEmptyString, { default: [] })),
|
||||
commands: Type.Optional(Type.Array(NonEmptyString)),
|
||||
permissions: Type.Optional(Type.Record(NonEmptyString, Type.Boolean())),
|
||||
pathEnv: Type.Optional(Type.String()),
|
||||
role: Type.Optional(NonEmptyString),
|
||||
scopes: Type.Optional(Type.Array(NonEmptyString)),
|
||||
device: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
publicKey: NonEmptyString,
|
||||
signature: NonEmptyString,
|
||||
signedAt: Type.Integer({ minimum: 0 }),
|
||||
nonce: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
auth: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
token: Type.Optional(Type.String()),
|
||||
bootstrapToken: Type.Optional(Type.String()),
|
||||
deviceToken: Type.Optional(Type.String()),
|
||||
password: Type.Optional(Type.String()),
|
||||
approvalRuntimeToken: Type.Optional(Type.String()),
|
||||
agentRuntimeIdentityToken: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
locale: Type.Optional(Type.String()),
|
||||
userAgent: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Successful gateway hello response with negotiated protocol and initial state. */
|
||||
export const HelloOkSchema = Type.Object(
|
||||
{
|
||||
type: Type.Literal("hello-ok"),
|
||||
protocol: Type.Integer({ minimum: 1 }),
|
||||
server: Type.Object(
|
||||
{
|
||||
version: NonEmptyString,
|
||||
connId: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
features: Type.Object(
|
||||
{
|
||||
methods: Type.Array(NonEmptyString),
|
||||
events: Type.Array(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
snapshot: SnapshotSchema,
|
||||
pluginSurfaceUrls: Type.Optional(Type.Record(NonEmptyString, NonEmptyString)),
|
||||
auth: Type.Object(
|
||||
{
|
||||
deviceToken: Type.Optional(NonEmptyString),
|
||||
role: NonEmptyString,
|
||||
scopes: Type.Array(NonEmptyString),
|
||||
issuedAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
deviceTokens: Type.Optional(
|
||||
Type.Array(
|
||||
Type.Object(
|
||||
{
|
||||
deviceToken: NonEmptyString,
|
||||
role: NonEmptyString,
|
||||
scopes: Type.Array(NonEmptyString),
|
||||
issuedAtMs: Type.Integer({ minimum: 0 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
policy: Type.Object(
|
||||
{
|
||||
maxPayload: Type.Integer({ minimum: 1 }),
|
||||
maxBufferedBytes: Type.Integer({ minimum: 1 }),
|
||||
tickIntervalMs: Type.Integer({ minimum: 1 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Standard structured error shape used in response frames and connect failures. */
|
||||
export const ErrorShapeSchema = Type.Object(
|
||||
{
|
||||
code: NonEmptyString,
|
||||
message: NonEmptyString,
|
||||
details: Type.Optional(Type.Unknown()),
|
||||
retryable: Type.Optional(Type.Boolean()),
|
||||
retryAfterMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Client request frame envelope; `method` selects the payload validator. */
|
||||
export const RequestFrameSchema = Type.Object(
|
||||
{
|
||||
type: Type.Literal("req"),
|
||||
id: NonEmptyString,
|
||||
method: NonEmptyString,
|
||||
params: Type.Optional(Type.Unknown()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Server response frame envelope paired with a prior request id. */
|
||||
export const ResponseFrameSchema = Type.Object(
|
||||
{
|
||||
type: Type.Literal("res"),
|
||||
id: NonEmptyString,
|
||||
ok: Type.Boolean(),
|
||||
payload: Type.Optional(Type.Unknown()),
|
||||
error: Type.Optional(ErrorShapeSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Server event frame envelope; `event` selects the payload validator. */
|
||||
export const EventFrameSchema = Type.Object(
|
||||
{
|
||||
type: Type.Literal("event"),
|
||||
event: NonEmptyString,
|
||||
payload: Type.Optional(Type.Unknown()),
|
||||
seq: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
stateVersion: Type.Optional(StateVersionSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
// Discriminated union of all top-level frames. Using a discriminator makes
|
||||
// downstream codegen (quicktype) produce tighter types instead of all-optional
|
||||
// blobs.
|
||||
export const GatewayFrameSchema = Type.Union(
|
||||
[RequestFrameSchema, ResponseFrameSchema, EventFrameSchema],
|
||||
{ discriminator: "type" },
|
||||
);
|
||||
200
packages/gateway-protocol/src/schema/logs-chat.ts
Normal file
200
packages/gateway-protocol/src/schema/logs-chat.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import type { Static } from "typebox";
|
||||
import { Type } from "typebox";
|
||||
import { ChatSendSessionKeyString, InputProvenanceSchema, NonEmptyString } from "./primitives.js";
|
||||
|
||||
/** Cursor-based request for the gateway log tail endpoint. */
|
||||
export const LogsTailParamsSchema = Type.Object(
|
||||
{
|
||||
cursor: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5000 })),
|
||||
maxBytes: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_000_000 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Gateway log tail payload returned to dashboard clients. */
|
||||
export const LogsTailResultSchema = Type.Object(
|
||||
{
|
||||
file: NonEmptyString,
|
||||
cursor: Type.Integer({ minimum: 0 }),
|
||||
size: Type.Integer({ minimum: 0 }),
|
||||
lines: Type.Array(Type.String()),
|
||||
truncated: Type.Optional(Type.Boolean()),
|
||||
reset: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Session-scoped history request used by WebChat and native WebSocket clients. */
|
||||
export const ChatHistoryParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),
|
||||
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
maxChars: Type.Optional(Type.Integer({ minimum: 1, maximum: 500_000 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Lightweight chat metadata request; optional agent scope keeps selector state explicit. */
|
||||
export const ChatMetadataParamsSchema = Type.Object(
|
||||
{
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Fetches one stored chat message without forcing history callers to request huge payloads. */
|
||||
export const ChatMessageGetParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
messageId: NonEmptyString,
|
||||
maxChars: Type.Optional(Type.Integer({ minimum: 1, maximum: 2_000_000 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Result envelope for single-message lookup, including the stable miss/visibility reason. */
|
||||
export const ChatMessageGetResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Boolean(),
|
||||
message: Type.Optional(Type.Unknown()),
|
||||
unavailableReason: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("not_found"),
|
||||
Type.Literal("oversized"),
|
||||
Type.Literal("not_visible"),
|
||||
]),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
/** Typed result shape for callers that branch on message availability. */
|
||||
export type ChatMessageGetResult = Static<typeof ChatMessageGetResultSchema>;
|
||||
|
||||
/** User-to-agent send request; idempotency key lets clients safely retry transport failures. */
|
||||
export const ChatSendParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: ChatSendSessionKeyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
sessionId: Type.Optional(NonEmptyString),
|
||||
message: Type.String(),
|
||||
thinking: Type.Optional(Type.String()),
|
||||
fastMode: Type.Optional(Type.Union([Type.Boolean(), Type.Literal("auto")])),
|
||||
// One-turn override for auto fast-mode cutoff seconds.
|
||||
fastAutoOnSeconds: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
deliver: Type.Optional(Type.Boolean()),
|
||||
originatingChannel: Type.Optional(Type.String()),
|
||||
originatingTo: Type.Optional(Type.String()),
|
||||
originatingAccountId: Type.Optional(Type.String()),
|
||||
originatingThreadId: Type.Optional(Type.String()),
|
||||
attachments: Type.Optional(Type.Array(Type.Unknown())),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
systemInputProvenance: Type.Optional(InputProvenanceSchema),
|
||||
systemProvenanceReceipt: Type.Optional(Type.String()),
|
||||
suppressCommandInterpretation: Type.Optional(Type.Boolean()),
|
||||
idempotencyKey: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Cancels the active or named run for a chat session. */
|
||||
export const ChatAbortParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
runId: Type.Optional(NonEmptyString),
|
||||
preserveSideRuns: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Inserts an operator-visible synthetic message into an existing chat transcript. */
|
||||
export const ChatInjectParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
message: NonEmptyString,
|
||||
label: Type.Optional(Type.String({ maxLength: 100 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Shared event fields preserve stream ordering and route events to the right session. */
|
||||
const ChatEventBaseSchema = {
|
||||
runId: NonEmptyString,
|
||||
sessionKey: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
spawnedBy: Type.Optional(NonEmptyString),
|
||||
seq: Type.Integer({ minimum: 0 }),
|
||||
};
|
||||
|
||||
/** Stable error categories exposed over the chat stream. */
|
||||
const ChatEventErrorKindSchema = Type.Union([
|
||||
Type.Literal("refusal"),
|
||||
Type.Literal("timeout"),
|
||||
Type.Literal("rate_limit"),
|
||||
Type.Literal("context_length"),
|
||||
Type.Literal("unknown"),
|
||||
]);
|
||||
|
||||
/** Incremental assistant output event; `replace` marks full-content refresh deltas. */
|
||||
export const ChatDeltaEventSchema = Type.Object(
|
||||
{
|
||||
...ChatEventBaseSchema,
|
||||
state: Type.Literal("delta"),
|
||||
message: Type.Optional(Type.Unknown()),
|
||||
deltaText: Type.String(),
|
||||
replace: Type.Optional(Type.Boolean()),
|
||||
usage: Type.Optional(Type.Unknown()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Successful terminal event for a completed chat run. */
|
||||
export const ChatFinalEventSchema = Type.Object(
|
||||
{
|
||||
...ChatEventBaseSchema,
|
||||
state: Type.Literal("final"),
|
||||
message: Type.Optional(Type.Unknown()),
|
||||
usage: Type.Optional(Type.Unknown()),
|
||||
stopReason: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Terminal event for user-initiated or coordinator-initiated cancellation. */
|
||||
export const ChatAbortedEventSchema = Type.Object(
|
||||
{
|
||||
...ChatEventBaseSchema,
|
||||
state: Type.Literal("aborted"),
|
||||
message: Type.Optional(Type.Unknown()),
|
||||
stopReason: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Terminal event for failed chat runs with an optional normalized failure kind. */
|
||||
export const ChatErrorEventSchema = Type.Object(
|
||||
{
|
||||
...ChatEventBaseSchema,
|
||||
state: Type.Literal("error"),
|
||||
message: Type.Optional(Type.Unknown()),
|
||||
errorMessage: Type.Optional(Type.String()),
|
||||
errorKind: Type.Optional(ChatEventErrorKindSchema),
|
||||
usage: Type.Optional(Type.Unknown()),
|
||||
stopReason: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Public chat stream event union consumed by gateway protocol validators. */
|
||||
export const ChatEventSchema = Type.Union([
|
||||
ChatDeltaEventSchema,
|
||||
ChatFinalEventSchema,
|
||||
ChatAbortedEventSchema,
|
||||
ChatErrorEventSchema,
|
||||
]);
|
||||
232
packages/gateway-protocol/src/schema/nodes.ts
Normal file
232
packages/gateway-protocol/src/schema/nodes.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/** Pending node work classes that the gateway may queue for paired devices. */
|
||||
const NodePendingWorkTypeSchema = Type.String({
|
||||
enum: ["status.request", "location.request"],
|
||||
});
|
||||
|
||||
/** Queue priority accepted when operators enqueue node work. */
|
||||
const NodePendingWorkPrioritySchema = Type.String({
|
||||
enum: ["normal", "high"],
|
||||
});
|
||||
|
||||
/** Reasons a node can report itself alive without implying an operator action. */
|
||||
export const NodePresenceAliveReasonSchema = Type.String({
|
||||
enum: [
|
||||
"background",
|
||||
"silent_push",
|
||||
"bg_app_refresh",
|
||||
"significant_location",
|
||||
"manual",
|
||||
"connect",
|
||||
],
|
||||
});
|
||||
|
||||
/** Presence heartbeat payload sent by remote nodes to refresh gateway state. */
|
||||
export const NodePresenceAlivePayloadSchema = Type.Object(
|
||||
{
|
||||
trigger: NodePresenceAliveReasonSchema,
|
||||
sentAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
displayName: Type.Optional(NonEmptyString),
|
||||
version: Type.Optional(NonEmptyString),
|
||||
platform: Type.Optional(NonEmptyString),
|
||||
deviceFamily: Type.Optional(NonEmptyString),
|
||||
modelIdentifier: Type.Optional(NonEmptyString),
|
||||
pushTransport: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Normalized result for node-originated events after gateway dispatch. */
|
||||
export const NodeEventResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Boolean(),
|
||||
event: NonEmptyString,
|
||||
handled: Type.Boolean(),
|
||||
reason: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Pairing request metadata advertised by a node before trust is granted. */
|
||||
export const NodePairRequestParamsSchema = Type.Object(
|
||||
{
|
||||
nodeId: NonEmptyString,
|
||||
displayName: Type.Optional(NonEmptyString),
|
||||
platform: Type.Optional(NonEmptyString),
|
||||
version: Type.Optional(NonEmptyString),
|
||||
coreVersion: Type.Optional(NonEmptyString),
|
||||
uiVersion: Type.Optional(NonEmptyString),
|
||||
deviceFamily: Type.Optional(NonEmptyString),
|
||||
modelIdentifier: Type.Optional(NonEmptyString),
|
||||
caps: Type.Optional(Type.Array(NonEmptyString)),
|
||||
commands: Type.Optional(Type.Array(NonEmptyString)),
|
||||
permissions: Type.Optional(Type.Record(NonEmptyString, Type.Boolean())),
|
||||
remoteIp: Type.Optional(NonEmptyString),
|
||||
silent: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Lists pending node-pairing requests. */
|
||||
export const NodePairListParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** Approves a pending node-pairing request by request id. */
|
||||
export const NodePairApproveParamsSchema = Type.Object(
|
||||
{ requestId: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Rejects a pending node-pairing request by request id. */
|
||||
export const NodePairRejectParamsSchema = Type.Object(
|
||||
{ requestId: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Removes an already paired node from the gateway trust set. */
|
||||
export const NodePairRemoveParamsSchema = Type.Object(
|
||||
{ nodeId: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Verifies node ownership with a short-lived pairing token. */
|
||||
export const NodePairVerifyParamsSchema = Type.Object(
|
||||
{ nodeId: NonEmptyString, token: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Renames a paired node while preserving its stable node id. */
|
||||
export const NodeRenameParamsSchema = Type.Object(
|
||||
{ nodeId: NonEmptyString, displayName: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Lists paired nodes known to the gateway. */
|
||||
export const NodeListParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** Acknowledges queued node work that the node has consumed. */
|
||||
export const NodePendingAckParamsSchema = Type.Object(
|
||||
{
|
||||
ids: Type.Array(NonEmptyString, { minItems: 1 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Requests detailed metadata for one paired node. */
|
||||
export const NodeDescribeParamsSchema = Type.Object(
|
||||
{ nodeId: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Invokes a command on a paired node; idempotency allows safe retries. */
|
||||
export const NodeInvokeParamsSchema = Type.Object(
|
||||
{
|
||||
nodeId: NonEmptyString,
|
||||
command: NonEmptyString,
|
||||
params: Type.Optional(Type.Unknown()),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
idempotencyKey: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Result callback payload for a node command invocation. */
|
||||
export const NodeInvokeResultParamsSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
nodeId: NonEmptyString,
|
||||
ok: Type.Boolean(),
|
||||
payload: Type.Optional(Type.Unknown()),
|
||||
payloadJSON: Type.Optional(Type.String()),
|
||||
error: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
code: Type.Optional(NonEmptyString),
|
||||
message: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Generic node event envelope accepted by the gateway. */
|
||||
export const NodeEventParamsSchema = Type.Object(
|
||||
{
|
||||
event: NonEmptyString,
|
||||
payload: Type.Optional(Type.Unknown()),
|
||||
payloadJSON: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Request for a bounded batch of queued work assigned to the calling node. */
|
||||
export const NodePendingDrainParamsSchema = Type.Object(
|
||||
{
|
||||
maxItems: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** One queued node-work item returned by pending-work drain calls. */
|
||||
export const NodePendingDrainItemSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
type: NodePendingWorkTypeSchema,
|
||||
priority: Type.String({ enum: ["default", "normal", "high"] }),
|
||||
createdAtMs: Type.Integer({ minimum: 0 }),
|
||||
expiresAtMs: Type.Optional(Type.Union([Type.Integer({ minimum: 0 }), Type.Null()])),
|
||||
payload: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Drain response with a revision marker for node queue state. */
|
||||
export const NodePendingDrainResultSchema = Type.Object(
|
||||
{
|
||||
nodeId: NonEmptyString,
|
||||
revision: Type.Integer({ minimum: 0 }),
|
||||
items: Type.Array(NodePendingDrainItemSchema),
|
||||
hasMore: Type.Boolean(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Enqueues gateway-initiated work for a paired node. */
|
||||
export const NodePendingEnqueueParamsSchema = Type.Object(
|
||||
{
|
||||
nodeId: NonEmptyString,
|
||||
type: NodePendingWorkTypeSchema,
|
||||
priority: Type.Optional(NodePendingWorkPrioritySchema),
|
||||
expiresInMs: Type.Optional(Type.Integer({ minimum: 1_000, maximum: 86_400_000 })),
|
||||
wake: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Enqueue result echoes queue revision and whether wake delivery was attempted. */
|
||||
export const NodePendingEnqueueResultSchema = Type.Object(
|
||||
{
|
||||
nodeId: NonEmptyString,
|
||||
revision: Type.Integer({ minimum: 0 }),
|
||||
queued: NodePendingDrainItemSchema,
|
||||
wakeTriggered: Type.Boolean(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Event payload used by the gateway to ask a node to run a command. */
|
||||
export const NodeInvokeRequestEventSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
nodeId: NonEmptyString,
|
||||
command: NonEmptyString,
|
||||
paramsJSON: Type.Optional(Type.String()),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
idempotencyKey: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
50
packages/gateway-protocol/src/schema/plugin-approvals.ts
Normal file
50
packages/gateway-protocol/src/schema/plugin-approvals.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Plugin approval schemas.
|
||||
*
|
||||
* These payloads cross from plugin/tool execution into reviewer-facing UI, so
|
||||
* title, description, decision set, and timeout limits are part of the public
|
||||
* gateway contract.
|
||||
*/
|
||||
const MAX_PLUGIN_APPROVAL_TIMEOUT_MS = 600_000;
|
||||
const PLUGIN_APPROVAL_TITLE_MAX_LENGTH = 80;
|
||||
const PLUGIN_APPROVAL_DESCRIPTION_MAX_LENGTH = 256;
|
||||
|
||||
/** Approval request raised by a plugin before a sensitive tool action proceeds. */
|
||||
export const PluginApprovalRequestParamsSchema = Type.Object(
|
||||
{
|
||||
pluginId: Type.Optional(NonEmptyString),
|
||||
title: Type.String({ minLength: 1, maxLength: PLUGIN_APPROVAL_TITLE_MAX_LENGTH }),
|
||||
description: Type.String({ minLength: 1, maxLength: PLUGIN_APPROVAL_DESCRIPTION_MAX_LENGTH }),
|
||||
severity: Type.Optional(Type.String({ enum: ["info", "warning", "critical"] })),
|
||||
toolName: Type.Optional(Type.String()),
|
||||
toolCallId: Type.Optional(Type.String()),
|
||||
allowedDecisions: Type.Optional(
|
||||
Type.Array(Type.String({ enum: ["allow-once", "allow-always", "deny"] }), {
|
||||
minItems: 1,
|
||||
maxItems: 3,
|
||||
}),
|
||||
),
|
||||
agentId: Type.Optional(Type.String()),
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
turnSourceChannel: Type.Optional(Type.String()),
|
||||
turnSourceTo: Type.Optional(Type.String()),
|
||||
turnSourceAccountId: Type.Optional(Type.String()),
|
||||
turnSourceThreadId: Type.Optional(Type.Union([Type.String(), Type.Number()])),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_PLUGIN_APPROVAL_TIMEOUT_MS })),
|
||||
twoPhase: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Reviewer decision payload resolving one pending plugin approval request. */
|
||||
export const PluginApprovalResolveParamsSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
decision: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
84
packages/gateway-protocol/src/schema/plugins.ts
Normal file
84
packages/gateway-protocol/src/schema/plugins.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Plugin control-surface protocol schemas.
|
||||
*
|
||||
* These payloads let the gateway expose plugin-provided UI actions without
|
||||
* baking plugin-specific payload shapes into the core protocol.
|
||||
*/
|
||||
/** Arbitrary plugin-owned JSON payload carried opaquely through the gateway. */
|
||||
export const PluginJsonValueSchema = Type.Unknown();
|
||||
|
||||
/** Descriptor for one plugin-provided control UI action or surface. */
|
||||
export const PluginControlUiDescriptorSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
pluginId: NonEmptyString,
|
||||
pluginName: Type.Optional(NonEmptyString),
|
||||
surface: Type.Union([
|
||||
Type.Literal("session"),
|
||||
Type.Literal("tool"),
|
||||
Type.Literal("run"),
|
||||
Type.Literal("settings"),
|
||||
]),
|
||||
label: NonEmptyString,
|
||||
description: Type.Optional(Type.String()),
|
||||
placement: Type.Optional(Type.String()),
|
||||
schema: Type.Optional(PluginJsonValueSchema),
|
||||
requiredScopes: Type.Optional(Type.Array(NonEmptyString)),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Empty request payload for listing plugin UI descriptors. */
|
||||
export const PluginsUiDescriptorsParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** Response payload containing all plugin UI descriptors visible to the client. */
|
||||
export const PluginsUiDescriptorsResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Literal(true),
|
||||
descriptors: Type.Array(PluginControlUiDescriptorSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Request payload for invoking one plugin-owned session action. */
|
||||
export const PluginsSessionActionParamsSchema = Type.Object(
|
||||
{
|
||||
pluginId: NonEmptyString,
|
||||
actionId: NonEmptyString,
|
||||
sessionKey: Type.Optional(NonEmptyString),
|
||||
payload: Type.Optional(PluginJsonValueSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Successful plugin action result, optionally continuing the agent turn. */
|
||||
export const PluginsSessionActionSuccessResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Literal(true),
|
||||
result: Type.Optional(PluginJsonValueSchema),
|
||||
continueAgent: Type.Optional(Type.Boolean()),
|
||||
reply: Type.Optional(PluginJsonValueSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Failed plugin action result with plugin-owned detail payload. */
|
||||
export const PluginsSessionActionFailureResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Literal(false),
|
||||
error: Type.String(),
|
||||
code: Type.Optional(Type.String()),
|
||||
details: Type.Optional(PluginJsonValueSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Discriminated plugin action result returned to gateway clients. */
|
||||
export const PluginsSessionActionResultSchema = Type.Union([
|
||||
PluginsSessionActionSuccessResultSchema,
|
||||
PluginsSessionActionFailureResultSchema,
|
||||
]);
|
||||
113
packages/gateway-protocol/src/schema/primitives.ts
Normal file
113
packages/gateway-protocol/src/schema/primitives.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../client-info.js";
|
||||
import {
|
||||
EXEC_SECRET_REF_ID_JSON_SCHEMA_PATTERN,
|
||||
FILE_SECRET_REF_ID_ABSOLUTE_JSON_SCHEMA_PATTERN,
|
||||
FILE_SECRET_REF_ID_INVALID_ESCAPE_JSON_SCHEMA_PATTERN,
|
||||
SECRET_PROVIDER_ALIAS_PATTERN,
|
||||
SINGLE_VALUE_FILE_REF_ID,
|
||||
} from "../secret-ref-contract.js";
|
||||
|
||||
/**
|
||||
* Shared schema primitives reused by gateway protocol request/result schemas.
|
||||
*
|
||||
* Keep these schemas small and transport-oriented; feature-specific validation
|
||||
* belongs in the owning schema module or runtime handler.
|
||||
*/
|
||||
const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
|
||||
const INPUT_PROVENANCE_KIND_VALUES = ["external_user", "inter_session", "internal_system"] as const;
|
||||
const SESSION_LABEL_MAX_LENGTH = 512;
|
||||
|
||||
/** Non-empty string primitive for protocol fields that reject blank values. */
|
||||
export const NonEmptyString = Type.String({ minLength: 1 });
|
||||
/** Maximum stable session key length accepted by chat-send protocol requests. */
|
||||
export const CHAT_SEND_SESSION_KEY_MAX_LENGTH = 512;
|
||||
/** Chat-send session key string primitive with bounded length. */
|
||||
export const ChatSendSessionKeyString = Type.String({
|
||||
minLength: 1,
|
||||
maxLength: CHAT_SEND_SESSION_KEY_MAX_LENGTH,
|
||||
});
|
||||
/** Human-readable session label primitive with bounded display length. */
|
||||
export const SessionLabelString = Type.String({
|
||||
minLength: 1,
|
||||
maxLength: SESSION_LABEL_MAX_LENGTH,
|
||||
});
|
||||
/** Provenance marker for content copied from another user/session/system source. */
|
||||
export const InputProvenanceSchema = Type.Object(
|
||||
{
|
||||
kind: Type.String({ enum: [...INPUT_PROVENANCE_KIND_VALUES] }),
|
||||
originSessionId: Type.Optional(Type.String()),
|
||||
sourceSessionKey: Type.Optional(Type.String()),
|
||||
sourceChannel: Type.Optional(Type.String()),
|
||||
sourceTool: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Closed gateway client id schema aligned with `GATEWAY_CLIENT_IDS`. */
|
||||
export const GatewayClientIdSchema = Type.Enum(GATEWAY_CLIENT_IDS);
|
||||
|
||||
/** Closed gateway client mode schema aligned with `GATEWAY_CLIENT_MODES`. */
|
||||
export const GatewayClientModeSchema = Type.Enum(GATEWAY_CLIENT_MODES);
|
||||
|
||||
/** Supported secret reference backing stores for protocol SecretRef payloads. */
|
||||
export const SecretRefSourceSchema = Type.Union([
|
||||
Type.Literal("env"),
|
||||
Type.Literal("file"),
|
||||
Type.Literal("exec"),
|
||||
]);
|
||||
|
||||
const SecretProviderAliasString = Type.String({
|
||||
pattern: SECRET_PROVIDER_ALIAS_PATTERN.source,
|
||||
});
|
||||
|
||||
const EnvSecretRefSchema = Type.Object(
|
||||
{
|
||||
source: Type.Literal("env"),
|
||||
provider: SecretProviderAliasString,
|
||||
id: Type.String({ pattern: ENV_SECRET_REF_ID_RE.source }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
const FileSecretRefIdSchema = Type.Unsafe<string>({
|
||||
type: "string",
|
||||
anyOf: [
|
||||
{ const: SINGLE_VALUE_FILE_REF_ID },
|
||||
{
|
||||
allOf: [
|
||||
{ pattern: FILE_SECRET_REF_ID_ABSOLUTE_JSON_SCHEMA_PATTERN },
|
||||
{ not: { pattern: FILE_SECRET_REF_ID_INVALID_ESCAPE_JSON_SCHEMA_PATTERN } },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const FileSecretRefSchema = Type.Object(
|
||||
{
|
||||
source: Type.Literal("file"),
|
||||
provider: SecretProviderAliasString,
|
||||
id: FileSecretRefIdSchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
const ExecSecretRefSchema = Type.Object(
|
||||
{
|
||||
source: Type.Literal("exec"),
|
||||
provider: SecretProviderAliasString,
|
||||
id: Type.String({ pattern: EXEC_SECRET_REF_ID_JSON_SCHEMA_PATTERN }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Structured secret reference accepted by config and channel protocol payloads. */
|
||||
export const SecretRefSchema = Type.Union([
|
||||
EnvSecretRefSchema,
|
||||
FileSecretRefSchema,
|
||||
ExecSecretRefSchema,
|
||||
]);
|
||||
|
||||
/** Secret input value: either an inline string or a structured SecretRef. */
|
||||
export const SecretInputSchema = Type.Union([Type.String(), SecretRefSchema]);
|
||||
645
packages/gateway-protocol/src/schema/protocol-schemas.ts
Normal file
645
packages/gateway-protocol/src/schema/protocol-schemas.ts
Normal file
@@ -0,0 +1,645 @@
|
||||
/**
|
||||
* Central registry for every gateway protocol schema.
|
||||
*
|
||||
* The keys in this object are the public schema names used by validators,
|
||||
* generated static types, and protocol tooling. Add new entries here only after
|
||||
* the owning schema module exports the canonical TypeBox schema.
|
||||
*/
|
||||
import type { TSchema } from "typebox";
|
||||
import {
|
||||
AgentEventSchema,
|
||||
AgentIdentityParamsSchema,
|
||||
AgentIdentityResultSchema,
|
||||
AgentParamsSchema,
|
||||
AgentWaitParamsSchema,
|
||||
MessageActionParamsSchema,
|
||||
PollParamsSchema,
|
||||
SendParamsSchema,
|
||||
WakeParamsSchema,
|
||||
} from "./agent.js";
|
||||
import {
|
||||
AgentSummarySchema,
|
||||
AgentsCreateParamsSchema,
|
||||
AgentsCreateResultSchema,
|
||||
AgentsDeleteParamsSchema,
|
||||
AgentsDeleteResultSchema,
|
||||
AgentsFileEntrySchema,
|
||||
AgentsFilesGetParamsSchema,
|
||||
AgentsFilesGetResultSchema,
|
||||
AgentsFilesListParamsSchema,
|
||||
AgentsFilesListResultSchema,
|
||||
AgentsFilesSetParamsSchema,
|
||||
AgentsFilesSetResultSchema,
|
||||
AgentsListParamsSchema,
|
||||
AgentsListResultSchema,
|
||||
AgentsUpdateParamsSchema,
|
||||
AgentsUpdateResultSchema,
|
||||
ModelChoiceSchema,
|
||||
ModelsListParamsSchema,
|
||||
ModelsListResultSchema,
|
||||
SkillsBinsParamsSchema,
|
||||
SkillsBinsResultSchema,
|
||||
SkillsDetailParamsSchema,
|
||||
SkillsDetailResultSchema,
|
||||
SkillsInstallParamsSchema,
|
||||
SkillsProposalActionParamsSchema,
|
||||
SkillsProposalApplyResultSchema,
|
||||
SkillsProposalCreateParamsSchema,
|
||||
SkillsProposalInspectParamsSchema,
|
||||
SkillsProposalInspectResultSchema,
|
||||
SkillsProposalRecordResultSchema,
|
||||
SkillsProposalRequestRevisionParamsSchema,
|
||||
SkillsProposalRequestRevisionResultSchema,
|
||||
SkillsProposalReviseParamsSchema,
|
||||
SkillsProposalUpdateParamsSchema,
|
||||
SkillsProposalsListParamsSchema,
|
||||
SkillsProposalsListResultSchema,
|
||||
SkillsSearchParamsSchema,
|
||||
SkillsSearchResultSchema,
|
||||
SkillsSecurityVerdictsParamsSchema,
|
||||
SkillsSecurityVerdictsResultSchema,
|
||||
SkillsSkillCardParamsSchema,
|
||||
SkillsSkillCardResultSchema,
|
||||
SkillsStatusParamsSchema,
|
||||
SkillsUploadBeginParamsSchema,
|
||||
SkillsUploadChunkParamsSchema,
|
||||
SkillsUploadCommitParamsSchema,
|
||||
SkillsUpdateParamsSchema,
|
||||
ToolCatalogEntrySchema,
|
||||
ToolCatalogGroupSchema,
|
||||
ToolCatalogProfileSchema,
|
||||
ToolsCatalogParamsSchema,
|
||||
ToolsCatalogResultSchema,
|
||||
ToolsEffectiveEntrySchema,
|
||||
ToolsEffectiveGroupSchema,
|
||||
ToolsEffectiveNoticeSchema,
|
||||
ToolsEffectiveParamsSchema,
|
||||
ToolsEffectiveResultSchema,
|
||||
ToolsInvokeErrorSchema,
|
||||
ToolsInvokeParamsSchema,
|
||||
ToolsInvokeResultSchema,
|
||||
} from "./agents-models-skills.js";
|
||||
import {
|
||||
ArtifactSummarySchema,
|
||||
ArtifactsDownloadParamsSchema,
|
||||
ArtifactsDownloadResultSchema,
|
||||
ArtifactsGetParamsSchema,
|
||||
ArtifactsGetResultSchema,
|
||||
ArtifactsListParamsSchema,
|
||||
ArtifactsListResultSchema,
|
||||
} from "./artifacts.js";
|
||||
import {
|
||||
ChannelsStartParamsSchema,
|
||||
ChannelsStopParamsSchema,
|
||||
ChannelsLogoutParamsSchema,
|
||||
TalkEventSchema,
|
||||
TalkCatalogParamsSchema,
|
||||
TalkCatalogResultSchema,
|
||||
TalkClientCreateParamsSchema,
|
||||
TalkClientCreateResultSchema,
|
||||
TalkAgentControlResultSchema,
|
||||
TalkClientSteerParamsSchema,
|
||||
TalkClientToolCallParamsSchema,
|
||||
TalkClientToolCallResultSchema,
|
||||
TalkConfigParamsSchema,
|
||||
TalkConfigResultSchema,
|
||||
TalkSessionAppendAudioParamsSchema,
|
||||
TalkSessionCancelOutputParamsSchema,
|
||||
TalkSessionCancelTurnParamsSchema,
|
||||
TalkSessionCloseParamsSchema,
|
||||
TalkSessionCreateParamsSchema,
|
||||
TalkSessionCreateResultSchema,
|
||||
TalkSessionJoinParamsSchema,
|
||||
TalkSessionJoinResultSchema,
|
||||
TalkSessionOkResultSchema,
|
||||
TalkSessionSteerParamsSchema,
|
||||
TalkSessionSubmitToolResultParamsSchema,
|
||||
TalkSessionTurnResultSchema,
|
||||
TalkSessionTurnParamsSchema,
|
||||
TalkSpeakParamsSchema,
|
||||
TalkSpeakResultSchema,
|
||||
ChannelsStatusParamsSchema,
|
||||
ChannelsStatusResultSchema,
|
||||
TalkModeParamsSchema,
|
||||
WebLoginStartParamsSchema,
|
||||
WebLoginWaitParamsSchema,
|
||||
} from "./channels.js";
|
||||
import {
|
||||
CommandEntrySchema,
|
||||
CommandsListParamsSchema,
|
||||
CommandsListResultSchema,
|
||||
} from "./commands.js";
|
||||
import {
|
||||
ConfigApplyParamsSchema,
|
||||
ConfigGetParamsSchema,
|
||||
ConfigPatchParamsSchema,
|
||||
ConfigSchemaLookupParamsSchema,
|
||||
ConfigSchemaLookupResultSchema,
|
||||
ConfigSchemaParamsSchema,
|
||||
ConfigSchemaResponseSchema,
|
||||
ConfigSetParamsSchema,
|
||||
UpdateStatusParamsSchema,
|
||||
UpdateRunParamsSchema,
|
||||
} from "./config.js";
|
||||
import { CrestodianChatParamsSchema, CrestodianChatResultSchema } from "./crestodian.js";
|
||||
import {
|
||||
CronAddParamsSchema,
|
||||
CronGetParamsSchema,
|
||||
CronJobSchema,
|
||||
CronListParamsSchema,
|
||||
CronRemoveParamsSchema,
|
||||
CronRunLogEntrySchema,
|
||||
CronRunParamsSchema,
|
||||
CronRunsParamsSchema,
|
||||
CronStatusParamsSchema,
|
||||
CronUpdateParamsSchema,
|
||||
} from "./cron.js";
|
||||
import {
|
||||
DevicePairApproveParamsSchema,
|
||||
DevicePairListParamsSchema,
|
||||
DevicePairRemoveParamsSchema,
|
||||
DevicePairRejectParamsSchema,
|
||||
DevicePairRequestedEventSchema,
|
||||
DevicePairResolvedEventSchema,
|
||||
DevicePairSetupCodeParamsSchema,
|
||||
DevicePairSetupCodeResultSchema,
|
||||
DeviceTokenRevokeParamsSchema,
|
||||
DeviceTokenRotateParamsSchema,
|
||||
} from "./devices.js";
|
||||
import {
|
||||
EnvironmentSummarySchema,
|
||||
EnvironmentsListParamsSchema,
|
||||
EnvironmentsListResultSchema,
|
||||
EnvironmentsStatusParamsSchema,
|
||||
EnvironmentsStatusResultSchema,
|
||||
EnvironmentStatusSchema,
|
||||
} from "./environments.js";
|
||||
import {
|
||||
ExecApprovalsGetParamsSchema,
|
||||
ExecApprovalsNodeGetParamsSchema,
|
||||
ExecApprovalsNodeSetParamsSchema,
|
||||
ExecApprovalsSetParamsSchema,
|
||||
ExecApprovalsSnapshotSchema,
|
||||
ExecApprovalGetParamsSchema,
|
||||
ExecApprovalRequestParamsSchema,
|
||||
ExecApprovalResolveParamsSchema,
|
||||
} from "./exec-approvals.js";
|
||||
import {
|
||||
ConnectParamsSchema,
|
||||
ErrorShapeSchema,
|
||||
EventFrameSchema,
|
||||
GatewayFrameSchema,
|
||||
HelloOkSchema,
|
||||
RequestFrameSchema,
|
||||
ResponseFrameSchema,
|
||||
ShutdownEventSchema,
|
||||
TickEventSchema,
|
||||
} from "./frames.js";
|
||||
import {
|
||||
ChatAbortedEventSchema,
|
||||
ChatAbortParamsSchema,
|
||||
ChatDeltaEventSchema,
|
||||
ChatErrorEventSchema,
|
||||
ChatEventSchema,
|
||||
ChatFinalEventSchema,
|
||||
ChatHistoryParamsSchema,
|
||||
ChatMetadataParamsSchema,
|
||||
ChatMessageGetParamsSchema,
|
||||
ChatMessageGetResultSchema,
|
||||
ChatInjectParamsSchema,
|
||||
ChatSendParamsSchema,
|
||||
LogsTailParamsSchema,
|
||||
LogsTailResultSchema,
|
||||
} from "./logs-chat.js";
|
||||
import {
|
||||
NodeDescribeParamsSchema,
|
||||
NodeEventParamsSchema,
|
||||
NodeEventResultSchema,
|
||||
NodePendingDrainParamsSchema,
|
||||
NodePendingDrainResultSchema,
|
||||
NodePendingEnqueueParamsSchema,
|
||||
NodePendingEnqueueResultSchema,
|
||||
NodePresenceAlivePayloadSchema,
|
||||
NodePresenceAliveReasonSchema,
|
||||
NodeInvokeParamsSchema,
|
||||
NodeInvokeResultParamsSchema,
|
||||
NodeInvokeRequestEventSchema,
|
||||
NodeListParamsSchema,
|
||||
NodePendingAckParamsSchema,
|
||||
NodePairApproveParamsSchema,
|
||||
NodePairListParamsSchema,
|
||||
NodePairRemoveParamsSchema,
|
||||
NodePairRejectParamsSchema,
|
||||
NodePairRequestParamsSchema,
|
||||
NodePairVerifyParamsSchema,
|
||||
NodeRenameParamsSchema,
|
||||
} from "./nodes.js";
|
||||
import {
|
||||
PluginApprovalRequestParamsSchema,
|
||||
PluginApprovalResolveParamsSchema,
|
||||
} from "./plugin-approvals.js";
|
||||
import {
|
||||
PluginControlUiDescriptorSchema,
|
||||
PluginsSessionActionFailureResultSchema,
|
||||
PluginsSessionActionParamsSchema,
|
||||
PluginsSessionActionResultSchema,
|
||||
PluginsSessionActionSuccessResultSchema,
|
||||
PluginsUiDescriptorsParamsSchema,
|
||||
PluginsUiDescriptorsResultSchema,
|
||||
} from "./plugins.js";
|
||||
import { PushTestParamsSchema, PushTestResultSchema } from "./push.js";
|
||||
import {
|
||||
SecretsReloadParamsSchema,
|
||||
SecretsResolveAssignmentSchema,
|
||||
SecretsResolveParamsSchema,
|
||||
SecretsResolveResultSchema,
|
||||
} from "./secrets.js";
|
||||
import {
|
||||
SessionsAbortParamsSchema,
|
||||
SessionsCompactParamsSchema,
|
||||
SessionsCompactionBranchParamsSchema,
|
||||
SessionsCompactionBranchResultSchema,
|
||||
SessionsCompactionGetParamsSchema,
|
||||
SessionsCompactionGetResultSchema,
|
||||
SessionsCompactionListParamsSchema,
|
||||
SessionsCompactionListResultSchema,
|
||||
SessionsCompactionRestoreParamsSchema,
|
||||
SessionsCompactionRestoreResultSchema,
|
||||
SessionFileBrowserEntrySchema,
|
||||
SessionFileBrowserResultSchema,
|
||||
SessionCompactionCheckpointSchema,
|
||||
SessionFileEntrySchema,
|
||||
SessionFileKindSchema,
|
||||
SessionFileRelevanceSchema,
|
||||
SessionOperationEventSchema,
|
||||
SessionsCleanupParamsSchema,
|
||||
SessionsCreateParamsSchema,
|
||||
SessionsDeleteParamsSchema,
|
||||
SessionsDescribeParamsSchema,
|
||||
SessionsFilesGetParamsSchema,
|
||||
SessionsFilesGetResultSchema,
|
||||
SessionsFilesListParamsSchema,
|
||||
SessionsFilesListResultSchema,
|
||||
SessionsListParamsSchema,
|
||||
SessionsMessagesSubscribeParamsSchema,
|
||||
SessionsMessagesUnsubscribeParamsSchema,
|
||||
SessionsPatchParamsSchema,
|
||||
SessionsPluginPatchParamsSchema,
|
||||
SessionsPluginPatchResultSchema,
|
||||
SessionsPreviewParamsSchema,
|
||||
SessionsResetParamsSchema,
|
||||
SessionsResolveParamsSchema,
|
||||
SessionsSendParamsSchema,
|
||||
SessionsUsageParamsSchema,
|
||||
} from "./sessions.js";
|
||||
import { PresenceEntrySchema, SnapshotSchema, StateVersionSchema } from "./snapshot.js";
|
||||
import {
|
||||
TasksCancelParamsSchema,
|
||||
TasksCancelResultSchema,
|
||||
TasksGetParamsSchema,
|
||||
TasksGetResultSchema,
|
||||
TasksListParamsSchema,
|
||||
TasksListResultSchema,
|
||||
TaskSummarySchema,
|
||||
} from "./tasks.js";
|
||||
import {
|
||||
TerminalAckResultSchema,
|
||||
TerminalAttachParamsSchema,
|
||||
TerminalAttachResultSchema,
|
||||
TerminalCloseParamsSchema,
|
||||
TerminalDataEventSchema,
|
||||
TerminalEventSchema,
|
||||
TerminalExitEventSchema,
|
||||
TerminalInputParamsSchema,
|
||||
TerminalListResultSchema,
|
||||
TerminalOpenParamsSchema,
|
||||
TerminalOpenResultSchema,
|
||||
TerminalResizeParamsSchema,
|
||||
TerminalSessionInfoSchema,
|
||||
TerminalTextParamsSchema,
|
||||
TerminalTextResultSchema,
|
||||
} from "./terminal.js";
|
||||
import {
|
||||
WizardCancelParamsSchema,
|
||||
WizardNextParamsSchema,
|
||||
WizardNextResultSchema,
|
||||
WizardStartParamsSchema,
|
||||
WizardStartResultSchema,
|
||||
WizardStatusParamsSchema,
|
||||
WizardStatusResultSchema,
|
||||
WizardStepSchema,
|
||||
} from "./wizard.js";
|
||||
|
||||
/** Public schema registry keyed by stable protocol schema name. */
|
||||
export const ProtocolSchemas = {
|
||||
// Handshake, transport frames, state snapshots, and shared error envelopes.
|
||||
ConnectParams: ConnectParamsSchema,
|
||||
HelloOk: HelloOkSchema,
|
||||
RequestFrame: RequestFrameSchema,
|
||||
ResponseFrame: ResponseFrameSchema,
|
||||
EventFrame: EventFrameSchema,
|
||||
GatewayFrame: GatewayFrameSchema,
|
||||
PresenceEntry: PresenceEntrySchema,
|
||||
StateVersion: StateVersionSchema,
|
||||
Snapshot: SnapshotSchema,
|
||||
ErrorShape: ErrorShapeSchema,
|
||||
|
||||
// Environment and agent-facing control RPC payloads.
|
||||
EnvironmentStatus: EnvironmentStatusSchema,
|
||||
EnvironmentSummary: EnvironmentSummarySchema,
|
||||
EnvironmentsListParams: EnvironmentsListParamsSchema,
|
||||
EnvironmentsListResult: EnvironmentsListResultSchema,
|
||||
EnvironmentsStatusParams: EnvironmentsStatusParamsSchema,
|
||||
EnvironmentsStatusResult: EnvironmentsStatusResultSchema,
|
||||
AgentEvent: AgentEventSchema,
|
||||
MessageActionParams: MessageActionParamsSchema,
|
||||
SendParams: SendParamsSchema,
|
||||
PollParams: PollParamsSchema,
|
||||
AgentParams: AgentParamsSchema,
|
||||
AgentIdentityParams: AgentIdentityParamsSchema,
|
||||
AgentIdentityResult: AgentIdentityResultSchema,
|
||||
AgentWaitParams: AgentWaitParamsSchema,
|
||||
WakeParams: WakeParamsSchema,
|
||||
|
||||
// Node pairing, invocation, presence, and pending-queue payloads.
|
||||
NodePairRequestParams: NodePairRequestParamsSchema,
|
||||
NodePairListParams: NodePairListParamsSchema,
|
||||
NodePairApproveParams: NodePairApproveParamsSchema,
|
||||
NodePairRejectParams: NodePairRejectParamsSchema,
|
||||
NodePairRemoveParams: NodePairRemoveParamsSchema,
|
||||
NodePairVerifyParams: NodePairVerifyParamsSchema,
|
||||
NodeRenameParams: NodeRenameParamsSchema,
|
||||
NodeListParams: NodeListParamsSchema,
|
||||
NodePendingAckParams: NodePendingAckParamsSchema,
|
||||
NodeDescribeParams: NodeDescribeParamsSchema,
|
||||
NodeInvokeParams: NodeInvokeParamsSchema,
|
||||
NodeInvokeResultParams: NodeInvokeResultParamsSchema,
|
||||
NodeEventParams: NodeEventParamsSchema,
|
||||
NodeEventResult: NodeEventResultSchema,
|
||||
NodePresenceAlivePayload: NodePresenceAlivePayloadSchema,
|
||||
NodePresenceAliveReason: NodePresenceAliveReasonSchema,
|
||||
NodePendingDrainParams: NodePendingDrainParamsSchema,
|
||||
NodePendingDrainResult: NodePendingDrainResultSchema,
|
||||
NodePendingEnqueueParams: NodePendingEnqueueParamsSchema,
|
||||
NodePendingEnqueueResult: NodePendingEnqueueResultSchema,
|
||||
NodeInvokeRequestEvent: NodeInvokeRequestEventSchema,
|
||||
|
||||
// Push and secret-resolution payloads used by mobile/control integrations.
|
||||
PushTestParams: PushTestParamsSchema,
|
||||
PushTestResult: PushTestResultSchema,
|
||||
SecretsReloadParams: SecretsReloadParamsSchema,
|
||||
SecretsResolveParams: SecretsResolveParamsSchema,
|
||||
SecretsResolveAssignment: SecretsResolveAssignmentSchema,
|
||||
SecretsResolveResult: SecretsResolveResultSchema,
|
||||
|
||||
// Session lifecycle, message routing, compaction, and usage accounting.
|
||||
SessionsListParams: SessionsListParamsSchema,
|
||||
SessionsCleanupParams: SessionsCleanupParamsSchema,
|
||||
SessionsPreviewParams: SessionsPreviewParamsSchema,
|
||||
SessionsDescribeParams: SessionsDescribeParamsSchema,
|
||||
SessionsResolveParams: SessionsResolveParamsSchema,
|
||||
SessionCompactionCheckpoint: SessionCompactionCheckpointSchema,
|
||||
SessionOperationEvent: SessionOperationEventSchema,
|
||||
SessionsCompactionListParams: SessionsCompactionListParamsSchema,
|
||||
SessionsCompactionGetParams: SessionsCompactionGetParamsSchema,
|
||||
SessionsCompactionBranchParams: SessionsCompactionBranchParamsSchema,
|
||||
SessionsCompactionRestoreParams: SessionsCompactionRestoreParamsSchema,
|
||||
SessionsCompactionListResult: SessionsCompactionListResultSchema,
|
||||
SessionsCompactionGetResult: SessionsCompactionGetResultSchema,
|
||||
SessionsCompactionBranchResult: SessionsCompactionBranchResultSchema,
|
||||
SessionsCompactionRestoreResult: SessionsCompactionRestoreResultSchema,
|
||||
SessionFileBrowserEntry: SessionFileBrowserEntrySchema,
|
||||
SessionFileBrowserResult: SessionFileBrowserResultSchema,
|
||||
SessionFileKind: SessionFileKindSchema,
|
||||
SessionFileEntry: SessionFileEntrySchema,
|
||||
SessionFileRelevance: SessionFileRelevanceSchema,
|
||||
SessionsFilesListParams: SessionsFilesListParamsSchema,
|
||||
SessionsFilesListResult: SessionsFilesListResultSchema,
|
||||
SessionsFilesGetParams: SessionsFilesGetParamsSchema,
|
||||
SessionsFilesGetResult: SessionsFilesGetResultSchema,
|
||||
SessionsCreateParams: SessionsCreateParamsSchema,
|
||||
SessionsSendParams: SessionsSendParamsSchema,
|
||||
SessionsMessagesSubscribeParams: SessionsMessagesSubscribeParamsSchema,
|
||||
SessionsMessagesUnsubscribeParams: SessionsMessagesUnsubscribeParamsSchema,
|
||||
SessionsAbortParams: SessionsAbortParamsSchema,
|
||||
SessionsPatchParams: SessionsPatchParamsSchema,
|
||||
SessionsPluginPatchParams: SessionsPluginPatchParamsSchema,
|
||||
SessionsPluginPatchResult: SessionsPluginPatchResultSchema,
|
||||
SessionsResetParams: SessionsResetParamsSchema,
|
||||
SessionsDeleteParams: SessionsDeleteParamsSchema,
|
||||
SessionsCompactParams: SessionsCompactParamsSchema,
|
||||
SessionsUsageParams: SessionsUsageParamsSchema,
|
||||
|
||||
// Task ledger and config/wizard setup payloads.
|
||||
TaskSummary: TaskSummarySchema,
|
||||
TasksListParams: TasksListParamsSchema,
|
||||
TasksListResult: TasksListResultSchema,
|
||||
TasksGetParams: TasksGetParamsSchema,
|
||||
TasksGetResult: TasksGetResultSchema,
|
||||
TasksCancelParams: TasksCancelParamsSchema,
|
||||
TasksCancelResult: TasksCancelResultSchema,
|
||||
ConfigGetParams: ConfigGetParamsSchema,
|
||||
ConfigSetParams: ConfigSetParamsSchema,
|
||||
ConfigApplyParams: ConfigApplyParamsSchema,
|
||||
ConfigPatchParams: ConfigPatchParamsSchema,
|
||||
ConfigSchemaParams: ConfigSchemaParamsSchema,
|
||||
ConfigSchemaLookupParams: ConfigSchemaLookupParamsSchema,
|
||||
ConfigSchemaResponse: ConfigSchemaResponseSchema,
|
||||
ConfigSchemaLookupResult: ConfigSchemaLookupResultSchema,
|
||||
CrestodianChatParams: CrestodianChatParamsSchema,
|
||||
CrestodianChatResult: CrestodianChatResultSchema,
|
||||
WizardStartParams: WizardStartParamsSchema,
|
||||
WizardNextParams: WizardNextParamsSchema,
|
||||
WizardCancelParams: WizardCancelParamsSchema,
|
||||
WizardStatusParams: WizardStatusParamsSchema,
|
||||
WizardStep: WizardStepSchema,
|
||||
WizardNextResult: WizardNextResultSchema,
|
||||
WizardStartResult: WizardStartResultSchema,
|
||||
WizardStatusResult: WizardStatusResultSchema,
|
||||
|
||||
// Realtime Talk client/session events and channel control payloads.
|
||||
TalkModeParams: TalkModeParamsSchema,
|
||||
TalkEvent: TalkEventSchema,
|
||||
TalkCatalogParams: TalkCatalogParamsSchema,
|
||||
TalkCatalogResult: TalkCatalogResultSchema,
|
||||
TalkClientCreateParams: TalkClientCreateParamsSchema,
|
||||
TalkClientCreateResult: TalkClientCreateResultSchema,
|
||||
TalkClientSteerParams: TalkClientSteerParamsSchema,
|
||||
TalkAgentControlResult: TalkAgentControlResultSchema,
|
||||
TalkClientToolCallParams: TalkClientToolCallParamsSchema,
|
||||
TalkClientToolCallResult: TalkClientToolCallResultSchema,
|
||||
TalkConfigParams: TalkConfigParamsSchema,
|
||||
TalkConfigResult: TalkConfigResultSchema,
|
||||
TalkSessionAppendAudioParams: TalkSessionAppendAudioParamsSchema,
|
||||
TalkSessionCancelOutputParams: TalkSessionCancelOutputParamsSchema,
|
||||
TalkSessionCancelTurnParams: TalkSessionCancelTurnParamsSchema,
|
||||
TalkSessionCreateParams: TalkSessionCreateParamsSchema,
|
||||
TalkSessionCreateResult: TalkSessionCreateResultSchema,
|
||||
TalkSessionJoinParams: TalkSessionJoinParamsSchema,
|
||||
TalkSessionJoinResult: TalkSessionJoinResultSchema,
|
||||
TalkSessionTurnParams: TalkSessionTurnParamsSchema,
|
||||
TalkSessionTurnResult: TalkSessionTurnResultSchema,
|
||||
TalkSessionSteerParams: TalkSessionSteerParamsSchema,
|
||||
TalkSessionSubmitToolResultParams: TalkSessionSubmitToolResultParamsSchema,
|
||||
TalkSessionCloseParams: TalkSessionCloseParamsSchema,
|
||||
TalkSessionOkResult: TalkSessionOkResultSchema,
|
||||
TalkSpeakParams: TalkSpeakParamsSchema,
|
||||
TalkSpeakResult: TalkSpeakResultSchema,
|
||||
ChannelsStatusParams: ChannelsStatusParamsSchema,
|
||||
ChannelsStatusResult: ChannelsStatusResultSchema,
|
||||
ChannelsStartParams: ChannelsStartParamsSchema,
|
||||
ChannelsStopParams: ChannelsStopParamsSchema,
|
||||
ChannelsLogoutParams: ChannelsLogoutParamsSchema,
|
||||
WebLoginStartParams: WebLoginStartParamsSchema,
|
||||
WebLoginWaitParams: WebLoginWaitParamsSchema,
|
||||
|
||||
// Agent files, artifacts, model catalogs, commands, tools, and skill workshop.
|
||||
AgentSummary: AgentSummarySchema,
|
||||
AgentsCreateParams: AgentsCreateParamsSchema,
|
||||
AgentsCreateResult: AgentsCreateResultSchema,
|
||||
AgentsUpdateParams: AgentsUpdateParamsSchema,
|
||||
AgentsUpdateResult: AgentsUpdateResultSchema,
|
||||
AgentsDeleteParams: AgentsDeleteParamsSchema,
|
||||
AgentsDeleteResult: AgentsDeleteResultSchema,
|
||||
AgentsFileEntry: AgentsFileEntrySchema,
|
||||
AgentsFilesListParams: AgentsFilesListParamsSchema,
|
||||
AgentsFilesListResult: AgentsFilesListResultSchema,
|
||||
AgentsFilesGetParams: AgentsFilesGetParamsSchema,
|
||||
AgentsFilesGetResult: AgentsFilesGetResultSchema,
|
||||
AgentsFilesSetParams: AgentsFilesSetParamsSchema,
|
||||
AgentsFilesSetResult: AgentsFilesSetResultSchema,
|
||||
ArtifactSummary: ArtifactSummarySchema,
|
||||
ArtifactsListParams: ArtifactsListParamsSchema,
|
||||
ArtifactsListResult: ArtifactsListResultSchema,
|
||||
ArtifactsGetParams: ArtifactsGetParamsSchema,
|
||||
ArtifactsGetResult: ArtifactsGetResultSchema,
|
||||
ArtifactsDownloadParams: ArtifactsDownloadParamsSchema,
|
||||
ArtifactsDownloadResult: ArtifactsDownloadResultSchema,
|
||||
AgentsListParams: AgentsListParamsSchema,
|
||||
AgentsListResult: AgentsListResultSchema,
|
||||
ModelChoice: ModelChoiceSchema,
|
||||
ModelsListParams: ModelsListParamsSchema,
|
||||
ModelsListResult: ModelsListResultSchema,
|
||||
CommandEntry: CommandEntrySchema,
|
||||
CommandsListParams: CommandsListParamsSchema,
|
||||
CommandsListResult: CommandsListResultSchema,
|
||||
SkillsStatusParams: SkillsStatusParamsSchema,
|
||||
ToolsCatalogParams: ToolsCatalogParamsSchema,
|
||||
ToolCatalogProfile: ToolCatalogProfileSchema,
|
||||
ToolCatalogEntry: ToolCatalogEntrySchema,
|
||||
ToolCatalogGroup: ToolCatalogGroupSchema,
|
||||
ToolsCatalogResult: ToolsCatalogResultSchema,
|
||||
ToolsEffectiveParams: ToolsEffectiveParamsSchema,
|
||||
ToolsEffectiveEntry: ToolsEffectiveEntrySchema,
|
||||
ToolsEffectiveGroup: ToolsEffectiveGroupSchema,
|
||||
ToolsEffectiveNotice: ToolsEffectiveNoticeSchema,
|
||||
ToolsEffectiveResult: ToolsEffectiveResultSchema,
|
||||
ToolsInvokeParams: ToolsInvokeParamsSchema,
|
||||
ToolsInvokeError: ToolsInvokeErrorSchema,
|
||||
ToolsInvokeResult: ToolsInvokeResultSchema,
|
||||
SkillsBinsParams: SkillsBinsParamsSchema,
|
||||
SkillsBinsResult: SkillsBinsResultSchema,
|
||||
SkillsSearchParams: SkillsSearchParamsSchema,
|
||||
SkillsSearchResult: SkillsSearchResultSchema,
|
||||
SkillsDetailParams: SkillsDetailParamsSchema,
|
||||
SkillsDetailResult: SkillsDetailResultSchema,
|
||||
SkillsProposalsListParams: SkillsProposalsListParamsSchema,
|
||||
SkillsProposalsListResult: SkillsProposalsListResultSchema,
|
||||
SkillsProposalInspectParams: SkillsProposalInspectParamsSchema,
|
||||
SkillsProposalInspectResult: SkillsProposalInspectResultSchema,
|
||||
SkillsProposalCreateParams: SkillsProposalCreateParamsSchema,
|
||||
SkillsProposalUpdateParams: SkillsProposalUpdateParamsSchema,
|
||||
SkillsProposalReviseParams: SkillsProposalReviseParamsSchema,
|
||||
SkillsProposalRequestRevisionParams: SkillsProposalRequestRevisionParamsSchema,
|
||||
SkillsProposalRequestRevisionResult: SkillsProposalRequestRevisionResultSchema,
|
||||
SkillsProposalActionParams: SkillsProposalActionParamsSchema,
|
||||
SkillsProposalApplyResult: SkillsProposalApplyResultSchema,
|
||||
SkillsProposalRecordResult: SkillsProposalRecordResultSchema,
|
||||
SkillsSecurityVerdictsParams: SkillsSecurityVerdictsParamsSchema,
|
||||
SkillsSecurityVerdictsResult: SkillsSecurityVerdictsResultSchema,
|
||||
SkillsSkillCardParams: SkillsSkillCardParamsSchema,
|
||||
SkillsSkillCardResult: SkillsSkillCardResultSchema,
|
||||
SkillsUploadBeginParams: SkillsUploadBeginParamsSchema,
|
||||
SkillsUploadChunkParams: SkillsUploadChunkParamsSchema,
|
||||
SkillsUploadCommitParams: SkillsUploadCommitParamsSchema,
|
||||
SkillsInstallParams: SkillsInstallParamsSchema,
|
||||
SkillsUpdateParams: SkillsUpdateParamsSchema,
|
||||
|
||||
// Scheduler, logs, approval, plugin control, device, chat, and lifecycle events.
|
||||
CronJob: CronJobSchema,
|
||||
CronListParams: CronListParamsSchema,
|
||||
CronStatusParams: CronStatusParamsSchema,
|
||||
CronGetParams: CronGetParamsSchema,
|
||||
CronAddParams: CronAddParamsSchema,
|
||||
CronUpdateParams: CronUpdateParamsSchema,
|
||||
CronRemoveParams: CronRemoveParamsSchema,
|
||||
CronRunParams: CronRunParamsSchema,
|
||||
CronRunsParams: CronRunsParamsSchema,
|
||||
CronRunLogEntry: CronRunLogEntrySchema,
|
||||
LogsTailParams: LogsTailParamsSchema,
|
||||
LogsTailResult: LogsTailResultSchema,
|
||||
TerminalOpenParams: TerminalOpenParamsSchema,
|
||||
TerminalOpenResult: TerminalOpenResultSchema,
|
||||
TerminalInputParams: TerminalInputParamsSchema,
|
||||
TerminalResizeParams: TerminalResizeParamsSchema,
|
||||
TerminalCloseParams: TerminalCloseParamsSchema,
|
||||
TerminalAttachParams: TerminalAttachParamsSchema,
|
||||
TerminalAttachResult: TerminalAttachResultSchema,
|
||||
TerminalSessionInfo: TerminalSessionInfoSchema,
|
||||
TerminalListResult: TerminalListResultSchema,
|
||||
TerminalTextParams: TerminalTextParamsSchema,
|
||||
TerminalTextResult: TerminalTextResultSchema,
|
||||
TerminalAckResult: TerminalAckResultSchema,
|
||||
TerminalDataEvent: TerminalDataEventSchema,
|
||||
TerminalExitEvent: TerminalExitEventSchema,
|
||||
TerminalEvent: TerminalEventSchema,
|
||||
ExecApprovalsGetParams: ExecApprovalsGetParamsSchema,
|
||||
ExecApprovalsSetParams: ExecApprovalsSetParamsSchema,
|
||||
ExecApprovalsNodeGetParams: ExecApprovalsNodeGetParamsSchema,
|
||||
ExecApprovalsNodeSetParams: ExecApprovalsNodeSetParamsSchema,
|
||||
ExecApprovalsSnapshot: ExecApprovalsSnapshotSchema,
|
||||
ExecApprovalGetParams: ExecApprovalGetParamsSchema,
|
||||
ExecApprovalRequestParams: ExecApprovalRequestParamsSchema,
|
||||
ExecApprovalResolveParams: ExecApprovalResolveParamsSchema,
|
||||
PluginApprovalRequestParams: PluginApprovalRequestParamsSchema,
|
||||
PluginApprovalResolveParams: PluginApprovalResolveParamsSchema,
|
||||
PluginControlUiDescriptor: PluginControlUiDescriptorSchema,
|
||||
PluginsSessionActionFailureResult: PluginsSessionActionFailureResultSchema,
|
||||
PluginsSessionActionParams: PluginsSessionActionParamsSchema,
|
||||
PluginsSessionActionResult: PluginsSessionActionResultSchema,
|
||||
PluginsSessionActionSuccessResult: PluginsSessionActionSuccessResultSchema,
|
||||
PluginsUiDescriptorsParams: PluginsUiDescriptorsParamsSchema,
|
||||
PluginsUiDescriptorsResult: PluginsUiDescriptorsResultSchema,
|
||||
DevicePairListParams: DevicePairListParamsSchema,
|
||||
DevicePairApproveParams: DevicePairApproveParamsSchema,
|
||||
DevicePairRejectParams: DevicePairRejectParamsSchema,
|
||||
DevicePairRemoveParams: DevicePairRemoveParamsSchema,
|
||||
DevicePairSetupCodeParams: DevicePairSetupCodeParamsSchema,
|
||||
DevicePairSetupCodeResult: DevicePairSetupCodeResultSchema,
|
||||
DeviceTokenRotateParams: DeviceTokenRotateParamsSchema,
|
||||
DeviceTokenRevokeParams: DeviceTokenRevokeParamsSchema,
|
||||
DevicePairRequestedEvent: DevicePairRequestedEventSchema,
|
||||
DevicePairResolvedEvent: DevicePairResolvedEventSchema,
|
||||
ChatHistoryParams: ChatHistoryParamsSchema,
|
||||
ChatMetadataParams: ChatMetadataParamsSchema,
|
||||
ChatMessageGetParams: ChatMessageGetParamsSchema,
|
||||
ChatMessageGetResult: ChatMessageGetResultSchema,
|
||||
ChatSendParams: ChatSendParamsSchema,
|
||||
ChatAbortParams: ChatAbortParamsSchema,
|
||||
ChatInjectParams: ChatInjectParamsSchema,
|
||||
ChatDeltaEvent: ChatDeltaEventSchema,
|
||||
ChatFinalEvent: ChatFinalEventSchema,
|
||||
ChatAbortedEvent: ChatAbortedEventSchema,
|
||||
ChatErrorEvent: ChatErrorEventSchema,
|
||||
ChatEvent: ChatEventSchema,
|
||||
UpdateStatusParams: UpdateStatusParamsSchema,
|
||||
UpdateRunParams: UpdateRunParamsSchema,
|
||||
TickEvent: TickEventSchema,
|
||||
ShutdownEvent: ShutdownEventSchema,
|
||||
} satisfies Record<string, TSchema>;
|
||||
|
||||
export {
|
||||
MIN_CLIENT_PROTOCOL_VERSION,
|
||||
MIN_PROBE_PROTOCOL_VERSION,
|
||||
PROTOCOL_VERSION,
|
||||
} from "../version.js";
|
||||
93
packages/gateway-protocol/src/schema/push.ts
Normal file
93
packages/gateway-protocol/src/schema/push.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Push-notification protocol schemas.
|
||||
*
|
||||
* APNS test schemas exercise native push routing; Web Push schemas describe the
|
||||
* browser subscription lifecycle exposed by the gateway.
|
||||
*/
|
||||
const ApnsEnvironmentSchema = Type.String({ enum: ["sandbox", "production"] });
|
||||
|
||||
/** Request payload for sending a test APNS notification to one node. */
|
||||
export const PushTestParamsSchema = Type.Object(
|
||||
{
|
||||
nodeId: NonEmptyString,
|
||||
title: Type.Optional(Type.String()),
|
||||
body: Type.Optional(Type.String()),
|
||||
environment: Type.Optional(ApnsEnvironmentSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Result payload from an APNS push test, including provider status and transport. */
|
||||
export const PushTestResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Boolean(),
|
||||
status: Type.Integer(),
|
||||
apnsId: Type.Optional(Type.String()),
|
||||
reason: Type.Optional(Type.String()),
|
||||
tokenSuffix: Type.String(),
|
||||
topic: Type.String(),
|
||||
environment: ApnsEnvironmentSchema,
|
||||
transport: Type.String({ enum: ["direct", "relay"] }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
// --- Web Push schemas ---
|
||||
|
||||
const WebPushKeysSchema = Type.Object(
|
||||
{
|
||||
p256dh: Type.String({ minLength: 1, maxLength: 512 }),
|
||||
auth: Type.String({ minLength: 1, maxLength: 512 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Empty request payload for fetching the Web Push VAPID public key. */
|
||||
export const WebPushVapidPublicKeyParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** Browser Web Push subscription payload registered with the gateway. */
|
||||
export const WebPushSubscribeParamsSchema = Type.Object(
|
||||
{
|
||||
endpoint: Type.String({ minLength: 1, maxLength: 2048, pattern: "^https://" }),
|
||||
keys: WebPushKeysSchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Browser Web Push endpoint removal payload. */
|
||||
export const WebPushUnsubscribeParamsSchema = Type.Object(
|
||||
{
|
||||
endpoint: Type.String({ minLength: 1, maxLength: 2048, pattern: "^https://" }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Request payload for sending a test Web Push notification to current subscriptions. */
|
||||
export const WebPushTestParamsSchema = Type.Object(
|
||||
{
|
||||
title: Type.Optional(Type.String()),
|
||||
body: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Empty request type for fetching the Web Push VAPID public key. */
|
||||
export type WebPushVapidPublicKeyParams = Record<string, never>;
|
||||
/** Browser PushSubscription subset persisted by the gateway. */
|
||||
export type WebPushSubscribeParams = {
|
||||
endpoint: string;
|
||||
keys: { p256dh: string; auth: string };
|
||||
};
|
||||
/** Browser PushSubscription endpoint removal request. */
|
||||
export type WebPushUnsubscribeParams = {
|
||||
endpoint: string;
|
||||
};
|
||||
/** Optional title/body overrides for a Web Push test notification. */
|
||||
export type WebPushTestParams = {
|
||||
title?: string;
|
||||
body?: string;
|
||||
};
|
||||
60
packages/gateway-protocol/src/schema/secrets.ts
Normal file
60
packages/gateway-protocol/src/schema/secrets.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type, type Static } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Secret-provider protocol schemas.
|
||||
*
|
||||
* These payloads request secret materialization from the gateway while keeping
|
||||
* caller scope, allowed paths, and provider overrides explicit.
|
||||
*/
|
||||
/** Empty request payload for reloading configured secret providers. */
|
||||
export const SecretsReloadParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||
|
||||
/** Request payload for resolving the secrets needed by one command invocation. */
|
||||
export const SecretsResolveParamsSchema = Type.Object(
|
||||
{
|
||||
commandName: NonEmptyString,
|
||||
targetIds: Type.Array(NonEmptyString),
|
||||
allowedPaths: Type.Optional(Type.Array(NonEmptyString)),
|
||||
forcedActivePaths: Type.Optional(Type.Array(NonEmptyString)),
|
||||
optionalActivePaths: Type.Optional(Type.Array(NonEmptyString)),
|
||||
providerOverrides: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
webSearch: Type.Optional(NonEmptyString),
|
||||
webFetch: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Static type for secret resolution requests. */
|
||||
export type SecretsResolveParams = Static<typeof SecretsResolveParamsSchema>;
|
||||
|
||||
/** One resolved secret assignment path plus its provider-owned value. */
|
||||
export const SecretsResolveAssignmentSchema = Type.Object(
|
||||
{
|
||||
path: Type.Optional(NonEmptyString),
|
||||
pathSegments: Type.Array(NonEmptyString),
|
||||
value: Type.Unknown(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Secret resolution response with assignments and safe diagnostics. */
|
||||
export const SecretsResolveResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Optional(Type.Boolean()),
|
||||
assignments: Type.Optional(Type.Array(SecretsResolveAssignmentSchema)),
|
||||
diagnostics: Type.Optional(Type.Array(NonEmptyString)),
|
||||
inactiveRefPaths: Type.Optional(Type.Array(NonEmptyString)),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Static type for secret resolution responses. */
|
||||
export type SecretsResolveResult = Static<typeof SecretsResolveResultSchema>;
|
||||
544
packages/gateway-protocol/src/schema/sessions.ts
Normal file
544
packages/gateway-protocol/src/schema/sessions.ts
Normal file
@@ -0,0 +1,544 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { PluginJsonValueSchema } from "./plugins.js";
|
||||
import { NonEmptyString, SessionLabelString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Session protocol schemas.
|
||||
*
|
||||
* These requests and results cover transcript discovery, lifecycle control,
|
||||
* compaction checkpoints, per-session plugin state, and usage reporting. The
|
||||
* schemas are shared by dashboard, CLI, ACP, and gateway RPC callers.
|
||||
*/
|
||||
|
||||
/** Reason a compaction checkpoint was created. */
|
||||
export const SessionCompactionCheckpointReasonSchema = Type.Union([
|
||||
Type.Literal("manual"),
|
||||
Type.Literal("auto-threshold"),
|
||||
Type.Literal("overflow-retry"),
|
||||
Type.Literal("timeout-retry"),
|
||||
]);
|
||||
|
||||
/** Start/end event emitted while a session compaction operation runs. */
|
||||
export const SessionOperationEventSchema = Type.Object(
|
||||
{
|
||||
operationId: NonEmptyString,
|
||||
operation: Type.Literal("compact"),
|
||||
phase: Type.Union([Type.Literal("start"), Type.Literal("end")]),
|
||||
sessionKey: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
completed: Type.Optional(Type.Boolean()),
|
||||
reason: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Reference to the transcript location before or after compaction. */
|
||||
export const SessionCompactionTranscriptReferenceSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
sessionFile: Type.Optional(NonEmptyString),
|
||||
leafId: Type.Optional(NonEmptyString),
|
||||
entryId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Stored compaction checkpoint metadata for branching or restoring a session. */
|
||||
export const SessionCompactionCheckpointSchema = Type.Object(
|
||||
{
|
||||
checkpointId: NonEmptyString,
|
||||
sessionKey: NonEmptyString,
|
||||
sessionId: NonEmptyString,
|
||||
createdAt: Type.Integer({ minimum: 0 }),
|
||||
reason: SessionCompactionCheckpointReasonSchema,
|
||||
tokensBefore: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
tokensAfter: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
summary: Type.Optional(Type.String()),
|
||||
firstKeptEntryId: Type.Optional(NonEmptyString),
|
||||
preCompaction: SessionCompactionTranscriptReferenceSchema,
|
||||
postCompaction: SessionCompactionTranscriptReferenceSchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Session file grouping used by the Control UI session workspace rail. */
|
||||
export const SessionFileKindSchema = Type.Union([Type.Literal("modified"), Type.Literal("read")]);
|
||||
|
||||
/** Session relevance marker for browser entries. */
|
||||
export const SessionFileRelevanceSchema = Type.Union([
|
||||
Type.Literal("modified"),
|
||||
Type.Literal("read"),
|
||||
Type.Literal("mixed"),
|
||||
]);
|
||||
|
||||
/** One file path referenced by a session transcript. */
|
||||
export const SessionFileEntrySchema = Type.Object(
|
||||
{
|
||||
path: NonEmptyString,
|
||||
name: NonEmptyString,
|
||||
kind: SessionFileKindSchema,
|
||||
missing: Type.Boolean(),
|
||||
size: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
updatedAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
content: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** One file or folder in the session-rooted browser. */
|
||||
export const SessionFileBrowserEntrySchema = Type.Object(
|
||||
{
|
||||
path: Type.String(),
|
||||
name: NonEmptyString,
|
||||
kind: Type.Union([Type.Literal("file"), Type.Literal("directory")]),
|
||||
sessionKind: Type.Optional(SessionFileRelevanceSchema),
|
||||
size: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
updatedAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Folder listing or search result rooted at the session workspace. */
|
||||
export const SessionFileBrowserResultSchema = Type.Object(
|
||||
{
|
||||
path: Type.String(),
|
||||
parentPath: Type.Optional(Type.String()),
|
||||
search: Type.Optional(Type.String()),
|
||||
entries: Type.Array(SessionFileBrowserEntrySchema),
|
||||
truncated: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Lists files touched by a session transcript. */
|
||||
export const SessionsFilesListParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
path: Type.Optional(Type.String()),
|
||||
search: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** File references visible in one session workspace. */
|
||||
export const SessionsFilesListResultSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
root: Type.Optional(NonEmptyString),
|
||||
files: Type.Array(SessionFileEntrySchema),
|
||||
browser: Type.Optional(SessionFileBrowserResultSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Reads one session-referenced file by path. */
|
||||
export const SessionsFilesGetParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
path: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Result for reading one session-referenced file. */
|
||||
export const SessionsFilesGetResultSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
root: Type.Optional(NonEmptyString),
|
||||
file: SessionFileEntrySchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Lists sessions with optional scope, activity, label, and preview filters. */
|
||||
export const SessionsListParamsSchema = Type.Object(
|
||||
{
|
||||
/**
|
||||
* Maximum rows to return. Omitted Gateway RPC calls use a bounded default
|
||||
* to keep large session stores from monopolizing the event loop.
|
||||
*/
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
activeMinutes: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
includeGlobal: Type.Optional(Type.Boolean()),
|
||||
includeUnknown: Type.Optional(Type.Boolean()),
|
||||
/**
|
||||
* Limit returned agent-scoped rows to agents currently present in config.
|
||||
* Broad disk discovery remains the default for recovery/ACP consumers.
|
||||
*/
|
||||
configuredAgentsOnly: Type.Optional(Type.Boolean()),
|
||||
/**
|
||||
* Read first 8KB of each session transcript to derive title from first user message.
|
||||
* Performs a file read per session - use `limit` to bound result set on large stores.
|
||||
*/
|
||||
includeDerivedTitles: Type.Optional(Type.Boolean()),
|
||||
/**
|
||||
* Read last 16KB of each session transcript to extract most recent message preview.
|
||||
* Performs a file read per session - use `limit` to bound result set on large stores.
|
||||
*/
|
||||
includeLastMessage: Type.Optional(Type.Boolean()),
|
||||
label: Type.Optional(SessionLabelString),
|
||||
spawnedBy: Type.Optional(NonEmptyString),
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
search: Type.Optional(Type.String()),
|
||||
/** True lists archived sessions; false or omitted lists active sessions. */
|
||||
archived: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Repairs or removes invalid session records from the selected agent scope. */
|
||||
export const SessionsCleanupParamsSchema = Type.Object(
|
||||
{
|
||||
agent: Type.Optional(NonEmptyString),
|
||||
allAgents: Type.Optional(Type.Boolean()),
|
||||
enforce: Type.Optional(Type.Boolean()),
|
||||
activeKey: Type.Optional(NonEmptyString),
|
||||
fixMissing: Type.Optional(Type.Boolean()),
|
||||
fixDmScope: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Reads short previews for selected session keys. */
|
||||
export const SessionsPreviewParamsSchema = Type.Object(
|
||||
{
|
||||
keys: Type.Array(NonEmptyString, { minItems: 1 }),
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
maxChars: Type.Optional(Type.Integer({ minimum: 20 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Describes one session and optional derived title/last-message previews. */
|
||||
export const SessionsDescribeParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
includeDerivedTitles: Type.Optional(Type.Boolean()),
|
||||
includeLastMessage: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Resolves a session by key, raw session id, label, or parent/agent scope. */
|
||||
export const SessionsResolveParamsSchema = Type.Object(
|
||||
{
|
||||
key: Type.Optional(NonEmptyString),
|
||||
sessionId: Type.Optional(NonEmptyString),
|
||||
label: Type.Optional(SessionLabelString),
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
spawnedBy: Type.Optional(NonEmptyString),
|
||||
includeGlobal: Type.Optional(Type.Boolean()),
|
||||
includeUnknown: Type.Optional(Type.Boolean()),
|
||||
/** Return a successful `{ ok: false }` response when the selector does not match a session. */
|
||||
allowMissing: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Creates or adopts a session with optional model, label, and parent linkage. */
|
||||
export const SessionsCreateParamsSchema = Type.Object(
|
||||
{
|
||||
key: Type.Optional(NonEmptyString),
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
label: Type.Optional(SessionLabelString),
|
||||
model: Type.Optional(NonEmptyString),
|
||||
parentSessionKey: Type.Optional(NonEmptyString),
|
||||
emitCommandHooks: Type.Optional(Type.Boolean()),
|
||||
task: Type.Optional(Type.String()),
|
||||
message: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Sends one message into an existing session. */
|
||||
export const SessionsSendParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
message: Type.String(),
|
||||
thinking: Type.Optional(Type.String()),
|
||||
attachments: Type.Optional(Type.Array(Type.Unknown())),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
idempotencyKey: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Subscribes a client to live message updates for one session. */
|
||||
export const SessionsMessagesSubscribeParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Removes a live message subscription for one session. */
|
||||
export const SessionsMessagesUnsubscribeParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Aborts the active or named run for a session. */
|
||||
export const SessionsAbortParamsSchema = Type.Object(
|
||||
{
|
||||
key: Type.Optional(NonEmptyString),
|
||||
runId: Type.Optional(NonEmptyString),
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Mutable per-session preferences and routing metadata. */
|
||||
export const SessionsPatchParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
label: Type.Optional(Type.Union([SessionLabelString, Type.Null()])),
|
||||
archived: Type.Optional(Type.Boolean()),
|
||||
pinned: Type.Optional(Type.Boolean()),
|
||||
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
fastMode: Type.Optional(Type.Union([Type.Boolean(), Type.Literal("auto"), Type.Null()])),
|
||||
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
traceLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
reasoningLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
responseUsage: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("off"),
|
||||
Type.Literal("tokens"),
|
||||
Type.Literal("full"),
|
||||
// Backward compat with older clients/stores.
|
||||
Type.Literal("on"),
|
||||
Type.Null(),
|
||||
]),
|
||||
),
|
||||
elevatedLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
execHost: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
execSecurity: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
execAsk: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
execNode: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
spawnedBy: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
spawnedWorkspaceDir: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
spawnedCwd: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
spawnDepth: Type.Optional(Type.Union([Type.Integer({ minimum: 0 }), Type.Null()])),
|
||||
subagentRole: Type.Optional(
|
||||
Type.Union([Type.Literal("orchestrator"), Type.Literal("leaf"), Type.Null()]),
|
||||
),
|
||||
subagentControlScope: Type.Optional(
|
||||
Type.Union([Type.Literal("children"), Type.Literal("none"), Type.Null()]),
|
||||
),
|
||||
inheritedToolAllow: Type.Optional(Type.Union([Type.Array(NonEmptyString), Type.Null()])),
|
||||
inheritedToolDeny: Type.Optional(Type.Union([Type.Array(NonEmptyString), Type.Null()])),
|
||||
sendPolicy: Type.Optional(
|
||||
Type.Union([Type.Literal("allow"), Type.Literal("deny"), Type.Null()]),
|
||||
),
|
||||
groupActivation: Type.Optional(
|
||||
Type.Union([Type.Literal("mention"), Type.Literal("always"), Type.Null()]),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Updates or clears one plugin namespace value on a session record. */
|
||||
export const SessionsPluginPatchParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
pluginId: NonEmptyString,
|
||||
namespace: NonEmptyString,
|
||||
value: Type.Optional(PluginJsonValueSchema),
|
||||
unset: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Result returned after patching session plugin state. */
|
||||
export const SessionsPluginPatchResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Literal(true),
|
||||
key: NonEmptyString,
|
||||
value: Type.Optional(PluginJsonValueSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Resets a session to a new or reset transcript state. */
|
||||
export const SessionsResetParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
reason: Type.Optional(Type.Union([Type.Literal("new"), Type.Literal("reset")])),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Deletes a session record and optionally its transcript. */
|
||||
export const SessionsDeleteParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
deleteTranscript: Type.Optional(Type.Boolean()),
|
||||
// Internal compare-and-delete guard for lifecycle-owned cleanup.
|
||||
expectedSessionId: Type.Optional(NonEmptyString),
|
||||
expectedLifecycleRevision: Type.Optional(NonEmptyString),
|
||||
expectedSessionUpdatedAt: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
// Internal control: when false, still unbind thread bindings but skip hook emission.
|
||||
emitLifecycleHooks: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Requests manual compaction for a session transcript. */
|
||||
export const SessionsCompactParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
maxLines: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Lists compaction checkpoints for one session. */
|
||||
export const SessionsCompactionListParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Reads one compaction checkpoint by id. */
|
||||
export const SessionsCompactionGetParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
checkpointId: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Creates a new branch from a compaction checkpoint. */
|
||||
export const SessionsCompactionBranchParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
checkpointId: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Restores an existing session to a compaction checkpoint. */
|
||||
export const SessionsCompactionRestoreParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
checkpointId: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** List response for session compaction checkpoints. */
|
||||
export const SessionsCompactionListResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Literal(true),
|
||||
key: NonEmptyString,
|
||||
checkpoints: Type.Array(SessionCompactionCheckpointSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Get response for a single compaction checkpoint. */
|
||||
export const SessionsCompactionGetResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Literal(true),
|
||||
key: NonEmptyString,
|
||||
checkpoint: SessionCompactionCheckpointSchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Branch response with the newly created session key and entry metadata. */
|
||||
export const SessionsCompactionBranchResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Literal(true),
|
||||
sourceKey: NonEmptyString,
|
||||
key: NonEmptyString,
|
||||
sessionId: NonEmptyString,
|
||||
checkpoint: SessionCompactionCheckpointSchema,
|
||||
entry: Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
updatedAt: Type.Integer({ minimum: 0 }),
|
||||
},
|
||||
{ additionalProperties: true },
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Restore response with updated session entry metadata. */
|
||||
export const SessionsCompactionRestoreResultSchema = Type.Object(
|
||||
{
|
||||
ok: Type.Literal(true),
|
||||
key: NonEmptyString,
|
||||
sessionId: NonEmptyString,
|
||||
checkpoint: SessionCompactionCheckpointSchema,
|
||||
entry: Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
updatedAt: Type.Integer({ minimum: 0 }),
|
||||
},
|
||||
{ additionalProperties: true },
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Usage report query across one session, one agent, or all agent sessions. */
|
||||
export const SessionsUsageParamsSchema = Type.Object(
|
||||
{
|
||||
/** Specific session key to analyze; if omitted returns sessions for the effective agent. */
|
||||
key: Type.Optional(NonEmptyString),
|
||||
/** Agent scope for list-style usage queries. */
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
/** Explicit all-agent scope for list-style usage queries. */
|
||||
agentScope: Type.Optional(Type.Literal("all")),
|
||||
/** Start date for range filter (YYYY-MM-DD). */
|
||||
startDate: Type.Optional(Type.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}$" })),
|
||||
/** End date for range filter (YYYY-MM-DD). */
|
||||
endDate: Type.Optional(Type.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}$" })),
|
||||
/** How start/end dates should be interpreted. Defaults to UTC when omitted. */
|
||||
mode: Type.Optional(
|
||||
Type.Union([Type.Literal("utc"), Type.Literal("gateway"), Type.Literal("specific")]),
|
||||
),
|
||||
/** Preset range for usage queries when explicit start/end dates are omitted. */
|
||||
range: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("7d"),
|
||||
Type.Literal("30d"),
|
||||
Type.Literal("90d"),
|
||||
Type.Literal("1y"),
|
||||
Type.Literal("all"),
|
||||
]),
|
||||
),
|
||||
/** Usage row grouping. `family` rolls up known rotated session ids for a logical key. */
|
||||
groupBy: Type.Optional(Type.Union([Type.Literal("instance"), Type.Literal("family")])),
|
||||
/** Backward-compatible alias for requesting family grouping. */
|
||||
includeHistorical: Type.Optional(Type.Boolean()),
|
||||
/** UTC offset to use when mode is `specific` (for example, UTC-4 or UTC+5:30). */
|
||||
utcOffset: Type.Optional(Type.String({ pattern: "^UTC[+-]\\d{1,2}(?::[0-5]\\d)?$" })),
|
||||
/** Maximum sessions to return (default 50). */
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
/** Include context weight breakdown (systemPromptReport). */
|
||||
includeContextWeight: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
84
packages/gateway-protocol/src/schema/snapshot.ts
Normal file
84
packages/gateway-protocol/src/schema/snapshot.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Gateway state snapshot schemas.
|
||||
*
|
||||
* Snapshots are sent during hello and later event streams; they summarize node
|
||||
* presence, health, session defaults, and version counters for clients.
|
||||
*/
|
||||
/** One gateway-visible presence record for a node/client/runtime. */
|
||||
export const PresenceEntrySchema = Type.Object(
|
||||
{
|
||||
host: Type.Optional(NonEmptyString),
|
||||
ip: Type.Optional(NonEmptyString),
|
||||
version: Type.Optional(NonEmptyString),
|
||||
platform: Type.Optional(NonEmptyString),
|
||||
deviceFamily: Type.Optional(NonEmptyString),
|
||||
modelIdentifier: Type.Optional(NonEmptyString),
|
||||
mode: Type.Optional(NonEmptyString),
|
||||
lastInputSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
reason: Type.Optional(NonEmptyString),
|
||||
tags: Type.Optional(Type.Array(NonEmptyString)),
|
||||
text: Type.Optional(Type.String()),
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
deviceId: Type.Optional(NonEmptyString),
|
||||
roles: Type.Optional(Type.Array(NonEmptyString)),
|
||||
scopes: Type.Optional(Type.Array(NonEmptyString)),
|
||||
instanceId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Health snapshot is intentionally opaque because providers contribute nested shapes. */
|
||||
export const HealthSnapshotSchema = Type.Any();
|
||||
|
||||
/** Default session routing keys included in initial gateway snapshots. */
|
||||
export const SessionDefaultsSchema = Type.Object(
|
||||
{
|
||||
defaultAgentId: NonEmptyString,
|
||||
mainKey: NonEmptyString,
|
||||
mainSessionKey: NonEmptyString,
|
||||
scope: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Monotonic version counters for snapshot subtrees. */
|
||||
export const StateVersionSchema = Type.Object(
|
||||
{
|
||||
presence: Type.Integer({ minimum: 0 }),
|
||||
health: Type.Integer({ minimum: 0 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Initial and incremental gateway state snapshot payload. */
|
||||
export const SnapshotSchema = Type.Object(
|
||||
{
|
||||
presence: Type.Array(PresenceEntrySchema),
|
||||
health: HealthSnapshotSchema,
|
||||
stateVersion: StateVersionSchema,
|
||||
uptimeMs: Type.Integer({ minimum: 0 }),
|
||||
configPath: Type.Optional(NonEmptyString),
|
||||
stateDir: Type.Optional(NonEmptyString),
|
||||
sessionDefaults: Type.Optional(SessionDefaultsSchema),
|
||||
authMode: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("none"),
|
||||
Type.Literal("token"),
|
||||
Type.Literal("password"),
|
||||
Type.Literal("trusted-proxy"),
|
||||
]),
|
||||
),
|
||||
updateAvailable: Type.Optional(
|
||||
Type.Object({
|
||||
currentVersion: NonEmptyString,
|
||||
latestVersion: NonEmptyString,
|
||||
channel: NonEmptyString,
|
||||
}),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
106
packages/gateway-protocol/src/schema/tasks.ts
Normal file
106
packages/gateway-protocol/src/schema/tasks.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/**
|
||||
* Task ledger protocol schemas.
|
||||
*
|
||||
* Tasks represent long-running SDK/agent operations exposed through the gateway;
|
||||
* these schemas keep list/get/cancel payloads bounded and status values closed.
|
||||
*/
|
||||
/** Closed task lifecycle statuses visible in the gateway task ledger. */
|
||||
export const TaskLedgerStatusSchema = Type.Union([
|
||||
Type.Literal("queued"),
|
||||
Type.Literal("running"),
|
||||
Type.Literal("completed"),
|
||||
Type.Literal("failed"),
|
||||
Type.Literal("cancelled"),
|
||||
Type.Literal("timed_out"),
|
||||
]);
|
||||
|
||||
const TimestampSchema = Type.Union([Type.String(), Type.Integer({ minimum: 0 })]);
|
||||
|
||||
/** Public task summary returned by task list/get/cancel responses. */
|
||||
export const TaskSummarySchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
kind: Type.Optional(Type.String()),
|
||||
runtime: Type.Optional(Type.String()),
|
||||
status: TaskLedgerStatusSchema,
|
||||
title: Type.Optional(Type.String()),
|
||||
agentId: Type.Optional(Type.String()),
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
childSessionKey: Type.Optional(Type.String()),
|
||||
ownerKey: Type.Optional(Type.String()),
|
||||
runId: Type.Optional(Type.String()),
|
||||
taskId: Type.Optional(Type.String()),
|
||||
flowId: Type.Optional(Type.String()),
|
||||
parentTaskId: Type.Optional(Type.String()),
|
||||
sourceId: Type.Optional(Type.String()),
|
||||
createdAt: Type.Optional(TimestampSchema),
|
||||
updatedAt: Type.Optional(TimestampSchema),
|
||||
startedAt: Type.Optional(TimestampSchema),
|
||||
endedAt: Type.Optional(TimestampSchema),
|
||||
progressSummary: Type.Optional(Type.String()),
|
||||
terminalSummary: Type.Optional(Type.String()),
|
||||
error: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Task list filters with bounded pagination. */
|
||||
export const TasksListParamsSchema = Type.Object(
|
||||
{
|
||||
status: Type.Optional(Type.Union([TaskLedgerStatusSchema, Type.Array(TaskLedgerStatusSchema)])),
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
sessionKey: Type.Optional(NonEmptyString),
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 500 })),
|
||||
cursor: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Task list page response. */
|
||||
export const TasksListResultSchema = Type.Object(
|
||||
{
|
||||
tasks: Type.Array(TaskSummarySchema),
|
||||
nextCursor: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Lookup request for one task id. */
|
||||
export const TasksGetParamsSchema = Type.Object(
|
||||
{
|
||||
taskId: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Lookup result for one task summary. */
|
||||
export const TasksGetResultSchema = Type.Object(
|
||||
{
|
||||
task: TaskSummarySchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Cancel request for one task id with optional operator reason. */
|
||||
export const TasksCancelParamsSchema = Type.Object(
|
||||
{
|
||||
taskId: NonEmptyString,
|
||||
reason: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Cancel result, including the task snapshot when it was found. */
|
||||
export const TasksCancelResultSchema = Type.Object(
|
||||
{
|
||||
found: Type.Boolean(),
|
||||
cancelled: Type.Boolean(),
|
||||
reason: Type.Optional(Type.String()),
|
||||
task: Type.Optional(TaskSummarySchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
184
packages/gateway-protocol/src/schema/terminal.ts
Normal file
184
packages/gateway-protocol/src/schema/terminal.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
// Gateway Protocol schema module for the operator terminal surface.
|
||||
// Terminal methods open a PTY-backed shell session bound to one authenticated
|
||||
// operator connection and stream its bytes back over the existing WebSocket.
|
||||
import type { Static } from "typebox";
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
// PTY grids are bounded so a hostile client cannot request an allocation that
|
||||
// overflows the terminal backend's row/column math.
|
||||
const TerminalDimension = Type.Integer({ minimum: 1, maximum: 2000 });
|
||||
|
||||
/** Opens a shell session; the server picks the shell, cwd, and confinement. */
|
||||
export const TerminalOpenParamsSchema = Type.Object(
|
||||
{
|
||||
// Optional agent selector; defaults to the gateway's default agent. The
|
||||
// session starts in that agent's workspace and inherits its isolation.
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
cols: TerminalDimension,
|
||||
rows: TerminalDimension,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalOpenParams = Static<typeof TerminalOpenParamsSchema>;
|
||||
|
||||
/** Result of a successful open; carries the facts the UI header renders. */
|
||||
export const TerminalOpenResultSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
agentId: NonEmptyString,
|
||||
shell: NonEmptyString,
|
||||
cwd: NonEmptyString,
|
||||
// True when the shell runs inside the agent's sandbox and cannot escape the
|
||||
// workspace; false for a host shell that can navigate the whole filesystem.
|
||||
confined: Type.Boolean(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalOpenResult = Static<typeof TerminalOpenResultSchema>;
|
||||
|
||||
/** Writes client keystrokes to the session stdin. */
|
||||
export const TerminalInputParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
// Raw terminal input (already-encoded escape sequences from the emulator).
|
||||
data: Type.String(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalInputParams = Static<typeof TerminalInputParamsSchema>;
|
||||
|
||||
/** Resizes the PTY grid after the client viewport changes. */
|
||||
export const TerminalResizeParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
cols: TerminalDimension,
|
||||
rows: TerminalDimension,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalResizeParams = Static<typeof TerminalResizeParamsSchema>;
|
||||
|
||||
/** Closes a session and kills its process tree. */
|
||||
export const TerminalCloseParamsSchema = Type.Object(
|
||||
{ sessionId: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalCloseParams = Static<typeof TerminalCloseParamsSchema>;
|
||||
|
||||
/**
|
||||
* Rebinds a live-or-detached session to the calling admin connection.
|
||||
* Attach is take-over (tmux-like): the previous owner, if still connected,
|
||||
* receives `terminal.exit` with reason "detached".
|
||||
*/
|
||||
export const TerminalAttachParamsSchema = Type.Object(
|
||||
{ sessionId: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalAttachParams = Static<typeof TerminalAttachParamsSchema>;
|
||||
|
||||
/** Result of a successful attach; mirrors open plus the replay buffer. */
|
||||
export const TerminalAttachResultSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
agentId: NonEmptyString,
|
||||
shell: NonEmptyString,
|
||||
cwd: NonEmptyString,
|
||||
confined: Type.Boolean(),
|
||||
// Recent raw output from the server's bounded ring buffer, replayed into
|
||||
// the client emulator before live terminal.data resumes. Not a true screen
|
||||
// snapshot: after truncation it can start mid-escape-sequence; emulators
|
||||
// recover on the next full repaint (prompt, clear, resize redraw).
|
||||
buffer: Type.String(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalAttachResult = Static<typeof TerminalAttachResultSchema>;
|
||||
|
||||
/** One attachable session, as reported by terminal.list. */
|
||||
export const TerminalSessionInfoSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
agentId: NonEmptyString,
|
||||
shell: NonEmptyString,
|
||||
cwd: NonEmptyString,
|
||||
confined: Type.Boolean(),
|
||||
/** False while the session is detached (no connection owns its stream). */
|
||||
attached: Type.Boolean(),
|
||||
createdAtMs: Type.Integer({ minimum: 0 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalSessionInfo = Static<typeof TerminalSessionInfoSchema>;
|
||||
|
||||
/**
|
||||
* Sessions a reconnecting admin client can attach. All admin connections see
|
||||
* the same list: the terminal surface is already operator.admin (full host
|
||||
* access), so cross-connection visibility adds no privilege.
|
||||
*/
|
||||
export const TerminalListResultSchema = Type.Object(
|
||||
{ sessions: Type.Array(TerminalSessionInfoSchema) },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalListResult = Static<typeof TerminalListResultSchema>;
|
||||
|
||||
/** Reads the current output buffer as plain text without attaching. */
|
||||
export const TerminalTextParamsSchema = Type.Object(
|
||||
{ sessionId: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalTextParams = Static<typeof TerminalTextParamsSchema>;
|
||||
|
||||
/** Plain-text buffer contents (ANSI stripped); an agent/LLM affordance. */
|
||||
export const TerminalTextResultSchema = Type.Object(
|
||||
{ text: Type.String() },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalTextResult = Static<typeof TerminalTextResultSchema>;
|
||||
|
||||
/** Shared ok/void result for input, resize, and close. */
|
||||
export const TerminalAckResultSchema = Type.Object(
|
||||
{ ok: Type.Boolean() },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalAckResult = Static<typeof TerminalAckResultSchema>;
|
||||
|
||||
/** Streamed output chunk; seq lets the client detect gaps and preserve order. */
|
||||
export const TerminalDataEventSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
seq: Type.Integer({ minimum: 0 }),
|
||||
data: Type.String(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalDataEvent = Static<typeof TerminalDataEventSchema>;
|
||||
|
||||
/** Terminal end-of-life notice; the session id is invalid after this event. */
|
||||
export const TerminalExitEventSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
exitCode: Type.Optional(Type.Union([Type.Integer(), Type.Null()])),
|
||||
signal: Type.Optional(Type.Union([Type.Integer(), Type.Null()])),
|
||||
// Stable reason code so clients can distinguish process exit from a
|
||||
// server-side teardown (disconnect, idle sweep, config disable).
|
||||
reason: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("process_exit"),
|
||||
Type.Literal("closed"),
|
||||
Type.Literal("disconnected"),
|
||||
// Another admin connection attached the session away; the session is
|
||||
// still alive server-side, but no longer streams to this connection.
|
||||
Type.Literal("detached"),
|
||||
Type.Literal("error"),
|
||||
]),
|
||||
),
|
||||
error: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export type TerminalExitEvent = Static<typeof TerminalExitEventSchema>;
|
||||
|
||||
/** Union of every event a terminal session can emit. */
|
||||
export const TerminalEventSchema = Type.Union([TerminalDataEventSchema, TerminalExitEventSchema]);
|
||||
export type TerminalEvent = Static<typeof TerminalEventSchema>;
|
||||
299
packages/gateway-protocol/src/schema/types.ts
Normal file
299
packages/gateway-protocol/src/schema/types.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Static TypeScript types derived from the canonical gateway protocol schemas.
|
||||
*
|
||||
* Keep aliases wired through `ProtocolSchemas` so validators, runtime schemas,
|
||||
* and exported compile-time types cannot drift apart.
|
||||
*/
|
||||
import type { Static } from "typebox";
|
||||
import { ProtocolSchemas } from "./protocol-schemas.js";
|
||||
|
||||
/** Stable schema names registered in the protocol schema registry. */
|
||||
type ProtocolSchemaName = keyof typeof ProtocolSchemas;
|
||||
/** Inferred TypeScript type for a named TypeBox protocol schema. */
|
||||
type SchemaType<TName extends ProtocolSchemaName> = Static<(typeof ProtocolSchemas)[TName]>;
|
||||
|
||||
/** Connection handshake, envelope, snapshot, and shared error wire types. */
|
||||
export type ConnectParams = SchemaType<"ConnectParams">;
|
||||
export type HelloOk = SchemaType<"HelloOk">;
|
||||
export type RequestFrame = SchemaType<"RequestFrame">;
|
||||
export type ResponseFrame = SchemaType<"ResponseFrame">;
|
||||
export type EventFrame = SchemaType<"EventFrame">;
|
||||
export type GatewayFrame = SchemaType<"GatewayFrame">;
|
||||
export type Snapshot = SchemaType<"Snapshot">;
|
||||
export type PresenceEntry = SchemaType<"PresenceEntry">;
|
||||
export type ErrorShape = SchemaType<"ErrorShape">;
|
||||
export type StateVersion = SchemaType<"StateVersion">;
|
||||
|
||||
/** Environment status RPC payloads used by CLI and Control UI surfaces. */
|
||||
export type EnvironmentStatus = SchemaType<"EnvironmentStatus">;
|
||||
export type EnvironmentSummary = SchemaType<"EnvironmentSummary">;
|
||||
export type EnvironmentsListParams = SchemaType<"EnvironmentsListParams">;
|
||||
export type EnvironmentsListResult = SchemaType<"EnvironmentsListResult">;
|
||||
export type EnvironmentsStatusParams = SchemaType<"EnvironmentsStatusParams">;
|
||||
export type EnvironmentsStatusResult = SchemaType<"EnvironmentsStatusResult">;
|
||||
|
||||
/** Agent activity, identity, send, poll, wait, and wake protocol payloads. */
|
||||
export type AgentEvent = SchemaType<"AgentEvent">;
|
||||
export type AgentIdentityParams = SchemaType<"AgentIdentityParams">;
|
||||
export type AgentIdentityResult = SchemaType<"AgentIdentityResult">;
|
||||
export type MessageActionParams = SchemaType<"MessageActionParams">;
|
||||
export type PollParams = SchemaType<"PollParams">;
|
||||
export type AgentWaitParams = SchemaType<"AgentWaitParams">;
|
||||
export type WakeParams = SchemaType<"WakeParams">;
|
||||
|
||||
/** Node pairing, presence, invoke, and pending-queue protocol payloads. */
|
||||
export type NodePairRequestParams = SchemaType<"NodePairRequestParams">;
|
||||
export type NodePairListParams = SchemaType<"NodePairListParams">;
|
||||
export type NodePairApproveParams = SchemaType<"NodePairApproveParams">;
|
||||
export type NodePairRejectParams = SchemaType<"NodePairRejectParams">;
|
||||
export type NodePairRemoveParams = SchemaType<"NodePairRemoveParams">;
|
||||
export type NodePairVerifyParams = SchemaType<"NodePairVerifyParams">;
|
||||
export type NodeRenameParams = SchemaType<"NodeRenameParams">;
|
||||
export type NodeListParams = SchemaType<"NodeListParams">;
|
||||
export type NodePendingAckParams = SchemaType<"NodePendingAckParams">;
|
||||
export type NodeDescribeParams = SchemaType<"NodeDescribeParams">;
|
||||
export type NodeInvokeParams = SchemaType<"NodeInvokeParams">;
|
||||
export type NodeInvokeResultParams = SchemaType<"NodeInvokeResultParams">;
|
||||
export type NodeEventParams = SchemaType<"NodeEventParams">;
|
||||
export type NodeEventResult = SchemaType<"NodeEventResult">;
|
||||
export type NodePresenceAlivePayload = SchemaType<"NodePresenceAlivePayload">;
|
||||
export type NodePresenceAliveReason = SchemaType<"NodePresenceAliveReason">;
|
||||
export type NodePendingDrainParams = SchemaType<"NodePendingDrainParams">;
|
||||
export type NodePendingDrainResult = SchemaType<"NodePendingDrainResult">;
|
||||
export type NodePendingEnqueueParams = SchemaType<"NodePendingEnqueueParams">;
|
||||
export type NodePendingEnqueueResult = SchemaType<"NodePendingEnqueueResult">;
|
||||
|
||||
/** Push notification test result contracts exposed through gateway RPC. */
|
||||
export type PushTestParams = SchemaType<"PushTestParams">;
|
||||
export type PushTestResult = SchemaType<"PushTestResult">;
|
||||
|
||||
/** Session lifecycle, message routing, compaction, patch, and usage payloads. */
|
||||
export type SessionsListParams = SchemaType<"SessionsListParams">;
|
||||
export type SessionsCleanupParams = SchemaType<"SessionsCleanupParams">;
|
||||
export type SessionsPreviewParams = SchemaType<"SessionsPreviewParams">;
|
||||
export type SessionsDescribeParams = SchemaType<"SessionsDescribeParams">;
|
||||
export type SessionsResolveParams = SchemaType<"SessionsResolveParams">;
|
||||
export type SessionCompactionCheckpoint = SchemaType<"SessionCompactionCheckpoint">;
|
||||
export type SessionOperationEvent = SchemaType<"SessionOperationEvent">;
|
||||
export type SessionsCompactionListParams = SchemaType<"SessionsCompactionListParams">;
|
||||
export type SessionsCompactionGetParams = SchemaType<"SessionsCompactionGetParams">;
|
||||
export type SessionsCompactionBranchParams = SchemaType<"SessionsCompactionBranchParams">;
|
||||
export type SessionsCompactionRestoreParams = SchemaType<"SessionsCompactionRestoreParams">;
|
||||
export type SessionsCompactionListResult = SchemaType<"SessionsCompactionListResult">;
|
||||
export type SessionsCompactionGetResult = SchemaType<"SessionsCompactionGetResult">;
|
||||
export type SessionsCompactionBranchResult = SchemaType<"SessionsCompactionBranchResult">;
|
||||
export type SessionsCompactionRestoreResult = SchemaType<"SessionsCompactionRestoreResult">;
|
||||
export type SessionsCreateParams = SchemaType<"SessionsCreateParams">;
|
||||
export type SessionsSendParams = SchemaType<"SessionsSendParams">;
|
||||
export type SessionsMessagesSubscribeParams = SchemaType<"SessionsMessagesSubscribeParams">;
|
||||
export type SessionsMessagesUnsubscribeParams = SchemaType<"SessionsMessagesUnsubscribeParams">;
|
||||
export type SessionsAbortParams = SchemaType<"SessionsAbortParams">;
|
||||
export type SessionsPatchParams = SchemaType<"SessionsPatchParams">;
|
||||
export type SessionsPluginPatchParams = SchemaType<"SessionsPluginPatchParams">;
|
||||
export type SessionsPluginPatchResult = SchemaType<"SessionsPluginPatchResult">;
|
||||
export type SessionsResetParams = SchemaType<"SessionsResetParams">;
|
||||
export type SessionsDeleteParams = SchemaType<"SessionsDeleteParams">;
|
||||
export type SessionsCompactParams = SchemaType<"SessionsCompactParams">;
|
||||
export type SessionsUsageParams = SchemaType<"SessionsUsageParams">;
|
||||
|
||||
/** Task ledger query and cancellation payloads. */
|
||||
export type TaskSummary = SchemaType<"TaskSummary">;
|
||||
export type TasksListParams = SchemaType<"TasksListParams">;
|
||||
export type TasksListResult = SchemaType<"TasksListResult">;
|
||||
export type TasksGetParams = SchemaType<"TasksGetParams">;
|
||||
export type TasksGetResult = SchemaType<"TasksGetResult">;
|
||||
export type TasksCancelParams = SchemaType<"TasksCancelParams">;
|
||||
export type TasksCancelResult = SchemaType<"TasksCancelResult">;
|
||||
|
||||
/** Config read/write/schema payloads plus update status and run controls. */
|
||||
export type ConfigGetParams = SchemaType<"ConfigGetParams">;
|
||||
export type ConfigSetParams = SchemaType<"ConfigSetParams">;
|
||||
export type ConfigApplyParams = SchemaType<"ConfigApplyParams">;
|
||||
export type ConfigPatchParams = SchemaType<"ConfigPatchParams">;
|
||||
export type ConfigSchemaParams = SchemaType<"ConfigSchemaParams">;
|
||||
export type ConfigSchemaLookupParams = SchemaType<"ConfigSchemaLookupParams">;
|
||||
export type ConfigSchemaResponse = SchemaType<"ConfigSchemaResponse">;
|
||||
export type ConfigSchemaLookupResult = SchemaType<"ConfigSchemaLookupResult">;
|
||||
export type UpdateStatusParams = SchemaType<"UpdateStatusParams">;
|
||||
|
||||
/** Crestodian chat payloads exchanged by clients and the gateway. */
|
||||
export type CrestodianChatParams = SchemaType<"CrestodianChatParams">;
|
||||
export type CrestodianChatResult = SchemaType<"CrestodianChatResult">;
|
||||
|
||||
/** Wizard setup flow payloads exchanged by CLI, UI, and gateway. */
|
||||
export type WizardStartParams = SchemaType<"WizardStartParams">;
|
||||
export type WizardNextParams = SchemaType<"WizardNextParams">;
|
||||
export type WizardCancelParams = SchemaType<"WizardCancelParams">;
|
||||
export type WizardStatusParams = SchemaType<"WizardStatusParams">;
|
||||
export type WizardStep = SchemaType<"WizardStep">;
|
||||
export type WizardNextResult = SchemaType<"WizardNextResult">;
|
||||
export type WizardStartResult = SchemaType<"WizardStartResult">;
|
||||
export type WizardStatusResult = SchemaType<"WizardStatusResult">;
|
||||
|
||||
/** Realtime Talk client/session/event payloads. */
|
||||
export type TalkEvent = SchemaType<"TalkEvent">;
|
||||
export type TalkModeParams = SchemaType<"TalkModeParams">;
|
||||
export type TalkCatalogParams = SchemaType<"TalkCatalogParams">;
|
||||
export type TalkCatalogResult = SchemaType<"TalkCatalogResult">;
|
||||
export type TalkConfigParams = SchemaType<"TalkConfigParams">;
|
||||
export type TalkConfigResult = SchemaType<"TalkConfigResult">;
|
||||
export type TalkClientCreateParams = SchemaType<"TalkClientCreateParams">;
|
||||
export type TalkClientCreateResult = SchemaType<"TalkClientCreateResult">;
|
||||
export type TalkClientSteerParams = SchemaType<"TalkClientSteerParams">;
|
||||
export type TalkAgentControlResult = SchemaType<"TalkAgentControlResult">;
|
||||
export type TalkClientToolCallParams = SchemaType<"TalkClientToolCallParams">;
|
||||
export type TalkClientToolCallResult = SchemaType<"TalkClientToolCallResult">;
|
||||
export type TalkSessionCreateParams = SchemaType<"TalkSessionCreateParams">;
|
||||
export type TalkSessionCreateResult = SchemaType<"TalkSessionCreateResult">;
|
||||
export type TalkSessionJoinParams = SchemaType<"TalkSessionJoinParams">;
|
||||
export type TalkSessionJoinResult = SchemaType<"TalkSessionJoinResult">;
|
||||
export type TalkSessionAppendAudioParams = SchemaType<"TalkSessionAppendAudioParams">;
|
||||
export type TalkSessionTurnParams = SchemaType<"TalkSessionTurnParams">;
|
||||
export type TalkSessionCancelTurnParams = SchemaType<"TalkSessionCancelTurnParams">;
|
||||
export type TalkSessionCancelOutputParams = SchemaType<"TalkSessionCancelOutputParams">;
|
||||
export type TalkSessionTurnResult = SchemaType<"TalkSessionTurnResult">;
|
||||
export type TalkSessionSteerParams = SchemaType<"TalkSessionSteerParams">;
|
||||
export type TalkSessionSubmitToolResultParams = SchemaType<"TalkSessionSubmitToolResultParams">;
|
||||
export type TalkSessionCloseParams = SchemaType<"TalkSessionCloseParams">;
|
||||
export type TalkSessionOkResult = SchemaType<"TalkSessionOkResult">;
|
||||
export type TalkSpeakParams = SchemaType<"TalkSpeakParams">;
|
||||
export type TalkSpeakResult = SchemaType<"TalkSpeakResult">;
|
||||
|
||||
/** Channel control and web-login payloads. */
|
||||
export type ChannelsStatusParams = SchemaType<"ChannelsStatusParams">;
|
||||
export type ChannelsStatusResult = SchemaType<"ChannelsStatusResult">;
|
||||
export type ChannelsStartParams = SchemaType<"ChannelsStartParams">;
|
||||
export type ChannelsStopParams = SchemaType<"ChannelsStopParams">;
|
||||
export type ChannelsLogoutParams = SchemaType<"ChannelsLogoutParams">;
|
||||
export type WebLoginStartParams = SchemaType<"WebLoginStartParams">;
|
||||
export type WebLoginWaitParams = SchemaType<"WebLoginWaitParams">;
|
||||
|
||||
/** Agent config-file CRUD and artifact download/list payloads. */
|
||||
export type AgentSummary = SchemaType<"AgentSummary">;
|
||||
export type AgentsFileEntry = SchemaType<"AgentsFileEntry">;
|
||||
export type AgentsCreateParams = SchemaType<"AgentsCreateParams">;
|
||||
export type AgentsCreateResult = SchemaType<"AgentsCreateResult">;
|
||||
export type AgentsUpdateParams = SchemaType<"AgentsUpdateParams">;
|
||||
export type AgentsUpdateResult = SchemaType<"AgentsUpdateResult">;
|
||||
export type AgentsDeleteParams = SchemaType<"AgentsDeleteParams">;
|
||||
export type AgentsDeleteResult = SchemaType<"AgentsDeleteResult">;
|
||||
export type AgentsFilesListParams = SchemaType<"AgentsFilesListParams">;
|
||||
export type AgentsFilesListResult = SchemaType<"AgentsFilesListResult">;
|
||||
export type AgentsFilesGetParams = SchemaType<"AgentsFilesGetParams">;
|
||||
export type AgentsFilesGetResult = SchemaType<"AgentsFilesGetResult">;
|
||||
export type AgentsFilesSetParams = SchemaType<"AgentsFilesSetParams">;
|
||||
export type AgentsFilesSetResult = SchemaType<"AgentsFilesSetResult">;
|
||||
export type SessionFileKind = SchemaType<"SessionFileKind">;
|
||||
export type SessionFileRelevance = SchemaType<"SessionFileRelevance">;
|
||||
export type SessionFileEntry = SchemaType<"SessionFileEntry">;
|
||||
export type SessionFileBrowserEntry = SchemaType<"SessionFileBrowserEntry">;
|
||||
export type SessionFileBrowserResult = SchemaType<"SessionFileBrowserResult">;
|
||||
export type SessionsFilesListParams = SchemaType<"SessionsFilesListParams">;
|
||||
export type SessionsFilesListResult = SchemaType<"SessionsFilesListResult">;
|
||||
export type SessionsFilesGetParams = SchemaType<"SessionsFilesGetParams">;
|
||||
export type SessionsFilesGetResult = SchemaType<"SessionsFilesGetResult">;
|
||||
export type ArtifactSummary = SchemaType<"ArtifactSummary">;
|
||||
export type ArtifactsListParams = SchemaType<"ArtifactsListParams">;
|
||||
export type ArtifactsListResult = SchemaType<"ArtifactsListResult">;
|
||||
export type ArtifactsGetParams = SchemaType<"ArtifactsGetParams">;
|
||||
export type ArtifactsGetResult = SchemaType<"ArtifactsGetResult">;
|
||||
export type ArtifactsDownloadParams = SchemaType<"ArtifactsDownloadParams">;
|
||||
export type ArtifactsDownloadResult = SchemaType<"ArtifactsDownloadResult">;
|
||||
|
||||
/** Model, command, plugin UI action, tool catalog, and skill workshop payloads. */
|
||||
export type AgentsListParams = SchemaType<"AgentsListParams">;
|
||||
export type AgentsListResult = SchemaType<"AgentsListResult">;
|
||||
export type ModelChoice = SchemaType<"ModelChoice">;
|
||||
export type ModelsListParams = SchemaType<"ModelsListParams">;
|
||||
export type ModelsListResult = SchemaType<"ModelsListResult">;
|
||||
export type ChatMetadataParams = SchemaType<"ChatMetadataParams">;
|
||||
export type CommandEntry = SchemaType<"CommandEntry">;
|
||||
export type CommandsListParams = SchemaType<"CommandsListParams">;
|
||||
export type CommandsListResult = SchemaType<"CommandsListResult">;
|
||||
export type PluginControlUiDescriptor = SchemaType<"PluginControlUiDescriptor">;
|
||||
export type PluginsUiDescriptorsParams = SchemaType<"PluginsUiDescriptorsParams">;
|
||||
export type PluginsUiDescriptorsResult = SchemaType<"PluginsUiDescriptorsResult">;
|
||||
export type PluginsSessionActionParams = SchemaType<"PluginsSessionActionParams">;
|
||||
export type PluginsSessionActionResult = SchemaType<"PluginsSessionActionResult">;
|
||||
export type SkillsStatusParams = SchemaType<"SkillsStatusParams">;
|
||||
export type ToolsCatalogParams = SchemaType<"ToolsCatalogParams">;
|
||||
export type ToolCatalogProfile = SchemaType<"ToolCatalogProfile">;
|
||||
export type ToolCatalogEntry = SchemaType<"ToolCatalogEntry">;
|
||||
export type ToolCatalogGroup = SchemaType<"ToolCatalogGroup">;
|
||||
export type ToolsCatalogResult = SchemaType<"ToolsCatalogResult">;
|
||||
export type ToolsEffectiveParams = SchemaType<"ToolsEffectiveParams">;
|
||||
export type ToolsEffectiveEntry = SchemaType<"ToolsEffectiveEntry">;
|
||||
export type ToolsEffectiveGroup = SchemaType<"ToolsEffectiveGroup">;
|
||||
export type ToolsEffectiveNotice = SchemaType<"ToolsEffectiveNotice">;
|
||||
export type ToolsEffectiveResult = SchemaType<"ToolsEffectiveResult">;
|
||||
export type ToolsInvokeParams = SchemaType<"ToolsInvokeParams">;
|
||||
export type ToolsInvokeResult = SchemaType<"ToolsInvokeResult">;
|
||||
export type SkillsBinsParams = SchemaType<"SkillsBinsParams">;
|
||||
export type SkillsBinsResult = SchemaType<"SkillsBinsResult">;
|
||||
export type SkillsSearchParams = SchemaType<"SkillsSearchParams">;
|
||||
export type SkillsSearchResult = SchemaType<"SkillsSearchResult">;
|
||||
export type SkillsDetailParams = SchemaType<"SkillsDetailParams">;
|
||||
export type SkillsDetailResult = SchemaType<"SkillsDetailResult">;
|
||||
export type SkillsProposalsListParams = SchemaType<"SkillsProposalsListParams">;
|
||||
export type SkillsProposalsListResult = SchemaType<"SkillsProposalsListResult">;
|
||||
export type SkillsProposalInspectParams = SchemaType<"SkillsProposalInspectParams">;
|
||||
export type SkillsProposalInspectResult = SchemaType<"SkillsProposalInspectResult">;
|
||||
export type SkillsProposalCreateParams = SchemaType<"SkillsProposalCreateParams">;
|
||||
export type SkillsProposalUpdateParams = SchemaType<"SkillsProposalUpdateParams">;
|
||||
export type SkillsProposalReviseParams = SchemaType<"SkillsProposalReviseParams">;
|
||||
export type SkillsProposalRequestRevisionParams = SchemaType<"SkillsProposalRequestRevisionParams">;
|
||||
export type SkillsProposalRequestRevisionResult = SchemaType<"SkillsProposalRequestRevisionResult">;
|
||||
export type SkillsProposalActionParams = SchemaType<"SkillsProposalActionParams">;
|
||||
export type SkillsProposalApplyResult = SchemaType<"SkillsProposalApplyResult">;
|
||||
export type SkillsProposalRecordResult = SchemaType<"SkillsProposalRecordResult">;
|
||||
export type SkillsSecurityVerdictsParams = SchemaType<"SkillsSecurityVerdictsParams">;
|
||||
export type SkillsSecurityVerdictsResult = SchemaType<"SkillsSecurityVerdictsResult">;
|
||||
export type SkillsSkillCardParams = SchemaType<"SkillsSkillCardParams">;
|
||||
export type SkillsSkillCardResult = SchemaType<"SkillsSkillCardResult">;
|
||||
export type SkillsUploadBeginParams = SchemaType<"SkillsUploadBeginParams">;
|
||||
export type SkillsUploadChunkParams = SchemaType<"SkillsUploadChunkParams">;
|
||||
export type SkillsUploadCommitParams = SchemaType<"SkillsUploadCommitParams">;
|
||||
export type SkillsInstallParams = SchemaType<"SkillsInstallParams">;
|
||||
export type SkillsUpdateParams = SchemaType<"SkillsUpdateParams">;
|
||||
|
||||
/** Cron scheduler and run-log payloads. */
|
||||
export type CronJob = SchemaType<"CronJob">;
|
||||
export type CronListParams = SchemaType<"CronListParams">;
|
||||
export type CronStatusParams = SchemaType<"CronStatusParams">;
|
||||
export type CronGetParams = SchemaType<"CronGetParams">;
|
||||
export type CronAddParams = SchemaType<"CronAddParams">;
|
||||
export type CronUpdateParams = SchemaType<"CronUpdateParams">;
|
||||
export type CronRemoveParams = SchemaType<"CronRemoveParams">;
|
||||
export type CronRunParams = SchemaType<"CronRunParams">;
|
||||
export type CronRunsParams = SchemaType<"CronRunsParams">;
|
||||
export type CronRunLogEntry = SchemaType<"CronRunLogEntry">;
|
||||
|
||||
/** Logs and approval payloads for chat, exec commands, plugins, and devices. */
|
||||
export type LogsTailParams = SchemaType<"LogsTailParams">;
|
||||
export type LogsTailResult = SchemaType<"LogsTailResult">;
|
||||
export type ExecApprovalsGetParams = SchemaType<"ExecApprovalsGetParams">;
|
||||
export type ExecApprovalsSetParams = SchemaType<"ExecApprovalsSetParams">;
|
||||
export type ExecApprovalsNodeGetParams = SchemaType<"ExecApprovalsNodeGetParams">;
|
||||
export type ExecApprovalsNodeSetParams = SchemaType<"ExecApprovalsNodeSetParams">;
|
||||
export type ExecApprovalsSnapshot = SchemaType<"ExecApprovalsSnapshot">;
|
||||
export type ExecApprovalGetParams = SchemaType<"ExecApprovalGetParams">;
|
||||
export type ExecApprovalRequestParams = SchemaType<"ExecApprovalRequestParams">;
|
||||
export type ExecApprovalResolveParams = SchemaType<"ExecApprovalResolveParams">;
|
||||
export type PluginApprovalRequestParams = SchemaType<"PluginApprovalRequestParams">;
|
||||
export type PluginApprovalResolveParams = SchemaType<"PluginApprovalResolveParams">;
|
||||
export type DevicePairListParams = SchemaType<"DevicePairListParams">;
|
||||
export type DevicePairApproveParams = SchemaType<"DevicePairApproveParams">;
|
||||
export type DevicePairRejectParams = SchemaType<"DevicePairRejectParams">;
|
||||
export type DevicePairRemoveParams = SchemaType<"DevicePairRemoveParams">;
|
||||
export type DevicePairSetupCodeParams = SchemaType<"DevicePairSetupCodeParams">;
|
||||
export type DevicePairSetupCodeResult = SchemaType<"DevicePairSetupCodeResult">;
|
||||
export type DeviceTokenRotateParams = SchemaType<"DeviceTokenRotateParams">;
|
||||
export type DeviceTokenRevokeParams = SchemaType<"DeviceTokenRevokeParams">;
|
||||
export type ChatAbortParams = SchemaType<"ChatAbortParams">;
|
||||
export type ChatInjectParams = SchemaType<"ChatInjectParams">;
|
||||
export type ChatEvent = SchemaType<"ChatEvent">;
|
||||
|
||||
/** Gateway update and process lifecycle event payloads. */
|
||||
export type UpdateRunParams = SchemaType<"UpdateRunParams">;
|
||||
export type TickEvent = SchemaType<"TickEvent">;
|
||||
export type ShutdownEvent = SchemaType<"ShutdownEvent">;
|
||||
118
packages/gateway-protocol/src/schema/wizard.ts
Normal file
118
packages/gateway-protocol/src/schema/wizard.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// Gateway Protocol schema module defines protocol validation shapes.
|
||||
import { Type } from "typebox";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
/** Runtime state reported for gateway-driven setup wizard sessions. */
|
||||
const WizardRunStatusSchema = Type.Union([
|
||||
Type.Literal("running"),
|
||||
Type.Literal("done"),
|
||||
Type.Literal("cancelled"),
|
||||
Type.Literal("error"),
|
||||
]);
|
||||
|
||||
/** Starts a setup wizard, optionally scoped to a local or remote workspace. */
|
||||
export const WizardStartParamsSchema = Type.Object(
|
||||
{
|
||||
mode: Type.Optional(Type.Union([Type.Literal("local"), Type.Literal("remote")])),
|
||||
workspace: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Client answer payload for the current wizard step. */
|
||||
export const WizardAnswerSchema = Type.Object(
|
||||
{
|
||||
stepId: NonEmptyString,
|
||||
value: Type.Optional(Type.Unknown()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Advances a wizard session, with an answer when the previous step requested input. */
|
||||
export const WizardNextParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
answer: Type.Optional(WizardAnswerSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Shared session-id-only params for cancel and status requests. */
|
||||
const WizardSessionIdParamsSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Cancels an active wizard session. */
|
||||
export const WizardCancelParamsSchema = WizardSessionIdParamsSchema;
|
||||
|
||||
/** Reads status for an active or recently completed wizard session. */
|
||||
export const WizardStatusParamsSchema = WizardSessionIdParamsSchema;
|
||||
|
||||
/** Selectable value shown in a choice-based wizard step. */
|
||||
export const WizardStepOptionSchema = Type.Object(
|
||||
{
|
||||
value: Type.Unknown(),
|
||||
label: NonEmptyString,
|
||||
hint: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** UI contract for one wizard step rendered by gateway clients. */
|
||||
export const WizardStepSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
type: Type.Union([
|
||||
Type.Literal("note"),
|
||||
Type.Literal("select"),
|
||||
Type.Literal("text"),
|
||||
Type.Literal("confirm"),
|
||||
Type.Literal("multiselect"),
|
||||
Type.Literal("progress"),
|
||||
Type.Literal("action"),
|
||||
]),
|
||||
title: Type.Optional(Type.String()),
|
||||
message: Type.Optional(Type.String()),
|
||||
format: Type.Optional(Type.Union([Type.Literal("plain")])),
|
||||
options: Type.Optional(Type.Array(WizardStepOptionSchema)),
|
||||
initialValue: Type.Optional(Type.Unknown()),
|
||||
placeholder: Type.Optional(Type.String()),
|
||||
sensitive: Type.Optional(Type.Boolean()),
|
||||
executor: Type.Optional(Type.Union([Type.Literal("gateway"), Type.Literal("client")])),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Common response fields for start and next calls. */
|
||||
const WizardResultFields = {
|
||||
done: Type.Boolean(),
|
||||
step: Type.Optional(WizardStepSchema),
|
||||
status: Type.Optional(WizardRunStatusSchema),
|
||||
error: Type.Optional(Type.String()),
|
||||
};
|
||||
|
||||
/** Result after advancing a wizard session. */
|
||||
export const WizardNextResultSchema = Type.Object(WizardResultFields, {
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
/** Result returned when a wizard session is created. */
|
||||
export const WizardStartResultSchema = Type.Object(
|
||||
{
|
||||
sessionId: NonEmptyString,
|
||||
...WizardResultFields,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Minimal status poll result used when the client does not need the next step. */
|
||||
export const WizardStatusResultSchema = Type.Object(
|
||||
{
|
||||
status: WizardRunStatusSchema,
|
||||
error: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
12
packages/gateway-protocol/src/secret-ref-contract.ts
Normal file
12
packages/gateway-protocol/src/secret-ref-contract.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/** Canonical id for file secret providers that expose exactly one value. */
|
||||
export const SINGLE_VALUE_FILE_REF_ID = "value";
|
||||
|
||||
/** Shared alias grammar for env/file/exec secret provider names. */
|
||||
export const SECRET_PROVIDER_ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
|
||||
/** JSON-schema fragment that rejects absolute file secret ref ids. */
|
||||
export const FILE_SECRET_REF_ID_ABSOLUTE_JSON_SCHEMA_PATTERN = "^/";
|
||||
/** JSON-schema fragment that rejects invalid JSON-pointer escape sequences. */
|
||||
export const FILE_SECRET_REF_ID_INVALID_ESCAPE_JSON_SCHEMA_PATTERN = "~(?:[^01]|$)";
|
||||
/** JSON-schema pattern for exec secret ref ids, excluding dot-path traversal. */
|
||||
export const EXEC_SECRET_REF_ID_JSON_SCHEMA_PATTERN =
|
||||
"^(?!.*(?:^|/)\\.{1,2}(?:/|$))[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}$";
|
||||
67
packages/gateway-protocol/src/startup-unavailable.ts
Normal file
67
packages/gateway-protocol/src/startup-unavailable.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/** Structured error reason used while gateway startup sidecars are still initializing. */
|
||||
export const GATEWAY_STARTUP_UNAVAILABLE_REASON = "startup-sidecars";
|
||||
/** Internal close cause that distinguishes startup retry closes from generic disconnects. */
|
||||
export const GATEWAY_STARTUP_PENDING_CLOSE_CAUSE = "startup-sidecars-pending";
|
||||
/** WebSocket close code for temporary gateway unavailability. */
|
||||
export const GATEWAY_STARTUP_CLOSE_CODE = 1013;
|
||||
/** Human-readable WebSocket close reason for temporary gateway startup unavailability. */
|
||||
export const GATEWAY_STARTUP_CLOSE_REASON = "gateway starting";
|
||||
/** Default retry-after hint sent with startup-unavailable handshake errors. */
|
||||
export const GATEWAY_STARTUP_RETRY_AFTER_MS = 500;
|
||||
const GATEWAY_STARTUP_RETRY_MIN_MS = 100;
|
||||
const GATEWAY_STARTUP_RETRY_MAX_MS = 2_000;
|
||||
|
||||
/** Details payload attached to retryable startup-unavailable gateway errors. */
|
||||
export type GatewayStartupUnavailableDetails = {
|
||||
reason: typeof GATEWAY_STARTUP_UNAVAILABLE_REASON;
|
||||
};
|
||||
|
||||
/** Builds the canonical startup-unavailable details payload. */
|
||||
export function gatewayStartupUnavailableDetails(): GatewayStartupUnavailableDetails {
|
||||
return { reason: GATEWAY_STARTUP_UNAVAILABLE_REASON };
|
||||
}
|
||||
|
||||
function isGatewayStartupUnavailableDetails(
|
||||
details: unknown,
|
||||
): details is GatewayStartupUnavailableDetails {
|
||||
return (
|
||||
typeof details === "object" &&
|
||||
details !== null &&
|
||||
(details as { reason?: unknown }).reason === GATEWAY_STARTUP_UNAVAILABLE_REASON
|
||||
);
|
||||
}
|
||||
|
||||
/** Detects the structured retryable error emitted while startup sidecars are pending. */
|
||||
export function isRetryableGatewayStartupUnavailableError(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") {
|
||||
return false;
|
||||
}
|
||||
const shaped = error as {
|
||||
code?: unknown;
|
||||
gatewayCode?: unknown;
|
||||
retryable?: unknown;
|
||||
details?: unknown;
|
||||
};
|
||||
const code = shaped.gatewayCode ?? shaped.code;
|
||||
return (
|
||||
code === "UNAVAILABLE" &&
|
||||
shaped.retryable === true &&
|
||||
isGatewayStartupUnavailableDetails(shaped.details)
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolves a bounded retry-after delay from a startup-unavailable error. */
|
||||
export function resolveGatewayStartupRetryAfterMs(error: unknown): number | null {
|
||||
if (!isRetryableGatewayStartupUnavailableError(error)) {
|
||||
return null;
|
||||
}
|
||||
const retryAfterMs = (error as { retryAfterMs?: unknown }).retryAfterMs;
|
||||
const raw =
|
||||
typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs)
|
||||
? retryAfterMs
|
||||
: GATEWAY_STARTUP_RETRY_AFTER_MS;
|
||||
return Math.min(
|
||||
Math.max(Math.floor(raw), GATEWAY_STARTUP_RETRY_MIN_MS),
|
||||
GATEWAY_STARTUP_RETRY_MAX_MS,
|
||||
);
|
||||
}
|
||||
87
packages/gateway-protocol/src/talk-config.contract.test.ts
Normal file
87
packages/gateway-protocol/src/talk-config.contract.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// Gateway Protocol tests cover talk config.contract behavior.
|
||||
import fs from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildTalkConfigResponse } from "../../../src/config/talk.js";
|
||||
import { validateTalkConfigResult } from "./index.js";
|
||||
|
||||
/**
|
||||
* Talk config contract tests shared between config normalization and gateway
|
||||
* protocol validation. Fixtures capture provider selection and timeout behavior
|
||||
* so config changes cannot silently diverge from the public RPC response shape.
|
||||
*/
|
||||
|
||||
/** Expected resolved provider/config selection for one fixture case. */
|
||||
type ExpectedSelection = {
|
||||
provider: string;
|
||||
normalizedPayload: boolean;
|
||||
voiceId?: string;
|
||||
apiKey?: string;
|
||||
};
|
||||
|
||||
/** Fixture row that validates normalized Talk provider selection. */
|
||||
type SelectionContractCase = {
|
||||
id: string;
|
||||
defaultProvider: string;
|
||||
payloadValid: boolean;
|
||||
expectedSelection: ExpectedSelection | null;
|
||||
talk: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/** Fixture row that validates Talk silence-timeout normalization. */
|
||||
type TimeoutContractCase = {
|
||||
id: string;
|
||||
fallback: number;
|
||||
expectedTimeoutMs: number;
|
||||
talk: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/** JSON fixture file shape used by this contract test. */
|
||||
type TalkConfigContractFixture = {
|
||||
selectionCases: SelectionContractCase[];
|
||||
timeoutCases: TimeoutContractCase[];
|
||||
};
|
||||
|
||||
/** External fixture keeps the matrix readable and reusable across config edits. */
|
||||
const fixturePath = new URL("../../../test/fixtures/talk-config-contract.json", import.meta.url);
|
||||
const fixtures = JSON.parse(fs.readFileSync(fixturePath, "utf-8")) as TalkConfigContractFixture;
|
||||
|
||||
describe("talk.config contract fixtures", () => {
|
||||
for (const fixture of fixtures.selectionCases) {
|
||||
it(fixture.id, () => {
|
||||
const payload = { config: { talk: buildTalkConfigResponse(fixture.talk) } };
|
||||
if (fixture.payloadValid) {
|
||||
expect(validateTalkConfigResult(payload)).toBe(true);
|
||||
} else {
|
||||
expect(validateTalkConfigResult(payload)).toBe(false);
|
||||
}
|
||||
|
||||
if (!fixture.expectedSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const talk = payload.config.talk as
|
||||
| {
|
||||
resolved?: {
|
||||
provider?: string;
|
||||
config?: {
|
||||
voiceId?: string;
|
||||
apiKey?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
expect(talk?.resolved?.provider ?? fixture.defaultProvider).toBe(
|
||||
fixture.expectedSelection.provider,
|
||||
);
|
||||
expect(talk?.resolved?.config?.voiceId).toBe(fixture.expectedSelection.voiceId);
|
||||
expect(talk?.resolved?.config?.apiKey).toBe(fixture.expectedSelection.apiKey);
|
||||
});
|
||||
}
|
||||
|
||||
for (const fixture of fixtures.timeoutCases) {
|
||||
it(`timeout:${fixture.id}`, () => {
|
||||
const payload = buildTalkConfigResponse(fixture.talk);
|
||||
expect(payload?.silenceTimeoutMs ?? fixture.fallback).toBe(fixture.expectedTimeoutMs);
|
||||
});
|
||||
}
|
||||
});
|
||||
6
packages/gateway-protocol/src/version.ts
Normal file
6
packages/gateway-protocol/src/version.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/** Current gateway protocol version emitted by modern clients and servers. */
|
||||
export const PROTOCOL_VERSION = 4 as const;
|
||||
/** Lowest client protocol version accepted by the gateway. */
|
||||
export const MIN_CLIENT_PROTOCOL_VERSION = 4 as const;
|
||||
/** Lowest lightweight probe protocol version accepted by the gateway. */
|
||||
export const MIN_PROBE_PROTOCOL_VERSION = 4 as const;
|
||||
Reference in New Issue
Block a user