@intx/hub-sessions 0.1.2 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/README.md +84 -1
- package/dist/agent-repo.d.ts +89 -0
- package/dist/agent-repo.js +109 -0
- package/dist/agent-state-kind.d.ts +12 -0
- package/dist/agent-state-kind.js +185 -0
- package/dist/asset-service.d.ts +123 -0
- package/dist/asset-service.js +349 -0
- package/dist/available-skills-stanza.d.ts +21 -0
- package/dist/available-skills-stanza.js +32 -0
- package/dist/credential-push.d.ts +32 -0
- package/dist/credential-push.js +85 -0
- package/dist/event-collector-registry.d.ts +20 -0
- package/dist/event-collector-registry.js +115 -0
- package/dist/event-collector.d.ts +39 -0
- package/dist/event-collector.js +357 -0
- package/dist/hub-session-lookups.d.ts +17 -0
- package/dist/hub-session-lookups.js +204 -0
- package/dist/hub-session-orchestrator.d.ts +25 -0
- package/dist/hub-session-orchestrator.js +122 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +16 -0
- package/dist/package-registry-kind.d.ts +70 -0
- package/dist/package-registry-kind.js +260 -0
- package/dist/repo-store/index.d.ts +4 -0
- package/dist/repo-store/index.js +3 -0
- package/dist/repo-store/store.d.ts +41 -0
- package/dist/repo-store/store.js +1692 -0
- package/dist/repo-store/subscribe-kind.d.ts +53 -0
- package/dist/repo-store/subscribe-kind.js +179 -0
- package/dist/repo-store/types.d.ts +483 -0
- package/dist/repo-store/types.js +42 -0
- package/dist/session-service.d.ts +235 -0
- package/dist/session-service.js +997 -0
- package/dist/skill-kind.d.ts +41 -0
- package/dist/skill-kind.js +288 -0
- package/dist/substrate.d.ts +8 -0
- package/dist/substrate.js +21 -0
- package/dist/workflow-kind.d.ts +21 -0
- package/dist/workflow-kind.js +263 -0
- package/dist/workflow-run-event-log.d.ts +21 -0
- package/dist/workflow-run-event-log.js +51 -0
- package/dist/workflow-run-kind.d.ts +326 -0
- package/dist/workflow-run-kind.js +2646 -0
- package/dist/workflow-run-reader.d.ts +47 -0
- package/dist/workflow-run-reader.js +157 -0
- package/dist/ws/index.d.ts +3 -0
- package/dist/ws/index.js +3 -0
- package/dist/ws/sidecar-events.d.ts +134 -0
- package/dist/ws/sidecar-events.js +70 -0
- package/dist/ws/sidecar-handler.d.ts +184 -0
- package/dist/ws/sidecar-handler.js +1603 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +15 -0
- package/dist/ws/sidecar-token-authenticator.js +24 -0
- package/package.json +34 -12
- package/src/agent-repo.test.ts +0 -310
- package/src/agent-repo.ts +0 -165
- package/src/agent-state-kind.test.ts +0 -247
- package/src/agent-state-kind.ts +0 -204
- package/src/asset-service.test.ts +0 -540
- package/src/asset-service.ts +0 -378
- package/src/available-skills-stanza.test.ts +0 -87
- package/src/available-skills-stanza.ts +0 -47
- package/src/credential-push.ts +0 -65
- package/src/event-collector-registry.test.ts +0 -73
- package/src/event-collector-registry.ts +0 -171
- package/src/event-collector.test.ts +0 -1387
- package/src/event-collector.ts +0 -424
- package/src/hub-session-lookups.ts +0 -206
- package/src/hub-session-orchestrator.test.ts +0 -510
- package/src/hub-session-orchestrator.ts +0 -213
- package/src/index.ts +0 -78
- package/src/repo-store/index.ts +0 -15
- package/src/repo-store/store.test.ts +0 -1169
- package/src/repo-store/store.ts +0 -428
- package/src/repo-store/types.ts +0 -253
- package/src/session-service.test.ts +0 -895
- package/src/session-service.ts +0 -464
- package/src/skill-kind.test.ts +0 -599
- package/src/skill-kind.ts +0 -350
- package/src/ws/index.ts +0 -18
- package/src/ws/sidecar-events.test.ts +0 -96
- package/src/ws/sidecar-events.ts +0 -231
- package/src/ws/sidecar-handler.test.ts +0 -2217
- package/src/ws/sidecar-handler.ts +0 -1574
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// KindHandler for the `package-registry` asset kind.
|
|
2
|
+
//
|
|
3
|
+
// A package-registry asset is a git repo whose top-level tree holds:
|
|
4
|
+
//
|
|
5
|
+
// - `tarballs/<filename>.tgz` — one or more npm-style tarballs. Each
|
|
6
|
+
// tarball is opened during validation; its inner `package.json`
|
|
7
|
+
// must validate against `PackageJSON`. Anything else under
|
|
8
|
+
// `tarballs/` is rejected.
|
|
9
|
+
// - `package-registry.json` (optional) — a hub-authored index. The
|
|
10
|
+
// substrate does not enforce its shape today; the resolver layer
|
|
11
|
+
// consumes it when present.
|
|
12
|
+
// - `.gitignore` — supplied by the asset routes' genesis init body.
|
|
13
|
+
//
|
|
14
|
+
// Any top-level entry outside this set fails the push.
|
|
15
|
+
import { getLogger } from "@intx/log";
|
|
16
|
+
import { extractTarballPackageJSON } from "@intx/tool-packaging";
|
|
17
|
+
import {} from "./repo-store/index.js";
|
|
18
|
+
const logger = getLogger(["hub-sessions", "package-registry-kind"]);
|
|
19
|
+
export const TARBALLS_PREFIX = "tarballs/";
|
|
20
|
+
export const REGISTRY_INDEX_PATH = "package-registry.json";
|
|
21
|
+
/**
|
|
22
|
+
* Canonical asset name for the workspace's bundled package-registry —
|
|
23
|
+
* the in-tree `@intx/tools-*` packages live in an asset of this name
|
|
24
|
+
* under the workspace's root tenant. The hub's scope-routing config
|
|
25
|
+
* maps `@intx` to this registry, the seed script ensures the asset
|
|
26
|
+
* row exists, and the publish-tool-packages CLI defaults its target
|
|
27
|
+
* registry to this name. The constant lives at one site so a rename
|
|
28
|
+
* does not have to chase three independent string literals.
|
|
29
|
+
*
|
|
30
|
+
* Callers:
|
|
31
|
+
* - `bin/dev.ts` — orchestrator default for the registry the dev
|
|
32
|
+
* stack publishes the built-ins into.
|
|
33
|
+
* - `bin/seed.ts` — seeder that pins the workspace-builtins into
|
|
34
|
+
* the registry asset at boot.
|
|
35
|
+
* - `bin/publish-tool-packages.ts` — CLI default for the
|
|
36
|
+
* `--registry` flag.
|
|
37
|
+
*
|
|
38
|
+
* No test asserts that the seed's pinned built-ins actually land
|
|
39
|
+
* under this exact name; a mismatch between the constant and a
|
|
40
|
+
* caller would surface at apply time as a `tarball.missing`
|
|
41
|
+
* structured failure, not as a build error.
|
|
42
|
+
*/
|
|
43
|
+
export const WORKSPACE_BUILTINS_REGISTRY = "workspace-builtins";
|
|
44
|
+
/**
|
|
45
|
+
* Filename rule for tarballs in the repo tree. Filenames must end in
|
|
46
|
+
* `.tgz`, start with an alphanumeric / underscore / scope-marker
|
|
47
|
+
* character, and otherwise contain only filename-safe characters. The
|
|
48
|
+
* leading-character constraint forbids hidden-style names like
|
|
49
|
+
* `..tgz` or `.hidden.tgz` that the filesystem treats as dotfiles —
|
|
50
|
+
* those would be invisible to a `readdir` shell expansion and split
|
|
51
|
+
* the resolver's view of the registry tree from the operator's. The
|
|
52
|
+
* same rule is enforced at the REST upload boundary so a push and a
|
|
53
|
+
* PUT cannot produce diverging contents.
|
|
54
|
+
*/
|
|
55
|
+
export const TARBALL_FILENAME_PATTERN = /^[A-Za-z0-9_@+][A-Za-z0-9_.@+-]*\.tgz$/;
|
|
56
|
+
/**
|
|
57
|
+
* Validates that `path` belongs to a tarball entry shape
|
|
58
|
+
* `tarballs/<filename>.tgz`. Returns the bare filename when valid,
|
|
59
|
+
* `null` otherwise.
|
|
60
|
+
*/
|
|
61
|
+
export function asTarballEntry(path) {
|
|
62
|
+
if (!path.startsWith(TARBALLS_PREFIX))
|
|
63
|
+
return null;
|
|
64
|
+
const filename = path.slice(TARBALLS_PREFIX.length);
|
|
65
|
+
if (filename.length === 0)
|
|
66
|
+
return null;
|
|
67
|
+
if (filename.includes("/"))
|
|
68
|
+
return null;
|
|
69
|
+
if (!TARBALL_FILENAME_PATTERN.test(filename))
|
|
70
|
+
return null;
|
|
71
|
+
return filename;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Open an npm-style tarball, find the top-level `package.json` entry,
|
|
75
|
+
* parse and validate it. Returns the typed descriptor on success; a
|
|
76
|
+
* structured reason otherwise. The shape validation lives inside
|
|
77
|
+
* `extractTarballPackageJSON`; this wrapper only translates the
|
|
78
|
+
* outcome into the substrate's `ValidatePushResult`-shaped reason
|
|
79
|
+
* strings.
|
|
80
|
+
*/
|
|
81
|
+
export async function validateTarballPackageJSON(filename, bytes) {
|
|
82
|
+
const outcome = await extractTarballPackageJSON(bytes);
|
|
83
|
+
if (outcome.kind === "missing-entry") {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: `tarball ${filename} has no top-level package.json entry`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (outcome.kind === "multiple-entries") {
|
|
90
|
+
// The hub validates the first top-level package.json the tar walk
|
|
91
|
+
// emits, but the sidecar's `tar.extract({ strip: 1 })` overwrites
|
|
92
|
+
// on every subsequent path with the same stripped name and ends up
|
|
93
|
+
// loading the LAST entry. A tarball carrying more than one
|
|
94
|
+
// `<seg>/package.json` therefore would have its hub-side validation
|
|
95
|
+
// and sidecar-side runtime read different descriptors — exactly
|
|
96
|
+
// the kind of TOCTOU gap a single signed integrity hash cannot
|
|
97
|
+
// close. Reject the upload at the validation boundary.
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
reason: `tarball ${filename} contains multiple top-level package.json entries (${outcome.paths
|
|
101
|
+
.map((p) => JSON.stringify(p))
|
|
102
|
+
.join(", ")}); npm tarballs must hold exactly one top-level package directory`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (outcome.kind === "parse-error") {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
reason: `tarball ${filename} failed to parse: ${outcome.message}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (outcome.kind === "json-error") {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
reason: `tarball ${filename} package.json is not valid JSON: ${outcome.message}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (outcome.kind === "shape-invalid") {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
reason: `tarball ${filename} package.json failed validation: ${outcome.message}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, pkg: outcome.parsed };
|
|
124
|
+
}
|
|
125
|
+
export const packageRegistryKindHandler = {
|
|
126
|
+
kind: "package-registry",
|
|
127
|
+
directoryPrefix: "assets/package-registry",
|
|
128
|
+
async validatePush({ repoId, ref, topLevelTreePaths, readBlob, listDir, }) {
|
|
129
|
+
// The substrate hands us every top-level tree entry — both
|
|
130
|
+
// directories and files — from both the receivePack and writeTree
|
|
131
|
+
// adapters. The allowed set is `tarballs`, `.gitignore`, and
|
|
132
|
+
// `package-registry.json`.
|
|
133
|
+
for (const entry of topLevelTreePaths) {
|
|
134
|
+
if (entry === "tarballs" ||
|
|
135
|
+
entry === ".gitignore" ||
|
|
136
|
+
entry === REGISTRY_INDEX_PATH) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
reason: `unexpected top-level entry ${JSON.stringify(entry)}; allowed: "tarballs", "${REGISTRY_INDEX_PATH}", ".gitignore"`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Enumerate every entry under `tarballs/`, validate the filename
|
|
145
|
+
// shape, then open each tarball and validate its package.json.
|
|
146
|
+
// Gate on `topLevelTreePaths` so a tree without a `tarballs/`
|
|
147
|
+
// subtree (the genesis commit, or any push that simply does not
|
|
148
|
+
// include tarballs yet) skips the enumeration: the substrate's
|
|
149
|
+
// `listDir` returns `[]` for an absent path, which is
|
|
150
|
+
// indistinguishable from a present-but-empty `tarballs/` subtree.
|
|
151
|
+
// "Is the subtree there at all" is therefore answered at the handler
|
|
152
|
+
// from the top-level enumeration the substrate already supplies, not
|
|
153
|
+
// from a probe call. With `tarballs` confirmed present, any throw
|
|
154
|
+
// from the `listDir` below is a real fault (EACCES, EIO, malformed
|
|
155
|
+
// tree, transient transport), so it is surfaced rather than treated
|
|
156
|
+
// as "no tarballs."
|
|
157
|
+
let tarballChildren;
|
|
158
|
+
if (!topLevelTreePaths.includes("tarballs")) {
|
|
159
|
+
tarballChildren = [];
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
try {
|
|
163
|
+
tarballChildren = await listDir("tarballs");
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
// The top-level enumeration already told us `tarballs` is
|
|
167
|
+
// present, so any failure here is a real listDir fault
|
|
168
|
+
// (EACCES, EIO, malformed-tree, transient transport) — not
|
|
169
|
+
// "the subtree is absent." Surface it as a push rejection
|
|
170
|
+
// rather than collapsing it into "no tarballs to validate,"
|
|
171
|
+
// which would silently let a push through whose tarballs
|
|
172
|
+
// subtree could not be enumerated.
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
reason: `failed to list tarballs subtree: ${err instanceof Error ? err.message : String(err)}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Two tarballs publishing the same `${name}@${version}` make the
|
|
180
|
+
// resolver's AssetRegistrySource overwrite by fs.readdir order —
|
|
181
|
+
// an undefined-behaviour outcome across filesystems. Reject the
|
|
182
|
+
// collision at the substrate boundary so the registry asset's
|
|
183
|
+
// closure is unambiguous regardless of how the loader walks it.
|
|
184
|
+
const publishedNameVersions = new Set();
|
|
185
|
+
for (const child of tarballChildren) {
|
|
186
|
+
const repoPath = `${TARBALLS_PREFIX}${child}`;
|
|
187
|
+
const filename = asTarballEntry(repoPath);
|
|
188
|
+
if (filename === null) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
reason: `tarball path ${JSON.stringify(repoPath)} must match tarballs/<filename>.tgz with filename-safe characters and a .tgz extension`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
let bytes;
|
|
195
|
+
try {
|
|
196
|
+
bytes = await readBlob(repoPath);
|
|
197
|
+
}
|
|
198
|
+
catch (cause) {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
reason: `tarball ${repoPath} could not be read from the tree: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const outcome = await validateTarballPackageJSON(filename, bytes);
|
|
205
|
+
if (!outcome.ok) {
|
|
206
|
+
logger.debug `package-registry validatePush rejected ${repoId.kind}/${repoId.id} on ${ref}: ${outcome.reason}`;
|
|
207
|
+
return { ok: false, reason: outcome.reason };
|
|
208
|
+
}
|
|
209
|
+
const key = `${outcome.pkg.name}@${outcome.pkg.version}`;
|
|
210
|
+
if (publishedNameVersions.has(key)) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
reason: `package-registry contains multiple tarballs publishing ${key}; each name@version pair must appear in exactly one tarball`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
publishedNameVersions.add(key);
|
|
217
|
+
}
|
|
218
|
+
return { ok: true };
|
|
219
|
+
},
|
|
220
|
+
onRefUpdated() {
|
|
221
|
+
// No cached index today. The resolver reads the asset's contents
|
|
222
|
+
// through `readAssetBlob`/`listAssetBlobs` at session time.
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Authorize policy for package-registry repos. Sidecars never write
|
|
227
|
+
* to package-registry repos (they consume contents through the
|
|
228
|
+
* in-process read API, not the smart-HTTP layer); only the hub and
|
|
229
|
+
* authenticated users with the right grant may push.
|
|
230
|
+
*/
|
|
231
|
+
export const packageRegistryAuthorize = (principal, repoId, _ref, action) => {
|
|
232
|
+
if (repoId.kind !== "package-registry") {
|
|
233
|
+
return {
|
|
234
|
+
allowed: false,
|
|
235
|
+
reason: `package-registry authorize received non-package-registry repo ${repoId.kind}/${repoId.id}`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (principal.kind === "hub") {
|
|
239
|
+
return { allowed: true };
|
|
240
|
+
}
|
|
241
|
+
if (principal.kind === "sidecar") {
|
|
242
|
+
return {
|
|
243
|
+
allowed: false,
|
|
244
|
+
reason: `sidecars do not access package-registry assets via the substrate; action=${action}`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// The smart-HTTP route layer treats package-registry repos as
|
|
248
|
+
// user-write-denied by design: tarball writes are constrained to the
|
|
249
|
+
// shape the kind handler validates (`tarballs/<filename>.tgz` plus a
|
|
250
|
+
// hub-authored index), and the REST PUT/DELETE endpoints on the
|
|
251
|
+
// asset routes (`PUT /api/tenants/:tid/assets/:assetId/tarballs/:filename`
|
|
252
|
+
// and the matching DELETE) are the supported path for users who
|
|
253
|
+
// need to publish a tarball. Smart-HTTP would let a user push
|
|
254
|
+
// arbitrary tree shapes that the kind handler would then have to
|
|
255
|
+
// reject after the fact; the REST surface validates ahead of write.
|
|
256
|
+
return {
|
|
257
|
+
allowed: false,
|
|
258
|
+
reason: `principal kind ${principal.kind} cannot push to package-registry over smart-HTTP; use the REST tarball endpoints (PUT/DELETE /api/tenants/:tid/assets/:assetId/tarballs/:filename)`,
|
|
259
|
+
};
|
|
260
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { AuthorizeFn, InitRepoOpts, KindHandler, NewlyTerminalRun, PriorDeltaReads, Principal, RefEntry, RepoAction, RepoId, RepoKind, RepoStore, RepoStoreSubscribeEvent, TreeContent, ValidatePushResult, WriteResult, WriteTreePreservingPrefixArgs, } from "./types.js";
|
|
2
|
+
export { UserPrincipal } from "./types.js";
|
|
3
|
+
export { createRepoStore, type CreateRepoStoreConfig } from "./store.js";
|
|
4
|
+
export { subscribeKind, type SubscribeKindOpts, type SubscribeKindEntry, } from "./subscribe-kind.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type CommitSigner, type GCPolicy } from "@intx/storage-isogit";
|
|
2
|
+
import type { AuthorizeFn, KindHandler, RepoId, RepoKind, RepoStore } from "./types.js";
|
|
3
|
+
type SigningKey = {
|
|
4
|
+
privateKey: Uint8Array;
|
|
5
|
+
publicKey: Uint8Array;
|
|
6
|
+
};
|
|
7
|
+
export type CreateRepoStoreConfig = {
|
|
8
|
+
dataDir: string;
|
|
9
|
+
signingKey: SigningKey;
|
|
10
|
+
/**
|
|
11
|
+
* Handler map keyed by repo kind. The substrate throws at request
|
|
12
|
+
* time when a kind has no registered handler, so callers may omit
|
|
13
|
+
* kinds they do not service (e.g. a per-asset-kind store that only
|
|
14
|
+
* registers `skill`).
|
|
15
|
+
*/
|
|
16
|
+
handlers: Partial<Record<RepoKind, KindHandler>>;
|
|
17
|
+
authorize: AuthorizeFn;
|
|
18
|
+
/**
|
|
19
|
+
* Optional per-repo signing callback. When supplied and the callback
|
|
20
|
+
* returns a `CommitSigner` for the given `repoId`, the substrate
|
|
21
|
+
* passes that signer to `initRepo` so the genesis commit is authored
|
|
22
|
+
* as `interchange-hub` and signed. When the callback returns
|
|
23
|
+
* `undefined`, or the field is omitted entirely, the substrate falls
|
|
24
|
+
* back to the unsigned harness-authored genesis.
|
|
25
|
+
*/
|
|
26
|
+
signingCallback?: (repoId: RepoId) => CommitSigner | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Optional write-path garbage-collection policy. When supplied, after
|
|
29
|
+
* every successful write under the repo lock the substrate applies the
|
|
30
|
+
* shared reclaim policy to repos whose kind is in `kinds`. Omitted
|
|
31
|
+
* entirely, the substrate never reclaims and never warns. The `kinds`
|
|
32
|
+
* allowlist keeps the policy scoped to the kinds the caller intends
|
|
33
|
+
* (e.g. `agent-state`) rather than every kind the store happens to
|
|
34
|
+
* service.
|
|
35
|
+
*/
|
|
36
|
+
gc?: GCPolicy & {
|
|
37
|
+
kinds: readonly RepoKind[];
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export declare function createRepoStore(config: CreateRepoStoreConfig): RepoStore;
|
|
41
|
+
export {};
|