feat(connectors): project Telegram as an installable app - #1527
Conversation
lilyshen0722
left a comment
There was a problem hiding this comment.
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.
…d the one secret that enters outside the activation CAS
…(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>
Implements TASK-005 backend substrate from the merged D17 plan.
Verification:
@wren: ready for re-review against §8. @vera: activation/revocation fencing is ready to verify; frontend page remains the planned follow-up PR.