oc_chat
Loading...
Searching...
No Matches
risk_profile.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/core/risk_profile.py
4
5"""
6Risk-level runtime profile (M8.0 WU-8; §6, F66 / F33).
7
8Purpose
9-------
10The M8 enforcement posture is one of three operator-selected runtime profiles,
11named for the *risk level* the operator accepts (never the deployment context):
12
13* ``permissive`` — L1 hardened in-process; importable-tree writes allowed;
14 external-content fetch confirm-gated; provider egress governed. The in-process
15 ACE residual is *accepted* (T1; no determined in-process adversary).
16* ``guarded`` — ``permissive`` + the deployment-shape invariant enforced
17 (WU-7 / Appendix C G-1): refuses to start if a harness importable root is
18 inside the sandbox, closing write→import by topology (T2).
19* ``isolated`` — L3 cross-process broker; the agent holds no sink and no key
20 (M8.6b). The first real boundary (T3).
21
22**OOB-only (F33):** the profile is selected *operator-out-of-band only* — read
23from the ``OC_CHAT_PROFILE`` process environment variable set at launch, NEVER
24from a settings file / prompt / resume / model output (any of which an injected
25agent could influence). An unset / unrecognized value falls to the M8.0 default
26``permissive`` (the dogfooding posture, §6: "monorepo dogfooding is permissive");
27released / T2 deployments select ``guarded`` out-of-band (the installer / docs
28guide this, F69). The resolved profile is logged at resolution.
29
30This module establishes the profile *contract*; the profile-scoped behaviors
31(the external-content confirmation gate F67, the ``permissive`` importable-write
32human-confirm tier F70, and the ``guarded`` deployment-shape invariant) hook into
33it in their own work-units (WU-8 follow-ups / WU-7).
34
35Responsibilities
36----------------
37- :data:`RiskProfile` / :data:`VALID_PROFILES` — the three profile names, as a
38 type and as the validation set.
39- :data:`PROFILE_ENV_VAR` / :data:`DEFAULT_PROFILE` — the out-of-band selection
40 variable and the M8.0 default profile.
41- :func:`resolve_risk_profile` — read the active profile from the process
42 environment and log the resolved value.
43
44Contracts
45---------
46- The profile is read from the ``OC_CHAT_PROFILE`` process environment variable
47 only; no settings file, prompt, resume payload or model output is consulted.
48- Matching is case-insensitive and whitespace-tolerant, and the returned value is
49 always a member of :data:`VALID_PROFILES`.
50- An unset, empty or unrecognized value resolves to :data:`DEFAULT_PROFILE`
51 (``permissive``); resolution never raises.
52
53Diagnostics
54-----------
55Domain: OC_CHAT.CORE
56Levels:
57 L2 — the resolved active profile, and an unrecognized ``OC_CHAT_PROFILE``
58 value falling to the default
59 L3 — (none) resolution has no per-call detail beyond the L2 records
60 L4 — deep tracing
61
62Dependencies
63------------
64- oc_diagnostics (structured debug logging via _dbg)
65"""
66from __future__ import annotations
67
68import os
69from typing import Literal
70
71from oc_diagnostics import _dbg
72
73RiskProfile = Literal["permissive", "guarded", "isolated"]
74
75#: The valid profile names.
76VALID_PROFILES: frozenset[str] = frozenset({"permissive", "guarded", "isolated"})
77
78#: The out-of-band selection variable (process env only; never settings/model).
79PROFILE_ENV_VAR = "OC_CHAT_PROFILE"
80
81#: M8.0 default (the dogfooding posture). Released/T2 deployments set "guarded".
82DEFAULT_PROFILE: RiskProfile = "permissive"
83
84
85def resolve_risk_profile() -> RiskProfile:
86 """Return the active risk profile, selected out-of-band only (F33).
87
88 Reads ``OC_CHAT_PROFILE`` from the process environment (case-insensitive,
89 whitespace-tolerant). Never consults a settings file, prompt, resume payload,
90 or model output. An unset value uses :data:`DEFAULT_PROFILE`; an explicitly
91 set but unrecognized value is logged and also falls to the default.
92 """
93 func = "resolve_risk_profile"
94 raw = os.environ.get(PROFILE_ENV_VAR, "").strip().lower()
95 if raw in VALID_PROFILES:
96 profile: RiskProfile = raw # type: ignore[assignment]
97 else:
98 if raw:
99 _dbg("OC_CHAT.CORE", func,
100 f"unrecognized {PROFILE_ENV_VAR}={raw!r}; falling to {DEFAULT_PROFILE!r}", 2)
101 profile = DEFAULT_PROFILE
102 _dbg("OC_CHAT.CORE", func, f"active risk profile: {profile}", 2)
103 return profile
104
105
106__all__ = ["RiskProfile", "VALID_PROFILES", "PROFILE_ENV_VAR", "DEFAULT_PROFILE", "resolve_risk_profile"]
RiskProfile resolve_risk_profile()