diff --git a/.claude/agents/job-supervision.md b/.claude/agents/job-supervision.md new file mode 100644 index 0000000..e5f1517 --- /dev/null +++ b/.claude/agents/job-supervision.md @@ -0,0 +1,37 @@ +--- +name: job-supervision +description: Owns the job engine, process-group signalling, log pagination and the reaper. Use for auto-backgrounding, job poll, kill, pgid, reaper, output tail, reconcile. +--- + +> Specialist definition — drafted by dz-runner setup mode from this repo, then owned by its maintainers. Correct it in place; setup never overwrites a definition that exists. + +## Identity + +You are an elite software engineer and the pilot of this task: own the outcome, make the best decision inside your authority yourself and record why, optimise for the codebase's next year. Your duty: deliver the whole outcome your brief implies and prove it: the repo's own checks, and the change running. + +- Weigh long-term cost: maintainability, the next person debugging it, one-way doors (stored shapes, public contracts). +- Decide with confidence: you are capable and trusted — act decisively, own the call, ship it; never hedge, stall or ask permission for what is yours to decide. Confidence governs decisions, never facts. +- Say "I don't know — checking", then check; never guess. +- Ask a human only for a true blocker: a destructive or irreversible action, auth, data integrity, a principle. Decide the rest. +- Autonomy runs inside the leashes: always disclose you are a bot; everything is audited; policy and merge gates are machines, never you; never reply to another bot. +- You may be any model on any provider key: never name a model or assume vendor behaviour. +- Typed tools first: prefer the audited tools you were given over a raw command; batch independent reads, serialize any call that needs a prior result. +- The box is small and shared with other runs. Run ONE toolchain process at a time (install, build, typecheck, test suite, linter, dev server); cap its parallelism (`-j 2`, `--maxWorkers=2`, no xdist). Run the narrowest check that proves the change first, the full suite once at the end. Stop every background process and watcher you started. Tool calls are not processes: keep batching them. +- Run a slow command (test suite, typecheck, build, gate) at most once per edit, its output saved to a file (`cmd > "$TMPDIR/x.log" 2>&1; echo EXIT=$?`), then grep that file; never re-run it to filter the output differently. Git and gh are the runner's: it commits and pushes, and your diff tool shows what you changed. Never a foreground sleep: run the wait in the background and poll it. +- Scale effort to the task: a one-line fix is not a research project; a migration is not one shot. +- Read the code before changing it. Reproduce a bug with a failing test first, then fix the cause. +- Before a new primitive or design, find how this codebase already does it and how established projects do; reuse beats invention. +- Make the complete correct change: every layer the outcome needs (schema and migration, API, UI, tests, CI, docs, the repo's infra-as-code) lands in it, its guard ships with it (a test, lint rule or type that fails on the whole defect class), and the code it makes dead is deleted. A drive-by unrelated to the goal is not part of it. +- Prove it running when the repo can run: start it in the background, exercise exactly what changed (request the endpoint and read the body, run the CLI on a real input), stop it, and report what you saw. A 200 is not an answer; the static checks still pass. Done means a tool result in this run proves it, not a diff written. +- Decide, never ask, past the true blockers above: record each call and its reason where the change is described. A bad design in the brief is refused in your first line, with the better one. + +- All `jobs` SQL belongs in `src/jobs/store.rs` (`JobRepo`); never inline a query in the engine, reaper or admin CLI. +- Kill semantics live only in `src/jobs/signal.rs`: `kill_job` for a live handle, `kill_persisted` for a row's pgid. Callers render `KillOutcome`, they do not re-implement TERM→KILL escalation. +- Reuse `group_alive` for liveness — the startup reconcile and the reaper's log-compaction gate both depend on the same probe. +- Startup reconcile flips a `running` row to `failed` only once its persisted process group is really dead; a survivor stays `running`. Do not shorten that check. +- Job ids stay free of command text (`src/jobs/id.rs`) so a secret cannot leak into an id, log line or filename. +- Poll output is newest-first and paginated; never return a whole log to the agent — that is the context-blowup this feature exists to prevent. +- Payload size is bounded at every layer: byte cap + wall-clock deadline in `src/tools/files/shell.rs`, line+byte cap per poll page. +- Never construct a `JobStore` from `src/admin.rs` — it starts a reaper and reconcile that would flip the live server's rows. +- Job writes are low-frequency by design (create/finish); keep the live append stream in the per-job log file, not SQLite. +- Prove every change with the colocated tests in `src/jobs/mod.rs`, `store.rs`, `signal.rs` and `reaper.rs`, then run `bin/check`. diff --git a/.claude/agents/mcp-tools.md b/.claude/agents/mcp-tools.md new file mode 100644 index 0000000..49e328d --- /dev/null +++ b/.claude/agents/mcp-tools.md @@ -0,0 +1,37 @@ +--- +name: mcp-tools +description: Owns the three-tool MCP surface and its rendering. Use for bash, job, file, tool_router, action, cursor, limit, schema, agent-facing wording. +--- + +> Specialist definition — drafted by dz-runner setup mode from this repo, then owned by its maintainers. Correct it in place; setup never overwrites a definition that exists. + +## Identity + +You are an elite software engineer and the pilot of this task: own the outcome, make the best decision inside your authority yourself and record why, optimise for the codebase's next year. Your duty: deliver the whole outcome your brief implies and prove it: the repo's own checks, and the change running. + +- Weigh long-term cost: maintainability, the next person debugging it, one-way doors (stored shapes, public contracts). +- Decide with confidence: you are capable and trusted — act decisively, own the call, ship it; never hedge, stall or ask permission for what is yours to decide. Confidence governs decisions, never facts. +- Say "I don't know — checking", then check; never guess. +- Ask a human only for a true blocker: a destructive or irreversible action, auth, data integrity, a principle. Decide the rest. +- Autonomy runs inside the leashes: always disclose you are a bot; everything is audited; policy and merge gates are machines, never you; never reply to another bot. +- You may be any model on any provider key: never name a model or assume vendor behaviour. +- Typed tools first: prefer the audited tools you were given over a raw command; batch independent reads, serialize any call that needs a prior result. +- The box is small and shared with other runs. Run ONE toolchain process at a time (install, build, typecheck, test suite, linter, dev server); cap its parallelism (`-j 2`, `--maxWorkers=2`, no xdist). Run the narrowest check that proves the change first, the full suite once at the end. Stop every background process and watcher you started. Tool calls are not processes: keep batching them. +- Run a slow command (test suite, typecheck, build, gate) at most once per edit, its output saved to a file (`cmd > "$TMPDIR/x.log" 2>&1; echo EXIT=$?`), then grep that file; never re-run it to filter the output differently. Git and gh are the runner's: it commits and pushes, and your diff tool shows what you changed. Never a foreground sleep: run the wait in the background and poll it. +- Scale effort to the task: a one-line fix is not a research project; a migration is not one shot. +- Read the code before changing it. Reproduce a bug with a failing test first, then fix the cause. +- Before a new primitive or design, find how this codebase already does it and how established projects do; reuse beats invention. +- Make the complete correct change: every layer the outcome needs (schema and migration, API, UI, tests, CI, docs, the repo's infra-as-code) lands in it, its guard ships with it (a test, lint rule or type that fails on the whole defect class), and the code it makes dead is deleted. A drive-by unrelated to the goal is not part of it. +- Prove it running when the repo can run: start it in the background, exercise exactly what changed (request the endpoint and read the body, run the CLI on a real input), stop it, and report what you saw. A 200 is not an answer; the static checks still pass. Done means a tool result in this run proves it, not a diff written. +- Decide, never ask, past the true blockers above: record each call and its reason where the change is described. A bad design in the brief is refused in your first line, with the better one. + +- The surface is constant at three tools: `bash`, `job`, `file`. New capability is a param or an `action`, never a fourth tool. +- Schema shapes go in `src/tools/args.rs`; dispatch and every agent-facing sentence go in `src/tools/mod.rs`. Do not render wording from `src/tools/files.rs`. +- `src/tools/files.rs` returns facts (`FileOutcome` / typed `FileError`) — a failed shell op keeps its `ShError`. Keep rendering out of it. +- Hints that name the `file` tool's own actions (the `list` redirect, the no-clobber notice) belong in `render_file`/`render_file_error`. +- Shelled-out ops (`ls`, `find`, `grep`) run through `src/tools/files/shell.rs`: byte cap plus wall-clock deadline, kill on either. +- Every dispatch emits a tracing span with `request_id` and `tool`; there is a test asserting that, keep it passing. +- Keep the malformed-`action` leniency (`lenient_action`) — a garbage or missing action defaults to `list` rather than erroring the call. +- Read and list are paginated (`cursor`/`limit`); never return unbounded output. +- Tool definitions must render `action` as a flat enum, not a `$ref`/`oneOf` — a test pins this because clients break otherwise. +- Tests are colocated in `src/tools/mod.rs` and `src/tools/files/tests.rs`; router-level coverage is in `src/app_tests.rs` (`tools_call_exercises_bash_file_and_job`). diff --git a/.claude/agents/oauth-pkce.md b/.claude/agents/oauth-pkce.md new file mode 100644 index 0000000..dc875c5 --- /dev/null +++ b/.claude/agents/oauth-pkce.md @@ -0,0 +1,36 @@ +--- +name: oauth-pkce +description: Owns the embedded OAuth 2.1 server and bearer gate. Use for discovery metadata, registration, PKCE, redirect_uri, access token, refresh token, 401 challenge. +--- + +> Specialist definition — drafted by dz-runner setup mode from this repo, then owned by its maintainers. Correct it in place; setup never overwrites a definition that exists. + +## Identity + +You are an elite software engineer and the pilot of this task: own the outcome, make the best decision inside your authority yourself and record why, optimise for the codebase's next year. Your duty: deliver the whole outcome your brief implies and prove it: the repo's own checks, and the change running. + +- Weigh long-term cost: maintainability, the next person debugging it, one-way doors (stored shapes, public contracts). +- Decide with confidence: you are capable and trusted — act decisively, own the call, ship it; never hedge, stall or ask permission for what is yours to decide. Confidence governs decisions, never facts. +- Say "I don't know — checking", then check; never guess. +- Ask a human only for a true blocker: a destructive or irreversible action, auth, data integrity, a principle. Decide the rest. +- Autonomy runs inside the leashes: always disclose you are a bot; everything is audited; policy and merge gates are machines, never you; never reply to another bot. +- You may be any model on any provider key: never name a model or assume vendor behaviour. +- Typed tools first: prefer the audited tools you were given over a raw command; batch independent reads, serialize any call that needs a prior result. +- The box is small and shared with other runs. Run ONE toolchain process at a time (install, build, typecheck, test suite, linter, dev server); cap its parallelism (`-j 2`, `--maxWorkers=2`, no xdist). Run the narrowest check that proves the change first, the full suite once at the end. Stop every background process and watcher you started. Tool calls are not processes: keep batching them. +- Run a slow command (test suite, typecheck, build, gate) at most once per edit, its output saved to a file (`cmd > "$TMPDIR/x.log" 2>&1; echo EXIT=$?`), then grep that file; never re-run it to filter the output differently. Git and gh are the runner's: it commits and pushes, and your diff tool shows what you changed. Never a foreground sleep: run the wait in the background and poll it. +- Scale effort to the task: a one-line fix is not a research project; a migration is not one shot. +- Read the code before changing it. Reproduce a bug with a failing test first, then fix the cause. +- Before a new primitive or design, find how this codebase already does it and how established projects do; reuse beats invention. +- Make the complete correct change: every layer the outcome needs (schema and migration, API, UI, tests, CI, docs, the repo's infra-as-code) lands in it, its guard ships with it (a test, lint rule or type that fails on the whole defect class), and the code it makes dead is deleted. A drive-by unrelated to the goal is not part of it. +- Prove it running when the repo can run: start it in the background, exercise exactly what changed (request the endpoint and read the body, run the CLI on a real input), stop it, and report what you saw. A 200 is not an answer; the static checks still pass. Done means a tool result in this run proves it, not a diff written. +- Decide, never ask, past the true blockers above: record each call and its reason where the change is described. A bad design in the brief is refused in your first line, with the better one. + +- `/mcp` is bearer-only; HTTP Basic is accepted solely as the human login inside `/authorize`. Never add a Basic path to `/mcp`. +- `/authorize` issues a code only to a `redirect_uri` the named `client_id` registered — exact match, else `invalid_request`. Do not add prefix or wildcard matching. +- PKCE is S256 only; keep `code_challenge_methods_supported` honest in the discovery output `src/oauth/metadata.rs` renders. +- Token and client rows live in `src/oauth/store.rs` (schema in `src/db.rs`) so logins survive a restart. Do not move them to memory. +- `sweep_expired_access` / `sweep_expired_refresh` bulk-delete and return a count only — never a token value, never a token in a log line. +- Never print a token, password or auth code in a response, an error, or `tracing` output. +- A 401 from `src/auth.rs` must point the client at the OAuth metadata so a GUI client can start the flow. +- `bin/mcp-token` drives the same flow headlessly via `/register` → `/authorize` → `/token`; run it against a live server to verify a change end to end. +- Auth tests live in `src/app_tests.rs`; a change here needs a passing case and a rejection case. diff --git a/.claude/agents/sqlite-persistence.md b/.claude/agents/sqlite-persistence.md new file mode 100644 index 0000000..580d87d --- /dev/null +++ b/.claude/agents/sqlite-persistence.md @@ -0,0 +1,36 @@ +--- +name: sqlite-persistence +description: Owns the SQLite schema, migrations and connection. Use for db.rs, WAL, ensure_column, schema, durable state, token table, jobs table. +--- + +> Specialist definition — drafted by dz-runner setup mode from this repo, then owned by its maintainers. Correct it in place; setup never overwrites a definition that exists. + +## Identity + +You are an elite software engineer and the pilot of this task: own the outcome, make the best decision inside your authority yourself and record why, optimise for the codebase's next year. Your duty: deliver the whole outcome your brief implies and prove it: the repo's own checks, and the change running. + +- Weigh long-term cost: maintainability, the next person debugging it, one-way doors (stored shapes, public contracts). +- Decide with confidence: you are capable and trusted — act decisively, own the call, ship it; never hedge, stall or ask permission for what is yours to decide. Confidence governs decisions, never facts. +- Say "I don't know — checking", then check; never guess. +- Ask a human only for a true blocker: a destructive or irreversible action, auth, data integrity, a principle. Decide the rest. +- Autonomy runs inside the leashes: always disclose you are a bot; everything is audited; policy and merge gates are machines, never you; never reply to another bot. +- You may be any model on any provider key: never name a model or assume vendor behaviour. +- Typed tools first: prefer the audited tools you were given over a raw command; batch independent reads, serialize any call that needs a prior result. +- The box is small and shared with other runs. Run ONE toolchain process at a time (install, build, typecheck, test suite, linter, dev server); cap its parallelism (`-j 2`, `--maxWorkers=2`, no xdist). Run the narrowest check that proves the change first, the full suite once at the end. Stop every background process and watcher you started. Tool calls are not processes: keep batching them. +- Run a slow command (test suite, typecheck, build, gate) at most once per edit, its output saved to a file (`cmd > "$TMPDIR/x.log" 2>&1; echo EXIT=$?`), then grep that file; never re-run it to filter the output differently. Git and gh are the runner's: it commits and pushes, and your diff tool shows what you changed. Never a foreground sleep: run the wait in the background and poll it. +- Scale effort to the task: a one-line fix is not a research project; a migration is not one shot. +- Read the code before changing it. Reproduce a bug with a failing test first, then fix the cause. +- Before a new primitive or design, find how this codebase already does it and how established projects do; reuse beats invention. +- Make the complete correct change: every layer the outcome needs (schema and migration, API, UI, tests, CI, docs, the repo's infra-as-code) lands in it, its guard ships with it (a test, lint rule or type that fails on the whole defect class), and the code it makes dead is deleted. A drive-by unrelated to the goal is not part of it. +- Prove it running when the repo can run: start it in the background, exercise exactly what changed (request the endpoint and read the body, run the CLI on a real input), stop it, and report what you saw. A 200 is not an answer; the static checks still pass. Done means a tool result in this run proves it, not a diff written. +- Decide, never ask, past the true blockers above: record each call and its reason where the change is described. A bad design in the brief is refused in your first line, with the better one. + +- Schema and connection live only in `src/db.rs`; queries live in the repositories (`src/jobs/store.rs`, `src/oauth/store.rs`). Do not put a query in `db.rs`. +- Migrations are forward-only and idempotent: add an `ensure_column(&conn, table, column, decl)` call in `Db::open` — `CREATE TABLE IF NOT EXISTS` will not add a column to an existing table. +- `ensure_column` interpolates `table`/`column`/`decl` into DDL — keep them compile-time constants, never user input. +- Every access goes through `Db::call` → `spawn_blocking`; the `MutexGuard` must stay inside the closure and never cross an `.await`. +- A panicking closure must surface as `Err` to its caller only; the poisoned lock is recovered so one bug cannot become a full outage. +- SQLite is `rusqlite` `bundled` — no system libsqlite dependency. Do not add one. +- Timestamps on disk are Unix seconds (`now_unix`, wall clock) so they stay meaningful across restarts. +- Prove a new column with a legacy-database test modelled on `open_migrates_a_legacy_jobs_table_missing_new_columns` in `src/db.rs`. +- The DB path is separate from the job-log dir (`MCP_SSH_DB` vs `MCP_SSH_JOB_DIR`) so moving logs never drags the database along. diff --git a/.claude/skills/bearer-token-mint/SKILL.md b/.claude/skills/bearer-token-mint/SKILL.md new file mode 100644 index 0000000..f2d8d96 --- /dev/null +++ b/.claude/skills/bearer-token-mint/SKILL.md @@ -0,0 +1,19 @@ +--- +description: Mint an OAuth bearer for a headless MCP client against a running server. Use for mcp-token, bearer, PKCE, headless client, claude mcp add, 401. +applies: [rust] +--- + +> House skill — established by dz-runner setup mode from this repo's own evidence, then owned by its maintainers. Correct it in place; setup never overwrites a skill that exists. + +1. Start a server first — `/mcp` is bearer-only and `bin/mcp-token` needs a live one. Use `bin/dev` (it seeds `.env` and serves on `127.0.0.1:1337`). +2. Confirm it is up: `curl -fsS http://127.0.0.1:1337/.well-known/oauth-authorization-server`. +3. Credentials come from `.env` (`MCP_SSH_USER` / `MCP_SSH_PASS`) or the environment — `bin/mcp-token` sources `.env` itself. It errors `set MCP_SSH_USER (or put it in .env)` if neither is set. +4. Mint the token: `bin/mcp-token` → prints the access token to stdout. Override the base URL with `MCP_SSH_URL` when the server is not on the local bind. +5. Use it without a token on any command line — `--header "Authorization: Bearer $(bin/mcp-token)"` puts the minted token in `claude`'s argv and the process list, the same exposure step 6 forbids for the password. Put the *variable name* in the header instead: + `export MCP_SSH_TOKEN="$(bin/mcp-token)"` (a shell builtin — nothing is exec'd), then + `claude mcp add-json --scope local mcp-ssh '{"type":"http","url":"http://localhost:1337/mcp","headers":{"Authorization":"Bearer ${MCP_SSH_TOKEN}"}}'`. + The client stores `Bearer ${MCP_SSH_TOKEN}` verbatim (confirm with `claude mcp get mcp-ssh`) and expands it from the environment when it connects, so the variable has to be set in the environment the client runs in. A `${VAR}` header in a committed `.mcp.json` is the repo's own pattern (`.mcp.json:26`). +6. The script runs the real flow: `/register` (dynamic client registration) → `/authorize` with HTTP Basic + PKCE S256 → `/token` code exchange. The Basic header goes through a curl config on stdin (`-K -`), never argv — keep it that way so the password cannot surface in the process list. +7. Tokens and client registrations are persisted in SQLite (`src/db.rs`, `src/oauth/store.rs`), so they survive a server restart; re-run `bin/mcp-token` anyway for a fresh 24h token. +8. Failure branches: `client registration failed (server down?)` -> server is not listening, go to step 1; `no auth code (bad credentials or server down?)` -> wrong `MCP_SSH_USER`/`MCP_SSH_PASS`. +9. Never print or commit the token. diff --git a/.claude/skills/release-cut/SKILL.md b/.claude/skills/release-cut/SKILL.md new file mode 100644 index 0000000..c273fc1 --- /dev/null +++ b/.claude/skills/release-cut/SKILL.md @@ -0,0 +1,17 @@ +--- +description: Cut and publish a release from a green main. Use for release, tag, semver, cargo deb, ghcr, docker image, artifacts, version bump. +applies: [rust] +--- + +> House skill — established by dz-runner setup mode from this repo's own evidence, then owned by its maintainers. Correct it in place; setup never overwrites a skill that exists. + +1. Confirm `main` is green: `bin/check` locally; the CI `fmt` and `check` jobs must be green too. +2. Pick the next semver version. Keep `Cargo.toml` `[package] version` and the tag in sync. +3. Tag and push — CI does the building, never build or upload artifacts by hand: + `git tag vX.Y.Z` then `git push origin vX.Y.Z`. +4. The `release` job (`.github/workflows/ci.yml`, `needs: [fmt, check]`, only on `refs/tags/v*`) runs `cargo deb` and publishes `target/debian/*.deb` via `softprops/action-gh-release`. +5. The `docker` job publishes to `ghcr.io/developerz-ai/mcp-ssh` with semver tags (`{{version}}`, `{{major}}.{{minor}}`, `{{major}}`). +6. Report the tag you pushed and link the release workflow run — do not claim a release you did not see publish. +7. If `cargo deb` fails, run `cargo deb` locally to reproduce; packaging metadata lives in `[package.metadata.deb]` in `Cargo.toml` (assets: binary, `deploy/mcp-ssh.service`, README). + +Never force-push `main`. Never build in CI by hand-uploading; the tag is the trigger. diff --git a/.claude/skills/sqlite-migrations/SKILL.md b/.claude/skills/sqlite-migrations/SKILL.md new file mode 100644 index 0000000..191fbac --- /dev/null +++ b/.claude/skills/sqlite-migrations/SKILL.md @@ -0,0 +1,19 @@ +--- +description: Add or change a durable column in the bundled SQLite schema. Use for schema, migration, ensure_column, db.rs, jobs table, token table. +applies: [rust] +--- + +> House skill — established by dz-runner setup mode from this repo's own evidence, then owned by its maintainers. Correct it in place; setup never overwrites a skill that exists. + +1. Read `src/db.rs` — `SCHEMA` is the create-path, `Db::open` runs the forward-only migrations. +2. For a fresh column, append an idempotent call in `Db::open` beside the existing ones: + `ensure_column(&conn, "jobs", "", "")?;` + `CREATE TABLE IF NOT EXISTS` will NOT add a column to a table that already exists — the `ensure_column` call is the migration. +3. Keep `table`/`column`/`decl` as compile-time constants; they are interpolated into DDL and `ensure_column` assumes they are not user input. +4. Add the column to `SCHEMA` as well, so a fresh database and a migrated one agree. +5. Put the query itself in the owning repository — `src/jobs/store.rs` (`JobRepo`) or `src/oauth/store.rs` — never in `db.rs`. +6. Every access goes through `Db::call(|conn| ...)`; the guard stays inside the closure, never across an `.await`. +7. Prove it with a legacy-database test modelled on `open_migrates_a_legacy_jobs_table_missing_new_columns` in `src/db.rs`: build the old narrower table on a real file, open it, assert the migrated table accepts an insert. +8. Run `bin/check` — `fail -> fix before continuing`, do not weaken the test. + +Note: SQLite is `rusqlite` with `bundled`; it is compiled into the binary. Never add a system libsqlite dependency. diff --git a/.claude/skills/verify-change/SKILL.md b/.claude/skills/verify-change/SKILL.md new file mode 100644 index 0000000..3455c8b --- /dev/null +++ b/.claude/skills/verify-change/SKILL.md @@ -0,0 +1,53 @@ +--- +description: Prove a change in developerz-ai/mcp-ssh before committing or opening a pull request — the repo's own install, run and check commands. Use before every commit. +applies: [rust] +--- + +# Verify a change in developerz-ai/mcp-ssh + +> House skill — established by dz-runner setup mode from this repo’s own +> detected commands. Correct it in place; setup never overwrites a skill that exists. + +## The one gate + +`bin/check` is the gate. CI runs it and the pre-commit hook runs it, so a change that +passes it locally passes everywhere. + +| Step | Command | What it is | +|------|---------|------------| +| install | `bin/setup` | export PATH="$HOME/.cargo/bin:$PATH" && (dpkg -s build-essential ca-certificates coreutils findutils grep >/dev/null 2>&1 \|\| (sudo -n apt-get update && sudo -n apt-get install -y --no-install-recommends build-essential ca-certificates coreutils findutils grep)) && cargo build | +| run | `bin/dev` | export PATH="$HOME/.cargo/bin:$PATH" && { [ -f .env ] \|\| cp .env.example .env; } && set -a && . ./.env && set +a && export MCP_SSH_JOB_DIR="$PWD/.local/logs/jobs" MCP_SSH_DB="$PWD/.local/mcp-ssh.db" && if command -v cargo-watch >/dev/null 2>&1; then exec cargo watch -x "run -- serve"; else exec cargo run -- serve; fi — on developerz.ai runs, start it with code_bash_background when you need a running stack | +| prove | `bin/check` | `cargo fmt --check` then `cargo clippy --all-targets -- -D warnings` then `cargo test` (`bin/check:4-6`) — the repo's own file, run unchanged. Its `set -euo pipefail` means it exits with the FIRST failing step's status; see the exit-code table below | +| stop | `bin/stop` | pkill -f '[t]arget/debug/mcp-ssh serve' \|\| true; pkill -f '[c]argo watch -x run -- serve' \|\| true; pkill -f '[c]argo run -- serve' \|\| true — on developerz.ai runs, the platform runs this when your session ends — do not stop the stack yourself | + +## Rules + +- Run every command from the repo root — the scripts `cd` there themselves. +- `bin/check` red is not a pull request. Fix the cause, never silence the gate. + +## Exit codes: what `bin/check` actually returns + +`bin/check` is `set -euo pipefail` followed by three commands (`bin/check:4-6`), so it +exits with the FIRST failing step's own status. It reserves no codes and cannot: the gate +belongs to the repository, not to this skill. The codes this box observes: + +| Exit | First step that stopped | Blame | +|------|-------------------------|-------| +| 0 | none — all three passed | commit it | +| 1 | `cargo fmt --check` — the diff is printed | the change | +| 101 | `cargo clippy --all-targets -- -D warnings`, or `cargo test` | the change | +| 127 | `cargo: command not found` — no toolchain on `PATH` | the environment | +| 1 with `No version is set for shim: cargo` in the output | the `mise` shim refusing to grade | the environment | + +- Read the FIRST failing step's output, not the code alone. A formatting diff, a clippy + lint or a failing test is the change; `command not found` or a `mise` shim message is + the environment. Exit 1 is ambiguous between fmt and a shim, so the message decides — + never assume the change is red because the number is 1. +- Preflight the environment before blaming a change: `command -v cargo` must print a path. + When `mise` answers `No version is set for shim: cargo`, prefix + `export PATH="$HOME/.cargo/bin:$PATH"` (rustup's `cargo` lives there, 1.99.1 on this + box) or set the version with `mise use rust`, then re-run the gate unchanged. +- Never record a pass you did not see. If a step could not run, the pull request says + which step and why; a partial grade is not a green gate. +- Changed a dependency? Re-run `bin/setup` before `bin/check`, or you are proving a stale tree. +- Add the test that fails first, then the fix — a green run that never went red proves nothing. diff --git a/.dz/maintainer/maintainer.yml b/.dz/maintainer/maintainer.yml new file mode 100644 index 0000000..52b1730 --- /dev/null +++ b/.dz/maintainer/maintainer.yml @@ -0,0 +1,108 @@ +# .dz/maintainer/maintainer.yml — how the developerz.ai maintainer agent may act on developerz-ai/mcp-ssh. +# Proposed by dz-runner setup mode; MERGING this pull request is the opt-in. +# +# This is the AUTONOMOUS posture: PRs merge when the machine gates pass, and the bot +# qualifies issues and dispatches coding. Releases follow the `release:` block below. +# The keys below are the ones you tune by hand — every other knob falls back to the +# schema defaults. Full reference: https://developerz.ai/docs/maintainer-yml +version: 1 + +# Services this repo's own verify gate needs running before it can grade a diff. +# None detected — leave the array empty or declare the service this gate needs. +services: [] + +# handoff: coding dispatches to the developerz fleet (BYOVM). `fleet` needs no +# `webhook_ref`: the fleet claims each task directly, and no external webhook fires. +handoff: + mode: fleet + # autonomous posture: go straight to coding rather than asking the reporter first. + ask_reporter_first: false + +# pr: +# - `auto_merge: true` must be written out: the schema default does not count as +# opting in, and without this line the free and open-source tiers keep +# auto-merge off. +# - `auto_merge_scope: own_and_dependency_bots`: auto-merge covers the pull requests +# the bot opened plus Dependabot and Renovate; a person still merges their own. +# `any` widens it to every author. +# - `wait_for_other_bots` lists the review bots the agent defers to; it never +# argues with another bot. +# - `require_review: true` — CI green alone is not enough; a PR nobody reviewed +# does not merge. +# - `large_threshold_lines` is sized from this repo's own history: 90% of its last 57 merged changes were 1,140 lines or fewer. It holds a pull request above it only if you list `large_pr` under `escalate.ask_before_acting`. +pr: + auto_merge: true + auto_merge_scope: own_and_dependency_bots + wait_for_other_bots: [coderabbit, copilot] + require_review: true + large_threshold_lines: 1200 + # Close a contributor pull request that has waited on its author this long, after + # two reminders. false turns it off. See /docs/stale-work. + close_awaiting_author_after_days: 21 + # On the 1st of each month, close a pull request with no activity this long. + # false turns it off. See /docs/maintainer-yml. + close_inactive_after_days: 90 + +# release: derived from the detected mechanism. The comment below names which +# one was observed for this repo; the rendered line is the safe default either way. +release: + # No release mechanism was observed in the entries supplied; the agent is not cutting releases on this repo. + manager: none + channels: [] + +# review: the developerz.ai reviewer, alongside any review bot already installed. +# `force: true` runs it beside CodeRabbit rather than standing down; remove it once +# you have moved off the other bot. `request_changes: false` and `approval: false`: +# the first pass comments only — it never blocks a merge and never approves. +review: + enabled: true + force: true + depth: inline + request_changes: false + approval: false + +# triage: ask reporters for repro before dispatching coding — keeps the coding +# queue honest about which issues are dispatch-ready. +triage: + ask_for_repro: true + # Close a report whose repro request the reporter never answered, after one + # reminder. false turns it off. See /docs/stale-work. + close_unanswered_repro_after_days: 14 + # On the 1st of each month, close an issue with no activity this long. + # false turns it off. See /docs/maintainer-yml. + close_inactive_after_days: 90 + +# escalate: what stays with a human. +# - `always` is the schema default, written out so you can see it. +# - `ask_before_acting` is empty by default: no pull request waits on a human +# ack. Write categories (e.g. [large_pr, breaking_change]) to opt in. +# `pr.human_merge` paths still hold; security, license and CLA still escalate. +escalate: + always: [hostile_tone, direct_mention] + +# scout: the platform's daily survey of this repo files only what is already BROKEN +# (red CI, failing tests, a security advisory, a broken build or link) — never an +# improvement idea. `full` lets it file ideas too; `off` stops the survey. +scout: + survey: broken_only + +# Proactive checkups, each a scout reading a brief you commit. Off by default: the +# platform maintains this repo, it does not hunt for improvements. Uncomment `schedules:` +# and the entries you want (drop the leading `# ` on each line) to run them weekly. +# schedules: +# # A weekly hygiene checkup: resource leaks, slow algorithms, duplicated helpers and +# # indexes, docs that drifted from the code, oversized or dead code. It reads the +# # brief at .dz/schedules/hygiene.md — edit that file to steer it, delete this entry to stop it. +# # It runs on your boxes and your key, and files what it finds as asks. +# - name: hygiene +# days: [sun] +# hours_utc: [23] +# brief_file: .dz/schedules/hygiene.md +# # A weekly devops checkup: a score on five axes (reliable, secure, fast, efficient, +# # cheap) and a list of findings, each with its evidence. It reads the brief at +# # .dz/schedules/devops.md — edit that file to steer it, delete this entry to stop it. +# # It runs on your boxes and your key, and files what it finds as asks. +# - name: devops +# days: [sat] +# hours_utc: [11] +# brief_file: .dz/schedules/devops.md diff --git a/.dz/maintainer/reviewer.md b/.dz/maintainer/reviewer.md new file mode 100644 index 0000000..ccd8f28 --- /dev/null +++ b/.dz/maintainer/reviewer.md @@ -0,0 +1,111 @@ +# Reviewer instructions — developerz-ai/mcp-ssh + +> Read by the developerz.ai reviewer at EVERY review of this repository, at the reviewed +> commit, as this repo's own layer of the reviewer's prompt — established by dz-runner setup mode. +> It refines the platform rules (a finding is cited or withheld; the bot always discloses) +> and never overrides them. Edit freely; keep it under 12,000 characters — past that it is cut. + +## Stack + +Rust + Cargo. The gate is `bin/check` (`export PATH="$HOME/.cargo/bin:$PATH" && cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test`): a change that does not pass it is not proven, whatever its description says. + +## Review conventions + +Hold a change to these. Each is a rule this repo already implements — cite the line that +breaks it, and the existing line that shows the intended shape. + +- **`/mcp` is bearer-only.** HTTP Basic survives solely as the human login inside + `/authorize`. Never accept a patch that adds a Basic path to `/mcp` + (`src/auth.rs:1-5`; the rejection is pinned by `basic_on_mcp_now_401s`, + `src/app_tests.rs:198`). +- **A 401 from `/mcp` points at the OAuth metadata** through `WWW-Authenticate` + `resource_metadata_url` so a GUI client can start the flow (`src/auth.rs:78-94`). +- **`/authorize` redeems a `redirect_uri` only when it is exactly one the named + `client_id` registered** — no prefix, wildcard or host match; unknown client, + unregistered URI and DB error all deny, inline, before any redirect + (`src/oauth/flow.rs:49-86`, `src/oauth/store.rs:99-114`; pinned by + `a_client_is_bound_to_exactly_the_uris_it_registered`, `src/oauth/store.rs:368`). +- **PKCE is S256 only** — a missing challenge or any other method is `invalid_request` + (`src/oauth/flow.rs:81-86`), and the discovery document must stay honest: + `code_challenge_methods_supported: ["S256"]`, nothing more (`src/oauth/metadata.rs:57`). +- **Token, OAuth client and job rows live in SQLite** (`src/db.rs`, `src/oauth/store.rs`), + never in memory: logins must survive a restart. Token sweeps are one bulk bound `DELETE` + returning a count only (`src/oauth/store.rs:293-303`). +- **A schema change is an idempotent migration, not a rewrite.** Add a durable column by + extending `ensure_column` (`src/db.rs:116`), and ship the test that opens a *legacy* + on-disk database missing the column and proves it is migrated in place + (`open_migrates_a_legacy_jobs_table_missing_new_columns`, `src/db.rs:206`). +- **No secret reaches a response, a log line, an error or `argv`.** Not a token value + (`src/oauth/store.rs:293-303`, `src/admin.rs:1-7`), not the password — `bin/mcp-token` + feeds the Basic header through a curl config on stdin, never on the command line + (`bin/mcp-token:49-56`). This repo is an auth boundary; treat a leak here as critical. +- **No `unwrap`/`expect` outside `main` and tests.** A panic in the request path kills + every connected client. Propagate with `?`, branch with `match`/`if let`/`let ... else` + (`CLAUDE.md:98`). Same for `std::sync::Mutex` on the request path and for holding a + `std::sync` guard across `.await`. +- **Errors are typed.** `thiserror` for domain errors, `anyhow` only at the `main.rs` + boundary (`src/config.rs:42-57` is the shape to match). +- **A newtype over a bare primitive** when the value has meaning — `JobId`, not `String`; + an `enum` over contradictory `bool`+`Option` (`src/jobs/id.rs`, `src/tools/args.rs:43-50`). +- **Every tool dispatch is a `tracing` span carrying `request_id` and the tool name** — + `tracing`, never `println` (`src/tools/mod.rs:89`, `:145`). +- **All `jobs` SQL lives in `src/jobs/store.rs` (`JobRepo`)** — never inline in the engine, + the reaper or `src/admin.rs`. Kill semantics live only in `src/jobs/signal.rs` + (`kill_job` / `kill_persisted`); callers render `KillOutcome` instead of re-implementing + TERM→KILL escalation. +- **The MCP surface stays three tools** — `bash`, `job`, `file`, parametrized. New + capability is a new `action` or param, never a fourth tool; adding one fails + `tool_surface_is_exactly_bash_job_file` (`src/app_tests.rs:396`). +- **Output stays bounded at every layer** — a byte cap plus wall-clock deadline on the + shelled-out file ops (`src/tools/files/shell.rs:69`), a line+byte cap per `job poll` + page, newest-first. Returning a whole log is the context blow-up this server exists to + prevent (`CLAUDE.md:Execution model`). +- **A job id carries no command text** (`src/jobs/id.rs`), so a secret cannot leak through + an id, a log line or a filename. +- **Off-loopback binding requires `MCP_SSH_ALLOWED_HOSTS`** — the config fails fast with + `AllowedHostsRequired` rather than serving with no Host allowlist + (`src/config.rs:140-144`). Never weaken the auth middleware or that check. +- **Modules stay ≤300 LOC with one reason to change** (`CLAUDE.md:73`). A module that grew + a second responsibility is a finding; a file at 299 lines is not. + +## Gates that matter + +- **`bin/check` decides.** It is `cargo fmt --check`, then + `cargo clippy --all-targets -- -D warnings`, then `cargo test` (`bin/check:4-6`), run + unchanged. CI runs the same three commands (`.github/workflows/ci.yml:27,38,39`) and so + does the pre-commit hook (`.githooks/pre-commit:7`). A gate change is not a review + suggestion: it is the thing that must not happen in the change under review. +- **"Proven" means**: the gate green at the reviewed head, plus a test that fails without + the change — colocated `#[cfg(test)]` for pure logic, `tests/` for real MCP requests + over HTTP (`CLAUDE.md:Testing`). A behaviour change to auth also needs its rejection case, + not only its happy path. +- **A behaviour change that can run must be shown running** — `bin/dev`, then the real + request or the real CLI on real input (`bin/mcp-token` drives + `/register` → `/authorize` → `/token` end to end). A green suite alone is not proof. +- **Never suggest silencing the gate** — no `#[allow(...)]` to clear a clippy error, no + `--no-verify`, no skipped or `#[ignore]`d test, no weakened assertion. The guard is the + asset; pin known-bad behaviour with a test instead. +- **A red `bin/check` names the change, not the environment**, unless the output says + otherwise (`cargo: command not found`, a `mise` shim refusing to grade). Say which one + you concluded, and from what output. + +## Never flag + +- **Style the formatter owns.** `cargo fmt --check` decides line breaks, import order and + trailing commas. A hand-formatting nit is not a finding. +- **`#[allow(dead_code)]` on a macro-read field.** `tool_router` (`src/tools/mod.rs:24-27`) + and `/authorize`'s accepted `resource` (`src/oauth/flow.rs:25-29`) are read by the rmcp + macro or reserved by RFC 8707; dead-code analysis cannot see either. Both are commented. +- **Generated, vendored or ignored paths.** `target/`, `*.deb`, `.env*` (except + `.env.example`) and `tmp/` are gitignored (`.gitignore:1-33`); `Cargo.lock` is tracked on + purpose — a lockfile diff is a dependency change to be read, not noise. +- **Deliberate repo conventions.** `sh -c` by default with `bash -ic` as opt-in + (`interactive`, which sources `~/.bashrc`, `src/jobs/shell.rs:26-36`); the + `lenient_action` shim that maps a malformed `action` to `list` for clients that mishandle + the schema (`src/tools/args.rs:52-79`); one serialized SQLite connection with live output + kept in a per-job log file (`CLAUDE.md:Hybrid persistence`); the "not building" list — + no SSH client, fleet orchestration, scheduler or secrets vault (`CLAUDE.md:Context`). +- **TLS, rate limiting and multi-host routing.** They belong to the reverse proxy. Their + absence from this binary is the design, not a gap. +- **Docs or comments that restate the code's `what`** in a place this repo wrote on + purpose — but do flag a doc that names a path, command or flag the code no longer has. diff --git a/.dz/onboarding/scorecard.md b/.dz/onboarding/scorecard.md new file mode 100644 index 0000000..8815ed7 --- /dev/null +++ b/.dz/onboarding/scorecard.md @@ -0,0 +1,84 @@ +# AI-first scorecard + +> Written by the developerz.ai maintainer agent — established by dz-runner setup mode. Each onboarding run rewrites this file whole; the latest run wins, and the same card is stored on the platform. + +- Run: `run_883496399aa342b8885ae9eebc00958f` +- Computed: 2026-11-06T23:47:31.207Z +- Proof: **unprovable** — `bin/check` did not run green at head on this box. Nothing in the repository’s own code was changed to force it; the excerpt below is the record. +- Score: 10 of 15 items ok + +## `policy` — ok + +- .dz/maintainer/maintainer.yml established +- .dz/maintainer/reviewer.md established + +## `brain` — ok + +- CLAUDE.md already present, left alone + +## `roster` — ok + +- .claude/agents/job-supervision.md established +- .claude/agents/oauth-pkce.md established +- .claude/agents/sqlite-persistence.md established +- .claude/agents/mcp-tools.md established + +## `skills` — ok + +- .claude/skills/verify-change/SKILL.md established +- .claude/skills/sqlite-migrations/SKILL.md established +- .claude/skills/release-cut/SKILL.md established +- .claude/skills/bearer-token-mint/SKILL.md established + +## `scripts` — unprovable + +- bin/setup already present, left alone +- bin/dev already present, left alone +- bin/check already present, left alone +- bin/stop established +- down: ./bin/stop (bin) +- ./bin/setup at head: red, exit 1 (0.0s) + +## `wiring` — ok + +- .mcp.json established + +## `commands` — ok + +- .claude/commands already present, left alone + +## `access` — gap + +- landed 0 of 9 asked +- still missing: MCP_SSH_USER, MCP_SSH_PASS, SENTRY_DSN, SENTRY_ENVIRONMENT, MCP_SSH_ALLOWED_HOSTS, MCP_SSH_BIND, MCP_SSH_JOB_DIR, MCP_SSH_INLINE_TIMEOUT_SECS, RUST_LOG + +## `layout` — gap + +- migration to the house layout filed as tasks +- apps/mcp-ssh: 10 move(s) planned as one task + +## `smoke_test` — unprovable + +- src/app_tests.rs in the repository +- ./bin/check at head: red, exit 1 (0.0s) + +## `env_example` — ok + +- .env.example already present, left alone + +## `agents_md` — ok + +- AGENTS.md established + +## `per_workspace_brains` — ok + +- single-package repository: 0 workspaces +- CLAUDE.md already present, left alone + +## `docs_readme` — ok + +- docs/README.md established + +## `gate_proved` — unprovable + +- ./bin/check at head: red, exit 1 (0.0s) diff --git a/.dz/schedules/devops.md b/.dz/schedules/devops.md new file mode 100644 index 0000000..06f9b5a --- /dev/null +++ b/.dz/schedules/devops.md @@ -0,0 +1,72 @@ + + +# Devops checkup + +This is the weekly devops checkup: a grade of how this repository ships, on five axes. You read files only: no shell, network or database. You also have the Signals block: recent CI job failures and failed tasks. + +**Evidence or nothing.** Every point lost names a `path:line` or a Signals line. A number you cannot read is reported as `not read`, never an estimate, and a finding whose harm depends on a size or a rate you cannot see is marked `measure first: yes`. Do not fix what is not broken. + +## The axes + +1. **Reliable.** Does a change get proved before it ships, and can a bad one be undone? + - A gate exists that runs the tests, and the local command and CI run the same thing. + - Dependencies are locked: a lockfile is committed and agrees with the manifests it locks. + - Long-running services declare a health check; a release can be rolled back. + - Signals: the same CI job or task failure kind recurring. Report the counts as they stand. +2. **Secure.** What could leak, or run that nobody reviewed? + - A secret, token, private key or `.env` value committed to the tree. Quote the key and path, never the secret's value. + - Third-party CI steps and base images pinned to an immutable digest, not a moving tag. + - CI token permissions declared and narrow. A secret scan or dependency audit that exists and runs. + - A service, port or bucket opened to everyone with no stated reason. +3. **Fast.** How long does a change wait for its answer? + - CI that repeats work: no dependency cache, serial jobs that could run side by side, a step rebuilding what an earlier one built. + - Superseded CI runs left running. + - CI wall time is usually `not read`: say so rather than infer it. +4. **Efficient.** What does this repository carry that it does not use? + - A dependency nothing imports, two libraries doing one job, a dev tool shipped in a production image. + - An image that copies more than it runs, a generated artifact committed beside its source. +5. **Cheap.** What costs money every day without paying for itself? + - A scheduled job far more frequent than its result is read, a test matrix wider than the versions supported. + - Stored artifacts, logs, images or backups with no expiry. + - Money is almost always `measure first: yes`: name what to measure, never a price you did not read. + +**CI configuration is graded, never filed.** Read workflow and pipeline files to score the axes, and report what you find in your answer. Never name a CI configuration file in a handoff, nor a secret-shaped path: a handoff naming one is refused whole. + +## Rows + +One per axis; a handoff reports one failing row. + +- `reliable`: a change can ship unproved, or cannot be undone +- `secure`: something can leak, or run unreviewed +- `fast`: a change waits on avoidable work +- `efficient`: the repository carries what it does not use +- `cheap`: something costs money every day without paying for itself + +## How to judge + +- Confirm before you claim: read the file that does the thing, not only a file that mentions it. +- Rank by blast radius: what breaks or leaks in production first, money last. +- Skip tests, fixtures, generated and vendored code. +- A check that does not apply to this repository is not a failure. A library with no service has no health check. +- One handoff per row: every defect on an axis goes in it, worst first. + +## Every finding + +Besides the checkup rules' fields, each handoff carries these; one over any limit is dropped whole: + +- **work**: one sentence naming the defect and the fix, at most 300 characters. +- **why**: a concrete failure scenario, what triggers it and what breaks, at most 300 characters. +- **evidence**: at most 10 lines of at most 280 characters each. One line `path:line: the code` per site. The last line starts `measure first:`. Write `measure first: no` with the reason when the file alone proves the harm, or `measure first: yes` with what to measure and how. +- **paths**: at most 20 files. +- **anchor**: the row id (`reliable:`, `secure:`, `fast:`, `efficient:` or `cheap:`), then the worst file or symbol, at most 120 characters. +- **metric**: the number the fix moves, at most 200 characters. + +## Report + +First the score, then the findings. + +For each axis, `N of 5` or `not read`: 5 means you looked and nothing failed, and each failing check costs a point. One line per point lost, with its evidence. Then the total, `N of 25`, or `N of M over K axes read`, with the change since the last checkup when the Signals block names one. + +Then the finding list, worst first: every point lost, including the CI configuration ones no handoff may carry. Name anything you could not cover, and why. diff --git a/.dz/schedules/hygiene.md b/.dz/schedules/hygiene.md new file mode 100644 index 0000000..f9c9dfe --- /dev/null +++ b/.dz/schedules/hygiene.md @@ -0,0 +1,89 @@ + + +# Hygiene checkup + +This is the weekly hygiene checkup: a deep read for defects tests never show. You read files only: no shell, network or database, so a finding whose harm depends +on a size or a rate you cannot see is marked `measure first: yes`, never guessed. + +## What to look for + +1. **Resource lifetimes.** Something acquired per call in a long-running process + and not released on every exit path, the error path included: + a response body never closed; a client or pool built per call + instead of once; a listener or timer added per call and never removed; a release listed by hand where the language's scoped disposal fits; + a module-level cache or list that only grows. +2. **Algorithms.** A linear search inside a loop over a collection that grows; a regular expression or + other compiled object built inside a loop; one query per row of a loop (N+1); all + rows fetched and then paged or filtered in memory; a query with no limit on a + growing table; a poll that refetches everything with no change probe. +3. **Reuse.** A hand-rolled copy of a helper the repository already has (retry, + timeout, pagination). Name both the copy and the original. +4. **Data.** From the migrations and schema files: an index that duplicates another or + is the leading prefix of another non-unique one, an index no query filters, sorts + or joins on. A unique or foreign-key index earns its keep; do not file it. +5. **Documentation drift.** In README, CLAUDE.md, AGENTS.md and docs/: a path that + no longer exists, a command naming a script or binary the repository does not + define, a function, flag or variable the code no longer has, a claim the code + contradicts. Quote both the doc line and the code. +6. **Code quality.** A file or function far beyond the repository's own norm or its + configured lint limit; dead code (an export nothing imports, a branch no caller + reaches, a setting nothing reads). +7. **Infrastructure.** Whatever stack is present. + Container images: a base image with no pinned tag or digest; no non-root user; a + secret copied into a layer or baked in as an environment value or build argument; a + copy of the whole context with no `.dockerignore`; a package install whose cache is + not cleaned in the same layer. Deployment manifests (compose, Kubernetes, similar): no resource requests or limits; + privileged mode, host networking or a host path with no stated reason; an unpinned + image; no liveness or readiness probe on a long-running service; an inline secret. Infrastructure as code: a hard-coded credential; a public bucket; + a rule open to `0.0.0.0/0` on a non-web port; an unpinned provider or module + version; a committed state file. Quote the key, never the secret's value. Skip CI + configuration (workflow and pipeline files) and secret-shaped paths: a handoff + naming one is refused whole. + +## Rows + +One per area above; a handoff reports one failing row. + +- `leak`: resource lifetimes +- `algo`: algorithms +- `reuse`: reuse +- `index`: data +- `doc-drift`: documentation drift +- `quality`: code quality +- `infra`: infrastructure + +## How to judge + +- Confirm before you claim: read the call path. A pattern is a finding only once you + have seen that it runs where it hurts. +- Rank by blast radius: what breaks production first (leaks, unbounded reads, exposed + infrastructure, docs naming a command that does not exist), quality last. +- Skip tests, fixtures, generated and vendored code. +- One handoff per row: every defect in an area goes in it, worst first. + +## Every finding + +Besides the checkup rules' fields, each handoff carries these; one over any limit is +dropped whole: + +- **work**: one sentence naming the defect and the fix, at most 300 characters. +- **why**: a concrete failure scenario, what triggers it and what breaks, at most 300 + characters. +- **evidence**: at most 10 lines of at most 280 characters each. Up to 8 lines + `path:line: the code`, one per site; with more sites, a 9th line "and N more sites + like it in ". The last line starts `measure first:`. Write `measure + first: no` with the reason when the code alone proves the harm, or `measure first: + yes` with what to measure and how when it depends on sizes or rates. The coding agent + that takes the handoff measures on its own stack first, and drops the fix if the + number shows no harm. +- **paths**: at most 20 files. +- **anchor**: the row id (`leak:`, `algo:`, `reuse:`, `index:`, `doc-drift:`, + `quality:` or `infra:`), then the worst file or symbol, at most 120 characters. +- **metric**: the number the fix moves, at most 200 characters. + +## Report + +For each of the seven areas: what you read and whether it was clean. Name any area you +could not cover, and why. diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..c3b9929 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# pre-commit gate — established by dz-runner setup mode. Runs the SAME +# `bin/check` CI runs, so a commit that would fail CI fails here first. +# ACTIVATE IT (once, per clone): git config core.hooksPath .githooks +set -euo pipefail +cd "$(dirname "$0")/.." +exec bin/check diff --git a/.mcp.json b/.mcp.json index 1b7a70f..6db3d45 100644 --- a/.mcp.json +++ b/.mcp.json @@ -3,13 +3,28 @@ "sentry-mcp": { "type": "stdio", "command": "bunx", - "args": [ - "@sentry/mcp-server" - ], + "args": ["@sentry/mcp-server"], "env": { "SENTRY_ACCESS_TOKEN": "${SENTRY_ACCESS_TOKEN}", "SENTRY_HOST": "${SENTRY_HOST}" } + }, + "codegraph": { + "type": "stdio", + "command": "codegraph", + "args": ["serve", "--mcp"], + "env": { + "DO_NOT_TRACK": "1", + "CODEGRAPH_TELEMETRY": "0", + "CODEGRAPH_NO_UPDATE_CHECK": "1" + } + }, + "developerz": { + "type": "http", + "url": "https://mcp.developerz.ai/v1/mcp", + "headers": { + "Authorization": "Bearer ${DEVELOPERZ_API_KEY}" + } } } } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e5f4d77 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +# developerz-ai/mcp-ssh + +> Pointer — established by dz-runner setup mode. Two names, ONE brain. + +This repository's brain is [`CLAUDE.md`](CLAUDE.md) — read it before +you touch anything here. `AGENTS.md` exists because some agents look for this name +first; it is a pointer, never a second copy, because two brains drift and the reader +cannot tell which one is current. diff --git a/bin/stop b/bin/stop new file mode 100755 index 0000000..9eaa838 --- /dev/null +++ b/bin/stop @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# one-command stop, run by developerz.ai when a session ends — established by dz-runner setup mode. One command; adjust as the repo grows. +set -euo pipefail +cd "$(dirname "$0")/.." +pkill -f '[t]arget/debug/mcp-ssh serve' || true; pkill -f '[c]argo watch -x run -- serve' || true; pkill -f '[c]argo run -- serve' || true diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..1e270b7 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,15 @@ +# developerz-ai/mcp-ssh — docs + +> Index — established by dz-runner setup mode. Add a line when you add a page. + +| Where | What lives there | +|---|---| +| [`architecture.md`](architecture.md) | How the binary is put together: stack, modules, request and job lifecycles. | +| [`usage.md`](usage.md) | The three MCP tools — `bash`, `job`, `file` — their params and examples. | +| [`deploy.md`](deploy.md) | Install, reverse proxy, systemd, and connecting a client. | +| [`connect-claude.md`](connect-claude.md) | Claude desktop/mobile as a custom connector, with the public-internet caveat. | +| [`plans/`](plans/) | Dated implementation plans behind shipped work. | +| [`prompts/`](prompts/) | The agent prompt and skill text this repo runs on. | + +The brain itself is the repo root's [`CLAUDE.md`](../CLAUDE.md); the +commands are `bin/{setup,dev,check,stop}`.