feat(http_request): add env-backed credential profiles and onboarding policy presets#2148
Conversation
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... 📒 Files selected for processing (6)
✏️ Tip: You can disable in-progress messages and the fortune message in your review settings. Note
|
| Cohort / File(s) | Summary |
|---|---|
Docs & Config Reference docs/config-reference.md |
Documented new http_request.credential_profiles, examples (TOML and JSON), and usage notes for env-backed header injection. |
Config Schema src/config/schema.rs |
Added HttpRequestCredentialProfile (header_name, env_var, value_prefix) and HttpRequestConfig.credential_profiles: HashMap<String, HttpRequestCredentialProfile>; added defaults and validators for profile names, header_name, and env_var; updated tests. |
Public Re-exports src/config/mod.rs |
Adjusted public re-exports: removed several prior exports and re-exported HttpRequestCredentialProfile plus a broad set of schema types (see diff). Added test ensuring constructibility of the new profile type. |
Onboarding Wizard src/onboard/wizard.rs |
Added interactive flows/utilities: productivity allowlist, CSV domain parsing, env-var validation, profile name normalization, default env var derivation, and setup_http_request_credential_profiles; integrated profile setup into web tool prompts and added tests. |
HTTP Tool Implementation src/tools/http_request.rs |
HttpRequestTool gains credential_profiles field; new constructor parameter; added resolve_credential_profile, has_header_name_conflict, and redact_sensitive_values; supports credential_profile arg, injects headers from env, detects header conflicts (case-insensitive), and redacts sensitive headers/body; tests updated. |
Tool Initialization src/tools/mod.rs |
Wired http_config.credential_profiles into HttpRequestTool::new calls in all_tools_with_runtime, updating constructor signature usage. |
Sequence Diagram
sequenceDiagram
participant User as User/Wizard
participant Config as Config Schema
participant Tool as HttpRequestTool
participant Env as Environment
User->>Config: Provide credential_profile name & env_var
Config->>Config: Validate profile name and env_var
Config->>Config: Store profile in credential_profiles
User->>Tool: Execute HTTP request with credential_profile arg
Tool->>Config: Lookup profile in credential_profiles
Tool->>Env: Read environment variable
Env-->>Tool: Return env_var value
Tool->>Tool: Construct header (header_name, value_prefix + value)
Tool->>Tool: Check header name conflicts (case-insensitive)
Tool->>Tool: Inject header and mark value as sensitive
Tool->>Tool: Execute HTTP request
Tool->>Tool: Redact sensitive values from response headers and body
Tool-->>User: Return sanitized response
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
- feat(config): make config writes atomic with rollback-safe replacement #190: Related changes to tool initialization APIs and wiring that touch
all_tools_with_runtimeand tool constructor signatures. - feat(security): add shell risk classification, approval gates, and action throttling #187: Related adjustments to tools initialization and constructor signatures affecting runtime-aware tool setup.
Suggested labels
tool
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 57.45% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | The title accurately summarizes the main changes: adding env-backed credential profiles to http_request and improving onboarding with domain policy presets. |
| Description check | ✅ Passed | PR description includes all required template sections with substantive content covering problem, changes, validation, security, compatibility, and rollback planning. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
- 📝 Generate docstrings (stacked PR)
- 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
issue-reddit-hardening-onboard-cred-flow
Comment @coderabbitai help to get the list of available commands and usage tips.
PR intake checks found warnings (non-blocking)Fast safe checks found advisory issues. CI lint/test/build gates still enforce merge quality.
Action items:
Detected Linear keys: RMN-211 Run logs: https://github.com/zeroclaw-labs/zeroclaw/actions/runs/22521271518 Detected blocking line issues (sample):
Detected advisory line issues (sample):
|
|
Thanks for contributing to ZeroClaw. For faster review, please ensure:
See |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
src/config/mod.rs (1)
31-43: Consider adding a constructibility test forHttpRequestCredentialProfile.The existing tests verify that re-exported config types are constructible. Since
HttpRequestCredentialProfileis a new public export central to this PR, consider adding a similar test to ensure the type remains constructible and to document its expected structure.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/mod.rs` around lines 31 - 43, Add a new unit test in the existing tests module that constructs an HttpRequestCredentialProfile and asserts its fields are initialized as expected; for example create a test function named reexported_http_request_credential_profile_is_constructible that either calls HttpRequestCredentialProfile::default() or constructs it directly and verifies key public fields (e.g., that optional fields are Some/None or strings are non-empty as appropriate) to ensure the type is constructible and exported correctly.src/tools/http_request.rs (1)
296-297: Validatecredential_profiletype explicitly when the key is present.If
credential_profileis provided with a non-string value, it is silently ignored today. Returning a structured error keeps parameter validation strict and predictable.🧭 Proposed fix
- let credential_profile = args.get("credential_profile").and_then(|v| v.as_str()); + let credential_profile = match args.get("credential_profile") { + Some(value) => match value.as_str() { + Some(name) => Some(name), + None => { + return Ok(ToolResult { + success: false, + output: String::new(), + error: Some("Invalid 'credential_profile': expected string".into()), + }); + } + }, + None => None, + };As per coding guidelines
src/tools/**/*.rs: "ImplementTooltrait ... with strict parameter schema, validate and sanitize all inputs."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tools/http_request.rs` around lines 296 - 297, The code currently silently ignores non-string credential_profile values; update the argument validation around args.get("credential_profile") (where credential_profile is set) to explicitly check the value's type and return a structured error when the key exists but is not a string (similar to how body is handled), e.g., detect presence with args.contains_key("credential_profile") then confirm args.get(...).and_then(|v| v.as_str()). If the type is wrong, return an Err describing an invalid parameter (keep the error shape consistent with other validation errors in this module/Tool implementation) so parameter validation for credential_profile is strict and predictable.docs/config-reference.md (1)
552-559: Mention header-conflict behavior in the usage snippet.Consider adding one sentence that
args.headersmust not include the same header key as the selectedcredential_profile(case-insensitive), since runtime rejects that combination.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/config-reference.md` around lines 552 - 559, Add a short sentence to the http_request usage snippet clarifying header-conflict behavior: state that args.headers must not include any header key that is also provided by the selected credential_profile (case-insensitive), because the runtime rejects requests where a header key duplicates a credential-provided header; reference the http_request call and the args.headers and credential_profile parameters so readers know which fields this applies to.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/config/schema.rs`:
- Around line 6331-6340: The current validation for profile.header_name only
checks for control characters; update the validation in the block handling
profile.header_name (the header_name variable coming from
profile.header_name.trim()) to attempt parsing the header name using
reqwest::header::HeaderName::from_bytes(header_name.as_bytes()) and treat any
parse error as a validation failure (bail with a message like
"http_request.credential_profiles.{profile_name}.header_name is invalid:
<error>"). Remove the header_name.chars().any(char::is_control) check and ensure
the new check runs after trimming so invalid tokens (spaces, separators,
non-token chars per RFC 7230) are rejected at config load time.
- Around line 1401-1402: The serde default for the value_prefix field currently
uses #[serde(default)] which deserializes to an empty string; change it to
#[serde(default = "default_http_request_credential_value_prefix")] so omitted
configs get the intended "Bearer " default; ensure the helper function
default_http_request_credential_value_prefix() exists and returns the desired
String and keep the struct's impl Default consistent with that helper (update or
add the helper if missing and reference the field name value_prefix to locate
the change).
In `@src/onboard/wizard.rs`:
- Around line 3085-3088: The code currently falls back to wildcard when
parse_allowed_domains_csv(&raw) returns an empty vector, which unintentionally
broadens access; in the block that checks domains.is_empty() inside wizard.rs
replace the wildcard fallback with a safe handling path — either return an
explicit validation error indicating the custom domain list is empty or return
Ok(vec![]) so no domains are allowed; update any caller/validation logic that
expects a non-empty list accordingly. Ensure you modify the branch around
parse_allowed_domains_csv and domains.is_empty() rather than other validation
paths so the function name and behavior remain consistent.
- Around line 3188-3227: The normalized profile name produced by
normalize_http_request_profile_name may collide with existing keys and the
current http_request_config.credential_profiles.insert call will silently
overwrite; update the code to detect collisions and fail fast: before inserting
the HttpRequestCredentialProfile for profile_name check whether
http_request_config.credential_profiles already contains that key (use
contains_key or the entry API) and if so return an error / anyhow::bail! with a
clear message referencing the original raw_name and normalized profile_name to
prevent silent overwrites.
In `@src/tools/http_request.rs`:
- Around line 186-194: The function redact_sensitive_values currently skips any
sensitive value shorter than 6 characters, which can leak short secrets; update
redact_sensitive_values to remove the minimum-length gate (delete the
needle.len() < 6 check) but keep the trim and empty-string guard, so every
non-empty trimmed value in sensitive_values is replaced with "***REDACTED***" in
the text; reference the function name redact_sensitive_values and the
sensitive_values iteration to locate and change the logic.
---
Nitpick comments:
In `@docs/config-reference.md`:
- Around line 552-559: Add a short sentence to the http_request usage snippet
clarifying header-conflict behavior: state that args.headers must not include
any header key that is also provided by the selected credential_profile
(case-insensitive), because the runtime rejects requests where a header key
duplicates a credential-provided header; reference the http_request call and the
args.headers and credential_profile parameters so readers know which fields this
applies to.
In `@src/config/mod.rs`:
- Around line 31-43: Add a new unit test in the existing tests module that
constructs an HttpRequestCredentialProfile and asserts its fields are
initialized as expected; for example create a test function named
reexported_http_request_credential_profile_is_constructible that either calls
HttpRequestCredentialProfile::default() or constructs it directly and verifies
key public fields (e.g., that optional fields are Some/None or strings are
non-empty as appropriate) to ensure the type is constructible and exported
correctly.
In `@src/tools/http_request.rs`:
- Around line 296-297: The code currently silently ignores non-string
credential_profile values; update the argument validation around
args.get("credential_profile") (where credential_profile is set) to explicitly
check the value's type and return a structured error when the key exists but is
not a string (similar to how body is handled), e.g., detect presence with
args.contains_key("credential_profile") then confirm args.get(...).and_then(|v|
v.as_str()). If the type is wrong, return an Err describing an invalid parameter
(keep the error shape consistent with other validation errors in this
module/Tool implementation) so parameter validation for credential_profile is
strict and predictable.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
docs/config-reference.mdsrc/config/mod.rssrc/config/schema.rssrc/onboard/wizard.rssrc/tools/http_request.rssrc/tools/mod.rs
| let domains = parse_allowed_domains_csv(&raw); | ||
| if domains.is_empty() { | ||
| Ok(vec!["*".to_string()]) | ||
| } else { |
There was a problem hiding this comment.
Avoid silently escalating to wildcard when custom domain input is empty.
At Line 3087, an empty custom list falls back to *, which unexpectedly broadens access to all domains after the user explicitly chose “Custom domain list”.
🔒 Proposed fix
let domains = parse_allowed_domains_csv(&raw);
if domains.is_empty() {
- Ok(vec!["*".to_string()])
+ anyhow::bail!(
+ "Custom domain list cannot be empty. Use 'Allow all public domains (*)' if that is intended."
+ )
} else {
Ok(domains)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let domains = parse_allowed_domains_csv(&raw); | |
| if domains.is_empty() { | |
| Ok(vec!["*".to_string()]) | |
| } else { | |
| let domains = parse_allowed_domains_csv(&raw); | |
| if domains.is_empty() { | |
| anyhow::bail!( | |
| "Custom domain list cannot be empty. Use 'Allow all public domains (*)' if that is intended." | |
| ) | |
| } else { | |
| Ok(domains) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/onboard/wizard.rs` around lines 3085 - 3088, The code currently falls
back to wildcard when parse_allowed_domains_csv(&raw) returns an empty vector,
which unintentionally broadens access; in the block that checks
domains.is_empty() inside wizard.rs replace the wildcard fallback with a safe
handling path — either return an explicit validation error indicating the custom
domain list is empty or return Ok(vec![]) so no domains are allowed; update any
caller/validation logic that expects a non-empty list accordingly. Ensure you
modify the branch around parse_allowed_domains_csv and domains.is_empty() rather
than other validation paths so the function name and behavior remain consistent.
| fn redact_sensitive_values(text: &str, sensitive_values: &[String]) -> String { | ||
| let mut redacted = text.to_string(); | ||
| for value in sensitive_values { | ||
| let needle = value.trim(); | ||
| if needle.is_empty() || needle.len() < 6 { | ||
| continue; | ||
| } | ||
| redacted = redacted.replace(needle, "***REDACTED***"); | ||
| } |
There was a problem hiding this comment.
Remove the minimum-length redaction gate to avoid leaking short secrets.
Values shorter than 6 characters are currently skipped, which can expose valid credentials (short API keys/tokens) in output.
🔐 Proposed fix
for value in sensitive_values {
let needle = value.trim();
- if needle.is_empty() || needle.len() < 6 {
+ if needle.is_empty() {
continue;
}
redacted = redacted.replace(needle, "***REDACTED***");
}As per coding guidelines src/{security,runtime,gateway,tools,observability}/**/*.rs: "never log secrets, raw tokens, or sensitive payloads."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/tools/http_request.rs` around lines 186 - 194, The function
redact_sensitive_values currently skips any sensitive value shorter than 6
characters, which can leak short secrets; update redact_sensitive_values to
remove the minimum-length gate (delete the needle.len() < 6 check) but keep the
trim and empty-string guard, so every non-empty trimmed value in
sensitive_values is replaced with "***REDACTED***" in the text; reference the
function name redact_sensitive_values and the sensitive_values iteration to
locate and change the logic.
ab59111 to
6bf9a27
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (2)
src/onboard/wizard.rs (2)
3269-3276:⚠️ Potential issue | 🟡 MinorGuard against normalized profile-name collisions before insert.
At Line 3269,
insertcan silently overwrite an existing profile if two raw names normalize to the same key.🛠️ Suggested fix
+ if http_request_config + .credential_profiles + .contains_key(&profile_name) + { + anyhow::bail!( + "Credential profile '{}' normalizes to '{}' which already exists. Choose a different profile name.", + raw_name, + profile_name + ); + } + http_request_config.credential_profiles.insert( profile_name.clone(), HttpRequestCredentialProfile { header_name, env_var, value_prefix, }, );Based on learnings, "Keep error paths obvious and localized; prefer explicit
bail!/errors for unsupported or unsafe states; never silently broaden permissions/capabilities".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/onboard/wizard.rs` around lines 3269 - 3276, The code currently calls http_request_config.credential_profiles.insert(profile_name.clone(), HttpRequestCredentialProfile { header_name, env_var, value_prefix }) which can silently overwrite an existing entry when two raw profile names normalize to the same key; modify the logic to check for an existing key before inserting (e.g., use contains_key or entry API), and if a collision is detected return an explicit error/bail (with a descriptive message including profile_name) instead of overwriting; ensure this check happens immediately before inserting into credential_profiles and reference the HttpRequestCredentialProfile construction so you only insert after the collision guard passes.
3134-3137:⚠️ Potential issue | 🟠 MajorDo not widen custom HTTP domain policy to wildcard on empty input.
At Line 3135, empty custom input currently resolves to
*, which silently broadens permissions right after the operator chose “Custom domain list.”🔒 Suggested fix
let domains = parse_allowed_domains_csv(&raw); if domains.is_empty() { - Ok(vec!["*".to_string()]) + anyhow::bail!( + "Custom domain list cannot be empty. Use 'Allow all public domains (*)' if that is intended." + ) } else { Ok(domains) }As per coding guidelines, "Prefer explicit
bail!/errors for unsupported or unsafe states; never silently broaden permissions/capabilities".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/onboard/wizard.rs` around lines 3134 - 3137, The current logic in the onboarding flow uses parse_allowed_domains_csv(&raw) and returns Ok(vec!["*".to_string()]) when the parsed domains list is empty, which silently broadens the HTTP domain policy; change this to treat an empty custom input as an error instead: when parse_allowed_domains_csv(&raw) yields an empty Vec, return an explicit error (e.g., bail! or Err with a clear message) indicating that a custom domain list cannot be empty rather than substituting the wildcard; update the surrounding function (the onboarding/wizard handler that calls parse_allowed_domains_csv) to propagate this error so callers can surface it to the user.
🧹 Nitpick comments (1)
src/tools/http_request.rs (1)
807-839: Consider using a dedicated test environment variable instead ofHOME.While using
HOMEworks reliably, consider setting a project-scoped test env var (e.g.,ZEROCLAW_TEST_CREDENTIAL_VALUE) at the start of the test. This avoids any risk of real user paths appearing in failure output and aligns with the principle of keeping test fixtures project-scoped.♻️ Optional: Use dedicated test env var
#[test] fn resolve_credential_profile_injects_env_backed_header() { + let test_secret = "test-credential-value-12345"; + std::env::set_var("ZEROCLAW_TEST_HTTP_CREDENTIAL", test_secret); + let mut profiles = HashMap::new(); profiles.insert( "github".to_string(), HttpRequestCredentialProfile { header_name: "Authorization".to_string(), - env_var: "HOME".to_string(), + env_var: "ZEROCLAW_TEST_HTTP_CREDENTIAL".to_string(), value_prefix: "Bearer ".to_string(), }, ); // ... tool construction ... - let home = std::env::var("HOME").expect("HOME should be set in test environment"); let (headers, sensitive_values) = tool .resolve_credential_profile("github") .expect("profile should resolve"); assert_eq!(headers.len(), 1); assert_eq!(headers[0].0, "Authorization"); - assert_eq!(headers[0].1, format!("Bearer {home}")); - assert!(sensitive_values.contains(&home)); - assert!(sensitive_values.contains(&format!("Bearer {home}"))); + assert_eq!(headers[0].1, format!("Bearer {test_secret}")); + assert!(sensitive_values.contains(&test_secret.to_string())); + assert!(sensitive_values.contains(&format!("Bearer {test_secret}"))); + + std::env::remove_var("ZEROCLAW_TEST_HTTP_CREDENTIAL"); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tools/http_request.rs` around lines 807 - 839, The test resolve_credential_profile_injects_env_backed_header should not rely on HOME; set a dedicated test env var (e.g., "ZEROCLAW_TEST_CREDENTIAL_VALUE") at the start of the test, update the HttpRequestCredentialProfile.env_var to that name, and use std::env::set_var to seed it with a deterministic value then read it via std::env::var as before; after the assertions, clean up with std::env::remove_var to avoid test pollution. Locate the test function resolve_credential_profile_injects_env_backed_header, the HttpRequestCredentialProfile creation, and the call to tool.resolve_credential_profile to apply these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/onboard/wizard.rs`:
- Around line 3269-3276: The code currently calls
http_request_config.credential_profiles.insert(profile_name.clone(),
HttpRequestCredentialProfile { header_name, env_var, value_prefix }) which can
silently overwrite an existing entry when two raw profile names normalize to the
same key; modify the logic to check for an existing key before inserting (e.g.,
use contains_key or entry API), and if a collision is detected return an
explicit error/bail (with a descriptive message including profile_name) instead
of overwriting; ensure this check happens immediately before inserting into
credential_profiles and reference the HttpRequestCredentialProfile construction
so you only insert after the collision guard passes.
- Around line 3134-3137: The current logic in the onboarding flow uses
parse_allowed_domains_csv(&raw) and returns Ok(vec!["*".to_string()]) when the
parsed domains list is empty, which silently broadens the HTTP domain policy;
change this to treat an empty custom input as an error instead: when
parse_allowed_domains_csv(&raw) yields an empty Vec, return an explicit error
(e.g., bail! or Err with a clear message) indicating that a custom domain list
cannot be empty rather than substituting the wildcard; update the surrounding
function (the onboarding/wizard handler that calls parse_allowed_domains_csv) to
propagate this error so callers can surface it to the user.
---
Nitpick comments:
In `@src/tools/http_request.rs`:
- Around line 807-839: The test
resolve_credential_profile_injects_env_backed_header should not rely on HOME;
set a dedicated test env var (e.g., "ZEROCLAW_TEST_CREDENTIAL_VALUE") at the
start of the test, update the HttpRequestCredentialProfile.env_var to that name,
and use std::env::set_var to seed it with a deterministic value then read it via
std::env::var as before; after the assertions, clean up with
std::env::remove_var to avoid test pollution. Locate the test function
resolve_credential_profile_injects_env_backed_header, the
HttpRequestCredentialProfile creation, and the call to
tool.resolve_credential_profile to apply these changes.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
docs/config-reference.mdsrc/config/mod.rssrc/config/schema.rssrc/onboard/wizard.rssrc/tools/http_request.rssrc/tools/mod.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/config-reference.md
- src/config/schema.rs
6bf9a27 to
bba7a0d
Compare
Summary
Problem
http_requestcalls currently require callers to pass raw auth headers/tokens directly in tool arguments, which increases secret-handling risk and onboarding friction.Why It Matters
Users need a safer default for authenticated HTTP automation and a guided onboarding path for common integrations.
What Changed
http_request.credential_profilesconfig (header name + env var + optional value prefix).http_requestruntime to:http_requestpolicy modes and optional credential-profile setup.docs/config-reference.md.Label Snapshot
config: coredocsonboard: wizardrisk: highsize: Ltool: http_requestChange Metadata
issue-reddit-hardening-onboard-cred-flowmain@theonlyhennygodLinked Issue
Validation Evidence
Commands executed locally in this pass:
cargo test credential_profile -- --nocapturecargo fmt --all -- --checkCurrent outcome:
main.Security Impact
http_requestoutput paths.Privacy and Data Hygiene
Compatibility
http_requestusage withoutcredential_profile.i18n Follow-Through
Human Verification
Side Effects
Rollback Plan
#2148frommain.credential_profilesusage from runtime and config.Risks and Mitigations
Summary by CodeRabbit