Skip to content

docs: use bawbel.io@gmail.com as the security and conduct contact- #68 - #73

Merged
chaksaray merged 32 commits into
mainfrom
develop
Oct 18, 2026
Merged

chaksaray merged 32 commits into
mainfrom
develop

Conversation

@chaksaray

Copy link
Copy Markdown
Member

No description provided.

chaksaray and others added 30 commits July 6, 2026 11:20
* feat: governance

* fix: add argument_guards to manifestStanza fragment type

manifestStanza in $defs (DESIGN.md 4.2) was missing argument_guards,
which the ave-mitigations spec (Section 3) includes in the stanza shape.
Both must agree on mergeable keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(M0): project scaffold, three schemas, AVE 1.2 migration script

- pyproject.toml + pre-commit (ruff, bandit, json/yaml checks)
- _const.py: all magic strings, effect order, provenance classes, deny reasons
- _schemas.py: cached schema loader, BAWBEL_SCHEMAS_DIR env override
- schemas/ave-1.2.json: AVE record schema with provenance_vector,
  trifecta_profile, capability_mitigation as nullable optional fields
- schemas/capability-manifest-v1.json: verbatim DESIGN.md 4.2 plus
  $defs/manifestStanza (fragment type for harden --ave stanzas)
- schemas/bawbel-ave-mitigations-v1.json: envelope for ave-mitigations.json;
  reviewed entries require reviewed_by, reviewed_at, rule_ids
- scripts/migrate_ave_v1_2.py: null-insert migration CLI, idempotent,
  --dry-run, validates output against ave-1.2.json
- tests/: 36 tests covering schema validity, example fixtures, migration
  (unit + CLI integration, idempotency, dry-run, error paths)
- corpus fixtures: v1.1 and v1.2 sample records + github-mcp manifest

* feat(M1): multiplexer skeleton, audit chain, learning mode, CLI

- audit/canonical.py: single canonical JSON implementation (I6 invariant)
- audit/writer.py: thread-safe hash-chained JSONL writer + chain verifier (I7)
- mux/config.py: gate.yaml loader with fail-fast validation, ApprovalConfig
  enforces on_timeout=deny only (no fail-open path)
- mux/session.py: monotone SessionState, with_taint() for property tests (I2/I3)
- mux/namespace.py: tool namespacing {server}__{tool} both directions
- mux/transport.py: stdio JSON-RPC read/write, spawn_server subprocess helper
- learn/recorder.py: thread-safe tool call recorder + JSONL flush
- learn/report.py: per-server usage table renderer
- learn/synthesize.py: draft manifest generator (write tools -> approve,
  common arg prefix conditions, wildcard deny, DRAFT banner)
- cli.py: full CLI surface (serve, learn, audit, clear, lint, harden stubs)
- pyproject.toml: requires-python lowered to >=3.10 (runtime is 3.10.12)
- 34 new tests: canonical, chain writer, chain verifier, session state,
  namespace, config loader, recorder, synthesizer

* M2: enforcement core — policy engine, taint, trifecta invariant, approval channel

Implements the full enforcement stack per DESIGN.md sections 5-7:

- policy/manifest.py: Manifest dataclasses + YAML loader with JSON Schema
  validation, secret scanning (AWS/GitLab/Vault/PEM/JWT patterns + Shannon
  entropy), argument guards, taint rules with wildcard prefix matching
- policy/engine.py: 6-step resolve() algorithm from DESIGN.md 5.2; lattice_min()
  combiner; PERMISSIVENESS exported for I2 property tests; fail-closed on every
  unmatched, erroring, or condition-failing path
- policy/approval.py: terminal approval channel; secret-redacted arg display;
  timeout -> deny; injected input_fn for test isolation; uses canonical_str()
  not raw json.dumps to respect invariant I6
- audit/writer.py: AuditWriter.from_existing() for SESSION_CLEAR chain resumption
- cli.py: SESSION_CLEAR writes audited record continuing the existing chain

Tests (151 total, all passing):
- tests/property/test_invariants.py: 14 Hypothesis property tests for I1-I5
- tests/replay/test_github_mcp_exfil.py: 11 replay tests scripting the 2025
  GitHub-MCP exfil chain (poisoned issue -> private read -> PR to fork), each
  of the three enforcement layers asserted independently
- tests/test_m2_policy.py: 32 unit tests (lattice, grants, conditions, guards,
  taint rules, trifecta, JSON-RPC error format, manifest loader)
- tests/test_m2_approval.py: 24 tests (arg redaction, prompt format, y/n/timeout
  paths, SESSION_CLEAR chain continuity)

Lint: pre-commit clean (ruff preview, bandit pass_filenames:false, E501 fixed
across all source and test files; B105/B404 nosec annotations in const/transport)

* M3: integrity pinning, harden --ave, rug-pull replay test, verify CLI

Tool schema pinning per DESIGN.md 8.1:
- integrity/pinning.py: pin_tool_schema() sorts tools by name before
  hashing (order-independent), uses canonical_bytes (I6), detect_drift()
  returns DriftResult with both hashes for operator diff
- integrity/harden.py: apply_harden() loads stanza from ave-mitigations.json,
  blocks unreviewed stanzas unless --allow-unreviewed, merge_stanza() appends
  taint_rules/integrity_watch/argument_guards idempotently, computes YAML diff
- mitigations/ave-mitigations.json: initial content for AVE-2026-00041 and
  AVE-2026-00042, both reviewed, with manifest_stanza ready for harden
- cli.py: verify command (check pin, --accept to repin); harden command (full
  implementation replacing M2 stub: load stanza, show diff, --write to apply)

Tests (203 total, all passing):
- tests/property/test_integrity.py: 12 property tests (pin stability, format,
  drift detection determinism, key-order independence, tool-order normalisation)
- tests/replay/test_tool_schema_drift.py: 13 replay tests scripting AVE-2026-00042
  rug pull pattern (pin at VERIFY, poison detected, server suspended, calls denied),
  end-to-end chain asserted atomically
- tests/test_m3_integrity.py: 27 unit tests (pinning, stanza load/merge, verify
  and harden CLI including write, --allow-unreviewed, missing AVE error paths)

Lint fixes: SIM102 in manifest.py (nested-if collapsed), E501 in test files and
policy/manifest.py; all pre-commit hooks passing.

* M4: benign corpus, approval budget, and latency gates

- Add tests/corpus/sessions/github_daily_dev.jsonl (10 benign records,
  5 sessions covering get_issue and create_pull_request flows)
- Add tests/test_m4_corpus.py: p95=0 approval prompts, p99<=1 approval
  prompts, p95 latency <=5ms, and 5 static security invariant checks
- Fix E501 in test_m3_integrity, test_integrity, test_tool_schema_drift
  and harden.py (ruff auto-fix + manual splits)

213 tests passing; pre-commit clean.

* M5: Operations layer -- OTel spans, CEF/syslog, posture report

- Add src/bawbel_gate/ops/ module:
  otel.py: record_decision_span + emit_alert_event; graceful noop when
    opentelemetry-sdk is not installed; setup_otel reads
    OTEL_EXPORTER_OTLP_ENDPOINT
  syslog_cef.py: format_cef_record for all 6 alertable event classes per
    DESIGN.md 8.5; ArcSight CEF v0 format; escapes |, =, backslash, newlines
  posture.py: compute_posture (parse audit JSONL) + render_posture_report
    (text or JSON); PostureError on missing log
- Add 'posture' CLI command (--audit-log, --json)
- Add alertable event constants and CEF severity map to _const.py
- Add optional [ops] extra in pyproject.toml for opentelemetry deps
- 35 new tests in tests/test_m5_ops.py covering constants, CEF format,
  posture computation, OTel noop, and posture CLI

248 tests passing; pre-commit clean.

* M6: Embedded console + Console API

- Add src/bawbel_gate/console/ module:
  auth.py: mint_token (256-bit random hex) + verify_token (hmac.compare_digest)
  state.py: ConsoleState + serialize_state for GET /v1/state shape per DESIGN.md 13.1
  approvals.py: ApprovalRegistry with first-writer-wins race semantics (thread-safe)
  server.py: ConsoleServer (stdlib http.server) serving:
    GET /v1/state (auth required, full snapshot)
    GET /v1/events (SSE stream of audit records)
    POST /v1/approvals/{id} (console approval channel; 409 on second write)
    GET / (embedded UI bundle; no build-time network fetch)
    all other paths -> 404 (enforces read-only property)
- Add --console HOST:PORT flag to 'serve' command
- 31 new tests in tests/test_m6_console.py:
  bearer token, state serializer, approval registry races, HTTP endpoints,
  read-only guarantee (no manifest/taint/tool-call mutation paths)

279 tests passing; pre-commit clean.

* M7: bawbel-hub fleet ingest, state store, enrollment, and posture

- Add src/bawbel_gate/hub/ module:
  store.py: SQLite fleet state store (idempotent upsert by gate/session/seq,
    rebuild from records, chain head tracking, quarantine table)
  ingest.py: process_batch() with per-session chain verification; raises
    ChainMismatch on tamper evidence (-> 409); replays are idempotent
  enroll.py: EnrollmentRegistry with single-use tokens and admin tokens
  fleet.py: compute_fleet_posture() -> list[GatePostureRow] for fleet console
  server.py: HubServer with:
    GET  /v1/fleet/state       -- fleet posture snapshot
    GET  /v1/fleet/gates/{id}  -- per-gate state
    POST /v1/gates/{id}/records -- NDJSON ingest (chain-verified)
    POST /v1/enrollment/tokens  -- mint enrollment token
    POST /v1/enroll             -- consume token, get gate_id
    (no command channel; all other paths -> 404, DESIGN.md 14.6)
- Add src/bawbel_gate/ingest/ module:
  client.py: IngestCursor (cursor file per session), build_batch() (reads
    audit JSONL past the acked cursor, max_records cap)
- CLI: 'enroll' command, 'hub serve' + 'hub token new' subcommands
- 36 new tests in tests/test_m7_hub.py covering store, chain verification,
  enrollment lifecycle, fleet posture, HTTP server, ingest client

315 tests passing; pre-commit clean.

* Update README to reflect M0-M7 feature-complete state; fix E501 in test_m7_hub

Rewrites README as the authoritative operator reference covering all 8 milestones.
Fixes two E501 violations in tests/test_m7_hub.py (lines 110, 131) surfaced by pre-commit.
* feat: governance

* fix: add argument_guards to manifestStanza fragment type

manifestStanza in $defs (DESIGN.md 4.2) was missing argument_guards,
which the ave-mitigations spec (Section 3) includes in the stanza shape.
Both must agree on mergeable keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(M0): project scaffold, three schemas, AVE 1.2 migration script

- pyproject.toml + pre-commit (ruff, bandit, json/yaml checks)
- _const.py: all magic strings, effect order, provenance classes, deny reasons
- _schemas.py: cached schema loader, BAWBEL_SCHEMAS_DIR env override
- schemas/ave-1.2.json: AVE record schema with provenance_vector,
  trifecta_profile, capability_mitigation as nullable optional fields
- schemas/capability-manifest-v1.json: verbatim DESIGN.md 4.2 plus
  $defs/manifestStanza (fragment type for harden --ave stanzas)
- schemas/bawbel-ave-mitigations-v1.json: envelope for ave-mitigations.json;
  reviewed entries require reviewed_by, reviewed_at, rule_ids
- scripts/migrate_ave_v1_2.py: null-insert migration CLI, idempotent,
  --dry-run, validates output against ave-1.2.json
- tests/: 36 tests covering schema validity, example fixtures, migration
  (unit + CLI integration, idempotency, dry-run, error paths)
- corpus fixtures: v1.1 and v1.2 sample records + github-mcp manifest

* feat(M1): multiplexer skeleton, audit chain, learning mode, CLI

- audit/canonical.py: single canonical JSON implementation (I6 invariant)
- audit/writer.py: thread-safe hash-chained JSONL writer + chain verifier (I7)
- mux/config.py: gate.yaml loader with fail-fast validation, ApprovalConfig
  enforces on_timeout=deny only (no fail-open path)
- mux/session.py: monotone SessionState, with_taint() for property tests (I2/I3)
- mux/namespace.py: tool namespacing {server}__{tool} both directions
- mux/transport.py: stdio JSON-RPC read/write, spawn_server subprocess helper
- learn/recorder.py: thread-safe tool call recorder + JSONL flush
- learn/report.py: per-server usage table renderer
- learn/synthesize.py: draft manifest generator (write tools -> approve,
  common arg prefix conditions, wildcard deny, DRAFT banner)
- cli.py: full CLI surface (serve, learn, audit, clear, lint, harden stubs)
- pyproject.toml: requires-python lowered to >=3.10 (runtime is 3.10.12)
- 34 new tests: canonical, chain writer, chain verifier, session state,
  namespace, config loader, recorder, synthesizer

* M2: enforcement core — policy engine, taint, trifecta invariant, approval channel

Implements the full enforcement stack per DESIGN.md sections 5-7:

- policy/manifest.py: Manifest dataclasses + YAML loader with JSON Schema
  validation, secret scanning (AWS/GitLab/Vault/PEM/JWT patterns + Shannon
  entropy), argument guards, taint rules with wildcard prefix matching
- policy/engine.py: 6-step resolve() algorithm from DESIGN.md 5.2; lattice_min()
  combiner; PERMISSIVENESS exported for I2 property tests; fail-closed on every
  unmatched, erroring, or condition-failing path
- policy/approval.py: terminal approval channel; secret-redacted arg display;
  timeout -> deny; injected input_fn for test isolation; uses canonical_str()
  not raw json.dumps to respect invariant I6
- audit/writer.py: AuditWriter.from_existing() for SESSION_CLEAR chain resumption
- cli.py: SESSION_CLEAR writes audited record continuing the existing chain

Tests (151 total, all passing):
- tests/property/test_invariants.py: 14 Hypothesis property tests for I1-I5
- tests/replay/test_github_mcp_exfil.py: 11 replay tests scripting the 2025
  GitHub-MCP exfil chain (poisoned issue -> private read -> PR to fork), each
  of the three enforcement layers asserted independently
- tests/test_m2_policy.py: 32 unit tests (lattice, grants, conditions, guards,
  taint rules, trifecta, JSON-RPC error format, manifest loader)
- tests/test_m2_approval.py: 24 tests (arg redaction, prompt format, y/n/timeout
  paths, SESSION_CLEAR chain continuity)

Lint: pre-commit clean (ruff preview, bandit pass_filenames:false, E501 fixed
across all source and test files; B105/B404 nosec annotations in const/transport)

* M3: integrity pinning, harden --ave, rug-pull replay test, verify CLI

Tool schema pinning per DESIGN.md 8.1:
- integrity/pinning.py: pin_tool_schema() sorts tools by name before
  hashing (order-independent), uses canonical_bytes (I6), detect_drift()
  returns DriftResult with both hashes for operator diff
- integrity/harden.py: apply_harden() loads stanza from ave-mitigations.json,
  blocks unreviewed stanzas unless --allow-unreviewed, merge_stanza() appends
  taint_rules/integrity_watch/argument_guards idempotently, computes YAML diff
- mitigations/ave-mitigations.json: initial content for AVE-2026-00041 and
  AVE-2026-00042, both reviewed, with manifest_stanza ready for harden
- cli.py: verify command (check pin, --accept to repin); harden command (full
  implementation replacing M2 stub: load stanza, show diff, --write to apply)

Tests (203 total, all passing):
- tests/property/test_integrity.py: 12 property tests (pin stability, format,
  drift detection determinism, key-order independence, tool-order normalisation)
- tests/replay/test_tool_schema_drift.py: 13 replay tests scripting AVE-2026-00042
  rug pull pattern (pin at VERIFY, poison detected, server suspended, calls denied),
  end-to-end chain asserted atomically
- tests/test_m3_integrity.py: 27 unit tests (pinning, stanza load/merge, verify
  and harden CLI including write, --allow-unreviewed, missing AVE error paths)

Lint fixes: SIM102 in manifest.py (nested-if collapsed), E501 in test files and
policy/manifest.py; all pre-commit hooks passing.

* M4: benign corpus, approval budget, and latency gates

- Add tests/corpus/sessions/github_daily_dev.jsonl (10 benign records,
  5 sessions covering get_issue and create_pull_request flows)
- Add tests/test_m4_corpus.py: p95=0 approval prompts, p99<=1 approval
  prompts, p95 latency <=5ms, and 5 static security invariant checks
- Fix E501 in test_m3_integrity, test_integrity, test_tool_schema_drift
  and harden.py (ruff auto-fix + manual splits)

213 tests passing; pre-commit clean.

* M5: Operations layer -- OTel spans, CEF/syslog, posture report

- Add src/bawbel_gate/ops/ module:
  otel.py: record_decision_span + emit_alert_event; graceful noop when
    opentelemetry-sdk is not installed; setup_otel reads
    OTEL_EXPORTER_OTLP_ENDPOINT
  syslog_cef.py: format_cef_record for all 6 alertable event classes per
    DESIGN.md 8.5; ArcSight CEF v0 format; escapes |, =, backslash, newlines
  posture.py: compute_posture (parse audit JSONL) + render_posture_report
    (text or JSON); PostureError on missing log
- Add 'posture' CLI command (--audit-log, --json)
- Add alertable event constants and CEF severity map to _const.py
- Add optional [ops] extra in pyproject.toml for opentelemetry deps
- 35 new tests in tests/test_m5_ops.py covering constants, CEF format,
  posture computation, OTel noop, and posture CLI

248 tests passing; pre-commit clean.

* M6: Embedded console + Console API

- Add src/bawbel_gate/console/ module:
  auth.py: mint_token (256-bit random hex) + verify_token (hmac.compare_digest)
  state.py: ConsoleState + serialize_state for GET /v1/state shape per DESIGN.md 13.1
  approvals.py: ApprovalRegistry with first-writer-wins race semantics (thread-safe)
  server.py: ConsoleServer (stdlib http.server) serving:
    GET /v1/state (auth required, full snapshot)
    GET /v1/events (SSE stream of audit records)
    POST /v1/approvals/{id} (console approval channel; 409 on second write)
    GET / (embedded UI bundle; no build-time network fetch)
    all other paths -> 404 (enforces read-only property)
- Add --console HOST:PORT flag to 'serve' command
- 31 new tests in tests/test_m6_console.py:
  bearer token, state serializer, approval registry races, HTTP endpoints,
  read-only guarantee (no manifest/taint/tool-call mutation paths)

279 tests passing; pre-commit clean.

* M7: bawbel-hub fleet ingest, state store, enrollment, and posture

- Add src/bawbel_gate/hub/ module:
  store.py: SQLite fleet state store (idempotent upsert by gate/session/seq,
    rebuild from records, chain head tracking, quarantine table)
  ingest.py: process_batch() with per-session chain verification; raises
    ChainMismatch on tamper evidence (-> 409); replays are idempotent
  enroll.py: EnrollmentRegistry with single-use tokens and admin tokens
  fleet.py: compute_fleet_posture() -> list[GatePostureRow] for fleet console
  server.py: HubServer with:
    GET  /v1/fleet/state       -- fleet posture snapshot
    GET  /v1/fleet/gates/{id}  -- per-gate state
    POST /v1/gates/{id}/records -- NDJSON ingest (chain-verified)
    POST /v1/enrollment/tokens  -- mint enrollment token
    POST /v1/enroll             -- consume token, get gate_id
    (no command channel; all other paths -> 404, DESIGN.md 14.6)
- Add src/bawbel_gate/ingest/ module:
  client.py: IngestCursor (cursor file per session), build_batch() (reads
    audit JSONL past the acked cursor, max_records cap)
- CLI: 'enroll' command, 'hub serve' + 'hub token new' subcommands
- 36 new tests in tests/test_m7_hub.py covering store, chain verification,
  enrollment lifecycle, fleet posture, HTTP server, ingest client

315 tests passing; pre-commit clean.

* Update README to reflect M0-M7 feature-complete state; fix E501 in test_m7_hub

Rewrites README as the authoritative operator reference covering all 8 milestones.
Fixes two E501 violations in tests/test_m7_hub.py (lines 110, 131) surfaced by pre-commit.

* Remove hard-coded network/timing values; add env var overrides for all runtime tunables

All default ports, hosts, and internal timing values moved to _const.py.
CLI options for console addr, hub host/port, hub db, and audit log path now
accept env vars (BAWBEL_CONSOLE_ADDR, BAWBEL_HUB_HOST, BAWBEL_HUB_PORT,
BAWBEL_HUB_DB, BAWBEL_AUDIT_LOG, OTEL_EXPORTER_OTLP_ENDPOINT already existed).
Adds .env.example documenting every supported env var. Adds .env and *.db to
.gitignore so local runtime files are never committed.

* Add complete step-by-step testing guide (docs/TESTING.md)

* Remove .env.example; document env vars inline in TESTING.md

.env files are readable by subprocesses and unsuitable for a security product.
Env var reference now lives in docs/TESTING.md and README only.
.env stays in .gitignore as defence-in-depth.
…#3)

* feat: governance

* fix: add argument_guards to manifestStanza fragment type

manifestStanza in $defs (DESIGN.md 4.2) was missing argument_guards,
which the ave-mitigations spec (Section 3) includes in the stanza shape.
Both must agree on mergeable keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(M0): project scaffold, three schemas, AVE 1.2 migration script

- pyproject.toml + pre-commit (ruff, bandit, json/yaml checks)
- _const.py: all magic strings, effect order, provenance classes, deny reasons
- _schemas.py: cached schema loader, BAWBEL_SCHEMAS_DIR env override
- schemas/ave-1.2.json: AVE record schema with provenance_vector,
  trifecta_profile, capability_mitigation as nullable optional fields
- schemas/capability-manifest-v1.json: verbatim DESIGN.md 4.2 plus
  $defs/manifestStanza (fragment type for harden --ave stanzas)
- schemas/bawbel-ave-mitigations-v1.json: envelope for ave-mitigations.json;
  reviewed entries require reviewed_by, reviewed_at, rule_ids
- scripts/migrate_ave_v1_2.py: null-insert migration CLI, idempotent,
  --dry-run, validates output against ave-1.2.json
- tests/: 36 tests covering schema validity, example fixtures, migration
  (unit + CLI integration, idempotency, dry-run, error paths)
- corpus fixtures: v1.1 and v1.2 sample records + github-mcp manifest

* feat(M1): multiplexer skeleton, audit chain, learning mode, CLI

- audit/canonical.py: single canonical JSON implementation (I6 invariant)
- audit/writer.py: thread-safe hash-chained JSONL writer + chain verifier (I7)
- mux/config.py: gate.yaml loader with fail-fast validation, ApprovalConfig
  enforces on_timeout=deny only (no fail-open path)
- mux/session.py: monotone SessionState, with_taint() for property tests (I2/I3)
- mux/namespace.py: tool namespacing {server}__{tool} both directions
- mux/transport.py: stdio JSON-RPC read/write, spawn_server subprocess helper
- learn/recorder.py: thread-safe tool call recorder + JSONL flush
- learn/report.py: per-server usage table renderer
- learn/synthesize.py: draft manifest generator (write tools -> approve,
  common arg prefix conditions, wildcard deny, DRAFT banner)
- cli.py: full CLI surface (serve, learn, audit, clear, lint, harden stubs)
- pyproject.toml: requires-python lowered to >=3.10 (runtime is 3.10.12)
- 34 new tests: canonical, chain writer, chain verifier, session state,
  namespace, config loader, recorder, synthesizer

* M2: enforcement core — policy engine, taint, trifecta invariant, approval channel

Implements the full enforcement stack per DESIGN.md sections 5-7:

- policy/manifest.py: Manifest dataclasses + YAML loader with JSON Schema
  validation, secret scanning (AWS/GitLab/Vault/PEM/JWT patterns + Shannon
  entropy), argument guards, taint rules with wildcard prefix matching
- policy/engine.py: 6-step resolve() algorithm from DESIGN.md 5.2; lattice_min()
  combiner; PERMISSIVENESS exported for I2 property tests; fail-closed on every
  unmatched, erroring, or condition-failing path
- policy/approval.py: terminal approval channel; secret-redacted arg display;
  timeout -> deny; injected input_fn for test isolation; uses canonical_str()
  not raw json.dumps to respect invariant I6
- audit/writer.py: AuditWriter.from_existing() for SESSION_CLEAR chain resumption
- cli.py: SESSION_CLEAR writes audited record continuing the existing chain

Tests (151 total, all passing):
- tests/property/test_invariants.py: 14 Hypothesis property tests for I1-I5
- tests/replay/test_github_mcp_exfil.py: 11 replay tests scripting the 2025
  GitHub-MCP exfil chain (poisoned issue -> private read -> PR to fork), each
  of the three enforcement layers asserted independently
- tests/test_m2_policy.py: 32 unit tests (lattice, grants, conditions, guards,
  taint rules, trifecta, JSON-RPC error format, manifest loader)
- tests/test_m2_approval.py: 24 tests (arg redaction, prompt format, y/n/timeout
  paths, SESSION_CLEAR chain continuity)

Lint: pre-commit clean (ruff preview, bandit pass_filenames:false, E501 fixed
across all source and test files; B105/B404 nosec annotations in const/transport)

* M3: integrity pinning, harden --ave, rug-pull replay test, verify CLI

Tool schema pinning per DESIGN.md 8.1:
- integrity/pinning.py: pin_tool_schema() sorts tools by name before
  hashing (order-independent), uses canonical_bytes (I6), detect_drift()
  returns DriftResult with both hashes for operator diff
- integrity/harden.py: apply_harden() loads stanza from ave-mitigations.json,
  blocks unreviewed stanzas unless --allow-unreviewed, merge_stanza() appends
  taint_rules/integrity_watch/argument_guards idempotently, computes YAML diff
- mitigations/ave-mitigations.json: initial content for AVE-2026-00041 and
  AVE-2026-00042, both reviewed, with manifest_stanza ready for harden
- cli.py: verify command (check pin, --accept to repin); harden command (full
  implementation replacing M2 stub: load stanza, show diff, --write to apply)

Tests (203 total, all passing):
- tests/property/test_integrity.py: 12 property tests (pin stability, format,
  drift detection determinism, key-order independence, tool-order normalisation)
- tests/replay/test_tool_schema_drift.py: 13 replay tests scripting AVE-2026-00042
  rug pull pattern (pin at VERIFY, poison detected, server suspended, calls denied),
  end-to-end chain asserted atomically
- tests/test_m3_integrity.py: 27 unit tests (pinning, stanza load/merge, verify
  and harden CLI including write, --allow-unreviewed, missing AVE error paths)

Lint fixes: SIM102 in manifest.py (nested-if collapsed), E501 in test files and
policy/manifest.py; all pre-commit hooks passing.

* M4: benign corpus, approval budget, and latency gates

- Add tests/corpus/sessions/github_daily_dev.jsonl (10 benign records,
  5 sessions covering get_issue and create_pull_request flows)
- Add tests/test_m4_corpus.py: p95=0 approval prompts, p99<=1 approval
  prompts, p95 latency <=5ms, and 5 static security invariant checks
- Fix E501 in test_m3_integrity, test_integrity, test_tool_schema_drift
  and harden.py (ruff auto-fix + manual splits)

213 tests passing; pre-commit clean.

* M5: Operations layer -- OTel spans, CEF/syslog, posture report

- Add src/bawbel_gate/ops/ module:
  otel.py: record_decision_span + emit_alert_event; graceful noop when
    opentelemetry-sdk is not installed; setup_otel reads
    OTEL_EXPORTER_OTLP_ENDPOINT
  syslog_cef.py: format_cef_record for all 6 alertable event classes per
    DESIGN.md 8.5; ArcSight CEF v0 format; escapes |, =, backslash, newlines
  posture.py: compute_posture (parse audit JSONL) + render_posture_report
    (text or JSON); PostureError on missing log
- Add 'posture' CLI command (--audit-log, --json)
- Add alertable event constants and CEF severity map to _const.py
- Add optional [ops] extra in pyproject.toml for opentelemetry deps
- 35 new tests in tests/test_m5_ops.py covering constants, CEF format,
  posture computation, OTel noop, and posture CLI

248 tests passing; pre-commit clean.

* M6: Embedded console + Console API

- Add src/bawbel_gate/console/ module:
  auth.py: mint_token (256-bit random hex) + verify_token (hmac.compare_digest)
  state.py: ConsoleState + serialize_state for GET /v1/state shape per DESIGN.md 13.1
  approvals.py: ApprovalRegistry with first-writer-wins race semantics (thread-safe)
  server.py: ConsoleServer (stdlib http.server) serving:
    GET /v1/state (auth required, full snapshot)
    GET /v1/events (SSE stream of audit records)
    POST /v1/approvals/{id} (console approval channel; 409 on second write)
    GET / (embedded UI bundle; no build-time network fetch)
    all other paths -> 404 (enforces read-only property)
- Add --console HOST:PORT flag to 'serve' command
- 31 new tests in tests/test_m6_console.py:
  bearer token, state serializer, approval registry races, HTTP endpoints,
  read-only guarantee (no manifest/taint/tool-call mutation paths)

279 tests passing; pre-commit clean.

* M7: bawbel-hub fleet ingest, state store, enrollment, and posture

- Add src/bawbel_gate/hub/ module:
  store.py: SQLite fleet state store (idempotent upsert by gate/session/seq,
    rebuild from records, chain head tracking, quarantine table)
  ingest.py: process_batch() with per-session chain verification; raises
    ChainMismatch on tamper evidence (-> 409); replays are idempotent
  enroll.py: EnrollmentRegistry with single-use tokens and admin tokens
  fleet.py: compute_fleet_posture() -> list[GatePostureRow] for fleet console
  server.py: HubServer with:
    GET  /v1/fleet/state       -- fleet posture snapshot
    GET  /v1/fleet/gates/{id}  -- per-gate state
    POST /v1/gates/{id}/records -- NDJSON ingest (chain-verified)
    POST /v1/enrollment/tokens  -- mint enrollment token
    POST /v1/enroll             -- consume token, get gate_id
    (no command channel; all other paths -> 404, DESIGN.md 14.6)
- Add src/bawbel_gate/ingest/ module:
  client.py: IngestCursor (cursor file per session), build_batch() (reads
    audit JSONL past the acked cursor, max_records cap)
- CLI: 'enroll' command, 'hub serve' + 'hub token new' subcommands
- 36 new tests in tests/test_m7_hub.py covering store, chain verification,
  enrollment lifecycle, fleet posture, HTTP server, ingest client

315 tests passing; pre-commit clean.

* Update README to reflect M0-M7 feature-complete state; fix E501 in test_m7_hub

Rewrites README as the authoritative operator reference covering all 8 milestones.
Fixes two E501 violations in tests/test_m7_hub.py (lines 110, 131) surfaced by pre-commit.

* Remove hard-coded network/timing values; add env var overrides for all runtime tunables

All default ports, hosts, and internal timing values moved to _const.py.
CLI options for console addr, hub host/port, hub db, and audit log path now
accept env vars (BAWBEL_CONSOLE_ADDR, BAWBEL_HUB_HOST, BAWBEL_HUB_PORT,
BAWBEL_HUB_DB, BAWBEL_AUDIT_LOG, OTEL_EXPORTER_OTLP_ENDPOINT already existed).
Adds .env.example documenting every supported env var. Adds .env and *.db to
.gitignore so local runtime files are never committed.

* Add complete step-by-step testing guide (docs/TESTING.md)

* Remove .env.example; document env vars inline in TESTING.md

.env files are readable by subprocesses and unsuitable for a security product.
Env var reference now lives in docs/TESTING.md and README only.
.env stays in .gitignore as defence-in-depth.

* Wire the MCP multiplexer request loop; fix hub enrollment auth

serve/serve --console previously did nothing: the enforce/learn loop was a
stub that printed a placeholder and returned, killing the console's daemon
thread before it was reachable. mux/proxy.py implements the real request
loop per DESIGN.md 3.3/6.2: spawn upstream servers, merge/namespace
tools/list, route every tools/call through the policy engine (manifest ->
taint -> trifecta -> approval), forward on allow/approve, deny without
touching upstream, and write the hash-chained audit trail.

Also fixes bawbel-hub's /v1/enroll, which required the admin bearer token
an unenrolled gate doesn't have yet -- the single-use enrollment token is
itself the credential (DESIGN.md 14.3), so the documented enroll flow was
unreachable.

Adds tests/test_mux_proxy.py (drives the real loop end-to-end against a
fake stdio MCP server) and two hub tests covering the enroll bootstrap
path over HTTP, the gap that let the auth bug ship untested.

* Add README badges, architecture diagram, and real CLI screenshots

Badges (license, Python version, status, test count, fail-closed posture)
and a mermaid diagram of the manifest -> taint -> trifecta enforcement
pipeline, so the README reads at a glance.

Screenshots for harden, serve --console, and hub are captured from the
now-working commands (see previous commit) rather than mocked. Also
updates docs/TESTING.md test counts and a few pre-existing wording
mismatches against actual CLI output (lint, audit verify, pin format)
found while running the guide end-to-end, and documents that console-
driven approval routing isn't wired yet.

* Add a minimal browser UI to bawbel-hub

The hub had no browser-viewable page: /v1/fleet/state is JSON-only and
requires an Authorization header a plain browser navigation can't send.
DESIGN.md 14.4 specs a fleet console view ("the console... plus a fleet
dimension") that was never built -- only the raw API existed.

Adds a / route serving an embedded page in the same minimal style as the
gate's own console UI (fetch + <pre>, same-origin so no CORS is needed),
and a ?t=<admin_token> query-param auth fallback for browser navigation,
mirroring how the gate console already handles its SSE endpoint.

* Build a real console dashboard; fix a concurrency bug that starved it

The embedded console's UI was a bare <pre> dump of raw JSON with SSE
events only logged to devtools, not rendered. Replaces it with a real
dashboard: sessions/servers tables, a pending-approvals panel wired to
POST /v1/approvals/{id}, and a live-scrolling audit feed driven by the
existing SSE stream.

Building it surfaced a real bug: ConsoleServer used a plain
http.server.HTTPServer, which handles one request at a time. /v1/events
is a long-lived SSE stream, so a single connected client (e.g. the
dashboard's own EventSource) starved every other request -- state polls,
approvals, even the page itself -- until that connection closed. Switches
to ThreadingHTTPServer. Verified live (curl hung against the old server
mid-SSE-stream, returns instantly after the fix) and covered by a new
regression test that opens an SSE connection and asserts /v1/state still
responds; confirmed it fails against the old code before the fix.

Drops the harden/hub screenshots from the README per discussion: hub is
moving to its own repo (ARCHITECTURE.md Layer 5a), so further UI
investment belongs there, not here.
… docs) (#5)

Adds what SECURITY.md already claimed was in place but wasn't: .github/
didn't exist at all, so "we track advisories via Dependabot and OpenSSF
Scorecard" was aspirational, not true.

- .github/dependabot.yml: weekly pip + github-actions update checks.
- .github/workflows/scorecard.yml: OpenSSF Scorecard analysis, published
  to the public API. Note: Scorecard evaluates the default branch (main),
  which is currently just the initial commit -- the badge will read low
  until main is updated with the real code on develop.
- GOVERNANCE.md: honest solo-maintainer model (CLAUDE.md already says
  "solo-maintained"; this makes the decision process and bus-factor
  explicit instead of leaving it implicit).
- CODE_OF_CONDUCT.md: Contributor Covenant v2.1, enforcement contact
  reused from SECURITY.md (no other contact channel existed).
- README: badges for Scorecard, security policy, code of conduct, DCO;
  links CONTRIBUTING.md to the new governance/conduct docs.
- Fixes a stale test count (324 -> 327) in README and TESTING.md, left
  over from before the last merge.

Commit signing is already real (GPG-signed, GitHub-verified) but nothing
requires it -- develop has no branch protection at all. Enabling
"required signed commits" needs two sequential gh api calls that the
permission layer wouldn't let me run non-interactively even after
confirmation; commands are in the PR description / chat history for the
maintainer to run directly.
* Chore: verifiable-hygiene pass (badges, disclosure policy, governance docs)

Adds what SECURITY.md already claimed was in place but wasn't: .github/
didn't exist at all, so "we track advisories via Dependabot and OpenSSF
Scorecard" was aspirational, not true.

- .github/dependabot.yml: weekly pip + github-actions update checks.
- .github/workflows/scorecard.yml: OpenSSF Scorecard analysis, published
  to the public API. Note: Scorecard evaluates the default branch (main),
  which is currently just the initial commit -- the badge will read low
  until main is updated with the real code on develop.
- GOVERNANCE.md: honest solo-maintainer model (CLAUDE.md already says
  "solo-maintained"; this makes the decision process and bus-factor
  explicit instead of leaving it implicit).
- CODE_OF_CONDUCT.md: Contributor Covenant v2.1, enforcement contact
  reused from SECURITY.md (no other contact channel existed).
- README: badges for Scorecard, security policy, code of conduct, DCO;
  links CONTRIBUTING.md to the new governance/conduct docs.
- Fixes a stale test count (324 -> 327) in README and TESTING.md, left
  over from before the last merge.

Commit signing is already real (GPG-signed, GitHub-verified) but nothing
requires it -- develop has no branch protection at all. Enabling
"required signed commits" needs two sequential gh api calls that the
permission layer wouldn't let me run non-interactively even after
confirmation; commands are in the PR description / chat history for the
maintainer to run directly.

* add matt pocock skills
* Add CI security scan pipeline: code scan, dependency scan, secret scan

Runs on every PR and push to develop/main:

- code-scan: CodeQL Python analysis, results in the repo's code scanning
  alerts.
- dependency-scan: actions/dependency-review-action (fails PRs that
  introduce high-severity vulnerable or license-incompatible deps) plus
  pip-audit against the actual installed dependency tree, PR-only since
  dependency-review compares base vs head. Upgrades pip/setuptools
  before auditing -- verified locally that a stock venv's bundled
  setuptools 59.6.0 carries three CVEs of its own, which would fail
  every run on bootstrap tooling rather than this project's real
  dependencies.
- secret-scan: Gitleaks, full history fetch, push and PR.

Also enabled at the repo level (not a file change, so not in this diff):
secret scanning, secret scanning push protection, and Dependabot
security updates -- all were "disabled", including secret scanning,
which is normally free and on by default for public repos.

None of these are required status checks yet -- the "Protect develop"
ruleset has no required_status_checks rule, so right now these run and
report but do not block a merge. That needs a separate ruleset update.

* Fix secret-scan and dependency-scan CI failures found on first PR run

Both new jobs failed immediately on PR #13's first run:

- gitleaks/gitleaks-action requires a paid GITLEAKS_LICENSE for
  org-owned repos (bawbel is an org; the free tier is personal-account
  only). Switched to calling the gitleaks CLI directly -- the tool
  itself is fully open source (Apache-2.0), only the GitHub Action
  wrapper has the org-licensing gate. Version pinned (8.30.1) for
  reproducible runs rather than resolved at run time.

- dependency-review-action needs the repo's Dependency graph feature
  enabled; it wasn't. Enabled via the vulnerability-alerts API (repo
  setting, not a file change).

Running gitleaks against this repo's actual history (locally, before
wiring it back in) found 6 "leaks" -- all in tests/test_m2_policy.py,
which deliberately contains fake JWT/GitLab-PAT strings as fixtures for
policy/manifest.py's own secret-scan detector, not real credentials.
Added .gitleaks.toml with a path-scoped allowlist for that one file
rather than disabling the check; verified locally it suppresses exactly
those six and nothing else ("no leaks found").

* Add CHANGELOG.md; README: security badges + gate.yaml sample

Splits ci.yml into three separate workflow files (code-scan.yml,
dependency-scan.yml, secret-scan.yml) -- GitHub's native workflow
badges are per-workflow-file, not per-job, so a combined workflow
can't produce three distinct badges. Same jobs, same triggers, just
separated so each has its own README badge.

README additions:
- Badges for the three new security scans, pointing at develop (where
  they actually run continuously; main doesn't have them yet).
- The gate.yaml sample from DESIGN.md 3.1, since the Quickstart section
  referenced --config gate.yaml several times without ever showing
  what one looks like. Verified against the real config loader
  (bawbel_gate.mux.config.load_config), not just pasted in --
  it parses cleanly.

CHANGELOG.md: Keep a Changelog format, single [Unreleased] section.
No version has been tagged yet, so there's nothing to backdate against;
the M0-M7 build stays in README's milestone table rather than being
itemized here. Covers everything since that baseline: the multiplexer
fix, hub fixes, console dashboard, governance docs, PyPI packaging, and
this CI security pipeline.
* Implement PyPI packaging: fix a real install-breaking bug, add metadata

_schemas.py located schemas/ via Path(__file__).parent.parent.parent,
which only resolves inside an editable/source checkout. A real
`pip install bawbel-gate` wheel does not carry that path at all --
every schema-dependent command (lint, verify, harden, manifest loading
in general) would have raised FileNotFoundError for anyone installing
from PyPI. Confirmed by building the wheel, installing it into a clean
venv, and running `bawbel-gate lint` against it: fails without the fix,
passes with it.

Fix: bundle schemas/ into the wheel itself via
[tool.hatch.build.targets.wheel.force-include], and make _schemas.py
prefer that packaged copy over the repo-root path when both could
resolve. Extracted the selection into _resolve_schema_dir() with two
unit tests (packaged-present, packaged-missing) since the priority
logic is exactly what was wrong.

Also completes pyproject.toml's PyPI metadata (readme, authors,
classifiers, keywords, project.urls, SPDX license expression instead of
the deprecated {text=...} form) and adds a release-triggered publish
workflow using PyPI trusted publishing (OIDC, no stored API token) that
runs the full test suite and a real wheel-install smoke test before
ever uploading.

Verified: `python -m build` succeeds, `twine check dist/*` passes on
both sdist and wheel, wheel installed + exercised in a throwaway venv,
329 tests passing, pre-commit clean.

Not done here, and can't be from this repo: creating the PyPI project
itself and configuring its trusted publisher (repo/workflow/environment
name) at pypi.org -- that requires the maintainer's own PyPI account.
Until that's done and a release is actually cut, README's "not yet on
PyPI" status stays accurate; not flipping it preemptively.

* Split publish workflow: auto to TestPyPI, manual approval to PyPI

publish-testpypi runs automatically after build (skip-existing so a
re-run of the workflow doesn't hard-fail on a version already there).
publish-pypi now needs publish-testpypi and targets the "pypi"
environment, which has a required_reviewers protection rule -- that's
what actually pauses the job for manual approval, not anything in this
file. Both environments (testpypi unprotected, pypi with chaksaray as
required reviewer, self-review allowed since this is solo-maintained)
are already configured on the repo.

* Rename publish.yml jobs to match bawbel-scanner's convention

Verify Tests / Build Distribution / Publish to TestPyPI / Publish to PyPI,
for a consistent job graph across the bawbel repos. Behavior unchanged --
the pypi environment's required_reviewers rule still gates the last job.

* Expand keywords for discoverability; fix a real sdist leak

Adds ~15 more PyPI search keywords covering the actual surface area
(mcp-server, agentic-ai, taint-tracking, capability-based-security,
supply-chain-security, etc.) instead of the original 6.

While checking sdist size, found that hatchling's sdist `include` key
only adds to its default "everything not gitignored" file set -- it
does not replace it. Since .agents/ (148K of untracked Claude Code
skill files, never part of this project) isn't in .gitignore, it was
silently ending up in the sdist tarball. Switched to `only-include`,
which is a strict allowlist and ignores the default VCS-based set
entirely.

This means the sdist's contents no longer depend on whatever untracked
files happen to be sitting in the builder's working directory --
actually reproducible, not just small.

Verified: wheel already had zero stray files (packages= is a proper
allowlist by nature, this was sdist-only). Rebuilt after both changes:
twine check passes on both artifacts, wheel installed and exercised in
a throwaway venv, 329 tests passing, pre-commit clean. Wheel ~77KB,
sdist ~94KB either way -- the leak was small in bytes, but the
nondeterminism was the real bug.

* Fix missing trailing newline in setup-gate-skill/SKILL.md
)

Adopts the issue-first workflow ahead of the Factory unblock sequence.

- .github/ISSUE_TEMPLATE: bug_report, feature_request, design_change
  forms, each with an area dropdown. config.yml disables blank issues
  and routes vulnerability reports to private advisories (SECURITY.md).
- .github/PULL_REQUEST_TEMPLATE.md: Closes line, output contract,
  verification block, invariant-ordering block for policy/, taint/,
  audit/, integrity/ changes.
- .github/CODEOWNERS: * @chaksaray (solo-maintained, GOVERNANCE.md).
- CONTRIBUTING.md: new "Issue before PR" section; new "Property test
  before implementation" subsection requiring separate, unsquashed
  commits; Pull requests section now states that CI does not run the
  test suite, instead of claiming it does.
- gate-implementation skill workflow: issue-first step, property test
  committed on its own, PR references the issue.

Signed-off-by: chaksaray <chaksaray@gmail.com>
* test: replay tests for MCP surface coverage

