Skip to content
Merged
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
23 changes: 18 additions & 5 deletions cmd/ai_prompt_logger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ bazelisk build --config=release //cmd/ai_prompt_logger:ai-usage-agent-native-hos

Settings live in **`ai_usage_native_host.yaml`** (see **`ai_usage_native_host.yaml.example`**).

The example ships every setting **commented out** at its compiled-in default. A commented (or
missing) setting always resolves to that compiled default, so a future Agent upgrade that changes
a default takes effect on existing installs without needing to edit this file. A setting is only
active (uncommented) when it has been explicitly set — either by hand, or by the installer for
`trace_agent_url` when it differs from the default (see below).

The active file itself is **currently regenerated from the packaged template on every install and
upgrade**, so any hand edit to it — commented or not — is discarded by the next Agent upgrade. This
is a deliberate short-term measure: it forces already-installed machines onto the shipped defaults
while those defaults are still settling. Once they are stable (expected within a few Agent
releases), the installer will go back to preserving an existing `ai_usage_native_host.yaml` and
only creating it when missing.

- **Explicit file** (same idea as `agent run -c` / `system-probe --config`):

```bash
Expand All @@ -36,14 +49,14 @@ Settings live in **`ai_usage_native_host.yaml`** (see **`ai_usage_native_host.ya

- **No flags**: the binary looks for `ai_usage_native_host.yaml` under the packaged config location. On Windows, it first uses the Agent MSI `ConfigRoot` registry value, then falls back to `%ProgramData%\Datadog\ai_usage_native_host.yaml`; otherwise it searches under `{install_root}/etc/…` inferred from the executable path.

On a **packaged macOS** agent, Chrome is pointed at **`embedded/bin/run_ai_usage_native_host.sh`**, which **`exec`s** the binary with **`--config=$install_root/etc/ai_usage_native_host.yaml`**. On first config creation, the installer generates `trace_agent_url` from the Agent's `apm_config.receiver_port` in `datadog.yaml`.
On a **packaged macOS** agent, Chrome is pointed at **`embedded/bin/run_ai_usage_native_host.sh`**, which **`exec`s** the binary with **`--config=$install_root/etc/ai_usage_native_host.yaml`**. On every install/upgrade, the installer regenerates the whole file from the packaged template (see **Configuration** above) and activates `trace_agent_url` with the Agent's `apm_config.receiver_port` from `datadog.yaml` only when it differs from the default port `8126`; otherwise the line stays commented at the compiled default.

On a **packaged Windows** agent with End User Device Monitoring enabled, the **ai-usage** fleet installer extension points Chrome directly at the **`ai-usage-agent-native-host.exe`** shipped in the extension layer through a machine-wide HKLM Native Messaging Host registration; no shell wrapper is used.

EVP / Agent URL behaviour (defaults):

- URL: `{trace_agent_url}/evp_proxy/v{evp_proxy_api_version}/api/v2/aiusage` (defaults match trace receiver / EVP v2).
- Header: `X-Datadog-EVP-Subdomain: {ai_usage_evp_subdomain}` (default `softinv-intake`).
- Header: `X-Datadog-EVP-Subdomain: {ai_usage_evp_subdomain}` (default `eudm-intake`).
- The Agent injects `DD-API-KEY` and forwards to the dedicated AI usage intake for your site.

Ensure the Agent is listening on the trace port (default `127.0.0.1:8126`) with EVP proxy enabled.
Expand Down Expand Up @@ -126,7 +139,7 @@ On Windows, the native host is delivered by the `ai-usage` fleet installer exten
| `C:\ProgramData\Datadog\Installer\packages\datadog-agent\<version>\ext\eudm\ai-usage-agent-native-host.exe` | The host binary (signed), shipped in the extension layer and run in place. |
| `C:\ProgramData\Datadog\Installer\packages\datadog-agent\<version>\ext\eudm\com.datadoghq.ai_usage_agent.native_host.json` | Chrome Native Messaging Host manifest generated by the extension install hook. |
| `C:\ProgramData\Datadog\ai_usage_native_host.yaml.example` | Config example. |
| `C:\ProgramData\Datadog\ai_usage_native_host.yaml` | Active config generated by the extension install hook if missing. |
| `C:\ProgramData\Datadog\ai_usage_native_host.yaml` | Active config, currently regenerated from the packaged template by the extension install hook on every install and upgrade (see **Configuration** above). |

The extension install hook writes machine-wide registry keys so every Chrome user on the machine can discover the same native host:

Expand All @@ -135,11 +148,11 @@ HKLM\SOFTWARE\Google\Chrome\NativeMessagingHosts\com.datadoghq.ai_usage_agent.na
HKLM\SOFTWARE\WOW6432Node\Google\Chrome\NativeMessagingHosts\com.datadoghq.ai_usage_agent.native_host
```

The default value for both keys points to the manifest in the extension layer (the `ext\eudm` directory above). The manifest's `path` field points to the host executable in that same directory, and `allowed_origins` uses the installer default Chrome extension ID (`gkmbhgbippkmmmidcikijiblbagbjgjj`). The active config's `trace_agent_url` is generated from the Agent's `apm_config.receiver_port` in `datadog.yaml`.
The default value for both keys points to the manifest in the extension layer (the `ext\eudm` directory above). The manifest's `path` field points to the host executable in that same directory, and `allowed_origins` uses the installer default Chrome extension ID (`gkmbhgbippkmmmidcikijiblbagbjgjj`). The generated config's `trace_agent_url` is activated from the Agent's `apm_config.receiver_port` in `datadog.yaml` only when it differs from the default port `8126`; otherwise it stays commented at the compiled default.

The extension install hook registers a Task Scheduler logon task named `Datadog AI Usage Agent`. The task launches the same host executable from the extension layer with `--desktop-monitor --config "C:\ProgramData\Datadog\ai_usage_native_host.yaml"` in the interactive user session. Chrome native messaging registration remains separate and continues to launch the host without `--desktop-monitor`. When the `DatadogAgent` service is stopped, the desktop monitor remains resident but skips desktop scans until SCM reports the service running again.

The extension remove hook deletes the scheduled task and Chrome registry keys and stops the running host. The host binary and manifest live in the extension layer and are removed with the extension itself. It preserves the user-editable `C:\ProgramData\Datadog\ai_usage_native_host.yaml`.
The extension remove hook deletes the scheduled task and Chrome registry keys and stops the running host. The host binary and manifest live in the extension layer and are removed with the extension itself. It leaves `C:\ProgramData\Datadog\ai_usage_native_host.yaml` in place.

## macOS install (DMG)

Expand Down
19 changes: 13 additions & 6 deletions cmd/ai_prompt_logger/ai_usage_native_host.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,33 @@
# You can also point the host at this file explicitly (same idea as `agent run -c` /
# `system-probe --config`):
# ai-usage-agent-native-host --config=/path/to/ai_usage_native_host.yaml
#
# Settings below are shown commented out with their compiled-in default. A commented
# (or missing) setting always resolves to that compiled default, so a future Agent
# upgrade that changes a default takes effect without needing to touch this file.
# Uncomment and edit a line to override it.

# Base URL of the Agent trace receiver (same role as DD_TRACE_AGENT_URL).
# Use http:// only (local receiver is plain HTTP; this host has no TLS client).
trace_agent_url: "http://127.0.0.1:8126"
# Default: "http://127.0.0.1:8126". The installer renders this line active automatically
# when the Agent's apm_config.receiver_port in datadog.yaml differs from the default.
# trace_agent_url: "http://127.0.0.1:8126"

# EVP proxy API version (same role as DD_EVP_PROXY_API_VERSION).
evp_proxy_api_version: 2
# evp_proxy_api_version: 2

# AI usage EVP intake subdomain header.
ai_usage_evp_subdomain: "softinv-intake"
# ai_usage_evp_subdomain: "eudm-intake"

# Standalone desktop monitoring settings. These are used only when the binary is
# launched with --desktop-monitor; Chrome native messaging mode ignores them.
desktop_monitoring:
enabled: true
poll_interval_seconds: 60
# enabled: false
# poll_interval_seconds: 60
# Hosted AI CLIs are counted only after their process-level read/write
# counters advance since the previous poll. The first poll initializes the
# baseline and does not emit AI-usage event.
process_activity_window_seconds: 600
# process_activity_window_seconds: 600

# The built-in AI tool and host process lookup tables are compiled into the
# desktop monitor so software upgrades can update them without rewriting this
Expand Down
118 changes: 116 additions & 2 deletions cmd/ai_prompt_logger/src/datadog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::desktop::config;
pub use crate::desktop::config::DesktopMonitoringConfig;

const CONFIG_BASENAME: &str = "ai_usage_native_host.yaml";
const AI_USAGE_EVP_SUBDOMAIN: &str = "softinv-intake";
const AI_USAGE_EVP_SUBDOMAIN: &str = "eudm-intake";
const AI_USAGE_EVP_PATH: &str = "/api/v2/aiusage";

/// Cap for connect + full request so the native host thread cannot block indefinitely
Expand Down Expand Up @@ -74,7 +74,7 @@ impl DatadogClient {
/// - `trace_agent_url` (default `http://127.0.0.1:8126`; use **http** only — the local trace
/// receiver is plain HTTP and this binary has no TLS client)
/// - `evp_proxy_api_version` (default `2`)
/// - `ai_usage_evp_subdomain` (default `softinv-intake`)
/// - `ai_usage_evp_subdomain` (default `eudm-intake`)
///
/// No `DD_API_KEY` is required here; the Agent injects the key when forwarding.
pub fn load(config_path: Option<PathBuf>) -> Self {
Expand Down Expand Up @@ -340,4 +340,118 @@ mod tests {
assert!(!body.contains_key("status"));
assert!(!body.contains_key("date"));
}

fn defaults() -> (String, u32, String) {
(
"http://127.0.0.1:8126".to_string(),
2,
AI_USAGE_EVP_SUBDOMAIN.to_string(),
)
}

#[test]
fn apply_yaml_empty_contents_keeps_compiled_defaults() {
let (mut agent_base, mut proxy_version, mut evp_subdomain) = defaults();

DatadogClient::apply_yaml("", &mut agent_base, &mut proxy_version, &mut evp_subdomain);

assert_eq!(agent_base, "http://127.0.0.1:8126");
assert_eq!(proxy_version, 2);
assert_eq!(evp_subdomain, AI_USAGE_EVP_SUBDOMAIN);
}

#[test]
fn apply_yaml_fully_commented_keeps_compiled_defaults() {
let (mut agent_base, mut proxy_version, mut evp_subdomain) = defaults();
let contents = r#"
# trace_agent_url: "http://127.0.0.1:8126"
# evp_proxy_api_version: 2
# ai_usage_evp_subdomain: "eudm-intake"
"#;

DatadogClient::apply_yaml(
contents,
&mut agent_base,
&mut proxy_version,
&mut evp_subdomain,
);

assert_eq!(agent_base, "http://127.0.0.1:8126");
assert_eq!(proxy_version, 2);
assert_eq!(evp_subdomain, AI_USAGE_EVP_SUBDOMAIN);
}

#[test]
fn apply_yaml_partial_override_only_changes_present_key() {
let (mut agent_base, mut proxy_version, mut evp_subdomain) = defaults();

DatadogClient::apply_yaml(
"evp_proxy_api_version: 3\n",
&mut agent_base,
&mut proxy_version,
&mut evp_subdomain,
);

assert_eq!(
agent_base, "http://127.0.0.1:8126",
"unset key stays default"
);
assert_eq!(proxy_version, 3, "present key overrides");
assert_eq!(
evp_subdomain, AI_USAGE_EVP_SUBDOMAIN,
"unset key stays default"
);
}

#[test]
fn apply_yaml_full_override_changes_all_settings() {
let (mut agent_base, mut proxy_version, mut evp_subdomain) = defaults();
let contents = r#"
trace_agent_url: "http://127.0.0.1:9999"
evp_proxy_api_version: 3
ai_usage_evp_subdomain: "custom-intake"
"#;

DatadogClient::apply_yaml(
contents,
&mut agent_base,
&mut proxy_version,
&mut evp_subdomain,
);

assert_eq!(agent_base, "http://127.0.0.1:9999");
assert_eq!(proxy_version, 3);
assert_eq!(evp_subdomain, "custom-intake");
}

#[test]
fn load_with_missing_config_path_uses_compiled_defaults() {
let client = DatadogClient::load(Some(PathBuf::from(
"/nonexistent/ai_usage_native_host.yaml",
)));

assert_eq!(
client.intake_url,
"http://127.0.0.1:8126/evp_proxy/v2/api/v2/aiusage"
);
assert_eq!(client.evp_subdomain, AI_USAGE_EVP_SUBDOMAIN);
}

#[test]
fn load_from_file_with_partial_override_merges_with_defaults() {
let path = std::env::temp_dir().join(format!(
"ai_usage_native_host_partial_load_{}.yaml",
std::process::id()
));
std::fs::write(&path, "evp_proxy_api_version: 5\n").expect("should write temp config");

let client = DatadogClient::load(Some(path.clone()));
let _ = std::fs::remove_file(&path);

assert_eq!(
client.intake_url,
"http://127.0.0.1:8126/evp_proxy/v5/api/v2/aiusage"
);
assert_eq!(client.evp_subdomain, AI_USAGE_EVP_SUBDOMAIN);
}
}
6 changes: 3 additions & 3 deletions cmd/ai_prompt_logger/src/desktop/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl Default for DesktopMonitoringConfig {
}

fn default_desktop_monitoring_enabled() -> bool {
true
false
}

fn default_desktop_monitoring_poll_interval_seconds() -> u64 {
Expand Down Expand Up @@ -676,7 +676,7 @@ desktop_monitoring:
let cfg = load_desktop_monitoring_config(Some(path.clone()), || None);
let _ = std::fs::remove_file(path);

assert!(cfg.enabled);
assert!(!cfg.enabled);
assert_eq!(cfg.debug, 0);
assert_eq!(cfg.poll_interval_seconds, 60);
assert!(
Expand Down Expand Up @@ -717,7 +717,7 @@ desktop_monitoring:
let fallback_cfg = load_desktop_monitoring_config(Some(path.clone()), || None);
let _ = std::fs::remove_file(path);

assert!(fallback_cfg.enabled);
assert!(!fallback_cfg.enabled);
assert_eq!(fallback_cfg.debug, 2);
assert_eq!(fallback_cfg.poll_interval_seconds, 7);
}
Expand Down
4 changes: 0 additions & 4 deletions cmd/ai_prompt_logger/src/desktop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,6 @@ pub(crate) fn log_startup_warning(message: impl AsRef<str>) {
/// Run the desktop monitor polling loop and submit detected AI usage events.
pub fn run(dd_client: &DatadogClient, mut config: DesktopMonitoringConfig) -> Result<()> {
let mut logger = DesktopLogger::new(config.debug);
if !config.enabled {
logger.info("desktop monitoring disabled by config");
return Ok(());
}

#[cfg(any(windows, target_os = "macos"))]
{
Expand Down
9 changes: 7 additions & 2 deletions omnibus/package-scripts/agent-dmg/postinst
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ if [ ! -e "$CONF_DIR/datadog.yaml" ]; then
sed -E 's/^api_key:$/api_key: APIKEY/' $CONF_DIR/datadog.yaml.example > $CONF_DIR/datadog.yaml
fi

if [ ! -f "$CONF_DIR/ai_usage_native_host.yaml" ] && [ -f "$CONF_DIR/ai_usage_native_host.yaml.example" ]; then
# Always regenerate from the packaged template (even on upgrade) so default changes
# (e.g. EVP track, desktop monitoring defaults) reach already-installed machines.
if [ -f "$CONF_DIR/ai_usage_native_host.yaml.example" ]; then
cp "$CONF_DIR/ai_usage_native_host.yaml.example" "$CONF_DIR/ai_usage_native_host.yaml"
apm_receiver_port=$(awk '
/^[[:space:]]*#/ { next }
Expand All @@ -120,7 +122,10 @@ if [ ! -f "$CONF_DIR/ai_usage_native_host.yaml" ] && [ -f "$CONF_DIR/ai_usage_na
}
}
' "$CONF_DIR/datadog.yaml" 2>/dev/null || true)
if [ -n "$apm_receiver_port" ]; then
# Only activate the line when the port differs from the compiled-in default (8126); when it
# matches, leave the line as shipped (commented) so a future default change applies without
# needing another overwrite of this file.
if [ -n "$apm_receiver_port" ] && [ "$apm_receiver_port" != "8126" ]; then
sed -i '' "s#^[ #]*trace_agent_url:.*#trace_agent_url: \"http://127.0.0.1:$apm_receiver_port\"#" "$CONF_DIR/ai_usage_native_host.yaml"
fi
fi
Expand Down
1 change: 1 addition & 0 deletions pkg/fleet/installer/packages/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ go_test(
"@in_gopkg_yaml_v3//:yaml_v3",
] + select({
"@rules_go//go/platform:windows": [
"//pkg/fleet/installer/paths",
"//pkg/fleet/installer/setup/config",
"@in_yaml_go_yaml_v3//:yaml",
],
Expand Down
36 changes: 21 additions & 15 deletions pkg/fleet/installer/packages/datadog_agent_eudm_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,11 @@ func postInstallEUDMExtension(ctx HookContext) error {
removeAIUsageScheduledTask(ctx.Context)
}()

// 1) Generate ai_usage_native_host.yaml in ProgramData (best effort; preserve an existing file),
// then grant Everyone read/execute on it. C:\ProgramData\Datadog is ACL-restricted, so the
// config would otherwise be unreadable by the interactive browser user that Chrome and the
// desktop-monitor task launch the host as.
// 1) Generate ai_usage_native_host.yaml in ProgramData (best effort; always overwritten from
// the packaged template so default changes reach already-installed machines), then grant
// Everyone read/execute on it. C:\ProgramData\Datadog is ACL-restricted, so the config would
// otherwise be unreadable by the interactive browser user that Chrome and the desktop-monitor
// task launch the host as.
configPath := filepath.Join(paths.DatadogDataDir, aiUsageConfigName)
examplePath := filepath.Join(extensionPath, aiUsageConfigName+".example")
if err := writeAIUsageConfig(examplePath, configPath); err != nil {
Expand Down Expand Up @@ -203,22 +204,27 @@ func preRemoveEUDMExtension(ctx HookContext) error {
return nil
}

// writeAIUsageConfig renders ai_usage_native_host.yaml from the example template, substituting
// trace_agent_url with the local trace receiver URL. An existing config is preserved.
// writeAIUsageConfig renders ai_usage_native_host.yaml from the example template. This always
// overwrites an existing config (on every install and upgrade) so packaged default changes reach
// already-installed machines; any manual edits to the generated file do not survive the next
// agent upgrade.
//
// The example template ships trace_agent_url commented out at its compiled-in default
// (http://127.0.0.1:8126). trace_agent_url is only substituted and activated when the Agent's
// apm_config.receiver_port differs from that default; otherwise the line is left as shipped
// (commented) so a future default change takes effect without needing another overwrite.
func writeAIUsageConfig(examplePath, configPath string) error {
if _, err := os.Stat(configPath); err == nil {
log.Debugf("AI Usage: %s already exists, not modifying it", configPath)
return nil
}
example, err := os.ReadFile(examplePath)
if err != nil {
return fmt.Errorf("could not read example config %s: %w", examplePath, err)
}
port := readAIUsageReceiverPort()
rendered := aiUsageTraceAgentURLRe.ReplaceAllString(
string(example),
fmt.Sprintf(`trace_agent_url: "http://127.0.0.1:%d"`, port),
)
rendered := string(example)
Comment thread
guohdd marked this conversation as resolved.
if port := readAIUsageReceiverPort(); port != aiUsageDefaultReceiverPort {
rendered = aiUsageTraceAgentURLRe.ReplaceAllString(
rendered,
fmt.Sprintf(`trace_agent_url: "http://127.0.0.1:%d"`, port),
)
}
if err := os.WriteFile(configPath, []byte(rendered), 0o644); err != nil {
return fmt.Errorf("could not write %s: %w", configPath, err)
}
Expand Down
Loading
Loading