Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ mayros -p "refactor auth flow" # headless mode (non-interactive)

- 🎨 3 themes (dark, light, high-contrast) — `/theme`
- 📝 3 output styles (standard, explanatory, learning) — `/style`
- ⌨️ Vim mode with motions, operators, undo — `/vim`
- 📋 `Ctrl+V` image paste from clipboard
- 📊 `/diff` inline diff viewer · `/context` token usage chart
- 🗺️ `/plan` semantic plan mode (Cortex-backed)
Expand All @@ -165,15 +164,15 @@ mayros -p "refactor auth flow" # headless mode (non-interactive)

**Slash commands (30+):**

| Command | Description | Command | Description |
| ---------------- | ----------------- | ---------------- | -------------------- |
| `/help` | List all commands | `/plan` | Semantic plan mode |
| `/new` | Reset session | `/diff` | Show pending changes |
| `/compact` | Compact context | `/context` | Token usage chart |
| `/think <level>` | Set thinking | `/theme` | Cycle themes |
| `/model <name>` | Switch model | `/vim` | Toggle vim mode |
| `/permission` | Permission mode | `/copy` | Copy last response |
| `/fast` | Fast mode | `/export [file]` | Export session |
| Command | Description | Command | Description |
| ---------------- | ----------------- | ------------- | -------------------- |
| `/help` | List all commands | `/plan` | Semantic plan mode |
| `/new` | Reset session | `/diff` | Show pending changes |
| `/compact` | Compact context | `/context` | Token usage chart |
| `/think <level>` | Set thinking | `/theme` | Cycle themes |
| `/model <name>` | Switch model | `/permission` | Permission mode |
| `/fast` | Fast mode | `/copy` | Copy last response |
| `/export [file]` | Export session | `/context` | Token usage chart |

**Markdown-driven extensibility:**

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
2 changes: 1 addition & 1 deletion apps/macos/Sources/Mayros/ModelCatalogLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ enum ModelCatalogLoader {
]
for root in roots {
let candidate = root
.appendingPathComponent("node_modules/@mariozechner/pi-ai/dist/models.generated.js")
.appendingPathComponent("node_modules/@earendil-works/pi-ai/dist/models.generated.js")
if FileManager().isReadableFile(atPath: candidate.path) {
return candidate.path
}
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/oauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ mayros models status

## OAuth exchange (how login works)

Mayros’s interactive login flows are implemented in `@mariozechner/pi-ai` and wired into the wizards/commands.
Mayros’s interactive login flows are implemented in `@earendil-works/pi-ai` and wired into the wizards/commands.

### Anthropic (Claude Pro/Max) setup-token

Expand Down
2 changes: 1 addition & 1 deletion docs/gateway/doctor.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Current migrations:
### 2b) OpenCode Zen provider overrides

If you’ve added `models.providers.opencode` (or `opencode-zen`) manually, it
overrides the built-in OpenCode Zen catalog from `@mariozechner/pi-ai`. That can
overrides the built-in OpenCode Zen catalog from `@earendil-works/pi-ai`. That can
force every model onto a single API or zero out costs. Doctor warns so you can
remove the override and restore per-model API routing + costs.

Expand Down
12 changes: 6 additions & 6 deletions docs/pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ Mayros uses the pi SDK to embed an AI coding agent into its messaging gateway ar

```json
{
"@mariozechner/pi-agent-core": "0.49.3",
"@mariozechner/pi-ai": "0.49.3",
"@mariozechner/pi-coding-agent": "0.49.3",
"@mariozechner/pi-tui": "0.49.3"
"@earendil-works/pi-agent-core": "0.49.3",
"@earendil-works/pi-ai": "0.49.3",
"@earendil-works/pi-coding-agent": "0.49.3",
"@earendil-works/pi-tui": "0.49.3"
}
```

Expand Down Expand Up @@ -164,7 +164,7 @@ import {
DefaultResourceLoader,
SessionManager,
SettingsManager,
} from "@mariozechner/pi-coding-agent";
} from "@earendil-works/pi-coding-agent";

const resourceLoader = new DefaultResourceLoader({
cwd: resolvedWorkspace,
Expand Down Expand Up @@ -504,7 +504,7 @@ Mayros also has a local TUI mode that uses pi-tui components directly:

```typescript
// src/tui/tui.ts
import { ... } from "@mariozechner/pi-tui";
import { ... } from "@earendil-works/pi-tui";
```

This provides the interactive terminal experience similar to pi's native mode.
Expand Down
Loading
Loading