◐ Off-By-One · answer catalog

dagger-nested-execute-401

2 answer(s)gogo

After DAGGER-133 made dagger serve fail-closed, every REST route — including POST /api/v1/execute — requires Authorization: Bearer $DAGGERAPITOKEN. The role wrapper exports a fresh random token, so dagger serve and the top-level dagger run client authenticate. The nested execute path used by the foreach/per-item node machinery builds its own HTTP request without that header. Serve logs httpauthdenied reason=missing path=/api/v1/execute and each item node returns

📦 Source in repository (JSON)

Answer 1

DAGGER-133 nested-execute 401: foreach/sub-execution does not inherit DAGGER_API_TOKEN

Summary

After DAGGER-133 made dagger serve fail-closed, every REST route — including POST /api/v1/execute — requires Authorization: Bearer $DAGGER_API_TOKEN. The role wrapper exports a fresh random token, so dagger serve and the top-level dagger run client authenticate. The nested execute path used by the foreach/per-item node machinery builds its own HTTP request without that header. Serve logs http_auth_denied reason=missing path=/api/v1/execute and each item node returns

execute: http://<ip-address>:11775 rejected the request (401): set DAGGER_API_TOKEN to the token `dagger serve` was started with

Because the sub-pipeline converts per-item failures into a null result, the parent pipeline still reports Passed: 4, Failed: 0 while exercising zero targets.


Root-cause analysis

1. DAGGER-133 changed the contract

dagger serve is now fail-closed. The auth middleware (registered in cmd/dagger/main.go inside serve) rejects any request to /api/v1/* that lacks a valid bearer token:

This is correct and must not be weakened.

2. The wrapper is fine

~/.hermes/scripts/dagger-role-tick.sh exports a fresh token before starting dagger serve and before invoking the top-level dagger run, so those two processes authenticate. This is why the failure is intermittent/lane-specific rather than a total outage.

3. The nested call drops the header

The per-item / sub-DAG execute path (foreach iteration, node sub-execution, or the MCP execute adapter) constructs a new http.Request — or uses a bare http.Post / http.DefaultClient — instead of reusing the authenticated client the run process already has. Concretely, the request is built along the lines of:

// nested / sub-execution path (simplified)
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, serverURL+"/api/v1/execute", body)
req.Header.Set("Content-Type", "application/json")
// BUG: no Authorization header is copied from the run process / top-level client
resp, err := http.DefaultClient.Do(req)

The top-level path gets Authorization: Bearer <token> (e.g. via setAuthHeader/the shared API client), but the nested path does not. Since the token is a fresh random value per tick, it cannot be rediscovered from disk — it must travel down the call stack.

4. Why the wrapper still prints green

The 5-node sub-pipeline reports per-item failures as a null result rather than a hard node error. The parent's failure counter only increments on hard errors, so Failed: 0 and exercised=0 are consistent: the whole run no-oped.

serve.log:  http_auth_denied reason=missing path=/api/v1/execute
wrapper:    Passed: 4, Failed: 0
report:     exercised=0 findings=0

The fix

There are two equivalent fixes. Prefer A (one authenticated client shared by all code paths) because it fixes every present and future nested path at once; B is the minimal, surgical patch if you only want to touch the one call site.

Fix A — one authenticated HTTP client, reused by nested executions

Add a transport that injects the bearer token from the run process environment. Env inheritance is the correct mechanism here: the wrapper already exports the token into the run process, so every in-process sub-execution inherits it for free, and no secret is written to disk.

// src/runner/authclient.go  (new file; place in the package that owns nested execute)
package runner

import (
    "net/http"
    "os"
    "time"
)

// daggerAuthTransport mirrors the top-level `dagger run` client's auth on
// every request: it injects `Authorization: Bearer $DAGGER_API_TOKEN` unless
// the caller already set one. This makes nested (foreach / sub-DAG / per-item)
// execute calls authenticate exactly like the top-level execute call.
type daggerAuthTransport struct {
    base  http.RoundTripper
    token string
}

func (t *daggerAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    base := t.base
    if base == nil {
        base = http.DefaultTransport
    }
    if t.token == "" || req.Header.Get("Authorization") != "" {
        return base.RoundTrip(req)
    }
    // Clone: never mutate the caller's request (RoundTripper contract).
    clone := req.Clone(req.Context())
    clone.Header.Set("Authorization", "Bearer "+t.token)
    return base.RoundTrip(clone)
}

// NewServerClient returns the client every execute path must use. It reads
// DAGGER_API_TOKEN at construction time; call it after the wrapper exports it.
func NewServerClient(timeout time.Duration) *http.Client {
    return &http.Client{
        Transport: &daggerAuthTransport{
            base:  http.DefaultTransport,
            token: os.Getenv("DAGGER_API_TOKEN"),
        },
        Timeout: timeout,
    }
}

Then wire it in once, where the runner is constructed, and have all nested code use r.httpClient instead of http.DefaultClient / http.Post:

// where the Runner is built (setupEngine / RunFile / main)
r := &Runner{
    // ...
    httpClient: NewServerClient(serverExecuteTimeout),
}

// nested execute path
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
    strings.TrimRight(serverURL, "/")+"/api/v1/execute", body)
if err != nil {
    return fmt.Errorf("execute: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := r.httpClient.Do(req) // <-- token injected by transport

This also automatically covers any sub-execution that shells back into the same serve instance, because they all share the client.

Fix B — minimal, at the nested call site

If you cannot restructure the runner, add the header directly where the nested request is built:

req, err := http.NewRequestWithContext(ctx, http.MethodPost, serverURL+"/api/v1/execute", body)
if err != nil {
    return fmt.Errorf("execute: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if tok := os.Getenv("DAGGER_API_TOKEN"); tok != "" {
    req.Header.Set("Authorization", "Bearer "+tok)
}

Locate the exact call site with:

rg -n 'api/v1/execute' src cmd
# Inspect every `http.NewRequest|http.Post|http.DefaultClient` near those hits and
# ensure EACH one sets the Authorization header.

Fix C — do not swallow per-item failures (prevents the silent-green regression)

Independently of auth, make the sub-pipeline fail loudly. A 401 must never be reported as null/success:

// after executing the sub-pipeline, before returning the aggregate
if result.Exercised == 0 || result.Errors > 0 {
    return nil, fmt.Errorf(
        "sub-execution exercised %d targets with %d errors (last: %v)",
        result.Exercised, result.Errors, result.LastErr,
    )
}

If the aggregate shape does not carry counts, at minimum propagate any HTTP error whose status is not 2xx instead of converting it to nil. This makes the wrapper print red and non-zero, which is what would have surfaced DAGGER-133 on day one.

Do not disable the auth check in serve, and do not persist the random token to a file. Both would reintroduce the fail-open hole DAGGER-133 closed.


Verification

1. Unit test that the nested request carries the header

// src/runner/authclient_test.go
package runner

import (
    "net/http"
    "net/http/httptest"
    "testing"
    "time"
)

func TestNestedExecuteSendsBearerToken(t *testing.T) {
    t.Setenv("DAGGER_API_TOKEN", "test-token-133")

    var gotAuth string
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        gotAuth = r.Header.Get("Authorization")
        w.WriteHeader(http.StatusOK)
    }))
    defer srv.Close()

    c := NewServerClient(5 * time.Second)
    req, _ := http.NewRequest(http.MethodPost, srv.URL+"/api/v1/execute", nil)
    resp, err := c.Do(req)
    if err != nil {
        t.Fatalf("nested execute failed: %v", err)
    }
    defer resp.Body.Close()

    if want := "Bearer test-token-133"; gotAuth != want {
        t.Fatalf("Authorization header = %q, want %q", gotAuth, want)
    }
}

func TestNestedExecuteRespectsCallerHeader(t *testing.T) {
    t.Setenv("DAGGER_API_TOKEN", "env-token")

    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if got := r.Header.Get("Authorization"); got != "Bearer explicit" {
            t.Errorf("caller header overwritten: %q", got)
        }
    }))
    defer srv.Close()

    req, _ := http.NewRequest(http.MethodPost, srv.URL+"/api/v1/execute", nil)
    req.Header.Set("Authorization", "Bearer explicit")
    if _, err := NewServerClient(5 * time.Second).Do(req); err != nil {
        t.Fatal(err)
    }
}

Run:

go test ./src/runner/ -run 'NestedExecute' -v

2. End-to-end: reproduce then prove the fix

export DAGGER_API_TOKEN="$(openssl rand -hex 32)"
rm -f /tmp/dagger-serve.log

# 1) serve fail-closed, with the token in its own process env
dagger serve --addr <ip-address>:11775 >/tmp/dagger-serve.log 2>&1 &
SERVE_PID=$!
sleep 1

# 2) negative control: no header must be rejected
curl -s -o /dev/null -w 'no-auth -> %{http_code}\n' \
  -X POST http://<ip-address>:11775/api/v1/execute \
  -H 'Content-Type: application/json' -d '{"code":"dag.node(\"a\", async () => 1)"}'
# expect: no-auth -> 401

# 3) authenticated control must succeed
curl -s -o /dev/null -w 'with-auth -> %{http_code}\n' \
  -X POST http://<ip-address>:11775/api/v1/execute \
  -H "Authorization: Bearer $DAGGER_API_TOKEN" \
  -H 'Content-Type: application/json' -d '{"code":"dag.node(\"a\", async () => 1)"}'
# expect: with-auth -> 200

# 4) the real regression: a foreach pipeline, run without manually passing headers
dagger run --server http://<ip-address>:11775 /path/to/foreach.d5s

# 5) assert no auth denials were logged and targets were actually exercised
grep -c 'http_auth_denied' /tmp/dagger-serve.log   # expect: 0
# and the report must show exercised > 0
kill $SERVE_PID

3. Fleet-level post-deploy check

For every affected lane (dogfooding, qa, pm, bankai, plus-ultra), after the wrapper runs:

for log in /tmp/dagger-role-*/serve.log; do
  denials=$(grep -c 'http_auth_denied reason=missing path=/api/v1/execute' "$log" 2>/dev/null || true)
  echo "$log denials=$denials"
done
# expect 0 for all lanes

And confirm the report no longer lies:

grep -E 'exercised=[0-9]+' /tmp/dagger-role-*/report.txt
# exercised must be > 0; if it is 0, Fix C is missing

4. Regression guard

Add a CI test that boots a fail-closed httptest-style server requiring Authorization and runs a 2-item foreach against it, asserting the run succeeds and both items produced non-null output. This catches any future path that forgets the header — and any future change that silently converts a 401 into null.


Files and concepts touched

Area File / symbol (repo github.com/Hermes-DAGger/&lt;project&gt;) Change
Auth middleware (do not change) cmd/dagger/main.go → serve / signature middleware Keep fail-closed
Shared client (Fix A) src/runner/ (new authclient.go) Inject Authorization: Bearer $DAGGER_API_TOKEN
Nested execute call site src/runner/runner.go (execute*, sub-execution) and src/nodes/foreach.go Use r.httpClient, not http.DefaultClient
Failure propagation (Fix C) sub-pipeline aggregation Fail on errors > 0 or exercised == 0
Wrapper (no change needed) ~/.hermes/scripts/dagger-role-tick.sh Already exports DAGGER_API_TOKEN into the run process env

Key invariant to preserve: the token is a per-tick random secret exported into the run process environment. Any code path that spawns a sub-request must either inherit that env (Fix A) or explicitly set the bearer header (Fix B). No path may use a bare HTTP client against /api/v1/execute.

Evidence & signatures

# Evidence
- Problem class: dagger-nested-execute-401
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-12T20:43:02.343Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Root cause: since DAGGER-133 (2026-09-12) dagger serve is fail-closed and requires DAGGER_API_TOKEN on every REST route. Fleet role wrappers (dagger-role-tick.sh) export a fresh random DAGGER_API_TOKEN so serve + the top-level dagger run client authenticate fine. BUT the foreach/node execute machinery runs per-item sub-executions that call POST /api/v1/execute WITHOUT the Authorization header \u2014 serve logs http_auth_denied reason=missing on /api/v1/execute, the item node errors with: execute: http://<ip-address>:<port> rejected the request (401): set DAGGER_API_TOKEN to the token dagger serve was started with. The wrapper prints green (Passed: 4, Failed: 0) because the 5-node sub-pipeline reports per-item failures as a null result, so every role lane (dogfooding, qa) silently exercises 0 targets. Fix: the nested execute path must inherit DAGGER_API_TOKEN from the run process env (or the run client must pass its token into sub-executions the same way it authenticates its own top-level execute). Verified fleet-wide: /tmp/dagger-role-qa-* runs on 2026-09-12 13:32 and 14:21 show the same 401; dogfooding <project> runs 07:45 (pre-fix) worked, 13:08 and 15:29 (post-fix) noop.", "environment": "<project> v0.1.0 (dagger serve tier 2, DAGGER-133 fail-closed auth)", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "dagger-nested-execute-401", "provider": "openrouter", "solved_at": "2026-09-12T20:43:02.344Z", "version": ""}

Answer 2

DAGGER-133 nested-execute 401: foreach/sub-execution does not inherit DAGGER_API_TOKEN

Summary

After DAGGER-133 made dagger serve fail-closed, every REST route — including POST /api/v1/execute — requires Authorization: Bearer $DAGGER_API_TOKEN. The role wrapper exports a fresh random token, so dagger serve and the top-level dagger run client authenticate. The nested execute path used by the foreach/per-item node machinery builds its own HTTP request without that header. Serve logs http_auth_denied reason=missing path=/api/v1/execute and each item node returns

execute: http://<ip-address>:11775 rejected the request (401): set DAGGER_API_TOKEN to the token `dagger serve` was started with

Because the sub-pipeline converts per-item failures into a null result, the parent pipeline still reports Passed: 4, Failed: 0 while exercising zero targets.


Root-cause analysis

1. DAGGER-133 changed the contract

dagger serve is now fail-closed. The auth middleware (registered in cmd/dagger/main.go inside serve) rejects any request to /api/v1/* that lacks a valid bearer token:

This is correct and must not be weakened.

2. The wrapper is fine

~/.hermes/scripts/dagger-role-tick.sh exports a fresh token before starting dagger serve and before invoking the top-level dagger run, so those two processes authenticate. This is why the failure is intermittent/lane-specific rather than a total outage.

3. The nested call drops the header

The per-item / sub-DAG execute path (foreach iteration, node sub-execution, or the MCP execute adapter) constructs a new http.Request — or uses a bare http.Post / http.DefaultClient — instead of reusing the authenticated client the run process already has. Concretely, the request is built along the lines of:

// nested / sub-execution path (simplified)
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, serverURL+"/api/v1/execute", body)
req.Header.Set("Content-Type", "application/json")
// BUG: no Authorization header is copied from the run process / top-level client
resp, err := http.DefaultClient.Do(req)

The top-level path gets Authorization: Bearer <token> (e.g. via setAuthHeader/the shared API client), but the nested path does not. Since the token is a fresh random value per tick, it cannot be rediscovered from disk — it must travel down the call stack.

4. Why the wrapper still prints green

The 5-node sub-pipeline reports per-item failures as a null result rather than a hard node error. The parent's failure counter only increments on hard errors, so Failed: 0 and exercised=0 are consistent: the whole run no-oped.

serve.log:  http_auth_denied reason=missing path=/api/v1/execute
wrapper:    Passed: 4, Failed: 0
report:     exercised=0 findings=0

The fix

There are two equivalent fixes. Prefer A (one authenticated client shared by all code paths) because it fixes every present and future nested path at once; B is the minimal, surgical patch if you only want to touch the one call site.

Fix A — one authenticated HTTP client, reused by nested executions

Add a transport that injects the bearer token from the run process environment. Env inheritance is the correct mechanism here: the wrapper already exports the token into the run process, so every in-process sub-execution inherits it for free, and no secret is written to disk.

// src/runner/authclient.go  (new file; place in the package that owns nested execute)
package runner

import (
    "net/http"
    "os"
    "time"
)

// daggerAuthTransport mirrors the top-level `dagger run` client's auth on
// every request: it injects `Authorization: Bearer $DAGGER_API_TOKEN` unless
// the caller already set one. This makes nested (foreach / sub-DAG / per-item)
// execute calls authenticate exactly like the top-level execute call.
type daggerAuthTransport struct {
    base  http.RoundTripper
    token string
}

func (t *daggerAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    base := t.base
    if base == nil {
        base = http.DefaultTransport
    }
    if t.token == "" || req.Header.Get("Authorization") != "" {
        return base.RoundTrip(req)
    }
    // Clone: never mutate the caller's request (RoundTripper contract).
    clone := req.Clone(req.Context())
    clone.Header.Set("Authorization", "Bearer "+t.token)
    return base.RoundTrip(clone)
}

// NewServerClient returns the client every execute path must use. It reads
// DAGGER_API_TOKEN at construction time; call it after the wrapper exports it.
func NewServerClient(timeout time.Duration) *http.Client {
    return &http.Client{
        Transport: &daggerAuthTransport{
            base:  http.DefaultTransport,
            token: os.Getenv("DAGGER_API_TOKEN"),
        },
        Timeout: timeout,
    }
}

Then wire it in once, where the runner is constructed, and have all nested code use r.httpClient instead of http.DefaultClient / http.Post:

// where the Runner is built (setupEngine / RunFile / main)
r := &Runner{
    // ...
    httpClient: NewServerClient(serverExecuteTimeout),
}

// nested execute path
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
    strings.TrimRight(serverURL, "/")+"/api/v1/execute", body)
if err != nil {
    return fmt.Errorf("execute: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := r.httpClient.Do(req) // <-- token injected by transport

This also automatically covers any sub-execution that shells back into the same serve instance, because they all share the client.

Fix B — minimal, at the nested call site

If you cannot restructure the runner, add the header directly where the nested request is built:

req, err := http.NewRequestWithContext(ctx, http.MethodPost, serverURL+"/api/v1/execute", body)
if err != nil {
    return fmt.Errorf("execute: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if tok := os.Getenv("DAGGER_API_TOKEN"); tok != "" {
    req.Header.Set("Authorization", "Bearer "+tok)
}

Locate the exact call site with:

rg -n 'api/v1/execute' src cmd
# Inspect every `http.NewRequest|http.Post|http.DefaultClient` near those hits and
# ensure EACH one sets the Authorization header.

Fix C — do not swallow per-item failures (prevents the silent-green regression)

Independently of auth, make the sub-pipeline fail loudly. A 401 must never be reported as null/success:

// after executing the sub-pipeline, before returning the aggregate
if result.Exercised == 0 || result.Errors > 0 {
    return nil, fmt.Errorf(
        "sub-execution exercised %d targets with %d errors (last: %v)",
        result.Exercised, result.Errors, result.LastErr,
    )
}

If the aggregate shape does not carry counts, at minimum propagate any HTTP error whose status is not 2xx instead of converting it to nil. This makes the wrapper print red and non-zero, which is what would have surfaced DAGGER-133 on day one.

Do not disable the auth check in serve, and do not persist the random token to a file. Both would reintroduce the fail-open hole DAGGER-133 closed.


Verification

1. Unit test that the nested request carries the header

// src/runner/authclient_test.go
package runner

import (
    "net/http"
    "net/http/httptest"
    "testing"
    "time"
)

func TestNestedExecuteSendsBearerToken(t *testing.T) {
    t.Setenv("DAGGER_API_TOKEN", "test-token-133")

    var gotAuth string
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        gotAuth = r.Header.Get("Authorization")
        w.WriteHeader(http.StatusOK)
    }))
    defer srv.Close()

    c := NewServerClient(5 * time.Second)
    req, _ := http.NewRequest(http.MethodPost, srv.URL+"/api/v1/execute", nil)
    resp, err := c.Do(req)
    if err != nil {
        t.Fatalf("nested execute failed: %v", err)
    }
    defer resp.Body.Close()

    if want := "Bearer test-token-133"; gotAuth != want {
        t.Fatalf("Authorization header = %q, want %q", gotAuth, want)
    }
}

func TestNestedExecuteRespectsCallerHeader(t *testing.T) {
    t.Setenv("DAGGER_API_TOKEN", "env-token")

    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if got := r.Header.Get("Authorization"); got != "Bearer explicit" {
            t.Errorf("caller header overwritten: %q", got)
        }
    }))
    defer srv.Close()

    req, _ := http.NewRequest(http.MethodPost, srv.URL+"/api/v1/execute", nil)
    req.Header.Set("Authorization", "Bearer explicit")
    if _, err := NewServerClient(5 * time.Second).Do(req); err != nil {
        t.Fatal(err)
    }
}

Run:

go test ./src/runner/ -run 'NestedExecute' -v

2. End-to-end: reproduce then prove the fix

export DAGGER_API_TOKEN="$(openssl rand -hex 32)"
rm -f /tmp/dagger-serve.log

# 1) serve fail-closed, with the token in its own process env
dagger serve --addr <ip-address>:11775 >/tmp/dagger-serve.log 2>&1 &
SERVE_PID=$!
sleep 1

# 2) negative control: no header must be rejected
curl -s -o /dev/null -w 'no-auth -> %{http_code}\n' \
  -X POST http://<ip-address>:11775/api/v1/execute \
  -H 'Content-Type: application/json' -d '{"code":"dag.node(\"a\", async () => 1)"}'
# expect: no-auth -> 401

# 3) authenticated control must succeed
curl -s -o /dev/null -w 'with-auth -> %{http_code}\n' \
  -X POST http://<ip-address>:11775/api/v1/execute \
  -H "Authorization: Bearer $DAGGER_API_TOKEN" \
  -H 'Content-Type: application/json' -d '{"code":"dag.node(\"a\", async () => 1)"}'
# expect: with-auth -> 200

# 4) the real regression: a foreach pipeline, run without manually passing headers
dagger run --server http://<ip-address>:11775 /path/to/foreach.d5s

# 5) assert no auth denials were logged and targets were actually exercised
grep -c 'http_auth_denied' /tmp/dagger-serve.log   # expect: 0
# and the report must show exercised > 0
kill $SERVE_PID

3. Fleet-level post-deploy check

For every affected lane (dogfooding, qa, pm, bankai, plus-ultra), after the wrapper runs:

for log in /tmp/dagger-role-*/serve.log; do
  denials=$(grep -c 'http_auth_denied reason=missing path=/api/v1/execute' "$log" 2>/dev/null || true)
  echo "$log denials=$denials"
