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
Bridge: pickImage(), openFile() and share() — reliable photo capture, attachment download and sharing #147
Ruled: 6593858903 · letter A · 2026-11-03T02:26Z
Blocked-by: #144
Context
The 2.0 shell relies on WebView defaults for <input type=file> and downloads (docs/HYBRID-SHELL.md, Known limits). Those behave inconsistently across iOS and Android: camera capture permissions, files that open nowhere, and no share sheet.
Ask
pickImage(opts?: { camera?: boolean; maxWidth?: number; quality?: number }): resolves a File-compatible payload (base64 + name + mime) that the Console uploads, or null on cancel. Uses expo-image-picker, with downscaling before transfer.
openFile(url, filename?): downloads with the WebView session (same-origin URL only; reject other origins) through expo-file-system, then opens or previews it with the OS.
share({ url?, text? }): the OS share sheet.
Android: camera / media permissions, and <input type=file> keeps working as a fallback.
Follow docs/HYBRID-SHELL.md (Adding a method) for each method.
Acceptance
Unit tests cover argument validation (origin check for openFile), cancel paths and payload size limits.
Manual device checks on both platforms.
Follow-up: after acceptance, the seat files the Console consumer card in objectstack-ai/objectstack.
Ruling: batch #1 item 3 · letter A · maintainer 「同意」 2026-11-03T02:26Z
The maintainer accepted the batch's recommendations in the session session_01QSeKYnW1Xf6HDSWz1eGAqv. The presentation (item 3) put A as the recommendation, and the reply 「同意」 applies to it.
Option A:
pickImage() and share() are built as the card says.
openFile becomes openFile({ base64, name, mime }). The Console fetches the file with its own session and hands the bytes over; the native side previews or shares them. ⛔ The native side never reads cookies or calls the API for a download.
Set a payload size limit, and measure it.
Effort for A (card's column): mobile lane, 1 PR, plus a Console consumer card.
Next: the card moves to pm:queue, and the seat dispatches A.
Claim: PM loop round 2
Session: session_01QSeKYnW1Xf6HDSWz1eGAqv
Account: os-justin
Branch: claude/issue-147-pick-share-open
Worktree: mobile-issue-147
Domain: n/a — single-lane repo (repo:mobile)
Seat: repo:mobile#1
File surface: lib/shell/bridge/** (pickImage, share, openFile with bytes), a file-handoff module under lib/, app.config.ts, package.json and pnpm-lock.yaml (image-picker, sharing and file-system modules), locales/*.json (all ten), __tests__/**, docs/HYBRID-SHELL.md, .changeset/147-pick-share-open.md (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: default tier (opus) — no path-derived mandate, effort: high
Clause-②: no
Metadata-protocol: none
Responsibility: n/a — not a defect card
Thread-read: 6593858903
Premise: holds. On main, BRIDGE_METHODS has no pickImage, share or openFile, and docs/HYBRID-SHELL.md Known limits says downloads rely on WebView defaults. Ruling 6593858903: option A (⛔ the native side never reads cookies or calls the API for a download).
Serial constraints cleared: #145 and #147 are dispatched in the same round and both touch lib/shell/bridge/** and docs/HYBRID-SHELL.md. That is ordinary concurrency; the PR that lands second reconciles.
{
"issue": 147,
"status": "done",
"branch": "claude/issue-147-pick-share-open",
"pr": "https://github.com/objectstack-ai/mobile/pull/180",
"session": "session_01QSeKYnW1Xf6HDSWz1eGAqv",
"premise_still_valid": true,
"summary": "Ruling A is implemented as three bridge methods: pickImage (expo-image-picker plus an expo-image-manipulator downscale, JPEG, null on cancel), share (React Native Share) and openFile, which takes bytes and never fetches (expo-file-system writes to the cache, then Android ACTION_VIEW through expo-intent-launcher with a share-sheet fallback, and the expo-sharing sheet on iOS). Files are limited to 8 MiB: the bootstrap refuses an oversize call before posting and the handler refuses it again with bad_args, and an oversize pickImage result fails. Only openFile from the current connection's origin may exceed the 64 KB message cap, and the origin is checked before parsing. The camera text was updated and a new photo-library text added in all ten locales; expo-image-picker is configured without a microphone. Mechanism assumptions disproved: image-picker cannot resize (manipulator added), expo-sharing cannot share a link or text (RN Share used), and Android open needs ACTION_VIEW (intent-launcher added). Landing point outside the claimed surface: app/console.tsx (+4 lines), where BridgeDeps are built. Native rebuild required; device checks are not verified.",
"tests": "head c3ca88e: npx jest --ci --coverage exit 0, 46/46 suites, 935/935 tests (lib/shell/bridge 100% statements; file-handoff.ts 100% statements / 95.12% branches). Cold run, npx jest --ci --no-cache __tests__/components/ConsoleScreen.test.tsx: exit 0, 13/13. Ablations (ablation-replace.mjs wrap mode, mutation proven on disk, restore proven by blob equal to HEAD and an empty git diff HEAD). Size limit: handler openFile check deleted, red 'refuses a file over the size limit with the error envelope'; pickImage result check reduced, red 'refuses an image over the size limit after downscaling'; bootstrap pre-check set to false, red 'refuses a file over the size limit in the page' (via the 5 s timeout: the call posts and never settles); parseInbound openFile-only exemption deleted, red 'keeps the ordinary cap for every other method and for events'. No fetch: handler accepts a url key, red 'takes no URL'; fetch inserted in openFileBytes, red 'never fetches' plus the structural pin (and 6 more, fetch being undefined under jest); File.downloadFileAsync inserted, red exactly 'never fetches' and the structural pin. The first attempts at the last two were refused by the tool (replacement contained the anchor), were no-ops, and were re-anchored and re-run.",
"gates": "local, on head c3ca88e, exit codes captured before any pipe: npx tsc --noEmit 0; npx eslint . --ext .ts,.tsx 0; npx jest --ci --coverage 0; cold ConsoleScreen 0; pnpm install --frozen-lockfile 0; control-byte grep over changed files: no hit. Exit codes recorded in scratchpad issue-147/gates.txt. CI check-runs on c3ca88e: 'Lint & Type Check' completed/success, 'Code Quality' completed/success. dispatch-gates.mjs not applicable (an objectstack tool; the mobile gates are the dispatch's list).",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "2 relay/stamped writes: (1) fleet-write relay stroke pr_create, dispatched as POST /repos/objectstack-ai/objectstack/dispatches; the run opened POST /repos/objectstack-ai/mobile/pulls -> #180; read back 11428 bytes sent and stored, identical. (2) post-stamped.mjs POST /repos/objectstack-ai/mobile/issues/147/comments (this report). Plus git push (not REST), and a first relay attempt refused at validation (draft key), zero writes.",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · noted, not filed — channel limit measured in Chromium 153 over CDP only; React Native event, Hermes, WKWebView and devices NOT MEASURED (no device in container); on-device timing at 8 MiB is part of the device checks",
"carrier: none · noted, not filed — an Android photo taken while the OS kills the app is not delivered (getPendingResultAsync not wired); documented in Known limits",
"carrier: none · noted, not filed — the refusal text says '8 MB' where the constant is 8 MiB",
"carrier: none · noted, not filed — the first branch commit 6577439 carries the harness's model-named Co-Authored-By trailer; later commits use the model-free pair; history not rewritten",
"carrier: none · noted, not filed — '需要你做的' is one action (rebuild, then run the device checks) carrying the four checks the dispatch named"
]
}
ACCEPT → PR #180 (head c3ca88e). The seat landed it through the merge queue. A native rebuild is owed (five new Expo modules). The Console consumer card is filed in objectstack.
CI on the merged result: the merge-group CI (jest) was still running when the queue merged, because the ruleset requires only Lint & Type Check. The seat follows it to completion.
Activity
objectstack-fleet commentedon Nov 2, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
决策:下载附件时,壳能不能拿登录态自己去下?
一句话问题: 卡面
openFile(url)让原生层带着用户的登录态自己下载文件。可桥接契约写明「壳不持凭据、不自己调 API」,两者冲突。Governing text:
docs/HYBRID-SHELL.md〈Session and auth〉:"the shell holds no API credential and makes no API calls of its own"。lanes/mobile.md常设承诺:「壳不持 API 凭据、不调平台 API」。前提(带 re-check):
grep -n "Downloads and file inputs" docs/HYBRID-SHELL.mdpickImage与share不碰登录态,不在本冲突内。只有openFile冲突。选项:
pickImage与share照做。openFile改为openFile({ base64, name, mime }):Console 用自己的登录态取文件,交给原生预览或分享;原生不碰 cookieexpo-file-system同源下载openFile,改为接管 WebView 自带下载事件(安卓 DownloadListener、iOS 下载代理),交 OS 打开业务含义: A 是网页把文件递给手机,由手机负责打开。B 是把网站钥匙配一把给手机,让它自己去取。C 是手机替浏览器接下下载动作。
四轴(业务立场):
推荐:A。
裁后执行: 席位在卡上记改法,把
openFile的 Ask 改为 A 的形状,整卡派 os-dev。验收后在 objectstack 立 Console 消费卡。Analysis-tier: default (seat tier)
objectstack-fleet commentedon Nov 3, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
Ruling: batch #1 item 3 · letter A · maintainer 「同意」 2026-11-03T02:26Z
The maintainer accepted the batch's recommendations in the session
session_01QSeKYnW1Xf6HDSWz1eGAqv. The presentation (item 3) put A as the recommendation, and the reply 「同意」 applies to it.Option A:
pickImage()andshare()are built as the card says.openFilebecomesopenFile({ base64, name, mime }). The Console fetches the file with its own session and hands the bytes over; the native side previews or shares them. ⛔ The native side never reads cookies or calls the API for a download.Effort for A (card's column): mobile lane, 1 PR, plus a Console consumer card.
Next: the card moves to
pm:queue, and the seat dispatches A.objectstack-fleet commentedon Nov 3, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
Claim: PM loop round 2
Session:
session_01QSeKYnW1Xf6HDSWz1eGAqvAccount:
os-justinBranch:
claude/issue-147-pick-share-openWorktree:
mobile-issue-147Domain:
n/a — single-lane repo (repo:mobile)Seat:
repo:mobile#1File surface:
lib/shell/bridge/**(pickImage,share,openFilewith bytes), a file-handoff module underlib/,app.config.ts,package.jsonandpnpm-lock.yaml(image-picker, sharing and file-system modules),locales/*.json(all ten),__tests__/**,docs/HYBRID-SHELL.md,.changeset/147-pick-share-open.md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default tier (opus) — no path-derived mandate,effort: highClause-②: no
Metadata-protocol: none
Responsibility:
n/a — not a defect cardThread-read: 6593858903
Premise: holds. On
main,BRIDGE_METHODShas nopickImage,shareoropenFile, anddocs/HYBRID-SHELL.mdKnown limits says downloads rely on WebView defaults. Ruling 6593858903: option A (⛔ the native side never reads cookies or calls the API for a download).Serial constraints cleared: #145 and #147 are dispatched in the same round and both touch
lib/shell/bridge/**anddocs/HYBRID-SHELL.md. That is ordinary concurrency; the PR that lands second reconciles.objectstack-fleet commentedon Nov 3, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
os-dev-report
{ "issue": 147, "status": "done", "branch": "claude/issue-147-pick-share-open", "pr": "https://github.com/objectstack-ai/mobile/pull/180", "session": "session_01QSeKYnW1Xf6HDSWz1eGAqv", "premise_still_valid": true, "summary": "Ruling A is implemented as three bridge methods: pickImage (expo-image-picker plus an expo-image-manipulator downscale, JPEG, null on cancel), share (React Native Share) and openFile, which takes bytes and never fetches (expo-file-system writes to the cache, then Android ACTION_VIEW through expo-intent-launcher with a share-sheet fallback, and the expo-sharing sheet on iOS). Files are limited to 8 MiB: the bootstrap refuses an oversize call before posting and the handler refuses it again with bad_args, and an oversize pickImage result fails. Only openFile from the current connection's origin may exceed the 64 KB message cap, and the origin is checked before parsing. The camera text was updated and a new photo-library text added in all ten locales; expo-image-picker is configured without a microphone. Mechanism assumptions disproved: image-picker cannot resize (manipulator added), expo-sharing cannot share a link or text (RN Share used), and Android open needs ACTION_VIEW (intent-launcher added). Landing point outside the claimed surface: app/console.tsx (+4 lines), where BridgeDeps are built. Native rebuild required; device checks are not verified.", "tests": "head c3ca88e: npx jest --ci --coverage exit 0, 46/46 suites, 935/935 tests (lib/shell/bridge 100% statements; file-handoff.ts 100% statements / 95.12% branches). Cold run, npx jest --ci --no-cache __tests__/components/ConsoleScreen.test.tsx: exit 0, 13/13. Ablations (ablation-replace.mjs wrap mode, mutation proven on disk, restore proven by blob equal to HEAD and an empty git diff HEAD). Size limit: handler openFile check deleted, red 'refuses a file over the size limit with the error envelope'; pickImage result check reduced, red 'refuses an image over the size limit after downscaling'; bootstrap pre-check set to false, red 'refuses a file over the size limit in the page' (via the 5 s timeout: the call posts and never settles); parseInbound openFile-only exemption deleted, red 'keeps the ordinary cap for every other method and for events'. No fetch: handler accepts a url key, red 'takes no URL'; fetch inserted in openFileBytes, red 'never fetches' plus the structural pin (and 6 more, fetch being undefined under jest); File.downloadFileAsync inserted, red exactly 'never fetches' and the structural pin. The first attempts at the last two were refused by the tool (replacement contained the anchor), were no-ops, and were re-anchored and re-run.", "gates": "local, on head c3ca88e, exit codes captured before any pipe: npx tsc --noEmit 0; npx eslint . --ext .ts,.tsx 0; npx jest --ci --coverage 0; cold ConsoleScreen 0; pnpm install --frozen-lockfile 0; control-byte grep over changed files: no hit. Exit codes recorded in scratchpad issue-147/gates.txt. CI check-runs on c3ca88e: 'Lint & Type Check' completed/success, 'Code Quality' completed/success. dispatch-gates.mjs not applicable (an objectstack tool; the mobile gates are the dispatch's list).", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "2 relay/stamped writes: (1) fleet-write relay stroke pr_create, dispatched as POST /repos/objectstack-ai/objectstack/dispatches; the run opened POST /repos/objectstack-ai/mobile/pulls -> #180; read back 11428 bytes sent and stored, identical. (2) post-stamped.mjs POST /repos/objectstack-ai/mobile/issues/147/comments (this report). Plus git push (not REST), and a first relay attempt refused at validation (draft key), zero writes.", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted, not filed — channel limit measured in Chromium 153 over CDP only; React Native event, Hermes, WKWebView and devices NOT MEASURED (no device in container); on-device timing at 8 MiB is part of the device checks", "carrier: none · noted, not filed — an Android photo taken while the OS kills the app is not delivered (getPendingResultAsync not wired); documented in Known limits", "carrier: none · noted, not filed — the refusal text says '8 MB' where the constant is 8 MiB", "carrier: none · noted, not filed — the first branch commit 6577439 carries the harness's model-named Co-Authored-By trailer; later commits use the model-free pair; history not rewritten", "carrier: none · noted, not filed — '需要你做的' is one action (rebuild, then run the device checks) carrying the four checks the dispatch named" ] }objectstack-fleet commentedon Nov 3, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
ACCEPT → PR #180 (head
c3ca88e). The seat landed it through the merge queue. A native rebuild is owed (five new Expo modules). The Console consumer card is filed in objectstack.objectstack-fleet commentedon Nov 3, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
Landed (seat landing). #180 merged through the merge queue at 03:03:18Z. The card closed
completedviaFixes #147;pm:dispatchedremoved.CI(jest) was still running when the queue merged, because the ruleset requires onlyLint & Type Check. The seat follows it to completion.