loopengine 0.3.1 → 0.3.2
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/core/agent-config.d.ts +2 -0
- package/dist/core/http-notifier.js +14 -3
- package/dist/core/http-notify-triggers/telegram.d.ts +20 -1
- package/dist/core/http-notify-triggers/telegram.js +63 -17
- package/dist/core/messaging/connector.d.ts +7 -0
- package/dist/core/messaging/connector.js +1 -1
- package/dist/core/messaging/connectors/slack/events.d.ts +2 -1
- package/dist/core/messaging/connectors/slack/events.js +6 -2
- package/dist/core/messaging/connectors/slack/setup.d.ts +2 -1
- package/dist/core/messaging/connectors/slack/setup.js +10 -5
- package/dist/core/messaging/connectors/telegram/format.d.ts +6 -0
- package/dist/core/messaging/connectors/telegram/format.js +57 -0
- package/dist/core/messaging/connectors/telegram/index.d.ts +39 -0
- package/dist/core/messaging/connectors/telegram/index.js +136 -0
- package/dist/core/messaging/connectors/telegram/store.d.ts +31 -0
- package/dist/core/messaging/connectors/telegram/store.js +69 -0
- package/dist/core/messaging/connectors/telegram/updates.d.ts +40 -0
- package/dist/core/messaging/connectors/telegram/updates.js +112 -0
- package/dist/core/messaging/jobs.d.ts +44 -0
- package/dist/core/messaging/jobs.js +100 -0
- package/dist/core/messaging/threads.d.ts +12 -0
- package/dist/core/messaging/threads.js +36 -0
- package/dist/core/notifier-file.d.ts +13 -5
- package/dist/core/notifier-file.js +41 -6
- package/dist/core/system-agents/workflow-planner.d.ts +37 -4
- package/dist/core/system-agents/workflow-planner.js +79 -5
- package/dist/core/workflow-triggers.d.ts +15 -5
- package/dist/core/workflow-triggers.js +25 -8
- package/dist/web/agents-config-page.js +168 -50
- package/dist/web/notifier-admin.d.ts +69 -13
- package/dist/web/notifier-admin.js +127 -31
- package/dist/web/workflow-admin.d.ts +10 -0
- package/dist/web/workflow-admin.js +42 -3
- package/dist/web/workflow-page.js +93 -2
- package/package.json +1 -1
|
@@ -16,8 +16,19 @@ How to plan:
|
|
|
16
16
|
- nothing found can do it: set "missing" to what's needed, and add it to gaps.
|
|
17
17
|
3. Rules: every tool the workflow uses gets one. Reading and searching: allow. Anything a person should see first (sending, publishing, paying, creating something others see): ask. Never set deny unless asked.
|
|
18
18
|
4. Skill: the steps in order, as numbered instructions the agent follows, naming the tools and saying exactly when to stop and ask. Write it so the agent keeps going after each answer or decision.
|
|
19
|
-
5. Trigger: a
|
|
20
|
-
|
|
19
|
+
5. Trigger: how a run starts. startMessage is what the agent receives when it starts.
|
|
20
|
+
- schedule: a time or interval ("every Monday at 9" is every "7d", at "09:00", weekday 1).
|
|
21
|
+
- webhook: when something happens in another service that can call a URL ("when a Stripe dispute comes in", "when a GitHub issue is opened", "when a Shopify order is paid", "when our app sends a new signup"). Fill in "webhook" (below). Prefer it over polling on a schedule whenever the service sends webhooks.
|
|
22
|
+
- on_request: started by a person, from Chat, a message, the Run button or an app's API call.
|
|
23
|
+
A workflow that runs on a schedule and on an event as well: trigger schedule, and fill in "webhook" too.
|
|
24
|
+
webhook: sender ("stripe", "github", "shopify", "standard" for Standard Webhooks senders such as Clerk, Resend or Svix, else "custom" for the person's own app or any other service); a short name ("Stripe: new disputes"); setup: one sentence on what to pick in the sender's webhook settings; filter: which events start a run; mapping: each parameter from a dot path in the payload. What senders send:
|
|
25
|
+
- Stripe: one endpoint can get many event types, so filter path "type", values the event names (charge.dispute.created, checkout.session.completed, invoice.payment_failed, customer.subscription.deleted). The object is at data.object: data.object.id, data.object.customer, data.object.amount, data.object.charge.
|
|
26
|
+
- GitHub: the event type is chosen in GitHub's settings (Issues, Pull requests, Pushes), so say it in setup; filter path "action", values such as opened, closed, created. Fields: issue.number, issue.title, pull_request.number, repository.full_name, sender.login.
|
|
27
|
+
- Shopify: one webhook per topic (orders/paid, orders/create, customers/create), chosen in Shopify, so say it in setup and use no filter. The body is the object itself: id, name (the order number, like #1001), email, total_price, financial_status.
|
|
28
|
+
- standard: filter path "type" (user.created, email.bounced); fields under data (data.id, data.email_addresses).
|
|
29
|
+
- custom: whatever the person's app sends; say in setup what to post (JSON with these fields) and use the paths you described. Set "header" when the person names the signature header.
|
|
30
|
+
Every required parameter should come from the payload through mapping; parameters are the only way event details reach the run.
|
|
31
|
+
6. Notify (scheduled and webhook workflows): whether each run's result is sent to the person through the agent's messaging app. true when the result is for them and no step already delivers it ("tell me", "send me", "let me know", a summary or findings that no step posts or emails). false when a step already sends it (posting to a channel, emailing a report): it would arrive twice. false when the work itself is the outcome (answering emails, filing tasks), where a message every run is noise; questions and approvals reach them either way. Say why in one short sentence.
|
|
21
32
|
7. Parameters: if the job is about something that changes from run to run (an order, a ticket, an alert, a customer, a date range), declare it as a parameter: a short snake_case name, a one-line description, and required when a run makes no sense without it. Write $name wherever its value goes, in startMessage and in the skill ("Check the refund request for order $order_number."). Apps and people fill them in when they start a run. A scheduled job that finds its own work ("check the inbox every 10 minutes", "last week's sales") needs none. Don't make fixed settings, such as the repository or the channel, parameters: write them into the skill.
|
|
22
33
|
8. Target: an existing agent only when it already has (or can install) the tools, none of the workflow's rules would change a decision it already has, and the same people would approve. Otherwise a new agent, with a short lowercase-hyphenated name and a system prompt saying who it is and what it's for. Give the reason either way.
|
|
23
34
|
9. keys: the setting names its abilities need that list_connections or the agents don't show as set (from ability_details settings). Not for MCP servers: their keys are asked for on the plan.
|
|
@@ -59,7 +70,7 @@ export function workflowPlannerTools(withMcp = false) {
|
|
|
59
70
|
trigger: {
|
|
60
71
|
type: 'object',
|
|
61
72
|
properties: {
|
|
62
|
-
kind: { type: 'string', enum: ['schedule', 'on_request'] },
|
|
73
|
+
kind: { type: 'string', enum: ['schedule', 'webhook', 'on_request'] },
|
|
63
74
|
description: { type: 'string' },
|
|
64
75
|
every: { type: 'string' },
|
|
65
76
|
at: { type: 'string' },
|
|
@@ -74,9 +85,22 @@ export function workflowPlannerTools(withMcp = false) {
|
|
|
74
85
|
needsApprovals: { type: 'boolean' },
|
|
75
86
|
skill: { type: 'object', properties: { description: { type: 'string' }, body: { type: 'string' } }, required: ['description', 'body'] },
|
|
76
87
|
startMessage: { type: 'string' },
|
|
88
|
+
webhook: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
description: 'When an outside service\'s events start runs: its sender, which events, and the payload fields that fill each parameter.',
|
|
91
|
+
properties: {
|
|
92
|
+
sender: { type: 'string', enum: ['stripe', 'github', 'shopify', 'standard', 'custom'] },
|
|
93
|
+
name: { type: 'string' },
|
|
94
|
+
header: { type: 'string', description: 'custom only: the signature header, when known' },
|
|
95
|
+
setup: { type: 'string', description: 'one sentence: what to pick in the sender\'s webhook settings' },
|
|
96
|
+
filter: { type: 'object', properties: { path: { type: 'string' }, values: { type: 'array', items: { type: 'string' } } }, required: ['path', 'values'] },
|
|
97
|
+
mapping: { type: 'object', description: 'parameter name -> dot path in the payload', additionalProperties: { type: 'string' } },
|
|
98
|
+
},
|
|
99
|
+
required: ['sender', 'name'],
|
|
100
|
+
},
|
|
77
101
|
notify: {
|
|
78
102
|
type: 'object',
|
|
79
|
-
description: 'Scheduled workflows: whether each run\'s result goes to the person through the agent\'s messaging app, and why in one short sentence.',
|
|
103
|
+
description: 'Scheduled and webhook workflows: whether each run\'s result goes to the person through the agent\'s messaging app, and why in one short sentence.',
|
|
80
104
|
properties: { on: { type: 'boolean' }, reason: { type: 'string' } },
|
|
81
105
|
required: ['on', 'reason'],
|
|
82
106
|
},
|
|
@@ -144,7 +168,9 @@ export function validateWorkflowPlan(raw) {
|
|
|
144
168
|
...(typeof triggerRaw.at === 'string' && /^\d{1,2}:\d{2}$/.test(triggerRaw.at) ? { at: triggerRaw.at } : {}),
|
|
145
169
|
...(typeof triggerRaw.weekday === 'number' && triggerRaw.weekday >= 0 && triggerRaw.weekday <= 7 ? { weekday: triggerRaw.weekday } : {}),
|
|
146
170
|
}
|
|
147
|
-
:
|
|
171
|
+
: triggerRaw.kind === 'webhook'
|
|
172
|
+
? { kind: 'webhook', description: str(triggerRaw.description, 200) ?? 'when an event comes in' }
|
|
173
|
+
: { kind: 'on_request', description: str(triggerRaw.description, 200) ?? 'when asked' };
|
|
148
174
|
// An MCP step's permissions are set when its server becomes an ability.
|
|
149
175
|
const mcpTools = new Set(steps.flatMap((s) => (s.mcp && s.tool ? [s.tool] : [])));
|
|
150
176
|
const rules = (Array.isArray(r.rules) ? r.rules : []).flatMap((x) => {
|
|
@@ -172,6 +198,15 @@ export function validateWorkflowPlan(raw) {
|
|
|
172
198
|
const startMessage = str(r.startMessage, 1000) ?? `Run the "${name}" workflow.`;
|
|
173
199
|
const gaps = Array.isArray(r.gaps) ? r.gaps.flatMap((g) => (str(g, 400) ? [str(g, 400)] : [])).slice(0, 10) : [];
|
|
174
200
|
const planned = planParameters(r.parameters, { startMessage, skillBody, scheduled: trigger.kind === 'schedule' });
|
|
201
|
+
const webhook = planWebhook(r.webhook, planned.parameters);
|
|
202
|
+
// A webhook described means events start it; a webhook start with none
|
|
203
|
+
// described is started by hand.
|
|
204
|
+
if (webhook && trigger.kind === 'on_request')
|
|
205
|
+
trigger.kind = 'webhook';
|
|
206
|
+
if (!webhook && trigger.kind === 'webhook')
|
|
207
|
+
trigger.kind = 'on_request';
|
|
208
|
+
if (webhook)
|
|
209
|
+
planned.notes.push(...webhookNotes(webhook, planned.parameters));
|
|
175
210
|
return {
|
|
176
211
|
name,
|
|
177
212
|
id,
|
|
@@ -185,6 +220,7 @@ export function validateWorkflowPlan(raw) {
|
|
|
185
220
|
skill: { description: str(skillRaw.description, 400) ?? summary, body: skillBody },
|
|
186
221
|
startMessage,
|
|
187
222
|
parameters: planned.parameters,
|
|
223
|
+
...(webhook ? { webhook } : {}),
|
|
188
224
|
notify: planNotify(r.notify),
|
|
189
225
|
target: {
|
|
190
226
|
kind: targetRaw.kind === 'existing' && str(targetRaw.agent, 80) ? 'existing' : 'new',
|
|
@@ -195,6 +231,44 @@ export function validateWorkflowPlan(raw) {
|
|
|
195
231
|
gaps: [...gaps, ...planned.notes].slice(0, 12),
|
|
196
232
|
};
|
|
197
233
|
}
|
|
234
|
+
const SENDERS = ['stripe', 'github', 'shopify', 'standard', 'custom'];
|
|
235
|
+
/** A plan's webhook, tidied: a known sender, a filter with values, and a
|
|
236
|
+
* mapping only for parameters the workflow has. Undefined without a
|
|
237
|
+
* sender. Also used on a plan edited before Create. */
|
|
238
|
+
export function planWebhook(raw, parameters) {
|
|
239
|
+
if (!raw || typeof raw !== 'object')
|
|
240
|
+
return undefined;
|
|
241
|
+
const w = raw;
|
|
242
|
+
if (!SENDERS.includes(w.sender))
|
|
243
|
+
return undefined;
|
|
244
|
+
const sender = w.sender;
|
|
245
|
+
const filterRaw = (w.filter ?? {});
|
|
246
|
+
const filterPath = str(filterRaw.path, 120);
|
|
247
|
+
const values = Array.isArray(filterRaw.values) ? filterRaw.values.flatMap((v) => (str(v, 120) ? [str(v, 120)] : [])).slice(0, 10) : [];
|
|
248
|
+
const mapping = {};
|
|
249
|
+
if (w.mapping && typeof w.mapping === 'object' && !Array.isArray(w.mapping)) {
|
|
250
|
+
for (const [param, path] of Object.entries(w.mapping)) {
|
|
251
|
+
const p = str(path, 200);
|
|
252
|
+
if (p && parameters[param] && /^[A-Za-z0-9_.-]+$/.test(p))
|
|
253
|
+
mapping[param] = p;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const header = sender === 'custom' ? str(w.header, 80) : undefined;
|
|
257
|
+
return {
|
|
258
|
+
sender,
|
|
259
|
+
name: str(w.name, 80) ?? `${sender === 'custom' ? 'App' : sender[0].toUpperCase() + sender.slice(1)} events`,
|
|
260
|
+
...(header && /^[A-Za-z0-9-]{1,80}$/.test(header) ? { header } : {}),
|
|
261
|
+
...(filterPath && values.length ? { filter: { path: filterPath, values } } : {}),
|
|
262
|
+
...(Object.keys(mapping).length ? { mapping } : {}),
|
|
263
|
+
...(str(w.setup, 300) ? { setup: str(w.setup, 300) } : {}),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** What the person should know about a planned webhook: required
|
|
267
|
+
* parameters nothing in the payload fills. */
|
|
268
|
+
export function webhookNotes(webhook, parameters) {
|
|
269
|
+
const unfilled = Object.entries(parameters).filter(([name, p]) => p.required && !webhook.mapping?.[name]).map(([name]) => name);
|
|
270
|
+
return unfilled.length ? [`Nothing in the webhook's payload fills ${unfilled.join(', ')}, so those runs would fail; map ${unfilled.length > 1 ? 'them' : 'it'} on the workflow's Webhooks after it's created.`] : [];
|
|
271
|
+
}
|
|
198
272
|
/** A plan's notify choice; off when the planner gave none. */
|
|
199
273
|
export function planNotify(raw) {
|
|
200
274
|
const n = raw && typeof raw === 'object' ? raw : {};
|
|
@@ -57,9 +57,12 @@ export interface WorkflowTrigger {
|
|
|
57
57
|
/** Delivery ids already handled (the latest 200), for retries. */
|
|
58
58
|
seen_ids: string[];
|
|
59
59
|
}
|
|
60
|
-
/** A trigger as the console and API show it: no token hash, no secret
|
|
60
|
+
/** A trigger as the console and API show it: no token hash, no secret,
|
|
61
|
+
* only whether one is set. */
|
|
61
62
|
export type PublicTrigger = Omit<WorkflowTrigger, 'token_hash' | 'verify' | 'seen_ids'> & {
|
|
62
|
-
verify?: Omit<TriggerVerify, 'secret'
|
|
63
|
+
verify?: Omit<TriggerVerify, 'secret'> & {
|
|
64
|
+
secret_set: boolean;
|
|
65
|
+
};
|
|
63
66
|
};
|
|
64
67
|
export declare class TriggerError extends Error {
|
|
65
68
|
}
|
|
@@ -96,8 +99,14 @@ export interface TriggerSettings {
|
|
|
96
99
|
callback_url?: unknown;
|
|
97
100
|
}
|
|
98
101
|
/** Checks settings for a trigger of `workflow`; throws TriggerError saying
|
|
99
|
-
* what's wrong. `existing`
|
|
100
|
-
*
|
|
102
|
+
* what's wrong. With `existing` (a change), a setting left out keeps its
|
|
103
|
+
* value and null removes it, and a sender's secret is kept when only the
|
|
104
|
+
* sender is given again (the console never sees the secret).
|
|
105
|
+
*
|
|
106
|
+
* A trigger that checks a signature can be made turned off without its
|
|
107
|
+
* secret yet: the workflow planner makes one that way, for the person to
|
|
108
|
+
* add the secret the sender shows them. It can't be turned on until the
|
|
109
|
+
* secret is there. */
|
|
101
110
|
export declare function checkTriggerSettings(input: TriggerSettings, workflow: Workflow, existing?: WorkflowTrigger): Pick<WorkflowTrigger, 'name' | 'enabled' | 'verify' | 'filter' | 'mapping' | 'payload_param' | 'notify' | 'callback_url'>;
|
|
102
111
|
/** Makes a trigger for a workflow; returns it with its token (shown once). */
|
|
103
112
|
export declare function createTrigger(agent: string, workflow: Workflow, input: TriggerSettings): Promise<{
|
|
@@ -113,7 +122,8 @@ export declare function rotateTrigger(id: string): Promise<{
|
|
|
113
122
|
export declare function listTriggers(agent: string, workflowId?: string): Promise<WorkflowTrigger[]>;
|
|
114
123
|
/** The trigger a URL's token belongs to. */
|
|
115
124
|
export declare function findTriggerByToken(token: string): Promise<WorkflowTrigger | undefined>;
|
|
116
|
-
/** Whether the sender's own signature on the raw body is right.
|
|
125
|
+
/** Whether the sender's own signature on the raw body is right. Never
|
|
126
|
+
* with no secret: anyone can sign with an empty key. */
|
|
117
127
|
export declare function verifySenderSignature(verify: TriggerVerify, headers: Record<string, string | string[] | undefined>, raw: Buffer, nowS?: number): boolean;
|
|
118
128
|
/** The payload: JSON, a form, or plain text (as { body }). */
|
|
119
129
|
export declare function parsePayload(raw: Buffer, contentType: string | undefined): unknown;
|
|
@@ -96,16 +96,30 @@ export function publicTrigger(t) {
|
|
|
96
96
|
const { token_hash: _hash, verify, seen_ids: _seen, ...rest } = t;
|
|
97
97
|
if (!verify)
|
|
98
98
|
return rest;
|
|
99
|
-
const { secret
|
|
100
|
-
return { ...rest, verify: shown };
|
|
99
|
+
const { secret, ...shown } = verify;
|
|
100
|
+
return { ...rest, verify: { ...shown, secret_set: Boolean(secret) } };
|
|
101
101
|
}
|
|
102
102
|
/** Checks settings for a trigger of `workflow`; throws TriggerError saying
|
|
103
|
-
* what's wrong. `existing`
|
|
104
|
-
*
|
|
103
|
+
* what's wrong. With `existing` (a change), a setting left out keeps its
|
|
104
|
+
* value and null removes it, and a sender's secret is kept when only the
|
|
105
|
+
* sender is given again (the console never sees the secret).
|
|
106
|
+
*
|
|
107
|
+
* A trigger that checks a signature can be made turned off without its
|
|
108
|
+
* secret yet: the workflow planner makes one that way, for the person to
|
|
109
|
+
* add the secret the sender shows them. It can't be turned on until the
|
|
110
|
+
* secret is there. */
|
|
105
111
|
export function checkTriggerSettings(input, workflow, existing) {
|
|
112
|
+
if (existing) {
|
|
113
|
+
const keys = ['verify', 'filter', 'mapping', 'payload_param', 'notify', 'callback_url'];
|
|
114
|
+
input = { ...input };
|
|
115
|
+
for (const key of keys)
|
|
116
|
+
if (input[key] === undefined && existing[key] !== undefined)
|
|
117
|
+
input[key] = existing[key];
|
|
118
|
+
}
|
|
106
119
|
const name = typeof input.name === 'string' && input.name.trim() ? input.name.trim().slice(0, 80) : existing?.name ?? 'Webhook';
|
|
107
120
|
if (input.enabled !== undefined && typeof input.enabled !== 'boolean')
|
|
108
121
|
throw new TriggerError('enabled must be true or false');
|
|
122
|
+
const enabled = input.enabled === undefined ? existing?.enabled ?? true : input.enabled;
|
|
109
123
|
let verify;
|
|
110
124
|
if (input.verify !== undefined && input.verify !== null) {
|
|
111
125
|
const v = input.verify;
|
|
@@ -113,10 +127,10 @@ export function checkTriggerSettings(input, workflow, existing) {
|
|
|
113
127
|
throw new TriggerError(`verify.sender must be one of ${SENDERS.join(', ')}`);
|
|
114
128
|
const kept = existing?.verify && existing.verify.sender === v.sender ? existing.verify.secret : '';
|
|
115
129
|
const secret = typeof v.secret === 'string' && v.secret.trim() ? v.secret.trim() : kept;
|
|
116
|
-
if (!secret)
|
|
130
|
+
if (!secret && enabled)
|
|
117
131
|
throw new TriggerError(`Give the signing secret ${v.sender === 'custom' ? 'the sender' : v.sender} shows for this webhook, so its signature can be checked.`);
|
|
118
132
|
verify = { sender: v.sender, secret };
|
|
119
|
-
if (v.sender === 'standard' && !/^(whsec_)?[A-Za-z0-9+/]+=*$/.test(secret))
|
|
133
|
+
if (v.sender === 'standard' && secret && !/^(whsec_)?[A-Za-z0-9+/]+=*$/.test(secret))
|
|
120
134
|
throw new TriggerError('A Standard Webhooks secret looks like "whsec_" followed by base64 text.');
|
|
121
135
|
if (v.sender === 'custom') {
|
|
122
136
|
const headerName = typeof v.header === 'string' ? v.header.trim() : '';
|
|
@@ -169,7 +183,7 @@ export function checkTriggerSettings(input, workflow, existing) {
|
|
|
169
183
|
const callbackUrl = input.callback_url ? checkCallbackUrl(input.callback_url) : undefined;
|
|
170
184
|
return {
|
|
171
185
|
name,
|
|
172
|
-
enabled
|
|
186
|
+
enabled,
|
|
173
187
|
...(verify ? { verify } : {}),
|
|
174
188
|
...(filter ? { filter } : {}),
|
|
175
189
|
...(mapping ? { mapping } : {}),
|
|
@@ -238,8 +252,11 @@ const header = (headers, name) => {
|
|
|
238
252
|
const v = headers[name];
|
|
239
253
|
return Array.isArray(v) ? v[0] : v;
|
|
240
254
|
};
|
|
241
|
-
/** Whether the sender's own signature on the raw body is right.
|
|
255
|
+
/** Whether the sender's own signature on the raw body is right. Never
|
|
256
|
+
* with no secret: anyone can sign with an empty key. */
|
|
242
257
|
export function verifySenderSignature(verify, headers, raw, nowS = Math.floor(Date.now() / 1000)) {
|
|
258
|
+
if (!verify.secret)
|
|
259
|
+
return false;
|
|
243
260
|
if (verify.sender === 'stripe') {
|
|
244
261
|
const parts = (header(headers, 'stripe-signature') ?? '').split(',').map((p) => p.split('='));
|
|
245
262
|
const t = Number(parts.find(([k]) => k === 't')?.[1]);
|