@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,571 @@
1
+ import type { RepoKind, RepoId, RepoAction } from "@intx/types/sidecar";
2
+ export type { RepoKind, RepoId, RepoAction };
3
+ /**
4
+ * arktype validator for the `user` principal variant. The substrate
5
+ * only requires the `kind` discriminant; kind handlers that accept
6
+ * user-token-authenticated requests rely on this shape to
7
+ * cross-check the pre-resolved authz verdict against the bearer
8
+ * token's claims. The validator is exported alongside the type so
9
+ * handlers can call it for a structural narrow without re-declaring
10
+ * the shape.
11
+ *
12
+ * Field semantics:
13
+ * - `authz`: the pre-resolved grant verdict from the route layer.
14
+ * The kind handler does NOT re-query the grant store; it only
15
+ * sanity-checks that the verdict targets the right resource and
16
+ * grant verb, then defers to `effect`.
17
+ * - `tokenClaims`: the bearer-token's scope. The kind handler
18
+ * verifies the requested `(ref, action)` falls inside this scope
19
+ * synchronously (`actions.includes(action)`,
20
+ * `glob.match(refPattern, ref)`, `Date.now() < expiresAt`).
21
+ */
22
+ export declare const UserPrincipal: import("arktype/internal/variants/object.ts").ObjectType<{
23
+ kind: "user";
24
+ principalId: string;
25
+ tenantId: string;
26
+ authz: {
27
+ effect: "allow" | "deny";
28
+ resource: string;
29
+ grantVerb: string;
30
+ };
31
+ tokenClaims: {
32
+ refPattern: string;
33
+ actions: ("init" | "writeTree" | "receivePack" | "createPack" | "resolveRef")[];
34
+ expiresAt: number;
35
+ };
36
+ }, {}>;
37
+ export type UserPrincipal = typeof UserPrincipal.infer;
38
+ /**
39
+ * Regex defining the shape of a valid `RepoId.id`. The substrate
40
+ * validates against this at every public operation and throws an Error
41
+ * prefixed with `"repo_id_invalid: "` on mismatch.
42
+ */
43
+ export declare const SAFE_REPO_ID: RegExp;
44
+ /**
45
+ * Principal is a discriminated-union extension point. The substrate
46
+ * requires only the `kind` discriminant; concrete principal shapes live
47
+ * in kind-handler packages and are narrowed via `principal.kind === "..."`
48
+ * checks plus arktype validation in the handler. No index signature is
49
+ * declared here so that handlers do not need `as Type` casts to access
50
+ * their own fields.
51
+ */
52
+ export type Principal = {
53
+ readonly kind: string;
54
+ };
55
+ /**
56
+ * Authorization callback supplied to the repo-store. Called once per
57
+ * substrate operation that requires gating (`writeTree`, `receivePack`,
58
+ * `createPack`, `resolveRef`, plus the bulk-read variants below). The
59
+ * substrate translates an `allowed: false` verdict into a thrown Error
60
+ * prefixed with `"authorize_denied: "` carrying the supplied reason.
61
+ *
62
+ * The substrate passes the literal string `"*"` as `ref` when it calls
63
+ * the authorize hook on behalf of the bulk-read methods `listRefs` and
64
+ * `resolveHead`. Both methods enumerate refs across the whole repo and
65
+ * have no single ref to feed into a per-ref claim check; the sentinel
66
+ * lets kind handlers recognise the call and skip the per-ref
67
+ * `refPattern` match while still gating on `action` (always
68
+ * `"resolveRef"` for the bulk case) and expiry. Per-ref refPattern
69
+ * filtering for the response payload is the responsibility of the
70
+ * caller (the advertise-refs layer), not the authorize hook.
71
+ */
72
+ export type AuthorizeFn = (principal: Principal, repoId: RepoId, ref: string, action: RepoAction) => {
73
+ allowed: true;
74
+ } | {
75
+ allowed: false;
76
+ reason: string;
77
+ };
78
+ /**
79
+ * A run whose terminal event (e.g. `RunCompleted`) is *newly added* by the
80
+ * commit under validation -- present in the prospective tree and absent from
81
+ * the prior tree. The kind handler authoritatively detects this during its
82
+ * validation walk and surfaces it so callers do not re-derive terminal-ness
83
+ * by sniffing committed path shapes. `status` is the terminal run state the
84
+ * terminal event maps to, matching the `workflow_run.status` vocabulary, so a
85
+ * caller flips the run's row without re-parsing the event type.
86
+ * `terminalEventJson` carries the raw bytes of the terminal event blob so a
87
+ * caller that needs the full event (the supervisor's terminal-write broadcast)
88
+ * can reconstruct it without a second read. A commit that carries an
89
+ * already-terminal run forward unchanged (e.g. a later compaction commit) is
90
+ * NOT newly terminal and does not appear here.
91
+ */
92
+ export type NewlyTerminalRun = {
93
+ runId: string;
94
+ status: "completed" | "failed" | "cancelled";
95
+ terminalEventJson: string;
96
+ };
97
+ export type ValidatePushResult = {
98
+ ok: true;
99
+ newlyTerminalRuns?: NewlyTerminalRun[];
100
+ } | {
101
+ ok: false;
102
+ reason: string;
103
+ };
104
+ /**
105
+ * Result of a `writeTree` / `writeTreePreservingPrefix` commit. `commitSha` is
106
+ * the new commit. `newlyTerminalRuns` surfaces the kind handler's terminal
107
+ * detection (empty for handlers and commits that produce none) so callers can
108
+ * react to a run reaching a terminal event without re-deriving it from the
109
+ * committed path shape.
110
+ */
111
+ export type WriteResult = {
112
+ commitSha: string;
113
+ newlyTerminalRuns: readonly NewlyTerminalRun[];
114
+ };
115
+ /**
116
+ * Per-call options for `initRepo`. Currently a single override —
117
+ * `gitignore` — that overrides the body written to `.gitignore` in
118
+ * the genesis tree. When omitted, the substrate's default body is
119
+ * used. The asset REST handler supplies a richer body that includes
120
+ * OS/editor cruft, common build output, and `keys/`.
121
+ */
122
+ export type InitRepoOpts = {
123
+ gitignore?: string;
124
+ };
125
+ /**
126
+ * A single ref entry returned by `RepoStore.listRefs`. `name` is the
127
+ * fully-qualified ref name (`refs/heads/main`, `refs/tags/v1`, ...);
128
+ * `sha` is the SHA-1 the ref currently resolves to.
129
+ */
130
+ export type RefEntry = {
131
+ readonly name: string;
132
+ readonly sha: string;
133
+ };
134
+ export type TreeContent = {
135
+ /**
136
+ * Map of repo-relative path to file contents. Each entry is written
137
+ * to the working tree and staged before commit.
138
+ */
139
+ files: Record<string, string | Uint8Array>;
140
+ /**
141
+ * Optional directory-subtree prefix to clear before staging. When
142
+ * set, every tracked path beginning with this prefix is removed
143
+ * from the git index and the corresponding directory on disk is
144
+ * deleted before `files` is written. Must end with `/` and must
145
+ * not contain `..` or absolute path components. When unset,
146
+ * writeTree is purely additive.
147
+ */
148
+ clearPrefix?: string;
149
+ /** Commit message for the resulting commit. */
150
+ message: string;
151
+ };
152
+ /**
153
+ * Per-call options for `RepoStore.writeTreePreservingPrefix`. The
154
+ * substrate enumerates existing blobs under `preservePrefix` from the
155
+ * current ref tip while holding the per-repo lock, calls the caller's
156
+ * `merge` callback with those entries, and writes the returned set as
157
+ * the new value of the prefix subtree. Two concurrent callers
158
+ * targeting the same prefix serialize at the lock so neither one
159
+ * observes a stale pre-image of the other's commit.
160
+ */
161
+ export type WriteTreePreservingPrefixArgs = {
162
+ /**
163
+ * Directory-subtree prefix whose existing blobs are surfaced to
164
+ * `merge` and then replaced wholesale by its return value. Must end
165
+ * with `/` and must not contain `..` or absolute path components.
166
+ */
167
+ preservePrefix: string;
168
+ /**
169
+ * Called under the per-repo lock with the current set of blobs
170
+ * directly under `preservePrefix` (keyed by repo-root-relative
171
+ * path, including the prefix). Returns the full set of files the
172
+ * substrate should write at the prefix; the prefix subtree is
173
+ * cleared and replaced with this set in a single commit. Paths
174
+ * outside the prefix are passed through unchanged. The callback may
175
+ * throw to abort the write; the substrate releases the lock and
176
+ * propagates the error.
177
+ */
178
+ merge: (existing: ReadonlyMap<string, Uint8Array>) => Promise<Record<string, string | Uint8Array>>;
179
+ /** Commit message for the resulting commit. */
180
+ message: string;
181
+ };
182
+ /**
183
+ * Per-call options for `RepoStore.writeTreeDelta`. `computeDelta` runs
184
+ * under the per-repo lock against the pinned parent tip (`parentCommitSha`,
185
+ * null when the ref does not yet exist) and returns the exact files to
186
+ * write and paths to delete; the substrate carries every other entry
187
+ * forward by its object id. A delete ending in `/` clears that subtree;
188
+ * any other delete clears the single file at that path. A delete whose
189
+ * base entry is the wrong type -- a no-slash delete naming a directory,
190
+ * or a trailing-slash delete descending into a file -- is rejected.
191
+ *
192
+ * `changedPathPrefixes` is the validation scoping hint for the touched
193
+ * region (e.g. `addresses/<seg>/`), supplied by the caller because a
194
+ * delta has no single clear-prefix to derive it from; `undefined` means
195
+ * validate the whole tree.
196
+ *
197
+ * The `prior` argument exposes cache-backed reads of that pinned parent
198
+ * tree -- `listDirOids` for a directory's `{name, oid}` children and
199
+ * `readBlobByOid` for a blob by its object id -- so the callback reads
200
+ * through the store's per-repo object cache under the same lock rather
201
+ * than re-opening the repo.
202
+ */
203
+ export type PriorDeltaReads = {
204
+ readBlobByOid: (oid: string) => Promise<Uint8Array>;
205
+ listDirOids: (path: string) => Promise<{
206
+ name: string;
207
+ oid: string;
208
+ }[]>;
209
+ };
210
+ export type WriteTreeDeltaArgs = {
211
+ computeDelta: (parentCommitSha: string | null, prior: PriorDeltaReads) => Promise<{
212
+ puts: Record<string, string | Uint8Array>;
213
+ deletes: readonly string[];
214
+ }>;
215
+ changedPathPrefixes: ReadonlySet<string> | undefined;
216
+ message: string;
217
+ };
218
+ export interface KindHandler {
219
+ kind: RepoKind;
220
+ /**
221
+ * On-disk directory under `dataDir` for repos of this kind. Allows
222
+ * each kind to declare its own layout (e.g. "agents") so the
223
+ * substrate does not hard-code a `<kind>/<id>` path.
224
+ */
225
+ directoryPrefix: string;
226
+ /**
227
+ * Inspect the prospective commit's tree before the ref is
228
+ * advanced. Return `{ ok: false, reason }` to reject the write.
229
+ * The substrate translates rejection into a thrown Error whose
230
+ * message begins with `"path_violation: "`.
231
+ *
232
+ * Runs on every `receivePack` and every `writeTree` independently
233
+ * of the authorize verdict: authorize gates access, validatePush
234
+ * enforces content rules.
235
+ *
236
+ * `topLevelTreePaths` lists the names directly under the prospective
237
+ * tree root. `readBlob` reads any blob in the prospective tree by
238
+ * repo-root-relative POSIX path (e.g. `greet/SKILL.md`). `listDir`
239
+ * enumerates the names directly under a tree-root-relative POSIX
240
+ * directory path (no trailing slash, no leading slash); pass the
241
+ * empty string to list the root. Handlers that only need path-level
242
+ * checks can ignore `readBlob` and `listDir`.
243
+ *
244
+ * `priorReadBlob` and `priorListDir` mirror `readBlob` / `listDir`
245
+ * against the parent commit's tree — the ref's tip at the moment
246
+ * validatePush runs. Handlers use these to compare prospective
247
+ * content against the immediately-prior bytes (e.g. enforcing
248
+ * append-only invariants by rejecting any path whose prior bytes
249
+ * differ from the prospective bytes). `priorReadBlob` returns
250
+ * `null` when the path did not exist at the prior tree (or the
251
+ * ref has no prior commit — first push). `priorListDir` returns
252
+ * an empty array in the same cases.
253
+ *
254
+ * `principal` is the principal performing the push, the same value
255
+ * fed to the `authorize` hook. Handlers use it for principal-vs-
256
+ * payload cross-checks that a structural shape validator cannot
257
+ * express (e.g. "only a `hub` principal may write a `CancelRequested`
258
+ * whose origin is `hub-admin`").
259
+ *
260
+ * `changedPathPrefixes` is the set of repo-root-relative POSIX path
261
+ * prefixes (each ending in `/`) under which this commit could have
262
+ * mutated tree entries -- the cleared prefix for a `writeTree`
263
+ * carrying a `clearPrefix`, or the subtrees whose object differs from
264
+ * the parent commit for a received pack. Every path the commit can
265
+ * have changed relative to its parent is under one of these prefixes;
266
+ * any path outside them is carried forward byte-identical by the
267
+ * substrate. It is `undefined` when the substrate cannot bound the
268
+ * change set (no parent to diff against and no `clearPrefix`), in
269
+ * which case the handler must validate the whole prospective tree. A
270
+ * handler with per-subtree invariants that cannot be affected by a
271
+ * commit outside that subtree (workflow-run's per-run append-only
272
+ * log) uses this to skip re-validating subtrees the commit provably
273
+ * did not touch; a handler with no such structure ignores it and
274
+ * validates unconditionally.
275
+ *
276
+ * `priorListDirOids` mirrors `priorListDir` but returns each child
277
+ * entry's git object id alongside its name, read straight from the
278
+ * prior commit's tree (git trees are content-addressed, so the listing
279
+ * already carries the OID). A handler that validates a large retained
280
+ * subtree by its per-commit delta uses it to prove a retained entry is
281
+ * byte-unchanged by OID equality instead of re-reading the blob. It is
282
+ * `undefined` when no prior commit exists.
283
+ *
284
+ * `listDirOids` is the prospective-side mirror of `priorListDirOids`:
285
+ * each child entry's OID read straight from the prospective tree's
286
+ * listing, so a handler comparing a retained subtree by OID gets the
287
+ * prospective OID without re-reading and hashing every entry's bytes.
288
+ * It is `undefined` on paths that do not surface it (a hand-built
289
+ * validatePush in a test), in which case the handler falls back to
290
+ * hashing the prospective bytes.
291
+ */
292
+ validatePush: (args: {
293
+ repoId: RepoId;
294
+ ref: string;
295
+ principal: Principal;
296
+ topLevelTreePaths: string[];
297
+ readBlob: (path: string) => Promise<Uint8Array>;
298
+ listDir: (path: string) => Promise<string[]>;
299
+ listDirOids?: (path: string) => Promise<{
300
+ name: string;
301
+ oid: string;
302
+ }[]>;
303
+ priorReadBlob: (path: string) => Promise<Uint8Array | null>;
304
+ priorListDir: (path: string) => Promise<string[]>;
305
+ priorListDirOids?: (path: string) => Promise<{
306
+ name: string;
307
+ oid: string;
308
+ }[]>;
309
+ changedPathPrefixes?: ReadonlySet<string> | undefined;
310
+ }) => Promise<ValidatePushResult> | ValidatePushResult;
311
+ /**
312
+ * Fired after a successful ref update from any operation. `oldSha`
313
+ * is `null` when the ref did not exist before the update.
314
+ */
315
+ onRefUpdated: (args: {
316
+ repoId: RepoId;
317
+ ref: string;
318
+ oldSha: string | null;
319
+ newSha: string;
320
+ }) => Promise<void> | void;
321
+ }
322
+ /**
323
+ * A single child entry returned by `CommittedReads.listDir`. `name` is
324
+ * the entry's own path segment (no parent prefix); `oid` is its git
325
+ * object id; `type` is the git tree-entry kind. `oid` lets a consumer
326
+ * read a blob's bytes via `readBlobByOid` without re-resolving the path,
327
+ * and lets it prove a subtree byte-unchanged by OID equality.
328
+ */
329
+ export type CommittedTreeEntry = {
330
+ readonly name: string;
331
+ readonly oid: string;
332
+ readonly type: "blob" | "tree" | "commit";
333
+ };
334
+ /**
335
+ * Cache-backed reads pinned to the commit a ref resolved to at the
336
+ * moment `openCommittedReads` was called. Every read resolves against
337
+ * the git object store, never the materialized working tree, so a
338
+ * consumer observes committed state even when the on-disk checkout lags
339
+ * the committed tree. The pin is fixed at open time: a concurrent commit
340
+ * that advances the ref afterwards does not shift the reads, so an
341
+ * enumerate-then-read sequence sees a single coherent snapshot.
342
+ *
343
+ * `listDir` returns the direct children of a repo-root-relative POSIX
344
+ * directory path (no leading or trailing slash; the empty string lists
345
+ * the root). A path that is absent, or resolves to a non-tree, lists as
346
+ * the empty array — mirroring the prior-tree closures the substrate
347
+ * hands a kind handler. `readBlobByOid` reads a blob's bytes by its
348
+ * object id; a read fault surfaces as a thrown error rather than an
349
+ * empty result so a consumer cannot silently degrade a missing object
350
+ * into an absent event.
351
+ */
352
+ export type CommittedReads = {
353
+ listDir(relPath: string): Promise<CommittedTreeEntry[]>;
354
+ readBlobByOid(oid: string): Promise<Uint8Array>;
355
+ /**
356
+ * The git tree object id of the subtree at `relPath` (the empty string
357
+ * or "." is the commit's root tree), or `null` when `relPath` is absent
358
+ * or resolves to a non-tree. This is the content identity a source
359
+ * closure freezes for a package materialized from the tree: the sidecar
360
+ * re-checks the checked-out subtree against it.
361
+ */
362
+ treeOid(relPath: string): Promise<string | null>;
363
+ };
364
+ export interface RepoStore {
365
+ /**
366
+ * Bookkeeping primitive. Idempotent. Creates the repo directory
367
+ * and initializes git when not already present. Not gated by
368
+ * `authorize`: the only state it can produce is an empty repo, so
369
+ * the higher-level question of who may mint a new `<kind>/<id>`
370
+ * lives at the caller. The substrate also calls `initRepo`
371
+ * internally from `writeTree` and `receivePack`, so first-touch
372
+ * operations succeed without an explicit init call.
373
+ */
374
+ initRepo(repoId: RepoId, opts?: InitRepoOpts): Promise<void>;
375
+ writeTree(principal: Principal, repoId: RepoId, ref: string, content: TreeContent): Promise<WriteResult>;
376
+ /**
377
+ * Read-then-write variant for use cases that mutate a single
378
+ * directory subtree against its current contents (overwrite one
379
+ * entry, delete one entry, augment by one entry). The substrate
380
+ * enumerates blobs under `args.preservePrefix` while holding the
381
+ * per-repo lock, invokes `args.merge` with those entries, and
382
+ * commits the returned set as the new value of the prefix.
383
+ *
384
+ * Two concurrent callers targeting the same prefix serialize at the
385
+ * lock, so the merge callback's pre-image is always the previous
386
+ * commit's tip — there is no read-outside-the-lock window where one
387
+ * caller could base its write on a stale view of the prefix.
388
+ *
389
+ * The substrate handles `clearPrefix` and the commit internally;
390
+ * paths outside the prefix are untouched.
391
+ */
392
+ writeTreePreservingPrefix(principal: Principal, repoId: RepoId, ref: string, args: WriteTreePreservingPrefixArgs): Promise<WriteResult>;
393
+ /**
394
+ * Delta variant for mutating a few named entries in a large subtree
395
+ * without re-materializing the untouched siblings. `computeDelta` runs
396
+ * under the per-repo lock against the pinned parent tip and returns the
397
+ * files to put and the paths to delete; everything else is carried
398
+ * forward by object id. Use this over `writeTreePreservingPrefix` when
399
+ * the untouched remainder of the prefix is large (e.g. a claim-check
400
+ * move that adds one entry and deletes another while the consumed dedup
401
+ * index carries forward unchanged).
402
+ */
403
+ writeTreeDelta(principal: Principal, repoId: RepoId, ref: string, args: WriteTreeDeltaArgs): Promise<WriteResult>;
404
+ /**
405
+ * Receive a packfile and advance `ref` to `commitSha`.
406
+ *
407
+ * `expectedOldSha` is a compare-and-set guard the substrate runs
408
+ * under the per-repo lock. Pass a SHA string to require the ref
409
+ * currently points there; pass `null` to require the ref does not
410
+ * yet exist. On mismatch the call throws with a `non_fast_forward:`
411
+ * prefix and leaves the ref untouched.
412
+ *
413
+ * Callers that do not have the old SHA in hand should resolve it
414
+ * via `resolveRef` first; the substrate exposes no force-write
415
+ * mode because silently overwriting a losing concurrent update is
416
+ * never the right behavior.
417
+ *
418
+ * Returns the runs the received pack drove to a terminal event,
419
+ * aggregated across every commit it carried. The kind handler
420
+ * detects terminal-ness authoritatively during validation; the
421
+ * substrate forwards it so a caller can react (flip the run's DB
422
+ * row, deactivate its principal) without re-deriving terminal-ness
423
+ * from the committed path shape. Empty for kinds and packs that
424
+ * produce none.
425
+ */
426
+ receivePack(principal: Principal, repoId: RepoId, ref: string, pack: Uint8Array, commitSha: string, expectedOldSha: string | null): Promise<NewlyTerminalRun[]>;
427
+ createPack(principal: Principal, repoId: RepoId, ref: string): Promise<{
428
+ pack: Uint8Array;
429
+ commitSha: string;
430
+ ref: string;
431
+ }>;
432
+ /**
433
+ * Commit the "last shipped tip" for an incremental (`workflow-run`)
434
+ * pack ref, advancing the cursor `createPack` walks back to. The
435
+ * caller invokes this only once the receiver has acked the transfer
436
+ * that shipped `commitSha`, so an incremental pack the receiver never
437
+ * acknowledged — a transfer a reconnect cancelled before its ack —
438
+ * leaves the cursor where it was and the next `createPack` re-ships
439
+ * the un-acked commits. Building a pack must NOT advance the cursor
440
+ * itself: a built-but-un-acked transfer would then strand the
441
+ * receiver with a chain whose base commit the pack omits. A no-op for
442
+ * kinds whose packs are not incremental.
443
+ */
444
+ commitPackedTip(repoId: RepoId, ref: string, commitSha: string): void;
445
+ resolveRef(principal: Principal, repoId: RepoId, ref: string): Promise<string | null>;
446
+ /**
447
+ * Enumerate the repo's refs (branches and tags), lexicographically
448
+ * sorted by name. The principal is gated under the same
449
+ * `resolveRef` action that `resolveRef` itself enforces — the
450
+ * substrate does not duplicate the check on a per-ref basis. When
451
+ * the on-disk repo does not yet exist (the bookkeeping primitive
452
+ * `initRepo` has never been called), the result is the empty list.
453
+ */
454
+ listRefs(principal: Principal, repoId: RepoId): Promise<RefEntry[]>;
455
+ /**
456
+ * Resolve HEAD into the ref it symbolically points at plus the SHA
457
+ * that ref currently resolves to. The principal is gated under the
458
+ * same `resolveRef` action that `resolveRef` and `listRefs`
459
+ * enforce. Returns `null` when:
460
+ * - The on-disk repo does not yet exist (mirrors `listRefs`'s
461
+ * empty-list contract for uninitialised repos).
462
+ * - HEAD is detached (no symbolic target).
463
+ * - HEAD's symbolic target does not resolve (unborn ref).
464
+ * The smart-HTTP advertise layer uses the result to emit
465
+ * `symref=HEAD:<target>` so stock `git clone` lands on a real
466
+ * branch instead of leaving the working tree unborn.
467
+ */
468
+ resolveHead(principal: Principal, repoId: RepoId): Promise<{
469
+ symbolicTarget: string;
470
+ sha: string;
471
+ } | null>;
472
+ /**
473
+ * Synchronously return the on-disk directory backing the repo.
474
+ * The path is the result of composing the substrate's `dataDir`,
475
+ * the kind handler's `directoryPrefix`, and the validated
476
+ * `repoId.id`. This carries no authorize gate: it is a pure path
477
+ * computation. Consumers of the path (the smart-HTTP wire
478
+ * handlers) remain authorize-gated through the substrate methods
479
+ * they reach into for ref-listing and pack negotiation.
480
+ */
481
+ getRepoDir(repoId: RepoId): string;
482
+ /**
483
+ * Open cache-backed reads of the committed tree at `ref`'s tip. The
484
+ * ref is resolved once, at call time, and every read the returned
485
+ * handle serves is pinned to that commit and resolves through the git
486
+ * object store — not the materialized working tree `getRepoDir` points
487
+ * at. A consumer that must observe committed state (e.g. start-time
488
+ * recovery reconstructing a ledger from the persisted log) reads
489
+ * through this rather than the working tree, which a non-atomic
490
+ * post-commit materialization can leave lagging on a contended
491
+ * filesystem.
492
+ *
493
+ * Gated under the same `resolveRef` action as `resolveRef` / `listRefs`
494
+ * / `subscribe`. Returns `null` when the repo does not yet exist
495
+ * (mirrors `listRefs`'s empty-list contract for uninitialised repos)
496
+ * or when `ref` does not resolve to a commit.
497
+ */
498
+ openCommittedReads(principal: Principal, repoId: RepoId, ref: string): Promise<CommittedReads | null>;
499
+ /**
500
+ * Open cache-backed reads of the committed tree at an explicit commit,
501
+ * the by-SHA counterpart of `openCommittedReads`. A consumer that
502
+ * already holds a commit id — e.g. the `newSha`/`oldSha` of a
503
+ * ref-update event it is diffing — reads that exact commit through
504
+ * this, even after the ref has advanced past it. Every read the handle
505
+ * serves resolves through the git object store, pinned to `commitSha`.
506
+ *
507
+ * Gated under the same `resolveRef` action as `openCommittedReads`.
508
+ * `commitSha` is validated at the boundary: a malformed SHA throws
509
+ * `commit_sha_invalid`. Returns `null` when the repo does not yet exist
510
+ * or when `commitSha` names no commit in the object store (a commit a
511
+ * concurrent GC pruned between the caller learning of it and reading
512
+ * it), so a caller diffing a possibly-vanished commit gets an empty
513
+ * view rather than a mid-walk throw.
514
+ */
515
+ openCommittedReadsAtCommit(principal: Principal, repoId: RepoId, commitSha: string): Promise<CommittedReads | null>;
516
+ /**
517
+ * Tail a ref's commit log. Returns an async iterator that emits
518
+ * `{ seq, event }` entries: one per commit on the ref. `seq` is
519
+ * zero-indexed at the ref's root commit and counts ancestors
520
+ * walking forward to HEAD, so the same commit always carries the
521
+ * same `seq` across restarts. The emitted `event` is the
522
+ * substrate-level commit descriptor; consumers that need richer
523
+ * shapes layer their own decoding on top.
524
+ *
525
+ * Cancellation: when `opts.signal` aborts, the iterator ends
526
+ * cleanly (no throw from the consumer's `for await`). The
527
+ * substrate releases the watcher slot on the same abort tick.
528
+ *
529
+ * Replay vs live:
530
+ * - `from: { seq: number }` enumerates every prior commit on the
531
+ * ref whose computed `seq` is >= the supplied number, then
532
+ * transitions to live mode and continues with new commits.
533
+ * - `from: "head"` records HEAD-of-ref at subscribe time and
534
+ * emits only commits that land strictly after.
535
+ *
536
+ * Backpressure: events are buffered in userspace bounded by
537
+ * `bufferLimit` (default 1024). On overrun the iterator throws a
538
+ * loud error; silent drop would corrupt audit. Consumers that
539
+ * cannot keep up are expected to abort.
540
+ *
541
+ * The substrate's vocabulary is the ref-update envelope. Consumers
542
+ * that need to filter on a richer event kind (e.g. a workflow-event
543
+ * `type` discriminator committed at the new ref) layer a decoder on
544
+ * top — see `subscribeKind` for the typed entrypoint that loads the
545
+ * committed payload, narrows it with an arktype validator, and
546
+ * applies a per-call kind filter.
547
+ */
548
+ subscribe(principal: Principal, repoId: RepoId, ref: string, opts: {
549
+ signal: AbortSignal;
550
+ from: "head" | {
551
+ seq: number;
552
+ };
553
+ bufferLimit?: number;
554
+ }): AsyncIterableIterator<{
555
+ seq: number;
556
+ event: unknown;
557
+ }>;
558
+ }
559
+ /**
560
+ * Substrate-level event shape emitted by `RepoStore.subscribe`.
561
+ * Each successful commit on a watched ref produces one event with
562
+ * this shape. The substrate is schema-agnostic; higher layers that
563
+ * want to surface richer event vocabularies build their own decoders
564
+ * on top (see `subscribeKind` for the workflow-event entrypoint).
565
+ */
566
+ export type RepoStoreSubscribeEvent = {
567
+ readonly type: "ref.updated";
568
+ readonly ref: string;
569
+ readonly oldSha: string | null;
570
+ readonly newSha: string;
571
+ };
@@ -0,0 +1,42 @@
1
+ import { type } from "arktype";
2
+ import { RepoAction as RepoActionSchema } from "@intx/types/sidecar";
3
+ /**
4
+ * arktype validator for the `user` principal variant. The substrate
5
+ * only requires the `kind` discriminant; kind handlers that accept
6
+ * user-token-authenticated requests rely on this shape to
7
+ * cross-check the pre-resolved authz verdict against the bearer
8
+ * token's claims. The validator is exported alongside the type so
9
+ * handlers can call it for a structural narrow without re-declaring
10
+ * the shape.
11
+ *
12
+ * Field semantics:
13
+ * - `authz`: the pre-resolved grant verdict from the route layer.
14
+ * The kind handler does NOT re-query the grant store; it only
15
+ * sanity-checks that the verdict targets the right resource and
16
+ * grant verb, then defers to `effect`.
17
+ * - `tokenClaims`: the bearer-token's scope. The kind handler
18
+ * verifies the requested `(ref, action)` falls inside this scope
19
+ * synchronously (`actions.includes(action)`,
20
+ * `glob.match(refPattern, ref)`, `Date.now() < expiresAt`).
21
+ */
22
+ export const UserPrincipal = type({
23
+ kind: "'user'",
24
+ principalId: "string",
25
+ tenantId: "string",
26
+ authz: {
27
+ effect: "'allow' | 'deny'",
28
+ resource: "string",
29
+ grantVerb: "string",
30
+ },
31
+ tokenClaims: {
32
+ refPattern: "string",
33
+ actions: RepoActionSchema.array(),
34
+ expiresAt: "number",
35
+ },
36
+ });
37
+ /**
38
+ * Regex defining the shape of a valid `RepoId.id`. The substrate
39
+ * validates against this at every public operation and throws an Error
40
+ * prefixed with `"repo_id_invalid: "` on mismatch.
41
+ */
42
+ export const SAFE_REPO_ID = /^[a-zA-Z0-9_-]+$/;