Skip to content

Commit b93f053

Browse files
feat(useragent): detect pi.dev agent via PI_CODING_AGENT env var
Adds pi.dev to the agent detector table so pup automatically switches to agent mode when running inside the pi.dev coding agent. Detection checks PI_CODING_AGENT=true|1, consistent with all other agents. Also refactors the useragent test module to use ENV_LOCK and a shared clear_all_agent_vars() helper, eliminating latent parallel-test races that existed in the prior ad-hoc per-test cleanup. Updates README, ARCHITECTURE.md, and LLM_GUIDE.md to document the new detector. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 31f1b42 commit b93f053

4 files changed

Lines changed: 77 additions & 37 deletions

File tree

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ list of commands as built.
6060
| APM Services || `apm services`, `apm entities`, `apm dependencies`, `apm flow-map` | Services stats, operations, resources; entity queries; dependencies; flow visualization |
6161
| Traces || `traces search`, `traces aggregate`, `traces metrics` | Span search/aggregation and span-based metric definitions |
6262
| Profiling || `profiling aggregate`, `profiling analytics`, `profiling timeline`, … | Continuous Profiler queries (requires API + App keys) |
63+
| Database Monitoring || `dbm samples search` | DBM query sample search |
6364
| Session Replay || - | Not yet implemented |
6465

6566
</details>
@@ -96,6 +97,9 @@ list of commands as built.
9697
| Data Governance || `data-governance scanner-rules list` | Sensitive data scanner rules |
9798
| CSM Threats || `csm-threats` | Cloud Security Management threat rules and agent rules |
9899
| Sensitive Data Scanner || `data-governance scanner-rules list` | Listed via Data Governance row above |
100+
| Agentless Scanning || `agentless-scanning aws list/get/create/update/delete`, `agentless-scanning gcp list`, `agentless-scanning azure list` | Cloud agentless scanning configuration for AWS, GCP, and Azure |
101+
| Logs Restriction || `logs-restriction list`, `logs-restriction get`, `logs-restriction create`, `logs-restriction update`, `logs-restriction delete` | Log restriction queries for fine-grained log access control |
102+
| Data Deletion || `data-deletion requests list`, `data-deletion requests create`, `data-deletion requests cancel` | GDPR/compliance data deletion request management |
99103

100104
</details>
101105

@@ -132,6 +136,8 @@ list of commands as built.
132136
| Investigations || `investigations list`, `investigations get`, `investigations trigger` | Bits AI SRE investigation management |
133137
| Change Management || `change-management create`, `change-management get`, `change-management update`, `change-management create-branch`, `change-management decisions` | Change request management with decisions and branching |
134138
| Incident Services/Teams || `incidents services`, `incidents teams` | Service and team CRUD scoped to incident management |
139+
| Live Debugger || `debugger probes list`, `debugger probes get`, `debugger probes create`, `debugger probes delete`, `debugger probes watch` | Remote log probe management for Live Debugger |
140+
| Software Catalog || `software-catalog entities list`, `software-catalog entities upsert`, `software-catalog kinds list`, `software-catalog relations list` | Software Catalog entity and kind management (next-gen catalog) |
135141

136142
</details>
137143

@@ -144,6 +150,7 @@ list of commands as built.
144150
| Test Optimization || `cicd tests`, `cicd flaky-tests`, `test-optimization` | Test events, flaky test management, and Test Optimization API |
145151
| DORA Metrics || `cicd dora` | DORA deployment patching |
146152
| Code Coverage || `code-coverage branch-summary`, `code-coverage commit-summary` | Branch and commit-level coverage summaries |
153+
| Deployment Gates || `deployment-gates gates`, `deployment-gates evaluations`, `deployment-gates rules` | Deployment gate CRUD, evaluation triggers, and rule management |
147154

148155
</details>
149156

@@ -158,6 +165,7 @@ list of commands as built.
158165
| App Keys || `app-keys list`, `app-keys get`, `app-keys create`, `app-keys update`, `app-keys delete` | Full application key CRUD |
159166
| Service Accounts || - | Managed via users commands |
160167
| Roles || - | Only list via users |
168+
| AuthN Mappings || `authn-mappings list`, `authn-mappings get`, `authn-mappings create`, `authn-mappings update`, `authn-mappings delete` | SAML/IdP attribute-to-role authentication mappings |
161169

162170
</details>
163171

@@ -169,7 +177,10 @@ list of commands as built.
169177
| Usage Metering || `usage summary`, `usage hourly` | Usage and billing metrics |
170178
| Cost Management || `costs datadog projected`, `costs datadog attribution`, `costs datadog by-org`, `costs datadog aws-config`, `costs datadog azure-config`, `costs datadog gcp-config`, `costs ccm custom-costs`, `costs ccm tag-descriptions`, `costs ccm tag-metadata`, `costs ccm tags`, `costs ccm tag-keys`, `costs ccm budgets`, `costs ccm commitments` | Cost attribution, cloud cost configs (AWS/Azure/GCP), and Cloud Cost Management (custom costs, tag descriptions, budgets, commitment programs) |
171179
| Product Analytics || `product-analytics events send`, `product-analytics query` | Server-side product analytics events and queries |
172-
| Integrations || `integrations slack`, `integrations pagerduty`, `integrations webhooks`, `integrations jira`, `integrations servicenow`, `integrations google-chat` | Third-party integrations with Jira, ServiceNow, and Google Chat support |
180+
| Integrations || `integrations slack`, `integrations pagerduty`, `integrations webhooks`, `integrations jira`, `integrations servicenow`, `integrations google-chat`, `integrations ms-teams` | Third-party integrations including Jira, ServiceNow, Google Chat, and Microsoft Teams |
181+
| Feature Flags || `feature-flags flags`, `feature-flags environments`, `feature-flags allocations`, `feature-flags exposure`, `feature-flags enable`, `feature-flags disable` | Feature flag management with environment, allocation, and exposure control |
182+
| Data Streams (Kafka) || `kafka topic-configs`, `kafka broker-configs`, `kafka client-configs`, `kafka read-messages` | **Experimental** — Kafka cluster inspection via Datadog |
183+
| Restricted Datasets || `datasets list`, `datasets get`, `datasets create`, `datasets update`, `datasets delete` | Restricted dataset management for data access control |
173184
| Observability Pipelines || `obs-pipelines list`, `obs-pipelines get`, `obs-pipelines create`, `obs-pipelines update`, `obs-pipelines delete`, `obs-pipelines validate` | Full pipeline CRUD and validation |
174185
| LLM Observability || `llm-obs projects`, `llm-obs experiments`, `llm-obs datasets` | **New** — LLM Obs projects, experiments, and dataset management |
175186
| Reference Tables || `reference-tables list`, `reference-tables get`, `reference-tables create`, `reference-tables batch-query` | **New** — Reference table management for log enrichment |
@@ -383,6 +394,7 @@ Agent mode is **auto-detected** when any of these environment variables are set
383394
| `AMAZON_Q` or `AWS_Q_DEVELOPER` | Amazon Q |
384395
| `GEMINI_CODE_ASSIST` | Gemini Code Assist |
385396
| `SRC_CODY` | Sourcegraph Cody |
397+
| `PI_CODING_AGENT` | pi.dev |
386398
| `FORCE_AGENT_MODE` | Any agent (manual override) |
387399

388400
You can also enable it explicitly with the `--agent` flag or by setting `FORCE_AGENT_MODE=1`:

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ pup/v0.1.0 (rust; os darwin; arch arm64; ai-agent claude-code) # With agent
147147
**AI Agent Detection** (`src/useragent.rs`):
148148

149149
Table-driven registry detecting AI coding agents via environment variables. First match wins:
150-
- Claude Code (`CLAUDECODE`, `CLAUDE_CODE`), Cursor (`CURSOR_AGENT`), Codex (`CODEX`, `OPENAI_CODEX`), OpenCode (`OPENCODE`), Aider (`AIDER`), Cline (`CLINE`), Windsurf (`WINDSURF_AGENT`), GitHub Copilot (`GITHUB_COPILOT`), Amazon Q (`AMAZON_Q`, `AWS_Q_DEVELOPER`), Gemini Code Assist (`GEMINI_CODE_ASSIST`), Sourcegraph Cody (`SRC_CODY`), Generic Agent (`AGENT`)
150+
- Claude Code (`CLAUDECODE`, `CLAUDE_CODE`), Cursor (`CURSOR_AGENT`), Codex (`CODEX`, `OPENAI_CODEX`), OpenCode (`OPENCODE`), Aider (`AIDER`), Cline (`CLINE`), Windsurf (`WINDSURF_AGENT`), GitHub Copilot (`GITHUB_COPILOT`), Amazon Q (`AMAZON_Q`, `AWS_Q_DEVELOPER`), Gemini Code Assist (`GEMINI_CODE_ASSIST`), Sourcegraph Cody (`SRC_CODY`), pi.dev (`PI_CODING_AGENT`), Generic Agent (`AGENT`)
151151
- Manual override: `FORCE_AGENT_MODE=1` or `--agent` flag
152152

153153
**Agent Mode Behavior** (when detected):

docs/LLM_GUIDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Pup auto-detects AI coding agents and switches to **agent mode**, which changes
1010

1111
| Method | Example |
1212
|--------|---------|
13-
| Auto-detect | `CLAUDECODE=1`, `CLAUDE_CODE=1`, `CURSOR_AGENT=1`, `CODEX=1`, `OPENAI_CODEX=1`, `OPENCODE=1`, `AIDER=1`, `CLINE=1`, `WINDSURF_AGENT=1`, `GITHUB_COPILOT=1`, `AMAZON_Q=1`, `AWS_Q_DEVELOPER=1`, `GEMINI_CODE_ASSIST=1`, `SRC_CODY=1`, `AGENT=1` |
13+
| Auto-detect | `CLAUDECODE=1`, `CLAUDE_CODE=1`, `CURSOR_AGENT=1`, `CODEX=1`, `OPENAI_CODEX=1`, `OPENCODE=1`, `AIDER=1`, `CLINE=1`, `WINDSURF_AGENT=1`, `GITHUB_COPILOT=1`, `AMAZON_Q=1`, `AWS_Q_DEVELOPER=1`, `GEMINI_CODE_ASSIST=1`, `SRC_CODY=1`, `PI_CODING_AGENT=1`, `AGENT=1` |
1414
| Explicit flag | `pup --agent <command>` |
1515
| Environment override | `FORCE_AGENT_MODE=1` |
1616

src/useragent.rs

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ static AGENT_DETECTORS: &[AgentDetector] = &[
5757
name: "sourcegraph-cody",
5858
env_vars: &["SRC_CODY"],
5959
},
60+
AgentDetector {
61+
name: "pi-dev",
62+
env_vars: &["PI_CODING_AGENT"],
63+
},
6064
AgentDetector {
6165
name: "generic-agent",
6266
env_vars: &["AGENT"],
@@ -151,29 +155,36 @@ pub fn get_with_command(command: Option<&str>) -> String {
151155
#[cfg(test)]
152156
mod tests {
153157
use super::*;
158+
use crate::test_utils::ENV_LOCK;
159+
160+
fn clear_all_agent_vars() {
161+
for det in AGENT_DETECTORS {
162+
for var in det.env_vars {
163+
std::env::remove_var(var);
164+
}
165+
}
166+
std::env::remove_var("FORCE_AGENT_MODE");
167+
}
154168

155169
#[test]
156170
fn test_is_env_truthy() {
157-
// These tests use env vars that shouldn't be set in normal environments
171+
let _guard = ENV_LOCK.blocking_lock();
158172
std::env::set_var("__PUP_TEST_TRUE__", "true");
159173
assert!(is_env_truthy("__PUP_TEST_TRUE__"));
160-
161174
std::env::set_var("__PUP_TEST_ONE__", "1");
162175
assert!(is_env_truthy("__PUP_TEST_ONE__"));
163-
164176
std::env::set_var("__PUP_TEST_FALSE__", "false");
165177
assert!(!is_env_truthy("__PUP_TEST_FALSE__"));
166-
167178
assert!(!is_env_truthy("__PUP_TEST_NONEXISTENT__"));
168-
169-
// Clean up
170179
std::env::remove_var("__PUP_TEST_TRUE__");
171180
std::env::remove_var("__PUP_TEST_ONE__");
172181
std::env::remove_var("__PUP_TEST_FALSE__");
173182
}
174183

175184
#[test]
176185
fn test_user_agent_format() {
186+
let _guard = ENV_LOCK.blocking_lock();
187+
clear_all_agent_vars();
177188
let ua = get();
178189
assert!(ua.starts_with("pup/"));
179190
assert!(ua.contains("rust"));
@@ -184,6 +195,8 @@ mod tests {
184195

185196
#[test]
186197
fn test_user_agent_with_command() {
198+
let _guard = ENV_LOCK.blocking_lock();
199+
clear_all_agent_vars();
187200
let ua = get_with_command(Some("security-findings-analyze"));
188201
assert!(ua.starts_with("pup/"));
189202
assert!(ua.contains("; cmd security-findings-analyze)"));
@@ -196,6 +209,8 @@ mod tests {
196209
/// until the UA is fixed.
197210
#[test]
198211
fn test_user_agent_parses_for_smart_edge_telemetry() {
212+
let _guard = ENV_LOCK.blocking_lock();
213+
clear_all_agent_vars();
199214
for ua in [
200215
get_with_command(None),
201216
get_with_command(Some("monitors-list")),
@@ -228,6 +243,8 @@ mod tests {
228243

229244
#[test]
230245
fn test_user_agent_with_no_command() {
246+
let _guard = ENV_LOCK.blocking_lock();
247+
clear_all_agent_vars();
231248
let ua = get_with_command(None);
232249
assert!(!ua.contains("cmd "));
233250
assert_eq!(ua, get());
@@ -241,21 +258,17 @@ mod tests {
241258

242259
#[test]
243260
fn test_detect_agent_info_no_agent() {
244-
// Clear all agent env vars
245-
for det in AGENT_DETECTORS {
246-
for var in det.env_vars {
247-
std::env::remove_var(var);
248-
}
249-
}
250-
std::env::remove_var("FORCE_AGENT_MODE");
251-
261+
let _guard = ENV_LOCK.blocking_lock();
262+
clear_all_agent_vars();
252263
let info = detect_agent_info();
253264
assert!(!info.detected);
254265
assert!(info.name.is_empty());
255266
}
256267

257268
#[test]
258269
fn test_detect_agent_info_claude_code() {
270+
let _guard = ENV_LOCK.blocking_lock();
271+
clear_all_agent_vars();
259272
std::env::set_var("CLAUDE_CODE", "1");
260273
let info = detect_agent_info();
261274
assert!(info.detected);
@@ -265,9 +278,8 @@ mod tests {
265278

266279
#[test]
267280
fn test_detect_agent_info_cursor() {
268-
// Clear higher-priority detectors
269-
std::env::remove_var("CLAUDECODE");
270-
std::env::remove_var("CLAUDE_CODE");
281+
let _guard = ENV_LOCK.blocking_lock();
282+
clear_all_agent_vars();
271283
std::env::set_var("CURSOR_AGENT", "true");
272284
let info = detect_agent_info();
273285
assert!(info.detected);
@@ -277,32 +289,33 @@ mod tests {
277289

278290
#[test]
279291
fn test_is_agent_mode_force() {
292+
let _guard = ENV_LOCK.blocking_lock();
293+
clear_all_agent_vars();
280294
std::env::set_var("FORCE_AGENT_MODE", "1");
281295
assert!(is_agent_mode());
282296
std::env::remove_var("FORCE_AGENT_MODE");
283297
}
284298

285299
#[test]
286300
fn test_is_agent_mode_via_detector() {
287-
std::env::remove_var("FORCE_AGENT_MODE");
301+
let _guard = ENV_LOCK.blocking_lock();
302+
clear_all_agent_vars();
288303
std::env::set_var("CLAUDE_CODE", "true");
289304
assert!(is_agent_mode());
290305
std::env::remove_var("CLAUDE_CODE");
291306
}
292307

293308
#[test]
294309
fn test_is_agent_mode_false() {
295-
std::env::remove_var("FORCE_AGENT_MODE");
296-
for det in AGENT_DETECTORS {
297-
for var in det.env_vars {
298-
std::env::remove_var(var);
299-
}
300-
}
310+
let _guard = ENV_LOCK.blocking_lock();
311+
clear_all_agent_vars();
301312
assert!(!is_agent_mode());
302313
}
303314

304315
#[test]
305316
fn test_user_agent_with_detected_agent() {
317+
let _guard = ENV_LOCK.blocking_lock();
318+
clear_all_agent_vars();
306319
std::env::set_var("CLAUDE_CODE", "1");
307320
let ua = get();
308321
assert!(
@@ -314,11 +327,8 @@ mod tests {
314327

315328
#[test]
316329
fn test_user_agent_without_agent() {
317-
for det in AGENT_DETECTORS {
318-
for var in det.env_vars {
319-
std::env::remove_var(var);
320-
}
321-
}
330+
let _guard = ENV_LOCK.blocking_lock();
331+
clear_all_agent_vars();
322332
let ua = get();
323333
assert!(
324334
!ua.contains("ai-agent"),
@@ -327,13 +337,31 @@ mod tests {
327337
assert!(ua.ends_with(')'));
328338
}
329339

340+
#[test]
341+
fn test_detect_agent_info_pi_dev() {
342+
let _guard = ENV_LOCK.blocking_lock();
343+
clear_all_agent_vars();
344+
std::env::set_var("PI_CODING_AGENT", "true");
345+
let info = detect_agent_info();
346+
assert!(info.detected);
347+
assert_eq!(info.name, "pi-dev");
348+
std::env::remove_var("PI_CODING_AGENT");
349+
}
350+
351+
#[test]
352+
fn test_detect_agent_info_pi_dev_falsy() {
353+
let _guard = ENV_LOCK.blocking_lock();
354+
clear_all_agent_vars();
355+
std::env::set_var("PI_CODING_AGENT", "false");
356+
let info = detect_agent_info();
357+
assert!(!info.detected);
358+
std::env::remove_var("PI_CODING_AGENT");
359+
}
360+
330361
#[test]
331362
fn test_detect_agent_info_generic_agent() {
332-
for det in AGENT_DETECTORS {
333-
for var in det.env_vars {
334-
std::env::remove_var(var);
335-
}
336-
}
363+
let _guard = ENV_LOCK.blocking_lock();
364+
clear_all_agent_vars();
337365
std::env::set_var("AGENT", "1");
338366
let info = detect_agent_info();
339367
assert!(info.detected);

0 commit comments

Comments
 (0)