{"id":"0949e593-8c7e-4a68-8c0e-5c89dd3c3230","entityType":"agent","slug":"clawhub-mosluce-line-oa-chat-send","name":"line-oa-chat-send","canonicalUrl":"https://www.xpersona.co/agent/clawhub-mosluce-line-oa-chat-send","canonicalPath":"/agent/clawhub-mosluce-line-oa-chat-send","generatedAt":"2026-10-11T20:57:39.825Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-11T17:04:05.712Z","emptyReason":null},"description":"Send explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control. Skill: line-oa-chat-send Owner: mosluce Summary: Send explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control. Tags: latest:0.1.6 Version history: v0.1.6 | 2026-07-29T08:06:26.333Z | user v0.1.6 — first clean publish; the skill itself is unchanged since","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s17934mwfw7y691b30bz9n5hhs8aqhhd:line-oa-chat-send","sourceUrl":"https://clawhub.ai/mosluce/line-oa-chat-send","homepage":"https://clawhub.ai/mosluce/skills/line-oa-chat-send","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/mosluce/line-oa-chat-send","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/mosluce/skills/line-oa-chat-send","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":60,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Send explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a tem"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T17:04:05.712Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T17:04:05.712Z","emptyReason":null},"stars":null,"forks":null,"downloads":1022,"packageName":null,"latestVersion":"0.1.6","tractionLabel":"1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T17:04:05.629Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T17:04:05.712Z","lastCrawledAt":"2026-10-11T17:04:05.629Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T17:04:05.629Z","lastVerifiedAt":null,"highlights":[{"version":"0.1.6","createdAt":"2026-07-29T08:06:26.333Z","changelog":"v0.1.6 — first clean publish; the skill itself is unchanged since 0.1.4 No behaviour change. SKILL.md, references/, and every script the skill runs are byte-identical to 0.1.4. Nothing new to run. 0.1.5 does not exist for this skill. It was published to a skill named after a temporary directory, because the CLI derives the slug from the folder name and the workflow had started staging into `mktemp -d`. The run reported success and every check passed -- all of them verifying correct things about the wrong destination. This release is the first with the publish path fully corrected: slug and name are passed explicitly, so identity no longer depends on where files sit on disk, and the response is checked against the expected slug so a mispublish is a red run rather than a silent one. version comes from the git tag rather than the CLI's \"next patch\" default, which had matched by coincidence and would have diverged at the first minor bump. the changelog is the annotated tag message, guarded on tag object type because a lightweight tag's contents fall back to the commit message. the package excludes openspec/ and .claude/, 69 files down to 31, and the staged tree is link-checked before upload so that trimming it cannot ship dead links. only release tags publish, and checkout runs on Node 24.","fileCount":31,"zipByteSize":62814},{"version":"0.1.4","createdAt":"2026-07-29T06:55:17.707Z","changelog":"**Added containerized test and runtime environment, script improvements, and documentation cleanup.** - Introduced a container-based test environment with supporting scripts and documentation (`containers/`). - Added and documented a one-step environment check (`scripts/doctor.sh`) and clarified setup instructions. - Revised and trimmed skill documentation for clarity, with a simplified quick start and reference guide. - Improved handoff flow robustness: now verifies and only issues valid, confirmed noVNC URLs. - Removed outdated or redundant files; added contribution guidelines.","fileCount":56,"zipByteSize":123540},{"version":"0.1.3","createdAt":"2026-07-28T15:43:59.265Z","changelog":"line-oa-chat-send 0.1.3 changelog - Clarified and strengthened guidance around temporary noVNC handoff usage for LINE login or reauthentication: it now requires explicit user request and enforces a strict TTL. - Added new security boundary notes highlighting risk and operational constraints for noVNC handoff. Clearer instructions for URL secrecy, TTLs, and handoff revocation. - Updated SKILL.md and README sections to reflect refined security model and operational process for temporary browser handoff. - Removed obsolete or redundant documentation file (skill-card.md).","fileCount":10,"zipByteSize":20582},{"version":"0.1.2","createdAt":"2026-07-28T10:50:18.095Z","changelog":"- Added additional security checks to the VNC/noVNC handoff: now verifies the public URL returns a valid noVNC page and confirms the browser title before sharing with the user. - Improved handling of Xvfb display selection by checking if the default display is in use and allowing safe selection of alternate private displays. - Updated SKILL.md documentation with the new verification steps for bearer URLs and display assignment recommendations. - Removed deprecated or no longer needed script files, consolidating setup processes.","fileCount":10,"zipByteSize":19330},{"version":"0.1.1","createdAt":"2026-07-27T22:09:25.889Z","changelog":"- Added startup scripts: `start_line_oa_chromium.sh` (for secure Chromium session handling) and `start_line_oa_vnc_handoff.sh` (for protected VNC/noVNC user login handoff). - Expanded documentation for secure session setup, including explicit operator- and user-facing instructions for profile, runtime, and GUI/VNC/noVNC/tunnel flows. - Updated `setup_line_oa_runtime.sh` and environment variable usage for more robust, configurable, and private runtime and profile management. - Detailed secure provisioning and shutdown procedures to prevent credential exposure, widen operator flexibility, and strengthen session discipline. - Clarified step-by-step process for both interactive user authentication and safe automation, covering both environment bootstrapping and resource cleanup.","fileCount":10,"zipByteSize":17278},{"version":"0.1.0","createdAt":"2026-07-27T09:13:36.808Z","changelog":"line-oa-chat-send 0.1.0 – Initial Release - Send and verify messages to a specific recipient in a LINE Official Account Chat via an authenticated, persistent Chromium session. - Requires user to complete authentication in a protected, interactive browser before sending. - Uses Playwright to connect to a local Chromium CDP endpoint without handling any credentials directly. - Ensures safe environment setup for browser and Python runtime isolation; does not create or manage browser profiles. - Provides CLI utilities for recipient/message selection, with safeguards against ambiguous recipients and accidental sends. - Only sends messages explicitly authorized by the user and verifies delivery in the chat transcript.","fileCount":8,"zipByteSize":10581}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17934mwfw7y691b30bz9n5hhs8aqhhd:line-oa-chat-send","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-mosluce-line-oa-chat-send/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-mosluce-line-oa-chat-send/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-mosluce-line-oa-chat-send/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-mosluce-line-oa-chat-send/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-mosluce-line-oa-chat-send/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-mosluce-line-oa-chat-send/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T20:57:39.821Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-mosluce-line-oa-chat-send/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-mosluce-line-oa-chat-send/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-mosluce-line-oa-chat-send/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-mosluce-line-oa-chat-send/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-11T17:04:05.712Z","emptyReason":null},"readme":"Skill: line-oa-chat-send\n\nOwner: mosluce\n\nSummary: Send explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control.\n\nTags: latest:0.1.6\n\nVersion history:\n\nv0.1.6 | 2026-07-29T08:06:26.333Z | user\n\nv0.1.6 — first clean publish; the skill itself is unchanged since 0.1.4\n\nNo behaviour change. SKILL.md, references/, and every script the skill\nruns are byte-identical to 0.1.4. Nothing new to run.\n\n0.1.5 does not exist for this skill. It was published to a skill named\nafter a temporary directory, because the CLI derives the slug from the\nfolder name and the workflow had started staging into `mktemp -d`. The\nrun reported success and every check passed -- all of them verifying\ncorrect things about the wrong destination.\n\nThis release is the first with the publish path fully corrected:\n\n  slug and name are passed explicitly, so identity no longer depends on\n  where files sit on disk, and the response is checked against the\n  expected slug so a mispublish is a red run rather than a silent one.\n\n  version comes from the git tag rather than the CLI's \"next patch\"\n  default, which had matched by coincidence and would have diverged at\n  the first minor bump.\n\n  the changelog is the annotated tag message, guarded on tag object type\n  because a lightweight tag's contents fall back to the commit message.\n\n  the package excludes openspec/ and .claude/, 69 files down to 31, and\n  the staged tree is link-checked before upload so that trimming it\n  cannot ship dead links.\n\n  only release tags publish, and checkout runs on Node 24.\n\nv0.1.4 | 2026-07-29T06:55:17.707Z | auto\n\n**Added containerized test and runtime environment, script improvements, and documentation cleanup.**\n\n- Introduced a container-based test environment with supporting scripts and documentation (`containers/`).\n- Added and documented a one-step environment check (`scripts/doctor.sh`) and clarified setup instructions.\n- Revised and trimmed skill documentation for clarity, with a simplified quick start and reference guide.\n- Improved handoff flow robustness: now verifies and only issues valid, confirmed noVNC URLs.\n- Removed outdated or redundant files; added contribution guidelines.\n\nv0.1.3 | 2026-07-28T15:43:59.265Z | auto\n\nline-oa-chat-send 0.1.3 changelog\n\n- Clarified and strengthened guidance around temporary noVNC handoff usage for LINE login or reauthentication: it now requires explicit user request and enforces a strict TTL.\n- Added new security boundary notes highlighting risk and operational constraints for noVNC handoff. Clearer instructions for URL secrecy, TTLs, and handoff revocation.\n- Updated SKILL.md and README sections to reflect refined security model and operational process for temporary browser handoff.\n- Removed obsolete or redundant documentation file (skill-card.md).\n\nv0.1.2 | 2026-07-28T10:50:18.095Z | auto\n\n- Added additional security checks to the VNC/noVNC handoff: now verifies the public URL returns a valid noVNC page and confirms the browser title before sharing with the user.\n- Improved handling of Xvfb display selection by checking if the default display is in use and allowing safe selection of alternate private displays.\n- Updated SKILL.md documentation with the new verification steps for bearer URLs and display assignment recommendations.\n- Removed deprecated or no longer needed script files, consolidating setup processes.\n\nv0.1.1 | 2026-07-27T22:09:25.889Z | auto\n\n- Added startup scripts: `start_line_oa_chromium.sh` (for secure Chromium session handling) and `start_line_oa_vnc_handoff.sh` (for protected VNC/noVNC user login handoff).\n- Expanded documentation for secure session setup, including explicit operator- and user-facing instructions for profile, runtime, and GUI/VNC/noVNC/tunnel flows.\n- Updated `setup_line_oa_runtime.sh` and environment variable usage for more robust, configurable, and private runtime and profile management.\n- Detailed secure provisioning and shutdown procedures to prevent credential exposure, widen operator flexibility, and strengthen session discipline.\n- Clarified step-by-step process for both interactive user authentication and safe automation, covering both environment bootstrapping and resource cleanup.\n\nv0.1.0 | 2026-07-27T09:13:36.808Z | auto\n\nline-oa-chat-send 0.1.0 – Initial Release\n\n- Send and verify messages to a specific recipient in a LINE Official Account Chat via an authenticated, persistent Chromium session.\n- Requires user to complete authentication in a protected, interactive browser before sending.\n- Uses Playwright to connect to a local Chromium CDP endpoint without handling any credentials directly.\n- Ensures safe environment setup for browser and Python runtime isolation; does not create or manage browser profiles.\n- Provides CLI utilities for recipient/message selection, with safeguards against ambiguous recipients and accidental sends.\n- Only sends messages explicitly authorized by the user and verifies delivery in the chat transcript.\n\nArchive index:\n\nArchive v0.1.6: 31 files, 62814 bytes\n\nFiles: containers/build.sh (1564b), containers/Dockerfile (5463b), containers/entrypoint.sh (1298b), containers/README.md (7178b), containers/reset.sh (1722b), containers/run.sh (3982b), containers/test/default.sh (13120b), containers/test/tunnel.sh (6146b), CONTRIBUTING.md (3449b), LICENSE (1064b), README.md (5752b), references/handoff-operations.md (7546b), references/line-oa-ui-selectors.md (2854b), references/public-repository-checklist.md (2499b), scripts/check_doc_links.sh (1390b), scripts/doctor.sh (7824b), scripts/lib/caddy.sh (1602b), scripts/lib/phase_timing.sh (2507b), scripts/lib/ports.sh (2473b), scripts/lib/session.sh (3981b), scripts/measure_handoff.sh (10557b), scripts/run_line_oa_chat.sh (1508b), scripts/send_line_oa_chat.py (5181b), scripts/setup_line_oa_runtime.sh (1644b), scripts/start_line_oa_chromium.sh (12588b), scripts/start_line_oa_vnc_handoff.sh (8918b), scripts/stop_line_oa_chromium.sh (3457b), scripts/stop_line_oa_vnc_handoff.sh (2252b), skill-card.md (2431b), SKILL.md (6183b), _meta.json (136b)\n\nFile v0.1.6:SKILL.md\n\n---\nname: line-oa-chat-send\ndescription: Send explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control.\n---\n\n# LINE OA Chat: send a message\n\n## Use when\n\nA user provides or has already opened a LINE OA Chat URL and asks to send a\nspecific message to a named chat recipient.\n\n## Prerequisites\n\n- The persistent Chromium profile is already authenticated to LINE by the user.\n- A browser session is running with its loopback CDP endpoint reachable.\n- The user has explicitly authorized the outgoing message. Do not infer a\n  message other than an unambiguous test message.\n\n## Security boundary\n\n- Normal message work uses local CDP only. It does not expose a browser, CDP,\n  VNC, screenshots, cookies, or profile data to the network.\n- The optional noVNC handoff is **only** for a user to complete LINE login or\n  reauthentication. It grants whoever holds its URL interactive control of the\n  browser, so treat the URL as a high-risk bearer secret—not as a general\n  browsing or support channel.\n- Start a handoff only after the user explicitly requests this\n  login/reauthentication route. It has a default 15-minute TTL (configurable only\n  from 60 to 3600 seconds), must be shared only in a direct private channel, and\n  must be revoked immediately after login. Do not use it to send messages, browse\n  unrelated sites, or perform autonomous actions.\n- Never request, handle, record, or transmit LINE passwords, OTPs, or other\n  credentials. The user completes every authentication step themselves.\n- Never log, commit, reuse, or relay a handoff URL.\n\nRevoking a handoff removes external reachability only; the browser session keeps\nrunning. That narrows the cost of revoking promptly — it does not narrow what the\nhandoff grants while it is armed.\n\n## Quick start\n\n```bash\n# 1. Check the environment. Exit 0 = ready; 3 = usable but not logged in; 1 = blocked.\nbash scripts/doctor.sh\n\n# 2. Start the browser session if one is not running.\n#    Owns the display, Chromium, and a loopback-only screen-sharing stack.\n#    Nothing is externally reachable.\nbash scripts/start_line_oa_chromium.sh\n\n# 3. ONLY when the user explicitly asks to log in or re-authenticate.\n#    Prints one URL, already verified reachable, or fails non-zero.\nexport LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login\nexport LINE_OA_SEND_CHAT_HANDOFF_TTL_SECONDS=900   # 60-3600, default 900\nbash scripts/start_line_oa_vnc_handoff.sh\n\n# 4. Revoke as soon as the user says login is done. Session keeps running.\nbash scripts/stop_line_oa_vnc_handoff.sh\n\n# 5. Find and open the chat without sending. This is the safe default.\nbash scripts/run_line_oa_chat.sh --recipient \"<exact recipient>\" --message \"<message>\"\n\n# 6. Send, only after the user authorized this exact recipient and message.\nbash scripts/run_line_oa_chat.sh --recipient \"<exact recipient>\" --message \"<message>\" --send\n\n# 7. Stop the session when the work is finished. The profile is preserved.\nbash scripts/stop_line_oa_chromium.sh\n```\n\nProvision a Python runtime only if `doctor.sh` reports one missing:\n\n```bash\nbash scripts/setup_line_oa_runtime.sh --runtime-dir <private-dir> --skip-browser-install\nexport LINE_OA_PYTHON=<private-dir>/venv/bin/python\n```\n\n## What the scripts guarantee\n\nYou do not need to re-check these by hand.\n\n- **The handoff prints a URL only after verifying it.** It fetches the public URL\n  and requires HTTP 200 with noVNC content. On failure it prints no URL, revokes\n  what it started, and exits non-zero naming the phase that failed. Do not\n  construct or share a URL yourself.\n- **Arming refuses** with no session, with stale session state, without the login\n  purpose, with an out-of-range TTL, and when a handoff is already armed.\n- **Nothing is externally reachable between handoffs.** The front end answers 404\n  until a token route is armed.\n- **The send CLI refuses ambiguous recipients**, requires `--send` for the\n  external side effect, and verifies the message landed before reporting success.\n- **Shutdown is scripted.** It revokes any armed handoff first, terminates the\n  recorded process gracefully, and reports rather than force-killing on timeout.\n\n## When to stop and ask the user\n\n- **The recipient search matched more than one chat.** Stop. Ask which\n  conversation to use. Never guess which person receives a message.\n- **Post-send verification failed.** Do **not** retry. LINE may have accepted the\n  message already, so a retry risks sending it twice. Inspect the browser first,\n  then report what you found.\n- **LINE asks for a password, QR confirmation, MFA, OTP, or a security prompt.**\n  Only the user resolves these, through the handoff. Do not read, type, store,\n  relay, or log any credential or verification code.\n- **The session expired or was revoked mid-task.** Pause and ask the user to\n  reauthenticate through the handoff, then re-check before continuing.\n- **`doctor.sh` reports a blocker.** Report the exact missing pieces and its\n  remediation. Do not invoke `sudo` or a package manager, and never weaken a\n  network binding to work around a failure.\n- **The profile is locked by another hostname.** Another Chromium may genuinely\n  hold it. Report it; do not clear the lock.\n\n## Reference material\n\nRead these when you need depth; they are not needed for a normal send.\n\n| File | Read it when |\n| --- | --- |\n| [references/line-oa-ui-selectors.md](references/line-oa-ui-selectors.md) | The send flow fails to find the search box, the chat, or the composer |\n| [references/handoff-operations.md](references/handoff-operations.md) | Changing the session or handoff scripts, or diagnosing a handoff that will not verify |\n| [references/public-repository-checklist.md](references/public-repository-checklist.md) | Making the repository public |\n| [CONTRIBUTING.md](CONTRIBUTING.md) | Changing this repository |\n| [containers/README.md](containers/README.md) | Running the scripts in the Linux test environment |\n\n`scripts/send_line_oa_chat.py` is the only implementation of the send flow. Do\nnot write a second one.\n\nFile v0.1.6:containers/README.md\n\n# Container test environment\n\nA reproducible Linux environment for running and deliberately breaking this\nrepository's scripts. It exists because the scripts target Linux while\ndevelopment happens elsewhere, and because failure paths are far cheaper to\nexercise by picking a variant than by breaking a real host by hand.\n\n## What this is and is not authoritative for\n\n| Authoritative for | Not authoritative for |\n| --- | --- |\n| Script behavior | Which startup phase dominates |\n| Refusal and error paths | Absolute phase durations |\n| Dependency detection and remediation messages | Any reported speedup figure |\n| Handoff arm/revoke lifecycle | End-to-end message send |\n\nLatency results from this container are usable for rehearsing a measurement and\nfor same-host before-and-after comparison. They are **not** an answer to which\nphase dominates handoff startup. Both poles are distorted here, in the same\ndirection but by different and unpredictable amounts:\n\n- **Chromium** is slowed by the VM's CPU allocation and filesystem layer, and is\n  simultaneously *sped up* by using a throwaway profile instead of a real one.\n- **Tunnel registration** is slowed because Quick Tunnels prefer QUIC over UDP,\n  which a desktop VM's NAT can degrade into an HTTP/2 fallback, and because\n  egress goes through a workstation network rather than a datacenter link.\n\n## Distance from the target host\n\nThe target is **x86_64 Debian 13 (trixie)**, running as a Kubernetes pod. The\ncontainer here is **arm64 Debian 12 (bookworm)** on a Docker Desktop VM. They\ndiffer in instruction set, distribution release, and virtualization layer.\n\n| Differs | Effect |\n| --- | --- |\n| arm64 here vs x86_64 there | Different silicon; no basis for comparing absolute times |\n| Debian 12 vs Debian 13 | Different Chromium, Caddy, and cloudflared builds |\n| Docker Desktop VM vs a pod | Different CPU allocation and filesystem layer |\n| Throwaway profile instead of a real authenticated one | Deflates Chromium cold start |\n| Workstation egress, QUIC through the VM's NAT | Changes tunnel registration and DNS behaviour |\n| `seccomp=unconfined`, absent on the target | Changes sandbox setup cost |\n\nMeasured, once both sides had run the same script:\n\n| | container | target |\n| --- | --- | --- |\n| session start | 0.53s | 1.63s |\n| `tunnel_url` | 2.79s | 4.62s |\n\nThe container was **faster** on both — the opposite of what \"a VM inflates\nstartup\" would predict. That is the point: the direction of the distortion was\nnot predictable in advance, which is why this environment is not authoritative\nfor latency and why the measurements had to be repeated on the target.\n\nA concrete case: the container found that a 3s grace window failed outright,\nwhich looked like evidence of a propagation floor. The target sweep showed no\nfloor at all — misses are sporadic and independent of how long you wait. The\ncontainer result was one unlucky sample generalized into a rule. It was labelled\nnon-authoritative, and that label is why it was re-tested rather than shipped.\n\nTwo further divergences from a real host, both introduced by the container and\nneither present on the target:\n\n- `--security-opt seccomp=unconfined`, because Docker's default seccomp filter\n  blocks the namespace creation Chromium's zygote needs. This drops the\n  container's outer confinement so Chromium can build its own sandbox; the\n  browser's sandbox stays intact. Preferred over `--no-sandbox`, which would\n  mean teaching the scripts a container-only flag.\n- `--shm-size=1g`, because Chromium exhausts the 64MB default and dies with a\n  broken zygote pipe.\n\n## Credentials\n\nThe container is credential-free by construction, not by convention. Every\nbehavior it covers works against the LINE **login** page, so no session is ever\nneeded. `run.sh` builds every mount itself and forwards no Docker flags, and it\nrefuses an inherited `LINE_OA_SEND_CHAT_CHROMIUM_PROFILE` rather than silently\nignoring it. Do not mount a real profile.\n\n## Variants\n\nAll four derive from one Dockerfile; the table in `build.sh` is the only place\nthey differ, and their base layers are byte-identical.\n\n| Variant | Handoff deps | Playwright runtime | Profile |\n| --- | --- | --- | --- |\n| `full` | yes | yes | empty |\n| `no-runtime` | yes | no (and no `uv`) | empty |\n| `no-handoff-deps` | no | yes | empty |\n| `unauth-profile` | yes | yes | initialized, no session |\n\n`no-runtime` omits `uv` deliberately: `run_line_oa_chat.sh` falls back to\n`uv run --with playwright`, so leaving `uv` in place would let the variant\nsilently succeed and prove nothing.\n\n`unauth-profile` runs Chromium once at build time so its profile is initialized\nand previously used rather than an empty directory, which would make it\nidentical to `full`.\n\n## Usage\n\n```bash\n# Build all four variants (native architecture only)\ncontainers/build.sh\n\n# Build one\ncontainers/build.sh full\n\n# Run a command in a variant; default is an interactive shell\ncontainers/run.sh full bash\ncontainers/run.sh no-runtime bash scripts/run_line_oa_chat.sh --help\n\n# Start a browser session and check CDP from inside\ncontainers/run.sh full bash -lc '\n  bash scripts/start_line_oa_chromium.sh >/tmp/c.log 2>&1 &\n  sleep 20\n  curl -fsS http://127.0.0.1:9222/json/list'\n```\n\nThe repository is mounted read-only at `/workspace`. The browser profile lives\non a per-variant Docker volume, never a host path. No port is published, so\nnothing is externally reachable unless a command inside starts an outbound\ntunnel itself.\n\n## Tests\n\n```bash\n# Default path: no external exposure\ncontainers/test/default.sh\n\n# Opt-in: creates a real, externally reachable Cloudflare Quick Tunnel\nLINE_OA_TEST_ALLOW_TUNNEL=1 containers/test/tunnel.sh 60\n```\n\nThe default path covers the architecture guard, dependency presence, isolation,\nbrowser session startup, and every handoff refusal that happens before a tunnel\nwould be created. The tunnel test is separate and gated because it genuinely\nexposes the container's browser for the life of the tunnel.\n\n## Teardown\n\nContainers run with `--rm` and clean themselves up. Profile volumes persist on\npurpose, mirroring the real system's persistent profile, so removing them is\nexplicit:\n\n```bash\ncontainers/reset.sh            # drop profile volumes\ncontainers/reset.sh --images   # also drop the built images\n```\n\n## Architecture guard\n\nThe container refuses to run under emulation. `run.sh` passes the host\narchitecture in, and the entrypoint compares it against the container's own;\na mismatch, or a missing host architecture, exits non-zero. Emulated Chromium\nwould be slow enough to make every test unpleasant and every timing meaningless.\n\n## Known behavior worth remembering\n\nChromium writes a `SingletonLock` into the profile recording the hostname\nholding it. Docker assigns a random hostname per container, so a lock left by a\nkilled container is read as held \"on another computer\" and every later run is\nrefused. `run.sh` pins a per-variant hostname so Chromium recognizes a stale\nlock as its own and breaks it. A volume already poisoned by a random-hostname\nlock needs `containers/reset.sh` once. The same stale lock can strand a\nhard-killed Chromium on a real host.\n\nFile v0.1.6:README.md\n\n# line-oa-chat-send\n\nA safety-first CLI for sending messages through an already authenticated\n[LINE Official Account Chat](https://chat.line.biz/) browser session.\n\nThe send command attaches to a local Chromium DevTools (CDP) endpoint. It does\nnot start a browser, log in to LINE, or collect credentials.\n\n> **Optional login handoff — high-risk capability:** when a user explicitly asks\n> to complete LINE login or reauthentication through a remote GUI,\n> `scripts/start_line_oa_vnc_handoff.sh` arms a temporary Cloudflare noVNC URL\n> that grants its holder interactive control of the browser. It attaches to a\n> running session rather than starting one, requires the explicit\n> `LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login` scope, defaults to a 15-minute\n> TTL, keeps VNC and CDP loopback-only, and must be revoked as soon as login\n> ends. Revoking it leaves the browser session running.\n\n## What it does\n\n- Searches for a chat recipient and refuses ambiguous matches.\n- Opens the selected conversation and confirms the composer is available.\n- Defaults to a no-send dry run; requires an explicit `--send` to transmit.\n- After a send, checks that the composer cleared and the exact message appears in\n  the active transcript.\n\n## Requirements\n\n1. A user-authenticated LINE OA Chat session in Chromium.\n2. Chromium exposing a loopback CDP endpoint (default `http://127.0.0.1:9222`).\n3. Python with the `playwright` package. No browser download is needed — the tool\n   attaches to an existing Chromium.\n4. Explicit authorization for the recipient and the outgoing message.\n\nLinux is the supported platform. `containers/` provides a reproducible Linux\nenvironment for running the scripts from another OS.\n\n> **Authentication:** complete LINE login, password entry, QR confirmation, MFA,\n> OTP, and security prompts yourself in the interactive browser. This project\n> never accepts, stores, or transmits those secrets.\n\n## Check the environment first\n\n```bash\nbash scripts/doctor.sh\n```\n\n| Exit | Meaning |\n| --- | --- |\n| 0 | Ready; a send can run |\n| 3 | Environment usable, LINE authentication required — a checkpoint, not a failure |\n| 1 | Blocked; the missing pieces and their remediation are printed |\n\nIt reports what the launcher will actually do, and never suggests widening a\nnetwork binding. It invokes no package manager and no `sudo`.\n\nIf it reports no Python runtime:\n\n```bash\nbash scripts/setup_line_oa_runtime.sh --runtime-dir <private-dir> --skip-browser-install\nexport LINE_OA_PYTHON=<private-dir>/venv/bin/python\n```\n\n## Usage\n\n```bash\n# Start the browser session. Owns the display, Chromium, and a loopback-only\n# screen-sharing stack. Nothing is externally reachable.\nbash scripts/start_line_oa_chromium.sh\n\n# Safe default: find and open a uniquely matched chat, send nothing.\nbash scripts/run_line_oa_chat.sh --recipient \"Recipient name\" --message \"Message text\"\n\n# Send, only after the recipient and exact text have been authorized.\nbash scripts/run_line_oa_chat.sh --recipient \"Recipient name\" --message \"Message text\" --send\n\n# Stop the session. The persistent profile is preserved.\nbash scripts/stop_line_oa_chromium.sh\n```\n\nUse `--cdp-url` if the endpoint is not the default. `--help` lists all options.\n\n## Login / reauthentication handoff\n\nOnly after the user explicitly requests an interactive login session. It is a\nlogin-only capability; do not use it to send messages or for unrelated browser\ncontrol.\n\n```bash\nexport LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login\nexport LINE_OA_SEND_CHAT_HANDOFF_TTL_SECONDS=900   # optional; 60-3600\nbash scripts/start_line_oa_vnc_handoff.sh\n\n# When the user says login is done:\nbash scripts/stop_line_oa_vnc_handoff.sh\n```\n\nThe script attaches to the running session, arms a private high-entropy route,\nstarts the tunnel, and **verifies the public URL is reachable before printing\nit**. On failure it prints no URL, revokes what it started, and exits non-zero\nnaming the failing phase. It refuses to arm with no session, with stale session\nstate, without the login purpose, with an out-of-range TTL, or when a handoff is\nalready armed.\n\nThe printed URL is a bearer secret. Share it only with that user in a private\nchannel, never log or reuse it, and revoke as soon as login ends. TTL expiry is a\nbackstop, not a substitute for prompt revocation.\n\nArming takes roughly 20–25 seconds, most of it a deliberate quiet window before\nthe first DNS lookup. See\n[references/handoff-operations.md](references/handoff-operations.md) for why\nprobing sooner makes it slower.\n\n## Safety behavior\n\n- A run without `--send` never transmits a message.\n- If more than one chat may match the recipient, the command stops instead of\n  guessing.\n- If the authenticated UI, chat selection, composer, or post-send verification\n  fails, the command exits non-zero.\n- Do not retry a failed post-send verification automatically: LINE may have\n  accepted the message even if verification did not complete.\n- CDP, VNC, websockify, and the HTTP front end stay on loopback. The front end\n  answers 404 until a handoff is armed. Nothing is externally reachable between\n  handoffs.\n\n## Testing\n\n```bash\ncontainers/build.sh                                  # four dependency variants\ncontainers/test/default.sh                           # no external exposure\nLINE_OA_TEST_ALLOW_TUNNEL=1 containers/test/tunnel.sh 60   # opt-in; exposes a browser\n```\n\nSee [containers/README.md](containers/README.md), including what the container is\nand is not authoritative for.\n\n## Documentation\n\n- [SKILL.md](SKILL.md) — what an agent needs to decide and act\n- [references/](references/) — UI selectors, handoff operations, publication checklist\n- [CONTRIBUTING.md](CONTRIBUTING.md) — development and release process\n\nFile v0.1.6:_meta.json\n\n{\n  \"ownerId\": \"kn743ga1fg7x5xfqphk150apyd8apt23\",\n  \"slug\": \"line-oa-chat-send\",\n  \"version\": \"0.1.6\",\n  \"publishedAt\": 1785312386333\n}\n\nFile v0.1.6:references/handoff-operations.md\n\n# Handoff operations\n\nWhy the session and handoff scripts look the way they do. Everything here is\nalready enforced in code — this is the reasoning, for when the code needs\nchanging. Read it before editing `scripts/start_line_oa_chromium.sh`,\n`scripts/start_line_oa_vnc_handoff.sh`, or `scripts/lib/*.sh`.\n\n## Shape\n\n```\nBrowser session (long-lived, loopback only)\n  Xvfb ── Chromium ── CDP 127.0.0.1:9222\n    └──── x11vnc ──── websockify ──── Caddy (404 until armed)\n\nLogin handoff (ephemeral, the only thing that is ever externally reachable)\n    Caddy token route ──── cloudflared Quick Tunnel ──── public HTTPS URL\n```\n\nThe session owns the browser. The handoff attaches. Revoking the handoff removes\nthe tunnel and the route and leaves everything else running.\n\n## Ports are never assumed free\n\nVNC, websockify, Caddy, and Caddy's admin endpoint all get dynamically selected\nloopback ports, chosen in one call that holds each socket until all are chosen —\notherwise the same port can be handed out twice.\n\nFixed ports silently attach the handoff to something else. A hard-coded `5900`\ncan bind to another display and produce a black canvas; a hard-coded front-end\nport can collide and leave a working tunnel URL serving nothing.\n\n## Caddy's output is not discarded\n\nA bind conflict with output sent to `/dev/null` surfaces as \"the URL works but\nthe page is blank\" — expensive to diagnose, trivial to prevent. Output goes to\nthe run log.\n\n## The site address is host-agnostic\n\n```\n:PORT {\n  bind 127.0.0.1\n  ...\n}\n```\n\nNot `http://127.0.0.1:PORT`. The tunnel arrives carrying a public Host header,\nand a host-specific site address rejects it. `bind 127.0.0.1` keeps the listener\non loopback while leaving the matcher host-agnostic.\n\nKeep `handle` blocks multi-line. Compact inline blocks can fail Caddy parsing.\n\n## x11vnc needs `-noxrecord -noxfixes -noxdamage`\n\nWithout them noVNC can stay black even while Chromium has a mapped window on the\ndisplay.\n\nThe cost is real: `-noxdamage` disables the damage extension, so x11vnc polls\nthe whole screen instead of being told what changed. That means higher CPU and a\nless responsive remote canvas. It is a deliberate trade — a slow picture beats no\npicture — and it affects interaction latency, not startup time.\n\n## Verification waits before it probes\n\nThe single most counterintuitive part.\n\nA fresh `trycloudflare.com` hostname is **not resolvable when cloudflared prints\nit**. A lookup that misses is cached as a negative answer, and re-querying keeps\nrefreshing that negative entry — so an eager polling loop holds itself in\nfailure long past actual propagation.\n\nMeasured:\n\n| Approach | Outcome |\n| --- | --- |\n| Probe immediately, poll every 1s | Hostname never resolved within 120s |\n| Wait 45s quietly, then query once | Resolved on the first query |\n\nThis is not a container artifact. systemd-resolved, dnsmasq, and container\nembedded DNS all cache negative answers.\n\nSo the handoff waits a quiet grace window (`LINE_OA_SEND_CHAT_HANDOFF_VERIFY_GRACE`,\ndefault 5s) before its first lookup, then backs off progressively, and records\nDNS resolution as a phase separate from HTTP reachability — they are different\nwaits with different causes.\n\n### There is no propagation floor to clear\n\nThe obvious model — propagation takes about N seconds, so wait N — does not\nsurvive measurement. Target host, three runs at each value plus three baseline\nruns at the 20s default:\n\n| grace | first-lookup hits | `url_verified` when it hit |\n| --- | --- | --- |\n| 5s | 3/3 | ~10.7s |\n| 8s | 3/3 | ~13.3s |\n| 12s | 3/3 | ~16.7s |\n| 16s | 2/3 | ~20.5s |\n| 20s | 3/3 sweep, 2/3 baseline | ~25.4s |\n\nTwo misses in 18 runs, ~11%, and both were large outliers: the lookup took\n40–60s longer than the grace, not slightly longer.\n\nSo propagation completed inside 5s for at least nine consecutive tunnels, and\nthe 20s run that missed waited 60s — four times the grace would not have saved\nit. Waiting longer costs ~15s on every fast arm and does not buy protection on\nthe slow ones.\n\nDo not read the misses landing on the two longest values as evidence that longer\nis *worse*. With three runs each that is well within chance. The defensible\nreading is that misses look independent of the grace window.\n\nHence: a short grace, and a backoff that recovers when an outlier appears. It\ndoes recover — the 16s miss verified at 61s rather than failing.\n\nTypical successful arm at the 5s default:\n\n```\nroute_armed     0.03s\ntunnel_url      4.60s   URL printed here, and not yet usable\ndns_resolved    9.60s   resolved on the first query after the grace window\nurl_verified   10.70s   HTTP succeeded on the first attempt\n```\n\n## A local request is not verification\n\n`curl http://127.0.0.1:<caddy-port>/...` proves the local chain works. It says\nnothing about whether the tunnel carries traffic. The script fetches the **public\nURL** and requires HTTP 200 with noVNC content before printing anything.\n\nNothing is printed on failure. The script revokes what it started and exits\nnon-zero naming the phase.\n\n## Phase timing\n\nEvery run of the session and handoff scripts writes a phase log to\n`$RUNTIME_DIR/timing/`, mode 600, with phase names and durations only — never a\nURL, route token, or credential. There is no debug flag, because the slow runs\nare the ones nobody thought to instrument.\n\n## Chromium's SingletonLock\n\nChromium writes a `SingletonLock` symlink into the profile recording\n`<hostname>-<pid>`. A hard-killed Chromium leaves it behind.\n\n- Same hostname, dead PID → stale; the session start clears it and says so.\n- Different hostname → **not** cleared. Another Chromium may genuinely hold the\n  profile, and clearing it could corrupt a live session. Reported distinctly so\n  the cause is obvious rather than surfacing as a generic launch failure.\n\nThis bites in containers especially, where a random per-container hostname makes\nevery rerun look like \"in use on another computer\".\n\n## Chromium in a container\n\nTwo concessions apply to the test environment only, never to a real host:\n`--security-opt seccomp=unconfined` (Docker's default filter blocks the\nnamespace creation the zygote needs) and `--shm-size` (Chromium exhausts the\n64MB default). See `containers/README.md`. Neither is a reason to teach the\nscripts a container-only flag.\n\n## Running as root\n\nChromium refuses to start as root with its sandbox enabled\n(`Running as root without --no-sandbox is not supported`).\n\nThe fix is to run unprivileged, under a service identity that owns the profile\ndirectory. That is how the browser is meant to run anyway.\n\n```bash\nuseradd --create-home --uid 1000 lineoa\nchown -R lineoa:lineoa /opt/data/chromium\nsu - lineoa -c 'cd <repo> && bash scripts/start_line_oa_chromium.sh'\n```\n\nFor deployments that genuinely cannot drop privileges — a pod fixed to root, for\ninstance — there is an explicit opt-in:\n\n```bash\nLINE_OA_SEND_CHAT_ALLOW_NO_SANDBOX=1 bash scripts/start_line_oa_chromium.sh\n```\n\nIt is opt-in and it warns on every start, because the cost is not theoretical.\nOrdinarily this browser only visits `chat.line.biz`, so a disabled renderer\nsandbox has little to contain. **A login handoff changes that**: it grants\ninteractive control, so whoever holds the URL can navigate the browser anywhere,\nand an unsandboxed renderer contains far less of whatever they reach. The same\nbrowser holds the authenticated LINE session.\n\nIf you must run this way, keep handoffs short and treat the URL with more care,\nnot less.\n\nFile v0.1.6:references/line-oa-ui-selectors.md\n\n# LINE OA Chat: UI selector notes\n\nWhy the send flow targets what it targets. The implementation is\n`scripts/send_line_oa_chat.py` — the only one. This file explains the reasoning\nbehind its selectors so they can be repaired when LINE changes the UI, and does\nnot restate the algorithm.\n\n## Choosing the page\n\nSelect the page whose URL begins with `https://chat.line.biz/`, and require\nexactly one such page. More than one is ambiguous — two tabs can hold two\ndifferent conversations, and picking either is a guess about where a message\nlands. Zero means the session is not authenticated or the browser is elsewhere.\n\n## Searching for a recipient\n\nThe chat-list search is the sidebar input labelled `搜尋`.\n\n**Match the placeholder exactly.** Playwright's placeholder matching is\nsubstring-based by default, and LINE has an unrelated `輸入搜尋內容` field that a\nloose match also hits. `get_by_placeholder(\"搜尋\", exact=True)` selects the chat\nlist search and nothing else.\n\n## Selecting the result\n\nTake the anchor that contains the matching `<mark>`, not a text match on the\nrecipient name.\n\nTwo distinct reasons:\n\n- **Name collisions reach the wrong element.** `get_by_text(recipient,\n  exact=True)` can match the signed-in account menu rather than the chat result\n  when the names coincide. The account menu is not a conversation.\n- **A hidden label is not a missing result.** On a narrow layout the sidebar\n  hides the chat label, so the matching text has no bounding box — but its\n  containing chat-list `<a>` is still present and clickable. Treating the hidden\n  label as \"not found\" turns a responsive layout into a false negative. Locate\n  the anchor from the `<mark>` instead.\n\n## Confirming the conversation loaded\n\nCheck that the message composer is visible and prior conversation content is\npresent before typing. A click that has been dispatched is not a conversation\nthat has rendered.\n\n## The composer\n\nThe composer lives inside a custom element. `page.locator(\"textarea-ex\")\n.locator(\"textarea\")` pierces the shadow DOM to the real editable element.\n\nA placeholder-based lookup matches both the `textarea-ex` host and its internal\n`textarea`, and filling the host does not fill the field. Chain the locators so\nthe target is unambiguous.\n\nSubmit with `input[type=\"submit\"][value=\"傳送\"]`.\n\n## Verifying the send\n\nA successful click is not evidence that a message was sent. Two conditions\ntogether are:\n\n1. the composer cleared, and\n2. the exact text appears in the active transcript.\n\nPrefer checking the recent transcript tail over a page-wide text match. A\npage-wide match can be satisfied by an identical message sent earlier, which\nwould confirm a send that did not happen.\n\n**If verification fails, do not retry.** LINE may have accepted the message\nalready; a retry risks sending twice. Inspect the browser first.\n\nFile v0.1.6:references/public-repository-checklist.md\n\n# Making the repository public\n\nFollow this in order. Publication is irreversible in practice: anything reachable\nin Git history stays reachable once it has been fetched, and removing it later\ndoes not un-publish it.\n\n## 1. Audit all reachable history, not just the current files\n\nOld commits and merged pull requests remain visible after publication. Scan the\nfull history, not the working tree.\n\nLook for:\n\n- credentials, tokens, and authentication headers\n- tunnel or share URLs (they are bearer secrets)\n- browser-profile data, cookies, or session artefacts\n- host-specific runtime paths that reveal infrastructure\n\nWhen reporting findings, report **commit IDs and pattern categories only** —\nnever the matched values. A report that quotes the secret has republished it.\n\nRemove or rewrite genuinely sensitive history before publication. Rewriting\nhistory after the fact is far more disruptive than doing it now.\n\n## 2. Apply public-facing settings before changing visibility\n\n- Remove collaboration surfaces that are not in use.\n- Enable automatic deletion of merged branches.\n- Disable Actions that have not been reviewed for a public context. A workflow\n  that was safe in a private repository can leak in a public one, and public\n  forks change who can trigger what.\n\n## 3. Change visibility\n\n## 4. Verify from outside\n\nFetch the repository and its README through an **unauthenticated** request. A\nbrowser already signed in proves nothing.\n\n## 5. Enable public-repository security controls\n\n- secret scanning\n- push protection\n- Dependabot\n\nGitHub may enable some of these automatically and may reject others depending on\nplan and org policy, so **read the resulting state back** rather than assuming\nthe request applied.\n\n## 6. Licensing is a separate decision\n\nTreat it as a legal question, not a checklist item. Do not choose a licence\nwithout the user's direction.\n\n## Notes specific to this repository\n\n- The ClawHub workflow publishes on tag push and needs its token secret to exist\n  in the repository's Actions secrets. Confirm the secret's scope before the\n  repository becomes public.\n- `scripts/` contains no credentials by design: the browser profile lives outside\n  the repository, and the runtime directory is created at run time. Confirm that\n  no run has written a profile, a runtime directory, or a timing log inside the\n  working tree.\n- Phase-timing logs contain durations only, but they live in the private runtime\n  directory and should never be committed regardless.\n\nFile v0.1.6:CONTRIBUTING.md\n\n# Contributing\n\nDevelopment process for this repository. This is not skill knowledge — an agent\nsending a message does not need any of it. See [SKILL.md](SKILL.md) for that.\n\n## Changing the skill\n\nUse the normal Git/PR lifecycle. Do not commit to `main` directly.\n\n1. Start from the current `main` and create a focused branch, e.g.\n   `docs/update-authentication`.\n2. Make the change and validate it.\n3. Commit and push the branch.\n4. Open a PR with `gh pr create`, summarising the change and how it was verified.\n5. Merge only after the user explicitly approves. A concise \"merge\" is sufficient\n   authorization. Use a squash merge, delete the feature branch, then verify the\n   PR state and branch deletion with `gh`.\n\nDocumentation-only changes, including README updates, follow the same path.\nBranch, validate, push, open a PR, leave it for review.\n\n## Verifying a PR before reporting it\n\nTest the exact remote revision, not a local copy that might differ:\n\n```bash\ngh pr view <number> --json headRefOid\ngit switch -c pr-<number> <sha>     # non-tracking branch at the verified SHA\n```\n\nA non-tracking branch is the fallback when a shallow clone's fetch refspec\nprevents `gh pr checkout` from setting up tracking.\n\nThen run, at minimum:\n\n```bash\nbash containers/build.sh                 # all four variants\nbash containers/test/default.sh          # no external exposure\nbash scripts/doctor.sh                   # environment verdict\n```\n\nThe container suite covers script behavior, refusal paths, and dependency\ndetection. It is **not** authoritative for latency — see\n[containers/README.md](containers/README.md).\n\nTunnel behavior is opt-in because it genuinely exposes a browser:\n\n```bash\nLINE_OA_TEST_ALLOW_TUNNEL=1 containers/test/tunnel.sh 60\n```\n\nA change touching the send path additionally needs a no-send dry run against a\nreal authenticated LINE UI, which the credential-free container cannot provide.\n\n## GitHub CLI authentication\n\nRun `gh auth status` in the same OS user and runtime that will run the commands.\nA browser session or a CLI login in another shell does not authenticate this one.\n\nIf auth is missing:\n\n```bash\ngh auth login --hostname github.com --git-protocol https --web\n```\n\nWhen the user asks you to \"just run it and give me the code\", run the flow and\nreply with only the one-time device code and `https://github.com/login/device`.\nNo extra explanation. Wait for their authorization, then confirm with\n`gh auth status`.\n\n## Publishing\n\n`.github/workflows/publish-clawhub.yml` publishes to ClawHub on tag push. It\npublishes the repository root, so `references/` and `containers/` ship with the\nskill. Broken relative links reach consumers — check them before tagging.\n\nThe workflow needs `CLAW_HUB_TOKEN` in the repository's Actions secrets.\n\n## Making the repository public\n\nSee [the public repository checklist](references/public-repository-checklist.md)\nfor the full audit, settings, and verification sequence. Audit **all reachable\nhistory**, not just the current files.\n\n## Repository layout\n\n| Path | Purpose |\n| --- | --- |\n| `SKILL.md` | What an agent needs to decide and act |\n| `README.md` | What a human evaluating or operating the tool needs |\n| `scripts/` | The implementation |\n| `scripts/lib/` | Shared shell libraries |\n| `references/` | Depth material, read on demand |\n| `containers/` | Linux test environment and its suites |\n| `openspec/` | Change proposals, designs, specs, and tasks |\n\nFile v0.1.6:skill-card.md\n\n## Description:\n\nSend explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[mosluce](https://clawhub.ai/user/mosluce)\n\n### License/Terms of Use:\n\nMIT\n\n## Use Case:\n\nExternal users and operators use this skill to send an explicitly authorized message to a named LINE Official Account Chat recipient from an already authenticated Chromium session. It also guides user-operated login or reauthentication through a temporary noVNC handoff when needed.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can send LINE business messages and security evidence reports a real recipient-matching flaw.\n\nMitigation: Use only non-sensitive test messages until exact recipient matching is fixed, require explicit recipient and message authorization, and stop rather than retry when post-send verification fails.\n\nRisk: Security evidence reports unpinned dependency execution through the Playwright runtime path.\n\nMitigation: Provision a pinned and reviewed Playwright runtime before production use.\n\nRisk: The optional login handoff grants interactive browser control to anyone with the handoff URL.\n\nMitigation: Run the browser under an unprivileged dedicated account, share handoff URLs only with the intended user in a private channel, treat each URL as a short-lived bearer secret, and revoke it promptly after login.\n\n## Reference(s):\n\n- [Handoff operations](references/handoff-operations.md)\n- [LINE OA Chat UI selector notes](references/line-oa-ui-selectors.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Configuration, Guidance, Text]\n\n**Output Format:** [Markdown with inline shell commands and operational guidance]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Produces no-send dry-run guidance by default and requires explicit send authorization for external message delivery.]\n\n## Skill Version(s):\n\n0.1.6 (source: server release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nFile v0.1.6:LICENSE\n\nMIT License\n\nCopyright (c) 2026 mosluce\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\nArchive v0.1.4: 56 files, 123540 bytes\n\nFiles: containers/build.sh (1564b), containers/Dockerfile (5463b), containers/entrypoint.sh (1298b), containers/README.md (7178b), containers/reset.sh (1722b), containers/run.sh (3982b), containers/test/default.sh (13120b), containers/test/tunnel.sh (6146b), CONTRIBUTING.md (3449b), LICENSE (1064b), openspec/changes/archive/2026-07-29-add-container-test-env/design.md (8283b), openspec/changes/archive/2026-07-29-add-container-test-env/proposal.md (2573b), openspec/changes/archive/2026-07-29-add-container-test-env/specs/container-test-env/spec.md (5249b), openspec/changes/archive/2026-07-29-add-container-test-env/tasks.md (4498b), openspec/changes/archive/2026-07-29-speed-up-login-handoff/after.md (1094b), openspec/changes/archive/2026-07-29-speed-up-login-handoff/baseline.md (2557b), openspec/changes/archive/2026-07-29-speed-up-login-handoff/design.md (13819b), openspec/changes/archive/2026-07-29-speed-up-login-handoff/docs-handoff.md (6162b), openspec/changes/archive/2026-07-29-speed-up-login-handoff/proposal.md (3757b), openspec/changes/archive/2026-07-29-speed-up-login-handoff/results.md (7141b), openspec/changes/archive/2026-07-29-speed-up-login-handoff/specs/browser-session/spec.md (4257b), openspec/changes/archive/2026-07-29-speed-up-login-handoff/specs/login-handoff/spec.md (5062b), openspec/changes/archive/2026-07-29-speed-up-login-handoff/target-host-runbook.md (9198b), openspec/changes/archive/2026-07-29-speed-up-login-handoff/tasks.md (8058b), openspec/changes/archive/2026-07-29-trim-skill-docs/classification.md (4195b), openspec/changes/archive/2026-07-29-trim-skill-docs/design.md (7432b), openspec/changes/archive/2026-07-29-trim-skill-docs/proposal.md (3338b), openspec/changes/archive/2026-07-29-trim-skill-docs/specs/environment-preflight/spec.md (3010b), openspec/changes/archive/2026-07-29-trim-skill-docs/specs/skill-documentation/spec.md (4189b), openspec/changes/archive/2026-07-29-trim-skill-docs/tasks.md (5488b), openspec/config.yaml (573b), openspec/specs/browser-session/spec.md (4508b), openspec/specs/container-test-env/spec.md (5525b), openspec/specs/environment-preflight/spec.md (3228b), openspec/specs/login-handoff/spec.md (5245b), openspec/specs/skill-documentation/spec.md (4393b), README.md (5752b), references/handoff-operations.md (7546b), references/line-oa-ui-selectors.md (2854b), references/public-repository-checklist.md (2499b), scripts/doctor.sh (7824b), scripts/lib/caddy.sh (1602b), scripts/lib/phase_timing.sh (2507b), scripts/lib/ports.sh (2473b), scripts/lib/session.sh (3981b), scripts/measure_handoff.sh (10557b), scripts/run_line_oa_chat.sh (1508b), scripts/send_line_oa_chat.py (5181b), scripts/setup_line_oa_runtime.sh (1644b), scripts/start_line_oa_chromium.sh (12588b), scripts/start_line_oa_vnc_handoff.sh (8918b), scripts/stop_line_oa_chromium.sh (3457b), scripts/stop_line_oa_vnc_handoff.sh (2252b), skill-card.md (2712b), SKILL.md (6183b), _meta.json (136b)\n\nFile v0.1.4:SKILL.md\n\n---\nname: line-oa-chat-send\ndescription: Send explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control.\n---\n\n# LINE OA Chat: send a message\n\n## Use when\n\nA user provides or has already opened a LINE OA Chat URL and asks to send a\nspecific message to a named chat recipient.\n\n## Prerequisites\n\n- The persistent Chromium profile is already authenticated to LINE by the user.\n- A browser session is running with its loopback CDP endpoint reachable.\n- The user has explicitly authorized the outgoing message. Do not infer a\n  message other than an unambiguous test message.\n\n## Security boundary\n\n- Normal message work uses local CDP only. It does not expose a browser, CDP,\n  VNC, screenshots, cookies, or profile data to the network.\n- The optional noVNC handoff is **only** for a user to complete LINE login or\n  reauthentication. It grants whoever holds its URL interactive control of the\n  browser, so treat the URL as a high-risk bearer secret—not as a general\n  browsing or support channel.\n- Start a handoff only after the user explicitly requests this\n  login/reauthentication route. It has a default 15-minute TTL (configurable only\n  from 60 to 3600 seconds), must be shared only in a direct private channel, and\n  must be revoked immediately after login. Do not use it to send messages, browse\n  unrelated sites, or perform autonomous actions.\n- Never request, handle, record, or transmit LINE passwords, OTPs, or other\n  credentials. The user completes every authentication step themselves.\n- Never log, commit, reuse, or relay a handoff URL.\n\nRevoking a handoff removes external reachability only; the browser session keeps\nrunning. That narrows the cost of revoking promptly — it does not narrow what the\nhandoff grants while it is armed.\n\n## Quick start\n\n```bash\n# 1. Check the environment. Exit 0 = ready; 3 = usable but not logged in; 1 = blocked.\nbash scripts/doctor.sh\n\n# 2. Start the browser session if one is not running.\n#    Owns the display, Chromium, and a loopback-only screen-sharing stack.\n#    Nothing is externally reachable.\nbash scripts/start_line_oa_chromium.sh\n\n# 3. ONLY when the user explicitly asks to log in or re-authenticate.\n#    Prints one URL, already verified reachable, or fails non-zero.\nexport LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login\nexport LINE_OA_SEND_CHAT_HANDOFF_TTL_SECONDS=900   # 60-3600, default 900\nbash scripts/start_line_oa_vnc_handoff.sh\n\n# 4. Revoke as soon as the user says login is done. Session keeps running.\nbash scripts/stop_line_oa_vnc_handoff.sh\n\n# 5. Find and open the chat without sending. This is the safe default.\nbash scripts/run_line_oa_chat.sh --recipient \"<exact recipient>\" --message \"<message>\"\n\n# 6. Send, only after the user authorized this exact recipient and message.\nbash scripts/run_line_oa_chat.sh --recipient \"<exact recipient>\" --message \"<message>\" --send\n\n# 7. Stop the session when the work is finished. The profile is preserved.\nbash scripts/stop_line_oa_chromium.sh\n```\n\nProvision a Python runtime only if `doctor.sh` reports one missing:\n\n```bash\nbash scripts/setup_line_oa_runtime.sh --runtime-dir <private-dir> --skip-browser-install\nexport LINE_OA_PYTHON=<private-dir>/venv/bin/python\n```\n\n## What the scripts guarantee\n\nYou do not need to re-check these by hand.\n\n- **The handoff prints a URL only after verifying it.** It fetches the public URL\n  and requires HTTP 200 with noVNC content. On failure it prints no URL, revokes\n  what it started, and exits non-zero naming the phase that failed. Do not\n  construct or share a URL yourself.\n- **Arming refuses** with no session, with stale session state, without the login\n  purpose, with an out-of-range TTL, and when a handoff is already armed.\n- **Nothing is externally reachable between handoffs.** The front end answers 404\n  until a token route is armed.\n- **The send CLI refuses ambiguous recipients**, requires `--send` for the\n  external side effect, and verifies the message landed before reporting success.\n- **Shutdown is scripted.** It revokes any armed handoff first, terminates the\n  recorded process gracefully, and reports rather than force-killing on timeout.\n\n## When to stop and ask the user\n\n- **The recipient search matched more than one chat.** Stop. Ask which\n  conversation to use. Never guess which person receives a message.\n- **Post-send verification failed.** Do **not** retry. LINE may have accepted the\n  message already, so a retry risks sending it twice. Inspect the browser first,\n  then report what you found.\n- **LINE asks for a password, QR confirmation, MFA, OTP, or a security prompt.**\n  Only the user resolves these, through the handoff. Do not read, type, store,\n  relay, or log any credential or verification code.\n- **The session expired or was revoked mid-task.** Pause and ask the user to\n  reauthenticate through the handoff, then re-check before continuing.\n- **`doctor.sh` reports a blocker.** Report the exact missing pieces and its\n  remediation. Do not invoke `sudo` or a package manager, and never weaken a\n  network binding to work around a failure.\n- **The profile is locked by another hostname.** Another Chromium may genuinely\n  hold it. Report it; do not clear the lock.\n\n## Reference material\n\nRead these when you need depth; they are not needed for a normal send.\n\n| File | Read it when |\n| --- | --- |\n| [references/line-oa-ui-selectors.md](references/line-oa-ui-selectors.md) | The send flow fails to find the search box, the chat, or the composer |\n| [references/handoff-operations.md](references/handoff-operations.md) | Changing the session or handoff scripts, or diagnosing a handoff that will not verify |\n| [references/public-repository-checklist.md](references/public-repository-checklist.md) | Making the repository public |\n| [CONTRIBUTING.md](CONTRIBUTING.md) | Changing this repository |\n| [containers/README.md](containers/README.md) | Running the scripts in the Linux test environment |\n\n`scripts/send_line_oa_chat.py` is the only implementation of the send flow. Do\nnot write a second one.\n\nFile v0.1.4:containers/README.md\n\n# Container test environment\n\nA reproducible Linux environment for running and deliberately breaking this\nrepository's scripts. It exists because the scripts target Linux while\ndevelopment happens elsewhere, and because failure paths are far cheaper to\nexercise by picking a variant than by breaking a real host by hand.\n\n## What this is and is not authoritative for\n\n| Authoritative for | Not authoritative for |\n| --- | --- |\n| Script behavior | Which startup phase dominates |\n| Refusal and error paths | Absolute phase durations |\n| Dependency detection and remediation messages | Any reported speedup figure |\n| Handoff arm/revoke lifecycle | End-to-end message send |\n\nLatency results from this container are usable for rehearsing a measurement and\nfor same-host before-and-after comparison. They are **not** an answer to which\nphase dominates handoff startup. Both poles are distorted here, in the same\ndirection but by different and unpredictable amounts:\n\n- **Chromium** is slowed by the VM's CPU allocation and filesystem layer, and is\n  simultaneously *sped up* by using a throwaway profile instead of a real one.\n- **Tunnel registration** is slowed because Quick Tunnels prefer QUIC over UDP,\n  which a desktop VM's NAT can degrade into an HTTP/2 fallback, and because\n  egress goes through a workstation network rather than a datacenter link.\n\n## Distance from the target host\n\nThe target is **x86_64 Debian 13 (trixie)**, running as a Kubernetes pod. The\ncontainer here is **arm64 Debian 12 (bookworm)** on a Docker Desktop VM. They\ndiffer in instruction set, distribution release, and virtualization layer.\n\n| Differs | Effect |\n| --- | --- |\n| arm64 here vs x86_64 there | Different silicon; no basis for comparing absolute times |\n| Debian 12 vs Debian 13 | Different Chromium, Caddy, and cloudflared builds |\n| Docker Desktop VM vs a pod | Different CPU allocation and filesystem layer |\n| Throwaway profile instead of a real authenticated one | Deflates Chromium cold start |\n| Workstation egress, QUIC through the VM's NAT | Changes tunnel registration and DNS behaviour |\n| `seccomp=unconfined`, absent on the target | Changes sandbox setup cost |\n\nMeasured, once both sides had run the same script:\n\n| | container | target |\n| --- | --- | --- |\n| session start | 0.53s | 1.63s |\n| `tunnel_url` | 2.79s | 4.62s |\n\nThe container was **faster** on both — the opposite of what \"a VM inflates\nstartup\" would predict. That is the point: the direction of the distortion was\nnot predictable in advance, which is why this environment is not authoritative\nfor latency and why the measurements had to be repeated on the target.\n\nA concrete case: the container found that a 3s grace window failed outright,\nwhich looked like evidence of a propagation floor. The target sweep showed no\nfloor at all — misses are sporadic and independent of how long you wait. The\ncontainer result was one unlucky sample generalized into a rule. It was labelled\nnon-authoritative, and that label is why it was re-tested rather than shipped.\n\nTwo further divergences from a real host, both introduced by the container and\nneither present on the target:\n\n- `--security-opt seccomp=unconfined`, because Docker's default seccomp filter\n  blocks the namespace creation Chromium's zygote needs. This drops the\n  container's outer confinement so Chromium can build its own sandbox; the\n  browser's sandbox stays intact. Preferred over `--no-sandbox`, which would\n  mean teaching the scripts a container-only flag.\n- `--shm-size=1g`, because Chromium exhausts the 64MB default and dies with a\n  broken zygote pipe.\n\n## Credentials\n\nThe container is credential-free by construction, not by convention. Every\nbehavior it covers works against the LINE **login** page, so no session is ever\nneeded. `run.sh` builds every mount itself and forwards no Docker flags, and it\nrefuses an inherited `LINE_OA_SEND_CHAT_CHROMIUM_PROFILE` rather than silently\nignoring it. Do not mount a real profile.\n\n## Variants\n\nAll four derive from one Dockerfile; the table in `build.sh` is the only place\nthey differ, and their base layers are byte-identical.\n\n| Variant | Handoff deps | Playwright runtime | Profile |\n| --- | --- | --- | --- |\n| `full` | yes | yes | empty |\n| `no-runtime` | yes | no (and no `uv`) | empty |\n| `no-handoff-deps` | no | yes | empty |\n| `unauth-profile` | yes | yes | initialized, no session |\n\n`no-runtime` omits `uv` deliberately: `run_line_oa_chat.sh` falls back to\n`uv run --with playwright`, so leaving `uv` in place would let the variant\nsilently succeed and prove nothing.\n\n`unauth-profile` runs Chromium once at build time so its profile is initialized\nand previously used rather than an empty directory, which would make it\nidentical to `full`.\n\n## Usage\n\n```bash\n# Build all four variants (native architecture only)\ncontainers/build.sh\n\n# Build one\ncontainers/build.sh full\n\n# Run a command in a variant; default is an interactive shell\ncontainers/run.sh full bash\ncontainers/run.sh no-runtime bash scripts/run_line_oa_chat.sh --help\n\n# Start a browser session and check CDP from inside\ncontainers/run.sh full bash -lc '\n  bash scripts/start_line_oa_chromium.sh >/tmp/c.log 2>&1 &\n  sleep 20\n  curl -fsS http://127.0.0.1:9222/json/list'\n```\n\nThe repository is mounted read-only at `/workspace`. The browser profile lives\non a per-variant Docker volume, never a host path. No port is published, so\nnothing is externally reachable unless a command inside starts an outbound\ntunnel itself.\n\n## Tests\n\n```bash\n# Default path: no external exposure\ncontainers/test/default.sh\n\n# Opt-in: creates a real, externally reachable Cloudflare Quick Tunnel\nLINE_OA_TEST_ALLOW_TUNNEL=1 containers/test/tunnel.sh 60\n```\n\nThe default path covers the architecture guard, dependency presence, isolation,\nbrowser session startup, and every handoff refusal that happens before a tunnel\nwould be created. The tunnel test is separate and gated because it genuinely\nexposes the container's browser for the life of the tunnel.\n\n## Teardown\n\nContainers run with `--rm` and clean themselves up. Profile volumes persist on\npurpose, mirroring the real system's persistent profile, so removing them is\nexplicit:\n\n```bash\ncontainers/reset.sh            # drop profile volumes\ncontainers/reset.sh --images   # also drop the built images\n```\n\n## Architecture guard\n\nThe container refuses to run under emulation. `run.sh` passes the host\narchitecture in, and the entrypoint compares it against the container's own;\na mismatch, or a missing host architecture, exits non-zero. Emulated Chromium\nwould be slow enough to make every test unpleasant and every timing meaningless.\n\n## Known behavior worth remembering\n\nChromium writes a `SingletonLock` into the profile recording the hostname\nholding it. Docker assigns a random hostname per container, so a lock left by a\nkilled container is read as held \"on another computer\" and every later run is\nrefused. `run.sh` pins a per-variant hostname so Chromium recognizes a stale\nlock as its own and breaks it. A volume already poisoned by a random-hostname\nlock needs `containers/reset.sh` once. The same stale lock can strand a\nhard-killed Chromium on a real host.\n\nFile v0.1.4:README.md\n\n# line-oa-chat-send\n\nA safety-first CLI for sending messages through an already authenticated\n[LINE Official Account Chat](https://chat.line.biz/) browser session.\n\nThe send command attaches to a local Chromium DevTools (CDP) endpoint. It does\nnot start a browser, log in to LINE, or collect credentials.\n\n> **Optional login handoff — high-risk capability:** when a user explicitly asks\n> to complete LINE login or reauthentication through a remote GUI,\n> `scripts/start_line_oa_vnc_handoff.sh` arms a temporary Cloudflare noVNC URL\n> that grants its holder interactive control of the browser. It attaches to a\n> running session rather than starting one, requires the explicit\n> `LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login` scope, defaults to a 15-minute\n> TTL, keeps VNC and CDP loopback-only, and must be revoked as soon as login\n> ends. Revoking it leaves the browser session running.\n\n## What it does\n\n- Searches for a chat recipient and refuses ambiguous matches.\n- Opens the selected conversation and confirms the composer is available.\n- Defaults to a no-send dry run; requires an explicit `--send` to transmit.\n- After a send, checks that the composer cleared and the exact message appears in\n  the active transcript.\n\n## Requirements\n\n1. A user-authenticated LINE OA Chat session in Chromium.\n2. Chromium exposing a loopback CDP endpoint (default `http://127.0.0.1:9222`).\n3. Python with the `playwright` package. No browser download is needed — the tool\n   attaches to an existing Chromium.\n4. Explicit authorization for the recipient and the outgoing message.\n\nLinux is the supported platform. `containers/` provides a reproducible Linux\nenvironment for running the scripts from another OS.\n\n> **Authentication:** complete LINE login, password entry, QR confirmation, MFA,\n> OTP, and security prompts yourself in the interactive browser. This project\n> never accepts, stores, or transmits those secrets.\n\n## Check the environment first\n\n```bash\nbash scripts/doctor.sh\n```\n\n| Exit | Meaning |\n| --- | --- |\n| 0 | Ready; a send can run |\n| 3 | Environment usable, LINE authentication required — a checkpoint, not a failure |\n| 1 | Blocked; the missing pieces and their remediation are printed |\n\nIt reports what the launcher will actually do, and never suggests widening a\nnetwork binding. It invokes no package manager and no `sudo`.\n\nIf it reports no Python runtime:\n\n```bash\nbash scripts/setup_line_oa_runtime.sh --runtime-dir <private-dir> --skip-browser-install\nexport LINE_OA_PYTHON=<private-dir>/venv/bin/python\n```\n\n## Usage\n\n```bash\n# Start the browser session. Owns the display, Chromium, and a loopback-only\n# screen-sharing stack. Nothing is externally reachable.\nbash scripts/start_line_oa_chromium.sh\n\n# Safe default: find and open a uniquely matched chat, send nothing.\nbash scripts/run_line_oa_chat.sh --recipient \"Recipient name\" --message \"Message text\"\n\n# Send, only after the recipient and exact text have been authorized.\nbash scripts/run_line_oa_chat.sh --recipient \"Recipient name\" --message \"Message text\" --send\n\n# Stop the session. The persistent profile is preserved.\nbash scripts/stop_line_oa_chromium.sh\n```\n\nUse `--cdp-url` if the endpoint is not the default. `--help` lists all options.\n\n## Login / reauthentication handoff\n\nOnly after the user explicitly requests an interactive login session. It is a\nlogin-only capability; do not use it to send messages or for unrelated browser\ncontrol.\n\n```bash\nexport LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login\nexport LINE_OA_SEND_CHAT_HANDOFF_TTL_SECONDS=900   # optional; 60-3600\nbash scripts/start_line_oa_vnc_handoff.sh\n\n# When the user says login is done:\nbash scripts/stop_line_oa_vnc_handoff.sh\n```\n\nThe script attaches to the running session, arms a private high-entropy route,\nstarts the tunnel, and **verifies the public URL is reachable before printing\nit**. On failure it prints no URL, revokes what it started, and exits non-zero\nnaming the failing phase. It refuses to arm with no session, with stale session\nstate, without the login purpose, with an out-of-range TTL, or when a handoff is\nalready armed.\n\nThe printed URL is a bearer secret. Share it only with that user in a private\nchannel, never log or reuse it, and revoke as soon as login ends. TTL expiry is a\nbackstop, not a substitute for prompt revocation.\n\nArming takes roughly 20–25 seconds, most of it a deliberate quiet window before\nthe first DNS lookup. See\n[references/handoff-operations.md](references/handoff-operations.md) for why\nprobing sooner makes it slower.\n\n## Safety behavior\n\n- A run without `--send` never transmits a message.\n- If more than one chat may match the recipient, the command stops instead of\n  guessing.\n- If the authenticated UI, chat selection, composer, or post-send verification\n  fails, the command exits non-zero.\n- Do not retry a failed post-send verification automatically: LINE may have\n  accepted the message even if verification did not complete.\n- CDP, VNC, websockify, and the HTTP front end stay on loopback. The front end\n  answers 404 until a handoff is armed. Nothing is externally reachable between\n  handoffs.\n\n## Testing\n\n```bash\ncontainers/build.sh                                  # four dependency variants\ncontainers/test/default.sh                           # no external exposure\nLINE_OA_TEST_ALLOW_TUNNEL=1 containers/test/tunnel.sh 60   # opt-in; exposes a browser\n```\n\nSee [containers/README.md](containers/README.md), including what the container is\nand is not authoritative for.\n\n## Documentation\n\n- [SKILL.md](SKILL.md) — what an agent needs to decide and act\n- [references/](references/) — UI selectors, handoff operations, publication checklist\n- [CONTRIBUTING.md](CONTRIBUTING.md) — development and release process\n\nFile v0.1.4:_meta.json\n\n{\n  \"ownerId\": \"kn743ga1fg7x5xfqphk150apyd8apt23\",\n  \"slug\": \"line-oa-chat-send\",\n  \"version\": \"0.1.4\",\n  \"publishedAt\": 1785308117707\n}\n\nFile v0.1.4:references/handoff-operations.md\n\n# Handoff operations\n\nWhy the session and handoff scripts look the way they do. Everything here is\nalready enforced in code — this is the reasoning, for when the code needs\nchanging. Read it before editing `scripts/start_line_oa_chromium.sh`,\n`scripts/start_line_oa_vnc_handoff.sh`, or `scripts/lib/*.sh`.\n\n## Shape\n\n```\nBrowser session (long-lived, loopback only)\n  Xvfb ── Chromium ── CDP 127.0.0.1:9222\n    └──── x11vnc ──── websockify ──── Caddy (404 until armed)\n\nLogin handoff (ephemeral, the only thing that is ever externally reachable)\n    Caddy token route ──── cloudflared Quick Tunnel ──── public HTTPS URL\n```\n\nThe session owns the browser. The handoff attaches. Revoking the handoff removes\nthe tunnel and the route and leaves everything else running.\n\n## Ports are never assumed free\n\nVNC, websockify, Caddy, and Caddy's admin endpoint all get dynamically selected\nloopback ports, chosen in one call that holds each socket until all are chosen —\notherwise the same port can be handed out twice.\n\nFixed ports silently attach the handoff to something else. A hard-coded `5900`\ncan bind to another display and produce a black canvas; a hard-coded front-end\nport can collide and leave a working tunnel URL serving nothing.\n\n## Caddy's output is not discarded\n\nA bind conflict with output sent to `/dev/null` surfaces as \"the URL works but\nthe page is blank\" — expensive to diagnose, trivial to prevent. Output goes to\nthe run log.\n\n## The site address is host-agnostic\n\n```\n:PORT {\n  bind 127.0.0.1\n  ...\n}\n```\n\nNot `http://127.0.0.1:PORT`. The tunnel arrives carrying a public Host header,\nand a host-specific site address rejects it. `bind 127.0.0.1` keeps the listener\non loopback while leaving the matcher host-agnostic.\n\nKeep `handle` blocks multi-line. Compact inline blocks can fail Caddy parsing.\n\n## x11vnc needs `-noxrecord -noxfixes -noxdamage`\n\nWithout them noVNC can stay black even while Chromium has a mapped window on the\ndisplay.\n\nThe cost is real: `-noxdamage` disables the damage extension, so x11vnc polls\nthe whole screen instead of being told what changed. That means higher CPU and a\nless responsive remote canvas. It is a deliberate trade — a slow picture beats no\npicture — and it affects interaction latency, not startup time.\n\n## Verification waits before it probes\n\nThe single most counterintuitive part.\n\nA fresh `trycloudflare.com` hostname is **not resolvable when cloudflared prints\nit**. A lookup that misses is cached as a negative answer, and re-querying keeps\nrefreshing that negative entry — so an eager polling loop holds itself in\nfailure long past actual propagation.\n\nMeasured:\n\n| Approach | Outcome |\n| --- | --- |\n| Probe immediately, poll every 1s | Hostname never resolved within 120s |\n| Wait 45s quietly, then query once | Resolved on the first query |\n\nThis is not a container artifact. systemd-resolved, dnsmasq, and container\nembedded DNS all cache negative answers.\n\nSo the handoff waits a quiet grace window (`LINE_OA_SEND_CHAT_HANDOFF_VERIFY_GRACE`,\ndefault 5s) before its first lookup, then backs off progressively, and records\nDNS resolution as a phase separate from HTTP reachability — they are different\nwaits with different causes.\n\n### There is no propagation floor to clear\n\nThe obvious model — propagation takes about N seconds, so wait N — does not\nsurvive measurement. Target host, three runs at each value plus three baseline\nruns at the 20s default:\n\n| grace | first-lookup hits | `url_verified` when it hit |\n| --- | --- | --- |\n| 5s | 3/3 | ~10.7s |\n| 8s | 3/3 | ~13.3s |\n| 12s | 3/3 | ~16.7s |\n| 16s | 2/3 | ~20.5s |\n| 20s | 3/3 sweep, 2/3 baseline | ~25.4s |\n\nTwo misses in 18 runs, ~11%, and both were large outliers: the lookup took\n40–60s longer than the grace, not slightly longer.\n\nSo propagation completed inside 5s for at least nine consecutive tunnels, and\nthe 20s run that missed waited 60s — four times the grace would not have saved\nit. Waiting longer costs ~15s on every fast arm and does not buy protection on\nthe slow ones.\n\nDo not read the misses landing on the two longest values as evidence that longer\nis *worse*. With three runs each that is well within chance. The defensible\nreading is that misses look independent of the grace window.\n\nHence: a short grace, and a backoff that recovers when an outlier appears. It\ndoes recover — the 16s miss verified at 61s rather than failing.\n\nTypical successful arm at the 5s default:\n\n```\nroute_armed     0.03s\ntunnel_url      4.60s   URL printed here, and not yet usable\ndns_resolved    9.60s   resolved on the first query after the grace window\nurl_verified   10.70s   HTTP succeeded on the first attempt\n```\n\n## A local request is not verification\n\n`curl http://127.0.0.1:<caddy-port>/...` proves the local chain works. It says\nnothing about whether the tunnel carries traffic. The script fetches the **public\nURL** and requires HTTP 200 with noVNC content before printing anything.\n\nNothing is printed on failure. The script revokes what it started and exits\nnon-zero naming the phase.\n\n## Phase timing\n\nEvery run of the session and handoff scripts writes a phase log to\n`$RUNTIME_DIR/timing/`, mode 600, with phase names and durations only — never a\nURL, route token, or credential. There is no debug flag, because the slow runs\nare the ones nobody thought to instrument.\n\n## Chromium's SingletonLock\n\nChromium writes a `SingletonLock` symlink into the profile recording\n`<hostname>-<pid>`. A hard-killed Chromium leaves it behind.\n\n- Same hostname, dead PID → stale; the session start clears it and says so.\n- Different hostname → **not** cleared. Another Chromium may genuinely hold the\n  profile, and clearing it could corrupt a live session. Reported distinctly so\n  the cause is obvious rather than surfacing as a generic launch failure.\n\nThis bites in containers especially, where a random per-container hostname makes\nevery rerun look like \"in use on another computer\".\n\n## Chromium in a container\n\nTwo concessions apply to the test environment only, never to a real host:\n`--security-opt seccomp=unconfined` (Docker's default filter blocks the\nnamespace creation the zygote needs) and `--shm-size` (Chromium exhausts the\n64MB default). See `containers/README.md`. Neither is a reason to teach the\nscripts a container-only flag.\n\n## Running as root\n\nChromium refuses to start as root with its sandbox enabled\n(`Running as root without --no-sandbox is not supported`).\n\nThe fix is to run unprivileged, under a service identity that owns the profile\ndirectory. That is how the browser is meant to run anyway.\n\n```bash\nuseradd --create-home --uid 1000 lineoa\nchown -R lineoa:lineoa /opt/data/chromium\nsu - lineoa -c 'cd <repo> && bash scripts/start_line_oa_chromium.sh'\n```\n\nFor deployments that genuinely cannot drop privileges — a pod fixed to root, for\ninstance — there is an explicit opt-in:\n\n```bash\nLINE_OA_SEND_CHAT_ALLOW_NO_SANDBOX=1 bash scripts/start_line_oa_chromium.sh\n```\n\nIt is opt-in and it warns on every start, because the cost is not theoretical.\nOrdinarily this browser only visits `chat.line.biz`, so a disabled renderer\nsandbox has little to contain. **A login handoff changes that**: it grants\ninteractive control, so whoever holds the URL can navigate the browser anywhere,\nand an unsandboxed renderer contains far less of whatever they reach. The same\nbrowser holds the authenticated LINE session.\n\nIf you must run this way, keep handoffs short and treat the URL with more care,\nnot less.\n\nFile v0.1.4:references/line-oa-ui-selectors.md\n\n# LINE OA Chat: UI selector notes\n\nWhy the send flow targets what it targets. The implementation is\n`scripts/send_line_oa_chat.py` — the only one. This file explains the reasoning\nbehind its selectors so they can be repaired when LINE changes the UI, and does\nnot restate the algorithm.\n\n## Choosing the page\n\nSelect the page whose URL begins with `https://chat.line.biz/`, and require\nexactly one such page. More than one is ambiguous — two tabs can hold two\ndifferent conversations, and picking either is a guess about where a message\nlands. Zero means the session is not authenticated or the browser is elsewhere.\n\n## Searching for a recipient\n\nThe chat-list search is the sidebar input labelled `搜尋`.\n\n**Match the placeholder exactly.** Playwright's placeholder matching is\nsubstring-based by default, and LINE has an unrelated `輸入搜尋內容` field that a\nloose match also hits. `get_by_placeholder(\"搜尋\", exact=True)` selects the chat\nlist search and nothing else.\n\n## Selecting the result\n\nTake the anchor that contains the matching `<mark>`, not a text match on the\nrecipient name.\n\nTwo distinct reasons:\n\n- **Name collisions reach the wrong element.** `get_by_text(recipient,\n  exact=True)` can match the signed-in account menu rather than the chat result\n  when the names coincide. The account menu is not a conversation.\n- **A hidden label is not a missing result.** On a narrow layout the sidebar\n  hides the chat label, so the matching text has no bounding box — but its\n  containing chat-list `<a>` is still present and clickable. Treating the hidden\n  label as \"not found\" turns a responsive layout into a false negative. Locate\n  the anchor from the `<mark>` instead.\n\n## Confirming the conversation loaded\n\nCheck that the message composer is visible and prior conversation content is\npresent before typing. A click that has been dispatched is not a conversation\nthat has rendered.\n\n## The composer\n\nThe composer lives inside a custom element. `page.locator(\"textarea-ex\")\n.locator(\"textarea\")` pierces the shadow DOM to the real editable element.\n\nA placeholder-based lookup matches both the `textarea-ex` host and its internal\n`textarea`, and filling the host does not fill the field. Chain the locators so\nthe target is unambiguous.\n\nSubmit with `input[type=\"submit\"][value=\"傳送\"]`.\n\n## Verifying the send\n\nA successful click is not evidence that a message was sent. Two conditions\ntogether are:\n\n1. the composer cleared, and\n2. the exact text appears in the active transcript.\n\nPrefer checking the recent transcript tail over a page-wide text match. A\npage-wide match can be satisfied by an identical message sent earlier, which\nwould confirm a send that did not happen.\n\n**If verification fails, do not retry.** LINE may have accepted the message\nalready; a retry risks sending twice. Inspect the browser first.\n\nFile v0.1.4:references/public-repository-checklist.md\n\n# Making the repository public\n\nFollow this in order. Publication is irreversible in practice: anything reachable\nin Git history stays reachable once it has been fetched, and removing it later\ndoes not un-publish it.\n\n## 1. Audit all reachable history, not just the current files\n\nOld commits and merged pull requests remain visible after publication. Scan the\nfull history, not the working tree.\n\nLook for:\n\n- credentials, tokens, and authentication headers\n- tunnel or share URLs (they are bearer secrets)\n- browser-profile data, cookies, or session artefacts\n- host-specific runtime paths that reveal infrastructure\n\nWhen reporting findings, report **commit IDs and pattern categories only** —\nnever the matched values. A report that quotes the secret has republished it.\n\nRemove or rewrite genuinely sensitive history before publication. Rewriting\nhistory after the fact is far more disruptive than doing it now.\n\n## 2. Apply public-facing settings before changing visibility\n\n- Remove collaboration surfaces that are not in use.\n- Enable automatic deletion of merged branches.\n- Disable Actions that have not been reviewed for a public context. A workflow\n  that was safe in a private repository can leak in a public one, and public\n  forks change who can trigger what.\n\n## 3. Change visibility\n\n## 4. Verify from outside\n\nFetch the repository and its README through an **unauthenticated** request. A\nbrowser already signed in proves nothing.\n\n## 5. Enable public-repository security controls\n\n- secret scanning\n- push protection\n- Dependabot\n\nGitHub may enable some of these automatically and may reject others depending on\nplan and org policy, so **read the resulting state back** rather than assuming\nthe request applied.\n\n## 6. Licensing is a separate decision\n\nTreat it as a legal question, not a checklist item. Do not choose a licence\nwithout the user's direction.\n\n## Notes specific to this repository\n\n- The ClawHub workflow publishes on tag push and needs its token secret to exist\n  in the repository's Actions secrets. Confirm the secret's scope before the\n  repository becomes public.\n- `scripts/` contains no credentials by design: the browser profile lives outside\n  the repository, and the runtime directory is created at run time. Confirm that\n  no run has written a profile, a runtime directory, or a timing log inside the\n  working tree.\n- Phase-timing logs contain durations only, but they live in the private runtime\n  directory and should never be committed regardless.\n\nFile v0.1.4:CONTRIBUTING.md\n\n# Contributing\n\nDevelopment process for this repository. This is not skill knowledge — an agent\nsending a message does not need any of it. See [SKILL.md](SKILL.md) for that.\n\n## Changing the skill\n\nUse the normal Git/PR lifecycle. Do not commit to `main` directly.\n\n1. Start from the current `main` and create a focused branch, e.g.\n   `docs/update-authentication`.\n2. Make the change and validate it.\n3. Commit and push the branch.\n4. Open a PR with `gh pr create`, summarising the change and how it was verified.\n5. Merge only after the user explicitly approves. A concise \"merge\" is sufficient\n   authorization. Use a squash merge, delete the feature branch, then verify the\n   PR state and branch deletion with `gh`.\n\nDocumentation-only changes, including README updates, follow the same path.\nBranch, validate, push, open a PR, leave it for review.\n\n## Verifying a PR before reporting it\n\nTest the exact remote revision, not a local copy that might differ:\n\n```bash\ngh pr view <number> --json headRefOid\ngit switch -c pr-<number> <sha>     # non-tracking branch at the verified SHA\n```\n\nA non-tracking branch is the fallback when a shallow clone's fetch refspec\nprevents `gh pr checkout` from setting up tracking.\n\nThen run, at minimum:\n\n```bash\nbash containers/build.sh                 # all four variants\nbash containers/test/default.sh          # no external exposure\nbash scripts/doctor.sh                   # environment verdict\n```\n\nThe container suite covers script behavior, refusal paths, and dependency\ndetection. It is **not** authoritative for latency — see\n[containers/README.md](containers/README.md).\n\nTunnel behavior is opt-in because it genuinely exposes a browser:\n\n```bash\nLINE_OA_TEST_ALLOW_TUNNEL=1 containers/test/tunnel.sh 60\n```\n\nA change touching the send path additionally needs a no-send dry run against a\nreal authenticated LINE UI, which the credential-free container cannot provide.\n\n## GitHub CLI authentication\n\nRun `gh auth status` in the same OS user and runtime that will run the commands.\nA browser session or a CLI login in another shell does not authenticate this one.\n\nIf auth is missing:\n\n```bash\ngh auth login --hostname github.com --git-protocol https --web\n```\n\nWhen the user asks you to \"just run it and give me the code\", run the flow and\nreply with only the one-time device code and `https://github.com/login/device`.\nNo extra explanation. Wait for their authorization, then confirm with\n`gh auth status`.\n\n## Publishing\n\n`.github/workflows/publish-clawhub.yml` publishes to ClawHub on tag push. It\npublishes the repository root, so `references/` and `containers/` ship with the\nskill. Broken relative links reach consumers — check them before tagging.\n\nThe workflow needs `CLAW_HUB_TOKEN` in the repository's Actions secrets.\n\n## Making the repository public\n\nSee [the public repository checklist](references/public-repository-checklist.md)\nfor the full audit, settings, and verification sequence. Audit **all reachable\nhistory**, not just the current files.\n\n## Repository layout\n\n| Path | Purpose |\n| --- | --- |\n| `SKILL.md` | What an agent needs to decide and act |\n| `README.md` | What a human evaluating or operating the tool needs |\n| `scripts/` | The implementation |\n| `scripts/lib/` | Shared shell libraries |\n| `references/` | Depth material, read on demand |\n| `containers/` | Linux test environment and its suites |\n| `openspec/` | Change proposals, designs, specs, and tasks |\n\nFile v0.1.4:openspec/changes/archive/2026-07-29-add-container-test-env/design.md\n\n## Context\n\nThe scripts assume a Linux host with Xvfb, x11vnc, websockify, noVNC assets, Caddy, cloudflared, Chromium, and a Python runtime with Playwright. Development happens on macOS on Apple Silicon. The verification tasks in `speed-up-login-handoff` — the refusal matrix, the forced verification failure, the arm/revoke lifecycle — and the four-way environment matrix in `trim-skill-docs` currently have nowhere to run.\n\nA container solves execution but introduces a measurement trap. The central open question in `speed-up-login-handoff` is which pole dominates handoff startup: Chromium cold start or Cloudflare Quick Tunnel registration. On Docker Desktop on Apple Silicon both poles are distorted, in the same direction but by different and unpredictable amounts:\n\n- Chromium is slowed by the VM's CPU allocation and by bind-mount filesystem overhead, would be catastrophically slowed by running an amd64 image under emulation, and runs on different silicon from the likely x86_64 target. It is simultaneously *sped up* by using a small throwaway profile instead of a real authenticated one.\n- Tunnel registration is slowed because Quick Tunnels prefer QUIC over UDP, which Docker Desktop's NAT can degrade into an HTTP/2 fallback, and because egress goes through a workstation network rather than a datacenter link.\n\nThe ratio between the two poles is therefore unreliable, and the ratio is the entire question. This has to be a stated constraint, not a footnote, or someone will read container numbers as an answer.\n\nOne property makes the container unusually safe here: phase timing and every refusal path exercise Chromium loading the LINE **login** page. None of them require an authenticated session, so the container never needs a real profile or any credential.\n\n## Goals / Non-Goals\n\n**Goals:**\n\n- Give the repository's scripts a reproducible Linux execution surface.\n- Make failure paths cheap to exercise by varying the environment declaratively instead of breaking a host by hand.\n- Keep the container credential-free by construction.\n- State, in the specification rather than in prose, what container results may and may not be used to conclude.\n\n**Non-Goals:**\n\n- Producing latency attribution or final speedup numbers. Those belong to the target host.\n- Running the scripts in production from a container. This is a test and rehearsal surface.\n- Supporting macOS or Windows as script targets. Linux remains the only target; the container is how a non-Linux workstation reaches it.\n- Adding CI execution. The image is usable from CI later, but wiring that is out of scope.\n\n## Decisions\n\n### The image is architecture-native, and a mismatch fails loudly\n\nThe image builds and runs for the host architecture. An amd64 image under emulation on Apple Silicon would make every timing observation meaningless and every test slow enough to be abandoned, so an architecture mismatch is detected at container start and fails with a clear message rather than proceeding slowly.\n\nChromium comes from the distribution's own package rather than Playwright's managed download, since Playwright's Chromium builds do not cover linux-arm64 uniformly. This matches how the scripts already behave: `start_line_oa_chromium.sh` discovers a system Chromium, and `setup_line_oa_runtime.sh --skip-browser-install` exists precisely for the CDP-only case.\n\n### The browser profile lives on a container volume; repository sources are bind-mounted read-only\n\nTwo different concerns, two different mechanisms. The Chromium profile is I/O-heavy, so it goes on a container-native volume where filesystem performance is closest to native. The repository scripts are small and edited constantly, so they are bind-mounted read-only for fast iteration without copying the image on every change.\n\nRead-only mounting also prevents a test run from modifying the working tree.\n\n### A real authenticated profile is never mounted into the container\n\nThe container is credential-free by construction, not by convention. Every behavior it needs to exercise — startup phases, refusal paths, dependency detection, arm and revoke — works against the LINE login page. Mounting a real profile would add no test coverage and would put a live session inside a throwaway environment.\n\nA consequence worth stating: the container cannot verify a successful message send end-to-end. That verification stays on the target host with a real session.\n\n### Environment variants are build targets, not separate definitions\n\nThe variants — full, missing Python/Playwright runtime, missing handoff dependencies, present-but-unauthenticated profile — derive from one definition so they cannot drift apart. `trim-skill-docs` names exactly these four for `doctor.sh`; deriving them from a common base means a change to the base propagates to all of them.\n\nAlternative considered: separate definitions per variant. Rejected — four copies of an environment definition diverge, and a stale variant produces a false pass.\n\n### Tunnel-dependent tests are opt-in and time-bounded\n\nArming a handoff creates a publicly reachable URL through an outbound tunnel, requiring no published ports but exposing a browser to the internet for the life of the tunnel. Most tests — the refusal matrix, dependency detection, arm-refusal cases — never reach the point of creating a tunnel.\n\nThe default test path therefore requires no external egress. Tests that create a real tunnel are explicitly opted into and inherit the handoff's own TTL bound. The container's browser holds no session, so the exposure is a blank LINE login page, but the default should not silently open one.\n\n### The container's authority is scoped in the specification\n\nContainer results are authoritative for: script behavior, refusal and error paths, dependency detection and remediation messages, and arm/revoke lifecycle correctness.\n\nContainer results are **not** authoritative for: which startup phase dominates, absolute phase durations, or any reported speedup figure. Those require a run on the target Linux host.\n\nWriting this as a requirement rather than a README note is deliberate — the numbers are easy to produce and tempting to quote.\n\n## Risks / Trade-offs\n\n- **Container numbers get quoted as latency conclusions anyway** → The scoping requirement is explicit, and the measurement tasks in `speed-up-login-handoff` are marked target-host-only so the boundary appears where the work happens, not only in this change.\n- **Container passes but the target host fails, because of a distribution or architecture difference** → The container is a rehearsal surface, not a substitute for target-host verification. Both changes retain their target-host verification tasks.\n- **The image drifts from what the target host actually has** → The image installs the same dependencies the scripts check for, so `doctor.sh` running clean inside the container is itself a consistency signal.\n- **Maintaining a container becomes its own cost for a small repository** → Kept minimal: one definition, variants as build targets, no orchestration, no CI wiring.\n- **A test tunnel is left running after a failed test** → Tunnel tests are opt-in and bounded by the handoff TTL, and container teardown removes the tunnel with the container.\n\n## Open Questions\n\n- ~~What is the target host's architecture and distribution?~~ **Answered, and the first answer was wrong.** Recorded as arm64 Debian from the operator's initial statement; the first measurement run reported `x86_64 Debian GNU/Linux 13 (trixie)`, running as a Kubernetes pod. The container is arm64 Debian 12. So the two differ in instruction set, distribution release, and virtualization layer — the opposite of what was recorded, and the \"no silicon difference\" claim was withdrawn.\n\n  This strengthens rather than weakens the scoping decision. Had the container been treated as authoritative for latency, it would have shipped a wrong conclusion: it found a 3s grace window failing outright, which looked like a propagation floor, while the target showed no floor at all. Measured deltas are recorded in `containers/README.md`; notably the container was *faster* on both session start and tunnel registration, so even the direction of the distortion was not predictable in advance.\n\nFile v0.1.4:openspec/changes/archive/2026-07-29-add-container-test-env/proposal.md\n\n## Why\n\nEvery script in this repository targets Linux, but development happens on macOS, so there is currently no place to execute the verification tasks that `speed-up-login-handoff` and `trim-skill-docs` depend on. The repository also has no test surface at all — no tests, and CI that only publishes on tags. Both problems are solved by the same thing: a reproducible Linux container that can run the scripts and be deliberately broken to exercise their failure paths.\n\n## What Changes\n\n- Add a container image that provides the full Linux runtime the scripts expect: X display server, VNC server, websockify with noVNC assets, HTTP front end, tunnel client, Chromium, and a Python/Playwright runtime.\n- Add environment variants of that image that are missing specific dependencies, so failure and remediation paths can be exercised without hand-breaking a real host.\n- Add a documented way to run the repository's scripts inside the container against a throwaway browser profile.\n- Constrain what the container may be used to conclude: it is authoritative for behavior, refusal paths, and dependency detection, and explicitly **not** authoritative for latency attribution or reported speedup numbers.\n- Make tunnel-dependent tests opt-in rather than part of the default test path, since arming a real handoff creates a publicly reachable URL.\n- Annotate the tasks in `speed-up-login-handoff` and `trim-skill-docs` that must run on the real target host rather than in the container.\n\n## Capabilities\n\n### New Capabilities\n- `container-test-env`: a reproducible Linux environment for executing and deliberately breaking this repository's scripts, including its dependency variants, its credential and egress boundaries, and the limits on what its results may be used to conclude.\n\n### Modified Capabilities\n<!-- No existing specs in openspec/specs/ yet; the capability above is new. -->\n\n## Impact\n\n- New container definition and its environment variants.\n- New documentation for running the scripts inside the container, referenced from `CONTRIBUTING.md` by the `trim-skill-docs` change.\n- `speed-up-login-handoff` — its verification tasks gain an execution surface; its measurement tasks gain an explicit \"target host only\" marker.\n- `trim-skill-docs` — its `doctor.sh` environment-variant task gains an execution surface.\n- No change to any script's behavior, network exposure model, or security boundary. The container consumes the scripts as they are.\n- Ordering: this change lands before the other two, because their verification tasks are otherwise blocked.\n\nFile v0.1.4:openspec/changes/archive/2026-07-29-add-container-test-env/specs/container-test-env/spec.md\n\n## ADDED Requirements\n\n### Requirement: The container provides the full Linux runtime the scripts expect\n\nThe container image SHALL provide every dependency the repository's scripts check for, so that a preflight run inside the container reports a usable environment.\n\n#### Scenario: Full variant satisfies preflight\n\n- **WHEN** the environment preflight is run inside the full container variant\n- **THEN** the display server, VNC server, websockify with noVNC assets, HTTP front end, tunnel client, Chromium, and Python runtime with Playwright are all reported usable\n\n#### Scenario: Chromium comes from the distribution\n\n- **WHEN** the image provides Chromium\n- **THEN** it uses the distribution's own package rather than a Playwright managed download\n- **AND** the Python runtime is provisioned without requiring a browser download\n\n### Requirement: The container runs architecture-native and refuses emulation\n\nThe container SHALL run natively on the host architecture and SHALL fail loudly rather than run under emulation.\n\n#### Scenario: Architecture mismatch is detected\n\n- **WHEN** the container starts and its architecture does not match the host architecture\n- **THEN** it exits non-zero with a message naming the mismatch\n- **AND** does not proceed to run any script\n\n### Requirement: The container is credential-free by construction\n\nThe container SHALL NOT be given a real authenticated LINE profile or any credential.\n\n#### Scenario: Throwaway profile only\n\n- **WHEN** a container is started for testing\n- **THEN** its Chromium profile is a throwaway profile created for that container\n- **AND** no host path containing a real authenticated profile is mounted\n\n#### Scenario: Login page is sufficient for covered behavior\n\n- **WHEN** startup phases, refusal paths, or dependency detection are exercised\n- **THEN** they operate against the LINE login page\n- **AND** require no authenticated session\n\n#### Scenario: End-to-end send is out of the container's reach\n\n- **WHEN** a successful message send needs verification\n- **THEN** that verification is performed on the target host with a real session\n- **AND** is not claimed to be covered by the container\n\n### Requirement: Storage mechanism matches the workload\n\nThe container SHALL place the browser profile on container-native storage and SHALL mount repository sources read-only.\n\n#### Scenario: Profile on a container volume\n\n- **WHEN** the container runs Chromium\n- **THEN** its profile is on a container-native volume rather than a host bind mount\n\n#### Scenario: Sources are read-only\n\n- **WHEN** repository sources are made available inside the container\n- **THEN** they are mounted read-only\n- **AND** a test run cannot modify the working tree\n\n### Requirement: Environment variants derive from one definition\n\nThe image SHALL provide variants that omit specific dependencies, and all variants SHALL derive from a common base so they cannot drift apart.\n\n#### Scenario: Required variants exist\n\n- **WHEN** environment variants are built\n- **THEN** a full variant, a variant without the Python/Playwright runtime, a variant without the handoff dependencies, and a variant with a present but unauthenticated profile are all available\n\n#### Scenario: Variants share a base\n\n- **WHEN** a dependency in the base changes\n- **THEN** every variant reflects that change without a separate edit\n\n#### Scenario: Variants drive preflight verdicts\n\n- **WHEN** the environment preflight is run in a variant missing a dependency\n- **THEN** it reports that dependency as unusable with its documented remediation\n- **AND** does not report a different or unrelated failure\n\n### Requirement: Tunnel-creating tests are opt-in\n\nTests that create a publicly reachable tunnel SHALL NOT run as part of the default test path.\n\n#### Scenario: Default path needs no external egress\n\n- **WHEN** the default test path is run\n- **THEN** it completes without creating any externally reachable URL\n\n#### Scenario: Tunnel test is explicit and bounded\n\n- **WHEN** a test that creates a real tunnel is run\n- **THEN** it is explicitly opted into\n- **AND** its exposure is bounded by the handoff's time-to-live\n- **AND** removing the container removes the tunnel\n\n### Requirement: Container results are authoritative only for behavior\n\nResults obtained in the container SHALL be treated as authoritative for behavior and SHALL NOT be treated as authoritative for latency attribution or reported speedup.\n\n#### Scenario: Behavioral results are accepted\n\n- **WHEN** the container exercises refusal paths, error paths, dependency detection and remediation messages, or the arm and revoke lifecycle\n- **THEN** those results are accepted as authoritative\n\n#### Scenario: Latency results are not accepted as conclusions\n\n- **WHEN** the container produces phase timings\n- **THEN** they are usable for rehearsing the measurement and for same-host before-and-after comparison\n- **AND** they are not used to decide which startup phase dominates\n- **AND** they are not reported as the change's speedup figure\n\n#### Scenario: Measurement tasks are marked target-host-only\n\n- **WHEN** a task determines phase dominance or reports a final speedup number\n- **THEN** it is marked as requiring execution on the target Linux host\n\nFile v0.1.4:openspec/changes/archive/2026-07-29-add-container-test-env/tasks.md\n\n## 1. Base image\n\n- [x] 1.1 Choose a Linux base and confirm it packages Xvfb, x11vnc, websockify, noVNC assets, Caddy, and Chromium for the host architecture\n- [x] 1.2 Add the tunnel client from its signed vendor repository, matching the instructions the handoff script already prints\n- [x] 1.3 Provision the Python runtime with Playwright without a browser download, using the existing skip-browser-install path\n- [x] 1.4 Add an architecture guard at container start that exits non-zero on a mismatch with the host architecture\n- [x] 1.5 Build natively for the host architecture and confirm no emulation layer is engaged\n\n## 2. Runtime wiring\n\n- [x] 2.1 Place the Chromium profile on a container-native volume, not a host bind mount\n- [x] 2.2 Mount repository sources read-only and confirm a test run cannot modify the working tree\n- [x] 2.3 Confirm no host path containing a real authenticated profile can be mounted by the documented run commands\n- [x] 2.4 Start a browser session inside the container and confirm the CDP endpoint responds and the LINE login page loads\n- [x] 2.5 Confirm no port is published and nothing is externally reachable while no handoff is armed\n\n## 3. Environment variants\n\n- [x] 3.1 Derive all variants from the common base as build targets\n- [x] 3.2 Add the full variant\n- [x] 3.3 Add a variant without the Python/Playwright runtime\n- [x] 3.4 Add a variant without the handoff dependencies\n- [x] 3.5 Add a variant with a present but unauthenticated profile\n- [x] 3.6 Confirm a base dependency change propagates to every variant without a separate edit\n\n## 4. Test paths\n\n- [x] 4.1 Define a default test path that completes without creating any externally reachable URL\n- [x] 4.2 Make tunnel-creating tests opt-in, bounded by the handoff TTL, and removed with the container\n- [x] 4.3 Document how to run the repository's scripts inside the container for each variant\n- [x] 4.4 Confirm container teardown leaves no tunnel, no volume, and no process behind\n\n## 5. Prove the variants drive real verdicts\n\n> Verdict-level checks moved to `trim-skill-docs` task group 2, because they\n> need `scripts/doctor.sh`, which that change creates: a single usable /\n> not-usable summary, \"authentication required\" as a verdict distinct from a\n> broken environment, and \"no remediation widens a network binding\". What\n> remains here is proving each variant actually presents the environment state\n> those checks will consume, against the dependency detection that exists today.\n> All four are asserted in `containers/test/default.sh`.\n\n- [x] 5.1 Confirm the full variant presents every dependency: Xvfb, x11vnc, websockify, noVNC assets, Caddy, cloudflared, Chromium, and an importable Playwright runtime\n- [x] 5.2 Confirm the missing-runtime variant makes `run_line_oa_chat.sh` report that no Python runtime with Playwright was found\n- [x] 5.3 Confirm the missing-handoff-dependency variant makes the handoff script name the exact missing commands and print operator install instructions without invoking a package manager\n- [x] 5.4 Confirm the unauthenticated-profile variant presents an initialized, previously-used, session-free profile\n\n## 6. Scope the container's authority\n\n- [x] 6.1 Document that container results are authoritative for behavior, refusal and error paths, dependency detection, and arm/revoke lifecycle\n- [x] 6.2 Document that they are not authoritative for phase dominance, absolute durations, or reported speedup\n- [x] 6.3 Record the distortions that motivate the limit: virtualized CPU and filesystem, architecture difference from the target, throwaway rather than real profile, and tunnel egress path\n- [x] 6.4 Resolve the open question on the target host's architecture and distribution, and record how far it diverges from the container (target is arm64 Debian: architecture and distribution match, so no silicon difference and no emulation; virtualization, throwaway profile, egress path, and seccomp=unconfined still differ)\n\n## 7. Wire into the dependent changes\n\n- [x] 7.1 Mark `speed-up-login-handoff` tasks 1.5, 1.6, 1.7, and 6.8 as requiring execution on the target Linux host\n- [x] 7.2 Note in `speed-up-login-handoff` that tasks 6.1 through 6.7 can be rehearsed in the container before the target-host run\n- [x] 7.3 Note in `trim-skill-docs` task 2.6 that its four environment variants are provided by this change\n- [x] 7.4 Confirm both dependent changes still hold their own target-host verification rather than delegating it to the container\n\nArchive v0.1.3: 10 files, 20582 bytes\n\nFiles: LICENSE (1064b), README.md (5350b), scripts/run_line_oa_chat.sh (1508b), scripts/send_line_oa_chat.py (5181b), scripts/setup_line_oa_runtime.sh (1644b), scripts/start_line_oa_chromium.sh (4526b), scripts/start_line_oa_vnc_handoff.sh (5069b), skill-card.md (2474b), SKILL.md (18277b), _meta.json (136b)\n\nFile v0.1.3:SKILL.md\n\n---\nname: line-oa-chat-send\ndescription: Send explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control.\n---\n\n# LINE OA Chat: send a message\n\n## Use when\nA user provides or has already opened a LINE OA Chat URL and asks to send a specific message to a named chat recipient.\n\n## Prerequisites\n- The persistent Chromium profile is already authenticated to LINE by the user.\n- Chromium is available via its local CDP endpoint (normally `http://127.0.0.1:9222`).\n- The user has explicitly authorized the outgoing message. Do not infer a message other than an unambiguous test message.\n\n## Security boundary\n- Normal message work uses local CDP only. It does not expose a browser, CDP, VNC, screenshots, cookies, or profile data to the network.\n- The optional noVNC handoff is **only** for a user to complete LINE login or reauthentication. It grants whoever holds its URL interactive control of the browser, so treat the URL as a high-risk bearer secret—not as a general browsing or support channel.\n- Start a handoff only after the user explicitly requests this login/reauthentication route. It has a default 15-minute TTL (configurable only from 60 to 3600 seconds), must be shared only in a direct private channel, and must be revoked immediately after login. Do not use it to send messages, browse unrelated sites, or perform autonomous actions.\n\n## Authentication and session setup\n1. Run Chromium with a dedicated persistent `--user-data-dir` owned only by the automation service. Do not use an ephemeral browser profile; the user’s LINE OA session and cookies must survive later message runs.\n2. Provide the user with a temporary protected interactive browser session **only after they explicitly request login or reauthentication through it**. Start it with:\n   ```bash\n   export LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login\n   export LINE_OA_SEND_CHAT_HANDOFF_TTL_SECONDS=900  # 60–3600; default 900\n   bash scripts/start_line_oa_vnc_handoff.sh\n   ```\n   It starts headed Chromium, loopback-only VNC/noVNC, a high-entropy private Caddy path, and a temporary Cloudflare Quick Tunnel, then emits one HTTPS noVNC URL. The holder of that URL can interactively control the browser: share it only with the intended user in a direct private channel; never log, commit, reuse, or relay it. Before sharing it, verify the public URL returns a non-empty HTTP 200 page containing `noVNC`; when available, confirm the title is `noVNC`, not an empty page. If either check fails, revoke the process, correct the local Caddy/noVNC route, and generate a fresh URL. Raw VNC and CDP remain loopback-only. When the user reports login completion, terminate the handoff immediately; the TTL is only a safety backstop. Before launching, ensure `LINE_OA_SEND_CHAT_XVFB_DISPLAY` is unused; if the default `:99` is already active, select another private local display (for example `:100`) rather than touching the existing display.\n3. The user completes LINE authentication themselves in that browser session, including password, QR confirmation, MFA, OTP, security prompts, and recovery flows. Do not request, read, type, store, relay, or log any credentials or verification codes.\n4. After the user says login is complete, inspect the existing browser page. Authentication is ready only when a `https://chat.line.biz/` page loads the authenticated chat UI rather than a sign-in, expired-session, or access-denied screen.\n5. Reuse the same persistent profile for subsequent sends. If LINE expires or revokes the session, pause the task and ask the user to reauthenticate through the protected interactive browser; then repeat the authenticated-UI check.\n\n## Starting Chromium and CDP\nWhen the CDP endpoint is unavailable, start **one** headed Chromium. Its persistent profile path is selected by `LINE_OA_SEND_CHAT_CHROMIUM_PROFILE`, falling back to `/opt/data/chromium`; the startup helper creates that directory with permission mode `700` when it is absent.\n\n1. Ensure a protected headed display already exists (`DISPLAY` or `--display`). The profile contains retained LINE session data: never commit, copy, inspect, or disclose it. A newly created fallback profile is not logged in, so the user must authenticate through the protected GUI before any send.\n2. Select the Chromium executable explicitly with `LINE_OA_CHROMIUM` / `--chromium`, or let the startup script discover a system Chromium command.\n3. Start the foreground process through a supervisor or tracked background process:\n   ```bash\n   export LINE_OA_SEND_CHAT_CHROMIUM_PROFILE=\"/private/operator-chosen/chrome-profile\"  # optional; default: /opt/data/chromium\n   export LINE_OA_CHROMIUM=\"/path/to/chromium\"  # optional when Chromium is on PATH\n   bash scripts/start_line_oa_chromium.sh\n   ```\n   `--profile-dir` can override the environment-selected profile for one invocation. The script binds CDP only to `127.0.0.1:9222`, refuses to start if that endpoint already responds, and opens `https://chat.line.biz/`. It does not log in or expose VNC/CDP publicly.\n4. Confirm CDP is live before using the send CLI:\n   ```bash\n   curl --max-time 3 -fsS http://127.0.0.1:9222/json/version >/dev/null\n   ```\n   Then inspect the existing LINE OA page. If LINE requires login, QR, MFA, OTP, or a challenge, only the user may resolve it through the protected GUI.\n\n## Shutdown after use\nWhen the user says the LINE OA work is finished, close Chromium to release server resources while preserving the private persistent profile for later reuse.\n\n1. Confirm no send, dry-run, setup, or user GUI interaction is active. If an unfinished composer draft or another user session may be open, ask before closing.\n2. Identify exactly one operator-owned Chromium root process through its configured local CDP endpoint or service supervisor. Do not rely on a fixed binary path, profile path, PID, or a broad `pkill` pattern.\n3. Send the root process a graceful termination signal (`SIGTERM`) and allow it time to exit. Do not delete, copy, inspect, or modify the persistent browser profile; it contains the retained session.\n4. Verify the local CDP endpoint is unreachable and Chromium browser/renderer processes have exited. A crash-report helper may remain briefly and does not retain the browser session.\n5. If the browser does not exit after the graceful timeout, report the blocker and wait for user direction before using a forced kill. Shut down any protected VNC/noVNC/tunnel components separately only when they exist solely for this browser and are no longer needed.\n\nA later start with the same private persistent profile will usually retain LINE login, but LINE may still expire or revoke the session and require user reauthentication through the protected GUI.\n\n## Environment preflight and safe provisioning\nBefore reporting that the environment is unavailable, **the agent must inspect and prepare it itself**. Do not merely ask the operator to install a missing Python package, browser, profile directory, or display when the following safe steps can resolve it.\n\n1. Select a private runtime directory from `LINE_OA_SEND_CHAT_RUNTIME_DIR`, or use `${XDG_STATE_HOME:-$HOME/.local/state}/line-oa-chat-send`. This is runtime/cache data only; never place it inside the Chromium profile or Git repository.\n2. If Playwright, its managed Chromium, or the selected runtime is absent, provision all three in one command:\n   ```bash\n   runtime_dir=\"${LINE_OA_SEND_CHAT_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/line-oa-chat-send}\"\n   bash scripts/setup_line_oa_runtime.sh --runtime-dir \"$runtime_dir\"\n   export LINE_OA_PYTHON=\"$runtime_dir/venv/bin/python\"\n   export LINE_OA_SEND_CHAT_BROWSER_DIR=\"$runtime_dir/ms-playwright\"\n   ```\n   The setup script creates private `700` directories, installs the Python Playwright dependency, and downloads its managed Chromium into the private runtime. It never reads a browser profile or handles LINE credentials. Use `--skip-browser-install` only for a client-only/CDP test that will not launch Chromium.\n3. Run `scripts/start_line_oa_chromium.sh`. It creates the selected profile directory when absent and, if `DISPLAY` is missing, starts a private local `Xvfb` display for the lifetime of its Chromium child. It does **not** expose VNC, noVNC, CDP, or a GUI to the network. A protected GUI-sharing service remains an operator-controlled prerequisite for user login.\n4. If `uv`, `Xvfb`, or a VNC/noVNC handoff dependency is missing, report the exact missing commands and print package-manager instructions for an operator with host package permission. On Debian-family hosts, install the base handoff packages first, then add Cloudflare's signed `cloudflared` apt repository before installing `cloudflared`; it is not assumed to exist in the default Debian repositories. The skill must not invoke `sudo`, `apt-get`, or another system package manager itself. Never weaken network bindings.\n5. After provisioning, health-check CDP and inspect the LINE page. A new profile will require the user to authenticate through the protected GUI; this is a security checkpoint, not an install failure.\n\nDo **not** assume a particular home directory, virtual environment, browser cache, or runtime folder exists. For browser-profile selection specifically, use `LINE_OA_SEND_CHAT_CHROMIUM_PROFILE` or its explicit fallback `/opt/data/chromium` as described above. The runtime requirements are: (1) a Python environment containing the `playwright` package and (2) an already-running, user-authenticated Chromium reachable through a local CDP endpoint. Connecting over CDP does not require Playwright to download or launch another browser.\n\nThe portable launcher still honors an explicit `LINE_OA_PYTHON`, then any current Python that imports Playwright, then `uv run --with playwright`. It connects only to an existing CDP browser; it does not log in or accept credentials.\n\n## CLI script\nUse `scripts/run_line_oa_chat.sh` rather than invoking `python scripts/send_line_oa_chat.py` directly. It connects only to an already-running local CDP endpoint; it does not perform login or accept credentials.\n\n```bash\n# Safe default: find and open the uniquely matched chat, but do not send.\nbash scripts/run_line_oa_chat.sh \\\n  --recipient \"<exact recipient>\" --message \"<message>\"\n\n# Send only after the user explicitly authorizes this exact recipient and message.\nbash scripts/run_line_oa_chat.sh \\\n  --recipient \"<exact recipient>\" --message \"<message>\" --send\n```\n\nThe script refuses ambiguous recipient results, requires `--send` for the external side effect, and exits non-zero when session/UI verification fails. After sending, it verifies that the composer cleared and the exact text appeared in the active transcript. Do not retry a failed verification until the protected browser is inspected, because LINE may already have accepted the message.\n\n## Procedure\n1. Connect with Playwright's sync API over CDP. Inspect all open pages and select the page whose URL begins with `https://chat.line.biz/`.\n2. Inspect the chat page before acting. Use the sidebar input with `aria-label=\"搜尋\"` / placeholder `搜尋` to search the requested recipient.\n3. Select the result from the chat list, not a same-named account/profile menu item. In this UI the searchable chat's text can be inside a `<mark>` and its clickable parent is an ancestor `<a>`.\n4. Confirm the selected conversation has loaded by checking that the message composer and prior conversation content are visible.\n5. Fill the actual composer inside LINE's custom element using the shadow-DOM-piercing locator `page.locator('textarea-ex').locator('textarea')`, then click `input[type=\"submit\"][value=\"傳送\"]`.\n6. Wait for UI update and verify the exact message appears in the active chat transcript. Report only after this verification succeeds.\n\n## Reference implementation\n```python\nfrom playwright.sync_api import sync_playwright\n\nrecipient = \"<exact recipient>\"\nmessage = \"<message>\"\nwith sync_playwright() as p:\n    browser = p.chromium.connect_over_cdp(\"http://127.0.0.1:9222\")\n    page = next(pg for ctx in browser.contexts for pg in ctx.pages\n                if pg.url.startswith(\"https://chat.line.biz/\"))\n    search = page.get_by_placeholder(\"搜尋\", exact=True)\n    search.fill(recipient)\n    page.wait_for_timeout(800)\n    chat = page.locator(\"mark\").filter(has_text=recipient).first.locator(\"xpath=ancestor::a\")\n    chat.click()\n    page.locator(\"textarea-ex\").locator(\"textarea\").fill(message)\n    page.locator('input[type=\"submit\"][value=\"傳送\"]').click()\n    page.wait_for_timeout(1200)\n    assert message in page.locator(\"body\").inner_text()\n```\n\n## UI reference\nSee [LINE OA UI selector notes](references/line-oa-ui-selectors.md) for the current DOM patterns and responsive-layout behavior observed in the web client.\n\n## Publishing updates\nWhen changing this skill in its GitHub repository, use the normal Git/PR lifecycle rather than committing to `main` directly.\n\n### Making the repository public\nUse [the public repository checklist](references/public-repository-checklist.md) for the full audit, settings, and verification sequence.\n\nBefore changing visibility, audit **all reachable Git history**, not only the current files: old commits and merged PRs remain visible after publication. Scan for credentials, authentication headers, tunnel/share URLs, browser-profile data, and host-specific runtime paths, reporting only commit IDs and pattern categories—not matched secret values. Remove or rewrite genuinely sensitive history before publication.\n\nApply public-facing settings before changing visibility: remove unused collaboration surfaces, enable automatic deletion of merged branches, and disable unused Actions until a reviewed workflow exists. After changing visibility, verify the repository and README through an unauthenticated request, then enable supported public-repository security controls (secret scanning, push protection, Dependabot). GitHub may automatically enable or reject some settings for public repositories, so read back the resulting state. Treat LICENSE selection as a separate legal decision; do not choose one without the user’s direction.\n\n1. Run `gh auth status` in the same OS user and runtime that will run Git/`gh` commands. A GitHub browser session or CLI login in another shell/user does not authenticate this runtime.\n2. If CLI auth is missing, start the browser/device flow with `gh auth login --hostname github.com --git-protocol https --web`. When the user asks to “just run it and give me the code,” run the flow directly and reply only with the generated one-time device code plus `https://github.com/login/device`; do not add extra explanation. Wait for their authorization, then verify with `gh auth status` before proceeding.\n3. Start from the current `main` branch and create a focused branch such as `docs/update-authentication`.\n4. Validate the edited `SKILL.md`, commit the change, and push the branch.\n5. Use GitHub CLI (`gh pr create`) to open a PR that summarizes the change and its verification.\n6. Test the exact remote PR revision before reporting it: obtain `headRefOid` with `gh pr view <number> --json headRefOid`, then check out that SHA in a clean worktree and run Python compilation, `--help`, and a no-send dry-run against the authenticated LINE UI. If a shallow clone has a restrictive fetch refspec and `gh pr checkout` cannot set tracking for the PR branch, create a local non-tracking branch at the verified `headRefOid` (for example, `git switch -c pr-<number> <sha>`). Do not test an unverified local copy instead.\n7. Treat documentation-only changes (including README updates) as repository changes too: branch, validate, push, open a PR, and leave it unmerged for review.\n8. Merge only after the user explicitly approves it. A concise `merge` instruction is sufficient authorization; use a squash merge and delete the feature branch, then verify the PR state and remote branch deletion with `gh`.\n\n## Pitfalls\n- `get_by_text(recipient, exact=True)` may target the signed-in account menu instead of the chat result when names collide.\n- On a narrow layout the matching chat label may have no bounding box because the sidebar hides text, while its containing chat-list `<a>` is still clickable. Locate the anchor from the matching `<mark>` instead of treating a hidden label as a failed search.\n- Playwright placeholder matching is substring-based by default. Use `page.get_by_placeholder(\"搜尋\", exact=True)` so the chat-list search does not also match LINE's unrelated `輸入搜尋內容` field.\n- `get_by_placeholder(...)` can match both the custom `textarea-ex` host and its internal textarea. Use `textarea-ex >> textarea` / chained locators so Playwright targets the true editable element.\n- A successful click alone is not sufficient; always verify the exact outgoing text in the active transcript after submission, preferably in the recent transcript tail rather than via a broad page-wide match.\n- If the recipient search yields more than one distinct chat, stop and ask the user which conversation to use.\n- Never request, handle, record, or transmit LINE passwords, OTPs, or other credentials.\n- A local `curl` success is insufficient for a Cloudflare Quick Tunnel: verify the bearer URL from outside the local listener. If the tunnel page is empty, check that Caddy uses a host-agnostic site address (for example `:6081`) with `bind 127.0.0.1`; `http://127.0.0.1:6081` can reject the public tunnel Host header. Keep multi-line Caddy `handle` blocks structurally formatted, because compact inline blocks may fail Caddy parsing.\n- Never assume VNC `5900` or websockify `6080` are free. Select unused loopback ports for each handoff and configure Caddy to reverse-proxy to the chosen websockify port. Fixed ports can silently attach the handoff to another display and yield a black canvas.\n- For an Xvfb display, invoke `x11vnc` with `-noxrecord -noxfixes -noxdamage`; otherwise noVNC can remain black even while Chromium has a mapped window. Confirm the actual remote canvas visually shows the LINE login window before instructing the user to authenticate.\n\nFile v0.1.3:README.md\n\n# line-oa-chat-send\n\nA reusable, safety-first CLI helper for sending messages through an already authenticated [LINE Official Account Chat](https://chat.line.biz/) browser session.\n\nThe command attaches to an existing local Chromium DevTools (CDP) endpoint. It does **not** start a second browser, log in to LINE, or collect credentials.\n\n> **Optional login handoff — high-risk capability:** If a user explicitly asks to complete LINE login or reauthentication through a remote GUI, this repository also provides `scripts/start_line_oa_vnc_handoff.sh`. It creates a temporary Cloudflare noVNC URL that grants its holder interactive control of the browser. It is not used for ordinary messaging, browsing, or autonomous actions. The script requires the explicit `LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login` scope, defaults to a 15-minute TTL, keeps VNC and CDP loopback-only, and must be stopped immediately when login ends.\n\n## What it does\n\n- Searches for a chat recipient and refuses ambiguous matches.\n- Opens the selected conversation and confirms that the message composer is available.\n- Defaults to a no-send dry run.\n- Requires an explicit `--send` flag before it can send a message.\n- After a send, checks that the composer cleared and the exact message appears in the active chat transcript.\n\n## Requirements\n\n1. A user-authenticated LINE OA Chat session is open in Chromium.\n2. Chromium exposes a local CDP endpoint (the default is `http://127.0.0.1:9222`).\n3. Python with the `playwright` package is available. Browser downloads are not required because this tool attaches to an existing Chromium over CDP.\n4. The person running the command has explicit authorization for the recipient and outgoing message.\n\n> **Authentication:** complete LINE login, password entry, QR confirmation, MFA, OTP, and security prompts yourself in the interactive browser. This project never accepts, stores, or transmits those secrets.\n\n## Environment setup\n\nNo runtime directory is assumed. The launcher checks an explicit `LINE_OA_PYTHON`, then current `python3`/`python`, and finally uses `uv run --with playwright` when `uv` is installed.\n\nIf none is available, provision a private runtime in an explicit location chosen by the operator:\n\n```bash\nbash scripts/setup_line_oa_runtime.sh --runtime-dir \"$HOME/.local/share/line-oa-chat-runtime\"\nexport LINE_OA_PYTHON=\"$HOME/.local/share/line-oa-chat-runtime/venv/bin/python\"\n```\n\nThe path above is an example only. The setup script never creates or accesses a browser profile, never opens a login flow, and never handles credentials. If CDP is unavailable, start one headed Chromium with a private profile and loopback-only CDP endpoint, then have the user log in through a protected interactive GUI. Do not start a second browser against an existing profile.\n\n## Login / reauthentication handoff\n\nUse this only after the user explicitly requests an interactive LINE login or reauthentication session. It is a login-only capability; do not use it to send messages or for unrelated browser control.\n\n```bash\nexport LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login\nexport LINE_OA_SEND_CHAT_HANDOFF_TTL_SECONDS=900  # optional; allowed range 60–3600\nbash scripts/start_line_oa_vnc_handoff.sh\n```\n\nThe printed URL is a bearer secret. Share it only with that user in a private channel, do not log or reuse it, and stop the process immediately after they finish login. TTL expiry is a backstop, not a replacement for prompt revocation.\n\n## Usage\n\nRun the launcher from the repository root:\n\n```bash\n# Safe default: find and open a uniquely matched chat, but do not send anything.\nbash scripts/run_line_oa_chat.sh \\\n  --recipient \"Recipient name\" \\\n  --message \"Message text\"\n```\n\nTo send a message, use `--send` **only** after the recipient and exact text have been explicitly authorized:\n\n```bash\nbash scripts/run_line_oa_chat.sh \\\n  --recipient \"Recipient name\" \\\n  --message \"Message text\" \\\n  --send\n```\n\nUse `--cdp-url` if the local endpoint is not the default. View all options with:\n\n```bash\nbash scripts/run_line_oa_chat.sh --help\n```\n\n## Safety behavior\n\n- A run without `--send` never transmits a message.\n- If more than one chat may match the supplied recipient, the command stops instead of guessing.\n- If the authenticated LINE UI, chat selection, composer, or post-send verification fails, the command exits non-zero.\n- Do not retry a failed post-send verification automatically: LINE may have accepted the message even if verification did not complete.\n- Keep the CDP endpoint local. Do not expose browser debugging ports, persistent profiles, cookies, screenshots, or VNC ports publicly.\n\n## Testing\n\nThe command can be tested safely against an authenticated session with a dry run:\n\n```bash\n# Direct Python reports a concrete recovery instruction when Playwright is missing.\npython3 scripts/send_line_oa_chat.py --help\n\n# The portable launcher selects or provisions a valid Python runtime.\nbash scripts/run_line_oa_chat.sh --help\nbash scripts/run_line_oa_chat.sh \\\n  --recipient \"Recipient name\" \\\n  --message \"CLI dry-run verification\"\n```\n\nA successful dry run reports the selected chat and confirms that no message was sent.\n\n## Skill documentation\n\nSee [SKILL.md](SKILL.md) for the complete automation procedure, authentication/session guidance, UI selector notes, and publishing workflow.\n\nFile v0.1.3:_meta.json\n\n{\n  \"ownerId\": \"kn743ga1fg7x5xfqphk150apyd8apt23\",\n  \"slug\": \"line-oa-chat-send\",\n  \"version\": \"0.1.3\",\n  \"publishedAt\": 1785253439265\n}\n\nFile v0.1.3:skill-card.md\n\n## Description: <br>\nSend explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[mosluce](https://clawhub.ai/user/mosluce) <br>\n\n### License/Terms of Use: <br>\nMIT <br>\n\n\n## Use Case: <br>\nDevelopers and operators use this skill to send a specific, explicitly authorized LINE Official Account Chat message to a named recipient through an already authenticated browser session. It also guides user-operated login or reauthentication when the existing session is missing or expired. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill operates against an authenticated LINE Official Account browser profile and can send chat messages. <br>\nMitigation: Use dry-run first, send only explicitly approved messages, and keep the Chromium profile private. <br>\nRisk: The optional noVNC handoff grants interactive browser control to anyone holding the temporary URL. <br>\nMitigation: Use handoff only for explicit login or reauthentication, share the URL only with the intended user in a private channel, and revoke it immediately after login. <br>\nRisk: Retrying after a failed post-send verification could duplicate a message if LINE accepted the first send. <br>\nMitigation: Inspect the protected browser session before retrying any failed send verification. <br>\n\n\n## Reference(s): <br>\n- [LINE Official Account Chat](https://chat.line.biz/) <br>\n- [ClawHub skill page](https://clawhub.ai/mosluce/skills/line-oa-chat-send) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Text, Shell commands, Configuration, Guidance] <br>\n**Output Format:** [Markdown guidance with inline shell commands and CLI status text] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Dry-run by default; external message sending requires an explicit --send flag and an authorized recipient/message pair.] <br>\n\n## Skill Version(s): <br>\n0.1.3 (source: server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nFile v0.1.3:LICENSE\n\nMIT License\n\nCopyright (c) 2026 mosluce\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\nArchive v0.1.2: 10 files, 19330 bytes\n\nFiles: LICENSE (1064b), README.md (4092b), scripts/run_line_oa_chat.sh (1508b), scripts/send_line_oa_chat.py (5181b), scripts/setup_line_oa_runtime.sh (1644b), scripts/start_line_oa_chromium.sh (4526b), scripts/start_line_oa_vnc_handoff.sh (4128b), skill-card.md (2386b), SKILL.md (17185b), _meta.json (136b)\n\nFile v0.1.2:SKILL.md\n\n---\nname: line-oa-chat-send\ndescription: Send and verify a LINE Official Account Chat message through an already authenticated persistent Chromium session.\n---\n\n# LINE OA Chat: send a message\n\n## Use when\nA user provides or has already opened a LINE OA Chat URL and asks to send a specific message to a named chat recipient.\n\n## Prerequisites\n- The persistent Chromium profile is already authenticated to LINE OA by the user.\n- Chromium is available via its local CDP endpoint (normally `http://127.0.0.1:9222`).\n- The user has explicitly authorized the outgoing message. Do not infer a message other than an unambiguous test message.\n\n## Authentication and session setup\n1. Run Chromium with a dedicated persistent `--user-data-dir` owned only by the automation service. Do not use an ephemeral browser profile; the user’s LINE OA session and cookies must survive later message runs.\n2. Provide the user with a temporary, protected interactive browser session. Run `bash scripts/start_line_oa_vnc_handoff.sh` when no authenticated profile exists. It starts headed Chromium, loopback-only VNC/noVNC, a high-entropy private Caddy path, and a temporary Cloudflare Quick Tunnel, then emits one HTTPS noVNC URL. Before sharing it, verify the public URL returns a non-empty HTTP 200 page containing `noVNC`; when available, open it in the browser tool and confirm the title is `noVNC`, not an empty page. If either check fails, revoke the failed process, correct the local Caddy/noVNC route, and generate a fresh bearer URL—never reuse the failed URL. Share only the verified bearer URL with the intended user in the DM; never log, commit, or reuse it. Raw VNC `5900` and CDP remain loopback-only. Before launching, ensure `LINE_OA_SEND_CHAT_XVFB_DISPLAY` is unused; if the default `:99` is already active, select another private local display (for example `:100`) for that invocation rather than touching the existing display. The user directly clicks, types, pastes, and scrolls in noVNC.\n3. The user completes LINE authentication themselves in that browser session, including password, QR confirmation, MFA, OTP, security prompts, and recovery flows. Do not request, read, type, store, relay, or log any credentials or verification codes.\n4. After the user says login is complete, inspect the existing browser page. Authentication is ready only when a `https://chat.line.biz/` page loads the authenticated chat UI rather than a sign-in, expired-session, or access-denied screen.\n5. Reuse the same persistent profile for subsequent sends. If LINE expires or revokes the session, pause the task and ask the user to reauthenticate through the protected interactive browser; then repeat the authenticated-UI check.\n\n## Starting Chromium and CDP\nWhen the CDP endpoint is unavailable, start **one** headed Chromium. Its persistent profile path is selected by `LINE_OA_SEND_CHAT_CHROMIUM_PROFILE`, falling back to `/opt/data/chromium`; the startup helper creates that directory with permission mode `700` when it is absent.\n\n1. Ensure a protected headed display already exists (`DISPLAY` or `--display`). The profile contains retained LINE session data: never commit, copy, inspect, or disclose it. A newly created fallback profile is not logged in, so the user must authenticate through the protected GUI before any send.\n2. Select the Chromium executable explicitly with `LINE_OA_CHROMIUM` / `--chromium`, or let the startup script discover a system Chromium command.\n3. Start the foreground process through a supervisor or tracked background process:\n   ```bash\n   export LINE_OA_SEND_CHAT_CHROMIUM_PROFILE=\"/private/operator-chosen/chrome-profile\"  # optional; default: /opt/data/chromium\n   export LINE_OA_CHROMIUM=\"/path/to/chromium\"  # optional when Chromium is on PATH\n   bash scripts/start_line_oa_chromium.sh\n   ```\n   `--profile-dir` can override the environment-selected profile for one invocation. The script binds CDP only to `127.0.0.1:9222`, refuses to start if that endpoint already responds, and opens `https://chat.line.biz/`. It does not log in or expose VNC/CDP publicly.\n4. Confirm CDP is live before using the send CLI:\n   ```bash\n   curl --max-time 3 -fsS http://127.0.0.1:9222/json/version >/dev/null\n   ```\n   Then inspect the existing LINE OA page. If LINE requires login, QR, MFA, OTP, or a challenge, only the user may resolve it through the protected GUI.\n\n## Shutdown after use\nWhen the user says the LINE OA work is finished, close Chromium to release server resources while preserving the private persistent profile for later reuse.\n\n1. Confirm no send, dry-run, setup, or user GUI interaction is active. If an unfinished composer draft or another user session may be open, ask before closing.\n2. Identify exactly one operator-owned Chromium root process through its configured local CDP endpoint or service supervisor. Do not rely on a fixed binary path, profile path, PID, or a broad `pkill` pattern.\n3. Send the root process a graceful termination signal (`SIGTERM`) and allow it time to exit. Do not delete, copy, inspect, or modify the persistent browser profile; it contains the retained session.\n4. Verify the local CDP endpoint is unreachable and Chromium browser/renderer processes have exited. A crash-report helper may remain briefly and does not retain the browser session.\n5. If the browser does not exit after the graceful timeout, report the blocker and wait for user direction before using a forced kill. Shut down any protected VNC/noVNC/tunnel components separately only when they exist solely for this browser and are no longer needed.\n\nA later start with the same private persistent profile will usually retain LINE login, but LINE may still expire or revoke the session and require user reauthentication through the protected GUI.\n\n## Environment preflight and safe provisioning\nBefore reporting that the environment is unavailable, **the agent must inspect and prepare it itself**. Do not merely ask the operator to install a missing Python package, browser, profile directory, or display when the following safe steps can resolve it.\n\n1. Select a private runtime directory from `LINE_OA_SEND_CHAT_RUNTIME_DIR`, or use `${XDG_STATE_HOME:-$HOME/.local/state}/line-oa-chat-send`. This is runtime/cache data only; never place it inside the Chromium profile or Git repository.\n2. If Playwright, its managed Chromium, or the selected runtime is absent, provision all three in one command:\n   ```bash\n   runtime_dir=\"${LINE_OA_SEND_CHAT_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/line-oa-chat-send}\"\n   bash scripts/setup_line_oa_runtime.sh --runtime-dir \"$runtime_dir\"\n   export LINE_OA_PYTHON=\"$runtime_dir/venv/bin/python\"\n   export LINE_OA_SEND_CHAT_BROWSER_DIR=\"$runtime_dir/ms-playwright\"\n   ```\n   The setup script creates private `700` directories, installs the Python Playwright dependency, and downloads its managed Chromium into the private runtime. It never reads a browser profile or handles LINE credentials. Use `--skip-browser-install` only for a client-only/CDP test that will not launch Chromium.\n3. Run `scripts/start_line_oa_chromium.sh`. It creates the selected profile directory when absent and, if `DISPLAY` is missing, starts a private local `Xvfb` display for the lifetime of its Chromium child. It does **not** expose VNC, noVNC, CDP, or a GUI to the network. A protected GUI-sharing service remains an operator-controlled prerequisite for user login.\n4. If `uv`, `Xvfb`, or a VNC/noVNC handoff dependency is missing, report the exact missing commands and print package-manager instructions for an operator with host package permission. On Debian-family hosts, install the base handoff packages first, then add Cloudflare's signed `cloudflared` apt repository before installing `cloudflared`; it is not assumed to exist in the default Debian repositories. The skill must not invoke `sudo`, `apt-get`, or another system package manager itself. Never weaken network bindings.\n5. After provisioning, health-check CDP and inspect the LINE page. A new profile will require the user to authenticate through the protected GUI; this is a security checkpoint, not an install failure.\n\nDo **not** assume a particular home directory, virtual environment, browser cache, or runtime folder exists. For browser-profile selection specifically, use `LINE_OA_SEND_CHAT_CHROMIUM_PROFILE` or its explicit fallback `/opt/data/chromium` as described above. The runtime requirements are: (1) a Python environment containing the `playwright` package and (2) an already-running, user-authenticated Chromium reachable through a local CDP endpoint. Connecting over CDP does not require Playwright to download or launch another browser.\n\nThe portable launcher still honors an explicit `LINE_OA_PYTHON`, then any current Python that imports Playwright, then `uv run --with playwright`. It connects only to an existing CDP browser; it does not log in or accept credentials.\n\n## CLI script\nUse `scripts/run_line_oa_chat.sh` rather than invoking `python scripts/send_line_oa_chat.py` directly. It connects only to an already-running local CDP endpoint; it does not perform login or accept credentials.\n\n```bash\n# Safe default: find and open the uniquely matched chat, but do not send.\nbash scripts/run_line_oa_chat.sh \\\n  --recipient \"<exact recipient>\" --message \"<message>\"\n\n# Send only after the user explicitly authorizes this exact recipient and message.\nbash scripts/run_line_oa_chat.sh \\\n  --recipient \"<exact recipient>\" --message \"<message>\" --send\n```\n\nThe script refuses ambiguous recipient results, requires `--send` for the external side effect, and exits non-zero when session/UI verification fails. After sending, it verifies that the composer cleared and the exact text appeared in the active transcript. Do not retry a failed verification until the protected browser is inspected, because LINE may already have accepted the message.\n\n## Procedure\n1. Connect with Playwright's sync API over CDP. Inspect all open pages and select the page whose URL begins with `https://chat.line.biz/`.\n2. Inspect the chat page before acting. Use the sidebar input with `aria-label=\"搜尋\"` / placeholder `搜尋` to search the requested recipient.\n3. Select the result from the chat list, not a same-named account/profile menu item. In this UI the searchable chat's text can be inside a `<mark>` and its clickable parent is an ancestor `<a>`.\n4. Confirm the selected conversation has loaded by checking that the message composer and prior conversation content are visible.\n5. Fill the actual composer inside LINE's custom element using the shadow-DOM-piercing locator `page.locator('textarea-ex').locator('textarea')`, then click `input[type=\"submit\"][value=\"傳送\"]`.\n6. Wait for UI update and verify the exact message appears in the active chat transcript. Report only after this verification succeeds.\n\n## Reference implementation\n```python\nfrom playwright.sync_api import sync_playwright\n\nrecipient = \"<exact recipient>\"\nmessage = \"<message>\"\nwith sync_playwright() as p:\n    browser = p.chromium.connect_over_cdp(\"http://127.0.0.1:9222\")\n    page = next(pg for ctx in browser.contexts for pg in ctx.pages\n                if pg.url.startswith(\"https://chat.line.biz/\"))\n    search = page.get_by_placeholder(\"搜尋\", exact=True)\n    search.fill(recipient)\n    page.wait_for_timeout(800)\n    chat = page.locator(\"mark\").filter(has_text=recipient).first.locator(\"xpath=ancestor::a\")\n    chat.click()\n    page.locator(\"textarea-ex\").locator(\"textarea\").fill(message)\n    page.locator('input[type=\"submit\"][value=\"傳送\"]').click()\n    page.wait_for_timeout(1200)\n    assert message in page.locator(\"body\").inner_text()\n```\n\n## UI reference\nSee [LINE OA UI selector notes](references/line-oa-ui-selectors.md) for the current DOM patterns and responsive-layout behavior observed in the web client.\n\n## Publishing updates\nWhen changing this skill in its GitHub repository, use the normal Git/PR lifecycle rather than committing to `main` directly.\n\n### Making the repository public\nUse [the public repository checklist](references/public-repository-checklist.md) for the full audit, settings, and verification sequence.\n\nBefore changing visibility, audit **all reachable Git history**, not only the current files: old commits and merged PRs remain visible after publication. Scan for credentials, authentication headers, tunnel/share URLs, browser-profile data, and host-specific runtime paths, reporting only commit IDs and pattern categories—not matched secret values. Remove or rewrite genuinely sensitive history before publication.\n\nApply public-facing settings before changing visibility: remove unused collaboration surfaces, enable automatic deletion of merged branches, and disable unused Actions until a reviewed workflow exists. After changing visibility, verify the repository and README through an unauthenticated request, then enable supported public-repository security controls (secret scanning, push protection, Dependabot). GitHub may automatically enable or reject some settings for public repositories, so read back the resulting state. Treat LICENSE selection as a separate legal decision; do not choose one without the user’s direction.\n\n1. Run `gh auth status` in the same OS user and runtime that will run Git/`gh` commands. A GitHub browser session or CLI login in another shell/user does not authenticate this runtime.\n2. If CLI auth is missing, start the browser/device flow with `gh auth login --hostname github.com --git-protocol https --web`. When the user asks to “just run it and give me the code,” run the flow directly and reply only with the generated one-time device code plus `https://github.com/login/device`; do not add extra explanation. Wait for their authorization, then verify with `gh auth status` before proceeding.\n3. Start from the current `main` branch and create a focused branch such as `docs/update-authentication`.\n4. Validate the edited `SKILL.md`, commit the change, and push the branch.\n5. Use GitHub CLI (`gh pr create`) to open a PR that summarizes the change and its verification.\n6. Test the exact remote PR revision before reporting it: obtain `headRefOid` with `gh pr view <number> --json headRefOid`, then check out that SHA in a clean worktree and run Python compilation, `--help`, and a no-send dry-run against the authenticated LINE UI. If a shallow clone has a restrictive fetch refspec and `gh pr checkout` cannot set tracking for the PR branch, create a local non-tracking branch at the verified `headRefOid` (for example, `git switch -c pr-<number> <sha>`). Do not test an unverified local copy instead.\n7. Treat documentation-only changes (including README updates) as repository changes too: branch, validate, push, open a PR, and leave it unmerged for review.\n8. Merge only after the user explicitly approves it. A concise `merge` instruction is sufficient authorization; use a squash merge and delete the feature branch, then verify the PR state and remote branch deletion with `gh`.\n\n## Pitfalls\n- `get_by_text(recipient, exact=True)` may target the signed-in account menu instead of the chat result when names collide.\n- On a narrow layout the matching chat label may have no bounding box because the sidebar hides text, while its containing chat-list `<a>` is still clickable. Locate the anchor from the matching `<mark>` instead of treating a hidden label as a failed search.\n- Playwright placeholder matching is substring-based by default. Use `page.get_by_placeholder(\"搜尋\", exact=True)` so the chat-list search does not also match LINE's unrelated `輸入搜尋內容` field.\n- `get_by_placeholder(...)` can match both the custom `textarea-ex` host and its internal textarea. Use `textarea-ex >> textarea` / chained locators so Playwright targets the true editable element.\n- A successful click alone is not sufficient; always verify the exact outgoing text in the active transcript after submission, preferably in the recent transcript tail rather than via a broad page-wide match.\n- If the recipient search yields more than one distinct chat, stop and ask the user which conversation to use.\n- Never request, handle, record, or transmit LINE passwords, OTPs, or other credentials.\n- A local `curl` success is insufficient for a Cloudflare Quick Tunnel: verify the bearer URL from outside the local listener. If the tunnel page is empty, check that Caddy uses a host-agnostic site address (for example `:6081`) with `bind 127.0.0.1`; `http://127.0.0.1:6081` can reject the public tunnel Host header. Keep multi-line Caddy `handle` blocks structurally formatted, because compact inline blocks may fail Caddy parsing.\n- Never assume VNC `5900` or websockify `6080` are free. Select unused loopback ports for each handoff and configure Caddy to reverse-proxy to the chosen websockify port. Fixed ports can silently attach the handoff to another display and yield a black canvas.\n- For an Xvfb display, invoke `x11vnc` with `-noxrecord -noxfixes -noxdamage`; otherwise noVNC can remain black even while Chromium has a mapped window. Confirm the actual remote canvas visually shows the LINE login window before instructing the user to authenticate.\n\nFile v0.1.2:README.md\n\n# line-oa-chat-send\n\nA reusable, safety-first CLI helper for sending messages through an already authenticated [LINE Official Account Chat](https://chat.line.biz/) browser session.\n\nThe command attaches to an existing local Chromium DevTools (CDP) endpoint. It does **not** start a second browser, log in to LINE, or collect credentials.\n\n## What it does\n\n- Searches for a chat recipient and refuses ambiguous matches.\n- Opens the selected conversation and confirms that the message composer is available.\n- Defaults to a no-send dry run.\n- Requires an explicit `--send` flag before it can send a message.\n- After a send, checks that the composer cleared and the exact messag\n\nArchive v0.1.1: 10 files, 17278 bytes\n\nFiles: LICENSE (1064b), README.md (4092b), scripts (0b), scripts/run_line_oa_chat.sh (1508b), scripts/send_line_oa_chat.py (5181b), scripts/setup_line_oa_runtime.sh (1644b), scripts/start_line_oa_chromium.sh (4526b), scripts/start_line_oa_vnc_handoff.sh (3524b), SKILL.md (15644b), _meta.json (136b)\n\nArchive v0.1.0: 8 files, 10581 bytes\n\nFiles: LICENSE (1064b), README.md (4092b), scripts (0b), scripts/run_line_oa_chat.sh (1508b), scripts/send_line_oa_chat.py (5181b), scripts/setup_line_oa_runtime.sh (1272b), SKILL.md (8246b), _meta.json (136b)","readmeExcerpt":"Skill: line-oa-chat-send Owner: mosluce Summary: Send explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control. Tags: latest:0.1.6 Version history: v0.1.6 | 2026-07-29T08:06:26.333Z | user v0.1.6 — first clean publish; the skill itself is unchanged since","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"# 1. Check the environment. Exit 0 = ready; 3 = usable but not logged in; 1 = blocked.\nbash scripts/doctor.sh\n\n# 2. Start the browser session if one is not running.\n#    Owns the display, Chromium, and a loopback-only screen-sharing stack.\n#    Nothing is externally reachable.\nbash scripts/start_line_oa_chromium.sh\n\n# 3. ONLY when the user explicitly asks to log in or re-authenticate.\n#    Prints one URL, already verified reachable, or fails non-zero.\nexport LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login\nexport LINE_OA_SEND_CHAT_HANDOFF_TTL_SECONDS=900   # 60-3600, default 900\nbash scripts/start_line_oa_vnc_handoff.sh\n\n# 4. Revoke as soon as the user says login is done. Session keeps running.\nbash scripts/stop_line_oa_vnc_handoff.sh\n\n# 5. Find and open the chat without sending. This is the safe default.\nbash scripts/run_line_oa_chat.sh --recipient \"<exact recipient>\" --message \"<message>\"\n\n# 6. Send, only after the user authorized this exact recipient and message.\nbash scripts/run_line_oa_chat.sh --recipient \"<exact recipient>\" --message \"<message>\" --send\n\n# 7. Stop the session when the work is finished. The profile is preserved.\nbash scripts/stop_line_oa_chromium.sh"},{"language":"bash","snippet":"bash scripts/setup_line_oa_runtime.sh --runtime-dir <private-dir> --skip-browser-install\nexport LINE_OA_PYTHON=<private-dir>/venv/bin/python"},{"language":"bash","snippet":"curl -fsS http://127.0.0.1:9222/json/list'"},{"language":"bash","snippet":"# Build all four variants (native architecture only)\ncontainers/build.sh\n\n# Build one\ncontainers/build.sh full\n\n# Run a command in a variant; default is an interactive shell\ncontainers/run.sh full bash\ncontainers/run.sh no-runtime bash scripts/run_line_oa_chat.sh --help\n\n# Start a browser session and check CDP from inside\ncontainers/run.sh full bash -lc '\n  bash scripts/start_line_oa_chromium.sh >/tmp/c.log 2>&1 &\n  sleep 20\n  curl -fsS http://127.0.0.1:9222/json/list'"},{"language":"bash","snippet":"# Default path: no external exposure\ncontainers/test/default.sh\n\n# Opt-in: creates a real, externally reachable Cloudflare Quick Tunnel\nLINE_OA_TEST_ALLOW_TUNNEL=1 containers/test/tunnel.sh 60"},{"language":"bash","snippet":"containers/reset.sh            # drop profile volumes\ncontainers/reset.sh --images   # also drop the built images"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: line-oa-chat-send\ndescription: Send explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control.\n---\n\n# LINE OA Chat: send a message\n\n## Use when\n\nA user provides or has already opened a LINE OA Chat URL and asks to send a\nspecific message to a named chat recipient.\n\n## Prerequisites\n\n- The persistent Chromium profile is already authenticated to LINE by the user.\n- A browser session is running with its loopback CDP endpoint reachable.\n- The user has explicitly authorized the outgoing message. Do not infer a\n  message other than an unambiguous test message.\n\n## Security boundary\n\n- Normal message work uses local CDP only. It does not expose a browser, CDP,\n  VNC, screenshots, cookies, or profile data to the network.\n- The optional noVNC handoff is **only** for a user to complete LINE login or\n  reauthentication. It grants whoever holds its URL interactive control of the\n  browser, so treat the URL as a high-risk bearer secret—not as a general\n  browsing or support channel.\n- Start a handoff only after the user explicitly requests this\n  login/reauthentication route. It has a default 15-minute TTL (configurable only\n  from 60 to 3600 seconds), must be shared only in a direct private channel, and\n  must be revoked immediately after login. Do not use it to send messages, browse\n  unrelated sites, or perform autonomous actions.\n- Never request, handle, record, or transmit LINE passwords, OTPs, or other\n  credentials. The user completes every authentication step themselves.\n- Never log, commit, reuse, or relay a handoff URL.\n\nRevoking a handoff removes external reachability only; the browser session keeps\nrunning. That narrows the cost of revoking promptly — it does not narrow what the\nhandoff grants while it is armed.\n\n## Quick start\n\n```bash\n# 1. Check the environment. Exit 0 = ready; 3 = usable but not logged in; 1 = blocked.\nbash scripts/doctor.sh\n\n# 2. Start the browser session if one is not running.\n#    Owns the display, Chromium, and a loopback-only screen-sharing stack.\n#    Nothing is externally reachable.\nbash scripts/start_line_oa_chromium.sh\n\n# 3. ONLY when the user explicitly asks to log in or re-authenticate.\n#    Prints one URL, already verified reachable, or fails non-zero.\nexport LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login\nexport LINE_OA_SEND_CHAT_HANDOFF_TTL_SECONDS=900   # 60-3600, default 900\nbash scripts/start_line_oa_vnc_handoff.sh\n\n# 4. Revoke as soon as the user says login is done. Session keeps running.\nbash scripts/stop_line_oa_vnc_handoff.sh\n\n# 5. Find and open the chat without sending. This is the safe default.\nbash scripts/run_line_oa_chat.sh --recipient \"<exact recipient>\" --message \"<message>\"\n\n# 6. Send, only after the user authorized this exact recipient and message.\nbash scripts/run_line_oa_chat.sh --recipient \"<exact recipient>\" --message \"<messa"},{"path":"containers/README.md","content":"# Container test environment\n\nA reproducible Linux environment for running and deliberately breaking this\nrepository's scripts. It exists because the scripts target Linux while\ndevelopment happens elsewhere, and because failure paths are far cheaper to\nexercise by picking a variant than by breaking a real host by hand.\n\n## What this is and is not authoritative for\n\n| Authoritative for | Not authoritative for |\n| --- | --- |\n| Script behavior | Which startup phase dominates |\n| Refusal and error paths | Absolute phase durations |\n| Dependency detection and remediation messages | Any reported speedup figure |\n| Handoff arm/revoke lifecycle | End-to-end message send |\n\nLatency results from this container are usable for rehearsing a measurement and\nfor same-host before-and-after comparison. They are **not** an answer to which\nphase dominates handoff startup. Both poles are distorted here, in the same\ndirection but by different and unpredictable amounts:\n\n- **Chromium** is slowed by the VM's CPU allocation and filesystem layer, and is\n  simultaneously *sped up* by using a throwaway profile instead of a real one.\n- **Tunnel registration** is slowed because Quick Tunnels prefer QUIC over UDP,\n  which a desktop VM's NAT can degrade into an HTTP/2 fallback, and because\n  egress goes through a workstation network rather than a datacenter link.\n\n## Distance from the target host\n\nThe target is **x86_64 Debian 13 (trixie)**, running as a Kubernetes pod. The\ncontainer here is **arm64 Debian 12 (bookworm)** on a Docker Desktop VM. They\ndiffer in instruction set, distribution release, and virtualization layer.\n\n| Differs | Effect |\n| --- | --- |\n| arm64 here vs x86_64 there | Different silicon; no basis for comparing absolute times |\n| Debian 12 vs Debian 13 | Different Chromium, Caddy, and cloudflared builds |\n| Docker Desktop VM vs a pod | Different CPU allocation and filesystem layer |\n| Throwaway profile instead of a real authenticated one | Deflates Chromium cold start |\n| Workstation egress, QUIC through the VM's NAT | Changes tunnel registration and DNS behaviour |\n| `seccomp=unconfined`, absent on the target | Changes sandbox setup cost |\n\nMeasured, once both sides had run the same script:\n\n| | container | target |\n| --- | --- | --- |\n| session start | 0.53s | 1.63s |\n| `tunnel_url` | 2.79s | 4.62s |\n\nThe container was **faster** on both — the opposite of what \"a VM inflates\nstartup\" would predict. That is the point: the direction of the distortion was\nnot predictable in advance, which is why this environment is not authoritative\nfor latency and why the measurements had to be repeated on the target.\n\nA concrete case: the container found that a 3s grace window failed outright,\nwhich looked like evidence of a propagation floor. The target sweep showed no\nfloor at all — misses are sporadic and independent of how long you wait. The\ncontainer result was one unlucky sample generalized into a rule. It was labelled\nnon-authoritative, and that label is why it was"},{"path":"README.md","content":"# line-oa-chat-send\n\nA safety-first CLI for sending messages through an already authenticated\n[LINE Official Account Chat](https://chat.line.biz/) browser session.\n\nThe send command attaches to a local Chromium DevTools (CDP) endpoint. It does\nnot start a browser, log in to LINE, or collect credentials.\n\n> **Optional login handoff — high-risk capability:** when a user explicitly asks\n> to complete LINE login or reauthentication through a remote GUI,\n> `scripts/start_line_oa_vnc_handoff.sh` arms a temporary Cloudflare noVNC URL\n> that grants its holder interactive control of the browser. It attaches to a\n> running session rather than starting one, requires the explicit\n> `LINE_OA_SEND_CHAT_HANDOFF_PURPOSE=line-login` scope, defaults to a 15-minute\n> TTL, keeps VNC and CDP loopback-only, and must be revoked as soon as login\n> ends. Revoking it leaves the browser session running.\n\n## What it does\n\n- Searches for a chat recipient and refuses ambiguous matches.\n- Opens the selected conversation and confirms the composer is available.\n- Defaults to a no-send dry run; requires an explicit `--send` to transmit.\n- After a send, checks that the composer cleared and the exact message appears in\n  the active transcript.\n\n## Requirements\n\n1. A user-authenticated LINE OA Chat session in Chromium.\n2. Chromium exposing a loopback CDP endpoint (default `http://127.0.0.1:9222`).\n3. Python with the `playwright` package. No browser download is needed — the tool\n   attaches to an existing Chromium.\n4. Explicit authorization for the recipient and the outgoing message.\n\nLinux is the supported platform. `containers/` provides a reproducible Linux\nenvironment for running the scripts from another OS.\n\n> **Authentication:** complete LINE login, password entry, QR confirmation, MFA,\n> OTP, and security prompts yourself in the interactive browser. This project\n> never accepts, stores, or transmits those secrets.\n\n## Check the environment first\n\n```bash\nbash scripts/doctor.sh\n```\n\n| Exit | Meaning |\n| --- | --- |\n| 0 | Ready; a send can run |\n| 3 | Environment usable, LINE authentication required — a checkpoint, not a failure |\n| 1 | Blocked; the missing pieces and their remediation are printed |\n\nIt reports what the launcher will actually do, and never suggests widening a\nnetwork binding. It invokes no package manager and no `sudo`.\n\nIf it reports no Python runtime:\n\n```bash\nbash scripts/setup_line_oa_runtime.sh --runtime-dir <private-dir> --skip-browser-install\nexport LINE_OA_PYTHON=<private-dir>/venv/bin/python\n```\n\n## Usage\n\n```bash\n# Start the browser session. Owns the display, Chromium, and a loopback-only\n# screen-sharing stack. Nothing is externally reachable.\nbash scripts/start_line_oa_chromium.sh\n\n# Safe default: find and open a uniquely matched chat, send nothing.\nbash scripts/run_line_oa_chat.sh --recipient \"Recipient name\" --message \"Message text\"\n\n# Send, only after the recipient and exact text have been authorized.\nbash scripts/run_line_oa_chat.sh --recipient \""},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn743ga1fg7x5xfqphk150apyd8apt23\",\n  \"slug\": \"line-oa-chat-send\",\n  \"version\": \"0.1.6\",\n  \"publishedAt\": 1785312386333\n}"},{"path":"references/handoff-operations.md","content":"# Handoff operations\n\nWhy the session and handoff scripts look the way they do. Everything here is\nalready enforced in code — this is the reasoning, for when the code needs\nchanging. Read it before editing `scripts/start_line_oa_chromium.sh`,\n`scripts/start_line_oa_vnc_handoff.sh`, or `scripts/lib/*.sh`.\n\n## Shape\n\n```\nBrowser session (long-lived, loopback only)\n  Xvfb ── Chromium ── CDP 127.0.0.1:9222\n    └──── x11vnc ──── websockify ──── Caddy (404 until armed)\n\nLogin handoff (ephemeral, the only thing that is ever externally reachable)\n    Caddy token route ──── cloudflared Quick Tunnel ──── public HTTPS URL\n```\n\nThe session owns the browser. The handoff attaches. Revoking the handoff removes\nthe tunnel and the route and leaves everything else running.\n\n## Ports are never assumed free\n\nVNC, websockify, Caddy, and Caddy's admin endpoint all get dynamically selected\nloopback ports, chosen in one call that holds each socket until all are chosen —\notherwise the same port can be handed out twice.\n\nFixed ports silently attach the handoff to something else. A hard-coded `5900`\ncan bind to another display and produce a black canvas; a hard-coded front-end\nport can collide and leave a working tunnel URL serving nothing.\n\n## Caddy's output is not discarded\n\nA bind conflict with output sent to `/dev/null` surfaces as \"the URL works but\nthe page is blank\" — expensive to diagnose, trivial to prevent. Output goes to\nthe run log.\n\n## The site address is host-agnostic\n\n```\n:PORT {\n  bind 127.0.0.1\n  ...\n}\n```\n\nNot `http://127.0.0.1:PORT`. The tunnel arrives carrying a public Host header,\nand a host-specific site address rejects it. `bind 127.0.0.1` keeps the listener\non loopback while leaving the matcher host-agnostic.\n\nKeep `handle` blocks multi-line. Compact inline blocks can fail Caddy parsing.\n\n## x11vnc needs `-noxrecord -noxfixes -noxdamage`\n\nWithout them noVNC can stay black even while Chromium has a mapped window on the\ndisplay.\n\nThe cost is real: `-noxdamage` disables the damage extension, so x11vnc polls\nthe whole screen instead of being told what changed. That means higher CPU and a\nless responsive remote canvas. It is a deliberate trade — a slow picture beats no\npicture — and it affects interaction latency, not startup time.\n\n## Verification waits before it probes\n\nThe single most counterintuitive part.\n\nA fresh `trycloudflare.com` hostname is **not resolvable when cloudflared prints\nit**. A lookup that misses is cached as a negative answer, and re-querying keeps\nrefreshing that negative entry — so an eager polling loop holds itself in\nfailure long past actual propagation.\n\nMeasured:\n\n| Approach | Outcome |\n| --- | --- |\n| Probe immediately, poll every 1s | Hostname never resolved within 120s |\n| Wait 45s quietly, then query once | Resolved on the first query |\n\nThis is not a container artifact. systemd-resolved, dnsmasq, and container\nembedded DNS all cache negative answers.\n\nSo the handoff waits a quiet grace window (`LINE_OA_SEND_CHAT_HANDOFF_V"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Send explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control. Skill: line-oa-chat-send Owner: mosluce Summary: Send explicitly authorized LINE Official Account Chat messages through a persistent Chromium session; user-operated LINE login or reauthentication may use a temporary remote noVNC handoff that grants interactive browser control. Tags: latest:0.1.6 Version history: v0.1.6 | 2026-07-29T08:06:26.333Z | user v0.1.6 — first clean publish; the skill itself is unchanged since","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1854,"uniquenessScore":48,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T17:04:05.712Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T17:04:05.712Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T20:57:39.825Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}