feat: E2E #2 (Stash) gap remediation + S6 hardening
Some checks failed
CI / Rust — fmt, clippy, test (push) Failing after 6m19s
CI / Dashboard — check (push) Successful in 9m48s

Closes the gaps and the one security finding from the second end-to-end
CLI test (E2E_STASH_REPORT.md), plus the H1 boot-regression found while
re-reviewing those fixes.

Security
- S6: reserved-path validation (`check_reserved`) now case-folds before
  comparing, so `/API/v2/x`, `/HEALTHZ`, `/Admin/x` are rejected like
  their lowercase forms. Request-time matching stays case-sensitive.
- S10: "public route != public data" callout in sdk-shape.md (script_gate
  skips authz when the principal is anonymous).

Observability / features
- G1: trigger executions now write `execution_logs`. Migration 0043 adds
  a `source` column (CHECK mirrors ExecutionSource/OutboxSourceKind,
  DEFAULT 'http' backfills history); a shared `build_execution_log` helper
  in executor-core; dispatcher logging for outbox triggers + queue
  consumers (skips sync-HTTP rows the orchestrator already logs). `pic
  logs` gains a source column + `--source` filter.
- G5: dev-only in-memory email capture under PICLOUD_DEV_MODE with no SMTP
  (email::send succeeds locally), readable at GET /api/v1/admin/dev/emails
  (Owner/Admin only; route mounted only in capture mode).
- G6: generalized the Rhai in-place-mutation footgun note (trim/replace/
  make_upper/make_lower/crop/truncate/pad return ()).
- G2/G3/G4 (CLI): `pic members`, `pic files`, `pic queues`, read-only
  `pic kv` (+ new kv_api.rs); `pic deploy --timeout/--memory/--kind/
  --sandbox`; first-class `pic triggers create-{docs,files,pubsub,queue,
  email}` wrappers. All new client path segments percent-encoded via seg().

H1 regression fix (found in re-review)
- The S6 change also runs in `compile_routes`, which compiles every stored
  route at boot and on each route CRUD. A single stored route the new
  validation rejects (creatable while the S6 gap existed) made the whole
  compile Err and aborted startup. `compile_routes` is now lenient: it
  skips an un-compilable row with a warning instead of bricking boot
  (route creation still validates separately). Migration 0044 sweeps
  pre-existing reserved-path routes on upgrade (WHERE mirrors
  check_reserved exactly). Added regression tests for both.

Verified: cargo fmt, clippy --all-targets --all-features -D warnings, the
schema_snapshot test, and the new S6/lenient-compile unit tests all pass;
boot-resilience and G1/G5 confirmed live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-13 15:01:04 +02:00
parent a91b134285
commit 51f14fa2b1
33 changed files with 2223 additions and 195 deletions

View File

