Skip to content

Bridge: pickImage(), openFile() and share() — reliable photo capture, attachment download and sharing #147

Closed

Description

@hotlong

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

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.

Activity

  1. added
    enhancementNew feature or request
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; on a PR: awaiting the maintainer's review
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    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):

    • 现在下载靠 WebView 默认行为,iOS 与安卓表现不一致。re-check:grep -n "Downloads and file inputs" docs/HYBRID-SHELL.md
    • pickImage 与 share 不碰登录态,不在本冲突内。只有 openFile 冲突。

    选项:

    选项 做什么 客户可感知后果 工作量
    A 拆卡。pickImage 与 share 照做。openFile 改为 openFile({ base64, name, mime }):Console 用自己的登录态取文件,交给原生预览或分享;原生不碰 cookie 附件能打开、能分享;超大文件受桥接载荷上限约束(需定上限) mobile 车道 · 1 PR + Console 消费卡 · 无先例
    B 照卡面:原生读 WebView cookie,用 expo-file-system 同源下载 大文件也流畅;壳开始持有并使用会话凭据 mobile 车道 · 1 PR · 无先例
    C 不加 openFile,改为接管 WebView 自带下载事件(安卓 DownloadListener、iOS 下载代理),交 OS 打开 Console 无需改动;是否带上会话取决于平台,需逐平台实测 mobile 车道 · 1 PR · 无先例

    业务含义: A 是网页把文件递给手机,由手机负责打开。B 是把网站钥匙配一把给手机,让它自己去取。C 是手机替浏览器接下下载动作。

    四轴(业务立场):

    • 长远合理性(权重最高):「壳无凭据」是本仓 2.0 重建的核心不变量。B 首开先例,之后每个原生能力都可能援引。A 守住不变量。C 也守住,但依赖平台下载 API 的行为差异。
    • 业务拉动: 附件下载是真实用户场景(Console 文件字段),拉动成立。
    • 防 AI 犯错: B 下,AI 以后写原生代码会把「拿 cookie 调 API」当成合法模式复制,是静默扩权。A 下凭据只在网页一侧,越界会被结构挡住。
    • 创业阶段不扩散: A 与 C 不新增凭据面;B 新增一个需要长期审计的凭据通道。
    os-decision-facets
    ① 长远:A 守「壳无凭据」不变量;B 破例开先河
    ② 拉动:附件下载是真实场景,三案都满足
    ③ 防错:A 凭据单侧,结构隔离;B 让「原生拿 cookie」成为可复制模式
    ④ 不扩散:A/C 无新凭据面;B 新增凭据通道
    Prior rulings read: openFile|expo-file-system → 3 hits (#147 itself, #144 rebuild PR, #10 old 1.x); ADR none; thread: none
    

    推荐:A。

    • 置信缺口:base64 过桥的大小上限未实测。若常见附件超过上限,再议 C。
    • 不取 B:破坏 2.0 的核心不变量。
    • 不取 C:平台下载行为差异未实测,不宜作首选。
    • 自检:只看①选 A;②③④ 是否翻转:否。

    裁后执行: 席位在卡上记改法,把 openFile 的 Ask 改为 A 的形状,整卡派 os-dev。验收后在 objectstack 立 Console 消费卡。

    Analysis-tier: default (seat tier)

  3. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    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() 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.

  4. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; on a PR: awaiting the maintainer's review
    pm:queueReady for the PM dispatch loop
    on Nov 3, 2026
  5. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    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.

  6. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    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"
      ]
    }
  7. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    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.

  8. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    Landed (seat landing). #180 merged through the merge queue at 03:03:18Z. The card closed completed via Fixes #147; pm:dispatched removed.

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions