Skip to content

feat(lineworks): LINE WORKS platform adapter - #1456

Merged
thepagent merged 16 commits into
openabdev:mainfrom
canyugs:feat/lineworks-adaptor
Jul 30, 2026
Merged

feat(lineworks): LINE WORKS platform adapter#1456
thepagent merged 16 commits into
openabdev:mainfrom
canyugs:feat/lineworks-adaptor

Conversation

@canyugs

@canyugs canyugs commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

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

LINE WORKS cloud                      OpenAB (unified binary)
┌───────────────┐  HTTPS POST        ┌────────────────────────────────────┐
│ user message   │  X-WORKS-Signature │ /webhook/lineworks (axum)          │
│ (1:1 / channel)│ ─────────────────► │  ├─ HMAC-SHA256 verify + bot-id    │
└───────────────┘                    │  ├─ mention gate (@BotName, strip) │
                                     │  ├─ fileId attachments (302-follow)│
        ▲                            │  └─ GatewayEvent ──► trust gate    │
        │ POST /bots/{id}/           │                        │           │
        │   users/{userId}/messages  │                   Dispatcher ─► agent
        │   channels/{chId}/messages │                        │           │
        │ (Bearer: service-account   │  dispatch_lineworks_reply ◄────────┘
        │  JWT → access token,       │   ├─ markdown → flex bubble        │
        │  cached + auto-refresh)    │   ├─ fallback plain text, 10k split│
        └─────────────────────────── │   └─ 401 → token refresh + retry   │
                                     └────────────────────────────────────┘

Prior Art & Industry Research

Neither reference project supports LINE WORKS:

  • OpenClaw — GitHub code search for lineworks and worksmobile in openclaw/openclaw: 0 results each. Its channel system covers consumer platforms; the workplace LINE variant (different API host, auth model, and event schema) is absent.
  • Hermes Agent — code search for lineworks in NousResearch/hermes-agent: 0 results.

Patterns were instead drawn from inside this repo: the service-account JWT token cache mirrors googlechat.rs#GoogleChatTokenCache (same jwt-bearer grant), webhook signature verification mirrors line.rs, and the flex renderer follows the feishu_card.rs precedent 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: RS256 JWT (iss=client id, sub=service account) → auth.worksmobile.com token exchange, cached behind RwLock with refresh margin; 401 on send triggers one invalidate+retry.
  • Inbound: constant-time HMAC verify, X-WORKS-BotId cross-check, one event per POST. Channel ids: group channelId as-is, 1:1 encoded user:{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).
  • Mention gating: callbacks carry no structured mention data, so channel messages match/strip the literal @BotDisplayName (auto-resolved from GET /bots/{botId}, bot_name override, fail-open).
  • Rich replies: markdown → flexible-template bubble (headings/lists/code boxes/spans) with unconditional plain-text fallback (no-markdown, >20KB/120-component, or API rejection); 10,000-char splitting.
  • Attachments: fileId download with manual 302-follow (the storage host requires the Authorization header that reqwest strips on cross-host redirects); shared media pipeline (image resize→vision, audio→STT path, text-extension whitelist).
  • Ack message: optional receipt text (ack_message) since the platform has no reaction/typing APIs.
  • Platform lists: NON_EDITABLE_PLATFORMS, root lineworks feature + unified, has_unified_platform, trust registry, cron VALID_PLATFORMS/configured_platforms/cron_adapters. (Documented as a bring-up checklist in the platform quirks/schema.)
  • Docs: docs/lineworks.md setup guide, docs/platforms/schema/lineworks.toml (passes conformance), config-reference.md, config.toml.example, inbound-attachments matrix row.

Why This Approach

  • Gateway crate, webhook-only — LINE WORKS has no websocket/long-poll option; the callback URL requires a CA-signed cert. This matches the existing telegram/line/feishu adapter tier exactly.
  • Flex-first with fallback rather than plain-text-only — the platform renders markdown literally; flex recovers most readability at zero risk since every failure path degrades to text (content is never lost).
  • Plain-text mention matching — the only option the callback schema allows (no mentionees/isSelf); accepted tradeoffs: manual "@name" strings trigger the bot, Console renames need a restart or bot_name override.
  • user: channel-id prefix over a schema change — keeps GatewayReply untouched; the id stays opaque to core.
  • Known limitation: no streaming (no edit API — enforced via NON_EDITABLE_PLATFORMS), no reactions, no threads. These are platform ceilings, documented in the schema file.

Alternatives Considered

  • Core-crate adapter (Slack/Discord style) — rejected: no persistent-connection mode exists on this platform.
  • Reply-token cache like LINE — rejected: LINE WORKS has no reply-token mechanism; sends are uniform pushes.
  • Directory/Users API for 1:1 targeting — rejected: requires an extra OAuth scope; the send path accepting loginId (verified) makes it unnecessary.
  • Structured mention support via message metadata — impossible: callback content is {type, text} only (official docs), confirmed against live traffic.

Validation

  • cargo check / cargo check --features unified / cargo clippy -p openab-gateway clean; 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-dependent secrets::resolve_exec_nonzero_exit failure reproduces on unmodified main); platform-schema conformance passes with the new lineworks.toml.
  • Live E2E against production LINE WORKS (unified binary + claude-agent-acp in k8s behind a Cloudflare tunnel): token exchange; signature accept/reject through the public callback; real 1:1 + channel conversations with agent replies; flex rendering accepted by the API; mention gate drop/strip verified with real traffic; image attachment → agent vision description; /models text command; cron to a channel and to user:<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

  • Streaming/edit/delete/reactions/threads (platform has no APIs for them).
  • Outbound media upload (repo convention: agents call platform APIs directly; a lineworks sendimages-style doc is a follow-up).
  • Ambient mode for lineworks (prepared separately on top of an AmbientDispatcher generalization; will be its own PR).
  • Multi-bot / multi-domain support; Directory/Users API integration.

Accepted Residual Risks

  • Mention gating is plain-text matching: a typed "@botName" string triggers the bot, and a Console rename silently disables gating until restart/bot_name override (fail-open keeps the bot responsive rather than silent).
  • Flex size ceilings (20KB / 120 components) are conservative estimates, not published limits; oversized bubbles fall back to plain text, so the failure mode is cosmetic.
  • quota_model for proactive push is best-effort (?-flagged in the schema): no published per-message quota was found.

Acceptance Criteria

  • All existing suites stay green; new adapter tests cover signature, token, mapping, gating, rendering, dispatch retry, and attachment paths.
  • Platform-schema conformance passes with lineworks.toml (17-feature set complete, code-refs resolve).
  • A lineworks-only deployment activates unified mode, mounts the webhook, and round-trips a real message (evidenced above).
  • No behavior change for any existing platform (all platform-list additions are additive).

Follow-ups

  • Cross-platform: fix UnifiedGatewayAdapter::create_thread's synthetic thread ref for thread-less platforms (affects all gateway platforms; review round-3 F9).
  • Wire the remaining attachment-capable gateway adapters onto IngressTrustProbe (round-3 F11).
  • Gateway observability framework: per-adapter counters, readiness, runtime disable (round-3 F12).
  • docs/sendimages-lineworks.md agent guide for outbound media (upload API → fileId send).
  • Ambient mode PR (AmbientDispatcher generalization + lineworks wiring, already prototyped and live-tested).
  • Button/list template + postback interactivity; [lineworks]-level trust fields (allow_all_users/allowed_users) to graduate off the uniform GATEWAY_* seed.

canyugs and others added 9 commits July 26, 2026 03:04
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>
@canyugs
canyugs force-pushed the feat/lineworks-adaptor branch from 65ed43d to 2302b97 Compare July 25, 2026 19:05
@canyugs

canyugs commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main (resolved additive conflicts with the new acp feature — both features kept side by side in the feature lists / adapter registries) and dropped an internal planning file from the branch.

CI: everything passes except smoke-test (Dockerfile.mimocode), which failed on a Docker Hub registry timeout while pulling node:22-trixie-slim (dial tcp 54.83.55.124:443: i/o timeout) before any project code was involved — log. The same variant passes in the smoke-test-unified matrix. A re-run should clear it.

@ijbhxhu

ijbhxhu commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

CHANGES REQUESTED ⚠️

