@ricsam/r5d-worker 0.0.172 → 0.0.174
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/dist/cjs/package.json +1 -1
- package/dist/mjs/internal-r5dctl.cjs +1 -1
- package/dist/mjs/main.mjs +2 -2
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/personal/client.mjs +19 -8
- package/dist/mjs/personal/runtime.mjs +12 -1
- package/dist/mjs/personal/terminal-stream-protocol.mjs +66 -0
- package/dist/mjs/personal/terminal-stream.mjs +161 -0
- package/dist/mjs/runtime/client.mjs +58 -1
- package/dist/mjs/runtime/daemon.mjs +26 -3
- package/dist/mjs/runtime/executor.mjs +76 -4
- package/dist/mjs/runtime/protocol.mjs +15 -1
- package/dist/mjs/runtime/workspace/authority.mjs +78 -14
- package/dist/mjs/runtime/workspace/files.mjs +6 -3
- package/dist/types/personal/runtime.d.ts +29 -0
- package/dist/types/personal/terminal-stream-protocol.d.ts +132 -0
- package/dist/types/personal/terminal-stream.d.ts +31 -0
- package/dist/types/runtime/client.d.ts +18 -1
- package/dist/types/runtime/executor.d.ts +9 -2
- package/dist/types/runtime/protocol.d.ts +98 -0
- package/dist/types/runtime/releases/rpc-protocol.d.ts +136 -0
- package/dist/types/runtime/workspace/authority.d.ts +53 -5
- package/dist/types/runtime/workspace/contracts.d.ts +1 -1
- package/dist/types/runtime/workspace/files.d.ts +4 -0
- package/package.json +2 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { OperationEnvelope, OwnershipFence, RUNTIME_PROTOCOL, RuntimeHello, RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
|
|
3
3
|
const EXECUTOR_CAPABILITY = "host-executor-v1";
|
|
4
|
+
const EXECUTOR_STREAM_CAPABILITY = "host-stream-v1";
|
|
5
|
+
const STREAM_CHUNK_BYTES = 64 * 1024;
|
|
4
6
|
const MAX_FRAME_BYTES = 1024 * 1024;
|
|
5
7
|
const text = z.string().max(256 * 1024).refine((s) => !s.includes("\0"), "NUL is not allowed");
|
|
6
8
|
const absolutePath = z.string().min(1).max(4096).refine((s) => s.startsWith("/") && !s.includes("\0"), "Absolute POSIX path required");
|
|
@@ -54,7 +56,17 @@ const ExecutorCommand = z.discriminatedUnion("method", [
|
|
|
54
56
|
cols: z.number().int().min(1).max(1e3),
|
|
55
57
|
rows: z.number().int().min(1).max(1e3)
|
|
56
58
|
}).strict(),
|
|
57
|
-
z.object({ method: z.literal("cancel"), run: RunIdentity, actionId: RuntimeId }).strict()
|
|
59
|
+
z.object({ method: z.literal("cancel"), run: RunIdentity, actionId: RuntimeId }).strict(),
|
|
60
|
+
// Live terminal channel. Neither command has a durable action identity: a
|
|
61
|
+
// subscription is replayed from an offset, and live input is keystrokes the
|
|
62
|
+
// person can simply type again, exactly as over ssh.
|
|
63
|
+
z.object({ method: z.literal("subscribe"), run: RunIdentity, offset: z.number().int().nonnegative().default(0) }).strict(),
|
|
64
|
+
z.object({
|
|
65
|
+
method: z.literal("input"),
|
|
66
|
+
run: RunIdentity,
|
|
67
|
+
data: text.optional(),
|
|
68
|
+
resize: z.object({ cols: z.number().int().min(1).max(1e3), rows: z.number().int().min(1).max(1e3) }).strict().optional()
|
|
69
|
+
}).strict().refine((value) => value.data !== void 0 || value.resize !== void 0, "Input carries data or a size")
|
|
58
70
|
]);
|
|
59
71
|
const ExecutorRequest = z.object({
|
|
60
72
|
protocol: z.literal(RUNTIME_PROTOCOL),
|
|
@@ -124,6 +136,7 @@ export {
|
|
|
124
136
|
AdapterConfig,
|
|
125
137
|
AdapterGrant,
|
|
126
138
|
EXECUTOR_CAPABILITY,
|
|
139
|
+
EXECUTOR_STREAM_CAPABILITY,
|
|
127
140
|
ExecutorCommand,
|
|
128
141
|
ExecutorConfig,
|
|
129
142
|
ExecutorCredential,
|
|
@@ -133,6 +146,7 @@ export {
|
|
|
133
146
|
MAX_FRAME_BYTES,
|
|
134
147
|
Principal,
|
|
135
148
|
RunIdentity,
|
|
149
|
+
STREAM_CHUNK_BYTES,
|
|
136
150
|
ShellPayload,
|
|
137
151
|
isDefiniteStartRejection,
|
|
138
152
|
isStartNonAdmissionCode
|
|
@@ -50,6 +50,7 @@ class WorkspaceAuthority {
|
|
|
50
50
|
closed = false;
|
|
51
51
|
closeTask;
|
|
52
52
|
actions = /* @__PURE__ */ new Set();
|
|
53
|
+
subscriptions = /* @__PURE__ */ new Set();
|
|
53
54
|
repositoryInitializations = /* @__PURE__ */ new Map();
|
|
54
55
|
outerRepository;
|
|
55
56
|
pending = 0;
|
|
@@ -263,8 +264,13 @@ class WorkspaceAuthority {
|
|
|
263
264
|
const result = [];
|
|
264
265
|
for (const [operationId, metadata] of Object.entries(b.state.runs)) {
|
|
265
266
|
if (metadata.state === "rejected_capacity" || (metadata.sessionId ?? b.config.sessionId) !== identity.sessionId) continue;
|
|
266
|
-
|
|
267
|
-
|
|
267
|
+
try {
|
|
268
|
+
const poll = await this.poll(identity, { userId: identity.userId, sessionId: identity.sessionId, operationId, fence: route.workerFence }, 0, 0, 1);
|
|
269
|
+
result.push({ ...metadata, ...poll.receipt, workerLabel: route.workerFence.resourceId });
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (!(error instanceof ExecutorError && error.code === "run_not_found")) throw error;
|
|
272
|
+
result.push({ ...metadata, operationId, userId: identity.userId, sessionId: identity.sessionId, state: "unknown", pid: null, pty: false, lane: "general", stdoutBytes: 0, stderrBytes: 0, stdinClosed: true, exitCode: null, signal: null, reason: "run_not_found", workerLabel: route.workerFence.resourceId });
|
|
273
|
+
}
|
|
268
274
|
}
|
|
269
275
|
return result;
|
|
270
276
|
}
|
|
@@ -405,8 +411,20 @@ class WorkspaceAuthority {
|
|
|
405
411
|
return { head, treeHash, files: changes, ...patch === void 0 ? {} : { patch } };
|
|
406
412
|
});
|
|
407
413
|
}
|
|
414
|
+
/** Bench state is one in-memory object with several writers on different
|
|
415
|
+
* queues (checkout work, run receipts). Writes of its file are chained so
|
|
416
|
+
* they never overlap and the write that lands last always serializes the
|
|
417
|
+
* newest state; the chain covers only the write itself, never the work
|
|
418
|
+
* around it, so a run receipt does not wait for a publication. */
|
|
408
419
|
save(b) {
|
|
409
|
-
|
|
420
|
+
const write = (b.saving ?? Promise.resolve()).catch(() => {
|
|
421
|
+
}).then(() => durableJson(path.join(b.directory, "state.json"), b.state));
|
|
422
|
+
b.saving = write;
|
|
423
|
+
void write.finally(() => {
|
|
424
|
+
if (b.saving === write) b.saving = void 0;
|
|
425
|
+
}).catch(() => {
|
|
426
|
+
});
|
|
427
|
+
return write;
|
|
410
428
|
}
|
|
411
429
|
async owned(admitted = false) {
|
|
412
430
|
if (this.closing && !admitted) throw new WorkspaceError("authority_closed", "Workspace authority is closing");
|
|
@@ -1845,7 +1863,12 @@ class WorkspaceAuthority {
|
|
|
1845
1863
|
}
|
|
1846
1864
|
const nonmutating = Object.keys(requestedEnv).length === 0 && payload.credentials.length === 0 && b.config.nonmutatingArgv.some((argv) => canonicalJson(argv) === canonicalJson(payload.argv));
|
|
1847
1865
|
b.state.runs[operation.operationId] = { artifactEnvironment: true, sessionId: identity.sessionId, payloadHash: hash, mutating: !nonmutating, state: "unknown", argv: payload.argv, cwd: payload.cwd, startedAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1848
|
-
|
|
1866
|
+
try {
|
|
1867
|
+
await this.save(b);
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
delete b.state.runs[operation.operationId];
|
|
1870
|
+
throw error;
|
|
1871
|
+
}
|
|
1849
1872
|
for (let attempt = 0; ; attempt++) {
|
|
1850
1873
|
try {
|
|
1851
1874
|
const receipt = await route.client.start(operation);
|
|
@@ -1895,19 +1918,59 @@ class WorkspaceAuthority {
|
|
|
1895
1918
|
return this.action(async () => {
|
|
1896
1919
|
const { b, route } = await this.runRoute(identity, run);
|
|
1897
1920
|
const result = await route.client.poll({ ...run, fence: route.workerFence }, stdoutOffset, stderrOffset, maxBytes);
|
|
1898
|
-
await this.
|
|
1899
|
-
b,
|
|
1900
|
-
async () => {
|
|
1901
|
-
b.state.runs[run.operationId].state = result.receipt.state;
|
|
1902
|
-
b.state.runs[run.operationId].updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1903
|
-
if (["completed", "cancelled"].includes(result.receipt.state)) b.state.runs[run.operationId].completedAt ??= (/* @__PURE__ */ new Date()).toISOString();
|
|
1904
|
-
await this.save(b);
|
|
1905
|
-
},
|
|
1906
|
-
true
|
|
1907
|
-
);
|
|
1921
|
+
await this.recordRunState(b, run.operationId, result.receipt.state);
|
|
1908
1922
|
return result;
|
|
1909
1923
|
});
|
|
1910
1924
|
}
|
|
1925
|
+
/** Run liveness is process state observed from the executor, not a workspace
|
|
1926
|
+
* mutation: it is recorded on its own short queue, never behind the checkout's
|
|
1927
|
+
* hydration, commit or publication work. A crash between two writers loses
|
|
1928
|
+
* nothing lasting — `refresh` re-reads every open run from the executor. */
|
|
1929
|
+
recordRunState(b, operationId, state) {
|
|
1930
|
+
return this.serialKey(
|
|
1931
|
+
`runs:${b.config.id}`,
|
|
1932
|
+
async () => {
|
|
1933
|
+
const run = b.state.runs[operationId];
|
|
1934
|
+
if (!run) return;
|
|
1935
|
+
run.state = state;
|
|
1936
|
+
run.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1937
|
+
if (["completed", "cancelled"].includes(state)) run.completedAt ??= (/* @__PURE__ */ new Date()).toISOString();
|
|
1938
|
+
await this.save(b);
|
|
1939
|
+
},
|
|
1940
|
+
true
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
/** Live terminal channel. The subscription is not an admitted action: it
|
|
1944
|
+
* lives as long as its transport, outlives the request budget, and is torn
|
|
1945
|
+
* down on close. Its exit frame is the only durable-state effect. */
|
|
1946
|
+
async subscribe(identity, run, offset, handlers) {
|
|
1947
|
+
if (this.closing) throw new WorkspaceError("authority_closed", "Workspace authority is closing");
|
|
1948
|
+
const { b, route } = await this.runRoute(identity, run);
|
|
1949
|
+
if (!route.client.subscribe) throw new WorkspaceError("stream_unavailable", "Executor does not advertise terminal streaming", true);
|
|
1950
|
+
const holder = {};
|
|
1951
|
+
holder.subscription = await route.client.subscribe({ ...run, fence: route.workerFence }, offset, {
|
|
1952
|
+
frame: (frame) => {
|
|
1953
|
+
if (frame.event === "exit") void this.recordRunState(b, run.operationId, frame.receipt.state).catch(() => {
|
|
1954
|
+
});
|
|
1955
|
+
handlers.frame(frame);
|
|
1956
|
+
},
|
|
1957
|
+
close: (error) => {
|
|
1958
|
+
if (holder.subscription) this.subscriptions.delete(holder.subscription);
|
|
1959
|
+
handlers.close(error);
|
|
1960
|
+
}
|
|
1961
|
+
});
|
|
1962
|
+
this.subscriptions.add(holder.subscription);
|
|
1963
|
+
return holder.subscription;
|
|
1964
|
+
}
|
|
1965
|
+
/** Live PTY input: fenced and owned like a write, but with no action receipt
|
|
1966
|
+
* and no queue in front of it. */
|
|
1967
|
+
async streamInput(identity, run, data, resize) {
|
|
1968
|
+
return this.action(async () => {
|
|
1969
|
+
const { route } = await this.runRoute(identity, run);
|
|
1970
|
+
if (!route.client.input) throw new WorkspaceError("stream_unavailable", "Executor does not advertise terminal streaming", true);
|
|
1971
|
+
return route.client.input({ ...run, fence: route.workerFence }, data, resize);
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1911
1974
|
async actionOutcome(b, actionId, invoke) {
|
|
1912
1975
|
try {
|
|
1913
1976
|
const result = await invoke();
|
|
@@ -1963,6 +2026,7 @@ class WorkspaceAuthority {
|
|
|
1963
2026
|
return task;
|
|
1964
2027
|
}
|
|
1965
2028
|
async finishClose() {
|
|
2029
|
+
for (const subscription of this.subscriptions) subscription.close();
|
|
1966
2030
|
await Promise.allSettled([...this.actions]);
|
|
1967
2031
|
await Promise.allSettled([...this.queues.values()]);
|
|
1968
2032
|
for (const b of this.benches.values()) await this.refresh(b);
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
5
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { safeTreePath, STORAGE_LIMITS } from "./storage-wire.mjs";
|
|
7
7
|
import { WorkspaceError } from "./contracts.mjs";
|
|
8
8
|
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
@@ -84,14 +84,17 @@ async function ensureAuthorityGitRepositoryLayout(repo) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
async function durableJson(file, value) {
|
|
87
|
-
const temporary = `${file}.next`;
|
|
87
|
+
const temporary = `${file}.${randomBytes(6).toString("hex")}.next`;
|
|
88
88
|
const handle = await fs.open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
89
89
|
try {
|
|
90
90
|
await handle.writeFile(JSON.stringify(value));
|
|
91
91
|
await handle.sync();
|
|
92
|
-
}
|
|
92
|
+
} catch (error) {
|
|
93
93
|
await handle.close();
|
|
94
|
+
await fs.rm(temporary, { force: true });
|
|
95
|
+
throw error;
|
|
94
96
|
}
|
|
97
|
+
await handle.close();
|
|
95
98
|
await fs.rename(temporary, file);
|
|
96
99
|
const dir = await fs.open(path.dirname(file), constants.O_RDONLY);
|
|
97
100
|
try {
|
|
@@ -38,6 +38,19 @@ export declare const PersonalWorkerGrant: z.ZodObject<{
|
|
|
38
38
|
}, z.core.$strict>;
|
|
39
39
|
leaseExpiresAt: z.ZodCoercedNumber<unknown>;
|
|
40
40
|
}, z.core.$loose>;
|
|
41
|
+
declare const Workbench: z.ZodObject<{
|
|
42
|
+
id: z.ZodString;
|
|
43
|
+
repositoryId: z.ZodString;
|
|
44
|
+
branch: z.ZodString;
|
|
45
|
+
sessionId: z.ZodString;
|
|
46
|
+
rootProfile: z.ZodDefault<z.ZodEnum<{
|
|
47
|
+
account: "account";
|
|
48
|
+
project: "project";
|
|
49
|
+
}>>;
|
|
50
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
51
|
+
projectName: z.ZodOptional<z.ZodString>;
|
|
52
|
+
baseCommitHash: z.ZodOptional<z.ZodString>;
|
|
53
|
+
}, z.core.$strict>;
|
|
41
54
|
export declare const PersonalWorkspaceRequest: z.ZodObject<{
|
|
42
55
|
protocol: z.ZodLiteral<1>;
|
|
43
56
|
userId: z.ZodString;
|
|
@@ -151,5 +164,21 @@ export declare function openPersonalWorkerRuntime(options: {
|
|
|
151
164
|
unchanged?: boolean;
|
|
152
165
|
error?: string;
|
|
153
166
|
}[]>;
|
|
167
|
+
subscribeTerminal: (input: {
|
|
168
|
+
sessionId: string;
|
|
169
|
+
workbench: z.infer<typeof Workbench>;
|
|
170
|
+
operationId: string;
|
|
171
|
+
offset: number;
|
|
172
|
+
}, handlers: Parameters<WorkspaceAuthority["subscribe"]>[3]) => Promise<import("../runtime/client").ExecutorSubscription>;
|
|
173
|
+
terminalInput: (input: {
|
|
174
|
+
sessionId: string;
|
|
175
|
+
operationId: string;
|
|
176
|
+
data?: string;
|
|
177
|
+
resize?: {
|
|
178
|
+
cols: number;
|
|
179
|
+
rows: number;
|
|
180
|
+
};
|
|
181
|
+
}) => Promise<import("../runtime/protocol").InputResult>;
|
|
154
182
|
close(): Promise<void>;
|
|
155
183
|
}>;
|
|
184
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Live terminal channel between a personal worker and the web runtime, and
|
|
3
|
+
* between the web runtime and a browser. Frames are JSON text messages.
|
|
4
|
+
*
|
|
5
|
+
* Nothing on this channel is durable: a lost frame is retyped by the person
|
|
6
|
+
* at the keyboard or re-read from the durable offset by the browser. That is
|
|
7
|
+
* what keeps a keystroke off the relay ledger and out of every workspace queue. */
|
|
8
|
+
export declare const TERMINAL_STREAM_CAPABILITY = "terminal-stream-v1";
|
|
9
|
+
export declare const STREAM_INPUT_MAX_CHARS = 100000;
|
|
10
|
+
export declare const STREAM_HEARTBEAT_MS = 25000;
|
|
11
|
+
export declare const StreamWorkbench: z.ZodObject<{
|
|
12
|
+
id: z.ZodString;
|
|
13
|
+
repositoryId: z.ZodString;
|
|
14
|
+
branch: z.ZodString;
|
|
15
|
+
sessionId: z.ZodString;
|
|
16
|
+
rootProfile: z.ZodDefault<z.ZodEnum<{
|
|
17
|
+
account: "account";
|
|
18
|
+
project: "project";
|
|
19
|
+
}>>;
|
|
20
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
21
|
+
projectName: z.ZodOptional<z.ZodString>;
|
|
22
|
+
baseCommitHash: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strict>;
|
|
24
|
+
export type StreamWorkbench = z.output<typeof StreamWorkbench>;
|
|
25
|
+
/** Web runtime → worker. */
|
|
26
|
+
export declare const ServerToWorkerFrame: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
27
|
+
type: z.ZodLiteral<"attach">;
|
|
28
|
+
streamId: z.ZodString;
|
|
29
|
+
sessionId: z.ZodString;
|
|
30
|
+
operationId: z.ZodString;
|
|
31
|
+
offset: z.ZodNumber;
|
|
32
|
+
workbench: z.ZodObject<{
|
|
33
|
+
id: z.ZodString;
|
|
34
|
+
repositoryId: z.ZodString;
|
|
35
|
+
branch: z.ZodString;
|
|
36
|
+
sessionId: z.ZodString;
|
|
37
|
+
rootProfile: z.ZodDefault<z.ZodEnum<{
|
|
38
|
+
account: "account";
|
|
39
|
+
project: "project";
|
|
40
|
+
}>>;
|
|
41
|
+
namespace: z.ZodOptional<z.ZodString>;
|
|
42
|
+
projectName: z.ZodOptional<z.ZodString>;
|
|
43
|
+
baseCommitHash: z.ZodOptional<z.ZodString>;
|
|
44
|
+
}, z.core.$strict>;
|
|
45
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
46
|
+
type: z.ZodLiteral<"input">;
|
|
47
|
+
streamId: z.ZodString;
|
|
48
|
+
data: z.ZodOptional<z.ZodString>;
|
|
49
|
+
resize: z.ZodOptional<z.ZodObject<{
|
|
50
|
+
cols: z.ZodNumber;
|
|
51
|
+
rows: z.ZodNumber;
|
|
52
|
+
}, z.core.$strict>>;
|
|
53
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
54
|
+
type: z.ZodLiteral<"detach">;
|
|
55
|
+
streamId: z.ZodString;
|
|
56
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
57
|
+
type: z.ZodLiteral<"ping">;
|
|
58
|
+
}, z.core.$strict>], "type">;
|
|
59
|
+
export type ServerToWorkerFrame = z.infer<typeof ServerToWorkerFrame>;
|
|
60
|
+
/** Worker → web runtime. */
|
|
61
|
+
export declare const WorkerToServerFrame: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
62
|
+
type: z.ZodLiteral<"hello">;
|
|
63
|
+
version: z.ZodString;
|
|
64
|
+
capabilities: z.ZodArray<z.ZodString>;
|
|
65
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
66
|
+
type: z.ZodLiteral<"attached">;
|
|
67
|
+
streamId: z.ZodString;
|
|
68
|
+
offset: z.ZodNumber;
|
|
69
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
70
|
+
type: z.ZodLiteral<"output">;
|
|
71
|
+
streamId: z.ZodString;
|
|
72
|
+
offset: z.ZodNumber;
|
|
73
|
+
base64: z.ZodString;
|
|
74
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
75
|
+
type: z.ZodLiteral<"exit">;
|
|
76
|
+
streamId: z.ZodString;
|
|
77
|
+
exitCode: z.ZodNullable<z.ZodNumber>;
|
|
78
|
+
signal: z.ZodNullable<z.ZodString>;
|
|
79
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
80
|
+
type: z.ZodLiteral<"error">;
|
|
81
|
+
streamId: z.ZodString;
|
|
82
|
+
code: z.ZodString;
|
|
83
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
84
|
+
type: z.ZodLiteral<"detached">;
|
|
85
|
+
streamId: z.ZodString;
|
|
86
|
+
code: z.ZodOptional<z.ZodString>;
|
|
87
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
88
|
+
type: z.ZodLiteral<"pong">;
|
|
89
|
+
}, z.core.$strict>], "type">;
|
|
90
|
+
export type WorkerToServerFrame = z.infer<typeof WorkerToServerFrame>;
|
|
91
|
+
/** Browser → web runtime. The PTY, its offset and the worker are fixed by the
|
|
92
|
+
* socket's URL, so the browser only ever sends what a keyboard produces. */
|
|
93
|
+
export declare const BrowserToServerFrame: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
94
|
+
type: z.ZodLiteral<"input">;
|
|
95
|
+
data: z.ZodString;
|
|
96
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
97
|
+
type: z.ZodLiteral<"resize">;
|
|
98
|
+
cols: z.ZodNumber;
|
|
99
|
+
rows: z.ZodNumber;
|
|
100
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
101
|
+
type: z.ZodLiteral<"ping">;
|
|
102
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
103
|
+
type: z.ZodLiteral<"pong">;
|
|
104
|
+
}, z.core.$strict>], "type">;
|
|
105
|
+
export type BrowserToServerFrame = z.infer<typeof BrowserToServerFrame>;
|
|
106
|
+
/** Web runtime → browser. `offset` is the durable stdout offset after the
|
|
107
|
+
* frame's bytes, the same number the polling transport calls its cursor. */
|
|
108
|
+
export type ServerToBrowserFrame = {
|
|
109
|
+
type: "attached";
|
|
110
|
+
offset: number;
|
|
111
|
+
} | {
|
|
112
|
+
type: "output";
|
|
113
|
+
offset: number;
|
|
114
|
+
base64: string;
|
|
115
|
+
} | {
|
|
116
|
+
type: "exit";
|
|
117
|
+
exitCode: number | null;
|
|
118
|
+
signal: string | null;
|
|
119
|
+
} | {
|
|
120
|
+
type: "error";
|
|
121
|
+
code: string;
|
|
122
|
+
} | {
|
|
123
|
+
type: "unavailable";
|
|
124
|
+
reason?: string;
|
|
125
|
+
} | {
|
|
126
|
+
type: "ping";
|
|
127
|
+
} | {
|
|
128
|
+
type: "pong";
|
|
129
|
+
};
|
|
130
|
+
/** Number of leading bytes that end on a UTF-8 sequence boundary. A split
|
|
131
|
+
* suffix is carried into the next frame so offsets never cut a character. */
|
|
132
|
+
export declare function completeUtf8Prefix(bytes: Uint8Array): number;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PersonalWorkerRuntime } from "./runtime";
|
|
2
|
+
/** The subset of a WebSocket this client drives; Bun's global client fits it
|
|
3
|
+
* and tests substitute a scripted one. */
|
|
4
|
+
export type StreamSocket = {
|
|
5
|
+
onopen: ((event?: unknown) => void) | null;
|
|
6
|
+
onmessage: ((event: {
|
|
7
|
+
data: unknown;
|
|
8
|
+
}) => void) | null;
|
|
9
|
+
onclose: ((event?: unknown) => void) | null;
|
|
10
|
+
onerror: ((event?: unknown) => void) | null;
|
|
11
|
+
send(data: string): void;
|
|
12
|
+
close(code?: number, reason?: string): void;
|
|
13
|
+
};
|
|
14
|
+
export type TerminalStreamRuntime = Pick<PersonalWorkerRuntime, "subscribeTerminal" | "terminalInput">;
|
|
15
|
+
export type TerminalStreamOptions = {
|
|
16
|
+
/** ws(s) URL of this worker's stream endpoint, instance included. */
|
|
17
|
+
url: string;
|
|
18
|
+
credential: string;
|
|
19
|
+
version: string;
|
|
20
|
+
runtime: TerminalStreamRuntime;
|
|
21
|
+
connect?: (url: string, headers: Record<string, string>) => StreamSocket;
|
|
22
|
+
log?: (line: string) => void;
|
|
23
|
+
/** Reconnect delays; the last one repeats. */
|
|
24
|
+
backoffMs?: number[];
|
|
25
|
+
};
|
|
26
|
+
/** One long-lived socket per worker carries every attached shell. The socket
|
|
27
|
+
* is the transport only: PTYs, their receipts and their bytes stay in the
|
|
28
|
+
* executor, so a reconnect re-attaches at the browser's durable offset. */
|
|
29
|
+
export declare function startTerminalStream(options: TerminalStreamOptions): {
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
};
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { OperationEnvelope, OwnershipFence } from "@ricsam/r5d-api/runtime-protocol";
|
|
2
|
-
import { AdapterConfig, type ActionReceipt, type AdapterGrant, type AdapterRegistration, type ExecutorCommand, type PollResult, type RunIdentity, type RunReceipt } from "./protocol";
|
|
2
|
+
import { AdapterConfig, ExecutorError, type ActionReceipt, type AdapterGrant, type AdapterRegistration, type ExecutorCommand, type InputResult, type PollResult, type RunIdentity, type RunReceipt, type StreamFrame, type SubscribeResult } from "./protocol";
|
|
3
|
+
export type ExecutorSubscription = SubscribeResult & {
|
|
4
|
+
close(): void;
|
|
5
|
+
};
|
|
6
|
+
export type ExecutorStreamHandlers = {
|
|
7
|
+
frame(frame: StreamFrame): void;
|
|
8
|
+
/** Called once, after the last frame, with the transport's failure if any. */
|
|
9
|
+
close(error?: ExecutorError): void;
|
|
10
|
+
};
|
|
3
11
|
/** Stateless one-request connections: disconnect/timeout never implies the operation did not happen. */
|
|
4
12
|
export declare class HostExecutorClient {
|
|
5
13
|
readonly config: ReturnType<typeof AdapterConfig.parse>;
|
|
6
14
|
constructor(config: AdapterConfig);
|
|
15
|
+
private frame;
|
|
16
|
+
/** Keeps its socket open: the admission reply resolves the promise, then every
|
|
17
|
+
* newline-delimited frame reaches `handlers.frame` until either side closes. */
|
|
18
|
+
subscribe(run: RunIdentity, offset: number, handlers: ExecutorStreamHandlers): Promise<ExecutorSubscription>;
|
|
7
19
|
request<T = unknown>(command: ExecutorCommand): Promise<T>;
|
|
8
20
|
status(): Promise<unknown>;
|
|
9
21
|
registerAdapter(grant: AdapterGrant): Promise<AdapterRegistration>;
|
|
@@ -14,4 +26,9 @@ export declare class HostExecutorClient {
|
|
|
14
26
|
write(run: RunIdentity, actionId: string, data: string, eof?: boolean): Promise<ActionReceipt>;
|
|
15
27
|
resize(run: RunIdentity, actionId: string, cols: number, rows: number): Promise<ActionReceipt>;
|
|
16
28
|
cancel(run: RunIdentity, actionId: string): Promise<ActionReceipt>;
|
|
29
|
+
/** Live PTY input without a durable action: nothing to replay, nothing to poison. */
|
|
30
|
+
input(run: RunIdentity, data?: string, resize?: {
|
|
31
|
+
cols: number;
|
|
32
|
+
rows: number;
|
|
33
|
+
}): Promise<InputResult>;
|
|
17
34
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RuntimeHello } from "@ricsam/r5d-api/runtime-protocol";
|
|
2
|
-
import { ExecutorConfig, type ResolvedConfig } from "./protocol";
|
|
2
|
+
import { ExecutorConfig, type ResolvedConfig, type StreamSink } from "./protocol";
|
|
3
3
|
import { ExecutorStore } from "./storage";
|
|
4
4
|
type PtyModule = typeof import("node-pty");
|
|
5
5
|
export declare const tokenHash: (token: string) => string;
|
|
@@ -11,6 +11,7 @@ export declare class HostExecutor {
|
|
|
11
11
|
private readonly adapters;
|
|
12
12
|
readonly hello: RuntimeHello;
|
|
13
13
|
private readonly active;
|
|
14
|
+
private readonly subscribers;
|
|
14
15
|
private readonly leaseTimer;
|
|
15
16
|
private closing;
|
|
16
17
|
private poisoned;
|
|
@@ -19,8 +20,14 @@ export declare class HostExecutor {
|
|
|
19
20
|
/** Authority is synchronous and authenticated; never queue lease renewal behind a blocked OS write.
|
|
20
21
|
* Resource commands retain one ordered admission/action boundary. Commands already admitted may
|
|
21
22
|
* complete after promotion; queued commands and yielded admission hashes must recheck the fence.
|
|
23
|
+
* The live terminal channel (subscribe, input) carries no durable action and
|
|
24
|
+
* never waits behind that boundary: a keystroke is not queued behind a spawn.
|
|
22
25
|
*/
|
|
23
|
-
handle(input: unknown): Promise<unknown>;
|
|
26
|
+
handle(input: unknown, sink?: StreamSink): Promise<unknown>;
|
|
27
|
+
/** Drops a subscription whose transport closed. Idempotent. */
|
|
28
|
+
unsubscribe(sink: StreamSink): void;
|
|
29
|
+
private notify;
|
|
30
|
+
private pump;
|
|
24
31
|
private authenticate;
|
|
25
32
|
private authority;
|
|
26
33
|
private workerFence;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const EXECUTOR_CAPABILITY = "host-executor-v1";
|
|
3
|
+
/** Live terminal channel: a kept-open subscription socket plus fenced,
|
|
4
|
+
* receipt-free input. Advertised by executors that stream. */
|
|
5
|
+
export declare const EXECUTOR_STREAM_CAPABILITY = "host-stream-v1";
|
|
6
|
+
/** Longest single output frame on a subscription socket. */
|
|
7
|
+
export declare const STREAM_CHUNK_BYTES: number;
|
|
3
8
|
export declare const MAX_FRAME_BYTES: number;
|
|
4
9
|
export declare const Lane: z.ZodEnum<{
|
|
5
10
|
general: "general";
|
|
@@ -248,6 +253,40 @@ export declare const ExecutorCommand: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
248
253
|
}, z.core.$strict>;
|
|
249
254
|
}, z.core.$strict>;
|
|
250
255
|
actionId: z.ZodString;
|
|
256
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
257
|
+
method: z.ZodLiteral<"subscribe">;
|
|
258
|
+
run: z.ZodObject<{
|
|
259
|
+
operationId: z.ZodString;
|
|
260
|
+
userId: z.ZodString;
|
|
261
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
262
|
+
fence: z.ZodObject<{
|
|
263
|
+
installationId: z.ZodString;
|
|
264
|
+
resourceType: z.ZodString;
|
|
265
|
+
resourceId: z.ZodString;
|
|
266
|
+
ownerId: z.ZodString;
|
|
267
|
+
epoch: z.ZodNumber;
|
|
268
|
+
}, z.core.$strict>;
|
|
269
|
+
}, z.core.$strict>;
|
|
270
|
+
offset: z.ZodDefault<z.ZodNumber>;
|
|
271
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
272
|
+
method: z.ZodLiteral<"input">;
|
|
273
|
+
run: z.ZodObject<{
|
|
274
|
+
operationId: z.ZodString;
|
|
275
|
+
userId: z.ZodString;
|
|
276
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
277
|
+
fence: z.ZodObject<{
|
|
278
|
+
installationId: z.ZodString;
|
|
279
|
+
resourceType: z.ZodString;
|
|
280
|
+
resourceId: z.ZodString;
|
|
281
|
+
ownerId: z.ZodString;
|
|
282
|
+
epoch: z.ZodNumber;
|
|
283
|
+
}, z.core.$strict>;
|
|
284
|
+
}, z.core.$strict>;
|
|
285
|
+
data: z.ZodOptional<z.ZodString>;
|
|
286
|
+
resize: z.ZodOptional<z.ZodObject<{
|
|
287
|
+
cols: z.ZodNumber;
|
|
288
|
+
rows: z.ZodNumber;
|
|
289
|
+
}, z.core.$strict>>;
|
|
251
290
|
}, z.core.$strict>], "method">;
|
|
252
291
|
export type ExecutorCommand = z.input<typeof ExecutorCommand>;
|
|
253
292
|
export declare const ExecutorRequest: z.ZodObject<{
|
|
@@ -419,6 +458,40 @@ export declare const ExecutorRequest: z.ZodObject<{
|
|
|
419
458
|
}, z.core.$strict>;
|
|
420
459
|
}, z.core.$strict>;
|
|
421
460
|
actionId: z.ZodString;
|
|
461
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
462
|
+
method: z.ZodLiteral<"subscribe">;
|
|
463
|
+
run: z.ZodObject<{
|
|
464
|
+
operationId: z.ZodString;
|
|
465
|
+
userId: z.ZodString;
|
|
466
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
467
|
+
fence: z.ZodObject<{
|
|
468
|
+
installationId: z.ZodString;
|
|
469
|
+
resourceType: z.ZodString;
|
|
470
|
+
resourceId: z.ZodString;
|
|
471
|
+
ownerId: z.ZodString;
|
|
472
|
+
epoch: z.ZodNumber;
|
|
473
|
+
}, z.core.$strict>;
|
|
474
|
+
}, z.core.$strict>;
|
|
475
|
+
offset: z.ZodDefault<z.ZodNumber>;
|
|
476
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
477
|
+
method: z.ZodLiteral<"input">;
|
|
478
|
+
run: z.ZodObject<{
|
|
479
|
+
operationId: z.ZodString;
|
|
480
|
+
userId: z.ZodString;
|
|
481
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
482
|
+
fence: z.ZodObject<{
|
|
483
|
+
installationId: z.ZodString;
|
|
484
|
+
resourceType: z.ZodString;
|
|
485
|
+
resourceId: z.ZodString;
|
|
486
|
+
ownerId: z.ZodString;
|
|
487
|
+
epoch: z.ZodNumber;
|
|
488
|
+
}, z.core.$strict>;
|
|
489
|
+
}, z.core.$strict>;
|
|
490
|
+
data: z.ZodOptional<z.ZodString>;
|
|
491
|
+
resize: z.ZodOptional<z.ZodObject<{
|
|
492
|
+
cols: z.ZodNumber;
|
|
493
|
+
rows: z.ZodNumber;
|
|
494
|
+
}, z.core.$strict>>;
|
|
422
495
|
}, z.core.$strict>], "method">;
|
|
423
496
|
}, z.core.$strict>;
|
|
424
497
|
export declare const ExecutorCredential: z.ZodObject<{
|
|
@@ -521,6 +594,31 @@ export type ActionReceipt = {
|
|
|
521
594
|
actionId: string;
|
|
522
595
|
state: "accepted" | "completed" | "unknown";
|
|
523
596
|
};
|
|
597
|
+
/** Frames written after a subscription's admission reply, one per line. `offset`
|
|
598
|
+
* is the durable stdout offset after the frame's bytes. */
|
|
599
|
+
export type StreamFrame = {
|
|
600
|
+
event: "output";
|
|
601
|
+
offset: number;
|
|
602
|
+
base64: string;
|
|
603
|
+
} | {
|
|
604
|
+
event: "exit";
|
|
605
|
+
receipt: RunReceipt;
|
|
606
|
+
};
|
|
607
|
+
export type SubscribeResult = {
|
|
608
|
+
receipt: RunReceipt;
|
|
609
|
+
offset: number;
|
|
610
|
+
};
|
|
611
|
+
export type InputResult = {
|
|
612
|
+
ok: true;
|
|
613
|
+
};
|
|
614
|
+
/** Transport a subscription pushes into; the daemon binds it to one socket. */
|
|
615
|
+
export type StreamSink = {
|
|
616
|
+
/** Resolves once the admission reply has been written; frames wait for it. */
|
|
617
|
+
ready: Promise<void>;
|
|
618
|
+
/** Resolves once the frame is flushed to the transport (backpressure). */
|
|
619
|
+
send(frame: StreamFrame): Promise<void>;
|
|
620
|
+
end(): void;
|
|
621
|
+
};
|
|
524
622
|
export type PollResult = {
|
|
525
623
|
receipt: RunReceipt;
|
|
526
624
|
stdout: {
|