Skip to content
Draft
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
72 changes: 72 additions & 0 deletions .github/workflows/deployment-guide-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Single-Tenant Deployment Guide Smoke

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

jobs:
configuration-contract:
name: Fail-closed configuration contract (not cloud evidence)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.12"
cache: pip

- name: Set up pnpm
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
with:
version: 10.23.0

- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "20"
cache: pnpm
cache-dependency-path: apps/web/pnpm-lock.yaml

- name: Install test dependencies
run: |
python -m venv .venv
./.venv/bin/python -m pip install -e '.[dev]'

- name: Install web test dependencies
run: pnpm --dir apps/web install --frozen-lockfile

- name: Test the exact-origin web trust contract
run: pnpm --dir apps/web test -- lib/api.test.ts

- name: Test the deployment contract
run: >-
./.venv/bin/python -m pytest
tests/unit/test_single_tenant_deployment.py
tests/unit/test_config.py::test_production_api_settings_do_not_require_migration_database_url
-q -p no:cacheprovider

- name: Emit explicitly non-cloud evidence
run: |
mkdir -p artifacts/phase5
./.venv/bin/python scripts/run_single_tenant_deployment_smoke.py \
--environment ephemeral_ci \
--output artifacts/phase5/single-tenant-deployment-smoke.json

- name: Upload sanitized configuration receipt
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: single-tenant-deployment-config-contract
path: artifacts/phase5/single-tenant-deployment-smoke.json
if-no-files-found: error
retention-days: 14
92 changes: 92 additions & 0 deletions .github/workflows/ops-evidence.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Phase 5 Ops Evidence

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

jobs:
ops-evidence:
name: Backup, restore, upgrade, and monitoring evidence
runs-on: ubuntu-24.04
timeout-minutes: 30
services:
postgres:
image: pgvector/pgvector:pg16@sha256:1d533553fefe4f12e5d80c7b80622ba0c382abb5758856f52983d8789179f0fb
env:
POSTGRES_USER: alicebot_admin
POSTGRES_PASSWORD: ci
POSTGRES_DB: alicebot
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U alicebot_admin -d alicebot"
--health-interval 5s
--health-timeout 5s
--health-retries 20
env:
DATABASE_ADMIN_URL: postgresql://alicebot_admin:ci@localhost:5432/alicebot
DATABASE_URL: postgresql://alicebot_app:ci@localhost:5432/alicebot
steps:
- name: Checkout full history
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.12"
cache: pip

- name: Select PostgreSQL 16 client tools
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends postgresql-client-16
echo "/usr/lib/postgresql/16/bin" >> "$GITHUB_PATH"
/usr/lib/postgresql/16/bin/pg_dump --version
/usr/lib/postgresql/16/bin/pg_restore --version

- name: Install Alice and test dependencies
run: |
python -m venv .venv
./.venv/bin/python -m pip install -e '.[dev]'

- name: Bootstrap the role-separated application role
env:
PGHOST: localhost
PGPASSWORD: ci
POSTGRES_USER: alicebot_admin
POSTGRES_DB: alicebot
ALICEBOT_APP_USER: alicebot_app
ALICEBOT_APP_PASSWORD: ci
run: infra/postgres/init/001_roles.sh

- name: Run evidence contract tests
run: >-
./.venv/bin/python -m pytest
tests/unit/test_phase5_ops_evidence.py
tests/unit/test_phase5_enterprise_handoff_truth.py
-q -p no:cacheprovider

- name: Execute both backend drills
run: |
mkdir -p artifacts/phase5
./.venv/bin/python scripts/run_phase5_ops_evidence.py \
--backend all \
--work-dir "${RUNNER_TEMP}" \
--output artifacts/phase5/ops-evidence.json

- name: Upload sanitized evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: phase5-ops-evidence
path: artifacts/phase5/ops-evidence.json
if-no-files-found: error
retention-days: 14
5 changes: 4 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2222,7 +2222,10 @@ jobs:
DATABASE_ADMIN_URL: postgresql://alicebot_admin:ci@localhost:5432/alicebot
run: |
unset ALICE_LEGACY_SURFACES ALICE_MCP_LEGACY_TOOLS ALICE_AGENT_API_KEY
./.venv/bin/python -m pytest tests/integration/test_default_surface_integration.py -q -p no:cacheprovider --require-executed-tests
./.venv/bin/python -m pytest \
tests/integration/test_default_surface_integration.py \
tests/integration/test_openai_agents_sdk_tool.py \
-q -p no:cacheprovider --require-executed-tests

web:
name: Web tests, types, accessibility, and budgets
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ REVIEW_REPORT.md
!docs/handoff/2026-07-18-v0.12.0-phase3-structural-refactor/REVIEW_REPORT.md
!docs/handoff/2026-07-19-v0.13.0-phase4-sprint4-synthesis/BUILD_REPORT.md
!docs/handoff/2026-07-19-v0.13.0-phase4-sprint4-synthesis/REVIEW_REPORT.md
!docs/handoff/2026-07-21-v0.14.0-phase5-enterprise-track/BUILD_REPORT.md
!docs/handoff/2026-07-21-v0.14.0-phase5-enterprise-track/REVIEW_REPORT.md
ARCHIVE_RECOMMENDATIONS.md
RECOMMENDED_ADRS.md

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ release-static:
$(PYTHON) scripts/check_control_doc_truth.py
$(PYTHON) scripts/release_check.py
$(PYTHON) -m ruff check apps/api/src/alicebot_api scripts tests eval/longmemeval
$(PYTHON) -m mypy --ignore-missing-imports apps/api/src/alicebot_api scripts/release_check.py scripts/test_distribution_artifact.py scripts/normalize_sdist.py scripts/render_release_body.py scripts/decode_github_release_body.py scripts/prepare_mainprotect_update.py scripts/check_python_coverage.py scripts/check_control_doc_truth.py scripts/check_github_release_checks.py scripts/check_release_controls_attestation.py
$(PYTHON) -m mypy --ignore-missing-imports apps/api/src/alicebot_api scripts/release_check.py scripts/test_distribution_artifact.py scripts/normalize_sdist.py scripts/render_release_body.py scripts/decode_github_release_body.py scripts/prepare_mainprotect_update.py scripts/check_python_coverage.py scripts/check_control_doc_truth.py scripts/check_github_release_checks.py scripts/check_release_controls_attestation.py scripts/run_phase5_ops_evidence.py scripts/_phase5_ops_seed.py scripts/run_single_tenant_deployment_smoke.py

release-identity:
git fetch --no-tags origin main
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ What that means in practice:
- [Custom agent guide](https://github.com/samrusani/AliceBot/blob/main/docs/alpha/custom-agent-guide.md)
- [Known limitations](https://github.com/samrusani/AliceBot/blob/main/docs/alpha/known-limitations.md)
- [Backup and restore](https://github.com/samrusani/AliceBot/blob/main/docs/alpha/backup-and-restore.md)
- [Disaster recovery](https://github.com/samrusani/AliceBot/blob/main/docs/runbooks/disaster-recovery.md)
- [Health and monitoring](https://github.com/samrusani/AliceBot/blob/main/docs/runbooks/health-and-monitoring.md)
- [Upgrade v0.12.0 to current](https://github.com/samrusani/AliceBot/blob/main/docs/runbooks/upgrade-v0.12-to-current.md)
- [Security and privacy](https://github.com/samrusani/AliceBot/blob/main/docs/alpha/security-and-privacy.md)
- [v0.10.4 release notes](https://github.com/samrusani/AliceBot/blob/main/docs/release/v0.10.4-release-notes.md)
- [v0.11.0 release notes](https://github.com/samrusani/AliceBot/blob/main/docs/release/v0.11.0-release-notes.md)
Expand Down
60 changes: 51 additions & 9 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Security Policy

## Supported Scope
## Supported Versions

Alice is pre-1.0 software. Security fixes target the latest minor release series (currently `v0.9.x`); older tags are not maintained. Security posture in this repo is scoped to the shipped local/runtime surfaces and deterministic verification paths on the current baseline.
Alice is pre-1.0 software. Security fixes target the latest published minor
release series. Older minor series and development snapshots are not maintained
with security backports; move to the latest published release before reporting
or validating a fix. The default branch is useful for reproducing a forthcoming
fix, but it is not a supported release until it is tagged and published.

## Reporting a Vulnerability

Expand All @@ -17,14 +21,52 @@ Do not open public issues for active security vulnerabilities.

## Security Boundaries

- Postgres remains the system of record.
- User-owned data paths are RLS-governed.
- Public CLI/MCP/importer surfaces should not bypass trust/provenance boundaries.
- Alice is local-first, single-user, self-hosted software. It is not a
multi-tenant service boundary.
- **Keyless means local-machine-owner trust.** A keyless vNext deployment is
safe only when the API remains on loopback and every local process and OS user
that can reach it is trusted as the Alice owner. In this mode a caller-supplied
`user_id` is routing context, not proof of identity.
- Once a user has any active agent API key, protected `/v0/vnext` requests for
that user reject keyless access and require `Authorization: Bearer
alice_sk_...`. A browser-clipper one-time capability is a deliberately narrow
exception for its single capture route; it is not a general API credential.
- Do not expose a keyless API or MCP process to a LAN, container peer, public
interface, or untrusted reverse-proxy client. Remote access requires active
agent keys, a TLS-terminating authenticated reverse proxy, a restrictive CORS
allowlist, and host/firewall controls.
- PostgreSQL runtime access uses the application role and per-user RLS. Keep the
admin database credential confined to migrations and administrative recovery.
SQLite is a single-user on-ramp protected by owner-only filesystem
permissions, not by database RLS.
- Public CLI, MCP, connector, and importer surfaces must preserve provenance and
policy boundaries. Imported and provider-returned text is data, never policy.
- Consequential side effects remain approval-bounded.

The shipped threat model and current evidence boundaries are documented in
[`docs/security/`](docs/security/README.md).

## Browser Clipper Credentials

Visited-page JavaScript is hostile credential context. The bookmarklet must
never contain, request, or persist an agent API key or reusable connector
`capture_token`.

The trusted Alice UI issues a short-lived, origin-bound, one-time capability for
the selected page origin. The visited page can observe that narrow capability,
so it may make the one authorized submission; it cannot replay it, redeem it
from another origin, or turn it into general Alice access. Trusted API clients
may still use Bearer authentication plus a reusable `capture_token`, but must
never pass that reusable token to a bookmarklet or other visited-page script.

## Hardening Notes

- keep `.env` local and do not commit secrets
- keep local services bound to loopback where possible
- treat per-agent API keys (`alicebot agent keys create`) as secrets; they are stored hashed, printed exactly once, and can be revoked with `alicebot agent keys revoke`
- run verification commands before release tagging
- Keep `.env` files local and do not commit secrets.
- Keep API and web services bound to loopback unless the authenticated TLS
deployment boundary above is in place.
- Treat per-agent API keys (`alicebot agent keys create`) as secrets. Alice
stores a SHA-256 verifier and short identification prefix, displays the raw
key once, and supports revocation with `alicebot agent keys revoke`.
- Treat logs, exports, backups, and imported source archives as sensitive user
data even when application secrets have been redacted.
- Run the release verification and security evidence commands before tagging.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Add one-time, origin-bound browser clip capabilities.

Only a SHA-256 digest is persisted. Expiry is derived from the database
clock and redemption is a single conditional UPDATE so concurrent requests
cannot both consume the same capability.

Revision ID: 20260721_0094
Revises: 20260721_0093
"""

from __future__ import annotations

from alembic import op


revision = "20260721_0094"
down_revision = "20260721_0093"
branch_labels = None
depends_on = None


_UPGRADE_SCHEMA = """
CREATE TABLE browser_clip_capabilities (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
capability_hash text NOT NULL UNIQUE,
origin text NOT NULL,
expires_at timestamptz NOT NULL,
consumed_at timestamptz NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (id, user_id),
CONSTRAINT browser_clip_capabilities_hash_check
CHECK (capability_hash ~ '^[0-9a-f]{64}$'),
CONSTRAINT browser_clip_capabilities_origin_length_check
CHECK (length(origin) BETWEEN 8 AND 2048),
CONSTRAINT browser_clip_capabilities_expiry_range_check
CHECK (
expires_at > created_at
AND expires_at <= created_at + interval '5 minutes'
),
CONSTRAINT browser_clip_capabilities_consumed_range_check
CHECK (consumed_at IS NULL OR consumed_at >= created_at)
);

CREATE INDEX browser_clip_capabilities_live_expiry_idx
ON browser_clip_capabilities (user_id, expires_at)
WHERE consumed_at IS NULL;

CREATE INDEX browser_clip_capabilities_consumed_idx
ON browser_clip_capabilities (user_id, consumed_at)
WHERE consumed_at IS NOT NULL;
"""

_GRANTS = (
"GRANT SELECT, INSERT, UPDATE, DELETE ON browser_clip_capabilities TO alicebot_app",
)

_POLICY = """
CREATE POLICY browser_clip_capabilities_is_owner
ON browser_clip_capabilities
USING (user_id = app.current_user_id())
WITH CHECK (user_id = app.current_user_id());
"""


def upgrade() -> None:
op.execute(_UPGRADE_SCHEMA)
for statement in _GRANTS:
op.execute(statement)
op.execute("ALTER TABLE browser_clip_capabilities ENABLE ROW LEVEL SECURITY")
op.execute("ALTER TABLE browser_clip_capabilities FORCE ROW LEVEL SECURITY")
op.execute(_POLICY)


def downgrade() -> None:
op.execute("DROP TABLE IF EXISTS browser_clip_capabilities")
Loading
Loading