Skip to content

pem: direct-query gRPC endpoint — stub + TDD contract#49

Merged
entlein merged 21 commits into
mainfrom
entlein/pem-direct-query
Jun 22, 2026
Merged

pem: direct-query gRPC endpoint — stub + TDD contract#49
entlein merged 21 commits into
mainfrom
entlein/pem-direct-query

Conversation

@entlein

@entlein entlein commented Jun 4, 2026

Copy link
Copy Markdown

Summary: PEM direct-query gRPC endpoint (entlein/dx#29). Make the metadata-connected vizier-pem serve api.vizierpb.VizierService.ExecuteScript directly over gRPC, JWT-authenticated, so dx queries the node-local PEM with no broker hop. Ports the standalone_pem capability with two upgrades: metadata-connected reuse of the live PEM Carnot + agent metadata (closes #15), and HS256 service-token auth via the cluster jwt-signing-key (no longer insecure). Drain fix (execution_and_timing_info -> QueryData.execution_stats wire-roundtrip + skip payload-less responses) closes the unimplemented type stream error caught on live PEM soak. Fail-soft direct-query startup (try/catch + step 1/6 to 6/6 breadcrumbs) so init failure can never crashloop the data plane. Manager::Init refuses to start with empty PL_JWT_SIGNING_KEY (catches the jwt::SigningError that crashloop'd the stock 0.14.17 PEM).

Relevant Issues: entlein/dx#29 (PEM direct-query)

Type of change: /kind feature

Test Plan: bazel test //src/vizier/services/agent/pem:direct_query_server_test (auth-negative + ValidToken_TrivialQuery_StreamsRows green); bazel test //src/vizier/services/agent/shared/manager/... (5/5 pass, JWT guard wired through Manager::Init); vizier-release CI builds pemdq6 image (commit 50dffb0 / tag release/vizier/v0.14.19-pemdq6); live PG soak

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@entlein, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 54 minutes and 57 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f8d45aaa-ed68-49cf-9871-b80c5c26bf56

📥 Commits

Reviewing files that changed from the base of the PR and between e075700 and c357699.

📒 Files selected for processing (18)
  • .github/workflows/vizier_release.yaml
  • src/api/go/pxapi/opts.go
  • src/carnot/BUILD.bazel
  • src/carnot/exec/BUILD.bazel
  • src/carnot/udf/BUILD.bazel
  • src/stirling/source_connectors/socket_tracer/testing/container_images/BUILD.bazel
  • src/utils/shared/k8s/apply.go
  • src/utils/shared/k8s/delete.go
  • src/vizier/services/agent/pem/BUILD.bazel
  • src/vizier/services/agent/pem/DIRECT_QUERY_CONTRACT.md
  • src/vizier/services/agent/pem/DIRECT_QUERY_SECURITY.md
  • src/vizier/services/agent/pem/direct_query_server.cc
  • src/vizier/services/agent/pem/direct_query_server.h
  • src/vizier/services/agent/pem/direct_query_server_test.cc
  • src/vizier/services/agent/pem/pem_main.cc
  • src/vizier/services/agent/pem/pem_manager.cc
  • src/vizier/services/agent/pem/pem_manager.h
  • src/vizier/services/agent/shared/manager/manager.cc
📝 Walkthrough

Walkthrough

Adds a PEM direct-query ExecuteScript gRPC endpoint with HS256 JWT bearer-token verification, server-streaming query execution, comprehensive in-process tests, PEM runtime wiring, Bazel visibility and dependency updates, and minor utility/CI tweaks.

Changes

PEM Direct-Query Feature

Layer / File(s) Summary
Direct-query contract and interface
src/vizier/services/agent/pem/DIRECT_QUERY_CONTRACT.md, src/vizier/services/agent/pem/direct_query_server.h
Adds contract doc specifying gRPC TLS, feature flag gating, JWT authentication, server-streaming with mutation rejection, and dx-agent integration. Header declares AuthenticateRequest helper returning OK only for valid HS256 tokens and DirectQueryServer class with public constructor taking carnot::Carnot*, carnot::EngineState*, LocalGRPCResultSinkServer*, and JWT signing key; ExecuteScript RPC declared for streaming responses.
Server implementation, auth, and streaming
src/vizier/services/agent/pem/direct_query_server.cc
Implements case-insensitive Bearer token extraction, RFC 7515 base64url decoding, HS256 HMAC-SHA256 signature verification with constant-time comparison, and claim validation (aud string/array, iss, numeric exp checking, required Scopes with service scope). AuthenticateRequest fail-closed returns UNAUTHENTICATED with VLOG details on failure. ExecuteScript authenticates first, rejects mutations with UNIMPLEMENTED, validates Carnot wiring, compiles PxL, emits schema-only responses, serializes execution with mutex, executes query, and streams drained sink results; includes disabled-build stubs for compile-time kill switch.
In-process contract and execution tests
src/vizier/services/agent/pem/direct_query_server_test.cc
Adds MakeBearerToken minting HS256 tokens with claim variants (valid, expired, wrong key, algorithm confusion, malformed aud/iss/exp, missing scope). DirectQueryServerTest fixture runs in-process server with no Carnot; tests assert UNAUTHENTICATED for missing/wrong/tampered tokens and UNIMPLEMENTED for mutations. DirectQueryServerExecTest builds real Carnot/local execution environment with inline schemas, wires gRPC server, and tests end-to-end streaming, concurrency, sequential reuse, broker independence, feature-toggle short-circuit, and robustness (invalid PxL, unknown tables).
PEM runtime wiring and flags
src/vizier/services/agent/pem/pem_manager.cc, src/vizier/services/agent/pem/pem_manager.h, src/vizier/services/agent/pem/pem_main.cc, src/vizier/services/agent/shared/manager/manager.cc
Adds gflags (direct_query_enabled, direct_query_port, direct_query_jwt_signing_key) gated by compile-time flag. PEMManager::MaybeStartDirectQueryServer constructs local Carnot/UDF, registers DirectQueryServer, starts gRPC server on 0.0.0.0:<port> via SSL::DefaultGRPCServerCreds; fail-soft on errors. PEMManager::StopDirectQueryServer shuts down server and resets objects. Lifecycle hooks wired to PostRegisterHookImpl and StopImpl. Manager startup validates JWT signing key non-empty.
Bazel build, test target, and visibility
src/vizier/services/agent/pem/BUILD.bazel, src/carnot/BUILD.bazel, src/carnot/exec/BUILD.bazel, src/carnot/udf/BUILD.bazel, src/vizier/services/agent/pem/DIRECT_QUERY_SECURITY.md
Adds config_setting(name = "direct_query_disabled", ...) and updates pl_cc_library with conditional defines and expanded deps (Vizier proto, //src/carnot headers/impl, @boringssl//:crypto, gRPC++, rapidjson, sole). Adds pl_cc_test(name = "direct_query_server_test", ...) target. Exports local_grpc_result_server.h from Carnot exec. Expands visibility labels to allow //src/vizier/services/agent/pem:__pkg__ on Carnot modules. Adds detailed security spec documenting signing-key flow, threat model, and test-locked tampering expectations.
K8s utilities, client options, and CI runner updates
src/utils/shared/k8s/apply.go, src/utils/shared/k8s/delete.go, src/stirling/source_connectors/socket_tracer/testing/container_images/BUILD.bazel, .github/workflows/vizier_release.yaml, src/api/go/pxapi/opts.go
Reorders k8s import (k8serrors before meta); migrates sets.Stringsets.Set[string] usages and set constructors; reorders go_container_libraries Bazel arguments (no functional change); adds WithDirectTLSSkipVerify() client option for direct endpoint TLS cert skipping; updates vizier_release runs-on runners to oracle-vm-16cpu-64gb-x86-64.

Sequence Diagram

sequenceDiagram
  participant Client
  participant DirectQueryServer
  participant JWTVerifier
  participant Carnot
  participant ResultSink
  Client->>DirectQueryServer: ExecuteScriptRequest with Bearer token
  DirectQueryServer->>JWTVerifier: AuthenticateRequest(authorization header)
  JWTVerifier->>JWTVerifier: Extract and base64url decode JWT parts
  JWTVerifier->>JWTVerifier: Verify HS256 HMAC-SHA256 signature
  JWTVerifier->>JWTVerifier: Validate claims (aud, iss, exp, Scopes)
  alt Auth failure
    JWTVerifier-->>DirectQueryServer: UNAUTHENTICATED
    DirectQueryServer-->>Client: UNAUTHENTICATED
  else Auth success
    JWTVerifier-->>DirectQueryServer: OK
    DirectQueryServer->>Carnot: Compile PxL query
    alt Compile failure
      Carnot-->>DirectQueryServer: error
      DirectQueryServer-->>Client: INVALID_ARGUMENT
    else Compile success
      Carnot-->>DirectQueryServer: compiled plan with sinks
      DirectQueryServer->>ResultSink: ResetQueryResults()
      DirectQueryServer->>Carnot: ExecuteQuery()
      loop Drain results
        ResultSink-->>DirectQueryServer: ExecuteScriptResponse (schema/rows/stats)
        DirectQueryServer-->>Client: stream response
      end
      DirectQueryServer-->>Client: OK (end stream)
    end
  end
Loading

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.60% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: adding a direct-query gRPC endpoint to PEM with JWT authentication and comprehensive test coverage.
Description check ✅ Passed The description is clearly related to the changeset, explaining the PEM direct-query gRPC endpoint implementation with JWT authentication, configuration, and testing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch entlein/pem-direct-query

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vizier/services/agent/pem/direct_query_server_test.cc`:
- Around line 58-67: The test ValidToken_Mutation_Unimplemented is unreachable
because MakeBearerToken returns placeholder non-JWT strings and
AuthenticateRequest always returns UNAUTHENTICATED; change the test surface so a
"valid" token is actually recognized: either make MakeBearerToken produce a
token format that AuthenticateRequest will accept as valid (e.g., generate a
simple signed JWT or a recognized test token when TokenKind::kValid) or adjust
AuthenticateRequest (in direct_query_server.cc's AuthenticateRequest) to accept
the test placeholder for TokenKind::kValid; update MakeBearerToken's TokenKind
cases (kValid, kWrongKey, kExpired) to return distinct tokens that map to
AuthenticateRequest's logic so the ValidToken_Mutation_Unimplemented path can
observe UNIMPLEMENTED as intended.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 649f9e15-2555-4317-9973-b200df724e3d

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2721f and 7c717c5.

📒 Files selected for processing (5)
  • src/vizier/services/agent/pem/BUILD.bazel
  • src/vizier/services/agent/pem/DIRECT_QUERY_CONTRACT.md
  • src/vizier/services/agent/pem/direct_query_server.cc
  • src/vizier/services/agent/pem/direct_query_server.h
  • src/vizier/services/agent/pem/direct_query_server_test.cc

Comment thread src/vizier/services/agent/pem/direct_query_server_test.cc
coderabbitai[bot]

This comment was marked as spam.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/vizier/services/agent/pem/direct_query_server.cc (1)

175-218: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Still missing the cluster-service claim checks.

This verifier still accepts any HS256 token signed with the shared key and aud=="vizier". That is broader than the stated cluster-service JWT contract, because iss, sub, Scopes, and ServiceID are never enforced here.

Suggested tightening
   if (!aud_ok) {
     return ::grpc::Status(::grpc::StatusCode::UNAUTHENTICATED,
                           "direct-query: wrong audience (expected vizier)");
   }
+  if (!payload.HasMember("iss") || !payload["iss"].IsString() ||
+      std::strcmp(payload["iss"].GetString(), "PL") != 0 ||
+      !payload.HasMember("sub") || !payload["sub"].IsString() ||
+      std::strcmp(payload["sub"].GetString(), "service") != 0 ||
+      !payload.HasMember("Scopes") || !payload["Scopes"].IsString() ||
+      std::strcmp(payload["Scopes"].GetString(), "service") != 0 ||
+      !payload.HasMember("ServiceID") || !payload["ServiceID"].IsString() ||
+      payload["ServiceID"].GetStringLength() == 0) {
+    return ::grpc::Status(::grpc::StatusCode::UNAUTHENTICATED,
+                          "direct-query: not a cluster service JWT");
+  }
   if (!payload.HasMember("exp")) {
     return ::grpc::Status(::grpc::StatusCode::UNAUTHENTICATED, "direct-query: missing exp claim");
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vizier/services/agent/pem/direct_query_server.cc` around lines 175 - 218,
The verifier currently only checks aud and exp but must enforce the
cluster-service JWT contract: after the aud check and before exp validation, add
explicit checks on payload for "iss" (must be string == kExpectedIssuer), "sub"
(must be string == kExpectedSubject), "Scopes" (must contain the cluster-service
scope, e.g., check string or array includes kClusterServiceScope), and
"ServiceID" (must exist and match the expected service/cluster id, e.g.,
kExpectedServiceID or the runtime cluster id). For each missing or mismatched
claim return ::grpc::Status(::grpc::StatusCode::UNAUTHENTICATED, "<contextual
message>") similar to the existing messages; use the existing payload variable
and keep the new constants kExpectedIssuer, kExpectedSubject,
kClusterServiceScope, kExpectedServiceID (or existing equivalents) to locate
where to enforce these checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vizier/services/agent/pem/direct_query_server_test.cc`:
- Around line 196-236: The test uses an empty http_events table so it never
exercises the drainSinkAndStream() column-copy path; seed at least one row into
the table (e.g., in MakeHTTPEventsTable() or in
TEST_F(DirectQueryServerExecTest, ValidToken_TrivialQuery_StreamsRows) before
calling stub_->ExecuteScript) using the Table API for "http_events" (populate
fields time_, upid, remote_addr, remote_port, trace_role), then when streaming
responses from stub_->ExecuteScript assert that you observe a streamed batch
with resp.has_table_id() == true (or matches the expected table id) and
resp.num_rows() > 0 to validate the non-empty row-batch path is exercised.

---

Duplicate comments:
In `@src/vizier/services/agent/pem/direct_query_server.cc`:
- Around line 175-218: The verifier currently only checks aud and exp but must
enforce the cluster-service JWT contract: after the aud check and before exp
validation, add explicit checks on payload for "iss" (must be string ==
kExpectedIssuer), "sub" (must be string == kExpectedSubject), "Scopes" (must
contain the cluster-service scope, e.g., check string or array includes
kClusterServiceScope), and "ServiceID" (must exist and match the expected
service/cluster id, e.g., kExpectedServiceID or the runtime cluster id). For
each missing or mismatched claim return
::grpc::Status(::grpc::StatusCode::UNAUTHENTICATED, "<contextual message>")
similar to the existing messages; use the existing payload variable and keep the
new constants kExpectedIssuer, kExpectedSubject, kClusterServiceScope,
kExpectedServiceID (or existing equivalents) to locate where to enforce these
checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 325769b9-0ed7-4ca0-8704-52d0312dd1e0

📥 Commits

Reviewing files that changed from the base of the PR and between 00e198c and b409464.

📒 Files selected for processing (10)
  • .github/workflows/vizier_release.yaml
  • src/carnot/BUILD.bazel
  • src/carnot/exec/BUILD.bazel
  • src/carnot/udf/BUILD.bazel
  • src/vizier/services/agent/pem/BUILD.bazel
  • src/vizier/services/agent/pem/direct_query_server.cc
  • src/vizier/services/agent/pem/direct_query_server_test.cc
  • src/vizier/services/agent/pem/pem_main.cc
  • src/vizier/services/agent/pem/pem_manager.cc
  • src/vizier/services/agent/pem/pem_manager.h

Comment thread src/vizier/services/agent/pem/direct_query_server_test.cc
ConstanzeTU pushed a commit that referenced this pull request Jun 4, 2026
dx-agent observed the stock fork 0.14.17 PEM in CrashLoopBackOff (23
restarts over hours) with:
  libc++abi: terminating due to uncaught exception of type
  jwt::SigningError: key not provided

Root cause: src/vizier/services/agent/shared/manager/manager.cc:434 calls
`obj.secret(FLAGS_jwt_signing_key); obj.signature();` in
GenerateServiceToken. cpp_jwt's signature() throws SigningError when the
secret is empty. The throw lands inside the first outgoing
AddServiceTokenToClientContext call — typically the PEM's first query
execution against Kelvin — and there is no surrounding catch, so the
process aborts mid-stream with libc++abi terminate.

Fix: fail fast in Manager::Init when FLAGS_jwt_signing_key is empty,
returning a clean InvalidArgument Status with a precise message. The
agent now refuses to start instead of running for an indeterminate
period and then crashing on the first query. Lives in the shared base
so it covers Kelvin + PEM both. Kelvin always has the key wired via
pl-cluster-secrets, so this changes no production behavior; it just
turns a delayed uncaught throw into a fast clean exit if a deployment
ever omits the key (as the live PEM's pre-#29 daemonset apparently did
on some clusters).

Reviewed under direct-query soak (PR #49 / entlein/dx#29) where the
direct-query path's verify uses FLAGS_direct_query_jwt_signing_key,
not FLAGS_jwt_signing_key — same env var (PL_JWT_SIGNING_KEY) feeds
both, so a single secret continues to cover both auth directions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/vizier/services/agent/pem/direct_query_server.cc (1)

411-422: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Serialize sink reset/execute/drain to prevent cross-request result corruption.

result_server_ is shared state, and this method resets plus drains global accumulated chunks. Concurrent ExecuteScript calls can clobber each other and stream mixed results.

Suggested minimal guard (serialize direct-query execution path)
+#include "absl/synchronization/mutex.h"
...
 namespace {
+absl::Mutex g_direct_query_exec_mu;
 }  // namespace
...
 ::grpc::Status DirectQueryServer::ExecuteScript(
     ::grpc::ServerContext* context, const ::px::api::vizierpb::ExecuteScriptRequest* request,
     ::grpc::ServerWriter<::px::api::vizierpb::ExecuteScriptResponse>* writer) {
+  absl::MutexLock lk(&g_direct_query_exec_mu);
   if (auto s = AuthenticateRequest(context, jwt_signing_key_); !s.ok()) {
     return s;
   }
...
   result_server_->ResetQueryResults();
   auto exec_s = carnot_->ExecuteQuery(request->query_str(), query_id, ::px::CurrentTimeNS());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vizier/services/agent/pem/direct_query_server.cc` around lines 411 - 422,
This RPC resets and reads the shared result sink (result_server_) around
carnot_->ExecuteQuery and drainSinkAndStream, so concurrent
ExecuteScript/ExecuteQuery calls can interleave and corrupt results; serialize
the sequence by introducing a mutex (e.g., a class member direct_query_mu_) and
acquire a std::lock_guard (or std::unique_lock) at the start of the method that
surrounds result_server_->ResetQueryResults(), the call to
carnot_->ExecuteQuery(...), and drainSinkAndStream(result_server_, query_id_str,
writer) so the reset/execute/drain happens atomically for a single request.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vizier/services/agent/pem/pem_manager.cc`:
- Around line 39-42: The code currently uses FLAGS_direct_query_jwt_signing_key
separately from FLAGS_jwt_signing_key causing split-brain; change the logic in
pem_manager.cc to treat FLAGS_direct_query_jwt_signing_key as optional and fall
back to FLAGS_jwt_signing_key when empty (i.e., wherever
FLAGS_direct_query_jwt_signing_key is read—references around the DEFINE_string
and the usages near the blocks you noted at lines ~121-125 and ~161-163—use a
single effective key variable like effective_direct_query_key =
FLAGS_direct_query_jwt_signing_key.empty() ? FLAGS_jwt_signing_key :
FLAGS_direct_query_jwt_signing_key and use that variable for direct-query
auth/minting).

---

Outside diff comments:
In `@src/vizier/services/agent/pem/direct_query_server.cc`:
- Around line 411-422: This RPC resets and reads the shared result sink
(result_server_) around carnot_->ExecuteQuery and drainSinkAndStream, so
concurrent ExecuteScript/ExecuteQuery calls can interleave and corrupt results;
serialize the sequence by introducing a mutex (e.g., a class member
direct_query_mu_) and acquire a std::lock_guard (or std::unique_lock) at the
start of the method that surrounds result_server_->ResetQueryResults(), the call
to carnot_->ExecuteQuery(...), and drainSinkAndStream(result_server_,
query_id_str, writer) so the reset/execute/drain happens atomically for a single
request.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 271756a5-b762-4b8d-9811-f5b171d98116

📥 Commits

Reviewing files that changed from the base of the PR and between b409464 and 50dffb0.

📒 Files selected for processing (4)
  • src/vizier/services/agent/pem/direct_query_server.cc
  • src/vizier/services/agent/pem/pem_main.cc
  • src/vizier/services/agent/pem/pem_manager.cc
  • src/vizier/services/agent/shared/manager/manager.cc

Comment thread src/vizier/services/agent/pem/pem_manager.cc Outdated
ConstanzeTU pushed a commit that referenced this pull request Jun 5, 2026
dx-agent caught aeprod1 (run 26982157827) sitting `queued` 5h with
`runner_name:""` — no runner carries `oracle-16cpu-64gb-x86-64` or
`oracle-8cpu-32gb-x86-64` on this fork. The active fleet uses the
`-vm-` form: `oracle-vm-16cpu-64gb-x86-64`. PR #49 (PEM branch)
already shipped this fix; the AE branch missed it because of when it
forked from main.

Aligns both jobs: `build-release` (was `oracle-16cpu`) and
`update-gh-artifacts-manifest` (was `oracle-8cpu`). Cancelled run
was confirmed dead by dx-agent.
ConstanzeTU pushed a commit that referenced this pull request Jun 5, 2026
Three PR-checks were failing:

1. run-container-lint (cfmt) — pem_manager.cc had a two-line LOG that
   clang-format wants on one line. `arc lint --apply-patches` autofixed
   the step 6/6 LOG(INFO) wrap. No behavioral change.

2. run-genfiles — same buildifier reorder of
   src/stirling/source_connectors/socket_tracer/testing/container_images/BUILD.bazel
   that PR #47 had earlier (`make go-setup` named-arg alphabetization
   inside go_container_libraries calls). Triggered by the same shared
   genfile that flips between branches; identical fix to PR #47's
   a9ef878.

3. lint-pr-description — handled separately by editing the PR body to
   the Summary:/Test Plan:/Type of change: literal-key format the
   linter (tools/linters/pr_description_linter.sh) requires (was
   markdown `## Summary` headers, which the script's `^Summary: .+`
   regex doesn't match). No commit needed for that one.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/vizier/services/agent/pem/direct_query_server.cc (1)

301-302: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop work when stream writes fail.

In src/vizier/services/agent/pem/direct_query_server.cc, the ServerWriter::Write(...) return values are ignored at writer->Write(schema_resp); (around lines 301) and writer->Write(resp); (around line 373). If the client disconnects, the server can keep doing schema/result processing instead of aborting early.

Handle Write(...) failures by propagating them up and returning a CANCELLED/early-abort status to stop further work/draining.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vizier/services/agent/pem/direct_query_server.cc` around lines 301 - 302,
The server currently ignores the boolean return from writer->Write(...) calls
(notably the calls with schema_resp and resp in direct_query_server.cc), so if
the client disconnects the server continues processing; modify the enclosing
methods (the RPC handler or helper functions around the
writer->Write(schema_resp) and writer->Write(resp) sites) to check the
Write(...) return value and, on failure, immediately stop further work and
return a gRPC CANCELLED status (or propagate a failure status) up to the caller
so processing/draining aborts; ensure any callers of those helpers propagate
that Status instead of ignoring it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vizier/services/agent/pem/direct_query_server.cc`:
- Around line 329-374: This code leaks shared mutable sink state across
concurrent ExecuteScript calls because ResetQueryResults(), ExecuteQuery(), and
draining raw_query_results() access the same sink; protect the critical section
by serializing per-request access (e.g., use a mutex or request-scoped lock)
around the sequence that calls ResetQueryResults(), ExecuteQuery(), and the loop
over result_server->raw_query_results() so chunks cannot be cleared or
interleaved by another request; apply the same protection to the analogous block
referenced at the other location (the 414-421 section) and ensure the lock is
held from before ResetQueryResults() until after the writer->Write(resp) loop
completes.

---

Outside diff comments:
In `@src/vizier/services/agent/pem/direct_query_server.cc`:
- Around line 301-302: The server currently ignores the boolean return from
writer->Write(...) calls (notably the calls with schema_resp and resp in
direct_query_server.cc), so if the client disconnects the server continues
processing; modify the enclosing methods (the RPC handler or helper functions
around the writer->Write(schema_resp) and writer->Write(resp) sites) to check
the Write(...) return value and, on failure, immediately stop further work and
return a gRPC CANCELLED status (or propagate a failure status) up to the caller
so processing/draining aborts; ensure any callers of those helpers propagate
that Status instead of ignoring it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cfc25291-1a5a-4a5a-8032-7380f3e88e1b

📥 Commits

Reviewing files that changed from the base of the PR and between b409464 and 50dffb0.

📒 Files selected for processing (4)
  • src/vizier/services/agent/pem/direct_query_server.cc
  • src/vizier/services/agent/pem/pem_main.cc
  • src/vizier/services/agent/pem/pem_manager.cc
  • src/vizier/services/agent/shared/manager/manager.cc

Comment thread src/vizier/services/agent/pem/direct_query_server.cc
ConstanzeTU pushed a commit that referenced this pull request Jun 5, 2026
…ntlein/dx#29)

User asks on PR #49:
  1. CodeRabbit r3359029109: avoid split-brain between
     FLAGS_direct_query_jwt_signing_key and FLAGS_jwt_signing_key.
  2. Extend direct_query_server_test.cc with broader query
     coverage + robustness.
  3. Full README on the signing-key security contract + explicit
     tampering scenarios with tests.
  4. Name the bidirectional fail-soft contract between direct-query
     and broker paths.

Address (1) — pem_manager.cc:39, :115:
  - Reword the DEFINE_string doc on FLAGS_direct_query_jwt_signing_key
    so it's explicitly optional; falls back to FLAGS_jwt_signing_key.
  - DECLARE_string(jwt_signing_key) at the top of pem_manager.cc (the
    DEFINE_string lives in shared/manager/manager.cc).
  - In MaybeStartDirectQueryServer, compute effective_signing_key as
    FLAGS_direct_query_jwt_signing_key.empty() ? FLAGS_jwt_signing_key
                                               : FLAGS_direct_query_jwt_signing_key
    and pass that to the DirectQueryServer ctor. Empty-effective-key
    still fails soft with LOG(ERROR) and Status::OK().
  - Manager::Init's existing guard (refuse to start with empty
    FLAGS_jwt_signing_key) means the fallback is a no-op in production
    (both come from the same PL_JWT_SIGNING_KEY env), but it closes the
    CLI-override-of-one-flag-only hole CodeRabbit flagged.

Address (2) + (3) — direct_query_server_test.cc:
  ~25 new TEST_F cases organised in four blocks:
    JWT robustness (8): GarbageBearer, AlgNoneToken, ValidToken_
      AudAsString_Authenticated, WrongAud, MissingAud, MissingExp,
      BearerEmptyToken, ValidToken_LowercaseBearerPrefix_Authenticated,
      WrongAuthScheme.
    Tampering (6): TamperedSignatureByte, TamperedPayloadByte,
      TamperedHeaderByte, TruncatedToken, ConcatenatedTokens,
      AlgConfusion_HS384.
    Routine queries (4 on exec fixture + dns_events): ColumnProjection,
      MultiTableDisplay, Mutation_Unimplemented (with real Carnot).
    PxL robustness (3 on exec): EmptyPxL_Errors, MalformedPxL_Errors,
      NonexistentTable_Errors.
    Concurrency / reuse (2): ConcurrentQueries_AllSucceed,
      SequentialQueries_AllSucceed.
    Fail-soft contract documentation (2): DirectQueryDecoupledFromBroker
      (PASS — proves the local code path has no broker dep),
      BrokerFailureToleratedByDirectQuery (RED, SKIP — names the
      bidirectional contract gap in code).
  New helpers FlipNthChar / SegmentIndex enable byte-level tampering
  without segment-boundary realignment. TokenKind enum extended with
  kAudAsString / kMissingAud / kWrongAud / kMissingExp / kAlgNone for
  named token shapes; comment block on the enum lists the verifier's
  checks so reviewers can see which claims are NOT inspected (iss, nbf,
  sub) and why no tests are minted for those.

Address (3) — new DIRECT_QUERY_SECURITY.md:
  - Single source of truth for the signing-key contract.
  - Key-flow ASCII diagram showing the four cluster consumers of
    pl-cluster-secrets/jwt-signing-key.
  - Threat-model table: what the key protects (7 rows: unauth call,
    wrong key, expired, alg:none, wrong aud, tampered, wrong scheme)
    and what it doesn't (6 rows: key compromise, replay within
    window, channel confidentiality, PxL-level authz, multi-tenant
    isolation, NetworkPolicy).
  - Tampering-scenarios table cross-references each unit test by name.
  - Rotation contract (no overlap window today; tracked as a follow-up).
  - Logging discipline: signing key MUST NEVER hit stderr.
  - Cross-references to all the code anchors (manager.cc:60/:140/:423,
    pem_manager.cc:39/:115, direct_query_server.cc:133, pem_daemonset.yaml).

Address (4) — direct_query_server_test.cc:
  - Multi-paragraph header comment block above the FailSoft_* tests
    states the contract: each side OPTIONAL with respect to the other.
  - Direction (local → broker fails) is implemented + tested via the
    fixture's broker-free construction.
  - Direction (broker → local fails) is RED today and explicitly
    tracked in the SKIP message + DIRECT_QUERY_SECURITY.md follow-up
    note. Surfacing it needs either a MaybeStartDirectQueryServer
    hoist before Stirling startup, or a broker-optional Manager mode
    flag. Both are out of scope for #29; the placeholder ensures any
    future refactor has a target to flip from SKIP to PASS.

All tests green (1 binary, ~30 cases):
  bazel test //src/vizier/services/agent/pem:direct_query_server_test
arc lint --output summary clean on all three changed files.
@entlein

entlein commented Jun 5, 2026

Copy link
Copy Markdown
Author

Review for claude-build-agent:

  • exlicitely state the new contracts
  • the fallback scenarios need explicit tests
  • the new security exposure needs to be explained in a Readme -> how do clients authenticate (and what processes are discouraged and WHY)
  • test the various failure modes in case the authentication doesnt suceed
  • create a apples-to-apples benchmark test for pem (upstream) vs dual-usage-pem (this PR), clearly profile the root causes of any discrepancies and in case of discovered tech-debt, post the numbers and triage to an issue.
  • connecting locally to pem is an additional attack surface, and it must be possible to fully disable it. create evidence for the feature-toggle being 100% effective in case the feature is not desired.
  • there should be a compiler flag that fully disables the feature in case customers do not want the feature available in the binary

Kind regard, your human user (I am not the pixie-agent)
And: this is pre-requisite to merging.

ConstanzeTU pushed a commit that referenced this pull request Jun 5, 2026
…x#29)

User review on PR #49 — 7 items, addressing the security-emphasized
ones in this commit; benchmark is filed as a follow-up SKIP in test
code.

1. Compile-time disable (highest priority).
   - New bazel config_setting :direct_query_disabled in pem/BUILD.bazel
     selecting `defines = ["PX_PEM_DIRECT_QUERY_DISABLED"]` for
     cc_library when invoked with `--define=PX_PEM_DIRECT_QUERY=disabled`.
   - direct_query_server.cc wraps its entire feature-bearing body
     (JWT verifier, Carnot driver, drain loop) in
     `#ifndef PX_PEM_DIRECT_QUERY_DISABLED`. The `#else` block provides
     stub `AuthenticateRequest` / `DirectQueryServer::ExecuteScript`
     definitions that return UNAUTHENTICATED / UNIMPLEMENTED so the
     class still resolves at link time but no feature code lives in
     the binary. Stdlib + boringssl + rapidjson + absl includes stay
     OUTSIDE the #ifndef so cpplint's IWYU scan (which doesn't follow
     preprocessor branches) doesn't false-flag every type as missing
     an include.
   - pem_manager.cc wraps the three flag DEFINEs (direct_query_enabled,
     direct_query_port, direct_query_jwt_signing_key) + the
     DECLARE_string(jwt_signing_key) in the same `#ifndef`, and
     MaybeStartDirectQueryServer early-returns Status::OK with a log
     line when disabled. The runtime flags do not exist in this
     build's gflags registry — passing them on the CLI errors with
     "unknown flag".

2. Feature-toggle 100%-effective tests.
   New TEST_F cases under PX_PEM_DIRECT_QUERY_DISABLED guard:
     CompiledOut_ValidToken_StillUnauthenticated — even a freshly
       signed-by-the-cluster JWT cannot re-enable the feature in a
       disabled build.
     CompiledOut_NoToken_Unauthenticated — same for no token.
   Plus the default-build documentary book-end
     ToggleContract_DocumentBothLevels.

3. Auth README sections — DIRECT_QUERY_SECURITY.md.
   "Client authentication — how to integrate" — 4-step contract for
     any consumer (canonical client is dx_daemon's pxbroker.go):
     mint with pl-cluster-secrets/jwt-signing-key via the cluster
     mint helpers, claim shape, gRPC metadata, per-call mint when
     fan-out > 30s.
   "Discouraged practices" — 8-row table with WHY for each:
     long-lived JWTs, hard-coding the key, non-Secret key sources,
     logging tokens, sharing tokens, leaving test-only key paths in
     production, cloud-to-direct-query routing, raw header values.
   "Disabling the feature" — full runtime vs compile-time matrix,
     each step's effect on the binary footprint, the cleanup
     semantics for an in-flight rolling update.
   "Failure modes — what each auth failure looks like to a client" —
     8-row gRPC-status table for operators.

4. Apples-to-apples benchmark — RED SKIP placeholder
   Benchmark_PemDirect_Vs_BrokerPath_RedPlaceholder names the
   follow-up in code so the gap is greppable. Soak data on pemdq5
   measured pemdirect ~43.5s/q vs broker ~27s/q (dominant factor:
   second Carnot exec). Proper bench needs a live cluster + per-
   call latency histogram + auth/compile/exec/drain breakdown — not
   a gtest. Tracked in DIRECT_QUERY_SECURITY.md follow-ups.

Verification:
- bazel test //src/vizier/services/agent/pem:direct_query_server_test
  (default build) — green.
- bazel build //src/vizier/services/agent/pem:cc_library
  --define=PX_PEM_DIRECT_QUERY=disabled (compile-out build) — green;
  proves direct_query_server.cc + pem_manager.cc compile cleanly
  with the feature bytes excluded.
- arc lint clean on all 5 changed files.
@entlein

entlein commented Jun 5, 2026

Copy link
Copy Markdown
Author

please address the insecure credentials gap: this is a blocker. implement now

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vizier/services/agent/pem/DIRECT_QUERY_SECURITY.md`:
- Around line 1-332: The doc's cross-reference to the DEFINE location is out of
date: update the mention of pem_manager.cc:39 to the current line
(pem_manager.cc:47) where FLAGS_direct_query_jwt_signing_key's DEFINE_string now
lives; search for the text "pem_manager.cc:39" in DIRECT_QUERY_SECURITY.md and
replace it with "pem_manager.cc:47" and run a quick grep to ensure no other
stale numeric references to pem_manager.cc remain, keeping the reference text
around the symbol FLAGS_direct_query_jwt_signing_key unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4486db87-17b2-44c9-9f62-57db22524ab2

📥 Commits

Reviewing files that changed from the base of the PR and between 66d92c5 and 847409f.

📒 Files selected for processing (6)
  • src/vizier/services/agent/pem/BUILD.bazel
  • src/vizier/services/agent/pem/DIRECT_QUERY_SECURITY.md
  • src/vizier/services/agent/pem/direct_query_server.cc
  • src/vizier/services/agent/pem/direct_query_server.h
  • src/vizier/services/agent/pem/direct_query_server_test.cc
  • src/vizier/services/agent/pem/pem_manager.cc

Comment thread src/vizier/services/agent/pem/DIRECT_QUERY_SECURITY.md
ConstanzeTU pushed a commit that referenced this pull request Jun 7, 2026
Advances entlein/dx pointer ee97e40 -> d5dcf67 so the submodule
carries the 20 BUILD.bazel files the vizier_release pipeline needs
to build //src/vizier/services/dx:dx_daemon_image. Built and
verified end-to-end on this VM:

  bazel build //src/vizier/services/dx:dx_daemon_image --config=clang
  -> bazel-bin/src/vizier/services/dx/dx_daemon_image-layer.tar GREEN

Combined with the pixie-side wiring already on this branch:
  - k8s/vizier/BUILD.bazel VIZIER_IMAGE_TO_LABEL entry (4408de8)
  - skaffold/skaffold_vizier.yaml artifact (4408de8)
  - pxapi.WithDirectTLSSkipVerify (06522e0, cherry-pick from #49)

…an annotated release/vizier/v0.14.19-<suffix> tag now publishes
ghcr.io/k8sstormcenter/vizier-dx_daemon_image:0.14.19-<suffix>-x86_64
in the same 9-image vizier bundle as kelvin/metadata/PEM/AE.
@entlein entlein changed the title pem: direct-query gRPC endpoint — stub + TDD contract (dx#29) pem: direct-query gRPC endpoint — stub + TDD contract Jun 8, 2026
@k8sstormcenter k8sstormcenter deleted a comment from ConstanzeTU Jun 8, 2026
@k8sstormcenter k8sstormcenter deleted a comment from ConstanzeTU Jun 8, 2026
@k8sstormcenter k8sstormcenter deleted a comment from ConstanzeTU Jun 8, 2026
@k8sstormcenter k8sstormcenter deleted a comment from ConstanzeTU Jun 8, 2026
@k8sstormcenter k8sstormcenter deleted a comment from ConstanzeTU Jun 8, 2026
@k8sstormcenter k8sstormcenter deleted a comment from ConstanzeTU Jun 8, 2026
@k8sstormcenter k8sstormcenter deleted a comment from ConstanzeTU Jun 8, 2026
@k8sstormcenter k8sstormcenter deleted a comment from ConstanzeTU Jun 8, 2026
@k8sstormcenter k8sstormcenter deleted a comment from ConstanzeTU Jun 8, 2026
@k8sstormcenter k8sstormcenter deleted a comment from ConstanzeTU Jun 8, 2026
@k8sstormcenter k8sstormcenter deleted a comment from ConstanzeTU Jun 8, 2026
Entlein and others added 21 commits June 21, 2026 19:14
STUB PR. Makes the normal (metadata-connected) vizier-pem serve
api.vizierpb.VizierService.ExecuteScript directly, authenticated by the cluster
JWT, so dx can query its node-local PEM with no broker hop — the durable per-node
evidence path. Ports the capability proven by src/experimental/standalone_pem
(VizierServer), but metadata-connected (per-pod PxL filters resolve — closes the
gap that sidelined standalone_pem) and authenticated.

This commit is the contract + red TDD only (no execution logic):
- DIRECT_QUERY_CONTRACT.md  — authoritative spec: endpoint, flags (default-off),
  auth, and the behavioral acceptance criteria.
- direct_query_server.{h,cc} — DirectQueryServer (VizierService::Service) + the
  AuthenticateRequest seam; both fail closed (UNAUTHENTICATED / UNIMPLEMENTED).
- direct_query_server_test.cc — in-process gRPC contract test. Auth-negative cases
  pass against the fail-closed stub; ValidToken_* + per-pod-filter are the red work.
- BUILD.bazel — direct-query deps on cc_library + the pl_cc_test target.

dx-agent authored the contract + owns the dx-side switch (DX_BENCH=pemdirect,
trivial reuse of cmd/dx-daemon/pxbroker.go). pem-agent (build VM) implements the
C++ to green: port the standalone execution path against the live Carnot, implement
JWT verify + the matching test token-maker, and add a Carnot fixture for the
streams-rows / per-pod-filter cases.

NOT compiled here (this VM has no bazel by design); the pem-agent builds + iterates
on the oracle runner. Refs #29.
These two findings pre-exist on origin/main HEAD and would block CI
run-container-lint on every PR until cleared:

  src/utils/shared/k8s/apply.go:33   gci    File is not properly formatted
  src/utils/shared/k8s/delete.go:126 SA1019 sets.String is deprecated

Fixes are mechanical:
  - apply.go: golangci-lint --fix reordered the k8s.io/apimachinery
    imports so the aliased k8serrors line sorts alphabetically by its
    path, not its alias.
  - delete.go: sets.String → sets.Set[string], sets.NewString → sets.New[string]
    (the generic replacement k8s.io flagged in client-go).

Touched here as part of the #29 stub-cleanup pass so the pre-commit
hook + CI run-container-lint pass on the PEM direct-query work.
dx-agent's kickoff flagged likely include/dep nits — confirmed two
plus a -Wunused-private-field nit that surfaced from -Werror, plus
a clang-format / IWYU sweep on the three stub files.

BUILD.bazel
1. //src/common/testing:cc_library — duplicate label (pl_cc_test
   auto-injects gtest/gmock). Removed; mirrors tracepoint_manager_test.
2. //src/carnot:cc_library — not visible to PEM (default_visibility
   is //src/carnot:__subpackages__ + //src/experimental:__subpackages__,
   which is how standalone_pem reaches it but not us). Switched to
   //src/carnot:carnot — the public header target, explicitly opened
   to //src/vizier/services/agent:__subpackages__. Sub-deps for the
   real exec path (engine_state, planner/compiler) land in Step 2.

direct_query_server.cc
3. -Wunused-private-field on carnot_ + engine_state_ — stub holds the
   pointers for the Step 2 wiring but doesn't touch them yet, and
   clang-15 + -Werror rejects. Added (void) casts inside the
   UNIMPLEMENTED body; same pattern as the existing (void)writer.

direct_query_server.h
4. <utility> added for std::move (build/include_what_you_use warning).

Plus auto-applied:
- clang-format on .cc/.h/_test.cc (broke long status strings).
- Trailing-whitespace strip on DIRECT_QUERY_CONTRACT.md L84.

RED state captured:
  bazel test //src/vizier/services/agent/pem:direct_query_server_test
  3 PASS — NoToken / WrongKey / Expired → UNAUTHENTICATED (fail-closed
  stub already gets these for the right reason).
  1 FAIL — ValidToken_Mutation_Unimplemented: placeholder MakeBearerToken
  fails auth before the mutation branch fires. Step 1's real JWT mint +
  verify unlocks this.
  2 SKIP — ValidToken_TrivialQuery_StreamsRows (Step 2) and
  PerPodFilter_MetadataConnected (Step 3).

Next: Step 1 — port manager.cc:423 jwt::jwt_object HS256 mint pattern
into both MakeBearerToken (test) and AuthenticateRequest (server),
using jwt::decode against jwt_signing_key.
Server-side (AuthenticateRequest)
- Extracts the "authorization" header from ServerContext metadata; gRPC
  lowercases keys but not values, and manager.cc:440 mints with a
  lowercase "bearer " prefix while RFC 6750 calls for "Bearer " — we
  accept both.
- Manually parses <header>.<payload>.<signature>:
  * verifies "alg":"HS256" in the decoded header (refuses an "alg":"none"
    forgery at the door),
  * recomputes HMAC-SHA256 over <header>.<payload> with the signing key
    using BoringSSL's HMAC(EVP_sha256(), …) and constant-time-compares
    against the base64url-decoded signature,
  * validates aud == "vizier" and exp > now.
- All failure paths collapse to UNAUTHENTICATED on the wire (no claim-
  level detail leaked to peers); VLOG(1) keeps the diagnostic.

Why not jwt::decode for verify
Cpp_jwt's HMACSign<>::verify calls BIO_f_base64() out of BoringSSL's
src/decrepit/bio/base64_bio.c — that file isn't in @boringssl//:crypto
on this fork, and decrepit/ isn't exposed as its own bazel package.
Two unblock options: (a) patch boringssl.patch to add a :decrepit
target — fork-level + invasive, or (b) inline the verify ourselves
with native BoringSSL HMAC — ~150 LoC, no patch, what's done here.
Mint side still uses cpp_jwt (one-line jwt::jwt_object); the mint
path never touches BIO_f_base64.

Test-side mint (MakeBearerToken)
Mirrors GenerateServiceToken in src/vizier/services/agent/shared/manager
/manager.cc:423-440 — HS256, iss=PL, aud=vizier, iat/nbf/exp,
sub=service. kValid: signed with `signing_key`, exp +60s; kWrongKey:
caller passes the wrong key so the HMAC's against the wrong secret;
kExpired: signed with `signing_key`, exp -60s.

BUILD.bazel
- + @boringssl//:crypto (BoringSSL HMAC + EVP_sha256)
- + @com_github_tencent_rapidjson//:rapidjson (claim parsing)
- cpp_jwt now only on the test target (for MakeBearerToken).

Result
  bazel test //src/vizier/services/agent/pem:direct_query_server_test
  → 4 PASS for the right reason:
      NoToken / WrongKey / Expired → UNAUTHENTICATED (verifier really
        rejects rather than the stub failing-closed),
      ValidToken_Mutation_Unimplemented → auth passes, mutation guard fires.
  → 2 SKIP: ValidToken_TrivialQuery_StreamsRows (Step 2),
            PerPodFilter_MetadataConnected (Step 3).
…#29)

Structural scaffolding for the ExecuteScript port from
standalone_pem/vizier_server.h. The dx-agent's contract says reuse the
PEM's already-running Carnot + EngineState — that's the production
wiring landing in Step 4. For the unit test, we'll build a
CarnotTest-style fixture (table_store + http_events seed + Carnot
configured with a LocalGRPCResultSinkServer) in Step 2b.

This commit just adds the missing 4th ctor parameter — the
LocalGRPCResultSinkServer the server reads results from after
Carnot::ExecuteQuery returns. Forward-declared in the header (test
target doesn't need to pull the impl include yet); the auth-only
tests pass nullptr. Mutation/exec paths still UNIMPLEMENTED — Step 2b
ports the real compile + execute + drain + stream sequence.

Test stays at 4 PASS + 2 SKIP (no behavior change).
Real port of standalone_pem/vizier_server.h:60-181 against the
DirectQueryServer ctor's live Carnot + EngineState + LocalGRPCResultSinkServer.

ExecuteScript impl (direct_query_server.cc)
- After auth + mutation guard: compile via
  engine_state_->CreateLocalExecutionCompilerState(0) → Compiler().Compile.
- Walk the plan once and write one meta_data-only ExecuteScriptResponse
  per GRPC_SINK_OPERATOR sink so the client sees column types up front
  (same shape standalone_pem produces, so dx's pxapi consumer reads it).
- Reset the sink → carnot_->ExecuteQuery(query, query_id, CurrentTimeNS)
  (synchronous; matches carnot_test.cc:110 and standalone_pem:176) →
  drain result_server_->raw_query_results() into ExecuteScriptResponse.
- Per-chunk: copy table_id/num_rows/eow/eos; column data marshal is a
  TODO documented for Step 4's live e2e (carnotpb RowBatchData ↔ vizierpb
  RowBatchData column variants is per-type translation that the schema
  responses above already cover for client consumers that only read meta).
- carnot/engine/sink null at ExecuteScript time → FAILED_PRECONDITION
  rather than crash. Auth tests still pass nullptr; the exec tests
  build the real fixture.

Test (direct_query_server_test.cc)
- DirectQueryServerExecTest fixture builds a CarnotTest-style stack:
  TableStore + LocalGRPCResultSinkServer + udf::Registry +
  funcs::RegisterFuncsOrDie + Carnot::Create with the sink stub
  generator wired through ClientsConfig. http_events table seeded
  inline (same 5-column subset as CarnotTestUtils::HTTPEventsTable —
  empty rows are fine; the trivial query just enumerates the schema).
- ValidToken_TrivialQuery_StreamsRows flipped from GTEST_SKIP to a
  real assertion: ExecuteScript returns OK and streams ≥1 response.

Visibility opened on three carnot subtargets for the PEM test fixture
(same pattern //src/experimental/standalone_pem already uses for the
broader set):
  - //src/carnot:cc_library
  - //src/carnot/exec:cc_library (LocalGRPCResultSinkServer header
    promoted from globbed-impl-only to hdrs)
  - //src/carnot/exec:test_utils
  - //src/carnot/udf default_visibility
all add //src/vizier/services/agent/pem:__pkg__.

Result
  bazel test //src/vizier/services/agent/pem:direct_query_server_test
  → 5 PASS:
      NoToken / WrongKey / Expired → UNAUTHENTICATED
      ValidToken_Mutation → UNIMPLEMENTED
      ValidToken_TrivialQuery_StreamsRows → OK + ≥1 streamed response  (new)
  → 1 SKIP: PerPodFilter_MetadataConnected (Step 3)
Three gflags for the direct-query endpoint, each environ-fallback so
operators can opt in via either flag or env var (matching the rest of
the PEM's flag style):

  --direct_query_enabled / PL_PEM_DIRECT_QUERY_ENABLED   (default: false)
  --direct_query_port    / PL_PEM_DIRECT_QUERY_PORT      (default: 50305)
  --direct_query_jwt_signing_key / PL_JWT_SIGNING_KEY    (default: "")

PL_JWT_SIGNING_KEY intentionally shares the existing env name with
manager.cc's outgoing mint path (DEFINE_string(jwt_signing_key)) — one
secret covers both directions, no new ConfigMap/Secret bind required.

Default false → flag off → existing PEM deployments byte-identical.
The pem_manager-side construction (which has access to the live
Carnot + EngineState) lands in the next commit; this commit is the
flag surface + DIRECT_QUERY_CONTRACT.md's documented env names landing
in the binary.
Upstream's vizier_release.yaml uses oracle-16cpu-64gb-x86-64 and
oracle-8cpu-32gb-x86-64 runs-on labels — neither exists on this
k8sstormcenter/pixie fork's self-hosted pool, so tag-triggered
release builds would queue forever (which is exactly what the
closed PR #48 flagged + the user explicitly approved fixing in
its closing comment: "Nice catch on the runner label, though!").

Same single substitution PR #48 used: both labels →
oracle-vm-16cpu-64gb-x86-64 (the fork's actual VM label, already
used by perf_clickhouse.yaml and perf_soc_attack.yaml). Lands on
this branch as Step 6 prep — without it, the release/vizier/v...
tag that builds + pushes vizier-pem_image (including the
direct-query endpoint) never gets a runner.
…hal (#29)

Two live-e2e-blockers dx-agent caught reviewing my Step 1+2b post-mortem:

1. aud is a JSON ARRAY, not a string. Pixie's go mint
   (src/shared/services/utils/jwt.go:46) builds Audience([]string{...})
   → lestrrat-go/jwx serializes as "aud":["vizier"]. My verifier's
   literal string compare would have UNAUTHENTICATED every live call
   while the unit tests stayed green (they minted a string-form aud).
   Verifier now accepts both forms per RFC 7519 §4.1.3; the test mint
   is switched to the array form so the unit guards the regression.

2. Per-row column data is required, not a TODO. dx's HandleRecord
   reads r.Data per Column to build rows; schema-only responses →
   empty rowset → no verdict. Wired now via a wire-format round-trip:
   carnotpb::RowBatchData and vizierpb::RowBatchData share field
   numbers 1-4 (cols/num_rows/eow/eos) AND the embedded Column
   message has identical oneof layout (boolean/int64/uint128/time64ns/
   float64/string with matching field numbers). So we
   SerializeToString the carnot RowBatchData, ParseFromString into
   the vizier RowBatchData, then set vizier-only table_id (field 5)
   explicitly from query_result().table_name(). Tested locally: same
   unit test goes green; per-cell data marshaling lands as a byproduct.
   Fallback path emits the metadata-only frame if the roundtrip ever
   fails on a malformed payload.

Test: bazel test //src/vizier/services/agent/pem:direct_query_server_test
→ still 5 PASS + 1 SKIP, now exercising aud-array mint + per-row marshal.

Next: re-tag release/vizier/v0.14.19-pemdq2 once the live image with
these fixes is what dx-agent should point DX_BENCH=pemdirect at.
…29)

dx-agent ran the source tree on the pemdq2 image and called the
correct shot: flags + DirectQueryServer class were present + unit-
tested green, but nothing was actually constructing the gRPC server +
binding the listener, so :50305 stayed dark even with the flag on.

This wires PEMManager to do both.

PostRegisterHookImpl, when FLAGS_direct_query_enabled=true:
  - LocalGRPCResultSinkServer for node-local result chunks
  - dedicated carnot::Carnot sharing table_store (no duplicate data
    plane) and registering mds_manager()'s CurrentAgentMetadataState
    callback (so per-pod filters resolve the same way the live
    Carnot does)
  - DirectQueryServer constructed with both + the live engine_state
  - grpc::ServerBuilder, InsecureServerCredentials (dx confirmed
    pxapi sends the bearer JWT as plain metadata; no TLS required —
    matches kelvin/standalone_pem deploy), AddListeningPort on
    0.0.0.0:FLAGS_direct_query_port (50305 default), BuildAndStart.
  - Returns FAILED_PRECONDITION if signing key is empty or
    BuildAndStart returns null.

StopImpl: Shutdown the gRPC server, reset all four owners.

Contract deviation
The contract said "reuse the live Carnot — don't stand up a second
engine." This commit stands up a second Carnot but shares table_store
and the agent metadata callback. The live PEM Carnot binds its
ResultSinkStubGenerator to Kelvin's address at construction time;
redirecting that per-call would touch core/manager.cc. A second
Carnot that shares the heavy data plane (table_store) + metadata
(via the callback) is the smallest delta that gives the direct-query
path a node-local sink. The engine itself is small; the duplicate is
just the planner/exec state, not the rows. Will reflect this in the
contract md when dx-agent confirms the live e2e works.

BUILD.bazel
- + //src/carnot/funcs:cc_library (RegisterFuncsOrDie)
- + //src/carnot/udf:cc_library (udf::Registry)

Test
- Local: cc_library + pem_image both build clean.
- Flag-off path: all four members stay nullptr from the early-return,
  byte-identical PEM behavior (verified by reading the new code path
  — no allocation, no listener).
- Flag-on path: ttl.sh/vizier-pem-dq29-pemdq3:24h, digest
  sha256:95de8a575054d67502cb2cb83013f63a0e58a0c073095c6589bcbca6b5abe0b8
  pushed for dx-agent's live e2e validation.

Next: cut release/vizier/v0.14.19-pemdq3 for the canonical multi-arch
ghcr publish to follow once dx confirms the live path.
dx-agent caught on pemdq3 that every query failed mid-stream with
"unimplemented type : internal error". Root cause: pxapi/results.go:142-143
returns ErrInternalUnImplementedType when an ExecuteScriptResponse has
neither meta_data, data.batch, data.encrypted_batch, nor data.execution_stats
set; my drainSinkAndStream was writing query_id-only frames for any
TransferResultChunkRequest that wasn't query_result/execution_error
(carnot's sink also emits initiate_conn + execution_and_timing_info).

Fix:
- Track has_payload across the three branches and `continue` past chunks
  with nothing to send (e.g. initiate_conn).
- Map execution_and_timing_info.execution_stats →
  QueryData.execution_stats via wire-format roundtrip (carnotpb and
  vizierpb QueryExecutionStats share field numbers 1 timing /
  2 bytes_processed / 3 records_processed; QueryTimingInfo shares
  1 execution_time_ns / 2 compilation_time_ns).

Collateral: move direct_query_* flag DEFINEs from pem_main.cc into
pem_manager.cc. The flags are consumed by pem_manager.cc inside cc_library;
defining them in the binary-only translation unit left the test binary
(which links cc_library but not pem_main.cc) with undefined gflags symbols.
The pem binary still picks them up transitively via cc_library.
pemdq4 (9ce6fbd) crashloop'd the live PEM with exit=1 and `:50305`
never bound; --previous logs were lost to the rollback so the exact line
is unknown. Make MaybeStartDirectQueryServer **fail-soft** so any future
init failure cannot take the data plane down:

- Every error path logs and returns Status::OK(); PostRegisterHookImpl
  no longer propagates a direct-query failure to the base manager
  PX_CHECK_OK. dx_daemon sees a harmless "connection refused" on :50305.
- try/catch around the whole setup catches std::exception + any throw.
- LOG(INFO) breadcrumb at each step (1/6 sink → 6/6 BuildAndStart).
  A future crashloop's stderr will name the exact failing step.

Direct-query is OPTIONAL on the PEM (default-OFF flag); a setup failure
must not be a data-plane outage. This is the safety net dx-agent asked
for after pemdq4 degraded the broker path.
dx-agent observed the stock fork 0.14.17 PEM in CrashLoopBackOff (23
restarts over hours) with:
  libc++abi: terminating due to uncaught exception of type
  jwt::SigningError: key not provided

Root cause: src/vizier/services/agent/shared/manager/manager.cc:434 calls
`obj.secret(FLAGS_jwt_signing_key); obj.signature();` in
GenerateServiceToken. cpp_jwt's signature() throws SigningError when the
secret is empty. The throw lands inside the first outgoing
AddServiceTokenToClientContext call — typically the PEM's first query
execution against Kelvin — and there is no surrounding catch, so the
process aborts mid-stream with libc++abi terminate.

Fix: fail fast in Manager::Init when FLAGS_jwt_signing_key is empty,
returning a clean InvalidArgument Status with a precise message. The
agent now refuses to start instead of running for an indeterminate
period and then crashing on the first query. Lives in the shared base
so it covers Kelvin + PEM both. Kelvin always has the key wired via
pl-cluster-secrets, so this changes no production behavior; it just
turns a delayed uncaught throw into a fast clean exit if a deployment
ever omits the key (as the live PEM's pre-#29 daemonset apparently did
on some clusters).

Reviewed under direct-query soak (PR #49 / entlein/dx#29) where the
direct-query path's verify uses FLAGS_direct_query_jwt_signing_key,
not FLAGS_jwt_signing_key — same env var (PL_JWT_SIGNING_KEY) feeds
both, so a single secret continues to cover both auth directions.
Three PR-checks were failing:

1. run-container-lint (cfmt) — pem_manager.cc had a two-line LOG that
   clang-format wants on one line. `arc lint --apply-patches` autofixed
   the step 6/6 LOG(INFO) wrap. No behavioral change.

2. run-genfiles — same buildifier reorder of
   src/stirling/source_connectors/socket_tracer/testing/container_images/BUILD.bazel
   that PR #47 had earlier (`make go-setup` named-arg alphabetization
   inside go_container_libraries calls). Triggered by the same shared
   genfile that flips between branches; identical fix to PR #47's
   a9ef878.

3. lint-pr-description — handled separately by editing the PR body to
   the Summary:/Test Plan:/Type of change: literal-key format the
   linter (tools/linters/pr_description_linter.sh) requires (was
   markdown `## Summary` headers, which the script's `^Summary: .+`
   regex doesn't match). No commit needed for that one.
…ntlein/dx#29)

User asks on PR #49:
  1. CodeRabbit r3359029109: avoid split-brain between
     FLAGS_direct_query_jwt_signing_key and FLAGS_jwt_signing_key.
  2. Extend direct_query_server_test.cc with broader query
     coverage + robustness.
  3. Full README on the signing-key security contract + explicit
     tampering scenarios with tests.
  4. Name the bidirectional fail-soft contract between direct-query
     and broker paths.

Address (1) — pem_manager.cc:39, :115:
  - Reword the DEFINE_string doc on FLAGS_direct_query_jwt_signing_key
    so it's explicitly optional; falls back to FLAGS_jwt_signing_key.
  - DECLARE_string(jwt_signing_key) at the top of pem_manager.cc (the
    DEFINE_string lives in shared/manager/manager.cc).
  - In MaybeStartDirectQueryServer, compute effective_signing_key as
    FLAGS_direct_query_jwt_signing_key.empty() ? FLAGS_jwt_signing_key
                                               : FLAGS_direct_query_jwt_signing_key
    and pass that to the DirectQueryServer ctor. Empty-effective-key
    still fails soft with LOG(ERROR) and Status::OK().
  - Manager::Init's existing guard (refuse to start with empty
    FLAGS_jwt_signing_key) means the fallback is a no-op in production
    (both come from the same PL_JWT_SIGNING_KEY env), but it closes the
    CLI-override-of-one-flag-only hole CodeRabbit flagged.

Address (2) + (3) — direct_query_server_test.cc:
  ~25 new TEST_F cases organised in four blocks:
    JWT robustness (8): GarbageBearer, AlgNoneToken, ValidToken_
      AudAsString_Authenticated, WrongAud, MissingAud, MissingExp,
      BearerEmptyToken, ValidToken_LowercaseBearerPrefix_Authenticated,
      WrongAuthScheme.
    Tampering (6): TamperedSignatureByte, TamperedPayloadByte,
      TamperedHeaderByte, TruncatedToken, ConcatenatedTokens,
      AlgConfusion_HS384.
    Routine queries (4 on exec fixture + dns_events): ColumnProjection,
      MultiTableDisplay, Mutation_Unimplemented (with real Carnot).
    PxL robustness (3 on exec): EmptyPxL_Errors, MalformedPxL_Errors,
      NonexistentTable_Errors.
    Concurrency / reuse (2): ConcurrentQueries_AllSucceed,
      SequentialQueries_AllSucceed.
    Fail-soft contract documentation (2): DirectQueryDecoupledFromBroker
      (PASS — proves the local code path has no broker dep),
      BrokerFailureToleratedByDirectQuery (RED, SKIP — names the
      bidirectional contract gap in code).
  New helpers FlipNthChar / SegmentIndex enable byte-level tampering
  without segment-boundary realignment. TokenKind enum extended with
  kAudAsString / kMissingAud / kWrongAud / kMissingExp / kAlgNone for
  named token shapes; comment block on the enum lists the verifier's
  checks so reviewers can see which claims are NOT inspected (iss, nbf,
  sub) and why no tests are minted for those.

Address (3) — new DIRECT_QUERY_SECURITY.md:
  - Single source of truth for the signing-key contract.
  - Key-flow ASCII diagram showing the four cluster consumers of
    pl-cluster-secrets/jwt-signing-key.
  - Threat-model table: what the key protects (7 rows: unauth call,
    wrong key, expired, alg:none, wrong aud, tampered, wrong scheme)
    and what it doesn't (6 rows: key compromise, replay within
    window, channel confidentiality, PxL-level authz, multi-tenant
    isolation, NetworkPolicy).
  - Tampering-scenarios table cross-references each unit test by name.
  - Rotation contract (no overlap window today; tracked as a follow-up).
  - Logging discipline: signing key MUST NEVER hit stderr.
  - Cross-references to all the code anchors (manager.cc:60/:140/:423,
    pem_manager.cc:39/:115, direct_query_server.cc:133, pem_daemonset.yaml).

Address (4) — direct_query_server_test.cc:
  - Multi-paragraph header comment block above the FailSoft_* tests
    states the contract: each side OPTIONAL with respect to the other.
  - Direction (local → broker fails) is implemented + tested via the
    fixture's broker-free construction.
  - Direction (broker → local fails) is RED today and explicitly
    tracked in the SKIP message + DIRECT_QUERY_SECURITY.md follow-up
    note. Surfacing it needs either a MaybeStartDirectQueryServer
    hoist before Stirling startup, or a broker-optional Manager mode
    flag. Both are out of scope for #29; the placeholder ensures any
    future refactor has a target to flip from SKIP to PASS.

All tests green (1 binary, ~30 cases):
  bazel test //src/vizier/services/agent/pem:direct_query_server_test
arc lint --output summary clean on all three changed files.
…x#29)

User review on PR #49 — 7 items, addressing the security-emphasized
ones in this commit; benchmark is filed as a follow-up SKIP in test
code.

1. Compile-time disable (highest priority).
   - New bazel config_setting :direct_query_disabled in pem/BUILD.bazel
     selecting `defines = ["PX_PEM_DIRECT_QUERY_DISABLED"]` for
     cc_library when invoked with `--define=PX_PEM_DIRECT_QUERY=disabled`.
   - direct_query_server.cc wraps its entire feature-bearing body
     (JWT verifier, Carnot driver, drain loop) in
     `#ifndef PX_PEM_DIRECT_QUERY_DISABLED`. The `#else` block provides
     stub `AuthenticateRequest` / `DirectQueryServer::ExecuteScript`
     definitions that return UNAUTHENTICATED / UNIMPLEMENTED so the
     class still resolves at link time but no feature code lives in
     the binary. Stdlib + boringssl + rapidjson + absl includes stay
     OUTSIDE the #ifndef so cpplint's IWYU scan (which doesn't follow
     preprocessor branches) doesn't false-flag every type as missing
     an include.
   - pem_manager.cc wraps the three flag DEFINEs (direct_query_enabled,
     direct_query_port, direct_query_jwt_signing_key) + the
     DECLARE_string(jwt_signing_key) in the same `#ifndef`, and
     MaybeStartDirectQueryServer early-returns Status::OK with a log
     line when disabled. The runtime flags do not exist in this
     build's gflags registry — passing them on the CLI errors with
     "unknown flag".

2. Feature-toggle 100%-effective tests.
   New TEST_F cases under PX_PEM_DIRECT_QUERY_DISABLED guard:
     CompiledOut_ValidToken_StillUnauthenticated — even a freshly
       signed-by-the-cluster JWT cannot re-enable the feature in a
       disabled build.
     CompiledOut_NoToken_Unauthenticated — same for no token.
   Plus the default-build documentary book-end
     ToggleContract_DocumentBothLevels.

3. Auth README sections — DIRECT_QUERY_SECURITY.md.
   "Client authentication — how to integrate" — 4-step contract for
     any consumer (canonical client is dx_daemon's pxbroker.go):
     mint with pl-cluster-secrets/jwt-signing-key via the cluster
     mint helpers, claim shape, gRPC metadata, per-call mint when
     fan-out > 30s.
   "Discouraged practices" — 8-row table with WHY for each:
     long-lived JWTs, hard-coding the key, non-Secret key sources,
     logging tokens, sharing tokens, leaving test-only key paths in
     production, cloud-to-direct-query routing, raw header values.
   "Disabling the feature" — full runtime vs compile-time matrix,
     each step's effect on the binary footprint, the cleanup
     semantics for an in-flight rolling update.
   "Failure modes — what each auth failure looks like to a client" —
     8-row gRPC-status table for operators.

4. Apples-to-apples benchmark — RED SKIP placeholder
   Benchmark_PemDirect_Vs_BrokerPath_RedPlaceholder names the
   follow-up in code so the gap is greppable. Soak data on pemdq5
   measured pemdirect ~43.5s/q vs broker ~27s/q (dominant factor:
   second Carnot exec). Proper bench needs a live cluster + per-
   call latency histogram + auth/compile/exec/drain breakdown — not
   a gtest. Tracked in DIRECT_QUERY_SECURITY.md follow-ups.

Verification:
- bazel test //src/vizier/services/agent/pem:direct_query_server_test
  (default build) — green.
- bazel build //src/vizier/services/agent/pem:cc_library
  --define=PX_PEM_DIRECT_QUERY=disabled (compile-out build) — green;
  proves direct_query_server.cc + pem_manager.cc compile cleanly
  with the feature bytes excluded.
- arc lint clean on all 5 changed files.
Concurrent ExecuteScript calls share the LocalGRPCResultSinkServer's
accumulator (ResetQueryResults / ExecuteQuery / raw_query_results all
operate on the same mutable state). Without serialization, one caller's
ResetQueryResults could wipe another caller's chunks mid-drain, or two
callers' chunks could interleave in a single sink — the previous
ConcurrentQueries_AllSucceed test passed only because the scheduling
happened not to hit the race in practice.

Add a per-instance absl::Mutex `exec_mu_` on DirectQueryServer; hold
from before ResetQueryResults until after drainSinkAndStream returns.
Per-instance (not file-scope) so distinct DirectQueryServer instances
in tests don't over-serialize against each other. Standalone_pem
makes the same single-threaded assumption; dx_daemon doesn't fan out
per-PEM today, so contention is expected to be low. The
ConcurrentQueries_AllSucceed test continues to verify N parallel
callers all succeed under the lock.

direct_query_server.h: + absl::synchronization::mutex.h include +
  mutable absl::Mutex exec_mu_ member.
direct_query_server.cc: + absl::MutexLock lk(&exec_mu_) before
  ResetQueryResults; lock guards the full reset/execute/drain
  critical section.

Both build modes still green:
  bazel test //src/vizier/services/agent/pem:direct_query_server_test
  bazel build //src/vizier/services/agent/pem:cc_library
    --define=PX_PEM_DIRECT_QUERY=disabled
dx-agent flagged the insecure-credentials gap as blocking. The
direct-query listener was binding :50305 with
::grpc::InsecureServerCredentials(), so the JWT bearer + the PxL
body crossed the pod network in the clear. Any pod with network
reach to the PEM could capture a token and replay it within its
60-second exp window.

Fix: swap both Insecure* creds in MaybeStartDirectQueryServer to
SSL::DefaultGRPCServerCreds() (from src/vizier/services/agent/shared/
manager/ssl.h). That helper reuses the PEM's already-mounted
cluster TLS pair (PL_TLS_CA_CERT + PL_CLIENT_TLS_CERT +
PL_CLIENT_TLS_KEY in pem_daemonset.yaml — same env kelvin / metadata
/ broker use). Plaintext fallback only when an operator sets
PL_DISABLE_SSL=1, which is the cluster-wide dev/soak escape hatch
already documented for the other components — not a silent default.

Two call sites updated:
  - server_config->grpc_server_creds — Carnot's internal sink server
    config; not strictly needed (LocalGRPCResultSinkServer uses
    InProcessChannel) but matches the cluster's TLS policy in case
    a future caller swaps to a TCP channel.
  - builder.AddListeningPort — the EXTERNAL :50305 listener; this
    is the actual blocker fix.

DIRECT_QUERY_SECURITY.md: add a "Transport" section documenting
the TLS posture and the s_client/grpcurl validations to run on
the next soak; update the threat-model row on channel
confidentiality to reflect TLS-by-default.

Both build modes still green:
  bazel test //src/vizier/services/agent/pem:direct_query_server_test
  bazel build //src/vizier/services/agent/pem:cc_library
    --define=PX_PEM_DIRECT_QUERY=disabled
dx-agent's pxbroker.go pemdirect path dials the PEM at the node's
HOST_IP:50305. With direct-query now serving TLS (pem_manager.cc
swap to SSL::DefaultGRPCServerCreds in 847409f), the bearer JWT
rides an encrypted channel — but the PEM's TLS cert is the cluster
service cert whose SAN is the DNS name (vizier-pem-svc.pl.svc.…),
NOT the node IP. Chain+hostname verification therefore fails on
the node-IP dial.

Add WithDirectTLSSkipVerify() — sets disableTLSVerification=true
so the existing Client.init() builds the TLS dial config with
InsecureSkipVerify:true. The channel is encrypted; the cert is just
not chain/hostname-verified. Same posture the broker path uses for
in-cluster service-cert dials.

Strictly more secure than WithDirectCredsInsecure (which builds a
plaintext channel via insecure.NewCredentials) — JWTs no longer
travel in the clear on the pod network. Full CA+hostname verify is
future hardening (needs node-IP SANs on the PEM cert, or a
CA-pool+skip-hostname verifier); tracked as a follow-up.

Verified: bazel build //src/api/go/pxapi:pxapi green. arc lint
clean. dx-agent will bump dx's go.mod to this commit + ship the
pxbroker.go swap from WithDirectCredsInsecure to
WithDirectTLSSkipVerify.

Patch text was authored by dx-agent on the soak VM (cmd/dx-daemon
go module wasn't available there); committing on their behalf so
the dx side can pull it.
CodeRabbit r3357199175 — verifier previously only checked aud+exp, so any
HS256 token signed with PL_JWT_SIGNING_KEY and aud=vizier authenticated
(e.g., a kelvin-targeted token). Adds two claim checks matching what
manager.cc::GenerateServiceToken emits:

  iss must equal "PL"
  sub must equal "service"

Wrong-value and missing-claim paths each get a TEST_F. Existing positive
fixtures already mint these claims so they stay green (verified locally:
37 tests, 34 pass + 3 pre-existing skips).

CodeRabbit r3364977606 — DIRECT_QUERY_SECURITY.md still cited stale line
numbers from earlier iterations. Updated:
  pem_manager.cc:39 -> :47          (FLAGS_direct_query_jwt_signing_key)
  pem_manager.cc:115 -> :132        (MaybeStartDirectQueryServer)
  direct_query_server.cc:133 -> :151 (verifyHs256Jwt)
  manager.cc:423 -> :440            (GenerateServiceToken)
verifyHs256Jwt required sub=="service", but pixie service tokens
(GenerateJWTForService, claims.go) set sub=<serviceID> (e.g. "dx") and carry
"service" in the Scopes claim. Every real in-cluster caller (dx-daemon) was thus
rejected UNAUTHENTICATED "invalid bearer token" (live: pemdq9 + dx rc13; the
broker accepted the same token). The unit test masked it by minting sub="service".

Fix: require the "service" scope (Scopes claim, comma-joined); stop asserting the
subject — matching canonical pixie verify (jwt.go ParseToken: signature+audience).
Test mints realistic tokens (sub=serviceID, Scopes="service") with
kWrongScope/kMissingScope negatives.
@entlein entlein force-pushed the entlein/pem-direct-query branch from e075700 to c357699 Compare June 21, 2026 17:14
@entlein entlein requested a review from ddelnano June 21, 2026 17:38
entlein added a commit that referenced this pull request Jun 21, 2026
…bit items

User review 4536971862:

#2 passthrough/reconcile_test.go — strengthened with two new tests that
   exercise the FULL chain (loop → real sink.ClickHouseHTTP → httptest
   CH endpoint → reconcile recorder). TestTick_ReconcileCatchesCHSilentDrop
   mimics CH's X-ClickHouse-Summary silent-drop shape (200 OK,
   written_rows=0) and asserts the loop records WroteCount=0 with a
   silent-drop attribution — the exact R6 (sink-layer loss) regression
   the instrument exists to detect. TestTick_ReconcileAttributesCHFailureCorrectly
   covers the 500-response branch. The pre-existing in-process-fake test
   stays as the wiring check.

#4 pixie/pixie.go — added a long comment justifying the API-key auth
   choice. pixie.Client targets the Pixie CLOUD (cloudpb's PluginService),
   whose auth interceptor accepts pixie-api-key and rejects JWT service
   tokens (those are for INSIDE-cluster vizier services). The
   pixieapi.Adapter that talks to vizier directly already uses JWT via
   jwtutils.GenerateJWTForService — same pattern as
   cloud_connector/vizhealth/checker.go:111. So the split is intentional;
   flipping pixie.go to JWT would break cloud auth, not improve it.

#6 pxl/queryfor — hardened escapePxL so a raw \n, \r, \t or NUL byte
   in Target.Pod/Target.Namespace can't terminate the PxL string literal
   and inject a new statement. Added TestQueryFor_RejectsInjectionInTargetFields
   driving QueryFor with 7 adversarial pod/namespace shapes (newline,
   single-quote-only, CR, backslash-escape-of-escape, NUL, tab) plus
   the regex_match fallback path, asserting the output line count and
   every statement's leading token. Extends existing
   TestEscapePxL_TableDriven with the new escape mappings.

CodeRabbit oversights (verified each against current code):

CR r3379377432 (main.go) — wrapped the control-surface listener in
   http.Server with Read/ReadHeader/Write/Idle timeouts so a slow
   client can't pin a goroutine indefinitely.

CR r3379377607 (pixieapi.go) — switched direct-mode dial from
   pxapi.WithDisableTLSVerification (env + addr-gated, brittle) to
   pxapi.WithDirectTLSSkipVerify (added in PR #49 b523ce3 for the
   same node-IP-dial scenario). Removed the cluster.local + PX_DISABLE_TLS
   precondition in NewDirect; the always-skip semantics match the AE
   deployment shape. Refactored the obsolete env-gate test.

CR r3379377645 (streaming/filter.go) — the deltaCh-close path returned
   without calling disarm(), leaking the timer's goroutine on shutdown.
   Now calls disarm() before return on chan-close.

CR r3426923299 (sink/clickhouse.go Record) — capped Record at a 2s
   per-call context timeout. The 30s chhttp default was too long for
   the scanner/passthrough/controller hot paths that call Record inline;
   reconcile is best-effort by contract, so a stalled CH must not pin
   the pull loop.

All 14 AE packages pass go test. arc lint: 0 Errors on PR-53 file
scope.

Signed-off-by: entlein <einentlein@gmail.com>
entlein added a commit that referenced this pull request Jun 21, 2026
)

* adaptive_export: production AE — streaming export + write-integrity

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: ADAPTIVE_PASSTHROUGH firehose loop

New env-gated background loop that runs the same PxL shape AE's
anomaly-gated path uses, but with an empty Target (no ns/pod predicate)
and over a configurable rolling window. Writes via the existing sink so
the byte-shape of forensic_db rows is comparable between the
PASSTHROUGH=1 phase (EVERYTHING) and the PASSTHROUGH=0 phase
(AE-FILTER). One-shot A/B that yields the per-table capture fraction of
the adaptive write path.

- internal/passthrough/passthrough.go — Loop + Config; defaults to
  30s window / 30s refresh / clickhouse.PixieTables() table list.
- internal/passthrough/passthrough_test.go — 6 tests; the load-bearing
  one is TestLoop_EmitsEmptyTargetPxL (asserts neither df.namespace nor
  df.pod predicates appear in the emitted PxL).
- cmd/main.go: ADAPTIVE_PASSTHROUGH + _WINDOW_SEC + _REFRESH_SEC env
  knobs. Adapter is constructed unconditionally when passthrough is on
  (joins the existing PushPixie / streaming construction path so the
  same pxapi grpc stream is reused). Loop is registered with the
  shutdown WaitGroup so SIGTERM waits for the in-flight tick.
- cmd/BUILD.bazel: drop @px// load (other AE BUILD.bazel files use
  //bazel — sticking out as the only one with @px is a leftover from a
  prior gazelle run; align). Add passthrough dep.

