Skip to content

Add Pixie OSS syncing. Modify certain fork changes to make it more maintenance friendly - #29

Merged
ddelnano merged 1 commit into
mainfrom
ddelnano/add-oss-sync-workflow
Feb 26, 2026
Merged

Add Pixie OSS syncing. Modify certain fork changes to make it more maintenance friendly#29
ddelnano merged 1 commit into
mainfrom
ddelnano/add-oss-sync-workflow

Conversation

@ddelnano

Copy link
Copy Markdown

Summary: Add Pixie OSS syncing. Modify certain fork changes to make it more maintenance friendly

Relevant Issues: N/A

Type of change: /kind feature

Test Plan: Will run the workflow after merging to verify the job works

…intenance friendly

Signed-off-by: Dom Del Nano <ddelnano@gmail.com>
@ddelnano
ddelnano marked this pull request as ready for review February 26, 2026 04:25
@ddelnano
ddelnano merged commit 15d6367 into main Feb 26, 2026
6 checks passed
@ddelnano
ddelnano deleted the ddelnano/add-oss-sync-workflow branch February 26, 2026 04:25
@ddelnano
ddelnano temporarily deployed to pr-actions-approval February 26, 2026 04:25 — with GitHub Actions Inactive
entlein pushed a commit that referenced this pull request Jun 2, 2026
…flow

Add Pixie OSS syncing. Modify certain fork changes to make it more maintenance friendly
ConstanzeTU pushed a commit that referenced this pull request Jun 4, 2026
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.
ConstanzeTU pushed a commit that referenced this pull request Jun 4, 2026
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.
ConstanzeTU pushed a commit that referenced this pull request Jun 4, 2026
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).
ConstanzeTU pushed a commit that referenced this pull request Jun 4, 2026
…#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).
ConstanzeTU pushed a commit that referenced this pull request Jun 4, 2026
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)
ConstanzeTU pushed a commit that referenced this pull request Jun 4, 2026
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.
ConstanzeTU pushed a commit that referenced this pull request Jun 4, 2026
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.
ConstanzeTU pushed a commit that referenced this pull request Jun 4, 2026
…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.
ConstanzeTU pushed a commit that referenced this pull request Jun 4, 2026
…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.
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.
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 pushed a commit that referenced this pull request Jun 21, 2026
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.
entlein added a commit that referenced this pull request Jun 21, 2026
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.
entlein added a commit that referenced this pull request Jun 21, 2026
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.
entlein added a commit that referenced this pull request Jun 21, 2026
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).
entlein added a commit that referenced this pull request Jun 21, 2026
…#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).
entlein added a commit that referenced this pull request Jun 21, 2026
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)
entlein added a commit that referenced this pull request Jun 21, 2026
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.
entlein added a commit that referenced this pull request Jun 21, 2026
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.
entlein added a commit that referenced this pull request Jun 21, 2026
…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.
entlein added a commit that referenced this pull request Jun 21, 2026
…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.
entlein added a commit that referenced this pull request Jun 21, 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.
entlein added a commit that referenced this pull request Jun 21, 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 added a commit that referenced this pull request Jun 22, 2026
* pem: direct-query gRPC endpoint — stub + TDD contract (entlein/dx#29)

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.

* utils/shared/k8s: clean pre-existing lint debt (gci + sets.String)

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.

* pem/direct-query: Step 0 — build the stub clean (#29)

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.

* pem/direct-query: Step 1 — HS256 JWT verify + matching test mint (#29)

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).

* pem/direct-query: Step 2a — wire LocalGRPCResultSinkServer ctor param (#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).

* pem/direct-query: Step 2b — port ExecuteScript exec path (#29)

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)

* pem/direct-query: Step 4 — pem_main.cc flag wiring, default OFF (#29)

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.

* ci: point vizier_release.yaml at the fork's runner label (#29 unblock)

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.

* pem/direct-query: dx-agent feedback — aud array + per-row column marshal (#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.

* pem/direct-query: Step 9 — manager constructs+starts the gRPC server (#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.

* pem: drain exec stats + skip payload-less responses (entlein/dx#29)

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.

* pem: direct-query startup is fail-soft + breadcrumbs (entlein/dx#29)

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.

* agent: refuse to start if PL_JWT_SIGNING_KEY is empty (entlein/dx#29)

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.

* ci: fix PR-checks (genfile + cfmt) on PR #49

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.

* pem: jwt key fallback + signing-key security doc + tampering tests (entlein/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.

* pem: compile-time disable + auth/discouraged-practices doc (entlein/dx#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.

* pem: serialize sink access per request (CodeRabbit r3364645000)

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

* pem: direct-query :50305 uses cluster TLS (entlein/dx#29 — blocker)

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

* pxapi: WithDirectTLSSkipVerify for node-IP direct dial (entlein/dx#29)

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.

* pem: enforce iss=PL + sub=service claims; refresh doc line refs

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)

* pem direct-query: verify service SCOPE, not sub=="service"

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.

---------

Co-authored-by: Entlein <eineintlein@gmail.com>
entlein pushed a commit that referenced this pull request Jul 30, 2026
…insert=0 writes

Two fresh-rig blockers for the full evidence set:
1. pem-direct (:50305) serves fast node-local queries but REFUSES mutations
   (direct-query: mutations out of scope #29), so the bpftrace deploy failed and
   the dark tables (dc_snoop/creds_change/stack_trace) stayed empty. Deploy the
   tracepoints via the in-cluster broker (:50300) with the same JWT when the query
   adapter is pem-direct; keep querying via pem-direct.
2. Fresh ClickHouse defaults async_insert=1 → the AE's INSERT buffered and returned
   written_rows=0 (evidence trickled in minutes later / looked lost). Write with
   SETTINGS async_insert=0 so evidence lands + is counted immediately.
entlein pushed a commit that referenced this pull request Aug 7, 2026
…insert=0 writes

Two fresh-rig blockers for the full evidence set:
1. pem-direct (:50305) serves fast node-local queries but REFUSES mutations
   (direct-query: mutations out of scope #29), so the bpftrace deploy failed and
   the dark tables (dc_snoop/creds_change/stack_trace) stayed empty. Deploy the
   tracepoints via the in-cluster broker (:50300) with the same JWT when the query
   adapter is pem-direct; keep querying via pem-direct.
2. Fresh ClickHouse defaults async_insert=1 → the AE's INSERT buffered and returned
   written_rows=0 (evidence trickled in minutes later / looked lost). Write with
   SETTINGS async_insert=0 so evidence lands + is counted immediately.

Signed-off-by: entlein <einentlein@gmail.com>
entlein added a commit that referenced this pull request Aug 7, 2026
…n (8 dx_* tables) (#89)

* feat(ae pixie-io#126): 8 dark-vector tracepoint tables (schema + 4-file allowlist)

Part A of aeprod28. Adds dx_execve, dx_vfs_events, dx_unlink, dx_dlookup,
dx_mprotect, dx_creds, dx_bpf, dx_ptrace — the pid-keyed tracepoint tables for the
dark vectors (V1/V2/V6/V7/V8). Generic 4-file allowlist edit (schema.sql, ddl.go
KnownTables+PixieTables, apply.go OperatorOwnedTables, pxl/tables.go builtinTables)
+ the count guard. DDL is one-col/line (line-oriented verify parser); all carry the
requiredPixieColumns (namespace/pod/hostname/time_) so VerifyPixieSchema passes.
clickhouse + pxl tests green.

TODO aeprod28: pid->pod pull enrichment, AE-owned no-TTL tracepoint deploy,
pgsql-timeout (#7), steering rework (pixie-io#93/#62).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017k7uYSNUctQvkTAZYJbaB3
Signed-off-by: entlein <einentlein@gmail.com>

* feat(ae): pgsql firehose timeout (#7) + pid->pod enrichment for dark tables (pixie-io#126)

Part B+enrichment for aeprod28.

#7 pgsql write: the firehose pull bounded query+write by cfg.Refresh (~30s) — far
too tight for pgsql_events (full SQL text + heavy socket_tracer parse), so
ExecuteScript hit context-deadline and pgsql landed 0 rows. Added a dedicated
QueryTimeout (env-overridable, default 150s, matching the OrderQuery 180s budget);
the pull now bounds the query by it, not Refresh.

pixie-io#126 pid->pod enrichment: the 8 dark-vector tracepoint tables emit raw kernel pid
(no upid) — the native px.upid_to_pod_name path fails on them. Added PodEnrichPxL:
native tables keep upid resolution; dark tables merge process_stats on pid ONLY
(the validated join-pod.pxl query — NOT pid+asid, since px.asid() is the kelvin
asid on a dynamic tracepoint, not the per-PEM asid). Wired into both the passthrough
(CompilePassthrough) and targeted (QueryFor) builders; the OrderQuery pod filter
uses bare-pod equality for dark tables (their df.pod is the bare name). Test:
TestPodEnrichPxL_DarkVsNative. Build + pxl/passthrough/clickhouse tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017k7uYSNUctQvkTAZYJbaB3
Signed-off-by: entlein <einentlein@gmail.com>

* adaptive_export: dc_snoop replaces execve in the dark-table export set

The custom bprm_execve kprobe does not fire on 6.x kernels (inlined/renamed);
the shipped Pixie dc_snoop (kprobe:lookup_fast) is the working process+file
instrument and captures live (MANIFESTO §10). Swap the dark-vector export:

- tables.go: dx_execve -> dx_dcsnoop in builtinTables.
- schema.sql / ddl.go / apply.go: dx_execve DDL -> dx_dcsnoop, + the dc_snoop
  `t` column (R=reference / M=miss from the dcache lookup).
- compile.go: dx_dcsnoop is a pid-keyed dark-vector table (PodEnrichPxL merges
  process_stats on pid for pod+namespace).

One dentry-cache probe serves both R0001 (process launch = binary lookup) and
R0002 (file access). 14 AE packages green.

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

* adaptive_export: dark-vector pod filter must use the namespaced pod key

proc.ctx['pod'] yields the NAMESPACED pod name (ns/pod) on Pixie v0.14.20+, not
the bare pod name the ported comment assumed. Verified live (rig 6a5f6bc0):
df.pod=='specimen/activity-gen-xxx' matches 2 rows, df.pod=='activity-gen-xxx'
matches 0 — so every dark-vector pull silently returned empty while the native
protocol pulls (namespaced key) worked. Match the namespaced key for dark
vectors too. Native protocol export already proven end-to-end (dns/conn land in
forensic_db for the steered pod).

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

* adaptive_export: register dc_snoop + stack_trace + creds_change retention scripts at boot

The operator already reconciles ClickHouse-plugin retention scripts on boot
(installPresetScripts, gated INSTALL_PRESET_SCRIPTS=true: GetClusterScripts →
purge operator-managed → AddDataRetentionScript for each builtin). Add three
dark-vector/profiler export scripts to the builtin set so they are registered
IF-NOT-PRESENT, permanently, via the native OTel→ClickHouse plugin — no external
wrapper:

- ch-dc_snoop     — UpsertTracepoint(lookup_fast, "876000h" ≈ permanent) + px.export
                    (dentry cache = process+file, V1/V2). bare bprm_execve/d_lookup
                    don't fire on 6.x (MANIFESTO §10); lookup_fast does.
- ch-stack_trace  — native continuous profiler stack_traces.beta (V9, no tracepoint)
                    → px.export. The OTel "profiles" stack-trace signal.
- ch-creds_change — UpsertTracepoint(commit_creds, "876000h") + px.export: a process
                    committing new uid==0 while its previous real uid>0 = privilege
                    escalation to root (V7).

Scripts embedded (go:embed internal/script/presets/*.pxl); each uses
px.plugin.start_time/end_time (plugin-managed window) + px.otel.ClickHouseRows.
Tracepoints use a ~100y TTL (no built-in "permanent" flag; a huge TTL is
effectively permanent + survives a cron stall). CH DDL for the 3 tables added to
schema.sql so the operator self-creates them. 14 AE packages green.

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

* adaptive_export: TrimSpace API key / host / DSN / cluster-id (fixes RST_STREAM PROTOCOL_ERROR)

A pixie-api-key sourced via a k8s secret created with `kubectl --from-file`
keeps the file's trailing newline. Sent as the pixie-api-key gRPC metadata
header to the cloud PluginService, that newline is an HTTP/2 header protocol
violation → the server replies with RST_STREAM PROTOCOL_ERROR. It surfaces as
"could not ensure ClickHouse plugin is enabled" / "get cluster scripts: ...
PROTOCOL_ERROR" and blocks retention-script registration entirely, even though
the key is valid (verified: same key via $(...) — which strips the newline —
lists the scripts fine).

TrimSpace the API key (and defensively the endpoint host, ClickHouse DSN, and
cluster id) so a whitespace-padded secret can't break the cloud calls. Proven
live: with the newline stripped the operator registers all 14 preset scripts
(incl. ch-dc_snoop / ch-stack_trace / ch-creds_change) on the cluster.

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

* fix(ae): ASCII-only comments in stack_trace.pxl preset

The arc `mypy` linter (mypy 1.20.2, `show_column_numbers = True`) crashes
with an INTERNAL ERROR on stack_trace.pxl in CI. The file was the only
preset carrying a multibyte character the linter had not seen before — a
`->` (U+2192 arrow) in a comment; no other .pxl in the tree uses one. With
column reporting on, mypy's byte-vs-character column bookkeeping over a
multibyte comment char is a known crash vector. The sibling presets that
lint clean are ASCII (dc_snoop) or carry only an em-dash that does not
trigger it (creds_change), so this narrows stack_trace.pxl's comments to
ASCII to match the known-good profile. No script behaviour changes.

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

* fix(lint): disable mypy on-disk cache to stop concurrent-run crashes

arc lint runs `mypy --config-file=mypy.ini <file>` once per file, in
parallel. The three adaptive_export preset .pxl files (added together)
are linted concurrently and share the incremental cache in the repo root;
concurrent writers corrupt it, producing a nondeterministic mypy
`INTERNAL ERROR` on whichever file loses the race (stack_trace.pxl on one
run, dc_snoop.pxl on the next -- neither a content issue). Setting
`cache_dir = /dev/null` makes each per-file invocation self-contained, so
there is no shared cache to race on. Type-checking semantics are
unchanged; per-file runs get no incremental benefit anyway.

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

* fix(lint): add license headers to AE presets + fix dc_snoop E127

Once the mypy cache crash was out of the way, arc lint surfaced two real
violations on the three preset .pxl files:
- missing Apache license header (all three) -- added the canonical
  header used by the other pxl scripts;
- flake8 E127 (continuation line over-indented) on dc_snoop's
  pxtrace.UpsertTracepoint call -- collapsed to a single line, matching
  creds_change's call style.

flake8 (.pxl.flake8rc) and mypy both clean locally on all three.

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

* adaptive_export: native ClickHouse DSN for retention plugin (fixes vizier crash)

The retention plugin's export sink is the query engine's native
ClickHouseExportSink (clickhouse-cpp over TCP :9000), not the AE's own
HTTP write path (:8123). It requires the DSN in native format
clickhouse://user:pass@host:9000/db. Passing the AE's HTTP DSN
(http://host:8123/db) made the sink parse "http" as the username and
crash on connect, taking the whole vizier Unhealthy.

- config: add NativeDSN() builder (native TCP port, no http scheme),
  distinct from DSN() which remains the AE's own HTTP write endpoint.
- main: pass NativeDSN() to EnsureClickHousePluginEnabled, and retry the
  ensure up to 5x (the vizier plugin service can 404 for the first few
  seconds after boot, permanently skipping enablement otherwise).

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

* adaptive_export: dark-table schema must match the native export sink

The retention plugin's ClickHouseExportSink (clickhouse_export_sink_node.cc)
serializes Pixie columns with a fixed type mapping and auto-appends an
event_time column as DateTime64(3) (milliseconds). The dark-vector tables
were declared with types that don't match what the sink sends, so every
INSERT threw TYPE_MISMATCH server-side and the clickhouse-cpp client
segfaulted on ReceiveException — crashlooping kelvin.

Corrected to the sink's actual output types:
- time_       UInt64        -> DateTime64(9)   (TIME64NS)
- upid        UInt128       -> String          (UINT128 serialized as String)
- pid         Int32         -> Int64           (all Pixie ints are INT64)
- old/new_uid UInt32        -> Int64
- event_time  DateTime64(9) -> DateTime64(3)   (sink auto-appends millis)

Validated live: stack_trace exports 5609+ rows of real profiler data,
0 insert errors, kelvin stable, vizier Healthy.

NOTE (follow-up): the protocol builtinPresetScripts tables
(http_events/dns_events/conn_stats/pgsql_events + redis/mysql/cql/mongodb/
amqp/mux/tls_events) export through the same sink and have the identical
event_time DateTime64(3) requirement, currently unmet — latent until those
tables receive rows.

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

* adaptive_export: presets emit event_time (nanos) so sink keeps DateTime64(9)

The ClickHouse export sink auto-appends an event_time column as DateTime64(3)
(milliseconds) ONLY when the exported df has a time_ column but no event_time
column (clickhouse_export_sink_node.cc:186 `has_time_ && !has_event_time`).
That millisecond column mismatches the tables' DateTime64(9) event_time and
crashes the native client on INSERT.

Rather than degrade every table to DateTime64(3) millis (which would break the
nanosecond-consistent event_time contract shared with the AE HTTP write path
and dx/soc joins — see the schema.sql header), each preset now sets
`df.event_time = df.time_`. That makes the sink treat event_time as a normal
TIME64NS column and emit DateTime64(9) nanoseconds, matching the schema.

- dc_snoop.pxl / creds_change.pxl / stack_trace.pxl: add df.event_time = df.time_
- builtinPresetScripts (all protocol presets): same, before px.display
- schema.sql: dark-table event_time reverted DateTime64(3) -> DateTime64(9)
  (the other sink-type fixes stay: time_ DateTime64(9), upid String, pid/uid Int64)

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

* adaptive_export: AE deploys dark-vector bpftraces at boot (owns tables + traces)

Retention/cron export scripts cannot deploy a tracepoint — the cron executor
drops the pxtrace mutation, so dc_snoop/creds_change tables were never created
(Table not found forever), while stack_trace worked only because it's the native
profiler (no tracepoint). The AE now owns tracepoint deployment.

- script.DesiredTracepoints(): source of truth for the bpftraces the AE deploys
  (dc_snoop, creds_change; extend for V6/V8). Each has a <name>_deploy.pxl
  (import pxtrace + UpsertTracepoint, permanent TTL, idempotent upsert).
- main.deployDesiredTracepoints: at boot (INSTALL_PRESET_SCRIPTS=true) run each
  deploy script as a mutation ExecuteScript over the pixie adapter, with retry.
  pxapi auto-sets Mutation:true for `import pxtrace`.
- Split the export presets: dc_snoop.pxl / creds_change.pxl are now query+export
  ONLY (no UpsertTracepoint) — they read the already-deployed table.
- CONTRACTS.md: C12 expanded to "AE owns schemata + table deployments + trace
  deployments"; new C16 (native-DSN export + nanos event_time) and C17 (AE
  deploys bpftraces, cron never does); boot diagram updated.

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

* adaptive_export: confirm tracepoint deploy by table, not the mutation stream

pxapi's result collector cannot decode the mutation-info response the vizier
returns for a pxtrace deploy ("stream: unimplemented type"), so the deploy Query
always errored even though the UpsertTracepoint applied server-side (verified:
dc_snoop + creds_change reach RUNNING_STATE, dc_snoop exports 220k rows). The
old loop treated that as failure → 5 redundant re-deploys + a false "could not
deploy" warning per tracepoint, and delayed the next tracepoint.

Now the deploy fires once, then confirms success by polling the tracepoint's
OUTPUT TABLE (a plain DataFrame query: "Table not found" = not deployed; compiles
= RUNNING, 0 rows ok). Re-fires the deploy every few attempts as a fallback.
Real success/failure signal, no false negatives, no wasted re-deploys.

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

* adaptive_export/e2e: creds_change trace calibration (fire + verify in CH)

End-to-end calibration for the creds_change dark-vector tracepoint (V7). Every
run proves the two properties the trace exists for:
  a) the trace WORKS — the AE-deployed commit_creds bpftrace captures a real
     privilege escalation (REAL uid >0 -> 0), and
  b) attribution reaches ClickHouse — the event flows Pixie -> AE retention
     export -> forensic_db.creds_change carrying pid + comm.

Fires deterministically with a stock python:3-slim Job, no custom image:
setresuid(12345,0,0) drops the real uid to a sentinel while KEEPING euid=0
(privileged), then setuid(0) pulls the real uid back to 0 — exactly the
commit_creds(new_uid==0 && old_uid>0) the tracepoint filters for. The sentinel
old_uid=12345 makes the row unambiguous. Asserts the row lands with pid+comm;
pod (pid->pod enrichment) is assert-or-log so it greens automatically later.

Live+e2e gated (AELOAD_LIVE=1 AELOAD_E2E=1). VALIDATED live on rig 6a5fbc75
(aeprod38): count=1 pid=905034 comm=python3 old_uid=12345 new_uid=0, 25s e2e.

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

* fix(lint): gofmt config.go (align one-line clickhouse accessors)

The NativeDSN() addition broke gofmt's alignment of the consecutive
one-line accessor funcs; golangci's format check (gci/gofmt) flagged
config.go. Pure gofmt -w, no behavior change.

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

* adaptive_export: pid->pod/namespace enrichment for dc_snoop + creds_change

Dark-vector tracepoints emit a raw kernel pid with no upid, so the pod/namespace
columns landed empty (unattributed cluster-wide firehose). The dc_snoop and
creds_change export presets now resolve namespace+pod via the validated
process_stats merge on pid (px.upid_to_pid, PodEnrichPxL join; pid-only, not
pid+asid — on a dynamic tracepoint px.asid() is the aggregator asid). Best-effort
left join: blank pod for host/transient pids (correct). The presets then select
exactly the forensic_db columns so the merge's pid_x etc. don't reach the sink.

This makes the dark tables filterable/steerable by pod, not just pid/comm.

creds_change calibration updated: the escalation process now sleeps ~20s so
process_stats samples its pid (a sub-second process is never attributed); the
test verifies pod/namespace resolve to the calibration namespace when attribution
lands (assert-or-log until proven stable live). CONTRACTS: +C18 (dark-vector
pod attribution).

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

* adaptive_export: full k8s metadata enrichment (namespace/pod/container/node)

Extends the dc_snoop/creds_change pid->pod enrichment to the full workload
identity: the process_stats merge now also resolves container (ctx['container'])
and node (px.upid_to_node_name), alongside namespace + pod. DDL adds a container
column to both dark tables; presets select the exact column set so the merge's
pid_x doesn't reach the sink. This gives forensic attribution the full k8s
metadata (which container, in which pod, in which namespace, on which node) for
every dark-vector event, not just pid/comm.

creds_change calibration logs the full metadata and still asserts namespace
resolves to the firing workload. CONTRACTS C18 updated.

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

* adaptive_export: AE auto-creates dc_snoop/creds_change/stack_trace (canonical dark tables)

The AE registered dx_dcsnoop/dx_creds (old buggy schemas: time_ UInt64, pid Int32,
event_time DateTime64(3) millis, single uid) in builtinTables/OperatorOwnedTables/
KnownTables/darkVectorTables, but the export presets + tracepoints use
dc_snoop/creds_change/stack_trace with the debugged, calibration-proven schemas
(DateTime64(9) nanos, Int64, old_uid/new_uid, full k8s metadata). So the AE created
tables the presets never wrote to and never created the ones they did — export hit
"table not found" unless the tables were made by hand.

Reconcile to the canonical names across every list (option A, matches the dx#129
alignment already requested of dx-agent):
- builtinTables / OperatorOwnedTables / ddl.go KnownTables(x2): dx_dcsnoop -> dc_snoop,
  dx_creds -> creds_change, + stack_trace (V9 native profiler). dx_vfs_events/unlink/
  dlookup/mprotect/bpf/ptrace kept, reserved for the bpftraces still to be written.
- compile.go darkVectorTables: dc_snoop/creds_change (pid-merge enrichment); stack_trace
  resolves via upid, not listed.
- schema.sql: dc_snoop/creds_change/stack_trace reformatted one-column-per-line (the
  schema-verify parser is line-oriented — multi-column lines silently dropped columns,
  which would fail VerifyPixieSchema at boot); + hostname on stack_trace; removed the
  superseded dx_dcsnoop/dx_creds blocks.
- tests: builtinTables count 21 -> 22; dark-vector test names -> dc_snoop/creds_change.

Validated live (rig 6a61314b, aeprod41 + manual tables): dc_snoop 140k rows with full
namespace/pod/container/node metadata; creds_change calibration PASS with attribution
namespace=creds-calib pod=creds-calib/... container=escalate node=cplane-01.

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

* adaptive_export: enforce nanosecond-timestamp + single-auth invariants via tests

Audit + guardrails so the two cross-cutting invariants can never silently
regress.

Timestamps (one unit = nanoseconds):
- TestPixieTablesUseNanosecondTimestamps / TestNoMillisecondTimestampReintroduced
  assert EVERY pixie observation table (PixieTables) stores time_ + event_time as
  DateTime64(9), never DateTime64(3) millis. kubescape_logs (unix-ns UInt64 input)
  and alerts (kubescape millis) are non-pixie tables, excluded by construction.
- The audit surfaced 6 not-yet-active dark tables (dx_vfs_events, dx_unlink,
  dx_dlookup, dx_mprotect, dx_bpf, dx_ptrace) still on the OLD millis schema
  (time_ UInt64, pid Int32, event_time DateTime64(3)); fixed to the canonical
  nanosecond shape (DateTime64(9), Int64) matching dc_snoop/creds_change so they
  cannot crash the native export sink when their bpftraces are written.

Authentication (one method per context, no reinvention):
- TestCloudClientAuthIsPixieAPIKeyHeader pins the cloud plugin client to the
  canonical "pixie-api-key" gRPC header (never bearer/JWT); RejectsEmptyKey
  forbids a silent unauthenticated fallback.
- TestNoAuthReinvention walks the whole AE tree: every JWT goes through the shared
  jwtutils lib (GenerateJWTForService / SignJWTClaims / ParseToken) — no
  golang-jwt/dgrijalva/jwt.New/SignedString/jwt.Parse — and "pixie-api-key" lives
  only in internal/pixie. Exactly two surfaces: cloud=api-key, in-cluster=service JWT.

Full AE suite: 15 packages green.

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

* adaptive_export: dark tables carry consistent full k8s metadata (+ tests)

The 6 not-yet-active dx_ tables were inconsistent with the canonical
dc_snoop/creds_change beyond the timestamp scale: all six lacked the container
column, and dx_vfs_events/dx_unlink lacked comm (yet dx#129's projector reads
comm from them). Reconciled every dark table to the same attribution shape:
comm + namespace + pod + container + hostname (stack_trace uses upid for identity).

New guardrails (internal/clickhouse):
- TestDarkVectorTablesHaveFullMetadata: every dark table has namespace/pod/
  container/hostname — uniform workload attribution, nothing dropped at dx join.
- TestDarkVectorTablesCarryProcessIdentity: every dark table has comm or upid.
- TestDarkVectorSetMatchesPixieTables: the dark set stays inside PixieTables() so
  the nanosecond + metadata guards actually cover it.

Full AE suite: 15 packages green.

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

* adaptive_export: ASCII-only .pxl comments (lint — matches build-agent's stack_trace fix)

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

* adaptive_export: elect one pod for cluster-scoped setup (fix duplicate exports)

RCA of ~35% duplicate rows in the dark tables (dc_snoop 69.6M rows / 45.1M
distinct; the creds_change calibration event stored twice): the AE is a DaemonSet
(one pod per node), but installPresetScripts + deployDesiredTracepoints are
CLUSTER-scoped. Every pod registered them, so each preset got one duplicate cron
script per node (observed: 28 cron scripts = 2x the 14 presets on a 2-node rig),
and every dark table was exported once per node.

Fix: elect a single deterministic leader — the AE pod on the lexicographically
smallest node name (leaderNode). Every pod computes the same winner from the same
DaemonSet pod list, so no lease/coordination is needed; the pod-list RBAC is
already held (findVizierNamespace). Gate installPresetScripts + deployDesiredTracepoints
on it. The node-local trigger/data-plane still runs on every pod. Fail-open on any
k8s error (a transient duplicate beats skipping setup). Tracepoint deploy was
already idempotent (UpsertTracepoint), so only the cron registration duplicated.

leader_test.go pins the election: smallest node wins, deterministic, exactly one
leader across all pods' identical views.

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

* fix(build): add k8s client deps to cmd/BUILD.bazel

The leader-election commit added k8s.io/apimachinery, client-go/kubernetes,
and client-go/rest imports to cmd/main.go but did not update the go_library
deps, so GoCompilePkg failed on the AE image build. Adds the three deps
(same labels the internal/config package already uses) -- what gazelle
would generate. No source change.

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

* adaptive_export: bazel BUILD deps for the leader guard + invariant test targets

aeprod44 release build failed: bazel "missing strict dependencies" for the k8s
client-go imports the leader election added. Add them to cmd_lib (mirroring the
config package): @io_k8s_apimachinery//pkg/apis/meta/v1:meta, @io_k8s_client_go//
kubernetes, @io_k8s_client_go//rest.

Also register the new invariant tests in their pl_go_test targets so bazel test
runs (+ enforces) them: cmd_test (leader_test.go), pixie_test (auth_invariants_test.go),
and metadata_/timestamp_invariants_test.go in clickhouse_test. The source-walk
guard (TestNoAuthReinvention) now t.Skip's in a sandboxed build where the tree
isn't on disk, instead of failing — the behavioral auth tests run everywhere.

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

* adaptive_export: exclude our own monitoring/infra pods from dc_snoop + stack_trace

The dark tables were dominated by SELF-OBSERVATION noise — Pixie's own pem/kelvin/
vizier-* + the AE (all in `pl`) + the forensic ClickHouse generate the bulk of
dc_snoop dentry lookups and stack_trace profiler samples (69M+ dc_snoop rows on an
idle rig, mostly pem/kelvin). That is not workload evidence.

The dc_snoop + stack_trace export presets now drop rows whose resolved namespace is
our stack's: pl, px-operator, olm, clickhouse, kube-system. The filter runs at the
export, so the noise never lands in ClickHouse. Workload pods and host/kernel
(blank-namespace) rows are retained. creds_change is left inclusive on purpose — a
privilege escalation from our own components is a compromise signal, not noise, and
it is low-volume.

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

* adaptive_export: protocol presets export via px.export, not px.display

The builtin protocol retention presets (dns_events, conn_stats, http_events,
redis_events, …) used px.display(df, '<table>'), which relies on the retention
plugin routing display output to forensic_db.<table>. Verified on a clean rig
that this never writes: dns_events/conn_stats/http_events = 0 parts ever, while
the DarkVectorPresets (dc_snoop/stack_trace) — which use px.export via the OTel
ClickHouse sink — populate (dc_snoop 220k). Switch the protocol presets to the
same px.export(px.otel.ClickHouseRows(table=…)) path so they write directly
through the sink, self-contained, no plugin-routing dependency.

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

* dc_snoop.pxl: exclude host runtime + node-agent(honey) from export

dc_snoop was ~99% self-observation + host on a live rig (k3s-server 295k,
k3s-agent 218k, containerd-shim 193k, node-agent 55k rows). The export dropped
only 5 namespaces and explicitly RETAINED host/blank-namespace rows -- which is
the bulk of the noise. Now drop honey(node-agent)+kube-* namespaces AND the
host/runtime/monitoring comms (k3s-*, containerd*, runc, kelvin, vizier-pem,
systemd, iptables, kubelet). Kept in sync with dx benchlive.writeSelfExclusion.

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

* adaptive_export: decouple tracepoint deploy from retention firehose

DEPLOY_TRACEPOINTS (default on) deploys the bpftraces permanently,
independent of INSTALL_PRESET_SCRIPTS. When INSTALL_PRESET_SCRIPTS is off,
purge the operator-managed cron scripts so the cluster-wide export firehose
stops and dx drives per-anomaly deduped export instead.

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

* adaptive_export: trail fan-out watermark by QueryLag to stop losing sparse evidence

Root cause: the per-table fan-out advanced its watermark to now each pass, but
socket_tracer flushes rows a few seconds late. For a long-lived anomaly the
watermark stays at ~now, so every sparse event (dns_events, dc_snoop) loses the
flush race and is skipped forever, while continuous tables (conn_stats) always
have fresh post-watermark rows and export fully. Fix: query up to now-QueryLag
(default 30s, ADAPTIVE_QUERY_LAG_SEC) so late-flushed rows stay queryable.

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

* adaptive_export: /export/start drives full steer-all export (dx steers, AE grabs all)

In pull mode the control /export/start only Upsert'd the streaming activeSet — a
no-op for the fan-out. Add controller.OrderExportAll (one-shot OrderQuery for
every configured pixie table, concurrent, deterministic query_id for dedup) and
have handleStart trigger it when a querier is wired. dx already calls StartExport
default-on per referral with no triage gate, so this makes dx steer AE to capture
the COMPLETE evidence set for each anomaly's pod — filtered only to namespace/pod.

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

* adaptive_export: floor OrderExportAll per target (stop dx StartExport flood)

dx fires StartExport on every referral (~1s floor), so a sustained attack made
OrderExportAll re-run the full 20-table capture many times/sec for the same pod
over overlapping windows — the broker's globalSem saturated and nothing completed
(dx-steered exports wrote 0). Add a per-target ExportAllFloor (ADAPTIVE_EXPORT_ALL_FLOOR_SEC,
default 30s): one full capture per target per floor; the rolling window still
covers every event.

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

* adaptive_export: wire EXPORT_MODE=never to actually disable trigger self-steer

EXPORT_MODE was validated but never consumed — the kubescape trigger self-steered
regardless, so EXPORT_MODE=never was a no-op and the AE could not be put in a
dx-only export mode. Add Config.DisableSelfSteer (set by EXPORT_MODE=never in
main.go, inverted bool so the zero value preserves legacy self-steering), and gate
the trigger's pushPixieRows spawn (handle + Rehydrate) on it. The control surface
(dx /export/start OrderExportAll, /query OrderQuery) is unaffected — so with
EXPORT_MODE=never the AE exports ONLY what dx steers.

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

* chore: remove stray 91MB cmd binary accidentally committed at repo root

A compiled ARM aarch64 ELF executable named `cmd` (91MB) was committed at
the repo root -- accidental `go build`/bazel output, not gitignored. It
bloats the tree and would trip filename/artifact lint. Removed; no source
or BUILD change (the real binary target is //src/vizier/services/adaptive_export/cmd).

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

* fix(lint): gofumpt presets.go (blank line between var blocks)

golangci-lint's gofumpt formatter flagged presets.go:16 -- gofumpt wants a
blank line between the two consecutive top-level var blocks
(defaultExcludeNamespaces / defaultExcludeComms). Applied gofumpt -w to that
file only. No behavior change.

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

* fix(lint): gci/gofumpt main.go + rename min var to avoid shadowing builtin

golangci flagged main.go: (1) gci/gofumpt formatting — the new env-var
consts and controller.Config fields broke alignment; (2) predeclared —
leaderNode's local var `min` shadows the Go 1.21 builtin. Reformatted with
gci+gofumpt (repo sections standard/default/prefix(px.dev)) and renamed the
var to `smallest`. Builds clean; no behavior change.

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

* fix(lint): add pl_go_test target + E265-clean dc_snoop sentinel

Two CI lint failures from the config-driven-presets work:
- internal/script/BUILD.bazel: presets_test.go was added without the
  gazelle-generated pl_go_test target -> 'Gazelle was not run'. Added the
  script_test target (matches gazelle diff + cmd/BUILD.bazel style).
- dc_snoop.pxl: the runtime sentinel '#__DC_SNOOP_EXCLUSION__' tripped
  flake8 E265 (block comment needs '# '). Renamed to '# __DC_SNOOP_EXCLUSION__'
  and updated the matching strings.Replace target in presets.go so the
  env-injected exclusion still substitutes byte-for-byte. No behavior change.

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

* adaptive_export: node-scope dark-vector tables so the malignant evidence lands

The dark tables (dc_snoop/creds_change/dx_*) came back EMPTY: the AE filtered them
by pod, but an incident's transient malignant pids (whoami/cat/getent children)
are too short-lived to enter process_stats, so their ns/pod resolves blank and the
filter dropped exactly the evidence. The AE is node-local (pem-direct → the node's
own PEM), so QueryFor now keeps every dark row in the window and only drops the
infra/self comms (DC_SNOOP_EXCLUDE_COMMS, env-tunable) — the workload's dark
activity is captured node-scoped, no relevance filtering.

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

* adaptive_export: default to pem-direct (node-local PEM :50305)

pem-direct is the robust query path for the node-scoped AE: node-local (matches
its scope), desync-immune (bypasses the kelvin/broker aggregation that the
recurring PEM desync silently breaks) and fast. Default to HOST_IP:50305 when the
deploy provides HOST_IP (downward API) + PL_JWT_SIGNING_KEY; explicit
ADAPTIVE_VIZIER_DIRECT_ADDR still wins; cloud passthrough only as fallback.

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

* chore: remove re-added 91MB cmd binary + gitignore /cmd

The stray adaptive_export ELF binary got committed at the repo root again
(a repo-root `go build` output). Removed it and added `/cmd` to .gitignore
so it stops recurring. No source/BUILD change; the real binary target is
//src/vizier/services/adaptive_export/cmd.

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

* chore: gitignore /cmd (durable fix for recurring root binary)

Follow-up to fd9108a, which removed the binary but did not land the
.gitignore rule. Adds `/cmd` so a repo-root `go build` output stops getting
re-committed. No source/BUILD change.

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

* adaptive_export: tracepoints via broker-direct on pem-direct + async_insert=0 writes

Two fresh-rig blockers for the full evidence set:
1. pem-direct (:50305) serves fast node-local queries but REFUSES mutations
   (direct-query: mutations out of scope #29), so the bpftrace deploy failed and
   the dark tables (dc_snoop/creds_change/stack_trace) stayed empty. Deploy the
   tracepoints via the in-cluster broker (:50300) with the same JWT when the query
   adapter is pem-direct; keep querying via pem-direct.
2. Fresh ClickHouse defaults async_insert=1 → the AE's INSERT buffered and returned
   written_rows=0 (evidence trickled in minutes later / looked lost). Write with
   SETTINGS async_insert=0 so evidence lands + is counted immediately.

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

* adaptive_export: stabilize dark-vector query (comm-filter before process_stats merge)

The dc_snoop node-scope query timed out / silently dropped: it merged the node's
ENTIRE dark stream (Formatter/vector/runc/... thousands of rows/window) against
process_stats before filtering. Reorder: drop the infra/self comms FIRST, then
merge — the pid-join now runs on the handful of workload rows so the dark capture
completes reliably. Also expand the default comm-exclusion (runc:[2:INIT],
CgrpMemUsgObsr, Formatter, iptables-save, vector-worker, metrics-server, ...) in
CODE, since the env value with '[' ':' breaks the PxL filter.

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

* adaptive_export/control: read steer timestamps as nanoseconds (pipeline unit)

The control API (dx -> AE: /export/start t_end, /query window) carries unix
timestamps, and the evidence pipeline's ONE unit is nanoseconds (event_time,
dx referral windows). The handlers read them with time.Unix(v, 0) — i.e. as
SECONDS — so dx's nanosecond t_end (~1.78e18) became a year-56-billion instant.

Effect: every dx-steered OrderExportAll captured [t_end-600s, t_end] over that
garbage instant, a window that overlaps NO data, so the full-evidence set
(all dark tables: dc_snoop/conn_stats/creds_change/stack_trace, and every
protocol table) silently returned zero rows on the dx-steered path. Only the
self-steer path (its own now-based window) ever wrote anything, which is why
redis_events populated but the dx-ordered dark capture never did. The same
garbage window also flowed into ae_reconcile.win_start/win_end, overflowing
the DateTime formatter and making CH reject the reconcile insert (HTTP 400) —
which hid the whole failure from the reconcile instrument.

Fix: read the control timestamps as nanoseconds, time.Unix(0, ns), matching
the pipeline. No unit autodetection — one unit, everywhere. Test + struct
doc-comments updated to nanoseconds.

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

* adaptive_export/pxl: bound dark-table process_stats scan to 2m (was 5m)

The dark-vector pod-resolution merge scanned process_stats over a 5-minute
window. On a busy node that table samples every live pid every ~10-30s, so the
scan is large and is the dominant cost of the dark query — heavy enough that,
sharing the fan-out's query-slot budget with the fast native-table queries, the
dark capture either starved (too few slots) or, once the steer windows were
real, saturated the node-local PEM (too many). A 2-minute window still resolves
the pods that matter — long-lived workload pids (redis-server) are sampled
continuously — while transient attack pids never enter process_stats and
resolve blank either way. Cuts the merge scan ~2.5x so the dark tables complete
within the fan-out budget alongside the native queries.

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

* adaptive_export: dedup evidence tables via ReplacingMergeTree on natural event keys

The evidence tables were plain MergeTree, so the dx-steered OrderExportAll — which
re-pulls a rolling 600s window every ExportAllFloor — re-inserted the SAME kernel/
protocol events as fresh rows on every overlapping capture. dc_snoop showed 4,323
rows for one redis incident where the true unique count is a fraction of that.

Switch the seven evidence tables to ReplacingMergeTree keyed on each event's
NATURAL identity so re-pulls of the same event collapse:
  dc_snoop      (time_, pid, comm, t, file, pod)
  creds_change  (time_, pid, comm, old_uid, new_uid, pod)
  stack_trace   (time_, upid, stack_trace_id, pod)
  redis_events  (hostname, event_time, time_, upid, trace_role, remote_port, local_port, latency, req_cmd)
  dns_events    (... , req_body)
  http_events   (... , req_method, req_path)
  conn_stats    (hostname, event_time, time_, upid, remote_addr, remote_port, trace_role)

Keys are deliberately conservative — nanosecond time_ + pid/upid + connection tuple
+ a payload discriminator — so two DISTINCT events never share a key (false-collapse
would drop real evidence, worse than a dup). Dedup is applied on background merge;
exact counts read with FINAL / count(DISTINCT key). Bookkeeping tables already used
ReplacingMergeTree; this brings the evidence tables in line.

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

* adaptive_export/pxl: source stack_trace from canonical stack_traces.beta

The per-anomaly fan-out (QueryFor / OrderExportAll) queried px.DataFrame(table=
'stack_trace') — the ClickHouse table name, which is NOT a Pixie table — so the
stack_trace evidence table never populated on the steered path. The native
continuous profiler is 'stack_traces.beta' (upid-keyed, always-on, no tracepoint);
only the retention preset used it, and that path is disabled.

Fix: pixieSourceFor() maps stack_trace -> 'stack_traces.beta' for the DataFrame
source (dotted-name DataFrames compile fine in a direct query — verified live),
and a dedicated QueryFor branch resolves namespace/pod/container/hostname via
ctx + upid_to_node_name and stamps event_time=time_, exactly like the export
preset, then scopes to the alert's pod with the namespaced '<ns>/<pod>' key
(ctx['pod'] is namespaced — verified live: pl/vizier-metadata-0 -> 112 samples).
stack_trace is upid-native, NOT a dark-vector pid-merge table, so it takes the
native resolution path, not the process_stats merge.

Verified on a live cluster: the profiler emits across all pods, and the exact
generated query returns real samples for a filtered pod.

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

---------

Signed-off-by: entlein <einentlein@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Entlein <eineintlein@gmail.com>
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.

1 participant