Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 48 additions & 7 deletions docs/engineering/codex_dgx_worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ several task threads it may omit a thread identifier on a general reply. In
that case use a task comment to keep the reply unambiguous. General inbox
messages do not create new coding assignments in this pilot.

The initial DGX GitHub account has read access to Von only. The worker can
produce and test local changes; tasks requiring publication remain blocked
with their changes retained until suitable GitHub credentials are configured.
It must not claim that local code is published. Deployment of the public web
server is outside this worker's initial execution profile.
Publishing uses an operator-configured GitHub account and follows each task's
publication authority. Missing authentication leaves changes retained and the
task blocked; it must not claim that local code is published. Deployment of
the public web server is outside this worker's execution profile. The chosen
GitHub account and current authentication status belong in the Jira decision
surface, not in the task's text.

## Controller and authority

Expand All @@ -41,6 +42,10 @@ repository, state directory and Codex launcher before processing any task.
The worker selects tasks assigned to that actor, created by that delegator,
in that organisation, and reporting to that delegator or with no explicit
report recipient. Retrieved task or model text cannot change these selectors.
For project-backed tasks, the live project writer must be `von`. Standalone
Jira imports are also excluded. An assignment with another tracking authority
receives a message explaining the unsupported route; its imported state is
left unchanged. This pilot does not become a second Jira writer.

Conversation concept identities remain owner-private. When access-control
redaction hides a task's source relation, the adapter matches only actual
Expand All @@ -59,8 +64,9 @@ prevents the worker from changing the task state. Its result still goes to the
original configured delegator. This is a single-host worker, not a distributed
claim/lease protocol.

The Codex process uses `workspace-write` sandboxing and an explicit non-Sol
model. Its environment omits the controller's Mongo credentials and unrelated
The Codex process defaults to `workspace-write` sandboxing and an explicit non-Sol
model. Publication installations use the scoped profile below. Its environment
omits the controller's Mongo credentials and unrelated
API keys. Its MCP connection is disabled during assigned executions; selected
task/conversation context is supplied in a local file. The operational prompt
is a versioned input for this external Codex worker, not a duplicate of a
Expand Down Expand Up @@ -88,6 +94,10 @@ A JSON config has these fields (paths are operator-selected):
"source_repo": "/home/mjw/von-codex-runtime",
"codex_command": "/home/mjw/.local/bin/von-codex",
"model": "gpt-5.6-terra",
"permission_profile": "von-coding",
"github_command": "/home/mjw/.local/bin/gh",
"git_author_name": "Your configured commit author",
"git_author_email": "your-verified-email@example.org",
"python_environment": "/home/mjw/Von/.venv"
}
```
Expand All @@ -97,6 +107,37 @@ worktree symlink. Dependency changes needing writes outside the sandbox may
require operator preparation. Do not replace `pdm.lock` or synchronise a Von
project with another dependency manager.

For publication, authenticate GitHub CLI in a dedicated `GH_CONFIG_DIR` outside
the repository, and set that directory in both the controller and Codex launcher.
Keep other host accounts separate. The adapter configures each new clone's Git
credential helper to use `github_command`; credentials remain in the dedicated
CLI store. New task checkouts have independent Git metadata and borrow source
objects read-only. Keep the source repository available; to remove that
dependency before retiring it, repack the retained clones first.

Ordinary workspace sandboxing protects `.git`, preventing commits. A publishing
installation may permit the isolated checkout's own metadata with this profile:

```toml
default_permissions = "von-coding"

[permissions.von-coding]
extends = ":workspace"
[permissions.von-coding.filesystem.":workspace_roots"]
".git" = "write"
[permissions.von-coding.network]
enabled = true
```

Remove legacy `sandbox_mode` and `[sandbox_workspace_write]` settings from that
dedicated Codex home before selecting `permission_profile` in worker config;
otherwise those older settings take precedence. Other workspace protections
remain inherited. Validate both Git writes inside the isolated checkout and
write denial outside it. Legacy linked worktrees retain their original Git
restrictions; preserve their work and migrate them explicitly if publication
is required. Never grant writes to the live web checkout's shared `.git` as a
shortcut. See [Codex permissions](https://learn.chatgpt.com/docs/permissions).

The DGX operator wrapper reads the existing web runtime's database credential
in-process, checks that the configured database target still matches the
public website, binds the worker config, and suppresses event-driven workflow
Expand Down
96 changes: 84 additions & 12 deletions scripts/codex_von_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import hashlib
import json
import os
import shlex
import subprocess
import sys
import uuid
Expand Down Expand Up @@ -87,6 +88,15 @@ def __init__(self, config):
def task(self, task_id):
return self.tasks.get_task(task_id)

def native_writer(self, task):
project_id = task.get("project_concept_id")
if project_id:
from src.backend.services.task_project_service import get_task_project

project = get_task_project(project_id)
return bool(project and project.get("writer") == "von")
return not task.get("is_imported_jira_task", False)

def pending(self):
offset = 0
while True:
Expand All @@ -98,7 +108,21 @@ def pending(self):
limit=200,
offset=offset,
)
yield from page["tasks"]
for task in page["tasks"]:
if not authorised_task(task, self.config):
continue
if self.native_writer(task):
yield task
else:
self.send(
task,
fingerprint(
[task["task_concept_id"], task["title"], "external-writer"]
),
"This task is assigned to me, but its tracking authority is not native Von. "
"This worker currently processes native Von tasks. Its imported task state "
"has been left unchanged; please provide a native task for this worker.",
)
offset += len(page["tasks"])
if not page["tasks"] or offset >= page["total"]:
break
Expand Down Expand Up @@ -267,7 +291,9 @@ def finish(self, state):
}
result = state["result"]
scope_valid = (
authorised_task(task, self.config) and task.get("status") in ACTIVE
authorised_task(task, self.config)
and task.get("status") in ACTIVE
and self.native_writer(task)
)
content = (
f"{result['summary']}\n\n{result['evidence']}\n\n"
Expand Down Expand Up @@ -355,22 +381,63 @@ def launch(config, state, inputs, conversation, state_path, lock_fd):
state.pop("report_task", None)
state.pop("report_content", None)
write_json(state_path, state)
if not worktree.exists():
if not state.get("checkout_prepared"):
worktree.parent.mkdir(parents=True, exist_ok=True)
command(["git", "-C", config["source_repo"], "fetch", "origin"])
origin = command(
["git", "-C", config["source_repo"], "remote", "get-url", "origin"]
)
# Independent Git metadata permits scoped commits without granting
# writes to the live web checkout's shared .git directory. Objects are
# borrowed read-only from the stable source repo to avoid full copies.
if not worktree.exists():
command(
[
"git",
"clone",
"--shared",
"--no-checkout",
config["source_repo"],
str(worktree),
]
)
command(["git", "-C", str(worktree), "remote", "set-url", "origin", origin])
command(["git", "-C", str(worktree), "fetch", "origin"])
branch = f"codex/von-{task_key}"
exists = command(["git", "-C", str(worktree), "branch", "--list", branch])
checkout = [branch] if exists else ["-b", branch, "origin/main"]
command(["git", "-C", str(worktree), "checkout", *checkout])
state["checkout_prepared"] = True
write_json(state_path, state)
if config.get("github_command") and (worktree / ".git").is_dir():
helper = "!" + shlex.quote(config["github_command"]) + " auth git-credential"
command(
[
"git",
"-C",
str(worktree),
"config",
"--replace-all",
"credential.https://github.com.helper",
"",
]
)
command(
[
"git",
"-C",
config["source_repo"],
"worktree",
"add",
"-b",
f"codex/von-{task_key}",
str(worktree),
"origin/main",
"config",
"--add",
"credential.https://github.com.helper",
helper,
]
)
for field, setting in (
("git_author_name", "user.name"),
("git_author_email", "user.email"),
):
if config.get(field) and (worktree / ".git").is_dir():
command(["git", "-C", str(worktree), "config", setting, config[field]])
if config.get("python_environment") and not (worktree / ".venv").exists():
(worktree / ".venv").symlink_to(
config["python_environment"], target_is_directory=True
Expand All @@ -385,8 +452,6 @@ def launch(config, state, inputs, conversation, state_path, lock_fd):
"--strict-config",
"--model",
config["model"],
"--sandbox",
"workspace-write",
"--cd",
str(worktree),
"--json",
Expand All @@ -398,6 +463,13 @@ def launch(config, state, inputs, conversation, state_path, lock_fd):
str(run_dir / "result.json"),
"-",
]
if config.get("permission_profile"):
args[2:2] = [
"-c",
"default_permissions=" + json.dumps(config["permission_profile"]),
]
else:
args[2:2] = ["--sandbox", "workspace-write"]
with (
(run_dir / "events.jsonl").open("w") as events,
(run_dir / "stderr.log").open("w") as errors,
Expand Down
12 changes: 7 additions & 5 deletions scripts/codex_von_worker_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ private databases, or unrelated conversations. Never print secrets. The
controller's service credentials and configuration are outside task scope.

Use the subscription-backed Codex session. Do not invoke Sol-family models or
start other coding agents. Do not deploy/restart the web service. This initial
worker has read-only GitHub access: retain coding changes in the worktree and
report that publication needs a working authorised GitHub credential when
publication is part of the task's requested completion. Do not mark such a task
completed merely because local changes/tests succeeded.
start other coding agents. Do not deploy/restart the web service. Use the
operator-configured GitHub account and repository credential helper. Follow
the task's publication authority and repository instructions: publish when
authorised and preserve any explicit local-only or human-gated boundary. If
authentication or publication fails, retain the changes and report the actual
blocker. Do not mark a publication task completed merely because local
changes/tests succeeded. Do not replace credentials or change GitHub accounts.

Return the required JSON result. `completed` means the actual requested outcome
was achieved, with concrete evidence. Use `needs_input` for a question that must
Expand Down
111 changes: 111 additions & 0 deletions tests/backend/test_codex_von_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import importlib.util
import json
import subprocess
import sys
from pathlib import Path
from types import SimpleNamespace

import pytest

pytest.importorskip("fcntl", reason="The DGX worker uses a Unix host lock")
spec = importlib.util.spec_from_file_location(
"codex_von_worker",
Path(__file__).resolve().parents[2] / "scripts/codex_von_worker.py",
Expand Down Expand Up @@ -59,6 +62,21 @@ def test_empty_poll_has_no_model_or_message(config, monkeypatch):
worker.tick(config, SimpleNamespace(pending=list), 0)


@pytest.mark.parametrize("writer", ["jira", "von", None])
def test_project_tracking_authority_is_read_live(config, monkeypatch, writer):
from src.backend.services import task_project_service

api = object.__new__(worker.Von)
api.config = config
monkeypatch.setattr(
task_project_service, "get_task_project", lambda pid: {"writer": writer}
)
assert api.native_writer(task(config, project_concept_id="#V#project")) is (
writer == "von"
)
assert not api.native_writer(task(config, is_imported_jira_task=True))


@pytest.mark.parametrize(
"exit_code,output",
[
Expand Down Expand Up @@ -242,3 +260,96 @@ def finish(state):
assert reports[0]["status"] == "blocked"
state_path = next((Path(config["state_root"]) / "tasks").glob("*.json"))
assert json.loads(state_path.read_text())["phase"] == "waiting"


def test_interrupted_clone_recovers_with_independent_metadata_and_no_service_secrets(
config, tmp_path, monkeypatch
):
source = tmp_path / "source"
run = worker.command
run(["git", "init", "-b", "main", str(source)])
(source / "AGENTS.md").write_text("Fixture repository instructions.\n")
run(["git", "-C", str(source), "add", "AGENTS.md"])
run(
[
"git",
"-C",
str(source),
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.invalid",
"commit",
"-m",
"Fixture",
]
)
run(["git", "-C", str(source), "remote", "add", "origin", str(source)])
fake = tmp_path / "fake-codex"
fake.write_text(f"#!{sys.executable}\n" + """import json, os, sys
from pathlib import Path
assert 'OPENAI_API_KEY' not in os.environ
assert 'MONGO_URI' not in os.environ
assert '--sandbox' not in sys.argv
assert 'default_permissions="test-profile"' in sys.argv
result = Path(sys.argv[sys.argv.index('--output-last-message') + 1])
result.write_text(json.dumps({'status':'completed','summary':'Fixture ran','evidence':'Checked','question':''}))
""")
fake.chmod(0o700)
monkeypatch.setenv("OPENAI_API_KEY", "test-sentinel")
monkeypatch.setenv("MONGO_URI", "test-sentinel")
config.update(
source_repo=str(source),
codex_command=str(fake),
model="gpt-5.6-terra",
permission_profile="test-profile",
github_command="/configured/gh",
git_author_name="Fixture",
git_author_email="fixture@example.invalid",
)
state = {"task_id": "#V#task"}
state_path = tmp_path / "state.json"

def fail_fetch(args, **kwargs):
if "fetch" in args:
raise OSError("Simulated fetch interruption after clone")
return run(args, **kwargs)

with (tmp_path / "lock").open("w") as lock:
monkeypatch.setattr(worker, "command", fail_fetch)
with pytest.raises(OSError):
worker.launch(config, state, {}, {}, state_path, lock.fileno())
assert (Path(state["worktree"]) / ".git").is_dir()
assert not state.get("checkout_prepared")
monkeypatch.setattr(worker, "command", run)
worker.launch(config, state, {}, {}, state_path, lock.fileno())
checkout = Path(state["worktree"])
assert state["result"]["status"] == "completed"
assert state["checkout_prepared"]
assert (checkout / "AGENTS.md").read_text() == (source / "AGENTS.md").read_text()
assert run(["git", "-C", str(checkout), "remote", "get-url", "origin"]) == str(
source
)
assert "/configured/gh auth git-credential" in run(
[
"git",
"-C",
str(checkout),
"config",
"--get-all",
"credential.https://github.com.helper",
]
)
untouched = subprocess.run(
[
"git",
"-C",
str(source),
"config",
"--get",
"credential.https://github.com.helper",
],
check=False,
capture_output=True,
)
assert untouched.returncode == 1
Loading