fix(shared): F-Q-004 unify error variants — Forbidden on QueueError, rename Unavailable→Backend

Brings QueueError / PubsubError / InvokeError in line with sibling
shape used by KvError / DocsError / FilesError / SecretsError:

- QueueError gains an explicit Forbidden variant (previously authz
  denial was squashed into Rejected("forbidden") in queue_service.rs:68,
  losing the structured variant a 403-translation layer would need).
- QueueError::Unavailable renamed → Backend.
- PubsubError::Unavailable renamed → Backend.
- InvokeError::Unavailable renamed → Backend.
- Call sites updated (queue_service, pubsub_service, invoke_service,
  Noop* stubs, From<PubsubRepoError> impl, one test assertion).
- New unit test verifying authed-denied → QueueError::Forbidden.

AUDIT.md anchor: F-Q-004.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 19:44:23 +02:00
parent 450badaabf
commit b192cf2cc9
6 changed files with 78 additions and 25 deletions

View File

@@ -105,9 +105,10 @@ pub enum InvokeError {
#[error("invoke rejected: {0}")]
Rejected(String),
/// Backend / database error.
/// Backend / database error. Named `Backend` to align with KvError /
/// DocsError / FilesError / SecretsError / QueueError / PubsubError.
#[error("invoke backend error: {0}")]
Unavailable(String),
Backend(String),
}
/// Test-only stub: every call errors. Lets harnesses build a `Services`
@@ -122,7 +123,7 @@ impl InvokeService for NoopInvokeService {
_cx: &SdkCallCx,
_target: InvokeTarget,
) -> Result<ResolvedScript, InvokeError> {
Err(InvokeError::Unavailable("invoke is not wired in".into()))
Err(InvokeError::Backend("invoke is not wired in".into()))
}
async fn enqueue_async(
@@ -131,6 +132,6 @@ impl InvokeService for NoopInvokeService {
_target: InvokeTarget,
_args: serde_json::Value,
) -> Result<ExecutionId, InvokeError> {
Err(InvokeError::Unavailable("invoke is not wired in".into()))
Err(InvokeError::Backend("invoke is not wired in".into()))
}
}

View File

@@ -52,7 +52,7 @@ pub trait PubsubService: Send + Sync {
ttl_seconds: Option<i64>,
) -> Result<String, PubsubError> {
let _ = (cx, topics, ttl_seconds);
Err(PubsubError::Unavailable(
Err(PubsubError::Backend(
"subscriber tokens are not wired in".into(),
))
}
@@ -80,9 +80,10 @@ pub enum PubsubError {
#[error("{0}")]
SubscriberToken(String),
/// Anything else — Postgres unavailable, etc.
/// Anything else — Postgres unavailable, etc. Named `Backend` to
/// align with KvError / DocsError / FilesError / SecretsError.
#[error("pubsub backend error: {0}")]
Unavailable(String),
Backend(String),
}
/// Match a stored `topic_pattern` against a published `topic`.
@@ -145,7 +146,7 @@ impl PubsubService for NoopPubsubService {
_topic: &str,
_message: serde_json::Value,
) -> Result<(), PubsubError> {
Err(PubsubError::Unavailable("pubsub is not wired in".into()))
Err(PubsubError::Backend("pubsub is not wired in".into()))
}
}

View File

@@ -61,6 +61,13 @@ pub enum QueueError {
#[error("queue_name must not be empty")]
EmptyName,
/// Caller principal lacked the required capability. Only raised when
/// `cx.principal.is_some()` (script-as-gate; public HTTP skips it).
/// Sibling shape across KvError / DocsError / PubsubError — uniform
/// for 403 translation at the HTTP boundary.
#[error("forbidden")]
Forbidden,
/// Payload could not be serialized (e.g. a Rhai FnPtr / closure
/// captured into the message). Rejected at the SDK boundary; surface
/// the wording verbatim so scripts see the documented message.
@@ -71,9 +78,10 @@ pub enum QueueError {
#[error("{0}")]
InvalidOpts(String),
/// Backend unavailable (Postgres down, etc.).
/// Backend failure (Postgres unavailable, etc.). Named `Backend` to
/// align with KvError / DocsError / FilesError / SecretsError.
#[error("queue backend error: {0}")]
Unavailable(String),
Backend(String),
}
/// Test-only stub: every call errors `Unavailable`. Used by harnesses
@@ -90,14 +98,14 @@ impl QueueService for NoopQueueService {
_payload: serde_json::Value,
_opts: EnqueueOpts,
) -> Result<QueueMessageId, QueueError> {
Err(QueueError::Unavailable("queue is not wired in".into()))
Err(QueueError::Backend("queue is not wired in".into()))
}
async fn depth(&self, _cx: &SdkCallCx, _queue_name: &str) -> Result<u64, QueueError> {
Err(QueueError::Unavailable("queue is not wired in".into()))
Err(QueueError::Backend("queue is not wired in".into()))
}
async fn depth_pending(&self, _cx: &SdkCallCx, _queue_name: &str) -> Result<u64, QueueError> {
Err(QueueError::Unavailable("queue is not wired in".into()))
Err(QueueError::Backend("queue is not wired in".into()))
}
}