Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/canvaslink.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: CanvasLink alpha

on:
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
contracts:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci --ignore-scripts
- run: npx playwright install --with-deps chromium
- run: npm run test:canvaslink
- run: npm run typecheck

android-debug:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: android-actions/setup-android@v3
with:
packages: platform-tools
log-accepted-android-sdk-licenses: 'false'
- run: sdkmanager 'platforms;android-35' 'build-tools;35.0.0'
- run: node integrations/canvaslink/scripts/build-android.cjs
- name: Upload alpha APK (ephemeral CI development signature)
uses: actions/upload-artifact@v4
with:
name: CanvasLink-debug
path: integrations/canvaslink/dist/CanvasLink-debug.apk
retention-days: 30
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ npm run dev

The opt-in **Settings → Controls → Even G2** integration adapts CanvasTTY's terminal and AI-agent workflows to Even G2 glasses. Pair with six digits on the same local network, approve access on the computer, then read responses on the glasses HUD, dictate through local Nemotron speech recognition, and create, rename or close shared sessions. The companion includes a More agents picker and supports the desktop's provider list. Its source lives in `integrations/even-g2` and builds with the desktop. See [setup, distribution status and acceptance limits](docs/even-g2.md); a compatible public installer and Even Hub approval are still pending.

## CanvasLink for Android (alpha)

**Settings → CanvasLink** streams the current CanvasTTY window to an Android phone or tablet on the same LAN, with touch, direct terminal keyboard input and a shortcut bar. Access starts off; pairing uses a short-lived six-digit code. The desktop host is integrated, with no separate application. See [setup, APK builds and platform limits](docs/canvaslink.md). The prototype has macOS/Android user acceptance; this source port remains experimental, with Windows/Linux device acceptance pending.

## Web companion

The opt-in [Web companion](docs/mobile-companion.md) shares selected sessions with Android, iOS, and desktop browsers through **Tailscale Serve HTTPS**, not Funnel. CanvasTTY binds its companion endpoint to `127.0.0.1:3481` in this mode; pair with a short-lived code and approve each browser on the desktop. Open **Settings → Controls → Web companion** for setup, grants, and revocation. End-to-end Tailscale Serve deployment on Windows, Linux, and macOS remains unverified.
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ CanvasTTY is a spatial Electron desktop for real local terminals and AI-agent CL
| [Built-in browser and audit log](browser.md) | Canvas controls, settings, agent access, website/file boundaries, activity, and persistent redacted audit files |
| [File browser and reader](files.md) | Opening a Files card, session and folder roots, supported content, read limits, quick open, persistence, and read-only security boundaries |
| [Web companion](mobile-companion.md) | Tailscale Serve HTTPS setup, browser pairing, session grants, revocation, and security limits |
| [CanvasLink for Android (alpha)](canvaslink.md) | Full-window LAN streaming, touch/keyboard input, APK build, security boundaries and platform limits |
| [Installing, releases, and local data](installing-and-security.md) | Installer formats, unsigned-preview caveats, credential boundaries, and release checks |
| [Widget authoring](widget-authoring.md) | Source-level extension paths, visual grammar, process boundaries, and an AI-agent brief |
| [Pixel terminal themes (Russian)](pixel-skin-packs.ru.md) | Ten-PNG ZIP layout, terminal apertures, app import, and local agent CLI |
Expand Down
101 changes: 101 additions & 0 deletions docs/canvaslink-security-audit-2026-10-26.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# CanvasLink: bounded security and failure-path audit

Reviewed on 2026-10-26, starting at `f643b31c476e490904437c95c13f0db33ad7c310`.
This report covers the embedded host, Android transport/direct-input assets,
pairing/channel implementation, IPC ownership and installation contract. It is
not a penetration-test certification or acceptance of an installed release.

## Confirmed findings and fixes

Severity below is a local assessment of the demonstrated behavior, not a CVSS score.

| Finding | Preconditions and impact | Fix and regression evidence |
| --- | --- | --- |
| Medium: session expiry did not revoke an established video peer | An already paired client could stop polling and keep receiving video after the advertised 12-hour session lifetime. Request/input checks rejected expired credentials, but no passive expiry path stopped media. This did not bypass the PIN. | `Pairing.expire()` removes credentials before invoking revocation; requests and outgoing signals invoke it, and the host invokes it every 200 ms independently of client traffic and UI health polling. Tests cover the exact expiry boundary, one-time revocation, silent clients and rejected expired requests/queues. Timer delivery still depends on the Electron main event loop. |
| Medium: revocation waited for an asynchronous UI refresh | A stalled or rejected settings-state read delayed or prevented closing a live peer after a valid revoke event. | The capture worker closes tracks, peer and input ownership synchronously before refreshing its UI. A deferred/rejected refresh test proves media and control stop first. |
| Low: delayed signalling could undo client suspension/disconnect | In-flight offers could create a new peer while hidden or locally disconnected. A delayed native pairing callback could also re-arm the client after explicit disconnect. These were stale messages from an existing authenticated connection, not unauthenticated remote pairing. | Ignore offers/disconnect signals unless paired and visible; ignore pairing completions after explicit disconnect until a new connect request. Tests cover unpaired, hidden and disconnected clients, delayed pairing completion and negotiation completion after disconnect. |
| Reliability: stale SDP failure overwrote current connection state | Failure of a superseded offer could mark a healthy new connection as failed. | Peer generations and checks after asynchronous SDP steps isolate stale completion/errors. |
| Reliability: buffered pointer motion survived session close | A queued animation-frame callback could submit old pointer movement to the next connection. | Close discards buffered movement and cancels its frame callback. No clicks, text or keys are replayed. |
| Input safety: one-shot Ctrl leaked through other commits | A Unicode/multi-character IME commit or native key could leave Ctrl armed, turning the next letter into an unexpected shortcut such as Ctrl+C. | Consume Ctrl on each committed text/key event. Browser fixture covers Cyrillic, emoji, multi-character text and native arrow input. |

Before fixes, the new regressions failed in seven cases in the first targeted
run and two further cases in the revocation-focused run. These were application
failures. A separate HTTP fixture initially lacked Node's `Buffer` global; that
test-harness error was corrected and is not an application finding.

## Boundaries exercised

- Real SRP mutual authentication and AES-GCM request/response code, using
synthetic keys and messages. Wrong PIN, used/expired code, attempt limits,
wrong address, tampering, reflection and replay are rejected.
- The actual HTTP request handler, driven by in-memory Node request streams:
public source addresses, browser origins (including `null`), unsupported
routes/methods, non-JSON content types, invalid JSON and oversized bodies are
rejected. An authenticated signal reaches the renderer once; a forged,
replayed, unsupported or revoked signal does not.
- IPC owner/main-frame checks and serialized settings operations, including a
window closing while a command waits. Capture is scoped to the owned window;
no global input injector or shell execution was added.
- Android navigation/resource allowlists, disabled file/content access, denied
WebView media permissions and encrypted app-private session storage were
inspected. Those native boundaries were not penetration-tested on hardware.
- Installation remains pinned to the published APK hash/certificate and stops
on signing conflicts. No private signing files or pairing credentials were
copied into the source tree or report.

## Verification of this patch

- **115/115 CanvasLink tests passed**, no skips; 12 tests added to the previous 103.
- **3/3 desktop installation-prompt tests passed**.
- TypeScript checks, production desktop bundles and the source/built-output
secret audit passed.
- The Android development APK compiled with SDK 35 / JDK 17; apksigner verified
its v2/v3 signatures. This is a local development artifact, not a release APK.
- Tests use simulated WebRTC/Electron objects and browser fixtures; HTTP tests
do not open a network listener. They prove the demonstrated control flow,
not real GPU capture or physical input on Windows/Linux/Android.

The installed desktop/tablet and public `0.3.0-alpha.1` APK were not replaced.
The client-side fixes require a subsequent APK build/release; desktop fixes
require a CanvasTTY build containing this patch. No new release is claimed.

## Open dependency issues

Fresh `npm audit --package-lock-only --json` reports **6 vulnerable packages:
5 high, 1 moderate**, matching an audit of the unchanged upstream baseline
`0fa6895d3675b40ec4dc75886826dd2db234c234` with its own manifest/lockfile.
The package names are Electron, Vite, undici, zod, source-map-js and fast-uri.
An installed-tree audit reports 18 affected packages including dependent parent
packages; this is a different audit tree/count, not 12 newly introduced root
advisories. CanvasLink's bonjour/SRP dependencies are not listed by these audits.

`npm audit --omit=dev` reports zero, but that **does not establish runtime safety**:
Electron is declared as a development dependency and is shipped in the desktop.
Its pinned version here is 43.7.7. The maintainer's
[Electron advisory](https://github.com/electron/electron/security/advisories/GHSA-jr5w-w6x2-84p9)
describes compromised intermediate TLS certificates; 43.8.1 contains the fix
for this release line. Exploitation requires intercepted TLS traffic and a
relevant certificate. This audit did not demonstrate exploitation in CanvasTTY.
Vite's
[file-serving advisory](https://github.com/vitejs/vite/security/advisories/GHSA-rq7h-c2jc-7f22)
requires an exposed development server and additional path/import conditions;
it is not evidence that CanvasLink's separate JSON listener serves files.

Dependency updates and their desktop/native packaging validation remain open;
this patch does not silently change the upstream Electron/build toolchain.

## Remaining acceptance and review gaps

- Physical source-port acceptance on macOS, Windows, Linux and Android remains
pending. Prior prototype acceptance is not acceptance of this patch or APK.
- Native Android logout clears persisted credentials only after its queued
network revoke attempt. Abrupt process death during an offline logout and
pairing/restore cancellation need a native lifecycle regression and follow-up;
the JavaScript disconnect guard does not prove durable native logout.
- No independent cryptographic review of the SRP implementation or WebRTC stack
was performed. Passing protocol tests is not such a review.
- A paired device controls the entire owned CanvasTTY window, including its
terminals and settings. Pairing is a broad trust grant, not terminal sandboxing.
LAN/source-address checks do not protect against a compromised paired device.
- No claim of denial-of-service resistance under adversarial LAN load,
dependency remediation, zero vulnerabilities or release readiness is made.
99 changes: 99 additions & 0 deletions docs/canvaslink-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# CanvasLink alpha: verification scope

Follow-up: [2026-10-26 security/failure-path audit](canvaslink-security-audit-2026-10-26.md)
records six fixed findings, 115 passing integration tests, dependency issues and
the remaining native/runtime acceptance gaps. The older counts below describe
the earlier source-port and distribution snapshots.

The source port is based on upstream commit
`0fa6895d3675b40ec4dc75886826dd2db234c234` (package version 1.8.1).
It does not replace the user's installed CanvasTTY 1.8.2 or its active preview.

## Prior prototype acceptance

The user manually exercised the macOS/Android prototype and confirmed it worked
before publication preparation. Its installed APK SHA-256 was
`48b967d7102c8e733666a630562b420e5bee5ac3bc734ad065d7a89ffd038783`.
Prior bounded device checks covered real terminal input/ACKs, Settings touch,
native selects, resume, Wi-Fi and high-density capture on Android 12.
This is prototype evidence, not device acceptance of this source-port package.

## Source-port checks

- General upstream suite in an isolated source snapshot with a clean environment:
2,221 passed, 27 skipped, zero failures. No Go toolchain was present; native-helper
parity/release coverage is not claimed.
- 103 CanvasLink tests passed: pairing/authentication/replay rejection, input
ordering and uncertainty, reconnect races, direct-input/gesture fixtures,
select/touch behavior, graphics bounds, icons and IPC/lifecycle ownership.
- TypeScript node and renderer checks passed.
- Production desktop bundles built with the integrated settings section,
preload API and copied host assets.
- Android 0.3.0-alpha.1 debug APK built; apksigner verified v2/v3 signatures.
Instrumentation entry points are excluded from the normal APK.
- Even G2: 47 tests passed; its access and pairing implementation is unchanged.
- Source and built-output secret audit passed.
- npm audit found six advisories (five high, one moderate). An audit of the
unchanged upstream lockfile reports the same six dependency names; no
dependency-security remediation is claimed by this feature port.

The general upstream suite initially exposed a path-handling bug in existing
hook tests when the checkout path contains spaces (URL `.pathname` retains
`%20`). The affected hook tests passed through a temporary path without spaces
using Node's `--preserve-symlinks` and `--preserve-symlinks-main`. The final full
run used a real source copy without spaces and removed inherited agent-capability
environment variables. This port does not change unrelated hook handling.

The Android HTML/CSS/JS/font assets are byte-identical to the accepted prototype.
The new normal APK omits instrumentation and carries the alpha version:
SHA-256 `509e44e12d0be251db6ed323e03035a0e2d16897c519a439679e8533ff5934fb`.
It has not replaced the installed APK.

A macOS directory package reached ASAR creation; inspection found the host assets,
bonjour-service and secure-remote-password and no acceptance instrumentation.
Final ad-hoc signing failed with a local “internal error in Code Signing
subsystem”. No signing certificate/keychain access was requested. This is not a
successfully signed distributable. The standalone hidden Electron smoke also
failed to start to completion in this environment (timeout with no script output),
including an attempt from a disposable unprotected directory. Its runtime result
is **unverified**; the installed prototype remains the only user-accepted runtime.

## Required release acceptance

Real packaged-build acceptance for this source port remains pending. CI and
synthetic fixtures do not prove GPU capture, Android IME, TalkBack, rotation,
host firewalls or real device input on Windows/Linux. Official desktop release
signing, store distribution and a 30 FPS guarantee are outside this alpha.

The `CanvasLink alpha` workflow runs the contracts and builds a downloadable
development APK. Its CI signature is ephemeral; it is not an update channel.
Run `node scripts/smoke-canvaslink.mjs` after the desktop build to exercise real
Electron preload/IPC and host enable/revoke/disable against a disposable hidden
fixture. It does not send input to user terminals or provide video acceptance.

## Community APK distribution and onboarding (2026-10-26)

- [CanvasLink](https://github.com/teo-nex/CanvasLink) is a separate public
distribution repository; application source remains in this PR.
- Version `0.3.0-alpha.1` is the APK from successful CI run `40235936427`, source
commit `eba7baf1f3db716b6600a605f1fda07c39e9119f`, re-signed locally with a
persistent distribution key. All ZIP entries outside `META-INF` match the CI
input byte for byte. Normal APKs contain no acceptance instrumentation.
- Distribution APK SHA-256:
`0534857896b2e22d7e832a8aaa6146b3fc6acb19f3df857cc9d3e8c1caf5a760`.
Public signing certificate SHA-256:
`0235b1aee591e4f0e24e59484927ead112889521a1b92e40959076d03635b3cb`.
The private key and password remain outside both repositories.
- Settings can copy a bilingual, pinned installation prompt and open the guide.
Copying does not enable the host, mutate pairing, install an APK or expose the
pairing code/device name. Three handler tests cover both locales and clipboard
failure; all passed. They use the repository's React test stand-in, not a live
desktop clipboard or physical Android device.
- The 103 integration tests, TypeScript checks, production build and source/built
output secret audit passed again after the settings change.
- The agent prompt selects one ADB device, verifies the APK hash, preserves app
data and stops on signature/downgrade conflicts. USB debugging and the initial
Android RSA confirmation belong to the device owner.
- Earlier debug signatures may conflict with the new distribution signature.
No APK was installed on the accepted tablet and no installed desktop was replaced.
Publication is distinct from physical acceptance of this exact APK/desktop port.
Loading
Loading