Repository navigation
feat(service-push)!: push sends through the hosted push relay, authenticated by the server's cloud binding - #30749
Conversation
…pellings move to core Claude-Session: https://claude.ai/code/session_01Utp6CZX77joBRgcruRQSMN Co-authored-by: Claude <noreply@anthropic.com>
…ng credential Claude-Session: https://claude.ai/code/session_01Utp6CZX77joBRgcruRQSMN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CZX77joBRgcruRQSMN Co-authored-by: Claude <noreply@anthropic.com>
…ud-binding contract Claude-Session: https://claude.ai/code/session_01Utp6CZX77joBRgcruRQSMN Co-authored-by: Claude <noreply@anthropic.com>
…eclined-spelling reader Claude-Session: https://claude.ai/code/session_01Utp6CZX77joBRgcruRQSMN Co-authored-by: Claude <noreply@anthropic.com>
…rict data and rate-limit retryable WIP handover: the seat released the card at 22:01Z. These pins were written, but the full gate set has not run yet; the handover note on the card lists it. Claude-Session: https://claude.ai/code/session_01Utp6CZX77joBRgcruRQSMN Co-authored-by: Claude <noreply@anthropic.com>
… the two wire narrowings as prose check:adr-0087-registration reads every FROM line of a runtime-export-retired changeset as a retired export. The package name in parentheses and the two wire-schema lines were read as operands the marker does not name. Claude-Session: https://claude.ai/code/session_016WV4cznEfnAJ8CyWBdiL2G Co-authored-by: Claude <noreply@anthropic.com>
…from source The relay tests parse with PushRelayRequestSchema and the plugin reads isControlPlaneDeclined from core; unaliased, both resolve to dist and the verdict follows build state (check:test-source-alias). Claude-Session: https://claude.ai/code/session_016WV4cznEfnAJ8CyWBdiL2G Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 43 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 14 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 182 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 d4a6cbf319aa7b2b53d0bfcec08d3851cbf5ef3a && git checkout d4a6cbf319aa7b2b53d0bfcec08d3851cbf5ef3a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11 d14d749e6b978c11f1d959ba985a21fd864d776e && git checkout -B drift-repro 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11 && git merge --no-ff d14d749e6b978c11f1d959ba985a21fd864d776e
node scripts/docs-audit/affected-docs.mjs --json 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11
|
|
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7 Contract reviewServed-tier: Inputs: card #29920 (body and all 14 comments, rulings Q1 A, Q2 A, Q3 A at 6670221824, Q4 C at 6670237128 with correction 6670407970, dev report 6673285338), PR #30749 body and file list (37 files, identical to the local net diff against merge-base 4bf9a2d), the net diff, and the check-runs on this head. Readings are against objectstack-ai/objectstack at ① Derived judgmentsAccept-set changes the diff implies, each judged:
② Semver level
③ Boundary flagsOpen question 1 — the ADR-0087 marker with the wire narrowings in prose (commit 2e35f60). Accept A. Reasons: (a) the ledger is the data source of Open question 2 — two anchored source aliases in Open question 3 — Other flags from the dev report, answered:
④ Design judgmentpackages/spec/src/api/push-relay.zod.ts — holds. Against ADR-0012 §6.1: the five content keys are refused by name at both the payload and the data layer, packages/spec/src/contracts/cloud-binding-service.ts — holds against Q1 A as corrected: three members, read on every call,
packages/spec/src/contracts/index.ts — holds: one packages/spec/src/stack.zod.ts — holds against Q2 A: the packages/spec/src/system/push-config.zod.ts — holds against Q2 A and its first consumer No must-fix found. Check-runs on Implemented-by: VERDICT: PASS |
The status line, the push row in section 4, the dead-token note in section 6 and section 6.1's served-on-main bullet said the relay was not served and anchored the retired PUSH_RELAY_NOT_SERVED_MESSAGE export. Each amended passage quotes what it replaces. The push-config test's heading comment says the same. Claude-Session: https://claude.ai/code/session_016WV4cznEfnAJ8CyWBdiL2G Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7 Contract reviewServed-tier: Delta review: one commit on top of ① Derived judgments
② Semver levelUnchanged from 6673473934: five packages at ③ Boundary flags
④ Design judgmentdocs/adr/0012-notification-platform.md (governed rules face) — holds. For each amended passage, against the tree at
The mechanical verdict on the rules face: packages/spec/src/system/push-config.test.ts — holds: a four-line heading comment, now consistent with the No must-fix found. Check-runs on Implemented-by: VERDICT: PASS |
|
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7 席位核对 · ACCEPTPR #30749 · head 维护者速读(席位校正)The body's six lines stand, read against the diff, with one correction: since 席位核对
|
|
domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7 Maintainer word · BREAKING · request for governed approvalPR #30749 · head
|
Requested by Elon · project thread
Fixes #29920
Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: none — the narrowing is on the server-to-relay wire and the relay's error answer, which no metadata author or skill writes; the operator page
content/docs/kernel/runtime-services/push-service.mdxis updated heredomain:server#5 card thread · session
session_016WV4cznEfnAJ8CyWBdiL2G· branchclaude/project-thread-yvu7tv(the card thread's platform-assigned branch, named in claim amendment 6671581752). Resumes os-bill's released WIP (Release 6670797151,claude/project-thread-tk5wos@634f4145); that work is carried here commit for commit, with oneorigin/mainmerge and two gate fixes on top.维护者速读
cloud.objectos.ai/api/v1/cloud/push/send(或OS_CLOUD_URL、或配置里的endpoint),标题和正文不出服务器;原来这种配置在启动时直接被拒。push: {}本来就是规格里的默认(中继),但发送端一直没做,推送等于不能用;按裁决 Q1 A / Q2 A / Q3 A / Q4 C 实现。data改为严格(多余键被拒而不是被丢),限流错误必须retryable: true,导出PUSH_RELAY_NOT_SERVED_MESSAGE改名为PUSH_RELAY_AIR_GAPPED_MESSAGE;未绑定的服务器照常启动、推送暂停(走站内信),声明无控制面(OS_CLOUD_URL=off)的部署配中继会在启动时拒绝;direct 直连通道不变。回滚即还原本 PR。packages/services/service-push/src/transports/relay.ts(发送、401/429/invalid 的映射);packages/cloud-connection/src/cloud-binding.ts+packages/spec/src/contracts/cloud-binding-service.ts(新的cloud-binding服务与接口);packages/spec/src/api/push-relay.zod.ts(严格data、retryable)。pnpm --filter @objectstack/service-push exec vitest run src/transports/relay.test.ts src/push-plugin.test.ts,预期 2 个文件 32 例全绿,其中未绑定时vendors()为空、401 不重试、OS_CLOUD_URL=off时启动被拒。What changes
@objectstack/service-pushsends through the hosted relay (transports/relay.ts,RelayPushSender+relayTransport). OnePushRelayRequestSchemaPOST per push, one delivery per device token, parsed before it leaves the server. The endpoint is the relay block'sendpoint, else the control-plane base URL plusPUSH_RELAY_PATH(/api/v1/cloud/push/send, the constant lives here and not inpackages/spec, ruling Q3 A, comment 6670221824). The bearer is read from thecloud-bindingservice on every send and goes into theAuthorizationheader only.okis sent;invalidanswersinvalid_token, so the channel retires the device row as for the direct transports;retryis a retryable failure; 401 fails non-retryable, naming the lapsed binding; 429 follows the relay'sretryable(RATE_LIMIT_EXCEEDEDretryable,QUOTA_EXCEEDEDnot); 5xx retryable; any other status not.PushServicePlugin.initandresolvePushCapabilityArgrefuse a relay configuration only when the deployment declared it has no control plane (isControlPlaneDeclined()), withPUSH_RELAY_AIR_GAPPED_MESSAGEnaming the in-app inbox and the direct transport. Anywhere else the relay boots, bound or not. While unbound the relay transport is unavailable (IPushTransport.available()),vendors()lists nothing, the push channel reportstransport_not_configuredthrough its existingisAvailablereading, and the plugin warns once atkernel:ready. A bind done while the server runs takes effect without a restart.@objectstack/cloud-connectionregisterscloud-bindingininit()(ruling Q1 A):ICloudBindingService(credential(),bound(),controlPlaneUrl()), new inpackages/spec/src/contracts/cloud-binding-service.ts, additive. Its ownkernel:readycontrol-plane routes now read the credential through the same instance, so the credential rule keeps one owner.@objectstack/coregainsCLOUD_DECLINED_SPELLINGS,isControlPlaneDeclinedSpellingandisControlPlaneDeclinedbesideresolveControlPlaneApiKey; cloud-connection reads them from there and re-exportsisControlPlaneDeclinedunchanged. Moved so service-push can read the air-gap declaration without depending on cloud-connection (which would close a cycle).service-messaging/src/push-channel.ts) picks the generic line byresolveRecipientLocale(sys_user.locale, then the deployment default, then English) and passes it asPushMessage.notice;messaging-service-plugin.tshands the push channelgetData/getDefaultTemplateLocaleas it does for sms and email (one line).push-relay.zod.ts,push-config.zod.ts,stack.zod.ts): the not-served markers flip to served;PushRelayPayloadSchema.datais judged strictly (a content key is refused with the ID-only guidance instead of stripped);RATE_LIMIT_EXCEEDEDcarriesretryable: true(defaulttrue); the docblock sentence carried in by comment 6606506582 and thetextdescribe ("in the recipient's language") are corrected. Regenerated: reference docs, api-surface, export-origins, the strictness ledger counts..changeset/29920-push-relay-cloud-binding.md: five packages atpatch(Metadata-protocol: none),feat(service-push)!, BREAKING section with the FROM/TO for the retired export, ADR-0087not-required (runtime-export-retired …).Why
PushServiceConfigSchemareads{}as the hosted relay, and until now the boot refused it, so a stack that requiredpushwith the default configuration could not push at all. The card's ask, as amended by the maintainer's rulings on it (Q1 A, Q2 A, Q3 A, Q4 C; summary 6670237128, correction 6670407970).Tests
service-push/src/transports/relay.test.ts(new, 13): POST shape and bearer, explicit endpoint wins, English default line, credential read per send,invalid/retry/ 401 / 429 / 5xx mapping, unbound and no-address failures, content key refused before sending, vendors listed only while bound.service-push/src/push-plugin.test.ts: relay boots for no config,{},transport: 'relay'and a staging endpoint; bound sends one POST and no log line carries the credential; unbound warns once atkernel:readyand the bind takes effect without a restart;OS_CLOUD_URL=offrefuses each relay form and still boots a direct block.capability-arg.test.tsfollows the same split.cloud-connection/src/cloud-binding.test.ts(new): the credential rule (service key over the boundoscc_bearer,''unbound), per-call reads,controlPlaneUrl()and the declined spellings, the service registered ininit().core/src/cloud-managed.test.ts: the declined-spelling reader.service-messaging/src/push-channel.test.ts: zh-CN line for a zh-CN recipient; deployment default then English; a failed locale read never costs the push.spec/src/api/push-relay.test.ts,system/push-config.test.ts,stack-email-sms.test.ts: served markers, strictdata,retryablepinned on the rate-limit answer.cli/src/commands/serve-stack-email-sms-door.contract.test.tsandverify/src/harness.served-composition.test.ts: the served composition boots the relay instead of refusing it.Acceptance notes
packages/spec/src/contracts/cloud-binding-service.ts,packages/spec/src/api/push-relay.zod.ts,packages/spec/src/system/push-config.zod.ts,packages/spec/src/stack.zod.ts,packages/core/src/stack-auth.ts, theservice-pushindex exports,packages/cloud-connection/src/cloud-binding.tsand the changeset. Queueing needs the maintainer's per-PR word.check:adr-0087-registrationpredicate 3:runtime-export-retiredreads every FROM line as a retired export, so the package name in parentheses and the two wire lines (data,RATE_LIMIT_EXCEEDED/retryable) were read as unnamed operands. The FROM/TO now names onlyPUSH_RELAY_NOT_SERVED_MESSAGE; the two wire narrowings are stated as plain instructions. No category covers a*.zod.tswire narrowing other thanregistered, and registering one would mean apackages/spec/src/migrations/entry outside this card's surface; the marker's reason says why the ledger has nothing to rewrite (no author writes the wire, nothing stores it). The reviewer should confirm that reading.check:test-source-aliaswas red: the new relay tests import@objectstack/spec/apiand the plugin reads@objectstack/core, both resolving todist/. Two anchored aliases inservice-push/vitest.config.ts.CLOUD_BINDING_SERVICE('cloud-binding') is spelled once in cloud-connection and once in service-push, because service-push does not depend on cloud-connection; the interface lives in spec contracts. Noted, not filed.find(…) ?? results[0]); with one delivery per request the two coincide. Noted, not filed.content/docs/kernel/runtime-services/push-service.mdx(hand-written operator page) is updated for the served relay; it sits outside the claim's listed file surface, carried from the released branch. ADR-0012 §6.1's "device locale" wording stays for ADR-0012 §6.1: the hosted push relay picks the generic notice line in the recipient's language, not "the device locale" (follow-up to #29920 Q4 C) #30670.OS_CLOUD_URLunset but the cloud-connection plugin's owncontrolPlaneUrloption set tooffboots the relay (the init check reads the environment) and every send then failsrelay_address_not_published. Noted, not filed.Verification
Readings against objectstack-ai/objectstack. Code head
2d054bbe(theorigin/mainmerge at4bf9a2d7) for the package suites, ablations and dogfood; the two later commits change only the changeset text (2e35f600) and the service-push vitest alias (1b82707d), and the service-push suite and the derived gates were re-run on1b82707d.Package suites and typecheck (os-verify-lock, after the dependency-closure build)
pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/commands/serve-stack-email-sms-door.contract.test.ts: 1 file, 10 passed.pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2 src/harness.served-composition.test.ts: 1 file, 10 passed.pnpm --filter @objectstack/spec test(full suite): 817 files, 25796 passed, 1 todo.pnpm --filter @objectstack/service-push test: 5 files, 74 passed (re-run on1b82707dwith the source aliases: 74 passed).pnpm --filter @objectstack/service-messaging test: 66 files, 849 passed.pnpm --filter @objectstack/cloud-connection test: 71 files, 896 passed.pnpm --filter @objectstack/core test: 136 files, 3087 passed.typecheckexit 0 for spec, core, cloud-connection, service-push (also on1b82707d), service-messaging, cli, verify.pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date.node scripts/check-platform-checklist.mjs --changed-from origin/main: OK, 11 anchors judged, 0 lost.pnpm lint(eslint . --no-inline-config): exit 0 at2e35f600; the one file1b82707dadds lines to,service-push/vitest.config.ts, re-linted: exit 0.Ablations (scripts/ablation-replace.mjs; each restored to blob == HEAD, git diff HEAD empty)
Seven legs at
2d054bbe; each red where expected, then restored (blob == HEAD).retryable: true: 1 failed / 12 passed (relay.test, the 401 case).invalidno longer maps toinvalid_token: 1 failed / 12 passed (the dead-token case).if (!credential)made false): 2 failed / 11 passed (unbound send; bind-at-runtime).PushServicePlugin.init: 4 failed / 15 passed (each relay form underOS_CLOUD_URL=off).vendors()ignoresavailable(): 6 failed / 26 passed (push-plugin + relay.test, every unbound case).PushRelayPayloadSchema.databack to the strippingPushDeliveryDataSchema: 1 failed / 39 passed (spec push-relay.test, the strict-data case).git status --porcelainempty).Full dogfood (BREAKING)
pnpm --filter @objectstack/dogfood testunderos-verify-lockat2d054bbe(noOS_TEST_SHARD, no path), after the dogfood dependency-closure build: Test Files 421 passed, 3 skipped (424); Tests 3513 passed, 36 skipped (3549); VERDICT command-exit 0. No failures.Derived gates (dispatch-gates --commands)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 129 families derived; the list is identical at2d054bbeand1b82707d.2d054bbe: two real reds, both fixed in this PR —check-adr-0087-registration(the changeset, see Acceptance notes) andcheck:test-source-alias(service-push aliases). Three exited 3 (prerequisite):check-plugin-teardown-shape --self-test(pinned fixture commit fetched),check:skill-examplesandcheck:dual-build-cjs-loads(whole-treepnpm build, 118 tasks green).1b82707d, after the whole-tree build: all 129 exit 0.--ranreconciliation at1b82707d: 129 derived, 129 run, 0 NOT-MEASURED, 0 UNRUN.check:commit-card-trailerspassed on each push.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code