@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/dist/skill-kind.js
CHANGED
|
@@ -281,6 +281,10 @@ export const skillAuthorize = (principal, repoId, ref, action) => {
|
|
|
281
281
|
reason: `authz verdict denied for ${expectedResource} ${expectedGrantVerb}`,
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
|
+
// Fail closed on any kind not handled above. The tenant-level
|
|
285
|
+
// `workflow` principal kind (`@intx/types` principalKinds) is a
|
|
286
|
+
// grant owner, not a skill bearer, and never carries a skill repo
|
|
287
|
+
// push here -- so it is intentionally left denied.
|
|
284
288
|
return {
|
|
285
289
|
allowed: false,
|
|
286
290
|
reason: `unknown principal kind: ${principal.kind}`,
|
package/dist/substrate.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { DEFAULT_CONSUMED_RETENTION_MS, dequeueToProcessing, enqueueInbox, markConsumed, readOwnedMessageIds, readProcessingEntry, replayProcessingToInbox, WORKFLOW_RUN_AGENT_STATE_PREFIX, } from "./workflow-run-kind.js";
|
|
2
|
-
export type { WorkflowRunSupervisorPrincipal, WorkflowRunWorkflowProcessPrincipal, DequeueToProcessingResult, EnqueueInboxArgs, EnqueueInboxResult, MarkConsumedArgs, MarkConsumedResult, ReplayProcessingToInboxOpts, ReplayProcessingToInboxResult, } from "./workflow-run-kind.js";
|
|
1
|
+
export { DEFAULT_CONSUMED_RETENTION_MS, dequeueToProcessing, enqueueInbox, markConsumed, parseEventSeq, readOwnedMessageIds, readCommittedWorkflowRunLifecycle, readWorkflowRunLifecycle, readProcessingEntry, replayProcessingToInbox, requireEventSeq, StaleInboxEnqueueError, WORKFLOW_RUN_AGENT_STATE_PREFIX, WORKFLOW_RUN_EVENTS_DIR, WORKFLOW_RUN_RUNS_PREFIX, } from "./workflow-run-kind.js";
|
|
2
|
+
export type { WorkflowRunSupervisorPrincipal, WorkflowRunWorkflowProcessPrincipal, DequeueToProcessingResult, EnqueueAlreadyPresentReason, EnqueueInboxArgs, EnqueueInboxOutcome, EnqueueInboxResult, MarkConsumedArgs, MarkConsumedResult, WorkflowRunLifecycle, ReplayProcessingToInboxOpts, ReplayProcessingToInboxResult, } from "./workflow-run-kind.js";
|
|
3
3
|
export { encodeCombinedEventLog, splitCombinedEventLog, WORKFLOW_RUN_EVENTS_FILE, } from "./workflow-run-event-log.js";
|
|
4
4
|
export { workflowDefinitionEnvelopeSchema } from "./workflow-kind.js";
|
|
5
5
|
export { subscribeKind } from "./repo-store/subscribe-kind.js";
|
|
6
6
|
export type { SubscribeKindEntry } from "./repo-store/subscribe-kind.js";
|
|
7
7
|
export { createAgentRepoStore } from "./agent-repo.js";
|
|
8
|
-
export type { Principal, RepoId, RepoStore, WriteResult, InitRepoOpts, NewlyTerminalRun, } from "./repo-store/types.js";
|
|
8
|
+
export type { CommittedReads, CommittedTreeEntry, Principal, RepoId, RepoStore, WriteResult, InitRepoOpts, NewlyTerminalRun, } from "./repo-store/types.js";
|
package/dist/substrate.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// runs nowhere near a database -- can import it without dragging `@intx/db`
|
|
15
15
|
// into its boot graph. Every symbol re-exported here is defined in a db-free
|
|
16
16
|
// module. The barrel still re-exports all of these for hub-side consumers.
|
|
17
|
-
export { DEFAULT_CONSUMED_RETENTION_MS, dequeueToProcessing, enqueueInbox, markConsumed, readOwnedMessageIds, readProcessingEntry, replayProcessingToInbox, WORKFLOW_RUN_AGENT_STATE_PREFIX, } from "./workflow-run-kind.js";
|
|
17
|
+
export { DEFAULT_CONSUMED_RETENTION_MS, dequeueToProcessing, enqueueInbox, markConsumed, parseEventSeq, readOwnedMessageIds, readCommittedWorkflowRunLifecycle, readWorkflowRunLifecycle, readProcessingEntry, replayProcessingToInbox, requireEventSeq, StaleInboxEnqueueError, WORKFLOW_RUN_AGENT_STATE_PREFIX, WORKFLOW_RUN_EVENTS_DIR, WORKFLOW_RUN_RUNS_PREFIX, } from "./workflow-run-kind.js";
|
|
18
18
|
export { encodeCombinedEventLog, splitCombinedEventLog, WORKFLOW_RUN_EVENTS_FILE, } from "./workflow-run-event-log.js";
|
|
19
19
|
export { workflowDefinitionEnvelopeSchema } from "./workflow-kind.js";
|
|
20
20
|
export { subscribeKind } from "./repo-store/subscribe-kind.js";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type DB, type SidecarAllocation } from "@intx/db";
|
|
2
|
+
import { type CredentialCipher, type SidecarPlacementRequirement } from "@intx/types";
|
|
3
|
+
import type { ToolPackagePin } from "@intx/types/tool-packages";
|
|
4
|
+
import type { WorkflowDefinitionSource } from "@intx/types/workflow-sources";
|
|
5
|
+
import type { DeployContent } from "./agent-repo.js";
|
|
6
|
+
import { type SidecarPluginRegistry } from "./sidecar-allocation/index.js";
|
|
7
|
+
import { type DeployWorkflowDefinitionResult, type PreparedWorkflowDeployer } from "./session-service.js";
|
|
8
|
+
import type { SidecarAllocationRouter } from "./ws/sidecar-handler.js";
|
|
9
|
+
export declare class ExclusiveWorkflowPlacementError extends Error {
|
|
10
|
+
readonly code: string;
|
|
11
|
+
constructor(code: string, message: string);
|
|
12
|
+
}
|
|
13
|
+
export type PrepareExclusiveWorkflowDeploymentArgs = {
|
|
14
|
+
readonly tenantId: string;
|
|
15
|
+
readonly anchorRunId: string;
|
|
16
|
+
readonly deploymentDomain: string;
|
|
17
|
+
/** Where the definition's bytes come from at probe time. */
|
|
18
|
+
readonly source: WorkflowDefinitionSource;
|
|
19
|
+
/** The `interchange.workflow` entry-module path the sidecar evaluates. */
|
|
20
|
+
readonly entry: string;
|
|
21
|
+
/**
|
|
22
|
+
* A `name@range` spec for the definition package. REQUIRED for the `registry`
|
|
23
|
+
* and asset-`tarball` variants; omitted for the asset-`source` variant.
|
|
24
|
+
*/
|
|
25
|
+
readonly pin?: string;
|
|
26
|
+
readonly definitionAssetId: string;
|
|
27
|
+
readonly placement: SidecarPlacementRequirement & {
|
|
28
|
+
readonly sharing: "exclusive";
|
|
29
|
+
};
|
|
30
|
+
readonly sessionId: string;
|
|
31
|
+
readonly sourceAuthorityPrincipalId: string;
|
|
32
|
+
readonly sourceOfferingIds: readonly string[];
|
|
33
|
+
readonly defaultSourceOfferingId: string;
|
|
34
|
+
readonly deployContent: DeployContent;
|
|
35
|
+
readonly toolPackagePins?: readonly ToolPackagePin[];
|
|
36
|
+
};
|
|
37
|
+
export type PreparedExclusiveWorkflowDeployment = {
|
|
38
|
+
readonly anchorRunId: string;
|
|
39
|
+
readonly deploymentAddress: string;
|
|
40
|
+
readonly allocationId: string;
|
|
41
|
+
readonly status: "pending";
|
|
42
|
+
};
|
|
43
|
+
export type WorkflowAllocationService = {
|
|
44
|
+
prepareExclusiveDeployment(args: PrepareExclusiveWorkflowDeploymentArgs): Promise<PreparedExclusiveWorkflowDeployment>;
|
|
45
|
+
deployReadyAllocation(allocation: SidecarAllocation): Promise<DeployWorkflowDefinitionResult | null>;
|
|
46
|
+
};
|
|
47
|
+
export type WorkflowAllocationServiceDeps = {
|
|
48
|
+
readonly db: DB["db"];
|
|
49
|
+
readonly plugins: SidecarPluginRegistry;
|
|
50
|
+
readonly preparedDeployer: PreparedWorkflowDeployer;
|
|
51
|
+
readonly credentialCipher?: CredentialCipher;
|
|
52
|
+
readonly allocationRouter: Pick<SidecarAllocationRouter, "isAllocatedWorkflowActive">;
|
|
53
|
+
readonly createAllocationId?: () => string;
|
|
54
|
+
readonly now?: () => Date;
|
|
55
|
+
};
|
|
56
|
+
/** Resolve the tenant-inherited sidecar placement into one launch decision. */
|
|
57
|
+
export declare function resolveWorkflowSidecarPlacement(db: DB["db"], tenantId: string): Promise<SidecarPlacementRequirement | null>;
|
|
58
|
+
export declare function createWorkflowAllocationService({ db, plugins, preparedDeployer, credentialCipher, allocationRouter, createAllocationId, now, }: WorkflowAllocationServiceDeps): WorkflowAllocationService;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { createSidecarAllocationStore, createWorkflowRunLaunchSpecStore, getAncestorChain, resolveSourcesByOfferingIds, } from "@intx/db";
|
|
2
|
+
import { grant, tenant as tenantTable, workflowRun } from "@intx/db/schema";
|
|
3
|
+
import { eq, inArray } from "drizzle-orm";
|
|
4
|
+
import { generateId } from "@intx/hub-common";
|
|
5
|
+
import { hexEncode, TenantConfig, } from "@intx/types";
|
|
6
|
+
import { deriveRunAddress, deriveRunAgentId } from "@intx/workflow-deploy";
|
|
7
|
+
import { resolveEffectiveSidecarPlacement, } from "./sidecar-allocation/index.js";
|
|
8
|
+
import { SessionLaunchError, } from "./session-service.js";
|
|
9
|
+
export class ExclusiveWorkflowPlacementError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ExclusiveWorkflowPlacementError";
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function randomAllocationId() {
|
|
18
|
+
return `sal_${hexEncode(crypto.getRandomValues(new Uint8Array(16)))}`;
|
|
19
|
+
}
|
|
20
|
+
/** Resolve the tenant-inherited sidecar placement into one launch decision. */
|
|
21
|
+
export async function resolveWorkflowSidecarPlacement(db, tenantId) {
|
|
22
|
+
const tenantIds = await getAncestorChain(db, tenantId);
|
|
23
|
+
const rows = await db.query.tenant.findMany({
|
|
24
|
+
where: inArray(tenantTable.id, tenantIds),
|
|
25
|
+
columns: { id: true, config: true },
|
|
26
|
+
});
|
|
27
|
+
const configsByTenantId = new Map(rows.map((row) => [
|
|
28
|
+
row.id,
|
|
29
|
+
row.config === null
|
|
30
|
+
? TenantConfig.assert({})
|
|
31
|
+
: TenantConfig.assert(row.config),
|
|
32
|
+
]));
|
|
33
|
+
const tenantConfigs = tenantIds.map((id) => {
|
|
34
|
+
const config = configsByTenantId.get(id);
|
|
35
|
+
if (config === undefined) {
|
|
36
|
+
throw new Error(`Tenant ${id} disappeared while resolving sidecar placement`);
|
|
37
|
+
}
|
|
38
|
+
return config;
|
|
39
|
+
});
|
|
40
|
+
return resolveEffectiveSidecarPlacement({ tenantConfigs });
|
|
41
|
+
}
|
|
42
|
+
export function createWorkflowAllocationService({ db, plugins, preparedDeployer, credentialCipher, allocationRouter, createAllocationId = randomAllocationId, now = () => new Date(), }) {
|
|
43
|
+
const allocationStore = createSidecarAllocationStore(db);
|
|
44
|
+
const launchSpecStore = createWorkflowRunLaunchSpecStore(db);
|
|
45
|
+
async function prepareExclusiveDeployment(args) {
|
|
46
|
+
// Fail closed before any probe: exclusive placement has no meaning without a
|
|
47
|
+
// provisioner to stand up its dedicated sidecar, and the shared-capacity
|
|
48
|
+
// probe below is wasted work if no provisioner exists. (In-tree this is
|
|
49
|
+
// always null -- exclusive is dormant -- so a live prepare never runs here.)
|
|
50
|
+
const provisioner = plugins.getDefaultProvisioner();
|
|
51
|
+
if (provisioner === null) {
|
|
52
|
+
throw new ExclusiveWorkflowPlacementError("exclusive_provisioner_unavailable", "No default sidecar provisioner is configured for exclusive workflows");
|
|
53
|
+
}
|
|
54
|
+
if (args.sourceOfferingIds.length === 0) {
|
|
55
|
+
throw new ExclusiveWorkflowPlacementError("invalid_source_offerings", "Exclusive workflow deployment requires at least one catalog offering");
|
|
56
|
+
}
|
|
57
|
+
if (!args.sourceOfferingIds.includes(args.defaultSourceOfferingId)) {
|
|
58
|
+
throw new ExclusiveWorkflowPlacementError("invalid_source_offerings", "The default source offering must be present in the source offering chain");
|
|
59
|
+
}
|
|
60
|
+
const sourceCheck = await resolveSourcesByOfferingIds(db, args.tenantId, args.sourceOfferingIds, credentialCipher);
|
|
61
|
+
if (!sourceCheck.ok) {
|
|
62
|
+
throw new ExclusiveWorkflowPlacementError("source_offering_unavailable", `Catalog offering ${sourceCheck.offeringId} cannot be used by the deployment authority`);
|
|
63
|
+
}
|
|
64
|
+
// Probe + gate + freeze the code-sourced definition ONCE, on shared
|
|
65
|
+
// capacity, at request time. The freeze is sidecar-agnostic (a wire hash
|
|
66
|
+
// over the inert projection plus the resolved closure), so the frozen bundle
|
|
67
|
+
// deploys later to the dedicated allocation with no re-probe. A non-approval
|
|
68
|
+
// surfaces as a `WorkflowDefinitionInvalidError` from the deployer, which the
|
|
69
|
+
// route maps to a 409.
|
|
70
|
+
const approved = await preparedDeployer.installAndApproveWorkflowSource({
|
|
71
|
+
source: args.source,
|
|
72
|
+
entry: args.entry,
|
|
73
|
+
...(args.pin !== undefined ? { pin: args.pin } : {}),
|
|
74
|
+
definitionAssetId: args.definitionAssetId,
|
|
75
|
+
});
|
|
76
|
+
if (!approved.approval.ok) {
|
|
77
|
+
// `installAndApproveWorkflowSource` already fails closed on a non-approval;
|
|
78
|
+
// restate the narrowing so the frozen bundle below reads the ok arm.
|
|
79
|
+
throw new Error("prepareExclusiveDeployment: install did not yield an approved definition");
|
|
80
|
+
}
|
|
81
|
+
// Capture the narrowed values before the transaction: TS drops the
|
|
82
|
+
// `approval.ok` narrowing inside the async callback below.
|
|
83
|
+
const definitionId = approved.approval.definitionId;
|
|
84
|
+
const frozenApprovalBundle = {
|
|
85
|
+
source: args.source,
|
|
86
|
+
entry: args.entry,
|
|
87
|
+
projection: approved.projection,
|
|
88
|
+
closure: approved.closure,
|
|
89
|
+
approvedWireHash: approved.approval.approvedWireHash,
|
|
90
|
+
approvedGrants: [...approved.approval.approvedGrants],
|
|
91
|
+
};
|
|
92
|
+
const allocationId = createAllocationId();
|
|
93
|
+
const createdAt = now();
|
|
94
|
+
const deploymentAddress = deriveRunAddress({
|
|
95
|
+
runId: args.anchorRunId,
|
|
96
|
+
domain: args.deploymentDomain,
|
|
97
|
+
});
|
|
98
|
+
await db.transaction(async (tx) => {
|
|
99
|
+
// The freeze already ensured (create-if-absent) the definition row keyed by
|
|
100
|
+
// the approved wire hash and returned its id; anchor to THAT row rather
|
|
101
|
+
// than re-ensuring, so the anchor's definition is exactly the one approved.
|
|
102
|
+
await tx.insert(workflowRun).values({
|
|
103
|
+
id: args.anchorRunId,
|
|
104
|
+
tenantId: args.tenantId,
|
|
105
|
+
anchorRunId: args.anchorRunId,
|
|
106
|
+
definitionId,
|
|
107
|
+
address: deploymentAddress,
|
|
108
|
+
// Born "deployed": the anchor is live but pre-trigger. The first trigger
|
|
109
|
+
// flips it to "running" (see `anchorWithPrincipal`).
|
|
110
|
+
status: "deployed",
|
|
111
|
+
createdAt,
|
|
112
|
+
});
|
|
113
|
+
await tx.insert(grant).values({
|
|
114
|
+
id: generateId("grant"),
|
|
115
|
+
tenantId: args.tenantId,
|
|
116
|
+
principalId: args.sourceAuthorityPrincipalId,
|
|
117
|
+
resource: `workflow-run:${args.anchorRunId}`,
|
|
118
|
+
action: "read",
|
|
119
|
+
effect: "allow",
|
|
120
|
+
origin: "creator",
|
|
121
|
+
createdAt,
|
|
122
|
+
updatedAt: createdAt,
|
|
123
|
+
});
|
|
124
|
+
await launchSpecStore.create({
|
|
125
|
+
anchorRunId: args.anchorRunId,
|
|
126
|
+
sessionId: args.sessionId,
|
|
127
|
+
deploymentDomain: args.deploymentDomain,
|
|
128
|
+
sourceAuthorityPrincipalId: args.sourceAuthorityPrincipalId,
|
|
129
|
+
frozenApprovalBundle,
|
|
130
|
+
sourceOfferingIds: [...args.sourceOfferingIds],
|
|
131
|
+
defaultSourceOfferingId: args.defaultSourceOfferingId,
|
|
132
|
+
deployContent: args.deployContent,
|
|
133
|
+
...(args.toolPackagePins !== undefined
|
|
134
|
+
? { toolPackagePins: [...args.toolPackagePins] }
|
|
135
|
+
: {}),
|
|
136
|
+
createdAt,
|
|
137
|
+
}, tx);
|
|
138
|
+
await allocationStore.createPending({
|
|
139
|
+
id: allocationId,
|
|
140
|
+
anchorRunId: args.anchorRunId,
|
|
141
|
+
tenantId: args.tenantId,
|
|
142
|
+
provisionerId: provisioner.id,
|
|
143
|
+
provisionerApiVersion: provisioner.apiVersion,
|
|
144
|
+
provisionerBindingFingerprint: provisioner.bindingFingerprint,
|
|
145
|
+
placement: args.placement,
|
|
146
|
+
now: createdAt,
|
|
147
|
+
}, tx);
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
anchorRunId: args.anchorRunId,
|
|
151
|
+
deploymentAddress,
|
|
152
|
+
allocationId,
|
|
153
|
+
status: "pending",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async function deployReadyAllocation(allocation) {
|
|
157
|
+
if (allocation.status !== "allocated" ||
|
|
158
|
+
allocation.ensureAcceptedGeneration !== allocation.generation) {
|
|
159
|
+
throw new Error(`Allocation ${allocation.id} generation ${String(allocation.generation)} is not accepted for deployment`);
|
|
160
|
+
}
|
|
161
|
+
const allocationTarget = {
|
|
162
|
+
allocationId: allocation.id,
|
|
163
|
+
generation: allocation.generation,
|
|
164
|
+
};
|
|
165
|
+
const anchor = await db.query.workflowRun.findFirst({
|
|
166
|
+
where: eq(workflowRun.id, allocation.anchorRunId),
|
|
167
|
+
columns: { publicKey: true, definitionId: true },
|
|
168
|
+
});
|
|
169
|
+
if (anchor === undefined) {
|
|
170
|
+
throw new Error(`Allocation ${allocation.id} has no workflow anchor run`);
|
|
171
|
+
}
|
|
172
|
+
if (await allocationRouter.isAllocatedWorkflowActive(allocationTarget)) {
|
|
173
|
+
if (anchor.publicKey !== null)
|
|
174
|
+
return null;
|
|
175
|
+
throw new SessionLaunchError("provision", new Error(`Allocation ${allocation.id} has an active workflow without a completed initialization key`), true);
|
|
176
|
+
}
|
|
177
|
+
if (anchor.definitionId === null) {
|
|
178
|
+
throw new Error(`Allocation ${allocation.id} anchor run has no frozen workflow definition`);
|
|
179
|
+
}
|
|
180
|
+
const spec = await launchSpecStore.get(allocation.anchorRunId);
|
|
181
|
+
if (spec === null) {
|
|
182
|
+
throw new Error(`Allocation ${allocation.id} has no workflow launch specification`);
|
|
183
|
+
}
|
|
184
|
+
// The frozen bundle deploys verbatim -- no re-probe. Rehydrate the approval
|
|
185
|
+
// hand-off from it: the approved grant set becomes a `Set`, and the frozen
|
|
186
|
+
// definition id is the anchor's own (set at prepare time from this freeze).
|
|
187
|
+
const bundle = spec.frozenApprovalBundle;
|
|
188
|
+
const approved = {
|
|
189
|
+
approval: {
|
|
190
|
+
ok: true,
|
|
191
|
+
definitionId: anchor.definitionId,
|
|
192
|
+
approvedWireHash: bundle.approvedWireHash,
|
|
193
|
+
approvedGrants: new Set(bundle.approvedGrants),
|
|
194
|
+
projection: bundle.projection,
|
|
195
|
+
},
|
|
196
|
+
projection: bundle.projection,
|
|
197
|
+
closure: bundle.closure,
|
|
198
|
+
};
|
|
199
|
+
// Re-resolve the inference chain from the catalog at launch time -- the
|
|
200
|
+
// launch spec stores offering ids, never resolved sources, so a rotated
|
|
201
|
+
// credential is picked up here and no secret was ever persisted.
|
|
202
|
+
const resolved = await resolveSourcesByOfferingIds(db, allocation.tenantId, spec.sourceOfferingIds, credentialCipher);
|
|
203
|
+
if (!resolved.ok) {
|
|
204
|
+
throw new Error(`Catalog offering ${resolved.offeringId} is unavailable while recovering allocation ${allocation.id}`);
|
|
205
|
+
}
|
|
206
|
+
const defaultSource = resolved.sources.find((source) => source.id === spec.defaultSourceOfferingId);
|
|
207
|
+
if (defaultSource === undefined) {
|
|
208
|
+
throw new Error(`Default offering ${spec.defaultSourceOfferingId} was not resolved for allocation ${allocation.id}`);
|
|
209
|
+
}
|
|
210
|
+
const deploymentAddress = deriveRunAddress({
|
|
211
|
+
runId: allocation.anchorRunId,
|
|
212
|
+
domain: spec.deploymentDomain,
|
|
213
|
+
});
|
|
214
|
+
const config = {
|
|
215
|
+
sessionId: spec.sessionId,
|
|
216
|
+
agentId: deriveRunAgentId({ runId: allocation.anchorRunId }),
|
|
217
|
+
tenantId: allocation.tenantId,
|
|
218
|
+
principalId: spec.sourceAuthorityPrincipalId,
|
|
219
|
+
agentAddress: deploymentAddress,
|
|
220
|
+
systemPrompt: "",
|
|
221
|
+
tools: [],
|
|
222
|
+
grants: [],
|
|
223
|
+
sources: resolved.sources,
|
|
224
|
+
defaultSource: defaultSource.id,
|
|
225
|
+
};
|
|
226
|
+
return preparedDeployer.deployPreparedCodeSourcedWorkflow({
|
|
227
|
+
tenantId: allocation.tenantId,
|
|
228
|
+
anchorRunId: allocation.anchorRunId,
|
|
229
|
+
deploymentDomain: spec.deploymentDomain,
|
|
230
|
+
agentAddress: deploymentAddress,
|
|
231
|
+
source: bundle.source,
|
|
232
|
+
approved,
|
|
233
|
+
config,
|
|
234
|
+
allocationTarget,
|
|
235
|
+
...(credentialCipher !== undefined ? { credentialCipher } : {}),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return { prepareExclusiveDeployment, deployReadyAllocation };
|
|
239
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type PackumentFetcher, type RegistryConfig } from "@intx/tool-packaging";
|
|
2
|
+
import type { WorkflowSourceAssetMount } from "@intx/types/sidecar";
|
|
3
|
+
import type { ToolPackageManifest } from "@intx/types/tool-packages";
|
|
4
|
+
import type { WorkflowDefinitionAssetSource, WorkflowDefinitionRegistrySource } from "@intx/types/workflow-sources";
|
|
5
|
+
import { type SourceTreeReads } from "./workflow-source-closure.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a workflow definition's dependency closure against the npm registry
|
|
8
|
+
* the source names.
|
|
9
|
+
*/
|
|
10
|
+
export type ResolveWorkflowClosureRegistryArgs = {
|
|
11
|
+
readonly source: WorkflowDefinitionRegistrySource;
|
|
12
|
+
/** A `name@range` spec for the workflow definition package. */
|
|
13
|
+
readonly pin: string;
|
|
14
|
+
/** URL and credentials for the registry named by `source.registry`. */
|
|
15
|
+
readonly registryConfig: RegistryConfig;
|
|
16
|
+
/**
|
|
17
|
+
* Test seam for packument fetches. Omitted in production, where the
|
|
18
|
+
* default `npm-registry-fetch` wrapper reaches the configured URL.
|
|
19
|
+
*/
|
|
20
|
+
readonly fetchPackument?: PackumentFetcher;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a workflow definition's dependency closure against a single hub
|
|
24
|
+
* `package-registry` asset.
|
|
25
|
+
*
|
|
26
|
+
* The resolver is single-source by design: the asset is the sole and default
|
|
27
|
+
* registry, so a self-contained definition (its `@intx/*` imports bundled at
|
|
28
|
+
* author time) resolves to one `kind:"asset"` entry. A specifier the asset does
|
|
29
|
+
* not publish fails loud from `AssetRegistrySource` -- there is no silent
|
|
30
|
+
* fallback to npm. A definition that genuinely needs external npm dependencies
|
|
31
|
+
* would resolve through the mixed `AssetRegistrySource` + `HttpRegistrySource`
|
|
32
|
+
* map `session-service` already builds for tool packages; that path is not
|
|
33
|
+
* threaded here.
|
|
34
|
+
*/
|
|
35
|
+
export type ResolveWorkflowClosureAssetTarballArgs = {
|
|
36
|
+
/** An asset source whose `package.format` is `"tarball"`. */
|
|
37
|
+
readonly source: WorkflowDefinitionAssetSource;
|
|
38
|
+
/** A `name@range` spec for the workflow definition package. */
|
|
39
|
+
readonly pin: string;
|
|
40
|
+
/** Reads a blob at `path` from the asset the definition is sourced from. */
|
|
41
|
+
readonly readBlob: (path: string) => Promise<Uint8Array>;
|
|
42
|
+
/** Lists the blob names directly under `dir` in that asset. */
|
|
43
|
+
readonly listBlobs: (dir: string) => Promise<string[]>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a source-format workflow definition's dependency closure from the
|
|
47
|
+
* asset's git tree at the pinned commit. The workspace-local members become
|
|
48
|
+
* `format:"source"` entries and the external npm deps resolve through the
|
|
49
|
+
* npm registry the caller supplies; there is no `name@range` pin (the member
|
|
50
|
+
* is selected from `source.package.packageName`).
|
|
51
|
+
*/
|
|
52
|
+
export type ResolveWorkflowClosureAssetSourceArgs = {
|
|
53
|
+
/** An asset source whose `package.format` is `"source"`. */
|
|
54
|
+
readonly source: WorkflowDefinitionAssetSource;
|
|
55
|
+
/** Git-tree reads pinned to `source.package.commitSha`. */
|
|
56
|
+
readonly reads: SourceTreeReads;
|
|
57
|
+
/**
|
|
58
|
+
* The registry name external deps are stamped with in the frozen closure.
|
|
59
|
+
* Must be a name the sidecar's registry map is keyed by (its npm registry),
|
|
60
|
+
* since the sidecar resolves each external entry's `source.registry` against
|
|
61
|
+
* that map at materialization.
|
|
62
|
+
*/
|
|
63
|
+
readonly registryName: string;
|
|
64
|
+
/** URL and credentials for the npm registry external deps resolve against. */
|
|
65
|
+
readonly registryConfig: RegistryConfig;
|
|
66
|
+
/**
|
|
67
|
+
* Test seam for packument fetches. Omitted in production, where the
|
|
68
|
+
* default `npm-registry-fetch` wrapper reaches the configured URL.
|
|
69
|
+
*/
|
|
70
|
+
readonly fetchPackument?: PackumentFetcher;
|
|
71
|
+
};
|
|
72
|
+
export type ResolveWorkflowClosureArgs = ResolveWorkflowClosureRegistryArgs | ResolveWorkflowClosureAssetTarballArgs | ResolveWorkflowClosureAssetSourceArgs;
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a workflow definition's dependency closure to concrete versions
|
|
75
|
+
* and integrity SRIs, reusing the tool-packaging closure resolver.
|
|
76
|
+
*
|
|
77
|
+
* @returns the frozen closure: a `ToolPackageManifest` whose `topLevel`
|
|
78
|
+
* pins the definition package and whose `entries` carry the concrete
|
|
79
|
+
* version and integrity SRI of every transitive dependency.
|
|
80
|
+
*/
|
|
81
|
+
export declare function resolveWorkflowClosure(args: ResolveWorkflowClosureArgs): Promise<ToolPackageManifest>;
|
|
82
|
+
/**
|
|
83
|
+
* The workspace-relative mount path a workflow definition's source asset is
|
|
84
|
+
* delivered under, keyed by asset id. Both the probe (inline delivery) and the
|
|
85
|
+
* deploy (streamed fan-out) build their `assetId -> mountPath` maps through
|
|
86
|
+
* this one helper, so the same asset lands at the same path on both paths.
|
|
87
|
+
*/
|
|
88
|
+
export declare function workflowSourceAssetMountPath(assetId: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* Resolves an asset (by id) to the git pack a source-ref frame delivers for it.
|
|
91
|
+
* The caller binds the ref: an asset source binds the asset's default ref, so
|
|
92
|
+
* the install/deploy glue stays ref-agnostic.
|
|
93
|
+
*/
|
|
94
|
+
export type ResolveAssetAttachmentFn = (assetId: string) => Promise<{
|
|
95
|
+
pack: Uint8Array;
|
|
96
|
+
ref: string;
|
|
97
|
+
commitSha: string;
|
|
98
|
+
}>;
|
|
99
|
+
/**
|
|
100
|
+
* Build the inline asset mounts a source-ref frame delivers for an asset-sourced
|
|
101
|
+
* closure: one per distinct asset the closure's `kind:"asset"` entries name (so
|
|
102
|
+
* a multi-asset closure delivers every backing asset, not just the source), each
|
|
103
|
+
* carrying the git pack the caller resolves for that asset. `mountPath` is
|
|
104
|
+
* derived through the shared helper so probe and deploy agree on it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function buildSourceAssetMounts(closure: ToolPackageManifest, resolveAttachment: ResolveAssetAttachmentFn): Promise<WorkflowSourceAssetMount[]>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Hub-side dependency-closure resolution for a code-sourced workflow
|
|
2
|
+
// definition.
|
|
3
|
+
//
|
|
4
|
+
// Given a `WorkflowDefinitionSource` and a pin, walk the full dependency
|
|
5
|
+
// closure to concrete versions plus integrity SRIs, reusing the tool-packaging
|
|
6
|
+
// closure resolver. This is the "what exactly would we install" step: packument
|
|
7
|
+
// and package.json metadata reads only, no code execution. The returned
|
|
8
|
+
// `ToolPackageManifest` is the frozen closure the deploy path later ships by
|
|
9
|
+
// source-ref.
|
|
10
|
+
//
|
|
11
|
+
// There are three arms, one per `(source.kind, package.format)`:
|
|
12
|
+
// - `registry` names an npm registry; its URL and credentials are supplied by
|
|
13
|
+
// the caller as a `RegistryConfig`, since the caller owns the registry
|
|
14
|
+
// configuration.
|
|
15
|
+
// - `asset` + `tarball` names a hub asset that publishes the definition
|
|
16
|
+
// package as a tarball; the caller supplies bound `readBlob`/`listBlobs`
|
|
17
|
+
// closures over that asset (the same shape `session-service` mints for
|
|
18
|
+
// `AssetRegistrySource`).
|
|
19
|
+
// - `asset` + `source` names a hub asset whose definition package lives as a
|
|
20
|
+
// git subtree at a pinned commit; the caller supplies a `SourceTreeReads`
|
|
21
|
+
// pinned to that commit plus a `RegistryConfig` for the external npm deps.
|
|
22
|
+
// This arm delegates to `resolveSourceWorkflowClosure`, which reads the
|
|
23
|
+
// tree directly and needs no `name@range` pin (it selects the member from
|
|
24
|
+
// the source's `packageName`).
|
|
25
|
+
// Every arm takes pre-bound read capabilities, keeping this module free of any
|
|
26
|
+
// asset-service or repo-store dependency.
|
|
27
|
+
import { base64Encode } from "@intx/types";
|
|
28
|
+
import { AssetRegistrySource, HttpRegistrySource, createClosureResolver, parsePin, } from "@intx/tool-packaging";
|
|
29
|
+
import { resolveSourceWorkflowClosure, } from "./workflow-source-closure.js";
|
|
30
|
+
// Both asset arms carry an identical `source` field type, so narrow on the
|
|
31
|
+
// source's own `package.format` discriminant rather than adding a redundant
|
|
32
|
+
// discriminant to the args.
|
|
33
|
+
function isAssetSourceArgs(args) {
|
|
34
|
+
return (args.source.kind === "asset" && args.source.package.format === "source");
|
|
35
|
+
}
|
|
36
|
+
function isAssetTarballArgs(args) {
|
|
37
|
+
return (args.source.kind === "asset" && args.source.package.format === "tarball");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a workflow definition's dependency closure to concrete versions
|
|
41
|
+
* and integrity SRIs, reusing the tool-packaging closure resolver.
|
|
42
|
+
*
|
|
43
|
+
* @returns the frozen closure: a `ToolPackageManifest` whose `topLevel`
|
|
44
|
+
* pins the definition package and whose `entries` carry the concrete
|
|
45
|
+
* version and integrity SRI of every transitive dependency.
|
|
46
|
+
*/
|
|
47
|
+
export async function resolveWorkflowClosure(args) {
|
|
48
|
+
if (isAssetSourceArgs(args)) {
|
|
49
|
+
return resolveSourceWorkflowClosure({
|
|
50
|
+
source: args.source,
|
|
51
|
+
reads: args.reads,
|
|
52
|
+
registryName: args.registryName,
|
|
53
|
+
registryConfig: args.registryConfig,
|
|
54
|
+
...(args.fetchPackument !== undefined
|
|
55
|
+
? { fetchPackument: args.fetchPackument }
|
|
56
|
+
: {}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (isAssetTarballArgs(args)) {
|
|
60
|
+
// The assetId is the source name so every walker resolution error is
|
|
61
|
+
// traceable to the exact asset; it is the sole and default source.
|
|
62
|
+
const name = args.source.assetId;
|
|
63
|
+
const assetSource = new AssetRegistrySource({
|
|
64
|
+
name,
|
|
65
|
+
assetId: args.source.assetId,
|
|
66
|
+
readBlob: args.readBlob,
|
|
67
|
+
listBlobs: args.listBlobs,
|
|
68
|
+
});
|
|
69
|
+
const resolver = createClosureResolver({
|
|
70
|
+
registries: new Map([[name, assetSource]]),
|
|
71
|
+
defaultRegistry: name,
|
|
72
|
+
});
|
|
73
|
+
return resolver.resolveClosure([parsePin(args.pin)]);
|
|
74
|
+
}
|
|
75
|
+
const registrySource = new HttpRegistrySource({
|
|
76
|
+
name: args.source.registry,
|
|
77
|
+
config: args.registryConfig,
|
|
78
|
+
...(args.fetchPackument !== undefined
|
|
79
|
+
? { fetchPackument: args.fetchPackument }
|
|
80
|
+
: {}),
|
|
81
|
+
});
|
|
82
|
+
const resolver = createClosureResolver({
|
|
83
|
+
registries: new Map([[registrySource.name, registrySource]]),
|
|
84
|
+
defaultRegistry: registrySource.name,
|
|
85
|
+
});
|
|
86
|
+
return resolver.resolveClosure([parsePin(args.pin)]);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The workspace-relative mount path a workflow definition's source asset is
|
|
90
|
+
* delivered under, keyed by asset id. Both the probe (inline delivery) and the
|
|
91
|
+
* deploy (streamed fan-out) build their `assetId -> mountPath` maps through
|
|
92
|
+
* this one helper, so the same asset lands at the same path on both paths.
|
|
93
|
+
*/
|
|
94
|
+
export function workflowSourceAssetMountPath(assetId) {
|
|
95
|
+
return `source-assets/${assetId}/`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build the inline asset mounts a source-ref frame delivers for an asset-sourced
|
|
99
|
+
* closure: one per distinct asset the closure's `kind:"asset"` entries name (so
|
|
100
|
+
* a multi-asset closure delivers every backing asset, not just the source), each
|
|
101
|
+
* carrying the git pack the caller resolves for that asset. `mountPath` is
|
|
102
|
+
* derived through the shared helper so probe and deploy agree on it.
|
|
103
|
+
*/
|
|
104
|
+
export async function buildSourceAssetMounts(closure, resolveAttachment) {
|
|
105
|
+
const assetIds = new Set();
|
|
106
|
+
for (const entry of closure.entries) {
|
|
107
|
+
if (entry.source.kind === "asset") {
|
|
108
|
+
assetIds.add(entry.source.assetId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const mounts = [];
|
|
112
|
+
for (const assetId of assetIds) {
|
|
113
|
+
const attachment = await resolveAttachment(assetId);
|
|
114
|
+
mounts.push({
|
|
115
|
+
assetId,
|
|
116
|
+
mountPath: workflowSourceAssetMountPath(assetId),
|
|
117
|
+
pack: base64Encode(attachment.pack),
|
|
118
|
+
ref: attachment.ref,
|
|
119
|
+
commitSha: attachment.commitSha,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return mounts;
|
|
123
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type DBExecutor, type WorkflowDefinitionSelector } from "@intx/db";
|
|
2
|
+
/**
|
|
3
|
+
* The first-class `workflow_definition` a selector names, created if absent. A
|
|
4
|
+
* native workflow carries its body in the asset it points at, so this only
|
|
5
|
+
* projects a definition row -- plus its version "1" -- over that asset, keyed
|
|
6
|
+
* by the selector's wire hash, so a run can anchor on a first-class definition.
|
|
7
|
+
* A single asset backs many definitions distinguished by their wire hash, so
|
|
8
|
+
* distinct hashes over one asset yield distinct definitions.
|
|
9
|
+
* `currentVersion`/`status` are left to the table defaults; the definition
|
|
10
|
+
* carries no `modelRequirements` manifest, so it deploys as a workflow rather
|
|
11
|
+
* than launching as an instance.
|
|
12
|
+
*
|
|
13
|
+
* Idempotent and concurrency-safe: the definition insert conflicts on the
|
|
14
|
+
* unique `(assetId, wireHash)` and the version on `(definitionId, version)`, so
|
|
15
|
+
* two concurrent deploys of the same selector still yield exactly one
|
|
16
|
+
* definition and one version. The returned id is read back after the conflict
|
|
17
|
+
* -- the winner's id, not a locally-minted one -- so a losing insert anchors
|
|
18
|
+
* its version on the real row. `created` distinguishes a fresh projection from
|
|
19
|
+
* a pre-existing one for callers that report fold counts.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureWorkflowDefinitionForAsset(db: DBExecutor, selector: WorkflowDefinitionSelector): Promise<{
|
|
22
|
+
definitionId: string;
|
|
23
|
+
created: boolean;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { resolveDefinitionIdForAsset, } from "@intx/db";
|
|
3
|
+
import { asset, workflowDefinition, workflowDefinitionVersion, } from "@intx/db/schema";
|
|
4
|
+
import { generateId } from "@intx/hub-common";
|
|
5
|
+
/**
|
|
6
|
+
* The first-class `workflow_definition` a selector names, created if absent. A
|
|
7
|
+
* native workflow carries its body in the asset it points at, so this only
|
|
8
|
+
* projects a definition row -- plus its version "1" -- over that asset, keyed
|
|
9
|
+
* by the selector's wire hash, so a run can anchor on a first-class definition.
|
|
10
|
+
* A single asset backs many definitions distinguished by their wire hash, so
|
|
11
|
+
* distinct hashes over one asset yield distinct definitions.
|
|
12
|
+
* `currentVersion`/`status` are left to the table defaults; the definition
|
|
13
|
+
* carries no `modelRequirements` manifest, so it deploys as a workflow rather
|
|
14
|
+
* than launching as an instance.
|
|
15
|
+
*
|
|
16
|
+
* Idempotent and concurrency-safe: the definition insert conflicts on the
|
|
17
|
+
* unique `(assetId, wireHash)` and the version on `(definitionId, version)`, so
|
|
18
|
+
* two concurrent deploys of the same selector still yield exactly one
|
|
19
|
+
* definition and one version. The returned id is read back after the conflict
|
|
20
|
+
* -- the winner's id, not a locally-minted one -- so a losing insert anchors
|
|
21
|
+
* its version on the real row. `created` distinguishes a fresh projection from
|
|
22
|
+
* a pre-existing one for callers that report fold counts.
|
|
23
|
+
*/
|
|
24
|
+
export async function ensureWorkflowDefinitionForAsset(db, selector) {
|
|
25
|
+
const { assetId, wireHash } = selector;
|
|
26
|
+
const assetRow = await db
|
|
27
|
+
.select({
|
|
28
|
+
tenantId: asset.tenantId,
|
|
29
|
+
creatorPrincipalId: asset.creatorPrincipalId,
|
|
30
|
+
name: asset.name,
|
|
31
|
+
displayName: asset.displayName,
|
|
32
|
+
})
|
|
33
|
+
.from(asset)
|
|
34
|
+
.where(eq(asset.id, assetId))
|
|
35
|
+
.limit(1)
|
|
36
|
+
.then((rows) => rows[0]);
|
|
37
|
+
if (assetRow === undefined) {
|
|
38
|
+
throw new Error(`ensureWorkflowDefinitionForAsset: no asset found for id "${assetId}"`);
|
|
39
|
+
}
|
|
40
|
+
const inserted = await db
|
|
41
|
+
.insert(workflowDefinition)
|
|
42
|
+
.values({
|
|
43
|
+
id: generateId("workflowDefinition"),
|
|
44
|
+
tenantId: assetRow.tenantId,
|
|
45
|
+
creatorPrincipalId: assetRow.creatorPrincipalId,
|
|
46
|
+
assetId,
|
|
47
|
+
wireHash,
|
|
48
|
+
name: assetRow.name,
|
|
49
|
+
description: assetRow.displayName,
|
|
50
|
+
})
|
|
51
|
+
.onConflictDoNothing({
|
|
52
|
+
target: [workflowDefinition.assetId, workflowDefinition.wireHash],
|
|
53
|
+
})
|
|
54
|
+
.returning({ id: workflowDefinition.id });
|
|
55
|
+
const created = inserted.length > 0;
|
|
56
|
+
const insertedId = inserted[0]?.id;
|
|
57
|
+
const definitionId = insertedId ?? (await resolveDefinitionIdForAsset(db, selector));
|
|
58
|
+
if (definitionId === null || definitionId === undefined) {
|
|
59
|
+
throw new Error(`ensureWorkflowDefinitionForAsset: definition for asset "${assetId}" (wire hash "${wireHash}") missing after insert`);
|
|
60
|
+
}
|
|
61
|
+
await db
|
|
62
|
+
.insert(workflowDefinitionVersion)
|
|
63
|
+
.values({
|
|
64
|
+
id: generateId("workflowDefinitionVersion"),
|
|
65
|
+
definitionId,
|
|
66
|
+
version: "1",
|
|
67
|
+
})
|
|
68
|
+
.onConflictDoNothing({
|
|
69
|
+
target: [
|
|
70
|
+
workflowDefinitionVersion.definitionId,
|
|
71
|
+
workflowDefinitionVersion.version,
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
return { definitionId, created };
|
|
75
|
+
}
|