@intx/hub-sessions 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -5
- package/dist/agent-repo.d.ts +9 -5
- package/dist/agent-repo.js +2 -2
- package/dist/agent-state-kind.js +4 -0
- package/dist/asset-service.d.ts +1 -20
- package/dist/asset-service.js +9 -91
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +7 -6
- package/dist/credential-push.js +42 -18
- package/dist/event-collector-registry.d.ts +1 -1
- package/dist/event-collector-registry.js +4 -4
- package/dist/event-collector.d.ts +1 -1
- package/dist/event-collector.js +10 -2
- package/dist/hub-session-lookups.d.ts +125 -7
- package/dist/hub-session-lookups.js +539 -80
- package/dist/hub-session-orchestrator.js +14 -49
- package/dist/index.d.ts +17 -8
- package/dist/index.js +14 -6
- package/dist/repo-store/index.d.ts +1 -1
- 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/session-service.d.ts +277 -96
- package/dist/session-service.js +741 -547
- 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.js +4 -0
- package/dist/substrate.d.ts +3 -3
- package/dist/substrate.js +1 -1
- 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 +17 -1
- package/dist/workflow-kind.js +127 -80
- package/dist/workflow-probe-gate.d.ts +214 -0
- package/dist/workflow-probe-gate.js +207 -0
- package/dist/workflow-run-kind.d.ts +128 -14
- package/dist/workflow-run-kind.js +353 -83
- package/dist/workflow-run-reader.d.ts +1 -1
- package/dist/workflow-run-reader.js +3 -7
- 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 -3
- package/dist/ws/index.js +1 -1
- package/dist/ws/sidecar-events.d.ts +100 -12
- package/dist/ws/sidecar-events.js +2 -0
- package/dist/ws/sidecar-handler.d.ts +128 -7
- package/dist/ws/sidecar-handler.js +1069 -135
- package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
- package/dist/ws/sidecar-token-authenticator.js +64 -7
- package/package.json +14 -13
- package/dist/available-skills-stanza.d.ts +0 -21
- package/dist/available-skills-stanza.js +0 -32
package/README.md
CHANGED
|
@@ -95,11 +95,9 @@ for the authoritative type shapes.
|
|
|
95
95
|
### Asset service
|
|
96
96
|
|
|
97
97
|
- `createAssetService` / `AssetService`, `AssetServiceError`,
|
|
98
|
-
`DEFAULT_ASSET_REF`, and the supporting types `Asset`,
|
|
99
|
-
`
|
|
100
|
-
`
|
|
101
|
-
`AssetServiceErrorReason`, `ReadAssetBlobParams`,
|
|
102
|
-
`ListAssetBlobsParams`.
|
|
98
|
+
`DEFAULT_ASSET_REF`, and the supporting types `Asset`,
|
|
99
|
+
`CreateAssetParams`, `PopulateAssetParams`, `AssetServiceErrorReason`,
|
|
100
|
+
`ReadAssetBlobParams`, `ListAssetBlobsParams`.
|
|
103
101
|
|
|
104
102
|
### Skills stanza and credential push
|
|
105
103
|
|
package/dist/agent-repo.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { GCPolicy } from "@intx/storage-isogit";
|
|
1
|
+
import type { GCPolicy } from "@intx/storage-isogit/node";
|
|
2
2
|
import type { ToolPackageManifest } from "@intx/types/tool-packages";
|
|
3
|
-
import type { RepoId, RepoStore } from "./repo-store/index.js";
|
|
3
|
+
import type { NewlyTerminalRun, RepoId, RepoStore } from "./repo-store/index.js";
|
|
4
4
|
export type DeployContent = {
|
|
5
5
|
systemPrompt: string;
|
|
6
6
|
/**
|
|
@@ -44,7 +44,7 @@ export type AgentRepoStore = {
|
|
|
44
44
|
* tree.
|
|
45
45
|
*
|
|
46
46
|
* `repoId.kind` must be `"agent-state"`. The `repoId.id` is used as
|
|
47
|
-
* the
|
|
47
|
+
* the run address internally. Stamps an
|
|
48
48
|
* `AgentStateSidecarPrincipal` so the agent-state kind handler
|
|
49
49
|
* authorizes the receivePack as a per-agent sidecar write.
|
|
50
50
|
*/
|
|
@@ -55,13 +55,17 @@ export type AgentRepoStore = {
|
|
|
55
55
|
* tree.
|
|
56
56
|
*
|
|
57
57
|
* `repoId.kind` must be `"workflow-run"`. Stamps a
|
|
58
|
-
* `WorkflowRunSupervisorPrincipal` whose `
|
|
58
|
+
* `WorkflowRunSupervisorPrincipal` whose `anchorRunId` equals
|
|
59
59
|
* `repoId.id`; the workflow-run kind handler denies sidecar-kind
|
|
60
60
|
* writes by design, so the supervisor branch is the correct path for
|
|
61
61
|
* sidecar-originated run-event commits (the supervisor lives on the
|
|
62
62
|
* sidecar and is the actual signer of the run events).
|
|
63
|
+
*
|
|
64
|
+
* Returns the runs the pack drove terminal, surfaced from the
|
|
65
|
+
* substrate's per-commit validation so a caller can flip the run's
|
|
66
|
+
* DB state without re-deriving terminal-ness.
|
|
63
67
|
*/
|
|
64
|
-
receiveWorkflowRunPack(repoId: RepoId, pack: Uint8Array, ref: string, commitSha: string): Promise<
|
|
68
|
+
receiveWorkflowRunPack(repoId: RepoId, pack: Uint8Array, ref: string, commitSha: string): Promise<NewlyTerminalRun[]>;
|
|
65
69
|
/** Resolve the current deploy ref SHA, or null if no deploy exists. */
|
|
66
70
|
getDeployRef(agentId: string): Promise<string | null>;
|
|
67
71
|
/** Raw 32-byte Ed25519 public key used to sign deploy commits. */
|
package/dist/agent-repo.js
CHANGED
|
@@ -93,10 +93,10 @@ export function createAgentRepoStore(config) {
|
|
|
93
93
|
}
|
|
94
94
|
const principal = {
|
|
95
95
|
kind: "supervisor",
|
|
96
|
-
|
|
96
|
+
anchorRunId: incomingRepoId.id,
|
|
97
97
|
};
|
|
98
98
|
const expectedOldSha = await store.resolveRef(hub, incomingRepoId, ref);
|
|
99
|
-
|
|
99
|
+
return store.receivePack(principal, incomingRepoId, ref, pack, commitSha, expectedOldSha);
|
|
100
100
|
},
|
|
101
101
|
async getDeployRef(agentId) {
|
|
102
102
|
return store.resolveRef(hub, repoId(agentId), AGENT_STATE_DEPLOY_REF);
|
package/dist/agent-state-kind.js
CHANGED
|
@@ -178,6 +178,10 @@ export const agentStateAuthorize = (principal, repoId, ref, action) => {
|
|
|
178
178
|
reason: `authz verdict denied for ${expectedResource} ${expectedGrantVerb}`,
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
|
+
// Fail closed on any kind not handled above. The tenant-level
|
|
182
|
+
// `workflow` principal kind (`@intx/types` principalKinds) is a
|
|
183
|
+
// grant owner, not an agent-state bearer, and never carries an
|
|
184
|
+
// agent-state repo push here -- so it is intentionally left denied.
|
|
181
185
|
return {
|
|
182
186
|
allowed: false,
|
|
183
187
|
reason: `unknown principal kind: ${principal.kind}`,
|
package/dist/asset-service.d.ts
CHANGED
|
@@ -11,18 +11,6 @@ export type Asset = {
|
|
|
11
11
|
createdAt: Date;
|
|
12
12
|
updatedAt: Date;
|
|
13
13
|
};
|
|
14
|
-
export type AccessMode = "read-only" | "read-write";
|
|
15
|
-
export type AgentAsset = {
|
|
16
|
-
id: string;
|
|
17
|
-
agentId: string;
|
|
18
|
-
assetId: string;
|
|
19
|
-
ref: string;
|
|
20
|
-
accessMode: AccessMode;
|
|
21
|
-
createdAt: Date;
|
|
22
|
-
};
|
|
23
|
-
export type AgentAssetWithAsset = AgentAsset & {
|
|
24
|
-
asset: Pick<Asset, "id" | "tenantId" | "kind" | "name" | "displayName">;
|
|
25
|
-
};
|
|
26
14
|
export type CreateAssetParams = {
|
|
27
15
|
tenantId: string;
|
|
28
16
|
/** Accepted kinds: "skill", "package-registry", "workflow".
|
|
@@ -48,19 +36,11 @@ export type PopulateAssetParams = {
|
|
|
48
36
|
* (e.g. skillAuthorize only permits `kind: "hub"` writes). */
|
|
49
37
|
principal: Principal;
|
|
50
38
|
};
|
|
51
|
-
export type AttachAssetParams = {
|
|
52
|
-
agentId: string;
|
|
53
|
-
assetId: string;
|
|
54
|
-
ref: string;
|
|
55
|
-
accessMode?: AccessMode;
|
|
56
|
-
};
|
|
57
39
|
export interface AssetService {
|
|
58
40
|
createAsset(params: CreateAssetParams): Promise<Asset>;
|
|
59
41
|
populateAsset(params: PopulateAssetParams): Promise<{
|
|
60
42
|
commitSha: string;
|
|
61
43
|
}>;
|
|
62
|
-
attachAsset(params: AttachAssetParams): Promise<AgentAsset>;
|
|
63
|
-
listAgentAssets(agentId: string): Promise<AgentAssetWithAsset[]>;
|
|
64
44
|
/**
|
|
65
45
|
* In-process blob read. Resolves the asset's row, then reads the blob
|
|
66
46
|
* at `path` from the commit pointed to by `ref` (defaults to
|
|
@@ -99,6 +79,7 @@ export declare const DEFAULT_ASSET_REF = "refs/heads/main";
|
|
|
99
79
|
/** Discriminator for AssetServiceError variants. Lets callers branch
|
|
100
80
|
* without instanceof gymnastics across the different error subclasses. */
|
|
101
81
|
export type AssetServiceErrorReason = "unsupported_kind" | "duplicate_asset" | "duplicate_attachment" | "invalid_name" | "invalid_reference" | "name_reserved" | "not_found" | "path_violation";
|
|
82
|
+
export declare const ASSET_NAME_PATTERN: RegExp;
|
|
102
83
|
export declare class AssetServiceError extends Error {
|
|
103
84
|
readonly reason: AssetServiceErrorReason;
|
|
104
85
|
constructor(reason: AssetServiceErrorReason, message: string, cause?: unknown);
|
package/dist/asset-service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// In-process service for creating and
|
|
1
|
+
// In-process service for creating and populating skill-asset repos.
|
|
2
2
|
//
|
|
3
3
|
// Three responsibilities are layered here, mirroring the substrate's
|
|
4
4
|
// own layering (DB row, repo bookkeeping, content validation):
|
|
@@ -8,19 +8,15 @@
|
|
|
8
8
|
// populateAsset drives RepoStore.writeTree, which runs the kind
|
|
9
9
|
// handler's validatePush before advancing the ref.
|
|
10
10
|
// Content rejections surface as AssetValidationError.
|
|
11
|
-
// attachAsset inserts an agent_asset row, surfacing the
|
|
12
|
-
// (agentId, assetId) uniqueness violation (which
|
|
13
|
-
// prevents the same asset being attached to one
|
|
14
|
-
// agent twice) as AssetAttachError.
|
|
15
11
|
//
|
|
16
12
|
// The factory is closure-based to match createAgentRepoStore and
|
|
17
13
|
// createRepoStore. There is no class because there is no per-instance
|
|
18
14
|
// mutable state — every method is a pure function over the deps.
|
|
19
15
|
import fs from "node:fs";
|
|
20
|
-
import {
|
|
16
|
+
import { eq } from "drizzle-orm";
|
|
21
17
|
import git from "isomorphic-git";
|
|
22
|
-
import { pgErrorCode, PG_UNIQUE_VIOLATION
|
|
23
|
-
import {
|
|
18
|
+
import { pgErrorCode, PG_UNIQUE_VIOLATION } from "@intx/db";
|
|
19
|
+
import { asset as assetTable } from "@intx/db/schema";
|
|
24
20
|
import { generateId } from "@intx/hub-common";
|
|
25
21
|
import { getLogger } from "@intx/log";
|
|
26
22
|
const logger = getLogger(["hub-sessions", "asset-service"]);
|
|
@@ -36,8 +32,11 @@ export const DEFAULT_ASSET_REF = "refs/heads/main";
|
|
|
36
32
|
// character set; validate at the createAsset boundary so a bad name
|
|
37
33
|
// fails at creation time rather than at materialization time. Names
|
|
38
34
|
// must be lowercase-kebab: lowercase letters, digits, hyphens, with
|
|
39
|
-
// no leading or trailing hyphen.
|
|
40
|
-
|
|
35
|
+
// no leading or trailing hyphen. Exported so a caller deriving a name
|
|
36
|
+
// (the agent-fold materializer) asserts the shape at its own boundary
|
|
37
|
+
// rather than discovering a violation three layers in as a generic
|
|
38
|
+
// `invalid_name`.
|
|
39
|
+
export const ASSET_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
41
40
|
export class AssetServiceError extends Error {
|
|
42
41
|
reason;
|
|
43
42
|
constructor(reason, message, cause) {
|
|
@@ -46,9 +45,6 @@ export class AssetServiceError extends Error {
|
|
|
46
45
|
this.reason = reason;
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
|
-
function isAccessMode(value) {
|
|
50
|
-
return value === "read-only" || value === "read-write";
|
|
51
|
-
}
|
|
52
48
|
/**
|
|
53
49
|
* Reject malformed `dir` arguments at the `listAssetBlobs` boundary
|
|
54
50
|
* before any tree walk begins. The empty string is the documented
|
|
@@ -112,19 +108,6 @@ function rowToAsset(row) {
|
|
|
112
108
|
updatedAt: row.updatedAt,
|
|
113
109
|
};
|
|
114
110
|
}
|
|
115
|
-
function rowToAgentAsset(row) {
|
|
116
|
-
if (!isAccessMode(row.accessMode)) {
|
|
117
|
-
throw new Error(`agent_asset row ${row.id} has unknown accessMode ${JSON.stringify(row.accessMode)}`);
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
id: row.id,
|
|
121
|
-
agentId: row.agentId,
|
|
122
|
-
assetId: row.assetId,
|
|
123
|
-
ref: row.ref,
|
|
124
|
-
accessMode: row.accessMode,
|
|
125
|
-
createdAt: row.createdAt,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
111
|
export function createAssetService(deps) {
|
|
129
112
|
const { db, repoStore } = deps;
|
|
130
113
|
const reservedPackageRegistryNames = deps.reservedPackageRegistryNames ?? new Set();
|
|
@@ -216,69 +199,6 @@ export function createAssetService(deps) {
|
|
|
216
199
|
throw err;
|
|
217
200
|
}
|
|
218
201
|
}
|
|
219
|
-
async function attachAsset(params) {
|
|
220
|
-
const id = generateId("agentAsset");
|
|
221
|
-
const accessMode = params.accessMode ?? "read-only";
|
|
222
|
-
const insertRow = {
|
|
223
|
-
id,
|
|
224
|
-
agentId: params.agentId,
|
|
225
|
-
assetId: params.assetId,
|
|
226
|
-
ref: params.ref,
|
|
227
|
-
accessMode,
|
|
228
|
-
createdAt: new Date(),
|
|
229
|
-
};
|
|
230
|
-
let inserted;
|
|
231
|
-
try {
|
|
232
|
-
const rows = await db
|
|
233
|
-
.insert(agentAssetTable)
|
|
234
|
-
.values(insertRow)
|
|
235
|
-
.returning();
|
|
236
|
-
const row = rows[0];
|
|
237
|
-
if (row === undefined) {
|
|
238
|
-
throw new Error("insert into agent_asset returned no rows");
|
|
239
|
-
}
|
|
240
|
-
inserted = row;
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
if (pgErrorCode(err) === PG_UNIQUE_VIOLATION) {
|
|
244
|
-
throw new AssetServiceError("duplicate_attachment", `agent_asset (agentId=${params.agentId}, assetId=${params.assetId}) already attached`, err);
|
|
245
|
-
}
|
|
246
|
-
if (pgErrorCode(err) === PG_FOREIGN_KEY_VIOLATION) {
|
|
247
|
-
throw new AssetServiceError("invalid_reference", `agent_asset (agentId=${params.agentId}, assetId=${params.assetId}) references a missing agent or asset`, err);
|
|
248
|
-
}
|
|
249
|
-
throw err;
|
|
250
|
-
}
|
|
251
|
-
return rowToAgentAsset(inserted);
|
|
252
|
-
}
|
|
253
|
-
async function listAgentAssets(agentId) {
|
|
254
|
-
// Order by (createdAt, id) so the row sequence is stable across reads
|
|
255
|
-
// — the available_skills stanza and pack fan-out both depend on a
|
|
256
|
-
// deterministic order, and Postgres does not guarantee one without
|
|
257
|
-
// an explicit orderBy.
|
|
258
|
-
const rows = await db
|
|
259
|
-
.select({
|
|
260
|
-
agentAsset: agentAssetTable,
|
|
261
|
-
asset: assetTable,
|
|
262
|
-
})
|
|
263
|
-
.from(agentAssetTable)
|
|
264
|
-
.innerJoin(assetTable, eq(agentAssetTable.assetId, assetTable.id))
|
|
265
|
-
.where(eq(agentAssetTable.agentId, agentId))
|
|
266
|
-
.orderBy(asc(agentAssetTable.createdAt), asc(agentAssetTable.id));
|
|
267
|
-
return rows.map((row) => {
|
|
268
|
-
const aa = rowToAgentAsset(row.agentAsset);
|
|
269
|
-
const a = rowToAsset(row.asset);
|
|
270
|
-
return {
|
|
271
|
-
...aa,
|
|
272
|
-
asset: {
|
|
273
|
-
id: a.id,
|
|
274
|
-
tenantId: a.tenantId,
|
|
275
|
-
kind: a.kind,
|
|
276
|
-
name: a.name,
|
|
277
|
-
displayName: a.displayName,
|
|
278
|
-
},
|
|
279
|
-
};
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
202
|
async function resolveAssetRowOrThrow(assetId, label) {
|
|
283
203
|
const row = await db.query.asset.findFirst({
|
|
284
204
|
where: eq(assetTable.id, assetId),
|
|
@@ -341,8 +261,6 @@ export function createAssetService(deps) {
|
|
|
341
261
|
return {
|
|
342
262
|
createAsset,
|
|
343
263
|
populateAsset,
|
|
344
|
-
attachAsset,
|
|
345
|
-
listAgentAssets,
|
|
346
264
|
readAssetBlob,
|
|
347
265
|
listAssetBlobs,
|
|
348
266
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommittedReads } from "./repo-store/types.js";
|
|
2
|
+
import type { SourceTreeReads } from "./workflow-source-closure.js";
|
|
3
|
+
/**
|
|
4
|
+
* Wrap `reads` so `readBlob(path)` resolves a repo-relative POSIX path to its
|
|
5
|
+
* blob: list the path's parent directory, match the final segment as a `blob`
|
|
6
|
+
* entry, then read by its object id. `listDir` and `treeOid` pass straight
|
|
7
|
+
* through -- both handles already speak repo-relative paths and return the same
|
|
8
|
+
* shapes.
|
|
9
|
+
*/
|
|
10
|
+
export declare function committedReadsToSourceTree(reads: CommittedReads): SourceTreeReads;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Adapts a repo-store `CommittedReads` handle (reads pinned to a commit,
|
|
2
|
+
// addressed by object id) to the `SourceTreeReads` shape the source-closure
|
|
3
|
+
// resolver consumes (reads addressed by repo-relative path). This bridge lives
|
|
4
|
+
// at the caller layer, not inside the resolver: the resolver stays agnostic of
|
|
5
|
+
// where its tree bytes come from (a test fake, this store, a future one), and
|
|
6
|
+
// the repo-store stays unaware of the closure interface. Only a layer that
|
|
7
|
+
// composes both types -- the install/deploy glue's caller -- owns the join.
|
|
8
|
+
/**
|
|
9
|
+
* Wrap `reads` so `readBlob(path)` resolves a repo-relative POSIX path to its
|
|
10
|
+
* blob: list the path's parent directory, match the final segment as a `blob`
|
|
11
|
+
* entry, then read by its object id. `listDir` and `treeOid` pass straight
|
|
12
|
+
* through -- both handles already speak repo-relative paths and return the same
|
|
13
|
+
* shapes.
|
|
14
|
+
*/
|
|
15
|
+
export function committedReadsToSourceTree(reads) {
|
|
16
|
+
return {
|
|
17
|
+
async readBlob(blobPath) {
|
|
18
|
+
// The parent of a top-level path (no slash) is the root tree, which
|
|
19
|
+
// `CommittedReads.listDir` addresses with the empty string.
|
|
20
|
+
const slash = blobPath.lastIndexOf("/");
|
|
21
|
+
const parentDir = slash === -1 ? "" : blobPath.slice(0, slash);
|
|
22
|
+
const name = slash === -1 ? blobPath : blobPath.slice(slash + 1);
|
|
23
|
+
const entries = await reads.listDir(parentDir);
|
|
24
|
+
// Match on the blob type so a path naming a directory or a gitlink fails
|
|
25
|
+
// loud here rather than handing a tree/commit oid to `readBlobByOid`.
|
|
26
|
+
const entry = entries.find((e) => e.name === name && e.type === "blob");
|
|
27
|
+
if (entry === undefined) {
|
|
28
|
+
throw new Error(`committedReadsToSourceTree: no blob at ${JSON.stringify(blobPath)}`);
|
|
29
|
+
}
|
|
30
|
+
return reads.readBlobByOid(entry.oid);
|
|
31
|
+
},
|
|
32
|
+
listDir: (dir) => reads.listDir(dir),
|
|
33
|
+
treeOid: (dir) => reads.treeOid(dir),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { DB } from "@intx/db";
|
|
2
|
+
import type { CredentialCipher } from "@intx/types";
|
|
2
3
|
import type { SidecarRouter } from "./ws/sidecar-handler.js";
|
|
3
4
|
/**
|
|
4
5
|
* Re-resolve a single running instance's inference sources from the catalog
|
|
5
|
-
* (the
|
|
6
|
-
* the instance) and push the ordered list to its sidecar. The head of the
|
|
6
|
+
* (the definition's model requirements plus the invoker preferences persisted
|
|
7
|
+
* on the instance) and push the ordered list to its sidecar. The head of the
|
|
7
8
|
* catalog-priority-ordered list is the active default; the tail is the
|
|
8
9
|
* failover chain.
|
|
9
10
|
*
|
|
@@ -12,21 +13,21 @@ import type { SidecarRouter } from "./ws/sidecar-handler.js";
|
|
|
12
13
|
*/
|
|
13
14
|
export declare function pushInstanceSourceUpdate(db: DB["db"], sidecarRouter: Pick<SidecarRouter, "sendSourcesUpdate">, instance: {
|
|
14
15
|
address: string;
|
|
15
|
-
|
|
16
|
+
definitionId: string;
|
|
16
17
|
tenantId: string;
|
|
17
18
|
modelPreferences: unknown;
|
|
18
|
-
}): Promise<void>;
|
|
19
|
+
}, credentialCipher?: CredentialCipher): Promise<void>;
|
|
19
20
|
/**
|
|
20
21
|
* After a credential secret is rotated, re-resolve every running instance in
|
|
21
22
|
* the tenant against the catalog and push the updates. A rotated secret flows
|
|
22
23
|
* through because resolution dereferences the provider's credential reference
|
|
23
24
|
* to the current secret.
|
|
24
25
|
*/
|
|
25
|
-
export declare function pushSourceUpdates(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string): Promise<void>;
|
|
26
|
+
export declare function pushSourceUpdates(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string, credentialCipher?: CredentialCipher): Promise<void>;
|
|
26
27
|
/**
|
|
27
28
|
* After a catalog edit in a tenant, re-resolve and push to every running
|
|
28
29
|
* instance in that tenant AND its descendants. Descendants inherit the
|
|
29
30
|
* edited tenant's catalog, so a change there (a disabled provider, a new
|
|
30
31
|
* offering, a price update) alters their resolved sources too.
|
|
31
32
|
*/
|
|
32
|
-
export declare function pushSourceUpdatesSubtree(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string): Promise<void>;
|
|
33
|
+
export declare function pushSourceUpdatesSubtree(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string, credentialCipher?: CredentialCipher): Promise<void>;
|
package/dist/credential-push.js
CHANGED
|
@@ -2,24 +2,25 @@
|
|
|
2
2
|
// the catalog and pushing the update to its sidecar.
|
|
3
3
|
//
|
|
4
4
|
// Used after a credential secret rotation (a model provider's credential
|
|
5
|
-
// changes the resolved source's apiKey) and
|
|
6
|
-
|
|
5
|
+
// changes the resolved source's apiKey) and after a catalog edit in a tenant
|
|
6
|
+
// or its subtree.
|
|
7
|
+
import { eq, and, inArray, isNull, isNotNull } from "drizzle-orm";
|
|
7
8
|
import { getLogger } from "@intx/log";
|
|
8
|
-
import {
|
|
9
|
+
import { workflowRun } from "@intx/db/schema";
|
|
9
10
|
import { resolveInstanceModelSources, getDescendantTenants } from "@intx/db";
|
|
10
11
|
const log = getLogger(["hub", "credentials"]);
|
|
11
12
|
/**
|
|
12
13
|
* Re-resolve a single running instance's inference sources from the catalog
|
|
13
|
-
* (the
|
|
14
|
-
* the instance) and push the ordered list to its sidecar. The head of the
|
|
14
|
+
* (the definition's model requirements plus the invoker preferences persisted
|
|
15
|
+
* on the instance) and push the ordered list to its sidecar. The head of the
|
|
15
16
|
* catalog-priority-ordered list is the active default; the tail is the
|
|
16
17
|
* failover chain.
|
|
17
18
|
*
|
|
18
19
|
* No-op when the instance resolves to no launchable source — the resolver's
|
|
19
20
|
* own logger is the signal for why.
|
|
20
21
|
*/
|
|
21
|
-
export async function pushInstanceSourceUpdate(db, sidecarRouter, instance) {
|
|
22
|
-
const resolution = await resolveInstanceModelSources(db, instance.tenantId, instance);
|
|
22
|
+
export async function pushInstanceSourceUpdate(db, sidecarRouter, instance, credentialCipher) {
|
|
23
|
+
const resolution = await resolveInstanceModelSources(db, instance.tenantId, instance, credentialCipher);
|
|
23
24
|
if (!resolution.ok)
|
|
24
25
|
return;
|
|
25
26
|
const [head] = resolution.sources;
|
|
@@ -29,11 +30,15 @@ export async function pushInstanceSourceUpdate(db, sidecarRouter, instance) {
|
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
32
|
* Re-resolve every running instance in the given tenants against the catalog
|
|
32
|
-
* and push the updates to sidecars.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* and push the updates to sidecars. The running instances are the folded runs
|
|
34
|
+
* a launch produces: born running, with a routing address and no deployment.
|
|
35
|
+
* Deployment-anchor runs (which own a deployment id and a workflow-derived
|
|
36
|
+
* address) and address-less child runs route via the deployment, not this
|
|
37
|
+
* per-instance push, so they are excluded. Each instance re-resolves from its
|
|
38
|
+
* own tenant's context (its ancestor chain), so the rotated/edited upstream
|
|
39
|
+
* entry flows through. Errors are logged per-instance but do not propagate.
|
|
35
40
|
*/
|
|
36
|
-
async function pushSourceUpdatesToTenants(db, sidecarRouter, tenantIds) {
|
|
41
|
+
async function pushSourceUpdatesToTenants(db, sidecarRouter, tenantIds, credentialCipher) {
|
|
37
42
|
if (tenantIds.length === 0)
|
|
38
43
|
return;
|
|
39
44
|
// Callers fire this without awaiting, so it must never reject: a failure to
|
|
@@ -41,12 +46,31 @@ async function pushSourceUpdatesToTenants(db, sidecarRouter, tenantIds) {
|
|
|
41
46
|
// rejection. The push is best effort — the next mutation or a sidecar
|
|
42
47
|
// reconnect re-resolves sources.
|
|
43
48
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
// The instance-shaped runs: running, addressable, and not anchored on a
|
|
50
|
+
// deployment. `anchorRunId IS NULL` excludes the deployment-anchor runs
|
|
51
|
+
// (which set it to their own id), so no deployment-anchor run address can
|
|
52
|
+
// reach the address-targeted push below. Mirrors the /me/workflows/runs and
|
|
53
|
+
// tenant run-list predicate.
|
|
54
|
+
const instances = await db.query.workflowRun.findMany({
|
|
55
|
+
where: and(inArray(workflowRun.tenantId, tenantIds), eq(workflowRun.status, "running"), isNull(workflowRun.anchorRunId), isNotNull(workflowRun.address)),
|
|
46
56
|
});
|
|
47
57
|
if (instances.length === 0)
|
|
48
58
|
return;
|
|
49
|
-
const results = await Promise.allSettled(instances.map((instance) =>
|
|
59
|
+
const results = await Promise.allSettled(instances.map(async (instance) => {
|
|
60
|
+
// The isNotNull(address) filter guarantees a value; a null here is a
|
|
61
|
+
// broken invariant. The callback is async, so this throw becomes a
|
|
62
|
+
// rejected promise captured per-instance by allSettled and logged
|
|
63
|
+
// below -- one bad row is surfaced, not fatal to the whole batch.
|
|
64
|
+
if (instance.address === null) {
|
|
65
|
+
throw new Error(`running run ${instance.id} matched the non-null-address filter but has a null address`);
|
|
66
|
+
}
|
|
67
|
+
return pushInstanceSourceUpdate(db, sidecarRouter, {
|
|
68
|
+
address: instance.address,
|
|
69
|
+
definitionId: instance.definitionId,
|
|
70
|
+
tenantId: instance.tenantId,
|
|
71
|
+
modelPreferences: instance.modelPreferences,
|
|
72
|
+
}, credentialCipher);
|
|
73
|
+
}));
|
|
50
74
|
for (const result of results) {
|
|
51
75
|
if (result.status === "rejected") {
|
|
52
76
|
log.warn `Failed to push source update: ${String(result.reason)}`;
|
|
@@ -63,8 +87,8 @@ async function pushSourceUpdatesToTenants(db, sidecarRouter, tenantIds) {
|
|
|
63
87
|
* through because resolution dereferences the provider's credential reference
|
|
64
88
|
* to the current secret.
|
|
65
89
|
*/
|
|
66
|
-
export async function pushSourceUpdates(db, sidecarRouter, tenantId) {
|
|
67
|
-
await pushSourceUpdatesToTenants(db, sidecarRouter, [tenantId]);
|
|
90
|
+
export async function pushSourceUpdates(db, sidecarRouter, tenantId, credentialCipher) {
|
|
91
|
+
await pushSourceUpdatesToTenants(db, sidecarRouter, [tenantId], credentialCipher);
|
|
68
92
|
}
|
|
69
93
|
/**
|
|
70
94
|
* After a catalog edit in a tenant, re-resolve and push to every running
|
|
@@ -72,7 +96,7 @@ export async function pushSourceUpdates(db, sidecarRouter, tenantId) {
|
|
|
72
96
|
* edited tenant's catalog, so a change there (a disabled provider, a new
|
|
73
97
|
* offering, a price update) alters their resolved sources too.
|
|
74
98
|
*/
|
|
75
|
-
export async function pushSourceUpdatesSubtree(db, sidecarRouter, tenantId) {
|
|
99
|
+
export async function pushSourceUpdatesSubtree(db, sidecarRouter, tenantId, credentialCipher) {
|
|
76
100
|
let tenants;
|
|
77
101
|
try {
|
|
78
102
|
tenants = await getDescendantTenants(db, tenantId);
|
|
@@ -81,5 +105,5 @@ export async function pushSourceUpdatesSubtree(db, sidecarRouter, tenantId) {
|
|
|
81
105
|
log.warn `Failed to enumerate descendants for source push: ${String(err)}`;
|
|
82
106
|
return;
|
|
83
107
|
}
|
|
84
|
-
await pushSourceUpdatesToTenants(db, sidecarRouter, tenants);
|
|
108
|
+
await pushSourceUpdatesToTenants(db, sidecarRouter, tenants, credentialCipher);
|
|
85
109
|
}
|
|
@@ -3,7 +3,7 @@ import type { InferenceEvent } from "@intx/types/runtime";
|
|
|
3
3
|
import type { SessionStatus } from "@intx/types";
|
|
4
4
|
import { type TurnFinalized } from "./event-collector.js";
|
|
5
5
|
export type EventCollectorRegistry = {
|
|
6
|
-
create(agentAddress: string, tenantId: string, sessionId: string,
|
|
6
|
+
create(agentAddress: string, tenantId: string, sessionId: string, runId: string): void;
|
|
7
7
|
dispatch(agentAddress: string, event: InferenceEvent): void;
|
|
8
8
|
abandon(agentAddress: string): void;
|
|
9
9
|
has(agentAddress: string): boolean;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// Registry of active event collectors, keyed by
|
|
1
|
+
// Registry of active event collectors, keyed by run address.
|
|
2
2
|
//
|
|
3
3
|
// The hub creates a collector when an instance starts and removes it when the
|
|
4
4
|
// instance ends or the sidecar disconnects. The hub session orchestrator's
|
|
5
|
-
// `agent.event` listener looks up the collector by
|
|
5
|
+
// `agent.event` listener looks up the collector by run address and
|
|
6
6
|
// dispatches the event.
|
|
7
7
|
import { getLogger } from "@intx/log";
|
|
8
8
|
import { createEventCollector, } from "./event-collector.js";
|
|
@@ -33,7 +33,7 @@ export function createEventCollectorRegistry(config) {
|
|
|
33
33
|
const { db, onTurnFinalized } = config;
|
|
34
34
|
const collectors = new Map();
|
|
35
35
|
const statuses = new Map();
|
|
36
|
-
function create(agentAddress, tenantId, sessionId,
|
|
36
|
+
function create(agentAddress, tenantId, sessionId, runId) {
|
|
37
37
|
if (collectors.has(agentAddress)) {
|
|
38
38
|
log.warn `Collector already exists for ${agentAddress}, replacing`;
|
|
39
39
|
abandon(agentAddress);
|
|
@@ -41,7 +41,7 @@ export function createEventCollectorRegistry(config) {
|
|
|
41
41
|
const collector = createEventCollector({
|
|
42
42
|
db,
|
|
43
43
|
sessionId,
|
|
44
|
-
|
|
44
|
+
runId,
|
|
45
45
|
tenantId,
|
|
46
46
|
...(onTurnFinalized
|
|
47
47
|
? {
|
package/dist/event-collector.js
CHANGED
|
@@ -10,7 +10,7 @@ import { parseTurnPartType } from "@intx/db";
|
|
|
10
10
|
import { generateId } from "@intx/hub-common";
|
|
11
11
|
const log = getLogger(["hub", "event-collector"]);
|
|
12
12
|
export function createEventCollector(config) {
|
|
13
|
-
const { db, sessionId,
|
|
13
|
+
const { db, sessionId, runId, tenantId, onTurnFinalized } = config;
|
|
14
14
|
// Current inference turn being accumulated. A new turn is created on each
|
|
15
15
|
// inference.start. Finalized on connector.reply, reactor.done,
|
|
16
16
|
// reactor.error (fatal), or abandon. Null when no turn is active.
|
|
@@ -173,7 +173,7 @@ export function createEventCollector(config) {
|
|
|
173
173
|
await db.insert(inferenceTurn).values({
|
|
174
174
|
id: currentTurnId,
|
|
175
175
|
sessionId,
|
|
176
|
-
|
|
176
|
+
runId,
|
|
177
177
|
tenantId,
|
|
178
178
|
model,
|
|
179
179
|
status: "running",
|
|
@@ -208,6 +208,14 @@ export function createEventCollector(config) {
|
|
|
208
208
|
// declines differently from regular assistant output.
|
|
209
209
|
await insertPart("refusal", block.reason, null);
|
|
210
210
|
break;
|
|
211
|
+
case "safety_rating":
|
|
212
|
+
// Structured safety signals (e.g. Gemini
|
|
213
|
+
// promptFeedback.blockReason). Persist the reason under a
|
|
214
|
+
// dedicated part kind so a blocked turn is not empty in
|
|
215
|
+
// the audit trail. Content is the provider-native reason
|
|
216
|
+
// string (observed: PROHIBITED_CONTENT).
|
|
217
|
+
await insertPart("safety_rating", block.blockReason, null);
|
|
218
|
+
break;
|
|
211
219
|
case "tool_call":
|
|
212
220
|
callNames.set(block.id, block.name);
|
|
213
221
|
callArgs.set(block.id, block.arguments);
|