@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/entrypoint.js
CHANGED
|
@@ -76,6 +76,7 @@ const entities = pgTable(`entities`, {
|
|
|
76
76
|
tags: jsonb(`tags`).notNull().default({}),
|
|
77
77
|
tagsIndex: text(`tags_index`).array().notNull().default(sql`'{}'::text[]`),
|
|
78
78
|
spawnArgs: jsonb(`spawn_args`).default({}),
|
|
79
|
+
sandbox: jsonb(`sandbox`),
|
|
79
80
|
parent: text(`parent`),
|
|
80
81
|
createdBy: text(`created_by`),
|
|
81
82
|
typeRevision: integer(`type_revision`),
|
|
@@ -117,6 +118,7 @@ const runners = pgTable(`runners`, {
|
|
|
117
118
|
kind: text(`kind`).notNull().default(`local`),
|
|
118
119
|
adminStatus: text(`admin_status`).notNull().default(`enabled`),
|
|
119
120
|
wakeStream: text(`wake_stream`).notNull(),
|
|
121
|
+
sandboxProfiles: jsonb(`sandbox_profiles`).notNull().default([]),
|
|
120
122
|
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
121
123
|
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
122
124
|
}, (table) => [
|
|
@@ -422,6 +424,7 @@ function toPublicEntity(entity) {
|
|
|
422
424
|
dispatch_policy: entity.dispatch_policy,
|
|
423
425
|
tags: entity.tags,
|
|
424
426
|
spawn_args: entity.spawn_args,
|
|
427
|
+
sandbox: entity.sandbox,
|
|
425
428
|
parent: entity.parent,
|
|
426
429
|
created_by: entity.created_by,
|
|
427
430
|
created_at: entity.created_at,
|
|
@@ -639,7 +642,7 @@ var StreamClient = class {
|
|
|
639
642
|
});
|
|
640
643
|
});
|
|
641
644
|
}
|
|
642
|
-
async fork(path$1, sourcePath) {
|
|
645
|
+
async fork(path$1, sourcePath, opts) {
|
|
643
646
|
return await withSpan(`stream.fork`, async (span) => {
|
|
644
647
|
span.setAttributes({
|
|
645
648
|
[ATTR.STREAM_PATH]: path$1,
|
|
@@ -649,6 +652,11 @@ var StreamClient = class {
|
|
|
649
652
|
"content-type": `application/json`,
|
|
650
653
|
"Stream-Forked-From": new URL(this.streamUrl(sourcePath)).pathname
|
|
651
654
|
};
|
|
655
|
+
if (opts?.forkPointer) {
|
|
656
|
+
const ZERO_OFFSET = `0000000000000000_0000000000000000`;
|
|
657
|
+
headers[`Stream-Fork-Offset`] = opts.forkPointer.offset ?? ZERO_OFFSET;
|
|
658
|
+
if (opts.forkPointer.subOffset > 0) headers[`Stream-Fork-Sub-Offset`] = String(opts.forkPointer.subOffset);
|
|
659
|
+
}
|
|
652
660
|
injectTraceHeaders(headers);
|
|
653
661
|
const response = await fetch(this.streamUrl(path$1), {
|
|
654
662
|
method: `PUT`,
|
|
@@ -893,11 +901,11 @@ var StreamClient = class {
|
|
|
893
901
|
if (res.status === 404 || res.status === 204) return;
|
|
894
902
|
if (!res.ok) throw new Error(`Subscription delete failed: ${res.status} ${await res.text()}`);
|
|
895
903
|
}
|
|
896
|
-
async addSubscriptionStreams(subscriptionId, streams
|
|
904
|
+
async addSubscriptionStreams(subscriptionId, streams) {
|
|
897
905
|
const res = await fetch(this.subscriptionChildUrl(subscriptionId, `streams`), {
|
|
898
906
|
method: `POST`,
|
|
899
907
|
headers: await this.requestHeaders({ "content-type": `application/json` }),
|
|
900
|
-
body: JSON.stringify({ streams: streams
|
|
908
|
+
body: JSON.stringify({ streams: streams.map((stream) => this.backendSubscriptionPath(normalizeSubscriptionStreamPath(stream))) })
|
|
901
909
|
});
|
|
902
910
|
return await this.subscriptionJson(res, `Subscription stream add failed`);
|
|
903
911
|
}
|
|
@@ -1037,13 +1045,13 @@ function buildElectricProxyTarget(options) {
|
|
|
1037
1045
|
if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
|
|
1038
1046
|
const table = options.incomingUrl.searchParams.get(`table`);
|
|
1039
1047
|
if (table === `entities`) {
|
|
1040
|
-
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"`);
|
|
1048
|
+
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"`);
|
|
1041
1049
|
applyTenantShapeWhere(target, options.tenantId);
|
|
1042
1050
|
} else if (table === `entity_types`) {
|
|
1043
1051
|
target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
|
|
1044
1052
|
applyTenantShapeWhere(target, options.tenantId);
|
|
1045
1053
|
} else if (table === `runners`) {
|
|
1046
|
-
target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","created_at","updated_at"`);
|
|
1054
|
+
target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`);
|
|
1047
1055
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral$2(options.principalUrl ?? ``)}`]);
|
|
1048
1056
|
} else if (table === `runner_runtime_diagnostics`) {
|
|
1049
1057
|
target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
|
|
@@ -1139,35 +1147,48 @@ const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
|
|
|
1139
1147
|
const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
|
|
1140
1148
|
const USE_FILE_LOGS = process.env.ELECTRIC_AGENTS_LOG_FILE !== `false`;
|
|
1141
1149
|
const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST && !IS_ELECTRON_MAIN;
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
if (
|
|
1145
|
-
const streams = [];
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1150
|
+
let _logger;
|
|
1151
|
+
function getLogger() {
|
|
1152
|
+
if (_logger) return _logger;
|
|
1153
|
+
const streams = [];
|
|
1154
|
+
try {
|
|
1155
|
+
if (USE_FILE_LOGS) {
|
|
1156
|
+
const logDir = process.env.ELECTRIC_AGENTS_LOG_DIR ?? path.resolve(process.cwd(), `logs`);
|
|
1157
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
1158
|
+
const logFile = path.join(logDir, `agent-server-${Date.now()}.jsonl`);
|
|
1159
|
+
streams.push({ stream: pino.destination({
|
|
1160
|
+
dest: logFile,
|
|
1161
|
+
sync: IS_ELECTRON_MAIN
|
|
1162
|
+
}) });
|
|
1163
|
+
}
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
process.stderr.write(`[agents-server] Failed to initialize file logging: ${err instanceof Error ? err.message : err}\n`);
|
|
1166
|
+
}
|
|
1167
|
+
try {
|
|
1168
|
+
if (USE_PRETTY_LOGS) streams.push({ stream: pino.transport({
|
|
1169
|
+
target: `pino-pretty`,
|
|
1170
|
+
options: {
|
|
1171
|
+
colorize: true,
|
|
1172
|
+
ignore: `pid,hostname,name`,
|
|
1173
|
+
translateTime: `SYS:HH:MM:ss`
|
|
1174
|
+
}
|
|
1175
|
+
}) });
|
|
1176
|
+
} catch {}
|
|
1177
|
+
_logger = streams.length > 0 ? pino({
|
|
1178
|
+
base: void 0,
|
|
1179
|
+
level: LOG_LEVEL
|
|
1180
|
+
}, pino.multistream(streams)) : pino({
|
|
1181
|
+
base: void 0,
|
|
1182
|
+
enabled: false,
|
|
1183
|
+
level: LOG_LEVEL
|
|
1184
|
+
});
|
|
1185
|
+
return _logger;
|
|
1186
|
+
}
|
|
1166
1187
|
function formatArgs(args) {
|
|
1167
1188
|
const errors = [];
|
|
1168
1189
|
const parts = [];
|
|
1169
|
-
for (const
|
|
1170
|
-
else parts.push(typeof
|
|
1190
|
+
for (const value of args) if (value instanceof Error) errors.push(value);
|
|
1191
|
+
else parts.push(typeof value === `string` ? value : JSON.stringify(value));
|
|
1171
1192
|
return {
|
|
1172
1193
|
err: errors[0],
|
|
1173
1194
|
msg: parts.join(` `)
|
|
@@ -1176,20 +1197,20 @@ function formatArgs(args) {
|
|
|
1176
1197
|
const serverLog = {
|
|
1177
1198
|
info(...args) {
|
|
1178
1199
|
const { msg } = formatArgs(args);
|
|
1179
|
-
|
|
1200
|
+
getLogger().info(msg);
|
|
1180
1201
|
},
|
|
1181
1202
|
warn(...args) {
|
|
1182
1203
|
const { err, msg } = formatArgs(args);
|
|
1183
|
-
if (err)
|
|
1184
|
-
else
|
|
1204
|
+
if (err) getLogger().warn({ err }, msg);
|
|
1205
|
+
else getLogger().warn(msg);
|
|
1185
1206
|
},
|
|
1186
1207
|
error(...args) {
|
|
1187
1208
|
const { err, msg } = formatArgs(args);
|
|
1188
|
-
if (err)
|
|
1189
|
-
else
|
|
1209
|
+
if (err) getLogger().error({ err }, msg);
|
|
1210
|
+
else getLogger().error(msg);
|
|
1190
1211
|
},
|
|
1191
1212
|
event(obj, msg) {
|
|
1192
|
-
|
|
1213
|
+
getLogger().info(obj, msg);
|
|
1193
1214
|
}
|
|
1194
1215
|
};
|
|
1195
1216
|
|
|
@@ -1876,6 +1897,125 @@ function parseDispatchPolicy(value, label = `dispatch_policy`) {
|
|
|
1876
1897
|
throw new Error(details ? `${label} does not match dispatch policy schema: ${details}` : `${label} does not match dispatch policy schema`);
|
|
1877
1898
|
}
|
|
1878
1899
|
|
|
1900
|
+
//#endregion
|
|
1901
|
+
//#region src/sandbox-choice-schema.ts
|
|
1902
|
+
/**
|
|
1903
|
+
* Wire schema for a spawn-time sandbox CHOICE (the request input), as opposed to
|
|
1904
|
+
* the resolved {@link import('./electric-agents-types.js').EntitySandboxSelection}
|
|
1905
|
+
* persisted on the entity. The matching `SandboxChoice` type is hand-maintained
|
|
1906
|
+
* in `electric-agents-types.ts` — mirrors how `dispatchPolicySchema` pairs with
|
|
1907
|
+
* the `DispatchPolicy` type in `dispatch-policy-schema.ts`.
|
|
1908
|
+
*
|
|
1909
|
+
* Validation happens once, at the router boundary (this schema is embedded in
|
|
1910
|
+
* the spawn body schema); the spawn resolver consumes already-validated input,
|
|
1911
|
+
* so there is intentionally no separate `parse` helper here.
|
|
1912
|
+
*/
|
|
1913
|
+
const sandboxChoiceSchema = Type.Object({
|
|
1914
|
+
profile: Type.Optional(Type.String()),
|
|
1915
|
+
key: Type.Optional(Type.String()),
|
|
1916
|
+
scope: Type.Optional(Type.Union([Type.Literal(`entity`), Type.Literal(`wake`)])),
|
|
1917
|
+
persistent: Type.Optional(Type.Boolean()),
|
|
1918
|
+
owner: Type.Optional(Type.Boolean()),
|
|
1919
|
+
inherit: Type.Optional(Type.Boolean())
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
//#endregion
|
|
1923
|
+
//#region src/routing/sandbox.ts
|
|
1924
|
+
/**
|
|
1925
|
+
* Resolve and validate a spawn's sandbox CHOICE into the {@link
|
|
1926
|
+
* EntitySandboxSelection} persisted on the entity. Sibling of
|
|
1927
|
+
* `dispatch-policy.ts`'s `resolveEffectiveDispatchPolicyForSpawn`: kept off the
|
|
1928
|
+
* EntityManager so the spawn path reads as composed resolution steps.
|
|
1929
|
+
*
|
|
1930
|
+
* Profiles are a per-runner concern: each runner advertises what it supports.
|
|
1931
|
+
* When the spawn pins a runner via dispatch_policy, the chosen profile must be
|
|
1932
|
+
* in that runner's advertised set; otherwise we'd persist an unserviceable
|
|
1933
|
+
* choice that fails late at first wake. For unpinned dispatch (webhook /
|
|
1934
|
+
* parent-inherited) we can't pick a target ahead of time, so we fall back to a
|
|
1935
|
+
* tenant-wide "some runner offers this" check — better than nothing.
|
|
1936
|
+
*/
|
|
1937
|
+
async function resolveSandboxForSpawn(registry, dispatchPolicy, requested, parentEntity) {
|
|
1938
|
+
if (!requested) return void 0;
|
|
1939
|
+
const choice = applyInheritedSandbox(requested, parentEntity);
|
|
1940
|
+
if (!choice) return void 0;
|
|
1941
|
+
const chosenName = choice.profile;
|
|
1942
|
+
if (!chosenName) throw new ElectricAgentsError(ErrCodeInvalidRequest, `sandbox requires a "profile" (or "inherit": true with a parent that has a shared sandbox).`, 400);
|
|
1943
|
+
const chosenIsRemote = await resolveChosenProfileRemote(registry, chosenName, dispatchPolicy);
|
|
1944
|
+
assertSharedSandboxColocated(choice.key, chosenIsRemote, dispatchPolicy);
|
|
1945
|
+
const selection = { profile: chosenName };
|
|
1946
|
+
if (choice.key !== void 0) selection.key = choice.key;
|
|
1947
|
+
else if (choice.scope !== void 0) selection.scope = choice.scope;
|
|
1948
|
+
if (choice.persistent !== void 0) selection.persistent = choice.persistent;
|
|
1949
|
+
if (choice.owner === false) selection.owner = false;
|
|
1950
|
+
return selection;
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Resolve `inherit` against the parent's *stored* sandbox. `inherit` reuses the
|
|
1954
|
+
* parent's keyed sandbox as a non-owner (attach-only). It's graceful: if the
|
|
1955
|
+
* parent has no shareable (keyed) sandbox the child simply gets none (returns
|
|
1956
|
+
* `undefined`), so `spawn_worker` can always request inheritance without
|
|
1957
|
+
* breaking unkeyed parents. (A running parent wake resolves inherit to its live
|
|
1958
|
+
* explicit key in the runtime instead — this server-side path covers direct API
|
|
1959
|
+
* callers, where only the parent's *stored* explicit key is available.)
|
|
1960
|
+
*
|
|
1961
|
+
* For a non-inherit choice the request passes through unchanged.
|
|
1962
|
+
*
|
|
1963
|
+
* NOTE: `inherit: true` takes the parent's identity AND durability wholesale —
|
|
1964
|
+
* any sibling field on the request (e.g. a caller-supplied `persistent: false`)
|
|
1965
|
+
* is intentionally ignored, because a child attaches to the parent's existing
|
|
1966
|
+
* sandbox and cannot change how that sandbox is torn down. `sandboxChoiceSchema`
|
|
1967
|
+
* permits the `{ inherit: true, persistent: ... }` combination, so the
|
|
1968
|
+
* precedence is resolved here rather than rejected at the schema level.
|
|
1969
|
+
*/
|
|
1970
|
+
function applyInheritedSandbox(requested, parentEntity) {
|
|
1971
|
+
if (!requested.inherit) return requested;
|
|
1972
|
+
const parentKey = parentEntity?.sandbox?.key;
|
|
1973
|
+
if (!parentKey) return void 0;
|
|
1974
|
+
return {
|
|
1975
|
+
profile: parentEntity.sandbox.profile,
|
|
1976
|
+
key: parentKey,
|
|
1977
|
+
persistent: parentEntity.sandbox.persistent,
|
|
1978
|
+
owner: false
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Validate the chosen profile is advertised by the relevant runner(s) and
|
|
1983
|
+
* determine whether it is a remote (off-host) sandbox, reachable from any
|
|
1984
|
+
* runner. Defaults to host-local (co-location required) unless every relevant
|
|
1985
|
+
* advertisement marks it remote. Throws if the profile is unserviceable.
|
|
1986
|
+
*/
|
|
1987
|
+
async function resolveChosenProfileRemote(registry, chosenName, dispatchPolicy) {
|
|
1988
|
+
const runnerIds = [];
|
|
1989
|
+
for (const target of dispatchPolicy?.targets ?? []) if (target.type === `runner`) runnerIds.push(target.runnerId);
|
|
1990
|
+
if (runnerIds.length > 0) {
|
|
1991
|
+
let allRemote = true;
|
|
1992
|
+
for (const runnerId of runnerIds) {
|
|
1993
|
+
const runner = await registry.getRunner(runnerId);
|
|
1994
|
+
const advertised = runner?.sandbox_profiles ?? [];
|
|
1995
|
+
const match = advertised.find((p) => p.name === chosenName);
|
|
1996
|
+
if (!match) throw new ElectricAgentsError(ErrCodeInvalidRequest, `sandbox profile "${chosenName}" is not advertised by runner "${runnerId}" (advertised: ${advertised.map((p) => p.name).join(`, `) || `(none)`}).`, 400);
|
|
1997
|
+
if (match.remote !== true) allRemote = false;
|
|
1998
|
+
}
|
|
1999
|
+
return allRemote;
|
|
2000
|
+
}
|
|
2001
|
+
const available = await registry.listSandboxProfiles();
|
|
2002
|
+
const matches = available.filter((p) => p.name === chosenName);
|
|
2003
|
+
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);
|
|
2004
|
+
return matches.every((p) => p.remote === true);
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Co-location: a shared *local* sandbox lives on one host, so every
|
|
2008
|
+
* collaborator must be pinned to the same single runner. Subagents inherit the
|
|
2009
|
+
* parent's dispatch policy, so this holds once the root is pinned. A shared
|
|
2010
|
+
* *remote* sandbox is reachable from any runner, so the guard does not apply.
|
|
2011
|
+
*/
|
|
2012
|
+
function assertSharedSandboxColocated(key, chosenIsRemote, dispatchPolicy) {
|
|
2013
|
+
if (key === void 0 || chosenIsRemote) return;
|
|
2014
|
+
const targets = dispatchPolicy?.targets ?? [];
|
|
2015
|
+
const pinnedToSingleRunner = targets.length === 1 && targets[0]?.type === `runner`;
|
|
2016
|
+
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);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1879
2019
|
//#endregion
|
|
1880
2020
|
//#region src/tenant.ts
|
|
1881
2021
|
const DEFAULT_TENANT_ID = `default`;
|
|
@@ -1919,6 +2059,12 @@ var PostgresRegistry = class {
|
|
|
1919
2059
|
async createRunner(input) {
|
|
1920
2060
|
const now = new Date();
|
|
1921
2061
|
const wakeStream = input.wakeStream ?? runnerWakeStream(input.id);
|
|
2062
|
+
const sandboxProfilesValue = input.sandboxProfiles ? input.sandboxProfiles.map((p) => ({
|
|
2063
|
+
name: p.name,
|
|
2064
|
+
label: p.label,
|
|
2065
|
+
...p.description !== void 0 && { description: p.description },
|
|
2066
|
+
...p.remote !== void 0 && { remote: p.remote }
|
|
2067
|
+
})) : void 0;
|
|
1922
2068
|
await this.db.insert(runners).values({
|
|
1923
2069
|
tenantId: this.tenantId,
|
|
1924
2070
|
id: input.id,
|
|
@@ -1927,6 +2073,7 @@ var PostgresRegistry = class {
|
|
|
1927
2073
|
kind: input.kind ?? `local`,
|
|
1928
2074
|
adminStatus: input.adminStatus ?? `enabled`,
|
|
1929
2075
|
wakeStream,
|
|
2076
|
+
...sandboxProfilesValue !== void 0 && { sandboxProfiles: sandboxProfilesValue },
|
|
1930
2077
|
updatedAt: now
|
|
1931
2078
|
}).onConflictDoUpdate({
|
|
1932
2079
|
target: [runners.tenantId, runners.id],
|
|
@@ -1936,6 +2083,7 @@ var PostgresRegistry = class {
|
|
|
1936
2083
|
kind: input.kind ?? `local`,
|
|
1937
2084
|
adminStatus: input.adminStatus ?? `enabled`,
|
|
1938
2085
|
wakeStream,
|
|
2086
|
+
...sandboxProfilesValue !== void 0 && { sandboxProfiles: sandboxProfilesValue },
|
|
1939
2087
|
updatedAt: now
|
|
1940
2088
|
}
|
|
1941
2089
|
});
|
|
@@ -1943,6 +2091,30 @@ var PostgresRegistry = class {
|
|
|
1943
2091
|
if (!runner) throw new Error(`Failed to read back runner "${input.id}"`);
|
|
1944
2092
|
return runner;
|
|
1945
2093
|
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Every sandbox profile advertised by a runner in this tenant (one entry
|
|
2096
|
+
* per runner that advertises it — names may repeat across runners). Used by
|
|
2097
|
+
* spawn validation for unpinned dispatch to learn whether a chosen profile
|
|
2098
|
+
* is remote (so a shared sandbox can skip the single-runner guard).
|
|
2099
|
+
*/
|
|
2100
|
+
async listSandboxProfiles() {
|
|
2101
|
+
const rows = await this.db.select({ sandboxProfiles: runners.sandboxProfiles }).from(runners).where(eq(runners.tenantId, this.tenantId));
|
|
2102
|
+
const profiles = [];
|
|
2103
|
+
for (const row of rows) {
|
|
2104
|
+
const list = row.sandboxProfiles;
|
|
2105
|
+
if (!Array.isArray(list)) continue;
|
|
2106
|
+
for (const entry of list) {
|
|
2107
|
+
if (!entry || typeof entry.name !== `string`) continue;
|
|
2108
|
+
profiles.push({
|
|
2109
|
+
name: entry.name,
|
|
2110
|
+
label: typeof entry.label === `string` ? entry.label : entry.name,
|
|
2111
|
+
...typeof entry.description === `string` && { description: entry.description },
|
|
2112
|
+
...typeof entry.remote === `boolean` && { remote: entry.remote }
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
return profiles;
|
|
2117
|
+
}
|
|
1946
2118
|
async getRunner(id) {
|
|
1947
2119
|
const rows = await this.db.select().from(runners).where(and(eq(runners.tenantId, this.tenantId), eq(runners.id, id))).limit(1);
|
|
1948
2120
|
return rows[0] ? this.rowToRunner(rows[0]) : null;
|
|
@@ -2203,6 +2375,7 @@ var PostgresRegistry = class {
|
|
|
2203
2375
|
tags: normalizeTags(entity.tags),
|
|
2204
2376
|
tagsIndex: buildTagsIndex(entity.tags),
|
|
2205
2377
|
spawnArgs: entity.spawn_args ?? {},
|
|
2378
|
+
sandbox: entity.sandbox ?? null,
|
|
2206
2379
|
parent: entity.parent ?? null,
|
|
2207
2380
|
createdBy: entity.created_by ?? null,
|
|
2208
2381
|
typeRevision: entity.type_revision ?? null,
|
|
@@ -2540,6 +2713,7 @@ var PostgresRegistry = class {
|
|
|
2540
2713
|
write_token: row.writeToken,
|
|
2541
2714
|
tags: row.tags ?? {},
|
|
2542
2715
|
spawn_args: row.spawnArgs,
|
|
2716
|
+
sandbox: row.sandbox ?? void 0,
|
|
2543
2717
|
parent: row.parent ?? void 0,
|
|
2544
2718
|
created_by: row.createdBy ?? void 0,
|
|
2545
2719
|
type_revision: row.typeRevision ?? void 0,
|
|
@@ -2587,6 +2761,7 @@ var PostgresRegistry = class {
|
|
|
2587
2761
|
kind: assertRunnerKind(row.kind),
|
|
2588
2762
|
admin_status: assertRunnerAdminStatus(row.adminStatus),
|
|
2589
2763
|
wake_stream: row.wakeStream,
|
|
2764
|
+
sandbox_profiles: row.sandboxProfiles ?? [],
|
|
2590
2765
|
created_at: row.createdAt.toISOString(),
|
|
2591
2766
|
updated_at: row.updatedAt.toISOString()
|
|
2592
2767
|
};
|
|
@@ -2934,6 +3109,7 @@ var EntityManager = class {
|
|
|
2934
3109
|
if (!parentEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Parent entity "${req.parent}" not found`, 404);
|
|
2935
3110
|
}
|
|
2936
3111
|
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;
|
|
3112
|
+
const sandbox = await resolveSandboxForSpawn(this.registry, dispatchPolicy, req.sandbox, parentEntity);
|
|
2937
3113
|
const now = Date.now();
|
|
2938
3114
|
const entityData = {
|
|
2939
3115
|
type: typeName,
|
|
@@ -2948,6 +3124,7 @@ var EntityManager = class {
|
|
|
2948
3124
|
write_token: writeToken,
|
|
2949
3125
|
tags: initialTags,
|
|
2950
3126
|
spawn_args: req.args,
|
|
3127
|
+
sandbox,
|
|
2951
3128
|
type_revision: entityType.revision,
|
|
2952
3129
|
inbox_schemas: entityType.inbox_schemas,
|
|
2953
3130
|
state_schemas: entityType.state_schemas,
|
|
@@ -3077,27 +3254,66 @@ var EntityManager = class {
|
|
|
3077
3254
|
const writeEntityLocks = new Set();
|
|
3078
3255
|
const writeStreamLocks = new Set();
|
|
3079
3256
|
try {
|
|
3080
|
-
|
|
3257
|
+
let sourceTree;
|
|
3258
|
+
if (opts.forkPointer) {
|
|
3259
|
+
const rootEntity = await this.registry.getEntity(rootUrl);
|
|
3260
|
+
if (!rootEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3261
|
+
if (isTerminalEntityStatus(rootEntity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${rootEntity.url}"`, 409);
|
|
3262
|
+
sourceTree = await this.listEntitySubtree(rootEntity);
|
|
3263
|
+
} else sourceTree = await this.waitForIdleSubtree(rootUrl, opts, workLocks);
|
|
3081
3264
|
const sourceRoot = sourceTree[0];
|
|
3082
3265
|
if (sourceRoot.parent) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Only top-level entities can be forked`, 400);
|
|
3083
|
-
|
|
3266
|
+
let preFilteredRoot;
|
|
3267
|
+
if (opts.forkPointer) {
|
|
3268
|
+
const sourceEvents = await this.streamClient.readJson(sourceRoot.streams.main);
|
|
3269
|
+
const flat = sourceEvents.flatMap((item) => Array.isArray(item) ? item : [item]);
|
|
3270
|
+
const target = this.resolveForkPointerTarget(flat, opts.forkPointer, sourceRoot.streams.main);
|
|
3271
|
+
const filteredEvents = flat.slice(0, target);
|
|
3272
|
+
const rootManifests = this.reduceStateRows(filteredEvents, `manifest`);
|
|
3273
|
+
const sharedStateIds = new Set();
|
|
3274
|
+
for (const manifest of rootManifests.values()) this.collectSharedStateIds(manifest, sharedStateIds);
|
|
3275
|
+
preFilteredRoot = {
|
|
3276
|
+
manifests: rootManifests,
|
|
3277
|
+
childStatuses: this.reduceStateRows(filteredEvents, `child_status`),
|
|
3278
|
+
replayWatermarks: this.reduceStateRows(filteredEvents, `replay_watermark`),
|
|
3279
|
+
sharedStateIds
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
const effectiveSubtree = preFilteredRoot ? this.computeEffectiveSubtree(sourceTree, sourceRoot.url, preFilteredRoot.manifests) : sourceTree;
|
|
3283
|
+
if (opts.forkPointer) {
|
|
3284
|
+
const descendants = effectiveSubtree.filter((entity) => entity.url !== sourceRoot.url);
|
|
3285
|
+
if (descendants.length > 0) await this.waitForGivenEntitiesIdle(descendants, opts, workLocks);
|
|
3286
|
+
}
|
|
3287
|
+
const snapshot = await this.readForkStateSnapshot(
|
|
3288
|
+
// Skip the root when we've already pre-filtered it — avoid both a
|
|
3289
|
+
// wasted HEAD read of main and a re-population that would clobber
|
|
3290
|
+
// the filtered entries.
|
|
3291
|
+
preFilteredRoot ? effectiveSubtree.filter((entity) => entity.url !== sourceRoot.url) : effectiveSubtree
|
|
3292
|
+
);
|
|
3293
|
+
if (preFilteredRoot) {
|
|
3294
|
+
snapshot.manifestsByEntity.set(sourceRoot.url, preFilteredRoot.manifests);
|
|
3295
|
+
snapshot.childStatusesByEntity.set(sourceRoot.url, preFilteredRoot.childStatuses);
|
|
3296
|
+
snapshot.replayWatermarksByEntity.set(sourceRoot.url, preFilteredRoot.replayWatermarks);
|
|
3297
|
+
for (const id of preFilteredRoot.sharedStateIds) snapshot.sharedStateIds.add(id);
|
|
3298
|
+
}
|
|
3084
3299
|
const suffix = randomUUID().slice(0, 8);
|
|
3085
|
-
const entityUrlMap = await this.buildForkEntityUrlMap(
|
|
3300
|
+
const entityUrlMap = await this.buildForkEntityUrlMap(effectiveSubtree, {
|
|
3086
3301
|
suffix,
|
|
3087
3302
|
rootUrl,
|
|
3088
3303
|
rootInstanceId: opts.rootInstanceId
|
|
3089
3304
|
});
|
|
3090
3305
|
const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
|
|
3091
3306
|
const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
|
|
3092
|
-
const entityPlans = this.buildForkEntityPlans(
|
|
3093
|
-
this.addForkLocks(this.forkWriteLockedEntities,
|
|
3307
|
+
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap);
|
|
3308
|
+
this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
|
|
3094
3309
|
this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => getSharedStateStreamPath(id)), writeStreamLocks);
|
|
3095
3310
|
const createdStreams = [];
|
|
3096
3311
|
const createdEntities = [];
|
|
3097
3312
|
const activeManifestsByEntity = new Map();
|
|
3098
3313
|
try {
|
|
3099
3314
|
for (const plan of entityPlans) {
|
|
3100
|
-
|
|
3315
|
+
const isRoot = plan.source.url === rootUrl;
|
|
3316
|
+
await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
|
|
3101
3317
|
createdStreams.push(plan.fork.streams.main);
|
|
3102
3318
|
await this.streamClient.fork(plan.fork.streams.error, plan.source.streams.error);
|
|
3103
3319
|
createdStreams.push(plan.fork.streams.error);
|
|
@@ -3190,6 +3406,38 @@ var EntityManager = class {
|
|
|
3190
3406
|
}
|
|
3191
3407
|
held.clear();
|
|
3192
3408
|
}
|
|
3409
|
+
/**
|
|
3410
|
+
* Variant of {@link waitForIdleSubtree} that takes an explicit entity
|
|
3411
|
+
* list instead of walking the registry from `rootUrl`. Used by the
|
|
3412
|
+
* pointer-fork path to wait+lock only the kept descendants, since
|
|
3413
|
+
* the root is being forked from history and doesn't need to be idle.
|
|
3414
|
+
*/
|
|
3415
|
+
async waitForGivenEntitiesIdle(entities$1, opts, workLocks) {
|
|
3416
|
+
if (entities$1.length === 0) return;
|
|
3417
|
+
const timeoutMs = opts.waitTimeoutMs ?? DEFAULT_FORK_WAIT_TIMEOUT_MS;
|
|
3418
|
+
const pollMs = opts.waitPollMs ?? DEFAULT_FORK_WAIT_POLL_MS;
|
|
3419
|
+
const refresh = async () => {
|
|
3420
|
+
const refreshed = await Promise.all(entities$1.map((entity) => this.registry.getEntity(entity.url)));
|
|
3421
|
+
return refreshed.filter((entity) => !!entity);
|
|
3422
|
+
};
|
|
3423
|
+
const deadline = Date.now() + timeoutMs;
|
|
3424
|
+
while (true) {
|
|
3425
|
+
const present = await refresh();
|
|
3426
|
+
const stopped = present.find((entity) => isTerminalEntityStatus(entity.status));
|
|
3427
|
+
if (stopped) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${stopped.url}"`, 409);
|
|
3428
|
+
let active = present.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3429
|
+
if (active.length === 0) {
|
|
3430
|
+
this.addForkLocks(this.forkWorkLockedEntities, present.map((entity) => entity.url), workLocks);
|
|
3431
|
+
const reChecked = await refresh();
|
|
3432
|
+
const reActive = reChecked.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3433
|
+
if (reActive.length === 0) return;
|
|
3434
|
+
this.releaseForkLocks(this.forkWorkLockedEntities, workLocks);
|
|
3435
|
+
active = reActive;
|
|
3436
|
+
}
|
|
3437
|
+
if (Date.now() >= deadline) throw new ElectricAgentsError(ErrCodeForkWaitTimeout, `Timed out waiting for descendants to become idle`, 409, { active: active.map((entity) => entity.url) });
|
|
3438
|
+
await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3193
3441
|
async waitForIdleSubtree(rootUrl, opts, workLocks) {
|
|
3194
3442
|
const timeoutMs = opts.waitTimeoutMs ?? DEFAULT_FORK_WAIT_TIMEOUT_MS;
|
|
3195
3443
|
const pollMs = opts.waitPollMs ?? DEFAULT_FORK_WAIT_POLL_MS;
|
|
@@ -3219,6 +3467,73 @@ var EntityManager = class {
|
|
|
3219
3467
|
await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
|
|
3220
3468
|
}
|
|
3221
3469
|
}
|
|
3470
|
+
/**
|
|
3471
|
+
* Translate `forkPointer` into a 1-indexed CUMULATIVE position in the
|
|
3472
|
+
* source's flattened history. Throws a 400 if the pointer doesn't
|
|
3473
|
+
* address a real event.
|
|
3474
|
+
*
|
|
3475
|
+
* Semantics (mirroring the durable-streams server interpretation):
|
|
3476
|
+
* `{ offset: X, subOffset: N }` means "from anchor X, take N flattened
|
|
3477
|
+
* messages forward." Concretely, the target event is the N-th event
|
|
3478
|
+
* after the last event whose `headers.offset` is ≤ X. (When `X` is
|
|
3479
|
+
* `null`, the anchor is the stream start and the target is the N-th
|
|
3480
|
+
* event from the very beginning.) The returned position is the count
|
|
3481
|
+
* of events to KEEP — events 1..position survive the filter.
|
|
3482
|
+
*
|
|
3483
|
+
* A pointer is valid when:
|
|
3484
|
+
* - `pointer.offset` is `null` (stream start) OR matches some
|
|
3485
|
+
* event's `headers.offset` value, AND
|
|
3486
|
+
* - `pointer.subOffset` is in `[1, total events past the anchor]`.
|
|
3487
|
+
*/
|
|
3488
|
+
resolveForkPointerTarget(events, pointer, streamPath) {
|
|
3489
|
+
let positionAtAnchor = 0;
|
|
3490
|
+
let anchorSeen = pointer.offset === null;
|
|
3491
|
+
for (const event of events) {
|
|
3492
|
+
const headers = isRecord(event.headers) ? event.headers : void 0;
|
|
3493
|
+
const eventOffset = typeof headers?.offset === `string` ? headers.offset : void 0;
|
|
3494
|
+
if (eventOffset === void 0) continue;
|
|
3495
|
+
if (pointer.offset === null) continue;
|
|
3496
|
+
if (eventOffset === pointer.offset) anchorSeen = true;
|
|
3497
|
+
if (eventOffset <= pointer.offset) positionAtAnchor++;
|
|
3498
|
+
}
|
|
3499
|
+
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);
|
|
3500
|
+
const eventsPastAnchor = events.length - positionAtAnchor;
|
|
3501
|
+
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);
|
|
3502
|
+
return positionAtAnchor + pointer.subOffset;
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Compute the subset of `sourceTree` that survives the manifest filter
|
|
3506
|
+
* applied at the root. After filtering the root's manifest at the fork
|
|
3507
|
+
* pointer, only children whose manifest entries landed at or before the
|
|
3508
|
+
* pointer remain; those kept children carry their CURRENT (HEAD) subtree
|
|
3509
|
+
* along with them. Children dropped from the root's manifest, and any
|
|
3510
|
+
* of their descendants, are excluded.
|
|
3511
|
+
*/
|
|
3512
|
+
computeEffectiveSubtree(sourceTree, rootUrl, filteredRootManifests) {
|
|
3513
|
+
const keptChildUrls = new Set();
|
|
3514
|
+
for (const value of filteredRootManifests.values()) if (value.kind === `child` && typeof value.entity_url === `string`) keptChildUrls.add(value.entity_url);
|
|
3515
|
+
const childrenByParent = new Map();
|
|
3516
|
+
for (const entity of sourceTree) {
|
|
3517
|
+
if (!entity.parent) continue;
|
|
3518
|
+
const list = childrenByParent.get(entity.parent) ?? [];
|
|
3519
|
+
list.push(entity);
|
|
3520
|
+
childrenByParent.set(entity.parent, list);
|
|
3521
|
+
}
|
|
3522
|
+
const rootEntity = sourceTree.find((e) => e.url === rootUrl);
|
|
3523
|
+
if (!rootEntity) return [];
|
|
3524
|
+
const result = [rootEntity];
|
|
3525
|
+
const queue = [];
|
|
3526
|
+
for (const child of childrenByParent.get(rootUrl) ?? []) if (keptChildUrls.has(child.url)) queue.push(child);
|
|
3527
|
+
const seen = new Set([rootUrl]);
|
|
3528
|
+
while (queue.length > 0) {
|
|
3529
|
+
const entity = queue.shift();
|
|
3530
|
+
if (seen.has(entity.url)) continue;
|
|
3531
|
+
seen.add(entity.url);
|
|
3532
|
+
result.push(entity);
|
|
3533
|
+
for (const grandchild of childrenByParent.get(entity.url) ?? []) if (!seen.has(grandchild.url)) queue.push(grandchild);
|
|
3534
|
+
}
|
|
3535
|
+
return result;
|
|
3536
|
+
}
|
|
3222
3537
|
async listEntitySubtree(root) {
|
|
3223
3538
|
const result = [];
|
|
3224
3539
|
const queue = [root];
|
|
@@ -4326,6 +4641,7 @@ var EntityManager = class {
|
|
|
4326
4641
|
streams: entity.streams,
|
|
4327
4642
|
tags: entity.tags,
|
|
4328
4643
|
spawnArgs: entity.spawn_args,
|
|
4644
|
+
sandbox: entity.sandbox,
|
|
4329
4645
|
createdBy: entity.created_by
|
|
4330
4646
|
},
|
|
4331
4647
|
principal: principalFromCreatedBy(entity.created_by),
|
|
@@ -4616,6 +4932,7 @@ const spawnBodySchema = Type.Object({
|
|
|
4616
4932
|
tags: Type.Optional(stringRecordSchema$1),
|
|
4617
4933
|
parent: Type.Optional(Type.String()),
|
|
4618
4934
|
dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
4935
|
+
sandbox: Type.Optional(sandboxChoiceSchema),
|
|
4619
4936
|
initialMessage: Type.Optional(Type.Unknown()),
|
|
4620
4937
|
wake: Type.Optional(Type.Object({
|
|
4621
4938
|
subscriberUrl: Type.String(),
|
|
@@ -4657,7 +4974,11 @@ const inboxMessageBodySchema = Type.Object({
|
|
|
4657
4974
|
});
|
|
4658
4975
|
const forkBodySchema = Type.Object({
|
|
4659
4976
|
instance_id: Type.Optional(Type.String()),
|
|
4660
|
-
waitTimeoutMs: Type.Optional(Type.Number())
|
|
4977
|
+
waitTimeoutMs: Type.Optional(Type.Number()),
|
|
4978
|
+
fork_pointer: Type.Optional(Type.Object({
|
|
4979
|
+
offset: Type.Union([Type.String(), Type.Null()]),
|
|
4980
|
+
sub_offset: Type.Number()
|
|
4981
|
+
}))
|
|
4661
4982
|
});
|
|
4662
4983
|
const setTagBodySchema = Type.Object({ value: Type.String() });
|
|
4663
4984
|
const entitySignalSchema = Type.Union([
|
|
@@ -4975,7 +5296,11 @@ async function forkEntity(request, ctx) {
|
|
|
4975
5296
|
await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
|
|
4976
5297
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
4977
5298
|
rootInstanceId: parsed.instance_id,
|
|
4978
|
-
waitTimeoutMs: parsed.waitTimeoutMs
|
|
5299
|
+
waitTimeoutMs: parsed.waitTimeoutMs,
|
|
5300
|
+
...parsed.fork_pointer && { forkPointer: {
|
|
5301
|
+
offset: parsed.fork_pointer.offset,
|
|
5302
|
+
subOffset: parsed.fork_pointer.sub_offset
|
|
5303
|
+
} }
|
|
4979
5304
|
});
|
|
4980
5305
|
for (const forkedEntity of result.entities) await linkEntityDispatchSubscription(ctx, forkedEntity);
|
|
4981
5306
|
return json({
|
|
@@ -5073,6 +5398,7 @@ async function spawnEntity(request, ctx) {
|
|
|
5073
5398
|
tags: parsed.tags,
|
|
5074
5399
|
parent: parsed.parent,
|
|
5075
5400
|
dispatch_policy: dispatchPolicy,
|
|
5401
|
+
sandbox: parsed.sandbox,
|
|
5076
5402
|
initialMessage: void 0,
|
|
5077
5403
|
wake: parsed.wake,
|
|
5078
5404
|
created_by: principal.url
|
|
@@ -5327,6 +5653,12 @@ function withLeadingSlash(path$1) {
|
|
|
5327
5653
|
|
|
5328
5654
|
//#endregion
|
|
5329
5655
|
//#region src/routing/runners-router.ts
|
|
5656
|
+
const sandboxProfileBodySchema = Type.Object({
|
|
5657
|
+
name: Type.String(),
|
|
5658
|
+
label: Type.String(),
|
|
5659
|
+
description: Type.Optional(Type.String()),
|
|
5660
|
+
remote: Type.Optional(Type.Boolean())
|
|
5661
|
+
});
|
|
5330
5662
|
const registerRunnerBodySchema = Type.Object({
|
|
5331
5663
|
id: Type.String(),
|
|
5332
5664
|
owner_principal: Type.Optional(Type.String()),
|
|
@@ -5339,7 +5671,8 @@ const registerRunnerBodySchema = Type.Object({
|
|
|
5339
5671
|
Type.Literal(`server`)
|
|
5340
5672
|
])),
|
|
5341
5673
|
admin_status: Type.Optional(Type.Union([Type.Literal(`enabled`), Type.Literal(`disabled`)])),
|
|
5342
|
-
wake_stream: Type.Optional(Type.String())
|
|
5674
|
+
wake_stream: Type.Optional(Type.String()),
|
|
5675
|
+
sandbox_profiles: Type.Optional(Type.Array(sandboxProfileBodySchema))
|
|
5343
5676
|
});
|
|
5344
5677
|
const heartbeatBodySchema = Type.Object({
|
|
5345
5678
|
lease_ms: Type.Optional(Type.Number()),
|
|
@@ -5437,7 +5770,8 @@ async function registerRunner(request, ctx) {
|
|
|
5437
5770
|
label: parsed.label,
|
|
5438
5771
|
kind: parsed.kind,
|
|
5439
5772
|
adminStatus: parsed.admin_status,
|
|
5440
|
-
wakeStream: parsed.wake_stream
|
|
5773
|
+
wakeStream: parsed.wake_stream,
|
|
5774
|
+
sandboxProfiles: parsed.sandbox_profiles
|
|
5441
5775
|
});
|
|
5442
5776
|
await ctx.streamClient.ensure(runner.wake_stream, { contentType: `application/json` });
|
|
5443
5777
|
return json(runner, { status: 201 });
|
|
@@ -5647,7 +5981,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
5647
5981
|
leaseExpiresAt: input.claim.lease_ttl_ms ? new Date(Date.now() + input.claim.lease_ttl_ms) : void 0
|
|
5648
5982
|
});
|
|
5649
5983
|
await ctx.entityManager.registry.updateStatus(entity.url, `running`);
|
|
5650
|
-
const streams
|
|
5984
|
+
const streams = input.claim.streams.map((stream) => ({
|
|
5651
5985
|
path: withLeadingSlash(stream.path),
|
|
5652
5986
|
offset: stream.tail_offset ?? ``
|
|
5653
5987
|
}));
|
|
@@ -5656,7 +5990,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
5656
5990
|
epoch: input.claim.generation,
|
|
5657
5991
|
wakeId: input.claim.wake_id,
|
|
5658
5992
|
streamPath: primaryStream,
|
|
5659
|
-
streams
|
|
5993
|
+
streams,
|
|
5660
5994
|
callback: appendPathToUrl(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(input.claim.wake_id)}`),
|
|
5661
5995
|
claimToken: input.claim.token,
|
|
5662
5996
|
triggerEvent: `message_received`,
|
|
@@ -5667,6 +6001,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
5667
6001
|
streams: entity.streams,
|
|
5668
6002
|
tags: entity.tags,
|
|
5669
6003
|
spawnArgs: entity.spawn_args,
|
|
6004
|
+
sandbox: entity.sandbox,
|
|
5670
6005
|
createdBy: entity.created_by
|
|
5671
6006
|
},
|
|
5672
6007
|
principal: principalFromCreatedBy(entity.created_by)
|
|
@@ -8077,11 +8412,19 @@ var WakeRegistry = class {
|
|
|
8077
8412
|
}
|
|
8078
8413
|
const kind = operation === `delete` ? `delete` : operation === `update` ? `update` : `insert`;
|
|
8079
8414
|
if (condition.ops && condition.ops.length > 0 && !condition.ops.includes(kind)) return null;
|
|
8080
|
-
|
|
8415
|
+
const change = {
|
|
8081
8416
|
collection: eventType,
|
|
8082
8417
|
kind,
|
|
8083
8418
|
key: event.key || ``
|
|
8084
|
-
}
|
|
8419
|
+
};
|
|
8420
|
+
if (eventType === `inbox`) {
|
|
8421
|
+
const value = event.value;
|
|
8422
|
+
if (typeof value?.from === `string`) change.from = value.from;
|
|
8423
|
+
if (`payload` in (value ?? {})) change.payload = value?.payload;
|
|
8424
|
+
if (typeof value?.timestamp === `string`) change.timestamp = value.timestamp;
|
|
8425
|
+
if (typeof value?.message_type === `string`) change.message_type = value.message_type;
|
|
8426
|
+
}
|
|
8427
|
+
return { change };
|
|
8085
8428
|
}
|
|
8086
8429
|
};
|
|
8087
8430
|
|