@rigkit/sdk 0.1.8
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/README.md +19 -0
- package/package.json +41 -0
- package/src/cli.ts +116 -0
- package/src/host.ts +46 -0
- package/src/index.test.ts +53 -0
- package/src/index.ts +84 -0
- package/src/runtime/api-handlers.ts +166 -0
- package/src/runtime/api.ts +69 -0
- package/src/runtime/app.test.ts +924 -0
- package/src/runtime/app.ts +63 -0
- package/src/runtime/cli.ts +115 -0
- package/src/runtime/control.ts +163 -0
- package/src/runtime/errors.ts +108 -0
- package/src/runtime/index.ts +81 -0
- package/src/runtime/openapi.ts +5 -0
- package/src/runtime/operations.ts +267 -0
- package/src/runtime/protocol.ts +193 -0
- package/src/runtime/runs.ts +292 -0
- package/src/runtime/server.ts +182 -0
- package/src/runtime/sessions.ts +257 -0
- package/src/runtime/state.ts +12 -0
- package/src/runtime/token.ts +16 -0
- package/src/runtime/types.ts +35 -0
- package/src/runtime/version.ts +1 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { Socket } from "@effect/platform";
|
|
2
|
+
import { RIGKIT_ENGINE_VERSION } from "@rigkit/engine";
|
|
3
|
+
import { Effect, type Scope } from "effect";
|
|
4
|
+
import { RIGKIT_RUNTIME_VERSION } from "./version.ts";
|
|
5
|
+
import {
|
|
6
|
+
RuntimeHostRequestError,
|
|
7
|
+
RuntimeRunCancelledError,
|
|
8
|
+
} from "./errors.ts";
|
|
9
|
+
import { clearPendingHostRequest, type RuntimeAppState } from "./control.ts";
|
|
10
|
+
import {
|
|
11
|
+
RUNTIME_PROTOCOL_HASH,
|
|
12
|
+
type RuntimeOperation,
|
|
13
|
+
} from "./protocol.ts";
|
|
14
|
+
import {
|
|
15
|
+
closeHostCapabilityResource,
|
|
16
|
+
failRun,
|
|
17
|
+
subscribeRunEvents,
|
|
18
|
+
type RunRecord,
|
|
19
|
+
} from "./runs.ts";
|
|
20
|
+
|
|
21
|
+
type RuntimeSessionConnection = {
|
|
22
|
+
onMessage(value: unknown): void;
|
|
23
|
+
onClose(): void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type RuntimeSessionTransport = {
|
|
27
|
+
send(message: string): void;
|
|
28
|
+
close(code?: number, reason?: string): void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function runSessionSocketEffect(
|
|
32
|
+
state: RuntimeAppState,
|
|
33
|
+
runId: string,
|
|
34
|
+
socket: Socket.Socket,
|
|
35
|
+
): Effect.Effect<void, never, Scope.Scope> {
|
|
36
|
+
return Effect.gen(function* () {
|
|
37
|
+
const write = yield* socket.writer;
|
|
38
|
+
const transport: RuntimeSessionTransport = {
|
|
39
|
+
send(message) {
|
|
40
|
+
Effect.runFork(write(message).pipe(Effect.ignore));
|
|
41
|
+
},
|
|
42
|
+
close(code, reason) {
|
|
43
|
+
Effect.runFork(write(new Socket.CloseEvent(code, reason)).pipe(Effect.ignore));
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const run = state.store.runs.get(runId);
|
|
47
|
+
if (!run) {
|
|
48
|
+
transport.close(1008, `Unknown run ${runId}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const session = openRunSession(state, run, transport);
|
|
53
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => session.onClose()));
|
|
54
|
+
yield* socket.runRaw((raw) => Effect.sync(() => session.onMessage(raw))).pipe(Effect.ignore);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function openRunSession(
|
|
59
|
+
state: RuntimeAppState,
|
|
60
|
+
run: RunRecord,
|
|
61
|
+
socket: RuntimeSessionTransport,
|
|
62
|
+
): RuntimeSessionConnection {
|
|
63
|
+
let acknowledged = false;
|
|
64
|
+
let sentEvents = 0;
|
|
65
|
+
state.store.activeSessions += 1;
|
|
66
|
+
state.context.touch();
|
|
67
|
+
|
|
68
|
+
const sendBacklog = () => {
|
|
69
|
+
while (sentEvents < run.events.length) {
|
|
70
|
+
socket.send(JSON.stringify(sessionEvent(run.events[sentEvents++])));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const unsubscribe = subscribeRunEvents(run, () => {
|
|
75
|
+
if (acknowledged) sendBacklog();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
onMessage(value) {
|
|
80
|
+
state.context.touch();
|
|
81
|
+
const message = parseSessionMessage(value);
|
|
82
|
+
if (!message) return;
|
|
83
|
+
if (message.type === "hello") {
|
|
84
|
+
const unsupported = unsupportedSessionRequirements(message, run.operationDefinition);
|
|
85
|
+
if (unsupported.length > 0) {
|
|
86
|
+
failRun(
|
|
87
|
+
run,
|
|
88
|
+
new RuntimeHostRequestError({
|
|
89
|
+
message: `Host does not support required ${unsupported.join(", ")}`,
|
|
90
|
+
}),
|
|
91
|
+
state.store,
|
|
92
|
+
);
|
|
93
|
+
acknowledged = true;
|
|
94
|
+
sendBacklog();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
acknowledged = true;
|
|
98
|
+
sendHelloAck(socket, run);
|
|
99
|
+
sendBacklog();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (message.type === "heartbeat") {
|
|
103
|
+
socket.send(JSON.stringify({ type: "heartbeat.ack", at: new Date().toISOString() }));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (message.type === "run.cancel") {
|
|
107
|
+
failRun(run, new RuntimeRunCancelledError(), state.store);
|
|
108
|
+
acknowledged = true;
|
|
109
|
+
sendBacklog();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (message.type === "host.capability.closed") {
|
|
113
|
+
if (!message.id) return;
|
|
114
|
+
closeHostCapabilityResource(
|
|
115
|
+
state.store,
|
|
116
|
+
message.id,
|
|
117
|
+
message.error
|
|
118
|
+
? new RuntimeHostRequestError({
|
|
119
|
+
requestId: message.id,
|
|
120
|
+
hostCode: typeof message.error.code === "string" ? message.error.code : undefined,
|
|
121
|
+
message: message.error.message ?? "Host capability resource closed with an error",
|
|
122
|
+
})
|
|
123
|
+
: undefined,
|
|
124
|
+
);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (message.type === "response") {
|
|
128
|
+
if (!message.id) return;
|
|
129
|
+
const pending = state.store.hostResponses.get(message.id);
|
|
130
|
+
if (!pending) return;
|
|
131
|
+
state.store.hostResponses.delete(message.id);
|
|
132
|
+
clearPendingHostRequest(state.store, message.id);
|
|
133
|
+
if (message.error) {
|
|
134
|
+
pending.reject(new RuntimeHostRequestError({
|
|
135
|
+
requestId: message.id,
|
|
136
|
+
hostCode: typeof message.error.code === "string" ? message.error.code : undefined,
|
|
137
|
+
message: message.error.message ?? "Host request failed",
|
|
138
|
+
}));
|
|
139
|
+
} else {
|
|
140
|
+
pending.resolve(message.result ?? null);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
onClose() {
|
|
146
|
+
unsubscribe();
|
|
147
|
+
state.store.activeSessions = Math.max(0, state.store.activeSessions - 1);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function sendHelloAck(ws: RuntimeSessionTransport, run: { operation: string; operationDefinition?: RuntimeOperation }): void {
|
|
153
|
+
ws.send(JSON.stringify({
|
|
154
|
+
type: "hello.ack",
|
|
155
|
+
transportVersion: 1,
|
|
156
|
+
runtime: {
|
|
157
|
+
engineVersion: RIGKIT_ENGINE_VERSION,
|
|
158
|
+
runtimeVersion: RIGKIT_RUNTIME_VERSION,
|
|
159
|
+
protocolHash: RUNTIME_PROTOCOL_HASH,
|
|
160
|
+
},
|
|
161
|
+
operation: {
|
|
162
|
+
id: run.operation,
|
|
163
|
+
requiredHostCapabilities: run.operationDefinition?.requiredHostCapabilities ?? [],
|
|
164
|
+
requiredHostMethods: run.operationDefinition?.requiredHostMethods ?? [],
|
|
165
|
+
},
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function unsupportedSessionRequirements(message: SessionMessage, operation: RuntimeOperation | undefined): string[] {
|
|
170
|
+
const unsupported: string[] = [];
|
|
171
|
+
for (const requirement of operation?.requiredHostMethods ?? []) {
|
|
172
|
+
const hostMethod = sessionItems(message.hostMethods).find((item) => item.id === requirement.id);
|
|
173
|
+
if (!hostMethod || !supportsModes(hostMethod.modes, requirement.modes)) {
|
|
174
|
+
unsupported.push(`host method ${formatRequirement(requirement.id, requirement.modes)}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const requirement of operation?.requiredHostCapabilities ?? []) {
|
|
178
|
+
const capability = sessionItems(message.hostCapabilities).find((item) => item.id === requirement.id);
|
|
179
|
+
if (!capability || (requirement.schemaHash && capability.schemaHash !== requirement.schemaHash)) {
|
|
180
|
+
unsupported.push(requirement.schemaHash
|
|
181
|
+
? `host capability ${requirement.id}@${requirement.schemaHash}`
|
|
182
|
+
: `host capability ${requirement.id}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return unsupported;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sessionItems(value: unknown): Array<{ id: string; modes?: string[]; schemaHash?: string }> {
|
|
189
|
+
if (!Array.isArray(value)) return [];
|
|
190
|
+
return value.flatMap((item) => {
|
|
191
|
+
if (!isRecord(item) || typeof item.id !== "string") return [];
|
|
192
|
+
return [{
|
|
193
|
+
id: item.id,
|
|
194
|
+
modes: Array.isArray(item.modes) ? item.modes.filter((mode): mode is string => typeof mode === "string") : undefined,
|
|
195
|
+
schemaHash: typeof item.schemaHash === "string" ? item.schemaHash : undefined,
|
|
196
|
+
}];
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function supportsModes(hostModes: string[] | undefined, requiredModes: string[] | undefined): boolean {
|
|
201
|
+
if (!requiredModes?.length) return true;
|
|
202
|
+
const supported = new Set(hostModes ?? []);
|
|
203
|
+
return requiredModes.every((mode) => supported.has(mode));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function formatRequirement(id: string, modes: string[] | undefined): string {
|
|
207
|
+
return modes?.length ? `${id}:${modes.join("|")}` : id;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function sessionEvent(event: unknown): Record<string, unknown> {
|
|
211
|
+
if (isRecord(event) && event.type === "host.request") {
|
|
212
|
+
return {
|
|
213
|
+
type: "host.request",
|
|
214
|
+
id: typeof event.id === "string" ? event.id : event.requestId,
|
|
215
|
+
method: event.method,
|
|
216
|
+
params: event.params,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (isRecord(event) && event.type === "host.capability.request") {
|
|
220
|
+
return {
|
|
221
|
+
type: "host.capability.request",
|
|
222
|
+
id: typeof event.id === "string" ? event.id : event.requestId,
|
|
223
|
+
capability: event.capability,
|
|
224
|
+
params: event.params,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (isRecord(event) && typeof event.type === "string" && event.type.startsWith("run.")) return event;
|
|
228
|
+
return { type: "run.event", event };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
type SessionMessage = Record<string, unknown> & {
|
|
232
|
+
type: string;
|
|
233
|
+
id?: string;
|
|
234
|
+
result?: unknown;
|
|
235
|
+
error?: { code?: string; message?: string };
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
function parseSessionMessage(value: unknown): SessionMessage | undefined {
|
|
239
|
+
const text = typeof value === "string"
|
|
240
|
+
? value
|
|
241
|
+
: value instanceof Uint8Array
|
|
242
|
+
? new TextDecoder().decode(value)
|
|
243
|
+
: undefined;
|
|
244
|
+
if (text === undefined) return undefined;
|
|
245
|
+
try {
|
|
246
|
+
const parsed = JSON.parse(text);
|
|
247
|
+
if (!isRecord(parsed) || typeof parsed.type !== "string") return undefined;
|
|
248
|
+
if (parsed.type === "response" && typeof parsed.id !== "string") return undefined;
|
|
249
|
+
return parsed as SessionMessage;
|
|
250
|
+
} catch {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
256
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
257
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createStateStore,
|
|
3
|
+
type StateService,
|
|
4
|
+
type StateServiceOptions,
|
|
5
|
+
} from "@rigkit/engine";
|
|
6
|
+
|
|
7
|
+
export type RuntimeStateService = StateService;
|
|
8
|
+
export type RuntimeStateServiceOptions = StateServiceOptions;
|
|
9
|
+
|
|
10
|
+
export function createRuntimeStateService(options: RuntimeStateServiceOptions): RuntimeStateService {
|
|
11
|
+
return createStateStore(options);
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function readOrCreateToken(path: string): string {
|
|
5
|
+
if (existsSync(path)) return readFileSync(path, "utf8").trim();
|
|
6
|
+
|
|
7
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
8
|
+
const token = `rigkit_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
9
|
+
writeFileSync(path, `${token}\n`);
|
|
10
|
+
try {
|
|
11
|
+
chmodSync(path, 0o600);
|
|
12
|
+
} catch {
|
|
13
|
+
// Best effort on platforms without chmod support.
|
|
14
|
+
}
|
|
15
|
+
return token;
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { JsonValue } from "@rigkit/engine";
|
|
2
|
+
|
|
3
|
+
export type ServeRuntimeOptions = {
|
|
4
|
+
projectId: string;
|
|
5
|
+
projectDir: string;
|
|
6
|
+
configPath: string;
|
|
7
|
+
statePath?: string;
|
|
8
|
+
source?: JsonValue;
|
|
9
|
+
handlePath: string;
|
|
10
|
+
tokenPath: string;
|
|
11
|
+
token?: string;
|
|
12
|
+
host?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
idleMs?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type RuntimeServer = {
|
|
18
|
+
url: string;
|
|
19
|
+
token: string;
|
|
20
|
+
closed: Promise<void>;
|
|
21
|
+
stop(): void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type RuntimeContext = {
|
|
25
|
+
readonly projectId: string;
|
|
26
|
+
readonly projectDir: string;
|
|
27
|
+
readonly configPath: string;
|
|
28
|
+
readonly statePath?: string;
|
|
29
|
+
readonly source?: JsonValue;
|
|
30
|
+
readonly token: string;
|
|
31
|
+
readonly startedAt: string;
|
|
32
|
+
readonly getExpiresAt: () => string;
|
|
33
|
+
readonly touch: () => void;
|
|
34
|
+
readonly stop: () => void;
|
|
35
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const RIGKIT_RUNTIME_VERSION = "0.1.8";
|
package/src/version.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const RIGKIT_SDK_VERSION = "0.1.8";
|