Skip to content

Commit 80e5c7f

Browse files
authored
Merge pull request #126 from jsilvanus/claude/repo-review-ph9le2
docs: add strategic review11 (multi-tenant auth, public-repo sharing, locked-model-set, interface parity)
2 parents 130cb18 + ee6a338 commit 80e5c7f

3 files changed

Lines changed: 338 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The latest review is here. Do not edit the review file, but update that file wit
1414

1515
| Document | Purpose |
1616
|---|---|
17-
| [`docs/review10.md`](docs/review10.md) | Latest strategic review: knowledge-graph completion (Phases 105–112), LSP/MCP network transports (Phases 113–117), review9 close-out verification |
17+
| [`docs/review11.md`](docs/review11.md) | Latest strategic review: Multi-Tenant Auth (Phases 122–125), Public Repo Sharing (126–127), Locked-Model-Set/BYOK (128–130), the Interface Parity Track (131–148), and review10 close-out verification |
1818

1919
When implementing a new feature or phase:
2020
1. Add the feature to **`docs/features.md`** under the relevant group.

docs/PLAN.md

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6078,6 +6078,209 @@ rather than continuing to carry it forward.
60786078

60796079
---
60806080

6081+
## review11 Security Close-Out Track (Phases 150–152)
6082+
6083+
> **Design:** no separate design doc — scoped directly from the three
6084+
> findings in [`docs/review11.md`](review11.md). Each phase closes one
6085+
> finding (with the review's lower-severity hygiene items folded into the
6086+
> phase that shares their root cause). Open design forks were resolved with
6087+
> the user before scheduling; those decisions are recorded inline per phase.
6088+
> The phases are independent and can ship in any order, but the priority
6089+
> order from review11 §6 is 150 → 151 → 152.
6090+
6091+
| Phase | Finding | Title | Deliverable |
6092+
|---|---|---|---|
6093+
| **150** | review11 §2.1 + §3.2 | Git argument-injection close-out | Reject leading-`-` refs / `--`-separate positionals at every `git` call site that takes a user-influenced ref, closing the PoC-confirmed arbitrary-file-overwrite reachable via `semantic_bisect`/`triage`, and applying the same hygiene systemically. |
6094+
| **151** | review11 §2.2 | Read-route repo authorization gate | Opt-in multi-tenant enforcement: a post-`repoSessionMiddleware` check requiring the resolved user to hold a `read` grant on the named `repoId` (or the repo to be `public`), closing "read any private repo by ID." Repo-level only; per-branch filtering deferred. |
6095+
| **152** | review11 §3.1 + §3.3 | BYOK SSRF guard + list-tool bounds | Block private/loopback/link-local ranges for BYOK endpoints by default (operator allowlist to re-permit internal hosts), and upper-bound every newly-network-exposed list tool's `top_k`/`limit`. |
6096+
6097+
---
6098+
6099+
### Phase 150 — Git argument-injection close-out (review11 §2.1 + §3.2)
6100+
6101+
**Design:** no separate design doc — scoped directly from review11 §2.1
6102+
(PoC-confirmed) and §3.2. Root cause: the review9/10 injection fixes switched
6103+
git calls from `execSync(\`git … ${x}\`)` (shell) to `execFileSync('git',
6104+
[...])` (no shell). That defeats shell metacharacters but **not** git's own
6105+
option parsing — a ref beginning with `-` is still parsed as a git *flag*.
6106+
`git log --output=<file>` (and similar) then becomes an arbitrary-file-write
6107+
primitive. **Approach:** validate the ref at the sink (reuse the existing
6108+
`isSafeGitRange()` allowlist from `src/core/narrator/narrator.ts`, which
6109+
already rejects leading `-` and shell metacharacters) **and** interpose a
6110+
`--` end-of-options separator before positional refs so a value can never be
6111+
read as a flag — belt and suspenders, applied uniformly rather than
6112+
per-site.
6113+
6114+
**Goal:** Close the network-reachable git argument-injection sink
6115+
(arbitrary file overwrite via `semantic_bisect`/`triage`) and eliminate the
6116+
same missing-`--` hygiene gap at every other git call site that passes a
6117+
user-influenced ref, so this injection *class* stops recurring as new git
6118+
call sites or network entry points are added.
6119+
6120+
**Scope:**
6121+
1. Add ref validation to `resolveRefToTimestamp()`
6122+
(`src/core/search/clustering/clustering.ts:574`) — reject any `ref`
6123+
matching `/^-/` (or failing `isSafeGitRange()`) before the `git log`
6124+
call, and pass `--` before the positional ref. This is the confirmed
6125+
HIGH sink reached from `computeSemanticBisect`
6126+
(`src/core/search/semanticBisect.ts:97-98`, currently unvalidated) via
6127+
the `semantic_bisect` MCP tool (`src/mcp/tools/insights.ts:42-43`) and
6128+
the HTTP `triage` bundle (`src/server/routes/analysis.ts:764`).
6129+
2. Apply the same fix to the lower-impact §3.2 sites:
6130+
`src/core/search/temporal/timeSearch.ts:166,168`
6131+
(`git rev-parse --verify` / `git show`) and
6132+
`src/core/git/branchDiff.ts:13` (`git merge-base`).
6133+
3. Introduce a shared `runGit(subcommand, flags, refs)` helper (or extend an
6134+
existing git util) that always `--`-separates refs and rejects leading-`-`
6135+
ref values, and route the above call sites through it so the safe form is
6136+
the default for future call sites. `isSafeGitRange()` should be lifted to
6137+
a shared location (e.g. `src/core/git/`) if it currently lives only in
6138+
`narrator.ts`, so both narrator and these sites share one allowlist.
6139+
4. Add regression tests: a unit test asserting `resolveRefToTimestamp('--output=…')`
6140+
(and the other sites) throws/rejects rather than shelling out, plus an
6141+
integration test driving `semantic_bisect`/`triage` with a `--output=`
6142+
ref and asserting no file is written.
6143+
6144+
**Acceptance criteria:**
6145+
- `semantic_bisect` / `triage` called with `good_ref`/`bad_ref` beginning
6146+
with `-` returns an error and writes no file (PoC no longer reproduces).
6147+
- Every `execFileSync('git', …)` site that takes a user-influenced ref
6148+
either validates the ref or `--`-separates it (ideally both), verified by
6149+
grep/test.
6150+
- `pnpm build && pnpm test` clean; new regression tests pass.
6151+
6152+
**Files (anticipated):** `src/core/search/clustering/clustering.ts`,
6153+
`src/core/search/semanticBisect.ts`, `src/core/search/temporal/timeSearch.ts`,
6154+
`src/core/git/branchDiff.ts`, a shared git helper under `src/core/git/`,
6155+
`src/core/narrator/narrator.ts` (if `isSafeGitRange` is relocated), tests,
6156+
`.changeset/`.
6157+
6158+
---
6159+
6160+
### Phase 151 — Read-route repo authorization gate (review11 §2.2)
6161+
6162+
**Design:** no separate design doc — scoped directly from review11 §2.2
6163+
(disclosed in PLAN.md as "Phase 123 deviation #1"). The Multi-Tenant Auth
6164+
Track built `resolveUserRepoAccess`/`roleSatisfies` and the `repo_grants`
6165+
model, but they are called only from `remote.ts`/`orgs.ts` — none of the
6166+
~16 read/search/analysis/evolution/graph/insights routes enforce them, and
6167+
`repoSessionMiddleware` serves any named `repoId` with no grant check.
6168+
**Resolved design decisions (with the user):**
6169+
- **Scope:** repo-level gate first — enforce "user holds a `read` grant on
6170+
this `repoId`, or the repo is `public`." The larger `branch: string →
6171+
string[]` per-branch filter plumbing is **deferred** to a follow-on phase
6172+
(see "Deferred" below), since it is orthogonal to closing the
6173+
private-repo-read hole.
6174+
- **Enforcement mode:** opt-in multi-tenant posture — enforcement activates
6175+
only when the server is multi-tenant (e.g. `GITSEMA_SERVE_KEY` set, or an
6176+
explicit `GITSEMA_MULTI_TENANT` flag; exact trigger to be finalized in
6177+
implementation). A default open single-dev server (no key, no flag) stays
6178+
unchanged, so this is not a breaking change for existing local
6179+
deployments.
6180+
6181+
**Goal:** Make the authorization model the auth track already ships actually
6182+
gate the routes that return repo content, so that in a multi-tenant posture
6183+
a caller can only read repos they hold a grant on (or public repos) — closing
6184+
"any authenticated user (or anyone, on an open server) can read any repo by
6185+
naming its `repoId`."
6186+
6187+
**Scope:**
6188+
1. Add an authorization middleware that runs after `repoSessionMiddleware`
6189+
(`src/server/middleware/repoSession.ts`): when the server is in
6190+
multi-tenant mode and a `repoId` was resolved, require
6191+
`roleSatisfies(resolveUserRepoAccess(req.userId, repoId), 'read')` unless
6192+
the repo's `visibility === 'public'` (Phase 126 column). Reject with 403
6193+
otherwise. When not in multi-tenant mode, pass through unchanged.
6194+
2. Mount it on the ~16 data routes currently behind `repoSessionMiddleware`
6195+
(search, analysis, evolution, graph, insights). Legacy per-repo
6196+
`repo_tokens` (which set `req.repoId`) continue to work as today — a
6197+
scoped token already implies access to its one repo.
6198+
3. Decide and document the multi-tenant trigger (reuse `GITSEMA_SERVE_KEY`
6199+
presence, or add `GITSEMA_MULTI_TENANT`/`auth.multiTenant`) in
6200+
`CLAUDE.md`'s Configuration table.
6201+
4. Tests: authorized user reads a granted repo (200); un-granted user reads
6202+
a private repo (403); any caller reads a public repo (200); open-server
6203+
mode still serves without a grant (behavior preserved).
6204+
6205+
**Acceptance criteria:**
6206+
- In multi-tenant mode, a request naming a `repoId` the caller has no `read`
6207+
grant on (and that isn't public) gets 403 from a data route.
6208+
- Open single-dev server (no key/flag) behavior is unchanged — no new 403s.
6209+
- `resolveUserRepoAccess` is now referenced by the request pipeline, not
6210+
just `remote.ts`/`orgs.ts`.
6211+
- `pnpm build && pnpm test` clean.
6212+
6213+
**Deferred (explicitly out of scope for this phase):** per-branch result
6214+
filtering — rewriting the routes' single `branch: string` param into a
6215+
per-user granted-branch-set (`branch: string[]`) filter threaded through
6216+
`vectorSearch`/`analysis` (PLAN.md's "single biggest implementation-surface
6217+
item"). Should be scheduled as its own follow-on phase once the repo-level
6218+
gate lands.
6219+
6220+
**Files (anticipated):** new middleware under `src/server/middleware/`,
6221+
`src/server/app.ts` (mounting), `src/core/auth/grants.ts` (if a
6222+
repo-level helper is added), `CLAUDE.md` (config docs), tests,
6223+
`.changeset/`.
6224+
6225+
---
6226+
6227+
### Phase 152 — BYOK SSRF guard + newly-exposed list-tool bounds (review11 §3.1 + §3.3)
6228+
6229+
**Design:** no separate design doc — scoped directly from review11 §3.1 and
6230+
§3.3. §3.1: Phase 130's BYOK uses a request-supplied `byok.http_url`
6231+
verbatim as the endpoint the *server* calls, on network-facing
6232+
`narrate`/`explain`/`guide` routes, with no scheme check, allowlist, or
6233+
loopback/private-range block — an SSRF vector on a shared server.
6234+
**Resolved design decision (with the user):** default-**block** private,
6235+
loopback, and link-local ranges out of the box; operators re-permit internal
6236+
hosts (e.g. a local model server) via an explicit allowlist
6237+
(`GITSEMA_BYOK_ALLOW_HOSTS`). This is a behavior change for anyone currently
6238+
pointing BYOK at `localhost`/private IPs — call it out in the changeset and
6239+
docs. §3.3: the Phase 147/148 exposure added several list-returning tools;
6240+
confirm each has an *upper* bound, not just a positive-int check.
6241+
6242+
**Goal:** Prevent BYOK from being used to make the server issue requests to
6243+
internal/metadata endpoints by default, while leaving a documented path for
6244+
legitimate internal model servers, and ensure no newly-network-exposed list
6245+
tool can be asked to serialize an unbounded result set.
6246+
6247+
**Scope:**
6248+
1. Validate `byok.http_url` before constructing the provider in
6249+
`resolveNarratorProvider`/`resolveGuideConfig`
6250+
(`src/core/narrator/resolveNarrator.ts:188-198`): require `http(s)`
6251+
scheme; resolve the host and reject loopback (`127.0.0.0/8`, `::1`),
6252+
link-local (`169.254.0.0/16`, `fe80::/10`, incl. the cloud metadata IP),
6253+
and RFC-1918 private ranges — unless the host matches
6254+
`GITSEMA_BYOK_ALLOW_HOSTS` (comma-separated host/CIDR allowlist,
6255+
default empty). Reject with a clear error on the HTTP routes.
6256+
2. Audit every Phase 147/148-exposed list tool (`refactor_candidates`,
6257+
`graph_relate`, `graph_similar`, `blast_radius`, `deps`, `co_change`,
6258+
and any others surfaced in that track) for an upper bound on
6259+
`top_k`/`limit`; add `.max(<n>)` to the Zod schema (HTTP + MCP) wherever
6260+
only `.positive()` exists today, matching the `hotspots.topK` `.max(500)`
6261+
precedent (review10 §2.1).
6262+
3. Docs: note the BYOK SSRF behavior and `GITSEMA_BYOK_ALLOW_HOSTS` next to
6263+
the `--byok-*` flags (`README.md`/`docs/features.md`) and in `CLAUDE.md`'s
6264+
Configuration table.
6265+
4. Tests: BYOK with a loopback/metadata URL is rejected by default; the same
6266+
URL is permitted when its host is in `GITSEMA_BYOK_ALLOW_HOSTS`; a list
6267+
tool called with an over-large `top_k` is clamped/rejected.
6268+
6269+
**Acceptance criteria:**
6270+
- `POST /guide/chat` (and narrate/explain) with `byok.http_url` pointing at
6271+
`http://169.254.169.254/…` or `http://127.0.0.1/…` is rejected unless the
6272+
host is allowlisted.
6273+
- Every newly-exposed list tool's `top_k`/`limit` has a documented upper
6274+
bound in its schema.
6275+
- `pnpm build && pnpm test` clean.
6276+
6277+
**Files (anticipated):** `src/core/narrator/resolveNarrator.ts` (+ a small
6278+
URL-guard helper), `src/server/routes/{narrator,guide}.ts`,
6279+
`src/mcp/tools/*.ts` and `src/server/routes/*.ts` for the list-bound schemas,
6280+
`README.md`/`docs/features.md`/`CLAUDE.md`, tests, `.changeset/`.
6281+
6282+
---
6283+
60816284
## Deployment scenarios & usage envisioning
60826285

60836286
The architecture of gitsema supports three distinct deployment scenarios, each with different operational models and target users. This section clarifies the intended usage patterns and the infrastructure requirements for each.

0 commit comments

Comments
 (0)