Repository navigation
feat(runtime): add decision policy governance types #1921
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 6 commits
b1d544b
ba7c086
08133ca
62b4117
7da33c6
d4a8603
5098f5d
cd2bc83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -92,7 +92,9 @@ | |
| MemorySearchRequest, | ||
| MemoryWriteAssessment, | ||
| MemoryWriteGate, | ||
| MemoryWriteGatePreflight, | ||
| MemoryWriteGateRequest, | ||
| MemoryWriteObservationSink, | ||
| MemoryWritePlan, | ||
| MemoryWriteRejectionCode, | ||
| MemoryWriteVerdict, | ||
|
|
@@ -259,7 +261,9 @@ def __init__( | |
| artifact_resolver: _ArtifactResolver | None = None, | ||
| id_factory: IdFactory | None = None, | ||
| prompt_context: ScopedPrompts | None = None, | ||
| scope_id: str = "unscoped", | ||
| write_gate: MemoryWriteGate | None = None, | ||
| write_gate_observation_sink: MemoryWriteObservationSink | None = None, | ||
| capacity_budget: MemoryCapacityBudget | None = None, | ||
| compaction: MemoryCompactionPolicy | None = None, | ||
| max_history_revisions: int = 100, | ||
|
|
@@ -268,6 +272,8 @@ def __init__( | |
| self._prompt_context = prompt_context | ||
| self._candidate_pipeline = candidate_pipeline | ||
| self._write_gate = write_gate | ||
| self._write_gate_observation_sink = write_gate_observation_sink | ||
| self._scope_id = scope_id | ||
| self._embedding_model = embedding_model | ||
| if rerank_candidate_limit < 1: | ||
| raise _InvalidMemoryOperationError("search-limit") | ||
|
|
@@ -1375,17 +1381,23 @@ async def _assess_write( | |
| if self._write_gate is None: | ||
| return None | ||
| projection = await self._gate_evidence(base, candidates, evidence, current_entries) | ||
| request = MemoryWriteGateRequest( | ||
| candidates=tuple(candidate.text for candidate in candidates), | ||
| evidence=projection.entries, | ||
| expected_revision=None if base is None else base.revision, | ||
| scope_id=self._scope_id, | ||
| operation_id=_gate_operation_id(base), | ||
| subject_refs=_gate_subject_refs(candidates), | ||
| evidence_refs=tuple(_gate_evidence_ref(entry) for entry in projection.entries), | ||
| observation_sink=self._write_gate_observation_sink, | ||
|
Copilot marked this conversation as resolved.
Outdated
|
||
| ) | ||
| if projection.rejection is not None: | ||
| if isinstance(self._write_gate, MemoryWriteGatePreflight): | ||
| return await self._write_gate.assess_preflight(request, projection.rejection) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2]
Before this PR the projection-rejection path never called gate code, so this is a new failure mode introduced by the preflight hook. Suggest wrapping the preflight call in the same fail-open fallback (return an ACCEPT assessment with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 5098f5d. assess_preflight now shares the fail-open behavior of assess(): any gate exception returns an ACCEPT assessment with used_fallback=True, leaving the plan committable. Added a focused raising-preflight regression test. |
||
| _log_gate_assessment(projection.rejection) | ||
| return projection.rejection | ||
| try: | ||
| return await self._write_gate.assess( | ||
| MemoryWriteGateRequest( | ||
| candidates=tuple(candidate.text for candidate in candidates), | ||
| evidence=projection.entries, | ||
| expected_revision=None if base is None else base.revision, | ||
| ) | ||
| ) | ||
| return await self._write_gate.assess(request) | ||
| except Exception: | ||
| return MemoryWriteAssessment( | ||
| verdict=MemoryWriteVerdict.ACCEPT, | ||
|
|
@@ -1839,6 +1851,25 @@ def _candidate_gate_identity(candidate_index: int, identity: str) -> str: | |
| return f"candidate:{candidate_index} {identity}" | ||
|
|
||
|
|
||
| def _gate_operation_id(base: Memory | None) -> str: | ||
| if base is None: | ||
| return "memory-write:new" | ||
| return f"memory-write:{base.artifact_id}@{base.revision + 1}" | ||
|
|
||
|
|
||
| def _gate_subject_refs(candidates: tuple[MemoryEntryInput, ...]) -> tuple[str, ...]: | ||
| return tuple( | ||
| f"candidate:{index}" | ||
| if candidate.entry is None | ||
| else f"entry:{candidate.entry.entry_id}@{candidate.entry.entry_version_id}" | ||
| for index, candidate in enumerate(candidates, start=1) | ||
|
Comment on lines
+1987
to
+1991
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 5098f5d. Applied writes replace candidate placeholders with exact committed entry/version refs and the committed Memory revision operation ID. Held, unchanged, and failed candidates cannot be reconstructed without retaining raw input, so their sidecars explicitly clear subject_refs and set incomplete_subject_count rather than claiming replayability.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Retain references for the complete assessed candidate batch Applied single-candidate writes now have exact committed refs, but batch coverage is still incomplete on cd2bc83. Through
|
||
| ) | ||
|
|
||
|
|
||
| def _gate_evidence_ref(value: str) -> str: | ||
| return value.partition("\n")[0] | ||
|
|
||
|
|
||
| def _source_gate_content(source: Source, resolver: _SourceResolver | None) -> str | None: | ||
| content = getattr(source, "content", None) | ||
| if isinstance(content, str): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] The example value contradicts the actual default for the privacy boundary
RuntimeConfig.memory_write_gate_privacy_boundarydefaults tono_external_call(runtime/config.py, andtest_memory_write_gate_defaults_to_no_external_callpins it), and the comment above this line correctly says "The default makes no decision-model call". But by this file's own convention the commented line shows the default (..._MODE=shadowtwo lines up matches its actual default), and here it showslocal_only- a value that DOES make a decision-model call on a loopback backend.An operator who uncomments the line to "keep the documented default" actually enables model calls. Suggest changing the example to
# POWERCONTEXT_SERVER_RUNTIME_MEMORY_WRITE_GATE_PRIVACY_BOUNDARY=no_external_call(and optionally a follow-up line showing thelocal_onlyopt-in), or rewording so the example is clearly an opt-in and not the default.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 5098f5d. .env.example now shows no_external_call, matching RuntimeConfig and the surrounding documentation. local_only remains described as the explicit loopback opt-in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 5098f5d. .env.example now shows no_external_call, matching RuntimeConfig and the surrounding documentation. local_only remains described as the explicit loopback opt-in.