Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
2026-09-02 00:52:04 +02:00
|
|
|
import json
|
|
|
|
|
import threading
|
|
|
|
|
from pathlib import Path
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
from wsgiref.simple_server import make_server
|
|
|
|
|
|
|
|
|
|
from .api import App
|
2026-09-02 00:52:04 +02:00
|
|
|
from .audit import AuditCoreSink, OutboxWorker
|
|
|
|
|
from .auth import Identity, JWTAuthenticator, StaticTokenAuthenticator
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
from .store import Engine
|
|
|
|
|
|
2026-09-02 00:52:04 +02:00
|
|
|
ALL_SCOPES = frozenset(
|
|
|
|
|
{
|
|
|
|
|
"approval:observe",
|
|
|
|
|
"approval:emit",
|
|
|
|
|
"approval:create",
|
|
|
|
|
"approval:read",
|
|
|
|
|
"approval:approve",
|
|
|
|
|
"approval:revoke",
|
|
|
|
|
"approval:supersede",
|
|
|
|
|
"approval:consume",
|
|
|
|
|
}
|
|
|
|
|
)
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
|
2026-09-02 00:52:04 +02:00
|
|
|
|
|
|
|
|
def _common_db(sub: argparse.ArgumentParser) -> None:
|
|
|
|
|
sub.add_argument("--db", default="approvals.sqlite")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parser() -> argparse.ArgumentParser:
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
parser = argparse.ArgumentParser(prog="approval-engine")
|
|
|
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
2026-09-02 00:52:04 +02:00
|
|
|
serve = sub.add_parser("serve", help="serve the authenticated HTTP API")
|
|
|
|
|
_common_db(serve)
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
serve.add_argument("--host", default="127.0.0.1")
|
|
|
|
|
serve.add_argument("--port", type=int, default=8787)
|
2026-09-02 00:52:04 +02:00
|
|
|
serve.add_argument("--production", action="store_true")
|
|
|
|
|
serve.add_argument("--tenant", default="platform")
|
|
|
|
|
serve.add_argument("--jwt-issuer")
|
|
|
|
|
serve.add_argument("--jwt-audience")
|
|
|
|
|
serve.add_argument("--jwks-url")
|
|
|
|
|
serve.add_argument("--dev-token-file")
|
|
|
|
|
serve.add_argument("--audit-url")
|
|
|
|
|
serve.add_argument("--audit-token-file")
|
|
|
|
|
serve.add_argument("--outbox-poll-seconds", type=float, default=5)
|
|
|
|
|
serve.add_argument("--heartbeat-seconds", type=int, default=86400)
|
|
|
|
|
migrate = sub.add_parser("migrate", help="apply repeatable schema migrations")
|
|
|
|
|
_common_db(migrate)
|
|
|
|
|
verify = sub.add_parser("verify", help="verify schema and SQLite integrity")
|
|
|
|
|
_common_db(verify)
|
|
|
|
|
backup = sub.add_parser("backup", help="create and verify an online backup")
|
|
|
|
|
_common_db(backup)
|
|
|
|
|
backup.add_argument("--output", required=True)
|
|
|
|
|
return parser
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _authenticator(args: argparse.Namespace, parser: argparse.ArgumentParser):
|
|
|
|
|
jwt_values = (args.jwt_issuer, args.jwt_audience, args.jwks_url)
|
|
|
|
|
if all(jwt_values):
|
|
|
|
|
return JWTAuthenticator(
|
|
|
|
|
issuer=args.jwt_issuer,
|
|
|
|
|
audience=args.jwt_audience,
|
|
|
|
|
jwks_url=args.jwks_url,
|
|
|
|
|
)
|
|
|
|
|
if any(jwt_values):
|
|
|
|
|
parser.error("--jwt-issuer, --jwt-audience, and --jwks-url are a set")
|
|
|
|
|
if args.production:
|
|
|
|
|
parser.error("production requires KeyCape JWT verifier configuration")
|
|
|
|
|
if not args.dev_token_file:
|
|
|
|
|
parser.error("configure JWT verification or explicit --dev-token-file")
|
|
|
|
|
token = Path(args.dev_token_file).read_text(encoding="utf-8").strip()
|
|
|
|
|
if not token:
|
|
|
|
|
parser.error("development token file is empty")
|
|
|
|
|
identity = Identity(
|
|
|
|
|
subject="development-agent",
|
|
|
|
|
issuer="local-development",
|
|
|
|
|
audiences=("approval-engine",),
|
|
|
|
|
principal_type="agent",
|
|
|
|
|
tenant="development",
|
|
|
|
|
roles=frozenset({"developer"}),
|
|
|
|
|
scopes=ALL_SCOPES,
|
|
|
|
|
assurance={"method": "explicit-development-token"},
|
|
|
|
|
evidence_ref="local-development-token",
|
|
|
|
|
)
|
|
|
|
|
return StaticTokenAuthenticator({token: identity})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _serve(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
|
|
|
|
|
if args.production and args.db == ":memory:":
|
|
|
|
|
parser.error("production requires a persistent database")
|
|
|
|
|
if bool(args.audit_url) != bool(args.audit_token_file):
|
|
|
|
|
parser.error("--audit-url and --audit-token-file are a set")
|
|
|
|
|
if args.production and not args.audit_url:
|
|
|
|
|
parser.error("production requires authenticated audit delivery")
|
|
|
|
|
authenticator = _authenticator(args, parser)
|
|
|
|
|
engine = Engine(args.db, auto_migrate=not args.production, tenant=args.tenant)
|
|
|
|
|
storage = engine.storage_status(integrity=True)
|
|
|
|
|
if args.production and (not storage["ok"] or not storage["persistent"]):
|
|
|
|
|
parser.error("production store is not ready; run migrate and verify")
|
|
|
|
|
app = App(engine, authenticator, require_persistent=args.production)
|
|
|
|
|
stop = threading.Event()
|
|
|
|
|
if args.audit_url:
|
|
|
|
|
sink = AuditCoreSink(args.audit_url, args.audit_token_file)
|
|
|
|
|
worker = OutboxWorker(
|
|
|
|
|
engine,
|
|
|
|
|
sink,
|
|
|
|
|
heartbeat_interval_seconds=args.heartbeat_seconds,
|
|
|
|
|
)
|
|
|
|
|
threading.Thread(
|
|
|
|
|
target=worker.run_forever,
|
|
|
|
|
args=(stop, args.outbox_poll_seconds),
|
|
|
|
|
daemon=True,
|
|
|
|
|
name="approval-outbox",
|
|
|
|
|
).start()
|
|
|
|
|
print(f"approval-engine listening on {args.host}:{args.port} db={args.db}")
|
|
|
|
|
try:
|
|
|
|
|
if args.production:
|
|
|
|
|
from waitress import serve
|
|
|
|
|
|
|
|
|
|
serve(app, host=args.host, port=args.port, threads=4)
|
|
|
|
|
else:
|
|
|
|
|
make_server(args.host, args.port, app).serve_forever()
|
|
|
|
|
finally:
|
|
|
|
|
stop.set()
|
|
|
|
|
engine.close()
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
|
|
|
parser = _parser()
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
args = parser.parse_args(argv)
|
2026-09-02 00:52:04 +02:00
|
|
|
if args.cmd == "migrate":
|
|
|
|
|
engine = Engine(args.db, auto_migrate=True)
|
|
|
|
|
print(json.dumps(engine.storage_status(integrity=True), sort_keys=True))
|
|
|
|
|
engine.close()
|
|
|
|
|
return 0
|
|
|
|
|
if args.cmd == "verify":
|
|
|
|
|
engine = Engine(args.db, auto_migrate=False)
|
|
|
|
|
status = engine.storage_status(integrity=True)
|
|
|
|
|
print(json.dumps(status, sort_keys=True))
|
|
|
|
|
engine.close()
|
|
|
|
|
return 0 if status["ok"] else 1
|
|
|
|
|
if args.cmd == "backup":
|
|
|
|
|
engine = Engine(args.db, auto_migrate=False)
|
|
|
|
|
print(json.dumps(engine.backup(args.output), sort_keys=True))
|
|
|
|
|
engine.close()
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
return 0
|
2026-09-02 00:52:04 +02:00
|
|
|
if args.cmd == "serve":
|
|
|
|
|
return _serve(args, parser)
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
return 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|