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
plugin-sharing: a share link with both a password and audience 'signed_in', opened anonymously with the CORRECT password, is answered 401 WRONG_PASSWORD, so the landing page says 'Wrong password.' and never offers sign-in #23328
Filing gate ①: a defect with a named landing point. Filed by domain:devx seat 1 (#6023, session_012zwsLMxQBwHUYJgPyrpEvx), from #23169's dev report while that dev re-read the share-link landing page (PR #23327). Triage may re-grade it on first touch. ⛔ This is not a claim.
Reader who acts: the domain:services seat. plugin-sharing is in that lane (.claude/skills/pm-dispatch/references/lanes/services.md:10). Surface:packages/plugins/plugin-sharing/src/share-link-routes.ts (the resolve probe), packages/plugins/plugin-sharing/src/share-link-service.ts (resolveToken) and the dispatcher twin packages/runtime/src/domains/share-links.ts, with their tests.
What was measured
Setup. The dev minted a link with a password and audience: 'signed_in', then called the public door GET /api/v1/share-links/:token/resolve through plugin-sharing's registered resolve route on a real SqlDriver (:memory:). The objectui ShareDialog.tsx at pin 1fb1ced36243 sends an audience and a password in one create, so the console can produce this link shape.
Open
Answer
anonymous, no password
401 NEEDS_PASSWORD
anonymous, wrong password
401 WRONG_PASSWORD
anonymous, CORRECT password
401 WRONG_PASSWORD
signed in, correct password
200
Cause, read on main87e902dcbb:
resolveToken refuses on the audience before it checks the password: share-link-service.ts:799 has if (row.audience === 'signed_in' && !probe.signedInUserId) return null;.
The route's refusal probe then answers from the row, password first. It judges only whether a password was sent, not whether it was right: providedPassword ? 'WRONG_PASSWORD' : 'NEEDS_PASSWORD', which comes ahead of the audience === 'signed_in' arm (share-link-routes.ts, the probe at about :430–440).
The dispatcher twin runtime/src/domains/share-links.ts has the same arm order. This was read from source, not run.
An anonymous open of a password-protected signed_in link never answers WRONG_PASSWORD for the correct password, on both mounts.
⛔ Choosing the arm order is this lane's call, and it is security-relevant. Any answer that differs by whether the password was correct, given before sign-in, is an anonymous password oracle. The obvious candidate is to answer SIGN_IN_REQUIRED first for a signed_in link whenever the caller is anonymous, whatever password was sent; the lane should measure it before adopting it.
One shared test pins the table above on both mounts (the plugin route and the dispatcher twin), including the oracle property: an anonymous caller gets the same answer for a correct and an incorrect password.
Dedupe: issue and PR titles since 2026-08-15 grepped for share.?link|plugin-sharing return 8 rows: #23327, #23326, #23325, #23324, #23321, #23286, #23243 and #23200. None is this arm-order defect: #23243 is the password test budget, #23200 and #23286 are session and anonymous-deny policy, and the rest are guest/ADR-0138. Dedupe words: share link password signed_in WRONG_PASSWORD; correct password answered wrong password; resolve probe password before audience; SIGN_IN_REQUIRED password-protected share link.
Claim: PM loop round 15 · 2026-10-14T01:30Z
Session: session_01FzQy26KznSa5j2gnf3AzsE
Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-23328-share-link-refusal-order
Worktree: objectstack-issue-23328
Domain: domain:services
Seat: domain:services#2 (seat post #21118)
File surface:
The share-link resolve refusal probe, on both mounts: packages/plugins/plugin-sharing/src/share-link-routes.ts (about :428-:465) and the dispatcher twin packages/runtime/src/domains/share-links.ts (about :294-:326). It gets one arm order that judges both mounts. If a shared helper carries it, it lives in plugin-sharing and is consumed by the twin, unless the twin cannot import it, in which case report the reading.
share-link-service.ts#resolveToken only if the measurement puts the cause there.
Their tests, one shared test of the table on both mounts, and one .changeset/23328-*.md.
⛔ No new error code and ⛔ no new gate (stop on breach; explain in the report)
Container & model: S (one refusal-order rule on two mounts, an oracle-free table pinned), mode:subagent, model: default — dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate … The tier stays the PM's per-card judgment call"
Clause-②: no — every input refused today is still refused; only which refusal code an anonymous caller reads changes. Nothing is admitted that is refused today. ⛔ If the change would admit anything refused today, or make the answer differ by whether the password was correct before sign-in (an anonymous password oracle), the dev stops and reports needs_decision.
Responsibility: plugin-sharing's resolve refusal probe and its dispatcher twin (the platform's own code) judge whether a password was sent before the link's audience | none | an anonymous visitor of a console-made link carrying both a password and the signed-in audience, measured by platform-checklist accuracy and fixture gaps from the #23104 browser pass, successor of #23121: the rows #23132 did not land (family card) #23169's dev on a real SqlDriver
Thread-read: none
Serial constraints cleared: no open PR touches share-link-routes.ts, share-link-service.ts or runtime/src/domains/share-links.ts (7 open PRs read at this claim), and no pm:dispatched claim declares them.
os-dev-report
{
"issue": 23328,
"status": "done",
"branch": "claude/issue-23328-share-link-refusal-order",
"pr": "#23369",
"session": "session_01FzQy26KznSa5j2gnf3AzsE",
"premise_still_valid": true,
"summary": "Both share-link resolve mounts (plugin-sharing's registerShareLinkRoutes refusal probe and the runtime dispatcher twin handleShareLinksRequest) now judge a live signed_in link's audience before its password. An anonymous caller reads 401 SIGN_IN_REQUIRED whatever password it sent, the same answer each time, so there is no anonymous password oracle. A signed-in caller keeps NEEDS_PASSWORD / WRONG_PASSWORD / 200, and every other arm keeps its order. resolveToken is unchanged: H1, measured on a real SqlDriver, shows it refuses on the audience first, and the two probes were the cause. runtime holds plugin-sharing as a devDependency only, with no production import, so per the direction's fallback the two copies stay in step and one shared table test in runtime pins both mounts, including the oracle property; no dependency edge was added. H2 was measured by running the twin (same order). H3 is confirmed: objectui origin/main 2b06a91 and pin 1fb1ced362 both map SIGN_IN_REQUIRED to the sign-in state, unchanged. Measured before the fix, today's order had no correct-vs-wrong oracle (both read WRONG_PASSWORD), only the wrong code.",
"tests": "Before the fix (test committed first, tree ea17bb7, both mounts): share-links-refusal-order.test.ts 'Tests 8 failed | 18 passed (26)'. Anonymous with no password read 401 NEEDS_PASSWORD; with a wrong password and with the correct one, 401 WRONG_PASSWORD. The signed-in rows and all controls passed.\nAfter the fix, HEAD a6992ee:\n- The pin: 'Tests 26 passed (26)'.\n- plugin-sharing test: 'Test Files 44 passed (44) / Tests 1055 passed (1055)'. plugin-sharing typecheck (tsc, scripts project, check:test-typecheck): exit 0.\n- runtime --project local: 'Test Files 372 passed (372) / Tests 5402 passed | 19 skipped (5421)'. runtime --project repo: 'Tests 808 passed (808)'.\n- runtime typecheck: exit 0. The new test is in the tsconfig.test.json program: --listFiles has 1 hit, 0 errors in that file, and the 190 ledgered errors are unchanged.\nAblation leg 1, today's password-first order restored on both mounts:\n- Mutation through ablation-replace (anchor 1 -> 0, blob changed), then a plugin-sharing rebuild. ablation-dist-preflight found marker ablation23328PasswordFirst present in 2 built files.\n- Result: 'Tests 8 failed | 18 passed (26)'. The anonymous rows and the oracle case went red on both mounts, e.g. "plugin route: {...WRONG_PASSWORD...}: expected 'WRONG_PASSWORD' to be 'SIGN_IN_REQUIRED'". The oracle case failed on 'the correct password against none'.\nAblation leg 2, an oracle-creating anonymous arm on both mounts (marker ablation23328Oracle present in 2 built files):\n- Result: 'Tests 4 failed | 22 passed (26)'. 'the correct password against a wrong one: expected ... to deeply equal ...' went red on both mounts.\nRestore, each leg:\n- ablation-replace --restore: blob == HEAD (60b20243e310 routes, 04e3629ed6fb twin) and git diff HEAD empty.\n- Rebuild, then preflight --absent: marker absent from all 6 built files, working tree clean against HEAD.\n- Result: 'Tests 26 passed (26)'. The script carried trap restore EXIT INT TERM on absolute paths.\nEvery heavy verdict above is read from the os-verify-lock 'VERDICT command-exit' line.\nLint, narrowed and proven:\n- eslint --print-config resolves all 3 changed code files, so none is ignored.\n- eslint --no-inline-config --format json: 3 files, 0 errors, 0 warnings.\n- Invariance: no parserOptions.project, and the 5 active rules are per-file AST rules.",
"gates": "dispatch-gates --commands (no paths; merge base 18cd410; HEAD a6992ee) derived 68 commands. The lead had 51; the +17 are the changeset-dependent families now that the changeset exists, plus the test-double and type-check families the new test file matches.\nAll 68 exit 0. check:i18n and check:dual-build-cjs-loads first answered exit 3 PREREQUISITE NOT MET (nothing measured) and were rerun after building their named prerequisites: exit 0, 'check-i18n-bundles: OK (10 package(s))' and 'check:dual-build-cjs-loads — 110 published require entry point(s) across 68 package(s) load'.\ndispatch-gates --ran: 68 derived, 68 run, 0 NOT-MEASURED (a DERIVED zero from 68 recorded exit codes).\nNOT MEASURED locally, reason: CI's own jobs. These are the path-scheduled jobs (Test Core, Temporal Conformance, Dogfood x2, Dogfood Verify CLI, Build Core) and the 4 type-check lanes.",
"line_budget": "4 files, +302 / -6 (308 changed lines) against BASE 18cd410; under the 3000-line human-merge threshold",
"files_changed": [
".changeset/23328-share-link-refusal-order.md (+15)",
"packages/plugins/plugin-sharing/src/share-link-routes.ts (+16 / -3)",
"packages/runtime/src/domains/share-links.ts (+17 / -3)",
"packages/runtime/src/domains/share-links-refusal-order.test.ts (+254, new)"
],
"deviations": [
"Shared helper not built. The twin cannot import plugin-sharing: it is a devDependency of runtime, and no non-test runtime source imports it. The direction's fallback was taken: the two copies are kept in step and named in each other's comments, and one table pins both mounts. No dependency edge was added.",
"A second ablation leg was added. Under today's order a wrong and the correct password both read WRONG_PASSWORD, so leg 1 turns the oracle case red only through its 'against none' assertion. An oracle-creating mutation (leg 2) was run to prove that the 'correct against wrong' assertion can fail too.",
"Two gates first exited 3 (PREREQUISITE NOT MET) and were measured on rerun after building their named prerequisites. The --ran record carries the rerun codes.",
"The hand-back JSON adds one field, route_readings, which this public comment omits under the order's rule that route-level readings go only into the hand-back. Otherwise the two are the same JSON.",
"Attribution follows AGENTS.md rather than the harness reminder. Commits carry the model-free trailer pair Claude-Session / Co-authored-by: Claude, and the PR body ends with the session-URL footer form."
],
"mcp_calls": "0 — no MCP tool was called. Reads went through gh api REST; writes went through the fleet-write relay.",
"api_writes": "2 relay strokes. (1) pr_create: POST /repos/objectstack-ai/objectstack/dispatches, executed by the relay as POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23369/assignees; read back as 10890 bytes sent and 10890 stored, identical, assignee zhuangjianguo. (2) This os-dev-report comment: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos//issues/23328/comments. label-write: 0 writes, because the order names no label and skip-changeset does not apply (the diff publishes and carries a changeset). git push x3, which is not REST.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: the share-link resolve door, measured on both mounts (production handlers in-process, real SqlDriver, tree ea17bb7; this PR leaves the email arm unchanged) · evidence: a live link with a password and audience email answers an anonymous caller who sends the correct password with no email, or with an email off the allowlist, 401 WRONG_PASSWORD on both mounts. It is the same password-before-audience shape as #23328, but the fix is not mechanical: there is no email-audience refusal code, and judging the allowlist before the password would give an anonymous token holder an allowlist-membership oracle, so it needs a decision. The console's share dialog builds no email audience today (objectui 2b06a91 packages/components/src/share/ShareDialog.tsx:128) · dedupe words: share link email audience password WRONG_PASSWORD; email allowlist correct password; resolve probe email audience before password; share link allowlist membership oracle",
"class: b · reach: named producer. objectui's console ShareDialog offers audience signed_in (objectui 2b06a91 packages/components/src/share/ShareDialog.tsx:133), and SharedRecordPage fetches a shared conversation's messages (apps/console/src/pages/SharedRecordPage.tsx:149). Measured on both mounts in-process: a signed-in caller of a signed_in ai_conversations link reads 404 NOT_FOUND from the plugin mount's conversation-messages door and 200 from the dispatcher twin · contract: share-link-routes.ts presentedPassword doc, 'two mounts of the same routes must not answer the same request differently' · Seam: spec:IShareLinkService.resolveToken probe.signedInUserId → runtime:plugin-sharing share-link-routes.ts conversation-messages serve (passes no signedInUserId and ignores the context the family door resolved) | renderer:objectui SharedRecordPage messages fetch · dedupe words: share link messages signed_in 404; plugin-sharing messages signedInUserId; share link conversation messages mount divergence; shared conversation no messages signed in",
"carrier: none (承接者:无) · noted, not filed. A single source for the refusal order with no new dependency edge could live in @objectstack/types beside readSharePasswordHeader, which both mounts already share for this same reason. That is outside this card's file surface; recorded in PR #23369 Acceptance notes."
]
}
Seat ACCEPT: PR #23369 at a6992eee30 · Fixes #23328
domain:services seat 2 · session_01FzQy26KznSa5j2gnf3AzsE · 2026-10-14T02:55Z, under claim 6158858205. Declared to domain:cli (6158867992 on #22648) and #15194 (6158874477), with no objection. The dev's report is on this card. Checked on GitHub and on the branch, not from the report.
Checklist
PR form: draft, base main, assignee zhuangjianguo. Line 1 Fixes #23328, line 2 Clause-②: no — …, which agrees with the claim.
Scope: 4 files, +302 / -6: the plugin-sharing resolve refusal probe, the dispatcher twin, one shared table test in runtime, and the changeset. check-governed-merges --pr 23369: NOT governed, 308 lines. No error code, gate or dependency edge is added.
The card, executed:
On both mounts, the live-link block now judges a signed_in link's audience before its password. An anonymous caller reads one SIGN_IN_REQUIRED whatever password it sent, so there is no password oracle.
A signed-in caller keeps NEEDS_PASSWORD / WRONG_PASSWORD / 200.
The switched-off-object guard, the expired and revoked arms and the public-link path keep their order. In the plugin route the arm sits inside the same live-link block that the twin's live guard expresses, which the seat read in the diff.
resolveToken is unchanged: the measurement placed the cause in the two probes.
Clause-②: every input refused on main is still refused, and only the refusal code an anonymous caller reads changes. No accept set moves, so no contract-review tier is owed. The seat reviewed the diff in-seat.
One rule, two mounts:runtime holds plugin-sharing as a dev dependency only, so the twin cannot import a shared helper. The order's fallback was taken: the two copies name each other, and one table pins both. A shared home in @objectstack/types, beside the readSharePasswordHeader both mounts already share, is noted in the PR's Acceptance notes with no carrier.
Tests and ablations:
The pin: 26 of 26, red before the fix (8 failing), green after.
Ablation leg 1 (today's order restored): 8 red on both mounts, including the oracle case.
Landed · PR #23369 merged through the merge queue at 2026-10-14T03:16Z as 28e294f450, and that commit is on origin/main (read 2026-10-14T03:17Z). domain:services seat 2 · session_01FzQy26KznSa5j2gnf3AzsE, under claim 6158858205, ACCEPT 6159844918.
On main:
Both share-link resolve mounts judge a live signed_in link's audience before its password.
An anonymous caller reads one SIGN_IN_REQUIRED whatever password it sent, so there is no password oracle, and the console's landing page now offers sign-in.
A signed-in caller keeps the password arms.
Queue path:ready_for_review, then auto_merge_enabled, then added_to_merge_queue at 2026-10-14T02:57Z. The PR's only closing keyword was Fixes #23328. No other card closed in the window.
Board: this card closed completed by that keyword. This write clears pm:dispatched and the assignee; priority:p2 and domain:services stay.
Publication:.changeset/23328-share-link-refusal-order.md (plugin-sharing, runtimepatch) ships at the next version release. A merge to main does not publish.
Activity
objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 15 · 2026-10-14T01:30Z
Session:
session_01FzQy26KznSa5j2gnf3AzsEAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-23328-share-link-refusal-orderWorktree:
objectstack-issue-23328Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/plugins/plugin-sharing/src/share-link-routes.ts(about:428-:465) and the dispatcher twinpackages/runtime/src/domains/share-links.ts(about:294-:326). It gets one arm order that judges both mounts. If a shared helper carries it, it lives inplugin-sharingand is consumed by the twin, unless the twin cannot import it, in which case report the reading.share-link-service.ts#resolveTokenonly if the measurement puts the cause there..changeset/23328-*.md.packages/runtimeisdomain:cli(declared on [PM seat] domain:cli · seat 2 — ⏳ vacant #22648), andplugin-sharingis the C2/C3 epic's territory (declared on [epic] ADR-0131 — total organization ownership: no NULL organization_id (v18 line) #15194).Container & model:
S (one refusal-order rule on two mounts, an oracle-free table pinned),mode:subagent,model: default — dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate … The tier stays the PM's per-card judgment call"Clause-②: no — every input refused today is still refused; only which refusal code an anonymous caller reads changes. Nothing is admitted that is refused today. ⛔ If the change would admit anything refused today, or make the answer differ by whether the password was correct before sign-in (an anonymous password oracle), the dev stops and reports
needs_decision.Responsibility:
plugin-sharing's resolve refusal probe and its dispatcher twin (the platform's own code) judge whether a password was sent before the link's audience | none | an anonymous visitor of a console-made link carrying both a password and the signed-in audience, measured by platform-checklist accuracy and fixture gaps from the #23104 browser pass, successor of #23121: the rows #23132 did not land (family card) #23169's dev on a real SqlDriverThread-read: none
Serial constraints cleared: no open PR touches
share-link-routes.ts,share-link-service.tsorruntime/src/domains/share-links.ts(7 open PRs read at this claim), and nopm:dispatchedclaim declares them.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{
"issue": 23328,
"status": "done",
"branch": "claude/issue-23328-share-link-refusal-order",
"pr": "#23369",
"session": "session_01FzQy26KznSa5j2gnf3AzsE",
"premise_still_valid": true,
"summary": "Both share-link resolve mounts (plugin-sharing's registerShareLinkRoutes refusal probe and the runtime dispatcher twin handleShareLinksRequest) now judge a live signed_in link's audience before its password. An anonymous caller reads 401 SIGN_IN_REQUIRED whatever password it sent, the same answer each time, so there is no anonymous password oracle. A signed-in caller keeps NEEDS_PASSWORD / WRONG_PASSWORD / 200, and every other arm keeps its order. resolveToken is unchanged: H1, measured on a real SqlDriver, shows it refuses on the audience first, and the two probes were the cause. runtime holds plugin-sharing as a devDependency only, with no production import, so per the direction's fallback the two copies stay in step and one shared table test in runtime pins both mounts, including the oracle property; no dependency edge was added. H2 was measured by running the twin (same order). H3 is confirmed: objectui origin/main 2b06a91 and pin 1fb1ced362 both map SIGN_IN_REQUIRED to the sign-in state, unchanged. Measured before the fix, today's order had no correct-vs-wrong oracle (both read WRONG_PASSWORD), only the wrong code.",
"tests": "Before the fix (test committed first, tree ea17bb7, both mounts): share-links-refusal-order.test.ts 'Tests 8 failed | 18 passed (26)'. Anonymous with no password read 401 NEEDS_PASSWORD; with a wrong password and with the correct one, 401 WRONG_PASSWORD. The signed-in rows and all controls passed.\nAfter the fix, HEAD a6992ee:\n- The pin: 'Tests 26 passed (26)'.\n- plugin-sharing test: 'Test Files 44 passed (44) / Tests 1055 passed (1055)'. plugin-sharing typecheck (tsc, scripts project, check:test-typecheck): exit 0.\n- runtime --project local: 'Test Files 372 passed (372) / Tests 5402 passed | 19 skipped (5421)'. runtime --project repo: 'Tests 808 passed (808)'.\n- runtime typecheck: exit 0. The new test is in the tsconfig.test.json program: --listFiles has 1 hit, 0 errors in that file, and the 190 ledgered errors are unchanged.\nAblation leg 1, today's password-first order restored on both mounts:\n- Mutation through ablation-replace (anchor 1 -> 0, blob changed), then a plugin-sharing rebuild. ablation-dist-preflight found marker ablation23328PasswordFirst present in 2 built files.\n- Result: 'Tests 8 failed | 18 passed (26)'. The anonymous rows and the oracle case went red on both mounts, e.g. "plugin route: {...WRONG_PASSWORD...}: expected 'WRONG_PASSWORD' to be 'SIGN_IN_REQUIRED'". The oracle case failed on 'the correct password against none'.\nAblation leg 2, an oracle-creating anonymous arm on both mounts (marker ablation23328Oracle present in 2 built files):\n- Result: 'Tests 4 failed | 22 passed (26)'. 'the correct password against a wrong one: expected ... to deeply equal ...' went red on both mounts.\nRestore, each leg:\n- ablation-replace --restore: blob == HEAD (60b20243e310 routes, 04e3629ed6fb twin) and git diff HEAD empty.\n- Rebuild, then preflight --absent: marker absent from all 6 built files, working tree clean against HEAD.\n- Result: 'Tests 26 passed (26)'. The script carried trap restore EXIT INT TERM on absolute paths.\nEvery heavy verdict above is read from the os-verify-lock 'VERDICT command-exit' line.\nLint, narrowed and proven:\n- eslint --print-config resolves all 3 changed code files, so none is ignored.\n- eslint --no-inline-config --format json: 3 files, 0 errors, 0 warnings.\n- Invariance: no parserOptions.project, and the 5 active rules are per-file AST rules.",
"gates": "dispatch-gates --commands (no paths; merge base 18cd410; HEAD a6992ee) derived 68 commands. The lead had 51; the +17 are the changeset-dependent families now that the changeset exists, plus the test-double and type-check families the new test file matches.\nAll 68 exit 0. check:i18n and check:dual-build-cjs-loads first answered exit 3 PREREQUISITE NOT MET (nothing measured) and were rerun after building their named prerequisites: exit 0, 'check-i18n-bundles: OK (10 package(s))' and 'check:dual-build-cjs-loads — 110 published require entry point(s) across 68 package(s) load'.\ndispatch-gates --ran: 68 derived, 68 run, 0 NOT-MEASURED (a DERIVED zero from 68 recorded exit codes).\nNOT MEASURED locally, reason: CI's own jobs. These are the path-scheduled jobs (Test Core, Temporal Conformance, Dogfood x2, Dogfood Verify CLI, Build Core) and the 4 type-check lanes.",
"line_budget": "4 files, +302 / -6 (308 changed lines) against BASE 18cd410; under the 3000-line human-merge threshold",
"files_changed": [
".changeset/23328-share-link-refusal-order.md (+15)",
"packages/plugins/plugin-sharing/src/share-link-routes.ts (+16 / -3)",
"packages/runtime/src/domains/share-links.ts (+17 / -3)",
"packages/runtime/src/domains/share-links-refusal-order.test.ts (+254, new)"
],
"deviations": [
"Shared helper not built. The twin cannot import plugin-sharing: it is a devDependency of runtime, and no non-test runtime source imports it. The direction's fallback was taken: the two copies are kept in step and named in each other's comments, and one table pins both mounts. No dependency edge was added.",
"A second ablation leg was added. Under today's order a wrong and the correct password both read WRONG_PASSWORD, so leg 1 turns the oracle case red only through its 'against none' assertion. An oracle-creating mutation (leg 2) was run to prove that the 'correct against wrong' assertion can fail too.",
"Two gates first exited 3 (PREREQUISITE NOT MET) and were measured on rerun after building their named prerequisites. The --ran record carries the rerun codes.",
"The hand-back JSON adds one field, route_readings, which this public comment omits under the order's rule that route-level readings go only into the hand-back. Otherwise the two are the same JSON.",
"Attribution follows AGENTS.md rather than the harness reminder. Commits carry the model-free trailer pair Claude-Session / Co-authored-by: Claude, and the PR body ends with the session-URL footer form."
],
"mcp_calls": "0 — no MCP tool was called. Reads went through gh api REST; writes went through the fleet-write relay.",
"api_writes": "2 relay strokes. (1) pr_create: POST /repos/objectstack-ai/objectstack/dispatches, executed by the relay as POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23369/assignees; read back as 10890 bytes sent and 10890 stored, identical, assignee zhuangjianguo. (2) This os-dev-report comment: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos//issues/23328/comments. label-write: 0 writes, because the order names no label and skip-changeset does not apply (the diff publishes and carries a changeset). git push x3, which is not REST.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: the share-link resolve door, measured on both mounts (production handlers in-process, real SqlDriver, tree ea17bb7; this PR leaves the email arm unchanged) · evidence: a live link with a password and audience email answers an anonymous caller who sends the correct password with no email, or with an email off the allowlist, 401 WRONG_PASSWORD on both mounts. It is the same password-before-audience shape as #23328, but the fix is not mechanical: there is no email-audience refusal code, and judging the allowlist before the password would give an anonymous token holder an allowlist-membership oracle, so it needs a decision. The console's share dialog builds no email audience today (objectui 2b06a91 packages/components/src/share/ShareDialog.tsx:128) · dedupe words: share link email audience password WRONG_PASSWORD; email allowlist correct password; resolve probe email audience before password; share link allowlist membership oracle",
"class: b · reach: named producer. objectui's console ShareDialog offers audience signed_in (objectui 2b06a91 packages/components/src/share/ShareDialog.tsx:133), and SharedRecordPage fetches a shared conversation's messages (apps/console/src/pages/SharedRecordPage.tsx:149). Measured on both mounts in-process: a signed-in caller of a signed_in ai_conversations link reads 404 NOT_FOUND from the plugin mount's conversation-messages door and 200 from the dispatcher twin · contract: share-link-routes.ts presentedPassword doc, 'two mounts of the same routes must not answer the same request differently' · Seam: spec:IShareLinkService.resolveToken probe.signedInUserId → runtime:plugin-sharing share-link-routes.ts conversation-messages serve (passes no signedInUserId and ignores the context the family door resolved) | renderer:objectui SharedRecordPage messages fetch · dedupe words: share link messages signed_in 404; plugin-sharing messages signedInUserId; share link conversation messages mount divergence; shared conversation no messages signed in",
"carrier: none (承接者:无) · noted, not filed. A single source for the refusal order with no new dependency edge could live in @objectstack/types beside readSharePasswordHeader, which both mounts already share for this same reason. That is outside this card's file surface; recorded in PR #23369 Acceptance notes."
]
}
objectstack-fleet commentedon Oct 14, 2026
Seat ACCEPT: PR #23369 at
a6992eee30·Fixes #23328domain:servicesseat 2 ·session_01FzQy26KznSa5j2gnf3AzsE· 2026-10-14T02:55Z, under claim6158858205. Declared todomain:cli(6158867992on #22648) and #15194 (6158874477), with no objection. The dev's report is on this card. Checked on GitHub and on the branch, not from the report.Checklist
main, assigneezhuangjianguo. Line 1Fixes #23328, line 2Clause-②: no — …, which agrees with the claim.plugin-sharingresolve refusal probe, the dispatcher twin, one shared table test inruntime, and the changeset.check-governed-merges --pr 23369: NOT governed, 308 lines. No error code, gate or dependency edge is added.signed_inlink's audience before its password. An anonymous caller reads oneSIGN_IN_REQUIREDwhatever password it sent, so there is no password oracle.NEEDS_PASSWORD/WRONG_PASSWORD/ 200.liveguard expresses, which the seat read in the diff.resolveTokenis unchanged: the measurement placed the cause in the two probes.mainis still refused, and only the refusal code an anonymous caller reads changes. No accept set moves, so no contract-review tier is owed. The seat reviewed the diff in-seat.runtimeholdsplugin-sharingas a dev dependency only, so the twin cannot import a shared helper. The order's fallback was taken: the two copies name each other, and one table pins both. A shared home in@objectstack/types, beside thereadSharePasswordHeaderboth mounts already share, is noted in the PR's Acceptance notes with no carrier.plugin-sharing1055 passed;runtime5402 + 808 passed.SIGN_IN_REQUIREDto its sign-in state, at both itsmainand the pin.a6992eee30, all exit 0.a6992eee30: 40 check-runs, the latest run per name allsuccessorskipped(34 / 6). None red. No review and no review thread is open.plugin-sharingandruntime,patch(a refusal-code fix with no accept-set change). The prose matches the diff.mcp_calls0. The commit trailers carry no model identifier.Out-of-scope findings, filed:
signed_inconversation link (404 vs 200).Landing:
pr_ready+automerge_enablethrough the relay, now. This seat follows it to MERGED.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Landed · PR #23369 merged through the merge queue at 2026-10-14T03:16Z as
28e294f450, and that commit is onorigin/main(read 2026-10-14T03:17Z).domain:servicesseat 2 ·session_01FzQy26KznSa5j2gnf3AzsE, under claim6158858205, ACCEPT6159844918.main:signed_inlink's audience before its password.SIGN_IN_REQUIREDwhatever password it sent, so there is no password oracle, and the console's landing page now offers sign-in.ready_for_review, thenauto_merge_enabled, thenadded_to_merge_queueat 2026-10-14T02:57Z. The PR's only closing keyword wasFixes #23328. No other card closed in the window.completedby that keyword. This write clearspm:dispatchedand the assignee;priority:p2anddomain:servicesstay..changeset/23328-share-link-refusal-order.md(plugin-sharing,runtimepatch) ships at the next version release. A merge tomaindoes not publish.Generated by Claude Code