Connect Codex to Nerudo

This guide gives the procedure to connect Codex to Nerudo and examine a change result.

Terms

Technical nounMeaning in this guide
CLICommand-line interface. A program that receives commands from a terminal.
MCPModel Context Protocol. A protocol for a client to use server tools.
native MCP serverThe Rust MCP server in the Nerudo product repository. It uses standard input and standard output.
clientA program, such as Codex, that uses MCP tools.
agentA program that prepares a change. Its agent name identifies its fork.
GitThe version-control program.
repositoryA store of files and Git commit history.
checkoutThe local files and Git data for a repository.
branchA Git reference that identifies a line of commits.
commitA Git record of a file tree, parents, and a message.
input commitThe commit at HEAD when the agent uses land.
trunk commitThe commit that the sequencer writes to trunk.
commit IDThe full Git hash that identifies a commit.
HEADThe Git reference for the selected commit in the checkout.
trunk IDThe service identifier for a trunk.
trunkThe repository branch that only the sequencer writes. Here, it is main.
nerudo/mainThe local reference that the sync tool sets to the trunk commit.
base commitThe common ancestor that the server calculates from HEAD and nerudo/main.
forkThe repository for changes from one agent.
changeOne proposal to add a commit to trunk. A change has a stable change ID.
revisionA numbered version of a change. The server selects the revision number.
taskA work item with an ID such as T-12.
change contextThe change record with the plan, decisions, alternatives, and task description.
sequencerThe Nerudo component that does checks of changes and writes them to trunk.
batchA group of changes for one set of sequencer checks.
CIContinuous integration. The checks that the sequencer does for a batch.
footprint unitA group of paths that the repository rules identify for change order.
receiptThe result record for one change revision.
landing logThe sequence of records for changes that are on trunk. Hashes connect the records.
sealA cryptographic approval signature from a person for one change revision and commit.
allowanceA cryptographic signature that gives an agent permission with limits.
agent bearer tokenThe operator-supplied AGENT credential for native agent operations. The server sends it in the HTTP Authorization header.
developer credentialA personal credential that starts with nrd_dev_. It gives access to selected account API operations.
agent keyA secret value that identifies one agent. It is not a person's private key.
environment variableA named value that a program receives from its parent process.
launcherA local Python program that reads credentials and starts nd.
configuration entryThe TOML section that tells Codex how to start the MCP server.
POSIXAn operating-system interface standard for the commands in this guide.
TOMLTom's Obvious Minimal Language. The data format for the Codex configuration file.
JSONJavaScript Object Notation. A data format for the credential file and HTTP records.
raw JSON recordThe JSON result from an HTTP request, before MCP text conversion.
HTTPHypertext Transfer Protocol. HTTPS adds transport encryption.
URLA network address for a resource.
APIApplication programming interface. The HTTP routes that the Nerudo service supplies.

Prerequisites and installation

The commands nd and nerudo use the same entry point. Cargo uses nerudo for cargo run -p nerudo-cli. The source files are main.rs and mcp.rs. These files are in the Nerudo product repository. The documentation repository does not contain the CLI source.

Nerudo controls repository changes through a sequencer. Only the sequencer writes to trunk. LandSeq is the previous product name. The service and the CLI do not accept LandSeq names. Storage identifiers and signed records keep their values.

This procedure is for a POSIX system with Git, Python 3, Rust, Cargo, and a Codex client with local MCP tools. The Rust toolchain must be compatible with the dependencies in cf/Cargo.lock. A Git author name and email address are necessary in the checkout. The service must contain the repository nerudo/nerudo. The service operator supplies an agent bearer token and, for a fork from a previous session, its agent key.

A personal developer credential gives no landing, Git transport, approval, or agent-delegation authority. The AGENT credential is necessary for this integration. An administrator token or a person's private key is not necessary.

  1. Get the repository checkout from the operator.

  2. Select the checkout directory in your terminal.

  3. Install the package with this command:

    bash
    cargo install --path cf/nerudo-cli --locked --bins
  4. Find nd:

    bash
    command -v nd
  5. Find nerudo:

    bash
    command -v nerudo
  6. Record the full path to nd.

  7. Record the full path to python3.

  8. Make sure that Git has the correct author name and email address:

    bash
    git var GIT_AUTHOR_IDENT

The package is nerudo-cli in the cf Cargo workspace. Installation supplies both commands. The CLI does not have submit or receipt subcommands. The MCP server supplies the land and receipt tools.

Local credentials and launcher

The launcher starts this command with credentials in its process environment:

text
nd mcp --trunk nerudo/nerudo --agent <agent>

The placeholder <agent> identifies the selected agent name. The service address is https://nerudo.dev. NERUDO_REPO identifies the full path to the authorized checkout. Without this value, the server uses its working directory.

Environment variableValue
NERUDO_WORKERhttps://nerudo.dev. Without this value, the CLI uses http://127.0.0.1:8787.
NERUDO_TOKENThe agent bearer token from the service operator.
NERUDO_REPOThe absolute checkout path.
NERUDO_AGENT_KEYOptional agent key. It replaces the agent key from the local file.

On first fork creation, the native server receives an agent key. Without an environment override, it keeps the key in ~/.nerudo/agents/<trunk-id>/<agent> with owner-only access. An agent key is necessary for a fork from a previous session. The operator can supply the key for a fork from a previous session.

  1. Keep tokens and keys out of prompts, command arguments, shared configuration, and change context.

  2. Keep credential files in a directory that is not in the checkout.

  3. Make the local configuration directory:

    bash
    mkdir -p "$HOME/.config/nerudo"
  4. Set directory access to mode 0700:

    bash
    chmod 700 "$HOME/.config/nerudo"
  5. Set the file creation mask:

    bash
    umask 077
  6. Make ~/.config/nerudo/credentials.json with a local editor.

  7. Use this structure with your local values:

    json
    {  "nd": "/absolute/path/to/nd",  "agent": "codex_sol",  "NERUDO_WORKER": "https://nerudo.dev",  "NERUDO_TOKEN": "REPLACE_LOCALLY",  "NERUDO_REPO": "/absolute/path/to/checkout"}
  8. For a fork from a previous session, add NERUDO_AGENT_KEY with the agent key from the operator.

  9. Set credential file access to mode 0600:

    bash
    chmod 600 "$HOME/.config/nerudo/credentials.json"
  10. Copy this launcher to ~/.config/nerudo/launch.py:

python
import jsonimport osfrom pathlib import Pathimport reimport statimport sysimport urllib.parseimport urllib.request​CREDENTIALS = Path.home() / ".config/nerudo/credentials.json"TRUNK = "nerudo/nerudo"​​def load_credentials():    fd = os.open(CREDENTIALS, os.O_RDONLY | os.O_NOFOLLOW)    with os.fdopen(fd) as source:        info = os.fstat(source.fileno())        if (not stat.S_ISREG(info.st_mode)                or info.st_uid != os.getuid()                or stat.S_IMODE(info.st_mode) != 0o600):            raise SystemExit("Credential file must belong to you, with mode 0600.")        config = json.load(source)    required = ("nd", "agent", "NERUDO_WORKER", "NERUDO_TOKEN", "NERUDO_REPO")    if not all(isinstance(config.get(k), str) and config[k] for k in required):        raise SystemExit("A required configuration value is absent.")    if config["NERUDO_WORKER"] != "https://nerudo.dev":        raise SystemExit("Worker address must be https://nerudo.dev.")    if (config["NERUDO_TOKEN"] == "REPLACE_LOCALLY"            or config["NERUDO_TOKEN"].startswith("nrd_dev_")):        raise SystemExit("Set the operator-supplied agent bearer token locally.")    if not re.fullmatch(r"[A-Za-z0-9_]+", config["agent"]):        raise SystemExit("Agent name must contain only letters, digits, or underscores.")    if not Path(config["nd"]).is_absolute():        raise SystemExit("The nd path must be absolute.")    repo = Path(config["NERUDO_REPO"])    if not repo.is_absolute() or not repo.is_dir():        raise SystemExit("The checkout path must be an absolute directory path.")    if "NERUDO_AGENT_KEY" in config:        if not isinstance(config["NERUDO_AGENT_KEY"], str) or not config["NERUDO_AGENT_KEY"]:            raise SystemExit("The optional agent key must be a nonempty string.")    return config​​class NoRedirect(urllib.request.HTTPRedirectHandler):    def redirect_request(self, request, response, code, message, headers, url):        return None​​def get_json(config, path):    headers = {        "Authorization": "Bearer " + config["NERUDO_TOKEN"],        "User-Agent": "nerudo-guide/1",    }    if config.get("NERUDO_AGENT_KEY"):        headers["X-Nerudo-Agent-Key"] = config["NERUDO_AGENT_KEY"]    request = urllib.request.Request(config["NERUDO_WORKER"] + path, headers=headers)    opener = urllib.request.build_opener(NoRedirect)    with opener.open(request, timeout=120) as response:        return json.load(response)​​def main():    config = load_credentials()    args = sys.argv[1:]    if args in (["mcp"], ["verify"]):        allowed = ("HOME", "PATH", "LANG", "TMPDIR", "SSL_CERT_FILE", "SSL_CERT_DIR")        env = {k: os.environ[k] for k in allowed if k in os.environ}        for k in ("NERUDO_WORKER", "NERUDO_TOKEN", "NERUDO_REPO", "NERUDO_AGENT_KEY"):            if k in config:                env[k] = config[k]        command = [config["nd"], args[0], "--trunk", TRUNK]        if args == ["mcp"]:            command += ["--agent", config["agent"]]        os.chdir(config["NERUDO_REPO"])        os.execve(config["nd"], command, env)    elif (len(args) == 3 and args[0] == "record"          and re.fullmatch(r"[A-Za-z0-9._-]{1,64}", args[1])          and args[2].isdigit() and int(args[2]) > 0):        suffix = f"changes/{args[1]}?rev={int(args[2])}"    elif len(args) == 2 and args[0] == "log" and args[1].isdigit():        suffix = f"log?since={int(args[1])}"    else:        raise SystemExit("Use: launch.py mcp | verify | record CHANGE REV | log SINCE")    trunk = TRUNK    if "/" in trunk:        trunk = get_json(config, "/v1/hub/repos/" + trunk)["trunk"]    trunk = urllib.parse.quote(trunk, safe="")    result = get_json(config, f"/v1/trunks/{trunk}/{suffix}")    print(json.dumps(result, indent=2))​​if __name__ == "__main__":    main()

The mcp and verify modes start native nd commands. The record and log modes make HTTP GET requests. They are launcher modes, not nd subcommands. The launcher sends no credentials in command arguments. It stops on HTTP redirects to keep credentials at the selected origin.

The route GET /v1/hub/repos/nerudo/nerudo gives the trunk ID for this public repository. For a private repository, the agent uses the trunk ID from the operator. The native CLI uses an administrator token for private repository name resolution.

  1. For a private repository, replace TRUNK in the launcher with the supplied trunk ID.
  2. If you cannot get access to a fork from a previous session, tell the operator.
  3. Do not put the operator's key output in the conversation.
  4. Do not give the agent an administrator token or a person's signature key.

Codex configuration

Codex uses a TOML configuration entry for each MCP server. The official OpenAI documentation gives information about the command, args, and timeout fields. The entry below uses the server name nerudo. It contains paths, not credentials.

  1. Add this entry to your local ~/.codex/config.toml:

    ini
    [mcp_servers.nerudo]command = "/absolute/path/to/python3"args = ["/absolute/path/to/.config/nerudo/launch.py", "mcp"]startup_timeout_sec = 60tool_timeout_sec = 180
  2. Replace the example paths with the recorded local paths.

  3. Stop the client.

  4. Start the client.

  5. Examine its MCP tool list for nerudo.

  6. Make sure that the tools include sync, rules, land, and receipt.

The native tool list also contains tasks, claim, new_task, in_flight, status, context, and cleanup. It contains comment, mentions, request_review, react, and release_propose. The server also lists request_approval when the client can show the MCP approval form.

Prepare and send a change

These examples show MCP tool names and argument objects. They are not terminal commands. sync fetches trunk into nerudo/main without a branch switch or rebase. The repo argument for a tool can select only the configured checkout or a directory below it.

  1. Use tasks with {}.
  2. If no applicable task is available, use new_task with {"title":"Change title","body":"Change purpose"}.
  3. Select the applicable task ID.
  4. Use claim with {"task":"T-12"} for that task.
  5. Use sync with {}.
  6. Use rules with {}.
  7. Use in_flight with {}.
  8. Examine the rules and changes that wait.
  9. If one more change is for the same code, use context with its change_id.
  10. Make a branch from trunk:
bash
git switch -c T-12-change-name nerudo/main
  1. Make the necessary changes.
  2. Do the applicable checks.
  3. Examine the difference with git diff.
  4. Add the necessary paths with git add <path>.
  5. Make one commit with git commit.
  6. Give the cause of the change in the commit message body.
  7. Make sure that the last commit paragraph contains the applicable task trailer, such as Task: T-12.
  8. Record the full commit ID:
bash
git rev-parse HEAD
  1. Examine the commit:
bash
git show --stat HEAD
  1. Make sure that this command gives no output:
bash
git status --porcelain
  1. Make sure that HEAD identifies the correct commit.
  2. Use land with the applicable values:
json
{  "change_id": "T-12-change-name",  "task": "T-12",  "context": "Plan: Add the requested guide. Decision: Use the native MCP server. Rejected alternative: A separate adapter adds unnecessary code.",  "wait_s": 50}
  1. Record the change ID and revision number from the result text.
  2. Use receipt with those result values:
json
{"change_id":"T-12-change-name","rev":1}
  1. If the revision from the result is not 1, replace the example revision with the number from the result.

The native land tool reads HEAD, fetches trunk, and calculates the base commit with Git merge-base. It selects the revision internally. Its arguments do not include a commit ID, base commit, or revision number. It sends the commit to the agent fork and sends a request to add the change to trunk. It keeps supplied change context on the fork's nerudo/context branch.

