feat(lineworks): LINE WORKS platform adapter - #1456
Conversation
Gateway adapter (crates/openab-gateway/src/adapters/lineworks.rs):
- webhook handler with X-WORKS-Signature HMAC verification and bot-id check
- service-account JWT token cache (RS256 jwt-bearer grant, auto-refresh)
- reply dispatch: users/{id} vs channels/{id} endpoint via user: prefix,
10k-char splitting, 401 refresh-retry, unsupported commands no-op
- 15 unit tests (wiremock) covering signature, mapping, token, dispatch
Config-first threading per openabdev#1375: [lineworks] section in openab-core
config, GatewayLineWorksConfig bridge, conformance COVERED + prefix, docs.
Platform-list wiring hit during real-stack bring-up (checklist in PLAN.md):
NON_EDITABLE_PLATFORMS, root lineworks feature + unified, has_unified_platform,
gateway trust registry, cron VALID_PLATFORMS/configured_platforms/adapters.
Verified end-to-end against the real LINE WORKS API with the unified
binary + claude-agent-acp in local k8s: inbound/outbound messages, cron
to channel, and 1:1 sends addressed by loginId (email) instead of UUID.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Channel (group) messages must @-mention the bot to reach the agent; the
mention is stripped from the prompt on a hit and 1:1 messages always pass.
The LINE WORKS callback carries no structured mention data (content is
type + text only), so matching is plain-text against the bot display name,
resolved from GET /bots/{botId} at first use (cached) or overridden via
[lineworks].bot_name. require_mention = false restores ambient listening.
Fail-open when the name cannot be resolved.
Config-first: require_mention / bot_name fields + LINEWORKS_* env fallbacks,
conformance COVERED entries, config-reference docs. 4 new unit tests.
Verified live: bot name auto-resolved ("Nuphos (Dev)"), unmentioned channel
message dropped, mentioned message answered with the mention stripped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New lineworks_flex.rs converts agent markdown to a LINE WORKS flex bubble: headings → bold sized text, fenced code → shaded box, lists → bulleted lines, blockquotes → gray italic, inline bold/italic/code → text spans. Dispatch tries flex first (rich_messages config, default true) and falls back to the plain-text path when the reply has no markdown, exceeds the size ceilings (20KB / 120 components), or the API rejects the payload — content is never lost. altText carries the first 400 chars for notifications. Config-first: [lineworks].rich_messages + LINEWORKS_RICH_MESSAGES, conformance entry, docs. 12 new unit tests (renderer + dispatch flex/ fallback/plain paths). Verified live: markdown reply delivered as flex (kind="flex" in logs, accepted by the real API). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LINE WORKS has no reaction or typing-indicator API, so users saw nothing between sending a message and the full reply landing. When [lineworks].ack_message (LINEWORKS_ACK_MESSAGE) is set, the adapter pushes that short text as soon as an inbound message passes signature/mention/ trust gates and is handed to the agent. Fire-and-forget: an ack failure never delays or blocks the reply path. Unset = disabled (default). Cron messages do not ack (they bypass the webhook). Config-first field + conformance entry + docs, 1 new webhook test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Message events carrying a fileId now download the content via
GET /bots/{botId}/attachments/{fileId}, following the 302 redirect to the
storage host manually so the Authorization header survives the cross-host
hop (reqwest strips it on automatic redirects). Bytes flow through the
shared media pipeline: images resize/compress (≤1200px JPEG) for LLM
vision, audio stores raw for STT, files are gated by the text-extension
whitelist (binaries rejected without download). Size caps enforced during
streaming (10MB image / 20MB audio+file); every failure surfaces as a
rejected attachment with a category reason instead of a dropped event.
Adds LINE WORKS to the inbound-attachments support matrix. 5 new tests
(redirect flow, whitelist, size cap, config-error mapping).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docs/platforms/schema/lineworks.toml: full three-part schema (capability facts with official-doc sources, the closed 17-feature support set with code refs, five dated quirks) — passes the platform-schema conformance suite. - docs/lineworks.md: operator setup guide (Console app/bot creation, callback CA-cert requirement, config example, behavior notes, cron loginId targeting, trust). - Register the platform in docs/platforms/README.md; rustfmt the two new adapter source files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
65ed43d to
2302b97
Compare
|
Rebased onto latest CI: everything passes except |
|
CHANGES REQUESTED Reviewed at
Requested Changes
Signature verification, token refresh, Unicode-safe splitting, attachment size caps, endpoint routing, and Flex fallback were also inspected. No local checkout was available, so tests were not independently rerun. Decision: CHANGES REQUESTED pending F1–F5. |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Important
CHANGES REQUESTED
Consolidated review: #1456 (comment)
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Important
CHANGES REQUESTED
Consolidated review: #1456 (comment)
…irst, least privilege
F1 Callback acks after signature/envelope validation only; mention gating,
ack message, and attachment download move to a bounded post-ack worker
(new lineworks webhook semaphore, same pattern as the LINE adapter).
F2 Authorization before I/O: classification is now pure (classify_event
returns a PendingAttachment descriptor); the download runs only after
the mention gate passes, so dropped events consume no network/storage.
F3 Explicit loss policy: the webhook refuses to ack (503) when no gateway
event consumer is attached, and a post-ack enqueue failure is surfaced
at error level with the event id (LINE WORKS never resends callbacks).
F4 Single activation validator: ResolvedLineWorks::is_complete() (config →
env with empty values treated as unset on both layers) is now used by
startup preflight, cron platform registration, and matches adapter
construction — LINEWORKS_BOT_ID alone no longer activates the platform.
F5 Least-privilege OAuth: scope is now exactly bot.message,bot.read
(verified against the live token endpoint); docs updated and the token
test asserts the exact scope string.
F6 X-WORKS-BotId is required: missing or mismatched header → 401 before
any parsing/dispatch.
P1 Attachment downloads reuse one adapter-owned client (connection pool).
P2 Bot-name resolution is single-flight (mutex held across the fetch).
P3 split_text tracks the running char count — linear instead of quadratic.
H1 Non-HTTPS redirect targets are refused before the bearer token is
forwarded (loopback HTTP allowed for the test harness only).
New regression tests: slow attachment does not delay the ack, gated channel
message never hits the download API (expect-0 mock), missing consumer →
503, missing BotId header → 401, insecure redirect refused, exact scope
string, activation validator missing/empty-credential matrix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All review findings addressed in 7396273 — thank you for the thorough review. Point-by-point:
Validation: |
The conformance anti-drift check correctly caught that build_gateway_event was renamed to classify_event in the review-fix commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
CHANGES REQUESTED
…hing, validation edges
F1/F4 The trust probe now runs for EVERY accepted event and the ack is
awaited inline inside the bounded worker AFTER the gates: untrusted
senders get no ack, no download, and bursts cannot fan out unbounded
outbound tasks. (Standalone-gateway probe absence documented on the
type — that deployment has no core trust registry by design.)
F2 Key material must parse (jsonwebtoken RS256) at activation AND at
adapter construction — invalid PEM now fails startup preflight
instead of the first token exchange.
F3 Explicit ingress full policy: try_acquire; saturation answers 503
immediately with an error log instead of accumulating waiting
request handlers.
F5 Bot-name lookup failures are negatively cached (60s cooldown): an
upstream outage retries once per window instead of per event.
F6 Both dispatch call sites (unified adapter + gateway reply loop) now
surface a failed reply delivery at error level.
F7 Mention matching is boundary-aware: "@bot" no longer matches inside
"@Bottage"; only boundary-valid mentions are stripped.
F8 Token exchange checks HTTP status before parsing the body, so
upstream 4xx/5xx surface as "token endpoint HTTP <n>" errors.
F10 config-reference.md attachment paragraph updated to match the
implemented download pipeline.
New tests: boundary matrix, invalid-PEM rejection, saturation → 503,
HTTP-error surfacing, negative-cache single-hit (expect(1) mock).
Live-verified on a real deployment: boundary false-positive dropped,
untrusted 1:1 attachment skipped download + deny-echo, normal mention
reply + ack unchanged.
F9/F11/F12 are answered with rationale on the PR (shared unified-adapter
behavior, probe generalization follow-up, observability follow-up).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round-3 findings addressed in ea358eb — 9 fixed, 3 answered with rationale below. Fixed (F1–F8, F10):
Answered with rationale (proposed as Follow-ups):
Validation: 308 gateway tests, conformance, clippy clean, |
has_unified_platform reaches the symbol through a runtime cfg! check, so default-feature builds (no lineworks) need a stub answering false. Adds the default-feature combo to the local validation matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ation, cron threads, ack docs - F1: two-tier bounded ingress — a 64-slot queue absorbs bursts while the 8-worker pool bounds concurrent slow work; a signed callback is only rejected (503) on queue overflow, no longer whenever all workers are busy - F2: token invalidation is conditional on the failed token, so a delayed 401 cannot clear a token another concurrent task just refreshed - F3: exclude lineworks from synthetic cron-thread creation (no thread API; flat-channel delivery documented in docs/cronjob.md) - F4: config-reference now documents the awaited-in-bounded-worker ack behavior instead of fire-and-forget Regression tests: saturated_workers_queue_callback_then_drain, ingress_queue_overflow_returns_503, delayed_401_does_not_clear_refreshed_token, lineworks_cron_never_requests_synthetic_thread
This comment has been minimized.
This comment has been minimized.
|
LGTM What This PR Does How It Works Findings
Feedback Reconciliation Inline Review Threads Baseline And Validation
Three Reasons We Might Not Need It
Conclusion |
|
Note LGTM ✅ — architecture-conformance review against the identity-trust-none ADR (#1291): the adapter follows the three-layer Receiver → Trust Gate → Handler model correctly. The two actionable gaps found in this audit (F2, F3) are now addressed in What This PR DoesAdds a LINE WORKS gateway platform adapter (signed webhook ingest, service-account JWT auth, flex-rendered replies with plain-text fallback, inbound attachments, mention gating, ack message, cron/trust wiring) plus its config section, docs, and platform schema. How It Works (trust-architecture view)This round specifically audited conformance with the three-layer trust architecture adopted in #1291 (
Findings
What 527632e adds
Baseline Check
5️⃣ Three Reasons We Might Not Need This PR
Verified (at
|
… rows Graduates lineworks L3 identity trust off the deprecated uniform GATEWAY_ALLOW_ALL_USERS/GATEWAY_ALLOWED_USERS seed (review follow-up, now in-PR): - [lineworks].allow_all_users / allowed_users (LINEWORKS_ALLOW_ALL_USERS / LINEWORKS_ALLOWED_USERS env fallback; deny-all default per identity-trust-none ADR), trust_config() view like wecom/googlechat/teams - wired into platform_trust_override keyed by lineworks_activated(), so the Phase-1 deprecation warning now fires when trust still rides the uniform GATEWAY_* seed (removes the silent Phase-2 breakage path, openabdev#1356) - conformance COVERED set + config-reference/lineworks.md/config.toml.example updated; schema trust_gate note reflects the first-class section - ADR pinned tables gain the LINE WORKS rows: is_bot always false (user messages only, LINE precedent) and push-send deny-echo (no reply-token mechanism; per-sender throttle; unpublished quota ?-flagged)
Per D-17. CI has not run since 30e0475 at 12:07Z: openabdev#1456 (LINE WORKS, c5a75ac) merged to main at 12:50:10Z and this branch has been CONFLICTING since. `ci.yml` triggers on `pull_request`, which needs GitHub to compute a merge ref; it could not, so no run was ever created. `review-contract.yml` and the discussion check use `pull_request_target`, which reads the base branch and needs no merge ref — so they kept reporting success. Every check looked green because the ones still green were the ones that do not need the broken thing. Merge rather than rebase: rebasing 141 commits requires a force-push and the runbook forbids force-pushing this branch. A merge commit resolves the conflict without rewriting history. Two conflicting files, both additive, resolved by keeping both sides: - Cargo.toml — main added `lineworks` to `unified` and a new `lineworks` feature line; this branch had edited the `acp` line to append `openab-core/acp-mcp`. `unified` now carries acp AND lineworks, and the acp line keeps openab-core/acp-mcp. Dropping main's lineworks entry would silently un-ship a merged adapter and would not have failed the gate. - crates/openab-gateway/src/lib.rs — main added the LINE WORKS concurrency consts and the IngressTrustProbe alias; this branch added `acp_tunnel_registry` to AppState in three places. Different regions. Verified before committing: no conflict markers; `unified` carries both; acp_tunnel_registry appears 6x and the lineworks consts/alias 24x; and the resolved tree is byte-identical to one that had already passed the full gate in a scratch worktree. Gate on the merged tree: 14/14, including `cargo test --workspace` actually EXECUTED under default features — the dimension CI runs and this gate never had (it only ran `--no-run`, compiling those targets without running them).
Discord Discussion
https://discord.com/channels/1491295327620169908/1491365158868619404/1530065146326683769
What problem does this solve?
OpenAB has no adapter for LINE WORKS — the workplace edition of LINE, widely deployed in Japanese/Korean/Taiwanese companies. Teams running their org chat on LINE WORKS currently cannot reach an OpenAB agent at all. This PR adds a full gateway platform adapter: signed webhook ingestion, service-account JWT auth, rich replies, inbound attachments, mention gating, cron targeting, and identity trust — verified end-to-end against the production LINE WORKS API.
At a Glance
Prior Art & Industry Research
Neither reference project supports LINE WORKS:
lineworksandworksmobileinopenclaw/openclaw: 0 results each. Its channel system covers consumer platforms; the workplace LINE variant (different API host, auth model, and event schema) is absent.lineworksinNousResearch/hermes-agent: 0 results.Patterns were instead drawn from inside this repo: the service-account JWT token cache mirrors
googlechat.rs#GoogleChatTokenCache(samejwt-bearergrant), webhook signature verification mirrorsline.rs, and the flex renderer follows thefeishu_card.rsprecedent of platform-side markdown conversion.Proposed Solution
New gateway adapter (
crates/openab-gateway/src/adapters/lineworks.rs+lineworks_flex.rs), config-first[lineworks]section per #1375, and the cross-cutting platform-list wiring:auth.worksmobile.comtoken exchange, cached behindRwLockwith refresh margin; 401 on send triggers one invalidate+retry.X-WORKS-BotIdcross-check, one event per POST. Channel ids: groupchannelIdas-is, 1:1 encodeduser:{userId}so reply dispatch picks the users/channels endpoint from the opaque id (cron inherits this for free; the API also accepts email-form loginId in that path — verified live).@BotDisplayName(auto-resolved fromGET /bots/{botId},bot_nameoverride, fail-open).ack_message) since the platform has no reaction/typing APIs.NON_EDITABLE_PLATFORMS, rootlineworksfeature +unified,has_unified_platform, trust registry, cronVALID_PLATFORMS/configured_platforms/cron_adapters. (Documented as a bring-up checklist in the platform quirks/schema.)docs/lineworks.mdsetup guide,docs/platforms/schema/lineworks.toml(passes conformance),config-reference.md,config.toml.example, inbound-attachments matrix row.Why This Approach
mentionees/isSelf); accepted tradeoffs: manual "@name" strings trigger the bot, Console renames need a restart orbot_nameoverride.user:channel-id prefix over a schema change — keepsGatewayReplyuntouched; the id stays opaque to core.NON_EDITABLE_PLATFORMS), no reactions, no threads. These are platform ceilings, documented in the schema file.Alternatives Considered
contentis{type, text}only (official docs), confirmed against live traffic.Validation
cargo check/cargo check --features unified/cargo clippy -p openab-gatewayclean;cargo test -p openab-gateway: 298 passed (35 lineworks-specific: token cache, signature, event mapping, mention gate, flex renderer, dispatch flex/fallback/401-retry, attachment 302-flow/whitelist/size-cap);cargo test -p openab-core --lib: passes (one pre-existing env-dependentsecrets::resolve_exec_nonzero_exitfailure reproduces on unmodifiedmain); platform-schema conformance passes with the newlineworks.toml./modelstext command; cron to a channel and touser:<loginId>; L3 allowlist deny (request-access echo) and allow.Review Contract
Goal
Add a production-ready LINE WORKS gateway platform adapter (webhook ingest, JWT auth, replies with flex rendering and splitting, inbound attachments, mention gating, ack message, cron/trust wiring) plus its config-first section, docs, and platform schema.
Non-goals
sendimages-style doc is a follow-up).Accepted Residual Risks
bot_nameoverride (fail-open keeps the bot responsive rather than silent).quota_modelfor proactive push is best-effort (?-flagged in the schema): no published per-message quota was found.Acceptance Criteria
lineworks.toml(17-feature set complete, code-refs resolve).Follow-ups
UnifiedGatewayAdapter::create_thread's synthetic thread ref for thread-less platforms (affects all gateway platforms; review round-3 F9).IngressTrustProbe(round-3 F11).docs/sendimages-lineworks.mdagent guide for outbound media (upload API → fileId send).[lineworks]-level trust fields (allow_all_users/allowed_users) to graduate off the uniformGATEWAY_*seed.