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
16 changes: 10 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@ jobs:
- run: pnpm lint
- run: pnpm format:check
- run: pnpm typecheck
- run: pnpm test:coverage
# The desktop app, built and launched under a virtual display. Ubuntu 24 restricts the user
# namespaces Chromium's sandbox needs: allowed back, so the app runs sandboxed as shipped.
# Ubuntu 24 restricts the user namespaces Chromium's sandbox needs: allowed back (Electron runs
# sandboxed as shipped). The tests run under a virtual display: the Electron target's tests
# launch a fixture desktop app.
- run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
- run: xvfb-run -a pnpm test:coverage
env:
# Slow shutdown phases of a desktop app's launch, said (the Electron target's evidence).
KIFRAME_ELECTRON_TIMING: "1"
# The desktop app, built and launched under a virtual display.
- name: Desktop end-to-end
run: |
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
xvfb-run -a pnpm --filter @kiframe/desktop e2e
run: xvfb-run -a pnpm --filter @kiframe/desktop e2e
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
Expand Down
5 changes: 4 additions & 1 deletion docs/OBJECT-MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,10 @@ One scene, three phases. The agent's own instructions say the same (`packages/st
- **Named by bundle id, never a path or a program** (a project may come from someone else): the desktop app finds the bundle on each machine and keeps the user's approval in its own settings (bundle id, real path, signing team); only `.app` bundles holding `Electron Framework.framework`; App Store (sandboxed) apps refused. `args` are positional only (no switches). `origins`: the https sites a wrapper app (Slack, Notion) shows as its own.
- **No address**: a goto or a URL condition on it is refused; a scene starting in it opens on its first window; it takes no secrets in v0 (the user signs in by hand: the handover).
- **Each run launches it sandboxed** (its own HOME, `CFFIXED_USER_HOME`, XDG and temp folders, `--user-data-dir`; an allowlisted environment, never Kiframe's), attached with `--remote-debugging-port=0` + `connectOverCDP`; the viewport is emulated (a spike measured frames and clicks with the window covered, off-screen, minimized or hidden: a run never needs the user's screen; the window itself can't be moved over CDP). Isolation is verified when the app is added: after a trial launch its state must be inside the sandbox, else refused. Its process group killed after each run, leftovers swept.
- **Stated limits:** native dialogs and menus are unreachable; links open in the user's real browser; a sign-in through the browser with an app-scheme callback isn't supported in v0; the keychain is the user's (an app reading its own item signs in as them); a window built from several views films one view.
- **Confinement: a guard, sealed at launch (PR 2, redesigned after three review rounds, design reviewed).** Once the app settles (its windows unchanged 1.5 s), its main window is the last opened still open; its own scheme or dev server is learned from that window alone, then sealed (nothing a run does adds to it). A main window that isn't the app's own (a wrapper's unlisted site) refuses the launch ("list it in its origins"). From then a **guard** checks every navigation of every frame of every window as it happens: a page that isn't the app's own is stopped at once (a popup closed, any other frame blanked: never left for a read or a frame) and recorded; the step fails `off-app`. The app's own: a local `file:` in its bundle or sandbox (decoded, normalized), its sealed scheme or dev server, a listed https site (`sameApp`, never plain http), a blob of those. Stated (as a web run): the guard acts as a page commits, so a page that isn't the app's own has loaded (its scripts ran, with the app's own rights) before it's stopped: following a link the app offers is what its user does too (blocking before the load needs request interception, which never sees `file:`); and the runner's own navigation events name it as they do for web runs (origin and path, known secret values scrubbed). What the main window embeds as it opens (a frame of another site) is the app as shipped: its origin is the app's own. A main window stopped goes back to the app's page (no goto there to bring it back); a failed load is said as one. **One app per run:** a desktop app's run never shows a site (no goto, URL condition or web preset; a web interrupt rule's goto skipped, said), is confined while grounding too, and keeps no sign-in between runs yet.
- **Windows (PR 2, known limits, to settle on real apps in PR 3 and PR 5: decided by the user 2026-10-08):** the main window is the last opened still open once the app's windows were unchanged for 1.5 s (a splash that stays up longer is taken for the main window); windows the app's main process opens are followed as popups are (a hidden helper window opened during a step may be followed: CDP can't tell it's hidden); only the launch window is sent back when it goes off-app (another window is closed); a stop that lands after its step ended counts against the next step. A window that opens blank gets a moment to load (as a web page does); a frame of a window the run isn't driving is said, never the step's failure.
- **Measured on a fixture app (PR 2, 2026-10-08):** HOME, XDG and `--user-data-dir` hold; macOS's own places don't move: Electron's `appData` (`~/Library/Application Support`) and the app's preferences (`NSUserDefaults`, written by `cfprefsd` into the user's real `~/Library/Preferences/<bundle id>.plist`) stay the user's whatever `HOME` or `CFFIXED_USER_HOME` say. The trial launch therefore checks both: the app's state inside the sandbox, and its preferences file unchanged; else the user is told.
- **Stated limits:** native dialogs and menus are unreachable; links open in the user's real browser; a sign-in through the browser with an app-scheme callback isn't supported in v0; the keychain and an app's macOS preferences are the user's (an app reading its own keychain item signs in as them); a window built from several views films one view.
- **Built in steps** (epic `epic/electron-targets`): the format (2026-10-08), the launcher and target, the desktop approval and trial, the studio and batch on it, a real pass on a sandboxed minmux.

**Existing projects convert** on open: `target` becomes `apps.app` (the rest unchanged).
Expand Down
11 changes: 10 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import { defineConfig } from "eslint/config"
import tseslint from "typescript-eslint"

export default defineConfig(
{ ignores: ["**/dist/**", "**/coverage/**", ".kiframe-local/**", "apps/desktop/out/**"] },
{
ignores: [
"**/dist/**",
"**/coverage/**",
".kiframe-local/**",
"apps/desktop/out/**",
// A fixture desktop app (plain CommonJS Electron loads as is: never part of a TS project).
"packages/runtime/test/fixtures/electron-app/**",
],
},
js.configs.recommended,
tseslint.configs.recommendedTypeChecked,
{
Expand Down
Loading
Loading