Reviewed at 2302b97232105b299662dd16ea6d5876ee155add. The following findings are ORIGINAL.

# Status Finding
F1 🟡 Blocking The webhook downloads and processes attachments before returning HTTP 200. LINE WORKS recommends asynchronous callback handling and does not resend failed callbacks.
F2 🟡 Blocking Attachments are processed before mention and identity trust checks, allowing rejected events to consume network, CPU, memory, and storage.
F3 🟡 Blocking event_tx.send() failures are ignored while the callback still returns 200, permanently losing the event.
F4 🟡 Blocking Startup, cron, and adapter construction apply inconsistent checks for incomplete or empty LINE WORKS credentials.
F5 🟡 Blocking The adapter requests the broad bot OAuth scope instead of the least-privilege bot.message,bot.read scopes.
H1 🟢 Non-blocking Reject non-HTTPS attachment redirects before forwarding the bearer token. A fixed hostname allowlist is not requested because LINE WORKS does not document a complete stable list.

Requested Changes

  • Return HTTP 200 after signature/envelope validation and process attachments through a bounded asynchronous worker.
  • Apply mention and identity authorization before downloading or storing attachments.
  • Handle event enqueue failures explicitly and acknowledge only successfully accepted events.
  • Reuse one complete, non-empty LINE WORKS configuration validator across startup, cron, and adapter construction.
  • Request and document bot.message,bot.read.
  • Add focused regression tests for slow attachments, rejected attachments, missing receivers, incomplete environment variables, and the exact OAuth scope.

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.

@chaodu-obk

This comment has been minimized.

@chaodu-obk chaodu-obk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

CHANGES REQUESTED ⚠️ - F1-F6 remain open at the reviewed head: callback latency, authorization ordering, event-loss handling, incomplete configuration activation, OAuth least privilege, and required BotId validation.

Consolidated review: #1456 (comment)

@chaodu-obk

This comment has been minimized.

@chaodu-obk chaodu-obk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

CHANGES REQUESTED ⚠️ - Final aggregate confirms F1-F6 and P1-P3 remain open at the reviewed head; no inline threads were present to reply to or resolve.

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>
@canyugs

canyugs commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

All review findings addressed in 7396273 — thank you for the thorough review. Point-by-point:

# Resolution
F1 The callback now acks right after signature + required-header + envelope validation. Mention gating, the ack message, and attachment I/O run in a bounded post-ack worker (LINEWORKS_WEBHOOK_CONCURRENCY_MAX = 8, permit acquired pre-ack so bursts wait for capacity — same pattern as the LINE adapter). Regression test: a 5-second attachment stall no longer delays the 200.
F2 classify_event is now pure (no I/O) and returns a PendingAttachment descriptor; the download runs only after the mention gate passes. Regression test: an unmentioned channel image event completes with an expect(0) mock on the attachment endpoint. Identity (L3) trust still runs in the shared core ingress gate post-broadcast — moving it ahead of the download would require plumbing the core trust registry into the gateway crate, which no existing adapter does; noted as a possible follow-up.
F3 Explicit loss policy: the webhook refuses to ack (503) when event_tx has no receivers ("an acknowledged callback must correspond to an accepted event"), and a post-ack enqueue failure logs at error level with the event id. The remaining window is a consumer that disappears mid-processing, documented in the worker's doc comment.
F4 Single validator: ResolvedLineWorks::is_complete() — resolution now treats empty strings as unset on both the config and env layers — is used by startup preflight and cron registration, matching adapter construction. LINEWORKS_BOT_ID alone no longer activates anything. Test matrix covers missing credentials, empty config values, empty env values, and key-file-as-key-material.
F5 Scope is now exactly bot.message,bot.read, verified against the live LINE WORKS token endpoint (returns "scope": "bot.message bot.read"). The token test asserts the exact string; docs/lineworks.md documents the two scopes to grant.
F6 X-WORKS-BotId is required: missing or mismatched → 401 before parsing. Missing-header regression test added.
P1 One adapter-owned download client (redirects disabled) replaces the per-call client.
P2 Bot-name resolution is single-flight: the cache mutex is held across the fetch, so concurrent first-use events share one token + bot-info request.
P3 split_text tracks the running char count incrementally — linear.
H1 Redirect targets that are not HTTPS are refused before the bearer token is forwarded (loopback HTTP exempted for the wiremock harness). Regression test included.

Validation: cargo test -p openab-gateway 301 passed (39 lineworks-specific), platform-schema conformance passes, clippy clean, --features unified builds. The least-privilege scope was additionally verified end-to-end against the production auth endpoint with real credentials.

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>
@chaodu-obk

This comment has been minimized.

@chaodu-obk

This comment has been minimized.

@chaodu-obk chaodu-obk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHANGES REQUESTED ⚠️ - The current head retains important authorization, key-validation, backpressure, reliability, cron, and documentation gaps. Complete aggregate: #1456 (comment)

…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>
@canyugs

canyugs commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Round-3 findings addressed in ea358eb — 9 fixed, 3 answered with rationale below.

Fixed (F1–F8, F10):

# Resolution
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 and no download, and a burst cannot fan out unbounded outbound tasks. Live-verified: an untrusted 1:1 media event logs attachment download skipped and flows to the core deny + request-access echo. The standalone gateway genuinely has no core trust registry to probe — that deployment shape keeps the post-broadcast gate, now documented on the type.
F2 Key material must parse as RS256 PEM at both activation (valid_private_key) and adapter construction — invalid PEM fails startup preflight instead of the first token exchange. Tests cover both layers.
F3 Explicit full policy: try_acquire — saturation answers 503 immediately with an error log instead of accumulating waiting handlers. Saturation regression test included.
F5 Bot-name lookup failures are negatively cached (60 s cooldown): an outage retries once per window; the expect(1) mock test proves the second call performs no I/O. Fail-open stays (documented tradeoff) but is now cheap.
F6 Both production call sites (unified adapter + gateway reply loop) surface failed reply delivery at error level with channel + command.
F7 Boundary-aware mention matching: @Bot no longer matches inside @Bottage, and only boundary-valid mentions are stripped. Boundary matrix test + live verification (a @BotNamexyz message is dropped).
F8 Token exchange checks HTTP status before body parsing — upstream failures surface as token endpoint HTTP <n>: <snippet>.
F10 config-reference.md attachment paragraph now matches the implemented pipeline.

Answered with rationale (proposed as Follow-ups):

  • F9 (cron synthetic thread ref): this is UnifiedGatewayAdapter::create_thread behavior shared by all gateway platforms (telegram/feishu/wecom identical) — not introduced by this PR. Changing it here would alter cron semantics for every platform; proposing it as a separate cross-platform fix.
  • F11 (probe generality): noting this asks the opposite of round 2's F2, which required exactly this cross-crate capability. The probe is deliberately small shared infrastructure; wiring the remaining attachment-capable adapters onto it is a natural follow-up rather than a reason to fold it back adapter-local.
  • F12 (metrics/readiness/kill-switch): no per-adapter counter or runtime-disable mechanism exists anywhere in the gateway today; introducing that framework inside a platform PR would grow scope well past the Review Contract. Per the policy's non-blocking-hardening clause, proposing as a Follow-up (happy to help design it separately).

Validation: 308 gateway tests, conformance, clippy clean, --features unified + both lineworks-only combos compile. Live E2E on a real deployment re-verified: normal mention reply + ack unchanged, boundary false-positive dropped, untrusted attachment produced zero download I/O.

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>
@chaodu-agent

This comment has been minimized.

@chaodu-obk

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
@chaodu-agent

This comment has been minimized.

@chaodu-obk

chaodu-obk Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

LGTM

What This PR Does
Adds the LINE WORKS platform adapter: signed webhook ingress, JWT OAuth access-token management, inbound event mapping and attachment handling, outbound plain/flex replies, configuration and deployment documentation, and cron delivery support.

How It Works
Validated callbacks are classified quickly and acknowledged before post-ack processing. A bounded 8-worker pool handles mention/trust gates, optional receipt messages, attachment work, and broadcast. A separate 64-slot ingress queue absorbs short bursts while preserving a bounded, logged 503 policy on true overflow. Outbound sends use cached OAuth tokens and retry once after a 401. Cron treats LINE WORKS as threadless because the platform has no topic/thread API.