land usually waits a maximum of 50 seconds for a receipt. The maximum wait_s value is 600 seconds. The client timeout can stop the tool request first. A client timeout does not show that the server stopped the change. The text shows only the first part of each commit ID. The raw JSON record supplies full commit IDs.

Receipt formats and result checks

The native tools land and receipt give MCP text results with this structure:

json
{"content":[{"type":"text","text":"LANDED: ..."}],"isError":false}

This example shows only the first part of the result text. isError: false shows that the tool call did not give an error. It does not show that the change is on trunk. The text can also show QUEUED, PARKED FOR A HUMAN, or an ejection message.

The HTTP request GET /v1/trunks/<trunk-id>/changes/<change-id>?rev=N gives a raw JSON record. The record has state, req, seq, and trunk_commit fields directly. It does not have an outer receipt field or a landed boolean. req contains change_id, rev, commit, base, and fork. A result with state: "landed" identifies the landed revision.

  1. Get the record with the change ID from the result and revision number:

    bash
    python3 "$HOME/.config/nerudo/launch.py" record T-12-change-name 1
  2. Make sure that req.change_id and req.rev agree with the values from land.

  3. Compare req.commit with the full commit ID recorded before land.

  4. Make sure that state is landed.

  5. Record the sequence number in seq.

  6. Record the full trunk commit ID in trunk_commit.

The sequencer makes a new trunk commit with its trailers. The input commit ID and trunk commit ID can be different. An ancestor check of only the input commit cannot show that the change is on trunk.

Examine Git and the landing log independently

  1. Use the MCP sync tool with {}.

  2. Make sure that Git contains the recorded trunk commit:

    bash
    git cat-file -t <trunk-commit>

    The output must be commit.

  3. Make sure that trunk contains that commit:

    bash
    git merge-base --is-ancestor <trunk-commit> nerudo/main

    Exit code 0 shows that the commit is an ancestor of the last trunk commit.

  4. Examine the trunk change:

    bash
    git show --format=fuller <trunk-commit> -- <changed-path>
  5. Compare the files with the necessary change.

  6. Get the landing log with SINCE equal to the recorded sequence number minus one:

    bash
    python3 "$HOME/.config/nerudo/launch.py" log SINCE
  7. Select the log entry with the recorded sequence number.

  8. Compare its change_id, rev, and commit_in with the recorded change ID, revision number, and input commit ID.

  9. Compare its commit_out with the recorded trunk commit ID.

  10. Do the native log and signature checks:

    bash
    python3 "$HOME/.config/nerudo/launch.py" verify
  11. Make sure that the command gives exit code 0.

The launcher verify mode starts nd verify --trunk nerudo/nerudo with the local credentials. That command reads trunk independently. It does checks of the landing log and approval signatures. The log route gives at most 500 entries after since. A subsequent change can move trunk to a newer commit than the recorded trunk_commit.

Retries and human approvals

Result textAction
LANDEDDo the result checks for the revision from the result. Use sync before the next change.
QUEUEDUse receipt again with the same change_id and rev.
PARKED FOR A HUMANStop. Tell the user the cause and the approval command from the receipt.
CLOSED: conflictUse sync. Rebase with git rebase nerudo/main. Repair the conflict. Do the checks again.
EJECTED BY CIUse sync. Rebase on nerudo/main. Repair the reported errors. Do the checks again.
WAITING FOR A PICKStop work on this change. Wait for a person to select a change.
CLOSED, NOT PICKEDStop work on this change.
REPLACEDGet the newer revision number. Use receipt with that number.
Other ejectionExamine the cause before one more land request.
  1. After a conflict or CI repair, record the new HEAD commit ID.
  2. Use land again with the same change_id and task ID.
  3. Record the new revision number from its result.
  4. Use receipt with that new revision number.
  5. If a request times out, use receipt before one more land request.
  6. If the revision number is unknown, use receipt with only change_id to find the last revision number.
  7. Then use receipt with that revision number.
  8. Compare the raw record's req.commit with the recorded HEAD.
  9. If no record is available, do an investigation of the request before one more land request.
  10. Do not change HEAD until you know the result.
  11. Do not sign an approval, seal, allowance, or release as the user.
  12. Do all necessary approval procedures and checks.
  13. If the user does not accept an approval form, do not show the form again without the user's instruction.

A missing record does not show that a previous request cannot finish.

For the same commit that waits, land reports the same revision. For a different commit, land can replace a revision that waits. For an ejected revision, land selects the next revision number. No new revision is possible for a change on trunk. The API client sends the HTTP request again for 503 responses with Retry-After at most three times. It does not automatically send the request again for all network errors.

The optional request_approval tool shows a form to the person. Only the person's accepted form with approve: true gives permission for the signature. An agent does not complete the form for the person.

The language review record gives the language scope and inspection limits.