dsh-ops 0.0.0-stage → 0.2.2

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.
Files changed (140) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/LICENSE +30 -0
  3. package/NOTICE +106 -0
  4. package/PROVENANCE.md +435 -0
  5. package/README.en.md +126 -0
  6. package/README.md +115 -2
  7. package/README.zh.md +116 -0
  8. package/bin/dsh-ops.mjs +1216 -0
  9. package/cordis.patch.yml +160 -0
  10. package/docs/manual-validation.md +53 -0
  11. package/docs/release-0.2.1.md +72 -0
  12. package/docs/schema-baseline.json +64 -0
  13. package/docs/schema-current.json +84 -0
  14. package/docs/schema-measurement.md +17 -0
  15. package/dsh-plugin.json +88 -0
  16. package/icon.svg +12 -0
  17. package/lib/binary.js +409 -0
  18. package/lib/config.js +198 -0
  19. package/lib/handshake.js +252 -0
  20. package/lib/index.js +108 -0
  21. package/lib/jobs.js +42 -0
  22. package/lib/policy.js +64 -0
  23. package/lib/presentation.js +63 -0
  24. package/lib/profile-install.js +61 -0
  25. package/lib/rust.js +194 -0
  26. package/lib/session-shells.js +78 -0
  27. package/lib/shells.js +998 -0
  28. package/lib/tools.js +657 -0
  29. package/locale/en.json +6 -0
  30. package/locale/zh.json +6 -0
  31. package/package.json +114 -4
  32. package/vendor/fastctx/Cargo.lock +3210 -0
  33. package/vendor/fastctx/Cargo.toml +94 -0
  34. package/vendor/fastctx/FORK.md +119 -0
  35. package/vendor/fastctx/LICENSE-APACHE +201 -0
  36. package/vendor/fastctx/NOTICE +40 -0
  37. package/vendor/fastctx/README.md +439 -0
  38. package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
  39. package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
  40. package/vendor/fastctx/UPSTREAM.md +49 -0
  41. package/vendor/fastctx/build.rs +413 -0
  42. package/vendor/fastctx/src/background_status.rs +403 -0
  43. package/vendor/fastctx/src/binary.rs +75 -0
  44. package/vendor/fastctx/src/bounded_sort.rs +500 -0
  45. package/vendor/fastctx/src/budget.rs +781 -0
  46. package/vendor/fastctx/src/cli/mod.rs +110 -0
  47. package/vendor/fastctx/src/context_guard.rs +289 -0
  48. package/vendor/fastctx/src/control/mod.rs +6 -0
  49. package/vendor/fastctx/src/control/paths.rs +49 -0
  50. package/vendor/fastctx/src/control/settings.rs +753 -0
  51. package/vendor/fastctx/src/control/transaction.rs +531 -0
  52. package/vendor/fastctx/src/edit/document.rs +535 -0
  53. package/vendor/fastctx/src/edit/locks.rs +371 -0
  54. package/vendor/fastctx/src/edit/mod.rs +213 -0
  55. package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
  56. package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
  57. package/vendor/fastctx/src/edit/private_storage.rs +234 -0
  58. package/vendor/fastctx/src/edit/replace.rs +1030 -0
  59. package/vendor/fastctx/src/edit_server.rs +53 -0
  60. package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
  61. package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
  62. package/vendor/fastctx/src/encoding.rs +1118 -0
  63. package/vendor/fastctx/src/file_executor.rs +1151 -0
  64. package/vendor/fastctx/src/file_snapshot.rs +1491 -0
  65. package/vendor/fastctx/src/glob_filter.rs +98 -0
  66. package/vendor/fastctx/src/glob_tool.rs +653 -0
  67. package/vendor/fastctx/src/grep_sink.rs +1162 -0
  68. package/vendor/fastctx/src/grep_tool.rs +2449 -0
  69. package/vendor/fastctx/src/lib.rs +45 -0
  70. package/vendor/fastctx/src/main.rs +15 -0
  71. package/vendor/fastctx/src/model.rs +51 -0
  72. package/vendor/fastctx/src/model_guidance.rs +62 -0
  73. package/vendor/fastctx/src/operation.rs +356 -0
  74. package/vendor/fastctx/src/ordered_window.rs +1235 -0
  75. package/vendor/fastctx/src/os_environment.rs +414 -0
  76. package/vendor/fastctx/src/path_codec.rs +850 -0
  77. package/vendor/fastctx/src/paths.rs +244 -0
  78. package/vendor/fastctx/src/process_identity.rs +763 -0
  79. package/vendor/fastctx/src/process_policy.rs +74 -0
  80. package/vendor/fastctx/src/read_tool/batch.rs +496 -0
  81. package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
  82. package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
  83. package/vendor/fastctx/src/read_tool/mod.rs +245 -0
  84. package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
  85. package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
  86. package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
  87. package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
  88. package/vendor/fastctx/src/render_plan.rs +468 -0
  89. package/vendor/fastctx/src/runtime/activity.rs +159 -0
  90. package/vendor/fastctx/src/runtime/hosts.rs +99 -0
  91. package/vendor/fastctx/src/runtime/journal.rs +556 -0
  92. package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
  93. package/vendor/fastctx/src/runtime/mod.rs +746 -0
  94. package/vendor/fastctx/src/runtime/protocol.rs +296 -0
  95. package/vendor/fastctx/src/runtime/session.rs +536 -0
  96. package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
  97. package/vendor/fastctx/src/search_parallelism.rs +106 -0
  98. package/vendor/fastctx/src/search_text.rs +227 -0
  99. package/vendor/fastctx/src/server.rs +359 -0
  100. package/vendor/fastctx/src/server_manifest.rs +468 -0
  101. package/vendor/fastctx/src/server_support.rs +826 -0
  102. package/vendor/fastctx/src/session.rs +629 -0
  103. package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
  104. package/vendor/fastctx/src/shell/bash.rs +263 -0
  105. package/vendor/fastctx/src/shell/buffer.rs +108 -0
  106. package/vendor/fastctx/src/shell/encoding.rs +403 -0
  107. package/vendor/fastctx/src/shell/foreground.rs +115 -0
  108. package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
  109. package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
  110. package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
  111. package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
  112. package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
  113. package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
  114. package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
  115. package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
  116. package/vendor/fastctx/src/shell/mod.rs +345 -0
  117. package/vendor/fastctx/src/shell/normalize.rs +389 -0
  118. package/vendor/fastctx/src/shell/output.rs +406 -0
  119. package/vendor/fastctx/src/shell/process.rs +493 -0
  120. package/vendor/fastctx/src/shell_server.rs +156 -0
  121. package/vendor/fastctx/src/skip_report.rs +83 -0
  122. package/vendor/fastctx/src/stdio_transport.rs +177 -0
  123. package/vendor/fastctx/src/tool_schema.rs +204 -0
  124. package/vendor/fastctx/src/traversal.rs +846 -0
  125. package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
  126. package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
  127. package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
  128. package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
  129. package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
  130. package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
  131. package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
  132. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
  133. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
  134. package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
  135. package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
  136. package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
  137. package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
  138. package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
  139. package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
  140. package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
@@ -0,0 +1,629 @@
1
+ // Modified by dsh-ops (https://github.com/T-Auto/dsh-ops): replaced the deleted Codex provider detection with the selected-tier output policy and dropped the Codex profile paths it read.
2
+ //! Immutable per-connection working directory, environment, and output policy.
3
+
4
+ use crate::budget::{
5
+ GLOB_TOKEN_BUDGET_ENV, GLOBAL_TOKEN_BUDGET_ENV, GREP_TOKEN_BUDGET_ENV,
6
+ JOB_OUTPUT_TOKEN_BUDGET_ENV, READ_TOKEN_BUDGET_ENV, RUN_TOKEN_BUDGET_ENV,
7
+ };
8
+ use crate::context_guard::{BURST_GAP, BurstTicket, GuardedBurstPool};
9
+ use crate::control::paths::ControlPaths;
10
+ use crate::control::settings::{FastCtxSettings, Tier, ToolBudgetPreferences, ToolBudgets};
11
+ use std::cell::RefCell;
12
+ use std::env::VarError;
13
+ use std::ffi::{OsStr, OsString};
14
+ use std::path::{Path, PathBuf};
15
+ use std::process::Command;
16
+ use std::sync::Arc;
17
+
18
+ /// Effective output mode for one configuration snapshot.
19
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
20
+ pub enum EffectiveOutputMode {
21
+ /// The user's selected tier is effective.
22
+ SelectedTier,
23
+ /// The provider route is unverified or compacts locally, so Guarded ceilings are effective.
24
+ ///
25
+ /// Nothing selects this mode since the Codex provider detection was removed; the guarded burst
26
+ /// machinery that reads it is retained unchanged.
27
+ Guarded,
28
+ }
29
+
30
+ /// Concrete output limits consumed by one runtime session.
31
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
32
+ pub struct EffectiveOutput {
33
+ /// User preference retained independently from the environment-derived mode.
34
+ pub selected_tier: Tier,
35
+ /// Environment-derived effective mode.
36
+ pub mode: EffectiveOutputMode,
37
+ /// Host token limit the removed Apply path used to write.
38
+ pub host_limit: i64,
39
+ /// Global FastCtx response budget.
40
+ pub fastctx_budget: usize,
41
+ /// Concrete per-tool shares, including defaults for the effective mode.
42
+ pub tool_budgets: ToolBudgets,
43
+ }
44
+
45
+ /// Resolves the output policy for one connection.
46
+ ///
47
+ /// Provider detection was removed with the Codex integration, so the user's selected tier is
48
+ /// always effective — the unguarded policy this crate already used whenever no Codex profile was
49
+ /// readable.
50
+ fn effective_output(selected_tier: Tier, preferences: ToolBudgetPreferences) -> EffectiveOutput {
51
+ EffectiveOutput {
52
+ selected_tier,
53
+ mode: EffectiveOutputMode::SelectedTier,
54
+ host_limit: selected_tier.host_limit(),
55
+ fastctx_budget: selected_tier.fastctx_budget(),
56
+ tool_budgets: preferences.resolve(selected_tier),
57
+ }
58
+ }
59
+
60
+ thread_local! {
61
+ static ACTIVE_ENVIRONMENT: RefCell<Option<Arc<SessionEnvironment>>> = const { RefCell::new(None) };
62
+ }
63
+
64
+ /// The native process state captured by one stdio proxy before it connects.
65
+ ///
66
+ /// Environment values remain `OsString`s so Unix bytes and Windows unpaired UTF-16 units survive
67
+ /// the IPC handshake without lossy conversion.
68
+ #[derive(Clone, Debug, Eq, PartialEq)]
69
+ pub struct SessionEnvironment {
70
+ cwd: PathBuf,
71
+ variables: Arc<Vec<(OsString, OsString)>>,
72
+ }
73
+
74
+ #[derive(serde::Deserialize, serde::Serialize)]
75
+ struct SessionEnvironmentWire {
76
+ cwd: NativeOsString,
77
+ variables: Vec<(NativeOsString, NativeOsString)>,
78
+ }
79
+
80
+ #[derive(serde::Deserialize, serde::Serialize)]
81
+ #[serde(transparent)]
82
+ struct NativeOsString(String);
83
+
84
+ impl serde::Serialize for SessionEnvironment {
85
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
86
+ where
87
+ S: serde::Serializer,
88
+ {
89
+ SessionEnvironmentWire {
90
+ cwd: NativeOsString::encode(self.cwd.as_os_str()),
91
+ variables: self
92
+ .variables
93
+ .iter()
94
+ .map(|(name, value)| (NativeOsString::encode(name), NativeOsString::encode(value)))
95
+ .collect(),
96
+ }
97
+ .serialize(serializer)
98
+ }
99
+ }
100
+
101
+ impl<'de> serde::Deserialize<'de> for SessionEnvironment {
102
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103
+ where
104
+ D: serde::Deserializer<'de>,
105
+ {
106
+ let wire = SessionEnvironmentWire::deserialize(deserializer)?;
107
+ let cwd = PathBuf::from(wire.cwd.decode().map_err(serde::de::Error::custom)?);
108
+ let variables = wire
109
+ .variables
110
+ .into_iter()
111
+ .map(|(name, value)| {
112
+ Ok((
113
+ name.decode().map_err(serde::de::Error::custom)?,
114
+ value.decode().map_err(serde::de::Error::custom)?,
115
+ ))
116
+ })
117
+ .collect::<Result<Vec<_>, D::Error>>()?;
118
+ Ok(Self::new(cwd, variables))
119
+ }
120
+ }
121
+
122
+ impl NativeOsString {
123
+ fn encode(value: &OsStr) -> Self {
124
+ use base64::Engine;
125
+ Self(base64::engine::general_purpose::STANDARD_NO_PAD.encode(native_bytes(value)))
126
+ }
127
+
128
+ fn decode(self) -> Result<OsString, String> {
129
+ use base64::Engine;
130
+ let bytes = base64::engine::general_purpose::STANDARD_NO_PAD
131
+ .decode(self.0)
132
+ .map_err(|error| format!("invalid native environment encoding: {error}"))?;
133
+ native_string(bytes)
134
+ }
135
+ }
136
+
137
+ #[cfg(unix)]
138
+ pub(crate) fn native_bytes(value: &OsStr) -> Vec<u8> {
139
+ use std::os::unix::ffi::OsStrExt;
140
+ value.as_bytes().to_vec()
141
+ }
142
+
143
+ #[cfg(windows)]
144
+ pub(crate) fn native_bytes(value: &OsStr) -> Vec<u8> {
145
+ use std::os::windows::ffi::OsStrExt;
146
+ value
147
+ .encode_wide()
148
+ .flat_map(u16::to_le_bytes)
149
+ .collect::<Vec<_>>()
150
+ }
151
+
152
+ #[cfg(not(any(unix, windows)))]
153
+ pub(crate) fn native_bytes(value: &OsStr) -> Vec<u8> {
154
+ value.to_string_lossy().as_bytes().to_vec()
155
+ }
156
+
157
+ #[cfg(unix)]
158
+ fn native_string(bytes: Vec<u8>) -> Result<OsString, String> {
159
+ use std::os::unix::ffi::OsStringExt;
160
+ Ok(OsString::from_vec(bytes))
161
+ }
162
+
163
+ #[cfg(windows)]
164
+ fn native_string(bytes: Vec<u8>) -> Result<OsString, String> {
165
+ use std::os::windows::ffi::OsStringExt;
166
+ if !bytes.len().is_multiple_of(2) {
167
+ return Err("invalid odd-length native Windows environment value".to_string());
168
+ }
169
+ let units = bytes
170
+ .as_chunks::<2>()
171
+ .0
172
+ .iter()
173
+ .map(|pair| u16::from_le_bytes(*pair))
174
+ .collect::<Vec<_>>();
175
+ Ok(OsString::from_wide(&units))
176
+ }
177
+
178
+ #[cfg(not(any(unix, windows)))]
179
+ fn native_string(bytes: Vec<u8>) -> Result<OsString, String> {
180
+ String::from_utf8(bytes)
181
+ .map(OsString::from)
182
+ .map_err(|error| format!("invalid native environment UTF-8: {error}"))
183
+ }
184
+
185
+ impl SessionEnvironment {
186
+ /// Captures the current process state without reading any configuration file.
187
+ pub fn capture() -> Result<Self, String> {
188
+ let cwd = std::env::current_dir()
189
+ .map_err(|error| format!("Cannot determine the session working directory: {error}."))?;
190
+ Ok(Self::new(cwd, std::env::vars_os().collect()))
191
+ }
192
+
193
+ /// Builds an immutable snapshot from native values received over IPC.
194
+ pub fn new(cwd: PathBuf, variables: Vec<(OsString, OsString)>) -> Self {
195
+ Self {
196
+ cwd,
197
+ variables: Arc::new(variables),
198
+ }
199
+ }
200
+
201
+ /// The exact working directory captured by the stdio proxy.
202
+ pub fn cwd(&self) -> &Path {
203
+ &self.cwd
204
+ }
205
+
206
+ /// The exact native environment captured by the stdio proxy.
207
+ pub fn variables(&self) -> &[(OsString, OsString)] {
208
+ self.variables.as_slice()
209
+ }
210
+
211
+ /// Looks up one environment value using the platform's variable-name rules.
212
+ pub fn var_os(&self, name: &str) -> Option<OsString> {
213
+ self.variables
214
+ .iter()
215
+ .rev()
216
+ .find(|(candidate, _)| environment_name_eq(candidate, name))
217
+ .map(|(_, value)| value.clone())
218
+ }
219
+
220
+ /// Looks up one UTF-8 environment value with `std::env::var`-compatible failures.
221
+ pub fn var(&self, name: &str) -> Result<String, VarError> {
222
+ match self.var_os(name) {
223
+ Some(value) => value.into_string().map_err(VarError::NotUnicode),
224
+ None => Err(VarError::NotPresent),
225
+ }
226
+ }
227
+
228
+ /// Replaces a child command's inherited environment with this exact snapshot.
229
+ pub fn configure_command(&self, command: &mut Command) {
230
+ command.env_clear().envs(self.variables.iter().cloned());
231
+ }
232
+
233
+ /// Runs synchronous work with legacy budget/path helpers bound to this connection.
234
+ pub(crate) fn activate<R>(self: &Arc<Self>, operation: impl FnOnce() -> R) -> R {
235
+ let _scope = SessionEnvironmentScope::install(Arc::clone(self));
236
+ operation()
237
+ }
238
+
239
+ fn with_guarded_output(&self, output: EffectiveOutput) -> Self {
240
+ if output.mode != EffectiveOutputMode::Guarded {
241
+ return self.clone();
242
+ }
243
+ let mut variables = self
244
+ .variables
245
+ .iter()
246
+ .filter(|(name, _)| !is_budget_variable(name))
247
+ .cloned()
248
+ .collect::<Vec<_>>();
249
+ variables.push((
250
+ OsString::from(GLOBAL_TOKEN_BUDGET_ENV),
251
+ OsString::from(output.fastctx_budget.to_string()),
252
+ ));
253
+ append_tool_budget(
254
+ &mut variables,
255
+ READ_TOKEN_BUDGET_ENV,
256
+ output.tool_budgets.read.resolve(output.fastctx_budget),
257
+ );
258
+ append_tool_budget(
259
+ &mut variables,
260
+ GREP_TOKEN_BUDGET_ENV,
261
+ output.tool_budgets.grep.resolve(output.fastctx_budget),
262
+ );
263
+ append_tool_budget(
264
+ &mut variables,
265
+ GLOB_TOKEN_BUDGET_ENV,
266
+ output.tool_budgets.glob.resolve(output.fastctx_budget),
267
+ );
268
+ append_tool_budget(
269
+ &mut variables,
270
+ RUN_TOKEN_BUDGET_ENV,
271
+ output.tool_budgets.run.resolve(output.fastctx_budget),
272
+ );
273
+ append_tool_budget(
274
+ &mut variables,
275
+ JOB_OUTPUT_TOKEN_BUDGET_ENV,
276
+ output
277
+ .tool_budgets
278
+ .job_output
279
+ .resolve(output.fastctx_budget),
280
+ );
281
+ Self::new(self.cwd.clone(), variables)
282
+ }
283
+ }
284
+
285
+ /// All immutable state derived once when a control-center connection is accepted.
286
+ #[derive(Clone, Debug)]
287
+ pub struct SessionContext {
288
+ /// Exact native cwd/env captured by this connection's stdio proxy.
289
+ ///
290
+ /// This stays the sole basis for FastCtx's own identity — control paths, endpoint, budgets —
291
+ /// so restoring the machine's persisted environment can never relocate a user's state.
292
+ pub environment: Arc<SessionEnvironment>,
293
+ /// Session state used only by FastCtx's internal path and response-budget helpers.
294
+ tool_environment: Arc<SessionEnvironment>,
295
+ /// Environment handed to commands the user runs, with the machine's persisted values restored.
296
+ pub command_environment: Arc<SessionEnvironment>,
297
+ /// Per-user paths resolved exclusively from the connection environment.
298
+ pub control_paths: ControlPaths,
299
+ /// Saved FastCtx preferences visible to this connection.
300
+ pub settings: FastCtxSettings,
301
+ /// Provider-aware output policy made for this connection.
302
+ pub effective_output: EffectiveOutput,
303
+ /// Aggregate response pool owned by this connection when Guarded is effective.
304
+ guarded_burst: Option<Arc<GuardedBurstPool>>,
305
+ }
306
+
307
+ impl SessionContext {
308
+ /// Captures and resolves one standalone server process as a single connection.
309
+ pub fn capture() -> Result<Arc<Self>, String> {
310
+ Self::from_environment(SessionEnvironment::capture()?)
311
+ }
312
+
313
+ /// Resolves per-user paths, settings, provider, and runtime budgets from one handshake.
314
+ pub fn from_environment(environment: SessionEnvironment) -> Result<Arc<Self>, String> {
315
+ let control_paths = control_paths_from_environment(&environment)?;
316
+ let settings = crate::control::settings::load(&control_paths)?;
317
+ let effective_output = effective_output(settings.tier, settings.tool_budgets);
318
+ let tool_environment = Arc::new(environment.with_guarded_output(effective_output));
319
+ let guarded_burst = guarded_burst_for(effective_output);
320
+ let command_environment =
321
+ Arc::new(crate::os_environment::command_environment(&environment));
322
+ let environment = Arc::new(environment);
323
+ Ok(Arc::new(Self {
324
+ environment,
325
+ tool_environment,
326
+ command_environment,
327
+ control_paths,
328
+ settings,
329
+ effective_output,
330
+ guarded_burst,
331
+ }))
332
+ }
333
+
334
+ /// Best-effort context used only by direct library constructors that cannot return startup errors.
335
+ pub(crate) fn library_default() -> Arc<Self> {
336
+ Self::capture().unwrap_or_else(|_| {
337
+ let environment = SessionEnvironment::capture().unwrap_or_else(|_| {
338
+ SessionEnvironment::new(PathBuf::from("."), std::env::vars_os().collect())
339
+ });
340
+ let home = environment
341
+ .var_os("HOME")
342
+ .filter(|value| !value.is_empty())
343
+ .or_else(|| {
344
+ environment
345
+ .var_os("USERPROFILE")
346
+ .filter(|value| !value.is_empty())
347
+ })
348
+ .map(PathBuf::from)
349
+ .unwrap_or_else(|| environment.cwd().to_path_buf());
350
+ let control_paths = ControlPaths::for_home(home);
351
+ let settings = FastCtxSettings::default();
352
+ let effective_output = effective_output(settings.tier, settings.tool_budgets);
353
+ let tool_environment = Arc::new(environment.with_guarded_output(effective_output));
354
+ let guarded_burst = guarded_burst_for(effective_output);
355
+ let command_environment =
356
+ Arc::new(crate::os_environment::command_environment(&environment));
357
+ Arc::new(Self {
358
+ tool_environment,
359
+ command_environment,
360
+ environment: Arc::new(environment),
361
+ control_paths,
362
+ settings,
363
+ effective_output,
364
+ guarded_burst,
365
+ })
366
+ })
367
+ }
368
+
369
+ /// Runs synchronous FastCtx work with this connection's cwd and effective response budgets.
370
+ pub(crate) fn activate<R>(&self, operation: impl FnOnce() -> R) -> R {
371
+ self.tool_environment.activate(operation)
372
+ }
373
+
374
+ /// Effective per-tool shares retained for diagnostics and tests.
375
+ pub fn tool_budgets(&self) -> ToolBudgets {
376
+ self.effective_output.tool_budgets
377
+ }
378
+
379
+ /// Registers one tool response with this connection's Guarded burst, when active.
380
+ pub(crate) fn begin_guarded_response(&self) -> Option<BurstTicket> {
381
+ self.guarded_burst.as_ref().map(GuardedBurstPool::begin)
382
+ }
383
+ }
384
+
385
+ fn guarded_burst_for(output: EffectiveOutput) -> Option<Arc<GuardedBurstPool>> {
386
+ if output.mode != EffectiveOutputMode::Guarded {
387
+ return None;
388
+ }
389
+ Some(GuardedBurstPool::new(output.fastctx_budget, BURST_GAP))
390
+ }
391
+
392
+ struct SessionEnvironmentScope {
393
+ previous: Option<Arc<SessionEnvironment>>,
394
+ }
395
+
396
+ impl SessionEnvironmentScope {
397
+ fn install(environment: Arc<SessionEnvironment>) -> Self {
398
+ let previous = ACTIVE_ENVIRONMENT.with(|slot| slot.borrow_mut().replace(environment));
399
+ Self { previous }
400
+ }
401
+ }
402
+
403
+ impl Drop for SessionEnvironmentScope {
404
+ fn drop(&mut self) {
405
+ ACTIVE_ENVIRONMENT.with(|slot| *slot.borrow_mut() = self.previous.take());
406
+ }
407
+ }
408
+
409
+ /// Clones the active connection environment for work that must cross a thread boundary.
410
+ #[cfg(feature = "pdf")]
411
+ pub(crate) fn active_environment() -> Option<Arc<SessionEnvironment>> {
412
+ ACTIVE_ENVIRONMENT.with(|slot| slot.borrow().clone())
413
+ }
414
+
415
+ /// Returns the active connection cwd, falling back only for non-daemon library/control callers.
416
+ pub(crate) fn current_dir() -> std::io::Result<PathBuf> {
417
+ ACTIVE_ENVIRONMENT.with(|slot| {
418
+ slot.borrow()
419
+ .as_ref()
420
+ .map(|environment| environment.cwd.clone())
421
+ .map(Ok)
422
+ .unwrap_or_else(std::env::current_dir)
423
+ })
424
+ }
425
+
426
+ /// Returns an active native environment value, falling back only outside a tool request.
427
+ #[cfg(feature = "pdf")]
428
+ pub(crate) fn var_os(name: &str) -> Option<OsString> {
429
+ ACTIVE_ENVIRONMENT.with(|slot| match slot.borrow().as_ref() {
430
+ Some(environment) => environment.var_os(name),
431
+ None => std::env::var_os(name),
432
+ })
433
+ }
434
+
435
+ /// Returns an active connection variable, falling back only outside a tool request.
436
+ pub(crate) fn var(name: &str) -> Result<String, VarError> {
437
+ ACTIVE_ENVIRONMENT.with(|slot| {
438
+ slot.borrow()
439
+ .as_ref()
440
+ .map(|environment| environment.var(name))
441
+ .unwrap_or_else(|| std::env::var(name))
442
+ })
443
+ }
444
+
445
+ /// Resolves a temporary directory from the active session without consulting daemon state.
446
+ #[cfg(feature = "pdf")]
447
+ pub(crate) fn temp_dir() -> PathBuf {
448
+ let Some(environment) = active_environment() else {
449
+ return std::env::temp_dir();
450
+ };
451
+ let names: &[&str] = if cfg!(windows) {
452
+ &["TMP", "TEMP"]
453
+ } else {
454
+ &["TMPDIR", "TMP", "TEMP"]
455
+ };
456
+ if let Some(path) = names
457
+ .iter()
458
+ .filter_map(|name| environment.var_os(name))
459
+ .find(|value| !value.is_empty())
460
+ .map(PathBuf::from)
461
+ {
462
+ return if path.is_absolute() {
463
+ path
464
+ } else {
465
+ environment.cwd().join(path)
466
+ };
467
+ }
468
+ #[cfg(windows)]
469
+ {
470
+ environment
471
+ .var_os("USERPROFILE")
472
+ .filter(|value| !value.is_empty())
473
+ .map(PathBuf::from)
474
+ .map(|path| path.join("AppData/Local/Temp"))
475
+ .unwrap_or_else(|| environment.cwd().join(".fastctx-tmp"))
476
+ }
477
+ #[cfg(not(windows))]
478
+ {
479
+ PathBuf::from("/tmp")
480
+ }
481
+ }
482
+
483
+ fn control_paths_from_environment(
484
+ environment: &SessionEnvironment,
485
+ ) -> Result<ControlPaths, String> {
486
+ let home = environment
487
+ .var_os("HOME")
488
+ .filter(|value| !value.is_empty())
489
+ .or_else(|| {
490
+ environment
491
+ .var_os("USERPROFILE")
492
+ .filter(|value| !value.is_empty())
493
+ })
494
+ .map(PathBuf::from)
495
+ .ok_or_else(|| {
496
+ "Cannot determine the user home directory. Set HOME or USERPROFILE and retry."
497
+ .to_string()
498
+ })?;
499
+ Ok(ControlPaths::for_home(home))
500
+ }
501
+
502
+ fn append_tool_budget(
503
+ variables: &mut Vec<(OsString, OsString)>,
504
+ name: &'static str,
505
+ value: Option<usize>,
506
+ ) {
507
+ if let Some(value) = value {
508
+ variables.push((OsString::from(name), OsString::from(value.to_string())));
509
+ }
510
+ }
511
+
512
+ fn is_budget_variable(name: &OsStr) -> bool {
513
+ [
514
+ GLOBAL_TOKEN_BUDGET_ENV,
515
+ READ_TOKEN_BUDGET_ENV,
516
+ GREP_TOKEN_BUDGET_ENV,
517
+ GLOB_TOKEN_BUDGET_ENV,
518
+ RUN_TOKEN_BUDGET_ENV,
519
+ JOB_OUTPUT_TOKEN_BUDGET_ENV,
520
+ ]
521
+ .iter()
522
+ .any(|candidate| environment_name_eq(name, candidate))
523
+ }
524
+
525
+ #[cfg(windows)]
526
+ pub(crate) fn environment_name_eq(candidate: &OsStr, expected: &str) -> bool {
527
+ candidate
528
+ .to_str()
529
+ .is_some_and(|candidate| candidate.eq_ignore_ascii_case(expected))
530
+ }
531
+
532
+ #[cfg(not(windows))]
533
+ pub(crate) fn environment_name_eq(candidate: &OsStr, expected: &str) -> bool {
534
+ candidate == OsStr::new(expected)
535
+ }
536
+
537
+ /// Compares two native variable names under the same rules `environment_name_eq` applies.
538
+ pub(crate) fn environment_name_eq_os(candidate: &OsStr, expected: &OsStr) -> bool {
539
+ match expected.to_str() {
540
+ Some(expected) => environment_name_eq(candidate, expected),
541
+ None => candidate == expected,
542
+ }
543
+ }
544
+
545
+ #[cfg(test)]
546
+ mod tests {
547
+ use super::{EffectiveOutputMode, SessionContext, SessionEnvironment};
548
+ use crate::budget::{GLOBAL_TOKEN_BUDGET_ENV, GREP_TOKEN_BUDGET_ENV, tool_token_budget};
549
+ use crate::control::settings::Tier;
550
+ use std::ffi::OsString;
551
+ use std::path::PathBuf;
552
+ use std::sync::Arc;
553
+
554
+ #[test]
555
+ fn session_resolves_the_selected_tier_and_preserves_native_state() {
556
+ let temp = tempfile::tempdir().unwrap();
557
+ let home = temp.path().join("home");
558
+ std::fs::create_dir_all(&home).unwrap();
559
+ let environment = SessionEnvironment::new(
560
+ temp.path().to_path_buf(),
561
+ vec![
562
+ (OsString::from("HOME"), home.into_os_string()),
563
+ (
564
+ OsString::from(GLOBAL_TOKEN_BUDGET_ENV),
565
+ OsString::from("54000"),
566
+ ),
567
+ (
568
+ OsString::from(GREP_TOKEN_BUDGET_ENV),
569
+ OsString::from("10800"),
570
+ ),
571
+ (OsString::from("SESSION_SENTINEL"), OsString::from("kept")),
572
+ ],
573
+ );
574
+
575
+ let session = SessionContext::from_environment(environment).unwrap();
576
+ assert_eq!(
577
+ session.effective_output.mode,
578
+ EffectiveOutputMode::SelectedTier
579
+ );
580
+ assert_eq!(
581
+ session.tool_budgets(),
582
+ Tier::Standard.default_budgets(),
583
+ "a default profile follows the standard tier's recommended shares"
584
+ );
585
+ assert_eq!(
586
+ session.environment.var(GLOBAL_TOKEN_BUDGET_ENV).unwrap(),
587
+ "54000"
588
+ );
589
+ assert_eq!(
590
+ session.environment.var(GREP_TOKEN_BUDGET_ENV).unwrap(),
591
+ "10800"
592
+ );
593
+ assert_eq!(session.environment.var("SESSION_SENTINEL").unwrap(), "kept");
594
+ session.activate(|| {
595
+ assert_eq!(
596
+ tool_token_budget(GREP_TOKEN_BUDGET_ENV).unwrap().value,
597
+ 10_800
598
+ );
599
+ });
600
+ }
601
+
602
+ #[test]
603
+ fn nested_activation_restores_the_outer_connection() {
604
+ let first = Arc::new(SessionEnvironment::new(
605
+ PathBuf::from("first"),
606
+ vec![(OsString::from("MARK"), OsString::from("first"))],
607
+ ));
608
+ let second = Arc::new(SessionEnvironment::new(
609
+ PathBuf::from("second"),
610
+ vec![(OsString::from("MARK"), OsString::from("second"))],
611
+ ));
612
+ first.activate(|| {
613
+ assert_eq!(super::var("MARK").unwrap(), "first");
614
+ second.activate(|| assert_eq!(super::var("MARK").unwrap(), "second"));
615
+ assert_eq!(super::var("MARK").unwrap(), "first");
616
+ });
617
+ }
618
+
619
+ #[test]
620
+ fn environment_json_round_trip_preserves_native_values() {
621
+ let environment = SessionEnvironment::new(
622
+ PathBuf::from("workspace"),
623
+ vec![(OsString::from("NAME"), OsString::from("value"))],
624
+ );
625
+ let encoded = serde_json::to_vec(&environment).unwrap();
626
+ let decoded: SessionEnvironment = serde_json::from_slice(&encoded).unwrap();
627
+ assert_eq!(decoded, environment);
628
+ }
629
+ }
@@ -0,0 +1,41 @@
1
+ // Modified by dsh-ops (https://github.com/T-Auto/dsh-ops): the apply_patch misuse note points at the host's own tool instead of Codex's built-in shell.
2
+ //! Recognition of the host's `apply_patch` editing channel being run as a shell program.
3
+
4
+ const CHANNEL_NAME: &str = "apply_patch";
5
+ const MISUSE_NOTE: &str = "(Note: apply_patch is not a program and no shell can run it. Reach it through your host's own apply_patch tool call, not through this tool.)";
6
+
7
+ /// Returns the guidance note for a failed command that reads as an attempt to run `apply_patch`.
8
+ ///
9
+ /// The host resolves `apply_patch` on its own shell channel before any real bash sees it, so the
10
+ /// word only ever reaches a shell when it is routed through a tool the host does not inspect —
11
+ /// and bash then reports it as a missing command. The note stays tentative and never blocks the
12
+ /// command, because a user may legitimately own an executable by that name: it appears only after
13
+ /// the command has actually failed, so a working script is never interrupted or second-guessed.
14
+ pub(crate) fn misuse_note(
15
+ command: &str,
16
+ exit_code: i32,
17
+ timeout_ms: Option<u64>,
18
+ ) -> Option<String> {
19
+ let failed_on_its_own = timeout_ms.is_none() && exit_code != 0;
20
+ (failed_on_its_own && invokes_channel(command)).then(|| MISUSE_NOTE.to_string())
21
+ }
22
+
23
+ /// Reports whether any command position in the line starts the bare `apply_patch` word.
24
+ ///
25
+ /// Splitting on shell separators keeps `echo apply_patch` and `/usr/bin/apply_patch` out: the
26
+ /// first is not in command position, the second names a real executable the user does own.
27
+ fn invokes_channel(command: &str) -> bool {
28
+ command.contains(CHANNEL_NAME)
29
+ && command
30
+ .split([';', '&', '|', '\n', '(', '{'])
31
+ .any(|segment| leading_word(segment) == CHANNEL_NAME)
32
+ }
33
+
34
+ /// Extracts the first word of a segment, stopping at whitespace or a redirection operator.
35
+ fn leading_word(segment: &str) -> &str {
36
+ let trimmed = segment.trim_start();
37
+ let end = trimmed
38
+ .find(|character: char| character.is_whitespace() || character == '<' || character == '>')
39
+ .unwrap_or(trimmed.len());
40
+ &trimmed[..end]
41
+ }