Skip to content

TigerGit Phase-2 /api services — Repo + Release (TIGER-367) - #358

Merged
WebTigers merged 10 commits into
mainfrom
feat/tiger-367-git-services
Oct 21, 2026
Merged

WebTigers merged 10 commits into
mainfrom
feat/tiger-367-git-services

Conversation

@WebTigers

Copy link
Copy Markdown
Owner

The /api service surface over the transports + GitHubAdapter (stacked on #357 → #356). 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 (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.

  • 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 row is recorded. A bad token revokes the just-stored credential and leaves nothing behind.
  • A connection carries no secret (only a credential ref, which isn't exposed either).
  • Reconnect-after-disconnect is unblocked by the active-only unique.
  • status degrades gracefully (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/submit require the tag/submit privilege on the Tiger_Git::RESOURCE_RELEASE capability (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.
  • A released version is immutable — tag refuses 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/pull over the API remain NOT_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

WebTigers and others added 7 commits October 21, 2026 13:07
…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
@WebTigers

Copy link
Copy Markdown
Owner Author

Git_Service_Update landed (6e4480b) — the receive-updates side completes the Phase-2 service surface.

check · apply · setPolicy · rollback (admin).

  • check — which connected modules have a newer approved release (one module or org-wide); 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, 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 sit behind Base seams (setAdapterFactory / setInstaller), so the tests need no network and never touch the modules dir.

Tests: 10 new integration. 36 Git integration + 1182 unit green. PR now covers Repo + Release + Update — the full Phase-2 /api surface. TigerPanel admin UI is the next slice.

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
@WebTigers

Copy link
Copy Markdown
Owner Author

Admin UI + MCP reach landed (cd8af71) — the module is now runnable three ways: the admin screen, /api, and MCP.

Admin UI (ADMIN.md) — Git_AdminController at /git/admin, an admin-nav item, admin-gated. One client/server dashboard: 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 + update-policy, tag → submit → QA approve/reject, re-check env, 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 — already there: the Git_Service_* methods are the /api surface (an operator here and an agent over /api drive the same services).

MCP — made 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 (repo/connect, release/submit) carry typed MCP inputSchemas via @apiRequest.

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
@WebTigers

Copy link
Copy Markdown
Owner Author

No-.git reconciler landed (792d686) — status / diff / pull / switchBranch over the API now work, completing the ApiTransport (the last TIGER-364 piece; it rides this branch because the service status test depends on it).

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 — .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 comes from GitHub's compare API; ahead is always 0 (no local-only commits on this transport).

  • status — branch + ahead/behind + A/M/D + clean; resilient to a stale/GC'd 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.

The marker dir is never committed (commit() takes explicit paths) and is skipped by the walk; walks are capped so a pathological tree can't hang the host. Git_Service_Repo::status now really reports the working tree over an API connection.

Tests: +5 unit (ApiTransportReconcileTest: A/M/D + behind, line counts, FF pull preserving local edits, MERGE_CONFLICT, dirty-switch refusal); updated the service status test. 1186 unit + 37 Git integration green.

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
@WebTigers
WebTigers changed the base branch from feat/tiger-364-git-transports to main October 21, 2026 21:44
@WebTigers
WebTigers merged commit 563f927 into main Oct 21, 2026
14 checks passed
@WebTigers
WebTigers deleted the feat/tiger-367-git-services branch October 21, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant