shared/invoke.rs introduces the InvokeService trait + InvokeTarget enum:
- InvokeTarget::Path(String) — resolves via the route trie
- InvokeTarget::Name(String) — (cx.app_id, name) -> script_id via get_by_name
- InvokeTarget::Id(ScriptId) — direct lookup
InvokeService::resolve enforces same-app at the service layer
(InvokeError::CrossApp). InvokeService::enqueue_async writes a v1.1.9
OutboxSourceKind::Invoke row for fire-and-forget composition.
Errors: NotFound, CrossApp, DepthExceeded(u32), Forbidden, Rejected,
Unavailable. NoopInvokeService surfaces all calls as Unavailable for
harnesses that don't wire the service.
ScriptRepository gains get_by_name(app_id, name) backed by the existing
(app_id, name) uniqueness constraint. Postgres impl + in-memory mock +
the picloud crate's PostgresScriptRepoHandle delegate added.
Services::new gains invoke: Arc<dyn InvokeService> positionally after
queue. with_noop_services wires NoopInvokeService. 12 test sites
threaded through. picloud/lib.rs binds NoopInvokeService at this commit
boundary — the real InvokeResolver lands in commit 8.
Deviation from plan (flagged for HANDBACK §7): the plan called for
moving ExecutorClient + ScriptIdentity from orchestrator-core to shared
so the invoke bridge could call a re-entrant variant. Re-evaluated:
the bridge lives in executor-core which can hold Arc<Engine> directly,
making the trait move unnecessary. Engine::execute is sync, returns
ExecResponse, and shares the engine instance + per-call SdkCallCx
exactly as required. AST cache reuse across invokes is a v1.2 optimization
(invokes recompile each call — ms-scale cost, acceptable).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- shared/services.rs: Services::new gains queue: Arc<dyn QueueService>
positionally after users (mirrors v1.1.8's append of users).
with_noop_services adds NoopQueueService.
- manager-core/queue_service.rs: QueueServiceImpl wraps QueueRepo with
script-as-gate authz on AppQueueEnqueue. enqueue clamps max_attempts
to [1,20] and delay_ms to [0, 86_400_000ms]. depth/depth_pending are
read-only — no authz check (scripts in the app can see their own
queue depths). cx.principal threads through as enqueued_by_principal
(forensic only).
- manager-core/authz.rs: AppQueueEnqueue(AppId) capability — script:write
scope, granted to editor+ (same trust shape as AppPubsubPublish).
- picloud/lib.rs: wires PostgresQueueRepo + QueueServiceImpl into
Services::new alongside the existing v1.1.7+ services.
- 11 sdk test binaries + manager-core/realtime_authority.rs updated to
pass NoopQueueService to Services::new.
Unit tests cover empty queue_name reject, max_attempts clamping
(0/21 → invalid), delay_ms negative-reject, anonymous principal skips
authz, depth/depth_pending pass-through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep the v1.1.8-introduced clippy warnings under
--workspace --all-targets --all-features -- -D warnings:
* 12x map_unwrap_or in executor-core/sdk/users.rs — rewrite as
map_or; mostly the Option<X> -> Dynamic conversion shape.
* Doc-list-without-indentation in
app_user_password_reset_repo.rs's module docstring — rewrap
so a continuation line doesn't start with `+ `.
* usize-as-i64 cast in app_user_repo.rs list — use
i64::try_from(...).unwrap_or(i64::MAX) for the cursor cap.
* 2x map_unwrap_or in users_service.rs env helpers — map_or.
* single-pattern match in users_service.rs login — let-else
rewrite so the dummy-Argon2 side-effect stays in the
diverging branch (no semantic change).
Also updated the 10 executor-core integration-test bins
(sdk_email, sdk_kv, sdk_docs, sdk_files, sdk_pubsub,
sdk_secrets, sdk_http, sdk_subscriber_token,
module_redaction_logging, modules) to pass the new
NoopUsersService positional arg into Services::new().
NOTE — the brief specifies running this attestation under `cargo
clean` first. I skipped the clean step because an earlier
`cargo test --workspace` froze this host (the user explicitly
asked me to keep subsequent builds lighter). The incremental
cache was hot for every workspace crate when clippy ran; test
binaries appeared in the Checking output as expected. Flagged in
HANDBACK §6 F2 + §7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inbound email: a provider POSTs a normalized JSON message to
POST /api/v1/email-inbound/{app_id}/{trigger_id}; the public receiver
verifies the optional HMAC signature, builds a TriggerEvent::Email, and
enqueues an outbox row the dispatcher delivers like any async trigger.
Handlers see ctx.event.email = #{from,to,cc,subject,text,html,
received_at,message_id}.
- migration 0024: widen triggers.kind + outbox.source_kind CHECKs to
'email'; new email_trigger_details table.
- TriggerKind::Email, TriggerDetails::Email{has_inbound_secret},
OutboxSourceKind::Email, TriggerEvent::Email; dispatcher routes the
email row via the generic resolve_trigger path.
- Admin POST /apps/{id}/triggers/email (validate_trigger_target; module
+ cross-app rejection). inbound_secret is stored ENCRYPTED via the
master key (deviation from the brief's plaintext default; decrypted
per inbound request — see HANDBACK §7).
- Dashboard: email trigger form on the Triggers tab + webhook URL +
expected-payload help.
- 8 DB-gated e2e tests (202/401/404/422/cross-app/handler-fire) +
receiver unit tests (HMAC verify, secret round-trip, payload parse).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Outbound email reachable from scripts as email::send(#{...}) (plain
text) and email::send_html(#{...}) (multipart text + HTML). Backed by a
lettre SMTP relay configured from PICLOUD_SMTP_HOST/PORT/USER/PASSWORD/
TLS/TIMEOUT_SECS; if HOST/USER/PASSWORD aren't all set the service runs
in disabled mode (every send throws NotConfigured, warned at startup).
- EmailService trait + OutboundEmail DTO (picloud-shared);
EmailServiceImpl + EmailTransport seam + lettre transport
(manager-core), wired into the Services bundle and Rhai engine.
- Capability::AppEmailSend (→ script:write); seven-scope commitment held.
- Required-field + RFC5322-ish address validation; 25 MB per-message cap
(PICLOUD_EMAIL_MAX_MESSAGE_BYTES). reply_to defaults to from.
- Per-call connection (pooling deferred to v1.2); no per-app from
validation (operator's SMTP/SPF/DKIM concern).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>