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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Unit, schema, and security regression tests
run: python -m unittest discover -s tests -v
- name: Demo smoke test
run: kinegrant-demo > /tmp/kinegrant-demo.json && python -m json.tool /tmp/kinegrant-demo.json >/dev/null
run: kinegrant-demo > /tmp/kinegrant-demo.json && python -c 'import json; assert json.load(open("/tmp/kinegrant-demo.json"))["overall_result"] == "PASS"'
- name: Machine Permission Test
run: kinegrant-mpt --source-commit "$GITHUB_SHA" --output /tmp/kinegrant-mpt.json >/dev/null && python challenge/verify_evidence.py /tmp/kinegrant-mpt.json
- name: External reproduction packet
Expand Down
45 changes: 38 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,42 @@ jobs:
hashes: ${{ steps.hash.outputs.hashes }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"
- name: Verify tag matches package version
shell: bash
run: |
tag_version="${GITHUB_REF_NAME#v}"
package_version="$(python - <<'PY'
import tomllib
from pathlib import Path

print(tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"])
PY
)"
source_version="$(python - <<'PY'
import ast
from pathlib import Path

tree = ast.parse(Path("src/kinegrant/__init__.py").read_text())
for node in tree.body:
if isinstance(node, ast.Assign) and any(
isinstance(target, ast.Name) and target.id == "__version__"
for target in node.targets
):
print(ast.literal_eval(node.value))
break
else:
raise SystemExit("__version__ not found")
PY
)"
test "$tag_version" = "$package_version"
test "$tag_version" = "$source_version"
- name: Get source date epoch
id: epoch
run: echo "epoch=" >> "$GITHUB_OUTPUT"
run: echo "epoch=$(git log -1 --format=%ct)" >> "$GITHUB_OUTPUT"
- name: Build distributions
env:
SOURCE_DATE_EPOCH: ${{ steps.epoch.outputs.epoch }}
Expand All @@ -31,9 +60,11 @@ jobs:
shell: bash
run: |
cd dist
version="${GITHUB_REF_NAME#v}"
sha256sum *.whl *.tar.gz | sort -k2 > "SHA256SUMS-v${version}.txt"
echo "hashes=$(sha256sum * | base64 -w0)" >> "$GITHUB_OUTPUT"
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: dist
path: dist/
Expand All @@ -46,7 +77,7 @@ jobs:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true
Expand All @@ -59,12 +90,12 @@ jobs:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: dist
path: dist/
- name: Create release and upload assets
uses: softprops/action-gh-release@v3
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with:
files: dist/*
generate_release_notes: true
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ kinegrant-demo
docker run --rm ghcr.io/zoahdev/kinegrant-protocol
```

The command ends with `"overall_result": "PASS"` only when all four boundary
checks pass: the exact action is allowed, recording is denied, replay is denied,
and the signed receipt chain verifies against the caller's executor trust anchor.

> **KGP-001 实验性开放草案 0.1 · 稳定线格式 1.0**
>
> **参考实现 v2.65.5 · Apache-2.0**
Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ kinegrant-demo
docker run --rm ghcr.io/zoahdev/kinegrant-protocol
```

命令只有在四项边界检查全部通过时才会输出 `"overall_result": "PASS"`:精确动作被允许、录像被拒绝、重放被拒绝,并且签名收据链通过调用方提供的执行方信任锚验证。

> **KGP-001 实验性开放草案 0.1 · 稳定线格式 1.0**
>
> **参考实现 v2.65.5 · Apache-2.0**
Expand Down
9 changes: 5 additions & 4 deletions docs/RELEASE-REPRODUCIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
mtime (after fixing file times), content, and order across builds. The only
remaining byte difference is the top-level directory mtime, which
setuptools recreates at build time.
- **Authoritative hashes**: produced by the CI release workflow
(`SHA256SUMS-v2.65.5.txt` and the SLSA provenance attestation). Use these
for release verification.
- **Authoritative hashes**: the release workflow derives the build timestamp
from the tagged commit, rejects tags that disagree with both package version
declarations, and publishes `SHA256SUMS-v<version>.txt` alongside the SLSA
provenance attestation. Use these for release verification.

## Not byte-reproducible (known limit)

Expand All @@ -38,4 +39,4 @@ export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)
python -m build --sdist --wheel
sha256sum dist/*
# compare against SHA256SUMS-v2.65.5.txt
```
```
36 changes: 31 additions & 5 deletions src/kinegrant/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,44 @@ def run_demo() -> dict[str, object]:
gate = ActionGate(trusted_issuers={authority.kid})
claims = gate.authorize(capability, request)

replay_denied = False
try:
gate.authorize(capability, request)
except PermissionError:
replay_denied = True

recording_request = ActionRequest(
request_id="demo-request-002",
agent=request.agent,
target=request.target,
action="record",
purpose="training",
context=request.context,
)
recording_decision = engine.evaluate(recording_request)

executor = Ed25519KeyPair.generate()
log = ReceiptLog(executor)
receipt = log.append(claims, result="succeeded", evidence_hash="sha256:" + "00" * 32)
receipt_chain_valid = verify_receipt_chain(
log.entries,
trusted_executors={executor.kid},
expected_capability_ids={claims["capability_id"]},
)
checks = {
"exact_action_allowed": decision.allowed,
"recording_denied": not recording_decision.allowed,
"replay_denied": replay_denied,
"receipt_chain_valid": receipt_chain_valid,
}
return {
"overall_result": "PASS" if all(checks.values()) else "FAIL",
"checks": checks,
"decision": decision.to_dict(),
"recording_decision": recording_decision.to_dict(),
"capability": capability,
"receipt": receipt,
"receipt_chain_valid": verify_receipt_chain(
log.entries,
trusted_executors={executor.kid},
expected_capability_ids={claims["capability_id"]},
),
"receipt_chain_valid": receipt_chain_valid,
}


Expand Down
26 changes: 26 additions & 0 deletions tests/test_protocol.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from __future__ import annotations

import copy
import json
import unittest
from contextlib import redirect_stdout
from datetime import timedelta
from io import StringIO

from kinegrant.adapters import (
matter_command_request,
Expand All @@ -18,6 +21,29 @@
from kinegrant.models import ActionRequest, PolicyRule, utc_now
from kinegrant.policy import PolicyEngine
from kinegrant.receipt import ReceiptLog, verify_receipt_chain
from kinegrant.cli import main as demo_main, run_demo


class DemoTests(unittest.TestCase):
def test_demo_is_a_self_verifying_permission_boundary(self) -> None:
report = run_demo()
self.assertEqual(report["overall_result"], "PASS")
self.assertEqual(
report["checks"],
{
"exact_action_allowed": True,
"recording_denied": True,
"replay_denied": True,
"receipt_chain_valid": True,
},
)

def test_demo_cli_emits_machine_readable_json(self) -> None:
output = StringIO()
with redirect_stdout(output):
demo_main()
report = json.loads(output.getvalue())
self.assertEqual(report["overall_result"], "PASS")


class PolicyTests(unittest.TestCase):
Expand Down
Loading