@intx/hub-sessions 0.2.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.
- package/README.md +3 -5
- package/dist/agent-repo.d.ts +9 -5
- package/dist/agent-repo.js +2 -2
- package/dist/agent-state-kind.js +4 -0
- package/dist/asset-service.d.ts +1 -20
- package/dist/asset-service.js +9 -91
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +7 -6
- package/dist/credential-push.js +42 -18
- package/dist/event-collector-registry.d.ts +1 -1
- package/dist/event-collector-registry.js +4 -4
- package/dist/event-collector.d.ts +1 -1
- package/dist/event-collector.js +10 -2
- package/dist/hub-session-lookups.d.ts +125 -7
- package/dist/hub-session-lookups.js +539 -80
- package/dist/hub-session-orchestrator.js +14 -49
- package/dist/index.d.ts +17 -8
- package/dist/index.js +14 -6
- package/dist/repo-store/index.d.ts +1 -1
- package/dist/repo-store/store.d.ts +1 -1
- package/dist/repo-store/store.js +138 -1
- package/dist/repo-store/subscribe-kind.d.ts +6 -3
- package/dist/repo-store/subscribe-kind.js +42 -77
- package/dist/repo-store/types.d.ts +94 -6
- package/dist/session-service.d.ts +277 -96
- package/dist/session-service.js +741 -547
- package/dist/sidecar-allocation/contracts.d.ts +78 -0
- package/dist/sidecar-allocation/contracts.js +21 -0
- package/dist/sidecar-allocation/index.d.ts +4 -0
- package/dist/sidecar-allocation/index.js +3 -0
- package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
- package/dist/sidecar-allocation/placement-policy.js +21 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
- package/dist/sidecar-allocation/plugin-registry.js +37 -0
- package/dist/sidecar-allocation/reconciler.d.ts +42 -0
- package/dist/sidecar-allocation/reconciler.js +431 -0
- package/dist/skill-kind.js +4 -0
- package/dist/substrate.d.ts +3 -3
- package/dist/substrate.js +1 -1
- package/dist/workflow-allocation-service.d.ts +58 -0
- package/dist/workflow-allocation-service.js +239 -0
- package/dist/workflow-closure-resolution.d.ts +106 -0
- package/dist/workflow-closure-resolution.js +123 -0
- package/dist/workflow-definition-ensure.d.ts +24 -0
- package/dist/workflow-definition-ensure.js +75 -0
- package/dist/workflow-dispatch-service.d.ts +40 -0
- package/dist/workflow-dispatch-service.js +146 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +17 -1
- package/dist/workflow-kind.js +127 -80
- package/dist/workflow-probe-gate.d.ts +214 -0
- package/dist/workflow-probe-gate.js +207 -0
- package/dist/workflow-run-kind.d.ts +128 -14
- package/dist/workflow-run-kind.js +353 -83
- package/dist/workflow-run-reader.d.ts +1 -1
- package/dist/workflow-run-reader.js +3 -7
- package/dist/workflow-run-restore.d.ts +15 -0
- package/dist/workflow-run-restore.js +26 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/ws/index.d.ts +3 -3
- package/dist/ws/index.js +1 -1
- package/dist/ws/sidecar-events.d.ts +100 -12
- package/dist/ws/sidecar-events.js +2 -0
- package/dist/ws/sidecar-handler.d.ts +128 -7
- package/dist/ws/sidecar-handler.js +1069 -135
- package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
- package/dist/ws/sidecar-token-authenticator.js +64 -7
- package/package.json +14 -13
- package/dist/available-skills-stanza.d.ts +0 -21
- package/dist/available-skills-stanza.js +0 -32
|
@@ -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
|
+
}
|
package/dist/ws/index.d.ts
CHANGED
|
@@ -1,3 +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";
|
|
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";
|
package/dist/ws/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { createSidecarRouter, } from "./sidecar-handler.js";
|
|
2
|
-
export { createSidecarTokenAuthenticator, } from "./sidecar-token-authenticator.js";
|
|
2
|
+
export { createSidecarCredentialResolver, createSidecarTokenAuthenticator, } from "./sidecar-token-authenticator.js";
|
|
3
3
|
export { createSidecarEmitter, } from "./sidecar-events.js";
|
|
@@ -1,15 +1,44 @@
|
|
|
1
|
-
import type { PackRejectReason, RepoId } from "@intx/types/sidecar";
|
|
2
|
-
import type { ConnectorThreadState } from "@intx/types/runtime";
|
|
1
|
+
import type { PackRejectReason, RepoId, RunGrantsFrame } from "@intx/types/sidecar";
|
|
2
|
+
import type { ApprovalSnapshot, ConnectorThreadState } from "@intx/types/runtime";
|
|
3
|
+
import type { SignalKind } from "@intx/types";
|
|
3
4
|
export type SidecarMailPersistedRow = {
|
|
4
5
|
id: string;
|
|
5
6
|
createdAt: Date;
|
|
6
7
|
direction: "inbound" | "outbound";
|
|
7
|
-
|
|
8
|
+
runId: string | null;
|
|
8
9
|
address: string;
|
|
9
10
|
};
|
|
10
11
|
export type SidecarMailPersistedPayload = SidecarMailPersistedRow & {
|
|
11
12
|
raw: Uint8Array;
|
|
12
13
|
};
|
|
14
|
+
/** Authenticated connection scope attached to a workflow-run pack. */
|
|
15
|
+
export type WorkflowRunPackSource = {
|
|
16
|
+
readonly kind: "shared";
|
|
17
|
+
readonly agentAddress: string;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "allocated";
|
|
20
|
+
readonly agentAddress: string;
|
|
21
|
+
readonly allocationId: string;
|
|
22
|
+
readonly anchorRunId: string;
|
|
23
|
+
readonly generation: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Outcome of reserving a mail-triggered run's grants. `skip` means the
|
|
27
|
+
* recipient names no workflow deployment; `rejected` means the deployment's
|
|
28
|
+
* stable run is terminal or its requirements cannot be authorized;
|
|
29
|
+
* `materialized` carries the canonical persisted wire rows.
|
|
30
|
+
*/
|
|
31
|
+
export type MailTriggeredRunGrantsResult = {
|
|
32
|
+
outcome: "skip";
|
|
33
|
+
} | {
|
|
34
|
+
outcome: "rejected";
|
|
35
|
+
status: 403 | 409;
|
|
36
|
+
code: string;
|
|
37
|
+
message: string;
|
|
38
|
+
} | {
|
|
39
|
+
outcome: "materialized";
|
|
40
|
+
stepGrants: RunGrantsFrame["stepGrants"];
|
|
41
|
+
};
|
|
13
42
|
export type SidecarEventMap = {
|
|
14
43
|
/** Notification. Emitted for every agent.event frame the wire layer
|
|
15
44
|
* decodes. The wire layer also forwards the event to in-process agent
|
|
@@ -26,6 +55,16 @@ export type SidecarEventMap = {
|
|
|
26
55
|
* alike -- so lifecycle teardown covers both. */
|
|
27
56
|
"sidecar.disconnect": {
|
|
28
57
|
ownedAddresses: string[];
|
|
58
|
+
/** Present only when the closing socket was the current allocated owner. */
|
|
59
|
+
allocated?: {
|
|
60
|
+
allocationId: string;
|
|
61
|
+
generation: number;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/** Notification after the exact authenticated allocation generation registers. */
|
|
65
|
+
"sidecar.allocated.connected": {
|
|
66
|
+
allocationId: string;
|
|
67
|
+
generation: number;
|
|
29
68
|
};
|
|
30
69
|
/** Notification. Emitted when a mail.outbound frame from a sidecar
|
|
31
70
|
* names recipients that the wire layer could not deliver locally and
|
|
@@ -40,11 +79,30 @@ export type SidecarEventMap = {
|
|
|
40
79
|
* the rows; this event fires for each so subscribers can react
|
|
41
80
|
* per-row (e.g. dispatch a delivered event). */
|
|
42
81
|
"mail.persisted": SidecarMailPersistedPayload;
|
|
82
|
+
/** Notification after a sidecar confirms a mail trigger is in its durable
|
|
83
|
+
* local inbox. For an exclusive worker, `allocated` identifies the exact
|
|
84
|
+
* generation that acknowledged the message. This is not workflow
|
|
85
|
+
* settlement; the Hub retains the payload until the Git claim-check records
|
|
86
|
+
* consumption. */
|
|
87
|
+
"mail.inbound.acknowledged": {
|
|
88
|
+
agentAddress: string;
|
|
89
|
+
messageId: string;
|
|
90
|
+
allocated?: {
|
|
91
|
+
allocationId: string;
|
|
92
|
+
anchorRunId: string;
|
|
93
|
+
generation: number;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
43
96
|
/** Awaited. Emitted when an agent.deploy.ack frame arrives. Rejection
|
|
44
97
|
* fails the pending deploy with the listener's error. */
|
|
45
98
|
"agent.deploy.ack": {
|
|
46
99
|
agentAddress: string;
|
|
47
100
|
publicKey: string;
|
|
101
|
+
allocated?: {
|
|
102
|
+
allocationId: string;
|
|
103
|
+
anchorRunId: string;
|
|
104
|
+
generation: number;
|
|
105
|
+
};
|
|
48
106
|
};
|
|
49
107
|
/** Notification. Emitted when the sidecar reports a change to an
|
|
50
108
|
* agent's connector-thread state. The wire layer caches the state
|
|
@@ -105,9 +163,42 @@ export type SidecarLookups = {
|
|
|
105
163
|
recipients: string[];
|
|
106
164
|
raw: Uint8Array;
|
|
107
165
|
}) => Promise<SidecarMailPersistedRow[]>;
|
|
166
|
+
/** Co-writes the `signal_correlation` routing row and the `approval` row
|
|
167
|
+
* for a suspending workflow agent step, in one transaction. Called from
|
|
168
|
+
* the `signal.correlation.register` frame handler after the wire layer has
|
|
169
|
+
* confirmed the sending sidecar owns `agentAddress`. Idempotent: a
|
|
170
|
+
* redelivered frame (reconnect, workflow-log replay, supervisor restart
|
|
171
|
+
* re-emitting) is a no-op, not an error. The wire layer does not carry
|
|
172
|
+
* `signalName`; the host derives it from `correlationId`. Resolves the
|
|
173
|
+
* tenancy from the workflow deployment the address names. */
|
|
174
|
+
registerSignalCorrelation?: (args: {
|
|
175
|
+
correlationId: string;
|
|
176
|
+
runId: string;
|
|
177
|
+
anchorRunId: string;
|
|
178
|
+
agentAddress: string;
|
|
179
|
+
kind: SignalKind;
|
|
180
|
+
approvalSnapshot: ApprovalSnapshot;
|
|
181
|
+
}) => Promise<void>;
|
|
182
|
+
/** Reserves a mail-triggered workflow run's grants from the receiving
|
|
183
|
+
* deployment's definition, returning a discriminated result the
|
|
184
|
+
* `mail.outbound` handler orders against delivery. Called for each recipient
|
|
185
|
+
* that is a workflow deployment. The `runId` is the deployment's stable
|
|
186
|
+
* address-derived run id.
|
|
187
|
+
*
|
|
188
|
+
* On `materialized`, `stepGrants` are already persisted and the caller sends
|
|
189
|
+
* them ahead of the inbound mail. Reservation is idempotent on the runId, so
|
|
190
|
+
* a redelivered inbound mail neither double-mints nor throws. On `skip` the
|
|
191
|
+
* address names no deployed workflow deployment, so no grants are sent and
|
|
192
|
+
* the mail still forwards. On `rejected` the stable run is terminal or a
|
|
193
|
+
* declared requirement's authority is insufficient; the caller fails the
|
|
194
|
+
* mail closed for that recipient. */
|
|
195
|
+
materializeMailTriggeredRunGrants?: (args: {
|
|
196
|
+
agentAddress: string;
|
|
197
|
+
runId: string;
|
|
198
|
+
}) => Promise<MailTriggeredRunGrantsResult>;
|
|
108
199
|
/** Ingests a received agent-state pack and returns whether the wire
|
|
109
200
|
* layer should ack or reject the pack to the sidecar. `repoId.kind`
|
|
110
|
-
* is `"agent-state"` and `repoId.id` is the
|
|
201
|
+
* is `"agent-state"` and `repoId.id` is the run address. The wire
|
|
111
202
|
* layer dispatches on `repoId.kind` against the receive lookups
|
|
112
203
|
* before calling either; this lookup must reject any pack whose
|
|
113
204
|
* `repoId.kind` is not `"agent-state"`. */
|
|
@@ -118,14 +209,11 @@ export type SidecarLookups = {
|
|
|
118
209
|
reason: PackRejectReason;
|
|
119
210
|
}>;
|
|
120
211
|
/** Ingests a received workflow-run pack and returns whether the wire
|
|
121
|
-
* layer should ack or reject the pack to the sidecar. `
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
|
|
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<{
|
|
212
|
+
* layer should ack or reject the pack to the sidecar. `source` is derived
|
|
213
|
+
* from the authenticated socket, never from the frame. The lookup must
|
|
214
|
+
* revalidate that source against durable deployment/allocation ownership
|
|
215
|
+
* before advancing the Git ref. */
|
|
216
|
+
receiveWorkflowRunPack?: (repoId: RepoId, pack: Uint8Array, ref: string, commitSha: string, source: WorkflowRunPackSource) => Promise<{
|
|
129
217
|
accepted: true;
|
|
130
218
|
} | {
|
|
131
219
|
accepted: false;
|
|
@@ -24,8 +24,10 @@ export function createSidecarEmitter() {
|
|
|
24
24
|
const listeners = {
|
|
25
25
|
"agent.event": new Set(),
|
|
26
26
|
"sidecar.disconnect": new Set(),
|
|
27
|
+
"sidecar.allocated.connected": new Set(),
|
|
27
28
|
"mail.outbound.undelivered": new Set(),
|
|
28
29
|
"mail.persisted": new Set(),
|
|
30
|
+
"mail.inbound.acknowledged": new Set(),
|
|
29
31
|
"agent.deploy.ack": new Set(),
|
|
30
32
|
"agent.reconnected": new Set(),
|
|
31
33
|
"deploy.ref.stale": new Set(),
|