voidforge-build 23.19.0 → 23.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/.claude/agents/celebrimbor-forge-artist.md +1 -0
  2. package/dist/.claude/agents/ducem-token-economics.md +1 -0
  3. package/dist/.claude/agents/galadriel-frontend.md +1 -0
  4. package/dist/.claude/agents/romanoff-integrations.md +4 -0
  5. package/dist/.claude/agents/silver-surfer-herald.md +19 -4
  6. package/dist/.claude/commands/architect.md +4 -3
  7. package/dist/.claude/commands/assemble.md +12 -0
  8. package/dist/.claude/commands/assess.md +1 -0
  9. package/dist/.claude/commands/build.md +8 -0
  10. package/dist/.claude/commands/contextmeter.md +56 -0
  11. package/dist/.claude/commands/debrief.md +10 -0
  12. package/dist/.claude/commands/engage.md +5 -0
  13. package/dist/.claude/commands/git.md +19 -3
  14. package/dist/.claude/commands/imagine.md +1 -1
  15. package/dist/.claude/commands/seal.md +81 -0
  16. package/dist/.claude/commands/ux.md +13 -0
  17. package/dist/.claude/workflows/gauntlet.workflow.js +13 -1
  18. package/dist/CHANGELOG.md +63 -0
  19. package/dist/CLAUDE.md +10 -1
  20. package/dist/HOLOCRON.md +16 -2
  21. package/dist/VERSION.md +3 -1
  22. package/dist/docs/methods/AI_INTELLIGENCE.md +3 -0
  23. package/dist/docs/methods/ASSEMBLER.md +12 -0
  24. package/dist/docs/methods/BUILD_PROTOCOL.md +15 -0
  25. package/dist/docs/methods/CAMPAIGN.md +11 -0
  26. package/dist/docs/methods/DEVOPS_ENGINEER.md +66 -0
  27. package/dist/docs/methods/FIELD_MEDIC.md +1 -0
  28. package/dist/docs/methods/FORGE_ARTIST.md +3 -4
  29. package/dist/docs/methods/GAUNTLET.md +6 -0
  30. package/dist/docs/methods/MUSTER.md +2 -0
  31. package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +18 -0
  32. package/dist/docs/methods/QA_ENGINEER.md +21 -1
  33. package/dist/docs/methods/RELEASE_MANAGER.md +38 -0
  34. package/dist/docs/methods/SECURITY_AUDITOR.md +11 -1
  35. package/dist/docs/methods/SUB_AGENTS.md +33 -0
  36. package/dist/docs/methods/SYSTEMS_ARCHITECT.md +15 -0
  37. package/dist/docs/methods/TESTING.md +2 -0
  38. package/dist/docs/methods/TROUBLESHOOTING.md +2 -2
  39. package/dist/docs/methods/WORKFLOWS.md +14 -0
  40. package/dist/docs/patterns/ai-prompt-safety.ts +85 -0
  41. package/dist/docs/patterns/data-pipeline.ts +59 -1
  42. package/dist/docs/patterns/egress-sandbox.sh +43 -0
  43. package/dist/docs/patterns/exclusion-set-invariant.md +62 -0
  44. package/dist/docs/patterns/multi-tenant-property-test.ts +64 -0
  45. package/dist/docs/patterns/nginx-vhost.conf +156 -0
  46. package/dist/docs/patterns/oauth-token-lifecycle.ts +21 -0
  47. package/dist/docs/patterns/post-deploy-probe.sh +115 -0
  48. package/dist/docs/patterns/rls-test-fixture.py +140 -0
  49. package/dist/docs/patterns/structural-sql-sentinel.py +134 -0
  50. package/dist/scripts/statusline/README.md +38 -0
  51. package/dist/scripts/statusline/context-awareness-hook.sh +53 -0
  52. package/dist/scripts/statusline/settings-snippet.json +17 -0
  53. package/dist/scripts/statusline/voidforge-statusline.sh +91 -0
  54. package/dist/scripts/voidforge.js +69 -6
  55. package/dist/wizard/lib/claude-md-strategy.d.ts +87 -0
  56. package/dist/wizard/lib/claude-md-strategy.js +198 -0
  57. package/dist/wizard/lib/marker.d.ts +48 -1
  58. package/dist/wizard/lib/marker.js +58 -2
  59. package/dist/wizard/lib/patterns/oauth-token-lifecycle.d.ts +14 -0
  60. package/dist/wizard/lib/patterns/oauth-token-lifecycle.js +21 -0
  61. package/dist/wizard/lib/project-init.js +59 -0
  62. package/dist/wizard/lib/updater.d.ts +19 -0
  63. package/dist/wizard/lib/updater.js +84 -33
  64. package/package.json +2 -2
@@ -0,0 +1,156 @@
1
+ # nginx vhost — Cloudflare-Flexible-compatible reverse proxy with ACME passthrough
2
+ #
3
+ # Reference implementation for a per-tenant origin vhost that sits behind
4
+ # Cloudflare in *Flexible* SSL mode (browser<->Cloudflare is HTTPS, but
5
+ # Cloudflare<->origin is plain HTTP on :80). Use this when the origin app
6
+ # does NOT terminate TLS itself and Cloudflare fronts the zone.
7
+ #
8
+ # Evidence: field report #344 F2 (origin 301 HTTP->HTTPS redirect loops on
9
+ # Cloudflare Flexible zones) and #344 F4a (missing/origin-level security header
10
+ # stack + per-tenant log isolation).
11
+ #
12
+ # When to use it:
13
+ # - Cloudflare zone SSL mode = Flexible, origin speaks HTTP only.
14
+ # - You want the security header stack applied at the origin so it survives
15
+ # even if a Cloudflare Transform/Response-Header rule is removed.
16
+ # - You need Let's Encrypt http-01 (ACME) to keep working through the proxy.
17
+ #
18
+ # When NOT to use it (use a TLS-terminating vhost instead):
19
+ # - Cloudflare SSL mode = Full or Full (strict): the origin must serve HTTPS
20
+ # and you SHOULD redirect HTTP->HTTPS. Adding the 301 below would NOT loop
21
+ # in that mode. This template deliberately omits the redirect for Flexible.
22
+ #
23
+ # Install:
24
+ # - Copy to /etc/nginx/sites-available/<tenant>.conf, edit the @@PLACEHOLDERS@@,
25
+ # symlink into sites-enabled/, then `nginx -t && systemctl reload nginx`.
26
+ # - Replace @@SERVER_NAME@@, @@UPSTREAM@@, @@TENANT@@ before loading.
27
+ #
28
+ # Placeholders:
29
+ # @@SERVER_NAME@@ -> e.g. app.example.com
30
+ # @@UPSTREAM@@ -> origin app host:port, e.g. 127.0.0.1:3000
31
+ # @@TENANT@@ -> short slug used for log filenames, e.g. acme-corp
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Rate-limit zone declaration.
35
+ #
36
+ # IMPORTANT (field report #344 F4a): limit_req_zone is an http{}-context-only
37
+ # directive. It MUST live in the top-level http{} block (e.g. nginx.conf or a
38
+ # file in conf.d/ that is included at http scope) — it is NOT valid inside a
39
+ # server{} or location{} block and `nginx -t` will reject it there with
40
+ # "limit_req_zone directive is not allowed here".
41
+ #
42
+ # Declare the zone ONCE at http{} scope, then *apply* it per-location with
43
+ # `limit_req` (which IS valid in server{}/location{}). The line below is shown
44
+ # here for reference only — move it to your http{} context:
45
+ #
46
+ # limit_req_zone $binary_remote_addr zone=@@TENANT@@_perip:10m rate=20r/s;
47
+ #
48
+ # The WebSocket upgrade map below is ALSO http{}-context-only — declare it once
49
+ # at http{} scope alongside the rate-limit zone, not inside server{}:
50
+ #
51
+ # map $http_upgrade $connection_upgrade {
52
+ # default upgrade;
53
+ # '' close; # plain HTTP keep-alive: send Connection: close-able
54
+ # }
55
+ # ---------------------------------------------------------------------------
56
+
57
+ upstream @@TENANT@@_origin {
58
+ # Origin application. keepalive reduces TCP churn on the proxy hop.
59
+ server @@UPSTREAM@@;
60
+ keepalive 32;
61
+ }
62
+
63
+ server {
64
+ listen 80;
65
+ listen [::]:80;
66
+ server_name @@SERVER_NAME@@;
67
+
68
+ # --- Per-tenant access/error logs (field report #344 F4a) -------------
69
+ # Isolated per tenant so one tenant's traffic/errors never contaminate
70
+ # another's audit trail. Ensure /var/log/nginx exists and is writable.
71
+ access_log /var/log/nginx/@@TENANT@@.access.log combined;
72
+ error_log /var/log/nginx/@@TENANT@@.error.log warn;
73
+
74
+ # --- ACME http-01 passthrough (field report #344 F4a) -----------------
75
+ # Let's Encrypt validates by fetching /.well-known/acme-challenge/<token>
76
+ # over plain HTTP. This location MUST be reachable on :80 and MUST NOT be
77
+ # redirected or proxied to the app, or certificate issuance/renewal fails.
78
+ # Point root at the webroot your ACME client writes challenges into.
79
+ location ^~ /.well-known/acme-challenge/ {
80
+ default_type "text/plain";
81
+ root /var/www/acme;
82
+ # No proxy here — serve the challenge token straight from the webroot.
83
+ # NOTE: nginx add_header inheritance is replace-not-merge, but a block
84
+ # with zero add_header of its own inherits the parent's. The security
85
+ # headers declared at server{} scope below are thus also emitted here;
86
+ # that is harmless for a bare token response. If you must strip them on
87
+ # this path, redeclare the headers you want (or none) inside this block.
88
+ try_files $uri =404;
89
+ }
90
+
91
+ # --- Security header stack (field report #344 F4a) --------------------
92
+ # Applied at the origin so the posture survives even if a Cloudflare
93
+ # response-header rule is later removed. `always` ensures the headers are
94
+ # emitted on error responses (4xx/5xx) too, not just 2xx/3xx.
95
+ #
96
+ # HSTS-aware: behind Cloudflare Flexible the browser<->edge leg is HTTPS,
97
+ # so advertising HSTS is correct for the public hostname. Start with a
98
+ # short max-age while validating, then raise to 1y + preload once certain
99
+ # the apex and every subdomain are HTTPS-only at the edge.
100
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
101
+ add_header X-Content-Type-Options "nosniff" always;
102
+ add_header X-Frame-Options "SAMEORIGIN" always;
103
+ # frame-ancestors is the modern replacement for X-Frame-Options; both are
104
+ # sent for backward compatibility with older user agents.
105
+ add_header Content-Security-Policy "frame-ancestors 'self'" always;
106
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
107
+
108
+ # --- Reverse proxy to the origin app ----------------------------------
109
+ # NOTE (field report #344 F2): there is deliberately NO
110
+ # `return 301 https://$host$request_uri;`
111
+ # and NO `if ($http_x_forwarded_proto != "https") { return 301 ...; }`
112
+ # here. On a Cloudflare *Flexible* zone the edge talks HTTP to the origin,
113
+ # so any origin-level HTTP->HTTPS redirect produces an infinite redirect
114
+ # loop (ERR_TOO_MANY_REDIRECTS). Let Cloudflare handle the HTTPS upgrade
115
+ # at the edge. If you switch the zone to Full/Full-strict, terminate TLS
116
+ # at the origin and add the redirect in a separate :80 server block.
117
+ location / {
118
+ # Apply the http{}-scoped rate-limit zone here (this is the valid
119
+ # context for limit_req; the zone itself is declared at http{} scope
120
+ # per the note at the top of this file).
121
+ limit_req zone=@@TENANT@@_perip burst=40 nodelay;
122
+
123
+ proxy_pass http://@@TENANT@@_origin;
124
+ proxy_http_version 1.1;
125
+
126
+ # Preserve client + protocol context for the app. X-Forwarded-Proto is
127
+ # taken from Cloudflare's CF-Visitor / the edge; default to https since
128
+ # the public-facing leg is HTTPS even though this hop is HTTP.
129
+ proxy_set_header Host $host;
130
+ proxy_set_header X-Real-IP $remote_addr;
131
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
132
+ proxy_set_header X-Forwarded-Proto https;
133
+ proxy_set_header X-Forwarded-Host $host;
134
+
135
+ # WebSocket / upgrade support. $connection_upgrade is the http{}-scoped
136
+ # map declared at the top of this file: it sends "upgrade" for WS and
137
+ # "close" for plain HTTP. This single directive replaces the conflicting
138
+ # `Connection "upgrade"` + `Connection ""` pair and lets keepalive to
139
+ # the upstream coexist with WebSocket upgrades.
140
+ proxy_set_header Upgrade $http_upgrade;
141
+ proxy_set_header Connection $connection_upgrade;
142
+
143
+ proxy_connect_timeout 5s;
144
+ proxy_send_timeout 60s;
145
+ proxy_read_timeout 60s;
146
+
147
+ proxy_buffering on;
148
+ }
149
+
150
+ # Lightweight health endpoint that bypasses the app, for the edge/monitor.
151
+ location = /healthz {
152
+ access_log off;
153
+ default_type "application/json";
154
+ return 200 '{"status":"ok"}';
155
+ }
156
+ }
@@ -8,6 +8,20 @@
8
8
  * - Failure escalation: retry 3x → pause platform → alert → requires_reauth
9
9
  * - Token stored as encrypted blob in vault, keyed by platform name
10
10
  * - Session token (daemon) rotates every 24 hours (§9.19.15)
11
+ * - VERIFY EXPIRY + REFRESH-GRANT BEHAVIOR AGAINST THE PROVIDER'S LIVE DOCS AT
12
+ * INTEGRATION TIME. The PLATFORM_CONFIGS TTLs below are STARTING ASSUMPTIONS,
13
+ * not ground truth — providers change them and "no refresh token / never
14
+ * expires" is a common false assumption. Field report #373: a Todoist
15
+ * integration shipped on "tokens don't expire," but the modern API issues
16
+ * ~1h access tokens WITH a refresh token; the code discarded the refresh
17
+ * token + expiry and registered no refresher, so it died ~1h after every
18
+ * connect across four sessions — looking exactly like intermittent
19
+ * revocation. At integration time: (1) read the provider's OAuth docs and
20
+ * quote the verified access-token TTL + whether a refresh_token is issued;
21
+ * (2) if a refresh_token exists, PERSIST it and register a refresher — never
22
+ * discard it; (3) distinguish "expired" from "revoked" via the API's OWN
23
+ * error body, not by inference (an expired token that mimics revocation will
24
+ * send you reauth-hunting instead of refreshing).
11
25
  *
