Commit Graph

1243 Commits

Author SHA1 Message Date
Teknium
81c7e5de48 docs(a2a): website docs page + canonical agent-card.json path in prose
- New website/docs/user-guide/messaging/a2a.md: when/where to use A2A
  (cross-machine, specialist peers, being callable) vs delegation/kanban
  for same-machine multi-agent; enable, outbound tools, inbound surface,
  security model, env reference, quick test, troubleshooting. Registered
  in sidebars.ts and the messaging index.
- README/DESIGN/plugin.yaml/protocol.py prose updated to name the A2A
  v1.0 canonical discovery path /.well-known/agent-card.json (the code
  already served both; only the docs lagged).
2026-08-02 15:10:15 -07:00
Ben Kamholtz
3271cb6907 fix(a2a): override authorization_is_upstream for A2A peers
A2A authenticates every inbound request via bearer token in do_POST
(401 before dispatch). Without overriding authorization_is_upstream=True,
the gateway's per-platform user allow-list ({PLATFORM}_ALLOWED_USERS)
rejects A2A peers because their identity is a token-derived name or
pod IP, not a platform account in any configured allow-list. Messages
never reach the agent and callers get empty replies.

This is authorization delegated to the bearer-token transport, not a
fail-open: every request is 401'd if the credential is wrong.

