@intx/hub-sessions 0.2.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 (73) hide show
  1. package/README.md +3 -5
  2. package/dist/agent-repo.d.ts +9 -5
  3. package/dist/agent-repo.js +2 -2
  4. package/dist/agent-state-kind.js +4 -0
  5. package/dist/asset-service.d.ts +1 -20
  6. package/dist/asset-service.js +9 -91
  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 +7 -6
  10. package/dist/credential-push.js +42 -18
  11. package/dist/event-collector-registry.d.ts +1 -1
  12. package/dist/event-collector-registry.js +4 -4
  13. package/dist/event-collector.d.ts +1 -1
  14. package/dist/event-collector.js +10 -2
  15. package/dist/hub-session-lookups.d.ts +125 -7
  16. package/dist/hub-session-lookups.js +539 -80
  17. package/dist/hub-session-orchestrator.js +14 -49
  18. package/dist/index.d.ts +17 -8
  19. package/dist/index.js +14 -6
  20. package/dist/repo-store/index.d.ts +1 -1
  21. package/dist/repo-store/store.d.ts +1 -1
  22. package/dist/repo-store/store.js +138 -1
  23. package/dist/repo-store/subscribe-kind.d.ts +6 -3
  24. package/dist/repo-store/subscribe-kind.js +42 -77
  25. package/dist/repo-store/types.d.ts +94 -6
  26. package/dist/session-service.d.ts +277 -96
  27. package/dist/session-service.js +741 -547
  28. package/dist/sidecar-allocation/contracts.d.ts +78 -0
  29. package/dist/sidecar-allocation/contracts.js +21 -0
  30. package/dist/sidecar-allocation/index.d.ts +4 -0
  31. package/dist/sidecar-allocation/index.js +3 -0
  32. package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
  33. package/dist/sidecar-allocation/placement-policy.js +21 -0
  34. package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
  35. package/dist/sidecar-allocation/plugin-registry.js +37 -0
  36. package/dist/sidecar-allocation/reconciler.d.ts +42 -0
  37. package/dist/sidecar-allocation/reconciler.js +431 -0
  38. package/dist/skill-kind.js +4 -0
  39. package/dist/substrate.d.ts +3 -3
  40. package/dist/substrate.js +1 -1
  41. package/dist/workflow-allocation-service.d.ts +58 -0
  42. package/dist/workflow-allocation-service.js +239 -0
  43. package/dist/workflow-closure-resolution.d.ts +106 -0
  44. package/dist/workflow-closure-resolution.js +123 -0
  45. package/dist/workflow-definition-ensure.d.ts +24 -0
  46. package/dist/workflow-definition-ensure.js +75 -0
  47. package/dist/workflow-dispatch-service.d.ts +40 -0
  48. package/dist/workflow-dispatch-service.js +146 -0
  49. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  50. package/dist/workflow-dispatch-settlement.js +140 -0
  51. package/dist/workflow-kind.d.ts +17 -1
  52. package/dist/workflow-kind.js +127 -80
  53. package/dist/workflow-probe-gate.d.ts +214 -0
  54. package/dist/workflow-probe-gate.js +207 -0
  55. package/dist/workflow-run-kind.d.ts +128 -14
  56. package/dist/workflow-run-kind.js +353 -83
  57. package/dist/workflow-run-reader.d.ts +1 -1
  58. package/dist/workflow-run-reader.js +3 -7
  59. package/dist/workflow-run-restore.d.ts +15 -0
  60. package/dist/workflow-run-restore.js +26 -0
  61. package/dist/workflow-source-closure.d.ts +35 -0
  62. package/dist/workflow-source-closure.js +342 -0
  63. package/dist/ws/index.d.ts +3 -3
  64. package/dist/ws/index.js +1 -1
  65. package/dist/ws/sidecar-events.d.ts +100 -12
  66. package/dist/ws/sidecar-events.js +2 -0
  67. package/dist/ws/sidecar-handler.d.ts +128 -7
  68. package/dist/ws/sidecar-handler.js +1069 -135
  69. package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
  70. package/dist/ws/sidecar-token-authenticator.js +64 -7
  71. package/package.json +14 -13
  72. package/dist/available-skills-stanza.d.ts +0 -21
  73. package/dist/available-skills-stanza.js +0 -32
@@ -11,12 +11,11 @@
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";
18
+ import { parseAgentId } from "./hub-session-lookups.js";
20
19
  const log = getLogger(["hub", "orchestrator"]);
21
20
  export function createHubSessionOrchestrator(deps) {
22
21
  const { events, router, db, eventCollectors, agentRepoStore } = deps;
@@ -45,55 +44,21 @@ export function createHubSessionOrchestrator(deps) {
45
44
  recipients: recipients.join(", "),
46
45
  });
47
46
  }));
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));
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)
63
52
  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
- }
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.
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
- }
61
+ .where(eq(workflowRun.address, agentAddress));
97
62
  }));
98
63
  unsubscribers.push(events.on("deploy.ref.stale", async ({ agentAddress }) => {
99
64
  const agentId = parseAgentId(agentAddress);
package/dist/index.d.ts CHANGED
@@ -1,18 +1,27 @@
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, 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";
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 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";
7
10
  export { createHubSessionOrchestrator, type HubSessionOrchestrator, type HubSessionOrchestratorDeps, type HubSessionRouterFacade, } from "./hub-session-orchestrator.js";
8
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";
9
18
  export { skillKindHandler, skillAuthorize, skillFrontmatterSchema, getSkillIndex, type SkillIndexEntry, type SkillFrontmatter, type SkillPrincipal, type SkillHubPrincipal, type SkillSidecarPrincipal, } from "./skill-kind.js";
10
19
  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";
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";
15
24
  export { createWorkflowRunReader, type WorkflowRunReader, type WorkflowRunEvent, } from "./workflow-run-reader.js";
16
25
  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";
26
+ export type { AuthorizeFn, CommittedReads, CommittedTreeEntry, CreateRepoStoreConfig, InitRepoOpts, KindHandler, NewlyTerminalRun, Principal, RefEntry, RepoAction, RepoId, RepoStore, SubscribeKindEntry, SubscribeKindOpts, ValidatePushResult, WriteResult, } from "./repo-store/index.js";
18
27
  export { createRepoStore, subscribeKind, UserPrincipal } from "./repo-store/index.js";
package/dist/index.js CHANGED
@@ -1,16 +1,24 @@
1
1
  export { createAgentRepoStore, } from "./agent-repo.js";
2
- export { createSessionService, SessionLaunchError, bridgeOrchestratorDeployContent, } from "./session-service.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";
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, 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
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";
8
16
  export { skillKindHandler, skillAuthorize, skillFrontmatterSchema, getSkillIndex, } from "./skill-kind.js";
9
17
  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";
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";
12
21
  export { createAssetService, AssetServiceError, DEFAULT_ASSET_REF, } from "./asset-service.js";
13
- export { buildAvailableSkillsStanza, } from "./available-skills-stanza.js";
14
22
  export { createWorkflowRunReader, } from "./workflow-run-reader.js";
15
23
  export { WORKFLOW_RUN_EVENTS_FILE, splitCombinedEventLog, encodeCombinedEventLog, } from "./workflow-run-event-log.js";
16
24
  export { createRepoStore, subscribeKind, UserPrincipal } from "./repo-store/index.js";
@@ -1,4 +1,4 @@
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
3
  export { createRepoStore, type CreateRepoStoreConfig } from "./store.js";
4
4
  export { subscribeKind, type SubscribeKindOpts, type SubscribeKindEntry, } 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
@@ -1,17 +1,5 @@
1
- import fs from "node:fs";
2
- import git from "isomorphic-git";
3
1
  import { type } from "arktype";
4
- import { hasCode } from "@intx/types";
5
- /**
6
- * Path-layout constants for the workflow-run repo's event log. The
7
- * workflow-run kind handler commits one JSON blob per event under
8
- * `runs/<runId>/events/<seq>.json` (the layout decided in the
9
- * pre-build interface decisions for the workflow-run repo). The
10
- * `subscribeKind` helper owns this layout so the substrate `subscribe`
11
- * stays generic and free of workflow-event vocabulary.
12
- */
13
- const RUNS_PREFIX = "runs";
14
- const EVENTS_DIR = "events";
2
+ import { requireEventSeq, WORKFLOW_RUN_EVENTS_DIR, WORKFLOW_RUN_RUNS_PREFIX, } from "../workflow-run-kind.js";
15
3
  /**
16
4
  * Substrate envelope shape. Imported textually rather than from
17
5
  * `types.ts` so this helper has a local, validator-checked view of
@@ -24,11 +12,6 @@ const SubstrateEvent = type({
24
12
  "oldSha?": "string | null",
25
13
  newSha: "string",
26
14
  });
27
- /**
28
- * Per-event filename shape. Filenames are `<seq>.json` under
29
- * `runs/<runId>/events/`. `<seq>` is a non-negative decimal integer.
30
- */
31
- const EVENT_FILENAME_RE = /^(0|[1-9][0-9]*)\.json$/;
32
15
  const KindEnvelope = type({
33
16
  type: "string",
34
17
  });
@@ -41,9 +24,12 @@ const KindEnvelope = type({
41
24
  * kinds filter against the inner `type` discriminator, and yields one
42
25
  * `{ seq, event }` entry per matching blob.
43
26
  *
44
- * The path layout is owned here, not in the substrate: the substrate
45
- * `subscribe` is a generic ref-tail primitive and knows nothing about
46
- * workflow-runs.
27
+ * The path-layout vocabulary (the `runs/`/`events/` prefixes and the
28
+ * `<seq>.json` filename shape) lives in the workflow-run kind handler,
29
+ * which is the authority for what may land under this prefix; this
30
+ * helper imports it rather than re-encoding it. The substrate
31
+ * `subscribe` it wraps is a generic ref-tail primitive and knows
32
+ * nothing about workflow-runs.
47
33
  *
48
34
  * Diff scope: the helper enumerates event blobs present in the new
49
35
  * commit but absent from the old commit. A commit that does not add
@@ -53,7 +39,6 @@ const KindEnvelope = type({
53
39
  */
54
40
  export async function* subscribeKind(store, principal, repoId, ref, validator, opts) {
55
41
  const kindsAllowed = new Set(opts.kinds);
56
- const dir = store.getRepoDir(repoId);
57
42
  const subscribeOpts = {
58
43
  signal: opts.signal,
59
44
  from: opts.from,
@@ -67,9 +52,17 @@ export async function* subscribeKind(store, principal, repoId, ref, validator, o
67
52
  if (envelope instanceof type.errors) {
68
53
  throw new Error(`subscribe_kind_envelope_invalid: ${envelope.summary}`);
69
54
  }
70
- const candidates = await collectAddedEventBlobs(dir, envelope.oldSha ?? null, envelope.newSha);
55
+ const newReads = await store.openCommittedReadsAtCommit(principal, repoId, envelope.newSha);
56
+ // A commit a concurrent GC pruned between the ref-update event and
57
+ // this read yields nothing; the substrate surfaces that as `null`.
58
+ if (newReads === null)
59
+ continue;
60
+ const oldReads = envelope.oldSha === undefined || envelope.oldSha === null
61
+ ? null
62
+ : await store.openCommittedReadsAtCommit(principal, repoId, envelope.oldSha);
63
+ const candidates = await collectAddedEventBlobs(newReads, oldReads);
71
64
  for (const candidate of candidates) {
72
- const raw = await readBlobAtCommit(dir, envelope.newSha, candidate.blobPath);
65
+ const raw = await newReads.readBlobByOid(candidate.oid);
73
66
  let parsedJson;
74
67
  try {
75
68
  parsedJson = JSON.parse(new TextDecoder().decode(raw));
@@ -94,20 +87,22 @@ export async function* subscribeKind(store, principal, repoId, ref, validator, o
94
87
  }
95
88
  }
96
89
  /**
97
- * Walk the `runs/<runId>/events/` subtree at `newSha` and at `oldSha`
98
- * (when present) and return the set of event candidates present in
99
- * the new commit but not in the old. Filenames that fail the
100
- * `<seq>.json` shape are surfaced as errors rather than silently
101
- * skipped the workflow-run kind handler's validatePush is the
102
- * authority for what may land under this prefix, so an unexpected
103
- * shape here is a substrate-side invariant violation.
90
+ * Diff the `runs/<runId>/events/` subtree between the new and old commit
91
+ * reads and return the event candidates present in the new commit but
92
+ * not the old. `oldReads` is `null` for the ref's first commit (no prior
93
+ * tip) or when the prior commit is no longer in the object store, in
94
+ * which case every event in the new commit is treated as added.
95
+ * Filenames that fail the `<seq>.json` shape are surfaced as errors
96
+ * rather than silently skipped — the workflow-run kind handler's
97
+ * validatePush is the authority for what may land under this prefix, so
98
+ * an unexpected shape here is a substrate-side invariant violation.
104
99
  */
105
- async function collectAddedEventBlobs(dir, oldSha, newSha) {
106
- const newSet = await enumerateEventBlobs(dir, newSha);
107
- if (oldSha === null) {
100
+ async function collectAddedEventBlobs(newReads, oldReads) {
101
+ const newSet = await enumerateEventBlobs(newReads);
102
+ if (oldReads === null) {
108
103
  return [...newSet.values()].sort(byRunThenSeq);
109
104
  }
110
- const oldSet = await enumerateEventBlobs(dir, oldSha);
105
+ const oldSet = await enumerateEventBlobs(oldReads);
111
106
  const out = [];
112
107
  for (const [key, candidate] of newSet) {
113
108
  if (oldSet.has(key))
@@ -124,56 +119,26 @@ function byRunThenSeq(a, b) {
124
119
  return 1;
125
120
  return a.seq - b.seq;
126
121
  }
127
- async function enumerateEventBlobs(dir, commitOid) {
122
+ async function enumerateEventBlobs(reads) {
128
123
  const out = new Map();
129
- let commit;
130
- try {
131
- commit = await git.readCommit({ fs, dir, oid: commitOid });
132
- }
133
- catch (err) {
134
- if (hasCode(err) && err.code === "NotFoundError")
135
- return out;
136
- throw err;
137
- }
138
- const runsOid = await lookupSubtree(dir, commit.commit.tree, RUNS_PREFIX);
139
- if (runsOid === null)
140
- return out;
141
- const { tree: runs } = await git.readTree({ fs, dir, oid: runsOid });
124
+ const runs = await reads.listDir(WORKFLOW_RUN_RUNS_PREFIX);
142
125
  for (const runEntry of runs) {
143
126
  if (runEntry.type !== "tree")
144
127
  continue;
145
- const runId = runEntry.path;
146
- const eventsOid = await lookupSubtree(dir, runEntry.oid, EVENTS_DIR);
147
- if (eventsOid === null)
148
- continue;
149
- const { tree: events } = await git.readTree({ fs, dir, oid: eventsOid });
128
+ const runId = runEntry.name;
129
+ const events = await reads.listDir(`${WORKFLOW_RUN_RUNS_PREFIX}/${runId}/${WORKFLOW_RUN_EVENTS_DIR}`);
150
130
  for (const blob of events) {
151
131
  if (blob.type !== "blob")
152
132
  continue;
153
- const match = EVENT_FILENAME_RE.exec(blob.path);
154
- if (match === null) {
155
- throw new Error(`subscribe_kind_unexpected_event_filename: ${RUNS_PREFIX}/${runId}/${EVENTS_DIR}/${blob.path}`);
156
- }
157
- const seqStr = match[1];
158
- if (seqStr === undefined)
159
- throw new Error("unreachable");
160
- const seq = Number.parseInt(seqStr, 10);
161
- const blobPath = `${RUNS_PREFIX}/${runId}/${EVENTS_DIR}/${blob.path}`;
162
- out.set(`${runId}/${blob.path}`, { seq, runId, blobPath });
133
+ const blobPath = `${WORKFLOW_RUN_RUNS_PREFIX}/${runId}/${WORKFLOW_RUN_EVENTS_DIR}/${blob.name}`;
134
+ const seq = requireEventSeq(blob.name, blobPath);
135
+ out.set(`${runId}/${blob.name}`, {
136
+ seq,
137
+ runId,
138
+ blobPath,
139
+ oid: blob.oid,
140
+ });
163
141
  }
164
142
  }
165
143
  return out;
166
144
  }
167
- async function lookupSubtree(dir, parentTreeOid, name) {
168
- const { tree } = await git.readTree({ fs, dir, oid: parentTreeOid });
169
- const entry = tree.find((e) => e.path === name);
170
- if (entry === undefined)
171
- return null;
172
- if (entry.type !== "tree")
173
- return null;
174
- return entry.oid;
175
- }
176
- async function readBlobAtCommit(dir, commitOid, filepath) {
177
- const { blob } = await git.readBlob({ fs, dir, oid: commitOid, filepath });
178
- return blob;
179
- }