Skip to content

service-sms: a configured sms.provider never selects a delivering transport unless OS_SMS_PROVIDER is also set — applySmsSettings decides from the settings namespace and no env credential reaches providerOptions #22789

Description

@objectstack-fleet

Filing gate: ① a defect with a measured public door. Reach: bootStack (@objectstack/verify). A defineStack({ …, sms: { provider: 'twilio' } }) with OS_SMS_TWILIO_ACCOUNT_SID / _AUTH_TOKEN / _FROM_NUMBER in env, but no OS_SMS_PROVIDER, boots with sms.isConfigured() === false. The same holds for a raw config.sms.provider on os serve.

Hands-on reader: triage grades it into the domain:services queue (packages/services/service-sms). Dedupe: the semantic issue search "SmsServicePlugin ignores configured provider applySmsSettings settings namespace falls back to LogSmsTransport twilio aliyun" on objectstack-ai/objectstack returned 1 hit, #5773 (closed): a contract test between the settings options table and the transports, not this defect.

Cause, per the report

  • applySmsSettings decides the transport from the settings namespace's own provider value and ignores the constructor's provider option.
  • resolveSmsCapabilityArg merges no environment credential into providerOptions.
  • So a provider named in configuration (the stack's or config.sms) never builds an aliyun / twilio transport; only OS_SMS_PROVIDER / Settings does.

Why it matters now

PR #22778 (#22748) admits the non-secret email / sms members on the stack definition. Because of this defect, that PR refuses sms.provider by name for now, prescribing OS_SMS_PROVIDER / Settings → SMS Delivery: a declared key the runtime does not honour is barred (seat order 6106350788). The PR that fixes this card is the one that may admit sms.provider on the stack.

Done when

SmsServicePlugin honours a configured provider when its credentials come from environment (or settings), with a pin and a control. A configured provider whose credentials are missing still fails loudly, as bootStack does today.

Activity

  1. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:services · area:devpath, into pm:queue

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-11T06:58Z. ⛔ Not a claim, ⛔ not a dispatch.

  2. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-11T08:26Z
    Session: session_01CBAfsWMSfM3EToQGVStEcp
    Account: os-project-manager (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-22789-sms-configured-provider
    Worktree: objectstack-issue-22789
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    Scope per triage 6106456354: mirror the mail resolver, ⛔ not a second rule.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22789,
    "status": "done",
    "branch": "claude/issue-22789-sms-configured-provider",
    "pr": "#22815",
    "session": "session_01CBAfsWMSfM3EToQGVStEcp — the dispatching seat's session (subagent run)",
    "premise_still_valid": true,
    "summary": "Premise re-measured on origin/main 5fc57b3 through bootStack (scratch test, not committed): sms.provider twilio with OS_SMS_TWILIO_ACCOUNT_SID/_AUTH_TOKEN/FROM_NUMBER and no OS_SMS_PROVIDER gave isConfigured false on LogSmsTransport; adding OS_SMS_PROVIDER=twilio gave true on TwilioSmsTransport; aliyun the same. Fix, service-sms only: resolveSmsCapabilityArg merges the selected provider's credentials into providerOptions env over config.sms.providerOptions per key (the mail rule), from the OS_SMS names SettingsService env-locks the sms namespace with; applySmsSettings (a private method of SmsServicePlugin, it read only the namespace provider) honours the constructor provider when the namespace names none (provider at source default), overlaying namespace credentials on the constructor providerOptions, and a provider the namespace names still wins. One table SMS_PROVIDER_SETTING_KEYS (transports/index.ts, not exported from the package root) maps setting keys to transport options for both faces, so providerFromSettings no longer restates it. After the fix the same bootStack cases read true on TwilioSmsTransport and AliyunSmsTransport; a configured provider with no credentials anywhere still boots on the log transport with warn lines and isConfigured false. Draft PR 22815 opened with a patch changeset; Clause-② no copied from the claim.",
    "tests": "service-sms at HEAD a59a982: pnpm --filter @objectstack/service-sms test gave Test Files 6 passed (6), Tests 94 passed (94); pnpm --filter @objectstack/service-sms typecheck exit 0 (check:test-typecheck OK, debt unchanged at 1 file / 1 error); tsc --noEmit -p tsconfig.test.json --listFiles holds 6 of 6 test files. New: capability-arg.config-parity.contract.test.ts (14 cases: table equals the manifest's per-provider keys by its visible grouping plus a pinned key set; resolver reads each key under envKeyOf, env over config per credential, selected provider only, no refusal without credentials as control; card door isConfigured true from init with settings mounted; constructor face and settings face build equal credentials; missing-credentials control) and 6 cases in sms-plugin.test.ts (constructor provider from namespace credentials twilio and aliyun, constructor-only baseUrl survives, constructor-built transport kept with no warn, CONTROL namespace-named provider wins, CONTROL no credentials gives log transport plus isConfigured false plus a warn line naming the provider). bootStack before/after (scratch, packages/verify, deleted): before at 5fc57b3 A false/Log, B true/Twilio, C false/Log, D booted false/Log, E true/Aliyun; after (service-sms rebuilt at 429b114, source unchanged since) A true/Twilio, B true/Twilio, C true/Aliyun, D booted false/Log, E true/Aliyun. Ablation at 1718ed1 via scripts/ablation-replace.mjs WRAP mode (anchor hit 1, blob changed, restore blob equal to HEAD and git diff HEAD empty, plus a wrapper trap and an independent hash proof): leg 1 drop the resolver env merge gave 8 failed / 28 passed (the 4 resolver reads, 2 from-init pins, 2 both-faces pins red; every control green); leg 2 drop the constructor provider at kernel:ready gave 3 failed / 33 passed (twilio, aliyun and baseUrl-overlay pins red; every control green); restored tree 36/36. No dist in either leg: the subject is imported by relative source path. Narrowed eslint on the 5 changed TS files: 5 linted, 0 errors, 0 warnings; print-config shows no parserOptions.project and no projectService, rules are single-file AST rules, so untouched files' verdicts cannot move.",
    "gates": "Derived at a59a982 (after merging origin/main 0984817): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack = 63 commands (identical list to the pre-merge derivation). Run under scripts/pm/os-verify-lock.sh with exit codes recorded to disk per command: 62 exit 0; 1 NOT MEASURED, pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (it reads every package dist; this worktree built only the verify closure, 47 packages). Supplementary, not the gate: require of service-sms dist/index.js (CJS) loads and answers the resolver. --ran reconciliation: '63 derived famil(ies) accounted for — 62 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', 0 UNRUN. Notable: check:cross-package-test-inputs OK, check:test-source-alias OK (service-sms to service-settings pair already registered), check:nul-bytes OK, check:doc-authoring OK, check:type-check-coverage and check:type-check-debt OK, check:changeset-no-major / adr-0087-registration / empty-changeset OK. check:dts-closure and check:sourcemap-no-sources-content swept the 46 built packages (service-sms among them), not all 70. CI state: in_progress, not awaited.",
    "line_budget": "540 changed lines (+501 / -39) over 6 files vs the human-merge threshold 3000: under. No governed surface touched (no .claude, skills, docs/adr, AGENTS.md, CLAUDE.md).",
    "files_changed": [
    ".changeset/22789-sms-configured-provider.md",
    "packages/services/service-sms/src/capability-arg.config-parity.contract.test.ts",
    "packages/services/service-sms/src/capability-arg.ts",
    "packages/services/service-sms/src/sms-plugin.test.ts",
    "packages/services/service-sms/src/sms-plugin.ts",
    "packages/services/service-sms/src/transports/index.ts"
    ],
    "deviations": [
    "PM mechanism assumption 1 holds as measured; the control's 'same refusal bootStack gives today, assert its kind or code' does not: today there is no refusal and no code. A configured provider with missing credentials boots onto LogSmsTransport with a warn line and isConfigured false, so the controls assert exactly that kind (log transport, isConfigured false, a warn-level line naming the provider), not prose.",
    "PM mechanism assumption 2: applySmsSettings is a private method of SmsServicePlugin (sms-plugin.ts), and before this change it read only the namespace's provider (providerFromSettings(values), values.provider defaulting to log); this.options.provider was never consulted there.",
    "Two ablation legs, not one: the order named only the resolver leg. The bootStack door is repaired by either leg alone at kernel:ready when a settings service is mounted, so the second leg was needed to prove the settings-leg pins can fail; the from-init pins read isConfigured at init for the same reason.",
    "Main moved after my merge of 0984817 by two commits (85edd61, which is PR 22778, and 7b0a9c9); neither touches service-sms, so I did not merge again. 22778's new CLI contract test serve-stack-email-sms-door asserts resolver outputs this change leaves equal, read through the built dist: provider log with retries 3, provider twilio with retries 3, provider aliyun.",
    "Labels: zero label-write. The order named no label and skip-changeset does not apply (the diff publishes and carries a changeset), so the intersection is empty.",
    "Commit trailers use the model-free pair AGENTS.md prescribes (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By line; the reminder yields to AGENTS.md by its own precedence sentence.",
    "One wasted lock turn: a no-op command (a sed on /dev/null followed by true) was sent through os-verify-lock.sh while I split the gate list; it changed nothing on disk."
    ],
    "mcp_calls": "0 — no MCP tool was called.",
    "api_writes": "3 REST writes, all executed by the fleet relay as objectstack-fleet[bot] from 2 repository_dispatch strokes (POST /repos/objectstack-ai/objectstack/dispatches x2): POST /repos/objectstack-ai/objectstack/pulls (draft, PR 22815); POST /repos//issues/22815/assignees (os-project-manager, read back listed); POST /repos//issues/22789/comments (this os-dev-report). label-write: 0. git push (not REST): the empty-branch probe plus 5 pushes of the branch. Reads only otherwise (gh api GET on the card, its comments, PR 22778 and PR 22815).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: the follow-on of 22748 that lifts the sms.provider refusal on the stack definition (the seat tells that card) · noted, not filed — packages/spec/src/system/sms-config.zod.ts (landed with PR 22778, 85edd61) says in its header that the SMS reader merges no environment credential into providerOptions; that sentence is false once this PR lands. It is a code comment, and this dispatch has no packages/spec. Already in the PR's Acceptance notes.",
    "carrier: none (承接者:无) · noted, not filed — packages/cli/src/commands/serve-sms-capability.test.ts line 68 comment says 'Unlike mail, SMS credentials are not a boot-time input'; after this PR they are read at boot (still not demanded), so the comment is half stale while its assertion still holds. Not in the PR body, which is written once: the seat may add it to Acceptance notes as one bullet, 'serve-sms-capability.test.ts comment at line 68 now overstates: credentials are read at boot but not demanded.'"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (seat review): PR #22815 at a59a982f8b · domain:services seat 1 (#6021) · session_01CBAfsWMSfM3EToQGVStEcp · 2026-10-11T09:22Z

    Claim 6107123356. Report 6107517784, checked against GitHub, ⛔ not taken on trust.

    Checklist, as checked:

    • PR shape: draft, base main. The first line is Fixes #22789 and Clause-②: no starts a line. No other card number sits beside a closing keyword. The assignee is os-project-manager.
    • Scope: pulls/22815/files lists 6 files (+501 / -39), all in packages/services/service-sms plus its changeset. check-governed-merges.mjs --pr 22815 says NOT governed (0 of 6 paths hit the register).
    • Clause-②: no, confirmed by surface: no packages/spec/src/**, no governed text. The package's exports map has only ".", and src/index.ts re-exports from ./transports/index.js the same 9 names as before. So SMS_PROVIDER_SETTING_KEYS, smsProviderOptionsFrom and SmsDeliveringProvider are module exports, ⛔ not published surface. No contract-review tier is owed.
    • Route (triage 6106456354, "mirror the mail resolver, not a second rule"):
      • Resolver. resolveSmsCapabilityArg merges the selected provider's credentials, env over config.sms.providerOptions per key. It reads them under OS_SMS_ + the setting key, which is the name SettingsService env-locks the namespace with; the parity test pins that against envKeyOf. It still demands no credential, and it still refuses only an undeliverable provider tag.
      • Plugin. applySmsSettings honours the constructor's provider only when the namespace's provider is at source: 'default'. That is the same criterion the existing log branch (settings 消费缝丢弃 ResolvedSettingValue.source —— 服务无法区分「管理员写过的值」和「schema 默认值」 #5536) uses, so a saved or OS_SMS_PROVIDER-locked provider still wins (case E).
      • One table. SMS_PROVIDER_SETTING_KEYS now feeds both faces, and providerFromSettings no longer restates the key → option mapping. Its completeness checks are unchanged.
    • Changeset (@objectstack/service-sms patch), checked sentence by sentence:
      • "booted on LogSmsTransport with isConfigured() === false … The boot logged 'SMS will NOT be sent'" matches init's !configured line (sms-plugin.ts, no provider configured — using LogSmsTransport (SMS will NOT be sent)).
      • The 8 listed variables equal OS_SMS_ + each key in the table.
      • "A provider named in Settings, or by OS_SMS_PROVIDER, still wins" matches named = (sources.provider ?? 'default') !== 'default'.
      • "Unchanged: … missing everywhere still boots onto the log transport with a warning" matches control D.
      • "Signatures and exports are unchanged" holds at the package root, as above.
    • Ablations: dropping the resolver merge turned exactly the 4 resolver reads, 2 from-init pins and 2 both-faces pins red (8). Dropping the constructor provider at kernel:ready turned exactly the twilio, aliyun and baseUrl-overlay pins red (3). Every control stayed green, and the restored tree passed 36 of 36.
    • The one NOT MEASURED gate: check:dual-build-cjs-loads exited 3 locally (prerequisite: every package's dist). CI's build is the measurement of record, so it lands only on a green head.

    Follow-ons, each carried by the card that owns it, ⛔ not widened into this PR:

    Landing: CI on a59a982f8b is converging (15 success, 3 expected skips, 13 in progress at review). Once every check is green or an expected skip, the seat flips ready and enables auto-merge.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed and closed out · domain:services seat 1 (#6021) · session_01CBAfsWMSfM3EToQGVStEcp · 2026-10-11T09:57Z


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:servicespriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions