@zuoyehaoduoa/wire 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ RemoteMachineDescriptionSchema,
4
+ RemoteRpcEnvelopeSchema,
5
+ RemoteRpcResultEnvelopeSchema,
6
+ } from "./remote-protocol";
7
+
8
+ describe("remote-protocol", () => {
9
+ it("parses remote RPC envelopes", () => {
10
+ const parsed = RemoteRpcEnvelopeSchema.parse({
11
+ requestId: "req-1",
12
+ body: {
13
+ method: "GET",
14
+ path: "/api/projects",
15
+ },
16
+ });
17
+
18
+ expect(parsed.requestId).toBe("req-1");
19
+ });
20
+
21
+ it("parses success and error RPC results", () => {
22
+ const success = RemoteRpcResultEnvelopeSchema.parse({
23
+ ok: true,
24
+ requestId: "req-1",
25
+ body: { status: 200 },
26
+ });
27
+ const failure = RemoteRpcResultEnvelopeSchema.parse({
28
+ ok: false,
29
+ requestId: "req-2",
30
+ error: "boom",
31
+ });
32
+
33
+ expect(success.ok).toBe(true);
34
+ expect(failure.ok).toBe(false);
35
+ });
36
+
37
+ it("parses remote machine descriptions", () => {
38
+ const parsed = RemoteMachineDescriptionSchema.parse({
39
+ id: "machine-a",
40
+ metadata: {
41
+ machineId: "machine-a",
42
+ label: "MacBook",
43
+ host: "macbook.local",
44
+ platform: "darwin",
45
+ codexDir: "/Users/test/.codex",
46
+ cliVersion: "0.3.0",
47
+ rpcSigningPublicKey: "pk",
48
+ },
49
+ state: {
50
+ status: "running",
51
+ connectedAt: 1,
52
+ lastHeartbeatAt: 2,
53
+ localWebUrl: null,
54
+ },
55
+ active: true,
56
+ activeAt: 2,
57
+ });
58
+
59
+ expect(parsed.metadata.platform).toBe("darwin");
60
+ });
61
+ });
@@ -0,0 +1,53 @@
1
+ import * as z from "zod";
2
+
3
+ export const RemoteRpcEnvelopeSchema = z.object({
4
+ requestId: z.string(),
5
+ body: z.unknown(),
6
+ });
7
+ export type RemoteRpcEnvelope = z.infer<typeof RemoteRpcEnvelopeSchema>;
8
+
9
+ export const RemoteRpcResultEnvelopeSchema = z.discriminatedUnion("ok", [
10
+ z.object({
11
+ ok: z.literal(true),
12
+ requestId: z.string(),
13
+ body: z.unknown(),
14
+ }),
15
+ z.object({
16
+ ok: z.literal(false),
17
+ requestId: z.string(),
18
+ error: z.string(),
19
+ }),
20
+ ]);
21
+ export type RemoteRpcResultEnvelope = z.infer<
22
+ typeof RemoteRpcResultEnvelopeSchema
23
+ >;
24
+
25
+ export const RemoteMachineMetadataSchema = z.object({
26
+ machineId: z.string(),
27
+ label: z.string(),
28
+ host: z.string(),
29
+ platform: z.string(),
30
+ codexDir: z.string(),
31
+ cliVersion: z.string(),
32
+ rpcSigningPublicKey: z.string().min(1),
33
+ });
34
+ export type RemoteMachineMetadata = z.infer<typeof RemoteMachineMetadataSchema>;
35
+
36
+ export const RemoteMachineStateSchema = z.object({
37
+ status: z.enum(["running", "offline", "error"]),
38
+ connectedAt: z.number(),
39
+ lastHeartbeatAt: z.number(),
40
+ localWebUrl: z.string().nullable().optional(),
41
+ });
42
+ export type RemoteMachineState = z.infer<typeof RemoteMachineStateSchema>;
43
+
44
+ export const RemoteMachineDescriptionSchema = z.object({
45
+ id: z.string(),
46
+ metadata: RemoteMachineMetadataSchema,
47
+ state: RemoteMachineStateSchema.nullable(),
48
+ active: z.boolean(),
49
+ activeAt: z.number(),
50
+ });
51
+ export type RemoteMachineDescription = z.infer<
52
+ typeof RemoteMachineDescriptionSchema
53
+ >;
@@ -0,0 +1,170 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createId } from "@paralleldrive/cuid2";
3
+ import {
4
+ createEnvelope,
5
+ sessionEnvelopeSchema,
6
+ sessionEventSchema,
7
+ type SessionEvent,
8
+ } from "./sessionProtocol";
9
+
10
+ describe("session protocol schemas", () => {
11
+ it("accepts all supported event types", () => {
12
+ const events: SessionEvent[] = [
13
+ { t: "text", text: "hello" },
14
+ { t: "text", text: "thinking", thinking: true },
15
+ { t: "service", text: "**Service:** restarting MCP bridge" },
16
+ {
17
+ t: "tool-call-start",
18
+ call: "call-1",
19
+ name: "CodexBash",
20
+ title: "Run `ls`",
21
+ description: "Run `ls -la` in the repo root",
22
+ args: { command: "ls -la" },
23
+ },
24
+ { t: "tool-call-end", call: "call-1" },
25
+ { t: "file", ref: "upload-1", name: "report.txt", size: 1024 },
26
+ {
27
+ t: "file",
28
+ ref: "upload-2",
29
+ name: "image.png",
30
+ size: 2048,
31
+ image: { thumbhash: "abc", width: 100, height: 80 },
32
+ },
33
+ { t: "turn-start" },
34
+ { t: "start", title: "Research agent" },
35
+ { t: "turn-end", status: "completed" },
36
+ { t: "stop" },
37
+ ];
38
+
39
+ for (const event of events) {
40
+ expect(sessionEventSchema.safeParse(event).success).toBe(true);
41
+ }
42
+ });
43
+
44
+ it("rejects malformed events", () => {
45
+ expect(
46
+ sessionEventSchema.safeParse({ t: "tool-call-start", call: "1" }).success,
47
+ ).toBe(false);
48
+ expect(
49
+ sessionEventSchema.safeParse({ t: "file", ref: "x", name: "x" }).success,
50
+ ).toBe(false);
51
+ expect(
52
+ sessionEventSchema.safeParse({
53
+ t: "file",
54
+ ref: "x",
55
+ name: "x",
56
+ size: 1,
57
+ image: { width: 10, height: 10 },
58
+ }).success,
59
+ ).toBe(false);
60
+ expect(sessionEventSchema.safeParse({ t: "turn-end" }).success).toBe(false);
61
+ expect(
62
+ sessionEventSchema.safeParse({ t: "turn-end", status: "canceled" })
63
+ .success,
64
+ ).toBe(false);
65
+ expect(sessionEventSchema.safeParse({ t: "start", title: 1 }).success).toBe(
66
+ false,
67
+ );
68
+ expect(sessionEventSchema.safeParse({ t: "service" }).success).toBe(false);
69
+ expect(sessionEventSchema.safeParse({ t: "not-real" }).success).toBe(false);
70
+ });
71
+
72
+ it("validates envelopes that include turn/subagent", () => {
73
+ const subagent = createId();
74
+ const envelope = {
75
+ id: "msg-1",
76
+ time: 1234,
77
+ role: "agent" as const,
78
+ turn: "turn-1",
79
+ subagent,
80
+ ev: { t: "text", text: "hello" } as const,
81
+ };
82
+
83
+ const parsed = sessionEnvelopeSchema.safeParse(envelope);
84
+ expect(parsed.success).toBe(true);
85
+ });
86
+
87
+ it("rejects session role envelopes for text events", () => {
88
+ const parsed = sessionEnvelopeSchema.safeParse({
89
+ id: "msg-session-1",
90
+ role: "session",
91
+ ev: { t: "text", text: "shadow copy of user message" },
92
+ });
93
+
94
+ expect(parsed.success).toBe(false);
95
+ });
96
+
97
+ it("rejects service from non-agent role", () => {
98
+ const parsed = sessionEnvelopeSchema.safeParse({
99
+ id: "msg-2",
100
+ role: "user",
101
+ ev: { t: "service", text: "internal event" },
102
+ });
103
+
104
+ expect(parsed.success).toBe(false);
105
+ });
106
+
107
+ it("rejects start from non-agent role", () => {
108
+ const subagent = createId();
109
+ const parsed = sessionEnvelopeSchema.safeParse({
110
+ id: "msg-3",
111
+ role: "user",
112
+ subagent,
113
+ ev: { t: "start", title: "Research agent" },
114
+ });
115
+
116
+ expect(parsed.success).toBe(false);
117
+ });
118
+
119
+ it("rejects non-cuid subagent values", () => {
120
+ const parsed = sessionEnvelopeSchema.safeParse({
121
+ id: "msg-4",
122
+ role: "agent",
123
+ turn: "turn-1",
124
+ subagent: "provider-tool-id",
125
+ ev: { t: "text", text: "hello" },
126
+ });
127
+
128
+ expect(parsed.success).toBe(false);
129
+ });
130
+ });
131
+
132
+ describe("createEnvelope", () => {
133
+ it("creates id by default", () => {
134
+ const envelope = createEnvelope("agent", { t: "turn-start" });
135
+ expect(typeof envelope.id).toBe("string");
136
+ expect(typeof envelope.time).toBe("number");
137
+ expect(envelope.id.length).toBeGreaterThan(0);
138
+ expect(envelope.role).toBe("agent");
139
+ expect(envelope.ev.t).toBe("turn-start");
140
+ });
141
+
142
+ it("respects explicit options", () => {
143
+ const subagent = createId();
144
+ const envelope = createEnvelope(
145
+ "agent",
146
+ { t: "tool-call-end", call: "call-1" },
147
+ {
148
+ id: "fixed-id",
149
+ time: 12345,
150
+ turn: "turn-1",
151
+ subagent,
152
+ },
153
+ );
154
+
155
+ expect(envelope).toEqual({
156
+ id: "fixed-id",
157
+ time: 12345,
158
+ role: "agent",
159
+ turn: "turn-1",
160
+ subagent,
161
+ ev: { t: "tool-call-end", call: "call-1" },
162
+ });
163
+ });
164
+
165
+ it("validates role/event compatibility", () => {
166
+ expect(() =>
167
+ createEnvelope("user", { t: "service", text: "internal event" }),
168
+ ).toThrow();
169
+ });
170
+ });
@@ -0,0 +1,141 @@
1
+ import { createId, isCuid } from "@paralleldrive/cuid2";
2
+ import * as z from "zod";
3
+
4
+ export const sessionRoleSchema = z.enum(["user", "agent"]);
5
+ export type SessionRole = z.infer<typeof sessionRoleSchema>;
6
+
7
+ export const sessionTextEventSchema = z.object({
8
+ t: z.literal("text"),
9
+ text: z.string(),
10
+ thinking: z.boolean().optional(),
11
+ });
12
+
13
+ export const sessionServiceMessageEventSchema = z.object({
14
+ t: z.literal("service"),
15
+ text: z.string(),
16
+ });
17
+
18
+ export const sessionToolCallStartEventSchema = z.object({
19
+ t: z.literal("tool-call-start"),
20
+ call: z.string(),
21
+ name: z.string(),
22
+ title: z.string(),
23
+ description: z.string(),
24
+ args: z.record(z.string(), z.unknown()),
25
+ });
26
+
27
+ export const sessionToolCallEndEventSchema = z.object({
28
+ t: z.literal("tool-call-end"),
29
+ call: z.string(),
30
+ });
31
+
32
+ export const sessionFileEventSchema = z.object({
33
+ t: z.literal("file"),
34
+ ref: z.string(),
35
+ name: z.string(),
36
+ size: z.number(),
37
+ image: z
38
+ .object({
39
+ width: z.number(),
40
+ height: z.number(),
41
+ thumbhash: z.string(),
42
+ })
43
+ .optional(),
44
+ });
45
+
46
+ export const sessionTurnStartEventSchema = z.object({
47
+ t: z.literal("turn-start"),
48
+ });
49
+
50
+ export const sessionStartEventSchema = z.object({
51
+ t: z.literal("start"),
52
+ title: z.string().optional(),
53
+ });
54
+
55
+ export const sessionTurnEndStatusSchema = z.enum([
56
+ "completed",
57
+ "failed",
58
+ "cancelled",
59
+ ]);
60
+ export type SessionTurnEndStatus = z.infer<typeof sessionTurnEndStatusSchema>;
61
+
62
+ export const sessionTurnEndEventSchema = z.object({
63
+ t: z.literal("turn-end"),
64
+ status: sessionTurnEndStatusSchema,
65
+ });
66
+
67
+ export const sessionStopEventSchema = z.object({
68
+ t: z.literal("stop"),
69
+ });
70
+
71
+ export const sessionEventSchema = z.discriminatedUnion("t", [
72
+ sessionTextEventSchema,
73
+ sessionServiceMessageEventSchema,
74
+ sessionToolCallStartEventSchema,
75
+ sessionToolCallEndEventSchema,
76
+ sessionFileEventSchema,
77
+ sessionTurnStartEventSchema,
78
+ sessionStartEventSchema,
79
+ sessionTurnEndEventSchema,
80
+ sessionStopEventSchema,
81
+ ]);
82
+
83
+ export type SessionEvent = z.infer<typeof sessionEventSchema>;
84
+
85
+ export const sessionEnvelopeSchema = z
86
+ .object({
87
+ id: z.string(),
88
+ time: z.number(),
89
+ role: sessionRoleSchema,
90
+ turn: z.string().optional(),
91
+ subagent: z
92
+ .string()
93
+ .refine((value) => isCuid(value), {
94
+ message: "subagent must be a cuid2 value",
95
+ })
96
+ .optional(),
97
+ ev: sessionEventSchema,
98
+ })
99
+ .superRefine((envelope, ctx) => {
100
+ if (envelope.ev.t === "service" && envelope.role !== "agent") {
101
+ ctx.addIssue({
102
+ code: z.ZodIssueCode.custom,
103
+ message: 'service events must use role "agent"',
104
+ path: ["role"],
105
+ });
106
+ }
107
+ if (
108
+ (envelope.ev.t === "start" || envelope.ev.t === "stop") &&
109
+ envelope.role !== "agent"
110
+ ) {
111
+ ctx.addIssue({
112
+ code: z.ZodIssueCode.custom,
113
+ message: `${envelope.ev.t} events must use role "agent"`,
114
+ path: ["role"],
115
+ });
116
+ }
117
+ });
118
+
119
+ export type SessionEnvelope = z.infer<typeof sessionEnvelopeSchema>;
120
+
121
+ export type CreateEnvelopeOptions = {
122
+ id?: string;
123
+ time?: number;
124
+ turn?: string;
125
+ subagent?: string;
126
+ };
127
+
128
+ export function createEnvelope(
129
+ role: SessionRole,
130
+ ev: SessionEvent,
131
+ opts: CreateEnvelopeOptions = {},
132
+ ): SessionEnvelope {
133
+ return sessionEnvelopeSchema.parse({
134
+ id: opts.id ?? createId(),
135
+ time: opts.time ?? Date.now(),
136
+ role,
137
+ ...(opts.turn ? { turn: opts.turn } : {}),
138
+ ...(opts.subagent ? { subagent: opts.subagent } : {}),
139
+ ev,
140
+ });
141
+ }