Skip to content

Toggle native secret mechanism on agentspan.embedded; host-delivered secrets when embedded#307

Open
NicholasDCole wants to merge 10 commits into
mainfrom
feature/embedded-secret-toggle
Open

Toggle native secret mechanism on agentspan.embedded; host-delivered secrets when embedded#307
NicholasDCole wants to merge 10 commits into
mainfrom
feature/embedded-secret-toggle

Conversation

@NicholasDCole

@NicholasDCole NicholasDCole commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

AgentSpan keeps its full native credential mechanism and gates it behind the
agentspan.embedded feature flag instead of deleting it (an earlier abandoned branch had ripped it
out). One flag decides who delivers secrets:

Deployment agentspan.embedded Secret delivery
Standalone agentspan server false (default) Native — encrypted store, execution-token minting, POST /api/workers/secrets pull, SDK fetchers. Unchanged from main.
Embedded in orkes-conductor / conductor-oss true Native dormant (all beans gated off); the host delivers secrets.

Design doc (full detail + sequence diagrams): design/2026-07-09-embedded-secret-toggle-design.md.

How secrets are delivered when embedded (split by task type)

  • LLM provider keys → the host AI integration (not a workflow secret). LLM_CHAT_COMPLETE
    resolves its model and API key from the configured AI integration by provider name
    (OrkesAIModelProvidergetIntegrationapi_key); agentspan stamps nothing. Verified in
    orkes-conductor. (We deliberately do not map a provider to a conventionally-named workflow
    secret — that would duplicate and can conflict with the integration.)
  • HTTP / MCP / planner-context headers → ${workflow.secrets.NAME} (the user's external-API
    secrets). The compiler rewrites ${NAME}${workflow.secrets.NAME} when embedded; the host
    substitutes in memory before the in-process call.
  • Worker tools (SIMPLE, polled by the SDK) → the worker's secrets, host-resolved at poll. The
    target is TaskDef.runtimeMetadata (declare names → host injects values onto the wire-only
    Task.runtimeMetadata). Until the conductor client libraries expose that field, this PR ships
    an interim, client-compatible path: stamp
    inputParameters.__resolved_credentials__ = { NAME: "${workflow.secrets.NAME}" }, which the host
    resolves via PR #1255's substituteSecrets — it rides in the task-input Map every client already
    keeps. Same runtime cost; the target is strictly better on persistence-safety (the migration + the
    per-client Task.runtimeMetadata change are documented in the design doc).

Server changes

  • Gating. @ConditionalOnProperty(name="agentspan.embedded", havingValue="false", matchIfMissing=true)
    on all native secret beans (WorkerController, CredentialResolutionService, ExecutionTokenService,
    CredentialAwareMcpService, CredentialMaskingResponseAdvice, EncryptedDbCredentialStoreProvider,
    MasterKeyConfig, CredentialEnvSeeder, CredentialSchemaMigrator, CredentialDataSourceConfig,
    NoOpSecretOutputMasker; joins the already-gated SecretController/CredentialAwareHttpTaskConfig).
    Active consumers made tolerant: AgentspanAIModelProvider via ObjectProvider + guards;
    AgentService/AgentEventListener @Autowired(required=false) + null guards (token minting skipped).
  • LLM keys — no agentspan code. Resolved by the host AI integration. (The earlier
    injectCredentialReferences + LlmProviderEnv apiKey-stamping was redundant and bypassed by
    OrkesAIModelProvider, so it was removed.)
  • Worker tools (interim). ToolCompiler.buildWorkerCredConfig + JavaScriptBuilder enrich-script
    injection + AgentCompiler.collectToolCredentials stamp __resolved_credentials__ onto SIMPLE tasks.

SDK read-path (all four) — auto-detect

Each worker prefers the host-delivered map from task input, else falls back to the native token-pull
(standalone); resolved values feed the existing accessor/injection machinery unchanged; the key is
stripped. Native fetcher code is kept. Interim reads inputData.__resolved_credentials__; the target
reads task.runtimeMetadata.

  • TypeScriptworker.ts / credentials.ts · JavaWorkerManager.readResolvedCredentials
    · Python_dispatch.py · C#WorkerManager.ReadResolvedCredentials.

Dependency

