Skip to content

feat(secrets): re-land ADR-0031 Phase 3 stack onto main — environments registry + per-context LLM keys + credentials registry - #87

Merged
gustavobertoi merged 8 commits into
mainfrom
feat/credentials-registry
Jun 3, 2026
Merged

feat(secrets): re-land ADR-0031 Phase 3 stack onto main — environments registry + per-context LLM keys + credentials registry#87
gustavobertoi merged 8 commits into
mainfrom
feat/credentials-registry

Conversation

@gustavobertoi

Copy link
Copy Markdown
Collaborator

Why this PR

Only #83 (per-execution environment threading) actually landed on main. PRs #84
(environments registry + strict validation), #85 (per-context LLM provider keys), and #86
(credentials registry) were each merged into their stacked feature-branch base rather than
main, so their code never reached main. This PR re-lands all three together onto main from
the tip of the stack. Nothing in here is new code — it's the already-reviewed content of
#84 + #85 + #86, merged.

Contents (all ADR-0031)

Migrations 000010/000011 follow main's 000009 sequentially.

Verification

make lint 0 issues · make build ok · make test 697 pass · make swagger clean (verified
on the branch tip before this PR).

Follow-ups (not in this PR)

  • Credentials PR 2 — consumption wiring (source:"credential", {{credential:id.field}},
    LLM_<PROVIDER>_CREDENTIAL).
  • Deliverable 4 — Infisical read-only SecretStore backend.

🤖 Generated with Claude Code

gustavobertoi and others added 8 commits June 3, 2026 16:16
…n (ADR-0031 Phase 3)

Make "environment" a per-execution scoping dimension instead of a single
server-wide config string baked into the secret resolver at DI time.

- workflow.New gains an environment field + getter; the WorkflowHandler builds
  a per-workflow secrets.Resolver scoped to that environment. New executions
  take the environment from the trigger (default FUSE_ENVIRONMENT); replay reads
  it from the reconstructed workflow so resolution stays deterministic across
  restart/recovery/retry.
- Thread environment trigger -> message -> workflow_sup spawn -> instance
  supervisor -> WorkflowHandlerInitArgs. Sub-workflows inherit the parent's
  environment. TriggerWorkflowRequest/Response gain an optional environment
  field; CLI `fuse workflow` gains -e/--environment.
- Persist environment on the workflows row (migration 000009, NOT NULL DEFAULT
  'default' backfill) and read it back in Get; excluded from the upsert UPDATE
  clause so state-change saves never clobber it.
- Drop the now-orphaned process-wide secret resolver DI provider.

Tests: unit coverage for New/Environment(); functional contract subtest asserts
Save/Get preserves environment (memory + postgres). Strict environments registry
and trigger validation follow in a separate PR.

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

Provider API keys / base-URLs may now be {{secret:NAME}} references resolved
from the SecretStore against the running workflow's environment, so different
workflows in one process can use different provider credentials.

- pkg/llm: Registry is now factory-based — Get/Default take (ctx, environment);
  NewRegistry(map[string]ProviderFactory); NewStaticRegistry shim for the
  static fast path and tests.
- di/llm: provideLLMRegistry builds per-provider factories. Fully-static config
  is built once (singleton fast path, unchanged behavior); a config with
  {{secret:}} refs resolves via SecretStore + ReplaceSecretRefs per call and
  builds a fresh provider. Resolved keys are Reveal()'d only into the SDK
  client, never logged.
- Thread the workflow environment to functions: ExecutionInfo.Environment
  (carried on ExecuteFunctionMessage, set from a.workflow.Environment() at the
  3 handler dispatch sites). ai/chat & ai/agent resolve the provider inside
  their async goroutine (off the pool worker) using execInfo.Environment;
  agent resolves once and reuses it across the loop. Sub/tool ExecutionInfo
  inherits the environment.

Backward compatible: all-literal config with the memory store behaves exactly
as before. Tests: registry factory dispatch, chat env-routing, di per-env
resolution + singleton fast path. ADR-0031 + .env.example updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…idation (ADR-0031 Phase 3) (#84)

Make environments declarable and validated, completing the explicit
environments model on top of per-execution scoping.

- Environment domain (pkg/workflow) with name-format validation and the
  DefaultEnvironmentName constant.
- EnvironmentRepository (memory seeds 'default'; postgres + migration 000010
  seeding 'default'), EnvironmentService with IsValid, DTOs, and CRUD HTTP API:
  GET /v1/environments, GET/PUT/DELETE /v1/environments/{name} (the default
  environment cannot be deleted).
- Trigger validation: the trigger handler rejects an unknown environment with
  HTTP 400 (default is always valid). DI wires the repo/service/handlers.

Tests: domain Validate, memory repo, service (IsValid + CRUD), and a
memory+postgres functional contract test asserting the seeded default.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… values (ADR-0031 Phase 2)

Introduce typed, centrally-managed credential objects (ADR-0031 Option B),
referenced by id. A credential is metadata (id, free-form type, description,
field names); its field VALUES live in the existing SecretStore under the
reserved name cred/<id>/<field>, per environment — one custody point, reusing
Phase-1 encryption + scoping.

This PR is the registry/management layer only (no engine wiring yet):
- pkg/workflow/credential.go: Credential domain + validation (lowercase id,
  mixed-case dot-free field names).
- pkg/secrets/credential.go: CredentialSecretName + {{credential:id.field}}
  token helpers. The "/" separator is outside the {{secret:NAME}} charset, so
  credential values can never collide with secret references (tested).
- CredentialRepository (memory + postgres TEXT[] fields) + migration 000011.
- CredentialService: values written to the SecretStore via ManagedSecretStore
  per environment (read-only backends rejected with ErrReadOnlySecretStore);
  Fields merged across saves; reads return metadata only, never values.
- CRUD API GET /v1/credentials, GET/PUT/DELETE /v1/credentials/{id}
  (?environment= selects the value scope; values never returned on GET).
- `fuse credentials set/list/delete` CLI.

Consumption wiring (source:"credential", {{credential:}} token, LLM
provider-by-id) follows in a separate PR. Migration 000011 must land after the
environments migration 000010.

Tests: domain/helper/namespace-collision, memory repo, service (value→store,
per-env isolation, read-only fallback, field merge), memory+postgres contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…scope' into feat/per-context-llm-keys

# Conflicts:
#	docs/adr/0031-settings-secrets-and-environments.md
…feat/credentials-registry

# Conflicts:
#	internal/actors/mux_worker.go
#	internal/app/di/actors.go
#	internal/app/di/repos.go
#	internal/app/di/services.go
#	tests/functional/postgres_test.go
…stry

# Conflicts:
#	internal/handlers/trigger_workflow.go
The fields TEXT[] column is NOT NULL; a nil []string encodes as SQL NULL and
violated the constraint when saving a credential with no fields (caught by the
functional postgres contract test). Coerce nil -> []string{}.

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.

1 participant