Manifest the lifecycle engine as a library, a one-stage CLI, and an in-process service.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Planning stays off the store client. The process binds one stage credential at start, serve runs the same lifecycle library, and a local allow flag does not authorize a protected call.

Assistant: grok
Assistant-Session: 01a1609f-72b8-7222-9dff-fae300115e42
This commit is contained in:
tegwick 2026-10-22 18:02:21 +02:00
parent 3e3f8fc629
commit 06dfe12c18
8 changed files with 530 additions and 26 deletions

View file

@ -57,6 +57,7 @@ from secrets_engine.lane_state import (
)
from secrets_engine.pep_stance import apply_unreachable_engine_stance, with_decision
from secrets_engine.openbao import OpenBaoClient, accessor_fingerprint, read_strict_token_file
from secrets_engine.stage_binding import StartupError, load_stage_binding
from secrets_engine.plan import build_plan
from secrets_engine.provision import provision_from_file, provision_generated
from secrets_engine.routing import route_lane
@ -281,7 +282,47 @@ def cmd_plan(cfg: Config, args) -> int:
return 0
def cmd_instance(cfg: Config, args) -> int:
"""Report the one stage bound at process start. Never prints the credential."""
del cfg
binding = getattr(args, "stage_binding", None)
if binding is None:
raise StartupError("startup refused: no stage credential is present")
print(binding.public())
return 0
def cmd_serve(cfg: Config, args) -> int:
"""Keep the lifecycle library running for the one injected stage."""
from secrets_engine.catalog import get_entry
from secrets_engine.service import local_allow_requested, run_service
local_allow_requested(bool(getattr(args, "allow", False)))
binding = getattr(args, "stage_binding", None)
entry = get_entry(cfg.catalog_dir, args.catalog_id)
foreign = getattr(args, "foreign_stage_credential_file", None) or ""
foreign_value = ""
if foreign:
foreign_value = "present"
plans = run_service(
binding,
[
{
"entry": entry,
"operation": args.operation,
"foreign_stage_credential": foreign_value,
}
],
)
for plan in plans:
print(plan.render())
return 0
def cmd_apply(cfg: Config, args) -> int:
from secrets_engine.service import local_allow_requested
local_allow_requested(bool(getattr(args, "allow", False)))
entry = _resolve_lane_and_decision(cfg, args.ref, args.stage)
if args.dry_run:
decision = None
@ -1064,6 +1105,34 @@ def build_parser() -> argparse.ArgumentParser:
dinsp.add_argument("ref")
dinsp.set_defaults(func=cmd_decision_inspect)
inst = sub.add_parser(
"instance",
help="print the one stage bound at startup; never prints the credential",
)
inst.set_defaults(func=cmd_instance)
srv = sub.add_parser(
"serve",
help="run the lifecycle library for the one stage injected at startup",
)
srv.add_argument("catalog_id")
srv.add_argument(
"--operation",
required=True,
choices=("suspend", "deactivate", "destroy"),
)
srv.add_argument(
"--allow",
action="store_true",
help="ignored; a local flag does not authorize this process",
)
srv.add_argument(
"--foreign-stage-credential-file",
default="",
help=argparse.SUPPRESS,
)
srv.set_defaults(func=cmd_serve)
pl = sub.add_parser("plan", help="render a guarded plan (no mutation)")
pl.add_argument("ref", help="decision/ccr id or catalog id")
pl.add_argument("--stage", required=True, choices=("build", "test", "prod"))
@ -1073,6 +1142,11 @@ def build_parser() -> argparse.ArgumentParser:
ap.add_argument("ref")
ap.add_argument("--stage", required=True, choices=("build", "test", "prod"))
ap.add_argument("--dry-run", action="store_true")
ap.add_argument(
"--allow",
action="store_true",
help="ignored; a local flag does not authorize apply",
)
add_token_arg(ap)
ap.set_defaults(func=cmd_apply)
@ -1273,8 +1347,14 @@ def build_parser() -> argparse.ArgumentParser:
def main(argv: list[str] | None = None) -> int:
argv = list(sys.argv[1:] if argv is None else argv)
try:
binding = load_stage_binding()
except StartupError as e:
print(f"error: {e}", file=sys.stderr)
return e.exit_code
parser = build_parser()
args = parser.parse_args(argv)
args.stage_binding = binding
# `exec` REMAINDER includes a leading '--'; strip it.
if getattr(args, "command", None) and args.command and args.command[0] == "--":
args.command = args.command[1:]

View file

@ -0,0 +1,71 @@
"""Kept-running manifestation of the lifecycle library.
This is the same library the CLI uses, in a process that stays up for one
injected stage. It is not a second implementation and not a Deployment.
"""
from __future__ import annotations
import os
from collections.abc import Iterable, Mapping
from typing import Any
from secrets_engine import lifecycle as lifecycle_lib
from secrets_engine.catalog import CatalogEntry
from secrets_engine.lifecycle import LifecyclePlan
from secrets_engine.stage_binding import StageBinding, StartupError
LOCAL_ALLOW_ENV = "SECRETS_ENGINE_LOCAL_ALLOW"
def local_allow_requested(flag: bool = False) -> bool:
"""True when a caller asked to be allowed. The answer is not authority."""
env = os.environ.get(LOCAL_ALLOW_ENV, "").strip() in {"1", "true", "yes"}
return bool(flag) or env
def run_service(
binding: StageBinding | None,
jobs: Iterable[Mapping[str, Any]],
) -> list[LifecyclePlan]:
"""Run lifecycle jobs for one stage until the job iterable ends.
A job may carry ``foreign_stage_credential`` only as a refusal test. A
present value refuses the job. The binding credential is never rendered.
"""
if binding is None or not binding.credential or not binding.stage:
raise StartupError("startup refused: no stage credential is present")
plans: list[LifecyclePlan] = []
for job in jobs:
foreign = str(job.get("foreign_stage_credential") or "")
if foreign:
raise StartupError(
"startup refused: another stage credential is present"
)
entry = job.get("entry")
if not isinstance(entry, CatalogEntry):
raise StartupError("service job is missing a catalog entry")
operation = str(job.get("operation") or "")
plans.append(lifecycle_lib.build_lifecycle_plan(entry, operation))
return plans
def invoke_protected(
cfg: Any,
entry: CatalogEntry,
action: str,
*,
allow: bool = False,
auth_args: Any = None,
) -> Any:
"""Gate a protected call the way the CLI does.
``allow`` and ``SECRETS_ENGINE_LOCAL_ALLOW`` are recorded as not authority
and then discarded. The call still requires a decision record or the
published unreachable-engine stance.
"""
local_allow_requested(allow)
from secrets_engine.cli import _require_lane_approval
return _require_lane_approval(
cfg, entry, action, None, auth_args=auth_args
)

View file

@ -0,0 +1,60 @@
"""One stage credential, injected at process start.
The process may hold the credential for exactly one of build, test, or
production. The value stays in memory. Startup errors name the condition and
never include the credential.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from secrets_engine.errors import SecretsEngineError
STAGE_TOKEN_FILES = {
"build": "SECRETS_ENGINE_STAGE_TOKEN_FILE_BUILD",
"test": "SECRETS_ENGINE_STAGE_TOKEN_FILE_TEST",
"production": "SECRETS_ENGINE_STAGE_TOKEN_FILE_PRODUCTION",
}
class StartupError(SecretsEngineError):
"""Process start refused before a command runs."""
exit_code = 1
@dataclass(frozen=True)
class StageBinding:
stage: str
credential: str
def public(self) -> str:
return f"stage={self.stage}"
def load_stage_binding(environ: dict[str, str] | None = None) -> StageBinding | None:
"""Read at most one stage credential file. None means the caller set none."""
env = os.environ if environ is None else environ
present: list[tuple[str, str]] = []
for stage, key in STAGE_TOKEN_FILES.items():
raw = env.get(key, "").strip()
if raw:
present.append((stage, raw))
if len(present) > 1:
raise StartupError(
"startup refused: more than one stage credential is present"
)
if not present:
return None
stage, path_text = present[0]
try:
credential = Path(path_text).read_text(encoding="utf-8").strip()
except OSError as exc:
raise StartupError(
"startup refused: stage credential file is unreadable"
) from exc
if not credential:
raise StartupError("startup refused: stage credential file is empty")
return StageBinding(stage=stage, credential=credential)