GT-603: the audit ledger can finally say whether a human or an agent did it - #81
Open
beyondnetPeru wants to merge 1 commit into
Open
GT-603: the audit ledger can finally say whether a human or an agent did it#81beyondnetPeru wants to merge 1 commit into
beyondnetPeru wants to merge 1 commit into
Conversation
…ico (GT-603) El ledger de turnos agénticos estaba escrito, probado y SIN REGISTRAR: `IAgentExecutionPort` no aparecía en ningún contenedor ni en ningún endpoint, y `/assistant/converse` —la única vía por la que un agente tocaba el corpus de un cliente— proxyaba al runtime sin persistir nada. La afirmación diferencial del producto (distinguir un acto humano de uno agéntico en el expediente) era falsa en el código. Y era la ficha que CADUCA: `audit_entries` es append-only por trigger desde COH-014, así que toda fila escrita antes de que existiera el discriminador queda inatribuible para siempre — el UPDATE que la tiparía está prohibido por la base. - `AuditActor`/`AuditActorType`: human | agent | system, más `unknown` reservado a lo anterior a la migración y EXCLUIDO de toda ruta de escritura. - `AuditEntry.Create` exige la atribución como parámetro obligatorio: los 8 sitios que escriben en el expediente declaran ahora quién escribe. Se añade `Rehydrate` para poder LEER `unknown` sin poder escribirlo. - Migración `AddAuditActorAttribution`: `actor_type` NOT NULL + `agent_id`, `model_id`, `session_id`. El DEFAULT `'unknown'` vive sólo dentro del `ADD COLUMN` (fast default: sin reescritura, sin disparar el trigger) y se RETIRA acto seguido, para que ninguna fila nueva entre sin declarar su actor. Los tres triggers append-only quedan intactos. - `AssistantEndpoints` pasa por el puerto: scope comprobado, turno asentado y sólo entonces se deja actuar al runtime. La respuesta se devuelve intacta. - `AgentTurnAuditor` CONFIRMA la escritura (`SaveEntitiesAsync`): sin ella, «el turno se pudo registrar» era una afirmación sobre memoria que nadie persistía. - Un turno sin identidad de agente se rechaza: no es atribuible, y en un registro que no admite corrección es preferible no escribirlo a escribirlo mal. - `audit-trail.robot.mjs` verifica extremo a extremo, contra la API viva, que un turno agéntico aterriza tipado como `agent` con su agente identificado y que un acto humano en el MISMO expediente sigue diciendo `human`. Verificado: 1034 pruebas + 14 de arquitectura en verde; las pruebas vivas de esquema contra PostgreSQL real (EVOLITH_CORE_LIVE=1) y RoboSoft `audit-trail` 25/25 contra tracker-api en marcha. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes GT-603 (P0). All four acceptance criteria met, each watched red before green.
Why this one was urgent in a way no other row is
audit_entriesis append-only by database trigger. Every row written before the discriminator exists is permanently unattributable — this is the one item on the board that expires instead of accruing cost. The product's differentiating claim (human-versus-agent attribution) was false in code that was already 90% written:AgentExecutionServiceandAgentTurnAuditorexisted, were tested, and appeared in zero DI registrations and zero endpoints.The append-only trigger, probed rather than assumed
Before writing the migration, the trigger was tested against a real PostgreSQL. It forbids UPDATE, DELETE, TRUNCATE — not INSERT, and not DDL. In one transaction:
ADD COLUMN actor_type varchar(32) NOT NULL DEFAULT 'unknown'succeeds (PG≥11 fast default, no rewrite, no DML trigger), whileUPDATE … SET actor_type='human'is rejected:So back-filling is not merely undesirable — it is impossible. Confirmed independently by the integrating session on the live database.
Chosen shape: the
DEFAULTexists only for theADD COLUMNstatement and is dropped immediately after, plus aCOMMENT ON COLUMNrecording the semantics. Old rows readunknown— "not declared, and untypeable" — and any new INSERT omitting the discriminator fails with23502.AuditActorType.WritableexcludesunknownandAuditEntry.Createrejects it, so no application path can produce it;Rehydrateexists so old rows stay readable. The three append-only triggers are untouched, and a live test asserts all three survive the migration.Verified on the live DB by the integrating session:
actor_typeisvarchar(32) NOT NULLwith no default, and the rows readagent 4 · human 6 · unknown 1.Evidence per criterion
AssistantEndpointsgoes through them. RED:...to have an item matching (d.ServiceType == IAgentExecutionPort), andExpected asientos not to be empty. GREEN: 20/20 on those classes (re-run independently), full solution 1034 passed / 0 failed plus 14/14 architecture tests. The endpoint response body is unchanged, asserted by a test.42703: column "actor_type" does not exist. GREEN after20260731021901_AddAuditActorAttribution, generated bydotnet efwith the snapshot untouched by hand.AssistantEndpoints.csand restarting the server:✗ the agent turn is in the trail — the agent path wrote nothing,actorType=undefined,FAIL audit-trail (19 ok, 6 failed). GREEN:PASS audit-trail (25 ok, 0 failed), withactorType=agent,agentId,sessionId,scope, and the prompt itself is not stored.A latent bug fixed on the way
AgentTurnAuditorcalledAddAsyncwith noSaveEntitiesAsync. "Audit before executing, abort if the audit write fails" was a claim about an in-memory change nobody committed. It now commits, and converts write failures into aResultinstead of an exception. Relatedly, a turn with no agent identity is now rejected before anything is written: on an append-only ledger a mis-attributed row is permanent, so not writing beats writing wrong.AuditEntry.Createnow takes attribution as a required parameter, so all 8 write sites had to declare their type; the AOP sink translates the envelope's existingSubjectTyperather than inventing one.ADR: warranted, not written
A
T-NNNshould cover the closed set of actor types, the decision that pre-migration rows stayunknownforever, and the ratification that agent turns shareaudit_entriesrather than a separateAgentActionLog— that last one currently lives only in code comments from EAG-16.Deliberately left out
reference/specs/design/tracker-postgresql-data-design.md §12.1describesaudit_entriesin a schema and with columns that never matched the implementation — pre-existing drift, untouched.model_idis config-driven rather than harvested from the runtime response, because the row is written before execution.EVOLITH_CORE_LIVE=1; they need a running Evolith Core and are unrelated.🤖 Generated with Claude Code