Skip to content

Add inbox control and read-only mode - #27

Merged
kongesque merged 11 commits into
mainfrom
feat/inbox-control
Nov 7, 2026
Merged

kongesque merged 11 commits into
mainfrom
feat/inbox-control

Conversation

@kongesque

Copy link
Copy Markdown
Owner

Summary

The v0.6.0 "inbox control and agent safety" release: commands for everyday inbox work, plus a read-only switch so scripts and agents can't change anything.

  • Read-only mode. --read-only before the command, or LINE_CLI_READ_ONLY=1, refuses send, read, react, unsend, invites accept/reject, login, logout, auth migrate, and update installs. It stops before any prompt, stdin, lock, credential access, request sequence, or network call, using the new JSON code read_only. session.Manager.ReadOnly adds a second check on mutations, sequence reservation, and both login paths, so a command that forgets its own check still can't change LINE. An unrecognized env value fails every command instead of being ignored.
  • line read CHAT [--message ID] sends a read receipt. It is now the only path to sendChatChecked, and an AST test enforces that. Without --message, it marks read up to the newest message from someone else. Your own messages are refused, matching Chrome.
  • line chats --unread filters on your machine. Chats with unknown or malformed counts are excluded, and the legacy JSON shape is unchanged.
  • line invites [accept|reject GROUP_ID] lists pending group invitations and changes one after confirming it is pending. Accepting never registers or fetches a group key.
  • line doctor [--offline] [--json] runs eight read-only checks: version, install, storage, session, line_auth, e2ee_keys, clock, and watch. Each non-passing check has a next step. Output contains no tokens, keys, MIDs, names, email addresses, or messages, and home paths are shown as ~/….
  • line completion bash|zsh|fish|powershell prints static scripts generated from one spec.
  • UX polish. Help goes to stdout, line help COMMAND works, and help uses grouped --flag lists with examples. Typos get one-line "did you mean" errors. In a terminal, a name that doesn't match exactly opens a chooser; scripts get an error listing up to five candidate IDs. watch prints readable lines in a terminal.
  • Docs. The changelog moves Unreleased into [0.6.0] with upgrade notes. The JSON reference is updated, and the ja/th/zh-TW READMEs and guides are aligned with English anchors kept.

Protocol evidence

docs/protocol/endpoints/read-receipts.md and chat-invitations.md record a static read of the public LINE Chrome extension 3.7.2. Its SHA-256 matches the package cited in the existing protocol docs. No login, capture, or live call was involved.

  • Chrome sends sendChatChecked as [reqSeq, chatMid, lastMessageId, 0]. reqSeq comes from a nonzero, incrementing 31-bit counter, so the persisted-sequence rule applies as is.
  • Accept and reject each take one {reqSeq, chatMid} struct. Neither is followed by a key or receipt call in Chrome.
  • getChats doesn't return who sent an invitation, so line invites doesn't show an inviter.

For reviewers

  • Behavior changes:
    • Help moved from stderr to stdout.
    • watch attached to a terminal is now readable text; piped output and --json are unchanged byte for byte. Scripts that run watch attached to a terminal need --json. The changelog has upgrade notes for both.
  • Unchanged contracts: JSON error codes, exit codes, and piped output. An unmatched chat name is still operation_failed; only its message now lists candidates. Switching that to invalid_arguments is an open question.
  • Mutation rules: every new change reserves a persisted request sequence, is attempted once, and records a typed fault.Mutation outcome. Nothing retries a mutation automatically.
  • Supporting changes:
    • pkg/line.Client.ResponseClock records the gateway Date header.
    • session.ErrWatchBusy replaces the plain watch-busy error, with the same message.
    • update.Service.Inspect is the offline part of Check.
    • The legacy SendChatChecked that hardcoded sequence 0 now requires a sequence.

Validation

  • Passed on macOS arm64 with Go 1.27.3:
    • ./build.sh, go test -race ./internal/... ./cmd/line ./pkg/line/... ./pkg/e2ee, goimports, go vet, staticcheck, and git diff --check, before every commit.
    • go test -count=1 -timeout=20m ./pkg ./pkg/ltsm.
    • ./scripts/check-toolchain.sh.
    • ./scripts/security-check.sh ./bin/line: govulncheck v1.9.0 found 0 reachable vulnerabilities in source or binary. go.mod/go.sum are unchanged from main.
    • Both fuzz targets for 15s each, and sh -n scripts/install-release.sh.
    • Cross-build and vet for linux and windows (amd64/arm64) and darwin/amd64.
  • Completion: bash 3.2 and zsh 5.9 scripts are run in tests. fish and PowerShell are structure-tested only, because neither shell was available; please try them.
  • Not tested against live LINE; all responses are synthetic. line doctor was not run against a real Keychain.
  • Not yet run: CI on this commit (native Linux Secret Service, Windows DPAPI, Windows tests) and the branch packaging rehearsal.
  • Translations haven't had native-speaker review.

`line --read-only COMMAND` or LINE_CLI_READ_ONLY=1 refuses send, react,
unsend, login, logout, auth migrate, and update installs. The refusal
comes right after option parsing, so help still works, and before any
prompt, stdin, attachment, lock, credential access, request sequence, or
network request. It uses the new JSON error code read_only at stage
preparation, and message actions report not_attempted.

Global options are now parsed by one shared function in any order, each
at most once. An unrecognized LINE_CLI_READ_ONLY value fails every
command instead of being ignored, and --read-only=false cannot turn the
environment switch off.

As defense in depth, session.Manager.ReadOnly makes MutateContext,
ReserveSequence, and both login paths fail with the same typed error, so
a command that forgets its gate still cannot change LINE.
`line read CHAT [--message ID]` marks a chat as read; others in the
chat see "Read". It is the only command that calls sendChatChecked.
History, watch, download, members, chats, and pickers stay receipt-free,
and an AST regression test keeps it that way.

Static evidence from the public LINE Chrome extension 3.7.2 (recorded in
docs/protocol/endpoints/read-receipts.md) shows Chrome sends a nonzero
incrementing request sequence with [reqSeq, chatMid, lastMessageId, 0].
The CLI reserves a persisted sequence, attempts once, and records a typed
outcome. The old helper that hardcoded sequence 0 now requires one.

Without --message the CLI picks the newest message from someone else
among the latest 50, skipping SKIP_BADGE_COUNT messages like Chrome. It
neither decrypts nor fetches keys. With --message it verifies the ID with
the bounded action lookup and refuses your own messages. Guided mode
lists unread chats first and asks before sending. --read-only blocks it.

Also records the static evidence for group invitations used next.
`line chats --unread` keeps chats whose unread count, as reported by
LINE, is above zero. The filter runs on the client after fetching,
because the server's unreadOnly option has no protocol evidence yet.
Missing or malformed counts are excluded rather than treated as unread.

It combines with --search, --limit, --all, --show-ids, --json, and
--details. --limit applies after filtering, and the legacy JSON row shape
is unchanged. Listing unread chats never sends a read receipt.
`line invites` lists pending group invitations from getAllChatMids with
best-effort names from getChats. If names can't be loaded, invitations
are still listed, with the name unavailable. `line invites accept|reject
GROUP_ID` takes a full group ID and confirms it is pending before
reserving a persisted request sequence. Each change is attempted once
and records a typed accept_invitation or reject_invitation outcome. In a
terminal, `line invites` lets you choose an invitation, then accept,
reject, or skip.

Accepting never registers or fetches a group key and never sends a read
receipt, matching the Chrome extension call sites. The inviter isn't
shown because no request the CLI makes returns it. --read-only blocks
accept and reject but still lists invitations.

Adds strict context variants for getAllChatMids, acceptChatInvitation,
and rejectChatInvitation to pkg/line and session.API.
`line doctor [--offline] [--json]` runs read-only checks with stable IDs:
version (and latest release unless --offline), install (every line on
PATH and which one runs), storage, session, line_auth (one profile read,
never a new login), e2ee_keys (count only), clock (skew against the LINE
response Date header), and watch (lock held or free, checkpoint present).
Each check reports pass, warn, fail, or skipped, and every non-pass
result has a next step. It exits 1 only when a check fails.

The report is meant for GitHub issues. It never includes tokens, keys,
MIDs, names, email addresses, messages, or server responses, and it shows
paths inside the home folder as ~/... It works logged out, with headless
storage, and in --read-only mode.

Supporting changes: pkg/line records the gateway Date header for clock
checks, session.ErrWatchBusy identifies a running watcher (same message),
and update.Service.Inspect reports the installation method offline.
`line completion bash|zsh|fish|powershell` prints a static script for
commands, subcommands, options, and fixed values such as reactions and
--error-format. All four scripts come from one spec in Go, and the
command reads no session and makes no network requests.

Drift tests compare the spec with every command's --help (both
directions) and with the command list in `line help`, and check that
each script covers each flag. The bash and zsh scripts are also run in
tests on macOS and Linux.
--help, -h, and the new `line help COMMAND` print to stdout and exit 0,
so `line send --help | less` works. A shared usage printer replaces Go's
flag dump. It spells flags --flag, groups them by purpose (for send:
message, target, delivery, output), and adds examples to every command.
logout and auth now explain what they do.

Errors are one line on stderr with a pointer back to help instead of a
usage dump. Unknown commands and options suggest the closest match
(`line chat` -> line chats, --limt -> --limit), global options given
after the command get a hint to move them, and option values are never
echoed. JSON error codes and exit codes are unchanged.
In a terminal, a chat name that matches no chat exactly, or several,
now opens a chooser of chats and contacts whose names contain it
(case-insensitive). The user's pick is the explicit selection. The
chooser runs without the session lock, and the account is checked again
before the command continues.

Without a terminal (scripts, --json, --stdin), the command still fails,
but the error lists up to five similar names with full IDs. A single
similar match is never chosen automatically, so sends and other changes
still need a full ID, a unique exact name, or a choice the user made.
The JSON error code is unchanged.
When stdout is a terminal and --json is not given, `line watch` prints
one line per message: time, chat, sender, and text or attachment type.
Chat and sender names are looked up once and cached, with IDs as the
fallback, and remote text is sanitized. Other events are hidden unless
--verbose is given. Hidden events still advance the resume position but
don't count toward --limit.

Piped or redirected output and --json keep the existing NDJSON stream
byte for byte. The watcher gains an optional Emit hook. When it is set,
session state is reloaded after output, so a token refresh during a
name lookup is never overwritten by the checkpoint save.
Move Unreleased into [0.6.0] with upgrade notes for --read-only and
LINE_CLI_READ_ONLY, explicit read receipts through `line read`, readable
watch output in a terminal, and help on stdout. Add the new commands to
the README command tables and automation sections.

Align the Japanese, Thai, and Traditional Chinese READMEs and guides:
new sections for read-only mode, marking chats read, group invitations,
doctor, and shell completion, with the English anchors kept. Update the
unread filter, chat chooser, readable watch, help, timeout table, JSON
tables, troubleshooting, and limitations. The release checklist and test
notes now describe this candidate.
@kongesque kongesque changed the title Add inbox control and read-only mode for v0.6.0 Add inbox control and read-only mode Nov 7, 2026
The test passed --timeout 1ns and expected the deadline to fire before
the command finished. On Windows CI the coarse timer let the command
complete first, so it failed. Now one case uses an already-expired
context, and the other checks the --timeout flag with a lock hook that
waits until the deadline has fired. Both still assert that no read
receipt is sent.
@kongesque
kongesque merged commit 127bdd3 into main Nov 7, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant