Module: KairosMcp::Daemon::DaemonPolicy
- Defined in:
- lib/kairos_mcp/daemon/daemon_policy.rb
Overview
DaemonPolicy — Default-deny authorization policy for daemon-mode execution.
Design (v0.2 §3.1.x / CF-4):
When the Kairos daemon runs in the background, it must not be able to
silently modify L0 (framework) or L1 (knowledge) state, mint/burn
tokens, or administer service grants. Those operations require a live
human owner session. Only L2 (context / workspace) operations are
permitted by default — L2 is the daemon's natural scratch space.
Mechanism:
Safety exposes a class-level policy registry (Safety.register_policy).
Each capability method (can_modify_l0?, can_manage_tokens?, …) looks
up a block keyed by its name and calls it with the current_user hash.
When no policy is registered, most capabilities default-allow — we
therefore register explicit deny blocks for the daemon user and set
a synthetic `{ role: 'daemon' }` user context so the blocks fire.
Escape hatch:
A command explicitly run by the human owner (e.g. an interactive MCP
session attached to the daemon) can still use a fresh Safety instance
without the daemon user context; that path is unaffected.
Constant Summary collapse
- DAEMON_USER =
Synthetic user context used when the daemon acts on its own behalf.
{ user: 'kairos_daemon', role: 'daemon' }.freeze
- DENIED_CAPABILITIES =
Capability keys this module controls.
%i[ can_modify_l0 can_modify_l1 can_manage_tokens can_manage_grants ].freeze
- ALLOWED_CAPABILITIES =
%i[ can_modify_l2 ].freeze
Class Method Summary collapse
-
.apply!(safety = nil) ⇒ KairosMcp::Safety?
Register deny policies on the Safety class and stamp the given Safety instance with the daemon user context so the policies fire.
-
.daemon_user?(user) ⇒ Boolean
Is the user context a daemon-owned one? Accepts both symbol and string key hashes because different callers (HTTP auth, internal stamping) produce different shapes.
-
.remove! ⇒ Object
Remove all policies this module registers and restore prior ones.
Class Method Details
.apply!(safety = nil) ⇒ KairosMcp::Safety?
Register deny policies on the Safety class and stamp the given Safety instance with the daemon user context so the policies fire.
Codex-R1 fix: policies compose with existing RBAC by saving prior policies and delegating for non-daemon users. This prevents overwriting owner/member restrictions with the daemon’s blanket rules.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/kairos_mcp/daemon/daemon_policy.rb', line 53 def self.apply!(safety = nil) @prior_policies = {} DENIED_CAPABILITIES.each do |cap| @prior_policies[cap] = KairosMcp::Safety.policy_for(cap) prior = @prior_policies[cap] KairosMcp::Safety.register_policy(cap) do |user| if daemon_user?(user) false # daemon always denied elsif prior prior.call(user) # delegate to existing RBAC else true # no prior policy → default allow (STDIO mode) end end end ALLOWED_CAPABILITIES.each do |cap| @prior_policies[cap] = KairosMcp::Safety.policy_for(cap) prior = @prior_policies[cap] KairosMcp::Safety.register_policy(cap) do |user| if daemon_user?(user) true # daemon allowed for L2 elsif prior prior.call(user) # delegate to existing RBAC else true end end end safety&.set_user(DAEMON_USER.dup) safety end |
.daemon_user?(user) ⇒ Boolean
Is the user context a daemon-owned one? Accepts both symbol and string key hashes because different callers (HTTP auth, internal stamping) produce different shapes.
105 106 107 108 109 |
# File 'lib/kairos_mcp/daemon/daemon_policy.rb', line 105 def self.daemon_user?(user) return false unless user.is_a?(Hash) role = user[:role] || user['role'] role.to_s == 'daemon' end |
.remove! ⇒ Object
Remove all policies this module registers and restore prior ones. Intended for tests and for clean shutdown.
90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/kairos_mcp/daemon/daemon_policy.rb', line 90 def self.remove! (DENIED_CAPABILITIES + ALLOWED_CAPABILITIES).each do |cap| prior = @prior_policies&.dig(cap) if prior KairosMcp::Safety.register_policy(cap, &prior) else KairosMcp::Safety.unregister_policy(cap) end end @prior_policies = nil end |