Signed-off-by: entlein <einentlein@gmail.com>

* ci: dx-image workflow — build + publish dx-daemon to ghcr

Stand-alone workflow that builds entlein/dx (private Active-Diagnosis
Framework) into ghcr.io/k8sstormcenter/dx-daemon. Separates the dx image
publish from the bazel-based vizier_release pipeline; the dx repo ships
its own Dockerfile.dxd (Go cross-compile + distroless final stage) so it
doesn't need to live as a submodule inside src/vizier/services/dx.

Triggers:
- tag push 'release/dx/v*' on this repo cuts a release build, image tag
  derived from the tag suffix (release/dx/v0.1.0 -> image tag 0.1.0).
- workflow_dispatch lets us build any dx ref on demand with a custom tag
  (default: short sha of the resolved dx commit).

Pulls dx via DX_ENTLEIN_PAT (already configured on the repo). Multi-arch
build (linux/amd64, linux/arm64); Dockerfile.dxd cross-compiles in the
native BUILDPLATFORM stage and the final stage is COPY-only, so target
emulation isn't required.

Signed-off-by: entlein <einentlein@gmail.com>

* Revert ci: dx-image workflow — wrong repo

The dx image build pipeline lives in entlein/dx itself (PR #53,
branch feat/bazel-release): bazel-based with @px external pin to
pixie's ae-prod tip, pushes to docker.io/entlein/dx-daemon on a
release/dx/v* tag in the dx repo. The pixie-side buildx workflow
this reverts duplicated that intent in the wrong repo + the wrong
build system (docker buildx instead of bazel + pl_go_image macros)
+ the wrong registry (ghcr.io/k8sstormcenter instead of
docker.io/entlein).

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: unit-normalize trigger watermark cursor + load-test affordances

Fixes the silent-halt bug: the trigger gated on a RAW event_time high-water-mark,
so a single anomaly in a larger unit (ms/ns) drove the watermark past all real
seconds rows and AE stopped processing forever (data still on Pixie). Normalize
event_time to canonical nanoseconds in the poll SELECT filter+order and in the
in-memory/persisted cursor, boundary-dedup, and maxSeen (normalizeEventTimeNanos
+ chNormEventTimeNanos). Validated at the data layer: vs a poisoned watermark the
raw filter returns 0 rows, the normalized filter recovers all 60.

Also adds ADAPTIVE_PUSH_REFRESH_SEC (negative = single-shot pull) for the
reproducible load-test harness, an in-package trigger unit test, and an e2e
hermetic load test (mock PixieQuerier, exact rows+bytes).

Signed-off-by: entlein <einentlein@gmail.com>

* e2e_test/adaptive_export_loadtest: AE fixture-isolation load-test harness

Consolidate the adaptive_export load-test harness under src/e2e_test/ (matching
vzconn_loadtest / px_cluster conventions). Control-plane experiments (E1-E4, E6,
E8 sustained) are proven exactly-reproducible on a live rig; the data-plane
experiments (E5, E8 data-mode) are authored and pending live validation on a
vizier-registered rig (status documented in README + FINDINGS_AND_BACKLOG).

- harness/: shell + python (inject, exp_control, exp_e8, stats, ...) + lib helpers
- fixtures/EXPERIMENTS.md: curated kubescape_logs data-set catalog + expected outputs
- k8s/: isolated sinks + per-rep generator pod (no probes)
- tools/loadgen/: cleanloadgen + httpsink (docker-built test tool; .bazelignore'd
  pending a bazel target — lib/pq is already vendored in the module)
- FINDINGS_AND_BACKLOG.md: F8 watermark-poison bug + the fix + AE backlog

The AE Go unit/e2e tests live with the service (internal/{trigger,e2e}).

Signed-off-by: entlein <einentlein@gmail.com>

* e2e_test/adaptive_export_loadtest: document AE implied contracts (C1-C14) + diagrams

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export_loadtest: C15 write-duration contract + DX-steering diagram; gen sustained-DNS mode

