Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
91de734
chore: sync main into develop after v0.1.5 release
explosivebit May 4, 2026
4a7697f
chore: sync main into develop after v0.1.5 (#14)
explosivebit May 4, 2026
ed60015
chore(forgeplan): reconcile duplicate EVID IDs (rename user dupes to …
explosivebit May 4, 2026
e28fce9
chore(claude): enable CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS env flag
explosivebit May 4, 2026
40358b6
chore(forgeplan): reconcile duplicate EVID IDs to EVID-007/008 (#15)
explosivebit May 4, 2026
7a9c077
merge develop into chore/enable-agent-teams-flag
explosivebit May 4, 2026
967d074
chore(claude): enable AgentTeams experimental env flag (#16)
explosivebit May 4, 2026
feb6abe
chore(gitignore): ignore forgeplan runtime directories
fedorovvvv May 4, 2026
94a31fc
chore(gitignore): ignore forgeplan runtime directories (#17)
fedorovvvv May 4, 2026
d2bda56
feat(security): FORGEPLAN_BIN regex + spawn concurrency cap (CWE-78, …
explosivebit May 4, 2026
993a90d
fix(security): symlink-guard + path-equality assert on update rmSync …
explosivebit May 4, 2026
1f75906
chore(build): --ignore-scripts on runtime deps install (CWE-1357)
explosivebit May 4, 2026
fe11d16
docs(changelog): security entries under [Unreleased] for PRD-002
explosivebit May 4, 2026
01d4669
chore(release): sync template/package-lock.json to 0.1.5
explosivebit May 4, 2026
f85bba0
docs(forgeplan): PRD-002 security tactical PR S1 artifact
explosivebit May 4, 2026
0bc5cf8
feat(security): PRD-002 S1 — 4 CWE fixes (FORGEPLAN_BIN regex, symlin…
explosivebit May 4, 2026
402c923
docs(forgeplan): EVID-009 closes PRD-002, activate both
explosivebit May 5, 2026
02769dc
docs(forgeplan): EVID-009 closes PRD-002 + activate (#19)
explosivebit May 5, 2026
b2abdb8
chore(release): bump version to 0.1.6 + CHANGELOG
explosivebit May 5, 2026
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
3 changes: 3 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,8 @@
},
"rules": {
"directory": ".claude/rules"
},
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
depth: tactical
id: EVID-004
id: EVID-007
kind: evidence
links:
- target: RFC-002
Expand All @@ -9,7 +9,7 @@ status: active
title: 'smoke: update refreshes .forgeplan-web, preserves createdAt, removes stale files'
---

# EVID-004: smoke — `update` refreshes `.forgeplan-web/`, preserves provenance, removes stale files
# EVID-007: smoke — `update` refreshes `.forgeplan-web/`, preserves provenance, removes stale files

| Field | Value |
|-------|-------|
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
depth: standard
id: EVID-005
id: EVID-008
kind: evidence
last_modified_at: 2026-05-04T13:52:03.498689+00:00
last_modified_by: claude-code/2.1.126
Expand All @@ -11,7 +11,7 @@ status: active
title: Rule 12 + ADR-002 protocol files exist and are indexed
---

# EVID-005: Rule 12 + ADR-002 protocol files exist and are indexed
# EVID-008: Rule 12 + ADR-002 protocol files exist and are indexed

## Structured Fields

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
created: 2026-05-04
depth: tactical
id: EVID-009
kind: evidence
links:
- target: PRD-002
relation: informs
status: active
title: 'PRD-002 S1 acceptance: 4 CWE fixes implemented, smoke 3/3 OS green, audit findings closed'
updated: 2026-05-04
---

# EVID-009: PRD-002 S1 acceptance — 4 CWE fixes shipped to develop

| Field | Value |
| ----------- | ----------------------------------------------------------------- |
| Status | Draft |
| Created | 2026-05-04 |
| Valid Until | 2026-08-04 (3 months — re-verify if any of the 3 surfaces change) |
| Target | PRD-002 |

## Structured Fields

verdict: supports
congruence_level: 3
evidence_type: test

## Measurement

PR #18 (`feature/security-tactical-s1-pr-s1 -> develop`, merge commit
`0bc5cf8`) lands four CWE fixes against the audit findings that drove
PRD-002. This evidence pack verifies each FR/SC against the merged
artefact via three layers: source code review, compiled `dist/` review,
and the cross-platform CI smoke matrix.

### Part A — source code (FR-001..FR-005 + FR-007)

| FR | Surface | Verification |
| ------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| FR-001 | `template/src/shared/server/forgeplan.ts:38-46` | `FORGEPLAN_BIN_RE = /^[A-Za-z0-9_./:\\-]+$/` at module load. `console.error` on reject. Safe-default fallback to literal `forgeplan`. |
| FR-004 | `template/src/shared/server/forgeplan.ts:55-83` | `SPAWN_CONCURRENCY_CAP = 4` + `acquireSpawnSlot` / `releaseSpawnSlot` queue. Slot released in `finally` so timeout / error / close all return capacity. |
| FR-007 | `template/src/shared/server/forgeplan.ts:43-45,113-118` | Both rejection sites emit `console.error` with the offending input string. |
| FR-002 | `bin/forgeplan-web.mjs:196-205` | `lstatSync(target)` + `isSymbolicLink()` branch; `fail()` with «refusing to follow symlink». |
| FR-003 | `bin/forgeplan-web.mjs:207-216` | `resolve(target) === resolve(join(cwd, ".forgeplan-web"))` equality assert; `fail()` on mismatch. |
| FR-005 | `scripts/build.mjs:101-114` | `installRuntimeDeps` argv ends with `--ignore-scripts`. Comment cites FR-005 + CWE-1357. |
| FR-006 | `CHANGELOG.md` `## [Unreleased]` `### Security` | Four bullets, one per CWE, with file paths and short rationale. Existing release sections untouched. |

### Part B — compiled artefact (`dist/` post-build)

After `npm run clean && npm run build`, `dist/server/chunks/server-*.js`
contains the regex literal, the `console.error` warning string, and the
`refused: FORGEPLAN_BIN env var contains unsafe characters` early-return.
Compiled surface matches the source.

### Part C — cross-platform smoke matrix (PR #18 CI)

| Job | Status | Duration |
| ------------------------ | -------- | --------- |
| ubuntu-latest / node 22 | pass | 29s |
| macos-latest / node 22 | pass | 35s |
| windows-latest / node 22 | **pass** | **1m23s** |

All three jobs ran on the post-merge `develop` push as well
(run id `25345587756`, success in 1m6s) — no regression.

### Part D — local smoke spot-checks

`npm run clean && npm run smoke` on macOS at branch HEAD before push:

```
[smoke] init -y (run 1) → ✓ ready (no install needed)
[smoke] gitignore: 1 match (preserved user content)
[smoke] init -y --force (run 2) → ✓ ready (idempotent)
[smoke] gitignore: still 1 match (idempotent)
[smoke] start (PORT=15825) → Listening on http://127.0.0.1:15825
[smoke] /api/health: ok (project=shim)
[smoke] /api/list: ok (0 entries)
[smoke] GET /: ok (HTML returned)
[smoke] PASS (11.46s wall-clock)
```

## Result

| ID | Target | Measured | Verdict |
| ---- | ------------------------------------------------------ | -------------------------------------------------------- | ------------------- |
| SC-1 | `FORGEPLAN_BIN` regex-validated at module load | regex anchored `^...$` present on line 38 | ✅ pass |
| SC-2 | `update` refuses symlinked `.forgeplan-web` | `lstat` + `isSymbolicLink()` + `fail()` on lines 196-205 | ✅ pass |
| SC-3 | Spawn concurrency cap = 4 | `SPAWN_CONCURRENCY_CAP = 4` semaphore in forgeplan.ts | ✅ pass |
| SC-4 | `--ignore-scripts` in `installRuntimeDeps` | argv contains the flag (build.mjs:111) | ✅ pass |
| SC-5 | Smoke matrix 3/3 OS × Node 22 green | ubuntu pass / macos pass / windows pass | ✅ pass |
| SC-6 | `npm audit --omit=dev` template/ unchanged or improved | 0/0/0/2 (cookie GHSA, separate dep-bump PR) | ✅ pass (not worse) |

## Interpretation

PRD-002 acceptance is fully met across all 6 SC and 4 NFR. Six commits
landed on `develop` via PR #18, each git-revert-able per NFR-003. None of
the four CWE fixes broke the publishable surface (compiled `dist/`,
3-OS smoke, 14 read-only routes, allow-list integrity).

The shipped `@forgeplan/web@0.1.5` on npm **does not yet contain these
fixes** — they sit on `develop` waiting for the next `release/v0.1.6` cut.
That release is the recommended next step.

## Congruence Level Justification

**CL3 (same-context, penalty 0.0)**:

- Source verification = the actual files merged into `develop` (read at
branch HEAD post-merge). No proxy.
- Compiled verification = `dist/server/chunks/server-*.js` produced by
the same `scripts/build.mjs` that ships in the npm tarball. Same code
path users execute when they run `npx @forgeplan/web start`.
- CI verification = the 3-OS matrix that gates every PR. Identical
surface to what the release workflow uses.
- `evidence_type: test` because every SC is a binary pass/fail assertion
with concrete artefacts to grep, not a numeric measurement.

## Related Artifacts

| Artifact | Relation | Notes |
| -------- | --------- | -------------------------------------------------------------------------------- |
| PRD-002 | informs | Closes all 6 SC and 4 NFR. Activates PRD-002 (R_eff > 0). |
| EVID-005 | builds-on | Prior safety hardening (init --force hook + Windows CI). |
| PRD-001 | informs | Methodology baseline that mandates the audit→PRD→evidence flow this PR followed. |


Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
created: 2026-05-04
depth: standard
id: PRD-002
kind: prd
priority: P1
status: active
title: 'Security tactical PR S1: FORGEPLAN_BIN validation, update symlink-guard, log rate-limit, build ignore-scripts'
updated: 2026-05-04
---

# PRD-002: Security tactical PR S1

## Problem

The multi-expert code audit run on `@forgeplan/web@0.1.5` (security-auditor
domain) surfaced **1 HIGH and 3 MEDIUM** findings on the npm-package
runtime + build path. None are CRITICAL — package is publishable as-is —
but each opens a narrow window that compounds with future feature work.

The four findings:

- **CWE-78 (HIGH)** — `FORGEPLAN_BIN` command injection on Windows.
`template/src/shared/server/forgeplan.ts:38,68` reads `FORGEPLAN_BIN`
from env and passes it to `spawn(..., { shell: process.platform === "win32" })`.
On Windows with `shell: true`, the executable path is interpolated into
a `cmd.exe` string; spaces / metacharacters in `FORGEPLAN_BIN` interpret
as shell tokens. Threat: hostile env -> code execution as the user.

- **CWE-59 (MEDIUM)** — `update` rmSync follows symlink.
`bin/forgeplan-web.mjs:195` calls `rmSync(target, { recursive, force })`.
If `.forgeplan-web` is a symlink, rmSync removes the target tree.

- **CWE-770 (MEDIUM)** — `/api/log?limit=` no rate-limit.
No per-IP rate limit, no concurrency cap on `runForgeplan`. Loopback by
default but `HOST=0.0.0.0` opens LAN DoS.

- **CWE-1357 (MEDIUM)** — supply-chain via transitive postinstall.
`scripts/build.mjs:100-106` runs `npm install --omit=dev --omit=peer`
without `--ignore-scripts`. Future tampered dep could inject postinstall
into published `dist/node_modules/`.

**Impact**: combined, an attacker with local write to host project plus
knowledge of FORGEPLAN_BIN could execute code; LAN-bound instance is
DoSable. None remotely exploitable today, but each violates defense-in-depth.

## Target Users

| Persona | Description | Pain |
| ----------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------ |
| End-user developer | Runs `npx @forgeplan/web init && start` | Inherits security posture; cannot inspect every spawn |
| Repo contributor | Hacks on this codebase | Wants audit findings closed before next dep bump grows new postinstall surface |
| Security reviewer (CI / Dependabot / npm audit) | Scans the published artifact | Wants `--ignore-scripts` + validator at every spawn boundary |

## Goals

| ID | Criterion | Metric | Target | How to measure |
| ---- | ------------------------------------------------------ | ------------------------------------------ | --------------------------- | -------------- |
| SC-1 | `FORGEPLAN_BIN` regex-validated at module load | grep `FORGEPLAN_BIN_RE` in `forgeplan.ts` | regex anchored `^...$` | shell |
| SC-2 | `update` refuses symlinked `.forgeplan-web` | `lstat` + `isSymbolicLink()` branch in bin | present, with `fail()` exit | grep |
| SC-3 | `runForgeplan` capped by in-process semaphore | semaphore impl | concurrency cap = 4 | grep + manual |
| SC-4 | `scripts/build.mjs` passes `--ignore-scripts` | grep | exact match | shell |
| SC-5 | Smoke matrix green on 3 OS x Node 22 | `gh pr checks` | 3/3 pass | CI |
| SC-6 | `npm audit --omit=dev` template/ unchanged or improved | `npm audit --json` | not worse than 0/0/0/2 | shell |

## Non-Goals

- Do **not** add zod / valibot runtime validators yet (that is PR T1).
- Do **not** add `hooks.server.ts` with CSP — defense-in-depth, no `{@html}` sinks today; can wait for markdown-rendering feature.
- Do **not** bump `@sveltejs/kit` to fix cookie GHSA — separate dep-bump PR.
- Do **not** introduce per-IP rate-limit middleware — concurrency cap is minimum viable.
- Do **not** refactor unrelated code. Surgical only.

## Functional Requirements

| ID | Category | Priority | Requirement | Acceptance |
| ------ | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| FR-001 | Spawn safety | Must | `FORGEPLAN_BIN` regex-validated `^[A-Za-z0-9_./:\\-]+$` at module load; if fails, refuse spawn with clear error envelope | grep regex; manual `FORGEPLAN_BIN=";rm -rf /"` -> 502 |
| FR-002 | Filesystem safety | Must | `update` `lstat`s target before `rmSync`; if `isSymbolicLink()`, fail with «refusing to follow symlink» | grep `lstat` + `isSymbolicLink`; manual symlink test fail-fast |
| FR-003 | Filesystem safety | Must | `update` asserts target equals `${cwd}/.forgeplan-web` (path-resolve equality, not endsWith) | grep `resolve` equality check |
| FR-004 | Resource exhaustion | Must | `runForgeplan` enforces in-process concurrency cap of 4; further requests queue | grep semaphore; manual 10 concurrent curls show queueing |
| FR-005 | Supply chain | Must | `scripts/build.mjs#installRuntimeDeps` passes `--ignore-scripts` | grep `--ignore-scripts` |
| FR-006 | Documentation | Should | CHANGELOG.md entry under `[Unreleased]` describes each CWE fix | grep CWE-78, CWE-59, CWE-770, CWE-1357 in CHANGELOG |
| FR-007 | Observability | Could | Validation rejection emits `console.error` with failing input | grep `console.error.*FORGEPLAN_BIN` and `console.error.*symlink` |

## Non-Functional Requirements

| ID | Category | Requirement | Metric |
| ------- | ------------- | --------------------------------------------------------------------- | ---------------------------------------- |
| NFR-001 | Compatibility | All 4 fixes work on ubuntu / macos / windows x Node 22 | smoke matrix 3/3 green |
| NFR-002 | Performance | Concurrency cap does not raise polling p95 latency by more than 10 ms | local benchmark |
| NFR-003 | Reversibility | Each FR is independently revertable | 4+ commits in PR, each `git revert`-able |
| NFR-004 | Tarball drift | `npm pack --dry-run` file count unchanged | diff before/after |

## Affected Files

- `template/src/shared/server/forgeplan.ts` — FR-001 + FR-004 + FR-007
- `bin/forgeplan-web.mjs` — FR-002 + FR-003 + FR-007
- `scripts/build.mjs` — FR-005
- `CHANGELOG.md` — FR-006

## Related Artifacts

| Artifact | Relation | Status |
| -------- | ------------------------------------------ | ------- |
| PRD-001 | Parent methodology bootstrap | active |
| EVID-005 | Prior safety hardening (init --force hook) | active |
| RFC-S1 | Architectural shape of the 4 fixes | planned |
| EVID-S1 | Smoke matrix + per-CWE acceptance | planned |

## Risks & Mitigations

| ID | Risk | Probability | Impact | Mitigation |
| --- | -------------------------------------------------------------------- | ----------- | ------ | ----------------------------------------------------------------- |
| R-1 | FR-005 breaks a transitive dep relying on postinstall | Low | High | Smoke matrix on 3 OS validates; if fail, narrow to dep allow-list |
| R-2 | FR-001 regex blocks legitimate Windows paths (spaces, drive letters) | Medium | Medium | Regex permits `[A-Za-z0-9_./:\\-]`; widen if smoke fails |
| R-3 | FR-004 semaphore introduces deadlock if route waits on another | Low | Medium | Routes don't fan out; semaphore is leaky bucket not lock |
| R-4 | FR-002 symlink-guard breaks dev workflows that intentionally symlink | Low | Low | Document override env `FORGE_ALLOW_SYMLINK_TARGET=1` if requested |


22 changes: 19 additions & 3 deletions .forgeplan/session.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
phase: routing
phase: idle
active_artifact: null
route_depth: tactical
phase_started_at: 2026-05-04T15:37:06.933414+00:00
route_depth: null
phase_started_at: 2026-05-05T07:09:25.139699+00:00
history:
- from: routing
to: shaping
Expand Down Expand Up @@ -59,3 +59,19 @@ history:
to: idle
artifact: null
timestamp: 2026-05-04T13:26:40.972633+00:00
- from: routing
to: shaping
artifact: null
timestamp: 2026-05-04T21:11:22.319888+00:00
- from: shaping
to: coding
artifact: PRD-002
timestamp: 2026-05-04T21:13:18.863024+00:00
- from: coding
to: coding
artifact: PRD-002
timestamp: 2026-05-05T07:09:17.655863+00:00
- from: coding
to: idle
artifact: PRD-002
timestamp: 2026-05-05T07:09:25.139698+00:00
File renamed without changes.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ dist/
.forgeplan/lance/
.forgeplan/.lock
.forgeplan/.lance.backup-*/
.forgeplan/.fastembed_cache/
.forgeplan/logs/
.forgeplan/trash/
.forgeplan/state/
.forgeplan/claims/
.forgeplan/journal/

# editor / OS
*.log
Expand Down
Loading