@@ -12,26 +12,26 @@ use picloud_executor_core::{Engine, Limits};
use picloud_manager_core::{
admin_router, admins_router, api_keys_router, app_members_router, apps_api, apps_router,
attach_principal_if_present, auth_router, compile_routes, dead_letters_router,
email_inbound_router, files_admin_router, migrations, require_authenticated,
route_admin_router, secrets_router, topics_router, triggers_router, AbandonedRepo,
AdminPrincipalResolver, AdminSessionRepository, AdminState, AdminUserRepository, AdminsState,
ApiKeyRepository, ApiKeysState, AppDomainRepository, AppMembersRepository, AppMembersState,
AppRepository, AppsState, AuthState, AuthzRepo, DeadLetterRepo, DeadLettersState, Dispatcher,
DocsServiceImpl, EmailInboundState, EmailServiceImpl, FilesAdminState, FilesConfig,
FilesServiceImpl, FsFilesRepo, HttpConfig, HttpServiceImpl, InboundNonceDedup, KvServiceImpl,
OutboxEventEmitter, OutboxRepo, PostgresAbandonedRepo, PostgresAdminSessionRepository,
PostgresAdminUserRepository, PostgresApiKeyRepository, PostgresAppDomainRepository,
PostgresAppMembersRepository, PostgresAppRepository, PostgresAppSecretsRepo,
PostgresAppUserInvitationRepo, PostgresAppUserPasswordResetRepo, PostgresAppUserRepository,
PostgresAppUserRoleRepo, PostgresAppUserSessionRepository, PostgresAppUserVerificationRepo,
PostgresDeadLetterRepo, PostgresDeadLetterService, PostgresDocsRepo,
PostgresExecutionLogRepository, PostgresExecutionLogSink, PostgresKvRepo, PostgresOutboxRepo,
PostgresPubsubRepo, PostgresRouteRepository, PostgresScriptRepository, PostgresSecretsRepo,
PostgresTopicRepo, PostgresTriggerRepo, PrincipalResolver, PubsubServiceImpl,
RealtimeAuthorityImpl, RepoResolver, RouteAdminState, RouteRepository, SandboxCeiling,
ScriptRepository, SecretsConfig, SecretsServiceImpl, SecretsState, SubscriberTokenConfig,
TopicRepo, TopicsState, TriggerConfig, TriggerRepo, TriggersState, UsersServiceConfig,
UsersServiceImpl,
dev_emails_router, email_inbound_router, files_admin_router, kv_admin_router, migrations,
require_authenticated, route_admin_router, secrets_router, topics_router, triggers_router,
AbandonedRepo, AdminPrincipalResolver, AdminSessionRepository, AdminState, AdminUserRepository,
AdminsState, ApiKeyRepository, ApiKeysState, AppDomainRepository, AppMembersRepository,
AppMembersState, AppRepository, AppsState, AuthState, AuthzRepo, DeadLetterRepo,
DeadLettersState, DevEmailState, Dispatcher, DocsServiceImpl, EmailInboundState,
EmailServiceImpl, FilesAdminState, FilesConfig, FilesServiceImpl, FsFilesRepo, HttpConfig,
HttpServiceImpl, InboundNonceDedup, KvAdminState, KvServiceImpl, OutboxEventEmitter,
OutboxRepo, PostgresAbandonedRepo, PostgresAdminSessionRepository, PostgresAdminUserRepository,
PostgresApiKeyRepository, PostgresAppDomainRepository, PostgresAppMembersRepository,
PostgresAppRepository, PostgresAppSecretsRepo, PostgresAppUserInvitationRepo,
PostgresAppUserPasswordResetRepo, PostgresAppUserRepository, PostgresAppUserRoleRepo,
PostgresAppUserSessionRepository, PostgresAppUserVerificationRepo, PostgresDeadLetterRepo,
PostgresDeadLetterService, PostgresDocsRepo, PostgresExecutionLogRepository,
PostgresExecutionLogSink, PostgresKvRepo, PostgresOutboxRepo, PostgresPubsubRepo,
PostgresRouteRepository, PostgresScriptRepository, PostgresSecretsRepo, PostgresTopicRepo,
PostgresTriggerRepo, PrincipalResolver, PubsubServiceImpl, RealtimeAuthorityImpl, RepoResolver,
RouteAdminState, RouteRepository, SandboxCeiling, ScriptRepository, SecretsConfig,
SecretsServiceImpl, SecretsState, SubscriberTokenConfig, TopicRepo, TopicsState, TriggerConfig,
TriggerRepo, TriggersState, UsersServiceConfig, UsersServiceImpl,
};
use picloud_orchestrator_core::realtime::DEFAULT_GC_INTERVAL_SECS;
use picloud_orchestrator_core::routing::{AppDomainTable, RouteTable};
@@ -146,7 +146,7 @@ pub async fn build_app(
outbox_repo.clone(),
));
let kv: Arc<dyn KvService> = Arc::new(KvServiceImpl::with_max_value_bytes(
kv_repo,
kv_repo.clone(),
authz.clone(),
events.clone(),
picloud_manager_core::kv_service::kv_max_value_bytes_from_env(),
@@ -239,8 +239,12 @@ pub async fn build_app(
secrets_config,
));
// v1.1.7 outbound email. Builds a lettre SMTP transport from
// PICLOUD_SMTP_* env (disabled mode + warning if unconfigured).
let email: Arc<dyn EmailService> = Arc::new(EmailServiceImpl::from_env(authz.clone()));
// PICLOUD_SMTP_* env (disabled mode + warning if unconfigured). G5: in
// dev mode with no relay, captures mail in memory instead of erroring;
// `dev_email_sink` is `Some` then, and we mount the dev inspection
// endpoint below.
let (email_impl, dev_email_sink) = EmailServiceImpl::from_env_with_dev_capture(authz.clone());
let email: Arc<dyn EmailService> = Arc::new(email_impl);
// v1.1.8 data-plane user management. Wires Argon2id-hashed user
// rows + SHA-256-hashed sliding-window sessions to the Rhai
// `users::*` namespace and the admin /apps/{id}/users HTTP surface.
@@ -290,8 +294,10 @@ pub async fn build_app(
// routes.
let route_table = Arc::new(RouteTable::new());
let initial = route_repo.list_all().await?;
let compiled = compile_routes(&initial)
.map_err(|e| anyhow::anyhow!("failed to compile stored routes: {e}"))?;
// Lenient: a single un-compilable stored route (e.g. one whose path
// became reserved under stricter validation) is skipped-with-warning
// inside compile_routes, never aborting boot. (H1)
let compiled = compile_routes(&initial);
route_table.replace_all(compiled);
// v1.1.9 function composition. InvokeServiceImpl resolves targets
@@ -380,6 +386,7 @@ pub async fn build_app(
principals,
executor: executor.clone(),
gate,
log_sink: log_sink.clone(),
inbox: inbox_resolver,
queue: queue_repo.clone(),
config: trigger_config,
@@ -544,7 +551,7 @@ pub async fn build_app(
// else under /admin gets the require_authenticated layer; capability
// checks live in each handler (after the resource is loaded so the
// capability binds to the resource's actual app_id).
let guarded_admin = Router::new()
let mut guarded_admin = Router::new()
.merge(admin_router(admin))
.merge(route_admin_router(route_admin))
.merge(admins_router(admins_state))
@@ -565,13 +572,24 @@ pub async fn build_app(
},
))
.merge(files_admin_router(files_admin_state))
.merge(kv_admin_router(KvAdminState {
kv: kv_repo.clone(),
apps: apps_repo.clone(),
authz: authz.clone(),
}))
.merge(topics_router(topics_state))
.merge(secrets_router(secrets_state))
.merge(dead_letters_router(dead_letters_state))
.layer(from_fn_with_state(
auth_state.clone(),
require_authenticated,
));
.merge(dead_letters_router(dead_letters_state));
// G5: dev-only mail inspection — mounted exactly when the email
// service is capturing in memory (dev mode + no relay). Same
// `require_authenticated` layer as the rest of /admin applies below.
if let Some(sink) = dev_email_sink {
guarded_admin = guarded_admin.merge(dev_emails_router(DevEmailState { sink }));
}
let guarded_admin = guarded_admin.layer(from_fn_with_state(
auth_state.clone(),
require_authenticated,
));
// Silence "unused import" lint on `apps_api` — we re-export via the
// facade above; the bare module path is retained so it's discoverable.