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.
- package/CHANGELOG.md +202 -0
- package/LICENSE +30 -0
- package/NOTICE +106 -0
- package/PROVENANCE.md +435 -0
- package/README.en.md +126 -0
- package/README.md +115 -2
- package/README.zh.md +116 -0
- package/bin/dsh-ops.mjs +1216 -0
- package/cordis.patch.yml +160 -0
- package/docs/manual-validation.md +53 -0
- package/docs/release-0.2.1.md +72 -0
- package/docs/schema-baseline.json +64 -0
- package/docs/schema-current.json +84 -0
- package/docs/schema-measurement.md +17 -0
- package/dsh-plugin.json +88 -0
- package/icon.svg +12 -0
- package/lib/binary.js +409 -0
- package/lib/config.js +198 -0
- package/lib/handshake.js +252 -0
- package/lib/index.js +108 -0
- package/lib/jobs.js +42 -0
- package/lib/policy.js +64 -0
- package/lib/presentation.js +63 -0
- package/lib/profile-install.js +61 -0
- package/lib/rust.js +194 -0
- package/lib/session-shells.js +78 -0
- package/lib/shells.js +998 -0
- package/lib/tools.js +657 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +114 -4
- package/vendor/fastctx/Cargo.lock +3210 -0
- package/vendor/fastctx/Cargo.toml +94 -0
- package/vendor/fastctx/FORK.md +119 -0
- package/vendor/fastctx/LICENSE-APACHE +201 -0
- package/vendor/fastctx/NOTICE +40 -0
- package/vendor/fastctx/README.md +439 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
- package/vendor/fastctx/UPSTREAM.md +49 -0
- package/vendor/fastctx/build.rs +413 -0
- package/vendor/fastctx/src/background_status.rs +403 -0
- package/vendor/fastctx/src/binary.rs +75 -0
- package/vendor/fastctx/src/bounded_sort.rs +500 -0
- package/vendor/fastctx/src/budget.rs +781 -0
- package/vendor/fastctx/src/cli/mod.rs +110 -0
- package/vendor/fastctx/src/context_guard.rs +289 -0
- package/vendor/fastctx/src/control/mod.rs +6 -0
- package/vendor/fastctx/src/control/paths.rs +49 -0
- package/vendor/fastctx/src/control/settings.rs +753 -0
- package/vendor/fastctx/src/control/transaction.rs +531 -0
- package/vendor/fastctx/src/edit/document.rs +535 -0
- package/vendor/fastctx/src/edit/locks.rs +371 -0
- package/vendor/fastctx/src/edit/mod.rs +213 -0
- package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
- package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
- package/vendor/fastctx/src/edit/private_storage.rs +234 -0
- package/vendor/fastctx/src/edit/replace.rs +1030 -0
- package/vendor/fastctx/src/edit_server.rs +53 -0
- package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
- package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
- package/vendor/fastctx/src/encoding.rs +1118 -0
- package/vendor/fastctx/src/file_executor.rs +1151 -0
- package/vendor/fastctx/src/file_snapshot.rs +1491 -0
- package/vendor/fastctx/src/glob_filter.rs +98 -0
- package/vendor/fastctx/src/glob_tool.rs +653 -0
- package/vendor/fastctx/src/grep_sink.rs +1162 -0
- package/vendor/fastctx/src/grep_tool.rs +2449 -0
- package/vendor/fastctx/src/lib.rs +45 -0
- package/vendor/fastctx/src/main.rs +15 -0
- package/vendor/fastctx/src/model.rs +51 -0
- package/vendor/fastctx/src/model_guidance.rs +62 -0
- package/vendor/fastctx/src/operation.rs +356 -0
- package/vendor/fastctx/src/ordered_window.rs +1235 -0
- package/vendor/fastctx/src/os_environment.rs +414 -0
- package/vendor/fastctx/src/path_codec.rs +850 -0
- package/vendor/fastctx/src/paths.rs +244 -0
- package/vendor/fastctx/src/process_identity.rs +763 -0
- package/vendor/fastctx/src/process_policy.rs +74 -0
- package/vendor/fastctx/src/read_tool/batch.rs +496 -0
- package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
- package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
- package/vendor/fastctx/src/read_tool/mod.rs +245 -0
- package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
- package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
- package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
- package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
- package/vendor/fastctx/src/render_plan.rs +468 -0
- package/vendor/fastctx/src/runtime/activity.rs +159 -0
- package/vendor/fastctx/src/runtime/hosts.rs +99 -0
- package/vendor/fastctx/src/runtime/journal.rs +556 -0
- package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
- package/vendor/fastctx/src/runtime/mod.rs +746 -0
- package/vendor/fastctx/src/runtime/protocol.rs +296 -0
- package/vendor/fastctx/src/runtime/session.rs +536 -0
- package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
- package/vendor/fastctx/src/search_parallelism.rs +106 -0
- package/vendor/fastctx/src/search_text.rs +227 -0
- package/vendor/fastctx/src/server.rs +359 -0
- package/vendor/fastctx/src/server_manifest.rs +468 -0
- package/vendor/fastctx/src/server_support.rs +826 -0
- package/vendor/fastctx/src/session.rs +629 -0
- package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
- package/vendor/fastctx/src/shell/bash.rs +263 -0
- package/vendor/fastctx/src/shell/buffer.rs +108 -0
- package/vendor/fastctx/src/shell/encoding.rs +403 -0
- package/vendor/fastctx/src/shell/foreground.rs +115 -0
- package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
- package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
- package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
- package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
- package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
- package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
- package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
- package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
- package/vendor/fastctx/src/shell/mod.rs +345 -0
- package/vendor/fastctx/src/shell/normalize.rs +389 -0
- package/vendor/fastctx/src/shell/output.rs +406 -0
- package/vendor/fastctx/src/shell/process.rs +493 -0
- package/vendor/fastctx/src/shell_server.rs +156 -0
- package/vendor/fastctx/src/skip_report.rs +83 -0
- package/vendor/fastctx/src/stdio_transport.rs +177 -0
- package/vendor/fastctx/src/tool_schema.rs +204 -0
- package/vendor/fastctx/src/traversal.rs +846 -0
- package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
- 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
|
+
}
|