Skip to content

Wire the Temporal engine's minikube profile so bridged agents actually run, and prove it - #205

Merged
imaustink merged 3 commits into
mainfrom
test/temporal-minikube-e2e
Aug 15, 2026
Merged

Wire the Temporal engine's minikube profile so bridged agents actually run, and prove it#205
imaustink merged 3 commits into
mainfrom
test/temporal-minikube-e2e

Conversation

@imaustink

@imaustink imaustink commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to #204 (which fixed the missing durable-agents.dev/bridged chart annotation). That fix alone was necessary but not sufficient to actually exercise a bridged agent end-to-end in this suite's minikube profile: no Temporal server exists there at all, and several of the Temporal engine's own config knobs had no default suited to a hermetic profile, each independently causing every turn to silently degrade to a bare conversational answer or an unresolvable identity-link park — never reaching a real bridged episode.

Found and fixed all of them, empirically, by driving real turns through the deployed cluster and reading Temporal's own workflow history rather than inferring from logs:

  1. No Temporal server in minikube. e2e/manifests/temporal-dev-server.yaml adds a single-node dev-mode server (temporalio/admin-tools's temporal server start-dev), applied by e2e/scripts/up.sh before the release deploys.
  2. temporal-engine.nats.url unset → bridged pod agents disabled entirely at the worker.
  3. temporal-engine.qdrant.host unset → all retrieval activities disabled, so no Agent could ever be found.
  4. temporal-engine.gateway.identity.defaultSubject/defaultRoles unset → every internal call from agent-orchestrator arrives at the Go gateway with no bearer token at all (its own AGENT_TEMPORAL_ENGINE_TOKEN secret key doesn't exist), resolving to an empty Caller.Subject — and the workflow treats an empty subject as "skip the catalog, answer bare," unconditionally.
  5. temporal-engine.identityLink.gatewayUrl unset → the worker uses an in-memory dev fake identity-link store, completely disconnected from this suite's real credential seeding (e2e/support/credential-store.ts, which writes real Kubernetes Secrets the real integration-gateway API reads).
  6. A Go bug (engines/temporal/internal/identitylink/identitylink.go): the claude-remote provider's /claude-auth/api/token?mode=login response shape uses a credentialsJson field, but the client only ever read token — so a real, retrievable credential was silently treated as absent, permanently parking any agent requiring the claude-remote provider.
  7. A core-controller bug (controllers/core-controller/internal/controller/agentrun_controller.go): the Temporal engine names AgentRun CRs agentrun-<agentId>-<uuid> itself. The reconciler's Job name template re-prefixed agentrun- onto that, producing a Job name — and therefore the API server's auto-added job-name pod-template label, which reuses it verbatim — over Kubernetes' 63-byte label limit. This broke Job creation for any AgentRun the Temporal engine launches, not just bridged ones.
  8. GATEWAY_SENDER_ASSERTION_SECRET unset on the Temporal gatewaytemporal-engine.gateway.senderAssertion.secretName was never wired, so the Go gateway could never verify the signed sender-login assertion agent-orchestrator's TemporalEngine client sends for every webhook-driven turn (it's a signed-header-only contract, no unsigned fallback field for this hop). The sender login was silently dropped, so AGENT_ACTOR_LOGIN never reached a launched AgentRun's secretEnv and any gh/tool-name the declarative planner guessed at got rejected — the originally-reported production failure.
  9. Caller-supplied tools (ADR 0035) were silently dropped over /invoke. agent-orchestrator's TemporalEngine.ts never forwarded a chat turn's resolved callerTools, callerToolChoiceRequired, or prior client-executed tool-call results to the engine's /invoke endpoint, and the endpoint itself had no fields to receive them. Added CallerTools/CallerToolRequired/PriorCallerToolCalls to invokeRequest, passed through as already-resolved data (agent-orchestrator ranks/validates tools itself before this hop — the engine doesn't re-parse or re-rank).
  10. No per-user identity ever reached the Temporal engine for chat turns. Every internal hop from agent-orchestrator authenticates with one shared service identity (or none), so every Open WebUI user collapsed onto the same subject — credential convergence (ADR 0031) could never work under this engine. Added a new signed "caller identity" assertion (x-gateway-caller-identity, mirrors the existing sender-assertion mechanism and reuses the same shared secret) carrying the caller's real subject/roles plus a PerUser flag — true only for a genuinely per-request identity (Open WebUI's forwarded-user JWT), never a shared token, since that flag gates the GitHub-link-based principal upgrade credential sharing depends on (authz.go's planProviders/adopt). Scoped deliberately to the forwarded-user-JWT path only — an earlier version also resolved identity from the plain shared authToken fallback, which regressed webhook-relayed turns by overriding their correct default subject with integration-gateway's own service identity.
  11. identitylink.Client.Rekey parsed a field that never existed on the wire. The real /claude-auth/api/rekey response is {status: "moved"|"not-found"|"occupied"}, but the client read a boolean moved field — always decoding false, so credential adoption (moving a pre-principal credential onto the caller's resolved principal) silently always "failed" and fell through to re-prompting the human to re-authorize.
  12. Message wording/label drift from the original TypeScript implementation. authz.composeLinkRequired's multi-link message dropped the "link N accounts" count entirely, and the claude-remote provider's label was "Claude (Remote Control)" instead of upstream's "Claude Remote Control".
  13. A stale e2e test fixture. bridged-agent-workflow.e2e.ts seeded credentials at a placeholder subject (client-integration-gateway) that only "worked" back when sender-assertion verification (bug Self-improvement fallback: relevance-gated tool fit + best-effort answer, no hardcoded fallback agent #8) was still broken; now that principal resolution is correct, credentials need to be seeded at the canonical principal, matching identity-keying.e2e.ts's own pattern.

The actual proof point

e2e/specs/bridged-agent-workflow.e2e.ts drives the same webhook path happy-path.e2e.ts does, then asks Temporal itself (via the temporal CLI against the dev server, not a log line or an inferred side effect) whether a BridgedAgentWorkflow execution actually completed for the route's target agent.

Test plan

  • bridged-agent-workflow.e2e.ts passes on its own against a fully-wired minikube cluster
  • happy-path.e2e.ts passes (previously failed: AGENT_ACTOR_LOGIN missing from the launched AgentRun's secretEnv)
  • caller-tools.e2e.ts passes 18/18 (previously 14/18: caller-supplied tools and their resumed results were silently dropped over /invoke)
  • identity-keying.e2e.ts passes all 15/15 (previously hung indefinitely — root cause was the chain of bugs docs: kubectl guide for operating the Open WebUI chat UI #10Add cluster-debug-skill: read-only kubectl + SigNoz query Tools #13 above)
  • chat-harness.e2e.ts, waitfor-guard.e2e.ts pass
  • resilience.e2e.ts has 2 pre-existing failures unrelated to anything in this PR: a raw gRPC-level stream cancellation (stream terminated by RST_STREAM with error code: CANCEL) surfaces instead of the workflow's own friendly idle-timeout message, and a follow-up-after-rollout turn times out waiting for a recovered reply. Not caused by any change here (verified before and after); flagged as a real, separate, pre-existing gap in the Temporal engine worth its own investigation, not fabricated as passing.
  • go build ./..., go vet ./..., gofmt -l ., and go test ./... clean in engines/temporal (new tests: caller_identity_assertion_test.go — including a TypeScript-generated cross-implementation vector, matching sender_assertion_test.go's own discipline — and rekey_test.go)
  • npx tsc --noEmit clean in apps/agent-orchestrator (pre-existing, unrelated errors in nats-agent-channel.ts/nats-job-receiver.ts excluded — a stale @controller-agent/messaging type gap, not touched by this PR)

🤖 Generated with Claude Code

https://claude.ai/code/session_01HctRMtdZixptEZeMebhADq

imaustink and others added 3 commits August 8, 2026 08:32
The chart annotation fix (PR #204) was necessary but not sufficient: no
Temporal server exists in the e2e minikube profile, and the engine's own
config had several unset wires that silently degrade every turn to a
conversational bare-answer or a link-required park, never reaching a real
bridged episode. Fixed all of them and added the e2e spec that proves it:

- e2e/manifests/temporal-dev-server.yaml: a single-node dev-mode Temporal
  server (temporalio/admin-tools' `temporal server start-dev`), applied by
  e2e/scripts/up.sh before the release deploys.
- charts/agent-controller/values-e2e.yaml: wires temporal-engine.nats.url,
  .qdrant.host (retrieval was otherwise disabled entirely),
  .gateway.identity.defaultSubject/defaultRoles (every internal call from
  agent-orchestrator arrives tokenless, so an empty Caller.Subject made
  every turn bare-answer regardless of capability/retrieval/forcedAgentId),
  and .identityLink.gatewayUrl (without it the worker uses an in-memory dev
  fake, completely disconnected from this suite's real credential seeding).
- engines/temporal/internal/identitylink/identitylink.go: the claude-remote
  provider's `/claude-auth/api/token?mode=login` response uses a
  `credentialsJson` field, not `token` -- the client only ever read `token`,
  so a real, retrievable credential was silently treated as absent.
- controllers/core-controller/internal/controller/agentrun_controller.go:
  the Temporal engine names AgentRun CRs "agentrun-<agentId>-<uuid>" itself;
  prefixing "agentrun-" onto that again produced a Job name (and the API
  server's auto-added job-name pod-template label, which reuses it
  verbatim) over the 63-byte label limit, so the reconciler could never
  create a Job for ANY AgentRun this engine launches.
- e2e/specs/bridged-agent-workflow.e2e.ts: drives the same webhook path
  happy-path.e2e.ts does, then asks Temporal itself (not a log line) for a
  completed BridgedAgentWorkflow execution -- verified passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HctRMtdZixptEZeMebhADq
It was reusing 18092, the same local port identity-keying.e2e.ts
already binds for its own gateway port-forward -- a collision waiting
to bite the full suite even though file-level serial execution mostly
hides it today.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HctRMtdZixptEZeMebhADq
…adoption and message wording

Chasing this PR's remaining test-plan item (a full e2e regression pass)
surfaced several more independent bugs, all now fixed:

- The Temporal engine gateway never forwarded/verified a per-request
  caller identity for chat turns -- every internal hop from
  agent-orchestrator resolved to the SAME shared default subject
  regardless of which human was chatting, so credential convergence
  (ADR 0031) could never work under this engine. Added a signed
  "caller identity" assertion (mirrors the existing sender-assertion
  mechanism, reuses the same secret) carrying the caller's real
  subject/roles plus a PerUser flag -- true only for a genuinely
  per-request identity (Open WebUI's forwarded-user JWT), never a
  shared token, since that flag gates the GitHub-link-based principal
  upgrade that credential sharing depends on. Scoped deliberately to
  the forwarded-user-JWT path only, after an initial version
  regressed webhook-driven turns by also resolving (and overriding
  the correct subject with) integration-gateway's own shared service
  token.
- identitylink.Client.Rekey parsed a boolean `moved` field that never
  existed on the wire -- the real response is
  `{status: "moved"|"not-found"|"occupied"}` -- so it silently read
  false for every response, making credential adoption always appear
  to fail and fall through to re-prompting the caller.
- authz.composeLinkRequired's multi-link message wording and the
  "claude-remote" provider label had drifted from the original
  TypeScript implementation (missing the "N accounts" count, wrong
  parenthesized label), breaking round-trip parity with upstream copy.
- bridged-agent-workflow.e2e.ts seeded credentials at a stale
  placeholder subject that only "worked" before principal resolution
  was fixed; updated to the correct canonical-principal subject.

Verified: identity-keying.e2e.ts's full 15 tests pass (previously hung
indefinitely), caller-tools.e2e.ts 18/18, happy-path/bridged-agent-
workflow/chat-harness/waitfor-guard all pass individually.
resilience.e2e.ts has 2 pre-existing failures (a gRPC-level stream
cancellation surfacing instead of the workflow's own idle-timeout
message) unrelated to any change in this session -- left as a known
follow-up, not fabricated as passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@imaustink
imaustink merged commit 7adbb2b into main Aug 15, 2026
6 checks passed
@imaustink
imaustink deleted the test/temporal-minikube-e2e branch August 15, 2026 12:19
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