feat(agent-runtime): add credential-injection proxy package (fixes #39278) - #39483
feat(agent-runtime): add credential-injection proxy package (fixes #39278)#39483kuangmi-bit wants to merge 2 commits into
Conversation
…ggenius#39278 design) Add internal/core/proxy/ — a secret-injection outbound proxy that resolves placeholder env-var values into real secrets at the network boundary. Real credentials never enter the agent process address space. Based on the design from langgenius#39278 (Hardening environment variables in agent runtime). Package contents: - types.go — DomainRule, SecretMapping, ProviderProfile, InjectionConfig - config.go — providers.yaml parsing + BuildEnv - handler.go — InjectionHandler (http.Handler) + InjectionRoundTripper - handler_test.go — 12 conformance tests (all PASS) Security invariants: 1. Placeholder model — agent sees __secret:provider:env__, never real value 2. Domain allowlist with suffix-attack prevention 3. Structured rejection (403, not silent-strip) 4. Path traversal prevention on value_file 5. Audit logging via structured slog Tested: go build + go test (12/12 PASS, 0.018s)
📄 Knowledge review🆕 New pages1 new page was drafted from this PR.
|
There was a problem hiding this comment.
Pull request overview
This PR introduces a new dify-agent-runtime/internal/core/proxy package that implements a credential-injection outbound proxy. It keeps real secrets out of the agent process by using __secret:<provider>:<env>__ placeholders and resolving them at the network boundary based on domain allowlists defined in providers.yaml.
Changes:
- Add core types/config parsing for provider profiles, secret mappings, and placeholder generation.
- Implement an
http.Handlerandhttp.RoundTripperthat inject secrets into outbound headers only for allowlisted domains, with audit logging and traversal checks. - Add a conformance-style test suite and wire in YAML dependency updates (
gopkg.in/yaml.v3).
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| dify-agent-runtime/internal/core/proxy/types.go | Defines config schema (providers/secrets/domain rules) and placeholder helpers/validation. |
| dify-agent-runtime/internal/core/proxy/config.go | Loads/parses providers.yaml and builds placeholder-only env exposure. |
| dify-agent-runtime/internal/core/proxy/handler.go | Implements injection logic for both http.Handler and http.RoundTripper. |
| dify-agent-runtime/internal/core/proxy/handler_test.go | Adds tests for placeholder format, injection/rejection behavior, and config/env helpers. |
| dify-agent-runtime/go.mod | Adds YAML v3 dependency. |
| dify-agent-runtime/go.sum | Adds checksums for new dependencies. |
Comments suppressed due to low confidence (3)
dify-agent-runtime/internal/core/proxy/handler.go:102
- This rejection path uses http.Error with a JSON string, which results in a text/plain response and can produce invalid JSON if values contain quotes. Prefer writing an application/json response with properly escaped fields.
http.Error(w,
fmt.Sprintf(`{"error":"domain_not_allowed","provider":"%s","env":"%s","domain":"%s"}`,
profile.Name, secret.Env, targetHost),
http.StatusForbidden,
)
dify-agent-runtime/internal/core/proxy/handler.go:127
- This error response is produced via http.Error with an embedded JSON string, which sets Content-Type to text/plain and can break JSON escaping. For a structured JSON error, write the response body directly with Content-Type application/json and escaped values.
http.Error(w,
fmt.Sprintf(`{"error":"secret_unavailable","provider":"%s","env":"%s"}`,
profile.Name, secret.Env),
http.StatusInternalServerError,
)
dify-agent-runtime/internal/core/proxy/handler.go:153
- This uses http.Error to return a JSON-looking rejection body, but it will be served as text/plain and may not be valid JSON if values contain quotes. Write JSON directly with Content-Type application/json and escaped fields.
http.Error(w,
fmt.Sprintf(`{"error":"domain_not_allowed","provider":"%s","env":"%s","domain":"%s"}`,
profile.Name, secret.Env, targetHost),
http.StatusForbidden,
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func hostFromRequest(r *http.Request) string { | ||
| if r.Host != "" { | ||
| h, _, err := net.SplitHostPort(r.Host) | ||
| if err != nil { | ||
| // Host header may not include port. | ||
| return r.Host | ||
| } | ||
| return h | ||
| } | ||
| if r.URL != nil { | ||
| return r.URL.Hostname() | ||
| } | ||
| return "" | ||
| } |
| func (h *InjectionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
| if h.Config == nil || len(h.Config.Providers) == 0 { | ||
| h.Next.ServeHTTP(w, r) | ||
| return | ||
| } |
| func (t *InjectionRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { | ||
| if t.Config == nil { | ||
| return t.Next.RoundTrip(r) | ||
| } | ||
|
|
||
| targetHost := hostFromRequest(r) | ||
| if targetHost == "" { | ||
| return t.Next.RoundTrip(r) | ||
| } |
| resolved := secret.Inject.ResolveValue(realValue) | ||
| r.Header.Set(headerName, resolved) |
| if err != nil { | ||
| return nil, fmt.Errorf("secret injection: %w", err) | ||
| } | ||
| r.Header.Set(headerName, secret.Inject.ResolveValue(realValue)) |
| http.Error(w, | ||
| fmt.Sprintf(`{"error":"secret_unavailable","provider":"%s","env":"%s"}`, | ||
| profile.Name, secret.Env), | ||
| http.StatusInternalServerError, | ||
| ) | ||
| return |
| // Find the provider for this env. | ||
| for _, p := range c.Providers { | ||
| for _, s := range p.Secrets { | ||
| if s.Env == name { | ||
| result = append(result, | ||
| fmt.Sprintf("%s=%s", name, Placeholder(p.Name, name))) | ||
| seen[name] = true | ||
| break | ||
| } | ||
| } | ||
| } |
| func (c InjectionConfig) Validate() error { | ||
| if len(c.Providers) == 0 { | ||
| return fmt.Errorf("at least one provider is required") | ||
| } | ||
| for name, p := range c.Providers { | ||
| p.Name = name | ||
| if err := p.Validate(); err != nil { | ||
| return err | ||
| } | ||
| c.Providers[name] = p | ||
| } | ||
| return nil | ||
| } |
Security Review (TLS / Credential Injection Boundary)Thanks for pushing this forward — the placeholder model and domain policy are strong foundations. 1) TLS implication (critical)For HTTPS traffic, a plain forward proxy without TLS interception cannot modify HTTP headers, because after So if we need to intercept agent HTTPS requests and inject credentials at proxy layer, this architecture must be explicitly MITM TLS termination + re-encryption. PoC RequestCould you please provide a PoC that demonstrates the full credential-injection flow, includin
The PoC should clearly show:
It’s okay if this PoC requires temporary changes in |
|
@wylswz thanks for the thorough security review. Re: TLS concern — the Re: Security boundary. The agent (LLM-generated code) runs inside Dify's sandbox with restricted filesystem access.
Re: PoC. Preparing a separate PoC branch demonstrating:
Will update here when ready. |
|
@wylswz PoC ready at Demonstrates the full flow: Dry-run (no token needed): Live test (with a real token): echo "ghp_yourtoken" > /tmp/dify-poc-secrets/github/token
go run ./cmd/credential-proxy-poc/
→ Calls api.github.com/user with the real injected tokenThe PoC separates concerns cleanly:
No MITM needed — the RoundTripper mutates headers in-memory before Go's TLS handshake. |
|
I'll be working on #39771. Thank you for the work any way. Will close this pull request as duplicated. |
Summary
Add
internal/core/proxy/todify-agent-runtime— a credential-injection outbound proxy that resolves placeholder env-var values into real secrets at the network boundary.Based on the design discussion in #39278.
What it does
Real API keys are replaced with placeholders in the agent process and resolved at the network boundary:
Package contents
types.goconfig.gohandler.gohandler_test.goSecurity invariants
__secret:provider:env__, never real value*.dify.internaldoes NOT matchevil-dify.internal.attacker.comvalue_fileslogIntegration
The proxy is a portable
http.RoundTripper. It can be mounted:cmd/dify-credential-proxy/)http.Client.TransportTests
References
CC: @wylswz @zeweihan @zyssyz123