chore: merge develop into hare/develop

This commit is contained in:
2026-10-03 19:14:17 +09:00
147 changed files with 33175 additions and 2270 deletions
+7
View File
@@ -108,6 +108,13 @@ pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[
description: "Internal Memory consolidation service.",
imports: BASE_IMPORT,
},
BuiltinProfileResource {
selector: Some("builtin:subjektiv-memory-consolidation"),
path: "profiles/subjektiv-memory-consolidation.dcdl",
source: include_str!("../../../resources/profiles/subjektiv-memory-consolidation.dcdl"),
description: "Delegated subject Memory consolidation service.",
imports: BASE_IMPORT,
},
];
#[derive(Debug, Clone, PartialEq, Eq)]
+104 -3
View File
@@ -19,9 +19,10 @@ use crate::{
CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits,
McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConsolidationProfileConfig,
MemoryExtractionProfileConfig, MemoryFeatureProfileConfig, MemoryResidentProfileConfig,
MergeRequestFeatureConfig, ResolvedMemoryFeatureConfig, ScopeConfig, SessionConfig,
SkillsConfig, TicketFeatureConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule,
WebConfig, WorkerFeatureConfig, WorkerManifest, WorkerMeta,
MergeRequestFeatureConfig, ResolvedMemoryFeatureConfig, ResolvedSubjektivFeatureConfig,
ScopeConfig, SessionConfig, SkillsConfig, SubjektivFeatureProfileConfig, TicketFeatureConfig,
ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WebConfig, WorkerFeatureConfig,
WorkerManifest, WorkerMeta,
};
/// Partial-form Worker manifest. Every field is optional; one or more
@@ -76,6 +77,8 @@ pub struct FeatureConfigPartial {
#[serde(default)]
pub memory: Option<MemoryFeatureConfigPartial>,
#[serde(default)]
pub subjektiv: Option<SubjektivFeatureConfigPartial>,
#[serde(default)]
pub web: Option<FeatureFlagConfigPartial>,
#[serde(default)]
pub image: Option<FeatureFlagConfigPartial>,
@@ -104,6 +107,11 @@ impl FeatureConfigPartial {
Self {
task: merge_option(self.task, other.task, FeatureFlagConfigPartial::merge),
memory: merge_option(self.memory, other.memory, MemoryFeatureConfigPartial::merge),
subjektiv: merge_option(
self.subjektiv,
other.subjektiv,
SubjektivFeatureConfigPartial::merge,
),
web: merge_option(self.web, other.web, FeatureFlagConfigPartial::merge),
image: merge_option(self.image, other.image, FeatureFlagConfigPartial::merge),
sub_worker: merge_option(
@@ -227,6 +235,28 @@ pub struct MemoryConsolidationProfileConfigPartial {
pub request_enabled: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SubjektivFeatureConfigPartial {
#[serde(default)]
pub enabled: Option<bool>,
#[serde(default)]
pub extraction: Option<MemoryExtractionProfileConfigPartial>,
}
impl SubjektivFeatureConfigPartial {
fn merge(self, other: Self) -> Self {
Self {
enabled: other.enabled.or(self.enabled),
extraction: merge_option(
self.extraction,
other.extraction,
MemoryExtractionProfileConfigPartial::merge,
),
}
}
}
impl MemoryFeatureConfigPartial {
fn merge(self, other: Self) -> Self {
Self {
@@ -331,6 +361,10 @@ impl From<FeatureConfigPartial> for FeatureConfig {
.memory
.map(ResolvedMemoryFeatureConfig::from)
.unwrap_or_default(),
subjektiv: value
.subjektiv
.map(ResolvedSubjektivFeatureConfig::from)
.unwrap_or_default(),
web: value.web.map(FeatureFlagConfig::from).unwrap_or_default(),
image: value.image.map(FeatureFlagConfig::from).unwrap_or_default(),
sub_worker: value
@@ -447,6 +481,42 @@ impl From<ResolvedMemoryFeatureConfig> for MemoryFeatureConfigPartial {
}
}
impl From<SubjektivFeatureConfigPartial> for ResolvedSubjektivFeatureConfig {
fn from(value: SubjektivFeatureConfigPartial) -> Self {
let extraction = value.extraction.unwrap_or_default();
Self {
profile: SubjektivFeatureProfileConfig {
enabled: value.enabled.unwrap_or_default(),
extraction: MemoryExtractionProfileConfig {
enabled: extraction.enabled.unwrap_or(true),
model: extraction.model,
reasoning: extraction.reasoning,
threshold: extraction.threshold.or(Some(50_000)),
worker_max_turns: extraction
.worker_max_turns
.or(defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS),
},
},
workspace_settings: None,
}
}
}
impl From<ResolvedSubjektivFeatureConfig> for SubjektivFeatureConfigPartial {
fn from(value: ResolvedSubjektivFeatureConfig) -> Self {
Self {
enabled: Some(value.profile.enabled),
extraction: Some(MemoryExtractionProfileConfigPartial {
enabled: Some(value.profile.extraction.enabled),
model: value.profile.extraction.model,
reasoning: value.profile.extraction.reasoning,
threshold: value.profile.extraction.threshold,
worker_max_turns: value.profile.extraction.worker_max_turns,
}),
}
}
}
impl From<TicketFeatureConfigPartial> for TicketFeatureConfig {
fn from(value: TicketFeatureConfigPartial) -> Self {
Self {
@@ -500,6 +570,7 @@ impl From<FeatureConfig> for FeatureConfigPartial {
Self {
task: Some(value.task.into()),
memory: Some(value.memory.into()),
subjektiv: Some(value.subjektiv.into()),
web: Some(value.web.into()),
image: Some(value.image.into()),
sub_worker: Some(value.sub_worker.into()),
@@ -2490,6 +2561,36 @@ directories = [".claude/skills", ".cursor/skills"]
);
}
#[test]
fn subjektiv_config_is_explicit_and_roundtrips_without_subject_identity() {
let config = WorkerManifestConfig::from_toml(
r#"
[worker]
name = "subject-worker"
[feature.subjektiv]
enabled = true
[feature.subjektiv.extraction]
enabled = true
threshold = 321
worker_max_turns = 7
"#,
)
.unwrap();
let partial = config.feature.subjektiv.unwrap();
assert_eq!(partial.enabled, Some(true));
assert_eq!(partial.extraction.as_ref().unwrap().threshold, Some(321));
let resolved = ResolvedSubjektivFeatureConfig::from(partial);
assert!(resolved.profile.enabled);
assert_eq!(resolved.profile.extraction.threshold, Some(321));
assert_eq!(resolved.profile.extraction.worker_max_turns, Some(7));
let encoded = serde_json::to_value(SubjektivFeatureConfigPartial::from(resolved)).unwrap();
assert!(encoded.get("subject_id").is_none());
assert_eq!(encoded["enabled"], true);
}
#[test]
fn merge_preserves_ref() {
let lower = WorkerManifestConfig {
+109 -1
View File
@@ -108,6 +108,11 @@ pub struct FeatureConfig {
pub task: FeatureFlagConfig,
#[serde(default)]
pub memory: ResolvedMemoryFeatureConfig,
/// Subject-scoped Memory extraction. Subject identity is resolved by the
/// Workspace host from the Worker's keyed singleton lease, never from this
/// model-visible manifest.
#[serde(default)]
pub subjektiv: ResolvedSubjektivFeatureConfig,
#[serde(default)]
pub web: FeatureFlagConfig,
#[serde(default)]
@@ -139,6 +144,7 @@ impl Default for FeatureConfig {
Self {
task: FeatureFlagConfig::disabled(),
memory: ResolvedMemoryFeatureConfig::default(),
subjektiv: ResolvedSubjektivFeatureConfig::default(),
web: FeatureFlagConfig::disabled(),
image: FeatureFlagConfig::disabled(),
sub_worker: FeatureFlagConfig::disabled(),
@@ -350,6 +356,85 @@ impl ResolvedMemoryFeatureConfig {
}
}
/// Profile-owned subjektiv activation and extraction policy. The subject id is
/// deliberately absent: the Workspace host binds it from `subjektiv:<id>`
/// singleton ownership for each operation.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default, deny_unknown_fields)]
pub struct SubjektivFeatureProfileConfig {
pub enabled: bool,
pub extraction: MemoryExtractionProfileConfig,
}
impl Default for SubjektivFeatureProfileConfig {
fn default() -> Self {
Self {
enabled: false,
extraction: MemoryExtractionProfileConfig::default(),
}
}
}
/// Immutable subjektiv execution configuration persisted in a resolved Worker
/// Manifest. Workspace settings provide only trusted extraction language and
/// are not subject identity.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(default, deny_unknown_fields)]
pub struct ResolvedSubjektivFeatureConfig {
pub profile: SubjektivFeatureProfileConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_settings: Option<WorkspaceMemorySettingsSnapshot>,
}
impl ResolvedSubjektivFeatureConfig {
pub fn enabled(&self) -> bool {
self.profile.enabled
}
/// Returns whether the host attached this profile policy to trusted subject
/// authority. Profile enablement without a settings snapshot is inert.
pub fn execution_enabled(&self) -> bool {
self.profile.enabled && self.workspace_settings.is_some()
}
pub fn bind_workspace_settings(
&mut self,
settings: WorkspaceMemorySettingsSnapshot,
) -> Result<(), &'static str> {
if !self.profile.enabled {
if self.workspace_settings.is_some() {
return Err("disabled subjektiv feature must not carry Workspace settings");
}
return Ok(());
}
if self.workspace_settings.is_some() {
return Err("subjektiv Workspace settings are already bound");
}
self.workspace_settings = Some(settings);
Ok(())
}
pub fn workspace_settings(&self) -> Option<WorkspaceMemorySettingsSnapshot> {
self.workspace_settings.clone()
}
pub fn validate_execution(&self) -> Result<(), &'static str> {
// `profile.enabled` is reusable policy. Only a host-bound settings
// snapshot activates subject execution; an ordinary Worker or Internal
// SubWorker with the same Profile remains inert.
if !self.profile.enabled && self.workspace_settings.is_some() {
return Err("disabled subjektiv feature must not carry Workspace settings");
}
if let Some(settings) = &self.workspace_settings
&& (settings.settings_revision == 0
|| !is_normalized_workspace_memory_language(&settings.language))
{
return Err("subjektiv Workspace settings snapshot metadata is invalid");
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct TicketFeatureConfig {
#[serde(default)]
@@ -963,7 +1048,9 @@ impl Default for CompactionConfig {
impl WorkerManifest {
pub fn requires_persisted_execution_snapshot(&self) -> bool {
self.profile.is_some() || self.feature.memory.workspace_settings.is_some()
self.profile.is_some()
|| self.feature.memory.workspace_settings.is_some()
|| self.feature.subjektiv.workspace_settings.is_some()
}
/// Parse a manifest from a TOML string.
@@ -1638,6 +1725,27 @@ model_id = "claude-sonnet-4-20250514"
assert!(disabled.bind_workspace_settings(snapshot).is_err());
}
#[test]
fn resolved_subjektiv_policy_is_inert_until_trusted_attachment() {
let snapshot = WorkspaceMemorySettingsSnapshot {
workspace_id: "workspace-1".to_string(),
settings_revision: 1,
language: "English".to_string(),
};
let mut enabled = ResolvedSubjektivFeatureConfig::default();
enabled.profile.enabled = true;
assert!(enabled.validate_execution().is_ok());
assert!(!enabled.execution_enabled());
enabled.bind_workspace_settings(snapshot.clone()).unwrap();
assert!(enabled.validate_execution().is_ok());
assert!(enabled.execution_enabled());
let mut disabled = ResolvedSubjektivFeatureConfig::default();
disabled.workspace_settings = Some(snapshot.clone());
assert!(disabled.validate_execution().is_err());
assert!(disabled.bind_workspace_settings(snapshot).is_err());
}
#[test]
fn current_manifest_rejects_legacy_top_level_memory_authority() {
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\nlanguage = \"Japanese\"\n");
+123 -9
View File
@@ -144,6 +144,7 @@ pub enum WorkspaceAuthorityRequirement {
Flow,
ManageWorkdir,
Memory,
Subjektiv,
MergeRequest,
Objective,
Orchestration,
@@ -157,6 +158,7 @@ impl fmt::Display for WorkspaceAuthorityRequirement {
Self::Flow => formatter.write_str("feature.flow"),
Self::ManageWorkdir => formatter.write_str("feature.manage_workdir"),
Self::Memory => formatter.write_str("feature.memory"),
Self::Subjektiv => formatter.write_str("feature.subjektiv"),
Self::MergeRequest => formatter.write_str("feature.merge_request"),
Self::Objective => formatter.write_str("feature.objective"),
Self::Orchestration => formatter.write_str("feature.orchestration"),
@@ -185,6 +187,9 @@ pub fn validate_profile_execution_target(
if feature.memory.profile.enabled || feature.memory.profile.staging_tools {
requirements.insert(WorkspaceAuthorityRequirement::Memory);
}
if feature.subjektiv.profile.enabled {
requirements.insert(WorkspaceAuthorityRequirement::Subjektiv);
}
if feature.merge_request.show
|| feature.merge_request.open
|| feature.merge_request.review
@@ -1252,6 +1257,64 @@ mod tests {
}
}
#[test]
fn builtin_workspace_profiles_cut_over_to_subjektiv_without_legacy_memory() {
let tmp = TempDir::new().unwrap();
let resolve = |name: &str| {
ProfileResolver::new()
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, name),
ProfileResolveOptions::with_worker_name(format!("{name}-worker")),
)
.unwrap()
.manifest
};
for name in ["coder", "companion", "intake", "reviewer", "orchestrator"] {
let manifest = resolve(name);
assert!(
manifest.feature.subjektiv.profile.enabled,
"{name} must enable subjektiv"
);
assert!(
manifest.feature.subjektiv.profile.extraction.enabled,
"{name} must enable subject extraction"
);
assert!(
!manifest.feature.memory.profile.enabled,
"{name} must not enable legacy Workspace Memory"
);
}
for name in ["default", "standalone", "backend-job"] {
let manifest = resolve(name);
assert!(!manifest.feature.memory.profile.enabled, "{name}");
assert!(!manifest.feature.subjektiv.profile.enabled, "{name}");
}
let legacy_consolidator = resolve("memory-consolidation");
assert!(legacy_consolidator.feature.memory.profile.enabled);
assert!(legacy_consolidator.feature.memory.profile.staging_tools);
assert!(
legacy_consolidator
.feature
.memory
.profile
.extraction
.enabled
);
assert!(
legacy_consolidator
.feature
.memory
.profile
.consolidation
.request_enabled
);
assert!(!legacy_consolidator.feature.subjektiv.profile.enabled);
}
#[test]
fn builtin_profiles_pin_role_models_and_reasoning() {
use crate::model::{ReasoningControl, ReasoningEffort};
@@ -1276,6 +1339,7 @@ mod tests {
"coder",
"reviewer",
"memory-consolidation",
"subjektiv-memory-consolidation",
] {
let role = resolve(name);
assert_eq!(
@@ -1308,7 +1372,7 @@ mod tests {
role.manifest.engine.reasoning,
Some(ReasoningControl::Effort(ReasoningEffort::High))
);
let extraction = &role.manifest.feature.memory.profile.extraction;
let extraction = &role.manifest.feature.subjektiv.profile.extraction;
assert_eq!(
extraction
.model
@@ -1322,14 +1386,63 @@ mod tests {
);
}
let consolidation = resolve("memory-consolidation");
for name in ["memory-consolidation", "subjektiv-memory-consolidation"] {
let consolidation = resolve(name);
assert_eq!(
consolidation.manifest.model.ref_.as_deref(),
Some("codex-oauth/gpt-5.6-luna")
);
assert_eq!(
consolidation.manifest.engine.reasoning,
Some(ReasoningControl::Effort(ReasoningEffort::Medium))
);
}
}
#[test]
fn subjektiv_memory_consolidation_profile_is_lifecycle_and_capability_isolated() {
let tmp = TempDir::new().unwrap();
let resolved = ProfileResolver::new()
.with_workspace_base(tmp.path())
.resolve(
&ProfileSelector::source_named(
ProfileRegistrySource::Builtin,
"subjektiv-memory-consolidation",
),
ProfileResolveOptions::with_worker_name("subject-consolidator"),
)
.unwrap();
let manifest = resolved.manifest;
let feature = &manifest.feature;
assert!(!feature.memory.profile.enabled);
assert!(!feature.memory.profile.staging_tools);
assert!(feature.subjektiv.profile.enabled);
assert!(!feature.subjektiv.profile.extraction.enabled);
assert!(!feature.task.enabled);
assert!(!feature.web.enabled);
assert!(!feature.image.enabled);
assert!(!feature.sub_worker.enabled);
assert!(!feature.flow.enabled);
assert!(!feature.worker.enabled);
assert!(!feature.worker.direct_spawn);
assert!(!feature.workspace_worker_discovery.enabled);
assert!(!feature.objective.enabled);
assert!(!feature.manage_workdir.enabled);
assert!(!feature.ticket.enabled);
assert!(!feature.ticket.authoring);
assert!(!feature.ticket.thread);
assert!(!feature.ticket.intake);
assert!(!feature.ticket.workflow);
assert!(!feature.merge_request.show);
assert!(!feature.merge_request.open);
assert!(!feature.merge_request.review);
assert!(!feature.merge_request.readiness_check);
assert!(!feature.merge_request.complete);
assert!(!feature.orchestration.enabled);
assert_eq!(
consolidation.manifest.model.ref_.as_deref(),
Some("codex-oauth/gpt-5.6-luna")
);
assert_eq!(
consolidation.manifest.engine.reasoning,
Some(ReasoningControl::Effort(ReasoningEffort::Medium))
manifest.web.as_ref().and_then(|web| web.enabled),
Some(false)
);
}
@@ -1477,7 +1590,8 @@ mod tests {
panic!("unexpected error: {error}");
};
assert_eq!(target, ProfileExecutionTarget::Standalone);
assert!(requirements.contains(&WorkspaceAuthorityRequirement::Memory));
assert!(requirements.contains(&WorkspaceAuthorityRequirement::Subjektiv));
assert!(!requirements.contains(&WorkspaceAuthorityRequirement::Memory));
assert!(requirements.contains(&WorkspaceAuthorityRequirement::MergeRequest));
assert!(requirements.contains(&WorkspaceAuthorityRequirement::Ticket));
assert!(!diagnostic.contains(tmp.path().to_string_lossy().as_ref()));