@team-agent/installer 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +8 -0
- package/crates/team-agent/src/cli/diagnose.rs +51 -10
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +217 -80
- package/crates/team-agent/src/cli/send.rs +1 -0
- package/crates/team-agent/src/cli/status_port.rs +135 -7
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +8 -1
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +39 -0
- package/crates/team-agent/src/cli/types.rs +5 -1
- package/crates/team-agent/src/coordinator/backoff.rs +57 -9
- package/crates/team-agent/src/coordinator/health.rs +65 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +28 -16
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +87 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/tick.rs +195 -43
- package/crates/team-agent/src/leader/helpers.rs +2 -0
- package/crates/team-agent/src/leader/rediscover.rs +1 -0
- package/crates/team-agent/src/leader/start.rs +9 -1
- package/crates/team-agent/src/leader/takeover.rs +18 -1
- package/crates/team-agent/src/lifecycle/launch.rs +434 -29
- package/crates/team-agent/src/lifecycle/profile_launch.rs +110 -4
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +4 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +19 -2
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/core.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +44 -9
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +2 -1
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +14 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +15 -1
- package/crates/team-agent/src/mcp_server/tools.rs +65 -9
- package/crates/team-agent/src/mcp_server/wire.rs +2 -1
- package/crates/team-agent/src/message_store.rs +80 -0
- package/crates/team-agent/src/messaging/results.rs +76 -5
- package/crates/team-agent/src/messaging/send.rs +3 -1
- package/crates/team-agent/src/messaging/types.rs +15 -1
- package/crates/team-agent/src/messaging/watchers.rs +68 -30
- package/crates/team-agent/src/model/enums.rs +7 -1
- package/crates/team-agent/src/model/permissions.rs +7 -0
- package/crates/team-agent/src/model/spec.rs +3 -1
- package/crates/team-agent/src/provider/adapter.rs +472 -7
- package/crates/team-agent/src/provider/classify.rs +6 -2
- package/crates/team-agent/src/provider/faults.rs +3 -2
- package/crates/team-agent/src/provider/startup_prompt.rs +25 -7
- package/crates/team-agent/src/provider/types.rs +11 -0
- package/crates/team-agent/src/session_capture.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +95 -19
- package/crates/team-agent/src/tmux_backend/tests.rs +8 -7
- package/crates/team-agent/src/tmux_backend.rs +80 -6
- package/crates/team-agent/src/transport.rs +32 -0
- package/npm/install.mjs +21 -0
- package/package.json +4 -4
|
@@ -6,8 +6,8 @@ use std::process::Command;
|
|
|
6
6
|
use super::helpers::{find_session_id, parse_jsonl_records, patterns};
|
|
7
7
|
use super::types::{
|
|
8
8
|
AuthHintStatus, CaptureVia, CapturedSession, CommandPlan, Confidence, McpConfig,
|
|
9
|
-
ProviderCaps, ProviderCommandContext, ProviderError, RolloutPath,
|
|
10
|
-
StatusPatterns,
|
|
9
|
+
ProviderCaps, ProviderCommandContext, ProviderCommandOverrides, ProviderError, RolloutPath,
|
|
10
|
+
SessionId, StatusPatterns,
|
|
11
11
|
};
|
|
12
12
|
use super::{AuthMode, Provider};
|
|
13
13
|
|
|
@@ -210,6 +210,20 @@ pub trait ProviderAdapter {
|
|
|
210
210
|
checks: usize,
|
|
211
211
|
sleep_s: f64,
|
|
212
212
|
) -> Vec<crate::provider::HandledPrompt> {
|
|
213
|
+
self.handle_startup_prompts_outcome(transport, target, checks, sleep_s)
|
|
214
|
+
.handled
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// swallow batch 2 ② (A1): the structured variant — `capture_error` surfaces a pane
|
|
218
|
+
/// that could not even be captured, so callers can log the failure instead of
|
|
219
|
+
/// silently treating it as "no prompts" (CLAUDE.md §5).
|
|
220
|
+
fn handle_startup_prompts_outcome(
|
|
221
|
+
&self,
|
|
222
|
+
transport: &dyn crate::transport::Transport,
|
|
223
|
+
target: &crate::transport::Target,
|
|
224
|
+
checks: usize,
|
|
225
|
+
sleep_s: f64,
|
|
226
|
+
) -> super::startup_prompt::StartupPromptOutcome {
|
|
213
227
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| match self.provider() {
|
|
214
228
|
Provider::Codex => {
|
|
215
229
|
super::startup_prompt::codex_handle_startup_prompts(transport, target, checks, sleep_s)
|
|
@@ -219,10 +233,32 @@ pub trait ProviderAdapter {
|
|
|
219
233
|
transport, target, checks, sleep_s,
|
|
220
234
|
)
|
|
221
235
|
}
|
|
222
|
-
_ =>
|
|
236
|
+
_ => super::startup_prompt::StartupPromptOutcome::default(),
|
|
223
237
|
}))
|
|
224
238
|
.unwrap_or_default()
|
|
225
239
|
}
|
|
240
|
+
|
|
241
|
+
/// Python launch/core.py:235-237 + tmux_prompt.py:124-129 — `runtime.fast` toggles
|
|
242
|
+
/// codex fast mode by sending `/fast` + Enter to the worker pane after spawn.
|
|
243
|
+
/// Providers without a fast-mode toggle are a no-op so upper layers stay
|
|
244
|
+
/// provider-agnostic (F032). Returns whether a toggle was sent.
|
|
245
|
+
fn enable_fast_mode(
|
|
246
|
+
&self,
|
|
247
|
+
transport: &dyn crate::transport::Transport,
|
|
248
|
+
target: &crate::transport::Target,
|
|
249
|
+
) -> bool {
|
|
250
|
+
match self.provider() {
|
|
251
|
+
Provider::Codex => {
|
|
252
|
+
let keys: Vec<crate::transport::Key> = "/fast"
|
|
253
|
+
.chars()
|
|
254
|
+
.map(crate::transport::Key::Char)
|
|
255
|
+
.chain([crate::transport::Key::Enter])
|
|
256
|
+
.collect();
|
|
257
|
+
transport.send_keys(target, &keys).is_ok()
|
|
258
|
+
}
|
|
259
|
+
_ => false,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
226
262
|
}
|
|
227
263
|
|
|
228
264
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -278,6 +314,17 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
278
314
|
native_mcp_config: false,
|
|
279
315
|
writes_global_settings: false,
|
|
280
316
|
},
|
|
317
|
+
// Copilot(C-4-1 cr verdict):resume 走 --resume <sid>;**无 fork** 旗标,
|
|
318
|
+
// session-store 不支持 branched continuation → caps.fork=false 显式拒。
|
|
319
|
+
// native_mcp_config=true(`--additional-mcp-config` 接 inline JSON 或 @file);
|
|
320
|
+
// writes_global_settings=false(session 走 --session-id 预定 UUID,不污染
|
|
321
|
+
// ~/.copilot/mcp-config.json,help 原文 "augments config for this session")。
|
|
322
|
+
Provider::Copilot => ProviderCaps {
|
|
323
|
+
resume: true,
|
|
324
|
+
fork: false,
|
|
325
|
+
native_mcp_config: true,
|
|
326
|
+
writes_global_settings: false,
|
|
327
|
+
},
|
|
281
328
|
Provider::GeminiCli => ProviderCaps {
|
|
282
329
|
resume: false,
|
|
283
330
|
fork: false,
|
|
@@ -322,6 +369,13 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
322
369
|
fn auth_hint(&self, auth_mode: AuthMode) -> AuthHintStatus {
|
|
323
370
|
match self.provider {
|
|
324
371
|
Provider::Claude | Provider::ClaudeCode => claude_auth_hint(auth_mode),
|
|
372
|
+
// C-A-5 cr verdict v2(诚实 MUST-NOT-13) — copilot 无 auth status 子命令
|
|
373
|
+
// (main-help Commands 节仅 completion/help/init/login/mcp/plugin/update/
|
|
374
|
+
// version)。framework 只能弱检测(命令在 PATH + ~/.copilot/config.json
|
|
375
|
+
// 存在),不能假报强 Present;Subscription 档返 PresentWeak,doctor 文案
|
|
376
|
+
// 标"weak / no auth-status command available";Compatible/Official 走 BYOK
|
|
377
|
+
// 路径,有 COPILOT_PROVIDER_BASE_URL 时已脱离 GitHub 登录通道。
|
|
378
|
+
Provider::Copilot => copilot_auth_hint(auth_mode),
|
|
325
379
|
_ => match auth_mode {
|
|
326
380
|
AuthMode::Subscription => AuthHintStatus::Present,
|
|
327
381
|
AuthMode::OfficialApi | AuthMode::CompatibleApi => AuthHintStatus::MissingOrUnknown,
|
|
@@ -351,7 +405,16 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
351
405
|
Provider::Claude | Provider::ClaudeCode => {
|
|
352
406
|
Ok(claude_launch_command(self, auth_mode, mcp_config, system_prompt, model, tools)?)
|
|
353
407
|
}
|
|
354
|
-
Provider::Codex => Ok(codex_base_command(None, auth_mode, mcp_config, system_prompt, model, tools)),
|
|
408
|
+
Provider::Codex => Ok(codex_base_command(None, auth_mode, mcp_config, system_prompt, model, tools, None)),
|
|
409
|
+
// §C1 worker argv 形态 + C-1/C-5/C-6 cr verdict:
|
|
410
|
+
// copilot --no-color --no-auto-update [<dangerous|granular>] [--model]
|
|
411
|
+
// --additional-mcp-config <inline json> --session-id <expected_uuid> -C <cwd>
|
|
412
|
+
// system_prompt 经 spawn env(COPILOT_CUSTOM_INSTRUCTIONS_DIRS)+ per-worker
|
|
413
|
+
// AGENTS.md(launch 路径写文件,见 lifecycle/launch.rs)注入,**不入 argv**
|
|
414
|
+
// (B2 灵魂件降级,C-1-2 禁 silent 写全局)。
|
|
415
|
+
Provider::Copilot => Ok(copilot_base_command(
|
|
416
|
+
auth_mode, mcp_config, system_prompt, model, tools,
|
|
417
|
+
)),
|
|
355
418
|
Provider::GeminiCli => {
|
|
356
419
|
let mut argv = vec!["gemini".to_string()];
|
|
357
420
|
if let Some(model) = model {
|
|
@@ -394,6 +457,40 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
394
457
|
managed_mcp_config: managed,
|
|
395
458
|
})
|
|
396
459
|
}
|
|
460
|
+
// codex.py:105-118 — the profile command overrides (codex_profile / codex_config)
|
|
461
|
+
// ride on `agent["_provider_profile"]`, which only the plan path carries.
|
|
462
|
+
Provider::Codex => Ok(CommandPlan::argv_only(codex_base_command(
|
|
463
|
+
None,
|
|
464
|
+
ctx.auth_mode,
|
|
465
|
+
ctx.mcp_config,
|
|
466
|
+
ctx.system_prompt,
|
|
467
|
+
ctx.model,
|
|
468
|
+
ctx.tools,
|
|
469
|
+
ctx.profile_launch.map(|profile| &profile.command_overrides),
|
|
470
|
+
))),
|
|
471
|
+
// §C1 + §C4 cr verdict — copilot plan 端预定 UUID + workspace `-C` 双保险:
|
|
472
|
+
// * `--session-id <uuid>`(claude 同法,捕获免目录扫描,sqlite 仅校验)
|
|
473
|
+
// * `-C <workspace>`(双保险,即便 spawn cwd 漂移也能锚定)
|
|
474
|
+
// mcp_config inline 形态由 build_command 写入,launch 路径会用
|
|
475
|
+
// point_native_mcp_config_at_file 重写为 @<file> 形(§C1 note)。
|
|
476
|
+
Provider::Copilot => {
|
|
477
|
+
let expected = next_session_token();
|
|
478
|
+
let mut argv = copilot_base_command(
|
|
479
|
+
ctx.auth_mode,
|
|
480
|
+
ctx.mcp_config,
|
|
481
|
+
ctx.system_prompt,
|
|
482
|
+
ctx.model,
|
|
483
|
+
ctx.tools,
|
|
484
|
+
);
|
|
485
|
+
argv.push("--session-id".to_string());
|
|
486
|
+
argv.push(expected.clone());
|
|
487
|
+
Ok(CommandPlan {
|
|
488
|
+
argv,
|
|
489
|
+
expected_session_id: Some(SessionId::new(expected)),
|
|
490
|
+
provider_projects_root: None,
|
|
491
|
+
managed_mcp_config: false,
|
|
492
|
+
})
|
|
493
|
+
}
|
|
397
494
|
_ => self
|
|
398
495
|
.build_command_with_tools(
|
|
399
496
|
ctx.auth_mode,
|
|
@@ -500,6 +597,7 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
500
597
|
system_prompt,
|
|
501
598
|
model,
|
|
502
599
|
tools,
|
|
600
|
+
None,
|
|
503
601
|
);
|
|
504
602
|
argv.push(session_id.as_str().to_string());
|
|
505
603
|
Ok(argv)
|
|
@@ -511,6 +609,16 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
511
609
|
argv.push(session_id.as_str().to_string());
|
|
512
610
|
Ok(argv)
|
|
513
611
|
}
|
|
612
|
+
// §C1 cr verdict:resume 同 base + `--resume <sid>`(去 --session-id,
|
|
613
|
+
// copilot --resume 接受 id|name)。
|
|
614
|
+
Provider::Copilot => {
|
|
615
|
+
let mut argv = copilot_base_command_resume(
|
|
616
|
+
auth_mode, mcp_config, system_prompt, model, tools,
|
|
617
|
+
);
|
|
618
|
+
argv.push("--resume".to_string());
|
|
619
|
+
argv.push(session_id.as_str().to_string());
|
|
620
|
+
Ok(argv)
|
|
621
|
+
}
|
|
514
622
|
Provider::GeminiCli | Provider::Fake => Err(ProviderError::ResumeUnavailable(format!(
|
|
515
623
|
"{} resume requires session_id",
|
|
516
624
|
provider_wire(self.provider)
|
|
@@ -548,6 +656,30 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
548
656
|
plan.managed_mcp_config = managed;
|
|
549
657
|
Ok(plan)
|
|
550
658
|
}
|
|
659
|
+
Provider::Codex => {
|
|
660
|
+
if !self.session_is_resumable(session_id, ctx.auth_mode)? {
|
|
661
|
+
return Err(ProviderError::ResumeUnavailable(format!(
|
|
662
|
+
"{} resume requires session_id",
|
|
663
|
+
provider_wire(self.provider)
|
|
664
|
+
)));
|
|
665
|
+
}
|
|
666
|
+
let Some(session_id) = session_id else {
|
|
667
|
+
return Err(ProviderError::ResumeUnavailable(
|
|
668
|
+
"resume requires session_id".to_string(),
|
|
669
|
+
));
|
|
670
|
+
};
|
|
671
|
+
let mut argv = codex_base_command(
|
|
672
|
+
Some("resume"),
|
|
673
|
+
ctx.auth_mode,
|
|
674
|
+
ctx.mcp_config,
|
|
675
|
+
ctx.system_prompt,
|
|
676
|
+
ctx.model,
|
|
677
|
+
ctx.tools,
|
|
678
|
+
ctx.profile_launch.map(|profile| &profile.command_overrides),
|
|
679
|
+
);
|
|
680
|
+
argv.push(session_id.as_str().to_string());
|
|
681
|
+
Ok(CommandPlan::argv_only(argv))
|
|
682
|
+
}
|
|
551
683
|
_ => self
|
|
552
684
|
.build_resume_command_with_context(
|
|
553
685
|
session_id,
|
|
@@ -597,6 +729,7 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
597
729
|
system_prompt,
|
|
598
730
|
model,
|
|
599
731
|
tools,
|
|
732
|
+
None,
|
|
600
733
|
);
|
|
601
734
|
argv.push(session_id.as_str().to_string());
|
|
602
735
|
Ok(argv)
|
|
@@ -611,6 +744,13 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
611
744
|
argv.push("--fork-session".to_string());
|
|
612
745
|
Ok(argv)
|
|
613
746
|
}
|
|
747
|
+
// C-4-2 cr verdict: copilot 无 fork 旗标 + session-store 不支持 branched
|
|
748
|
+
// continuation → 显式 CapabilityUnsupported,**绝不** silent fallback 到
|
|
749
|
+
// restart-from-scratch(MUST-NOT-13 诚实)。本分支理论上不可达(caps.fork=false
|
|
750
|
+
// 已在 fork_with_context 入口拦截,line 582),保留作 totality 守护。
|
|
751
|
+
Provider::Copilot => Err(ProviderError::CapabilityUnsupported(
|
|
752
|
+
"copilot CLI 无 fork 旗标,session-store 不支持 branched continuation".to_string(),
|
|
753
|
+
)),
|
|
614
754
|
Provider::GeminiCli | Provider::Fake => Err(ProviderError::CapabilityUnsupported(format!(
|
|
615
755
|
"{} does not support native session fork",
|
|
616
756
|
provider_wire(self.provider)
|
|
@@ -661,6 +801,30 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
661
801
|
managed_mcp_config: managed,
|
|
662
802
|
})
|
|
663
803
|
}
|
|
804
|
+
Provider::Codex => {
|
|
805
|
+
if !self.caps().fork || ctx.auth_mode == AuthMode::CompatibleApi {
|
|
806
|
+
return Err(ProviderError::CapabilityUnsupported(format!(
|
|
807
|
+
"{} does not support native session fork",
|
|
808
|
+
provider_wire(self.provider)
|
|
809
|
+
)));
|
|
810
|
+
}
|
|
811
|
+
let Some(session_id) = session_id else {
|
|
812
|
+
return Err(ProviderError::ResumeUnavailable(
|
|
813
|
+
"fork requires session_id".to_string(),
|
|
814
|
+
));
|
|
815
|
+
};
|
|
816
|
+
let mut argv = codex_base_command(
|
|
817
|
+
Some("fork"),
|
|
818
|
+
ctx.auth_mode,
|
|
819
|
+
ctx.mcp_config,
|
|
820
|
+
ctx.system_prompt,
|
|
821
|
+
ctx.model,
|
|
822
|
+
ctx.tools,
|
|
823
|
+
ctx.profile_launch.map(|profile| &profile.command_overrides),
|
|
824
|
+
);
|
|
825
|
+
argv.push(session_id.as_str().to_string());
|
|
826
|
+
Ok(CommandPlan::argv_only(argv))
|
|
827
|
+
}
|
|
664
828
|
_ => self
|
|
665
829
|
.fork_with_context(
|
|
666
830
|
session_id,
|
|
@@ -705,6 +869,9 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
705
869
|
match self.provider {
|
|
706
870
|
Provider::Claude | Provider::ClaudeCode => patterns(r"[>❯]\s", r"[✶✢✽✻✳·].*…", r"Error|Traceback"),
|
|
707
871
|
Provider::Codex => patterns(r"(›|❯|codex>)", r"•.*esc to interrupt", r"Error|Traceback|panic"),
|
|
872
|
+
// C-3-3 cr verdict: copilot 真值待用户真会话样本(§E3 line 105),一期占位
|
|
873
|
+
// 仅 error 行至少能识别;idle/processing 留 Unknown(N11 守,classify→None)。
|
|
874
|
+
Provider::Copilot => patterns(r">", r"working|processing", r"Error|panic"),
|
|
708
875
|
Provider::GeminiCli | Provider::Fake => patterns(r">", r"working|processing", r"Error|Traceback"),
|
|
709
876
|
}
|
|
710
877
|
}
|
|
@@ -718,6 +885,7 @@ fn command_name(provider: Provider) -> &'static str {
|
|
|
718
885
|
match provider {
|
|
719
886
|
Provider::Claude | Provider::ClaudeCode => "claude",
|
|
720
887
|
Provider::Codex => "codex",
|
|
888
|
+
Provider::Copilot => "copilot",
|
|
721
889
|
Provider::GeminiCli => "gemini",
|
|
722
890
|
Provider::Fake => "team-agent",
|
|
723
891
|
}
|
|
@@ -728,6 +896,7 @@ fn provider_wire(provider: Provider) -> &'static str {
|
|
|
728
896
|
Provider::Claude => "claude",
|
|
729
897
|
Provider::ClaudeCode => "claude_code",
|
|
730
898
|
Provider::Codex => "codex",
|
|
899
|
+
Provider::Copilot => "copilot",
|
|
731
900
|
Provider::GeminiCli => "gemini_cli",
|
|
732
901
|
Provider::Fake => "fake",
|
|
733
902
|
}
|
|
@@ -741,6 +910,27 @@ fn auth_mode_wire(auth_mode: AuthMode) -> &'static str {
|
|
|
741
910
|
}
|
|
742
911
|
}
|
|
743
912
|
|
|
913
|
+
/// C-A-5 cr verdict v2 — copilot 弱检测(无 auth status 子命令)。
|
|
914
|
+
/// 当 copilot 命令在 PATH 且 `~/.copilot/config.json` 存在 → PresentWeak;否则 Missing
|
|
915
|
+
/// (PATH 缺)或 MissingOrUnknown(无 config 文件)。Compatible/Official 走 BYOK,
|
|
916
|
+
/// 由 profile_launch 端校验(COPILOT_PROVIDER_BASE_URL 等),hint 层报 Unknown。
|
|
917
|
+
fn copilot_auth_hint(auth_mode: AuthMode) -> AuthHintStatus {
|
|
918
|
+
if !matches!(auth_mode, AuthMode::Subscription) {
|
|
919
|
+
return AuthHintStatus::Unknown;
|
|
920
|
+
}
|
|
921
|
+
if !command_on_path("copilot") {
|
|
922
|
+
return AuthHintStatus::Missing;
|
|
923
|
+
}
|
|
924
|
+
let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
|
|
925
|
+
return AuthHintStatus::MissingOrUnknown;
|
|
926
|
+
};
|
|
927
|
+
if home.join(".copilot").join("config.json").exists() {
|
|
928
|
+
AuthHintStatus::PresentWeak
|
|
929
|
+
} else {
|
|
930
|
+
AuthHintStatus::MissingOrUnknown
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
744
934
|
fn claude_auth_hint(auth_mode: AuthMode) -> AuthHintStatus {
|
|
745
935
|
if auth_mode != AuthMode::Subscription {
|
|
746
936
|
return AuthHintStatus::MissingOrUnknown;
|
|
@@ -777,11 +967,22 @@ fn scan_session_candidates_once(
|
|
|
777
967
|
provider: Provider,
|
|
778
968
|
context: &CaptureSessionContext,
|
|
779
969
|
) -> Result<Vec<CapturedSessionCandidate>, ProviderError> {
|
|
970
|
+
// §C4 + cr verdict: copilot session 真相源是 ~/.copilot/session-store.db(sqlite),
|
|
971
|
+
// 不是 jsonl 流。点查 sessions(cwd==spawn_cwd)取最新行,**禁** 走目录扫描
|
|
972
|
+
// (PERF P2 不放大;sqlite 点查天然有界)。decoy 文件不进 parse_session_records,
|
|
973
|
+
// 不会"被毒文件炸"。
|
|
974
|
+
if matches!(provider, Provider::Copilot) {
|
|
975
|
+
return Ok(scan_copilot_session_store(context));
|
|
976
|
+
}
|
|
780
977
|
let candidates = candidate_session_files(provider, context)?;
|
|
781
978
|
let mut out = Vec::new();
|
|
782
979
|
for candidate in candidates {
|
|
783
980
|
let path = candidate.path;
|
|
784
|
-
|
|
981
|
+
// P2 (C-P2-1/4) / Python claude.py:432 — bounded HEAD read (session_meta / cwd /
|
|
982
|
+
// sessionId live in the file head; Python stops at 200 lines). A poisoned
|
|
983
|
+
// (invalid UTF-8) tail must not silently drop the candidate the way a
|
|
984
|
+
// whole-file read_to_string did.
|
|
985
|
+
let Ok(text) = read_head_text(&path, CAPTURE_HEAD_BYTES) else {
|
|
785
986
|
continue;
|
|
786
987
|
};
|
|
787
988
|
let records = parse_session_records(&text);
|
|
@@ -836,6 +1037,54 @@ fn scan_session_candidates_once(
|
|
|
836
1037
|
Ok(out)
|
|
837
1038
|
}
|
|
838
1039
|
|
|
1040
|
+
/// §C4 cr verdict — copilot session 真相源 sqlite 点查。
|
|
1041
|
+
///
|
|
1042
|
+
/// 路径:`<HOME>/.copilot/session-store.db`,sessions 表(id/cwd/created_at/updated_at)
|
|
1043
|
+
/// where `cwd == context.spawn_cwd` 取 updated_at 最新行。**绝不**全文件扫描、**绝不**
|
|
1044
|
+
/// 走 `parse_session_records`(jsonl)路径 → decoy 毒文件不会触碰任何解析器。
|
|
1045
|
+
///
|
|
1046
|
+
/// 失败(HOME 缺、db 缺、表缺、sqlite 错)统一返回空 candidate 列表,与既有
|
|
1047
|
+
/// `collect_optional_candidate_files` 同精神(absent → empty)。
|
|
1048
|
+
fn scan_copilot_session_store(context: &CaptureSessionContext) -> Vec<CapturedSessionCandidate> {
|
|
1049
|
+
let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
|
|
1050
|
+
return Vec::new();
|
|
1051
|
+
};
|
|
1052
|
+
let db_path = home.join(".copilot").join("session-store.db");
|
|
1053
|
+
if !db_path.exists() {
|
|
1054
|
+
return Vec::new();
|
|
1055
|
+
}
|
|
1056
|
+
let Ok(conn) = rusqlite::Connection::open_with_flags(
|
|
1057
|
+
&db_path,
|
|
1058
|
+
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
|
1059
|
+
) else {
|
|
1060
|
+
return Vec::new();
|
|
1061
|
+
};
|
|
1062
|
+
let cwd = context.spawn_cwd.to_string_lossy().to_string();
|
|
1063
|
+
let mut stmt = match conn.prepare(
|
|
1064
|
+
"select id from sessions where cwd = ?1 order by updated_at desc, id desc limit 1",
|
|
1065
|
+
) {
|
|
1066
|
+
Ok(stmt) => stmt,
|
|
1067
|
+
Err(_) => return Vec::new(),
|
|
1068
|
+
};
|
|
1069
|
+
let row: Option<String> = stmt
|
|
1070
|
+
.query_row([cwd.as_str()], |row| row.get::<_, String>(0))
|
|
1071
|
+
.ok();
|
|
1072
|
+
let Some(session_id) = row else {
|
|
1073
|
+
return Vec::new();
|
|
1074
|
+
};
|
|
1075
|
+
vec![CapturedSessionCandidate {
|
|
1076
|
+
captured: CapturedSession {
|
|
1077
|
+
session_id: Some(SessionId::new(session_id)),
|
|
1078
|
+
rollout_path: Some(RolloutPath::new(db_path.clone())),
|
|
1079
|
+
captured_via: CaptureVia::FsWatch,
|
|
1080
|
+
attribution_confidence: Confidence::High,
|
|
1081
|
+
spawn_cwd: context.spawn_cwd.clone(),
|
|
1082
|
+
},
|
|
1083
|
+
positive_agent_id_match: false,
|
|
1084
|
+
agent_path_match: false,
|
|
1085
|
+
}]
|
|
1086
|
+
}
|
|
1087
|
+
|
|
839
1088
|
fn command_on_path(name: &str) -> bool {
|
|
840
1089
|
let Some(path) = std::env::var_os("PATH") else {
|
|
841
1090
|
return false;
|
|
@@ -866,7 +1115,12 @@ fn candidate_session_files(
|
|
|
866
1115
|
collect_optional_candidate_files(&home.join(".claude").join("sessions"), &context.agent_id, &mut out)?;
|
|
867
1116
|
collect_optional_candidate_files(&home.join(".claude").join("projects"), &context.agent_id, &mut out)?;
|
|
868
1117
|
}
|
|
869
|
-
|
|
1118
|
+
// §C4 cr verdict + 设计 §C: copilot session 真相源是 ~/.copilot/session-store.db
|
|
1119
|
+
// (sqlite 点查 sessions.cwd==spawn_cwd 最新行)和 ~/.copilot/session-state/<uuid>/
|
|
1120
|
+
// workspace.yaml — **不走全文件扫描**(PERF P2 禁不放大)。主路径是
|
|
1121
|
+
// build_command_plan 预定 UUID(--session-id <expected>)→ pending_session_id
|
|
1122
|
+
// 直接命中,这里只补 sqlite 查询的二期入口。一期返空,resume 走 caps 校验。
|
|
1123
|
+
Provider::Copilot | Provider::GeminiCli | Provider::Fake => {}
|
|
870
1124
|
}
|
|
871
1125
|
}
|
|
872
1126
|
out.sort_by(|a, b| {
|
|
@@ -875,9 +1129,58 @@ fn candidate_session_files(
|
|
|
875
1129
|
.then_with(|| a.path.to_string_lossy().cmp(&b.path.to_string_lossy()))
|
|
876
1130
|
});
|
|
877
1131
|
out.dedup_by(|a, b| a.path == b.path && a.requires_cwd_match == b.requires_cwd_match);
|
|
1132
|
+
cap_candidates_by_mtime(&mut out, CAPTURE_CANDIDATE_CAP);
|
|
878
1133
|
Ok(out)
|
|
879
1134
|
}
|
|
880
1135
|
|
|
1136
|
+
/// P2 (C-P2-2/3) / Python claude.py:300 — candidates are capped to the newest `cap`
|
|
1137
|
+
/// by mtime (descending priority: old candidates must not crowd out new ones; the cap
|
|
1138
|
+
/// may be raised above Python's 300 but never lowered). The existing selection
|
|
1139
|
+
/// ordering of the survivors is preserved.
|
|
1140
|
+
const CAPTURE_CANDIDATE_CAP: usize = 300;
|
|
1141
|
+
|
|
1142
|
+
/// P2 (C-P2-1): head window ≥ Python's 200-line read (meta fields live in the head).
|
|
1143
|
+
const CAPTURE_HEAD_BYTES: u64 = 65_536;
|
|
1144
|
+
|
|
1145
|
+
fn cap_candidates_by_mtime(out: &mut Vec<SessionCandidate>, cap: usize) {
|
|
1146
|
+
if out.len() <= cap {
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
let mut ranked: Vec<(std::time::SystemTime, usize)> = out
|
|
1150
|
+
.iter()
|
|
1151
|
+
.enumerate()
|
|
1152
|
+
.map(|(index, candidate)| {
|
|
1153
|
+
let mtime = std::fs::metadata(&candidate.path)
|
|
1154
|
+
.and_then(|meta| meta.modified())
|
|
1155
|
+
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
|
|
1156
|
+
(mtime, index)
|
|
1157
|
+
})
|
|
1158
|
+
.collect();
|
|
1159
|
+
ranked.sort_by(|a, b| b.0.cmp(&a.0));
|
|
1160
|
+
let keep: std::collections::BTreeSet<usize> =
|
|
1161
|
+
ranked.into_iter().take(cap).map(|(_, index)| index).collect();
|
|
1162
|
+
let mut index = 0;
|
|
1163
|
+
out.retain(|_| {
|
|
1164
|
+
let kept = keep.contains(&index);
|
|
1165
|
+
index += 1;
|
|
1166
|
+
kept
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/// P2: bounded head read, truncated to the last complete line (a cut record must not
|
|
1171
|
+
/// reach the JSONL parser); lossy UTF-8 so a mid-codepoint boundary stays safe.
|
|
1172
|
+
fn read_head_text(path: &Path, max_bytes: u64) -> std::io::Result<String> {
|
|
1173
|
+
use std::io::Read;
|
|
1174
|
+
let file = std::fs::File::open(path)?;
|
|
1175
|
+
let mut bytes = Vec::new();
|
|
1176
|
+
file.take(max_bytes).read_to_end(&mut bytes)?;
|
|
1177
|
+
let complete = match bytes.iter().rposition(|byte| *byte == b'\n') {
|
|
1178
|
+
Some(last_newline) => &bytes[..=last_newline],
|
|
1179
|
+
None => &bytes[..],
|
|
1180
|
+
};
|
|
1181
|
+
Ok(String::from_utf8_lossy(complete).into_owned())
|
|
1182
|
+
}
|
|
1183
|
+
|
|
881
1184
|
fn collect_optional_candidate_files(
|
|
882
1185
|
dir: &Path,
|
|
883
1186
|
agent_id: &str,
|
|
@@ -1104,6 +1407,7 @@ fn codex_base_command(
|
|
|
1104
1407
|
system_prompt: Option<&str>,
|
|
1105
1408
|
model: Option<&str>,
|
|
1106
1409
|
tools: &[&str],
|
|
1410
|
+
overrides: Option<&ProviderCommandOverrides>,
|
|
1107
1411
|
) -> Vec<String> {
|
|
1108
1412
|
let mut argv = vec![
|
|
1109
1413
|
"codex".to_string(),
|
|
@@ -1118,6 +1422,11 @@ fn codex_base_command(
|
|
|
1118
1422
|
"--disable".to_string(),
|
|
1119
1423
|
"apps".to_string(),
|
|
1120
1424
|
]);
|
|
1425
|
+
// codex.py:105-107 — profile CODEX_PROFILE before the sandbox/approval flags.
|
|
1426
|
+
if let Some(profile) = overrides.and_then(|o| o.codex_profile.as_deref()) {
|
|
1427
|
+
argv.push("--profile".to_string());
|
|
1428
|
+
argv.push(profile.to_string());
|
|
1429
|
+
}
|
|
1121
1430
|
if codex_dangerous_auto_approve(tools) {
|
|
1122
1431
|
argv.push("--dangerously-bypass-approvals-and-sandbox".to_string());
|
|
1123
1432
|
} else {
|
|
@@ -1130,9 +1439,21 @@ fn codex_base_command(
|
|
|
1130
1439
|
argv.push("--model".to_string());
|
|
1131
1440
|
argv.push(model.to_string());
|
|
1132
1441
|
}
|
|
1442
|
+
// codex.py:117-118 — profile codex_config `-c` items before developer_instructions.
|
|
1443
|
+
if let Some(overrides) = overrides {
|
|
1444
|
+
for config in &overrides.codex_config {
|
|
1445
|
+
argv.push("-c".to_string());
|
|
1446
|
+
argv.push(config.clone());
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1133
1449
|
if let Some(prompt) = system_prompt {
|
|
1450
|
+
// codex.py:120 — escape order matters: backslash first, then quote, then newline.
|
|
1451
|
+
let escaped = prompt
|
|
1452
|
+
.replace('\\', "\\\\")
|
|
1453
|
+
.replace('"', "\\\"")
|
|
1454
|
+
.replace('\n', "\\n");
|
|
1134
1455
|
argv.push("-c".to_string());
|
|
1135
|
-
argv.push(format!("developer_instructions=\"{}\""
|
|
1456
|
+
argv.push(format!("developer_instructions=\"{escaped}\""));
|
|
1136
1457
|
}
|
|
1137
1458
|
// Contract C / MUST-8: Codex CLI (2026-06) does NOT take Claude's `--mcp-config <json>` flag;
|
|
1138
1459
|
// instead it uses `-c mcp_servers.<name>.<field>=...` overrides, the same pattern used by
|
|
@@ -1174,6 +1495,10 @@ fn append_codex_mcp_overrides(argv: &mut Vec<String>, raw: &serde_json::Value) {
|
|
|
1174
1495
|
argv.push("-c".to_string());
|
|
1175
1496
|
argv.push(format!("mcp_servers.{name}.{key}={}", json_inline(value)));
|
|
1176
1497
|
}
|
|
1498
|
+
// codex.py:129 — every MCP server gets a 600s tool timeout so long-running
|
|
1499
|
+
// team_orchestrator calls (report_result etc.) survive the codex default.
|
|
1500
|
+
argv.push("-c".to_string());
|
|
1501
|
+
argv.push(format!("mcp_servers.{name}.tool_timeout_sec=600.0"));
|
|
1177
1502
|
}
|
|
1178
1503
|
}
|
|
1179
1504
|
|
|
@@ -1188,6 +1513,146 @@ fn codex_dangerous_auto_approve(tools: &[&str]) -> bool {
|
|
|
1188
1513
|
tools.contains(&"dangerous_auto_approve")
|
|
1189
1514
|
}
|
|
1190
1515
|
|
|
1516
|
+
// ---------------------------------------------------------------------------
|
|
1517
|
+
// COPILOT base command(v2 实证 + cr verdict v2 30 约束)
|
|
1518
|
+
// ---------------------------------------------------------------------------
|
|
1519
|
+
//
|
|
1520
|
+
// 设计 v2 §B argv 形态(每条带 help 出处,逐字落地):
|
|
1521
|
+
// copilot --no-color --no-auto-update --no-remote # C-1-2 噪音 + 禁远控
|
|
1522
|
+
// --disable-builtin-mcps # C-3-1 P0 禁内建 github-mcp-server
|
|
1523
|
+
// --additional-mcp-config @<file> # C-3-4 用 @file 形,避 wrapper 语义
|
|
1524
|
+
// --allow-tool 'team_orchestrator' # C-3-5 mcp_team 免审批 (server 级)
|
|
1525
|
+
// --session-id <uuid> -n <agent_id> # C-7-1 plan/launch 加
|
|
1526
|
+
// -C <workspace> # 双保险,launch 加
|
|
1527
|
+
// [--allow-all | <granular deny>] # C-5-1/C-5-2
|
|
1528
|
+
// [--model <m>]
|
|
1529
|
+
// [--log-dir <dir> --log-level info] # C-6-2 launch 加
|
|
1530
|
+
// env: COPILOT_CUSTOM_INSTRUCTIONS_DIRS=<ws>/.../<agent_id>/ # C-2-1 launch 注入
|
|
1531
|
+
// COPILOT_DISABLE_TERMINAL_TITLE=1 # C-4 P0 N39 红线,launch 注入
|
|
1532
|
+
// banner 不入 argv(v1 错的 --banner=never 删除;banner 走 config 文件,非 CLI flag)。
|
|
1533
|
+
// `-i`/`-p`/`--share*`/`--no-ask-user` **绝不**入 argv(RC-1/RC-14/RC-16)。
|
|
1534
|
+
//
|
|
1535
|
+
// system_prompt(B2 灵魂件)**不进 argv**:走 spawn env COPILOT_CUSTOM_INSTRUCTIONS_DIRS
|
|
1536
|
+
// + per-worker AGENTS.md(B2 单源,不另拼);本函数 system_prompt 参数静默忽略。
|
|
1537
|
+
fn copilot_base_command(
|
|
1538
|
+
auth_mode: AuthMode,
|
|
1539
|
+
mcp_config: Option<&McpConfig>,
|
|
1540
|
+
system_prompt: Option<&str>,
|
|
1541
|
+
model: Option<&str>,
|
|
1542
|
+
tools: &[&str],
|
|
1543
|
+
) -> Vec<String> {
|
|
1544
|
+
let _ = (auth_mode, system_prompt);
|
|
1545
|
+
let mut argv = vec![
|
|
1546
|
+
"copilot".to_string(),
|
|
1547
|
+
// C-1-2 v2:噪音控制三件 + 禁远控(防 GitHub web 远控 worker)
|
|
1548
|
+
"--no-color".to_string(),
|
|
1549
|
+
"--no-auto-update".to_string(),
|
|
1550
|
+
"--no-remote".to_string(),
|
|
1551
|
+
// C-3-1 v2 (P0):禁内建 github-mcp-server(main-help:70-71);残留风险
|
|
1552
|
+
// 通过 spawn 前 `copilot mcp list` 扫描 + 按名 `--disable-mcp-server <n>` 补
|
|
1553
|
+
// (那一段在 launch 路径加,因为需要 spawn-time 探测)。
|
|
1554
|
+
"--disable-builtin-mcps".to_string(),
|
|
1555
|
+
];
|
|
1556
|
+
if copilot_dangerous_auto_approve(tools) {
|
|
1557
|
+
// C-5-1 v2 实证:--allow-all == --yolo == 三件套(tools+paths+urls 等价),
|
|
1558
|
+
// help-permissions Enabling All Permissions 节原文。**禁** --allow-all-tools
|
|
1559
|
+
// (仅 tools 一档,语义不全,RC-13)。
|
|
1560
|
+
argv.push("--allow-all".to_string());
|
|
1561
|
+
} else {
|
|
1562
|
+
// C-5-2 v2:角色缺某 canonical 能力 → 精细 deny(deny 恒优先,即便
|
|
1563
|
+
// --allow-all-tools,help-permissions 原文);**禁** --allow-all/--yolo(RC-14)。
|
|
1564
|
+
for flag in copilot_permission_flags(tools) {
|
|
1565
|
+
argv.push(flag);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
// C-3-5 v2:mcp_team ∈ canonical(team_orchestrator 是我们的 server)→ 免审批
|
|
1569
|
+
// (模式 `<mcp-server-name>(tool-name?)` 省略 tool = 该 server 全工具集)。
|
|
1570
|
+
argv.push("--allow-tool".to_string());
|
|
1571
|
+
argv.push("team_orchestrator".to_string());
|
|
1572
|
+
if let Some(model) = model {
|
|
1573
|
+
argv.push("--model".to_string());
|
|
1574
|
+
argv.push(model.to_string());
|
|
1575
|
+
}
|
|
1576
|
+
if let Some(config) = mcp_config {
|
|
1577
|
+
// §C1 v2 + C-3-4 cr verdict v2(te 真机实证 cmd-mcp-add schema):
|
|
1578
|
+
// copilot 的 mcp 配置 schema 字段名是 `transport`(取值 stdio|http|sse),
|
|
1579
|
+
// 不是 codex/claude 的 `type`。McpConfig.raw 是 canonical(type),写
|
|
1580
|
+
// --additional-mcp-config 时必须翻译 type→transport(仅 copilot 走此分支)。
|
|
1581
|
+
argv.push("--additional-mcp-config".to_string());
|
|
1582
|
+
argv.push(copilot_translate_mcp_config(&config.raw).to_string());
|
|
1583
|
+
}
|
|
1584
|
+
argv
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/// C-3-4 cr verdict v2 — 把 McpConfig.raw 的 canonical schema(`type`)翻译成
|
|
1588
|
+
/// copilot mcp add/--additional-mcp-config 期望的 `transport` 字段(stdio|http|sse)。
|
|
1589
|
+
/// 仅 Copilot 适配走此翻译,claude/codex 路径不动。
|
|
1590
|
+
fn copilot_translate_mcp_config(raw: &serde_json::Value) -> serde_json::Value {
|
|
1591
|
+
let Some(servers) = raw.as_object() else {
|
|
1592
|
+
return raw.clone();
|
|
1593
|
+
};
|
|
1594
|
+
let mut translated = serde_json::Map::new();
|
|
1595
|
+
for (name, server) in servers {
|
|
1596
|
+
let Some(obj) = server.as_object() else {
|
|
1597
|
+
translated.insert(name.clone(), server.clone());
|
|
1598
|
+
continue;
|
|
1599
|
+
};
|
|
1600
|
+
let mut out = serde_json::Map::new();
|
|
1601
|
+
for (key, value) in obj {
|
|
1602
|
+
if key == "type" {
|
|
1603
|
+
out.insert("transport".to_string(), value.clone());
|
|
1604
|
+
} else {
|
|
1605
|
+
out.insert(key.clone(), value.clone());
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
translated.insert(name.clone(), serde_json::Value::Object(out));
|
|
1609
|
+
}
|
|
1610
|
+
serde_json::Value::Object(translated)
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/// resume 路径同 base + `--resume <sid>`(去 --session-id);单列出
|
|
1614
|
+
/// 避免 plan 端误 push --session-id 与 --resume 同帧。
|
|
1615
|
+
fn copilot_base_command_resume(
|
|
1616
|
+
auth_mode: AuthMode,
|
|
1617
|
+
mcp_config: Option<&McpConfig>,
|
|
1618
|
+
system_prompt: Option<&str>,
|
|
1619
|
+
model: Option<&str>,
|
|
1620
|
+
tools: &[&str],
|
|
1621
|
+
) -> Vec<String> {
|
|
1622
|
+
copilot_base_command(auth_mode, mcp_config, system_prompt, model, tools)
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
fn copilot_dangerous_auto_approve(tools: &[&str]) -> bool {
|
|
1626
|
+
tools.contains(&"dangerous_auto_approve")
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/// C-5-2 v2 verdict — copilot 细粒度 deny 映射(canonical tool → copilot flag,
|
|
1630
|
+
/// 全部走 `--deny-tool <kind>`,help-permissions Tool Permissions 节四 kind:
|
|
1631
|
+
/// shell/write/mcp/url):
|
|
1632
|
+
/// execute_bash ∉ allowed → `--deny-tool 'shell'`
|
|
1633
|
+
/// fs_write ∉ allowed → `--deny-tool 'write'`
|
|
1634
|
+
/// network ∉ allowed → `--deny-tool 'url'`(help-permissions: "url(domain-or-url?)
|
|
1635
|
+
/// … If omitted, matches all URLs")
|
|
1636
|
+
/// fs_read/fs_list 在 copilot 上无对应 deny kind(C-5-3 prompt_only 诚实)。
|
|
1637
|
+
fn copilot_permission_flags(tools: &[&str]) -> Vec<String> {
|
|
1638
|
+
let mut flags = Vec::new();
|
|
1639
|
+
if !tools.contains(&"execute_bash") {
|
|
1640
|
+
flags.push("--deny-tool".to_string());
|
|
1641
|
+
flags.push("shell".to_string());
|
|
1642
|
+
}
|
|
1643
|
+
if !tools.contains(&"fs_write") {
|
|
1644
|
+
flags.push("--deny-tool".to_string());
|
|
1645
|
+
flags.push("write".to_string());
|
|
1646
|
+
}
|
|
1647
|
+
if !tools.contains(&"network") {
|
|
1648
|
+
// v2 修正:`--deny-tool 'url'`(省略 domain 匹配全 URL),不是 `--deny-url '*'`
|
|
1649
|
+
// (RC-19 反向 case 守 — 全 URL 拒绝走 deny-tool kind,不走 deny-url path)。
|
|
1650
|
+
flags.push("--deny-tool".to_string());
|
|
1651
|
+
flags.push("url".to_string());
|
|
1652
|
+
}
|
|
1653
|
+
flags
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1191
1656
|
fn claude_dangerous_auto_approve(tools: &[&str]) -> bool {
|
|
1192
1657
|
tools.contains(&"dangerous_auto_approve")
|
|
1193
1658
|
}
|