Layers richer config validation, an explicit adapter allowlist, and a
stricter bridge routing path on top of upstream `d336314` ("binding
rule"), which shipped the same `channel_id` field as our PR #1127 in a
parallel implementation. Replaces upstream's `sender_user_id`/
`platform_id` heuristic with a single source of truth shared between
config validation and routing.
## What changes vs upstream `d336314`
**Data model** (`openfang-types/src/config.rs`)
- `#[serde(deny_unknown_fields)]` on `AgentBinding` so a typo at the
binding level (e.g. `match_rules` plural) fails loudly instead of
silently leaving the rule defaulted to "match everything". Upstream
has it on `BindingMatchRule` only.
- New `pub const CHANNELS_WITH_PLATFORM_ID_AS_CHANNEL` (19 adapters:
discord, slack, telegram, matrix, mattermost, teams, webex,
rocketchat, nextcloud, pumble, revolt, guilded, feishu, lark,
keybase, google_chat, line, twist, flock, twitch). Single source of
truth shared with the bridge — no drift between routing and
validation paths possible. Hybrid adapters (IRC, Zulip) are
excluded; see source comment.
- Startup validation: warn when a binding sets `channel_id` for a
non-supporting adapter, or when `channel_id` is set without
`channel`. Documents the metadata escape hatch in the warning.
- Top-level `KernelConfig` keeps no `deny_unknown_fields` — comment
explains the §5.5 scoping decision so a future reader doesn't
"tighten" it without realizing it would break forward-compat keys.
**Bridge** (`openfang-channels/src/bridge.rs`)
- Replaces upstream's `sender_channel_id()` heuristic ("if metadata has
`sender_user_id` and it differs from `platform_id`, assume
`platform_id` IS the channel") with `binding_context_for(message)`,
which delegates to `ChannelMessage::channel_id()`. The heuristic
worked for Discord/Slack but would fail silently on Matrix, Teams,
Mattermost, Telegram, etc. — adapters whose `platform_id` IS the
channel ID but whose metadata does not happen to set
`sender_user_id` differently.
- Routes both dispatch paths (text + blocks) through
`resolve_with_context` so `guild_id` and `channel_id` bindings can
match. (Upstream's `resolve_with_channel_id` only handled
channel_id.)
**Channels types** (`openfang-channels/src/types.rs`)
- New `ChannelMessage::channel_id()` accessor: reads `platform_id` for
allowlisted adapters, falls back to `metadata["channel_id"]` for
opt-in adapters, else `None`. Case-folds `Custom(...)` variants so
a stray `Custom("Twitch")` cannot silently slip past the allowlist
(the validation path lowercases user input — accessor must match).
**Tests** (+8 in `bridge.rs`, +5 in `config.rs`, +1 in `types.rs`)
- Bridge: Discord/Telegram/Matrix/custom-supported/user-id-only-adapter
/metadata-fallback/guild-id-from-metadata/Email-returns-None
coverage of `binding_context_for` and `channel_id()`.
- Config: typo rejection on both `BindingMatchRule` and `AgentBinding`;
channel_id-without-channel warning; unsupported-adapter warning;
no-warning for discord/slack/telegram.
- Types: `channel_id()` case-insensitivity for Custom variants
including the Lark/Feishu Intl spelling.
**Docs** (`docs/channel-adapters.md`)
- Routing section rewritten: bindings are step 1 in the resolution
order. New "Bindings" subsection documents the rule shape, the
`peer_id` vs `channel_id` distinction (the easy confusion), full
specificity table, the adapter allowlist with the metadata escape
hatch, and the strict-field parsing rule.
## Why this layering instead of replacing d336314
Upstream's commit and our PR #1127 are functionally equivalent on
Discord and Slack. Shipping a richer extension on top is less churn
than ripping out the upstream commit and substituting ours, and keeps
the API surface upstream just added (`resolve_with_channel_id`)
intact for any third-party consumers.
`cargo check --workspace` and `cargo test -p openfang-types -p
openfang-channels --lib` (850 tests) pass.
Dashboard passwords were hashed with plain SHA256 (no salt), vulnerable
to rainbow tables and GPU brute force. Switch to Argon2id with random
per-hash salts. Breaking change: existing SHA256 hashes in config.toml
must be regenerated with `openfang auth hash-password`.
Add a [heartbeat] section to KernelConfig so users can tune the
inactivity timeout that determines when agents are marked unresponsive.
Reactive agents (hands) that sit idle between infrequent requests were
getting marked as crashed after the hardcoded 180s default, causing
the first request after idle to fail.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- GET /api/cron/jobs: show actual {jobs: [...], total} wrapper and
document the ?agent_id query filter
- POST /api/cron/jobs: fix status code to 201 Created, show the actual
{result: "<stringified-json>"} response shape
- GET /api/cron/jobs/{id}/status: show full JobMeta structure with
nested job object, one_shot, last_status, consecutive_errors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add POST /api/cron/jobs/{id}/run endpoint that triggers a cron job
immediately without waiting for its next scheduled fire time. The job
executes asynchronously in the background and its status can be polled
via the existing /status endpoint.
Key changes:
- Extract per-job execution logic from the inline cron tick loop into
a reusable `cron_run_job()` method on OpenFangKernel, called by both
the background scheduler and the new API endpoint
- Add `reserve_run()` on CronScheduler to pre-advance next_run for
overdue jobs before spawning manual runs, preventing duplicate
execution from the scheduler tick (only advances when next_run <= now
to avoid skipping imminent scheduled runs)
- Fix dashboard scheduler.js to call the correct cron API endpoint
instead of the legacy /api/schedules/ path
- Document all cron/scheduler endpoints in api-reference.md
Partially addresses upstream issue #634.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rebased on latest main (f1ca527) after codebase changes. This is a
fresh submission after PR #22 was closed as stale.
## Why This Feature
Enables enterprise GCP deployments using existing service accounts
instead of requiring separate Gemini API keys. Many organizations
already have GCP infrastructure and prefer OAuth-based auth.
## What's New
- VertexAIDriver with full streaming support
- OAuth 2.0 token caching (50 min TTL) with auto-refresh via gcloud
- Auto-detection of project_id from service account JSON
- Security: tokens stored with Zeroizing<String>
- Provider aliases: vertex-ai, vertex, google-vertex
- Compatible with new ContentBlock::provider_metadata field
## Testing
- 6 unit tests passing
- Clippy clean (no warnings)
- End-to-end tested with real GCP service account + gemini-2.0-flash
- Both streaming and non-streaming paths verified
## Usage
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa.json
# Set provider=vertex-ai, model=gemini-2.0-flash in config.toml
The docs listed `duckduckgo` as the config value but the actual serde
deserialization produces `duck_duck_go` — serde's rename_all = "snake_case"
on the DuckDuckGo enum variant inserts underscores at each word boundary.
Updated all three occurrences in configuration.md to match the real value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add WebSocket long-connection receive mode for the Feishu/Lark adapter
as an alternative to webhook callbacks. WebSocket mode is enabled by
default, requiring no public IP or domain.
- FeishuConnectionMode enum (Webhook/WebSocket) with mode dispatch
- Protobuf binary frame parsing (prost) based on Feishu pbbp2 protocol
- Auto-reconnect, ping/pong heartbeat, ACK, multi-part payload combine
- handle_data_frame reuses parse_event() pipeline (dedup, group filter)
- FeishuMode config enum with bridge-layer adapter creation per mode