Skip to content

feat(connectors): project Telegram as an installable app - #1527

Merged
lilyshen0722 merged 14 commits into
mainfrom
kai/connector-installable-app
Sep 4, 2026
Merged

lilyshen0722 merged 14 commits into
mainfrom
kai/connector-installable-app

Conversation

@lilyshen0722

@lilyshen0722 lilyshen0722 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Implements TASK-005 backend substrate from the merged D17 plan.

  • Adds the builtin Telegram Installable, atomic claim/activation lifecycle, projectors, reconciler, and identity-derived install/uninstall verbs.
  • Stale revocations are terminally unprojected, then re-install creates a new parent and Integration; no old code is revived.
  • Activation stamps its generation and mints the code in one projection CAS; missing/revoked projections return typed install_lock_lost (409).
  • Replaces the hardcoded relay call with pod-scoped installable event dispatch; two same-pod user subscriptions relay once to each selected private chat while legacy direct Telegram integrations still work.
  • Applies the Discord legacy-uninstall type fence before writing installation back-pointers.

Verification:

  • Focused Jest suite: 82 passing tests across installation lifecycle, dispatcher, installable routes, Discord management, agent-message, Telegram bridge, and connect-code tests.
  • npm run tsc:check
  • npm run build
  • ESLint on changed TypeScript

@wren: ready for re-review against §8. @vera: activation/revocation fencing is ready to verify; frontend page remains the planned follow-up PR.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

SR-GATE: APPROVED @ 25672027 — all checks green. Kernel change, gated against the plan doc I reviewed on #1509/#1515.

The implementation is faithful to the spec, including the parts that were hard-won: three-write activation with activating as a live state, mintConnectCode reachable once, the resume path that reads an existing code rather than re-minting, podId moved atomically with activation, and — the one I most expected to find broken — no loser-tidies-up path. unprojectInstallation is called only from the two uninstall entries (:553, :645), never from an install failure, and markProjectionFailure is itself CAS'd on {_id, status:'installing', claimId} so a stale owner cannot mark the winner's row error. renewActivationLease is an addition beyond the plan and a good one: it narrows the window between the last parent CAS and the cross-collection write.

Baseline 38/38 across the four suites (Node 22).

The finding: the generation fence is present on all four parent writes and pinned by none

claimId is the safety property the plan spends its most words on — "a TTL lease is a liveness parameter; safety requires a per-claim generation in every write's filter." I removed it from each parent CAS in turn and ran the two service/route suites:

mutation result
finishActivation (write 3) drops claimId 26 passed, 0 failed
transitionToActivating (write 1) drops claimId 26 passed, 0 failed
renewActivationLease drops claimId 26 passed, 0 failed
projection-commit write drops claimId 26 passed, 0 failed

This is not "there is no concurrency test". refuses a revived stale owner without changing the winner code or unprojecting (:321) is a real interleaving — it pauses owner A inside the Integration write, ages the lease, lets B take over and complete, then releases A and asserts A rejects, the winner's code survives, exactly one Integration exists, and unproject was never called. It is a good test.

It passes with the fence removed because of where it pauses. A is released only after B has fully finished, so the parent is already active when A reaches write 3; A is refused by the status term, and claimId never has to do anything. The interleaving where claimId is the only guard — A resuming while B sits between its own write 2 and write 3, parent still activating under B's generation — is the one case not exercised.

So the behaviour is correct today and the fence is doing nothing observable. If a later change reorders the status transitions, the fence becomes load-bearing and its removal would go unnoticed. One test: hold B between write 2 and write 3, release A, assert A's finishActivation does not complete B's row.

Note for the merge order

#1531 is stacked on this branch — its base is kai/connector-installable-app, not main (verified via gh pr view). It must be retargeted to main before this is pressed, or it will show an empty or inverted diff once this merges. Flagging per Sam's 63149.

@lilyshen0722
lilyshen0722 merged commit e6851d6 into main Sep 4, 2026
14 checks passed
@lilyshen0722
lilyshen0722 deleted the kai/connector-installable-app branch September 4, 2026 08:09
samxu01 pushed a commit that referenced this pull request Sep 4, 2026
…d the one secret that enters outside the activation CAS
lilyshen0722 added a commit that referenced this pull request Sep 4, 2026
…(TASK-006) (#1535)

* docs(plans): Slack as the second installable connector — design note (TASK-006)

Telegram with two substitutions: the OAuth state is the connect code and the
callback is the enable command; the per-workspace bot token makes ADR-025 D6's
secret reference a prerequisite. Everything else in #1527/#1531 is reused.

* docs(plans): Slack note — the #1527 invariants checked one by one, and the one secret that enters outside the activation CAS

* docs(plans): Slack note — the callback proves a Slack identity, not the right one: session nonce + confirm-in-Commonly before any bind routes (Vera)

* docs(plans): Slack note — the one-write window after the exchange, and why token rotation stays off

* docs(plans): Slack note — pending-bind expiry enforced at Confirm; reconciler goes on the scheduler (Vera)

* docs(plans): Slack note — cite Slack's OAuth and rotation docs; drop the uncited same-token claim from the crash path

* docs(plans): Slack note — the dropped-exchange log line names the workspace (Vera)

* docs(plans): Slack note — shared event_id receipts (rollouts run two processes) and a key ring with a re-wrap path (Vera)

* docs(plans): Slack note — the event receipt is a claim with state; stale processing is taken over by CAS (Vera)

* docs(slack): the OAuth nonce is stored plain and compared in constant time — no digest

Matches #1537 b801a6e. The hashed cookie was CodeQL's js/insufficient-password-hash
(#1779) on every digest variant, and the hash defended against nothing: a row reader
already holds state in the same document. Confirm is the wall.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant