Repository navigation
Conversation
* 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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.