Commit Graph

4589 Commits

Author SHA1 Message Date
e39fc92074 fix(ci): replace gh pr CLI with Gitea v1 REST in workflows + scripts (#80)
Class A of #75 sweep. 23 bash + 9 python tests pass. Live-integration verified against prod Gitea. Approved by security-auditor.
2026-05-07 23:39:22 +00:00
1819ac21f4 Merge branch 'main' into fix/issue75-class-A-gh-pr-to-gitea-rest 2026-05-07 23:37:57 +00:00
d84d88ad70 feat(workspace-server): local-dev provisioner builds from Gitea source (#70)
Hongming-locked Option C: MOLECULE_IMAGE_REGISTRY presence as mode marker. ADR-002 captures rationale. 30 new tests + 64 existing preserved. Hostile-review weakest 3 filed as #204/#205/#206 follow-ups. Closes #63 (Task #194). Approved by security-auditor.
2026-05-07 23:37:56 +00:00
6bb272360d Merge branch 'main' into feat/issue-63-local-build-from-gitea-v2 2026-05-07 23:33:03 +00:00
1f1ead1833 fix(ci): rewrite auto-promote-staging for Gitea (#78)
Removes ~60 lines polling+dispatch (Gitea fires on:push naturally on token-merge). Uses Gitea merge_when_checks_succeed; preserves required_approvals=1 on main. Closes #73. Approved by security-auditor.
2026-05-07 23:32:58 +00:00
c5f40de585 Merge branch 'main' into fix/195-auto-promote-staging-gitea-rest 2026-05-07 23:30:09 +00:00
330a5842ab Merge pull request 'feat(canvas): ActivityTab → ACTIVITY_LOGGED subscriber (#61 stage 3, final)' (#76) from feat/canvas-activity-tab-ws-subscribe into main 2026-05-07 23:27:32 +00:00
2505b36a2c Merge branch 'main' into fix/195-auto-promote-staging-gitea-rest 2026-05-07 23:22:24 +00:00
security-auditor
e0feae18f4 Merge remote-tracking branch 'origin/main' into feat/canvas-activity-tab-ws-subscribe 2026-05-07 16:18:34 -07:00
502aa082bc Merge pull request 'feat(canvas): A2ATopologyOverlay → ACTIVITY_LOGGED subscriber (#61 stage 2)' (#71) from feat/canvas-topology-overlay-ws-subscribe into main 2026-05-07 23:18:24 +00:00
security-auditor
8f732511b1 Merge remote-tracking branch 'origin/main' into feat/canvas-activity-tab-ws-subscribe 2026-05-07 16:04:39 -07:00
security-auditor
7d0df65474 Merge remote-tracking branch 'origin/main' into feat/canvas-topology-overlay-ws-subscribe 2026-05-07 16:04:29 -07:00
33327cf077 Merge pull request 'feat(canvas): CommunicationOverlay → ACTIVITY_LOGGED subscriber (#61 stage 1)' (#69) from feat/canvas-comm-overlay-ws-subscribe into main 2026-05-07 23:04:18 +00:00
security-auditor
865a366573 Merge remote-tracking branch 'origin/main' into feat/canvas-activity-tab-ws-subscribe 2026-05-07 15:56:56 -07:00
security-auditor
b73f599184 Merge remote-tracking branch 'origin/main' into feat/canvas-topology-overlay-ws-subscribe 2026-05-07 15:56:52 -07:00
security-auditor
5855be50b4 Merge remote-tracking branch 'origin/main' into feat/canvas-comm-overlay-ws-subscribe 2026-05-07 15:56:49 -07:00
e766061800 Merge pull request 'fix(ratelimit): tenant-aware bucket keying — close canvas 429 storm (#59)' (#60) from fix/canvas-429-tenant-aware-ratelimit into main 2026-05-07 22:56:38 +00:00
security-auditor
e909417224 Merge remote-tracking branch 'origin/main' into feat/canvas-activity-tab-ws-subscribe 2026-05-07 15:54:06 -07:00
security-auditor
9bb4bbdff7 Merge remote-tracking branch 'origin/main' into feat/canvas-topology-overlay-ws-subscribe 2026-05-07 15:54:03 -07:00
security-auditor
bec1cb3786 Merge remote-tracking branch 'origin/main' into feat/canvas-comm-overlay-ws-subscribe 2026-05-07 15:54:00 -07:00
security-auditor
1d6b09f2bd Merge remote-tracking branch 'origin/main' into fix/canvas-429-tenant-aware-ratelimit 2026-05-07 15:53:57 -07:00
0be89053e8 Merge pull request 'chore(observability): edge-429 probe + ratelimit runbook (unblocks #62, #64)' (#85) from chore/edge-429-probe-and-ratelimit-runbook into main 2026-05-07 22:53:48 +00:00
9ecee78782 Merge branch 'main' into fix/issue75-class-A-gh-pr-to-gitea-rest 2026-05-07 22:53:11 +00:00
141dfdae52 Merge branch 'main' into feat/issue-63-local-build-from-gitea-v2 2026-05-07 22:53:04 +00:00
d21c09babe Merge branch 'main' into fix/195-auto-promote-staging-gitea-rest 2026-05-07 22:53:00 +00:00
security-auditor
9eb530bbf0 Merge remote-tracking branch 'origin/main' into chore/edge-429-probe-and-ratelimit-runbook 2026-05-07 15:52:29 -07:00
security-auditor
62e793040e chore(observability): edge-429 probe + ratelimit observability runbook
Two artifacts that unblock the parked follow-ups from #59:

  1. scripts/edge-429-probe.sh (closes the "operator-blocked" status of
     #62). An operator without CF/Vercel dashboard access can reproduce
     a canvas-sized burst against a tenant subdomain and read each 429's
     response shape — workspace-server bucket overflow (JSON body +
     X-RateLimit-* headers) is distinguishable from CF (cf-ray) and
     Vercel (x-vercel-id) by inspection of the report. Read-only,
     parallel via background subshells (no GNU parallel dependency),
     no credential use. Smoke-tested against example.com end-to-end.

  2. docs/engineering/ratelimit-observability.md (closes the
     "metric-blocked" status of #64). The existing
     molecule_http_requests_total{path,status} counter + X-RateLimit-*
     response headers already cover #64's acceptance criterion ("watch
     metrics for two weeks"). The runbook collects the PromQL queries,
     a decision tree for the re-tune (keep / per-tenant override /
     change default), an alert rule template, and a hard "do not roll
     ad-hoc per-bucket-key exposure" note (in-memory map includes
     SHA-256 of bearer tokens — exposing it is a security review
     surface, file a follow-up if needed).

Neither artifact changes runtime behaviour. Pure operational tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:48:34 -07:00
6946cd12c5 ci(branch-protection): check-name parity gate (#144) (#56)
Adds tools/branch-protection/check_name_parity.sh regression guard + 6 shell tests + branch-protection-drift.yml wire-up.

Closed #144. Approved by security-auditor.
2026-05-07 22:42:08 +00:00
e43bd7ceb0 chore: 2nd verification trigger for #75 class A (per Phase 4 ≥2 green runs)
Empty commit to trigger CI a second consecutive time per the SOP
'verify ≥1 representative workflow per class via workflow_dispatch
or push event ... ≥2 consecutive successful runs per class'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:41:00 -07:00
bcc72419ce Merge branch 'main' into fix/144-branch-protection-check-name-parity-audit 2026-05-07 22:35:33 +00:00
050cb035d6 fix(ci): pre-clone manifest deps in harness-replays workflow (#50)
Mirrors PR #66/#173 pre-clone-manifest pattern. Closes #173 (followup).

Approved by security-auditor.
2026-05-07 22:33:51 +00:00
e075557b19 fix(ci): replace gh pr CLI with Gitea v1 REST in workflows + scripts (#75 class A)
Part of the post-#66 sweep to remove `gh` CLI dependencies that fail
silently against Gitea (which exposes /api/v1 only — no GraphQL → 405,
no /api/v3 → 404). Class A covers `gh pr list / view / diff / comment`
shapes.

Affected:

- `.github/workflows/auto-tag-runtime.yml`
  Replaced `gh pr list --search SHA --json number,labels` with a curl
  to `/api/v1/repos/.../pulls?state=closed&sort=newest&limit=50` +
  jq filter on `merge_commit_sha == github.sha`. Same end-to-end
  behaviour: locate the merged PR for this push, read its labels,
  pick the bump kind. Defensive `?.name // empty` jq guard handles
  unlabelled PRs without erroring. The 50-PR window is comfortably
  larger than the volume of staging→main promotes that close in any
  reasonable detection window.

- `scripts/check-stale-promote-pr.sh`
  Rewrote `fetch_prs` and `post_comment` to call Gitea's REST API
  directly. Gitea doesn't expose GitHub's compound `mergeStateStatus`
  / `reviewDecision` fields, so the new fetcher pulls
  `/pulls?state=open&base=main` then for each PR pulls
  `/pulls/{n}/reviews` and synthesizes the GitHub-shape JSON the rest
  of the script (and the existing fixture-based unit tests) consume:
    BLOCKED + REVIEW_REQUIRED  ↔ mergeable=true AND 0 APPROVED reviews
    DIRTY                      ↔ mergeable=false (alarm doesn't fire)
    CLEAN + APPROVED           ↔ mergeable=true AND ≥1 APPROVED review
  Comment-posting moves to `POST /repos/.../issues/{n}/comments`
  (Gitea treats PRs as issues for the comment surface, same as
  GitHub's REST). All 23 fixture-driven unit tests still pass —
  fixtures pass GitHub-shape JSON via PR_FIXTURE which short-circuits
  the live fetch path.

- `scripts/ops/check_migration_collisions.py`
  Replaced `gh pr list` + `gh pr diff` calls with stdlib `urllib`
  against /api/v1. Helper `_gitea_get` centralizes auth + error
  handling; uses GITEA_TOKEN env, falling back to GITHUB_TOKEN
  (act_runner) and GH_TOKEN. Return shape from
  `open_prs_with_migration_prefix` mimics the historical
  `--json number,headRefName` so the call sites are unchanged. All 9
  regex-classifier unit tests still pass; live integration test
  against the production Gitea API returns 0 collisions for prefix=999
  as expected.

curl invocation pattern is `curl --fail-with-body -sS` (NOT `-fsS` —
the two short-fail flags are mutually exclusive in modern curl;
caught by `curl: You must select either --fail or --fail-with-body,
not both` during local verification).

Token model: workflows pass act_runner's GITHUB_TOKEN (per-run, repo
read scope) — same surface used by the auto-sync fix in PR #66 plus
the surrounding workflows. No new repo secrets required.

Verification: bash unit tests (23/23 pass), python unittest (9/9 pass),
live curl call against production Gitea returns 200 with the expected
shape, YAML / shell / Python syntax all validate.

Closes part of #75. Other classes (D — `gh api`; F — `gh run list`)
land in follow-up PRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:29:26 -07:00
b83b533381 Merge branch 'main' into fix/144-branch-protection-check-name-parity-audit 2026-05-07 22:24:45 +00:00
a23cf6a6bb Merge branch 'main' into fix/harness-replays-pre-clone-manifest 2026-05-07 22:24:42 +00:00
6acd63fa5a fix(ci): rewrite auto-promote staging→main for Gitea REST API
Root cause: same as #65/PR-#66 — gh CLI calls Gitea GraphQL
(/api/graphql) which returns HTTP 405. Additionally, gh workflow
run calls /actions/workflows/{id}/dispatches which does not
exist on Gitea 1.22.6 (verified via swagger.v1.json).

Fix:
- Replace gh run list with Gitea REST combined-status endpoint
  (GET /repos/{owner}/{repo}/commits/{ref}/status). Combined state
  encodes the AND across every check context — simpler than the
  per-workflow loop and immune to workflow-name collisions.
- Replace gh pr create / merge --auto with direct curl calls to
  POST /pulls and POST /pulls/{N}/merge with merge_when_checks_succeed.
- Remove the post-merge polling tail entirely. The GitHub-era
  GITHUB_TOKEN no-recursion rule does not apply on Gitea Actions
  (verified empirically: PR #66 merge fired downstream pushes
  naturally). Even if we wanted to dispatch, Gitea has no
  workflow_dispatch REST endpoint.

Critical constraint: main has enable_push: false with no whitelist;
direct push is impossible for any persona. PR-mediated merge is the
only path. main has required_approvals: 1 — auto-merge waits for
Hongming's approval before landing, preserving the
feedback_prod_apply_needs_hongming_chat_go contract.

Identity: AUTO_SYNC_TOKEN (devops-engineer persona). Not founder PAT.
Per feedback_per_agent_gitea_identity_default. Same persona used by
auto-sync (PR #66) — keeps identity model coherent.

Header comment block fully rewritten with 4 failure-mode runbooks
(A: gates not green, B: PR-create non-201, C: merge schedule fails,
D: token rotated/scope wrong) per PR #66's pattern.

Refs: #65, #73, #195, PR #66 (canonical reference)
Closes #73

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:24:28 -07:00
security-auditor
c0f4c16cc9 feat(canvas): ActivityTab subscribes to ACTIVITY_LOGGED — drop 5s polling
Stage 3 of #61 (final stage). Replaces the 5s setInterval poll with:
  1. Initial bootstrap on mount + on filter-change + on workspaceId-
     change (preserved from existing useEffect on loadActivities).
  2. Manual Refresh button (preserved — still triggers loadActivities).
  3. useSocketEvent subscription to ACTIVITY_LOGGED — every event
     for THIS workspace prepends to the list, gated on the user's
     autoRefresh toggle and current filter selection.

No interval poll. Steady-state HTTP traffic from this tab drops from
12 req/min (5s × 1 active workspace) to 0 outside of bootstraps and
manual refreshes. Live update latency drops from up to 5s to ~10ms.

The autoRefresh ("Live" / "Paused") toggle now gates LIVE updates
instead of polling cadence — semantically the same (paused = list
stays frozen), implementationally simpler.

The filter selection is honoured by the WS handler so a user
filtering to "Tasks" doesn't see live a2a_send rows trickle in. Same
shape the server-side `?type=<filter>` enforces on the bootstrap.

Test changes:
  - 27 existing tests pass unchanged (filter / autoRefresh /
    Refresh / loading / error / empty / count / row-content all
    preserved)
  - 7 new WS-subscription tests:
      - WS push for matching workspace prepends with NO HTTP call
      - WS push for different workspace ignored
      - WS push respects active filter (non-matching ignored)
      - WS push respects active filter (matching renders)
      - WS push while autoRefresh paused ignored
      - WS push for already-in-list row deduped (no double-render)
      - NO 5s interval polling after mount

Mutation-tested:
  - drop workspace_id filter → "different workspace" test fails
  - drop autoRefresh gate → "paused" test fails
  - drop filter gate → "non-matching activity_type" test fails
  - drop dedup-by-id → "already in list deduped" test fails

Full canvas suite: 1396 passing, 0 failing. tsc clean.

No API or schema change. /workspaces/:id/activity HTTP endpoint
stays — used for bootstrap + manual refresh + filter-change reload.
ACTIVITY_LOGGED event shape unchanged.

Hostile self-review (three weakest spots):
  1. Server-side activity_logs row UPDATES (status flips, etc.) are
     not reflected post-#61 — the dedup-by-id check skips a re-fired
     ACTIVITY_LOGGED for an existing row. Acceptable: activity_logs
     is append-only by design (audit trail); status updates surface
     as new task_update rows, not as in-place mutations. If a future
     server change adds in-place updates, fire ACTIVITY_UPDATED as a
     distinct event so this dedup logic stays simple.
  2. WS handler is recreated on every render (filter / autoRefresh /
     workspaceId state changes). useSocketEvent's ref-based pattern
     keeps the bus subscription stable, but the handler closure
     re-captures each render. Side effect: fine — handler call cost
     is negligible.
  3. The "error" filter matches activity_type === "error" (mirrors
     server semantics). It does NOT match status === "error" rows
     of other activity types — same as the polling version. Worth
     re-evaluating in a separate PR if users expect the broader
     semantic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:21:38 -07:00
security-auditor
7194b08987 feat(canvas): A2ATopologyOverlay subscribes to ACTIVITY_LOGGED — drop 60s polling
Stage 2 of #61. Replaces the 60s setInterval poll that fanned out
across every visible workspace fetching `?type=delegation&limit=500`
with:
  1. One bootstrap fan-out on mount (or on visible-ID-set change),
     same shape as before — preserves the 60-min look-back history.
  2. useSocketEvent subscription to ACTIVITY_LOGGED — every event
     with activity_type=delegation + method=delegate from a visible
     workspace appends to a local rolling buffer, edges are re-derived
     via the existing buildA2AEdges helper.
  3. showA2AEdges toggle off: clears edges + buffer.

No interval poll. The visibleIdsKey selector gate that fixed the
2026-05-04 render-loop incident is preserved — peer-discovery /
status-flip writes still don't trigger a wasteful re-bootstrap.

Steady-state HTTP traffic from this overlay drops from N req/min
(N visible workspaces × 1 cycle/min) to 0 outside of mount + visible-
ID-set-change bootstraps. Live update latency drops from up to 60s
to ~10ms.

Bootstrap race-aware: any WS arrivals that landed in the buffer
during the fetch await are preserved by id-dedup-with-fetched-first
ordering. No row is double-counted; no row is lost during in-flight
updates.

Test changes:
  - 27 existing tests pass unchanged (buildA2AEdges purity preserved,
    component visibility/visibleIdsKey/error-swallow behaviour
    preserved).
  - 6 new WS-subscription tests:
      - NO 60s polling after bootstrap (clock advance fires nothing)
      - WS push for delegation updates edges with NO HTTP call
      - WS push for non-delegation activity_type ignored
      - WS push for delegate_result ignored (mirrors buildA2AEdges
        method filter)
      - WS push from hidden workspace ignored
      - WS push while showA2AEdges=false ignored

Mutation-tested:
  - drop activity_type filter → "non-delegation" test fails
  - drop method===delegate filter → "delegate_result" test fails
  - drop visible-ws membership filter → "hidden workspace" test fails

Full canvas suite: 1395 passing, 0 failing. tsc clean.

No API or schema change. ACTIVITY_LOGGED event shape unchanged.
The /workspaces/:id/activity HTTP endpoint stays — used for bootstrap.

Hostile self-review (three weakest spots):
  1. Bootstrap fetches up to 500 rows × N workspaces. Worst-case
     buffer ~3000 entries before window-prune. Acceptable: window-
     prune runs on every recomputeAndPush, buildA2AEdges aggregates
     to at most N² edges. Real-world usage stays well under both.
  2. WS handler re-arms on every bootstrap dependency change
     (visibleIds change). useSocketEvent's ref-based pattern means
     the bus subscription stays stable across renders, but the
     handler closure re-captures bootstrap each time. Side effect:
     fine — handler invocation just calls recomputeAndPush which is
     idempotent.
  3. delegate_result rows arriving over WS are silently dropped.
     Acceptable: the existing buildA2AEdges already filters them out
     at aggregation time (avoids double-counting); pre-filtering at
     the WS handler is the correct mirror — keeps the bus path and
     the bootstrap path consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:17:19 -07:00
d9e380c5bc feat(workspace-server): local-dev provisioner builds from Gitea source when MOLECULE_IMAGE_REGISTRY is unset (#63, Task #194)
OSS contributors who clone molecule-core and `go run ./workspace-server/cmd/server`
now get a working end-to-end provision without authenticating to GHCR or AWS ECR.

Pre-fix: with MOLECULE_IMAGE_REGISTRY unset, the provisioner attempted to pull
ghcr.io/molecule-ai/workspace-template-<runtime>:latest, which has been
returning 403 since the 2026-05-06 GitHub-org suspension.

Post-fix: when MOLECULE_IMAGE_REGISTRY is unset, the provisioner switches to
local-build mode — looks up the workspace-template-<runtime> repo's HEAD sha
on Gitea via a single API call, shallow-clones into ~/.cache/molecule/, and
runs `docker build --platform=linux/amd64`. SHA-pinned cache key skips the
clone+build entirely on subsequent provisions.

Production tenants are unaffected: every prod tenant sets the var to its
private ECR mirror, so the SaaS pull path is byte-for-byte identical.

SSOT for mode detection lives in Resolve() (registry_mode.go) returning a
discriminated RegistrySource{Mode, Prefix} so call sites that branch on
mode get a compile-time push instead of a string-equality footgun.

Coverage:
* registry_mode.go            — new SSOT (Resolve, RegistryMode, IsKnownRuntime)
* registry_mode_test.go       — 8 tests pinning mode-decision contract
* localbuild.go               — clone+build pipeline (570 LOC, fully unit-tested)
* localbuild_test.go          — 22 tests covering happy/sad paths, fail-closed
* provisioner.go              — Start() inserts ensureLocalImageHook in local mode
* docs/adr/ADR-002            — design rationale + alternatives + security review
* docs/development/local-development.md — local-build flow + env overrides

Security:
* Allowlist-only runtime names (knownRuntimes) gate the clone path.
* Repo prefix hardcoded to git.moleculesai.app/molecule-ai/molecule-ai-workspace-template-;
  forks via opt-in MOLECULE_LOCAL_TEMPLATE_REPO_PREFIX.
* MOLECULE_GITEA_TOKEN masked in every log line via maskTokenInURL/maskTokenInString.
* Fail-closed: Gitea unreachable / runtime not mirrored → clear error, never
  silently fall back to GHCR/ECR.
* docker build invocation passes no --build-arg from external input.
* HTTP body cap 64KB on Gitea API responses (defence vs malicious upstream).

Closes #63 / Task #194.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:16:51 -07:00
f8a238dfdd chore: second auto-sync verification (post-#66/#67) (#68) 2026-05-07 22:11:30 +00:00
security-auditor
830de70e84 feat(canvas): CommunicationOverlay subscribes to ACTIVITY_LOGGED — drop 30s polling
Stage 1 of #61. Replaces the 30s setInterval poll with:
  1. One bootstrap fan-out on mount (cap of 3 retained from the
     2026-05-04 fix), gives the initial recent-comms window without
     waiting for live events.
  2. useSocketEvent subscription to ACTIVITY_LOGGED — every event
     with a comm-overlay-relevant activity_type from a visible online
     workspace prepends to the rendered list.
  3. Re-bootstrap on visibility-toggle re-open so the snapshot is
     fresh after a long collapsed period.

No interval poll. Inherits the singleton ReconnectingSocket's
reconnect / backoff / health-check guarantees via useSocketEvent.

Steady-state HTTP traffic from this overlay drops from ~6 req/min
(3 ws × 2 cycles/min) to 0 outside of mount/visibility-toggle
bootstraps. Live updates arrive within ~10ms of the server insert
instead of after up to 30s.

Test changes:
  - Bootstrap fan-out cap of 3 — kept (was the cadence test's role
    pre-#61)
  - 30s cadence test — replaced with "no interval polling" test
    that pins the absence of any cadence-driven HTTP after bootstrap
  - Visibility gate test — extended to verify both: no fetches while
    closed, AND re-bootstrap on re-open
  - WS subscription tests (new):
      - WS push extends rendered list with NO HTTP call
      - WS push for offline workspace ignored
      - WS push for non-comm activity_type ignored
      - WS push while collapsed ignored
      - non-ACTIVITY_LOGGED events ignored

Mutation-tested:
  - drop visibility gate → visibility test fails
  - drop activity_type filter → "non-comm activity_type" test fails
  - drop workspace online-set filter → "offline workspace" test fails

Full canvas suite: 1393 passing, 0 failing. tsc clean.

No API or schema change. ACTIVITY_LOGGED event shape pinned by
existing socket-events tests.

Hostile self-review (three weakest spots):
  1. Sustained WS outage shows stale comms until visibility-toggle
     re-bootstrap. Acceptable: the singleton socket already auto-
     reconnects and the comm overlay isn't a critical-path surface.
  2. Bootstrap on visibility-toggle costs another 3 HTTP calls each
     re-open. Acceptable: visibility-toggle is a deliberate user
     action, not a tight loop.
  3. The WS handler reads the latest `nodes` via nodesRef rather
     than re-subscribing on node changes. By design — the bus
     listener stays bound for the component lifetime to avoid the
     "tear-down storm" pattern A2ATopologyOverlay's comment warns
     about (ref-based current-state lookup, stable subscription).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:11:02 -07:00
3f68ac1fcb chore: second consecutive trigger for auto-sync verification (post-#66/#67) 2026-05-07 15:10:40 -07:00
5efa92fbc6 chore: verify auto-sync main→staging post-#66 (#67) 2026-05-07 22:10:04 +00:00
f0664264cb chore: empty commit to verify auto-sync main→staging post-#66 2026-05-07 15:09:18 -07:00
7b194eb1aa fix(ci): rewrite auto-sync main→staging for Gitea direct push (#66, closes #65) 2026-05-07 22:07:00 +00:00
6235ef7461 fix(ci): rewrite auto-sync main→staging for Gitea direct push
Root cause of `Auto-sync main → staging / sync-staging (push)`
failing every push to main since the GitHub→Gitea migration:

The workflow assumed a GitHub `merge_queue` ruleset on staging
(blocking direct push) and used `gh pr create` + `gh pr merge
--auto` to land sync via the queue. On Gitea this fails at the
`gh pr create` step with `HTTP 405 Method Not Allowed
(https://git.moleculesai.app/api/graphql)` — Gitea exposes no
GraphQL endpoint, and the GitHub-CLI cannot ship PRs against
Gitea.

Verified failure mode in run 1117/job 0 (token logs at
/tmp/log2.txt, run target /molecule-ai/molecule-core/actions/
runs/1117/jobs/0). The merge step succeeded and pushed
auto-sync/main-1e1f4d63; the PR step failed with the 405. So
every main push left an orphan auto-sync/* branch and a red CI
status, with no PR to land it.

Fix: the staging branch protection on Gitea
(`enable_push: true`, `push_whitelist_usernames:
[devops-engineer]`) already permits direct push from the
devops-engineer persona. Drop the entire merge-queue PR
architecture and replace with:

  1. Checkout staging with secrets.AUTO_SYNC_TOKEN
     (devops-engineer persona token, NOT founder PAT —
     `feedback_per_agent_gitea_identity_default`).
  2. `git fetch origin main` + ff-merge or no-ff merge.
  3. `git push origin staging` directly.

The AUTO_SYNC_TOKEN repo secret already exists (created
2026-05-07 14:00 alongside the staging push_whitelist update).
Workflow name + job name unchanged → required-check name
`Auto-sync main → staging / sync-staging (push)` keeps the
same context, no branch-protection edits needed.

Rejected alternatives (documented in workflow header):
- Reuse PR architecture via Gitea REST: ~80 LOC of API
  plumbing for no benefit; direct push works.
- GH_HOST=git.moleculesai.app: still calls /api/graphql,
  same 405; doesn't fix the root issue.
- Custom JS action: external dep for a 5-line `git push`.

Header comment in the workflow now documents:
- What this workflow does (SSOT for staging advancing).
- Why direct push (GitHub merge_queue → Gitea push_whitelist).
- Identity and token (anti-bot-ring per saved memory).
- Failure modes A–D with operator runbook for each.
- Loop safety (push to staging doesn't fire push:main → no
  recursion).

Verification plan: this fix-PR's merge to main is itself the
trigger; watch the workflow run on the merge commit and on
one follow-up trigger commit, expect both green.

Refs: failing run https://git.moleculesai.app/molecule-ai/
molecule-core/actions/runs/1117/jobs/0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:04:12 -07:00
security-auditor
5b7b669b4c docs(ratelimit): tighten dev-mode comment after keyFor refactor
The previous comment said "all share one IP bucket" — accurate before
the keyFor refactor, slightly stale after it. The dev-mode rationale
(bucket fills fast, blanks the page on a single-user dev box) is
unchanged; only the bucket-key flavour text needed updating.

Doc-only follow-up from #60's hostile self-review #3. No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:57:21 -07:00
security-auditor
9dda84d671 fix(ratelimit): tenant-aware bucket keying — close canvas 429 storm
Closes #59.

Symptom: /workspaces/:id/activity returns 429 with rate-limit-exceeded
on hongming.moleculesai.app whenever multiple workspaces are visible
in the canvas. Single-tab, single-user, well within the documented
600 req/min budget — but every request collapsed into one bucket.

Root cause: workspace-server's RateLimiter keyed buckets on
c.ClientIP(). After issue #179 turned off proxy-header trust
(SetTrustedProxies(nil), correctly closing the XFF spoofing hole),
c.ClientIP() returns the TCP RemoteAddr — which in production is the
upstream proxy (Caddy on per-tenant EC2; CP/Vercel on the SaaS plane).
Every browser tab + every canvas consumer + every poll loop for every
tenant collapsed into one bucket.

Fix: bucket key derivation moves into a single keyFor helper that
mirrors the SSOT pattern of:
  - molecule-controlplane/internal/middleware/ratelimit.go (org > user > IP)
  - this package's own MCPRateLimiter (token-hash via tokenKey)

Priority: X-Molecule-Org-Id header → SHA-256(Authorization Bearer)
→ ClientIP. Token values are kept hashed in the bucket map so the
in-memory state can't become a token dump.

Tests:
  - TestKeyFor_OrgIdHeaderTrumpsBearerAndIP — priority order
  - TestKeyFor_BearerTokenWhenNoOrgId — middle tier + raw-token leak pin
  - TestKeyFor_IPFallbackWhenNoOrgIdNoBearer — anon probe path
  - TestRateLimit_TwoOrgsSameIP_IndependentBuckets — load-bearing
    regression (issue #59) — two tenants behind same upstream proxy
    must not share a bucket
  - TestRateLimit_TwoTokensSameIP_IndependentBuckets — same shape
    for the per-tenant Caddy box
  - TestRateLimit_SameOrgDifferentTokens_SharedBucket — counter-pin:
    rotating tokens within one org must NOT bypass the org's quota
  - TestRateLimit_Middleware_RoutesThroughKeyFor — AST gate, mirrors
    the SSOT gates established in #36/#10/#12

Mutation-tested:
  - strip org-id branch in keyFor → 3 tests fail
  - strip bearer-token branch → 2 tests fail
  - reintroduce direct c.ClientIP() in Middleware → 3 tests fail
    (including the AST gate)

Existing tests pass unchanged: dev-mode fail-open, X-RateLimit-*
headers (#105), Retry-After on 429 (#105), XFF anti-spoofing (#179).

No schema/API change. 429 response body and X-RateLimit-* headers
unchanged. RATE_LIMIT env var semantics unchanged.

Hostile self-review (three weakest spots) is in the issue body:
  1. one-shot Docker-inspect cost is now bucket-key derivation cost
     (string compare + SHA-256 of bearer); single-digit microseconds.
  2. X-Molecule-Org-Id is unvalidated at the rate-limiter layer —
     spoofing is closed by tenant SG + CP front; documented in
     keyFor's docstring with the conditions under which to revisit.
  3. cpProv-style SaaS surface is out of scope; CP's own limiter
     handles that hop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:51:08 -07:00
Hongming Wang
7c6acc18ae ci(branch-protection): check-name parity gate (#144)
Audit finding: every workflow that emits a required-status-check name
on molecule-core's branch protection (apply.sh's STAGING_CHECKS +
MAIN_CHECKS) ALREADY uses the safe always-runs-with-conditional-steps
shape — Platform/Canvas/Python/Shellcheck in ci.yml, Canvas tabs E2E
in e2e-staging-canvas.yml, E2E API Smoke in e2e-api.yml, PR-built
wheel in runtime-prbuild-compat.yml, the codeql Analyze matrix, and
the always-on Secret scan + Detect changes. No production drift to
fix today.

Adds a regression-guard so the next path-filter / matrix refactor /
workflow rename can't silently re-introduce the bug shape called out
in saved memory feedback_branch_protection_check_name_parity:

  "Path filters … silently break branch protection because no job
   emits the protected sentinel status when path-filter returns false."

New tools:
  - tools/branch-protection/check_name_parity.sh — extracts every
    required check name from apply.sh's heredocs, then for each name
    classifies the owning workflow as safe (no top-level paths:) /
    safe (per-step if-gates without top-level paths:) / unsafe
    (top-level paths: without per-step if-gates) / unsafe-mix
    (top-level paths: WITH per-step if-gates — the workflow may still
    skip entirely on path exclusion, leaving the gates dormant) /
    missing (no emitter at all). Special-cases codeql.yml's matrix-
    expanded `Analyze (${{ matrix.language }})`.
  - tools/branch-protection/test_check_name_parity.sh — 6 unit tests
    covering each classification: safe, unsafe-path-filter, missing,
    safe-with-per-step-gates, unsafe-mix, matrix-expansion. Each test
    builds a synthetic apply.sh + workflow file in a tmpdir, invokes
    the script, and asserts on exit code + stderr substring. Per
    feedback_assert_exact_not_substring the assertions pin specific
    classifications, not just non-zero exit.

Wired into branch-protection-drift.yml so every PR touching
.github/workflows/** runs the parity check; the existing daily
schedule covers between-PR drift. The check is cheap (~1s) and runs
without the admin token — only reads files in the checkout. Self-
test step runs the unit tests on every invocation, so a regression
in the script can't false-pass on production.

Per BSD-vs-GNU portability hygiene: heredoc-marker extraction stays
in plain awk + sed (no gawk-only `match()` array form), grep regex
avoids `^` anchor for `if:` lines because real workflows use
`      - if:` with the `-` step-marker between leading spaces and
`if:` (the original anchor missed every workflow's per-step gates).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:42:50 -07:00
1e1f4d635b fix(ci): convert CodeQL workflow to no-op stub on Gitea (#156) (#51)
Closes #156. Touches #142.

Approved-by: security-auditor
2026-05-07 21:37:04 +00:00
3a00dd236f fix(ci): convert CodeQL workflow to no-op stub on Gitea (#156)
Why
---
PR #35 marked `continue-on-error: true` at the JOB level (correct YAML),
but Gitea Actions 1.22.6 does NOT propagate job-level continue-on-error
to the commit-status API — every matrix leg still posts `failure`. That
keeps OVERALL=failure on every push to main + staging and blocks the
auto-promote signal even when every other gate is green.

Worse: the underlying CodeQL run never actually worked on Gitea. The
github/codeql-action/init@v4 step calls api.github.com bundle endpoints
(CLI download + query packs + telemetry) that Gitea does NOT proxy.
Confirmed via live-tested run 1d/3101 on operator host:

    2026-05-07T20:55:17 ::group::Run Initialize CodeQL
      with: languages: ${{ matrix.language }}
            queries: security-extended
    2026-05-07T20:55:36 ::error::404 page not found
    2026-05-07T20:55:50 Failure - Main Initialize CodeQL
    2026-05-07T20:55:51 skipping Perform CodeQL Analysis (main skipped)
    2026-05-07T20:55:51 :⚠️:No files were found at sarif-results/go/

The SARIF artifact upload was already a no-op (warning above) — the
analyze step never wrote anything because init failed. So nothing of
value is being lost by stubbing this out.

What
----
- Convert the workflow to a single-step stub that emits success per
  matrix language (go, javascript-typescript, python).
- Keep workflow `name: CodeQL` exactly (auto-promote-staging.yml
  line 67 keys on it as a workflow_run gate).
- Keep job name template `Analyze (${{ matrix.language }})` and the
  3-leg matrix exactly (commit-status context names + branch
  protection + #144 required-check-name parity).
- Keep all four triggers (push / pull_request / merge_group /
  schedule) so merge_group required-checks parity holds.
- Drop the codeql-action steps, the Autobuild step, the SARIF parse
  step, and the upload-artifact step — all four of those are now
  dead code (init can never succeed against Gitea's API surface).

Policy
------
Per Hongming decision 2026-05-07 (#156): CodeQL is ADVISORY, not
blocking, until a Gitea-compatible SAST pipeline lands. The header
of the new workflow file documents this decision + lists the three
re-enable options (self-hosted Semgrep, Sonatype, GitHub mirror)
plus the compensating controls in place (secret-scan, block-internal-
paths, lint-curl-status-capture, branch-protection-drift).

Closes #156. Touches #142 (no capital-M Molecule-AI refs in this
file — already lowercase per e01077be).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:26:57 -07:00