Repository navigation
feat(spec): the stack definition admits appName and the non-secret email / sms members; their credentials are refused by name - #22778
Conversation
…il / sms members Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
…umeration; changeset Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
…ack-email-sms-door
…unts Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
…ack-email-sms-door # Conflicts: # packages/spec/src/type-alias-convention.pin.test.ts
Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ac63919b0af569f8ddb2eec979c629ffd1079857 && git checkout ac63919b0af569f8ddb2eec979c629ffd1079857
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a2e94c2a053ab004b5cb9fea27824bb614e5759c 34286f646a982afcdc8ecdc962ad1afeaa93d043 && git checkout -B drift-repro a2e94c2a053ab004b5cb9fea27824bb614e5759c && git merge --no-ff 34286f646a982afcdc8ecdc962ad1afeaa93d043
node scripts/docs-audit/affected-docs.mjs --json a2e94c2a053ab004b5cb9fea27824bb614e5759c
|
The stack cannot select the delivering SMS transport: the sms settings namespace builds it from its own provider setting, so a stack-declared provider is refused with the OS_SMS_PROVIDER / Settings prescription, as providerOptions is. SmsProviderSchema leaves with its only consumer. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #22748 (body; comments ① Derived judgmentsSecurity — can a secret reach a built artifact through the stack definition? No. The three readers were enumerated at the head and every key each one reads off config was classified:
Admitted on the stack (
Honoured at runtime — every admitted key reaches a reader from a
By-name refusals — message, env var, and where they fire.
Merge semantics. Generated artifacts and docs match their sources.
Skill edit ( ② Semver level
③ Boundary flagsReport
Report
Residuals carried forward, none blocking: Check-runs on the head: 42 completed: 38 success, 4 skipped ( Implemented-by: VERDICT: PASS |
维护者速读 · PR #22778(#22748:stack 定义开放
|
Fixes #22748
Clause-②: yes (widening: the stack definition admits
appName, the non-secret members ofemail, andsmscarryingretriesalone, which it refused as unrecognized keys)The mail and SMS readers already read
config.email,config.smsandconfig.appName, but the stack definition refused all three keys, so only a configdefineStackdid not build could reach them. This PR gives the stack a door for them. It follows the triage direction (6104830037), claim6105299924and the seat's patch-round order6106350788:appNameand the non-secret members ofemailare admitted, typed from the existing schemas.smscarriesretriesalone.Status: draft. The diff touches
skills/objectstack-platform/SKILL.md, a governed Tier H surface:check:skill-top-level-keysreconciles that page's top-level key list against the schema, so it cannot stay out of this PR. Landing therefore needs an authorized approval (see the maintainer summary below). The contract review atCONTRACT_REVIEW_TIERis owed on the head named in the evidence section.Patch round 1 (seat order
6106350788)mainwas merged in (a2e94c2a05) as a merge commit, throughscripts/pm/os-regen-merge.sh.type-alias-convention.pin.test.tsconflicted on content. Both sides' entries were kept, and the merged pin was green at 777.gen:schemathengen:docsin its own commit (3a68830032).strictObjectguidance form stays as it was.sms.provideris now refused by name, with theOS_SMS_PROVIDER/ Settings → SMS Delivery prescription, exactly assms.providerOptionsis.smskeepsretriesalone, and is not refused whole.retriesis the SMS service's own attempt budget, and the service keeps it across the settings namespace's transport swap:setTransportreplaces the transport and nothing else. So it applies to whichever transport delivers. It has no environment or settings carrier, so this block is its only authoring door, and refusingsmswhole would make a live knob unreachable. Aretries-only block does not mislead, because aproviderwritten beside it is refused by name, with the right setting.SmsProviderSchemaleaves with its only consumer. It is gone, together with its isomorphic-alias pin (the pin file is back tomain's bytes) and the cli parity half. It was never released. It comes back, with the key, in the change that makesSmsServicePluginhonour a constructor provider.Reader-key census (the admitted shape is this table)
Each key the readers read off the config, not the environment, is classified. No reader needs a secret from the config file: every secret has a carrier outside it. So the triage stop rule did not fire.
resolveEmailCapabilityArgproviderOS_EMAIL_PROVIDERapiKeyOS_EMAIL_API_KEYdefaultFromOS_EMAIL_FROMretriesOS_EMAIL_RETRIESqueueDeliveryOS_EMAIL_QUEUE_ENABLEDpersistOS_EMAIL_PERSIST_ENABLEDappNameOS_APP_NAMEdefaultTemplateContextOS_APP_NAMEfor itsappName)options.host/.port/.secure/.userOS_EMAIL_SMTP_HOST/_PORT/_SECURE/_USERoptions.messageStreamoptions.passwordOS_EMAIL_SMTP_PASSWORDoptions.apiKeymakeTransportspreadsoptionsover the key for resend / postmark)OS_EMAIL_API_KEYresolveCapabilityArgument(core),resolveDeploymentAppNameappNameOS_APP_NAMEresolveSmsCapabilityArgretriesproviderOS_SMS_PROVIDER/ Settings → SMS DeliveryproviderOptionslogtransport requires a credential inside it (TwilioauthToken, AliyunaccessKeySecret)smssettings namespace overrides (OS_SMS_TWILIO_*,OS_SMS_ALIYUN_*)Why the SMS provider settings stop at the door
sms.providercannot select the transport that delivers. Measured throughbootStack:defineStack-builtsms: { provider: 'twilio' }, withOS_SMS_TWILIO_ACCOUNT_SID/_AUTH_TOKEN/_FROM_NUMBERset, boots withsms.isConfigured() === false. The log says "provider='twilio' selected but transport build failed … falling back to LogSmsTransport".OS_SMS_PROVIDER=twilio, logs "transport rebuilt from settings (provider=twilio)" and givesisConfigured() === true.applySmsSettingsbuilds the delivering transport from the settings namespace's ownprovider. The constructor's provider has no credential to build with, because a stack carries none.sms.providerOptionsis refused whole, whileemail.optionsis narrowed. This too was measured.OS_EMAIL_SMTP_*overconfig.email.optionskey by key, so host and user from the file compose with the password from the environment.providerOptionsto the transport as written. Non-secret members alone gaveisConfigured=falsefor Twilio and for Aliyun, with the environment credential set; the control withauthTokengavetrue.What changes
packages/spec/src/system/email-config.zod.tsStackEmailConfigSchemais derived fromEmailServiceConfigSchema: it.omit()sapiKeyandoptions, then addsoptions: StackEmailProviderOptionsSchema. A spec pin asserts the difference is exactly['apiKey'].StackEmailProviderOptionsSchemais a strict block holdinghost/port/secure/user/messageStream.resolveEmailCapabilityArg(@objectstack/plugin-email) as the reader, notserve.ts.packages/spec/src/system/sms-config.zod.ts(new).packages/spechad no SMS configuration schema, sosmsis typed from whatresolveSmsCapabilityArgreads.StackSmsConfigSchemais{ retries }, strict, and refusesprovider,providerOptionsand a flatauthToken/accessKeySecretby name.packages/spec/src/stack.zod.tsdeclaresappName,emailandsmson the stack.COMPOSE_KEY_DISPOSITIONS) all three are'single': one product name, one mail configuration and one SMS configuration per booted stack.concatkeys, they stay out of the assembled package body.strictObjectguidanceentry, the stack's own idiom for keys at the wrong layer (seat ruling on open question 1). The first sentence names the key, and the prescription bullet names the setting.Through the public door,
os validateexits 1 on a stack carryingemail.apiKeyand on one carryingsms.provider. Each refusal names its key and its setting. The admitted shape exits 0.The admitted shape,
appNameplusemail: { provider, defaultFrom, persist }plussms: { retries }, exits 0.sms.retriesreaches the transport that delivers. Measured throughbootStackwithsms: { retries: 2 }, plusOS_SMS_PROVIDER=twilioand the Twilio credentials in the environment:isConfigured() === true;service.options.retries === 2, on aTwilioSmsTransport.Pins
packages/spec/src/stack-email-sms.test.ts(16 cases):email: { provider, defaultFrom }builds throughdefineStack. So do every non-secret mail member,appNameandsms: { retries }.email.apiKey,email.options.password,email.options.apiKey,sms.provider,sms.providerOptionsand a flatsms.authTokenare each refused. The pin asserts theSTACK_SCHEMA_INVALID/ 422 envelope, the issue path and key, and the setting in the prescription.packages/cli/src/commands/serve-stack-email-sms-door.contract.test.ts(4 cases). It drives the callservemakes for each provider,resolveCapabilityArgumentwith the provider module, on a stackdefineStackreturned:emailblock reaches the email provider, withOS_EMAIL_SMTP_PASSWORDlayered over the file's host and user.appNamereaches the template context and AuthPlugin's name.sms.retriesreaches the SMS provider, beside the providerOS_SMS_PROVIDERnames.OS_EMAIL_*still wins per key.Ablation (two legs, through
scripts/ablation-replace.mjs)The spec pins import
./stack.zodfromsrc/, so nodist/sits between the mutation and the reading.Both legs ran on
34286f646a. Each was restored byablation-replace, and a trap restored fromHEADas a fallback.stack-email-sms.test.ts: 16 / 16 passedemail.apiKeyrefusal removedguidance: { apiKey: STACK_EMAIL_API_KEY_PRESCRIPTION },→guidance: {},(anchor 1 → 0, replacement 0 → 1, blobe9d625633b26→d5c0f4a5cb8b)email.apiKeypin went red: the message became "Unrecognized key(s) on the stack email block: apiKey. This block is strict from birth…", with noOS_EMAIL_API_KEYsms.providerrefusal removedprovider: STACK_SMS_PROVIDER_SETTINGS_PRESCRIPTION,→provider_ABLATED: …(anchor 1 → 0, replacement 0 → 1, blob0015c421bb5b→8ae310bea6c3)sms.providerpin went red: the message became "Unrecognized key(s) on the stack sms block: provider. This block is strict from birth…", with noOS_SMS_PROVIDERablation-replacerestoreHEADblob (e9d625633b26,0015c421bb5b), andgit diff HEADis emptyThe direction was the expected one: red. The key is still refused, but without its prescription.
Evidence
Builds and tests ran through
scripts/pm/os-verify-lock.shon head34286f646a. The closure was built first:pnpm turbo run build --filter=@objectstack/cli... --filter=@objectstack/example-showcase^... …, 68 successful.--project localover the touched and adjacent files:stack-email-sms,assembled-package-body,type-alias-convention.pin,compose-key-dispositions-export.pin,shared/alias-integrity,shared/strict-object,system/email-configpnpm --filter @objectstack/spec typecheckpnpm --filter @objectstack/cli typecheck--project unit: the rider (4 cases),serve-auth-app-name,serve-email-capability,serve-sms-capability,test/option-b-reader-acceptance.pincapability-composition, plugin-emailcapability-arg.config-parity.contract, runtimeartifact-collections, verifyharness.served-compositionpnpm --filter @objectstack/spec check:generatedos validate --jsononexamples/app-crm,app-multi-package,app-showcaseandapp-todovalid: true. The payloads minusdurationare byte-identical to round 1's pre-change reading on the base dist: no example app changedRound 1's full spec suites ran on the pre-merge tree.
--project localgave 644 files and 19228 passed, 1 todo;--project repogave 54 of 55, the one red being the concurrent-regeneration flake, 20 / 20 when re-run alone.Gates, all on head
34286f646a(git rev-parse --short HEAD, working tree clean):node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 123 commands. Every one ran with its exit code captured before any pipe, and all 123 exited 0. The changeset gatescheck-changeset-no-majorandcheck-adr-0087-registrationare among them and read the newClause-②line.node scripts/pm/dispatch-gates.mjs --ran ran.listreports "123 derived, 123 run, 0 NOT-MEASURED, 0 UNRUN".check:meta-url-spelling,check:authz-resolver,check:error-code-casing,check:filter-alias-parity,check-changeset-fixed,check:published-readme-exports,check:stack-collection-maps.--no-inline-config --format json) on the 8 touched script files reports 8 files, 0 errors, 0 warnings.eslint.config.mjsenables no type-aware linting, so this diff cannot move the verdict on any untouched file.pnpm lintand the workspace type-check lanes.The published skill: two readings, as the governed-skills rule requires
These are unchanged by this round. The page names
sms, neversms.provider.skills/objectstack-platform/SKILL.md: 489 → 488 lines, 23322 → 23301 bytes, 5831 → 5826 tokens (ceiling 5833).skills/*/SKILL.md: 4410 → 4409 lines, 210541 → 210520 bytes.ObjectStackDefinitionInput") that the section's opening sentence already makes. Nothing is re-wrapped.Deviations from the claim's file surface, each forced by a gate or a measurement
skills/objectstack-platform/SKILL.md(governed, Tier H):check:skill-top-level-keys.packages/cli/test/option-b-reader-acceptance.pin.test.tsandpackages/spec/src/assembled-package-body.test.ts: their envelope-key lists.packages/spec/llms.txt(check:llms-txt): 203 → 204 schemas; system 34 → 35.content/docs/getting-started/quick-reference.mdx(check:quick-reference-counts): System, 34 → 35 reference pages.content/docs/getting-started/quick-start.mdx: its "Not in the table, and why" list names the three keys.Acceptance notes (not filed here)
sms.providercomes back with the fix that honours it. The seat files the services-lane card from thebootStackmeasurement above. That change re-admits the key, with its provider vocabulary.@objectstack/plugin-email.transportOptions,timeoutandendpointare read by the transports but declared by no spec text, so the strictemail.optionsblock does not admit them.transportOptionscan carry credentials of its own.llms.txtsystem row named a retired schema ("Compliance"), and the quick-start list lackeddevHint/devLogins.appNamewere re-read. None states that the key cannot be set in a config file, or namesserve.tsas the reader.维护者速读(草稿)
改了什么:
objectstack.config.ts(defineStack)现在可以写appName、email(发件方、重试、是否落库、队列投递、模板上下文、SMTP 主机/端口/TLS/用户名等)和sms(仅retries重试次数)。过去这三个键会被当成"未知键"拒绝,运行时却一直在读它们。所有密钥(邮件 API Key、SMTP 密码、短信供应商凭据)以及短信供应商sms.provider都不能写进配置文件,写了会被点名拒绝,并告诉作者改用哪个环境变量,或在 Setup → 设置 → 短信投递 中配置。已发布技能skills/objectstack-platform/SKILL.md的顶层键清单补上这三个键,所以本 PR 属于 Tier H。为什么改:配置文件的这一半一直有人读,却没有入口可写。配置文件会被
objectstack build编译进制品,制品会被发布,所以密钥一律不进制品。sms.provider实测从配置文件选不到真正投递的通道(投递通道由设置页 /OS_SMS_PROVIDER决定),所以按"声明即兑现"原则先点名拒绝,待短信插件支持后再开放。风险与代价(含回滚):只放宽,不收紧,已有的合法配置不受影响(四个示例应用的
os validate前后一致)。技能文件为凑 token 余量删去了一处重复说明。回滚:revert 本 PR 即可,无数据迁移。席位意见:
你要做的:审阅并批准(Tier H,因触及
skills/**)。Generated by Claude Code