Skip to content

Setup Invitations: with no mail transport the toast says "Invitation sent to …" and there is no Copy invite link, so the invitation dead-ends (#27521 row 6) #27797

Closed

Description

@objectstack-fleet

domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe

Ruled: 6512831254 · letter E · 2026-10-30T01:40Z · server half #27843 (wait for a server seat; #27797 is Blocked-by #27843)

Filing gate: ③ triage-directed split. Triage on #27521 (comment 6495068090) said: "row 6 … and row 27 … Each becomes its own p2 card, because both are wrong statements to an administrator, not suggestions." The seat files the split before it picks rows.
Reach: measured on the 18.3.0 RC browser walkthrough (wave anchor #27377, W3-UX track 2), framework main @ c4c515ad, stock examples/app-showcase with --seed-admin, admin, desktop 1440×900.
Business reason: an administrator who invites someone on a deployment with no mail transport is told the invitation was sent. Nothing was sent, and there is no way to hand the link over by hand, so the invitee never arrives and nobody knows why.
Reader who acts: domain:ui (Setup → Invitations, the invite toast, the row menu and the detail drawer). The platform already knows whether mail can leave: IEmailService.isConfigured?() answers false for the log transport (#25335).
Duplicate search: search_issues "invitation sent toast no mail transport copy invite link" → 2 hits, both closed: #25335 (the Setup → Email page states the live transport; not the invite toast or a copy action) and #8090 (a broken copied-link URL in the workspace Invitations tab, not_planned). Neither covers this.

What happens (row 6 of #27521)

Admin, desktop, Setup → Invitations → Invite. On the stock deployment, which has no mail transport, the toast says "Invitation sent to …". There is no Copy invite link in the row menu or in the detail drawer, so the invitation dead-ends.

Expected

Acceptance

  • One measured repro on current main first.
  • Pins: the toast wording follows the transport reading; the copied URL is the accept page under the console mount.

Related: #27521 (parent, row 6), #25335, #8090.
Size: S/M.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

    Claim: PM loop round 3 · 2026-10-29T23:47Z
    Session: session_01Utp6CY2n1JmZZKrHgtp9jC (seat; implementation by a card thread, mode:thread)
    Account: os-bill
    Branch: claude/project-thread-18obwq (the card thread's harness push branch, self-reported — the measured platform exception to claude/issue-N-slug)
    Worktree: objectstack-issue-27797
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: the invite dialog / toast, the Setup → Invitations row menu and detail drawer (measured and named by the dev), their tests, i18n keys in the ten packs, a changeset; ⛔ packages/spec/**, every server file (a server change to return the invite link stops the card with needs_decision), examples/**; PR carries Fixes #27797 + Refs #27521 (row 6)
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: high
    Clause-②: no (expected; the dev measures from the diff)
    Metadata-protocol: none (expected; the dev measures from the diff)
    Responsibility: the invite confirmation claiming "sent" without reading the transport, and no copy-link action | the server's transport reading (IEmailService.isConfigured, #25335) and the accept route are fixed inputs | every administrator who invites someone on a deployment with no mail transport, today
    Thread-read: none
    Premise: holds as filed (measured on the 18.3.0 RC walkthrough at c4c515ad); step 1 of the order re-measures on current main and step 2 measures whether the client can obtain the invite link at all
    Serial constraints cleared: split from #27521 row 6 by seat 3 (triage 6495068090); seat 3 handed it to seat 2; the #27509 auth pages (sign-up) landed earlier and are not in this surface

    Dispatch order: /mnt/project-files/ui-seat2/dispatch-27797.md in the Claude Code project (not on GitHub); its ruling section quotes the triage text above verbatim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

    os-dev-report

    {
      "issue": 27797,
      "status": "needs_decision",
      "branch": "claude/project-thread-18obwq",
      "pr": null,
      "session": "session_01Utp6CbAThbSd1afHuxTxn6 (card thread; seat session_01Utp6CY2n1JmZZKrHgtp9jC)",
      "premise_still_valid": true,
      "summary": "Measured on objectstack origin/main 561b7642, source reading (no live run). The defect holds, but every surface the card names is server metadata, not ui code, so the ui file surface cannot deliver it. (1) Setup -> Invitations is the generic ObjectView over sys_invitation (setup-nav.contributions.ts, nav item nav_invitations, type object). (2) The toast is the declared successMessage of the invite_user action, 'Invitation sent to {{ result.email }}', in packages/platform-objects/src/identity/sys-invitation.object.ts and its three mirrors (sys-user, sys-member, sys-organization); the runner shows it as declared (ObjectView.inviteFeedback-11817.test.tsx pins that path). (3) The row menu and the detail drawer render the sys_invitation list_item actions (cancel_invitation, resend_invitation), also declared in that file. (4) The spec ActionType enum is script | url | modal | flow | api | form: no action can copy to the clipboard, and nothing in the console registers a generic copy script. (5) Transport: the client has no machine-readable fact. Setup -> Email reads GET /api/settings/mail, whose live_status info_banner carries prose (label, bannerText, bannerSeverity) and no boolean; the one server reader is AuthManager.isEmailDeliveryLive, private, and nothing reaches the invite-member answer or the public features. (6) The link itself IS obtainable without a server change: the invitation row id gives resolveConsoleUrl('accept-invitation/' + id), the exact pattern the workspace pages already use (organizations/manage InvitationsPage handleCopyLink and InviteMemberDialog), and the server builds the same route in sendInvitationEmail. A ui-only fix would have to special-case one object and one action inside the generic renderer and infer the transport from banner prose, which ui AGENTS.md #0.1 and ADR-0060 rule 2 forbid. The toast half cannot proceed alone either: its copy is metadata. Stopped before any edit.",
      "tests": "none: no code written. Live repro on stock app-showcase NOT MEASURED, reason: the source reading already places the toast literal and both menus in server metadata; the live repro moves to whichever card the decision dispatches.",
      "gates": "none run: empty diff.",
      "dogfood_battery": "n/a: no PR",
      "mcp_calls": "4: issue_read get x1, issue_read get_comments x3; no write tool",
      "api_writes": "1: this comment, POST /repos/objectstack-ai/objectstack/issues/27797/comments via scripts/pm/post-stamped.mjs",
      "open_questions": [
        {
          "question": "Where do the transport-aware toast and the Copy invite link action live, given both surfaces are sys_invitation metadata and the client has no machine-readable deliverability fact?",
          "options": [
            "A. Server and contract first, ui after: the invite-member answer (or the public features) carries the deliverability fact as a boolean; the spec gains a clipboard-copy executor for actions, resolved mount-aware with resolveConsoleUrl by the renderer; sys_invitation and the three invite_user mirrors declare the copy action and the branch in their copy. Split as a server/contract card plus this card's ui half. Size L, Clause-2 yes (widening), Metadata-protocol added.",
            "B. Point Setup -> Invitations at the existing workspace Invitations page (organizations/manage), which already has a per-row Copy invitation link and an 'Invitation created, share the link' dialog. One nav metadata change plus a transport read in that dialog. Moves administrators off the governed metadata action (role-cap option gates, cloud-managed gate, membership reach) onto the less governed dialog, against the rule that the governed side of two implementations wins; the two invite implementations stay divergent.",
            "C. ui only: the renderer special-cases sys_invitation / invite_user, reads the mail live_status banner severity, overrides the declared successMessage and injects a menu item. Inside the file surface, but against ui AGENTS.md #0.1 (contract-first) and ADR-0060 rule 2, and it parses prose.",
            "D. Narrow now: change invite_user successMessage on the four mirrors to transport-neutral wording ('Invitation created for ...'); copy link deferred to A. Small server-metadata edit; breaks the card's 'sent stays when mail is live' acceptance."
          ],
          "recommendation": "A, because both halves are metadata and the deliverability fact has no client door today: declaring the copy action and the fact once keeps the renderer generic, keeps the governed invite action the one implementation, and lets the next share-link surface reuse the executor. C is the shortcut the contract-first rule exists to refuse."
        }
      ],
      "out_of_scope_findings": [
        "carrier: seat 2 · noted, not filed: two invite implementations diverge (sys_invitation metadata action vs organizations/manage InviteMemberDialog); only the second offers the accept link. Dedupe words: invite member dialog duplicate, sys_invitation copy link, invitation share link"
      ]
    }
  3. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    director#1 · os-bill · session_01Utp6Cb7FDwjs2e5wWuRbRq

    Ruling: where the transport-aware invite toast and the Copy invite link live (project decision thread 「决定无邮件时邀请如何分享链接」) · letter E · maintainer 「同意你的建议,但是你应该等车道开发,着急的标记高优先」 2026-10-30T01:39Z

    The maintainer's own ruling, given in the project thread at 2026-10-30T01:37Z on the review thread's six-section analysis (/mnt/project-files/decisions/27797-邀请无邮件分享链接-呈批.md, recommendation E). Marker: maintainer-ruled, presented by the review thread; not a proxy ruling. It replaces the card thread's recommendation A (report 6511276997) and domain:ui#2's brief, both of which asked for a spec clipboard executor.

    What was re-verified (code read at 561b7642 on main)

    • The card thread's measurement holds: the toast is invite_user.successMessage in packages/platform-objects (sys-invitation.object.ts and the sys-user / sys-member / sys-organization mirrors); the row menu and drawer are the object's list_item actions; the client has no machine-readable mail-deliverability fact (Setup → Email's live_status banner is prose); AuthManager.isEmailDeliveryLive() is private.
    • Option A's premise — "the spec has no copy executor" — is true of a clipboard action type and beside the point. packages/spec/src/ui/action.zod.ts already carries outcomeMessages (success copy keyed by the server's outcome, for type: 'api' and type: 'script'; the console runner reads payload.outcome and composes the toast from it) and resultDialog with format: 'text' — "plain text with copy" — which sys_oauth_application.rotate_client_secret uses from list_item today. ActionResultDialog.tsx writes the clipboard.
    • A type: 'script' action in the console is the platform action route (POST /api/v1/actions/<object>/<action> → engine.registerAction), so a server handler can answer the link without the renderer learning anything about invitations.
    • The server already builds the accept link once, getConsolePageUrl('/accept-invitation/<id>') in sendInvitationEmail, and documents that no second link builder exists. The console: copied invitation link is unusable — buildAcceptUrl rebuilds the trailing-dot host that resolveHomeUrl already retired #8090 lesson concerns the client's import.meta.env.BASE_URL, which this route never touches.
    • Adding a member to ActionType stops the console compiling until an executor exists (the enum's own docblock: the ActionRunner dispatch table is typed Record<RunnableActionType, …> for that purpose), so A is a spec Tier H change plus a lock-step console change for a capability the spec already has.

    Governing text

    The ruling

    E — use the contract the spec already has; no spec change. One domain:server card (#27843) carries the server and metadata half; #27797 keeps the ui half (a live re-measure and a pin once the server card is on main).

    Execution parameters, fixed (一次裁清):

    1. A successful invite-member / resend answer carries a top-level outcome: 'sent' | 'link_only', decided at call time by isEmailDeliveryLive() — ⛔ never by hasEmailTransport / isEmailServiceAvailable.
    2. plugin-auth registers sys_invitation.copy_invitation_link on the platform action route; it answers { acceptUrl } from getConsolePageUrl('/accept-invitation/<id>'), reached by the resend_invitation gates (organization feature, invite_member reach, hidden under cloudManaged) and only for a pending row. The URL is a bearer credential: never logged outside dev.
    3. sys_invitation and its three mirrors: invite_user and resend_invitation move their success copy to outcomeMessages (sent keeps today's wording; link_only says the invitation was created and mail is not configured, never "sent"); a new copy_invitation_link action, list_item + record_header, type: 'script', resultDialog with format: 'text' on acceptUrl.
    4. Pins: log transport → link_only and no "sent" in the toast; a configured transport → sent; the dialog's URL equals the mail's URL for the same invitation; refused outside the reach and for a non-pending row; hidden under cloudManaged.
    5. Clause-②: no; Metadata-protocol: none. Review tier by the touched paths (packages/plugins/plugin-auth, packages/platform-objects).

    ⛔ Not chosen: A (a spec clipboard executor — Tier H and a console lock-step for a capability the spec has), B (pointing Setup → Invitations at the workspace page, which moves administrators off the governed action and keeps two invite implementations), C (a renderer special case reading banner prose), D (neutral wording alone — absorbed by parameter 3, so not dispatched separately).

    Who does the server card — the maintainer's word, verbatim: 「你应该等车道开发,着急的标记高优先」. The server card goes to the domain:server queue and waits for a server seat; ⛔ no cross-lane exception by the ui seat, no seat opened for it by the review thread. Its grade stays p2 (the parent split's grade); the maintainer raises it if it is urgent.

    Execution after the ruling

  4. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

    Release (seat's own claim) · 2026-10-30T01:42Z

    Blocked-by: #27843


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

    Unblocked · 2026-10-30T13:59Z

    Blocked-by: #27843 is met: the server card closed when PR #27902 merged at 2026-10-30T04:07Z (merge commit f552fa81f1). Per the release note (ruled E, 6512831254), the next claimant re-dispatches this card as a live re-measure plus one ObjectView pin (the outcome toast and the result-dialog copy). No ui code is expected. State: pm:blocked → pm:queue. Seat 2 is going off duty and does not claim it.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-tesla · session_01L9qibzC6RxMfB59EsgAoNN

    Claim: PM loop round 2 · 2026-10-30T14:58Z
    Session: session_01L9qibzC6RxMfB59EsgAoNN (seat; implementation by card thread session session_01L9qibvGisaRusgvzEwJxoQ, mode:thread)
    Account: os-tesla (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-tk3ydm (the card thread's designated push branch, the recorded platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-27797
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: the ObjectView / action-runner test beside ObjectView.inviteFeedback-11817.test.tsx under ui/ (nothing else expected; a ui-side producer fix only if the live re-measure shows one, written into the report and PR body; stop on breach and explain)
    Container & model: M, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the invite confirmation toast and the copy-invite-link answer, per ruling E (6512831254): server-owned via outcomeMessages and a resultDialog with format: 'text'; the renderer learns nothing about invitations
    Thread-read: 6524625478
    Premise: holds (#27843 closed by PR #27902, merged f552fa81f1; sys-invitation.object.ts on origin/main 10fc9bf1 carries the outcome messages and the result dialog; seat 2 unblocked the card in 6524625478)
    Serial constraints cleared: no live claim or open PR touches the ObjectView invite pins; domain:ui#2 released its own claim (6512865993) and is going off duty

    Clause-② is the seat's expectation; the dev measures it on the diff and the seat amends the claim if it moves. Order: /mnt/project-files/ui-seat-1/r2026-10-30/dispatch-27797.md under FRAME.md (delivered in the card thread's opening message).

  7. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-tesla · session_01L9qibvGisaRusgvzEwJxoQ

    os-dev-report

    {
      "issue": 27797,
      "status": "done",
      "branch": "claude/project-thread-tk3ydm",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/28182",
      "session": "session_01L9qibvGisaRusgvzEwJxoQ (card thread; seat session_01L9qibzC6RxMfB59EsgAoNN)",
      "premise_still_valid": true,
      "summary": "Live re-measure on stock app-showcase at origin/main 10fc9bf1 (full workspace + console build from the tree, Chromium 1440x900): the ruled server fix (#27843, PR #27902) reaches the console unchanged. With the default log transport the invite door answers outcome link_only and the toast reads 'Invitation created for ... Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it.'; resend reads 'Invitation renewed for ...'. The row menu and the drawer record header both offer Copy Invite Link, whose result dialog shows and copies http://localhost:38797/_console/accept-invitation/ID (200 text/html). With OS_EMAIL_PROVIDER=smtp against a local SMTP sink the door answers outcome sent, the toast reads 'Invitation sent to ...', and the mail's accept URL equals the dialog's URL byte for byte. No ui code was needed; the diff is one ObjectView pin (link_only toast and its sent control; copy-link dialog to clipboard and its non-pending control).",
      "tests": "pnpm --dir ui exec vitest run packages/app-shell/src/views/ObjectView.inviteOutcome-27797.test.tsx at 43ba24a6 under os-verify-lock: VERDICT command-exit 0, Tests 4 passed (4). First run failed 3/4 because the fixture omitted features.cloudManaged (served predicates read it; absent fails closed; a stock boot serves false); fixture corrected to the served value. No ablation: the diff changes no tested behaviour.",
      "gates": "dispatch-gates --commands at 43ba24a6: 24 derived, all exit 0 (pnpm --dir ui exec vitest run scripts/: 169 files passed, 2 skipped, 4901 tests passed; node scripts/check-sdui-manifest.mjs + --self-test exit 0). --ran reconciliation: 24 run, 0 NOT-MEASURED, 0 UNRUN. Outside the derivation: ui check-new-cross-file-line-citations exit 0 (0 new); app-shell tsc -p tsconfig.test.json exit 0 (0 errors); eslint on the pin exit 0. CI check-runs on PR 28182: in_progress at report time.",
      "line_budget": "+357 / -0, 1 file, under the 3000 human-merge threshold",
      "files_changed": ["ui/packages/app-shell/src/views/ObjectView.inviteOutcome-27797.test.tsx"],
      "deviations": "skip-changeset not applied by the dev per the order (tests only in private @object-ui/app-shell; built dist has 0 hits for the test vs positive control 8 for ActionResultDialog): the seat labels after its own check. The primary checkout was detached to host the designated branch in the worktree objectstack-issue-27797.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "4 — issue_read get x1, issue_read get_comments x2, pull_request_read get_files x1; no write tool",
      "api_writes": "2 — pr_create via fleet-write relay (POST /repos/objectstack-ai/objectstack/pulls, run 41082772131, read-back identical); this comment via post-stamped (POST /repos/objectstack-ai/objectstack/issues/27797/comments)",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: console result dialog measured live (Copy Invite Link dialog: the copy button's aria-label and text are both empty) · evidence: CopyButton in ui/packages/app-shell/src/views/ActionResultDialog.tsx renders an icon-only Button with no accessible name, used by the text, secret, qrcode, code-list and json formats, so every resultDialog's copy control is an unnamed button to a screen reader · dedupe words: result dialog copy button accessible name, CopyButton aria-label, resultDialog copy icon"
      ]
    }
  8. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-tesla · session_01L9qibzC6RxMfB59EsgAoNN

    Landed · 2026-10-30T19:37Z · PR #28182 (seat ACCEPT 6526955724, re-pinned to 05e22529).

    reading value
    PR merged 2026-10-30T19:35:51Z as 86cf7ceff6 through the merge queue. It was ejected once on main-red #28219 and re-queued after #28227
    origin/main content ui/packages/app-shell/src/views/ObjectView.inviteOutcome-27797.test.tsx present

    State: the card closed as completed by Fixes #27797, and pm:dispatched comes off in the same stroke as this note. The live re-measure on the PR shows the ruled server fix (#27843) reaching the console, and the pin holds it. Nothing remains on this card. The out-of-scope copy-button finding is #28186.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:uipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions