feat(contracts): add shared agent/controller protocol (#759)

* feat(contracts): add shared agent/controller protocol

* chore: pr feedback
This commit is contained in:
Nico 2026-04-08 08:17:15 +02:00 committed by GitHub
parent 049becb900
commit 3169627b79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 245 additions and 0 deletions

View file

@ -34,6 +34,7 @@
"@tanstack/react-router": "^1.168.1",
"@tanstack/react-router-ssr-query": "^1.166.10",
"@tanstack/react-start": "^1.167.1",
"@zerobyte/contracts": "workspace:*",
"@zerobyte/core": "workspace:*",
"better-auth": "^1.5.5",
"class-variance-authority": "^0.7.1",
@ -114,6 +115,18 @@
"wait-for-expect": "^4.0.0",
},
},
"packages/contracts": {
"name": "@zerobyte/contracts",
"dependencies": {
"@zerobyte/core": "workspace:*",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^6",
},
},
"packages/core": {
"name": "@zerobyte/core",
"devDependencies": {
@ -1044,6 +1057,8 @@
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
"@zerobyte/contracts": ["@zerobyte/contracts@workspace:packages/contracts"],
"@zerobyte/core": ["@zerobyte/core@workspace:packages/core"],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],

View file

@ -59,6 +59,7 @@
"@tanstack/react-router": "^1.168.1",
"@tanstack/react-router-ssr-query": "^1.166.10",
"@tanstack/react-start": "^1.167.1",
"@zerobyte/contracts": "workspace:*",
"@zerobyte/core": "workspace:*",
"better-auth": "^1.5.5",
"class-variance-authority": "^0.7.1",

View file

@ -0,0 +1,24 @@
{
"name": "@zerobyte/contracts",
"private": true,
"type": "module",
"exports": {
"./agent-protocol": {
"types": "./src/agent-protocol.ts",
"import": "./src/agent-protocol.ts",
"default": "./src/agent-protocol.ts"
}
},
"scripts": {
"tsc": "tsc --noEmit"
},
"dependencies": {
"@zerobyte/core": "workspace:*"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^6"
}
}

View file

@ -0,0 +1,175 @@
import { z } from "zod";
import { safeJsonParse } from "@zerobyte/core/utils";
import {
repositoryConfigSchema,
resticBackupOutputSchema,
resticBackupProgressSchema,
type CompressionMode,
} from "@zerobyte/core/restic";
const compressionModeSchema = z.enum(["off", "auto", "max"]) satisfies z.ZodType<CompressionMode>;
const backupExecutionOptionsSchema = z.object({
tags: z.array(z.string()).optional(),
oneFileSystem: z.boolean().optional(),
exclude: z.array(z.string()).optional(),
excludeIfPresent: z.array(z.string()).optional(),
includePaths: z.array(z.string()).optional(),
includePatterns: z.array(z.string()).optional(),
customResticParams: z.array(z.string()).optional(),
compressionMode: compressionModeSchema.optional(),
});
const backupRuntimeSchema = z.object({
password: z.string(),
cacheDir: z.string(),
passFile: z.string(),
defaultExcludes: z.array(z.string()),
hostname: z.string().optional(),
});
const backupRunSchema = z.object({
type: z.literal("backup.run"),
payload: z.object({
jobId: z.string(),
scheduleId: z.string(),
organizationId: z.string(),
sourcePath: z.string(),
repositoryConfig: repositoryConfigSchema,
options: backupExecutionOptionsSchema,
runtime: backupRuntimeSchema,
}),
});
const backupCancelSchema = z.object({
type: z.literal("backup.cancel"),
payload: z.object({ jobId: z.string(), scheduleId: z.string() }),
});
const heartbeatPingSchema = z.object({
type: z.literal("heartbeat.ping"),
payload: z.object({ sentAt: z.number() }),
});
const agentReadySchema = z.object({
type: z.literal("agent.ready"),
payload: z.object({ agentId: z.string() }),
});
const backupStartedSchema = z.object({
type: z.literal("backup.started"),
payload: z.object({ jobId: z.string(), scheduleId: z.string() }),
});
const backupProgressSchema = z.object({
type: z.literal("backup.progress"),
payload: z.object({
jobId: z.string(),
scheduleId: z.string(),
progress: resticBackupProgressSchema,
}),
});
const backupCompletedSchema = z.object({
type: z.literal("backup.completed"),
payload: z.object({
jobId: z.string(),
scheduleId: z.string(),
exitCode: z.number(),
result: resticBackupOutputSchema.nullable(),
warningDetails: z.string().optional(),
}),
});
const backupFailedSchema = z.object({
type: z.literal("backup.failed"),
payload: z.object({
jobId: z.string(),
scheduleId: z.string(),
error: z.string(),
errorDetails: z.string().optional(),
}),
});
const backupCancelledSchema = z.object({
type: z.literal("backup.cancelled"),
payload: z.object({
jobId: z.string(),
scheduleId: z.string(),
message: z.string().optional(),
}),
});
const heartbeatPongSchema = z.object({
type: z.literal("heartbeat.pong"),
payload: z.object({ sentAt: z.number() }),
});
const controllerMessageSchema = z.discriminatedUnion("type", [
backupRunSchema,
backupCancelSchema,
heartbeatPingSchema,
]);
const agentMessageSchema = z.discriminatedUnion("type", [
agentReadySchema,
backupStartedSchema,
backupProgressSchema,
backupCompletedSchema,
backupFailedSchema,
backupCancelledSchema,
heartbeatPongSchema,
]);
export type BackupRunPayload = z.infer<typeof backupRunSchema>["payload"];
export type BackupCancelPayload = z.infer<typeof backupCancelSchema>["payload"];
export type BackupStartedPayload = z.infer<typeof backupStartedSchema>["payload"];
export type BackupProgressPayload = z.infer<typeof backupProgressSchema>["payload"];
export type BackupCompletedPayload = z.infer<typeof backupCompletedSchema>["payload"];
export type BackupFailedPayload = z.infer<typeof backupFailedSchema>["payload"];
export type BackupCancelledPayload = z.infer<typeof backupCancelledSchema>["payload"];
export type ControllerMessage = z.infer<typeof controllerMessageSchema>;
export type AgentMessage = z.infer<typeof agentMessageSchema>;
type Brand<TValue, TBrand extends string> = TValue & {
readonly __brand: TBrand;
};
export type ControllerWireMessage = Brand<string, "ControllerWireMessage">;
export type AgentWireMessage = Brand<string, "AgentWireMessage">;
type PayloadForMessage<TMessage extends { type: string; payload: unknown }, TType extends TMessage["type"]> = Extract<
TMessage,
{ type: TType }
>["payload"];
export const parseControllerMessage = (data: string) => {
const parsed = safeJsonParse(data);
if (parsed === null) {
return null;
}
return controllerMessageSchema.safeParse(parsed);
};
export const parseAgentMessage = (data: string) => {
const parsed = safeJsonParse(data);
if (parsed === null) {
return null;
}
return agentMessageSchema.safeParse(parsed);
};
export const createControllerMessage = <TType extends ControllerMessage["type"]>(
type: TType,
payload: PayloadForMessage<ControllerMessage, TType>,
) => {
return JSON.stringify(controllerMessageSchema.parse({ type, payload })) as ControllerWireMessage;
};
export const createAgentMessage = <TType extends AgentMessage["type"]>(
type: TType,
payload: PayloadForMessage<AgentMessage, TType>,
) => {
return JSON.stringify(agentMessageSchema.parse({ type, payload })) as AgentWireMessage;
};

View file

@ -0,0 +1,30 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"types": ["bun", "node"],
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}