@intx/hub-sessions 0.1.2 → 0.2.2

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