Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/black-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,6 @@ test/test_frontend_edition_build.py
test/test_frontmatter.py
test/test_gateway_appkit_endpoints.py
test/test_gitlab_client_coverage.py
test/test_governance_policy_viewer.py
test/test_handler_cron_list.py
test/test_handlers_channel_clear_context.py
test/test_handlers_channel_presets.py
Expand Down
17 changes: 16 additions & 1 deletion docs/system-specs/modules/governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ evaluator edits.
> mirror), **not** this re-triage. See `platform-context.md` for the design
> record.

## Enterprise built-in baseline (unnamed scopes)

In an `oss` build a scope the policy does not name is ungoverned and permitted (`_PERMIT_NOT_GOVERNED`). In an enterprise build (`edition.is_enterprise()`) it is answered by the built-in baseline policy instead: each scope's minimal set, or off when it has none ([RFC D3](../../request-for-change/rfc-crew-enterprise-edition.md), appendix "Minimal set"). The table is data in `platform/governance_parts/baseline.py` (`BASELINE_CONTROLS`), parsed by the same per-control parser a remote policy uses, so it obeys the catalog's `always_permitted` floor.

- **Where it applies.** `evaluator._policy_level` fills the POLICY level only, in `resolve`. `_query_level` still permits a `None` control, because it also answers the profile level: a profile that does not name a scope must not impose the baseline again beneath a remote widening. For the same reason no baseline is added when a `Profile` sits in the ceiling slot (`governance_profiles` asks `may_skip_gate` about one profile alone) or when `gate_decision`'s alias-group pass asks the profile layer alone (`resolve(..., baseline=False)`). `policy_level_control` exposes the same lookup to readers that inspect a ceiling directly: `may_skip_gate` and `_ceiling_mentions_mcp_server` use it, so an auto-approve grant cannot skip the gate for an unnamed scope, the policy viewer (`GET /api/governance/policy`) reports the baseline instead of "ungoverned", and the member-dashboard script grant (`member_dashboard._egress_decision`) reads an unnamed `network.egress` as the baseline's off. A `folders.*` query finds the policy's `filesystem.*` rule before the baseline answers, because the parser stores it under the canonical scope.
- **Who may widen.** The remote policy, by naming the scope. A profile only narrows.
- **Not gated.** `BASELINE_UNGATED` lists the two ordinals (`approval_mode`, `sandbox.min_level`), which keep Crew's own default. A scope in neither table (one registered at run time) is denied.
- **Minimal sets that differ from the appendix wording.** `approval_modes` is `normal`, `trust`, `trust_reads` (the catalog floor; only `yolo` is off). `filesystem.read` is Crew's fixed default workspace root (`config.paths._default_workspace_base()` + `_WORKSPACE_DIR_NAME`), never the configured one, because the configured root is a local source. `tools` is off, not read-only tools: `classify_tool_title` also asks `commands` about every bare tool name, so a read-only set could never pass while `commands` is off. Read-only tools wait on the tools/commands classifier fix, a separate item. `updates.source` is not a catalog scope and has no baseline.
- **Local policy tiers.** `compose_tier_ladder` first fills the authority tier's unnamed scopes from the baseline (`tiers._with_enterprise_baseline`), so the tighten-only `_intersect_ceilings` composes a local tier (`env`, `home`, `bundled`) WITH the baseline: a local tier can narrow it and never supply a scope's whole answer. With no central document the authority is itself local, so it is intersected with the baseline scope by scope rather than filled; only a central authority's named scopes replace the baseline. The installed ceiling therefore names every gated scope in an enterprise build, and `kirocrew policy show` lists the baseline rows beside the document's own.
- **Not covered here.** The `ceiling is None and profile is None` early returns in `hook_runtime/governance_gate.py`, `mcp_gateway/app_call.py` and `governance_profiles.governance_permits` still permit before `resolve` runs; an enterprise host with no policy at all is the enterprise gate's to refuse (RFC Migration plan phase 2).

## Loading + precedence

`load_security_policy()` precedence, highest first. The centrally distributed
Expand Down Expand Up @@ -3702,8 +3713,10 @@ carve-out stay as code. It expects `CONTRACT_VERSION == 1` (pinned pre-launch).
`compose_tier_ladder` with its refresh entry point `compose_installed_ceiling`
(+ `_TierProcessState`, the per-process tier state: the remembered bundled tier
and the two once-per-process latches), and `load_security_policy`.
- `platform/governance_parts/baseline.py` — the enterprise built-in baseline
(`BASELINE_CONTROLS`, `BASELINE_UNGATED`); data only.
- `platform/governance_parts/evaluator.py` — `resolve`, `resolve_ordinal`, `gate_decision`,
`resolve_pinned_commands` + `COMMANDS_SCOPE` force-pins.
`resolve_pinned_commands` + `COMMANDS_SCOPE` force-pins, `policy_level_control`.
- `platform/admission.py` — `canonical_signing_bytes` / `hmac_signature` (shared
by both trust roots), `ed25519_verify` (policy issuers only),
`require_policy_signature` / `trust_keys` / `trust_public_keys`, and
Expand Down Expand Up @@ -3769,6 +3782,8 @@ extensibility), `test_governance_policy_signatures.py` (the `identity.signature`
states, the opt-in fail-closed gate, and the `policy show` provenance reporting),
`test_governance_policy_decisions.py` (an unnamed capability is ungoverned, `policy
validate` names the ungoverned rows, and an alias group is one gate question),
`test_governance_enterprise_baseline.py` (an unnamed scope answers the enterprise
baseline, every catalog scope has a baseline row, and `oss` is unchanged),
`test_governance_tier_ladder.py` (the tier ladder: that the central document
outranks every local one, that a lower tier can only tighten it, that the reserved
sandbox boot flags compose across tiers, that an env document beneath an authority
Expand Down
9 changes: 5 additions & 4 deletions src/kiro_crew/dashboard/handlers/member_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -1358,16 +1358,17 @@ def _egress_decision(owner_agent: str = "", session_key: str = "") -> _EgressDec
from kiro_crew.platform.governance import MODE_DENY, ScopedRuleset

try:
from kiro_crew.platform.governance import last_composed_ceiling
from kiro_crew.platform.governance import last_composed_ceiling, policy_level_control

ceiling = last_composed_ceiling()
controls = (ceiling.controls or {}) if ceiling is not None else {}
# The policy level as ``resolve`` enforces it: in an enterprise build an
# unnamed ``network.egress`` is the built-in baseline (off), not absent.
control = policy_level_control(ceiling, "network.egress")
# NO CEILING, OR NONE GOVERNING EGRESS, IS NOT AN ANSWER -- it only means the
# outer bound restricts nothing, and the profile below still decides. Returning
# True here was the hole: the standalone default is every install that
# configures no policy, and a profile can restrict egress in all of them.
if "network.egress" in controls:
control = controls["network.egress"]
if control is not None:
if not isinstance(control, ScopedRuleset):
return _EgressDecision(False, "policy", "ceiling_egress_control_unreadable")
# Only one shape leaves the agent unrestricted: deny-mode denying nothing.
Expand Down
1 change: 1 addition & 0 deletions src/kiro_crew/dashboard/handlers/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,7 @@ def _mutate(denied: dict) -> None:
ScopedRuleset,
_AndRuleset,
_compose_controls,
policy_level_control,
)
from kiro_crew.platform.governance_profiles import ( # noqa: F401
HOST_SESSION_KEY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
ScopedRuleset,
_AndRuleset,
_compose_controls,
policy_level_control,
)
from kiro_crew.platform.governance_profiles import (
HOST_SESSION_KEY,
Expand Down Expand Up @@ -262,7 +263,9 @@ def build_governance_policy_snapshot() -> dict:
# would be a permanently-ungoverned duplicate row.
if scope in _SCOPE_ALIASES:
continue
policy_control = ceiling.get(scope) if ceiling is not None else None
# The policy level as ``resolve`` enforces it: in an enterprise build an
# unnamed scope reports the built-in baseline, not "ungoverned".
policy_control = policy_level_control(ceiling, scope)
profile_control = profile.get(scope) if profile is not None else None

if policy_control is not None and profile_control is not None:
Expand Down
31 changes: 23 additions & 8 deletions src/kiro_crew/platform/governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
Tuple,
)

from kiro_crew import edition
from kiro_crew.config.paths import config_dir # noqa: F401 - re-export
from kiro_crew.mcp_provenance import is_marked
from kiro_crew.platform.admission import ( # noqa: F401 - re-export
Expand Down Expand Up @@ -87,7 +88,7 @@
)
from kiro_crew.platform.governance_parts.carriers import GovernanceCeiling, Profile
from kiro_crew.platform.governance_parts.catalog import ORDINAL, SCOPE_CATALOG
from kiro_crew.platform.governance_parts.evaluator import gate_decision
from kiro_crew.platform.governance_parts.evaluator import gate_decision, policy_level_control
from kiro_crew.platform.governance_parts.tiers import _POLICY_CACHE_LEAF

logger = logging.getLogger("kiro_crew.platform.governance")
Expand Down Expand Up @@ -186,9 +187,7 @@ def _ceiling_mentions_mcp_server(ceiling: Optional[GovernanceCeiling], server: s
test for the scope-level question. Do not "fix" this function to return True
on an empty ruleset: that would tax every server for a rule about one.
"""
if ceiling is None:
return False
ruleset = ceiling.get("mcp")
ruleset = policy_level_control(ceiling, "mcp")
if not isinstance(ruleset, RulesetLike):
return False
prefix = f"@{server}".casefold()
Expand Down Expand Up @@ -285,7 +284,9 @@ def may_skip_gate(ref: str, ceiling: Optional[GovernanceCeiling]) -> bool:
BUILTIN_TOOL_SCOPES.get(ref, ())
):
return False
if ceiling is None:
# An enterprise build has no ungoverned host: an unnamed scope is answered by
# the built-in baseline, read through ``policy_level_control`` below.
if ceiling is None and not edition.is_enterprise():
return True
try:
if ref.startswith("@"):
Expand All @@ -308,7 +309,7 @@ def may_skip_gate(ref: str, ceiling: Optional[GovernanceCeiling]) -> bool:
# gate, so if the ceiling has an opinion on ANY argument-derived scope,
# withhold and let the gate apply it against the real path/host.
arg_scopes = ("filesystem.read", "filesystem.write", "network.egress")
if any(_ruleset_has_an_opinion(ceiling.get(_s)) for _s in arg_scopes):
if any(_ruleset_has_an_opinion(policy_level_control(ceiling, _s)) for _s in arg_scopes):
return False
return True

Expand All @@ -319,10 +320,10 @@ def may_skip_gate(ref: str, ceiling: Optional[GovernanceCeiling]) -> bool:
# bypassed a `tools`-scope ceiling (e.g. tools.deny=["report"]). The
# capability scopes it DOES map to add path/host granularity on top.
scopes = ("tools",) + tuple(BUILTIN_TOOL_SCOPES.get(ref, ()))
if any(_ruleset_has_an_opinion(ceiling.get(scope)) for scope in scopes):
if any(_ruleset_has_an_opinion(policy_level_control(ceiling, scope)) for scope in scopes):
return False
capability = BUILTIN_TOOL_CAPABILITIES.get(ref)
return not (capability and _capability_restricts(ceiling.get(capability)))
return not (capability and _capability_restricts(policy_level_control(ceiling, capability)))
except Exception: # noqa: BLE001 — see the fail-closed note above
logger.warning("ceiling probe failed for %r; not auto-approving", ref, exc_info=True)
return False
Expand Down Expand Up @@ -1062,6 +1063,7 @@ def assert_governance_paths_protected() -> None:
# ``tool_calls`` -- the governed (scope, item) pairs one tool call touches
# ``archetypes`` -- the decision record, the four archetypes and their composition
# ``catalog`` -- which scopes exist, and which archetype each one is
# ``baseline`` -- the enterprise built-in baseline: each scope's minimal set
# ``carriers`` -- ``GovernanceCeiling``, ``Profile`` and their policy-only blocks
# ``parsing`` -- a policy or profile document into its carrier
# ``signature`` -- proving a policy's ``identity.signature``
Expand All @@ -1073,6 +1075,7 @@ def assert_governance_paths_protected() -> None:
"kiro_crew.platform.governance_parts.tool_calls",
"kiro_crew.platform.governance_parts.archetypes",
"kiro_crew.platform.governance_parts.catalog",
"kiro_crew.platform.governance_parts.baseline",
"kiro_crew.platform.governance_parts.carriers",
"kiro_crew.platform.governance_parts.parsing",
"kiro_crew.platform.governance_parts.signature",
Expand All @@ -1099,6 +1102,13 @@ def assert_governance_paths_protected() -> None:
_reject_unknown_keys,
_str_tuple,
)
from kiro_crew.platform.governance_parts.baseline import ( # noqa: F401
_CAPABILITY_OFF,
_DEFAULT_WORKSPACE_ROOTS,
_OFF,
BASELINE_CONTROLS,
BASELINE_UNGATED,
)
from kiro_crew.platform.governance_parts.carriers import ( # noqa: F401
_AGENTCORE_POLICY_ONLY,
_AGENTCORE_POSTURES,
Expand Down Expand Up @@ -1144,7 +1154,10 @@ def assert_governance_paths_protected() -> None:
register_scope,
)
from kiro_crew.platform.governance_parts.evaluator import ( # noqa: F401
_BASELINE_REASON,
_DENY_UNLISTED,
_PERMIT_NOT_GOVERNED,
_policy_level,
_query_level,
resolve,
resolve_ordinal,
Expand Down Expand Up @@ -1177,6 +1190,7 @@ def assert_governance_paths_protected() -> None:
_parse_control,
_parse_controls,
_read_json_file,
baseline_controls,
parse_policy,
parse_profile,
)
Expand Down Expand Up @@ -1211,6 +1225,7 @@ def assert_governance_paths_protected() -> None:
_TierProcessState,
_warn_env_beneath_central_once,
_warn_home_unreadable_beneath_authority_once,
_with_enterprise_baseline,
compose_installed_ceiling,
compose_tier_ladder,
last_composed_ceiling,
Expand Down
6 changes: 4 additions & 2 deletions src/kiro_crew/platform/governance_parts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
that composes two controls of one archetype;
* ``catalog`` -- which scopes exist: ``SCOPE_CATALOG`` with its archetype ids,
``register_scope`` and the scope aliases;
* ``baseline`` -- the enterprise built-in baseline: the minimal set an unnamed scope
answers in an enterprise build (data only);
* ``carriers`` -- what a parsed policy and profile hold: ``GovernanceCeiling`` and
``Profile`` with their boot, update, distribution and agentcore blocks, and profile
inheritance;
Expand All @@ -21,8 +23,8 @@
refuses an unproven one;
* ``tiers`` -- the tier ladder: where each local policy is read, the per-process tier
state, and the fold into the installed ceiling;
* ``evaluator`` -- answering a query: ``resolve``, ``resolve_ordinal``, ``gate_decision``
and the command force-pins.
* ``evaluator`` -- answering a query: ``resolve``, ``resolve_ordinal``, ``gate_decision``,
the command force-pins, and the policy level's control with the baseline filled in.

The facade keeps the auto-approve surface it decides itself -- ``may_skip_gate`` with
the builtin-tool scope maps, the auto-approve strip, the confirm-tools gate,
Expand Down
89 changes: 89 additions & 0 deletions src/kiro_crew/platform/governance_parts/baseline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""The enterprise built-in baseline policy: each governed scope's minimal set.

Data only. In an enterprise build a scope the remote policy does not name is
answered from this table instead of being permitted (RFC
``docs/request-for-change/rfc-crew-enterprise-edition.md``, decision D3 and the
appendix "Minimal set" column). The remote policy may widen past it; nothing
local can, because a profile only ever narrows the policy level it sits under.

``BASELINE_CONTROLS`` maps a ``SCOPE_CATALOG`` scope to the raw control a policy
document would carry for it, parsed by the same per-control parser a remote
policy goes through. ``BASELINE_UNGATED`` lists the scopes that keep Crew's own
default, with the reason. Every catalog scope (aliases aside) is in exactly one
of the two; ``test_governance_enterprise_baseline.py`` fails on a new scope that
is in neither.

Two rows differ from the appendix. ``tools`` is off, not read-only tools: read-only
tools wait on the tools/commands classifier fix (a separate item), because the gate
also asks ``commands`` about every bare tool name and ``commands`` is off.
``approval_modes`` carries the catalog's undeniable floor (``normal``, ``trust``,
``trust_reads``), so only ``yolo`` is off.
"""

from __future__ import annotations

import os
from typing import Dict, Mapping

_OFF: Mapping[str, object] = {"mode": "allow", "allow": []}
_CAPABILITY_OFF: Mapping[str, object] = {"enabled": False}

# Crew's default workspace root: ``config.paths._default_workspace_base()`` joined
# with ``_WORKSPACE_DIR_NAME``. The base is ``~/workplace`` everywhere, or
# ``/Volumes/workplace`` on macOS when that volume exists. Fixed here on purpose:
# the configured root comes from ``KIROCREW_WORKSPACE``, the saved
# ``workspace_dir`` file or ``workspaces[].dir`` in config, all local sources
# D3 says may not widen a control. ``~`` is expanded at match time.
_DEFAULT_WORKSPACE_ROOTS = (
os.path.join("~", "workplace", "kirocrew-workspace"),
os.path.join("/Volumes", "workplace", "kirocrew-workspace"),
)

BASELINE_CONTROLS: Dict[str, Mapping[str, object]] = {
# Off, although the appendix's minimal set is read-only tools. The gate asks
# ``commands`` about every bare tool name too (``classify_tool_title``), so
# with ``commands`` off a read-only set here could never pass. Read-only tools
# wait on the tools/commands classifier fix, a separate item.
"tools": _OFF,
# The catalog's ``always_permitted`` floor: only ``yolo`` is off.
"approval_modes": {"mode": "allow", "allow": ["normal", "trust", "trust_reads"]},
"mcp": {"mode": "allow", "allow": ["@kirocrew-core"]},
"apps": _OFF,
"commands": _OFF,
"filesystem.read": {
"mode": "allow",
"allow": [p for root in _DEFAULT_WORKSPACE_ROOTS for p in (root, os.path.join(root, "**"))],
},
"filesystem.write": _OFF,
"network.egress": _OFF,
"channels": {"members": _OFF},
"yolo_duration": _OFF,
# ``acp_backends.POLICY_ID_KIRO``.
"agent_backend": {"mode": "allow", "allow": ["kiro"]},
"capabilities.spawn": _CAPABILITY_OFF,
"capabilities.memory_writes": _CAPABILITY_OFF,
"capabilities.browse": _CAPABILITY_OFF,
"capabilities.script_hooks": _CAPABILITY_OFF,
"capabilities.cron": _CAPABILITY_OFF,
"capabilities.messaging": _CAPABILITY_OFF,
"capabilities.agentcore": _CAPABILITY_OFF,
# ``publish_provider.DEFAULT_PROVIDER``.
"capabilities.publish": {
"enabled": True,
"scopes": {"destinations": {"mode": "allow", "allow": ["default"]}},
},
"capabilities.theme_persona": _CAPABILITY_OFF,
"capabilities.theme_install": _CAPABILITY_OFF,
"capabilities.telemetry": _CAPABILITY_OFF,
"capabilities.tailnet_origin": _CAPABILITY_OFF,
"capabilities.mobile_connect": _CAPABILITY_OFF,
"capabilities.social_share": _CAPABILITY_OFF,
"capabilities.feature_videos_download": _CAPABILITY_OFF,
"capabilities.decisions": _CAPABILITY_OFF,
"capabilities.decisions_local": _CAPABILITY_OFF,
}

BASELINE_UNGATED: Dict[str, str] = {
"approval_mode": "ordinal; never item-queried, so Crew's own approval default applies",
"sandbox.min_level": "ordinal; the RFC appendix keeps Crew's current sandbox default",
}
4 changes: 4 additions & 0 deletions src/kiro_crew/platform/governance_parts/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ class ScopeSpec:
# validate`` reports a partially-governed ``capabilities`` block so the gap is
# visible instead of implied.
#
# An ENTERPRISE build is the exception: there an unnamed scope is answered by the
# built-in baseline (``baseline.py``, RFC D3), so a new row here also needs a row
# in ``BASELINE_CONTROLS`` or ``BASELINE_UNGATED``; a test fails until it has one.
#
# The absent-key semantics below are verified by executing parse_policy + resolve,
# not inferred from the comments.

Expand Down
Loading
Loading