12
26
  * Agents: Breeze (platform relations), Dockson (vault)
13
27
  *
@@ -50,6 +64,13 @@ interface PlatformTokenConfig {
50
64
  revokeEndpoint?: string;
51
65
  }
52
66
 
67
+ // ASSUMPTIONS, NOT GROUND TRUTH (field report #373). These TTLs and the
68
+ // "refreshTokenTtlDays: 0 = never expires" entries are starting points. At
69
+ // integration time, VERIFY each value against the provider's current OAuth
70
+ // docs and the live token response (`expires_in`, presence of `refresh_token`)
71
+ // — a provider that "doesn't expire" today may issue ~1h tokens tomorrow, and
72
+ // a missing refresher then surfaces as recurring prod token-death that mimics
73
+ // revocation. Treat any new platform here the same way before shipping.
53
74
  const PLATFORM_CONFIGS: PlatformTokenConfig[] = [
54
75
  { platform: 'meta', accessTokenTtlHours: 1440, refreshTokenTtlDays: 0, refreshEndpoint: 'https://graph.facebook.com/v19.0/oauth/access_token' },
55
76
  { platform: 'google', accessTokenTtlHours: 1, refreshTokenTtlDays: 0, refreshEndpoint: 'https://oauth2.googleapis.com/token' },
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bash
2
+ # Post-Deploy Probe — Assert sensitive paths are NOT publicly served.
3
+ #
4
+ # Reference implementation for .claude/commands/deploy.md Step 4.5.
5
+ # Probes a denylist of paths against a live deploy URL.
6
+ #
7
+ # CONTENT-AWARE, NOT STATUS-ONLY (field report #371). A Single-Page App with a
8
+ # catch-all route returns HTTP 200 for EVERY path — /.env, /.git/config,
9
+ # /id_rsa — by serving index.html. A status-only probe reads those 200s as
10
+ # "EXPOSED" and would trigger a ROLLBACK of a clean deploy (false positive),
11
+ # while a real leak that happens to 200 looks identical to the shell. So we
12
+ # assert on CONTENT and Content-Type, not status alone:
13
+ # - 200 + text/html shell (<!doctype html> / <html) -> PASS (SPA fallback)
14
+ # - 200 + non-HTML body (KEY=VALUE, JSON, PEM, "ref:", binary) -> EXPOSED (real leak)
15
+ # - non-200 -> PASS (not served)
16
+ # A real .env leak is text/plain `KEY=VALUE`; a .git/config is an INI `[core]`
17
+ # block; an id_rsa is a `-----BEGIN ... PRIVATE KEY-----` PEM. None are HTML.
18
+ #
19
+ # Evidence: field reports #305 (32-day credential leak), #303 (methodology
20
+ # exposure), #371 (SPA catch-all status-only false-positive → would-be rollback).
21
+ #
22
+ # Usage:
23
+ # DEPLOY_URL=https://example.com bash docs/patterns/post-deploy-probe.sh
24
+ # DEPLOY_URL=https://example.com DEPLOY_PROBE_EXTRA=$'/admin\n/private.key' bash docs/patterns/post-deploy-probe.sh
25
+
26
+ set -euo pipefail
27
+
28
+ : "${DEPLOY_URL:?DEPLOY_URL is required (e.g. https://example.com)}"
29
+
30
+ # Strip trailing slash for clean URL composition.
31
+ DEPLOY_URL="${DEPLOY_URL%/}"
32
+
33
+ TMP="$(mktemp -t postdeploy-probe.XXXXXX)"
34
+ BODY="$(mktemp -t postdeploy-body.XXXXXX)"
35
+ cleanup() { rm -f "$TMP" "$BODY"; }
36
+ trap cleanup EXIT INT TERM
37
+
38
+ # Fixed denylist — mirrors Step 4.5 in .claude/commands/deploy.md.
39
+ DENYLIST=(
40
+ "/.env"
41
+ "/.env.production"
42
+ "/.env.local"
43
+ "/.git/config"
44
+ "/.git/HEAD"
45
+ "/.claude/agents/silver-surfer-herald.md"
46
+ "/docs/methods/FORGE_KEEPER.md"
47
+ "/HOLOCRON.md"
48
+ "/CHANGELOG.md"
49
+ "/VERSION.md"
50
+ "/package.json"
51
+ "/tsconfig.json"
52
+ "/id_rsa"
53
+ "/.ssh/id_rsa"
54
+ )
55
+
56
+ # Optional extensible denylist (newline-separated).
57
+ if [[ -n "${DEPLOY_PROBE_EXTRA:-}" ]]; then
58
+ while IFS= read -r extra; do
59
+ [[ -n "$extra" ]] && DENYLIST+=("$extra")
60
+ done <<< "$DEPLOY_PROBE_EXTRA"
61
+ fi
62
+
63
+ # Decide whether a fetched path is a REAL leak vs an SPA HTML fallback.
64
+ # Inputs: $1 status, $2 content-type header, body file at $BODY.
65
+ # Echoes "leak" or "ok".
66
+ classify() {
67
+ local status="$1" ctype="$2"
68
+ # Only a 200 can possibly be a leak; anything else is not served.
69
+ [[ "$status" == "200" ]] || { echo "ok"; return; }
70
+
71
+ # Lowercase the content-type for matching.
72
+ local ct; ct="$(printf '%s' "$ctype" | tr '[:upper:]' '[:lower:]')"
73
+
74
+ # An HTML response is the SPA catch-all shell, not the sensitive file. PASS.
75
+ if [[ "$ct" == *"text/html"* ]]; then echo "ok"; return; fi
76
+ # Body-sniff fallback when the server omits/mislabels Content-Type: a leading
77
+ # <!doctype html> or <html is the SPA shell.
78
+ if head -c 256 "$BODY" | tr '[:upper:]' '[:lower:]' | grep -qE '<!doctype html|<html'; then
79
+ echo "ok"; return
80
+ fi
81
+
82
+ # 200 + non-HTML body = the real file is being served. EXPOSED.
83
+ echo "leak"
84
+ }
85
+
86
+ hits=0
87
+ checked=0
88
+
89
+ for path in "${DENYLIST[@]}"; do
90
+ checked=$((checked + 1))
91
+ url="${DEPLOY_URL}${path}"
92
+ # Capture status + content-type, and the body (capped) for sniffing.
93
+ read -r status ctype < <(
94
+ curl -s -o "$BODY" --max-time 10 \
95
+ -w '%{http_code} %{content_type}\n' "$url" 2>/dev/null || echo "000 -"
96
+ )
97
+ verdict="$(classify "$status" "$ctype")"
98
+ if [[ "$verdict" == "leak" ]]; then
99
+ hits=$((hits + 1))
100
+ printf 'LEAK %s %-24s -> %s\n' "$status" "$ctype" "$url" | tee -a "$TMP" >&2
101
+ else
102
+ printf 'ok %s %-24s -> %s\n' "$status" "$ctype" "$url"
103
+ fi
104
+ done
105
+
106
+ printf '{"action":"post-deploy-probe","url":"%s","checked":%d,"hits":%d,"mode":"content-aware"}\n' \
107
+ "$DEPLOY_URL" "$checked" "$hits"
108
+
109
+ if (( hits > 0 )); then
110
+ echo "[post-deploy-probe] ${hits} sensitive path(s) served as non-HTML content. Rollback and fix deploy surface." >&2
111
+ exit 1
112
+ fi
113
+
114
+ echo "[post-deploy-probe] clean (SPA HTML fallbacks treated as PASS)"
115
+ exit 0
@@ -0,0 +1,140 @@
1
+ """
2
+ Pattern: RLS Test Fixture (db_as_app SAVEPOINT)
3
+
4
+ Source: Field report #318 §5. Cara Dune (Union Station, M-05) discovered that
5
+ Testcontainers' default `us_test` user is `SUPERUSER + BYPASSRLS=t`.
6
+ Superusers bypass FORCE RLS at the engine level — the policy doesn't fire.
7
+ Any test using the shared `db` fixture for cross-tenant assertions will
8
+ silently pass even when the policy is broken.
9
+
10
+ Without this pattern, RLS tests that pass in development WILL silently fail
11
+ in production under the runtime non-owner role.
12
+
13
+ Use this pattern in every Python/asyncpg + pytest project with FORCE RLS.
14
+ The same shape ports to SQLAlchemy + sync sessions, psycopg, and Django ORM.
15
+ """
16
+
17
+ import pytest
18
+ import pytest_asyncio
19
+ import asyncpg
20
+ from contextlib import asynccontextmanager
21
+ from typing import AsyncIterator
22
+
23
+ # ── Fixtures ──────────────────────────────────────────────────────────────
24
+
25
+
26
+ @pytest_asyncio.fixture
27
+ async def db_as_app(db: asyncpg.Connection) -> AsyncIterator[asyncpg.Connection]:
28
+ """
29
+ Wrap the standard `db` fixture so RLS-sensitive tests run under the app
30
+ role (BYPASSRLS=f), not the SUPERUSER bootstrap role. Connection state
31
+ is restored on test teardown.
32
+
33
+ Use this fixture for any test that asserts an RLS policy fires. Use the
34
+ standard `db` fixture only for schema setup or admin-only operations.
35
+
36
+ Pairs with a `pg_container_app` fixture (below) that provisions an
37
+ app-level role with `LOGIN NOBYPASSRLS NOSUPERUSER` matching the
38
+ runtime DSN identity.
39
+ """
40
+ await db.execute("SAVEPOINT rls_test")
41
+ try:
42
+ await db.execute(f"SET LOCAL ROLE {APP_ROLE_NAME}")
43
+ # If the test sets a tenant ContextVar, wire it through:
44
+ # await db.execute("SELECT set_config('app.current_org_id', $1, true)", org_id)
45
+ yield db
46
+ finally:
47
+ await db.execute("ROLLBACK TO SAVEPOINT rls_test")
48
+
49
+
50
+ @pytest.fixture(scope="session")
51
+ def app_role_name() -> str:
52
+ return APP_ROLE_NAME
53
+
54
+
55
+ # ── Container provisioning (run once per test session) ────────────────────
56
+
57
+ APP_ROLE_NAME = "unionstation_app" # Match production DSN identity
58
+
59
+
60
+ async def provision_app_role(admin_conn: asyncpg.Connection) -> None:
61
+ """
62
+ Create the runtime non-owner role inside the test container. Mirrors
63
+ production: NOLOGIN if password-less; LOGIN with a fixed test password
64
+ if the test harness needs to connect as this role directly.
65
+
66
+ NOSUPERUSER and NOBYPASSRLS are the load-bearing settings. Without these,
67
+ the role retains FORCE RLS bypass and the fixture buys you nothing.
68
+ """
69
+ await admin_conn.execute(f"""
70
+ DO $$
71
+ BEGIN
72
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{APP_ROLE_NAME}') THEN
73
+ CREATE ROLE {APP_ROLE_NAME}
74
+ LOGIN
75
+ NOSUPERUSER
76
+ NOBYPASSRLS
77
+ NOCREATEDB
78
+ NOCREATEROLE
79
+ PASSWORD 'test_app_password';
80
+ GRANT USAGE ON SCHEMA public TO {APP_ROLE_NAME};
81
+ GRANT SELECT, INSERT, UPDATE, DELETE
82
+ ON ALL TABLES IN SCHEMA public TO {APP_ROLE_NAME};
83
+ GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO {APP_ROLE_NAME};
84
+ END IF;
85
+ END $$;
86
+ """)
87
+
88
+
89
+ # ── Usage example ─────────────────────────────────────────────────────────
90
+
91
+
92
+ @pytest.mark.asyncio
93
+ async def test_rls_blocks_cross_org_select(db_as_app: asyncpg.Connection) -> None:
94
+ """
95
+ Use db_as_app — NOT db — for any RLS-policy assertion. Under the
96
+ SUPERUSER `db` fixture, this test would pass even if the policy were
97
+ deleted.
98
+ """
99
+ await db_as_app.execute(
100
+ "SELECT set_config('app.current_org_id', '1', true)"
101
+ )
102
+ rows = await db_as_app.fetch("SELECT id, org_id FROM people")
103
+ assert all(row["org_id"] == 1 for row in rows), \
104
+ "RLS allowed cross-org rows under FORCE — policy is broken or role has BYPASSRLS=t"
105
+
106
+
107
+ # ── Asynccontextmanager variant for non-pytest contexts ───────────────────
108
+
109
+
110
+ @asynccontextmanager
111
+ async def as_app_role(conn: asyncpg.Connection) -> AsyncIterator[asyncpg.Connection]:
112
+ """
113
+ Imperative variant of the fixture for scripts and one-off RLS exercises.
114
+ """
115
+ await conn.execute("SAVEPOINT as_app_role")
116
+ try:
117
+ await conn.execute(f"SET LOCAL ROLE {APP_ROLE_NAME}")
118
+ yield conn
119
+ finally:
120
+ await conn.execute("ROLLBACK TO SAVEPOINT as_app_role")
121
+
122
+
123
+ # ── Anti-patterns ─────────────────────────────────────────────────────────
124
+ #
125
+ # 1. Using `db` fixture for RLS assertions. SUPERUSER bypass means
126
+ # every test passes regardless of policy correctness. Production blows up.
127
+ #
128
+ # 2. Provisioning the app role with BYPASSRLS=t "for convenience." Defeats
129
+ # the entire FORCE RLS deployment.
130
+ #
131
+ # 3. SET ROLE without SAVEPOINT. Test pollution: subsequent tests run under
132
+ # whichever role the previous test ended in.
133
+ #
134
+ # 4. Skipping ROLLBACK TO SAVEPOINT in the finally branch. Connection
135
+ # pooling will hand out a connection still scoped to APP_ROLE_NAME.
136
+ #
137
+ # 5. SET LOCAL ROLE inside an asyncpg pool callback (which runs outside any
138
+ # transaction). Use SET ROLE (session-scoped) plus explicit RESET ROLE
139
+ # on connection release. See field report #319 §1 — same trap surfaced
140
+ # in M-04c W3.
@@ -0,0 +1,134 @@
1
+ """
2
+ Pattern: Structural SQL Sentinel (with adversarial-test discipline)
3
+
4
+ Source: Field report #319 §3. V083 sentinel #2 originally used a single regex
5
+ matching `current_setting(...) = ''`. Three Wave 3 reviewers independently
6
+ flagged that the regex misses commuted (`'' = current_setting(...)`),
7
+ cast (`current_setting(...)::text = ''`), IS NULL canonical
8
+ (`current_setting(...) IS NULL`), and coalesce-wrapped variants. Each missed
9
+ form is a future fail-open re-introduction the sentinel is supposed to block.
10
+
11
+ A single-form structural sentinel is a single point of failure. This pattern
12
+ documents the discipline: every structural sentinel has positive controls,
13
+ adversarial alternation tests, AND fixture-bindability proof.
14
+
15
+ Use this pattern for any SQL-shape policing — fail-open detection in RLS
16
+ policies, dangerous catalog reads, deprecated function calls, plaintext
17
+ storage in encrypted columns.
18
+ """
19
+
20
+ import re
21
+ import pytest
22
+ from typing import Iterable
23
+
24
+
25
+ # ── The Sentinel ──────────────────────────────────────────────────────────
26
+
27
+ # Comprehensive regex that matches all known fail-open forms. Each
28
+ # alternation is a CVE-class pattern that has bitten production at least once.
29
+ FAIL_OPEN_RE = re.compile(
30
+ r"""
31
+ (
32
+ # Direct equality
33
+ current_setting\([^)]*\)\s*=\s*'' |
34
+ # Commuted (Postgres doesn't canonicalize operand order)
35
+ ''\s*=\s*current_setting\([^)]*\) |
36
+ # Cast on the function call
37
+ current_setting\([^)]*\)\s*::\s*\w+\s*=\s*'' |
38
+ # IS NULL form (treats unset GUC as fail-open)
39
+ current_setting\([^)]*\)\s*IS\s+NULL |
40
+ # COALESCE wrap
41
+ coalesce\(\s*current_setting\([^)]*\)\s*,\s*''\s*\)\s*=\s*''
42
+ )
43
+ """,
44
+ re.IGNORECASE | re.VERBOSE,
45
+ )
46
+
47
+
48
+ def policy_is_fail_open(policy_qual: str) -> bool:
49
+ """Return True if the policy expression contains a known fail-open arm."""
50
+ return bool(FAIL_OPEN_RE.search(policy_qual))
51
+
52
+
53
+ # ── Positive controls (must trigger) ─────────────────────────────────────
54
+
55
+ POSITIVE_FORMS = [
56
+ # Direct
57
+ "current_setting('app.current_org_id', true) = ''",
58
+ # Whitespace tolerance
59
+ "current_setting('app.current_org_id', true) = ''",
60
+ # Commuted
61
+ "'' = current_setting('app.current_org_id', true)",
62
+ # Cast
63
+ "current_setting('app.current_org_id', true)::text = ''",
64
+ # IS NULL
65
+ "current_setting('app.current_org_id', true) IS NULL",
66
+ # COALESCE wrap
67
+ "coalesce(current_setting('app.current_org_id', true), '') = ''",
68
+ ]
69
+
70
+
71
+ # ── Negative controls (must NOT trigger) ──────────────────────────────────
72
+
73
+ NEGATIVE_FORMS = [
74
+ # The legitimate org_id check the sentinel is protecting
75
+ "org_id = current_setting('app.current_org_id', true)::int",
76
+ "org_id::text = current_setting('app.current_org_id', true)",
77
+ # Other unrelated comparisons
78
+ "deleted_at IS NULL",
79
+ "tenant_id = (SELECT id FROM tenants WHERE name = 'system')",
80
+ ]
81
+
82
+
83
+ # ── Adversarial-bindability test ──────────────────────────────────────────
84
+
85
+
86
+ @pytest.mark.parametrize("form", POSITIVE_FORMS)
87
+ def test_sentinel_catches_fail_open_form(form: str) -> None:
88
+ """Every known fail-open variant must trip the sentinel."""
89
+ assert policy_is_fail_open(form), \
90
+ f"SENTINEL GAP: form did not trip — '{form}'"
91
+
92
+
93
+ @pytest.mark.parametrize("form", NEGATIVE_FORMS)
94
+ def test_sentinel_does_not_false_positive(form: str) -> None:
95
+ """Legitimate policy expressions must not trip the sentinel."""
96
+ assert not policy_is_fail_open(form), \
97
+ f"FALSE POSITIVE: legitimate form tripped — '{form}'"
98
+
99
+
100
+ # ── Fixture-bindability proof ─────────────────────────────────────────────
101
+ #
102
+ # A structural sentinel is meaningful only if it can FAIL on a deliberate
103
+ # regression. Test that, too:
104
+
105
+
106
+ def test_sentinel_can_bind() -> None:
107
+ """
108
+ Construct a deliberate regression and assert the sentinel catches it.
109
+ If this assertion ever flips, either the regex was changed silently
110
+ or the fail-open form is no longer detectable. Either way, an alert
111
+ is mandatory.
112
+ """
113
+ deliberate_regression = "current_setting('x', true) = ''"
114
+ assert policy_is_fail_open(deliberate_regression), \
115
+ "BINDABILITY FAILURE: sentinel cannot fail under any input — it's a no-op"
116
+
117
+
118
+ # ── Anti-patterns ─────────────────────────────────────────────────────────
119
+ #
120
+ # 1. SUBSTRING match instead of regex (LIKE '%...%'). Misses commuted, cast,
121
+ # and IS NULL forms. Field report #319 §3.
122
+ #
123
+ # 2. Single regex variant without alternation. The first migration author who
124
+ # writes a different form silently re-introduces the class.
125
+ #
126
+ # 3. Positive controls only. Without negative controls, false positives
127
+ # flood the alert channel and reviewers learn to ignore them.
128
+ #
129
+ # 4. No bindability proof. A sentinel that algebraically cannot fail is a
130
+ # no-op. See /docs/patterns/adr-verification-gate.md.
131
+ #
132
+ # 5. Sentinel lives in one place (CI grep) without a database-side mirror.
133
+ # Belt-and-suspenders: lint the policy text in CI AND assert
134
+ # policy_is_fail_open() against pg_policies.qual at runtime.
@@ -0,0 +1,38 @@
1
+ # Context Meter — status line + awareness hook
2
+
3
+ Two small scripts that surface how full the context window is — one for the human, one for the model.
4
+
5
+ | Script | Wired to | Audience | What it does |
6
+ |--------|----------|----------|--------------|
7
+ | `voidforge-statusline.sh` | `statusLine` (settings.json) | you | Renders one line: model + a colored meter (`⟦████████░░⟧ 78%`) + tokens remaining. Green → yellow → red as the window fills. |
8
+ | `context-awareness-hook.sh` | `UserPromptSubmit` hook | Claude | Once usage crosses a threshold, injects "you have ~X% left, checkpoint soon" into the model's own context each turn. Silent below the threshold. |
9
+
10
+ The model can't see its own remaining context. The status line tells *you*; the hook tells *Claude* — so it can wrap up open loops and suggest `/vault` or `/seal` before compaction instead of being surprised by it.
11
+
12
+ ## Install
13
+
14
+ **Default-on.** `npx voidforge-build init` already wires both scripts into a new project's `.claude/settings.json` (warn 80% / crit 92%). Nothing to do for a fresh project.
15
+
16
+ To re-install, retune, or activate on a project that predates this feature, run **`/contextmeter`** — it chmods these scripts and merges the right block into `.claude/settings.json`. Or wire it by hand: merge `settings-snippet.json` into `.claude/settings.json`. Remove with `/contextmeter --uninstall`.
17
+
18
+ ## How it reads context
19
+
20
+ - **Status line:** prefers the native `context_window` object Claude Code pipes on stdin (`used_percentage`, `context_window_size`). Falls back to deriving usage from the most recent assistant `message.usage` in `transcript_path` on older Claude Code that doesn't send the field.
21
+ - **Hook:** the hook stdin has no `context_window` object, so it always derives from `transcript_path` (`input_tokens + cache_read_input_tokens + cache_creation_input_tokens`).
22
+ - 1M-token sessions are detected automatically (usage above 200k ⇒ 1,000,000 denominator), or set `VOIDFORGE_CONTEXT_WINDOW`.
23
+
24
+ ## Tuning (env)
25
+
26
+ | Var | Default | Effect |
27
+ |-----|---------|--------|
28
+ | `VOIDFORGE_CONTEXT_WINDOW` | `200000` | Denominator when the size field is absent. |
29
+ | `VOIDFORGE_CONTEXT_WARN_PCT` | `80` | Hook starts speaking — and the meter turns yellow — at this % used. |
30
+ | `VOIDFORGE_CONTEXT_CRIT_PCT` | `92` | Hook escalates to "checkpoint NOW" — and the meter turns red — at this %. |
31
+
32
+ Both scripts read the same two thresholds, so the meter's yellow/red bands stay in lockstep with the hook's warn/critical bands. `/contextmeter --warn-pct N` / `--crit-pct N` bake these into the command strings in settings.json so they persist without a shell export.
33
+
34
+ ## Requirements & caveats
35
+
36
+ - **`jq`** is required. Without it the status line prints a one-line "install jq" notice and the hook no-ops — neither ever breaks your session.
37
+ - Only the **first line** of status-line stdout is shown by Claude Code, so the meter is deliberately single-line.
38
+ - **Name:** this ships as `/contextmeter`, not `/statusline` — Claude Code's native `/statusline` and `/context` commands always shadow a same-named project command (see `docs/NATIVE_CAPABILITIES.md`).