@rynx-ai/daemon 0.1.9 → 0.1.10-beta.2
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/app-browser-host-supervisor.d.ts +97 -0
- package/dist/app-browser-host-supervisor.js +529 -0
- package/dist/browser-artifact-management.d.ts +20 -0
- package/dist/browser-artifact-management.js +61 -0
- package/dist/chrome-for-testing-store.js +1 -1
- package/dist/chrome-inspection-gateway.d.ts +58 -0
- package/dist/chrome-inspection-gateway.js +806 -0
- package/dist/chrome-inspection-manager.d.ts +94 -0
- package/dist/chrome-inspection-manager.js +573 -0
- package/dist/cli.js +13 -2
- package/dist/control-client.d.ts +9 -0
- package/dist/control-client.js +63 -0
- package/dist/daemon-build-status.d.ts +8 -0
- package/dist/daemon-build-status.js +18 -0
- package/dist/daemon-server.d.ts +19 -2
- package/dist/daemon-server.js +615 -59
- package/dist/db.js +56 -0
- package/dist/desktop-browser-host-client.d.ts +2 -1
- package/dist/desktop-browser-host-client.js +3 -1
- package/dist/direct-runtime-authenticator.js +11 -6
- package/dist/headless-browser-host.js +18 -1
- package/dist/index-daemon.js +28 -1
- package/dist/maintenance-management.d.ts +11 -0
- package/dist/maintenance-management.js +13 -0
- package/dist/plugin-installer.d.ts +35 -0
- package/dist/plugin-installer.js +195 -94
- package/dist/plugin-management-service.d.ts +58 -0
- package/dist/plugin-management-service.js +240 -0
- package/dist/plugin-package.d.ts +26 -3
- package/dist/plugin-package.js +235 -56
- package/dist/pm2.js +37 -5
- package/dist/remote-runtime-access-store.d.ts +1 -0
- package/dist/remote-runtime-access-store.js +7 -0
- package/dist/remote-runtime-admin.d.ts +3 -0
- package/dist/remote-runtime-admin.js +3 -0
- package/dist/remote-runtime-connection-manager.d.ts +12 -1
- package/dist/remote-runtime-connection-manager.js +170 -4
- package/dist/remote-runtime-target-control.d.ts +5 -1
- package/dist/remote-runtime-target-control.js +62 -7
- package/dist/remote-runtime-target-store.d.ts +4 -1
- package/dist/remote-runtime-target-store.js +21 -2
- package/dist/session-log-store.js +29 -0
- package/dist/session-meta-store.js +3 -1
- package/dist/session-pending-message-store.d.ts +2 -0
- package/dist/session-pending-message-store.js +41 -0
- package/dist/session-resource-store.d.ts +32 -0
- package/dist/session-resource-store.js +700 -0
- package/dist/setup.d.ts +16 -0
- package/dist/setup.js +136 -2
- package/package.json +14 -9
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY, } from "@rynx-ai/protocol/runtime-browser-inspect";
|
|
2
3
|
import { RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY, } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
3
4
|
import { RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY, } from "@rynx-ai/protocol/runtime-emulator-surface";
|
|
4
5
|
import { assertRemoteRuntimeRpcMethodSupported, assertRemoteRuntimeSessionEventsSupported, assertRemoteRuntimeSessionTerminalSupported, parseRemoteRuntimeRpcRequest, REMOTE_RUNTIME_RPC_METHOD_METADATA, } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
5
6
|
import { REMOTE_RUNTIME_CORE_PROTOCOL, } from "@rynx-ai/protocol/remote-runtime";
|
|
6
|
-
import { connectDirectRuntime, openDirectRuntimeBrowserSurface, DirectRuntimeCallTransportError, DirectRuntimeClientCapacityError, DirectRuntimeClientError, DirectRuntimeRpcError, DirectRuntimeSessionEventsError, DirectRuntimeSessionTerminalError, } from "@rynx-ai/remote-runtime-client";
|
|
7
|
+
import { connectDirectRuntime, openDirectRuntimeBrowserInspect, openDirectRuntimeBrowserSurface, DirectRuntimeCallTransportError, DirectRuntimeClientCapacityError, DirectRuntimeClientError, DirectRuntimeRpcError, DirectRuntimeSessionEventsError, DirectRuntimeSessionTerminalError, } from "@rynx-ai/remote-runtime-client";
|
|
7
8
|
import { SessionTerminalOpenError } from "@rynx-ai/server";
|
|
8
9
|
const LOCAL_SELECTOR = "local";
|
|
9
10
|
const TARGET_SELECTOR_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,255}$/;
|
|
@@ -41,6 +42,8 @@ export class DaemonRuntimeConnectionManager {
|
|
|
41
42
|
maxIdleConnections;
|
|
42
43
|
slots = new Map();
|
|
43
44
|
generations = new Map();
|
|
45
|
+
/** Independent inspect sockets must outlive shared control-channel eviction. */
|
|
46
|
+
browserInspectClosers = new Map();
|
|
44
47
|
localStatusSnapshot;
|
|
45
48
|
closed = false;
|
|
46
49
|
managerClosePromise;
|
|
@@ -48,6 +51,7 @@ export class DaemonRuntimeConnectionManager {
|
|
|
48
51
|
this.options = options;
|
|
49
52
|
this.client = options.client ?? {
|
|
50
53
|
connect: (credential) => connectDirectRuntime(credential),
|
|
54
|
+
openBrowserInspect: (credential, target, inspectOptions) => openDirectRuntimeBrowserInspect(credential, target, inspectOptions),
|
|
51
55
|
openBrowserSurface: (credential, frame, surfaceOptions) => openDirectRuntimeBrowserSurface(credential, frame, surfaceOptions),
|
|
52
56
|
};
|
|
53
57
|
this.idleTtlMs = nonNegativeInteger(options.idleTtlMs, DEFAULT_IDLE_TTL_MS, "idleTtlMs");
|
|
@@ -89,11 +93,12 @@ export class DaemonRuntimeConnectionManager {
|
|
|
89
93
|
}
|
|
90
94
|
this.bumpGeneration(daemonId);
|
|
91
95
|
const slot = this.slots.get(daemonId);
|
|
96
|
+
const closingInspects = this.closeBrowserInspects(daemonId);
|
|
92
97
|
if (!slot)
|
|
93
|
-
return
|
|
98
|
+
return closingInspects;
|
|
94
99
|
this.slots.delete(daemonId);
|
|
95
100
|
this.cancelIdleTimer(slot);
|
|
96
|
-
return this.closeSlot(slot);
|
|
101
|
+
return closeAll([this.closeSlot(slot), closingInspects]);
|
|
97
102
|
}
|
|
98
103
|
close() {
|
|
99
104
|
if (this.managerClosePromise)
|
|
@@ -105,7 +110,12 @@ export class DaemonRuntimeConnectionManager {
|
|
|
105
110
|
this.bumpGeneration(slot.daemonId);
|
|
106
111
|
this.cancelIdleTimer(slot);
|
|
107
112
|
}
|
|
108
|
-
|
|
113
|
+
const inspectClosures = [...this.browserInspectClosers.keys()]
|
|
114
|
+
.map((daemonId) => this.closeBrowserInspects(daemonId));
|
|
115
|
+
this.managerClosePromise = closeAll([
|
|
116
|
+
...slots.map((slot) => this.closeSlot(slot)),
|
|
117
|
+
...inspectClosures,
|
|
118
|
+
]);
|
|
109
119
|
return this.managerClosePromise;
|
|
110
120
|
}
|
|
111
121
|
localLease() {
|
|
@@ -226,6 +236,9 @@ export class DaemonRuntimeConnectionManager {
|
|
|
226
236
|
throw error;
|
|
227
237
|
}
|
|
228
238
|
},
|
|
239
|
+
openBrowserInspect: async () => {
|
|
240
|
+
throw managerError("incompatible", "Local Runtime Browser Inspect is not available through the Runtime connection lease", undefined, "not_started");
|
|
241
|
+
},
|
|
229
242
|
openEmulatorSurface: async (sessionId, bindingId, surfaceOptions = {}) => {
|
|
230
243
|
if (released)
|
|
231
244
|
throw managerError("closed", "Runtime connection lease was released");
|
|
@@ -487,6 +500,20 @@ export class DaemonRuntimeConnectionManager {
|
|
|
487
500
|
}
|
|
488
501
|
return await this.openDedicatedDirectBrowserSurface(slot, frame, surfaceOptions);
|
|
489
502
|
},
|
|
503
|
+
openBrowserInspect: async (target, inspectOptions = {}) => {
|
|
504
|
+
if (released)
|
|
505
|
+
throw managerError("closed", "Runtime connection lease was released");
|
|
506
|
+
this.assertOpen();
|
|
507
|
+
this.assertSlotCurrent(slot);
|
|
508
|
+
throwIfAborted(inspectOptions.signal);
|
|
509
|
+
const status = slot.status;
|
|
510
|
+
if (!status)
|
|
511
|
+
throw managerError("closed", "Runtime status is not available");
|
|
512
|
+
if (!status.semanticCapabilities.includes(RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY)) {
|
|
513
|
+
throw managerError("incompatible", "Remote Runtime Browser Inspect is unavailable", undefined, "not_started");
|
|
514
|
+
}
|
|
515
|
+
return await this.openDedicatedDirectBrowserInspect(slot, target, inspectOptions);
|
|
516
|
+
},
|
|
490
517
|
openEmulatorSurface: async (sessionId, bindingId, surfaceOptions = {}) => {
|
|
491
518
|
if (released)
|
|
492
519
|
throw managerError("closed", "Runtime connection lease was released");
|
|
@@ -701,6 +728,71 @@ export class DaemonRuntimeConnectionManager {
|
|
|
701
728
|
resolveOpeningOperation();
|
|
702
729
|
}
|
|
703
730
|
}
|
|
731
|
+
async openDedicatedDirectBrowserInspect(slot, target, options) {
|
|
732
|
+
const timeoutMs = parseCallTimeout(options.timeoutMs);
|
|
733
|
+
let persisted;
|
|
734
|
+
try {
|
|
735
|
+
persisted = await this.options.credentials.load(slot.target.credentialRef);
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
throw managerError("authentication_failed", "Runtime target credential is unavailable", error);
|
|
739
|
+
}
|
|
740
|
+
if (persisted.daemonId !== slot.target.daemonId ||
|
|
741
|
+
persisted.identityPublicKey !== slot.target.identityPublicKey) {
|
|
742
|
+
throw managerError("identity_mismatch", "Runtime target metadata does not match its protected credential");
|
|
743
|
+
}
|
|
744
|
+
this.assertSlotCurrent(slot);
|
|
745
|
+
throwIfAborted(options.signal);
|
|
746
|
+
const credential = {
|
|
747
|
+
...persisted,
|
|
748
|
+
directEndpoint: slot.target.directRoute.endpoint,
|
|
749
|
+
};
|
|
750
|
+
const openBrowserInspect = this.client.openBrowserInspect
|
|
751
|
+
?? ((inspectCredential, inspectTarget, inspectOptions) => openDirectRuntimeBrowserInspect(inspectCredential, inspectTarget, inspectOptions));
|
|
752
|
+
const signal = combinedSignal(options.signal, slot.dedicatedAbortController.signal);
|
|
753
|
+
let resolveOpeningOperation;
|
|
754
|
+
const openingOperation = new Promise((resolve) => {
|
|
755
|
+
resolveOpeningOperation = resolve;
|
|
756
|
+
});
|
|
757
|
+
slot.pendingDedicatedOpenings.add(openingOperation);
|
|
758
|
+
let resolvedConnection;
|
|
759
|
+
let opening;
|
|
760
|
+
try {
|
|
761
|
+
throwIfAborted(signal);
|
|
762
|
+
opening = openBrowserInspect(credential, target, {
|
|
763
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
764
|
+
...(signal ? { signal } : {}),
|
|
765
|
+
}).then((opened) => {
|
|
766
|
+
resolvedConnection = opened;
|
|
767
|
+
return opened;
|
|
768
|
+
});
|
|
769
|
+
const connection = await waitFor(opening, signal);
|
|
770
|
+
this.assertSlotCurrent(slot);
|
|
771
|
+
throwIfAborted(signal);
|
|
772
|
+
slot.lastUsedAt = Date.now();
|
|
773
|
+
return this.trackDedicatedBrowserInspect(slot, connection, options.signal);
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
if (resolvedConnection) {
|
|
777
|
+
await resolvedConnection.close().catch(() => undefined);
|
|
778
|
+
}
|
|
779
|
+
else if (opening) {
|
|
780
|
+
const closeLate = opening.then((opened) => opened.close()).catch(() => undefined);
|
|
781
|
+
if (slot.dedicatedAbortController.signal.aborted)
|
|
782
|
+
await closeLate;
|
|
783
|
+
else
|
|
784
|
+
void closeLate;
|
|
785
|
+
}
|
|
786
|
+
if (slot.dedicatedAbortController.signal.aborted && isAbort(error)) {
|
|
787
|
+
throw managerError("closed", "Runtime connection was invalidated while opening Browser Inspect");
|
|
788
|
+
}
|
|
789
|
+
throw error;
|
|
790
|
+
}
|
|
791
|
+
finally {
|
|
792
|
+
slot.pendingDedicatedOpenings.delete(openingOperation);
|
|
793
|
+
resolveOpeningOperation();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
704
796
|
async openDedicatedDirectEmulatorSurface(slot, sessionId, bindingId, options) {
|
|
705
797
|
const timeoutMs = parseCallTimeout(options.timeoutMs);
|
|
706
798
|
let persisted;
|
|
@@ -934,6 +1026,80 @@ export class DaemonRuntimeConnectionManager {
|
|
|
934
1026
|
},
|
|
935
1027
|
};
|
|
936
1028
|
}
|
|
1029
|
+
trackDedicatedBrowserInspect(slot, connection, signal) {
|
|
1030
|
+
let close;
|
|
1031
|
+
let finished = false;
|
|
1032
|
+
const onAbort = () => {
|
|
1033
|
+
void close().catch(() => undefined);
|
|
1034
|
+
};
|
|
1035
|
+
close = this.registerBrowserInspectCloser(slot.daemonId, () => {
|
|
1036
|
+
finished = true;
|
|
1037
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1038
|
+
return connection.close();
|
|
1039
|
+
}, connection.closed.then(() => {
|
|
1040
|
+
finished = true;
|
|
1041
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1042
|
+
}));
|
|
1043
|
+
if (signal?.aborted)
|
|
1044
|
+
onAbort();
|
|
1045
|
+
else
|
|
1046
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1047
|
+
return {
|
|
1048
|
+
daemonInstanceId: connection.daemonInstanceId,
|
|
1049
|
+
send: async (message) => {
|
|
1050
|
+
if (finished) {
|
|
1051
|
+
throw managerError("closed", "Remote Browser Inspect connection is closed");
|
|
1052
|
+
}
|
|
1053
|
+
throwIfAborted(signal);
|
|
1054
|
+
await connection.send(message);
|
|
1055
|
+
},
|
|
1056
|
+
close,
|
|
1057
|
+
[Symbol.asyncIterator]: () => {
|
|
1058
|
+
const iterator = connection[Symbol.asyncIterator]();
|
|
1059
|
+
return {
|
|
1060
|
+
next: async () => {
|
|
1061
|
+
if (finished)
|
|
1062
|
+
return { done: true, value: undefined };
|
|
1063
|
+
throwIfAborted(signal);
|
|
1064
|
+
return await iterator.next();
|
|
1065
|
+
},
|
|
1066
|
+
return: async () => {
|
|
1067
|
+
await close();
|
|
1068
|
+
return { done: true, value: undefined };
|
|
1069
|
+
},
|
|
1070
|
+
};
|
|
1071
|
+
},
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
registerBrowserInspectCloser(daemonId, closeResource, naturallyClosed) {
|
|
1075
|
+
let closers = this.browserInspectClosers.get(daemonId);
|
|
1076
|
+
if (!closers) {
|
|
1077
|
+
closers = new Set();
|
|
1078
|
+
this.browserInspectClosers.set(daemonId, closers);
|
|
1079
|
+
}
|
|
1080
|
+
let closePromise;
|
|
1081
|
+
const forget = () => {
|
|
1082
|
+
closers.delete(close);
|
|
1083
|
+
if (closers.size === 0)
|
|
1084
|
+
this.browserInspectClosers.delete(daemonId);
|
|
1085
|
+
};
|
|
1086
|
+
const close = () => {
|
|
1087
|
+
closePromise ??= Promise.resolve()
|
|
1088
|
+
.then(closeResource)
|
|
1089
|
+
.finally(forget);
|
|
1090
|
+
return closePromise;
|
|
1091
|
+
};
|
|
1092
|
+
closers.add(close);
|
|
1093
|
+
void naturallyClosed.then(forget, forget);
|
|
1094
|
+
return close;
|
|
1095
|
+
}
|
|
1096
|
+
closeBrowserInspects(daemonId) {
|
|
1097
|
+
const closers = this.browserInspectClosers.get(daemonId);
|
|
1098
|
+
if (!closers)
|
|
1099
|
+
return Promise.resolve();
|
|
1100
|
+
this.browserInspectClosers.delete(daemonId);
|
|
1101
|
+
return closeAll([...closers].map((close) => close()));
|
|
1102
|
+
}
|
|
937
1103
|
trackDedicatedEmulatorSurface(slot, connection, surface) {
|
|
938
1104
|
const close = this.registerDedicatedCloser(slot, async () => {
|
|
939
1105
|
try {
|
|
@@ -5,7 +5,7 @@ import { type DaemonRuntimeHost, type RuntimeTargetControlHost, type RuntimeTarg
|
|
|
5
5
|
import { DaemonRuntimeConnectionManager } from "./remote-runtime-connection-manager.js";
|
|
6
6
|
import { FileRemoteRuntimeCredentialStore } from "./remote-runtime-credential-store.js";
|
|
7
7
|
import { RemoteRuntimeTargetStore } from "./remote-runtime-target-store.js";
|
|
8
|
-
export type RuntimeTargetControlErrorCode = "invalid_offer" | "invalid_target" | "not_found" | "conflict" | "tls_error" | "identity_mismatch" | "incompatible" | "authentication_failed" | "protocol_error" | "deadline_exceeded" | "outcome_unknown" | "unreachable";
|
|
8
|
+
export type RuntimeTargetControlErrorCode = "invalid_offer" | "offer_expired" | "offer_rejected" | "invalid_target" | "not_found" | "conflict" | "tls_error" | "identity_mismatch" | "incompatible" | "authentication_failed" | "protocol_error" | "deadline_exceeded" | "outcome_unknown" | "unreachable";
|
|
9
9
|
export declare class RuntimeTargetControlError extends Error {
|
|
10
10
|
readonly code: RuntimeTargetControlErrorCode;
|
|
11
11
|
constructor(code: RuntimeTargetControlErrorCode, message: string, options?: ErrorOptions);
|
|
@@ -13,6 +13,8 @@ export declare class RuntimeTargetControlError extends Error {
|
|
|
13
13
|
interface PairInput {
|
|
14
14
|
offer: unknown;
|
|
15
15
|
displayName?: string;
|
|
16
|
+
replaceExisting?: boolean;
|
|
17
|
+
expectedDaemonId?: string;
|
|
16
18
|
}
|
|
17
19
|
interface RuntimeTargetClientAdapter {
|
|
18
20
|
pair(offer: PairingOffer, options: PairDirectRuntimeOptions): ReturnType<typeof pairDirectRuntime>;
|
|
@@ -33,7 +35,9 @@ export declare class DaemonRuntimeTargetControl implements RuntimeTargetControlH
|
|
|
33
35
|
status: DaemonStatus;
|
|
34
36
|
}>;
|
|
35
37
|
private discardExpiredPending;
|
|
38
|
+
private removePendingEnrollment;
|
|
36
39
|
private resumePendingEnrollment;
|
|
40
|
+
private cleanupReplacedTarget;
|
|
37
41
|
private probe;
|
|
38
42
|
test(selector: string): Promise<{
|
|
39
43
|
target: RuntimeTargetView;
|
|
@@ -51,15 +51,25 @@ export class DaemonRuntimeTargetControl {
|
|
|
51
51
|
if (offer.daemonId === this.localRuntime.status().daemonId) {
|
|
52
52
|
throw controlError("conflict", "cannot pair the local daemon as a remote target");
|
|
53
53
|
}
|
|
54
|
+
if (input.replaceExisting
|
|
55
|
+
? input.expectedDaemonId !== offer.daemonId
|
|
56
|
+
: input.expectedDaemonId !== undefined) {
|
|
57
|
+
throw controlError("invalid_target", "the fresh pairing link does not match the Runtime selected for reauthorization");
|
|
58
|
+
}
|
|
54
59
|
// Validate all client-local metadata before any network enrollment.
|
|
55
|
-
const targetName = displayName(input.displayName, offer.daemonId);
|
|
56
60
|
const existing = this.targets.get(offer.daemonId);
|
|
57
61
|
if (existing) {
|
|
58
62
|
if (existing.identityPublicKey !== offer.identityPublicKey) {
|
|
59
63
|
throw controlError("identity_mismatch", "a target with this daemonId has a different pinned identity key");
|
|
60
64
|
}
|
|
61
|
-
|
|
65
|
+
if (!input.replaceExisting) {
|
|
66
|
+
throw controlError("conflict", "this Runtime target is already paired");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (input.replaceExisting) {
|
|
70
|
+
throw controlError("not_found", "the Runtime target to reauthorize was not found");
|
|
62
71
|
}
|
|
72
|
+
const targetName = displayName(input.displayName ?? (input.replaceExisting ? existing?.displayName : undefined), offer.daemonId);
|
|
63
73
|
const fingerprint = pairingOfferFingerprint(offer);
|
|
64
74
|
let pending = this.targets.getPendingEnrollment(offer.claimId);
|
|
65
75
|
const otherPending = this.targets.getPendingEnrollmentForDaemon(offer.daemonId);
|
|
@@ -70,7 +80,7 @@ export class DaemonRuntimeTargetControl {
|
|
|
70
80
|
if (pending)
|
|
71
81
|
assertPendingOffer(pending, offer, fingerprint);
|
|
72
82
|
if (!pending && parsed.expired) {
|
|
73
|
-
throw controlError("
|
|
83
|
+
throw controlError("offer_expired", "pairing offer has expired");
|
|
74
84
|
}
|
|
75
85
|
if (!pending) {
|
|
76
86
|
const identity = generateDirectRuntimeClientIdentity();
|
|
@@ -93,7 +103,7 @@ export class DaemonRuntimeTargetControl {
|
|
|
93
103
|
throw persistenceError("failed to prepare the Runtime enrollment", error);
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
|
-
return await this.resumePendingEnrollment(pending, offer, targetName, parsed.expired);
|
|
106
|
+
return await this.resumePendingEnrollment(pending, offer, targetName, parsed.expired, input.replaceExisting ? existing : undefined);
|
|
97
107
|
}
|
|
98
108
|
finally {
|
|
99
109
|
this.pairing = false;
|
|
@@ -103,15 +113,18 @@ export class DaemonRuntimeTargetControl {
|
|
|
103
113
|
if (Date.parse(pending.expiresAt) > Date.now()) {
|
|
104
114
|
throw controlError("conflict", "a different unexpired enrollment is already pending for this Runtime");
|
|
105
115
|
}
|
|
116
|
+
await this.removePendingEnrollment(pending, "failed to discard an expired Runtime enrollment");
|
|
117
|
+
}
|
|
118
|
+
async removePendingEnrollment(pending, failureMessage) {
|
|
106
119
|
try {
|
|
107
120
|
await this.credentials.remove(pending.credentialRef);
|
|
108
121
|
this.targets.removePendingEnrollment(pending.claimId);
|
|
109
122
|
}
|
|
110
123
|
catch (error) {
|
|
111
|
-
throw persistenceError(
|
|
124
|
+
throw persistenceError(failureMessage, error);
|
|
112
125
|
}
|
|
113
126
|
}
|
|
114
|
-
async resumePendingEnrollment(pending, offer, targetName, offerExpired) {
|
|
127
|
+
async resumePendingEnrollment(pending, offer, targetName, offerExpired, replacedTarget) {
|
|
115
128
|
let enrollment;
|
|
116
129
|
try {
|
|
117
130
|
enrollment = await this.credentials.loadEnrollment(pending.credentialRef);
|
|
@@ -131,7 +144,11 @@ export class DaemonRuntimeTargetControl {
|
|
|
131
144
|
});
|
|
132
145
|
}
|
|
133
146
|
catch (error) {
|
|
134
|
-
|
|
147
|
+
const normalized = normalizePairingClientError(error);
|
|
148
|
+
if (normalized.code === "offer_rejected") {
|
|
149
|
+
await this.removePendingEnrollment(pending, "failed to discard the rejected Runtime enrollment");
|
|
150
|
+
}
|
|
151
|
+
throw normalized;
|
|
135
152
|
}
|
|
136
153
|
assertPairResult(paired, offer, enrollment.credential);
|
|
137
154
|
accepted = secretCredentialFrom(paired.credential);
|
|
@@ -148,6 +165,9 @@ export class DaemonRuntimeTargetControl {
|
|
|
148
165
|
assertCredentialTarget(accepted, pending.daemonId, pending.identityPublicKey);
|
|
149
166
|
status = await this.probe(directCredentialForOffer(accepted, offer));
|
|
150
167
|
}
|
|
168
|
+
const invalidation = replacedTarget
|
|
169
|
+
? this.connections.invalidate(replacedTarget.daemonId)
|
|
170
|
+
: undefined;
|
|
151
171
|
let target;
|
|
152
172
|
try {
|
|
153
173
|
target = this.targets.commitPendingEnrollment(pending.claimId, {
|
|
@@ -156,13 +176,42 @@ export class DaemonRuntimeTargetControl {
|
|
|
156
176
|
identityPublicKey: offer.identityPublicKey,
|
|
157
177
|
credentialRef: pending.credentialRef,
|
|
158
178
|
directEndpoint: offer.directEndpoint,
|
|
179
|
+
}, {
|
|
180
|
+
replaceExisting: replacedTarget !== undefined,
|
|
159
181
|
});
|
|
160
182
|
}
|
|
161
183
|
catch (error) {
|
|
184
|
+
await invalidation?.catch(() => undefined);
|
|
162
185
|
throw persistenceError("failed to persist the paired Runtime target", error);
|
|
163
186
|
}
|
|
187
|
+
if (replacedTarget) {
|
|
188
|
+
await this.cleanupReplacedTarget(replacedTarget, invalidation);
|
|
189
|
+
}
|
|
164
190
|
return { target: targetView(target), status };
|
|
165
191
|
}
|
|
192
|
+
async cleanupReplacedTarget(replacedTarget, invalidation) {
|
|
193
|
+
const failures = [];
|
|
194
|
+
try {
|
|
195
|
+
await invalidation;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
failures.push(error);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
await this.credentials.remove(replacedTarget.credentialRef);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
failures.push(error);
|
|
205
|
+
}
|
|
206
|
+
if (failures.length > 0) {
|
|
207
|
+
console.warn(JSON.stringify({
|
|
208
|
+
level: "warn",
|
|
209
|
+
type: "remote-runtime-reauthorization-cleanup",
|
|
210
|
+
daemonId: replacedTarget.daemonId,
|
|
211
|
+
errors: failures.map((error) => error instanceof Error ? error.message : String(error)),
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
166
215
|
async probe(credential) {
|
|
167
216
|
let connection;
|
|
168
217
|
try {
|
|
@@ -408,6 +457,12 @@ function normalizeClientError(error) {
|
|
|
408
457
|
}
|
|
409
458
|
return controlError("unreachable", "remote Runtime connection failed", error);
|
|
410
459
|
}
|
|
460
|
+
function normalizePairingClientError(error) {
|
|
461
|
+
if (error instanceof DirectRuntimeClientError && error.code === "authentication_failed") {
|
|
462
|
+
return controlError("offer_rejected", "the remote Host rejected this pairing offer; it may already have been used, expired, or revoked", error);
|
|
463
|
+
}
|
|
464
|
+
return normalizeClientError(error);
|
|
465
|
+
}
|
|
411
466
|
function normalizeConnectionManagerError(error) {
|
|
412
467
|
const code = error.code === "closed" || error.code === "cancelled" ? "unreachable" : error.code;
|
|
413
468
|
return controlError(code, error.message, error);
|
|
@@ -34,6 +34,9 @@ export interface AddRemoteRuntimePendingEnrollmentInput {
|
|
|
34
34
|
offerFingerprint: string;
|
|
35
35
|
expiresAt: string;
|
|
36
36
|
}
|
|
37
|
+
export interface CommitRemoteRuntimePendingEnrollmentOptions {
|
|
38
|
+
replaceExisting?: boolean;
|
|
39
|
+
}
|
|
37
40
|
export declare class RemoteRuntimeTargetStore {
|
|
38
41
|
private readonly connection;
|
|
39
42
|
private readonly clock;
|
|
@@ -43,7 +46,7 @@ export declare class RemoteRuntimeTargetStore {
|
|
|
43
46
|
getPendingEnrollment(claimIdInput: string): RemoteRuntimePendingEnrollment | undefined;
|
|
44
47
|
getPendingEnrollmentForDaemon(daemonIdInput: string): RemoteRuntimePendingEnrollment | undefined;
|
|
45
48
|
removePendingEnrollment(claimIdInput: string): RemoteRuntimePendingEnrollment | undefined;
|
|
46
|
-
commitPendingEnrollment(claimIdInput: string, input: AddRemoteRuntimeTargetInput): RemoteRuntimeTarget;
|
|
49
|
+
commitPendingEnrollment(claimIdInput: string, input: AddRemoteRuntimeTargetInput, options?: CommitRemoteRuntimePendingEnrollmentOptions): RemoteRuntimeTarget;
|
|
47
50
|
get(daemonIdInput: string): RemoteRuntimeTarget | undefined;
|
|
48
51
|
list(): RemoteRuntimeTarget[];
|
|
49
52
|
remove(daemonIdInput: string): RemoteRuntimeTarget | undefined;
|
|
@@ -78,7 +78,7 @@ export class RemoteRuntimeTargetStore {
|
|
|
78
78
|
});
|
|
79
79
|
return remove.immediate();
|
|
80
80
|
}
|
|
81
|
-
commitPendingEnrollment(claimIdInput, input) {
|
|
81
|
+
commitPendingEnrollment(claimIdInput, input, options = {}) {
|
|
82
82
|
const claimId = validId(claimIdInput, "claimId");
|
|
83
83
|
const normalized = normalizeTargetInput(input);
|
|
84
84
|
const now = validNow(this.clock()).toISOString();
|
|
@@ -91,7 +91,12 @@ export class RemoteRuntimeTargetStore {
|
|
|
91
91
|
pending.credentialRef !== normalized.credentialRef) {
|
|
92
92
|
throw new Error("pending Runtime enrollment does not match the accepted credential");
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
if (options.replaceExisting) {
|
|
95
|
+
replaceTarget(this.connection, normalized, now);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
insertTarget(this.connection, normalized, now);
|
|
99
|
+
}
|
|
95
100
|
const deleted = this.connection.prepare("DELETE FROM runtime_pending_enrollments WHERE claim_id = ?").run(claimId);
|
|
96
101
|
if (deleted.changes !== 1) {
|
|
97
102
|
throw new Error("pending Runtime enrollment changed concurrently");
|
|
@@ -193,6 +198,20 @@ function insertTarget(connection, input, now) {
|
|
|
193
198
|
(daemon_id, endpoint, updated_at)
|
|
194
199
|
VALUES (?, ?, ?)`).run(input.daemonId, input.directEndpoint, now);
|
|
195
200
|
}
|
|
201
|
+
function replaceTarget(connection, input, now) {
|
|
202
|
+
const updatedTarget = connection.prepare(`UPDATE runtime_targets
|
|
203
|
+
SET display_name = ?, credential_ref = ?, updated_at = ?
|
|
204
|
+
WHERE daemon_id = ? AND identity_public_key = ?`).run(input.displayName, input.credentialRef, now, input.daemonId, input.identityPublicKey);
|
|
205
|
+
if (updatedTarget.changes !== 1) {
|
|
206
|
+
throw new RemoteRuntimeTargetConflictError("the Runtime target to reauthorize is missing or has a different pinned identity");
|
|
207
|
+
}
|
|
208
|
+
const updatedRoute = connection.prepare(`UPDATE runtime_direct_routes
|
|
209
|
+
SET endpoint = ?, updated_at = ?
|
|
210
|
+
WHERE daemon_id = ?`).run(input.directEndpoint, now, input.daemonId);
|
|
211
|
+
if (updatedRoute.changes !== 1) {
|
|
212
|
+
throw new Error("the Runtime target route to reauthorize is missing");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
196
215
|
function targetFromRow(row) {
|
|
197
216
|
const input = normalizeTargetInput({
|
|
198
217
|
daemonId: row.daemon_id,
|
|
@@ -37,6 +37,35 @@ export class SqliteSessionLogStore {
|
|
|
37
37
|
created_by: stored.createdBy ?? null,
|
|
38
38
|
created_at: stored.createdAt,
|
|
39
39
|
});
|
|
40
|
+
if (stored.type === "message" && stored.data.role === "user") {
|
|
41
|
+
const resourceIds = stored.data.content.flatMap((part) => part.type === "input_image" ? [part.resourceId] : []);
|
|
42
|
+
const operationIds = new Set();
|
|
43
|
+
for (const resourceId of resourceIds) {
|
|
44
|
+
const resource = conn.prepare(`SELECT state, message_item_id
|
|
45
|
+
FROM session_resources
|
|
46
|
+
WHERE id = ? AND session_id = ?`).get(resourceId, sessionId);
|
|
47
|
+
if (!resource ||
|
|
48
|
+
resource.state === "uploading" ||
|
|
49
|
+
(resource.message_item_id && resource.message_item_id !== stored.id)) {
|
|
50
|
+
throw new Error(`Session image resource ${resourceId} is not committable`);
|
|
51
|
+
}
|
|
52
|
+
conn.prepare(`UPDATE session_resources
|
|
53
|
+
SET state = 'committed', message_item_id = ?,
|
|
54
|
+
committed_at = COALESCE(committed_at, ?)
|
|
55
|
+
WHERE id = ? AND session_id = ?`).run(stored.id, stored.createdAt, resourceId, sessionId);
|
|
56
|
+
const operation = conn.prepare(`SELECT client_message_id
|
|
57
|
+
FROM session_message_operation_resources
|
|
58
|
+
WHERE session_id = ? AND resource_id = ?`).get(sessionId, resourceId);
|
|
59
|
+
if (operation)
|
|
60
|
+
operationIds.add(operation.client_message_id);
|
|
61
|
+
}
|
|
62
|
+
for (const clientMessageId of operationIds) {
|
|
63
|
+
conn.prepare(`UPDATE session_message_operations
|
|
64
|
+
SET state = 'mirrored', error = NULL, updated_at = ?
|
|
65
|
+
WHERE session_id = ? AND client_message_id = ?
|
|
66
|
+
AND state IN ('injecting', 'injected', 'outcome_unknown')`).run(stored.createdAt, sessionId, clientMessageId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
40
69
|
return stored;
|
|
41
70
|
});
|
|
42
71
|
});
|
|
@@ -94,7 +94,9 @@ export const sessionRegistry = {
|
|
|
94
94
|
function rowToMeta(row) {
|
|
95
95
|
return {
|
|
96
96
|
id: row.id,
|
|
97
|
-
provider: row.provider === "codex" || row.provider === "
|
|
97
|
+
provider: row.provider === "codex" || row.provider === "traex" || row.provider === "claude"
|
|
98
|
+
? row.provider
|
|
99
|
+
: undefined,
|
|
98
100
|
agent: row.agent ?? undefined,
|
|
99
101
|
config: row.config ? JSON.parse(row.config) : undefined,
|
|
100
102
|
model: row.model ?? undefined,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { db } from "./db.js";
|
|
2
|
+
export const sessionPendingMessageStore = {
|
|
3
|
+
put(message) {
|
|
4
|
+
const result = db()
|
|
5
|
+
.prepare(`INSERT OR IGNORE INTO session_pending_messages
|
|
6
|
+
(session_id, message, state, created_at)
|
|
7
|
+
VALUES (?, ?, 'queued', ?)`)
|
|
8
|
+
.run(message.sessionId, message.message, message.createdAt);
|
|
9
|
+
return result.changes === 1;
|
|
10
|
+
},
|
|
11
|
+
get(sessionId) {
|
|
12
|
+
const row = db()
|
|
13
|
+
.prepare(`SELECT session_id, message, state, created_at
|
|
14
|
+
FROM session_pending_messages WHERE session_id = ?`)
|
|
15
|
+
.get(sessionId);
|
|
16
|
+
return row ? fromRow(row) : undefined;
|
|
17
|
+
},
|
|
18
|
+
list() {
|
|
19
|
+
return db()
|
|
20
|
+
.prepare(`SELECT session_id, message, state, created_at
|
|
21
|
+
FROM session_pending_messages ORDER BY created_at`)
|
|
22
|
+
.all().map(fromRow);
|
|
23
|
+
},
|
|
24
|
+
markOutcomeUnknown(sessionId) {
|
|
25
|
+
db()
|
|
26
|
+
.prepare(`UPDATE session_pending_messages SET state = 'outcome_unknown'
|
|
27
|
+
WHERE session_id = ? AND state = 'queued'`)
|
|
28
|
+
.run(sessionId);
|
|
29
|
+
},
|
|
30
|
+
delete(sessionId) {
|
|
31
|
+
db().prepare("DELETE FROM session_pending_messages WHERE session_id = ?").run(sessionId);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
function fromRow(row) {
|
|
35
|
+
return {
|
|
36
|
+
sessionId: row.session_id,
|
|
37
|
+
message: row.message,
|
|
38
|
+
state: row.state,
|
|
39
|
+
createdAt: row.created_at,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type UserContentPart } from "@rynx-ai/core";
|
|
2
|
+
import { type RemoteRuntimeSessionResourceDeleteParams, type RemoteRuntimeSessionResourceDeleteResult, type RemoteRuntimeSessionResourceGetParams, type RemoteRuntimeSessionResourceGetResult, type RemoteRuntimeSessionResourcePolicyResult, type RemoteRuntimeSessionResourceReadParams, type RemoteRuntimeSessionResourceReadResult, type RemoteRuntimeSessionResourceUploadBeginParams, type RemoteRuntimeSessionResourceUploadBeginResult, type RemoteRuntimeSessionResourceUploadChunkParams, type RemoteRuntimeSessionResourceUploadChunkResult, type RemoteRuntimeSessionResourceUploadCommitParams, type RemoteRuntimeSessionResourceUploadCommitResult } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
3
|
+
import { type MachineSessionPreparedInput, type MachineSessionResourcePort } from "@rynx-ai/server";
|
|
4
|
+
export declare class SqliteSessionResourceStore implements MachineSessionResourcePort {
|
|
5
|
+
private readonly root;
|
|
6
|
+
constructor(root?: string);
|
|
7
|
+
policy(_sessionId: string): RemoteRuntimeSessionResourcePolicyResult;
|
|
8
|
+
beginUpload(input: RemoteRuntimeSessionResourceUploadBeginParams): Promise<RemoteRuntimeSessionResourceUploadBeginResult>;
|
|
9
|
+
writeUploadChunk(input: RemoteRuntimeSessionResourceUploadChunkParams): Promise<RemoteRuntimeSessionResourceUploadChunkResult>;
|
|
10
|
+
commitUpload(input: RemoteRuntimeSessionResourceUploadCommitParams): Promise<RemoteRuntimeSessionResourceUploadCommitResult>;
|
|
11
|
+
getResource(input: RemoteRuntimeSessionResourceGetParams): Promise<RemoteRuntimeSessionResourceGetResult>;
|
|
12
|
+
readResource(input: RemoteRuntimeSessionResourceReadParams): Promise<RemoteRuntimeSessionResourceReadResult>;
|
|
13
|
+
deleteResource(input: RemoteRuntimeSessionResourceDeleteParams): Promise<RemoteRuntimeSessionResourceDeleteResult>;
|
|
14
|
+
prepareInput(sessionId: string, content: UserContentPart[], clientMessageId?: string): Promise<MachineSessionPreparedInput>;
|
|
15
|
+
markMessageInjecting(sessionId: string, clientMessageId: string): void;
|
|
16
|
+
markMessageInjected(sessionId: string, clientMessageId: string): void;
|
|
17
|
+
markMessageOutcomeUnknown(sessionId: string, clientMessageId: string, reason: string): void;
|
|
18
|
+
markMessageFailedNotStarted(sessionId: string, clientMessageId: string, reason: string): void;
|
|
19
|
+
deleteSession(sessionId: string): void;
|
|
20
|
+
private prepareMessageOperation;
|
|
21
|
+
private bindOperationResources;
|
|
22
|
+
private transitionOperation;
|
|
23
|
+
private finalizeUpload;
|
|
24
|
+
private reconcileUploadOffset;
|
|
25
|
+
private rowByUpload;
|
|
26
|
+
private requireUpload;
|
|
27
|
+
private requireResource;
|
|
28
|
+
private sessionDirectory;
|
|
29
|
+
private partialPath;
|
|
30
|
+
private finalPath;
|
|
31
|
+
private sweepExpiredStagedResources;
|
|
32
|
+
}
|