Skip to content

spec: push relay transport — transport: relay | direct on PushServiceConfigSchema, and the server→relay wire shape (ruling cloud#3519) #29816

Closed

Description

@hotlong

Blocked-by: objectstack-ai/cloud#3519

Context

objectstack-ai/cloud#3519 was ruled 1C 2A 3A 4B 5A 6B 7B on 2026-11-03. The store-published ObjectOS app's APNs and FCM credentials stay with us. Every server, private ones included, pushes through a hosted relay that lives in objectstack-ai/cloud, authenticated by its ADR-0008 binding credential (oscc_). The relay only ever sees an opaque payload.

Today packages/spec/src/system/push-config.zod.ts knows only direct mode:

  • PushServiceConfigSchema = { apns?, fcm? }, each with a credentialRef, all marked [PLANNED — not yet read];
  • its docblock calls a hosted surface out of scope.

Ask

  1. transport discriminated union on PushServiceConfigSchema, built with strictObject:
    • { transport: 'relay', endpoint?: URL }. This is the default. endpoint defaults to the production relay and exists only for staging. The block carries no credential, because the binding is the credential.
    • { transport: 'direct', apns?, fcm? }. This is the current shape, kept for white-label builds.
    • Mixed shapes are refused when authored: credentials inside a relay block, or direct without any credentialRef.
    • Keep [PLANNED — not yet read] until Push channel: build the device registry routes (/notifications/devices) and the APNs + FCM transport that ADR-0012 marks "MUST ship M1" #29041's reader lands (ADR-0049, second state).
    • Rewrite the docblock: the hosted relay is the default, and direct is for white-label apps.
  2. New packages/spec/src/api/push-relay.zod.ts. One Zod schema that the sender (Push channel: build the device registry routes (/notifications/devices) and the APNs + FCM transport that ADR-0012 marks "MUST ship M1" #29041) and the receiver (the cloud relay) both pin.
    • Request: { deliveries: [{ token, vendor, payload, priority }] }. payload is an opaque object holding PushDeliveryDataSchema (ID-only, per ruling 2A) plus the generic visible text. There are no title or body fields.
    • Response: one result per token, ok | invalid | retry.
    • Error envelope: 401 (bad or revoked binding) and 429 (quota or rate limit, with reason and next step).
  3. Unchanged this phase.
    • PushVendorSchema stays apns | fcm; it widens in the same PR as the China transports.
    • RegisterDeviceRequestSchema gets no pushKey; that waits for the E2EE phase.
  4. Declarations. Changeset level minor, Clause-②: yes (widening), Metadata-protocol: added. Regenerate the generated artifacts, as feat(spec): push channel protocol face — device registration widening, stored device shape, device contract members, push route, APNs/FCM config by credentialRef #29218 did.

Acceptance

Activity

  1. added
    priority:p1High: required for production / M2
    domain:specDomain lane — seat card indexed by label:pm:seat
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Nov 3, 2026
  2. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

    Claim: PM loop round 3
    Session: session_01WGufzhJG3tKnksHVa1krBz
    Account: os-support-ai
    Branch: claude/project-thread-g5fdkn (card thread, executor session_01WGufzjEE2HfFt4gfFWntQG; the thread can push only its own branch, the measured platform exception to claude/issue-29816-…)
    Worktree: objectstack-issue-29816
    Domain: domain:spec
    Seat: domain:spec#2
    File surface: packages/spec/src/system/push-config.zod.ts + test (the transport union, the mixed-shape refusals, the docblock), new packages/spec/src/api/push-relay.zod.ts + test, the module indexes, regenerated artifacts, .changeset/29816-*.md
    Container & model: M, mode:thread, model: TIER_DEFAULT, effort: xhigh (p1); contract review at CONTRACT_REVIEW_TIER in seat before landing
    Clause-②: yes (widening)
    Metadata-protocol: added
    Responsibility: PushServiceConfigSchema declares direct transport only | none — the relay ruling has no spec shape to pin | the push sender (#29041) and the cloud relay
    Thread-read: none
    Premise: holds on the card's account of objectstack-ai/cloud#3519's ruling (this session cannot read that repository); push-config.zod.ts on main is { apns?, fcm? } under [PLANNED — not yet read]. The executor measures whether the union refuses any shape that parses on main and stops on a narrowing arm instead of shipping it under widening.
    Serial constraints cleared: no open PR edits push-config.zod.ts; #29817 (ADR-0012 amendment) is Blocked-by this card and is not dispatched with it.

  3. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p1 kept · domain:spec kept · pm:queue · pm:blocking · 2026-11-03T06:53Z

    ⛔ Not a claim.


    Generated by Claude Code

  4. added
    enhancementNew feature or request
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    pm:queueReady for the PM dispatch loop
    on Nov 3, 2026
  5. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    domain:spec#2 · os-support-ai · session_01WGufzjEE2HfFt4gfFWntQG
    os-dev-report
    Stamp: 2026-11-03T09:37Z

    {
      "issue": 29816,
      "status": "done",
      "branch": "claude/project-thread-g5fdkn",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29886",
      "session": "session_01WGufzjEE2HfFt4gfFWntQG",
      "premise_still_valid": true,
      "summary": "PushServiceConfigSchema is now z.discriminatedUnion('transport', [PushRelayTransportConfigSchema, PushDirectTransportConfigSchema], { unionFallback: true }), both strictObject. Relay is the default and carries no credential; endpoint is optional and https only. Direct is today's apns/fcm shape and needs at least one vendor block (projectable requiredOneOf). Both members default transport, so a block without it keeps parsing: {} reads as relay, and a block with a vendor block reads as direct. A credential or vendor block in a relay block, a direct block with no vendor, and endpoint on direct are refused by name. New api/push-relay.zod.ts: the request (deliveries of token, vendor, ID-only payload {data: PushDeliveryData, text}, priority high|normal), the response (one {token, status: ok|invalid|retry} per delivery, in request order), and PushRelayErrorResponseSchema. That error schema reuses ErrorResponseSchema and EnhancedApiErrorSchema with existing codes: UNAUTHENTICATED 401 with details.reason binding_invalid|binding_revoked, RATE_LIMIT_EXCEEDED 429 with retryAfterSeconds, and QUOTA_EXCEEDED 429; every member requires helpText as the next step. Every PLANNED marker kept or added. Pin count 822 -> 828. The order's premise 'no other open PR edits push-config.zod.ts' was false: #29794 (#29041) does. The seat ruled this card lands first, and the PR's Acceptance notes say so.",
      "tests": "At head 88fe2df6 (merge of origin/main 4b71a82b), all under os-verify-lock. pnpm --filter @objectstack/spec test && typecheck: Test Files 808 passed (808), Tests 24107 passed and 1 todo, VERDICT command-exit 0. At 726a5aa4: test:repo Test Files 89 passed (89), Tests 1522 passed, VERDICT 0; full turbo build Tasks 118 successful of 118, VERDICT 0. check:generated 14/14 up to date after --fix regenerated api-surface/system.json and export-origins/system.json post-merge. eslint --no-inline-config on the 6 touched files: exit 0. Ablation via ablation-replace.mjs on push-config.zod.ts (src import, no dist leg), restored each time with blob 6f536835 equal to HEAD and git diff HEAD empty. Leg A dropped the requiredOneOf refine (marker 0 to 1): 2 failed of 37. Leg B dropped the relay apns/fcm guidance rows (marker 0 to 1): 1 failed of 37. A first leg-A attempt with replacement ')' was refused by the tool (count -2) and was a no-op.",
      "gates": "dispatch-gates --commands at 88fe2df6: 102 derived, the same list as at 726a5aa4. All 102 exit 0 (serial, stdin closed, exit captured before any pipe); --ran: 102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN. The first pass at 06321962 had check:llms-txt and check:variant-docs red, both fixed in 726a5aa4. Seven exit-3 prerequisites cleared by the full build plus a fetch of the teardown fixture commit. check-changeset-no-major --event with the PR body: exit 0, reading yes (widening) and added. The control with 'none' exits 1 (none-baseline-moved). PR CI check-runs: in_progress at report time.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "5 — mcp__github__issue_read x2 (29816 get, get_comments), mcp__github__pull_request_read x3 (29218 get_files, 29794 get_files, 29794 get); no write tool",
      "api_writes": "2 — fleet-write relay pr_create (objectstack-ai/objectstack#29886, 16619 bytes sent and stored, identical, no assignees) and this os-dev-report comment via post-stamped.mjs; git push is not a REST write",
      "open_questions": [
        { "question": "One JS-only value main accepts is now refused: vendor members present as undefined with no vendor block, e.g. { apns: undefined }. Every JSON document still parses. Keep Clause-②: yes (widening)?", "options": ["A keep widening and name it in Acceptance notes (done)", "B declare apns/fcm as undefined-only keys on the relay block to keep it parsing"], "recommendation": "A, because B would put phantom keys on the relay's authorable surface and published JSON Schema, for a value that delivers to nothing and that no reader parses." },
        { "question": "{} changes declared meaning from 'no vendor delivers' to 'the relay' (the card's default). Is Metadata-protocol: added enough, or is it semantic?", "options": ["A added (declared; nothing reads the shape, and the gate reads added)", "B semantic"], "recommendation": "A, because no reader exists, so no deployment's behaviour moves." }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: cloud · which credential a cloud-hosted server (OS_CLOUD_API_KEY, no oscc_ bearer) presents to the relay is the relay's to state; this PR says only that the binding credential is the bearer · noted, not filed",
        "carrier: #29041 · PR #29794 reads top-level apns/fcm; it owes the transport narrowing in service-push when it merges after this card (seat ruling: serial) · noted, not filed"
      ],
      "deviations": [
        "The PR body's attribution thread link is this card thread's (harness session context), not the seat-2 thread link in the order file.",
        "The card's 'ADR-0008 binding credential' is cited as cloud ADR-0008: in this repository ADR-0008 is the metadata repository and change log.",
        "No PR assignee or reviewer and no PR activity subscription, per os-dev rule 2 and the order, over the harness's default."
      ]
    }
  6. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    pm:queueReady for the PM dispatch loop
    on Nov 3, 2026
  7. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

    Landed. PR #29886 merged through the queue (merge commit 1bb9536e); this card closed as completed by its Fixes line.

    Reading Value
    PR #29886 MERGED at 2026-11-03T10:32Z, head 88fe2df6, contract review PASS 6600358082, ACCEPT 6600717510
    Card #29816 closed (completed)

    State change: pm:dispatched removed. What shipped: PushServiceConfigSchema is a transport union with the hosted relay as the default, and api/push-relay.zod.ts carries the relay request, the per-token response and the 401 / 429 envelopes. Left elsewhere: the ADR-0012 amendment #29817 (Blocked-by this card, now unblocked); the two relay-wire should-fixes ride with the first producer, #29041 (posted there); #29794 owes the transport === 'direct' narrowing per the serial note 6598066351.

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

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-setpriority:p1High: required for production / M2protocol:system

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions