@hachej/boring-automation 0.1.79 → 0.1.81
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/README.md +1 -1
- package/dist/front/index.js +3 -1
- package/dist/server/index.d.ts +58 -27
- package/dist/server/index.js +332 -161
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +3 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -15,4 +15,4 @@ Executable model values use explicit `provider:model-id` syntax. Legacy unqualif
|
|
|
15
15
|
|
|
16
16
|
Execution is currently composed for trusted local CLI folder mode. Scheduling has no background timer: user-owned cron/systemd may invoke `POST /api/v1/boring-automation/due` once per minute while the CLI server is running. Missed minutes are not backfilled.
|
|
17
17
|
|
|
18
|
-
Hosted persistence
|
|
18
|
+
Hosted persistence and creator-scoped execution are available in full-app. The deployment migration callback is `runBoringAutomationMigrations`; configure `BORING_AUTOMATION_TRIGGER_TOKEN` and have the platform scheduler invoke `POST /api/v1/boring-automation/due/hosted` with `Authorization: Bearer <token>`. The endpoint re-checks each creator and fails closed when authorization is lost. It has no hidden timer.
|
package/dist/front/index.js
CHANGED
|
@@ -21,7 +21,9 @@ var BORING_AUTOMATION_ERROR_CODES = {
|
|
|
21
21
|
RUN_ALREADY_RECORDED: "BORING_AUTOMATION_RUN_ALREADY_RECORDED",
|
|
22
22
|
RUN_EXECUTOR_UNAVAILABLE: "BORING_AUTOMATION_RUN_EXECUTOR_UNAVAILABLE",
|
|
23
23
|
TRIGGER_FORBIDDEN: "BORING_AUTOMATION_TRIGGER_FORBIDDEN",
|
|
24
|
-
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED"
|
|
24
|
+
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED",
|
|
25
|
+
OWNER_UNAUTHORIZED: "BORING_AUTOMATION_OWNER_UNAUTHORIZED",
|
|
26
|
+
TRIGGER_UNAUTHORIZED: "BORING_AUTOMATION_TRIGGER_UNAUTHORIZED"
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
// src/shared/schema.ts
|
package/dist/server/index.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { WorkspaceAgentDispatcherResolver } from '@hachej/boring-agent/server';
|
|
|
2
2
|
import { FastifyRequest, FastifyInstance } from 'fastify';
|
|
3
3
|
import { WorkspaceAgentServerPluginContext } from '@hachej/boring-workspace/app/server';
|
|
4
4
|
import { WorkspaceServerPlugin } from '@hachej/boring-workspace/server';
|
|
5
|
+
import postgres from 'postgres';
|
|
5
6
|
import { Automation, AutomationCreate, AutomationPatch, AutomationRunBegin, AutomationRun, AutomationRunLifecyclePatch, BoringAutomationErrorCode, AutomationScheduleDecision } from '../shared/index.js';
|
|
6
7
|
export { AUTOMATION_SCHEDULE_ERRORS, AutomationCreateInput, AutomationCreateSchema, AutomationPatchInput, AutomationPatchSchema, AutomationRunBeginInput, AutomationRunBeginSchema, AutomationRunLifecyclePatchInput, AutomationRunLifecyclePatchSchema, AutomationRunStatus, AutomationRunStatusSchema, AutomationRunTrigger, AutomationRunTriggerSchema, AutomationScheduleDueDecision, AutomationScheduleDueReason, AutomationScheduleSkipDecision, AutomationScheduleSkipReason, AutomationScheduleValidationResult, BORING_AUTOMATION_ERROR_CODES, BORING_AUTOMATION_PLUGIN_ID, BORING_AUTOMATION_PLUGIN_LABEL, BORING_AUTOMATION_ROUTE_PREFIX, EvaluateAutomationScheduleInput, EvaluateAutomationScheduleResult, IdParamsSchema, PromptUpdateSchema, evaluateAutomationSchedule, isValidFiveFieldCron, isValidIanaTimeZone, validateAutomationSchedule } from '../shared/index.js';
|
|
7
|
-
import postgres from 'postgres';
|
|
8
8
|
import 'zod';
|
|
9
9
|
|
|
10
10
|
/** Plugin-local dependency injection seam for the single-workspace automation store. */
|
|
@@ -46,6 +46,7 @@ interface ManualRunInput {
|
|
|
46
46
|
request: FastifyRequest;
|
|
47
47
|
trigger?: "manual" | "scheduled";
|
|
48
48
|
scheduledFor?: string | null;
|
|
49
|
+
actor?: VerifiedAutomationActor;
|
|
49
50
|
}
|
|
50
51
|
declare class ManualRunExecutor {
|
|
51
52
|
private readonly options;
|
|
@@ -97,6 +98,55 @@ declare class DueRunService {
|
|
|
97
98
|
runDue(request: FastifyRequest): Promise<DueRunResult>;
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
interface HostedAutomationActor {
|
|
102
|
+
workspaceId: string;
|
|
103
|
+
userId: string;
|
|
104
|
+
}
|
|
105
|
+
interface HostedAutomationCandidate {
|
|
106
|
+
automation: Automation;
|
|
107
|
+
actor: HostedAutomationActor;
|
|
108
|
+
runs: AutomationRun[];
|
|
109
|
+
}
|
|
110
|
+
type Sql = postgres.Sql;
|
|
111
|
+
/** Hosted store bound to a verified workspace/user pair; every query is scoped by both. */
|
|
112
|
+
declare class PostgresAutomationStore implements AutomationStore {
|
|
113
|
+
private readonly sql;
|
|
114
|
+
private readonly actor;
|
|
115
|
+
private readonly clock;
|
|
116
|
+
constructor(sql: Sql, actor: HostedAutomationActor, clock?: () => Date);
|
|
117
|
+
listAutomations(): Promise<Automation[]>;
|
|
118
|
+
getAutomation(id: string): Promise<Automation | null>;
|
|
119
|
+
createAutomation(input: AutomationCreate): Promise<Automation>;
|
|
120
|
+
updateAutomation(id: string, patch: AutomationPatch): Promise<Automation>;
|
|
121
|
+
deleteAutomation(id: string): Promise<void>;
|
|
122
|
+
getPrompt(automationId: string): Promise<string>;
|
|
123
|
+
updatePrompt(automationId: string, body: string): Promise<void>;
|
|
124
|
+
reconcileOrphanedRuns(automationId: string): Promise<void>;
|
|
125
|
+
beginRun(input: AutomationRunBegin): Promise<AutomationRun>;
|
|
126
|
+
updateRunLifecycle(runId: string, patch: AutomationRunLifecyclePatch): Promise<AutomationRun>;
|
|
127
|
+
listRuns(automationId: string): Promise<AutomationRun[]>;
|
|
128
|
+
private findRun;
|
|
129
|
+
}
|
|
130
|
+
declare function listHostedAutomationCandidates(sql: Sql): Promise<HostedAutomationCandidate[]>;
|
|
131
|
+
|
|
132
|
+
interface HostedDueRunServiceOptions {
|
|
133
|
+
sql: postgres.Sql;
|
|
134
|
+
dispatcherResolver: WorkspaceAgentDispatcherResolver;
|
|
135
|
+
verifyActor: (actor: HostedAutomationActor) => Promise<boolean> | boolean;
|
|
136
|
+
clock?: () => Date;
|
|
137
|
+
}
|
|
138
|
+
interface HostedDueRunResult {
|
|
139
|
+
now: string;
|
|
140
|
+
outcomes: DueRunOutcome[];
|
|
141
|
+
}
|
|
142
|
+
/** Runs due work for every creator while preserving creator-scoped execution. */
|
|
143
|
+
declare class HostedDueRunService {
|
|
144
|
+
private readonly options;
|
|
145
|
+
private readonly clock;
|
|
146
|
+
constructor(options: HostedDueRunServiceOptions);
|
|
147
|
+
runDue(request: FastifyRequest): Promise<HostedDueRunResult>;
|
|
148
|
+
}
|
|
149
|
+
|
|
100
150
|
type AtomicWriter = (path: string, content: string) => Promise<void>;
|
|
101
151
|
interface FileAutomationStoreOptions {
|
|
102
152
|
writer?: AtomicWriter;
|
|
@@ -134,36 +184,13 @@ declare class FileAutomationStore implements AutomationStore {
|
|
|
134
184
|
/** Deployment-owned hosted schema registration for the automation plugin. */
|
|
135
185
|
declare function runBoringAutomationMigrations(sql: postgres.Sql): Promise<void>;
|
|
136
186
|
|
|
137
|
-
interface HostedAutomationActor {
|
|
138
|
-
workspaceId: string;
|
|
139
|
-
userId: string;
|
|
140
|
-
}
|
|
141
|
-
type Sql = postgres.Sql;
|
|
142
|
-
/** Hosted store bound to a verified workspace/user pair; every query is scoped by both. */
|
|
143
|
-
declare class PostgresAutomationStore implements AutomationStore {
|
|
144
|
-
private readonly sql;
|
|
145
|
-
private readonly actor;
|
|
146
|
-
private readonly clock;
|
|
147
|
-
constructor(sql: Sql, actor: HostedAutomationActor, clock?: () => Date);
|
|
148
|
-
listAutomations(): Promise<Automation[]>;
|
|
149
|
-
getAutomation(id: string): Promise<Automation | null>;
|
|
150
|
-
createAutomation(input: AutomationCreate): Promise<Automation>;
|
|
151
|
-
updateAutomation(id: string, patch: AutomationPatch): Promise<Automation>;
|
|
152
|
-
deleteAutomation(id: string): Promise<void>;
|
|
153
|
-
getPrompt(automationId: string): Promise<string>;
|
|
154
|
-
updatePrompt(automationId: string, body: string): Promise<void>;
|
|
155
|
-
reconcileOrphanedRuns(automationId: string): Promise<void>;
|
|
156
|
-
beginRun(input: AutomationRunBegin): Promise<AutomationRun>;
|
|
157
|
-
updateRunLifecycle(runId: string, patch: AutomationRunLifecyclePatch): Promise<AutomationRun>;
|
|
158
|
-
listRuns(automationId: string): Promise<AutomationRun[]>;
|
|
159
|
-
private findRun;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
187
|
interface AutomationRoutesOptions {
|
|
163
188
|
store: AutomationStore;
|
|
164
189
|
storeForRequest?: (request: FastifyRequest) => Promise<AutomationStore> | AutomationStore;
|
|
165
190
|
manualRunExecutor?: Pick<ManualRunExecutor, "run">;
|
|
166
191
|
dueRunService?: Pick<DueRunService, "runDue">;
|
|
192
|
+
hostedDueRunService?: Pick<HostedDueRunService, "runDue">;
|
|
193
|
+
hostedTriggerToken?: string;
|
|
167
194
|
}
|
|
168
195
|
declare function automationRoutes(app: FastifyInstance, opts: AutomationRoutesOptions): Promise<void>;
|
|
169
196
|
|
|
@@ -172,8 +199,12 @@ interface BoringAutomationServerPluginOptions {
|
|
|
172
199
|
store?: AutomationStore;
|
|
173
200
|
dispatcherResolver?: WorkspaceAgentDispatcherResolver;
|
|
174
201
|
actorResolver?: (request: FastifyRequest) => Promise<VerifiedAutomationActor> | VerifiedAutomationActor;
|
|
202
|
+
storeForRequest?: (request: FastifyRequest, actor: VerifiedAutomationActor) => Promise<AutomationStore> | AutomationStore;
|
|
203
|
+
actorVerifier?: (actor: VerifiedAutomationActor) => Promise<boolean> | boolean;
|
|
204
|
+
hostedTriggerToken?: string;
|
|
205
|
+
hostedDueRunService?: Pick<HostedDueRunService, "runDue">;
|
|
175
206
|
}
|
|
176
207
|
declare function createBoringAutomationServerPlugin(options?: BoringAutomationServerPluginOptions): WorkspaceServerPlugin;
|
|
177
208
|
declare function defaultBoringAutomationServerPlugin(options?: BoringAutomationServerPluginOptions, ctx?: Pick<WorkspaceAgentServerPluginContext, "workspaceRoot"> & Partial<Pick<WorkspaceAgentServerPluginContext, "trusted">>): WorkspaceServerPlugin;
|
|
178
209
|
|
|
179
|
-
export { Automation, AutomationCreate, AutomationPatch, type AutomationRoutesOptions, AutomationRun, AutomationRunBegin, AutomationRunLifecyclePatch, AutomationScheduleDecision, type AutomationStore, AutomationStoreError, BoringAutomationErrorCode, type BoringAutomationServerPluginOptions, type DueRunOutcome, type DueRunResult, DueRunService, type DueRunServiceOptions, type DueRunSummary, FileAutomationStore, type FileAutomationStoreOptions, type HostedAutomationActor, ManualRunExecutor, type ManualRunExecutorOptions, type ManualRunInput, PostgresAutomationStore, type VerifiedAutomationActor, automationNotFound, automationRoutes, createBoringAutomationServerPlugin, defaultBoringAutomationServerPlugin as default, parseAutomationModel, runAlreadyActive, runAlreadyRecorded, runBoringAutomationMigrations, runNotFound };
|
|
210
|
+
export { Automation, AutomationCreate, AutomationPatch, type AutomationRoutesOptions, AutomationRun, AutomationRunBegin, AutomationRunLifecyclePatch, AutomationScheduleDecision, type AutomationStore, AutomationStoreError, BoringAutomationErrorCode, type BoringAutomationServerPluginOptions, type DueRunOutcome, type DueRunResult, DueRunService, type DueRunServiceOptions, type DueRunSummary, FileAutomationStore, type FileAutomationStoreOptions, type HostedAutomationActor, type HostedAutomationCandidate, type HostedDueRunResult, HostedDueRunService, type HostedDueRunServiceOptions, ManualRunExecutor, type ManualRunExecutorOptions, type ManualRunInput, PostgresAutomationStore, type VerifiedAutomationActor, automationNotFound, automationRoutes, createBoringAutomationServerPlugin, defaultBoringAutomationServerPlugin as default, listHostedAutomationCandidates, parseAutomationModel, runAlreadyActive, runAlreadyRecorded, runBoringAutomationMigrations, runNotFound };
|
package/dist/server/index.js
CHANGED
|
@@ -19,7 +19,9 @@ var BORING_AUTOMATION_ERROR_CODES = {
|
|
|
19
19
|
RUN_ALREADY_RECORDED: "BORING_AUTOMATION_RUN_ALREADY_RECORDED",
|
|
20
20
|
RUN_EXECUTOR_UNAVAILABLE: "BORING_AUTOMATION_RUN_EXECUTOR_UNAVAILABLE",
|
|
21
21
|
TRIGGER_FORBIDDEN: "BORING_AUTOMATION_TRIGGER_FORBIDDEN",
|
|
22
|
-
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED"
|
|
22
|
+
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED",
|
|
23
|
+
OWNER_UNAUTHORIZED: "BORING_AUTOMATION_OWNER_UNAUTHORIZED",
|
|
24
|
+
TRIGGER_UNAUTHORIZED: "BORING_AUTOMATION_TRIGGER_UNAUTHORIZED"
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
// src/shared/schema.ts
|
|
@@ -524,7 +526,7 @@ var ManualRunExecutor = class {
|
|
|
524
526
|
options;
|
|
525
527
|
clock;
|
|
526
528
|
async run(input) {
|
|
527
|
-
const actor = await this.options.actorResolver(input.request);
|
|
529
|
+
const actor = input.actor ?? await this.options.actorResolver(input.request);
|
|
528
530
|
const store = await this.options.storeForRequest?.(input.request, actor) ?? this.options.store;
|
|
529
531
|
const automation = await store.getAutomation(input.automationId);
|
|
530
532
|
if (!automation) {
|
|
@@ -705,8 +707,277 @@ function durationMs(startedAt, completedAt) {
|
|
|
705
707
|
return Math.max(0, new Date(completedAt).getTime() - new Date(startedAt).getTime());
|
|
706
708
|
}
|
|
707
709
|
|
|
710
|
+
// src/server/postgresStore.ts
|
|
711
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
712
|
+
var PostgresAutomationStore = class {
|
|
713
|
+
constructor(sql, actor, clock = () => /* @__PURE__ */ new Date()) {
|
|
714
|
+
this.sql = sql;
|
|
715
|
+
this.actor = actor;
|
|
716
|
+
this.clock = clock;
|
|
717
|
+
}
|
|
718
|
+
sql;
|
|
719
|
+
actor;
|
|
720
|
+
clock;
|
|
721
|
+
async listAutomations() {
|
|
722
|
+
const rows = await this.sql`
|
|
723
|
+
SELECT id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
724
|
+
FROM boring_automation_automations
|
|
725
|
+
WHERE workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
726
|
+
ORDER BY created_at, id
|
|
727
|
+
`;
|
|
728
|
+
return rows.map(toAutomation);
|
|
729
|
+
}
|
|
730
|
+
async getAutomation(id) {
|
|
731
|
+
const rows = await this.sql`
|
|
732
|
+
SELECT id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
733
|
+
FROM boring_automation_automations
|
|
734
|
+
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
735
|
+
`;
|
|
736
|
+
return rows[0] ? toAutomation(rows[0]) : null;
|
|
737
|
+
}
|
|
738
|
+
async createAutomation(input) {
|
|
739
|
+
const now = this.clock().toISOString();
|
|
740
|
+
const id = randomUUID2();
|
|
741
|
+
const prompt = input.prompt ?? "";
|
|
742
|
+
const rows = await this.sql`
|
|
743
|
+
INSERT INTO boring_automation_automations (id, workspace_id, owner_user_id, title, enabled, cron, timezone, model, prompt, created_at, updated_at)
|
|
744
|
+
VALUES (${id}, ${this.actor.workspaceId}, ${this.actor.userId}, ${input.title}, ${input.enabled ?? true}, ${input.cron}, ${input.timezone}, ${input.model}, ${prompt}, ${now}, ${now})
|
|
745
|
+
RETURNING id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
746
|
+
`;
|
|
747
|
+
return toAutomation(rows[0]);
|
|
748
|
+
}
|
|
749
|
+
async updateAutomation(id, patch) {
|
|
750
|
+
const current = await this.getAutomation(id);
|
|
751
|
+
if (!current) throw automationNotFound(id);
|
|
752
|
+
const next = { ...current, ...patch, updatedAt: this.clock().toISOString() };
|
|
753
|
+
const rows = await this.sql`
|
|
754
|
+
UPDATE boring_automation_automations
|
|
755
|
+
SET title = ${next.title}, enabled = ${next.enabled}, cron = ${next.cron}, timezone = ${next.timezone}, model = ${next.model}, updated_at = ${next.updatedAt}
|
|
756
|
+
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
757
|
+
RETURNING id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
758
|
+
`;
|
|
759
|
+
if (!rows[0]) throw automationNotFound(id);
|
|
760
|
+
return toAutomation(rows[0]);
|
|
761
|
+
}
|
|
762
|
+
async deleteAutomation(id) {
|
|
763
|
+
const result = await this.sql`
|
|
764
|
+
DELETE FROM boring_automation_automations
|
|
765
|
+
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
766
|
+
`;
|
|
767
|
+
if (result.count === 0) throw automationNotFound(id);
|
|
768
|
+
}
|
|
769
|
+
async getPrompt(automationId) {
|
|
770
|
+
const automation = await this.getAutomation(automationId);
|
|
771
|
+
if (!automation) throw automationNotFound(automationId);
|
|
772
|
+
const rows = await this.sql`
|
|
773
|
+
SELECT prompt FROM boring_automation_automations
|
|
774
|
+
WHERE id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
775
|
+
`;
|
|
776
|
+
return rows[0]?.prompt ?? "";
|
|
777
|
+
}
|
|
778
|
+
async updatePrompt(automationId, body) {
|
|
779
|
+
const result = await this.sql`
|
|
780
|
+
UPDATE boring_automation_automations SET prompt = ${body}, updated_at = ${this.clock().toISOString()}
|
|
781
|
+
WHERE id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
782
|
+
`;
|
|
783
|
+
if (result.count === 0) throw automationNotFound(automationId);
|
|
784
|
+
}
|
|
785
|
+
async reconcileOrphanedRuns(automationId) {
|
|
786
|
+
await this.sql`
|
|
787
|
+
UPDATE boring_automation_runs
|
|
788
|
+
SET status = 'failed', completed_at = ${this.clock().toISOString()}, error = 'Automation host restarted before the run completed', updated_at = ${this.clock().toISOString()}
|
|
789
|
+
WHERE automation_id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId} AND status IN ('queued', 'running')
|
|
790
|
+
`;
|
|
791
|
+
}
|
|
792
|
+
async beginRun(input) {
|
|
793
|
+
const automation = await this.getAutomation(input.automationId);
|
|
794
|
+
if (!automation) throw automationNotFound(input.automationId);
|
|
795
|
+
const active = await this.sql`
|
|
796
|
+
SELECT id FROM boring_automation_runs
|
|
797
|
+
WHERE automation_id = ${input.automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId} AND status IN ('queued', 'running')
|
|
798
|
+
LIMIT 1
|
|
799
|
+
`;
|
|
800
|
+
if (active[0]) throw runAlreadyActive(input.automationId);
|
|
801
|
+
try {
|
|
802
|
+
const now = input.createdAt ?? this.clock().toISOString();
|
|
803
|
+
const id = randomUUID2();
|
|
804
|
+
const rows = await this.sql`
|
|
805
|
+
INSERT INTO boring_automation_runs (id, automation_id, workspace_id, owner_user_id, session_id, status, trigger, scheduled_for, started_at, completed_at, duration_ms, input_tokens, output_tokens, total_tokens, prompt_snapshot, model_snapshot, error, created_at, updated_at)
|
|
806
|
+
VALUES (${id}, ${input.automationId}, ${this.actor.workspaceId}, ${this.actor.userId}, NULL, 'queued', ${input.trigger}, ${input.scheduledFor ?? null}, NULL, NULL, NULL, NULL, NULL, NULL, ${input.promptSnapshot}, ${input.modelSnapshot}, NULL, ${now}, ${now})
|
|
807
|
+
RETURNING *
|
|
808
|
+
`;
|
|
809
|
+
return toRun(rows[0]);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
if (isUniqueViolation(error)) {
|
|
812
|
+
if (constraintName(error) === "boring_automation_runs_active_once_idx") throw runAlreadyActive(input.automationId);
|
|
813
|
+
if (input.trigger === "scheduled" && input.scheduledFor) throw runAlreadyRecorded(input.automationId, input.scheduledFor);
|
|
814
|
+
}
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
async updateRunLifecycle(runId, patch) {
|
|
819
|
+
const current = await this.findRun(runId);
|
|
820
|
+
if (!current) throw runNotFound(runId);
|
|
821
|
+
const next = { ...current, ...patch, updatedAt: this.clock().toISOString() };
|
|
822
|
+
const rows = await this.sql`
|
|
823
|
+
UPDATE boring_automation_runs
|
|
824
|
+
SET session_id = ${next.sessionId}, status = ${next.status}, started_at = ${next.startedAt}, completed_at = ${next.completedAt}, duration_ms = ${next.durationMs}, input_tokens = ${next.inputTokens}, output_tokens = ${next.outputTokens}, total_tokens = ${next.totalTokens}, error = ${next.error}, updated_at = ${next.updatedAt}
|
|
825
|
+
WHERE id = ${runId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
826
|
+
RETURNING *
|
|
827
|
+
`;
|
|
828
|
+
if (!rows[0]) throw runNotFound(runId);
|
|
829
|
+
return toRun(rows[0]);
|
|
830
|
+
}
|
|
831
|
+
async listRuns(automationId) {
|
|
832
|
+
const rows = await this.sql`
|
|
833
|
+
SELECT * FROM boring_automation_runs
|
|
834
|
+
WHERE automation_id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
835
|
+
ORDER BY created_at DESC, id DESC
|
|
836
|
+
`;
|
|
837
|
+
return rows.map(toRun);
|
|
838
|
+
}
|
|
839
|
+
async findRun(runId) {
|
|
840
|
+
const rows = await this.sql`
|
|
841
|
+
SELECT * FROM boring_automation_runs
|
|
842
|
+
WHERE id = ${runId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
843
|
+
`;
|
|
844
|
+
return rows[0] ? toRun(rows[0]) : null;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
async function listHostedAutomationCandidates(sql) {
|
|
848
|
+
const automationRows = await sql`
|
|
849
|
+
SELECT id, workspace_id, owner_user_id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
850
|
+
FROM boring_automation_automations
|
|
851
|
+
ORDER BY id
|
|
852
|
+
`;
|
|
853
|
+
const runRows = await sql`
|
|
854
|
+
SELECT * FROM boring_automation_runs ORDER BY automation_id, created_at DESC, id DESC
|
|
855
|
+
`;
|
|
856
|
+
const runsByAutomation = /* @__PURE__ */ new Map();
|
|
857
|
+
for (const row of runRows) {
|
|
858
|
+
const list = runsByAutomation.get(row.automation_id) ?? [];
|
|
859
|
+
list.push(toRun(row));
|
|
860
|
+
runsByAutomation.set(row.automation_id, list);
|
|
861
|
+
}
|
|
862
|
+
return automationRows.map((row) => ({
|
|
863
|
+
automation: toAutomation(row),
|
|
864
|
+
actor: { workspaceId: row.workspace_id, userId: row.owner_user_id },
|
|
865
|
+
runs: runsByAutomation.get(row.id) ?? []
|
|
866
|
+
}));
|
|
867
|
+
}
|
|
868
|
+
function toAutomation(row) {
|
|
869
|
+
return { id: row.id, title: row.title, enabled: row.enabled, cron: row.cron, timezone: row.timezone, model: row.model, promptRef: `hosted:${row.id}`, createdAt: iso(row.created_at), updatedAt: iso(row.updated_at) };
|
|
870
|
+
}
|
|
871
|
+
function toRun(row) {
|
|
872
|
+
return { id: row.id, automationId: row.automation_id, sessionId: row.session_id, status: row.status, trigger: row.trigger, scheduledFor: nullableIso(row.scheduled_for), startedAt: nullableIso(row.started_at), completedAt: nullableIso(row.completed_at), durationMs: row.duration_ms, inputTokens: row.input_tokens, outputTokens: row.output_tokens, totalTokens: row.total_tokens, promptSnapshot: row.prompt_snapshot, modelSnapshot: row.model_snapshot, error: row.error, createdAt: iso(row.created_at), updatedAt: iso(row.updated_at) };
|
|
873
|
+
}
|
|
874
|
+
function iso(value) {
|
|
875
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
876
|
+
}
|
|
877
|
+
function nullableIso(value) {
|
|
878
|
+
return value === null ? null : iso(value);
|
|
879
|
+
}
|
|
880
|
+
function isUniqueViolation(error) {
|
|
881
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "23505");
|
|
882
|
+
}
|
|
883
|
+
function constraintName(error) {
|
|
884
|
+
return error && typeof error === "object" && "constraint_name" in error ? String(error.constraint_name) : void 0;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/server/hostedDueRunService.ts
|
|
888
|
+
var HostedDueRunService = class {
|
|
889
|
+
constructor(options) {
|
|
890
|
+
this.options = options;
|
|
891
|
+
this.clock = options.clock ?? (() => /* @__PURE__ */ new Date());
|
|
892
|
+
}
|
|
893
|
+
options;
|
|
894
|
+
clock;
|
|
895
|
+
async runDue(request) {
|
|
896
|
+
const now = this.clock();
|
|
897
|
+
const candidates = await listHostedAutomationCandidates(this.options.sql);
|
|
898
|
+
const outcomes = [];
|
|
899
|
+
for (const candidate of candidates) {
|
|
900
|
+
if (!await this.options.verifyActor(candidate.actor)) {
|
|
901
|
+
outcomes.push({
|
|
902
|
+
kind: "failed",
|
|
903
|
+
automationId: candidate.automation.id,
|
|
904
|
+
scheduledFor: now.toISOString(),
|
|
905
|
+
code: BORING_AUTOMATION_ERROR_CODES.OWNER_UNAUTHORIZED,
|
|
906
|
+
message: "automation creator is no longer authorized"
|
|
907
|
+
});
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const evaluated = evaluateAutomationSchedule({
|
|
911
|
+
automations: [candidate.automation],
|
|
912
|
+
runs: candidate.runs,
|
|
913
|
+
now
|
|
914
|
+
});
|
|
915
|
+
const decision = evaluated.due[0];
|
|
916
|
+
if (!decision) {
|
|
917
|
+
const skipped = evaluated.decisions[0];
|
|
918
|
+
if (skipped?.kind === "skip") outcomes.push({
|
|
919
|
+
kind: "skipped",
|
|
920
|
+
automationId: candidate.automation.id,
|
|
921
|
+
scheduledFor: skipped.scheduledFor,
|
|
922
|
+
reason: skipped.reason,
|
|
923
|
+
message: skipped.message
|
|
924
|
+
});
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
const store = new PostgresAutomationStore(this.options.sql, candidate.actor, this.clock);
|
|
928
|
+
const executor = new ManualRunExecutor({
|
|
929
|
+
store,
|
|
930
|
+
dispatcherResolver: this.options.dispatcherResolver,
|
|
931
|
+
actorResolver: () => candidate.actor
|
|
932
|
+
});
|
|
933
|
+
try {
|
|
934
|
+
const run = await executor.run({
|
|
935
|
+
automationId: candidate.automation.id,
|
|
936
|
+
request,
|
|
937
|
+
trigger: "scheduled",
|
|
938
|
+
scheduledFor: decision.scheduledFor,
|
|
939
|
+
actor: candidate.actor
|
|
940
|
+
});
|
|
941
|
+
outcomes.push({
|
|
942
|
+
kind: "started",
|
|
943
|
+
automationId: candidate.automation.id,
|
|
944
|
+
scheduledFor: decision.scheduledFor,
|
|
945
|
+
run: toSummary(run)
|
|
946
|
+
});
|
|
947
|
+
} catch (error) {
|
|
948
|
+
outcomes.push({
|
|
949
|
+
kind: "failed",
|
|
950
|
+
automationId: candidate.automation.id,
|
|
951
|
+
scheduledFor: decision.scheduledFor,
|
|
952
|
+
code: error instanceof Error && "code" in error ? String(error.code) : BORING_AUTOMATION_ERROR_CODES.RUN_FAILED,
|
|
953
|
+
message: error instanceof Error ? error.message : "Automation run failed"
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
outcomes.sort((a, b) => a.automationId.localeCompare(b.automationId));
|
|
958
|
+
return { now: now.toISOString(), outcomes };
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
function toSummary(run) {
|
|
962
|
+
return {
|
|
963
|
+
id: run.id,
|
|
964
|
+
automationId: run.automationId,
|
|
965
|
+
sessionId: run.sessionId,
|
|
966
|
+
status: run.status,
|
|
967
|
+
trigger: run.trigger,
|
|
968
|
+
scheduledFor: run.scheduledFor,
|
|
969
|
+
startedAt: run.startedAt,
|
|
970
|
+
completedAt: run.completedAt,
|
|
971
|
+
durationMs: run.durationMs,
|
|
972
|
+
inputTokens: run.inputTokens,
|
|
973
|
+
outputTokens: run.outputTokens,
|
|
974
|
+
totalTokens: run.totalTokens
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
708
978
|
// src/server/routes.ts
|
|
709
979
|
import { ZodError } from "zod";
|
|
980
|
+
import { timingSafeEqual } from "crypto";
|
|
710
981
|
async function automationRoutes(app, opts) {
|
|
711
982
|
app.get(`${BORING_AUTOMATION_ROUTE_PREFIX}/automations`, async (_request, reply) => {
|
|
712
983
|
try {
|
|
@@ -769,6 +1040,16 @@ async function automationRoutes(app, opts) {
|
|
|
769
1040
|
return sendError(reply, cause);
|
|
770
1041
|
}
|
|
771
1042
|
});
|
|
1043
|
+
app.post(`${BORING_AUTOMATION_ROUTE_PREFIX}/due/hosted`, async (request, reply) => {
|
|
1044
|
+
try {
|
|
1045
|
+
if (!opts.hostedDueRunService || !opts.hostedTriggerToken || !hasBearerToken(request.headers.authorization, opts.hostedTriggerToken)) {
|
|
1046
|
+
throw new AutomationStoreError(BORING_AUTOMATION_ERROR_CODES.TRIGGER_UNAUTHORIZED, "hosted automation trigger is unauthorized");
|
|
1047
|
+
}
|
|
1048
|
+
return { ok: true, ...await opts.hostedDueRunService.runDue(request) };
|
|
1049
|
+
} catch (cause) {
|
|
1050
|
+
return sendError(reply, cause);
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
772
1053
|
app.post(`${BORING_AUTOMATION_ROUTE_PREFIX}/due`, async (request, reply) => {
|
|
773
1054
|
try {
|
|
774
1055
|
if (!isLoopbackAddress(request.ip)) {
|
|
@@ -846,6 +1127,12 @@ function zodIssueErrorCode(issue) {
|
|
|
846
1127
|
if (field === "timezone") return BORING_AUTOMATION_ERROR_CODES.INVALID_TIMEZONE;
|
|
847
1128
|
return BORING_AUTOMATION_ERROR_CODES.INVALID_BODY;
|
|
848
1129
|
}
|
|
1130
|
+
function hasBearerToken(header, expected) {
|
|
1131
|
+
if (!header?.startsWith("Bearer ")) return false;
|
|
1132
|
+
const actual = Buffer.from(header.slice(7), "utf8");
|
|
1133
|
+
const target = Buffer.from(expected, "utf8");
|
|
1134
|
+
return actual.length === target.length && timingSafeEqual(actual, target);
|
|
1135
|
+
}
|
|
849
1136
|
function isLoopbackAddress(address) {
|
|
850
1137
|
const normalized = address.trim().toLowerCase();
|
|
851
1138
|
return normalized === "127.0.0.1" || normalized === "::1" || normalized.startsWith("::ffff:127.");
|
|
@@ -865,6 +1152,10 @@ function httpStatusForStoreError(error) {
|
|
|
865
1152
|
return 409;
|
|
866
1153
|
case BORING_AUTOMATION_ERROR_CODES.TRIGGER_FORBIDDEN:
|
|
867
1154
|
return 403;
|
|
1155
|
+
case BORING_AUTOMATION_ERROR_CODES.TRIGGER_UNAUTHORIZED:
|
|
1156
|
+
return 401;
|
|
1157
|
+
case BORING_AUTOMATION_ERROR_CODES.OWNER_UNAUTHORIZED:
|
|
1158
|
+
return 403;
|
|
868
1159
|
case BORING_AUTOMATION_ERROR_CODES.RUN_EXECUTOR_UNAVAILABLE:
|
|
869
1160
|
return 503;
|
|
870
1161
|
case BORING_AUTOMATION_ERROR_CODES.RUN_FAILED:
|
|
@@ -934,172 +1225,32 @@ async function runBoringAutomationMigrations(sql) {
|
|
|
934
1225
|
`);
|
|
935
1226
|
}
|
|
936
1227
|
|
|
937
|
-
// src/server/postgresStore.ts
|
|
938
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
939
|
-
var PostgresAutomationStore = class {
|
|
940
|
-
constructor(sql, actor, clock = () => /* @__PURE__ */ new Date()) {
|
|
941
|
-
this.sql = sql;
|
|
942
|
-
this.actor = actor;
|
|
943
|
-
this.clock = clock;
|
|
944
|
-
}
|
|
945
|
-
sql;
|
|
946
|
-
actor;
|
|
947
|
-
clock;
|
|
948
|
-
async listAutomations() {
|
|
949
|
-
const rows = await this.sql`
|
|
950
|
-
SELECT id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
951
|
-
FROM boring_automation_automations
|
|
952
|
-
WHERE workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
953
|
-
ORDER BY created_at, id
|
|
954
|
-
`;
|
|
955
|
-
return rows.map(toAutomation);
|
|
956
|
-
}
|
|
957
|
-
async getAutomation(id) {
|
|
958
|
-
const rows = await this.sql`
|
|
959
|
-
SELECT id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
960
|
-
FROM boring_automation_automations
|
|
961
|
-
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
962
|
-
`;
|
|
963
|
-
return rows[0] ? toAutomation(rows[0]) : null;
|
|
964
|
-
}
|
|
965
|
-
async createAutomation(input) {
|
|
966
|
-
const now = this.clock().toISOString();
|
|
967
|
-
const id = randomUUID2();
|
|
968
|
-
const prompt = input.prompt ?? "";
|
|
969
|
-
const rows = await this.sql`
|
|
970
|
-
INSERT INTO boring_automation_automations (id, workspace_id, owner_user_id, title, enabled, cron, timezone, model, prompt, created_at, updated_at)
|
|
971
|
-
VALUES (${id}, ${this.actor.workspaceId}, ${this.actor.userId}, ${input.title}, ${input.enabled ?? true}, ${input.cron}, ${input.timezone}, ${input.model}, ${prompt}, ${now}, ${now})
|
|
972
|
-
RETURNING id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
973
|
-
`;
|
|
974
|
-
return toAutomation(rows[0]);
|
|
975
|
-
}
|
|
976
|
-
async updateAutomation(id, patch) {
|
|
977
|
-
const current = await this.getAutomation(id);
|
|
978
|
-
if (!current) throw automationNotFound(id);
|
|
979
|
-
const next = { ...current, ...patch, updatedAt: this.clock().toISOString() };
|
|
980
|
-
const rows = await this.sql`
|
|
981
|
-
UPDATE boring_automation_automations
|
|
982
|
-
SET title = ${next.title}, enabled = ${next.enabled}, cron = ${next.cron}, timezone = ${next.timezone}, model = ${next.model}, updated_at = ${next.updatedAt}
|
|
983
|
-
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
984
|
-
RETURNING id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
985
|
-
`;
|
|
986
|
-
if (!rows[0]) throw automationNotFound(id);
|
|
987
|
-
return toAutomation(rows[0]);
|
|
988
|
-
}
|
|
989
|
-
async deleteAutomation(id) {
|
|
990
|
-
const result = await this.sql`
|
|
991
|
-
DELETE FROM boring_automation_automations
|
|
992
|
-
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
993
|
-
`;
|
|
994
|
-
if (result.count === 0) throw automationNotFound(id);
|
|
995
|
-
}
|
|
996
|
-
async getPrompt(automationId) {
|
|
997
|
-
const automation = await this.getAutomation(automationId);
|
|
998
|
-
if (!automation) throw automationNotFound(automationId);
|
|
999
|
-
const rows = await this.sql`
|
|
1000
|
-
SELECT prompt FROM boring_automation_automations
|
|
1001
|
-
WHERE id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
1002
|
-
`;
|
|
1003
|
-
return rows[0]?.prompt ?? "";
|
|
1004
|
-
}
|
|
1005
|
-
async updatePrompt(automationId, body) {
|
|
1006
|
-
const result = await this.sql`
|
|
1007
|
-
UPDATE boring_automation_automations SET prompt = ${body}, updated_at = ${this.clock().toISOString()}
|
|
1008
|
-
WHERE id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
1009
|
-
`;
|
|
1010
|
-
if (result.count === 0) throw automationNotFound(automationId);
|
|
1011
|
-
}
|
|
1012
|
-
async reconcileOrphanedRuns(automationId) {
|
|
1013
|
-
await this.sql`
|
|
1014
|
-
UPDATE boring_automation_runs
|
|
1015
|
-
SET status = 'failed', completed_at = ${this.clock().toISOString()}, error = 'Automation host restarted before the run completed', updated_at = ${this.clock().toISOString()}
|
|
1016
|
-
WHERE automation_id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId} AND status IN ('queued', 'running')
|
|
1017
|
-
`;
|
|
1018
|
-
}
|
|
1019
|
-
async beginRun(input) {
|
|
1020
|
-
const automation = await this.getAutomation(input.automationId);
|
|
1021
|
-
if (!automation) throw automationNotFound(input.automationId);
|
|
1022
|
-
const active = await this.sql`
|
|
1023
|
-
SELECT id FROM boring_automation_runs
|
|
1024
|
-
WHERE automation_id = ${input.automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId} AND status IN ('queued', 'running')
|
|
1025
|
-
LIMIT 1
|
|
1026
|
-
`;
|
|
1027
|
-
if (active[0]) throw runAlreadyActive(input.automationId);
|
|
1028
|
-
try {
|
|
1029
|
-
const now = input.createdAt ?? this.clock().toISOString();
|
|
1030
|
-
const id = randomUUID2();
|
|
1031
|
-
const rows = await this.sql`
|
|
1032
|
-
INSERT INTO boring_automation_runs (id, automation_id, workspace_id, owner_user_id, session_id, status, trigger, scheduled_for, started_at, completed_at, duration_ms, input_tokens, output_tokens, total_tokens, prompt_snapshot, model_snapshot, error, created_at, updated_at)
|
|
1033
|
-
VALUES (${id}, ${input.automationId}, ${this.actor.workspaceId}, ${this.actor.userId}, NULL, 'queued', ${input.trigger}, ${input.scheduledFor ?? null}, NULL, NULL, NULL, NULL, NULL, NULL, ${input.promptSnapshot}, ${input.modelSnapshot}, NULL, ${now}, ${now})
|
|
1034
|
-
RETURNING *
|
|
1035
|
-
`;
|
|
1036
|
-
return toRun(rows[0]);
|
|
1037
|
-
} catch (error) {
|
|
1038
|
-
if (isUniqueViolation(error)) {
|
|
1039
|
-
if (constraintName(error) === "boring_automation_runs_active_once_idx") throw runAlreadyActive(input.automationId);
|
|
1040
|
-
if (input.trigger === "scheduled" && input.scheduledFor) throw runAlreadyRecorded(input.automationId, input.scheduledFor);
|
|
1041
|
-
}
|
|
1042
|
-
throw error;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
async updateRunLifecycle(runId, patch) {
|
|
1046
|
-
const current = await this.findRun(runId);
|
|
1047
|
-
if (!current) throw runNotFound(runId);
|
|
1048
|
-
const next = { ...current, ...patch, updatedAt: this.clock().toISOString() };
|
|
1049
|
-
const rows = await this.sql`
|
|
1050
|
-
UPDATE boring_automation_runs
|
|
1051
|
-
SET session_id = ${next.sessionId}, status = ${next.status}, started_at = ${next.startedAt}, completed_at = ${next.completedAt}, duration_ms = ${next.durationMs}, input_tokens = ${next.inputTokens}, output_tokens = ${next.outputTokens}, total_tokens = ${next.totalTokens}, error = ${next.error}, updated_at = ${next.updatedAt}
|
|
1052
|
-
WHERE id = ${runId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
1053
|
-
RETURNING *
|
|
1054
|
-
`;
|
|
1055
|
-
if (!rows[0]) throw runNotFound(runId);
|
|
1056
|
-
return toRun(rows[0]);
|
|
1057
|
-
}
|
|
1058
|
-
async listRuns(automationId) {
|
|
1059
|
-
const rows = await this.sql`
|
|
1060
|
-
SELECT * FROM boring_automation_runs
|
|
1061
|
-
WHERE automation_id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
1062
|
-
ORDER BY created_at DESC, id DESC
|
|
1063
|
-
`;
|
|
1064
|
-
return rows.map(toRun);
|
|
1065
|
-
}
|
|
1066
|
-
async findRun(runId) {
|
|
1067
|
-
const rows = await this.sql`
|
|
1068
|
-
SELECT * FROM boring_automation_runs
|
|
1069
|
-
WHERE id = ${runId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
1070
|
-
`;
|
|
1071
|
-
return rows[0] ? toRun(rows[0]) : null;
|
|
1072
|
-
}
|
|
1073
|
-
};
|
|
1074
|
-
function toAutomation(row) {
|
|
1075
|
-
return { id: row.id, title: row.title, enabled: row.enabled, cron: row.cron, timezone: row.timezone, model: row.model, promptRef: `hosted:${row.id}`, createdAt: iso(row.created_at), updatedAt: iso(row.updated_at) };
|
|
1076
|
-
}
|
|
1077
|
-
function toRun(row) {
|
|
1078
|
-
return { id: row.id, automationId: row.automation_id, sessionId: row.session_id, status: row.status, trigger: row.trigger, scheduledFor: nullableIso(row.scheduled_for), startedAt: nullableIso(row.started_at), completedAt: nullableIso(row.completed_at), durationMs: row.duration_ms, inputTokens: row.input_tokens, outputTokens: row.output_tokens, totalTokens: row.total_tokens, promptSnapshot: row.prompt_snapshot, modelSnapshot: row.model_snapshot, error: row.error, createdAt: iso(row.created_at), updatedAt: iso(row.updated_at) };
|
|
1079
|
-
}
|
|
1080
|
-
function iso(value) {
|
|
1081
|
-
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
1082
|
-
}
|
|
1083
|
-
function nullableIso(value) {
|
|
1084
|
-
return value === null ? null : iso(value);
|
|
1085
|
-
}
|
|
1086
|
-
function isUniqueViolation(error) {
|
|
1087
|
-
return Boolean(error && typeof error === "object" && "code" in error && error.code === "23505");
|
|
1088
|
-
}
|
|
1089
|
-
function constraintName(error) {
|
|
1090
|
-
return error && typeof error === "object" && "constraint_name" in error ? String(error.constraint_name) : void 0;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
1228
|
// src/server/index.ts
|
|
1094
1229
|
function createBoringAutomationServerPlugin(options = {}) {
|
|
1095
1230
|
const store = options.store ?? createDefaultStore(options.workspaceRoot);
|
|
1096
|
-
const manualRunExecutor = options.dispatcherResolver && options.actorResolver ? new ManualRunExecutor({
|
|
1097
|
-
|
|
1231
|
+
const manualRunExecutor = options.dispatcherResolver && options.actorResolver ? new ManualRunExecutor({
|
|
1232
|
+
store,
|
|
1233
|
+
storeForRequest: options.storeForRequest,
|
|
1234
|
+
dispatcherResolver: options.dispatcherResolver,
|
|
1235
|
+
actorResolver: options.actorResolver
|
|
1236
|
+
}) : void 0;
|
|
1237
|
+
const dueRunService = manualRunExecutor && !options.storeForRequest ? new DueRunService({ store, executor: manualRunExecutor }) : void 0;
|
|
1098
1238
|
return defineServerPlugin({
|
|
1099
1239
|
id: BORING_AUTOMATION_PLUGIN_ID,
|
|
1100
1240
|
label: BORING_AUTOMATION_PLUGIN_LABEL,
|
|
1101
1241
|
routes: async (app) => {
|
|
1102
|
-
await automationRoutes(app, {
|
|
1242
|
+
await automationRoutes(app, {
|
|
1243
|
+
store,
|
|
1244
|
+
storeForRequest: options.storeForRequest ? async (request) => {
|
|
1245
|
+
const actor = options.actorResolver ? await options.actorResolver(request) : void 0;
|
|
1246
|
+
if (!actor) throw new Error("automation actor resolver is unavailable");
|
|
1247
|
+
return await options.storeForRequest(request, actor);
|
|
1248
|
+
} : void 0,
|
|
1249
|
+
manualRunExecutor,
|
|
1250
|
+
dueRunService,
|
|
1251
|
+
hostedDueRunService: options.hostedDueRunService,
|
|
1252
|
+
hostedTriggerToken: options.hostedTriggerToken
|
|
1253
|
+
});
|
|
1103
1254
|
}
|
|
1104
1255
|
});
|
|
1105
1256
|
}
|
|
@@ -1109,6 +1260,24 @@ function createDefaultStore(workspaceRoot) {
|
|
|
1109
1260
|
}
|
|
1110
1261
|
function defaultBoringAutomationServerPlugin(options, ctx) {
|
|
1111
1262
|
const trusted = ctx?.trusted;
|
|
1263
|
+
if (!options?.store && trusted?.sql && trusted.workspaceAgentDispatcherResolver && trusted.actorResolver) {
|
|
1264
|
+
const sql = trusted.sql;
|
|
1265
|
+
const fallbackStore = new PostgresAutomationStore(sql, { workspaceId: "unbound", userId: "unbound" });
|
|
1266
|
+
return createBoringAutomationServerPlugin({
|
|
1267
|
+
...options,
|
|
1268
|
+
store: fallbackStore,
|
|
1269
|
+
storeForRequest: async (_request, actor) => new PostgresAutomationStore(sql, actor),
|
|
1270
|
+
dispatcherResolver: options?.dispatcherResolver ?? trusted.workspaceAgentDispatcherResolver,
|
|
1271
|
+
actorResolver: options?.actorResolver ?? trusted.actorResolver,
|
|
1272
|
+
actorVerifier: options?.actorVerifier ?? trusted.actorVerifier,
|
|
1273
|
+
hostedTriggerToken: options?.hostedTriggerToken ?? trusted.hostedAutomationTriggerToken,
|
|
1274
|
+
hostedDueRunService: options?.hostedDueRunService ?? new HostedDueRunService({
|
|
1275
|
+
sql,
|
|
1276
|
+
dispatcherResolver: options?.dispatcherResolver ?? trusted.workspaceAgentDispatcherResolver,
|
|
1277
|
+
verifyActor: options?.actorVerifier ?? trusted.actorVerifier
|
|
1278
|
+
})
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1112
1281
|
return createBoringAutomationServerPlugin({
|
|
1113
1282
|
...options,
|
|
1114
1283
|
workspaceRoot: options?.workspaceRoot ?? ctx?.workspaceRoot,
|
|
@@ -1131,6 +1300,7 @@ export {
|
|
|
1131
1300
|
BORING_AUTOMATION_ROUTE_PREFIX,
|
|
1132
1301
|
DueRunService,
|
|
1133
1302
|
FileAutomationStore,
|
|
1303
|
+
HostedDueRunService,
|
|
1134
1304
|
IdParamsSchema,
|
|
1135
1305
|
ManualRunExecutor,
|
|
1136
1306
|
PostgresAutomationStore,
|
|
@@ -1142,6 +1312,7 @@ export {
|
|
|
1142
1312
|
evaluateAutomationSchedule,
|
|
1143
1313
|
isValidFiveFieldCron,
|
|
1144
1314
|
isValidIanaTimeZone,
|
|
1315
|
+
listHostedAutomationCandidates,
|
|
1145
1316
|
parseAutomationModel,
|
|
1146
1317
|
runAlreadyActive,
|
|
1147
1318
|
runAlreadyRecorded,
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ declare const BORING_AUTOMATION_ERROR_CODES: {
|
|
|
16
16
|
readonly RUN_EXECUTOR_UNAVAILABLE: "BORING_AUTOMATION_RUN_EXECUTOR_UNAVAILABLE";
|
|
17
17
|
readonly TRIGGER_FORBIDDEN: "BORING_AUTOMATION_TRIGGER_FORBIDDEN";
|
|
18
18
|
readonly RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED";
|
|
19
|
+
readonly OWNER_UNAUTHORIZED: "BORING_AUTOMATION_OWNER_UNAUTHORIZED";
|
|
20
|
+
readonly TRIGGER_UNAUTHORIZED: "BORING_AUTOMATION_TRIGGER_UNAUTHORIZED";
|
|
19
21
|
};
|
|
20
22
|
type BoringAutomationErrorCode = typeof BORING_AUTOMATION_ERROR_CODES[keyof typeof BORING_AUTOMATION_ERROR_CODES];
|
|
21
23
|
|
package/dist/shared/index.js
CHANGED
|
@@ -15,7 +15,9 @@ var BORING_AUTOMATION_ERROR_CODES = {
|
|
|
15
15
|
RUN_ALREADY_RECORDED: "BORING_AUTOMATION_RUN_ALREADY_RECORDED",
|
|
16
16
|
RUN_EXECUTOR_UNAVAILABLE: "BORING_AUTOMATION_RUN_EXECUTOR_UNAVAILABLE",
|
|
17
17
|
TRIGGER_FORBIDDEN: "BORING_AUTOMATION_TRIGGER_FORBIDDEN",
|
|
18
|
-
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED"
|
|
18
|
+
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED",
|
|
19
|
+
OWNER_UNAUTHORIZED: "BORING_AUTOMATION_OWNER_UNAUTHORIZED",
|
|
20
|
+
TRIGGER_UNAUTHORIZED: "BORING_AUTOMATION_TRIGGER_UNAUTHORIZED"
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
// src/shared/schema.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-automation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.81",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
"fastify": "^5.9.0",
|
|
44
44
|
"react": "^18.0.0 || ^19.0.0",
|
|
45
45
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
46
|
-
"@hachej/boring-workspace": "0.1.
|
|
46
|
+
"@hachej/boring-workspace": "0.1.81"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"croner": "^10.0.1",
|
|
50
50
|
"lucide-react": "^1.21.0",
|
|
51
51
|
"postgres": "^3.4.7",
|
|
52
52
|
"zod": "^3.23.0",
|
|
53
|
-
"@hachej/boring-ui-kit": "0.1.
|
|
53
|
+
"@hachej/boring-ui-kit": "0.1.81"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"tsup": "^8.4.0",
|
|
67
67
|
"typescript": "~6.0.3",
|
|
68
68
|
"vitest": "^4.1.9",
|
|
69
|
-
"@hachej/boring-workspace": "0.1.
|
|
69
|
+
"@hachej/boring-workspace": "0.1.81"
|
|
70
70
|
},
|
|
71
71
|
"peerDependenciesMeta": {
|
|
72
72
|
"fastify": {
|