@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.
- package/LICENSE +176 -0
- package/README.md +82 -1
- package/dist/agent-repo.d.ts +93 -0
- package/dist/agent-repo.js +109 -0
- package/dist/agent-state-kind.d.ts +12 -0
- package/dist/agent-state-kind.js +189 -0
- package/dist/asset-service.d.ts +104 -0
- package/dist/asset-service.js +267 -0
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +33 -0
- package/dist/credential-push.js +109 -0
- package/dist/event-collector-registry.d.ts +20 -0
- package/dist/event-collector-registry.js +115 -0
- package/dist/event-collector.d.ts +39 -0
- package/dist/event-collector.js +365 -0
- package/dist/hub-session-lookups.d.ts +135 -0
- package/dist/hub-session-lookups.js +663 -0
- package/dist/hub-session-orchestrator.d.ts +25 -0
- package/dist/hub-session-orchestrator.js +87 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/package-registry-kind.d.ts +70 -0
- package/dist/package-registry-kind.js +260 -0
- package/dist/repo-store/index.d.ts +4 -0
- package/dist/repo-store/index.js +3 -0
- package/dist/repo-store/store.d.ts +41 -0
- package/dist/repo-store/store.js +1829 -0
- package/dist/repo-store/subscribe-kind.d.ts +56 -0
- package/dist/repo-store/subscribe-kind.js +144 -0
- package/dist/repo-store/types.d.ts +571 -0
- package/dist/repo-store/types.js +42 -0
- package/dist/session-service.d.ts +416 -0
- package/dist/session-service.js +1191 -0
- package/dist/sidecar-allocation/contracts.d.ts +78 -0
- package/dist/sidecar-allocation/contracts.js +21 -0
- package/dist/sidecar-allocation/index.d.ts +4 -0
- package/dist/sidecar-allocation/index.js +3 -0
- package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
- package/dist/sidecar-allocation/placement-policy.js +21 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
- package/dist/sidecar-allocation/plugin-registry.js +37 -0
- package/dist/sidecar-allocation/reconciler.d.ts +42 -0
- package/dist/sidecar-allocation/reconciler.js +431 -0
- package/dist/skill-kind.d.ts +41 -0
- package/dist/skill-kind.js +292 -0
- package/dist/substrate.d.ts +8 -0
- package/dist/substrate.js +21 -0
- package/dist/workflow-allocation-service.d.ts +58 -0
- package/dist/workflow-allocation-service.js +239 -0
- package/dist/workflow-closure-resolution.d.ts +106 -0
- package/dist/workflow-closure-resolution.js +123 -0
- package/dist/workflow-definition-ensure.d.ts +24 -0
- package/dist/workflow-definition-ensure.js +75 -0
- package/dist/workflow-dispatch-service.d.ts +40 -0
- package/dist/workflow-dispatch-service.js +146 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +37 -0
- package/dist/workflow-kind.js +310 -0
- package/dist/workflow-probe-gate.d.ts +214 -0
- package/dist/workflow-probe-gate.js +207 -0
- package/dist/workflow-run-event-log.d.ts +21 -0
- package/dist/workflow-run-event-log.js +51 -0
- package/dist/workflow-run-kind.d.ts +440 -0
- package/dist/workflow-run-kind.js +2916 -0
- package/dist/workflow-run-reader.d.ts +47 -0
- package/dist/workflow-run-reader.js +153 -0
- package/dist/workflow-run-restore.d.ts +15 -0
- package/dist/workflow-run-restore.js +26 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/ws/index.d.ts +3 -0
- package/dist/ws/index.js +3 -0
- package/dist/ws/sidecar-events.d.ts +222 -0
- package/dist/ws/sidecar-events.js +72 -0
- package/dist/ws/sidecar-handler.d.ts +305 -0
- package/dist/ws/sidecar-handler.js +2537 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +17 -0
- package/dist/ws/sidecar-token-authenticator.js +81 -0
- package/package.json +35 -12
- package/src/agent-repo.test.ts +0 -310
- package/src/agent-repo.ts +0 -165
- package/src/agent-state-kind.test.ts +0 -247
- package/src/agent-state-kind.ts +0 -204
- package/src/asset-service.test.ts +0 -540
- package/src/asset-service.ts +0 -378
- package/src/available-skills-stanza.test.ts +0 -87
- package/src/available-skills-stanza.ts +0 -47
- package/src/credential-push.ts +0 -65
- package/src/event-collector-registry.test.ts +0 -73
- package/src/event-collector-registry.ts +0 -171
- package/src/event-collector.test.ts +0 -1387
- package/src/event-collector.ts +0 -424
- package/src/hub-session-lookups.ts +0 -206
- package/src/hub-session-orchestrator.test.ts +0 -510
- package/src/hub-session-orchestrator.ts +0 -213
- package/src/index.ts +0 -78
- package/src/repo-store/index.ts +0 -15
- package/src/repo-store/store.test.ts +0 -1169
- package/src/repo-store/store.ts +0 -428
- package/src/repo-store/types.ts +0 -253
- package/src/session-service.test.ts +0 -895
- package/src/session-service.ts +0 -464
- package/src/skill-kind.test.ts +0 -599
- package/src/skill-kind.ts +0 -350
- package/src/ws/index.ts +0 -18
- package/src/ws/sidecar-events.test.ts +0 -96
- package/src/ws/sidecar-events.ts +0 -231
- package/src/ws/sidecar-handler.test.ts +0 -2217
- package/src/ws/sidecar-handler.ts +0 -1574
- package/tsconfig.json +0 -4
- 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_-]+$/;
|