feat(executor-core): plumb app_id/principal/depth through ExecRequest
Adds the four internal-only fields every v1.1.x stateful service needs
to isolate by app and audit by caller:
- app_id — owning app for this invocation
- principal — Option<Principal>; data-plane is unauthenticated
today so the orchestrator passes None until the
opportunistic middleware lands in the next commit
- trigger_depth — 0 for direct invocations; the triggers framework
(v1.1.1) bounds runaway feedback loops via this
- root_execution_id — equal to execution_id for direct invocations;
preserved across trigger fan-out for audit grouping
ExecRequest stays serializable (cluster mode still has to ship it across
processes when v1.3+ arrives). principal is `#[serde(skip)]` because
shared::Principal has no wire derivation today — when cluster mode lands
the wire-Principal question gets revisited properly.
Engine now carries a Services bundle (empty in v1.1.0). Engine::execute
constructs an SdkCallCx from the request and hands it to sdk::register_all
just after the per-call Rhai engine is built. The hook is a no-op in v1.1.0;
v1.1.1 KV registers its first native fns there.
Adds ExecError::Overloaded { retry_after_secs } and the matching 503 +
Retry-After mapping in orchestrator-core's IntoResponse. The gate that
actually produces this variant lands in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,8 @@ use axum::{
|
||||
use chrono::Utc;
|
||||
use picloud_executor_core::{ExecError, ExecRequest, ExecResponse, InvocationType};
|
||||
use picloud_shared::{
|
||||
AppId, ExecutionId, ExecutionLog, ExecutionLogSink, ExecutionStatus, RequestId, ScriptId,
|
||||
AppId, ExecutionId, ExecutionLog, ExecutionLogSink, ExecutionStatus, Principal, RequestId,
|
||||
ScriptId,
|
||||
};
|
||||
use serde_json::Value as Json_;
|
||||
use uuid::Uuid;
|
||||
@@ -97,7 +98,10 @@ where
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound(id))?;
|
||||
|
||||
let mut req = build_exec_request(id, &script.name, &headers, &body)?;
|
||||
// Principal stays `None` until the data-plane `attach_principal_if_present`
|
||||
// middleware lands in the picloud-wiring commit. Both shapes are
|
||||
// valid against `ExecRequest.principal: Option<Principal>`.
|
||||
let mut req = build_exec_request(id, &script.name, &headers, &body, script.app_id, None)?;
|
||||
req.sandbox_overrides = script.sandbox;
|
||||
let request_id = req.request_id;
|
||||
let request_path = req.path.clone();
|
||||
@@ -195,6 +199,8 @@ where
|
||||
&script.name,
|
||||
&headers,
|
||||
&body_bytes,
|
||||
app_id,
|
||||
None,
|
||||
)?;
|
||||
req.path = path;
|
||||
req.params = matched.params;
|
||||
@@ -264,6 +270,8 @@ fn build_exec_request(
|
||||
name: &str,
|
||||
headers: &HeaderMap,
|
||||
body: &Bytes,
|
||||
app_id: AppId,
|
||||
principal: Option<Principal>,
|
||||
) -> Result<ExecRequest, ApiError> {
|
||||
let mut hmap = BTreeMap::new();
|
||||
for (k, v) in headers {
|
||||
@@ -279,8 +287,9 @@ fn build_exec_request(
|
||||
.map_err(|e| ApiError::BadRequest(format!("invalid JSON body: {e}")))?
|
||||
};
|
||||
|
||||
let execution_id = ExecutionId::new();
|
||||
Ok(ExecRequest {
|
||||
execution_id: ExecutionId::new(),
|
||||
execution_id,
|
||||
request_id: RequestId::new(),
|
||||
script_id: id,
|
||||
script_name: name.to_string(),
|
||||
@@ -293,6 +302,13 @@ fn build_exec_request(
|
||||
rest: String::new(),
|
||||
// Overwritten by the handler after the script is resolved.
|
||||
sandbox_overrides: picloud_shared::ScriptSandbox::default(),
|
||||
app_id,
|
||||
principal,
|
||||
// Direct invocations are at depth 0 with a self-referential
|
||||
// root. The triggers framework (v1.1.1) increments depth and
|
||||
// preserves the original root for chained executions.
|
||||
trigger_depth: 0,
|
||||
root_execution_id: execution_id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -396,6 +412,21 @@ pub enum ApiError {
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
// Overloaded is the only variant that needs to attach an HTTP
|
||||
// header (Retry-After), so it short-circuits the (status, body)
|
||||
// reduction below. Axum's tuple builder makes per-arm header
|
||||
// injection awkward otherwise.
|
||||
if let ApiError::Exec(ExecError::Overloaded { retry_after_secs }) = &self {
|
||||
let retry = retry_after_secs.to_string();
|
||||
let body = Json(serde_json::json!({ "error": self.to_string() }));
|
||||
return (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
[(axum::http::header::RETRY_AFTER, retry)],
|
||||
body,
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
use ApiError as E;
|
||||
let (status, message) = match &self {
|
||||
E::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
|
||||
@@ -416,6 +447,7 @@ impl IntoResponse for ApiError {
|
||||
(StatusCode::INSUFFICIENT_STORAGE, e.to_string())
|
||||
}
|
||||
ExecError::Runtime(_) => (StatusCode::BAD_GATEWAY, e.to_string()),
|
||||
ExecError::Overloaded { .. } => unreachable!("handled above"),
|
||||
},
|
||||
};
|
||||
(status, Json(serde_json::json!({ "error": message }))).into_response()
|
||||
|
||||
Reference in New Issue
Block a user