Repository navigation
TigerGit Phase-2 /api services — Repo + Release (TIGER-367) - #358
Conversation
…credentials, data model (TIGER-334) Epic TIGER-334's Phase-1 substrate for the Tiger_Git module: version a custom module in a private repo, cut a release through QA, and receive approved updates — no local machine, no terminal, and (on shared hosting with exec disabled) no git binary. Ships the foundation only; the concrete transports + /api services + TigerPanel UI are Phase 2+. - Tiger_Git (facade): constants, semver/host-allowlist/repo-slug validators, RESOURCE_RELEASE capability resource (owner-only tag/submit, per TIGER-335 — never a role-string compare). - Tiger_Git_Transport_Interface: the one contract ShellTransport + ApiTransport implement with equivalent results; fast-forward-only, allowlisted-host-only, structured secret-free failures. - Tiger_Git_Environment (TIGER-363): capability detection (git binary + exec usable + outbound) → recommended transport; ApiTransport needs only outbound. Network probe behind a test seam. - Tiger_Git_Credential (TIGER-365): write-only credential manager — encrypt-at-rest via Tiger_Crypto, resolve-for-transport-only (no controller/log path), rotate (new before old revoked), revoke, and an Ed25519 deploy-key generator. Supports a Tiger_Secret ref too. - Data model (TIGER-366, migration 20261021000001): git_repo_connection, git_credential, git_operation_log (append-only audit), module_release, module_install_pin — standard columns, ACTIVE-ONLY unique indexes so a reconnect after disconnect is never blocked. - Tiger_Git_WorkingTree + Tiger_Git_Exception value objects; module scaffold (module.json, Bootstrap, acl.ini contract stub); CAPABILITIES.md regenerated. Tests: 19 unit + 12 integration (proven against real MariaDB) — path-escape refusal, env decision logic, keygen framing, credential write-only/round-trip/rotate/revoke, active-only unique freeing on soft-delete, and the model finders. Full unit suite stays green (1150). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
Matches the TigerShop/TigerServer product-name convention. Slug (`git`), table prefix (`git_*`), and class names (Tiger_Git_* / Git_*) are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
…API, no .git (TIGER-364)
Phase 2 leads with the module's biggest risk: committing with no local git repo. Tiger_Git_Transport_Api
implements the commit path through the GitHub Git Data API:
1. read the branch head (ref) + its base tree
2. POST a blob per changed file (base64); a null-sha entry per deleted file
3. POST a new tree (on base_tree) then a new commit (parent = the old head)
4. fast-forward the branch ref (force:false) — a branch that moved under us is reported
NON_FAST_FORWARD, never clobbered
With the API transport there is no local index, so commit() writes to the remote in one flow and push()
is a verified no-op (committing IS pushing). Also implemented: branches(), createBranch() (refuses the
default branch). status/diff/switch/pull/tag/listReleases/fetchArchive throw NOT_IMPLEMENTED — the rest
of TIGER-364.
The HTTP layer is a seam (setHttp), the token is resolved per request via Tiger_Git_Credential (never
held), and every call is pinned to api.github.com. Errors map to stable, secret-free codes
(401->CREDENTIAL_INVALID, 403 rate->RATE_LIMITED, 404->INVALID_REF, 422 'fast forward'->NON_FAST_FORWARD).
Tests: 10 unit (29 across the Git suite) — the full four-step sequence + payloads asserted against canned
GitHub responses (no network), plus NON_FAST_FORWARD, missing-credential (no call made), nothing-to-commit,
empty-message, createBranch, branches, push no-op, and the NOT_IMPLEMENTED guards. CAPABILITIES.md regenerated.
Built on the Phase-1 substrate (PR #356); rebase onto main once that merges. A live-GitHub integration
test (real throwaway repo + token) is the Testing-column gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
The counterpart to ApiTransport, behind the same interface: git operations driven by the `git` binary over a real .git working tree, for a host that has git and can spawn a subprocess. Host-agnostic (works against any git remote, not just GitHub). Implemented: status (branch + porcelain changes + ahead/behind vs upstream), diff (numstat, size-capped), commit (stage -> -c author commit -> rev-parse; NOTHING_TO_COMMIT when empty), branches, createBranch (off HEAD, refuses the default), switchBranch (refuses a dirty tree -> DIRTY_TREE), push (origin, NO --force ever -> NON_FAST_FORWARD on rejection), pull (--ff-only), tag (annotated + pushed to origin). listReleases/fetchArchive are a provider-API concern (GitHub adapter routes them to ApiTransport) -> NOT_IMPLEMENTED. Safety, structural: git is invoked via proc_open with an ARGUMENT ARRAY (no shell string, no metacharacter interpretation); only a fixed subcommand set runs; every path is resolved under the module root first; ref/branch/tag names are validated (no leading '-', no '..'); a token is injected through an inline credential.helper that reads GIT_TOKEN from the ENV, so the secret is never in argv (ps-visible) nor written to .git/config. A command-runner seam (setRunner) allows stubbing. Tests: 12, proven END TO END against a real local repo + a bare file:// "remote", with a second clone used to force a genuine NON_FAST_FORWARD (push refused, never forced) and to verify push/tag actually reach the remote. Skipped where git is absent. Full Git suite 41 green; full unit suite 1172 green. CAPABILITIES.md regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
…PI (TIGER-364) Tiger_Git_Provider_GitHub composes the two transports into one object a service uses without caring: it picks the WORKING transport for a connection (the git binary where the host allows it, the API otherwise) for the git ops, and ALWAYS routes the provider-API ops (listReleases/fetchArchive) to the API — because a GitHub release is not a git concept, so even a shell connection reaches it over the API. New Tiger_Git_Provider_Interface is the N-provider seam (GitLab/Bitbucket land later without touching the transports). Implements the two provider-API verbs on ApiTransport (their reason to exist): - listReleases() — newest first, drafts excluded. - fetchArchive() — GitHub's tarball 302 captured; the redirect host is confirmed against the allowlist before the CDN fetch, and our token is never forwarded to the signed codeload URL. send() now captures response headers for the capture. Remaining ApiTransport verbs (status/diff/switch/pull/tag over the API) stay NOT_IMPLEMENTED — the rest of TIGER-364. 10 new unit tests (routing proven with both seams; releases/archive incl. the 302 capture, host-allowlist refusal, bad-tag refusal). 68 Git, 1181 unit green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
The /api surface over the transports + GitHubAdapter. Deny-by-default acl.ini, one rule per privilege (TIGER-335); Git_Service_Base shares the connection / working-tree / adapter / audit-log / error-mapping plumbing (adapter behind a static test seam, no network). Git_Service_Repo (admin): connect / status / recheck / list / log / disconnect. connect stores the token write-only (Tiger_Git_Credential), detects the transport, and VERIFIES against the real repo via listReleases — always a remote authenticated GET (the adapter routes releases to the API, so it proves the token+repo on BOTH transports, unlike a local `branches`) — BEFORE the connection is recorded; a bad token revokes the just-stored credential and leaves nothing. A connection carries no secret. Reconnect-after-disconnect is unblocked by the active-only unique. status degrades gracefully (status_available:false) on a transport that can't yet. Git_Service_Release (admin reaches it; tag/submit are superadmin-only via the Tiger_Git::RESOURCE_RELEASE capability — named for the capability, not a role tier): tag / submit / decide (QA approve|reject) / list. A released version is immutable (tag refuses reuse). Folds in the remaining TIGER-364 verb the Release service needs: ApiTransport::tag() over the Git Data API (annotated tag object → ref; returns the tagged commit sha, so both transports agree). status/diff/switch/pull over the API remain NOT_IMPLEMENTED. Tests: 14 integration (connect/verify/no-secret/reconnect/disconnect/ACL-refusal; owner-only capability gate — admin refused, superadmin allowed — submit→decide QA, semver + immutable-tag guards) + 1 unit (tag-over-API). 1182 unit, 14 integration green; CAPABILITIES 244 classes. Admin UI (TigerPanel) + Git_Service_Update (receive-updates) are the next slices. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
Git_Service_Update (admin): check / apply / setPolicy / rollback — the consumer side of the module. - check: which connected modules have a newer APPROVED release (one module or the whole org); respects up-to-date and the pinned channel. - apply: installs ONLY an approved release at its exact tag (never an arbitrary branch/version) — fetchArchive (adapter) → the shared installer tail (Tiger_Module_Installer::installFromTarball, force/update-in-place: backup → extract → place → migrate → publish → record). Moves the pin and records previous_version as the rollback anchor; refuses an unapproved version and the version a rollback pinned. - setPolicy: auto | notify | pinned (+ pinned_version). - rollback: restores previous_version (which must still be approved) and PINS the version it fled, so auto-update never immediately reapplies a bad release. The archive download and the install step both sit behind Base seams (setAdapterFactory / setInstaller), so the tests need no network and never touch the modules dir. acl.ini: Git_Service_Update per privilege → admin. i18n en. Tests: 10 integration (pending/up-to-date/pinned check; apply moves the pin + installs force, refuses unapproved + pinned; setPolicy; rollback restores + pins the fled version, refuses with no previous). 36 Git integration + 1182 unit green. Completes the Phase-2 /api service surface (Repo + Release + Update). TigerPanel admin UI is the next slice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
|
The archive download and the install step sit behind Base seams ( Tests: 10 new integration. 36 Git integration + 1182 unit green. PR now covers Repo + Release + Update — the full Phase-2 |
The human interface over the same /api services an agent already drives — plus the MCP wiring so "run the module" works over API, MCP, and the UI alike. Admin UI (ADMIN.md): Git_AdminController + /git/admin, admin-nav item, admin-gated in acl.ini. One dashboard screen, client/server: the controller renders the shell (connect + submit forms for their fields + CSRF); git.admin.js draws the connections and drives every action over /api — connect, check/apply/rollback + policy, tag → submit → QA approve/reject, re-check, activity log, disconnect. Bootstrap/PUMA only (no inline script/style), TigerButton busy states, TigerDOM feedback, TigerModal confirm on disconnect, escaped output, aria-labels on icon buttons. API + MCP: the /api surface already existed (the services); this makes it MCP-runnable out of the box — `git` added to Tiger_Mcp_Token::DEFAULT_MODULES (admin-gated, so it only surfaces for an admin-role token) so all 14 git tools reflect into tools/list and tools/call proxies to /api with no new code. The two form-backed tools get typed MCP inputSchemas via @APIrequest (Git_Form_Connect / Git_Form_Submit), like the other curated create/edit tools. Tests: +1 controller dispatch test (shell renders, forms handed to the view). 37 Git integration + 1182 unit green; Mcp suite green with git in the default set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
|
Admin UI + MCP reach landed ( Admin UI (ADMIN.md) — API — already there: the MCP — made runnable out of the box: Tests: +1 controller dispatch test. 37 Git integration + 1182 unit green; Mcp suite green with git in the default set. This completes the TigerGit build on this branch: foundation → transports → GitHubAdapter → Phase-2 services (Repo/Release/Update) → admin UI + MCP. Remaining on the epic: the live-GitHub Testing-gate run (needs a throwaway private repo + token), and status/diff/pull over the API (the optional reconciler). |
…e API (TIGER-364) Completes the ApiTransport: the four verbs that were NOT_IMPLEMENTED now work with no local .git, so an exec-disabled shared host gets full authoring parity. The design problem: with no local repo there is no index or HEAD, so "what did I change" can't be told from "the remote moved." The transport solves it with a tiny merge-base MARKER — `.tiger-git/head`, the last commit the working tree was synced to, written by commit() and pull(). Working-tree changes = on-disk files hashed as git blobs vs the base tree (A/M/D). `behind` = GitHub's compare API (base...head ahead_by); `ahead` is always 0 (no local-only commits on this transport). - status: branch + ahead/behind + A/M/D changes + clean; resilient to a stale base. - diff: per-file added/removed via a bounded line-LCS (rolling rows), binary flagged. - pull: fast-forward that PRESERVES non-conflicting local edits; a file changed both locally and remotely is a MERGE_CONFLICT (tree untouched, conflicting paths reported). - switchBranch: materialises the target tree to disk (the no-.git checkout), refuses a dirty tree, resets the base marker. The marker dir is never committed (commit() takes explicit paths) and is skipped by the working-tree walk; walks are capped (MAX_FILES) so a pathological tree can't hang the host. Repo_Service::status now really reports the working tree over an API connection (the old status_available:false path is gone). Tests: +5 unit (ApiTransportReconcileTest: A/M/D + behind, line counts, FF pull preserving local edits, MERGE_CONFLICT, dirty-switch refusal); removed the obsolete "deferred NOT_IMPLEMENTED" assertion; updated the service status test. 1186 unit + 37 Git integration green. This retires the last epic item: TigerGit is feature-complete (foundation → transports → GitHubAdapter → Repo/Release/Update services → admin UI + MCP → the API reconciler). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
|
No- The design problem: with no local repo there's no index or HEAD, so "what did I change" can't be told from "the remote moved." Solved with a tiny merge-base marker —
The marker dir is never committed ( Tests: +5 unit ( TigerGit is now feature-complete across the stack (#356→#357→#358): foundation → transports → GitHubAdapter → Repo/Release/Update services → admin UI + MCP → the API reconciler. (The reconciler is unit-proven against a canned fixture; a live-GitHub check is available on request.) |
…rep 1.38.0
CI fix (the real one): IntegrationTestCase migrates core-only, so the git integration
tests created the git tables via direct SQL with NO tiger_migration ledger entry. git
is the first bundled tiger-core module with its own migrations/ dir, so
InstallerLifecycleTest/InstallerCoverageTest (installFromTarball → _migrate(), which
now scans modules/git/migrations) saw the git migration as un-run and collided with
the existing tables ("1050 Table 'git_repo_connection' already exists"). Fix: the git
tests now apply the schema THROUGH Tiger_Db_Migrator, which records the version in the
ledger, so a later _migrate() skips it (idempotent). Verified git + installer suites
coexist (55/55 in one process).
Release prep: Tiger_Version 1.37.0 → 1.38.0 (the TigerGit epic is @SInCE 1.38.0) +
CHANGELOG 1.38.0 section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
The
/apiservice surface over the transports + GitHubAdapter (stacked on #357 → #356). Deny-by-defaultacl.ini, one rule per privilege (TIGER-335).Git_Service_Baseshares the connection / working-tree / adapter / audit-log / error-mapping plumbing (the adapter sits behind a static test seam, so the integration tests run with no network).Git_Service_Repo(admin)connect·status·recheck·list·log·disconnect.Tiger_Git_Credential), detects the transport, and verifies against the real repo vialistReleases— always a remote authenticated GET (the adapter routes releases to the API), so it proves the token + repo on both transports, unlike a localbranches— before the connection row is recorded. A bad token revokes the just-stored credential and leaves nothing behind.status_available:false) on a transport that can't do it yet (the API — the reconciler is later work).Git_Service_Release(admin reaches it; tag/submit are superadmin-only)tag·submit·decide(QA approve/reject) ·list.tag/submitrequire thetag/submitprivilege on theTiger_Git::RESOURCE_RELEASEcapability (granted to superadmin, named for the capability not a role tier, TIGER-335) — so an ordinary admin who can read releases still cannot cut one.tagrefuses a version already taken by a live release.Folded-in TIGER-364 remainder
ApiTransport::tag()over the Git Data API (annotated tag object → ref; returns the tagged commit sha, so both transports agree) — the one remaining verb the Release service needs.status/diff/switch/pullover the API remainNOT_IMPLEMENTED.Tests
14 integration (connect/verify/no-secret/reconnect/disconnect/ACL-refusal; the owner-only capability gate — admin refused, superadmin allowed — the submit→decide QA flow, semver + immutable-tag guards) + 1 unit (tag-over-API). 1182 unit, 14 integration green; CAPABILITIES 244 classes.
Admin UI (TigerPanel) +
Git_Service_Update(receive-updates: check/apply/setPolicy/rollback) are the next slices.🤖 Generated with Claude Code
https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