feat(v1.1.4): outbound HTTP SDK + cron triggers
HTTP (`http::*`):
- `HttpService` trait (picloud-shared) + reqwest-backed `HttpServiceImpl`
(manager-core), wired into the `Services` bundle.
- SSRF deny-list applied to the resolved IP via a custom reqwest
`dns_resolver` (covers every redirect hop + defeats DNS rebinding) plus
a literal-IP check at URL-parse time. Scheme/port restrictions, request
+ response body caps (stream-with-cap), layered timeout. Error reason is
a CIDR category, never the IP. `PICLOUD_HTTP_ALLOW_PRIVATE` dev override
(logs a startup warning).
- Rhai bridge with three-arg split `verb(url, body, opts)` (resolves the
brief's body-vs-opts contradiction; unknown opt keys throw). Body
dispatch by type; response `#{status,headers,body,body_raw}` with JSON
auto-parse; non-2xx does not throw.
- `Capability::AppHttpRequest` → existing `script:write` scope (no new
Scope variant). `SdkCallCx` gains `script_id` (attribution + User-Agent).
Cron triggers (4th trigger kind):
- Migration 0017 widens the kind/source_kind CHECKs and adds
`cron_trigger_details`. `cron`/`chrono-tz` parse + validate 6-field
schedules and IANA timezones.
- `spawn_cron_scheduler` polls due triggers and enqueues to the universal
outbox; the dispatcher delivers them (one-line match-arm extension).
Catch-up fires exactly once per trigger per tick, not once per missed
window. `ctx.event.cron` for handlers.
- `POST /api/v1/admin/apps/{id}/triggers/cron` reuses the v1.1.3
cross-app + kind!=module target check.
- Dashboard: admin-gated Triggers tab (cron create form + list).
Follow-ups: redact module backend errors at the resolver boundary (log
original at error level); pin `rhai = "=1.24"`; CHANGELOG incl. retroactive
v1.1.3 cross-app-trigger security note. Version bumps: workspace 1.1.4,
SDK 1.5, dashboard 0.10.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
43
crates/manager-core/migrations/0017_cron_triggers.sql
Normal file
43
crates/manager-core/migrations/0017_cron_triggers.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- v1.1.4: Extend the triggers framework to recognise `cron` as the
|
||||
-- fourth concrete kind (after `kv` v1.1.1, `dead_letter` v1.1.1, `docs`
|
||||
-- v1.1.2). Mirrors the 0014 docs extension: two CHECK constraints widen
|
||||
-- (strictly gaining `'cron'`), one new detail table.
|
||||
--
|
||||
-- Cron rows route through the SAME generic dispatcher path as kv/docs/
|
||||
-- dead_letter (single match-arm extension on the Rust side). The only
|
||||
-- new machinery is a scheduler task that enqueues due cron triggers
|
||||
-- into the outbox; dispatch itself is unchanged.
|
||||
|
||||
-- Extend triggers.kind to include 'cron'. No existing row carries a
|
||||
-- value outside the widened set, so the drop+add is safe.
|
||||
ALTER TABLE triggers DROP CONSTRAINT triggers_kind_check;
|
||||
ALTER TABLE triggers ADD CONSTRAINT triggers_kind_check
|
||||
CHECK (kind IN ('kv', 'dead_letter', 'docs', 'cron'));
|
||||
|
||||
-- Extend outbox.source_kind to include 'cron'. v1.1.x's existing
|
||||
-- source_kinds ('http', 'kv', 'dead_letter', 'docs') stay.
|
||||
ALTER TABLE outbox DROP CONSTRAINT outbox_source_kind_check;
|
||||
ALTER TABLE outbox ADD CONSTRAINT outbox_source_kind_check
|
||||
CHECK (source_kind IN ('http', 'kv', 'dead_letter', 'docs', 'cron'));
|
||||
|
||||
-- One row per cron trigger.
|
||||
-- schedule — 6-field cron expression (with seconds), validated
|
||||
-- at insert time by the `cron` crate.
|
||||
-- timezone — IANA tz name (e.g. "America/Los_Angeles"), validated
|
||||
-- via chrono-tz. Required so schedules like "every
|
||||
-- weekday at 9am" are unambiguous. Defaults to UTC.
|
||||
-- last_fired_at — set transactionally with each enqueue. NULL until
|
||||
-- the trigger first fires. The scheduler computes the
|
||||
-- next fire time in-process from
|
||||
-- (schedule, timezone, last_fired_at); there is no
|
||||
-- stored next_fire column (kept stateless on purpose).
|
||||
CREATE TABLE cron_trigger_details (
|
||||
trigger_id UUID PRIMARY KEY REFERENCES triggers(id) ON DELETE CASCADE,
|
||||
schedule TEXT NOT NULL,
|
||||
timezone TEXT NOT NULL DEFAULT 'UTC',
|
||||
last_fired_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- Hot lookup for the scheduler: "all enabled cron triggers due now"
|
||||
-- scans by last_fired_at.
|
||||
CREATE INDEX idx_cron_triggers_due ON cron_trigger_details (last_fired_at);
|
||||
Reference in New Issue
Block a user