@intx/hub-sessions 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/README.md +82 -1
- package/dist/agent-repo.d.ts +93 -0
- package/dist/agent-repo.js +109 -0
- package/dist/agent-state-kind.d.ts +12 -0
- package/dist/agent-state-kind.js +189 -0
- package/dist/asset-service.d.ts +104 -0
- package/dist/asset-service.js +267 -0
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +33 -0
- package/dist/credential-push.js +109 -0
- package/dist/event-collector-registry.d.ts +20 -0
- package/dist/event-collector-registry.js +115 -0
- package/dist/event-collector.d.ts +39 -0
- package/dist/event-collector.js +365 -0
- package/dist/hub-session-lookups.d.ts +135 -0
- package/dist/hub-session-lookups.js +663 -0
- package/dist/hub-session-orchestrator.d.ts +25 -0
- package/dist/hub-session-orchestrator.js +87 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/package-registry-kind.d.ts +70 -0
- package/dist/package-registry-kind.js +260 -0
- package/dist/repo-store/index.d.ts +4 -0
- package/dist/repo-store/index.js +3 -0
- package/dist/repo-store/store.d.ts +41 -0
- package/dist/repo-store/store.js +1829 -0
- package/dist/repo-store/subscribe-kind.d.ts +56 -0
- package/dist/repo-store/subscribe-kind.js +144 -0
- package/dist/repo-store/types.d.ts +571 -0
- package/dist/repo-store/types.js +42 -0
- package/dist/session-service.d.ts +416 -0
- package/dist/session-service.js +1191 -0
- package/dist/sidecar-allocation/contracts.d.ts +78 -0
- package/dist/sidecar-allocation/contracts.js +21 -0
- package/dist/sidecar-allocation/index.d.ts +4 -0
- package/dist/sidecar-allocation/index.js +3 -0
- package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
- package/dist/sidecar-allocation/placement-policy.js +21 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
- package/dist/sidecar-allocation/plugin-registry.js +37 -0
- package/dist/sidecar-allocation/reconciler.d.ts +42 -0
- package/dist/sidecar-allocation/reconciler.js +431 -0
- package/dist/skill-kind.d.ts +41 -0
- package/dist/skill-kind.js +292 -0
- package/dist/substrate.d.ts +8 -0
- package/dist/substrate.js +21 -0
- package/dist/workflow-allocation-service.d.ts +58 -0
- package/dist/workflow-allocation-service.js +239 -0
- package/dist/workflow-closure-resolution.d.ts +106 -0
- package/dist/workflow-closure-resolution.js +123 -0
- package/dist/workflow-definition-ensure.d.ts +24 -0
- package/dist/workflow-definition-ensure.js +75 -0
- package/dist/workflow-dispatch-service.d.ts +40 -0
- package/dist/workflow-dispatch-service.js +146 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +37 -0
- package/dist/workflow-kind.js +310 -0
- package/dist/workflow-probe-gate.d.ts +214 -0
- package/dist/workflow-probe-gate.js +207 -0
- package/dist/workflow-run-event-log.d.ts +21 -0
- package/dist/workflow-run-event-log.js +51 -0
- package/dist/workflow-run-kind.d.ts +440 -0
- package/dist/workflow-run-kind.js +2916 -0
- package/dist/workflow-run-reader.d.ts +47 -0
- package/dist/workflow-run-reader.js +153 -0
- package/dist/workflow-run-restore.d.ts +15 -0
- package/dist/workflow-run-restore.js +26 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/ws/index.d.ts +3 -0
- package/dist/ws/index.js +3 -0
- package/dist/ws/sidecar-events.d.ts +222 -0
- package/dist/ws/sidecar-events.js +72 -0
- package/dist/ws/sidecar-handler.d.ts +305 -0
- package/dist/ws/sidecar-handler.js +2537 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +17 -0
- package/dist/ws/sidecar-token-authenticator.js +81 -0
- package/package.json +35 -12
- package/src/agent-repo.test.ts +0 -310
- package/src/agent-repo.ts +0 -165
- package/src/agent-state-kind.test.ts +0 -247
- package/src/agent-state-kind.ts +0 -204
- package/src/asset-service.test.ts +0 -540
- package/src/asset-service.ts +0 -378
- package/src/available-skills-stanza.test.ts +0 -87
- package/src/available-skills-stanza.ts +0 -47
- package/src/credential-push.ts +0 -65
- package/src/event-collector-registry.test.ts +0 -73
- package/src/event-collector-registry.ts +0 -171
- package/src/event-collector.test.ts +0 -1387
- package/src/event-collector.ts +0 -424
- package/src/hub-session-lookups.ts +0 -206
- package/src/hub-session-orchestrator.test.ts +0 -510
- package/src/hub-session-orchestrator.ts +0 -213
- package/src/index.ts +0 -78
- package/src/repo-store/index.ts +0 -15
- package/src/repo-store/store.test.ts +0 -1169
- package/src/repo-store/store.ts +0 -428
- package/src/repo-store/types.ts +0 -253
- package/src/session-service.test.ts +0 -895
- package/src/session-service.ts +0 -464
- package/src/skill-kind.test.ts +0 -599
- package/src/skill-kind.ts +0 -350
- package/src/ws/index.ts +0 -18
- package/src/ws/sidecar-events.test.ts +0 -96
- package/src/ws/sidecar-events.ts +0 -231
- package/src/ws/sidecar-handler.test.ts +0 -2217
- package/src/ws/sidecar-handler.ts +0 -1574
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type AuthorizeFn, type KindHandler } from "./repo-store/index.js";
|
|
2
|
+
export type SkillHubPrincipal = {
|
|
3
|
+
readonly kind: "hub";
|
|
4
|
+
};
|
|
5
|
+
export type SkillSidecarPrincipal = {
|
|
6
|
+
readonly kind: "sidecar";
|
|
7
|
+
readonly agentId: string;
|
|
8
|
+
};
|
|
9
|
+
export type SkillPrincipal = SkillHubPrincipal | SkillSidecarPrincipal;
|
|
10
|
+
/**
|
|
11
|
+
* arktype schema for the SKILL.md frontmatter. Required fields are
|
|
12
|
+
* `name` and `description`; the Claude Code superset of optional fields
|
|
13
|
+
* (`when_to_use`, `allowed-tools`, `paths`, `model`, ...) is accepted
|
|
14
|
+
* via `onUndeclaredKey("ignore")` but not enforced.
|
|
15
|
+
*
|
|
16
|
+
* The forbidden-name narrow rejects `"anthropic"` and `"claude"`
|
|
17
|
+
* because those values are reserved by the upstream agentskills.io
|
|
18
|
+
* spec for vendor-owned skill packs.
|
|
19
|
+
*/
|
|
20
|
+
export declare const skillFrontmatterSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
}, {}>;
|
|
24
|
+
export type SkillFrontmatter = typeof skillFrontmatterSchema.infer;
|
|
25
|
+
export type SkillIndexEntry = {
|
|
26
|
+
/** Equal to the containing directory name and to `frontmatter.name`. */
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
/** Full parsed frontmatter, including any accepted optional fields. */
|
|
30
|
+
frontmatter: Record<string, unknown>;
|
|
31
|
+
/** Path of the skill subdirectory relative to the asset mount, with trailing slash. */
|
|
32
|
+
workspaceSubpath: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Returns the parsed skill index for `(assetId, ref)`, or an empty
|
|
36
|
+
* array if no index has been populated yet. The index is refreshed by
|
|
37
|
+
* the kind handler's `onRefUpdated` hook after each successful write.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getSkillIndex(assetId: string, ref: string): SkillIndexEntry[];
|
|
40
|
+
export declare const skillKindHandler: KindHandler;
|
|
41
|
+
export declare const skillAuthorize: AuthorizeFn;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { getLogger } from "@intx/log";
|
|
3
|
+
import { glob, repoActionToGrantVerb } from "@intx/hub-common";
|
|
4
|
+
import { UserPrincipal, } from "./repo-store/index.js";
|
|
5
|
+
const logger = getLogger(["hub-sessions", "skill-kind"]);
|
|
6
|
+
/**
|
|
7
|
+
* arktype schema for the SKILL.md frontmatter. Required fields are
|
|
8
|
+
* `name` and `description`; the Claude Code superset of optional fields
|
|
9
|
+
* (`when_to_use`, `allowed-tools`, `paths`, `model`, ...) is accepted
|
|
10
|
+
* via `onUndeclaredKey("ignore")` but not enforced.
|
|
11
|
+
*
|
|
12
|
+
* The forbidden-name narrow rejects `"anthropic"` and `"claude"`
|
|
13
|
+
* because those values are reserved by the upstream agentskills.io
|
|
14
|
+
* spec for vendor-owned skill packs.
|
|
15
|
+
*/
|
|
16
|
+
export const skillFrontmatterSchema = type({
|
|
17
|
+
name: type(/^[a-z0-9]+(-[a-z0-9]+)*$/)
|
|
18
|
+
.and("string<=64")
|
|
19
|
+
.narrow((n, ctx) => {
|
|
20
|
+
if (n === "anthropic" || n === "claude") {
|
|
21
|
+
return ctx.mustBe(`not the reserved name "anthropic" or "claude"`);
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}),
|
|
25
|
+
description: type("1 <= string <= 1024").and(type(/^(?!.*<[^>]+>).*$/s)),
|
|
26
|
+
}).onUndeclaredKey("ignore");
|
|
27
|
+
function cacheKey(assetId, ref) {
|
|
28
|
+
return `${assetId}\u0000${ref}`;
|
|
29
|
+
}
|
|
30
|
+
const skillIndex = new Map();
|
|
31
|
+
const pendingIndex = new Map();
|
|
32
|
+
/**
|
|
33
|
+
* Returns the parsed skill index for `(assetId, ref)`, or an empty
|
|
34
|
+
* array if no index has been populated yet. The index is refreshed by
|
|
35
|
+
* the kind handler's `onRefUpdated` hook after each successful write.
|
|
36
|
+
*/
|
|
37
|
+
export function getSkillIndex(assetId, ref) {
|
|
38
|
+
return skillIndex.get(cacheKey(assetId, ref)) ?? [];
|
|
39
|
+
}
|
|
40
|
+
const FRONTMATTER_DELIMITER = "---";
|
|
41
|
+
const YamlMapping = type("Record<string, unknown>");
|
|
42
|
+
function parseSkillMd(body) {
|
|
43
|
+
const lines = body.split(/\r?\n/);
|
|
44
|
+
if (lines[0] !== FRONTMATTER_DELIMITER) {
|
|
45
|
+
throw new Error("SKILL.md is missing YAML frontmatter delimiter");
|
|
46
|
+
}
|
|
47
|
+
let endIdx = -1;
|
|
48
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
49
|
+
if (lines[i] === FRONTMATTER_DELIMITER) {
|
|
50
|
+
endIdx = i;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (endIdx === -1) {
|
|
55
|
+
throw new Error("SKILL.md frontmatter has no closing delimiter");
|
|
56
|
+
}
|
|
57
|
+
const yamlText = lines.slice(1, endIdx).join("\n");
|
|
58
|
+
const parsed = Bun.YAML.parse(yamlText);
|
|
59
|
+
const validated = YamlMapping(parsed);
|
|
60
|
+
if (validated instanceof type.errors) {
|
|
61
|
+
throw new Error(`SKILL.md frontmatter must be a YAML mapping: ${validated.summary}`);
|
|
62
|
+
}
|
|
63
|
+
return { frontmatter: validated };
|
|
64
|
+
}
|
|
65
|
+
async function parseSkillEntry(subdir, readBlob) {
|
|
66
|
+
const skillPath = `${subdir}/SKILL.md`;
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = await readBlob(skillPath);
|
|
70
|
+
}
|
|
71
|
+
catch (cause) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
reason: `skill ${subdir} is missing SKILL.md: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const body = new TextDecoder().decode(raw);
|
|
78
|
+
let parsed;
|
|
79
|
+
try {
|
|
80
|
+
parsed = parseSkillMd(body);
|
|
81
|
+
}
|
|
82
|
+
catch (cause) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
reason: `skill ${subdir} frontmatter parse failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const result = skillFrontmatterSchema(parsed.frontmatter);
|
|
89
|
+
if (result instanceof type.errors) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: `skill ${subdir} frontmatter is invalid: ${result.summary}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (result.name !== subdir) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
reason: `skill ${subdir} frontmatter.name ${JSON.stringify(result.name)} does not match directory name`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
entry: {
|
|
104
|
+
name: result.name,
|
|
105
|
+
description: result.description,
|
|
106
|
+
frontmatter: parsed.frontmatter,
|
|
107
|
+
workspaceSubpath: `${subdir}/`,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function buildSkillIndex(topLevelTreePaths, readBlob, listDir) {
|
|
112
|
+
const entries = [];
|
|
113
|
+
// Sort so the index ordering is deterministic across reads.
|
|
114
|
+
const candidates = [...topLevelTreePaths].sort();
|
|
115
|
+
for (const candidate of candidates) {
|
|
116
|
+
// Skill subdirectories are arbitrarily named (`<skill-name>/`), so
|
|
117
|
+
// there is no enumerable allowlist of names the way the workflow,
|
|
118
|
+
// package-registry, and agent-state handlers use. The structural
|
|
119
|
+
// distinction is tree-vs-blob: a top-level tree entry is a skill
|
|
120
|
+
// subdir, a top-level blob (e.g. `.gitignore` seeded by the
|
|
121
|
+
// genesis init) is not.
|
|
122
|
+
//
|
|
123
|
+
// Probe via `listDir`. The substrate's two implementations
|
|
124
|
+
// disagree on the not-a-tree signal — the receivePack closures
|
|
125
|
+
// throw, the writeTree closures return an empty array — so this
|
|
126
|
+
// probe accepts either signal as "not a directory" and skips the
|
|
127
|
+
// entry. Git does not store empty trees, so an empty `listDir`
|
|
128
|
+
// result is equivalent to "this name is not a directory in the
|
|
129
|
+
// prospective tree".
|
|
130
|
+
let children;
|
|
131
|
+
try {
|
|
132
|
+
children = await listDir(candidate);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (children.length === 0)
|
|
138
|
+
continue;
|
|
139
|
+
const outcome = await parseSkillEntry(candidate, readBlob);
|
|
140
|
+
if (!outcome.ok) {
|
|
141
|
+
return { ok: false, reason: outcome.reason };
|
|
142
|
+
}
|
|
143
|
+
entries.push(outcome.entry);
|
|
144
|
+
}
|
|
145
|
+
return { ok: true, entries };
|
|
146
|
+
}
|
|
147
|
+
export const skillKindHandler = {
|
|
148
|
+
kind: "skill",
|
|
149
|
+
directoryPrefix: "assets/skill",
|
|
150
|
+
async validatePush({ repoId, ref, topLevelTreePaths, readBlob, listDir, }) {
|
|
151
|
+
// Drop any staged entry from a previous attempt first. The
|
|
152
|
+
// substrate calls validatePush before advancing the ref, so a prior
|
|
153
|
+
// validation that was accepted but never followed by onRefUpdated
|
|
154
|
+
// (e.g. the commit step threw after validation succeeded) would
|
|
155
|
+
// otherwise leave a stale entry that a later rejected attempt
|
|
156
|
+
// would silently inherit.
|
|
157
|
+
const key = cacheKey(repoId.id, ref);
|
|
158
|
+
pendingIndex.delete(key);
|
|
159
|
+
const result = await buildSkillIndex(topLevelTreePaths, readBlob, listDir);
|
|
160
|
+
if (!result.ok) {
|
|
161
|
+
logger.debug `skill validatePush rejected ${repoId.kind}/${repoId.id} on ${ref}: ${result.reason}`;
|
|
162
|
+
return { ok: false, reason: result.reason };
|
|
163
|
+
}
|
|
164
|
+
// Stage the parsed index against (repoId.id, ref). The substrate
|
|
165
|
+
// calls onRefUpdated immediately after the ref is advanced and we
|
|
166
|
+
// promote the staged entry into the live cache there.
|
|
167
|
+
pendingIndex.set(key, result.entries);
|
|
168
|
+
return { ok: true };
|
|
169
|
+
},
|
|
170
|
+
onRefUpdated({ repoId, ref }) {
|
|
171
|
+
const key = cacheKey(repoId.id, ref);
|
|
172
|
+
const staged = pendingIndex.get(key);
|
|
173
|
+
if (staged === undefined) {
|
|
174
|
+
throw new Error(`skillKindHandler.onRefUpdated: no validated tree pending for ${repoId.id} @ ${ref}`);
|
|
175
|
+
}
|
|
176
|
+
pendingIndex.delete(key);
|
|
177
|
+
skillIndex.set(key, staged);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
const SidecarPrincipal = type({
|
|
181
|
+
kind: "'sidecar'",
|
|
182
|
+
agentId: "string",
|
|
183
|
+
});
|
|
184
|
+
export const skillAuthorize = (principal, repoId, ref, action) => {
|
|
185
|
+
if (repoId.kind !== "skill") {
|
|
186
|
+
return {
|
|
187
|
+
allowed: false,
|
|
188
|
+
reason: `skill authorize received non-skill repo ${repoId.kind}/${repoId.id}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (principal.kind === "hub") {
|
|
192
|
+
return { allowed: true };
|
|
193
|
+
}
|
|
194
|
+
if (principal.kind === "sidecar") {
|
|
195
|
+
const parsed = SidecarPrincipal(principal);
|
|
196
|
+
if (parsed instanceof type.errors) {
|
|
197
|
+
return {
|
|
198
|
+
allowed: false,
|
|
199
|
+
reason: `sidecar principal is malformed: ${parsed.summary}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
switch (action) {
|
|
203
|
+
case "createPack":
|
|
204
|
+
case "resolveRef":
|
|
205
|
+
return { allowed: true };
|
|
206
|
+
case "init":
|
|
207
|
+
case "writeTree":
|
|
208
|
+
case "receivePack":
|
|
209
|
+
return {
|
|
210
|
+
allowed: false,
|
|
211
|
+
reason: `sidecars may only read skill assets, not ${action}`,
|
|
212
|
+
};
|
|
213
|
+
default: {
|
|
214
|
+
const _exhaustive = action;
|
|
215
|
+
return {
|
|
216
|
+
allowed: false,
|
|
217
|
+
reason: `unhandled action: ${String(_exhaustive)}`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (principal.kind === "user") {
|
|
223
|
+
// The route layer has already pre-resolved the grant verdict and
|
|
224
|
+
// attached it as `authz`. The substrate does NOT re-query the
|
|
225
|
+
// grant store here; it (a) checks the bearer-token's claims
|
|
226
|
+
// bound the requested (ref, action) and have not expired, and
|
|
227
|
+
// (b) sanity-checks that the pre-resolved verdict targets this
|
|
228
|
+
// exact resource and grant verb. Both gates must pass before the
|
|
229
|
+
// verdict's `effect` is honoured.
|
|
230
|
+
const parsed = UserPrincipal(principal);
|
|
231
|
+
if (parsed instanceof type.errors) {
|
|
232
|
+
return {
|
|
233
|
+
allowed: false,
|
|
234
|
+
reason: `user principal is malformed: ${parsed.summary}`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (!parsed.tokenClaims.actions.includes(action)) {
|
|
238
|
+
return {
|
|
239
|
+
allowed: false,
|
|
240
|
+
reason: `token does not grant action ${action}`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// `ref === "*"` is the substrate's sentinel for a bulk read used
|
|
244
|
+
// by `listRefs`. The advertise-refs layer applies the per-ref
|
|
245
|
+
// refPattern filter on the listing it returns, so gating the bulk
|
|
246
|
+
// read on refPattern here would prevent any token with a
|
|
247
|
+
// narrow-pattern (e.g. `refs/heads/main`) from listing refs at
|
|
248
|
+
// all. Only the action and expiry are enforced for the bulk read;
|
|
249
|
+
// every other call site passes a concrete ref and is gated below.
|
|
250
|
+
if (ref !== "*" && !glob.match(parsed.tokenClaims.refPattern, ref)) {
|
|
251
|
+
return {
|
|
252
|
+
allowed: false,
|
|
253
|
+
reason: `token refPattern ${parsed.tokenClaims.refPattern} does not match ${ref}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (Date.now() >= parsed.tokenClaims.expiresAt) {
|
|
257
|
+
return {
|
|
258
|
+
allowed: false,
|
|
259
|
+
reason: `token expired at ${parsed.tokenClaims.expiresAt}`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const expectedResource = `asset:${repoId.id}`;
|
|
263
|
+
if (parsed.authz.resource !== expectedResource) {
|
|
264
|
+
return {
|
|
265
|
+
allowed: false,
|
|
266
|
+
reason: `authz verdict resource ${parsed.authz.resource} does not match ${expectedResource}`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const expectedGrantVerb = repoActionToGrantVerb(action);
|
|
270
|
+
if (parsed.authz.grantVerb !== expectedGrantVerb) {
|
|
271
|
+
return {
|
|
272
|
+
allowed: false,
|
|
273
|
+
reason: `authz verdict grantVerb ${parsed.authz.grantVerb} does not match ${expectedGrantVerb}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (parsed.authz.effect === "allow") {
|
|
277
|
+
return { allowed: true };
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
allowed: false,
|
|
281
|
+
reason: `authz verdict denied for ${expectedResource} ${expectedGrantVerb}`,
|
|
282
|
+
};
|
|
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.
|
|
288
|
+
return {
|
|
289
|
+
allowed: false,
|
|
290
|
+
reason: `unknown principal kind: ${principal.kind}`,
|
|
291
|
+
};
|
|
292
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export { encodeCombinedEventLog, splitCombinedEventLog, WORKFLOW_RUN_EVENTS_FILE, } from "./workflow-run-event-log.js";
|
|
4
|
+
export { workflowDefinitionEnvelopeSchema } from "./workflow-kind.js";
|
|
5
|
+
export { subscribeKind } from "./repo-store/subscribe-kind.js";
|
|
6
|
+
export type { SubscribeKindEntry } from "./repo-store/subscribe-kind.js";
|
|
7
|
+
export { createAgentRepoStore } from "./agent-repo.js";
|
|
8
|
+
export type { CommittedReads, CommittedTreeEntry, Principal, RepoId, RepoStore, WriteResult, InitRepoOpts, NewlyTerminalRun, } from "./repo-store/types.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The db-free substrate layer of this package.
|
|
2
|
+
//
|
|
3
|
+
// `@intx/hub-sessions` holds two layers: the git-backed repo-store substrate
|
|
4
|
+
// (the `RepoStore`, the workflow-run claim-check and event-log primitives, the
|
|
5
|
+
// kind-handler protocol) and the db-backed control-plane services (sessions,
|
|
6
|
+
// credential push, the websocket layer). The substrate layer never imports
|
|
7
|
+
// `@intx/db`; the services layer does. The package barrel (`index.ts`)
|
|
8
|
+
// re-exports both, so importing any symbol through it pulls drizzle and the
|
|
9
|
+
// rest of the hub data layer into module-evaluation.
|
|
10
|
+
//
|
|
11
|
+
// This entry exposes only the substrate layer, so any consumer that needs
|
|
12
|
+
// repo/substrate access without the control-plane services -- for example a
|
|
13
|
+
// process that reads and writes the agent-state and workflow-run repos but
|
|
14
|
+
// runs nowhere near a database -- can import it without dragging `@intx/db`
|
|
15
|
+
// into its boot graph. Every symbol re-exported here is defined in a db-free
|
|
16
|
+
// module. The barrel still re-exports all of these for hub-side consumers.
|
|
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
|
+
export { encodeCombinedEventLog, splitCombinedEventLog, WORKFLOW_RUN_EVENTS_FILE, } from "./workflow-run-event-log.js";
|
|
19
|
+
export { workflowDefinitionEnvelopeSchema } from "./workflow-kind.js";
|
|
20
|
+
export { subscribeKind } from "./repo-store/subscribe-kind.js";
|
|
21
|
+
export { createAgentRepoStore } from "./agent-repo.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
|
+
}
|