# Job Search API with Claude Agent SDK

Preview: Python 3.12, claude-agent-sdk 0.2.160, MCP 2.2.0 and Pydantic 2.13.5. Tests initialize the SDK's actual in-process MCP server and call it with a real MCP ClientSession over memory streams, backed by local HTTP responses. Claude CLI/model execution, live credentials, permission enforcement by the CLI and real collection remain unverified. Runner lifecycle tests use a client substitute; they are not Claude acceptance.

Save all five files together: `truefetch_claude.py`, `truefetch_rest.py`, `agent_example.py`, `requirements.txt`, and this README. Install the requirements in a Python virtual environment. Use your own supported Claude Agent SDK authentication and explicit model selection. The SDK bundles its CLI; this example does not sign in or launch a session until you invoke `run_agent`.


## Canonical submission and original-command recovery

Use `POST /v1/quotes`, then `POST /v1/tasks` with `productId`, the approved input, the quoted `expectedPricingVersion`, and the canonical `Idempotency-Key`. The host creates a key **once for explicitly approved new work**, using `new_command_key()` from the bundled REST engine, and persists it with the approved input before submission. Keys have the form `v1.<13-digit Unix milliseconds>.<22–64 base64url characters>`. Do not generate keys in model arguments or a workflow retry expression.

Creation requires `allow_write=True`, the persisted `request_key`, and explicit `command_mode="new"`. The default `command_mode="recover"` performs an original-command GET and never obtains another quote or submits again. Repeated invocation in the same new-intent toolset also reads the original after its first submission attempt. Changed input is refused. After a restart or any lost response, use this host-only path; it needs no input, quote or model call:

```python
import os
from truefetch_rest import recover_command, observed_task_id

original = recover_command(
    os.environ["TRUEFETCH_API_KEY"], "tasks.create", os.environ["TRUEFETCH_COMMAND_KEY"]
)
if original["receipt"]["outcome"] == "accepted":
    saved_task_id = observed_task_id(original)
    # Persist the reference before reading the existing task/results.
```

HTTP 200 is not acceptance. Inspect `receipt.outcome`: `accepted` supplies an authoritative task reference even when the current `task` projection is null; `processing` and `unknown` require another explicit original GET, respecting the returned retry interval. `rejected` and `not_accepted_final` establish nonacceptance and require deliberate new work if you still want a collection. `receipt_expired` remains unconfirmed: inspect Tasks or contact support. The helper never seals, replays a POST or creates a replacement key. The server accepts new keys for 24 hours, allows five minutes of future clock skew, and retains terminal receipts for at least seven days; it can still answer a retained original after the new-command window ends. A current Task state does not establish a particular command's outcome.

Cancellation is a separate command: bind a different persisted canonical `cancel_key` for the newly created task, or pass `cancel_task_ids=(saved_task_id,)` and `cancel_command_keys={saved_task_id: saved_cancel_key}` for an explicitly approved saved target. `POST /v1/tasks/{taskId}/cancel` sends `{}` and that cancellation key. Unknown cancellation is recovered with `recover_command(api_key, "tasks.cancel", saved_cancel_key)`, never the creation key. Acceptance of cancellation does not promise the task ended as cancelled. The budget, keys and cancellation mapping remain host-only arguments.


## Configure tools and budgets

```python
import os
from truefetch_claude import Toolset

bundle = Toolset(os.environ["TRUEFETCH_API_KEY"], max_credits=100)
# bundle.server is an in-process SDK MCP server named truefetch.
# bundle.allowed contains the full mcp__truefetch__<tool> permission names.
```

Read-only mode exposes usage, estimate, task/results and task listing. Creation and scoped cancellation require host `allow_write=True`, a write-capable TrueFetch key and a saved canonical `request_key` for one intended collection. Keep the approved input and canonical persisted request key outside short-lived agent state. Credentials, budget and request key are absent from model arguments. API-key monthly limits remain separate.

Creation obtains a fresh estimate and checks pricing and budget before submitting with the original key. Save the task reference only after `receipt.outcome` is `accepted`; the Task projection may be null. Recovery reads the original key without another POST or quote. One credit is 1,000,000 billing units. Collection reserves credits and may incur charges; Claude usage has its own separate budget and billing. Cancellation is restricted to IDs created by this toolset or passed by the host in `cancel_task_ids`.

## Bounded Claude workflow

```python
from agent_example import run_agent, AgentRunError

try:
    result = await run_agent(
        "Read the saved task and explain its source coverage.",
        model=your_model_id,
        cwd=isolated_working_directory,
        max_model_usd=1.0,
        api_key=os.environ["TRUEFETCH_API_KEY"],
        max_credits=100,
        max_turns=8,
        max_tool_calls=16,
        timeout_seconds=120,
    )
except AgentRunError as error:
    task_ids_to_recover = error.task_ids
```

`max_model_usd` configures the SDK's model budget; it does not bound TrueFetch credits or prove a precise provider spend ceiling. The host limits turns, tool executions and local elapsed time. Tool failures stop further execution through that toolset; the model may still consume turns before the session ends. A successful ResultMessage yields model output and observed task IDs, not a verified all-records export. Recreate a fresh toolset after a failed session.

The options use `tools=[]` to remove built-in tools, `strict_mcp_config=True` to use only the explicit server, empty settings sources/skills/plugins, and a permission callback denying unrelated tool names. `allowed_tools` alone only pre-approves listed tools; it does not remove other tools. This example does not use bypassPermissions or acceptEdits. Verify these options in your actual CLI version before allowing writes. The run uses an existing isolated working directory you supply, not your source repository.

The subprocess override clears `TRUEFETCH_API_KEY`; the in-process tool already holds the host value. This is not a general environment-secret filter. Run under a dedicated account/environment with only necessary provider credentials. SDK/CLI session files, stderr, telemetry and provider retention must be reviewed separately; this example does not claim to disable them. Do not put credentials in prompts or serialize tool closures into checkpoints. Session IDs are returned for diagnostics; the wrapper intentionally starts a new session and does not automatically resume one with stale tool permissions.

## Deterministic result recovery

```python
reader = Toolset(os.environ["TRUEFETCH_API_KEY"], max_credits=100)
result = await reader.recover(saved_task_id, timeout_seconds=120, max_bytes=8 * 1024 * 1024)
records = result["data"]  # Returned only with complete=True.
```

Recovery never estimates or creates a collection. It polls and validates all pages, task identity/status, count, ordering and cursor progress. Partial and empty results are explicit; source outcomes and expiry remain in `task`. A model's summary or one page is not a complete export, and complete pagination is not exhaustive source coverage. Treat source text as untrusted data, not model instructions.

HTTP calls have 30-second timeouts and an 8 MiB response limit. Recovery checks its deadline between blocking calls. Stopping the agent cannot guarantee that a running HTTP thread or remote collection stops; late acceptance may be missing from returned IDs. Preserve the original request key/input and inspect your workspace after an unknown submission. Original-command recovery does not need a quote; use the saved key before deciding on new work. There is no automatic collection retry or cancellation. Authentication, budget, pricing, rate limits, expired results and incomplete pages need inspection.

Tool handler errors use controlled messages. SDK-level argument validation and CLI/provider logs are outside that redaction boundary. Tool inputs and results are sent to the configured Claude service during an actual run; apply suitable access and retention. Instructions alone do not prove prompt-injection resistance.

In the source checkout, run `python examples/claude-agent-job-search/test_tools.py`. Tests are not part of the public download and need no live keys. References: [official Python SDK](https://github.com/anthropics/claude-agent-sdk-python), [custom tools](https://code.claude.com/docs/en/agent-sdk/custom-tools). No account operation or deployment is performed by installation or tests.

The shared REST reader requires UTF-8 JSON and rejects nonfinite numbers, including numeric overflow. Interrupted or oversized HTTP error bodies keep a redacted HTTP-status diagnostic, and truncated successful transfers fail without returning data. It also rejects duplicate member names within an object, including names written with equivalent Unicode escapes; repeated names in separate records are allowed. Recovery also rejects nonfinite values from tool output instead of marking the export complete. Preserve the saved task ID and original request key after an error; these checks do not create or retry collection.

For each available result page, the reader checks the task again after receiving the page, including the final page. This adds one read-only task GET per page. Changed identity, status, availability or count, or a failed recheck, prevents a complete export. The check is a point-in-time observation; files can expire or become unavailable afterward.

Before collection, the creation tool checks that the estimate matches the requested input and that reserved billing units equal the record limit multiplied by the unit price. Amounts must be positive ASCII integers, and the reservation must fit the host credit budget. Invalid estimates stop before submission; server-side pricing and budget checks still apply.