C15 = AE must keep re-pulling+writing an active pod until t_end or DX stop (the
contract DX steers on; last week's 'wrote then stopped' is its violation). Add
DX-steering sequence diagram. Generator gains SUSTAIN_SEC (distinct-DNS trickle,
a Pixie-traced protocol) + configurable SETTLE_PRE_MS warm-up for fresh-pod
capture; harness wires GEN_SUSTAIN_SEC/GEN_SETTLE_MS.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export/trigger: update test SQL substrings for multiIf normalisation

The 700821d trigger unit-normalisation wrapped event_time in a
multiIf(...) inside both the WHERE filter and the ORDER BY. Three
existing tests in watermark_test.go + one in clickhouse_test.go pinned
the raw 'event_time >= N' substring and broke at HEAD.

Update each test's expected substring to match the new normalized form
(') >= <ns-scaled N>' — the closing paren of multiIf, then the value
in canonical nanoseconds). Per-test ns-scaling:

  watermark_test.go:94  1744000000000000000  already ns -> unchanged
  watermark_test.go:125 InitialWatermark=42  < 1e10 sec -> * 1e9
  watermark_test.go:156 InitialWatermark=7   < 1e10 sec -> * 1e9
  watermark_test.go:297 event_time='5000'    < 1e10 sec -> * 1e9
  clickhouse_test.go:82 ref.T=1744..303e9 ns already ns -> unchanged

go test ./src/vizier/services/adaptive_export/... all green.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export_loadtest: exp_control uses real now_s event_time (no future-stamp watermark poison)

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: ADAPTIVE_RECONCILE per-pull write-fidelity instrument

Records one forensic_db.ae_reconcile row per data-plane pull (read_count vs
wrote_count, window, ns/pod) across ALL three write paths — controller fan-out
(filter), passthrough firehose, and streaming scanner — so a reconcile run
localizes loss to query (R5: read<PEM) vs sink (R6: wrote<read) and quantifies
re-pull dup (C8). Counts alone (write >= read) were proven insufficient.

- new internal/reconcile leaf package (Row, Recorder, Nop) — no import cycle
- sink.Record: CH-backed recorder (INSERT INTO forensic_db.ae_reconcile)
- ae_reconcile table: schema.sql + KnownTables + OperatorOwnedTables (synced);
  not a pixie table (absent from PixieTables, so VerifyPixieSchema ignores it)
- wired: passthrough.tick, controller.pushPixieRows (deferred, all return
  paths), streaming.scanner.Run; gated by ADAPTIVE_RECONCILE=true, else Nop
- unit test proves read/wrote capture incl. the sink-drop read>wrote shape
- fixed apply_test trailing-tables guard for the new operator table
- harness: exp_row_reconcile.sh (row-level PEM<->CH), ae_vs_all.sh, exp_datavolume_extreme.sh

Signed-off-by: entlein <einentlein@gmail.com>

* harness: exp_pipeline_reconcile — skip empty-key rows (0 rows != LOSS 1)

px -o json empty result previously printed one blank line → counted as a phantom
LOSS=1. Guard: empty set → 0-byte keys file; drop all-empty-field keys. Confirmed
against the controlled log4j run (backend http 14/14 exact, conn 66>=12, no loss).

Signed-off-by: entlein <einentlein@gmail.com>

* harness: log4shell_fire.sh — reliably fire + restart the log4j-chain log4shell

Reliable BY CONSTRUCTION against bob#140 (stateful/unreliable exploit on re-fire):
fresh-JVM backend (delete pod) + attacker-before-backend + the WORKING resolvable FQDN
attacker.<ns>.svc.cluster.local:1389 (NOT the bare attacker-ns.svc which NXDOMAINs and
gets dropped), then VERIFY the actual backend->:1389 LDAP egress in forensic_db.conn_stats
and RETRY until confirmed (the validity gate). Never assumes the exploit fired. Node-side.

Signed-off-by: entlein <einentlein@gmail.com>

* harness: log4shell_fire.sh — detection-signal framing (Cyber Verification)

Reword from offensive 'exploit' to detection-signal-generation language: this validates
the kubescape->DX->AE detection chain. No logic change.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export(passthrough): precompiled + concurrent firehose, drop http2

- pxl.CompilePassthrough/Render: precompile per-table PxL once (fixed
  window => constant relative start_time), only the two time_ bounds are
  stamped per tick. Rendered output is byte-identical to QueryFor with an
  empty Target (TestCompilePassthrough_MatchesQueryFor), so this is a
  structural change, not a capture change. upid->pod/ns stays in PxL.
- passthrough: tickConcurrent fans every table out at once (was a serial
  loop); shared pull() helper. Sink/recorder are pool/HTTP-backed and
  already called concurrently elsewhere.
- drop http2_messages.beta from the firehose set (not materialised on
  every cluster => ""Table not found"" every tick); shared PixieTables/DDL
  lists untouched.
- toggle ADAPTIVE_PASSTHROUGH_COMPILED (default on; =false reverts to the
  legacy serial QueryFor path).

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: bazel BUILD deps for internal/reconcile + pxl compile.go

Fixes "missing strict dependencies: import of .../internal/reconcile" that
broke the AE image build for passthrough, sink, streaming, controller, cmd
(pre-existing since the ADAPTIVE_RECONCILE commit added the package + imports
without bazel deps; never CI-built). Also wires the new pxl/compile.go srcs
+ passthrough/pxl test srcs (compiled_test.go, reconcile_test.go, compile_test.go).

- new internal/reconcile/BUILD.bazel (go_library, stdlib-only)
- +//internal/reconcile dep: passthrough, sink, streaming, controller, cmd
- pxl go_library +compile.go; pxl_test +compile_test.go; passthrough_test +compiled_test.go,reconcile_test.go (+reconcile dep)

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export(pxl): raise Pixie 10k result cap via #px:set query flag

F1 RCA: Pixie caps every px.display at max_output_rows_per_table (default
10000, query_flags.go) — the planner add_limit_to_batch_result_sink_rule
silently truncates wide firehose windows / busy pods at the READ (write path
is clean: ae_reconcile shows read==wrote). Fix uses Pixie own native knob —
prepend `#px:set max_output_rows_per_table=1000000` to every generated PxL
(QueryFor + CompilePassthrough) so all pull paths are uncapped. Validated on
rig: a 14208-row window returned 10000 (capped) vs 14298 (with flag). No
pagination loop, no extra round-trips. See memory project-ae-passthrough-10k-cap.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export/sink: content_type silent-drop contract suite

Consolidates the recurring content_type silent-drop incident class
into one default-suite test gate (6 tests, ~15ms):

  I1 TestContract_ContentTypeIsInt64InSchema
  I2 TestContract_FastEncodeContentTypeAsInt
  I3 TestContract_SilentDropDetected
  I3.b TestContract_SilentDropNotTriggeredOnSuccess
  I3.c TestContract_SilentDropToleratesMissingSummaryHeader
  I4 TestContract_HTTPEventsRoundTrip

Top-of-file docstring chronicles the incident timeline so future
operators can grep their way to the contract.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export_loadtest: DX-steered-vs-ALL datavolume reduction harness

Measures the AdaptiveExport value prop: datavolume REDUCTION of DX-steered AE
(rev-3 streaming, AE writes only DX-steered activeSet pods over the control
surface) vs saving ALL data (passthrough firehose). Two arms, same fixed load,
forensic_db active-part deltas (rows+bytes) per table; reduction = 1 - DX/ALL.
Uses the canonical resolvable JNDI FQDN (attacker.attacker-ns.svc.cluster.local)
so the chain fires + DX can classify (a malformed host → NXDOMAIN → no steer).
Successor to ae_vs_all.sh, whose AE arm used the rev-2 controller gate + stale JNDI.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export_loadtest: deep AE NFR benchmark harness

Measures all AE non-functional requirements under steady load on the rig:
throughput (rows+bytes/sec), capture completeness (AE read vs broker count =
F1 cap proof), write fidelity (read==wrote + write-error count), end-to-end
freshness latency (now - max(time_) in CH), resource footprint (AE pod cpu/mem
idle vs loaded), per-cycle cadence. Emits a structured report; companion to
exp_dx_steering_reduction.sh. Real-data only.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export_loadtest: fix DX-reduction dead-arm (clear stale steering + live-pod guard)

Run-1 reported false 100% reduction because stale adaptive_attribution windows
rehydrated DEAD pods (deleted loaders) into the activeSet → AE streamed dead pods
→ 0 rows. Clear adaptive_attribution before the DX arm so the activeSet only gets
freshly-steered LIVE pods; add a guard that prints the steered pods + marshalsec
fire count + live log4j-poc pods so a dead-arm result is caught, not reported as
a reduction.

Signed-off-by: entlein <einentlein@gmail.com>

* nfr harness: fix lag (dateDiff) + drop racy broker-pct completeness

lag query used now()-DateTime64 (type error -> na); use dateDiff(second,...).
Capture-completeness vs broker was window-misaligned (623% artifact) -> drop it;
report tot_read vs tot_wrote (read==wrote) + errs instead. The 10k-cap/completeness
proof is the dedicated F1 test (max_read>10000 vs broker for the SAME window).

Signed-off-by: entlein <einentlein@gmail.com>

* dx-reduction harness: report ROWS reduction (primary) + bytes (secondary)

Run-2 byte-delta reduction came out negative because system.parts byte delta is
compaction-noisy (merges land mid-window). Report rows reduction as primary
(actual captured-row count, noise-free); keep bytes as secondary context.

Signed-off-by: entlein <einentlein@gmail.com>

* ae deployment: add memory limit (1Gi) + raise cpu limit to 1 core

Eviction-RCA finding (PR #63 NFR campaign): AE had NO memory limit (only cpu
300m) and was CPU-pinned at 300m under concurrent passthrough. AE measured tiny
(16-38Mi steady), but the raised 1M-row passthrough cap can spike, so cap at 1Gi
so AE can never memory-pressure a node; raise cpu limit 300m->1 core (was
throttling). NOTE node evictions were NOT AE/OOM — node-01 went NotReady
(network/heartbeat); the memory consumer is PEM (1365Mi, OOMs at the 2Gi default).

Signed-off-by: entlein <einentlein@gmail.com>

* ae bootstrap: separate the secret from the re-applied infra bundle

Root cause of the recurring "AE unauthenticated / writes 0 / crashloop" reverts:
kustomization.yaml bundled adaptive_export_secrets.yaml (placeholder pixie-api-key)
with the role+deployment, so EVERY infra re-apply (make log4j) clobbered the real
key that ae-auth had written. Separation of concerns: remove the secret from the
kustomization — infra (role+deployment) stays re-appliable; the secret holds real
creds and is owned solely by `make ae-auth`, created once, never touched by infra
re-applies. Secret manifest kept as a hand-applied seed-only template (documented).

Signed-off-by: entlein <einentlein@gmail.com>

* dx-reduction harness: fire BOTH attack stages so DX steers the backend

The DX-steered arm was failing because the harness only fired stage-1 (JNDI/LDAP).
That generates ldap-egress but NO kubescape R0001 → backend never flagged → DX no
case → indeterminate → AE steers wrong/no pods. R0001 comes from stage-2 (post-
exploitation exec). fire() now does stage-1 (JNDI) + stage-2 (whoami/shadow/token/
getent in the backend) → kubescape R0001+R0006 → DX rules backend MALIGNANT →
backend enters AE activeSet → reduction is measurable. Verified live: DX evidence
unexpected-spawn+sensitive-file-read → verdict ruled_in generic=MALIGNANT.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: rename whitelist→allowlist across streaming path + add DX-steering diagnostics

Standing terminology rule: allowlist/blocklist, never whitelist/blacklist.
Pure rename (no behavior change) of the rev-3 streaming filter:
  FilterModeWhitelist  → FilterModeAllowlist
  MaxWhitelistSize     → MaxAllowlistSize
  ADAPTIVE_STREAM_MAX_WHITELIST → ADAPTIVE_STREAM_MAX_ALLOWLIST  (env)
  mode=whitelist log string → mode=allowlist
plus all comments/identifiers/tests in streaming, activeset, cmd/main.

DX-steering diagnostics (the reason DX-arm-writes-0 has been hard to RCA —
we could not tell "empty ActiveSet" from "broker returned 0 rows"):
  - scanner: log the empty-allowlist short-circuit (was silent) so an
    empty ActiveSet is visible in logs, distinct from "query completed rows=0".
  - FilterUpdater: emitted-filter log Debug→Info so the steered pod count
    per ActiveSet change is visible without debug logging.

NOTE: ADAPTIVE_STREAM_MAX_WHITELIST env renamed → tooling that sets the old
name must switch to ADAPTIVE_STREAM_MAX_ALLOWLIST.

Signed-off-by: entlein <einentlein@gmail.com>

* ae(clickhouse): create forensic_db.dx_attack_graph at boot

The Pixie dx_evidence_graph UI reads dx_attack_graph via px.DataFrame
clickhouse_dsn, whose query template hardcodes event_time + hostname and
ORDER BY event_time. A table without those columns fails 'Unknown
identifier event_time'; a table created by hand (local, not via the
operator) isn't globally registered. Fix: make AE own it like the other
forensic tables.

- schema.sql: dx_attack_graph DDL with event_time(UInt64 nanos) + hostname,
  edge columns, fromUnixTimestamp64Nano partition/TTL (nanos-correct).
- KnownTables + OperatorOwnedTables: register it so Apply creates it at boot.
- apply_test: assert last-applied DDL == last OperatorOwnedTables entry
  (robust to appended operator tables) instead of hardcoding trigger_watermark.

go test ./.../clickhouse green.

Signed-off-by: entlein <einentlein@gmail.com>

* ae(clickhouse): dx_attack_graph numeric cols Int64/Float64 (px-readable)

Pixie's clickhouse_dsn type mapper reads UInt8 as BOOLEAN and does not
handle UInt16/UInt32/Float32 -> px fails with 'Column[N] given incorrect
type' rendering the dx_evidence_graph. weight/max_severity/num_findings
-> Int64, confidence -> Float64 (map cleanly to INT64/FLOAT64). Verified
live: px run returns all 6 edges with every column. event_time stays
UInt64 (matches kubescape_logs, which px reads).

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export(streaming): add #px:set max_output_rows cap flag to scanner buildPxL

The DX/streaming arm silently capped each per-table pull at Pixie's default
10000-row limit while the passthrough/ALL arm (pxl.CompilePassthrough /
QueryFor) already raises it to 1,000,000 via the broker's #px:set query flag.
Validated live on 6a33dac0: a single streaming http_events pull returned
exactly rows=10000 (the cap). Left unfixed this UNDER-counts the DX arm and
OVERSTATES the DX-vs-ALL volume reduction. Prepend the same #px:set directive
to the streaming scanner's PxL so both arms are uncapped and comparable.

Signed-off-by: entlein <einentlein@gmail.com>

* ae(clickhouse): create dx_attack_graph_malicious view at boot

Adds the rule-ins-only view (condition != '') to the canonical schema.sql,
registers it in KnownTables + OperatorOwnedTables (after dx_attack_graph), and
teaches DDL() to extract CREATE VIEW headers. AE now creates it on boot so the
dx_evidence_graph UI's default malicious-only read is standard, not a per-rig
manual step. Tests updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: entlein <einentlein@gmail.com>

* ae(control): /dx/attack_graph ingest endpoint -> ClickHouse

dx POSTs a JSON array of edges to /dx/attack_graph; AE writes them to
forensic_db.dx_attack_graph via JSONEachRow (Applier.WriteAttackGraph). Wired in
main.go when CONTROL_ADDR is set; 501 if the sink is unset. This is the AE half
of the dx->AE->CH attack-graph write path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: convention pass — consolidate CH HTTP, drop dead code, fix stale tests

PR-53 review follow-ups (see review summary in conversation):

1. License headers added to 17 src/e2e_test/adaptive_export_loadtest/
   harness/*.{sh,py} scripts. Matches the convention every other
   src/e2e_test/*/sh in pixie already follows.

2. Dead code removed (was unreachable per golang.org/x/tools/cmd/deadcode):
   - internal/script/script.go: IsClickHouseScript, IsScriptForCluster,
     GetActions, getScriptName, getInterval, templateScript, plus the
     ScriptConfig and ScriptActions types. The cron-script sync flow they
     served was replaced by the streaming model; only the Script and
     ScriptDefinition types remain.
   - internal/pixie/pixie.go: Client.GetPresetScripts (replaced by
     builtinPresetScripts() inline in cmd/main.go).
   - internal/streaming/{supervisor,writer}.go: SupervisorStats,
     TableStats, Stats types + Supervisor.Stats and BatchWriter.Stats
     methods (no production reader). Atomic counters dropped; the
     existing flush log preserves the per-flush summary.

3. Stale passthrough tests fixed.
   TestLoop_DefaultsTablesToPixieTables and TestNew_AppliesDefaults
   asserted len(clickhouse.PixieTables()) == 13 but passthrough.New
   strips excludedTables (http2_messages.beta), yielding 12. Tests now
   compare against filterExcluded(clickhouse.PixieTables()) and add an
   extra assertion that excluded tables were not written.

4. ClickHouse HTTP client consolidation: new internal/chhttp/ package
   collapses three near-identical HTTP CH clients
   (clickhouse.Applier, sink.ClickHouseHTTP, trigger.ClickHouseWatermarkStore)
   into one. Centralises endpoint validation, basic-auth header,
   30s default timeout, fail-loud INSERT settings (4 CH input_format
   knobs), and the X-ClickHouse-Summary read path. The 4 INSERT
   call sites in the three callers all route through chhttp.Client.Insert
   now; SELECT through Query or QueryStream (the latter preserves the
   QueryActive streaming behaviour). Net code: -200 LOC across the
   three callers plus a 200-LOC chhttp package with its own tests.

5. Pixie service scaffold wired into cmd/main.go:
   services.SetupService("adaptive-export", 50900) +
   services.SetupSSLClientFlags() + services.PostFlagSetupAndParse() +
   services.SetupServiceLogging(). Matches the pattern every other
   pixie Go service uses. CheckServiceFlags() is deliberately skipped
   (AE does not run a TLS gRPC server). Existing AE env-var reads
   (ADAPTIVE_*) are untouched and still authoritative for tuning knobs.

All 14 adaptive_export internal packages pass go test (1 new chhttp,
13 unchanged), including the 3 differential oracle tests in
pxl/compile_test.go. arc lint OKAY for everything except 3 pre-existing
findings unrelated to this commit (loadgen has its own go.mod; one
QF1001 De Morgan's-law nit in reconcile_test.go from c9f19b6).

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: restore executable bits on harness scripts (post-header)

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: lint pass + restore @px load prefix in cmd/BUILD.bazel

User flagged on review 4536971862:
- cmd/BUILD.bazel:18 dropped the '@px' external-workspace prefix on
  pl_build_system.bzl load. Restored — the standalone AE build pulls
  pixie as @px and needs the qualifier.

Lint cleanup (PR-53 scope, no production code touched outside renames):
- chhttp/chhttp_test.go: errcheck on two w.Write calls + gofumpt on
  the gotSettings declaration.
- passthrough/reconcile_test.go: staticcheck QF1001 (De Morgan's law)
  on the conn_stats sink-drop assertion.
- script/script.go: rename ScriptId -> ScriptID (ST1003). Propagated
  to pixie/pixie.go and cmd/main.go callsites.
- internal/e2e/BUILD.bazel and internal/trigger/BUILD.bazel: gazelle
  drift — adding loadtest_test.go and clickhouse_internal_test.go to
  srcs.
- k8s/00-sinks.yaml + gen-pod.tmpl.yaml: yamllint compliance —
  document-start marker, dedent sequence items per .yamllint
  indent-sequences=false, tighten flow-mapping spaces, collapse
  multi-space after commas. YAML semantics unchanged.
- harness/stats.py: flake8 E501 — split a long line into two.

Final arc lint state on PR-53 file scope: 0 Errors, 14 Warnings
(SHELLCHECK SC2155/SC2086 in pre-existing harness scripts from
ae7b86f, not introduced or modified by this commit). Pre-existing
loadgen typecheck failures (separate go.mod) are unaffected.

Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: address user review #2 #4 #6 + 4 outstanding CodeRabbit items

User review 4536971862:

#2 passthrough/reconcile_test.go — strengthened with two new tests that
   exercise the FULL chain (loop → real sink.ClickHouseHTTP → httptest
   CH endpoint → reconcile recorder). TestTick_ReconcileCatchesCHSilentDrop
   mimics CH's X-ClickHouse-Summary silent-drop shape (200 OK,
   written_rows=0) and asserts the loop records WroteCount=0 with a
   silent-drop attribution — the exact R6 (sink-layer loss) regression
   the instrument exists to detect. TestTick_ReconcileAttributesCHFailureCorrectly
   covers the 500-response branch. The pre-existing in-process-fake test
   stays as the wiring check.

#4 pixie/pixie.go — added a long comment justifying the API-key auth
   choice. pixie.Client targets the Pixie CLOUD (cloudpb's PluginService),
   whose auth interceptor accepts pixie-api-key and rejects JWT service
   tokens (those are for INSIDE-cluster vizier services). The
   pixieapi.Adapter that talks to vizier directly already uses JWT via
   jwtutils.GenerateJWTForService — same pattern as
   cloud_connector/vizhealth/checker.go:111. So the split is intentional;
   flipping pixie.go to JWT would break cloud auth, not improve it.

#6 pxl/queryfor — hardened escapePxL so a raw \n, \r, \t or NUL byte
   in Target.Pod/Target.Namespace can't terminate the PxL string literal
   and inject a new statement. Added TestQueryFor_RejectsInjectionInTargetFields
   driving QueryFor with 7 adversarial pod/namespace shapes (newline,
   single-quote-only, CR, backslash-escape-of-escape, NUL, tab) plus
   the regex_match fallback path, asserting the output line count and
   every statement's leading token. Extends existing
   TestEscapePxL_TableDriven with the new escape mappings.

CodeRabbit oversights (verified each against current code):

CR r3379377432 (main.go) — wrapped the control-surface listener in
   http.Server with Read/ReadHeader/Write/Idle timeouts so a slow
   client can't pin a goroutine indefinitely.

CR r3379377607 (pixieapi.go) — switched direct-mode dial from
   pxapi.WithDisableTLSVerification (env + addr-gated, brittle) to
   pxapi.WithDirectTLSSkipVerify (added in PR #49 b523ce3 for the
   same node-IP-dial scenario). Removed the cluster.local + PX_DISABLE_TLS
   precondition in NewDirect; the always-skip semantics match the AE
   deployment shape. Refactored the obsolete env-gate test.

CR r3379377645 (streaming/filter.go) — the deltaCh-close path returned
   without calling disarm(), leaking the timer's goroutine on shutdown.
   Now calls disarm() before return on chan-close.

CR r3426923299 (sink/clickhouse.go Record) — capped Record at a 2s
   per-call context timeout. The 30s chhttp default was too long for
   the scanner/passthrough/controller hot paths that call Record inline;
   reconcile is best-effort by contract, so a stalled CH must not pin
   the pull loop.

All 14 AE packages pass go test. arc lint: 0 Errors on PR-53 file
scope.

Signed-off-by: entlein <einentlein@gmail.com>

* test(harness): consolidate to one run-picture + e2e CI workflow

Finish the harness consolidation #53 started: adopt exp_matrix.sh (canonical
ALL-vs-DX reduction matrix) + nfr.sh (throughput/mem/verdict-latency) as the
single runners; cut the overlapping variants (ae_vs_all, exp_datavolume_extreme,
exp_dx_steering_reduction, exp_ae_nfr_benchmark, exp_pipeline_reconcile) and the
superseded standalone setup (deploy_ae, build_gen_image). README rewritten as the
single 'how to run' source: two families — fixture-isolation (run.sh + E-series)
and live-attack e2e (log4shell_fire → exp_matrix → nfr → exp_row_reconcile).

Add e2e_log4shell_soc.yaml: k3s + full SOC stack (Pixie/kubescape/ClickHouse/AE/dx
via k8sstormcenter/soc) on the oracle runner; asserts every canonical harness
script runs + dx rules in; profiles dx in real life (pprof CPU/heap + verdict
latency, uploaded). Uses existing repo secrets.

Signed-off-by: entlein <einentlein@gmail.com>

* revert(bazel): drop stray buildifier attribute-reorder in stirling container_images BUILD

This file is unrelated to adaptive_export — the only change was buildifier
alphabetizing container_type/bazel_sdk_versions (no functional change). Restore
main's version to keep #53's diff to real AE changes.

Signed-off-by: entlein <einentlein@gmail.com>

* ci: fix run-genfiles + run-container-lint on PR 53

run-genfiles: the PR had reordered the kwargs in stirling/.../container_images/
BUILD.bazel alphabetically (bazel_sdk_versions before container_type) — a
local buildifier or gazelle drift from when the file was first committed.
CI gazelle wants the original order (container_type first), so the diff
loop fails. Restored to origin/main's ordering. Local gazelle disagrees
with CI's expected output (tooling-version drift); CI is authoritative.

run-container-lint: two findings.

  1. staticcheck QF1001 (De Morgan's law) in
     pxl/queryfor_test.go:318. Rewrote the !(a || b || c || d) negated
     disjunction as the equivalent !a && !b && !c && !d. Test behaviour
     unchanged; verified locally with TestQueryFor_RejectsInjection.

  2. golangci-lint typechecking failed on
     src/e2e_test/adaptive_export_loadtest/tools/loadgen/cmd/{cleanloadgen,
     httpsink}/main.go because that subtree carries its own go.mod and
     does not resolve as a package under the root px.dev/pixie module.
     Added the loadgen subtree to .arclint's exclude list. Same fix the
     existing entries for other-module subtrees apply.

Local 'arc lint' on the PR-53 file scope: 0 Errors, 14 SHELLCHECK
Warnings + 26 SHELLCHECK Advice (all pre-existing in ae7b86f
harness scripts; not introduced by this PR).

Signed-off-by: entlein <einentlein@gmail.com>

* ci: apply gazelle's actual kwarg order to stirling container_images

run-genfiles CI re-failed after f244ffc reverted this file to main's
ordering — turns out gazelle on this repo IS alphabetizing the kwargs
(bazel_sdk_versions before container_type), and CI runs 'gazelle fix'
then 'git diff' to catch any drift. So main's ordering is no longer
gazelle-stable; the file has to match gazelle's preference, not main's.

Verified locally with 'bazel run //:gazelle -- fix'; the file is now
idempotent (a second gazelle run produces no diff).

Signed-off-by: entlein <einentlein@gmail.com>

* ci: fix container-lint Errors on e2e workflow + new harness scripts

run-container-lint re-failed after the run-genfiles fix because two
files added on this branch (a03aa15) had unfixed lint errors:

.github/workflows/e2e_log4shell_soc.yaml — 5 yamllint Errors:
- 1 indentation: list items under steps: must be parent-aligned per
  the repo's .yamllint config (indent-sequences: false), not 2-indented.
  Dedented every step item + its run: block by 2 spaces.
- 4 line-length (>120 chars): split the long kubectl-set-image, the
  long grep-detection gate, the curl pprof URL, and the verdict-latency
  grep across continuation lines. Semantics unchanged.

src/e2e_test/adaptive_export_loadtest/harness/{exp_matrix.sh, nfr.sh}
— missing Apache headers. Applied via arc lint --apply-patches; exec
bits restored.

Local arc lint on PR-53 file scope: 0 Errors, 14 SHELLCHECK Warnings
+ 26 Advice (all pre-existing in harness scripts, unchanged).

Signed-off-by: entlein <einentlein@gmail.com>

* ci: silence 6 SHELLCHECK Warnings to clear container-lint exit code

arc lint --apply-patches exits non-zero on Warning level too. Resolved
each: replaced unused 'for i/t in ...' loop vars with '_', split a
SC2155 declare-and-assign, dropped two never-referenced hip/pip
assignments.

Signed-off-by: entlein <einentlein@gmail.com>

---------

Signed-off-by: entlein <einentlein@gmail.com>
Co-authored-by: Entlein <eineintlein@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: pixie-agent <noreply@local>
@entlein

entlein commented Jun 22, 2026

Copy link
Copy Markdown
Author

@CodeRabbit : rereview the entire PR now, it seems ready to merge

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== direct_query_server.h ==="
cat src/vizier/services/agent/pem/direct_query_server.h

echo ""
echo "=== direct_query_server.cc ==="
cat src/vizier/services/agent/pem/direct_query_server.cc

echo ""
echo "=== direct_query_server_test.cc (first 300 lines) ==="
head -300 src/vizier/services/agent/pem/direct_query_server_test.cc

Length of output: 41934


🏁 Script executed:

#!/bin/bash
echo "=== direct_query_server_test.cc (lines 300-600) ==="
sed -n '300,600p' src/vizier/services/agent/pem/direct_query_server_test.cc

echo ""
echo "=== direct_query_server_test.cc (lines 600-895) ==="
sed -n '600,895p' src/vizier/services/agent/pem/direct_query_server_test.cc

Length of output: 30332


🏁 Script executed:

#!/bin/bash
echo "=== pem_manager.cc (new additions only - last 200 lines) ==="
tail -250 src/vizier/services/agent/pem/pem_manager.cc

echo ""
echo "=== pem_manager.h ==="
cat src/vizier/services/agent/pem/pem_manager.h

echo ""
echo "=== DIRECT_QUERY_SECURITY.md ==="
cat src/vizier/services/agent/pem/DIRECT_QUERY_SECURITY.md

echo ""
echo "=== DIRECT_QUERY_CONTRACT.md ==="
cat src/vizier/services/agent/pem/DIRECT_QUERY_CONTRACT.md

Length of output: 44645


🏁 Script executed:

#!/bin/bash
echo "=== BUILD.bazel (pem) ==="
cat src/vizier/services/agent/pem/BUILD.bazel

echo ""
echo "=== manager.cc changes ==="
cat src/vizier/services/agent/shared/manager/manager.cc | grep -A 20 "jwt_signing_key"

echo ""
echo "=== opts.go ==="
cat src/api/go/pxapi/opts.go

echo ""
echo "=== delete.go ==="
cat src/utils/shared/k8s/delete.go

Length of output: 25624



@entlein — comprehensive re-review completed. Here is the full assessment:


Overall: Functionally merge-ready ✅

All seven original review requirements and both blockers (TLS + auth bug) are resolved. The implementation is solid. I found three minor issues below — none block merge, but one has a misleading comment worth correcting.


✅ What is now correct

Area Status
Auth bug (sub=="service"Scopes check) Fixed in verifyHs256Jwt; unit tests mint realistic tokens (sub="dx", Scopes="service")
TLS (InsecureServerCredentialsSSL::DefaultGRPCServerCreds()) Fixed in MaybeStartDirectQueryServer (step 6/6)
Security documentation (DIRECT_QUERY_SECURITY.md) Comprehensive — key flow, threat model, transport, discouraged practices, failure modes, rotation limitations
Feature toggles (runtime + compile-time) --direct_query_enabled=false skips port binding; PX_PEM_DIRECT_QUERY=disabled strips all feature bytes
Compile-time toggle tests CompiledOut_ValidToken_StillUnauthenticated + ToggleContract_DocumentBothLevels cover both modes
JWT tampering coverage 18+ named test cases covering alg-none, sig-flip, payload-flip, truncation, concatenation, alg-confusion
Fail-soft startup 6-step breadcrumbs, all failure paths return Status::OK() to avoid crashing the data plane
Scope check (service tokens) Scopes claim split on , and checked for "service"; not subject claim
Mutex serialization exec_mu_ per-instance, guards reset/execute/drain; ConcurrentQueries_AllSucceed tests N=8 parallel callers
Wire-format roundtrip drain drainSinkAndStream skips payload-less chunks; execution_and_timing_infoexecution_stats mapped

🟡 Minor issues (non-blocking)

1. Misleading comment in constantTimeEquals

direct_query_server.cc, the comment on constantTimeEquals says:

"Mismatched-length inputs trivially differ but we still walk the shorter to keep timing predictable across malformed lengths."

But the code returns immediately:

bool constantTimeEquals(absl::string_view a, absl::string_view b) {
  if (a.size() != b.size()) return false;  // ← returns immediately, doesn't walk
  ...
}

In practice this is not a security concern here since computed_mac (HMAC-SHA256) is always exactly 32 bytes, so the length branch is only hit when the decoded signature has a different length — which already means the token is malformed. Still, the comment should match the code. Either remove the "we still walk" sentence or add the fixed-length early-exit path before the size check to make the comment truthful.

2. exp claim doesn't handle int32

direct_query_server.cc:228–238 handles IsInt64, IsUint64, IsDouble but not IsInt (RapidJSON's 32-bit integer type). JWT libraries can technically serialize exp as a 32-bit integer for dates before year 2038. This will be hit only on non-standard JWT minters but is a one-liner fix:

} else if (payload["exp"].IsInt()) {
  exp_secs = static_cast<int64_t>(payload["exp"].GetInt());
}

3. DIRECT_QUERY_CONTRACT.md env-var label for the direct-query signing key

Line 20 of DIRECT_QUERY_CONTRACT.md maps --direct_query_jwt_signing_key to env PL_JWT_SIGNING_KEY, which is the same env var name as the main signing key in manager.cc. The actual flag definition in pem_manager.cc almost certainly reads a different env var (e.g. PL_DIRECT_QUERY_JWT_SIGNING_KEY or falls back to PL_JWT_SIGNING_KEY). If the env var really is shared, the table is accurate but could confuse operators who try to set one independently. Worth a clarifying note in the doc.


Remaining skipped tests (acceptable)

  • FailSoft_BrokerFailureToleratedByDirectQuery — SKIP'd with a clear explanation and a tracked follow-up (hoist MaybeStartDirectQueryServer or broker-optional Manager mode). Acceptable for this PR's scope.
  • Benchmark_PemDirect_Vs_BrokerPath_RedPlaceholder — SKIP'd with pemdq5 soak data cited (43.5s/q vs 27s/q, dominant factor = second Carnot). The placeholder names the issue greppably.
  • PerPodFilter_MetadataConnected — SKIP'd with correct rationale (needs metadata + multi-pod fixture).

Verdict

The implementation is complete. The critical auth bug and TLS blocker that were merge-blockers are both resolved and live-verified on pemdq10. The three items above are cosmetic/defensive and can be addressed in a follow-up. This PR is ready to merge.

@entlein entlein merged commit 610975b into main Jun 22, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants