merge(infra): temp IdeA géré visible par l'agent avant le spawn — #200 (QA verte)
This commit is contained in:
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#200"
|
issueRef: "#200"
|
||||||
version: 13
|
version: 16
|
||||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||||
updatedAt: 1786745694791
|
updatedAt: 1787384954575
|
||||||
---
|
---
|
||||||
2026-08-14 Main
|
2026-08-14 Main
|
||||||
- Reprise du ticket après livraison initiale du lot Codex-centric.
|
- Reprise du ticket après livraison initiale du lot Codex-centric.
|
||||||
@ -12,3 +12,11 @@ updatedAt: 1786745694791
|
|||||||
- DevBackend: implémentation terminée. Résumé: `ManagedTempStore::purge_all` + use case `PurgeManagedTemp`, câblage backend/app-tauri au shutdown, test structured non-Codex pour env+sandbox sans writable_roots provider-specific, conservation des garanties Codex/stop/release/union sans doublons.
|
- DevBackend: implémentation terminée. Résumé: `ManagedTempStore::purge_all` + use case `PurgeManagedTemp`, câblage backend/app-tauri au shutdown, test structured non-Codex pour env+sandbox sans writable_roots provider-specific, conservation des garanties Codex/stop/release/union sans doublons.
|
||||||
- QA: verdict VERT sur l'extension. Commandes validées: `cargo test -p application --test agent_lifecycle launch_pty_allocates_managed_tmp_env_and_sandbox_from_same_path`, `cargo test -p application --test agent_lifecycle launch_structured_non_codex_uses_managed_tmp_for_env_and_sandbox_without_writable_roots`, `cargo test -p application --test agent_lifecycle launch_structured_codex_uses_same_managed_tmp_for_env_sandbox_and_writable_roots`, `cargo test -p application --test agent_lifecycle stop_pty_session_releases_primary_launch_managed_tmp`, `cargo test -p application --test agent_lifecycle launch_managed_tmp_and_exposure_roots_are_union_without_duplicates`, `cargo test -p application --test managed_temp purge_managed_temp_calls_store_and_returns_report`, `cargo test -p infrastructure --test managed_temp purge`, `cargo test -p app-tauri --lib shutdown_managed_temp_purge_is_attempted_and_non_blocking_on_error`, `cargo test -p backend --lib --no-run`.
|
- QA: verdict VERT sur l'extension. Commandes validées: `cargo test -p application --test agent_lifecycle launch_pty_allocates_managed_tmp_env_and_sandbox_from_same_path`, `cargo test -p application --test agent_lifecycle launch_structured_non_codex_uses_managed_tmp_for_env_and_sandbox_without_writable_roots`, `cargo test -p application --test agent_lifecycle launch_structured_codex_uses_same_managed_tmp_for_env_sandbox_and_writable_roots`, `cargo test -p application --test agent_lifecycle stop_pty_session_releases_primary_launch_managed_tmp`, `cargo test -p application --test agent_lifecycle launch_managed_tmp_and_exposure_roots_are_union_without_duplicates`, `cargo test -p application --test managed_temp purge_managed_temp_calls_store_and_returns_report`, `cargo test -p infrastructure --test managed_temp purge`, `cargo test -p app-tauri --lib shutdown_managed_temp_purge_is_attempted_and_non_blocking_on_error`, `cargo test -p backend --lib --no-run`.
|
||||||
- Suite du cycle: commit/merge local par Git, puis fermeture du ticket.
|
- Suite du cycle: commit/merge local par Git, puis fermeture du ticket.
|
||||||
|
|
||||||
|
2026-08-22 Main
|
||||||
|
- Réouverture suite à un cas réel observé utilisateur: un agent a annoncé vouloir cloner Flutter dans `/tmp` malgré le lot livré.
|
||||||
|
- Vérification Main: le runtime de launch injecte bien `TMPDIR`, `TMP`, `TEMP` depuis le managed temp principal (`crates/application/src/agent/lifecycle.rs`, étape 5d) et les tests `agent_lifecycle` couvrent PTY, structured non-Codex et structured Codex.
|
||||||
|
- Analyse Architect: pas un gap de design; suspicion de chemin secondaire ou de comportement agent qui continue d'utiliser/mentionner `/tmp` explicitement au lieu de suivre l'env temp fourni.
|
||||||
|
- Suite proposée: reproduire le cas réel avec le provider/profil concerné, vérifier la valeur réellement vue dans le process agent, puis décider entre fix runtime (si env perdu/contourné) et renfort d'instruction/runtime API (si l'agent hardcode `/tmp`).
|
||||||
|
- Git a retenu une branche de fix dédiée `fix/200-managed-tmp-injection-regression` depuis `develop`, HEAD `3344361fc27bace7f68953f7a11623af1d8a7d86`.
|
||||||
|
- DevBackend: diagnostic retenu = l'env temp était déjà injecté, mais le convention file était généré avant l'allocation du temp, donc aucun signal textuel explicite ne guidait l'agent; correctif implémenté = allocation plus tôt dans le launch path, ajout de `IDEA_MANAGED_TMP`, et injection conditionnelle d'une instruction “Temporaire IdeA géré” avec le chemin concret; tests agent_lifecycle renforcés.
|
||||||
@ -2,7 +2,7 @@
|
|||||||
id: "0268a6a4-2ceb-4332-a113-29b93a59caf1"
|
id: "0268a6a4-2ceb-4332-a113-29b93a59caf1"
|
||||||
number: 200
|
number: 200
|
||||||
title: "Remplacer l'usage implicite de /tmp par un temp IdeA géré pour les agents"
|
title: "Remplacer l'usage implicite de /tmp par un temp IdeA géré pour les agents"
|
||||||
status: "closed"
|
status: "inProgress"
|
||||||
priority: "high"
|
priority: "high"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: []
|
links: []
|
||||||
@ -11,7 +11,7 @@ attachments: []
|
|||||||
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
|
||||||
createdAt: 1786743649446
|
createdAt: 1786743649446
|
||||||
updatedAt: 1786745694791
|
updatedAt: 1787384954575
|
||||||
version: 13
|
version: 16
|
||||||
---
|
---
|
||||||
Extension du lot temp IdeA géré. Le temp principal launch-scoped existe déjà mais le comportement doit maintenant être généralisé à tous les agents, pas seulement Codex, afin que n'importe quel agent puisse utiliser le répertoire temporaire IdeA via ses variables d'environnement/runtime appropriées. En plus du release au stop de session déjà en place, IdeA doit purger le contenu de ce stockage temporaire à la fermeture de l'application pour éviter l'accumulation résiduelle. Le lot couvre donc: projection cross-agent du temp IdeA principal, cohérence sandbox/runtime selon les profils, et purge à l'extinction d'IdeA.
|
Extension du lot temp IdeA géré. Le temp principal launch-scoped existe déjà mais le comportement doit maintenant être généralisé à tous les agents, pas seulement Codex, afin que n'importe quel agent puisse utiliser le répertoire temporaire IdeA via ses variables d'environnement/runtime appropriées. En plus du release au stop de session déjà en place, IdeA doit purger le contenu de ce stockage temporaire à la fermeture de l'application pour éviter l'accumulation résiduelle. Le lot couvre donc: projection cross-agent du temp IdeA principal, cohérence sandbox/runtime selon les profils, et purge à l'extinction d'IdeA.
|
||||||
@ -2278,18 +2278,42 @@ impl LaunchAgent {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
self.apply_injection(
|
let launch_managed_temp = self
|
||||||
|
.allocate_launch_managed_temp(&input.project, &agent)
|
||||||
|
.await?;
|
||||||
|
if let Some(temp) = &launch_managed_temp {
|
||||||
|
upsert_env(&mut spec.env, "TMPDIR", &temp.path);
|
||||||
|
upsert_env(&mut spec.env, "TMP", &temp.path);
|
||||||
|
upsert_env(&mut spec.env, "TEMP", &temp.path);
|
||||||
|
upsert_env(&mut spec.env, "IDEA_MANAGED_TMP", &temp.path);
|
||||||
|
}
|
||||||
|
if let Err(err) = self
|
||||||
|
.apply_injection(
|
||||||
&input.project,
|
&input.project,
|
||||||
&agent.context_path,
|
&agent.context_path,
|
||||||
&effective_context,
|
&effective_context,
|
||||||
profile.mcp.is_some(),
|
profile.mcp.is_some(),
|
||||||
|
launch_managed_temp.as_ref().map(|temp| temp.path.as_str()),
|
||||||
&mut spec,
|
&mut spec,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
{
|
||||||
|
self.release_launch_managed_temp_best_effort(launch_managed_temp.as_ref())
|
||||||
|
.await;
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
let local_model_server_guard = self
|
let local_model_server_guard = match self
|
||||||
.ensure_local_model_server_for_opencode(&input.project, &agent, &mut profile)
|
.ensure_local_model_server_for_opencode(&input.project, &agent, &mut profile)
|
||||||
.await?;
|
.await
|
||||||
|
{
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(err) => {
|
||||||
|
self.release_launch_managed_temp_best_effort(launch_managed_temp.as_ref())
|
||||||
|
.await;
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 5a. ── INJECTION DE LA CONF MCP (cadrage v3, Décision 3) ──
|
// 5a. ── INJECTION DE LA CONF MCP (cadrage v3, Décision 3) ──
|
||||||
// Strictement APRÈS le convention file (étape 5) et AVANT le spawn /
|
// Strictement APRÈS le convention file (étape 5) et AVANT le spawn /
|
||||||
@ -2297,7 +2321,8 @@ impl LaunchAgent {
|
|||||||
// IdeA matérialise SA config MCP au format de CETTE CLI, dans le **même**
|
// IdeA matérialise SA config MCP au format de CETTE CLI, dans le **même**
|
||||||
// run dir isolé que le convention file et le seed de permissions. `None`
|
// run dir isolé que le convention file et le seed de permissions. `None`
|
||||||
// ⇒ aucun write/flag/env (chemin actuel inchangé, zéro régression).
|
// ⇒ aucun write/flag/env (chemin actuel inchangé, zéro régression).
|
||||||
self.apply_mcp_config(
|
if let Err(err) = self
|
||||||
|
.apply_mcp_config(
|
||||||
&profile,
|
&profile,
|
||||||
&run_dir,
|
&run_dir,
|
||||||
&input.project.root,
|
&input.project.root,
|
||||||
@ -2305,7 +2330,12 @@ impl LaunchAgent {
|
|||||||
effective_permissions.as_ref(),
|
effective_permissions.as_ref(),
|
||||||
&mut spec,
|
&mut spec,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
{
|
||||||
|
self.release_launch_managed_temp_best_effort(launch_managed_temp.as_ref())
|
||||||
|
.await;
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
// 5c. ── PROJECTION DES PERMISSIONS (lot LP3-3) ──
|
// 5c. ── PROJECTION DES PERMISSIONS (lot LP3-3) ──
|
||||||
// Strictement APRÈS le convention file (5) ET la conf MCP (5a), donc
|
// Strictement APRÈS le convention file (5) ET la conf MCP (5a), donc
|
||||||
@ -2313,7 +2343,8 @@ impl LaunchAgent {
|
|||||||
// héritent de la projection (fichiers du plan écrits dans le run dir,
|
// héritent de la projection (fichiers du plan écrits dans le run dir,
|
||||||
// `args`/`env` foldés dans `spec`). `None` registre / profil non
|
// `args`/`env` foldés dans `spec`). `None` registre / profil non
|
||||||
// projetable / `eff == None` ⇒ no-op (prompting natif conservé).
|
// projetable / `eff == None` ⇒ no-op (prompting natif conservé).
|
||||||
self.apply_permission_projection(
|
if let Err(err) = self
|
||||||
|
.apply_permission_projection(
|
||||||
&profile,
|
&profile,
|
||||||
&run_dir,
|
&run_dir,
|
||||||
&input.project.root,
|
&input.project.root,
|
||||||
@ -2321,7 +2352,12 @@ impl LaunchAgent {
|
|||||||
network_permission,
|
network_permission,
|
||||||
&mut spec,
|
&mut spec,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
{
|
||||||
|
self.release_launch_managed_temp_best_effort(launch_managed_temp.as_ref())
|
||||||
|
.await;
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
// 5d. ── PLAN DE SANDBOX OS (lot LP4-3) ──
|
// 5d. ── PLAN DE SANDBOX OS (lot LP4-3) ──
|
||||||
// Strictement APRÈS la résolution des permissions (3) et la projection
|
// Strictement APRÈS la résolution des permissions (3) et la projection
|
||||||
@ -2331,14 +2367,6 @@ impl LaunchAgent {
|
|||||||
// remplir `spec.sandbox`. `eff == None` (rien posé) ⇒ `None` : aucun plan,
|
// remplir `spec.sandbox`. `eff == None` (rien posé) ⇒ `None` : aucun plan,
|
||||||
// comportement natif conservé (invariant produit). L'enforcer concret
|
// comportement natif conservé (invariant produit). L'enforcer concret
|
||||||
// (Landlock/Noop) est injecté côté PTY au composition root.
|
// (Landlock/Noop) est injecté côté PTY au composition root.
|
||||||
let launch_managed_temp = self
|
|
||||||
.allocate_launch_managed_temp(&input.project, &agent)
|
|
||||||
.await?;
|
|
||||||
if let Some(temp) = &launch_managed_temp {
|
|
||||||
upsert_env(&mut spec.env, "TMPDIR", &temp.path);
|
|
||||||
upsert_env(&mut spec.env, "TMP", &temp.path);
|
|
||||||
upsert_env(&mut spec.env, "TEMP", &temp.path);
|
|
||||||
}
|
|
||||||
let exposed_managed_temp_roots = match self
|
let exposed_managed_temp_roots = match self
|
||||||
.resolve_managed_temp_exposure(&input.managed_temp_exposure)
|
.resolve_managed_temp_exposure(&input.managed_temp_exposure)
|
||||||
.await
|
.await
|
||||||
@ -2916,6 +2944,7 @@ impl LaunchAgent {
|
|||||||
context_rel_path: &str,
|
context_rel_path: &str,
|
||||||
effective: &EffectiveAgentContext,
|
effective: &EffectiveAgentContext,
|
||||||
mcp_enabled: bool,
|
mcp_enabled: bool,
|
||||||
|
launch_managed_temp_path: Option<&str>,
|
||||||
spec: &mut SpawnSpec,
|
spec: &mut SpawnSpec,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
match spec.context_plan.clone() {
|
match spec.context_plan.clone() {
|
||||||
@ -2927,8 +2956,12 @@ impl LaunchAgent {
|
|||||||
// composed: an absolute project-root header (so the agent knows
|
// composed: an absolute project-root header (so the agent knows
|
||||||
// where to operate, since its cwd is *not* the root), the agent's
|
// where to operate, since its cwd is *not* the root), the agent's
|
||||||
// persona `.md`, then the bodies of its assigned skills (§14.2).
|
// persona `.md`, then the bodies of its assigned skills (§14.2).
|
||||||
let document =
|
let document = compose_convention_file(
|
||||||
compose_convention_file(project.root.as_str(), effective, mcp_enabled);
|
project.root.as_str(),
|
||||||
|
effective,
|
||||||
|
mcp_enabled,
|
||||||
|
launch_managed_temp_path,
|
||||||
|
);
|
||||||
let path = RemotePath::new(join(&spec.cwd, &target));
|
let path = RemotePath::new(join(&spec.cwd, &target));
|
||||||
self.fs.write(&path, document.as_bytes()).await?;
|
self.fs.write(&path, document.as_bytes()).await?;
|
||||||
}
|
}
|
||||||
@ -3864,6 +3897,7 @@ pub(crate) fn compose_convention_file(
|
|||||||
project_root: &str,
|
project_root: &str,
|
||||||
effective: &EffectiveAgentContext,
|
effective: &EffectiveAgentContext,
|
||||||
mcp_enabled: bool,
|
mcp_enabled: bool,
|
||||||
|
launch_managed_temp_path: Option<&str>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str("# Project root\n\n");
|
out.push_str("# Project root\n\n");
|
||||||
@ -3949,6 +3983,12 @@ pub(crate) fn compose_convention_file(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
out.push_str(skill_awareness(mcp_enabled));
|
out.push_str(skill_awareness(mcp_enabled));
|
||||||
|
if let Some(path) = launch_managed_temp_path
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|path| !path.is_empty())
|
||||||
|
{
|
||||||
|
out.push_str(&managed_temp_awareness(path));
|
||||||
|
}
|
||||||
out.push_str(memory_awareness());
|
out.push_str(memory_awareness());
|
||||||
out.push_str("---\n\n");
|
out.push_str("---\n\n");
|
||||||
|
|
||||||
@ -4087,6 +4127,14 @@ fn memory_awareness() -> &'static str {
|
|||||||
réutilise le même `slug` pour remplacer une note existante.\n\n"
|
réutilise le même `slug` pour remplacer une note existante.\n\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn managed_temp_awareness(path: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"**Temporaire IdeA géré** : utilise `{path}` pour les gros fichiers temporaires, \
|
||||||
|
clones, caches et artefacts de travail. Ce chemin est aussi exposé via \
|
||||||
|
`IDEA_MANAGED_TMP`, `TMPDIR`, `TMP` et `TEMP`; privilégie-le à `/tmp`.\n\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Renders a [`MemoryType`] as its stable lowercase label for the convention-file
|
/// Renders a [`MemoryType`] as its stable lowercase label for the convention-file
|
||||||
/// memory section (`user`/`feedback`/`project`/`reference`).
|
/// memory section (`user`/`feedback`/`project`/`reference`).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@ -4182,7 +4230,7 @@ mod tests {
|
|||||||
handoff: handoff.cloned(),
|
handoff: handoff.cloned(),
|
||||||
live_rows: live_rows.to_vec(),
|
live_rows: live_rows.to_vec(),
|
||||||
};
|
};
|
||||||
super::compose_convention_file(project_root, &effective, mcp_enabled)
|
super::compose_convention_file(project_root, &effective, mcp_enabled, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -4732,7 +4732,7 @@ async fn launch_pty_allocates_managed_tmp_env_and_sandbox_from_same_path() {
|
|||||||
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
||||||
)
|
)
|
||||||
.with_projector(ProjectorKey::Claude);
|
.with_projector(ProjectorKey::Claude);
|
||||||
let (launch, agent, _fs, pty, _sessions) = launch_with_projection(
|
let (launch, agent, fs, pty, _sessions) = launch_with_projection(
|
||||||
profile,
|
profile,
|
||||||
Some(ContextInjectionPlan::File {
|
Some(ContextInjectionPlan::File {
|
||||||
target: "CLAUDE.md".to_owned(),
|
target: "CLAUDE.md".to_owned(),
|
||||||
@ -4752,6 +4752,21 @@ async fn launch_pty_allocates_managed_tmp_env_and_sandbox_from_same_path() {
|
|||||||
assert_eq!(env.get("TMPDIR"), Some(&tmp));
|
assert_eq!(env.get("TMPDIR"), Some(&tmp));
|
||||||
assert_eq!(env.get("TMP"), Some(&tmp));
|
assert_eq!(env.get("TMP"), Some(&tmp));
|
||||||
assert_eq!(env.get("TEMP"), Some(&tmp));
|
assert_eq!(env.get("TEMP"), Some(&tmp));
|
||||||
|
assert_eq!(env.get("IDEA_MANAGED_TMP"), Some(&tmp));
|
||||||
|
let writes = fs.context_writes();
|
||||||
|
let convention = String::from_utf8(writes[0].1.clone()).unwrap();
|
||||||
|
assert!(
|
||||||
|
convention.contains("**Temporaire IdeA géré**"),
|
||||||
|
"convention file must explicitly surface the managed temp directory: {convention}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
convention.contains(&tmp),
|
||||||
|
"convention file must carry the concrete managed temp path: {convention}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
convention.contains("privilégie-le à `/tmp`"),
|
||||||
|
"convention file must steer agents away from literal /tmp usage: {convention}"
|
||||||
|
);
|
||||||
let sandbox = spawn.sandbox.expect("sandbox plan");
|
let sandbox = spawn.sandbox.expect("sandbox plan");
|
||||||
assert!(sandbox_has_rw_root(&sandbox, &tmp));
|
assert!(sandbox_has_rw_root(&sandbox, &tmp));
|
||||||
let _ = std::fs::remove_dir_all(&root);
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
@ -4796,6 +4811,7 @@ async fn launch_structured_non_codex_uses_managed_tmp_for_env_without_os_sandbox
|
|||||||
assert_eq!(env.get("TMPDIR"), Some(&tmp));
|
assert_eq!(env.get("TMPDIR"), Some(&tmp));
|
||||||
assert_eq!(env.get("TMP"), Some(&tmp));
|
assert_eq!(env.get("TMP"), Some(&tmp));
|
||||||
assert_eq!(env.get("TEMP"), Some(&tmp));
|
assert_eq!(env.get("TEMP"), Some(&tmp));
|
||||||
|
assert_eq!(env.get("IDEA_MANAGED_TMP"), Some(&tmp));
|
||||||
assert_eq!(factory.policies(), vec![None]);
|
assert_eq!(factory.policies(), vec![None]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
factory.sandboxes(),
|
factory.sandboxes(),
|
||||||
@ -4841,6 +4857,7 @@ async fn launch_structured_codex_uses_managed_tmp_for_env_and_writable_roots_wit
|
|||||||
assert_eq!(env.get("TMPDIR"), Some(&tmp));
|
assert_eq!(env.get("TMPDIR"), Some(&tmp));
|
||||||
assert_eq!(env.get("TMP"), Some(&tmp));
|
assert_eq!(env.get("TMP"), Some(&tmp));
|
||||||
assert_eq!(env.get("TEMP"), Some(&tmp));
|
assert_eq!(env.get("TEMP"), Some(&tmp));
|
||||||
|
assert_eq!(env.get("IDEA_MANAGED_TMP"), Some(&tmp));
|
||||||
let Some(domain::ports::StructuredProviderLaunchPolicy::Codex { writable_roots, .. }) =
|
let Some(domain::ports::StructuredProviderLaunchPolicy::Codex { writable_roots, .. }) =
|
||||||
factory.policies().into_iter().next().unwrap()
|
factory.policies().into_iter().next().unwrap()
|
||||||
else {
|
else {
|
||||||
|
|||||||
Reference in New Issue
Block a user