woods 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/CONTRIBUTING.md +12 -2
- data/docs/CONFIGURATION_REFERENCE.md +8 -1
- data/docs/CONSOLE_MCP_SETUP.md +85 -8
- data/docs/MCP_HTTP_TRANSPORT.md +54 -2
- data/docs/UPGRADING_TO_2.md +60 -0
- data/exe/woods-mcp-http +16 -9
- data/lib/woods/cache/cache_middleware.rb +12 -11
- data/lib/woods/console/adapter_family.rb +39 -0
- data/lib/woods/console/credential_index.rb +33 -3
- data/lib/woods/console/embedded_executor.rb +401 -43
- data/lib/woods/console/model_validator.rb +8 -0
- data/lib/woods/console/rack_middleware.rb +39 -10
- data/lib/woods/console/redactor.rb +24 -10
- data/lib/woods/console/safe_context.rb +44 -7
- data/lib/woods/console/sql_noise_stripper.rb +41 -12
- data/lib/woods/console/sql_table_scanner.rb +45 -34
- data/lib/woods/console/sql_validator.rb +37 -2
- data/lib/woods/mcp/bearer_auth.rb +1 -1
- data/lib/woods/mcp/origin_guard.rb +24 -77
- data/lib/woods/mcp/origin_policy.rb +124 -0
- data/lib/woods/railtie_support.rb +8 -0
- data/lib/woods/version.rb +1 -1
- data/plugin/skills/woods-diagnose/SKILL.md +11 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +15 -0
- data/plugin/skills/woods-setup/SKILL.md +8 -0
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c00b317c5f1df618b940db9d8d2b596c27c1538b4d713d60c9263b1f0ab6acae
|
|
4
|
+
data.tar.gz: b0fceeb5a3220efdd9d3f0f5dd77d9bc12e1d780ddc689dfd2121f163d9ec75d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ec485c382f5a334d3607f2f18dd1489c02a6de17df0ba8922dd1d7271a36c8ffa1a71ec16346fe148460632e26f7ccbfc51c938ad96d1c8d03e68672d590993
|
|
7
|
+
data.tar.gz: 011e559a5cbd6478d579c0fc477c020048352b75872daaab75a52df527dd0b2c0e27645f0627321b03a87c9844aa52b7b5f9fab0e90b7e6ec071a244c8697ef1
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.0.1] - 2026-09-26
|
|
11
|
+
|
|
12
|
+
### Documentation
|
|
13
|
+
|
|
14
|
+
- Clarify supported Console redaction, SQL projection, adapter timeout and HTTP origin behavior, including maintenance-line differences.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Report invalidly encoded HTTP origin settings with bounded startup diagnostics; preserve existing origin defaults and access checks.
|
|
19
|
+
- Use the same Console configuration error for malformed origins during automatic and manual middleware construction.
|
|
20
|
+
- HTTP startup diagnostics consistently name `WOODS_MCP_HTTP_ALLOWED_ORIGINS` for malformed origins, including under a POSIX (`C`) locale.
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
|
|
24
|
+
- Refuse unsupported PostgreSQL escaped identifiers in raw Console SQL before query execution. Ordinary quoted identifiers, literal contents, and comments retain their existing behavior.
|
|
25
|
+
- Share validated Console query projections between execution and typed EAV redaction context, including whitespace normalization.
|
|
26
|
+
- Refuse SQL relation and CTE column alias lists while output redaction is configured, with an explicit redaction-identity error before execution. Direct unaliased projections remain supported.
|
|
27
|
+
- Harden Console request validation, protected result handling, and HTTP authentication and origin enforcement.
|
|
28
|
+
- Apply typed key/value redaction consistently to case-variant and schema-qualified source table names, retaining all matching model types when source schemas are ambiguous.
|
|
29
|
+
- Isolate cached retrieval contexts between application instances sharing a cache backend.
|
|
30
|
+
|
|
10
31
|
## [2.0.0] - 2026-09-23
|
|
11
32
|
|
|
12
33
|
### Added
|
data/CONTRIBUTING.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Contributing to Woods
|
|
2
2
|
|
|
3
3
|
<!-- release-state:contributing-intro -->
|
|
4
|
-
Woods welcomes bug fixes, extractor coverage, storage and retrieval improvements, MCP compatibility work, documentation, and focused performance changes. This guide covers the shared contribution contract. Coding agents working from a source checkout should also read the repository's [AGENTS.md](https://github.com/lost-in-the/woods/blob/v2.0.
|
|
4
|
+
Woods welcomes bug fixes, extractor coverage, storage and retrieval improvements, MCP compatibility work, documentation, and focused performance changes. This guide covers the shared contribution contract. Coding agents working from a source checkout should also read the repository's [AGENTS.md](https://github.com/lost-in-the/woods/blob/v2.0.1/AGENTS.md).
|
|
5
5
|
<!-- release-state:end -->
|
|
6
6
|
|
|
7
7
|
## Choose the right channel
|
|
@@ -46,7 +46,7 @@ Create a branch from current `main`. Keep each pull request to one logical chang
|
|
|
46
46
|
| `plugin/skills/` | Distributed Woods skills (setup/upgrade, MCP configuration, investigation, agent enablement, diagnosis) |
|
|
47
47
|
|
|
48
48
|
<!-- release-state:contributing-architecture -->
|
|
49
|
-
Read [CLAUDE.md](https://github.com/lost-in-the/woods/blob/v2.0.
|
|
49
|
+
Read [CLAUDE.md](https://github.com/lost-in-the/woods/blob/v2.0.1/CLAUDE.md) for architecture and implementation gotchas before changing runtime behavior.
|
|
50
50
|
<!-- release-state:end -->
|
|
51
51
|
|
|
52
52
|
### Agent orientation and static self-map
|
|
@@ -460,6 +460,16 @@ fresh tag-push CI run. Updating main's tooling alone never authorizes different
|
|
|
460
460
|
candidate bytes. Main's v2 release contract remains unchanged. Do not create or
|
|
461
461
|
push tags, dispatch, publish, or claim 1.6.3 is available during preparation.
|
|
462
462
|
|
|
463
|
+
### 2.0.1 maintenance CI contract
|
|
464
|
+
|
|
465
|
+
The 2.0.1 candidate uses trusted main's reviewed maintenance profile, not this
|
|
466
|
+
candidate's copy of the release tooling. That profile intentionally omits
|
|
467
|
+
`exact_ci_jobs` and `package_spec`: validation uses the normal v2 required-job
|
|
468
|
+
list and `spec/integration/packaged_gem_spec.rb`, rather than the legacy matrix
|
|
469
|
+
or package spec. The 1.6.4 profile is separate and has no C-locale artifact-reader
|
|
470
|
+
CI lane. Candidate evidence must identify the actual jobs and artifacts tested;
|
|
471
|
+
profile approval and publication remain maintainer steps.
|
|
472
|
+
|
|
463
473
|
### Stable branches
|
|
464
474
|
|
|
465
475
|
A stable branch is `N-M-stable`, cut from the release tag. Create one only when a released line needs a patch after a newer major has shipped on `main`; until then, `main` is the development branch. The explicitly approved short-lived `release/1.6.3` security exception above does not establish an `N-M-stable` branch.
|
|
@@ -291,6 +291,13 @@ unchanged. This is a reasonable default for hosts that don't bundle `sqlite3`.
|
|
|
291
291
|
|
|
292
292
|
## Retrieval cache options
|
|
293
293
|
|
|
294
|
+
The 2.0.1 maintenance patch scopes retrieval contexts to one retriever instance.
|
|
295
|
+
Reload retires only that instance's namespace, including results still in flight;
|
|
296
|
+
already-running requests may finish against the previous corpus. Restarting starts
|
|
297
|
+
a fresh context namespace. Retired entries expire under their configured TTL or
|
|
298
|
+
backend eviction; disabling both can retain unused entries indefinitely. Embedding
|
|
299
|
+
caches remain separate and are not cleared by context invalidation.
|
|
300
|
+
|
|
294
301
|
The optional cache wraps both embedding-provider calls and assembled retrieval
|
|
295
302
|
contexts. It is disabled by default and is separate from the Index Server's
|
|
296
303
|
tool-result `_meta` cache hint.
|
|
@@ -682,7 +689,7 @@ deployment guide including defense layers.
|
|
|
682
689
|
| `console_mcp_enabled` | Boolean | `false` | Master switch. When `false`, stdio exits and the mounted Console middleware passes requests through to Rails. |
|
|
683
690
|
| `console_mcp_http_enabled` | Boolean | `true` | HTTP transport switch; effective only while the master switch is on. Set `false` for stdio-only use without HTTP token validation or an active HTTP endpoint. Read at request time. |
|
|
684
691
|
| `console_mcp_token` | String | `ENV['WOODS_CONSOLE_MCP_TOKEN']` or `nil` | Bearer token required on every enabled Console HTTP request. With both Console flags enabled, production boot raises on a missing token; other environments warn and requests fail closed with 401. A configured token shorter than 32 characters raises at boot while HTTP is enabled. Explicit stdio-only configurations skip HTTP token validation. Generate with `SecureRandom.hex(32)`. |
|
|
685
|
-
| `console_mcp_allowed_origins` | Array\<String\> | `%w[http://localhost http://127.0.0.1 http://[::1]]` | `OriginGuard` allowlist.
|
|
692
|
+
| `console_mcp_allowed_origins` | Array\<String\> | `%w[http://localhost http://127.0.0.1 http://[::1]]` | `OriginGuard` allowlist shared with SDK dispatch. Portless entries permit same-authority requests; cross-origin ports must be listed explicitly. Default HTTP(S) ports normalize to their omitted form. Invalid entries fail at boot. |
|
|
686
693
|
| `console_mcp_path` | String | `/mcp/console` | URL path the Rack middleware responds on. |
|
|
687
694
|
| `console_embedded_read_tools` | Boolean | `false` | Register `console_sql` and `console_query` in supported stdio and Rack modes. |
|
|
688
695
|
| `console_blocked_tables` | Array\<String\> | `Woods::DEFAULT_CONSOLE_BLOCKED_TABLES` | TableGate denylist (case-insensitive). Bare names match every schema; qualified names (`schema.table`) match exactly. |
|
data/docs/CONSOLE_MCP_SETUP.md
CHANGED
|
@@ -209,7 +209,10 @@ Rails/MCP host. If a browser-based client sends an `Origin` header from a
|
|
|
209
209
|
different host, include that exact origin too. This allow-list controls both
|
|
210
210
|
DNS-rebinding Host checks and browser CORS; keep Rails' own `config.hosts`, TLS,
|
|
211
211
|
and proxy rules aligned with it. Server-to-server clients normally omit
|
|
212
|
-
`Origin`, but
|
|
212
|
+
`Origin`, but a present request `Host` must still be allowed. Supporting security
|
|
213
|
+
revisions deliberately allow configured non-loopback Hosts through the SDK too;
|
|
214
|
+
2.0.0 could refuse them at that inner layer despite the Woods allowlist. This
|
|
215
|
+
widening is limited to the configured authorities and retains bearer auth.
|
|
213
216
|
|
|
214
217
|
Do not mount `Woods::Console::RackMiddleware` by itself. The Railtie composes
|
|
215
218
|
`OriginGuard`, `BearerAuth`, and the Console middleware in the supported order.
|
|
@@ -636,6 +639,9 @@ Redaction is defense-in-depth, prefer not storing plaintext secrets in database
|
|
|
636
639
|
|
|
637
640
|
### `console_redacted_key_values`
|
|
638
641
|
|
|
642
|
+
See [read policy compatibility](#read-policy-compatibility) for SQL column-list
|
|
643
|
+
restrictions, conservative typed masking, exact key spelling and binary columns.
|
|
644
|
+
|
|
639
645
|
Column-name redaction falls short when credentials are stored in a **key-value (EAV)** table, e.g. a Stripe Connect `authorizations` row of `{key: "stripe_access_token", value: "sk_live_..."}`. The column holding the secret is called `value`, which is generic: adding `value` to `console_redacted_columns` would over-redact every unrelated row in the table.
|
|
640
646
|
|
|
641
647
|
`console_redacted_key_values` takes one or more patterns that describe "when a row has `key_column` set to one of these names, redact its `value_column`":
|
|
@@ -700,7 +706,7 @@ these controls, in order:
|
|
|
700
706
|
|
|
701
707
|
1. `SqlValidator` rejects DML/DDL (`INSERT`/`UPDATE`/`DELETE`/`MERGE`/`DROP`/`TRUNCATE`/`ALTER`/`CREATE`/`REPLACE`), row-lock clauses (`FOR UPDATE`, `FOR SHARE`, `LOCK IN SHARE MODE`), writable CTEs (every `AS (...)` body, not just the first), `UNION`/`INTO`/`COPY`, multi-statement and comment-hidden injections, and most administrative keywords (`DO`, `SET`, `LISTEN`, `NOTIFY`, `CALL`, `LOAD`, `VACUUM`, `PREPARE`, transaction control, `EXPLAIN ANALYZE`) at the string level. Enforces a read-only **function allowlist** (`ALLOWED_FUNCTIONS`), anything not on it is rejected by name, quoted forms (`"pg_terminate_backend"(…)`) included. Only `SELECT`, `WITH…SELECT`, and plain `EXPLAIN` pass.
|
|
702
708
|
2. `TableGate` refuses any SQL, model, or join that touches a `console_blocked_tables` entry.
|
|
703
|
-
3. `SafeContext` wraps every request in a rolled-back transaction with
|
|
709
|
+
3. `SafeContext` wraps every request in a rolled-back transaction with an adapter-dependent statement timeout. **It does NOT cover async side effects**: ActiveJob `perform_later`, ActionMailer `deliver_later`, direct HTTP egress, `Thread.new`-spawned work, `after_rollback` callbacks, and writes through a different shard all execute as live. Treat the Console MCP as an admin-trust boundary, not a sandbox.
|
|
704
710
|
4. `CredentialScanner` + column/EAV redaction scrub results.
|
|
705
711
|
|
|
706
712
|
Keep the flag off when the host requires a narrower database capability.
|
|
@@ -718,7 +724,7 @@ supported transport (stdio, Docker/SSH launcher, and HTTP).
|
|
|
718
724
|
| 1 | Blocked tables | `console_blocked_tables` | Tool dispatch, before executor | Reject any tool call that touches a named table (model, table, or sql arg) |
|
|
719
725
|
| 2 | Credential scanner | `console_disabled_scanner_patterns` (`[:all]` to disable entirely) | After executor, before render | Content-shape redaction of credential-shaped strings anywhere in the response tree |
|
|
720
726
|
| 3 | Column + EAV redaction | `console_redacted_columns`, `console_redacted_key_values` | After executor, before Layer 2 | Identity-based redaction by column name and by key/value row shape |
|
|
721
|
-
| 4 | SqlValidator + SafeContext | built-in | Inside executor | SQL
|
|
727
|
+
| 4 | SqlValidator + SafeContext | built-in | Inside executor | SQL validation and function allowlist for `console_sql`; transaction rollback for every request |
|
|
722
728
|
|
|
723
729
|
Layers 0–3 are configured via `Woods.configure`. Layer 4 is always on and has no knobs. Observability hooks, `console.table_gate.rejected` for Layer 1, `console.credential_scan.hits` for Layer 2, emit structured log lines via `Woods::Observability::StructuredLogger` so operators can audit enforcement without scraping MCP wire traffic.
|
|
724
730
|
|
|
@@ -754,13 +760,20 @@ boundary remain necessary.
|
|
|
754
760
|
|
|
755
761
|
### Statement timeout
|
|
756
762
|
|
|
757
|
-
|
|
763
|
+
SafeContext attempts a **5000ms** (5-second) timeout. Support depends on the
|
|
764
|
+
adapter and server; an unsupported setting is skipped and logged when a Rails
|
|
765
|
+
logger is available.
|
|
758
766
|
|
|
759
767
|
| Adapter | Mechanism | Scope |
|
|
760
768
|
|---------|-----------|-------|
|
|
761
|
-
| PostgreSQL | `SET statement_timeout = '5000ms'` |
|
|
762
|
-
| MySQL | `SET max_execution_time = 5000`
|
|
763
|
-
|
|
|
769
|
+
| PostgreSQL | `SET LOCAL statement_timeout = '5000ms'` | Transaction-local; discarded on rollback |
|
|
770
|
+
| MySQL | `SET max_execution_time = 5000` | SELECT only; previous session value restored in `ensure` |
|
|
771
|
+
| MariaDB | `SET max_statement_time = 5.0` | Seconds; previous session value restored in `ensure` |
|
|
772
|
+
| SQLite / unrecognized family | No supported per-statement timeout | Do not rely on a query time limit |
|
|
773
|
+
|
|
774
|
+
Rollback remains active when a timeout setting is unsupported. Recognition of a
|
|
775
|
+
MySQL-family adapter alone does not establish that its server supports the
|
|
776
|
+
corresponding timeout variable.
|
|
764
777
|
|
|
765
778
|
### SQL validation (tier 4 `console_sql`)
|
|
766
779
|
|
|
@@ -771,7 +784,7 @@ Validation runs **once**, inside the executor, with the dialect of the live adap
|
|
|
771
784
|
|
|
772
785
|
- **Allowed prefixes:** `SELECT`, `WITH...SELECT`, and plain `EXPLAIN`. `EXPLAIN ANALYZE` is rejected, it executes the query rather than just planning it (both the whitespace and `EXPLAIN (ANALYZE, …)` option-list spellings).
|
|
773
786
|
- **Rejected prefixes:** `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `DROP`, `ALTER`, `TRUNCATE`, `CREATE`, `GRANT`, `REVOKE`
|
|
774
|
-
- **Rejected anywhere in query:** `UNION`, `INTO`, `COPY`; row-lock clauses (`FOR UPDATE`, `FOR NO KEY UPDATE`, `FOR SHARE`, `FOR KEY SHARE`, `FOR UPDATE NOWAIT`/`SKIP LOCKED`, MySQL `LOCK IN SHARE MODE`) — these take live row locks even inside the rolled-back transaction. The lock check is adapter-aware: `console_sql` validates with the active adapter's dialect, including MySQL double-quoted strings/backtick identifiers and PostgreSQL quoted identifiers/E-strings.
|
|
787
|
+
- **Rejected anywhere in query:** `UNION`, `INTO`, `COPY`; row-lock clauses (`FOR UPDATE`, `FOR NO KEY UPDATE`, `FOR SHARE`, `FOR KEY SHARE`, `FOR UPDATE NOWAIT`/`SKIP LOCKED`, MySQL `LOCK IN SHARE MODE`) — these take live row locks even inside the rolled-back transaction. The lock check is adapter-aware: `console_sql` validates with the active adapter's dialect, including MySQL double-quoted strings/backtick identifiers and PostgreSQL quoted identifiers/E-strings. Direct validator calls without a dialect conservatively scan all supported normalizations; packaged `console_sql` refuses unrecognized adapter families. Every view is scanned under both MySQL executable-comment (`/*!...*/`) semantics, so `#` comments and version-guarded comments cannot split a clause apart.
|
|
775
788
|
- **Function allowlist (the authoritative function control):** every function-call-shaped identifier must appear in `ALLOWED_FUNCTIONS`, a conservative set of pure read-only functions (aggregates, window functions, string/number/date/JSON readers) kept portable across MySQL, PostgreSQL, and SQLite. Anything else is rejected by name, quoted forms (`"pg_terminate_backend"(…)`) included. This is an allowlist because a denylist cannot enumerate every side-effecting function (`nextval`, `pg_advisory_lock`, `pg_terminate_backend`, …). A legacy `DANGEROUS_FUNCTIONS` denylist (`pg_sleep`, `lo_import`, `lo_export`, `pg_read_file`, `pg_write_file`, `load_file`, `sleep`, `benchmark`) still runs first as belt-and-suspenders.
|
|
776
789
|
- **Rejected patterns:** multiple statements (semicolons), writable CTEs (every `AS (...)` body is checked, so a writable CTE in any WITH position is refused — `WITH a AS (SELECT 1), b AS (DELETE FROM users RETURNING *) SELECT * FROM b`), a CTE list attached to top-level DML (`WITH a AS (SELECT 1) DELETE FROM users RETURNING *`), comment-hidden injections
|
|
777
790
|
|
|
@@ -902,3 +915,67 @@ These corrections require `2.0.0.beta4` or a reviewed development revision that
|
|
|
902
915
|
contains them. Confirm that a patched release is available before selecting it.
|
|
903
916
|
On affected versions, disable Console where these policies are required; Index MCP
|
|
904
917
|
can stay enabled because it reads the published code index separately.
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
## Maintenance policy corrections
|
|
921
|
+
|
|
922
|
+
The 2.0.1 maintenance patch applies authentication and origin policy to each
|
|
923
|
+
Console HTTP mount. Keep the normal Railtie setup: these additional checks do not
|
|
924
|
+
make an unreviewed manual mount the recommended installation path. Configure
|
|
925
|
+
allowed origins before boot and restart after changes; malformed entries fail
|
|
926
|
+
once at boot with the offending entry identified.
|
|
927
|
+
|
|
928
|
+
Protected collection values are masked as complete cells. EAV key policy checks
|
|
929
|
+
both stored and cast keys, including in predicates and ordering. Raw SQL refuses
|
|
930
|
+
ambiguous protected source identity, protected whole-row projections, positional
|
|
931
|
+
ordering that could expose protected values, and unsupported result types.
|
|
932
|
+
Explicit unaliased scalar columns and the structured tools are the recovery path.
|
|
933
|
+
|
|
934
|
+
SQL dialect detection follows adapter ancestry before adapter names. PostgreSQL
|
|
935
|
+
subclasses, Mysql2, MariaDB, Trilogy and SQLite use their applicable policies.
|
|
936
|
+
Genuinely unknown families are refused only by raw `console_sql`; this restriction
|
|
937
|
+
does not disable structured Tier-1 tools. Existing tool opt-ins remain unchanged.
|
|
938
|
+
|
|
939
|
+
PostgreSQL Unicode-escaped identifiers are refused by `console_sql` before
|
|
940
|
+
execution. Use ordinary identifiers or standard quoted identifiers instead;
|
|
941
|
+
structured query tools are unaffected by this syntax restriction.
|
|
942
|
+
|
|
943
|
+
### Read policy compatibility
|
|
944
|
+
|
|
945
|
+
These rules describe the security-patch source; verify the installed revision
|
|
946
|
+
and loaded gem path until its release is published.
|
|
947
|
+
|
|
948
|
+
- **Column alias lists:** when either `console_redacted_columns` or
|
|
949
|
+
`console_redacted_key_values` is nonempty, `console_sql` refuses relation
|
|
950
|
+
and CTE column alias lists before execution, including lists on base tables,
|
|
951
|
+
derived tables, parenthesized `VALUES` sources and table functions. This applies
|
|
952
|
+
even when the selected names are not protected and independently of function
|
|
953
|
+
validation. Ordinary relation
|
|
954
|
+
aliases, CTEs without column lists and allowed scalar functions remain subject
|
|
955
|
+
to the normal SQL policy. Use explicit, unaliased protected columns or a
|
|
956
|
+
structured Console tool.
|
|
957
|
+
- **Conservative typed masking:** EAV type lookup matches the final source-table
|
|
958
|
+
name case-insensitively and includes every matching registered model, even
|
|
959
|
+
across schemas. Any matching type can cause masking. This deliberately may
|
|
960
|
+
mask extra values when table names differ only by case or share that final
|
|
961
|
+
name; qualifying the table does not narrow that type set.
|
|
962
|
+
- **Exact sensitive values:** `sensitive_keys` compares the stored and cast key
|
|
963
|
+
values with exact case after string conversion. Configure their actual raw or
|
|
964
|
+
cast spelling. `CredentialIndex` also matches credential substrings with exact
|
|
965
|
+
case; it does not decode hexadecimal binary output such as PostgreSQL `bytea`.
|
|
966
|
+
Binary cells have no general text-scanning guarantee: non-UTF-8 data can fail
|
|
967
|
+
JSON normalization before scanning. Put binary secret columns in
|
|
968
|
+
`console_redacted_columns` so they are masked before serialization.
|
|
969
|
+
- **Adapter boundary:** raw `console_sql` requires PostgreSQL, MySQL-family
|
|
970
|
+
(including Mysql2, MariaDB and Trilogy), or SQLite classification. Compatible
|
|
971
|
+
subclasses are recognized by ancestry. Unknown families receive a validation
|
|
972
|
+
refusal for raw SQL; structured tools remain available under their normal
|
|
973
|
+
gates. See [statement timeouts](#statement-timeout) for adapter limits.
|
|
974
|
+
- **SQL functions and select entries:** 2.x enforces a read-only function
|
|
975
|
+
allowlist; 1.6.x retains a function denylist. For `console_query`, provide one
|
|
976
|
+
expression per `select` array entry. 2.x refuses comma-combined entries that
|
|
977
|
+
1.6.4 splits before validation. Execution and typed redaction use the same
|
|
978
|
+
validated projection on all patched lines.
|
|
979
|
+
- **Association counts:** polymorphic `belongs_to` counts remain unsupported
|
|
980
|
+
on this maintenance line and fail closed with a generic execution error.
|
|
981
|
+
The 2.1 functional correction is not included in this backport.
|
data/docs/MCP_HTTP_TRANSPORT.md
CHANGED
|
@@ -86,16 +86,68 @@ Clients must send `Authorization: Bearer $WOODS_MCP_HTTP_TOKEN` on every request
|
|
|
86
86
|
|
|
87
87
|
### Browser origins (DNS rebinding defense)
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
**Unreleased diagnostic correction:** invalid origin encodings also refuse before
|
|
90
|
+
binding HTTP. `woods-mcp-http` exits 2 with one bounded `ConfigurationError`
|
|
91
|
+
message naming the invalid entry; re-enter that origin using an ASCII hostname
|
|
92
|
+
(or its Punycode form). No allowlist keeps the existing defaults. Explicit entries
|
|
93
|
+
are literal origins, not wildcard patterns.
|
|
94
|
+
Retain the actual request Host through a reverse proxy; forwarded headers do not
|
|
95
|
+
replace it. When authentication is configured, requests without Host still pass
|
|
96
|
+
through bearer authentication.
|
|
97
|
+
|
|
98
|
+
In the 2.0.1 maintenance patch, preflight and SDK dispatch share an immutable
|
|
99
|
+
normalized policy. A configured list replaces the default browser origins;
|
|
100
|
+
include loopback explicitly if needed. Cross-origin ports must match an entry,
|
|
101
|
+
while omitted default HTTP(S) ports match their explicit 80/443 forms. A portless
|
|
102
|
+
entry permits same-authority traffic rather than every cross-port browser origin.
|
|
103
|
+
Console defaults remain HTTP loopback; Index HTTP defaults include HTTP and HTTPS
|
|
104
|
+
loopback. When no list is set, these defaults are unchanged. Invalid entries fail
|
|
105
|
+
at boot, naming the offending entry. Restart after changing the configuration.
|
|
106
|
+
|
|
107
|
+
A second middleware, `Woods::MCP::OriginGuard`, rejects requests whose `Origin` header is outside an allow-list. Requests without an `Origin` header still pass through Host validation and configured bearer authentication.
|
|
90
108
|
|
|
91
109
|
| Scenario | `WOODS_MCP_HTTP_ALLOWED_ORIGINS` | Origins accepted |
|
|
92
110
|
|--------------------|-----------------------------------------|-------------------------------------------------------------------|
|
|
93
|
-
| default | unset |
|
|
111
|
+
| default | unset | HTTP(S) loopback, matching request authority; explicit entries for cross-port origins |
|
|
94
112
|
| explicit list | `https://app.example.com` | exactly `https://app.example.com`, loopback no longer allowed |
|
|
95
113
|
| multiple origins | `https://a.example,https://b.example` | each listed origin |
|
|
96
114
|
|
|
97
115
|
`OPTIONS` preflights are answered with the matching `Access-Control-Allow-*` headers; successful responses carry `Access-Control-Allow-Origin` and `Vary: Origin`. `Access-Control-Expose-Headers: Mcp-Session-Id` appears only in legacy session mode (`WOODS_MCP_HTTP_STATELESS=0`).
|
|
98
116
|
|
|
117
|
+
### Origin configuration compatibility
|
|
118
|
+
|
|
119
|
+
These details apply to the supporting security-patch revisions described above.
|
|
120
|
+
|
|
121
|
+
The Index HTTP launcher names `WOODS_MCP_HTTP_ALLOWED_ORIGINS` and the offending
|
|
122
|
+
entry when refusing malformed origin configuration, including under a POSIX
|
|
123
|
+
(`C`) locale. It emits one diagnostic before index resolution or HTTP binding.
|
|
124
|
+
|
|
125
|
+
Configured origins are literal values, with lowercasing, one trailing slash
|
|
126
|
+
removed and HTTP(S) default-port normalization. Wildcard-looking hostnames are
|
|
127
|
+
literal hostnames, not patterns. A nonempty explicit list replaces browser-origin
|
|
128
|
+
defaults, including loopback; add the loopback browser origins you need. Console's
|
|
129
|
+
configured defaults are HTTP loopback; the Index defaults include HTTP and HTTPS
|
|
130
|
+
loopback. Loopback **Host** acceptance is separate from browser-origin acceptance.
|
|
131
|
+
|
|
132
|
+
Ruby-configured 2.x origin entries reject surrounding whitespace; 1.6.4 trims it.
|
|
133
|
+
The `woods-mcp-http` comma-separated environment setting trims each entry on both
|
|
134
|
+
lines. Use whitespace-free values for portable configuration. The exact-port and
|
|
135
|
+
same-authority rules above still apply; literal matching does not imply wildcard
|
|
136
|
+
or arbitrary cross-port access.
|
|
137
|
+
|
|
138
|
+
The guards read the actual `Origin` and `Host`; `Forwarded` and
|
|
139
|
+
`X-Forwarded-*` do not replace them. Preserve the public Host through a proxy.
|
|
140
|
+
A genuinely absent Host is accepted by the Host policy, but any supplied Origin
|
|
141
|
+
must still pass its own check. Console requests and token-configured Index
|
|
142
|
+
requests still require bearer authentication before dispatch. The loopback-only
|
|
143
|
+
Index mode without a token retains its documented unauthenticated behavior;
|
|
144
|
+
CORS preflights do not dispatch tools.
|
|
145
|
+
|
|
146
|
+
Malformed Origin/Host values receive constant `403` responses without reflecting
|
|
147
|
+
the values. Invalid Authorization receives `401` when it reaches the auth guard.
|
|
148
|
+
Malformed HTTP framing can instead receive Puma's `400` before Rack runs. Other
|
|
149
|
+
HTTP servers may reject framing at their own boundary.
|
|
150
|
+
|
|
99
151
|
### TLS termination
|
|
100
152
|
|
|
101
153
|
The server speaks plain HTTP. Any deployment beyond a single trusted host should front it with a reverse proxy that handles TLS, HTTP/2, and connection limits.
|
data/docs/UPGRADING_TO_2.md
CHANGED
|
@@ -8,6 +8,35 @@ published 1.6.x security patch as the rollback version.
|
|
|
8
8
|
<!-- release-state:upgrade-availability -->
|
|
9
9
|
<!-- release-state:end -->
|
|
10
10
|
|
|
11
|
+
## 2.0.1 security maintenance update
|
|
12
|
+
|
|
13
|
+
This maintenance line adds Console request/output policy corrections and isolates
|
|
14
|
+
retrieval contexts between retriever instances. It does not add the graph or
|
|
15
|
+
extraction features under development for 2.1. Confirm the installed package
|
|
16
|
+
version and use its matching tag documentation.
|
|
17
|
+
|
|
18
|
+
No index or database schema migration is required. Restart Console/MCP processes
|
|
19
|
+
after upgrading. Explicit malformed HTTP origin entries now fail at boot with the
|
|
20
|
+
offending entry named; fix the entry rather than weakening authentication.
|
|
21
|
+
Automatic and manual Console mounts raise `Woods::ConfigurationError` for these
|
|
22
|
+
settings, including invalidly encoded entries. With
|
|
23
|
+
no allowlist configured, the existing loopback defaults remain unchanged. See
|
|
24
|
+
[HTTP origin matching](MCP_HTTP_TRANSPORT.md#browser-origins-dns-rebinding-defense).
|
|
25
|
+
|
|
26
|
+
Raw `console_sql` now refuses ambiguous protected results and genuinely unknown
|
|
27
|
+
adapter families. PostgreSQL-subclass adapters retain PostgreSQL handling;
|
|
28
|
+
structured Console tools remain available with other adapters. Prefer explicit
|
|
29
|
+
unaliased scalar projections or structured reads when a query is refused.
|
|
30
|
+
[Console setup](CONSOLE_MCP_SETUP.md#maintenance-policy-corrections) describes the
|
|
31
|
+
compatibility boundary. Context caches refill after restart; retired entries
|
|
32
|
+
follow the configured TTL or backend eviction. See
|
|
33
|
+
[retrieval cache options](CONFIGURATION_REFERENCE.md#retrieval-cache-options).
|
|
34
|
+
Rolling back restores the affected behavior.
|
|
35
|
+
|
|
36
|
+
Polymorphic `belongs_to` association counts remain unsupported in 2.0.1 and
|
|
37
|
+
1.6.4 and return a generic execution error; the 2.1 functional correction is
|
|
38
|
+
not backported.
|
|
39
|
+
|
|
11
40
|
## Upgrade outcome
|
|
12
41
|
|
|
13
42
|
After this runbook you will have:
|
|
@@ -19,6 +48,37 @@ After this runbook you will have:
|
|
|
19
48
|
- an MCP client connected to the v2 packaged tool surface;
|
|
20
49
|
- a documented way back to v1 if verification fails.
|
|
21
50
|
|
|
51
|
+
### Console read compatibility
|
|
52
|
+
|
|
53
|
+
For supporting security-patch revisions, any nonempty column or EAV redaction
|
|
54
|
+
policy makes `console_sql` refuse relation and CTE column alias lists, including
|
|
55
|
+
lists on base tables, derived tables, parenthesized `VALUES` sources and table
|
|
56
|
+
functions, regardless of the selected names. Use explicit, unaliased protected
|
|
57
|
+
columns or structured tools.
|
|
58
|
+
Typed EAV lookup includes all registered models with a case-insensitive matching
|
|
59
|
+
final table name, including across schemas. It can therefore mask extra values;
|
|
60
|
+
qualification does not narrow this conservative type set. Sensitive key values
|
|
61
|
+
still require their exact stored or cast spelling.
|
|
62
|
+
|
|
63
|
+
The 1.6.x function denylist becomes a read-only function allowlist in 2.x.
|
|
64
|
+
Supply one `console_query` expression per `select` array entry: 2.x refuses
|
|
65
|
+
comma-combined entries that 1.6.4 splits. Raw SQL requires a recognized adapter
|
|
66
|
+
family; structured tools remain available on other adapters. Configure binary
|
|
67
|
+
secret columns explicitly for redaction. See the canonical
|
|
68
|
+
[read policy compatibility](CONSOLE_MCP_SETUP.md#read-policy-compatibility) and
|
|
69
|
+
[statement timeouts](CONSOLE_MCP_SETUP.md#statement-timeout) for limits.
|
|
70
|
+
|
|
71
|
+
Supporting Console HTTP revisions deliberately permit allowlisted non-loopback
|
|
72
|
+
Hosts through the SDK where 2.0.0 could refuse them. Bearer authentication remains
|
|
73
|
+
required. An explicit origin list replaces browser-origin defaults; wildcards
|
|
74
|
+
are not supported. Ruby-configured 2.x origins reject surrounding whitespace
|
|
75
|
+
that 1.6.4 trims; the HTTP executable trims comma-separated environment entries
|
|
76
|
+
on both lines. Review [HTTP origin configuration](MCP_HTTP_TRANSPORT.md#origin-configuration-compatibility).
|
|
77
|
+
|
|
78
|
+
Context-cache namespace rotation retires entries for normal TTL or backend
|
|
79
|
+
eviction; disabling both can retain them indefinitely. See
|
|
80
|
+
[retrieval cache options](CONFIGURATION_REFERENCE.md#retrieval-cache-options).
|
|
81
|
+
|
|
22
82
|
## What changes
|
|
23
83
|
|
|
24
84
|
| v2 change | What can break | Required response |
|
data/exe/woods-mcp-http
CHANGED
|
@@ -46,6 +46,20 @@ require_relative '../lib/woods/embedding/text_preparer'
|
|
|
46
46
|
require_relative '../lib/woods/embedding/indexer'
|
|
47
47
|
|
|
48
48
|
begin
|
|
49
|
+
raw_origins = ENV.fetch('WOODS_MCP_HTTP_ALLOWED_ORIGINS', '')
|
|
50
|
+
unless raw_origins.valid_encoding?
|
|
51
|
+
invalid_entry = raw_origins.b.split(',').find do |entry|
|
|
52
|
+
!entry.dup.force_encoding(raw_origins.encoding).valid_encoding?
|
|
53
|
+
end
|
|
54
|
+
label = invalid_entry.to_s.b.byteslice(0, 160).inspect
|
|
55
|
+
raise Woods::ConfigurationError, "Invalid WOODS_MCP_HTTP_ALLOWED_ORIGINS entry #{label}: invalid encoding"
|
|
56
|
+
end
|
|
57
|
+
allowed_origins = raw_origins.split(',').map(&:strip).reject(&:empty?)
|
|
58
|
+
begin
|
|
59
|
+
origin_policy = Woods::MCP::OriginPolicy.new(allowed_origins: allowed_origins)
|
|
60
|
+
rescue ArgumentError => e
|
|
61
|
+
raise Woods::ConfigurationError, "#{e.message} (WOODS_MCP_HTTP_ALLOWED_ORIGINS)"
|
|
62
|
+
end
|
|
49
63
|
index_dir = Woods::MCP::Bootstrapper.resolve_index_dir(ARGV)
|
|
50
64
|
retriever, bootstrap_state = Woods::MCP::Bootstrapper.build_retriever(index_dir: index_dir)
|
|
51
65
|
snapshot_store = Woods::MCP::Bootstrapper.build_snapshot_store(index_dir)
|
|
@@ -96,17 +110,10 @@ server = Woods::MCP::Server.build(
|
|
|
96
110
|
# hatch is transitional — the spec has removed all three. See
|
|
97
111
|
# docs/MCP_HTTP_TRANSPORT.md#statelessness.
|
|
98
112
|
stateless = !%w[0 false no].include?(ENV.fetch('WOODS_MCP_HTTP_STATELESS', '1').strip.downcase)
|
|
99
|
-
allowed_origins = ENV.fetch('WOODS_MCP_HTTP_ALLOWED_ORIGINS', '').split(',').map(&:strip).reject(&:empty?)
|
|
100
|
-
allowed_hosts = allowed_origins.filter_map do |origin|
|
|
101
|
-
URI.parse(origin).host
|
|
102
|
-
rescue URI::InvalidURIError
|
|
103
|
-
nil
|
|
104
|
-
end
|
|
105
113
|
transport = MCP::Server::Transports::StreamableHTTPTransport.new(
|
|
106
114
|
server,
|
|
107
115
|
stateless: stateless,
|
|
108
|
-
|
|
109
|
-
allowed_hosts: allowed_hosts
|
|
116
|
+
**origin_policy.transport_options
|
|
110
117
|
)
|
|
111
118
|
server.transport = transport
|
|
112
119
|
|
|
@@ -116,7 +123,7 @@ inner = proc do |env|
|
|
|
116
123
|
transport.handle_request(Rack::Request.new(env))
|
|
117
124
|
end
|
|
118
125
|
app = token ? Woods::MCP::BearerAuth.new(inner, token: token) : inner
|
|
119
|
-
app = Woods::MCP::OriginGuard.new(app,
|
|
126
|
+
app = Woods::MCP::OriginGuard.new(app, policy: origin_policy)
|
|
120
127
|
|
|
121
128
|
origin_summary = allowed_origins.empty? ? 'loopback' : allowed_origins.join(',')
|
|
122
129
|
auth_mode = token ? 'bearer' : 'none'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'digest'
|
|
4
|
+
require 'securerandom'
|
|
4
5
|
require_relative 'cache_store'
|
|
5
6
|
# CachedEmbeddingProvider includes Embedding::Provider::Interface at load
|
|
6
7
|
# time, so the interface must be defined before this file's class bodies run
|
|
@@ -408,6 +409,8 @@ module Woods
|
|
|
408
409
|
@retriever = retriever
|
|
409
410
|
@cache_store = cache_store
|
|
410
411
|
@context_ttl = context_ttl
|
|
412
|
+
@context_namespace = SecureRandom.hex(16)
|
|
413
|
+
@context_mutex = Mutex.new
|
|
411
414
|
end
|
|
412
415
|
|
|
413
416
|
# Expose the wrapped stores so the MCP +reload+ tool and
|
|
@@ -427,20 +430,17 @@ module Woods
|
|
|
427
430
|
@retriever.corpus_status(include_types: include_types) if @retriever.respond_to?(:corpus_status)
|
|
428
431
|
end
|
|
429
432
|
|
|
430
|
-
#
|
|
431
|
-
# tool after the retriever's stores have been re-hydrated from a fresh
|
|
432
|
-
# embed — otherwise cached results from the old embedding run would
|
|
433
|
-
# linger until their TTL expires and contradict the new stores.
|
|
433
|
+
# Retire this retriever's cached contexts after its corpus reloads.
|
|
434
434
|
#
|
|
435
|
-
#
|
|
436
|
-
#
|
|
437
|
-
#
|
|
435
|
+
# Each retriever has its own unpredictable namespace, even when several
|
|
436
|
+
# applications share a backend. Rotating it also prevents an in-flight
|
|
437
|
+
# request from repopulating the active cache with a pre-reload result.
|
|
438
|
+
# Old entries expire by their configured TTL; no global backend deletion
|
|
439
|
+
# is needed. Embedding caches are independent and remain available.
|
|
438
440
|
#
|
|
439
441
|
# @return [void]
|
|
440
442
|
def invalidate_context_cache!
|
|
441
|
-
@
|
|
442
|
-
rescue StandardError => e
|
|
443
|
-
warn("[Woods] CachedRetriever context-cache invalidation failed: #{e.message}")
|
|
443
|
+
@context_mutex.synchronize { @context_namespace = SecureRandom.hex(16) }
|
|
444
444
|
end
|
|
445
445
|
|
|
446
446
|
# Execute the retrieval pipeline with context-level caching.
|
|
@@ -492,7 +492,8 @@ module Woods
|
|
|
492
492
|
# @param exclude_types [Array<String, Symbol>, nil]
|
|
493
493
|
# @return [String]
|
|
494
494
|
def context_key(query, budget, types: nil, exclude_types: nil, packages: nil, source_paths: nil, evidence: 'full') # rubocop:disable Metrics/ParameterLists
|
|
495
|
-
|
|
495
|
+
namespace = @context_mutex.synchronize { @context_namespace }
|
|
496
|
+
parts = [namespace, query, budget.to_s, fingerprint(types), fingerprint(exclude_types)]
|
|
496
497
|
parts << 'lexical' if mode == :lexical
|
|
497
498
|
parts << JSON.generate(evidence: evidence) unless evidence == 'full'
|
|
498
499
|
if Retrieval::Scope.requested?(packages: packages, source_paths: source_paths)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Woods
|
|
4
|
+
module Console
|
|
5
|
+
# Adapter names differ from the SQL grammar and session settings they use.
|
|
6
|
+
module AdapterFamily
|
|
7
|
+
# Classify a live connection by adapter ancestry, database configuration,
|
|
8
|
+
# then its display name. Unknown families remain nil: SQL callers must
|
|
9
|
+
# refuse rather than silently omit dialect-specific safeguards.
|
|
10
|
+
# @param connection [Object] Active Record connection or compatible adapter
|
|
11
|
+
# @return [Symbol, nil] :postgres, :mysql, :sqlite, or unknown
|
|
12
|
+
def self.for(connection)
|
|
13
|
+
ancestry = connection.class.ancestors.filter_map(&:name)
|
|
14
|
+
return :postgres if ancestry.include?('ActiveRecord::ConnectionAdapters::PostgreSQLAdapter')
|
|
15
|
+
return :mysql if ancestry.any? { |name| name.match?(/::(?:AbstractMysql|Mysql2|Trilogy)Adapter\z/) }
|
|
16
|
+
return :sqlite if ancestry.include?('ActiveRecord::ConnectionAdapters::SQLite3Adapter')
|
|
17
|
+
|
|
18
|
+
from_name(configured_adapter(connection)) || from_name(connection.adapter_name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.configured_adapter(connection)
|
|
22
|
+
return unless connection.respond_to?(:pool) && connection.pool.respond_to?(:db_config)
|
|
23
|
+
|
|
24
|
+
connection.pool.db_config.adapter
|
|
25
|
+
end
|
|
26
|
+
private_class_method :configured_adapter
|
|
27
|
+
|
|
28
|
+
def self.from_name(value)
|
|
29
|
+
name = value.to_s.downcase
|
|
30
|
+
return :mysql if name.include?('mysql') || %w[trilogy mariadb].include?(name)
|
|
31
|
+
return :postgres if name.include?('postgre') || %w[postgis cockroachdb redshift].include?(name)
|
|
32
|
+
return :sqlite if name.include?('sqlite')
|
|
33
|
+
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
private_class_method :from_name
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -46,7 +46,7 @@ module Woods
|
|
|
46
46
|
# index.redact("token: sk_live_actual_secret_value")
|
|
47
47
|
# # => "token: [REDACTED:credential]"
|
|
48
48
|
#
|
|
49
|
-
class CredentialIndex
|
|
49
|
+
class CredentialIndex # rubocop:disable Metrics/ClassLength
|
|
50
50
|
# Captured at require time so the mtime-check warning has a stable
|
|
51
51
|
# reference point even if the clock skews later. Frozen immediately
|
|
52
52
|
# to prevent accidental mutation.
|
|
@@ -187,7 +187,10 @@ module Woods
|
|
|
187
187
|
def initialize(secrets:)
|
|
188
188
|
filtered = Array(secrets).select { |s| s.is_a?(String) && s.length >= MIN_LENGTH }
|
|
189
189
|
@secrets = filtered.to_set.freeze
|
|
190
|
-
|
|
190
|
+
# Regexp alternatives match in order: a shorter prefix must not consume
|
|
191
|
+
# only the start of a longer indexed credential and expose its suffix.
|
|
192
|
+
longest_first = @secrets.each_with_index.sort_by { |secret, idx| [-secret.length, idx] }.map(&:first)
|
|
193
|
+
@pattern = @secrets.empty? ? nil : Regexp.union(longest_first)
|
|
191
194
|
end
|
|
192
195
|
|
|
193
196
|
# @return [Boolean] true when no secrets were collected (missing key,
|
|
@@ -212,7 +215,34 @@ module Woods
|
|
|
212
215
|
def redact(str)
|
|
213
216
|
return str if empty? || !str.is_a?(String) || !@pattern.match?(str)
|
|
214
217
|
|
|
215
|
-
|
|
218
|
+
offset = 0
|
|
219
|
+
parts = []
|
|
220
|
+
redaction_spans(str).each do |start, finish|
|
|
221
|
+
parts << str[offset...start] << REDACTED
|
|
222
|
+
offset = finish
|
|
223
|
+
end
|
|
224
|
+
parts << str[offset..]
|
|
225
|
+
parts.join
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
# Search from each match's start, rather than its end, so an overlapping
|
|
231
|
+
# credential cannot expose its suffix after an earlier replacement.
|
|
232
|
+
# Union the covered spans before changing the original string.
|
|
233
|
+
def redaction_spans(str)
|
|
234
|
+
spans = []
|
|
235
|
+
offset = 0
|
|
236
|
+
while (match = @pattern.match(str, offset))
|
|
237
|
+
start, finish = match.offset(0)
|
|
238
|
+
if spans.last && start < spans.last[1]
|
|
239
|
+
spans.last[1] = [spans.last[1], finish].max
|
|
240
|
+
else
|
|
241
|
+
spans << [start, finish]
|
|
242
|
+
end
|
|
243
|
+
offset = start + 1
|
|
244
|
+
end
|
|
245
|
+
spans
|
|
216
246
|
end
|
|
217
247
|
end
|
|
218
248
|
end
|