@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.
Files changed (87) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +84 -1
  3. package/dist/agent-repo.d.ts +89 -0
  4. package/dist/agent-repo.js +109 -0
  5. package/dist/agent-state-kind.d.ts +12 -0
  6. package/dist/agent-state-kind.js +185 -0
  7. package/dist/asset-service.d.ts +123 -0
  8. package/dist/asset-service.js +349 -0
  9. package/dist/available-skills-stanza.d.ts +21 -0
  10. package/dist/available-skills-stanza.js +32 -0
  11. package/dist/credential-push.d.ts +32 -0
  12. package/dist/credential-push.js +85 -0
  13. package/dist/event-collector-registry.d.ts +20 -0
  14. package/dist/event-collector-registry.js +115 -0
  15. package/dist/event-collector.d.ts +39 -0
  16. package/dist/event-collector.js +357 -0
  17. package/dist/hub-session-lookups.d.ts +17 -0
  18. package/dist/hub-session-lookups.js +204 -0
  19. package/dist/hub-session-orchestrator.d.ts +25 -0
  20. package/dist/hub-session-orchestrator.js +122 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.js +16 -0
  23. package/dist/package-registry-kind.d.ts +70 -0
  24. package/dist/package-registry-kind.js +260 -0
  25. package/dist/repo-store/index.d.ts +4 -0
  26. package/dist/repo-store/index.js +3 -0
  27. package/dist/repo-store/store.d.ts +41 -0
  28. package/dist/repo-store/store.js +1692 -0
  29. package/dist/repo-store/subscribe-kind.d.ts +53 -0
  30. package/dist/repo-store/subscribe-kind.js +179 -0
  31. package/dist/repo-store/types.d.ts +483 -0
  32. package/dist/repo-store/types.js +42 -0
  33. package/dist/session-service.d.ts +235 -0
  34. package/dist/session-service.js +997 -0
  35. package/dist/skill-kind.d.ts +41 -0
  36. package/dist/skill-kind.js +288 -0
  37. package/dist/substrate.d.ts +8 -0
  38. package/dist/substrate.js +21 -0
  39. package/dist/workflow-kind.d.ts +21 -0
  40. package/dist/workflow-kind.js +263 -0
  41. package/dist/workflow-run-event-log.d.ts +21 -0
  42. package/dist/workflow-run-event-log.js +51 -0
  43. package/dist/workflow-run-kind.d.ts +326 -0
  44. package/dist/workflow-run-kind.js +2646 -0
  45. package/dist/workflow-run-reader.d.ts +47 -0
  46. package/dist/workflow-run-reader.js +157 -0
  47. package/dist/ws/index.d.ts +3 -0
  48. package/dist/ws/index.js +3 -0
  49. package/dist/ws/sidecar-events.d.ts +134 -0
  50. package/dist/ws/sidecar-events.js +70 -0
  51. package/dist/ws/sidecar-handler.d.ts +184 -0
  52. package/dist/ws/sidecar-handler.js +1603 -0
  53. package/dist/ws/sidecar-token-authenticator.d.ts +15 -0
  54. package/dist/ws/sidecar-token-authenticator.js +24 -0
  55. package/package.json +34 -12
  56. package/src/agent-repo.test.ts +0 -310
  57. package/src/agent-repo.ts +0 -165
  58. package/src/agent-state-kind.test.ts +0 -247
  59. package/src/agent-state-kind.ts +0 -204
  60. package/src/asset-service.test.ts +0 -540
  61. package/src/asset-service.ts +0 -378
  62. package/src/available-skills-stanza.test.ts +0 -87
  63. package/src/available-skills-stanza.ts +0 -47
  64. package/src/credential-push.ts +0 -65
  65. package/src/event-collector-registry.test.ts +0 -73
  66. package/src/event-collector-registry.ts +0 -171
  67. package/src/event-collector.test.ts +0 -1387
  68. package/src/event-collector.ts +0 -424
  69. package/src/hub-session-lookups.ts +0 -206
  70. package/src/hub-session-orchestrator.test.ts +0 -510
  71. package/src/hub-session-orchestrator.ts +0 -213
  72. package/src/index.ts +0 -78
  73. package/src/repo-store/index.ts +0 -15
  74. package/src/repo-store/store.test.ts +0 -1169
  75. package/src/repo-store/store.ts +0 -428
  76. package/src/repo-store/types.ts +0 -253
  77. package/src/session-service.test.ts +0 -895
  78. package/src/session-service.ts +0 -464
  79. package/src/skill-kind.test.ts +0 -599
  80. package/src/skill-kind.ts +0 -350
  81. package/src/ws/index.ts +0 -18
  82. package/src/ws/sidecar-events.test.ts +0 -96
  83. package/src/ws/sidecar-events.ts +0 -231
  84. package/src/ws/sidecar-handler.test.ts +0 -2217
  85. package/src/ws/sidecar-handler.ts +0 -1574
  86. package/tsconfig.json +0 -4
  87. 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";
@@ -0,0 +1,3 @@
1
+ export { createSidecarRouter, } from "./sidecar-handler.js";
2
+ export { createSidecarTokenAuthenticator, } from "./sidecar-token-authenticator.js";
3
+ export { createSidecarEmitter, } from "./sidecar-events.js";
@@ -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;