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

@ -10,13 +10,17 @@
## One-liner
secrets-engine is the Lifecycle engine for cataloged credential work: a
decision-gated Python CLI that validates non-secret lane metadata and
orchestrates narrowly scoped OpenBao policy, AppRole, provisioning,
verification, delivery, routing, handoff, and revocation actions.
secrets-engine is the Lifecycle engine for cataloged credential work. Its
library validates non-secret lane metadata and orchestrates narrowly scoped
OpenBao policy, AppRole, provisioning, verification, delivery, routing,
handoff, and revocation actions. The CLI is that library in one process. The
process binds one stage, `build`, `test`, or `production`, and receives that
stage's credential at start. `serve` keeps the same library running for that
stage. Other engines may use OpenBao for other capabilities.
It is a deterministic API over OpenBao, not a vault, not a policy decision
point, not an identity provider, and not a general secrets API.
It is a deterministic processor over OpenBao, not a vault, not a policy
decision point, not an identity provider, and not a general secrets API.
Exec-time delivery remains the default for a one-task caller.
## Implemented Capabilities
@ -225,9 +229,25 @@ through the merge-safe write path. Persistent overlay states
directory; they do not recreate OpenBao objects and do not rotate provider
credentials.
## One-stage process
- Planning stays in the library and does not open the store. Apply still uses
the OpenBao client.
- `instance` prints the one bound stage. The injected credential is not
printed, returned, or written to Git, State Hub, logs, or the workplan.
- A second stage credential in the same process refuses startup.
- `serve` keeps the lifecycle library the CLI uses running for that one stage.
It is the in-process manifestation of this library.
- `--allow` and `SECRETS_ENGINE_LOCAL_ALLOW` do not authorize apply or the
service. Permission to use an instance is the access-engine claim in
`docs/engine-use-claim.md`. Protected side effects still require a decision
record or the published unreachable-engine stance.
## CLI Surface
```text
secrets-engine instance
secrets-engine serve <catalog-id> --operation suspend|deactivate|destroy
secrets-engine catalog list|show
secrets-engine decision inspect
secrets-engine plan
@ -262,7 +282,8 @@ metadata. `secrets-engine wrap` implements response-wrapped operator handoff.
## Not Implemented
- A service API, daemon, UI, queue, scheduler, or remote multi-user service.
- A remote multi-user API, UI, queue, or scheduler. `serve` is the kept-running
in-process manifestation of this library for one stage.
- Engine-action authority for the `service-jwt` identity. The platform JWT
role is login-only, and its CCR negative checks (RPF-WP-0035-T02) are open.
- Provider-side / workload consumer rotation. Overlay lane state is local and
@ -387,7 +408,9 @@ title: AppRole-scoped exec delivery
description: Fetches one declared KV field through a lane AppRole and injects it into a child
process using exec-env or a temporary npm config, with output redaction, explicit scoped-token
self-revocation, and non-secret cleanup evidence. Adapters: exec-env, npm-config, exec-file;
`wrap` provides response-wrapped operator handoff. This is CLI-local delivery; service delivery is absent.
`wrap` provides response-wrapped operator handoff. Exec-time delivery remains
the default for a one-task caller. `serve` keeps the lifecycle library running
for one injected stage and does not deliver values.
keywords: [secrets, delivery, exec, npm, injection, redaction, openbao]
```

View file

@ -24,7 +24,7 @@
| workplan | SECRETS-WP-0013 | proposed | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
| workplan | SECRETS-WP-0014 | active | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
| workplan | SECRETS-WP-0015 | active | — | workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md |
| workplan | SECRETS-WP-0016 | ready | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| workplan | SECRETS-WP-0016 | finished | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| task | SECRETS-WP-ADHOC-2026-08-21-T01 | done | — | workplans/ADHOC-2026-08-21.md |
| task | SECRETS-WP-ADHOC-2026-08-23-T01 | done | — | workplans/ADHOC-2026-08-23.md |
| task | SECRETS-WP-0001-T01 | done | — | workplans/SECRETS-WP-0001-statehub-bootstrap.md |
@ -98,11 +98,11 @@
| task | SECRETS-WP-0015-T03 | done | — | workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md |
| task | SECRETS-WP-0015-T04 | wait | — | workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md |
| task | SECRETS-WP-0015-T05 | done | — | workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md |
| task | SECRETS-WP-0016-T01 | todo | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| task | SECRETS-WP-0016-T02 | todo | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| task | SECRETS-WP-0016-T03 | todo | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| task | SECRETS-WP-0016-T04 | todo | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| task | SECRETS-WP-0016-T05 | todo | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| task | SECRETS-WP-0016-T01 | done | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| task | SECRETS-WP-0016-T02 | done | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| task | SECRETS-WP-0016-T03 | done | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| task | SECRETS-WP-0016-T04 | done | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| task | SECRETS-WP-0016-T05 | done | — | workplans/SECRETS-WP-0016-engine-manifestations.md |
| intake | SECRETS-IN-0001 | closed | — | intakes/intakes.md |
| intake | SECRETS-IN-0002 | closed | — | intakes/intakes.md |
| intake | SECRETS-IN-0003 | closed | — | intakes/intakes.md |

22
docs/engine-use-claim.md Normal file
View file

@ -0,0 +1,22 @@
# Use claim consumed by secrets-engine
Permission to start or call one secrets-engine stage instance is an
access-engine decision. This engine consumes that claim. It does not decide it
and it does not store a credential for it.
## Claim
- Decider: access-engine.
- Subject: an operator allowed to use one stage instance.
- Object: one secrets-engine process bound to exactly one stage.
- Stage: `build`, `test`, or `production`.
- Action: use.
- Effect: the operator may call that instance for the bound stage.
The stage credential is injected into the process at start and stays there.
Use of the instance does not confer that credential. Delegation of a task
transfers neither the use decision nor the injected credential.
`--allow` and `SECRETS_ENGINE_LOCAL_ALLOW` are not this claim. They do not
authorize apply or the service. A protected side effect still requires a
decision record or the published unreachable-engine stance.

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)

View file

@ -0,0 +1,247 @@
"""SECRETS-WP-0016: plan stays off the store client; apply still calls it.
Later cases cover one-stage process binding, the in-process service, and the
refusal of a local allow flag. Fixture credentials stay out of captured output.
"""
from __future__ import annotations
import copy
from pathlib import Path
import pytest
from secrets_engine import cli, lifecycle
from secrets_engine.apply import apply_plan
from secrets_engine.catalog import validate_entry
from secrets_engine.config import Config, repo_root
from secrets_engine.errors import DecisionError
from secrets_engine.openbao import OpenBaoClient
from secrets_engine.plan import build_plan
from secrets_engine.service import invoke_protected, run_service
from secrets_engine.stage_binding import STAGE_TOKEN_FILES, StageBinding, StartupError
from tests.test_catalog import VALID
SENTINEL = "fixture-stage-credential-wp0016"
STAGE_ENV = tuple(STAGE_TOKEN_FILES.values())
class RecordingApplyClient:
def __init__(self):
self.calls: list[str] = []
def kv_mount_exists(self, mount):
self.calls.append("kv_mount_exists")
assert mount
return False
def ensure_kv_mount(self, mount):
self.calls.append("ensure_kv_mount")
assert mount
def read_policy(self, name):
self.calls.append("read_policy")
assert name
return None
def write_policy(self, name, hcl):
self.calls.append("write_policy")
assert name and hcl
def ensure_approle_enabled(self):
self.calls.append("ensure_approle_enabled")
def write_approle(self, role_name, policies, ttl="30m", **kwargs):
self.calls.append("write_approle")
assert role_name and policies and ttl
del kwargs
def _clear_stage_env(monkeypatch):
for key in STAGE_ENV:
monkeypatch.delenv(key, raising=False)
monkeypatch.delenv("SECRETS_ENGINE_LOCAL_ALLOW", raising=False)
def _isolate_authority(monkeypatch, tmp_path: Path):
"""No serving PDP, no published stance file, no stage credential."""
_clear_stage_env(monkeypatch)
monkeypatch.setenv("SECRETS_ENGINE_PEP_STANCE", str(tmp_path / "missing-stance.yaml"))
for key in (
"SECRETS_ENGINE_APPROVAL_URL",
"SECRETS_ENGINE_APPROVAL_TOKEN_FILE",
"SECRETS_ENGINE_PDP_URL",
"SECRETS_ENGINE_PDP_TOKEN_FILE",
"SECRETS_ENGINE_PDP_TRUST_FILE",
"SECRETS_ENGINE_PDP_VERIFIER",
"SECRETS_ENGINE_OPENBAO_JWT_LOGIN",
"BAO_TOKEN",
"VAULT_TOKEN",
):
monkeypatch.delenv(key, raising=False)
def test_plan_path_constructs_no_openbao_client(monkeypatch):
constructed: list[str] = []
def _refuse_init(self, *args, **kwargs):
constructed.append("init")
raise AssertionError("plan path constructed an OpenBao client")
def _refuse_resolve(cls, *args, **kwargs):
constructed.append("resolve")
raise AssertionError("plan path resolved an OpenBao client")
monkeypatch.setattr(OpenBaoClient, "__init__", _refuse_init)
monkeypatch.setattr(OpenBaoClient, "resolve", _refuse_resolve)
entry = validate_entry(copy.deepcopy(VALID))
plan = build_plan(entry, "test", decision_id="d1")
assert plan.catalog_id == entry.id
assert plan.actions
assert constructed == []
def test_apply_path_issues_openbao_calls():
entry = validate_entry(copy.deepcopy(VALID))
plan = build_plan(entry, "test", decision_id="d1")
client = RecordingApplyClient()
result = apply_plan(client, entry, plan)
for name in ("ensure_kv_mount", "write_policy", "write_approle"):
assert name in client.calls
assert any(name.startswith("kv-mount") for name in result.applied)
assert any(name.startswith("policy") for name in result.applied)
assert any(name.startswith("approle") for name in result.applied)
def _write_stage_file(tmp_path: Path, name: str) -> Path:
path = tmp_path / name
path.write_text(SENTINEL + "\n", encoding="utf-8")
return path
def _assert_sentinel_absent(captured):
assert SENTINEL not in captured.out
assert SENTINEL not in captured.err
@pytest.mark.parametrize("stage", ("build", "test", "production"))
def test_cli_binds_one_stage_and_hides_the_credential(stage, tmp_path, monkeypatch, capsys):
_clear_stage_env(monkeypatch)
path = _write_stage_file(tmp_path, stage)
monkeypatch.setenv(STAGE_TOKEN_FILES[stage], str(path))
for _ in range(2):
capsys.readouterr()
code = cli.main(["instance"])
captured = capsys.readouterr()
assert code == 0
assert captured.out.strip() == f"stage={stage}"
_assert_sentinel_absent(captured)
def test_cli_refuses_two_stage_credentials(tmp_path, monkeypatch, capsys):
_clear_stage_env(monkeypatch)
monkeypatch.setenv(STAGE_TOKEN_FILES["build"], str(_write_stage_file(tmp_path, "build")))
monkeypatch.setenv(STAGE_TOKEN_FILES["test"], str(_write_stage_file(tmp_path, "test")))
for _ in range(2):
capsys.readouterr()
code = cli.main(["instance"])
captured = capsys.readouterr()
assert code != 0
assert "more than one stage credential is present" in captured.err
_assert_sentinel_absent(captured)
def test_service_lifecycle_uses_the_cli_library_function(monkeypatch):
entry = validate_entry(copy.deepcopy(VALID))
binding = StageBinding(stage="test", credential=SENTINEL)
seen: list[tuple[str, str]] = []
real = lifecycle.build_lifecycle_plan
def spy(entry_arg, operation):
seen.append((entry_arg.id, operation))
return real(entry_arg, operation)
monkeypatch.setattr(lifecycle, "build_lifecycle_plan", spy)
plans = run_service(binding, [{"entry": entry, "operation": "suspend"}])
direct = real(entry, "suspend")
assert seen == [(entry.id, "suspend")]
assert plans[0].operation == direct.operation
assert plans[0].catalog_id == direct.catalog_id
assert [action.kind for action in plans[0].actions] == [
action.kind for action in direct.actions
]
rendered = plans[0].render()
assert SENTINEL not in rendered
def test_service_refuses_a_second_stage_credential():
entry = validate_entry(copy.deepcopy(VALID))
binding = StageBinding(stage="build", credential=SENTINEL)
with pytest.raises(StartupError, match="another stage credential"):
run_service(
binding,
[
{
"entry": entry,
"operation": "suspend",
"foreign_stage_credential": "present",
}
],
)
def test_no_deployment_manifest_was_added():
root = repo_root()
for path in root.rglob("*"):
if ".git" in path.parts or path.suffix not in {".yaml", ".yml"}:
continue
text = path.read_text(encoding="utf-8", errors="ignore")
assert "kind: Deployment" not in text
def test_local_allow_does_not_authorize_protected_apply(tmp_path, monkeypatch, capsys):
_isolate_authority(monkeypatch, tmp_path)
monkeypatch.setenv("SECRETS_ENGINE_LOCAL_ALLOW", "1")
monkeypatch.setattr(
OpenBaoClient,
"resolve",
lambda *_a, **_k: pytest.fail("protected apply opened a backend"),
)
code = cli.main(
[
"apply",
"whynot-design-npm-publish",
"--stage",
"prod",
"--auth",
"env",
"--allow",
]
)
captured = capsys.readouterr()
assert code != 0
assert "applied:" not in captured.out
assert "unable to load PEP stance" in captured.err or "DecisionError" in captured.err or "error:" in captured.err
_assert_sentinel_absent(captured)
def test_local_allow_does_not_authorize_protected_service_call(tmp_path, monkeypatch):
_isolate_authority(monkeypatch, tmp_path)
monkeypatch.setenv("SECRETS_ENGINE_LOCAL_ALLOW", "1")
entry = cli.get_entry(repo_root() / "catalog", "whynot-design-npm-publish")
with pytest.raises(DecisionError):
invoke_protected(Config.load(), entry, "apply", allow=True, auth_args=_Auth())
class _Auth:
auth = "env"
bootstrap_token_file = None
def test_use_claim_names_access_engine_and_stores_no_credential():
path = repo_root() / "docs" / "engine-use-claim.md"
text = path.read_text(encoding="utf-8")
assert "access-engine" in text
assert SENTINEL not in text
lowered = text.lower()
assert "token=" not in lowered
assert "bao_token" not in lowered

View file

@ -4,7 +4,7 @@ type: workplan
title: "Manifest the lifecycle engine as one library, a CLI, and a single-stage service"
domain: infotech
repo: secrets-engine
status: ready
status: finished
owner: secrets-engine
topic_slug: custodian
created: "2026-10-22"
@ -24,12 +24,13 @@ Implements the engine shape in `INTENT.md` after gate-house ADR-015
(accepted 2026-10-22) and `ArchitectureBlueprint.md` §2.2.
Reviewed against the tree on 2026-10-22. `secrets_engine.plan.build_plan`
already renders a plan without an OpenBao client. `secrets_engine.cli` is the
process. `SCOPE.md` records that service delivery is absent, and that a
service API or daemon is not implemented. `service-jwt` can log in and cannot
yet carry engine actions. Accepted security-layer-model v0.8 still governs
PEP behavior. Proposed v0.9 §3.3 carries the same engine wording and does not
yet govern.
renders a plan without an OpenBao client. `secrets_engine.cli` is the process,
bound to one stage credential at start. `serve` keeps that same library
running for the bound stage. `SCOPE.md` records the library, the one-stage
CLI, and that in-process manifestation. Exec-time delivery remains the default
for a one-task caller. `service-jwt` can log in and cannot yet carry engine
actions. Accepted security-layer-model v0.8 still governs PEP behavior.
Proposed v0.9 §3.3 carries the same engine wording and does not yet govern.
This workplan does not add a release-unit declaration, does not deploy a
workload, and does not make this engine the only client of OpenBao or a
@ -39,7 +40,7 @@ decider of who may use it.
```task
id: SECRETS-WP-0016-T01
status: todo
status: done
priority: high
state_hub_task_id: "4319d9c6-f7e1-500c-9011-a58c216d555d"
```
@ -56,7 +57,7 @@ path still performs the OpenBao calls.
```task
id: SECRETS-WP-0016-T02
status: todo
status: done
priority: high
depends_on: [SECRETS-WP-0016-T01]
state_hub_task_id: "f5d5e9ba-ba56-56cc-8920-1417d70beb1d"
@ -75,7 +76,7 @@ stderr, and shows startup failure when two stage credentials are present.
```task
id: SECRETS-WP-0016-T03
status: todo
status: done
priority: medium
depends_on: [SECRETS-WP-0016-T02]
state_hub_task_id: "c37280f3-e9d0-5cdd-ba15-a95ec5f9aa0c"
@ -92,7 +93,7 @@ lifecycle call goes through the shipped library functions used by the CLI.
```task
id: SECRETS-WP-0016-T04
status: todo
status: done
priority: medium
depends_on: [SECRETS-WP-0016-T03]
state_hub_task_id: "89b71b5b-a2ce-57d6-b16b-0f94d76e0104"
@ -111,7 +112,7 @@ and the claim shape is written in `docs/` without storing a credential.
```task
id: SECRETS-WP-0016-T05
status: todo
status: done
priority: low
depends_on: [SECRETS-WP-0016-T03, SECRETS-WP-0016-T04]
state_hub_task_id: "6b013ea1-4a04-57e8-bab2-acb196303e0a"