chore: refresh T-588 onto develop
This commit is contained in:
@@ -1020,11 +1020,10 @@ where
|
|||||||
worker.manifest(),
|
worker.manifest(),
|
||||||
worker.workspace_client_handle(),
|
worker.workspace_client_handle(),
|
||||||
worker.prompts().load_full(),
|
worker.prompts().load_full(),
|
||||||
)
|
)?;
|
||||||
.await?;
|
|
||||||
let memory_prompt_contribution = memory_install_plan.as_ref().map(|plan| {
|
let memory_prompt_contribution = memory_install_plan.as_ref().map(|plan| {
|
||||||
(
|
(
|
||||||
plan.resident_summary.clone(),
|
plan.resident_summary_source.clone(),
|
||||||
plan.system_prompt_override.clone(),
|
plan.system_prompt_override.clone(),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ use crate::feature::{
|
|||||||
ToolDeclaration,
|
ToolDeclaration,
|
||||||
};
|
};
|
||||||
use crate::worker::{
|
use crate::worker::{
|
||||||
WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceRequestMethod,
|
SystemPromptContributionSource, WorkspaceClient, WorkspaceClientError, WorkspaceRequest,
|
||||||
|
WorkspaceRequestMethod,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -342,15 +343,44 @@ fn query_schema() -> serde_json::Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MemoryFeatureInstallPlan {
|
struct WorkspaceResidentSummarySource {
|
||||||
pub module: MemoryToolsFeature,
|
client: Arc<dyn WorkspaceClient>,
|
||||||
pub resident_summary: Option<String>,
|
}
|
||||||
pub system_prompt_override: Option<String>,
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SystemPromptContributionSource for WorkspaceResidentSummarySource {
|
||||||
|
async fn load(&self) -> Option<String> {
|
||||||
|
match self
|
||||||
|
.client
|
||||||
|
.execute_memory_backend_operation(
|
||||||
|
memory::backend::MemoryBackendOperation::ResidentSummary(
|
||||||
|
memory::backend::MemoryResidentSummaryOperation::default(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(memory::backend::MemoryBackendOperationResult::ToolOutput(output)) => output.content,
|
||||||
|
Ok(other) => {
|
||||||
|
tracing::debug!(?other, "unexpected resident Memory Backend result");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::debug!(%error, "resident Memory summary unavailable");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct MemoryFeatureInstallPlan {
|
||||||
|
pub(crate) module: MemoryToolsFeature,
|
||||||
|
pub(crate) resident_summary_source: Option<Arc<dyn SystemPromptContributionSource>>,
|
||||||
|
pub(crate) system_prompt_override: Option<String>,
|
||||||
pub(crate) resolved_config: manifest::ResolvedMemoryFeatureConfig,
|
pub(crate) resolved_config: manifest::ResolvedMemoryFeatureConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryFeatureInstallPlan {
|
impl MemoryFeatureInstallPlan {
|
||||||
pub async fn prepare(
|
pub fn prepare(
|
||||||
manifest: &manifest::WorkerManifest,
|
manifest: &manifest::WorkerManifest,
|
||||||
client: Arc<dyn WorkspaceClient>,
|
client: Arc<dyn WorkspaceClient>,
|
||||||
prompts: Arc<crate::prompt::catalog::PromptCatalog>,
|
prompts: Arc<crate::prompt::catalog::PromptCatalog>,
|
||||||
@@ -361,10 +391,9 @@ impl MemoryFeatureInstallPlan {
|
|||||||
prompts,
|
prompts,
|
||||||
manifest.profile.clone(),
|
manifest.profile.clone(),
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare_resolved(
|
fn prepare_resolved(
|
||||||
config: manifest::ResolvedMemoryFeatureConfig,
|
config: manifest::ResolvedMemoryFeatureConfig,
|
||||||
client: Arc<dyn WorkspaceClient>,
|
client: Arc<dyn WorkspaceClient>,
|
||||||
prompts: Arc<crate::prompt::catalog::PromptCatalog>,
|
prompts: Arc<crate::prompt::catalog::PromptCatalog>,
|
||||||
@@ -405,30 +434,11 @@ impl MemoryFeatureInstallPlan {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let resident_summary = if config.profile.resident.inject_summary {
|
let resident_summary_source = config.profile.resident.inject_summary.then(|| {
|
||||||
match client
|
Arc::new(WorkspaceResidentSummarySource {
|
||||||
.execute_memory_backend_operation(
|
client: Arc::clone(&client),
|
||||||
memory::backend::MemoryBackendOperation::ResidentSummary(
|
}) as Arc<dyn SystemPromptContributionSource>
|
||||||
memory::backend::MemoryResidentSummaryOperation::default(),
|
});
|
||||||
),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(memory::backend::MemoryBackendOperationResult::ToolOutput(output)) => {
|
|
||||||
output.content
|
|
||||||
}
|
|
||||||
Ok(other) => {
|
|
||||||
tracing::debug!(?other, "unexpected resident Memory Backend result");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
tracing::debug!(%error, "resident Memory summary unavailable");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let system_prompt_override = if memory_consolidation_worker {
|
let system_prompt_override = if memory_consolidation_worker {
|
||||||
let language = settings.language;
|
let language = settings.language;
|
||||||
Some(
|
Some(
|
||||||
@@ -442,7 +452,7 @@ impl MemoryFeatureInstallPlan {
|
|||||||
|
|
||||||
Ok(Some(Self {
|
Ok(Some(Self {
|
||||||
module: MemoryToolsFeature::new(client, config.profile.staging_tools),
|
module: MemoryToolsFeature::new(client, config.profile.staging_tools),
|
||||||
resident_summary,
|
resident_summary_source,
|
||||||
system_prompt_override,
|
system_prompt_override,
|
||||||
resolved_config: config,
|
resolved_config: config,
|
||||||
}))
|
}))
|
||||||
@@ -559,7 +569,6 @@ mod tests {
|
|||||||
prompts.clone(),
|
prompts.clone(),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(disabled.is_none());
|
assert!(disabled.is_none());
|
||||||
|
|
||||||
@@ -573,7 +582,6 @@ mod tests {
|
|||||||
prompts.clone(),
|
prompts.clone(),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
enabled
|
enabled
|
||||||
@@ -592,7 +600,6 @@ mod tests {
|
|||||||
prompts.clone(),
|
prompts.clone(),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
let plan = MemoryFeatureInstallPlan::prepare_resolved(
|
let plan = MemoryFeatureInstallPlan::prepare_resolved(
|
||||||
@@ -601,10 +608,9 @@ mod tests {
|
|||||||
prompts.clone(),
|
prompts.clone(),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(plan.resident_summary.is_none());
|
assert!(plan.resident_summary_source.is_none());
|
||||||
assert!(plan.system_prompt_override.is_none());
|
assert!(plan.system_prompt_override.is_none());
|
||||||
|
|
||||||
enabled.profile.resident.inject_summary = true;
|
enabled.profile.resident.inject_summary = true;
|
||||||
@@ -614,14 +620,20 @@ mod tests {
|
|||||||
prompts,
|
prompts,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(plan.resident_summary.as_deref(), Some("# Durable Memory"));
|
assert_eq!(
|
||||||
|
plan.resident_summary_source
|
||||||
|
.unwrap()
|
||||||
|
.load()
|
||||||
|
.await
|
||||||
|
.as_deref(),
|
||||||
|
Some("# Durable Memory")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn memory_prompt_contribution_rereads_resident_summary_for_each_install() {
|
async fn memory_prompt_contribution_defers_resident_summary_until_loaded() {
|
||||||
let prompts = crate::prompt::catalog::PromptCatalog::builtins_only().unwrap();
|
let prompts = crate::prompt::catalog::PromptCatalog::builtins_only().unwrap();
|
||||||
let mut config = manifest::ResolvedMemoryFeatureConfig::default();
|
let mut config = manifest::ResolvedMemoryFeatureConfig::default();
|
||||||
config.profile.enabled = true;
|
config.profile.enabled = true;
|
||||||
@@ -639,7 +651,6 @@ mod tests {
|
|||||||
prompts.clone(),
|
prompts.clone(),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let restored = MemoryFeatureInstallPlan::prepare_resolved(
|
let restored = MemoryFeatureInstallPlan::prepare_resolved(
|
||||||
@@ -648,16 +659,25 @@ mod tests {
|
|||||||
prompts,
|
prompts,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
first.resident_summary.as_deref(),
|
first
|
||||||
|
.resident_summary_source
|
||||||
|
.unwrap()
|
||||||
|
.load()
|
||||||
|
.await
|
||||||
|
.as_deref(),
|
||||||
Some("first resident summary")
|
Some("first resident summary")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
restored.resident_summary.as_deref(),
|
restored
|
||||||
|
.resident_summary_source
|
||||||
|
.unwrap()
|
||||||
|
.load()
|
||||||
|
.await
|
||||||
|
.as_deref(),
|
||||||
Some("updated resident summary")
|
Some("updated resident summary")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ struct ModelBlocker {
|
|||||||
ticket: String,
|
ticket: String,
|
||||||
kind: String,
|
kind: String,
|
||||||
state: Option<String>,
|
state: Option<String>,
|
||||||
resolved: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -436,7 +435,6 @@ fn project_blocker(value: &Value) -> Result<ModelBlocker, String> {
|
|||||||
ticket: resource_ref(blocker, "blocking_resource_key", "T-")?,
|
ticket: resource_ref(blocker, "blocking_resource_key", "T-")?,
|
||||||
kind: string_field(blocker, "relation_kind")?,
|
kind: string_field(blocker, "relation_kind")?,
|
||||||
state: optional_string(blocker, "blocking_state")?,
|
state: optional_string(blocker, "blocking_state")?,
|
||||||
resolved: bool_field(blocker, "resolved")?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,6 +758,70 @@ mod tests {
|
|||||||
assert!(!objective_json.contains("00001TICKETINTERNAL"));
|
assert!(!objective_json.contains("00001TICKETINTERNAL"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ticket_detail_projection_accepts_current_blocker_shape() {
|
||||||
|
let projected = project_ticket_detail(json!({
|
||||||
|
"id": "internal-ticket",
|
||||||
|
"resource_key": "T-588",
|
||||||
|
"title": "Queued Submit",
|
||||||
|
"body": "Body",
|
||||||
|
"state": "planning",
|
||||||
|
"readiness": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"created_at": "2026-09-03T00:00:00Z",
|
||||||
|
"updated_at": "2026-09-03T00:00:00Z",
|
||||||
|
"events": [],
|
||||||
|
"relations": {
|
||||||
|
"outgoing": [],
|
||||||
|
"incoming": [],
|
||||||
|
"blockers": [{
|
||||||
|
"blocking_ticket": "internal-blocker",
|
||||||
|
"blocking_resource_key": "T-584",
|
||||||
|
"reason_kind": "depends_on",
|
||||||
|
"relation_kind": "depends_on",
|
||||||
|
"note": "required foundation",
|
||||||
|
"blocking_state": "planning"
|
||||||
|
}],
|
||||||
|
"notices": []
|
||||||
|
},
|
||||||
|
"linked_objectives": [],
|
||||||
|
"implementation_reports": [],
|
||||||
|
"assignments": [],
|
||||||
|
"current_coder": null,
|
||||||
|
"merge_request": null,
|
||||||
|
"evidence": {
|
||||||
|
"has_merge_request": false,
|
||||||
|
"has_current_subject_ref": false,
|
||||||
|
"has_review_request": false,
|
||||||
|
"has_commit": false,
|
||||||
|
"review_status": null,
|
||||||
|
"approved_current_subject": false,
|
||||||
|
"unresolved_request_changes": false,
|
||||||
|
"complete_for_integration": false,
|
||||||
|
"missing": ["merge_request"]
|
||||||
|
},
|
||||||
|
"action_eligibility": {
|
||||||
|
"can_assign_orchestrator": true,
|
||||||
|
"can_unassign_orchestrator": false,
|
||||||
|
"can_queue": false,
|
||||||
|
"can_start_manual_coder": false
|
||||||
|
},
|
||||||
|
"event_page": {"next_cursor": null, "has_more": false}
|
||||||
|
}))
|
||||||
|
.expect("current Ticket blocker shape must project");
|
||||||
|
|
||||||
|
let projected = serde_json::to_value(projected).expect("serialize Ticket detail");
|
||||||
|
assert_eq!(
|
||||||
|
projected["relations"]["blockers"],
|
||||||
|
json!([{
|
||||||
|
"ticket": "T-584",
|
||||||
|
"kind": "depends_on",
|
||||||
|
"state": "planning"
|
||||||
|
}])
|
||||||
|
);
|
||||||
|
assert!(!projected.to_string().contains("internal-blocker"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn relation_projection_accepts_current_workspace_api_shapes() {
|
fn relation_projection_accepts_current_workspace_api_shapes() {
|
||||||
let outgoing = project_relation(
|
let outgoing = project_relation(
|
||||||
|
|||||||
@@ -384,14 +384,21 @@ impl Tool for SubWorkerSpawnTool {
|
|||||||
child_bash_output_dir.display()
|
child_bash_output_dir.display()
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
workdir_rules.push(WorkdirDelegationRule {
|
|
||||||
target: WorkdirPath::new_scoped(child_bash_output_dir.to_string_lossy())
|
|
||||||
.map_err(|error| ToolError::ExecutionFailed(error.to_string()))?,
|
|
||||||
permission: WorkdirDelegationPermission::Read,
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
let source_workdir_session =
|
let source_workdir_session =
|
||||||
require_active_workdir_session(self.source_workdir_session.as_ref())?;
|
require_active_workdir_session(self.source_workdir_session.as_ref())?;
|
||||||
|
let transports_delegation_context = source_workdir_session.transports_delegation_context();
|
||||||
|
// Provider-transported sessions resolve every delegation rule in the
|
||||||
|
// receiving Workdir namespace. The Bash spill directory instead belongs
|
||||||
|
// to this Worker host, so forwarding it would widen the request with a
|
||||||
|
// foreign absolute path and fail the provider's existing scope check.
|
||||||
|
if !transports_delegation_context {
|
||||||
|
workdir_rules.push(WorkdirDelegationRule {
|
||||||
|
target: WorkdirPath::new_scoped(child_bash_output_dir.to_string_lossy())
|
||||||
|
.map_err(|error| ToolError::ExecutionFailed(error.to_string()))?,
|
||||||
|
permission: WorkdirDelegationPermission::Read,
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
let delegation_request = workdir_delegation_request(input.cwd.as_deref(), workdir_rules)?;
|
let delegation_request = workdir_delegation_request(input.cwd.as_deref(), workdir_rules)?;
|
||||||
let workdir_delegation = source_workdir_session
|
let workdir_delegation = source_workdir_session
|
||||||
.delegate(delegation_request)
|
.delegate(delegation_request)
|
||||||
@@ -1016,10 +1023,12 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use manifest::{DelegationScope, Permission, Scope, SharedScope};
|
use manifest::{DelegationScope, Permission, Scope, SharedScope};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
use std::sync::Mutex;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::WorkspaceId;
|
use crate::WorkspaceId;
|
||||||
|
use crate::feature::builtin::manage_workdir::WorkspaceAttachedWorkdirSession;
|
||||||
use agen::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent};
|
use agen::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent};
|
||||||
use agen::llm_client::{ClientError, LlmClient, Request};
|
use agen::llm_client::{ClientError, LlmClient, Request};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -1242,6 +1251,15 @@ enabled = false
|
|||||||
let record = registry
|
let record = registry
|
||||||
.get_internal("reviewer-child")
|
.get_internal("reviewer-child")
|
||||||
.expect("Internal reviewer registry record");
|
.expect("Internal reviewer registry record");
|
||||||
|
let child_bash_output_dir = bash_output_dir.join("sub-workers").join("reviewer-child");
|
||||||
|
record
|
||||||
|
.workdir_delegation
|
||||||
|
.scoped_session
|
||||||
|
.stat(workdir::StatRequest {
|
||||||
|
path: WorkdirPath::new_scoped(child_bash_output_dir.to_string_lossy()).unwrap(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("local child retains read scope for its Bash output directory");
|
||||||
for required in ["Read", "Write", "Edit", "Glob", "Grep", "Bash"] {
|
for required in ["Read", "Write", "Edit", "Glob", "Grep", "Bash"] {
|
||||||
assert!(
|
assert!(
|
||||||
record.installed_tools.iter().any(|name| name == required),
|
record.installed_tools.iter().any(|name| name == required),
|
||||||
@@ -1389,6 +1407,130 @@ enabled = false
|
|||||||
assert!(spawner_scope.snapshot().is_writable(&workspace_root));
|
assert!(spawner_scope.snapshot().is_writable(&workspace_root));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn remote_subworker_spawn_keeps_worker_host_output_path_out_of_workdir_scope() {
|
||||||
|
let runtime = TempDir::new().unwrap();
|
||||||
|
let workspace_root = runtime.path().join("worker-host/project");
|
||||||
|
let bash_output_dir = runtime.path().join("worker-host/bash-output");
|
||||||
|
std::fs::create_dir_all(&workspace_root).unwrap();
|
||||||
|
std::fs::create_dir_all(&bash_output_dir).unwrap();
|
||||||
|
|
||||||
|
let mut manifest = parent_manifest(&workspace_root, None);
|
||||||
|
manifest
|
||||||
|
.scope
|
||||||
|
.allow
|
||||||
|
.push(abs_rule(&bash_output_dir, Permission::Read));
|
||||||
|
manifest.delegation_scope = ScopeConfig {
|
||||||
|
allow: vec![abs_rule(&workspace_root, Permission::Write)],
|
||||||
|
deny: Vec::new(),
|
||||||
|
};
|
||||||
|
let spawner_scope = SharedScope::new(Scope::from_config(&manifest.scope).unwrap());
|
||||||
|
let registry = SpawnedWorkerRegistry::new_internal("parent".into(), spawner_scope.clone());
|
||||||
|
let workspace_context = crate::worker::WorkerWorkspaceContext::with_client(
|
||||||
|
Some(WorkspaceId::new("workspace-test").unwrap()),
|
||||||
|
Arc::new(AvailableWorkspaceClient),
|
||||||
|
);
|
||||||
|
let remote_client = Arc::new(StrictRemoteWorkdirWorkspaceClient::default());
|
||||||
|
let source_workdir_session = workdir::delegation_capable_session(
|
||||||
|
WorkspaceAttachedWorkdirSession::handle(remote_client.clone()),
|
||||||
|
);
|
||||||
|
let calls = Arc::new(AtomicUsize::new(0));
|
||||||
|
let (parent_method_tx, _parent_method_rx) = mpsc::channel(8);
|
||||||
|
let tool = SubWorkerSpawnTool::new(
|
||||||
|
"parent".into(),
|
||||||
|
workspace_context,
|
||||||
|
ParentNotificationTarget::Controller(parent_method_tx.downgrade()),
|
||||||
|
runtime.path().to_path_buf(),
|
||||||
|
bash_output_dir.clone(),
|
||||||
|
workspace_root.clone(),
|
||||||
|
Some(source_workdir_session),
|
||||||
|
registry.clone(),
|
||||||
|
manifest,
|
||||||
|
PromptCatalogSource::builtins_only(),
|
||||||
|
AvailableProfiles::discover(&workspace_root),
|
||||||
|
)
|
||||||
|
.with_internal_client(Box::new(ScriptedInternalClient {
|
||||||
|
calls: calls.clone(),
|
||||||
|
parent_scope: spawner_scope,
|
||||||
|
delegated_path: workspace_root.clone(),
|
||||||
|
observed_parent_write_revoked: Arc::new(AtomicBool::new(false)),
|
||||||
|
observed_instruction_override: Arc::new(AtomicBool::new(false)),
|
||||||
|
fail_requests: Arc::new(AtomicBool::new(false)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
tool.execute(
|
||||||
|
&serde_json::json!({
|
||||||
|
"name": "remote-child",
|
||||||
|
"profile": "inherit",
|
||||||
|
"instruction": "role.reviewer",
|
||||||
|
"task": "inspect the remote Workdir",
|
||||||
|
"scope": [{
|
||||||
|
"target": ".",
|
||||||
|
"permission": "write",
|
||||||
|
"recursive": true
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
agen::tool::ToolExecutionContext::direct(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("remote Workdir delegation must not receive Worker-host paths");
|
||||||
|
|
||||||
|
let record = registry
|
||||||
|
.get_internal("remote-child")
|
||||||
|
.expect("remote Internal Worker registry record");
|
||||||
|
assert_eq!(
|
||||||
|
record.session.wait_until_idle().await,
|
||||||
|
crate::internal_worker::InternalWorkerSessionStatus::Idle
|
||||||
|
);
|
||||||
|
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||||
|
assert_eq!(
|
||||||
|
remote_client
|
||||||
|
.foreign_scope_rejections
|
||||||
|
.load(Ordering::SeqCst),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
let child_bash_output_dir = bash_output_dir.join("sub-workers").join("remote-child");
|
||||||
|
assert!(child_bash_output_dir.is_dir());
|
||||||
|
for required in ["Read", "Write", "Edit", "Glob", "Grep", "Bash"] {
|
||||||
|
assert!(
|
||||||
|
record.installed_tools.iter().any(|name| name == required),
|
||||||
|
"remote write-scoped child is missing {required}: {:?}",
|
||||||
|
record.installed_tools
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let remote_requests = remote_client.requests();
|
||||||
|
let operate_requests = remote_requests
|
||||||
|
.iter()
|
||||||
|
.filter(|request| request.body.is_some())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
operate_requests.len(),
|
||||||
|
1,
|
||||||
|
"remote requests: {remote_requests:?}"
|
||||||
|
);
|
||||||
|
let operation_body: serde_json::Value = serde_json::from_str(
|
||||||
|
operate_requests[0]
|
||||||
|
.body
|
||||||
|
.as_deref()
|
||||||
|
.expect("remote operation body"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let rules = operation_body["delegations"][0]["rules"]
|
||||||
|
.as_array()
|
||||||
|
.expect("delegation rules");
|
||||||
|
assert_eq!(rules.len(), 1, "remote operation body: {operation_body}");
|
||||||
|
assert_eq!(rules[0]["target"], "");
|
||||||
|
assert!(
|
||||||
|
!operation_body.to_string().contains(
|
||||||
|
child_bash_output_dir
|
||||||
|
.to_str()
|
||||||
|
.expect("UTF-8 test output directory")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spawn_worker_input_schema_includes_optional_cwd() {
|
fn spawn_worker_input_schema_includes_optional_cwd() {
|
||||||
let schema = serde_json::to_value(schemars::schema_for!(SubWorkerSpawnInput)).unwrap();
|
let schema = serde_json::to_value(schemars::schema_for!(SubWorkerSpawnInput)).unwrap();
|
||||||
@@ -1523,6 +1665,97 @@ enabled = false
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct StrictRemoteWorkdirWorkspaceClient {
|
||||||
|
requests: Mutex<Vec<WorkspaceRequest>>,
|
||||||
|
foreign_scope_rejections: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrictRemoteWorkdirWorkspaceClient {
|
||||||
|
fn requests(&self) -> Vec<WorkspaceRequest> {
|
||||||
|
self.requests
|
||||||
|
.lock()
|
||||||
|
.expect("remote Workdir request lock")
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkspaceClient for StrictRemoteWorkdirWorkspaceClient {
|
||||||
|
fn workspace_id(&self) -> Option<&str> {
|
||||||
|
Some("workspace-test")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> &str {
|
||||||
|
"strict-remote-workdir-test"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_available(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute(
|
||||||
|
&self,
|
||||||
|
request: WorkspaceRequest,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||||
|
self.requests
|
||||||
|
.lock()
|
||||||
|
.expect("remote Workdir request lock")
|
||||||
|
.push(request.clone());
|
||||||
|
if request.path.ends_with("/fence") {
|
||||||
|
return Ok(WorkspaceResponse {
|
||||||
|
status: 200,
|
||||||
|
body: serde_json::json!({ "value": "remote-fence-1" }).to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: serde_json::Value = serde_json::from_str(
|
||||||
|
request
|
||||||
|
.body
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| WorkspaceClientError::Request("missing request body".into()))?,
|
||||||
|
)
|
||||||
|
.map_err(|error| WorkspaceClientError::Request(error.to_string()))?;
|
||||||
|
let has_foreign_scope = body
|
||||||
|
.get("delegations")
|
||||||
|
.and_then(serde_json::Value::as_array)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.flat_map(|delegation| {
|
||||||
|
delegation
|
||||||
|
.get("rules")
|
||||||
|
.and_then(serde_json::Value::as_array)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
})
|
||||||
|
.filter_map(|rule| rule.get("target").and_then(serde_json::Value::as_str))
|
||||||
|
.any(|target| Path::new(target).is_absolute());
|
||||||
|
if has_foreign_scope {
|
||||||
|
self.foreign_scope_rejections.fetch_add(1, Ordering::SeqCst);
|
||||||
|
return Ok(WorkspaceResponse {
|
||||||
|
status: 403,
|
||||||
|
body: serde_json::json!({
|
||||||
|
"code": "out_of_scope",
|
||||||
|
"message": "Worker-host path is outside the remote Workdir namespace"
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(WorkspaceResponse {
|
||||||
|
status: 200,
|
||||||
|
body: serde_json::json!({
|
||||||
|
"operation": "stat",
|
||||||
|
"result": {
|
||||||
|
"path": "",
|
||||||
|
"kind": "directory",
|
||||||
|
"size": 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parent_manifest(root: &Path, deny: Option<&Path>) -> WorkerManifest {
|
fn parent_manifest(root: &Path, deny: Option<&Path>) -> WorkerManifest {
|
||||||
WorkerManifestConfig {
|
WorkerManifestConfig {
|
||||||
worker: WorkerMetaConfig {
|
worker: WorkerMetaConfig {
|
||||||
|
|||||||
+78
-16
@@ -1765,6 +1765,11 @@ impl WorkerSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub(crate) trait SystemPromptContributionSource: Send + Sync {
|
||||||
|
async fn load(&self) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
/// An independent agent execution unit.
|
/// An independent agent execution unit.
|
||||||
///
|
///
|
||||||
/// Holds a [`Engine`] directly and persists session state via
|
/// Holds a [`Engine`] directly and persists session state via
|
||||||
@@ -1908,8 +1913,8 @@ pub struct Worker<C: LlmClient, St: Store> {
|
|||||||
prompts: Arc<ArcSwap<PromptCatalog>>,
|
prompts: Arc<ArcSwap<PromptCatalog>>,
|
||||||
/// Test/internal policy gate for installed resident prompt contributions.
|
/// Test/internal policy gate for installed resident prompt contributions.
|
||||||
inject_resident_summary: bool,
|
inject_resident_summary: bool,
|
||||||
/// Materialized resident prompt context installed by an enabled Feature.
|
/// Deferred resident prompt source installed by an enabled Feature.
|
||||||
feature_resident_summary: Option<String>,
|
feature_resident_summary_source: Option<Arc<dyn SystemPromptContributionSource>>,
|
||||||
/// Complete system prompt replacement installed by an enabled Feature.
|
/// Complete system prompt replacement installed by an enabled Feature.
|
||||||
feature_system_prompt_override: Option<String>,
|
feature_system_prompt_override: Option<String>,
|
||||||
/// Typed user submissions in submit order. K-th entry corresponds to
|
/// Typed user submissions in submit order. K-th entry corresponds to
|
||||||
@@ -2145,7 +2150,7 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
|||||||
runtime_ticket_role: None,
|
runtime_ticket_role: None,
|
||||||
prompts,
|
prompts,
|
||||||
inject_resident_summary: true,
|
inject_resident_summary: true,
|
||||||
feature_resident_summary: None,
|
feature_resident_summary_source: None,
|
||||||
feature_system_prompt_override: None,
|
feature_system_prompt_override: None,
|
||||||
user_segments: Vec::new(),
|
user_segments: Vec::new(),
|
||||||
sink: SegmentLogSink::new(),
|
sink: SegmentLogSink::new(),
|
||||||
@@ -2191,10 +2196,10 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
|||||||
|
|
||||||
pub(crate) fn install_system_prompt_contribution(
|
pub(crate) fn install_system_prompt_contribution(
|
||||||
&mut self,
|
&mut self,
|
||||||
resident_summary: Option<String>,
|
resident_summary_source: Option<Arc<dyn SystemPromptContributionSource>>,
|
||||||
system_prompt_override: Option<String>,
|
system_prompt_override: Option<String>,
|
||||||
) {
|
) {
|
||||||
self.feature_resident_summary = resident_summary;
|
self.feature_resident_summary_source = resident_summary_source;
|
||||||
self.feature_system_prompt_override = system_prompt_override;
|
self.feature_system_prompt_override = system_prompt_override;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3206,10 +3211,14 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let resident_summary = self
|
let resident_summary = if self.inject_resident_summary {
|
||||||
.inject_resident_summary
|
match &self.feature_resident_summary_source {
|
||||||
.then(|| self.feature_resident_summary.clone())
|
Some(source) => source.load().await,
|
||||||
.flatten();
|
None => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let worker_language = worker_language(&self.manifest.engine);
|
let worker_language = worker_language(&self.manifest.engine);
|
||||||
let scope_snapshot = self.scope.snapshot();
|
let scope_snapshot = self.scope.snapshot();
|
||||||
let cwd_for_prompt = self
|
let cwd_for_prompt = self
|
||||||
@@ -5394,7 +5403,7 @@ where
|
|||||||
runtime_ticket_role: None,
|
runtime_ticket_role: None,
|
||||||
prompts: common.prompts,
|
prompts: common.prompts,
|
||||||
inject_resident_summary: true,
|
inject_resident_summary: true,
|
||||||
feature_resident_summary: None,
|
feature_resident_summary_source: None,
|
||||||
feature_system_prompt_override: None,
|
feature_system_prompt_override: None,
|
||||||
user_segments: Vec::new(),
|
user_segments: Vec::new(),
|
||||||
sink: SegmentLogSink::new(),
|
sink: SegmentLogSink::new(),
|
||||||
@@ -5478,7 +5487,7 @@ where
|
|||||||
runtime_ticket_role: None,
|
runtime_ticket_role: None,
|
||||||
prompts: common.prompts,
|
prompts: common.prompts,
|
||||||
inject_resident_summary: true,
|
inject_resident_summary: true,
|
||||||
feature_resident_summary: None,
|
feature_resident_summary_source: None,
|
||||||
feature_system_prompt_override: None,
|
feature_system_prompt_override: None,
|
||||||
user_segments: Vec::new(),
|
user_segments: Vec::new(),
|
||||||
sink: SegmentLogSink::new(),
|
sink: SegmentLogSink::new(),
|
||||||
@@ -5596,7 +5605,7 @@ where
|
|||||||
runtime_ticket_role: None,
|
runtime_ticket_role: None,
|
||||||
prompts: common.prompts,
|
prompts: common.prompts,
|
||||||
inject_resident_summary: true,
|
inject_resident_summary: true,
|
||||||
feature_resident_summary: None,
|
feature_resident_summary_source: None,
|
||||||
feature_system_prompt_override: None,
|
feature_system_prompt_override: None,
|
||||||
user_segments: Vec::new(),
|
user_segments: Vec::new(),
|
||||||
sink: SegmentLogSink::new(),
|
sink: SegmentLogSink::new(),
|
||||||
@@ -5971,7 +5980,7 @@ where
|
|||||||
runtime_ticket_role: None,
|
runtime_ticket_role: None,
|
||||||
prompts: common.prompts,
|
prompts: common.prompts,
|
||||||
inject_resident_summary: true,
|
inject_resident_summary: true,
|
||||||
feature_resident_summary: None,
|
feature_resident_summary_source: None,
|
||||||
feature_system_prompt_override: None,
|
feature_system_prompt_override: None,
|
||||||
user_segments: state.user_segments,
|
user_segments: state.user_segments,
|
||||||
// Seed the mirror with the entries we just replayed so a
|
// Seed the mirror with the entries we just replayed so a
|
||||||
@@ -7345,6 +7354,32 @@ permission = "read"
|
|||||||
mod build_summary_prompt_tests {
|
mod build_summary_prompt_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
struct TestSystemPromptContributionSource {
|
||||||
|
value: Option<String>,
|
||||||
|
load_count: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl SystemPromptContributionSource for TestSystemPromptContributionSource {
|
||||||
|
async fn load(&self) -> Option<String> {
|
||||||
|
self.load_count.fetch_add(1, Ordering::SeqCst);
|
||||||
|
self.value.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_system_prompt_contribution_source(
|
||||||
|
value: Option<String>,
|
||||||
|
) -> (Arc<dyn SystemPromptContributionSource>, Arc<AtomicUsize>) {
|
||||||
|
let load_count = Arc::new(AtomicUsize::new(0));
|
||||||
|
(
|
||||||
|
Arc::new(TestSystemPromptContributionSource {
|
||||||
|
value,
|
||||||
|
load_count: Arc::clone(&load_count),
|
||||||
|
}),
|
||||||
|
load_count,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn test_summary_input(items: &[Item]) -> String {
|
fn test_summary_input(items: &[Item]) -> String {
|
||||||
build_summary_input(
|
build_summary_input(
|
||||||
items,
|
items,
|
||||||
@@ -8801,6 +8836,32 @@ mod build_summary_prompt_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn worker_without_initial_system_prompt_does_not_load_feature_contribution() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let cwd = dir.path().join("workspace");
|
||||||
|
std::fs::create_dir_all(&cwd).unwrap();
|
||||||
|
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
|
||||||
|
let mut worker = Worker::new(
|
||||||
|
minimal_manifest(),
|
||||||
|
Engine::<_, Mutable, SessionHistoryMetadata>::new_annotated(NoopClient),
|
||||||
|
store,
|
||||||
|
WorkerWorkspaceContext::no_workspace(),
|
||||||
|
WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone()),
|
||||||
|
Scope::writable(&cwd).unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let (source, load_count) =
|
||||||
|
test_system_prompt_contribution_source(Some("# Durable Memory".to_string()));
|
||||||
|
worker.install_system_prompt_contribution(Some(source), None);
|
||||||
|
|
||||||
|
worker.ensure_system_prompt_materialized().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(load_count.load(Ordering::SeqCst), 0);
|
||||||
|
assert!(worker.history().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn memory_consolidation_prompt_uses_bound_workspace_language() {
|
async fn memory_consolidation_prompt_uses_bound_workspace_language() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -8899,16 +8960,17 @@ mod build_summary_prompt_tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
worker.set_resident_memory_injection(gates.summary);
|
worker.set_resident_memory_injection(gates.summary);
|
||||||
let resident_summary = if memory_config
|
let resident_summary_source = if memory_config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|cfg| cfg.profile.resident.inject_summary)
|
.is_some_and(|cfg| cfg.profile.resident.inject_summary)
|
||||||
&& gates.summary
|
&& gates.summary
|
||||||
{
|
{
|
||||||
summary_doc.and_then(summary_content_for_backend)
|
let summary = summary_doc.and_then(summary_content_for_backend);
|
||||||
|
Some(test_system_prompt_contribution_source(summary).0)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
worker.install_system_prompt_contribution(resident_summary, None);
|
worker.install_system_prompt_contribution(resident_summary_source, None);
|
||||||
let template = SystemPromptTemplate::parse(
|
let template = SystemPromptTemplate::parse(
|
||||||
"default",
|
"default",
|
||||||
crate::prompt::source::PromptCatalogSource::builtins_only(),
|
crate::prompt::source::PromptCatalogSource::builtins_only(),
|
||||||
|
|||||||
@@ -813,13 +813,25 @@ permission = "write"
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn builtin_orchestrator_exposes_worker_remove_and_workdir_delete() {
|
async fn builtin_orchestrator_exposes_worker_remove_and_workdir_delete() {
|
||||||
let workspace = tempfile::tempdir().unwrap();
|
let workspace = tempfile::tempdir().unwrap();
|
||||||
let resolved = ProfileResolver::new()
|
let mut resolved = ProfileResolver::new()
|
||||||
.with_workspace_base(workspace.path())
|
.with_workspace_base(workspace.path())
|
||||||
.resolve(
|
.resolve(
|
||||||
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, "orchestrator"),
|
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, "orchestrator"),
|
||||||
ProfileResolveOptions::with_worker_name("orchestrator-worker"),
|
ProfileResolveOptions::with_worker_name("orchestrator-worker"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
if resolved.manifest.feature.memory.enabled() {
|
||||||
|
resolved
|
||||||
|
.manifest
|
||||||
|
.feature
|
||||||
|
.memory
|
||||||
|
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
|
||||||
|
workspace_id: "workspace-test".to_string(),
|
||||||
|
settings_revision: 1,
|
||||||
|
language: "English".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
let workspace_context =
|
let workspace_context =
|
||||||
WorkerWorkspaceContext::with_client(None, Arc::new(NoopWorkspaceClient));
|
WorkerWorkspaceContext::with_client(None, Arc::new(NoopWorkspaceClient));
|
||||||
let client = MockClient::new(simple_text_events());
|
let client = MockClient::new(simple_text_events());
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ fn main_config_contract_with_schema(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
|
||||||
#[ts(export)]
|
|
||||||
pub struct WorkspaceConfigState {
|
pub struct WorkspaceConfigState {
|
||||||
pub snapshot: ConfigTreeSnapshot,
|
pub snapshot: ConfigTreeSnapshot,
|
||||||
pub contract: ToolchainContract,
|
pub contract: ToolchainContract,
|
||||||
@@ -118,7 +117,6 @@ pub struct EvaluatedConfigCandidate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
|
||||||
#[ts(export)]
|
|
||||||
pub struct ConfigCommitRequest {
|
pub struct ConfigCommitRequest {
|
||||||
#[ts(type = "number")]
|
#[ts(type = "number")]
|
||||||
pub base_revision: u64,
|
pub base_revision: u64,
|
||||||
|
|||||||
@@ -567,7 +567,7 @@ Deno.test("Worker Console composer keeps a compact bounded chip editor", async (
|
|||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
consolePage.includes("<ComposerInput") &&
|
consolePage.includes("<ComposerInput") &&
|
||||||
consolePage.includes('<div class="composer-input-shell">') &&
|
consolePage.includes('class="composer-input-shell"') &&
|
||||||
!consolePage.includes("handleComposerShellClick") &&
|
!consolePage.includes("handleComposerShellClick") &&
|
||||||
consolePage.includes("bind:this={composerInputElement}") &&
|
consolePage.includes("bind:this={composerInputElement}") &&
|
||||||
consolePage.includes("onchange={handleComposerChange}") &&
|
consolePage.includes("onchange={handleComposerChange}") &&
|
||||||
@@ -616,7 +616,7 @@ Deno.test("Worker Console paste chips preserve typed draft and target authority"
|
|||||||
consolePage.includes("if (!composerEditable) return") &&
|
consolePage.includes("if (!composerEditable) return") &&
|
||||||
composerInput.includes('chip.setAttribute("aria-label", label)') &&
|
composerInput.includes('chip.setAttribute("aria-label", label)') &&
|
||||||
composerInput.includes("preserveExactText = false") &&
|
composerInput.includes("preserveExactText = false") &&
|
||||||
consolePage.includes("buildComposerSegmentsRequest(value.segments, {") &&
|
consolePage.includes("const command = buildComposerSegmentsRequest(") &&
|
||||||
consolePage.includes("preserveExactText: value.textPastes.length > 0") &&
|
consolePage.includes("preserveExactText: value.textPastes.length > 0") &&
|
||||||
consolePage.includes("composerDrafts.set(activeComposerTargetKey") &&
|
consolePage.includes("composerDrafts.set(activeComposerTargetKey") &&
|
||||||
consolePage.includes("switchComposerTarget(target)") &&
|
consolePage.includes("switchComposerTarget(target)") &&
|
||||||
|
|||||||
Reference in New Issue
Block a user