Findings

# Severity Finding Location
1 Praise The prior worker-saturation loss mode is addressed with a bounded 64-slot ingress queue. It holds capacity while waiting for one of 8 workers, releases it after worker acquisition, logs real overflow, and has acceptance/drain plus overflow regression coverage. crates/openab-gateway/src/adapters/lineworks.rs; crates/openab-gateway/src/lib.rs
2 Praise 401 recovery now invalidates only the token that actually failed. The cache comparison occurs under the write lock, so a delayed response cannot erase a newer token; the concurrent stale-401 regression test also verifies current-token invalidation. crates/openab-gateway/src/adapters/lineworks.rs
3 Praise Cron now explicitly classifies LINE WORKS as threadless, preserves existing Google Chat behavior, covers the policy in tests, and documents channel and 1:1 destinations. crates/openab-core/src/cron.rs; docs/cronjob.md
4 Praise The ack_message documentation now matches the implementation: it follows callback acknowledgement and mention/trust gates, is awaited inside the bounded worker, and precedes attachment download and agent dispatch. docs/config-reference.md; crates/openab-gateway/src/adapters/lineworks.rs

Feedback Reconciliation
Previously raised callback, authentication, configuration, OAuth scope, bot-header, and documentation concerns remain addressed in the current implementation. The four prior round findings were re-reviewed on this exact commit and are resolved as described above. Broader cross-platform work remains non-blocking follow-up scope: standalone trust-probe coverage, adapter metrics/readiness and runtime disable controls, and consolidation of activation validation.

Inline Review Threads
GitHub reports 0 inline review threads on this PR. Therefore there were no inline comments to reply to or resolve.

Baseline And Validation

  • Reviewed the local incremental diff from 7fe5f27 to this commit: 5 files, +186/-28.
  • git diff --check passed for the incremental and current PR diff.
  • Read the queue, conditional-invalidation, cron-policy, documentation, and regression-test changes locally.
  • GitHub reports 41 completed check runs for this head; all non-skipped runs succeeded, including validation, conformance, builds, and platform smoke tests.
  • Local cargo execution was unavailable in this environment, so CI is the test-execution evidence for this round.

Three Reasons We Might Not Need It

  1. Existing supported chat platforms may already cover the deployment's user base.
  2. Operators may not want to maintain LINE WORKS credentials, signing configuration, and webhook exposure.
  3. A deployment that does not need LINE WORKS channel or 1:1 automation can avoid this additional integration surface.

Conclusion
No current important or critical finding remains on the reviewed SHA. LGTM.

Review key: #145645dcd31:round-7

@chaodu-agent

chaodu-agent commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

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 527632e (pushed to this branch); F4 is a platform ceiling already recorded as an accepted residual risk.

What This PR Does

Adds 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 (docs/adr/identity-trust-none.md):

  • Receiver (L1 only): lineworks.rs verifies X-WORKS-Signature with constant-time HMAC-SHA256 comparison (subtle::ConstantTimeEq) plus a required X-WORKS-BotId cross-check, then normalizes to GatewayEvent. It makes no trust decisions.
  • Trust Gate (unified, in core): events flow through the shared gate_gateway_event()router.gate_incoming()trust::decide(), keyed by platform="lineworks". Same gate and registry as every other platform — trust is automatic for the new adapter, exactly as the ADR mandates.
  • Trust-none default: "lineworks" is registered in the trust registry with L3 deny-all by default (Phase 3 semantics).
  • Decision in core, echo via platform: on identity-deny, core produces the request-access echo; delivery routes through UnifiedGatewayAdapter::send_message → dispatch_reply → dispatch_lineworks_reply — core never calls the LINE WORKS API directly. Echo is throttled per (platform, sender) (300s window).
  • IngressTrustProbe used correctly: the adapter's probe is a resource-saving pre-check only — untrusted senders get no ack and no attachment download, but the event still broadcasts so the core gate remains the decision authority and delivers the deny echo. Covered by the untrusted_sender_never_downloads_attachment regression test.
  • Documented Receiver exceptions honored: the @mention gate runs upstream of the Trust Gate (the ADR's deliberate Receiver exception), and is_bot: false hardcoded matches the pinned LINE row (no bot-to-bot webhook delivery; fail-safe direction — everything stays subject to L3).

Findings

# Severity Finding Status
1 🟢 Full conformance with the #1291 three-layer model: L1-only Receiver, shared core Trust Gate, deny-all L3 default, platform-delivered throttled deny-echo, probe as pre-check not bypass
2 🟡→✅ No first-class [lineworks] trust section — L3 rode solely on the deprecated uniform GATEWAY_* seed with no Phase-1 deprecation warning, so Phase 2 of #1356 would have broken lineworks deployments with no prior warning Addressed in 527632e
3 🟡→✅ The ADR's pinned per-platform tables (is_bot derivation, echo mechanism) had no lineworks row Addressed in 527632e
4 ℹ️ (accepted residual risk) Deny-echo is necessarily a push send (LINE WORKS has no reply-token mechanism), so LINE's "Reply-only, never Push" quota protection cannot apply; the platform quota model is ?-flagged as unverified. Mitigated by the per-sender echo throttle Documented (now also in the ADR echo table)
What 527632e adds
  • [lineworks].allow_all_users / allowed_users with LINEWORKS_ALLOW_ALL_USERS / LINEWORKS_ALLOWED_USERS env fallback (deny-all default per the ADR), exposed via trust_config() — same shape as wecom/googlechat/teams/feishu.
  • Wired into platform_trust_override keyed by lineworks_activated(), so the Phase-1 deprecation warning now fires when lineworks trust still rides the uniform GATEWAY_* seed — closing the silent Phase-2 breakage path.
  • Config-first conformance COVERED set, docs/config-reference.md, docs/lineworks.md Trust section, and config.toml.example updated; the schema trust_gate note reflects the first-class section.
  • ADR pinned tables gain LINE WORKS rows: is_bot always false (message callbacks deliver user messages only, LINE precedent) and push-send deny-echo (no reply-token mechanism; per-sender throttle; unpublished quota ?-flagged).
  • Test coverage: platform_trust_sections_parse_from_toml extended with a [lineworks] case (section parse + trust_config() view + absent-section fallback).
Baseline Check
  • Branch head previously 45dcd31, contains latest origin/main (merge-base check passed) — no staleness.
  • main has no lineworks support — the entire adapter is net-new value.
  • The prior round-7 external review verdict (LGTM) covered 45dcd31; 527632e is additive (trust-section graduation + docs), no behavior change for senders already admitted via GATEWAY_* (precedence: uniform seed < [gateway] < platform section, unchanged).

5️⃣ Three Reasons We Might Not Need This PR

  1. Platform coverage overlap — deployments already reachable via consumer LINE or Slack/Teams may not need the workplace LINE variant.
  2. Operational surface — LINE WORKS requires a CA-signed public callback URL, service-account key management, and console configuration that some operators will not want to maintain.
  3. Maintenance cost of an unofficial rendering layer — the flex size ceilings are conservative estimates, not published limits; the renderer may need upkeep as the platform evolves (mitigated by the unconditional plain-text fallback).

Verified (at 527632e)

  • cargo check --workspace --all-features — clean; cargo clippy --workspace --all-features — 0 warnings/errors.
  • cargo test -p openab-core config — 74 passed (includes the new [lineworks] trust parse/trust_config() assertions).
  • cargo test -p openab-core --all-features — 709 passed, 1 failed: secrets::tests::resolve_exec_nonzero_exit, the known pre-existing env-dependent failure that reproduces on unmodified main.
  • cargo test -p openab-gateway --features lineworks — 310 passed (all 35 lineworks tests) + config-first conformance 2 passed with the extended COVERED set.
  • cargo test --bin openab — 18 passed (trust-seed precedence suite included); platform-schema conformance — 12 passed with the updated lineworks.toml note.

Review key: #1456527632e:trust-architecture-audit

… 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)
@thepagent
thepagent merged commit c5a75ac into openabdev:main Jul 30, 2026
59 of 60 checks passed
brettchien added a commit to brettchien/openab that referenced this pull request Jul 30, 2026
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants