Das ist das Tool, das Codex CLI aufruft, wenn es ein Change Set nach GitHub oder GitLab bringen will, so wie der MCP-Server von osFoundry es jedem Client auflistet, ob angemeldet oder nicht:
{"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"]}]} Interessant ist, was fehlt. Kein Branch-Name, kein Titel, kein Ziel-Branch, kein Draft-Flag, kein Token. Der Client nennt ein Change Set und zwei Fakten, die er darüber beobachtet hat. Alles andere kommt von der Seite des Owners, wenn er freigibt. "Push only its dedicated branch" ist das Versprechen, das dieser Post auseinandernimmt. Außerdem geht es um das Tool, das normalerweise vorher kommt, propose_gitnest_file_import, und darum, was sein Budget von 200 KB für echte Dateien bedeutet.
Auf diesem Mac ist kein codex-Binary installiert, also übernimmt ein Python-Skript die Client-Seite, ausgeführt hier (Apple M1, Python 3.12 über uv, git 2.39.5). Die Netzwerkwerte wurden aus Japan gemessen.
$ 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 (Die Listen der Pflichtfelder sind in der Ausgabe oben gekürzt. Die Tokens stammen aus cl100k_base und sind für den eigenen Tokenizer von Codex daher nur ein Näherungswert.) Die vier Tools kosten weniger als 1,000 der 12,662 Tokens der Liste. Eine Codex-Konfiguration, die sie in enabled_tools einträgt, hält die übrigen 55 aus dem Prompt heraus.
Warum sind beide Publication-Tools "open world", der Import aber nicht?
openWorldHint sagt dem Client, ob ein Tool über ein geschlossenes System hinausgreift. Der Datei-Import schreibt in ein GitNest-Repository, das nur das Projekt sieht. Ein Pull Request, auch ein nativer, ist etwas, über das andere Leute benachrichtigt werden und auf das sie reagieren. Codex nutzt solche Annotations, um zu entscheiden, wann es anhält und nachfragt. Die Hints des Servers sind also das erste Glied der Freigabekette, noch vor der eigenen Karte von osFoundry.
Was kann ein Datei-Import transportieren?
Das meiste beantwortet das Schema. Zwölf Fälle gegen die echten inputSchemas, validiert mit 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 Ein Datei-Eintrag besteht aus path und content, sonst nichts. Keine Mode-Bits, ein Skript landet also nicht ausführbar. Keine Löschungen, denn dafür bräuchte es einen Eintrag ohne Inhalt. Jeder Eintrag legt eine Datei mit ihrem vollständigen Text an oder ersetzt sie, und genau das liest der Owner auch auf der Freigabekarte.
Ein Limit kann das Schema nicht ausdrücken: die 200 KB insgesamt aus der Beschreibung. maxLength zählt Zeichen. Das Budget zählt UTF-8-Bytes:
JA file: 100,000 chars passes maxLength; UTF-8 = 300,000 bytes Eine japanische Datei an der Zeichengrenze pro Datei ist bereits anderthalbmal so groß wie das Budget des ganzen Commits. Ein Client, der nur gegen das Schema prüft, baut Aufrufe, die der Server ablehnen muss. Also die Bytes auf dem Client zählen.
Wie viele Freigabekarten ergibt ein echter Import?
Teil 3 des Skripts packt echte Verzeichnisse per First-Fit nach Größe in Import-Aufrufe unter 20 Dateien und 200 KB und zählt die Proposals. Jedes Proposal ist eine Karte, die der Owner öffnen muss:
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 Die Korpora sind die Frontend-Quellen von osFoundry selbst und die japanischen Entwürfe dieses Blogs, direkt von der Platte gelesen. Kleine Quelldateien stoßen zuerst an die Grenze von 20 Dateien. Große Komponenten und nicht-lateinischer Text stoßen zuerst an das Byte-Budget. Ob man "KB" als 1,000 oder 1,024 Bytes liest, änderte bei diesen drei Sets nichts.
Das andere, worauf man achten sollte, ist die Zahl der Commits. Vier Proposals heißen vier Commits auf dem Branch, einzeln freigegeben, und der Owner kann jeden davon ablehnen. Wenn die Dateien nur zusammen Sinn ergeben, sollte das in jeder Commit-Message stehen, oder der Import bleibt klein genug für eine einzige Karte.
Was bedeutet "push only its dedicated branch" in git?
Der serverseitige Provider-Push von osFoundry ist nicht öffentlich. Der Push der Desktop-App, den der Button Push Change Set Branch in GitNest verwendet, schon: gitnest_push_change_set_branch in gitnest_desktop.rs. Er nimmt die Werte, die ein Publication-Preflight zurückgegeben hat, und macht in dieser Reihenfolge Folgendes:
- Liest Fetch-URL und Push-URL von
originund bricht ab, wenn sie auf verschiedene Repositories zeigen oder nicht auf den Provider, Owner und das Repo, die der Preflight genannt hat. - Verlangt, dass der Branch exakt
osfoundry/change-set-<id>heißt, mit<id>als UUID. - Verlangt, dass der Marker exakt
GitNest-Change-Set: <id>lautet. - Verlangt, dass die Composition-OID aus 4 bis 64 Hex-Zeichen besteht, und führt dann
git cat-file -e <oid>^{commit}aus. - Führt
git update-ref refs/heads/<branch> <oid> <old>aus, wobei<old>der Wert ist, den die Ref beim Lesen hatte, oder leer, falls sie nicht existierte. - Pusht genau eine Refspec:
git push origin refs/heads/<branch>:refs/heads/<branch>.
Das Skript baut das gegen ein Bare-Repository nach, das GitHub vertritt. Der Clone enthält zwei Dinge, die der Owner nicht gepusht hat: einen lokalen Branch wip/secret-pricing und einen zusätzlichen Commit auf release/2.4. Der komponierte Commit entsteht ohne Worktree (ein Wegwerf-Index, 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 Die zweite Zeile ist der ganze Grund für die explizite Refspec. Ein bequemer Push aus dem Clone des Owners hätte zusammen mit dem Change Set einen ungeprüften Hotfix und einen privaten Branch veröffentlicht. Die explizite Refspec bewegt genau eine Ref, deren Name aus der Change-Set-ID abgeleitet ist, und kann daher mit niemandes Branch kollidieren.
Was wird abgelehnt?
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 Der Fall "HEAD" wirkt pedantisch, ist es aber nicht. Ein symbolischer Name löst sich zu dem auf, was zum Zeitpunkt des Pushs ausgecheckt ist, und das muss nicht das sein, was komponiert und geprüft wurde. Eine Hex-OID kann nur einen Commit meinen. Das veraltete update-ref ist das Compare-and-Swap von git selbst: Hat sich der lokale Branch zwischen Lesen und Schreiben bewegt, schlägt das Schreiben fehl, statt ihn stillschweigend zu ersetzen. Die Prüfung auf getrennte URLs schließt eine unauffälligere Lücke: git remote set-url --push kann Pushes an einen anderen Ort schicken als den, von dem gefetcht wird, und der Code verlangt, dass beide auf dasselbe Repository zeigen.
Was bringt die erneute Prüfung bei der Freigabe?
Die git-Schritte schützen den Push. Die beiden expected_-Felder schützen die Entscheidung. Teil 5 des Skripts rekonstruiert die erneute Prüfung aus den Tool-Beschreibungen ("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 hat Version 3 und Commit 73e59f27bc4f… gelesen, und die Karte des Owners zeigt beides. Komponiert jemand das Change Set in der Zwischenzeit neu, würde die Freigabe der Karte etwas veröffentlichen, das niemand angesehen hat, also schlägt das Proposal fehl. Die Version allein reicht nicht, weil zwei Compositions derselben Version sich unterscheiden können. Die OID allein reicht auch nicht, weil sie nichts darüber sagt, welchen Stand des Change Sets der Owner gemeint hat. Beides festzunageln kostet den Client zwei Felder.
Was kostet das?
20 runs, local bare remote: compose median 114.2 ms, checks+update-ref+push median 140.0 ms (max 430.0) Die sechs Prüfungen und der Push kosten gegen ein lokales Bare-Remote etwa 140 ms. Bei einem echten Provider dominiert der Netzwerk-Roundtrip. Das ganze Skript, inklusive 5 echter tools/list-Aufrufe und 21 Publications, brauchte 11.7 s Wall-Clock-Zeit und 3.5 s User-CPU.
Wenn man das selbst verdrahtet
- Den Agent das Change Set und das Beobachtete (Version und Commit) benennen lassen, nie Branch, Titel, Ziel oder Token. Den Branch-Namen aus der Change-Set-ID ableiten.
- Mit genau einer expliziten Refspec pushen. Nie
--all,--mirroroder ein nacktesgit pushaus einem Clone, den auch ein Mensch benutzt. - Die lokale Ref mit einem erwarteten alten Wert schreiben und verlangen, dass Fetch- und Push-URL dasselbe Repository bezeichnen.
- Für alles, was veröffentlicht wird, nur Hex-OIDs akzeptieren. Symbolische Namen vor dem Review auflösen, nicht danach.
- Das Import-Budget in Bytes angeben und auf dem Client prüfen. Zeichenlimits im Schema erwischen nicht-lateinischen Text nicht.
- Tools, die andere Menschen erreichen, mit
openWorldHint: truemarkieren, damit schon der Freigabedialog des Clients anspringt, bevor der eigene kommt.
Wie Change Sets, Checks und Publication in einem Repository zusammenspielen, steht auf der GitNest-Seite, die Protokollseite im Glossareintrag zu MCP.
