Skip to content

feat(server): embedded secret delivery via TaskDef.runtimeMetadata + AgentspanSecretsDAO#312

Merged
v1r3n merged 12 commits into
mainfrom
feat/embedded-secrets-server-taskdef
Jul 11, 2026
Merged

feat(server): embedded secret delivery via TaskDef.runtimeMetadata + AgentspanSecretsDAO#312
v1r3n merged 12 commits into
mainfrom
feat/embedded-secrets-server-taskdef

Conversation

@v1r3n

@v1r3n v1r3n commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Server-side of host-delivered worker secrets when AgentSpan is embedded (agentspan.embedded=true). Split out from #307 (server half). SDK half: companion PR.

  • Declare secret names on TaskDef.runtimeMetadata at registration. AgentService stamps each worker tool's credential names (via AgentCompiler.collectToolCredentials), extended to the non-worker user-code task kinds — guardrails, callbacks, stop_when, gates, instructions, routers, graph node/edge — using agent-level creds (collectAgentCredentials). Declarative/control-flow tasks (termination, transfer/handoff) stay empty. Gated on EmbeddedMode.isEmbedded().
  • AgentspanSecretsDAO bridges conductor's global SecretsDAO to AgentSpan's encrypted CredentialStoreProvider, scoped to the anonymous/system user. Selected by conductor.secrets.type=agentspan; the embedded conductor's RuntimeMetadataResolver resolves the declared names at each SIMPLE task's poll and injects values onto the wire-only Task.runtimeMetadata.
  • Store beans (datasource, master key, schema migrator, store provider) gate on conductor.secrets.type=agentspan (defaulted on in application.properties).
  • Native secret mechanism gated on agentspan.embedded; LLM keys via the host AI integration (no apiKey stamping). Conductor bumped to 3.32.0-rc.5 (carries TaskDef/Task.runtimeMetadata).

Tests

WorkerRuntimeMetadataTest, AgentspanSecretsDAOTest, NativeSecretGatingTest. Full :conductor-agentspan-server:test green (683/0).

Validated live

Embedded host + conductor.secrets.type=agentspan: a secret stored in the encrypted store resolves onto the polled Task.runtimeMetadata.

Note: the design doc still describes the retired interim __resolved_credentials__ path — doc cleanup tracked separately.

🤖 Split from #307 with Claude Code

v1r3n and others added 12 commits July 10, 2026 10:54
…AgentspanSecretsDAO

Server-side of host-delivered worker secrets when embedded (agentspan.embedded=true):

- Declare each worker tool's credential names on TaskDef.runtimeMetadata at
  registration (AgentService + AgentCompiler.collectToolCredentials/collectAgentCredentials),
  extended to the non-worker user-code task kinds (guardrail/callback/stop_when/
  gate/instructions/router/graph) via agent-level creds. Gated on EmbeddedMode.
- AgentspanSecretsDAO: bridges conductor's SecretsDAO to AgentSpan's encrypted
  CredentialStoreProvider (scoped to the anonymous/system user), selected by
  conductor.secrets.type=agentspan; the embedded conductor's RuntimeMetadataResolver
  resolves declared names at task poll onto the wire-only Task.runtimeMetadata.
- Store beans (datasource, master key, schema migrator, store provider) re-gated on
  conductor.secrets.type=agentspan (defaulted on in application.properties).
- Native secret mechanism gated on agentspan.embedded; LLM keys via host AI integration.

Tests: WorkerRuntimeMetadataTest, AgentspanSecretsDAOTest, NativeSecretGatingTest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…module

It is a standalone-server-only @ControllerAdvice (its only consumers are the
server-module masking tests). Co-locating it in conductor-agentspan-server
removes it from the embeddable library, where it never applied.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lobal) store

Remove userId from the CredentialStoreProvider SPI (get/set/delete/list). The
EncryptedDbCredentialStoreProvider keeps its credentials_store schema (the user_id
column stays, part of the (user_id, name) PK) but always reads/writes under the
fixed 00000000-0000-0000-0000-000000000000 owner, so lookups are effectively by name.

- SPI: drop userId from all four methods.
- EncryptedDbCredentialStoreProvider: DEFAULT_USER_ID constant in the SQL.
- Consumers drop the userId argument: CredentialResolutionService, SecretController
  (currentUserId() retained for audit logging), CredentialEnvSeeder, AgentspanSecretsDAO.
- Per-user isolation is gone; delete the obsolete isolation tests
  (WorkerCredentialsIntegrationTest, AgentspanAIModelProviderPerUserTest) and update the
  remaining tests to the no-userId signatures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
The execution-token mechanism gated the native out-of-process worker credential
pull, but provides no real auth/tenancy in agentspan today (the credential store is
global, userId already removed). Remove it; real security will be added separately.
Credential delivery relies on host-resolved TaskDef.runtimeMetadata.

DELETE:
- ExecutionTokenService, WorkerController (/api/workers/secrets), ResolveRequest
- Token-only tests: ExecutionTokenServiceTest, AgentEventListenerTokenRevocationTest,
  WorkerCredentialsTest

MODIFY:
- AgentService: drop the mint block + extractDeclaredCredentials/collectCredentialsRecursive;
  keep preallocatedExecutionId/setWorkflowId (host time-based IDs). The Lombok
  @requiredargsconstructor now serves tests too.
- AgentEventListener: drop the token-revocation path.
- AgentExceptionHandler: drop the 401 token handler.
- Remove __agentspan_ctx__ end-to-end (only carried the token): AgentService writer,
  the forwarders in AgentCompiler/MultiAgentCompiler/ToolCompiler/JavaScriptBuilder/
  PlanAndCompileTask, and the strips in AgentChatCompleteTaskMapper/PlanAndCompileTask.
- Update NativeSecretGatingTest, AgentServiceTokenTest, MultiAgentCompilerTest,
  PlanAndCompileTaskTest.

Survives: CredentialResolutionService (+ CredentialNotFoundException), CredentialAware
Http/Mcp (resolve directly), credentialMasterKey/MasterKeyConfig.

Consequence (server-only now): no worker credential-pull path remains; delivery is via
TaskDef.runtimeMetadata. Coordinated follow-up in SDK PR #313 must remove the native
token-pull fetchers and rely on the runtimeMetadata read-path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….secrets.NAME}

Rewrite credential placeholders in HTTP/MCP/planner-context headers from ${NAME} to
conductor's native ${workflow.secrets.NAME}, resolved by ParametersUtils.substituteSecrets
wire-only at task hand-off (poll / async+sync system tasks) against the configured
SecretsDAO — AgentspanSecretsDAO by default. Plaintext is never persisted.

- ToolCompiler.rewriteCredentialPlaceholders + MultiAgentCompiler planner-context: always
  emit ${workflow.secrets.NAME} (drop the embedded/#{ split; remove EmbeddedMode usage).
- Delete the now-redundant #{NAME} resolvers: CredentialAwareHttpTask +
  CredentialAwareHttpTaskConfig (reverts to conductor's built-in @component("HTTP") HttpTask)
  and CredentialAwareMcpService (reverts to conductor-ai's base @component MCPService). Their
  tests are removed.
- CredentialResolutionService stays (still used by AgentspanAIModelProvider for LLM keys).
- Bonus: planner-context secrets now resolve in standalone too (previously only embedded),
  since AgentspanSecretsDAO is the default SecretsDAO.

Tests updated: ToolCompilerTest, MultiAgentCompilerTest, EnrichToolsScriptTest assert the
${workflow.secrets.NAME} form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (plaintext leak)

The ${workflow.secrets.NAME} rewrite baked the reference into the enrich INLINE task's
'expression' input; conductor's substituteSecrets resolves any such reference in an INLINE
task's input to PLAINTEXT at that task's hand-off, and the plaintext then persisted via the
script's dynamicTasks output into the forked HTTP/MCP tasks' inputParameters — defeating the
wire-only guarantee. A secret containing a quote/backslash also broke the GraalJS script.

Two-form design:
- Transport form #{NAME} (dots allowed for JSONPath): inert to BOTH ParametersUtils passes,
  rides safely through every INLINE hop (script text, $.mcpConfig/$.apiConfig data, toolSpecs).
  ToolCompiler.rewriteCredentialPlaceholders now emits this everywhere headers are escaped.
- Real reference ${workflow.secrets.NAME}: applied ONLY at final placement into a real task's
  inputParameters, where conductor defers it at binding and resolves wire-only at that task's
  own hand-off:
  * Java secretRefHeaders() for statically built tasks (LIST_MCP_TOOLS inputs, api spec-fetch
    HTTP tasks — both variants).
  * JS _sec() at dynamic-task emission in both enrich scripts (HTTP, MCP, API headers). The
    '$' + '{workflow.secrets.' split keeps the contiguous reference literal out of the script
    SOURCE so it can never be early-resolved.

Also widens both CREDENTIAL_PLACEHOLDER patterns to allow dotted names (${GCP_SVC.project_id}
-> ${workflow.secrets.GCP_SVC.project_id}, supported by conductor's resolveWithOptionalJsonPath),
fixing the dotted-JSONPath regression.

Tests (fail-first validated): leak tripwires assert the enrich script source never contains
'${workflow.secrets.' (ToolCompilerTest, EnrichToolsScriptTest); GraalJS emission tests assert
#{NAME} configs emit ${workflow.secrets.NAME} on dynamic HTTP/MCP task inputs, incl. dotted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…re module

They test ToolCompiler and JavaScriptBuilder, which live in conductor-agentspan —
tests belong with the module of the class under test. The core module's test
classpath already carries GraalJS for the script-execution harness. No content
changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…odule

Tests belong with the module of the class under test. Moves the 29 pure unit
tests that exercise conductor-agentspan classes (compilers, normalizers, models,
services, utils, ai providers, ProviderController) plus SkillNormalizerTest's
/skills JSON fixtures (used by it alone).

Stays in conductor-agentspan-server: the @SpringBootTest/MockMvc integration
tests, everything exercising server-module classes (encrypted store, seeder,
migrator, master key, AgentspanSecretsDAO, CredentialMaskingResponseAdvice,
skill package store), and ServerSmokeTest.

Pure git mv — no content changes. Suites: core 523, server 159, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ci-server.yml (Server CI): agentspan library + server module tests and the
  Go CLI tests. Triggers on server/**, cli/**.
- ci-sdk.yml (SDK CI): the four SDK unit-test jobs plus all e2e suites — e2e
  is part of the SDK pipeline. Builds its own server JAR (-x test; server
  correctness is Server CI's job) so the workflows share no artifacts.
  Triggers on sdk/**, server/**, cli/** — server changes still prove the SDKs
  keep working via e2e.

Job bodies are unchanged from the old ci.yml; this is a reorganization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
runtimeMetadata is now the only worker credential-delivery path (the native
execution-token pull was removed). Standalone resolves it end-to-end already:
the built-in conductor core's RuntimeMetadataResolver + AgentspanSecretsDAO
(the default SecretsDAO) resolve declared names at poll from the encrypted
store. Gating the declaration on EmbeddedMode left standalone with no
credential delivery at all (review finding #3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drops ci-sdk.yml (SDK unit tests + all e2e suites). Server CI (ci-server.yml)
remains as the only workflow, covering the agentspan library/server modules and
the Go CLI. SDK test coverage moves out of per-PR CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@v1r3n v1r3n merged commit 7cdecc2 into main Jul 11, 2026
3 checks passed
@v1r3n v1r3n deleted the feat/embedded-secrets-server-taskdef branch July 11, 2026 23:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant