Wire the Temporal engine's minikube profile so bridged agents actually run, and prove it - #205
Merged
Merged
Conversation
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>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #204 (which fixed the missing
durable-agents.dev/bridgedchart 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:
e2e/manifests/temporal-dev-server.yamladds a single-node dev-mode server (temporalio/admin-tools'stemporal server start-dev), applied bye2e/scripts/up.shbefore the release deploys.temporal-engine.nats.urlunset → bridged pod agents disabled entirely at the worker.temporal-engine.qdrant.hostunset → all retrieval activities disabled, so no Agent could ever be found.temporal-engine.gateway.identity.defaultSubject/defaultRolesunset → every internal call fromagent-orchestratorarrives at the Go gateway with no bearer token at all (its ownAGENT_TEMPORAL_ENGINE_TOKENsecret key doesn't exist), resolving to an emptyCaller.Subject— and the workflow treats an empty subject as "skip the catalog, answer bare," unconditionally.temporal-engine.identityLink.gatewayUrlunset → 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 realintegration-gatewayAPI reads).engines/temporal/internal/identitylink/identitylink.go): the claude-remote provider's/claude-auth/api/token?mode=loginresponse shape uses acredentialsJsonfield, but the client only ever readtoken— so a real, retrievable credential was silently treated as absent, permanently parking any agent requiring theclaude-remoteprovider.controllers/core-controller/internal/controller/agentrun_controller.go): the Temporal engine namesAgentRunCRsagentrun-<agentId>-<uuid>itself. The reconciler's Job name template re-prefixedagentrun-onto that, producing a Job name — and therefore the API server's auto-addedjob-namepod-template label, which reuses it verbatim — over Kubernetes' 63-byte label limit. This broke Job creation for anyAgentRunthe Temporal engine launches, not just bridged ones.GATEWAY_SENDER_ASSERTION_SECRETunset on the Temporal gateway —temporal-engine.gateway.senderAssertion.secretNamewas never wired, so the Go gateway could never verify the signed sender-login assertionagent-orchestrator'sTemporalEngineclient 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, soAGENT_ACTOR_LOGINnever reached a launchedAgentRun's secretEnv and anygh/tool-name the declarative planner guessed at got rejected — the originally-reported production failure./invoke.agent-orchestrator'sTemporalEngine.tsnever forwarded a chat turn's resolvedcallerTools,callerToolChoiceRequired, or prior client-executed tool-call results to the engine's/invokeendpoint, and the endpoint itself had no fields to receive them. AddedCallerTools/CallerToolRequired/PriorCallerToolCallstoinvokeRequest, passed through as already-resolved data (agent-orchestrator ranks/validates tools itself before this hop — the engine doesn't re-parse or re-rank).agent-orchestratorauthenticates 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 aPerUserflag — 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'splanProviders/adopt). Scoped deliberately to the forwarded-user-JWT path only — an earlier version also resolved identity from the plain sharedauthTokenfallback, which regressed webhook-relayed turns by overriding their correct default subject with integration-gateway's own service identity.identitylink.Client.Rekeyparsed a field that never existed on the wire. The real/claude-auth/api/rekeyresponse is{status: "moved"|"not-found"|"occupied"}, but the client read a booleanmovedfield — always decodingfalse, 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.authz.composeLinkRequired's multi-link message dropped the "link N accounts" count entirely, and theclaude-remoteprovider's label was"Claude (Remote Control)"instead of upstream's"Claude Remote Control".bridged-agent-workflow.e2e.tsseeded 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, matchingidentity-keying.e2e.ts's own pattern.The actual proof point
e2e/specs/bridged-agent-workflow.e2e.tsdrives the same webhook pathhappy-path.e2e.tsdoes, then asks Temporal itself (via thetemporalCLI against the dev server, not a log line or an inferred side effect) whether aBridgedAgentWorkflowexecution actually completed for the route's target agent.Test plan
bridged-agent-workflow.e2e.tspasses on its own against a fully-wired minikube clusterhappy-path.e2e.tspasses (previously failed:AGENT_ACTOR_LOGINmissing from the launchedAgentRun's secretEnv)caller-tools.e2e.tspasses 18/18 (previously 14/18: caller-supplied tools and their resumed results were silently dropped over/invoke)identity-keying.e2e.tspasses all 15/15 (previously hung indefinitely — root cause was the chain of bugs docs: kubectl guide for operating the Open WebUI chat UI #10–Add cluster-debug-skill: read-only kubectl + SigNoz query Tools #13 above)chat-harness.e2e.ts,waitfor-guard.e2e.tspassresilience.e2e.tshas 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 ., andgo test ./...clean inengines/temporal(new tests:caller_identity_assertion_test.go— including a TypeScript-generated cross-implementation vector, matchingsender_assertion_test.go's own discipline — andrekey_test.go)npx tsc --noEmitclean inapps/agent-orchestrator(pre-existing, unrelated errors innats-agent-channel.ts/nats-job-receiver.tsexcluded — a stale@controller-agent/messagingtype gap, not touched by this PR)🤖 Generated with Claude Code
https://claude.ai/code/session_01HctRMtdZixptEZeMebhADq