feat(contracts): add shared agent/controller protocol
This commit is contained in:
parent
049becb900
commit
71f0ec02d3
5 changed files with 247 additions and 0 deletions
15
bun.lock
15
bun.lock
|
|
@ -34,6 +34,7 @@
|
||||||
"@tanstack/react-router": "^1.168.1",
|
"@tanstack/react-router": "^1.168.1",
|
||||||
"@tanstack/react-router-ssr-query": "^1.166.10",
|
"@tanstack/react-router-ssr-query": "^1.166.10",
|
||||||
"@tanstack/react-start": "^1.167.1",
|
"@tanstack/react-start": "^1.167.1",
|
||||||
|
"@zerobyte/contracts": "workspace:*",
|
||||||
"@zerobyte/core": "workspace:*",
|
"@zerobyte/core": "workspace:*",
|
||||||
"better-auth": "^1.5.5",
|
"better-auth": "^1.5.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|
@ -114,6 +115,18 @@
|
||||||
"wait-for-expect": "^4.0.0",
|
"wait-for-expect": "^4.0.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/contracts": {
|
||||||
|
"name": "@zerobyte/contracts",
|
||||||
|
"dependencies": {
|
||||||
|
"@zerobyte/core": "workspace:*",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@zerobyte/core",
|
"name": "@zerobyte/core",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -1044,6 +1057,8 @@
|
||||||
|
|
||||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
||||||
|
|
||||||
|
"@zerobyte/contracts": ["@zerobyte/contracts@workspace:packages/contracts"],
|
||||||
|
|
||||||
"@zerobyte/core": ["@zerobyte/core@workspace:packages/core"],
|
"@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=="],
|
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@
|
||||||
"@tanstack/react-router": "^1.168.1",
|
"@tanstack/react-router": "^1.168.1",
|
||||||
"@tanstack/react-router-ssr-query": "^1.166.10",
|
"@tanstack/react-router-ssr-query": "^1.166.10",
|
||||||
"@tanstack/react-start": "^1.167.1",
|
"@tanstack/react-start": "^1.167.1",
|
||||||
|
"@zerobyte/contracts": "workspace:*",
|
||||||
"@zerobyte/core": "workspace:*",
|
"@zerobyte/core": "workspace:*",
|
||||||
"better-auth": "^1.5.5",
|
"better-auth": "^1.5.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|
|
||||||
24
packages/contracts/package.json
Normal file
24
packages/contracts/package.json
Normal 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": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
177
packages/contracts/src/agent-protocol.ts
Normal file
177
packages/contracts/src/agent-protocol.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
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"];
|
||||||
|
|
||||||
|
const parseJsonMessage = (data: string) => safeJsonParse<unknown>(data);
|
||||||
|
|
||||||
|
export const parseControllerMessage = (data: ControllerWireMessage) => {
|
||||||
|
const parsed = parseJsonMessage(data);
|
||||||
|
if (parsed === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return controllerMessageSchema.safeParse(parsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseAgentMessage = (data: string) => {
|
||||||
|
const parsed = parseJsonMessage(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;
|
||||||
|
};
|
||||||
30
packages/contracts/tsconfig.json
Normal file
30
packages/contracts/tsconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue