Skip to content
Open
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
15 changes: 13 additions & 2 deletions crates/fractal-kernel/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,8 @@ impl LimitsConfig {
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct ProgramsConfig {
/// `audit` lets a write outside a run's authority proceed and records it; `enforce` refuses it.
/// `enforce` (the default) refuses a write outside a run's authority; `audit` lets it proceed
/// and records it.
pub authority: RunAuthorityMode,
}

Expand All @@ -480,8 +481,8 @@ impl ProgramsConfig {
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RunAuthorityMode {
#[default]
Audit,
#[default]
Enforce,
}

Expand Down Expand Up @@ -1414,6 +1415,16 @@ fn machine_host_name() -> String {

#[cfg(test)]
mod tests {
#[test]
fn run_authority_is_enforced_unless_the_config_asks_for_audit() {
use super::RunAuthorityMode;
let absent: super::Config = toml::from_str("").unwrap();
assert_eq!(absent.programs.authority, RunAuthorityMode::Enforce);
let audit: super::Config = toml::from_str("[programs]\nauthority = \"audit\"\n").unwrap();
assert_eq!(audit.programs.authority, RunAuthorityMode::Audit);
assert!(toml::from_str::<super::Config>("[programs]\nauthority = \"off\"\n").is_err());
}

#[test]
fn usage_writer_configuration_is_explicit_and_agent_only() {
let absent: super::Config = toml::from_str("").unwrap();
Expand Down
7 changes: 4 additions & 3 deletions crates/fractal-store/src/store/run_authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
//! person, agent or daemon starter holds everything in free mode except what the rules restrict,
//! so each write is also decided by the rules for that starter.
//!
//! A write outside the authority is audited (`rule.audited` on `rule/run-authority`) or refused
//! (`run-authority-denied`), as the daemon's `[programs] authority` says.
//! A write outside the authority is refused (`run-authority-denied`), or audited (`rule.audited`
//! on `rule/run-authority`) when the daemon's `[programs] authority` says `audit`.
use super::*;
use crate::config::RunAuthorityMode;
use crate::model::{AuthorityGrant, AuthorityVerb};
Expand All @@ -20,7 +20,8 @@ pub const AUDIT_RULE: &str = "run-authority";
/// Run starters nest at most this deep; a deeper chain grants nothing.
const MAX_STARTERS: usize = 32;

static ENFORCE: AtomicBool = AtomicBool::new(false);
/// Enforced until the daemon's config says otherwise, as `[programs] authority` defaults.
static ENFORCE: AtomicBool = AtomicBool::new(true);

/// Set what a write outside a run's authority does, from the daemon's `[programs] authority`.
pub fn set_run_authority_mode(mode: RunAuthorityMode) {
Expand Down
5 changes: 3 additions & 2 deletions crates/fractal-store/src/store/run_authority_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ fn a_run_writes_only_within_its_authority_intersected_with_its_starters() {
#[test]
fn writers_other_than_runs_are_untouched() {
let _mode = hold_mode();
set_run_authority_mode(RunAuthorityMode::Enforce);
// Enforced by default, before any config sets the mode.
assert_eq!(run_authority_mode(), RunAuthorityMode::Enforce);
let store = missions();
write(&store, "person/operator", "custom/other/person").unwrap();
store.append_claim(&ClaimInput {
Expand All @@ -226,5 +227,5 @@ fn writers_other_than_runs_are_untouched() {
expected_subject: None,
idempotency_key: None,
}).unwrap();
set_run_authority_mode(RunAuthorityMode::Audit);
assert_eq!(run_authority_mode(), RunAuthorityMode::Enforce);
}
7 changes: 4 additions & 3 deletions docs/site/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,12 @@ daemon's `[programs]` table says what a write outside it does:

```toml
[programs]
authority = "audit" # the default: the write proceeds and `rule.audited` records it on `rule/run-authority`
# authority = "enforce" # the write is refused with `run-authority-denied`
authority = "enforce" # the default: the write is refused with `run-authority-denied`
# authority = "audit" # the write proceeds and `rule.audited` records it on `rule/run-authority`
```

Read the audit log with `fractal rules audit run-authority` before you enforce. The setting is read
Audit lets you discover the grants a new program needs: read them with
`fractal rules audit run-authority`, declare them, then return to enforce. The setting is read
when the daemon starts.

## Definitions do not start work
Expand Down
6 changes: 3 additions & 3 deletions docs/st3/mission-graph-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,9 @@ generation with a predecessor needs `revise ID`, as does everything a revision w
it changes, each on that run's mission; any other claim needs `write SUBJECT`. A run always
journals `program.step` on its own step runs. `cancel` is reserved and grants nothing yet: setting
another run's outcome, which is how a run is cancelled, takes a person or agent actor. A write
outside the effective authority is recorded as `rule.audited` on `rule/run-authority` and
proceeds, or is refused with `run-authority-denied`, as the daemon's `[programs] authority` says
(`audit` by default).
outside the effective authority is refused with `run-authority-denied`, or, when the daemon's
`[programs] authority` is `audit`, recorded as `rule.audited` on `rule/run-authority` and let
through (`enforce` by default).

A request whose caller's ancestry carries a live run credential
(`ST_RUN_CREDENTIAL`, one per program attempt) is bound to `mission-run/RUN`: like a bound agent, it
Expand Down
11 changes: 11 additions & 0 deletions release-notes/mission-run-authority-enforced.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 1,
"summary": "Mission run authority is enforced by default: a claim a program writes as `mission-run/RUN` outside its declared authority, intersected with its starter's, is refused with `run-authority-denied`; `[programs] authority = \"audit\"` lets it through and records it instead",
"replay": {"status": "none", "detail": "No claim, projection or replay change."},
"schema": {"status": "none", "detail": "No table, index or migration change."},
"rules": {"status": "changed", "detail": "The default of `[programs] authority` changes from `audit` to `enforce`, both in the config and in the store before the daemon reads its config. Writers that are not mission runs are untouched."},
"compatibility": {"status": "changed", "detail": "A program whose mission does not declare the authority its effects use now has those writes refused. Programs land in production only from the next milestone; a daemon that needs to discover grants first sets `[programs] authority = \"audit\"`."},
"downtime": {"status": "none", "detail": "The mode is read when the daemon starts; changing it needs a restart."},
"manual": {"status": "none", "detail": "No operator action is required for existing work, since nothing wrote as a run before program dispatch carried run credentials."},
"recovery": {"status": "none", "detail": "Set `[programs] authority = \"audit\"` and restart to let refused writes proceed while their grants are declared."}
}
Loading