Files
ironclaw/.gitignore
Pranav Raja 49f3e8d566 feat(credentials): path-based credential matching for per-endpoint auth (#2168)
* feat(credentials): path-based credential matching for per-endpoint auth

Add `path_patterns` field to `CredentialMapping` to scope credentials to
specific URL path prefixes on a host. When set, the request path must
match a prefix at a segment boundary (`/` or `?`). When empty (default),
credentials match all paths on the host — fully backwards compatible.

Key changes:
- `CredentialMapping.matches(host, path)` with segment-boundary enforcement
- `path_matches_prefix()` rejects `..` traversal, normalizes trailing slashes
- `host_matches_pattern()` deduplicated to single source in secrets/types.rs,
  case-insensitive per RFC 4343
- HTTP tool uses `find_for_url(host, path)` for path-aware credential lookup
- Auth manager pre-flight check uses path-aware `find_for_url`
- WASM tool/channel wrappers carry `path_patterns` through to injection time
- `CredentialMappingSchema`, `SkillCredentialSpec` support `path_patterns`

Tests cover segment-boundary attacks, path traversal rejection, case-insensitive
host matching, path-scoped injection, and different credentials for different
paths on the same host.

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

* fix(credentials): address PR #2168 review feedback

- Narrow `..` rejection in path_matches_prefix to per-segment so
  legitimate paths like /api/..config are no longer falsely blocked
  (path_matches_prefix was using path.contains("..")).
- Path-scope credential injection in the channels WASM wrapper:
  ResolvedHostCredential now carries path_patterns and
  inject_host_credentials honors it, matching the tools-side wrapper.
- Add path-aware CredentialInjector API (find_credentials_for_url /
  inject_for_url); deprecate the host-only variants and
  SharedCredentialRegistry::find_for_host with #[deprecated] attrs.
- Tighten CredentialMappingSchema.path_patterns from
  Option<Vec<String>> to #[serde(default)] Vec<String>, matching
  sibling types.
- Validate path_patterns in validate_credential_spec: require
  leading '/', reject empty, reject '..' as a segment.
- Expand comment in http.rs documenting why LLM-header blocking is
  host-scoped (exfil defense) while injection is path-scoped
  (minimum privilege).

Tests: +5 validation cases, +2 injector cases, +2 channel wrapper
cases, +2 path_matches_prefix cases covering dot-dot-inside-segment.

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

* fix(credentials): address PR #2168 round-3 review

- secrets/types: reject %2e / %2E in paths (percent-encoded traversal
  bypass for servers that decode before routing, e.g. IIS/Tomcat)
- sandbox/proxy/policy: find_credential now honors path_patterns via
  CredentialMapping::matches, using request.path (regression test added)
- wasm wrappers: extract shared extract_url_path_for_matching helper in
  secrets/types, with tracing::debug! on URL parse failure; removes
  duplicated 12-line block between tools/wasm and channels/wasm
- ironclaw_skills/validation: factor validate_path_pattern out of
  validate_credential_spec and reject '?' and '#' in path_patterns
  (Url::path() strips them, so these silently never match)
- tools/wasm/capabilities_schema: plumb the same validate_path_pattern
  through the WASM manifest loader — bad patterns log as warnings
  instead of silently failing to match
- tools/builtin/http: unit tests for extract_path_from_params (valid,
  missing url, query+fragment stripping, bare host, malformed)
- tests/skill_credential_injection: caller-level tests driving
  HttpTool::execute with path-scoped credentials — segment boundary
  and auth-gap-on-non-matching-path (per .claude/rules/testing.md
  "Test Through the Caller, Not Just the Helper")

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

* fix(credentials): address PR #2168 round-4 review

- secrets/types: path_matches_prefix now decodes each segment and rejects
  only dot-segments (literal . or .., plus percent-encoded equivalents
  like %2e, %2e%2e, mixed case, .%2e, %2e.). Legitimate literal paths
  with embedded encoded dots — /files/foo%2ebar → foo.bar,
  /releases/v1%2e2 → v1.2 — are now allowed. Replaces the previous
  "reject any %2e substring" rule which over-rejected normal filenames.
  (Firat #3125964627)

- secrets/types: add match_specificity(path_patterns, req_path) returning
  the length of the longest matching prefix (0 if unscoped). Exported
  pub(crate) for callers that need deterministic credential precedence.

- credential_injector + both wasm wrappers: sort matching credentials by
  ascending path specificity, tie-broken alphabetically on secret_name,
  before the last-write-wins header merge. The most-specific mapping now
  wins any header conflict regardless of HashMap iteration order, which
  fixes nondeterministic winner selection on overlapping mappings.
  ResolvedHostCredential gains a secret_name field purely for stable
  tie-breaks (no secret material exposed). (Firat #3125963270)

- bridge/auth_manager::check_http_auth: replace "return Ready on first
  resolved mapping" short-circuit with conjunctive evaluation — every
  non-optional matched mapping must resolve for Ready. Optional mappings
  are skipped. Missing required credentials are accumulated and returned
  as MissingCredentials so endpoints needing bearer + org-header surface
  the auth gate instead of failing at the wire with a raw 401.
  (Firat #3125963977)

- tools/builtin/http::execute: stop clearing missing_credential on peer
  success and drop the !injected_any_credential guard. Track the first
  missing required credential for the 401/403 remediation UX; skip
  optional mappings. Matches the new auth_manager behavior.

Regression tests:
- secrets/types: path_matches_prefix_rejects_percent_encoded_dot_segments
  (adds .%2e and %2e. mixed-form cases), path_matches_prefix_allows_legit_
  embedded_encoded_dot (foo%2ebar, v1%2e2), match_specificity_ranks_
  longer_prefixes_higher.
- tools/wasm/wrapper: test_inject_host_credentials_most_specific_path_wins
  verifies the sort is order-independent by constructing the same creds
  in both orders and asserting the specific one wins.
- bridge/auth_manager: check_http_conjunctive_auth_any_missing_required_
  raises_gate, check_http_conjunctive_auth_all_required_resolved_is_ready.

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

* style: cargo fmt after round-3/round-4 review fixes

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

* chore: untrack accidentally-committed local scratch files

Follow-up to 50f6771f, which inadvertently staged local-only scratch
files via `git add -A`:
- .DS_Store
- tests_all/ — operator scratch dir with env files (rotate any tokens
  that were in tests_all/source_env_vars.sh in that commit)
- integrations/abound/tests/source_env_vars.sh — same concern
- src/cli/snapshots/*.snap.new — stale insta snapshot proposals that
  should be resolved via `cargo insta review`, not committed

This commit removes them from the index and adds matching patterns to
.gitignore so they cannot recur. The content is left on disk for local
use.

NOTE: the tokens that were in the env files are still visible in the
50f6771f commit object and must be rotated regardless of this cleanup.

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

* fix(credentials): address PR #2168 round-5 review

- secrets/types: tighten the percent-decoded dot-segment check to also
  reject segments whose decoded form contains `/` (percent-encoded
  slash, e.g. `%2f`). Blocks `%2e%2e%2fadmin`-style smuggling on
  servers that decode encoded slashes before routing (Tomcat with
  `allowEncodedSlash=true`, older IIS, certain reverse proxies).
  Literal dots inside a single segment (`foo%2ebar`, `v1%2e2`) stay
  allowed. (Firat #3126256056)
- sandbox/proxy/policy: `find_credential` now returns the most-specific
  matching mapping via `max_by(...)` on `(match_specificity, secret_name)`
  rather than `.find(...)`. Behavior matches `SharedCredentialRegistry::
  find_for_url` and both WASM `inject_host_credentials` — a host with a
  global + a `/api/v1/write` credential picks WRITE_TOKEN on writes
  regardless of Vec order. (Firat #3126256060)
- tools/wasm/capabilities_schema: `to_credential_mapping` now returns
  `Option` and `to_http_capability` filter_maps invalid mappings out
  entirely. Previously `path_patterns: [""]` survived the warning-only
  validator and silently widened the credential back to global scope
  (`path_matches_prefix(path, "")` → true for every absolute path).
  The drop-on-invalid semantics match the skills pipeline. Removed the
  duplicate warn-loop in `validate()` since the load path now handles
  it. (Firat #3126256040)

Regression tests:
- secrets/types: path_matches_prefix_rejects_percent_encoded_slash_
  smuggling covers lowercase/uppercase `%2f` in traversal and embedded
  positions.
- sandbox/proxy/policy: test_sandbox_proxy_most_specific_credential_
  wins registers the same credentials in both orders and asserts
  WRITE_TOKEN always wins on the write path.
- tools/wasm/capabilities_schema: three tests covering empty-string
  pattern (drops mapping), missing-leading-slash (drops mapping), and
  valid pattern (preserved).

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 11:58:26 -07:00

66 lines
1.4 KiB
Plaintext

.env
.env.local
.env.*
!.env.example
# macOS metadata
.DS_Store
# Local scratch directories — contain operator-only env files with real
# production admin/API tokens. Never commit.
tests_all/
integrations/abound/tests/source_env_vars.sh
# Stale insta snapshot proposals (review via `cargo insta review`)
*.snap.new
# Claude Code worktrees and lock files
.claude/worktrees/
.claude/scheduled_tasks.lock
# Sidecar tool data
.sidecar/
.todos/
target/
# Python
__pycache__/
*.pyc
/tests/e2e/.venv/
# Benchmark results (local runs, not committed)
bench-results/
# Coverage reports (local runs, not committed)
/coverage/
# Canary / E2E run outputs (per-run logs, screenshots, trace artifacts —
# CI uploads these via actions/upload-artifact; never commit local copies)
artifacts/
# WASM build artifacts (loaded from disk, not bundled)
*.wasm
# Traces
trace_*.json
# Local Claude Code settings (machine-specific, should not be committed)
.claude/settings.local.json
.worktrees/
.ironclaw/
# Python cache
__pycache__/
*.pyc
*.pyo
*.pyd
engine_trace_*.json
tests/fixtures/llm_traces/live/github_dev_workflow_full_loop.json
tests/fixtures/llm_traces/live/github_dev_workflow_full_loop.log
# Per-test live-replay logs — generated when running `--ignored` live
# tests locally. Only the .json fixtures for each scenario are checked
# in; the .log files are local debugging artifacts.
tests/fixtures/llm_traces/live/*.log