உள்ளடக்கத்திற்குச் செல்லவும்
KAMOUniverse

மாற்று பதிவேடு

என்ன புதிது.

அது கப்பல்கள் போல், தளத்தில் ஒவ்வொரு மேம்பாடு.

  1. · kamo-shared-library

    Retry the database errors Yugabyte expects a client to retry

    Applying DDL bumps the catalog version, and every physical backend still holding the old one fails its next statement with SQLState 40001 — "schema version mismatch for table ...: expected 94, got 93". Hibernate does not retry it, so it reaches a user as a 500. The last occurrence produced 188 of them fleet-wide over fifteen minutes, and it arrived as a bug report about one widget: the failing query is nearly always a boring identity load that runs first on every authenticated request, so whichever feature shouts loudest gets blamed. There is no setting that removes this. Yugabyte's operations guide says concurrent DML during DDL "may use the old schema, the new schema, or encounter temporary schema mismatch errors that require client-side retries", and the two server flags that would narrow the window are not settable at runtime — checked with yb-ts-cli, which refuses both without -force — so changing either costs a restart of a database running one master and one tserver. An earlier note in this codebase blamed HikariCP's maxLifetime and suggested lowering it. That would have done nothing: the tserver runs --enable_ysql_conn_mgr=true, so the Java pools hold LOGICAL connections into Yugabyte's own connection manager and the stale catalog lives in shared physical backends the pool does not own. Costs nothing when nothing is wrong: retrying is driven entirely by an exception a healthy cluster never throws, so the steady-state path is one lambda call. Deliberately narrow about what it will retry — a constraint violation is the database saying the write is wrong, not that it was unlucky, and retrying it turns one fast error into three slow ones. An inconclusive failure is left alone. The same 40001 also covers genuine serialization conflicts and Yugabyte read restarts, both of which reach users as 500s today. Nothing calls it yet; wiring follows.

  2. · KlusterServices

    Say whether a terminal is working or waiting for its member

    Every terminal in the listing now carries `state`: busy, or waiting. It is the one question somebody asks of a window they cannot see, and it is what a minimized hexhead's colour will answer. A SHELL is waiting when the foreground command is a shell — `pane_current_command` is what tmux already sees in the pane, so this needs no bookkeeping and cannot go stale. Anything else in the foreground is work the member started. thinking or idle. Its transcript answers instead, from the last thing anybody actually SAID: assistant, stop_reason end_turn -> it has answered and handed back: WAITING assistant, stop_reason tool_use -> it is running something: BUSY user -> just submitted, nothing back yet: BUSY The last LINE is nearly always bookkeeping — cost, latches, queue operations, file snapshots, all appended after the answer — so reading it would report every idle session as busy. The scan goes back to the last conversational record. Unknown reads as WAITING, deliberately: a wrong "busy" paints a window green and hides that it needs somebody, where a wrong "waiting" only draws the eye. That is the safer direction for a status light to be wrong in. The listing therefore asks tmux for an eighth field. The title stays LAST and the split stays bounded; `pane_current_command` is a bare command name with no arguments, so it cannot carry the separator. Verified on the machine, not just in fixtures: of twelve real sessions the seven that were working read busy and the five that had finished read waiting — and the shell case was checked against a live `bash` (waiting) and a live `sleep` (busy). 226 tests.

  3. · kamo-internal

    Pick the model and effort before starting a coder session, once

    Send To AI dialog has, and the two are the SAME choice rather than two. Somebody who has settled on Sonnet at High has settled on it. Re-picking in the other place every time is the kind of friction nobody would ever report as a bug and everybody feels daily, so the pair is stored as a member UI preference — about the person, not the browser, so a laptop and a desktop agree and clearing a browser does not quietly put them back on Opus. It rides the preference map the launchpad already reads once per page load, so wanting it costs no extra request on first paint. Both surfaces can be open at once — the Interaction Center is a panel over the page a Send To AI dialog is on — so a change is announced as well as written, and the other follows without being remounted. Three things worth calling out: - The Send To AI dialog no longer re-seeds model and effort when it opens. It did, back when they were local state, and leaving that in would have written the defaults back over the member's standing choice every time a bug report was sent — the exact opposite of remembering it. - A stored preference outlives the code that wrote it, so it is checked rather than trusted, and PER FIELD: a model retired between releases must not also cost the member the effort they chose. Anything unrecognised falls back, which keeps a select from rendering a value that is not one of its options — that shows as an empty box, and is how somebody discovers their setting by it being gone. - The selects are native ones styled from the palette. This pane is painted from the app's colours on one surface and the emulator's on the other, and MUI's Select renders its menu into a portal that would need the whole palette threaded through it to avoid a white list dropping out of a black panel. A native control gets that from `color-scheme` and brings correct keyboard and screen-reader behaviour with it. Resuming a session uses the standing choice too: picking a conversation back up and picking a model are separate decisions. No new dictionary keys — the Model and Effort labels and every option name are the Send To AI dialog's own, reused rather than duplicated so they cannot drift. 13 of 14 guards and 5363 tests pass. check-i18n-keys is currently red on another session's in-flight subscription work, not on anything here.

  4. · SecurityService

    Carry the member's model and effort to a coder session

    and deliberately not examined here. `coder_flags` on the agent is the one thing that decides what a session runs with — its own comment calls it "the security boundary of the whole dispatch path" — so a second vocabulary in this service would be a second opinion that could disagree with it, and the one that disagreed would be the one nobody was reading. Anything outside it comes back as a refusal with a sentence the console prints verbatim, exactly as an unknown working directory already does. caller that predates this sends.

  5. · KlusterServices

    Let a coder session run at the model and effort the member picked

    for a new session and for a resumed one alike, since `--model` and `--effort` are per-session flags either way. They are the ONLY part of the command a caller can influence, and they still cannot express a flag: both go through `coder_flags`, the same fixed vocabulary the Send-to-AI dispatch uses, which maps a name onto fixed arguments and raises on anything else. The permission mode is not reachable from here and does not become a parameter. Naming NEITHER is allowed and means "whatever the member's own configuration says" — a better answer than one this agent invented, and what any caller that predates this sends. Naming only ONE is refused: running at their model but somebody else's effort is not what either answer meant. Ultra Code is xhigh plus a keyword in the MESSAGE, and an interactive session has no message for the agent to put it in. It therefore runs at xhigh and the member types the word themselves, which is the honest behaviour rather than a silent downgrade dressed up as something else. 215 tests.

  6. · kamo-internal

    Name a hexhead after the window it stands for

    A hexhead's tooltip read "User w12" — the head id, which is the window id, in a string written for member avatars. A dock of six minimized windows was six identical labels, so the only way to find the one you wanted was to open them. The title was already there: every spawn carries the window's title (the store requires it), the context menu already shows it, and HexHead already had an `alt` prop feeding both its tooltip and its accessible name that nothing ever passed. Thread `head.title` down through the stage and the spring into it. Keep it current too. A head is spawned with the title the window had at the moment it was minimized, and titles keep moving after that — a chat renamed over the socket, a roster change, an AI session named by the server once it answers. The tool bus now mirrors `tool:title` as `hexheads:title`, and the engine applies it the way it applies unread counts: absolute, same-array on no change, and never spawning a head for a window that has none.

  7. · SecurityService

    Refuse a sign-in whose host names no organization

    Such a host could sign in until now. It fell through to the workspace-choice path and succeeded on membership alone, which made it a door where none of the tenant's own controls applied: access rules and geo blocks are per-organization, so decide(ip, host) could only ever answer "allowed — no organization owns this host". Nothing was bypassed outright, because membership was still proven and completeSignIn re-checks the rules once a workspace is chosen. But the credential check itself sat behind no tenant policy at all, reachable by pointing any DNS name at the ingress, and the lockout ladder added alongside it had no blocklist to escalate into and counted under a synthetic tenant. Refused before the credentials are read and before the lockout gate. Order is half the point: an unknown host is not a wrong password, so counting it would walk a legitimate user toward a lockout over an operator's DNS mistake, and refusing here means the synthetic-tenant counters are never created at all. Said plainly rather than behind the generic credential refusal. Which hosts this platform answers on is not a secret — Traefik already routes by it — and an operator bringing up a new tenant domain who is told "wrong password" will spend the afternoon resetting one. Two carve-outs, both deliberate: localhost. OrgResolutionService.byHost excludes it explicitly, so it can never name an organization and the gate would otherwise make local development impossible. It is decided from the HOST, mirroring that existing carve-out, NOT from "the caller's address is not public" — every request inside the cluster looks that way, so the day X-Forwarded-For forwarding breaks in production that version of the test would switch the gate off across the fleet. Loopback literals only; a bare service name is not exempt. A kill switch, **************** shipping ON. If a domain-table problem ever stops legitimate hosts resolving, sign-in has to be reopenable without a redeploy — and reopening restores the old path exactly rather than a third behaviour. An exempt host must keep DEFERRING to workspace choice, not merely get past the gate. Sending it down the host-resolved path instead breaks it more quietly: authenticateUser's three-argument form refuses without a resolved FQDN, so "unknown host" would have arrived dressed as a wrong password. That was a real defect in the first cut of this change, caught by asking what localhost does next.

  8. · KlusterServices

    A coder terminal wears the name coder gave the conversation

    running had long since been titled "Startup fee implementation and Harmony account adjustment". Two separate things were swallowing that name. **The listing.** The title was WRITTEN once, at bootstrap, as a placeholder. It a conversation a few seconds in and renames it later if the work turns out to be something else, so a stored copy is wrong almost immediately — the same argument the rest of this file already makes for reading tmux and the disk rather than keeping a table. It costs ~6ms even against a 205 MB transcript, because coderSessionIndex reads only the two ends of a file and memoizes on (mtime, size). **The window.** tmux's `set-titles` is OFF by default, so the application's emitting it all along — the pane title was already right while the tool window because tmux's default would prefix every name with is the hostname: that would retitle every terminal "desktop-1" over the name its member gave it. at a glance and the name does not change shape between the window opening and the listing catching up — the same form SecurityService already uses for the initial label. A name the MEMBER typed still wins and is left exactly as typed; a derived name that overwrote it would make the rename field lie. The one exception is a stored placeholder it is, so terminals opened before this change start deriving on their own rather than needing anybody's tmux options rewritten. The join lives in the /terminals handler rather than inside either half — TerminalManager knows only tmux, coderSessionIndex knows only the disk, and neither should have to learn about the other to answer its own question. 212 tests. Verified against the live machine: the running terminal resolves to

  9. · kamo-internal

    Show the sign-in lockout as a rule that cannot be switched off

    SecurityService now applies a progressive sign-in lockout to every organization whether or not a row exists for it — five free attempts, then five seconds, then locks of five, fifteen and thirty minutes, then a permanent IP block. Detection Rules is where an administrator goes to find out what is protecting them, so it has to say so. It is the first rule on this screen whose switch is locked on, which needs more than a disabled input. The row sits first, tinted, with a padlock, an "Always on" chip and the ladder rendered read-only as chips — because a protection that cannot be configured still has to be legible, and "PROGRESSIVE LOGIN LOCKOUT" alone tells an administrator nothing about what will happen to their users. The delete button is ABSENT rather than disabled. Deleting is the other way to switch a rule off; SecurityService refuses it with a 409 either way, and a button whose only outcome is a refusal is a button that lies. Same reasoning for the edit dialog, which for this rule shows the ladder read-only and the alert recipients — the one field a tenant actually owns. The server ignores the other fields on the way in, so rendering them would offer edits that silently do not save. The locked switch names itself through slotProps.input. The row's words live in a sibling element, so without it a screen reader announces "checkbox, checked" and nothing else — and the tooltip carrying the reason for the lock is not reachable that way at all. inputProps would have been dropped silently by MUI's Switch; check-switch-aria exists because that has bitten this repo before. Nothing here enforces anything, and the tests say so: enforcement is pinned in SecurityService's DetectionRuleMandatoryTest. What is pinned here is that the screen does not lie about it — the switch cannot move, the delete is not offered, the ladder is visible, and an ordinary rule keeps every control it always had, so the lock is about this rule type and not about detection rules in general. Dictionary keys were pushed to kamo-translation-dictionary first; next-intl renders a missing key path verbatim, and messages/ is gitignored here, so the other order ships visible gibberish that a green local build would not catch.

  10. · kamo-login

    Show the progressive lockout, so a held sign-in explains itself

    The server now refuses sign-ins on a ladder — five free attempts, then five seconds, then locks of five, fifteen and thirty minutes, then a permanent block. Without any of this the screen would answer all of that with "Invalid username or password" and a button that quietly does nothing, which reads as the site being broken rather than as a security control working. Four states, and the differences between them are the design: A STRIKE METER appears under the password field once failures start accumulating, saying how many attempts remain before signing in pauses and for how long. Somebody who has genuinely forgotten their password should be nudged toward the reset link BEFORE they are locked out, not after. The FIVE-SECOND wait stays inline: the Sign in button becomes a countdown with a bar draining across it. Taking the form away and putting it back for five seconds would be more disruptive than the wait. A LOCK OF MINUTES takes over the card — a ring that empties, a mm:ss countdown, and a reset-password escape, because by the tenth failure the password is forgotten rather than mistyped and recovery is a door this lockout does not close. It OVERLAYS the form rather than replacing it: the fields stay mounted and disabled so a password manager mid-fill does not lose its target. A PERMANENT BLOCK gets no countdown, no ring and no retry affordance. An animation implying something is coming when nothing is would be a lie told slowly. It says the account is not the thing that was blocked, because that is true and is the first thing the person will want to know. No policy lives here. The browser owns the countdown and nothing else — a second copy of the ladder in client code is a copy an attacker can edit and a copy that drifts the moment either side changes. This repo has been bitten by exactly that before: the Node access-rule check compared rule types against "whitelist" while the publisher wrote "WHITELIST", matched nothing, and nobody noticed. So the API routes relay the server's numbers untouched, including forwarding 429 as itself rather than collapsing it into 400, which would report "bad request" for a response whose entire content is how long to wait. A lockout does not shake the password field or select its contents. The shake means "you typed the wrong password", and during a hold the password was never looked at — blaming the field would be blaming the wrong thing. All four states rendered and read back with puppeteer against canned server replies, because a green build proves nothing about how a page looks. Reduced motion is honoured explicitly; motion/react does not honour it on its own.

  11. · SecurityService

    Progressive sign-in lockout, escalating to a permanent IP block

    A wrong password now costs something, and the cost climbs. Counted since the subject's last successful sign-in: the first five failures are free, the sixth and every one after it waits five seconds, and the tenth, twentieth and thirtieth hold the door for five, fifteen and thirty minutes. The fortieth puts the address on the organization's permanent blocklist and mails sysadmin. WHO is counted is the part worth reading. The subject is normally the (address, identifier) PAIR, not either half alone. Counting the identifier alone would let anybody who knows a username lock its owner out from the other side of the world; counting the address alone would let one person behind a shared office NAT lock out their colleagues. A pair-only ladder has one hole, though — spray one password across two hundred usernames and no single pair ever reaches six — so an address seen failing against more than ten DISTINCT identifiers becomes a subject in its own right, and from then on every failure from it counts on one ladder whatever account it names. The harsher of the two governs. Checked BEFORE the password is compared, at every credential-bearing terminus: both sign-in paths and the MFA challenge, whose six-digit space gets its own ladder so a fumbled authenticator cannot spend the password step's budget. A caller who can still measure the difference between a right and a wrong password while locked out has a working oracle and a lockout that only wastes their time. Failures are counted for identifiers that name nobody, exactly as for ones that do. Counting only real accounts would make a wrong username cheap and a right one expensive, which is an account-enumeration oracle built out of latency. Redis-backed, 30-day sliding TTL, cleared where lastLoginService already stamps a real sign-in. Fail-open on every Redis error, matching AccessDecisionService's doctrine: a lockout that fails closed on an infrastructure wobble locks every tenant out of their own platform. Failures are logged WITH the throwable, not just its message — a defect in this path is otherwise indistinguishable from an outage, and would silently switch the whole control off. The fortieth failure blocks the address by itself rather than relying on the caller to go on and write the blocklist row. That row sits behind a 30-second verdict cache, and on a host naming no organization there is no row to write at all; a control that depends on its caller to be effective is one a later refactor switches off by accident. PROGRESSIVE_LOGIN_LOCKOUT appears in Detection Rules as a rule that cannot be switched off or deleted, and whose thresholds are not tenant data — an editable threshold is an off switch wearing a different hat. Recipients stay editable, or the block notice could only ever reach the platform operator. The lock lives in the service, not the controller: a gate that can be reached around is not a gate. The row is materialised on READ, because every existing organization already holds the five original rules and would never reach the seed-on-empty branch.

  12. · kamo-internal

    Show a member the redirect URI on their own domain

    This is the screen the whole change is for. A member setting up Google Workspace with their organization's own OAuth app comes here to find the address to paste into the Google console — and found api.kamocrm.com, hardcoded in the catalog, for an organization that has verified its own domain and whose members never see Kamo's hostname anywhere else. The address now comes from the server, per organization, and is rendered read-only rather than as a text box. That is not a styling choice: nothing ever read a value typed into that field. EmailService, MediaService and VOIPService each replay their own configured redirect on the token exchange, so a URI an org typed here was shown back to them and ignored — and an org that typed one which disagreed with what the service sends got a redirect_uri_mismatch from inside Google with nothing on this screen to explain it. It is no longer sent on save either, so no stored string can look authoritative on the next load. Every callback is rendered, not just the first. One Microsoft registration is the mailbox app and the Teams-meetings app; each half completes on its own service's callback, so Entra needs both and showing one leaves the other failing with no clue which URI was sent. The same applies to the platform card, so callbackUrl became callbackUrls there too — and Microsoft, Zoho, Zoom, Teams, Meta and Canva gained the entries they never had, which had left a platform operator with no address to register at all. The catalog's list stays platform-only and says so. Kamo's own app always runs on the platform host; reusing that list for an organization would put the platform's hostname in front of a white-labelled tenant and tell them to register an address their flow never sends.

  13. · SecurityService

    Serve each org the callback URL on its own domain

    This endpoint is where a member finds the string they paste into Google's console, so it is the endpoint that was wrong: it returned only the redirect the org had typed, and the screen filled the blank with a hardcoded api.kamocrm.com — the platform's hostname, in front of a white-labelled tenant, for an app that tenant registered itself. callbackUrls is server-computed and read-only. The host comes from this organization's verified domain and the paths from OAuthCallbackPaths, which is the same table EmailService, MediaService and VOIPService build their own redirect from — so the address shown here and the address sent on the wire are one string by construction rather than by agreement. Getting that wrong has no local symptom; it surfaces inside a third party, weeks later, as a redirect_uri_mismatch. A list, not a string: one Microsoft registration is the mailbox app and the Teams-meetings app, and each half completes on its own service's callback, so Entra needs both registered and the screen has to say so. Empty for a provider whose callback Kamo does not serve — X has no connect flow, and every payroll provider completes on a kamo-internal page on the workspace host. Those keep their editable field rather than being handed a plausible-looking api.* address nothing listens on. OrgDomains.verifiedRootDomain now delegates to the shared predicate for the same reason: two copies of the rule would let this screen and the services disagree about which domain routes.

  14. · VOIPService

    Fall back to the org's own hostname for the Teams redirect

    A redirect the org typed into its own registration still wins outright — Microsoft requires the URI replayed on the token exchange to match the authorization byte-for-byte, so whatever went into that Entra app is the only correct answer, and an existing connection must not move. What changes is the fallback under it. An org on its own app with no stored redirect was getting the platform hostname; it now gets api.<its own domain>, which is what its settings screen shows. An org on Kamo's multi-tenant app keeps the platform host, since that registration has one redirect and a per-org address against it is a mismatch. Both VOIP OAuth controllers already routed through this one method, so the org and member connect legs move together.

  15. · MediaService

    Send an org's own hostname as the meeting OAuth redirect

    The same rule the mailbox flow now follows, for Zoom and Teams meetings: an organization on its OWN app registered that app against api.<its own domain>, so that is what goes on the wire. The callback is public and sessionless, so both the organization and the provider come out of `state` — the provider as well as the org, because an org may be on its own Zoom app and Kamo's Entra app at once and the two must get different answers. An org on Kamo's app keeps the platform host: that registration holds one redirect URI and a per-org address against it is a mismatch nobody can fix from inside Kamo.

  16. · EmailService

    Send an org's own hostname as the mailbox OAuth redirect

    An organization that brought its own Google or Entra app registered that app in its own account, under its own brand, and is now told on its settings screen to authorize api.<its own domain>. This makes the service send that. Both legs go through one chooser, because they have to agree: the value on the authorization request and the value replayed on the token exchange must be byte-identical or the provider rejects the exchange. The callback leg is public and sessionless — the browser arrives from the provider with no Kamo session — so the organization comes out of `state`, which is why orgIdFromState exists alongside providerFromState. Gated on the org owning the client, and that gate is not incidental. Kamo's platform registration holds exactly one redirect URI, on the platform host; sending a per-org address against it is a redirect_uri_mismatch the org cannot fix from anywhere inside Kamo, because they do not own that Google project. hasOrgOwnClient is the same question PlatformOAuthClientResolver answers when it picks which client id to send, so the credential and the redirect can never disagree about which app is connecting. Only the authority is rewritten, so GOOGLE_OAUTH_REDIRECT_URI keeps its path and its scheme. OAuthRedirectUriTest calls the controller's own private chooser rather than restating it, and pins the configured defaults against OAuthCallbackPaths — the table the settings screen reads. That guard is the point of the test file: if the two drift, nothing here fails. It fails inside Google, weeks later, for an organization that copied a string off a screen that was already wrong. It also caught the one real bug in this change: platformProviderFor switches on the enum, and a switch over a null enum throws rather than falling to its default arm — so a callback whose state would not decode took an NPE on the way to its own error page.

  17. · kamo-shared-library

    Address an org's OAuth callbacks to its own domain

    An OAuth redirect URI is held by a third party. A member pastes it into Google's or Microsoft's console, and from then on the value the service sends on the authorization request and replays on the token exchange has to match what they typed, character for character. So the address a settings screen SHOWS and the address a service SENDS are one question, and until now they were answered by different constants in different repositories — with the platform's hostname baked into both. An organization that has verified acme.com registers its own OAuth app, in its own Google account, under its own brand, and was told to authorize api.kamocrm.com. OrgApiHosts answers "which host" and OAuthCallbackPaths answers "which path", and between them they are the one definition SecurityService, EmailService, MediaService and VOIPService all read. The domain predicate is the one the sign-in path already routes on — active, alias-less, ownership_verified, ssl_confirmed — and each condition earns its place here for its own reason. ownership_verified because duplicates across orgs are legal, so an unverified row would send a provider's callback, carrying a live authorization code, to somebody else's host. ssl_confirmed because `api` is one of the labels the SSL probe covers, so that flag is what makes api.<domain> a host with a certificate rather than a browser interstitial that swallows the code. reHost rewrites only the authority, so an operator who moved a callback in configuration keeps their path and their scheme, and a value that cannot be parsed is returned untouched rather than replaced by a guess. A provider may own more than one callback: a single Microsoft registration is the mailbox app AND the Teams-meetings app, and each half completes on its own service's path — so Entra needs both, and only a list can say so.

  18. · SecurityService

    User-account details belong to their owner, or to an operator

    One rule, in one place, for the three endpoints that write a USER account: the profile's member update, the profile's personal address, and the platform Users tab. They govern the same data and had drifted into three different answers. UserAccountEditPolicy: the person themselves, a god-eligible operator, the System User, or a platform administrator holding MANAGE_USERS / SYSTEM_USER — the Users tab on /account. The top-level-organization precondition rides along free, since PlatformRightsResolver resolves nothing for a session in a tenant. Two things it deliberately drops. MANAGE_MEMBERS and MANAGE_MEMBER_SECURITY no longer reach the legal name, the preferred name or the date of birth. Those are TENANT rights over an account every organization the person belongs to shares, and a tenant administrator can edit their own organization's security models — so a tenant could grant itself the ability to rewrite the identity everyone else sees. The same reasoning has always kept them off the password. This was also the only rule the endpoint enforced, so it is now stricter than before rather than merely different. "The current organization owns the account" is gone. It reads like the natural safeguard and was a dead end: the platform provides the account for 77 of 141 memberships, and 37 of those people are not members of the organization that owns their account at all — so no workspace existed in which they could ever edit their own name, preferred name or personal address. It also answered the wrong question for the owner, who is a PERSON, not an organization. Proof of the mailbox is untouched and is the safeguard that actually matters for the address. The policy resolves its operator arms even when the target account cannot be read. Refusing everybody on an unknown target sounds like failing closed and is really failing wrong: it would leave an account nobody could correct, which is the state an operator exists to get out of. `isSelf` on the member update stays by MEMBER id: an impersonating session has had its memberID swapped to the target, and a sudo session is meant to act as that member would.

  19. · kamo-shared-library

    A detection rule an org cannot switch off, and the letter it sends

    PROGRESSIVE_LOGIN_LOCKOUT joins DetectionRuleType. Every other value on that enum describes something a tenant opts into; this one describes a control SecurityService applies to every sign-in whether a row exists or not. The row exists so the policy is visible on the Security screen and its alert recipients are configurable — not so it can be turned off. The ladder itself is not per-org data and does not live in the table. 26 characters, so it fits rule_type varchar(30) with no DDL, and the column is mapped EnumType.STRING, so appending relabels no existing row. LOGIN_LOCKOUT_IP_BLOCKED is the letter sent when an address exhausts the whole ladder and lands on the permanent blocklist. It is the only alert in this family whose subject is not a member — it is about an attacking address — so every field it carries is attacker-controlled and every one of them uses the escaped twin. TemplateRenderer does no escaping of its own.

  20. · kamo-internal

    An operator may set a personal address, and ask about the right account

    Two changes to the member profile, both about the same confusion: which person a question is being asked about. The personal email card is no longer self-only. A god-eligible operator and the System User can set anybody's, because the member who needs it changed is usually the one who has just said their old mailbox is unreachable — the case the member's own flow cannot serve, since it works by mailing that address. Deliberately not MANAGE_MEMBERS or MANAGE_MEMBER_SECURITY: this is one of the three identifiers sign-in accepts and the one password recovery writes to, so setting it is taking the account over. Same line the password card draws. The card reads through the member-scoped endpoint even on the reader's own profile, so there is ONE code path and the SERVER decides whether it may be edited and in which shape. `mode` picks the wording, the write, and whether a password is asked for — an operator has already proved who they are, and asking them for a password they do not have would be theatre. Three things the operator variant must not do, each of which it did first: - render the pending-change panel. Its Confirm, Send-again and Discard call SELF-scoped endpoints, so an operator would have been handed three buttons that silently act on their OWN account while showing a colleague's address. - be suppressed by a change the member had pending. Overriding it is the point; the operator is told about it instead, including that setting an address cancels it. - offer the member's "where your account lives" explainer. An operator knows what a user account is; what they cannot see is that this write is not verified and does not stay quiet, so they are told that instead. Separately, isSecurityProviderOrg now means "this org owns the account behind the MEMBER BEING VIEWED" and comes from the server. It used to be derived in the browser from the READER's own account, which is a fact about whoever is looking: an administrator entitled to fix a colleague's legal name could be refused because their own account came from elsewhere, and one who was not entitled could be offered the box. For a member on their own profile the two readings coincide, so nothing about the self case changes. The banner that says "your account is managed by X" keeps the reader-scoped answer, because it is the one thing here genuinely about the reader — and it now renders only on their own profile. Telling somebody their account lives elsewhere while they are looking at a colleague's record answers a question they did not ask. Six tests pin the gate as a boundary, the sharpest being that an administrator holding both MANAGE_MEMBERS and MANAGE_MEMBER_SECURITY can neither see nor edit the address.

  21. · SecurityService

    Let a god operator and the System User set somebody's address

    The personal address was self-only. It is also the address an administrator has to be able to correct — a member who has lost the mailbox on their account cannot prove the new one, which is exactly the case the member's own path cannot serve. Who: a god-ELIGIBLE caller, or the platform System User. Deliberately NOT MANAGE_MEMBERS or MANAGE_MEMBER_SECURITY. This address is one of the three identifiers sign-in accepts and the one password recovery writes to, so being able to set it is being able to take the account over — the same line PasswordChangeController draws, for the same reason. Eligibility is read through SessionPlatformFacts rather than by reaching for `GD`, because that parser exists precisely so eligibility and an OPEN god-mode window cannot be confused. The target member must be in the caller's own organization, as the password endpoint requires. What does NOT apply is the security-provider rule: it exists to stop one tenant changing how somebody signs in to another, and a god operator and the System User are platform identities rather than a tenant. Keeping it would leave the capability unusable from the workspace an operator is actually standing in. No verification round trip on this path, and the address is marked UNVERIFIED — nobody proved it, and letting a written address inherit the verified standing of the one it replaced would make the flag a lie. Uniqueness within the provider still holds, because it is a sign-in identifier however it got written. The notice to the old mailbox still goes, and matters more here than on the member's own path: this is the case where the account holder did not start the change. Anything the member had pending is cancelled, so yesterday's confirmation link cannot silently undo the correction. The System User may now CALL this controller — administering other people's accounts is the point. It still may never be the TARGET. /context reports isGodEligible and isSystemUser so the console can decide what to offer. Neither is derivable there: the first must come through the shared parser, and the second is a column in no session at all, so without it the System User would open a profile and not be shown the card it is entitled to use. Both widen a screen; every write re-derives them.

  22. · KlusterServices

    Budget the last four services to gain a second replica

    BillingService, EmailService, KamoLOS and SecurityService now run two pods, each having put its shared-effect scheduled sweeps behind a distributed lease first. Between them that covers the work where running twice would have been most expensive: charging a customer, debiting a bank account, sending a campaign, and refreshing an OAuth grant.

  23. · SecurityService

    Lock the shared sweeps, and run two pods

    Sixteen of SecurityService's seventeen scheduled sweeps have a shared effect and now take a named distributed lease before running. There is no ShedLock anywhere on this platform, so the replica count was what made them correct — this service had the most scheduled work in the fleet and was therefore the most firmly pinned to one pod. The ones worth naming: **************** two pods refreshing the same OAuth grant can invalidate each other's new token and disconnect the calendar BulkExportRunner.pollForWork two pods claiming the same queued rows would run the same EHR/PHI export twice DomainVerificationWatcher flips ownership_verified, which decides which host routes to which org DocReminderSweep sends document reminders to borrowers LeadLockSweeper could release a lock another pod just granted **************** is a @Component nested inside a @Configuration, so it took the lease by hand rather than through the same mechanical edit as the rest. Four tests now exercise the locked body directly (sweepLocked, reverifyConfirmedLocked, pollForWorkLocked). They previously called the scheduled entry point, which now asks a mocked SingletonTaskRunner for a lease it never grants — so every assertion in them would have passed against a body that never ran. In AccessRuleService and OrgObjectStorageSweep the injected field is declared LAST: those use Lombok's @RequiredArgsConstructor, which builds its parameter list in field order, so putting it anywhere else silently reorders the constructor's existing arguments. replicas 1 -> 2. Verified in an isolated worktree off origin/main because this checkout currently carries another session's in-progress OrgNetworkRowBuilder refactor, which does not compile; with only these files applied the service builds and its suite is green apart from two failures that reproduce identically on origin/main (UnauthenticatedEndpointRatchet flags UserAccountController #verifyByToken, which is not part of this change).

  24. · kamolos

    Lock every cron job that moves money, and run two pods

    KamoLOS ran one pod and its ten cron jobs were correct only because of it. There is no ShedLock anywhere on this platform, so a second replica would have run each of them twice: AutoDebitRunJob DEBITS BORROWERS' BANK ACCOUNTS InterestAccrualJob accrues interest onto live loans — twice is interest charged twice StatementGenerationJob two statements for one period ReminderDispatchJob two payment reminders YearEndTaxJob two filings' worth of paperwork per borrower plus offer expiry, delinquency, cooling-off advance, reminder scheduling and reconciliation Each now takes a named lease through SingletonTaskRunner before doing anything. **************** is deliberately NOT locked. It downloads the OFAC SDN list and replaces a Set held in this pod's own memory, so it has to run everywhere — locking it would leave every pod but the leaseholder screening applicants against an EMPTY sanctions list, which is worse than the duplication it would prevent. replicas 1 -> 2.

  25. · EmailService

    Lock the shared sweeps, and run two pods

    Nine of EmailService's ten scheduled sweeps have a shared effect and now take a named lease before running. The most consequential is CampaignDispatcher: at two pods without a lock, every recipient of every running campaign would have received a duplicate. ReminderSchedulerService is the same shape for meeting reminders, and **************** for snoozed notifications. The tenth, **************** is deliberately NOT locked. It walks the mailboxes watched by clients connected to THIS pod and nudges the ones nothing is pushing, so it has to run everywhere — locking it would freeze the unread badge for every member whose WebSocket landed on a pod that is not the leaseholder. @Transactional is now on both the scheduled wrapper and the locked body in ReminderSchedulerService and SyncService. The outer one is what actually applies (the body is reached through a this:: reference, which never goes through the Spring proxy); the inner is declared because that is the method issuing the @Modifying query, which is what the arch ratchet reads — and the ratchet caught exactly this when the annotation was only on the wrapper. replicas 1 -> 2. 590 tests, one pre-existing failure unrelated to this change **************** fails identically on origin/main).

  26. · BillingService

    Lock every money-moving sweep, and run two pods

    BillingService ran one pod and its seven scheduled sweeps were correct only because of it. There is no ShedLock anywhere on this platform, so a second replica would have run each of them twice a tick: **************** submits metered usage to the payment provider — submitted twice, the customer is charged twice DunningSweep retries failed payments AND emails about them StorageAutoExpandSweep buys storage on the customer's behalf, and bills it TrialExpirySweep ends trials and moves subscriptions on MailboxBillingReconciler / PbxBillingReconciler fight over the same subscription items StorageSnapshotSweep double-counts every org's nightly usage figure Each now takes a named lease through SingletonTaskRunner before doing anything. StorageSnapshotSweep is the one exception in shape: its sweep() is ALSO called straight from StorageUsageController when an operator asks for a snapshot now, and returns the row count for the response. So the @Scheduled annotation moved to a new scheduledSweep() that takes the lease, and sweep() itself is untouched and still directly callable — an operator's explicit request should not be silently skipped because another pod happens to be mid-sweep. replicas 1 -> 2.

  27. · KlusterServices

    Budget the six services that just gained a second replica

    AIService, KBService, ESigService, VOIPService, DocsService and TranslateService now run two pods, so a drain of either node must evict them one at a time rather than both at once. The first five got there by putting their shared-effect scheduled sweeps behind a distributed lease; TranslateService needed no lock at all, because its one scheduled method refreshes a cache in the pod's own memory and is supposed to run everywhere.

  28. · TranslateService

    Run two pods

    The one @Scheduled method here, **************** populates a map held in this pod's own memory. It is supposed to run on every pod, there is nothing shared for a second replica to duplicate, and it deliberately gets no lock — locking it would leave every pod but the leaseholder with an empty language table. Nothing else in this service holds state between requests.

  29. · DocsService

    Lock the shared sweeps, and run two pods

    Every @Scheduled sweep here whose effect is shared now takes a named distributed lease through SingletonTaskRunner before it does anything, so it runs once across the fleet instead of once per pod. That was the only thing keeping this service at one replica — there is no ShedLock anywhere on this platform, so the replica count was what made the sweeps correct. @Transactional and friends stay on the OUTER method where present: Spring applies them with a proxy, and the inner method is reached through a this:: reference, which never goes through one. Moving them inward would leave the sweep silently non-transactional. replicas 1 -> 2, with a PodDisruptionBudget in KlusterServices so a node drain waits.

  30. · VOIPService

    Lock the shared sweeps, and run two pods

    Every @Scheduled sweep here whose effect is shared now takes a named distributed lease through SingletonTaskRunner before it does anything, so it runs once across the fleet instead of once per pod. That was the only thing keeping this service at one replica — there is no ShedLock anywhere on this platform, so the replica count was what made the sweeps correct. @Transactional and friends stay on the OUTER method where present: Spring applies them with a proxy, and the inner method is reached through a this:: reference, which never goes through one. Moving them inward would leave the sweep silently non-transactional. replicas 1 -> 2, with a PodDisruptionBudget in KlusterServices so a node drain waits.

உங்கள் உலகம் காத்திருக்கிறது.

நிமிடங்களில் இலவசமாகத் தொடங்கவும். நீங்கள் தயாராக இருக்கும் போது பொருட்களை சேர்க்கவும் - எந்த மாற்றமும் இல்லை, புதிய உள்நுழைவுகள் இல்லை.

எப்பொழுதும் இலவசம் — கடன் அட்டை தேவையில்லை