PlatformCoordination Signals
Swarm
Delegation Hygiene
Three structural predicates per delegation edge, decayed and averaged into one score, weighted 0.15 into the local scorecard.
Scoring after the fact, not enforcement at issuance
enforce_tier_scope. This is the read side: a deterministic function over capability-lineage rows that already exist, answering how much narrower each delegation an agent issued actually was. Reputation Scoring owns the eight-metric scorecard as a whole; this page owns one of the eight and the two gates that read it.Three questions per edge
A multi-agent run leaves behind a tree of capabilities: one root the operator issued, and every child an agent minted from it to hand a narrower authority to something it spawned. The tree is already persisted, row by row, in the capability_lineage table. Delegation hygiene is the deterministic score over that tree, and it asks exactly three questions of each parent-child pair:
- Did the child’s scope narrow?
scope_reduced(parent, child) - Does the child expire before the parent?
child.expires_at < parent.expires_at - Did a spend or invocation limit drop?
budget_reduced(parent, child)
Each answer is a boolean mapped through bool_to_score to 1.0 or 0.0, weighted by exponential time decay on the child’s issued_at, and averaged across every edge the subject issued. The three rates are then averaged into one score. There is no fourth question and no partial credit inside a question: an edge that narrows a scope from ten tools to one scores the same 1.0 on the scope leg as an edge that drops a single operation.
The metric is agent-generic. It does not know or care whether the child capability went to a sub-agent, a worker actor, or a one-off delegate. What it requires is a lineage row whose issuer_key is the subject and whose parent_capability_id is set. The Swarm Authority bundle is the object that makes those rows plentiful; the metric would run identically over a tree of one.
The whole implementation is eight private functions and one public struct, inside chio-reputation, a crate whose own header states it depends on no kernel and touches no storage. Its modules are stitched together with include! rather than mod, so model.rs, score.rs, compare.rs, and issuance.rs are one flat namespace at compile time. That is why compute_delegation_hygiene lives in compare.rs while its predicates live in issuance.rs and neither file imports the other.
Five fields, three of them rates
DelegationHygieneMetrics is one of eight metric structs on LocalReputationScorecard. It carries five fields: the composed score, an observation count, and the three per-question rates it was composed from.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DelegationHygieneMetrics {
pub score: MetricValue,
pub delegations_observed: usize,
pub scope_reduction_rate: MetricValue,
pub ttl_reduction_rate: MetricValue,
pub budget_reduction_rate: MetricValue,
}| Field | Type | Meaning |
|---|---|---|
score | MetricValue | The unweighted mean of the three rates, clamped to [0.0, 1.0] by MetricValue::known. This is the only field any downstream gate reads. |
delegations_observed | usize | Edges that scored, not edges that exist. It is scope_signals.len(), so a child whose parent capability is absent from the corpus is not counted anywhere. |
scope_reduction_rate | MetricValue | Decay-weighted share of edges where scope_reduced returned true. |
ttl_reduction_rate | MetricValue | Decay-weighted share of edges where the child’s absolute expires_at is strictly below the parent’s. Not a duration comparison. |
budget_reduction_rate | MetricValue | Decay-weighted share of edges where budget_reduced returned true. |
MetricValue is a two-variant enum, Known(f64) or Unknown, adjacently tagged in JSON as {"state":"known","value":0.5} and {"state":"unknown"}. The distinction is the whole design: unknown means no edge was scoreable, and it is not the same as a score of zero. Everything the metric does downstream turns on which of the two it reports.
LocalReputationScorecard carries no rename_all attribute, so these field names serialize verbatim in snake case, even where the enclosing envelope is camel case. In a chio --format json reputation local payload the outer fields are effectiveScore and probationaryStatus, while inside scorecard the keys are delegation_hygiene, scope_reduction_rate, and the rest. Both spellings appear in one document.
Which delegations are visible at all
Two filters decide the edge set, and both run before any predicate fires. compute_local_scorecard selects the candidate children, then compute_delegation_hygiene resolves each child’s parent by id against a map of the entire corpus.
let delegations_issued: Vec<&CapabilityLineageRecord> = corpus
.capabilities
.iter()
.filter(|record| record.issuer_key == subject_key && record.parent_capability_id.is_some())
.collect();for child in delegations {
let Some(parent_id) = child.parent_capability_id.as_deref() else {
continue;
};
let Some(parent) = capability_map.get(parent_id) else {
continue;
};
let weight = decay_weight(now, child.issued_at, config.temporal_decay_half_life_days);
scope_signals.push((
bool_to_score(scope_reduced(&parent.scope, &child.scope)),
weight,
));
ttl_signals.push((bool_to_score(child.expires_at < parent.expires_at), weight));
budget_signals.push((
bool_to_score(budget_reduced(&parent.scope, &child.scope)),
weight,
));
}The subject is the issuer, not the holder. An agent is scored on what it handed out, never on what it was handed. The corpus builder in the control plane unions two lineage queries for exactly this reason: list_capability_snapshots(Some(subject_key), None) pulls the capabilities the subject holds, and list_capability_snapshots(None, Some(subject_key)) pulls the ones it issued. The first query is what makes the parent resolvable, because a delegation’s parent is a capability whose subject is the delegator.
A child whose parent is not in the corpus is dropped by the continue above, silently, and never appears in delegations_observed. Because the count is scope_signals.len(), the number reported is edges scored, and it can be smaller than the number of delegation rows the agent actually wrote. The degenerate case is stated explicitly in the code: when delegations is non-empty but no parent resolves, the function returns the same all-unknown struct as an agent that never delegated at all, with delegations_observed: 0.
Where the parent link comes from
record_capability_snapshot persists the parent_capability_id it was handed rather than reading the token. The kernel supplies it. record_observed_capability_snapshot derives that argument from delegation_chain.last() and runs both from ChioKernel::issue_capability and from the evaluation path on every governed call, so a delegated token presented to a kernel that has a receipt store lands in capability_lineage with its parent set. Two other writers pass the field explicitly. The control plane’s own issuance wrapper in chio-control-plane/src/issuance/authority.rs passes None, correct rather than lossy because the capabilities it mints are roots with an empty chain. POST /v1/lineage on the trust service accepts an optional parentCapabilityId on its RecordCapabilitySnapshotRequest body. The condition that actually silences this metric is a kernel with no receipt store configured: with_receipt_store returns Ok(None) without writing, no lineage row exists, and delegation_hygiene reports unknown no matter how carefully the agents attenuate.The three predicates
All three run against ChioScope values decoded from the stored grants_json. Two of the three first resolve a per-grant parent through parent_grant_for, which is where the sharp edges are.
Resolving a grant’s parent
fn parent_grant_for<'a>(child: &ToolGrant, parent: &'a ChioScope) -> Option<&'a ToolGrant> {
parent
.grants
.iter()
.find(|grant| {
grant.server_id == child.server_id
&& grant.tool_name == child.tool_name
&& child.is_subset_of(grant)
})
.or_else(|| parent.grants.iter().find(|grant| child.is_subset_of(grant)))
}The preferred match is exact on server_id and tool_name. The fallback drops the two equality tests and relies on ToolGrant::is_subset_of alone, which admits a parent whose server_id or tool_name is the wildcard "*". So a wildcard parent grant can be the resolved parent of a concrete child grant, which is what makes scoring a delegation off a broad root capability work at all. Anything the subset test rejects, including a child that adds an operation the parent lacks or drops a cap the parent set, resolves to no parent.
scope_reduced
fn scope_reduced(parent: &ChioScope, child: &ChioScope) -> bool {
if child
.grants
.iter()
.any(|child_grant| parent_grant_for(child_grant, parent).is_none())
{
return false;
}
if child.grants.len() < parent.grants.len()
|| child.resource_grants.len() < parent.resource_grants.len()
|| child.prompt_grants.len() < parent.prompt_grants.len()
{
return true;
}
child.grants.iter().any(|child_grant| {
parent_grant_for(child_grant, parent)
.map(|parent_grant| grant_scope_reduced(parent_grant, child_grant))
.unwrap_or(false)
})
}Read the first block as a veto. One orphan child grant, meaning one grant that is not a subset of anything in the parent, makes the whole edge score zero on this leg regardless of how much the other grants narrowed. That is the intended reading: an edge that widens anywhere is not a narrowing.
The second block is a shortcut. Dropping any grant from any of the three grant lists is sufficient, and it returns before a single per-grant comparison runs. Dropping one resource grant while leaving the tool grants untouched scores a full 1.0.
Only when the counts hold does the third block ask whether any single grant narrowed, and grant_scope_reduced accepts five different narrowings: fewer operations, more constraints, a constraint the parent did not carry, a lower invocation cap, or a lower monetary cap. It opens with its own veto, returning false if the child holds any operation the parent lacks, which expanded_child_operations_are_not_counted_as_reduced pins directly.
The TTL leg
There is no function for this one. It is the inline expression child.expires_at < parent.expires_at, and three properties follow from that literal reading.
- It compares absolute Unix expiry stamps, not requested TTLs. A child issued an hour after its parent with the same nominal TTL has a strictly later
expires_atand scores 0 on this leg, even though its remaining lifetime is shorter than the parent’s was at issuance. - It is strict. An inherited expiry, the common case when a delegation copies the parent’s deadline, scores 0.
- It is fully independent of the other two legs. It never consults
parent_grant_for, so an edge that broadens the tool set into an orphan grant still collects full credit here.
budget_reduced
fn budget_reduced(parent: &ChioScope, child: &ChioScope) -> bool {
child.grants.iter().any(|child_grant| {
parent_grant_for(child_grant, parent)
.map(|parent_grant| {
invocation_limit_reduced(parent_grant, child_grant)
|| monetary_limit_reduced(parent_grant, child_grant)
})
.unwrap_or(false)
})
}invocation_limit_reduced returns true when both sides carry a cap and the child’s is strictly lower, and also when the parent is uncapped and the child introduces a cap. monetary_limit_reduced checks max_cost_per_invocation and max_total_cost through the same helper, with one extra condition: monetary_cap_reduced requires parent_amount.currency == child_amount.currency before comparing units. A child that halves a cap while switching currency scores 0 on this leg. Currency conversion is not modeled here and no cross-currency comparison is attempted.
Note that budget_reduced takes no veto branch of its own. It is a bare any with unwrap_or(false), so an orphan grant contributes nothing rather than poisoning the edge. In practice the same edge fails the scope leg through that leg’s veto, so both report 0, but for different reasons.
Grant-level budgets, not the budget store
ToolGrant fields. Neither reads a BudgetUsageRecord, and neither knows what was actually spent. Consumption against a cap is what resource_stewardship scores, from the budget database, on a separate weight. The delegation-hygiene budget leg asks only whether the delegator wrote a smaller number into the child token. Sub-Agent Budgets covers the enforcement side of those caps.Decay, averaging, and the two negative fixtures
Every signal is a (score, weight) pair, and the weight is an exponential decay on the child’s issuance time.
fn decay_weight(now: u64, timestamp: u64, half_life_days: u32) -> f64 {
if half_life_days == 0 {
return 1.0;
}
let age_seconds = now.saturating_sub(timestamp) as f64;
let half_life_seconds = half_life_days as f64 * SECONDS_PER_DAY as f64;
2f64.powf(-age_seconds / half_life_seconds)
}The half life comes from ReputationConfig::temporal_decay_half_life_days, default 30. Setting it to 0 disables decay entirely rather than collapsing every weight to zero, and saturating_sub means a child stamped in the future is treated as age zero and weighted 1.0. weighted_average divides by the summed weight and returns 0.0 rather than a NaN when that sum is zero.
Two integration tests in tests/local_reputation.rs pin the behavior, and both are worth reading as specifications rather than as coverage.
Attenuating beats pass-through
structural_delegation_hygiene_scores_attenuating_delegator_above_passthrough gives one delegator two children. The first copies its parent exactly: same server and tool, same two operations, no constraints, the same invocation cap of 100, and the same expires_at. It scores 0 on all three legs. The second drops Operation::Delegate, adds a DomainExact constraint, cuts the invocation cap from 50 to 5, cuts max_cost_per_invocation from 500 to 50 USD, and expires far earlier. It scores 1 on all three. Both children were issued inside two days of the evaluation stamp against a 30 day half life, so their weights are near-identical and each rate lands just above 0.50. The test asserts a deliberately loose band: delegations_observed == 2, a score in (0.45, 0.85), and a scope rate strictly inside (0.0, 1.0).
Expansion earns TTL credit anyway
expanded_child_grants_do_not_score_as_reductions is the sharper fixture. The parent grants fs/read_file uncapped; the child grants fs/write_file with a cap of 5 and an earlier expiry. The tool names differ and neither is a wildcard, so parent_grant_for resolves nothing, the scope veto fires, and the budget leg finds no parent grant to compare against. The assertions are exact values, not bands:
assert_eq!(scorecard.delegation_hygiene.delegations_observed, 1);
assert_eq!(
scorecard.delegation_hygiene.scope_reduction_rate,
MetricValue::Known(0.0)
);
assert_eq!(
scorecard.delegation_hygiene.budget_reduction_rate,
MetricValue::Known(0.0)
);
assert_eq!(
scorecard.delegation_hygiene.ttl_reduction_rate,
MetricValue::Known(1.0)
);A delegation that swaps a read tool for a write tool and adds a cap the parent never had scores (0 + 1 + 0) / 3, one third, because the TTL leg does not know what the other two legs found. Read the three rates, not just the score, before treating a mid-range value as evidence of moderate attenuation.
Two smaller unit tests in src/tests.rs pin the predicates directly: scope_reduction_detects_narrower_constraints asserts both scope_reduced and budget_reduced on a properly attenuated pair, and orphan_child_grant_is_not_counted_as_reduced asserts both are false when the child names a tool the parent never granted.
This metric does not verify anything
receipt_integrity_valid, which checks the receipt id, the Ed25519 signature, the action hash, and membership in trusted_kernel_keys. Delegation hygiene reads no receipts and performs no verification at all. It trusts the capability_lineage rows as stored, including the parent_capability_id recorded alongside them. The consequence is directly visible in the test suite: expanded_child_grants_do_not_score_as_reductions runs with a bare ReputationConfig::default(), whose trusted_kernel_keys set is empty and whose doc comment warns that every receipt will fail integrity validation, and still gets three Known rates back. A delegation-hygiene score is a statement about what the lineage table says, not about what was cryptographically proved.Where it sits in the scorecard
delegation_hygiene is the sixth of eight metric fields on LocalReputationScorecard and the sixth of eight weights on ReputationWeights. Its default weight is 0.15, tied with least_privilege and reliability and second only to boundary_pressure at 0.20. The eight defaults sum to 1.00.
impl Default for ReputationWeights {
fn default() -> Self {
Self {
boundary_pressure: 0.20,
resource_stewardship: 0.10,
least_privilege: 0.15,
history_depth: 0.10,
tool_diversity: 0.05,
delegation_hygiene: 0.15,
reliability: 0.15,
incident_correlation: 0.10,
}
}
}The composite is a weighted mean over only the metrics that reported a value. contribute_metric adds the weight to both the numerator and to effective_weight_sum when the metric is Known, and adds nothing at all when it is Unknown.
fn contribute_metric(
metric: Option<f64>,
weight: f64,
weighted_sum: &mut f64,
effective_weight_sum: &mut f64,
) {
if let Some(value) = metric {
*weighted_sum += weight * clamp01(value);
*effective_weight_sum += weight;
}
}An agent that has never delegated is therefore not penalized. Its 0.15 drops out of both sides and the remaining seven weights renormalize over a smaller denominator. This is the load-bearing consequence of the unknown-versus-zero split, and it is why effective_weight_sum is a published field rather than an internal. The degenerate case shows the encoding at its clearest: a subject with no receipts at all.
$ chio --json --receipt-db ./receipts.db reputation local \
--subject-public-key 0000000000000000000000000000000000000000000000000000000000000000 \
| jq '{weights: .scoring.weights,
$ delegation_hygiene: .scorecard.delegation_hygiene,
$ effective_weight_sum: .scorecard.effective_weight_sum,
$ composite_score: .scorecard.composite_score}'{
"weights": {
"boundary_pressure": 0.2,
"resource_stewardship": 0.1,
"least_privilege": 0.15,
"history_depth": 0.1,
"tool_diversity": 0.05,
"delegation_hygiene": 0.15,
"reliability": 0.15,
"incident_correlation": 0.1
},
"delegation_hygiene": {
"score": {
"state": "unknown"
},
"delegations_observed": 0,
"scope_reduction_rate": {
"state": "unknown"
},
"ttl_reduction_rate": {
"state": "unknown"
},
"budget_reduction_rate": {
"state": "unknown"
}
},
"effective_weight_sum": 0.0,
"composite_score": {
"state": "unknown"
}
}Every rate is {"state": "unknown"} rather than zero, delegations_observed is a plain integer beside them, and effective_weight_sum is 0.0 because nothing contributed. Note the two names for one concept: the weight key is tool_diversity while the scorecard field it weights is specialization.
Partial cases are the ones to read carefully, because the composite keeps its shape while its denominator shrinks. A subject that issued no delegations, held no capped grants, and had no incident feed loses delegation_hygiene at 0.15, resource_stewardship at 0.10, and incident_correlation at 0.10, leaving an effective_weight_sum of 0.65 and a composite that is a mean over five metrics presented as if it were a mean over eight. Read effective_weight_sum alongside every composite; a value below 1.00 is the only signal that some of the model did not run.
The two gates that consume it
Two distinct paths turn a delegation-hygiene score into a decision. One reads it only through the composite; the other reads it directly and can refuse on it alone.
The issuance tier ladder, through the composite
enforce_reputation_policy computes the scorecard, caps the composite at probationary_score_ceiling while the subject is probationary, resolves a named tier by band lookup, and enforces that tier’s max_scope against the capability being requested.
let effective_score = scorecard.composite_score.as_option().unwrap_or(0.0);
let effective_score = ceiling
.filter(|_| probationary)
.map_or(effective_score, |limit| effective_score.min(limit));
let resolved_tier = issuance_policy
.and_then(|policy| resolve_tier(policy, effective_score))Delegation hygiene reaches this decision only as one weighted term inside effective_score. The ladder cannot see the metric, and no tier band can be conditioned on it. The band the score lands in selects a TierScopeCeiling, and the ceiling fields that matter most to a delegator are max_delegation_depth, which refuses any grant carrying Operation::Delegate when set to Some(0), and constraints_required, which refuses an unconstrained tool grant outright. Both refusals are KernelError::CapabilityIssuanceDenied. A poor delegation-hygiene score can therefore cost an agent the right to delegate at all, but only by moving the composite down a band.
The weight itself is operator-tunable. The HushSpec reputation extension exposes extensions.reputation.scoring.weights.delegation_hygiene, validated to [0.0, 1.0] by validate_weight and merged over the default by materialize_reputation_issuance_policy. Nothing requires the eight weights to sum to anything, because the composite normalizes by whatever they do sum to. See the policy schema reference for the surrounding block.
The passport verifier policy, directly
The gate that does read the metric is the portable one. A ReputationCredential carries the whole LocalReputationScorecard as its credentialSubject.metrics, and PassportVerifierPolicy exposes a minDelegationHygiene threshold beside minCompositeScore, minReliability, minLeastPrivilege, and maxBoundaryPressure. It is validated to the unit interval by PassportVerifierPolicy::validate and enforced by require_metric_min.
fn require_metric_min(reasons: &mut Vec<String>, field: &str, value: MetricValue, minimum: f64) {
match value.as_option() {
Some(value) if value >= minimum => {}
Some(value) => reasons.push(format!(
"{field} {} is below required minimum {}",
value, minimum
)),
None => reasons.push(format!("{field} is unknown but policy requires a minimum")),
}
}The None arm is the one to internalize. Once a verifier sets minDelegationHygiene, an unknown metric fails the credential with delegation_hygiene is unknown but policy requires a minimum. The renormalization that protects a never-delegated agent inside the composite does not apply here: this gate reads the metric directly, and absence is a refusal. An agent that has genuinely never delegated cannot satisfy a verifier that requires this floor. examples/policies/passport-verifier.yaml ships that floor at 0.70. Reasons accumulate rather than short-circuit, so a rejected credential lists every failing threshold at once. Agent Passport covers the credential envelope this rides in.
Not the marketplace tier
chio-reputation also ships a four-value ReputationTier enum, tier_0 through tier_3, with hard-coded thresholds of 0.50, 0.75, and 0.90 plus a per-feed floor of 0.80. The scorecard has nothing to do with it. tier_from_deltas takes a slice of ScoreDelta values produced by the ReputationFeed trait, never a LocalReputationScorecard, and the workspace contains no non-test caller of it at all. The tenant tier that actually filters the guard catalog through satisfies_floor in market_list arrives as a command-line string parsed by parse_market_tier, which rejects anything outside tier0..tier3. Delegation hygiene never reaches marketplace visibility, and MarketplaceReputationTier in chio-appraisal is a separate mirror enum that its own doc comment describes as matching the shape without taking the dependency.
Reading it on a running node
chio reputation local --subject-public-key <hex> computes and prints the scorecard. --since and --until bound the receipt window, --policy supplies a HushSpec whose scoring block and tier ladder should apply, and the global --receipt-db is required because the lineage table lives in the receipt store. With --control-url the query is forwarded to GET /v1/reputation/local/{subject_key} on the trust service, which uses its own configured policy; passing --policy alongside it is a hard error.
| Symptom | Read this | Likely cause |
|---|---|---|
delegation_hygiene: unknown | delegations_observed is 0 | Either the subject issued nothing, or every lineage row it wrote is a root and carries a null parent_capability_id, or the parents are outside the corpus, or the kernel that observed the delegations runs without a receipt store and wrote no rows at all. |
| Score stuck near 0.33 | The three rates individually | TTL credit only. The delegations move the expiry in but never narrow scope or a limit, or they resolve to no parent grant. |
| Score stuck near 0.67 | budget_reduction_rate | Scope and TTL narrow but no cap moves. A parent with no max_invocations and a child that also sets none scores 0 on the budget leg. |
effective_weight_sum below 1.00 | Which metrics report unknown | Expected whenever a metric had no input. Delegation hygiene alone accounts for 0.15 of the shortfall. |
| Every metric unknown or zero | The chio_reputation warn-once log line | Empty trusted_kernel_keys. The five receipt-backed metrics collapse; delegation hygiene and resource_stewardship read no receipts and still report, which makes them a useful signal that the corpus is present and the trust anchor is not. |
The human renderer prints one line, delegation_hygiene: followed by the score to three decimals or the literal unknown. The three rates and delegations_observed appear only under --format json. Every diagnosis in the table above needs the JSON form.
chio reputation compare puts a live local scorecard beside the one baked into a passport and reports drift per metric. ReputationMetricDriftSet carries a delegation_hygiene entry with portable, local, and localMinusPortable. It compares the top-level score only. The three rates are not diffed, so a credential whose composed score matches while its scope and TTL rates have swapped shows zero drift. Portable Reputation covers the export side, including the nine-decimal rounding round_portable_score applies to every metric on the way out.
Guarantees and limits
| Status | Claim | Evidence |
|---|---|---|
| Shipped | The metric is exactly three boolean legs per edge, decay-weighted and averaged, then averaged again into one score. No leg carries partial credit and no fourth leg exists. | compute_delegation_hygiene in crates/trust/chio-reputation/src/compare.rs |
| Shipped | Deterministic and pure. Same corpus, same now, same config yields the same five field values. The crate opens no database and calls no kernel. | Crate header in chio-reputation/src/lib.rs; #![forbid(unsafe_code)] |
| Proved by test | An expanded child scores 0 on scope and budget and 1 on TTL, with delegations_observed == 1. Exact-value assertions, not bands. | expanded_child_grants_do_not_score_as_reductions in tests/local_reputation.rs |
| Proved by test | A delegator with one pass-through and one attenuating edge lands strictly between the two extremes on the scope rate and inside (0.45, 0.85) on the score. | structural_delegation_hygiene_scores_attenuating_delegator_above_passthrough |
| Proved by test | An orphan child grant makes both structural predicates false, and a child that adds an operation the parent lacks is not a narrowing. | orphan_child_grant_is_not_counted_as_reduced and expanded_child_operations_are_not_counted_as_reduced in src/tests.rs |
| Shipped | Unknown removes the weight from both sides of the composite rather than scoring zero, so a never-delegated agent is not penalized. effective_weight_sum reports the shortfall. | contribute_metric in src/issuance.rs; the 0.65 sum in the captured reputation local report under examples/internet-of-agents-web3-network/app/tests/fixtures/ |
| Shipped | minDelegationHygiene is a real verifier-policy gate, and an unknown metric fails it closed rather than being skipped. | require_metric_min in chio-credentials/src/policy.rs; PassportVerifierPolicy::validate |
| Shipped | The parent link is derived from the token’s delegation chain by the kernel, so ordinary delegated capabilities are scoreable without any extra call. A kernel configured with no receipt store writes no lineage row at all and reports the metric unknown. | record_observed_capability_snapshot in chio-kernel/src/kernel/dispatch.rs derives parent_capability_id from delegation_chain.last(); with_receipt_store returns without writing when no store is configured |
| Limit | No verification. The metric reads lineage rows as stored, checks no signature, and requires no trusted kernel key. It scores what the table claims, not what was proved. | No receipt_integrity_valid call on this path; expanded_child_grants_do_not_score_as_reductions passes with an empty trust set |
| Limit | The TTL leg compares absolute expiry stamps. A child issued later than its parent with an identical nominal TTL scores 0 despite a shorter remaining lifetime, and an inherited expiry scores 0 because the comparison is strict. | child.expires_at < parent.expires_at, inline in compute_delegation_hygiene |
| Limit | The three legs are independent. An edge that broadens the tool set still collects full TTL credit, so a score of one third can mean disciplined expiries over an expanding scope. | The TTL leg never calls parent_grant_for; the exact-value assertions in the expansion fixture |
| Limit | Magnitude is invisible. Dropping one operation and dropping nine score identically, and dropping a single resource grant short-circuits scope_reduced to true before any per-grant comparison runs. | bool_to_score; the grant-count branch in scope_reduced |
| Limit | A cross-currency cap reduction scores 0. monetary_cap_reduced requires matching currency codes before comparing units and attempts no conversion. | monetary_cap_reduced in src/issuance.rs |
| Limit | Unresolvable parents vanish. A child whose parent capability is absent from the corpus is skipped without a counter, and delegations_observed reports edges scored rather than edges written. | The two continue arms and delegations_observed: scope_signals.len() |
| Limit | The drift report compares the composed score only. The three rates are carried in the credential but never diffed against local state. | ReputationMetricDriftSet and compare_metric_values in chio-control-plane/src/reputation.rs |
| Not wired | delegation_hygiene_min under a tier’s promotion.required_metrics deserializes, gets no range check, and is read by nothing. A shipped example policy sets it to 0.80 and that value changes no decision. Tier resolution is a stateless score_range lookup on the composite. | ReputationRequiredMetrics in chio-policy/src/models/extensions.rs carries only deny_unknown_fields, and required_metrics has no reference anywhere outside that declaration; materialize_reputation_issuance_policy copies only score_range and max_scope; the example is examples/policies/hushspec-reputation.yaml |
| Unsupported | Marketplace visibility. ReputationTier is composed from feed deltas, tier_from_deltas has no non-test caller, and the tenant tier that filters the catalog is a parsed command-line string. | chio-reputation/src/tier.rs; parse_market_tier and market_list in chio-cli |
| Unsupported | Scoring an agent on delegations it received, or on what its delegates did next. The filter is issuer_key == subject_key, and a grandchild edge belongs to the intermediate delegator’s scorecard, not the root’s. | delegations_issued in compute_local_scorecard |
Next Steps
- Reputation Scoring · the other seven metrics, the config knobs, and the composite this one contributes 0.15 to
- Delegation & Attenuation · the enforcement side, where a widened child is refused at issuance instead of scored afterwards
- Agent Passport · the credential that carries this metric across an operator boundary, and the verifier policy that can refuse on it
- Sub-Agent Budgets · what the caps in the budget leg actually do once a child starts spending against them
- Reputation & Scarcity · what the signed structure bounds on its own, before any score is consulted