Skip to content
5 changes: 5 additions & 0 deletions .changeset/read-surface-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stainless-code/codemap": patch
---

Harden read surfaces: `codemap query --format …` blocks index mutations via the same read-only guard as `--json`; `codemap serve` requires `--token` when `--host` is not loopback (any `127.0.0.0/8` address counts as loopback, so `--token` stays optional on `127.0.0.2` and similar); `codemap validate` (and MCP/HTTP `validate`) can return `rejected` rows with a `reason` when a path escapes the project root, resolves outside via symlink, or is a broken symlink — output `path` keys are always project-relative POSIX paths.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ codemap dead-code --json # outcome alias →
codemap query --json --recipe fan-out # recipe SQL by id (alias: -r)
codemap query --json "SELECT name, file_path FROM symbols WHERE name = 'foo'" # ad-hoc SQL
codemap --files src/a.ts src/b.tsx # targeted re-index after edits
codemap validate --json # detect stale / missing / unindexed files
codemap validate --json # detect stale / missing / unindexed / rejected files
codemap context --compact --for "refactor auth" # JSON envelope + intent-matched recipes
codemap ingest-coverage coverage/coverage-final.json --json # Istanbul / LCOV (auto-detected) → coverage table; joins with symbols
NODE_V8_COVERAGE=.cov bun test && codemap ingest-coverage .cov --runtime --json # V8 protocol (per-process dumps); local-only
Expand Down Expand Up @@ -162,9 +162,9 @@ codemap query --format diff-json 'SELECT "README.md" AS file_path, 1 AS line_sta
codemap --with-fts --full
codemap query --recipe text-in-deprecated-functions # demonstrates FTS5 ⨯ symbols ⨯ coverage JOIN
# HTTP API — same tool taxonomy as `codemap mcp`, exposed over POST /tool/{name} for
# non-MCP consumers (CI scripts, curl, IDE plugins). Loopback default; optional --token.
# non-MCP consumers (CI scripts, curl, IDE plugins). Loopback default; --token required on non-loopback.
TOKEN=$(openssl rand -hex 32)
codemap serve --port 7878 --token "$TOKEN" &
codemap serve --port 7878 --token "$TOKEN" & # --token required when --host is not loopback
curl -s -X POST http://127.0.0.1:7878/tool/query \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ CI-aggregate flag on `codemap query` and `codemap audit`. Aliases `--format sari

### `codemap validate`

CLI subcommand comparing on-disk SHA-256 against `files.content_hash`. Statuses: `stale | missing | unindexed`. Exits `1` on any drift.
CLI subcommand comparing on-disk SHA-256 against `files.content_hash`. Statuses: `stale | missing | unindexed | rejected` (`rejected` carries optional `reason` when a path escapes the project root, resolves outside via symlink, or has a broken symlink; output `path` keys are always project-relative POSIX paths). Exits `1` on any drift.

### `module_cycles` (table) / circular imports

Expand Down Expand Up @@ -565,7 +565,7 @@ Long-running process that subscribes to filesystem changes via [chokidar v5](htt

### `codemap serve` / HTTP server

Long-running HTTP server exposing the same tool taxonomy as `codemap mcp` over `POST /tool/{name}` for non-MCP consumers (CI scripts, simple `curl`, IDE plugins that don't speak MCP). Default bind **`127.0.0.1:7878`** (loopback only — refuse `0.0.0.0` unless explicitly opted in via `--host 0.0.0.0`); optional `--token <secret>` requires `Authorization: Bearer <secret>` on every request. HTTP returns each tool's native JSON payload directly (NOT MCP's `{content: [...]}` wrapper); `query` / `query_recipe` match `codemap query --json` row arrays unless `summary` / `group_by` reshape the envelope, or `baseline` returns a diff envelope (incompatible with non-`json` `format` / `group_by`; save/list/drop remain separate tools); parity twins (`query batch`, `trace`, `explore`, `node`, `file`, `schema`, `symbols`, `context`, `ingest-coverage`, `ingest-churn`) always emit JSON on CLI without `--json`; other tools match their CLI `--json` payloads when that flag is set; `format: "sarif"` payloads ship as `application/sarif+json`, `format: "annotations"` / `"mermaid"` / `"diff"` / `"badge"` (markdown) as `text/plain; charset=utf-8`, `format: "diff-json"` / `"codeclimate"` / `"badge"` + `badge_style: "json"` as `application/json; charset=utf-8`. Routes: `POST /tool/{name}` (every MCP tool), `GET /resources/{encoded-uri}` (resource handler for `codemap://recipes`, `codemap://recipes/{id}`, `codemap://schema`, `codemap://skill`, `codemap://rule`, `codemap://mcp-instructions`, `codemap://files/{path}`, and `codemap://symbols/{name}`), `GET /health` (auth-exempt liveness probe — does not start the watcher), `GET /tools` / `GET /resources` (catalogs). With `--watch`, chokidar is refcount-gated per request and stops 5s after the last client (`HTTP_WATCH_RELEASE_GRACE_MS`) — distinct from MCP idle shutdown; the HTTP process keeps listening. Pure transport — same `tool-handlers.ts` / `resource-handlers.ts` MCP uses; no engine duplication. Errors → `{"error": "..."}` with HTTP status 400 / 401 / 403 / 404 / 500. SIGINT / SIGTERM → graceful drain. Every response carries `X-Codemap-Version: <semver>`. **CSRF + DNS-rebinding guard:** every request (including auth-exempt `/health`) is evaluated against `Sec-Fetch-Site` / `Origin` / `Host` when present — modern browsers send `Sec-Fetch-Site` and `Origin` on cross-origin fetches (header presence varies by request type, browser, and privacy settings), so the guard rejects browser-driven cross-origin requests like a malicious local webpage `fetch`-ing `http://127.0.0.1:7878/tool/save_baseline` to mutate `.codemap/index.db`. `Host` mismatch on a loopback bind blocks DNS rebinding (an attacker resolving `evil.com` to `127.0.0.1` post-load). Non-browser clients (curl, fetch from Node, MCP hosts, CI scripts) typically omit these headers and pass through. Implementation: `src/cli/cmd-serve.ts` (CLI shell) + `src/application/http-server.ts` (transport). See [`architecture.md` § HTTP wiring](./architecture.md#cli-usage).
Long-running HTTP server exposing the same tool taxonomy as `codemap mcp` over `POST /tool/{name}` for non-MCP consumers (CI scripts, simple `curl`, IDE plugins that don't speak MCP). Default bind **`127.0.0.1:7878`** (loopback only — refuse `0.0.0.0` unless explicitly opted in via `--host 0.0.0.0`; any **`127.0.0.0/8`** address counts as loopback for the token rule); `--token <secret>` is optional on loopback binds and **required** on non-loopback binds — when set, every request needs `Authorization: Bearer <secret>`. HTTP returns each tool's native JSON payload directly (NOT MCP's `{content: [...]}` wrapper); `query` / `query_recipe` match `codemap query --json` row arrays unless `summary` / `group_by` reshape the envelope, or `baseline` returns a diff envelope (incompatible with non-`json` `format` / `group_by`; save/list/drop remain separate tools); parity twins (`query batch`, `trace`, `explore`, `node`, `file`, `schema`, `symbols`, `context`, `ingest-coverage`, `ingest-churn`) always emit JSON on CLI without `--json`; other tools match their CLI `--json` payloads when that flag is set; `format: "sarif"` payloads ship as `application/sarif+json`, `format: "annotations"` / `"mermaid"` / `"diff"` / `"badge"` (markdown) as `text/plain; charset=utf-8`, `format: "diff-json"` / `"codeclimate"` / `"badge"` + `badge_style: "json"` as `application/json; charset=utf-8`. Routes: `POST /tool/{name}` (every MCP tool), `GET /resources/{encoded-uri}` (resource handler for `codemap://recipes`, `codemap://recipes/{id}`, `codemap://schema`, `codemap://skill`, `codemap://rule`, `codemap://mcp-instructions`, `codemap://files/{path}`, and `codemap://symbols/{name}`), `GET /health` (auth-exempt liveness probe — does not start the watcher), `GET /tools` / `GET /resources` (catalogs). With `--watch`, chokidar is refcount-gated per request and stops 5s after the last client (`HTTP_WATCH_RELEASE_GRACE_MS`) — distinct from MCP idle shutdown; the HTTP process keeps listening. Pure transport — same `tool-handlers.ts` / `resource-handlers.ts` MCP uses; no engine duplication. Errors → `{"error": "..."}` with HTTP status 400 / 401 / 403 / 404 / 500. SIGINT / SIGTERM → graceful drain. Every response carries `X-Codemap-Version: <semver>`. **CSRF + DNS-rebinding guard:** every request (including auth-exempt `/health`) is evaluated against `Sec-Fetch-Site` / `Origin` / `Host` when present — modern browsers send `Sec-Fetch-Site` and `Origin` on cross-origin fetches (header presence varies by request type, browser, and privacy settings), so the guard rejects browser-driven cross-origin requests like a malicious local webpage `fetch`-ing `http://127.0.0.1:7878/tool/save_baseline` to mutate `.codemap/index.db`. `Host` mismatch on a loopback bind blocks DNS rebinding (an attacker resolving `evil.com` to `127.0.0.1` post-load). Non-browser clients (curl, fetch from Node, MCP hosts, CI scripts) typically omit these headers and pass through. Implementation: `src/cli/cmd-serve.ts` (CLI shell) + `src/application/http-server.ts` (transport). See [`architecture.md` § HTTP wiring](./architecture.md#cli-usage).

### Code Climate format (`codeclimate`)

Expand Down
81 changes: 81 additions & 0 deletions docs/plans/impact-inpath-homonyms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# PR 2 — impact `inPath` homonym scoping

> **Status:** open (not started) · **PR:** 2 of 3 · **Effort:** S–M
>
> **Orchestrator:** [`security-hardening-orchestrator.md`](./security-hardening-orchestrator.md)
>
> **Motivator:** `findImpact` resolves homonym symbols but walks call graph by name only — wrong blast-radius. Align with shipped `define_in` (#165) and existing `show`/`trace` `inPath` patterns. Moat B substrate fidelity.

---

## Agent start here

**Blocked until PR 1 merges.**

### Key touchpoints

| File | What |
| -------------------------------------- | ---------------------------------------------------------- |
| `src/application/impact-engine.ts` | `inPath` on `FindImpactOpts`, per-file walks, `scopeFiles` |
| `src/cli/cmd-impact.ts` | `--in <path>` flag |
| `src/cli/cmd-composers.ts` | MCP/CLI composer wiring |
| `src/application/tool-handlers.ts` | HTTP/MCP `impact` handler |
| `src/application/mcp-server.ts` | Tool schema `in` param |
| `src/application/trace-engine.test.ts` | Homonym test patterns to mirror |

### Architecture

```text
findImpact({ target, inPath? })
→ resolveTarget → matched_in[]
→ if inPath set and ∉ matched_in → empty + skip reason
→ if homonym (|matched_in| > 1) and no inPath → walk per defining file, merge/dedup
→ walkCalls: scopeFiles filters call-site file_path
```

---

## Task list

| ID | Task | Status | Verify |
| --- | ---------------------------------------------------- | ------- | ------------------------------------------------ |
| 4.1 | `inPath?: string` on `FindImpactOpts` / `findImpact` | pending | `bun test src/application/impact-engine.test.ts` |
| 4.2 | Multi `matched_in` → per-file walks; merge/dedup | pending | homonym fixture |
| 4.3 | `inPath` ∉ `matched_in` → empty + skip reason | pending | test |
| 4.4 | Walkers: `scopeFiles` on call-site file | pending | test |
| 4.5 | CLI `codemap impact --in <path>` | pending | `bun test src/cli/cmd-impact.test.ts` |
| 4.6 | MCP/HTTP `impact` `in` param | pending | MCP tests |
| 4.7 | Doc lift (architecture § impact) | pending | format check |
| 4.s | Commit + PR + CI | pending | `bun run check` |

---

## Pre-locked decisions

| # | Decision |
| ---- | ------------------------------------------------------------------------------------------------------- |
| P2.1 | `inPath` semantics match `show-engine` prefix/exact rules (not `define_in` — that's write-side anchor). |
| P2.2 | Unscoped homonym → union per-file walks, not silent name-level merge. |
| P2.3 | Moat A safe — still composable graph envelope, not a verdict primitive. |

---

## Acceptance

- [ ] Homonym: unscoped walk unions per-defining-file graphs
- [ ] `inPath` outside `matched_in` → empty matches + skip reason
- [ ] CLI `--in` and MCP `in` wired
- [ ] PR merged to `main`

### Verify

```bash
bun test src/application/impact-engine.test.ts src/cli/cmd-impact.test.ts
bun run check
```

---

## Lifecycle

**Close when:** PR merged. Delete this file; lift to `docs/architecture.md` § impact; update orchestrator session log.
75 changes: 75 additions & 0 deletions docs/plans/runtime-test-isolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# PR 3 — runtime guards & test isolation

> **Status:** open (not started) · **PR:** 3 of 3 · **Effort:** S–M
>
> **Orchestrator:** [`security-hardening-orchestrator.md`](./security-hardening-orchestrator.md)
>
> **Motivator:** Codify one-root-per-process constraint; stop silent `initCodemap` root bleed in tests; fail-fast invalid config at load. Maintainer-heavy; small user-visible API change (`createCodemap` second root throws).

---

## Agent start here

**Blocked until PR 1 merges** (PR 2 optional beforehand).

### Key touchpoints

| File | What |
| ----------------------------------- | --------------------------------------------------- |
| `src/runtime-swap.ts` | Audit worktree root bracket (new) |
| `src/runtime.ts` | Throw on root switch |
| `src/resolver.ts` | Resolver reset / guard |
| `src/test-helpers/runtime-reset.ts` | `resetCodemapForTest`, `installCodemapTestTeardown` |
| `src/application/audit-engine.ts` | `makeWorktreeReindex` bracket |
| `src/config.ts` / `state-config.ts` | `loadUserConfig` validation |
| `src/api.ts` | Doc: throws vs last-wins |

### Suites needing teardown rollout (grep `initCodemap`)

`churn-ingest.test.ts`, `context-engine.test.ts`, `trace-engine.test.ts`, `worker-pool.dist.test.ts`, `cmd-affected` tests, `recipe-recency.test.ts`, `benchmark-config.test.ts`, `agents-init.test.ts`, … — complete list in PR diff.

---

## Task list

| ID | Task | Status | Verify |
| --- | -------------------------------------------------------- | ------- | ------------------------------ |
| 5.1 | `runtime-swap.ts` + audit worktree bracket | pending | `bun test src/runtime.test.ts` |
| 5.2 | `initCodemap` / `configureResolver` throw on root switch | pending | runtime tests |
| 5.3 | `resetCodemapForTest` + `installCodemapTestTeardown` | pending | — |
| 5.4 | Teardown rollout on `initCodemap` test suites | pending | affected `*.test.ts` |
| 5.5 | `loadUserConfig` → `parseCodemapUserConfig` at load | pending | `bun test src/config.test.ts` |
| 5.6 | `api.ts` + architecture: throws-on-root-switch | pending | — |
| 5.s | Commit + PR + CI | pending | `bun run check` |

---

## Pre-locked decisions

| # | Decision |
| ---- | ---------------------------------------------------------------------------------- |
| P3.1 | Audit `--base` worktree reindex is the **only** exempt root switch (swap bracket). |
| P3.2 | `createCodemap({ root: B })` after root A **throws** — document breaking tighten. |
| P3.3 | Teardown helper is maintainer-only; not a consumer surface. |

---

## Acceptance

- [ ] Second `initCodemap` with different root throws (audit exempt)
- [ ] Invalid explicit config fails at `loadUserConfig`
- [ ] Teardown on all `initCodemap` suites touched in PR
- [ ] PR merged to `main`

### Verify

```bash
bun test src/runtime.test.ts src/config.test.ts
bun run check
```

---

## Lifecycle

**Close when:** PR merged. Delete this file; lift to `docs/architecture.md`; update orchestrator session log.
Loading
Loading