Skip to content

feat(sdk): read host-delivered worker secrets from Task.runtimeMetadata (all 4 SDKs)#313

Open
v1r3n wants to merge 24 commits into
mainfrom
feat/embedded-secrets-sdk-taskdef
Open

feat(sdk): read host-delivered worker secrets from Task.runtimeMetadata (all 4 SDKs)#313
v1r3n wants to merge 24 commits into
mainfrom
feat/embedded-secrets-sdk-taskdef

Conversation

@v1r3n

@v1r3n v1r3n commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

SDK-side of host-delivered worker secrets. Split out from #307 (SDK half). Server half: companion PR.

Each worker prefers the host-resolved secret map on Task.runtimeMetadata (embedded mode), else falls back to the native execution-token pull (standalone). Resolved values feed the existing get_secret() / getCredential accessors unchanged.

  • JavaWorkerManager.readRuntimeMetadata; worker TaskDef registration is create-only so the host-declared runtimeMetadata isn't clobbered.
  • Python_dispatch.py reads task.runtime_metadata; tool_registry/runtime register create-only.
  • TypeScriptworker.ts reads task.runtimeMetadata (structural cast).
  • C#WorkerManager.ReadRuntimeMetadata.
  • Client dependency annotations/bumps + demo_secret_resolution.py.

Tests

ReadRuntimeMetadataTest / EmbeddedTaskDefRegistrationTest (Java), test_runtime_metadata / test_embedded_taskdef_registration (Python), RuntimeMetadataReadTests (C#), worker/credentials tests (TS).

Known follow-up

The conductor-python client model does not yet carry Task.runtime_metadata, so the wire field is dropped on deserialize and the Python read-path is inert until the client ships the field (a compat shim is the interim fix). Java/TS/C# read via model additions / structural casts.

🤖 Split from #307 with Claude Code

v1r3n and others added 13 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>
…ta (all 4 SDKs)

SDK-side of host-delivered worker secrets. Each worker prefers the host-resolved
map on Task.runtimeMetadata (embedded), else falls back to the native
execution-token pull (standalone); the resolved values feed the existing
get_secret()/getCredential accessors.

- Java   — WorkerManager.readRuntimeMetadata; create-only worker TaskDef registration
           so host-declared runtimeMetadata isn't clobbered.
- Python  — _dispatch.py reads task.runtime_metadata; tool_registry/runtime register
           create-only.
- TypeScript — worker.ts reads task.runtimeMetadata (structural cast).
- C#      — WorkerManager.ReadRuntimeMetadata.
- Client dep bumps + demo_secret_resolution.py.

Tests: ReadRuntimeMetadataTest / EmbeddedTaskDefRegistrationTest (Java),
test_runtime_metadata / test_embedded_taskdef_registration (Python),
RuntimeMetadataReadTests (C#), worker/credentials tests (TS).

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>
…anch

The endpoint-free SDK read-path requires the new server (TaskDef.runtimeMetadata
declaration in both modes + AgentspanSecretsDAO + wire-only resolution; the native
/api/workers/secrets pull is deleted). Stacking on the server branch makes this
PR's e2e run against that server.
@v1r3n v1r3n changed the base branch from main to feat/embedded-secrets-server-taskdef July 11, 2026 06:24
v1r3n and others added 7 commits July 10, 2026 23:35
The server resolved-secret delivery is now exclusively the wire-only
Task.runtimeMetadata (the native POST /api/workers/secrets pull and the
execution token were removed server-side). The SDK no longer calls any
endpoint for secrets.

- _task_compat.py shim: registers runtime_metadata on conductor-python's Task
  model (swagger_types/attribute_map/ctor/property) — published client releases
  drop the unregistered JSON key at deserialization. Remove once the client
  ships the field.
- _dispatch.py: _read_runtime_metadata() is the only source; deleted
  WorkerCredentialFetcher wiring + _extract_execution_token; declared-but-
  undelivered names log a warning and surface via get_secret().
- Framework adapters (langchain/langgraph/claude_agent_sdk): same swap.
- resolve_credentials() public helper: reads the task's runtime metadata
  instead of calling the removed endpoint.
- Deleted credentials/fetcher.py + test_fetcher.py; reworked delivery tests to
  runtime_metadata; new shim regression test proves the wire value survives
  ApiClient.deserialize.

1689 unit tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wire-only Task.runtimeMetadata (resolved by the conductor core at poll) is
the only credential-delivery path — the native POST /workers/secrets pull and
the execution token were removed server-side.

- credentials.ts: context reduced to the resolved map; getCredential() reads
  it directly (missing name -> CredentialNotFoundError); deleted
  extractExecutionToken + resolveCredentials (endpoint pull).
  runWithCredentialContext/setCredentialContext now take the resolved map.
- worker.ts: reads task.runtimeMetadata (structural cast — JS keeps unknown
  JSON keys, so no client-lib change needed); declared-but-undelivered names
  log a warning; the context wraps every execution.
- Tests reworked around runtimeMetadata delivery; the concurrency-isolation
  regression test drops its handler barrier (delivered secrets serialize
  handler bodies via the env-injection mutex by design).

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

The wire-only Task.runtimeMetadata (resolved by the conductor core at poll) is
the only credential-delivery path — the native POST /workers/secrets pull and
the execution token were removed server-side.

- WorkerManager.readRuntimeMetadata reads the field REFLECTIVELY: the published
  conductor-client (5.1.0, now pinned from Maven Central — mavenLocal dropped)
  does not carry Task.runtimeMetadata yet, so the read compiles today, returns
  empty against the current client, and lights up automatically once
  conductor-oss/java-sdk ships the accessor. (Older clients also drop the
  unregistered JSON key at deserialization, so no data is lost by degrading.)
- Deleted WorkerCredentialFetcher + extractExecutionToken + the terminal-error
  fallback; declared-but-undelivered names log a warning and surface via
  ToolContext.getCredential.
- ReadRuntimeMetadataTest reworked around a Task subclass exposing the accessor
  + a published-client degradation case.

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>
Base automatically changed from feat/embedded-secrets-server-taskdef to main July 11, 2026 23:31
v1r3n added 4 commits July 11, 2026 20:12
Fix docs build breakage from the submodule swap: the new repo nests the
agent docs under docs/agents/ instead of docs/, so retarget the
docs/python-sdk symlink there to keep mkdocs --strict passing. Also fetch
submodules in the docs CI/deploy checkouts, since the docs symlink now
resolves into submodule content.
Plain git clone now leaves sdk/python empty since it moved to a submodule.
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