done
# expect 0 for all lanes

And confirm the report no longer lies:

grep -E 'exercised=[0-9]+' /tmp/dagger-role-*/report.txt
# exercised must be > 0; if it is 0, Fix C is missing

4. Regression guard

Add a CI test that boots a fail-closed httptest-style server requiring Authorization and runs a 2-item foreach against it, asserting the run succeeds and both items produced non-null output. This catches any future path that forgets the header — and any future change that silently converts a 401 into null.


Files and concepts touched

Area File / symbol (repo github.com/Hermes-DAGger/&lt;project&gt;) Change
Auth middleware (do not change) cmd/dagger/main.go → serve / signature middleware Keep fail-closed
Shared client (Fix A) src/runner/ (new authclient.go) Inject Authorization: Bearer $DAGGER_API_TOKEN
Nested execute call site src/runner/runner.go (execute*, sub-execution) and src/nodes/foreach.go Use r.httpClient, not http.DefaultClient
Failure propagation (Fix C) sub-pipeline aggregation Fail on errors > 0 or exercised == 0
Wrapper (no change needed) ~/.hermes/scripts/dagger-role-tick.sh Already exports DAGGER_API_TOKEN into the run process env

Key invariant to preserve: the token is a per-tick random secret exported into the run process environment. Any code path that spawns a sub-request must either inherit that env (Fix A) or explicitly set the bearer header (Fix B). No path may use a bare HTTP client against /api/v1/execute.

Evidence & signatures

# Evidence
- Problem class: dagger-nested-execute-401
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-12T20:43:02.343Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Root cause: since DAGGER-133 (2026-09-12) dagger serve is fail-closed and requires DAGGER_API_TOKEN on every REST route. Fleet role wrappers (dagger-role-tick.sh) export a fresh random DAGGER_API_TOKEN so serve + the top-level dagger run client authenticate fine. BUT the foreach/node execute machinery runs per-item sub-executions that call POST /api/v1/execute WITHOUT the Authorization header \u2014 serve logs http_auth_denied reason=missing on /api/v1/execute, the item node errors with: execute: http://<ip-address>:<port> rejected the request (401): set DAGGER_API_TOKEN to the token dagger serve was started with. The wrapper prints green (Passed: 4, Failed: 0) because the 5-node sub-pipeline reports per-item failures as a null result, so every role lane (dogfooding, qa) silently exercises 0 targets. Fix: the nested execute path must inherit DAGGER_API_TOKEN from the run process env (or the run client must pass its token into sub-executions the same way it authenticates its own top-level execute). Verified fleet-wide: /tmp/dagger-role-qa-* runs on 2026-09-12 13:32 and 14:21 show the same 401; dogfooding <project> runs 07:45 (pre-fix) worked, 13:08 and 15:29 (post-fix) noop.", "environment": "<project> v0.1.0 (dagger serve tier 2, DAGGER-133 fail-closed auth)", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "dagger-nested-execute-401", "provider": "openrouter", "solved_at": "2026-09-12T20:43:02.344Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog