Skip to content

feat(cli,core): identidad per-repo (D1) — LOGIN-T1..T3, PKP-H1..H4 y precedencia state-branch - #167

Merged
cagodoy merged 17 commits into
mainfrom
feat/mvp-blockers-s78
Aug 5, 2026
Merged

feat(cli,core): identidad per-repo (D1) — LOGIN-T1..T3, PKP-H1..H4 y precedencia state-branch#167
cagodoy merged 17 commits into
mainfrom
feat/mvp-blockers-s78

Conversation

@cagodoy

@cagodoy cagodoy commented Aug 5, 2026

Copy link
Copy Markdown
Member

Public side of the mvp_blockers epic (s78/s78b). Companion to gitgovernance/private#42, which carries the specs, the SaaS side and the private E2E suite.

What's in

Per-repo identity (decision D1: human keys per-org, agent keys PER-REPO):

  • PKP-H1..H4 — repo scope in PrismaKeyProvider (core)
  • LOGIN-T1 — login uploads the repo's agent keys in the same syncKey request
  • LOGIN-T2 — the decision to upload agent keys becomes independent of the human path (local-vs-server diff); closed the already-synced gap, where a post-succession repo would never converge
  • LOGIN-T3--force-cloud uploads the diff too, using the freshly downloaded canonical publicKey (with the stale one the server answers CONFLICT). This is the correct path for the most common scenario: a dev running init on a new machine against an org that already has a canonical key
  • RSIG-A5RecordSigner rejects empty or whitespace-only notes

--state-branch precedence (Task 1.2d) — product bug surfaced by the smoke:

Command registration built the projector eagerly (index.ts:60), which triggered initializeStores()bootstrapWorktree() before Commander had parsed any option. With origin/gitgov-state present, the worktree ended up bound to the DEFAULT and that got persisted into config.json: gitgov init --state-branch <custom> inherited another project's actors and state with no error, and the contamination stuck for every subsequent command. LOGIN-P1 was exposed to the same pattern.

Root cause measured with GIT_TRACE (same singleton instance across all 5 calls: it was ordering, not identity). The fix is structural: registration receives a thunk and resolution happens in the handler, once the command has set its override — it no longer depends on who runs first. Class sweep: setupCommands is left without a single await, and none of the 16 register* calls receive pre-built instances.

Tests

  • [EARS-C3] unit in cli/src/commands/indexer/indexer.test.ts
  • [INIT-L3] e2e in e2e/tests/init_state_branch.test.ts — local bare remote, no GitHub, ~5s, CI-friendly

Gates: CLI 621 passed / 31 files · tsc 0 · zero forbidden casts in touched files · the 3 private flows that were red from this cause (sync_state, golden_path, webhook_recovery) recovered 3/3 without touching their tests — they had been right since gitgov_2.

Reviewer note

The packages/private pointer is NOT bumped in this PR (the bump was made and deliberately reverted: that call is Camilo's, handled separately). The matching content is pushed in the private repo; without the bump, this branch references a commit that predates those specs.

cagodoy added 17 commits August 3, 2026 17:59
…igner

Guard en createSignedRecord y signRecord: notes es parte del digest de firma y
el schema del protocolo exige minLength:1 — un notes vacio produce records que
firman OK pero fallan lint --strict (corrupcion silenciosa que bloquea sync
push; origen: 9 FeedbackRecords corruptos en QA). Validacion pre-sign: el
keyProvider nunca llega a invocarse. 9/9 tests.
Bloque 2 de agent_key_per_repo_model (D1: human keys per-org, agent keys
PER-REPO — RFC-02 §6.2):
- Constructor gana repoId? opcional. agent:* con scope resuelve/persiste por
  (orgId, repoId, actorId); human:* ignora el scope (per-org, filas NULL).
- storeKey: el archive/delete transaccional queda scoped por repo — rotar la
  key de un agente en un repo NUNCA archiva ni sobrescribe la de otro repo.
- H4: agente SIN scope filtra repoId null (solo filas legacy) — sin esto, un
  provider unscoped resolveria la key repo-scoped de un repo arbitrario
  (findFirst sin orderBy): no-determinismo silencioso. Las keys repo-scoped
  solo son alcanzables via provider scoped.
- Mock delegate espeja la semantica PG del cuadruple (NULLs distintos).
- audit_types.test.ts: import sin uso eliminado (tsc 0).
Gate: PKP 33/33 + KV e2e 12/12.
Bloque 3 de agent_key_per_repo_model (D1): gitgov login sube las agent:* keys
locales del repo junto con la humana, en el mismo request identity.syncKey —
cada entrada con su propio ephemeral keypair ECDH contra la server key.
- Enumeracion desde los ActorRecords del REPO (FsRecordStore + DEFAULT_ID_ENCODER
  sobre <worktree>/.gitgov/actors) — nunca decodificando filenames del keys dir
  (el mapping :/ -> _ es lossy) ni leyendo un directorio global.
- Hardening: log + skip por entrada — una key ilegible (permisos, corrupcion)
  no deja rehen al login; las demas y la humana siguen (simetrico con IDS-N2).
- Sin agent keys locales: agentKeys se omite (request identico al actual).
- Reporta agentKeysSynced de la respuesta.
Gate: 46 tests + 1 todo, tsc 0, KS9-KS12 e2e 6/6.
…path humano

s78b-15: gap descubierto por ejecucion real — el path already-synced del login
(key humana local == SaaS) retornaba sin subir agent keys, dejando a los repos
post-succession sin converger jamas. Fix simetrico al diseno existente
(status → decidir → sync), aprobado por revision:
- En todo login se computa el diff entre los agent keypairs locales y
  keyStatus.agentKeys (las publicKeys repo-scoped del server, IDS-O1); solo
  las entradas ausentes o distintas viajan como agentKeys[].
- Path already-synced: el diff sube via identity.syncKey en modo verify-only
  (publicKey humana actual SIN privateKeyEnvelope, IKS-G3) — la key humana no
  se retransmite nunca. Diff vacio → cero llamadas extra, request identico.
- Amendment LOGIN-T1: se sube el DIFF, no todo agent:* local — mata la clase
  del bug: cada mitad (humana/agentes) se evalua por su cuenta.
Gate: login-command 49 passed + 1 todo, tsc 0, cero casts nuevos.
…(s78)

- agent_discovery_module: 3 functions (discoverInstalledAgents,
  mergeAgentSources, packageToAgentRecord), 8 EARS DISC-A1..C3, 8 tests
- AORCH-P8: npm install suggestion when agent MODULE_NOT_FOUND
- AORCH-P9: list discovered-but-unregistered agents post-audit
- INIT-M1: agent status display post-init (installed/not-installed)
- INIT-M2: informational status for unresolved default agents
- Fix: stateBranchOverride defaults to 'gitgov-state' instead of throw
  (fixes 12 E2E failures in init/sync commands)
- Core barrel: export discoverInstalledAgents

615 CLI tests green, 3063 core tests green.


Private: agent_platform roadmap/overview updated, 7 specs with new EARS,
recall #3 for handoff, device_security epic docs, agent_contract_reference,
multi_agent_audit_flow spec, input lifecycle folders.

Core: LLM resolve test improvements (other agent stream).
E2E: protocol.prisma sync (other agent stream).
…ded canonical key

Tercer y ultimo camino del login que faltaba cubrir. LOGIN-T2 (s78b-15) dejo
el diff de agent keys en dos de los tres: already-synced y upload humano.
--force-cloud retornaba tras descargar la key sin tocar el diff.

No es un edge case: es el camino CORRECTO del escenario mas comun. Un dev que
hace gitgov init en una maquina nueva mintea un keypair humano nuevo, la org
ya tiene canonica, y el login entra en conflicto LOGIN-D2; adoptar la canonica
es lo que corresponde porque el SaaS es la autoridad de la key humana. Con el
gap, ese repo nunca recibia sus agent keys y los scans fallaban hasta que
algun login futuro tomara otro camino por accidente.

Detalle critico: el publicKey del syncKey verify-only es la canonica RECIEN
DESCARGADA, re-leida del provider despues del download — con la local stale el
server ve mismatch sin envelope y responde CONFLICT. El upload es no fatal:
si falla, warn y seguir; el login, funcion primaria, ya quedo resuelto.

Incluye la eliminacion de fondo de los 2 casts pre-existentes del test:
- as unknown as IKeyProvider tapaba un mock incompleto (4 de 6 metodos) —
  se completaron sign() y deletePrivateKey() y se sello con satisfies.
- as never de process.exit resuelto con mockImplementation(vi.fn()).

Descubierto ejecutando el gate del flow AKR (e2e-private, Bloque 6).
Gate: login CLI 51+1todo, tsc 0, cero casts en el archivo.
…nch override precedence

El registro de comandos construia el projector de forma ansiosa (index.ts:60),
lo que disparaba initializeStores() -> bootstrapWorktree() ANTES de que
Commander parseara ninguna opcion. Con `origin/gitgov-state` presente en el
repo, el bootstrap ataba el worktree al DEFAULT y lo persistia en config.json:
`gitgov init --state-branch <custom>` heredaba actores y estado de otro
proyecto SIN error, y la contaminacion quedaba para todos los comandos
siguientes. LOGIN-P1 estaba expuesto al mismo patron: su override tambien se
setea despues del registro.

Medido con GIT_TRACE: `git branch gitgov-state origin/gitgov-state` +
`git worktree add ... gitgov-state` disparados desde setupCommands, con
override=null e initMode=false; misma instancia del singleton en las 5
llamadas — era ORDEN, no identidad. Repro A/B/C: la unica variable es
`git fetch origin`; sin el, el bootstrap falla y el flujo cae al camino sano,
por eso el bug era invisible en repos limpios y aparecia en cualquier repo real.

El fix es estructural, no de orden afortunado: el registro recibe un thunk y
la resolucion ocurre en el handler, cuando el comando ya fijo su override.
Barrido de la clase: setupCommands quedo sin un solo await y ninguno de los
16 register* recibe instancias construidas.

Specs (privado): INIT-L3, EARS-C3, EARS-C15 amendment, EARS-B2 y WTSYNC-A5/A6.

Tests: unit `[EARS-C3]` (el primer intento fue falso verde — pasaba contra el
codigo roto; el assert que discrimina es que el handler SI resuelve al
ejecutar) + e2e `[INIT-L3]` con bare remote local, sin GitHub, 5s, CI-friendly.

Gates: unit C3 + e2e INIT-L3 + CLI 621 passed / 31 archivos + tsc 0 +
D/E/F 3/3 recuperados sin tocar sus tests.
…tras sesiones)

Material de texto que estaba sin versionar en la raiz del monorepo:

- inputs/ (8): adr_no_sigstore, kms_migration, landing_repositioning,
  marketplace_sandbox, pilot_validation, release_v1, triad_oss_launch,
  verify_command_alias
- landing.md, PLATFORM_REVIEW.md, gitgov_agent_prompt.md,
  gitgov_content_map_diagram.md
- .github/workflows/gitgov.yml — workflow CI
- .gitignore: +packages/triad/ y packages/triad_pi_agent/ (plugin instalado,
  futuro submodulo) · .claudeignore

Sin secretos: verificado contra patrones ghp_/github_pat_/AKIA/BEGIN PRIVATE
KEY/xox*. Quedan FUERA por decision explicita — ver reporte de la sesion:
screenshots de debugging, cli-main/ (proyecto ajeno), .playwright-mcp/
(artefactos de herramienta), packages/iatoolkit y packages/forge_apps
(repos git independientes, y iatoolkit tiene un .env real).
Los 8 inputs de producto de la raiz entraron por error en 6db1ffc. Se sacan
del tracking y quedan en disco (sin borrar) para que su dueño decida donde
viven. El resto de ese commit (workflow CI, .gitignore, .claudeignore y los
4 docs de raiz) se mantiene.
No corresponden al versionado del monorepo (decision de Camilo). Los archivos
siguen en disco, solo salen del control de versiones. Del commit anterior
quedan versionados: inputs/ (8 inputs de producto), .github/workflows/gitgov.yml
y .gitignore.
No debe ir versionado en el monorepo (decision de Camilo, s78b-34). El archivo
queda en el working tree como untracked — no se borra del disco.
…-F1..F4, Task 1.2d, recalls 91/92)"

This reverts commit 23d388a.
@cagodoy
cagodoy merged commit 6331639 into main Aug 5, 2026
3 of 4 checks passed
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 3.21.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant