@intx/hub-sessions 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -5
- package/dist/agent-repo.d.ts +23 -7
- package/dist/agent-repo.js +19 -6
- package/dist/agent-state-kind.js +18 -63
- package/dist/asset-service.d.ts +1 -20
- package/dist/asset-service.js +19 -97
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +52 -7
- package/dist/credential-push.js +178 -22
- package/dist/event-collector-registry.d.ts +3 -2
- package/dist/event-collector-registry.js +42 -13
- package/dist/event-collector.d.ts +12 -2
- package/dist/event-collector.js +45 -4
- package/dist/hub-session-lookups.d.ts +125 -7
- package/dist/hub-session-lookups.js +541 -86
- package/dist/hub-session-orchestrator.d.ts +2 -3
- package/dist/hub-session-orchestrator.js +22 -56
- package/dist/index.d.ts +19 -9
- package/dist/index.js +16 -7
- package/dist/reconciliation-scheduler.d.ts +14 -0
- package/dist/reconciliation-scheduler.js +55 -0
- package/dist/repo-store/index.d.ts +2 -1
- package/dist/repo-store/index.js +1 -0
- package/dist/repo-store/store.d.ts +1 -1
- package/dist/repo-store/store.js +138 -1
- package/dist/repo-store/subscribe-kind.d.ts +6 -3
- package/dist/repo-store/subscribe-kind.js +42 -77
- package/dist/repo-store/types.d.ts +94 -6
- package/dist/repo-store/user-principal-gate.d.ts +26 -0
- package/dist/repo-store/user-principal-gate.js +78 -0
- package/dist/session-service.d.ts +252 -126
- package/dist/session-service.js +851 -624
- package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
- package/dist/sidecar-allocation/capability-policy.js +124 -0
- package/dist/sidecar-allocation/contracts.d.ts +101 -0
- package/dist/sidecar-allocation/contracts.js +26 -0
- package/dist/sidecar-allocation/index.d.ts +5 -0
- package/dist/sidecar-allocation/index.js +4 -0
- package/dist/sidecar-allocation/operation.d.ts +10 -0
- package/dist/sidecar-allocation/operation.js +54 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +24 -0
- package/dist/sidecar-allocation/plugin-registry.js +61 -0
- package/dist/sidecar-allocation/reconciler.d.ts +54 -0
- package/dist/sidecar-allocation/reconciler.js +825 -0
- package/dist/skill-kind.js +12 -62
- package/dist/substrate.d.ts +3 -3
- package/dist/substrate.js +1 -1
- package/dist/workflow-allocation-service.d.ts +64 -0
- package/dist/workflow-allocation-service.js +554 -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 +42 -0
- package/dist/workflow-dispatch-service.js +209 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +29 -1
- package/dist/workflow-kind.js +143 -139
- package/dist/workflow-probe-gate.d.ts +286 -0
- package/dist/workflow-probe-gate.js +382 -0
- package/dist/workflow-run-kind.d.ts +239 -32
- package/dist/workflow-run-kind.js +874 -188
- package/dist/workflow-run-reader.d.ts +1 -1
- package/dist/workflow-run-reader.js +3 -7
- package/dist/workflow-run-restore.d.ts +16 -0
- package/dist/workflow-run-restore.js +30 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/workflow-source-pins.d.ts +8 -0
- package/dist/workflow-source-pins.js +14 -0
- package/dist/ws/index.d.ts +3 -3
- package/dist/ws/index.js +2 -2
- package/dist/ws/pending-tracker.d.ts +93 -0
- package/dist/ws/pending-tracker.js +132 -0
- package/dist/ws/sidecar-events.d.ts +139 -37
- package/dist/ws/sidecar-events.js +2 -2
- package/dist/ws/sidecar-handler.d.ts +218 -60
- package/dist/ws/sidecar-handler.js +1713 -732
- package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
- package/dist/ws/sidecar-handler.test-helpers.js +95 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
- package/dist/ws/sidecar-token-authenticator.js +78 -7
- package/package.json +14 -13
- package/dist/available-skills-stanza.d.ts +0 -21
- package/dist/available-skills-stanza.js +0 -32
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import git from "isomorphic-git";
|
|
3
1
|
import { type } from "arktype";
|
|
4
|
-
import {
|
|
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
|
|
45
|
-
* `
|
|
46
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* the
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
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(
|
|
106
|
-
const newSet = await enumerateEventBlobs(
|
|
107
|
-
if (
|
|
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(
|
|
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(
|
|
122
|
+
async function enumerateEventBlobs(reads) {
|
|
128
123
|
const out = new Map();
|
|
129
|
-
|
|
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.
|
|
146
|
-
const
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
}
|
|
@@ -80,14 +80,18 @@ export type AuthorizeFn = (principal: Principal, repoId: RepoId, ref: string, ac
|
|
|
80
80
|
* commit under validation -- present in the prospective tree and absent from
|
|
81
81
|
* the prior tree. The kind handler authoritatively detects this during its
|
|
82
82
|
* validation walk and surfaces it so callers do not re-derive terminal-ness
|
|
83
|
-
* by sniffing committed path shapes. `
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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.
|
|
88
91
|
*/
|
|
89
92
|
export type NewlyTerminalRun = {
|
|
90
93
|
runId: string;
|
|
94
|
+
status: "completed" | "failed" | "cancelled";
|
|
91
95
|
terminalEventJson: string;
|
|
92
96
|
};
|
|
93
97
|
export type ValidatePushResult = {
|
|
@@ -315,6 +319,48 @@ export interface KindHandler {
|
|
|
315
319
|
newSha: string;
|
|
316
320
|
}) => Promise<void> | void;
|
|
317
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
|
+
};
|
|
318
364
|
export interface RepoStore {
|
|
319
365
|
/**
|
|
320
366
|
* Bookkeeping primitive. Idempotent. Creates the repo directory
|
|
@@ -368,8 +414,16 @@ export interface RepoStore {
|
|
|
368
414
|
* via `resolveRef` first; the substrate exposes no force-write
|
|
369
415
|
* mode because silently overwriting a losing concurrent update is
|
|
370
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.
|
|
371
425
|
*/
|
|
372
|
-
receivePack(principal: Principal, repoId: RepoId, ref: string, pack: Uint8Array, commitSha: string, expectedOldSha: string | null): Promise<
|
|
426
|
+
receivePack(principal: Principal, repoId: RepoId, ref: string, pack: Uint8Array, commitSha: string, expectedOldSha: string | null): Promise<NewlyTerminalRun[]>;
|
|
373
427
|
createPack(principal: Principal, repoId: RepoId, ref: string): Promise<{
|
|
374
428
|
pack: Uint8Array;
|
|
375
429
|
commitSha: string;
|
|
@@ -425,6 +479,40 @@ export interface RepoStore {
|
|
|
425
479
|
* they reach into for ref-listing and pack negotiation.
|
|
426
480
|
*/
|
|
427
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>;
|
|
428
516
|
/**
|
|
429
517
|
* Tail a ref's commit log. Returns an async iterator that emits
|
|
430
518
|
* `{ seq, event }` entries: one per commit on the ref. `seq` is
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RepoAction, RepoId, Principal } from "./types.js";
|
|
2
|
+
export type AuthorizeUserPrincipalArgs = {
|
|
3
|
+
principal: Principal;
|
|
4
|
+
repoId: RepoId;
|
|
5
|
+
ref: string;
|
|
6
|
+
action: RepoAction;
|
|
7
|
+
/**
|
|
8
|
+
* The resource-kind prefix the pre-resolved authz verdict must carry
|
|
9
|
+
* for this kind: the verdict's `resource` is compared against
|
|
10
|
+
* `<resourcePrefix>:<repoId.id>`.
|
|
11
|
+
*/
|
|
12
|
+
resourcePrefix: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Verdict for a `user` principal performing `action` on `ref` of
|
|
16
|
+
* `repoId`, in the shape the substrate's `AuthorizeFn` contract
|
|
17
|
+
* expects. The caller dispatches on `principal.kind === "user"` first;
|
|
18
|
+
* this function narrows with `UserPrincipal` and applies the full
|
|
19
|
+
* claim/verdict cross-check.
|
|
20
|
+
*/
|
|
21
|
+
export declare function authorizeUserPrincipal({ principal, repoId, ref, action, resourcePrefix, }: AuthorizeUserPrincipalArgs): {
|
|
22
|
+
allowed: true;
|
|
23
|
+
} | {
|
|
24
|
+
allowed: false;
|
|
25
|
+
reason: string;
|
|
26
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Shared authorization gate for the `user` principal variant, used by
|
|
2
|
+
// every kind handler that accepts user-token-authenticated requests
|
|
3
|
+
// (workflow, skill, agent-state, workflow-run).
|
|
4
|
+
//
|
|
5
|
+
// The route layer has already pre-resolved the grant verdict and
|
|
6
|
+
// attached it as `authz`; the kind handlers do NOT re-query the grant
|
|
7
|
+
// store here. This gate (a) checks the bearer-token's claims bound the
|
|
8
|
+
// requested (ref, action) and have not expired, and (b) sanity-checks
|
|
9
|
+
// that the pre-resolved verdict targets this exact resource and grant
|
|
10
|
+
// verb. Both gates must pass before the verdict's `effect` is honoured.
|
|
11
|
+
//
|
|
12
|
+
// Funnelling every kind through this one gate keeps the security-
|
|
13
|
+
// critical claim/verdict cross-check from drifting between kinds. The
|
|
14
|
+
// only per-kind input is the `resourcePrefix` the verdict's `resource`
|
|
15
|
+
// must carry (`asset:<id>` for the codebase kinds, `agent-state:<id>`,
|
|
16
|
+
// `workflow-run:<id>`).
|
|
17
|
+
import { type } from "arktype";
|
|
18
|
+
import { glob, repoActionToGrantVerb } from "@intx/hub-common";
|
|
19
|
+
import { UserPrincipal } from "./types.js";
|
|
20
|
+
/**
|
|
21
|
+
* Verdict for a `user` principal performing `action` on `ref` of
|
|
22
|
+
* `repoId`, in the shape the substrate's `AuthorizeFn` contract
|
|
23
|
+
* expects. The caller dispatches on `principal.kind === "user"` first;
|
|
24
|
+
* this function narrows with `UserPrincipal` and applies the full
|
|
25
|
+
* claim/verdict cross-check.
|
|
26
|
+
*/
|
|
27
|
+
export function authorizeUserPrincipal({ principal, repoId, ref, action, resourcePrefix, }) {
|
|
28
|
+
const parsed = UserPrincipal(principal);
|
|
29
|
+
if (parsed instanceof type.errors) {
|
|
30
|
+
return {
|
|
31
|
+
allowed: false,
|
|
32
|
+
reason: `user principal is malformed: ${parsed.summary}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (!parsed.tokenClaims.actions.includes(action)) {
|
|
36
|
+
return {
|
|
37
|
+
allowed: false,
|
|
38
|
+
reason: `token does not grant action ${action}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// `ref === "*"` is the substrate's sentinel for the bulk read
|
|
42
|
+
// performed by `listRefs`. Per-ref filtering is the advertise-refs
|
|
43
|
+
// layer's responsibility, so the bulk read is gated on action and
|
|
44
|
+
// expiry alone.
|
|
45
|
+
if (ref !== "*" && !glob.match(parsed.tokenClaims.refPattern, ref)) {
|
|
46
|
+
return {
|
|
47
|
+
allowed: false,
|
|
48
|
+
reason: `token refPattern ${parsed.tokenClaims.refPattern} does not match ${ref}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (Date.now() >= parsed.tokenClaims.expiresAt) {
|
|
52
|
+
return {
|
|
53
|
+
allowed: false,
|
|
54
|
+
reason: `token expired at ${parsed.tokenClaims.expiresAt}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const expectedResource = `${resourcePrefix}:${repoId.id}`;
|
|
58
|
+
if (parsed.authz.resource !== expectedResource) {
|
|
59
|
+
return {
|
|
60
|
+
allowed: false,
|
|
61
|
+
reason: `authz verdict resource ${parsed.authz.resource} does not match ${expectedResource}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const expectedGrantVerb = repoActionToGrantVerb(action);
|
|
65
|
+
if (parsed.authz.grantVerb !== expectedGrantVerb) {
|
|
66
|
+
return {
|
|
67
|
+
allowed: false,
|
|
68
|
+
reason: `authz verdict grantVerb ${parsed.authz.grantVerb} does not match ${expectedGrantVerb}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (parsed.authz.effect === "allow") {
|
|
72
|
+
return { allowed: true };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
allowed: false,
|
|
76
|
+
reason: `authz verdict denied for ${expectedResource} ${expectedGrantVerb}`,
|
|
77
|
+
};
|
|
78
|
+
}
|