[package]
name = "noetl-worker"
version = "5.15.2"
edition = "2021"
license = "MIT"
repository = "https://github.com/noetl/noetl"
homepage = "https://noetl.io"
description = "NoETL Worker Pool - Executes workflow commands via NATS"

[[bin]]
name = "noetl-worker"
path = "src/main.rs"

[dependencies]
# Tool library.
#
# `noetl-tools = "2.18"` picks up:
# - `arrow_codec::try_encode_tabular_json` (2.10.0): the worker's
#   `build_call_done_result` calls it on the over-budget branch to
#   encode DuckDB / Postgres rowsets as Arrow IPC stream bytes
#   (media_type `application/vnd.apache.arrow.stream`) instead of
#   JSON, so cross-stack consumers can read the bytes with
#   `pyarrow.ipc.RecordBatchStreamReader` directly + colocated
#   consumers benefit from the columnar layout in the shm cache.
# - `ResultFetchTool` (2.11.0, noetl/tools#8): the playbook-facing
#   surface for explicit cross-step / cross-node fetches of a
#   `noetl://execution/<eid>/result/<step>/<id>` URI.  Tool kind
#   `result_fetch` is dispatched through the noetl-tools registry
#   when a playbook step declares it.  Per
#   `agents/rules/execution-model.md` fetch policy belongs in the
#   playbook layer; this tool is the surface authors invoke.
# - `NatsTool` (2.15.0, noetl/tools#12): JetStream / K/V / Object
#   Store operations dispatched via `tool_kind = "nats"`.  No
#   per-variant match arm needed — the dynamic registry handles it.
# - `McpTool` (2.16.0, noetl/tools#13): MCP JSON-RPC over HTTP bridge
#   dispatched via `tool_kind = "mcp"`.  Same dynamic-registry pattern.
# - `PythonTool` result-global capture (2.17.1, noetl/tools#17):
#   user code's `result = {...}` global is captured via a sentinel-
#   marker line on stdout and exposed as the tool's `data` field.
#   Without this, every playbook whose next.arcs / step.when guards
#   read step results would stall — the orchestrator couldn't
#   resolve `{{ step_name.field }}` references to a real value.
#   Surfaced via the Rust-only e2e probe on the Rust server (see
#   noetl/ai-meta#59 follow-up).
# - `TaskSequenceTool` (2.18.0, noetl/tools#18; breaking v3.0.0
#   noetl/tools#45): the runtime for the v10 list-shaped
#   `tool: [...]` pipeline.  Server emits `tool.kind =
#   task_sequence` for any pipeline-shape step (per
#   noetl/server#61's accepted-both-shapes parser); without this
#   tool the worker errored with "Tool not found: task_sequence"
#   and every pipeline-shape fixture stalled.  v3.0.0 replaces
#   `_prev`/`_results` injection with explicit `set:`/`input:`
#   forward-only data binding — each task declares `set:` to
#   capture `output` into named context variables, and later
#   tasks use `input:` to pull those variables.  No backward
#   compat fallback; all fixtures must use the new shape.
# - PostgresTool error detail (2.18.2, noetl/tools#21): the postgres
#   tool now surfaces the real SQLSTATE + message (e.g. `ERROR:
#   relation "..." does not exist (SQLSTATE 42P01)`) instead of the
#   opaque `db error` that tokio_postgres' Display renders.  Makes
#   failing-SQL playbook steps diagnosable from the event log.
#   (Cargo.lock now resolves noetl-tools 2.18.2; `^2.18` already
#   covered it.)
# - PostgresTool + DuckdbTool canonical v10 SQL shapes (2.18.3,
#   noetl/tools#23): the postgres tool accepts `command:` as an alias
#   for `query:` (canonical v10 postgres steps use `command:`, e.g.
#   v10_canonical_example.yaml), and both the postgres and duckdb tools
#   now run multi-statement SQL (`CREATE …; INSERT …; SELECT …`) by
#   batching the leading statements and keeping the final statement on
#   the typed single-statement path (so a trailing SELECT still returns
#   typed rows).  Without this, canonical postgres steps failed to parse
#   (`missing field query`) and multi-statement duckdb/postgres fixtures
#   errored on the single-statement prepared protocol.  Surfaced by the
#   Rust-only kind regression sweep for noetl/ai-meta#54.  (Cargo.lock
#   now resolves noetl-tools 2.18.3; `^2.18` already covered it.)
# - More canonical v10 config shapes (2.18.4, noetl/tools#26): the
#   duckdb tool accepts `command:` as an alias for `query:` (parity
#   with postgres), and the http tool coerces non-string `params` /
#   `headers` / `form` values — template rendering yields integers
#   (`offset: "{{ ctx.offset }}"`) or null (undefined ref), which the
#   `HashMap<String,String>` fields rejected with `invalid type:
#   integer/null, expected a string`.  Numbers/bools are stringified,
#   null entries dropped.  Unblocks the pagination + http fixtures.
#   (Cargo.lock now resolves noetl-tools 2.18.4; `^2.18` covers it.)
# - Dollar-quote-aware statement splitter (2.18.5, noetl/tools#27): the
#   multi-statement splitter added in 2.18.3 split on `;` inside
#   PostgreSQL `$$ … $$` dollar-quoted blocks, shredding plpgsql function
#   bodies / `DO` blocks (`unterminated dollar-quoted string`).  The
#   splitter now tracks dollar-quote state (`$$`/`$tag$`; `$1` params
#   excluded).  Surfaced by data_transfer/postgres_jsonb_test on the
#   Rust-only kind regression sweep (noetl/ai-meta#54).  (Cargo.lock now
#   resolves noetl-tools 2.18.5; `^2.18` covers it.)
# - GCP Secret Manager provider (2.19.0, noetl/tools#29): the
#   `secrets` / `secret_manager` tools now dispatch on a `provider` field
#   to a backend behind a `SecretProvider` trait — `env` (unchanged) or
#   `gcp` (Google Secret Manager via ambient GKE Workload-Identity token).
#   Secrets Wallet Phase 3a (noetl/ai-meta#61).  (Cargo.lock now resolves
#   noetl-tools 2.19.0; `^2.18` covers it.)
# - Multi-tool sibling references (3.1.1, noetl/ai-meta#87): in a
#   `tool: [list]` step, a later sub-tool can read an earlier sibling's
#   output via `{{ <label>.<field> }}` (was rendering empty).  (Cargo.lock
#   now resolves noetl-tools 3.1.1; `^3` covers it.)
noetl-tools = "3"

# Shared execution core -- types and utilities shared with the noetl
# CLI's local-mode runner.  Adoptions:
# - `condition::{Operator, Condition, evaluate_structured_condition}`
#   in place of the case_evaluator's inline duplicates (R-1.2 PR-2c).
# - `worker::source::{CommandSource, ClaimOutcome, Pulled, Command}`
#   in `nats::source::NatsCommandSource` (R-1.2 PR-2d-2).
# - `events::ExecutorEvent` as the wire-format event envelope sent
#   to `/api/events`, replacing the worker-local `WorkerEvent`
#   (R-1.2 PR-EE-3, this PR).  Both servers (Rust noetl-server +
#   Python broker) accept this shape after EE-2 + EE-4.
# See Appendix H of the global hybrid cloud blueprint at
# https://noetl.dev/docs/architecture/noetl_global_hybrid_cloud_grid_distributed_architecture_blueprint
# and especially § H.10 (the tree-walker vs pull-model finding).
noetl-executor = "0.5"

# Same-node zero-copy IPC cache for over-budget tool results
# (R-2.1 / noetl/cli#39).  When `call.done`'s serialised
# `result.context` exceeds the broker's 100KB inline budget,
# the worker stages the bytes in shared memory + emits an
# `IpcHint` reference instead of dropping the context entirely.
# See `executor::command::build_call_done_result` for the
# fallback chain.
noetl-arrow-cache = "0.1"

# Async runtime
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "time", "sync"] }

# Async trait macro — required for impl'ing noetl_executor's
# `worker::source::CommandSource` (the trait uses #[async_trait]).
async-trait = "0.1"

# NATS messaging
async-nats = "0.38"
futures = "0.3"

# HTTP client
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }

# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Error handling
thiserror = "2.0"
anyhow = "1.0"

# Regex
regex = "1.10"

# Secrets Wallet Phase 5c (noetl/ai-meta#61) — worker-side sealed credential
# delivery.  Long-lived X25519 StaticSecret registered as the recipient
# pubkey; ChaCha20-Poly1305 + HKDF-SHA256 unseal the server's SealedEnvelope
# (mirror of noetl/server's src/crypto/sealed.rs primitives).  base64 for
# the wire-shape decode; zeroize for explicit cleartext wipe after the
# caller's tool dispatch returns.
x25519-dalek = { version = "2", features = ["static_secrets"] }
chacha20poly1305 = "0.10"
hkdf = "0.12"
sha2 = "0.10"
base64 = "0.22"
zeroize = "1.7"
rand_core = "0.6"

# Utilities
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }

# Observability — per agents/rules/observability.md (codified
# 2026-05-30).  `prometheus` provides the metrics registry + the
# Prometheus text format encoder; `axum` exposes the `/metrics`
# endpoint on a dedicated port (default 9090) so sidecar scrapers
# can pull without going through the main control-plane traffic.
prometheus = "0.14"
axum = "0.8"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
dotenvy = "0.15"
hostname = "0.4"

# URL parsing for NATS_URL credential extraction.  Honors
# `nats://user:pass@host` inline auth (the convention the Python
# worker + most NATS clients use); async-nats 0.38's `connect(url)`
# only consumes the addr portion, so we extract creds ourselves
# and feed `ConnectOptions::with_user_and_password`.
url = "2"
urlencoding = "2"

[dev-dependencies]
tokio-test = "0.4"
# uuid used in integration tests (dispatch_nats_mcp.rs) to generate unique
# NATS KV bucket names so concurrent test runs don't collide.
uuid = { version = "1", features = ["v4"] }
