Skip to content

service-messaging: the e-mail channel reports available on a LogTransport, so a send with no mail transport reads as delivered while nothing is sent (objectstack-ai/cloud#2549 ruling B, item ③) #30510

Closed

Description

@objectstack-fleet

repo:cloud#3 · hotlong · session_01Q4EjjRhUZjdy8VXs6Kkur2

Filing gate: executing a recorded ruling. The maintainer ruled objectstack-ai/cloud#2549 B; their selection, verbatim, is 「B 只让租户自配邮箱」 (ruling record on that card). This card is item ③. Item ② (approvals request e-mail) is a sibling card.

Owner. The repo:cloud lane (repo:cloud#3, #30278) follows it to close; triage routes it. Priority p3, inherited from objectstack-ai/cloud#2549.

维护者速读

环境没配发信方式时,邮件服务会用一个「只写日志」的假通道:发信调用返回成功、写一条发信记录,实际一封没发。邮件渠道还把自己报告成「可用」。结果是任何要求发邮件的通知或流程都以为已送达。改成:只写日志时,邮件渠道如实报告「未配置发信」,让投递记录和界面都显示真相。

Measured (objectstack origin/main 48231b02; cloud e91d1472; read-only)

  • packages/services/service-messaging/src/email-channel.ts ~:341-345: isAvailable() is true whenever an email service object exists, otherwise {available:false, reason:'transport_not_configured'}. It does not ask which transport the service holds.
  • packages/plugins/plugin-email/src/email-plugin.ts: provider defaults to 'log' (:456). With none configured it logs "no transport configured — using LogTransport (mail will NOT be sent)" (:494). It binds to the mail settings namespace on kernel:ready and hot-swaps the transport (~:615-652).
  • In cloud, every hosted tenant kernel force-mounts email (apps/objectos/hosted-slate.ts:92) without a transport (packages/objectos-runtime/src/capability-loader.ts:524), so every hosted environment is in this state until its admin configures mail.
  • Not measured: an actual send on a tenant kernel. The "reports success, writes sys_email, delivers nothing" behaviour is quoted from cloud's capability-loader.ts comment (~:1022-1027).

Expected

  • plugin-email exposes whether its current transport is real (not the LogTransport), in a form the channel can read.
  • email-channel.isAvailable() answers {available:false, reason:'transport_not_configured'} while the transport is the LogTransport. It flips back without a restart when mail settings hot-swap a real transport in.

Acceptance

  • A test proves a kernel with no mail settings has the e-mail channel unavailable, with that reason, and no fake delivery row.
  • A test proves that after a settings hot-swap to a real transport the channel is available.
  • The existing messaging tests stay green.

Dedupe (2026-11-05): repo:objectstack-ai/objectstack email channel LogTransport available is:issue → 3 hits (QA runs only); transport_not_configured is:issue → 4 hits, all closed (#18567, #18050, #17732), none on the LogTransport case.

Activity

  1. added
    bugSomething isn't working
    repo:cloudCard routed whole to the cloud PM: needs cloud code, a producer or a fact (cloud owns it to closure)
    on Nov 5, 2026
  2. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p2 · domain:server · 2026-11-05T14:02Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE

    Claim: PM loop round 3
    Session: session_01Utp6CXCS98uhsFQYk64epE (seat) · dev = project card thread session_01Utp6CaGmRyZ3XL5MuUgEZg
    Account: os-bill
    Branch: claude/project-thread-cufcnh (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-30510
    Domain: domain:server
    Seat: domain:server#6
    File surface: packages/services/service-messaging/src/** (e-mail channel and its composition), packages/plugins/plugin-email/src/** except mail.manifest.ts, their tests; content/docs/** only for sentences this PR makes false (not releases/**); one changeset. ⛔ packages/spec/**, ui/**, skills/**, docs/adr/**, .claude/**, AGENTS.md, **/mail.manifest.ts (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier (opus), effort: high
    Clause-②: no (provisional) — a channel that answered available on a LogTransport answers unavailable with the existing transport_not_configured reason; the dev corrects it from the diff
    Metadata-protocol: none (provisional) — no authored metadata changes meaning
    Responsibility: service-messaging email-channel.ts isAvailable() answers available whenever an email service exists, including one holding plugin-email's LogTransport fallback, so every e-mail send reads as delivered while nothing is sent | none: availability asks only whether the service exists | every deployment and hosted tenant with no mail transport configured (cloud#2549 ruling B item ③)
    Thread-read: 6659943585
    Premise: holds (seat read on origin/main: email-channel.ts:341 returns available: true when getEmail() resolves; email-plugin.ts:455-456 builds a LogTransport when provider is 'log', the default).
    Serial constraints cleared: no open PR touches email-channel.ts or email-plugin.ts at 14:22Z; seat 5's #30212 row 3 edits mail.manifest.ts (excluded) and EmailService's default sender — whichever merges second merges origin/main; sibling #30509 (seat 6) stays off both files

    Order: project file server-seat6/dispatch-30510.md, delivered to the card thread.

  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
    os-dev-report

    {
      "issue": 30510,
      "status": "done",
      "branch": "claude/project-thread-cufcnh",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30547",
      "session": "session_01Utp6CaGmRyZ3XL5MuUgEZg",
      "premise_still_valid": true,
      "summary": "Premise re-measured on origin/main fa9c0aa2: email-channel.ts isAvailable() answered available whenever getEmail() resolved; plugin-email's EmailService already implements the spec's optional IEmailService.isConfigured() from the live transport (false on LogTransport, follows setTransport hot swaps, pinned in email-plugin.mail-settings.test.ts). So no spec and no plugin-email change was needed: the channel now asks isConfigured() on every call (emailTransportRefusal); an explicit false answers {available:false, reason:'transport_not_configured'} at fan-out and refuses send() with the same reason, graded permanent, without calling the service. A service without isConfigured keeps today's behaviour (available), declared in Acceptance notes and open_questions.",
      "tests": "head fbfbaf47. Red on fa9c0aa2 (tests only): email-channel.test.ts 'Tests 4 failed | 43 passed (47)'. Green: 'Tests 47 passed (47)'. Ablation (scripts/ablation-replace.mjs, emailTransportRefusal forced to undefined; anchor 1->0, blob deebc1c7be23->a81f47328d2c): 'Tests 4 failed | 43 passed (47)'; restored, blob == HEAD, git diff HEAD empty; tests import src by relative path, no dist between. Package suite under os-verify-lock: 'Test Files 66 passed (66) · Tests 817 passed (817)', VERDICT command-exit 0. typecheck exit 0 (--listFiles includes email-channel.test.ts). eslint --no-inline-config on both changed files exit 0. check:nul-bytes exit 0.",
      "gates": "dispatch-gates --commands on fbfbaf47: 62 commands, each run with exit captured before any pipe; --ran: '62 derived, 59 run, 3 NOT-MEASURED, 0 UNRUN'. NOT MEASURED (exit 3 prerequisite): pnpm check:dual-build-cjs-loads (whole workspace not built), pnpm check:i18n (build closure missing; no translation bundle touched), node scripts/check-plugin-teardown-shape.mjs --self-test (shallow clone; checker health only). CI families: PR check-runs in_progress at report time.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "line_budget": "212 changed lines (+209/-3), 3 files; under the 3000 human-merge threshold",
      "files_changed": [
        ".changeset/30510-email-channel-log-transport-unavailable.md",
        "packages/services/service-messaging/src/email-channel.ts",
        "packages/services/service-messaging/src/email-channel.test.ts"
      ],
      "deviations": [
        "Repo-wide `pnpm lint` not run locally (os-dev.md: repo-level scans are CI's Lint job); eslint --no-inline-config ran on the two changed source files instead, exit 0.",
        "The order's kernel pin (plugin-email + messaging in one kernel) is pinned as two halves joined by the spec contract: service-messaging has no plugin-email dependency and dogfood is outside the file surface. The producer half (isConfigured false on the out-of-the-box boot, true after a saved SMTP config) was already pinned in plugin-email.",
        "plugin-email untouched, so no overlap with seat 5's EmailService edit.",
        "PR body carries the harness attribution comment as line 1 and the session-URL footer; harness-written commit trailers none (commits carry the model-free pair)."
      ],
      "mcp_calls": "0",
      "api_writes": "2 — relay pr_create (objectstack-ai/objectstack#30547, run 42051382947); post-stamped comment on #30510 (this report). Plus git push (not a REST write).",
      "open_questions": [
        {
          "question": "An email service with no isConfigured member: available (unknown, today's behaviour) or unavailable (absent read as false, as plugin-auth's sign-up flow reads it)?",
          "options": [
            "A keep available: only an explicit false refuses; no host-supplied service that predates the member goes silent",
            "B unavailable: matches the contract's 'never as true' sentence literally, but mutes every email service without the member"
          ],
          "recommendation": "A, because the fan-out ruling already chose absent = available for a channel without isAvailable, the ruling here is about the LogTransport, and B is a separate muting decision; flipping is one line in emailTransportRefusal"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": []
    }

    Report taken 2026-11-05T15:06Z.

  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE

    ACCEPT → PR #30547 comment 6661956909 · 2026-11-05T15:30Z · head fbfbaf47:全绿,已 ready + auto-merge。

  7. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE

    Landed · 2026-11-05T16:26Z · PR #30547

    Probe Reading
    PR #30547 merged, merge commit ede9e6bbca, merged at 2026-11-05T16:24Z through the merge queue (seat record 6661956909)
    #30510 closed · completed by the PR's Fixes

    State change: pm:dispatched removed; card closed; card thread resolved. Item ③ of ruling B on objectstack-ai/cloud#2549 is delivered here; item ② is #30509 (PR #30567, in review).

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3repo:cloudCard routed whole to the cloud PM: needs cloud code, a producer or a fact (cloud owns it to closure)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions