The Agent Read the Document. Why Can't It Save the Summary to Project Memory?
Imagine an agent working on an integration with a billing service. It is allowed to read a confidential specification. It works through the interface, writes a short summary and proposes saving that summary to project me
Imagine an agent working on an integration with a billing service. It is allowed to read a confidential specification. It works through the interface, writes a short summary and proposes saving that summary to project memory so the next task can start faster.
The summary contains no password. The service name and method description look harmless. Both the source and the proposed memory entry are classified as confidential. The transfer seems reasonable.
But project memory has a different set of readers. Alongside the two participants in the integration, another agent would gain access to the summary. That agent could not read the original specification. The classification has not changed, but the access boundary has expanded.
I would make that mismatch explicit in configuration and check it before review. We will separate the context source from durable memory, then write a filter that detects a wider audience even when the data classification stays the same. The filter itself stores nothing and grants no permission.
Start with two separate decisions
For the current task, we need to decide what information the executor can access. After the task, a different question arises: which parts of the result may be retained, who may see them and under what conditions they may be reused.
One document can participate in both processes. An approved specification can live in a knowledge base while a relevant excerpt enters the context of a particular run. Permission to read that excerpt does not determine what may happen to the resulting summary.
The example below uses fields from the open NexFlow specification. Its context model describes sources separately from its memory model. The links are pinned to a specific revision so later documentation changes cannot silently alter what the example refers to.
NexFlow provides vocabulary and declaration structures here. This is not a claim that a ready-to-use runtime reads documents, manages memory or enforces isolation.
Declare the source without authorizing retention
We will declare one source in context.yaml:
specVersion: "0.1"
kind: ContextSet
metadata:
project: billing-integration
contextSources:
- id: billing-api-docs
type: docs
description: Approved billing API specification.
access:
default: read
allowedActors:
- implementation-agent
- reviewer
classification: confidential
refreshPolicy: on_request
freshness:
maxAge: 24h
staleAllowed: false
requiresCitation: true
The declaration identifies the requested source, its intended readers and the expected freshness of the information. The actor list is deliberately narrow.
However, refreshPolicy: on_request is not evidence that the document was actually fetched again. maxAge: 24h expresses an expectation, not the revision of the data that was retrieved. In a working system, the code accessing the source must check those conditions.
The source declaration also does not authorize storing results in project memory. That requires a separate policy.
Memory has its own readers and writers
We will describe the target scope in memory.yaml:
specVersion: "0.1"
kind: MemorySet
metadata:
project: billing-integration
memoryScopes:
- scope: project
retention: Review after interface changes; apply the project retention policy.
ownership: Integration maintainer.
visibility: implementation-agent and reviewer only.
updateRules: Review the source revision and the reusable conclusion before saving.
sensitivity: confidential
allowedConsumers:
- implementation-agent
- reviewer
updateMode: approval_required
allowedWriters:
- reviewer
allowedSourceScopes:
- task
prohibitedContent:
- raw_secrets
- credential_values
- personal_data
auditEvents:
- memory.updated
approvalGate: memory_review
Several independent restrictions matter here.
allowedConsumers identifies the intended memory consumers. allowedWriters is a separate list: implementation-agent may read this scope, but only reviewer may write to it in this example.
allowedSourceScopes allows material from task to be considered for promotion to project. It describes an eligible route, not approval for each individual entry.
updateMode, together with approvalGate, requires a separate approval. The string memory_review does not prove that a human has inspected the summary.
Finally, retention is a textual rule. It does not start a deletion timer or prove that archives and backups follow the same policy.
Both YAML documents can be checked against their respective JSON Schemas. That checks the structure of two documents, not the completeness of a project. A full assembly also needs declarations for the actors, the memory_review gate and the memory.updated event, along with permissions and the other related resources.
An agent declaration does not override scope policy
An agent definition also separates requests for context and memory. A components fragment might look like this:
components:
contextSourceRefs:
- billing-api-docs
memoryScopes:
- task
This is a fragment of an AgentDefinitionSet, not a complete standalone manifest.
The definition requests the billing-api-docs source and task memory. It does not select project. Even adding that value to memoryScopes would not make implementation-agent an authorized writer: the target scope's allowedWriters list remains a separate restriction.
The value project selects a scope within one project's assembly. It does not mean access to every project or create shared organization memory.
Check the proposed transfer before review
The check needs a candidate record: project, source identifier and revision, source scope, classification, permitted readers and content categories.
These are application metadata, not additional standard NexFlow fields. In a real system, they must come from trusted storage and access checks. The model must not be allowed to assign its own readers, lower a classification or declare that data has been sanitized.
The filter below covers one limited operation: preparing a transfer from an eligible source scope into project memory. It expects policy input that has already passed schema validation. It does not replace safe YAML parsing or full assembly validation.
from dataclasses import dataclass
LEVELS = {"public": 0, "internal": 1, "confidential": 2, "restricted": 3}
@dataclass(frozen=True)
class Candidate:
project: str
source_id: str
source_revision: str
source_scope: str
classification: str
readers: frozenset[str]
categories: frozenset[str]
def promotion_issues(candidate: Candidate, memory: dict, writer: str) -> tuple[str, ...]:
# Static pre-review checks for schema-validated policy input. No storage or authorization.
scopes = [s for s in memory["memoryScopes"] if s["scope"] == "project"]
if len(scopes) != 1:
return ("target_scope_missing_or_ambiguous",)
target = scopes[0]
issues = []
if candidate.project != memory["metadata"].get("project"):
issues.append("project_mismatch")
if not candidate.source_id.strip() or not candidate.source_revision.strip():
issues.append("source_identity_missing")
if writer not in target.get("allowedWriters", []):
issues.append("writer_not_allowed")
if candidate.source_scope not in target.get("allowedSourceScopes", []):
issues.append("source_scope_not_allowed")
source_level = LEVELS.get(candidate.classification)
target_level = LEVELS.get(target.get("sensitivity"))
if source_level is None or target_level is None:
issues.append("unknown_classification")
elif target_level < source_level:
issues.append("classification_downgrade")
consumers = frozenset(target["allowedConsumers"])
if not candidate.readers or not consumers:
issues.append("reader_boundary_missing")
elif not consumers.issubset(candidate.readers):
issues.append("reader_boundary_expands")
if candidate.categories.intersection(target.get("prohibitedContent", [])):
issues.append("prohibited_content")
if target.get("updateMode") != "approval_required" or not target.get("approvalGate"):
issues.append("explicit_review_gate_missing")
return tuple(issues)
The important check is consumers.issubset(candidate.readers). Under the conservative policy chosen for this example, the retained summary must not have a wider audience than its source material.
Comparing classification with sensitivity serves a different purpose: it prevents a silent downgrade of the data classification. Both checks are necessary. The same confidential label does not imply the same permissions.
If a wider audience is genuinely needed, it should go through a separate access-review process and preparation of a suitable version of the material. Routine promotion into memory should not widen access as a side effect.
The key test keeps the classification and changes the readers
Suppose memory contains the parsed MemorySet shown above. The candidate looks like this:
from copy import deepcopy
candidate = Candidate(
project="billing-integration",
source_id="billing-api-docs",
source_revision="rev-7",
source_scope="task",
classification="confidential",
readers=frozenset({"implementation-agent", "reviewer"}),
categories=frozenset({"integration_summary"}),
)
assert promotion_issues(candidate, memory, "reviewer") == ()
broader = deepcopy(memory)
broader["memoryScopes"][0]["allowedConsumers"].append("qa-agent")
assert promotion_issues(candidate, broader, "reviewer") == (
"reader_boundary_expands",
)
assert promotion_issues(candidate, memory, "implementation-agent") == (
"writer_not_allowed",
)
In the first case, an empty tuple of issues means only that the candidate is ready for review. No approval or write has occurred.
In the second case, both the source material and the memory are still confidential. The filter reports the extra reader; the calling process must defer the transfer.
In the third case, the data has not changed. The issue is who is attempting the write. Read access has not become write access.
For this filter, I would also test missing allowlists, another project, an unknown classification, an empty source revision, a prohibited category and an ambiguous target scope. It is also worth checking that the function does not mutate its inputs. The standard unittest framework is a convenient way to organize these cases.
What this example deliberately does not prove
Checking source_revision detects a missing revision, but a nonempty value does not prove freshness. The example's rev-7 is illustrative. Establishing the current revision and the document's applicability remains a separate responsibility. A populated source_id does not prove read permission either: that is assumed to have been checked before the candidate is formed.
Content categories are not a leak detector. If credential_values is not marked, the function cannot discover a password in the text. That needs separate content inspection and rules for handling sensitive information.
The filter does not authenticate writer. In a working system, that identity must come from trusted authentication, not an argument assembled by the model.
The example also does not verify an approval bound to the exact summary content, write to storage, protect against policy changes between check and write, delete records or revoke access. Placing this function in front of an arbitrary write operation would not implement memory governance.
Its practical result is narrower: obvious mismatches can be detected before human acceptance without making the reviewer find them across several files every time.
Retention should change the status of knowledge, not silently widen access
Return to the billing-interface summary. Its usefulness for the next task is not in dispute. The problem begins when the convenience of reuse is treated as sufficient justification for retention.
I would put this summary through two separate decisions. First, an automated check compares the project, source scope, classification, readers and writer. Then a human evaluates the conclusion itself, the applicable source revision and the conditions for reuse.
The team gets a candidate it can meaningfully accept into memory. A document does not become shared knowledge merely because an agent produced a good summary of it.
References
- NexFlow Context Model: source fields, read boundaries and freshness expectations.
- NexFlow Memory Model: memory scopes, consumers, writers and promotion between scopes.
- Context schema and Memory schema: the structure of the two declarations.
-
Software team agent definitions: the separation of
contextSourceRefsandmemoryScopesin definition components. - Python unittest: checks for expected results and rejection cases.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.