@intx/hub-sessions 0.1.2 → 0.3.0

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 (113) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +82 -1
  3. package/dist/agent-repo.d.ts +93 -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 +189 -0
  7. package/dist/asset-service.d.ts +104 -0
  8. package/dist/asset-service.js +267 -0
  9. package/dist/committed-source-tree.d.ts +10 -0
  10. package/dist/committed-source-tree.js +35 -0
  11. package/dist/credential-push.d.ts +33 -0
  12. package/dist/credential-push.js +109 -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 +365 -0
  17. package/dist/hub-session-lookups.d.ts +135 -0
  18. package/dist/hub-session-lookups.js +663 -0
  19. package/dist/hub-session-orchestrator.d.ts +25 -0
  20. package/dist/hub-session-orchestrator.js +87 -0
  21. package/dist/index.d.ts +27 -0
  22. package/dist/index.js +24 -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 +1829 -0
  29. package/dist/repo-store/subscribe-kind.d.ts +56 -0
  30. package/dist/repo-store/subscribe-kind.js +144 -0
  31. package/dist/repo-store/types.d.ts +571 -0
  32. package/dist/repo-store/types.js +42 -0
  33. package/dist/session-service.d.ts +416 -0
  34. package/dist/session-service.js +1191 -0
  35. package/dist/sidecar-allocation/contracts.d.ts +78 -0
  36. package/dist/sidecar-allocation/contracts.js +21 -0
  37. package/dist/sidecar-allocation/index.d.ts +4 -0
  38. package/dist/sidecar-allocation/index.js +3 -0
  39. package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
  40. package/dist/sidecar-allocation/placement-policy.js +21 -0
  41. package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
  42. package/dist/sidecar-allocation/plugin-registry.js +37 -0
  43. package/dist/sidecar-allocation/reconciler.d.ts +42 -0
  44. package/dist/sidecar-allocation/reconciler.js +431 -0
  45. package/dist/skill-kind.d.ts +41 -0
  46. package/dist/skill-kind.js +292 -0
  47. package/dist/substrate.d.ts +8 -0
  48. package/dist/substrate.js +21 -0
  49. package/dist/workflow-allocation-service.d.ts +58 -0
  50. package/dist/workflow-allocation-service.js +239 -0
  51. package/dist/workflow-closure-resolution.d.ts +106 -0
  52. package/dist/workflow-closure-resolution.js +123 -0
  53. package/dist/workflow-definition-ensure.d.ts +24 -0
  54. package/dist/workflow-definition-ensure.js +75 -0
  55. package/dist/workflow-dispatch-service.d.ts +40 -0
  56. package/dist/workflow-dispatch-service.js +146 -0
  57. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  58. package/dist/workflow-dispatch-settlement.js +140 -0
  59. package/dist/workflow-kind.d.ts +37 -0
  60. package/dist/workflow-kind.js +310 -0
  61. package/dist/workflow-probe-gate.d.ts +214 -0
  62. package/dist/workflow-probe-gate.js +207 -0
  63. package/dist/workflow-run-event-log.d.ts +21 -0
  64. package/dist/workflow-run-event-log.js +51 -0
  65. package/dist/workflow-run-kind.d.ts +440 -0
  66. package/dist/workflow-run-kind.js +2916 -0
  67. package/dist/workflow-run-reader.d.ts +47 -0
  68. package/dist/workflow-run-reader.js +153 -0
  69. package/dist/workflow-run-restore.d.ts +15 -0
  70. package/dist/workflow-run-restore.js +26 -0
  71. package/dist/workflow-source-closure.d.ts +35 -0
  72. package/dist/workflow-source-closure.js +342 -0
  73. package/dist/ws/index.d.ts +3 -0
  74. package/dist/ws/index.js +3 -0
  75. package/dist/ws/sidecar-events.d.ts +222 -0
  76. package/dist/ws/sidecar-events.js +72 -0
  77. package/dist/ws/sidecar-handler.d.ts +305 -0
  78. package/dist/ws/sidecar-handler.js +2537 -0
  79. package/dist/ws/sidecar-token-authenticator.d.ts +17 -0
  80. package/dist/ws/sidecar-token-authenticator.js +81 -0
  81. package/package.json +35 -12
  82. package/src/agent-repo.test.ts +0 -310
  83. package/src/agent-repo.ts +0 -165
  84. package/src/agent-state-kind.test.ts +0 -247
  85. package/src/agent-state-kind.ts +0 -204
  86. package/src/asset-service.test.ts +0 -540
  87. package/src/asset-service.ts +0 -378
  88. package/src/available-skills-stanza.test.ts +0 -87
  89. package/src/available-skills-stanza.ts +0 -47
  90. package/src/credential-push.ts +0 -65
  91. package/src/event-collector-registry.test.ts +0 -73
  92. package/src/event-collector-registry.ts +0 -171
  93. package/src/event-collector.test.ts +0 -1387
  94. package/src/event-collector.ts +0 -424
  95. package/src/hub-session-lookups.ts +0 -206
  96. package/src/hub-session-orchestrator.test.ts +0 -510
  97. package/src/hub-session-orchestrator.ts +0 -213
  98. package/src/index.ts +0 -78
  99. package/src/repo-store/index.ts +0 -15
  100. package/src/repo-store/store.test.ts +0 -1169
  101. package/src/repo-store/store.ts +0 -428
  102. package/src/repo-store/types.ts +0 -253
  103. package/src/session-service.test.ts +0 -895
  104. package/src/session-service.ts +0 -464
  105. package/src/skill-kind.test.ts +0 -599
  106. package/src/skill-kind.ts +0 -350
  107. package/src/ws/index.ts +0 -18
  108. package/src/ws/sidecar-events.test.ts +0 -96
  109. package/src/ws/sidecar-events.ts +0 -231
  110. package/src/ws/sidecar-handler.test.ts +0 -2217
  111. package/src/ws/sidecar-handler.ts +0 -1574
  112. package/tsconfig.json +0 -4
  113. 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:<runId>` 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,153 @@
1
+ import fs from "node:fs";
2
+ import git from "isomorphic-git";
3
+ import { requireEventSeq, 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
+ export function createWorkflowRunReader(repoStore) {
6
+ function repoDirOrNull(repoId) {
7
+ try {
8
+ return repoStore.getRepoDir(repoId);
9
+ }
10
+ catch {
11
+ // getRepoDir throws only when the repoId fails the kind handler's
12
+ // slug validation; an uninitialised-but-valid repo returns a path
13
+ // that does not yet exist on disk. A validation failure here means
14
+ // the caller handed an id the substrate would never have written,
15
+ // which is indistinguishable from "no such run repo" at this read
16
+ // boundary.
17
+ return null;
18
+ }
19
+ }
20
+ async function resolveRefOrNull(dir, ref) {
21
+ try {
22
+ return await git.resolveRef({ fs, dir, ref });
23
+ }
24
+ catch (cause) {
25
+ if (cause instanceof git.Errors.NotFoundError ||
26
+ (cause instanceof Error && /ENOENT|not found/i.test(cause.message))) {
27
+ return null;
28
+ }
29
+ throw cause;
30
+ }
31
+ }
32
+ async function listRunIds(repoId, ref) {
33
+ const dir = repoDirOrNull(repoId);
34
+ if (dir === null)
35
+ return [];
36
+ const oid = await resolveRefOrNull(dir, ref);
37
+ if (oid === null)
38
+ return [];
39
+ let tree;
40
+ try {
41
+ tree = await git.readTree({
42
+ fs,
43
+ dir,
44
+ oid,
45
+ filepath: WORKFLOW_RUN_RUNS_PREFIX,
46
+ });
47
+ }
48
+ catch (cause) {
49
+ if (cause instanceof git.Errors.NotFoundError)
50
+ return [];
51
+ throw cause;
52
+ }
53
+ return tree.tree
54
+ .filter((entry) => entry.type === "tree")
55
+ .map((entry) => entry.path);
56
+ }
57
+ async function readRunEvents(repoId, ref, runId) {
58
+ const dir = repoDirOrNull(repoId);
59
+ if (dir === null)
60
+ return [];
61
+ const oid = await resolveRefOrNull(dir, ref);
62
+ if (oid === null)
63
+ return [];
64
+ const runDir = `${WORKFLOW_RUN_RUNS_PREFIX}/${runId}`;
65
+ let runTree;
66
+ try {
67
+ runTree = await git.readTree({ fs, dir, oid, filepath: runDir });
68
+ }
69
+ catch (cause) {
70
+ if (cause instanceof git.Errors.NotFoundError)
71
+ return [];
72
+ throw cause;
73
+ }
74
+ // A terminated run is sealed into a single combined `events.jsonl`;
75
+ // an in-flight run keeps per-event `events/<seq>.json` files. The two
76
+ // forms are mutually exclusive in a run directory; a run carrying both
77
+ // is a botched seal, and silently preferring one would mask it, so
78
+ // surface it instead.
79
+ const combined = runTree.tree.find((e) => e.type === "blob" && e.path === WORKFLOW_RUN_EVENTS_FILE);
80
+ const perEventDir = runTree.tree.find((e) => e.type === "tree" && e.path === WORKFLOW_RUN_EVENTS_DIR);
81
+ if (combined !== undefined && perEventDir !== undefined) {
82
+ 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`);
83
+ }
84
+ if (combined !== undefined) {
85
+ const blob = await git.readBlob({ fs, dir, oid: combined.oid });
86
+ const source = `${runDir}/${WORKFLOW_RUN_EVENTS_FILE}`;
87
+ const events = [];
88
+ for (const line of splitCombinedEventLog(new TextDecoder().decode(blob.blob))) {
89
+ events.push(parseRunEventLine(line, source));
90
+ }
91
+ events.sort((a, b) => a.seq - b.seq);
92
+ return events;
93
+ }
94
+ const eventsDir = `${runDir}/${WORKFLOW_RUN_EVENTS_DIR}`;
95
+ let tree;
96
+ try {
97
+ tree = await git.readTree({ fs, dir, oid, filepath: eventsDir });
98
+ }
99
+ catch (cause) {
100
+ if (cause instanceof git.Errors.NotFoundError)
101
+ return [];
102
+ throw cause;
103
+ }
104
+ const events = [];
105
+ for (const entry of tree.tree) {
106
+ if (entry.type !== "blob")
107
+ continue;
108
+ const path = `${eventsDir}/${entry.path}`;
109
+ const seq = requireEventSeq(entry.path, path);
110
+ const blob = await git.readBlob({ fs, dir, oid: entry.oid });
111
+ const parsed = parseEventObject(new TextDecoder().decode(blob.blob), path);
112
+ const type = parsed["type"];
113
+ if (typeof type !== "string") {
114
+ throw new Error(`workflow-run reader: event at ${path} is missing a string \`type\` field`);
115
+ }
116
+ // The per-event form carries the seq in the filename; when the body
117
+ // also carries one, the two must agree (the combined form reads the
118
+ // seq from the body, so a disagreement would make the forms diverge).
119
+ const bodySeq = parsed["seq"];
120
+ if (typeof bodySeq === "number" && bodySeq !== seq) {
121
+ throw new Error(`workflow-run reader: event at ${path} body seq ${String(bodySeq)} does not match filename seq ${String(seq)}`);
122
+ }
123
+ events.push({ seq, type, body: parsed });
124
+ }
125
+ events.sort((a, b) => a.seq - b.seq);
126
+ return events;
127
+ }
128
+ // Parse one combined-log line (the verbatim text of a former
129
+ // `events/<seq>.json` blob): the seq is read from the body, since the
130
+ // combined form drops the per-event filename that carried it.
131
+ function parseRunEventLine(line, source) {
132
+ const parsed = parseEventObject(line, source);
133
+ const seq = parsed["seq"];
134
+ const type = parsed["type"];
135
+ if (typeof seq !== "number") {
136
+ throw new Error(`workflow-run reader: event in ${source} is missing a numeric \`seq\` field`);
137
+ }
138
+ if (typeof type !== "string") {
139
+ throw new Error(`workflow-run reader: event in ${source} is missing a string \`type\` field`);
140
+ }
141
+ return { seq, type, body: parsed };
142
+ }
143
+ function parseEventObject(text, source) {
144
+ const parsed = JSON.parse(text);
145
+ if (typeof parsed !== "object" ||
146
+ parsed === null ||
147
+ Array.isArray(parsed)) {
148
+ throw new Error(`workflow-run reader: event at ${source} is not a JSON object`);
149
+ }
150
+ return { ...parsed };
151
+ }
152
+ return { listRunIds, readRunEvents };
153
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentRepoStore } from "./agent-repo.js";
2
+ import type { AllocatedSidecarTarget, SidecarAllocationRouter } from "./ws/sidecar-handler.js";
3
+ export declare const WORKFLOW_RUN_RESTORE_REFS: readonly ["refs/heads/main", "refs/heads/events"];
4
+ /**
5
+ * Replay every authoritative workflow-run ref the runtime understands onto an
6
+ * exact replacement allocation. Refs are sent sequentially and the function
7
+ * resolves only after the worker acknowledges each one, making it a barrier
8
+ * the deploy path can place before supervisor spawn.
9
+ */
10
+ export declare function restoreWorkflowRunToAllocation(args: {
11
+ agentRepoStore: AgentRepoStore;
12
+ allocationRouter: Pick<SidecarAllocationRouter, "sendWorkflowRunPackToAllocation">;
13
+ allocationTarget: AllocatedSidecarTarget;
14
+ agentAddress: string;
15
+ }): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { deriveWorkflowRunRepoId } from "@intx/workflow-deploy";
2
+ export const WORKFLOW_RUN_RESTORE_REFS = [
3
+ "refs/heads/main",
4
+ "refs/heads/events",
5
+ ];
6
+ /**
7
+ * Replay every authoritative workflow-run ref the runtime understands onto an
8
+ * exact replacement allocation. Refs are sent sequentially and the function
9
+ * resolves only after the worker acknowledges each one, making it a barrier
10
+ * the deploy path can place before supervisor spawn.
11
+ */
12
+ export async function restoreWorkflowRunToAllocation(args) {
13
+ const { agentRepoStore, allocationRouter, allocationTarget, agentAddress } = args;
14
+ const principal = { kind: "hub" };
15
+ const repoId = {
16
+ kind: "workflow-run",
17
+ id: deriveWorkflowRunRepoId(agentAddress),
18
+ };
19
+ for (const ref of WORKFLOW_RUN_RESTORE_REFS) {
20
+ const tip = await agentRepoStore.repoStore.resolveRef(principal, repoId, ref);
21
+ if (tip === null)
22
+ continue;
23
+ const pack = await agentRepoStore.repoStore.createPack(principal, repoId, ref);
24
+ await allocationRouter.sendWorkflowRunPackToAllocation(allocationTarget, agentAddress, pack.pack, pack.ref, pack.commitSha);
25
+ }
26
+ }
@@ -0,0 +1,35 @@
1
+ import { type PackumentFetcher, type RegistryConfig } from "@intx/tool-packaging";
2
+ import type { ToolPackageManifest } from "@intx/types/tool-packages";
3
+ import type { WorkflowDefinitionAssetSource } from "@intx/types/workflow-sources";
4
+ import type { CommittedTreeEntry } from "./repo-store/types.js";
5
+ /** Git-tree reads pinned to the source's commit. */
6
+ export interface SourceTreeReads {
7
+ readBlob(path: string): Promise<Uint8Array>;
8
+ listDir(path: string): Promise<CommittedTreeEntry[]>;
9
+ treeOid(path: string): Promise<string | null>;
10
+ }
11
+ export interface ResolveSourceWorkflowClosureArgs {
12
+ /** A source whose `package.format` is `"source"`. */
13
+ readonly source: WorkflowDefinitionAssetSource;
14
+ readonly reads: SourceTreeReads;
15
+ /**
16
+ * The registry name external deps are stamped with in the frozen closure.
17
+ * The sidecar resolves each entry's `source.registry` against its own
18
+ * registry map at materialization, so this MUST be a name that map is keyed
19
+ * by (the sidecar's npm registry, e.g. `"npmjs"`); an unknown name fails the
20
+ * materialization loud. The caller owns the sidecar's registry configuration,
21
+ * so it supplies the name here rather than the walk inventing one.
22
+ */
23
+ readonly registryName: string;
24
+ /** URL and credentials for the npm registry external deps resolve against. */
25
+ readonly registryConfig: RegistryConfig;
26
+ /** Test seam for external packument fetches. */
27
+ readonly fetchPackument?: PackumentFetcher;
28
+ }
29
+ /**
30
+ * Resolve a source-format workflow definition's dependency closure to a frozen
31
+ * `ToolPackageManifest`: a `format:"source"` entry for the workflow package and
32
+ * each workspace-local dependency (identity = git tree oid), plus tarball
33
+ * entries for the external npm closure.
34
+ */
35
+ export declare function resolveSourceWorkflowClosure(args: ResolveSourceWorkflowClosureArgs): Promise<ToolPackageManifest>;
@@ -0,0 +1,342 @@
1
+ // Hub-side dependency-closure resolution for a SOURCE-format workflow
2
+ // definition -- a codebase living as a subtree of a hub git asset at a
3
+ // pinned commit.
4
+ //
5
+ // Unlike the registry and tarball arms (which resolve against npm packuments
6
+ // with SRI integrity), a source package has no packument: its content
7
+ // identity is the git tree oid of its subtree, and its dependency closure is
8
+ // a DISJOINT UNION of two origins built by construction --
9
+ // - workspace-local members (other subtrees in the SAME asset) become
10
+ // `format:"source"` entries and are walked recursively, and
11
+ // - external npm dependencies are resolved through the pristine registry
12
+ // walker (which keeps its SRI invariant intact) into tarball entries.
13
+ // A member name never appears in the external set, so the two origins cannot
14
+ // collide on a workspace member; the one residual collision (an external
15
+ // transitive dep whose `name@version` equals a member's) is detected and
16
+ // fails loud rather than silently picking an origin.
17
+ import { HttpRegistrySource, createClosureResolver, } from "@intx/tool-packaging";
18
+ /**
19
+ * Resolve a source-format workflow definition's dependency closure to a frozen
20
+ * `ToolPackageManifest`: a `format:"source"` entry for the workflow package and
21
+ * each workspace-local dependency (identity = git tree oid), plus tarball
22
+ * entries for the external npm closure.
23
+ */
24
+ export async function resolveSourceWorkflowClosure(args) {
25
+ const { source, reads, registryName, registryConfig, fetchPackument } = args;
26
+ if (source.package.format !== "source") {
27
+ throw new Error("resolveSourceWorkflowClosure: source.package.format must be 'source'");
28
+ }
29
+ const { commitSha, packageName } = source.package;
30
+ const assetId = source.assetId;
31
+ const { members, rootName, catalog } = await enumerateMembers(reads);
32
+ const selected = selectMember(members, packageName);
33
+ // BFS over the reachable workspace members. A dependency classifies by NAME,
34
+ // never by parsing its range: a name that is a member recurses (a
35
+ // workspace-local edge); the workspace root's own name is an invalid target
36
+ // (the root is not a member) and fails loud regardless of how the range is
37
+ // spelled; anything else is external and resolves through the registry walker.
38
+ //
39
+ // External deps collect by name, and a second member contributing the same
40
+ // name at a DIFFERENT resolved range fails loud rather than silently
41
+ // collapsing to one pin -- the walker resolves a single range per name, so a
42
+ // silent overwrite would drop a real constraint. This is a DELIBERATE v1
43
+ // limitation: two members with drifting-but-compatible ranges (e.g. `^1.0.0`
44
+ // and `^1.2.0`) that npm/bun would reconcile to one version are rejected here,
45
+ // not intersected. Aligning the ranges across members, or declaring the dep
46
+ // once in the root `catalog`, resolves it. (Semver intersection across
47
+ // members is tracked separately as INTR-460.)
48
+ const sourceEntries = [];
49
+ const externalPins = new Map();
50
+ const seen = new Set();
51
+ const queue = [selected.name];
52
+ while (queue.length > 0) {
53
+ const memberName = queue.shift();
54
+ if (memberName === undefined || seen.has(memberName))
55
+ continue;
56
+ seen.add(memberName);
57
+ const member = members.get(memberName);
58
+ if (member === undefined) {
59
+ throw new Error(`resolveSourceWorkflowClosure: internal -- member ${JSON.stringify(memberName)} was queued but not enumerated`);
60
+ }
61
+ const treeOid = await reads.treeOid(member.packageDir);
62
+ if (treeOid === null) {
63
+ throw new Error(`resolveSourceWorkflowClosure: subtree ${JSON.stringify(member.packageDir)} not found at commit ${commitSha}`);
64
+ }
65
+ sourceEntries.push({
66
+ name: member.name,
67
+ version: member.version,
68
+ source: {
69
+ kind: "asset",
70
+ assetId,
71
+ package: {
72
+ format: "source",
73
+ commitSha,
74
+ packageDir: member.packageDir,
75
+ treeOid,
76
+ },
77
+ },
78
+ });
79
+ for (const [depName, depSpec] of Object.entries(member.dependencies)) {
80
+ if (members.has(depName)) {
81
+ queue.push(depName);
82
+ continue;
83
+ }
84
+ if (rootName !== undefined && depName === rootName) {
85
+ throw new Error(`resolveSourceWorkflowClosure: member ${JSON.stringify(member.name)} depends on the workspace root ${JSON.stringify(rootName)}, which is not a workspace member`);
86
+ }
87
+ const resolvedSpec = resolveExternalSpec(depName, depSpec, catalog);
88
+ const existing = externalPins.get(depName);
89
+ if (existing !== undefined && existing !== resolvedSpec) {
90
+ throw new Error(`resolveSourceWorkflowClosure: workspace members pin external ${JSON.stringify(depName)} at conflicting ranges ${JSON.stringify(existing)} and ${JSON.stringify(resolvedSpec)}; the closure resolves a single range per name`);
91
+ }
92
+ externalPins.set(depName, resolvedSpec);
93
+ }
94
+ }
95
+ const externalEntries = externalPins.size > 0
96
+ ? await resolveExternalClosure(externalPins, registryName, registryConfig, fetchPackument)
97
+ : [];
98
+ return {
99
+ schemaVersion: "1",
100
+ topLevel: [{ name: selected.name, version: selected.version }],
101
+ entries: mergeDisjoint(sourceEntries, externalEntries),
102
+ };
103
+ }
104
+ /**
105
+ * Read the workspace members from the asset's tree. A repo with no
106
+ * `workspaces` field is a single package rooted at the tree (the root IS the
107
+ * one member). A `workspaces` monorepo enumerates its members from the globs;
108
+ * the root is the workspace root, NOT a member -- its `name`/`catalog` inform
109
+ * dependency classification but it contributes no closure entry.
110
+ */
111
+ async function enumerateMembers(reads) {
112
+ const root = await readPackageJSON(reads, ".");
113
+ if (root.workspaces === undefined) {
114
+ // A pnpm monorepo declares its members in `pnpm-workspace.yaml`, not the
115
+ // package.json `workspaces` field, so a pnpm root reaches here looking like
116
+ // a single package. Detect that layout and fail loud rather than silently
117
+ // misread the private root as the workflow. Full pnpm support is tracked in
118
+ // INTR-461.
119
+ if (await rootHasPnpmWorkspaceFile(reads)) {
120
+ throw new Error(`resolveSourceWorkflowClosure: the asset root declares a pnpm-workspace.yaml; the pnpm workspace layout is not supported -- declare members via a package.json "workspaces" array`);
121
+ }
122
+ // Single-package: the root is the workflow package itself, so it must
123
+ // declare a name and version.
124
+ const member = requireMember(root, ".");
125
+ return {
126
+ members: new Map([[member.name, member]]),
127
+ rootName: root.name,
128
+ catalog: root.catalog,
129
+ };
130
+ }
131
+ const members = new Map();
132
+ for (const glob of root.workspaces) {
133
+ for (const packageDir of await expandWorkspaceGlob(reads, glob)) {
134
+ // A `<dir>/*` glob matches every subdirectory, but not all are packages
135
+ // (docs, fixtures, ...). Skip a directory with no package.json rather than
136
+ // fail the whole resolution, matching how bun/yarn/pnpm treat a
137
+ // non-package directory that a workspace glob happens to match. A
138
+ // directory that HAS a package.json must still parse and declare a name
139
+ // and version, so a malformed member fails loud.
140
+ if (!(await dirHasPackageJSON(reads, packageDir)))
141
+ continue;
142
+ const parsed = await readPackageJSON(reads, packageDir);
143
+ const member = requireMember(parsed, packageDir);
144
+ if (members.has(member.name)) {
145
+ throw new Error(`resolveSourceWorkflowClosure: workspace member ${JSON.stringify(member.name)} is declared by more than one package directory`);
146
+ }
147
+ members.set(member.name, member);
148
+ }
149
+ }
150
+ if (members.size === 0) {
151
+ throw new Error(`resolveSourceWorkflowClosure: workspaces ${JSON.stringify(root.workspaces)} matched no member packages`);
152
+ }
153
+ return { members, rootName: root.name, catalog: root.catalog };
154
+ }
155
+ /**
156
+ * Turn a parsed package.json at `packageDir` into a workspace member, failing
157
+ * loud if it does not declare both a string `name` and `version` (the closure
158
+ * entry it produces is keyed on `name@version`).
159
+ */
160
+ function requireMember(parsed, packageDir) {
161
+ if (parsed.name === undefined || parsed.version === undefined) {
162
+ const blobPath = packageDir === "." ? "package.json" : `${packageDir}/package.json`;
163
+ throw new Error(`resolveSourceWorkflowClosure: ${blobPath} must declare string "name" and "version"`);
164
+ }
165
+ return {
166
+ name: parsed.name,
167
+ version: parsed.version,
168
+ packageDir,
169
+ dependencies: parsed.dependencies,
170
+ };
171
+ }
172
+ /**
173
+ * Expand one `workspaces` glob to the member directories it names. Supports
174
+ * `<base>/*` (each subtree directly under `<base>`) and an exact path (no glob
175
+ * character). Any richer shape (`**`, a mid-segment `*`, braces, negation)
176
+ * fails loud rather than risk mis-enumerating the workspace.
177
+ */
178
+ async function expandWorkspaceGlob(reads, glob) {
179
+ const trimmed = glob.replace(/\/+$/, "");
180
+ if (!/[*{}!]/.test(trimmed)) {
181
+ return [trimmed];
182
+ }
183
+ const suffix = "/*";
184
+ const base = trimmed.slice(0, -suffix.length);
185
+ if (trimmed.endsWith(suffix) && !/[*{}!]/.test(base)) {
186
+ const children = await reads.listDir(base === "" ? "." : base);
187
+ return children
188
+ .filter((child) => child.type === "tree")
189
+ .map((child) => (base === "" ? child.name : `${base}/${child.name}`));
190
+ }
191
+ throw new Error(`resolveSourceWorkflowClosure: unsupported workspaces glob ${JSON.stringify(glob)}; only "<dir>/*" and exact paths are supported`);
192
+ }
193
+ /** Whether `dir` holds a `package.json` blob (i.e. is a package directory). */
194
+ async function dirHasPackageJSON(reads, dir) {
195
+ const entries = await reads.listDir(dir);
196
+ return entries.some((entry) => entry.name === "package.json" && entry.type === "blob");
197
+ }
198
+ /** Whether the tree root holds a `pnpm-workspace.yaml` blob (the pnpm layout). */
199
+ async function rootHasPnpmWorkspaceFile(reads) {
200
+ const entries = await reads.listDir(".");
201
+ return entries.some((entry) => entry.name === "pnpm-workspace.yaml" && entry.type === "blob");
202
+ }
203
+ function selectMember(members, packageName) {
204
+ if (packageName !== undefined) {
205
+ const member = members.get(packageName);
206
+ if (member === undefined) {
207
+ throw new Error(`resolveSourceWorkflowClosure: workflow member ${JSON.stringify(packageName)} is not a workspace member`);
208
+ }
209
+ return member;
210
+ }
211
+ if (members.size !== 1) {
212
+ throw new Error("resolveSourceWorkflowClosure: a monorepo source requires a packageName selector");
213
+ }
214
+ const [only] = members.values();
215
+ if (only === undefined) {
216
+ throw new Error("resolveSourceWorkflowClosure: internal -- no members");
217
+ }
218
+ return only;
219
+ }
220
+ async function readPackageJSON(reads, packageDir) {
221
+ const blobPath = packageDir === "." ? "package.json" : `${packageDir}/package.json`;
222
+ let bytes;
223
+ try {
224
+ bytes = await reads.readBlob(blobPath);
225
+ }
226
+ catch (err) {
227
+ throw new Error(`resolveSourceWorkflowClosure: could not read ${blobPath}: ${err instanceof Error ? err.message : String(err)}`);
228
+ }
229
+ let raw;
230
+ try {
231
+ raw = JSON.parse(new TextDecoder().decode(bytes));
232
+ }
233
+ catch (err) {
234
+ throw new Error(`resolveSourceWorkflowClosure: ${blobPath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
235
+ }
236
+ if (!isRecord(raw)) {
237
+ throw new Error(`resolveSourceWorkflowClosure: ${blobPath} is not an object`);
238
+ }
239
+ // `name`/`version` are optional here: a private workspace root routinely
240
+ // omits them. A member that omits either fails loud where it is turned into a
241
+ // WorkspaceMember (`requireMember`), not here.
242
+ const nameRaw = raw["name"];
243
+ const versionRaw = raw["version"];
244
+ const name = typeof nameRaw === "string" ? nameRaw : undefined;
245
+ const version = typeof versionRaw === "string" ? versionRaw : undefined;
246
+ const depsRaw = raw["dependencies"];
247
+ const dependencies = {};
248
+ if (isRecord(depsRaw)) {
249
+ for (const [k, v] of Object.entries(depsRaw)) {
250
+ if (typeof v === "string")
251
+ dependencies[k] = v;
252
+ }
253
+ }
254
+ const workspacesRaw = raw["workspaces"];
255
+ let workspaces;
256
+ if (workspacesRaw === undefined) {
257
+ workspaces = undefined;
258
+ }
259
+ else if (Array.isArray(workspacesRaw) &&
260
+ workspacesRaw.every((w) => typeof w === "string")) {
261
+ workspaces = workspacesRaw;
262
+ }
263
+ else {
264
+ throw new Error(`resolveSourceWorkflowClosure: ${blobPath} "workspaces" must be an array of glob strings; the object form ({ packages, catalog, catalogs }) is not supported`);
265
+ }
266
+ const catalogRaw = raw["catalog"];
267
+ let catalog;
268
+ if (isRecord(catalogRaw)) {
269
+ catalog = {};
270
+ for (const [k, v] of Object.entries(catalogRaw)) {
271
+ if (typeof v === "string")
272
+ catalog[k] = v;
273
+ }
274
+ }
275
+ return { name, version, dependencies, workspaces, catalog };
276
+ }
277
+ function isRecord(value) {
278
+ return typeof value === "object" && value !== null && !Array.isArray(value);
279
+ }
280
+ /**
281
+ * Translate an EXTERNAL dependency specifier (one whose name is not a workspace
282
+ * member) to a concrete npm range the registry walker can pick against:
283
+ * - `workspace:` protocol -- the name is not a member, so this is an invalid
284
+ * workspace reference; fail loud.
285
+ * - bare `catalog:` -- expand against the root `catalog` object; fail loud if
286
+ * the catalog declares no entry for this name.
287
+ * - named `catalog:<name>` -- not supported; fail loud.
288
+ * - plain range -- pass through.
289
+ */
290
+ function resolveExternalSpec(depName, spec, catalog) {
291
+ if (spec.startsWith("workspace:")) {
292
+ throw new Error(`resolveSourceWorkflowClosure: ${depName} uses ${JSON.stringify(spec)} but is not a workspace member`);
293
+ }
294
+ if (spec === "catalog:") {
295
+ const range = catalog?.[depName];
296
+ if (range === undefined) {
297
+ throw new Error(`resolveSourceWorkflowClosure: ${depName} uses the default catalog but the workspace root declares no "catalog" entry for it`);
298
+ }
299
+ return range;
300
+ }
301
+ if (spec.startsWith("catalog:")) {
302
+ throw new Error(`resolveSourceWorkflowClosure: ${depName} uses named catalog ${JSON.stringify(spec)}, which is not supported`);
303
+ }
304
+ return spec;
305
+ }
306
+ async function resolveExternalClosure(pins, registryName, registryConfig, fetchPackument) {
307
+ const registrySource = new HttpRegistrySource({
308
+ name: registryName,
309
+ config: registryConfig,
310
+ ...(fetchPackument !== undefined ? { fetchPackument } : {}),
311
+ });
312
+ const resolver = createClosureResolver({
313
+ registries: new Map([[registryName, registrySource]]),
314
+ defaultRegistry: registryName,
315
+ });
316
+ const rootPins = [...pins].map(([name, version]) => ({
317
+ name,
318
+ version,
319
+ }));
320
+ const manifest = await resolver.resolveClosure(rootPins);
321
+ return [...manifest.entries];
322
+ }
323
+ /**
324
+ * Union the source and external entries, deduped on `name@version`. A key that
325
+ * appears in BOTH origins is an unrepresentable collision (the manifest keys on
326
+ * `name@version` with neither treeOid nor SRI participating), so fail loud
327
+ * rather than silently picking one origin.
328
+ */
329
+ function mergeDisjoint(sourceEntries, externalEntries) {
330
+ const byKey = new Map();
331
+ for (const entry of sourceEntries) {
332
+ byKey.set(`${entry.name}@${entry.version}`, entry);
333
+ }
334
+ for (const entry of externalEntries) {
335
+ const key = `${entry.name}@${entry.version}`;
336
+ if (byKey.has(key)) {
337
+ throw new Error(`resolveSourceWorkflowClosure: ${key} resolved from both a workspace member and an external registry; the closure cannot represent both origins`);
338
+ }
339
+ byKey.set(key, entry);
340
+ }
341
+ return [...byKey.values()];
342
+ }
@@ -0,0 +1,3 @@
1
+ export { createSidecarRouter, type SidecarRouter, type SidecarRouterConfig, type SidecarConnection, type SidecarAuthIdentity, type SidecarAuthenticator, type AllocatedSidecarTarget, type SidecarAllocationRouter, type SendPackOptions, type WsHandle, } from "./sidecar-handler.js";
2
+ export { createSidecarCredentialResolver, 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, type MailTriggeredRunGrantsResult, type WorkflowRunPackSource, } from "./sidecar-events.js";
@@ -0,0 +1,3 @@
1
+ export { createSidecarRouter, } from "./sidecar-handler.js";
2
+ export { createSidecarCredentialResolver, createSidecarTokenAuthenticator, } from "./sidecar-token-authenticator.js";
3
+ export { createSidecarEmitter, } from "./sidecar-events.js";