feat: gate built-in tools by profile features
This commit is contained in:
parent
41133e0cd5
commit
f0f6cc92d8
|
|
@ -29,6 +29,15 @@ return function(opts)
|
||||||
worker_context_max_tokens = 100000,
|
worker_context_max_tokens = 100000,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
feature = opts.feature or {
|
||||||
|
task = { enabled = true },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pod_management = { enabled = false },
|
||||||
|
ticket = { enabled = false, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = false },
|
||||||
|
},
|
||||||
|
|
||||||
memory = {
|
memory = {
|
||||||
extract_threshold = 50000,
|
extract_threshold = 50000,
|
||||||
consolidation_threshold_files = 5,
|
consolidation_threshold_files = 5,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,14 @@ return base {
|
||||||
slug = "coder",
|
slug = "coder",
|
||||||
description = "Coder role profile: GPT-5.5 with bundled default behavior",
|
description = "Coder role profile: GPT-5.5 with bundled default behavior",
|
||||||
model_ref = "codex-oauth/gpt-5.5",
|
model_ref = "codex-oauth/gpt-5.5",
|
||||||
|
feature = {
|
||||||
|
task = { enabled = true },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pod_management = { enabled = false },
|
||||||
|
ticket = { enabled = false, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = false },
|
||||||
|
},
|
||||||
language = "Japanese",
|
language = "Japanese",
|
||||||
scope = scope.workspace_write(),
|
scope = scope.workspace_write(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,13 @@ return base {
|
||||||
slug = "companion",
|
slug = "companion",
|
||||||
description = "Companion role profile: GPT-5.5 with bundled default behavior",
|
description = "Companion role profile: GPT-5.5 with bundled default behavior",
|
||||||
model_ref = "codex-oauth/gpt-5.5",
|
model_ref = "codex-oauth/gpt-5.5",
|
||||||
|
feature = {
|
||||||
|
task = { enabled = true },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pod_management = { enabled = false },
|
||||||
|
ticket = { enabled = false, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = false },
|
||||||
|
},
|
||||||
language = "Japanese",
|
language = "Japanese",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,13 @@ return base {
|
||||||
slug = "intake",
|
slug = "intake",
|
||||||
description = "Intake role profile: GPT-5.5 with bundled default behavior",
|
description = "Intake role profile: GPT-5.5 with bundled default behavior",
|
||||||
model_ref = "codex-oauth/gpt-5.5",
|
model_ref = "codex-oauth/gpt-5.5",
|
||||||
|
feature = {
|
||||||
|
task = { enabled = true },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pod_management = { enabled = false },
|
||||||
|
ticket = { enabled = true, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = false },
|
||||||
|
},
|
||||||
language = "Japanese",
|
language = "Japanese",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,14 @@ return base {
|
||||||
slug = "orchestrator",
|
slug = "orchestrator",
|
||||||
description = "Orchestrator role profile: GPT-5.5 with bundled default behavior",
|
description = "Orchestrator role profile: GPT-5.5 with bundled default behavior",
|
||||||
delegation_scope = scope.workspace_write(),
|
delegation_scope = scope.workspace_write(),
|
||||||
|
feature = {
|
||||||
|
task = { enabled = true },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pod_management = { enabled = true },
|
||||||
|
ticket = { enabled = true, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = true },
|
||||||
|
},
|
||||||
model_ref = "codex-oauth/gpt-5.5",
|
model_ref = "codex-oauth/gpt-5.5",
|
||||||
language = "Japanese",
|
language = "Japanese",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,13 @@ return base {
|
||||||
slug = "reviewer",
|
slug = "reviewer",
|
||||||
description = "Reviewer role profile: GPT-5.5 with bundled default behavior",
|
description = "Reviewer role profile: GPT-5.5 with bundled default behavior",
|
||||||
model_ref = "codex-oauth/gpt-5.5",
|
model_ref = "codex-oauth/gpt-5.5",
|
||||||
|
feature = {
|
||||||
|
task = { enabled = true },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pod_management = { enabled = false },
|
||||||
|
ticket = { enabled = false, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = false },
|
||||||
|
},
|
||||||
language = "Japanese",
|
language = "Japanese",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::defaults;
|
use crate::defaults;
|
||||||
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
|
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
|
||||||
use crate::{
|
use crate::{
|
||||||
CompactionConfig, FileUploadLimits, MemoryConfig, PodManifest, PodMeta, ScopeConfig,
|
CompactionConfig, FeatureConfig, FeatureFlagConfig, FileUploadLimits, MemoryConfig,
|
||||||
SessionConfig, SkillsConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule,
|
PodManifest, PodMeta, ScopeConfig, SessionConfig, SkillsConfig, TicketFeatureAccessConfig,
|
||||||
WebConfig, WorkerManifest,
|
TicketFeatureConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WebConfig,
|
||||||
|
WorkerManifest,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Partial-form Pod manifest. Every field is optional; one or more
|
/// Partial-form Pod manifest. Every field is optional; one or more
|
||||||
|
|
@ -47,6 +48,10 @@ pub struct PodManifestConfig {
|
||||||
/// is disabled; `Some` requires `default_action` during final resolve.
|
/// is disabled; `Some` requires `default_action` during final resolve.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub permissions: Option<PermissionConfigPartial>,
|
pub permissions: Option<PermissionConfigPartial>,
|
||||||
|
/// Explicit built-in feature/tool-surface enablement. Absent flags resolve
|
||||||
|
/// disabled after cascade merge.
|
||||||
|
#[serde(default)]
|
||||||
|
pub feature: FeatureConfigPartial,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub compaction: Option<CompactionConfigPartial>,
|
pub compaction: Option<CompactionConfigPartial>,
|
||||||
/// First-class web tool opt-in. See [`WebConfig`].
|
/// First-class web tool opt-in. See [`WebConfig`].
|
||||||
|
|
@ -60,6 +65,146 @@ pub struct PodManifestConfig {
|
||||||
pub skills: Option<SkillsConfig>,
|
pub skills: Option<SkillsConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct FeatureConfigPartial {
|
||||||
|
#[serde(default)]
|
||||||
|
pub task: Option<FeatureFlagConfigPartial>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub memory: Option<FeatureFlagConfigPartial>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub web: Option<FeatureFlagConfigPartial>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub pod_management: Option<FeatureFlagConfigPartial>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ticket: Option<TicketFeatureConfigPartial>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ticket_orchestration: Option<FeatureFlagConfigPartial>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureConfigPartial {
|
||||||
|
fn merge(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
task: merge_option(self.task, other.task, FeatureFlagConfigPartial::merge),
|
||||||
|
memory: merge_option(self.memory, other.memory, FeatureFlagConfigPartial::merge),
|
||||||
|
web: merge_option(self.web, other.web, FeatureFlagConfigPartial::merge),
|
||||||
|
pod_management: merge_option(
|
||||||
|
self.pod_management,
|
||||||
|
other.pod_management,
|
||||||
|
FeatureFlagConfigPartial::merge,
|
||||||
|
),
|
||||||
|
ticket: merge_option(self.ticket, other.ticket, TicketFeatureConfigPartial::merge),
|
||||||
|
ticket_orchestration: merge_option(
|
||||||
|
self.ticket_orchestration,
|
||||||
|
other.ticket_orchestration,
|
||||||
|
FeatureFlagConfigPartial::merge,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct FeatureFlagConfigPartial {
|
||||||
|
#[serde(default)]
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureFlagConfigPartial {
|
||||||
|
fn merge(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: other.enabled.or(self.enabled),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct TicketFeatureConfigPartial {
|
||||||
|
#[serde(default)]
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub access: Option<TicketFeatureAccessConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TicketFeatureConfigPartial {
|
||||||
|
fn merge(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: other.enabled.or(self.enabled),
|
||||||
|
access: other.access.or(self.access),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FeatureConfigPartial> for FeatureConfig {
|
||||||
|
fn from(value: FeatureConfigPartial) -> Self {
|
||||||
|
Self {
|
||||||
|
task: value.task.map(FeatureFlagConfig::from).unwrap_or_default(),
|
||||||
|
memory: value
|
||||||
|
.memory
|
||||||
|
.map(FeatureFlagConfig::from)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
web: value.web.map(FeatureFlagConfig::from).unwrap_or_default(),
|
||||||
|
pod_management: value
|
||||||
|
.pod_management
|
||||||
|
.map(FeatureFlagConfig::from)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
ticket: value
|
||||||
|
.ticket
|
||||||
|
.map(TicketFeatureConfig::from)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
ticket_orchestration: value
|
||||||
|
.ticket_orchestration
|
||||||
|
.map(FeatureFlagConfig::from)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FeatureFlagConfigPartial> for FeatureFlagConfig {
|
||||||
|
fn from(value: FeatureFlagConfigPartial) -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: value.enabled.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FeatureFlagConfig> for FeatureFlagConfigPartial {
|
||||||
|
fn from(value: FeatureFlagConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: Some(value.enabled),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TicketFeatureConfigPartial> for TicketFeatureConfig {
|
||||||
|
fn from(value: TicketFeatureConfigPartial) -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: value.enabled.unwrap_or_default(),
|
||||||
|
access: value.access.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TicketFeatureConfig> for TicketFeatureConfigPartial {
|
||||||
|
fn from(value: TicketFeatureConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: Some(value.enabled),
|
||||||
|
access: Some(value.access),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FeatureConfig> for FeatureConfigPartial {
|
||||||
|
fn from(value: FeatureConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
task: Some(value.task.into()),
|
||||||
|
memory: Some(value.memory.into()),
|
||||||
|
web: Some(value.web.into()),
|
||||||
|
pod_management: Some(value.pod_management.into()),
|
||||||
|
ticket: Some(value.ticket.into()),
|
||||||
|
ticket_orchestration: Some(value.ticket_orchestration.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct PodMetaConfig {
|
pub struct PodMetaConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -305,6 +450,7 @@ impl PodManifestConfig {
|
||||||
upper.permissions,
|
upper.permissions,
|
||||||
PermissionConfigPartial::merge,
|
PermissionConfigPartial::merge,
|
||||||
),
|
),
|
||||||
|
feature: self.feature.merge(upper.feature),
|
||||||
compaction: merge_option(
|
compaction: merge_option(
|
||||||
self.compaction,
|
self.compaction,
|
||||||
upper.compaction,
|
upper.compaction,
|
||||||
|
|
@ -690,6 +836,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
||||||
delegation_scope: cfg.delegation_scope,
|
delegation_scope: cfg.delegation_scope,
|
||||||
session,
|
session,
|
||||||
permissions,
|
permissions,
|
||||||
|
feature: FeatureConfig::from(cfg.feature),
|
||||||
compaction,
|
compaction,
|
||||||
web: cfg.web,
|
web: cfg.web,
|
||||||
memory: cfg.memory,
|
memory: cfg.memory,
|
||||||
|
|
@ -735,6 +882,7 @@ mod tests {
|
||||||
},
|
},
|
||||||
delegation_scope: ScopeConfig::default(),
|
delegation_scope: ScopeConfig::default(),
|
||||||
permissions: None,
|
permissions: None,
|
||||||
|
feature: FeatureConfigPartial::default(),
|
||||||
session: None,
|
session: None,
|
||||||
compaction: None,
|
compaction: None,
|
||||||
web: None,
|
web: None,
|
||||||
|
|
@ -1280,6 +1428,125 @@ worker_max_turns = 7
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn feature_flags_default_disabled_in_resolved_manifest() {
|
||||||
|
let manifest: PodManifest = minimal_valid().try_into().unwrap();
|
||||||
|
assert!(!manifest.feature.task.enabled);
|
||||||
|
assert!(!manifest.feature.memory.enabled);
|
||||||
|
assert!(!manifest.feature.web.enabled);
|
||||||
|
assert!(!manifest.feature.pod_management.enabled);
|
||||||
|
assert!(!manifest.feature.ticket.enabled);
|
||||||
|
assert!(!manifest.feature.ticket_orchestration.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_toml_parses_explicit_feature_flags() {
|
||||||
|
let cfg = PodManifestConfig::from_toml(
|
||||||
|
r#"
|
||||||
|
[feature.task]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[feature.ticket]
|
||||||
|
enabled = true
|
||||||
|
access = "read_only"
|
||||||
|
|
||||||
|
[feature.ticket_orchestration]
|
||||||
|
enabled = true
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let manifest: PodManifest = PodManifestConfig::builtin_defaults()
|
||||||
|
.merge(cfg)
|
||||||
|
.merge(PodManifestConfig {
|
||||||
|
pod: PodMetaConfig {
|
||||||
|
name: Some("feature-test".into()),
|
||||||
|
prompt_pack: None,
|
||||||
|
},
|
||||||
|
model: ModelManifest {
|
||||||
|
scheme: Some(SchemeKind::Anthropic),
|
||||||
|
model_id: Some("m".into()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
scope: ScopeConfig {
|
||||||
|
allow: vec![ScopeRule {
|
||||||
|
target: abs("/pod"),
|
||||||
|
permission: Permission::Read,
|
||||||
|
recursive: true,
|
||||||
|
}],
|
||||||
|
deny: Vec::new(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
assert!(manifest.feature.task.enabled);
|
||||||
|
assert!(manifest.feature.ticket.enabled);
|
||||||
|
assert_eq!(
|
||||||
|
manifest.feature.ticket.access,
|
||||||
|
TicketFeatureAccessConfig::ReadOnly
|
||||||
|
);
|
||||||
|
assert!(manifest.feature.ticket_orchestration.enabled);
|
||||||
|
assert!(!manifest.feature.memory.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn feature_flags_merge_as_partial_profile_layers() {
|
||||||
|
let base = PodManifestConfig::from_toml(
|
||||||
|
r#"
|
||||||
|
[feature.memory]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[feature.ticket]
|
||||||
|
enabled = true
|
||||||
|
access = "read_only"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let upper = PodManifestConfig::from_toml(
|
||||||
|
r#"
|
||||||
|
[feature.ticket]
|
||||||
|
access = "lifecycle"
|
||||||
|
|
||||||
|
[feature.web]
|
||||||
|
enabled = true
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let manifest: PodManifest = PodManifestConfig::builtin_defaults()
|
||||||
|
.merge(base)
|
||||||
|
.merge(upper)
|
||||||
|
.merge(PodManifestConfig {
|
||||||
|
pod: PodMetaConfig {
|
||||||
|
name: Some("feature-merge-test".into()),
|
||||||
|
prompt_pack: None,
|
||||||
|
},
|
||||||
|
model: ModelManifest {
|
||||||
|
scheme: Some(SchemeKind::Anthropic),
|
||||||
|
model_id: Some("m".into()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
scope: ScopeConfig {
|
||||||
|
allow: vec![ScopeRule {
|
||||||
|
target: abs("/pod"),
|
||||||
|
permission: Permission::Read,
|
||||||
|
recursive: true,
|
||||||
|
}],
|
||||||
|
deny: Vec::new(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
assert!(manifest.feature.memory.enabled);
|
||||||
|
assert!(manifest.feature.ticket.enabled);
|
||||||
|
assert_eq!(
|
||||||
|
manifest.feature.ticket.access,
|
||||||
|
TicketFeatureAccessConfig::Lifecycle
|
||||||
|
);
|
||||||
|
assert!(manifest.feature.web.enabled);
|
||||||
|
assert!(!manifest.feature.pod_management.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_toml_partial_layer_succeeds() {
|
fn from_toml_partial_layer_succeeds() {
|
||||||
// A project-layer manifest with only scope set must parse fine.
|
// A project-layer manifest with only scope set must parse fine.
|
||||||
|
|
|
||||||
|
|
@ -53,18 +53,20 @@ pub struct PodManifest {
|
||||||
/// permission layer is disabled and tool calls run as before.
|
/// permission layer is disabled and tool calls run as before.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub permissions: Option<ToolPermissionConfig>,
|
pub permissions: Option<ToolPermissionConfig>,
|
||||||
|
/// Explicit built-in feature/tool-surface enablement. Omitted feature flags
|
||||||
|
/// resolve disabled so Profile authors choose the exposed built-in surfaces.
|
||||||
|
#[serde(default)]
|
||||||
|
pub feature: FeatureConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub compaction: Option<CompactionConfig>,
|
pub compaction: Option<CompactionConfig>,
|
||||||
/// Memory subsystem opt-in. Presence of `[memory]` in TOML enables
|
/// Memory subsystem configuration. Presence of `[memory]` configures memory
|
||||||
/// the memory tools (MemoryRead / MemoryWrite / MemoryEdit) and
|
/// storage, extraction, consolidation, and resident injection, but memory
|
||||||
/// causes Pod to deny generic write access to `<workspace>/memory/`
|
/// tools are surfaced only when `[feature.memory].enabled = true`.
|
||||||
/// and `<workspace>/knowledge/`. Absent ⇒ legacy behaviour, no
|
|
||||||
/// memory tools registered.
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub memory: Option<MemoryConfig>,
|
pub memory: Option<MemoryConfig>,
|
||||||
/// First-class web tools configuration. Absent or `enabled = false` keeps
|
/// First-class web tools configuration. Network access remains fail-closed
|
||||||
/// WebSearch/WebFetch registered but disabled, so no network access occurs
|
/// under this config; WebSearch/WebFetch schemas are surfaced only when
|
||||||
/// unless a manifest explicitly opts in.
|
/// `[feature.web].enabled = true`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub web: Option<WebConfig>,
|
pub web: Option<WebConfig>,
|
||||||
/// External Agent Skills (`SKILL.md`) directories to ingest as
|
/// External Agent Skills (`SKILL.md`) directories to ingest as
|
||||||
|
|
@ -82,6 +84,94 @@ pub struct PodManifest {
|
||||||
pub profile: Option<profile::ProfileManifestSnapshot>,
|
pub profile: Option<profile::ProfileManifestSnapshot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Explicit built-in feature/tool-surface enablement. These flags are
|
||||||
|
/// profile/config data only: they do not carry runtime Pod names, sockets,
|
||||||
|
/// sessions, secrets, or resolved host state. Tool registration still applies
|
||||||
|
/// the normal scope, host-authority, backend, memory, and network checks.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct FeatureConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub task: FeatureFlagConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub memory: FeatureFlagConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub web: FeatureFlagConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub pod_management: FeatureFlagConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ticket: TicketFeatureConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ticket_orchestration: FeatureFlagConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FeatureConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
task: FeatureFlagConfig::disabled(),
|
||||||
|
memory: FeatureFlagConfig::disabled(),
|
||||||
|
web: FeatureFlagConfig::disabled(),
|
||||||
|
pod_management: FeatureFlagConfig::disabled(),
|
||||||
|
ticket: TicketFeatureConfig::default(),
|
||||||
|
ticket_orchestration: FeatureFlagConfig::disabled(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct FeatureFlagConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureFlagConfig {
|
||||||
|
pub const fn disabled() -> Self {
|
||||||
|
Self { enabled: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn enabled() -> Self {
|
||||||
|
Self { enabled: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FeatureFlagConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::disabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct TicketFeatureConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub enabled: bool,
|
||||||
|
/// Which non-orchestration Ticket surface to expose when `enabled = true`.
|
||||||
|
/// Orchestration-plan/relation tools are controlled independently by
|
||||||
|
/// `[feature.ticket_orchestration].enabled`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub access: TicketFeatureAccessConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TicketFeatureConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
access: TicketFeatureAccessConfig::Lifecycle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum TicketFeatureAccessConfig {
|
||||||
|
ReadOnly,
|
||||||
|
Lifecycle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TicketFeatureAccessConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Lifecycle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// External Agent Skills (`SKILL.md`) ingest configuration. Skills are
|
/// External Agent Skills (`SKILL.md`) ingest configuration. Skills are
|
||||||
/// loaded *only* from the directories listed here — there is no
|
/// loaded *only* from the directories listed here — there is no
|
||||||
/// implicit `$config_dir/skills/` or builtin probe. Profile and Manifest
|
/// implicit `$config_dir/skills/` or builtin probe. Profile and Manifest
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ use std::rc::Rc;
|
||||||
use mlua::{Lua, LuaOptions, LuaSerdeExt, RegistryKey, StdLib, Table, Value as LuaValue};
|
use mlua::{Lua, LuaOptions, LuaSerdeExt, RegistryKey, StdLib, Table, Value as LuaValue};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::config::{CompactionConfigPartial, PermissionConfigPartial, SessionConfigPartial};
|
use crate::config::{
|
||||||
|
CompactionConfigPartial, FeatureConfigPartial, PermissionConfigPartial, SessionConfigPartial,
|
||||||
|
};
|
||||||
use crate::model::{AuthRef, ModelManifest};
|
use crate::model::{AuthRef, ModelManifest};
|
||||||
use crate::{
|
use crate::{
|
||||||
MemoryConfig, Permission, PodManifest, PodManifestConfig, PodMetaConfig, ResolveError,
|
MemoryConfig, Permission, PodManifest, PodManifestConfig, PodMetaConfig, ResolveError,
|
||||||
|
|
@ -571,6 +573,7 @@ fn resolve_lua_profile_value(
|
||||||
),
|
),
|
||||||
session: profile.session,
|
session: profile.session,
|
||||||
permissions: profile.permissions,
|
permissions: profile.permissions,
|
||||||
|
feature: profile.feature,
|
||||||
compaction,
|
compaction,
|
||||||
web: profile.web,
|
web: profile.web,
|
||||||
memory: profile.memory,
|
memory: profile.memory,
|
||||||
|
|
@ -630,6 +633,8 @@ struct ProfileConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
permissions: Option<PermissionConfigPartial>,
|
permissions: Option<PermissionConfigPartial>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
feature: FeatureConfigPartial,
|
||||||
|
#[serde(default)]
|
||||||
compaction: Option<serde_json::Value>,
|
compaction: Option<serde_json::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
web: Option<WebConfig>,
|
web: Option<WebConfig>,
|
||||||
|
|
@ -1457,6 +1462,57 @@ return profile {
|
||||||
Some("coder")
|
Some("coder")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn resolves_lua_profile_feature_flags_without_runtime_state() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let profile = write_profile(
|
||||||
|
tmp.path(),
|
||||||
|
"feature.lua",
|
||||||
|
r#"
|
||||||
|
local profile = require("yoi.profile")
|
||||||
|
local scope = require("yoi.scope")
|
||||||
|
return profile {
|
||||||
|
slug = "feature",
|
||||||
|
model = { scheme = "anthropic", model_id = "claude-sonnet-4-20250514" },
|
||||||
|
scope = scope.workspace_read(),
|
||||||
|
delegation_scope = scope.workspace_write(),
|
||||||
|
feature = {
|
||||||
|
task = { enabled = true },
|
||||||
|
memory = { enabled = false },
|
||||||
|
web = { enabled = true },
|
||||||
|
pod_management = { enabled = true },
|
||||||
|
ticket = { enabled = true, access = "read_only" },
|
||||||
|
ticket_orchestration = { enabled = false },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let workspace = tmp.path().join("workspace");
|
||||||
|
std::fs::create_dir(&workspace).unwrap();
|
||||||
|
let resolved = ProfileResolver::new()
|
||||||
|
.with_workspace_base(&workspace)
|
||||||
|
.resolve(
|
||||||
|
&ProfileSelector::path(&profile),
|
||||||
|
ProfileResolveOptions::with_pod_name("runtime-pod"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resolved.manifest.pod.name, "runtime-pod");
|
||||||
|
assert!(resolved.manifest.feature.task.enabled);
|
||||||
|
assert!(!resolved.manifest.feature.memory.enabled);
|
||||||
|
assert!(resolved.manifest.feature.web.enabled);
|
||||||
|
assert!(resolved.manifest.feature.pod_management.enabled);
|
||||||
|
assert!(resolved.manifest.feature.ticket.enabled);
|
||||||
|
assert_eq!(
|
||||||
|
resolved.manifest.feature.ticket.access,
|
||||||
|
crate::TicketFeatureAccessConfig::ReadOnly
|
||||||
|
);
|
||||||
|
assert!(!resolved.manifest.feature.ticket_orchestration.enabled);
|
||||||
|
assert_eq!(
|
||||||
|
resolved.manifest.delegation_scope.allow[0].target,
|
||||||
|
workspace
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn host_modules_and_local_require_work() {
|
fn host_modules_and_local_require_work() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
use llm_worker::WorkerError;
|
use llm_worker::WorkerError;
|
||||||
use llm_worker::llm_client::client::LlmClient;
|
use llm_worker::llm_client::client::LlmClient;
|
||||||
|
use manifest::TicketFeatureAccessConfig;
|
||||||
use pod_store::PodMetadataStore;
|
use pod_store::PodMetadataStore;
|
||||||
use session_store::Store;
|
use session_store::Store;
|
||||||
use tokio::sync::{broadcast, mpsc, oneshot};
|
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||||
|
|
@ -223,7 +224,7 @@ impl PodController {
|
||||||
runtime_dir.socket_path(),
|
runtime_dir.socket_path(),
|
||||||
runtime_base.to_path_buf(),
|
runtime_base.to_path_buf(),
|
||||||
spawned_registry.clone(),
|
spawned_registry.clone(),
|
||||||
);
|
)?;
|
||||||
|
|
||||||
// Intake role Pods self-terminate only after a successful
|
// Intake role Pods self-terminate only after a successful
|
||||||
// TicketIntakeReady turn has fully settled back to Idle. The request
|
// TicketIntakeReady turn has fully settled back to Idle. The request
|
||||||
|
|
@ -499,7 +500,7 @@ fn register_pod_tools<C, St>(
|
||||||
spawner_socket: PathBuf,
|
spawner_socket: PathBuf,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
spawned_registry: Arc<SpawnedPodRegistry>,
|
spawned_registry: Arc<SpawnedPodRegistry>,
|
||||||
) -> tools::ScopedFs
|
) -> std::io::Result<tools::ScopedFs>
|
||||||
where
|
where
|
||||||
C: LlmClient + Clone + 'static,
|
C: LlmClient + Clone + 'static,
|
||||||
St: Store + PodMetadataStore + Clone + 'static,
|
St: Store + PodMetadataStore + Clone + 'static,
|
||||||
|
|
@ -513,6 +514,7 @@ where
|
||||||
let session_id_for_usage = pod.segment_id().to_string();
|
let session_id_for_usage = pod.segment_id().to_string();
|
||||||
let memory_config = pod.manifest().memory.clone();
|
let memory_config = pod.manifest().memory.clone();
|
||||||
let web_config = pod.manifest().web.clone();
|
let web_config = pod.manifest().web.clone();
|
||||||
|
let feature_config = pod.manifest().feature.clone();
|
||||||
let spawner_name = pod.manifest().pod.name.clone();
|
let spawner_name = pod.manifest().pod.name.clone();
|
||||||
let spawner_manifest = pod.manifest().clone();
|
let spawner_manifest = pod.manifest().clone();
|
||||||
let prompts = pod.prompts().clone();
|
let prompts = pod.prompts().clone();
|
||||||
|
|
@ -534,24 +536,47 @@ where
|
||||||
fs,
|
fs,
|
||||||
tracker.clone(),
|
tracker.clone(),
|
||||||
bash_output_dir,
|
bash_output_dir,
|
||||||
web_config,
|
|
||||||
));
|
));
|
||||||
|
if feature_config.web.enabled {
|
||||||
|
pod.worker_mut()
|
||||||
|
.register_tools(tools::web_builtin_tools(web_config));
|
||||||
|
}
|
||||||
|
|
||||||
let mut feature_registry = FeatureRegistryBuilder::new();
|
let mut feature_registry = FeatureRegistryBuilder::new();
|
||||||
feature_registry.add_module(task_feature);
|
if feature_config.task.enabled {
|
||||||
feature_registry.add_module(crate::feature::builtin::ticket_tools_feature(
|
feature_registry.add_module(task_feature);
|
||||||
&workspace_root,
|
}
|
||||||
));
|
if feature_config.ticket.enabled || feature_config.ticket_orchestration.enabled {
|
||||||
|
let ticket_access = match feature_config.ticket.access {
|
||||||
|
TicketFeatureAccessConfig::ReadOnly => {
|
||||||
|
crate::feature::builtin::ticket::TicketFeatureAccess::ReadOnly
|
||||||
|
}
|
||||||
|
TicketFeatureAccessConfig::Lifecycle => {
|
||||||
|
crate::feature::builtin::ticket::TicketFeatureAccess::Lifecycle
|
||||||
|
}
|
||||||
|
};
|
||||||
|
feature_registry.add_module(
|
||||||
|
crate::feature::builtin::ticket::ticket_tools_feature_with_options(
|
||||||
|
&workspace_root,
|
||||||
|
feature_config.ticket.enabled.then_some(ticket_access),
|
||||||
|
feature_config.ticket_orchestration.enabled,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
let _feature_install_report = pod.install_features(feature_registry);
|
let _feature_install_report = pod.install_features(feature_registry);
|
||||||
|
|
||||||
let worker = pod.worker_mut();
|
let worker = pod.worker_mut();
|
||||||
|
|
||||||
// Memory subsystem opt-in. When `[memory]` is present in the
|
// Memory tools require both explicit feature exposure and memory storage
|
||||||
// manifest, register the memory-specific Read/Write/Edit tools that
|
// configuration. This keeps resident-memory config separate from the
|
||||||
// target `<workspace>/memory/` and `<workspace>/knowledge/` with
|
// model-visible Memory*/Knowledge* tool surface.
|
||||||
// their built-in linter. Companion deny rules on the generic CRUD
|
if feature_config.memory.enabled {
|
||||||
// scope were already applied during `Pod::from_manifest`.
|
let mem = memory_config.as_ref().ok_or_else(|| {
|
||||||
if let Some(mem) = memory_config.as_ref() {
|
std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"[feature.memory].enabled = true requires a [memory] configuration section",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let layout = memory::WorkspaceLayout::resolve(mem, &workspace_root);
|
let layout = memory::WorkspaceLayout::resolve(mem, &workspace_root);
|
||||||
let query_cfg = memory::tool::QueryConfig::from(mem);
|
let query_cfg = memory::tool::QueryConfig::from(mem);
|
||||||
worker.register_tool(memory::tool::read_tool_with_usage(
|
worker.register_tool(memory::tool::read_tool_with_usage(
|
||||||
|
|
@ -567,28 +592,39 @@ where
|
||||||
|
|
||||||
// Pod-orchestration tools (SpawnPod + the four comm tools) share
|
// Pod-orchestration tools (SpawnPod + the four comm tools) share
|
||||||
// the Pod-scoped `SpawnedPodRegistry` (also consumed by the main
|
// the Pod-scoped `SpawnedPodRegistry` (also consumed by the main
|
||||||
// loop's `PodEvent` handler).
|
// loop's `PodEvent` handler). Expose them only behind the explicit
|
||||||
worker.register_tool(spawn_pod_tool(
|
// profile feature and require delegation authority up front so enabling
|
||||||
spawner_name.clone(),
|
// the surface cannot imply broad child scope by accident.
|
||||||
spawner_socket,
|
if feature_config.pod_management.enabled {
|
||||||
runtime_base.clone(),
|
if spawner_manifest.delegation_scope.allow.is_empty() {
|
||||||
workspace_root.clone(),
|
return Err(std::io::Error::new(
|
||||||
pwd.clone(),
|
std::io::ErrorKind::InvalidInput,
|
||||||
spawned_registry.clone(),
|
"[feature.pod_management].enabled = true requires non-empty [[delegation_scope.allow]]",
|
||||||
self_parent_socket,
|
));
|
||||||
spawner_manifest,
|
}
|
||||||
scope_handle,
|
worker.register_tool(spawn_pod_tool(
|
||||||
prompts,
|
spawner_name.clone(),
|
||||||
));
|
spawner_socket,
|
||||||
worker.register_tool(send_to_pod_tool(spawned_registry.clone()));
|
runtime_base.clone(),
|
||||||
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
|
workspace_root.clone(),
|
||||||
worker.register_tool(stop_pod_tool(spawned_registry.clone()));
|
pwd.clone(),
|
||||||
let discovery = PodDiscovery::new(pod_store, spawner_name, runtime_base, pwd, spawned_registry);
|
spawned_registry.clone(),
|
||||||
worker.register_tool(list_pods_tool(discovery.clone()));
|
self_parent_socket,
|
||||||
worker.register_tool(restore_pod_tool(discovery.clone()));
|
spawner_manifest,
|
||||||
worker.register_tool(send_to_peer_pod_tool(discovery));
|
scope_handle,
|
||||||
|
prompts,
|
||||||
|
));
|
||||||
|
worker.register_tool(send_to_pod_tool(spawned_registry.clone()));
|
||||||
|
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
|
||||||
|
worker.register_tool(stop_pod_tool(spawned_registry.clone()));
|
||||||
|
let discovery =
|
||||||
|
PodDiscovery::new(pod_store, spawner_name, runtime_base, pwd, spawned_registry);
|
||||||
|
worker.register_tool(list_pods_tool(discovery.clone()));
|
||||||
|
worker.register_tool(restore_pod_tool(discovery.clone()));
|
||||||
|
worker.register_tool(send_to_peer_pod_tool(discovery));
|
||||||
|
}
|
||||||
pod.attach_tracker(tracker);
|
pod.attach_tracker(tracker);
|
||||||
fs_for_view
|
Ok(fs_for_view)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Idle/Paused event loop. Each iteration either fires a staged
|
/// Idle/Paused event loop. Each iteration either fires a staged
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,5 @@ pub mod ticket;
|
||||||
pub use task::{TaskFeature, task_tools_feature};
|
pub use task::{TaskFeature, task_tools_feature};
|
||||||
pub use ticket::{
|
pub use ticket::{
|
||||||
TicketFeature, TicketFeatureAccess, ticket_tools_feature, ticket_tools_feature_with_access,
|
TicketFeature, TicketFeatureAccess, ticket_tools_feature, ticket_tools_feature_with_access,
|
||||||
|
ticket_tools_feature_with_options,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ use std::path::{Path, PathBuf};
|
||||||
use ticket::{
|
use ticket::{
|
||||||
LocalTicketBackend,
|
LocalTicketBackend,
|
||||||
config::{DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TicketConfig},
|
config::{DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TicketConfig},
|
||||||
tool::{TICKET_READ_ONLY_TOOL_NAMES, TICKET_TOOL_NAMES, ticket_tools},
|
tool::{
|
||||||
|
TICKET_BASE_READ_ONLY_TOOL_NAMES, TICKET_BASE_TOOL_NAMES,
|
||||||
|
TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES,
|
||||||
|
TICKET_READ_ONLY_TOOL_NAMES, TICKET_TOOL_NAMES, ticket_tools,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::feature::{
|
use crate::feature::{
|
||||||
|
|
@ -32,10 +36,17 @@ pub enum TicketFeatureAccess {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TicketFeatureAccess {
|
impl TicketFeatureAccess {
|
||||||
pub fn tool_names(self) -> &'static [&'static str] {
|
pub fn base_tool_names(self) -> &'static [&'static str] {
|
||||||
match self {
|
match self {
|
||||||
Self::ReadOnly => &TICKET_READ_ONLY_TOOL_NAMES,
|
Self::ReadOnly => &TICKET_BASE_READ_ONLY_TOOL_NAMES,
|
||||||
Self::Lifecycle => &TICKET_TOOL_NAMES,
|
Self::Lifecycle => &TICKET_BASE_TOOL_NAMES,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn orchestration_tool_names(self) -> &'static [&'static str] {
|
||||||
|
match self {
|
||||||
|
Self::ReadOnly => &TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES,
|
||||||
|
Self::Lifecycle => &TICKET_ORCHESTRATION_TOOL_NAMES,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +57,8 @@ pub struct TicketFeature {
|
||||||
record_language: Option<String>,
|
record_language: Option<String>,
|
||||||
config_error: Option<String>,
|
config_error: Option<String>,
|
||||||
access: TicketFeatureAccess,
|
access: TicketFeatureAccess,
|
||||||
|
include_base_tools: bool,
|
||||||
|
include_orchestration_tools: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TicketFeature {
|
impl TicketFeature {
|
||||||
|
|
@ -54,11 +67,21 @@ impl TicketFeature {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_access(backend_root: impl Into<PathBuf>, access: TicketFeatureAccess) -> Self {
|
pub fn new_with_access(backend_root: impl Into<PathBuf>, access: TicketFeatureAccess) -> Self {
|
||||||
|
Self::new_with_options(backend_root, Some(access), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_options(
|
||||||
|
backend_root: impl Into<PathBuf>,
|
||||||
|
access: Option<TicketFeatureAccess>,
|
||||||
|
include_orchestration_tools: bool,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
backend_root: backend_root.into(),
|
backend_root: backend_root.into(),
|
||||||
record_language: None,
|
record_language: None,
|
||||||
config_error: None,
|
config_error: None,
|
||||||
access,
|
access: access.unwrap_or(TicketFeatureAccess::Lifecycle),
|
||||||
|
include_base_tools: access.is_some(),
|
||||||
|
include_orchestration_tools,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,22 +92,36 @@ impl TicketFeature {
|
||||||
pub fn for_workspace_with_access(
|
pub fn for_workspace_with_access(
|
||||||
workspace: impl AsRef<Path>,
|
workspace: impl AsRef<Path>,
|
||||||
access: TicketFeatureAccess,
|
access: TicketFeatureAccess,
|
||||||
|
) -> Self {
|
||||||
|
Self::for_workspace_with_options(workspace, Some(access), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_workspace_with_options(
|
||||||
|
workspace: impl AsRef<Path>,
|
||||||
|
access: Option<TicketFeatureAccess>,
|
||||||
|
include_orchestration_tools: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let workspace = workspace.as_ref();
|
let workspace = workspace.as_ref();
|
||||||
match TicketConfig::load_workspace(workspace) {
|
match TicketConfig::load_workspace(workspace) {
|
||||||
Ok(config) => {
|
Ok(config) => {
|
||||||
let backend_root = config.backend_root().to_path_buf();
|
let backend_root = config.backend_root().to_path_buf();
|
||||||
let record_language = config.ticket_record_language().map(str::to_string);
|
let record_language = config.ticket_record_language().map(str::to_string);
|
||||||
let mut feature = Self::new_with_access(backend_root, access);
|
let mut feature =
|
||||||
|
Self::new_with_options(backend_root, access, include_orchestration_tools);
|
||||||
feature.record_language = record_language;
|
feature.record_language = record_language;
|
||||||
feature
|
feature
|
||||||
}
|
}
|
||||||
Err(error) => Self {
|
Err(error) => {
|
||||||
backend_root: workspace.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH),
|
let access_value = access.unwrap_or(TicketFeatureAccess::Lifecycle);
|
||||||
record_language: None,
|
Self {
|
||||||
config_error: Some(error.to_string()),
|
backend_root: workspace.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH),
|
||||||
access,
|
record_language: None,
|
||||||
},
|
config_error: Some(error.to_string()),
|
||||||
|
access: access_value,
|
||||||
|
include_base_tools: access.is_some(),
|
||||||
|
include_orchestration_tools,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,6 +133,23 @@ impl TicketFeature {
|
||||||
self.access
|
self.access
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enabled_tool_names(&self) -> Vec<&'static str> {
|
||||||
|
if self.include_base_tools && self.include_orchestration_tools {
|
||||||
|
return match self.access {
|
||||||
|
TicketFeatureAccess::ReadOnly => TICKET_READ_ONLY_TOOL_NAMES.to_vec(),
|
||||||
|
TicketFeatureAccess::Lifecycle => TICKET_TOOL_NAMES.to_vec(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let mut names = Vec::new();
|
||||||
|
if self.include_base_tools {
|
||||||
|
names.extend_from_slice(self.access.base_tool_names());
|
||||||
|
}
|
||||||
|
if self.include_orchestration_tools {
|
||||||
|
names.extend_from_slice(self.access.orchestration_tool_names());
|
||||||
|
}
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
fn authority(&self) -> HostAuthority {
|
fn authority(&self) -> HostAuthority {
|
||||||
HostAuthority::TicketBackend {
|
HostAuthority::TicketBackend {
|
||||||
root: self.backend_root.display().to_string(),
|
root: self.backend_root.display().to_string(),
|
||||||
|
|
@ -122,7 +176,8 @@ impl FeatureModule for TicketFeature {
|
||||||
self.authority(),
|
self.authority(),
|
||||||
AUTHORITY_REASON,
|
AUTHORITY_REASON,
|
||||||
));
|
));
|
||||||
for name in self.access.tool_names() {
|
let enabled_tool_names = self.enabled_tool_names();
|
||||||
|
for name in &enabled_tool_names {
|
||||||
descriptor = descriptor.with_tool(ToolDeclaration::new(*name, tool_description(name)));
|
descriptor = descriptor.with_tool(ToolDeclaration::new(*name, tool_description(name)));
|
||||||
}
|
}
|
||||||
descriptor
|
descriptor
|
||||||
|
|
@ -152,12 +207,15 @@ impl FeatureModule for TicketFeature {
|
||||||
let authority = self.authority();
|
let authority = self.authority();
|
||||||
let backend = LocalTicketBackend::new(usable_root)
|
let backend = LocalTicketBackend::new(usable_root)
|
||||||
.with_record_language(self.record_language.as_deref());
|
.with_record_language(self.record_language.as_deref());
|
||||||
let allowed_tool_names = self.access.tool_names();
|
let allowed_tool_names = self.enabled_tool_names();
|
||||||
let mut tools = context.tools();
|
let mut tools = context.tools();
|
||||||
for definition in ticket_tools(backend) {
|
for definition in ticket_tools(backend) {
|
||||||
let (meta, _) = definition();
|
let (meta, _) = definition();
|
||||||
let name = meta.name.clone();
|
let name = meta.name.clone();
|
||||||
if !allowed_tool_names.contains(&name.as_str()) {
|
if !allowed_tool_names
|
||||||
|
.iter()
|
||||||
|
.any(|allowed| *allowed == name.as_str())
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
tools.register(
|
tools.register(
|
||||||
|
|
@ -211,12 +269,24 @@ pub fn ticket_tools_feature_with_access(
|
||||||
TicketFeature::for_workspace_with_access(workspace, access)
|
TicketFeature::for_workspace_with_access(workspace, access)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ticket_tools_feature_with_options(
|
||||||
|
workspace: impl AsRef<Path>,
|
||||||
|
access: Option<TicketFeatureAccess>,
|
||||||
|
include_orchestration_tools: bool,
|
||||||
|
) -> TicketFeature {
|
||||||
|
TicketFeature::for_workspace_with_options(workspace, access, include_orchestration_tools)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::feature::{FeatureRegistryBuilder, FeatureRuntimeKind};
|
use crate::feature::{FeatureRegistryBuilder, FeatureRuntimeKind};
|
||||||
use crate::hook::HookRegistryBuilder;
|
use crate::hook::HookRegistryBuilder;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
use ticket::tool::{
|
||||||
|
TICKET_BASE_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES, TICKET_READ_ONLY_TOOL_NAMES,
|
||||||
|
TICKET_TOOL_NAMES,
|
||||||
|
};
|
||||||
|
|
||||||
fn make_ticket_root(root: &Path) {
|
fn make_ticket_root(root: &Path) {
|
||||||
std::fs::create_dir_all(root).unwrap();
|
std::fs::create_dir_all(root).unwrap();
|
||||||
|
|
@ -269,6 +339,40 @@ mod tests {
|
||||||
assert_eq!(descriptor.requested_host_authorities.len(), 1);
|
assert_eq!(descriptor.requested_host_authorities.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn descriptor_can_expose_base_ticket_without_orchestration_tools() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let feature = ticket_tools_feature_with_options(
|
||||||
|
temp.path(),
|
||||||
|
Some(TicketFeatureAccess::Lifecycle),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let descriptor = feature.descriptor();
|
||||||
|
assert_eq!(
|
||||||
|
descriptor
|
||||||
|
.tools
|
||||||
|
.iter()
|
||||||
|
.map(|tool| tool.name.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
TICKET_BASE_TOOL_NAMES
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn descriptor_can_expose_orchestration_only_tools() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let feature = ticket_tools_feature_with_options(temp.path(), None, true);
|
||||||
|
let descriptor = feature.descriptor();
|
||||||
|
assert_eq!(
|
||||||
|
descriptor
|
||||||
|
.tools
|
||||||
|
.iter()
|
||||||
|
.map(|tool| tool.name.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
TICKET_ORCHESTRATION_TOOL_NAMES
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_only_installation_does_not_expose_mutating_tools() {
|
fn read_only_installation_does_not_expose_mutating_tools() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -775,6 +775,7 @@ fn manifest_to_reusable_config(manifest: &PodManifest) -> PodManifestConfig {
|
||||||
default_action: Some(p.default_action),
|
default_action: Some(p.default_action),
|
||||||
rules: p.rules.clone(),
|
rules: p.rules.clone(),
|
||||||
}),
|
}),
|
||||||
|
feature: manifest.feature.clone().into(),
|
||||||
compaction: manifest
|
compaction: manifest
|
||||||
.compaction
|
.compaction
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,127 @@ async fn wait_for_status(handle: &PodHandle, status: PodStatus) {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn request_tool_names(request: &Request) -> Vec<String> {
|
||||||
|
let mut names = request
|
||||||
|
.tools
|
||||||
|
.iter()
|
||||||
|
.map(|tool| tool.name.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
names.sort();
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_captured_request(client: &MockClient) -> Request {
|
||||||
|
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||||
|
loop {
|
||||||
|
let requests = client.captured_requests();
|
||||||
|
if let Some(request) = requests.into_iter().next() {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
tokio::time::Instant::now() < deadline,
|
||||||
|
"timed out waiting for captured LLM request"
|
||||||
|
);
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn feature_flags_default_to_core_tool_surface_only() {
|
||||||
|
let client = MockClient::new(simple_text_events());
|
||||||
|
let client_for_assert = client.clone();
|
||||||
|
let pod = make_pod(client).await;
|
||||||
|
let handle = spawn_controller(pod).await;
|
||||||
|
|
||||||
|
handle.send(Method::run_text("Hello")).await.unwrap();
|
||||||
|
wait_for_status(&handle, PodStatus::Idle).await;
|
||||||
|
|
||||||
|
let request = wait_for_captured_request(&client_for_assert).await;
|
||||||
|
let names = request_tool_names(&request);
|
||||||
|
assert_eq!(names, vec!["Bash", "Edit", "Glob", "Grep", "Read", "Write"]);
|
||||||
|
assert!(!names.iter().any(|name| name == "TaskCreate"));
|
||||||
|
assert!(!names.iter().any(|name| name == "WebSearch"));
|
||||||
|
assert!(!names.iter().any(|name| name == "SpawnPod"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn enabled_task_and_web_features_register_their_tools() {
|
||||||
|
let manifest = r#"
|
||||||
|
[pod]
|
||||||
|
name = "feature-test-pod"
|
||||||
|
pwd = "./"
|
||||||
|
|
||||||
|
[model]
|
||||||
|
scheme = "anthropic"
|
||||||
|
model_id = "test-model"
|
||||||
|
|
||||||
|
[worker]
|
||||||
|
max_tokens = 100
|
||||||
|
|
||||||
|
[feature.task]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[feature.web]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[web]
|
||||||
|
enabled = false
|
||||||
|
|
||||||
|
[[scope.allow]]
|
||||||
|
target = "./"
|
||||||
|
permission = "write"
|
||||||
|
"#;
|
||||||
|
let client = MockClient::new(simple_text_events());
|
||||||
|
let client_for_assert = client.clone();
|
||||||
|
let pod = make_pod_with_pwd_and_manifest(client, manifest).await.0;
|
||||||
|
let handle = spawn_controller(pod).await;
|
||||||
|
|
||||||
|
handle.send(Method::run_text("Hello")).await.unwrap();
|
||||||
|
wait_for_status(&handle, PodStatus::Idle).await;
|
||||||
|
|
||||||
|
let request = wait_for_captured_request(&client_for_assert).await;
|
||||||
|
let names = request_tool_names(&request);
|
||||||
|
assert!(names.iter().any(|name| name == "TaskCreate"));
|
||||||
|
assert!(names.iter().any(|name| name == "TaskUpdate"));
|
||||||
|
assert!(names.iter().any(|name| name == "WebSearch"));
|
||||||
|
assert!(names.iter().any(|name| name == "WebFetch"));
|
||||||
|
assert!(!names.iter().any(|name| name == "SpawnPod"));
|
||||||
|
assert!(!names.iter().any(|name| name == "MemoryRead"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pod_management_feature_requires_delegation_scope() {
|
||||||
|
let manifest = r#"
|
||||||
|
[pod]
|
||||||
|
name = "pod-management-feature-test"
|
||||||
|
pwd = "./"
|
||||||
|
|
||||||
|
[model]
|
||||||
|
scheme = "anthropic"
|
||||||
|
model_id = "test-model"
|
||||||
|
|
||||||
|
[worker]
|
||||||
|
max_tokens = 100
|
||||||
|
|
||||||
|
[feature.pod_management]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[[scope.allow]]
|
||||||
|
target = "./"
|
||||||
|
permission = "write"
|
||||||
|
"#;
|
||||||
|
let client = MockClient::new(simple_text_events());
|
||||||
|
let pod = make_pod_with_pwd_and_manifest(client, manifest).await.0;
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let result = PodController::spawn(pod, tmp.path()).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
let message = result.err().unwrap().to_string();
|
||||||
|
assert!(
|
||||||
|
message.contains("[feature.pod_management].enabled = true requires non-empty"),
|
||||||
|
"unexpected error: {message}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn run_end_returns_to_idle_without_busy_status() {
|
async fn run_end_returns_to_idle_without_busy_status() {
|
||||||
let client = MockClient::new(simple_text_events());
|
let client = MockClient::new(simple_text_events());
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,31 @@ const MAX_BODY_MAX_BYTES: usize = 64 * 1024;
|
||||||
const DEFAULT_DIAGNOSTIC_LIMIT: usize = 100;
|
const DEFAULT_DIAGNOSTIC_LIMIT: usize = 100;
|
||||||
const MAX_DIAGNOSTIC_LIMIT: usize = 500;
|
const MAX_DIAGNOSTIC_LIMIT: usize = 500;
|
||||||
|
|
||||||
|
pub const TICKET_BASE_TOOL_NAMES: [&str; 9] = [
|
||||||
|
"TicketCreate",
|
||||||
|
"TicketList",
|
||||||
|
"TicketShow",
|
||||||
|
"TicketComment",
|
||||||
|
"TicketReview",
|
||||||
|
"TicketIntakeReady",
|
||||||
|
"TicketWorkflowState",
|
||||||
|
"TicketClose",
|
||||||
|
"TicketDoctor",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const TICKET_BASE_READ_ONLY_TOOL_NAMES: [&str; 3] =
|
||||||
|
["TicketList", "TicketShow", "TicketDoctor"];
|
||||||
|
|
||||||
|
pub const TICKET_ORCHESTRATION_TOOL_NAMES: [&str; 4] = [
|
||||||
|
"TicketRelationRecord",
|
||||||
|
"TicketRelationQuery",
|
||||||
|
"TicketOrchestrationPlanRecord",
|
||||||
|
"TicketOrchestrationPlanQuery",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES: [&str; 2] =
|
||||||
|
["TicketRelationQuery", "TicketOrchestrationPlanQuery"];
|
||||||
|
|
||||||
pub const TICKET_TOOL_NAMES: [&str; 13] = [
|
pub const TICKET_TOOL_NAMES: [&str; 13] = [
|
||||||
"TicketCreate",
|
"TicketCreate",
|
||||||
"TicketList",
|
"TicketList",
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ pub fn core_builtin_tools(
|
||||||
fs: ScopedFs,
|
fs: ScopedFs,
|
||||||
tracker: Tracker,
|
tracker: Tracker,
|
||||||
bash_output_dir: std::path::PathBuf,
|
bash_output_dir: std::path::PathBuf,
|
||||||
web_config: Option<manifest::WebConfig>,
|
|
||||||
) -> Vec<llm_worker::tool::ToolDefinition> {
|
) -> Vec<llm_worker::tool::ToolDefinition> {
|
||||||
vec![
|
vec![
|
||||||
read_tool(fs.clone(), tracker.clone()),
|
read_tool(fs.clone(), tracker.clone()),
|
||||||
|
|
@ -66,6 +65,13 @@ pub fn core_builtin_tools(
|
||||||
glob_tool(fs.clone()),
|
glob_tool(fs.clone()),
|
||||||
grep_tool(fs.clone()),
|
grep_tool(fs.clone()),
|
||||||
bash_tool(fs, bash_output_dir),
|
bash_tool(fs, bash_output_dir),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn web_builtin_tools(
|
||||||
|
web_config: Option<manifest::WebConfig>,
|
||||||
|
) -> Vec<llm_worker::tool::ToolDefinition> {
|
||||||
|
vec![
|
||||||
web_search_tool(web::WebTools::new(web_config.clone())),
|
web_search_tool(web::WebTools::new(web_config.clone())),
|
||||||
web_fetch_tool(web::WebTools::new(web_config)),
|
web_fetch_tool(web::WebTools::new(web_config)),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,7 @@ fn setup() -> (TempDir, TempDir, Registry) {
|
||||||
let scope = Scope::from_config(&config).unwrap();
|
let scope = Scope::from_config(&config).unwrap();
|
||||||
let fs = ScopedFs::new(scope, dir.path().to_path_buf());
|
let fs = ScopedFs::new(scope, dir.path().to_path_buf());
|
||||||
let tracker = Tracker::new();
|
let tracker = Tracker::new();
|
||||||
let reg = Registry::new(core_builtin_tools(
|
let reg = Registry::new(core_builtin_tools(fs, tracker, spill.path().to_path_buf()));
|
||||||
fs,
|
|
||||||
tracker,
|
|
||||||
spill.path().to_path_buf(),
|
|
||||||
None,
|
|
||||||
));
|
|
||||||
(dir, spill, reg)
|
(dir, spill, reg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,12 +56,7 @@ fn setup() -> (TempDir, TempDir, Registry) {
|
||||||
let scope = scope_with_spill(dir.path(), spill.path());
|
let scope = scope_with_spill(dir.path(), spill.path());
|
||||||
let fs = ScopedFs::new(scope, dir.path().to_path_buf());
|
let fs = ScopedFs::new(scope, dir.path().to_path_buf());
|
||||||
let tracker = Tracker::new();
|
let tracker = Tracker::new();
|
||||||
let reg = Registry::new(core_builtin_tools(
|
let reg = Registry::new(core_builtin_tools(fs, tracker, spill.path().to_path_buf()));
|
||||||
fs,
|
|
||||||
tracker,
|
|
||||||
spill.path().to_path_buf(),
|
|
||||||
None,
|
|
||||||
));
|
|
||||||
(dir, spill, reg)
|
(dir, spill, reg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,19 +77,7 @@ fn core_builtin_tools_registers_full_set() {
|
||||||
let (_dir, _spill, reg) = setup();
|
let (_dir, _spill, reg) = setup();
|
||||||
let mut names = reg.names();
|
let mut names = reg.names();
|
||||||
names.sort();
|
names.sort();
|
||||||
assert_eq!(
|
assert_eq!(names, vec!["Bash", "Edit", "Glob", "Grep", "Read", "Write"]);
|
||||||
names,
|
|
||||||
vec![
|
|
||||||
"Bash",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Read",
|
|
||||||
"WebFetch",
|
|
||||||
"WebSearch",
|
|
||||||
"Write"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -287,20 +270,11 @@ async fn edit_requires_read_across_tools() {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn deterministic_tool_order_is_registration_order() {
|
async fn deterministic_tool_order_is_registration_order() {
|
||||||
let (_dir, _spill, reg) = setup();
|
let (_dir, _spill, reg) = setup();
|
||||||
// Registration order from core_builtin_tools(): Read, Write, Edit, Glob, Grep, Bash, WebSearch, WebFetch
|
// Registration order from core_builtin_tools(): Read, Write, Edit, Glob, Grep, Bash
|
||||||
let names: Vec<&str> = reg.entries.iter().map(|(m, _)| m.name.as_str()).collect();
|
let names: Vec<&str> = reg.entries.iter().map(|(m, _)| m.name.as_str()).collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
names,
|
names,
|
||||||
vec![
|
vec!["Read", "Write", "Edit", "Glob", "Grep", "Bash",]
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,16 +282,7 @@ async fn deterministic_tool_order_is_registration_order() {
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_names_match_reference_spec() {
|
fn tool_names_match_reference_spec() {
|
||||||
let (_dir, _spill, reg) = setup();
|
let (_dir, _spill, reg) = setup();
|
||||||
for expected in [
|
for expected in ["Read", "Write", "Edit", "Glob", "Grep", "Bash"] {
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
] {
|
|
||||||
assert!(
|
assert!(
|
||||||
reg.entries.iter().any(|(m, _)| m.name == expected),
|
reg.entries.iter().any(|(m, _)| m.name == expected),
|
||||||
"missing tool {expected}"
|
"missing tool {expected}"
|
||||||
|
|
@ -337,7 +302,6 @@ async fn tracker_recent_files_tracks_read_write_edit() {
|
||||||
fs,
|
fs,
|
||||||
tracker.clone(),
|
tracker.clone(),
|
||||||
spill.path().to_path_buf(),
|
spill.path().to_path_buf(),
|
||||||
None,
|
|
||||||
));
|
));
|
||||||
|
|
||||||
let a = dir.path().join("a.txt");
|
let a = dir.path().join("a.txt");
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,15 @@ return profile {
|
||||||
worker_context_max_tokens = 100000,
|
worker_context_max_tokens = 100000,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
feature = {
|
||||||
|
task = { enabled = true },
|
||||||
|
memory = { enabled = true },
|
||||||
|
web = { enabled = true },
|
||||||
|
pod_management = { enabled = false },
|
||||||
|
ticket = { enabled = false, access = "lifecycle" },
|
||||||
|
ticket_orchestration = { enabled = false },
|
||||||
|
},
|
||||||
|
|
||||||
memory = {
|
memory = {
|
||||||
extract_threshold = 50000,
|
extract_threshold = 50000,
|
||||||
consolidation_threshold_files = 5,
|
consolidation_threshold_files = 5,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user