@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
@@ -1,540 +0,0 @@
1
- import { describe, test, expect, beforeEach } from "bun:test";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
-
6
- import type { DB } from "@intx/db";
7
- import {
8
- agentAsset as agentAssetTable,
9
- asset as assetTable,
10
- } from "@intx/db/schema";
11
- import { generateKeyPair } from "@intx/crypto-node";
12
- import type { KeyPair } from "@intx/types/runtime";
13
-
14
- import {
15
- AssetServiceError,
16
- createAssetService,
17
- type AssetService,
18
- } from "./asset-service";
19
- import { createRepoStore } from "./repo-store";
20
- import type { Principal, RepoStore } from "./repo-store";
21
- import { skillKindHandler, skillAuthorize } from "./skill-kind";
22
-
23
- // ---------------------------------------------------------------------------
24
- // DB stub
25
- //
26
- // Mirrors the mocking style used by hub-session-orchestrator.test.ts: a
27
- // minimal in-memory store routed by drizzle table identity. Captures the
28
- // rows passed to insert/values so the test can assert column-level shape,
29
- // and raises a typed Postgres unique-violation error (code "23505") to
30
- // exercise the AssetService's translation of duplicates into typed domain
31
- // errors.
32
- //
33
- // The where-clauses passed by drizzle's `eq` are opaque values; rather
34
- // than inspect them, the stub exposes `nextFindFirstAssetId` and
35
- // `nextSelectAgentId` setters that the tests use to tell the stub which
36
- // id the next query targets.
37
- // ---------------------------------------------------------------------------
38
-
39
- type AssetRow = {
40
- id: string;
41
- tenantId: string;
42
- kind: string;
43
- name: string;
44
- displayName: string | null;
45
- creatorPrincipalId: string | null;
46
- createdAt: Date;
47
- updatedAt: Date;
48
- };
49
-
50
- type AgentAssetRow = {
51
- id: string;
52
- agentId: string;
53
- assetId: string;
54
- ref: string;
55
- accessMode: string;
56
- createdAt: Date;
57
- };
58
-
59
- class UniqueViolation extends Error {
60
- readonly code = "23505";
61
- constructor(constraint: string) {
62
- super(`duplicate key value violates unique constraint "${constraint}"`);
63
- }
64
- }
65
-
66
- class ForeignKeyViolation extends Error {
67
- readonly code = "23503";
68
- constructor(constraint: string) {
69
- super(`insert violates foreign key constraint "${constraint}"`);
70
- }
71
- }
72
-
73
- type DBStub = {
74
- db: DB["db"];
75
- assets: AssetRow[];
76
- agentAssets: AgentAssetRow[];
77
- nextFindFirstAssetId: (id: string) => void;
78
- nextSelectAgentId: (id: string) => void;
79
- };
80
-
81
- function makeDB(): DBStub {
82
- const assets: AssetRow[] = [];
83
- const agentAssets: AgentAssetRow[] = [];
84
-
85
- let lastFindFirstId: string | null = null;
86
- let lastSelectAgentId: string | null = null;
87
-
88
- function isAssetRow(row: AssetRow | AgentAssetRow): row is AssetRow {
89
- return "tenantId" in row && "kind" in row;
90
- }
91
-
92
- function insertReturning(
93
- table: unknown,
94
- row: AssetRow | AgentAssetRow,
95
- ): Promise<unknown[]> {
96
- if (table === assetTable) {
97
- if (!isAssetRow(row)) {
98
- throw new Error("insert into asset received non-asset row shape");
99
- }
100
- if (
101
- assets.some(
102
- (a) =>
103
- a.tenantId === row.tenantId &&
104
- a.kind === row.kind &&
105
- a.name === row.name,
106
- )
107
- ) {
108
- return Promise.reject(new UniqueViolation("asset_tenant_kind_name"));
109
- }
110
- assets.push(row);
111
- return Promise.resolve([row]);
112
- }
113
- if (table === agentAssetTable) {
114
- if (isAssetRow(row)) {
115
- throw new Error("insert into agent_asset received asset row shape");
116
- }
117
- if (!assets.some((a) => a.id === row.assetId)) {
118
- return Promise.reject(
119
- new ForeignKeyViolation("agent_asset_asset_id_asset_id_fk"),
120
- );
121
- }
122
- if (
123
- agentAssets.some(
124
- (a) => a.agentId === row.agentId && a.assetId === row.assetId,
125
- )
126
- ) {
127
- return Promise.reject(new UniqueViolation("agent_asset_agent_asset"));
128
- }
129
- agentAssets.push(row);
130
- return Promise.resolve([row]);
131
- }
132
- throw new Error(`unexpected insert table`);
133
- }
134
-
135
- function findFirstAsset(): Promise<AssetRow | undefined> {
136
- if (lastFindFirstId === null) {
137
- return Promise.resolve(undefined);
138
- }
139
- const match = assets.find((a) => a.id === lastFindFirstId);
140
- lastFindFirstId = null;
141
- return Promise.resolve(match);
142
- }
143
-
144
- function joinAgentAssets(): Promise<unknown[]> {
145
- if (lastSelectAgentId === null) {
146
- return Promise.resolve([]);
147
- }
148
- const id = lastSelectAgentId;
149
- lastSelectAgentId = null;
150
- const joined = agentAssets
151
- .filter((aa) => aa.agentId === id)
152
- .map((aa) => {
153
- const a = assets.find((x) => x.id === aa.assetId);
154
- if (a === undefined) {
155
- throw new Error(
156
- `inconsistent stub: agent_asset ${aa.id} references missing asset ${aa.assetId}`,
157
- );
158
- }
159
- return { agentAsset: aa, asset: a };
160
- });
161
- return Promise.resolve(joined);
162
- }
163
-
164
- /* eslint-disable @typescript-eslint/no-unsafe-type-assertion --
165
- * drizzle PgDatabase type cannot be structurally satisfied in tests */
166
- const db = {
167
- insert(t: unknown) {
168
- return {
169
- values(row: AssetRow | AgentAssetRow) {
170
- return {
171
- returning: () => insertReturning(t, row),
172
- };
173
- },
174
- };
175
- },
176
- query: {
177
- asset: {
178
- findFirst: (_args: { where: unknown }) => findFirstAsset(),
179
- },
180
- },
181
- select(_cols: unknown) {
182
- return {
183
- from: (_t: unknown) => ({
184
- innerJoin: (_t2: unknown, _on: unknown) => ({
185
- where: (_w: unknown) => ({
186
- orderBy: (..._order: unknown[]) => joinAgentAssets(),
187
- }),
188
- }),
189
- }),
190
- };
191
- },
192
- } as unknown as DB["db"];
193
- /* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
194
-
195
- return {
196
- db,
197
- assets,
198
- agentAssets,
199
- nextFindFirstAssetId: (id) => {
200
- lastFindFirstId = id;
201
- },
202
- nextSelectAgentId: (id) => {
203
- lastSelectAgentId = id;
204
- },
205
- };
206
- }
207
-
208
- // ---------------------------------------------------------------------------
209
- // RepoStore fixture
210
- //
211
- // The skill kind handler runs against a real on-disk RepoStore so the
212
- // validatePush -> path_violation translation is exercised end-to-end.
213
- // ---------------------------------------------------------------------------
214
-
215
- const tempDirs: string[] = [];
216
-
217
- async function makeTempDir(prefix: string): Promise<string> {
218
- const d = await fs.promises.mkdtemp(path.join(os.tmpdir(), prefix));
219
- tempDirs.push(d);
220
- return d;
221
- }
222
-
223
- let signingKey: KeyPair;
224
-
225
- async function createSkillRepoStore(): Promise<RepoStore> {
226
- if (signingKey === undefined) {
227
- signingKey = await generateKeyPair();
228
- }
229
- const dataDir = await makeTempDir("asset-svc-");
230
- return createRepoStore({
231
- dataDir,
232
- signingKey,
233
- handlers: { skill: skillKindHandler },
234
- authorize: skillAuthorize,
235
- });
236
- }
237
-
238
- const HUB: Principal = { kind: "hub" };
239
-
240
- const VALID_SKILL_TREE = {
241
- files: {
242
- "greet/SKILL.md":
243
- "---\nname: greet\ndescription: Say hello in a chosen language\n---\n\nbody\n",
244
- },
245
- message: "initial",
246
- };
247
-
248
- const INVALID_SKILL_TREE = {
249
- files: {
250
- // Reserved name "anthropic" is rejected by the kind handler's
251
- // arktype frontmatter schema; the substrate translates the
252
- // rejection into a "path_violation:"-prefixed Error message.
253
- "anthropic/SKILL.md":
254
- "---\nname: anthropic\ndescription: reserved-name skill\n---\n\nbody\n",
255
- },
256
- message: "initial",
257
- };
258
-
259
- // ---------------------------------------------------------------------------
260
- // Tests
261
- // ---------------------------------------------------------------------------
262
-
263
- describe("AssetService", () => {
264
- let dbFixture: DBStub;
265
- let repoStore: RepoStore;
266
- let service: AssetService;
267
-
268
- beforeEach(async () => {
269
- dbFixture = makeDB();
270
- repoStore = await createSkillRepoStore();
271
- service = createAssetService({ db: dbFixture.db, repoStore });
272
- });
273
-
274
- describe("createAsset", () => {
275
- test("inserts the asset row and initializes the skill repo", async () => {
276
- const asset = await service.createAsset({
277
- tenantId: "tnt_1",
278
- kind: "skill",
279
- name: "greet",
280
- displayName: "Greeting skill",
281
- creatorPrincipalId: "prn_1",
282
- });
283
-
284
- expect(asset.kind).toBe("skill");
285
- expect(asset.tenantId).toBe("tnt_1");
286
- expect(asset.name).toBe("greet");
287
- expect(asset.displayName).toBe("Greeting skill");
288
- expect(asset.creatorPrincipalId).toBe("prn_1");
289
- expect(asset.id).toMatch(/^ast_/);
290
-
291
- expect(dbFixture.assets).toHaveLength(1);
292
- const inserted = dbFixture.assets[0];
293
- if (inserted === undefined) throw new Error("unreachable");
294
- expect(inserted.id).toBe(asset.id);
295
-
296
- // initRepo creates the on-disk repo so subsequent writeTree calls
297
- // succeed without ENOENT. Writing a tree against the new repo is
298
- // the cheapest way to assert the repo exists; the substrate would
299
- // throw at the first git operation otherwise.
300
- const out = await repoStore.writeTree(
301
- HUB,
302
- { kind: "skill", id: asset.id },
303
- "refs/heads/main",
304
- VALID_SKILL_TREE,
305
- );
306
- expect(out.commitSha).toMatch(/^[0-9a-f]{40}$/);
307
- });
308
-
309
- test("rejects kind agent-state", async () => {
310
- const err = await service
311
- .createAsset({
312
- tenantId: "tnt_1",
313
- kind: "agent-state",
314
- name: "x",
315
- })
316
- .catch((e: unknown) => e);
317
-
318
- expect(err).toBeInstanceOf(AssetServiceError);
319
- if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
320
- expect(err.reason).toBe("unsupported_kind");
321
- expect(dbFixture.assets).toHaveLength(0);
322
- });
323
-
324
- test("accepts a valid lowercase-kebab name", async () => {
325
- const asset = await service.createAsset({
326
- tenantId: "tnt_1",
327
- kind: "skill",
328
- name: "my-skills",
329
- });
330
- expect(asset.name).toBe("my-skills");
331
- expect(dbFixture.assets).toHaveLength(1);
332
- });
333
-
334
- test.each([
335
- [".", "single dot"],
336
- ["..", "double dot"],
337
- ["my skill", "embedded space"],
338
- ["skills/greet", "embedded slash"],
339
- ])("rejects name %p (%s) as invalid_name", async (badName) => {
340
- const err = await service
341
- .createAsset({
342
- tenantId: "tnt_1",
343
- kind: "skill",
344
- name: badName,
345
- })
346
- .catch((e: unknown) => e);
347
-
348
- expect(err).toBeInstanceOf(AssetServiceError);
349
- if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
350
- expect(err.reason).toBe("invalid_name");
351
- expect(dbFixture.assets).toHaveLength(0);
352
- });
353
-
354
- test("surfaces duplicate (tenantId, kind, name) as duplicate_asset", async () => {
355
- await service.createAsset({
356
- tenantId: "tnt_1",
357
- kind: "skill",
358
- name: "greet",
359
- });
360
-
361
- const err = await service
362
- .createAsset({
363
- tenantId: "tnt_1",
364
- kind: "skill",
365
- name: "greet",
366
- })
367
- .catch((e: unknown) => e);
368
-
369
- expect(err).toBeInstanceOf(AssetServiceError);
370
- if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
371
- expect(err.reason).toBe("duplicate_asset");
372
- expect(dbFixture.assets).toHaveLength(1);
373
- });
374
- });
375
-
376
- describe("populateAsset", () => {
377
- test("happy path writes the tree and returns commitSha", async () => {
378
- const asset = await service.createAsset({
379
- tenantId: "tnt_1",
380
- kind: "skill",
381
- name: "greet",
382
- });
383
-
384
- dbFixture.nextFindFirstAssetId(asset.id);
385
- const out = await service.populateAsset({
386
- assetId: asset.id,
387
- ref: "refs/heads/main",
388
- tree: VALID_SKILL_TREE,
389
- principal: HUB,
390
- });
391
- expect(out.commitSha).toMatch(/^[0-9a-f]{40}$/);
392
- });
393
-
394
- test("surfaces an unknown assetId as not_found", async () => {
395
- dbFixture.nextFindFirstAssetId("ast_missing");
396
- const err = await service
397
- .populateAsset({
398
- assetId: "ast_missing",
399
- ref: "refs/heads/main",
400
- tree: VALID_SKILL_TREE,
401
- principal: HUB,
402
- })
403
- .catch((e: unknown) => e);
404
-
405
- expect(err).toBeInstanceOf(AssetServiceError);
406
- if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
407
- expect(err.reason).toBe("not_found");
408
- });
409
-
410
- test("translates path_violation: error from validatePush", async () => {
411
- const asset = await service.createAsset({
412
- tenantId: "tnt_1",
413
- kind: "skill",
414
- name: "greet",
415
- });
416
-
417
- dbFixture.nextFindFirstAssetId(asset.id);
418
- const err = await service
419
- .populateAsset({
420
- assetId: asset.id,
421
- ref: "refs/heads/main",
422
- tree: INVALID_SKILL_TREE,
423
- principal: HUB,
424
- })
425
- .catch((e: unknown) => e);
426
-
427
- expect(err).toBeInstanceOf(AssetServiceError);
428
- if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
429
- expect(err.reason).toBe("path_violation");
430
- expect(err.message).toContain("path_violation:");
431
- });
432
- });
433
-
434
- describe("attachAsset", () => {
435
- test("inserts an agent_asset row", async () => {
436
- const asset = await service.createAsset({
437
- tenantId: "tnt_1",
438
- kind: "skill",
439
- name: "greet",
440
- });
441
-
442
- const aa = await service.attachAsset({
443
- agentId: "agt_1",
444
- assetId: asset.id,
445
- ref: "refs/heads/main",
446
- accessMode: "read-only",
447
- });
448
-
449
- expect(aa.id).toMatch(/^aas_/);
450
- expect(aa.agentId).toBe("agt_1");
451
- expect(aa.assetId).toBe(asset.id);
452
- expect(aa.accessMode).toBe("read-only");
453
- expect(dbFixture.agentAssets).toHaveLength(1);
454
- });
455
-
456
- test("defaults accessMode to read-only when omitted", async () => {
457
- const asset = await service.createAsset({
458
- tenantId: "tnt_1",
459
- kind: "skill",
460
- name: "greet",
461
- });
462
-
463
- const aa = await service.attachAsset({
464
- agentId: "agt_1",
465
- assetId: asset.id,
466
- ref: "refs/heads/main",
467
- });
468
- expect(aa.accessMode).toBe("read-only");
469
- });
470
-
471
- test("surfaces a missing assetId reference as invalid_reference", async () => {
472
- const err = await service
473
- .attachAsset({
474
- agentId: "agt_1",
475
- assetId: "ast_missing",
476
- ref: "refs/heads/main",
477
- })
478
- .catch((e: unknown) => e);
479
-
480
- expect(err).toBeInstanceOf(AssetServiceError);
481
- if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
482
- expect(err.reason).toBe("invalid_reference");
483
- expect(dbFixture.agentAssets).toHaveLength(0);
484
- });
485
-
486
- test("surfaces duplicate (agentId, assetId) as duplicate_attachment", async () => {
487
- const asset = await service.createAsset({
488
- tenantId: "tnt_1",
489
- kind: "skill",
490
- name: "greet",
491
- });
492
-
493
- await service.attachAsset({
494
- agentId: "agt_1",
495
- assetId: asset.id,
496
- ref: "refs/heads/main",
497
- });
498
-
499
- const err = await service
500
- .attachAsset({
501
- agentId: "agt_1",
502
- assetId: asset.id,
503
- ref: "refs/heads/main",
504
- })
505
- .catch((e: unknown) => e);
506
-
507
- expect(err).toBeInstanceOf(AssetServiceError);
508
- if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
509
- expect(err.reason).toBe("duplicate_attachment");
510
- expect(dbFixture.agentAssets).toHaveLength(1);
511
- });
512
- });
513
-
514
- describe("listAgentAssets", () => {
515
- test("returns joined rows including asset.name and asset.kind", async () => {
516
- const asset = await service.createAsset({
517
- tenantId: "tnt_1",
518
- kind: "skill",
519
- name: "greet",
520
- displayName: "Greeting",
521
- });
522
- await service.attachAsset({
523
- agentId: "agt_1",
524
- assetId: asset.id,
525
- ref: "refs/heads/main",
526
- });
527
-
528
- dbFixture.nextSelectAgentId("agt_1");
529
- const rows = await service.listAgentAssets("agt_1");
530
- expect(rows).toHaveLength(1);
531
- const row = rows[0];
532
- if (row === undefined) throw new Error("unreachable");
533
- expect(row.agentId).toBe("agt_1");
534
- expect(row.asset.id).toBe(asset.id);
535
- expect(row.asset.kind).toBe("skill");
536
- expect(row.asset.name).toBe("greet");
537
- expect(row.asset.displayName).toBe("Greeting");
538
- });
539
- });
540
- });