@roamcode.ai/server 1.3.0 → 1.4.1
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/index.d.ts +65 -1
- package/dist/index.js +923 -52
- package/dist/start.d.ts +23 -0
- package/dist/start.js +920 -50
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -9753,6 +9753,32 @@ var CloudHostHeartbeatV1Schema = z5.object({
|
|
|
9753
9753
|
context.addIssue({ code: "custom", path: ["capabilities"], message: "capabilities must be unique" });
|
|
9754
9754
|
}
|
|
9755
9755
|
});
|
|
9756
|
+
var CloudAutomationWebhookRegistrationSchema = z5.object({
|
|
9757
|
+
hookId: z5.string().regex(/^rcwh_[A-Za-z0-9_-]{24,80}$/),
|
|
9758
|
+
automationId: SafeIdentifierSchema,
|
|
9759
|
+
triggerId: SafeIdentifierSchema,
|
|
9760
|
+
secretHash: z5.string().regex(/^[a-f0-9]{64}$/),
|
|
9761
|
+
enabled: z5.boolean()
|
|
9762
|
+
}).strict();
|
|
9763
|
+
var CloudAutomationInvocationSchema = z5.object({
|
|
9764
|
+
id: z5.uuid(),
|
|
9765
|
+
automationId: SafeIdentifierSchema,
|
|
9766
|
+
triggerId: SafeIdentifierSchema,
|
|
9767
|
+
hookId: z5.string().regex(/^rcwh_[A-Za-z0-9_-]{24,80}$/),
|
|
9768
|
+
createdAt: TimestampSchema
|
|
9769
|
+
}).strict();
|
|
9770
|
+
var CloudAutomationSyncRequestSchema = z5.object({
|
|
9771
|
+
v: z5.literal(1),
|
|
9772
|
+
organizationId: SafeIdentifierSchema,
|
|
9773
|
+
hostId: SafeIdentifierSchema,
|
|
9774
|
+
instanceId: SafeIdentifierSchema,
|
|
9775
|
+
registrations: z5.array(CloudAutomationWebhookRegistrationSchema).max(128).refine(
|
|
9776
|
+
(registrations) => new Set(registrations.map((registration) => registration.hookId)).size === registrations.length,
|
|
9777
|
+
"webhook identities must be unique"
|
|
9778
|
+
),
|
|
9779
|
+
acknowledgements: z5.array(z5.uuid()).max(200).refine((values) => new Set(values).size === values.length)
|
|
9780
|
+
}).strict();
|
|
9781
|
+
var CloudAutomationSyncResponseSchema = z5.object({ invocations: z5.array(CloudAutomationInvocationSchema).max(50) }).strict();
|
|
9756
9782
|
var CloudAuthorizationScopeV1Schema = z5.discriminatedUnion("type", [
|
|
9757
9783
|
z5.object({ type: z5.literal("organization") }).strict(),
|
|
9758
9784
|
z5.object({ type: z5.literal("host"), id: SafeIdentifierSchema }).strict(),
|
|
@@ -10481,6 +10507,7 @@ function resolveCloudHostConfig(env, dataDir) {
|
|
|
10481
10507
|
|
|
10482
10508
|
// src/cloud-host-runtime.ts
|
|
10483
10509
|
var CLOUD_HOST_HEARTBEAT_PATH = "/api/v1/hosts/heartbeat";
|
|
10510
|
+
var CLOUD_HOST_AUTOMATION_SYNC_PATH = "/api/v1/hosts/automation-sync";
|
|
10484
10511
|
var CLOUD_HOST_AUTHORIZATION_SNAPSHOT_PATH = "/api/v1/hosts/authorization-snapshot";
|
|
10485
10512
|
var CLOUD_HOST_MAX_SIGNED_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
10486
10513
|
var DEFAULT_REQUEST_TIMEOUT_MS3 = 15e3;
|
|
@@ -10567,6 +10594,7 @@ function createCloudHostRuntime(options) {
|
|
|
10567
10594
|
let lastHeartbeatAt;
|
|
10568
10595
|
let lastAuthorizationAt;
|
|
10569
10596
|
let heartbeatFailures = 0;
|
|
10597
|
+
let automationFailures = 0;
|
|
10570
10598
|
let authorizationFailures = 0;
|
|
10571
10599
|
let lastAuthorizationIssue;
|
|
10572
10600
|
let running = false;
|
|
@@ -10576,6 +10604,8 @@ function createCloudHostRuntime(options) {
|
|
|
10576
10604
|
let heartbeatInFlight;
|
|
10577
10605
|
let authorizationInFlight;
|
|
10578
10606
|
const capabilities2 = () => [...new Set(typeof options.capabilities === "function" ? options.capabilities() : options.capabilities)].sort();
|
|
10607
|
+
const automationWebhooks = () => (typeof options.automationWebhooks === "function" ? options.automationWebhooks() : options.automationWebhooks ?? []).map((registration) => CloudAutomationWebhookRegistrationSchema.parse(registration)).sort((left, right) => left.hookId.localeCompare(right.hookId));
|
|
10608
|
+
const automationAcknowledgements = /* @__PURE__ */ new Set();
|
|
10579
10609
|
const request = async (path, init) => {
|
|
10580
10610
|
try {
|
|
10581
10611
|
return await fetchImpl(`${config.controlPlaneOrigin}${path}`, {
|
|
@@ -10695,6 +10725,51 @@ function createCloudHostRuntime(options) {
|
|
|
10695
10725
|
}
|
|
10696
10726
|
await response2.body?.cancel().catch(() => void 0);
|
|
10697
10727
|
lastHeartbeatAt = currentNow;
|
|
10728
|
+
if (state === "ready" && options.automationWebhooks && options.onAutomationInvocation) {
|
|
10729
|
+
try {
|
|
10730
|
+
let deliveryFailed = false;
|
|
10731
|
+
for (let page = 0; page < 4; page += 1) {
|
|
10732
|
+
const acknowledgements = [...automationAcknowledgements].slice(0, 200);
|
|
10733
|
+
const payload = CloudAutomationSyncRequestSchema.parse({
|
|
10734
|
+
v: 1,
|
|
10735
|
+
organizationId: config.organizationId,
|
|
10736
|
+
hostId: config.hostId,
|
|
10737
|
+
instanceId: options.instanceId,
|
|
10738
|
+
registrations: automationWebhooks(),
|
|
10739
|
+
acknowledgements
|
|
10740
|
+
});
|
|
10741
|
+
const automationResponse = await request(CLOUD_HOST_AUTOMATION_SYNC_PATH, {
|
|
10742
|
+
method: "POST",
|
|
10743
|
+
headers: authenticatedHeaders({ "content-type": "application/json", accept: "application/json" }),
|
|
10744
|
+
body: JSON.stringify(payload)
|
|
10745
|
+
});
|
|
10746
|
+
if (automationResponse.status === 404) {
|
|
10747
|
+
await automationResponse.body?.cancel().catch(() => void 0);
|
|
10748
|
+
break;
|
|
10749
|
+
}
|
|
10750
|
+
if (automationResponse.status !== 200) {
|
|
10751
|
+
await boundedResponseText2(automationResponse).catch(() => "");
|
|
10752
|
+
throw responseError(automationResponse);
|
|
10753
|
+
}
|
|
10754
|
+
const parsed = CloudAutomationSyncResponseSchema.parse(
|
|
10755
|
+
JSON.parse(await boundedResponseText2(automationResponse))
|
|
10756
|
+
);
|
|
10757
|
+
for (const id of acknowledgements) automationAcknowledgements.delete(id);
|
|
10758
|
+
for (const invocation of parsed.invocations) {
|
|
10759
|
+
try {
|
|
10760
|
+
await options.onAutomationInvocation(invocation);
|
|
10761
|
+
automationAcknowledgements.add(invocation.id);
|
|
10762
|
+
} catch {
|
|
10763
|
+
deliveryFailed = true;
|
|
10764
|
+
}
|
|
10765
|
+
}
|
|
10766
|
+
if (parsed.invocations.length === 0 && automationAcknowledgements.size === 0) break;
|
|
10767
|
+
}
|
|
10768
|
+
automationFailures = deliveryFailed ? automationFailures + 1 : 0;
|
|
10769
|
+
} catch {
|
|
10770
|
+
automationFailures += 1;
|
|
10771
|
+
}
|
|
10772
|
+
}
|
|
10698
10773
|
};
|
|
10699
10774
|
const jittered = (baseMs) => {
|
|
10700
10775
|
const sample = random();
|
|
@@ -10759,6 +10834,7 @@ function createCloudHostRuntime(options) {
|
|
|
10759
10834
|
status: () => ({
|
|
10760
10835
|
running,
|
|
10761
10836
|
heartbeatFailures,
|
|
10837
|
+
automationFailures,
|
|
10762
10838
|
authorizationFailures,
|
|
10763
10839
|
...lastAuthorizationIssue ? { authorizationIssue: lastAuthorizationIssue } : {},
|
|
10764
10840
|
...lastHeartbeatAt === void 0 ? {} : { lastHeartbeatAt },
|
|
@@ -14339,6 +14415,48 @@ function normalizeTrigger(value) {
|
|
|
14339
14415
|
}
|
|
14340
14416
|
return { type: "manual" };
|
|
14341
14417
|
}
|
|
14418
|
+
function validTimeZone(value) {
|
|
14419
|
+
try {
|
|
14420
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(0);
|
|
14421
|
+
return true;
|
|
14422
|
+
} catch {
|
|
14423
|
+
return false;
|
|
14424
|
+
}
|
|
14425
|
+
}
|
|
14426
|
+
function normalizeConfiguredTriggers(value) {
|
|
14427
|
+
if (!Array.isArray(value) || value.length > 16) throw new Error("invalid automation triggers");
|
|
14428
|
+
const ids = /* @__PURE__ */ new Set();
|
|
14429
|
+
return value.map((candidate) => {
|
|
14430
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
14431
|
+
throw new Error("invalid automation trigger");
|
|
14432
|
+
}
|
|
14433
|
+
const raw = candidate;
|
|
14434
|
+
const id = normalizeId(raw.id, "automation trigger id");
|
|
14435
|
+
if (ids.has(id)) throw new Error("duplicate automation trigger id");
|
|
14436
|
+
ids.add(id);
|
|
14437
|
+
if (typeof raw.enabled !== "boolean") throw new Error("invalid automation trigger state");
|
|
14438
|
+
if (raw.type === "schedule") {
|
|
14439
|
+
if (typeof raw.cron !== "string" || raw.cron.trim().split(/\s+/).length !== 5 || raw.cron.length > 120 || typeof raw.timeZone !== "string" || raw.timeZone.length > 80 || !validTimeZone(raw.timeZone) || raw.missedRunPolicy !== "skip" || Object.keys(raw).some((key) => !["id", "type", "enabled", "cron", "timeZone", "missedRunPolicy"].includes(key))) {
|
|
14440
|
+
throw new Error("invalid schedule trigger");
|
|
14441
|
+
}
|
|
14442
|
+
return {
|
|
14443
|
+
id,
|
|
14444
|
+
type: "schedule",
|
|
14445
|
+
enabled: raw.enabled,
|
|
14446
|
+
cron: raw.cron.trim().replace(/\s+/g, " "),
|
|
14447
|
+
timeZone: raw.timeZone,
|
|
14448
|
+
missedRunPolicy: "skip"
|
|
14449
|
+
};
|
|
14450
|
+
}
|
|
14451
|
+
if (raw.type === "webhook") {
|
|
14452
|
+
if (typeof raw.hookId !== "string" || !/^rcwh_[A-Za-z0-9_-]{24,80}$/.test(raw.hookId) || typeof raw.secretHash !== "string" || !/^[a-f0-9]{64}$/.test(raw.secretHash) || Object.keys(raw).some((key) => !["id", "type", "enabled", "hookId", "secretHash"].includes(key))) {
|
|
14453
|
+
throw new Error("invalid webhook trigger");
|
|
14454
|
+
}
|
|
14455
|
+
return { id, type: "webhook", enabled: raw.enabled, hookId: raw.hookId, secretHash: raw.secretHash };
|
|
14456
|
+
}
|
|
14457
|
+
throw new Error("invalid automation trigger");
|
|
14458
|
+
});
|
|
14459
|
+
}
|
|
14342
14460
|
function normalizeRuntimeOptions(value) {
|
|
14343
14461
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid runtime options");
|
|
14344
14462
|
let encoded;
|
|
@@ -14365,7 +14483,8 @@ function normalizeCreate2(input) {
|
|
|
14365
14483
|
cwd: normalizeCwd2(input.cwd),
|
|
14366
14484
|
instruction: normalizeInstruction(input.instruction),
|
|
14367
14485
|
runtimeOptions: normalizeRuntimeOptions(input.runtimeOptions ?? {}),
|
|
14368
|
-
trigger: normalizeTrigger(input.trigger ?? { type: "manual" })
|
|
14486
|
+
trigger: normalizeTrigger(input.trigger ?? { type: "manual" }),
|
|
14487
|
+
triggers: normalizeConfiguredTriggers(input.triggers ?? [])
|
|
14369
14488
|
};
|
|
14370
14489
|
}
|
|
14371
14490
|
function applyUpdate(current, input, now) {
|
|
@@ -14379,7 +14498,8 @@ function applyUpdate(current, input, now) {
|
|
|
14379
14498
|
cwd: input.cwd ?? current.cwd,
|
|
14380
14499
|
instruction: input.instruction ?? current.instruction,
|
|
14381
14500
|
runtimeOptions: input.runtimeOptions ?? current.runtimeOptions,
|
|
14382
|
-
trigger: input.trigger ?? current.trigger
|
|
14501
|
+
trigger: input.trigger ?? current.trigger,
|
|
14502
|
+
triggers: input.triggers ?? current.triggers
|
|
14383
14503
|
};
|
|
14384
14504
|
if (Object.values(input).some((value) => value === void 0) || typeof candidate.enabled !== "boolean") {
|
|
14385
14505
|
throw new Error("invalid automation update");
|
|
@@ -14401,9 +14521,12 @@ function createMemoryStore8(opts) {
|
|
|
14401
14521
|
const removedDefinitions = /* @__PURE__ */ new Set();
|
|
14402
14522
|
const runs = /* @__PURE__ */ new Map();
|
|
14403
14523
|
const runInputSnapshots = /* @__PURE__ */ new Map();
|
|
14524
|
+
const activities = /* @__PURE__ */ new Map();
|
|
14525
|
+
const triggerCursors = /* @__PURE__ */ new Map();
|
|
14404
14526
|
const nodeOwners = /* @__PURE__ */ new Map();
|
|
14405
14527
|
const generateAutomationId = opts.generateAutomationId ?? (() => randomId4("rca2"));
|
|
14406
14528
|
const generateRunId = opts.generateRunId ?? (() => randomId4("rcar"));
|
|
14529
|
+
const generateActivityId = opts.generateActivityId ?? (() => randomId4("rcae"));
|
|
14407
14530
|
return {
|
|
14408
14531
|
mode: "memory-fallback",
|
|
14409
14532
|
getNodeOwner(nodeId) {
|
|
@@ -14489,8 +14612,8 @@ function createMemoryStore8(opts) {
|
|
|
14489
14612
|
const snapshot = runInputSnapshots.get(id);
|
|
14490
14613
|
return snapshot ? clone9(snapshot) : void 0;
|
|
14491
14614
|
},
|
|
14492
|
-
getRunByInvocationId(
|
|
14493
|
-
const run = [...runs.values()].find((candidate) => candidate.invocationId ===
|
|
14615
|
+
getRunByInvocationId(invocationId2) {
|
|
14616
|
+
const run = [...runs.values()].find((candidate) => candidate.invocationId === invocationId2);
|
|
14494
14617
|
return run ? clone9(run) : void 0;
|
|
14495
14618
|
},
|
|
14496
14619
|
getRunBySessionId(sessionId) {
|
|
@@ -14588,11 +14711,43 @@ function createMemoryStore8(opts) {
|
|
|
14588
14711
|
const bounded = Math.max(1, Math.min(1e3, Math.trunc(limit)));
|
|
14589
14712
|
return [...runs.values()].filter((run) => !automationId || run.automationId === automationId).sort((a, b) => b.createdAt - a.createdAt || b.id.localeCompare(a.id)).slice(0, bounded).map(clone9);
|
|
14590
14713
|
},
|
|
14714
|
+
getTriggerCursor(triggerId) {
|
|
14715
|
+
return triggerCursors.get(normalizeId(triggerId, "automation trigger id"));
|
|
14716
|
+
},
|
|
14717
|
+
setTriggerCursor(triggerId, minute) {
|
|
14718
|
+
if (!Number.isSafeInteger(minute) || minute < 0) throw new Error("invalid automation trigger cursor");
|
|
14719
|
+
triggerCursors.set(normalizeId(triggerId, "automation trigger id"), minute);
|
|
14720
|
+
},
|
|
14721
|
+
createActivity(input, now = Date.now()) {
|
|
14722
|
+
const id = normalizeId(generateActivityId(), "automation activity id");
|
|
14723
|
+
if (activities.has(id)) throw new Error("automation activity id already exists");
|
|
14724
|
+
const activity = { id, ...clone9(input), createdAt: now, updatedAt: now };
|
|
14725
|
+
activities.set(id, activity);
|
|
14726
|
+
return clone9(activity);
|
|
14727
|
+
},
|
|
14728
|
+
getActivityByInvocationId(invocationId2) {
|
|
14729
|
+
const normalized = normalizeId(invocationId2, "invocation id");
|
|
14730
|
+
const activity = [...activities.values()].find((candidate) => candidate.invocationId === normalized);
|
|
14731
|
+
return activity ? clone9(activity) : void 0;
|
|
14732
|
+
},
|
|
14733
|
+
updateActivity(id, input, now = Date.now()) {
|
|
14734
|
+
const current = activities.get(id);
|
|
14735
|
+
if (!current) return void 0;
|
|
14736
|
+
const next = { ...current, ...clone9(input), updatedAt: now };
|
|
14737
|
+
activities.set(id, next);
|
|
14738
|
+
return clone9(next);
|
|
14739
|
+
},
|
|
14740
|
+
listActivities(automationId, limit = 100) {
|
|
14741
|
+
const bounded = Math.max(1, Math.min(1e3, Math.trunc(limit)));
|
|
14742
|
+
return [...activities.values()].filter((activity) => !automationId || activity.automationId === automationId).sort((left, right) => right.createdAt - left.createdAt || right.id.localeCompare(left.id)).slice(0, bounded).map(clone9);
|
|
14743
|
+
},
|
|
14591
14744
|
close() {
|
|
14592
14745
|
definitions.clear();
|
|
14593
14746
|
removedDefinitions.clear();
|
|
14594
14747
|
runs.clear();
|
|
14595
14748
|
runInputSnapshots.clear();
|
|
14749
|
+
activities.clear();
|
|
14750
|
+
triggerCursors.clear();
|
|
14596
14751
|
nodeOwners.clear();
|
|
14597
14752
|
}
|
|
14598
14753
|
};
|
|
@@ -14609,7 +14764,11 @@ function definitionFromRow(row) {
|
|
|
14609
14764
|
cwd: row.cwd,
|
|
14610
14765
|
instruction: row.instruction,
|
|
14611
14766
|
runtimeOptions: JSON.parse(row.runtime_options_json),
|
|
14612
|
-
trigger:
|
|
14767
|
+
trigger: { type: "manual" },
|
|
14768
|
+
triggers: (() => {
|
|
14769
|
+
const parsed = JSON.parse(row.trigger_json);
|
|
14770
|
+
return Array.isArray(parsed) ? normalizeConfiguredTriggers(parsed) : [];
|
|
14771
|
+
})(),
|
|
14613
14772
|
revision: row.revision,
|
|
14614
14773
|
createdAt: row.created_at,
|
|
14615
14774
|
updatedAt: row.updated_at
|
|
@@ -14642,6 +14801,22 @@ function runInputSnapshotFromRow(row) {
|
|
|
14642
14801
|
bootstrapState: row.bootstrap_state
|
|
14643
14802
|
};
|
|
14644
14803
|
}
|
|
14804
|
+
function activityFromRow(row) {
|
|
14805
|
+
return {
|
|
14806
|
+
id: row.id,
|
|
14807
|
+
automationId: row.automation_id,
|
|
14808
|
+
triggerId: row.trigger_id,
|
|
14809
|
+
source: row.source,
|
|
14810
|
+
status: row.status,
|
|
14811
|
+
invocationId: row.invocation_id,
|
|
14812
|
+
...row.scheduled_for === null ? {} : { scheduledFor: row.scheduled_for },
|
|
14813
|
+
...row.missed_count === null ? {} : { missedCount: row.missed_count },
|
|
14814
|
+
...row.run_id === null ? {} : { runId: row.run_id },
|
|
14815
|
+
...row.failure_code === null ? {} : { failureCode: row.failure_code },
|
|
14816
|
+
createdAt: row.created_at,
|
|
14817
|
+
updatedAt: row.updated_at
|
|
14818
|
+
};
|
|
14819
|
+
}
|
|
14645
14820
|
function openSessionAutomationStore(opts) {
|
|
14646
14821
|
let Database;
|
|
14647
14822
|
try {
|
|
@@ -14705,6 +14880,27 @@ function openSessionAutomationStore(opts) {
|
|
|
14705
14880
|
bootstrap_state TEXT NOT NULL DEFAULT 'pending'
|
|
14706
14881
|
CHECK(bootstrap_state IN ('pending','submitting','submitted'))
|
|
14707
14882
|
);
|
|
14883
|
+
CREATE TABLE IF NOT EXISTS session_automation_trigger_cursors (
|
|
14884
|
+
trigger_id TEXT PRIMARY KEY,
|
|
14885
|
+
minute INTEGER NOT NULL CHECK(minute >= 0),
|
|
14886
|
+
updated_at INTEGER NOT NULL
|
|
14887
|
+
);
|
|
14888
|
+
CREATE TABLE IF NOT EXISTS session_automation_activities (
|
|
14889
|
+
id TEXT PRIMARY KEY,
|
|
14890
|
+
automation_id TEXT NOT NULL REFERENCES session_automations(id) ON DELETE CASCADE,
|
|
14891
|
+
trigger_id TEXT NOT NULL,
|
|
14892
|
+
source TEXT NOT NULL CHECK(source IN ('schedule','webhook')),
|
|
14893
|
+
status TEXT NOT NULL CHECK(status IN ('queued','started','failed','missed','expired')),
|
|
14894
|
+
invocation_id TEXT NOT NULL UNIQUE,
|
|
14895
|
+
scheduled_for INTEGER,
|
|
14896
|
+
missed_count INTEGER CHECK(missed_count IS NULL OR missed_count > 0),
|
|
14897
|
+
run_id TEXT REFERENCES session_automation_runs(id) ON DELETE SET NULL,
|
|
14898
|
+
failure_code TEXT,
|
|
14899
|
+
created_at INTEGER NOT NULL,
|
|
14900
|
+
updated_at INTEGER NOT NULL
|
|
14901
|
+
);
|
|
14902
|
+
CREATE INDEX IF NOT EXISTS session_automation_activities_definition_idx
|
|
14903
|
+
ON session_automation_activities(automation_id, created_at DESC);
|
|
14708
14904
|
`);
|
|
14709
14905
|
const automationColumns = db.pragma("table_info(session_automations)");
|
|
14710
14906
|
if (!automationColumns.some((column) => column.name === "deleted_at")) {
|
|
@@ -14718,6 +14914,7 @@ function openSessionAutomationStore(opts) {
|
|
|
14718
14914
|
}
|
|
14719
14915
|
const generateAutomationId = opts.generateAutomationId ?? (() => randomId4("rca2"));
|
|
14720
14916
|
const generateRunId = opts.generateRunId ?? (() => randomId4("rcar"));
|
|
14917
|
+
const generateActivityId = opts.generateActivityId ?? (() => randomId4("rcae"));
|
|
14721
14918
|
const getDefinition = db.prepare("SELECT * FROM session_automations WHERE id = ? AND deleted_at IS NULL");
|
|
14722
14919
|
const getDefinitionIncludingRemoved = db.prepare("SELECT * FROM session_automations WHERE id = ?");
|
|
14723
14920
|
const listAll = db.prepare("SELECT * FROM session_automations WHERE deleted_at IS NULL ORDER BY updated_at DESC, id");
|
|
@@ -14744,6 +14941,30 @@ function openSessionAutomationStore(opts) {
|
|
|
14744
14941
|
const listRunsForAutomation = db.prepare(
|
|
14745
14942
|
"SELECT * FROM session_automation_runs WHERE automation_id = ? ORDER BY created_at DESC, id DESC LIMIT ?"
|
|
14746
14943
|
);
|
|
14944
|
+
const getTriggerCursorStatement = db.prepare(
|
|
14945
|
+
"SELECT minute FROM session_automation_trigger_cursors WHERE trigger_id = ?"
|
|
14946
|
+
);
|
|
14947
|
+
const setTriggerCursorStatement = db.prepare(
|
|
14948
|
+
`INSERT INTO session_automation_trigger_cursors(trigger_id,minute,updated_at) VALUES(?,?,?)
|
|
14949
|
+
ON CONFLICT(trigger_id) DO UPDATE SET minute=excluded.minute,updated_at=excluded.updated_at`
|
|
14950
|
+
);
|
|
14951
|
+
const getActivity = db.prepare("SELECT * FROM session_automation_activities WHERE id = ?");
|
|
14952
|
+
const getActivityByInvocationId = db.prepare("SELECT * FROM session_automation_activities WHERE invocation_id = ?");
|
|
14953
|
+
const insertActivity = db.prepare(
|
|
14954
|
+
`INSERT INTO session_automation_activities(
|
|
14955
|
+
id,automation_id,trigger_id,source,status,invocation_id,scheduled_for,missed_count,run_id,failure_code,
|
|
14956
|
+
created_at,updated_at
|
|
14957
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`
|
|
14958
|
+
);
|
|
14959
|
+
const updateActivityStatement = db.prepare(
|
|
14960
|
+
`UPDATE session_automation_activities SET status=?,run_id=?,failure_code=?,updated_at=? WHERE id=?`
|
|
14961
|
+
);
|
|
14962
|
+
const listActivitiesAll = db.prepare(
|
|
14963
|
+
"SELECT * FROM session_automation_activities ORDER BY created_at DESC, id DESC LIMIT ?"
|
|
14964
|
+
);
|
|
14965
|
+
const listActivitiesForAutomation = db.prepare(
|
|
14966
|
+
"SELECT * FROM session_automation_activities WHERE automation_id = ? ORDER BY created_at DESC, id DESC LIMIT ?"
|
|
14967
|
+
);
|
|
14747
14968
|
return {
|
|
14748
14969
|
mode: "sqlite",
|
|
14749
14970
|
getNodeOwner(nodeId) {
|
|
@@ -14783,7 +15004,7 @@ function openSessionAutomationStore(opts) {
|
|
|
14783
15004
|
definition.cwd,
|
|
14784
15005
|
definition.instruction,
|
|
14785
15006
|
JSON.stringify(definition.runtimeOptions),
|
|
14786
|
-
JSON.stringify(definition.
|
|
15007
|
+
JSON.stringify(definition.triggers),
|
|
14787
15008
|
definition.revision,
|
|
14788
15009
|
definition.createdAt,
|
|
14789
15010
|
definition.updatedAt
|
|
@@ -14805,7 +15026,7 @@ function openSessionAutomationStore(opts) {
|
|
|
14805
15026
|
next.cwd,
|
|
14806
15027
|
next.instruction,
|
|
14807
15028
|
JSON.stringify(next.runtimeOptions),
|
|
14808
|
-
JSON.stringify(next.
|
|
15029
|
+
JSON.stringify(next.triggers),
|
|
14809
15030
|
next.revision,
|
|
14810
15031
|
next.updatedAt,
|
|
14811
15032
|
next.id,
|
|
@@ -14866,8 +15087,8 @@ function openSessionAutomationStore(opts) {
|
|
|
14866
15087
|
const row = selectRunInputSnapshot.get(id);
|
|
14867
15088
|
return row ? runInputSnapshotFromRow(row) : void 0;
|
|
14868
15089
|
},
|
|
14869
|
-
getRunByInvocationId(
|
|
14870
|
-
const row = getRunByInvocationId.get(
|
|
15090
|
+
getRunByInvocationId(invocationId2) {
|
|
15091
|
+
const row = getRunByInvocationId.get(invocationId2);
|
|
14871
15092
|
return row ? runFromRow(row) : void 0;
|
|
14872
15093
|
},
|
|
14873
15094
|
getRunBySessionId(sessionId) {
|
|
@@ -14978,6 +15199,63 @@ function openSessionAutomationStore(opts) {
|
|
|
14978
15199
|
const rows = automationId ? listRunsForAutomation.all(automationId, bounded) : listRunsAll.all(bounded);
|
|
14979
15200
|
return rows.map(runFromRow);
|
|
14980
15201
|
},
|
|
15202
|
+
getTriggerCursor(triggerId) {
|
|
15203
|
+
const row = getTriggerCursorStatement.get(normalizeId(triggerId, "automation trigger id"));
|
|
15204
|
+
return row?.minute;
|
|
15205
|
+
},
|
|
15206
|
+
setTriggerCursor(triggerId, minute) {
|
|
15207
|
+
if (!Number.isSafeInteger(minute) || minute < 0) throw new Error("invalid automation trigger cursor");
|
|
15208
|
+
setTriggerCursorStatement.run(normalizeId(triggerId, "automation trigger id"), minute, Date.now());
|
|
15209
|
+
},
|
|
15210
|
+
createActivity(input, now = Date.now()) {
|
|
15211
|
+
const activity = {
|
|
15212
|
+
id: normalizeId(generateActivityId(), "automation activity id"),
|
|
15213
|
+
...input,
|
|
15214
|
+
automationId: normalizeId(input.automationId, "automation id"),
|
|
15215
|
+
triggerId: normalizeId(input.triggerId, "automation trigger id"),
|
|
15216
|
+
invocationId: normalizeId(input.invocationId, "invocation id"),
|
|
15217
|
+
createdAt: now,
|
|
15218
|
+
updatedAt: now
|
|
15219
|
+
};
|
|
15220
|
+
insertActivity.run(
|
|
15221
|
+
activity.id,
|
|
15222
|
+
activity.automationId,
|
|
15223
|
+
activity.triggerId,
|
|
15224
|
+
activity.source,
|
|
15225
|
+
activity.status,
|
|
15226
|
+
activity.invocationId,
|
|
15227
|
+
activity.scheduledFor ?? null,
|
|
15228
|
+
activity.missedCount ?? null,
|
|
15229
|
+
activity.runId ?? null,
|
|
15230
|
+
activity.failureCode ?? null,
|
|
15231
|
+
activity.createdAt,
|
|
15232
|
+
activity.updatedAt
|
|
15233
|
+
);
|
|
15234
|
+
return activity;
|
|
15235
|
+
},
|
|
15236
|
+
getActivityByInvocationId(invocationId2) {
|
|
15237
|
+
const row = getActivityByInvocationId.get(normalizeId(invocationId2, "invocation id"));
|
|
15238
|
+
return row ? activityFromRow(row) : void 0;
|
|
15239
|
+
},
|
|
15240
|
+
updateActivity(id, input, now = Date.now()) {
|
|
15241
|
+
const row = getActivity.get(id);
|
|
15242
|
+
if (!row) return void 0;
|
|
15243
|
+
const current = activityFromRow(row);
|
|
15244
|
+
const next = { ...current, ...input, updatedAt: now };
|
|
15245
|
+
const result = updateActivityStatement.run(
|
|
15246
|
+
next.status,
|
|
15247
|
+
next.runId ?? null,
|
|
15248
|
+
next.failureCode ?? null,
|
|
15249
|
+
next.updatedAt,
|
|
15250
|
+
current.id
|
|
15251
|
+
);
|
|
15252
|
+
return result.changes === 1 ? next : void 0;
|
|
15253
|
+
},
|
|
15254
|
+
listActivities(automationId, limit = 100) {
|
|
15255
|
+
const bounded = Math.max(1, Math.min(1e3, Math.trunc(limit)));
|
|
15256
|
+
const rows = automationId ? listActivitiesForAutomation.all(automationId, bounded) : listActivitiesAll.all(bounded);
|
|
15257
|
+
return rows.map(activityFromRow);
|
|
15258
|
+
},
|
|
14981
15259
|
close() {
|
|
14982
15260
|
db.close();
|
|
14983
15261
|
}
|
|
@@ -15141,7 +15419,7 @@ function resolveVapidKeys(opts) {
|
|
|
15141
15419
|
}
|
|
15142
15420
|
|
|
15143
15421
|
// src/transport.ts
|
|
15144
|
-
import { createHash as createHash10, randomBytes as randomBytes17, randomUUID as
|
|
15422
|
+
import { createHash as createHash10, randomBytes as randomBytes17, randomUUID as randomUUID8, timingSafeEqual as timingSafeEqual6 } from "crypto";
|
|
15145
15423
|
import { basename as pathBasename, join as join17, resolve as resolvePath } from "path";
|
|
15146
15424
|
import { createReadStream } from "fs";
|
|
15147
15425
|
import Fastify2 from "fastify";
|
|
@@ -15322,9 +15600,206 @@ function terminalSharedDir(opts) {
|
|
|
15322
15600
|
return join11(terminalSharedBase(opts), opts.sessionId);
|
|
15323
15601
|
}
|
|
15324
15602
|
|
|
15603
|
+
// src/automation-trigger-engine.ts
|
|
15604
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
15605
|
+
function parsePart(part, min, max, normalize) {
|
|
15606
|
+
const [rangePart, stepPart] = part.split("/");
|
|
15607
|
+
const step = stepPart === void 0 ? 1 : Number(stepPart);
|
|
15608
|
+
if (!Number.isSafeInteger(step) || step < 1 || !rangePart) throw new Error("invalid cron field");
|
|
15609
|
+
let start;
|
|
15610
|
+
let end;
|
|
15611
|
+
if (rangePart === "*") {
|
|
15612
|
+
start = min;
|
|
15613
|
+
end = max;
|
|
15614
|
+
} else if (rangePart.includes("-")) {
|
|
15615
|
+
const pair = rangePart.split("-");
|
|
15616
|
+
if (pair.length !== 2) throw new Error("invalid cron field");
|
|
15617
|
+
start = Number(pair[0]);
|
|
15618
|
+
end = Number(pair[1]);
|
|
15619
|
+
} else {
|
|
15620
|
+
start = Number(rangePart);
|
|
15621
|
+
end = start;
|
|
15622
|
+
}
|
|
15623
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < min || end > max || start > end) {
|
|
15624
|
+
throw new Error("invalid cron field");
|
|
15625
|
+
}
|
|
15626
|
+
const values = [];
|
|
15627
|
+
for (let value = start; value <= end; value += step) values.push(normalize ? normalize(value) : value);
|
|
15628
|
+
return values;
|
|
15629
|
+
}
|
|
15630
|
+
function parseField(source, min, max, normalize) {
|
|
15631
|
+
const values = /* @__PURE__ */ new Set();
|
|
15632
|
+
for (const part of source.split(",")) {
|
|
15633
|
+
for (const value of parsePart(part, min, max, normalize)) values.add(value);
|
|
15634
|
+
}
|
|
15635
|
+
return { any: source === "*", values };
|
|
15636
|
+
}
|
|
15637
|
+
function validateCronExpression(expression) {
|
|
15638
|
+
const normalized = expression.trim().replace(/\s+/g, " ");
|
|
15639
|
+
const fields = normalized.split(" ");
|
|
15640
|
+
if (fields.length !== 5) throw new Error("cron must contain five fields");
|
|
15641
|
+
parseField(fields[0], 0, 59);
|
|
15642
|
+
parseField(fields[1], 0, 23);
|
|
15643
|
+
parseField(fields[2], 1, 31);
|
|
15644
|
+
parseField(fields[3], 1, 12);
|
|
15645
|
+
parseField(fields[4], 0, 7, (value) => value === 7 ? 0 : value);
|
|
15646
|
+
return normalized;
|
|
15647
|
+
}
|
|
15648
|
+
function zonedParts(time, timeZone) {
|
|
15649
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
15650
|
+
timeZone,
|
|
15651
|
+
minute: "2-digit",
|
|
15652
|
+
hour: "2-digit",
|
|
15653
|
+
hourCycle: "h23",
|
|
15654
|
+
day: "2-digit",
|
|
15655
|
+
month: "2-digit",
|
|
15656
|
+
weekday: "short"
|
|
15657
|
+
}).formatToParts(time);
|
|
15658
|
+
const byType = new Map(parts.map((part) => [part.type, part.value]));
|
|
15659
|
+
const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(byType.get("weekday") ?? "");
|
|
15660
|
+
return {
|
|
15661
|
+
minute: Number(byType.get("minute")),
|
|
15662
|
+
hour: Number(byType.get("hour")),
|
|
15663
|
+
day: Number(byType.get("day")),
|
|
15664
|
+
month: Number(byType.get("month")),
|
|
15665
|
+
weekday
|
|
15666
|
+
};
|
|
15667
|
+
}
|
|
15668
|
+
function cronMatches(expression, timeZone, time) {
|
|
15669
|
+
const fields = validateCronExpression(expression).split(" ");
|
|
15670
|
+
const minute = parseField(fields[0], 0, 59);
|
|
15671
|
+
const hour = parseField(fields[1], 0, 23);
|
|
15672
|
+
const day = parseField(fields[2], 1, 31);
|
|
15673
|
+
const month = parseField(fields[3], 1, 12);
|
|
15674
|
+
const weekday = parseField(fields[4], 0, 7, (value2) => value2 === 7 ? 0 : value2);
|
|
15675
|
+
const value = zonedParts(time, timeZone);
|
|
15676
|
+
const dayMatches = day.values.has(value.day);
|
|
15677
|
+
const weekdayMatches = weekday.values.has(value.weekday);
|
|
15678
|
+
const calendarMatches = day.any && weekday.any ? true : day.any ? weekdayMatches : weekday.any ? dayMatches : dayMatches || weekdayMatches;
|
|
15679
|
+
return minute.values.has(value.minute) && hour.values.has(value.hour) && month.values.has(value.month) && calendarMatches;
|
|
15680
|
+
}
|
|
15681
|
+
function invocationId(source, automationId, triggerId, nonce) {
|
|
15682
|
+
const safeNonce = nonce.replace(/[^A-Za-z0-9._:-]/g, "_").slice(0, 96);
|
|
15683
|
+
return `rci_${source}_${automationId}_${triggerId}_${safeNonce}`.slice(0, 256);
|
|
15684
|
+
}
|
|
15685
|
+
function createAutomationTriggerEngine(options) {
|
|
15686
|
+
const now = options.now ?? Date.now;
|
|
15687
|
+
const interval = options.setInterval ?? globalThis.setInterval;
|
|
15688
|
+
const clearInterval2 = options.clearInterval ?? globalThis.clearInterval;
|
|
15689
|
+
const concurrency = Number.isSafeInteger(options.concurrency) && (options.concurrency ?? 0) > 0 ? options.concurrency : 2;
|
|
15690
|
+
const pending = [];
|
|
15691
|
+
const pendingIds = /* @__PURE__ */ new Set();
|
|
15692
|
+
const inFlightIds = /* @__PURE__ */ new Set();
|
|
15693
|
+
let active = 0;
|
|
15694
|
+
let ticking = false;
|
|
15695
|
+
let timer;
|
|
15696
|
+
const queue = (activity, deferDrain = false) => {
|
|
15697
|
+
if (activity.status !== "queued" || pendingIds.has(activity.id) || inFlightIds.has(activity.id)) return;
|
|
15698
|
+
pendingIds.add(activity.id);
|
|
15699
|
+
pending.push(activity);
|
|
15700
|
+
pending.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id));
|
|
15701
|
+
if (!deferDrain) drain();
|
|
15702
|
+
};
|
|
15703
|
+
const drain = () => {
|
|
15704
|
+
while (active < concurrency && pending.length > 0) {
|
|
15705
|
+
const activity = pending.shift();
|
|
15706
|
+
pendingIds.delete(activity.id);
|
|
15707
|
+
inFlightIds.add(activity.id);
|
|
15708
|
+
active += 1;
|
|
15709
|
+
void options.execute(activity).then(({ runId }) => options.store.updateActivity(activity.id, { status: "started", runId })).catch(
|
|
15710
|
+
() => options.store.updateActivity(activity.id, { status: "failed", failureCode: "TRIGGER_EXECUTION_FAILED" })
|
|
15711
|
+
).finally(() => {
|
|
15712
|
+
inFlightIds.delete(activity.id);
|
|
15713
|
+
active -= 1;
|
|
15714
|
+
drain();
|
|
15715
|
+
});
|
|
15716
|
+
}
|
|
15717
|
+
};
|
|
15718
|
+
const createQueued = (automation, trigger, source, nonce, scheduledFor) => {
|
|
15719
|
+
const id = invocationId(source, automation.id, trigger.id, nonce);
|
|
15720
|
+
const existing = options.store.getActivityByInvocationId(id);
|
|
15721
|
+
if (existing) {
|
|
15722
|
+
queue(existing);
|
|
15723
|
+
return existing;
|
|
15724
|
+
}
|
|
15725
|
+
const activity = options.store.createActivity({
|
|
15726
|
+
automationId: automation.id,
|
|
15727
|
+
triggerId: trigger.id,
|
|
15728
|
+
source,
|
|
15729
|
+
status: "queued",
|
|
15730
|
+
invocationId: id,
|
|
15731
|
+
...scheduledFor === void 0 ? {} : { scheduledFor }
|
|
15732
|
+
});
|
|
15733
|
+
queue(activity);
|
|
15734
|
+
return activity;
|
|
15735
|
+
};
|
|
15736
|
+
const tick = async (at = now()) => {
|
|
15737
|
+
if (ticking) return;
|
|
15738
|
+
ticking = true;
|
|
15739
|
+
try {
|
|
15740
|
+
const currentMinute = Math.floor(at / 6e4);
|
|
15741
|
+
for (const automation of options.store.list()) {
|
|
15742
|
+
if (!automation.enabled) continue;
|
|
15743
|
+
for (const trigger of automation.triggers) {
|
|
15744
|
+
if (trigger.type !== "schedule" || !trigger.enabled) continue;
|
|
15745
|
+
const previous = options.store.getTriggerCursor(trigger.id) ?? currentMinute - 1;
|
|
15746
|
+
if (previous >= currentMinute) continue;
|
|
15747
|
+
const firstMinute = Math.max(previous + 1, currentMinute - 43200);
|
|
15748
|
+
const matches = [];
|
|
15749
|
+
for (let minute = firstMinute; minute <= currentMinute; minute += 1) {
|
|
15750
|
+
if (cronMatches(trigger.cron, trigger.timeZone, minute * 6e4)) matches.push(minute);
|
|
15751
|
+
}
|
|
15752
|
+
const missed = matches.filter((minute) => minute < currentMinute);
|
|
15753
|
+
if (missed.length > 0) {
|
|
15754
|
+
const missedInvocation = invocationId("schedule", automation.id, trigger.id, `missed-${currentMinute}`);
|
|
15755
|
+
const exists = options.store.listActivities(automation.id, 1e3).some((activity) => activity.invocationId === missedInvocation);
|
|
15756
|
+
if (!exists) {
|
|
15757
|
+
options.store.createActivity({
|
|
15758
|
+
automationId: automation.id,
|
|
15759
|
+
triggerId: trigger.id,
|
|
15760
|
+
source: "schedule",
|
|
15761
|
+
status: "missed",
|
|
15762
|
+
invocationId: missedInvocation,
|
|
15763
|
+
scheduledFor: missed[missed.length - 1] * 6e4,
|
|
15764
|
+
missedCount: missed.length,
|
|
15765
|
+
failureCode: "NODE_OFFLINE_MISSED_SCHEDULE"
|
|
15766
|
+
});
|
|
15767
|
+
}
|
|
15768
|
+
}
|
|
15769
|
+
if (matches.includes(currentMinute)) {
|
|
15770
|
+
createQueued(automation, trigger, "schedule", String(currentMinute), currentMinute * 6e4);
|
|
15771
|
+
}
|
|
15772
|
+
options.store.setTriggerCursor(trigger.id, currentMinute);
|
|
15773
|
+
}
|
|
15774
|
+
}
|
|
15775
|
+
} finally {
|
|
15776
|
+
ticking = false;
|
|
15777
|
+
}
|
|
15778
|
+
};
|
|
15779
|
+
return {
|
|
15780
|
+
start() {
|
|
15781
|
+
if (timer) return;
|
|
15782
|
+
for (const activity of options.store.listActivities(void 0, 1e3)) queue(activity, true);
|
|
15783
|
+
drain();
|
|
15784
|
+
void tick();
|
|
15785
|
+
timer = interval(() => void tick(), 15e3);
|
|
15786
|
+
},
|
|
15787
|
+
stop() {
|
|
15788
|
+
if (!timer) return;
|
|
15789
|
+
clearInterval2(timer);
|
|
15790
|
+
timer = void 0;
|
|
15791
|
+
},
|
|
15792
|
+
tick,
|
|
15793
|
+
enqueueWebhook(automation, trigger, externalInvocationId) {
|
|
15794
|
+
if (trigger.type !== "webhook") throw new Error("invalid webhook trigger");
|
|
15795
|
+
return createQueued(automation, trigger, "webhook", externalInvocationId ?? randomUUID5());
|
|
15796
|
+
}
|
|
15797
|
+
};
|
|
15798
|
+
}
|
|
15799
|
+
|
|
15325
15800
|
// src/updater.ts
|
|
15326
15801
|
import { spawn as nodeSpawn } from "child_process";
|
|
15327
|
-
import { randomUUID as
|
|
15802
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
15328
15803
|
import {
|
|
15329
15804
|
chmodSync as nodeChmodSync,
|
|
15330
15805
|
existsSync as nodeExistsSync,
|
|
@@ -15338,7 +15813,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
15338
15813
|
|
|
15339
15814
|
// src/managed-runtime.ts
|
|
15340
15815
|
import { spawn as spawn3 } from "child_process";
|
|
15341
|
-
import { randomUUID as
|
|
15816
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
15342
15817
|
import {
|
|
15343
15818
|
chmodSync as chmodSync4,
|
|
15344
15819
|
existsSync as existsSync5,
|
|
@@ -15615,7 +16090,7 @@ function compareVersions(a, b) {
|
|
|
15615
16090
|
}
|
|
15616
16091
|
function atomicWrite(path, value, mode = 384) {
|
|
15617
16092
|
mkdirSync3(dirname9(path), { recursive: true, mode: 448 });
|
|
15618
|
-
const temp = `${path}.${process.pid}.${
|
|
16093
|
+
const temp = `${path}.${process.pid}.${randomUUID6()}.tmp`;
|
|
15619
16094
|
writeFileSync8(temp, value, { mode });
|
|
15620
16095
|
chmodSync4(temp, mode);
|
|
15621
16096
|
renameSync5(temp, path);
|
|
@@ -15759,7 +16234,7 @@ async function npmIntegrity(npmCommand, packageName, version, nodePath, log) {
|
|
|
15759
16234
|
return parsed;
|
|
15760
16235
|
}
|
|
15761
16236
|
async function smokeServer(serverEntry, dataDir, nodePath, log) {
|
|
15762
|
-
const smokeDir = join13(tmpdir(), `roamcode-smoke-${process.pid}-${
|
|
16237
|
+
const smokeDir = join13(tmpdir(), `roamcode-smoke-${process.pid}-${randomUUID6()}`);
|
|
15763
16238
|
mkdirSync3(smokeDir, { recursive: true, mode: 448 });
|
|
15764
16239
|
let output = "";
|
|
15765
16240
|
const child = spawn3(nodePath, [serverEntry], {
|
|
@@ -15767,7 +16242,7 @@ async function smokeServer(serverEntry, dataDir, nodePath, log) {
|
|
|
15767
16242
|
...process.env,
|
|
15768
16243
|
PORT: "0",
|
|
15769
16244
|
BIND_ADDRESS: "127.0.0.1",
|
|
15770
|
-
ACCESS_TOKEN: `rc-smoke-${
|
|
16245
|
+
ACCESS_TOKEN: `rc-smoke-${randomUUID6()}`,
|
|
15771
16246
|
ROAMCODE_DATA_DIR: smokeDir,
|
|
15772
16247
|
RC_TMUX_SOCKET: `rc-smoke-${process.pid}`,
|
|
15773
16248
|
ROAMCODE_INSTALL_ROOT: "",
|
|
@@ -15810,7 +16285,7 @@ async function smokeServer(serverEntry, dataDir, nodePath, log) {
|
|
|
15810
16285
|
}
|
|
15811
16286
|
}
|
|
15812
16287
|
function replaceSymlink(link2, target) {
|
|
15813
|
-
const temp = `${link2}.${process.pid}.${
|
|
16288
|
+
const temp = `${link2}.${process.pid}.${randomUUID6()}.new`;
|
|
15814
16289
|
try {
|
|
15815
16290
|
unlinkSync6(temp);
|
|
15816
16291
|
} catch {
|
|
@@ -15826,7 +16301,7 @@ function trimLog(log) {
|
|
|
15826
16301
|
async function installManagedRelease(opts) {
|
|
15827
16302
|
if (!isStableVersion(opts.version)) throw new Error(`invalid release version: ${opts.version}`);
|
|
15828
16303
|
const now = opts.now ?? Date.now;
|
|
15829
|
-
const operationId = opts.operationId ??
|
|
16304
|
+
const operationId = opts.operationId ?? randomUUID6();
|
|
15830
16305
|
const nodePath = opts.nodePath ?? process.execPath;
|
|
15831
16306
|
const npmCommand = opts.npmCommand ?? process.env.npm_execpath ?? "npm";
|
|
15832
16307
|
const expectedIntegrities = opts.expectedIntegrities ?? (opts.expectedIntegrity ? { roamcode: opts.expectedIntegrity } : {});
|
|
@@ -15835,7 +16310,7 @@ async function installManagedRelease(opts) {
|
|
|
15835
16310
|
mkdirSync3(paths.releases, { recursive: true, mode: 448 });
|
|
15836
16311
|
mkdirSync3(paths.staging, { recursive: true, mode: 448 });
|
|
15837
16312
|
const release = join13(paths.releases, opts.version);
|
|
15838
|
-
const stage = join13(paths.staging, `${opts.version}-${process.pid}-${
|
|
16313
|
+
const stage = join13(paths.staging, `${opts.version}-${process.pid}-${randomUUID6()}`);
|
|
15839
16314
|
let logText = "";
|
|
15840
16315
|
const log = (line) => {
|
|
15841
16316
|
logText += line.endsWith("\n") ? line : `${line}
|
|
@@ -15970,7 +16445,7 @@ async function installManagedRelease(opts) {
|
|
|
15970
16445
|
}
|
|
15971
16446
|
|
|
15972
16447
|
// src/updater.ts
|
|
15973
|
-
var RUNNING_VERSION = "1.
|
|
16448
|
+
var RUNNING_VERSION = "1.4.1" ? "1.4.1".replace(/^v/, "") : packageVersion();
|
|
15974
16449
|
var RUNNING_BUILD = RUNNING_VERSION;
|
|
15975
16450
|
var RELEASES_API = "https://api.github.com/repos/burakgon/roamcode/releases?per_page=100";
|
|
15976
16451
|
var RELEASE_MANIFEST_ASSET = "roamcode-release.json";
|
|
@@ -16271,7 +16746,7 @@ var Updater = class {
|
|
|
16271
16746
|
const path = join14(this.dataDir, "update-status.json");
|
|
16272
16747
|
const value = JSON.stringify(status, null, 2) + "\n";
|
|
16273
16748
|
if (this.fs.renameSync) {
|
|
16274
|
-
const temp = `${path}.${process.pid}.${
|
|
16749
|
+
const temp = `${path}.${process.pid}.${randomUUID7()}.tmp`;
|
|
16275
16750
|
this.fs.writeFileSync(temp, value, 384);
|
|
16276
16751
|
this.fs.renameSync(temp, path);
|
|
16277
16752
|
} else {
|
|
@@ -16334,7 +16809,7 @@ var Updater = class {
|
|
|
16334
16809
|
return { started: false, reason: error instanceof Error ? error.message : String(error) };
|
|
16335
16810
|
}
|
|
16336
16811
|
}
|
|
16337
|
-
const operationId =
|
|
16812
|
+
const operationId = randomUUID7();
|
|
16338
16813
|
const status = {
|
|
16339
16814
|
operationId,
|
|
16340
16815
|
state: "starting",
|
|
@@ -19758,9 +20233,12 @@ function buildOpenApiDocument(options) {
|
|
|
19758
20233
|
responses: {
|
|
19759
20234
|
"201": response("Created session automation", {
|
|
19760
20235
|
type: "object",
|
|
19761
|
-
required: ["automation"],
|
|
20236
|
+
required: ["automation", "webhookSecrets"],
|
|
19762
20237
|
additionalProperties: false,
|
|
19763
|
-
properties: {
|
|
20238
|
+
properties: {
|
|
20239
|
+
automation: ref("SessionAutomationDefinition"),
|
|
20240
|
+
webhookSecrets: { type: "array", items: ref("SessionAutomationWebhookSecret") }
|
|
20241
|
+
}
|
|
19764
20242
|
}),
|
|
19765
20243
|
"404": response("Node or runtime not found", ref("Error")),
|
|
19766
20244
|
...errorResponses
|
|
@@ -19789,9 +20267,12 @@ function buildOpenApiDocument(options) {
|
|
|
19789
20267
|
responses: {
|
|
19790
20268
|
"200": response("Updated session automation", {
|
|
19791
20269
|
type: "object",
|
|
19792
|
-
required: ["automation"],
|
|
20270
|
+
required: ["automation", "webhookSecrets"],
|
|
19793
20271
|
additionalProperties: false,
|
|
19794
|
-
properties: {
|
|
20272
|
+
properties: {
|
|
20273
|
+
automation: ref("SessionAutomationDefinition"),
|
|
20274
|
+
webhookSecrets: { type: "array", items: ref("SessionAutomationWebhookSecret") }
|
|
20275
|
+
}
|
|
19795
20276
|
}),
|
|
19796
20277
|
"404": response("Automation, node, or runtime not found", ref("Error")),
|
|
19797
20278
|
...errorResponses
|
|
@@ -19807,6 +20288,76 @@ function buildOpenApiDocument(options) {
|
|
|
19807
20288
|
}
|
|
19808
20289
|
}
|
|
19809
20290
|
},
|
|
20291
|
+
"/api/v2/automations/{automationId}/activity": {
|
|
20292
|
+
get: {
|
|
20293
|
+
operationId: "listSessionAutomationActivityV2",
|
|
20294
|
+
description: "Returns durable schedule and webhook delivery activity, including explicit missed schedules.",
|
|
20295
|
+
parameters: [
|
|
20296
|
+
idParameter("automationId"),
|
|
20297
|
+
{ name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 100, default: 25 } }
|
|
20298
|
+
],
|
|
20299
|
+
responses: {
|
|
20300
|
+
"200": response("Bounded automation trigger activity", {
|
|
20301
|
+
type: "object",
|
|
20302
|
+
required: ["activities"],
|
|
20303
|
+
additionalProperties: false,
|
|
20304
|
+
properties: { activities: { type: "array", items: ref("SessionAutomationActivity") } }
|
|
20305
|
+
}),
|
|
20306
|
+
"404": response("Automation not found", ref("Error")),
|
|
20307
|
+
...errorResponses
|
|
20308
|
+
}
|
|
20309
|
+
}
|
|
20310
|
+
},
|
|
20311
|
+
"/api/v2/automations/{automationId}/triggers/{triggerId}/secret": {
|
|
20312
|
+
post: {
|
|
20313
|
+
operationId: "rotateSessionAutomationWebhookSecretV2",
|
|
20314
|
+
parameters: [idParameter("automationId"), idParameter("triggerId"), idempotency],
|
|
20315
|
+
requestBody: {
|
|
20316
|
+
required: true,
|
|
20317
|
+
content: json({
|
|
20318
|
+
type: "object",
|
|
20319
|
+
required: ["expectedRevision"],
|
|
20320
|
+
additionalProperties: false,
|
|
20321
|
+
properties: { expectedRevision: { type: "integer", minimum: 1 } }
|
|
20322
|
+
})
|
|
20323
|
+
},
|
|
20324
|
+
responses: {
|
|
20325
|
+
"200": response("Rotated webhook secret shown exactly once", {
|
|
20326
|
+
type: "object",
|
|
20327
|
+
required: ["automation", "webhookSecret"],
|
|
20328
|
+
additionalProperties: false,
|
|
20329
|
+
properties: {
|
|
20330
|
+
automation: ref("SessionAutomationDefinition"),
|
|
20331
|
+
webhookSecret: ref("SessionAutomationWebhookSecret")
|
|
20332
|
+
}
|
|
20333
|
+
}),
|
|
20334
|
+
"404": response("Automation or webhook trigger not found", ref("Error")),
|
|
20335
|
+
...errorResponses
|
|
20336
|
+
}
|
|
20337
|
+
}
|
|
20338
|
+
},
|
|
20339
|
+
"/api/v2/automation-hooks/{hookId}": {
|
|
20340
|
+
post: {
|
|
20341
|
+
operationId: "signalSessionAutomationWebhookV2",
|
|
20342
|
+
description: "Queues a signal-only webhook. The request body is discarded and never becomes task instruction or stored prompt content.",
|
|
20343
|
+
security: [{ webhookAuth: [] }],
|
|
20344
|
+
parameters: [idParameter("hookId")],
|
|
20345
|
+
requestBody: {
|
|
20346
|
+
required: false,
|
|
20347
|
+
content: json({ description: "Ignored signal payload", nullable: true })
|
|
20348
|
+
},
|
|
20349
|
+
responses: {
|
|
20350
|
+
"202": response("Webhook signal accepted", {
|
|
20351
|
+
type: "object",
|
|
20352
|
+
required: ["accepted"],
|
|
20353
|
+
additionalProperties: false,
|
|
20354
|
+
properties: { accepted: { const: true } }
|
|
20355
|
+
}),
|
|
20356
|
+
"401": response("Unknown hook or invalid webhook secret", ref("Error")),
|
|
20357
|
+
"429": response("Webhook rate limit reached", ref("Error"))
|
|
20358
|
+
}
|
|
20359
|
+
}
|
|
20360
|
+
},
|
|
19810
20361
|
"/api/v2/automations/{automationId}/runs": {
|
|
19811
20362
|
get: {
|
|
19812
20363
|
operationId: "listSessionAutomationRunsV2",
|
|
@@ -19864,7 +20415,8 @@ function buildOpenApiDocument(options) {
|
|
|
19864
20415
|
},
|
|
19865
20416
|
components: {
|
|
19866
20417
|
securitySchemes: {
|
|
19867
|
-
bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "RoamCode device credential" }
|
|
20418
|
+
bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "RoamCode device credential" },
|
|
20419
|
+
webhookAuth: { type: "http", scheme: "bearer", bearerFormat: "One-time-shown webhook secret" }
|
|
19868
20420
|
},
|
|
19869
20421
|
schemas: {
|
|
19870
20422
|
Error: {
|
|
@@ -20838,6 +21390,62 @@ function buildOpenApiDocument(options) {
|
|
|
20838
21390
|
additionalProperties: false,
|
|
20839
21391
|
properties: { type: { const: "manual" } }
|
|
20840
21392
|
},
|
|
21393
|
+
SessionAutomationConfiguredTrigger: {
|
|
21394
|
+
oneOf: [
|
|
21395
|
+
{
|
|
21396
|
+
type: "object",
|
|
21397
|
+
required: ["id", "type", "enabled", "cron", "timeZone", "missedRunPolicy"],
|
|
21398
|
+
additionalProperties: false,
|
|
21399
|
+
properties: {
|
|
21400
|
+
id: { type: "string", minLength: 1, maxLength: 256 },
|
|
21401
|
+
type: { const: "schedule" },
|
|
21402
|
+
enabled: { type: "boolean" },
|
|
21403
|
+
cron: { type: "string", minLength: 9, maxLength: 120 },
|
|
21404
|
+
timeZone: { type: "string", minLength: 1, maxLength: 80 },
|
|
21405
|
+
missedRunPolicy: { const: "skip" }
|
|
21406
|
+
}
|
|
21407
|
+
},
|
|
21408
|
+
{
|
|
21409
|
+
type: "object",
|
|
21410
|
+
required: ["id", "type", "enabled", "hookId"],
|
|
21411
|
+
additionalProperties: false,
|
|
21412
|
+
properties: {
|
|
21413
|
+
id: { type: "string", minLength: 1, maxLength: 256 },
|
|
21414
|
+
type: { const: "webhook" },
|
|
21415
|
+
enabled: { type: "boolean" },
|
|
21416
|
+
hookId: { type: "string", pattern: "^rcwh_[A-Za-z0-9_-]{24,80}$" }
|
|
21417
|
+
}
|
|
21418
|
+
}
|
|
21419
|
+
]
|
|
21420
|
+
},
|
|
21421
|
+
SessionAutomationTriggerInput: {
|
|
21422
|
+
oneOf: [
|
|
21423
|
+
{
|
|
21424
|
+
type: "object",
|
|
21425
|
+
required: ["type", "enabled", "cron", "timeZone", "missedRunPolicy"],
|
|
21426
|
+
additionalProperties: false,
|
|
21427
|
+
properties: {
|
|
21428
|
+
id: { type: "string", minLength: 1, maxLength: 256 },
|
|
21429
|
+
type: { const: "schedule" },
|
|
21430
|
+
enabled: { type: "boolean" },
|
|
21431
|
+
cron: { type: "string", minLength: 9, maxLength: 120 },
|
|
21432
|
+
timeZone: { type: "string", minLength: 1, maxLength: 80 },
|
|
21433
|
+
missedRunPolicy: { const: "skip" }
|
|
21434
|
+
}
|
|
21435
|
+
},
|
|
21436
|
+
{
|
|
21437
|
+
type: "object",
|
|
21438
|
+
required: ["type", "enabled"],
|
|
21439
|
+
additionalProperties: false,
|
|
21440
|
+
properties: {
|
|
21441
|
+
id: { type: "string", minLength: 1, maxLength: 256 },
|
|
21442
|
+
type: { const: "webhook" },
|
|
21443
|
+
enabled: { type: "boolean" },
|
|
21444
|
+
hookId: { type: "string", pattern: "^rcwh_[A-Za-z0-9_-]{24,80}$" }
|
|
21445
|
+
}
|
|
21446
|
+
}
|
|
21447
|
+
]
|
|
21448
|
+
},
|
|
20841
21449
|
SessionAutomationDefinition: {
|
|
20842
21450
|
type: "object",
|
|
20843
21451
|
required: [
|
|
@@ -20850,6 +21458,7 @@ function buildOpenApiDocument(options) {
|
|
|
20850
21458
|
"provider",
|
|
20851
21459
|
"cwd",
|
|
20852
21460
|
"trigger",
|
|
21461
|
+
"triggers",
|
|
20853
21462
|
"instruction",
|
|
20854
21463
|
"runtimeOptions",
|
|
20855
21464
|
"revision",
|
|
@@ -20867,6 +21476,7 @@ function buildOpenApiDocument(options) {
|
|
|
20867
21476
|
provider: { type: "string", pattern: "^[a-z][a-z0-9-]{0,63}$" },
|
|
20868
21477
|
cwd: { type: "string", minLength: 1 },
|
|
20869
21478
|
trigger: ref("SessionAutomationTrigger"),
|
|
21479
|
+
triggers: { type: "array", maxItems: 16, items: ref("SessionAutomationConfiguredTrigger") },
|
|
20870
21480
|
instruction: {
|
|
20871
21481
|
type: "string",
|
|
20872
21482
|
minLength: 1,
|
|
@@ -20891,6 +21501,7 @@ function buildOpenApiDocument(options) {
|
|
|
20891
21501
|
agentRuntimeId: { type: "string", pattern: "^runtime_[A-Za-z0-9_-]{24}$" },
|
|
20892
21502
|
cwd: { type: "string", minLength: 1 },
|
|
20893
21503
|
trigger: ref("SessionAutomationTrigger"),
|
|
21504
|
+
triggers: { type: "array", maxItems: 16, items: ref("SessionAutomationTriggerInput") },
|
|
20894
21505
|
instruction: {
|
|
20895
21506
|
type: "string",
|
|
20896
21507
|
minLength: 1,
|
|
@@ -20914,6 +21525,7 @@ function buildOpenApiDocument(options) {
|
|
|
20914
21525
|
agentRuntimeId: { type: "string", pattern: "^runtime_[A-Za-z0-9_-]{24}$" },
|
|
20915
21526
|
cwd: { type: "string", minLength: 1 },
|
|
20916
21527
|
trigger: ref("SessionAutomationTrigger"),
|
|
21528
|
+
triggers: { type: "array", maxItems: 16, items: ref("SessionAutomationTriggerInput") },
|
|
20917
21529
|
instruction: {
|
|
20918
21530
|
type: "string",
|
|
20919
21531
|
minLength: 1,
|
|
@@ -20924,6 +21536,36 @@ function buildOpenApiDocument(options) {
|
|
|
20924
21536
|
runtimeOptions: { type: "object", additionalProperties: true, "x-maxBytes": 65536 }
|
|
20925
21537
|
}
|
|
20926
21538
|
},
|
|
21539
|
+
SessionAutomationWebhookSecret: {
|
|
21540
|
+
type: "object",
|
|
21541
|
+
required: ["triggerId", "hookId", "secret", "path"],
|
|
21542
|
+
additionalProperties: false,
|
|
21543
|
+
properties: {
|
|
21544
|
+
triggerId: { type: "string", minLength: 1, maxLength: 256 },
|
|
21545
|
+
hookId: { type: "string", pattern: "^rcwh_[A-Za-z0-9_-]{24,80}$" },
|
|
21546
|
+
secret: { type: "string", pattern: "^rcws_[A-Za-z0-9_-]{43}$" },
|
|
21547
|
+
path: { type: "string", pattern: "^/api/v2/automation-hooks/rcwh_[A-Za-z0-9_-]{24,80}$" }
|
|
21548
|
+
}
|
|
21549
|
+
},
|
|
21550
|
+
SessionAutomationActivity: {
|
|
21551
|
+
type: "object",
|
|
21552
|
+
required: ["id", "automationId", "triggerId", "source", "status", "invocationId", "createdAt", "updatedAt"],
|
|
21553
|
+
additionalProperties: false,
|
|
21554
|
+
properties: {
|
|
21555
|
+
id: { type: "string", minLength: 1, maxLength: 256 },
|
|
21556
|
+
automationId: { type: "string", minLength: 1, maxLength: 256 },
|
|
21557
|
+
triggerId: { type: "string", minLength: 1, maxLength: 256 },
|
|
21558
|
+
source: { enum: ["schedule", "webhook"] },
|
|
21559
|
+
status: { enum: ["queued", "started", "failed", "missed", "expired"] },
|
|
21560
|
+
invocationId: { type: "string", minLength: 1, maxLength: 256 },
|
|
21561
|
+
scheduledFor: { type: "integer", minimum: 0 },
|
|
21562
|
+
missedCount: { type: "integer", minimum: 1 },
|
|
21563
|
+
runId: { type: "string", minLength: 1, maxLength: 256 },
|
|
21564
|
+
failureCode: { type: "string", pattern: "^[A-Z][A-Z0-9_]{0,79}$" },
|
|
21565
|
+
createdAt: { type: "integer", minimum: 0 },
|
|
21566
|
+
updatedAt: { type: "integer", minimum: 0 }
|
|
21567
|
+
}
|
|
21568
|
+
},
|
|
20927
21569
|
SessionAutomationRun: {
|
|
20928
21570
|
description: "Privacy-bounded run state with its exact execution binding; terminal transcript, instruction, and provider detail are omitted.",
|
|
20929
21571
|
type: "object",
|
|
@@ -21358,9 +22000,9 @@ function cloudStatusResponse(status) {
|
|
|
21358
22000
|
};
|
|
21359
22001
|
}
|
|
21360
22002
|
const authorization = status.authorization;
|
|
21361
|
-
const syncFailures = status.authorizationFailures > 0 || status.heartbeatFailures > 0;
|
|
22003
|
+
const syncFailures = status.authorizationFailures > 0 || status.heartbeatFailures > 0 || status.automationFailures > 0;
|
|
21362
22004
|
const syncState = authorization.status === "expired" ? "expired" : authorization.status === "pending" ? "pending" : authorization.status === "unavailable" ? syncFailures ? "degraded" : "syncing" : syncFailures ? "degraded" : "healthy";
|
|
21363
|
-
const action = status.authorizationIssue === "credential-rejected" || status.authorizationIssue === "trust-expired" ? "reauthorize-host" : status.authorizationIssue === "invalid-control-plane-response" || status.authorizationIssue === "authorization-verification-failed" ? "contact-organization-admin" : status.authorizationIssue === "connectivity" ? "check-host-connectivity" : status.heartbeatFailures > 0 ? "check-host-connectivity" : authorization.status === "unavailable" ? "wait-for-cloud-sync" : authorization.status === "pending" ? "wait-for-authorization-activation" : authorization.status === "expired" ? "check-host-connectivity" : "none";
|
|
22005
|
+
const action = status.authorizationIssue === "credential-rejected" || status.authorizationIssue === "trust-expired" ? "reauthorize-host" : status.authorizationIssue === "invalid-control-plane-response" || status.authorizationIssue === "authorization-verification-failed" ? "contact-organization-admin" : status.authorizationIssue === "connectivity" ? "check-host-connectivity" : status.heartbeatFailures > 0 || status.automationFailures > 0 ? "check-host-connectivity" : authorization.status === "unavailable" ? "wait-for-cloud-sync" : authorization.status === "pending" ? "wait-for-authorization-activation" : authorization.status === "expired" ? "check-host-connectivity" : "none";
|
|
21364
22006
|
return {
|
|
21365
22007
|
v: 1,
|
|
21366
22008
|
mode: "managed",
|
|
@@ -21797,6 +22439,7 @@ function createServer(config, deps = {}) {
|
|
|
21797
22439
|
enabled: config.rateLimitRpm > 0
|
|
21798
22440
|
});
|
|
21799
22441
|
const pairingRateLimiter = new RateLimiter({ capacity: 30, windowMs: 6e4, burst: 10 });
|
|
22442
|
+
const automationWebhookRateLimiter = new RateLimiter({ capacity: 120, windowMs: 6e4, burst: 30 });
|
|
21800
22443
|
const fsService = new FsService({ root: config.fsRoot });
|
|
21801
22444
|
const terminalSharedRoot = terminalSharedBase({ dataDir, fsRoot: config.fsRoot });
|
|
21802
22445
|
const backfilledFileSessions = /* @__PURE__ */ new Set();
|
|
@@ -21816,7 +22459,7 @@ function createServer(config, deps = {}) {
|
|
|
21816
22459
|
const now = Date.now();
|
|
21817
22460
|
const media = attachmentMedia(file.filename);
|
|
21818
22461
|
store.putFile({
|
|
21819
|
-
id:
|
|
22462
|
+
id: randomUUID8(),
|
|
21820
22463
|
sessionId,
|
|
21821
22464
|
direction: "sent",
|
|
21822
22465
|
storage: "managed",
|
|
@@ -21888,6 +22531,7 @@ function createServer(config, deps = {}) {
|
|
|
21888
22531
|
}
|
|
21889
22532
|
const relayInternalCapability = randomBytes17(32).toString("base64url");
|
|
21890
22533
|
const authenticatedPrincipals = /* @__PURE__ */ new WeakMap();
|
|
22534
|
+
const automationWebhookRequests = /* @__PURE__ */ new WeakMap();
|
|
21891
22535
|
const hostPrincipal = () => ({
|
|
21892
22536
|
actorType: config.accessToken ? "host" : "local",
|
|
21893
22537
|
actorId: commandStore.getHost().id,
|
|
@@ -22034,6 +22678,28 @@ function createServer(config, deps = {}) {
|
|
|
22034
22678
|
if (request.is404 && isPublicPath(path)) return;
|
|
22035
22679
|
}
|
|
22036
22680
|
if (path === "/health") return;
|
|
22681
|
+
const automationHookMatch = /^\/api\/v2\/automation-hooks\/(rcwh_[A-Za-z0-9_-]{24,80})$/.exec(path);
|
|
22682
|
+
if (request.method === "POST" && automationHookMatch && !hasEncodedSep(request.url)) {
|
|
22683
|
+
const limit = automationWebhookRateLimiter.take(request.ip);
|
|
22684
|
+
if (!limit.allowed) {
|
|
22685
|
+
reply.header("retry-after", String(limit.retryAfterSeconds)).code(429).send({ error: "rate limited" });
|
|
22686
|
+
return;
|
|
22687
|
+
}
|
|
22688
|
+
const hookId = automationHookMatch[1];
|
|
22689
|
+
const match = sessionAutomationStore.list().flatMap((automation) => automation.triggers.map((trigger) => ({ automation, trigger }))).find(
|
|
22690
|
+
(entry) => entry.automation.enabled && entry.trigger.type === "webhook" && entry.trigger.enabled && entry.trigger.hookId === hookId
|
|
22691
|
+
);
|
|
22692
|
+
const secret = extractBearerToken(request.headers.authorization);
|
|
22693
|
+
const presented = secret ? createHash10("sha256").update(secret).digest() : Buffer.alloc(0);
|
|
22694
|
+
const expected = match?.trigger.type === "webhook" ? Buffer.from(match.trigger.secretHash, "hex") : Buffer.alloc(32);
|
|
22695
|
+
if (!match || presented.length !== expected.length || !timingSafeEqual6(presented, expected)) {
|
|
22696
|
+
reply.code(401).send({ error: "unauthorized" });
|
|
22697
|
+
return;
|
|
22698
|
+
}
|
|
22699
|
+
automationWebhookRequests.set(request, match);
|
|
22700
|
+
authenticatedPrincipals.set(request, hostPrincipal());
|
|
22701
|
+
return;
|
|
22702
|
+
}
|
|
22037
22703
|
if (request.method === "POST" && path === "/pairing/claim" && !hasEncodedSep(request.url)) {
|
|
22038
22704
|
const originAllowed2 = isOriginAllowed(request.headers.origin, request.headers.host, {
|
|
22039
22705
|
publicUrl: config.publicUrl,
|
|
@@ -22424,7 +23090,7 @@ function createServer(config, deps = {}) {
|
|
|
22424
23090
|
return;
|
|
22425
23091
|
}
|
|
22426
23092
|
const principal = authenticatedPrincipals.get(request) ?? hostPrincipal();
|
|
22427
|
-
const holderId = `ws:${
|
|
23093
|
+
const holderId = `ws:${randomUUID8()}`;
|
|
22428
23094
|
let leaseId;
|
|
22429
23095
|
let lastLeaseRevision = 0;
|
|
22430
23096
|
const sendLeaseState = (reason, revision) => {
|
|
@@ -22719,7 +23385,7 @@ function createServer(config, deps = {}) {
|
|
|
22719
23385
|
reply.code(400).send({ code: "INVALID_PROVIDER", error: "Invalid provider" });
|
|
22720
23386
|
return;
|
|
22721
23387
|
}
|
|
22722
|
-
const id = requestedSessionId ??
|
|
23388
|
+
const id = requestedSessionId ?? randomUUID8();
|
|
22723
23389
|
const existingMeta = requestedSessionId ? terminalManager.get(id) : void 0;
|
|
22724
23390
|
if (!terminalAvailable) {
|
|
22725
23391
|
reply.code(400).send({
|
|
@@ -23386,7 +24052,7 @@ function createServer(config, deps = {}) {
|
|
|
23386
24052
|
const peerIdempotencyKey = (request, peerId) => {
|
|
23387
24053
|
const actor = actorForRequest(request);
|
|
23388
24054
|
const provided = request.headers["idempotency-key"];
|
|
23389
|
-
const key = typeof provided === "string" ? provided :
|
|
24055
|
+
const key = typeof provided === "string" ? provided : randomUUID8();
|
|
23390
24056
|
return `peer-${createHash10("sha256").update(`${peerId}\0${actor.actorType}\0${actor.actorId}\0${key}`).digest("base64url")}`;
|
|
23391
24057
|
};
|
|
23392
24058
|
const workspaceAllowedByPeer = (peer, workspaceId) => typeof workspaceId === "string" && (peer.allowedWorkspaceIds === null || peer.allowedWorkspaceIds.includes(workspaceId));
|
|
@@ -25477,7 +26143,7 @@ data: ${JSON.stringify(data)}
|
|
|
25477
26143
|
});
|
|
25478
26144
|
return;
|
|
25479
26145
|
}
|
|
25480
|
-
holderId = `api-once:${
|
|
26146
|
+
holderId = `api-once:${randomUUID8()}`;
|
|
25481
26147
|
const acquired = inputLeases.acquire(request.params.id, holderId, principal);
|
|
25482
26148
|
if (acquired.status === "denied") {
|
|
25483
26149
|
reply.code(409).send({ code: "INPUT_LEASE_REQUIRED", error: "terminal input is already controlled" });
|
|
@@ -25631,7 +26297,7 @@ data: ${JSON.stringify(data)}
|
|
|
25631
26297
|
return;
|
|
25632
26298
|
}
|
|
25633
26299
|
const isImage = body.kind === "image" ? true : body.kind === "file" ? false : described.isImage;
|
|
25634
|
-
const id =
|
|
26300
|
+
const id = randomUUID8();
|
|
25635
26301
|
const now = Date.now();
|
|
25636
26302
|
const media = attachmentMedia(described.name);
|
|
25637
26303
|
const stored2 = {
|
|
@@ -26085,9 +26751,78 @@ data: ${JSON.stringify(data)}
|
|
|
26085
26751
|
const owner = currentNodeOwner();
|
|
26086
26752
|
return automation && automation.owner.type === owner.type && automation.owner.id === owner.id ? automation : void 0;
|
|
26087
26753
|
};
|
|
26754
|
+
const projectAutomationDefinition = (automation) => ({
|
|
26755
|
+
...automation,
|
|
26756
|
+
triggers: automation.triggers.map((trigger) => {
|
|
26757
|
+
if (trigger.type !== "webhook") return trigger;
|
|
26758
|
+
const { secretHash, ...publicTrigger } = trigger;
|
|
26759
|
+
void secretHash;
|
|
26760
|
+
return publicTrigger;
|
|
26761
|
+
})
|
|
26762
|
+
});
|
|
26763
|
+
const newTriggerId = () => `rct_${randomBytes17(12).toString("base64url")}`;
|
|
26764
|
+
const newWebhookHookId = () => `rcwh_${randomBytes17(24).toString("base64url")}`;
|
|
26765
|
+
const newWebhookSecret = () => `rcws_${randomBytes17(32).toString("base64url")}`;
|
|
26766
|
+
const prepareAutomationTriggers = (value, current) => {
|
|
26767
|
+
if (!Array.isArray(value) || value.length > 16) throw new Error("invalid automation triggers");
|
|
26768
|
+
const existing = new Map(current?.triggers.map((trigger) => [trigger.id, trigger]) ?? []);
|
|
26769
|
+
const ids = /* @__PURE__ */ new Set();
|
|
26770
|
+
const webhookSecrets = [];
|
|
26771
|
+
const triggers = value.map((candidate) => {
|
|
26772
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
26773
|
+
throw new Error("invalid automation trigger");
|
|
26774
|
+
}
|
|
26775
|
+
const raw = candidate;
|
|
26776
|
+
const requestedId = typeof raw.id === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(raw.id) ? raw.id : void 0;
|
|
26777
|
+
const id = requestedId ?? newTriggerId();
|
|
26778
|
+
if (ids.has(id)) throw new Error("duplicate automation trigger id");
|
|
26779
|
+
ids.add(id);
|
|
26780
|
+
if (typeof raw.enabled !== "boolean") throw new Error("invalid automation trigger state");
|
|
26781
|
+
if (raw.type === "schedule") {
|
|
26782
|
+
if (typeof raw.cron !== "string" || typeof raw.timeZone !== "string" || raw.timeZone.length > 80 || raw.missedRunPolicy !== "skip" || Object.keys(raw).some(
|
|
26783
|
+
(key) => !["id", "type", "enabled", "cron", "timeZone", "missedRunPolicy"].includes(key)
|
|
26784
|
+
)) {
|
|
26785
|
+
throw new Error("invalid schedule trigger");
|
|
26786
|
+
}
|
|
26787
|
+
new Intl.DateTimeFormat("en-US", { timeZone: raw.timeZone }).format(0);
|
|
26788
|
+
return {
|
|
26789
|
+
id,
|
|
26790
|
+
type: "schedule",
|
|
26791
|
+
enabled: raw.enabled,
|
|
26792
|
+
cron: validateCronExpression(raw.cron),
|
|
26793
|
+
timeZone: raw.timeZone,
|
|
26794
|
+
missedRunPolicy: "skip"
|
|
26795
|
+
};
|
|
26796
|
+
}
|
|
26797
|
+
if (raw.type === "webhook") {
|
|
26798
|
+
if (Object.keys(raw).some((key) => !["id", "type", "enabled", "hookId"].includes(key))) {
|
|
26799
|
+
throw new Error("invalid webhook trigger");
|
|
26800
|
+
}
|
|
26801
|
+
const previous = existing.get(id);
|
|
26802
|
+
if (previous?.type === "webhook") {
|
|
26803
|
+
if (raw.hookId !== void 0 && raw.hookId !== previous.hookId) {
|
|
26804
|
+
throw new Error("webhook identity cannot be replaced");
|
|
26805
|
+
}
|
|
26806
|
+
return { ...previous, enabled: raw.enabled };
|
|
26807
|
+
}
|
|
26808
|
+
const hookId = newWebhookHookId();
|
|
26809
|
+
const secret = newWebhookSecret();
|
|
26810
|
+
webhookSecrets.push({ triggerId: id, hookId, secret, path: `/api/v2/automation-hooks/${hookId}` });
|
|
26811
|
+
return {
|
|
26812
|
+
id,
|
|
26813
|
+
type: "webhook",
|
|
26814
|
+
enabled: raw.enabled,
|
|
26815
|
+
hookId,
|
|
26816
|
+
secretHash: createHash10("sha256").update(secret).digest("hex")
|
|
26817
|
+
};
|
|
26818
|
+
}
|
|
26819
|
+
throw new Error("invalid automation trigger");
|
|
26820
|
+
});
|
|
26821
|
+
return { triggers, webhookSecrets };
|
|
26822
|
+
};
|
|
26088
26823
|
const automationInvocationIdentity = (request, automationId) => {
|
|
26089
26824
|
const idempotency2 = mutationContexts.get(request)?.idempotency;
|
|
26090
|
-
if (!idempotency2) return { invocationId:
|
|
26825
|
+
if (!idempotency2) return { invocationId: randomUUID8(), sessionId: randomUUID8() };
|
|
26091
26826
|
const actor = actorForRequest(request);
|
|
26092
26827
|
const digest3 = createHash10("sha256").update("roamcode-automation-invocation-v1\0").update(JSON.stringify([actor.actorType, actor.actorId, idempotency2.key, idempotency2.fingerprint, automationId])).digest("hex");
|
|
26093
26828
|
const uuidHex = digest3.slice(0, 32).split("");
|
|
@@ -26356,10 +27091,13 @@ data: ${JSON.stringify(data)}
|
|
|
26356
27091
|
"cwd",
|
|
26357
27092
|
"instruction",
|
|
26358
27093
|
"runtimeOptions",
|
|
26359
|
-
"trigger"
|
|
27094
|
+
"trigger",
|
|
27095
|
+
"triggers"
|
|
26360
27096
|
]);
|
|
26361
27097
|
const automationUpdateKeys = /* @__PURE__ */ new Set([...automationCreateKeys, "expectedRevision"]);
|
|
26362
|
-
app.get("/api/v2/automations", async () => ({
|
|
27098
|
+
app.get("/api/v2/automations", async () => ({
|
|
27099
|
+
automations: sessionAutomationStore.list(currentNodeOwner()).map(projectAutomationDefinition)
|
|
27100
|
+
}));
|
|
26363
27101
|
app.post("/api/v2/automations", { bodyLimit: 128 * 1024 }, async (request, reply) => {
|
|
26364
27102
|
if (!validObjectBody(request.body)) return sendInvalidAutomation(reply);
|
|
26365
27103
|
if ("owner" in request.body || "provider" in request.body) {
|
|
@@ -26379,15 +27117,17 @@ data: ${JSON.stringify(data)}
|
|
|
26379
27117
|
);
|
|
26380
27118
|
if (!target) return;
|
|
26381
27119
|
try {
|
|
27120
|
+
const prepared = prepareAutomationTriggers(request.body.triggers ?? [], void 0);
|
|
26382
27121
|
const automation = sessionAutomationStore.create({
|
|
26383
27122
|
owner: currentNodeOwner(),
|
|
26384
27123
|
name: request.body.name,
|
|
26385
27124
|
...request.body.enabled === void 0 ? {} : { enabled: request.body.enabled },
|
|
26386
27125
|
...target,
|
|
26387
27126
|
instruction: request.body.instruction,
|
|
26388
|
-
...request.body.trigger === void 0 ? { trigger: { type: "manual" } } : { trigger: request.body.trigger }
|
|
27127
|
+
...request.body.trigger === void 0 ? { trigger: { type: "manual" } } : { trigger: request.body.trigger },
|
|
27128
|
+
triggers: prepared.triggers
|
|
26389
27129
|
});
|
|
26390
|
-
reply.code(201).send({ automation });
|
|
27130
|
+
reply.code(201).send({ automation: projectAutomationDefinition(automation), webhookSecrets: prepared.webhookSecrets });
|
|
26391
27131
|
} catch {
|
|
26392
27132
|
sendInvalidAutomation(reply);
|
|
26393
27133
|
}
|
|
@@ -26398,7 +27138,7 @@ data: ${JSON.stringify(data)}
|
|
|
26398
27138
|
reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
|
|
26399
27139
|
return;
|
|
26400
27140
|
}
|
|
26401
|
-
return { automation };
|
|
27141
|
+
return { automation: projectAutomationDefinition(automation) };
|
|
26402
27142
|
});
|
|
26403
27143
|
app.patch(
|
|
26404
27144
|
"/api/v2/automations/:automationId",
|
|
@@ -26436,18 +27176,20 @@ data: ${JSON.stringify(data)}
|
|
|
26436
27176
|
...target
|
|
26437
27177
|
};
|
|
26438
27178
|
try {
|
|
27179
|
+
const prepared = prepareAutomationTriggers(request.body.triggers ?? current.triggers, current);
|
|
27180
|
+
input.triggers = prepared.triggers;
|
|
26439
27181
|
const automation = sessionAutomationStore.update(current.id, input, request.body.expectedRevision);
|
|
26440
27182
|
if (!automation) {
|
|
26441
27183
|
reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
|
|
26442
27184
|
return;
|
|
26443
27185
|
}
|
|
26444
|
-
return { automation };
|
|
27186
|
+
return { automation: projectAutomationDefinition(automation), webhookSecrets: prepared.webhookSecrets };
|
|
26445
27187
|
} catch (error) {
|
|
26446
27188
|
if (error instanceof SessionAutomationRevisionConflictError) {
|
|
26447
27189
|
reply.code(409).send({
|
|
26448
27190
|
code: "SESSION_AUTOMATION_REVISION_CONFLICT",
|
|
26449
27191
|
error: "automation state changed",
|
|
26450
|
-
current: error.current
|
|
27192
|
+
current: projectAutomationDefinition(error.current)
|
|
26451
27193
|
});
|
|
26452
27194
|
return;
|
|
26453
27195
|
}
|
|
@@ -26462,6 +27204,70 @@ data: ${JSON.stringify(data)}
|
|
|
26462
27204
|
}
|
|
26463
27205
|
reply.code(204).send();
|
|
26464
27206
|
});
|
|
27207
|
+
app.get(
|
|
27208
|
+
"/api/v2/automations/:automationId/activity",
|
|
27209
|
+
async (request, reply) => {
|
|
27210
|
+
const automation = ownedAutomationIncludingRemoved(request.params.automationId);
|
|
27211
|
+
if (!automation) {
|
|
27212
|
+
reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
|
|
27213
|
+
return;
|
|
27214
|
+
}
|
|
27215
|
+
const limit = request.query.limit === void 0 ? 25 : Number(request.query.limit);
|
|
27216
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
27217
|
+
reply.code(400).send({ code: "INVALID_AUTOMATION_ACTIVITY_LIMIT", error: "limit must be between 1 and 100" });
|
|
27218
|
+
return;
|
|
27219
|
+
}
|
|
27220
|
+
return { activities: sessionAutomationStore.listActivities(automation.id, limit) };
|
|
27221
|
+
}
|
|
27222
|
+
);
|
|
27223
|
+
app.post(
|
|
27224
|
+
"/api/v2/automations/:automationId/triggers/:triggerId/secret",
|
|
27225
|
+
{ bodyLimit: 1024 },
|
|
27226
|
+
async (request, reply) => {
|
|
27227
|
+
const current = ownedAutomation(request.params.automationId);
|
|
27228
|
+
if (!current) {
|
|
27229
|
+
reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
|
|
27230
|
+
return;
|
|
27231
|
+
}
|
|
27232
|
+
if (!validObjectBody(request.body) || !hasOnlyKeys(request.body, /* @__PURE__ */ new Set(["expectedRevision"])) || !Number.isSafeInteger(request.body.expectedRevision) || request.body.expectedRevision !== current.revision) {
|
|
27233
|
+
return sendInvalidAutomation(reply, "a current expectedRevision is required");
|
|
27234
|
+
}
|
|
27235
|
+
const selected = current.triggers.find(
|
|
27236
|
+
(trigger) => trigger.id === request.params.triggerId && trigger.type === "webhook"
|
|
27237
|
+
);
|
|
27238
|
+
if (!selected || selected.type !== "webhook") {
|
|
27239
|
+
reply.code(404).send({ code: "AUTOMATION_TRIGGER_NOT_FOUND", error: "webhook trigger not found" });
|
|
27240
|
+
return;
|
|
27241
|
+
}
|
|
27242
|
+
const secret = newWebhookSecret();
|
|
27243
|
+
const triggers = current.triggers.map(
|
|
27244
|
+
(trigger) => trigger.id === selected.id ? { ...selected, secretHash: createHash10("sha256").update(secret).digest("hex") } : trigger
|
|
27245
|
+
);
|
|
27246
|
+
try {
|
|
27247
|
+
const automation = sessionAutomationStore.update(current.id, { triggers }, current.revision);
|
|
27248
|
+
if (!automation) throw new Error("automation disappeared");
|
|
27249
|
+
return {
|
|
27250
|
+
automation: projectAutomationDefinition(automation),
|
|
27251
|
+
webhookSecret: {
|
|
27252
|
+
triggerId: selected.id,
|
|
27253
|
+
hookId: selected.hookId,
|
|
27254
|
+
secret,
|
|
27255
|
+
path: `/api/v2/automation-hooks/${selected.hookId}`
|
|
27256
|
+
}
|
|
27257
|
+
};
|
|
27258
|
+
} catch (error) {
|
|
27259
|
+
if (error instanceof SessionAutomationRevisionConflictError) {
|
|
27260
|
+
reply.code(409).send({
|
|
27261
|
+
code: "SESSION_AUTOMATION_REVISION_CONFLICT",
|
|
27262
|
+
error: "automation state changed",
|
|
27263
|
+
current: projectAutomationDefinition(error.current)
|
|
27264
|
+
});
|
|
27265
|
+
return;
|
|
27266
|
+
}
|
|
27267
|
+
sendInvalidAutomation(reply);
|
|
27268
|
+
}
|
|
27269
|
+
}
|
|
27270
|
+
);
|
|
26465
27271
|
app.get(
|
|
26466
27272
|
"/api/v2/automations/:automationId/runs",
|
|
26467
27273
|
async (request, reply) => {
|
|
@@ -26690,6 +27496,46 @@ data: ${JSON.stringify(data)}
|
|
|
26690
27496
|
);
|
|
26691
27497
|
}
|
|
26692
27498
|
);
|
|
27499
|
+
const parsedAutomationConcurrency = Number.parseInt(process.env.ROAMCODE_AUTOMATION_CONCURRENCY ?? "2", 10);
|
|
27500
|
+
const automationTriggerEngine = createAutomationTriggerEngine({
|
|
27501
|
+
store: sessionAutomationStore,
|
|
27502
|
+
concurrency: Number.isSafeInteger(parsedAutomationConcurrency) && parsedAutomationConcurrency > 0 ? parsedAutomationConcurrency : 2,
|
|
27503
|
+
execute: async (activity) => {
|
|
27504
|
+
const response2 = await app.inject({
|
|
27505
|
+
method: "POST",
|
|
27506
|
+
url: `/api/v2/automations/${encodeURIComponent(activity.automationId)}/runs`,
|
|
27507
|
+
headers: {
|
|
27508
|
+
"idempotency-key": activity.id,
|
|
27509
|
+
...config.accessToken ? { authorization: `Bearer ${config.accessToken}` } : {}
|
|
27510
|
+
}
|
|
27511
|
+
});
|
|
27512
|
+
const body = (() => {
|
|
27513
|
+
try {
|
|
27514
|
+
return response2.json();
|
|
27515
|
+
} catch {
|
|
27516
|
+
return {};
|
|
27517
|
+
}
|
|
27518
|
+
})();
|
|
27519
|
+
if (response2.statusCode !== 201 || typeof body.run?.id !== "string") {
|
|
27520
|
+
throw new Error("automation trigger execution failed");
|
|
27521
|
+
}
|
|
27522
|
+
return { runId: body.run.id };
|
|
27523
|
+
}
|
|
27524
|
+
});
|
|
27525
|
+
app.post(
|
|
27526
|
+
"/api/v2/automation-hooks/:hookId",
|
|
27527
|
+
{ bodyLimit: 64 * 1024 },
|
|
27528
|
+
async (request, reply) => {
|
|
27529
|
+
const authorized = automationWebhookRequests.get(request);
|
|
27530
|
+
if (!authorized || authorized.trigger.type !== "webhook" || authorized.trigger.hookId !== request.params.hookId) {
|
|
27531
|
+
reply.code(401).send({ error: "unauthorized" });
|
|
27532
|
+
return;
|
|
27533
|
+
}
|
|
27534
|
+
automationTriggerEngine.enqueueWebhook(authorized.automation, authorized.trigger);
|
|
27535
|
+
reply.code(202).send({ accepted: true });
|
|
27536
|
+
}
|
|
27537
|
+
);
|
|
27538
|
+
app.addHook("onReady", async () => automationTriggerEngine.start());
|
|
26693
27539
|
app.get("/providers", async () => {
|
|
26694
27540
|
return { providers: await readProviderAvailability() };
|
|
26695
27541
|
});
|
|
@@ -27048,7 +27894,7 @@ data: ${JSON.stringify(data)}
|
|
|
27048
27894
|
reply.code(400).send({ error: "no file field in the upload" });
|
|
27049
27895
|
return;
|
|
27050
27896
|
}
|
|
27051
|
-
const id =
|
|
27897
|
+
const id = randomUUID8();
|
|
27052
27898
|
try {
|
|
27053
27899
|
const dir = await fsService.ensureDirWithinRoot(
|
|
27054
27900
|
`${terminalSharedDir({ dataDir, fsRoot: config.fsRoot, sessionId })}/${id}`
|
|
@@ -27100,7 +27946,7 @@ data: ${JSON.stringify(data)}
|
|
|
27100
27946
|
reply.code(400).send({ error: "an edited image is required" });
|
|
27101
27947
|
return;
|
|
27102
27948
|
}
|
|
27103
|
-
const id =
|
|
27949
|
+
const id = randomUUID8();
|
|
27104
27950
|
try {
|
|
27105
27951
|
const dir = await fsService.ensureDirWithinRoot(
|
|
27106
27952
|
`${terminalSharedDir({ dataDir, fsRoot: config.fsRoot, sessionId: source.sessionId })}/${id}`
|
|
@@ -27226,6 +28072,7 @@ data: ${JSON.stringify(data)}
|
|
|
27226
28072
|
);
|
|
27227
28073
|
if (deps.webDir) registerStatic(app, { webDir: deps.webDir });
|
|
27228
28074
|
app.addHook("onClose", async () => {
|
|
28075
|
+
automationTriggerEngine.stop();
|
|
27229
28076
|
clearInterval(sharedSweepTimer);
|
|
27230
28077
|
inputLeases.close();
|
|
27231
28078
|
unsubscribeAutomations();
|
|
@@ -27321,6 +28168,25 @@ data: ${JSON.stringify(data)}
|
|
|
27321
28168
|
authGate,
|
|
27322
28169
|
terminalManager,
|
|
27323
28170
|
terminalAvailable,
|
|
28171
|
+
automationWebhookRegistrations: () => sessionAutomationStore.list().flatMap(
|
|
28172
|
+
(automation) => automation.triggers.filter((trigger) => trigger.type === "webhook").map((trigger) => ({
|
|
28173
|
+
hookId: trigger.hookId,
|
|
28174
|
+
automationId: automation.id,
|
|
28175
|
+
triggerId: trigger.id,
|
|
28176
|
+
secretHash: trigger.secretHash,
|
|
28177
|
+
enabled: automation.enabled && trigger.enabled
|
|
28178
|
+
}))
|
|
28179
|
+
),
|
|
28180
|
+
acceptCloudAutomationInvocation: async (invocation) => {
|
|
28181
|
+
const automation = sessionAutomationStore.get(invocation.automationId);
|
|
28182
|
+
const trigger = automation?.triggers.find(
|
|
28183
|
+
(candidate) => candidate.type === "webhook" && candidate.id === invocation.triggerId && candidate.hookId === invocation.hookId
|
|
28184
|
+
);
|
|
28185
|
+
if (!automation?.enabled || !trigger?.enabled || trigger.type !== "webhook") {
|
|
28186
|
+
throw new Error("managed automation webhook is no longer active");
|
|
28187
|
+
}
|
|
28188
|
+
automationTriggerEngine.enqueueWebhook(automation, trigger, invocation.id);
|
|
28189
|
+
},
|
|
27324
28190
|
inputLeases,
|
|
27325
28191
|
teamStore,
|
|
27326
28192
|
policyStore,
|
|
@@ -27345,7 +28211,7 @@ function fileEntityTag(size, mtimeMs) {
|
|
|
27345
28211
|
import { pathToFileURL as pathToFileURL2, fileURLToPath as fileURLToPath4 } from "url";
|
|
27346
28212
|
import { join as join20 } from "path";
|
|
27347
28213
|
import { existsSync as existsSync7 } from "fs";
|
|
27348
|
-
import { randomUUID as
|
|
28214
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
27349
28215
|
|
|
27350
28216
|
// src/push-store.ts
|
|
27351
28217
|
import { createRequire as createRequire15 } from "module";
|
|
@@ -27667,7 +28533,7 @@ function createUsageService(opts) {
|
|
|
27667
28533
|
|
|
27668
28534
|
// src/claude-auth-service.ts
|
|
27669
28535
|
import { spawn as nodeSpawn2 } from "child_process";
|
|
27670
|
-
import { randomUUID as
|
|
28536
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
27671
28537
|
function parseAuthStatus(stdout) {
|
|
27672
28538
|
try {
|
|
27673
28539
|
const o = JSON.parse(stdout);
|
|
@@ -27744,7 +28610,7 @@ var ClaudeAuthService = class {
|
|
|
27744
28610
|
reject(err instanceof Error ? err : new Error("failed to spawn claude"));
|
|
27745
28611
|
return;
|
|
27746
28612
|
}
|
|
27747
|
-
const id =
|
|
28613
|
+
const id = randomUUID9();
|
|
27748
28614
|
let settled = false;
|
|
27749
28615
|
let buf = "";
|
|
27750
28616
|
const onData = (d) => {
|
|
@@ -29106,7 +29972,7 @@ var CodexMetadataService = class {
|
|
|
29106
29972
|
|
|
29107
29973
|
// src/providers/claude-metadata-service.ts
|
|
29108
29974
|
import { execFile as nodeExecFile2, spawn as nodeSpawn3 } from "child_process";
|
|
29109
|
-
import { randomUUID as
|
|
29975
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
29110
29976
|
import { StringDecoder } from "string_decoder";
|
|
29111
29977
|
var SAFE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._:/\u005b\u005d-]*$/;
|
|
29112
29978
|
var MAX_MODELS = 64;
|
|
@@ -29314,7 +30180,7 @@ function createClaudeMetadataRunner(options) {
|
|
|
29314
30180
|
reject(metadataUnavailable2());
|
|
29315
30181
|
return;
|
|
29316
30182
|
}
|
|
29317
|
-
const requestId = `roamcode-models-${
|
|
30183
|
+
const requestId = `roamcode-models-${randomUUID10()}`;
|
|
29318
30184
|
const decoder = new StringDecoder("utf8");
|
|
29319
30185
|
let stdoutBuffer = "";
|
|
29320
30186
|
let outputBytes = 0;
|
|
@@ -29566,7 +30432,7 @@ var CodexLatestService = class {
|
|
|
29566
30432
|
import { execFile } from "child_process";
|
|
29567
30433
|
import { constants as constants10 } from "fs";
|
|
29568
30434
|
import { access as access2, chmod, copyFile, link, realpath as realpath7, rename as rename3, rm as rm2, rmdir as rmdir2, stat as stat7 } from "fs/promises";
|
|
29569
|
-
import { randomUUID as
|
|
30435
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
29570
30436
|
import { basename as basename5, delimiter, dirname as dirname12, isAbsolute as isAbsolute9, join as join19, sep as sep5 } from "path";
|
|
29571
30437
|
var CODEX_EXECUTABLE_PROBE_TIMEOUT_MS = 5e3;
|
|
29572
30438
|
var OPENAI_CODE_SIGNING_TEAM_ID = "2DC432GLL2";
|
|
@@ -29676,7 +30542,7 @@ async function removeLegacyManagedCopy(dataDir) {
|
|
|
29676
30542
|
}
|
|
29677
30543
|
async function repairHomebrewExecutable(source, sourceStat, env, deps) {
|
|
29678
30544
|
if (!await deps.verifyOfficialSignature(source)) return false;
|
|
29679
|
-
const nonce =
|
|
30545
|
+
const nonce = randomUUID11();
|
|
29680
30546
|
const directory = dirname12(source);
|
|
29681
30547
|
const name = basename5(source);
|
|
29682
30548
|
const temporary = join19(directory, `.${name}.roamcode-repair-${nonce}`);
|
|
@@ -29906,13 +30772,14 @@ function cloudHostCapabilities(input) {
|
|
|
29906
30772
|
return [
|
|
29907
30773
|
`authorization.v${input.authorizationVersion}`,
|
|
29908
30774
|
...input.terminalAvailable ? ["terminal.v1"] : [],
|
|
30775
|
+
...input.terminalAvailable ? ["automation-schedule.v1", "automation-webhook.v1"] : [],
|
|
29909
30776
|
...input.relayEnabled ? ["relay.v1"] : [],
|
|
29910
30777
|
...input.managedDeviceEnrollmentEnabled && input.relayEnabled && input.terminalAvailable ? ["managed-device-enrollment.v1"] : []
|
|
29911
30778
|
];
|
|
29912
30779
|
}
|
|
29913
30780
|
async function startServer(env = process.env, options = {}) {
|
|
29914
30781
|
const config = loadServerConfig(env);
|
|
29915
|
-
const healthInstanceId =
|
|
30782
|
+
const healthInstanceId = randomUUID12();
|
|
29916
30783
|
ensureDataDir(config.dataDir);
|
|
29917
30784
|
const loopback = isLoopbackAddress(config.bindAddress);
|
|
29918
30785
|
let token;
|
|
@@ -30185,6 +31052,7 @@ async function startServer(env = process.env, options = {}) {
|
|
|
30185
31052
|
cloudStatus: () => cloudHostRuntime?.status() ?? {
|
|
30186
31053
|
running: false,
|
|
30187
31054
|
heartbeatFailures: 0,
|
|
31055
|
+
automationFailures: 0,
|
|
30188
31056
|
authorizationFailures: 0,
|
|
30189
31057
|
authorization: cloudAuthorizationStore.getState()
|
|
30190
31058
|
}
|
|
@@ -30217,6 +31085,8 @@ async function startServer(env = process.env, options = {}) {
|
|
|
30217
31085
|
relayEnabled: relayConfig !== void 0,
|
|
30218
31086
|
managedDeviceEnrollmentEnabled: cloudDeviceEnrollmentConfirmer !== void 0 && relayProvisioner !== void 0
|
|
30219
31087
|
}),
|
|
31088
|
+
automationWebhooks: () => result.automationWebhookRegistrations(),
|
|
31089
|
+
onAutomationInvocation: (invocation) => result.acceptCloudAutomationInvocation(invocation),
|
|
30220
31090
|
...relayConfig ? {
|
|
30221
31091
|
relayHostIdentity: {
|
|
30222
31092
|
publicKey: relayConfig.hostIdentity.publicKey,
|
|
@@ -30372,6 +31242,7 @@ export {
|
|
|
30372
31242
|
CLOUD_DEVICE_ENROLLMENT_CONFIRM_PATH,
|
|
30373
31243
|
CLOUD_DEVICE_ENROLLMENT_HOST_ROUTE,
|
|
30374
31244
|
CLOUD_HOST_AUTHORIZATION_SNAPSHOT_PATH,
|
|
31245
|
+
CLOUD_HOST_AUTOMATION_SYNC_PATH,
|
|
30375
31246
|
CLOUD_HOST_CONFIG_FILE,
|
|
30376
31247
|
CLOUD_HOST_HEARTBEAT_PATH,
|
|
30377
31248
|
CLOUD_HOST_MAX_SIGNED_RESPONSE_BYTES,
|