-
Notifications
You must be signed in to change notification settings - Fork 1
feat(context): enforce sensitivity policy — drop/redact restricted items from prompts #98
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
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
9a95fdd
feat(context): enforce sensitivity policy in context pipeline
dgenio bc8893f
docs: update pipeline descriptions and agent instructions for sensiti…
dgenio cf51e36
fix(context): adjust total_candidates for sensitivity drops in BuildS…
dgenio d669016
fix(context): use accumulation pattern for dropped_reasons[sensitivity]
dgenio 9d282df
fix(context): validate sensitivity_action to reject unknown values
dgenio 1c31509
refactor(context): remove unreachable fast-path guard in sensitivity …
dgenio 369dc5f
docs: fix BuildStats description only drops are recorded, not redact…
dgenio d60be54
docs: fix pipeline stage count seven eight
dgenio cd60369
merge: resolve CHANGELOG.md conflict with main
dgenio f0dea92
chore: remove test_output.txt and add to .gitignore
dgenio 8766508
docs(manager): renumber pipeline steps to include sensitivity filter
dgenio e582989
docs(manager): renumber inline step comments 1-8 for consistency
dgenio 97e2618
feat(sensitivity): add register_redaction_hook() for user-extensible …
dgenio 44c9347
style(sensitivity): move _VALID_ACTIONS to module scope
dgenio 81fcf83
fix(manager): shorten docstring line to satisfy E501
dgenio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
Binary file not shown.
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
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
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
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
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| """Sensitivity enforcement for the contextweaver Context Engine. | ||
|
|
||
| Filters or redacts :class:`~contextweaver.types.ContextItem` objects whose | ||
| sensitivity level meets or exceeds the configured | ||
| :attr:`~contextweaver.config.ContextPolicy.sensitivity_floor`. | ||
|
|
||
| Two actions are supported: | ||
|
|
||
| * **drop** (default) — the item is silently removed from the candidate list. | ||
| * **redact** — the item's text is replaced with a placeholder via a | ||
| :class:`~contextweaver.protocols.RedactionHook`. | ||
|
|
||
| The built-in :class:`MaskRedactionHook` replaces the text with | ||
| ``[REDACTED: {sensitivity}]`` while preserving all other item metadata. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import replace | ||
|
|
||
| from contextweaver.config import ContextPolicy | ||
| from contextweaver.protocols import RedactionHook | ||
| from contextweaver.types import ContextItem, Sensitivity | ||
|
|
||
| # Ordered severity levels for comparison. | ||
| _SENSITIVITY_ORDER: dict[Sensitivity, int] = { | ||
| Sensitivity.public: 0, | ||
| Sensitivity.internal: 1, | ||
| Sensitivity.confidential: 2, | ||
| Sensitivity.restricted: 3, | ||
| } | ||
|
|
||
| # Valid values for ContextPolicy.sensitivity_action. | ||
| _VALID_ACTIONS: set[str] = {"drop", "redact"} | ||
|
|
||
| # Hook registry (name → instance). Built-in hooks are registered at | ||
| # module load time; users can add their own via :func:`register_redaction_hook`. | ||
| _HOOK_REGISTRY: dict[str, RedactionHook] = {} | ||
|
|
||
|
|
||
| def register_redaction_hook(name: str, hook: RedactionHook) -> None: | ||
| """Register a custom :class:`~contextweaver.protocols.RedactionHook`. | ||
|
|
||
| Once registered, the *name* can be used in | ||
| :attr:`~contextweaver.config.ContextPolicy.redaction_hooks` just like the | ||
| built-in ``"mask"`` hook. | ||
|
|
||
| Args: | ||
| name: Short identifier for the hook (e.g. ``"my_custom_hook"``). | ||
| hook: An object implementing the :class:`RedactionHook` protocol. | ||
|
|
||
| Raises: | ||
| ValueError: If *name* is already registered. | ||
| """ | ||
| if name in _HOOK_REGISTRY: | ||
| msg = f"Redaction hook {name!r} is already registered" | ||
| raise ValueError(msg) | ||
| _HOOK_REGISTRY[name] = hook | ||
|
|
||
|
|
||
| class MaskRedactionHook: | ||
| """Replace item text with ``[REDACTED: {sensitivity}]``. | ||
|
|
||
| All other item fields (id, kind, metadata, parent_id, artifact_ref) are | ||
| preserved so the item still participates in dependency closure, stats | ||
| tracking, and rendering structure. | ||
| """ | ||
|
|
||
| def redact(self, item: ContextItem) -> ContextItem: | ||
| """Return a copy of *item* with its text replaced by a redaction mask. | ||
|
|
||
| Args: | ||
| item: The context item to redact. | ||
|
|
||
| Returns: | ||
| A new :class:`ContextItem` with masked text and a minimal | ||
| token estimate. | ||
| """ | ||
| placeholder = f"[REDACTED: {item.sensitivity.value}]" | ||
| return replace(item, text=placeholder, token_estimate=len(placeholder) // 4) | ||
|
|
||
|
|
||
| # Register the built-in hook so it can be referenced by name in | ||
| # ContextPolicy.redaction_hooks. | ||
| _HOOK_REGISTRY["mask"] = MaskRedactionHook() | ||
|
|
||
|
|
||
| def _resolve_hooks(names: list[str]) -> list[RedactionHook]: | ||
| """Resolve hook names to instances. | ||
|
|
||
| Args: | ||
| names: Hook names from :attr:`ContextPolicy.redaction_hooks`. | ||
|
|
||
| Returns: | ||
| Resolved :class:`RedactionHook` instances. | ||
|
|
||
| Raises: | ||
| ValueError: If a name cannot be resolved. | ||
| """ | ||
| hooks: list[RedactionHook] = [] | ||
| for name in names: | ||
| hook = _HOOK_REGISTRY.get(name) | ||
| if hook is None: | ||
| msg = f"Unknown redaction hook {name!r}. Available: {sorted(_HOOK_REGISTRY)}" | ||
| raise ValueError(msg) | ||
| hooks.append(hook) | ||
| return hooks | ||
|
|
||
|
|
||
| def apply_sensitivity_filter( | ||
| items: list[ContextItem], | ||
| policy: ContextPolicy, | ||
| ) -> tuple[list[ContextItem], int]: | ||
| """Filter or redact items whose sensitivity meets or exceeds the policy floor. | ||
|
|
||
| Args: | ||
| items: Candidate items to inspect. | ||
| policy: The active context policy (provides ``sensitivity_floor``, | ||
| ``sensitivity_action``, and ``redaction_hooks``). | ||
|
|
||
| Returns: | ||
| A 2-tuple ``(filtered_items, dropped_count)``. In ``"redact"`` mode | ||
| *dropped_count* is always ``0`` because items are kept (with masked | ||
| text). | ||
| """ | ||
| floor_level = _SENSITIVITY_ORDER[policy.sensitivity_floor] | ||
| action = policy.sensitivity_action | ||
|
|
||
| if action not in _VALID_ACTIONS: | ||
| msg = f"Unknown sensitivity_action {action!r}. Valid: {sorted(_VALID_ACTIONS)}" | ||
| raise ValueError(msg) | ||
|
|
||
| # Resolve redaction hooks once (only needed in redact mode). | ||
| hooks: list[RedactionHook] = [] | ||
| if action == "redact": | ||
| hook_names = policy.redaction_hooks or ["mask"] | ||
| hooks = _resolve_hooks(hook_names) | ||
|
|
||
| result: list[ContextItem] = [] | ||
| dropped = 0 | ||
| for item in items: | ||
| item_level = _SENSITIVITY_ORDER[item.sensitivity] | ||
| if item_level >= floor_level: | ||
| if action == "redact": | ||
| redacted = item | ||
| for hook in hooks: | ||
| redacted = hook.redact(redacted) | ||
| result.append(redacted) | ||
| else: | ||
| # Default: drop | ||
| dropped += 1 | ||
dgenio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| else: | ||
| result.append(item) | ||
|
|
||
| return result, dropped | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.