@@ -586,21 +586,21 @@ func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRep
586586 }
587587 }
588588
589- // Trace identity (ADR 0050 Level 1 + security correlation). Generated here,
589+ // Trace identity (ADR 0050 Level 1 + security correlation). Resolved here,
590590 // before the pre-script, so TRACEPARENT can propagate to child processes.
591- // The same id is reused as the security finding/audit trace id (dashed UUID)
592- // and, dash-stripped, as the W3C telemetry trace id — one id across both.
593- securityTraceID := security . GenerateTraceID ()
594- wTraceID := telemetry . TraceIDFromUUID ( securityTraceID )
595- rootSpanID := telemetry . NewSpanID ( )
591+ // An inbound TRACEPARENT (nested/instrumented invocation, issue #2779) is
592+ // adopted so the run continues the parent trace; otherwise a fresh id is
593+ // generated. Either way one id serves as both the security finding/audit
594+ // trace id (dashed UUID) and, dash-stripped, the W3C telemetry trace id.
595+ securityTraceID , traceCtx := resolveTraceIdentity ( os . Getenv ( "TRACEPARENT" ) )
596596 workItemID := resolveWorkItemID ()
597597
598598 // 2c. Run pre-script on the host (if configured).
599599 if h .PreScript != "" {
600600 preStart := time .Now ()
601601 printer .StepStart ("Running pre-script: " + h .PreScript )
602602 preCmd := exec .Command (h .PreScript )
603- preCmd .Env = childScriptEnv (h .RunnerEnv , telemetry .TraceParent ( wTraceID , rootSpanID ))
603+ preCmd .Env = childScriptEnv (h .RunnerEnv , telemetry .TraceParentWithFlags ( traceCtx . TraceID , traceCtx . RootSpanID , traceCtx . Flags ))
604604 preCmd .Stdout = os .Stdout
605605 preCmd .Stderr = os .Stderr
606606 if err := preCmd .Run (); err != nil {
@@ -626,7 +626,7 @@ func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRep
626626 // order — it runs last and the summary captures the whole run.
627627 var lastExitCode int
628628 var transcriptErrorOverride bool
629- rec := telemetry .New (runDir , wTraceID , rootSpanID , agentName , workItemID , runStart )
629+ rec := telemetry .New (runDir , traceCtx , agentName , workItemID , runStart )
630630 defer func () { rec .Finalize (telemetryExitCode (lastExitCode , runErr )) }()
631631
632632 createStart := time .Now ()
@@ -675,7 +675,7 @@ func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRep
675675 printer .StepStart ("Running post-script: " + h .PostScript )
676676 postCmd := exec .Command (h .PostScript )
677677 postCmd .Dir = runDir
678- postCmd .Env = childScriptEnv (h .RunnerEnv , telemetry .TraceParent ( wTraceID , rootSpanID ))
678+ postCmd .Env = childScriptEnv (h .RunnerEnv , telemetry .TraceParentWithFlags ( traceCtx . TraceID , traceCtx . RootSpanID , traceCtx . Flags ))
679679 postCmd .Stdout = os .Stdout
680680 postCmd .Stderr = os .Stderr
681681 if err := postCmd .Run (); err != nil {
@@ -1681,12 +1681,46 @@ func telemetryExitCode(lastExitCode int, runErr error) int {
16811681 return lastExitCode
16821682}
16831683
1684+ // resolveTraceIdentity resolves the run's trace identity (issue #2779). A
1685+ // valid inbound W3C TRACEPARENT is adopted: its trace-id becomes both the
1686+ // security trace id (re-dashed UUID) and the W3C telemetry trace-id, its
1687+ // span-id becomes the root span's remote parent, and its trace-flags carry
1688+ // the upstream sampling decision forward. Without a valid inbound value a
1689+ // fresh identity is generated, exactly as Level 1 always did. TRACESTATE is
1690+ // intentionally untouched — it passes through os.Environ to child scripts.
1691+ func resolveTraceIdentity (inbound string ) (securityTraceID string , tc telemetry.TraceContext ) {
1692+ if traceID , parentSpanID , flags , ok := telemetry .ParseTraceParent (inbound ); ok {
1693+ return telemetry .UUIDFromTraceID (traceID ), telemetry.TraceContext {
1694+ TraceID : traceID ,
1695+ RootSpanID : telemetry .NewSpanID (),
1696+ ParentSpanID : parentSpanID ,
1697+ Flags : flags ,
1698+ }
1699+ }
1700+ securityTraceID = security .GenerateTraceID ()
1701+ return securityTraceID , telemetry.TraceContext {
1702+ TraceID : telemetry .TraceIDFromUUID (securityTraceID ),
1703+ RootSpanID : telemetry .NewSpanID (),
1704+ Flags : "01" ,
1705+ }
1706+ }
1707+
16841708// childScriptEnv builds the environment for a host-side child script (pre- or
16851709// post-script): the harness RunnerEnv layered over the process environment,
1686- // plus the W3C TRACEPARENT for trace propagation (ADR 0050 Level 1). An empty
1687- // traceparent (telemetry disabled) is omitted rather than emitted blank.
1710+ // plus the W3C TRACEPARENT for trace propagation (ADR 0050 Level 1). Any
1711+ // TRACEPARENT already present — inherited from the process environment or
1712+ // set in runner_env — is filtered out first: env lookups resolve the first
1713+ // match, so a stale value would shadow fullsend's own, and fullsend's trace
1714+ // identity never derives from runner_env (issue #2779). An empty traceparent
1715+ // (telemetry disabled) is omitted rather than emitted blank.
16881716func childScriptEnv (runnerEnv map [string ]string , traceparent string ) []string {
1689- env := append (os .Environ (), envToList (runnerEnv )... )
1717+ merged := append (os .Environ (), envToList (runnerEnv )... )
1718+ env := make ([]string , 0 , len (merged )+ 1 )
1719+ for _ , e := range merged {
1720+ if ! strings .HasPrefix (e , "TRACEPARENT=" ) {
1721+ env = append (env , e )
1722+ }
1723+ }
16901724 if traceparent != "" {
16911725 env = append (env , "TRACEPARENT=" + traceparent )
16921726 }
@@ -1857,9 +1891,11 @@ func refreshOIDCToken(ctx context.Context, sandboxName, oidcURL, oidcAuth string
18571891// inside the sandbox. It finds known context files (including SKILL.md in
18581892// skill directories) in the repo directory and passes them as arguments.
18591893func buildScanContextCommand (repoDir , traceID string ) string {
1860- // Defense-in-depth: validate traceID before shell interpolation even though
1861- // GenerateTraceID() only produces safe hex characters.
1862- if ! security .IsValidTraceID (traceID ) {
1894+ // Defense-in-depth: validate traceID before shell interpolation. Uses
1895+ // IsShellSafeTraceID (not IsValidTraceID) because the id may have been
1896+ // adopted from an inbound W3C traceparent (issue #2779), so it is not
1897+ // necessarily UUID v4.
1898+ if ! security .IsShellSafeTraceID (traceID ) {
18631899 // Should never happen with internal generation, but fail safely.
18641900 traceID = "invalid-trace-id"
18651901 }
@@ -2196,10 +2232,10 @@ func scanOutputFiles(outputDir, traceID string, printer *ui.Printer) error {
21962232
21972233// injectTraceID appends the FULLSEND_TRACE_ID to the sandbox .env file.
21982234func injectTraceID (sandboxName , traceID string ) error {
2199- if ! security .IsValidTraceID (traceID ) {
2235+ if ! security .IsShellSafeTraceID (traceID ) {
22002236 return fmt .Errorf ("invalid trace ID format: %q" , traceID )
22012237 }
2202- // Safe: IsValidTraceID () above ensures traceID matches UUID v4 format only .
2238+ // Safe: IsShellSafeTraceID () above ensures traceID is only hex and dashes .
22032239 cmd := fmt .Sprintf ("echo 'export FULLSEND_TRACE_ID=%s' >> %s/.env" , traceID , sandbox .SandboxWorkspace )
22042240 _ , _ , _ , err := sandbox .Exec (sandboxName , cmd , 10 * time .Second )
22052241 return err
0 commit comments