@amalgm/automations 0.3.0 → 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/AXIOMS.md +7 -1
- package/PURPOSE.md +3 -1
- package/README.md +6 -1
- package/dist/src/cli/shell-runtime.js +34 -5
- package/dist/src/execution-limits.d.ts +6 -0
- package/dist/src/execution-limits.js +6 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/mcp.js +8 -1
- package/dist/src/run-journal.js +56 -1
- package/package.json +1 -1
- package/skills/amalgm-automations/SKILL.md +113 -0
- package/skills/amalgm-automations/agents/openai.yaml +4 -0
- package/skills/amalgm-automations/references/command-contract.md +173 -0
- package/skills/amalgm-automations/references/workflow-plans.md +152 -0
- package/supabase/migrations/20260904010000_typed_workflow_admission.sql +272 -0
- package/skills/automations/SKILL.md +0 -141
package/AXIOMS.md
CHANGED
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
state; it never owns a second automation-definition API.
|
|
42
42
|
15. Admitting a run atomically verifies the current enabled configuration,
|
|
43
43
|
stores a secret-free snapshot, and, for a schedule, advances exactly the
|
|
44
|
-
firing instant that was claimed.
|
|
44
|
+
firing instant that was claimed. Manual, schedule, and webhook admission
|
|
45
|
+
derive executability from the same declared set of supported workflow
|
|
46
|
+
versions.
|
|
45
47
|
16. Legacy local automation storage is not a compatibility authority. Engine
|
|
46
48
|
is deprecated read-only evidence; active callers use this SDK and its
|
|
47
49
|
standalone service, and no old store may run beside them.
|
|
@@ -123,3 +125,7 @@
|
|
|
123
125
|
credential, tunnel credential, or a standalone Automations authorization
|
|
124
126
|
value. Native tool authentication comes from that tool's own user-level
|
|
125
127
|
setup, not from Amalgm's private transport authority.
|
|
128
|
+
43. The durable step journal has one SDK-owned serialized size bound. Oversized
|
|
129
|
+
action or process results become an explicit byte count plus bounded JSON
|
|
130
|
+
preview before transport, so output can never strand a run outside the
|
|
131
|
+
ledger it is meant to update.
|
package/PURPOSE.md
CHANGED
|
@@ -58,7 +58,9 @@ the selected machine host supplies every effectful capability.
|
|
|
58
58
|
|
|
59
59
|
The machine records a step as running before invoking it and records its output
|
|
60
60
|
before advancing. Reconnect resumes at the first step that is not already
|
|
61
|
-
complete.
|
|
61
|
+
complete. One serialized journal bound applies to action and process output;
|
|
62
|
+
oversized results retain their byte count and a bounded preview, so evidence
|
|
63
|
+
cannot make its own durable update impossible. A transient network failure releases the same run back to the queue
|
|
62
64
|
with a bounded future retry time; a configuration, authorization, or execution
|
|
63
65
|
error fails it. Every execution capability receives a stable per-step
|
|
64
66
|
idempotency key. Amalgm actions can use it to make an uncertain acknowledgement
|
package/README.md
CHANGED
|
@@ -46,6 +46,8 @@ the API request.
|
|
|
46
46
|
- `@amalgm/automations/mcp`: agent tools over the same SDK.
|
|
47
47
|
- `@amalgm/automations/host`: standalone Fly service composition.
|
|
48
48
|
- `amalgm-automations`: CLI adapter.
|
|
49
|
+
- `skills/amalgm-automations`: portable agent skill for operating either the
|
|
50
|
+
MCP or global CLI surface safely.
|
|
49
51
|
|
|
50
52
|
## Agent CLI
|
|
51
53
|
|
|
@@ -169,7 +171,10 @@ newline. Either action `input` or process `stdin` may contain the exact template
|
|
|
169
171
|
Shell injects action and process capabilities into the shared executor. Process
|
|
170
172
|
results record exit code, signal, stdout, stderr, and truncation in run history;
|
|
171
173
|
timeouts, cancellation, and output limits are enforced on the selected
|
|
172
|
-
machine.
|
|
174
|
+
machine. The durable journal begins compacting after 1 MiB and stores oversized
|
|
175
|
+
results as an explicit original byte count plus a bounded JSON preview, keeping
|
|
176
|
+
every run update within the service transport bound. Children inherit the
|
|
177
|
+
user's ordinary machine environment so installed
|
|
173
178
|
CLI auth continues to work, but never inherit Shell's private runtime, tunnel,
|
|
174
179
|
or machine credentials. Values in workflow `env` are persisted non-secret
|
|
175
180
|
configuration.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { automationCommandDefinition, } from '../command-surface.js';
|
|
2
|
+
import { MAX_CLI_RUNTIME_RESPONSE_BYTES } from '../execution-limits.js';
|
|
2
3
|
import { AutomationError, ValidationError } from '../errors.js';
|
|
3
4
|
export function createShellAutomationCommands(options) {
|
|
4
5
|
const endpoint = shellAutomationEndpoint(options.runtimeUrl);
|
|
@@ -68,21 +69,49 @@ export function shellAutomationEndpoint(runtimeUrl) {
|
|
|
68
69
|
async function jsonResponse(response) {
|
|
69
70
|
let bytes;
|
|
70
71
|
try {
|
|
71
|
-
bytes =
|
|
72
|
+
bytes = await boundedResponse(response, MAX_CLI_RUNTIME_RESPONSE_BYTES);
|
|
72
73
|
}
|
|
73
74
|
catch (error) {
|
|
75
|
+
if (error instanceof AutomationError)
|
|
76
|
+
throw error;
|
|
74
77
|
throw new AutomationError('runtime_transport', `Cannot read Amalgm Shell runtime response: ${message(error)}`);
|
|
75
78
|
}
|
|
76
|
-
if (bytes.byteLength > 2 * 1024 * 1024) {
|
|
77
|
-
throw new AutomationError('runtime_protocol', 'Amalgm Shell runtime response is too large');
|
|
78
|
-
}
|
|
79
79
|
try {
|
|
80
|
-
return JSON.parse(
|
|
80
|
+
return JSON.parse(bytes.toString('utf8'));
|
|
81
81
|
}
|
|
82
82
|
catch {
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
async function boundedResponse(response, maximumBytes) {
|
|
87
|
+
const declared = Number(response.headers.get('content-length'));
|
|
88
|
+
if (Number.isFinite(declared) && declared > maximumBytes) {
|
|
89
|
+
throw new AutomationError('runtime_protocol', 'Amalgm Shell runtime response is too large');
|
|
90
|
+
}
|
|
91
|
+
if (!response.body)
|
|
92
|
+
return Buffer.alloc(0);
|
|
93
|
+
const chunks = [];
|
|
94
|
+
let size = 0;
|
|
95
|
+
const reader = response.body.getReader();
|
|
96
|
+
try {
|
|
97
|
+
while (true) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done)
|
|
100
|
+
break;
|
|
101
|
+
const chunk = Buffer.from(value);
|
|
102
|
+
size += chunk.byteLength;
|
|
103
|
+
if (size > maximumBytes) {
|
|
104
|
+
await reader.cancel();
|
|
105
|
+
throw new AutomationError('runtime_protocol', 'Amalgm Shell runtime response is too large');
|
|
106
|
+
}
|
|
107
|
+
chunks.push(chunk);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
reader.releaseLock();
|
|
112
|
+
}
|
|
113
|
+
return Buffer.concat(chunks, size);
|
|
114
|
+
}
|
|
86
115
|
function toolResult(value) {
|
|
87
116
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
88
117
|
throw new AutomationError('runtime_protocol', 'Automations MCP returned an invalid tool result');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Start compacting step output before metadata can approach the service body bound. */
|
|
2
|
+
export declare const RUN_JOURNAL_OUTPUT_BUDGET_BYTES: number;
|
|
3
|
+
/** One persisted journal, including step metadata, must remain below 1.5 MiB. */
|
|
4
|
+
export declare const MAX_RUN_JOURNAL_BYTES: number;
|
|
5
|
+
/** A get response can contain the default page of twenty maximum-sized journals. */
|
|
6
|
+
export declare const MAX_CLI_RUNTIME_RESPONSE_BYTES: number;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Start compacting step output before metadata can approach the service body bound. */
|
|
2
|
+
export const RUN_JOURNAL_OUTPUT_BUDGET_BYTES = 1024 * 1024;
|
|
3
|
+
/** One persisted journal, including step metadata, must remain below 1.5 MiB. */
|
|
4
|
+
export const MAX_RUN_JOURNAL_BYTES = 1536 * 1024;
|
|
5
|
+
/** A get response can contain the default page of twenty maximum-sized journals. */
|
|
6
|
+
export const MAX_CLI_RUNTIME_RESPONSE_BYTES = 32 * 1024 * 1024;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { createAutomationApi, type AuthenticateAutomationRequest, type Automatio
|
|
|
5
5
|
export { automationCliHelp, createShellAutomationCommands, runAutomationCli, runAutomationCliMain, shellAutomationEndpoint, type AutomationCliIo, type AutomationCliMainOptions, type ShellAutomationCommandOptions, } from './cli.js';
|
|
6
6
|
export { automationCommandDefinition, automationCommandDefinitions, createAutomationCommands, type AutomationCommandAnnotations, type AutomationCommandDefinition, type AutomationCommandName, type AutomationCommands, } from './command-surface.js';
|
|
7
7
|
export { automationFailure, type AutomationFailureEnvelope } from './command-result.js';
|
|
8
|
+
export { MAX_CLI_RUNTIME_RESPONSE_BYTES, MAX_RUN_JOURNAL_BYTES, RUN_JOURNAL_OUTPUT_BUDGET_BYTES, } from './execution-limits.js';
|
|
8
9
|
export { createAutomationMcpServer } from './mcp.js';
|
|
9
10
|
export { createAutomationToolSurface, type AutomationToolSurface, type AutomationView, type CreateAutomationDefinition, type UpdateAutomationDefinition, type WorkflowChange, } from './tool-surface.js';
|
|
10
11
|
export { SupabaseAutomationCrudRepository, type SupabaseRpcClient } from './supabase-crud.js';
|
package/dist/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { createAutomationApi } from './http.js';
|
|
|
12
12
|
export { automationCliHelp, createShellAutomationCommands, runAutomationCli, runAutomationCliMain, shellAutomationEndpoint, } from './cli.js';
|
|
13
13
|
export { automationCommandDefinition, automationCommandDefinitions, createAutomationCommands, } from './command-surface.js';
|
|
14
14
|
export { automationFailure } from './command-result.js';
|
|
15
|
+
export { MAX_CLI_RUNTIME_RESPONSE_BYTES, MAX_RUN_JOURNAL_BYTES, RUN_JOURNAL_OUTPUT_BUDGET_BYTES, } from './execution-limits.js';
|
|
15
16
|
export { createAutomationMcpServer } from './mcp.js';
|
|
16
17
|
export { createAutomationToolSurface, } from './tool-surface.js';
|
|
17
18
|
export { SupabaseAutomationCrudRepository } from './supabase-crud.js';
|
package/dist/src/mcp.js
CHANGED
|
@@ -4,7 +4,14 @@ import { automationCommandDefinitions, createAutomationCommands, } from './comma
|
|
|
4
4
|
import { automationFailure } from './command-result.js';
|
|
5
5
|
import { createAutomationToolSurface } from './tool-surface.js';
|
|
6
6
|
export { createAutomationToolSurface };
|
|
7
|
-
const resultSchema = {
|
|
7
|
+
const resultSchema = {
|
|
8
|
+
result: z.unknown().optional(),
|
|
9
|
+
error: z.object({
|
|
10
|
+
code: z.string(),
|
|
11
|
+
message: z.string(),
|
|
12
|
+
status: z.number().optional(),
|
|
13
|
+
}).strict().optional(),
|
|
14
|
+
};
|
|
8
15
|
export function createAutomationMcpServer(sdk) {
|
|
9
16
|
const server = new McpServer({ name: 'amalgm-automations-mcp-server', version: '0.1.0' });
|
|
10
17
|
const commands = createAutomationCommands(sdk);
|
package/dist/src/run-journal.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { MAX_RUN_JOURNAL_BYTES, RUN_JOURNAL_OUTPUT_BUDGET_BYTES, } from './execution-limits.js';
|
|
2
|
+
const MAX_OUTPUT_PREVIEW_CHARACTERS = 64 * 1024;
|
|
1
3
|
export function emptyJournal() {
|
|
2
4
|
return { version: 1, steps: [] };
|
|
3
5
|
}
|
|
4
6
|
export function runJournal(value, plan) {
|
|
5
7
|
if (value === undefined)
|
|
6
8
|
return emptyJournal();
|
|
9
|
+
if (jsonBytes(value) > MAX_RUN_JOURNAL_BYTES)
|
|
10
|
+
throw new Error('Run step journal exceeds its persistence bound');
|
|
7
11
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8
12
|
throw new Error('Run step journal is invalid');
|
|
9
13
|
const source = value;
|
|
@@ -34,12 +38,63 @@ export function putStep(journal, step) {
|
|
|
34
38
|
steps.push(step);
|
|
35
39
|
else
|
|
36
40
|
steps[index] = step;
|
|
37
|
-
|
|
41
|
+
let candidate = { version: 1, steps };
|
|
42
|
+
if (jsonBytes(candidate) > RUN_JOURNAL_OUTPUT_BUDGET_BYTES && step.output !== undefined) {
|
|
43
|
+
const compacted = { ...step, output: compactOutput(step.output) };
|
|
44
|
+
if (index === -1)
|
|
45
|
+
steps[steps.length - 1] = compacted;
|
|
46
|
+
else
|
|
47
|
+
steps[index] = compacted;
|
|
48
|
+
candidate = { version: 1, steps };
|
|
49
|
+
}
|
|
50
|
+
if (jsonBytes(candidate) > MAX_RUN_JOURNAL_BYTES) {
|
|
51
|
+
candidate = removeStoredPreviews(candidate);
|
|
52
|
+
}
|
|
53
|
+
if (jsonBytes(candidate) > MAX_RUN_JOURNAL_BYTES) {
|
|
54
|
+
throw new Error('Run step journal exceeds its persistence bound');
|
|
55
|
+
}
|
|
56
|
+
return candidate;
|
|
38
57
|
}
|
|
39
58
|
/** Journals are constructed from JSON fields only; this is the persistence boundary. */
|
|
40
59
|
export function journalJson(journal) {
|
|
60
|
+
if (jsonBytes(journal) > MAX_RUN_JOURNAL_BYTES)
|
|
61
|
+
throw new Error('Run step journal exceeds its persistence bound');
|
|
41
62
|
return journal;
|
|
42
63
|
}
|
|
64
|
+
function compactOutput(output) {
|
|
65
|
+
const serialized = json(output);
|
|
66
|
+
return {
|
|
67
|
+
$amalgmTruncated: true,
|
|
68
|
+
originalBytes: Buffer.byteLength(serialized),
|
|
69
|
+
jsonPreview: serialized.slice(0, MAX_OUTPUT_PREVIEW_CHARACTERS),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function removeStoredPreviews(journal) {
|
|
73
|
+
return {
|
|
74
|
+
version: 1,
|
|
75
|
+
steps: journal.steps.map((step) => {
|
|
76
|
+
if (!isCompactedOutput(step.output))
|
|
77
|
+
return step;
|
|
78
|
+
const { jsonPreview: _preview, ...output } = step.output;
|
|
79
|
+
return { ...step, output };
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function isCompactedOutput(value) {
|
|
84
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
85
|
+
return false;
|
|
86
|
+
const item = value;
|
|
87
|
+
return item.$amalgmTruncated === true && typeof item.jsonPreview === 'string';
|
|
88
|
+
}
|
|
89
|
+
function jsonBytes(value) {
|
|
90
|
+
return Buffer.byteLength(json(value));
|
|
91
|
+
}
|
|
92
|
+
function json(value) {
|
|
93
|
+
const serialized = JSON.stringify(value);
|
|
94
|
+
if (typeof serialized !== 'string')
|
|
95
|
+
throw new Error('Run step output must be JSON');
|
|
96
|
+
return serialized;
|
|
97
|
+
}
|
|
43
98
|
function runStep(value) {
|
|
44
99
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
45
100
|
throw new Error('Run step is invalid');
|
package/package.json
CHANGED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: amalgm-automations
|
|
3
|
+
description: Operate complete Amalgm automations through the Automations MCP tools or the global `amalgm automations` CLI, including schedules, webhooks, Run Now, run history, native commands, scripts, and Amalgm actions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Amalgm Automations
|
|
7
|
+
|
|
8
|
+
Use this skill to turn a user's requested recurring, event-driven, or manual
|
|
9
|
+
workflow into one durable Amalgm automation and verify what actually ran.
|
|
10
|
+
Automations owns configuration and permanent run history; the selected Amalgm
|
|
11
|
+
machine executes the workflow.
|
|
12
|
+
|
|
13
|
+
## Choose the available adapter
|
|
14
|
+
|
|
15
|
+
Prefer the six `amalgm_automations_*` MCP tools when they are callable. In a
|
|
16
|
+
native local agent without that MCP server, use the matching global CLI:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
MCP CLI
|
|
20
|
+
amalgm_automations_create amalgm automations create
|
|
21
|
+
amalgm_automations_list amalgm automations list
|
|
22
|
+
amalgm_automations_get amalgm automations get
|
|
23
|
+
amalgm_automations_update amalgm automations update
|
|
24
|
+
amalgm_automations_delete amalgm automations delete
|
|
25
|
+
amalgm_automations_run_now amalgm automations run-now
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Both adapters accept the same JSON object and call the same SDK command
|
|
29
|
+
surface. Do not translate one into a different resource-level API.
|
|
30
|
+
|
|
31
|
+
For CLI use, first check `amalgm automations --help`. A signed-in, running
|
|
32
|
+
Amalgm Shell supplies authentication through its loopback runtime. Never ask
|
|
33
|
+
the user to copy a runtime token, DPoP proof, device key, or machine credential.
|
|
34
|
+
If the runtime is unavailable, report that Shell must be signed in and running;
|
|
35
|
+
do not fall back to an invented credential flow.
|
|
36
|
+
|
|
37
|
+
Read [references/command-contract.md](references/command-contract.md) whenever
|
|
38
|
+
composing `create` or `update`, or when exact fields are uncertain. Read
|
|
39
|
+
[references/workflow-plans.md](references/workflow-plans.md) whenever a
|
|
40
|
+
workflow runs an Amalgm action, native executable, code script, or trigger
|
|
41
|
+
input.
|
|
42
|
+
|
|
43
|
+
## Preserve scope and intent
|
|
44
|
+
|
|
45
|
+
- List or get before changing an existing automation. Identify the exact
|
|
46
|
+
automation and preserve fields the user did not ask to change.
|
|
47
|
+
- The CLI is global. Its invocation directory never scopes the automation.
|
|
48
|
+
Execution belongs to the selected target machine.
|
|
49
|
+
- Omit `targetId` in a machine-bound session. Never guess or copy an opaque
|
|
50
|
+
target id. If the principal has multiple targets, let the service require an
|
|
51
|
+
explicit user choice.
|
|
52
|
+
- A process `cwd`, when supplied, must be absolute. If omitted, the selected
|
|
53
|
+
machine uses its documented default (normally that user's OS home).
|
|
54
|
+
- Workflow `env` is persisted ordinary configuration, not a secret store.
|
|
55
|
+
Never place Shell credentials or reusable service secrets in it.
|
|
56
|
+
- Use `--stdin` for CLI payloads containing a webhook signing secret or
|
|
57
|
+
complex source. Do not expose those values in command arguments or logs.
|
|
58
|
+
- Delete only after the exact id is established and deletion is within the
|
|
59
|
+
user's request. Deleting configuration intentionally retains run history.
|
|
60
|
+
|
|
61
|
+
## Build one complete definition
|
|
62
|
+
|
|
63
|
+
Create the automation, its schedule/webhook triggers, and its optional workflow
|
|
64
|
+
in one task-level call. The service stages multi-resource changes safely and
|
|
65
|
+
enables the definition only after setup succeeds. If setup fails, report the
|
|
66
|
+
returned disabled draft id rather than silently creating a replacement.
|
|
67
|
+
|
|
68
|
+
Use a finite `maxOccurrences` when the user's request is bounded. Do not turn
|
|
69
|
+
"ten times" into an unbounded schedule plus a future cleanup promise.
|
|
70
|
+
|
|
71
|
+
Choose workflow lanes explicitly:
|
|
72
|
+
|
|
73
|
+
- An Amalgm tool or Amalgm agent is an action step. `chat.chat_agent_run` is
|
|
74
|
+
the Amalgm-agent path and requires a configured agent installation.
|
|
75
|
+
- A user's native Codex, Claude Code, OpenCode, or other installed program is
|
|
76
|
+
a command step. Arguments are literal argv; no shell is implied.
|
|
77
|
+
- Inline shell, Node.js, or Python source is a script step. Choose this lane
|
|
78
|
+
only when code or shell semantics are intentional.
|
|
79
|
+
|
|
80
|
+
Version 1 is action-only. Use version 2 for command or script steps and when
|
|
81
|
+
mixing lanes. Discover Amalgm action ids through the Tools product and native
|
|
82
|
+
executables through the selected machine; never guess either.
|
|
83
|
+
|
|
84
|
+
## Verify Run Now to completion
|
|
85
|
+
|
|
86
|
+
`run-now` performs durable manual admission. Its initial `pending` result is
|
|
87
|
+
not evidence that work completed.
|
|
88
|
+
|
|
89
|
+
1. Supply a stable, caller-chosen `idempotency_key` whenever the request might
|
|
90
|
+
be retried.
|
|
91
|
+
2. If admission is retried, send the same automation id, input, and key. The
|
|
92
|
+
returned run id must remain the same.
|
|
93
|
+
3. Poll `get` with `include_runs: true` or a `run_query` until the admitted run
|
|
94
|
+
becomes `completed` or `failed`. Match the exact run id; do not assume the
|
|
95
|
+
newest unrelated run is yours.
|
|
96
|
+
4. Inspect every step's status and bounded output. Report failure honestly,
|
|
97
|
+
including the failing step and service error.
|
|
98
|
+
|
|
99
|
+
`pending` means durable and awaiting its machine. `sent` or `running` means the
|
|
100
|
+
machine holds a lease. Only `completed` and `failed` are terminal.
|
|
101
|
+
|
|
102
|
+
Diagnose at the failing boundary. Examples: `process_execution_unavailable`
|
|
103
|
+
means that machine lacks the process host; an executable-not-found error is a
|
|
104
|
+
machine PATH/install problem; `Installed agent has no model` is agent
|
|
105
|
+
configuration; an action-specific HTTP error belongs to that action service.
|
|
106
|
+
Do not relabel these as Run Now failures or patch the automation around them.
|
|
107
|
+
|
|
108
|
+
## Report the outcome
|
|
109
|
+
|
|
110
|
+
Return the automation id, target choice if the user selected one, trigger
|
|
111
|
+
summary, workflow lanes, and—when execution was requested—the exact run id and
|
|
112
|
+
terminal result. Mention that run history is durable. Never claim a schedule
|
|
113
|
+
fired merely because configuration creation succeeded.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Command contract
|
|
2
|
+
|
|
3
|
+
Use the field names below exactly. MCP receives the object directly. CLI
|
|
4
|
+
receives it through `--input '<json>'`, `--file <path>`, or `--stdin` and emits
|
|
5
|
+
one JSON envelope:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{"result": {}}
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Failures use stderr, a nonzero exit, and:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{"error":{"code":"...","message":"...","status":400}}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Do not scrape prose or infer success from exit code alone; parse the envelope.
|
|
18
|
+
|
|
19
|
+
## create
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"id": "optional-readable-id",
|
|
24
|
+
"targetId": "optional-explicit-target",
|
|
25
|
+
"name": "Optional name",
|
|
26
|
+
"description": "Optional description",
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"schedules": [{
|
|
29
|
+
"id": "optional-trigger-id",
|
|
30
|
+
"cron": "0 9 * * 1-5",
|
|
31
|
+
"timezone": "America/Los_Angeles",
|
|
32
|
+
"enabled": true,
|
|
33
|
+
"maxOccurrences": 10
|
|
34
|
+
}],
|
|
35
|
+
"webhooks": [{
|
|
36
|
+
"id": "optional-trigger-id",
|
|
37
|
+
"source": "github",
|
|
38
|
+
"event": "push",
|
|
39
|
+
"secret": "optional-provider-secret-at-least-16-characters",
|
|
40
|
+
"enabled": true
|
|
41
|
+
}],
|
|
42
|
+
"workflow": {
|
|
43
|
+
"id": "optional-workflow-id",
|
|
44
|
+
"name": "Optional workflow name",
|
|
45
|
+
"script": "Human-readable summary of what the workflow does.",
|
|
46
|
+
"compiled": {
|
|
47
|
+
"version": 1,
|
|
48
|
+
"steps": [{
|
|
49
|
+
"id": "notify",
|
|
50
|
+
"actionId": "channels.notify_user",
|
|
51
|
+
"input": {"title":"Reminder","message":"Call Mom"}
|
|
52
|
+
}]
|
|
53
|
+
},
|
|
54
|
+
"allowlist": null,
|
|
55
|
+
"limits": null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
At least one executable step is required when `compiled` is supplied. Omit
|
|
61
|
+
`targetId` when Shell has bound the principal to one machine. The returned
|
|
62
|
+
webhook URL is the provider destination; a configured signing secret is
|
|
63
|
+
write-only on later reads.
|
|
64
|
+
|
|
65
|
+
CLI example:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
printf '%s\n' '{"name":"Weekday check","schedules":[{"cron":"0 9 * * 1-5","timezone":"America/Los_Angeles"}],"workflow":{"script":"Run the weekday check.","compiled":{"version":2,"steps":[{"id":"check","kind":"command","command":"my-check","cwd":"/absolute/project"}]}}}' \
|
|
69
|
+
| amalgm automations create --stdin
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## list
|
|
73
|
+
|
|
74
|
+
All fields are optional:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{"targetId":"machine-id","enabled":true,"limit":20,"offset":0}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Use it to find candidates, then `get` the exact definition before mutation.
|
|
81
|
+
|
|
82
|
+
## get
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"automation_id": "automation-id",
|
|
87
|
+
"include_runs": true,
|
|
88
|
+
"run_query": {"status":"completed","limit":20,"offset":0}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`include_runs` and `run_query` are optional. `run_query.status` is one of
|
|
93
|
+
`pending`, `sent`, `running`, `completed`, or `failed`.
|
|
94
|
+
|
|
95
|
+
## update
|
|
96
|
+
|
|
97
|
+
Updates are grouped so one call can change metadata and owned resources:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"automation_id": "automation-id",
|
|
102
|
+
"patch": {
|
|
103
|
+
"targetId": "optional-new-target",
|
|
104
|
+
"name": "New name or null",
|
|
105
|
+
"description": "New description or null",
|
|
106
|
+
"enabled": true
|
|
107
|
+
},
|
|
108
|
+
"schedules": {
|
|
109
|
+
"create": [{"cron":"0 9 * * *","timezone":"UTC"}],
|
|
110
|
+
"update": [{"id":"schedule-id","patch":{"enabled":false}}],
|
|
111
|
+
"delete": ["old-schedule-id"]
|
|
112
|
+
},
|
|
113
|
+
"webhooks": {
|
|
114
|
+
"create": [{"source":"github","event":"push"}],
|
|
115
|
+
"update": [{"id":"webhook-id","patch":{"rotateUrl":true}}],
|
|
116
|
+
"delete": ["old-webhook-id"]
|
|
117
|
+
},
|
|
118
|
+
"workflow": {
|
|
119
|
+
"update": {
|
|
120
|
+
"name": "Updated workflow name",
|
|
121
|
+
"script": "Updated summary",
|
|
122
|
+
"compiled": {
|
|
123
|
+
"version": 2,
|
|
124
|
+
"steps": [{
|
|
125
|
+
"id": "check",
|
|
126
|
+
"kind": "command",
|
|
127
|
+
"command": "my-check",
|
|
128
|
+
"cwd": "/absolute/project"
|
|
129
|
+
}]
|
|
130
|
+
},
|
|
131
|
+
"allowlist": null,
|
|
132
|
+
"limits": null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Every included patch must contain a real change. Workflow change is exactly one
|
|
139
|
+
of `{ "create": ... }`, `{ "update": ... }`, or `{ "delete": true }`.
|
|
140
|
+
Schedule and webhook changes may combine create/update/delete arrays. Preserve
|
|
141
|
+
resource ids returned by `get`; ids are scoped to their automation.
|
|
142
|
+
|
|
143
|
+
## delete
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{"automation_id":"automation-id"}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This deletes current configuration, triggers, and workflow. It does not delete
|
|
150
|
+
historical runs.
|
|
151
|
+
|
|
152
|
+
## run-now
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"automation_id": "automation-id",
|
|
157
|
+
"input": {"requestedBy":"user"},
|
|
158
|
+
"idempotency_key": "stable-key-for-this-logical-request"
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`input` may be any JSON value. Admission requires an enabled automation with an
|
|
163
|
+
executable compiled workflow. The returned run starts as `pending`; follow it
|
|
164
|
+
with `get` until terminal. Reusing the same key for the same logical request
|
|
165
|
+
returns the same run rather than creating a duplicate.
|
|
166
|
+
|
|
167
|
+
## Webhook completion
|
|
168
|
+
|
|
169
|
+
Creating an Amalgm webhook trigger does not register it at GitHub or another
|
|
170
|
+
provider. Configure the returned URL in that provider separately, use JSON
|
|
171
|
+
content type, select only the requested events, and use the same optional
|
|
172
|
+
signing secret on both sides. Rotating the URL invalidates the old bearer
|
|
173
|
+
capability.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Workflow plans
|
|
2
|
+
|
|
3
|
+
The compiled plan is an immutable, ordered snapshot when a run is admitted.
|
|
4
|
+
Step ids must be unique and the plan must contain 1–100 steps.
|
|
5
|
+
|
|
6
|
+
## Step forms
|
|
7
|
+
|
|
8
|
+
Version 1 accepts action steps only:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"version": 1,
|
|
13
|
+
"steps": [{
|
|
14
|
+
"id": "notify",
|
|
15
|
+
"actionId": "channels.notify_user",
|
|
16
|
+
"input": {"title":"Reminder","message":"Call Mom"}
|
|
17
|
+
}]
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Version 2 accepts any ordered mix of these three forms:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
Amalgm action
|
|
25
|
+
{ id, actionId, input }
|
|
26
|
+
|
|
27
|
+
Native command
|
|
28
|
+
{ id, kind:"command", command, args?, cwd?, env?, stdin?,
|
|
29
|
+
timeoutMs?, maxOutputBytes? }
|
|
30
|
+
|
|
31
|
+
Code script
|
|
32
|
+
{ id, kind:"script", runtime:"shell"|"node"|"python", source,
|
|
33
|
+
args?, cwd?, env?, stdin?, timeoutMs?, maxOutputBytes? }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`id`, `actionId`, `command`, every `args` element, environment keys and values,
|
|
37
|
+
and `runtime` are strings. `timeoutMs` and `maxOutputBytes` are positive
|
|
38
|
+
integers. `cwd` is optional but must be absolute when present.
|
|
39
|
+
|
|
40
|
+
## Amalgm actions and agents
|
|
41
|
+
|
|
42
|
+
Action steps run through the selected machine's installed Amalgm Tools/action
|
|
43
|
+
catalog. Discover an action before using it. Do not derive action ids from MCP
|
|
44
|
+
tool names.
|
|
45
|
+
|
|
46
|
+
The Amalgm-agent action is distinct from a native CLI agent:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"id": "review-with-amalgm-agent",
|
|
51
|
+
"actionId": "chat.chat_agent_run",
|
|
52
|
+
"input": {
|
|
53
|
+
"agent_installation_id": "configured-installation-id",
|
|
54
|
+
"message": "Review this occurrence.",
|
|
55
|
+
"context": {"$runInput":true}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The installation must exist and declare a usable model/provider binding. Do
|
|
61
|
+
not guess an installation id from an adapter name such as `codex`.
|
|
62
|
+
|
|
63
|
+
## Native CLI agents
|
|
64
|
+
|
|
65
|
+
Use a command step for the user's independently installed CLI:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"id": "native-codex-review",
|
|
70
|
+
"kind": "command",
|
|
71
|
+
"command": "codex",
|
|
72
|
+
"args": ["exec", "Review this repository and write REVIEW.md"],
|
|
73
|
+
"cwd": "/Users/me/src/project",
|
|
74
|
+
"timeoutMs": 1800000,
|
|
75
|
+
"maxOutputBytes": 1048576
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The executable is resolved from the target machine's PATH and uses its own
|
|
80
|
+
user-level login. Args are passed directly with `shell:false`; `$HOME`, pipes,
|
|
81
|
+
redirection, globbing, and quoting syntax are literal arguments. If shell
|
|
82
|
+
syntax is intended, choose the script lane explicitly.
|
|
83
|
+
|
|
84
|
+
Equivalent installed programs such as `claude -p ...` or `opencode ...` use
|
|
85
|
+
the same command form. Their accepted args and authentication belong to those
|
|
86
|
+
programs, not Automations.
|
|
87
|
+
|
|
88
|
+
## Scripts
|
|
89
|
+
|
|
90
|
+
Scripts persist source in the workflow and execute through a temporary file on
|
|
91
|
+
the selected machine. The temporary file is removed after execution.
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"id": "process-event",
|
|
96
|
+
"kind": "script",
|
|
97
|
+
"runtime": "node",
|
|
98
|
+
"source": "let s=''; process.stdin.on('data', c => s += c).on('end', () => console.log(JSON.parse(s)));",
|
|
99
|
+
"cwd": "/absolute/project",
|
|
100
|
+
"stdin": {"$runInput":true},
|
|
101
|
+
"env": {"MODE":"automation"},
|
|
102
|
+
"timeoutMs": 30000,
|
|
103
|
+
"maxOutputBytes": 262144
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `runtime:"shell"` for POSIX shell semantics on Unix and PowerShell file
|
|
108
|
+
execution on Windows. Use `node` or `python` for explicit code in those
|
|
109
|
+
runtimes. Account for target-machine portability when selecting a runtime or
|
|
110
|
+
absolute path.
|
|
111
|
+
|
|
112
|
+
## Run input
|
|
113
|
+
|
|
114
|
+
The exact singleton object below is the only template marker:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{"$runInput":true}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It resolves recursively inside action `input`. In process steps it may be used
|
|
121
|
+
as `stdin`; strings are passed verbatim and other JSON values are serialized
|
|
122
|
+
with a trailing newline. Trigger payloads are never ambient.
|
|
123
|
+
|
|
124
|
+
## Execution and retries
|
|
125
|
+
|
|
126
|
+
Each step is recorded as running before its effect and committed as completed
|
|
127
|
+
before the next step begins. Completed steps are skipped after reconnect.
|
|
128
|
+
|
|
129
|
+
Every run-step pair receives the stable key `<run-id>:<step-id>`. Process steps
|
|
130
|
+
receive it as `AMALGM_AUTOMATION_IDEMPOTENCY_KEY`. Shell's private runtime,
|
|
131
|
+
tunnel, device, and machine credentials are stripped from child environments.
|
|
132
|
+
|
|
133
|
+
An uncertain machine crash can repeat the current effect. Amalgm actions can
|
|
134
|
+
deduplicate the step key; an arbitrary command or script is at-least-once and
|
|
135
|
+
must durably deduplicate that environment value when exactly-once effects are
|
|
136
|
+
required.
|
|
137
|
+
|
|
138
|
+
Process output records exit code, signal, stdout, stderr, and whether capture
|
|
139
|
+
was truncated. Timeouts, cancellation, and output bounds are enforced on the
|
|
140
|
+
machine. The durable journal compacts oversized results rather than allowing
|
|
141
|
+
large output to strand the run update.
|
|
142
|
+
|
|
143
|
+
## Common boundary diagnoses
|
|
144
|
+
|
|
145
|
+
- `process_execution_unavailable`: selected machine host lacks process support.
|
|
146
|
+
- Spawn/not-found error: executable or runtime is absent from that machine PATH.
|
|
147
|
+
- Nonzero exit: inspect bounded stdout/stderr; the automation correctly failed.
|
|
148
|
+
- Timeout: adjust only when the intended command legitimately needs longer.
|
|
149
|
+
- `Installed agent has no model`: select or configure an actual Amalgm agent
|
|
150
|
+
installation with a model; this is not a native CLI-auth failure.
|
|
151
|
+
- Action-specific HTTP/authorization error: verify that action's owning service
|
|
152
|
+
and input contract without changing Run Now semantics.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
-- Every delivery path admits the same declared workflow versions. The SDK owns
|
|
2
|
+
-- shape validation; this transaction-bound predicate prevents incomplete or
|
|
3
|
+
-- unsupported definitions from entering the durable execution ledger.
|
|
4
|
+
|
|
5
|
+
CREATE OR REPLACE FUNCTION public.amalgm_workflow_is_executable(p_compiled jsonb)
|
|
6
|
+
RETURNS boolean
|
|
7
|
+
LANGUAGE sql
|
|
8
|
+
IMMUTABLE
|
|
9
|
+
SET search_path = public
|
|
10
|
+
AS $$
|
|
11
|
+
SELECT jsonb_typeof(p_compiled) = 'object'
|
|
12
|
+
AND p_compiled->>'version' IN ('1', '2')
|
|
13
|
+
AND jsonb_typeof(p_compiled->'steps') = 'array'
|
|
14
|
+
AND jsonb_array_length(
|
|
15
|
+
CASE WHEN jsonb_typeof(p_compiled->'steps') = 'array'
|
|
16
|
+
THEN p_compiled->'steps' ELSE '[]'::jsonb END
|
|
17
|
+
) BETWEEN 1 AND 100;
|
|
18
|
+
$$;
|
|
19
|
+
|
|
20
|
+
CREATE OR REPLACE FUNCTION public.list_amalgm_event_triggers(p_user_id uuid, p_target_id text)
|
|
21
|
+
RETURNS TABLE (
|
|
22
|
+
user_id uuid,
|
|
23
|
+
automation_id text,
|
|
24
|
+
trigger_id text,
|
|
25
|
+
target_id text,
|
|
26
|
+
source text,
|
|
27
|
+
event text,
|
|
28
|
+
secret text
|
|
29
|
+
)
|
|
30
|
+
LANGUAGE sql
|
|
31
|
+
SECURITY DEFINER
|
|
32
|
+
SET search_path = public
|
|
33
|
+
AS $$
|
|
34
|
+
SELECT t.user_id, t.automation_id, t.id, a.target_id, t.source, t.event, t.secret
|
|
35
|
+
FROM public.amalgm_automation_triggers t
|
|
36
|
+
JOIN public.amalgm_automations a
|
|
37
|
+
ON a.user_id = t.user_id AND a.id = t.automation_id
|
|
38
|
+
JOIN public.amalgm_automation_workflows w
|
|
39
|
+
ON w.user_id = a.user_id AND w.automation_id = a.id
|
|
40
|
+
WHERE t.kind = 'webhook' AND t.user_id = p_user_id
|
|
41
|
+
AND a.target_id = p_target_id AND t.enabled AND a.enabled
|
|
42
|
+
AND public.amalgm_workflow_is_executable(w.compiled);
|
|
43
|
+
$$;
|
|
44
|
+
|
|
45
|
+
CREATE OR REPLACE FUNCTION public.list_due_amalgm_crons(p_now timestamptz)
|
|
46
|
+
RETURNS TABLE (
|
|
47
|
+
user_id uuid,
|
|
48
|
+
automation_id text,
|
|
49
|
+
trigger_id text,
|
|
50
|
+
target_id text,
|
|
51
|
+
cron text,
|
|
52
|
+
timezone text,
|
|
53
|
+
next_run_at timestamptz,
|
|
54
|
+
remaining_occurrences integer
|
|
55
|
+
)
|
|
56
|
+
LANGUAGE sql
|
|
57
|
+
SECURITY DEFINER
|
|
58
|
+
SET search_path = public
|
|
59
|
+
AS $$
|
|
60
|
+
SELECT t.user_id, t.automation_id, t.id, a.target_id,
|
|
61
|
+
t.cron, t.timezone, t.next_run_at, t.remaining_occurrences
|
|
62
|
+
FROM public.amalgm_automation_triggers t
|
|
63
|
+
JOIN public.amalgm_automations a
|
|
64
|
+
ON a.user_id = t.user_id AND a.id = t.automation_id
|
|
65
|
+
JOIN public.amalgm_automation_workflows w
|
|
66
|
+
ON w.user_id = a.user_id AND w.automation_id = a.id
|
|
67
|
+
WHERE t.kind = 'schedule' AND t.next_run_at <= p_now
|
|
68
|
+
AND (t.remaining_occurrences IS NULL OR t.remaining_occurrences > 0)
|
|
69
|
+
AND t.enabled AND a.enabled
|
|
70
|
+
AND public.amalgm_workflow_is_executable(w.compiled)
|
|
71
|
+
ORDER BY t.next_run_at, t.user_id, t.automation_id, t.id;
|
|
72
|
+
$$;
|
|
73
|
+
|
|
74
|
+
CREATE OR REPLACE FUNCTION public.resolve_amalgm_webhook(p_endpoint_id text)
|
|
75
|
+
RETURNS TABLE (
|
|
76
|
+
user_id uuid,
|
|
77
|
+
automation_id text,
|
|
78
|
+
trigger_id text,
|
|
79
|
+
target_id text,
|
|
80
|
+
endpoint_id text,
|
|
81
|
+
source text,
|
|
82
|
+
event text,
|
|
83
|
+
secret text
|
|
84
|
+
)
|
|
85
|
+
LANGUAGE sql
|
|
86
|
+
SECURITY DEFINER
|
|
87
|
+
SET search_path = public
|
|
88
|
+
AS $$
|
|
89
|
+
SELECT t.user_id, t.automation_id, t.id, a.target_id,
|
|
90
|
+
t.endpoint_id, t.source, t.event, t.secret
|
|
91
|
+
FROM public.amalgm_automation_triggers t
|
|
92
|
+
JOIN public.amalgm_automations a
|
|
93
|
+
ON a.user_id = t.user_id AND a.id = t.automation_id
|
|
94
|
+
JOIN public.amalgm_automation_workflows w
|
|
95
|
+
ON w.user_id = a.user_id AND w.automation_id = a.id
|
|
96
|
+
WHERE t.kind = 'webhook' AND t.endpoint_id = p_endpoint_id
|
|
97
|
+
AND t.enabled AND a.enabled
|
|
98
|
+
AND public.amalgm_workflow_is_executable(w.compiled);
|
|
99
|
+
$$;
|
|
100
|
+
|
|
101
|
+
CREATE OR REPLACE FUNCTION public.enqueue_amalgm_run(
|
|
102
|
+
p_kind text,
|
|
103
|
+
p_user_id uuid,
|
|
104
|
+
p_automation_id text,
|
|
105
|
+
p_trigger_id text,
|
|
106
|
+
p_target_id text,
|
|
107
|
+
p_endpoint_id text,
|
|
108
|
+
p_delivery_key text,
|
|
109
|
+
p_expected_next_run_at timestamptz,
|
|
110
|
+
p_next_run_at timestamptz,
|
|
111
|
+
p_input jsonb,
|
|
112
|
+
p_now timestamptz
|
|
113
|
+
) RETURNS SETOF public.amalgm_automation_runs
|
|
114
|
+
LANGUAGE plpgsql
|
|
115
|
+
SECURITY DEFINER
|
|
116
|
+
SET search_path = public
|
|
117
|
+
AS $$
|
|
118
|
+
DECLARE
|
|
119
|
+
v_inserted integer := 0;
|
|
120
|
+
BEGIN
|
|
121
|
+
IF p_kind = 'cron' THEN
|
|
122
|
+
IF p_endpoint_id IS NOT NULL OR p_delivery_key IS NOT NULL THEN
|
|
123
|
+
RAISE EXCEPTION 'Schedule admission cannot carry webhook identity';
|
|
124
|
+
END IF;
|
|
125
|
+
PERFORM 1
|
|
126
|
+
FROM public.amalgm_automation_triggers t
|
|
127
|
+
JOIN public.amalgm_automations a
|
|
128
|
+
ON a.user_id = t.user_id AND a.id = t.automation_id
|
|
129
|
+
WHERE t.user_id = p_user_id AND t.automation_id = p_automation_id
|
|
130
|
+
AND t.id = p_trigger_id AND a.target_id = p_target_id
|
|
131
|
+
AND t.kind = 'schedule' AND t.next_run_at = p_expected_next_run_at
|
|
132
|
+
AND (t.remaining_occurrences IS NULL OR t.remaining_occurrences > 0)
|
|
133
|
+
AND t.enabled AND a.enabled
|
|
134
|
+
FOR UPDATE OF t;
|
|
135
|
+
ELSIF p_kind = 'event' THEN
|
|
136
|
+
IF p_endpoint_id IS NULL THEN RAISE EXCEPTION 'Webhook endpoint is required'; END IF;
|
|
137
|
+
PERFORM 1
|
|
138
|
+
FROM public.amalgm_automation_triggers t
|
|
139
|
+
JOIN public.amalgm_automations a
|
|
140
|
+
ON a.user_id = t.user_id AND a.id = t.automation_id
|
|
141
|
+
WHERE t.user_id = p_user_id AND t.automation_id = p_automation_id
|
|
142
|
+
AND t.id = p_trigger_id AND a.target_id = p_target_id
|
|
143
|
+
AND t.kind = 'webhook' AND t.endpoint_id = p_endpoint_id
|
|
144
|
+
AND t.enabled AND a.enabled;
|
|
145
|
+
ELSE
|
|
146
|
+
RAISE EXCEPTION 'Invalid automation trigger kind';
|
|
147
|
+
END IF;
|
|
148
|
+
IF NOT FOUND THEN RETURN; END IF;
|
|
149
|
+
|
|
150
|
+
RETURN QUERY
|
|
151
|
+
INSERT INTO public.amalgm_automation_runs (
|
|
152
|
+
user_id, target_id, automation_id, trigger_id, workflow_id,
|
|
153
|
+
automation_payload, input, status, delivery_key, created_at
|
|
154
|
+
)
|
|
155
|
+
SELECT a.user_id, a.target_id, a.id, t.id, w.id,
|
|
156
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
157
|
+
'id', a.id, 'targetId', a.target_id, 'name', a.name,
|
|
158
|
+
'description', a.description, 'enabled', a.enabled,
|
|
159
|
+
'trigger', jsonb_strip_nulls(jsonb_build_object(
|
|
160
|
+
'id', t.id,
|
|
161
|
+
'kind', CASE WHEN t.kind = 'schedule' THEN 'cron' ELSE 'event' END,
|
|
162
|
+
'enabled', t.enabled, 'cron', t.cron, 'timezone', t.timezone,
|
|
163
|
+
'source', t.source, 'event', t.event
|
|
164
|
+
)),
|
|
165
|
+
'workflow', jsonb_strip_nulls(jsonb_build_object(
|
|
166
|
+
'id', w.id, 'name', w.name, 'script', w.script,
|
|
167
|
+
'compiled', w.compiled, 'allowlist', w.allowlist, 'limits', w.limits
|
|
168
|
+
))
|
|
169
|
+
)),
|
|
170
|
+
p_input, 'pending', p_delivery_key, p_now
|
|
171
|
+
FROM public.amalgm_automations a
|
|
172
|
+
JOIN public.amalgm_automation_triggers t
|
|
173
|
+
ON t.user_id = a.user_id AND t.automation_id = a.id
|
|
174
|
+
JOIN public.amalgm_automation_workflows w
|
|
175
|
+
ON w.user_id = a.user_id AND w.automation_id = a.id
|
|
176
|
+
WHERE a.user_id = p_user_id AND a.id = p_automation_id AND t.id = p_trigger_id
|
|
177
|
+
AND a.target_id = p_target_id AND t.enabled AND a.enabled
|
|
178
|
+
AND (p_kind = 'cron' OR (t.kind = 'webhook' AND t.endpoint_id = p_endpoint_id))
|
|
179
|
+
AND public.amalgm_workflow_is_executable(w.compiled)
|
|
180
|
+
ON CONFLICT (user_id, automation_id, trigger_id, delivery_key)
|
|
181
|
+
WHERE delivery_key IS NOT NULL
|
|
182
|
+
DO UPDATE SET delivery_key = EXCLUDED.delivery_key
|
|
183
|
+
RETURNING *;
|
|
184
|
+
GET DIAGNOSTICS v_inserted = ROW_COUNT;
|
|
185
|
+
|
|
186
|
+
IF p_kind = 'cron' AND v_inserted = 1 THEN
|
|
187
|
+
UPDATE public.amalgm_automation_triggers SET
|
|
188
|
+
next_run_at = p_next_run_at,
|
|
189
|
+
remaining_occurrences = CASE WHEN remaining_occurrences IS NULL
|
|
190
|
+
THEN NULL ELSE remaining_occurrences - 1 END,
|
|
191
|
+
enabled = CASE WHEN remaining_occurrences = 1 THEN false ELSE enabled END,
|
|
192
|
+
updated_at = p_now
|
|
193
|
+
WHERE user_id = p_user_id AND automation_id = p_automation_id
|
|
194
|
+
AND id = p_trigger_id AND kind = 'schedule'
|
|
195
|
+
AND next_run_at = p_expected_next_run_at;
|
|
196
|
+
END IF;
|
|
197
|
+
END;
|
|
198
|
+
$$;
|
|
199
|
+
|
|
200
|
+
CREATE OR REPLACE FUNCTION public.enqueue_amalgm_manual_run(
|
|
201
|
+
p_user_id uuid,
|
|
202
|
+
p_automation_id text,
|
|
203
|
+
p_input jsonb,
|
|
204
|
+
p_idempotency_key text,
|
|
205
|
+
p_now timestamptz
|
|
206
|
+
) RETURNS SETOF public.amalgm_automation_runs
|
|
207
|
+
LANGUAGE plpgsql
|
|
208
|
+
SECURITY DEFINER
|
|
209
|
+
SET search_path = public
|
|
210
|
+
AS $$
|
|
211
|
+
BEGIN
|
|
212
|
+
IF p_idempotency_key IS NOT NULL THEN
|
|
213
|
+
RETURN QUERY
|
|
214
|
+
SELECT r.*
|
|
215
|
+
FROM public.amalgm_automation_runs r
|
|
216
|
+
WHERE r.user_id = p_user_id
|
|
217
|
+
AND r.automation_id = p_automation_id
|
|
218
|
+
AND r.trigger_id = '@manual'
|
|
219
|
+
AND r.delivery_key = p_idempotency_key;
|
|
220
|
+
IF FOUND THEN RETURN; END IF;
|
|
221
|
+
END IF;
|
|
222
|
+
|
|
223
|
+
RETURN QUERY
|
|
224
|
+
INSERT INTO public.amalgm_automation_runs (
|
|
225
|
+
user_id, target_id, automation_id, trigger_id, workflow_id,
|
|
226
|
+
automation_payload, input, status, delivery_key, created_at
|
|
227
|
+
)
|
|
228
|
+
SELECT a.user_id, a.target_id, a.id, '@manual', w.id,
|
|
229
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
230
|
+
'id', a.id, 'targetId', a.target_id, 'name', a.name,
|
|
231
|
+
'description', a.description, 'enabled', a.enabled,
|
|
232
|
+
'trigger', jsonb_build_object(
|
|
233
|
+
'id', '@manual', 'kind', 'manual', 'enabled', true
|
|
234
|
+
),
|
|
235
|
+
'workflow', jsonb_strip_nulls(jsonb_build_object(
|
|
236
|
+
'id', w.id, 'name', w.name, 'script', w.script,
|
|
237
|
+
'compiled', w.compiled, 'allowlist', w.allowlist, 'limits', w.limits
|
|
238
|
+
))
|
|
239
|
+
)),
|
|
240
|
+
p_input, 'pending', p_idempotency_key, p_now
|
|
241
|
+
FROM public.amalgm_automations a
|
|
242
|
+
JOIN public.amalgm_automation_workflows w
|
|
243
|
+
ON w.user_id = a.user_id AND w.automation_id = a.id
|
|
244
|
+
WHERE a.user_id = p_user_id AND a.id = p_automation_id AND a.enabled
|
|
245
|
+
AND public.amalgm_workflow_is_executable(w.compiled)
|
|
246
|
+
ON CONFLICT (user_id, automation_id, trigger_id, delivery_key)
|
|
247
|
+
WHERE delivery_key IS NOT NULL
|
|
248
|
+
DO UPDATE SET delivery_key = EXCLUDED.delivery_key
|
|
249
|
+
RETURNING *;
|
|
250
|
+
END;
|
|
251
|
+
$$;
|
|
252
|
+
|
|
253
|
+
REVOKE ALL ON FUNCTION public.amalgm_workflow_is_executable(jsonb) FROM PUBLIC;
|
|
254
|
+
REVOKE ALL ON FUNCTION public.list_amalgm_event_triggers(uuid, text) FROM PUBLIC;
|
|
255
|
+
REVOKE ALL ON FUNCTION public.list_due_amalgm_crons(timestamptz) FROM PUBLIC;
|
|
256
|
+
REVOKE ALL ON FUNCTION public.resolve_amalgm_webhook(text) FROM PUBLIC;
|
|
257
|
+
REVOKE ALL ON FUNCTION public.enqueue_amalgm_run(
|
|
258
|
+
text, uuid, text, text, text, text, text, timestamptz, timestamptz, jsonb, timestamptz
|
|
259
|
+
) FROM PUBLIC;
|
|
260
|
+
REVOKE ALL ON FUNCTION public.enqueue_amalgm_manual_run(
|
|
261
|
+
uuid, text, jsonb, text, timestamptz
|
|
262
|
+
) FROM PUBLIC;
|
|
263
|
+
|
|
264
|
+
GRANT EXECUTE ON FUNCTION public.list_amalgm_event_triggers(uuid, text) TO service_role;
|
|
265
|
+
GRANT EXECUTE ON FUNCTION public.list_due_amalgm_crons(timestamptz) TO service_role;
|
|
266
|
+
GRANT EXECUTE ON FUNCTION public.resolve_amalgm_webhook(text) TO service_role;
|
|
267
|
+
GRANT EXECUTE ON FUNCTION public.enqueue_amalgm_run(
|
|
268
|
+
text, uuid, text, text, text, text, text, timestamptz, timestamptz, jsonb, timestamptz
|
|
269
|
+
) TO service_role;
|
|
270
|
+
GRANT EXECUTE ON FUNCTION public.enqueue_amalgm_manual_run(
|
|
271
|
+
uuid, text, jsonb, text, timestamptz
|
|
272
|
+
) TO service_role;
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: automations
|
|
3
|
-
description: Create, inspect, change, delete, or manually run complete Amalgm automation definitions and inspect their run history through the Automations MCP tools.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Amalgm Automations
|
|
7
|
-
|
|
8
|
-
Use the Automations tools for scheduled or webhook-driven work. The tools are
|
|
9
|
-
the agent adapter over the same hosted SDK used by the UI.
|
|
10
|
-
|
|
11
|
-
When MCP is unavailable but the authenticated Amalgm Shell CLI is available,
|
|
12
|
-
use the command with the matching suffix (`create`, `list`, `get`, `update`,
|
|
13
|
-
`delete`, or `run-now`). Pass the exact MCP input object as JSON, preferably on
|
|
14
|
-
stdin when it contains a provider secret:
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
printf '%s\n' '{"name":"Call Mom","schedules":[{"cron":"* * * * *","timezone":"America/Los_Angeles","maxOccurrences":10}],"workflow":{"script":"Notify me to call Mom.","compiled":{"version":1,"steps":[{"id":"notify","actionId":"channels.notify_user","input":{"title":"Reminder","message":"Call Mom"}}]}}}' \
|
|
18
|
-
| amalgm automations create --stdin
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
The CLI returns `{ "result": ... }` on stdout or a structured `error` object
|
|
22
|
-
on stderr with a nonzero exit. It has no separate low-level trigger or workflow
|
|
23
|
-
commands; complete definitions and grouped changes use the same task-level
|
|
24
|
-
surface as MCP.
|
|
25
|
-
|
|
26
|
-
The CLI is global. Its current directory does not scope or modify the
|
|
27
|
-
automation. Process location belongs in an absolute workflow-step `cwd`; when
|
|
28
|
-
it is omitted, Shell uses the selected machine user's OS home.
|
|
29
|
-
|
|
30
|
-
## Choose the execution lane deliberately
|
|
31
|
-
|
|
32
|
-
Use exactly one of these shapes for each compiled workflow step:
|
|
33
|
-
|
|
34
|
-
Version 1 remains action-only. Use plan version 2 for every workflow containing
|
|
35
|
-
a native command or code script; version 2 may mix all three lanes.
|
|
36
|
-
|
|
37
|
-
- **Amalgm action** — `{ "id", "actionId", "input" }`. Use this for Amalgm
|
|
38
|
-
tools, Channels, or the Amalgm agent path. `chat.chat_agent_run` is an Amalgm
|
|
39
|
-
agent, not the user's native CLI agent.
|
|
40
|
-
- **Native command** — `{ "id", "kind":"command", "command", "args"?,
|
|
41
|
-
"cwd"?, "env"?, "stdin"?, "timeoutMs"?, "maxOutputBytes"? }`. Use this for
|
|
42
|
-
installed executables such as `codex exec`, `claude -p`, `opencode`, or a
|
|
43
|
-
script file. Args are literal argv and never pass through a shell.
|
|
44
|
-
- **Code script** — `{ "id", "kind":"script", "runtime":"shell"|"node"|
|
|
45
|
-
"python", "source", "args"?, "cwd"?, "env"?, "stdin"?, "timeoutMs"?,
|
|
46
|
-
"maxOutputBytes"? }`. Use this only when inline code or shell semantics are
|
|
47
|
-
desired explicitly.
|
|
48
|
-
|
|
49
|
-
For a native Codex run on the selected machine:
|
|
50
|
-
|
|
51
|
-
```json
|
|
52
|
-
{
|
|
53
|
-
"version": 2,
|
|
54
|
-
"steps": [{
|
|
55
|
-
"id": "native-review",
|
|
56
|
-
"kind": "command",
|
|
57
|
-
"command": "codex",
|
|
58
|
-
"args": ["exec", "Review this repository and write REVIEW.md"],
|
|
59
|
-
"cwd": "/absolute/path/to/repository",
|
|
60
|
-
"timeoutMs": 1800000
|
|
61
|
-
}]
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Installed CLI authentication comes from that CLI's own user setup on the
|
|
66
|
-
selected machine. Never put Shell runtime, tunnel, or machine credentials in a
|
|
67
|
-
workflow. `env` is persisted ordinary configuration, not a secret store.
|
|
68
|
-
Process results and bounded stdout/stderr appear in run history. A native
|
|
69
|
-
process may repeat after an uncertain machine crash; it receives
|
|
70
|
-
`AMALGM_AUTOMATION_IDEMPOTENCY_KEY` for durable deduplication when needed.
|
|
71
|
-
|
|
72
|
-
## Create a scheduled notification
|
|
73
|
-
|
|
74
|
-
For a request such as “remind me to call my mom every minute for the next ten
|
|
75
|
-
minutes,” make one `amalgm_automations_create` call containing the complete
|
|
76
|
-
definition. Include a readable workflow summary and this compiled plan:
|
|
77
|
-
|
|
78
|
-
```json
|
|
79
|
-
{
|
|
80
|
-
"version": 1,
|
|
81
|
-
"steps": [
|
|
82
|
-
{
|
|
83
|
-
"id": "notify",
|
|
84
|
-
"actionId": "channels.notify_user",
|
|
85
|
-
"input": { "title": "Reminder", "message": "Call Mom" }
|
|
86
|
-
}
|
|
87
|
-
]
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
Include one schedule with cron `* * * * *`, the user's timezone, and
|
|
92
|
-
`maxOccurrences: 10`. Omit `targetId` in a machine-bound session; never guess
|
|
93
|
-
an opaque target id.
|
|
94
|
-
|
|
95
|
-
The tool stages multi-resource configuration disabled and enables it only after
|
|
96
|
-
setup succeeds. If setup fails, report the returned disabled draft id. Never
|
|
97
|
-
replace a bounded occurrence count with an unbounded schedule plus a promise to
|
|
98
|
-
clean it up later.
|
|
99
|
-
|
|
100
|
-
## Read and change
|
|
101
|
-
|
|
102
|
-
Use `amalgm_automations_list` or `amalgm_automations_get` before changing an
|
|
103
|
-
existing automation. Use `amalgm_automations_update` for grouped metadata,
|
|
104
|
-
schedule, webhook, or workflow changes, and `amalgm_automations_delete` only
|
|
105
|
-
after identifying the exact automation. Schedule and webhook triggers remain
|
|
106
|
-
distinct resources inside the definition. A workflow is zero-or-one per
|
|
107
|
-
automation. Run history is read-only, requested through `get`, and remains
|
|
108
|
-
after configuration deletion.
|
|
109
|
-
|
|
110
|
-
Use `amalgm_automations_run_now` when the user wants an enabled automation to
|
|
111
|
-
run immediately. Supply `idempotency_key` when a caller may retry the same
|
|
112
|
-
request. The result is a durable `pending` run, not proof that its selected
|
|
113
|
-
machine has finished it; use `amalgm_automations_get` with run history when the
|
|
114
|
-
user asks for the outcome.
|
|
115
|
-
|
|
116
|
-
Amalgm action discovery belongs to the Tools product. Native command discovery
|
|
117
|
-
belongs to the selected machine's PATH and installed user environment.
|
|
118
|
-
|
|
119
|
-
For webhook-driven work, pass the admitted provider payload into an action or
|
|
120
|
-
process by placing the exact value `{ "$runInput": true }` at the desired point
|
|
121
|
-
in action `input` or process `stdin`. For example, an agent-review action can
|
|
122
|
-
use a static `message` and set `context` to `{ "$runInput": true }`. Never paste
|
|
123
|
-
an example delivery into the workflow: every admitted run must use its own
|
|
124
|
-
durable input.
|
|
125
|
-
|
|
126
|
-
`pending` means a run is durable and waiting for its selected machine. `sent`
|
|
127
|
-
or `running` means that machine holds a lease. `completed` and `failed` are
|
|
128
|
-
terminal. Do not infer delivery from the schedule alone; inspect runs when the
|
|
129
|
-
user asks whether it actually happened.
|
|
130
|
-
|
|
131
|
-
Each webhook trigger returns one copyable `webhookUrl`; use that URL as the
|
|
132
|
-
provider destination. A separate provider signing secret is optional and
|
|
133
|
-
write-only. Never echo, log, or place either credential in a workflow. Rotate
|
|
134
|
-
the URL when its bearer capability may have been exposed.
|
|
135
|
-
|
|
136
|
-
For GitHub, register the returned URL as a repository webhook with JSON content
|
|
137
|
-
type and select only the requested events (normally `push`). If a provider
|
|
138
|
-
secret was configured on the trigger, use the same value as GitHub's webhook
|
|
139
|
-
secret. Creating the Amalgm trigger does not silently mutate GitHub; finish the
|
|
140
|
-
provider registration in the authenticated GitHub surface available to the
|
|
141
|
-
agent, then report both sides as configured.
|