Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/security-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: security-audit

on:
pull_request: {}
schedule:
- cron: "0 6 * * 1" # weekly Monday 06:00 UTC

concurrency:
group: security-audit-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
pip-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
- run: uv python install 3.10
- name: Export locked dependencies
run: uv export --all-extras --no-hashes > /tmp/requirements.txt
- name: Install pip-audit
run: uv tool install pip-audit
- name: Audit dependencies
run: pip-audit --no-deps --disable-pip -r /tmp/requirements.txt
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ When superpowers skills default to `docs/superpowers/specs/`, use `planning/spec
- **Backward-compat aliases for renames**: when renaming a public class, add a silent module-level alias (`OldName = NewName`) at the end of the file. Re-export both names from `__init__.py` if the old name was publicly exported. Aliases are class assignments, not subclasses — same class object, so `isinstance` behavior is preserved.
- **Frozen-config bypass in `__post_init__`**: it's acceptable to use `object.__setattr__(self, "field", value)` inside a frozen config's `__post_init__` to set a field that requires other config values to construct. Document with a one-line comment naming the trade-off (user-facing immutability vs. construction-time mutation).
- **Optional-import guard pattern**: top-level conditional imports (`if import_checker.is_X_installed: import X`) keep optional dependencies actually optional. Code that references `X` is only reached when `check_dependencies()` has already returned True; the runtime invariant is maintained by the inline `is_configured → check_dependencies → instantiate` flow in `BaseBootstrapper.__init__`. See "Type checking" below.
- **`from_dict` vs `from_object` accept different shapes for `None`**: `BaseConfig.from_dict({"service_name": None})` succeeds and explicitly overrides the default with `None`. `BaseConfig.from_object(obj)` where `obj.service_name is None` filters the attribute out and the dataclass default takes over. The asymmetry is documented in both methods' docstrings (`instruments/base.py:17, 23`) and pinned by tests in `tests/test_config.py:54-94`. Pick `from_dict` if explicit-None override is the load-bearing semantic.

### Type checking

Expand Down
5 changes: 5 additions & 0 deletions lite_bootstrap/bootstrappers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ def __init__(self, bootstrap_config: BaseConfig) -> None:
category=InstrumentDependencyMissingWarning,
stacklevel=3,
)
logger.warning(
"instrument %s skipped: %s",
instrument_type.__name__,
instrument_type.missing_dependency_message,
)
continue
self.instruments.append(instrument_type(bootstrap_config=self.bootstrap_config))

Expand Down
Loading
Loading