Skip to content

Releases: neulketing/openthumb

OpenThumb 1.3.0

Choose a tag to compare

@github-actions github-actions released this 30 Jul 03:55

Added

  • The sandbox image can be fetched at first run instead of bundled, which is
    what F-Droid requires: it builds every app from source and will not ship a
    tarball of several hundred precompiled executables it cannot rebuild. The
    projects that do get in — Termux, UserLAnd — all download theirs. Which path a
    build uses is decided by what the APK holds: the asset is used when present and
    the image is downloaded when absent, so nothing in gradle has to know and
    existing builds behave exactly as before.
  • The download is verified against a SHA-256 compiled into the app, and a
    mismatch deletes the file and fails. This is not belt-and-braces: without it,
    whoever can answer for that URL picks which native binaries land in the
    sandbox. An empty digest disables the download path entirely rather than
    accepting anything, so a build that forgot to set one fails loudly.
  • Resumable via HTTP Range, because 14 MB on a phone is long enough for Doze or
    a walk out of Wi-Fi range to interrupt, and restarting from zero turns a slow
    install into one that never finishes. A server that ignores the range and
    sends the whole file discards the partial bytes rather than splicing two
    copies together.
  • Downloading is a separate install state from Extracting. One is waiting on
    a network and can be retried or resumed, the other is waiting on the device; a
    single bar for both tells the user nothing about which is stuck.
  • scripts/publish-rootfs.sh builds the image, prints its digest, and refuses
    to publish while that digest disagrees with the constant in the app — an app
    shipping the wrong digest rejects its own image.

OpenThumb 1.2.0

Choose a tag to compare

@github-actions github-actions released this 30 Jul 02:35

Added

  • Sync no longer requires a Cloudflare account. tools/sync-server/ is the
    same backend as a single Python file on a machine you already own: SQLite
    where the worker uses D1, a directory where it uses R2, and no dependencies at
    all. Same three endpoints, same bearer token, same 64 KB inline limit, same
    last-write-wins rule, so the app cannot tell which one it is talking to and
    switching means changing a URL. 39 checks written from the worker's own source
    run in CI, because the thing worth testing is not that the server works but
    that the two cannot drift apart.
  • The server refuses to bind a non-loopback address without TLS rather than
    quietly putting the bearer token on the network in cleartext; the README gives
    three ways to reach it from a phone, in the order most people should try them.
    Blob filenames are hashed from kind and id, so an id — which is user data
    arriving from the network — never becomes a filesystem path.

OpenThumb 1.1.1

Choose a tag to compare

@github-actions github-actions released this 30 Jul 02:00

Fixed

  • The app could not start when an older build was still installed. The
    native-offload endpoint bound the abstract socket native-offload, and the
    Linux abstract namespace is global to the device rather than per-app — so
    whichever OpenThumb started first claimed it and the other died in
    Application.onCreate before drawing anything. Changing the application ID
    puts every existing user in exactly that state, because the new id installs
    beside the old rather than over it. The socket is now named after the
    application id. Verified on a Note8 with 1.0.x and this build running at the
    same time: both processes alive, no crash, bound as
    com.fug.openthumb.native-offload.

Added

  • Nothing the agent writes reaches another person without someone saying
    yes.
    The trigger engine had five gates — quiet hours, an active window, a
    per-rule cooldown, a concurrency cap, and a check that the notification can be
    replied to at all — and every one of them limits how often a reply is sent.
    None looked at what it says. So an agent that misread a message answered a
    real person, at speed, and the first anyone heard of it was the reply already
    in the thread. Replies now stop at a draft until the person whose name is on
    them decides.
  • The decision arrives where you already are. The draft comes as a
    notification with Send and Discard on it and an inline edit box, so correcting
    a draft and approving it is one gesture rather than two screens. It shows the
    reply in full rather than a summary — reading what is about to be sent is the
    entire point — and it cannot be swiped away, because dismissing a question is
    not an answer.
  • Three settings, written as consequences. Ask every time is the default,
    because a gate that ships off is not a gate. Ask except for chosen apps
    sends automatically only where you said to. Never ask is honest about when
    it is reasonable: when your rules answer nobody but you. A rule can also carry
    requireApproval, which only ever tightens — a rule marked that way waits even
    in Never ask, because whoever marked it knew something the global setting
    does not.
  • A record of what it tried to send, not only what it sent. Every draft and
    every decision — sent, edited, discarded, expired — appends to
    outbound-ledger.jsonl and is readable in the app. A log holding only
    successes cannot answer the question people actually have.
  • The decision itself is a pure function with no Android in it, and nine checks
    cover the cases where being wrong is expensive: an unreadable stored setting
    falls back to asking rather than sending, com.allowed.evil does not inherit
    com.allowed's permission, a rule's own flag holds a reply under every mode
    including Never ask, and a queued draft keeps the notification key without
    which an approved reply can never find its conversation again. Verified to
    have detection power by removing the tightening rule — exactly that check
    fails.
  • Ask except for chosen apps can now choose apps. The mode was selectable
    with no way to put anything on the list, which made it behave exactly like
    Ask every time — a setting that silently does nothing. Picking it now
    reveals the installed apps with checkboxes, and only apps with a launcher
    entry are offered, since a rule can fire on a bank alert but there is nowhere
    to send a reply.
  • minis://settings/triggers opens the notification-trigger screen, and the
    approval notification links there. The screen had no deep link at all, so
    tapping a draft did nothing and the only way to see how many were waiting was
    to find the app on the home screen.
  • The approval settings scroll. On a Note8 the dialog was taller than the
    screen: Never ask was cut mid-sentence, and the expiry setting and the
    pending list below it could not be reached by any gesture.
  • Drafts expire after 30 minutes by default. An answer to a message from hours
    ago fits the conversation worse than no answer at all.

Alpine rootfs 3.21.3 (aarch64, python3)

Pre-release

Choose a tag to compare

@neulketing neulketing released this 30 Jul 03:36

Sandbox image fetched at first run by builds that do not bundle it.

OpenThumb 1.1.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 18:23

Use 1.1.1 instead. This build binds the abstract socket
native-offload, which is device-global rather than per-app, so if an
earlier OpenThumb is still installed — which the note below tells you to
expect — whichever starts first claims the name and the other dies in
Application.onCreate before drawing anything. Fixed in 1.1.1 by naming
the socket after the application id. On a device with no earlier install
this build runs, but there is no reason to pick it over 1.1.1.

The application ID changes to com.fug.openthumb, so 1.1.0 installs beside
1.0.x rather than over it.
Android treats a different application ID as a
different app entirely. Uninstall the old one after checking anything you want
to keep — settings and provider credentials do not carry across. Doing this now
is deliberate: the ID is fixed forever once the app is on Google Play, and the
product is published under FUG. The app is still called OpenThumb.

Added

  • openthumb-fetch in the sandbox — fetch a page and be told whether what came
    back is actually the page. A WAF challenge, a consent wall and a bot
    interstitial all arrive as HTTP 200 with an HTML body, and an agent that
    trusts the status code summarises the interstitial as if it were the article.
    Seven checks decide: status semantics, structural WAF markers, caller-supplied
    stub sizes, JSON awareness, CSS selectors as positive proof, soft markers with
    the Akamai sensor cookie, and content density.
  • A failed fetch never returns a bare failure. It names untried_routes — the
    escalation steps the fetcher structurally could not take — so "the site blocks
    us" cannot be reported while the phone's own WebView, which holds the real TLS
    stack and cookie jar, has not been tried. 429 is classified as transient
    rather than a wall, which is the single most common cause of a premature
    give-up. An empty list is the permission to fail honestly.
  • The ladder is inverted from the upstream desktop tool: here the browser is the
    first escalation and cheap HTTP is the probe under it, because on a phone the
    WebView is the real client.
  • The fetcher returns readable text, not markup. A measured Threads profile
    is 861KB of HTML carrying 1.6KB of words — 516x. Returning the raw body would
    spend an on-device model's whole context on one page, and truncating instead
    drops whatever was past the cut, usually the article. Three sources compete
    and the most complete wins: schema.org articleBody (which carries the text
    even when the visible DOM is a JavaScript shell), the <article>/<main>
    block with navigation and footers removed, or all visible text. --raw still
    returns the HTML for the cases that need it.
  • A fetch interrupted by Doze resumes instead of restarting. Android
    suspends background work, and a fetch that walks several URL variants is long
    enough to be killed partway — with no record of what was already tried, the
    next run starts over and dies at the same place forever. Each attempt is now
    appended to a journal as it finishes, and a resumed run skips what the journal
    covers. Measured: 2.68s for a first pass, 0.14s to resume it. The journal
    expires after ten minutes so a lifted block is retried, and --forget starts
    clean.
  • Cookies are kept per host, so clearing a challenge once pays off. Hand the
    WebView's cookies in with --cookies after browser_use gets past a
    challenge and later fetches to that host start already solved. Storage is
    keyed by host and stores the host inside the file as well, so a cookie can
    never be attached to a request for a different site; the files are 0600
    because session cookies are credentials.
  • It degrades honestly when it cannot start. The Alpine rootfs ships 15
    packages and python3 is not one of them, and on a Galaxy Note8 apk cannot
    install it — network is fine (busybox wget fetches the same mirror over
    HTTPS), but apk's own fetch fails with IO ERROR over http and https alike and
    with both IPv6 and IPv4 nameservers. The wrapper tries apk add, and when
    that fails prints a JSON object saying so and escalating to browser_use,
    stating outright that this is a runtime problem on the phone rather than the
    site refusing the request. Verified on the device: valid JSON, exit 2. The
    same limitation applies to the inherited minis-mcp-cli, which installs
    itself the same way.
  • python3 is baked into the rootfs at build time, since it cannot be added
    later. scripts/rootfs_add_packages.py resolves an Alpine package's full
    dependency closure and unpacks it into the image using nothing but curl and
    the standard library — not apk, which is a Linux binary a macOS build host
    cannot run, and not Docker, which would make the shipped image depend on
    whether a daemon happened to be running. Measured: 3.7MB to 14.6MB for
    python3 3.12.13 and its 17 dependencies, taking the APK from 33MB to about
    44MB. Verified on a Note8: python3 runs, all 69 checks pass on the phone, and
    openthumb-fetch https://example.com returns the page with the text
    extracted. Details in docs/sandbox-python.md.
  • Ported from insane-search (MIT, github.com/fivetaku/insane-search). Pure
    standard library — CSS selectors are matched with html.parser rather than
    BeautifulSoup so nothing has to be installed on the phone. curl_cffi is used
    for TLS impersonation when importable; its absence costs capability, never
    correctness. scripts/test-fetch.sh runs 69 offline checks, wired into CI
    ahead of the build so a broken verdict fails in seconds rather than after the
    NDK compile.

OpenThumb 1.0.6

Choose a tag to compare

@github-actions github-actions released this 29 Jul 16:17

Installing over 1.0.5 or earlier will fail — uninstall first. Every release
up to 1.0.5 was signed with an Android debug key, because the signing config
falls back to one whenever the release keystore is absent. 1.0.6 is the first
build signed with OpenThumb's own key, and Android refuses to replace an app
with a build carrying a different certificate. Your settings and provider
credentials do not survive the uninstall; export or re-enter them. This is a
one-time break — 1.0.7 onward upgrade normally.

Added

  • scripts/bump-version.sh — moves versionName, versionCode, the CHANGELOG
    heading, the F-Droid changelog and the version on the landing page together.
    They were four hand edits before, and fastlane still carried a changelog for
    versionCode 21 while the app shipped 26. scripts/test-bump-version.sh
    checks that all of them move.
  • .github/workflows/release.yml — a tag push builds the signed APK, publishes
    the release with the notes read out of this file, and mails them. It checks
    that the tag agrees with the versionName it was cut from, and the tag is
    annotated because git push --follow-tags skips lightweight ones — a
    lightweight tag stays local and the release never fires.
    release-apk.yml and release-announce.yml are folded into it: both hung off
    release: published, which a release created with GITHUB_TOKEN does not
    raise, so automating release creation would have silently stopped both.
  • The landing page links the changelog, and shows which version is current.

Fixed

  • Release APK could be published signed with a throwaway key. Without the
    keystore secrets build.gradle.kts falls back to the debug config, and CI
    runners generate a fresh debug key per run — so a published build could not
    be upgraded onto by anyone holding an earlier one. The workflow now signs
    from secrets when they exist and fails rather than attaching a debug-signed
    APK.

OpenThumb 1.0.5 — 이미 있던 기능을 말하기

Choose a tag to compare

@neulketing neulketing released this 29 Jul 15:40

코드가 아니라 문서 패치. 포크가 이미 하고 있었지만 어디에도 적혀 있지 않던 것 3가지.

Changed

  • README가 개인 사용자부터 말합니다. 기존 헤드라인은 "One host. Many phones" — 폰 한 대 가진 사람이 남의 사용 사례를 읽는 구조였습니다. 서랍 속 폰 되살리기와 "메시지가 온 그 대화창에 답장"을 먼저 서술하고 함대는 그다음으로.
  • 계정 로그인이 문서화됐습니다. 앱에는 Claude·OpenAI·Gemini·xAI·Kimi·OpenRouter·Antigravity OAuth가 이미 구현돼 있는데 모든 사용자 대상 문서가 "your own API keys"만 말하고 OAuth 언급이 0회였습니다. 이미 결제 중인 구독으로 돌릴 수 있다는 걸 알 방법이 없었습니다.
  • RPC 인증 서술을 사실로 정정. "인증 없음"이라 적혀 있었는데 1.0.4부터 사실이 아닙니다.

Added

  • docs/recipes.md — 바로 쓰는 트리거 규칙 모음(패키지명·매칭·프롬프트 포함): 카톡/문자 답장, 회의시간 자동응답, 택배 알림 정리, 은행 알림 가계부 기록, 그리고 그것들을 읽어 아침 요약. 기존엔 규칙 화면이 빈 목록 + 자유 입력란으로 열려서, 뭘 써야 하는지 아는 사람만 쓸 수 있었습니다.

OpenThumb 1.0.4 — 디버그 서버 인증 강화

Choose a tag to compare

@neulketing neulketing released this 29 Jul 15:40

포크 감사에서 남아 있던 마지막 항목(디버그 서버 자체 인증)을 닫습니다.

Security

  • 디버그 서버가 loopback 포함 모든 연결에 토큰을 요구합니다. 기존엔 127.0.0.1을 "adb forward = 개발자 본인"으로 간주해 면제했는데, 실제로는 INTERNET 권한을 가진 폰 내 아무 앱이나 loopback 소켓을 열 수 있습니다. 이 RPC는 provider.export(저장된 API 키)·debug.readFile·샌드박스 명령 실행을 노출합니다. adb는 run-as로 토큰을 읽을 수 있어 그대로 동작하고, 같이 설치된 앱은 그 권한이 없습니다. 노트8 실측: 인증 없는 provider.export → 401, openthumb-fleet status → 정상.
  • Access-Control-Allow-Origin: * 제거. 폰 브라우저가 연 임의 페이지가 이 RPC를 교차출처로 호출하고 응답까지 읽을 수 있었습니다.

Changed

  • openthumb-fleet이 단말별 토큰을 run-as로 읽어 매 호출에 실어 보냅니다($OPENTHUMB_FLEET_DIR/tokens/에 캐시). 연관배열 대신 파일 — macOS는 아직 bash 3.2라 declare -A가 없고 set -u에 걸립니다.

OpenThumb 1.0.2 — crash·DoS·답장 안전성

Choose a tag to compare

@neulketing neulketing released this 29 Jul 15:40

포크 자체 코드 전수 감사에서 나온 결함 수리. 크래시 경로 2건과 상대방 대화창에 원시 JSON이 발송될 수 있던 경로 1건 포함.

Fixed

  • 트리거 실행 실패가 앱을 죽이지 않습니다. 실행 코루틴에 catch가 없고 스코프에 핸들러도 없어, Android 12+에서 포그라운드 서비스 시작이 거부되는 것만으로 프로세스가 죽고 알림 리스너까지 함께 사라졌습니다.
  • 답장이 원시 페이로드를 보내지 않습니다. assistant 메시지 파싱이 실패하면 폴백이 저장된 JSON 문자열을 그대로 반환했고, 그게 상대 대화창으로 발송됐습니다. 이제 파싱 실패 시 아무것도 보내지 않습니다.
  • 답장 실패가 실패로 기록됩니다. 세션만 있으면 ok로 남아, 사용자는 "실행됨"을 보는데 상대는 아무것도 못 받는 상태였습니다.
  • 답장 불가 알림은 건너뜁니다. canReply가 구현돼 있는데 호출되지 않아, 은행·택배 푸시가 최대 10분짜리 에이전트 실행과 쿨다운을 소모하고도 보낼 곳이 없었습니다.
  • 디버그 서버가 악의적 요청에 죽지 않습니다. Content-Length를 무제한 신뢰해 2GB 헤더 한 줄이 OOM을 유발했고, 이는 Error라 catch에 안 잡혀 accept 루프까지 끌고 죽었습니다(앱 재시작 전까지 서버 불능). 32MB 상한 + 커넥션별 supervisor.
  • 업스트림 태그명이 CI에서 코드를 실행할 수 없습니다. contents: write 권한 잡의 run:에 업스트림 릴리스 태그가 직접 삽입되고 있었습니다.
  • ensure_forward 포트 계산 수리. bash가 local 실행 전에 모든 워드를 확장하므로 호출자의 index를 읽고 있었습니다 — 변수명 우연에만 의존하던 상태로, 이름 하나만 바뀌면 전 단말 RPC가 0번 기기로 가면서 출력엔 각 단말 serial이 붙었을 것입니다.

Changed

  • CI가 포크 자체 유닛 테스트를 실행합니다(34건 전부 통과). 이전엔 한 번도 실행되지 않았습니다. 상속된 실패 3종은 이름으로 제외 — docs/upstream-test-baseline.md.
  • ShellCheck를 --severity=warning으로. error에서는 fleet 스크립트의 유일한 실결함(SC2318)이 걸러지고 있었습니다.

OpenThumb 1.0.1 — security patch

Choose a tag to compare

@neulketing neulketing released this 29 Jul 15:40

v1.0.0을 설치했다면 교체하세요. 그 릴리스의 유일한 에셋이 debug 빌드였고, debug 빌드는 JSON-RPC 디버그 서버를 띄웁니다. 이 서버는 0.0.0.0에 바인드되고 루프백 호출에는 토큰 검사를 면제하므로, 폰의 INTERNET 권한을 가진 아무 앱이나 provider.export를 호출해 저장된 API 키를 읽을 수 있었습니다.

Fixed

  • 릴리스가 더 이상 debug APK를 배포하지 않습니다. CI가 release 변형을 빌드하고, release-apk 워크플로가 발행 시 첨부합니다.
  • 비-ASCII 요청 본문이 디버그 서버를 멈추게 하던 문제. Content-Length는 바이트인데 CharArray로 읽어, 한글 1자=3바이트라 읽기가 끝나지 않고 30초 소켓 타임아웃으로 죽었습니다(빈 응답, 에러 없음). 갤럭시 노트8/Android 9에서 검증.
  • 프로바이더 연결 테스트가 정상 엔드포인트를 실패로 오진하던 문제. effectiveBaseURL이 이미 적용한 /v1 접미를 프로브가 다시 붙여, /paas/v4 같은 루트가 /v4/v1/models로 조회됐습니다.

Added

  • 알림이 온 대화에 에이전트가 직접 답장합니다(규칙별 옵트인, 기본 off). 알림의 빠른답장 액션을 쓰므로 카카오톡·SMS·LINE·텔레그램 등 해당 액션을 제공하는 메신저가 곧바로 양방향 채널이 됩니다. 안드로이드 공개 알림 규약만 사용 — 접근성 주입 없음.
  • 요청 본문 파싱·프로브 URL 회귀 테스트 추가.

릴리스 APK는 release-apk 워크플로가 빌드해 곧 이 릴리스에 첨부됩니다.