@crewhaus/gateway-protocol 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.
- package/package.json +42 -0
- package/src/index.test.ts +104 -0
- package/src/index.ts +228 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/gateway-protocol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "JSON-RPC wire protocol for the managed-daemon gateway — versioned envelope + Zod schemas",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "bun test src"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@crewhaus/errors": "0.0.0",
|
|
16
|
+
"zod": "^3.23.8"
|
|
17
|
+
},
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Max Meier",
|
|
21
|
+
"email": "max@studiomax.io",
|
|
22
|
+
"url": "https://studiomax.io"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
27
|
+
"directory": "packages/gateway-protocol"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/gateway-protocol#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "restricted"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"NOTICE"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
ErrorCode,
|
|
4
|
+
GatewayProtocolError,
|
|
5
|
+
PROTOCOL_VERSION,
|
|
6
|
+
decodeRequest,
|
|
7
|
+
encodeError,
|
|
8
|
+
encodeSuccess,
|
|
9
|
+
} from "./index";
|
|
10
|
+
|
|
11
|
+
describe("envelope", () => {
|
|
12
|
+
test("PROTOCOL_VERSION is the v1 string", () => {
|
|
13
|
+
expect(PROTOCOL_VERSION).toBe("crewhaus.v1");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("decodeRequest accepts a well-formed runs.create", () => {
|
|
17
|
+
const raw = {
|
|
18
|
+
protocol: "crewhaus.v1",
|
|
19
|
+
id: "abc",
|
|
20
|
+
method: "runs.create",
|
|
21
|
+
params: { spec: "x", input: "hi" },
|
|
22
|
+
};
|
|
23
|
+
const r = decodeRequest(raw);
|
|
24
|
+
expect(r.method).toBe("runs.create");
|
|
25
|
+
expect((r.params as { spec: string }).spec).toBe("x");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("rejects wrong protocol version", () => {
|
|
29
|
+
const raw = {
|
|
30
|
+
protocol: "crewhaus.v0",
|
|
31
|
+
id: "abc",
|
|
32
|
+
method: "runs.create",
|
|
33
|
+
params: { spec: "x", input: "" },
|
|
34
|
+
};
|
|
35
|
+
expect(() => decodeRequest(raw)).toThrow(GatewayProtocolError);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("rejects unknown method", () => {
|
|
39
|
+
const raw = {
|
|
40
|
+
protocol: "crewhaus.v1",
|
|
41
|
+
id: "abc",
|
|
42
|
+
method: "runs.nuke",
|
|
43
|
+
params: {},
|
|
44
|
+
};
|
|
45
|
+
expect(() => decodeRequest(raw)).toThrow(/unknown method "runs.nuke"/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("rejects invalid params for known method", () => {
|
|
49
|
+
const raw = {
|
|
50
|
+
protocol: "crewhaus.v1",
|
|
51
|
+
id: "abc",
|
|
52
|
+
method: "runs.create",
|
|
53
|
+
params: { spec: 1 }, // wrong type
|
|
54
|
+
};
|
|
55
|
+
expect(() => decodeRequest(raw)).toThrow(/invalid params/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("rejects extra unknown fields on the envelope (.strict)", () => {
|
|
59
|
+
const raw = {
|
|
60
|
+
protocol: "crewhaus.v1",
|
|
61
|
+
id: "abc",
|
|
62
|
+
method: "runs.create",
|
|
63
|
+
params: { spec: "x", input: "" },
|
|
64
|
+
foo: "bar",
|
|
65
|
+
};
|
|
66
|
+
expect(() => decodeRequest(raw)).toThrow(GatewayProtocolError);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("response encoders", () => {
|
|
71
|
+
test("encodeSuccess shape", () => {
|
|
72
|
+
const r = encodeSuccess("id-1", { ok: true });
|
|
73
|
+
expect(r).toEqual({ protocol: "crewhaus.v1", id: "id-1", result: { ok: true } });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("encodeError shape with no data", () => {
|
|
77
|
+
const r = encodeError("id-1", ErrorCode.NotFound, "missing");
|
|
78
|
+
expect(r).toEqual({
|
|
79
|
+
protocol: "crewhaus.v1",
|
|
80
|
+
id: "id-1",
|
|
81
|
+
error: { code: "not_found", message: "missing" },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("encodeError shape with data", () => {
|
|
86
|
+
const r = encodeError("id-1", ErrorCode.BadRequest, "bad", { field: "x" });
|
|
87
|
+
expect(r).toEqual({
|
|
88
|
+
protocol: "crewhaus.v1",
|
|
89
|
+
id: "id-1",
|
|
90
|
+
error: { code: "bad_request", message: "bad", data: { field: "x" } },
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("standard error codes are wire-stable", () => {
|
|
96
|
+
test("expected codes exist", () => {
|
|
97
|
+
expect(ErrorCode.Unauthorized).toBe("unauthorized");
|
|
98
|
+
expect(ErrorCode.Forbidden).toBe("forbidden");
|
|
99
|
+
expect(ErrorCode.NotFound).toBe("not_found");
|
|
100
|
+
expect(ErrorCode.BadRequest).toBe("bad_request");
|
|
101
|
+
expect(ErrorCode.BudgetExceeded).toBe("budget_exceeded");
|
|
102
|
+
expect(ErrorCode.InternalError).toBe("internal_error");
|
|
103
|
+
});
|
|
104
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog R16 `gateway-protocol` — wire contract for the managed-daemon
|
|
3
|
+
* gateway.
|
|
4
|
+
*
|
|
5
|
+
* Every request and response is wrapped in a versioned envelope so
|
|
6
|
+
* future protocol revisions can fan out on `protocol`. v1 carries:
|
|
7
|
+
*
|
|
8
|
+
* { protocol: "crewhaus.v1", id: <opaque>, method: <string>, params: <obj> }
|
|
9
|
+
*
|
|
10
|
+
* All schemas are exported as Zod runtime validators; the inferred
|
|
11
|
+
* types are the canonical TS API. Reference clients (TS, Python) ship
|
|
12
|
+
* alongside this package — see `clients/` for snippets external app
|
|
13
|
+
* servers can paste into their own build.
|
|
14
|
+
*
|
|
15
|
+
* Methods (v1):
|
|
16
|
+
* runs.create — start a new run, return runId
|
|
17
|
+
* runs.continue — append a user turn to an existing session
|
|
18
|
+
* runs.cancel — abort an in-flight run
|
|
19
|
+
* runs.subscribe — SSE stream of trace events for a runId
|
|
20
|
+
* sessions.list — list per-tenant sessions
|
|
21
|
+
* sessions.fork — branch a session at a specific event
|
|
22
|
+
* audit.tail — stream the per-tenant audit log
|
|
23
|
+
*
|
|
24
|
+
* Layer R16. Pairs with `gateway-server` (R16 — Bun.serve daemon) and
|
|
25
|
+
* `target-managed` (F2 — codegen).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
|
|
31
|
+
export const PROTOCOL_VERSION = "crewhaus.v1" as const;
|
|
32
|
+
|
|
33
|
+
export class GatewayProtocolError extends CrewhausError {
|
|
34
|
+
override readonly name = "GatewayProtocolError";
|
|
35
|
+
constructor(message: string, cause?: unknown) {
|
|
36
|
+
super("config", message, cause);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const protocolField = z.literal(PROTOCOL_VERSION);
|
|
41
|
+
|
|
42
|
+
const requestEnvelope = z
|
|
43
|
+
.object({
|
|
44
|
+
protocol: protocolField,
|
|
45
|
+
id: z.string().min(1),
|
|
46
|
+
method: z.string().min(1),
|
|
47
|
+
params: z.unknown(),
|
|
48
|
+
})
|
|
49
|
+
.strict();
|
|
50
|
+
|
|
51
|
+
const successEnvelope = z
|
|
52
|
+
.object({
|
|
53
|
+
protocol: protocolField,
|
|
54
|
+
id: z.string().min(1),
|
|
55
|
+
result: z.unknown(),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
|
|
59
|
+
const errorEnvelope = z
|
|
60
|
+
.object({
|
|
61
|
+
protocol: protocolField,
|
|
62
|
+
id: z.string().min(1),
|
|
63
|
+
error: z
|
|
64
|
+
.object({
|
|
65
|
+
code: z.string().min(1),
|
|
66
|
+
message: z.string().min(1),
|
|
67
|
+
data: z.unknown().optional(),
|
|
68
|
+
})
|
|
69
|
+
.strict(),
|
|
70
|
+
})
|
|
71
|
+
.strict();
|
|
72
|
+
|
|
73
|
+
export const ResponseEnvelope = z.union([successEnvelope, errorEnvelope]);
|
|
74
|
+
export const RequestEnvelope = requestEnvelope;
|
|
75
|
+
export type RequestEnvelopeT = z.infer<typeof RequestEnvelope>;
|
|
76
|
+
export type ResponseEnvelopeT = z.infer<typeof ResponseEnvelope>;
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Method-specific schemas.
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
export const RunsCreateParams = z
|
|
83
|
+
.object({
|
|
84
|
+
spec: z.string().min(1),
|
|
85
|
+
input: z.string(),
|
|
86
|
+
sessionId: z.string().min(1).optional(),
|
|
87
|
+
})
|
|
88
|
+
.strict();
|
|
89
|
+
export const RunsCreateResult = z
|
|
90
|
+
.object({
|
|
91
|
+
runId: z.string().min(1),
|
|
92
|
+
sessionId: z.string().min(1),
|
|
93
|
+
tenantId: z.string().min(1),
|
|
94
|
+
})
|
|
95
|
+
.strict();
|
|
96
|
+
export type RunsCreateParamsT = z.infer<typeof RunsCreateParams>;
|
|
97
|
+
export type RunsCreateResultT = z.infer<typeof RunsCreateResult>;
|
|
98
|
+
|
|
99
|
+
export const RunsContinueParams = z
|
|
100
|
+
.object({
|
|
101
|
+
sessionId: z.string().min(1),
|
|
102
|
+
input: z.string(),
|
|
103
|
+
})
|
|
104
|
+
.strict();
|
|
105
|
+
export const RunsContinueResult = z
|
|
106
|
+
.object({
|
|
107
|
+
runId: z.string().min(1),
|
|
108
|
+
sessionId: z.string().min(1),
|
|
109
|
+
tenantId: z.string().min(1),
|
|
110
|
+
})
|
|
111
|
+
.strict();
|
|
112
|
+
export type RunsContinueParamsT = z.infer<typeof RunsContinueParams>;
|
|
113
|
+
export type RunsContinueResultT = z.infer<typeof RunsContinueResult>;
|
|
114
|
+
|
|
115
|
+
export const RunsCancelParams = z.object({ runId: z.string().min(1) }).strict();
|
|
116
|
+
export const RunsCancelResult = z.object({ ok: z.boolean() }).strict();
|
|
117
|
+
export type RunsCancelParamsT = z.infer<typeof RunsCancelParams>;
|
|
118
|
+
export type RunsCancelResultT = z.infer<typeof RunsCancelResult>;
|
|
119
|
+
|
|
120
|
+
export const RunsSubscribeParams = z.object({ runId: z.string().min(1) }).strict();
|
|
121
|
+
export type RunsSubscribeParamsT = z.infer<typeof RunsSubscribeParams>;
|
|
122
|
+
|
|
123
|
+
export const SessionsListParams = z.object({}).strict();
|
|
124
|
+
export const SessionsListResult = z
|
|
125
|
+
.object({
|
|
126
|
+
sessions: z.array(
|
|
127
|
+
z
|
|
128
|
+
.object({
|
|
129
|
+
id: z.string().min(1),
|
|
130
|
+
tenantId: z.string().min(1),
|
|
131
|
+
updatedAt: z.string().min(1),
|
|
132
|
+
})
|
|
133
|
+
.strict(),
|
|
134
|
+
),
|
|
135
|
+
})
|
|
136
|
+
.strict();
|
|
137
|
+
export type SessionsListParamsT = z.infer<typeof SessionsListParams>;
|
|
138
|
+
export type SessionsListResultT = z.infer<typeof SessionsListResult>;
|
|
139
|
+
|
|
140
|
+
export const SessionsForkParams = z
|
|
141
|
+
.object({ sessionId: z.string().min(1), atEventTs: z.number().int().nonnegative() })
|
|
142
|
+
.strict();
|
|
143
|
+
export const SessionsForkResult = z.object({ newSessionId: z.string().min(1) }).strict();
|
|
144
|
+
export type SessionsForkParamsT = z.infer<typeof SessionsForkParams>;
|
|
145
|
+
export type SessionsForkResultT = z.infer<typeof SessionsForkResult>;
|
|
146
|
+
|
|
147
|
+
export const AuditTailParams = z
|
|
148
|
+
.object({ tenantId: z.string().min(1), sinceTs: z.number().int().nonnegative().optional() })
|
|
149
|
+
.strict();
|
|
150
|
+
export type AuditTailParamsT = z.infer<typeof AuditTailParams>;
|
|
151
|
+
|
|
152
|
+
export const Method = z.enum([
|
|
153
|
+
"runs.create",
|
|
154
|
+
"runs.continue",
|
|
155
|
+
"runs.cancel",
|
|
156
|
+
"runs.subscribe",
|
|
157
|
+
"sessions.list",
|
|
158
|
+
"sessions.fork",
|
|
159
|
+
"audit.tail",
|
|
160
|
+
]);
|
|
161
|
+
export type MethodT = z.infer<typeof Method>;
|
|
162
|
+
|
|
163
|
+
const PARAM_SCHEMAS: Record<MethodT, z.ZodType<unknown>> = {
|
|
164
|
+
"runs.create": RunsCreateParams,
|
|
165
|
+
"runs.continue": RunsContinueParams,
|
|
166
|
+
"runs.cancel": RunsCancelParams,
|
|
167
|
+
"runs.subscribe": RunsSubscribeParams,
|
|
168
|
+
"sessions.list": SessionsListParams,
|
|
169
|
+
"sessions.fork": SessionsForkParams,
|
|
170
|
+
"audit.tail": AuditTailParams,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export function decodeRequest(raw: unknown): RequestEnvelopeT & { method: MethodT } {
|
|
174
|
+
const parsed = RequestEnvelope.safeParse(raw);
|
|
175
|
+
if (!parsed.success) {
|
|
176
|
+
throw new GatewayProtocolError(
|
|
177
|
+
`invalid envelope: ${parsed.error.issues
|
|
178
|
+
.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
179
|
+
.join("; ")}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const m = Method.safeParse(parsed.data.method);
|
|
183
|
+
if (!m.success) {
|
|
184
|
+
throw new GatewayProtocolError(`unknown method "${parsed.data.method}"`);
|
|
185
|
+
}
|
|
186
|
+
const paramSchema = PARAM_SCHEMAS[m.data];
|
|
187
|
+
const params = paramSchema.safeParse(parsed.data.params);
|
|
188
|
+
if (!params.success) {
|
|
189
|
+
throw new GatewayProtocolError(
|
|
190
|
+
`invalid params for ${m.data}: ${params.error.issues
|
|
191
|
+
.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
192
|
+
.join("; ")}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return { ...parsed.data, method: m.data, params: params.data };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function encodeSuccess(id: string, result: unknown): ResponseEnvelopeT {
|
|
199
|
+
return { protocol: PROTOCOL_VERSION, id, result };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function encodeError(
|
|
203
|
+
id: string,
|
|
204
|
+
code: string,
|
|
205
|
+
message: string,
|
|
206
|
+
data?: unknown,
|
|
207
|
+
): ResponseEnvelopeT {
|
|
208
|
+
return {
|
|
209
|
+
protocol: PROTOCOL_VERSION,
|
|
210
|
+
id,
|
|
211
|
+
error: { code, message, ...(data !== undefined ? { data } : {}) },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Standard error codes — wire-stable so reference clients can switch on them.
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
export const ErrorCode = {
|
|
220
|
+
Unauthorized: "unauthorized",
|
|
221
|
+
Forbidden: "forbidden",
|
|
222
|
+
NotFound: "not_found",
|
|
223
|
+
BadRequest: "bad_request",
|
|
224
|
+
BudgetExceeded: "budget_exceeded",
|
|
225
|
+
InternalError: "internal_error",
|
|
226
|
+
} as const;
|
|
227
|
+
|
|
228
|
+
export type ErrorCodeT = (typeof ErrorCode)[keyof typeof ErrorCode];
|