umbra-core 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- umbra_core-0.1.0/.github/workflows/ci.yml +77 -0
- umbra_core-0.1.0/.github/workflows/release.yml +75 -0
- umbra_core-0.1.0/.gitignore +26 -0
- umbra_core-0.1.0/.umbra/admission.yaml +30 -0
- umbra_core-0.1.0/LICENSE +21 -0
- umbra_core-0.1.0/Makefile +17 -0
- umbra_core-0.1.0/PKG-INFO +272 -0
- umbra_core-0.1.0/README.md +241 -0
- umbra_core-0.1.0/demos/injection/demo.py +251 -0
- umbra_core-0.1.0/demos/injection/fixture-repo/.umbra/admission.yaml +18 -0
- umbra_core-0.1.0/demos/injection/fixture-repo/README.md +15 -0
- umbra_core-0.1.0/demos/injection/fixture-repo/package.json +7 -0
- umbra_core-0.1.0/docs/INTEGRATIONS.md +101 -0
- umbra_core-0.1.0/docs/RELEASING.md +46 -0
- umbra_core-0.1.0/integrations/git-hooks/pre-push +42 -0
- umbra_core-0.1.0/integrations/github-action/action.yml +141 -0
- umbra_core-0.1.0/integrations/github-action/example-workflow.yml +31 -0
- umbra_core-0.1.0/pyproject.toml +60 -0
- umbra_core-0.1.0/tests/__init__.py +0 -0
- umbra_core-0.1.0/tests/test_accountability.py +204 -0
- umbra_core-0.1.0/tests/test_cli_and_mcp.py +145 -0
- umbra_core-0.1.0/tests/test_executors.py +228 -0
- umbra_core-0.1.0/tests/test_injection_demo.py +42 -0
- umbra_core-0.1.0/tests/test_pipeline.py +257 -0
- umbra_core-0.1.0/umbra_core/__init__.py +101 -0
- umbra_core-0.1.0/umbra_core/cli.py +199 -0
- umbra_core-0.1.0/umbra_core/executors/__init__.py +16 -0
- umbra_core-0.1.0/umbra_core/executors/_shared.py +67 -0
- umbra_core-0.1.0/umbra_core/executors/base.py +108 -0
- umbra_core-0.1.0/umbra_core/executors/claude_code.py +179 -0
- umbra_core-0.1.0/umbra_core/executors/codex.py +147 -0
- umbra_core-0.1.0/umbra_core/executors/null.py +46 -0
- umbra_core-0.1.0/umbra_core/executors/registry.py +53 -0
- umbra_core-0.1.0/umbra_core/mcp_server.py +114 -0
- umbra_core-0.1.0/umbra_core/pipeline/__init__.py +104 -0
- umbra_core-0.1.0/umbra_core/pipeline/_helpers.py +52 -0
- umbra_core-0.1.0/umbra_core/pipeline/admission.py +422 -0
- umbra_core-0.1.0/umbra_core/pipeline/checks.py +245 -0
- umbra_core-0.1.0/umbra_core/pipeline/contract.py +403 -0
- umbra_core-0.1.0/umbra_core/pipeline/passport.py +218 -0
- umbra_core-0.1.0/umbra_core/pipeline/provenance.py +132 -0
- umbra_core-0.1.0/umbra_core/pipeline/receipt.py +208 -0
- umbra_core-0.1.0/umbra_core/pipeline/transparency.py +231 -0
- umbra_core-0.1.0/umbra_core/pipeline/trust_boundary.py +261 -0
- umbra_core-0.1.0/umbra_core/pipeline/verifier.py +167 -0
- umbra_core-0.1.0/uv.lock +984 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v5
|
|
16
|
+
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v6
|
|
19
|
+
|
|
20
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
21
|
+
run: uv python install ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Install
|
|
24
|
+
run: uv sync --extra dev --python ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Lint
|
|
27
|
+
run: uv run ruff check .
|
|
28
|
+
|
|
29
|
+
- name: Test
|
|
30
|
+
run: uv run pytest -q
|
|
31
|
+
|
|
32
|
+
- name: Verify prompt-injection defense (ungoverned vs governed)
|
|
33
|
+
run: uv run python demos/injection/demo.py
|
|
34
|
+
|
|
35
|
+
# Dogfood: umbra-core governs its own pull requests. Runs the admission
|
|
36
|
+
# pipeline on the PR's change (against .umbra/admission.yaml) and fails the
|
|
37
|
+
# check unless it earns at least Analyze (L1). This is the same governance
|
|
38
|
+
# any repo gets from the Umbra Admission action.
|
|
39
|
+
self-admission:
|
|
40
|
+
if: github.event_name == 'pull_request'
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v5
|
|
44
|
+
with:
|
|
45
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
46
|
+
fetch-depth: 0
|
|
47
|
+
|
|
48
|
+
- name: Install uv
|
|
49
|
+
uses: astral-sh/setup-uv@v6
|
|
50
|
+
|
|
51
|
+
- name: Install
|
|
52
|
+
run: uv sync --extra dev
|
|
53
|
+
|
|
54
|
+
- name: Stage the PR change as a working-tree diff
|
|
55
|
+
run: |
|
|
56
|
+
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
|
57
|
+
git fetch --no-tags origin "$BASE_SHA" || true
|
|
58
|
+
git reset --soft "$BASE_SHA" 2>/dev/null && git restore --staged . 2>/dev/null || true
|
|
59
|
+
|
|
60
|
+
- name: Umbra self-admission
|
|
61
|
+
run: |
|
|
62
|
+
uv run umbra --json admit . \
|
|
63
|
+
--agent none \
|
|
64
|
+
--mission "Governed change to umbra-core; must stay within the repo contract and pass the suite." \
|
|
65
|
+
--label "bkd-dotcom/umbra-core" \
|
|
66
|
+
--min-authority 1 \
|
|
67
|
+
--receipt-out umbra-receipt.json | tee umbra-report.json
|
|
68
|
+
|
|
69
|
+
- name: Upload receipt
|
|
70
|
+
if: always()
|
|
71
|
+
uses: actions/upload-artifact@v5
|
|
72
|
+
with:
|
|
73
|
+
name: umbra-self-receipt
|
|
74
|
+
path: |
|
|
75
|
+
umbra-receipt.json
|
|
76
|
+
umbra-report.json
|
|
77
|
+
if-no-files-found: ignore
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes umbra-core to PyPI when a version tag is pushed (e.g. v0.1.0).
|
|
4
|
+
# Uses PyPI Trusted Publishing (OIDC) — no API token is stored as a secret.
|
|
5
|
+
#
|
|
6
|
+
# One-time setup on PyPI (https://pypi.org/manage/account/publishing/):
|
|
7
|
+
# Project name: umbra-core
|
|
8
|
+
# Owner: bkd-dotcom
|
|
9
|
+
# Repository name: umbra-core
|
|
10
|
+
# Workflow name: release.yml
|
|
11
|
+
# Environment name: pypi
|
|
12
|
+
#
|
|
13
|
+
# Then: git tag v0.1.0 && git push origin v0.1.0
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
push:
|
|
17
|
+
tags:
|
|
18
|
+
- "v*"
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
build:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v5
|
|
28
|
+
|
|
29
|
+
- name: Install uv
|
|
30
|
+
uses: astral-sh/setup-uv@v6
|
|
31
|
+
|
|
32
|
+
- name: Verify the tag matches the package version
|
|
33
|
+
run: |
|
|
34
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
35
|
+
PKG=$(grep -m1 '^version' pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/')
|
|
36
|
+
echo "tag=$TAG pkg=$PKG"
|
|
37
|
+
if [ "$TAG" != "$PKG" ]; then
|
|
38
|
+
echo "::error::Tag v$TAG does not match pyproject version $PKG"; exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
- name: Test before publishing
|
|
42
|
+
run: |
|
|
43
|
+
uv sync --extra dev
|
|
44
|
+
uv run ruff check .
|
|
45
|
+
uv run pytest -q
|
|
46
|
+
|
|
47
|
+
- name: Build sdist + wheel
|
|
48
|
+
run: uv build
|
|
49
|
+
|
|
50
|
+
- name: Check distribution metadata
|
|
51
|
+
run: uvx twine check dist/*
|
|
52
|
+
|
|
53
|
+
- name: Upload build artifacts
|
|
54
|
+
uses: actions/upload-artifact@v5
|
|
55
|
+
with:
|
|
56
|
+
name: dist
|
|
57
|
+
path: dist/
|
|
58
|
+
|
|
59
|
+
publish:
|
|
60
|
+
needs: build
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
environment:
|
|
63
|
+
name: pypi
|
|
64
|
+
url: https://pypi.org/project/umbra-core/
|
|
65
|
+
permissions:
|
|
66
|
+
id-token: write # OIDC token for Trusted Publishing
|
|
67
|
+
steps:
|
|
68
|
+
- name: Download build artifacts
|
|
69
|
+
uses: actions/download-artifact@v5
|
|
70
|
+
with:
|
|
71
|
+
name: dist
|
|
72
|
+
path: dist/
|
|
73
|
+
|
|
74
|
+
- name: Publish to PyPI
|
|
75
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
|
|
11
|
+
# Test / coverage
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
htmlcov/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
|
|
17
|
+
# Env / secrets
|
|
18
|
+
.env
|
|
19
|
+
.env.*
|
|
20
|
+
!.env.example
|
|
21
|
+
|
|
22
|
+
# Editor / OS
|
|
23
|
+
.DS_Store
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
*.swp
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
task_type: library-change
|
|
3
|
+
# umbra-core governs its own pull requests with this contract. It bounds what an
|
|
4
|
+
# agent's change may touch and requires the suite to pass to earn branch-PR
|
|
5
|
+
# authority. Scope is generous (this is a library repo) but fails closed on the
|
|
6
|
+
# things an untrusted change must never touch.
|
|
7
|
+
allowed_paths:
|
|
8
|
+
- "umbra_core/**"
|
|
9
|
+
- "tests/**"
|
|
10
|
+
- "demos/**"
|
|
11
|
+
- "integrations/**"
|
|
12
|
+
- "docs/**"
|
|
13
|
+
- ".umbra/**"
|
|
14
|
+
- "*.md"
|
|
15
|
+
- "pyproject.toml"
|
|
16
|
+
- "Makefile"
|
|
17
|
+
- "uv.lock"
|
|
18
|
+
- ".github/**"
|
|
19
|
+
forbidden_paths:
|
|
20
|
+
- "**/.env*"
|
|
21
|
+
- "**/*secret*"
|
|
22
|
+
- "**/id_rsa*"
|
|
23
|
+
- "**/*.pem"
|
|
24
|
+
max_files_changed: 60
|
|
25
|
+
required_checks:
|
|
26
|
+
- "pytest"
|
|
27
|
+
network: deny
|
|
28
|
+
authority_on_success: branch_pr_only
|
|
29
|
+
policy_owner: bkd-dotcom
|
|
30
|
+
policy_version: "1.0"
|
umbra_core-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Binay Dalai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.PHONY: test lint build verify-injection install
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
uv sync --extra dev || uv pip install -e ".[dev]"
|
|
5
|
+
|
|
6
|
+
test:
|
|
7
|
+
uv run pytest
|
|
8
|
+
|
|
9
|
+
lint:
|
|
10
|
+
uv run ruff check .
|
|
11
|
+
|
|
12
|
+
build:
|
|
13
|
+
uv build
|
|
14
|
+
|
|
15
|
+
# Verify the prompt-injection defense: the same agent, ungoverned vs. governed.
|
|
16
|
+
verify-injection:
|
|
17
|
+
uv run python demos/injection/demo.py
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: umbra-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An agent-agnostic change-control plane for coding agents. Governs Codex, Claude Code, Cursor, or any agent behind one admission pipeline and proves every change with a signed receipt.
|
|
5
|
+
Project-URL: Homepage, https://github.com/bkd-dotcom/umbra-core
|
|
6
|
+
Project-URL: Repository, https://github.com/bkd-dotcom/umbra-core
|
|
7
|
+
Project-URL: Issues, https://github.com/bkd-dotcom/umbra-core/issues
|
|
8
|
+
Author: Binay Dalai
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agents,change-control,claude-code,codex,coding-agents,governance,prompt-injection,security,slsa,supply-chain
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: cryptography>=42.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
28
|
+
Provides-Extra: mcp
|
|
29
|
+
Requires-Dist: mcp>=1.0.0; extra == 'mcp'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# umbra-core
|
|
33
|
+
|
|
34
|
+
**An agent-agnostic change-control plane for coding agents.**
|
|
35
|
+
|
|
36
|
+
Coding agents can now change your repository. `umbra-core` is the layer that
|
|
37
|
+
decides how much authority a given change has earned — and proves it — for
|
|
38
|
+
**any** agent. Codex, Claude Code, Cursor, or a future agent are all governed by
|
|
39
|
+
one admission pipeline and adapted behind a single interface:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Executor (protocol)
|
|
43
|
+
├── CodexExecutor → codex exec (disposable checkout, no push/merge)
|
|
44
|
+
├── ClaudeCodeExecutor → claude -p (--bare: no CLAUDE.md auto-read, push/merge tools denied)
|
|
45
|
+
└── <your agent> → one adapter, no pipeline change
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The governing insight: **a coding agent cannot approve its own authority to make
|
|
49
|
+
a change.** The patch-writer is never the patch-approver. `umbra-core` is the
|
|
50
|
+
layer that can decide — agent-agnostically — and seals every decision in a
|
|
51
|
+
signed receipt.
|
|
52
|
+
|
|
53
|
+
## Why this is agent-agnostic (and why that matters)
|
|
54
|
+
|
|
55
|
+
Tools like Claude Code and Codex can *find* and *fix* issues — that's the
|
|
56
|
+
commoditized half. None of them govern *themselves*: none decide whether an
|
|
57
|
+
agent is **allowed** to make a change, quarantine untrusted repo text before the
|
|
58
|
+
agent reads it, verify the result independently, or emit a cryptographic proof
|
|
59
|
+
of the authority earned. `umbra-core` sits one layer above every agent and does
|
|
60
|
+
exactly that.
|
|
61
|
+
|
|
62
|
+
Claude Code runs `--bare`, so it does **not** auto-ingest `CLAUDE.md` — the
|
|
63
|
+
trust boundary, not the agent, decides what untrusted repository text the agent
|
|
64
|
+
may see. Push/commit/merge tools are refused at the CLI layer, so a governed run
|
|
65
|
+
can only ever *propose* a change.
|
|
66
|
+
|
|
67
|
+
## Install & govern everywhere
|
|
68
|
+
|
|
69
|
+
One core (`run_admission`), five checkpoints an agent's change must pass through
|
|
70
|
+
— see [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install umbra-core
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Surface | Governs | Command |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **PyPI package** | anything you script | `pip install umbra-core` |
|
|
79
|
+
| **CLI + git hook** | the agent on your machine | `umbra admit . --mission "..." --agent claude-code` |
|
|
80
|
+
| **GitHub Action** | **every** agent's PR (Claude Code, Codex, Cursor, Copilot, Devin) | [`bkd-dotcom/umbra-action@v1`](https://github.com/bkd-dotcom/umbra-action) |
|
|
81
|
+
| **MCP server** | agents that speak MCP | `python -m umbra_core.mcp_server` |
|
|
82
|
+
| **Hosted API** | any CI/agent that posts a change | see [umbra.engineer](https://umbra.engineer) |
|
|
83
|
+
|
|
84
|
+
The GitHub Action is the highest-reach checkpoint: it sits at the repo, so it
|
|
85
|
+
governs *any* agent that opens a PR. Make **"Umbra Admission"** a required status
|
|
86
|
+
check and nothing merges without a signed receipt. `auto_merge` is always false.
|
|
87
|
+
|
|
88
|
+
## Who it's for
|
|
89
|
+
|
|
90
|
+
- **Teams adopting coding agents** who need agent changes to be *bounded and
|
|
91
|
+
auditable* without turning every PR into an unbounded trust decision. Turn on
|
|
92
|
+
the required check; every agent PR arrives with a verdict and a signed receipt.
|
|
93
|
+
- **Platform / security engineers** enforcing a change-control policy for
|
|
94
|
+
autonomous agents (allowed paths, required checks, no secrets, no
|
|
95
|
+
prompt-injection-driven scope creep) uniformly across every agent in use.
|
|
96
|
+
- **Supply-chain / compliance owners** who need cryptographic, verifiable
|
|
97
|
+
evidence of *what an agent was allowed to change and why* — receipts map to
|
|
98
|
+
in-toto/SLSA provenance and enter an append-only transparency log.
|
|
99
|
+
|
|
100
|
+
It is **not** a replacement for code review or a coding agent. It is the
|
|
101
|
+
governance layer between the two: the agent proposes, umbra-core decides how much
|
|
102
|
+
authority the change earned and proves it, a human merges.
|
|
103
|
+
|
|
104
|
+
## Executor interface
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from umbra_core import resolve_available, get_executor
|
|
108
|
+
|
|
109
|
+
# pick the first available agent (honoring a preference order)
|
|
110
|
+
agent = resolve_available(["claude-code", "codex-cli"])
|
|
111
|
+
|
|
112
|
+
# or ask for one explicitly
|
|
113
|
+
agent = get_executor("claude-code")
|
|
114
|
+
|
|
115
|
+
result = agent.propose("bump the vulnerable dependency", repo_path=checkout)
|
|
116
|
+
print(result.executor) # "claude-code" | "codex-cli" | "unavailable"
|
|
117
|
+
print(result.diff) # recomputed from git on the final tree
|
|
118
|
+
print(result.model_identity) # honest provenance for the receipt
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Enable agents via environment flags (off by default, fail-closed):
|
|
122
|
+
|
|
123
|
+
- `UMBRA_ENABLE_CODEX_CLI=true` (+ `codex login`)
|
|
124
|
+
- `UMBRA_ENABLE_CLAUDE_CODE=true` (+ authenticated `claude` CLI)
|
|
125
|
+
|
|
126
|
+
## The admission pipeline
|
|
127
|
+
|
|
128
|
+
One governed, deterministic pipeline runs before any change is trusted — and it
|
|
129
|
+
is **identical for every executor**, so the verdict depends only on the evidence
|
|
130
|
+
the run produced, never on which agent ran:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
load executable contract (.umbra/admission.yaml)
|
|
134
|
+
→ redact untrusted repository text on disk (README / AGENTS.md / CLAUDE.md / …)
|
|
135
|
+
→ run required checks on the BASE commit (isolated worktree: regression vs pre-existing)
|
|
136
|
+
→ run the bounded task via ANY Executor in a disposable checkout
|
|
137
|
+
→ evaluate the changeset against the contract (deterministic, outside the model)
|
|
138
|
+
→ re-run required checks on the CHANGED tree (allowlisted profiles, secret-stripped env)
|
|
139
|
+
→ independently verify it (the patch-writer can't self-approve)
|
|
140
|
+
→ grant only the authority the run EARNED (0 observe · 1 analyze · 2 branch-PR)
|
|
141
|
+
→ seal it in an Ed25519-signed Remediation Receipt
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from pathlib import Path
|
|
146
|
+
from umbra_core import get_executor, run_admission, build_receipt, verify_receipt
|
|
147
|
+
|
|
148
|
+
agent = get_executor("claude-code")
|
|
149
|
+
report = run_admission(
|
|
150
|
+
repo_path=Path(checkout),
|
|
151
|
+
repo_label="acme/app",
|
|
152
|
+
mission="update the vulnerable dependency to its fixed version; change only manifests",
|
|
153
|
+
executor=agent,
|
|
154
|
+
)
|
|
155
|
+
print(report.authority_level, report.authority) # e.g. 2 branch_pr
|
|
156
|
+
print(report.outcome)
|
|
157
|
+
|
|
158
|
+
# seal + independently verify the signed receipt
|
|
159
|
+
envelope = build_receipt(
|
|
160
|
+
repo=report.repo, base_commit=report.base_commit, contract=report.contract,
|
|
161
|
+
contract_result=report.contract_result, verifier=report.verifier,
|
|
162
|
+
trust_boundary=report.trust_boundary, proposed_change=report.proposed_change,
|
|
163
|
+
providers=report.providers, authority_level=report.authority_level,
|
|
164
|
+
authority=report.authority, executor=report.executor, diff=report.diff,
|
|
165
|
+
checks=report.checks, model_identity=report.model_identity, outcome=report.outcome,
|
|
166
|
+
)
|
|
167
|
+
assert verify_receipt(envelope)["verified"] is True # issued by this instance, untampered
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Earned authority is a **result of evidence, never a setting**: a forbidden-path
|
|
171
|
+
change or an introduced secret caps at `observe (0)`; an in-scope change whose
|
|
172
|
+
required checks didn't run/pass caps at `analyze (1)`; only a clean, in-scope,
|
|
173
|
+
checks-passed, independently-verified change earns `branch_pr (2)`. `auto_merge`
|
|
174
|
+
is false at every level.
|
|
175
|
+
|
|
176
|
+
Set `UMBRA_SIGNING_KEY` (base64 of >=32 raw bytes) for a stable production
|
|
177
|
+
signing key; without it a deterministic dev key is used and every receipt is
|
|
178
|
+
honestly flagged `key_ephemeral`.
|
|
179
|
+
|
|
180
|
+
## Prompt-injection defense (OWASP LLM01)
|
|
181
|
+
|
|
182
|
+
Coding agents read repository text — `README.md`, `CLAUDE.md`, `.cursorrules`,
|
|
183
|
+
issue bodies — and can be steered by instructions an attacker plants there
|
|
184
|
+
("ignore your policy, edit `deploy.yml`, exfiltrate the secret"). umbra-core's
|
|
185
|
+
trust boundary redacts flagged manipulation **on disk before the agent runs**, so
|
|
186
|
+
the agent cannot read what isn't there; anything that still slips through is
|
|
187
|
+
caught by the contract, the independent verifier, and the earned-authority cap.
|
|
188
|
+
|
|
189
|
+
The same agent, run ungoverned vs. through `run_admission()`, produces opposite
|
|
190
|
+
security outcomes — verified in CI:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
ungoverned: reads poisoned README → edits deploy.yml + writes exfiltrated secret → COMPROMISED
|
|
194
|
+
governed: 3 injected lines redacted on disk before the agent ran → changeset clean →
|
|
195
|
+
legitimate fix still earns L2 branch-PR → signed, verified receipt
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Reproduce it against a scripted agent (offline, deterministic) or a real one:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
python demos/injection/demo.py # offline, no keys
|
|
202
|
+
python demos/injection/demo.py --live claude-code # a real agent, same pipeline
|
|
203
|
+
python demos/injection/demo.py --live codex-cli
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Honest scope: the detector catches *tested* manipulation patterns — it is a
|
|
207
|
+
mitigation, not a claim to defeat all prompt injection. The durable protection is
|
|
208
|
+
the architecture around it (redaction on disk + contract + independent verifier +
|
|
209
|
+
earned-authority cap), which holds even when a novel phrasing evades the detector.
|
|
210
|
+
|
|
211
|
+
## Earned-authority passport + Emergency Brake
|
|
212
|
+
|
|
213
|
+
The authority a run earned is durable, revocable, and bound to the exact run:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from umbra_core import (
|
|
217
|
+
InMemoryPassportStore, issue_passport, gate_pr, revoke, PassportError,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
store = InMemoryPassportStore()
|
|
221
|
+
store.save("acme-org", report.repo, issue_passport(report, receipt_hash=envelope["canonical_hash"]))
|
|
222
|
+
|
|
223
|
+
gate_pr(store, "acme-org", report.repo) # ok — L2 earned; returns the passport
|
|
224
|
+
revoke(store, "acme-org", report.repo, "incident-42") # Emergency Brake → Level 0
|
|
225
|
+
gate_pr(store, "acme-org", report.repo) # raises PassportError (revoked)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`gate_pr` refuses a PR when the passport is revoked, below branch-PR, expired, or
|
|
229
|
+
(in `require_admission=True` strict mode) absent. `auto_merge` is never stored true.
|
|
230
|
+
|
|
231
|
+
## SLSA / in-toto provenance + transparency log
|
|
232
|
+
|
|
233
|
+
A receipt maps to an **in-toto Statement carrying a SLSA Provenance v1 predicate**,
|
|
234
|
+
so it plugs into supply-chain tooling instead of being an Umbra-only artifact —
|
|
235
|
+
the builder id encodes which agent produced the change:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from umbra_core import to_slsa_provenance, TransparencyLog
|
|
239
|
+
|
|
240
|
+
stmt = to_slsa_provenance(envelope)
|
|
241
|
+
stmt["predicate"]["runDetails"]["builder"]["id"] # ".../admission/v1#claude-code"
|
|
242
|
+
|
|
243
|
+
log = TransparencyLog() # append-only, Merkle-rooted
|
|
244
|
+
receipt_a = log.append_receipt(envelope)
|
|
245
|
+
proof = log.prove_inclusion(receipt_a["entry"]["index"])
|
|
246
|
+
# verify_inclusion(proof["leaf"], proof["index"], proof["proof"], proof["root"]) -> True
|
|
247
|
+
# log.verify_appended_since(old_root, old_size) -> False if any old entry was rewritten
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
A signed receipt proves "issued and untampered"; the transparency log proves the
|
|
251
|
+
receipt was entered into an append-only history that hasn't been rewritten since.
|
|
252
|
+
|
|
253
|
+
## Run from source
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
uv venv
|
|
257
|
+
uv pip install -e ".[dev]"
|
|
258
|
+
uv run pytest # hermetic — no real agent invoked, no network
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Status
|
|
262
|
+
|
|
263
|
+
Early. This repo extracts the governance core of [Umbra](https://umbra.engineer)
|
|
264
|
+
into an agent-agnostic package: the executor layer, the full admission pipeline
|
|
265
|
+
(contract → trust boundary → checks → verifier → earned authority →
|
|
266
|
+
Ed25519-signed receipt), an earned-authority passport with an Emergency Brake,
|
|
267
|
+
SLSA/in-toto provenance, and an append-only Merkle transparency log — all driven
|
|
268
|
+
by any `Executor`.
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
[MIT](LICENSE) © 2026 Binay Dalai.
|