@intx/hub-sessions 0.1.2 → 0.2.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/LICENSE +176 -0
- package/README.md +84 -1
- package/dist/agent-repo.d.ts +89 -0
- package/dist/agent-repo.js +109 -0
- package/dist/agent-state-kind.d.ts +12 -0
- package/dist/agent-state-kind.js +185 -0
- package/dist/asset-service.d.ts +123 -0
- package/dist/asset-service.js +349 -0
- package/dist/available-skills-stanza.d.ts +21 -0
- package/dist/available-skills-stanza.js +32 -0
- package/dist/credential-push.d.ts +32 -0
- package/dist/credential-push.js +85 -0
- package/dist/event-collector-registry.d.ts +20 -0
- package/dist/event-collector-registry.js +115 -0
- package/dist/event-collector.d.ts +39 -0
- package/dist/event-collector.js +357 -0
- package/dist/hub-session-lookups.d.ts +17 -0
- package/dist/hub-session-lookups.js +204 -0
- package/dist/hub-session-orchestrator.d.ts +25 -0
- package/dist/hub-session-orchestrator.js +122 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +16 -0
- package/dist/package-registry-kind.d.ts +70 -0
- package/dist/package-registry-kind.js +260 -0
- package/dist/repo-store/index.d.ts +4 -0
- package/dist/repo-store/index.js +3 -0
- package/dist/repo-store/store.d.ts +41 -0
- package/dist/repo-store/store.js +1692 -0
- package/dist/repo-store/subscribe-kind.d.ts +53 -0
- package/dist/repo-store/subscribe-kind.js +179 -0
- package/dist/repo-store/types.d.ts +483 -0
- package/dist/repo-store/types.js +42 -0
- package/dist/session-service.d.ts +235 -0
- package/dist/session-service.js +997 -0
- package/dist/skill-kind.d.ts +41 -0
- package/dist/skill-kind.js +288 -0
- package/dist/substrate.d.ts +8 -0
- package/dist/substrate.js +21 -0
- package/dist/workflow-kind.d.ts +21 -0
- package/dist/workflow-kind.js +263 -0
- package/dist/workflow-run-event-log.d.ts +21 -0
- package/dist/workflow-run-event-log.js +51 -0
- package/dist/workflow-run-kind.d.ts +326 -0
- package/dist/workflow-run-kind.js +2646 -0
- package/dist/workflow-run-reader.d.ts +47 -0
- package/dist/workflow-run-reader.js +157 -0
- package/dist/ws/index.d.ts +3 -0
- package/dist/ws/index.js +3 -0
- package/dist/ws/sidecar-events.d.ts +134 -0
- package/dist/ws/sidecar-events.js +70 -0
- package/dist/ws/sidecar-handler.d.ts +184 -0
- package/dist/ws/sidecar-handler.js +1603 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +15 -0
- package/dist/ws/sidecar-token-authenticator.js +24 -0
- package/package.json +34 -12
- package/src/agent-repo.test.ts +0 -310
- package/src/agent-repo.ts +0 -165
- package/src/agent-state-kind.test.ts +0 -247
- package/src/agent-state-kind.ts +0 -204
- package/src/asset-service.test.ts +0 -540
- package/src/asset-service.ts +0 -378
- package/src/available-skills-stanza.test.ts +0 -87
- package/src/available-skills-stanza.ts +0 -47
- package/src/credential-push.ts +0 -65
- package/src/event-collector-registry.test.ts +0 -73
- package/src/event-collector-registry.ts +0 -171
- package/src/event-collector.test.ts +0 -1387
- package/src/event-collector.ts +0 -424
- package/src/hub-session-lookups.ts +0 -206
- package/src/hub-session-orchestrator.test.ts +0 -510
- package/src/hub-session-orchestrator.ts +0 -213
- package/src/index.ts +0 -78
- package/src/repo-store/index.ts +0 -15
- package/src/repo-store/store.test.ts +0 -1169
- package/src/repo-store/store.ts +0 -428
- package/src/repo-store/types.ts +0 -253
- package/src/session-service.test.ts +0 -895
- package/src/session-service.ts +0 -464
- package/src/skill-kind.test.ts +0 -599
- package/src/skill-kind.ts +0 -350
- package/src/ws/index.ts +0 -18
- package/src/ws/sidecar-events.test.ts +0 -96
- package/src/ws/sidecar-events.ts +0 -231
- package/src/ws/sidecar-handler.test.ts +0 -2217
- package/src/ws/sidecar-handler.ts +0 -1574
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { RepoId } from "./repo-store/types.js";
|
|
2
|
+
import type { RepoStore } from "./repo-store/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* A workflow-run event as committed under
|
|
5
|
+
* `runs/<runId>/events/<seq>.json`. The discriminator field is `type`;
|
|
6
|
+
* the per-type body is opaque to the reader and surfaced verbatim as
|
|
7
|
+
* `body` so consumers narrow on the discriminator they care about.
|
|
8
|
+
*/
|
|
9
|
+
export type WorkflowRunEvent = {
|
|
10
|
+
seq: number;
|
|
11
|
+
type: string;
|
|
12
|
+
body: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Reader for a workflow-run repo's committed event log. Projects the
|
|
16
|
+
* `runs/<runId>/events/<seq>.json` substrate the workflow-process child
|
|
17
|
+
* writes (validated at push time by the workflow-run kind handler) into
|
|
18
|
+
* seq-ordered in-memory events, mirroring the read-model approach the
|
|
19
|
+
* per-session timeline reconstruction uses against an agent-state repo.
|
|
20
|
+
*
|
|
21
|
+
* The reader is read-only: it composes `RepoStore.getRepoDir` (a pure
|
|
22
|
+
* path computation) with direct `isomorphic-git` tree/blob reads. It
|
|
23
|
+
* never writes, so it carries no authorize gate of its own; callers
|
|
24
|
+
* gate access at their own boundary (the REST routes use a
|
|
25
|
+
* `workflow-run:<deploymentId>` grant check).
|
|
26
|
+
*/
|
|
27
|
+
export interface WorkflowRunReader {
|
|
28
|
+
/**
|
|
29
|
+
* Enumerate the run ids present under `runs/` on `ref`. Returns an
|
|
30
|
+
* empty array when the repo has not been initialised yet (no on-disk
|
|
31
|
+
* repoDir, no ref, or no `runs/` tree). A corrupt repo, a
|
|
32
|
+
* present-but-malformed tree, or any other unexpected isomorphic-git
|
|
33
|
+
* error propagates so the caller sees the failure rather than
|
|
34
|
+
* treating it as "no runs yet".
|
|
35
|
+
*/
|
|
36
|
+
listRunIds(repoId: RepoId, ref: string): Promise<string[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Read every event under `runs/<runId>/events/` on `ref` and return
|
|
39
|
+
* them in ascending `seq` order. Returns an empty array when the run
|
|
40
|
+
* has not yet committed any events or the repo/ref has not been
|
|
41
|
+
* created. A blob that parses but is missing a string `type`
|
|
42
|
+
* discriminator is a substrate-invariant violation and throws rather
|
|
43
|
+
* than being silently dropped.
|
|
44
|
+
*/
|
|
45
|
+
readRunEvents(repoId: RepoId, ref: string, runId: string): Promise<WorkflowRunEvent[]>;
|
|
46
|
+
}
|
|
47
|
+
export declare function createWorkflowRunReader(repoStore: RepoStore): WorkflowRunReader;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import git from "isomorphic-git";
|
|
3
|
+
import { WORKFLOW_RUN_EVENTS_DIR, WORKFLOW_RUN_RUNS_PREFIX, } from "./workflow-run-kind.js";
|
|
4
|
+
import { WORKFLOW_RUN_EVENTS_FILE, splitCombinedEventLog, } from "./workflow-run-event-log.js";
|
|
5
|
+
const EVENT_FILENAME_RE = /^(0|[1-9][0-9]*)\.json$/;
|
|
6
|
+
export function createWorkflowRunReader(repoStore) {
|
|
7
|
+
function repoDirOrNull(repoId) {
|
|
8
|
+
try {
|
|
9
|
+
return repoStore.getRepoDir(repoId);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// getRepoDir throws only when the repoId fails the kind handler's
|
|
13
|
+
// slug validation; an uninitialised-but-valid repo returns a path
|
|
14
|
+
// that does not yet exist on disk. A validation failure here means
|
|
15
|
+
// the caller handed an id the substrate would never have written,
|
|
16
|
+
// which is indistinguishable from "no such run repo" at this read
|
|
17
|
+
// boundary.
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function resolveRefOrNull(dir, ref) {
|
|
22
|
+
try {
|
|
23
|
+
return await git.resolveRef({ fs, dir, ref });
|
|
24
|
+
}
|
|
25
|
+
catch (cause) {
|
|
26
|
+
if (cause instanceof git.Errors.NotFoundError ||
|
|
27
|
+
(cause instanceof Error && /ENOENT|not found/i.test(cause.message))) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
throw cause;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function listRunIds(repoId, ref) {
|
|
34
|
+
const dir = repoDirOrNull(repoId);
|
|
35
|
+
if (dir === null)
|
|
36
|
+
return [];
|
|
37
|
+
const oid = await resolveRefOrNull(dir, ref);
|
|
38
|
+
if (oid === null)
|
|
39
|
+
return [];
|
|
40
|
+
let tree;
|
|
41
|
+
try {
|
|
42
|
+
tree = await git.readTree({
|
|
43
|
+
fs,
|
|
44
|
+
dir,
|
|
45
|
+
oid,
|
|
46
|
+
filepath: WORKFLOW_RUN_RUNS_PREFIX,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (cause) {
|
|
50
|
+
if (cause instanceof git.Errors.NotFoundError)
|
|
51
|
+
return [];
|
|
52
|
+
throw cause;
|
|
53
|
+
}
|
|
54
|
+
return tree.tree
|
|
55
|
+
.filter((entry) => entry.type === "tree")
|
|
56
|
+
.map((entry) => entry.path);
|
|
57
|
+
}
|
|
58
|
+
async function readRunEvents(repoId, ref, runId) {
|
|
59
|
+
const dir = repoDirOrNull(repoId);
|
|
60
|
+
if (dir === null)
|
|
61
|
+
return [];
|
|
62
|
+
const oid = await resolveRefOrNull(dir, ref);
|
|
63
|
+
if (oid === null)
|
|
64
|
+
return [];
|
|
65
|
+
const runDir = `${WORKFLOW_RUN_RUNS_PREFIX}/${runId}`;
|
|
66
|
+
let runTree;
|
|
67
|
+
try {
|
|
68
|
+
runTree = await git.readTree({ fs, dir, oid, filepath: runDir });
|
|
69
|
+
}
|
|
70
|
+
catch (cause) {
|
|
71
|
+
if (cause instanceof git.Errors.NotFoundError)
|
|
72
|
+
return [];
|
|
73
|
+
throw cause;
|
|
74
|
+
}
|
|
75
|
+
// A terminated run is sealed into a single combined `events.jsonl`;
|
|
76
|
+
// an in-flight run keeps per-event `events/<seq>.json` files. The two
|
|
77
|
+
// forms are mutually exclusive in a run directory; a run carrying both
|
|
78
|
+
// is a botched seal, and silently preferring one would mask it, so
|
|
79
|
+
// surface it instead.
|
|
80
|
+
const combined = runTree.tree.find((e) => e.type === "blob" && e.path === WORKFLOW_RUN_EVENTS_FILE);
|
|
81
|
+
const perEventDir = runTree.tree.find((e) => e.type === "tree" && e.path === WORKFLOW_RUN_EVENTS_DIR);
|
|
82
|
+
if (combined !== undefined && perEventDir !== undefined) {
|
|
83
|
+
throw new Error(`workflow-run reader: run ${runId} carries both a combined ${WORKFLOW_RUN_EVENTS_FILE} and a per-event ${WORKFLOW_RUN_EVENTS_DIR}/ directory`);
|
|
84
|
+
}
|
|
85
|
+
if (combined !== undefined) {
|
|
86
|
+
const blob = await git.readBlob({ fs, dir, oid: combined.oid });
|
|
87
|
+
const source = `${runDir}/${WORKFLOW_RUN_EVENTS_FILE}`;
|
|
88
|
+
const events = [];
|
|
89
|
+
for (const line of splitCombinedEventLog(new TextDecoder().decode(blob.blob))) {
|
|
90
|
+
events.push(parseRunEventLine(line, source));
|
|
91
|
+
}
|
|
92
|
+
events.sort((a, b) => a.seq - b.seq);
|
|
93
|
+
return events;
|
|
94
|
+
}
|
|
95
|
+
const eventsDir = `${runDir}/${WORKFLOW_RUN_EVENTS_DIR}`;
|
|
96
|
+
let tree;
|
|
97
|
+
try {
|
|
98
|
+
tree = await git.readTree({ fs, dir, oid, filepath: eventsDir });
|
|
99
|
+
}
|
|
100
|
+
catch (cause) {
|
|
101
|
+
if (cause instanceof git.Errors.NotFoundError)
|
|
102
|
+
return [];
|
|
103
|
+
throw cause;
|
|
104
|
+
}
|
|
105
|
+
const events = [];
|
|
106
|
+
for (const entry of tree.tree) {
|
|
107
|
+
if (entry.type !== "blob")
|
|
108
|
+
continue;
|
|
109
|
+
const m = EVENT_FILENAME_RE.exec(entry.path);
|
|
110
|
+
if (m === null || m[1] === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
const seq = Number.parseInt(m[1], 10);
|
|
113
|
+
const blob = await git.readBlob({ fs, dir, oid: entry.oid });
|
|
114
|
+
const path = `${eventsDir}/${entry.path}`;
|
|
115
|
+
const parsed = parseEventObject(new TextDecoder().decode(blob.blob), path);
|
|
116
|
+
const type = parsed["type"];
|
|
117
|
+
if (typeof type !== "string") {
|
|
118
|
+
throw new Error(`workflow-run reader: event at ${path} is missing a string \`type\` field`);
|
|
119
|
+
}
|
|
120
|
+
// The per-event form carries the seq in the filename; when the body
|
|
121
|
+
// also carries one, the two must agree (the combined form reads the
|
|
122
|
+
// seq from the body, so a disagreement would make the forms diverge).
|
|
123
|
+
const bodySeq = parsed["seq"];
|
|
124
|
+
if (typeof bodySeq === "number" && bodySeq !== seq) {
|
|
125
|
+
throw new Error(`workflow-run reader: event at ${path} body seq ${String(bodySeq)} does not match filename seq ${String(seq)}`);
|
|
126
|
+
}
|
|
127
|
+
events.push({ seq, type, body: parsed });
|
|
128
|
+
}
|
|
129
|
+
events.sort((a, b) => a.seq - b.seq);
|
|
130
|
+
return events;
|
|
131
|
+
}
|
|
132
|
+
// Parse one combined-log line (the verbatim text of a former
|
|
133
|
+
// `events/<seq>.json` blob): the seq is read from the body, since the
|
|
134
|
+
// combined form drops the per-event filename that carried it.
|
|
135
|
+
function parseRunEventLine(line, source) {
|
|
136
|
+
const parsed = parseEventObject(line, source);
|
|
137
|
+
const seq = parsed["seq"];
|
|
138
|
+
const type = parsed["type"];
|
|
139
|
+
if (typeof seq !== "number") {
|
|
140
|
+
throw new Error(`workflow-run reader: event in ${source} is missing a numeric \`seq\` field`);
|
|
141
|
+
}
|
|
142
|
+
if (typeof type !== "string") {
|
|
143
|
+
throw new Error(`workflow-run reader: event in ${source} is missing a string \`type\` field`);
|
|
144
|
+
}
|
|
145
|
+
return { seq, type, body: parsed };
|
|
146
|
+
}
|
|
147
|
+
function parseEventObject(text, source) {
|
|
148
|
+
const parsed = JSON.parse(text);
|
|
149
|
+
if (typeof parsed !== "object" ||
|
|
150
|
+
parsed === null ||
|
|
151
|
+
Array.isArray(parsed)) {
|
|
152
|
+
throw new Error(`workflow-run reader: event at ${source} is not a JSON object`);
|
|
153
|
+
}
|
|
154
|
+
return { ...parsed };
|
|
155
|
+
}
|
|
156
|
+
return { listRunIds, readRunEvents };
|
|
157
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createSidecarRouter, type SidecarRouter, type SidecarRouterConfig, type SidecarConnection, type SidecarAuthIdentity, type SidecarAuthenticator, type SendPackOptions, type WsHandle, } from "./sidecar-handler.js";
|
|
2
|
+
export { createSidecarTokenAuthenticator, type CreateSidecarTokenAuthenticatorDeps, } from "./sidecar-token-authenticator.js";
|
|
3
|
+
export { createSidecarEmitter, type SidecarEventEmitter, type SidecarEventMap, type SidecarEventType, type SidecarEventListener, type SidecarLookups, type SidecarMailPersistedRow, type SidecarMailPersistedPayload, } from "./sidecar-events.js";
|
package/dist/ws/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { PackRejectReason, RepoId } from "@intx/types/sidecar";
|
|
2
|
+
import type { ConnectorThreadState } from "@intx/types/runtime";
|
|
3
|
+
export type SidecarMailPersistedRow = {
|
|
4
|
+
id: string;
|
|
5
|
+
createdAt: Date;
|
|
6
|
+
direction: "inbound" | "outbound";
|
|
7
|
+
instanceId: string | null;
|
|
8
|
+
address: string;
|
|
9
|
+
};
|
|
10
|
+
export type SidecarMailPersistedPayload = SidecarMailPersistedRow & {
|
|
11
|
+
raw: Uint8Array;
|
|
12
|
+
};
|
|
13
|
+
export type SidecarEventMap = {
|
|
14
|
+
/** Notification. Emitted for every agent.event frame the wire layer
|
|
15
|
+
* decodes. The wire layer also forwards the event to in-process agent
|
|
16
|
+
* subscribers registered via `router.subscribeAgent`; this event is
|
|
17
|
+
* the host-side observation point. */
|
|
18
|
+
"agent.event": {
|
|
19
|
+
agentAddress: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
event: unknown;
|
|
22
|
+
};
|
|
23
|
+
/** Notification. Emitted once when a sidecar's connection closes,
|
|
24
|
+
* carrying every address the connection owned -- challenged session
|
|
25
|
+
* addresses and hub-minted workflow-substrate deployment addresses
|
|
26
|
+
* alike -- so lifecycle teardown covers both. */
|
|
27
|
+
"sidecar.disconnect": {
|
|
28
|
+
ownedAddresses: string[];
|
|
29
|
+
};
|
|
30
|
+
/** Notification. Emitted when a mail.outbound frame from a sidecar
|
|
31
|
+
* names recipients that the wire layer could not deliver locally and
|
|
32
|
+
* could not enqueue for a disconnected agent. The host is free to
|
|
33
|
+
* relay it onto an external transport or drop it. */
|
|
34
|
+
"mail.outbound.undelivered": {
|
|
35
|
+
rawMessage: string;
|
|
36
|
+
recipients: string[];
|
|
37
|
+
};
|
|
38
|
+
/** Notification. Emitted once per row produced by the host's
|
|
39
|
+
* `persistMail` lookup. The wire layer calls `persistMail` to obtain
|
|
40
|
+
* the rows; this event fires for each so subscribers can react
|
|
41
|
+
* per-row (e.g. dispatch a delivered event). */
|
|
42
|
+
"mail.persisted": SidecarMailPersistedPayload;
|
|
43
|
+
/** Awaited. Emitted when an agent.deploy.ack frame arrives. Rejection
|
|
44
|
+
* fails the pending deploy with the listener's error. */
|
|
45
|
+
"agent.deploy.ack": {
|
|
46
|
+
agentAddress: string;
|
|
47
|
+
publicKey: string;
|
|
48
|
+
};
|
|
49
|
+
/** Notification. Emitted when the sidecar reports a change to an
|
|
50
|
+
* agent's connector-thread state. The wire layer caches the state
|
|
51
|
+
* per agent so the host can read it via
|
|
52
|
+
* `router.getConnectorState(agentAddress)`; this event is for hosts
|
|
53
|
+
* that want to observe transitions directly. `connectorState` is
|
|
54
|
+
* `null` when the agent has no active connector thread. */
|
|
55
|
+
"connector.state.changed": {
|
|
56
|
+
agentAddress: string;
|
|
57
|
+
connectorState: ConnectorThreadState | null;
|
|
58
|
+
};
|
|
59
|
+
/** Awaited. Emitted per address after challenge verification
|
|
60
|
+
* succeeds and before the disconnect queue is flushed. Rejection
|
|
61
|
+
* rolls that address back from the routing table; earlier listeners
|
|
62
|
+
* in registration order have already executed and their side effects
|
|
63
|
+
* are not undone. A subsequent reconnect arriving mid-flight may
|
|
64
|
+
* supersede this one, so listeners must be idempotent. */
|
|
65
|
+
"agent.reconnected": {
|
|
66
|
+
agentAddress: string;
|
|
67
|
+
};
|
|
68
|
+
/** Awaited. Emitted per address after the wire layer has confirmed
|
|
69
|
+
* the sidecar's deploy ref is stale relative to the hub's current
|
|
70
|
+
* ref. The listener's job is to push a fresh deploy pack. The wire
|
|
71
|
+
* layer fires this only when staleness is confirmed; subscribing
|
|
72
|
+
* without a `lookupDeployRef` configured on the router will never
|
|
73
|
+
* deliver. */
|
|
74
|
+
"deploy.ref.stale": {
|
|
75
|
+
agentAddress: string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
export type SidecarEventType = keyof SidecarEventMap;
|
|
79
|
+
export type SidecarEventListener<T extends SidecarEventType> = (payload: SidecarEventMap[T]) => void | Promise<void>;
|
|
80
|
+
export type SidecarEventEmitter = {
|
|
81
|
+
on<T extends SidecarEventType>(type: T, listener: SidecarEventListener<T>): () => void;
|
|
82
|
+
emit<T extends SidecarEventType>(type: T, payload: SidecarEventMap[T]): void;
|
|
83
|
+
emitAndAwait<T extends SidecarEventType>(type: T, payload: SidecarEventMap[T]): Promise<void>;
|
|
84
|
+
/** Number of listeners registered for `type`. Wire-layer callers use
|
|
85
|
+
* this to skip an `await` when nothing is listening, preserving the
|
|
86
|
+
* synchronous scheduling of unconfigured-handler paths. */
|
|
87
|
+
listenerCount(type: SidecarEventType): number;
|
|
88
|
+
};
|
|
89
|
+
export declare function createSidecarEmitter(): SidecarEventEmitter;
|
|
90
|
+
export type SidecarLookups = {
|
|
91
|
+
/** Returns the hex-encoded Ed25519 public key stored for the address,
|
|
92
|
+
* or `null` if the address is unknown. Used during the reconnect
|
|
93
|
+
* challenge to verify the sidecar's signature. */
|
|
94
|
+
lookupPublicKey?: (agentAddress: string) => Promise<string | null>;
|
|
95
|
+
/** Returns the hub's current deploy ref for the address, or `null` if
|
|
96
|
+
* no deploy state is tracked. The wire layer compares this against
|
|
97
|
+
* the sidecar's reported ref during reconnect and emits
|
|
98
|
+
* `deploy.ref.stale` only on mismatch. */
|
|
99
|
+
lookupDeployRef?: (agentAddress: string) => Promise<string | null>;
|
|
100
|
+
/** Persists a delivered outbound mail frame. Returns one row per
|
|
101
|
+
* persisted record; the wire layer attaches `raw` to each row and
|
|
102
|
+
* emits a `mail.persisted` event. */
|
|
103
|
+
persistMail?: (args: {
|
|
104
|
+
senderAddress: string;
|
|
105
|
+
recipients: string[];
|
|
106
|
+
raw: Uint8Array;
|
|
107
|
+
}) => Promise<SidecarMailPersistedRow[]>;
|
|
108
|
+
/** Ingests a received agent-state pack and returns whether the wire
|
|
109
|
+
* layer should ack or reject the pack to the sidecar. `repoId.kind`
|
|
110
|
+
* is `"agent-state"` and `repoId.id` is the agent address. The wire
|
|
111
|
+
* layer dispatches on `repoId.kind` against the receive lookups
|
|
112
|
+
* before calling either; this lookup must reject any pack whose
|
|
113
|
+
* `repoId.kind` is not `"agent-state"`. */
|
|
114
|
+
receiveAgentStatePack?: (repoId: RepoId, pack: Uint8Array, ref: string, commitSha: string) => Promise<{
|
|
115
|
+
accepted: true;
|
|
116
|
+
} | {
|
|
117
|
+
accepted: false;
|
|
118
|
+
reason: PackRejectReason;
|
|
119
|
+
}>;
|
|
120
|
+
/** Ingests a received workflow-run pack and returns whether the wire
|
|
121
|
+
* layer should ack or reject the pack to the sidecar. `repoId.kind`
|
|
122
|
+
* is `"workflow-run"` and `repoId.id` is the deployment id (which the
|
|
123
|
+
* hub-side substrate maps to a `WorkflowRunSupervisorPrincipal`
|
|
124
|
+
* during the receivePack call). The wire layer dispatches on
|
|
125
|
+
* `repoId.kind` against the receive lookups before calling either;
|
|
126
|
+
* this lookup must reject any pack whose `repoId.kind` is not
|
|
127
|
+
* `"workflow-run"`. */
|
|
128
|
+
receiveWorkflowRunPack?: (repoId: RepoId, pack: Uint8Array, ref: string, commitSha: string) => Promise<{
|
|
129
|
+
accepted: true;
|
|
130
|
+
} | {
|
|
131
|
+
accepted: false;
|
|
132
|
+
reason: PackRejectReason;
|
|
133
|
+
}>;
|
|
134
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Typed event emitter for the sidecar router.
|
|
2
|
+
//
|
|
3
|
+
// The router emits events at the points where wire-layer frame handling
|
|
4
|
+
// completes and a host-side decision or side effect is required. Two
|
|
5
|
+
// emission shapes are exposed:
|
|
6
|
+
//
|
|
7
|
+
// - `emit(type, payload)` — notification semantics. Each listener runs
|
|
8
|
+
// inside its own try/catch; a thrown error is logged and does not
|
|
9
|
+
// affect other listeners or the wire layer. Used for events whose
|
|
10
|
+
// outcome does not feed back into protocol behavior.
|
|
11
|
+
//
|
|
12
|
+
// - `emitAndAwait(type, payload)` — sequential await semantics.
|
|
13
|
+
// Listeners run in registration order; the first rejection propagates
|
|
14
|
+
// to the caller and stops the chain. Used for events whose outcome
|
|
15
|
+
// affects subsequent wire-layer state (e.g. reconnect rollback).
|
|
16
|
+
//
|
|
17
|
+
// The TSDoc on each entry in `SidecarEventMap` records which semantic
|
|
18
|
+
// applies. Mixing the two on a single event is intentional: today's
|
|
19
|
+
// wire layer already has both behaviors, and pretending otherwise
|
|
20
|
+
// would silently change failure handling.
|
|
21
|
+
import { getLogger } from "@intx/log";
|
|
22
|
+
const logger = getLogger(["hub", "ws", "sidecar", "events"]);
|
|
23
|
+
export function createSidecarEmitter() {
|
|
24
|
+
const listeners = {
|
|
25
|
+
"agent.event": new Set(),
|
|
26
|
+
"sidecar.disconnect": new Set(),
|
|
27
|
+
"mail.outbound.undelivered": new Set(),
|
|
28
|
+
"mail.persisted": new Set(),
|
|
29
|
+
"agent.deploy.ack": new Set(),
|
|
30
|
+
"agent.reconnected": new Set(),
|
|
31
|
+
"deploy.ref.stale": new Set(),
|
|
32
|
+
"connector.state.changed": new Set(),
|
|
33
|
+
};
|
|
34
|
+
function on(type, listener) {
|
|
35
|
+
listeners[type].add(listener);
|
|
36
|
+
return () => {
|
|
37
|
+
listeners[type].delete(listener);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function emit(type, payload) {
|
|
41
|
+
const set = listeners[type];
|
|
42
|
+
if (set.size === 0)
|
|
43
|
+
return;
|
|
44
|
+
for (const listener of [...set]) {
|
|
45
|
+
try {
|
|
46
|
+
const result = listener(payload);
|
|
47
|
+
if (result instanceof Promise) {
|
|
48
|
+
result.catch((err) => {
|
|
49
|
+
logger.warn `Listener for ${type} threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
logger.warn `Listener for ${type} threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function emitAndAwait(type, payload) {
|
|
59
|
+
const set = listeners[type];
|
|
60
|
+
if (set.size === 0)
|
|
61
|
+
return;
|
|
62
|
+
for (const listener of [...set]) {
|
|
63
|
+
await listener(payload);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function listenerCount(type) {
|
|
67
|
+
return listeners[type].size;
|
|
68
|
+
}
|
|
69
|
+
return { on, emit, emitAndAwait, listenerCount };
|
|
70
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { type AgentDeployFrame, type HubFrame, type RepoId } from "@intx/types/sidecar";
|
|
2
|
+
import type { ConnectorThreadState, HarnessConfig, InferenceSource } from "@intx/types/runtime";
|
|
3
|
+
import { type SidecarEventEmitter, type SidecarLookups } from "./sidecar-events.js";
|
|
4
|
+
export type SidecarConnection = {
|
|
5
|
+
sidecarId: string;
|
|
6
|
+
agentAddresses: Set<string>;
|
|
7
|
+
workflowAddresses: Set<string>;
|
|
8
|
+
send(frame: HubFrame): void;
|
|
9
|
+
};
|
|
10
|
+
export type SendPackOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* Repo-relative mount path under the sidecar's per-agent workspace.
|
|
13
|
+
* When set, the receiving sidecar materializes the pack as plain
|
|
14
|
+
* files at `<workspaceRoot>/<mountPath>/` and does NOT apply it to
|
|
15
|
+
* the agent's deploy git tree. Absent for agent-state deploy/state
|
|
16
|
+
* packs, which continue to apply to the deploy tree.
|
|
17
|
+
*/
|
|
18
|
+
mountPath?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Override the `repoId` emitted on the wire. The agent-state flow
|
|
21
|
+
* defaults to `{ kind: "agent-state", id: agentAddress }`; asset
|
|
22
|
+
* packs must pass the SOURCE asset's id so audit can correlate the
|
|
23
|
+
* pack back to its hub-side origin.
|
|
24
|
+
*/
|
|
25
|
+
repoId?: RepoId;
|
|
26
|
+
};
|
|
27
|
+
export type SidecarRouter = {
|
|
28
|
+
handleOpen(ws: WsHandle): void;
|
|
29
|
+
handleMessage(ws: WsHandle, data: string): void;
|
|
30
|
+
handleClose(ws: WsHandle): void;
|
|
31
|
+
routeMail(agentAddress: string, rawMessage: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Returns the current connector-thread state for the named agent, or
|
|
34
|
+
* `null` if the agent has no active connector thread (or if the
|
|
35
|
+
* sidecar has not yet reported any state — e.g. mid-reconnect, before
|
|
36
|
+
* the harness has loaded its context store). The state is cached
|
|
37
|
+
* from `connector.state.changed` frames; callers should treat `null`
|
|
38
|
+
* as "no threading info available" and fall through to whatever
|
|
39
|
+
* default the calling path uses.
|
|
40
|
+
*/
|
|
41
|
+
getConnectorState(agentAddress: string): ConnectorThreadState | null;
|
|
42
|
+
/**
|
|
43
|
+
* Send an `agent.deploy` frame to the sidecar. When `workflow` is
|
|
44
|
+
* supplied, the frame carries the multi-step deploy projection
|
|
45
|
+
* (workflow definition plus per-step source pins); the sidecar's
|
|
46
|
+
* deploy router routes it to the workflow deploy path. The sole
|
|
47
|
+
* caller supplies `workflow` on every deploy; per-step provisioning
|
|
48
|
+
* uses `sendProvisionStep`.
|
|
49
|
+
*
|
|
50
|
+
* The returned promise resolves with the supervisor's principal
|
|
51
|
+
* public key (hex-encoded Ed25519) carried on `agent.deploy.ack`.
|
|
52
|
+
* The legacy callers that ignore the return value continue to work
|
|
53
|
+
* unchanged.
|
|
54
|
+
*/
|
|
55
|
+
sendAgentDeploy(agentAddress: string, config: HarnessConfig, workflow?: AgentDeployFrame["workflow"]): Promise<{
|
|
56
|
+
publicKey: string;
|
|
57
|
+
}>;
|
|
58
|
+
sendAgentUndeploy(agentAddress: string, reason: string): Promise<void>;
|
|
59
|
+
sendSourcesUpdate(agentAddress: string, sources: InferenceSource[], defaultSource: string): Promise<void>;
|
|
60
|
+
sendPack(agentAddress: string, pack: Uint8Array, ref: string, commitSha: string, options?: SendPackOptions): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Bind a per-step workflow-substrate address to a sidecar for the staging
|
|
63
|
+
* window of a multi-step deploy, so `sendPack` can route the step's deploy
|
|
64
|
+
* and asset packs before the deployment-level frame spawns the child. The
|
|
65
|
+
* address enters the keyless `workflowAddresses` routing set; call
|
|
66
|
+
* `unbindStepRoute` once the step's packs land. Throws if no sidecar is
|
|
67
|
+
* available.
|
|
68
|
+
*/
|
|
69
|
+
bindStepRoute(stepAddress: string): void;
|
|
70
|
+
/**
|
|
71
|
+
* Remove a per-step route bound by `bindStepRoute`. Idempotent: an unbound
|
|
72
|
+
* address is a no-op.
|
|
73
|
+
*/
|
|
74
|
+
unbindStepRoute(stepAddress: string): void;
|
|
75
|
+
/**
|
|
76
|
+
* Provision one step of a multi-step deploy on the sidecar WITHOUT
|
|
77
|
+
* spawning: the sidecar initializes the step's agent-state repo and
|
|
78
|
+
* records the hub key so the follow-up deploy pack applies and verifies.
|
|
79
|
+
* The step address must already be bound via `bindStepRoute`. Resolves
|
|
80
|
+
* once the sidecar acks, so the caller can then deliver the deploy pack.
|
|
81
|
+
*/
|
|
82
|
+
sendProvisionStep(agentAddress: string, config: HarnessConfig): Promise<void>;
|
|
83
|
+
sendSyncRequest(agentAddress: string): void;
|
|
84
|
+
/**
|
|
85
|
+
* Deliver a workflow-run signal to the sidecar that hosts the named
|
|
86
|
+
* deployment-level mail address. The sidecar's hub-link routes the
|
|
87
|
+
* frame through its `signalInboundRouter` into the deployment's
|
|
88
|
+
* supervisor, which sends a `signal.deliver` control IPC frame to
|
|
89
|
+
* the workflow-process child. The child commits the resulting
|
|
90
|
+
* `SignalReceived` event through its own substrate -- the single
|
|
91
|
+
* writer of the workflow-run repo on the sidecar side -- so the
|
|
92
|
+
* pack-push pipeline that propagates the commit to the hub never
|
|
93
|
+
* sees a concurrent writer at the same ref.
|
|
94
|
+
*
|
|
95
|
+
* Throws when no sidecar is registered for `agentAddress`; the
|
|
96
|
+
* caller is responsible for ensuring the deployment is live.
|
|
97
|
+
*/
|
|
98
|
+
sendSignalDeliver(opts: {
|
|
99
|
+
agentAddress: string;
|
|
100
|
+
runId: string;
|
|
101
|
+
signalName: string;
|
|
102
|
+
signalId: string;
|
|
103
|
+
payload: unknown;
|
|
104
|
+
}): void;
|
|
105
|
+
/**
|
|
106
|
+
* Deliver a workflow-host drain control payload to the sidecar that
|
|
107
|
+
* hosts the named deployment-level mail address. The sidecar's
|
|
108
|
+
* hub-link routes the frame through its `drainInboundRouter` into
|
|
109
|
+
* the deployment's supervisor, which sends a `drain` control IPC
|
|
110
|
+
* frame to the workflow-process child and arms one `drainTimeout`
|
|
111
|
+
* accumulator per in-flight run. Cancel-mode steps abort on the
|
|
112
|
+
* child side; wait-mode steps continue. Accumulators commit a
|
|
113
|
+
* signed `CancelRequested{origin: "supervisor-drain"}` against the
|
|
114
|
+
* workflow-run repo when the deadline expires.
|
|
115
|
+
*
|
|
116
|
+
* Throws when no sidecar is registered for `agentAddress`; the
|
|
117
|
+
* caller is responsible for ensuring the deployment is live.
|
|
118
|
+
*/
|
|
119
|
+
sendDrain(opts: {
|
|
120
|
+
agentAddress: string;
|
|
121
|
+
deadlineMs: number;
|
|
122
|
+
}): void;
|
|
123
|
+
subscribeAgent(agentAddress: string, callback: (event: unknown) => void): () => void;
|
|
124
|
+
dispatchAgentEvent(agentAddress: string, event: unknown): void;
|
|
125
|
+
getConnectedSidecars(): string[];
|
|
126
|
+
getRoutableAddresses(): string[];
|
|
127
|
+
/** Typed event emitter for the receiver-dispatch surface. See
|
|
128
|
+
* `sidecar-events.ts` for the event map and emission semantics. */
|
|
129
|
+
events: SidecarEventEmitter;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* A verified sidecar-connection identity resolved by an authenticator from
|
|
133
|
+
* the credentials a sidecar presents on the WebSocket handshake. The
|
|
134
|
+
* `sidecarId` is the connection's own trusted id; it is not the untrusted
|
|
135
|
+
* `sidecarId` claimed on the register/reconnect frame, and it carries no
|
|
136
|
+
* tenant scope. Modeled as a discriminated union so a future non-sidecar
|
|
137
|
+
* principal (e.g. an operator user) can be added as an additional arm
|
|
138
|
+
* without changing existing consumers.
|
|
139
|
+
*/
|
|
140
|
+
export type SidecarAuthIdentity = {
|
|
141
|
+
kind: "sidecar";
|
|
142
|
+
sidecarId: string;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Resolves the credentials a sidecar presents on the handshake to a
|
|
146
|
+
* verified identity, or `null` when the credentials are not recognized.
|
|
147
|
+
* The claimed `sidecarId` is an unauthenticated hint; the authenticator
|
|
148
|
+
* derives the trusted identity from the `token` and the returned
|
|
149
|
+
* `sidecarId` is what the router keys connection state off of.
|
|
150
|
+
*/
|
|
151
|
+
export type SidecarAuthenticator = (claim: {
|
|
152
|
+
sidecarId: string;
|
|
153
|
+
token: string;
|
|
154
|
+
}) => Promise<SidecarAuthIdentity | null>;
|
|
155
|
+
export type SidecarRouterConfig = {
|
|
156
|
+
requestTimeoutMs?: number;
|
|
157
|
+
/** Hex-encoded 32-byte Ed25519 public key for signing deploy commits.
|
|
158
|
+
* Included in agent.deploy frames so sidecars can verify pack signatures. */
|
|
159
|
+
hubPublicKey?: string;
|
|
160
|
+
/** Resolves each register/reconnect handshake to a verified sidecar
|
|
161
|
+
* identity. Required: without it a connection could route on an
|
|
162
|
+
* unverified frame claim. Return null to reject the handshake. */
|
|
163
|
+
authenticateSidecar: SidecarAuthenticator;
|
|
164
|
+
challengeTimeoutMs?: number;
|
|
165
|
+
disconnectQueueMaxSize?: number;
|
|
166
|
+
disconnectQueueTTLMs?: number;
|
|
167
|
+
pingTimeoutMs?: number;
|
|
168
|
+
/** Query handlers the wire layer issues during frame processing.
|
|
169
|
+
* Each lookup is one-handler-returns-a-value; for multi-subscriber
|
|
170
|
+
* notifications use `router.events.on(...)` instead.
|
|
171
|
+
*
|
|
172
|
+
* `lookupDeployRef` and the `deploy.ref.stale` event are paired by
|
|
173
|
+
* convention: the wire layer only issues the staleness comparison
|
|
174
|
+
* when the lookup is set, and only emits the event on a confirmed
|
|
175
|
+
* mismatch. The host is responsible for subscribing a listener
|
|
176
|
+
* whenever the lookup is provided; the router does not enforce
|
|
177
|
+
* the pairing. */
|
|
178
|
+
lookups?: SidecarLookups;
|
|
179
|
+
};
|
|
180
|
+
export type WsHandle = {
|
|
181
|
+
send(data: string): void;
|
|
182
|
+
close(): void;
|
|
183
|
+
};
|
|
184
|
+
export declare function createSidecarRouter(config: SidecarRouterConfig): SidecarRouter;
|