Refs #20. Drives run_enforce() against tests/fixtures/hostile_mcp_server.py,
an upstream that serves injected content on resources/*, prompts/*, a
notifications/message, and a sampling/createMessage request sent ahead of
each tools/call response.

Pass at this commit (lock in current behavior):
- every non-tools host request is rejected with -32601 and never forwarded
- a rejected resources/read leaves taint and untrusted_seen unchanged
- initialize advertises the tools capability only
- host notifications are neither forwarded nor answered
- tools/call results are still provenance-tagged

Fail at this commit (proxy.py reads the next upstream message as the
response without matching its id):
- an upstream notification is returned as the tool result
- results shift by one call after an interleaved message
- the upstream's sampling/createMessage request is never answered

tests/proxy_harness.py holds the in-memory host driver that
test_mux_proxy.py previously inlined.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* fix: match upstream responses by id, refuse unsolicited upstream messages

Refs #20. _send_and_recv returned the next message an upstream wrote,
whatever it was. A notification or a server-to-client request written
ahead of a tools/call response was returned to the host as the result,
the real response was then returned for the next call to that server,
and the upstream's request was never answered.

_send_and_recv now returns only the response whose id matches the
request it sent (value and type). Everything else is handled at the gate
and never reaches the host:
- notifications: dropped
- server-to-client requests (sampling/createMessage, roots/list,
  elicitation/create, ...): answered upstream with -32601
- responses to ids the gate did not send, non-object messages: dropped
  with a stderr warning

Provenance tagging is unchanged: it ran on every forwarded call before
and still does. Upstream EOF propagates exactly as before.

JSONRPC_METHOD_NOT_FOUND replaces the -32601 literal in proxy.py, and
transport.warn is public so proxy.py writes warnings the same way.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: enumerate the MCP surface in DESIGN.md 3.3

Refs #20. 3.3 named tools/call and the provenance tagger but said nothing
about any other MCP message. It now lists, for host to gate, gate to
upstream, and upstream to gate, what the gate answers, forwards, refuses,
and drops, matching mux/proxy.py as of the previous commit.

States the two paths by which upstream content reaches the host: a
tools/call result (tagged) and tool descriptions in the merged tools/list
(untagged, pinned only when the manifest declares tool_schema_integrity).
resources/read and prompts/get are not proxied in v1; a release that
proxies a content-bearing method must tag its response first.

No new policy semantics: no effect, grant, taint, or trifecta rule
changes.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* chore: restore trailing newlines dropped in 7e1ea48

pre-commit end-of-file-fixer fails on develop at 7e1ea48 for these two
files. Whitespace only.

Signed-off-by: chaksaray <chaksaray@gmail.com>

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>
* docs: replace README pipeline diagram with the DESIGN.md 5.2 decision flow

Replaces the left-to-right overview diagram with a top-down decision flow:
capability manifest (5.2 steps 1-4), session taint (step 5), trifecta
invariant (step 6), then allow / approve / deny, with every decision
written to the audit log.

Two labels differ from the drafted diagram because the drafted text
claimed behavior the code does not have:
- Deny says "-32031 with the deny reason", not "cites the AVE id":
  policy/engine.py resolve() never populates Decision.ave, so data.ave
  is always [].
- Steps 1-4 have their own edge to Deny: resolve() returns early on no
  grant, a failed condition, or a guard hit.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: drop <small> from diagram labels; GitHub clips their last line

Signed-off-by: chaksaray <chaksaray@gmail.com>

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>
…able (#24)

* docs: replace README pipeline diagram with the DESIGN.md 5.2 decision flow

Replaces the left-to-right overview diagram with a top-down decision flow:
capability manifest (5.2 steps 1-4), session taint (step 5), trifecta
invariant (step 6), then allow / approve / deny, with every decision
written to the audit log.

Two labels differ from the drafted diagram because the drafted text
claimed behavior the code does not have:
- Deny says "-32031 with the deny reason", not "cites the AVE id":
  policy/engine.py resolve() never populates Decision.ave, so data.ave
  is always [].
- Steps 1-4 have their own edge to Deny: resolve() returns early on no
  grant, a failed condition, or a guard hit.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: lead README with what the gate does; add usage walkthrough and command table

Opening: one-line description, the JSON-RPC-only statement, a quickstart,
and doc links come first. Badges and the status section move into a
details block.

Usage replaces the Install, Quickstart, and CLI reference sections with
seven steps: gate.yaml, client config, learning mode, review and
synthesize, lint/pin/harden, enforce, audit. Each step states what
develop 7e1ea48 does, checked by running it against a stdio test server
and a FastMCP server:
- learning mode needs a manifest file per server (placeholder documented)
- learn synthesize overwrites <server>.cap.yaml (write drafts elsewhere)
- learning-mode observations hold 32-char argument prefixes in clear text
- the audit_log directory must exist; the path is relative to the cwd
- the terminal approval channel reads stdin, the client's JSON-RPC
  stream, so client-launched approvals resolve to deny
- clear appends a SESSION_CLEAR record and does not reach a running gate
- harden's default mitigations path exists only in a clone
"Not yet supported" lists the remaining gaps.

Commands: one table row per command with arguments, defaults, env vars,
and exit behavior, taken from cli.py rather than DESIGN.md 11.5.

Also: install from develop (not on PyPI; main lacks the schema bundling
fix), test count 327 -> 329, clone URL -> bawbel/gate, and removed the
Documentation links to the gitignored IMPLEMENTATION_PLAN.md and
BAWBEL_GATE_MITIGATIONS_SPEC.md. The demo GIF line is left out until the
recording exists.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: drop <small> from diagram labels; GitHub clips their last line

Signed-off-by: chaksaray <chaksaray@gmail.com>
(cherry picked from commit 6497b04)

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>
…26)

The badges and the status section were collapsed in a details block
(#24). Badges now sit directly under the title, and Status is a normal
section again, so both are visible on the first screen.

Also:
- Test count 329 -> 337 in the badge, the status text, and the dev
  install comment. #21 added 8 tests; 337 pass on develop 973d83c.
- The "Not yet supported" item on non-tools MCP methods now cites
  DESIGN.md 3.3, which #21 added.

Signed-off-by: chaksaray <chaksaray@gmail.com>
…without scrolling (#31)

* docs: remove the unclosed details block from README; fit code blocks without scrolling

Merge 451cd3c (main into develop) resolved a README conflict by keeping
both sides. It re-inserted the old opening above the badges: a second
copy of the description, quickstart, and doc links, followed by
<details><summary>Badges and project status</summary> with no closing
</details>. GitHub therefore collapsed everything after the doc links,
the diagram, Usage, Commands, and every later section, into one closed
"Badges and project status" block. This restores the README from #26.

Code blocks: measured on github.com at a 1280 px viewport, the pin
snippet (471 px), harden commands (47 px), and the learning-mode client
config (6 px) needed horizontal scrolling. Every displayed code line is
now 80 characters or less: comments moved above commands, long commands
wrapped with backslash continuations, and the pin snippet builds its
JSON-RPC lines with jq (tested against a FastMCP server; same pin hash).

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: put name, description, badges, and links at the top of README

Top order is now: title, description, badges, doc links, then the
quickstart and Status.

Also reformats the step 6 deny example one field per line. Its fence is
indented inside a list item, so the 80-character check in the previous
commit missed its 89-character line.

Signed-off-by: chaksaray <chaksaray@gmail.com>

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>
Refs #29. bawbel-gate 0.1.0 is on PyPI (published by publish.yml from
release v0.1.0).

README: quickstart and Install lead with `pip install bawbel-gate` and
`pip install "bawbel-gate[ops]"`; the develop-branch git install stays as
the way to run unreleased changes. PyPI badge added. "A PyPI release"
removed from "Not yet supported"; the handshake and stdout entries now
link #27 and #28.

CHANGELOG: [Unreleased] becomes [0.1.0] - 2026-10-11 with the #17 and #21
entries that shipped in it, a new empty [Unreleased], and link
references.

Signed-off-by: chaksaray <chaksaray@gmail.com>
… 12 (#39)

Split the open item: registry-distributed manifests are covered by the
signed index proposed in bawbel-registry ADR 0002; org-local manifests
stay open. Cited by path because bawbel-registry is private until launch.

Signed-off-by: chaksaray <chaksaray@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* docs: pip install from PyPI; record the 0.1.0 release

Refs #29. bawbel-gate 0.1.0 is on PyPI (published by publish.yml from
release v0.1.0).

README: quickstart and Install lead with `pip install bawbel-gate` and
`pip install "bawbel-gate[ops]"`; the develop-branch git install stays as
the way to run unreleased changes. PyPI badge added. "A PyPI release"
removed from "Not yet supported"; the handshake and stdout entries now
link #27 and #28.

CHANGELOG: [Unreleased] becomes [0.1.0] - 2026-10-11 with the #17 and #21
entries that shipped in it, a new empty [Unreleased], and link
references.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: make README links absolute so they resolve on PyPI; add Changelog URL

PyPI renders README.md as the project page and resolves no relative
links. The live 0.1.0 page has 19 broken ./ links, a broken console
image, and in-page anchors with no targets.

- 18 file links -> https://github.com/bawbel/gate/blob/main/...
- console image -> raw.githubusercontent.com
- 8 in-page anchors -> https://github.com/bawbel/gate#...
- (#27) and (#28) become issue links; GitHub does not autolink #N in
  repository files
- pyproject.toml: Changelog project URL
- CHANGELOG: entries under [Unreleased]

Signed-off-by: chaksaray <chaksaray@gmail.com>

* release: bump version to 0.1.1 for the PyPI page fix

Signed-off-by: chaksaray <chaksaray@gmail.com>

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>
* test: pin the audit chain construction with golden values

Refs #38. Asserts the construction audit/writer.py implements today, so
it cannot drift silently:
- the hashed content is canonical_bytes(record minus hash) and keeps prev
- prev is concatenated as the UTF-8 of its sha256:<hex> text, 71 bytes
- the first prev is sha256: + 64 zeros
- hashes are sha256: + lowercase hex
- two golden record hashes, and verify_chain accepts the pair

Also pins four deliberate differences from RFC 8785 (JCS): 1.0, 1e-07,
integers above 2^53, and code-point key order outside the BMP.

Passes at this commit: it records existing behavior, it does not
specify new behavior.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: ADR 0001, audit chain construction diverges from ACS v0.1.0

Refs #38. Records option (b) from the issue: the gate keeps its chain
construction and is not cross-verifiable with ACS v0.1.0 ContextEntry
chains. Checked against ACS at db5c89e4 (0.1.3): context-entry.json
requires JCS, drops entry_hash and previous_hash, and concatenates the
raw 32-byte previous digest; conformance.md says the release ships no
conformance test suite and no registry; the repository has no entry_hash
test vectors.

DESIGN.md 8.6 now states the construction exactly: canonical form (not
JCS, and where it differs), prev kept in the hashed content, prev
concatenated as its UTF-8 text, the genesis value. It points to the ADR
and the pinning test.

audit/writer.py: module docstring only. It called the concatenated value
prev_hash_bytes; it is utf8(prev).

Signed-off-by: chaksaray <chaksaray@gmail.com>

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>
…velop (#48)

* Docs/acs conformance (#45)

* M0-M7: Full enforcement core, ops stack, console, and fleet hub (#1)

* feat: governance

* fix: add argument_guards to manifestStanza fragment type

manifestStanza in $defs (DESIGN.md 4.2) was missing argument_guards,
which the ave-mitigations spec (Section 3) includes in the stanza shape.
Both must agree on mergeable keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(M0): project scaffold, three schemas, AVE 1.2 migration script

- pyproject.toml + pre-commit (ruff, bandit, json/yaml checks)
- _const.py: all magic strings, effect order, provenance classes, deny reasons
- _schemas.py: cached schema loader, BAWBEL_SCHEMAS_DIR env override
- schemas/ave-1.2.json: AVE record schema with provenance_vector,
  trifecta_profile, capability_mitigation as nullable optional fields
- schemas/capability-manifest-v1.json: verbatim DESIGN.md 4.2 plus
  $defs/manifestStanza (fragment type for harden --ave stanzas)
- schemas/bawbel-ave-mitigations-v1.json: envelope for ave-mitigations.json;
  reviewed entries require reviewed_by, reviewed_at, rule_ids
- scripts/migrate_ave_v1_2.py: null-insert migration CLI, idempotent,
  --dry-run, validates output against ave-1.2.json
- tests/: 36 tests covering schema validity, example fixtures, migration
  (unit + CLI integration, idempotency, dry-run, error paths)
- corpus fixtures: v1.1 and v1.2 sample records + github-mcp manifest

* feat(M1): multiplexer skeleton, audit chain, learning mode, CLI

- audit/canonical.py: single canonical JSON implementation (I6 invariant)
- audit/writer.py: thread-safe hash-chained JSONL writer + chain verifier (I7)
- mux/config.py: gate.yaml loader with fail-fast validation, ApprovalConfig
  enforces on_timeout=deny only (no fail-open path)
- mux/session.py: monotone SessionState, with_taint() for property tests (I2/I3)
- mux/namespace.py: tool namespacing {server}__{tool} both directions
- mux/transport.py: stdio JSON-RPC read/write, spawn_server subprocess helper
- learn/recorder.py: thread-safe tool call recorder + JSONL flush
- learn/report.py: per-server usage table renderer
- learn/synthesize.py: draft manifest generator (write tools -> approve,
  common arg prefix conditions, wildcard deny, DRAFT banner)
- cli.py: full CLI surface (serve, learn, audit, clear, lint, harden stubs)
- pyproject.toml: requires-python lowered to >=3.10 (runtime is 3.10.12)
- 34 new tests: canonical, chain writer, chain verifier, session state,
  namespace, config loader, recorder, synthesizer

* M2: enforcement core — policy engine, taint, trifecta invariant, approval channel

Implements the full enforcement stack per DESIGN.md sections 5-7:

- policy/manifest.py: Manifest dataclasses + YAML loader with JSON Schema
  validation, secret scanning (AWS/GitLab/Vault/PEM/JWT patterns + Shannon
  entropy), argument guards, taint rules with wildcard prefix matching
- policy/engine.py: 6-step resolve() algorithm from DESIGN.md 5.2; lattice_min()
  combiner; PERMISSIVENESS exported for I2 property tests; fail-closed on every
  unmatched, erroring, or condition-failing path
- policy/approval.py: terminal approval channel; secret-redacted arg display;
  timeout -> deny; injected input_fn for test isolation; uses canonical_str()
  not raw json.dumps to respect invariant I6
- audit/writer.py: AuditWriter.from_existing() for SESSION_CLEAR chain resumption
- cli.py: SESSION_CLEAR writes audited record continuing the existing chain

Tests (151 total, all passing):
- tests/property/test_invariants.py: 14 Hypothesis property tests for I1-I5
- tests/replay/test_github_mcp_exfil.py: 11 replay tests scripting the 2025
  GitHub-MCP exfil chain (poisoned issue -> private read -> PR to fork), each
  of the three enforcement layers asserted independently
- tests/test_m2_policy.py: 32 unit tests (lattice, grants, conditions, guards,
  taint rules, trifecta, JSON-RPC error format, manifest loader)
- tests/test_m2_approval.py: 24 tests (arg redaction, prompt format, y/n/timeout
  paths, SESSION_CLEAR chain continuity)

Lint: pre-commit clean (ruff preview, bandit pass_filenames:false, E501 fixed
across all source and test files; B105/B404 nosec annotations in const/transport)

* M3: integrity pinning, harden --ave, rug-pull replay test, verify CLI

Tool schema pinning per DESIGN.md 8.1:
- integrity/pinning.py: pin_tool_schema() sorts tools by name before
  hashing (order-independent), uses canonical_bytes (I6), detect_drift()
  returns DriftResult with both hashes for operator diff
- integrity/harden.py: apply_harden() loads stanza from ave-mitigations.json,
  blocks unreviewed stanzas unless --allow-unreviewed, merge_stanza() appends
  taint_rules/integrity_watch/argument_guards idempotently, computes YAML diff
- mitigations/ave-mitigations.json: initial content for AVE-2026-00041 and
  AVE-2026-00042, both reviewed, with manifest_stanza ready for harden
- cli.py: verify command (check pin, --accept to repin); harden command (full
  implementation replacing M2 stub: load stanza, show diff, --write to apply)

Tests (203 total, all passing):
- tests/property/test_integrity.py: 12 property tests (pin stability, format,
  drift detection determinism, key-order independence, tool-order normalisation)
- tests/replay/test_tool_schema_drift.py: 13 replay tests scripting AVE-2026-00042
  rug pull pattern (pin at VERIFY, poison detected, server suspended, calls denied),
  end-to-end chain asserted atomically
- tests/test_m3_integrity.py: 27 unit tests (pinning, stanza load/merge, verify
  and harden CLI including write, --allow-unreviewed, missing AVE error paths)

Lint fixes: SIM102 in manifest.py (nested-if collapsed), E501 in test files and
policy/manifest.py; all pre-commit hooks passing.

* M4: benign corpus, approval budget, and latency gates

- Add tests/corpus/sessions/github_daily_dev.jsonl (10 benign records,
  5 sessions covering get_issue and create_pull_request flows)
- Add tests/test_m4_corpus.py: p95=0 approval prompts, p99<=1 approval
  prompts, p95 latency <=5ms, and 5 static security invariant checks
- Fix E501 in test_m3_integrity, test_integrity, test_tool_schema_drift
  and harden.py (ruff auto-fix + manual splits)

213 tests passing; pre-commit clean.

* M5: Operations layer -- OTel spans, CEF/syslog, posture report

- Add src/bawbel_gate/ops/ module:
  otel.py: record_decision_span + emit_alert_event; graceful noop when
    opentelemetry-sdk is not installed; setup_otel reads
    OTEL_EXPORTER_OTLP_ENDPOINT
  syslog_cef.py: format_cef_record for all 6 alertable event classes per
    DESIGN.md 8.5; ArcSight CEF v0 format; escapes |, =, backslash, newlines
  posture.py: compute_posture (parse audit JSONL) + render_posture_report
    (text or JSON); PostureError on missing log
- Add 'posture' CLI command (--audit-log, --json)
- Add alertable event constants and CEF severity map to _const.py
- Add optional [ops] extra in pyproject.toml for opentelemetry deps
- 35 new tests in tests/test_m5_ops.py covering constants, CEF format,
  posture computation, OTel noop, and posture CLI

248 tests passing; pre-commit clean.

* M6: Embedded console + Console API

- Add src/bawbel_gate/console/ module:
  auth.py: mint_token (256-bit random hex) + verify_token (hmac.compare_digest)
  state.py: ConsoleState + serialize_state for GET /v1/state shape per DESIGN.md 13.1
  approvals.py: ApprovalRegistry with first-writer-wins race semantics (thread-safe)
  server.py: ConsoleServer (stdlib http.server) serving:
    GET /v1/state (auth required, full snapshot)
    GET /v1/events (SSE stream of audit records)
    POST /v1/approvals/{id} (console approval channel; 409 on second write)
    GET / (embedded UI bundle; no build-time network fetch)
    all other paths -> 404 (enforces read-only property)
- Add --console HOST:PORT flag to 'serve' command
- 31 new tests in tests/test_m6_console.py:
  bearer token, state serializer, approval registry races, HTTP endpoints,
  read-only guarantee (no manifest/taint/tool-call mutation paths)

279 tests passing; pre-commit clean.

* M7: bawbel-hub fleet ingest, state store, enrollment, and posture

- Add src/bawbel_gate/hub/ module:
  store.py: SQLite fleet state store (idempotent upsert by gate/session/seq,
    rebuild from records, chain head tracking, quarantine table)
  ingest.py: process_batch() with per-session chain verification; raises
    ChainMismatch on tamper evidence (-> 409); replays are idempotent
  enroll.py: EnrollmentRegistry with single-use tokens and admin tokens
  fleet.py: compute_fleet_posture() -> list[GatePostureRow] for fleet console
  server.py: HubServer with:
    GET  /v1/fleet/state       -- fleet posture snapshot
    GET  /v1/fleet/gates/{id}  -- per-gate state
    POST /v1/gates/{id}/records -- NDJSON ingest (chain-verified)
    POST /v1/enrollment/tokens  -- mint enrollment token
    POST /v1/enroll             -- consume token, get gate_id
    (no command channel; all other paths -> 404, DESIGN.md 14.6)
- Add src/bawbel_gate/ingest/ module:
  client.py: IngestCursor (cursor file per session), build_batch() (reads
    audit JSONL past the acked cursor, max_records cap)
- CLI: 'enroll' command, 'hub serve' + 'hub token new' subcommands
- 36 new tests in tests/test_m7_hub.py covering store, chain verification,
  enrollment lifecycle, fleet posture, HTTP server, ingest client

315 tests passing; pre-commit clean.

* Update README to reflect M0-M7 feature-complete state; fix E501 in test_m7_hub

Rewrites README as the authoritative operator reference covering all 8 milestones.
Fixes two E501 violations in tests/test_m7_hub.py (lines 110, 131) surfaced by pre-commit.

* Config hardening, testing guide, and no-.env policy (#2)

* feat: governance

* fix: add argument_guards to manifestStanza fragment type

manifestStanza in $defs (DESIGN.md 4.2) was missing argument_guards,
which the ave-mitigations spec (Section 3) includes in the stanza shape.
Both must agree on mergeable keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(M0): project scaffold, three schemas, AVE 1.2 migration script

- pyproject.toml + pre-commit (ruff, bandit, json/yaml checks)
- _const.py: all magic strings, effect order, provenance classes, deny reasons
- _schemas.py: cached schema loader, BAWBEL_SCHEMAS_DIR env override
- schemas/ave-1.2.json: AVE record schema with provenance_vector,
  trifecta_profile, capability_mitigation as nullable optional fields
- schemas/capability-manifest-v1.json: verbatim DESIGN.md 4.2 plus
  $defs/manifestStanza (fragment type for harden --ave stanzas)
- schemas/bawbel-ave-mitigations-v1.json: envelope for ave-mitigations.json;
  reviewed entries require reviewed_by, reviewed_at, rule_ids
- scripts/migrate_ave_v1_2.py: null-insert migration CLI, idempotent,
  --dry-run, validates output against ave-1.2.json
- tests/: 36 tests covering schema validity, example fixtures, migration
  (unit + CLI integration, idempotency, dry-run, error paths)
- corpus fixtures: v1.1 and v1.2 sample records + github-mcp manifest

* feat(M1): multiplexer skeleton, audit chain, learning mode, CLI

- audit/canonical.py: single canonical JSON implementation (I6 invariant)
- audit/writer.py: thread-safe hash-chained JSONL writer + chain verifier (I7)
- mux/config.py: gate.yaml loader with fail-fast validation, ApprovalConfig
  enforces on_timeout=deny only (no fail-open path)
- mux/session.py: monotone SessionState, with_taint() for property tests (I2/I3)
- mux/namespace.py: tool namespacing {server}__{tool} both directions
- mux/transport.py: stdio JSON-RPC read/write, spawn_server subprocess helper
- learn/recorder.py: thread-safe tool call recorder + JSONL flush
- learn/report.py: per-server usage table renderer
- learn/synthesize.py: draft manifest generator (write tools -> approve,
  common arg prefix conditions, wildcard deny, DRAFT banner)
- cli.py: full CLI surface (serve, learn, audit, clear, lint, harden stubs)
- pyproject.toml: requires-python lowered to >=3.10 (runtime is 3.10.12)
- 34 new tests: canonical, chain writer, chain verifier, session state,
  namespace, config loader, recorder, synthesizer

* M2: enforcement core — policy engine, taint, trifecta invariant, approval channel

Implements the full enforcement stack per DESIGN.md sections 5-7:

- policy/manifest.py: Manifest dataclasses + YAML loader with JSON Schema
  validation, secret scanning (AWS/GitLab/Vault/PEM/JWT patterns + Shannon
  entropy), argument guards, taint rules with wildcard prefix matching
- policy/engine.py: 6-step resolve() algorithm from DESIGN.md 5.2; lattice_min()
  combiner; PERMISSIVENESS exported for I2 property tests; fail-closed on every
  unmatched, erroring, or condition-failing path
- policy/approval.py: terminal approval channel; secret-redacted arg display;
  timeout -> deny; injected input_fn for test isolation; uses canonical_str()
  not raw json.dumps to respect invariant I6
- audit/writer.py: AuditWriter.from_existing() for SESSION_CLEAR chain resumption
- cli.py: SESSION_CLEAR writes audited record continuing the existing chain

Tests (151 total, all passing):
- tests/property/test_invariants.py: 14 Hypothesis property tests for I1-I5
- tests/replay/test_github_mcp_exfil.py: 11 replay tests scripting the 2025
  GitHub-MCP exfil chain (poisoned issue -> private read -> PR to fork), each
  of the three enforcement layers asserted independently
- tests/test_m2_policy.py: 32 unit tests (lattice, grants, conditions, guards,
  taint rules, trifecta, JSON-RPC error format, manifest loader)
- tests/test_m2_approval.py: 24 tests (arg redaction, prompt format, y/n/timeout
  paths, SESSION_CLEAR chain continuity)

Lint: pre-commit clean (ruff preview, bandit pass_filenames:false, E501 fixed
across all source and test files; B105/B404 nosec annotations in const/transport)

* M3: integrity pinning, harden --ave, rug-pull replay test, verify CLI

Tool schema pinning per DESIGN.md 8.1:
- integrity/pinning.py: pin_tool_schema() sorts tools by name before
  hashing (order-independent), uses canonical_bytes (I6), detect_drift()
  returns DriftResult with both hashes for operator diff
- integrity/harden.py: apply_harden() loads stanza from ave-mitigations.json,
  blocks unreviewed stanzas unless --allow-unreviewed, merge_stanza() appends
  taint_rules/integrity_watch/argument_guards idempotently, computes YAML diff
- mitigations/ave-mitigations.json: initial content for AVE-2026-00041 and
  AVE-2026-00042, both reviewed, with manifest_stanza ready for harden
- cli.py: verify command (check pin, --accept to repin); harden command (full
  implementation replacing M2 stub: load stanza, show diff, --write to apply)

Tests (203 total, all passing):
- tests/property/test_integrity.py: 12 property tests (pin stability, format,
  drift detection determinism, key-order independence, tool-order normalisation)
- tests/replay/test_tool_schema_drift.py: 13 replay tests scripting AVE-2026-00042
  rug pull pattern (pin at VERIFY, poison detected, server suspended, calls denied),
  end-to-end chain asserted atomically
- tests/test_m3_integrity.py: 27 unit tests (pinning, stanza load/merge, verify
  and harden CLI including write, --allow-unreviewed, missing AVE error paths)

Lint fixes: SIM102 in manifest.py (nested-if collapsed), E501 in test files and
policy/manifest.py; all pre-commit hooks passing.

* M4: benign corpus, approval budget, and latency gates

- Add tests/corpus/sessions/github_daily_dev.jsonl (10 benign records,
  5 sessions covering get_issue and create_pull_request flows)
- Add tests/test_m4_corpus.py: p95=0 approval prompts, p99<=1 approval
  prompts, p95 latency <=5ms, and 5 static security invariant checks
- Fix E501 in test_m3_integrity, test_integrity, test_tool_schema_drift
  and harden.py (ruff auto-fix + manual splits)

213 tests passing; pre-commit clean.

* M5: Operations layer -- OTel spans, CEF/syslog, posture report

- Add src/bawbel_gate/ops/ module:
  otel.py: record_decision_span + emit_alert_event; graceful noop when
    opentelemetry-sdk is not installed; setup_otel reads
    OTEL_EXPORTER_OTLP_ENDPOINT
  syslog_cef.py: format_cef_record for all 6 alertable event classes per
    DESIGN.md 8.5; ArcSight CEF v0 format; escapes |, =, backslash, newlines
  posture.py: compute_posture (parse audit JSONL) + render_posture_report
    (text or JSON); PostureError on missing log
- Add 'posture' CLI command (--audit-log, --json)
- Add alertable event constants and CEF severity map to _const.py
- Add optional [ops] extra in pyproject.toml for opentelemetry deps
- 35 new tests in tests/test_m5_ops.py covering constants, CEF format,
  posture computation, OTel noop, and posture CLI

248 tests passing; pre-commit clean.

* M6: Embedded console + Console API

- Add src/bawbel_gate/console/ module:
  auth.py: mint_token (256-bit random hex) + verify_token (hmac.compare_digest)
  state.py: ConsoleState + serialize_state for GET /v1/state shape per DESIGN.md 13.1
  approvals.py: ApprovalRegistry with first-writer-wins race semantics (thread-safe)
  server.py: ConsoleServer (stdlib http.server) serving:
    GET /v1/state (auth required, full snapshot)
    GET /v1/events (SSE stream of audit records)
    POST /v1/approvals/{id} (console approval channel; 409 on second write)
    GET / (embedded UI bundle; no build-time network fetch)
    all other paths -> 404 (enforces read-only property)
- Add --console HOST:PORT flag to 'serve' command
- 31 new tests in tests/test_m6_console.py:
  bearer token, state serializer, approval registry races, HTTP endpoints,
  read-only guarantee (no manifest/taint/tool-call mutation paths)

279 tests passing; pre-commit clean.

* M7: bawbel-hub fleet ingest, state store, enrollment, and posture

- Add src/bawbel_gate/hub/ module:
  store.py: SQLite fleet state store (idempotent upsert by gate/session/seq,
    rebuild from records, chain head tracking, quarantine table)
  ingest.py: process_batch() with per-session chain verification; raises
    ChainMismatch on tamper evidence (-> 409); replays are idempotent
  enroll.py: EnrollmentRegistry with single-use tokens and admin tokens
  fleet.py: compute_fleet_posture() -> list[GatePostureRow] for fleet console
  server.py: HubServer with:
    GET  /v1/fleet/state       -- fleet posture snapshot
    GET  /v1/fleet/gates/{id}  -- per-gate state
    POST /v1/gates/{id}/records -- NDJSON ingest (chain-verified)
    POST /v1/enrollment/tokens  -- mint enrollment token
    POST /v1/enroll             -- consume token, get gate_id
    (no command channel; all other paths -> 404, DESIGN.md 14.6)
- Add src/bawbel_gate/ingest/ module:
  client.py: IngestCursor (cursor file per session), build_batch() (reads
    audit JSONL past the acked cursor, max_records cap)
- CLI: 'enroll' command, 'hub serve' + 'hub token new' subcommands
- 36 new tests in tests/test_m7_hub.py covering store, chain verification,
  enrollment lifecycle, fleet posture, HTTP server, ingest client

315 tests passing; pre-commit clean.

* Update README to reflect M0-M7 feature-complete state; fix E501 in test_m7_hub

Rewrites README as the authoritative operator reference covering all 8 milestones.
Fixes two E501 violations in tests/test_m7_hub.py (lines 110, 131) surfaced by pre-commit.

* Remove hard-coded network/timing values; add env var overrides for all runtime tunables

All default ports, hosts, and internal timing values moved to _const.py.
CLI options for console addr, hub host/port, hub db, and audit log path now
accept env vars (BAWBEL_CONSOLE_ADDR, BAWBEL_HUB_HOST, BAWBEL_HUB_PORT,
BAWBEL_HUB_DB, BAWBEL_AUDIT_LOG, OTEL_EXPORTER_OTLP_ENDPOINT already existed).
Adds .env.example documenting every supported env var. Adds .env and *.db to
.gitignore so local runtime files are never committed.

* Add complete step-by-step testing guide (docs/TESTING.md)

* Remove .env.example; document env vars inline in TESTING.md

.env files are readable by subprocesses and unsuitable for a security product.
Env var reference now lives in docs/TESTING.md and README only.
.env stays in .gitignore as defence-in-depth.

* Wire the multiplexer, fix hub enroll/console concurrency, real README (#3)

* feat: governance

* fix: add argument_guards to manifestStanza fragment type

manifestStanza in $defs (DESIGN.md 4.2) was missing argument_guards,
which the ave-mitigations spec (Section 3) includes in the stanza shape.
Both must agree on mergeable keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(M0): project scaffold, three schemas, AVE 1.2 migration script

- pyproject.toml + pre-commit (ruff, bandit, json/yaml checks)
- _const.py: all magic strings, effect order, provenance classes, deny reasons
- _schemas.py: cached schema loader, BAWBEL_SCHEMAS_DIR env override
- schemas/ave-1.2.json: AVE record schema with provenance_vector,
  trifecta_profile, capability_mitigation as nullable optional fields
- schemas/capability-manifest-v1.json: verbatim DESIGN.md 4.2 plus
  $defs/manifestStanza (fragment type for harden --ave stanzas)
- schemas/bawbel-ave-mitigations-v1.json: envelope for ave-mitigations.json;
  reviewed entries require reviewed_by, reviewed_at, rule_ids
- scripts/migrate_ave_v1_2.py: null-insert migration CLI, idempotent,
  --dry-run, validates output against ave-1.2.json
- tests/: 36 tests covering schema validity, example fixtures, migration
  (unit + CLI integration, idempotency, dry-run, error paths)
- corpus fixtures: v1.1 and v1.2 sample records + github-mcp manifest

* feat(M1): multiplexer skeleton, audit chain, learning mode, CLI

- audit/canonical.py: single canonical JSON implementation (I6 invariant)
- audit/writer.py: thread-safe hash-chained JSONL writer + chain verifier (I7)
- mux/config.py: gate.yaml loader with fail-fast validation, ApprovalConfig
  enforces on_timeout=deny only (no fail-open path)
- mux/session.py: monotone SessionState, with_taint() for property tests (I2/I3)
- mux/namespace.py: tool namespacing {server}__{tool} both directions
- mux/transport.py: stdio JSON-RPC read/write, spawn_server subprocess helper
- learn/recorder.py: thread-safe tool call recorder + JSONL flush
- learn/report.py: per-server usage table renderer
- learn/synthesize.py: draft manifest generator (write tools -> approve,
  common arg prefix conditions, wildcard deny, DRAFT banner)
- cli.py: full CLI surface (serve, learn, audit, clear, lint, harden stubs)
- pyproject.toml: requires-python lowered to >=3.10 (runtime is 3.10.12)
- 34 new tests: canonical, chain writer, chain verifier, session state,
  namespace, config loader, recorder, synthesizer

* M2: enforcement core — policy engine, taint, trifecta invariant, approval channel

Implements the full enforcement stack per DESIGN.md sections 5-7:

- policy/manifest.py: Manifest dataclasses + YAML loader with JSON Schema
  validation, secret scanning (AWS/GitLab/Vault/PEM/JWT patterns + Shannon
  entropy), argument guards, taint rules with wildcard prefix matching
- policy/engine.py: 6-step resolve() algorithm from DESIGN.md 5.2; lattice_min()
  combiner; PERMISSIVENESS exported for I2 property tests; fail-closed on every
  unmatched, erroring, or condition-failing path
- policy/approval.py: terminal approval channel; secret-redacted arg display;
  timeout -> deny; injected input_fn for test isolation; uses canonical_str()
  not raw json.dumps to respect invariant I6
- audit/writer.py: AuditWriter.from_existing() for SESSION_CLEAR chain resumption
- cli.py: SESSION_CLEAR writes audited record continuing the existing chain

Tests (151 total, all passing):
- tests/property/test_invariants.py: 14 Hypothesis property tests for I1-I5
- tests/replay/test_github_mcp_exfil.py: 11 replay tests scripting the 2025
  GitHub-MCP exfil chain (poisoned issue -> private read -> PR to fork), each
  of the three enforcement layers asserted independently
- tests/test_m2_policy.py: 32 unit tests (lattice, grants, conditions, guards,
  taint rules, trifecta, JSON-RPC error format, manifest loader)
- tests/test_m2_approval.py: 24 tests (arg redaction, prompt format, y/n/timeout
  paths, SESSION_CLEAR chain continuity)

Lint: pre-commit clean (ruff preview, bandit pass_filenames:false, E501 fixed
across all source and test files; B105/B404 nosec annotations in const/transport)

* M3: integrity pinning, harden --ave, rug-pull replay test, verify CLI

Tool schema pinning per DESIGN.md 8.1:
- integrity/pinning.py: pin_tool_schema() sorts tools by name before
  hashing (order-independent), uses canonical_bytes (I6), detect_drift()
  returns DriftResult with both hashes for operator diff
- integrity/harden.py: apply_harden() loads stanza from ave-mitigations.json,
  blocks unreviewed stanzas unless --allow-unreviewed, merge_stanza() appends
  taint_rules/integrity_watch/argument_guards idempotently, computes YAML diff
- mitigations/ave-mitigations.json: initial content for AVE-2026-00041 and
  AVE-2026-00042, both reviewed, with manifest_stanza ready for harden
- cli.py: verify command (check pin, --accept to repin); harden command (full
  implementation replacing M2 stub: load stanza, show diff, --write to apply)

Tests (203 total, all passing):
- tests/property/test_integrity.py: 12 property tests (pin stability, format,
  drift detection determinism, key-order independence, tool-order normalisation)
- tests/replay/test_tool_schema_drift.py: 13 replay tests scripting AVE-2026-00042
  rug pull pattern (pin at VERIFY, poison detected, server suspended, calls denied),
  end-to-end chain asserted atomically
- tests/test_m3_integrity.py: 27 unit tests (pinning, stanza load/merge, verify
  and harden CLI including write, --allow-unreviewed, missing AVE error paths)

Lint fixes: SIM102 in manifest.py (nested-if collapsed), E501 in test files and
policy/manifest.py; all pre-commit hooks passing.

* M4: benign corpus, approval budget, and latency gates

- Add tests/corpus/sessions/github_daily_dev.jsonl (10 benign records,
  5 sessions covering get_issue and create_pull_request flows)
- Add tests/test_m4_corpus.py: p95=0 approval prompts, p99<=1 approval
  prompts, p95 latency <=5ms, and 5 static security invariant checks
- Fix E501 in test_m3_integrity, test_integrity, test_tool_schema_drift
  and harden.py (ruff auto-fix + manual splits)

213 tests passing; pre-commit clean.

* M5: Operations layer -- OTel spans, CEF/syslog, posture report

- Add src/bawbel_gate/ops/ module:
  otel.py: record_decision_span + emit_alert_event; graceful noop when
    opentelemetry-sdk is not installed; setup_otel reads
    OTEL_EXPORTER_OTLP_ENDPOINT
  syslog_cef.py: format_cef_record for all 6 alertable event classes per
    DESIGN.md 8.5; ArcSight CEF v0 format; escapes |, =, backslash, newlines
  posture.py: compute_posture (parse audit JSONL) + render_posture_report
    (text or JSON); PostureError on missing log
- Add 'posture' CLI command (--audit-log, --json)
- Add alertable event constants and CEF severity map to _const.py
- Add optional [ops] extra in pyproject.toml for opentelemetry deps
- 35 new tests in tests/test_m5_ops.py covering constants, CEF format,
  posture computation, OTel noop, and posture CLI

248 tests passing; pre-commit clean.

* M6: Embedded console + Console API

- Add src/bawbel_gate/console/ module:
  auth.py: mint_token (256-bit random hex) + verify_token (hmac.compare_digest)
  state.py: ConsoleState + serialize_state for GET /v1/state shape per DESIGN.md 13.1
  approvals.py: ApprovalRegistry with first-writer-wins race semantics (thread-safe)
  server.py: ConsoleServer (stdlib http.server) serving:
    GET /v1/state (auth required, full snapshot)
    GET /v1/events (SSE stream of audit records)
    POST /v1/approvals/{id} (console approval channel; 409 on second write)
    GET / (embedded UI bundle; no build-time network fetch)
    all other paths -> 404 (enforces read-only property)
- Add --console HOST:PORT flag to 'serve' command
- 31 new tests in tests/test_m6_console.py:
  bearer token, state serializer, approval registry races, HTTP endpoints,
  read-only guarantee (no manifest/taint/tool-call mutation paths)

279 tests passing; pre-commit clean.

* M7: bawbel-hub fleet ingest, state store, enrollment, and posture

- Add src/bawbel_gate/hub/ module:
  store.py: SQLite fleet state store (idempotent upsert by gate/session/seq,
    rebuild from records, chain head tracking, quarantine table)
  ingest.py: process_batch() with per-session chain verification; raises
    ChainMismatch on tamper evidence (-> 409); replays are idempotent
  enroll.py: EnrollmentRegistry with single-use tokens and admin tokens
  fleet.py: compute_fleet_posture() -> list[GatePostureRow] for fleet console
  server.py: HubServer with:
    GET  /v1/fleet/state       -- fleet posture snapshot
    GET  /v1/fleet/gates/{id}  -- per-gate state
    POST /v1/gates/{id}/records -- NDJSON ingest (chain-verified)
    POST /v1/enrollment/tokens  -- mint enrollment token
    POST /v1/enroll             -- consume token, get gate_id
    (no command channel; all other paths -> 404, DESIGN.md 14.6)
- Add src/bawbel_gate/ingest/ module:
  client.py: IngestCursor (cursor file per session), build_batch() (reads
    audit JSONL past the acked cursor, max_records cap)
- CLI: 'enroll' command, 'hub serve' + 'hub token new' subcommands
- 36 new tests in tests/test_m7_hub.py covering store, chain verification,
  enrollment lifecycle, fleet posture, HTTP server, ingest client

315 tests passing; pre-commit clean.

* Update README to reflect M0-M7 feature-complete state; fix E501 in test_m7_hub

Rewrites README as the authoritative operator reference covering all 8 milestones.
Fixes two E501 violations in tests/test_m7_hub.py (lines 110, 131) surfaced by pre-commit.

* Remove hard-coded network/timing values; add env var overrides for all runtime tunables

All default ports, hosts, and internal timing values moved to _const.py.
CLI options for console addr, hub host/port, hub db, and audit log path now
accept env vars (BAWBEL_CONSOLE_ADDR, BAWBEL_HUB_HOST, BAWBEL_HUB_PORT,
BAWBEL_HUB_DB, BAWBEL_AUDIT_LOG, OTEL_EXPORTER_OTLP_ENDPOINT already existed).
Adds .env.example documenting every supported env var. Adds .env and *.db to
.gitignore so local runtime files are never committed.

* Add complete step-by-step testing guide (docs/TESTING.md)

* Remove .env.example; document env vars inline in TESTING.md

.env files are readable by subprocesses and unsuitable for a security product.
Env var reference now lives in docs/TESTING.md and README only.
.env stays in .gitignore as defence-in-depth.

* Wire the MCP multiplexer request loop; fix hub enrollment auth

serve/serve --console previously did nothing: the enforce/learn loop was a
stub that printed a placeholder and returned, killing the console's daemon
thread before it was reachable. mux/proxy.py implements the real request
loop per DESIGN.md 3.3/6.2: spawn upstream servers, merge/namespace
tools/list, route every tools/call through the policy engine (manifest ->
taint -> trifecta -> approval), forward on allow/approve, deny without
touching upstream, and write the hash-chained audit trail.

Also fixes bawbel-hub's /v1/enroll, which required the admin bearer token
an unenrolled gate doesn't have yet -- the single-use enrollment token is
itself the credential (DESIGN.md 14.3), so the documented enroll flow was
unreachable.

Adds tests/test_mux_proxy.py (drives the real loop end-to-end against a
fake stdio MCP server) and two hub tests covering the enroll bootstrap
path over HTTP, the gap that let the auth bug ship untested.

* Add README badges, architecture diagram, and real CLI screenshots

Badges (license, Python version, status, test count, fail-closed posture)
and a mermaid diagram of the manifest -> taint -> trifecta enforcement
pipeline, so the README reads at a glance.

Screenshots for harden, serve --console, and hub are captured from the
now-working commands (see previous commit) rather than mocked. Also
updates docs/TESTING.md test counts and a few pre-existing wording
mismatches against actual CLI output (lint, audit verify, pin format)
found while running the guide end-to-end, and documents that console-
driven approval routing isn't wired yet.

* Add a minimal browser UI to bawbel-hub

The hub had no browser-viewable page: /v1/fleet/state is JSON-only and
requires an Authorization header a plain browser navigation can't send.
DESIGN.md 14.4 specs a fleet console view ("the console... plus a fleet
dimension") that was never built -- only the raw API existed.

Adds a / route serving an embedded page in the same minimal style as the
gate's own console UI (fetch + <pre>, same-origin so no CORS is needed),
and a ?t=<admin_token> query-param auth fallback for browser navigation,
mirroring how the gate console already handles its SSE endpoint.

* Build a real console dashboard; fix a concurrency bug that starved it

The embedded console's UI was a bare <pre> dump of raw JSON with SSE
events only logged to devtools, not rendered. Replaces it with a real
dashboard: sessions/servers tables, a pending-approvals panel wired to
POST /v1/approvals/{id}, and a live-scrolling audit feed driven by the
existing SSE stream.

Building it surfaced a real bug: ConsoleServer used a plain
http.server.HTTPServer, which handles one request at a time. /v1/events
is a long-lived SSE stream, so a single connected client (e.g. the
dashboard's own EventSource) starved every other request -- state polls,
approvals, even the page itself -- until that connection closed. Switches
to ThreadingHTTPServer. Verified live (curl hung against the old server
mid-SSE-stream, returns instantly after the fix) and covered by a new
regression test that opens an SSE connection and asserts /v1/state still
responds; confirmed it fails against the old code before the fix.

Drops the harden/hub screenshots from the README per discussion: hub is
moving to its own repo (ARCHITECTURE.md Layer 5a), so further UI
investment belongs there, not here.

* Chore: verifiable-hygiene pass (badges, disclosure policy, governance docs) (#5)

Adds what SECURITY.md already claimed was in place but wasn't: .github/
didn't exist at all, so "we track advisories via Dependabot and OpenSSF
Scorecard" was aspirational, not true.

- .github/dependabot.yml: weekly pip + github-actions update checks.
- .github/workflows/scorecard.yml: OpenSSF Scorecard analysis, published
  to the public API. Note: Scorecard evaluates the default branch (main),
  which is currently just the initial commit -- the badge will read low
  until main is updated with the real code on develop.
- GOVERNANCE.md: honest solo-maintainer model (CLAUDE.md already says
  "solo-maintained"; this makes the decision process and bus-factor
  explicit instead of leaving it implicit).
- CODE_OF_CONDUCT.md: Contributor Covenant v2.1, enforcement contact
  reused from SECURITY.md (no other contact channel existed).
- README: badges for Scorecard, security policy, code of conduct, DCO;
  links CONTRIBUTING.md to the new governance/conduct docs.
- Fixes a stale test count (324 -> 327) in README and TESTING.md, left
  over from before the last merge.

Commit signing is already real (GPG-signed, GitHub-verified) but nothing
requires it -- develop has no branch protection at all. Enabling
"required signed commits" needs two sequential gh api calls that the
permission layer wouldn't let me run non-interactively even after
confirmation; commands are in the PR description / chat history for the
maintainer to run directly.

* Chore: add Matt Pocock skills (#7)

* Chore: verifiable-hygiene pass (badges, disclosure policy, governance docs)

Adds what SECURITY.md already claimed was in place but wasn't: .github/
didn't exist at all, so "we track advisories via Dependabot and OpenSSF
Scorecard" was aspirational, not true.

- .github/dependabot.yml: weekly pip + github-actions update checks.
- .github/workflows/scorecard.yml: OpenSSF Scorecard analysis, published
  to the public API. Note: Scorecard evaluates the default branch (main),
  which is currently just the initial commit -- the badge will read low
  until main is updated with the real code on develop.
- GOVERNANCE.md: honest solo-maintainer model (CLAUDE.md already says
  "solo-maintained"; this makes the decision process and bus-factor
  explicit instead of leaving it implicit).
- CODE_OF_CONDUCT.md: Contributor Covenant v2.1, enforcement contact
  reused from SECURITY.md (no other contact channel existed).
- README: badges for Scorecard, security policy, code of conduct, DCO;
  links CONTRIBUTING.md to the new governance/conduct docs.
- Fixes a stale test count (324 -> 327) in README and TESTING.md, left
  over from before the last merge.

Commit signing is already real (GPG-signed, GitHub-verified) but nothing
requires it -- develop has no branch protection at all. Enabling
"required signed commits" needs two sequential gh api calls that the
permission layer wouldn't let me run non-interactively even after
confirmation; commands are in the PR description / chat history for the
maintainer to run directly.

* add matt pocock skills

* Add CI security scan pipeline (code, dependency, secret) (#13)

* Add CI security scan pipeline: code scan, dependency scan, secret scan

Runs on every PR and push to develop/main:

- code-scan: CodeQL Python analysis, results in the repo's code scanning
  alerts.
- dependency-scan: actions/dependency-review-action (fails PRs that
  introduce high-severity vulnerable or license-incompatible deps) plus
  pip-audit against the actual installed dependency tree, PR-only since
  dependency-review compares base vs head. Upgrades pip/setuptools
  before auditing -- verified locally that a stock venv's bundled
  setuptools 59.6.0 carries three CVEs of its own, which would fail
  every run on bootstrap tooling rather than this project's real
  dependencies.
- secret-scan: Gitleaks, full history fetch, push and PR.

Also enabled at the repo level (not a file change, so not in this diff):
secret scanning, secret scanning push protection, and Dependabot
security updates -- all were "disabled", including secret scanning,
which is normally free and on by default for public repos.

None of these are required status checks yet -- the "Protect develop"
ruleset has no required_status_checks rule, so right now these run and
report but do not block a merge. That needs a separate ruleset update.

* Fix secret-scan and dependency-scan CI failures found on first PR run

Both new jobs failed immediately on PR #13's first run:

- gitleaks/gitleaks-action requires a paid GITLEAKS_LICENSE for
  org-owned repos (bawbel is an org; the free tier is personal-account
  only). Switched to calling the gitleaks CLI directly -- the tool
  itself is fully open source (Apache-2.0), only the GitHub Action
  wrapper has the org-licensing gate. Version pinned (8.30.1) for
  reproducible runs rather than resolved at run time.

- dependency-review-action needs the repo's Dependency graph feature
  enabled; it wasn't. Enabled via the vulnerability-alerts API (repo
  setting, not a file change).

Running gitleaks against this repo's actual history (locally, before
wiring it back in) found 6 "leaks" -- all in tests/test_m2_policy.py,
which deliberately contains fake JWT/GitLab-PAT strings as fixtures for
policy/manifest.py's own secret-scan detector, not real credentials.
Added .gitleaks.toml with a path-scoped allowlist for that one file
rather than disabling the check; verified locally it suppresses exactly
those six and nothing else ("no leaks found").

* Add CHANGELOG.md; README: security badges + gate.yaml sample

Splits ci.yml into three separate workflow files (code-scan.yml,
dependency-scan.yml, secret-scan.yml) -- GitHub's native workflow
badges are per-workflow-file, not per-job, so a combined workflow
can't produce three distinct badges. Same jobs, same triggers, just
separated so each has its own README badge.

README additions:
- Badges for the three new security scans, pointing at develop (where
  they actually run continuously; main doesn't have them yet).
- The gate.yaml sample from DESIGN.md 3.1, since the Quickstart section
  referenced --config gate.yaml several times without ever showing
  what one looks like. Verified against the real config loader
  (bawbel_gate.mux.config.load_config), not just pasted in --
  it parses cleanly.

CHANGELOG.md: Keep a Changelog format, single [Unreleased] section.
No version has been tagged yet, so there's nothing to backdate against;
the M0-M7 build stays in README's milestone table rather than being
itemized here. Covers everything since that baseline: the multiplexer
fix, hub fixes, console dashboard, governance docs, PyPI packaging, and
this CI security pipeline.

* Implement PyPI packaging; fix an install-breaking schema-path bug (#12)

* Implement PyPI packaging: fix a real install-breaking bug, add metadata

_schemas.py located schemas/ via Path(__file__).parent.parent.parent,
which only resolves inside an editable/source checkout. A real
`pip install bawbel-gate` wheel does not carry that path at all --
every schema-dependent command (lint, verify, harden, manifest loading
in general) would have raised FileNotFoundError for anyone installing
from PyPI. Confirmed by building the wheel, installing it into a clean
venv, and running `bawbel-gate lint` against it: fails without the fix,
passes with it.

Fix: bundle schemas/ into the wheel itself via
[tool.hatch.build.targets.wheel.force-include], and make _schemas.py
prefer that packaged copy over the repo-root path when both could
resolve. Extracted the selection into _resolve_schema_dir() with two
unit tests (packaged-present, packaged-missing) since the priority
logic is exactly what was wrong.

Also completes pyproject.toml's PyPI metadata (readme, authors,
classifiers, keywords, project.urls, SPDX license expression instead of
the deprecated {text=...} form) and adds a release-triggered publish
workflow using PyPI trusted publishing (OIDC, no stored API token) that
runs the full test suite and a real wheel-install smoke test before
ever uploading.

Verified: `python -m build` succeeds, `twine check dist/*` passes on
both sdist and wheel, wheel installed + exercised in a throwaway venv,
329 tests passing, pre-commit clean.

Not done here, and can't be from this repo: creating the PyPI project
itself and configuring its trusted publisher (repo/workflow/environment
name) at pypi.org -- that requires the maintainer's own PyPI account.
Until that's done and a release is actually cut, README's "not yet on
PyPI" status stays accurate; not flipping it preemptively.

* Split publish workflow: auto to TestPyPI, manual approval to PyPI

publish-testpypi runs automatically after build (skip-existing so a
re-run of the workflow doesn't hard-fail on a version already there).
publish-pypi now needs publish-testpypi and targets the "pypi"
environment, which has a required_reviewers protection rule -- that's
what actually pauses the job for manual approval, not anything in this
file. Both environments (testpypi unprotected, pypi with chaksaray as
required reviewer, self-review allowed since this is solo-maintained)
are already configured on the repo.

* Rename publish.yml jobs to match bawbel-scanner's convention

Verify Tests / Build Distribution / Publish to TestPyPI / Publish to PyPI,
for a consistent job graph across the bawbel repos. Behavior unchanged --
the pypi environment's required_reviewers rule still gates the last job.

* Expand keywords for discoverability; fix a real sdist leak

Adds ~15 more PyPI search keywords covering the actual surface area
(mcp-server, agentic-ai, taint-tracking, capability-based-security,
supply-chain-security, etc.) instead of the original 6.

While checking sdist size, found that hatchling's sdist `include` key
only adds to its default "everything not gitignored" file set -- it
does not replace it. Since .agents/ (148K of untracked Claude Code
skill files, never part of this project) isn't in .gitignore, it was
silently ending up in the sdist tarball. Switched to `only-include`,
which is a strict allowlist and ignores the default VCS-based set
entirely.

This means the sdist's contents no longer depend on whatever untracked
files happen to be sitting in the builder's working directory --
actually reproducible, not just small.

Verified: wheel already had zero stray files (packages= is a proper
allowlist by nature, this was sdist-only). Rebuilt after both changes:
twine check passes on both artifacts, wheel installed and exercised in
a throwaway venv, 329 tests passing, pre-commit clean. Wheel ~77KB,
sdist ~94KB either way -- the leak was small in bytes, but the
nondeterminism was the real bug.

* Fix missing trailing newline in setup-gate-skill/SKILL.md

* chore: issue templates, PR template, CODEOWNERS, contributing rules (#17)

Adopts the issue-first workflow ahead of the Factory unblock sequence.

- .github/ISSUE_TEMPLATE: bug_report, feature_request, design_change
  forms, each with an area dropdown. config.yml disables blank issues
  and routes vulnerability reports to private advisories (SECURITY.md).
- .github/PULL_REQUEST_TEMPLATE.md: Closes line, output contract,
  verification block, invariant-ordering block for policy/, taint/,
  audit/, integrity/ changes.
- .github/CODEOWNERS: * @chaksaray (solo-maintained, GOVERNANCE.md).
- CONTRIBUTING.md: new "Issue before PR" section; new "Property test
  before implementation" subsection requiring separate, unsquashed
  commits; Pull requests section now states that CI does not run the
  test suite, instead of claiming it does.
- gate-implementation skill workflow: issue-first step, property test
  committed on its own, PR references the issue.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* fix: MCP surface coverage for provenance tagging (#21)

* test: replay tests for MCP surface coverage

Refs #20. Drives run_enforce() against tests/fixtures/hostile_mcp_server.py,
an upstream that serves injected content on resources/*, prompts/*, a
notifications/message, and a sampling/createMessage request sent ahead of
each tools/call response.

Pass at this commit (lock in current behavior):
- every non-tools host request is rejected with -32601 and never forwarded
- a rejected resources/read leaves taint and untrusted_seen unchanged
- initialize advertises the tools capability only
- host notifications are neither forwarded nor answered
- tools/call results are still provenance-tagged

Fail at this commit (proxy.py reads the next upstream message as the
response without matching its id):
- an upstream notification is returned as the tool result
- results shift by one call after an interleaved message
- the upstream's sampling/createMessage request is never answered

tests/proxy_harness.py holds the in-memory host driver that
test_mux_proxy.py previously inlined.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* fix: match upstream responses by id, refuse unsolicited upstream messages

Refs #20. _send_and_recv returned the next message an upstream wrote,
whatever it was. A notification or a server-to-client request written
ahead of a tools/call response was returned to the host as the result,
the real response was then returned for the next call to that server,
and the upstream's request was never answered.

_send_and_recv now returns only the response whose id matches the
request it sent (value and type). Everything else is handled at the gate
and never reaches the host:
- notifications: dropped
- server-to-client requests (sampling/createMessage, roots/list,
  elicitation/create, ...): answered upstream with -32601
- responses to ids the gate did not send, non-object messages: dropped
  with a stderr warning

Provenance tagging is unchanged: it ran on every forwarded call before
and still does. Upstream EOF propagates exactly as before.

JSONRPC_METHOD_NOT_FOUND replaces the -32601 literal in proxy.py, and
transport.warn is public so proxy.py writes warnings the same way.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: enumerate the MCP surface in DESIGN.md 3.3

Refs #20. 3.3 named tools/call and the provenance tagger but said nothing
about any other MCP message. It now lists, for host to gate, gate to
upstream, and upstream to gate, what the gate answers, forwards, refuses,
and drops, matching mux/proxy.py as of the previous commit.

States the two paths by which upstream content reaches the host: a
tools/call result (tagged) and tool descriptions in the merged tools/list
(untagged, pinned only when the manifest declares tool_schema_integrity).
resources/read and prompts/get are not proxied in v1; a release that
proxies a content-bearing method must tag its response first.

No new policy semantics: no effect, grant, taint, or trifecta rule
changes.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* chore: restore trailing newlines dropped in 7e1ea48

pre-commit end-of-file-fixer fails on develop at 7e1ea48 for these two
files. Whitespace only.

Signed-off-by: chaksaray <chaksaray@gmail.com>

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: README decision pipeline diagram (#23)

* docs: replace README pipeline diagram with the DESIGN.md 5.2 decision flow

Replaces the left-to-right overview diagram with a top-down decision flow:
capability manifest (5.2 steps 1-4), session taint (step 5), trifecta
invariant (step 6), then allow / approve / deny, with every decision
written to the audit log.

Two labels differ from the drafted diagram because the drafted text
claimed behavior the code does not have:
- Deny says "-32031 with the deny reason", not "cites the AVE id":
  policy/engine.py resolve() never populates Decision.ave, so data.ave
  is always [].
- Steps 1-4 have their own edge to Deny: resolve() returns early on no
  grant, a failed condition, or a guard hit.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: drop <small> from diagram labels; GitHub clips their last line

Signed-off-by: chaksaray <chaksaray@gmail.com>

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: restructure README opening; add usage walkthrough and command table (#24)

* docs: replace README pipeline diagram with the DESIGN.md 5.2 decision flow

Replaces the left-to-right overview diagram with a top-down decision flow:
capability manifest (5.2 steps 1-4), session taint (step 5), trifecta
invariant (step 6), then allow / approve / deny, with every decision
written to the audit log.

Two labels differ from the drafted diagram because the drafted text
claimed behavior the code does not have:
- Deny says "-32031 with the deny reason", not "cites the AVE id":
  policy/engine.py resolve() never populates Decision.ave, so data.ave
  is always [].
- Steps 1-4 have their own edge to Deny: resolve() returns early on no
  grant, a failed condition, or a guard hit.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: lead README with what the gate does; add usage walkthrough and command table

Opening: one-line description, the JSON-RPC-only statement, a quickstart,
and doc links come first. Badges and the status section move into a
details block.

Usage replaces the Install, Quickstart, and CLI reference sections with
seven steps: gate.yaml, client config, learning mode, review and
synthesize, lint/pin/harden, enforce, audit. Each step states what
develop 7e1ea48 does, checked by running it against a stdio test server
and a FastMCP server:
- learning mode needs a manifest file per server (placeholder documented)
- learn synthesize overwrites <server>.cap.yaml (write drafts elsewhere)
- learning-mode observations hold 32-char argument prefixes in clear text
- the audit_log directory must exist; the path is relative to the cwd
- the terminal approval channel reads stdin, the client's JSON-RPC
  stream, so client-launched approvals resolve to deny
- clear appends a SESSION_CLEAR record and does not reach a running gate
- harden's default mitigations path exists only in a clone
"Not yet supported" lists the remaining gaps.

Commands: one table row per command with arguments, defaults, env vars,
and exit behavior, taken from cli.py rather than DESIGN.md 11.5.

Also: install from develop (not on PyPI; main lacks the schema bundling
fix), test count 327 -> 329, clone URL -> bawbel/gate, and removed the
Documentation links to the gitignored IMPLEMENTATION_PLAN.md and
BAWBEL_GATE_MITIGATIONS_SPEC.md. The demo GIF line is left out until the
recording exists.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: drop <small> from diagram labels; GitHub clips their last line

Signed-off-by: chaksaray <chaksaray@gmail.com>
(cherry picked from commit 6497b04a5bd952ff66513d2ed074902288147a22)

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: show README badges under the title; un-hide the status section (#26)

The badges and the status section were collapsed in a details block
(#24). Badges now sit directly under the title, and Status is a normal
section again, so both are visible on the first screen.

Also:
- Test count 329 -> 337 in the badge, the status text, and the dev
  install comment. #21 added 8 tests; 337 pass on develop 973d83c.
- The "Not yet supported" item on non-tools MCP methods now cites
  DESIGN.md 3.3, which #21 added.

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: remove the unclosed details block from README; fit code blocks without scrolling (#31)

* docs: remove the unclosed details block from README; fit code blocks without scrolling

Merge 451cd3c (main into develop) resolved a README conflict by keeping
both sides. It re-inserted the old opening above the badges: a second
copy of the description, quickstart, and doc links, followed by
<details><summary>Badges and project status</summary> with no closing
</details>. GitHub therefore collapsed everything after the doc links,
the diagram, Usage, Commands, and every later section, into one closed
"Badges and project status" block. This restores the README from #26.

Code blocks: measured on github.com at a 1280 px viewport, the pin
snippet (471 px), harden commands (47 px), and the learning-mode client
config (6 px) needed horizontal scrolling. Every displayed code line is
now 80 characters or less: comments moved above commands, long commands
wrapped with backslash continuations, and the pin snippet builds its
JSON-RPC lines with jq (tested against a FastMCP server; same pin hash).

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: put name, description, badges, and links at the top of README

Top order is now: title, description, badges, doc links, then the
quickstart and Status.

Also reformats the step 6 deny example one field per line. Its fence is
indented inside a list item, so the 80-character check in the previous
commit missed its 89-character line.

Signed-off-by: chaksaray <chaksaray@gmail.com>

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: ACS conformance assessment; import the ACS mapping and threat model

Refs #41. Measured against ACS db5c89e4 (2026-10-09, version 0.1.3,
specification v0.1.0).

docs/ACS_CONFORMANCE.md (new): verdict (not ACS-Core conformant, no
claim of any profile), role mapping, the ten ACS-Core requirements with
status and gate evidence for each, the optional profiles, where the
gate's defaults are stricter than ACS's (no fail-open; ACS-Core's
on_decision_failure defaults to proceed), section 6 listing what was not
run (input to runbook step 13), and re-measure commands.

docs/ACS_GATE_MAPPING.md (imported from the sprint kit, corrected): the
status line records db5c89e4 and 0.1.3; the reference implementation
exists; the gate already exports OTel; the section 4 claim that the
hash-chained audit is a differentiator is struck (ACS-Core requires a
chain); former section 7 (vendor the APC library as the policy engine)
is withdrawn; the section 6 coupling risk flag is kept. A provenance
section lists each correction.

docs/THREAT_MODEL.md (imported, rewritten): defers to DESIGN.md 2 for
T1 to T6 and covers only what 2.1 does not, as X1 to X10, each with the
gate's status today. The draft's T1 to T10 collided with DESIGN.md's IDs
and are retired. X8 records that binary and remote_resource pins are not
enforced (#42).

Signed-off-by: chaksaray <chaksaray@gmail.com>

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>
(cherry picked from commit aac890f2d2c8e4f839e7016bee1439266f90cc33)
Signed-off-by: chaksaray <chaksaray@gmail.com>

* docs: correct README claims the code does not back; Status alpha (0.1.x)

See #46. CEF/syslog, alert emission without OTel, gate-side hub push, Postgres, hub quarantine and gate.chain.gap, deny reasons table, Status wording, dead PiranhaDB link. SECURITY.md supported versions; no hard-coded test count; em dashes removed from docs/TESTING.md; sandboxing keyword removed.

Signed-off-by: chaksaray <chaksaray@gmail.com>

---------

Signed-off-by: chaksaray <chaksaray@gmail.com>
…ed (#51)

The per-gate mTLS client certificate in 14.3 and the 13.2 transport is
not built. Ingest authenticates with the shared admin token, gate_id
comes from the request path, and hub serve listens on plain HTTP. Say
so in 14.3 so the spec does not read as a description of the code.

DESIGN.md only, per the Design change form. No behaviour change.

Refs #49

Signed-off-by: chaksaray <chaksaray@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
README: add per-gate identity, TLS, and token expiry to the bawbel-hub
section, and add a fleet ingest entry to Not yet supported, which the
Status section points to for gaps.

ARCHITECTURE: mark the gate-side push and per-gate mTLS identity as
specified, not implemented, in Flow D and the bawbel-hub entry, add a
Status line, and change "M7 next" to "M7 partial".

Docs only. Measured on 05ad472: no TLS code in src/, ingest checks
the shared admin token, gate_id comes from the request path, no token
expiry, and nothing outside tests calls the ingest client.

Closes #49

Signed-off-by: chaksaray <chaksaray@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The badge linked to #status-alpha-01x, which no heading produces. The
rendered README on develop has user-content-status-v010-feature-complete-pre-release
for the Status heading. Link to that. Heading wording is DEC-16 and is
not changed.

Closes #60

Signed-off-by: chaksaray <chaksaray@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ions (#65)

Pin all 20 uses: lines to the commit their current tag or branch points
to, with the version in a trailing comment, so behavior does not change
and Dependabot can keep the pins current. Add a top-level permissions
block (contents: read) to publish.yml; its publish jobs keep their own
id-token: write.

Scorecard flagged both: Pinned-Dependencies 0 and Token-Permissions 0
("no topLevel permission defined: publish.yml"). Hash-pinned pip
installs and a self-scan job are left for the maintainer (#63).

Refs #63

Signed-off-by: chaksaray <chaksaray@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The sibling repos list bawbel.io@gmail.com; gate listed
security@bawbel.io in SECURITY.md and CODE_OF_CONDUCT.md. Maintainer
decision: bawbel.io@gmail.com.

Closes #67

Signed-off-by: chaksaray <chaksaray@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
chaksaray and others added 2 commits October 18, 2026 22:04
…el/scanner (#70)

AVE is an independent project (aveproject/ave, aveproject.org@gmail.com);
bawbel/ave redirects there. The scanner repository is bawbel/scanner.
SECURITY.md and CONTRIBUTING.md named the old repositories.

Closes #69

Signed-off-by: chaksaray <chaksaray@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ts (#72)

ARCHITECTURE.md said the gate was at "DESIGN -> M1, M1 next", AVE was
"live: schema 1.2, 51 records" at ave.bawbel.io, and PiranhaDB was live.
Measured: gate is alpha 0.1.0 on PyPI with M5 and M7 partial; AVE is an
independent project (aveproject/ave) with 87 records on schema 1.1.0;
api.piranha.bawbel.io does not resolve. Flow C now says the
record-to-stanza step is planned: no AVE record carries
capability_mitigation, and harden --ave reads gate's own mitigations
file. README.md and the ave-record-authoring skill drop ave.bawbel.io.

DESIGN.md (header dependency, 9.2) waits on the schema decision in #71.

Refs #71

Signed-off-by: chaksaray <chaksaray@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@chaksaray
chaksaray merged commit 694f6a9 into main Oct 18, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant