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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
security(app-shell): AiChatPage auto-sends ?handoffPrompt= from the URL as the build agent's first message, so a crafted link makes a signed-in user author (and, with autoPublishAiBuilds, publish) metadata in one click #12493
Filing gate: ① a product defect with a public door (class a, security). Reported by a dev on objectstack-ai/cloud#2780 (report 6193031668, out_of_scope_findings 2). The client half was read from code; the cloud half was measured live on a local stack. Code re-read by the cloud seat repo:cloud#1 (seat post objectstack-ai/objectstack#6026, session session_01WVbr5J6u8BHh8EyFtcWciH). ⛔ Not a claim; grade and lane are triage's.
Priority p1 (security). Triage may raise it to p0: it is not measured what the build agent can author (for example, access rules), and that decides whether the chain can expose data.
What happens
packages/app-shell/src/console/ai/AiChatPage.tsx (same code at objectui 1fb1ced3 and on main):
L1256–1263: on the build route (agentSegment === 'build'), the page stashes it as the build pane's pending first message, and the pane sends it with no user action. The URL mirror strips the parameter afterwards.
The parameter exists for an in-app handoff (ADR-0057 P4, the ask surface's "Open in Builder →"). But it is read from the URL, so any link can supply it:
Direct: a user already signed in to an environment opens https://<env>/_console/ai/build?handoffPrompt=<text> from anywhere. The build agent receives <text> as that user's first message.
Through cloud (staging today): since objectstack-ai/cloud#2778, GET https://cloud.objectos.ai/api/v1/cloud/environments/production/sso-open?next=/_console/ai/build?handoffPrompt=<text> signs a cloud owner into their production environment and lands there. No hostname is needed, so the user does not even need a session in the environment. Cloud's next guard is same-site but parameter-agnostic by design. That landing was measured on a local stack.
Cloud environments publish AI builds automatically by default:features.autoPublishAiBuilds is true (cloud objectos-runtime runtime config). So one click can author and publish metadata as the environment owner.
Wanted
A URL parameter must never send a message on the user's behalf.
Carry the in-app handoff by navigation state (or a same-origin store), not by the query string. Or, if a URL parameter is kept, make it prefill only: it fills the composer, and the user presses send.
Cover it with a test: a page loaded with ?handoffPrompt= sends nothing.
If cloud#2780's prefill seam (?prompt=, prefill-only, never sent) is accepted later, it follows the same rule.
Exposure note
Production cloud was last deployed on 2026-10-12, before cloud#2778, so the cloud-side amplifier is on staging only. The direct path depends only on objectui and is wherever this console is served.
Triage: raised to priority:p0 · security · domain:ui · pm:queue · needs a seat now · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T13:57Z
Read fresh. AiChatPage.tsx reads ?handoffPrompt= from the URL and, on the build route, the build pane sends it as the signed-in user's first message with no user action (:882, :1256–:1263).
Any link can therefore drive the build agent as that user.
Through cloud's sso-open?next= (staging today, measured) it reaches an environment owner with no session needed.
With autoPublishAiBuilds on by default in cloud environments, one click can author and publish metadata as the owner.
Raised to p0. The card itself invited this.
The door is public (a link) and needs one click, and the outcome is an environment's metadata changed by text the attacker wrote, published as its owner. That is an integrity compromise whatever the agent's exact reach turns out to be.
Reach, read in this act:
objectstack's console pin (.objectui-shab9814b334d, the 18.0.0-next.1 console) carries the same auto-send;
so does the console of the released 17.7.0 (pin 0abd4f9f87, AiChatPage.tsx:857).
Under the maintainer's 「17.x 不再发版本」, 17.7.0 installs keep it, as they keep #23586.
Lane domain:ui (packages/app-shell).
Direction (triage routes it; it is not a decision card): the card's own Wanted.
A URL parameter never sends a message on the user's behalf. Carry the in-app handoff by navigation state or a same-origin store.
If a URL parameter is kept, it prefills only: the composer is filled and the user presses send.
Pins:
a page loaded with ?handoffPrompt= sends nothing;
the in-app "Open in Builder →" handoff still lands its text, by state;
CONTROL: a user-typed first message sends as today.
⛔ Not a confirm dialog in front of an auto-send. The rule is that the URL never sends.
After it lands:
this seat files the objectstack console pin bump (.objectui-sha), so the next 18 prerelease ships it;
cloud's deploy picks it up from there.
Whether production cloud's autoPublishAiBuilds default should change meanwhile is the maintainer's call. It is raised in this seat's round report.
Claim: PM loop round 1 · p0
Session: session_01XVp19E1MYUTFM3dnkwz8gB
Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-12493-handoff-never-sends
Worktree: objectui-issue-12493
Domain: domain:ui
Seat: domain:ui#2
File surface: packages/app-shell/src/console/ai/AiChatPage.tsx (the handoffPrompt reader at :882, the build-pane seed at :1252–:1263, the in-app producer at :1628–:1638, on mainbcf4df0), any other console AI entry that turns a URL parameter into a sent message (census in the dispatch), the tests beside each, and one .changeset/12493-*.md (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui reads no path-derived mandate)
Clause-②: no
Responsibility: objectui's AiChatPage, which reads ?handoffPrompt= from the URL and has the build pane send it as the signed-in user's first message | none: no confirmation, and cloud's sso-open next guard is parameter-agnostic by design | any signed-in console user, and through cloud#2778's sso-open (staging) any environment owner, who opens a crafted link; with autoPublishAiBuilds on by default, one click authors and publishes metadata as that user (also in the released 17.7.0 console)
Thread-read: 6193858584
Serial constraints cleared: no in-flight claim or open PR touches packages/app-shell/src/console/ai/. objectui#12474 (ObjectView.tsx and the user list-state slot) and objectui#12480 (the Studio Records grid), both this seat's, are disjoint.
{
"issue": 12493,
"status": "done",
"branch": "claude/issue-12493-handoff-never-sends",
"pr": "https://github.com/objectstack-ai/objectui/pull/12536",
"session": "session_01XVp19E1MYUTFM3dnkwz8gB",
"premise_still_valid": true,
"summary": "Premise confirmed on origin/main 1c3b0699a: AiChatPage read ?handoffPrompt= from the URL, and on the build route the pane auto-sent it. The page no longer reads ?handoffPrompt= (or ?parentConversationId=) from the URL. The in-app 'Open in Builder' handoff (openBuilder, the only producer repo-wide) now navigates with react-router state { handoffPrompt, parentConversationId } and keeps only ?package= in the URL. The seed effect is armed once per location.key. It stashes the prompt and then replaces the history entry's state without handoffPrompt, so a reload does not re-send. It is declared before the canonicalization and URL-mirror effects, so their navigations win in a shared flush. Census: no other console/ai entry turns a URL parameter into a sent message. BuilderHandoffState and readBuilderHandoffState are module-private, so the package's public surface is unchanged.",
"tests": "At head ea7adc772. Closure build pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build: exit 0. pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0. vitest packages/app-shell/src/console/ai/: exit 0, Test Files 36 passed (36), Tests 210 passed (210); this includes the new AiChatPage.handoffNeverFromUrl-12493.test.tsx with 4 pins (URL loads send nothing; the in-app handoff sends once via state and carries the parent id; a reload of the same entry sends nothing; CONTROL typed send). check:control-bytes, check:i18n-keys, check:handler-key-reads and check:unreferenced-sources: exit 0 each. check-changeset-fixed: exit 0. Narrowed eslint --no-inline-config on the 2 changed source files: 0 errors and 34 warnings, the same as base 1c3b0699a (0/34). Population is eslint.config.js (JSON shows 2 file results). Type-aware linting is off, so the verdict on untouched files is invariant. Repo-wide lint is left to CI. Ablation: leg 1 (URL read restored on the key-keyed guard) put the page into a replace-navigation loop and the run never returned a verdict; it was killed and is VOID. Leg 2 restored the URL read with the old value-keyed guard (ablation-replace: anchor 1->0, blob 8b7df383->bb8a2768). It went red on exactly pin 1: 'expected [ ... ] to deeply equal []', Tests 1 failed | 3 passed. Restore was proven by blob == HEAD 8b7df3836950 and an empty git diff HEAD. CI: in_progress, not awaited.",
"mcp_calls": "0",
"api_writes": "1 relay write: fleet-write dispatch (POST /repos/objectstack-ai/objectstack/dispatches), executing pr_create = POST /repos/objectstack-ai/objectui/pulls (draft) + POST issues/12536/assignees; read-back identical 5518/5518 bytes. Plus this os-dev-report comment (POST /repos/objectstack-ai/objectui/issues/12493/comments via the relay). git pushes x3, not REST. Labels: zero writes (objectui, none named).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: next editor of packages/plugin-chatbot/src/useObjectChat.ts — the parentConversationId re-arm comment (~L800-805) says the URL-mirror strips 'the param'; behaviour still holds (the mirror's state-less navigate drops it), only the wording is stale · noted in PR Acceptance notes, not filed",
"carrier: triage seat — the objectstack console pin (.objectui-sha) and the released 17.7.0 console still carry the URL auto-send; the pin bump is the triage seat's stated follow-up · not filed"
],
"deviations": "Ablation leg 1 hung (it held the lock about 10 min before I killed my own PIDs 27113/27065); a lock-queue timeout also voided one attempt (exit 124, never acquired). Neither changed the tree; the restore was verified by blob hash both times.",
"files_changed": [
"packages/app-shell/src/console/ai/AiChatPage.tsx",
"packages/app-shell/src/console/ai/__tests__/AiChatPage.handoffNeverFromUrl-12493.test.tsx",
".changeset/12493-handoff-never-sends.md"
]
}
Reviewed on GitHub at head ea7adc7, against triage's direction (6193858584), the seat's ruling in the dispatch and the claim (6193951021), not against the dev's report.
Shape: the PR is a draft against main. Its body opens Fixes #12493, the only closing keyword, followed by Clause-②: no. 3 files.
A URL parameter never sends a message (triage's rule), met:
AiChatPage no longer reads ?handoffPrompt=, or the companion ?parentConversationId=, from the URL.
The handoff now comes from location.state, which only same-origin script can set. A link from anywhere, including one through cloud's sso-open?next=, can no longer supply it.
No prefill seam is kept, as the seat ruled.
The in-app handoff still works:openBuilder (the ask surface's "Open in Builder →", the only producer repo-wide) navigates to /ai/build with state: { handoffPrompt, parentConversationId }, keeping only ?package= in the URL. The click is the user's own action.
No re-send on reload:
The seed effect is armed once per location.key. It stashes the prompt, then replaces the history entry with state that no longer carries handoffPrompt.
The effect is declared before the canonicalisation and URL-mirror effects, so their navigations win in a shared flush.
Census: no other console/ai/ entry turns a URL parameter into a sent message. BuilderHandoffState and readBuilderHandoffState are module-private, so the public surface is unchanged.
Pins: loading with the URL parameters sends nothing; the in-app handoff sends once, through state, and carries the parent id; a reload of the same entry sends nothing; CONTROL: a typed first message sends.
Ablation:
Restoring the URL read with the old value-keyed guard fails exactly the URL pin (1 failed, 3 passed). Restore was proven by blob hash.
A first leg (the URL read on the key-keyed guard) looped on replace-navigations and was voided, not counted.
Changeset:@object-ui/app-shell: patch, one plain sentence.
Evidence: the closure build and the app-shell type-check exit 0. console/ai/ gives 36 files and 210 tests passed. The battery subset and changeset-fixed exit 0, and narrowed eslint shows 0 errors (warnings equal base).
After landing:
The objectstack console pin (.objectui-sha) still carries the old auto-send, and so does the released 17.7.0 console. Bumping the pin so the next 18 prerelease ships this fix is triage's stated follow-up.
Whether production cloud's autoPublishAiBuilds default should change meanwhile is the maintainer's call; it is in this seat's round report.
Acceptance note (not filed):plugin-chatbot's useObjectChat.ts comment near the parentConversationId re-arm says the URL mirror strips "the param". The behaviour still holds; only the wording is stale.
CI at head: 34 success, 3 skipped, 0 failures (filter=latest, 37 runs), all 16 test shards included.
The objectstack console pin (.objectui-sha) and the released 17.7.0 console still carry the URL auto-send. Bumping the pin to a main carrying b512428, so the next 18 prerelease and cloud's deploy ship this fix, is triage's stated follow-up (6193858584).
Whether production cloud's autoPublishAiBuilds default should change meanwhile is the maintainer's call.
Label:pm:dispatched is removed in the same stroke as this comment.
Activity
objectstack-fleet commentedon Oct 15, 2026
Triage: raised to
priority:p0·security·domain:ui·pm:queue· needs a seat now · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T13:57ZRead fresh.
AiChatPage.tsxreads?handoffPrompt=from the URL and, on the build route, the build pane sends it as the signed-in user's first message with no user action (:882,:1256–:1263).sso-open?next=(staging today, measured) it reaches an environment owner with no session needed.autoPublishAiBuildson by default in cloud environments, one click can author and publish metadata as the owner.Raised to p0. The card itself invited this.
.objectui-shab9814b334d, the18.0.0-next.1console) carries the same auto-send;17.7.0(pin0abd4f9f87,AiChatPage.tsx:857).17.7.0installs keep it, as they keep #23586.Lane
domain:ui(packages/app-shell).Direction (triage routes it; it is not a decision card): the card's own Wanted.
?handoffPrompt=sends nothing;After it lands:
.objectui-sha), so the next 18 prerelease ships it;autoPublishAiBuildsdefault should change meanwhile is the maintainer's call. It is raised in this seat's round report.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 1 · p0
Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12493-handoff-never-sendsWorktree:
objectui-issue-12493Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/app-shell/src/console/ai/AiChatPage.tsx(thehandoffPromptreader at:882, the build-pane seed at:1252–:1263, the in-app producer at:1628–:1638, onmainbcf4df0), any other console AI entry that turns a URL parameter into a sent message (census in the dispatch), the tests beside each, and one.changeset/12493-*.md(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate)Clause-②: no
Responsibility:
objectui's AiChatPage, which reads ?handoffPrompt= from the URL and has the build pane send it as the signed-in user's first message | none: no confirmation, and cloud's sso-open next guard is parameter-agnostic by design | any signed-in console user, and through cloud#2778's sso-open (staging) any environment owner, who opens a crafted link; with autoPublishAiBuilds on by default, one click authors and publishes metadata as that user (also in the released 17.7.0 console)Thread-read: 6193858584
Serial constraints cleared: no in-flight claim or open PR touches
packages/app-shell/src/console/ai/. objectui#12474 (ObjectView.tsxand the user list-state slot) and objectui#12480 (the Studio Records grid), both this seat's, are disjoint.objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{ "issue": 12493, "status": "done", "branch": "claude/issue-12493-handoff-never-sends", "pr": "https://github.com/objectstack-ai/objectui/pull/12536", "session": "session_01XVp19E1MYUTFM3dnkwz8gB", "premise_still_valid": true, "summary": "Premise confirmed on origin/main 1c3b0699a: AiChatPage read ?handoffPrompt= from the URL, and on the build route the pane auto-sent it. The page no longer reads ?handoffPrompt= (or ?parentConversationId=) from the URL. The in-app 'Open in Builder' handoff (openBuilder, the only producer repo-wide) now navigates with react-router state { handoffPrompt, parentConversationId } and keeps only ?package= in the URL. The seed effect is armed once per location.key. It stashes the prompt and then replaces the history entry's state without handoffPrompt, so a reload does not re-send. It is declared before the canonicalization and URL-mirror effects, so their navigations win in a shared flush. Census: no other console/ai entry turns a URL parameter into a sent message. BuilderHandoffState and readBuilderHandoffState are module-private, so the package's public surface is unchanged.", "tests": "At head ea7adc772. Closure build pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build: exit 0. pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0. vitest packages/app-shell/src/console/ai/: exit 0, Test Files 36 passed (36), Tests 210 passed (210); this includes the new AiChatPage.handoffNeverFromUrl-12493.test.tsx with 4 pins (URL loads send nothing; the in-app handoff sends once via state and carries the parent id; a reload of the same entry sends nothing; CONTROL typed send). check:control-bytes, check:i18n-keys, check:handler-key-reads and check:unreferenced-sources: exit 0 each. check-changeset-fixed: exit 0. Narrowed eslint --no-inline-config on the 2 changed source files: 0 errors and 34 warnings, the same as base 1c3b0699a (0/34). Population is eslint.config.js (JSON shows 2 file results). Type-aware linting is off, so the verdict on untouched files is invariant. Repo-wide lint is left to CI. Ablation: leg 1 (URL read restored on the key-keyed guard) put the page into a replace-navigation loop and the run never returned a verdict; it was killed and is VOID. Leg 2 restored the URL read with the old value-keyed guard (ablation-replace: anchor 1->0, blob 8b7df383->bb8a2768). It went red on exactly pin 1: 'expected [ ... ] to deeply equal []', Tests 1 failed | 3 passed. Restore was proven by blob == HEAD 8b7df3836950 and an empty git diff HEAD. CI: in_progress, not awaited.", "mcp_calls": "0", "api_writes": "1 relay write: fleet-write dispatch (POST /repos/objectstack-ai/objectstack/dispatches), executing pr_create = POST /repos/objectstack-ai/objectui/pulls (draft) + POST issues/12536/assignees; read-back identical 5518/5518 bytes. Plus this os-dev-report comment (POST /repos/objectstack-ai/objectui/issues/12493/comments via the relay). git pushes x3, not REST. Labels: zero writes (objectui, none named).", "open_questions": [], "out_of_scope_findings": [ "carrier: next editor of packages/plugin-chatbot/src/useObjectChat.ts — the parentConversationId re-arm comment (~L800-805) says the URL-mirror strips 'the param'; behaviour still holds (the mirror's state-less navigate drops it), only the wording is stale · noted in PR Acceptance notes, not filed", "carrier: triage seat — the objectstack console pin (.objectui-sha) and the released 17.7.0 console still carry the URL auto-send; the pin bump is the triage seat's stated follow-up · not filed" ], "deviations": "Ablation leg 1 hung (it held the lock about 10 min before I killed my own PIDs 27113/27065); a lock-queue timeout also voided one attempt (exit 124, never acquired). Neither changed the tree; the restore was verified by blob hash both times.", "files_changed": [ "packages/app-shell/src/console/ai/AiChatPage.tsx", "packages/app-shell/src/console/ai/__tests__/AiChatPage.handoffNeverFromUrl-12493.test.tsx", ".changeset/12493-handoff-never-sends.md" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Review: ACCEPT (p0, security) — objectui PR #12536 ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-15T15:14ZReviewed on GitHub at head
ea7adc7, against triage's direction (6193858584), the seat's ruling in the dispatch and the claim (6193951021), not against the dev's report.main. Its body opensFixes #12493, the only closing keyword, followed byClause-②: no. 3 files.AiChatPageno longer reads?handoffPrompt=, or the companion?parentConversationId=, from the URL.location.state, which only same-origin script can set. A link from anywhere, including one through cloud'ssso-open?next=, can no longer supply it.openBuilder(theasksurface's "Open in Builder →", the only producer repo-wide) navigates to/ai/buildwithstate: { handoffPrompt, parentConversationId }, keeping only?package=in the URL. The click is the user's own action.location.key. It stashes the prompt, then replaces the history entry with state that no longer carrieshandoffPrompt.console/ai/entry turns a URL parameter into a sent message.BuilderHandoffStateandreadBuilderHandoffStateare module-private, so the public surface is unchanged.@object-ui/app-shell: patch, one plain sentence.console/ai/gives 36 files and 210 tests passed. The battery subset and changeset-fixed exit 0, and narrowed eslint shows 0 errors (warnings equal base)..objectui-sha) still carries the old auto-send, and so does the released17.7.0console. Bumping the pin so the next 18 prerelease ships this fix is triage's stated follow-up.autoPublishAiBuildsdefault should change meanwhile is the maintainer's call; it is in this seat's round report.plugin-chatbot'suseObjectChat.tscomment near theparentConversationIdre-arm says the URL mirror strips "the param". The behaviour still holds; only the wording is stale.filter=latest, 37 runs), all 16 test shards included.Next:
pr_ready+automerge_enable.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed (p0, security) ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-15T15:28Zmainb512428. The card closed by the PR'sFixes #12493, which is the only closing keyword.origin/main, all 3 files the PR touches are blob-identical to the accepted headea7adc7. They areAiChatPage.tsx, the security(app-shell): AiChatPage auto-sends ?handoffPrompt= from the URL as the build agent's first message, so a crafted link makes a signed-in user author (and, with autoPublishAiBuilds, publish) metadata in one click #12493 pin and.changeset/12493-handoff-never-sends.md. A grep ofpackages/onmainfinds nosearchParams.get('handoffPrompt')left.main:.objectui-sha) and the released17.7.0console still carry the URL auto-send. Bumping the pin to amaincarryingb512428, so the next 18 prerelease and cloud's deploy ship this fix, is triage's stated follow-up (6193858584).autoPublishAiBuildsdefault should change meanwhile is the maintainer's call.pm:dispatchedis removed in the same stroke as this comment.Generated by Claude Code
fix(app-shell): a URL parameter never sends a message on the AI build…