AgentSpan references no PR #1255 API, so it compiles/tests against the published
conductor 3.32.0-rc.3. ${workflow.secrets.NAME} is resolved at runtime by the embedded host
(which must include PR #1255). No SDK client-library rebuilds for the interim.

Tests (fail-first validated)

  • NativeSecretGatingTest — native beans present standalone, absent embedded.
  • ToolCompilerWorkerCredTest — GraalJS-runs the enrich script; SIMPLE task carries
    __resolved_credentials__ when embedded, nothing standalone.
  • ReadResolvedCredentialsTest (Java), test_resolved_credentials.py (Python), TS credentials.test.ts.
  • CI green: server-tests, build-server, all four SDK unit + e2e suites.

Not covered here

  • Target TaskDef.runtimeMetadata is documented but not implemented — blocked on the conductor
    client libraries exposing the field (see the design doc's per-client table + migration).

🤖 Generated with Claude Code

Test Evidence:

Orkes Conductor:

Manual Trigger

LLM Calls working (System Task OPEN AI API KEY Resolved from Integrations)

Task call working (worker receiving demo secret input in plain text)
image

Webhook Trigger

LLM Calls working (System Task OPEN AI API KEY Resolved from Integrations)

Task call working (worker receiving demo secret input in plain text)
image

NicholasDCole and others added 8 commits July 9, 2026 16:28
…ed; host-delivered secrets when embedded

Keep AgentSpan's full native credential mechanism and gate it on the
`agentspan.embedded` feature flag instead of deleting it:

- Standalone (embedded=false): native mechanism active, unchanged (encrypted
  store, execution-token minting, /api/workers/secrets pull, SDK fetchers).
- Embedded (embedded=true): native beans dormant; the host resolves secrets via
  `${workflow.secrets.NAME}` (conductor-oss PR #1255 substituteSecrets/SecretsDAO).

Server:
- Part A: @ConditionalOnProperty(agentspan.embedded=false, matchIfMissing=true)
  on all native secret beans (WorkerController, CredentialResolutionService,
  ExecutionTokenService, CredentialAwareMcpService, CredentialMaskingResponseAdvice,
  store/masterkey/seeder/migrator/datasource, NoOpSecretOutputMasker). Widely-injected
  consumers made tolerant (AgentspanAIModelProvider via ObjectProvider + guards).
- Part B: worker tools stamp inputParameters.__resolved_credentials__ =
  {NAME: "${workflow.secrets.NAME}"} (embedded only, via the enrich script);
  LLM apiKey stamped ${workflow.secrets.<PROVIDER_KEY>} (LlmProviderEnv).

SDKs: each worker prefers host-delivered __resolved_credentials__ from task input,
falling back to the native token-pull (standalone). No conductor client-library
rebuilds — the map rides in the preserved inputData.

Tests (fail-first validated): NativeSecretGatingTest, ToolCompilerWorkerCredTest,
ReadResolvedCredentialsTest (Java), test_resolved_credentials.py, TS credentials tests.
Conductor pinned to a local runtimemeta build (PR #1255) until it ships.

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

AgentSpan references no PR #1255 API (it only emits ${workflow.secrets.NAME}
strings; the embedded host resolves them at runtime via substituteSecrets/SecretsDAO),
so it builds against the published conductor. The local 3.32.0-rc.3-runtimemeta-LOCAL
build added a conductor-side SecretResource that shadowed agentspan's SecretController
GET /api/secrets, breaking CredentialControllerTest in standalone. Reverting the pin
fixes it; the full server suite is green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the server-tests CI failure (:spotlessJavaCheck) — reformat ToolCompiler
and the two new test classes to the project's spotless style. No logic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r workers, ${workflow.secrets} for system tasks

State the intended split explicitly: worker tools use TaskDef.runtimeMetadata
(host resolves onto wire-only Task.runtimeMetadata at poll); system tasks (LLM
apiKey, HTTP/MCP/planner headers) use ${workflow.secrets.NAME}. Add a per-SDK
table specifying exactly what the `Task` model change is in each conductor client
library (java-sdk, python-sdk, csharp-sdk, orkes TS SDK) required to adopt the
target, plus a migration checklist. Document the shipped __resolved_credentials__
path as the interim, client-compatible worker delivery until those clients ship
the runtimeMetadata field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gram + secret-resolution demo

- Rewrite the design doc to be concise: split-by-task-type delivery, interim
  enrichment sequence diagram + caveat, target-vs-interim (safety, not perf),
  per-SDK Task.runtimeMetadata change table, and a single status table.
- Add sdk/python/examples/demo_secret_resolution.py — a minimal 1-tool + LLM
  agent that reads DEMO_SECRET via get_secret(); doubles as a CI smoke check
  (exit 1 unless the secret resolved and the workflow completed). Validated
  end-to-end on local Orkes via SDK run and webhook trigger.

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

Expand the migration section into a concrete file-by-file breakdown of the
target change (AgentService.setRuntimeMetadata + removal of the enrich stamping;
per-SDK read swap to task.runtimeMetadata; client dep bumps). Add a labeled
target-state sequence diagram alongside the interim one so the two delivery paths
can be compared directly.

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

Embedded LLM auth is the host's job: OrkesAIModelProvider (@primary) resolves the
model per call from the AI integration by (orgId, integrationName) and never reads
an apiKey from task input — so agentspan's ${workflow.secrets.<PROVIDER_KEY>} stamping
was redundant and bypassed, and the hard-coded provider->env-var map was a smell.

- Delete LlmProviderEnv and AgentChatCompleteTaskMapper.injectCredentialReferences.
- Remove the dead embedded readResolvedApiKeyFromTaskInput branch in AgentspanAIModelProvider.
- Worker-tool + HTTP/MCP header secret delivery (${workflow.secrets}) unchanged; standalone
  per-user LLM key resolution unchanged.
- Doc: correct the LLM sequence diagram to the real OrkesAIModelProvider/integration flow
  (verified in orkes-conductor) and update the delivery/implementation/migration sections.

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

Fix the two spots that still lumped LLM keys into the ${workflow.secrets} system-task
path: system tasks are LLM (host AI integration) + HTTP/MCP/planner headers (${workflow.secrets}).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants