Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ CXXFLAGS_aarch64_pc_windows_msvc = { value = "-DNDEBUG /utf-8", force = false }
# connections. Nothing reads those statistics or sets a heap limit.
LIBSQLITE3_FLAGS = { value = "-DSQLITE_MAX_WORKER_THREADS=15 -DSQLITE_DEFAULT_MEMSTATUS=0", force = true }

# Global accounting (savings ledger, worldwide-counter flushes) is ON by
# Global accounting (the savings ledger) is ON by
# default for installed binaries, but `cargo test` / `cargo run` would then
# write temp-project rows into the developer's real ~/.tracedecay/global.db.
# Keep cargo-launched processes hermetic; tests that exercise the ledger
Expand Down
22 changes: 7 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,6 @@ authorized, sanitized, classified records under their remote policy.

Network-capable effects are separate from core retrieval:

- Worldwide counter: when opted in, sends the pending aggregate token-savings
amount, and `tracedecay doctor` reads the public total to check reachability.
- GitHub release checks and updates: contact GitHub release endpoints. An
explicitly configured private GitHub review source can use a read-only
credential from the operating-system keyring; missing, ambiguous,
Expand All @@ -212,19 +210,13 @@ Network-capable effects are separate from core retrieval:
path fails closed with typed `unavailable`, `denied`, or `stale-peer` state.

These services receive normal transport metadata, including the connection's
source address, in addition to any request payload. The counter request is an
aggregate amount, not repository content, but it is not an anonymity guarantee.
Use a network policy or firewall if a command must make no outbound connection.
An opt-out, denial, timeout, or unavailable remote service must remain visible
as that state; it is not evidence of a zero counter, an up-to-date release, or
current pricing. See [Security](SECURITY.md#network-access) for the complete
outbound-access and credential boundary.

Disable the worldwide counter with:

```bash
tracedecay disable-upload-counter
```
source address, in addition to any request payload. Use a network policy or
firewall if a command must make no outbound connection. A denial, timeout, or
unavailable remote service must remain visible as that state; it is not
evidence of an up-to-date release or current pricing. TraceDecay no longer has
a worldwide token counter: token savings and costs stay local. See
[Security](SECURITY.md#network-access) for the complete outbound-access and
credential boundary.

## Troubleshooting

Expand Down
2 changes: 0 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ Outbound capabilities include:
| Configured remote sources and authorities | Policy-authorized retrieval and remote operations | Configured peer authentication and disclosure policy | Admission or authorization failures are reported; review each peer before enabling |
| `github.com` | Download binary during `tracedecay upgrade` | None (public releases) | Error shown to user |
| `huggingface.co` and Hugging Face artifact hosts | Download missing, revision-pinned semantic-model artifacts when semantic auto-download is enabled | None | Semantic retrieval reports model acquisition state or failure; exact, lexical, and graph retrieval remain available |
| `tracedecay-counter.enzinol.workers.dev` | Aggregate token-savings counter | None | Silently ignored |

Provider usage and pricing do not add an outbound connection. `tracedecay cost`
reads immutable provider-native usage observations and the deterministic bundled
Expand Down Expand Up @@ -169,7 +168,6 @@ an old count of unsafe sites is not a security guarantee.

The following are **not** security issues:

- The aggregate token counter sending a count to the public Cloudflare Worker endpoint (this is documented behavior and contains no identifying information beyond an approximate country derived from IP by Cloudflare)
- The database containing symbol names or file paths from your project (this is core functionality)
- The MCP edit tools modifying files (this is opt-in functionality your AI agent invokes through its own tool-approval flow)
- `tracedecay_run_affected_tests` compiling and running your project's own test suite (this is the tool's documented purpose)
19 changes: 14 additions & 5 deletions crates/tracedecay-api/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ pub struct UserSettingsPatch {
pub expected_revision_id: String,
pub idempotency_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upload_enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub watcher_debounce: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extraction_timeout_secs: Option<u64>,
Expand Down Expand Up @@ -121,7 +119,6 @@ pub fn parse_code_index_worker_settings_patch(
/// write that unsigned value and do not parse the wire string again.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdmittedUserSettingsPatch {
pub upload_enabled: Option<bool>,
pub watcher_debounce_ms: Option<u64>,
pub extraction_timeout_secs: Option<u64>,
}
Expand Down Expand Up @@ -154,7 +151,6 @@ pub fn admit_user_settings_patch(
}
if errors.is_empty() {
Ok(AdmittedUserSettingsPatch {
upload_enabled: patch.upload_enabled,
watcher_debounce_ms,
extraction_timeout_secs: patch.extraction_timeout_secs,
})
Expand Down Expand Up @@ -332,12 +328,25 @@ mod tests {
let result = parse_user_settings_patch(json!({
"expected_revision_id": "configuration.revision.project",
"idempotency_key": "configuration.idempotency.fixture",
"upload_enabled": true,
"watcher_debounce": "2s",
"code_index_workers": { "mode": "automatic" }
}));

let (status, Json(body)) = result.expect_err("mixed authority must be rejected");
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(body["validation_errors"][0]["field"], "code_index_workers");
}

#[test]
fn user_patch_refuses_the_retired_worldwide_counter_upload_field() {
let result = parse_user_settings_patch(json!({
"expected_revision_id": "configuration.revision.profile",
"idempotency_key": "configuration.idempotency.fixture",
"upload_enabled": true
}));

let (status, Json(body)) = result.expect_err("a retired field must be refused");
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(body["validation_errors"][0]["field"], "upload_enabled");
}
}
14 changes: 0 additions & 14 deletions crates/tracedecay-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,20 +578,6 @@ pub enum Commands {
#[arg(short, long)]
path: Option<String>,
},
/// Disable uploading token counts to the worldwide counter
#[command(
name = "disable-upload-counter",
long_about = DISABLE_UPLOAD_COUNTER_LONG_ABOUT,
after_help = DISABLE_UPLOAD_COUNTER_AFTER_HELP
)]
DisableUploadCounter,
/// Enable uploading token counts to the worldwide counter
#[command(
name = "enable-upload-counter",
long_about = ENABLE_UPLOAD_COUNTER_LONG_ABOUT,
after_help = ENABLE_UPLOAD_COUNTER_AFTER_HELP
)]
EnableUploadCounter,
/// Check tracedecay installation, configuration, and agent integration
#[command(long_about = DOCTOR_LONG_ABOUT, after_help = DOCTOR_AFTER_HELP)]
Doctor {
Expand Down
23 changes: 0 additions & 23 deletions crates/tracedecay-cli/src/cli/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,29 +446,6 @@ Examples:

Related: tracedecay current-counter, tracedecay gain.";

pub(crate) const DISABLE_UPLOAD_COUNTER_LONG_ABOUT: &str = "\
Opts this machine out of contributing anonymous token-savings counts to the \
public worldwide counter. Only aggregate numbers are ever uploaded, never \
code, paths, or queries, but uploading is entirely optional.";

pub(crate) const DISABLE_UPLOAD_COUNTER_AFTER_HELP: &str = "\
Examples:
tracedecay disable-upload-counter Stop contributing counts

Related: tracedecay enable-upload-counter, tracedecay gain (local ledger
is unaffected).";

pub(crate) const ENABLE_UPLOAD_COUNTER_LONG_ABOUT: &str = "\
Re-enables contributing anonymous token-savings counts to the public \
worldwide counter after a previous `disable-upload-counter`. Only aggregate \
numbers are uploaded, never code, paths, or queries.";

pub(crate) const ENABLE_UPLOAD_COUNTER_AFTER_HELP: &str = "\
Examples:
tracedecay enable-upload-counter Resume contributing counts

Related: tracedecay disable-upload-counter, tracedecay gain.";

pub(crate) const DOCTOR_LONG_ABOUT: &str = "\
Checks the binary, PATH, daemon service, project index, and every agent \
integration, printing actionable fixes for anything broken. The running \
Expand Down
99 changes: 8 additions & 91 deletions crates/tracedecay-cli/src/cloud.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
//! HTTP client for the worldwide counter Cloudflare Worker and GitHub release
//! version checking.
//! HTTP client for GitHub release version checking.
//!
//! Counter operations are best-effort: failures surface as `None` / empty.
//! Release lookups classify every failure as a [`ReleaseLookupError`]. All
//! are synchronous `ureq` calls that block the calling thread for up to their
//! own timeout, which an enclosing Tokio deadline cannot cut short. A caller
Expand All @@ -14,20 +12,14 @@ use serde::de::DeserializeOwned;
use tracedecay_application::http_agent::http_agent;
use tracedecay_dashboard_api::cloud::ReleaseLookupError;

/// The Cloudflare Worker endpoint URL.
const WORKER_URL: &str = "https://tracedecay-counter.enzinol.workers.dev";

/// Base URL of the GitHub REST API release lookups are issued against.
pub(crate) const GITHUB_API_URL: &str = "https://api.github.com";
const GITHUB_OWNER: &str = "ScriptedAlchemy";
const GITHUB_REPOSITORY: &str = "tracedecay";

/// Timeout for flush (upload) requests.
const FLUSH_TIMEOUT: Duration = Duration::from_secs(2);

/// Timeout for opportunistic advisory reads (worldwide counter and the
/// background / `doctor` version probe). Keep this short so a stalled
/// GitHub or worker never blocks those paths.
/// Timeout for opportunistic advisory reads (the background / `doctor`
/// version probe). Keep this short so a stalled GitHub never blocks those
/// paths.
const FETCH_TIMEOUT: Duration = Duration::from_secs(1);

/// Bound for each GitHub release-metadata read of an explicit `tracedecay
Expand All @@ -38,53 +30,6 @@ const FETCH_TIMEOUT: Duration = Duration::from_secs(1);
/// keep [`FETCH_TIMEOUT`].
pub(crate) const UPGRADE_RELEASE_LOOKUP_TIMEOUT: Duration = Duration::from_secs(30);

/// Response from the worker's POST /increment and GET /total endpoints.
#[derive(serde::Deserialize)]
struct WorkerResponse {
total: u64,
}

/// Creates a ureq agent with the given timeout.
pub fn agent_with_timeout(timeout: Duration) -> ureq::Agent {
http_agent(
ureq::Agent::config_builder()
.timeout_global(Some(timeout))
.build(),
)
}

/// Uploads pending tokens to the worldwide counter.
/// Returns the new worldwide total on success, or None on any failure.
#[tracing::instrument(name = "cloud.flush_pending", level = "trace", skip_all)]
pub fn flush_pending(amount: u64) -> Option<u64> {
if amount == 0 {
return None;
}
let body = serde_json::json!({ "amount": amount });
let agent = agent_with_timeout(FLUSH_TIMEOUT);
let parsed: WorkerResponse = agent
.post(&format!("{WORKER_URL}/increment"))
.send_json(&body)
.ok()?
.body_mut()
.read_json()
.ok()?;
Some(parsed.total)
}

/// Fetches the current worldwide total from the worker.
/// Returns None on timeout, network error, or parse failure.
#[tracing::instrument(name = "cloud.fetch_worldwide_total", level = "trace", skip_all)]
pub fn fetch_worldwide_total() -> Option<u64> {
fetch_worldwide_total_from(&format!("{WORKER_URL}/total"))
}

fn fetch_worldwide_total_from(url: &str) -> Option<u64> {
let agent = agent_with_timeout(FETCH_TIMEOUT);
let parsed: WorkerResponse = agent.get(url).call().ok()?.body_mut().read_json().ok()?;
Some(parsed.total)
}

/// Response from GitHub releases API (only the fields we need).
#[derive(serde::Deserialize)]
struct GitHubRelease {
Expand Down Expand Up @@ -356,16 +301,15 @@ pub fn is_beta() -> bool {
tracedecay_dashboard_api::cloud::is_beta(env!("CARGO_PKG_VERSION"))
}

/// Admits these ureq implementations into the composition library so MCP
/// flush/version checks can run after the binary starts. Unregistered
/// lookups stay `None` (already best-effort).
/// Admits the ureq release lookup into the composition library so MCP version
/// checks can run after the binary starts. An unregistered lookup stays
/// `None` (already best-effort).
pub fn admit_sync_probes() {
tracedecay_dashboard_api::cloud::admit_sync_cloud_probes(flush_pending, fetch_latest_version);
tracedecay_dashboard_api::cloud::admit_sync_cloud_probes(fetch_latest_version);
}

pub fn doctor_network_probes() -> tracedecay::doctor::AdmittedDoctorNetworkProbes {
tracedecay::doctor::AdmittedDoctorNetworkProbes {
fetch_worldwide_total,
fetch_latest_version,
}
}
Expand Down Expand Up @@ -570,33 +514,6 @@ mod tests {
);
}

#[test]
fn advisory_worldwide_total_still_times_out_at_the_one_second_budget() {
let (base, _heads) = stub_after(
Duration::from_millis(1500),
Some(respond(
"200 OK",
"Content-Type: application/json\r\n",
r#"{"total":42}"#,
)),
);
let started = Instant::now();

assert!(
fetch_worldwide_total_from(&format!("{base}/total")).is_none(),
"advisory worldwide-total must keep the one-second budget"
);
let elapsed = started.elapsed();
assert!(
elapsed >= FETCH_TIMEOUT,
"advisory worldwide-total returned before its one-second budget: {elapsed:?}"
);
assert!(
elapsed < Duration::from_millis(1500),
"advisory worldwide-total waited for the delayed answer: {elapsed:?}"
);
}

fn respond(status: &str, headers: &str, body: &str) -> &'static str {
format!(
"HTTP/1.1 {status}\r\n{headers}Content-Length: {}\r\nConnection: close\r\n\r\n{body}",
Expand Down
9 changes: 4 additions & 5 deletions crates/tracedecay-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,11 @@ pub(crate) use index::{handle_init, handle_no_command, handle_sync};
pub(crate) use profile_storage::handle_profile_storage_action;
pub(crate) use scope::resolve_project_scope;
pub(crate) use settings::{
canonical_upload_enabled, current_configuration_revision, current_project_setting,
handle_upload_counter, mutate_project_configuration, project_configuration_set,
repeated_query_hints_enabled, report_configuration_receipt,
current_configuration_revision, current_project_setting, mutate_project_configuration,
project_configuration_set, repeated_query_hints_enabled, report_configuration_receipt,
};
pub(crate) use stale_store_reset::handle_wipe_stale;
pub(crate) use storage::{
ProfileOfflineAuthority, RestoreWording, annotate_reset_required, handle_list, handle_wipe,
join_outcome_and_restore, process_error_text, take_profile_offline, try_admit_profile_registry,
ProfileOfflineAuthority, RestoreWording, handle_list, handle_wipe, join_outcome_and_restore,
process_error_text, take_profile_offline, try_admit_profile_registry,
};
Loading
Loading