Esta es la herramienta que llama Codex CLI cuando quiere llevar un change set a GitHub o GitLab, tal como el servidor MCP de osFoundry la lista a cualquier cliente, haya iniciado sesión o no:
{"name": "propose_gitnest_provider_publication", "description": "Ask the repository owner to review a composed GitNest change set, push only its dedicated branch using their linked Git provider account, and open a draft pull request. Approval rechecks permissions, risk policy, required checks, and target commit.", "inputSchema": {"properties": { "repository_id": {"type": "string"}, "change_set_id": {"type": "string"}, "expected_change_set_version": {"type": "integer", "minimum": 1}, "expected_composition_oid": {"type": "string"}, "idempotency_key": {"type": "string", "minLength": 8, "maxLength": 128}}, "required": ["repository_id", "change_set_id", "expected_change_set_version", "expected_composition_oid", "idempotency_key"], "additionalProperties": false}, "annotations": {"readOnlyHint": false, "openWorldHint": true, "destructiveHint": true}, "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]} Fíjate en lo que no aparece. Ni nombre de rama, ni título, ni rama de destino, ni flag de draft, ni token. El cliente nombra un change set y dos datos que observó sobre él, y todo lo demás lo pone el propietario en el momento de aprobar. "Push only its dedicated branch" es la promesa que este post desmonta. También repasa la herramienta que suele ir antes, propose_gitnest_file_import, y lo que su presupuesto de 200 KB significa con archivos reales.
En este Mac no hay ningún binario de codex instalado, así que el lado del cliente es un script de Python ejecutado aquí (Apple M1, Python 3.12 vía uv, git 2.39.5). Las cifras de red se midieron desde Japón.
$ uv run --with tiktoken --with jsonschema python3 publish_sim.py == Part 1: live tools/list (no token) == tools/list x5 median 505.7 ms; 59 tools, 13 GitNest read_gitnest_repository osfoundry:read ro=True openWorld=False destructive=False propose_gitnest_file_import osfoundry:write ro=False openWorld=False destructive=True propose_gitnest_native_publication osfoundry:write ro=False openWorld=True destructive=True propose_gitnest_provider_publication osfoundry:write ro=False openWorld=True destructive=True these four = 989 of 12662 tokens (Las listas de campos obligatorios están recortadas en la salida. Los tokens son de cl100k_base, así que son una aproximación para el tokenizer propio de Codex.) Las cuatro herramientas cuestan menos de 1,000 tokens de los 12,662 de la lista. Una configuración de Codex que las nombre en enabled_tools deja las otras 55 fuera del prompt.
¿Por qué las dos herramientas de publicación son "open world" y la importación no?
openWorldHint le indica al cliente si una herramienta llega más allá de un sistema cerrado. La importación de archivos escribe en un repositorio de GitNest que solo ve el proyecto. Un pull request, aunque sea nativo, es algo que notifica a otras personas y sobre lo que esas personas actúan. Codex usa anotaciones como estas para decidir cuándo parar y preguntar, así que las pistas del servidor son el primer eslabón de la cadena de aprobación, antes incluso de la tarjeta de osFoundry.
¿Qué puede llevar una importación de archivos?
El schema responde a casi todo. Doce casos contra los inputSchema reales, validados con jsonschema (Draft 2020-12):
file_import 20 files ok file_import 21 files REJECT maxItems file_import one file 100,000 chars ok file_import one file 100,001 chars REJECT maxLength file_import file with mode '100755' REJECT additionalProperties file_import delete: {'path','delete':true} REJECT required file_import idempotency_key 7 chars REJECT minLength native_publication full pin ok native_publication no expected_composition_oid REJECT required provider_publication expected_change_set_version 0 REJECT minimum provider_publication extra 'draft': false REJECT additionalProperties provider_publication extra 'target_branch' REJECT additionalProperties Una entrada de archivo es path más content, y nada más. No hay bits de modo, así que un script llega sin permiso de ejecución. Tampoco hay borrados, porque un borrado necesitaría una entrada sin contenido. Cada entrada añade o reemplaza un archivo con su texto completo, que además es exactamente lo que el propietario lee en la tarjeta de aprobación.
Hay un límite que el schema no puede expresar: los 200 KB en total que menciona la descripción. maxLength cuenta caracteres. El presupuesto cuenta bytes UTF-8:
JA file: 100,000 chars passes maxLength; UTF-8 = 300,000 bytes Un archivo en japonés que llega al tope de caracteres por archivo ya ocupa una vez y media el presupuesto de todo el commit. Un cliente que solo valide contra el schema va a construir llamadas que el servidor tendrá que rechazar. Cuenta los bytes en el cliente.
¿En cuántas tarjetas de aprobación acaba una importación real?
La parte 3 del script reparte directorios reales en llamadas de importación de menos de 20 archivos y 200 KB, con first-fit por tamaño, y cuenta las propuestas. Cada propuesta es una tarjeta que el propietario tiene que abrir:
GitNest web lib (.ts) 25 files 126,756 chars 127,210 bytes (1.00 B/char) | 2 proposals per proposal (files, KB): 20/115, 5/12 GitNest web components (.tsx) 22 files 274,206 chars 274,819 bytes (1.00 B/char) | 2 proposals per proposal (files, KB): 10/200, 12/75 Team 2 JA articles (.md) 55 files 359,213 chars 662,285 bytes (1.84 B/char) | 4 proposals per proposal (files, KB): 13/200, 16/196, 18/194, 8/73 Los corpus son el código del frontend de osFoundry y los borradores en japonés de este blog, leídos directamente del disco. Los archivos de código pequeños topan primero con el límite de 20 archivos. Los componentes grandes y el texto no latino topan primero con el presupuesto de bytes. Interpretar "KB" como 1,000 o como 1,024 bytes no cambió nada en estos tres conjuntos.
La otra cosa en la que fijarse es el número de commits. Cuatro propuestas son cuatro commits en la rama, aprobados uno a uno, y el propietario puede rechazar cualquiera de ellos. Si los archivos solo tienen sentido juntos, dilo en cada mensaje de commit, o mantén la importación lo bastante pequeña como para que quepa en una sola tarjeta.
¿Qué significa "push only its dedicated branch" en git?
El push al proveedor que hace osFoundry en el servidor no es público. El de la app de escritorio, el que usa el botón Push Change Set Branch de GitNest, sí lo es: gitnest_push_change_set_branch en gitnest_desktop.rs. Recibe los valores que devolvió un preflight de publicación y hace lo siguiente, en este orden:
- Lee la URL de fetch y la URL de push de
originy se niega a seguir si apuntan a repositorios distintos, o si no apuntan al proveedor, owner y repo que indicó el preflight. - Exige que la rama sea exactamente
osfoundry/change-set-<id>, con<id>un UUID. - Exige que el marcador sea exactamente
GitNest-Change-Set: <id>. - Exige que el OID de la composición tenga entre 4 y 64 caracteres hexadecimales y luego ejecuta
git cat-file -e <oid>^{commit}. - Ejecuta
git update-ref refs/heads/<branch> <oid> <old>, donde<old>es lo que contenía la ref al leerla, o vacío si no existía. - Hace push de un único refspec:
git push origin refs/heads/<branch>:refs/heads/<branch>.
El script reconstruye todo esto contra un repositorio bare que hace de GitHub. El clon tiene dos cosas que el propietario no ha subido: una rama local wip/secret-pricing y un commit de más en release/2.4. El commit compuesto se construye sin worktree (un índice temporal, hash-object, update-index --cacheinfo, write-tree, commit-tree):
remote refs before: 3 (dana/refactor-cart, main, release/2.4) `git push --all` would change 2 remote refs: ['release/2.4', 'wip/secret-pricing'] explicit refspec push changed 1 ref: osfoundry/change-set-5b0c2e8e-3f6a-4d2b-9a71-0c4e9d1f7a23 -> 73e59f27bc4f untouched: True; marker in pushed commit: True Esa segunda línea es la razón de ser del refspec explícito. Un push cómodo desde el clon del propietario habría publicado, junto con el change set, un hotfix sin revisar y una rama privada. El refspec explícito mueve una sola ref, cuyo nombre se deriva del ID del change set, así que no puede chocar con la rama de nadie.
¿Qué se rechaza?
branch name 'codex/ci' instead of contract refused: publication branch does not match the preflight contract marker for another change set refused: publication marker does not match the preflight contract OID 'HEAD' instead of hex refused: composition OID must be a hex commit id hex OID that is not in the clone refused: fatal: Not a valid object name deadbeefdeadbeefdeadbeefdeadbeefdeadbeef^{commit} update-ref with a stale expected old value exit 128: fatal: update_ref failed for ref 'refs/heads/osfoundry/change-set-5b0c2e8e-3f6a- push URL differs from fetch URL refused: origin fetch and push remotes point at different repositories El caso de "HEAD" parece quisquilloso, pero no lo es. Un nombre simbólico se resuelve a lo que esté en checkout en el momento del push, que no tiene por qué ser lo que se compuso y se revisó. Un OID hexadecimal solo puede referirse a un commit. El update-ref con un valor antiguo desactualizado es el compare-and-swap del propio git: si la rama local se movió entre la lectura y la escritura, la escritura falla en lugar de sobrescribirla en silencio. La comprobación de URLs separadas cierra un agujero más discreto: git remote set-url --push puede mandar los push a un sitio distinto del que vienen los fetch, y el código exige que ambas URLs apunten al mismo repositorio.
¿Qué aporta la recomprobación al aprobar?
Los pasos de git protegen el push. Los dos campos expected_ protegen la decisión. La parte 5 del script reconstruye la recomprobación a partir de las descripciones de las herramientas ("Approval rechecks permissions, risk policy, required checks, and target commit"):
as proposed publish recomposed to v4 before the click refused: change set is v4, card says v3 lint re-ran and failed refused: required checks not passed: ['lint'] Codex leyó la versión 3 y el commit 73e59f27bc4f…, y la tarjeta del propietario muestra ambos. Si alguien recompone el change set entretanto, aprobar la tarjeta publicaría algo que nadie ha revisado, así que la propuesta falla. La versión sola no basta, porque dos composiciones de la misma versión podrían ser distintas. El OID solo tampoco, porque no dice a qué edición del change set se refería el propietario. Fijar ambos le cuesta al cliente dos campos.
¿Cuánto cuesta?
20 runs, local bare remote: compose median 114.2 ms, checks+update-ref+push median 140.0 ms (max 430.0) Las seis comprobaciones y el push añaden unos 140 ms contra un remoto bare local. Con un proveedor real, lo que domina es el viaje de ida y vuelta por la red. El script completo, incluidas 5 llamadas reales a tools/list y 21 publicaciones, tardó 11.7 s de tiempo real y 3.5 s de CPU de usuario.
Si lo montas por tu cuenta
- Deja que el agente nombre el change set y lo que observó (versión y commit), nunca la rama, el título, el destino ni el token. Deriva el nombre de la rama del ID del change set.
- Haz push con un único refspec explícito. Nunca
--all,--mirrorni ungit pusha secas desde un clon que también usa una persona. - Escribe la ref local con un valor antiguo esperado, y exige que las URLs de fetch y push apunten al mismo repositorio.
- Acepta solo OIDs hexadecimales para cualquier cosa que se vaya a publicar. Resuelve los nombres simbólicos antes de la revisión, no después.
- Expresa el presupuesto de importación en bytes y compruébalo en el cliente. Los límites de caracteres del schema no detectan el texto no latino.
- Marca con
openWorldHint: truelas herramientas que llegan a otras personas, para que el aviso de aprobación del propio cliente salte antes que el tuyo.
Cómo encajan los change sets, las comprobaciones y la publicación dentro de un repositorio se explica en la página de GitNest, y la parte del protocolo está en la entrada del glosario sobre MCP.

