---
.../credentials/AgentspanSecretsDAO.java | 34 +--
.../credentials/CredentialEnvSeeder.java | 14 +-
.../EncryptedDbCredentialStoreProvider.java | 26 ++-
.../AgentspanAIModelProviderPerUserTest.java | 129 ----------
.../CredentialMaskingIntegrationTest.java | 4 +-
.../ProviderStatusEndpointTest.java | 8 +-
.../WorkerCredentialsIntegrationTest.java | 221 ------------------
.../credentials/AgentspanSecretsDAOTest.java | 50 ++--
.../credentials/ConcurrentPutRaceTest.java | 14 +-
.../CredentialAwareHttpTaskTest.java | 10 +-
.../CredentialAwareMcpServiceTest.java | 10 +-
.../CredentialEnvSeederIntegrationTest.java | 7 +-
.../credentials/CredentialEnvSeederTest.java | 62 ++---
.../CredentialResolutionServiceTest.java | 21 +-
...ncryptedDbCredentialStoreProviderTest.java | 26 +--
.../SchemaMigratorUpgradePathTest.java | 12 +-
.../runtime/controller/SecretController.java | 20 +-
.../CredentialResolutionService.java | 4 +-
.../runtime/spi/CredentialStoreProvider.java | 10 +-
19 files changed, 152 insertions(+), 530 deletions(-)
delete mode 100644 server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderPerUserTest.java
delete mode 100644 server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/WorkerCredentialsIntegrationTest.java
diff --git a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/AgentspanSecretsDAO.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/AgentspanSecretsDAO.java
index af6023c7f..4da13cc39 100644
--- a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/AgentspanSecretsDAO.java
+++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/AgentspanSecretsDAO.java
@@ -19,7 +19,7 @@
/**
* Bridges conductor's global {@link SecretsDAO} to AgentSpan's own {@link CredentialStoreProvider}
- * (the encrypted credential store), scoped to the anonymous/system user.
+ * (the encrypted credential store).
*
* Active only when {@code conductor.secrets.type=agentspan} — the "agentspan-as-host" mode where
* the AgentSpan server embeds conductor ({@code agentspan.embedded=true}) and also serves as
@@ -30,11 +30,9 @@
* own env-variable / noop {@code SecretsDAO} implementations off (they require
* {@code conductor.secrets.type} to be {@code env}/absent or {@code noop}).
*
- * Conductor secrets are global (name only); AgentSpan's store is per-user, so every lookup is
- * scoped to {@link #ANONYMOUS_USER_ID} — the no-auth/system user, matching {@code CredentialEnvSeeder}
- * and {@code AuthFilter.ANONYMOUS}. Names are treated as flat keys (no dotted JSONPath): worker
- * credential names are simple identifiers, and {@link CredentialStoreProvider#get} resolves them
- * directly.
+ * Conductor secrets are global (name only), which matches {@link CredentialStoreProvider}'s
+ * single-scope store. Names are treated as flat keys (no dotted JSONPath): worker credential names
+ * are simple identifiers, and {@link CredentialStoreProvider#get} resolves them directly.
*
* The backing store beans ({@code EncryptedDbCredentialStoreProvider}, {@code MasterKeyConfig},
* {@code CredentialDataSourceConfig}, {@code CredentialSchemaMigrator}) are normally dormant when
@@ -47,46 +45,36 @@ public class AgentspanSecretsDAO implements SecretsDAO {
private static final Logger log = LoggerFactory.getLogger(AgentspanSecretsDAO.class);
- /**
- * User ID for the anonymous/OSS user — matches {@code CredentialEnvSeeder.ANONYMOUS_USER_ID} and
- * {@code AuthFilter.ANONYMOUS}. Conductor's global secret lookups resolve against this user.
- */
- static final String ANONYMOUS_USER_ID = "00000000-0000-0000-0000-000000000000";
-
private final CredentialStoreProvider store;
public AgentspanSecretsDAO(CredentialStoreProvider store) {
this.store = store;
- log.info(
- "AgentspanSecretsDAO active — embedded conductor secrets resolve from the AgentSpan "
- + "credential store (scoped to system user {})",
- ANONYMOUS_USER_ID);
+ log.info("AgentspanSecretsDAO active — embedded conductor secrets resolve from the "
+ + "AgentSpan credential store");
}
@Override
public String getSecret(String key) {
- return store.get(ANONYMOUS_USER_ID, key);
+ return store.get(key);
}
@Override
public boolean secretExists(String key) {
- return store.get(ANONYMOUS_USER_ID, key) != null;
+ return store.get(key) != null;
}
@Override
public List listSecretNames() {
- return store.list(ANONYMOUS_USER_ID).stream()
- .map(CredentialMeta::getName)
- .collect(Collectors.toList());
+ return store.list().stream().map(CredentialMeta::getName).collect(Collectors.toList());
}
@Override
public void putSecret(String key, String value) {
- store.set(ANONYMOUS_USER_ID, key, value);
+ store.set(key, value);
}
@Override
public void deleteSecret(String key) {
- store.delete(ANONYMOUS_USER_ID, key);
+ store.delete(key);
}
}
diff --git a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java
index 45ed18874..ed0b03c0e 100644
--- a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java
+++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/CredentialEnvSeeder.java
@@ -40,12 +40,6 @@ public class CredentialEnvSeeder implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(CredentialEnvSeeder.class);
- /**
- * User ID for the anonymous/OSS user — matches {@code AuthFilter.ANONYMOUS}.
- * In no-auth mode all credentials are stored under this ID.
- */
- static final String ANONYMOUS_USER_ID = "00000000-0000-0000-0000-000000000000";
-
/**
* Well-known provider environment variables to scan on startup.
*
@@ -90,7 +84,7 @@ public void run(ApplicationArguments args) {
String existing;
try {
- existing = storeProvider.get(ANONYMOUS_USER_ID, name);
+ existing = storeProvider.get(name);
} catch (Exception e) {
if (!(e.getCause() instanceof AEADBadTagException)) {
throw e; // not a key mismatch — propagate (e.g. DB connection failure)
@@ -101,8 +95,8 @@ public void run(ApplicationArguments args) {
name,
e);
try {
- storeProvider.delete(ANONYMOUS_USER_ID, name);
- storeProvider.set(ANONYMOUS_USER_ID, name, value);
+ storeProvider.delete(name);
+ storeProvider.set(name, value);
created++;
} catch (Exception re) {
log.warn("Credential '{}' could not be re-seeded — skipping", name, re);
@@ -120,7 +114,7 @@ public void run(ApplicationArguments args) {
}
try {
- storeProvider.set(ANONYMOUS_USER_ID, name, value);
+ storeProvider.set(name, value);
log.info("Credential seeded from environment: {}", name);
created++;
} catch (Exception e) {
diff --git a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProvider.java b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProvider.java
index 4fcd66fba..43ce78a33 100644
--- a/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProvider.java
+++ b/server/conductor-agentspan-server/src/main/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProvider.java
@@ -44,6 +44,13 @@ public class EncryptedDbCredentialStoreProvider implements CredentialStoreProvid
private static final int TAG_LENGTH = 128; // GCM auth tag bits
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+ /**
+ * Fixed single-scope owner for all rows. The {@code credentials_store} schema keeps its
+ * {@code user_id} column (part of the (user_id, name) primary key), but the store is global —
+ * every row is written and read under this constant, so lookups are effectively by name.
+ */
+ private static final String DEFAULT_USER_ID = "00000000-0000-0000-0000-000000000000";
+
private final NamedParameterJdbcTemplate jdbc;
private final byte[] masterKey;
@@ -55,24 +62,24 @@ public EncryptedDbCredentialStoreProvider(
}
@Override
- public String get(String userId, String name) {
+ public String get(String name) {
try {
byte[] encrypted = jdbc.queryForObject(
"SELECT encrypted_value FROM credentials_store " + "WHERE user_id = :uid AND name = :n",
- Map.of("uid", userId, "n", name),
+ Map.of("uid", DEFAULT_USER_ID, "n", name),
byte[].class);
if (encrypted == null) return null;
return decrypt(encrypted);
} catch (EmptyResultDataAccessException e) {
return null;
} catch (Exception e) {
- log.error("Failed to decrypt credential '{}' for user '{}': {}", name, userId, e.getMessage());
+ log.error("Failed to decrypt credential '{}': {}", name, e.getMessage());
throw new IllegalStateException("Failed to decrypt credential: " + name, e);
}
}
@Override
- public void set(String userId, String name, String value) {
+ public void set(String name, String value) {
try {
byte[] encrypted = encrypt(value);
String now = Instant.now().toString();
@@ -86,20 +93,21 @@ public void set(String userId, String name, String value) {
+ "ON CONFLICT(user_id, name) DO UPDATE SET "
+ " encrypted_value = excluded.encrypted_value, "
+ " updated_at = excluded.updated_at",
- Map.of("uid", userId, "n", name, "enc", encrypted, "now", now));
+ Map.of("uid", DEFAULT_USER_ID, "n", name, "enc", encrypted, "now", now));
} catch (Exception e) {
throw new IllegalStateException("Failed to store credential: " + name, e);
}
}
@Override
- public void delete(String userId, String name) {
+ public void delete(String name) {
jdbc.update(
- "DELETE FROM credentials_store WHERE user_id = :uid AND name = :n", Map.of("uid", userId, "n", name));
+ "DELETE FROM credentials_store WHERE user_id = :uid AND name = :n",
+ Map.of("uid", DEFAULT_USER_ID, "n", name));
}
@Override
- public List list(String userId) {
+ public List list() {
// Include encrypted_value in the SELECT so we can decrypt inline — avoids
// an N+1 query pattern. On SQLite the pool is capped at 1 connection so a
// nested get() call inside a RowMapper would deadlock; on PostgreSQL the
@@ -107,7 +115,7 @@ public List list(String userId) {
return jdbc.query(
"SELECT name, encrypted_value, created_at, updated_at "
+ "FROM credentials_store WHERE user_id = :uid ORDER BY name",
- Map.of("uid", userId),
+ Map.of("uid", DEFAULT_USER_ID),
(rs, row) -> {
String name = rs.getString("name");
byte[] enc = rs.getBytes("encrypted_value");
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderPerUserTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderPerUserTest.java
deleted file mode 100644
index 5e8f5aa68..000000000
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderPerUserTest.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (c) 2025 AgentSpan
- * Licensed under the MIT License.
- */
-package dev.agentspan.runtime.ai;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.time.Instant;
-
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.ActiveProfiles;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import dev.agentspan.runtime.AgentRuntime;
-import dev.agentspan.runtime.context.RequestContext;
-import dev.agentspan.runtime.context.RequestContextHolder;
-import dev.agentspan.runtime.spi.CredentialStoreProvider;
-
-/**
- * Audit Gap B — per-user resolution of LLM provider API keys.
- *
- * Catches: cross-tenant credential leak. Two users register
- * {@code OPENAI_API_KEY} with different values; if the per-user resolution
- * path mistakenly returns user B's value while running under user A's request
- * context (off-by-one user_id binding, ThreadLocal bleed, missing user
- * filter, …), an LLM call would route through the wrong tenant.
- *
- * The previous test for this class was 21 lines and only verified config
- * construction — it would not catch this bug.
- */
-@SpringBootTest(classes = AgentRuntime.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
-@ActiveProfiles("test")
-class AgentspanAIModelProviderPerUserTest {
-
- @Autowired
- private AgentspanAIModelProvider provider;
-
- @Autowired
- private CredentialStoreProvider store;
-
- private static final String USER_A = "ai-test-user-A";
- private static final String USER_B = "ai-test-user-B";
- private static final String OPENAI = "OPENAI_API_KEY";
- private static final String ANON = "00000000-0000-0000-0000-000000000000";
-
- private String savedAnonValue;
-
- @BeforeEach
- void setUp() {
- // Secrets are keyed by a userId string in credentials_store; no users table needed.
- // The CredentialEnvSeeder copies the developer/CI shell environment into
- // the anonymous user's store at startup, including OPENAI_API_KEY if
- // one is set in the shell. That seeding is the right behavior for
- // local-dev frictionlessness, but it pollutes this test which wants
- // to assert anonymous-fallback returns null. Snapshot and clear it.
- savedAnonValue = store.get(ANON, OPENAI);
- if (savedAnonValue != null) store.delete(ANON, OPENAI);
-
- // Same name, different values for each test user
- store.set(USER_A, OPENAI, "sk-userA-12345");
- store.set(USER_B, OPENAI, "sk-userB-67890");
- }
-
- @AfterEach
- void cleanUp() {
- store.delete(USER_A, OPENAI);
- store.delete(USER_B, OPENAI);
- // Restore the seeded anonymous-user value so other tests still see it.
- if (savedAnonValue != null) store.set(ANON, OPENAI, savedAnonValue);
- RequestContextHolder.clear();
- }
-
- @Test
- void resolveUserApiKey_returnsTheCorrectUserSValue() {
- // Under user A's request context, the resolved key is user A's value.
- RequestContextHolder.set(RequestContext.builder()
- .requestId("r-a")
- .userId(USER_A)
- .createdAt(Instant.now())
- .build());
-
- String resolvedA = (String) ReflectionTestUtils.invokeMethod(provider, "resolveUserApiKey", "openai");
- assertThat(resolvedA).isEqualTo("sk-userA-12345");
-
- // Switch to user B's request context — must now see B's value, never A's.
- RequestContextHolder.clear();
- RequestContextHolder.set(RequestContext.builder()
- .requestId("r-b")
- .userId(USER_B)
- .createdAt(Instant.now())
- .build());
-
- String resolvedB = (String) ReflectionTestUtils.invokeMethod(provider, "resolveUserApiKey", "openai");
- assertThat(resolvedB).isEqualTo("sk-userB-67890");
- }
-
- @Test
- void resolveUserApiKey_noRequestContext_fallsBackToAnonymousUserId() {
- // No RequestContext, no TaskContext: code falls back to anonymous UUID.
- // Anonymous has no value stored → null.
- RequestContextHolder.clear();
- String resolved = (String) ReflectionTestUtils.invokeMethod(provider, "resolveUserApiKey", "openai");
- assertThat(resolved).isNull();
- }
-
- @Test
- void resolveUserApiKey_unknownProvider_returnsNull() {
- RequestContextHolder.set(RequestContext.builder()
- .requestId("r")
- .userId(USER_A)
- .createdAt(Instant.now())
- .build());
-
- String resolved =
- (String) ReflectionTestUtils.invokeMethod(provider, "resolveUserApiKey", "no-such-provider-xyz");
- assertThat(resolved).isNull();
- }
-
- // Note: an `isProviderConfigured_distinguishesUsers` test would be nice but
- // is hard to write deterministically because the test profile may have
- // openai pre-registered as a startup-configured provider depending on
- // whether the shell environment has OPENAI_API_KEY at JVM start. Per-user
- // resolution is exercised directly by the three tests above.
-}
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/CredentialMaskingIntegrationTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/CredentialMaskingIntegrationTest.java
index 9da645d3d..92d72aed0 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/CredentialMaskingIntegrationTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/CredentialMaskingIntegrationTest.java
@@ -57,12 +57,12 @@ class CredentialMaskingIntegrationTest {
@BeforeEach
void setUp() {
- store.set(userId, CRED_NAME, CRED_VALUE);
+ store.set(CRED_NAME, CRED_VALUE);
}
@AfterEach
void cleanUp() {
- store.delete(userId, CRED_NAME);
+ store.delete(CRED_NAME);
}
// ── Advice URI coverage ─────────────────────────────────────────────
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/ProviderStatusEndpointTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/ProviderStatusEndpointTest.java
index 4d0bf45db..f5d5d61ec 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/ProviderStatusEndpointTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/ProviderStatusEndpointTest.java
@@ -52,14 +52,14 @@ class ProviderStatusEndpointTest {
@BeforeEach
void setUp() {
- savedOllamaUrl = store.get(ANON, "OLLAMA_BASE_URL");
- store.set(ANON, "OLLAMA_BASE_URL", UNREACHABLE_URL);
+ savedOllamaUrl = store.get("OLLAMA_BASE_URL");
+ store.set("OLLAMA_BASE_URL", UNREACHABLE_URL);
}
@AfterEach
void cleanUp() {
- store.delete(ANON, "OLLAMA_BASE_URL");
- if (savedOllamaUrl != null) store.set(ANON, "OLLAMA_BASE_URL", savedOllamaUrl);
+ store.delete("OLLAMA_BASE_URL");
+ if (savedOllamaUrl != null) store.set("OLLAMA_BASE_URL", savedOllamaUrl);
}
private JsonNode getStatus() throws Exception {
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/WorkerCredentialsIntegrationTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/WorkerCredentialsIntegrationTest.java
deleted file mode 100644
index be429ca9d..000000000
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/WorkerCredentialsIntegrationTest.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (c) 2025 AgentSpan
- * Licensed under the MIT License.
- */
-package dev.agentspan.runtime.controller;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.util.List;
-import java.util.Map;
-
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.http.ResponseEntity;
-import org.springframework.test.context.ActiveProfiles;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import dev.agentspan.runtime.AgentRuntime;
-import dev.agentspan.runtime.credentials.ExecutionTokenService;
-import dev.agentspan.runtime.model.credentials.ResolveRequest;
-import dev.agentspan.runtime.spi.CredentialStoreProvider;
-
-/**
- * Audit gaps C + F — full-stack {@code POST /api/workers/secrets} integration.
- *
- * {@code WorkerSecretsTest} uses Mockito to stub the resolution service —
- * good for controller wiring but blind to the user-scoping behavior of the
- * real store. This test wires the real Spring beans (real
- * {@code CredentialResolutionService}, real {@code EncryptedDbCredentialStoreProvider},
- * real {@code ExecutionTokenService}) and exercises the integration boundary.
- *
- * Gap C: two users, same secret name, different values.
- * Each user's execution token must return only its OWN value when the worker
- * calls {@code /api/workers/secrets}. Catches: cross-tenant leak via wrong
- * user_id binding in JDBC or wrong user_id extraction from the token.
- *
- * Gap F: rotation semantics. A token doesn't snapshot — when
- * a secret is updated mid-execution, subsequent resolves return the new value.
- * Documents the design intent that workers see live values, not point-in-time
- * captures.
- */
-@SpringBootTest(classes = AgentRuntime.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
-@ActiveProfiles("test")
-class WorkerCredentialsIntegrationTest {
-
- @Autowired
- private WorkerController controller;
-
- @Autowired
- private CredentialStoreProvider store;
-
- @Autowired
- private ExecutionTokenService tokenService;
-
- // Use a non-seeded name so the CredentialEnvSeeder doesn't accidentally
- // populate it for the anonymous user (and pollute the test).
- private static final String NAME = "_E2E_TENANT_TEST_KEY";
-
- private static final String USER_A = "tenant-test-user-A";
- private static final String USER_B = "tenant-test-user-B";
-
- @BeforeEach
- void setUp() {
- // Secrets are keyed by a userId string in credentials_store; no users table needed.
- // Same name, different values
- store.set(USER_A, NAME, "VALUE-FROM-A");
- store.set(USER_B, NAME, "VALUE-FROM-B");
-
- // Tame the rate limit in case earlier tests in the same JVM
- // exhausted it.
- ReflectionTestUtils.setField(controller, "resolveRateLimit", 1000);
- }
-
- @AfterEach
- void cleanUp() {
- store.delete(USER_A, NAME);
- store.delete(USER_B, NAME);
- }
-
- // ── Gap C: cross-user isolation ─────────────────────────────────────
-
- @Test
- @SuppressWarnings("unchecked")
- void resolve_eachUserGetsOnlyTheirOwnValue() {
- String tokenA = tokenService.mint(USER_A, "wf-A", List.of(NAME), 3600);
- String tokenB = tokenService.mint(USER_B, "wf-B", List.of(NAME), 3600);
-
- // User A's token
- ResolveRequest reqA = new ResolveRequest();
- reqA.setToken(tokenA);
- reqA.setNames(List.of(NAME));
- ResponseEntity> respA = controller.resolveCredentials(reqA);
- assertThat(respA.getStatusCode().value()).isEqualTo(200);
- Map bodyA = (Map) respA.getBody();
- assertThat(bodyA).containsEntry(NAME, "VALUE-FROM-A");
- // Critical: A's response must NOT contain B's value, anywhere.
- assertThat(bodyA.values()).doesNotContain("VALUE-FROM-B");
-
- // User B's token
- ResolveRequest reqB = new ResolveRequest();
- reqB.setToken(tokenB);
- reqB.setNames(List.of(NAME));
- ResponseEntity> respB = controller.resolveCredentials(reqB);
- assertThat(respB.getStatusCode().value()).isEqualTo(200);
- Map bodyB = (Map) respB.getBody();
- assertThat(bodyB).containsEntry(NAME, "VALUE-FROM-B");
- assertThat(bodyB.values()).doesNotContain("VALUE-FROM-A");
- }
-
- @Test
- @SuppressWarnings("unchecked")
- void resolve_userTokenCannotAccessOtherUserCredential_evenIfDeclared() {
- // User A's token declares a name that doesn't exist for user A.
- // (User B has it stored, but A's token must NOT reach B's storage.)
- store.delete(USER_A, NAME);
- String tokenA = tokenService.mint(USER_A, "wf-AX", List.of(NAME), 3600);
-
- ResolveRequest req = new ResolveRequest();
- req.setToken(tokenA);
- req.setNames(List.of(NAME));
-
- ResponseEntity> resp = controller.resolveCredentials(req);
- assertThat(resp.getStatusCode().value()).isEqualTo(200);
- Map body = (Map) resp.getBody();
- // Server returns 200 with the missing name omitted (SDK turns this
- // into CredentialNotFoundError on the worker side). The crucial
- // property: B's value MUST NOT leak via A's token.
- assertThat(body).doesNotContainKey(NAME);
- assertThat(body.values()).doesNotContain("VALUE-FROM-B");
- }
-
- // ── Gap F: rotation semantics ───────────────────────────────────────
-
- @Test
- @SuppressWarnings("unchecked")
- void resolve_sameTokenSeesUpdatedValueAfterRotation() {
- String token = tokenService.mint(USER_A, "wf-rot", List.of(NAME), 3600);
-
- ResolveRequest req = new ResolveRequest();
- req.setToken(token);
- req.setNames(List.of(NAME));
-
- // First resolve → V1
- Map first =
- (Map) controller.resolveCredentials(req).getBody();
- assertThat(first).containsEntry(NAME, "VALUE-FROM-A");
-
- // Rotate to V2 while the SAME token is still valid
- store.set(USER_A, NAME, "ROTATED-VALUE-V2");
-
- // Second resolve with the same token → V2 (not snapshotted at mint)
- Map second =
- (Map) controller.resolveCredentials(req).getBody();
- assertThat(second).containsEntry(NAME, "ROTATED-VALUE-V2");
-
- // Final state matches the latest write
- assertThat(store.get(USER_A, NAME)).isEqualTo("ROTATED-VALUE-V2");
- }
-
- @Test
- void resolve_deletedCredentialDuringExecution_becomesUnresolvable() {
- // Edge case: credential is deleted between mint and resolve. The token is
- // still valid (it doesn't lock the value, only declared names), but
- // the response simply omits the unresolvable name.
- String token = tokenService.mint(USER_A, "wf-del", List.of(NAME), 3600);
- store.delete(USER_A, NAME);
-
- ResolveRequest req = new ResolveRequest();
- req.setToken(token);
- req.setNames(List.of(NAME));
-
- @SuppressWarnings("unchecked")
- Map body =
- (Map) controller.resolveCredentials(req).getBody();
- assertThat(body).doesNotContainKey(NAME);
- }
-
- // ── Bug #4: empty declared-names list MUST NOT permit arbitrary names ──
-
- @Test
- @SuppressWarnings("unchecked")
- void resolve_emptyDeclaredNames_rejectsAllRequestedNames() {
- // A token minted with an empty declared_names list (which happens for
- // every agent that doesn't declare credentials — the common case) must
- // NOT permit resolving arbitrary credential names. The defense-in-depth
- // claim of declared-name binding requires this.
- String token = tokenService.mint(USER_A, "wf-empty", List.of(), 3600);
-
- ResolveRequest req = new ResolveRequest();
- req.setToken(token);
- req.setNames(List.of(NAME));
-
- Map body =
- (Map) controller.resolveCredentials(req).getBody();
- // Pre-fix: this returned the actual stored value (bypass). After fix:
- // empty declared list means nothing is resolvable through this token.
- assertThat(body).isEmpty();
- }
-
- @Test
- @SuppressWarnings("unchecked")
- void resolve_emptyDeclaredNames_userBValueAlsoBlocked() {
- // Stronger: token has empty declared list, asks for User B's credential.
- // Even though A's user_id is authoritative (so cross-user leak wouldn't
- // happen anyway), the binding-check should reject the name itself
- // before any user-scoped resolve is attempted.
- String token = tokenService.mint(USER_A, "wf-empty2", List.of(), 3600);
-
- ResolveRequest req = new ResolveRequest();
- req.setToken(token);
- req.setNames(List.of(NAME));
-
- Map body =
- (Map) controller.resolveCredentials(req).getBody();
- assertThat(body.values()).doesNotContain("VALUE-FROM-B");
- assertThat(body.values()).doesNotContain("VALUE-FROM-A");
- }
-}
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/AgentspanSecretsDAOTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/AgentspanSecretsDAOTest.java
index 82ccbd727..33f7c59b3 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/AgentspanSecretsDAOTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/AgentspanSecretsDAOTest.java
@@ -21,53 +21,43 @@
import dev.agentspan.runtime.spi.CredentialStoreProvider;
/**
- * {@link AgentspanSecretsDAO} bridges conductor's global {@code SecretsDAO} to AgentSpan's per-user
- * {@link CredentialStoreProvider}, scoped to the anonymous/system user. Verifies the name→value
- * round-trip is scoped to {@code ANONYMOUS_USER_ID} (so other users' secrets are invisible) and that
- * the bean is selected only by {@code conductor.secrets.type=agentspan}.
+ * {@link AgentspanSecretsDAO} bridges conductor's global {@code SecretsDAO} to AgentSpan's
+ * (single-scope) {@link CredentialStoreProvider}. Verifies the name→value round-trip and that the
+ * bean is selected only by {@code conductor.secrets.type=agentspan}.
*/
class AgentspanSecretsDAOTest {
- private static final String ANON = "00000000-0000-0000-0000-000000000000";
-
- /** In-memory {@link CredentialStoreProvider} keyed by (userId,name) so scope can be asserted. */
+ /** In-memory {@link CredentialStoreProvider} keyed by name (the store is global — no userId). */
static class FakeStore implements CredentialStoreProvider {
final Map data = new LinkedHashMap<>();
- private static String k(String u, String n) {
- return u + "|" + n;
- }
-
@Override
- public String get(String userId, String name) {
- return data.get(k(userId, name));
+ public String get(String name) {
+ return data.get(name);
}
@Override
- public void set(String userId, String name, String value) {
- data.put(k(userId, name), value);
+ public void set(String name, String value) {
+ data.put(name, value);
}
@Override
- public void delete(String userId, String name) {
- data.remove(k(userId, name));
+ public void delete(String name) {
+ data.remove(name);
}
@Override
- public List list(String userId) {
+ public List list() {
List out = new ArrayList<>();
- for (String key : data.keySet()) {
- int bar = key.indexOf('|');
- if (key.substring(0, bar).equals(userId)) {
- out.add(CredentialMeta.builder().name(key.substring(bar + 1)).build());
- }
+ for (String name : data.keySet()) {
+ out.add(CredentialMeta.builder().name(name).build());
}
return out;
}
}
@Test
- void roundTrip_scopedToAnonymousUser() {
+ void roundTrip() {
FakeStore store = new FakeStore();
AgentspanSecretsDAO dao = new AgentspanSecretsDAO(store);
@@ -75,8 +65,7 @@ void roundTrip_scopedToAnonymousUser() {
assertThat(dao.getSecret("GITHUB_TOKEN")).isNull();
dao.putSecret("GITHUB_TOKEN", "ghp_x");
- // written under the anonymous/system user — the scope conductor resolves against
- assertThat(store.data).containsEntry(ANON + "|GITHUB_TOKEN", "ghp_x");
+ assertThat(store.data).containsEntry("GITHUB_TOKEN", "ghp_x");
assertThat(dao.getSecret("GITHUB_TOKEN")).isEqualTo("ghp_x");
assertThat(dao.secretExists("GITHUB_TOKEN")).isTrue();
@@ -88,15 +77,6 @@ void roundTrip_scopedToAnonymousUser() {
assertThat(dao.listSecretNames()).containsExactly("SLACK_TOKEN");
}
- @Test
- void doesNotReadOtherUsersSecrets() {
- FakeStore store = new FakeStore();
- store.set("some-other-user", "GITHUB_TOKEN", "not-mine");
- AgentspanSecretsDAO dao = new AgentspanSecretsDAO(store);
- assertThat(dao.getSecret("GITHUB_TOKEN")).isNull();
- assertThat(dao.listSecretNames()).isEmpty();
- }
-
// ── gating: selected only by conductor.secrets.type=agentspan ──
@Configuration
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/ConcurrentPutRaceTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/ConcurrentPutRaceTest.java
index ff6e5d792..eac75d02c 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/ConcurrentPutRaceTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/ConcurrentPutRaceTest.java
@@ -46,12 +46,12 @@ class ConcurrentPutRaceTest {
@BeforeEach
void clean() {
- store.delete(USER, NAME);
+ store.delete(NAME);
}
@AfterEach
void clean2() {
- store.delete(USER, NAME);
+ store.delete(NAME);
}
@Test
@@ -63,7 +63,7 @@ void concurrentPut_newCredential_doesNotThrow() throws Exception {
final int idx = i;
futures[i] = CompletableFuture.runAsync(() -> {
try {
- store.set(USER, NAME, "value-" + idx);
+ store.set(NAME, "value-" + idx);
} catch (Throwable t) {
errors.incrementAndGet();
}
@@ -77,12 +77,12 @@ void concurrentPut_newCredential_doesNotThrow() throws Exception {
.isZero();
// And the value is set (last writer wins; we just assert SOME write succeeded).
- assertThat(store.get(USER, NAME)).isNotNull();
+ assertThat(store.get(NAME)).isNotNull();
}
@Test
void concurrentPut_existingCredential_doesNotThrow() throws Exception {
- store.set(USER, NAME, "initial-value");
+ store.set(NAME, "initial-value");
int N = 50;
AtomicInteger errors = new AtomicInteger();
@@ -91,7 +91,7 @@ void concurrentPut_existingCredential_doesNotThrow() throws Exception {
final int idx = i;
futures[i] = CompletableFuture.runAsync(() -> {
try {
- store.set(USER, NAME, "updated-value-" + idx);
+ store.set(NAME, "updated-value-" + idx);
} catch (Throwable t) {
errors.incrementAndGet();
}
@@ -100,6 +100,6 @@ void concurrentPut_existingCredential_doesNotThrow() throws Exception {
for (var f : futures) f.get();
assertThat(errors.get()).isZero();
- assertThat(store.get(USER, NAME)).startsWith("updated-value-");
+ assertThat(store.get(NAME)).startsWith("updated-value-");
}
}
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskTest.java
index 9ddee602a..c62e8d0aa 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskTest.java
@@ -35,7 +35,7 @@ class CredentialAwareHttpTaskTest {
@BeforeEach
void setUp() {
- storeProvider.set(USER_ID, "MY_API_KEY", "resolved-secret-value");
+ storeProvider.set("MY_API_KEY", "resolved-secret-value");
}
@Test
@@ -72,7 +72,7 @@ void resolveHeaders_noPlaceholders_returnsUnchanged() {
@Test
void resolveHeaders_credentialValueWithDollarSign_handledSafely() {
- storeProvider.set(USER_ID, "TRICKY_KEY", "val$with$dollars");
+ storeProvider.set("TRICKY_KEY", "val$with$dollars");
Map headers = new LinkedHashMap<>();
headers.put("Auth", "#{TRICKY_KEY}");
@@ -84,8 +84,8 @@ void resolveHeaders_credentialValueWithDollarSign_handledSafely() {
@Test
void resolveHeaders_multiplePlaceholders_inSameHeader() {
- storeProvider.set(USER_ID, "USER", "admin");
- storeProvider.set(USER_ID, "PASS", "secret123");
+ storeProvider.set("USER", "admin");
+ storeProvider.set("PASS", "secret123");
Map headers = new LinkedHashMap<>();
headers.put("Authorization", "Basic #{USER}:#{PASS}");
@@ -98,7 +98,7 @@ void resolveHeaders_multiplePlaceholders_inSameHeader() {
@Test
void resolveHeaders_dottedPathJsonExtraction() {
// Conductor-parity JSONPath: extract a field from a JSON-valued secret
- storeProvider.set(USER_ID, "GCP_SVC", "{\"project_id\":\"my-proj-99\",\"client_email\":\"sa@x.iam\"}");
+ storeProvider.set("GCP_SVC", "{\"project_id\":\"my-proj-99\",\"client_email\":\"sa@x.iam\"}");
Map headers = new LinkedHashMap<>();
headers.put("X-Project", "#{GCP_SVC.project_id}");
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareMcpServiceTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareMcpServiceTest.java
index f621c4d85..7f3915fff 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareMcpServiceTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareMcpServiceTest.java
@@ -36,7 +36,7 @@ class CredentialAwareMcpServiceTest {
@BeforeEach
void setUp() {
- storeProvider.set(USER_ID, "MY_API_KEY", "resolved-secret-value");
+ storeProvider.set("MY_API_KEY", "resolved-secret-value");
}
@Test
@@ -73,7 +73,7 @@ void resolveHeaders_noPlaceholders_returnsUnchanged() {
@Test
void resolveHeaders_credentialValueWithDollarSign_handledSafely() {
- storeProvider.set(USER_ID, "TRICKY_KEY", "val$with$dollars");
+ storeProvider.set("TRICKY_KEY", "val$with$dollars");
Map headers = new LinkedHashMap<>();
headers.put("Auth", "#{TRICKY_KEY}");
@@ -85,8 +85,8 @@ void resolveHeaders_credentialValueWithDollarSign_handledSafely() {
@Test
void resolveHeaders_multiplePlaceholders_inSameHeader() {
- storeProvider.set(USER_ID, "USER", "admin");
- storeProvider.set(USER_ID, "PASS", "secret123");
+ storeProvider.set("USER", "admin");
+ storeProvider.set("PASS", "secret123");
Map headers = new LinkedHashMap<>();
headers.put("Authorization", "Basic #{USER}:#{PASS}");
@@ -98,7 +98,7 @@ void resolveHeaders_multiplePlaceholders_inSameHeader() {
@Test
void resolveHeaders_dottedPathJsonExtraction() {
- storeProvider.set(USER_ID, "BLOB", "{\"auth\":{\"oauth\":{\"client_id\":\"abc123\"}}}");
+ storeProvider.set("BLOB", "{\"auth\":{\"oauth\":{\"client_id\":\"abc123\"}}}");
Map headers = new LinkedHashMap<>();
headers.put("X-Client-Id", "#{BLOB.auth.oauth.client_id}");
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialEnvSeederIntegrationTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialEnvSeederIntegrationTest.java
index 073d692e5..b1c30d515 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialEnvSeederIntegrationTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialEnvSeederIntegrationTest.java
@@ -4,7 +4,6 @@
*/
package dev.agentspan.runtime.credentials;
-import static dev.agentspan.runtime.credentials.CredentialEnvSeeder.ANONYMOUS_USER_ID;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
@@ -34,11 +33,11 @@ void seeder_storesCredential_withoutForeignKeyError() {
// The seeder runs at startup. If ANTHROPIC_API_KEY is in the env
// (or test properties), the credential should exist.
// At minimum, verify we can write and read back without errors.
- storeProvider.set(ANONYMOUS_USER_ID, "INTEGRATION_TEST_KEY", "test-value-123");
- String value = storeProvider.get(ANONYMOUS_USER_ID, "INTEGRATION_TEST_KEY");
+ storeProvider.set("INTEGRATION_TEST_KEY", "test-value-123");
+ String value = storeProvider.get("INTEGRATION_TEST_KEY");
assertThat(value).isEqualTo("test-value-123");
// Cleanup
- storeProvider.delete(ANONYMOUS_USER_ID, "INTEGRATION_TEST_KEY");
+ storeProvider.delete("INTEGRATION_TEST_KEY");
}
}
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialEnvSeederTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialEnvSeederTest.java
index 5e80238a9..231de59f6 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialEnvSeederTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialEnvSeederTest.java
@@ -4,7 +4,6 @@
*/
package dev.agentspan.runtime.credentials;
-import static dev.agentspan.runtime.credentials.CredentialEnvSeeder.ANONYMOUS_USER_ID;
import static org.assertj.core.api.Assertions.*;
import java.util.Map;
@@ -45,14 +44,14 @@ void cleanUp() {
try {
jdbc.update(
"DELETE FROM credentials_store WHERE user_id = :uid AND name LIKE '\\_TEST\\_%' ESCAPE '\\'",
- Map.of("uid", ANONYMOUS_USER_ID));
+ Map.of("uid", "00000000-0000-0000-0000-000000000000"));
} catch (Exception ignored) {
// Table may not exist yet on the first test run — safe to ignore.
}
- storeProvider.delete(ANONYMOUS_USER_ID, "GH_TOKEN");
- storeProvider.delete(ANONYMOUS_USER_ID, "GITHUB_TOKEN");
- storeProvider.delete(ANONYMOUS_USER_ID, "OPENAI_BASE_URL");
- storeProvider.delete(ANONYMOUS_USER_ID, "ANTHROPIC_BASE_URL");
+ storeProvider.delete("GH_TOKEN");
+ storeProvider.delete("GITHUB_TOKEN");
+ storeProvider.delete("OPENAI_BASE_URL");
+ storeProvider.delete("ANTHROPIC_BASE_URL");
}
@Test
@@ -75,19 +74,19 @@ void seeder_storesCredentialFromEnv_inRealDb() throws Exception {
field.set(realSeeder, "built-in");
// Delete existing credential first so seeder can create it
- storeProvider.delete(ANONYMOUS_USER_ID, "ANTHROPIC_API_KEY");
+ storeProvider.delete("ANTHROPIC_API_KEY");
realSeeder.run(new org.springframework.boot.DefaultApplicationArguments());
// Verify credential was stored in real DB
- String value = storeProvider.get(ANONYMOUS_USER_ID, "ANTHROPIC_API_KEY");
+ String value = storeProvider.get("ANTHROPIC_API_KEY");
assertThat(value).isEqualTo("sk-test-seeded-value");
}
@Test
void seeder_skipsExistingCredential_inRealDb() throws Exception {
// Store a credential first
- storeProvider.set(ANONYMOUS_USER_ID, "ANTHROPIC_API_KEY", "original-value");
+ storeProvider.set("ANTHROPIC_API_KEY", "original-value");
// Try to seed with a different value
Function envLookup =
@@ -101,14 +100,14 @@ void seeder_skipsExistingCredential_inRealDb() throws Exception {
seeder.run(new org.springframework.boot.DefaultApplicationArguments());
// Value should still be the original
- String value = storeProvider.get(ANONYMOUS_USER_ID, "ANTHROPIC_API_KEY");
+ String value = storeProvider.get("ANTHROPIC_API_KEY");
assertThat(value).isEqualTo("original-value");
}
@Test
void seeder_ignoresBlankEnvVars_inRealDb() throws Exception {
// Delete so we can detect if seeder creates it
- storeProvider.delete(ANONYMOUS_USER_ID, "ANTHROPIC_API_KEY");
+ storeProvider.delete("ANTHROPIC_API_KEY");
Function envLookup = name -> "ANTHROPIC_API_KEY".equals(name) ? " " : null;
@@ -120,13 +119,13 @@ void seeder_ignoresBlankEnvVars_inRealDb() throws Exception {
seeder.run(new org.springframework.boot.DefaultApplicationArguments());
// Blank value should NOT be stored
- String value = storeProvider.get(ANONYMOUS_USER_ID, "ANTHROPIC_API_KEY");
+ String value = storeProvider.get("ANTHROPIC_API_KEY");
assertThat(value).isNull();
}
@Test
void seeder_skipsWhenStoreIsNotBuiltIn() throws Exception {
- storeProvider.delete(ANONYMOUS_USER_ID, "ANTHROPIC_API_KEY");
+ storeProvider.delete("ANTHROPIC_API_KEY");
Function envLookup = name -> "ANTHROPIC_API_KEY".equals(name) ? "sk-should-not-store" : null;
@@ -137,7 +136,7 @@ void seeder_skipsWhenStoreIsNotBuiltIn() throws Exception {
seeder.run(new org.springframework.boot.DefaultApplicationArguments());
- String value = storeProvider.get(ANONYMOUS_USER_ID, "ANTHROPIC_API_KEY");
+ String value = storeProvider.get("ANTHROPIC_API_KEY");
assertThat(value).isNull();
}
@@ -145,7 +144,7 @@ void seeder_skipsWhenStoreIsNotBuiltIn() throws Exception {
void seeder_reseeds_whenDecryptionFailsDueToKeyMismatch() throws Exception {
// Simulate a credential encrypted with an old/rotated master key by writing
// garbage bytes directly into the DB — decryption will throw AEADBadTagException.
- storeProvider.delete(ANONYMOUS_USER_ID, "ANTHROPIC_API_KEY");
+ storeProvider.delete("ANTHROPIC_API_KEY");
String now = java.time.Instant.now().toString();
// 12-byte fake IV + 17 bytes of garbage ciphertext → GCM tag mismatch on decrypt
byte[] staleBytes = new byte[29];
@@ -153,7 +152,15 @@ void seeder_reseeds_whenDecryptionFailsDueToKeyMismatch() throws Exception {
jdbc.update(
"INSERT INTO credentials_store (user_id, name, encrypted_value, created_at, updated_at) "
+ "VALUES (:uid, :n, :enc, :now, :now)",
- Map.of("uid", ANONYMOUS_USER_ID, "n", "ANTHROPIC_API_KEY", "enc", staleBytes, "now", now));
+ Map.of(
+ "uid",
+ "00000000-0000-0000-0000-000000000000",
+ "n",
+ "ANTHROPIC_API_KEY",
+ "enc",
+ staleBytes,
+ "now",
+ now));
Function envLookup =
name -> "ANTHROPIC_API_KEY".equals(name) ? "sk-fresh-after-rotation" : null;
@@ -168,7 +175,7 @@ void seeder_reseeds_whenDecryptionFailsDueToKeyMismatch() throws Exception {
.doesNotThrowAnyException();
// Credential must be re-encrypted with the current key and readable
- String value = storeProvider.get(ANONYMOUS_USER_ID, "ANTHROPIC_API_KEY");
+ String value = storeProvider.get("ANTHROPIC_API_KEY");
assertThat(value).isEqualTo("sk-fresh-after-rotation");
}
@@ -178,18 +185,18 @@ void seeder_propagates_nonDecryptionExceptions() throws Exception {
// should NOT silently delete a valid credential.
CredentialStoreProvider failingStore = new CredentialStoreProvider() {
@Override
- public String get(String userId, String name) {
+ public String get(String name) {
throw new IllegalStateException("DB connection lost", new RuntimeException("timeout"));
}
@Override
- public void set(String userId, String name, String value) {}
+ public void set(String name, String value) {}
@Override
- public void delete(String userId, String name) {}
+ public void delete(String name) {}
@Override
- public java.util.List list(String userId) {
+ public java.util.List list() {
return java.util.List.of();
}
};
@@ -222,16 +229,15 @@ void seeder_storesBaseUrlVars_inRealDb() throws Exception {
seeder.run(new org.springframework.boot.DefaultApplicationArguments());
- assertThat(storeProvider.get(ANONYMOUS_USER_ID, "OPENAI_BASE_URL")).isEqualTo("https://my-proxy.org/v1");
- assertThat(storeProvider.get(ANONYMOUS_USER_ID, "ANTHROPIC_BASE_URL"))
- .isEqualTo("https://anthropic-proxy.internal/v1");
+ assertThat(storeProvider.get("OPENAI_BASE_URL")).isEqualTo("https://my-proxy.org/v1");
+ assertThat(storeProvider.get("ANTHROPIC_BASE_URL")).isEqualTo("https://anthropic-proxy.internal/v1");
}
@Test
void seeder_seedsOllamaBaseUrl_inRealDb() throws Exception {
// OLLAMA_BASE_URL is the documented Ollama variable and what the
// provider resolves from the credential store — it must be seeded.
- storeProvider.delete(ANONYMOUS_USER_ID, "OLLAMA_BASE_URL");
+ storeProvider.delete("OLLAMA_BASE_URL");
Function envLookup = name -> "OLLAMA_BASE_URL".equals(name) ? "http://gpu-box:11434" : null;
@@ -242,7 +248,7 @@ void seeder_seedsOllamaBaseUrl_inRealDb() throws Exception {
seeder.run(new org.springframework.boot.DefaultApplicationArguments());
- assertThat(storeProvider.get(ANONYMOUS_USER_ID, "OLLAMA_BASE_URL")).isEqualTo("http://gpu-box:11434");
+ assertThat(storeProvider.get("OLLAMA_BASE_URL")).isEqualTo("http://gpu-box:11434");
}
@Test
@@ -260,7 +266,7 @@ void seeder_storesGitHubCredentials_inRealDb() throws Exception {
seeder.run(new org.springframework.boot.DefaultApplicationArguments());
- assertThat(storeProvider.get(ANONYMOUS_USER_ID, "GH_TOKEN")).isEqualTo("ghp-test-gh-token");
- assertThat(storeProvider.get(ANONYMOUS_USER_ID, "GITHUB_TOKEN")).isEqualTo("ghp-test-github-token");
+ assertThat(storeProvider.get("GH_TOKEN")).isEqualTo("ghp-test-gh-token");
+ assertThat(storeProvider.get("GITHUB_TOKEN")).isEqualTo("ghp-test-github-token");
}
}
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialResolutionServiceTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialResolutionServiceTest.java
index 614119f23..6dc854f4b 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialResolutionServiceTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialResolutionServiceTest.java
@@ -36,7 +36,7 @@ class CredentialResolutionServiceTest {
@Qualifier("credentialJdbc")
private NamedParameterJdbcTemplate jdbc;
- private static final String USER_ID = "resolution-test-user-001";
+ private static final String USER_ID = "00000000-0000-0000-0000-000000000000";
@BeforeEach
void setUp() {
@@ -45,7 +45,7 @@ void setUp() {
@Test
void resolve_directLookup_returnsStoredValue() {
- storeProvider.set(USER_ID, "GITHUB_TOKEN", "ghp_directlookup");
+ storeProvider.set("GITHUB_TOKEN", "ghp_directlookup");
String value = service.resolve(USER_ID, "GITHUB_TOKEN");
@@ -70,10 +70,10 @@ void resolve_notInStore_noEnvFallback() {
@Test
void resolve_afterDelete_returnsNull() {
- storeProvider.set(USER_ID, "TEMP_KEY", "temp_value");
+ storeProvider.set("TEMP_KEY", "temp_value");
assertThat(service.resolve(USER_ID, "TEMP_KEY")).isEqualTo("temp_value");
- storeProvider.delete(USER_ID, "TEMP_KEY");
+ storeProvider.delete("TEMP_KEY");
assertThat(service.resolve(USER_ID, "TEMP_KEY")).isNull();
}
@@ -82,7 +82,6 @@ void resolve_afterDelete_returnsNull() {
@Test
void resolve_dottedPath_extractsTopLevelField() {
storeProvider.set(
- USER_ID,
"GCP_SVC",
"{\"type\":\"service_account\",\"project_id\":\"my-proj-123\",\"client_email\":\"sa@x.iam\"}");
@@ -92,14 +91,14 @@ void resolve_dottedPath_extractsTopLevelField() {
@Test
void resolve_dottedPath_extractsNestedField() {
- storeProvider.set(USER_ID, "BLOB", "{\"auth\":{\"oauth\":{\"client_id\":\"abc123\"}}}");
+ storeProvider.set("BLOB", "{\"auth\":{\"oauth\":{\"client_id\":\"abc123\"}}}");
assertThat(service.resolve(USER_ID, "BLOB.auth.oauth.client_id")).isEqualTo("abc123");
}
@Test
void resolve_dottedPath_missingField_returnsNull() {
- storeProvider.set(USER_ID, "JSONY", "{\"a\":\"1\",\"b\":\"2\"}");
+ storeProvider.set("JSONY", "{\"a\":\"1\",\"b\":\"2\"}");
assertThat(service.resolve(USER_ID, "JSONY.does_not_exist")).isNull();
}
@@ -112,7 +111,7 @@ void resolve_dottedPath_baseCredentialMissing_returnsNull() {
@Test
void resolve_dottedPath_nonJsonBase_returnsNull() {
- storeProvider.set(USER_ID, "FLAT_TOKEN", "not-a-json-value-just-text");
+ storeProvider.set("FLAT_TOKEN", "not-a-json-value-just-text");
assertThat(service.resolve(USER_ID, "FLAT_TOKEN.field")).isNull();
}
@@ -121,7 +120,7 @@ void resolve_dottedPath_nonJsonBase_returnsNull() {
void resolve_dottedPath_nonStringLeaf_returnsJsonRepresentation() {
// Number/boolean/object leaves serialize to their JSON form so HTTP/MCP
// placeholders can substitute them cleanly.
- storeProvider.set(USER_ID, "CFG", "{\"port\":8080,\"enabled\":true,\"nested\":{\"a\":1}}");
+ storeProvider.set("CFG", "{\"port\":8080,\"enabled\":true,\"nested\":{\"a\":1}}");
assertThat(service.resolve(USER_ID, "CFG.port")).isEqualTo("8080");
assertThat(service.resolve(USER_ID, "CFG.enabled")).isEqualTo("true");
@@ -133,8 +132,8 @@ void resolve_dottedPath_nonStringLeaf_returnsJsonRepresentation() {
void resolve_dottedPath_doesNotFallthroughToFullName() {
// Even if a literal-dotted name happens to be stored, dotted resolution
// ALWAYS treats the first segment as the base. Documented constraint.
- storeProvider.set(USER_ID, "LITERAL.NAME", "literally_dotted_value");
- storeProvider.set(USER_ID, "LITERAL", "{\"NAME\":\"json_value\"}");
+ storeProvider.set("LITERAL.NAME", "literally_dotted_value");
+ storeProvider.set("LITERAL", "{\"NAME\":\"json_value\"}");
assertThat(service.resolve(USER_ID, "LITERAL.NAME")).isEqualTo("json_value");
}
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProviderTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProviderTest.java
index 6d163a2ac..736620f4f 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProviderTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/EncryptedDbCredentialStoreProviderTest.java
@@ -32,7 +32,7 @@ class EncryptedDbCredentialStoreProviderTest {
@Qualifier("credentialJdbc")
private NamedParameterJdbcTemplate jdbc;
- private static final String USER_ID = "store-test-user-001";
+ private static final String USER_ID = "00000000-0000-0000-0000-000000000000";
@BeforeEach
void setUp() {
@@ -41,28 +41,28 @@ void setUp() {
@Test
void set_andGet_roundTripsEncryptedValue() {
- storeProvider.set(USER_ID, "GITHUB_TOKEN", "ghp_supersecret");
- String value = storeProvider.get(USER_ID, "GITHUB_TOKEN");
+ storeProvider.set("GITHUB_TOKEN", "ghp_supersecret");
+ String value = storeProvider.get("GITHUB_TOKEN");
assertThat(value).isEqualTo("ghp_supersecret");
}
@Test
void get_returnsNull_whenNotFound() {
- assertThat(storeProvider.get(USER_ID, "DOES_NOT_EXIST")).isNull();
+ assertThat(storeProvider.get("DOES_NOT_EXIST")).isNull();
}
@Test
void delete_removesCredential() {
- storeProvider.set(USER_ID, "TO_DELETE", "value");
- storeProvider.delete(USER_ID, "TO_DELETE");
- assertThat(storeProvider.get(USER_ID, "TO_DELETE")).isNull();
+ storeProvider.set("TO_DELETE", "value");
+ storeProvider.delete("TO_DELETE");
+ assertThat(storeProvider.get("TO_DELETE")).isNull();
}
@Test
void list_returnsPartialValues_notPlaintext() {
- storeProvider.set(USER_ID, "OPENAI_KEY", "sk-abcdefghijklmnop");
+ storeProvider.set("OPENAI_KEY", "sk-abcdefghijklmnop");
- List list = storeProvider.list(USER_ID);
+ List list = storeProvider.list();
CredentialMeta meta = list.stream()
.filter(m -> m.getName().equals("OPENAI_KEY"))
@@ -78,14 +78,14 @@ void list_returnsPartialValues_notPlaintext() {
@Test
void set_updatesExistingCredential() {
- storeProvider.set(USER_ID, "MY_KEY", "original");
- storeProvider.set(USER_ID, "MY_KEY", "updated");
- assertThat(storeProvider.get(USER_ID, "MY_KEY")).isEqualTo("updated");
+ storeProvider.set("MY_KEY", "original");
+ storeProvider.set("MY_KEY", "updated");
+ assertThat(storeProvider.get("MY_KEY")).isEqualTo("updated");
}
@Test
void encryptedValueInDb_isNotPlaintext() {
- storeProvider.set(USER_ID, "SECRET", "plaintext_value");
+ storeProvider.set("SECRET", "plaintext_value");
// Read raw bytes from DB
byte[] raw = jdbc.queryForObject(
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/SchemaMigratorUpgradePathTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/SchemaMigratorUpgradePathTest.java
index 3e45c26c7..dc6034dab 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/SchemaMigratorUpgradePathTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/SchemaMigratorUpgradePathTest.java
@@ -67,15 +67,15 @@ class SchemaMigratorUpgradePathTest {
@BeforeEach
void setUp() {
- store.delete(ANON, STAGE_NAME);
- store.delete(ANON, MIGRATED_NAME);
+ store.delete(STAGE_NAME);
+ store.delete(MIGRATED_NAME);
jdbc.getJdbcOperations().execute("DROP TABLE IF EXISTS secrets_store");
}
@AfterEach
void cleanUp() {
- store.delete(ANON, STAGE_NAME);
- store.delete(ANON, MIGRATED_NAME);
+ store.delete(STAGE_NAME);
+ store.delete(MIGRATED_NAME);
jdbc.getJdbcOperations().execute("DROP TABLE IF EXISTS secrets_store");
}
@@ -83,7 +83,7 @@ void cleanUp() {
void migratedRow_isReadableViaPublicApi() throws Exception {
// 1. Encrypt a plaintext through the live store (credentials_store).
// This guarantees the bytes are in the format the running server reads.
- store.set(ANON, STAGE_NAME, PLAINTEXT);
+ store.set(STAGE_NAME, PLAINTEXT);
byte[] encryptedBytes = jdbc.queryForObject(
"SELECT encrypted_value FROM credentials_store WHERE user_id = :u AND name = :n",
Map.of("u", ANON, "n", STAGE_NAME),
@@ -112,7 +112,7 @@ void migratedRow_isReadableViaPublicApi() throws Exception {
encryptedBytes,
"t",
Instant.now().toString()));
- store.delete(ANON, STAGE_NAME); // remove the staging row
+ store.delete(STAGE_NAME); // remove the staging row
// Sanity: the migrated name is NOT yet visible via the public API.
mvc.perform(get("/api/secrets/" + MIGRATED_NAME)).andExpect(status().isNotFound());
diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/SecretController.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/SecretController.java
index 31517e4b7..724eefd69 100644
--- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/SecretController.java
+++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/SecretController.java
@@ -63,9 +63,8 @@ public class SecretController {
/** POST /api/secrets — list all secret names (Conductor's primary listing endpoint). */
@PostMapping
public ResponseEntity> listAllNames() {
- List names = storeProvider.list(currentUserId()).stream()
- .map(CredentialMeta::getName)
- .toList();
+ List names =
+ storeProvider.list().stream().map(CredentialMeta::getName).toList();
return ResponseEntity.ok(names);
}
@@ -76,9 +75,8 @@ public ResponseEntity> listAllNames() {
*/
@GetMapping
public ResponseEntity> listGrantable() {
- Set names = new LinkedHashSet<>(storeProvider.list(currentUserId()).stream()
- .map(CredentialMeta::getName)
- .toList());
+ Set names = new LinkedHashSet<>(
+ storeProvider.list().stream().map(CredentialMeta::getName).toList());
return ResponseEntity.ok(names);
}
@@ -88,7 +86,7 @@ public ResponseEntity> listGrantable() {
*/
@GetMapping("/v2")
public ResponseEntity> listWithMeta() {
- return ResponseEntity.ok(storeProvider.list(currentUserId()));
+ return ResponseEntity.ok(storeProvider.list());
}
// ── Value CRUD ────────────────────────────────────────────────────
@@ -98,7 +96,7 @@ public ResponseEntity> listWithMeta() {
public ResponseEntity getSecret(@PathVariable String key) {
ResponseEntity> err = validateKey(key);
if (err != null) return ResponseEntity.status(err.getStatusCode()).build();
- String value = storeProvider.get(currentUserId(), key);
+ String value = storeProvider.get(key);
log.info("AUDIT get-secret: userId={} key={} found={}", currentUserId(), key, value != null);
if (value == null) return ResponseEntity.notFound().build();
return ResponseEntity.ok(value);
@@ -114,7 +112,7 @@ public ResponseEntity> putSecret(@PathVariable String key, @RequestBody(requir
if (value == null || value.isEmpty()) {
return ResponseEntity.badRequest().body("value is required");
}
- storeProvider.set(currentUserId(), key, value);
+ storeProvider.set(key, value);
log.info("AUDIT put-secret: userId={} key={}", currentUserId(), key);
return ResponseEntity.ok().build();
}
@@ -124,7 +122,7 @@ public ResponseEntity> putSecret(@PathVariable String key, @RequestBody(requir
public ResponseEntity> deleteSecret(@PathVariable String key) {
ResponseEntity> err = validateKey(key);
if (err != null) return err;
- storeProvider.delete(currentUserId(), key);
+ storeProvider.delete(key);
log.info("AUDIT delete-secret: userId={} key={}", currentUserId(), key);
return ResponseEntity.ok().build();
}
@@ -134,7 +132,7 @@ public ResponseEntity> deleteSecret(@PathVariable String key) {
public ResponseEntity exists(@PathVariable String key) {
ResponseEntity> err = validateKey(key);
if (err != null) return ResponseEntity.badRequest().build();
- boolean present = storeProvider.get(currentUserId(), key) != null;
+ boolean present = storeProvider.get(key) != null;
return ResponseEntity.ok(present);
}
diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/CredentialResolutionService.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/CredentialResolutionService.java
index 7d83e526f..f58d498b2 100644
--- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/CredentialResolutionService.java
+++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/credentials/CredentialResolutionService.java
@@ -61,14 +61,14 @@ public CredentialResolutionService(CredentialStoreProvider storeProvider) {
public String resolve(String userId, String name) {
int dot = name.indexOf('.');
if (dot < 0) {
- String value = storeProvider.get(userId, name);
+ String value = storeProvider.get(name);
if (value == null) log.debug("Credential '{}' not found for user '{}'", name, userId);
return value;
}
String base = name.substring(0, dot);
String path = name.substring(dot + 1);
- String json = storeProvider.get(userId, base);
+ String json = storeProvider.get(base);
if (json == null) {
log.debug("Base credential '{}' for path '{}' not found for user '{}'", base, path, userId);
return null;
diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/spi/CredentialStoreProvider.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/spi/CredentialStoreProvider.java
index 9c2a93ef8..9d8db7fe3 100644
--- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/spi/CredentialStoreProvider.java
+++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/spi/CredentialStoreProvider.java
@@ -21,21 +21,21 @@ public interface CredentialStoreProvider {
* Retrieve the plaintext value for a credential.
* Returns null if not found.
*/
- String get(String userId, String name);
+ String get(String name);
/**
* Store or update a credential value (encrypted at rest by the implementation).
*/
- void set(String userId, String name, String value);
+ void set(String name, String value);
/**
* Delete a credential. No-op if not found.
*/
- void delete(String userId, String name);
+ void delete(String name);
/**
- * List credential metadata for a user.
+ * List credential metadata for the store.
* Returns name + partial value + timestamps. Never returns plaintext values.
*/
- List list(String userId);
+ List list();
}
From f3149fd9b8ecbd7819e6b1acec3c3c8bb1cfc1ae Mon Sep 17 00:00:00 2001
From: Viren Baraiya
Date: Fri, 10 Jul 2026 12:02:39 -0700
Subject: [PATCH 05/22] refactor(credentials): remove userId from
CredentialResolutionService
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Follows the CredentialStoreProvider single-scope change: resolve is now
resolve(name), and the userId plumbing that fed it is removed.
- CredentialResolutionService.resolve(name) — drop userId (logs no longer reference it).
- WorkerController / CredentialAwareHttpTask / CredentialAwareMcpService: call resolve(name).
HTTP/MCP keep the execution-token presence check as an auth gate, but the header
resolution methods no longer take userId (resolveHeadersForUser -> resolveHeaders on HTTP).
- AgentspanAIModelProvider: resolveUserApiKey/resolveUserCredential resolve globally; the
now-dead extractUserIdFromTaskContext + the ExecutionTokenService dependency (field +
constructor param) are removed (ExecutionTokenService itself is unchanged and still used
by WorkerController et al.).
- Tests updated to the no-userId signatures; drop dead per-user constants.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../AgentspanAIModelProviderOllamaTest.java | 9 +-
.../ai/AgentspanAIModelProviderTest.java | 16 ++--
.../controller/WorkerCredentialsTest.java | 8 +-
.../CredentialAwareHttpTaskTest.java | 14 ++--
.../CredentialAwareMcpServiceTest.java | 14 ++--
.../CredentialResolutionServiceTest.java | 30 +++----
.../runtime/ai/AgentspanAIModelProvider.java | 84 +++----------------
.../runtime/controller/WorkerController.java | 2 +-
.../credentials/CredentialAwareHttpTask.java | 8 +-
.../CredentialAwareMcpService.java | 10 ++-
.../CredentialResolutionService.java | 8 +-
11 files changed, 66 insertions(+), 137 deletions(-)
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderOllamaTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderOllamaTest.java
index 6cdbc421b..c0cbbacf1 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderOllamaTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderOllamaTest.java
@@ -24,7 +24,6 @@
import com.netflix.conductor.sdk.workflow.executor.task.TaskContext;
import dev.agentspan.runtime.credentials.CredentialResolutionService;
-import dev.agentspan.runtime.credentials.ExecutionTokenService;
import okhttp3.OkHttpClient;
@@ -39,7 +38,6 @@
*/
class AgentspanAIModelProviderOllamaTest {
- private static final String ANON_USER = "00000000-0000-0000-0000-000000000000";
private static final String REMOTE_URL = "http://gpu-box:11434";
private CredentialResolutionService credentialService;
@@ -48,11 +46,10 @@ class AgentspanAIModelProviderOllamaTest {
@BeforeEach
void setUp() {
credentialService = mock(CredentialResolutionService.class);
- ExecutionTokenService tokenService = mock(ExecutionTokenService.class);
Environment env = mock(Environment.class);
when(env.getProperty(anyString(), anyString())).thenAnswer(i -> i.getArgument(1));
- provider = new AgentspanAIModelProvider(List.of(), env, new OkHttpClient(), credentialService, tokenService);
+ provider = new AgentspanAIModelProvider(List.of(), env, new OkHttpClient(), credentialService);
}
@AfterEach
@@ -75,7 +72,7 @@ private static String baseUrlOf(AIModel model) {
@Test
void getModel_ollama_usesCredentialStoreBaseUrl() {
- when(credentialService.resolve(ANON_USER, "OLLAMA_BASE_URL")).thenReturn(REMOTE_URL);
+ when(credentialService.resolve("OLLAMA_BASE_URL")).thenReturn(REMOTE_URL);
AIModel model = provider.getModel(ollamaInput());
@@ -84,7 +81,7 @@ void getModel_ollama_usesCredentialStoreBaseUrl() {
@Test
void getModel_ollama_usesPerAgentBaseUrlFromTaskInput() {
- when(credentialService.resolve(anyString(), anyString())).thenReturn(null);
+ when(credentialService.resolve(anyString())).thenReturn(null);
Task task = new Task();
task.setStatus(Task.Status.IN_PROGRESS);
task.setInputData(Map.of("baseUrl", "http://per-agent-host:11434"));
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderTest.java
index 9de719440..0b23d360e 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/ai/AgentspanAIModelProviderTest.java
@@ -14,28 +14,23 @@
import org.springframework.core.env.Environment;
import dev.agentspan.runtime.credentials.CredentialResolutionService;
-import dev.agentspan.runtime.credentials.ExecutionTokenService;
import okhttp3.OkHttpClient;
class AgentspanAIModelProviderTest {
- private static final String ANON_USER = "00000000-0000-0000-0000-000000000000";
-
private CredentialResolutionService credentialService;
- private ExecutionTokenService tokenService;
private OkHttpClient httpClient;
private AgentspanAIModelProvider provider;
@BeforeEach
void setUp() {
credentialService = mock(CredentialResolutionService.class);
- tokenService = mock(ExecutionTokenService.class);
httpClient = new OkHttpClient();
Environment env = mock(Environment.class);
when(env.getProperty(anyString(), anyString())).thenAnswer(i -> i.getArgument(1));
- provider = new AgentspanAIModelProvider(List.of(), env, httpClient, credentialService, tokenService);
+ provider = new AgentspanAIModelProvider(List.of(), env, httpClient, credentialService);
}
@Test
@@ -46,21 +41,21 @@ void constructorAcceptsInjectedHttpClient() {
@Test
void isProviderConfigured_returnsFalse_whenNoCredential() {
- when(credentialService.resolve(ANON_USER, "OPENAI_API_KEY")).thenReturn(null);
+ when(credentialService.resolve("OPENAI_API_KEY")).thenReturn(null);
assertThat(provider.isProviderConfigured("openai")).isFalse();
}
@Test
void isProviderConfigured_returnsTrue_whenCredentialFound() {
- when(credentialService.resolve(ANON_USER, "OPENAI_API_KEY")).thenReturn("sk-test-key");
+ when(credentialService.resolve("OPENAI_API_KEY")).thenReturn("sk-test-key");
assertThat(provider.isProviderConfigured("openai")).isTrue();
}
@Test
void isProviderConfigured_caseInsensitive() {
- when(credentialService.resolve(ANON_USER, "ANTHROPIC_API_KEY")).thenReturn("key");
+ when(credentialService.resolve("ANTHROPIC_API_KEY")).thenReturn("key");
assertThat(provider.isProviderConfigured("Anthropic")).isTrue();
assertThat(provider.isProviderConfigured("ANTHROPIC")).isTrue();
@@ -75,8 +70,7 @@ void isProviderConfigured_unknownProvider_returnsFalse() {
@Test
void isProviderConfigured_credentialServiceThrows_returnsFalse() {
- when(credentialService.resolve(ANON_USER, "OPENAI_API_KEY"))
- .thenThrow(new RuntimeException("store unavailable"));
+ when(credentialService.resolve("OPENAI_API_KEY")).thenThrow(new RuntimeException("store unavailable"));
assertThat(provider.isProviderConfigured("openai")).isFalse();
}
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/WorkerCredentialsTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/WorkerCredentialsTest.java
index 5d2b1269e..a2cf329ab 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/WorkerCredentialsTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/controller/WorkerCredentialsTest.java
@@ -67,7 +67,7 @@ void tearDown() {
@SuppressWarnings("unchecked")
void resolve_validToken_returnsValues() {
String token = tokenService.mint("u-test", "wf-1", List.of("GITHUB_TOKEN"), 3600);
- when(resolutionService.resolve("u-test", "GITHUB_TOKEN")).thenReturn("ghp_secret");
+ when(resolutionService.resolve("GITHUB_TOKEN")).thenReturn("ghp_secret");
ResolveRequest req = new ResolveRequest();
req.setToken(token);
@@ -100,7 +100,7 @@ void resolve_loginToken_returns401() {
void resolve_nameNotInDeclared_isExcluded() {
// Token only declares GITHUB_TOKEN, but request asks for OPENAI_KEY too
String token = tokenService.mint("u-test", "wf-1", List.of("GITHUB_TOKEN"), 3600);
- when(resolutionService.resolve(eq("u-test"), eq("GITHUB_TOKEN"))).thenReturn("ghp_val");
+ when(resolutionService.resolve(eq("GITHUB_TOKEN"))).thenReturn("ghp_val");
ResolveRequest req = new ResolveRequest();
req.setToken(token);
@@ -117,7 +117,7 @@ void resolve_dottedPathOfDeclaredBase_isAllowed() {
// is allowed because the JSONPath access doesn't expand the blast radius —
// the tool already had the whole blob via the declared parent name.
String token = tokenService.mint("u-test", "wf-jp", List.of("GCP_SVC"), 3600);
- when(resolutionService.resolve("u-test", "GCP_SVC.project_id")).thenReturn("my-proj-123");
+ when(resolutionService.resolve("GCP_SVC.project_id")).thenReturn("my-proj-123");
ResolveRequest req = new ResolveRequest();
req.setToken(token);
@@ -157,7 +157,7 @@ void resolve_prefixCollisionGuard() {
@Test
void resolve_rateLimitExceeded_returns429() {
String token = tokenService.mint("u-test", "wf-2", List.of("KEY_A"), 3600);
- when(resolutionService.resolve(anyString(), anyString())).thenReturn("val");
+ when(resolutionService.resolve(anyString())).thenReturn("val");
ResolveRequest req = new ResolveRequest();
req.setToken(token);
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskTest.java
index c62e8d0aa..eb2828ad2 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareHttpTaskTest.java
@@ -31,8 +31,6 @@ class CredentialAwareHttpTaskTest {
@Autowired
private CredentialAwareHttpTask httpTask;
- private static final String USER_ID = "http-task-test-user";
-
@BeforeEach
void setUp() {
storeProvider.set("MY_API_KEY", "resolved-secret-value");
@@ -44,7 +42,7 @@ void resolveHeaders_substitutesPlaceholders() {
headers.put("Authorization", "Bearer #{MY_API_KEY}");
headers.put("X-Static", "no-placeholder");
- Map resolved = httpTask.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = httpTask.resolveHeaders(headers);
assertThat(resolved.get("Authorization")).isEqualTo("Bearer resolved-secret-value");
assertThat(resolved.get("X-Static")).isEqualTo("no-placeholder");
@@ -55,7 +53,7 @@ void resolveHeaders_unresolvedPlaceholder_replacedWithEmpty() {
Map headers = new LinkedHashMap<>();
headers.put("Authorization", "Bearer #{NONEXISTENT}");
- Map resolved = httpTask.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = httpTask.resolveHeaders(headers);
assertThat(resolved.get("Authorization")).isEqualTo("Bearer ");
}
@@ -65,7 +63,7 @@ void resolveHeaders_noPlaceholders_returnsUnchanged() {
Map headers = new LinkedHashMap<>();
headers.put("X-Static", "value");
- Map resolved = httpTask.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = httpTask.resolveHeaders(headers);
assertThat(resolved.get("X-Static")).isEqualTo("value");
}
@@ -77,7 +75,7 @@ void resolveHeaders_credentialValueWithDollarSign_handledSafely() {
Map headers = new LinkedHashMap<>();
headers.put("Auth", "#{TRICKY_KEY}");
- Map resolved = httpTask.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = httpTask.resolveHeaders(headers);
assertThat(resolved.get("Auth")).isEqualTo("val$with$dollars");
}
@@ -90,7 +88,7 @@ void resolveHeaders_multiplePlaceholders_inSameHeader() {
Map headers = new LinkedHashMap<>();
headers.put("Authorization", "Basic #{USER}:#{PASS}");
- Map resolved = httpTask.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = httpTask.resolveHeaders(headers);
assertThat(resolved.get("Authorization")).isEqualTo("Basic admin:secret123");
}
@@ -104,7 +102,7 @@ void resolveHeaders_dottedPathJsonExtraction() {
headers.put("X-Project", "#{GCP_SVC.project_id}");
headers.put("X-Email", "#{GCP_SVC.client_email}");
- Map resolved = httpTask.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = httpTask.resolveHeaders(headers);
assertThat(resolved.get("X-Project")).isEqualTo("my-proj-99");
assertThat(resolved.get("X-Email")).isEqualTo("sa@x.iam");
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareMcpServiceTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareMcpServiceTest.java
index 7f3915fff..f2d57c422 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareMcpServiceTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialAwareMcpServiceTest.java
@@ -32,8 +32,6 @@ class CredentialAwareMcpServiceTest {
@Autowired
private CredentialAwareMcpService mcpService;
- private static final String USER_ID = "mcp-service-test-user";
-
@BeforeEach
void setUp() {
storeProvider.set("MY_API_KEY", "resolved-secret-value");
@@ -45,7 +43,7 @@ void resolveHeaders_substitutesPlaceholders() {
headers.put("Authorization", "Bearer #{MY_API_KEY}");
headers.put("X-Static", "no-placeholder");
- Map resolved = mcpService.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = mcpService.resolveHeadersForUser(headers);
assertThat(resolved.get("Authorization")).isEqualTo("Bearer resolved-secret-value");
assertThat(resolved.get("X-Static")).isEqualTo("no-placeholder");
@@ -56,7 +54,7 @@ void resolveHeaders_unresolvedPlaceholder_replacedWithEmpty() {
Map headers = new LinkedHashMap<>();
headers.put("Authorization", "Bearer #{NONEXISTENT}");
- Map resolved = mcpService.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = mcpService.resolveHeadersForUser(headers);
assertThat(resolved.get("Authorization")).isEqualTo("Bearer ");
}
@@ -66,7 +64,7 @@ void resolveHeaders_noPlaceholders_returnsUnchanged() {
Map headers = new LinkedHashMap<>();
headers.put("X-Static", "value");
- Map resolved = mcpService.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = mcpService.resolveHeadersForUser(headers);
assertThat(resolved.get("X-Static")).isEqualTo("value");
}
@@ -78,7 +76,7 @@ void resolveHeaders_credentialValueWithDollarSign_handledSafely() {
Map headers = new LinkedHashMap<>();
headers.put("Auth", "#{TRICKY_KEY}");
- Map resolved = mcpService.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = mcpService.resolveHeadersForUser(headers);
assertThat(resolved.get("Auth")).isEqualTo("val$with$dollars");
}
@@ -91,7 +89,7 @@ void resolveHeaders_multiplePlaceholders_inSameHeader() {
Map headers = new LinkedHashMap<>();
headers.put("Authorization", "Basic #{USER}:#{PASS}");
- Map resolved = mcpService.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = mcpService.resolveHeadersForUser(headers);
assertThat(resolved.get("Authorization")).isEqualTo("Basic admin:secret123");
}
@@ -103,7 +101,7 @@ void resolveHeaders_dottedPathJsonExtraction() {
Map headers = new LinkedHashMap<>();
headers.put("X-Client-Id", "#{BLOB.auth.oauth.client_id}");
- Map resolved = mcpService.resolveHeadersForUser(headers, USER_ID);
+ Map resolved = mcpService.resolveHeadersForUser(headers);
assertThat(resolved.get("X-Client-Id")).isEqualTo("abc123");
}
diff --git a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialResolutionServiceTest.java b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialResolutionServiceTest.java
index 6dc854f4b..2dd376de7 100644
--- a/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialResolutionServiceTest.java
+++ b/server/conductor-agentspan-server/src/test/java/dev/agentspan/runtime/credentials/CredentialResolutionServiceTest.java
@@ -47,14 +47,14 @@ void setUp() {
void resolve_directLookup_returnsStoredValue() {
storeProvider.set("GITHUB_TOKEN", "ghp_directlookup");
- String value = service.resolve(USER_ID, "GITHUB_TOKEN");
+ String value = service.resolve("GITHUB_TOKEN");
assertThat(value).isEqualTo("ghp_directlookup");
}
@Test
void resolve_notInStore_returnsNull() {
- String value = service.resolve(USER_ID, "TOTALLY_MISSING_KEY_XYZ");
+ String value = service.resolve("TOTALLY_MISSING_KEY_XYZ");
assertThat(value).isNull();
}
@@ -63,7 +63,7 @@ void resolve_notInStore_returnsNull() {
void resolve_notInStore_noEnvFallback() {
// PATH exists in every process environment, but the server should NOT
// fall back to env vars — the store is the source of truth.
- String value = service.resolve(USER_ID, "PATH");
+ String value = service.resolve("PATH");
assertThat(value).isNull();
}
@@ -71,10 +71,10 @@ void resolve_notInStore_noEnvFallback() {
@Test
void resolve_afterDelete_returnsNull() {
storeProvider.set("TEMP_KEY", "temp_value");
- assertThat(service.resolve(USER_ID, "TEMP_KEY")).isEqualTo("temp_value");
+ assertThat(service.resolve("TEMP_KEY")).isEqualTo("temp_value");
storeProvider.delete("TEMP_KEY");
- assertThat(service.resolve(USER_ID, "TEMP_KEY")).isNull();
+ assertThat(service.resolve("TEMP_KEY")).isNull();
}
// ── JSONPath (Conductor-parity dotted-path extraction) ─────────────
@@ -85,35 +85,35 @@ void resolve_dottedPath_extractsTopLevelField() {
"GCP_SVC",
"{\"type\":\"service_account\",\"project_id\":\"my-proj-123\",\"client_email\":\"sa@x.iam\"}");
- assertThat(service.resolve(USER_ID, "GCP_SVC.project_id")).isEqualTo("my-proj-123");
- assertThat(service.resolve(USER_ID, "GCP_SVC.type")).isEqualTo("service_account");
+ assertThat(service.resolve("GCP_SVC.project_id")).isEqualTo("my-proj-123");
+ assertThat(service.resolve("GCP_SVC.type")).isEqualTo("service_account");
}
@Test
void resolve_dottedPath_extractsNestedField() {
storeProvider.set("BLOB", "{\"auth\":{\"oauth\":{\"client_id\":\"abc123\"}}}");
- assertThat(service.resolve(USER_ID, "BLOB.auth.oauth.client_id")).isEqualTo("abc123");
+ assertThat(service.resolve("BLOB.auth.oauth.client_id")).isEqualTo("abc123");
}
@Test
void resolve_dottedPath_missingField_returnsNull() {
storeProvider.set("JSONY", "{\"a\":\"1\",\"b\":\"2\"}");
- assertThat(service.resolve(USER_ID, "JSONY.does_not_exist")).isNull();
+ assertThat(service.resolve("JSONY.does_not_exist")).isNull();
}
@Test
void resolve_dottedPath_baseCredentialMissing_returnsNull() {
// Base credential doesn't exist at all
- assertThat(service.resolve(USER_ID, "DOES_NOT_EXIST.anything")).isNull();
+ assertThat(service.resolve("DOES_NOT_EXIST.anything")).isNull();
}
@Test
void resolve_dottedPath_nonJsonBase_returnsNull() {
storeProvider.set("FLAT_TOKEN", "not-a-json-value-just-text");
- assertThat(service.resolve(USER_ID, "FLAT_TOKEN.field")).isNull();
+ assertThat(service.resolve("FLAT_TOKEN.field")).isNull();
}
@Test
@@ -122,10 +122,10 @@ void resolve_dottedPath_nonStringLeaf_returnsJsonRepresentation() {
// placeholders can substitute them cleanly.
storeProvider.set("CFG", "{\"port\":8080,\"enabled\":true,\"nested\":{\"a\":1}}");
- assertThat(service.resolve(USER_ID, "CFG.port")).isEqualTo("8080");
- assertThat(service.resolve(USER_ID, "CFG.enabled")).isEqualTo("true");
+ assertThat(service.resolve("CFG.port")).isEqualTo("8080");
+ assertThat(service.resolve("CFG.enabled")).isEqualTo("true");
// Object leaves come back as compact JSON
- assertThat(service.resolve(USER_ID, "CFG.nested")).isEqualTo("{\"a\":1}");
+ assertThat(service.resolve("CFG.nested")).isEqualTo("{\"a\":1}");
}
@Test
@@ -135,6 +135,6 @@ void resolve_dottedPath_doesNotFallthroughToFullName() {
storeProvider.set("LITERAL.NAME", "literally_dotted_value");
storeProvider.set("LITERAL", "{\"NAME\":\"json_value\"}");
- assertThat(service.resolve(USER_ID, "LITERAL.NAME")).isEqualTo("json_value");
+ assertThat(service.resolve("LITERAL.NAME")).isEqualTo("json_value");
}
}
diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ai/AgentspanAIModelProvider.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ai/AgentspanAIModelProvider.java
index 18af1e356..4ee3d7a42 100644
--- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ai/AgentspanAIModelProvider.java
+++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/ai/AgentspanAIModelProvider.java
@@ -32,9 +32,7 @@
import com.netflix.conductor.sdk.workflow.executor.task.TaskContext;
-import dev.agentspan.runtime.context.RequestContextHolder;
import dev.agentspan.runtime.credentials.CredentialResolutionService;
-import dev.agentspan.runtime.credentials.ExecutionTokenService;
import okhttp3.OkHttpClient;
@@ -85,26 +83,19 @@ public class AgentspanAIModelProvider extends AIModelProvider {
private static final Set KEYLESS_PROVIDERS = Set.of("ollama");
private final CredentialResolutionService resolutionService;
- private final ExecutionTokenService tokenService;
/**
- * Spring constructor. The native credential beans are optional: when
- * {@code agentspan.embedded=true} they are gated off (the host delivers secrets),
- * so they resolve to {@code null} here and native per-user resolution is skipped.
+ * Spring constructor. The native credential resolution service is optional: when
+ * {@code agentspan.embedded=true} it is gated off (the host delivers secrets), so it resolves
+ * to {@code null} here and native resolution is skipped.
*/
@Autowired
public AgentspanAIModelProvider(
List> modelConfigurations,
Environment env,
OkHttpClient conductorAiHttpClient,
- ObjectProvider resolutionService,
- ObjectProvider tokenService) {
- this(
- modelConfigurations,
- env,
- conductorAiHttpClient,
- resolutionService.getIfAvailable(),
- tokenService.getIfAvailable());
+ ObjectProvider resolutionService) {
+ this(modelConfigurations, env, conductorAiHttpClient, resolutionService.getIfAvailable());
}
/** Direct constructor (used by tests, and by the Spring constructor above). */
@@ -112,12 +103,10 @@ public AgentspanAIModelProvider(
List> modelConfigurations,
Environment env,
OkHttpClient conductorAiHttpClient,
- CredentialResolutionService resolutionService,
- ExecutionTokenService tokenService) {
+ CredentialResolutionService resolutionService) {
super(modelConfigurations, env);
this.conductorAiHttpClient = conductorAiHttpClient;
this.resolutionService = resolutionService;
- this.tokenService = tokenService;
log.info(
"AgentspanAIModelProvider initialized (native per-user credential resolution {})",
resolutionService != null ? "enabled" : "disabled — embedded/host-delivered");
@@ -163,66 +152,22 @@ public AIModel getModel(LLMWorkerInput input) {
}
/**
- * Resolve a per-user API key for the given LLM provider.
+ * Resolve the stored API key for the given LLM provider from the (global) credential store.
*
- * Uses the execution token from {@code __agentspan_ctx__} in the current
- * task's input data (via {@code TaskContext}) to identify the user. This works
- * across thread boundaries — unlike RequestContextHolder which is bound to
- * the HTTP request thread.
- *
- * @return per-user API key, or null if not found
+ * @return the API key, or null if not found
*/
private String resolveUserApiKey(String provider) {
if (resolutionService == null) return null; // native resolution gated off (embedded)
String envVarName = PROVIDER_TO_ENV_VAR.get(provider.toLowerCase());
if (envVarName == null) return null;
-
- // Try TaskContext first (works in worker threads)
- String userId = extractUserIdFromTaskContext();
-
- // Fall back to RequestContextHolder (works during HTTP request, e.g. compile)
- if (userId == null) {
- userId = RequestContextHolder.get().map(ctx -> ctx.getUserId()).orElse(null);
- }
-
- // Fall back to anonymous user (OSS / no-auth mode)
- if (userId == null) {
- userId = "00000000-0000-0000-0000-000000000000";
- }
-
try {
- return resolutionService.resolve(userId, envVarName);
+ return resolutionService.resolve(envVarName);
} catch (Exception e) {
log.debug("Credential not found for provider '{}': {}", provider, e.getMessage());
return null;
}
}
- /**
- * Extract user ID from the execution token in the current task's input data.
- */
- @SuppressWarnings("unchecked")
- private String extractUserIdFromTaskContext() {
- if (tokenService == null) return null; // native token service gated off (embedded)
- try {
- TaskContext ctx = TaskContext.get();
- if (ctx == null || ctx.getTask() == null) return null;
-
- Object agentspanCtx = ctx.getTask().getInputData().get("__agentspan_ctx__");
- String token = null;
- if (agentspanCtx instanceof Map, ?> ctxMap) {
- token = (String) ctxMap.get("execution_token");
- } else if (agentspanCtx instanceof String s) {
- token = s;
- }
- if (token == null) return null;
-
- return tokenService.validate(token).userId();
- } catch (Exception e) {
- return null;
- }
- }
-
/**
* Returns true if the provider is available: either configured at startup (via environment
* variables / application.properties) or has an API key credential in the current user's store.
@@ -248,19 +193,12 @@ public String resolveConfiguredBaseUrl(String provider) {
}
/**
- * Resolve any named credential for the current user.
+ * Resolve any named credential from the (global) credential store.
*/
private String resolveUserCredential(String credentialName) {
if (resolutionService == null) return null; // native resolution gated off (embedded)
- String userId = extractUserIdFromTaskContext();
- if (userId == null) {
- userId = RequestContextHolder.get().map(ctx -> ctx.getUserId()).orElse(null);
- }
- if (userId == null) {
- userId = "00000000-0000-0000-0000-000000000000";
- }
try {
- return resolutionService.resolve(userId, credentialName);
+ return resolutionService.resolve(credentialName);
} catch (Exception e) {
return null;
}
diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/WorkerController.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/WorkerController.java
index f8813d0a3..990b11626 100644
--- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/WorkerController.java
+++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/controller/WorkerController.java
@@ -118,7 +118,7 @@ public ResponseEntity
*/
private Map stripInternalFields(Map inputData) {
if (inputData == null || inputData.isEmpty()) {
@@ -942,7 +942,6 @@ private Map stripInternalFields(Map inputData) {
}
Map clean = new HashMap<>(inputData);
clean.remove("_agent_state");
- clean.remove("__agentspan_ctx__");
clean.remove("method"); // internal dispatch method name
return clean;
}
diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java
index 6fe6d2b7a..11ed76827 100644
--- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java
+++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/AgentCompiler.java
@@ -1061,7 +1061,6 @@ WorkflowTask compileSubAgent(
inputs.put("media", mediaRef);
inputs.put("session_id", "${workflow.input.session_id}");
// Forward execution token to sub-workflows for credential resolution
- inputs.put("__agentspan_ctx__", "${workflow.input.__agentspan_ctx__}");
// Pass context to sub-workflow for pipeline state
if (contextRef != null) {
inputs.put("context", contextRef);
@@ -1167,7 +1166,6 @@ PrefillCompilationResult compilePrefillTasks(AgentConfig config) {
task.setType("SIMPLE");
Map inputs = new LinkedHashMap<>(ptc.getArguments());
- inputs.put("__agentspan_ctx__", "${workflow.input.__agentspan_ctx__}");
task.setInputParameters(inputs);
tasks.add(task);
@@ -1319,7 +1317,6 @@ WorkflowTask buildLlmTask(
Map args = pr.arguments();
if (args != null && !args.isEmpty()) {
String summary = args.entrySet().stream()
- .filter(e -> !"__agentspan_ctx__".equals(e.getKey()))
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(", "));
if (!summary.isEmpty()) {
@@ -1390,7 +1387,6 @@ WorkflowTask buildLlmTask(
}
// Forward execution token so per-user credential resolution works in worker threads
- inputs.put("__agentspan_ctx__", "${workflow.input.__agentspan_ctx__}");
llm.setInputParameters(inputs);
@@ -1910,7 +1906,6 @@ private SubgraphNodeResult buildSubgraphNodeTasks(
// Pass subgraph input from prep output + execution token
Map subInputs = new LinkedHashMap<>();
subInputs.put("state", "${" + prepRef + ".output.subgraph_input}");
- subInputs.put("__agentspan_ctx__", "${workflow.input.__agentspan_ctx__}");
subTask.setInputParameters(subInputs);
defaultTasks.add(subTask);
@@ -3478,15 +3473,13 @@ WorkflowDef compileFrameworkPassthrough(AgentConfig config) {
"prompt", "${workflow.input.prompt}",
"session_id", "${workflow.input.session_id}",
"media", "${workflow.input.media}",
- "cwd", "${workflow.input.cwd}",
- "__agentspan_ctx__", "${workflow.input.__agentspan_ctx__}")));
+ "cwd", "${workflow.input.cwd}")));
WorkflowDef wf = new WorkflowDef();
wf.setName(config.getName());
wf.setVersion(1);
List inputs = new ArrayList<>(WORKFLOW_INPUTS);
inputs.add("context");
- inputs.add("__agentspan_ctx__");
wf.setInputParameters(inputs);
wf.setTasks(List.of(fwTask));
// Output both result and context so sequential pipelines can merge
diff --git a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java
index 4ebf6ffaa..1f969bf34 100644
--- a/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java
+++ b/server/conductor-agentspan/src/main/java/dev/agentspan/runtime/compiler/MultiAgentCompiler.java
@@ -1452,7 +1452,6 @@ private WorkflowDef compileSwarmAgentWorkflowWithSubAgents(AgentConfig agent, Li
innerInputs.put("prompt", "${workflow.input.prompt}");
innerInputs.put("media", "${workflow.input.media}");
innerInputs.put("session_id", "${workflow.input.session_id}");
- innerInputs.put("__agentspan_ctx__", "${workflow.input.__agentspan_ctx__}");
innerTask.setInputParameters(innerInputs);
// 2. Coerce inner result to string (may be array/null when last turn was tool calls)
@@ -1665,7 +1664,6 @@ private WorkflowDef wrapWithGuardrails(AgentConfig config, WorkflowDef strategyW
subInputs.put("prompt", "${workflow.input.prompt}");
subInputs.put("media", "${workflow.input.media}");
subInputs.put("session_id", "${workflow.input.session_id}");
- subInputs.put("__agentspan_ctx__", "${workflow.input.__agentspan_ctx__}");
subTask.setInputParameters(subInputs);
String contentRef = ref(subRef + ".output.result");
@@ -1784,7 +1782,6 @@ private List buildSwarmCaseTasks(
subInputs.put("prompt", "${workflow.variables.conversation}");
subInputs.put("media", "${workflow.input.media}");
subInputs.put("session_id", "${workflow.input.session_id}");
- subInputs.put("__agentspan_ctx__", "${workflow.input.__agentspan_ctx__}");
subInputs.put("context", "${workflow.variables._agent_state}");
task.setInputParameters(subInputs);
caseTasks.add(task);
@@ -2251,7 +2248,6 @@ private WorkflowDef compilePlanExecute(AgentConfig config) {
// toolArgs accidentally collided on these keys, the ambient values
// win.
readerInputs.put("session_id", "${workflow.input.session_id}");
- readerInputs.put("__agentspan_ctx__", "${workflow.input.__agentspan_ctx__}");
readerInputs.put("cwd", "${workflow.input.cwd}");
readerInputs.put("credentials", "${workflow.input.credentials}");
readerInputs.put("media", "${workflow.input.media}");
@@ -2509,7 +2505,6 @@ private List buildPlanExecutionBranch(
Map execInputs = new LinkedHashMap<>();
execInputs.put("prompt", "${workflow.input.prompt}");
execInputs.put("session_id", "${workflow.input.session_id}");
- execInputs.put("__agentspan_ctx__", "${workflow.input.__agentspan_ctx__}");
execInputs.put("context", "${workflow.variables.context}");
// Forward execution-scoped inputs that compiled tools may need: working
// directory (cwd) for filesystem tools, credentials map for tools that
@@ -2687,7 +2682,6 @@ private String emitPlannerContextBuilder(List