Questo è il tool che Codex CLI chiama quando vuole portare un change set su GitHub o GitLab, così come il server MCP di osFoundry lo elenca a qualsiasi client, autenticato 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"]}]} Conta soprattutto quello che manca. Niente nome del branch, niente titolo, niente branch di destinazione, niente flag draft, niente token. Il client indica un change set e due fatti che ha osservato su di esso, e tutto il resto arriva dal lato del proprietario, al momento dell'approvazione. "Push only its dedicated branch" è la promessa che questo post smonta pezzo per pezzo. Copre anche il tool che di solito viene prima, propose_gitnest_file_import, e cosa significa il suo budget di 200 KB con file reali.
Su questo Mac non è installato nessun binario codex, quindi il lato client è uno script Python, eseguito qui (Apple M1, Python 3.12 tramite uv, git 2.39.5). I tempi di rete sono stati misurati dal Giappone.
$ 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 (Le liste dei campi obbligatori sono state tagliate dall'output qui sopra. I token sono di cl100k_base, quindi sono un'approssimazione per il tokenizer di Codex.) I quattro tool costano meno di 1,000 token sui 12,662 della lista. Una config di Codex che li elenca in enabled_tools tiene gli altri 55 fuori dal prompt.
Perché entrambi i tool di pubblicazione sono "open world" e l'import no?
openWorldHint dice al client se un tool esce da un sistema chiuso. L'import di file scrive in un repository GitNest che vede solo il progetto. Una pull request, anche nativa, è qualcosa di cui altre persone ricevono notifica e su cui agiscono. Codex usa annotazioni come queste per decidere quando fermarsi e chiedere, quindi gli hint del server sono il primo anello della catena di approvazione, prima ancora della card di osFoundry.
Cosa può contenere un import di file?
Lo schema risponde quasi a tutto. Dodici casi contro gli inputSchema reali, validati 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 voce di file è path più content, e nient'altro. Niente bit di modo, quindi uno script arriva senza permesso di esecuzione. Niente cancellazioni, perché una cancellazione richiederebbe una voce senza contenuto. Ogni voce aggiunge o sostituisce un file con il suo testo completo, che è anche esattamente ciò che il proprietario legge sulla card di approvazione.
C'è un limite che lo schema non riesce a esprimere: i 200 KB totali indicati nella descrizione. maxLength conta caratteri. Il budget conta byte UTF-8:
JA file: 100,000 chars passes maxLength; UTF-8 = 300,000 bytes Un file giapponese al tetto di caratteri per file vale già una volta e mezza il budget dell'intero commit. Un client che controlla solo lo schema costruirà chiamate che il server dovrà rifiutare. Contate i byte lato client.
In quante card di approvazione si trasforma un import reale?
La parte 3 dello script suddivide directory reali in chiamate di import sotto i 20 file e i 200 KB, con first-fit per dimensione, e conta le proposte. Ogni proposta è una card che il proprietario deve aprire:
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 I corpus sono i sorgenti frontend di osFoundry e le bozze giapponesi di questo blog, letti direttamente da disco. I file sorgente piccoli sbattono prima contro il limite di 20 file. I componenti grandi e il testo non latino sbattono prima contro il budget in byte. Leggere "KB" come 1,000 o 1,024 byte non ha cambiato nulla per questi tre insiemi.
L'altra cosa da notare è il numero di commit. Quattro proposte vogliono dire quattro commit sul branch, approvati uno alla volta, e il proprietario può rifiutarne uno qualsiasi. Se i file hanno senso solo insieme, scrivetelo in ogni messaggio di commit, oppure tenete l'import abbastanza piccolo da stare in una sola card.
Cosa significa "push only its dedicated branch" in git?
Il push verso il provider che osFoundry fa lato server non è pubblico. Quello dell'app desktop, usato dal pulsante Push Change Set Branch di GitNest, sì: gitnest_push_change_set_branch in gitnest_desktop.rs. Prende i valori restituiti da un preflight di pubblicazione e fa questo, nell'ordine:
- Legge l'URL di fetch e l'URL di push di
origine si ferma se puntano a repository diversi, o non al provider, all'owner e al repo indicati dal preflight. - Richiede che il branch sia esattamente
osfoundry/change-set-<id>, con<id>un UUID. - Richiede che il marker sia esattamente
GitNest-Change-Set: <id>. - Richiede che l'OID della composizione sia di 4-64 caratteri esadecimali, poi esegue
git cat-file -e <oid>^{commit}. - Esegue
git update-ref refs/heads/<branch> <oid> <old>, dove<old>è il valore che la ref aveva al momento della lettura, o vuoto se non esisteva. - Pusha un solo refspec:
git push origin refs/heads/<branch>:refs/heads/<branch>.
Lo script lo ricostruisce contro un repository bare che fa le veci di GitHub. Il clone contiene due cose che il proprietario non ha pushato: un branch locale wip/secret-pricing e un commit in più su release/2.4. Il commit composto viene costruito senza worktree (un index usa e getta, 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 Quella seconda riga è l'intero motivo del refspec esplicito. Un push comodo dal clone del proprietario avrebbe pubblicato, insieme al change set, un hotfix non revisionato e un branch privato. Il refspec esplicito sposta una sola ref, il cui nome deriva dall'ID del change set, quindi non può scontrarsi con il branch di nessuno.
Cosa viene rifiutato?
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 Il caso "HEAD" sembra pignolo, ma non lo è. Un nome simbolico si risolve in ciò che è in checkout al momento del push, che non è per forza ciò che è stato composto e revisionato. Un OID esadecimale può indicare un solo commit. L'update-ref con un vecchio valore non aggiornato è il compare-and-swap di git stesso: se il branch locale si è spostato tra lettura e scrittura, la scrittura fallisce invece di sovrascriverlo in silenzio. Il controllo sugli URL separati chiude un buco più discreto: git remote set-url --push può mandare i push in un posto diverso da quello da cui arrivano i fetch, e il codice richiede che entrambi indichino lo stesso repository.
Cosa aggiunge il ricontrollo al momento dell'approvazione?
I passaggi git proteggono il push. I due campi expected_ proteggono la decisione. La parte 5 dello script ricostruisce il ricontrollo a partire dalle descrizioni dei tool ("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 ha letto la versione 3 e il commit 73e59f27bc4f…, e la card del proprietario mostra entrambi. Se nel frattempo qualcuno ricompone il change set, approvare la card pubblicherebbe qualcosa che nessuno ha guardato, quindi la proposta fallisce. La sola versione non basta, perché due composizioni della stessa versione potrebbero essere diverse. Nemmeno il solo OID basta, perché non dice a quale modifica del change set si riferiva il proprietario. Fissarli entrambi costa al client due campi.
Quanto costa?
20 runs, local bare remote: compose median 114.2 ms, checks+update-ref+push median 140.0 ms (max 430.0) I sei controlli e il push aggiungono circa 140 ms con un remote bare locale. Con un provider reale, domina il round trip di rete. L'intero script, comprese 5 chiamate reali a tools/list e 21 pubblicazioni, ha impiegato 11.7 s di tempo reale e 3.5 s di CPU utente.
Se lo implementate da soli
- Lasciate che l'agente indichi il change set e ciò che ha osservato (versione e commit), mai il branch, il titolo, la destinazione o il token. Derivate il nome del branch dall'ID del change set.
- Pushate con un solo refspec esplicito. Mai
--all,--mirroro ungit pushsecco da un clone che usa anche una persona. - Scrivete la ref locale con un vecchio valore atteso, e richiedete che gli URL di fetch e push indichino lo stesso repository.
- Accettate solo OID esadecimali per tutto ciò che verrà pubblicato. Risolvete i nomi simbolici prima della review, non dopo.
- Esprimete il budget di import in byte e controllatelo lato client. I limiti in caratteri dello schema non intercettano il testo non latino.
- Marcate con
openWorldHint: truei tool che raggiungono altre persone, così la richiesta di approvazione del client scatta prima della vostra.
Come change set, check e pubblicazione si combinano in un repository è spiegato nella pagina di GitNest, mentre il lato protocollo è nella voce del glossario su MCP.


