Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c98ef40
Add temporary source export workflow
George-RD Aug 1, 2026
6ef62ab
Run source export for the draft pull request
George-RD Aug 1, 2026
92df129
Fix temporary source export archive
George-RD Aug 1, 2026
fffa66c
test: specify responsibility delegation contracts
George-RD Aug 1, 2026
32af5b0
test: format responsibility contract specification
George-RD Aug 1, 2026
e282109
chore: export temporary Rust development environment
George-RD Aug 1, 2026
47f46c6
chore: export temporary OpenSpec development environment
George-RD Aug 1, 2026
083e535
chore: stage verified responsibility contract update
George-RD Aug 2, 2026
54c96e0
chore: finalize verified responsibility contract tree
George-RD Aug 2, 2026
a38c33e
fix: force removal of verified temporary patch
George-RD Aug 2, 2026
7697973
feat: finish protocol-neutral responsibility contract
github-actions[bot] Aug 2, 2026
a259a03
chore: run full verification
George-RD Aug 2, 2026
054ba09
test: expose responsibility contract review gaps
George-RD Aug 3, 2026
f89557c
test: pin owner review and lifecycle failures
George-RD Aug 3, 2026
ba18865
fix: enforce conservative delegation policy bounds
George-RD Aug 3, 2026
af62cca
fix: seal resolved delegation context behind catalog
George-RD Aug 3, 2026
3345046
fix: derive reviewed scope only from sealed context
George-RD Aug 3, 2026
d8eb848
fix: bind owner review to valid evidence and policy
George-RD Aug 3, 2026
e6077ea
fix: reject inactive responsibility compatibility
George-RD Aug 3, 2026
252903d
chore: format responsibility contract tests
George-RD Aug 3, 2026
9d6c3d7
chore: format owner review contract tests
George-RD Aug 3, 2026
66f496b
style: apply rustfmt to responsibility review contract
George-RD Aug 3, 2026
fe34430
docs: scope fail-closed behavior to delegation readiness
George-RD Aug 3, 2026
6b78b5a
docs: preserve known unimplemented action behavior
George-RD Aug 3, 2026
4785108
test: fix responsibility classification fixture shadowing
George-RD Aug 3, 2026
5e8233e
test: require delegation descriptors to be catalogued
George-RD Aug 3, 2026
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
40 changes: 40 additions & 0 deletions .raw/openspine-decision-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ Before changing a PRD section, check the relevant decision entry. If the propose
| D-143 | Encrypted vault storage in SecretStore for OAuth refresh/access tokens and identity metadata | Accepted |
| D-144 | Preemptive single-flight background token refresher (`OAuthRefresher`) with 300s expiration skew window and credential disabling | Accepted |
| D-145 | Model gateway bearer token resolution with automatic single-retry on HTTP 401 Unauthorized after inline token refresh | Accepted |
| D-146 | Reusable responsibilities use protocol-neutral reviewed context and remain reference views over ordinary workflow/standing-rule authority | Accepted |

---

Expand Down Expand Up @@ -3425,6 +3426,44 @@ Provider APIs return non-standard auth error codes requiring alternative retry t

---

# D-146 — Protocol-neutral reviewed responsibility contract above standing rules

## Decision

A reusable responsibility is an owner-facing reference/view over an ordinary reviewed workflow plus bounded standing-rule input. It is never a task grant or second live authority object. Reusable-delegation review requires two independent catalog declarations: protocol-neutral action semantics and a concrete versioned resolver/executor implementation. The kernel derives a sealed resolved context; owner-reviewed scope is expressed only through generic dimensions and is digest-bound with evidence, limits, compatibility versions, decisions, and lifecycle controls.

For communication and connector-write effects, dark-window `Allow` is forbidden. Missing descriptors, resolver/executor implementation, connector/account resolution, required scope, or compatibility versions fail closed before reusable execution; later drift moves the responsibility to `needs_review` rather than silently remapping it.

## Rationale

Standing rules already provide bounded composition input, but an action id plus quota is not enough to explain a delegated job or prove what the owner reviewed. A Gmail-specific wrapper would couple the autonomy model to one connector and would not survive adding WhatsApp, Slack, Outlook, or other protocols. Separating semantic action identity from concrete implementation identity makes connector and executor changes review-visible. A generic reviewed context also lets the same matcher and review contract apply to synthetic and future protocols without protocol branches.

Keeping responsibility as a reference view preserves D-007: every task still receives a fresh task grant and crosses `gate()`. Distinct evidence classes prevent explicit requests or manually supplied artifacts from being presented as learned repeated patterns. Forbidding communication Allow defaults avoids turning owner silence into an external write.

## Trade-offs

| Option | Benefit | Risk |
| --- | --- | --- |
| Treat each standing rule as the responsibility | Minimal new schema | Poor owner legibility; no implementation/context compatibility; standing rule can be mistaken for authority |
| Build Gmail-specific progressive delegation | Fastest first demo | Connector lock-in, duplicated matching/review logic, unsafe protocol expansion |
| Protocol-neutral two-axis contract and reference-view responsibility | Honest capability readiness, reusable across protocols, explicit drift | More schema and versioning before the first autonomous proof |

## Consequences

- `ActionCatalog` gains independent semantic and concrete implementation descriptor axes.
- `email.create_draft` may declare reviewed semantics while remaining fail-closed until a shared real implementation lands.
- `ResolvedActionContext`, `ReviewedActionScope`, `DelegationEvidence`, `OwnerReviewRequest`, and `ResponsibilityManifest` are explicit versioned schemas.
- Scope comparison and drift assessment use generic dimensions and typed reasons, not connector names.
- Connector/account removal cannot create a zombie responsibility; unresolved context requires review.
- Telegram, terminal, and future owner surfaces render one semantic review object.
- Existing per-instance Gmail selected-thread and approved-draft behavior remains unchanged by the contract slice.

## Would change if

A future adversarially verified design demonstrates that a tightly bounded communication Allow default is safer than explicit review, or if task grants cease to be the sole live authority object under a separately accepted replacement for D-007.

---

## Change Log

| Date | Change |
Expand Down Expand Up @@ -3472,4 +3511,5 @@ Provider APIs return non-standard auth error codes requiring alternative retry t
| 2026-07-24 | Added D-123 (production adoption at ambiguous route resolution), D-124 (canonical tied candidate ids), D-125 (lexicographic within-class selection), D-126 (invalid/failed competitors escalate), D-127 (all-non-applicable ties are silent non-matches), D-128 (rated-egress production guard), and D-129 (persist the selected composition snapshot), settled while implementing `wire-authority-equivalence-selection`. |
| 2026-07-24 | Added D-130–D-141 (pure proposed-only miner boundary, authenticated bounded grants, verified encrypted-reference evidence, derived exact-match repetition, correction/probe separation, fail-closed provenance, independent durable budgets, scoped consolidation, declarative scheduled grant composition, and owner-bound cross-grant evidence), settled while implementing `implement-reflection-miner`. |
| 2026-07-24 | Added D-142 (native OAuth 2.0 PKCE authentication), D-143 (encrypted vault storage in SecretStore), D-144 (preemptive single-flight background token refresher), and D-145 (gateway bearer token resolution with automatic single-retry on 401), settled while implementing `implement-model-provider-oauth-onboarding`. |
| 2026-08-03 | Added D-146 (protocol-neutral two-axis responsibility contract; responsibility remains a reference view over workflow/standing-rule inputs; communication dark-window Allow forbidden; drift fails to `needs_review`), settled while implementing `define-responsibility-contract`. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a consistent change-log date.

Line 3514 records 2026-08-03, but the current date is August 2, 2026 and the archive directory is dated 2026-08-02. Change the entry to 2026-08-02.

Proposed fix
-| 2026-08-03 | Added D-146 ...
+| 2026-08-02 | Added D-146 ...
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| 2026-08-03 | Added D-146 (protocol-neutral two-axis responsibility contract; responsibility remains a reference view over workflow/standing-rule inputs; communication dark-window Allow forbidden; drift fails to `needs_review`), settled while implementing `define-responsibility-contract`. |
| 2026-08-02 | Added D-146 (protocol-neutral two-axis responsibility contract; responsibility remains a reference view over workflow/standing-rule inputs; communication dark-window Allow forbidden; drift fails to `needs_review`), settled while implementing `define-responsibility-contract`. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.raw/openspine-decision-log.md at line 3514, Update the date in the D-146
changelog entry to 2026-08-02 so it matches the current date and archive
directory convention; leave the rest of the entry unchanged.


39 changes: 39 additions & 0 deletions crates/openspine-kernel/src/action_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ pub fn canonical_catalog() -> ActionCatalog {
SelectionTokenType::email_thread_selection(),
)])
.with_egress_declarations(decls)
.with_delegation_descriptors(action_catalog_data::delegation_descriptors())
.with_effect_paths([
EffectPath {
name: "notify_owner_best_effort".to_string(),
Expand Down Expand Up @@ -185,6 +186,9 @@ pub fn canonical_catalog() -> ActionCatalog {
#[cfg(test)]
mod tests {
use super::*;
use openspine_schemas::action::{
ActionImplementationId, DarkWindowPolicy, DelegationCatalogError,
};
use openspine_schemas::egress::EgressClass;
#[test]
fn test_catalog_effect_paths_are_fully_enumerated_and_classified() {
Expand Down Expand Up @@ -302,6 +306,41 @@ mod tests {
}
}

#[test]
fn every_delegation_descriptor_names_a_catalogued_action() {
let catalog = canonical_catalog();
for descriptor in action_catalog_data::delegation_descriptors() {
assert!(
catalog.contains(&descriptor.action_id),
"delegation descriptor names uncatalogued action {}",
descriptor.action_id
);
}
}

#[test]
fn email_draft_has_a_reviewed_descriptor_but_no_delegated_executor_yet() {
let catalog = canonical_catalog();
let descriptor = catalog
.delegation_descriptor_for(&id("email.create_draft"))
.expect("email draft delegation semantics must be catalog-owned");
assert!(descriptor.reusable_delegation);
assert!(matches!(
descriptor
.delegation_policy
.as_ref()
.expect("delegation policy")
.dark_window_policy,
DarkWindowPolicy::Prohibited
));
let implementation_id = ActionImplementationId::new("gmail.email.create_draft");
assert_eq!(
catalog.validated_delegation_contract(&id("email.create_draft"), &implementation_id),
Err(DelegationCatalogError::MissingImplementationDescriptor { implementation_id }),
"#127 must land the shared real executor before reusable admission is ready"
);
}

#[test]
fn overlay_export_restore_are_non_delegable_with_no_egress() {
let catalog = canonical_catalog();
Expand Down
72 changes: 71 additions & 1 deletion crates/openspine-kernel/src/action_catalog_data.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,80 @@
use openspine_schemas::action::{ActionEgressDeclaration, ActionId};
use std::collections::BTreeSet;

use openspine_schemas::action::{
ActionDescriptor, ActionEgressDeclaration, ActionId, ActionSemantics, BudgetWindowBounds,
DarkWindowPolicy, DataDestination, DelegationDefaults, DelegationPolicyBounds,
DelegationProposalMode, EffectKind, EffectReversibility, ReviewedScopeDimension,
};
use openspine_schemas::egress::EgressClass;
use openspine_schemas::standing_rule::BudgetWindow;

fn id(s: &str) -> ActionId {
ActionId::new(s)
}

/// Protocol-neutral semantics for actions that may eventually participate in
/// reusable delegation. A descriptor does not assert that a concrete
/// resolver/executor exists; that independent readiness axis lands in #127.
pub(crate) fn delegation_descriptors() -> Vec<ActionDescriptor> {
vec![ActionDescriptor {
schema_version: 1,
descriptor_version: 1,
action_id: id("email.create_draft"),
semantics: ActionSemantics {
owner_verb: "create".to_string(),
owner_object: "email draft".to_string(),
owner_target: "reviewed mailbox conversation".to_string(),
effect_kind: EffectKind::OwnerAccountWrite,
reversibility: EffectReversibility::Reversible,
destination: DataDestination::OwnerCloudAccount,
},
reusable_delegation: true,
required_scope_dimensions: BTreeSet::from([
ReviewedScopeDimension::ConnectorImplementation,
ReviewedScopeDimension::ConnectorInstance,
ReviewedScopeDimension::AccountRole,
ReviewedScopeDimension::AccountIdentity,
ReviewedScopeDimension::Target,
ReviewedScopeDimension::Counterparty,
ReviewedScopeDimension::RelationshipTier,
ReviewedScopeDimension::EffectDestination,
ReviewedScopeDimension::Workflow,
ReviewedScopeDimension::TaskShape,
]),
delegation_policy: Some(DelegationPolicyBounds {
schema_version: 1,
policy_version: 1,
quota: BudgetWindowBounds {
minimum_max: 1,
maximum_max: 20,
minimum_window_secs: 60,
maximum_window_secs: 30 * 24 * 3600,
},
rate: BudgetWindowBounds {
minimum_max: 1,
maximum_max: 5,
minimum_window_secs: 60,
maximum_window_secs: 24 * 3600,
},
maximum_lapse_secs: 90 * 24 * 3600,
proposal_mode: DelegationProposalMode::DefaultsPermitted,
defaults: Some(DelegationDefaults {
quota: BudgetWindow {
max: 5,
window_secs: 7 * 24 * 3600,
},
rate: BudgetWindow {
max: 1,
window_secs: 3600,
},
expires_after_secs: 90 * 24 * 3600,
}),
dark_window_policy: DarkWindowPolicy::Prohibited,
fresh_target_selection_required: true,
}),
}]
}

/// Explicit egress metadata for every canonical action. `None/None` is a
/// deliberate non-egress classification, never an implicit default.
pub(crate) fn egress_declarations() -> Vec<(ActionId, ActionEgressDeclaration)> {
Expand Down
82 changes: 82 additions & 0 deletions crates/openspine-schemas/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ use ulid::Ulid;

use crate::selection::SelectionTokenType;

pub use crate::delegation_contract::{
validate_delegation_contract, ActionDescriptor, ActionImplementationDescriptor,
ActionImplementationId, ActionSemantics, BudgetWindowBounds, DarkWindowPolicy, DataDestination,
DelegationCatalogError, DelegationDefaults, DelegationEligibilityError, DelegationPolicyBounds,
DelegationProposalMode, EffectKind, EffectReversibility, ReviewedScopeDimension,
};

use crate::artifact::ArtifactRef;
use crate::egress::EgressClass;
use crate::event::TargetRef;
Expand Down Expand Up @@ -119,6 +126,12 @@ pub struct ActionCatalog {
/// mandatory output-channel + egress-class declaration. Enforcement reads
/// ONLY this map; connector metadata is never consulted.
egress_declarations: HashMap<ActionId, ActionEgressDeclaration>,
/// Protocol-neutral declarations for actions eligible to participate in
/// reusable delegation. Absence is a fail-closed ineligibility signal.
delegation_descriptors: HashMap<ActionId, ActionDescriptor>,
/// Concrete resolver/executor declarations. An action descriptor alone
/// never proves that a live effect path is ready for delegation.
implementation_descriptors: HashMap<ActionImplementationId, ActionImplementationDescriptor>,
}

impl ActionCatalog {
Expand All @@ -131,6 +144,8 @@ impl ActionCatalog {
effect_paths: Vec::new(),
counterparty_facing_actions: HashSet::new(),
egress_declarations: HashMap::new(),
delegation_descriptors: HashMap::new(),
implementation_descriptors: HashMap::new(),
}
}

Expand Down Expand Up @@ -223,6 +238,73 @@ impl ActionCatalog {
self
}

/// Register protocol-neutral delegation descriptors. The builder keeps
/// the catalog pattern used by the other independent classification axes;
/// proposal code must still call [`validate_delegation_contract`] with a
/// concrete implementation before presenting reusable authority.
pub fn with_delegation_descriptors(
mut self,
descriptors: impl IntoIterator<Item = ActionDescriptor>,
) -> Self {
self.delegation_descriptors = descriptors
.into_iter()
.map(|descriptor| (descriptor.action_id.clone(), descriptor))
.collect();
self
}

/// Register concrete resolver/executor declarations.
pub fn with_implementation_descriptors(
mut self,
descriptors: impl IntoIterator<Item = ActionImplementationDescriptor>,
) -> Self {
self.implementation_descriptors = descriptors
.into_iter()
.map(|descriptor| (descriptor.implementation_id.clone(), descriptor))
.collect();
self
}

/// Return the reviewed delegation declaration for a canonical action.
pub fn delegation_descriptor_for(&self, id: &ActionId) -> Option<&ActionDescriptor> {
self.delegation_descriptors.get(id)
}

/// Return one concrete resolver/executor declaration by stable id.
pub fn implementation_descriptor_for(
&self,
id: &ActionImplementationId,
) -> Option<&ActionImplementationDescriptor> {
self.implementation_descriptors.get(id)
}

/// Resolve and validate both independent delegation-readiness axes.
/// Missing catalog membership, semantic declaration, implementation,
/// resolver, executor, scope, or policy fails before owner review.
pub fn validated_delegation_contract(
&self,
action_id: &ActionId,
implementation_id: &ActionImplementationId,
) -> Result<(&ActionDescriptor, &ActionImplementationDescriptor), DelegationCatalogError> {
if !self.contains(action_id) {
return Err(DelegationCatalogError::UnknownAction {
action_id: action_id.clone(),
});
}
let descriptor = self.delegation_descriptor_for(action_id).ok_or_else(|| {
DelegationCatalogError::MissingActionDescriptor {
action_id: action_id.clone(),
}
})?;
let implementation = self
.implementation_descriptor_for(implementation_id)
.ok_or_else(|| DelegationCatalogError::MissingImplementationDescriptor {
implementation_id: implementation_id.clone(),
})?;
validate_delegation_contract(descriptor, implementation)?;
Ok((descriptor, implementation))
}

/// True if a denial of `id` faces an external counterparty and must
/// surface the canonical deferral + owner escalation (AD-151).
/// Unknown/unclassified actions return false (fail closed).
Expand Down
Loading