You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
service-push: send through the hosted relay transport (push-relay.zod.ts wire, cloud binding credential) #29920
#29816 made transport: relay | direct the shape of PushServiceConfigSchema, with relay as the default ({} reads as relay), and added the server→relay wire in packages/spec/src/api/push-relay.zod.ts. PR #29794 (round 3) ships service-push reading only the direct arm. relay, including {}, is refused loudly at PushServicePlugin.init and names this card. The relay block keeps its refused-not-served marker until this card lands.
Ask
Implement the relay transport in service-push:
Send. POST a PushRelayRequestSchema body (one delivery per device token: token, vendor, ID-only payload, priority) to the relay endpoint, or the default endpoint when none is set.
Auth. The bearer is the server's cloud binding credential (cloud ADR-0008, the oscc_… binding). Resolve it through the binding, never inline config, and never log it. No binding means a loud init refusal that names the missing binding.
Answers. Map PushRelayResponseSchema per token: invalid retires the device row the same way the direct transports do; retry reuses the existing retry path. 401 is a loud, non-retryable failure that names the binding.
Privacy. The payload stays ID-only (phase 1). No title or body leaves the server.
Flip the refusal. Remove the init refusal, and turn the relay block and wire markers from not-served to served in the same PR, then regenerate.
Acceptance
Tests pin each arm: {}, an explicit relay endpoint, no binding, 401, and an invalid and a retry result.
Direct-transport behaviour is unchanged.
The changeset names the newly served relay transport.
Filed by the seat as the follow-up that PR #29794's relay refusal names.
Claim: PM loop round 1
Session: session_01Utp6CZMkHvuDeVYgTD73qF (seat) · dev card thread session_01Utp6CZX77joBRgcruRQSMN
Account: os-bill
Branch: claude/project-thread-tk5wos (the card thread's designated push branch, the recorded platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-29920
Domain: domain:server
Seat: domain:server#4
File surface: packages/services/service-push/** (the relay transport, the init refusal) and its tests, one changeset; narrow spec exception: the relay block's and wire's served/not-served markers in packages/spec/src/system/push-config.zod.ts and packages/spec/src/api/push-relay.zod.ts, the carried-in strict data on PushRelayPayloadSchema, and what those markers regenerate. ⛔ any other packages/spec/**, docs/adr/**, .claude/**, repo-root AGENTS.md, ui/**, skills/** (stop on breach; explain in the report)
Container & model: L, mode:thread, model: default judgment tier (opus) — dispatch-gates --tier: no path-derived mandate, effort: default
Clause-②: yes (widening) — transport: relay (and {}) configs refused at init today now serve; the dev names any narrowing arm (strict data on the wire) and declares the PR BREAKING if one holds
Metadata-protocol: none
Responsibility: PushServicePlugin.init refuses transport: relay, the spec's default, so a server configured {} sends no push | none: relay and {} configs refused loudly at init since PR #29794 | any deployment using the hosted push relay through its cloud binding
Thread-read: 6606506582
Premise: holds (Blocked-by #29794 is closed; the dev re-reads the init refusal on main)
Serial constraints cleared: seat 2 yielded the card; seats 3 and 5 confirmed no overlap; no card in flight edits service-push.
Dispatch order: /mnt/project-files/server-seat4/dispatch-29920.md (project shared folder). Ruling section: the card, triage first-touch 6604349195 (p2) and the carried-in scope 6606506582 (strict data, retryable: true on RATE_LIMIT_EXCEEDED). If packages/cloud-connection exposes no way to read the binding credential, the dev stops and reports. Fixes #29920.
Dev card thread session_01Utp6CZX77joBRgcruRQSMN, branch claude/project-thread-tk5wos (the Claim's). Status: needs_decision. No code, no PR. The order's stop condition holds: packages/cloud-connection exposes no way for a service to read the binding credential. Readings taken against objectstack-ai/objectstack at c5800818 (origin/main).
What was measured
CloudConnectionPlugin registers no service. The only registerService calls in packages/cloud-connection/src are seed-datasets and seed-replayer (install-local plugin).
The effective credential rule is a closure inside CloudConnectionPlugin.start(), in its kernel:ready hook (cloud-connection-plugin.ts:343): platform-managed runtimes present OS_CLOUD_API_KEY / the controlPlaneApiKey option (through resolveControlPlaneApiKey, packages/core/src/stack-auth.ts:133); self-hosted runtimes present the oscc_ bearer kept by ConnectionCredentialStore (default .objectstack/cloud-connection.json, overridable by the plugin's credentialPath). It is read per request on purpose, so a bind that completes while the runtime is up takes effect without a restart.
The package exports the ConnectionCredentialStore class (a file reader) and no accessor for the effective credential.
@objectstack/service-push depends on @objectstack/core and @objectstack/spec only; @objectstack/cloud-connection depends on runtime, objectql, metadata-core, metadata-protocol and observability.
No other reader of the credential file or of oscc_ exists under packages/** outside cloud-connection and spec.
Decisions owed
Q1. How does service-push get the binding credential?
A. cloud-connection registers a binding-credential service (shape to be ruled, for example credential() and bound()), and service-push reads it lazily on each send. One owner for the rule (Route and surface ownership, rule 1); sees a bind done at runtime. Cost: widens the file surface to packages/cloud-connection/**; may need a cross-package interface in packages/spec/src/contracts/, outside this order's narrow spec exception. About one extra day.
B. service-push imports ConnectionCredentialStore and resolveControlPlaneApiKey and spells the rule a second time. Cost: a second copy of the rule beside its owner, the shape ruling 6341690661 forbids for isCloudManaged; pulls the cloud-connection dependency closure into service-push; misses a host's credentialPath and controlPlaneApiKey options.
C. The host injects a resolver (PushServicePluginOptions gains one), and resolvePushCapabilityArg supplies the default. Cost: the default still needs A or B.
Recommendation: A, because the rule keeps one owner and the read is lazy.
Q2. What does "no binding means a loud init refusal" do to a fresh self-hosted boot? A self-hosted server is unbound on its first boot by construction (it binds through the running Setup page), and {} reads as the relay. An init refusal therefore fails every fresh self-hosted boot whose stack declares requires: ['push']. It also records a verdict at init about state another plugin owns (AGENTS.md, Startup registry reads).
(i) Refuse at init only when a binding is impossible (for example OS_CLOUD_URL=off). Otherwise register the service, answer each send failed, non-retryable, naming the missing binding, and log it once at warn.
(ii) Refuse at init as the card says. Fresh self-hosted boots with requires: ['push'] fail until the server is bound and restarted.
Recommendation: (i), because the binding is resolved per request everywhere else, and (ii) blocks the boot that has to happen before a bind is possible.
Q3. Default relay endpoint. ADR-0012 section 6.1: the production relay address is the cloud's to publish and is never written into the spec. No constant exists anywhere in the tree. A path under resolveCloudUrl() (DEFAULT_CLOUD_URL, OS_CLOUD_URL) is a plausible home, but the path belongs to objectstack-ai/cloud#3519, which this session cannot read. This cannot be decided in this repository; it waits on the cloud side.
Q4 (minor). The generic visible line "in the device locale".PushMessage carries no locale. The device row has locale, but the channel (packages/services/service-messaging/src/push-channel.ts) does not pass it, and service-messaging is outside the order's file surface.
A. service-push ships one fixed English line.
B. The channel passes locale and service-push picks the line (a service-messaging edit).
Recommendation: B, because the wire says the line is in the device locale.
Carried-in scope (no blocker, unchanged)
Strict data on PushRelayPayloadSchema: PushDeliveryDataSchema is a plain object schema that strips unknown keys. The relay payload would use a strict variant. This narrows the wire, so the PR would be BREAKING.
retryable: true on RATE_LIMIT_EXCEEDED: a new key on the 429 schema in push-relay.zod.ts.
The docblock sentence (comment 6606506582, item 3) sits in the same file as the markers.
{
"issue": 29920,
"status": "needs_decision",
"branch": "claude/project-thread-tk5wos",
"pr": null,
"session": "session_01Utp6CZX77joBRgcruRQSMN",
"premise_still_valid": true,
"summary": "Read-only premise check; the order's stop condition holds. packages/cloud-connection registers no service and exports no accessor for the effective binding credential: the rule is a closure in CloudConnectionPlugin.start() (OS_CLOUD_API_KEY when platform-managed, else the oscc_ bearer in ConnectionCredentialStore). No code written, no worktree created, no PR. Four decisions owed: credential seam (A/B/C), the init refusal on an unbound self-hosted boot, the default relay endpoint (cloud side), the locale of the generic line.",
"tests": "none run: no code written",
"gates": "none run: no changed paths",
"dogfood_battery": "n/a — no PR",
"mcp_calls": "3 — mcp__github__issue_read (get, get_comments x2); no write tool",
"api_writes": "1 — this comment, POST issues/29920/comments through the fleet-write relay (post-stamped)",
"open_questions": [
{ "question": "Q1: how does service-push read the cloud binding credential?", "options": ["A: cloud-connection registers a binding-credential service, read lazily per send", "B: service-push re-spells the rule with ConnectionCredentialStore and resolveControlPlaneApiKey", "C: host-injected resolver, default from A or B"], "recommendation": "A, because the rule keeps one owner and a bind done at runtime is seen" },
{ "question": "Q2: unbound self-hosted boot with requires push and the relay default", "options": ["(i) refuse at init only when binding is impossible; else serve and answer each send failed, non-retryable, naming the missing binding, warn once", "(ii) refuse at init whenever unbound"], "recommendation": "(i), because (ii) blocks the boot that must happen before a bind" },
{ "question": "Q3: the default relay endpoint", "options": ["wait for objectstack-ai/cloud to publish the relay path"], "recommendation": "wait for the cloud side; not decidable in this repository" },
{ "question": "Q4: locale of the generic visible line", "options": ["A: one fixed English line in service-push", "B: the channel passes the device locale (service-messaging edit)"], "recommendation": "B, because the wire says the line is in the device locale" }
],
"security_findings": "none",
"out_of_scope_findings": []
}
Seat hold · awaiting the maintainer's ruling · 2026-11-05T12:23Z
⛔ Not a landing. The claim stands; no code is written until the ruling.
The dev's premise check (needs_decision report 6657878085) hit the dispatch order's stop condition: packages/cloud-connection exposes no service that reads the binding credential. It also found that the card's "no binding → loud init refusal" would refuse every fresh self-hosted boot, because {} reads as relay and the bind happens at runtime. And the default relay endpoint is the cloud's to publish (ADR-0012 §6.1), not in this tree.
Questions Q1–Q4 in that report go to the maintainer. The seat recommends Q1 A (a binding-credential service owned by cloud-connection), Q2 (i) (refuse at init only when binding is impossible), and Q3 waiting on the cloud side's endpoint.
The card keeps pm:dispatched and the assignee while the ruling is pending. If the ruling widens scope into packages/cloud-connection/** or packages/spec/src/contracts/, the claim is re-posted with the new file surface.
Activity
objectstack-fleet commentedon Nov 3, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p2 ·
domain:serverkept ·pm:triage→pm:blocked· 2026-11-03T12:55Z⛔ Not a claim.
service-pushsends through the hosted relay (thepush-relay.zod.tswire, already declared by spec: push relay transport —transport: relay | directon PushServiceConfigSchema, and the server→relay wire shape (ruling cloud#3519) #29816, landed), authenticated by the cloud binding credential, and maps per-token answers. A server implementation of an already-declared contract, so p2. It follows cloud#3519's ruling.Blocked-by: #29794(open): that PR shipsservice-pushwith the direct arm only.Generated by Claude Code
Merge origin/main into the push channel branch (relay | direct union, #…
feat(service-push): serve the direct push transport and refuse the re…
fix(push): name no tracker id in operator-facing push text (check:doc…
objectstack-fleet commentedon Nov 3, 2026
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS
Carried into this card's scope (2026-11-03T14:31Z), from the PR #29794 round-3 contract review (6606491253 ③ and ④):
dataonPushRelayPayloadSchema(spec seat 2's should-fix, Push channel: build the device registry routes (/notifications/devices) and the APNs + FCM transport that ADR-0012 marks "MUST ship M1" #29041 6601310426, addressed to "the first producer of the relay wire", which is this card).retryable: trueonRATE_LIMIT_EXCEEDEDin the relay's error answer (same source).push-relay.zod.tsdocblock still says "No push channel delivers yet … until the push channel ships". After feat(notifications): the push channel — service-push (APNs + FCM) and its channel shell in service-messaging #29794 the channel ships; only the relay sender does not. Fix that sentence in the same PR that flips the markers to served.All three land with the relay sender, not before.
objectstack-fleet commentedon Nov 5, 2026
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
Claim: PM loop round 1
Session:
session_01Utp6CZMkHvuDeVYgTD73qF(seat) · dev card threadsession_01Utp6CZX77joBRgcruRQSMNAccount:
os-billBranch:
claude/project-thread-tk5wos(the card thread's designated push branch, the recorded platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-29920Domain:
domain:serverSeat:
domain:server#4File surface:
packages/services/service-push/**(the relay transport, the init refusal) and its tests, one changeset; narrow spec exception: the relay block's and wire's served/not-served markers inpackages/spec/src/system/push-config.zod.tsandpackages/spec/src/api/push-relay.zod.ts, the carried-in strictdataonPushRelayPayloadSchema, and what those markers regenerate. ⛔ any otherpackages/spec/**,docs/adr/**,.claude/**, repo-rootAGENTS.md,ui/**,skills/**(stop on breach; explain in the report)Container & model:
L,mode:thread,model: default judgment tier (opus) — dispatch-gates --tier: no path-derived mandate,effort: defaultClause-②: yes (widening) —
transport: relay(and{}) configs refused at init today now serve; the dev names any narrowing arm (strictdataon the wire) and declares the PR BREAKING if one holdsMetadata-protocol: none
Responsibility:
PushServicePlugin.init refuses transport: relay, the spec's default, so a server configured {} sends no push | none: relay and {} configs refused loudly at init since PR #29794 | any deployment using the hosted push relay through its cloud bindingThread-read: 6606506582
Premise: holds (Blocked-by #29794 is closed; the dev re-reads the init refusal on
main)Serial constraints cleared: seat 2 yielded the card; seats 3 and 5 confirmed no overlap; no card in flight edits
service-push.Dispatch order:
/mnt/project-files/server-seat4/dispatch-29920.md(project shared folder). Ruling section: the card, triage first-touch 6604349195 (p2) and the carried-in scope 6606506582 (strictdata,retryable: trueonRATE_LIMIT_EXCEEDED). Ifpackages/cloud-connectionexposes no way to read the binding credential, the dev stops and reports.Fixes #29920.Posted 2026-11-05T12:19Z.
objectstack-fleet commentedon Nov 5, 2026
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
os-dev-report
Dev card thread
session_01Utp6CZX77joBRgcruRQSMN, branchclaude/project-thread-tk5wos(the Claim's). Status: needs_decision. No code, no PR. The order's stop condition holds:packages/cloud-connectionexposes no way for a service to read the binding credential. Readings taken against objectstack-ai/objectstack atc5800818(origin/main).What was measured
CloudConnectionPluginregisters no service. The onlyregisterServicecalls inpackages/cloud-connection/srcareseed-datasetsandseed-replayer(install-local plugin).CloudConnectionPlugin.start(), in itskernel:readyhook (cloud-connection-plugin.ts:343): platform-managed runtimes presentOS_CLOUD_API_KEY/ thecontrolPlaneApiKeyoption (throughresolveControlPlaneApiKey,packages/core/src/stack-auth.ts:133); self-hosted runtimes present theoscc_bearer kept byConnectionCredentialStore(default.objectstack/cloud-connection.json, overridable by the plugin'scredentialPath). It is read per request on purpose, so a bind that completes while the runtime is up takes effect without a restart.ConnectionCredentialStoreclass (a file reader) and no accessor for the effective credential.@objectstack/service-pushdepends on@objectstack/coreand@objectstack/speconly;@objectstack/cloud-connectiondepends on runtime, objectql, metadata-core, metadata-protocol and observability.oscc_exists underpackages/**outside cloud-connection and spec.Decisions owed
Q1. How does service-push get the binding credential?
credential()andbound()), and service-push reads it lazily on each send. One owner for the rule (Route and surface ownership, rule 1); sees a bind done at runtime. Cost: widens the file surface topackages/cloud-connection/**; may need a cross-package interface inpackages/spec/src/contracts/, outside this order's narrow spec exception. About one extra day.ConnectionCredentialStoreandresolveControlPlaneApiKeyand spells the rule a second time. Cost: a second copy of the rule beside its owner, the shape ruling 6341690661 forbids forisCloudManaged; pulls the cloud-connection dependency closure into service-push; misses a host'scredentialPathandcontrolPlaneApiKeyoptions.PushServicePluginOptionsgains one), andresolvePushCapabilityArgsupplies the default. Cost: the default still needs A or B.Q2. What does "no binding means a loud init refusal" do to a fresh self-hosted boot? A self-hosted server is unbound on its first boot by construction (it binds through the running Setup page), and
{}reads as the relay. An init refusal therefore fails every fresh self-hosted boot whose stack declaresrequires: ['push']. It also records a verdict at init about state another plugin owns (AGENTS.md, Startup registry reads).OS_CLOUD_URL=off). Otherwise register the service, answer each sendfailed, non-retryable, naming the missing binding, and log it once atwarn.requires: ['push']fail until the server is bound and restarted.Q3. Default relay endpoint. ADR-0012 section 6.1: the production relay address is the cloud's to publish and is never written into the spec. No constant exists anywhere in the tree. A path under
resolveCloudUrl()(DEFAULT_CLOUD_URL,OS_CLOUD_URL) is a plausible home, but the path belongs to objectstack-ai/cloud#3519, which this session cannot read. This cannot be decided in this repository; it waits on the cloud side.Q4 (minor). The generic visible line "in the device locale".
PushMessagecarries no locale. The device row haslocale, but the channel (packages/services/service-messaging/src/push-channel.ts) does not pass it, and service-messaging is outside the order's file surface.localeand service-push picks the line (a service-messaging edit).Carried-in scope (no blocker, unchanged)
dataonPushRelayPayloadSchema:PushDeliveryDataSchemais a plain object schema that strips unknown keys. The relay payload would use a strict variant. This narrows the wire, so the PR would be BREAKING.retryable: trueonRATE_LIMIT_EXCEEDED: a new key on the 429 schema inpush-relay.zod.ts.{ "issue": 29920, "status": "needs_decision", "branch": "claude/project-thread-tk5wos", "pr": null, "session": "session_01Utp6CZX77joBRgcruRQSMN", "premise_still_valid": true, "summary": "Read-only premise check; the order's stop condition holds. packages/cloud-connection registers no service and exports no accessor for the effective binding credential: the rule is a closure in CloudConnectionPlugin.start() (OS_CLOUD_API_KEY when platform-managed, else the oscc_ bearer in ConnectionCredentialStore). No code written, no worktree created, no PR. Four decisions owed: credential seam (A/B/C), the init refusal on an unbound self-hosted boot, the default relay endpoint (cloud side), the locale of the generic line.", "tests": "none run: no code written", "gates": "none run: no changed paths", "dogfood_battery": "n/a — no PR", "mcp_calls": "3 — mcp__github__issue_read (get, get_comments x2); no write tool", "api_writes": "1 — this comment, POST issues/29920/comments through the fleet-write relay (post-stamped)", "open_questions": [ { "question": "Q1: how does service-push read the cloud binding credential?", "options": ["A: cloud-connection registers a binding-credential service, read lazily per send", "B: service-push re-spells the rule with ConnectionCredentialStore and resolveControlPlaneApiKey", "C: host-injected resolver, default from A or B"], "recommendation": "A, because the rule keeps one owner and a bind done at runtime is seen" }, { "question": "Q2: unbound self-hosted boot with requires push and the relay default", "options": ["(i) refuse at init only when binding is impossible; else serve and answer each send failed, non-retryable, naming the missing binding, warn once", "(ii) refuse at init whenever unbound"], "recommendation": "(i), because (ii) blocks the boot that must happen before a bind" }, { "question": "Q3: the default relay endpoint", "options": ["wait for objectstack-ai/cloud to publish the relay path"], "recommendation": "wait for the cloud side; not decidable in this repository" }, { "question": "Q4: locale of the generic visible line", "options": ["A: one fixed English line in service-push", "B: the channel passes the device locale (service-messaging edit)"], "recommendation": "B, because the wire says the line is in the device locale" } ], "security_findings": "none", "out_of_scope_findings": [] }Report taken 2026-11-05T12:22Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
Seat hold · awaiting the maintainer's ruling · 2026-11-05T12:23Z
⛔ Not a landing. The claim stands; no code is written until the ruling.
packages/cloud-connectionexposes no service that reads the binding credential. It also found that the card's "no binding → loud init refusal" would refuse every fresh self-hosted boot, because{}reads as relay and the bind happens at runtime. And the default relay endpoint is the cloud's to publish (ADR-0012 §6.1), not in this tree.pm:dispatchedand the assignee while the ruling is pending. If the ruling widens scope intopackages/cloud-connection/**orpackages/spec/src/contracts/, the claim is re-posted with the new file surface.