From 47e0bcc8a63c9420755b615fcf98341df1c20f5d Mon Sep 17 00:00:00 2001 From: Sebastian Bruckner Date: Fri, 9 Oct 2026 14:39:16 +0200 Subject: [PATCH 1/2] Send temporal-namespace header from ActivityClient WorkflowClient, ScheduleClient and NexusClient wrap their service stubs in NamespaceInjectWorkflowServiceStubs so every call carries the temporal-namespace header. ActivityClient did not, so Standalone Activity calls such as StartActivityExecution went out without it, and Temporal Cloud rejects them for API-key authentication with PERMISSION_DENIED: Request unauthorized. Co-Authored-By: Claude Opus 5.5 --- .../temporal/client/ActivityClientImpl.java | 2 ++ .../authorization/AuthorizationTokenTest.java | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java index 77bf2bd794..f73c80db9e 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java @@ -8,6 +8,7 @@ import io.temporal.common.interceptors.Header; import io.temporal.internal.client.ActivityClientInternal; import io.temporal.internal.client.ActivityHandleImpl; +import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs; import io.temporal.internal.client.RootActivityClientInvoker; import io.temporal.internal.client.external.GenericWorkflowClientImpl; import io.temporal.internal.client.external.ManualActivityCompletionClientFactory; @@ -37,6 +38,7 @@ class ActivityClientImpl implements ActivityClient, ActivityClientInternal { private final Scope metricsScope; ActivityClientImpl(WorkflowServiceStubs stubs, ActivityClientOptions options) { + stubs = new NamespaceInjectWorkflowServiceStubs(stubs, options.getNamespace()); this.stubs = stubs; this.options = options; this.metricsScope = diff --git a/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java b/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java index b69327eae6..85adeea53c 100644 --- a/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java +++ b/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.*; import io.grpc.*; +import io.temporal.client.ActivityClient; +import io.temporal.client.StartActivityOptions; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowOptions; import io.temporal.serviceclient.WorkflowServiceStubsOptions; @@ -18,6 +20,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -125,6 +128,37 @@ public void allRequestsShouldHaveAnAuthToken() { } } + @Test + public void activityClientRequestsShouldHaveANamespace() { + testEnvironment.start(); + ActivityClient client = testEnvironment.getActivityClient(); + StartActivityOptions options = + StartActivityOptions.newBuilder() + .setId("activity-id") + .setTaskQueue(TASK_QUEUE) + .setStartToCloseTimeout(Duration.ofMinutes(1)) + .build(); + + // The test server does not implement StartActivityExecution; only the outgoing headers matter. + assertThrows(StatusRuntimeException.class, () -> client.start("SomeActivity", options)); + + List startRequests = + loggedRequests.stream() + .filter(request -> request.methodName.equals("StartActivityExecution")) + .collect(Collectors.toList()); + assertFalse("StartActivityExecution should have been called", startRequests.isEmpty()); + for (GrpcRequest grpcRequest : startRequests) { + assertEquals( + "StartActivityExecution should have an auth token", + AUTH_TOKEN, + grpcRequest.authTokenValue); + assertEquals( + "StartActivityExecution should have a namespace", + testEnvironment.getNamespace(), + grpcRequest.namespace); + } + } + public static class EmptyWorkflowImpl implements TestWorkflows.TestWorkflow1 { @Override From feb131f9c301e1394a05b351aa94adbebb75b3ab Mon Sep 17 00:00:00 2001 From: Sebastian Bruckner Date: Mon, 12 Oct 2026 16:25:05 +0200 Subject: [PATCH 2/2] Keep caller's stubs on ActivityClientImpl, add changelog entry TestWorkflowEnvironmentActivityClientTest asserts the client holds the exact WorkflowServiceStubs it was given, so only wrap them for outgoing calls. The authorization test no longer assumes the server rejects StartActivityExecution. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +++ .../java/io/temporal/client/ActivityClientImpl.java | 11 ++++++++--- .../authorization/AuthorizationTokenTest.java | 7 +++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af4bac6c19..c62d659d0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,9 @@ to docs, or any other relevant information. specific to the Temporal CLI dev server implementation and may no longer be valid if that implementation changes. ### Fixed +- `ActivityClient` now sends the `temporal-namespace` gRPC header like `WorkflowClient` and `ScheduleClient` do, + so standalone activity calls are no longer rejected with `PERMISSION_DENIED` by namespace-scoped auth (e.g. + Temporal Cloud API keys). - Test server now honors retry expiration deadlines that fall exactly on a whole second. Previously such deadlines were ignored and retries were scheduled past them instead of failing with `RETRY_STATE_TIMEOUT`. diff --git a/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java index f73c80db9e..52d8b31831 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java @@ -38,16 +38,21 @@ class ActivityClientImpl implements ActivityClient, ActivityClientInternal { private final Scope metricsScope; ActivityClientImpl(WorkflowServiceStubs stubs, ActivityClientOptions options) { - stubs = new NamespaceInjectWorkflowServiceStubs(stubs, options.getNamespace()); this.stubs = stubs; this.options = options; + WorkflowServiceStubs namespacedStubs = + new NamespaceInjectWorkflowServiceStubs(stubs, options.getNamespace()); this.metricsScope = stubs.getOptions().getMetricsScope().tagged(MetricsTag.defaultTags(options.getNamespace())); - GenericWorkflowClientImpl genericClient = new GenericWorkflowClientImpl(stubs, metricsScope); + GenericWorkflowClientImpl genericClient = + new GenericWorkflowClientImpl(namespacedStubs, metricsScope); this.invoker = initializeClientInvoker(genericClient, options); this.manualActivityCompletionClientFactory = ManualActivityCompletionClientFactory.newFactory( - stubs, options.getNamespace(), options.getIdentity(), options.getDataConverter()); + namespacedStubs, + options.getNamespace(), + options.getIdentity(), + options.getDataConverter()); } private static ActivityClientCallsInterceptor initializeClientInvoker( diff --git a/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java b/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java index 85adeea53c..811fdfee36 100644 --- a/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java +++ b/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java @@ -139,8 +139,11 @@ public void activityClientRequestsShouldHaveANamespace() { .setStartToCloseTimeout(Duration.ofMinutes(1)) .build(); - // The test server does not implement StartActivityExecution; only the outgoing headers matter. - assertThrows(StatusRuntimeException.class, () -> client.start("SomeActivity", options)); + // Only the outgoing request headers matter, not whether the server supports the call. + try { + client.start("SomeActivity", options); + } catch (StatusRuntimeException ignored) { + } List startRequests = loggedRequests.stream()