Reported by kuangmi-bit (PR #41711 comment, Jun 27).
Attribution: gfdsa's a2a-hermes repro fixture (LOCAL PATCH triad-hermoperator).

The other two patches from gfdsa's fixture were already in our branch:
- k8s Agent Card URL derivation (_request_public_url, commit ea59b85c2)
- send() gating on metadata['notify'] (reply-capture fix, commit ea59b85c2)

All 168 tests pass (151 unit + 17 integration).
2026-08-02 15:10:15 -07:00
Ben Kamholtz
5a8102d71c fix(a2a): JSON-RPC conformance for a2a-sdk 1.1.0 compatibility
Two bugs reported by gfdsa (PR #41711 comment, Jul 12) that break the
official a2a-sdk 1.1.0 Python client:

1. Task objects serialized non-spec createdAt/lastModified fields.
   The A2A v1.0 Task proto (lf.a2a.v1.Task) only has id, contextId,
   status, artifacts, history, metadata. Strict ProtoJSON parsers
   reject unknown fields with ParseError. Removed both fields from
   build_task(); created_at param kept for call-site compatibility.

2. SSE streaming frames were not JSON-RPC wrapped. A2A v1.0 §9.4
   requires data: {"jsonrpc":"2.0","id":...,"result":{StreamResponse}}.
   sse_data() now accepts req_id and wraps in JSON-RPC envelope.
   sse_done() changed from 'data: {}' to SSE comment ': done' so
   SDK doesn't try to parse an empty JSON-RPC response.

All call sites in adapter.py (_emit_terminal, _rpc_message_stream,
_rpc_tasks_subscribe) updated to thread req_id through.

Tests updated: 153 pass (151 unit + 17 integration, including 2 new
tests for JSON-RPC envelope wrapping and fallback behavior).

Refs: gfdsa/a2a-hermes reproduction repo
2026-08-02 15:10:15 -07:00
Kevin (OpenClaw Bot)
b1819ceb7d fix(a2a): align multiplexer with v1 protocol and tenant isolation 2026-08-02 15:10:15 -07:00
Kevin (OpenClaw Bot)
fe1aca5770 feat(a2a): file/data Parts + push config full CRUD
## File/data Parts (v1.0 unified Part)
- file_part(url=, raw=, filename=, media_type=) builds v1.0 file Parts
- data_part(data, media_type=) builds v1.0 data Parts
- message_with_parts(role, parts, context_id=) builds Messages with mixed Part types
- extract_text now renders file/data Parts into the text stream:
  - File with URL: '[file: name] https://url (mediaType)'
  - File with raw: '[file: name] N bytes base64-encoded (mediaType)'
  - Data: '[data (mediaType)]\n{json}'
  - v0.3 file (file.fileWithUri) and data (kind=data) still accepted
- Outbound replies stay text-only (agent produces text)

## Push notification config full CRUD
- get_push_config(task_id, config_id) — retrieve by task, optionally by configId
- list_push_configs(task_id) — list all configs for a task (max 1 per task)
- delete_push_config(task_id, config_id) — remove a config
- New JSON-RPC methods: tasks/pushNotificationConfig/get, /list, /delete
- New adapter handlers: _rpc_push_config_get, _list, _delete
- All return spec-shaped PushNotificationConfig with configId + createdAt

## Tests
- 6 new unit tests for Part builders + extract_text with file/data
- 13 new unit tests for push config get/list/delete (happy + error paths)
- 2 new integration tests over real HTTP:
  - test_mixed_parts_delivered_to_agent: file URL + data JSON reach agent
  - test_push_config_crud_over_http: full create→get→list→delete cycle
- Old test_extract_text_skips_non_text_parts replaced (now renders, not skips)

Total: 151 tests (134 unit + 17 integration), 0 failed.
DESIGN.md updated: file/data Parts and push config CRUD removed from
out-of-scope list.
2026-08-02 15:10:15 -07:00
Kevin (OpenClaw Bot)
41c406e1ab feat(a2a): v1.0 upgrade + full code review fixes
Fable 5 pass: 40 turns, $13.56, 109k output tokens.

## A2A v1.0 upgrade
- SCREAMING_SNAKE task states (TASK_STATE_COMPLETED etc)
- ROLE_USER/ROLE_AGENT message roles
- Unified Parts (no kind field, member-presence discrimination)
- Agent Card: supportedInterfaces[], provider, capabilities.extendedAgentCard
- SSE: member-discriminated statusUpdate/artifactUpdate, closure=terminal
- contextId inside Message (not top-level params)
- ISO 8601 millisecond timestamps, createdAt/lastModified on Task
- New operations: tasks/list, tasks/subscribe
- input-required state reachable via [INPUT_REQUIRED] hint

## Security & correctness (all must-fix from review)
- Slash-command bypass removed — remote peers can't invoke operator commands
- Per-peer token auth (A2A_PEER_TOKENS) replaces self-asserted params.peer
- _pending_replies keyed by task_id with per-context FIFO (no cross-talk)
- Timeout returns TASK_STATE_FAILED, not completed
- reset_turns uses task's context from store (was silent no-op)
- Error codes: spec codes only for spec semantics, custom -32050..-32052
- Real latency metric (was fake 0.0)

## Dead features wired
- Push notifications: inline configuration.taskPushNotificationConfig in
  message/send + tasks/pushNotificationConfig/create. HMAC-signed e2e.
- Dynamic Agent Cards: skills from live tools.registry, A2A_ADVERTISED_TOOLSETS
- Persistence: new a2a_history(context_id) tool recalls conversations
- Dead helpers cut: rate_limit_status, is_open_mode, verify_push_signature,
  turn_count, check_bearer

## Architecture
- TurnTracker/RateLimiter/TaskStore on adapter instance (was module-global)
- Handler class at module level (was untestable closure)
- on_processing_complete for failure/cancel paths
- SSE hang fix: keepalive header no longer prevents socket closure

## a2a_orchestrate kept per user instruction
- best mode: only successful replies considered (long error can't win)
- all-error case: explicit 'All peers failed' listing
- Client paths deduped into _send_task helper

## Tests
- inspect.getsource() tests replaced with behavioral coverage
- 133 total: 118 unit + 15 integration
- v1.0 spec compliance, peer-token auth, FIFO replies, timeout→FAILED,
  tasks/get-after-complete, streaming SSE parse, subscribe replay,
  anti-loop rejection, 429s, push e2e, input-required e2e, orchestrate

## Docs
- DESIGN.md out-of-scope synced with reality
- README and plugin.yaml updated

Still TODO (in DESIGN.md): file/data Parts, push-config get/list/delete,
tenant, gRPC/HTTP+JSON bindings, true mid-turn task abort.
2026-08-02 15:10:15 -07:00
Kevin (OpenClaw Bot)
37481dccf4 fix(a2a): security hardening from code review
Critical fixes:
- SSRF protection: validate push notification callback URLs (block
  internal/private/loopback/metadata, enforce http/https only)
- Request body size limit: 1MB max (prevents memory exhaustion DoS)
- Thread safety: module-level locks for turn tracking, rate limiting,
  and pending task registry (was lazily initialized, racy)
- Peer identity: fall back to client IP when 'peer' field absent
  (prevents rate limiting collapse to single 'unknown' bucket)

Minor fixes:
- Watchdog survives reconnect: clear _watchdog_stop in connect()
- Redact error messages before sending to peers
- Remove dead _streaming_queues state
- Fix duplicate tags key in Agent Card skills
- Always send contextId in a2a_call (fixes client/server mismatch)
- Clear push_callbacks on disconnect
- SSE streaming cleanup via try/finally

16 new tests covering SSRF, body size, thread safety, watchdog
reconnect, error redaction, contextId consistency.
Tests: 97 passed, 3 deselected, 0 failed.
2026-08-02 15:10:15 -07:00
Kevin (OpenClaw Bot)
c6b0e3a80e feat(a2a): Phase 2+3 — SSE streaming, push notifications, anti-loop, orchestrate
Phase 2 (production features):
- SSE streaming: message/stream endpoint with proper event formatting
  (submitted → working → completed → done), keepalive pings
- Push notifications: HMAC-SHA256 signed webhooks via
  tasks/pushNotification/set, auto-fired on task completion
- Rate limiting: token-bucket per peer (A2A_RATE_LIMIT, default 60/min)
- Metrics: /metrics endpoint with counters, latency tracking, uptime
- Orphaned task watchdog: background thread cleans stale tasks (>300s)

Phase 3 (OpenClaw patterns):
- Anti-loop ping-pong: per-context turn counter with configurable
  max (A2A_MAX_PINGPONG_TURNS, default 5, max 20)
- Async durable messaging: pending task registry with register/
  complete/orphaned/clear lifecycle
- Capability-based routing: a2a_orchestrate tool with fan-out modes
  (all/first/best), matches peers by capabilities in config
- Dynamic Agent Cards: skills_from_real_toolsets() builds skill cards
  from actual toolset registry, not just names
- Trusted-peer approval (#56434): A2A_TRUSTED_PEERS env/config,
  is_trusted_peer() gate in inbound handler
- Task completion notifications (#56435): build_task includes
  status.message + artifacts for completed/failed states

Agent Card version bumped to 0.2.0, capabilities now advertise
streaming=True and pushNotifications=True.

Tests: 81 passed (45 existing + 36 new), 0 failed.
2026-08-02 15:10:15 -07:00
Kevin (OpenClaw Bot)
436e5a9cb5 fix(a2a): integrate all follow-up fixes for #41711
Consolidates 5 follow-up PRs onto the a2a-work branch:

1. Reply-capture fix (#56437): adapter.send() now only resolves the
   blocked RPC Future when metadata['notify'] is True (the gateway's
   final-reply marker). Interim sends no longer short-circuit the
   response. Also accepts **kwargs in connect() for reconnect compat.

2. Slash command passthrough (#53743): wrap_inbound() passes /-prefixed
   text through unwrapped so the gateway command processor sees it.
   Fixes /sethome deadlock during A2A onboarding. Documented security
   trade-off (bearer auth at network layer compensates).

3. Routable URL in Agent Card (#53736): _build_card() now derives URL
   from A2A_PUBLIC_URL env > X-Forwarded-Host/Host header > bind host.
   Fixes k8s bug where Agent Card advertised 0.0.0.0.

4. contextId multi-turn memory (#53756): _handle_inbound_task() now
   checks top-level params.contextId first (A2A spec), falls back to
   params.message.contextId (legacy). Outbound a2a_call also sends
   contextId at both top-level and inside message.

5. Type checker fixes (#53759): TypedDict for _SCHEMAS, _FunctionSchema,
   _ToolSchema. Removes str() band-aid casts.

All 45 tests pass including new tests for each fix.
Zero core files modified — only plugins/platforms/a2a/ and tests/.

Credits: @davidrobertson (#56437), @knoal (#53736, #53743, #53756,
#53759), @kuangmi-bit (slash command bug report), @gfdsa (k8s URL bug
report), @shivasymbl (#45996 userContext OBO).
2026-08-02 15:10:15 -07:00
David Robertson
38318cec1e fix(a2a): wait for final replies before resolving RPCs 2026-08-02 15:10:15 -07:00
teknium1
7d57422936 fix(a2a): client tools take args-as-dict positional; accept agent_name alias
Live Tier-3 testing (CLI agent -> a2a tools -> live peer gateway -> model)
surfaced two bugs the kwarg-style unit tests masked:

1. registry.dispatch calls handlers as handler(args, **kwargs) — args is the
   whole dict positional. The handlers used keyword params (url=, agent=), so
   the dict bound to the first param and .strip() raised
   'dict object has no attribute strip'. Rewrote all three handlers to take
   args: dict (matching the spotify/google_meet convention). Added a
   registry-dispatch regression test that exercises the real call path the
   direct-kwarg tests never hit.

2. The model repeatedly reached for agent_name= instead of agent= (6 retries
   before success). Accept agent_name/name and message/text/task aliases so a
   reasonable guess succeeds first try.

Verified live: client agent discovers the peer's Agent Card, calls it, and
gets the reply back (PONG round-trip confirmed on both client audit log and
peer conversation log). 39 plugin tests pass.
2026-08-02 15:10:15 -07:00
teknium1
837003b1ed feat(a2a): consolidated Agent-to-Agent protocol plugin (closes #514)
Single platform-adapter plugin under plugins/platforms/a2a/ — zero core
edits — that supersedes the entire A2A PR/issue cluster. Built on the
ctx.register_platform + ctx.register_tool surface the codebase now exposes.

Outbound (a2a toolset): a2a_discover / a2a_call / a2a_list let the agent
call any A2A-compliant peer over JSON-RPC message/send. Inbound (platform
adapter): a stdlib http.server serves an Agent Card at
/.well-known/agent.json and routes incoming tasks into the agent's LIVE
gateway session (the #11025 insight) — same agent, full memory — returning
the reply over A2A.

Security on by default: no bearer token => 127.0.0.1-only bind; constant-
time bearer auth; inbound prompt-injection filtering + untrusted-peer
framing; outbound credential redaction; append-only audit log; per-context
conversation persistence outside the compaction pipeline.

Stdlib only (no a2a-sdk). 37 tests incl. a live HTTP round-trip
(card + message/send + reply) and a bearer-auth 401 path.
2026-08-02 15:10:15 -07:00
Teknium
7483745da7 feat(gateway): simplex channel enumeration + show configured platforms in hermes send --list
Builds on the adapter list_channels() hook (cherry-picked from #43545 by
@Guoen0):

- plugins/platforms/simplex: implement list_channels() — enumerates
  contacts (/contacts) and groups (/groups) over the live daemon
  WebSocket into the channel directory. Returns None when the WS is
  down so the directory falls back to session discovery instead of
  wiping known targets.
- hermes send --list: merge configured-but-undiscovered platforms into
  the listing. Previously a platform configured only via env (e.g. a
  fresh SimpleX setup used for outbound sends) was silently omitted,
  leaving users guessing at platform names.
- format_directory_for_display(): accept an explicit platforms view and
  render empty platforms with a targeting hint instead of hiding them.
- docs: simplex hermes-send section.

Reported by Fedpostoffice on Discord (simplex missing from
hermes send --list; guessed platform names simplex-chat/simplex-relay).
2026-08-02 15:08:45 -07:00
kshitij
c2ff2e8b17 polish(discord): surface ffmpeg stderr on pcm_to_wav failure
Simplify-pass follow-up on the #68157 salvage (regression-neutral \u2014 the
old temp-file version was equally bare): capture ffmpeg's -loglevel
error output so a CalledProcessError carries the real message, and log
it at the voice-input catch site. Parity with transcription_tools'
ffmpeg call sites. Live-verified: forced ffmpeg failure produces the
captured message in the exception.
2026-08-02 23:21:00 +05:30
Jesse Casco
70a3c2d9c9 perf(discord): stream voice PCM to ffmpeg instead of a temp file
pcm_to_wav staged every captured utterance in a NamedTemporaryFile just to
hand ffmpeg an input path, then unlinked it. Feed the PCM to ffmpeg's stdin
instead: one fewer file created, written, read back and removed per voice
utterance, and the try/finally cleanup goes away with it.

The WAV output deliberately still goes to output_path rather than being
captured from stdout. ffmpeg cannot seek on a pipe, so a piped WAV is
written with placeholder 0xFFFFFFFF RIFF/data chunk sizes -- Python's wave
module then reports 2147483647 frames for a 1s clip, and strict readers
misjudge the length. Writing to the real path lets ffmpeg seek back and
patch the header.

Tests cover both halves: that the PCM goes over stdin with no temp file,
and (when ffmpeg is installed) that the resulting header reports the true
frame count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 23:21:00 +05:30
Teknium
99533f70b1 fix(secrets): resolve google_meet realtime key via secret scope at spawn time
The detached meet_bot child inherits the process environment, not the parent's contextvar secret scope, so process_manager.start() now resolves HERMES_MEET_REALTIME_KEY/OPENAI_API_KEY through get_secret in the parent and passes it explicitly in the child env (spawn-wrap shape). Adds the consolidated per-family regression test file.
2026-08-02 10:04:48 -07:00
Teknium
ebd61ce5ac fix(secrets): scope tier-3 credential reads (teams_pipeline, deepinfra models, FAL/XAI/VERCEL/DAYTONA/GITHUB presence, HERMES_API_KEY display)
NOTION_API_KEY/LINEAR_API_KEY defaults, DEEPINFRA_API_KEY model gate, FAL_KEY via the file's own _scoped_credential helper, XAI/VERCEL/DAYTONA presence checks, GITHUB_TOKEN/GH_TOKEN + GitHub App creds in tirith/skills_hub, and the masked HERMES_API_KEY display in tui_gateway config.show all route through get_secret.
2026-08-02 10:04:48 -07:00
Teknium
2438305a22 fix(secrets): scope browser plugin credential reads (browser_use/browserbase/firecrawl) 2026-08-02 10:04:48 -07:00
Teknium
a23ede5569 fix(secrets): scope image_gen plugin credential reads (openai/deepinfra/krea)
OPENAI_API_KEY, DEEPINFRA_API_KEY and KREA_API_KEY now resolve via get_secret; the OpenAI client is constructed with the scoped key explicitly instead of relying on the SDK's implicit environ read.
2026-08-02 10:04:48 -07:00
Teknium
74b28c8910 fix(secrets): scope memory plugin credential reads (hindsight/honcho/supermemory/mem0/retaindb)
Route HINDSIGHT_API_KEY/HINDSIGHT_LLM_API_KEY, HONCHO_API_KEY, SUPERMEMORY_API_KEY, MEM0_API_KEY and RETAINDB_API_KEY reads through agent.secret_scope.get_secret so multiplexed turns resolve the active profile's key instead of the process environment. Also guard supermemory post_setup's os.environ write on not is_multiplex_active() — writing one profile's key into the process-global env pollutes sibling profiles; the single-profile convenience path is unchanged.
2026-08-02 10:04:48 -07:00
kshitij
9bbd956b73 fix(memory/hindsight): evict timed-out retain ops + coarser status polls
Review follow-up on the #62871 salvage (simplify pass, HIGH):

1. Ops unresolved at the wait deadline were RETAINED in the pending set.
   A permanently failing status endpoint (auth error, endless 500s, or a
   server that loses ops without 404) would grow the set forever and make
   EVERY later prefetch burn the full 10s budget re-polling it — and
   prefetch()'s bounded 3s join sits on the reply path, so that money-quote
   'adds no response latency' claim breaks. Timed-out ops are now dropped
   (identical degradation to prefetch_waits_for_retain=False: possibly
   stale recall) with a WARNING so persistent server trouble is visible.
   Guard test mutation-checked (fails with eviction disabled).

2. Status polls now spaced 0.5s (was 0.05s shared with the local drain
   poll): a wedged op cost up to ~200 get_operation_status round trips
   per prefetch; now ~20 max over the default 10s budget.
2026-08-02 22:33:43 +05:30
And
1be353bf9c fix(memory/hindsight): gate prefetch on server-side retain completion, not just queue drain
Address PR #62871 review: with the default retain_async=True, aretain_batch
returns when the write is accepted, not when it's durable/recall-visible, so
draining the local writer queue (task_done) is not a read-after-write signal.
The next-turn prefetch could still recall before the just-completed turn was
observable on the server.

- Track the async operation_id/operation_ids returned by aretain_batch
- _wait_for_retains_drained now applies two ordered, budget-bounded barriers:
  (1) local writer queue drains, then (2) tracked server-side async ops report
  completion via operations.get_operation_status (an explicit read-after-write
  condition). NotFound (completed+evicted) counts as done; transient errors
  keep waiting until the deadline
- Completed ops are removed from the pending set so later prefetches don't
  re-poll them; the whole wait stays off the reply path
- Add TestPrefetchServerRetainVisibility: op-id tracking (single/multiple),
  no-op tracking when retain_async=False, prefetch waiting for server
  completion before recall, timeout fallback on a wedged op, and NotFound /
  transient-error status handling
2026-08-02 22:33:43 +05:30
And
94b10eccf5 feat(memory/hindsight): order background prefetch after pending retains
Async retain already keeps the memory WRITE off the reply path (writes drain
on the single writer thread while the user gets their response immediately).
This closes the remaining retain/prefetch race: the next turn's warm prefetch
runs on its own thread and could recall BEFORE the just-enqueued retain write
lands, silently dropping the latest turn from recall.

- The background prefetch now waits (bounded) for pending retains to drain
  before recalling, so warmed context includes the just-completed turn.
- The wait runs only on the background prefetch thread, never the reply path,
  so it adds zero latency to the user's response and loses no writes.
- Bounded by prefetch_retain_drain_timeout (default 10s) and polls
  unfinished_tasks so a wedged write can't hang the prefetch.
- New config keys: prefetch_waits_for_retain (default true),
  prefetch_retain_drain_timeout (default 10.0).
2026-08-02 22:33:43 +05:30
JabberELF
7a450ca5ce fix(memory): resolve dim=1 float32/float64 blob ambiguity
When hrr_dim=1 the prefixed float32 blob (4+4=8 bytes) collides in
size with a raw float64 blob (1×8=8 bytes), making the format
discriminator in bytes_to_phases ambiguous — a legacy blob starting
with HRR1 would be misread as a prefixed float32 vector.

- phases_to_bytes now accepts an optional dim and falls back to
  writing raw float64 when the two blob sizes are equal.
- bytes_to_phases prefers the legacy float64 interpretation when
  sizes collide and dim is provided, since phases_to_bytes never
  writes a prefixed float32 blob at dim=1.
- Three regression tests cover dim=1 write, round-trip, and the
  legacy-prefix collision case.

Addresses hermes-sweeper review on PR #30499.
2026-08-02 22:33:13 +05:30
JabberELF
958ffd1085 perf(memory): store holographic vectors as float32 2026-08-02 22:33:13 +05:30
Teknium
359ff01c23 fix(secrets): scope-aware standalone-send and startup credential reads in platform adapters
- discord/telegram/slack/matrix standalone senders read bot tokens via
  get_secret (in-turn: they run inside an installed scope — cron scheduler
  and delegate spawns propagate it), never borrowing another profile's
  env-bridged token under multiplex.
- telegram webhook-secret and matrix access-token/password startup reads
  use the Slack pattern (#59739): get_secret, falling back to os.environ
  only on UnscopedSecretError.
2026-08-02 10:02:33 -07:00
Teknium
ff89f1b862 fix(email): Slack-pattern helper for unscoped default-profile adapter + scope ports/trust flag
Follow-up to the salvaged #59076 commit:

- Replace the bare get_secret import with a module-level Slack-pattern
  helper (_get_esecret): try get_secret, on UnscopedSecretError fall back
  to os.getenv. The DEFAULT profile's email adapter constructs UNSCOPED
  under multiplexing, where a bare get_secret raises and would crash the
  email path on startup — the exact WhatsApp defect fixed in 5438e9c629
  (whatsapp_common._get_wsecret).
- Extend scope coverage to the remaining scope-blind reads:
  EMAIL_IMAP_PORT / EMAIL_SMTP_PORT / EMAIL_POLL_INTERVAL (_esecret_int
  replacing utils.env_int) and EMAIL_TRUST_FROM_HEADER (_esecret_bool
  replacing utils.env_bool).
- Add tests: default-profile unscoped-under-multiplex construction, and
  scoped ports/trust-flag no-environ-inheritance.
2026-08-02 10:01:16 -07:00
shikanga-hermes
f08f403157 fix(email): honor profile secret scope for email adapter env reads
The email adapter (plugins/platforms/email/adapter.py) read
EMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_IMAP_HOST, EMAIL_SMTP_HOST,
EMAIL_ALLOWED_USERS, and EMAIL_ALLOW_ALL_USERS via os.getenv()
directly. In a multiplexed gateway, os.environ holds the default
profile's .env values, so every secondary profile inherited the
default profile's email credentials instead of its own.

This was a sibling of the api_server env-leak bug (#52307/#50051):
the same os.getenv→get_secret migration that PR #50094 applies to
gateway/config.py, but for the email adapter itself, which neither
PR #50094 nor #51374 covers.

Changes:
- plugins/platforms/email/adapter.py: replace os.getenv with
  agent.secret_scope.get_secret for all EMAIL_* credential reads
  (adapter __init__, check_email_requirements, _allowlist_in_effect,
  _dispatch_message allowlist gate, _send_email SMTP helper).
- gateway/config.py: add _getenv/_getenv_str/_getenv_int helpers
  (from PR #50094) and replace os.getenv with _getenv for the email
  block in _apply_env_overrides, so config.platforms[EMAIL].extra
  is populated from the scoped value.
- tests/gateway/test_email_secret_scope.py: 5 new tests covering
  scoped credential reads, environ fallback without scope, missing-
  key-no-leak, allowlist scoping, and check_email_requirements scoping.

Related: #50051, #52307, PR #50094, PR #51374
2026-08-02 10:01:16 -07:00
Teknium
f4b268b785 fix(secrets): Slack-pattern scoped credential reads — Feishu, WeCom, Photon, Buzz
FEISHU_APP_SECRET/FEISHU_ENCRYPT_KEY/FEISHU_VERIFICATION_TOKEN, WECOM_SECRET,
PHOTON_PROJECT_SECRET/PHOTON_SIDECAR_TOKEN (adapter + auth.load_project_credentials)
and BUZZ_PRIVATE_KEY now read through _get_scoped_secret.
2026-08-02 10:00:44 -07:00
Teknium
6333180c9a fix(secrets): Slack-pattern scoped credential reads — ntfy, Home Assistant, SMS, DingTalk
NTFY_TOKEN, HASS_TOKEN, TWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKEN and
DINGTALK_CLIENT_SECRET now read through _get_scoped_secret. Also replaces
the SMS adapter's bare os.environ["TWILIO_AUTH_TOKEN"]/["TWILIO_ACCOUNT_SID"]
__init__ reads (KeyError-prone) with helper reads defaulting to "".
2026-08-02 10:00:44 -07:00
Teknium
d7404f197c fix(secrets): Slack-pattern scoped credential reads — IRC, LINE, Teams, Mattermost
Route IRC_SERVER_PASSWORD/IRC_NICKSERV_PASSWORD, LINE_CHANNEL_ACCESS_TOKEN/
LINE_CHANNEL_SECRET, TEAMS_GRAPH_ACCESS_TOKEN/TEAMS_CLIENT_SECRET and
MATTERMOST_TOKEN reads at __init__/availability/standalone-send time through
a module-level _get_scoped_secret helper (get_secret, UnscopedSecretError ->
os.getenv fallback), mirroring whatsapp_common._get_wsecret / Slack #59739.
Scoped miss returns the default — no cross-profile environ borrow.
2026-08-02 10:00:44 -07:00
spfcraze
89f74d58f6 perf(memory): hoist loop-invariant HRR encodes out of retrieval loops
FactRetriever.search() re-encoded the query vector once per candidate,
related() re-encoded both role atoms once per fact row, and probe()
re-encoded the role-content atom once per row. All three encoders are
deterministic (SHA-256 counter blocks), so the hoisted vectors are
bit-identical to the per-iteration values they replace.

Measured (300-fact store, dim=1024, median of 30 calls): search()
11.62 -> 1.46 ms/call (8.0x; encode_text 30 -> 1 per call), related()
63.08 -> 16.17 ms/call (3.9x; encode_atom 601 -> 3 per call), probe()
431.93 -> 389.36 ms/call (1.1x; dominated by per-fact content encoding,
which is inherent to the algorithm and unchanged).

Tests: call-count regression tests for each hoist plus a bit-exact
parity test of search() against the pre-fix per-candidate loop.
2026-08-02 21:16:34 +05:30
kshitij
c4d67c3add refactor(discord): extract shared reply-reference helpers; fix PartialMessage comment
Review follow-ups on the #76357 salvage:
- _message_reference_from_ids + _reply_reference_for_send collapse the
  3x duplicated MessageReference construction (naming mirrors telegram's
  _reply_to_message_id_for_send).
- The overflow elif's comment claimed PartialMessage has no to_reference;
  discord.py 2.7.1's PartialMessage does (message.py L1901) — the branch
  is belt-and-suspenders for duck-typed priors, now labeled as such.
2026-08-02 21:14:36 +05:30
spfcraze
01ca8be207 perf(discord): build reply references from ids instead of fetch_message
Every reply paid one extra Discord API round trip: the text send path,
the voice send path, and the edit path each called fetch_message() just
to obtain a reference or an editable handle. Discord resolves
message_reference payloads from ids alone, and PartialMessage.edit()
works without a fetch — so build MessageReference directly (with
fail_if_not_exists=False, preserving the deleted-target behavior the
existing send-side 10008 retry already covered) and use
channel.get_partial_message() for edits. Overflow continuations keep
threading via an ids-built reference fallback for PartialMessage.

Measured by call count (deterministic): reply sends and edits now make
ZERO fetch_message calls where they made 1 per reply and 1 per edit
(including every streaming edit tick).

Tests: pin that first-mode replies construct the reference without any
fetch, deleted-target retry test updated to assert fetch await_count==0
(retry now happens purely send-side), overflow/edit mocks retargeted
from fetch_message to get_partial_message (any fetch regression breaks
all five). Note: 4 discord-suite failures are pre-existing ordering
flakes — identical with the change stashed on clean main.
2026-08-02 21:14:36 +05:30
Teknium
5438e9c629 fix(whatsapp): default-profile UnscopedSecretError fallback + full bridge env set
Follow-ups on the #75382 salvage (review findings):
- _wenv/_get_wsecret now catch UnscopedSecretError and fall back to
  os.getenv for the DEFAULT profile's adapter, which constructs and sends
  outside any _profile_runtime_scope under multiplexing — a bare
  get_secret would crash its WhatsApp path (fixing one profile by
  breaking another). Same pattern as Slack SLACK_APP_TOKEN (#59739) and
  the Matrix recovery key. Scoped misses still return the default — no
  cross-profile borrow.
- bridge_env overlay extended to the full WHATSAPP_* set bridge.js
  consumes (DEBUG, FORWARD_OWNER_MESSAGES, REPLY_PREFIX,
  MAX_MESSAGE_LENGTH, CHUNK_DELAY_MS, SEND_TIMEOUT_MS).
- Removed the always-true conditional on WHATSAPP_MODE injection.
2026-08-02 00:11:50 -07:00
x7peeps
4f4ea9a6de fix(whatsapp): route WHATSAPP_* env reads through secret scope for multiplex profiles
Fix #75349

Root cause:
Under multiplex_profiles, secondary profiles run inside
_profile_runtime_scope which installs a per-profile secret scope via
set_secret_scope.  The WhatsApp adapter (and the shared
WhatsAppBehaviorMixin + Cloud API adapter) read WHATSAPP_MODE,
WHATSAPP_DM_POLICY, etc. via raw os.getenv(), bypassing the secret
scope.  Since os.environ doesn't contain secondary profile .env values,
the bridge silently falls back to 'self-chat' and rejects all inbound
messages with self_chat_mode_rejects_non_self.

Fix:
- Add _wenv() helper in adapter.py that reads WHATSAPP_* vars through
  get_secret() (agent.secret_scope), which honors the active scope.
- Replace all os.getenv('WHATSAPP_*') calls in adapter.py,
  whatsapp_common.py, and whatsapp_cloud.py with get_secret()-based
  equivalents.
- Inject resolved WHATSAPP_* values into the bridge subprocess
  environment so the Node.js bridge (which reads process.env) sees the
  profile's own configuration.

Changes:
- plugins/platforms/whatsapp/adapter.py: 37 lines (+ helper, bridge_env
  injection, 2 os.getenv→_wenv)
- gateway/platforms/whatsapp_common.py: 13 lines (6 os.getenv→_get_wsecret)
- gateway/platforms/whatsapp_cloud.py: 21 lines (9 os.getenv→_get_wsecret)
- New regression test: 6 test cases covering scope isolation, fallback,
  and cross-profile non-leakage.
2026-08-02 00:11:50 -07:00
sergioperezcheco
153442dd5b fix(matrix): honor profile secret scope for recovery key under multiplex
The Matrix adapter read MATRIX_RECOVERY_KEY via os.getenv, so under
gateway.multiplex_profiles every profile resolved the default profile's
key. That produced "recovery key verification failed: Key MAC does not
match" and broke E2EE for secondary profiles (#69090).

Route the read through agent.secret_scope.get_secret, which honors the
active profile's scope, with an os.getenv fallback for an unscoped read
under multiplex (default-profile startup loop) — mirroring the Slack
app-token pattern (#59739). Applied to both the startup verification
site and the status diagnostic.

Fixes #69090
2026-08-02 00:11:50 -07:00
fangliquanflq
f5ca0e2f0b fix(gateway): honor empty WhatsApp allow_from over env grants
Select allowlist source by config key presence so allow_from: [] does not fall through to WHATSAPP_* env carriers on Baileys or Cloud.
2026-08-02 11:50:05 +05:30
fangliquanflq
810c8777e1 fix(gateway): preserve WhatsApp allowlist config precedence on live checks
Track which source seeded the DM allowlist so live intake does not let a
stale env carrier override explicit config, while env-seeded adapters still
reread pairing mutations.
2026-08-02 11:50:05 +05:30
fangliquanflq
ddfc6342ad fix(gateway): revoke WhatsApp sole allowlist entry without restart
Clear live adapter _allow_from on pairing revoke and re-check DM
allowlist authz so sole-entry removal takes effect without restart.
2026-08-02 11:50:05 +05:30
Gille
58e85f4314 fix(browser): replace expired cloud sessions 2026-08-02 11:18:41 +05:30
Jaret Bottoms
eec6d3efde fix(teams): suppress SDK import-time dotenv instead of clearing environ
Teknium review on #62947: os.environ.clear()/update around deferred
loaders is unsafe under concurrency and misses teams_pipeline's direct
adapter import.

Defer microsoft_teams binding in the Teams adapter, no-op
dotenv.load_dotenv while the SDK imports, keep api_server explicit
disable, and add SDK-import + load_gateway_config canaries.

Fixes #62935
2026-08-01 15:34:19 -07:00
brooklyn!
97971643ab Merge pull request #76417 from NousResearch/bb/kanban-model-picker
Pick a kanban task's model and thinking depth from the board
2026-08-01 16:55:01 -05:00
Brooklyn Nicholson
f0ed0aebbc feat(kanban): expose the per-task reasoning effort over REST
Carries the new column through create, PATCH, and bulk. Clearing is an
explicit clear_reasoning_effort flag rather than a null, because a null in a
PATCH body means "field not sent", not "set to NULL" — the same shape the
model override already uses, and the reason "none" can stay a real value.

Tests cover normalization, the depth-survives-a-model-clear invariant, both
spawn-argv branches, and the REST round-trip. One asserts the worker CLI
actually accepts the --reasoning flag the dispatcher emits: a spawn arg no
parser accepts would fail every dispatch while every unit test stayed green.
2026-08-01 16:10:03 -05:00
Teknium
81c0691e17 fix(gateway): per-profile Discord/Telegram allow-deny gates under multiplex_profiles
Under gateway.multiplex_profiles, Discord and Telegram authorization gates
(allowed/ignored channels, allowed users/roles, allow-all flags) were read
from process-global os.environ, populated first-writer-wins by the YAML->env
bridge in each adapter's _apply_yaml_config. The first profile to initialize
pinned its allow/deny lists — and its ALLOW_ALL flags — for every other
profile in the process (issue #72348, incl. the Telegram mirror reported in
the thread).

Fix (per-adapter-instance gate reads, whole class):

- gateway/authz_mixin.py: new _platform_gate_env — scope-authoritative gate
  read: under an installed profile secret scope with multiplex active, a
  missing key returns the default instead of falling through to os.environ
  (which may hold another profile's value). Single-profile behavior is
  byte-identical to os.getenv.
- Discord adapter:
  - connect() snapshots all gate env vars (_GATE_ENV_KEYS) inside the owning
    profile's runtime scope into a per-adapter dict; new accessors
    (_get_allowed_channels/_get_ignored_channels/_get_allowed_users/
    _get_allowed_roles/_get_no_thread_channels/_discord_allow_all_users/
    _gateway_allow_all_users/_get_allow_bots) resolve snapshot -> config.extra
    -> scope-aware env, replacing every raw os.getenv gate read: on_message
    channel gates, _is_allowed_user allow-all flags, slash authorization,
    fail-closed diagnostics, missed-message backfill, bot-message gating,
    and _component_check_auth (component buttons).
  - _apply_yaml_config always seeds gate values into PlatformConfig.extra
    (incl. new allowed_roles / allow_all_users keys) and SKIPS the
    process-global env writes when loading a profile-scoped config under
    multiplex; the legacy first-writer env bridge is preserved verbatim for
    single-profile deployments.
  - _resolve_allowed_usernames no longer unconditionally rewrites
    os.environ[DISCORD_ALLOWED_USERS] — under multiplex the resolved IDs stay
    adapter-local (snapshot refresh); single-profile keeps the env rewrite.
- Telegram adapter (mirror of the same class): intake prefilter and
  callback-auth fallbacks, _telegram_auth_env_configured, and the
  allowed/ignored chats-topics-threads getters now read via the scoped gate
  reader; _apply_yaml_config skips authorization env writes for
  profile-scoped loads and seeds free_response_chats/ignored_threads extras.

Regression tests (tests/plugins/platforms/test_discord_gate_isolation.py):
two adapter instances with different allow-lists enforce their OWN lists
order-independently across message, slash, and component gates; negative
allow-all case proves profile A's open-access flag cannot authorize profile
B; username-resolution env-clobber; YAML-bridge seeding/skip matrix; and the
Telegram scoped-reader matrix. Sabotage-verified: reverting either the
Discord snapshot accessors or the Telegram scoped reader fails 12/2 tests
respectively.

Credit: builds on the per-adapter accessor direction of PR #72427
(@JonthanaHanh) and the scope-aware-reader approach validated live on v0.19.0
by @yournetworkplug-ctrl for the Telegram mirror; scope corrections from
jackjin1997's and cal88's analysis in the issue thread (allow-all flags,
unguarded username-resolution env write, per-site channel reads).

Fixes #72348
2026-08-01 10:51:42 -07:00
kshitij
151e72a5fc refactor: simplify pairing check return and drop over-defensive getattr
Follow-up cleanup from /simplify-code review:
- Replace 'if X: return True / return False' with 'return X'
- Replace 'getattr(source, "chat_type", None) or ""' with 'source.chat_type'
  (SessionSource.chat_type is a non-optional str field)
2026-08-01 12:38:38 +05:30
xxxigm
dae4cf6bb6 fix(telegram): let pairing-bound DMs past early auth with allowlist
The #40863 intake prefilter rejected unauthorized DMs whenever an allowlist
existed, so gateway pairing never ran even when the operator set
telegram.unauthorized_dm_behavior: pair (which must win over the #9337
allowlist silence default). Pass those DMs through; groups stay blocked.
2026-08-01 12:38:38 +05:30
Eugeniusz Gilewski
64dd865912 fix(deps): repair Google transitive security floors (#72108)
Google API and authentication packages permit vulnerable httplib2 and pyasn1
transitives, while the Workspace and Google Chat runtime installers previously
treated any importable version as sufficient. Existing environments could
therefore remain vulnerable after the project dependency pins were repaired.

Carry the fixed versions through the Google and Vertex extras, lazy feature
requirements, lockfile, and both runtime installers. Route the documented
Google Chat installation path through its maintained secure requirements
instead of an unconstrained direct pip command.

Detect stale distributions, install only unsatisfied requirements, and verify
the result before continuing. Behavioral tests cover those repair invariants
without freezing manifests, lockfiles, or complete package sets.

Related #72108
Extracted from #72840
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-07-31 23:18:38 -07:00
Teknium
fae0c4f5f4 fix(hindsight): create embedded profile env file owner-only (0600)
The embedded Hindsight daemon's profile env file carries the plaintext
HINDSIGHT_API_LLM_API_KEY but was written via bare write_text(), leaving
it with umask-derived (typically world-readable) permissions.

- Create/truncate the file via os.open(..., 0o600); chmod a pre-existing
  file to 0600 BEFORE writing new secret bytes.
- Post-write validation on POSIX: verify 0600, retry chmod, and raise if
  the file still isn't owner-only.
- If validation fails, unlink the secret file so a plaintext key is never
  left behind with unverified permissions.
- Regression tests under tests/plugins/ for fresh-write mode, tightening a
  pre-existing 0644 file, and cleanup on validation failure.

Narrowed reimplementation of #74236 confined to plugins/memory/hindsight/;
the core utils.py atomic-replace opt-out from the PR was dropped.

Co-authored-by: carrion256 <carrion256@proton.me>
2026-07-31 22:39:34 -07:00
Drexuxux
57a807373d fix(telegram): send_image uploads still went out on the short read timeout
524ab5399 widened the media read timeout from send_video to "all upload send
paths" - send_voice/send_audio/send_photo/send_document/send_media_group/
send_animation. Both send_photo calls inside send_image() were missed, so
they still ran on the short timeout the rest of the Bot API is tuned for
while the sibling media paths already pass it.

The missed pair is the worst one to miss: send_image tries a URL send first,
then falls back to downloading the image and uploading the bytes - the path
documented as "supports up to 10MB", i.e. the slowest send in the file and
the one whose server-side processing wait most often outlasts the short
budget. When it times out the handler's last resort posts the bare URL as
text, so the picture silently never arrives as a picture.

Pass _MEDIA_SEND_READ_TIMEOUT on both, covered by two behavioral tests that
drive send_image for real - the URL send and the forced byte-upload fallback
- and assert the read_timeout that actually reaches the Bot API.
2026-07-31 22:36:10 -07:00