@roamcode.ai/server 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/start.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import { pathToFileURL, fileURLToPath as fileURLToPath3 } from "url";
|
|
5
5
|
import { join as join19 } from "path";
|
|
6
6
|
import { existsSync as existsSync7 } from "fs";
|
|
7
|
-
import { randomUUID as
|
|
7
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
8
8
|
|
|
9
9
|
// src/transport.ts
|
|
10
|
-
import { createHash as createHash7, randomBytes as randomBytes11, randomUUID as
|
|
10
|
+
import { createHash as createHash7, randomBytes as randomBytes11, randomUUID as randomUUID8, timingSafeEqual as timingSafeEqual4 } from "crypto";
|
|
11
11
|
import { basename as pathBasename, join as join11, resolve as resolvePath } from "path";
|
|
12
12
|
import { createReadStream } from "fs";
|
|
13
13
|
import Fastify from "fastify";
|
|
@@ -4493,6 +4493,48 @@ function normalizeTrigger(value) {
|
|
|
4493
4493
|
}
|
|
4494
4494
|
return { type: "manual" };
|
|
4495
4495
|
}
|
|
4496
|
+
function validTimeZone(value) {
|
|
4497
|
+
try {
|
|
4498
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(0);
|
|
4499
|
+
return true;
|
|
4500
|
+
} catch {
|
|
4501
|
+
return false;
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
function normalizeConfiguredTriggers(value) {
|
|
4505
|
+
if (!Array.isArray(value) || value.length > 16) throw new Error("invalid automation triggers");
|
|
4506
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4507
|
+
return value.map((candidate) => {
|
|
4508
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
4509
|
+
throw new Error("invalid automation trigger");
|
|
4510
|
+
}
|
|
4511
|
+
const raw = candidate;
|
|
4512
|
+
const id = normalizeId(raw.id, "automation trigger id");
|
|
4513
|
+
if (ids.has(id)) throw new Error("duplicate automation trigger id");
|
|
4514
|
+
ids.add(id);
|
|
4515
|
+
if (typeof raw.enabled !== "boolean") throw new Error("invalid automation trigger state");
|
|
4516
|
+
if (raw.type === "schedule") {
|
|
4517
|
+
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))) {
|
|
4518
|
+
throw new Error("invalid schedule trigger");
|
|
4519
|
+
}
|
|
4520
|
+
return {
|
|
4521
|
+
id,
|
|
4522
|
+
type: "schedule",
|
|
4523
|
+
enabled: raw.enabled,
|
|
4524
|
+
cron: raw.cron.trim().replace(/\s+/g, " "),
|
|
4525
|
+
timeZone: raw.timeZone,
|
|
4526
|
+
missedRunPolicy: "skip"
|
|
4527
|
+
};
|
|
4528
|
+
}
|
|
4529
|
+
if (raw.type === "webhook") {
|
|
4530
|
+
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))) {
|
|
4531
|
+
throw new Error("invalid webhook trigger");
|
|
4532
|
+
}
|
|
4533
|
+
return { id, type: "webhook", enabled: raw.enabled, hookId: raw.hookId, secretHash: raw.secretHash };
|
|
4534
|
+
}
|
|
4535
|
+
throw new Error("invalid automation trigger");
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4496
4538
|
function normalizeRuntimeOptions(value) {
|
|
4497
4539
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid runtime options");
|
|
4498
4540
|
let encoded;
|
|
@@ -4519,7 +4561,8 @@ function normalizeCreate(input) {
|
|
|
4519
4561
|
cwd: normalizeCwd2(input.cwd),
|
|
4520
4562
|
instruction: normalizeInstruction(input.instruction),
|
|
4521
4563
|
runtimeOptions: normalizeRuntimeOptions(input.runtimeOptions ?? {}),
|
|
4522
|
-
trigger: normalizeTrigger(input.trigger ?? { type: "manual" })
|
|
4564
|
+
trigger: normalizeTrigger(input.trigger ?? { type: "manual" }),
|
|
4565
|
+
triggers: normalizeConfiguredTriggers(input.triggers ?? [])
|
|
4523
4566
|
};
|
|
4524
4567
|
}
|
|
4525
4568
|
function applyUpdate(current, input, now) {
|
|
@@ -4533,7 +4576,8 @@ function applyUpdate(current, input, now) {
|
|
|
4533
4576
|
cwd: input.cwd ?? current.cwd,
|
|
4534
4577
|
instruction: input.instruction ?? current.instruction,
|
|
4535
4578
|
runtimeOptions: input.runtimeOptions ?? current.runtimeOptions,
|
|
4536
|
-
trigger: input.trigger ?? current.trigger
|
|
4579
|
+
trigger: input.trigger ?? current.trigger,
|
|
4580
|
+
triggers: input.triggers ?? current.triggers
|
|
4537
4581
|
};
|
|
4538
4582
|
if (Object.values(input).some((value) => value === void 0) || typeof candidate.enabled !== "boolean") {
|
|
4539
4583
|
throw new Error("invalid automation update");
|
|
@@ -4555,9 +4599,12 @@ function createMemoryStore3(opts) {
|
|
|
4555
4599
|
const removedDefinitions = /* @__PURE__ */ new Set();
|
|
4556
4600
|
const runs = /* @__PURE__ */ new Map();
|
|
4557
4601
|
const runInputSnapshots = /* @__PURE__ */ new Map();
|
|
4602
|
+
const activities = /* @__PURE__ */ new Map();
|
|
4603
|
+
const triggerCursors = /* @__PURE__ */ new Map();
|
|
4558
4604
|
const nodeOwners = /* @__PURE__ */ new Map();
|
|
4559
4605
|
const generateAutomationId = opts.generateAutomationId ?? (() => randomId3("rca2"));
|
|
4560
4606
|
const generateRunId = opts.generateRunId ?? (() => randomId3("rcar"));
|
|
4607
|
+
const generateActivityId = opts.generateActivityId ?? (() => randomId3("rcae"));
|
|
4561
4608
|
return {
|
|
4562
4609
|
mode: "memory-fallback",
|
|
4563
4610
|
getNodeOwner(nodeId) {
|
|
@@ -4643,8 +4690,8 @@ function createMemoryStore3(opts) {
|
|
|
4643
4690
|
const snapshot = runInputSnapshots.get(id);
|
|
4644
4691
|
return snapshot ? clone2(snapshot) : void 0;
|
|
4645
4692
|
},
|
|
4646
|
-
getRunByInvocationId(
|
|
4647
|
-
const run = [...runs.values()].find((candidate) => candidate.invocationId ===
|
|
4693
|
+
getRunByInvocationId(invocationId2) {
|
|
4694
|
+
const run = [...runs.values()].find((candidate) => candidate.invocationId === invocationId2);
|
|
4648
4695
|
return run ? clone2(run) : void 0;
|
|
4649
4696
|
},
|
|
4650
4697
|
getRunBySessionId(sessionId) {
|
|
@@ -4742,11 +4789,43 @@ function createMemoryStore3(opts) {
|
|
|
4742
4789
|
const bounded = Math.max(1, Math.min(1e3, Math.trunc(limit)));
|
|
4743
4790
|
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(clone2);
|
|
4744
4791
|
},
|
|
4792
|
+
getTriggerCursor(triggerId) {
|
|
4793
|
+
return triggerCursors.get(normalizeId(triggerId, "automation trigger id"));
|
|
4794
|
+
},
|
|
4795
|
+
setTriggerCursor(triggerId, minute) {
|
|
4796
|
+
if (!Number.isSafeInteger(minute) || minute < 0) throw new Error("invalid automation trigger cursor");
|
|
4797
|
+
triggerCursors.set(normalizeId(triggerId, "automation trigger id"), minute);
|
|
4798
|
+
},
|
|
4799
|
+
createActivity(input, now = Date.now()) {
|
|
4800
|
+
const id = normalizeId(generateActivityId(), "automation activity id");
|
|
4801
|
+
if (activities.has(id)) throw new Error("automation activity id already exists");
|
|
4802
|
+
const activity = { id, ...clone2(input), createdAt: now, updatedAt: now };
|
|
4803
|
+
activities.set(id, activity);
|
|
4804
|
+
return clone2(activity);
|
|
4805
|
+
},
|
|
4806
|
+
getActivityByInvocationId(invocationId2) {
|
|
4807
|
+
const normalized = normalizeId(invocationId2, "invocation id");
|
|
4808
|
+
const activity = [...activities.values()].find((candidate) => candidate.invocationId === normalized);
|
|
4809
|
+
return activity ? clone2(activity) : void 0;
|
|
4810
|
+
},
|
|
4811
|
+
updateActivity(id, input, now = Date.now()) {
|
|
4812
|
+
const current = activities.get(id);
|
|
4813
|
+
if (!current) return void 0;
|
|
4814
|
+
const next = { ...current, ...clone2(input), updatedAt: now };
|
|
4815
|
+
activities.set(id, next);
|
|
4816
|
+
return clone2(next);
|
|
4817
|
+
},
|
|
4818
|
+
listActivities(automationId, limit = 100) {
|
|
4819
|
+
const bounded = Math.max(1, Math.min(1e3, Math.trunc(limit)));
|
|
4820
|
+
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(clone2);
|
|
4821
|
+
},
|
|
4745
4822
|
close() {
|
|
4746
4823
|
definitions.clear();
|
|
4747
4824
|
removedDefinitions.clear();
|
|
4748
4825
|
runs.clear();
|
|
4749
4826
|
runInputSnapshots.clear();
|
|
4827
|
+
activities.clear();
|
|
4828
|
+
triggerCursors.clear();
|
|
4750
4829
|
nodeOwners.clear();
|
|
4751
4830
|
}
|
|
4752
4831
|
};
|
|
@@ -4763,7 +4842,11 @@ function definitionFromRow(row) {
|
|
|
4763
4842
|
cwd: row.cwd,
|
|
4764
4843
|
instruction: row.instruction,
|
|
4765
4844
|
runtimeOptions: JSON.parse(row.runtime_options_json),
|
|
4766
|
-
trigger:
|
|
4845
|
+
trigger: { type: "manual" },
|
|
4846
|
+
triggers: (() => {
|
|
4847
|
+
const parsed = JSON.parse(row.trigger_json);
|
|
4848
|
+
return Array.isArray(parsed) ? normalizeConfiguredTriggers(parsed) : [];
|
|
4849
|
+
})(),
|
|
4767
4850
|
revision: row.revision,
|
|
4768
4851
|
createdAt: row.created_at,
|
|
4769
4852
|
updatedAt: row.updated_at
|
|
@@ -4796,6 +4879,22 @@ function runInputSnapshotFromRow(row) {
|
|
|
4796
4879
|
bootstrapState: row.bootstrap_state
|
|
4797
4880
|
};
|
|
4798
4881
|
}
|
|
4882
|
+
function activityFromRow(row) {
|
|
4883
|
+
return {
|
|
4884
|
+
id: row.id,
|
|
4885
|
+
automationId: row.automation_id,
|
|
4886
|
+
triggerId: row.trigger_id,
|
|
4887
|
+
source: row.source,
|
|
4888
|
+
status: row.status,
|
|
4889
|
+
invocationId: row.invocation_id,
|
|
4890
|
+
...row.scheduled_for === null ? {} : { scheduledFor: row.scheduled_for },
|
|
4891
|
+
...row.missed_count === null ? {} : { missedCount: row.missed_count },
|
|
4892
|
+
...row.run_id === null ? {} : { runId: row.run_id },
|
|
4893
|
+
...row.failure_code === null ? {} : { failureCode: row.failure_code },
|
|
4894
|
+
createdAt: row.created_at,
|
|
4895
|
+
updatedAt: row.updated_at
|
|
4896
|
+
};
|
|
4897
|
+
}
|
|
4799
4898
|
function openSessionAutomationStore(opts) {
|
|
4800
4899
|
let Database;
|
|
4801
4900
|
try {
|
|
@@ -4859,6 +4958,27 @@ function openSessionAutomationStore(opts) {
|
|
|
4859
4958
|
bootstrap_state TEXT NOT NULL DEFAULT 'pending'
|
|
4860
4959
|
CHECK(bootstrap_state IN ('pending','submitting','submitted'))
|
|
4861
4960
|
);
|
|
4961
|
+
CREATE TABLE IF NOT EXISTS session_automation_trigger_cursors (
|
|
4962
|
+
trigger_id TEXT PRIMARY KEY,
|
|
4963
|
+
minute INTEGER NOT NULL CHECK(minute >= 0),
|
|
4964
|
+
updated_at INTEGER NOT NULL
|
|
4965
|
+
);
|
|
4966
|
+
CREATE TABLE IF NOT EXISTS session_automation_activities (
|
|
4967
|
+
id TEXT PRIMARY KEY,
|
|
4968
|
+
automation_id TEXT NOT NULL REFERENCES session_automations(id) ON DELETE CASCADE,
|
|
4969
|
+
trigger_id TEXT NOT NULL,
|
|
4970
|
+
source TEXT NOT NULL CHECK(source IN ('schedule','webhook')),
|
|
4971
|
+
status TEXT NOT NULL CHECK(status IN ('queued','started','failed','missed','expired')),
|
|
4972
|
+
invocation_id TEXT NOT NULL UNIQUE,
|
|
4973
|
+
scheduled_for INTEGER,
|
|
4974
|
+
missed_count INTEGER CHECK(missed_count IS NULL OR missed_count > 0),
|
|
4975
|
+
run_id TEXT REFERENCES session_automation_runs(id) ON DELETE SET NULL,
|
|
4976
|
+
failure_code TEXT,
|
|
4977
|
+
created_at INTEGER NOT NULL,
|
|
4978
|
+
updated_at INTEGER NOT NULL
|
|
4979
|
+
);
|
|
4980
|
+
CREATE INDEX IF NOT EXISTS session_automation_activities_definition_idx
|
|
4981
|
+
ON session_automation_activities(automation_id, created_at DESC);
|
|
4862
4982
|
`);
|
|
4863
4983
|
const automationColumns = db.pragma("table_info(session_automations)");
|
|
4864
4984
|
if (!automationColumns.some((column) => column.name === "deleted_at")) {
|
|
@@ -4872,6 +4992,7 @@ function openSessionAutomationStore(opts) {
|
|
|
4872
4992
|
}
|
|
4873
4993
|
const generateAutomationId = opts.generateAutomationId ?? (() => randomId3("rca2"));
|
|
4874
4994
|
const generateRunId = opts.generateRunId ?? (() => randomId3("rcar"));
|
|
4995
|
+
const generateActivityId = opts.generateActivityId ?? (() => randomId3("rcae"));
|
|
4875
4996
|
const getDefinition = db.prepare("SELECT * FROM session_automations WHERE id = ? AND deleted_at IS NULL");
|
|
4876
4997
|
const getDefinitionIncludingRemoved = db.prepare("SELECT * FROM session_automations WHERE id = ?");
|
|
4877
4998
|
const listAll = db.prepare("SELECT * FROM session_automations WHERE deleted_at IS NULL ORDER BY updated_at DESC, id");
|
|
@@ -4898,6 +5019,30 @@ function openSessionAutomationStore(opts) {
|
|
|
4898
5019
|
const listRunsForAutomation = db.prepare(
|
|
4899
5020
|
"SELECT * FROM session_automation_runs WHERE automation_id = ? ORDER BY created_at DESC, id DESC LIMIT ?"
|
|
4900
5021
|
);
|
|
5022
|
+
const getTriggerCursorStatement = db.prepare(
|
|
5023
|
+
"SELECT minute FROM session_automation_trigger_cursors WHERE trigger_id = ?"
|
|
5024
|
+
);
|
|
5025
|
+
const setTriggerCursorStatement = db.prepare(
|
|
5026
|
+
`INSERT INTO session_automation_trigger_cursors(trigger_id,minute,updated_at) VALUES(?,?,?)
|
|
5027
|
+
ON CONFLICT(trigger_id) DO UPDATE SET minute=excluded.minute,updated_at=excluded.updated_at`
|
|
5028
|
+
);
|
|
5029
|
+
const getActivity = db.prepare("SELECT * FROM session_automation_activities WHERE id = ?");
|
|
5030
|
+
const getActivityByInvocationId = db.prepare("SELECT * FROM session_automation_activities WHERE invocation_id = ?");
|
|
5031
|
+
const insertActivity = db.prepare(
|
|
5032
|
+
`INSERT INTO session_automation_activities(
|
|
5033
|
+
id,automation_id,trigger_id,source,status,invocation_id,scheduled_for,missed_count,run_id,failure_code,
|
|
5034
|
+
created_at,updated_at
|
|
5035
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`
|
|
5036
|
+
);
|
|
5037
|
+
const updateActivityStatement = db.prepare(
|
|
5038
|
+
`UPDATE session_automation_activities SET status=?,run_id=?,failure_code=?,updated_at=? WHERE id=?`
|
|
5039
|
+
);
|
|
5040
|
+
const listActivitiesAll = db.prepare(
|
|
5041
|
+
"SELECT * FROM session_automation_activities ORDER BY created_at DESC, id DESC LIMIT ?"
|
|
5042
|
+
);
|
|
5043
|
+
const listActivitiesForAutomation = db.prepare(
|
|
5044
|
+
"SELECT * FROM session_automation_activities WHERE automation_id = ? ORDER BY created_at DESC, id DESC LIMIT ?"
|
|
5045
|
+
);
|
|
4901
5046
|
return {
|
|
4902
5047
|
mode: "sqlite",
|
|
4903
5048
|
getNodeOwner(nodeId) {
|
|
@@ -4937,7 +5082,7 @@ function openSessionAutomationStore(opts) {
|
|
|
4937
5082
|
definition.cwd,
|
|
4938
5083
|
definition.instruction,
|
|
4939
5084
|
JSON.stringify(definition.runtimeOptions),
|
|
4940
|
-
JSON.stringify(definition.
|
|
5085
|
+
JSON.stringify(definition.triggers),
|
|
4941
5086
|
definition.revision,
|
|
4942
5087
|
definition.createdAt,
|
|
4943
5088
|
definition.updatedAt
|
|
@@ -4959,7 +5104,7 @@ function openSessionAutomationStore(opts) {
|
|
|
4959
5104
|
next.cwd,
|
|
4960
5105
|
next.instruction,
|
|
4961
5106
|
JSON.stringify(next.runtimeOptions),
|
|
4962
|
-
JSON.stringify(next.
|
|
5107
|
+
JSON.stringify(next.triggers),
|
|
4963
5108
|
next.revision,
|
|
4964
5109
|
next.updatedAt,
|
|
4965
5110
|
next.id,
|
|
@@ -5020,8 +5165,8 @@ function openSessionAutomationStore(opts) {
|
|
|
5020
5165
|
const row = selectRunInputSnapshot.get(id);
|
|
5021
5166
|
return row ? runInputSnapshotFromRow(row) : void 0;
|
|
5022
5167
|
},
|
|
5023
|
-
getRunByInvocationId(
|
|
5024
|
-
const row = getRunByInvocationId.get(
|
|
5168
|
+
getRunByInvocationId(invocationId2) {
|
|
5169
|
+
const row = getRunByInvocationId.get(invocationId2);
|
|
5025
5170
|
return row ? runFromRow(row) : void 0;
|
|
5026
5171
|
},
|
|
5027
5172
|
getRunBySessionId(sessionId) {
|
|
@@ -5132,12 +5277,266 @@ function openSessionAutomationStore(opts) {
|
|
|
5132
5277
|
const rows = automationId ? listRunsForAutomation.all(automationId, bounded) : listRunsAll.all(bounded);
|
|
5133
5278
|
return rows.map(runFromRow);
|
|
5134
5279
|
},
|
|
5280
|
+
getTriggerCursor(triggerId) {
|
|
5281
|
+
const row = getTriggerCursorStatement.get(normalizeId(triggerId, "automation trigger id"));
|
|
5282
|
+
return row?.minute;
|
|
5283
|
+
},
|
|
5284
|
+
setTriggerCursor(triggerId, minute) {
|
|
5285
|
+
if (!Number.isSafeInteger(minute) || minute < 0) throw new Error("invalid automation trigger cursor");
|
|
5286
|
+
setTriggerCursorStatement.run(normalizeId(triggerId, "automation trigger id"), minute, Date.now());
|
|
5287
|
+
},
|
|
5288
|
+
createActivity(input, now = Date.now()) {
|
|
5289
|
+
const activity = {
|
|
5290
|
+
id: normalizeId(generateActivityId(), "automation activity id"),
|
|
5291
|
+
...input,
|
|
5292
|
+
automationId: normalizeId(input.automationId, "automation id"),
|
|
5293
|
+
triggerId: normalizeId(input.triggerId, "automation trigger id"),
|
|
5294
|
+
invocationId: normalizeId(input.invocationId, "invocation id"),
|
|
5295
|
+
createdAt: now,
|
|
5296
|
+
updatedAt: now
|
|
5297
|
+
};
|
|
5298
|
+
insertActivity.run(
|
|
5299
|
+
activity.id,
|
|
5300
|
+
activity.automationId,
|
|
5301
|
+
activity.triggerId,
|
|
5302
|
+
activity.source,
|
|
5303
|
+
activity.status,
|
|
5304
|
+
activity.invocationId,
|
|
5305
|
+
activity.scheduledFor ?? null,
|
|
5306
|
+
activity.missedCount ?? null,
|
|
5307
|
+
activity.runId ?? null,
|
|
5308
|
+
activity.failureCode ?? null,
|
|
5309
|
+
activity.createdAt,
|
|
5310
|
+
activity.updatedAt
|
|
5311
|
+
);
|
|
5312
|
+
return activity;
|
|
5313
|
+
},
|
|
5314
|
+
getActivityByInvocationId(invocationId2) {
|
|
5315
|
+
const row = getActivityByInvocationId.get(normalizeId(invocationId2, "invocation id"));
|
|
5316
|
+
return row ? activityFromRow(row) : void 0;
|
|
5317
|
+
},
|
|
5318
|
+
updateActivity(id, input, now = Date.now()) {
|
|
5319
|
+
const row = getActivity.get(id);
|
|
5320
|
+
if (!row) return void 0;
|
|
5321
|
+
const current = activityFromRow(row);
|
|
5322
|
+
const next = { ...current, ...input, updatedAt: now };
|
|
5323
|
+
const result = updateActivityStatement.run(
|
|
5324
|
+
next.status,
|
|
5325
|
+
next.runId ?? null,
|
|
5326
|
+
next.failureCode ?? null,
|
|
5327
|
+
next.updatedAt,
|
|
5328
|
+
current.id
|
|
5329
|
+
);
|
|
5330
|
+
return result.changes === 1 ? next : void 0;
|
|
5331
|
+
},
|
|
5332
|
+
listActivities(automationId, limit = 100) {
|
|
5333
|
+
const bounded = Math.max(1, Math.min(1e3, Math.trunc(limit)));
|
|
5334
|
+
const rows = automationId ? listActivitiesForAutomation.all(automationId, bounded) : listActivitiesAll.all(bounded);
|
|
5335
|
+
return rows.map(activityFromRow);
|
|
5336
|
+
},
|
|
5135
5337
|
close() {
|
|
5136
5338
|
db.close();
|
|
5137
5339
|
}
|
|
5138
5340
|
};
|
|
5139
5341
|
}
|
|
5140
5342
|
|
|
5343
|
+
// src/automation-trigger-engine.ts
|
|
5344
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5345
|
+
function parsePart(part, min, max, normalize) {
|
|
5346
|
+
const [rangePart, stepPart] = part.split("/");
|
|
5347
|
+
const step = stepPart === void 0 ? 1 : Number(stepPart);
|
|
5348
|
+
if (!Number.isSafeInteger(step) || step < 1 || !rangePart) throw new Error("invalid cron field");
|
|
5349
|
+
let start;
|
|
5350
|
+
let end;
|
|
5351
|
+
if (rangePart === "*") {
|
|
5352
|
+
start = min;
|
|
5353
|
+
end = max;
|
|
5354
|
+
} else if (rangePart.includes("-")) {
|
|
5355
|
+
const pair = rangePart.split("-");
|
|
5356
|
+
if (pair.length !== 2) throw new Error("invalid cron field");
|
|
5357
|
+
start = Number(pair[0]);
|
|
5358
|
+
end = Number(pair[1]);
|
|
5359
|
+
} else {
|
|
5360
|
+
start = Number(rangePart);
|
|
5361
|
+
end = start;
|
|
5362
|
+
}
|
|
5363
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < min || end > max || start > end) {
|
|
5364
|
+
throw new Error("invalid cron field");
|
|
5365
|
+
}
|
|
5366
|
+
const values = [];
|
|
5367
|
+
for (let value = start; value <= end; value += step) values.push(normalize ? normalize(value) : value);
|
|
5368
|
+
return values;
|
|
5369
|
+
}
|
|
5370
|
+
function parseField(source, min, max, normalize) {
|
|
5371
|
+
const values = /* @__PURE__ */ new Set();
|
|
5372
|
+
for (const part of source.split(",")) {
|
|
5373
|
+
for (const value of parsePart(part, min, max, normalize)) values.add(value);
|
|
5374
|
+
}
|
|
5375
|
+
return { any: source === "*", values };
|
|
5376
|
+
}
|
|
5377
|
+
function validateCronExpression(expression) {
|
|
5378
|
+
const normalized = expression.trim().replace(/\s+/g, " ");
|
|
5379
|
+
const fields = normalized.split(" ");
|
|
5380
|
+
if (fields.length !== 5) throw new Error("cron must contain five fields");
|
|
5381
|
+
parseField(fields[0], 0, 59);
|
|
5382
|
+
parseField(fields[1], 0, 23);
|
|
5383
|
+
parseField(fields[2], 1, 31);
|
|
5384
|
+
parseField(fields[3], 1, 12);
|
|
5385
|
+
parseField(fields[4], 0, 7, (value) => value === 7 ? 0 : value);
|
|
5386
|
+
return normalized;
|
|
5387
|
+
}
|
|
5388
|
+
function zonedParts(time, timeZone) {
|
|
5389
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
5390
|
+
timeZone,
|
|
5391
|
+
minute: "2-digit",
|
|
5392
|
+
hour: "2-digit",
|
|
5393
|
+
hourCycle: "h23",
|
|
5394
|
+
day: "2-digit",
|
|
5395
|
+
month: "2-digit",
|
|
5396
|
+
weekday: "short"
|
|
5397
|
+
}).formatToParts(time);
|
|
5398
|
+
const byType = new Map(parts.map((part) => [part.type, part.value]));
|
|
5399
|
+
const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(byType.get("weekday") ?? "");
|
|
5400
|
+
return {
|
|
5401
|
+
minute: Number(byType.get("minute")),
|
|
5402
|
+
hour: Number(byType.get("hour")),
|
|
5403
|
+
day: Number(byType.get("day")),
|
|
5404
|
+
month: Number(byType.get("month")),
|
|
5405
|
+
weekday
|
|
5406
|
+
};
|
|
5407
|
+
}
|
|
5408
|
+
function cronMatches(expression, timeZone, time) {
|
|
5409
|
+
const fields = validateCronExpression(expression).split(" ");
|
|
5410
|
+
const minute = parseField(fields[0], 0, 59);
|
|
5411
|
+
const hour = parseField(fields[1], 0, 23);
|
|
5412
|
+
const day = parseField(fields[2], 1, 31);
|
|
5413
|
+
const month = parseField(fields[3], 1, 12);
|
|
5414
|
+
const weekday = parseField(fields[4], 0, 7, (value2) => value2 === 7 ? 0 : value2);
|
|
5415
|
+
const value = zonedParts(time, timeZone);
|
|
5416
|
+
const dayMatches = day.values.has(value.day);
|
|
5417
|
+
const weekdayMatches = weekday.values.has(value.weekday);
|
|
5418
|
+
const calendarMatches = day.any && weekday.any ? true : day.any ? weekdayMatches : weekday.any ? dayMatches : dayMatches || weekdayMatches;
|
|
5419
|
+
return minute.values.has(value.minute) && hour.values.has(value.hour) && month.values.has(value.month) && calendarMatches;
|
|
5420
|
+
}
|
|
5421
|
+
function invocationId(source, automationId, triggerId, nonce) {
|
|
5422
|
+
const safeNonce = nonce.replace(/[^A-Za-z0-9._:-]/g, "_").slice(0, 96);
|
|
5423
|
+
return `rci_${source}_${automationId}_${triggerId}_${safeNonce}`.slice(0, 256);
|
|
5424
|
+
}
|
|
5425
|
+
function createAutomationTriggerEngine(options) {
|
|
5426
|
+
const now = options.now ?? Date.now;
|
|
5427
|
+
const interval = options.setInterval ?? globalThis.setInterval;
|
|
5428
|
+
const clearInterval2 = options.clearInterval ?? globalThis.clearInterval;
|
|
5429
|
+
const concurrency = Number.isSafeInteger(options.concurrency) && (options.concurrency ?? 0) > 0 ? options.concurrency : 2;
|
|
5430
|
+
const pending = [];
|
|
5431
|
+
const pendingIds = /* @__PURE__ */ new Set();
|
|
5432
|
+
const inFlightIds = /* @__PURE__ */ new Set();
|
|
5433
|
+
let active = 0;
|
|
5434
|
+
let ticking = false;
|
|
5435
|
+
let timer;
|
|
5436
|
+
const queue = (activity, deferDrain = false) => {
|
|
5437
|
+
if (activity.status !== "queued" || pendingIds.has(activity.id) || inFlightIds.has(activity.id)) return;
|
|
5438
|
+
pendingIds.add(activity.id);
|
|
5439
|
+
pending.push(activity);
|
|
5440
|
+
pending.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id));
|
|
5441
|
+
if (!deferDrain) drain();
|
|
5442
|
+
};
|
|
5443
|
+
const drain = () => {
|
|
5444
|
+
while (active < concurrency && pending.length > 0) {
|
|
5445
|
+
const activity = pending.shift();
|
|
5446
|
+
pendingIds.delete(activity.id);
|
|
5447
|
+
inFlightIds.add(activity.id);
|
|
5448
|
+
active += 1;
|
|
5449
|
+
void options.execute(activity).then(({ runId }) => options.store.updateActivity(activity.id, { status: "started", runId })).catch(
|
|
5450
|
+
() => options.store.updateActivity(activity.id, { status: "failed", failureCode: "TRIGGER_EXECUTION_FAILED" })
|
|
5451
|
+
).finally(() => {
|
|
5452
|
+
inFlightIds.delete(activity.id);
|
|
5453
|
+
active -= 1;
|
|
5454
|
+
drain();
|
|
5455
|
+
});
|
|
5456
|
+
}
|
|
5457
|
+
};
|
|
5458
|
+
const createQueued = (automation, trigger, source, nonce, scheduledFor) => {
|
|
5459
|
+
const id = invocationId(source, automation.id, trigger.id, nonce);
|
|
5460
|
+
const existing = options.store.getActivityByInvocationId(id);
|
|
5461
|
+
if (existing) {
|
|
5462
|
+
queue(existing);
|
|
5463
|
+
return existing;
|
|
5464
|
+
}
|
|
5465
|
+
const activity = options.store.createActivity({
|
|
5466
|
+
automationId: automation.id,
|
|
5467
|
+
triggerId: trigger.id,
|
|
5468
|
+
source,
|
|
5469
|
+
status: "queued",
|
|
5470
|
+
invocationId: id,
|
|
5471
|
+
...scheduledFor === void 0 ? {} : { scheduledFor }
|
|
5472
|
+
});
|
|
5473
|
+
queue(activity);
|
|
5474
|
+
return activity;
|
|
5475
|
+
};
|
|
5476
|
+
const tick = async (at = now()) => {
|
|
5477
|
+
if (ticking) return;
|
|
5478
|
+
ticking = true;
|
|
5479
|
+
try {
|
|
5480
|
+
const currentMinute = Math.floor(at / 6e4);
|
|
5481
|
+
for (const automation of options.store.list()) {
|
|
5482
|
+
if (!automation.enabled) continue;
|
|
5483
|
+
for (const trigger of automation.triggers) {
|
|
5484
|
+
if (trigger.type !== "schedule" || !trigger.enabled) continue;
|
|
5485
|
+
const previous = options.store.getTriggerCursor(trigger.id) ?? currentMinute - 1;
|
|
5486
|
+
if (previous >= currentMinute) continue;
|
|
5487
|
+
const firstMinute = Math.max(previous + 1, currentMinute - 43200);
|
|
5488
|
+
const matches = [];
|
|
5489
|
+
for (let minute = firstMinute; minute <= currentMinute; minute += 1) {
|
|
5490
|
+
if (cronMatches(trigger.cron, trigger.timeZone, minute * 6e4)) matches.push(minute);
|
|
5491
|
+
}
|
|
5492
|
+
const missed = matches.filter((minute) => minute < currentMinute);
|
|
5493
|
+
if (missed.length > 0) {
|
|
5494
|
+
const missedInvocation = invocationId("schedule", automation.id, trigger.id, `missed-${currentMinute}`);
|
|
5495
|
+
const exists = options.store.listActivities(automation.id, 1e3).some((activity) => activity.invocationId === missedInvocation);
|
|
5496
|
+
if (!exists) {
|
|
5497
|
+
options.store.createActivity({
|
|
5498
|
+
automationId: automation.id,
|
|
5499
|
+
triggerId: trigger.id,
|
|
5500
|
+
source: "schedule",
|
|
5501
|
+
status: "missed",
|
|
5502
|
+
invocationId: missedInvocation,
|
|
5503
|
+
scheduledFor: missed[missed.length - 1] * 6e4,
|
|
5504
|
+
missedCount: missed.length,
|
|
5505
|
+
failureCode: "NODE_OFFLINE_MISSED_SCHEDULE"
|
|
5506
|
+
});
|
|
5507
|
+
}
|
|
5508
|
+
}
|
|
5509
|
+
if (matches.includes(currentMinute)) {
|
|
5510
|
+
createQueued(automation, trigger, "schedule", String(currentMinute), currentMinute * 6e4);
|
|
5511
|
+
}
|
|
5512
|
+
options.store.setTriggerCursor(trigger.id, currentMinute);
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
} finally {
|
|
5516
|
+
ticking = false;
|
|
5517
|
+
}
|
|
5518
|
+
};
|
|
5519
|
+
return {
|
|
5520
|
+
start() {
|
|
5521
|
+
if (timer) return;
|
|
5522
|
+
for (const activity of options.store.listActivities(void 0, 1e3)) queue(activity, true);
|
|
5523
|
+
drain();
|
|
5524
|
+
void tick();
|
|
5525
|
+
timer = interval(() => void tick(), 15e3);
|
|
5526
|
+
},
|
|
5527
|
+
stop() {
|
|
5528
|
+
if (!timer) return;
|
|
5529
|
+
clearInterval2(timer);
|
|
5530
|
+
timer = void 0;
|
|
5531
|
+
},
|
|
5532
|
+
tick,
|
|
5533
|
+
enqueueWebhook(automation, trigger, externalInvocationId) {
|
|
5534
|
+
if (trigger.type !== "webhook") throw new Error("invalid webhook trigger");
|
|
5535
|
+
return createQueued(automation, trigger, "webhook", externalInvocationId ?? randomUUID3());
|
|
5536
|
+
}
|
|
5537
|
+
};
|
|
5538
|
+
}
|
|
5539
|
+
|
|
5141
5540
|
// src/node-domain.ts
|
|
5142
5541
|
import { createHash as createHash5 } from "crypto";
|
|
5143
5542
|
function productContextFromOwner(owner, name) {
|
|
@@ -5211,7 +5610,7 @@ function projectAgentRuntimeRecords(input) {
|
|
|
5211
5610
|
|
|
5212
5611
|
// src/updater.ts
|
|
5213
5612
|
import { spawn as nodeSpawn } from "child_process";
|
|
5214
|
-
import { randomUUID as
|
|
5613
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
5215
5614
|
import {
|
|
5216
5615
|
chmodSync as nodeChmodSync,
|
|
5217
5616
|
existsSync as nodeExistsSync,
|
|
@@ -5225,7 +5624,7 @@ import { fileURLToPath } from "url";
|
|
|
5225
5624
|
|
|
5226
5625
|
// src/managed-runtime.ts
|
|
5227
5626
|
import { spawn } from "child_process";
|
|
5228
|
-
import { randomUUID as
|
|
5627
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
5229
5628
|
import {
|
|
5230
5629
|
chmodSync as chmodSync2,
|
|
5231
5630
|
existsSync as existsSync4,
|
|
@@ -5323,7 +5722,7 @@ function readPreviousVersion(root) {
|
|
|
5323
5722
|
}
|
|
5324
5723
|
|
|
5325
5724
|
// src/updater.ts
|
|
5326
|
-
var RUNNING_VERSION = "1.
|
|
5725
|
+
var RUNNING_VERSION = "1.4.0" ? "1.4.0".replace(/^v/, "") : packageVersion();
|
|
5327
5726
|
var RELEASES_API = "https://api.github.com/repos/burakgon/roamcode/releases?per_page=100";
|
|
5328
5727
|
var RELEASE_MANIFEST_ASSET = "roamcode-release.json";
|
|
5329
5728
|
var CHECK_CACHE_MS = 15 * 6e4;
|
|
@@ -5622,7 +6021,7 @@ var Updater = class {
|
|
|
5622
6021
|
const path = join6(this.dataDir, "update-status.json");
|
|
5623
6022
|
const value = JSON.stringify(status, null, 2) + "\n";
|
|
5624
6023
|
if (this.fs.renameSync) {
|
|
5625
|
-
const temp = `${path}.${process.pid}.${
|
|
6024
|
+
const temp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
|
|
5626
6025
|
this.fs.writeFileSync(temp, value, 384);
|
|
5627
6026
|
this.fs.renameSync(temp, path);
|
|
5628
6027
|
} else {
|
|
@@ -5685,7 +6084,7 @@ var Updater = class {
|
|
|
5685
6084
|
return { started: false, reason: error instanceof Error ? error.message : String(error) };
|
|
5686
6085
|
}
|
|
5687
6086
|
}
|
|
5688
|
-
const operationId =
|
|
6087
|
+
const operationId = randomUUID5();
|
|
5689
6088
|
const status = {
|
|
5690
6089
|
operationId,
|
|
5691
6090
|
state: "starting",
|
|
@@ -10702,9 +11101,12 @@ function buildOpenApiDocument(options) {
|
|
|
10702
11101
|
responses: {
|
|
10703
11102
|
"201": response("Created session automation", {
|
|
10704
11103
|
type: "object",
|
|
10705
|
-
required: ["automation"],
|
|
11104
|
+
required: ["automation", "webhookSecrets"],
|
|
10706
11105
|
additionalProperties: false,
|
|
10707
|
-
properties: {
|
|
11106
|
+
properties: {
|
|
11107
|
+
automation: ref("SessionAutomationDefinition"),
|
|
11108
|
+
webhookSecrets: { type: "array", items: ref("SessionAutomationWebhookSecret") }
|
|
11109
|
+
}
|
|
10708
11110
|
}),
|
|
10709
11111
|
"404": response("Node or runtime not found", ref("Error")),
|
|
10710
11112
|
...errorResponses
|
|
@@ -10733,9 +11135,12 @@ function buildOpenApiDocument(options) {
|
|
|
10733
11135
|
responses: {
|
|
10734
11136
|
"200": response("Updated session automation", {
|
|
10735
11137
|
type: "object",
|
|
10736
|
-
required: ["automation"],
|
|
11138
|
+
required: ["automation", "webhookSecrets"],
|
|
10737
11139
|
additionalProperties: false,
|
|
10738
|
-
properties: {
|
|
11140
|
+
properties: {
|
|
11141
|
+
automation: ref("SessionAutomationDefinition"),
|
|
11142
|
+
webhookSecrets: { type: "array", items: ref("SessionAutomationWebhookSecret") }
|
|
11143
|
+
}
|
|
10739
11144
|
}),
|
|
10740
11145
|
"404": response("Automation, node, or runtime not found", ref("Error")),
|
|
10741
11146
|
...errorResponses
|
|
@@ -10751,6 +11156,76 @@ function buildOpenApiDocument(options) {
|
|
|
10751
11156
|
}
|
|
10752
11157
|
}
|
|
10753
11158
|
},
|
|
11159
|
+
"/api/v2/automations/{automationId}/activity": {
|
|
11160
|
+
get: {
|
|
11161
|
+
operationId: "listSessionAutomationActivityV2",
|
|
11162
|
+
description: "Returns durable schedule and webhook delivery activity, including explicit missed schedules.",
|
|
11163
|
+
parameters: [
|
|
11164
|
+
idParameter("automationId"),
|
|
11165
|
+
{ name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 100, default: 25 } }
|
|
11166
|
+
],
|
|
11167
|
+
responses: {
|
|
11168
|
+
"200": response("Bounded automation trigger activity", {
|
|
11169
|
+
type: "object",
|
|
11170
|
+
required: ["activities"],
|
|
11171
|
+
additionalProperties: false,
|
|
11172
|
+
properties: { activities: { type: "array", items: ref("SessionAutomationActivity") } }
|
|
11173
|
+
}),
|
|
11174
|
+
"404": response("Automation not found", ref("Error")),
|
|
11175
|
+
...errorResponses
|
|
11176
|
+
}
|
|
11177
|
+
}
|
|
11178
|
+
},
|
|
11179
|
+
"/api/v2/automations/{automationId}/triggers/{triggerId}/secret": {
|
|
11180
|
+
post: {
|
|
11181
|
+
operationId: "rotateSessionAutomationWebhookSecretV2",
|
|
11182
|
+
parameters: [idParameter("automationId"), idParameter("triggerId"), idempotency],
|
|
11183
|
+
requestBody: {
|
|
11184
|
+
required: true,
|
|
11185
|
+
content: json({
|
|
11186
|
+
type: "object",
|
|
11187
|
+
required: ["expectedRevision"],
|
|
11188
|
+
additionalProperties: false,
|
|
11189
|
+
properties: { expectedRevision: { type: "integer", minimum: 1 } }
|
|
11190
|
+
})
|
|
11191
|
+
},
|
|
11192
|
+
responses: {
|
|
11193
|
+
"200": response("Rotated webhook secret shown exactly once", {
|
|
11194
|
+
type: "object",
|
|
11195
|
+
required: ["automation", "webhookSecret"],
|
|
11196
|
+
additionalProperties: false,
|
|
11197
|
+
properties: {
|
|
11198
|
+
automation: ref("SessionAutomationDefinition"),
|
|
11199
|
+
webhookSecret: ref("SessionAutomationWebhookSecret")
|
|
11200
|
+
}
|
|
11201
|
+
}),
|
|
11202
|
+
"404": response("Automation or webhook trigger not found", ref("Error")),
|
|
11203
|
+
...errorResponses
|
|
11204
|
+
}
|
|
11205
|
+
}
|
|
11206
|
+
},
|
|
11207
|
+
"/api/v2/automation-hooks/{hookId}": {
|
|
11208
|
+
post: {
|
|
11209
|
+
operationId: "signalSessionAutomationWebhookV2",
|
|
11210
|
+
description: "Queues a signal-only webhook. The request body is discarded and never becomes task instruction or stored prompt content.",
|
|
11211
|
+
security: [{ webhookAuth: [] }],
|
|
11212
|
+
parameters: [idParameter("hookId")],
|
|
11213
|
+
requestBody: {
|
|
11214
|
+
required: false,
|
|
11215
|
+
content: json({ description: "Ignored signal payload", nullable: true })
|
|
11216
|
+
},
|
|
11217
|
+
responses: {
|
|
11218
|
+
"202": response("Webhook signal accepted", {
|
|
11219
|
+
type: "object",
|
|
11220
|
+
required: ["accepted"],
|
|
11221
|
+
additionalProperties: false,
|
|
11222
|
+
properties: { accepted: { const: true } }
|
|
11223
|
+
}),
|
|
11224
|
+
"401": response("Unknown hook or invalid webhook secret", ref("Error")),
|
|
11225
|
+
"429": response("Webhook rate limit reached", ref("Error"))
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
},
|
|
10754
11229
|
"/api/v2/automations/{automationId}/runs": {
|
|
10755
11230
|
get: {
|
|
10756
11231
|
operationId: "listSessionAutomationRunsV2",
|
|
@@ -10808,7 +11283,8 @@ function buildOpenApiDocument(options) {
|
|
|
10808
11283
|
},
|
|
10809
11284
|
components: {
|
|
10810
11285
|
securitySchemes: {
|
|
10811
|
-
bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "RoamCode device credential" }
|
|
11286
|
+
bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "RoamCode device credential" },
|
|
11287
|
+
webhookAuth: { type: "http", scheme: "bearer", bearerFormat: "One-time-shown webhook secret" }
|
|
10812
11288
|
},
|
|
10813
11289
|
schemas: {
|
|
10814
11290
|
Error: {
|
|
@@ -11782,6 +12258,62 @@ function buildOpenApiDocument(options) {
|
|
|
11782
12258
|
additionalProperties: false,
|
|
11783
12259
|
properties: { type: { const: "manual" } }
|
|
11784
12260
|
},
|
|
12261
|
+
SessionAutomationConfiguredTrigger: {
|
|
12262
|
+
oneOf: [
|
|
12263
|
+
{
|
|
12264
|
+
type: "object",
|
|
12265
|
+
required: ["id", "type", "enabled", "cron", "timeZone", "missedRunPolicy"],
|
|
12266
|
+
additionalProperties: false,
|
|
12267
|
+
properties: {
|
|
12268
|
+
id: { type: "string", minLength: 1, maxLength: 256 },
|
|
12269
|
+
type: { const: "schedule" },
|
|
12270
|
+
enabled: { type: "boolean" },
|
|
12271
|
+
cron: { type: "string", minLength: 9, maxLength: 120 },
|
|
12272
|
+
timeZone: { type: "string", minLength: 1, maxLength: 80 },
|
|
12273
|
+
missedRunPolicy: { const: "skip" }
|
|
12274
|
+
}
|
|
12275
|
+
},
|
|
12276
|
+
{
|
|
12277
|
+
type: "object",
|
|
12278
|
+
required: ["id", "type", "enabled", "hookId"],
|
|
12279
|
+
additionalProperties: false,
|
|
12280
|
+
properties: {
|
|
12281
|
+
id: { type: "string", minLength: 1, maxLength: 256 },
|
|
12282
|
+
type: { const: "webhook" },
|
|
12283
|
+
enabled: { type: "boolean" },
|
|
12284
|
+
hookId: { type: "string", pattern: "^rcwh_[A-Za-z0-9_-]{24,80}$" }
|
|
12285
|
+
}
|
|
12286
|
+
}
|
|
12287
|
+
]
|
|
12288
|
+
},
|
|
12289
|
+
SessionAutomationTriggerInput: {
|
|
12290
|
+
oneOf: [
|
|
12291
|
+
{
|
|
12292
|
+
type: "object",
|
|
12293
|
+
required: ["type", "enabled", "cron", "timeZone", "missedRunPolicy"],
|
|
12294
|
+
additionalProperties: false,
|
|
12295
|
+
properties: {
|
|
12296
|
+
id: { type: "string", minLength: 1, maxLength: 256 },
|
|
12297
|
+
type: { const: "schedule" },
|
|
12298
|
+
enabled: { type: "boolean" },
|
|
12299
|
+
cron: { type: "string", minLength: 9, maxLength: 120 },
|
|
12300
|
+
timeZone: { type: "string", minLength: 1, maxLength: 80 },
|
|
12301
|
+
missedRunPolicy: { const: "skip" }
|
|
12302
|
+
}
|
|
12303
|
+
},
|
|
12304
|
+
{
|
|
12305
|
+
type: "object",
|
|
12306
|
+
required: ["type", "enabled"],
|
|
12307
|
+
additionalProperties: false,
|
|
12308
|
+
properties: {
|
|
12309
|
+
id: { type: "string", minLength: 1, maxLength: 256 },
|
|
12310
|
+
type: { const: "webhook" },
|
|
12311
|
+
enabled: { type: "boolean" },
|
|
12312
|
+
hookId: { type: "string", pattern: "^rcwh_[A-Za-z0-9_-]{24,80}$" }
|
|
12313
|
+
}
|
|
12314
|
+
}
|
|
12315
|
+
]
|
|
12316
|
+
},
|
|
11785
12317
|
SessionAutomationDefinition: {
|
|
11786
12318
|
type: "object",
|
|
11787
12319
|
required: [
|
|
@@ -11794,6 +12326,7 @@ function buildOpenApiDocument(options) {
|
|
|
11794
12326
|
"provider",
|
|
11795
12327
|
"cwd",
|
|
11796
12328
|
"trigger",
|
|
12329
|
+
"triggers",
|
|
11797
12330
|
"instruction",
|
|
11798
12331
|
"runtimeOptions",
|
|
11799
12332
|
"revision",
|
|
@@ -11811,6 +12344,7 @@ function buildOpenApiDocument(options) {
|
|
|
11811
12344
|
provider: { type: "string", pattern: "^[a-z][a-z0-9-]{0,63}$" },
|
|
11812
12345
|
cwd: { type: "string", minLength: 1 },
|
|
11813
12346
|
trigger: ref("SessionAutomationTrigger"),
|
|
12347
|
+
triggers: { type: "array", maxItems: 16, items: ref("SessionAutomationConfiguredTrigger") },
|
|
11814
12348
|
instruction: {
|
|
11815
12349
|
type: "string",
|
|
11816
12350
|
minLength: 1,
|
|
@@ -11835,6 +12369,7 @@ function buildOpenApiDocument(options) {
|
|
|
11835
12369
|
agentRuntimeId: { type: "string", pattern: "^runtime_[A-Za-z0-9_-]{24}$" },
|
|
11836
12370
|
cwd: { type: "string", minLength: 1 },
|
|
11837
12371
|
trigger: ref("SessionAutomationTrigger"),
|
|
12372
|
+
triggers: { type: "array", maxItems: 16, items: ref("SessionAutomationTriggerInput") },
|
|
11838
12373
|
instruction: {
|
|
11839
12374
|
type: "string",
|
|
11840
12375
|
minLength: 1,
|
|
@@ -11858,6 +12393,7 @@ function buildOpenApiDocument(options) {
|
|
|
11858
12393
|
agentRuntimeId: { type: "string", pattern: "^runtime_[A-Za-z0-9_-]{24}$" },
|
|
11859
12394
|
cwd: { type: "string", minLength: 1 },
|
|
11860
12395
|
trigger: ref("SessionAutomationTrigger"),
|
|
12396
|
+
triggers: { type: "array", maxItems: 16, items: ref("SessionAutomationTriggerInput") },
|
|
11861
12397
|
instruction: {
|
|
11862
12398
|
type: "string",
|
|
11863
12399
|
minLength: 1,
|
|
@@ -11868,6 +12404,36 @@ function buildOpenApiDocument(options) {
|
|
|
11868
12404
|
runtimeOptions: { type: "object", additionalProperties: true, "x-maxBytes": 65536 }
|
|
11869
12405
|
}
|
|
11870
12406
|
},
|
|
12407
|
+
SessionAutomationWebhookSecret: {
|
|
12408
|
+
type: "object",
|
|
12409
|
+
required: ["triggerId", "hookId", "secret", "path"],
|
|
12410
|
+
additionalProperties: false,
|
|
12411
|
+
properties: {
|
|
12412
|
+
triggerId: { type: "string", minLength: 1, maxLength: 256 },
|
|
12413
|
+
hookId: { type: "string", pattern: "^rcwh_[A-Za-z0-9_-]{24,80}$" },
|
|
12414
|
+
secret: { type: "string", pattern: "^rcws_[A-Za-z0-9_-]{43}$" },
|
|
12415
|
+
path: { type: "string", pattern: "^/api/v2/automation-hooks/rcwh_[A-Za-z0-9_-]{24,80}$" }
|
|
12416
|
+
}
|
|
12417
|
+
},
|
|
12418
|
+
SessionAutomationActivity: {
|
|
12419
|
+
type: "object",
|
|
12420
|
+
required: ["id", "automationId", "triggerId", "source", "status", "invocationId", "createdAt", "updatedAt"],
|
|
12421
|
+
additionalProperties: false,
|
|
12422
|
+
properties: {
|
|
12423
|
+
id: { type: "string", minLength: 1, maxLength: 256 },
|
|
12424
|
+
automationId: { type: "string", minLength: 1, maxLength: 256 },
|
|
12425
|
+
triggerId: { type: "string", minLength: 1, maxLength: 256 },
|
|
12426
|
+
source: { enum: ["schedule", "webhook"] },
|
|
12427
|
+
status: { enum: ["queued", "started", "failed", "missed", "expired"] },
|
|
12428
|
+
invocationId: { type: "string", minLength: 1, maxLength: 256 },
|
|
12429
|
+
scheduledFor: { type: "integer", minimum: 0 },
|
|
12430
|
+
missedCount: { type: "integer", minimum: 1 },
|
|
12431
|
+
runId: { type: "string", minLength: 1, maxLength: 256 },
|
|
12432
|
+
failureCode: { type: "string", pattern: "^[A-Z][A-Z0-9_]{0,79}$" },
|
|
12433
|
+
createdAt: { type: "integer", minimum: 0 },
|
|
12434
|
+
updatedAt: { type: "integer", minimum: 0 }
|
|
12435
|
+
}
|
|
12436
|
+
},
|
|
11871
12437
|
SessionAutomationRun: {
|
|
11872
12438
|
description: "Privacy-bounded run state with its exact execution binding; terminal transcript, instruction, and provider detail are omitted.",
|
|
11873
12439
|
type: "object",
|
|
@@ -13091,7 +13657,7 @@ function createPluginRuntime(options) {
|
|
|
13091
13657
|
}
|
|
13092
13658
|
|
|
13093
13659
|
// src/input-lease.ts
|
|
13094
|
-
import { randomUUID as
|
|
13660
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
13095
13661
|
var INPUT_LEASE_TTL_MS = 3e4;
|
|
13096
13662
|
function copy(lease) {
|
|
13097
13663
|
return { ...lease };
|
|
@@ -13118,7 +13684,7 @@ var InputLeaseCoordinator = class {
|
|
|
13118
13684
|
throw new Error("invalid input lease TTL");
|
|
13119
13685
|
}
|
|
13120
13686
|
this.now = options.now ?? Date.now;
|
|
13121
|
-
this.generateId = options.generateId ??
|
|
13687
|
+
this.generateId = options.generateId ?? randomUUID6;
|
|
13122
13688
|
this.scheduleExpiry = options.scheduleExpiry ?? true;
|
|
13123
13689
|
this.onEvent = options.onEvent;
|
|
13124
13690
|
}
|
|
@@ -14789,7 +15355,7 @@ async function verifyPeerConnection(input) {
|
|
|
14789
15355
|
}
|
|
14790
15356
|
|
|
14791
15357
|
// src/presence.ts
|
|
14792
|
-
import { randomUUID as
|
|
15358
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
14793
15359
|
var PRESENCE_TTL_MS = 45e3;
|
|
14794
15360
|
var PRESENCE_HEARTBEAT_MS = 15e3;
|
|
14795
15361
|
function clone6(record) {
|
|
@@ -14833,7 +15399,7 @@ var PresenceCoordinator = class {
|
|
|
14833
15399
|
throw new Error("invalid presence actor limit");
|
|
14834
15400
|
}
|
|
14835
15401
|
this.now = options.now ?? Date.now;
|
|
14836
|
-
this.generateId = options.generateId ??
|
|
15402
|
+
this.generateId = options.generateId ?? randomUUID7;
|
|
14837
15403
|
if (options.scheduleExpiry ?? true) {
|
|
14838
15404
|
this.timer = setInterval(() => this.sweep(), Math.min(15e3, Math.max(1e3, Math.floor(this.ttlMs / 2))));
|
|
14839
15405
|
this.timer.unref?.();
|
|
@@ -15199,9 +15765,9 @@ function cloudStatusResponse(status) {
|
|
|
15199
15765
|
};
|
|
15200
15766
|
}
|
|
15201
15767
|
const authorization = status.authorization;
|
|
15202
|
-
const syncFailures = status.authorizationFailures > 0 || status.heartbeatFailures > 0;
|
|
15768
|
+
const syncFailures = status.authorizationFailures > 0 || status.heartbeatFailures > 0 || status.automationFailures > 0;
|
|
15203
15769
|
const syncState = authorization.status === "expired" ? "expired" : authorization.status === "pending" ? "pending" : authorization.status === "unavailable" ? syncFailures ? "degraded" : "syncing" : syncFailures ? "degraded" : "healthy";
|
|
15204
|
-
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";
|
|
15770
|
+
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";
|
|
15205
15771
|
return {
|
|
15206
15772
|
v: 1,
|
|
15207
15773
|
mode: "managed",
|
|
@@ -15638,6 +16204,7 @@ function createServer(config, deps = {}) {
|
|
|
15638
16204
|
enabled: config.rateLimitRpm > 0
|
|
15639
16205
|
});
|
|
15640
16206
|
const pairingRateLimiter = new RateLimiter({ capacity: 30, windowMs: 6e4, burst: 10 });
|
|
16207
|
+
const automationWebhookRateLimiter = new RateLimiter({ capacity: 120, windowMs: 6e4, burst: 30 });
|
|
15641
16208
|
const fsService = new FsService({ root: config.fsRoot });
|
|
15642
16209
|
const terminalSharedRoot = terminalSharedBase({ dataDir, fsRoot: config.fsRoot });
|
|
15643
16210
|
const backfilledFileSessions = /* @__PURE__ */ new Set();
|
|
@@ -15657,7 +16224,7 @@ function createServer(config, deps = {}) {
|
|
|
15657
16224
|
const now = Date.now();
|
|
15658
16225
|
const media = attachmentMedia(file.filename);
|
|
15659
16226
|
store.putFile({
|
|
15660
|
-
id:
|
|
16227
|
+
id: randomUUID8(),
|
|
15661
16228
|
sessionId,
|
|
15662
16229
|
direction: "sent",
|
|
15663
16230
|
storage: "managed",
|
|
@@ -15729,6 +16296,7 @@ function createServer(config, deps = {}) {
|
|
|
15729
16296
|
}
|
|
15730
16297
|
const relayInternalCapability = randomBytes11(32).toString("base64url");
|
|
15731
16298
|
const authenticatedPrincipals = /* @__PURE__ */ new WeakMap();
|
|
16299
|
+
const automationWebhookRequests = /* @__PURE__ */ new WeakMap();
|
|
15732
16300
|
const hostPrincipal = () => ({
|
|
15733
16301
|
actorType: config.accessToken ? "host" : "local",
|
|
15734
16302
|
actorId: commandStore.getHost().id,
|
|
@@ -15875,6 +16443,28 @@ function createServer(config, deps = {}) {
|
|
|
15875
16443
|
if (request.is404 && isPublicPath(path)) return;
|
|
15876
16444
|
}
|
|
15877
16445
|
if (path === "/health") return;
|
|
16446
|
+
const automationHookMatch = /^\/api\/v2\/automation-hooks\/(rcwh_[A-Za-z0-9_-]{24,80})$/.exec(path);
|
|
16447
|
+
if (request.method === "POST" && automationHookMatch && !hasEncodedSep(request.url)) {
|
|
16448
|
+
const limit = automationWebhookRateLimiter.take(request.ip);
|
|
16449
|
+
if (!limit.allowed) {
|
|
16450
|
+
reply.header("retry-after", String(limit.retryAfterSeconds)).code(429).send({ error: "rate limited" });
|
|
16451
|
+
return;
|
|
16452
|
+
}
|
|
16453
|
+
const hookId = automationHookMatch[1];
|
|
16454
|
+
const match = sessionAutomationStore.list().flatMap((automation) => automation.triggers.map((trigger) => ({ automation, trigger }))).find(
|
|
16455
|
+
(entry) => entry.automation.enabled && entry.trigger.type === "webhook" && entry.trigger.enabled && entry.trigger.hookId === hookId
|
|
16456
|
+
);
|
|
16457
|
+
const secret = extractBearerToken(request.headers.authorization);
|
|
16458
|
+
const presented = secret ? createHash7("sha256").update(secret).digest() : Buffer.alloc(0);
|
|
16459
|
+
const expected = match?.trigger.type === "webhook" ? Buffer.from(match.trigger.secretHash, "hex") : Buffer.alloc(32);
|
|
16460
|
+
if (!match || presented.length !== expected.length || !timingSafeEqual4(presented, expected)) {
|
|
16461
|
+
reply.code(401).send({ error: "unauthorized" });
|
|
16462
|
+
return;
|
|
16463
|
+
}
|
|
16464
|
+
automationWebhookRequests.set(request, match);
|
|
16465
|
+
authenticatedPrincipals.set(request, hostPrincipal());
|
|
16466
|
+
return;
|
|
16467
|
+
}
|
|
15878
16468
|
if (request.method === "POST" && path === "/pairing/claim" && !hasEncodedSep(request.url)) {
|
|
15879
16469
|
const originAllowed2 = isOriginAllowed(request.headers.origin, request.headers.host, {
|
|
15880
16470
|
publicUrl: config.publicUrl,
|
|
@@ -16265,7 +16855,7 @@ function createServer(config, deps = {}) {
|
|
|
16265
16855
|
return;
|
|
16266
16856
|
}
|
|
16267
16857
|
const principal = authenticatedPrincipals.get(request) ?? hostPrincipal();
|
|
16268
|
-
const holderId = `ws:${
|
|
16858
|
+
const holderId = `ws:${randomUUID8()}`;
|
|
16269
16859
|
let leaseId;
|
|
16270
16860
|
let lastLeaseRevision = 0;
|
|
16271
16861
|
const sendLeaseState = (reason, revision) => {
|
|
@@ -16560,7 +17150,7 @@ function createServer(config, deps = {}) {
|
|
|
16560
17150
|
reply.code(400).send({ code: "INVALID_PROVIDER", error: "Invalid provider" });
|
|
16561
17151
|
return;
|
|
16562
17152
|
}
|
|
16563
|
-
const id = requestedSessionId ??
|
|
17153
|
+
const id = requestedSessionId ?? randomUUID8();
|
|
16564
17154
|
const existingMeta = requestedSessionId ? terminalManager.get(id) : void 0;
|
|
16565
17155
|
if (!terminalAvailable) {
|
|
16566
17156
|
reply.code(400).send({
|
|
@@ -17227,7 +17817,7 @@ function createServer(config, deps = {}) {
|
|
|
17227
17817
|
const peerIdempotencyKey = (request, peerId) => {
|
|
17228
17818
|
const actor = actorForRequest(request);
|
|
17229
17819
|
const provided = request.headers["idempotency-key"];
|
|
17230
|
-
const key = typeof provided === "string" ? provided :
|
|
17820
|
+
const key = typeof provided === "string" ? provided : randomUUID8();
|
|
17231
17821
|
return `peer-${createHash7("sha256").update(`${peerId}\0${actor.actorType}\0${actor.actorId}\0${key}`).digest("base64url")}`;
|
|
17232
17822
|
};
|
|
17233
17823
|
const workspaceAllowedByPeer = (peer, workspaceId) => typeof workspaceId === "string" && (peer.allowedWorkspaceIds === null || peer.allowedWorkspaceIds.includes(workspaceId));
|
|
@@ -19318,7 +19908,7 @@ data: ${JSON.stringify(data)}
|
|
|
19318
19908
|
});
|
|
19319
19909
|
return;
|
|
19320
19910
|
}
|
|
19321
|
-
holderId = `api-once:${
|
|
19911
|
+
holderId = `api-once:${randomUUID8()}`;
|
|
19322
19912
|
const acquired = inputLeases.acquire(request.params.id, holderId, principal);
|
|
19323
19913
|
if (acquired.status === "denied") {
|
|
19324
19914
|
reply.code(409).send({ code: "INPUT_LEASE_REQUIRED", error: "terminal input is already controlled" });
|
|
@@ -19472,7 +20062,7 @@ data: ${JSON.stringify(data)}
|
|
|
19472
20062
|
return;
|
|
19473
20063
|
}
|
|
19474
20064
|
const isImage = body.kind === "image" ? true : body.kind === "file" ? false : described.isImage;
|
|
19475
|
-
const id =
|
|
20065
|
+
const id = randomUUID8();
|
|
19476
20066
|
const now = Date.now();
|
|
19477
20067
|
const media = attachmentMedia(described.name);
|
|
19478
20068
|
const stored2 = {
|
|
@@ -19926,9 +20516,78 @@ data: ${JSON.stringify(data)}
|
|
|
19926
20516
|
const owner = currentNodeOwner();
|
|
19927
20517
|
return automation && automation.owner.type === owner.type && automation.owner.id === owner.id ? automation : void 0;
|
|
19928
20518
|
};
|
|
20519
|
+
const projectAutomationDefinition = (automation) => ({
|
|
20520
|
+
...automation,
|
|
20521
|
+
triggers: automation.triggers.map((trigger) => {
|
|
20522
|
+
if (trigger.type !== "webhook") return trigger;
|
|
20523
|
+
const { secretHash, ...publicTrigger } = trigger;
|
|
20524
|
+
void secretHash;
|
|
20525
|
+
return publicTrigger;
|
|
20526
|
+
})
|
|
20527
|
+
});
|
|
20528
|
+
const newTriggerId = () => `rct_${randomBytes11(12).toString("base64url")}`;
|
|
20529
|
+
const newWebhookHookId = () => `rcwh_${randomBytes11(24).toString("base64url")}`;
|
|
20530
|
+
const newWebhookSecret = () => `rcws_${randomBytes11(32).toString("base64url")}`;
|
|
20531
|
+
const prepareAutomationTriggers = (value, current) => {
|
|
20532
|
+
if (!Array.isArray(value) || value.length > 16) throw new Error("invalid automation triggers");
|
|
20533
|
+
const existing = new Map(current?.triggers.map((trigger) => [trigger.id, trigger]) ?? []);
|
|
20534
|
+
const ids = /* @__PURE__ */ new Set();
|
|
20535
|
+
const webhookSecrets = [];
|
|
20536
|
+
const triggers = value.map((candidate) => {
|
|
20537
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
20538
|
+
throw new Error("invalid automation trigger");
|
|
20539
|
+
}
|
|
20540
|
+
const raw = candidate;
|
|
20541
|
+
const requestedId = typeof raw.id === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(raw.id) ? raw.id : void 0;
|
|
20542
|
+
const id = requestedId ?? newTriggerId();
|
|
20543
|
+
if (ids.has(id)) throw new Error("duplicate automation trigger id");
|
|
20544
|
+
ids.add(id);
|
|
20545
|
+
if (typeof raw.enabled !== "boolean") throw new Error("invalid automation trigger state");
|
|
20546
|
+
if (raw.type === "schedule") {
|
|
20547
|
+
if (typeof raw.cron !== "string" || typeof raw.timeZone !== "string" || raw.timeZone.length > 80 || raw.missedRunPolicy !== "skip" || Object.keys(raw).some(
|
|
20548
|
+
(key) => !["id", "type", "enabled", "cron", "timeZone", "missedRunPolicy"].includes(key)
|
|
20549
|
+
)) {
|
|
20550
|
+
throw new Error("invalid schedule trigger");
|
|
20551
|
+
}
|
|
20552
|
+
new Intl.DateTimeFormat("en-US", { timeZone: raw.timeZone }).format(0);
|
|
20553
|
+
return {
|
|
20554
|
+
id,
|
|
20555
|
+
type: "schedule",
|
|
20556
|
+
enabled: raw.enabled,
|
|
20557
|
+
cron: validateCronExpression(raw.cron),
|
|
20558
|
+
timeZone: raw.timeZone,
|
|
20559
|
+
missedRunPolicy: "skip"
|
|
20560
|
+
};
|
|
20561
|
+
}
|
|
20562
|
+
if (raw.type === "webhook") {
|
|
20563
|
+
if (Object.keys(raw).some((key) => !["id", "type", "enabled", "hookId"].includes(key))) {
|
|
20564
|
+
throw new Error("invalid webhook trigger");
|
|
20565
|
+
}
|
|
20566
|
+
const previous = existing.get(id);
|
|
20567
|
+
if (previous?.type === "webhook") {
|
|
20568
|
+
if (raw.hookId !== void 0 && raw.hookId !== previous.hookId) {
|
|
20569
|
+
throw new Error("webhook identity cannot be replaced");
|
|
20570
|
+
}
|
|
20571
|
+
return { ...previous, enabled: raw.enabled };
|
|
20572
|
+
}
|
|
20573
|
+
const hookId = newWebhookHookId();
|
|
20574
|
+
const secret = newWebhookSecret();
|
|
20575
|
+
webhookSecrets.push({ triggerId: id, hookId, secret, path: `/api/v2/automation-hooks/${hookId}` });
|
|
20576
|
+
return {
|
|
20577
|
+
id,
|
|
20578
|
+
type: "webhook",
|
|
20579
|
+
enabled: raw.enabled,
|
|
20580
|
+
hookId,
|
|
20581
|
+
secretHash: createHash7("sha256").update(secret).digest("hex")
|
|
20582
|
+
};
|
|
20583
|
+
}
|
|
20584
|
+
throw new Error("invalid automation trigger");
|
|
20585
|
+
});
|
|
20586
|
+
return { triggers, webhookSecrets };
|
|
20587
|
+
};
|
|
19929
20588
|
const automationInvocationIdentity = (request, automationId) => {
|
|
19930
20589
|
const idempotency2 = mutationContexts.get(request)?.idempotency;
|
|
19931
|
-
if (!idempotency2) return { invocationId:
|
|
20590
|
+
if (!idempotency2) return { invocationId: randomUUID8(), sessionId: randomUUID8() };
|
|
19932
20591
|
const actor = actorForRequest(request);
|
|
19933
20592
|
const digest2 = createHash7("sha256").update("roamcode-automation-invocation-v1\0").update(JSON.stringify([actor.actorType, actor.actorId, idempotency2.key, idempotency2.fingerprint, automationId])).digest("hex");
|
|
19934
20593
|
const uuidHex = digest2.slice(0, 32).split("");
|
|
@@ -20197,10 +20856,13 @@ data: ${JSON.stringify(data)}
|
|
|
20197
20856
|
"cwd",
|
|
20198
20857
|
"instruction",
|
|
20199
20858
|
"runtimeOptions",
|
|
20200
|
-
"trigger"
|
|
20859
|
+
"trigger",
|
|
20860
|
+
"triggers"
|
|
20201
20861
|
]);
|
|
20202
20862
|
const automationUpdateKeys = /* @__PURE__ */ new Set([...automationCreateKeys, "expectedRevision"]);
|
|
20203
|
-
app.get("/api/v2/automations", async () => ({
|
|
20863
|
+
app.get("/api/v2/automations", async () => ({
|
|
20864
|
+
automations: sessionAutomationStore.list(currentNodeOwner()).map(projectAutomationDefinition)
|
|
20865
|
+
}));
|
|
20204
20866
|
app.post("/api/v2/automations", { bodyLimit: 128 * 1024 }, async (request, reply) => {
|
|
20205
20867
|
if (!validObjectBody(request.body)) return sendInvalidAutomation(reply);
|
|
20206
20868
|
if ("owner" in request.body || "provider" in request.body) {
|
|
@@ -20220,15 +20882,17 @@ data: ${JSON.stringify(data)}
|
|
|
20220
20882
|
);
|
|
20221
20883
|
if (!target) return;
|
|
20222
20884
|
try {
|
|
20885
|
+
const prepared = prepareAutomationTriggers(request.body.triggers ?? [], void 0);
|
|
20223
20886
|
const automation = sessionAutomationStore.create({
|
|
20224
20887
|
owner: currentNodeOwner(),
|
|
20225
20888
|
name: request.body.name,
|
|
20226
20889
|
...request.body.enabled === void 0 ? {} : { enabled: request.body.enabled },
|
|
20227
20890
|
...target,
|
|
20228
20891
|
instruction: request.body.instruction,
|
|
20229
|
-
...request.body.trigger === void 0 ? { trigger: { type: "manual" } } : { trigger: request.body.trigger }
|
|
20892
|
+
...request.body.trigger === void 0 ? { trigger: { type: "manual" } } : { trigger: request.body.trigger },
|
|
20893
|
+
triggers: prepared.triggers
|
|
20230
20894
|
});
|
|
20231
|
-
reply.code(201).send({ automation });
|
|
20895
|
+
reply.code(201).send({ automation: projectAutomationDefinition(automation), webhookSecrets: prepared.webhookSecrets });
|
|
20232
20896
|
} catch {
|
|
20233
20897
|
sendInvalidAutomation(reply);
|
|
20234
20898
|
}
|
|
@@ -20239,7 +20903,7 @@ data: ${JSON.stringify(data)}
|
|
|
20239
20903
|
reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
|
|
20240
20904
|
return;
|
|
20241
20905
|
}
|
|
20242
|
-
return { automation };
|
|
20906
|
+
return { automation: projectAutomationDefinition(automation) };
|
|
20243
20907
|
});
|
|
20244
20908
|
app.patch(
|
|
20245
20909
|
"/api/v2/automations/:automationId",
|
|
@@ -20277,18 +20941,20 @@ data: ${JSON.stringify(data)}
|
|
|
20277
20941
|
...target
|
|
20278
20942
|
};
|
|
20279
20943
|
try {
|
|
20944
|
+
const prepared = prepareAutomationTriggers(request.body.triggers ?? current.triggers, current);
|
|
20945
|
+
input.triggers = prepared.triggers;
|
|
20280
20946
|
const automation = sessionAutomationStore.update(current.id, input, request.body.expectedRevision);
|
|
20281
20947
|
if (!automation) {
|
|
20282
20948
|
reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
|
|
20283
20949
|
return;
|
|
20284
20950
|
}
|
|
20285
|
-
return { automation };
|
|
20951
|
+
return { automation: projectAutomationDefinition(automation), webhookSecrets: prepared.webhookSecrets };
|
|
20286
20952
|
} catch (error) {
|
|
20287
20953
|
if (error instanceof SessionAutomationRevisionConflictError) {
|
|
20288
20954
|
reply.code(409).send({
|
|
20289
20955
|
code: "SESSION_AUTOMATION_REVISION_CONFLICT",
|
|
20290
20956
|
error: "automation state changed",
|
|
20291
|
-
current: error.current
|
|
20957
|
+
current: projectAutomationDefinition(error.current)
|
|
20292
20958
|
});
|
|
20293
20959
|
return;
|
|
20294
20960
|
}
|
|
@@ -20303,6 +20969,70 @@ data: ${JSON.stringify(data)}
|
|
|
20303
20969
|
}
|
|
20304
20970
|
reply.code(204).send();
|
|
20305
20971
|
});
|
|
20972
|
+
app.get(
|
|
20973
|
+
"/api/v2/automations/:automationId/activity",
|
|
20974
|
+
async (request, reply) => {
|
|
20975
|
+
const automation = ownedAutomationIncludingRemoved(request.params.automationId);
|
|
20976
|
+
if (!automation) {
|
|
20977
|
+
reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
|
|
20978
|
+
return;
|
|
20979
|
+
}
|
|
20980
|
+
const limit = request.query.limit === void 0 ? 25 : Number(request.query.limit);
|
|
20981
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
20982
|
+
reply.code(400).send({ code: "INVALID_AUTOMATION_ACTIVITY_LIMIT", error: "limit must be between 1 and 100" });
|
|
20983
|
+
return;
|
|
20984
|
+
}
|
|
20985
|
+
return { activities: sessionAutomationStore.listActivities(automation.id, limit) };
|
|
20986
|
+
}
|
|
20987
|
+
);
|
|
20988
|
+
app.post(
|
|
20989
|
+
"/api/v2/automations/:automationId/triggers/:triggerId/secret",
|
|
20990
|
+
{ bodyLimit: 1024 },
|
|
20991
|
+
async (request, reply) => {
|
|
20992
|
+
const current = ownedAutomation(request.params.automationId);
|
|
20993
|
+
if (!current) {
|
|
20994
|
+
reply.code(404).send({ code: "SESSION_AUTOMATION_NOT_FOUND", error: "automation not found" });
|
|
20995
|
+
return;
|
|
20996
|
+
}
|
|
20997
|
+
if (!validObjectBody(request.body) || !hasOnlyKeys(request.body, /* @__PURE__ */ new Set(["expectedRevision"])) || !Number.isSafeInteger(request.body.expectedRevision) || request.body.expectedRevision !== current.revision) {
|
|
20998
|
+
return sendInvalidAutomation(reply, "a current expectedRevision is required");
|
|
20999
|
+
}
|
|
21000
|
+
const selected = current.triggers.find(
|
|
21001
|
+
(trigger) => trigger.id === request.params.triggerId && trigger.type === "webhook"
|
|
21002
|
+
);
|
|
21003
|
+
if (!selected || selected.type !== "webhook") {
|
|
21004
|
+
reply.code(404).send({ code: "AUTOMATION_TRIGGER_NOT_FOUND", error: "webhook trigger not found" });
|
|
21005
|
+
return;
|
|
21006
|
+
}
|
|
21007
|
+
const secret = newWebhookSecret();
|
|
21008
|
+
const triggers = current.triggers.map(
|
|
21009
|
+
(trigger) => trigger.id === selected.id ? { ...selected, secretHash: createHash7("sha256").update(secret).digest("hex") } : trigger
|
|
21010
|
+
);
|
|
21011
|
+
try {
|
|
21012
|
+
const automation = sessionAutomationStore.update(current.id, { triggers }, current.revision);
|
|
21013
|
+
if (!automation) throw new Error("automation disappeared");
|
|
21014
|
+
return {
|
|
21015
|
+
automation: projectAutomationDefinition(automation),
|
|
21016
|
+
webhookSecret: {
|
|
21017
|
+
triggerId: selected.id,
|
|
21018
|
+
hookId: selected.hookId,
|
|
21019
|
+
secret,
|
|
21020
|
+
path: `/api/v2/automation-hooks/${selected.hookId}`
|
|
21021
|
+
}
|
|
21022
|
+
};
|
|
21023
|
+
} catch (error) {
|
|
21024
|
+
if (error instanceof SessionAutomationRevisionConflictError) {
|
|
21025
|
+
reply.code(409).send({
|
|
21026
|
+
code: "SESSION_AUTOMATION_REVISION_CONFLICT",
|
|
21027
|
+
error: "automation state changed",
|
|
21028
|
+
current: projectAutomationDefinition(error.current)
|
|
21029
|
+
});
|
|
21030
|
+
return;
|
|
21031
|
+
}
|
|
21032
|
+
sendInvalidAutomation(reply);
|
|
21033
|
+
}
|
|
21034
|
+
}
|
|
21035
|
+
);
|
|
20306
21036
|
app.get(
|
|
20307
21037
|
"/api/v2/automations/:automationId/runs",
|
|
20308
21038
|
async (request, reply) => {
|
|
@@ -20531,6 +21261,46 @@ data: ${JSON.stringify(data)}
|
|
|
20531
21261
|
);
|
|
20532
21262
|
}
|
|
20533
21263
|
);
|
|
21264
|
+
const parsedAutomationConcurrency = Number.parseInt(process.env.ROAMCODE_AUTOMATION_CONCURRENCY ?? "2", 10);
|
|
21265
|
+
const automationTriggerEngine = createAutomationTriggerEngine({
|
|
21266
|
+
store: sessionAutomationStore,
|
|
21267
|
+
concurrency: Number.isSafeInteger(parsedAutomationConcurrency) && parsedAutomationConcurrency > 0 ? parsedAutomationConcurrency : 2,
|
|
21268
|
+
execute: async (activity) => {
|
|
21269
|
+
const response2 = await app.inject({
|
|
21270
|
+
method: "POST",
|
|
21271
|
+
url: `/api/v2/automations/${encodeURIComponent(activity.automationId)}/runs`,
|
|
21272
|
+
headers: {
|
|
21273
|
+
"idempotency-key": activity.id,
|
|
21274
|
+
...config.accessToken ? { authorization: `Bearer ${config.accessToken}` } : {}
|
|
21275
|
+
}
|
|
21276
|
+
});
|
|
21277
|
+
const body = (() => {
|
|
21278
|
+
try {
|
|
21279
|
+
return response2.json();
|
|
21280
|
+
} catch {
|
|
21281
|
+
return {};
|
|
21282
|
+
}
|
|
21283
|
+
})();
|
|
21284
|
+
if (response2.statusCode !== 201 || typeof body.run?.id !== "string") {
|
|
21285
|
+
throw new Error("automation trigger execution failed");
|
|
21286
|
+
}
|
|
21287
|
+
return { runId: body.run.id };
|
|
21288
|
+
}
|
|
21289
|
+
});
|
|
21290
|
+
app.post(
|
|
21291
|
+
"/api/v2/automation-hooks/:hookId",
|
|
21292
|
+
{ bodyLimit: 64 * 1024 },
|
|
21293
|
+
async (request, reply) => {
|
|
21294
|
+
const authorized = automationWebhookRequests.get(request);
|
|
21295
|
+
if (!authorized || authorized.trigger.type !== "webhook" || authorized.trigger.hookId !== request.params.hookId) {
|
|
21296
|
+
reply.code(401).send({ error: "unauthorized" });
|
|
21297
|
+
return;
|
|
21298
|
+
}
|
|
21299
|
+
automationTriggerEngine.enqueueWebhook(authorized.automation, authorized.trigger);
|
|
21300
|
+
reply.code(202).send({ accepted: true });
|
|
21301
|
+
}
|
|
21302
|
+
);
|
|
21303
|
+
app.addHook("onReady", async () => automationTriggerEngine.start());
|
|
20534
21304
|
app.get("/providers", async () => {
|
|
20535
21305
|
return { providers: await readProviderAvailability() };
|
|
20536
21306
|
});
|
|
@@ -20889,7 +21659,7 @@ data: ${JSON.stringify(data)}
|
|
|
20889
21659
|
reply.code(400).send({ error: "no file field in the upload" });
|
|
20890
21660
|
return;
|
|
20891
21661
|
}
|
|
20892
|
-
const id =
|
|
21662
|
+
const id = randomUUID8();
|
|
20893
21663
|
try {
|
|
20894
21664
|
const dir = await fsService.ensureDirWithinRoot(
|
|
20895
21665
|
`${terminalSharedDir({ dataDir, fsRoot: config.fsRoot, sessionId })}/${id}`
|
|
@@ -20941,7 +21711,7 @@ data: ${JSON.stringify(data)}
|
|
|
20941
21711
|
reply.code(400).send({ error: "an edited image is required" });
|
|
20942
21712
|
return;
|
|
20943
21713
|
}
|
|
20944
|
-
const id =
|
|
21714
|
+
const id = randomUUID8();
|
|
20945
21715
|
try {
|
|
20946
21716
|
const dir = await fsService.ensureDirWithinRoot(
|
|
20947
21717
|
`${terminalSharedDir({ dataDir, fsRoot: config.fsRoot, sessionId: source.sessionId })}/${id}`
|
|
@@ -21067,6 +21837,7 @@ data: ${JSON.stringify(data)}
|
|
|
21067
21837
|
);
|
|
21068
21838
|
if (deps.webDir) registerStatic(app, { webDir: deps.webDir });
|
|
21069
21839
|
app.addHook("onClose", async () => {
|
|
21840
|
+
automationTriggerEngine.stop();
|
|
21070
21841
|
clearInterval(sharedSweepTimer);
|
|
21071
21842
|
inputLeases.close();
|
|
21072
21843
|
unsubscribeAutomations();
|
|
@@ -21162,6 +21933,25 @@ data: ${JSON.stringify(data)}
|
|
|
21162
21933
|
authGate,
|
|
21163
21934
|
terminalManager,
|
|
21164
21935
|
terminalAvailable,
|
|
21936
|
+
automationWebhookRegistrations: () => sessionAutomationStore.list().flatMap(
|
|
21937
|
+
(automation) => automation.triggers.filter((trigger) => trigger.type === "webhook").map((trigger) => ({
|
|
21938
|
+
hookId: trigger.hookId,
|
|
21939
|
+
automationId: automation.id,
|
|
21940
|
+
triggerId: trigger.id,
|
|
21941
|
+
secretHash: trigger.secretHash,
|
|
21942
|
+
enabled: automation.enabled && trigger.enabled
|
|
21943
|
+
}))
|
|
21944
|
+
),
|
|
21945
|
+
acceptCloudAutomationInvocation: async (invocation) => {
|
|
21946
|
+
const automation = sessionAutomationStore.get(invocation.automationId);
|
|
21947
|
+
const trigger = automation?.triggers.find(
|
|
21948
|
+
(candidate) => candidate.type === "webhook" && candidate.id === invocation.triggerId && candidate.hookId === invocation.hookId
|
|
21949
|
+
);
|
|
21950
|
+
if (!automation?.enabled || !trigger?.enabled || trigger.type !== "webhook") {
|
|
21951
|
+
throw new Error("managed automation webhook is no longer active");
|
|
21952
|
+
}
|
|
21953
|
+
automationTriggerEngine.enqueueWebhook(automation, trigger, invocation.id);
|
|
21954
|
+
},
|
|
21165
21955
|
inputLeases,
|
|
21166
21956
|
teamStore,
|
|
21167
21957
|
policyStore,
|
|
@@ -21581,7 +22371,7 @@ function createUsageService(opts) {
|
|
|
21581
22371
|
|
|
21582
22372
|
// src/claude-auth-service.ts
|
|
21583
22373
|
import { spawn as nodeSpawn2 } from "child_process";
|
|
21584
|
-
import { randomUUID as
|
|
22374
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
21585
22375
|
function parseAuthStatus(stdout) {
|
|
21586
22376
|
try {
|
|
21587
22377
|
const o = JSON.parse(stdout);
|
|
@@ -21658,7 +22448,7 @@ var ClaudeAuthService = class {
|
|
|
21658
22448
|
reject(err instanceof Error ? err : new Error("failed to spawn claude"));
|
|
21659
22449
|
return;
|
|
21660
22450
|
}
|
|
21661
|
-
const id =
|
|
22451
|
+
const id = randomUUID9();
|
|
21662
22452
|
let settled = false;
|
|
21663
22453
|
let buf = "";
|
|
21664
22454
|
const onData = (d) => {
|
|
@@ -23020,7 +23810,7 @@ var CodexMetadataService = class {
|
|
|
23020
23810
|
|
|
23021
23811
|
// src/providers/claude-metadata-service.ts
|
|
23022
23812
|
import { execFile as nodeExecFile2, spawn as nodeSpawn3 } from "child_process";
|
|
23023
|
-
import { randomUUID as
|
|
23813
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
23024
23814
|
import { StringDecoder } from "string_decoder";
|
|
23025
23815
|
var SAFE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._:/\u005b\u005d-]*$/;
|
|
23026
23816
|
var MAX_MODELS = 64;
|
|
@@ -23228,7 +24018,7 @@ function createClaudeMetadataRunner(options) {
|
|
|
23228
24018
|
reject(metadataUnavailable2());
|
|
23229
24019
|
return;
|
|
23230
24020
|
}
|
|
23231
|
-
const requestId = `roamcode-models-${
|
|
24021
|
+
const requestId = `roamcode-models-${randomUUID10()}`;
|
|
23232
24022
|
const decoder = new StringDecoder("utf8");
|
|
23233
24023
|
let stdoutBuffer = "";
|
|
23234
24024
|
let outputBytes = 0;
|
|
@@ -23480,7 +24270,7 @@ var CodexLatestService = class {
|
|
|
23480
24270
|
import { execFile } from "child_process";
|
|
23481
24271
|
import { constants as constants5 } from "fs";
|
|
23482
24272
|
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";
|
|
23483
|
-
import { randomUUID as
|
|
24273
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
23484
24274
|
import { basename as basename5, delimiter, dirname as dirname8, isAbsolute as isAbsolute9, join as join14, sep as sep5 } from "path";
|
|
23485
24275
|
var CODEX_EXECUTABLE_PROBE_TIMEOUT_MS = 5e3;
|
|
23486
24276
|
var OPENAI_CODE_SIGNING_TEAM_ID = "2DC432GLL2";
|
|
@@ -23590,7 +24380,7 @@ async function removeLegacyManagedCopy(dataDir) {
|
|
|
23590
24380
|
}
|
|
23591
24381
|
async function repairHomebrewExecutable(source, sourceStat, env, deps) {
|
|
23592
24382
|
if (!await deps.verifyOfficialSignature(source)) return false;
|
|
23593
|
-
const nonce =
|
|
24383
|
+
const nonce = randomUUID11();
|
|
23594
24384
|
const directory = dirname8(source);
|
|
23595
24385
|
const name = basename5(source);
|
|
23596
24386
|
const temporary = join14(directory, `.${name}.roamcode-repair-${nonce}`);
|
|
@@ -24821,6 +25611,32 @@ var CloudHostHeartbeatV1Schema = z9.object({
|
|
|
24821
25611
|
context.addIssue({ code: "custom", path: ["capabilities"], message: "capabilities must be unique" });
|
|
24822
25612
|
}
|
|
24823
25613
|
});
|
|
25614
|
+
var CloudAutomationWebhookRegistrationSchema = z9.object({
|
|
25615
|
+
hookId: z9.string().regex(/^rcwh_[A-Za-z0-9_-]{24,80}$/),
|
|
25616
|
+
automationId: SafeIdentifierSchema,
|
|
25617
|
+
triggerId: SafeIdentifierSchema,
|
|
25618
|
+
secretHash: z9.string().regex(/^[a-f0-9]{64}$/),
|
|
25619
|
+
enabled: z9.boolean()
|
|
25620
|
+
}).strict();
|
|
25621
|
+
var CloudAutomationInvocationSchema = z9.object({
|
|
25622
|
+
id: z9.uuid(),
|
|
25623
|
+
automationId: SafeIdentifierSchema,
|
|
25624
|
+
triggerId: SafeIdentifierSchema,
|
|
25625
|
+
hookId: z9.string().regex(/^rcwh_[A-Za-z0-9_-]{24,80}$/),
|
|
25626
|
+
createdAt: TimestampSchema
|
|
25627
|
+
}).strict();
|
|
25628
|
+
var CloudAutomationSyncRequestSchema = z9.object({
|
|
25629
|
+
v: z9.literal(1),
|
|
25630
|
+
organizationId: SafeIdentifierSchema,
|
|
25631
|
+
hostId: SafeIdentifierSchema,
|
|
25632
|
+
instanceId: SafeIdentifierSchema,
|
|
25633
|
+
registrations: z9.array(CloudAutomationWebhookRegistrationSchema).max(128).refine(
|
|
25634
|
+
(registrations) => new Set(registrations.map((registration) => registration.hookId)).size === registrations.length,
|
|
25635
|
+
"webhook identities must be unique"
|
|
25636
|
+
),
|
|
25637
|
+
acknowledgements: z9.array(z9.uuid()).max(200).refine((values) => new Set(values).size === values.length)
|
|
25638
|
+
}).strict();
|
|
25639
|
+
var CloudAutomationSyncResponseSchema = z9.object({ invocations: z9.array(CloudAutomationInvocationSchema).max(50) }).strict();
|
|
24824
25640
|
var CloudAuthorizationScopeV1Schema = z9.discriminatedUnion("type", [
|
|
24825
25641
|
z9.object({ type: z9.literal("organization") }).strict(),
|
|
24826
25642
|
z9.object({ type: z9.literal("host"), id: SafeIdentifierSchema }).strict(),
|
|
@@ -25859,6 +26675,7 @@ function openCloudAuthorizationStore(options) {
|
|
|
25859
26675
|
|
|
25860
26676
|
// src/cloud-host-runtime.ts
|
|
25861
26677
|
var CLOUD_HOST_HEARTBEAT_PATH = "/api/v1/hosts/heartbeat";
|
|
26678
|
+
var CLOUD_HOST_AUTOMATION_SYNC_PATH = "/api/v1/hosts/automation-sync";
|
|
25862
26679
|
var CLOUD_HOST_AUTHORIZATION_SNAPSHOT_PATH = "/api/v1/hosts/authorization-snapshot";
|
|
25863
26680
|
var CLOUD_HOST_MAX_SIGNED_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
25864
26681
|
var DEFAULT_REQUEST_TIMEOUT_MS3 = 15e3;
|
|
@@ -25945,6 +26762,7 @@ function createCloudHostRuntime(options) {
|
|
|
25945
26762
|
let lastHeartbeatAt;
|
|
25946
26763
|
let lastAuthorizationAt;
|
|
25947
26764
|
let heartbeatFailures = 0;
|
|
26765
|
+
let automationFailures = 0;
|
|
25948
26766
|
let authorizationFailures = 0;
|
|
25949
26767
|
let lastAuthorizationIssue;
|
|
25950
26768
|
let running = false;
|
|
@@ -25954,6 +26772,8 @@ function createCloudHostRuntime(options) {
|
|
|
25954
26772
|
let heartbeatInFlight;
|
|
25955
26773
|
let authorizationInFlight;
|
|
25956
26774
|
const capabilities2 = () => [...new Set(typeof options.capabilities === "function" ? options.capabilities() : options.capabilities)].sort();
|
|
26775
|
+
const automationWebhooks = () => (typeof options.automationWebhooks === "function" ? options.automationWebhooks() : options.automationWebhooks ?? []).map((registration) => CloudAutomationWebhookRegistrationSchema.parse(registration)).sort((left, right) => left.hookId.localeCompare(right.hookId));
|
|
26776
|
+
const automationAcknowledgements = /* @__PURE__ */ new Set();
|
|
25957
26777
|
const request = async (path, init) => {
|
|
25958
26778
|
try {
|
|
25959
26779
|
return await fetchImpl(`${config.controlPlaneOrigin}${path}`, {
|
|
@@ -26073,6 +26893,51 @@ function createCloudHostRuntime(options) {
|
|
|
26073
26893
|
}
|
|
26074
26894
|
await response2.body?.cancel().catch(() => void 0);
|
|
26075
26895
|
lastHeartbeatAt = currentNow;
|
|
26896
|
+
if (state === "ready" && options.automationWebhooks && options.onAutomationInvocation) {
|
|
26897
|
+
try {
|
|
26898
|
+
let deliveryFailed = false;
|
|
26899
|
+
for (let page = 0; page < 4; page += 1) {
|
|
26900
|
+
const acknowledgements = [...automationAcknowledgements].slice(0, 200);
|
|
26901
|
+
const payload = CloudAutomationSyncRequestSchema.parse({
|
|
26902
|
+
v: 1,
|
|
26903
|
+
organizationId: config.organizationId,
|
|
26904
|
+
hostId: config.hostId,
|
|
26905
|
+
instanceId: options.instanceId,
|
|
26906
|
+
registrations: automationWebhooks(),
|
|
26907
|
+
acknowledgements
|
|
26908
|
+
});
|
|
26909
|
+
const automationResponse = await request(CLOUD_HOST_AUTOMATION_SYNC_PATH, {
|
|
26910
|
+
method: "POST",
|
|
26911
|
+
headers: authenticatedHeaders({ "content-type": "application/json", accept: "application/json" }),
|
|
26912
|
+
body: JSON.stringify(payload)
|
|
26913
|
+
});
|
|
26914
|
+
if (automationResponse.status === 404) {
|
|
26915
|
+
await automationResponse.body?.cancel().catch(() => void 0);
|
|
26916
|
+
break;
|
|
26917
|
+
}
|
|
26918
|
+
if (automationResponse.status !== 200) {
|
|
26919
|
+
await boundedResponseText2(automationResponse).catch(() => "");
|
|
26920
|
+
throw responseError(automationResponse);
|
|
26921
|
+
}
|
|
26922
|
+
const parsed = CloudAutomationSyncResponseSchema.parse(
|
|
26923
|
+
JSON.parse(await boundedResponseText2(automationResponse))
|
|
26924
|
+
);
|
|
26925
|
+
for (const id of acknowledgements) automationAcknowledgements.delete(id);
|
|
26926
|
+
for (const invocation of parsed.invocations) {
|
|
26927
|
+
try {
|
|
26928
|
+
await options.onAutomationInvocation(invocation);
|
|
26929
|
+
automationAcknowledgements.add(invocation.id);
|
|
26930
|
+
} catch {
|
|
26931
|
+
deliveryFailed = true;
|
|
26932
|
+
}
|
|
26933
|
+
}
|
|
26934
|
+
if (parsed.invocations.length === 0 && automationAcknowledgements.size === 0) break;
|
|
26935
|
+
}
|
|
26936
|
+
automationFailures = deliveryFailed ? automationFailures + 1 : 0;
|
|
26937
|
+
} catch {
|
|
26938
|
+
automationFailures += 1;
|
|
26939
|
+
}
|
|
26940
|
+
}
|
|
26076
26941
|
};
|
|
26077
26942
|
const jittered = (baseMs) => {
|
|
26078
26943
|
const sample = random();
|
|
@@ -26137,6 +27002,7 @@ function createCloudHostRuntime(options) {
|
|
|
26137
27002
|
status: () => ({
|
|
26138
27003
|
running,
|
|
26139
27004
|
heartbeatFailures,
|
|
27005
|
+
automationFailures,
|
|
26140
27006
|
authorizationFailures,
|
|
26141
27007
|
...lastAuthorizationIssue ? { authorizationIssue: lastAuthorizationIssue } : {},
|
|
26142
27008
|
...lastHeartbeatAt === void 0 ? {} : { lastHeartbeatAt },
|
|
@@ -26594,13 +27460,14 @@ function cloudHostCapabilities(input) {
|
|
|
26594
27460
|
return [
|
|
26595
27461
|
`authorization.v${input.authorizationVersion}`,
|
|
26596
27462
|
...input.terminalAvailable ? ["terminal.v1"] : [],
|
|
27463
|
+
...input.terminalAvailable ? ["automation-schedule.v1", "automation-webhook.v1"] : [],
|
|
26597
27464
|
...input.relayEnabled ? ["relay.v1"] : [],
|
|
26598
27465
|
...input.managedDeviceEnrollmentEnabled && input.relayEnabled && input.terminalAvailable ? ["managed-device-enrollment.v1"] : []
|
|
26599
27466
|
];
|
|
26600
27467
|
}
|
|
26601
27468
|
async function startServer(env = process.env, options = {}) {
|
|
26602
27469
|
const config = loadServerConfig(env);
|
|
26603
|
-
const healthInstanceId =
|
|
27470
|
+
const healthInstanceId = randomUUID12();
|
|
26604
27471
|
ensureDataDir(config.dataDir);
|
|
26605
27472
|
const loopback = isLoopbackAddress(config.bindAddress);
|
|
26606
27473
|
let token;
|
|
@@ -26873,6 +27740,7 @@ async function startServer(env = process.env, options = {}) {
|
|
|
26873
27740
|
cloudStatus: () => cloudHostRuntime?.status() ?? {
|
|
26874
27741
|
running: false,
|
|
26875
27742
|
heartbeatFailures: 0,
|
|
27743
|
+
automationFailures: 0,
|
|
26876
27744
|
authorizationFailures: 0,
|
|
26877
27745
|
authorization: cloudAuthorizationStore.getState()
|
|
26878
27746
|
}
|
|
@@ -26905,6 +27773,8 @@ async function startServer(env = process.env, options = {}) {
|
|
|
26905
27773
|
relayEnabled: relayConfig !== void 0,
|
|
26906
27774
|
managedDeviceEnrollmentEnabled: cloudDeviceEnrollmentConfirmer !== void 0 && relayProvisioner !== void 0
|
|
26907
27775
|
}),
|
|
27776
|
+
automationWebhooks: () => result.automationWebhookRegistrations(),
|
|
27777
|
+
onAutomationInvocation: (invocation) => result.acceptCloudAutomationInvocation(invocation),
|
|
26908
27778
|
...relayConfig ? {
|
|
26909
27779
|
relayHostIdentity: {
|
|
26910
27780
|
publicKey: relayConfig.hostIdentity.publicKey,
|