Skip to content
Open
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
87 changes: 87 additions & 0 deletions .claude/skills/action-handler-development/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
name: action-handler-development
description: Add a new engine action handler to the CEG engine
---

# Action Handler Development

An action is a named entry in the single ingress `POST /v1/execute`. Handlers own
domain logic; the chassis owns HTTP, auth, tenant resolution, and packet framing
(Contracts 1–3).

## Signature

```python
async def handle_<action>(tenant: str, payload: dict[str, Any]) -> dict[str, Any]:
```

Return a **flat domain dict**. Do not build a `{"status": ..., "data": ...}`
envelope — `chassis/actions.py` wraps the return value via `deflate_egress()`
into a response `PacketEnvelope`. Wrapping it yourself double-nests the payload.

## Seven registration points

An action is only live when all seven are updated. Missing any one produces a
route that 404s, bypasses auth, or fails a contract test.

| # | File | What to add |
|---|------|-------------|
| 1 | `engine/handlers.py` | The `handle_<action>` function |
| 2 | `engine/handlers.py` → `register_all()` | `chassis_router.register_handler("<action>", handle_<action>)` |
| 3 | `chassis/actions.py` → `_engine_handlers` | `"<action>": handle_<action>` + the import |
| 4 | `engine/auth/capabilities.py` → `ACTION_PERMISSION_MAP` | `"<action>": "<scope>:<read\|write>"` |
| 5 | `engine/packet/packet_envelope.py` → `PacketType` | enum member if the action emits a new packet kind |
| 6 | `tests/contracts/_constants.py` → `KNOWN_ACTIONS` | the action name |
| 7 | `docs/contracts/api/openapi.yaml` | add to the `action` enum and the request examples |

Point 4 is the security-critical one: an action absent from `ACTION_PERMISSION_MAP`
has no capability requirement.

## Handler preamble

Every handler opens with the same four calls, in this order:

```python
graph_driver, domain_loader = _require_deps()
_validate_tenant_access(tenant, "<action>")
domain_spec = domain_loader.load_domain(tenant)
_enforce_capability(tenant, "<action>", domain_spec)
```

## Payload validation

Handlers validate explicitly and raise `ValidationError` with `action` and
`tenant` context — they do not assume a Pydantic model is applied upstream:

```python
entity_type = payload.get("entity_type")
if not entity_type:
raise ValidationError("entity_type required", action="<action>", tenant=tenant)
```

Document the payload schema in the docstring under `Payload schema:` — the
contract tests read handler docstrings.

## Cypher rules

- Labels and property names: `sanitize_label()` before interpolation (Contract 9)
- Values: always `$params` passed to `graph_driver.execute_query(...)`
- Scope every query with `database=domain_spec.domain.id` (Contract 3)

## Contracts to read first

- `docs/contracts/HANDLER_PAYLOADS.md`
- `docs/contracts/FIELD_NAMES.md`
- `docs/contracts/RETURN_VALUES.md`
- `docs/contracts/METHOD_SIGNATURES.md`

## Tests

Add to `tests/unit/test_handlers.py`:

- Valid payload returns the documented keys
- Missing required field raises `ValidationError`
- Caller without the mapped capability is rejected
- Injection attempt in a label-bearing field is rejected

Then run `make agent-check`.
61 changes: 58 additions & 3 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# --- L9_META ---
# l9_schema: 1
# l9_schema: 2
# origin: l9-template
# engine: graph
# layer: [ci]
# tags: [L9_TEMPLATE, ci, audit, harness]
# owner: platform
# tags: [delivery, harness]
# status: active
# --- /L9_META ---
name: L9 Audit Harness
Expand All @@ -14,6 +13,10 @@ on:
push:
branches: [main]

permissions:
contents: read
pull-requests: write

jobs:
audit:
runs-on: ubuntu-latest
Expand All @@ -35,3 +38,55 @@ jobs:
with:
name: l9-audit-reports
path: artifacts/

- name: Post harness report to PR
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const marker = '<!-- l9-audit-harness -->';
const reportPath = 'artifacts/harness_report.md';
const pr = context.payload.pull_request;

let report;
try {
report = fs.readFileSync(reportPath, 'utf8');
} catch (err) {
report = '⚠️ Audit harness did not produce a report at `' + reportPath +
'`. Check the [workflow run](' +
`https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` +
') for details.';
}

const maxLen = 60000;
if (report.length > maxLen) {
report = report.slice(0, maxLen) + '\n\n...(truncated — see workflow run artifacts for the full report)';
}
const body = `${marker}\n${report}`;

const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
per_page: 100
});
const botComment = [...comments].reverse().find(c =>
c.user.type === 'Bot' && c.body.includes(marker)
);

if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body
});
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ htmlcov/
# Current work (scratch / WIP)
current work/

# memory-bank/ is T0 resume SSOT — MUST stay tracked.
# Do not add memory-bank to this file or ~/.gitignore_global.

# Temporary
*.tmp
*.temp
Expand All @@ -63,3 +66,4 @@ secrets.json

# l9-ci-sdk runtime checkout (provisioned by tools/packet_envelope_gate.py)
.l9/runtime/
.venv-py312-mypy/
39 changes: 0 additions & 39 deletions .suite6-config.json

This file was deleted.

30 changes: 29 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
<!-- L9_META
l9_schema: 2
origin: engine-specific
engine: graph
layer: [agent-rules]
tags: [governance]
status: active
/L9_META -->

# AGENTS.md — L9 Graph Cognitive Engine

Cross-tool agent instructions for the CEG repository. Read by Claude Code, Codex, Cursor, Copilot, Jules, Aider, CodeRabbit, and all AGENTS.md-compatible tools.
Expand Down Expand Up @@ -65,7 +74,8 @@ tools/ # contract_scanner.py, verify_contracts.py, validate_do
- Type hints on every function signature
- Pydantic v2 BaseModel for all structured data
- `ruff format .` before commit (Black-compatible, 88-char)
- `structlog.get_logger(__name__)` for logging — never configure structlog in engine
- `logging.getLogger(__name__)` or `structlog.get_logger(__name__)` for logging — match the
surrounding module, and never configure logging in engine (see `docs/contracts/OBSERVABILITY.md`)
- Exception messages: `msg = f"..."; raise ValueError(msg)` (avoids EM101)
- Nullable: `x: str | None = None` — never `Optional`
- Datetime: always `datetime.now(tz=UTC)`
Expand Down Expand Up @@ -118,4 +128,22 @@ tools/ # contract_scanner.py, verify_contracts.py, validate_do
| `docs/TROUBLESHOOTING.md` | 10 common failure scenarios with diagnosis + resolution | Debugging errors, CI failures |
| `docs/AI_AGENT_REVIEW_CHECKLIST.md` | PR review rubric, severity scoring, comment templates | Reviewing PRs (CodeRabbit, Qodo, Claude) |
| `docs/CI_PIPELINE.md` | 7 CI phases, 15 pre-commit hooks, blocking vs advisory | CI failure diagnosis |
| `docs/CI_CONSTELLATION_BOUNDARY.md` | What's wired (`l9-ci-core`/`l9-ci-sdk`) vs. not (`l9-harness`/`l9-assurance`); extend-don't-replace rule for `audit.yml` | Before wiring any Quantum-L9 constellation repo, or touching `tools/audit_harness.py` / `.github/workflows/audit.yml` |
| `.claude/rules/contracts.md` | 24 contracts + enforcement matrix (automated vs manual) | Contract uncertainty |

<!-- BEGIN L9 FORMATTER OWNERSHIP (generated — do not edit) -->

## Formatter ownership

Workspace class: `biome_default` — Default for every governed workspace: Biome owns JS/TS/JSON, Ruff owns Python.

Exactly one formatter owns each language. Do not reformat a file with a tool other than its owner, and do not add config for a competing formatter: the result is a diff that churns on every save.

| Languages | Owner | Note |
|---|---|---|
| `javascript`, `javascriptreact`, `typescript`, `typescriptreact`, `json`, `jsonc` | **biome** | bound by the governed IDE profile |
| `python` | **ruff** | bound by the governed IDE profile |

Generated from `environment/ide/policy.json` in the governance clone by `ops/scripts/adapters/agentdocs.sh`. Edit the policy, not this block.

<!-- END L9 FORMATTER OWNERSHIP -->
29 changes: 29 additions & 0 deletions DEFERRED.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,32 @@ All inline TODO comments must be migrated here with a unique ID, owner, rational
**Priority:** HIGH — required for any LLM-powered feature to function

---

## DEFERRED-003

**Title:** `contracts/contract_NN.yaml` registry does not conform to `test_contract_registry.py` schema

**File:** `contracts/contract_01.yaml` through `contract_24.yaml` (all 24, committed via PR #70)

**Owner:** engine-team

**Rationale:** `tests/contracts/test_contract_registry.py` (added alongside the dual-chassis/SDK migration work) enforces a newer contract-YAML schema than the one the existing 24 `contracts/*.yaml` files were authored against:
- Every contract YAML is missing the required `docs: [...]` key entirely.
- Every contract's `verification.test` points at a retired per-contract test file (e.g. `tests/contracts/test_contract_01.py`) instead of a pytest node ID into the current monolithic `tests/contracts/test_contracts.py` (20 classes, not a 1:1 match against 24 contracts).
- Two aggregate checks also fail: every doc in `verify_contracts.py::REQUIRED_CONTRACTS` must be claimed by some contract's `docs` list, and every scanner rule ID in `tools/contract_scanner.py` must be claimed by some contract's `verification.scanner_rules` list.

Fixing this requires a deliberate, per-contract mapping decision (which of the 29 `docs/contracts/*.md` files and which `test_contracts.py` class each of the 24 contracts corresponds to) — not a mechanical fix, and getting the mapping wrong would create false confidence in a compliance-tracking system. Deferred rather than guessed.

**Acceptance Criteria:**
- Every `contracts/contract_NN.yaml` has a non-empty `docs` list of files that exist under `docs/contracts/`
- Every `contracts/contract_NN.yaml`'s `verification.test` is a resolvable `tests/contracts/test_contracts.py::ClassName` node ID
- Every `verification.scanner_rules` entry exists in `tools/contract_scanner.py`
- Every doc in `tools/verify_contracts.py::REQUIRED_CONTRACTS` is claimed by at least one contract's `docs` list
- Every scanner rule ID in `tools/contract_scanner.py` is claimed by at least one contract's `verification.scanner_rules` list
- `pytest tests/contracts/test_contract_registry.py` passes in full

**Blocked by:** Requires the contract author (or someone with full context on the 24-contract ↔ 29-doc ↔ 20-test-class intended mapping) to make the mapping decisions

**Priority:** MEDIUM — compliance/audit tooling gap, not a functional regression; existing `tools/verify_contracts.py` and `tools/contract_scanner.py` still run and enforce their own (older) contract set independently

---
6 changes: 5 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
# ── Stage 1: Dependencies ──────────────────────────────────
FROM python:3.12-slim AS deps

# git is required to resolve the git+https constellation-node-sdk dependency
RUN apt-get update && apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /build
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
Expand Down Expand Up @@ -46,6 +50,6 @@ EXPOSE 8000

# Health check
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
CMD python -c "import httpx; r=httpx.get('http://localhost:8000/v1/health'); exit(0 if r.status_code==200 else 1)"
CMD python -c "import httpx; r=httpx.get('http://localhost:8000/v1/health'); exit(0 if r.status_code==200 and r.json().get('ready', True) else 1)"

ENTRYPOINT ["/app/entrypoint.sh"]
15 changes: 12 additions & 3 deletions Dockerfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@

WORKDIR /build

# git is required to resolve the git+https constellation-node-sdk dependency
# (both by `poetry install` below and by the pip fallback)
RUN apt-get update && apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*

# Install build deps
RUN pip install --no-cache-dir poetry && \
poetry config virtualenvs.create false

COPY pyproject.toml poetry.lock* ./
RUN poetry install --no-dev --no-interaction --no-ansi 2>/dev/null || \
pip install --no-cache-dir fastapi uvicorn neo4j pydantic pydantic-settings pyyaml apscheduler redis structlog numpy scipy RestrictedPython
# --only main: install runtime deps, skip the [dev] group.
# --no-root: install dependencies only, not the l9-engine project itself
# (the "current project" install needs README.md, which isn't in the
# build context yet, and isn't needed — engine/domains/chassis are
# copied as raw source into the runtime stage below, not pip-installed).
RUN poetry install --only main --no-root --no-interaction --no-ansi

Check warning on line 32 in Dockerfile.prod

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Poetry allows execution of setup scripts on source builds by default. Make sure it is safe here.

See more on https://sonarcloud.io/project/issues?id=Quantum-L9_Cognitive.Engine.Graphs&issues=AZ-lTSdJdsAKk-AF0P16&open=AZ-lTSdJdsAKk-AF0P16&pullRequest=151

COPY . .

Expand Down Expand Up @@ -50,6 +59,6 @@
EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/v1/health')"
CMD python -c "import json,sys,urllib.request; sys.exit(0 if json.load(urllib.request.urlopen('http://localhost:8000/v1/health')).get('ready', True) else 1)"

CMD ["uvicorn", "chassis.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Loading
Loading