@electric-ax/agents-server 0.4.13 → 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 +394 -51
- package/dist/index.cjs +396 -51
- package/dist/index.d.cts +332 -164
- package/dist/index.d.ts +332 -164
- package/dist/index.js +396 -51
- package/drizzle/0010_sandbox_profiles.sql +5 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -6
- 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/log.ts +63 -52
- 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
|
};
|
|
@@ -1218,35 +1256,48 @@ const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
|
|
|
1218
1256
|
const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
|
|
1219
1257
|
const USE_FILE_LOGS = process.env.ELECTRIC_AGENTS_LOG_FILE !== `false`;
|
|
1220
1258
|
const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST && !IS_ELECTRON_MAIN;
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if (
|
|
1224
|
-
const streams = [];
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1259
|
+
let _logger;
|
|
1260
|
+
function getLogger() {
|
|
1261
|
+
if (_logger) return _logger;
|
|
1262
|
+
const streams = [];
|
|
1263
|
+
try {
|
|
1264
|
+
if (USE_FILE_LOGS) {
|
|
1265
|
+
const logDir = process.env.ELECTRIC_AGENTS_LOG_DIR ?? node_path.default.resolve(process.cwd(), `logs`);
|
|
1266
|
+
node_fs.default.mkdirSync(logDir, { recursive: true });
|
|
1267
|
+
const logFile = node_path.default.join(logDir, `agent-server-${Date.now()}.jsonl`);
|
|
1268
|
+
streams.push({ stream: pino.default.destination({
|
|
1269
|
+
dest: logFile,
|
|
1270
|
+
sync: IS_ELECTRON_MAIN
|
|
1271
|
+
}) });
|
|
1272
|
+
}
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
process.stderr.write(`[agents-server] Failed to initialize file logging: ${err instanceof Error ? err.message : err}\n`);
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
if (USE_PRETTY_LOGS) streams.push({ stream: pino.default.transport({
|
|
1278
|
+
target: `pino-pretty`,
|
|
1279
|
+
options: {
|
|
1280
|
+
colorize: true,
|
|
1281
|
+
ignore: `pid,hostname,name`,
|
|
1282
|
+
translateTime: `SYS:HH:MM:ss`
|
|
1283
|
+
}
|
|
1284
|
+
}) });
|
|
1285
|
+
} catch {}
|
|
1286
|
+
_logger = streams.length > 0 ? (0, pino.default)({
|
|
1287
|
+
base: void 0,
|
|
1288
|
+
level: LOG_LEVEL
|
|
1289
|
+
}, pino.default.multistream(streams)) : (0, pino.default)({
|
|
1290
|
+
base: void 0,
|
|
1291
|
+
enabled: false,
|
|
1292
|
+
level: LOG_LEVEL
|
|
1293
|
+
});
|
|
1294
|
+
return _logger;
|
|
1295
|
+
}
|
|
1245
1296
|
function formatArgs(args) {
|
|
1246
1297
|
const errors = [];
|
|
1247
1298
|
const parts = [];
|
|
1248
|
-
for (const
|
|
1249
|
-
else parts.push(typeof
|
|
1299
|
+
for (const value of args) if (value instanceof Error) errors.push(value);
|
|
1300
|
+
else parts.push(typeof value === `string` ? value : JSON.stringify(value));
|
|
1250
1301
|
return {
|
|
1251
1302
|
err: errors[0],
|
|
1252
1303
|
msg: parts.join(` `)
|
|
@@ -1255,20 +1306,20 @@ function formatArgs(args) {
|
|
|
1255
1306
|
const serverLog = {
|
|
1256
1307
|
info(...args) {
|
|
1257
1308
|
const { msg } = formatArgs(args);
|
|
1258
|
-
|
|
1309
|
+
getLogger().info(msg);
|
|
1259
1310
|
},
|
|
1260
1311
|
warn(...args) {
|
|
1261
1312
|
const { err, msg } = formatArgs(args);
|
|
1262
|
-
if (err)
|
|
1263
|
-
else
|
|
1313
|
+
if (err) getLogger().warn({ err }, msg);
|
|
1314
|
+
else getLogger().warn(msg);
|
|
1264
1315
|
},
|
|
1265
1316
|
error(...args) {
|
|
1266
1317
|
const { err, msg } = formatArgs(args);
|
|
1267
|
-
if (err)
|
|
1268
|
-
else
|
|
1318
|
+
if (err) getLogger().error({ err }, msg);
|
|
1319
|
+
else getLogger().error(msg);
|
|
1269
1320
|
},
|
|
1270
1321
|
event(obj, msg) {
|
|
1271
|
-
|
|
1322
|
+
getLogger().info(obj, msg);
|
|
1272
1323
|
}
|
|
1273
1324
|
};
|
|
1274
1325
|
|
|
@@ -1281,6 +1332,7 @@ const ENTITY_SHAPE_COLUMNS = [
|
|
|
1281
1332
|
`status`,
|
|
1282
1333
|
`tags`,
|
|
1283
1334
|
`spawn_args`,
|
|
1335
|
+
`sandbox`,
|
|
1284
1336
|
`parent`,
|
|
1285
1337
|
`type_revision`,
|
|
1286
1338
|
`inbox_schemas`,
|
|
@@ -1314,6 +1366,7 @@ function toMemberRow(entity) {
|
|
|
1314
1366
|
status: entity.status,
|
|
1315
1367
|
tags: entity.tags,
|
|
1316
1368
|
spawn_args: entity.spawn_args ?? {},
|
|
1369
|
+
sandbox: entity.sandbox ?? null,
|
|
1317
1370
|
parent: entity.parent ?? null,
|
|
1318
1371
|
type_revision: entity.type_revision ?? null,
|
|
1319
1372
|
inbox_schemas: entity.inbox_schemas ?? null,
|
|
@@ -1994,7 +2047,7 @@ var StreamClient = class {
|
|
|
1994
2047
|
});
|
|
1995
2048
|
});
|
|
1996
2049
|
}
|
|
1997
|
-
async fork(path$2, sourcePath) {
|
|
2050
|
+
async fork(path$2, sourcePath, opts) {
|
|
1998
2051
|
return await withSpan(`stream.fork`, async (span) => {
|
|
1999
2052
|
span.setAttributes({
|
|
2000
2053
|
[ATTR.STREAM_PATH]: path$2,
|
|
@@ -2004,6 +2057,11 @@ var StreamClient = class {
|
|
|
2004
2057
|
"content-type": `application/json`,
|
|
2005
2058
|
"Stream-Forked-From": new URL(this.streamUrl(sourcePath)).pathname
|
|
2006
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
|
+
}
|
|
2007
2065
|
injectTraceHeaders(headers);
|
|
2008
2066
|
const response = await fetch(this.streamUrl(path$2), {
|
|
2009
2067
|
method: `PUT`,
|
|
@@ -2248,11 +2306,11 @@ var StreamClient = class {
|
|
|
2248
2306
|
if (res.status === 404 || res.status === 204) return;
|
|
2249
2307
|
if (!res.ok) throw new Error(`Subscription delete failed: ${res.status} ${await res.text()}`);
|
|
2250
2308
|
}
|
|
2251
|
-
async addSubscriptionStreams(subscriptionId, streams
|
|
2309
|
+
async addSubscriptionStreams(subscriptionId, streams) {
|
|
2252
2310
|
const res = await fetch(this.subscriptionChildUrl(subscriptionId, `streams`), {
|
|
2253
2311
|
method: `POST`,
|
|
2254
2312
|
headers: await this.requestHeaders({ "content-type": `application/json` }),
|
|
2255
|
-
body: JSON.stringify({ streams: streams
|
|
2313
|
+
body: JSON.stringify({ streams: streams.map((stream) => this.backendSubscriptionPath(normalizeSubscriptionStreamPath(stream))) })
|
|
2256
2314
|
});
|
|
2257
2315
|
return await this.subscriptionJson(res, `Subscription stream add failed`);
|
|
2258
2316
|
}
|
|
@@ -2531,6 +2589,103 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
|
|
|
2531
2589
|
});
|
|
2532
2590
|
}
|
|
2533
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
|
+
|
|
2534
2689
|
//#endregion
|
|
2535
2690
|
//#region src/principal.ts
|
|
2536
2691
|
const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
|
|
@@ -2931,6 +3086,7 @@ var EntityManager = class {
|
|
|
2931
3086
|
if (!parentEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Parent entity "${req.parent}" not found`, 404);
|
|
2932
3087
|
}
|
|
2933
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);
|
|
2934
3090
|
const now = Date.now();
|
|
2935
3091
|
const entityData = {
|
|
2936
3092
|
type: typeName,
|
|
@@ -2945,6 +3101,7 @@ var EntityManager = class {
|
|
|
2945
3101
|
write_token: writeToken,
|
|
2946
3102
|
tags: initialTags,
|
|
2947
3103
|
spawn_args: req.args,
|
|
3104
|
+
sandbox,
|
|
2948
3105
|
type_revision: entityType.revision,
|
|
2949
3106
|
inbox_schemas: entityType.inbox_schemas,
|
|
2950
3107
|
state_schemas: entityType.state_schemas,
|
|
@@ -3074,27 +3231,66 @@ var EntityManager = class {
|
|
|
3074
3231
|
const writeEntityLocks = new Set();
|
|
3075
3232
|
const writeStreamLocks = new Set();
|
|
3076
3233
|
try {
|
|
3077
|
-
|
|
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);
|
|
3078
3241
|
const sourceRoot = sourceTree[0];
|
|
3079
3242
|
if (sourceRoot.parent) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Only top-level entities can be forked`, 400);
|
|
3080
|
-
|
|
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
|
+
}
|
|
3081
3276
|
const suffix = (0, node_crypto.randomUUID)().slice(0, 8);
|
|
3082
|
-
const entityUrlMap = await this.buildForkEntityUrlMap(
|
|
3277
|
+
const entityUrlMap = await this.buildForkEntityUrlMap(effectiveSubtree, {
|
|
3083
3278
|
suffix,
|
|
3084
3279
|
rootUrl,
|
|
3085
3280
|
rootInstanceId: opts.rootInstanceId
|
|
3086
3281
|
});
|
|
3087
3282
|
const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
|
|
3088
3283
|
const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
|
|
3089
|
-
const entityPlans = this.buildForkEntityPlans(
|
|
3090
|
-
this.addForkLocks(this.forkWriteLockedEntities,
|
|
3284
|
+
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap);
|
|
3285
|
+
this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
|
|
3091
3286
|
this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(id)), writeStreamLocks);
|
|
3092
3287
|
const createdStreams = [];
|
|
3093
3288
|
const createdEntities = [];
|
|
3094
3289
|
const activeManifestsByEntity = new Map();
|
|
3095
3290
|
try {
|
|
3096
3291
|
for (const plan of entityPlans) {
|
|
3097
|
-
|
|
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);
|
|
3098
3294
|
createdStreams.push(plan.fork.streams.main);
|
|
3099
3295
|
await this.streamClient.fork(plan.fork.streams.error, plan.source.streams.error);
|
|
3100
3296
|
createdStreams.push(plan.fork.streams.error);
|
|
@@ -3187,6 +3383,38 @@ var EntityManager = class {
|
|
|
3187
3383
|
}
|
|
3188
3384
|
held.clear();
|
|
3189
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
|
+
}
|
|
3190
3418
|
async waitForIdleSubtree(rootUrl, opts, workLocks) {
|
|
3191
3419
|
const timeoutMs = opts.waitTimeoutMs ?? DEFAULT_FORK_WAIT_TIMEOUT_MS;
|
|
3192
3420
|
const pollMs = opts.waitPollMs ?? DEFAULT_FORK_WAIT_POLL_MS;
|
|
@@ -3216,6 +3444,73 @@ var EntityManager = class {
|
|
|
3216
3444
|
await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
|
|
3217
3445
|
}
|
|
3218
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
|
+
}
|
|
3219
3514
|
async listEntitySubtree(root) {
|
|
3220
3515
|
const result = [];
|
|
3221
3516
|
const queue = [root];
|
|
@@ -4323,6 +4618,7 @@ var EntityManager = class {
|
|
|
4323
4618
|
streams: entity.streams,
|
|
4324
4619
|
tags: entity.tags,
|
|
4325
4620
|
spawnArgs: entity.spawn_args,
|
|
4621
|
+
sandbox: entity.sandbox,
|
|
4326
4622
|
createdBy: entity.created_by
|
|
4327
4623
|
},
|
|
4328
4624
|
principal: principalFromCreatedBy(entity.created_by),
|
|
@@ -5988,11 +6284,19 @@ var WakeRegistry = class {
|
|
|
5988
6284
|
}
|
|
5989
6285
|
const kind = operation === `delete` ? `delete` : operation === `update` ? `update` : `insert`;
|
|
5990
6286
|
if (condition.ops && condition.ops.length > 0 && !condition.ops.includes(kind)) return null;
|
|
5991
|
-
|
|
6287
|
+
const change = {
|
|
5992
6288
|
collection: eventType,
|
|
5993
6289
|
kind,
|
|
5994
6290
|
key: event.key || ``
|
|
5995
|
-
}
|
|
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 };
|
|
5996
6300
|
}
|
|
5997
6301
|
};
|
|
5998
6302
|
|
|
@@ -6399,13 +6703,13 @@ function buildElectricProxyTarget(options) {
|
|
|
6399
6703
|
if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
|
|
6400
6704
|
const table = options.incomingUrl.searchParams.get(`table`);
|
|
6401
6705
|
if (table === `entities`) {
|
|
6402
|
-
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"`);
|
|
6403
6707
|
applyTenantShapeWhere(target, options.tenantId);
|
|
6404
6708
|
} else if (table === `entity_types`) {
|
|
6405
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"`);
|
|
6406
6710
|
applyTenantShapeWhere(target, options.tenantId);
|
|
6407
6711
|
} else if (table === `runners`) {
|
|
6408
|
-
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"`);
|
|
6409
6713
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
|
|
6410
6714
|
} else if (table === `runner_runtime_diagnostics`) {
|
|
6411
6715
|
target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
|
|
@@ -6831,6 +7135,28 @@ async function proxyElectric(request, ctx) {
|
|
|
6831
7135
|
});
|
|
6832
7136
|
}
|
|
6833
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
|
+
|
|
6834
7160
|
//#endregion
|
|
6835
7161
|
//#region src/routing/entities-router.ts
|
|
6836
7162
|
const stringRecordSchema$1 = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
|
|
@@ -6853,6 +7179,7 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6853
7179
|
tags: __sinclair_typebox.Type.Optional(stringRecordSchema$1),
|
|
6854
7180
|
parent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6855
7181
|
dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
|
|
7182
|
+
sandbox: __sinclair_typebox.Type.Optional(sandboxChoiceSchema),
|
|
6856
7183
|
initialMessage: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
6857
7184
|
wake: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Object({
|
|
6858
7185
|
subscriberUrl: __sinclair_typebox.Type.String(),
|
|
@@ -6894,7 +7221,11 @@ const inboxMessageBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6894
7221
|
});
|
|
6895
7222
|
const forkBodySchema = __sinclair_typebox.Type.Object({
|
|
6896
7223
|
instance_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6897
|
-
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
|
+
}))
|
|
6898
7229
|
});
|
|
6899
7230
|
const setTagBodySchema = __sinclair_typebox.Type.Object({ value: __sinclair_typebox.Type.String() });
|
|
6900
7231
|
const entitySignalSchema = __sinclair_typebox.Type.Union([
|
|
@@ -7212,7 +7543,11 @@ async function forkEntity(request, ctx) {
|
|
|
7212
7543
|
await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
|
|
7213
7544
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
7214
7545
|
rootInstanceId: parsed.instance_id,
|
|
7215
|
-
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
|
+
} }
|
|
7216
7551
|
});
|
|
7217
7552
|
for (const forkedEntity of result.entities) await linkEntityDispatchSubscription(ctx, forkedEntity);
|
|
7218
7553
|
return (0, itty_router.json)({
|
|
@@ -7310,6 +7645,7 @@ async function spawnEntity(request, ctx) {
|
|
|
7310
7645
|
tags: parsed.tags,
|
|
7311
7646
|
parent: parsed.parent,
|
|
7312
7647
|
dispatch_policy: dispatchPolicy,
|
|
7648
|
+
sandbox: parsed.sandbox,
|
|
7313
7649
|
initialMessage: void 0,
|
|
7314
7650
|
wake: parsed.wake,
|
|
7315
7651
|
created_by: principal.url
|
|
@@ -7564,6 +7900,12 @@ function withLeadingSlash(path$2) {
|
|
|
7564
7900
|
|
|
7565
7901
|
//#endregion
|
|
7566
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
|
+
});
|
|
7567
7909
|
const registerRunnerBodySchema = __sinclair_typebox.Type.Object({
|
|
7568
7910
|
id: __sinclair_typebox.Type.String(),
|
|
7569
7911
|
owner_principal: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -7576,7 +7918,8 @@ const registerRunnerBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7576
7918
|
__sinclair_typebox.Type.Literal(`server`)
|
|
7577
7919
|
])),
|
|
7578
7920
|
admin_status: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`enabled`), __sinclair_typebox.Type.Literal(`disabled`)])),
|
|
7579
|
-
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))
|
|
7580
7923
|
});
|
|
7581
7924
|
const heartbeatBodySchema = __sinclair_typebox.Type.Object({
|
|
7582
7925
|
lease_ms: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
@@ -7674,7 +8017,8 @@ async function registerRunner(request, ctx) {
|
|
|
7674
8017
|
label: parsed.label,
|
|
7675
8018
|
kind: parsed.kind,
|
|
7676
8019
|
adminStatus: parsed.admin_status,
|
|
7677
|
-
wakeStream: parsed.wake_stream
|
|
8020
|
+
wakeStream: parsed.wake_stream,
|
|
8021
|
+
sandboxProfiles: parsed.sandbox_profiles
|
|
7678
8022
|
});
|
|
7679
8023
|
await ctx.streamClient.ensure(runner.wake_stream, { contentType: `application/json` });
|
|
7680
8024
|
return (0, itty_router.json)(runner, { status: 201 });
|
|
@@ -7884,7 +8228,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7884
8228
|
leaseExpiresAt: input.claim.lease_ttl_ms ? new Date(Date.now() + input.claim.lease_ttl_ms) : void 0
|
|
7885
8229
|
});
|
|
7886
8230
|
await ctx.entityManager.registry.updateStatus(entity.url, `running`);
|
|
7887
|
-
const streams
|
|
8231
|
+
const streams = input.claim.streams.map((stream) => ({
|
|
7888
8232
|
path: withLeadingSlash(stream.path),
|
|
7889
8233
|
offset: stream.tail_offset ?? ``
|
|
7890
8234
|
}));
|
|
@@ -7893,7 +8237,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7893
8237
|
epoch: input.claim.generation,
|
|
7894
8238
|
wakeId: input.claim.wake_id,
|
|
7895
8239
|
streamPath: primaryStream,
|
|
7896
|
-
streams
|
|
8240
|
+
streams,
|
|
7897
8241
|
callback: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(input.claim.wake_id)}`),
|
|
7898
8242
|
claimToken: input.claim.token,
|
|
7899
8243
|
triggerEvent: `message_received`,
|
|
@@ -7904,6 +8248,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7904
8248
|
streams: entity.streams,
|
|
7905
8249
|
tags: entity.tags,
|
|
7906
8250
|
spawnArgs: entity.spawn_args,
|
|
8251
|
+
sandbox: entity.sandbox,
|
|
7907
8252
|
createdBy: entity.created_by
|
|
7908
8253
|
},
|
|
7909
8254
|
principal: principalFromCreatedBy(entity.created_by)
|