Bump docker/setup-buildx-action from 3 to 4#2
Closed
dependabot[bot] wants to merge 1 commit into
Closed
Conversation
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](docker/setup-buildx-action@v3...v4) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <[email protected]>
Contributor
Author
LabelsThe following labels could not be found: Please fix the above issues or remove invalid values from |
Contributor
Author
|
OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting If you change your mind, just re-open this PR and I'll resolve any conflicts on it. |
dependabot
Bot
deleted the
dependabot/github_actions/docker/setup-buildx-action-4
branch
March 12, 2026 01:59
This was referenced Mar 15, 2026
8 tasks
This was referenced Apr 5, 2026
5 tasks
houko
added a commit
that referenced
this pull request
Apr 14, 2026
Three real issues found while reviewing the original commit on this branch: 1. **`/api/status` still reachable.** The status handler at routes/config.rs:48 returns the full agents listing (id, name, state, model provider, profile) plus `home_dir`, `api_listen`, session count and memory usage — exactly the enumeration surface `require_auth_for_reads` exists to close. The original commit kept it in `always_public_method_free`, so flipping the flag did not actually stop `/api/status` from leaking. Moved it into `dashboard_read_exact` (still GET-only) so the flag locks it down. 2. **Flag silently no-ops when only user_api_keys or dashboard credentials are configured.** The gate was `require_auth_for_reads && api_key_present` where `api_key_present = !api_key.trim().is_empty()`. Production deployments commonly configure per-user keys or dashboard user/pass without a standalone api_key — in those cases, flipping the flag did nothing. Replaced with the full "any auth configured" predicate: `api_key || !user_api_keys.is_empty() || dashboard_auth_enabled`, which mirrors the existing auth-bypass check a few lines below. 3. **No operator feedback when flag is on but no auth exists.** If `require_auth_for_reads = true` is set without any auth configured, the flag is (correctly) a no-op at the middleware layer — but the operator gets no signal. Added a startup `warn!` in `build_router` so the misconfig is visible in logs at boot. Tests added: - `test_require_auth_for_reads_blocks_api_status` — locks in the fix for #1. - `test_require_auth_for_reads_engages_with_user_api_keys_only` — locks in the fix for #2 (both the 401 on missing creds and the 200 on a valid per-user key). - `test_require_auth_for_reads_is_noop_without_any_auth` — pins the middleware contract for #3 (the warning handles UX, middleware stays permissive when no auth backends exist). Verified: `cargo test -p librefang-api --lib middleware` — 20 passed (7 new for this feature) and `cargo clippy -p librefang-api -p librefang-types --all-targets -- -D warnings` clean.
3 tasks
houko
added a commit
that referenced
this pull request
Apr 14, 2026
…2398) * feat(api): add require_auth_for_reads flag to lock down dashboard reads The dashboard public-read allowlist in the auth middleware hard-codes /api/agents, /api/config, /api/budget, /api/sessions, /api/approvals, /api/hands, /api/skills, /api/workflows and more as GET-public so the SPA can render before the user enters credentials. With the default api_listen = "0.0.0.0:4545", any host on the LAN can enumerate agents, read configuration (minus redacted api_key), observe spend, and list pending approvals without a token — a hard mismatch with the "Bearer token authentication" line in SECURITY.md. Introduce KernelConfig.require_auth_for_reads (default false, so existing deployments keep rendering unchanged). When it is true AND api_key is configured, the middleware collapses the allowlist to the static-asset / OAuth / health subset and forces every dashboard read through bearer authentication. Unauthenticated health probes, OAuth callback, and dashboard shell HTML remain reachable. Refactor the public-path match into matches!()-based groups so clippy (nonminimal_bool) stays quiet and the two tiers are obviously separated in code review. Regression coverage: - require_auth_for_reads=true blocks unauthenticated GET /api/agents - require_auth_for_reads=true still allows the correct bearer - /api/health stays public with the flag on - require_auth_for_reads=false preserves the legacy public GET * fix(api): close require_auth_for_reads gaps found in self-review Three real issues found while reviewing the original commit on this branch: 1. **`/api/status` still reachable.** The status handler at routes/config.rs:48 returns the full agents listing (id, name, state, model provider, profile) plus `home_dir`, `api_listen`, session count and memory usage — exactly the enumeration surface `require_auth_for_reads` exists to close. The original commit kept it in `always_public_method_free`, so flipping the flag did not actually stop `/api/status` from leaking. Moved it into `dashboard_read_exact` (still GET-only) so the flag locks it down. 2. **Flag silently no-ops when only user_api_keys or dashboard credentials are configured.** The gate was `require_auth_for_reads && api_key_present` where `api_key_present = !api_key.trim().is_empty()`. Production deployments commonly configure per-user keys or dashboard user/pass without a standalone api_key — in those cases, flipping the flag did nothing. Replaced with the full "any auth configured" predicate: `api_key || !user_api_keys.is_empty() || dashboard_auth_enabled`, which mirrors the existing auth-bypass check a few lines below. 3. **No operator feedback when flag is on but no auth exists.** If `require_auth_for_reads = true` is set without any auth configured, the flag is (correctly) a no-op at the middleware layer — but the operator gets no signal. Added a startup `warn!` in `build_router` so the misconfig is visible in logs at boot. Tests added: - `test_require_auth_for_reads_blocks_api_status` — locks in the fix for #1. - `test_require_auth_for_reads_engages_with_user_api_keys_only` — locks in the fix for #2 (both the 401 on missing creds and the 200 on a valid per-user key). - `test_require_auth_for_reads_is_noop_without_any_auth` — pins the middleware contract for #3 (the warning handles UX, middleware stays permissive when no auth backends exist). Verified: `cargo test -p librefang-api --lib middleware` — 20 passed (7 new for this feature) and `cargo clippy -p librefang-api -p librefang-types --all-targets -- -D warnings` clean. * fix(api): also lock /api/health/detail behind require_auth_for_reads Follow-up finding from self-review. `/api/health/detail`'s own handler doc comment at routes/config.rs:317 says "requires auth", but the middleware allowlist had it in the always-public set. The handler returns operational data that should not be reachable from a cold probe: - `panic_count` / `restart_count` from the supervisor - `agent_count` - `embedding_provider` / `embedding_model` / `extraction_model` (infra leak) - `config_warnings` — the full output of `KernelConfig::validate()`, which can tell a remote attacker exactly what's wrong with the deployment - event-bus `dropped_events` count Moved to `dashboard_read_exact` so it gets locked down when the flag is on. `/api/health` stays public because its payload is genuinely minimal (`status`, `version`, two-item `checks` array) and load balancers / orchestrators need it for probing. Test added: `test_require_auth_for_reads_blocks_api_health_detail` — pins both contracts (/api/health stays public, /api/health/detail becomes auth-required) in a single test. Note: this is a partial fix. With the flag OFF, `/api/health/detail` stays public to preserve backwards compatibility, which means the handler's own "requires auth" doc comment is still being violated in the default configuration. Making it always-require-auth is a separate behavioural change that belongs in its own PR. Tests: `cargo test -p librefang-api --lib middleware` — 21 passed (8 for require_auth_for_reads, up from 7). * fix(api): close residual info leaks in unauthenticated endpoints Two more findings from self-review of the always-public set: 1. **`/api/auth/dashboard-check` echoed the configured dashboard username to anonymous callers.** The SPA uses this endpoint before the user has logged in (to pick the right login form), so the route is legitimately unauthenticated — but returning `"username": "<admin>"` handed an anonymous remote caller one half of the credential pair, enabling targeted credential stuffing against `/auth/dashboard-login`. The `mode` field is sufficient for the SPA to pick the right login form; the user already knows their own username. Now always returns an empty string. 2. **`/api/version` echoed the machine hostname.** Version endpoints conventionally expose build info, but the hostname is a per-machine identifier that lets a remote probe correlate a daemon to a specific deployment target. Dropped from the payload. Operators who need the hostname should read it from the daemon's shell environment. No tests referenced either field, `cargo test -p librefang-api --lib` passes with 262 tests (no behaviour change for the dashboard SPA beyond needing the user to type their username at login time, which is how every other dashboard already works). Residual pre-existing gap noted for follow-up: `/api/health/detail`'s own doc comment says it "requires auth" but the middleware kept it public until this PR's flag was added. With the flag off it's still public, honouring backwards compatibility for existing operator probes. Making it unconditionally auth-required is a separate behavioural change. * fix(api): make /api/health/detail always require auth, matching its doc The handler at routes/config.rs:317 documents itself as "Full health diagnostics (requires auth)" and `/api/health`'s doc explicitly says "Use GET /api/health/detail for full diagnostics (requires auth)". The middleware was the only thing still treating it as public — a pre-existing mismatch that this PR's first pass only partially closed by flag-gating. Remove it from both `always_public` and `dashboard_read_exact`. With the endpoint in neither public list, the middleware's default auth-required path handles it, so `/api/health/detail` now requires a bearer token regardless of `require_auth_for_reads`. The handler contract and the middleware contract finally agree. Breaking change for operators: if anyone was probing `/api/health/detail` without auth, they'll start getting 401. `/api/health` (minimal liveness) stays public for load balancers and orchestrators, so the standard deployment probe path is unaffected. Monitoring setups that want the detailed view should configure a bearer token — that was the original design intent. Test replaced: `test_api_health_detail_always_requires_auth` now pins both directions — `/api/health` stays public with the flag off, and `/api/health/detail` is 401 regardless of whether the flag is on or off. Tests: `cargo test -p librefang-api --lib middleware` — 21 passed.
houko
added a commit
that referenced
this pull request
Apr 14, 2026
…uired The original code had two parallel paths to pull \`www_authenticate_header\` out of an \`rmcp::ClientInitializeError\`, and both were dead code: 1. \`extract_auth_required\` walked \`std::error::Error::source()\` hoping to downcast to \`StreamableHttpError::AuthRequired\`, but \`ClientInitializeError::TransportError::error\` is not annotated with \`#[source]\`, so \`source()\` never reaches inside \`DynamicTransportError\`. A \`TODO(rmcp)\` comment blamed the missing annotation, but (a) that's hypothetical future work and (b) it isn't the right diagnosis — see #2. 2. \`extract_www_authenticate\` scraped \`e.to_string()\` for a \`www_authenticate_header: "..."\` marker. Also dead in practice: rmcp's \`Display\` for \`StreamableHttpError::AuthRequired\` is literally \`#[error("Auth required")]\` — the field name never appears in any Display layer of the chain, only in \`Debug\`. The scraper looked busy but never returned \`Some(_)\`. Net effect: every auth-required MCP handshake silently went into OAuth discovery with \`www_authenticate = None\`, so Tier 1 discovery (resource_metadata from the header) was permanently skipped and we always fell through to Tier 2 (\`.well-known\` on the server URL). Fix: \`DynamicTransportError\` already exposes its \`pub error: Box<dyn Error + Send + Sync>\` publicly, so we can match \`ClientInitializeError::TransportError { error, .. }\` directly, then \`downcast_ref::<StreamableHttpError<reqwest::Error>>()\` on the box. The downcast reaches \`AuthRequired\` without any \`source()\` traversal or upstream annotation changes. - Replace both dead helpers with a single \`extract_auth_header_from_error\` that does the direct match + downcast. - Keep the substring check (\`"401"\` / \`"Unauthorized"\` / \`"Auth required"\`) only as a defensive fallback so we don't regress if rmcp ever reshapes its chain. - Drop the \`TODO(rmcp)\` comment (no longer blocked on upstream). - Replace the two old unit tests with \`test_extract_auth_header_from_error_returns_none_for_non_transport_variant\`. The positive case can't be constructed from outside rmcp because \`AuthRequiredError\` is \`#[non_exhaustive]\`, but the negative-path test pins the "bail out on the wrong variant" invariant. Closes #2401, #2402 — the bot issues auto-generated from the TODO comment. The TODO is gone and the functionality it promised is now actually present. Verified: \`cargo test -p librefang-runtime-mcp --lib\` — 49 passed. \`cargo clippy -p librefang-runtime-mcp --all-targets -- -D warnings\` — clean.
houko
added a commit
that referenced
this pull request
Apr 14, 2026
…uired (#2429) The original code had two parallel paths to pull \`www_authenticate_header\` out of an \`rmcp::ClientInitializeError\`, and both were dead code: 1. \`extract_auth_required\` walked \`std::error::Error::source()\` hoping to downcast to \`StreamableHttpError::AuthRequired\`, but \`ClientInitializeError::TransportError::error\` is not annotated with \`#[source]\`, so \`source()\` never reaches inside \`DynamicTransportError\`. A \`TODO(rmcp)\` comment blamed the missing annotation, but (a) that's hypothetical future work and (b) it isn't the right diagnosis — see #2. 2. \`extract_www_authenticate\` scraped \`e.to_string()\` for a \`www_authenticate_header: "..."\` marker. Also dead in practice: rmcp's \`Display\` for \`StreamableHttpError::AuthRequired\` is literally \`#[error("Auth required")]\` — the field name never appears in any Display layer of the chain, only in \`Debug\`. The scraper looked busy but never returned \`Some(_)\`. Net effect: every auth-required MCP handshake silently went into OAuth discovery with \`www_authenticate = None\`, so Tier 1 discovery (resource_metadata from the header) was permanently skipped and we always fell through to Tier 2 (\`.well-known\` on the server URL). Fix: \`DynamicTransportError\` already exposes its \`pub error: Box<dyn Error + Send + Sync>\` publicly, so we can match \`ClientInitializeError::TransportError { error, .. }\` directly, then \`downcast_ref::<StreamableHttpError<reqwest::Error>>()\` on the box. The downcast reaches \`AuthRequired\` without any \`source()\` traversal or upstream annotation changes. - Replace both dead helpers with a single \`extract_auth_header_from_error\` that does the direct match + downcast. - Keep the substring check (\`"401"\` / \`"Unauthorized"\` / \`"Auth required"\`) only as a defensive fallback so we don't regress if rmcp ever reshapes its chain. - Drop the \`TODO(rmcp)\` comment (no longer blocked on upstream). - Replace the two old unit tests with \`test_extract_auth_header_from_error_returns_none_for_non_transport_variant\`. The positive case can't be constructed from outside rmcp because \`AuthRequiredError\` is \`#[non_exhaustive]\`, but the negative-path test pins the "bail out on the wrong variant" invariant. Closes #2401, #2402 — the bot issues auto-generated from the TODO comment. The TODO is gone and the functionality it promised is now actually present. Verified: \`cargo test -p librefang-runtime-mcp --lib\` — 49 passed. \`cargo clippy -p librefang-runtime-mcp --all-targets -- -D warnings\` — clean.
6 tasks
houko
pushed a commit
that referenced
this pull request
Apr 14, 2026
…2503) * feat(gateway): add lib/identity.js identity normalization module (ID-01) - Pure functional module: isLidJid, isGroupJid, normalizeDeviceScopedJid, extractE164, phoneToJid, resolvePeerId, deriveOwnerJids - No side effects on require, no hidden state, cache passed as param - resolvePeerId honors 5-step heuristic from CONTEXT §A Specifics - Unit tests: 41 assertions across 7 describe blocks * refactor(gateway): import lib/identity in index.js Add import of identity helpers next to existing lib/echo-tracker import. No call-site changes yet — subsequent commits migrate sites one at a time. * refactor(gateway): use deriveOwnerJids for OWNER_JIDS Replace inline Set(map(n => n.replace(/^\+/, '') + '@s.whatsapp.net')) with the module helper. Behavior-preserving. * refactor(gateway): use phoneToJid for outbound JID normalization Unify the 3 identical inline copies in sendMessage/sendImage/sendAudio (they were verbatim duplicates; diff confirmed identical). Group JIDs still passthrough, phones still coerced to @s.whatsapp.net form. * refactor(gateway): use resolvePeerId + extractE164 in main cascade (ID-01, ID-03) - Main sender-resolution cascade (~line 1171-1197) replaced with a single resolvePeerId() call that honors the 5-step heuristic from CONTEXT §A. - Phone derivation via extractE164() — also fixes latent bug where device-scoped incoming JIDs ('123:[email protected]') previously yielded malformed '+123:45' phone strings; now correctly yields '+123'. - console.warn for unresolved identity becomes structured JSON per ID-03: {event: 'identity_unresolved', jid, reason, lid_cache_size, confidence}. - Preserved side effects outside the pure function per §Concerns #1: senderPn cache write and CS-02 resolveLidProactively both still run BEFORE resolvePeerId — the function only resolves, it never writes. - Renamed local 'isLidJid' (shadowed module fn) to 'isLid' per §Concerns #2 — all downstream references (ownerLidJids check at line ~1211) updated to the alias. * refactor(gateway): use identity helpers in catchup path - isLidJid/isGroupJid module fns replace inline endsWith checks. - phoneToJid replaces inline '+'-strip + '@s.whatsapp.net' append. * refactor(gateway): use isGroupJid in getGroupParticipants guard Final inline endsWith('@g.us') site removed. Zero inline JID suffix manipulation remains in index.js. * test(gateway): equivalence + ID-03 log tests for identity refactor - 12 equivalence assertions comparing pre-refactor inline logic to lib/identity helpers across: isLid, isGroup, deriveOwnerJids, phoneToJid, resolvePeerId (5 fixture shapes: plain phone, LID+senderPn, LID+cache, LID+participant, LID unresolvable), extractE164 device-strip latent fix, normalizeDeviceScopedJid passthrough. - 1 assertion validating ID-03 identity_unresolved JSON log shape (event, jid, reason, lid_cache_size, confidence all present). - Test count: 129 -> 142. --------- Co-authored-by: Federico Liva <[email protected]>
houko
added a commit
that referenced
this pull request
Apr 20, 2026
…oard parity, smoke script Six follow-ups bundled per request to keep the channel-progress effort in one PR. Done from #2 onward; #1 (per-OutputFormat backtick adaptation) was investigated and dropped — backtick renders correctly in TelegramHtml (<code>), SlackMrkdwn, Discord/Matrix Markdown (inline code), and the only adapter that strips backticks (PlainText for Mastodon) is already opted out of the progress path via suppress_error_responses. #2 buffered_text fallback test: new integration test test_bridge_streaming_adapter_kernel_and_transport_both_fail covers the V2 4th outcome (send_streaming Err + kernel Err). #3 surface context_warning PhaseChange so the user sees when the agent's context window was trimmed/overflowed. Other phases stay SSE-only. #4 collapse repeated tool calls within an iteration — replace last_progress_tool with iter_tools_seen HashSet cleared at every ContentComplete. Batch agents no longer spam "🔧 web_search" per parallel call; retries in a later iteration still get a fresh line. #5 i18n for the "failed" suffix — supports en/zh-CN/es/ja/de/fr (matches librefang_types::i18n). Language threaded from kernel.config_snapshot().language through start_stream_text_bridge. #6 prettify tool names — web_search → Web Search; MCP_call → MCP Call (preserves internal caps). Backticks dropped from progress lines since the prettified form is now the identity. #8 dashboard ToolCallCard parity — mirror prettifyToolName() in dashboard/src/lib/string.ts and apply it in the card header so chat reply and dashboard show the same rendering. #7 live-integration smoke script — scripts/tests/channel_progress_smoke.sh automates the daemon-side flow once the user supplies an LLM API key and a configured channel adapter. Documents the channel-delivery gap (needs an external webhook receiver). No driver injection hook added to the kernel — that would have been scope creep. Tests added/updated: - test_prettify_tool_name_snake_to_title / _kebab_and_dotted / _preserves_internal_caps - test_tr_progress_failed_languages - test_bridge_streaming_adapter_kernel_and_transport_both_fail - Updated existing assertions to expect prettified names
houko
added a commit
that referenced
this pull request
Apr 20, 2026
…m fix, show_progress, i18n, prettify, dashboard parity (#2793) * feat(channels): channel-progress v2 — Telegram fallback fix, show_progress config, integration test Three follow-ups to #2792 (channel progress markers): 1. Telegram (streaming-adapter) buffered_text fallback now uses the `_status` variant. The same regression class fixed for non-streaming adapters in V1 was still latent on the Telegram path: - send_streaming Ok + kernel Err → Done reaction + success=true - send_streaming Err + buffered → Done reaction + success=true Both now correctly emit Error reaction, record_delivery(false), and journal Failed when the kernel actually errored. The fallback path also honors `suppress_error_responses` when the buffered text is a sanitized error (defense-in-depth — Telegram is not in the opt-in list today, but the path is now uniform across all adapters). 2. New `agent.toml show_progress` field (default true) plumbed through the streaming bridge. When false, the bridge skips both the `🔧 tool_name` and `⚠️ tool_name failed` injections — useful for agents whose output is parsed downstream, or for pristine-output scenarios where status markers would leak into the response. The trait-impl side looks up the agent's manifest from the kernel registry once per dispatch and passes it through to `start_stream_text_bridge[_with_status]`. 3. New end-to-end integration test in `crates/librefang-channels/tests/bridge_integration_test.rs` that wires a real `BridgeManager` + `MockAdapter` (non-streaming) + `MockProgressHandle` (override of `send_message_streaming_with_sender_status` that synthesises a delta stream with progress markers). Verifies the V2 dispatch pipeline actually surfaces progress to non-streaming adapters end-to-end via the consolidated `send_response` call. Tests: - test_stream_bridge_show_progress_false_suppresses_all_markers: show_progress=false produces no 🔧/⚠️ markers but still flows the actual model prose through - test_bridge_non_streaming_adapter_sees_progress_markers: full BridgeManager → MockAdapter pipeline; verifies progress markers end up in the captured `send()` call Notes: - True "live daemon" integration test (per CLAUDE.md) requires LLM API keys + a configured channel adapter (Telegram bot token, etc.) which the daemon environment does not currently have. The in-process integration test exercises the full dispatch wiring using real tokio tasks/channels and a mock kernel handle. * fix(kernel/wizard): add show_progress to AgentManifest struct literal CI compile error: wizard.rs constructed AgentManifest with an explicit struct literal listing every field, so adding show_progress in V2 broke that one site (the wizard agent created via setup intent). All other AgentManifest constructors use ..Default::default() and pick up show_progress=true automatically; only this one was a full-literal. * feat(channels): channel-progress v3 — collapse, i18n, prettify, dashboard parity, smoke script Six follow-ups bundled per request to keep the channel-progress effort in one PR. Done from #2 onward; #1 (per-OutputFormat backtick adaptation) was investigated and dropped — backtick renders correctly in TelegramHtml (<code>), SlackMrkdwn, Discord/Matrix Markdown (inline code), and the only adapter that strips backticks (PlainText for Mastodon) is already opted out of the progress path via suppress_error_responses. #2 buffered_text fallback test: new integration test test_bridge_streaming_adapter_kernel_and_transport_both_fail covers the V2 4th outcome (send_streaming Err + kernel Err). #3 surface context_warning PhaseChange so the user sees when the agent's context window was trimmed/overflowed. Other phases stay SSE-only. #4 collapse repeated tool calls within an iteration — replace last_progress_tool with iter_tools_seen HashSet cleared at every ContentComplete. Batch agents no longer spam "🔧 web_search" per parallel call; retries in a later iteration still get a fresh line. #5 i18n for the "failed" suffix — supports en/zh-CN/es/ja/de/fr (matches librefang_types::i18n). Language threaded from kernel.config_snapshot().language through start_stream_text_bridge. #6 prettify tool names — web_search → Web Search; MCP_call → MCP Call (preserves internal caps). Backticks dropped from progress lines since the prettified form is now the identity. #8 dashboard ToolCallCard parity — mirror prettifyToolName() in dashboard/src/lib/string.ts and apply it in the card header so chat reply and dashboard show the same rendering. #7 live-integration smoke script — scripts/tests/channel_progress_smoke.sh automates the daemon-side flow once the user supplies an LLM API key and a configured channel adapter. Documents the channel-delivery gap (needs an external webhook receiver). No driver injection hook added to the kernel — that would have been scope creep. Tests added/updated: - test_prettify_tool_name_snake_to_title / _kebab_and_dotted / _preserves_internal_caps - test_tr_progress_failed_languages - test_bridge_streaming_adapter_kernel_and_transport_both_fail - Updated existing assertions to expect prettified names * fix(channels): correct success/err pairing + treat timeout as soft success + clippy collapsible_if Three review-driven fixes for the V3 PR: Bug 1 — Telegram path outcome 3 (send_streaming Err + kernel Ok) was recording delivery as success=true with err=Some(stream_error). The fallback send_response had already delivered the buffered text and the kernel succeeded, so the transport-side stream error is irrelevant to delivery accounting — keeping it in the err field produced a contradictory metric (success AND err). Now: err is Some only when kernel actually failed. Bug 2 — TIMEOUT_PARTIAL_OUTPUT_MARKER was being mapped to status=Err, which flipped the lifecycle reaction to Error and record_delivery to success=false. Pre-V2 the bridge had no status channel and treated these turns as Done because the model emitted useful partial output before the inactivity timer fired. Restore that semantics: status=Ok for timeouts. The user still sees the "[Task timed out…]" tail appended to their reply. Clippy collapsible_if — flatten the inner if show_progress && ... into match guards on the ToolExecutionResult and PhaseChange arms (CI clippy -D warnings caught this in run 24651228831). * chore(channels): drop dead 'let _ = e' annotation The variable 'e' (stream transport error) is already used at warn! line 2886 and the empty-buffer fallback at line 2953, so the explicit underscore-binding introduced by the Bug 1 fix is noise — clean it up. * test(channels): regression coverage for Bug 1 (success/err pairing) + Bug 2 (timeout-as-success) Both bugs were caught by review, not tests — add the missing coverage so they cannot silently regress. Bug 2 unit test: test_stream_bridge_timeout_partial_output_reports_ok_status Constructs a kernel handle that fails with a string containing TIMEOUT_PARTIAL_OUTPUT_MARKER. Asserts: - The user-facing text channel still receives the "[Task timed out…]" tail so the user knows the reply may be incomplete. - The status oneshot resolves to Ok(()) — NOT Err — so bridge.rs drives the lifecycle reaction to Done and record_delivery to success=true. Pre-V2 semantics preserved. Bug 1 integration test: test_bridge_streaming_adapter_kernel_ok_transport_fail_records_clean_success Adds MockKernelOkHandle (overrides send_message_streaming_with_sender_status to emit clean text + status=Ok, AND overrides record_delivery to capture every (success, err) pair). Combined with the existing MockFailingStreamingAdapter (always Err on send_streaming), the test exercises Telegram outcome 3: - Fallback send_response delivers the buffered text ✓ - record_delivery is called with success=true AND err=None — proving the transport-side stream error does NOT leak into the err field when the kernel itself succeeded. (Pre-fix this would have been success=true, err=Some(stream_e) — a contradictory metric.) * polish(channels): unify progress-line spacing, codepoint-safe dashboard prettify, smoke script auto-spawn Three minor follow-ups from review: #3 — Symmetric blank lines around progress markers. ToolUseStart used \n\n…\n; ToolExecutionResult and PhaseChange used \n…\n. Adjacent markers (e.g. 🔧 X right before⚠️ X failed) ended up on consecutive lines without a blank separator and many markdown renderers collapsed them into one paragraph. All three now use \n\n…\n\n so every renderer that respects markdown blank-line semantics shows them as separate blocks. #4 — prettifyToolName() in dashboard/src/lib/string.ts now iterates by Unicode codepoint (via spread) instead of UTF-16 unit. A tool name starting with a non-BMP character (e.g. emoji) no longer drops its surrogate half when uppercased. Mirrors the Rust-side prettifier in channel_bridge.rs which uses chars().next() (also codepoint-correct). Tool names are usually ASCII so this is forward-looking, not a fix. #5 — channel_progress_smoke.sh now auto-spawns a temporary agent when the daemon has none. Picks the first available LLM key (GROQ/OPENAI/ANTHROPIC/MINIMAX) for the provider, sends a minimal manifest_toml inline (the SpawnRequest schema requires either manifest_toml or template — name alone isn't valid), and despawns the agent on exit. Pre-existing agents are still reused untouched. * fix(channels/tests): extract DeliveryLog type alias to satisfy clippy::type_complexity CI clippy -D warnings rejected Arc<Mutex<Vec<(bool, Option<String>)>>> as too complex. Hoist into a named alias — same shape, named contract.
This was referenced May 19, 2026
houko
pushed a commit
that referenced
this pull request
May 20, 2026
…irect tests PR self-review caught four issues; this commit fixes all of them. Fix #1 + #2: SeenSet docstring corrections ========================================== The old docstring claimed two things that weren't true: * "the adapter classes keep those names available as @Property shims pointing at this class's ``ids`` / ``order`` attributes" — there are no @Property shims; tests were rewritten to read ``adapter._seen.ids`` directly. * Listed bluesky as a SeenSet consumer — bluesky's ``_mark_seen`` is a server-side ``updateSeen`` REST POST, NOT a dedupe shim; the name collision is coincidental. Both claims fixed. Webex's empty-id → False quirk is now also called out explicitly so future readers don't think the shared behaviour is universal. Fix #3: direct unit tests for the shared modules ================================================ Added three new test files: * ``tests/test_common.py`` (40 tests) — covers split_message (passthrough, exact-limit, hard-cut, newline-prefer, empty, unicode), split_csv (empty, whitespace, dropped empties, order), parse_retry_after (missing, integer, decimal, floor, custom floor, max-cap, garbage, negative), SeenSet (fresh, repeat, distinct ids, empty-id, eviction, contains, len, thread safety with 8 concurrent workers, int ids), and http_request (200 JSON, request metadata recording, lowercased response headers, 4xx/5xx surfaced via status not raise, empty body, non-JSON, default timeout, default method). * ``tests/test_ws.py`` (13 tests) — RFC 6455 constants (WS_GUID, opcodes, MAX_FRAME_PAYLOAD), ``_parse_url`` (wss, ws-with-port, default-path, query-string preservation, non-ws reject, missing-host reject), and ``WebSocketClient.__init__`` defaults. Full socket-level coverage (handshake, frame round-trip) needs a real local WS server fixture that's out of scope for this refactor PR. * ``tests/test_sidecar_fakes.py`` (15 tests) — HdrShim, FakeResp CM protocol, FakeUrlopen script handling, call-metadata recording (URL/method/timeout/headers/body), JSON + form-encoded body decoding, 3-tuple response-headers variant, 4xx/5xx HTTPError raising, script-exhaustion AssertionError, empty/bytes body passthrough, back-compat underscore aliases. Fix #4: actually hoist MAX_BACKOFF_SECS / RETRY_AFTER_DEFAULT_SECS ================================================================= The previous round-2 commit added these constants to ``librefang.sidecar.common`` and the commit message claimed every adapter would pick up the canonical values from there. **None did** — all 16 adapters that used them kept their local definitions unchanged, so the hoist was theoretical. This commit actually rewires the imports. Each of the 16 adapters that referenced ``MAX_BACKOFF_SECS = 60.0`` now imports it from ``common``; 11 adapters likewise for ``RETRY_AFTER_DEFAULT_SECS = 30.0``. Local duplicate definitions are removed. A handful of adapters have local overrides that aren't ``60.0`` or ``30.0`` (e.g. webex's stricter ``RETRY_AFTER_DEFAULT_SECS = 30.0`` matches but it has a 30-second cap not 60; mastodon's ``RETRY_AFTER_DEFAULT_SECS = 60.0`` is also canonical but different semantics) — those are intentionally left as local strategy knobs. Verification ============ ``pytest sdk/python/tests/`` — **1095 passed in 5.05s** (was 1027 in 5.00s; the new test files add 68). Diff: 20 files changed, +786 / -105.
houko
added a commit
that referenced
this pull request
May 21, 2026
Surfaces from the post-#5445 audit (6th in the dingtalk/qq/omnibus review chain — first verdict that wasn't BROKEN/NEEDS_HOTFIX). All three nits are doc/UX cleanups, none are runtime regressions: 1. **Docstring claim #3 was wrong** — said "429 Retry-After honoured on every outbound POST", but only Cloud API uses `_cloud_post_with_retry`; the gateway path calls `_http_request` directly and raises on any non-2xx. Soften the claim to "Cloud-API outbound POSTs only" + explain when gateway-side retry would matter (operators proxying the local Baileys gateway behind a rate-limiting reverse-proxy). 2. **WHATSAPP_VERIFY_TOKEN unset failed silently** — Meta's subscription handshake returned 403 with no log line pointing back to the missing env var; operators saw "subscription failed" in the Meta dashboard with nothing in their daemon logs. Now warns at __init__, matching the WHATSAPP_APP_SECRET pattern already there. 3. **WHATSAPP_GROUP_POLICY is dead config** — Cloud API webhook payloads don't surface a group/conversation distinction (we hardcode `is_group=False` at `_handle_post_webhook`), and gateway mode delegates inbound entirely to the Node Baileys gateway which never calls back into the sidecar's `should_handle_message` filter. So `WHATSAPP_GROUP_POLICY` has zero effect today. Mark the schema field with an explicit "(currently inert)" note in the label so operators don't waste time setting it. Kept the field itself for forward-compat — Meta has been rolling out group-chat support to the Cloud API gradually and we want the schema to be ready when it lands. Drive-by: dropped the misleading send-voice docstring claim (lines 22-23 + 36-39 promised a `{gateway}/message/send-voice` multipart upload route that doesn't exist in code; rationale comment about the daemon's ChannelContent::Voice always carrying a URL at the dispatch boundary explains why we don't need it). Test: - New test_get_verify_empty_self_token_rejects asserts BOTH the __init__ warn fires AND the handshake fails closed. Regression guard for #2 — without this an attacker who guesses the empty hub.verify_token could subscribe their own callback URL. - cd sdk/python && pytest tests/test_whatsapp_adapter.py — 79 passed (was 78; +1 regression guard). This closes the audit chain. WhatsApp itself is structurally clean — natural routing key (phone) is preserved as channel_id both directions, the bug family from #5417 → #5449 doesn't apply.
This was referenced May 22, 2026
This was referenced May 23, 2026
houko
pushed a commit
that referenced
this pull request
May 28, 2026
…eshold, LLM confidence, KV mirror retirement, instrumented spawn, fuzzy categories, configurable prompt cap, CHANGELOG, comment fix 10 follow-ups raised on the code review of the prior two commits in this PR. All within the same memory-system scope. * #1 root user_id is now a constant sentinel UUID (00000000-0000-0000-0000-72006f0074a0, exported as `ROOT_API_KEY_USER_ID`) rather than `UserId::from_name("root")`. The from_name UUIDv5 lives inside `LIBREFANG_USER_NAMESPACE`, so an operator-registered `[users] name = "root"` would have silently inherited the master credential's ACL + per-user budget cap. The sentinel falls outside that namespace; AuthManager returns None for it and the fail-open Owner-default ACL applies. Regression test `root_api_key_user_id_does_not_collide_with_any_named_user` pins the non-collision invariant against {root, admin, owner, system, operator, user}. * #3 `HotAction::UpdateProactiveMemory` now also calls `substrate.set_consolidation_duplicate_threshold(...)`, so when `POST /api/config/reload` swaps in a new `[proactive_memory] duplicate_threshold`, the periodic global consolidation sweep picks it up alongside the per-agent on-demand consolidate. Without this the per-agent path picked up the new value but the global sweep stayed on the old one — exactly the inconsistency H5 set out to remove. `ConsolidationEngine` switched to an `Arc<AtomicU32>` threshold (f32 bits) so the setter takes `&self`, which is required because the hot-reload code path holds only `Arc<MemorySubstrate>`. `docs/operations/config-reload.md` row updated to call out the new behaviour. * #4 `build_extraction_prompt` now asks the LLM to emit a per-memory `confidence` field with a brief calibration guide; the parser reads it, clamps to [0, 1], and stashes the value in `metadata["confidence"]` and `MemoryItem.confidence` so the C3 insert path actually lands a non-default value in the `confidence` column. Missing field still defaults to 1.0 (matches the rule-based extractor's prior behaviour — never silently drops a memory). Tests `parse_extraction_propagates_confidence_to_metadata` and `parse_extraction_clamps_confidence_to_unit_interval` pin the new behaviour. * #5 The KV `memory:*` mirror is gone — fully retired, not just "non-load-bearing". All `structured.set("memory:*", ...)` / `structured.delete("memory:*", ...)` / `list_kv` scans that walked the mirror have been deleted from `import_memories`, `add_with_decision`'s ADD + UPDATE branches, `add_with_level`, `delete`, `update`, `reset`, `clear_level`, `cleanup_expired_sessions`, the eviction loop, and the consolidation merge-loser path. The read path was already on semantic (C1); leaving the writes in place would have grown the mirror without bound and risked future divergence regressions. `test_delete_memory` rewritten to assert behaviour through `search()` (the trait-level contract) instead of probing the underlying KV store. Any legacy `memory:*` entries from older installs are silently ignored. * #6 The detached auto-consolidate `tokio::spawn` is wrapped in a `tracing::info_span!("auto_consolidate", task = "auto_consolidate", agent = ...)` via `.instrument(span)`, so a panic inside the consolidate future surfaces in tracing output (instead of disappearing silently the way bare-spawn panics do) and operators can grep `task = "auto_consolidate"` to find the detached work. * #7 Category allowlist match is now case-insensitive + tolerant of trailing `s` on either side. `"Preferences"` / `"PREFERENCE"` / `"preferences"` all snap to a configured `"preference"` and the canonical configured spelling lands in the column. Test `parse_extraction_fuzzy_matches_category_case_and_plural` pins the four variants. * #8 `format_context_max_chars` is now a field on `ProactiveMemoryConfig` (default 8000 chars / ~2000 tokens); the store's `format_context_with_query` / `format_context` read it from the live config and pass it to `format_memories_with_budget(memories, max_chars)`. The trait fallback (`DefaultMemoryExtractor::format_context`, `LlmMemoryExtractor::format_context`) keeps the const default for callers without config access. Operators on 200k+ context windows can now raise the cap via `config.toml` without recompiling. * #2 + #9 New `Fixed` entry in `[Unreleased]` documents the breaking audit-shape change (root-api_key requests now stamp a `user_id` where they previously stamped `None`) and the `add()` behaviour change (no raw-transcript fallback for extraction misses). Both were noted inline in commits but absent from the CHANGELOG. The entry also lists the full audit-sweep scope so an operator can size-up the upgrade impact from one place. * #10 `import_memories` comment rewritten to explain the 0.95 threshold without the confusing "stricter than extraction-time dedup" framing. Drive-by: collapsed three more `clippy::manual_option_zip` sites in `kernel/tests.rs` that the workspace-clippy gate would have caught on the next CI run. Auto-restamped `.secrets.baseline` line numbers shifted by the CHANGELOG insert. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-runtime -p librefang-types -p librefang-api -p librefang-kernel --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 269 passed * cargo test -p librefang-runtime --lib proactive_memory — 60 passed (5 new follow-up regression tests included) * cargo test -p librefang-api --lib --test memory_routes_integration --test agent_kv_authz_integration --test auth_public_allowlist — all green
houko
added a commit
that referenced
this pull request
May 29, 2026
…cay, dedup, prompt budget, async consolidate) (#5839) * fix(memory): split-brain reads, raw-transcript fallback, RBAC on writes, forget() leak, immortal decay 5 CRITICAL findings from a memory-system audit, all in the proactive memory layer: * C1 split-brain: list()/get() read from the KV mirror while search()/auto_retrieve read from the semantic store. The KV write was best-effort (warn-and-continue) so a failure left rows visible to search but invisible to list. retrieve_memory_items now reads from semantic (the authoritative source); KV writes stay as a non-load- bearing compatibility mirror. * C2 raw-transcript fallback: when the extractor returned no signal, add() stored the verbatim concatenated message content as a session-level memory with no category. This was the dominant source of `category=null` rows and duplicate transcripts on the dashboard. The fallback is removed; callers that want raw content use add_with_level explicitly. * C3 confidence hardcoded to 1.0 + immortal decay: remember_with_embedding_and_peer now honors metadata["confidence"] (clamped to 0..=1, defaulting to 1.0) so the LLM extractor's signal reaches the column. decay_confidence reworked: boost divides the rate instead of multiplying the result and clamping to 1.0. The old formula made any memory with >=2 accesses freeze at confidence 1.0 forever; the new formula keeps "popular memories decay slower" but strictly monotonic (boost capped at MAX_BOOST=4.0). * C4 RBAC on write endpoints: memory_add, memory_update, memory_delete, memory_bulk_delete, memory_reset_agent, memory_clear_level, memory_consolidate, memory_cleanup, memory_export_agent, memory_import_agent, memory_decay, memory_store_relations now route through the namespace guard. New ProactiveMemoryStore wrappers cover the previously-unguarded ops (reset, clear_level, export_all, import_memories, decay_confidence). The root api_key is attributed as an Owner- equivalent AuthenticatedApiUser in middleware so operators using only the master credential keep their POST/PUT/DELETE access. * C5 forget() never marked deleted_at: SemanticStore::forget* now stamps deleted_at alongside deleted=1 so the prune_soft_deleted_memories sweep (filter `deleted_at IS NOT NULL`) can actually hard-delete user-/API-initiated deletions. Without the stamp every soft-deleted row leaked its embedding BLOB forever. consolidation.rs's merge-loser delete now stamps it too. Drive-by: removed a clippy::manual_option_zip in kernel/background_lifecycle.rs flagged while clippy-gating the change. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-api --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 269 passed (5 pre-existing tests adapted to the new add()-no-fallback semantics; new regression tests for C1 list-from-semantic, C2 no-fallback, C3 monotonic-decay + extractor-confidence-roundtrip, C4 viewer-denied on every write wrapper, C5 forget* stamps deleted_at) * cargo test -p librefang-api --lib --test memory_routes_integration --test agent_kv_authz_integration --test auth_public_allowlist — all green * chore(codegen): auto-regenerate openapi.json + sdk + schema baselines [skip ci] * fix(memory): tighten dedup thresholds, validate LLM extraction, cap prompt budget, detach auto-consolidate, unify consolidation knob Follow-up sweep on the same memory-system audit as the prior commit — picks off the HIGH findings that were in scope for the same crate / file cluster. * H1 duplicate_threshold tightened. The configured default jumps from 0.5 → 0.85 (mem0's recommended near-duplicate cut-off); both metrics — cosine and Jaccard — agree that 0.5 means "topically related", which let opposite-meaning sentences sharing keywords silently merge. `DefaultMemoryExtractor::decide_action`'s hardcoded same-category 0.5 / cross-category 0.6 UPDATE thresholds rise to 0.7 / 0.8; the 0.95 NOOP gate stays. `import_memories`' hardcoded 0.9 dedup floor rises to 0.95 with a docstring explaining why bulk-import is stricter than extraction-time dedup. * H2 LLM-extraction validation. `parse_llm_extraction_response` now enforces a 4-char content floor (drops "ok" / "no" / single-letter junk that was trivially unique and survived dedup), validates the emitted `category` against the configured `extract_categories` allowlist (out-of-allowlist values downgrade to "general" instead of polluting the dashboard's facets), and caps a single extraction call at MAX_MEMORIES_PER_EXTRACTION=20 rows so a runaway model can't churn the eviction loop. * H4 prompt-injection budget. `format_context` now goes through a shared `format_memories_with_budget` helper capped at FORMAT_CONTEXT_MAX_CHARS=8000 (~2000 tokens). Pre-fix the formatter concatenated everything with no ceiling — 10 retrieved memories × 2000-char MAX_MEMORY_CONTENT_LENGTH could push 20 KB into every request. Excess rows are reported via a "[+N additional memories omitted to keep the prompt within budget]" footer so the truncation is observable in the rendered prompt rather than silent. * H6 auto-consolidation no longer blocks the agent. The every-10 trigger in `auto_memorize` now `tokio::spawn`s the consolidate call instead of awaiting it inline; the next agent turn doesn't pay for the O(n²) merge pass plus its SQLite transaction. The detached future borrows nothing from `self` thanks to `ProactiveMemoryStore`'s manual Clone over Arc'd inner state. * H5 single source of truth for the consolidation threshold. `ConsolidationEngine` gains a `duplicate_threshold` field (defaulting to 0.85 to match the new config default) with a `set_duplicate_threshold` setter; `MemorySubstrate` exposes a passthrough; kernel boot pushes `config.proactive_memory. duplicate_threshold` down to the engine. The periodic global consolidation sweep and the on-demand `ProactiveMemoryStore::consolidate` now agree on what counts as a near-duplicate. Keeps the existing 142 callers of `MemorySubstrate::open_in_memory(decay_rate)` source-compatible (no signature break). H7 / H8 from the same audit were already addressed by the C3 fix in the previous commit (popular-memory immortality + dead extraction_threshold). H3 (memory_store/recall vs auto_memorize/ retrieve being disconnected tool surfaces) is intentional product shape rather than a bug — left out. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-runtime -p librefang-types -p librefang-api --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 269 passed * cargo test -p librefang-runtime --lib (proactive_memory) — 57 passed, including the 5 new regressions (parse_extraction_drops_sub_minimum_content, parse_extraction_downgrades_unknown_category, parse_extraction_preserves_category_when_allowlist_empty, parse_extraction_caps_total_memories_per_call, format_context_caps_prompt_budget_with_truncation_marker). * cargo test -p librefang-api --test memory_routes_integration --test agent_kv_authz_integration --test auth_public_allowlist — all green. * fix(memory): review-followups — sentinel root user_id, hot-reload threshold, LLM confidence, KV mirror retirement, instrumented spawn, fuzzy categories, configurable prompt cap, CHANGELOG, comment fix 10 follow-ups raised on the code review of the prior two commits in this PR. All within the same memory-system scope. * #1 root user_id is now a constant sentinel UUID (00000000-0000-0000-0000-72006f0074a0, exported as `ROOT_API_KEY_USER_ID`) rather than `UserId::from_name("root")`. The from_name UUIDv5 lives inside `LIBREFANG_USER_NAMESPACE`, so an operator-registered `[users] name = "root"` would have silently inherited the master credential's ACL + per-user budget cap. The sentinel falls outside that namespace; AuthManager returns None for it and the fail-open Owner-default ACL applies. Regression test `root_api_key_user_id_does_not_collide_with_any_named_user` pins the non-collision invariant against {root, admin, owner, system, operator, user}. * #3 `HotAction::UpdateProactiveMemory` now also calls `substrate.set_consolidation_duplicate_threshold(...)`, so when `POST /api/config/reload` swaps in a new `[proactive_memory] duplicate_threshold`, the periodic global consolidation sweep picks it up alongside the per-agent on-demand consolidate. Without this the per-agent path picked up the new value but the global sweep stayed on the old one — exactly the inconsistency H5 set out to remove. `ConsolidationEngine` switched to an `Arc<AtomicU32>` threshold (f32 bits) so the setter takes `&self`, which is required because the hot-reload code path holds only `Arc<MemorySubstrate>`. `docs/operations/config-reload.md` row updated to call out the new behaviour. * #4 `build_extraction_prompt` now asks the LLM to emit a per-memory `confidence` field with a brief calibration guide; the parser reads it, clamps to [0, 1], and stashes the value in `metadata["confidence"]` and `MemoryItem.confidence` so the C3 insert path actually lands a non-default value in the `confidence` column. Missing field still defaults to 1.0 (matches the rule-based extractor's prior behaviour — never silently drops a memory). Tests `parse_extraction_propagates_confidence_to_metadata` and `parse_extraction_clamps_confidence_to_unit_interval` pin the new behaviour. * #5 The KV `memory:*` mirror is gone — fully retired, not just "non-load-bearing". All `structured.set("memory:*", ...)` / `structured.delete("memory:*", ...)` / `list_kv` scans that walked the mirror have been deleted from `import_memories`, `add_with_decision`'s ADD + UPDATE branches, `add_with_level`, `delete`, `update`, `reset`, `clear_level`, `cleanup_expired_sessions`, the eviction loop, and the consolidation merge-loser path. The read path was already on semantic (C1); leaving the writes in place would have grown the mirror without bound and risked future divergence regressions. `test_delete_memory` rewritten to assert behaviour through `search()` (the trait-level contract) instead of probing the underlying KV store. Any legacy `memory:*` entries from older installs are silently ignored. * #6 The detached auto-consolidate `tokio::spawn` is wrapped in a `tracing::info_span!("auto_consolidate", task = "auto_consolidate", agent = ...)` via `.instrument(span)`, so a panic inside the consolidate future surfaces in tracing output (instead of disappearing silently the way bare-spawn panics do) and operators can grep `task = "auto_consolidate"` to find the detached work. * #7 Category allowlist match is now case-insensitive + tolerant of trailing `s` on either side. `"Preferences"` / `"PREFERENCE"` / `"preferences"` all snap to a configured `"preference"` and the canonical configured spelling lands in the column. Test `parse_extraction_fuzzy_matches_category_case_and_plural` pins the four variants. * #8 `format_context_max_chars` is now a field on `ProactiveMemoryConfig` (default 8000 chars / ~2000 tokens); the store's `format_context_with_query` / `format_context` read it from the live config and pass it to `format_memories_with_budget(memories, max_chars)`. The trait fallback (`DefaultMemoryExtractor::format_context`, `LlmMemoryExtractor::format_context`) keeps the const default for callers without config access. Operators on 200k+ context windows can now raise the cap via `config.toml` without recompiling. * #2 + #9 New `Fixed` entry in `[Unreleased]` documents the breaking audit-shape change (root-api_key requests now stamp a `user_id` where they previously stamped `None`) and the `add()` behaviour change (no raw-transcript fallback for extraction misses). Both were noted inline in commits but absent from the CHANGELOG. The entry also lists the full audit-sweep scope so an operator can size-up the upgrade impact from one place. * #10 `import_memories` comment rewritten to explain the 0.95 threshold without the confusing "stricter than extraction-time dedup" framing. Drive-by: collapsed three more `clippy::manual_option_zip` sites in `kernel/tests.rs` that the workspace-clippy gate would have caught on the next CI run. Auto-restamped `.secrets.baseline` line numbers shifted by the CHANGELOG insert. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-runtime -p librefang-types -p librefang-api -p librefang-kernel --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 269 passed * cargo test -p librefang-runtime --lib proactive_memory — 60 passed (5 new follow-up regression tests included) * cargo test -p librefang-api --lib --test memory_routes_integration --test agent_kv_authz_integration --test auth_public_allowlist — all green * fix(api): attribute Owner on no-auth loopback so memory writes aren't 403 The RBAC gating added 12 memory-write ACL checks, but the default `librefang start` (no api_key, loopback bind) takes the no-auth bypass which returned next.run() WITHOUT attaching an AuthenticatedApiUser. Memory write handlers then saw None -> anonymous Viewer fallback -> 403 on every POST/PUT/DELETE /api/memory*, breaking the documented default workflow. No-auth + trusted origin (loopback / LIBREFANG_ALLOW_NO_AUTH) is the same trust level as the root master credential, so attribute the same Owner-equivalent user (ROOT_API_KEY_USER_ID). Non-loopback still fails closed. Add integration tests for both: loopback write != 403, non-loopback no-auth still 401. * fix(memory): read-only recall for listing paths; correct decay doc MEDIUM (#5839): list/get/export/list_all read paths called recall(), which unconditionally bumps access_count + accessed_at. A dashboard polling the memory list would perpetually reset accessed_at = now and inflate access_count — the exact signals the C3 decay logic keys idle/popularity off — so polled listings could keep memories from ever decaying (and turned a GET into a 10k-row write). Add recall_readonly() (shared impl, no bump) and route the four listing/export reads through it; genuine semantic recalls still track access. Regression test asserts recall_readonly leaves access_count untouched while recall() bumps by 1. Also correct the decay_confidence doc: the once-per-hour cadence is enforced by the periodic maintenance scheduler, not an internal throttle (a direct call decays immediately). --------- Co-authored-by: Evan <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
houko
pushed a commit
that referenced
this pull request
May 29, 2026
…loor, partial-status, dedup-strip, test tightening 8 follow-ups from the code review of the prior commit. All in scope for the same proactive-memory layer. * #1 misattributed doc-comment in `proactive.rs` near `AUTO_CONSOLIDATE_EVERY` / `NEGATION_WORDS`. The `/// Negation/contradiction words …` line was orphaned above `AUTO_CONSOLIDATE_EVERY` when the new const got inserted; both consts now carry their own intended docstring. * #2 lowered `STALE_COUNTER_FLOOR` from `AUTO_CONSOLIDATE_EVERY / 2` (5) to `/ 4` (2). The /2 floor cleaned up "stuck at 1..4" agents but also reset slow-burn agents (single auto_memorize per maintenance tick) before they could climb to 10, so a steady 1-call/hour stream effectively never consolidated. /4 keeps the cold-slot eviction directional while letting low-frequency agents still accumulate to the trigger. * #3 `PATCH /api/memory/config` response shape is now explicit about partial success. `body.status` is `"applied"` when the reload succeeded and `"partial"` when the disk write landed but the live reload failed (e.g. operator hand-edited an unrelated section into an invalid shape between PATCH writes). Clients MUST inspect `status`; the HTTP code stays 200 for both branches since the disk write itself succeeded. 207 / 500 were considered and rejected — 500 misrepresents that the request was rejected (it wasn't) and 207 forces every existing client to re-classify success. Mirrors the `import_agent_memory` partial-success pattern. * #4 documented the M13 cost tradeoff. Up to 4 SQLite roundtrips per insertion on the no-embedding fallback path (versus the pre-fix 1), but the embedding-driver path is unaffected and the loop short-circuits as soon as the union hits fetch_limit, so the common case ("first keyword filled the slate") still runs a single query. Operators on the no-embedding path pay the cost on writes only, against an already-unindexed `content LIKE` scan. * #5 defensively strip the M14 `_update_threshold_*` keys from the enriched item right after `decide_action` returns + assert callers don't pre-populate them. Both insertion branches today build their stored metadata from the original `item.metadata` (not `enriched_item.metadata`), so the threshold keys never reach the column — but stripping + asserting is cheap insurance for the next refactorer who repoints either branch at the enriched copy. * #6 relaxed the `extract_search_keywords_returns_multiple_ordered_longest_first` test: no more exact `kws.len() == 4` — the cap and the "longest distinctive word survives" invariants are what we care about, not the exact count. Future additions to `STOP_WORDS` no longer silently break the test. * #7 dropped the unused `_update_threshold_cross_cat` set in `decide_action_honors_config_update_thresholds`. Both candidate memories share the "preference" category, so the cross-cat threshold branch was unreachable. * #8 moved `STALE_COUNTER_FLOOR` from a `fn`-scope `const` to module scope, alongside `AUTO_CONSOLIDATE_EVERY` from which it derives. Matches the repo convention and lets future readers see the floor / trigger relationship at one site. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-runtime -p librefang-types -p librefang-api --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 273 passed * cargo test -p librefang-api --test memory_routes_integration — 14 passed
houko
pushed a commit
that referenced
this pull request
May 29, 2026
… + M14 regression coverage, comment polish 5 follow-ups from the second review pass on #5850. * B (was #1) M11 done properly. `consolidation_counters` is now `HashMap<String, CounterEntry>` where `CounterEntry` carries both the running count and a `last_touched: DateTime<Utc>` stamp. The maintenance sweep evicts entries whose `last_touched` is older than `STALE_COUNTER_IDLE_WINDOW = 2 hours` (~2× the maintenance rate-limit window), regardless of count. The previous count-threshold fix (followup #2 in the prior commit) mitigated but didn't solve the slow-burn case: any agent firing ≤ 1 × per maintenance window would still be reset before climbing past the count floor. The timestamp-based check closes that gap — an active slot, however slow, is preserved as long as it's been touched within the window; a truly idle slot is reclaimed within ~2 hours of going quiet. * C (was #2) `PATCH /api/memory/config` happy-path test added at `memory_routes_integration::patch_memory_config_hot_reloads_and_reports_applied`. Pre-seeds a minimal `config.toml` (the harness's tempdir previously didn't materialise one, which the file-level docstring flagged as out-of-scope; the docstring updated accordingly) and asserts `body["status"] == "applied"`, `body["reload_error"]` null, and that the PATCHed value round-trips into the response. Without this, the M12 status contract could silently revert and the rest of the suite wouldn't catch it. `RouterHarness._tmp` is exposed as `tmp` to let the test reach the seed location. * D (was #3) M14 strip regression test added at `proactive::tests::add_with_decision_does_not_leak_threshold_keys_to_stored_metadata`. Drives the full `add()` path through `add_with_decision`, then reads back via `list()` and asserts none of the private `_update_threshold_*` / `_embedding` keys leaked into the stored metadata column. Catches the regression where someone repoints the ADD or UPDATE branch at `enriched_item.metadata` (the decision-clone) instead of the original `item.metadata` (the caller's input). * E (was #4) the `debug_assert!` panic messages on the `_update_threshold_*` private keys re-worded from "caller leaked it" to "callers must not pre-populate ..." — neutral phrasing that doesn't presume the caller is buggy. Also added a one-line note that the production path stays safe regardless (the unconditional `insert` overwrites any leaked value before `decide_action` reads it), since `debug_assert!` is compiled out in release. * F (was #5) M13 cost-trade-off comment tightened. The "common case still runs a single query" claim was accurate for agents with sizeable stores (first keyword exhausts fetch_limit) but not for fresh / small stores where no individual keyword has enough matches to short-circuit. The comment now distinguishes the two regimes instead of overgeneralising. Re-stamped `.secrets.baseline` line numbers shifted by the test file edits. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-api --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 274 passed (incl. `add_with_decision_does_not_leak_threshold_keys_to_stored_metadata`) * cargo test -p librefang-api --test memory_routes_integration — 15 passed (incl. `patch_memory_config_hot_reloads_and_reports_applied`)
houko
pushed a commit
that referenced
this pull request
May 29, 2026
…epts partial, M14 strips _embedding too, prune rate-limit, chrono::Duration const portability 6 followups from the third review pass on #5850. * #1 M12 test pinned the contract properly. `serde_json::Value` indexed by a missing key returns `Value::Null`, so the prior `assert_eq!(body["reload_error"], Null)` silently passed even if the field had been removed. Now asserts `body.as_object() .contains_key(...)` for `status`, `restart_required`, `reload_error` first, then asserts their values. * #2 strip + test for the `_embedding` private-stash key. `add_with_decision` now also calls `enriched_item.metadata.remove("_embedding")` after `decide_action` returns, so all three private stash keys (`_update_threshold_*` + `_embedding`) get the same defensive treatment. Test renamed to `add_with_decision_does_not_leak_private_stash_keys_to_stored_metadata` and attaches a tiny mock `EmbeddingFn` so the `_embedding` stash path actually fires — the prior assertion against `_embedding` was decorative because the test had no embedding driver configured. * #3 rate-limited the counter prune via a new `last_counter_prune: Arc<Mutex<Option<DateTime<Utc>>>>` and a `maybe_prune_counters` helper that mirrors the `maybe_decay_confidence` / `maybe_cleanup_expired` once-per-hour pattern. Prior to this the prune ran on every `maybe_run_maintenance` call, reachable from `search` / `auto_retrieve` / `consolidate` at potentially many Hz. The retain itself was microseconds, so this is a wash today, but it brings the three maintenance sub-tasks under the same scheduling budget for future scaling. * #4 docstring on `STALE_COUNTER_IDLE_WINDOW_HOURS` re-phrased to describe the slot-keeping guarantee in terms of the maintenance rate-limit window, not "consecutive prune passes" — the old phrasing happened to be true only because the prune wasn't rate-limited yet (now rectified by #3). * #5 M12 test now accepts either `"applied"` or `"partial"` as the body status — both are valid post-fix outcomes; the pre-fix contract had no `status` field at all. The status field's presence (asserted via the #1 fix) is the actual contract we're pinning. Made the test robust to future `KernelConfig::default()` changes that might cause the seeded toml to fail reload validation. * #6 swapped `const STALE_COUNTER_IDLE_WINDOW: chrono::Duration = chrono::Duration::hours(2)` for `const STALE_COUNTER_IDLE_WINDOW_HOURS: i64 = 2` plus `chrono::Duration::hours(STALE_COUNTER_IDLE_WINDOW_HOURS)` at the call site. `chrono::Duration::hours` is a `const fn` at the currently pinned `chrono` minor but the const-ness isn't a stable contract across `0.4.x` versions, so a lockfile bump could silently break the build. The integer-hours + runtime conversion stays valid regardless. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-api --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 274 passed (incl. `add_with_decision_does_not_leak_private_stash_keys_to_stored_metadata` with embedding-driver coverage) * cargo test -p librefang-api --test memory_routes_integration — 15 passed (incl. tighter `patch_memory_config_hot_reloads_and_reports_applied` contract assertions)
houko
pushed a commit
that referenced
this pull request
May 29, 2026
…strip helper + non-empty partial-error 3 follow-ups from the fourth review pass on #5850. All three are about closing gaps between what the code does and what the docstrings / tests claim it does. * A `STALE_COUNTER_IDLE_WINDOW_HOURS` docstring: the "reclaimed within ~2 hours of going quiet" claim was accurate before the round-3 prune rate-limit landed, after which the worst-case reclaim latency is 2-3 hours (idle window + up to one prune rate-limit period because the prune itself only runs ≤ once per hour). Re-phrased with explicit upper/lower bounds; deleted the duplicate "previous fix (round-1 followup #2)" paragraph that had ended up in the doc twice during the round-2 edit. * B `strip_private_stash_keys` extracted into a module-level function driven by a single `ADD_WITH_DECISION_PRIVATE_STASH_KEYS` const, and unit-tested directly via `strip_private_stash_keys_removes_all_private_keys`. The integration test `add_with_decision_does_not_leak_private_stash_keys_to_stored_metadata` only catches a *coordinated two-step regression* (strip removed AND ADD/UPDATE branch repointed at `enriched_item.metadata`) — the current ADD path bypasses `enriched_item.metadata` entirely, so single-step regressions of either kind pass that test. The prior commit's docstring claimed "the strip code on the post-decide path is the only thing keeping it out of the stored column", which was wrong — `item.metadata` (the caller's input) is what actually keeps the keys out today. Updated the docstring to match reality and added the direct unit test on the helper to cover single-step strip regressions. * C `reload_error` partial-branch assertion in `patch_memory_config_hot_reloads_and_reports_applied` now also rejects empty / whitespace-only strings. `is_string()` alone passed `""` / `" "` / any other zero-info value — operators would see status=partial with a useless error blob and have no actionable diagnostic. Trimmed-non-empty makes the contract honest about what "carries the validator output" means. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-api --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 275 passed (1 new: `strip_private_stash_keys_removes_all_private_keys`) * cargo test -p librefang-api --test memory_routes_integration — 15 passed
houko
added a commit
that referenced
this pull request
May 29, 2026
…CH, multi-keyword search, configurable UPDATE thresholds (#5850) * fix(memory): MEDIUM follow-ups — counter map sweep, hot-reload on PATCH, multi-keyword search, configurable UPDATE thresholds Continuation of the audit sweep on the proactive-memory subsystem. 4 MEDIUM findings from the same audit, all in scope for this PR. * M11 `consolidation_counters` HashMap is now actively pruned every maintenance tick. Pre-fix it only swept when the map crossed 1000 entries (truncate-to-500 by count DESC), which delayed cleanup until the leak was observable AND deleted the highest-count entries — exactly the agents about to fire a real consolidate. The new sweep drops every counter that hasn't passed the halfway mark (`< AUTO_CONSOLIDATE_EVERY / 2 = 5`) on each maintenance tick: HashMap::retain in-place, evicts cold entries first, never touches an entry that's about to fire. Added named constant `AUTO_CONSOLIDATE_EVERY = 10` so the trigger + the prune floor stay in lockstep. * M12 `PATCH /api/memory/config` now calls `kernel.reload_config()` after writing `config.toml`, so dashboard saves take effect on the running kernel instead of staying disk-only until restart. Pre-fix the response always reported `restart_required: true`, which confused operators who could see GET return the new values while live behaviour (ProactiveMemoryStore::config, decay engine, etc.) stayed on the boot snapshot. `restart_required` now reflects the actual `ReloadPlan` — false when every diff field hot-reloads, true when any field needs a restart. A reload validation failure is surfaced via the new `reload_error` field instead of swallowing the disk write. * M13 `extract_search_keywords` returns a `Vec<String>` of the top 4 distinctive keywords ordered longest-first (post-stop-word filter, post-dedup), and the caller iterates over them unioning per-keyword LIKE recalls until `fetch_limit` is hit. Pre-fix it collapsed the four candidates to the single longest one, wasting the stop-word filter work on the other three and giving the no-embedding fallback path a frequently-too-generic substring (e.g. "analysis") to match against, OR a too-specific compound term that matched nothing. The fallback to the raw-content LIKE is preserved for the no-distinctive-words case so a near-verbatim duplicate is still detectable. * M14 the `decide_action` UPDATE thresholds are now configurable via two new `ProactiveMemoryConfig` fields: `update_threshold_same_category` (default 0.7) and `update_threshold_cross_category` (default 0.8). The trait method signature stays stable — `add_with_decision` stashes the live config values in the new memory's metadata under `_update_threshold_same_cat` / `_update_threshold_cross_cat`, the default `decide_action` reads them out (falling back to the const defaults for direct trait-method callers), and the LLM-backed extractor inherits the same behaviour via its fallback to the default heuristic on driver failure. This separates the per-insertion conflict-resolution threshold (UPDATE vs ADD) from the post-hoc consolidation threshold (`duplicate_threshold`) — pre-fix both were conflated. Drive-by fmt: two stale formatting hunks `cargo fmt` flagged in `runtime/tool_runner/wasm_skill.rs:159` and `api/tests/memory_routes_integration.rs:518` (neither mine, both inherited from main). Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-runtime -p librefang-types -p librefang-api -p librefang-kernel --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 273 passed (4 new regression tests: `extract_search_keywords_returns_multiple_ordered_longest_first`, `extract_search_keywords_empty_for_all_stop_words`, `decide_action_honors_config_update_thresholds`, and the existing suite re-verified against the new `update_threshold_*_category` config fields) * cargo test -p librefang-runtime --lib proactive_memory — 60 passed * cargo test -p librefang-api --lib --test memory_routes_integration --test agent_kv_authz_integration --test auth_public_allowlist — all green * fix(memory): review-followups on #5850 — doc-comment fix, threshold floor, partial-status, dedup-strip, test tightening 8 follow-ups from the code review of the prior commit. All in scope for the same proactive-memory layer. * #1 misattributed doc-comment in `proactive.rs` near `AUTO_CONSOLIDATE_EVERY` / `NEGATION_WORDS`. The `/// Negation/contradiction words …` line was orphaned above `AUTO_CONSOLIDATE_EVERY` when the new const got inserted; both consts now carry their own intended docstring. * #2 lowered `STALE_COUNTER_FLOOR` from `AUTO_CONSOLIDATE_EVERY / 2` (5) to `/ 4` (2). The /2 floor cleaned up "stuck at 1..4" agents but also reset slow-burn agents (single auto_memorize per maintenance tick) before they could climb to 10, so a steady 1-call/hour stream effectively never consolidated. /4 keeps the cold-slot eviction directional while letting low-frequency agents still accumulate to the trigger. * #3 `PATCH /api/memory/config` response shape is now explicit about partial success. `body.status` is `"applied"` when the reload succeeded and `"partial"` when the disk write landed but the live reload failed (e.g. operator hand-edited an unrelated section into an invalid shape between PATCH writes). Clients MUST inspect `status`; the HTTP code stays 200 for both branches since the disk write itself succeeded. 207 / 500 were considered and rejected — 500 misrepresents that the request was rejected (it wasn't) and 207 forces every existing client to re-classify success. Mirrors the `import_agent_memory` partial-success pattern. * #4 documented the M13 cost tradeoff. Up to 4 SQLite roundtrips per insertion on the no-embedding fallback path (versus the pre-fix 1), but the embedding-driver path is unaffected and the loop short-circuits as soon as the union hits fetch_limit, so the common case ("first keyword filled the slate") still runs a single query. Operators on the no-embedding path pay the cost on writes only, against an already-unindexed `content LIKE` scan. * #5 defensively strip the M14 `_update_threshold_*` keys from the enriched item right after `decide_action` returns + assert callers don't pre-populate them. Both insertion branches today build their stored metadata from the original `item.metadata` (not `enriched_item.metadata`), so the threshold keys never reach the column — but stripping + asserting is cheap insurance for the next refactorer who repoints either branch at the enriched copy. * #6 relaxed the `extract_search_keywords_returns_multiple_ordered_longest_first` test: no more exact `kws.len() == 4` — the cap and the "longest distinctive word survives" invariants are what we care about, not the exact count. Future additions to `STOP_WORDS` no longer silently break the test. * #7 dropped the unused `_update_threshold_cross_cat` set in `decide_action_honors_config_update_thresholds`. Both candidate memories share the "preference" category, so the cross-cat threshold branch was unreachable. * #8 moved `STALE_COUNTER_FLOOR` from a `fn`-scope `const` to module scope, alongside `AUTO_CONSOLIDATE_EVERY` from which it derives. Matches the repo convention and lets future readers see the floor / trigger relationship at one site. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-runtime -p librefang-types -p librefang-api --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 273 passed * cargo test -p librefang-api --test memory_routes_integration — 14 passed * fix(memory): review-followups (round 2) — proper M11 idle-window, M12 + M14 regression coverage, comment polish 5 follow-ups from the second review pass on #5850. * B (was #1) M11 done properly. `consolidation_counters` is now `HashMap<String, CounterEntry>` where `CounterEntry` carries both the running count and a `last_touched: DateTime<Utc>` stamp. The maintenance sweep evicts entries whose `last_touched` is older than `STALE_COUNTER_IDLE_WINDOW = 2 hours` (~2× the maintenance rate-limit window), regardless of count. The previous count-threshold fix (followup #2 in the prior commit) mitigated but didn't solve the slow-burn case: any agent firing ≤ 1 × per maintenance window would still be reset before climbing past the count floor. The timestamp-based check closes that gap — an active slot, however slow, is preserved as long as it's been touched within the window; a truly idle slot is reclaimed within ~2 hours of going quiet. * C (was #2) `PATCH /api/memory/config` happy-path test added at `memory_routes_integration::patch_memory_config_hot_reloads_and_reports_applied`. Pre-seeds a minimal `config.toml` (the harness's tempdir previously didn't materialise one, which the file-level docstring flagged as out-of-scope; the docstring updated accordingly) and asserts `body["status"] == "applied"`, `body["reload_error"]` null, and that the PATCHed value round-trips into the response. Without this, the M12 status contract could silently revert and the rest of the suite wouldn't catch it. `RouterHarness._tmp` is exposed as `tmp` to let the test reach the seed location. * D (was #3) M14 strip regression test added at `proactive::tests::add_with_decision_does_not_leak_threshold_keys_to_stored_metadata`. Drives the full `add()` path through `add_with_decision`, then reads back via `list()` and asserts none of the private `_update_threshold_*` / `_embedding` keys leaked into the stored metadata column. Catches the regression where someone repoints the ADD or UPDATE branch at `enriched_item.metadata` (the decision-clone) instead of the original `item.metadata` (the caller's input). * E (was #4) the `debug_assert!` panic messages on the `_update_threshold_*` private keys re-worded from "caller leaked it" to "callers must not pre-populate ..." — neutral phrasing that doesn't presume the caller is buggy. Also added a one-line note that the production path stays safe regardless (the unconditional `insert` overwrites any leaked value before `decide_action` reads it), since `debug_assert!` is compiled out in release. * F (was #5) M13 cost-trade-off comment tightened. The "common case still runs a single query" claim was accurate for agents with sizeable stores (first keyword exhausts fetch_limit) but not for fresh / small stores where no individual keyword has enough matches to short-circuit. The comment now distinguishes the two regimes instead of overgeneralising. Re-stamped `.secrets.baseline` line numbers shifted by the test file edits. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-api --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 274 passed (incl. `add_with_decision_does_not_leak_threshold_keys_to_stored_metadata`) * cargo test -p librefang-api --test memory_routes_integration — 15 passed (incl. `patch_memory_config_hot_reloads_and_reports_applied`) * fix(memory): review-followups (round 3) — M12 test key-presence + accepts partial, M14 strips _embedding too, prune rate-limit, chrono::Duration const portability 6 followups from the third review pass on #5850. * #1 M12 test pinned the contract properly. `serde_json::Value` indexed by a missing key returns `Value::Null`, so the prior `assert_eq!(body["reload_error"], Null)` silently passed even if the field had been removed. Now asserts `body.as_object() .contains_key(...)` for `status`, `restart_required`, `reload_error` first, then asserts their values. * #2 strip + test for the `_embedding` private-stash key. `add_with_decision` now also calls `enriched_item.metadata.remove("_embedding")` after `decide_action` returns, so all three private stash keys (`_update_threshold_*` + `_embedding`) get the same defensive treatment. Test renamed to `add_with_decision_does_not_leak_private_stash_keys_to_stored_metadata` and attaches a tiny mock `EmbeddingFn` so the `_embedding` stash path actually fires — the prior assertion against `_embedding` was decorative because the test had no embedding driver configured. * #3 rate-limited the counter prune via a new `last_counter_prune: Arc<Mutex<Option<DateTime<Utc>>>>` and a `maybe_prune_counters` helper that mirrors the `maybe_decay_confidence` / `maybe_cleanup_expired` once-per-hour pattern. Prior to this the prune ran on every `maybe_run_maintenance` call, reachable from `search` / `auto_retrieve` / `consolidate` at potentially many Hz. The retain itself was microseconds, so this is a wash today, but it brings the three maintenance sub-tasks under the same scheduling budget for future scaling. * #4 docstring on `STALE_COUNTER_IDLE_WINDOW_HOURS` re-phrased to describe the slot-keeping guarantee in terms of the maintenance rate-limit window, not "consecutive prune passes" — the old phrasing happened to be true only because the prune wasn't rate-limited yet (now rectified by #3). * #5 M12 test now accepts either `"applied"` or `"partial"` as the body status — both are valid post-fix outcomes; the pre-fix contract had no `status` field at all. The status field's presence (asserted via the #1 fix) is the actual contract we're pinning. Made the test robust to future `KernelConfig::default()` changes that might cause the seeded toml to fail reload validation. * #6 swapped `const STALE_COUNTER_IDLE_WINDOW: chrono::Duration = chrono::Duration::hours(2)` for `const STALE_COUNTER_IDLE_WINDOW_HOURS: i64 = 2` plus `chrono::Duration::hours(STALE_COUNTER_IDLE_WINDOW_HOURS)` at the call site. `chrono::Duration::hours` is a `const fn` at the currently pinned `chrono` minor but the const-ness isn't a stable contract across `0.4.x` versions, so a lockfile bump could silently break the build. The integer-hours + runtime conversion stays valid regardless. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-api --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 274 passed (incl. `add_with_decision_does_not_leak_private_stash_keys_to_stored_metadata` with embedding-driver coverage) * cargo test -p librefang-api --test memory_routes_integration — 15 passed (incl. tighter `patch_memory_config_hot_reloads_and_reports_applied` contract assertions) * fix(memory): review-followups (round 4) — docstring honesty + direct strip helper + non-empty partial-error 3 follow-ups from the fourth review pass on #5850. All three are about closing gaps between what the code does and what the docstrings / tests claim it does. * A `STALE_COUNTER_IDLE_WINDOW_HOURS` docstring: the "reclaimed within ~2 hours of going quiet" claim was accurate before the round-3 prune rate-limit landed, after which the worst-case reclaim latency is 2-3 hours (idle window + up to one prune rate-limit period because the prune itself only runs ≤ once per hour). Re-phrased with explicit upper/lower bounds; deleted the duplicate "previous fix (round-1 followup #2)" paragraph that had ended up in the doc twice during the round-2 edit. * B `strip_private_stash_keys` extracted into a module-level function driven by a single `ADD_WITH_DECISION_PRIVATE_STASH_KEYS` const, and unit-tested directly via `strip_private_stash_keys_removes_all_private_keys`. The integration test `add_with_decision_does_not_leak_private_stash_keys_to_stored_metadata` only catches a *coordinated two-step regression* (strip removed AND ADD/UPDATE branch repointed at `enriched_item.metadata`) — the current ADD path bypasses `enriched_item.metadata` entirely, so single-step regressions of either kind pass that test. The prior commit's docstring claimed "the strip code on the post-decide path is the only thing keeping it out of the stored column", which was wrong — `item.metadata` (the caller's input) is what actually keeps the keys out today. Updated the docstring to match reality and added the direct unit test on the helper to cover single-step strip regressions. * C `reload_error` partial-branch assertion in `patch_memory_config_hot_reloads_and_reports_applied` now also rejects empty / whitespace-only strings. `is_string()` alone passed `""` / `" "` / any other zero-info value — operators would see status=partial with a useless error blob and have no actionable diagnostic. Trimmed-non-empty makes the contract honest about what "carries the validator output" means. Verification: * cargo check --workspace --lib — clean * cargo clippy -p librefang-memory -p librefang-api --tests -- -D warnings — clean * cargo test -p librefang-memory --lib — 275 passed (1 new: `strip_private_stash_keys_removes_all_private_keys`) * cargo test -p librefang-api --test memory_routes_integration — 15 passed --------- Co-authored-by: Evan <[email protected]>
This was referenced May 29, 2026
houko
added a commit
that referenced
this pull request
May 29, 2026
…5862) Plugin hook subprocesses previously ran with network and filesystem access wide open, and the Linux seccomp / Landlock backends were not in the default feature set — so the shipped binary applied no syscall or LSM filtering at all. Secure-by-default changes (breaking): - Flip `allow_network` / `allow_filesystem` defaults from `true` to `false` in every default source: `HookConfig::default()` in plugin_runtime.rs and the serde `#[serde(default)]` for `ContextEngineHooks` in librefang-types (was `default_true_bool`, now the bool default `false`, which also makes the derived `Default` and the serde-omitted default agree — they previously disagreed). - Enable `seccomp-sandbox` and `landlock-sandbox` in the runtime's default feature set. Their Linux-only deps (`seccompiler`, `landlock`) are moved under `[target.'cfg(target_os = "linux")'.dependencies]` so the features can ship in `default` without breaking macOS / Windows builds (the `dep:` activations are no-ops where the dep is absent, and all code touching them is already `cfg(all(target_os = "linux", feature))`). Two latent bugs surfaced by turning the sandbox on by default (in-scope, same file / feature): - The seccomp allowlist was missing glibc / language-runtime start-up syscalls (rseq, set_robust_list, rt_sigtimedwait, statx, sched_getaffinity, …). With seccomp off by default the gap was never exercised; on by default it SIGSYS-killed every hook child before it read stdin (observed as "Broken pipe"). Added the universal start-up set (present on both x86_64 and aarch64). - The `unshare` namespace probe checked only `unshare --help`, not whether the kernel grants the namespace. In unprivileged containers / hardened CI the binary exists but CLONE_NEWNET / CLONE_NEWNS returns EPERM, so wrapping a deny-network / deny-filesystem hook with `unshare` killed the child instead of failing open to env-var / Landlock isolation. The probe now runs `unshare --<ns> -- true` and only wraps when the namespace can actually be created. Tests: - librefang-types: omitted `[hooks]` flags deserialize to deny; explicit opt-in honoured; derived Default matches serde default. - librefang-runtime: `HookConfig::default()` denies both; `locked_down_default_hook_completes_round_trip` runs a hook under the full deny-by-default sandbox (seccomp + unshare-or-fallback) and asserts a clean JSON round trip — the regression guard for the seccomp allowlist and the unshare probe. Verified in the Linux dev container: `cargo check -p librefang-runtime --lib`, `cargo clippy -p librefang-runtime --lib -D warnings`, `cargo test -p librefang-runtime --lib plugin` (58 pass), `cargo test -p librefang-types context_engine_hooks` (2 pass). Co-authored-by: Evan <[email protected]>
houko
added a commit
that referenced
this pull request
May 30, 2026
…log gen (#5924) extract_pr_numbers grepped every #N on a oneline subject, so in-title cross-references (issue refs like 'fixes #5740', prior-PR refs like 'post-#5053', and 'part N of M' markers like '(#2)') were treated as their own PRs and fed to 'gh pr view', pulling unrelated ancient or unmerged PRs into the generated release notes. A squash merge always appends the PR reference as the trailing (#N) of the subject, so parse only the last match per line. Split the parsing into a pure parse_pr_numbers helper with unit coverage. Co-authored-by: Evan <[email protected]>
houko
added a commit
that referenced
this pull request
Jun 21, 2026
The webhook store guarded its in-memory state with `std::sync::RwLock` and exposed synchronous `list`/`get`/`create`/`update`/`delete` methods called directly from async axum handlers. Holding a blocking lock (and running synchronous `std::fs` persistence) on the runtime threads can stall the executor under contention. Switch the guard to `tokio::sync::RwLock` and make the accessor methods `async`. Persistence now runs the file I/O on `spawn_blocking` so the atomic write never blocks the async runtime. `load` stays synchronous: it runs once at daemon boot / test setup, off the request path. Handlers are updated to `.await` the store calls. Where a handler held the `!Send` `ErrorTranslator` across the new await point, the translated messages are resolved up front and the translator dropped before the await (per CLAUDE.md), keeping the axum `Handler` bound satisfied. Closes #2 Co-authored-by: Evan <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps docker/setup-buildx-action from 3 to 4.
Release notes
Sourced from docker/setup-buildx-action's releases.
... (truncated)
Commits
4d04d5dMerge pull request #485 from docker/dependabot/npm_and_yarn/docker/actions-to...cd74e05chore: update generated contenteee38ecbuild(deps): bump@docker/actions-toolkitfrom 0.77.0 to 0.79.07a83f65Merge pull request #484 from docker/dependabot/github_actions/docker/setup-qe...a5aa967Merge pull request #464 from crazy-max/rm-deprecatede73d53fbuild(deps): bump docker/setup-qemu-action from 3 to 428a438eMerge pull request #483 from crazy-max/node24034e9d3chore: update generated contentb4664d8remove deprecated inputs/outputsa8257denode 24 as default runtimeDependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)