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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

Codex Bridge is a dedicated [protoAgent](https://github.com/protoLabsAI/protoAgent) plugin for migrating selected operator-owned [Codex](https://developers.openai.com/codex) state into a new protoAgent agent.

Status: foundation and source contract. Migration tools are not implemented yet. Track the first vertical slice in this repository's GitHub issues.
Status: v0.1 snapshot migration candidate. Source inventory, decision-grade review, skill/instruction/memory/MCP translation, and secret-free snapshot export are implemented. Fresh-agent apply remains a later tranche.

## Planned v0.1 scope

Expand Down Expand Up @@ -57,6 +57,16 @@ The bridge will never read or migrate:
- Fresh protoAgent agent creation only after explicit acknowledgement.
- No source mutation, in-place target merge, credential transfer, or plugin installation.

## Available tools

| Tool | Purpose |
|---|---|
| `codex_sources` | Report availability of configured user and repository roots without discovering others |
| `codex_import_scan` | Build a write-nothing human or JSON review and plan SHA-256 |
| `codex_export_snapshot` | Preview or write a secret-free protoAgent snapshot archive |

Snapshot export is dry-run by default. This candidate does not yet create a protoAgent agent.

## Planned delivery

1. Source inventory and classification.
Expand Down
14 changes: 6 additions & 8 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
"""Codex Bridge foundation plugin.

The foundation registers only its bundled planning skill. Source inventory,
translation, snapshot, and acknowledged-apply tools land through repository
issues and the implementation plan.
"""
"""Codex Bridge preview and snapshot plugin."""

from __future__ import annotations

Expand All @@ -13,6 +8,9 @@


def register(registry) -> None:
"""Register the bundled Codex Bridge planning guidance."""
"""Register Codex source-review and snapshot-export tools plus guidance."""
from .tools_import import build_tools

registry.register_tools(build_tools(dict(getattr(registry, "config", {}) or {})))
registry.register_skill_dir("skills")
log.info("[codex_bridge] foundation registered; migration tools are not implemented")
log.info("[codex_bridge] registered preview and snapshot-export tools")
42 changes: 42 additions & 0 deletions boundaries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Fenced, complete, byte-capped reads for Codex Bridge."""

from __future__ import annotations

from pathlib import Path


class ReadLimitExceeded(ValueError):
"""A source file exceeds its configured complete-read cap."""


class FencedRoot:
def __init__(self, name: str, root: str | Path) -> None:
self.name = name
self.root = Path(root).expanduser().resolve()

def resolve(self, rel: str = "") -> Path:
rel = str(rel or "").strip()
parts = Path(rel).parts
if rel.startswith(("/", "~")) or ".." in parts:
raise ValueError(f"path escapes the {self.name} root: {rel!r}")
target = (self.root / rel).resolve() if rel else self.root
if target != self.root and not target.is_relative_to(self.root):
raise ValueError(f"path escapes the {self.name} root: {rel!r}")
return target

def read_bytes(self, rel: str, max_bytes: int, *, limit_name: str = "max_read_bytes") -> bytes:
try:
limit = int(max_bytes)
except (TypeError, ValueError) as exc:
raise ValueError(f"{limit_name} must be a positive integer") from exc
if limit <= 0:
raise ValueError(f"{limit_name} must be a positive integer")
target = self.resolve(rel)
with target.open("rb") as handle:
data = handle.read(limit + 1)
if len(data) > limit:
raise ReadLimitExceeded(f"{rel} exceeds {limit_name}={limit}")
return data

def read_text(self, rel: str, max_bytes: int) -> str:
return self.read_bytes(rel, max_bytes).decode("utf-8", errors="strict")
2 changes: 1 addition & 1 deletion docs/IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Codex Bridge implementation plan

Status: approved foundation; v0.1 migration implementation pending.
Status: v0.1 snapshot migration candidate. Inventory, decision-grade review, translation, and secret-free snapshot export are implemented. Fresh-agent apply and release qualification remain later tranches.

## Goal

Expand Down
124 changes: 124 additions & 0 deletions export_bundle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Build secret-free protoAgent snapshots from Codex migration plans."""

from __future__ import annotations

import io
import zipfile
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any

import yaml

from .migration import MigrationPlan


SNAPSHOT_MANIFEST = "agent.snapshot.yaml"
SNAPSHOT_VERSION = 1


@dataclass(frozen=True)
class BundleReceipt:
agent_name: str
skills: list[str]
mcp_servers: list[str]
knowledge: dict[str, int]
required_values: list[dict[str, Any]]


def _knowledge_text(plan: MigrationPlan, domain: str) -> str:
sections = []
for document in plan.knowledge.get(domain, []):
sections.append(
f"## {document.heading}\n\n"
f"_Imported from Codex {document.source_class} source `{document.source}`; "
f"precedence {document.precedence}._\n\n"
f"{document.content}"
)
return "\n\n".join(sections)


def _review(receipt: BundleReceipt, plan: MigrationPlan, exported_at: str) -> str:
lines = [
f"# Snapshot review: {receipt.agent_name}",
"",
f"Translated from selected Codex sources and exported {exported_at}.",
"",
"This is a fresh-agent recipe, not a Codex backup. Credentials, sessions, databases, logs, caches, plugin code, and raw history are excluded.",
"",
"No plugins are pinned. Importing this snapshot installs no third-party plugin code.",
"",
f"Plan SHA-256: `{plan.review_dict()['plan_sha256']}`",
"",
"## Required values",
"",
]
if receipt.required_values:
lines += ["| Value | Present in source |", "|---|---|"]
lines += [
f"| `{item['name']}` | {'yes' if item.get('was_set') else 'no'} |" for item in receipt.required_values
]
else:
lines.append("None detected.")
lines += [
"",
"Names only are included. Source values stayed behind.",
"",
"## Report-only state",
"",
]
lines += [f"- `{item}`" for item in plan.report_only] or ["None detected."]
lines += [
"",
"Knowledge documents may contain private operator material. Review them before sharing this archive.",
]
return "\n".join(lines) + "\n"


def build_bundle(
*,
agent_name: str,
migration: MigrationPlan,
now: datetime | None = None,
) -> tuple[bytes, BundleReceipt]:
if migration.problems:
raise ValueError("migration plan contains unresolved migration problems")
stamp = (now or datetime.now(UTC)).isoformat()
knowledge_counts = {domain: len(documents) for domain, documents in migration.knowledge.items() if documents}
receipt = BundleReceipt(
agent_name=agent_name,
skills=[skill.name for skill in migration.skills],
mcp_servers=[str(server.get("name") or "?") for server in migration.mcp_servers],
knowledge=knowledge_counts,
required_values=list(migration.required_values),
)
config: dict[str, Any] = {"identity": {"name": agent_name}}
if migration.mcp_servers:
config["mcp"] = {"servers": migration.mcp_servers}
manifest = {
"snapshot_version": SNAPSHOT_VERSION,
"kind": "agent-snapshot",
"exported_at": stamp,
"agent": {"name": agent_name},
"plugins": [],
"config": config,
"soul": None,
"required_secrets": migration.required_values,
"knowledge": {"domains": knowledge_counts} if knowledge_counts else None,
"excludes": {
"credentials": "all Codex credential values remain in their source stores",
"runtime_state": "sessions, databases, logs, caches, plugins, and raw history are excluded",
"policy": "model, approval, sandbox, command-rule, and provider policy are report-only",
},
"translated_from": {"agent": "codex"},
}
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr(SNAPSHOT_MANIFEST, yaml.safe_dump(manifest, sort_keys=False, allow_unicode=True))
for skill in migration.skills:
for rel, content in skill.files.items():
archive.writestr(f"skills/codex/{skill.name}/{rel}", content)
for domain in sorted(knowledge_counts):
archive.writestr(f"knowledge/{domain}.md", _knowledge_text(migration, domain))
archive.writestr("REVIEW.md", _review(receipt, migration, stamp))
return buffer.getvalue(), receipt
Loading
Loading