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
5 changes: 3 additions & 2 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ Landed so far:
|---|---|---|
| 0 | #69 | Nothing |
| 1 | #71 | Nothing |
| 2 | #73, and attempt keys for a claim and a completion in the pull request after #74 | An attempt key on the other operations; an uncertain external effect blocking reassignment; waiting on an outside event as a state; the record's remaining fields (`parent`, `scores`, `implements`, `reversibility`, `artifacts_touched`, `policy_context`) |
| 3 | the pull request after #73 | A suite that gates a version of a definition; the data scope enforced; a definition required by default, once the system's own filings name one; tools offered a few at boot and the rest by search |
| 2 | #73, and attempt keys for a claim and a completion in #77 | An attempt key on the other operations; an uncertain external effect blocking reassignment; waiting on an outside event as a state; the record's remaining fields (`parent`, `scores`, `implements`, `reversibility`, `artifacts_touched`, `policy_context`) |
| 3 | #74 | A suite that gates a version of a definition; the data scope enforced; a definition required by default, once the system's own filings name one; tools offered a few at boot and the rest by search |
| 6 | #78 | `check_policy` selecting rules by the action; the unused-rule check over one review period; the approver of an exception checked against the layer breached; a new version of a rule keeping its place under the ceiling; a decision that survives an enforced module that fails; the author and the date of an imported rule not trusted; a session that reloads the policy when a rule changes; a verified rule that stays in force while its next version waits for review; a denial that names no policy counted for the module that made it, in the rates and the lint |

What stays unbuilt after these, and stays on README's list: federation across domains, authentication of people, logprob-based drift, and accelerator time, which only a gateway can count. So do the items on that list that need another machine or an outside service more than they need code: the limits on re-enactment, harness settings and decomposition templates in the platform domain, bytes stored and resource limits on the sandbox, the authoring of agent definitions, the search tool against its live provider, a harness under Seatbelt on a Mac, and the Gofannon bridge against a live deployment, whose agents make model calls that the egress does not meter.

Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,14 @@ two weeks. It is also the reason that none of it is the production shape.
- **Approval events.** An approval is the completion of a promotion task. It has no
scope and no expiry, and the perimeter does not check it when an action runs. There is
no `declassify` and no guard.
- **The rule lifecycle.** A rule is enforced when a person verified it and it has a
module. A rule has no status, no owner who is a person, and no review date that the
policy uses. There is no ceiling on rules, no reset, and no `check_policy` tool.
- **The rule lifecycle, in part.** `check_policy` gives all the rules in force for a
domain. It does not select rules by the action. The lint reports a rule after its
review date and an unused rule, from the full decision log and not from one review
period. The approver of an exception is a name in the answer. `amem` does not check
that this person approves. A new version of a rule is the newest rule under the
ceiling. While the next version of a rule waits for its review, the rule is in no
decision. When the module of an enforced rule fails in a decision, that decision
fails. An imported rule keeps the author and the date of its entry.
- **Lineage, in part.** An entry does not record its session. `amem taint` compares the
time of a read with the time of a later write, and it goes forward only. Reads and
sends are not in the event log. A write that the scan refuses is a file under `state/`,
Expand Down
27 changes: 25 additions & 2 deletions crates/index/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,29 @@ impl Index {
filter: &ReadFilter,
kind: Option<EntryType>,
limit: usize,
) -> Result<Vec<Hit>, Error> {
self.list_up_to(filter, kind, Some(limit))
}

/// Every entry of a kind within the filter, newest first. For a caller whose
/// answer is wrong when an entry is left out, as the policy is when a rule is.
///
/// # Errors
///
/// A database error, or a row the index did not write.
pub fn list_every(
&self,
filter: &ReadFilter,
kind: Option<EntryType>,
) -> Result<Vec<Hit>, Error> {
self.list_up_to(filter, kind, None)
}

fn list_up_to(
&self,
filter: &ReadFilter,
kind: Option<EntryType>,
limit: Option<usize>,
) -> Result<Vec<Hit>, Error> {
let (where_sql, mut values) = compile_filter(filter, 2);
let kind_sql = if kind.is_some() {
Expand All @@ -608,8 +631,8 @@ impl Index {
values.insert(0, kind.map_or_else(String::new, |k| k.to_string()).into());
let sql = format!(
"SELECT {HIT_COLUMNS} FROM entries e WHERE 1 = 1{kind_sql}{where_sql}
ORDER BY {NEWEST_FIRST} LIMIT {}",
limit.max(1)
ORDER BY {NEWEST_FIRST}{}",
limit.map_or_else(String::new, |n| format!(" LIMIT {}", n.max(1)))
);
let mut stmt = self.conn.prepare(&sql)?;
let rows = stmt.query_map(params_from_iter(values), |row| Ok(hit_from_row(row, 0.0)))?;
Expand Down
154 changes: 87 additions & 67 deletions crates/memory/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,45 @@ enum PolicyOp {
#[arg(long)]
file: bool,
},
/// The rules in force for an action in a domain, enforced and advisory.
Check {
/// What is about to be done, in a few words.
action: String,
#[arg(long)]
domain: Option<String>,
},
/// Ask for an exception to a rule, for one task, until an expiry. A person who did
/// not ask for it approves it with `amem approve`.
Except {
/// The policy id of the rule to breach.
breaches: String,
/// The one task the exception holds for.
#[arg(long)]
task: String,
/// What it covers, in words.
#[arg(long)]
scope: String,
/// When it lapses, RFC 3339.
#[arg(long)]
expiry: String,
/// Why it is needed.
#[arg(long)]
justification: String,
},
/// Propose an amendment to a rule: a task for the rule's owner, with the evidence.
Amend {
/// The policy id of the rule.
policy_id: String,
/// What happened, and the ids of the entries that show it.
#[arg(long)]
evidence: String,
},
/// Retire every rule of a domain that no person wrote or promoted, and keep the
/// rest. The owner's act.
Reset {
#[arg(long)]
domain: Option<String>,
},
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -4589,6 +4628,7 @@ async fn perimeter_cmd(m: &Memory, command: Command) -> Fallible {
Command::Policy {
op: PolicyOp::Lint { file },
} => policy_lint(m, file).await,
Command::Policy { op } => rule_cmd(m, op).await,
Command::Quarantine {
op: QuarantineOp::List,
} => json(
Expand Down Expand Up @@ -4648,7 +4688,11 @@ fn policy_list(m: &Memory) -> serde_json::Value {
serde_json::json!({
"id": e.meta.id, "entry": e.entry_id, "layer": e.meta.layer, "tier": e.tier,
"rule": e.meta.rule, "exception": e.meta.is_exception(),
"enforced": e.enforced(), "in_force": e.in_force(now),
"enforced": e.enforced() && p.state_of(e) == dm_policy::record::Status::Enforced,
"in_force": e.in_force(now),
"status": if e.meta.is_exception() { "exception" } else { p.state_of(e).as_str() },
"owner": e.meta.owner,
"review_by": e.meta.review_by,
"breaches": e.meta.breaches, "scope_task": e.meta.scope_task,
})
})
Expand All @@ -4659,79 +4703,55 @@ fn policy_list(m: &Memory) -> serde_json::Value {
})
}

/// Citations and overrides per policy, from the decision log: how often each rule
/// refused something, and how often an exception let something through.
fn policy_rates(root: &Path) -> serde_json::Value {
let text = std::fs::read_to_string(root.join("state/decisions.jsonl")).unwrap_or_default();
let mut cited: std::collections::BTreeMap<String, (u64, u64)> =
std::collections::BTreeMap::new();
for line in text.lines() {
let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
let allow = v["allow"].as_bool().unwrap_or(false);
for id in v["cites"]
.as_array()
.into_iter()
.flatten()
.filter_map(|c| c.as_str())
{
let row = cited.entry(id.to_string()).or_default();
if allow {
row.1 += 1;
} else {
row.0 += 1;
}
/// `amem policy check`, `except`, `amend` and `reset`: the rule lifecycle.
async fn rule_cmd(m: &Memory, op: PolicyOp) -> Fallible {
match op {
PolicyOp::Check { action, domain } => {
let rules = m.check_policy(&action, domain.as_deref()).await?;
json(&serde_json::json!({ "rules": rules }))
}
PolicyOp::Except {
breaches,
task,
scope,
expiry,
justification,
} => {
let ask = dm_server::rules::ExceptionRequest {
breaches,
task: Some(task),
scope,
expires: expiry,
justification,
};
json(&m.file_exception(ask).await?)
}
PolicyOp::Amend {
policy_id,
evidence,
} => {
let created = m.propose_amendment(&policy_id, &evidence).await?;
json(&serde_json::json!({
"task": created.task, "deduplicated": created.deduplicated,
}))
}
PolicyOp::Reset { domain } => json(&m.reset_rules(domain.as_deref()).await?),
PolicyOp::Show | PolicyOp::Eval { .. } | PolicyOp::List | PolicyOp::Lint { .. } => {
unreachable!("routed by perimeter_cmd")
}
}
serde_json::json!(cited
.into_iter()
.map(|(id, (refused, overridden))| serde_json::json!({"id": id, "refused": refused, "let_through": overridden}))
.collect::<Vec<_>>())
}

/// `amem policy lint`.
async fn policy_lint(m: &Memory, file: bool) -> Fallible {
let p = m.policy();
let mut findings = dm_policy::record::lint(p.entries(), time::OffsetDateTime::now_utc());
findings.extend(
p.refused()
.iter()
.map(|(id, why)| dm_policy::record::Finding {
policy: id.clone(),
kind: "module-refused".into(),
why: format!("{id}'s module would not load: {why}"),
}),
);
let mut filed = Vec::new();
if file {
let domain = m.resolve_domain(None)?;
for f in findings.iter().filter(|f| f.kind != "inert") {
let created = m
.file_own(dm_ledger::TaskSpec {
domain: domain.clone(),
title: format!("Policy bug, {}: {}", f.kind, f.policy),
body: format!(
"{}\n\nAcross layers the higher wins and the lower is fixed: amend or supersede {} so it agrees, or file an exception with a scope, an expiry and an approver. Within a layer, resolve from the stated intents and record the trade-off.",
f.why, f.policy
),
requirements: vec!["human".into()],
depends_on: Vec::new(),
origin: Some(format!("policy:{}", f.kind)),
upstream_event: Some(f.policy.clone()),
created_by: String::new(),
classification: dm_core::Label::Internal,
priority: 4,
..dm_ledger::TaskSpec::default()
})
.await?;
if !created.deduplicated {
filed.push(created.task.id);
}
}
}
let findings = m.policy_findings();
let filed = if file {
m.file_policy_findings(&findings).await?
} else {
Vec::new()
};
json(&serde_json::json!({
"findings": findings, "rates": policy_rates(m.root()), "filed": filed,
"findings": findings, "rates": m.policy_rates(), "filed": filed,
}))
}

Expand Down
12 changes: 9 additions & 3 deletions crates/policy/rego/amem.rego
Original file line number Diff line number Diff line change
Expand Up @@ -368,12 +368,15 @@ base_decision := erase_decision if input.action == "erase"
# `deny contains {"policy": <id>, "reason": <why>}` for a request. A denial wins over
# the decision above unless an exception in force covers it: an exception entry,
# verified and unexpired, that breaches that policy and is scoped to the task this
# session holds. An L0 rule takes no exception. Every decision says which policies it
# cites, the denials or the exceptions used: the paper's citation and override rates.
# session holds. A rule of the L0 core takes no exception. Every decision says which
# policies it cites, the denials or the exceptions used: the paper's citation and
# override rates. Only the modules of enforced rules are loaded here. The module of an
# advisory rule is evaluated apart, so nothing it says can refuse, whatever rule it
# names.
core_policy(pid) if {
some p in data.policies
p.id == pid
p.layer == 0
p.core == true
}

excepted(pid) if {
Expand All @@ -393,6 +396,8 @@ rule_denials contains d if {
not excepted(d.policy)
}

denial_list := [d | some d in rule_denials]

exceptions_used contains x if {
some d in all_denials
excepted(d.policy)
Expand All @@ -418,6 +423,7 @@ decision := {
"review": "human",
"reason": concat("; ", denied_reasons),
"cites": denied_list,
"denials": denial_list,
} if {
count(rule_denials) > 0
}
Loading
Loading