@intx/hub-sessions 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +82 -1
  3. package/dist/agent-repo.d.ts +93 -0
  4. package/dist/agent-repo.js +109 -0
  5. package/dist/agent-state-kind.d.ts +12 -0
  6. package/dist/agent-state-kind.js +189 -0
  7. package/dist/asset-service.d.ts +104 -0
  8. package/dist/asset-service.js +267 -0
  9. package/dist/committed-source-tree.d.ts +10 -0
  10. package/dist/committed-source-tree.js +35 -0
  11. package/dist/credential-push.d.ts +33 -0
  12. package/dist/credential-push.js +109 -0
  13. package/dist/event-collector-registry.d.ts +20 -0
  14. package/dist/event-collector-registry.js +115 -0
  15. package/dist/event-collector.d.ts +39 -0
  16. package/dist/event-collector.js +365 -0
  17. package/dist/hub-session-lookups.d.ts +135 -0
  18. package/dist/hub-session-lookups.js +663 -0
  19. package/dist/hub-session-orchestrator.d.ts +25 -0
  20. package/dist/hub-session-orchestrator.js +87 -0
  21. package/dist/index.d.ts +27 -0
  22. package/dist/index.js +24 -0
  23. package/dist/package-registry-kind.d.ts +70 -0
  24. package/dist/package-registry-kind.js +260 -0
  25. package/dist/repo-store/index.d.ts +4 -0
  26. package/dist/repo-store/index.js +3 -0
  27. package/dist/repo-store/store.d.ts +41 -0
  28. package/dist/repo-store/store.js +1829 -0
  29. package/dist/repo-store/subscribe-kind.d.ts +56 -0
  30. package/dist/repo-store/subscribe-kind.js +144 -0
  31. package/dist/repo-store/types.d.ts +571 -0
  32. package/dist/repo-store/types.js +42 -0
  33. package/dist/session-service.d.ts +416 -0
  34. package/dist/session-service.js +1191 -0
  35. package/dist/sidecar-allocation/contracts.d.ts +78 -0
  36. package/dist/sidecar-allocation/contracts.js +21 -0
  37. package/dist/sidecar-allocation/index.d.ts +4 -0
  38. package/dist/sidecar-allocation/index.js +3 -0
  39. package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
  40. package/dist/sidecar-allocation/placement-policy.js +21 -0
  41. package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
  42. package/dist/sidecar-allocation/plugin-registry.js +37 -0
  43. package/dist/sidecar-allocation/reconciler.d.ts +42 -0
  44. package/dist/sidecar-allocation/reconciler.js +431 -0
  45. package/dist/skill-kind.d.ts +41 -0
  46. package/dist/skill-kind.js +292 -0
  47. package/dist/substrate.d.ts +8 -0
  48. package/dist/substrate.js +21 -0
  49. package/dist/workflow-allocation-service.d.ts +58 -0
  50. package/dist/workflow-allocation-service.js +239 -0
  51. package/dist/workflow-closure-resolution.d.ts +106 -0
  52. package/dist/workflow-closure-resolution.js +123 -0
  53. package/dist/workflow-definition-ensure.d.ts +24 -0
  54. package/dist/workflow-definition-ensure.js +75 -0
  55. package/dist/workflow-dispatch-service.d.ts +40 -0
  56. package/dist/workflow-dispatch-service.js +146 -0
  57. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  58. package/dist/workflow-dispatch-settlement.js +140 -0
  59. package/dist/workflow-kind.d.ts +37 -0
  60. package/dist/workflow-kind.js +310 -0
  61. package/dist/workflow-probe-gate.d.ts +214 -0
  62. package/dist/workflow-probe-gate.js +207 -0
  63. package/dist/workflow-run-event-log.d.ts +21 -0
  64. package/dist/workflow-run-event-log.js +51 -0
  65. package/dist/workflow-run-kind.d.ts +440 -0
  66. package/dist/workflow-run-kind.js +2916 -0
  67. package/dist/workflow-run-reader.d.ts +47 -0
  68. package/dist/workflow-run-reader.js +153 -0
  69. package/dist/workflow-run-restore.d.ts +15 -0
  70. package/dist/workflow-run-restore.js +26 -0
  71. package/dist/workflow-source-closure.d.ts +35 -0
  72. package/dist/workflow-source-closure.js +342 -0
  73. package/dist/ws/index.d.ts +3 -0
  74. package/dist/ws/index.js +3 -0
  75. package/dist/ws/sidecar-events.d.ts +222 -0
  76. package/dist/ws/sidecar-events.js +72 -0
  77. package/dist/ws/sidecar-handler.d.ts +305 -0
  78. package/dist/ws/sidecar-handler.js +2537 -0
  79. package/dist/ws/sidecar-token-authenticator.d.ts +17 -0
  80. package/dist/ws/sidecar-token-authenticator.js +81 -0
  81. package/package.json +35 -12
  82. package/src/agent-repo.test.ts +0 -310
  83. package/src/agent-repo.ts +0 -165
  84. package/src/agent-state-kind.test.ts +0 -247
  85. package/src/agent-state-kind.ts +0 -204
  86. package/src/asset-service.test.ts +0 -540
  87. package/src/asset-service.ts +0 -378
  88. package/src/available-skills-stanza.test.ts +0 -87
  89. package/src/available-skills-stanza.ts +0 -47
  90. package/src/credential-push.ts +0 -65
  91. package/src/event-collector-registry.test.ts +0 -73
  92. package/src/event-collector-registry.ts +0 -171
  93. package/src/event-collector.test.ts +0 -1387
  94. package/src/event-collector.ts +0 -424
  95. package/src/hub-session-lookups.ts +0 -206
  96. package/src/hub-session-orchestrator.test.ts +0 -510
  97. package/src/hub-session-orchestrator.ts +0 -213
  98. package/src/index.ts +0 -78
  99. package/src/repo-store/index.ts +0 -15
  100. package/src/repo-store/store.test.ts +0 -1169
  101. package/src/repo-store/store.ts +0 -428
  102. package/src/repo-store/types.ts +0 -253
  103. package/src/session-service.test.ts +0 -895
  104. package/src/session-service.ts +0 -464
  105. package/src/skill-kind.test.ts +0 -599
  106. package/src/skill-kind.ts +0 -350
  107. package/src/ws/index.ts +0 -18
  108. package/src/ws/sidecar-events.test.ts +0 -96
  109. package/src/ws/sidecar-events.ts +0 -231
  110. package/src/ws/sidecar-handler.test.ts +0 -2217
  111. package/src/ws/sidecar-handler.ts +0 -1574
  112. package/tsconfig.json +0 -4
  113. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,25 @@
1
+ import type { DB } from "@intx/db";
2
+ import type { AgentRepoStore } from "./agent-repo.js";
3
+ import type { EventCollectorRegistry } from "./event-collector-registry.js";
4
+ import type { SidecarEventEmitter } from "./ws/sidecar-events.js";
5
+ /** Subset of `SidecarRouter` the orchestrator drives outbound. The
6
+ * narrow surface keeps tests honest and decouples the orchestrator
7
+ * from the rest of the router API. */
8
+ export type HubSessionRouterFacade = {
9
+ sendPack(agentAddress: string, pack: Uint8Array, ref: string, commitSha: string): Promise<void>;
10
+ dispatchAgentEvent(agentAddress: string, event: unknown): void;
11
+ };
12
+ export type HubSessionOrchestratorDeps = {
13
+ events: SidecarEventEmitter;
14
+ router: HubSessionRouterFacade;
15
+ db: DB["db"];
16
+ eventCollectors: EventCollectorRegistry;
17
+ agentRepoStore: AgentRepoStore;
18
+ };
19
+ export type HubSessionOrchestrator = {
20
+ /** Unsubscribe all listeners. Tests use this between cases; the hub
21
+ * application doesn't need to dispose because the orchestrator's
22
+ * lifetime matches the process. */
23
+ dispose(): void;
24
+ };
25
+ export declare function createHubSessionOrchestrator(deps: HubSessionOrchestratorDeps): HubSessionOrchestrator;
@@ -0,0 +1,87 @@
1
+ // Hub-side orchestrator for the sidecar event emitter.
2
+ //
3
+ // Subscribes to the sidecar router's events and runs the host-side
4
+ // reactions: dispatching inference events to the event collector,
5
+ // restoring the event collector on reconnect, re-deploying stale
6
+ // agents, and forwarding mail.delivered notifications back to
7
+ // subscribers.
8
+ //
9
+ // The orchestrator depends on a narrow `HubSessionRouterFacade` rather
10
+ // than the full SidecarRouter, so tests can drive subscriber behavior
11
+ // with a small stub and an isolated emitter.
12
+ import { eq } from "drizzle-orm";
13
+ import { type } from "arktype";
14
+ import { workflowRun } from "@intx/db/schema";
15
+ import { parseMailToEmail } from "@intx/mime";
16
+ import { parseInferenceEvent } from "@intx/types/runtime";
17
+ import { getLogger } from "@intx/log";
18
+ import { parseAgentId } from "./hub-session-lookups.js";
19
+ const log = getLogger(["hub", "orchestrator"]);
20
+ export function createHubSessionOrchestrator(deps) {
21
+ const { events, router, db, eventCollectors, agentRepoStore } = deps;
22
+ const unsubscribers = [];
23
+ unsubscribers.push(events.on("agent.event", ({ agentAddress, event }) => {
24
+ const validated = parseInferenceEvent(event);
25
+ if (validated instanceof type.errors) {
26
+ log.warn("Received invalid agent event for {agentAddress}: {summary}", {
27
+ agentAddress,
28
+ summary: validated.summary,
29
+ });
30
+ return;
31
+ }
32
+ eventCollectors.dispatch(agentAddress, validated);
33
+ }));
34
+ unsubscribers.push(events.on("sidecar.disconnect", ({ ownedAddresses }) => {
35
+ for (const addr of ownedAddresses) {
36
+ eventCollectors.abandon(addr);
37
+ }
38
+ }));
39
+ unsubscribers.push(events.on("mail.outbound.undelivered", ({ recipients }) => {
40
+ // The hub has no external mail transport today. Anything that
41
+ // could not be delivered locally or queued for a disconnected
42
+ // agent is dropped; log so operators can see it.
43
+ log.warn("Dropping mail with no local recipient: {recipients}", {
44
+ recipients: recipients.join(", "),
45
+ });
46
+ }));
47
+ unsubscribers.push(events.on("agent.deploy.ack", async (event) => {
48
+ const { agentAddress, publicKey, allocated } = event;
49
+ // Exclusive initialization publishes its key only after every deploy
50
+ // and asset pack succeeds under the allocation generation fence.
51
+ if (allocated !== undefined)
52
+ return;
53
+ // Every deploy address names a workflow run whose public key lives on its
54
+ // single self-anchored workflow_run row, keyed by address. Persist it
55
+ // there so the reconnect ownership challenge can verify the address off
56
+ // the same row `lookupPublicKey` reads. Only the deployment-level address
57
+ // owns a row, so a stray per-step ack updates nothing.
58
+ await db
59
+ .update(workflowRun)
60
+ .set({ publicKey })
61
+ .where(eq(workflowRun.address, agentAddress));
62
+ }));
63
+ unsubscribers.push(events.on("deploy.ref.stale", async ({ agentAddress }) => {
64
+ const agentId = parseAgentId(agentAddress);
65
+ const { pack, commitSha, ref } = await agentRepoStore.createDeployPack(agentId);
66
+ await router.sendPack(agentAddress, pack, ref, commitSha);
67
+ log.info("Re-deployed stale agent {agentAddress}", { agentAddress });
68
+ }));
69
+ unsubscribers.push(events.on("mail.persisted", (row) => {
70
+ const parsed = parseMailToEmail(row.raw, row.id);
71
+ router.dispatchAgentEvent(row.address, {
72
+ type: "mail.delivered",
73
+ data: {
74
+ ...parsed,
75
+ id: row.id,
76
+ direction: row.direction,
77
+ receivedAt: row.createdAt.toISOString(),
78
+ },
79
+ });
80
+ }));
81
+ return {
82
+ dispose() {
83
+ for (const off of unsubscribers)
84
+ off();
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,27 @@
1
+ export { createAgentRepoStore, type AgentRepoStore, type DeployContent, } from "./agent-repo.js";
2
+ export { createSessionService, SessionLaunchError, bridgeOrchestratorDeployContent, deployCodeSourcedWorkflow, type SessionService, type DeployWorkflowDefinitionResult, type DeployWorkflowFromSourceParams, type DeployPreparedCodeSourcedWorkflowParams, type InstallAndApproveWorkflowSourceParams, type PreparedWorkflowDeployer, type DeployCodeSourcedWorkflowArgs, } from "./session-service.js";
3
+ export { installAndApproveWorkflowDefinition, createDbFrozenApprovalWriter, type InstallAndApproveArgs, type InstallAndApproveResult, type ProbeGateResult, type ProbeApprovalPolicy, type ApproveProbedGrants, } from "./workflow-probe-gate.js";
4
+ export { committedReadsToSourceTree } from "./committed-source-tree.js";
5
+ export type { SourceTreeReads } from "./workflow-source-closure.js";
6
+ export type { WorkflowDefinition } from "@intx/workflow/definition";
7
+ export { createEventCollectorRegistry, type EventCollectorRegistry, } from "./event-collector-registry.js";
8
+ export { createSidecarRouter, type SidecarRouter, type SidecarRouterConfig, type SidecarAuthIdentity, type SidecarAuthenticator, type AllocatedSidecarTarget, type SidecarAllocationRouter, createSidecarCredentialResolver, createSidecarTokenAuthenticator, type CreateSidecarTokenAuthenticatorDeps, type WsHandle, createSidecarEmitter, type SidecarEventEmitter, type SidecarEventMap, type SidecarEventType, type SidecarEventListener, type SidecarLookups, type SidecarMailPersistedPayload, type SidecarMailPersistedRow, type MailTriggeredRunGrantsResult, type WorkflowRunPackSource, } from "./ws/index.js";
9
+ export { createHubSessionLookups, findRoutableById, parseAgentId, resolveRoutableAddress, resolveRunIdForSession, resolveRunSessionId, runRowToRoutableRecord, type HubSessionLookupsDeps, type RoutableEndpoint, type RoutableRecord, } from "./hub-session-lookups.js";
10
+ export { createHubSessionOrchestrator, type HubSessionOrchestrator, type HubSessionOrchestratorDeps, type HubSessionRouterFacade, } from "./hub-session-orchestrator.js";
11
+ export { pushSourceUpdates, pushSourceUpdatesSubtree } from "./credential-push.js";
12
+ export { createSidecarPluginRegistry, createSidecarAllocationReconciler, resolveEffectiveSidecarPlacement, type CreateSidecarPluginRegistryOpts, type DestroySidecarRequest, type DestroySidecarResult, type EnsureSidecarRequest, type EnsureSidecarResult, type ResolveEffectiveSidecarPlacementOpts, type SidecarCredentialIdentity, type SidecarCredentialResolver, type SidecarOperationFailure, type SidecarPluginRegistry, type SidecarProvisioner, type SidecarAllocationReconciler, type SidecarAllocationReconcilerDeps, } from "./sidecar-allocation/index.js";
13
+ export { ensureWorkflowDefinitionForAsset } from "./workflow-definition-ensure.js";
14
+ export { workflowSourceAssetMountPath } from "./workflow-closure-resolution.js";
15
+ export { createWorkflowAllocationService, ExclusiveWorkflowPlacementError, resolveWorkflowSidecarPlacement, type PrepareExclusiveWorkflowDeploymentArgs, type PreparedExclusiveWorkflowDeployment, type WorkflowAllocationService, type WorkflowAllocationServiceDeps, } from "./workflow-allocation-service.js";
16
+ export { createWorkflowDispatchService, type WorkflowDispatchAcknowledgement, type WorkflowDispatchService, type WorkflowDispatchServiceDeps, } from "./workflow-dispatch-service.js";
17
+ export { listAcceptedWorkflowDispatches, listConsumedWorkflowDispatches, listReceivedWorkflowSignals, type AcceptedWorkflowDispatch, type ConsumedWorkflowDispatch, type ReceivedWorkflowSignal, } from "./workflow-dispatch-settlement.js";
18
+ export { skillKindHandler, skillAuthorize, skillFrontmatterSchema, getSkillIndex, type SkillIndexEntry, type SkillFrontmatter, type SkillPrincipal, type SkillHubPrincipal, type SkillSidecarPrincipal, } from "./skill-kind.js";
19
+ export { packageRegistryKindHandler, packageRegistryAuthorize, asTarballEntry, validateTarballPackageJSON, TARBALLS_PREFIX, TARBALL_FILENAME_PATTERN, REGISTRY_INDEX_PATH, WORKSPACE_BUILTINS_REGISTRY, } from "./package-registry-kind.js";
20
+ export { workflowKindHandler, workflowAuthorize, workflowDefinitionEnvelopeSchema, WORKFLOW_JSON_PATH, CAPABILITY_DECLARATIONS_JSON_PATH, type WorkflowPrincipal, type WorkflowHubPrincipal, type WorkflowSidecarPrincipal, } from "./workflow-kind.js";
21
+ export { workflowRunKindHandler, workflowRunAuthorize, enqueueInbox, StaleInboxEnqueueError, dequeueToProcessing, readProcessingEntry, markConsumed, readOwnedMessageIds, readCommittedWorkflowRunLifecycle, readWorkflowRunLifecycle, replayProcessingToInbox, WORKFLOW_RUN_GITIGNORE_PATH, WORKFLOW_RUN_RUNS_PREFIX, WORKFLOW_RUN_EVENTS_DIR, WORKFLOW_RUN_GRANTS_FILE, WORKFLOW_RUN_AGENT_STATE_PREFIX, WORKFLOW_RUN_ADDRESSES_PREFIX, WORKFLOW_RUN_CONTROL_PREFIX, WORKFLOW_RUN_INBOX_DIR, WORKFLOW_RUN_PROCESSING_DIR, WORKFLOW_RUN_CONSUMED_DIR, WORKFLOW_RUN_WATERMARK_FILE, DEFAULT_CONSUMED_RETENTION_MS, type ClaimCheckEnvelope, type ConsumedEnvelope, type EnqueueAlreadyPresentReason, type EnqueueInboxArgs, type EnqueueInboxOutcome, type EnqueueInboxResult, type DequeueToProcessingResult, type ReadProcessingEntryResult, type MarkConsumedArgs, type MarkConsumedResult, type WorkflowRunLifecycle, type ReplayProcessingToInboxOpts, type ReplayProcessingToInboxResult, type WorkflowRunPrincipal, type WorkflowRunHubPrincipal, type WorkflowRunSidecarPrincipal, type WorkflowRunWorkflowProcessPrincipal, type WorkflowRunSupervisorPrincipal, } from "./workflow-run-kind.js";
22
+ export { restoreWorkflowRunToAllocation, WORKFLOW_RUN_RESTORE_REFS, } from "./workflow-run-restore.js";
23
+ export { createAssetService, AssetServiceError, DEFAULT_ASSET_REF, type AssetService, type Asset, type CreateAssetParams, type PopulateAssetParams, type AssetServiceErrorReason, type ReadAssetBlobParams, type ListAssetBlobsParams, } from "./asset-service.js";
24
+ export { createWorkflowRunReader, type WorkflowRunReader, type WorkflowRunEvent, } from "./workflow-run-reader.js";
25
+ export { WORKFLOW_RUN_EVENTS_FILE, splitCombinedEventLog, encodeCombinedEventLog, } from "./workflow-run-event-log.js";
26
+ export type { AuthorizeFn, CommittedReads, CommittedTreeEntry, CreateRepoStoreConfig, InitRepoOpts, KindHandler, NewlyTerminalRun, Principal, RefEntry, RepoAction, RepoId, RepoStore, SubscribeKindEntry, SubscribeKindOpts, ValidatePushResult, WriteResult, } from "./repo-store/index.js";
27
+ export { createRepoStore, subscribeKind, UserPrincipal } from "./repo-store/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ export { createAgentRepoStore, } from "./agent-repo.js";
2
+ export { createSessionService, SessionLaunchError, bridgeOrchestratorDeployContent, deployCodeSourcedWorkflow, } from "./session-service.js";
3
+ export { installAndApproveWorkflowDefinition, createDbFrozenApprovalWriter, } from "./workflow-probe-gate.js";
4
+ export { committedReadsToSourceTree } from "./committed-source-tree.js";
5
+ export { createEventCollectorRegistry, } from "./event-collector-registry.js";
6
+ export { createSidecarRouter, createSidecarCredentialResolver, createSidecarTokenAuthenticator, createSidecarEmitter, } from "./ws/index.js";
7
+ export { createHubSessionLookups, findRoutableById, parseAgentId, resolveRoutableAddress, resolveRunIdForSession, resolveRunSessionId, runRowToRoutableRecord, } from "./hub-session-lookups.js";
8
+ export { createHubSessionOrchestrator, } from "./hub-session-orchestrator.js";
9
+ export { pushSourceUpdates, pushSourceUpdatesSubtree } from "./credential-push.js";
10
+ export { createSidecarPluginRegistry, createSidecarAllocationReconciler, resolveEffectiveSidecarPlacement, } from "./sidecar-allocation/index.js";
11
+ export { ensureWorkflowDefinitionForAsset } from "./workflow-definition-ensure.js";
12
+ export { workflowSourceAssetMountPath } from "./workflow-closure-resolution.js";
13
+ export { createWorkflowAllocationService, ExclusiveWorkflowPlacementError, resolveWorkflowSidecarPlacement, } from "./workflow-allocation-service.js";
14
+ export { createWorkflowDispatchService, } from "./workflow-dispatch-service.js";
15
+ export { listAcceptedWorkflowDispatches, listConsumedWorkflowDispatches, listReceivedWorkflowSignals, } from "./workflow-dispatch-settlement.js";
16
+ export { skillKindHandler, skillAuthorize, skillFrontmatterSchema, getSkillIndex, } from "./skill-kind.js";
17
+ export { packageRegistryKindHandler, packageRegistryAuthorize, asTarballEntry, validateTarballPackageJSON, TARBALLS_PREFIX, TARBALL_FILENAME_PATTERN, REGISTRY_INDEX_PATH, WORKSPACE_BUILTINS_REGISTRY, } from "./package-registry-kind.js";
18
+ export { workflowKindHandler, workflowAuthorize, workflowDefinitionEnvelopeSchema, WORKFLOW_JSON_PATH, CAPABILITY_DECLARATIONS_JSON_PATH, } from "./workflow-kind.js";
19
+ export { workflowRunKindHandler, workflowRunAuthorize, enqueueInbox, StaleInboxEnqueueError, dequeueToProcessing, readProcessingEntry, markConsumed, readOwnedMessageIds, readCommittedWorkflowRunLifecycle, readWorkflowRunLifecycle, replayProcessingToInbox, WORKFLOW_RUN_GITIGNORE_PATH, WORKFLOW_RUN_RUNS_PREFIX, WORKFLOW_RUN_EVENTS_DIR, WORKFLOW_RUN_GRANTS_FILE, WORKFLOW_RUN_AGENT_STATE_PREFIX, WORKFLOW_RUN_ADDRESSES_PREFIX, WORKFLOW_RUN_CONTROL_PREFIX, WORKFLOW_RUN_INBOX_DIR, WORKFLOW_RUN_PROCESSING_DIR, WORKFLOW_RUN_CONSUMED_DIR, WORKFLOW_RUN_WATERMARK_FILE, DEFAULT_CONSUMED_RETENTION_MS, } from "./workflow-run-kind.js";
20
+ export { restoreWorkflowRunToAllocation, WORKFLOW_RUN_RESTORE_REFS, } from "./workflow-run-restore.js";
21
+ export { createAssetService, AssetServiceError, DEFAULT_ASSET_REF, } from "./asset-service.js";
22
+ export { createWorkflowRunReader, } from "./workflow-run-reader.js";
23
+ export { WORKFLOW_RUN_EVENTS_FILE, splitCombinedEventLog, encodeCombinedEventLog, } from "./workflow-run-event-log.js";
24
+ export { createRepoStore, subscribeKind, UserPrincipal } from "./repo-store/index.js";
@@ -0,0 +1,70 @@
1
+ import type { PackageJSON } from "@intx/types/package-json";
2
+ import { type AuthorizeFn, type KindHandler } from "./repo-store/index.js";
3
+ export declare const TARBALLS_PREFIX = "tarballs/";
4
+ export declare const REGISTRY_INDEX_PATH = "package-registry.json";
5
+ /**
6
+ * Canonical asset name for the workspace's bundled package-registry —
7
+ * the in-tree `@intx/tools-*` packages live in an asset of this name
8
+ * under the workspace's root tenant. The hub's scope-routing config
9
+ * maps `@intx` to this registry, the seed script ensures the asset
10
+ * row exists, and the publish-tool-packages CLI defaults its target
11
+ * registry to this name. The constant lives at one site so a rename
12
+ * does not have to chase three independent string literals.
13
+ *
14
+ * Callers:
15
+ * - `bin/dev.ts` — orchestrator default for the registry the dev
16
+ * stack publishes the built-ins into.
17
+ * - `bin/seed.ts` — seeder that pins the workspace-builtins into
18
+ * the registry asset at boot.
19
+ * - `bin/publish-tool-packages.ts` — CLI default for the
20
+ * `--registry` flag.
21
+ *
22
+ * No test asserts that the seed's pinned built-ins actually land
23
+ * under this exact name; a mismatch between the constant and a
24
+ * caller would surface at apply time as a `tarball.missing`
25
+ * structured failure, not as a build error.
26
+ */
27
+ export declare const WORKSPACE_BUILTINS_REGISTRY = "workspace-builtins";
28
+ /**
29
+ * Filename rule for tarballs in the repo tree. Filenames must end in
30
+ * `.tgz`, start with an alphanumeric / underscore / scope-marker
31
+ * character, and otherwise contain only filename-safe characters. The
32
+ * leading-character constraint forbids hidden-style names like
33
+ * `..tgz` or `.hidden.tgz` that the filesystem treats as dotfiles —
34
+ * those would be invisible to a `readdir` shell expansion and split
35
+ * the resolver's view of the registry tree from the operator's. The
36
+ * same rule is enforced at the REST upload boundary so a push and a
37
+ * PUT cannot produce diverging contents.
38
+ */
39
+ export declare const TARBALL_FILENAME_PATTERN: RegExp;
40
+ /**
41
+ * Validates that `path` belongs to a tarball entry shape
42
+ * `tarballs/<filename>.tgz`. Returns the bare filename when valid,
43
+ * `null` otherwise.
44
+ */
45
+ export declare function asTarballEntry(path: string): string | null;
46
+ type TarballValidationOutcome = {
47
+ ok: true;
48
+ pkg: PackageJSON;
49
+ } | {
50
+ ok: false;
51
+ reason: string;
52
+ };
53
+ /**
54
+ * Open an npm-style tarball, find the top-level `package.json` entry,
55
+ * parse and validate it. Returns the typed descriptor on success; a
56
+ * structured reason otherwise. The shape validation lives inside
57
+ * `extractTarballPackageJSON`; this wrapper only translates the
58
+ * outcome into the substrate's `ValidatePushResult`-shaped reason
59
+ * strings.
60
+ */
61
+ export declare function validateTarballPackageJSON(filename: string, bytes: Uint8Array): Promise<TarballValidationOutcome>;
62
+ export declare const packageRegistryKindHandler: KindHandler;
63
+ /**
64
+ * Authorize policy for package-registry repos. Sidecars never write
65
+ * to package-registry repos (they consume contents through the
66
+ * in-process read API, not the smart-HTTP layer); only the hub and
67
+ * authenticated users with the right grant may push.
68
+ */
69
+ export declare const packageRegistryAuthorize: AuthorizeFn;
70
+ export {};
@@ -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, CommittedReads, CommittedTreeEntry, 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/node";
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 {};