@electric-ax/agents-server 0.4.14 → 0.4.15
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/dist/entrypoint.js +345 -15
- package/dist/index.cjs +347 -15
- package/dist/index.d.cts +332 -164
- package/dist/index.d.ts +332 -164
- package/dist/index.js +347 -15
- package/drizzle/0010_sandbox_profiles.sql +5 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +5 -5
- package/src/db/schema.ts +2 -0
- package/src/electric-agents-types.ts +71 -0
- package/src/entity-manager.ts +337 -6
- package/src/entity-projector.ts +3 -0
- package/src/entity-registry.ts +73 -0
- package/src/routing/entities-router.ts +18 -0
- package/src/routing/runners-router.ts +10 -0
- package/src/routing/sandbox.ts +173 -0
- package/src/sandbox-choice-schema.ts +28 -0
- package/src/stream-client.ts +17 -1
- package/src/utils/server-utils.ts +2 -2
- package/src/wake-registry.ts +22 -11
package/dist/index.cjs
CHANGED
|
@@ -90,6 +90,7 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
|
|
|
90
90
|
tags: (0, drizzle_orm_pg_core.jsonb)(`tags`).notNull().default({}),
|
|
91
91
|
tagsIndex: (0, drizzle_orm_pg_core.text)(`tags_index`).array().notNull().default(drizzle_orm.sql`'{}'::text[]`),
|
|
92
92
|
spawnArgs: (0, drizzle_orm_pg_core.jsonb)(`spawn_args`).default({}),
|
|
93
|
+
sandbox: (0, drizzle_orm_pg_core.jsonb)(`sandbox`),
|
|
93
94
|
parent: (0, drizzle_orm_pg_core.text)(`parent`),
|
|
94
95
|
createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
|
|
95
96
|
typeRevision: (0, drizzle_orm_pg_core.integer)(`type_revision`),
|
|
@@ -131,6 +132,7 @@ const runners = (0, drizzle_orm_pg_core.pgTable)(`runners`, {
|
|
|
131
132
|
kind: (0, drizzle_orm_pg_core.text)(`kind`).notNull().default(`local`),
|
|
132
133
|
adminStatus: (0, drizzle_orm_pg_core.text)(`admin_status`).notNull().default(`enabled`),
|
|
133
134
|
wakeStream: (0, drizzle_orm_pg_core.text)(`wake_stream`).notNull(),
|
|
135
|
+
sandboxProfiles: (0, drizzle_orm_pg_core.jsonb)(`sandbox_profiles`).notNull().default([]),
|
|
134
136
|
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
135
137
|
updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
136
138
|
}, (table) => [
|
|
@@ -428,6 +430,7 @@ function toPublicEntity(entity) {
|
|
|
428
430
|
dispatch_policy: entity.dispatch_policy,
|
|
429
431
|
tags: entity.tags,
|
|
430
432
|
spawn_args: entity.spawn_args,
|
|
433
|
+
sandbox: entity.sandbox,
|
|
431
434
|
parent: entity.parent,
|
|
432
435
|
created_by: entity.created_by,
|
|
433
436
|
created_at: entity.created_at,
|
|
@@ -496,6 +499,12 @@ var PostgresRegistry = class {
|
|
|
496
499
|
async createRunner(input) {
|
|
497
500
|
const now = new Date();
|
|
498
501
|
const wakeStream = input.wakeStream ?? runnerWakeStream(input.id);
|
|
502
|
+
const sandboxProfilesValue = input.sandboxProfiles ? input.sandboxProfiles.map((p) => ({
|
|
503
|
+
name: p.name,
|
|
504
|
+
label: p.label,
|
|
505
|
+
...p.description !== void 0 && { description: p.description },
|
|
506
|
+
...p.remote !== void 0 && { remote: p.remote }
|
|
507
|
+
})) : void 0;
|
|
499
508
|
await this.db.insert(runners).values({
|
|
500
509
|
tenantId: this.tenantId,
|
|
501
510
|
id: input.id,
|
|
@@ -504,6 +513,7 @@ var PostgresRegistry = class {
|
|
|
504
513
|
kind: input.kind ?? `local`,
|
|
505
514
|
adminStatus: input.adminStatus ?? `enabled`,
|
|
506
515
|
wakeStream,
|
|
516
|
+
...sandboxProfilesValue !== void 0 && { sandboxProfiles: sandboxProfilesValue },
|
|
507
517
|
updatedAt: now
|
|
508
518
|
}).onConflictDoUpdate({
|
|
509
519
|
target: [runners.tenantId, runners.id],
|
|
@@ -513,6 +523,7 @@ var PostgresRegistry = class {
|
|
|
513
523
|
kind: input.kind ?? `local`,
|
|
514
524
|
adminStatus: input.adminStatus ?? `enabled`,
|
|
515
525
|
wakeStream,
|
|
526
|
+
...sandboxProfilesValue !== void 0 && { sandboxProfiles: sandboxProfilesValue },
|
|
516
527
|
updatedAt: now
|
|
517
528
|
}
|
|
518
529
|
});
|
|
@@ -520,6 +531,30 @@ var PostgresRegistry = class {
|
|
|
520
531
|
if (!runner) throw new Error(`Failed to read back runner "${input.id}"`);
|
|
521
532
|
return runner;
|
|
522
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* Every sandbox profile advertised by a runner in this tenant (one entry
|
|
536
|
+
* per runner that advertises it — names may repeat across runners). Used by
|
|
537
|
+
* spawn validation for unpinned dispatch to learn whether a chosen profile
|
|
538
|
+
* is remote (so a shared sandbox can skip the single-runner guard).
|
|
539
|
+
*/
|
|
540
|
+
async listSandboxProfiles() {
|
|
541
|
+
const rows = await this.db.select({ sandboxProfiles: runners.sandboxProfiles }).from(runners).where((0, drizzle_orm.eq)(runners.tenantId, this.tenantId));
|
|
542
|
+
const profiles = [];
|
|
543
|
+
for (const row of rows) {
|
|
544
|
+
const list = row.sandboxProfiles;
|
|
545
|
+
if (!Array.isArray(list)) continue;
|
|
546
|
+
for (const entry of list) {
|
|
547
|
+
if (!entry || typeof entry.name !== `string`) continue;
|
|
548
|
+
profiles.push({
|
|
549
|
+
name: entry.name,
|
|
550
|
+
label: typeof entry.label === `string` ? entry.label : entry.name,
|
|
551
|
+
...typeof entry.description === `string` && { description: entry.description },
|
|
552
|
+
...typeof entry.remote === `boolean` && { remote: entry.remote }
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return profiles;
|
|
557
|
+
}
|
|
523
558
|
async getRunner(id) {
|
|
524
559
|
const rows = await this.db.select().from(runners).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(runners.tenantId, this.tenantId), (0, drizzle_orm.eq)(runners.id, id))).limit(1);
|
|
525
560
|
return rows[0] ? this.rowToRunner(rows[0]) : null;
|
|
@@ -780,6 +815,7 @@ var PostgresRegistry = class {
|
|
|
780
815
|
tags: (0, __electric_ax_agents_runtime.normalizeTags)(entity.tags),
|
|
781
816
|
tagsIndex: (0, __electric_ax_agents_runtime.buildTagsIndex)(entity.tags),
|
|
782
817
|
spawnArgs: entity.spawn_args ?? {},
|
|
818
|
+
sandbox: entity.sandbox ?? null,
|
|
783
819
|
parent: entity.parent ?? null,
|
|
784
820
|
createdBy: entity.created_by ?? null,
|
|
785
821
|
typeRevision: entity.type_revision ?? null,
|
|
@@ -1117,6 +1153,7 @@ var PostgresRegistry = class {
|
|
|
1117
1153
|
write_token: row.writeToken,
|
|
1118
1154
|
tags: row.tags ?? {},
|
|
1119
1155
|
spawn_args: row.spawnArgs,
|
|
1156
|
+
sandbox: row.sandbox ?? void 0,
|
|
1120
1157
|
parent: row.parent ?? void 0,
|
|
1121
1158
|
created_by: row.createdBy ?? void 0,
|
|
1122
1159
|
type_revision: row.typeRevision ?? void 0,
|
|
@@ -1164,6 +1201,7 @@ var PostgresRegistry = class {
|
|
|
1164
1201
|
kind: assertRunnerKind(row.kind),
|
|
1165
1202
|
admin_status: assertRunnerAdminStatus(row.adminStatus),
|
|
1166
1203
|
wake_stream: row.wakeStream,
|
|
1204
|
+
sandbox_profiles: row.sandboxProfiles ?? [],
|
|
1167
1205
|
created_at: row.createdAt.toISOString(),
|
|
1168
1206
|
updated_at: row.updatedAt.toISOString()
|
|
1169
1207
|
};
|
|
@@ -1294,6 +1332,7 @@ const ENTITY_SHAPE_COLUMNS = [
|
|
|
1294
1332
|
`status`,
|
|
1295
1333
|
`tags`,
|
|
1296
1334
|
`spawn_args`,
|
|
1335
|
+
`sandbox`,
|
|
1297
1336
|
`parent`,
|
|
1298
1337
|
`type_revision`,
|
|
1299
1338
|
`inbox_schemas`,
|
|
@@ -1327,6 +1366,7 @@ function toMemberRow(entity) {
|
|
|
1327
1366
|
status: entity.status,
|
|
1328
1367
|
tags: entity.tags,
|
|
1329
1368
|
spawn_args: entity.spawn_args ?? {},
|
|
1369
|
+
sandbox: entity.sandbox ?? null,
|
|
1330
1370
|
parent: entity.parent ?? null,
|
|
1331
1371
|
type_revision: entity.type_revision ?? null,
|
|
1332
1372
|
inbox_schemas: entity.inbox_schemas ?? null,
|
|
@@ -2007,7 +2047,7 @@ var StreamClient = class {
|
|
|
2007
2047
|
});
|
|
2008
2048
|
});
|
|
2009
2049
|
}
|
|
2010
|
-
async fork(path$2, sourcePath) {
|
|
2050
|
+
async fork(path$2, sourcePath, opts) {
|
|
2011
2051
|
return await withSpan(`stream.fork`, async (span) => {
|
|
2012
2052
|
span.setAttributes({
|
|
2013
2053
|
[ATTR.STREAM_PATH]: path$2,
|
|
@@ -2017,6 +2057,11 @@ var StreamClient = class {
|
|
|
2017
2057
|
"content-type": `application/json`,
|
|
2018
2058
|
"Stream-Forked-From": new URL(this.streamUrl(sourcePath)).pathname
|
|
2019
2059
|
};
|
|
2060
|
+
if (opts?.forkPointer) {
|
|
2061
|
+
const ZERO_OFFSET = `0000000000000000_0000000000000000`;
|
|
2062
|
+
headers[`Stream-Fork-Offset`] = opts.forkPointer.offset ?? ZERO_OFFSET;
|
|
2063
|
+
if (opts.forkPointer.subOffset > 0) headers[`Stream-Fork-Sub-Offset`] = String(opts.forkPointer.subOffset);
|
|
2064
|
+
}
|
|
2020
2065
|
injectTraceHeaders(headers);
|
|
2021
2066
|
const response = await fetch(this.streamUrl(path$2), {
|
|
2022
2067
|
method: `PUT`,
|
|
@@ -2544,6 +2589,103 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
|
|
|
2544
2589
|
});
|
|
2545
2590
|
}
|
|
2546
2591
|
|
|
2592
|
+
//#endregion
|
|
2593
|
+
//#region src/routing/sandbox.ts
|
|
2594
|
+
/**
|
|
2595
|
+
* Resolve and validate a spawn's sandbox CHOICE into the {@link
|
|
2596
|
+
* EntitySandboxSelection} persisted on the entity. Sibling of
|
|
2597
|
+
* `dispatch-policy.ts`'s `resolveEffectiveDispatchPolicyForSpawn`: kept off the
|
|
2598
|
+
* EntityManager so the spawn path reads as composed resolution steps.
|
|
2599
|
+
*
|
|
2600
|
+
* Profiles are a per-runner concern: each runner advertises what it supports.
|
|
2601
|
+
* When the spawn pins a runner via dispatch_policy, the chosen profile must be
|
|
2602
|
+
* in that runner's advertised set; otherwise we'd persist an unserviceable
|
|
2603
|
+
* choice that fails late at first wake. For unpinned dispatch (webhook /
|
|
2604
|
+
* parent-inherited) we can't pick a target ahead of time, so we fall back to a
|
|
2605
|
+
* tenant-wide "some runner offers this" check — better than nothing.
|
|
2606
|
+
*/
|
|
2607
|
+
async function resolveSandboxForSpawn(registry, dispatchPolicy, requested, parentEntity) {
|
|
2608
|
+
if (!requested) return void 0;
|
|
2609
|
+
const choice = applyInheritedSandbox(requested, parentEntity);
|
|
2610
|
+
if (!choice) return void 0;
|
|
2611
|
+
const chosenName = choice.profile;
|
|
2612
|
+
if (!chosenName) throw new ElectricAgentsError(ErrCodeInvalidRequest, `sandbox requires a "profile" (or "inherit": true with a parent that has a shared sandbox).`, 400);
|
|
2613
|
+
const chosenIsRemote = await resolveChosenProfileRemote(registry, chosenName, dispatchPolicy);
|
|
2614
|
+
assertSharedSandboxColocated(choice.key, chosenIsRemote, dispatchPolicy);
|
|
2615
|
+
const selection = { profile: chosenName };
|
|
2616
|
+
if (choice.key !== void 0) selection.key = choice.key;
|
|
2617
|
+
else if (choice.scope !== void 0) selection.scope = choice.scope;
|
|
2618
|
+
if (choice.persistent !== void 0) selection.persistent = choice.persistent;
|
|
2619
|
+
if (choice.owner === false) selection.owner = false;
|
|
2620
|
+
return selection;
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Resolve `inherit` against the parent's *stored* sandbox. `inherit` reuses the
|
|
2624
|
+
* parent's keyed sandbox as a non-owner (attach-only). It's graceful: if the
|
|
2625
|
+
* parent has no shareable (keyed) sandbox the child simply gets none (returns
|
|
2626
|
+
* `undefined`), so `spawn_worker` can always request inheritance without
|
|
2627
|
+
* breaking unkeyed parents. (A running parent wake resolves inherit to its live
|
|
2628
|
+
* explicit key in the runtime instead — this server-side path covers direct API
|
|
2629
|
+
* callers, where only the parent's *stored* explicit key is available.)
|
|
2630
|
+
*
|
|
2631
|
+
* For a non-inherit choice the request passes through unchanged.
|
|
2632
|
+
*
|
|
2633
|
+
* NOTE: `inherit: true` takes the parent's identity AND durability wholesale —
|
|
2634
|
+
* any sibling field on the request (e.g. a caller-supplied `persistent: false`)
|
|
2635
|
+
* is intentionally ignored, because a child attaches to the parent's existing
|
|
2636
|
+
* sandbox and cannot change how that sandbox is torn down. `sandboxChoiceSchema`
|
|
2637
|
+
* permits the `{ inherit: true, persistent: ... }` combination, so the
|
|
2638
|
+
* precedence is resolved here rather than rejected at the schema level.
|
|
2639
|
+
*/
|
|
2640
|
+
function applyInheritedSandbox(requested, parentEntity) {
|
|
2641
|
+
if (!requested.inherit) return requested;
|
|
2642
|
+
const parentKey = parentEntity?.sandbox?.key;
|
|
2643
|
+
if (!parentKey) return void 0;
|
|
2644
|
+
return {
|
|
2645
|
+
profile: parentEntity.sandbox.profile,
|
|
2646
|
+
key: parentKey,
|
|
2647
|
+
persistent: parentEntity.sandbox.persistent,
|
|
2648
|
+
owner: false
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Validate the chosen profile is advertised by the relevant runner(s) and
|
|
2653
|
+
* determine whether it is a remote (off-host) sandbox, reachable from any
|
|
2654
|
+
* runner. Defaults to host-local (co-location required) unless every relevant
|
|
2655
|
+
* advertisement marks it remote. Throws if the profile is unserviceable.
|
|
2656
|
+
*/
|
|
2657
|
+
async function resolveChosenProfileRemote(registry, chosenName, dispatchPolicy) {
|
|
2658
|
+
const runnerIds = [];
|
|
2659
|
+
for (const target of dispatchPolicy?.targets ?? []) if (target.type === `runner`) runnerIds.push(target.runnerId);
|
|
2660
|
+
if (runnerIds.length > 0) {
|
|
2661
|
+
let allRemote = true;
|
|
2662
|
+
for (const runnerId of runnerIds) {
|
|
2663
|
+
const runner = await registry.getRunner(runnerId);
|
|
2664
|
+
const advertised = runner?.sandbox_profiles ?? [];
|
|
2665
|
+
const match = advertised.find((p) => p.name === chosenName);
|
|
2666
|
+
if (!match) throw new ElectricAgentsError(ErrCodeInvalidRequest, `sandbox profile "${chosenName}" is not advertised by runner "${runnerId}" (advertised: ${advertised.map((p) => p.name).join(`, `) || `(none)`}).`, 400);
|
|
2667
|
+
if (match.remote !== true) allRemote = false;
|
|
2668
|
+
}
|
|
2669
|
+
return allRemote;
|
|
2670
|
+
}
|
|
2671
|
+
const available = await registry.listSandboxProfiles();
|
|
2672
|
+
const matches = available.filter((p) => p.name === chosenName);
|
|
2673
|
+
if (matches.length === 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `sandbox profile "${chosenName}" is not offered by any registered runner (available: ${[...new Set(available.map((p) => p.name))].join(`, `) || `(none)`}).`, 400);
|
|
2674
|
+
return matches.every((p) => p.remote === true);
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Co-location: a shared *local* sandbox lives on one host, so every
|
|
2678
|
+
* collaborator must be pinned to the same single runner. Subagents inherit the
|
|
2679
|
+
* parent's dispatch policy, so this holds once the root is pinned. A shared
|
|
2680
|
+
* *remote* sandbox is reachable from any runner, so the guard does not apply.
|
|
2681
|
+
*/
|
|
2682
|
+
function assertSharedSandboxColocated(key, chosenIsRemote, dispatchPolicy) {
|
|
2683
|
+
if (key === void 0 || chosenIsRemote) return;
|
|
2684
|
+
const targets = dispatchPolicy?.targets ?? [];
|
|
2685
|
+
const pinnedToSingleRunner = targets.length === 1 && targets[0]?.type === `runner`;
|
|
2686
|
+
if (!pinnedToSingleRunner) throw new ElectricAgentsError(ErrCodeInvalidRequest, `a shared sandbox (sandbox.key / sandbox.inherit) requires the entity to be pinned to a single runner via dispatch_policy, so all collaborators share one host.`, 400);
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2547
2689
|
//#endregion
|
|
2548
2690
|
//#region src/principal.ts
|
|
2549
2691
|
const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
|
|
@@ -2944,6 +3086,7 @@ var EntityManager = class {
|
|
|
2944
3086
|
if (!parentEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Parent entity "${req.parent}" not found`, 404);
|
|
2945
3087
|
}
|
|
2946
3088
|
const dispatchPolicy = req.dispatch_policy ? this.validateDispatchPolicy(req.dispatch_policy, { label: `dispatch_policy` }) : parentEntity?.dispatch_policy ? applyTypeDefaultSubscriptionScope(parentEntity.dispatch_policy, entityType.default_dispatch_policy) : entityType.default_dispatch_policy;
|
|
3089
|
+
const sandbox = await resolveSandboxForSpawn(this.registry, dispatchPolicy, req.sandbox, parentEntity);
|
|
2947
3090
|
const now = Date.now();
|
|
2948
3091
|
const entityData = {
|
|
2949
3092
|
type: typeName,
|
|
@@ -2958,6 +3101,7 @@ var EntityManager = class {
|
|
|
2958
3101
|
write_token: writeToken,
|
|
2959
3102
|
tags: initialTags,
|
|
2960
3103
|
spawn_args: req.args,
|
|
3104
|
+
sandbox,
|
|
2961
3105
|
type_revision: entityType.revision,
|
|
2962
3106
|
inbox_schemas: entityType.inbox_schemas,
|
|
2963
3107
|
state_schemas: entityType.state_schemas,
|
|
@@ -3087,27 +3231,66 @@ var EntityManager = class {
|
|
|
3087
3231
|
const writeEntityLocks = new Set();
|
|
3088
3232
|
const writeStreamLocks = new Set();
|
|
3089
3233
|
try {
|
|
3090
|
-
|
|
3234
|
+
let sourceTree;
|
|
3235
|
+
if (opts.forkPointer) {
|
|
3236
|
+
const rootEntity = await this.registry.getEntity(rootUrl);
|
|
3237
|
+
if (!rootEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3238
|
+
if (isTerminalEntityStatus(rootEntity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${rootEntity.url}"`, 409);
|
|
3239
|
+
sourceTree = await this.listEntitySubtree(rootEntity);
|
|
3240
|
+
} else sourceTree = await this.waitForIdleSubtree(rootUrl, opts, workLocks);
|
|
3091
3241
|
const sourceRoot = sourceTree[0];
|
|
3092
3242
|
if (sourceRoot.parent) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Only top-level entities can be forked`, 400);
|
|
3093
|
-
|
|
3243
|
+
let preFilteredRoot;
|
|
3244
|
+
if (opts.forkPointer) {
|
|
3245
|
+
const sourceEvents = await this.streamClient.readJson(sourceRoot.streams.main);
|
|
3246
|
+
const flat = sourceEvents.flatMap((item) => Array.isArray(item) ? item : [item]);
|
|
3247
|
+
const target = this.resolveForkPointerTarget(flat, opts.forkPointer, sourceRoot.streams.main);
|
|
3248
|
+
const filteredEvents = flat.slice(0, target);
|
|
3249
|
+
const rootManifests = this.reduceStateRows(filteredEvents, `manifest`);
|
|
3250
|
+
const sharedStateIds = new Set();
|
|
3251
|
+
for (const manifest of rootManifests.values()) this.collectSharedStateIds(manifest, sharedStateIds);
|
|
3252
|
+
preFilteredRoot = {
|
|
3253
|
+
manifests: rootManifests,
|
|
3254
|
+
childStatuses: this.reduceStateRows(filteredEvents, `child_status`),
|
|
3255
|
+
replayWatermarks: this.reduceStateRows(filteredEvents, `replay_watermark`),
|
|
3256
|
+
sharedStateIds
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
const effectiveSubtree = preFilteredRoot ? this.computeEffectiveSubtree(sourceTree, sourceRoot.url, preFilteredRoot.manifests) : sourceTree;
|
|
3260
|
+
if (opts.forkPointer) {
|
|
3261
|
+
const descendants = effectiveSubtree.filter((entity) => entity.url !== sourceRoot.url);
|
|
3262
|
+
if (descendants.length > 0) await this.waitForGivenEntitiesIdle(descendants, opts, workLocks);
|
|
3263
|
+
}
|
|
3264
|
+
const snapshot = await this.readForkStateSnapshot(
|
|
3265
|
+
// Skip the root when we've already pre-filtered it — avoid both a
|
|
3266
|
+
// wasted HEAD read of main and a re-population that would clobber
|
|
3267
|
+
// the filtered entries.
|
|
3268
|
+
preFilteredRoot ? effectiveSubtree.filter((entity) => entity.url !== sourceRoot.url) : effectiveSubtree
|
|
3269
|
+
);
|
|
3270
|
+
if (preFilteredRoot) {
|
|
3271
|
+
snapshot.manifestsByEntity.set(sourceRoot.url, preFilteredRoot.manifests);
|
|
3272
|
+
snapshot.childStatusesByEntity.set(sourceRoot.url, preFilteredRoot.childStatuses);
|
|
3273
|
+
snapshot.replayWatermarksByEntity.set(sourceRoot.url, preFilteredRoot.replayWatermarks);
|
|
3274
|
+
for (const id of preFilteredRoot.sharedStateIds) snapshot.sharedStateIds.add(id);
|
|
3275
|
+
}
|
|
3094
3276
|
const suffix = (0, node_crypto.randomUUID)().slice(0, 8);
|
|
3095
|
-
const entityUrlMap = await this.buildForkEntityUrlMap(
|
|
3277
|
+
const entityUrlMap = await this.buildForkEntityUrlMap(effectiveSubtree, {
|
|
3096
3278
|
suffix,
|
|
3097
3279
|
rootUrl,
|
|
3098
3280
|
rootInstanceId: opts.rootInstanceId
|
|
3099
3281
|
});
|
|
3100
3282
|
const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
|
|
3101
3283
|
const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
|
|
3102
|
-
const entityPlans = this.buildForkEntityPlans(
|
|
3103
|
-
this.addForkLocks(this.forkWriteLockedEntities,
|
|
3284
|
+
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap);
|
|
3285
|
+
this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
|
|
3104
3286
|
this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(id)), writeStreamLocks);
|
|
3105
3287
|
const createdStreams = [];
|
|
3106
3288
|
const createdEntities = [];
|
|
3107
3289
|
const activeManifestsByEntity = new Map();
|
|
3108
3290
|
try {
|
|
3109
3291
|
for (const plan of entityPlans) {
|
|
3110
|
-
|
|
3292
|
+
const isRoot = plan.source.url === rootUrl;
|
|
3293
|
+
await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
|
|
3111
3294
|
createdStreams.push(plan.fork.streams.main);
|
|
3112
3295
|
await this.streamClient.fork(plan.fork.streams.error, plan.source.streams.error);
|
|
3113
3296
|
createdStreams.push(plan.fork.streams.error);
|
|
@@ -3200,6 +3383,38 @@ var EntityManager = class {
|
|
|
3200
3383
|
}
|
|
3201
3384
|
held.clear();
|
|
3202
3385
|
}
|
|
3386
|
+
/**
|
|
3387
|
+
* Variant of {@link waitForIdleSubtree} that takes an explicit entity
|
|
3388
|
+
* list instead of walking the registry from `rootUrl`. Used by the
|
|
3389
|
+
* pointer-fork path to wait+lock only the kept descendants, since
|
|
3390
|
+
* the root is being forked from history and doesn't need to be idle.
|
|
3391
|
+
*/
|
|
3392
|
+
async waitForGivenEntitiesIdle(entities$1, opts, workLocks) {
|
|
3393
|
+
if (entities$1.length === 0) return;
|
|
3394
|
+
const timeoutMs = opts.waitTimeoutMs ?? DEFAULT_FORK_WAIT_TIMEOUT_MS;
|
|
3395
|
+
const pollMs = opts.waitPollMs ?? DEFAULT_FORK_WAIT_POLL_MS;
|
|
3396
|
+
const refresh = async () => {
|
|
3397
|
+
const refreshed = await Promise.all(entities$1.map((entity) => this.registry.getEntity(entity.url)));
|
|
3398
|
+
return refreshed.filter((entity) => !!entity);
|
|
3399
|
+
};
|
|
3400
|
+
const deadline = Date.now() + timeoutMs;
|
|
3401
|
+
while (true) {
|
|
3402
|
+
const present = await refresh();
|
|
3403
|
+
const stopped = present.find((entity) => isTerminalEntityStatus(entity.status));
|
|
3404
|
+
if (stopped) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${stopped.url}"`, 409);
|
|
3405
|
+
let active = present.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3406
|
+
if (active.length === 0) {
|
|
3407
|
+
this.addForkLocks(this.forkWorkLockedEntities, present.map((entity) => entity.url), workLocks);
|
|
3408
|
+
const reChecked = await refresh();
|
|
3409
|
+
const reActive = reChecked.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3410
|
+
if (reActive.length === 0) return;
|
|
3411
|
+
this.releaseForkLocks(this.forkWorkLockedEntities, workLocks);
|
|
3412
|
+
active = reActive;
|
|
3413
|
+
}
|
|
3414
|
+
if (Date.now() >= deadline) throw new ElectricAgentsError(ErrCodeForkWaitTimeout, `Timed out waiting for descendants to become idle`, 409, { active: active.map((entity) => entity.url) });
|
|
3415
|
+
await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3203
3418
|
async waitForIdleSubtree(rootUrl, opts, workLocks) {
|
|
3204
3419
|
const timeoutMs = opts.waitTimeoutMs ?? DEFAULT_FORK_WAIT_TIMEOUT_MS;
|
|
3205
3420
|
const pollMs = opts.waitPollMs ?? DEFAULT_FORK_WAIT_POLL_MS;
|
|
@@ -3229,6 +3444,73 @@ var EntityManager = class {
|
|
|
3229
3444
|
await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
|
|
3230
3445
|
}
|
|
3231
3446
|
}
|
|
3447
|
+
/**
|
|
3448
|
+
* Translate `forkPointer` into a 1-indexed CUMULATIVE position in the
|
|
3449
|
+
* source's flattened history. Throws a 400 if the pointer doesn't
|
|
3450
|
+
* address a real event.
|
|
3451
|
+
*
|
|
3452
|
+
* Semantics (mirroring the durable-streams server interpretation):
|
|
3453
|
+
* `{ offset: X, subOffset: N }` means "from anchor X, take N flattened
|
|
3454
|
+
* messages forward." Concretely, the target event is the N-th event
|
|
3455
|
+
* after the last event whose `headers.offset` is ≤ X. (When `X` is
|
|
3456
|
+
* `null`, the anchor is the stream start and the target is the N-th
|
|
3457
|
+
* event from the very beginning.) The returned position is the count
|
|
3458
|
+
* of events to KEEP — events 1..position survive the filter.
|
|
3459
|
+
*
|
|
3460
|
+
* A pointer is valid when:
|
|
3461
|
+
* - `pointer.offset` is `null` (stream start) OR matches some
|
|
3462
|
+
* event's `headers.offset` value, AND
|
|
3463
|
+
* - `pointer.subOffset` is in `[1, total events past the anchor]`.
|
|
3464
|
+
*/
|
|
3465
|
+
resolveForkPointerTarget(events, pointer, streamPath) {
|
|
3466
|
+
let positionAtAnchor = 0;
|
|
3467
|
+
let anchorSeen = pointer.offset === null;
|
|
3468
|
+
for (const event of events) {
|
|
3469
|
+
const headers = isRecord(event.headers) ? event.headers : void 0;
|
|
3470
|
+
const eventOffset = typeof headers?.offset === `string` ? headers.offset : void 0;
|
|
3471
|
+
if (eventOffset === void 0) continue;
|
|
3472
|
+
if (pointer.offset === null) continue;
|
|
3473
|
+
if (eventOffset === pointer.offset) anchorSeen = true;
|
|
3474
|
+
if (eventOffset <= pointer.offset) positionAtAnchor++;
|
|
3475
|
+
}
|
|
3476
|
+
if (!anchorSeen) throw new ElectricAgentsError(ErrCodeInvalidRequest, `fork_pointer.offset (${pointer.offset ?? `<stream-start>`}) does not match any event's Stream-Next-Offset on ${streamPath}`, 400);
|
|
3477
|
+
const eventsPastAnchor = events.length - positionAtAnchor;
|
|
3478
|
+
if (pointer.subOffset < 1 || pointer.subOffset > eventsPastAnchor) throw new ElectricAgentsError(ErrCodeInvalidRequest, `fork_pointer.sub_offset ${pointer.subOffset} out of range past anchor on ${streamPath} (valid: 1..${eventsPastAnchor})`, 400);
|
|
3479
|
+
return positionAtAnchor + pointer.subOffset;
|
|
3480
|
+
}
|
|
3481
|
+
/**
|
|
3482
|
+
* Compute the subset of `sourceTree` that survives the manifest filter
|
|
3483
|
+
* applied at the root. After filtering the root's manifest at the fork
|
|
3484
|
+
* pointer, only children whose manifest entries landed at or before the
|
|
3485
|
+
* pointer remain; those kept children carry their CURRENT (HEAD) subtree
|
|
3486
|
+
* along with them. Children dropped from the root's manifest, and any
|
|
3487
|
+
* of their descendants, are excluded.
|
|
3488
|
+
*/
|
|
3489
|
+
computeEffectiveSubtree(sourceTree, rootUrl, filteredRootManifests) {
|
|
3490
|
+
const keptChildUrls = new Set();
|
|
3491
|
+
for (const value of filteredRootManifests.values()) if (value.kind === `child` && typeof value.entity_url === `string`) keptChildUrls.add(value.entity_url);
|
|
3492
|
+
const childrenByParent = new Map();
|
|
3493
|
+
for (const entity of sourceTree) {
|
|
3494
|
+
if (!entity.parent) continue;
|
|
3495
|
+
const list = childrenByParent.get(entity.parent) ?? [];
|
|
3496
|
+
list.push(entity);
|
|
3497
|
+
childrenByParent.set(entity.parent, list);
|
|
3498
|
+
}
|
|
3499
|
+
const rootEntity = sourceTree.find((e) => e.url === rootUrl);
|
|
3500
|
+
if (!rootEntity) return [];
|
|
3501
|
+
const result = [rootEntity];
|
|
3502
|
+
const queue = [];
|
|
3503
|
+
for (const child of childrenByParent.get(rootUrl) ?? []) if (keptChildUrls.has(child.url)) queue.push(child);
|
|
3504
|
+
const seen = new Set([rootUrl]);
|
|
3505
|
+
while (queue.length > 0) {
|
|
3506
|
+
const entity = queue.shift();
|
|
3507
|
+
if (seen.has(entity.url)) continue;
|
|
3508
|
+
seen.add(entity.url);
|
|
3509
|
+
result.push(entity);
|
|
3510
|
+
for (const grandchild of childrenByParent.get(entity.url) ?? []) if (!seen.has(grandchild.url)) queue.push(grandchild);
|
|
3511
|
+
}
|
|
3512
|
+
return result;
|
|
3513
|
+
}
|
|
3232
3514
|
async listEntitySubtree(root) {
|
|
3233
3515
|
const result = [];
|
|
3234
3516
|
const queue = [root];
|
|
@@ -4336,6 +4618,7 @@ var EntityManager = class {
|
|
|
4336
4618
|
streams: entity.streams,
|
|
4337
4619
|
tags: entity.tags,
|
|
4338
4620
|
spawnArgs: entity.spawn_args,
|
|
4621
|
+
sandbox: entity.sandbox,
|
|
4339
4622
|
createdBy: entity.created_by
|
|
4340
4623
|
},
|
|
4341
4624
|
principal: principalFromCreatedBy(entity.created_by),
|
|
@@ -6001,11 +6284,19 @@ var WakeRegistry = class {
|
|
|
6001
6284
|
}
|
|
6002
6285
|
const kind = operation === `delete` ? `delete` : operation === `update` ? `update` : `insert`;
|
|
6003
6286
|
if (condition.ops && condition.ops.length > 0 && !condition.ops.includes(kind)) return null;
|
|
6004
|
-
|
|
6287
|
+
const change = {
|
|
6005
6288
|
collection: eventType,
|
|
6006
6289
|
kind,
|
|
6007
6290
|
key: event.key || ``
|
|
6008
|
-
}
|
|
6291
|
+
};
|
|
6292
|
+
if (eventType === `inbox`) {
|
|
6293
|
+
const value = event.value;
|
|
6294
|
+
if (typeof value?.from === `string`) change.from = value.from;
|
|
6295
|
+
if (`payload` in (value ?? {})) change.payload = value?.payload;
|
|
6296
|
+
if (typeof value?.timestamp === `string`) change.timestamp = value.timestamp;
|
|
6297
|
+
if (typeof value?.message_type === `string`) change.message_type = value.message_type;
|
|
6298
|
+
}
|
|
6299
|
+
return { change };
|
|
6009
6300
|
}
|
|
6010
6301
|
};
|
|
6011
6302
|
|
|
@@ -6412,13 +6703,13 @@ function buildElectricProxyTarget(options) {
|
|
|
6412
6703
|
if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
|
|
6413
6704
|
const table = options.incomingUrl.searchParams.get(`table`);
|
|
6414
6705
|
if (table === `entities`) {
|
|
6415
|
-
target.searchParams.set(`columns`, `"tenant_id","url","type","status","dispatch_policy","tags","spawn_args","parent","type_revision","inbox_schemas","state_schemas","created_at","updated_at"`);
|
|
6706
|
+
target.searchParams.set(`columns`, `"tenant_id","url","type","status","dispatch_policy","tags","spawn_args","sandbox","parent","type_revision","inbox_schemas","state_schemas","created_at","updated_at"`);
|
|
6416
6707
|
applyTenantShapeWhere(target, options.tenantId);
|
|
6417
6708
|
} else if (table === `entity_types`) {
|
|
6418
6709
|
target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
|
|
6419
6710
|
applyTenantShapeWhere(target, options.tenantId);
|
|
6420
6711
|
} else if (table === `runners`) {
|
|
6421
|
-
target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","created_at","updated_at"`);
|
|
6712
|
+
target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`);
|
|
6422
6713
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
|
|
6423
6714
|
} else if (table === `runner_runtime_diagnostics`) {
|
|
6424
6715
|
target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
|
|
@@ -6844,6 +7135,28 @@ async function proxyElectric(request, ctx) {
|
|
|
6844
7135
|
});
|
|
6845
7136
|
}
|
|
6846
7137
|
|
|
7138
|
+
//#endregion
|
|
7139
|
+
//#region src/sandbox-choice-schema.ts
|
|
7140
|
+
/**
|
|
7141
|
+
* Wire schema for a spawn-time sandbox CHOICE (the request input), as opposed to
|
|
7142
|
+
* the resolved {@link import('./electric-agents-types.js').EntitySandboxSelection}
|
|
7143
|
+
* persisted on the entity. The matching `SandboxChoice` type is hand-maintained
|
|
7144
|
+
* in `electric-agents-types.ts` — mirrors how `dispatchPolicySchema` pairs with
|
|
7145
|
+
* the `DispatchPolicy` type in `dispatch-policy-schema.ts`.
|
|
7146
|
+
*
|
|
7147
|
+
* Validation happens once, at the router boundary (this schema is embedded in
|
|
7148
|
+
* the spawn body schema); the spawn resolver consumes already-validated input,
|
|
7149
|
+
* so there is intentionally no separate `parse` helper here.
|
|
7150
|
+
*/
|
|
7151
|
+
const sandboxChoiceSchema = __sinclair_typebox.Type.Object({
|
|
7152
|
+
profile: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7153
|
+
key: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7154
|
+
scope: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`entity`), __sinclair_typebox.Type.Literal(`wake`)])),
|
|
7155
|
+
persistent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
7156
|
+
owner: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
7157
|
+
inherit: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean())
|
|
7158
|
+
});
|
|
7159
|
+
|
|
6847
7160
|
//#endregion
|
|
6848
7161
|
//#region src/routing/entities-router.ts
|
|
6849
7162
|
const stringRecordSchema$1 = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
|
|
@@ -6866,6 +7179,7 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6866
7179
|
tags: __sinclair_typebox.Type.Optional(stringRecordSchema$1),
|
|
6867
7180
|
parent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6868
7181
|
dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
|
|
7182
|
+
sandbox: __sinclair_typebox.Type.Optional(sandboxChoiceSchema),
|
|
6869
7183
|
initialMessage: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
6870
7184
|
wake: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Object({
|
|
6871
7185
|
subscriberUrl: __sinclair_typebox.Type.String(),
|
|
@@ -6907,7 +7221,11 @@ const inboxMessageBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6907
7221
|
});
|
|
6908
7222
|
const forkBodySchema = __sinclair_typebox.Type.Object({
|
|
6909
7223
|
instance_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6910
|
-
waitTimeoutMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number())
|
|
7224
|
+
waitTimeoutMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
7225
|
+
fork_pointer: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Object({
|
|
7226
|
+
offset: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Null()]),
|
|
7227
|
+
sub_offset: __sinclair_typebox.Type.Number()
|
|
7228
|
+
}))
|
|
6911
7229
|
});
|
|
6912
7230
|
const setTagBodySchema = __sinclair_typebox.Type.Object({ value: __sinclair_typebox.Type.String() });
|
|
6913
7231
|
const entitySignalSchema = __sinclair_typebox.Type.Union([
|
|
@@ -7225,7 +7543,11 @@ async function forkEntity(request, ctx) {
|
|
|
7225
7543
|
await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
|
|
7226
7544
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
7227
7545
|
rootInstanceId: parsed.instance_id,
|
|
7228
|
-
waitTimeoutMs: parsed.waitTimeoutMs
|
|
7546
|
+
waitTimeoutMs: parsed.waitTimeoutMs,
|
|
7547
|
+
...parsed.fork_pointer && { forkPointer: {
|
|
7548
|
+
offset: parsed.fork_pointer.offset,
|
|
7549
|
+
subOffset: parsed.fork_pointer.sub_offset
|
|
7550
|
+
} }
|
|
7229
7551
|
});
|
|
7230
7552
|
for (const forkedEntity of result.entities) await linkEntityDispatchSubscription(ctx, forkedEntity);
|
|
7231
7553
|
return (0, itty_router.json)({
|
|
@@ -7323,6 +7645,7 @@ async function spawnEntity(request, ctx) {
|
|
|
7323
7645
|
tags: parsed.tags,
|
|
7324
7646
|
parent: parsed.parent,
|
|
7325
7647
|
dispatch_policy: dispatchPolicy,
|
|
7648
|
+
sandbox: parsed.sandbox,
|
|
7326
7649
|
initialMessage: void 0,
|
|
7327
7650
|
wake: parsed.wake,
|
|
7328
7651
|
created_by: principal.url
|
|
@@ -7577,6 +7900,12 @@ function withLeadingSlash(path$2) {
|
|
|
7577
7900
|
|
|
7578
7901
|
//#endregion
|
|
7579
7902
|
//#region src/routing/runners-router.ts
|
|
7903
|
+
const sandboxProfileBodySchema = __sinclair_typebox.Type.Object({
|
|
7904
|
+
name: __sinclair_typebox.Type.String(),
|
|
7905
|
+
label: __sinclair_typebox.Type.String(),
|
|
7906
|
+
description: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7907
|
+
remote: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean())
|
|
7908
|
+
});
|
|
7580
7909
|
const registerRunnerBodySchema = __sinclair_typebox.Type.Object({
|
|
7581
7910
|
id: __sinclair_typebox.Type.String(),
|
|
7582
7911
|
owner_principal: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -7589,7 +7918,8 @@ const registerRunnerBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7589
7918
|
__sinclair_typebox.Type.Literal(`server`)
|
|
7590
7919
|
])),
|
|
7591
7920
|
admin_status: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`enabled`), __sinclair_typebox.Type.Literal(`disabled`)])),
|
|
7592
|
-
wake_stream: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
7921
|
+
wake_stream: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7922
|
+
sandbox_profiles: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(sandboxProfileBodySchema))
|
|
7593
7923
|
});
|
|
7594
7924
|
const heartbeatBodySchema = __sinclair_typebox.Type.Object({
|
|
7595
7925
|
lease_ms: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
@@ -7687,7 +8017,8 @@ async function registerRunner(request, ctx) {
|
|
|
7687
8017
|
label: parsed.label,
|
|
7688
8018
|
kind: parsed.kind,
|
|
7689
8019
|
adminStatus: parsed.admin_status,
|
|
7690
|
-
wakeStream: parsed.wake_stream
|
|
8020
|
+
wakeStream: parsed.wake_stream,
|
|
8021
|
+
sandboxProfiles: parsed.sandbox_profiles
|
|
7691
8022
|
});
|
|
7692
8023
|
await ctx.streamClient.ensure(runner.wake_stream, { contentType: `application/json` });
|
|
7693
8024
|
return (0, itty_router.json)(runner, { status: 201 });
|
|
@@ -7917,6 +8248,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7917
8248
|
streams: entity.streams,
|
|
7918
8249
|
tags: entity.tags,
|
|
7919
8250
|
spawnArgs: entity.spawn_args,
|
|
8251
|
+
sandbox: entity.sandbox,
|
|
7920
8252
|
createdBy: entity.created_by
|
|
7921
8253
|
},
|
|
7922
8254
|
principal: principalFromCreatedBy(entity.created_by)
|