NK-WP-0056-T13 (part): executor CLI, clients, Containerfile, image workflow
All checks were successful
All checks were successful
CLI entry point and approval-engine/audit-core clients; events carry occurred_at (audit-core envelope); hash-locked image with pinned lldap stage builds locally; image workflow and executor tests in CI; pilot example pins the executed files. rapp reshaping still open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 994874@bnt-lap001 Assistant-Session: 6017c416-d619-47eb-bd1a-f670a02d7d87
This commit is contained in:
parent
fe18096aa3
commit
ba9d266b7e
18 changed files with 694 additions and 33 deletions
58
.forgejo/workflows/bounded-executor-image.yaml
Normal file
58
.forgejo/workflows/bounded-executor-image.yaml
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
name: Build and Publish bounded-executor
|
||||||
|
|
||||||
|
# Fleet image-publish pattern: build from a tarball of the pushed commit,
|
||||||
|
# never from a workstation tree. Executor tests run in identity-journeys.yaml
|
||||||
|
# on the same paths; do not pin a digest in rapp-bounded-executor if that job
|
||||||
|
# failed (NK-WP-0056-T13). Build context is the repository root.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "bounded-executor/**"
|
||||||
|
- "tools/bounded-execution-admission/validate.py"
|
||||||
|
- "canon/schemas/bounded-execution-admission_v0.1.schema.json"
|
||||||
|
- "sso-mfa/k8s/synthetic-journey/journey.py"
|
||||||
|
- ".forgejo/workflows/bounded-executor-image.yaml"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: forgejo.coulomb.social
|
||||||
|
IMAGE_NAME: coulomb/bounded-executor
|
||||||
|
DOCKER_HOST: tcp://127.0.0.1:2375
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: container-build
|
||||||
|
steps:
|
||||||
|
- name: Build and push image
|
||||||
|
env:
|
||||||
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
REF="${GITHUB_SHA:-main}"
|
||||||
|
SHORT="${REF:0:7}"
|
||||||
|
mkdir -p buildctx "${HOME}/bin"
|
||||||
|
wget -qO /tmp/repo.tar.gz \
|
||||||
|
"https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz"
|
||||||
|
tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1
|
||||||
|
wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \
|
||||||
|
| tar xz --strip-components=1 -C "${HOME}/bin" docker/docker
|
||||||
|
export PATH="${HOME}/bin:${PATH}"
|
||||||
|
echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${REGISTRY_USER}" --password-stdin
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
docker build -f buildctx/bounded-executor/Containerfile \
|
||||||
|
-t "${IMAGE}:latest" -t "${IMAGE}:main-${SHORT}" \
|
||||||
|
buildctx
|
||||||
|
docker push "${IMAGE}:latest"
|
||||||
|
docker push "${IMAGE}:main-${SHORT}"
|
||||||
|
echo "pushed ${IMAGE}:latest and ${IMAGE}:main-${SHORT}"
|
||||||
|
|
||||||
|
- name: Report immutable digest
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
export PATH="${HOME}/bin:${PATH}"
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
SHORT="${GITHUB_SHA:0:7}"
|
||||||
|
docker inspect --format='{{index .RepoDigests 0}}' "${IMAGE}:main-${SHORT}"
|
||||||
|
|
@ -2,7 +2,7 @@ name: Identity provider journey acceptance
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths: ["identity-provisioner/**", "sso-mfa/k8s/synthetic-journey/**", ".forgejo/workflows/identity-journeys.yaml"]
|
paths: ["identity-provisioner/**", "sso-mfa/k8s/synthetic-journey/**", "bounded-executor/**", "tools/bounded-execution-admission/**", "examples/bounded-execution-admission/**", ".forgejo/workflows/identity-journeys.yaml"]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
provider:
|
provider:
|
||||||
|
|
@ -23,3 +23,9 @@ jobs:
|
||||||
set -eu
|
set -eu
|
||||||
cd identity-source/sso-mfa/k8s/synthetic-journey
|
cd identity-source/sso-mfa/k8s/synthetic-journey
|
||||||
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest test_journey test_lane -v
|
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest test_journey test_lane -v
|
||||||
|
- name: Run bounded executor tests (NK-WP-0056-T11..T13)
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
cd identity-source
|
||||||
|
python3 -m pip install --no-cache-dir --require-hashes -r bounded-executor/requirements.txt
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s bounded-executor/tests -v
|
||||||
|
|
|
||||||
|
|
@ -355,7 +355,7 @@
|
||||||
| task | NK-WP-0056-T10 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
| task | NK-WP-0056-T10 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||||
| task | NK-WP-0056-T11 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
| task | NK-WP-0056-T11 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||||
| task | NK-WP-0056-T12 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
| task | NK-WP-0056-T12 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||||
| task | NK-WP-0056-T13 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
| task | NK-WP-0056-T13 | progress | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||||
| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
||||||
| intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
| intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |
|
||||||
| intake | NET-IN-0001 | open | — | intakes/intakes.md |
|
| intake | NET-IN-0001 | open | — | intakes/intakes.md |
|
||||||
|
|
|
||||||
18
bounded-executor/Containerfile
Normal file
18
bounded-executor/Containerfile
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Bounded executor image (NK-WP-0056-T13). Build context: the repository root.
|
||||||
|
# docker build -f bounded-executor/Containerfile .
|
||||||
|
# lldap_set_password comes from the same pinned LLDAP image production runs, so OPAQUE matches.
|
||||||
|
FROM docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d AS lldap
|
||||||
|
FROM python:3.12-slim@sha256:d764629ce0ddd8c71fd371e9901efb324a95789d2315a47db7e4d27e78f1b0e9
|
||||||
|
RUN useradd --create-home --uid 10001 executor
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=lldap /app/lldap_set_password /app/lldap_set_password
|
||||||
|
COPY bounded-executor/requirements.txt /app/bounded-executor/requirements.txt
|
||||||
|
RUN python -m pip install --no-cache-dir --require-hashes -r /app/bounded-executor/requirements.txt
|
||||||
|
# The executed files keep their repository paths: admissions pin them by path and sha256.
|
||||||
|
COPY canon/schemas/bounded-execution-admission_v0.1.schema.json /app/canon/schemas/
|
||||||
|
COPY tools/bounded-execution-admission/validate.py /app/tools/bounded-execution-admission/
|
||||||
|
COPY sso-mfa/k8s/synthetic-journey/journey.py /app/sso-mfa/k8s/synthetic-journey/
|
||||||
|
COPY bounded-executor/executor.py bounded-executor/operations.py bounded-executor/clients.py bounded-executor/cli.py /app/bounded-executor/
|
||||||
|
USER 10001:10001
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENTRYPOINT ["python", "/app/bounded-executor/cli.py"]
|
||||||
106
bounded-executor/cli.py
Normal file
106
bounded-executor/cli.py
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run one admission (NK-WP-0056-T13): the entry point of the rapp-bounded-executor Job.
|
||||||
|
|
||||||
|
python3 bounded-executor/cli.py --admission FILE --approval-id ID --config FILE [--result FILE]
|
||||||
|
|
||||||
|
The config (JSON, non-secret) names endpoints, token files and the pilot's targets:
|
||||||
|
|
||||||
|
executor_identity, runtime_principal, tenant, environment, approver_subjects{name: subject_id},
|
||||||
|
trust_satisfied[...] trust states the operator attests for this run (recorded)
|
||||||
|
approval_engine{url, token_file, ca_file?}
|
||||||
|
audit_core{url, token_file, ca_file?}
|
||||||
|
openbao{addr, k8s_role, jwt_file, ca_file?}
|
||||||
|
lldap{url, ca_file?, set_password: "/app/lldap_set_password"}
|
||||||
|
kubernetes{namespace, external_secret, deployment}
|
||||||
|
bind{kv, user}, journey{kv, config{...}}
|
||||||
|
|
||||||
|
Exit codes: 0 completed, 3 reverted, 4 refused before any effect, 5 stopped / unknown / revert failed / budget,
|
||||||
|
6 consumed but not in custody. The result printed is value-free.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HERE = pathlib.Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def _load(name):
|
||||||
|
spec = importlib.util.spec_from_file_location(name, HERE / f"{name}.py")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
X = _load("executor")
|
||||||
|
OPS = _load("operations")
|
||||||
|
C = _load("clients")
|
||||||
|
EXIT = {"completed": 0, "reverted": 3, "refused": 4}
|
||||||
|
|
||||||
|
|
||||||
|
def build(cfg: dict):
|
||||||
|
bao = OPS.OpenBao(cfg["openbao"]["addr"], k8s_role=cfg["openbao"]["k8s_role"],
|
||||||
|
jwt_file=cfg["openbao"]["jwt_file"], ca_file=cfg["openbao"].get("ca_file"))
|
||||||
|
directory = OPS.Directory(cfg["lldap"]["url"], [cfg["lldap"].get("set_password", "/app/lldap_set_password")],
|
||||||
|
ca_file=cfg["lldap"].get("ca_file"))
|
||||||
|
kube = OPS.Kube()
|
||||||
|
k = cfg["kubernetes"]
|
||||||
|
operations, checks = OPS.pilot_registry(
|
||||||
|
bao=bao, directory=directory, delivery=OPS.KubeDelivery(kube, k["namespace"], k["external_secret"]),
|
||||||
|
restart=OPS.KubeRestart(kube, k["namespace"], k["deployment"]), bind_kv=cfg["bind"]["kv"],
|
||||||
|
bind_user=cfg["bind"]["user"], journey_kv=cfg["journey"]["kv"], journey_config=cfg["journey"]["config"])
|
||||||
|
trusted = set(cfg.get("trust_satisfied", []))
|
||||||
|
|
||||||
|
def alert(kind: str, info: dict) -> None:
|
||||||
|
print(json.dumps({"alert": kind, **info}), file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
deps = X.Deps(carrier=C.ApprovalEngine(cfg["approval_engine"]["url"], cfg["approval_engine"]["token_file"],
|
||||||
|
cfg["approval_engine"].get("ca_file")),
|
||||||
|
operations=operations, checks=checks, trust=lambda state: state in trusted,
|
||||||
|
audit=C.AuditCore(cfg["audit_core"]["url"], cfg["audit_core"]["token_file"],
|
||||||
|
cfg["audit_core"].get("ca_file")),
|
||||||
|
alert=alert)
|
||||||
|
config = X.Config(executor_identity=cfg["executor_identity"], runtime_principal=cfg["runtime_principal"],
|
||||||
|
tenant=cfg["tenant"], environment=cfg["environment"],
|
||||||
|
approver_subjects=dict(cfg["approver_subjects"]))
|
||||||
|
return config, deps, bao
|
||||||
|
|
||||||
|
|
||||||
|
def exit_code(result: dict) -> int:
|
||||||
|
if result.get("reason") in ("receipt_not_in_custody", "audit_unavailable") and result["outcome"] != "refused":
|
||||||
|
return 6
|
||||||
|
return EXIT.get(result["outcome"], 5)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
||||||
|
parser.add_argument("--admission", required=True)
|
||||||
|
parser.add_argument("--approval-id", required=True)
|
||||||
|
parser.add_argument("--config", required=True)
|
||||||
|
parser.add_argument("--result", help="also write the result here")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
cfg = json.loads(pathlib.Path(args.config).read_text())
|
||||||
|
admission = X.bea.load(pathlib.Path(args.admission))
|
||||||
|
bao = None
|
||||||
|
try:
|
||||||
|
config, deps, bao = build(cfg)
|
||||||
|
result = X.Run(admission, args.approval_id, config, deps).execute()
|
||||||
|
except Exception as error: # setup failed before preflight: nothing was consumed
|
||||||
|
result = {"outcome": "refused", "reason": "setup:" + type(error).__name__, "run_id": None, "effects": False}
|
||||||
|
finally:
|
||||||
|
if bao is not None:
|
||||||
|
bao.revoke_self()
|
||||||
|
text = json.dumps(result, indent=2, sort_keys=True)
|
||||||
|
if args.result:
|
||||||
|
pathlib.Path(args.result).write_text(text + "\n")
|
||||||
|
print(text)
|
||||||
|
return exit_code(result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
79
bounded-executor/clients.py
Normal file
79
bounded-executor/clients.py
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
"""Clients for the executor's owners (NK-WP-0056-T13): approval-engine (carrier) and audit-core (sink).
|
||||||
|
|
||||||
|
Stdlib HTTP only. Bearer tokens are read from files at call time (projected or ESO-delivered), never logged.
|
||||||
|
Errors carry status codes only, never response bodies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import ssl
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
class ClientError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _Http:
|
||||||
|
def __init__(self, base: str, token_file: str, ca_file: str | None = None, timeout: float = 15):
|
||||||
|
parsed = urllib.parse.urlsplit(base)
|
||||||
|
if parsed.scheme not in ("https", "http") or not parsed.netloc or parsed.query or parsed.fragment:
|
||||||
|
raise ClientError("base url")
|
||||||
|
self.base = base.rstrip("/")
|
||||||
|
self.token_file = token_file
|
||||||
|
context = ssl.create_default_context(cafile=ca_file) if ca_file else ssl.create_default_context()
|
||||||
|
self.open = urllib.request.build_opener(urllib.request.ProxyHandler({}),
|
||||||
|
urllib.request.HTTPSHandler(context=context)).open
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def call(self, method: str, path: str, body: dict | None = None, headers: dict | None = None) -> tuple[int, dict]:
|
||||||
|
head = {"Authorization": "Bearer " + pathlib.Path(self.token_file).read_text().strip(),
|
||||||
|
"Accept": "application/json", **(headers or {})}
|
||||||
|
data = None
|
||||||
|
if body is not None:
|
||||||
|
head["Content-Type"] = "application/json"
|
||||||
|
data = json.dumps(body).encode()
|
||||||
|
request = urllib.request.Request(self.base + path, method=method, headers=head, data=data)
|
||||||
|
try:
|
||||||
|
with self.open(request, timeout=self.timeout) as response:
|
||||||
|
raw = response.read(1 << 20)
|
||||||
|
return response.status, (json.loads(raw) if raw else {})
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
error.close()
|
||||||
|
raise ClientError(f"{method} {error.code}") from None
|
||||||
|
|
||||||
|
|
||||||
|
class ApprovalEngine:
|
||||||
|
"""GET /v1/approvals/{id} and POST /v1/approvals/{id}/consume (approval-engine api.py)."""
|
||||||
|
|
||||||
|
def __init__(self, base: str, token_file: str, ca_file: str | None = None):
|
||||||
|
self.http = _Http(base, token_file, ca_file)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _path(approval_id: str) -> str:
|
||||||
|
if not approval_id or "/" in approval_id or len(approval_id) > 128:
|
||||||
|
raise ClientError("approval id")
|
||||||
|
return "/v1/approvals/" + urllib.parse.quote(approval_id, safe="")
|
||||||
|
|
||||||
|
def get(self, approval_id: str) -> dict:
|
||||||
|
return self.http.call("GET", self._path(approval_id))[1]
|
||||||
|
|
||||||
|
def consume(self, approval_id: str, request_digest: str, decision_id: str) -> dict:
|
||||||
|
return self.http.call("POST", self._path(approval_id) + "/consume",
|
||||||
|
{"request_digest": request_digest, "decision_id": decision_id})[1]
|
||||||
|
|
||||||
|
|
||||||
|
class AuditCore:
|
||||||
|
"""POST /v1/events with Idempotency-Key = event id (audit-core docs/event-envelope.md)."""
|
||||||
|
|
||||||
|
def __init__(self, base: str, token_file: str, ca_file: str | None = None):
|
||||||
|
self.http = _Http(base, token_file, ca_file)
|
||||||
|
|
||||||
|
def emit(self, event: dict) -> None:
|
||||||
|
status, _ = self.http.call("POST", "/v1/events", event, {"Idempotency-Key": event["id"]})
|
||||||
|
if status not in (200, 201):
|
||||||
|
raise ClientError(f"audit {status}")
|
||||||
|
|
@ -295,14 +295,16 @@ class Run:
|
||||||
return "completed"
|
return "completed"
|
||||||
|
|
||||||
# ------------------------------------------------------------------ evidence
|
# ------------------------------------------------------------------ evidence
|
||||||
def _event(self, kind: str, body: dict) -> dict:
|
def _event(self, kind: str, body: dict, occurred_at: dt.datetime) -> dict:
|
||||||
|
"""audit-core's ingest envelope (docs/event-envelope.md): all eight fields, offset-qualified time."""
|
||||||
return {"id": f"run:{self.run_id}:{'started' if kind == STARTED else 'receipt'}", "type": kind,
|
return {"id": f"run:{self.run_id}:{'started' if kind == STARTED else 'receipt'}", "type": kind,
|
||||||
"correlation_id": self.run_id, "subject": f"admission:{self.spec['id']}",
|
"correlation_id": self.run_id, "subject": f"admission:{self.spec['id']}",
|
||||||
"source": "net-kingdom.bounded-executor", "tenant": self.cfg.tenant, "data": body}
|
"source": "net-kingdom.bounded-executor", "tenant": self.cfg.tenant,
|
||||||
|
"occurred_at": occurred_at.astimezone(dt.UTC).isoformat(timespec="seconds"), "data": body}
|
||||||
|
|
||||||
def _receipt(self, outcome: str, attempts: int) -> dict:
|
def _receipt(self, outcome: str, attempts: int) -> dict:
|
||||||
end = self.d.wall()
|
end = self.d.wall()
|
||||||
return self._event(RECEIPT, {
|
return self._event(RECEIPT, occurred_at=end, body={
|
||||||
"run_id": self.run_id,
|
"run_id": self.run_id,
|
||||||
"initiator": {"sub": self.approver_subject, "role": "approver"},
|
"initiator": {"sub": self.approver_subject, "role": "approver"},
|
||||||
"executing_workload": self.cfg.executor_identity,
|
"executing_workload": self.cfg.executor_identity,
|
||||||
|
|
@ -343,7 +345,8 @@ class Run:
|
||||||
try:
|
try:
|
||||||
self.d.audit.emit(self._event(STARTED, {"run_id": self.run_id, "admission": self.spec["id"],
|
self.d.audit.emit(self._event(STARTED, {"run_id": self.run_id, "admission": self.spec["id"],
|
||||||
"approval": self.approval_id, "consumed_at": self.consumed_at,
|
"approval": self.approval_id, "consumed_at": self.consumed_at,
|
||||||
"executing_workload": self.cfg.executor_identity}))
|
"executing_workload": self.cfg.executor_identity},
|
||||||
|
occurred_at=self.d.wall()))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.d.alert("started_not_in_custody", {"admission": self.spec["id"], "run_id": self.run_id})
|
self.d.alert("started_not_in_custody", {"admission": self.spec["id"], "run_id": self.run_id})
|
||||||
return {"outcome": "stopped", "reason": "audit_unavailable", "run_id": self.run_id, "effects": "consume-only"}
|
return {"outcome": "stopped", "reason": "audit_unavailable", "run_id": self.run_id, "effects": "consume-only"}
|
||||||
|
|
|
||||||
2
bounded-executor/requirements.in
Normal file
2
bounded-executor/requirements.in
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
jsonschema>=4.23,<5
|
||||||
|
PyYAML>=6,<7
|
||||||
233
bounded-executor/requirements.txt
Normal file
233
bounded-executor/requirements.txt
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile requirements.in --generate-hashes --python-version 3.12 --python-platform x86_64-unknown-linux-gnu -o requirements.txt
|
||||||
|
attrs==26.1.0 \
|
||||||
|
--hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
|
||||||
|
--hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
|
||||||
|
# via
|
||||||
|
# jsonschema
|
||||||
|
# referencing
|
||||||
|
jsonschema==4.26.0 \
|
||||||
|
--hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \
|
||||||
|
--hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce
|
||||||
|
# via -r requirements.in
|
||||||
|
jsonschema-specifications==2025.9.1 \
|
||||||
|
--hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \
|
||||||
|
--hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d
|
||||||
|
# via jsonschema
|
||||||
|
pyyaml==6.0.3 \
|
||||||
|
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||||
|
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
||||||
|
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||||
|
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
|
||||||
|
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
||||||
|
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
|
||||||
|
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
||||||
|
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
|
||||||
|
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
|
||||||
|
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
|
||||||
|
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
||||||
|
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
|
||||||
|
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
|
||||||
|
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
|
||||||
|
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
|
||||||
|
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
||||||
|
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
|
||||||
|
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
|
||||||
|
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
|
||||||
|
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
||||||
|
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
|
||||||
|
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
|
||||||
|
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
||||||
|
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
||||||
|
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
||||||
|
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
||||||
|
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
||||||
|
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
|
||||||
|
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
|
||||||
|
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
|
||||||
|
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
||||||
|
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
||||||
|
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
|
||||||
|
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
||||||
|
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
|
||||||
|
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
|
||||||
|
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
||||||
|
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
||||||
|
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
||||||
|
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
||||||
|
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
||||||
|
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
|
||||||
|
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
|
||||||
|
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
||||||
|
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
||||||
|
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
||||||
|
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
||||||
|
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
||||||
|
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
||||||
|
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
|
||||||
|
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
|
||||||
|
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
|
||||||
|
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
||||||
|
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
||||||
|
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
|
||||||
|
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
||||||
|
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
|
||||||
|
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
|
||||||
|
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
||||||
|
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
|
||||||
|
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
||||||
|
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
|
||||||
|
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
|
||||||
|
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
||||||
|
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
||||||
|
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||||
|
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
||||||
|
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
||||||
|
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
||||||
|
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
|
||||||
|
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||||
|
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||||
|
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||||
|
# via -r requirements.in
|
||||||
|
referencing==0.37.0 \
|
||||||
|
--hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \
|
||||||
|
--hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8
|
||||||
|
# via
|
||||||
|
# jsonschema
|
||||||
|
# jsonschema-specifications
|
||||||
|
rpds-py==2026.9.1 \
|
||||||
|
--hash=sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25 \
|
||||||
|
--hash=sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68 \
|
||||||
|
--hash=sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495 \
|
||||||
|
--hash=sha256:0483515261947e4e8b8e1375bf7463e7eb6ccfb3d86e7b554d90cd5285f20f32 \
|
||||||
|
--hash=sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96 \
|
||||||
|
--hash=sha256:074a4d198bc34d9a8ea425114fc3ded6d11ec01f6a314a8db67454a5152d8834 \
|
||||||
|
--hash=sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0 \
|
||||||
|
--hash=sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6 \
|
||||||
|
--hash=sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e \
|
||||||
|
--hash=sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa \
|
||||||
|
--hash=sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759 \
|
||||||
|
--hash=sha256:136a1c3fe4402b7008bc81cb62ee538481795b61a7e83df88dff3b3f02b726ff \
|
||||||
|
--hash=sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7 \
|
||||||
|
--hash=sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c \
|
||||||
|
--hash=sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903 \
|
||||||
|
--hash=sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d \
|
||||||
|
--hash=sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9 \
|
||||||
|
--hash=sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e \
|
||||||
|
--hash=sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b \
|
||||||
|
--hash=sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a \
|
||||||
|
--hash=sha256:270bdcdaac5d5b6f73c5e22e7e135c7f2a50e789f71d9e241d5be8d90026e19a \
|
||||||
|
--hash=sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2 \
|
||||||
|
--hash=sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945 \
|
||||||
|
--hash=sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91 \
|
||||||
|
--hash=sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044 \
|
||||||
|
--hash=sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69 \
|
||||||
|
--hash=sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e \
|
||||||
|
--hash=sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3 \
|
||||||
|
--hash=sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4 \
|
||||||
|
--hash=sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33 \
|
||||||
|
--hash=sha256:3e524c7874ac72884d28e16dd5b8d839fd09e0fe76b020d3fbca23212a7b8c52 \
|
||||||
|
--hash=sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9 \
|
||||||
|
--hash=sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb \
|
||||||
|
--hash=sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35 \
|
||||||
|
--hash=sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299 \
|
||||||
|
--hash=sha256:44b32a7c4f0da3d28af31c259e38ddcff096f855e205ed0671d02fcf44f1ea1c \
|
||||||
|
--hash=sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063 \
|
||||||
|
--hash=sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a \
|
||||||
|
--hash=sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6 \
|
||||||
|
--hash=sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12 \
|
||||||
|
--hash=sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14 \
|
||||||
|
--hash=sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0 \
|
||||||
|
--hash=sha256:4c0d2cb595a420b34d5086db0add011e26e2c09d6a024afbac4228bf8f863a30 \
|
||||||
|
--hash=sha256:4cfaf02209061880210819934de2f4f6aa83dc04dafe6770276acc240a56da31 \
|
||||||
|
--hash=sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02 \
|
||||||
|
--hash=sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af \
|
||||||
|
--hash=sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef \
|
||||||
|
--hash=sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0 \
|
||||||
|
--hash=sha256:56cd8b3f77d7b6812f533b662186a1f28316931166ddc00fb893b1b0db7e9888 \
|
||||||
|
--hash=sha256:57492a550a1d88d29d003247e5f78dd8cf04a701fac0e4c8db8745a6d2504e0a \
|
||||||
|
--hash=sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c \
|
||||||
|
--hash=sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b \
|
||||||
|
--hash=sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86 \
|
||||||
|
--hash=sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f \
|
||||||
|
--hash=sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531 \
|
||||||
|
--hash=sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6 \
|
||||||
|
--hash=sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82 \
|
||||||
|
--hash=sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d \
|
||||||
|
--hash=sha256:6b723eb406dec5bc9ec516c73ab9c3239a3284e017f7eb89ee2b3258bd504fb7 \
|
||||||
|
--hash=sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448 \
|
||||||
|
--hash=sha256:6beb738155fe8ab8091afdfa5a3226b21c2b1593f1e50ebb90eb25b44dbc0391 \
|
||||||
|
--hash=sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb \
|
||||||
|
--hash=sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4 \
|
||||||
|
--hash=sha256:6eae33003518fd4cb4f83a218d5371469dd3001aa3b87128c005b07762f7fe5e \
|
||||||
|
--hash=sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c \
|
||||||
|
--hash=sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15 \
|
||||||
|
--hash=sha256:761fdae6728ceb99ab182fad2f0cc1e262f610834dc891aea1d1a2a2e634776f \
|
||||||
|
--hash=sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297 \
|
||||||
|
--hash=sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36 \
|
||||||
|
--hash=sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d \
|
||||||
|
--hash=sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b \
|
||||||
|
--hash=sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237 \
|
||||||
|
--hash=sha256:8171b44a054e5c67fd748ada04187f1250bf35b95f85e52ab64bcf3331a923bb \
|
||||||
|
--hash=sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477 \
|
||||||
|
--hash=sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c \
|
||||||
|
--hash=sha256:839dde845559254f34885267c6878f60d61d5205180226d976fe488d45fa128e \
|
||||||
|
--hash=sha256:84a6ecc0c940169190d2c23bd969debd48c94dbc855acd60188a68d71d421608 \
|
||||||
|
--hash=sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07 \
|
||||||
|
--hash=sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c \
|
||||||
|
--hash=sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35 \
|
||||||
|
--hash=sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0 \
|
||||||
|
--hash=sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785 \
|
||||||
|
--hash=sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad \
|
||||||
|
--hash=sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc \
|
||||||
|
--hash=sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9 \
|
||||||
|
--hash=sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025 \
|
||||||
|
--hash=sha256:a3dbc5ed9514908d5046107d7b1346bde71eea61de6e0e4919c19354f97e769f \
|
||||||
|
--hash=sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009 \
|
||||||
|
--hash=sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d \
|
||||||
|
--hash=sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118 \
|
||||||
|
--hash=sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db \
|
||||||
|
--hash=sha256:ab4b2fda7c2b542f7f9d886cc6a838c5079d2b76f72e6081411faba11adde2c9 \
|
||||||
|
--hash=sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab \
|
||||||
|
--hash=sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37 \
|
||||||
|
--hash=sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d \
|
||||||
|
--hash=sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc \
|
||||||
|
--hash=sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9 \
|
||||||
|
--hash=sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3 \
|
||||||
|
--hash=sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569 \
|
||||||
|
--hash=sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69 \
|
||||||
|
--hash=sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b \
|
||||||
|
--hash=sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018 \
|
||||||
|
--hash=sha256:ce4d4f52e2a4324396caddbd45a97d8d7be5f42edd25d2355282a9c34f9b2f7f \
|
||||||
|
--hash=sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c \
|
||||||
|
--hash=sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f \
|
||||||
|
--hash=sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5 \
|
||||||
|
--hash=sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445 \
|
||||||
|
--hash=sha256:d95a354e02393eada6d7351184671aced9d4cce109dabf927cb7aa99624352a1 \
|
||||||
|
--hash=sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e \
|
||||||
|
--hash=sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429 \
|
||||||
|
--hash=sha256:e01b3c878c8641913e688edd1b3f08658c6783d29cf6b826bd3c0d1ae7a1ffaa \
|
||||||
|
--hash=sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b \
|
||||||
|
--hash=sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250 \
|
||||||
|
--hash=sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e \
|
||||||
|
--hash=sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83 \
|
||||||
|
--hash=sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f \
|
||||||
|
--hash=sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd \
|
||||||
|
--hash=sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346 \
|
||||||
|
--hash=sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858 \
|
||||||
|
--hash=sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871 \
|
||||||
|
--hash=sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da \
|
||||||
|
--hash=sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0 \
|
||||||
|
--hash=sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f \
|
||||||
|
--hash=sha256:f3d6ed6a98cfd19155996605474982cc470d7601746a6439078f1a5a3fa8b050 \
|
||||||
|
--hash=sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31 \
|
||||||
|
--hash=sha256:fda1d96e542c37b6c804547dbf489c129fe7c97183a76a5ec275909ba1a063df \
|
||||||
|
--hash=sha256:fdcd198979b4ecffcc1beba366a7fbcf4eb41243691a82fe52ceb0b902f09c12 \
|
||||||
|
--hash=sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567
|
||||||
|
# via
|
||||||
|
# jsonschema
|
||||||
|
# referencing
|
||||||
|
typing-extensions==4.16.0 \
|
||||||
|
--hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
|
||||||
|
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
|
||||||
|
# via referencing
|
||||||
130
bounded-executor/tests/test_clients.py
Normal file
130
bounded-executor/tests/test_clients.py
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
"""approval-engine and audit-core clients, and the CLI's setup failure path (NK-WP-0056-T13). Fakes only."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import unittest
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
|
HERE = pathlib.Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def load(name):
|
||||||
|
spec = importlib.util.spec_from_file_location(name, HERE.parent / f"{name}.py")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
C = load("clients")
|
||||||
|
CLI = load("cli")
|
||||||
|
|
||||||
|
|
||||||
|
class Fake:
|
||||||
|
def __init__(self):
|
||||||
|
self.requests = []
|
||||||
|
self.status = 200
|
||||||
|
|
||||||
|
|
||||||
|
def serve(fake):
|
||||||
|
class H(BaseHTTPRequestHandler):
|
||||||
|
def handle_one(self, method):
|
||||||
|
raw = self.rfile.read(int(self.headers.get("Content-Length", "0") or 0))
|
||||||
|
self.server_fake.requests.append((method, self.path, dict(self.headers), json.loads(raw) if raw else None))
|
||||||
|
body = json.dumps({"id": "apr-1", "status": "approved"} if method == "GET" else
|
||||||
|
{"status": "consumed", "idempotent": False}).encode()
|
||||||
|
self.send_response(self.server_fake.status)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
self.handle_one("GET")
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
self.handle_one("POST")
|
||||||
|
|
||||||
|
def log_message(self, *a):
|
||||||
|
return
|
||||||
|
|
||||||
|
H.server_fake = fake
|
||||||
|
server = ThreadingHTTPServer(("127.0.0.1", 0), H)
|
||||||
|
threading.Thread(target=server.serve_forever, kwargs={"poll_interval": 0.02}, daemon=True).start()
|
||||||
|
return server
|
||||||
|
|
||||||
|
|
||||||
|
class ClientTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.fake = Fake()
|
||||||
|
self.server = serve(self.fake)
|
||||||
|
self.addCleanup(self.server.shutdown)
|
||||||
|
tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(tmp.cleanup)
|
||||||
|
self.token = pathlib.Path(tmp.name) / "token"
|
||||||
|
self.token.write_text("synthetic-bearer\n")
|
||||||
|
self.base = f"http://127.0.0.1:{self.server.server_port}"
|
||||||
|
|
||||||
|
def test_approval_engine_get_and_consume(self):
|
||||||
|
ae = C.ApprovalEngine(self.base, str(self.token))
|
||||||
|
self.assertEqual(ae.get("apr-1")["status"], "approved")
|
||||||
|
ae.consume("apr-1", "sha256:" + "a" * 64, "run-1")
|
||||||
|
(_, p1, h1, _), (_, p2, _, b2) = self.fake.requests
|
||||||
|
self.assertEqual((p1, p2), ("/v1/approvals/apr-1", "/v1/approvals/apr-1/consume"))
|
||||||
|
self.assertEqual(h1["Authorization"], "Bearer synthetic-bearer")
|
||||||
|
self.assertEqual(b2, {"request_digest": "sha256:" + "a" * 64, "decision_id": "run-1"})
|
||||||
|
|
||||||
|
def test_approval_id_cannot_escape_the_path(self):
|
||||||
|
ae = C.ApprovalEngine(self.base, str(self.token))
|
||||||
|
for bad in ("../admin", "", "a/b"):
|
||||||
|
with self.subTest(bad=bad), self.assertRaises(C.ClientError):
|
||||||
|
ae.get(bad)
|
||||||
|
self.assertEqual(self.fake.requests, [])
|
||||||
|
|
||||||
|
def test_audit_core_idempotency_key_equals_event_id(self):
|
||||||
|
event = {"id": "run:r1:started", "type": "execution.run.started", "source": "s", "subject": "x",
|
||||||
|
"tenant": "tenant:platform", "correlation_id": "r1", "occurred_at": "2026-11-03T09:30:00+00:00",
|
||||||
|
"data": {"a": 1}}
|
||||||
|
C.AuditCore(self.base, str(self.token)).emit(event)
|
||||||
|
((method, path, headers, body),) = self.fake.requests
|
||||||
|
self.assertEqual((method, path, headers["Idempotency-Key"]), ("POST", "/v1/events", "run:r1:started"))
|
||||||
|
self.assertEqual(body, event)
|
||||||
|
|
||||||
|
def test_errors_carry_status_only(self):
|
||||||
|
self.fake.status = 403
|
||||||
|
with self.assertRaises(C.ClientError) as caught:
|
||||||
|
C.ApprovalEngine(self.base, str(self.token)).get("apr-1")
|
||||||
|
self.assertEqual(str(caught.exception), "GET 403")
|
||||||
|
|
||||||
|
|
||||||
|
class CliTests(unittest.TestCase):
|
||||||
|
def test_exit_codes(self):
|
||||||
|
self.assertEqual(CLI.exit_code({"outcome": "completed"}), 0)
|
||||||
|
self.assertEqual(CLI.exit_code({"outcome": "reverted"}), 3)
|
||||||
|
self.assertEqual(CLI.exit_code({"outcome": "refused", "reason": "approval_entry"}), 4)
|
||||||
|
self.assertEqual(CLI.exit_code({"outcome": "revert_failed"}), 5)
|
||||||
|
self.assertEqual(CLI.exit_code({"outcome": "stopped", "reason": "audit_unavailable"}), 6)
|
||||||
|
self.assertEqual(CLI.exit_code({"outcome": "completed", "reason": "receipt_not_in_custody"}), 6)
|
||||||
|
|
||||||
|
def test_setup_failure_refuses_before_any_effect(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = pathlib.Path(tmp)
|
||||||
|
(root / "jwt").write_text("synthetic-jwt")
|
||||||
|
cfg = {"openbao": {"addr": "http://127.0.0.1:9", "k8s_role": "r", "jwt_file": str(root / "jwt")}}
|
||||||
|
(root / "cfg.json").write_text(json.dumps(cfg))
|
||||||
|
admission = CLI.X.REPO / "examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml"
|
||||||
|
import contextlib, io
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()) as out:
|
||||||
|
code = CLI.main(["--admission", str(admission), "--approval-id", "apr-1", "--config", str(root / "cfg.json")])
|
||||||
|
self.assertEqual(code, 4)
|
||||||
|
self.assertEqual(json.loads(out.getvalue())["reason"].split(":")[0], "setup")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -189,6 +189,10 @@ class HappyPathTests(Base):
|
||||||
self.assertEqual([e["type"] for e in self.w.events], [X.STARTED, X.RECEIPT])
|
self.assertEqual([e["type"] for e in self.w.events], [X.STARTED, X.RECEIPT])
|
||||||
started, receipt = self.w.events
|
started, receipt = self.w.events
|
||||||
self.assertEqual(started["id"], f"run:{result['run_id']}:started")
|
self.assertEqual(started["id"], f"run:{result['run_id']}:started")
|
||||||
|
for event in self.w.events: # audit-core: eight truthy fields, offset-qualified occurred_at
|
||||||
|
for key in ("id", "type", "source", "subject", "tenant", "correlation_id", "occurred_at", "data"):
|
||||||
|
self.assertTrue(event[key], key)
|
||||||
|
self.assertEqual(dt.datetime.fromisoformat(event["occurred_at"]).utcoffset(), dt.timedelta(0))
|
||||||
data = receipt["data"]
|
data = receipt["data"]
|
||||||
self.assertEqual(data["pdp"], "not-evaluated")
|
self.assertEqual(data["pdp"], "not-evaluated")
|
||||||
self.assertEqual({r["label"] for r in data["decision_refs"]}, {"admission", "admission_digest", "approval"})
|
self.assertEqual({r["label"] for r in data["decision_refs"]}, {"admission", "admission_digest", "approval"})
|
||||||
|
|
@ -215,7 +219,7 @@ class RefusalTests(Base):
|
||||||
def test_refusals_before_any_effect(self):
|
def test_refusals_before_any_effect(self):
|
||||||
cases = {
|
cases = {
|
||||||
"not_this_executor": lambda: setattr(self.cfg, "executor_identity", "service:other"),
|
"not_this_executor": lambda: setattr(self.cfg, "executor_identity", "service:other"),
|
||||||
"source_drift": lambda: (self.root / "tools/service-binds/bind-lane.py").write_text("changed"),
|
"source_drift": lambda: (self.root / "bounded-executor/operations.py").write_text("changed"),
|
||||||
"trust_not_satisfied:runtime_secret_trust": lambda: setattr(self.w, "trust_ok", False),
|
"trust_not_satisfied:runtime_secret_trust": lambda: setattr(self.w, "trust_ok", False),
|
||||||
"approval_binding": lambda: self.w.carrier.approval["binding"]["target"].update(admission_digest="sha256:" + "0" * 64),
|
"approval_binding": lambda: self.w.carrier.approval["binding"]["target"].update(admission_digest="sha256:" + "0" * 64),
|
||||||
"approval_status": lambda: self.w.carrier.approval.update(status="requested"),
|
"approval_status": lambda: self.w.carrier.approval.update(status="requested"),
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,12 @@ spec:
|
||||||
plan:
|
plan:
|
||||||
source:
|
source:
|
||||||
repo: net-kingdom
|
repo: net-kingdom
|
||||||
commit: 3b1abc405423
|
commit: fe18096aa398
|
||||||
files:
|
files:
|
||||||
- path: tools/service-binds/bind-lane.py
|
- path: bounded-executor/executor.py
|
||||||
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
|
sha256: d724f4f831640c92b8b542dd2adab2e77f40d87d88f46d9482bff9fdcc4f6c70
|
||||||
|
- path: bounded-executor/operations.py
|
||||||
|
sha256: f6deabb65ee308b70b9adaa07f0449cd880534ef8c603c6e278c999bdb4106c8
|
||||||
- path: sso-mfa/k8s/synthetic-journey/journey.py
|
- path: sso-mfa/k8s/synthetic-journey/journey.py
|
||||||
sha256: c639054597be7647a3ae88ac7da53490a43b38d9f4f95e24cf4e9f54b18f997c
|
sha256: c639054597be7647a3ae88ac7da53490a43b38d9f4f95e24cf4e9f54b18f997c
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -149,30 +151,30 @@ spec:
|
||||||
scope: k8s/sso/deployment/authelia
|
scope: k8s/sso/deployment/authelia
|
||||||
checks_after:
|
checks_after:
|
||||||
- synthetic-journey
|
- synthetic-journey
|
||||||
plan_digest: sha256:ce9253adf5084dc1b40c1004258d9f8480b236d8ed26fabbb2a983c853a4dc21
|
plan_digest: sha256:7e5a93a864110dcde76df56bbb890b5642306fd506940323eedd62155dcab364
|
||||||
checks:
|
checks:
|
||||||
- id: kv-advanced
|
- id: kv-advanced
|
||||||
description: KV svc-authelia has exactly one new version, CAS on the previous one
|
description: KV svc-authelia has exactly one new version, CAS on the previous one
|
||||||
ref: tools/service-binds/bind-lane.py
|
ref: bounded-executor/operations.py
|
||||||
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
|
sha256: f6deabb65ee308b70b9adaa07f0449cd880534ef8c603c6e278c999bdb4106c8
|
||||||
timeout_seconds: 30
|
timeout_seconds: 30
|
||||||
expect: pass
|
expect: pass
|
||||||
- id: bind-signs-in
|
- id: bind-signs-in
|
||||||
description: the new KV value signs in to LLDAP as svc-authelia
|
description: the new KV value signs in to LLDAP as svc-authelia
|
||||||
ref: tools/service-binds/bind-lane.py
|
ref: bounded-executor/operations.py
|
||||||
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
|
sha256: f6deabb65ee308b70b9adaa07f0449cd880534ef8c603c6e278c999bdb4106c8
|
||||||
timeout_seconds: 30
|
timeout_seconds: 30
|
||||||
expect: pass
|
expect: pass
|
||||||
- id: eso-synced
|
- id: eso-synced
|
||||||
description: the ExternalSecret is Ready with a refresh after the step started; the executor never reads the Secret
|
description: the ExternalSecret is Ready with a refresh after the step started; the executor never reads the Secret
|
||||||
ref: tools/service-binds/bind-lane.py
|
ref: bounded-executor/operations.py
|
||||||
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
|
sha256: f6deabb65ee308b70b9adaa07f0449cd880534ef8c603c6e278c999bdb4106c8
|
||||||
timeout_seconds: 120
|
timeout_seconds: 120
|
||||||
expect: pass
|
expect: pass
|
||||||
- id: synthetic-journey
|
- id: synthetic-journey
|
||||||
description: syn-journey signs in through KeyCape, Authelia and privacyIDEA at aal2
|
description: syn-journey signs in through KeyCape, Authelia and privacyIDEA at aal2
|
||||||
ref: sso-mfa/k8s/synthetic-journey/journey.py
|
ref: bounded-executor/operations.py
|
||||||
sha256: c639054597be7647a3ae88ac7da53490a43b38d9f4f95e24cf4e9f54b18f997c
|
sha256: f6deabb65ee308b70b9adaa07f0449cd880534ef8c603c6e278c999bdb4106c8
|
||||||
timeout_seconds: 90
|
timeout_seconds: 90
|
||||||
expect: pass
|
expect: pass
|
||||||
stop_and_revert:
|
stop_and_revert:
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ spec:
|
||||||
plan:
|
plan:
|
||||||
source:
|
source:
|
||||||
repo: net-kingdom
|
repo: net-kingdom
|
||||||
commit: 3b1abc405423
|
commit: fe18096aa398
|
||||||
files:
|
files:
|
||||||
- path: sso-mfa/k8s/lldap/bind-split.py
|
- path: sso-mfa/k8s/lldap/bind-split.py
|
||||||
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
||||||
|
|
@ -97,7 +97,7 @@ spec:
|
||||||
scope: openbao/platform/workloads/net-kingdom/directory-binds
|
scope: openbao/platform/workloads/net-kingdom/directory-binds
|
||||||
checks_after:
|
checks_after:
|
||||||
- accounts-sign-in
|
- accounts-sign-in
|
||||||
plan_digest: sha256:a4c60ad06ae93b7ebc4d12417678a7bf8e7507f6670d8861cb5b30c2cd517b68
|
plan_digest: sha256:ad9f23b9ba366b9749e17b290e9583973463afcec021aa6ed9c2ce7bf9706bac
|
||||||
checks:
|
checks:
|
||||||
- id: accounts-read-only
|
- id: accounts-read-only
|
||||||
description: each account exists in lldap_strict_readonly only and cannot write
|
description: each account exists in lldap_strict_readonly only and cannot write
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ spec:
|
||||||
plan:
|
plan:
|
||||||
source:
|
source:
|
||||||
repo: net-kingdom
|
repo: net-kingdom
|
||||||
commit: 3b1abc405423
|
commit: fe18096aa398
|
||||||
files:
|
files:
|
||||||
- path: sso-mfa/k8s/lldap/bind-split.py
|
- path: sso-mfa/k8s/lldap/bind-split.py
|
||||||
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
||||||
|
|
@ -73,7 +73,7 @@ spec:
|
||||||
scope: k8s/sso
|
scope: k8s/sso
|
||||||
checks_after:
|
checks_after:
|
||||||
- split-shape
|
- split-shape
|
||||||
plan_digest: sha256:8c2195390ce140d2600667f2787c9fa2685a62ff918fb298d17e3b3aa3762bc5
|
plan_digest: sha256:fe39d7f0eb2d04369e505dbd6a4ed8e028ec4263ef6bff10a3d1d41db9ddb6da
|
||||||
checks:
|
checks:
|
||||||
- id: split-shape
|
- id: split-shape
|
||||||
description: every consumer binds as its own svc- account; no service binds as admin
|
description: every consumer binds as its own svc- account; no service binds as admin
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ spec:
|
||||||
plan:
|
plan:
|
||||||
source:
|
source:
|
||||||
repo: net-kingdom
|
repo: net-kingdom
|
||||||
commit: 3b1abc405423
|
commit: fe18096aa398
|
||||||
files:
|
files:
|
||||||
- path: sso-mfa/k8s/lldap/bind-split.py
|
- path: sso-mfa/k8s/lldap/bind-split.py
|
||||||
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
||||||
|
|
@ -97,7 +97,7 @@ spec:
|
||||||
scope: k8s/sso/deployment/lldap
|
scope: k8s/sso/deployment/lldap
|
||||||
checks_after:
|
checks_after:
|
||||||
- synthetic-journey
|
- synthetic-journey
|
||||||
plan_digest: sha256:78e9b24a77955c0b7372d6733e8a6b4da17656eb5a9100a416c4d73eef981d6f
|
plan_digest: sha256:f9ff4e81d9e998e709a2aa41b938d595cf671a1bbd0a43e157de572889b07218
|
||||||
checks:
|
checks:
|
||||||
- id: ld-admin-administers
|
- id: ld-admin-administers
|
||||||
description: ld-admin signs in and administers; admin keeps lldap_admin membership
|
description: ld-admin signs in and administers; admin keeps lldap_admin membership
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ spec:
|
||||||
plan:
|
plan:
|
||||||
source:
|
source:
|
||||||
repo: net-kingdom
|
repo: net-kingdom
|
||||||
commit: 3b1abc405423
|
commit: fe18096aa398
|
||||||
files:
|
files:
|
||||||
- path: sso-mfa/k8s/lldap/bind-split.py
|
- path: sso-mfa/k8s/lldap/bind-split.py
|
||||||
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
|
||||||
|
|
@ -108,7 +108,7 @@ spec:
|
||||||
scope: k8s/sso/deployment/lldap
|
scope: k8s/sso/deployment/lldap
|
||||||
checks_after:
|
checks_after:
|
||||||
- synthetic-journey
|
- synthetic-journey
|
||||||
plan_digest: sha256:80c14028288caf0994c1c5295e423527969c91f174f0ab2f7e08900f71283887
|
plan_digest: sha256:dd7e679008345e0d36d09c065b3fdf92dab5c4c5d5bd48eb048844df24a0de13
|
||||||
checks:
|
checks:
|
||||||
- id: ld-admin-administers
|
- id: ld-admin-administers
|
||||||
description: ld-admin signs in and administers; admin keeps lldap_admin membership
|
description: ld-admin signs in and administers; admin keeps lldap_admin membership
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ def pin(*paths): return [{"path": p, "sha256": hashlib.sha256((ROOT / p).read_by
|
||||||
def sha(p): return hashlib.sha256((ROOT / p).read_bytes()).hexdigest()
|
def sha(p): return hashlib.sha256((ROOT / p).read_bytes()).hexdigest()
|
||||||
JOURNEY = "sso-mfa/k8s/synthetic-journey/journey.py"
|
JOURNEY = "sso-mfa/k8s/synthetic-journey/journey.py"
|
||||||
LANE = "tools/service-binds/bind-lane.py"
|
LANE = "tools/service-binds/bind-lane.py"
|
||||||
|
RUNNER = "bounded-executor/executor.py"
|
||||||
|
PILOT_OPS = "bounded-executor/operations.py"
|
||||||
SPLIT = "sso-mfa/k8s/lldap/bind-split.py"
|
SPLIT = "sso-mfa/k8s/lldap/bind-split.py"
|
||||||
COMMON = {
|
COMMON = {
|
||||||
"approval": {"approvers": ["admin"], "principal_type": "HUMAN", "assurance": "aal2",
|
"approval": {"approvers": ["admin"], "principal_type": "HUMAN", "assurance": "aal2",
|
||||||
|
|
@ -56,7 +58,7 @@ rotation = doc(
|
||||||
{"mode": "OPERATE", "scope": "k8s/sso/externalsecret/authelia-secrets", "environments": P},
|
{"mode": "OPERATE", "scope": "k8s/sso/externalsecret/authelia-secrets", "environments": P},
|
||||||
{"mode": "OPERATE", "scope": "k8s/sso/deployment/authelia", "environments": P}],
|
{"mode": "OPERATE", "scope": "k8s/sso/deployment/authelia", "environments": P}],
|
||||||
{"unit": "consumer", "consumers": ["authelia"], "max_concurrent": 1},
|
{"unit": "consumer", "consumers": ["authelia"], "max_concurrent": 1},
|
||||||
[LANE, JOURNEY],
|
[RUNNER, PILOT_OPS, JOURNEY],
|
||||||
[step("generate", "openbao.kv.write-next-version", "openbao:" + KV[len("openbao/"):], "authelia", "delegable", True, "OPERATE", KV, ["kv-advanced"], "restore-kv"),
|
[step("generate", "openbao.kv.write-next-version", "openbao:" + KV[len("openbao/"):], "authelia", "delegable", True, "OPERATE", KV, ["kv-advanced"], "restore-kv"),
|
||||||
step("set-directory", "lldap.user.change-own-password-from-kv", "lldap:svc-authelia", "authelia", "delegable", True, "OPERATE", "lldap/people/svc-authelia", ["bind-signs-in"], "restore-directory"),
|
step("set-directory", "lldap.user.change-own-password-from-kv", "lldap:svc-authelia", "authelia", "delegable", True, "OPERATE", "lldap/people/svc-authelia", ["bind-signs-in"], "restore-directory"),
|
||||||
step("deliver", "eso.force-sync", "k8s:sso/externalsecret/authelia-secrets", "authelia", "delegable", True, "OPERATE", "k8s/sso/externalsecret/authelia-secrets", ["eso-synced"], "redeliver"),
|
step("deliver", "eso.force-sync", "k8s:sso/externalsecret/authelia-secrets", "authelia", "delegable", True, "OPERATE", "k8s/sso/externalsecret/authelia-secrets", ["eso-synced"], "redeliver"),
|
||||||
|
|
@ -65,10 +67,10 @@ rotation = doc(
|
||||||
step("restore-directory", "lldap.user.change-own-password-from-kv", "lldap:svc-authelia", "authelia", "delegable", True, "OPERATE", "lldap/people/svc-authelia", ["bind-signs-in"]),
|
step("restore-directory", "lldap.user.change-own-password-from-kv", "lldap:svc-authelia", "authelia", "delegable", True, "OPERATE", "lldap/people/svc-authelia", ["bind-signs-in"]),
|
||||||
step("redeliver", "eso.force-sync", "k8s:sso/externalsecret/authelia-secrets", "authelia", "delegable", True, "OPERATE", "k8s/sso/externalsecret/authelia-secrets", ["eso-synced"]),
|
step("redeliver", "eso.force-sync", "k8s:sso/externalsecret/authelia-secrets", "authelia", "delegable", True, "OPERATE", "k8s/sso/externalsecret/authelia-secrets", ["eso-synced"]),
|
||||||
step("restart-again", "k8s.rollout-restart", "k8s:sso/deployment/authelia", "authelia", "delegable", True, "OPERATE", "k8s/sso/deployment/authelia", ["synthetic-journey"])],
|
step("restart-again", "k8s.rollout-restart", "k8s:sso/deployment/authelia", "authelia", "delegable", True, "OPERATE", "k8s/sso/deployment/authelia", ["synthetic-journey"])],
|
||||||
[check("kv-advanced", "KV svc-authelia has exactly one new version, CAS on the previous one", LANE, 30),
|
[check("kv-advanced", "KV svc-authelia has exactly one new version, CAS on the previous one", PILOT_OPS, 30),
|
||||||
check("bind-signs-in", "the new KV value signs in to LLDAP as svc-authelia", LANE, 30),
|
check("bind-signs-in", "the new KV value signs in to LLDAP as svc-authelia", PILOT_OPS, 30),
|
||||||
check("eso-synced", "the ExternalSecret is Ready with a refresh after the step started; the executor never reads the Secret", LANE, 120),
|
check("eso-synced", "the ExternalSecret is Ready with a refresh after the step started; the executor never reads the Secret", PILOT_OPS, 120),
|
||||||
check("synthetic-journey", "syn-journey signs in through KeyCape, Authelia and privacyIDEA at aal2", JOURNEY, 90)],
|
check("synthetic-journey", "syn-journey signs in through KeyCape, Authelia and privacyIDEA at aal2", PILOT_OPS, 90)],
|
||||||
{"on_check_failure": "revert", "on_budget_exhausted": "revert", "on_unknown": "stop",
|
{"on_check_failure": "revert", "on_budget_exhausted": "revert", "on_unknown": "stop",
|
||||||
"on_revert_failure": "stop-and-alert", "alert": "founder via State Hub message and audit-core"},
|
"on_revert_failure": "stop-and-alert", "alert": "founder via State Hub message and audit-core"},
|
||||||
{"wall_time_seconds": 1200, "attempts": 2, "cost": {"compute_cpu_seconds": 120, "egress_bytes": 5000000, "currency": "EUR", "amount": 0.05}},
|
{"wall_time_seconds": 1200, "attempts": 2, "cost": {"compute_cpu_seconds": 120, "egress_bytes": 5000000, "currency": "EUR", "amount": 0.05}},
|
||||||
|
|
|
||||||
|
|
@ -515,7 +515,7 @@ The image part moved to T13 (split agreed 2026-10-31).
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: NK-WP-0056-T13
|
id: NK-WP-0056-T13
|
||||||
status: todo
|
status: progress
|
||||||
priority: high
|
priority: high
|
||||||
assignee: claude-code
|
assignee: claude-code
|
||||||
depends_on: [NK-WP-0056-T12]
|
depends_on: [NK-WP-0056-T12]
|
||||||
|
|
@ -536,6 +536,24 @@ T03 ServiceAccount, Role and an egress-only NetworkPolicy.
|
||||||
**Done when:** the image builds in the workflow, the rapp renders and passes `server-dry-run` against a synthetic
|
**Done when:** the image builds in the workflow, the rapp renders and passes `server-dry-run` against a synthetic
|
||||||
manifest check, and nothing is applied.
|
manifest check, and nothing is applied.
|
||||||
|
|
||||||
|
**2026-10-31, in progress.** Done in net-kingdom:
|
||||||
|
|
||||||
|
- `bounded-executor/cli.py` (exit codes 0/3/4/5/6), `clients.py` (approval-engine GET/consume, audit-core
|
||||||
|
`POST /v1/events` with `Idempotency-Key`);
|
||||||
|
- events now carry `occurred_at`, which audit-core requires;
|
||||||
|
- `Containerfile`: pinned Python and LLDAP stages, hash-locked requirements, UID 10001; builds locally;
|
||||||
|
- `.forgejo/workflows/bounded-executor-image.yaml`, and the executor tests are in `identity-journeys.yaml`;
|
||||||
|
- the pilot example pins the executed files (`executor.py`, `operations.py`, `journey.py`). 35 tests.
|
||||||
|
|
||||||
|
Left:
|
||||||
|
|
||||||
|
- `rmgr rapp wrap` created local drafts in `~/rapp-bounded-executor` (not a git repo yet, no remote). Its
|
||||||
|
rapp-postgres consumer, Postgres handoff and generic `image.yaml` were removed: the executor has no database.
|
||||||
|
- The drafts still need reshaping to the decisions: one Job per admission in `sso`; no namespace, Deployment,
|
||||||
|
Service or probes; the T03 ServiceAccount, Role and RoleBinding; an egress-only NetworkPolicy; the job
|
||||||
|
template's render; tests; `runtime_dependencies` without the database engine.
|
||||||
|
- Then `git init` and commit, and the founder creates the Forgejo remote.
|
||||||
|
|
||||||
## Pilot: unattended bind rotation under one admission
|
## Pilot: unattended bind rotation under one admission
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue