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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history so the lockfile policy check can read commit messages.
fetch-depth: 0
submodules: false

- name: Setup Node environment
Expand All @@ -114,6 +116,10 @@ jobs:
- name: Check types, lint, and format
run: pnpm check

- name: Enforce lockfile change policy
if: github.event_name == 'pull_request'
run: node scripts/check-lockfile-policy.mjs --base "origin/${{ github.base_ref }}" --head "${{ github.event.pull_request.head.sha }}"

build-artifacts:
needs: [docs-scope, changed-scope, check]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
Expand Down
43 changes: 43 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"categories": {
"correctness": "warn"
},
"rules": {
"no-explicit-any": "warn",
"typescript/no-floating-promises": "warn",
"typescript/no-misused-promises": "warn",
"typescript/await-thenable": "warn",
"typescript/no-base-to-string": "warn",
"typescript/restrict-template-expressions": "warn",
"typescript/no-misused-spread": "warn",
"typescript/no-this-alias": "warn",
"no-unused-vars": "warn",
"no-unassigned-vars": "warn",
"no-dupe-keys": "warn",
"no-unused-expressions": "warn",
"no-misleading-character-class": "warn",
"no-control-regex": "warn",
"no-new-array": "off"
},
"overrides": [
{
"files": [
"**/*.test.ts",
"**/*.e2e.test.ts",
"**/*.live.test.ts",
"**/*.test-helpers.ts",
"**/test-helpers/**",
"**/test-utils/**"
],
"rules": {
"typescript/no-floating-promises": "off",
"typescript/no-misused-promises": "off",
"typescript/unbound-method": "off",
"no-explicit-any": "off",
"typescript/no-base-to-string": "off",
"typescript/restrict-template-expressions": "off"
}
}
]
}
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@
- Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability.
- Naming: use **Mayros** for product/app/docs headings; use `mayros` for CLI command, package/binary, paths, and config keys.

## Prompt Cache Discipline

Per-conversation prompt caching is sacred. A long-lived session reuses a cached prefix on every turn; anything that mutates past context, swaps toolsets, or rebuilds the system prompt with volatile content invalidates that cache and multiplies token cost. The project enforces caching via the `cacheRetention` stream option (handled by the provider layer), and the cost of an invalidation is real and silent.

- The system prompt is rebuilt on every run. Its content must be **stable across runs that have no reason to change it**. Do not add volatile data (timestamps that tick every second, random IDs, unordered maps) to the system prompt or to context files that feed it. The `Runtime:` line is the sanctioned place for run-scoped info and is designed to change only when the operator changes model, channel, or thinking level.
- Slash commands that mutate prompt-relevant state (`/model`, `/think`, `/reasoning`, `/elevated`, `/fast`, `/new`) are expected to invalidate the cache on the **next** turn by design. That is the only acceptable invalidation. A change that invalidates the cache mid-turn, or on every turn regardless of whether the operator touched anything, is a bug.
- When adding context to the prompt (new sections, hook output, injected memory), prefer **append-only** structure. Reordering existing sections or inserting in the middle invalidates everything after the insertion point.
- Hooks (`before_prompt_build`, `before_agent_start`) that override `systemPrompt` or `prependContext` must be deterministic for a given input. A hook that returns different text on consecutive calls with the same inputs breaks the cache and is a high-priority bug.
- The `diagnostics.cacheTrace` subsystem records a SHA-256 of the system prompt and messages at each pipeline stage. When investigating cache regressions, compare `systemDigest` across consecutive turns of a session where nothing should have changed — a drift there is the signature of a cache-breaking change.

## Release Channels (Naming)

- stable: tagged releases only (e.g. `vYYYY.M.D`), npm dist-tag `latest`.
Expand Down
92 changes: 92 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,48 @@ Mayros security guidance assumes:
- Anyone who can modify `~/.mayros` state/config (including `mayros.json`) is effectively a trusted operator.
- A single Gateway shared by mutually untrusted people is **not a recommended setup**. Use separate gateways (or at minimum separate OS users/hosts) per trust boundary.

## Trust Model

The single load-bearing security boundary in Mayros is **operating-system level
isolation**. Everything else is a heuristic or a convenience, not a containment
boundary.

This distinction matters because it is easy to over-trust in-process defenses.
The rule below governs how each layer should be treated:

### What is and is not a boundary

- **Real boundaries** — mechanisms the OS or kernel enforces against a
malicious process running inside them:
- Separate OS users, containers, VMs, namespaces, seccomp, Landlock.
- A sandboxed Docker container with `--cap-drop=ALL --read-only`.
- A separate host entirely (the strongest option).
- **In-process defenses** — useful, but **not containment**. A sufficiently
resourced attacker who has code execution inside the process can bypass them:
- The approval gate and interactive permission prompts.
- Output redaction and secret masking (`src/security/output-masking.ts`).
- The static skill scanner and bash command blocklist.
- Rate limiters, query/write caps, and the enrichment timeout.
- The WASM skill sandbox (it narrows the attack surface of untrusted skill
code, but the host process can still be reached via bugs in the runtime).

Treat the in-process defenses as **defense in depth and attack-surface
reduction**, never as the thing that stops a determined adversary. The thing
that stops a determined adversary is running the untrusted workload under a
real OS boundary.

### Practical implications

- For fully untrusted skills, plugins, or tool output, run the Gateway (or the
session) inside a container or separate OS user. Do not rely on the approval
gate alone.
- The default loopback-only bind is a real boundary against remote network
attackers; keep it unless you have a replacement boundary (Tailscale, SSH
tunnel, firewall).
- When reviewing a security finding, classify it by which boundary it crosses.
A bug that lets a plugin read another plugin's memory is contained by the OS
user; a bug that lets a remotely-reached endpoint execute code is not.

## Plugin Trust Boundary

Plugins/extensions are loaded **in-process** with the Gateway and are treated as trusted code.
Expand Down Expand Up @@ -123,6 +165,56 @@ docker run --read-only --cap-drop=ALL \
mayros/mayros:latest
```

## Dependency Supply Chain

Mayros treats its dependency tree as part of its trust boundary. A malicious or
broken transitive package reaches users the moment it enters the lockfile, so the
project enforces the following controls in `package.json`:

### Release age floor

`pnpm.minimumReleaseAge: 2880` (48 hours) gates every resolution. A package
published less than 48 hours ago is not eligible for install, which buffers
against compromised or yanked releases that get quarantined shortly after
publish.

### Forced security overrides

The `pnpm.overrides` map forces specific versions of transitive dependencies
known to carry security regressions. Each override exists because a transitive
in the tree resolved to a vulnerable version and the override is the most
durable way to pin the patched floor without waiting for upstreams to bump.

When adding or changing an override, record why in the commit message: the CVE
or GHSA it addresses (or the regression it prevents). Overrides without a
recorded rationale are rejected in review.

### Patched dependencies

Any dependency declared in `pnpm.patchedDependencies` must use an exact version
(no `^` or `~`). This is a hard rule: a range on a patched package lets a future
resolve drift past the patch, silently reverting the fix.

### Lockfile discipline

The npm/pnpm lockfile pins the full transitive tree by content hash. Treat the
lockfile as a reviewed artifact, not an auto-generated one:

- A change to `pnpm-lock.yaml` without a corresponding dependency change in
`package.json` is suspicious and must be explained in the commit.
- Dependabot/Renovate PRs that bump a transitive without a recorded CVE/fix are
held until the change is understood.
- `pnpm install --frozen-lockfile` is the canonical install command in CI;
`pnpm install` (which can mutate the lockfile) is reserved for local
dependency changes that are committed alongside a `deps:` commit.

### Native build scripts

`pnpm.onlyBuiltDependencies` enumerates the packages allowed to run native
build scripts (node-gyp, prebuilt binaries). Packages not in this list cannot
execute install scripts, which limits the blast radius of a compromised native
dependency.

## Security Scanning

This project uses `detect-secrets` for automated secret detection in CI/CD.
Expand Down
109 changes: 109 additions & 0 deletions docs/platforms/tui-redesign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
title: TUI Redesign Roadmap
description: Audit of the current terminal UI and prioritized plan for reaching a polished, best-in-class experience.
---

# TUI Redesign Roadmap

This document captures the current state of the Mayros terminal UI (`mayros code` / `mayros tui`), the gaps identified during an internal audit, and a prioritized roadmap for the next redesign pass. It is a planning artifact; implementation is tracked separately.

## Current state at a glance

The TUI is a gateway client: it renders in the terminal and talks to a running `mayros gateway` daemon over WebSocket. The rendering, input, and component layer comes from the shared terminal UI dependency; Mayros adds the slash-command surface, themes, vim mode, mouse scrolling, accessibility fallback, and the gateway chat client.

| Area | Maturity |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Rendering and streaming | Mature. Event-driven render loop, full assistant-message re-render on each delta. |
| Slash commands | Mature in count (40+ built-in), uneven in depth. Core session commands are first-class; ecosystem commands are prompt-injection shims. |
| Themes | Mature. Ten palettes with 22 color tokens each, live hot-swap. |
| Vim mode | Basic. Custom implementation; operators partially stubbed, no visual or command-line mode. |
| Accessibility | Split. A linear text-mode TUI exists but is a separate, feature-poor implementation. |
| Platform coverage | Gaps. Clipboard image paste and `/copy` work on macOS and Linux only; Windows and Wayland are unsupported. |
| Architecture | Workable but strained. Two monolithic files (`tui.ts`, `tui-command-handlers.ts`), a mutable god-object state, and two parallel palette systems. |

## Architectural constraints

These shape what a redesign can and cannot do without larger upstream work.

- **The terminal UI dependency is the rendering floor.** Layout capabilities (vertical-stack containers, select-list overlays) bound what Mayros can render. The tightest couplings are a `CustomEditor` that subclasses the shared editor and a `ChatLog` that subclasses the shared container. Refactoring either to composition requires an adapter layer or upstream changes.
- **The gateway is mandatory.** The TUI cannot run without a reachable gateway daemon. There is no in-process fallback, which affects offline use and dev ergonomics.
- **Theme hot-swap relies on singleton mutation.** Active theme objects are mutated in place so existing references stay live. Any snapshotting or caching of theme values would break live switching.
- **Mutable state object.** The TUI threads a single state object through every handler with manual render requests. There is no reactive store, although the signal libraries used elsewhere in the repo are available as a foundation.
- **Two parallel TUI implementations.** The graphical TUI and the accessibility TUI share almost no code. Every new feature currently needs to be implemented twice or skipped in one of them.
- **Two palette systems.** Terminal output (onboarding, prompts, CLI tables) and the TUI use separate palette modules with different accent colors. Visual consistency across the CLI experience requires unifying them.

## Gaps by category

### Rendering

- No inline image rendering despite the shared terminal dependency shipping the capability. Image content blocks currently render as text placeholders.
- The diff renderer hardcodes colors instead of consuming the active theme, so diffs look wrong under light or high-contrast themes.
- Only vertical-stack layout; no split panes or resizable regions beyond the select-list overlay.
- Full assistant-text re-render on every streaming delta; long messages may degrade.

### Input

- Vim operator-plus-motion combinations are a simplified stub (non-linewise motions collapse to single-character operations). No visual mode, no command-line mode, no search, no repeat, no registers beyond a single yank buffer.
- The vim bridge does not appear to be wired from the TUI side; motions may silently no-op if the bridge is never connected. This needs verification before any vim work.
- Ctrl+C does not abort the active run; only Escape does. This is surprising and should align with user expectations.
- A cross-session input history store exists in code but is not connected to the main loop.

### Commands

- Ecosystem commands (`/team`, `/tasks`, `/workflow`, `/rules`, `/mailbox`, `/sync`, `/trace`, `/kg`, `/plan`) inject a prompt that asks the model to call a tool, rather than being first-class features. Results depend on the model.
- Several commands lack argument autocomplete.
- The theme preset list is duplicated in two files and can drift.
- The README command list is stale relative to the implementation.

### Themes

- The terminal palette and the TUI palette are separate modules with different accent colors.
- No user-defined custom themes.
- No colorblind-optimized palettes beyond the existing high-contrast option.
- No explicit `NO_COLOR` or `FORCE_COLOR` handling in the TUI.

### Accessibility

- The accessibility TUI is a stripped-down parallel implementation: no slash commands beyond exit, no session switching, no abort, no model selection. It excludes screen-reader users from most functionality.
- No screen-reader announcements in the graphical TUI.
- No monochrome or no-color mode in the graphical TUI.

### Robustness and platform

- Clipboard image paste and `/copy` support macOS and Linux (xclip) only. Windows and Wayland are unsupported.
- Several silent `catch` blocks swallow errors without logging.
- Disconnect handling shows hints but does not auto-reconnect with backoff.
- Window resize handling is delegated entirely to the shared terminal dependency and is not verified end-to-end in Mayros.

## Roadmap

Priorities are assigned as P0 (correctness or blocker), P1 (high-value polish), and P2 (nice to have).

### P0 — Correctness and blockers

1. **Verify the vim bridge is connected.** If the editor bridge is never set from the TUI side, every vim motion that relies on it silently no-ops. Confirm or fix before any further vim work.
2. **Accessibility TUI feature parity.** Either linearize the graphical renderer for screen readers or bring slash commands, session switching, and abort into the accessibility TUI. The current split excludes users from most functionality.

### P1 — High-value polish

3. **Inline image rendering.** Use the shared terminal image component to render image content blocks inline instead of text placeholders.
4. **Unify the palettes.** Merge the terminal palette and the TUI palette into one source of truth; make the diff renderer and every component consume the active theme.
5. **Complete vim mode.** Implement real operator-plus-motion semantics and add visual mode, or adopt a proven keymap pattern.
6. **Windows and Wayland clipboard support.** Add the platform-native clipboard commands for `/copy` and image paste.
7. **Make Ctrl+C abort the active run** with a double-press-to-exit fallback, keeping Escape as a secondary abort.
8. **Refresh the README** to reflect the actual theme count and command list.

### P2 — Nice to have

9. Terminal color-scheme auto-detection to pick dark or light theme automatically.
10. Wire the cross-session input history store into the main loop, or remove the dead code.
11. User-defined custom themes loaded from a config directory.
12. Argument autocomplete for every command; deduplicate the theme preset list.
13. Colorblind-optimized palettes and a true monochrome mode.
14. Incremental streaming render for long assistant messages.
15. Replace silent error swallowing with structured logging behind a debug flag.
16. Split the two monolithic TUI files to bring them under the file-size guideline.

## Next steps

The first implementation pass should focus on the P0 items, since both block a trustworthy feature surface: vim cannot be extended on top of an unwired bridge, and the accessibility TUI cannot ship as the screen-reader path while it lacks core commands. The P1 items are the bulk of the visible polish work and can be sequenced independently once the P0 verifications land.
Loading
Loading