Skip to content

Expose an authoritative post-run guard result channel #7857

Description

@danielmeppiel

Problem

AWF can stop an agent when it exceeds its AI Credit limit, but after cleanup
the caller cannot reliably prove that this specific limit caused the failure.

The local AWF rejection and a real provider authorization error can both appear
as the same generic HTTP 403 message. The API proxy knows the difference while
it is running, but that state disappears when the proxy container is removed.

Why this matters

A caller such as gh-aw needs to know whether it should:

  • report a budget stop;
  • retry a provider failure; or
  • treat the result as unknown.

It cannot safely make that decision from agent output, exit codes, or files
the agent can modify.

Root cause

The authoritative evidence exists only inside the API proxy:

proxy detects credit limit
  -> proxy rejects request
  -> agent exits
  -> AWF removes proxy container
  -> caller receives only exit code and generic error text

The obvious alternatives are unsafe:

  • The agent can choose any exit code.
  • The agent controls stdout and stderr.
  • The agent can modify files under the workspace and /tmp/gh-aw.
  • A file output path can be replaced with a symlink.
  • The live proxy status endpoint is gone after cleanup.

Missing or conflicting evidence must therefore remain an ordinary failure, not
a confirmed budget stop.

Proposed fix

Use one optional anonymous pipe created by the trusted caller:

  1. The caller creates the pipe and keeps its read end.
  2. AWF validates the write end before starting the agent.
  3. AWF removes the descriptor from the agent environment.
  4. After the agent exits, AWF reads two matching proxy snapshots while the
    proxy is still running.
  5. AWF cleans up and verifies that the containers are gone.
  6. AWF writes one small JSON result to the caller-owned pipe.

There is no result file for the agent to forge or redirect.

If the pipe is absent, AWF behaves exactly as it does today. If the pipe is
invalid, AWF fails before the agent starts.

Result contents

The result should contain only the facts needed to classify the failure:

  • schema version;
  • AWF invocation and proxy IDs;
  • classification and reason;
  • agent exit code;
  • cleanup result;
  • authoritative credit total and limit;
  • final proxy event;
  • timestamps and freshness;
  • count of local limit rejections and upstream 403 responses.

It must not contain prompts, credentials, headers, bodies, or model content.

When a caller may trust the result

A budget stop is confirmed only when:

  • exactly one complete result is received;
  • invocation and proxy IDs match;
  • two proxy snapshots match and have no active requests;
  • the final event is the local AI Credit limit;
  • the total is at or above the configured limit;
  • the agent exit is nonzero;
  • cleanup succeeded;
  • the separately observed AWF exit agrees with the result.

Anything stale, malformed, incomplete, conflicting, still active, or
cleanup-failed remains unconfirmed.

Locally preserved implementation

  • Branch: danielmeppiel-expose-awf-guard-result
  • Commit: e458ff3d501e95a90fd848f772eb5fa99348d563

The implementation is intentionally described here rather than submitted as a
pull request.

Proof so far

  • API-proxy tests: 81 suites / 1,639 tests passed
  • Tests cover local limit versus upstream 403, HTTP and WebSocket completion,
    malformed/stale results, active requests, partial writes, closed pipes,
    invalid descriptors, and secret exclusion
  • TypeScript syntax and workflow YAML checks passed
  • Independent security review: READY
  • Architecture and test review findings were addressed

The final real-container CI test has not run. This issue must not be considered
complete until that test and the repository build/type/lint checks pass.

Done when

  • The agent cannot see or write the result pipe.
  • Agent-controlled files, output, symlinks, and exit codes cannot confirm
    a budget stop.
  • Snapshot happens before proxy cleanup; result emission happens after
    verified cleanup.
  • Local credit-limit rejection is distinguishable from a real upstream
    403 for HTTP and WebSocket traffic.
  • Stale, malformed, active, conflicting, or cleanup-failed evidence is
    rejected.
  • Existing behavior is unchanged when no result pipe is provided.
  • Build, type, lint, unit, and CodeQL checks pass.
  • The strict real-container test passes with malicious-write, stale retry,
    upstream 403, exact framing, closed-pipe, teardown, and cleanup cases.
Maintainer implementation notes

High-level host flow:

const sink = validateAnonymousPipe(process.env.AWF_GUARD_RESULT_FD);
delete process.env.AWF_GUARD_RESULT_FD;

const docker = prepareTrustedDockerControls();
await startContainers(docker);
const exitCode = await runAgent();

const snapshot = await captureTwoMatchingIdleProxySnapshots(docker);
const result = validateSnapshot(snapshot, exitCode);

const cleanup = await cleanupAndVerify(docker);
await sink.writeOnce({ ...result, cleanup });

The descriptor must be a writable anonymous pipe, not a file, directory,
socket, terminal, named pipe, closed descriptor, or read end. The JSON record
must be one newline-terminated write no larger than 4096 bytes.

Docker must also be frozen before agent execution:

  • canonical absolute Docker executable;
  • canonical local Unix socket only;
  • no agent-controlled Docker context, config, plugin, or Compose override;
  • control files outside agent mounts;
  • exclusive no-follow Compose-file creation;
  • revalidation after stale agent containers are removed.

The proxy must track every HTTP and WebSocket request until its final response,
accounting, retry, or close path completes. Snapshot capture fails while any
request remains active.

Main files:

  • containers/api-proxy/guard-result-state.js
  • containers/api-proxy/guards/ai-credits-guard.js
  • HTTP/WebSocket proxy lifecycle modules
  • src/guard-result.ts
  • src/guard-result-writer.ts
  • src/cli-workflow.ts
  • Docker lifecycle and environment helpers
  • tests/integration/api-proxy-guard-result.test.ts
  • .github/workflows/test-guard-result.yml

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions