Repository navigation
agent: fact provenance (declared source plus server-set origin) - #194
Merged
Merged
Conversation
record_fact takes a source field from a closed enum (target_response, tool_output, third_party_intel, operator_input, inference). An empty value is stored as inference; any other value is refused with the allowed list. The source is stored in the fact payload. The writer of a node is already recorded by the server in the origin column. compactNode now surfaces origin and source, so recent_facts in the overview, list_facts, expand_digest members and related-task facts carry them. The cold-digest compressor input carries source and origin next to confidence; the digest body is unchanged model text. prove_goal refuses a fact whose source is target_response, in addition to the existing confidence=observed rule. The goal stays open; findings are still accepted. The worker, pentest and planner default prompts ask for the source, and the planner treats target_response facts as non-authoritative for goals. Refs #124 (F4).
SeedTool is first-insert only and ToolResolve serves the stored tools row, so on an existing database the record_fact schema never gained the source property: the model never saw the enum, every fact was stored as inference, and prove_goal's target_response refusal could not fire. seedFactSourceSchema copies properties.source from the record_fact seed into the stored schema, only when the stored schema lacks it (same pattern as seedFindingEvidenceSchema). Operator edits to the description and to other properties are preserved, and a second run changes nothing. It runs from seedOrchestrationTools next to the other schema seeders. The PG test edits the stored row (no source, custom description and parameter text), runs seedOrchestrationTools twice, and checks the added enum, the preserved operator text, the unchanged row on the second run, and that ToolResolve serves the source property to the worker. Refs #124 (F4).
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.
What
F4 from the agentic attack-class audit (#124): fact provenance, for 0.7.0.
record_facttakes asourcefield from a closed enum:target_response,tool_output,third_party_intel,operator_input,inference. An empty value is stored asinference. Any other value is refused with a message that names the value and the allowed list. In batch mode the bad item is reported inerrorsand the other items are written. The source is stored in the fact payload.origincolumn (worker-N,planner,human,orchestrator). It is now surfaced. It is read only from that column, never from model text.compactNodecarriesoriginandsource, so they appear inrecent_facts(overview, including related-task facts),list_facts, andexpand_digestmembers. The cold-digest compressor input carriessourceandoriginnext toconfidence. The digest body is still model text; only its input changes.prove_goalrefuses a fact whose source istarget_response, in addition to the existingconfidence=observedrule. The message is explicit and the goal stays open. Findings are still accepted.target_responseas non-authoritative for proving a goal.seedFactSourceSchema(run fromseedOrchestrationTools) adds thesourceproperty with its enum to the storedrecord_factschema.SeedToolis first-insert only andToolResolveserves the stored row, so without this the model never saw the enum and every fact went toinference. The update only adds the missing property: operator edits to the description and other properties are kept, and a second run changes nothing.Why
The audit found a memory-poisoning gap and partial goal drift. A fact stored only the writing agent's name, which never reached a prompt. Nothing said whether its content came from the target. A worker could transcribe target text into a fact, and that fact then reached the planner overview, cold digests and
prove_goal. A target could therefore plant a statement that closes a goal. This PR records where the content came from and shows it to the readers that act on facts. It also stopsprove_goalfrom accepting target-sourced facts.Known limits
confirmed. Every fact is still stored with stateconfirmed, whatever its confidence. Changing that is a separate behaviour change that should be measured first; it is not in this PR.originis server-set. A model can mislabel target content astool_outputorinference, and the server cannot check that. This PR makes the declaration explicit and visible; it does not verify it.target_responseis only refused byprove_goal. Other decisions do not filter on it:add_intentparents, worker mandates, cold digests and the related-task overview. The planner prompt treats it as non-authoritative for goals only.source. They show nosourcekey and are not refused by the newprove_goalrule (the confidence rule still applies).record_factandprove_goaltool descriptions carry the rule on a fresh database; see the next point for existing databases.prove_goalandlist_factsstay old on an existing database. That is acceptable: the refusals are enforced server-side, whatever the description says. Therecord_factschema was the only blocking piece (without thesourceproperty the model could not declare a source), and it is now upgraded byseedFactSourceSchema. The storedrecord_factdescription also stays old; the addedsourceproperty carries its own description and enum.source=andorigin=without a description.Tests
New file
agent/fact_provenance_test.go.Pure (no database):
TestNormalizeFactSource: empty becomesinference, every enum value is kept, case and spaces are ignored, invalid values are refused with the value and the allowed list.TestCompactNodeProvenance:originandsourceare surfaced; a legacy node carries neither key.TestCompressionInputProvenance: the digest input line carriesconfidence,sourceandorigin; a legacy line is unchanged.TestPromptSeedsCarryFactSource: the worker and pentest seeds name every source; the planner seed treatstarget_responseas non-authoritative.PostgreSQL:
TestRecordFactStoresProvenance: source and origin are stored, the default isinference, an invalid value is refused with a clear message in single and batch mode, and nothing is written for it.TestFactProvenanceSurfaced:recent_facts,list_facts,expand_digestmembers and the digest input built from the stored graph expose both fields.TestProveGoalRefusesTargetResponseFact: an observedtarget_responsefact is refused and the goal stays open; an observedtool_outputfact proves; a finding proves.TestRecordFactSourceSchemaUpgrade(server/fact_source_schema_test.go): the storedrecord_factrow is edited to an old shape (nosource, custom description and parameter text),seedOrchestrationToolsruns twice, and the test checks thatsourceis added with the five enum values, that the operator text and other properties are intact, that the second run leaves the row unchanged (includingupdated_at), and thatToolResolveserves thesourceproperty to the worker. It fails when theseedFactSourceSchemacall is removed.Local gate (Go 1.26.10 linux/amd64, throwaway
postgres:16-alpinecontainer, fresh database):go build ./...: okgo vet ./...: okgo test -count=1 -p 1 ./agent ./server ./db:okfor all three packages. The PG tests above ran and passed.go build ./...ok,go vet ./...ok,go test -count=1 -p 1 ./server ./agent ./dbok for all three packages.agent/tools.goalready had unformatted lines onmain(detectStagnation,stagnation_warning); they are left as they are.Not verified
go test -raceand the full./...run were not run locally; CI covers them.