Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
23f128f
test(extension): require managed Agent Task allow-list
seonghobae Aug 11, 2026
7b1da43
test(extension): place managed admission at policy boundary
seonghobae Aug 11, 2026
e476f59
feat(extension): enforce managed Agent Task extension admission
seonghobae Aug 11, 2026
fb33d21
test(extension): remove misplaced core admission test
seonghobae Aug 11, 2026
a0d7673
docs(extension): record managed Agent Task admission
seonghobae Aug 11, 2026
e836e83
chore(extension): align managed admission with attached-tab authority
seonghobae Aug 11, 2026
f4521ba
test(extension): require managed policy freshness
seonghobae Aug 11, 2026
66dbc57
feat(extension): fail closed on stale managed policy
seonghobae Aug 11, 2026
759d2f0
docs(changelog): record extension policy freshness
seonghobae Aug 11, 2026
b63fb4d
test(extension): bound managed policy lifetime
seonghobae Aug 12, 2026
0962a34
style(extension): format policy lifetime regression
seonghobae Aug 12, 2026
9ea75cb
feat(extension): bound managed policy lifetime
seonghobae Aug 12, 2026
8db4d2d
chore(extension): align managed admission with current attached-tab head
seonghobae Aug 14, 2026
9d56aef
chore(extension): align policy freshness with current managed-admissi…
seonghobae Aug 14, 2026
d7d2ea4
chore(stack): refresh managed-extension dependency snapshot
seonghobae Aug 15, 2026
4a244e7
chore(stack): realign managed extension policy with attached-tab assu…
seonghobae Aug 15, 2026
c79ab00
chore(stack): realign extension policy freshness with managed admission
seonghobae Aug 15, 2026
d7d7ea2
fix(stack): preserve live managed-extension ancestry
seonghobae Aug 15, 2026
ad883c1
chore(stack): realign managed extension policy with current attached-…
seonghobae Aug 17, 2026
6b92b50
chore(stack): carry managed extension policy over current attached-ta…
seonghobae Aug 17, 2026
0de1ba2
docs: restore managed extension policy changelog
seonghobae Aug 17, 2026
f6fe39e
chore(stack): realign managed extension freshness with current admiss…
seonghobae Aug 17, 2026
891e8b7
docs: restore managed extension freshness changelog
seonghobae Aug 17, 2026
a123497
Merge current attached-tab assurance into managed extension policy
seonghobae Aug 17, 2026
a3cfb7c
test(policy): align extension request with current authority contract
seonghobae Aug 17, 2026
ed6d191
merge: reconcile extension freshness with live managed-policy parent
seonghobae Aug 17, 2026
556dcbd
Merge pull request #94 from ContextualWisdomLab/feat/agent-task-exten…
seonghobae Sep 4, 2026
c9adea6
Merge attached-tab assurance into extension policy
seonghobae Sep 4, 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Refreshed the product-gap queue to 126 open pull requests (54 ready, 72 draft) after #190, #188, #185, #192, #182, #184, #115, #181, #116, #117, #118, #183, #114, #127, #112, #109, #186, #110, #108, #111, #174, and #113 were merged into their immediate stacked prerequisites. PRs #147, #146, #145, #144, #143, #142, #141, #139, #136, #132, #129, and #128 moved to ready after exact-head checks and thread review; these are queue-consolidation results, not protected-main shipment.

### Added

- Time-bounded managed extension admission for isolated Agent Task profiles with an explicit half-open validity window and caller-supplied trusted evaluation time; invalid, not-yet-valid, and expired policy states fail closed before exact extension allow-list membership is considered.
- Fail-closed managed extension admission for isolated Agent Task profiles: an empty policy admits no extension, only exact canonical `ExtensionId` allow-list membership is accepted, duplicate entries cannot widen authority, and successful profile admission remains separate from `ExtensionAgentGrant` capability.
- Explicit reduced-assurance classification for attached human tabs when trusted adapter evidence says an existing extension can influence page state; the narrow rule does not detect extensions, prove extension absence, grant Agent authority, or turn an unclassified context into high-assurance evidence.
- Corrected the 2026-08-26 product-gap snapshot with current #229 presentation-identity evidence, stacked-only #205 integration evidence, current base/head pairs, the 126-PR queue count, explicit root-versus-child merge ordering, and the active GitHub counted-approval gate.
- Refreshed the product and technical gap baseline onto the 2026-08-26 live inventory: 126 open pull requests (54 ready, 72 draft), protected-main promotion of #168/#194/#196/#216/#151, a verified maintenance-loop record (supersession closure of #153, conflict reconciliations on #37/#149/#152/#173/#175, issue #212 option-(b) authorization on #43, Strix vuln-0001 homoglyph remediation on #124), provider-rerun outcome evidence, an organization review-pipeline congestion record, and refreshed merge-order queue guidance. Documentation evidence contracts were aligned to the same snapshot so the baseline, its dated markers, and the pinned exact-head rows cannot silently diverge.

Expand Down
97 changes: 96 additions & 1 deletion crates/originweave-policy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,107 @@ pub use sensitive_data::{
evaluate_handle_use,
};

use std::collections::BTreeSet;

use originweave_core::mcp::ValidatedMcpToolCall;
use originweave_core::{
ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose,
ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose, ExtensionId,
InstructionSource, PolicyContext, RiskClass, RobotsDecision, SecretDelivery, SessionMode,
};

/// Exact extension identities that may be present in one managed Agent Task profile.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentTaskExtensionPolicy {
managed_extensions: BTreeSet<ExtensionId>,
valid_from: u64,
valid_until: u64,
maximum_window: u64,
}

impl AgentTaskExtensionPolicy {
/// Build one fail-closed Agent Task extension admission policy.
///
/// Duplicate identifiers collapse to one exact managed identity. An empty
/// iterator therefore represents the default policy that admits no extension.
/// `valid_from` is inclusive and `valid_until` is exclusive. `maximum_window`
/// is the reviewed local ceiling for that interval. All three values are opaque
/// timestamps or durations in the same caller-defined trusted time domain and
/// units supplied to [`evaluate_agent_task_extension`]. This constructor does
/// not authenticate policy provenance or attest a clock; an invalid, empty, or
/// overlong validity window is retained so evaluation can fail closed
/// deterministically.
#[must_use]
pub fn new<I>(
managed_extensions: I,
valid_from: u64,
valid_until: u64,
maximum_window: u64,
) -> Self
where
I: IntoIterator<Item = ExtensionId>,
{
Self {
managed_extensions: managed_extensions.into_iter().collect(),
valid_from,
valid_until,
maximum_window,
}
}
}

/// Result of evaluating one extension identity for an isolated Agent Task profile.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentTaskExtensionDecision {
/// The exact canonical extension identity appears in the managed allow-list.
AllowManagedExtension,
/// The extension identity is absent from the managed allow-list.
DenyNotManaged,
/// The configured validity window is empty/reversed or its local ceiling is zero.
DenyInvalidPolicyWindow,
/// The configured policy validity interval exceeds the reviewed local maximum.
DenyPolicyWindowExceedsMaximum,
/// The trusted evaluation time precedes the policy validity window.
DenyPolicyNotYetValid,
/// The trusted evaluation time is at or beyond the policy expiry boundary.
DenyPolicyExpired,
}

/// Evaluate extension admission without minting OriginWeave Agent capability.
///
/// This pure boundary answers only whether the exact canonical extension may be
/// present in the caller's managed Agent Task profile at `trusted_time`.
/// `trusted_time`, [`AgentTaskExtensionPolicy::new`] `valid_from`, `valid_until`,
/// and `maximum_window` must use one caller-attested time domain and compatible
/// units; this function does not read or attest a clock. The validity window is
/// half-open (`valid_from <= trusted_time < valid_until`) and must not exceed the
/// reviewed local maximum. Chromium permissions, installation state, native
/// messaging, and [`originweave_core::ExtensionAgentGrant`] remain separate
/// authorities.
#[must_use]
pub fn evaluate_agent_task_extension(
extension_id: &ExtensionId,
policy: &AgentTaskExtensionPolicy,
trusted_time: u64,
) -> AgentTaskExtensionDecision {
if policy.valid_from >= policy.valid_until || policy.maximum_window == 0 {
return AgentTaskExtensionDecision::DenyInvalidPolicyWindow;
}
if policy.valid_until - policy.valid_from > policy.maximum_window {
return AgentTaskExtensionDecision::DenyPolicyWindowExceedsMaximum;
}
if trusted_time < policy.valid_from {
return AgentTaskExtensionDecision::DenyPolicyNotYetValid;
}
if trusted_time >= policy.valid_until {
return AgentTaskExtensionDecision::DenyPolicyExpired;
}
if policy.managed_extensions.contains(extension_id) {
AgentTaskExtensionDecision::AllowManagedExtension
} else {
AgentTaskExtensionDecision::DenyNotManaged
}
}

/// The result of evaluating one typed action request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Expand Down
147 changes: 147 additions & 0 deletions crates/originweave-policy/tests/agent_task_extension_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#![allow(clippy::expect_used)]

use originweave_core::{
BrowserSessionId, BrowsingContextId, ExtensionAccessDecision, ExtensionAccessRequest,
ExtensionAgentCapability, ExtensionId, Origin, evaluate_extension_access,
};
use originweave_policy::{
AgentTaskExtensionDecision, AgentTaskExtensionPolicy, evaluate_agent_task_extension,
};

fn extension_id(value: &str) -> ExtensionId {
ExtensionId::parse(value).expect("valid extension identifier")
}

fn session(value: u64) -> BrowserSessionId {
BrowserSessionId::new(value).expect("nonzero browser session")
}

fn context(value: u64) -> BrowsingContextId {
BrowsingContextId::new(value).expect("nonzero browsing context")
}

fn origin() -> Origin {
Origin::parse("https://app.example").expect("valid controlled origin")
}

#[test]
fn empty_agent_task_extension_policy_denies_every_extension() {
let policy = AgentTaskExtensionPolicy::new([], 10, 20, 10);
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");

assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 15),
AgentTaskExtensionDecision::DenyNotManaged
);
}

#[test]
fn managed_agent_task_extension_policy_allows_only_exact_identifiers() {
let allowed = extension_id("abcdefghijklmnopabcdefghijklmnop");
let other = extension_id("bcdefghijklmnopabcdefghijklmnopa");
let policy = AgentTaskExtensionPolicy::new([allowed.clone(), allowed.clone()], 10, 20, 10);

assert_eq!(
evaluate_agent_task_extension(&allowed, &policy, 10),
AgentTaskExtensionDecision::AllowManagedExtension
);
assert_eq!(
evaluate_agent_task_extension(&other, &policy, 19),
AgentTaskExtensionDecision::DenyNotManaged
);
}

#[test]
fn managed_agent_task_extension_policy_fails_closed_outside_its_validity_window() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let policy = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20, 10);

assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 9),
AgentTaskExtensionDecision::DenyPolicyNotYetValid
);
assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 20),
AgentTaskExtensionDecision::DenyPolicyExpired
);
assert_eq!(
evaluate_agent_task_extension(&extension, &policy, u64::MAX),
AgentTaskExtensionDecision::DenyPolicyExpired
);
}

#[test]
fn invalid_managed_extension_policy_window_fails_closed_before_membership() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let reversed = AgentTaskExtensionPolicy::new([extension.clone()], 20, 10, 10);
let empty = AgentTaskExtensionPolicy::new([extension.clone()], 20, 20, 10);
let zero_maximum = AgentTaskExtensionPolicy::new([extension.clone()], 20, 21, 0);

assert_eq!(
evaluate_agent_task_extension(&extension, &reversed, 15),
AgentTaskExtensionDecision::DenyInvalidPolicyWindow
);
assert_eq!(
evaluate_agent_task_extension(&extension, &empty, 20),
AgentTaskExtensionDecision::DenyInvalidPolicyWindow
);
assert_eq!(
evaluate_agent_task_extension(&extension, &zero_maximum, 20),
AgentTaskExtensionDecision::DenyInvalidPolicyWindow
);
}

#[test]
fn managed_extension_policy_window_cannot_exceed_local_maximum() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let exact = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20, 10);
let overlong = AgentTaskExtensionPolicy::new([extension.clone()], 10, 21, 10);

assert_eq!(
evaluate_agent_task_extension(&extension, &exact, 19),
AgentTaskExtensionDecision::AllowManagedExtension
);
assert_eq!(
evaluate_agent_task_extension(&extension, &overlong, 19),
AgentTaskExtensionDecision::DenyPolicyWindowExceedsMaximum
);
}

#[test]
fn maximum_timestamp_window_remains_half_open_without_overflow() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let policy = AgentTaskExtensionPolicy::new([extension.clone()], u64::MAX - 1, u64::MAX, 1);

assert_eq!(
evaluate_agent_task_extension(&extension, &policy, u64::MAX - 1),
AgentTaskExtensionDecision::AllowManagedExtension
);
assert_eq!(
evaluate_agent_task_extension(&extension, &policy, u64::MAX),
AgentTaskExtensionDecision::DenyPolicyExpired
);
}

#[test]
fn managed_agent_task_extension_admission_does_not_mint_agent_capability() {
let extension = extension_id("abcdefghijklmnopabcdefghijklmnop");
let policy = AgentTaskExtensionPolicy::new([extension.clone()], 10, 20, 10);

assert_eq!(
evaluate_agent_task_extension(&extension, &policy, 15),
AgentTaskExtensionDecision::AllowManagedExtension
);

let request = ExtensionAccessRequest::new(
extension,
session(31),
context(37),
origin(),
100,
ExtensionAgentCapability::ProposeTypedAction,
);
assert_eq!(
evaluate_extension_access(&request, None),
ExtensionAccessDecision::DenyMissingGrant
);
}
Loading