@intx/hub-sessions 0.2.2 → 0.4.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 (87) hide show
  1. package/README.md +3 -5
  2. package/dist/agent-repo.d.ts +23 -7
  3. package/dist/agent-repo.js +19 -6
  4. package/dist/agent-state-kind.js +18 -63
  5. package/dist/asset-service.d.ts +1 -20
  6. package/dist/asset-service.js +19 -97
  7. package/dist/committed-source-tree.d.ts +10 -0
  8. package/dist/committed-source-tree.js +35 -0
  9. package/dist/credential-push.d.ts +52 -7
  10. package/dist/credential-push.js +178 -22
  11. package/dist/event-collector-registry.d.ts +3 -2
  12. package/dist/event-collector-registry.js +42 -13
  13. package/dist/event-collector.d.ts +12 -2
  14. package/dist/event-collector.js +45 -4
  15. package/dist/hub-session-lookups.d.ts +125 -7
  16. package/dist/hub-session-lookups.js +541 -86
  17. package/dist/hub-session-orchestrator.d.ts +2 -3
  18. package/dist/hub-session-orchestrator.js +22 -56
  19. package/dist/index.d.ts +19 -9
  20. package/dist/index.js +16 -7
  21. package/dist/reconciliation-scheduler.d.ts +14 -0
  22. package/dist/reconciliation-scheduler.js +55 -0
  23. package/dist/repo-store/index.d.ts +2 -1
  24. package/dist/repo-store/index.js +1 -0
  25. package/dist/repo-store/store.d.ts +1 -1
  26. package/dist/repo-store/store.js +138 -1
  27. package/dist/repo-store/subscribe-kind.d.ts +6 -3
  28. package/dist/repo-store/subscribe-kind.js +42 -77
  29. package/dist/repo-store/types.d.ts +94 -6
  30. package/dist/repo-store/user-principal-gate.d.ts +26 -0
  31. package/dist/repo-store/user-principal-gate.js +78 -0
  32. package/dist/session-service.d.ts +252 -126
  33. package/dist/session-service.js +851 -624
  34. package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
  35. package/dist/sidecar-allocation/capability-policy.js +124 -0
  36. package/dist/sidecar-allocation/contracts.d.ts +101 -0
  37. package/dist/sidecar-allocation/contracts.js +26 -0
  38. package/dist/sidecar-allocation/index.d.ts +5 -0
  39. package/dist/sidecar-allocation/index.js +4 -0
  40. package/dist/sidecar-allocation/operation.d.ts +10 -0
  41. package/dist/sidecar-allocation/operation.js +54 -0
  42. package/dist/sidecar-allocation/plugin-registry.d.ts +24 -0
  43. package/dist/sidecar-allocation/plugin-registry.js +61 -0
  44. package/dist/sidecar-allocation/reconciler.d.ts +54 -0
  45. package/dist/sidecar-allocation/reconciler.js +825 -0
  46. package/dist/skill-kind.js +12 -62
  47. package/dist/substrate.d.ts +3 -3
  48. package/dist/substrate.js +1 -1
  49. package/dist/workflow-allocation-service.d.ts +64 -0
  50. package/dist/workflow-allocation-service.js +554 -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 +42 -0
  56. package/dist/workflow-dispatch-service.js +209 -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 +29 -1
  60. package/dist/workflow-kind.js +143 -139
  61. package/dist/workflow-probe-gate.d.ts +286 -0
  62. package/dist/workflow-probe-gate.js +382 -0
  63. package/dist/workflow-run-kind.d.ts +239 -32
  64. package/dist/workflow-run-kind.js +874 -188
  65. package/dist/workflow-run-reader.d.ts +1 -1
  66. package/dist/workflow-run-reader.js +3 -7
  67. package/dist/workflow-run-restore.d.ts +16 -0
  68. package/dist/workflow-run-restore.js +30 -0
  69. package/dist/workflow-source-closure.d.ts +35 -0
  70. package/dist/workflow-source-closure.js +342 -0
  71. package/dist/workflow-source-pins.d.ts +8 -0
  72. package/dist/workflow-source-pins.js +14 -0
  73. package/dist/ws/index.d.ts +3 -3
  74. package/dist/ws/index.js +2 -2
  75. package/dist/ws/pending-tracker.d.ts +93 -0
  76. package/dist/ws/pending-tracker.js +132 -0
  77. package/dist/ws/sidecar-events.d.ts +139 -37
  78. package/dist/ws/sidecar-events.js +2 -2
  79. package/dist/ws/sidecar-handler.d.ts +218 -60
  80. package/dist/ws/sidecar-handler.js +1713 -732
  81. package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
  82. package/dist/ws/sidecar-handler.test-helpers.js +95 -0
  83. package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
  84. package/dist/ws/sidecar-token-authenticator.js +78 -7
  85. package/package.json +14 -13
  86. package/dist/available-skills-stanza.d.ts +0 -21
  87. package/dist/available-skills-stanza.js +0 -32
@@ -1,20 +1,19 @@
1
1
  import type { DB } from "@intx/db";
2
- import type { AgentRepoStore } from "./agent-repo.js";
3
2
  import type { EventCollectorRegistry } from "./event-collector-registry.js";
4
3
  import type { SidecarEventEmitter } from "./ws/sidecar-events.js";
4
+ import type { SenderDeploySettledOutcome } from "./ws/sidecar-handler.js";
5
5
  /** Subset of `SidecarRouter` the orchestrator drives outbound. The
6
6
  * narrow surface keeps tests honest and decouples the orchestrator
7
7
  * from the rest of the router API. */
8
8
  export type HubSessionRouterFacade = {
9
- sendPack(agentAddress: string, pack: Uint8Array, ref: string, commitSha: string): Promise<void>;
10
9
  dispatchAgentEvent(agentAddress: string, event: unknown): void;
10
+ noteSenderDeploySettled(address: string, outcome: SenderDeploySettledOutcome): void;
11
11
  };
12
12
  export type HubSessionOrchestratorDeps = {
13
13
  events: SidecarEventEmitter;
14
14
  router: HubSessionRouterFacade;
15
15
  db: DB["db"];
16
16
  eventCollectors: EventCollectorRegistry;
17
- agentRepoStore: AgentRepoStore;
18
17
  };
19
18
  export type HubSessionOrchestrator = {
20
19
  /** Unsubscribe all listeners. Tests use this between cases; the hub
@@ -11,15 +11,13 @@
11
11
  // with a small stub and an isolated emitter.
12
12
  import { eq } from "drizzle-orm";
13
13
  import { type } from "arktype";
14
- import { agentInstance, workflowDeployment } from "@intx/db/schema";
14
+ import { workflowRun } from "@intx/db/schema";
15
15
  import { parseMailToEmail } from "@intx/mime";
16
16
  import { parseInferenceEvent } from "@intx/types/runtime";
17
17
  import { getLogger } from "@intx/log";
18
- import { isWorkflowDerivedAddress } from "@intx/workflow-deploy";
19
- import { findInstance, parseAgentId, requireInstance, } from "./hub-session-lookups.js";
20
18
  const log = getLogger(["hub", "orchestrator"]);
21
19
  export function createHubSessionOrchestrator(deps) {
22
- const { events, router, db, eventCollectors, agentRepoStore } = deps;
20
+ const { events, router, db, eventCollectors } = deps;
23
21
  const unsubscribers = [];
24
22
  unsubscribers.push(events.on("agent.event", ({ agentAddress, event }) => {
25
23
  const validated = parseInferenceEvent(event);
@@ -45,61 +43,29 @@ export function createHubSessionOrchestrator(deps) {
45
43
  recipients: recipients.join(", "),
46
44
  });
47
45
  }));
48
- unsubscribers.push(events.on("agent.deploy.ack", async ({ agentAddress, publicKey }) => {
49
- // Workflow-derived addresses (the deployment-level
50
- // `ins_<deploymentId>@<domain>` and the per-step
51
- // `ins_<deploymentId>-<stepId>@<domain>`) have no agent_instance row;
52
- // their public key lives on the workflow_deployment projection row,
53
- // keyed by address. Persist it there so the reconnect ownership
54
- // challenge can verify the deployment address. Only the
55
- // deployment-level address has a row, so a stray per-step ack updates
56
- // nothing. (This was previously a no-op, which is what left
57
- // workflow-deployment addresses un-verifiable on reconnect.)
58
- if (isWorkflowDerivedAddress(agentAddress)) {
59
- await db
60
- .update(workflowDeployment)
61
- .set({ publicKey })
62
- .where(eq(workflowDeployment.address, agentAddress));
46
+ unsubscribers.push(events.on("agent.deploy.ack", async (event) => {
47
+ const { agentAddress, publicKey, allocated } = event;
48
+ // Provisioned initialization publishes its key only after every deploy
49
+ // and asset pack succeeds under the allocation generation fence.
50
+ if (allocated !== undefined)
63
51
  return;
64
- }
65
- // A launched agent has an agent_instance row; a missing one is a bug
66
- // to surface, not to drop silently.
67
- const instance = await findInstance(db, agentAddress);
68
- if (instance === undefined) {
69
- throw new Error(`No active instance found for deploy ack on address "${agentAddress}"`);
70
- }
52
+ // Every deploy address names a workflow run whose public key lives on its
53
+ // single self-anchored workflow_run row, keyed by address. Persist it
54
+ // there as the deployment's published identity. Reconnect routing is
55
+ // allocation-authenticated, so this projection is not connection
56
+ // authority. Only the deployment-level address owns a row, so a stray
57
+ // per-step ack updates nothing.
71
58
  await db
72
- .update(agentInstance)
59
+ .update(workflowRun)
73
60
  .set({ publicKey })
74
- .where(eq(agentInstance.id, instance.id));
75
- }));
76
- unsubscribers.push(events.on("agent.reconnected", async ({ agentAddress }) => {
77
- const instance = await requireInstance(db, agentAddress);
78
- if (!instance.sessionId) {
79
- throw new Error(`Agent "${agentAddress}" reconnected but has no active session`);
80
- }
81
- const sessionId = instance.sessionId;
82
- // A supervised deployment carries its grants and sources in the
83
- // deploy pack and refreshes them over the supervisor's IPC
84
- // credentials snapshot at spawn and recycle, so reconnect does not
85
- // re-push them over the wire.
86
- const now = new Date();
87
- if (instance.status !== "running") {
88
- await db
89
- .update(agentInstance)
90
- .set({ status: "running", updatedAt: now })
91
- .where(eq(agentInstance.id, instance.id));
92
- }
93
- if (!eventCollectors.has(agentAddress)) {
94
- eventCollectors.create(agentAddress, instance.tenantId, sessionId, instance.id);
95
- log.info("Restored event collector for reconnected agent {agentAddress}", { agentAddress });
96
- }
97
- }));
98
- unsubscribers.push(events.on("deploy.ref.stale", async ({ agentAddress }) => {
99
- const agentId = parseAgentId(agentAddress);
100
- const { pack, commitSha, ref } = await agentRepoStore.createDeployPack(agentId);
101
- await router.sendPack(agentAddress, pack, ref, commitSha);
102
- log.info("Re-deployed stale agent {agentAddress}", { agentAddress });
61
+ .where(eq(workflowRun.address, agentAddress));
62
+ // The key is now durable. Wake any mail the run parked while pre-ack so it
63
+ // is delivered with the sender key co-delivered, closing the window where
64
+ // a run sends before its key is recorded. The write above happens-before
65
+ // this settle, so a re-drive resolves the recorded key. The address is the
66
+ // run's own deploy address, byte-identical to the sender address its mail
67
+ // was sent under.
68
+ router.noteSenderDeploySettled(agentAddress, { recorded: publicKey });
103
69
  }));
104
70
  unsubscribers.push(events.on("mail.persisted", (row) => {
105
71
  const parsed = parseMailToEmail(row.raw, row.id);
package/dist/index.d.ts CHANGED
@@ -1,18 +1,28 @@
1
1
  export { createAgentRepoStore, type AgentRepoStore, type DeployContent, } from "./agent-repo.js";
2
- export { createSessionService, SessionLaunchError, bridgeOrchestratorDeployContent, type SessionService, type DeployWorkflowDefinitionParams, type DeployWorkflowDefinitionResult, } from "./session-service.js";
2
+ export { createSessionService, recoverSenderDeploy, SessionLaunchError, bridgeOrchestratorDeployContent, deployCodeSourcedWorkflow, type SessionService, type DeployWorkflowDefinitionResult, 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";
3
6
  export type { WorkflowDefinition } from "@intx/workflow/definition";
4
7
  export { createEventCollectorRegistry, type EventCollectorRegistry, } from "./event-collector-registry.js";
5
- export { createSidecarRouter, type SidecarRouter, type SidecarRouterConfig, type SidecarAuthIdentity, type SidecarAuthenticator, createSidecarTokenAuthenticator, type CreateSidecarTokenAuthenticatorDeps, type WsHandle, createSidecarEmitter, type SidecarEventEmitter, type SidecarEventMap, type SidecarEventType, type SidecarEventListener, type SidecarLookups, type SidecarMailPersistedPayload, type SidecarMailPersistedRow, } from "./ws/index.js";
6
- export { createHubSessionLookups, parseAgentId, type HubSessionLookupsDeps, } from "./hub-session-lookups.js";
8
+ export { createSidecarRouter, type SidecarRouter, type SidecarRouterConfig, type SenderDeploySettledOutcome, type AllocatedSenderDeployAttempt, type SidecarAuthIdentity, type SidecarAuthenticator, type AllocatedSidecarTarget, type SidecarAllocationRouter, SidecarIdentityValidationError, 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";
7
10
  export { createHubSessionOrchestrator, type HubSessionOrchestrator, type HubSessionOrchestratorDeps, type HubSessionRouterFacade, } from "./hub-session-orchestrator.js";
8
- export { pushSourceUpdates, pushSourceUpdatesSubtree } from "./credential-push.js";
11
+ export { pushSourceUpdates, pushSourceUpdatesSubtree, pushCredentialRevoke, pushCredentialReconcile, } from "./credential-push.js";
12
+ export { chooseFirstSidecarProvisioner, createSidecarPluginRegistry, createSidecarAllocationReconciler, type CreateSidecarPluginRegistryOpts, type DestroySidecarRequest, type DestroySidecarResult, type EnsureSidecarRequest, type EnsureSidecarResult, type SidecarCredentialIdentity, type SidecarCredentialResolver, type SidecarOperationFailure, type SidecarPluginRegistry, type SidecarProvisioner, type SidecarProvisionerChooser, type SidecarAllocationReconciler, type SidecarAllocationReconcilerDeps, type SidecarReconciliationContext, } from "./sidecar-allocation/index.js";
13
+ export { ensureWorkflowDefinitionForAsset } from "./workflow-definition-ensure.js";
14
+ export { workflowSourceAssetMountPath } from "./workflow-closure-resolution.js";
15
+ export { createReconciliationScheduler, DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY, type ReconciliationSchedulerOptions, } from "./reconciliation-scheduler.js";
16
+ export { createWorkflowAllocationService, WorkflowProvisioningError, type PrepareProvisionedWorkflowDeploymentArgs, type PreparedProvisionedWorkflowDeployment, type WorkflowAllocationService, type WorkflowAllocationServiceDeps, } from "./workflow-allocation-service.js";
17
+ export { createWorkflowDispatchService, type WorkflowDispatchAcknowledgement, type WorkflowDispatchService, type WorkflowDispatchServiceDeps, } from "./workflow-dispatch-service.js";
18
+ export { listAcceptedWorkflowDispatches, listConsumedWorkflowDispatches, listReceivedWorkflowSignals, type AcceptedWorkflowDispatch, type ConsumedWorkflowDispatch, type ReceivedWorkflowSignal, } from "./workflow-dispatch-settlement.js";
9
19
  export { skillKindHandler, skillAuthorize, skillFrontmatterSchema, getSkillIndex, type SkillIndexEntry, type SkillFrontmatter, type SkillPrincipal, type SkillHubPrincipal, type SkillSidecarPrincipal, } from "./skill-kind.js";
10
20
  export { packageRegistryKindHandler, packageRegistryAuthorize, asTarballEntry, validateTarballPackageJSON, TARBALLS_PREFIX, TARBALL_FILENAME_PATTERN, REGISTRY_INDEX_PATH, WORKSPACE_BUILTINS_REGISTRY, } from "./package-registry-kind.js";
11
- export { workflowKindHandler, workflowAuthorize, workflowDefinitionEnvelopeSchema, WORKFLOW_JSON_PATH, CAPABILITY_DECLARATIONS_JSON_PATH, WORKFLOW_GITIGNORE_PATH, type WorkflowPrincipal, type WorkflowHubPrincipal, type WorkflowSidecarPrincipal, } from "./workflow-kind.js";
12
- export { workflowRunKindHandler, workflowRunAuthorize, enqueueInbox, dequeueToProcessing, readProcessingEntry, markConsumed, readOwnedMessageIds, replayProcessingToInbox, WORKFLOW_RUN_GITIGNORE_PATH, WORKFLOW_RUN_RUNS_PREFIX, WORKFLOW_RUN_EVENTS_DIR, 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 EnqueueInboxArgs, type EnqueueInboxResult, type DequeueToProcessingResult, type ReadProcessingEntryResult, type MarkConsumedArgs, type MarkConsumedResult, type ReplayProcessingToInboxOpts, type ReplayProcessingToInboxResult, type WorkflowRunPrincipal, type WorkflowRunHubPrincipal, type WorkflowRunSidecarPrincipal, type WorkflowRunWorkflowProcessPrincipal, type WorkflowRunSupervisorPrincipal, } from "./workflow-run-kind.js";
13
- export { createAssetService, AssetServiceError, DEFAULT_ASSET_REF, type AssetService, type Asset, type AgentAsset, type AgentAssetWithAsset, type AccessMode, type CreateAssetParams, type PopulateAssetParams, type AttachAssetParams, type AssetServiceErrorReason, type ReadAssetBlobParams, type ListAssetBlobsParams, } from "./asset-service.js";
14
- export { buildAvailableSkillsStanza, type AvailableSkillEntry, } from "./available-skills-stanza.js";
21
+ export { workflowKindHandler, workflowAuthorize, workflowDefinitionEnvelopeSchema, WORKFLOW_JSON_PATH, CAPABILITY_DECLARATIONS_JSON_PATH, type WorkflowPrincipal, type WorkflowHubPrincipal, type WorkflowSidecarPrincipal, } from "./workflow-kind.js";
22
+ export { workflowRunKindHandler, workflowRunAuthorize, enqueueInbox, StaleInboxEnqueueError, dequeueToProcessing, readProcessingEntry, markConsumed, classifyTerminalEvent, scanRunsForBoot, 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";
23
+ export { restoreWorkflowRunToAllocation, WORKFLOW_RUN_RESTORE_REFS, } from "./workflow-run-restore.js";
24
+ export { createAssetService, AssetServiceError, DEFAULT_ASSET_REF, type AssetService, type Asset, type CreateAssetParams, type PopulateAssetParams, type AssetServiceErrorReason, type ReadAssetBlobParams, type ListAssetBlobsParams, } from "./asset-service.js";
15
25
  export { createWorkflowRunReader, type WorkflowRunReader, type WorkflowRunEvent, } from "./workflow-run-reader.js";
16
26
  export { WORKFLOW_RUN_EVENTS_FILE, splitCombinedEventLog, encodeCombinedEventLog, } from "./workflow-run-event-log.js";
17
- export type { AuthorizeFn, CreateRepoStoreConfig, InitRepoOpts, KindHandler, NewlyTerminalRun, Principal, RefEntry, RepoAction, RepoId, RepoStore, SubscribeKindEntry, SubscribeKindOpts, ValidatePushResult, WriteResult, } from "./repo-store/index.js";
27
+ export type { AuthorizeFn, CommittedReads, CommittedTreeEntry, CreateRepoStoreConfig, InitRepoOpts, KindHandler, NewlyTerminalRun, Principal, RefEntry, RepoAction, RepoId, RepoStore, SubscribeKindEntry, SubscribeKindOpts, ValidatePushResult, WriteResult, } from "./repo-store/index.js";
18
28
  export { createRepoStore, subscribeKind, UserPrincipal } from "./repo-store/index.js";
package/dist/index.js CHANGED
@@ -1,16 +1,25 @@
1
1
  export { createAgentRepoStore, } from "./agent-repo.js";
2
- export { createSessionService, SessionLaunchError, bridgeOrchestratorDeployContent, } from "./session-service.js";
2
+ export { createSessionService, recoverSenderDeploy, SessionLaunchError, bridgeOrchestratorDeployContent, deployCodeSourcedWorkflow, } from "./session-service.js";
3
+ export { installAndApproveWorkflowDefinition, createDbFrozenApprovalWriter, } from "./workflow-probe-gate.js";
4
+ export { committedReadsToSourceTree } from "./committed-source-tree.js";
3
5
  export { createEventCollectorRegistry, } from "./event-collector-registry.js";
4
- export { createSidecarRouter, createSidecarTokenAuthenticator, createSidecarEmitter, } from "./ws/index.js";
5
- export { createHubSessionLookups, parseAgentId, } from "./hub-session-lookups.js";
6
+ export { createSidecarRouter, SidecarIdentityValidationError, createSidecarCredentialResolver, createSidecarTokenAuthenticator, createSidecarEmitter, } from "./ws/index.js";
7
+ export { createHubSessionLookups, findRoutableById, parseAgentId, resolveRoutableAddress, resolveRunIdForSession, resolveRunSessionId, runRowToRoutableRecord, } from "./hub-session-lookups.js";
6
8
  export { createHubSessionOrchestrator, } from "./hub-session-orchestrator.js";
7
- export { pushSourceUpdates, pushSourceUpdatesSubtree } from "./credential-push.js";
9
+ export { pushSourceUpdates, pushSourceUpdatesSubtree, pushCredentialRevoke, pushCredentialReconcile, } from "./credential-push.js";
10
+ export { chooseFirstSidecarProvisioner, createSidecarPluginRegistry, createSidecarAllocationReconciler, } from "./sidecar-allocation/index.js";
11
+ export { ensureWorkflowDefinitionForAsset } from "./workflow-definition-ensure.js";
12
+ export { workflowSourceAssetMountPath } from "./workflow-closure-resolution.js";
13
+ export { createReconciliationScheduler, DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY, } from "./reconciliation-scheduler.js";
14
+ export { createWorkflowAllocationService, WorkflowProvisioningError, } from "./workflow-allocation-service.js";
15
+ export { createWorkflowDispatchService, } from "./workflow-dispatch-service.js";
16
+ export { listAcceptedWorkflowDispatches, listConsumedWorkflowDispatches, listReceivedWorkflowSignals, } from "./workflow-dispatch-settlement.js";
8
17
  export { skillKindHandler, skillAuthorize, skillFrontmatterSchema, getSkillIndex, } from "./skill-kind.js";
9
18
  export { packageRegistryKindHandler, packageRegistryAuthorize, asTarballEntry, validateTarballPackageJSON, TARBALLS_PREFIX, TARBALL_FILENAME_PATTERN, REGISTRY_INDEX_PATH, WORKSPACE_BUILTINS_REGISTRY, } from "./package-registry-kind.js";
10
- export { workflowKindHandler, workflowAuthorize, workflowDefinitionEnvelopeSchema, WORKFLOW_JSON_PATH, CAPABILITY_DECLARATIONS_JSON_PATH, WORKFLOW_GITIGNORE_PATH, } from "./workflow-kind.js";
11
- export { workflowRunKindHandler, workflowRunAuthorize, enqueueInbox, dequeueToProcessing, readProcessingEntry, markConsumed, readOwnedMessageIds, replayProcessingToInbox, WORKFLOW_RUN_GITIGNORE_PATH, WORKFLOW_RUN_RUNS_PREFIX, WORKFLOW_RUN_EVENTS_DIR, 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";
19
+ export { workflowKindHandler, workflowAuthorize, workflowDefinitionEnvelopeSchema, WORKFLOW_JSON_PATH, CAPABILITY_DECLARATIONS_JSON_PATH, } from "./workflow-kind.js";
20
+ export { workflowRunKindHandler, workflowRunAuthorize, enqueueInbox, StaleInboxEnqueueError, dequeueToProcessing, readProcessingEntry, markConsumed, classifyTerminalEvent, scanRunsForBoot, 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";
21
+ export { restoreWorkflowRunToAllocation, WORKFLOW_RUN_RESTORE_REFS, } from "./workflow-run-restore.js";
12
22
  export { createAssetService, AssetServiceError, DEFAULT_ASSET_REF, } from "./asset-service.js";
13
- export { buildAvailableSkillsStanza, } from "./available-skills-stanza.js";
14
23
  export { createWorkflowRunReader, } from "./workflow-run-reader.js";
15
24
  export { WORKFLOW_RUN_EVENTS_FILE, splitCombinedEventLog, encodeCombinedEventLog, } from "./workflow-run-event-log.js";
16
25
  export { createRepoStore, subscribeKind, UserPrincipal } from "./repo-store/index.js";
@@ -0,0 +1,14 @@
1
+ export declare const DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY = 8;
2
+ export type ReconciliationSchedulerOptions = {
3
+ readonly name: string;
4
+ /** Claim and process at most one item; false means no due work was found. */
5
+ readonly reconcileNext: () => Promise<boolean>;
6
+ readonly concurrency?: number;
7
+ readonly intervalMs?: number;
8
+ };
9
+ /** Polls for work without waiting for other occupied slots to finish. */
10
+ export declare function createReconciliationScheduler({ name, reconcileNext, concurrency, intervalMs, }: ReconciliationSchedulerOptions): {
11
+ start(): void;
12
+ stop(): void;
13
+ wake: () => void;
14
+ };
@@ -0,0 +1,55 @@
1
+ import { getLogger } from "@intx/log";
2
+ const logger = getLogger(["hub", "reconciliation"]);
3
+ export const DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY = 8;
4
+ /** Polls for work without waiting for other occupied slots to finish. */
5
+ export function createReconciliationScheduler({ name, reconcileNext, concurrency = DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY, intervalMs = 1_000, }) {
6
+ if (!Number.isSafeInteger(concurrency) || concurrency <= 0) {
7
+ throw new Error("Reconciliation concurrency must be a positive integer");
8
+ }
9
+ if (!Number.isSafeInteger(intervalMs) || intervalMs <= 0) {
10
+ throw new Error("Reconciliation interval must be a positive integer");
11
+ }
12
+ let stopped = true;
13
+ let active = 0;
14
+ let timer;
15
+ function finish(worked) {
16
+ active -= 1;
17
+ if (worked)
18
+ wake();
19
+ }
20
+ function wake() {
21
+ if (stopped)
22
+ return;
23
+ while (active < concurrency) {
24
+ active += 1;
25
+ void Promise.resolve()
26
+ .then(() => (stopped ? false : reconcileNext()))
27
+ .then(finish, (error) => {
28
+ logger.error `${name} reconciliation failed: ${error instanceof Error ? error.message : String(error)}`;
29
+ finish(false);
30
+ });
31
+ }
32
+ }
33
+ function schedule(delayMs) {
34
+ timer = setTimeout(() => {
35
+ if (stopped)
36
+ return;
37
+ wake();
38
+ schedule(intervalMs);
39
+ }, delayMs);
40
+ timer.unref?.();
41
+ }
42
+ return {
43
+ start() {
44
+ if (!stopped)
45
+ return;
46
+ stopped = false;
47
+ schedule(0);
48
+ },
49
+ stop() {
50
+ stopped = true;
51
+ clearTimeout(timer);
52
+ },
53
+ wake,
54
+ };
55
+ }
@@ -1,4 +1,5 @@
1
- export type { AuthorizeFn, InitRepoOpts, KindHandler, NewlyTerminalRun, PriorDeltaReads, Principal, RefEntry, RepoAction, RepoId, RepoKind, RepoStore, RepoStoreSubscribeEvent, TreeContent, ValidatePushResult, WriteResult, WriteTreePreservingPrefixArgs, } from "./types.js";
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
2
  export { UserPrincipal } from "./types.js";
3
+ export { authorizeUserPrincipal, type AuthorizeUserPrincipalArgs, } from "./user-principal-gate.js";
3
4
  export { createRepoStore, type CreateRepoStoreConfig } from "./store.js";
4
5
  export { subscribeKind, type SubscribeKindOpts, type SubscribeKindEntry, } from "./subscribe-kind.js";
@@ -1,3 +1,4 @@
1
1
  export { UserPrincipal } from "./types.js";
2
+ export { authorizeUserPrincipal, } from "./user-principal-gate.js";
2
3
  export { createRepoStore } from "./store.js";
3
4
  export { subscribeKind, } from "./subscribe-kind.js";
@@ -1,4 +1,4 @@
1
- import { type CommitSigner, type GCPolicy } from "@intx/storage-isogit";
1
+ import { type CommitSigner, type GCPolicy } from "@intx/storage-isogit/node";
2
2
  import type { AuthorizeFn, KindHandler, RepoId, RepoKind, RepoStore } from "./types.js";
3
3
  type SigningKey = {
4
4
  privateKey: Uint8Array;
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import git, {} from "isomorphic-git";
4
4
  import { createSSHSignature } from "@intx/crypto";
5
- import { initRepo as storageInitRepo, createDeployPack, receivePackObjects, collectReachableObjects, maybeGC, } from "@intx/storage-isogit";
5
+ import { initRepo as storageInitRepo, createDeployPack, receivePackObjects, collectReachableObjects, maybeGC, } from "@intx/storage-isogit/node";
6
6
  import { hasCode } from "@intx/types";
7
7
  import { getLogger } from "@intx/log";
8
8
  import { SAFE_REPO_ID } from "./types.js";
@@ -638,6 +638,44 @@ export function createRepoStore(config) {
638
638
  out.set(e.path, e.oid);
639
639
  return out;
640
640
  }
641
+ // git tree-entry modes an admitted asset tree must not carry: a symlink can
642
+ // escape its directory when the tree is checked out, and a submodule gitlink
643
+ // is always a dangling reference (a pushed pack ships no submodule object).
644
+ const GIT_MODE_SYMLINK = "120000";
645
+ const GIT_MODE_SUBMODULE = "160000";
646
+ // Walk a commit's tree and return the first entry whose mode is disallowed
647
+ // (a symlink or a submodule), or null when the tree is clean. This mirrors
648
+ // the materialization backstop in `writeTreeToDisk`, but at the PUSH boundary
649
+ // so a directory-escaping symlink (or a dangling gitlink) is refused at
650
+ // admission rather than only caught later at checkout. It reads trees only,
651
+ // never blobs; `seen` deduplicates shared subtree oids across the commits of
652
+ // one pack so a subtree is walked once.
653
+ async function findDisallowedTreeMode(dir, treeOid, seen, prefix = "") {
654
+ if (seen.has(treeOid))
655
+ return null;
656
+ seen.add(treeOid);
657
+ const { tree } = await git.readTree({
658
+ fs,
659
+ dir,
660
+ cache: cacheFor(dir),
661
+ oid: treeOid,
662
+ });
663
+ for (const entry of tree) {
664
+ const entryPath = prefix === "" ? entry.path : `${prefix}/${entry.path}`;
665
+ if (entry.mode === GIT_MODE_SYMLINK) {
666
+ return { path: entryPath, kind: "symlink" };
667
+ }
668
+ if (entry.type === "commit" || entry.mode === GIT_MODE_SUBMODULE) {
669
+ return { path: entryPath, kind: "submodule" };
670
+ }
671
+ if (entry.type === "tree") {
672
+ const nested = await findDisallowedTreeMode(dir, entry.oid, seen, entryPath);
673
+ if (nested !== null)
674
+ return nested;
675
+ }
676
+ }
677
+ return null;
678
+ }
641
679
  // Bound the set of paths a received-pack commit may have changed
642
680
  // relative to its parent, as repo-root-relative POSIX prefixes ending
643
681
  // in `/`. Git is content-addressed: a subtree whose object id is
@@ -1081,6 +1119,13 @@ export function createRepoStore(config) {
1081
1119
  const { priorReadBlob, priorListDir, priorListDirOids } = buildPriorTreeClosures(dir, parentCommitSha);
1082
1120
  const prospective = buildTreeReadClosures(dir, newRootTreeOid);
1083
1121
  const changedPathPrefixes = w.changedPathPrefixes;
1122
+ // No disallowed-mode (symlink/submodule) gate here on purpose: the
1123
+ // substrate mode gate lives on the pack-receive path. This write path
1124
+ // cannot introduce a `120000`/`160000` entry -- `assembleTree` writes puts
1125
+ // as `100644` and passes base-tree entries through unchanged, and no
1126
+ // ingress ever admits such a mode into a base tree (the pack path is now
1127
+ // gated; this path only writes `100644`). Adding a walk here would gate a
1128
+ // tree that cannot carry those modes.
1084
1129
  const validation = await handler.validatePush({
1085
1130
  repoId,
1086
1131
  ref,
@@ -1325,6 +1370,7 @@ export function createRepoStore(config) {
1325
1370
  existingCommitsCache.set(existingKey, existingCommits);
1326
1371
  }
1327
1372
  const newCommitsFromPack = [];
1373
+ const newlyTerminalRuns = [];
1328
1374
  const oldSha = await receivePackObjects(dir, pack, ref, commitSha, transferId, expectedOldSha,
1329
1375
  // A pack may carry more than one new commit (e.g. supervisor
1330
1376
  // bootstrap that batches enqueue + dequeue before the hub has
@@ -1340,6 +1386,9 @@ export function createRepoStore(config) {
1340
1386
  async () => {
1341
1387
  const newCommits = await collectNewCommits(dir, commitSha, expectedOldSha, existingCommits);
1342
1388
  newCommitsFromPack.push(...newCommits);
1389
+ // Shared across the pack's commits so a subtree common to several is
1390
+ // walked once by the disallowed-mode gate below.
1391
+ const seenTreeOids = new Set();
1343
1392
  for (const newCommit of newCommits) {
1344
1393
  const { commit: parsed } = await git.readCommit({
1345
1394
  fs,
@@ -1391,6 +1440,22 @@ export function createRepoStore(config) {
1391
1440
  const { priorReadBlob, priorListDir, priorListDirOids } = buildPriorTreeClosures(dir, parentSha);
1392
1441
  const { topLevelTreePaths, readBlob, listDir } = await buildCommitTreeClosures(dir, newCommit);
1393
1442
  const changedPathPrefixes = await computeChangedPathPrefixes(dir, newCommit, parentSha);
1443
+ // Admission gate for EVERY kind, before the per-kind validatePush:
1444
+ // an asset tree carrying a symlink (which can escape its directory
1445
+ // at checkout) or a submodule (a dangling gitlink the pack cannot
1446
+ // reproduce) is refused here, so the ref never advances. No kind
1447
+ // legitimately pushes either; `writeTreeToDisk` rejects both at
1448
+ // materialization as the defense-in-depth backstop. Per-commit, so
1449
+ // an intermediate commit that adds a symlink a later commit deletes
1450
+ // is still caught.
1451
+ const disallowed = await findDisallowedTreeMode(dir, parsed.tree, seenTreeOids);
1452
+ if (disallowed !== null) {
1453
+ // The substrate owns the `path_violation:` prefix on the thrown
1454
+ // message, so the reason omits it (matching every handler reason).
1455
+ const reason = `${disallowed.kind} at ${disallowed.path} is not allowed in a pushed asset tree (commit ${newCommit})`;
1456
+ logger.debug `validatePush rejected ${repoId.kind}/${repoId.id} on ${ref} at commit ${newCommit}: ${reason}`;
1457
+ return { ok: false, reason };
1458
+ }
1394
1459
  const result = await handler.validatePush({
1395
1460
  repoId,
1396
1461
  ref,
@@ -1407,6 +1472,9 @@ export function createRepoStore(config) {
1407
1472
  logger.debug `validatePush rejected ${repoId.kind}/${repoId.id} on ${ref} at commit ${newCommit}: ${result.reason}`;
1408
1473
  return { ok: false, reason: result.reason };
1409
1474
  }
1475
+ if (result.newlyTerminalRuns !== undefined) {
1476
+ newlyTerminalRuns.push(...result.newlyTerminalRuns);
1477
+ }
1410
1478
  }
1411
1479
  return true;
1412
1480
  });
@@ -1420,6 +1488,7 @@ export function createRepoStore(config) {
1420
1488
  await handler.onRefUpdated({ repoId, ref, oldSha, newSha: commitSha });
1421
1489
  await emitRefUpdate(repoId, ref, oldSha, commitSha);
1422
1490
  await maybeRunGC(repoId);
1491
+ return newlyTerminalRuns;
1423
1492
  });
1424
1493
  }
1425
1494
  async function createPack(principal, repoId, ref) {
@@ -1528,6 +1597,72 @@ export function createRepoStore(config) {
1528
1597
  gateAccess(principal, repoId, ref, "resolveRef");
1529
1598
  return resolveRefSha(repoDir(repoId), ref);
1530
1599
  }
1600
+ // Build the committed-read closures pinned to a commit. Shared by the
1601
+ // by-ref (`openCommittedReads`) and by-commit
1602
+ // (`openCommittedReadsAtCommit`) entrypoints, which differ only in how
1603
+ // they obtain and validate the commit SHA.
1604
+ function committedReadsAt(dir, commitSha) {
1605
+ const { readBlobByOid } = buildPriorTreeClosures(dir, commitSha);
1606
+ const treeOid = async (relPath) => {
1607
+ // "." and "" both name the root tree; resolveTreeEntry treats the
1608
+ // empty string as the root, so normalize "." to it.
1609
+ const normalized = relPath === "." ? "" : relPath;
1610
+ return resolveTreeEntry(dir, commitSha, normalized, "tree");
1611
+ };
1612
+ const listDir = async (relPath) => {
1613
+ const oid = await treeOid(relPath);
1614
+ if (oid === null)
1615
+ return [];
1616
+ const { tree } = await git.readTree({
1617
+ fs,
1618
+ dir,
1619
+ cache: cacheFor(dir),
1620
+ oid,
1621
+ });
1622
+ return tree.map((e) => ({ name: e.path, oid: e.oid, type: e.type }));
1623
+ };
1624
+ return { listDir, readBlobByOid, treeOid };
1625
+ }
1626
+ async function repoHasGitDir(dir) {
1627
+ return fs.promises
1628
+ .stat(path.join(dir, ".git"))
1629
+ .then(() => true)
1630
+ .catch(() => false);
1631
+ }
1632
+ async function openCommittedReads(principal, repoId, ref) {
1633
+ gateAccess(principal, repoId, ref, "resolveRef");
1634
+ const dir = repoDir(repoId);
1635
+ if (!(await repoHasGitDir(dir)))
1636
+ return null;
1637
+ const commitSha = await resolveRefSha(dir, ref);
1638
+ if (commitSha === null)
1639
+ return null;
1640
+ return committedReadsAt(dir, commitSha);
1641
+ }
1642
+ async function openCommittedReadsAtCommit(principal, repoId, commitSha) {
1643
+ // No single ref to gate on; use the same `"*"`/`resolveRef` bulk-read
1644
+ // gate that `listRefs` and `resolveHead` use.
1645
+ gateAccess(principal, repoId, "*", "resolveRef");
1646
+ if (!/^[0-9a-f]{40}$/.test(commitSha)) {
1647
+ throw new Error(`commit_sha_invalid: ${commitSha}`);
1648
+ }
1649
+ const dir = repoDir(repoId);
1650
+ if (!(await repoHasGitDir(dir)))
1651
+ return null;
1652
+ // Confirm the commit object is present so a pruned commit reads as a
1653
+ // clean `null` rather than a lazy throw on the first tree walk.
1654
+ const present = await git
1655
+ .readCommit({ fs, dir, cache: cacheFor(dir), oid: commitSha })
1656
+ .then(() => true)
1657
+ .catch((err) => {
1658
+ if (hasCode(err) && err.code === "NotFoundError")
1659
+ return false;
1660
+ throw err;
1661
+ });
1662
+ if (!present)
1663
+ return null;
1664
+ return committedReadsAt(dir, commitSha);
1665
+ }
1531
1666
  function subscribe(principal, repoId, ref, opts) {
1532
1667
  gateAccess(principal, repoId, ref, "resolveRef");
1533
1668
  const bufferLimit = opts.bufferLimit ?? DEFAULT_SUBSCRIBE_BUFFER_LIMIT;
@@ -1687,6 +1822,8 @@ export function createRepoStore(config) {
1687
1822
  listRefs,
1688
1823
  resolveHead,
1689
1824
  getRepoDir,
1825
+ openCommittedReads,
1826
+ openCommittedReadsAtCommit,
1690
1827
  subscribe,
1691
1828
  };
1692
1829
  }
@@ -40,9 +40,12 @@ export type SubscribeKindEntry<T> = {
40
40
  * kinds filter against the inner `type` discriminator, and yields one
41
41
  * `{ seq, event }` entry per matching blob.
42
42
  *
43
- * The path layout is owned here, not in the substrate: the substrate
44
- * `subscribe` is a generic ref-tail primitive and knows nothing about
45
- * workflow-runs.
43
+ * The path-layout vocabulary (the `runs/`/`events/` prefixes and the
44
+ * `<seq>.json` filename shape) lives in the workflow-run kind handler,
45
+ * which is the authority for what may land under this prefix; this
46
+ * helper imports it rather than re-encoding it. The substrate
47
+ * `subscribe` it wraps is a generic ref-tail primitive and knows
48
+ * nothing about workflow-runs.
46
49
  *
47
50
  * Diff scope: the helper enumerates event blobs present in the new
48
51
  * commit but absent from the old commit. A commit that does not add