Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/change/change_log.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This file provides a chronological, human-readable record of applied codebase an
*Note: For operational task and run history, consult `.triagecore/ledger.jsonl`.*

## [Unreleased]
- Implemented CR-078 (Signed Ledger Event Coverage Plan): Documented signed-event coverage, added a signed `validation_result` ledger path with explicit `validation_result:sign` authorization, and covered provenance-only signing behavior with focused tests.
- Implemented TD-007 (TriageDesk GUI consolidation pass): Consolidated GUI improvements, added safety boundary documentation to `app.py`, fixed stale comments, and ensured all recent panels align with the read-only operator console goal.
- Implemented TD-006 (Packet Preview UI integration): Wired up the UI for `packet render` dry-runs within the Context Planner tab, producing safely bounded deterministic outputs visually in a read-only textbox without executing or mutating.
- Implemented TD-005 (Read-only Context Planner panel): Added a dedicated sidebar tab in TriageDesk for context planner dry-runs, allowing evaluation of token budgets without writing packets.
Expand Down
48 changes: 48 additions & 0 deletions docs/change/requests/CR-078-signed-ledger-event-coverage-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# CR-078: Signed Ledger Event Coverage Plan

## Status
Implemented

## Scope

- Document signed ledger event coverage beyond `route_audit`.
- Keep `route_audit` signed support unchanged.
- Add the first additional signed ledger event path for `validation_result`.
- Require the explicit `validation_result:sign` capability for that path.
- Add focused tests for valid signing, tampering, revoked agents, and unauthorized capability.
- Update backlog and change log wording so the active recommendation matches the current backlog.

## Non-Goals

- No runtime key rotation behavior.
- No approval, safety, or correctness inference from a valid signature.
- No private key persistence outside the existing local `.triagecore/identity/keys/` path.
- No signing requirement for every ledger event.
- No CLI expansion for non-`route_audit` signature verification in this slice.
- No changes to human review, admission, or safety gates.

## Signed Event Coverage

| Event type | Current signing status | Capability | Reason |
|---|---|---|---|
| `route_audit` | Signed path exists | `route_audit:sign` | Route-audit records are control-plane evidence and already have operator-facing verification. |
| `validation_result` | Signed path added in this slice | `validation_result:sign` | Validation records are useful reviewer evidence when tied to a known local validator identity. |
| `taskpacket_created` | Intentionally unsigned for now | Not assigned | Creation records may later need signing, but the payload contract should be reviewed first to avoid signing ambiguous or sensitive metadata. |
| `route_decision` | Intentionally unsigned for now | Not assigned | Route decisions are important, but signing them should be paired with a route-decision verification plan rather than silently widening current audit semantics. |
| `project_steward_decision` | Intentionally unsigned for now | Not assigned | Steward decisions are close to approval semantics, so they need a separate design pass to avoid implying that signature equals approval. |

## Acceptance Criteria

- [x] Signed and intentionally unsigned event types are documented.
- [x] `validation_result` events can be appended with signature metadata through an explicit helper.
- [x] The `validation_result` signed path requires `validation_result:sign`.
- [x] Tampering with signed `validation_result` event payloads fails verification.
- [x] Revoked identities cannot verify signed `validation_result` events.
- [x] Unauthorized identities cannot sign `validation_result` events.
- [x] A valid signature is documented as provenance only, not approval, safety, or correctness.

## Validation

- `python -m py_compile triage_core/task_ledger.py tests/test_task_ledger.py`
- `python -m pytest tests/test_task_ledger.py tests/test_agent_identity.py`
- `git diff --check -- triage_core/task_ledger.py tests/test_task_ledger.py docs/current_backlog.md docs/change/change_log.md docs/change/requests/CR-078-signed-ledger-event-coverage-plan.md`
21 changes: 6 additions & 15 deletions docs/current_backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

## Status

This document summarizes the active TriageCore backlog after CR-077.
This document summarizes the active TriageCore backlog after CR-078.

## Active GitHub Backlog

- Issue #72: Expand signed ledger event coverage beyond `route_audit`
- Status: open
- Purpose: selectively enforce identity checks and signatures for core ledger events beyond `route_audit` (e.g., decisions, validation results) without treating signatures as approval.
- Status: partially implemented by CR-078
- Purpose: selectively enforce identity checks and signatures for core ledger events beyond `route_audit` without treating signatures as approval. CR-078 adds the first additional signed path for `validation_result`; broader event coverage remains future work.

- Issue #73: Implement runtime key rotation behavior
- Status: open
Expand Down Expand Up @@ -36,13 +36,6 @@ This document summarizes the active TriageCore backlog after CR-077.
- Status: evidence structure complete; ledger integration remains future work
- Purpose: audit the admission of proposals separately from their execution.

- CLI task-envelope wizard MVP
- Source: CR-051 follow-on sequence
- Status: candidate future CR, not yet active GitHub backlog
- Purpose: guide the operator through task scope, allowed files, risk, agent lanes, and approval gates without requiring memorized fields.



- Textual read-only operator dashboard
- Source: CR-051 follow-on sequence
- Status: candidate future CR, not yet active GitHub backlog
Expand Down Expand Up @@ -105,20 +98,18 @@ This document summarizes the active TriageCore backlog after CR-077.

Keep three work lanes distinct:

- Identity lifecycle work #4 is closed. The active follow-ups are Issue #72 (signing expansion) and Issue #73 (runtime rotation behavior), which remain separate implementation slices.
- Identity lifecycle work #4 is closed. CR-078 partially implements Issue #72 by adding signed `validation_result` ledger events; remaining signed-event expansion and Issue #73 runtime rotation behavior should stay separate implementation slices.
- Model and runtime integrity work should build on CR-031 through CR-033. Keep
policy baseline, route-manifest artifact shape, manifest validation, and live
enforcement as separate reviewable slices.
- Repository consistency and secrets hygiene from CR-034 is complete. Future
hygiene work should be limited to stale documented claims or a separately
proposed repo-consistency checker.

The next feature-sized slice can be a runtime model-manifest enforcement
preview, but it should not collapse policy, artifact shape, manifest validation,
and backend probing into one change.
For signed ledger coverage, the next slice should either add operator-facing verification for signed `validation_result` records or deliberately choose one more event type. Do not treat a valid signature as approval, safety, or correctness.

For the empirical AI safety evaluation track, keep the next slices sequential: fixture validation first, evaluator CLI second, and broader adversarial/tampering studies only after the fixture contract is stable.

For external runtime interoperability, the next approved slice should be policy tests or execution-path validation for the bounded adapter path.

For operator UX, keep the next slices boring and sequential: task-envelope wizard first, Markdown report export second, and any Textual dashboard only after the artifact shape is stable.
For operator UX, future slices should focus on reviewability, export polish, and dashboard/TUI surfaces only after artifact contracts remain stable. Avoid re-opening completed wizard or Markdown renderer work unless there is a concrete regression or usability gap.
2 changes: 2 additions & 0 deletions docs/evals/actual_outcome_export.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ By exporting evidence as files:
- TriageCore does not need to self-score.
- There are no cross-repository Python imports, preventing tight coupling.

> **Reviewer Note:** For the conceptual explanation of how these actual outcome exports can be scored by an independent harness without importing TriageCore, see [Eval Integration Bridge](file:///c:/Users/corey/Documents/Science/AI/triagecore/docs/evals/eval_integration_bridge.md).

## JSON Contract Shape

TriageCore generates one JSON file per evaluation scenario. The exported file must match the following shape:
Expand Down
43 changes: 43 additions & 0 deletions docs/evals/eval_integration_bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Eval Integration Bridge

## Purpose
This document provides a reviewer-facing explanation of how TriageCore interfaces with the independent `agent-control-evals` repository. It clarifies how actual outcome exports produced by TriageCore can be deterministically scored by an external harness without importing or coupling the repositories.

## Integration Boundary
The integration between TriageCore and the evaluation harness is strictly **file-contract-based**.
There are no shared runtime dependencies, no cross-repository Python imports, and no hidden coupling. The boundary is entirely maintained by writing static JSON files to disk, which are then independently read and scored.

## What TriageCore Produces
TriageCore acts as a compatible producer of static actual outcome JSON records. When its control-plane scanner intercepts an action (for example, a privacy check or a forbidden tool call), it can map the resulting internal decision into a standardized external contract shape and write it to disk.

## What the External Harness Consumes
`agent-control-evals` acts as the independent evaluation suite. It consumes the static JSON records produced by TriageCore, validates them against its own expected-outcome fixtures, and produces a scored JSONL report showing where the system passed or failed.

## Example Handshake Commands
To execute the file-contract integration manually, you only need the built-in CLI commands from both repositories.

1. **In TriageCore**: Export the actual outcome to a shared or temporary directory.
```powershell
python -m triage_core.tc_cli eval export-privacy-smoke --output-dir actuals/privacy_smoke
```

2. **In `agent-control-evals`**: Score the actuals against the independent fixtures.
```powershell
python -m evals.runner --actuals <path-to-triagecore>/actuals/privacy_smoke --output reports/privacy_smoke_results.jsonl
```

## What This Demonstrates
This integration demonstrates a decoupled, inspectable control-plane evaluation pattern. It proves that a system's adherence to external boundaries (privacy, authorization, human approval) can be rigorously audited and scored by a completely independent test suite using a stable file contract.

## Non-Claims
To remain strictly within the bounds of this evaluation architecture, we explicitly note what this integration does **not** claim:

* **This does not certify TriageCore**: The eval kit proves architectural capability but is not a substitute for production-grade certification.
* **This does not prove model alignment**: The safety mechanisms evaluated here reside in the control plane's boundary enforcement, not the generative model itself.
* **This does not provide sandboxing**: This pattern evaluates policy and decision compliance, not OS-level process isolation.
* **This does not claim comprehensive safety coverage**: The fixtures test specific, bounded families of failure, not every conceivable adversarial attack.
* **This only demonstrates a narrow file-contract pattern** for scoring static control-plane outcomes.

## Future Work
* Expanding the JSON contract schema to handle multi-step interactions and conversational audits.
* Adding standardized export endpoints in TriageCore to produce actuals for the new Escalation Channel boundary family.
121 changes: 121 additions & 0 deletions tests/test_task_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from triage_core.task_ledger import (
TaskLedger,
verify_route_audit_event_signature,
verify_validation_result_event_signature,
)

def test_ledger_append_and_read():
Expand Down Expand Up @@ -710,3 +711,123 @@ def test_append_signed_route_audit_event_fails_for_unauthorized_agent():
signing_registry=registry,
signing_agent_id="context-planner",
)


def test_append_signed_validation_result_event_writes_signature_metadata():
with tempfile.TemporaryDirectory() as temp_dir:
ledger = TaskLedger(ledger_dir=temp_dir)
registry = AgentIdentityRegistry(ledger_dir=temp_dir)
registry.generate_identity(
"validator-tools",
"validator_tools",
["validation_result:sign"],
)

payload = {
"validator_name": "deterministic_demo_validator",
"validator_version": "1.0",
"validation_status": "passed",
"checked_files": ["triage_core/task_ledger.py"],
}
written_event = ledger.append_signed_validation_result_event(
"task-validation-123",
payload,
signing_registry=registry,
signing_agent_id="validator-tools",
)

assert written_event["event_type"] == "validation_result"
assert written_event["payload"] == payload
assert written_event["signature_metadata"]["agent_id"] == "validator-tools"
assert written_event["signature_metadata"]["capability"] == "validation_result:sign"
assert verify_validation_result_event_signature(written_event, registry) is True

stored_events = ledger.get_events("task-validation-123")
assert len(stored_events) == 1
assert "signature_metadata" in stored_events[0]
assert verify_validation_result_event_signature(stored_events[0], registry) is True


def test_signed_validation_result_event_verification_fails_after_payload_tamper():
with tempfile.TemporaryDirectory() as temp_dir:
ledger = TaskLedger(ledger_dir=temp_dir)
registry = AgentIdentityRegistry(ledger_dir=temp_dir)
registry.generate_identity(
"validator-tools",
"validator_tools",
["validation_result:sign"],
)

event = ledger.append_signed_validation_result_event(
"task-validation-456",
{
"validator_name": "deterministic_demo_validator",
"validation_status": "passed",
"checked_files": ["triage_core/task_ledger.py"],
},
signing_registry=registry,
signing_agent_id="validator-tools",
)

tampered_event = dict(event)
tampered_event["payload"] = dict(event["payload"])
tampered_event["payload"]["validation_status"] = "failed"

assert verify_validation_result_event_signature(tampered_event, registry) is False


def test_signed_validation_result_event_fails_for_revoked_agent():
with tempfile.TemporaryDirectory() as temp_dir:
registry = AgentIdentityRegistry(ledger_dir=temp_dir)
registry.generate_identity(
"revoked-validator",
"validator_tools",
["validation_result:sign"],
status=REVOKED_STATUS,
)

event = {
"event_id": "evt-validation-1",
"task_id": "task-validation-999",
"timestamp": "2026-06-12T00:00:00+00:00",
"schema_version": "0.2.0",
"role_taxonomy_version": "2026-06-worker-council-v2",
"event_type": "validation_result",
"payload": {
"validator_name": "deterministic_demo_validator",
"validation_status": "passed",
},
"signature_metadata": {
"agent_id": "revoked-validator",
"capability": "validation_result:sign",
"payload_hash": "abc",
"signature_algorithm": "ed25519",
"signed_at": "2026-06-12T00:00:00+00:00",
"signature": "ZmFrZQ==",
},
}

with pytest.raises(RevokedAgentError):
verify_validation_result_event_signature(event, registry)


def test_append_signed_validation_result_event_fails_for_unauthorized_agent():
with tempfile.TemporaryDirectory() as temp_dir:
ledger = TaskLedger(ledger_dir=temp_dir)
registry = AgentIdentityRegistry(ledger_dir=temp_dir)
registry.generate_identity(
"context-planner",
"context_planner",
["route_audit:sign"],
)

with pytest.raises(UnauthorizedCapabilityError):
ledger.append_signed_validation_result_event(
"task-validation-unauthorized",
{
"validator_name": "deterministic_demo_validator",
"validation_status": "passed",
},
signing_registry=registry,
signing_agent_id="context-planner",
)
50 changes: 50 additions & 0 deletions triage_core/task_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
LEDGER_SCHEMA_VERSION = "0.2.0"
ROLE_TAXONOMY_VERSION = "2026-06-worker-council-v2"
ROUTE_AUDIT_SIGN_CAPABILITY = "route_audit:sign"
VALIDATION_RESULT_SIGN_CAPABILITY = "validation_result:sign"


@dataclass
Expand Down Expand Up @@ -201,6 +202,30 @@ def append_signed_route_audit_event(
f.write(json.dumps(event) + "\n")
return event

def append_signed_validation_result_event(
self,
task_id: str,
payload: Dict[str, Any],
*,
signing_registry: AgentIdentityRegistry,
signing_agent_id: str,
signature_algorithm: str = "ed25519",
) -> Dict[str, Any]:
assert_persistent_privacy_safe(
payload,
artifact_name="TaskLedger payload for event_type=validation_result",
)
event = self._build_event(task_id, "validation_result", payload)
event["signature_metadata"] = signing_registry.sign_payload(
signing_agent_id,
VALIDATION_RESULT_SIGN_CAPABILITY,
validation_result_signature_payload(event),
signature_algorithm=signature_algorithm,
)
with open(self.ledger_path, "a", encoding="utf-8") as f:
f.write(json.dumps(event) + "\n")
return event

def _build_event(
self,
task_id: str,
Expand Down Expand Up @@ -648,6 +673,18 @@ def export_csv(self, export_path: str):
def route_audit_signature_payload(event: Dict[str, Any]) -> Dict[str, Any]:
if event.get("event_type") != "route_audit":
raise ValueError("route_audit_signature_payload only supports route_audit events.")
return ledger_event_signature_payload(event)


def validation_result_signature_payload(event: Dict[str, Any]) -> Dict[str, Any]:
if event.get("event_type") != "validation_result":
raise ValueError(
"validation_result_signature_payload only supports validation_result events."
)
return ledger_event_signature_payload(event)


def ledger_event_signature_payload(event: Dict[str, Any]) -> Dict[str, Any]:
return {
"event_id": event["event_id"],
"task_id": event["task_id"],
Expand All @@ -672,6 +709,19 @@ def verify_route_audit_event_signature(
)


def verify_validation_result_event_signature(
event: Dict[str, Any],
signing_registry: AgentIdentityRegistry,
) -> bool:
signature_metadata = event.get("signature_metadata")
if not signature_metadata:
return False
return signing_registry.verify_signed_payload(
validation_result_signature_payload(event),
signature_metadata,
)


def verify_route_audit_signatures_in_ledger(
ledger_path: str | Path,
) -> RouteAuditSignatureVerificationSummary:
Expand Down
Loading