Skip to content
Draft
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
69 changes: 69 additions & 0 deletions .github/workflows/request-promotion.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: request-promotion

on:
workflow_dispatch:
inputs:
sha:
description: Commit SHA to promote; defaults to the current ref
required: false
type: string
workflow_run:
workflows: [validate]
types: [completed]

permissions:
contents: read

concurrency:
group: request-promotion-${{ github.event.workflow_run.head_sha || inputs.sha || github.sha }}
cancel-in-progress: false

jobs:
request:
if: >
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.event == 'push')
runs-on: [self-hosted, linux, x64, hyrule-public-pr]
timeout-minutes: 5
steps:
- name: Generate promotion app token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.PROMOTION_APP_ID }}
private-key: ${{ secrets.PROMOTION_APP_PRIVATE_KEY }}
owner: AS215932
repositories: network-operations

- name: Request network-operations promotion PR
env:
GH_TOKEN: ${{ github.token }}
PROMOTION_TOKEN: ${{ steps.app-token.outputs.token }}
REPOSITORY: ${{ github.repository }}
INPUT_SHA: ${{ inputs.sha }}
WORKFLOW_RUN_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
app="${REPOSITORY#AS215932/}"
SHA="${INPUT_SHA:-${WORKFLOW_RUN_SHA:-${GITHUB_SHA}}}"
SHA="${SHA,,}"

if ! [[ "$SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::promotion SHA must be a 40-character hex commit SHA"
exit 1
fi

canonical="$(gh api "repos/${REPOSITORY}/commits/${SHA}" --jq .sha)"
if [ "$canonical" != "$SHA" ]; then
echo "::error::commit ${SHA} was not found in ${REPOSITORY}"
exit 1
fi

GH_TOKEN="$PROMOTION_TOKEN" gh api --method POST repos/AS215932/network-operations/dispatches \
-f event_type=app-promote \
-f "client_payload[repository]=$REPOSITORY" \
-f "client_payload[sha]=$SHA" \
-f "client_payload[title]=Promote ${app} ${SHA:0:7}" \
-f "client_payload[impact]=Automated promotion request from ${REPOSITORY}@${SHA}."
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,32 @@ uv run hyrule-knowledge loop --once --create-pr \

Synced decisions/labels land under `ledger/insights/` and ride the reviewed nightly PR — `ledger/` is excluded from the auto-merge allowlist precisely so this stream always crosses human eyes. Env equivalents: `HYRULE_KNOWLEDGE_LOOP_INSIGHT_SYNC`, `HYRULE_KNOWLEDGE_LOOP_INSIGHT_COLLECTOR_URL`, `HYRULE_KNOWLEDGE_LOOP_MAX_INSIGHT_RECORDS_PER_DAY`.

### Agentic coordinator intake

`hyrule-knowledge-coordinator` is the Knowledge adapter for Loop Handoff
Protocol v2. It serves policy-gated context to SOC, NOC, and Engineering with a
role derived from the authenticated source loop, so a caller cannot request a
more privileged Knowledge role. It also accepts approved, cited learning
proposals from those loops. Coordinator intake can only stage sanitized A4
events; it cannot write A1/A2 knowledge or bypass the Knowledge Loop PR.

```bash
HYRULE_COORDINATOR_URL=http://127.0.0.1:8771 \
HYRULE_COORDINATOR_SECRET=... \
uv run hyrule-knowledge-coordinator --once \
--proposal-dir /var/lib/hyrule-knowledge/coordinator-intake

uv run hyrule-knowledge loop --once --create-pr \
--learning-event /var/lib/hyrule-knowledge/coordinator-intake
```

SOC context uses the dedicated `soc_shadow` policy role. It permits cited
intended-state and sanitized observation lookup while denying production
actions, live diagnostics, secrets, raw logs, and packet captures. Learning
intake independently validates the coordinator-bound scope, producer/event
type, citations, forbidden data classes/keys, A4 authority, and retained human
review gate before writing the spool file.

## Governed agent consumption

Humans can browse `okf/index.md`. Agents should first read `okf/index.md`, then directory indexes, then individual concepts. Machine consumers should prefer `exports/knowledge.sqlite` and the JSONL exports:
Expand Down
30 changes: 30 additions & 0 deletions knowledge-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,30 @@ roles:
- production.*
- live_diagnostic.*
- secret.*
soc_shadow:
allow_actions:
- knowledge.resolve
- knowledge.search
- knowledge.claims
- knowledge.neighborhood
- knowledge.firewall_rules
- knowledge.server_registry
- knowledge.flows_into
- knowledge.flows_from
- knowledge.context_pack.generate
- knowledge.intended_state.read
- knowledge.observed_state.read
- knowledge.diff.read
- knowledge.intended_state
- knowledge.observed_state.fixture
- knowledge.diff_intended_observed
- knowledge.related_runbooks
- knowledge.find_conflicts
- knowledge.find_stale
deny_actions:
- production.*
- live_diagnostic.*
- secret.*
general:
allow_actions:
- knowledge.resolve
Expand Down Expand Up @@ -100,3 +124,9 @@ rules:
actors_any: [noc_shadow]
action_prefixes_any: [production., live_diagnostic., secret.]
reason: NOC shadow evals may not invoke live operational tools.
- id: soc_shadow_no_live_tools
effect: deny
when:
actors_any: [soc_shadow]
action_prefixes_any: [production., live_diagnostic., secret.]
reason: SOC context retrieval may not invoke live operational tools or disclose protected data.
12 changes: 12 additions & 0 deletions knowledge.config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ sources:
project_type: Service
title: Hyrule Engineering Loop
tags: [hyrule, engineering-loop, automation, langgraph]
- repo: AS215932/soc-agent
project_type: Service
title: Hyrule SOC Agent
tags: [hyrule, soc, security, posture, incident-response]
- repo: AS215932/agent-core
project_type: Service
title: Hyrule Agent Core
tags: [hyrule, agents, contracts, coordination, observability]
- repo: AS215932/agentic-observatory
project_type: Service
title: Hyrule Agentic Observatory
tags: [hyrule, agents, observability, approvals, control-plane]
- repo: AS215932/as215932.net
project_type: Service
title: as215932.net
Expand Down
5 changes: 5 additions & 0 deletions ledger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ Events are A4 proposals/fixtures unless promoted by human review into A1
curated lessons or A2 reviewed summaries. Promotion writes an audit record under
`ledger/reviews/` and an OKF concept under `okf/curated/lessons/` or
`okf/curated/summaries/`.

The LHP-v2 Knowledge coordinator adapter follows the same boundary. It stages
validated files in its configured coordinator-intake directory; the Knowledge
Loop imports those files and opens the usual review PR. A coordinator approval
authorizes staging only and is never authority to promote a claim or lesson.
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "OKF knowledge catalog, enrichment, quality, and observation tooli
requires-python = ">=3.12"
dependencies = [
"PyYAML>=6.0.2",
"agent-core",
"agent-core[coordination-client]",
]

[project.optional-dependencies]
Expand All @@ -20,6 +20,7 @@ api = ["starlette>=0.37", "uvicorn>=0.30"]
hyrule-knowledge = "hyrule_knowledge.cli:main"
hyrule-knowledge-mcp = "hyrule_knowledge.mcp_server:main"
hyrule-knowledge-api = "hyrule_knowledge.api:main"
hyrule-knowledge-coordinator = "hyrule_knowledge.coordination:main"

[tool.hatch.build.targets.wheel]
packages = ["src/hyrule_knowledge"]
Expand All @@ -29,6 +30,7 @@ dev = [
"mcp>=1.27.0",
"mypy>=1.11",
"pytest>=8",
"pytest-asyncio>=0.23",
"ruff>=0.6",
"types-PyYAML>=6.0.12",
]
Expand All @@ -51,6 +53,7 @@ ignore_missing_imports = true

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"

[tool.uv.sources]
agent-core = { git = "https://github.com/AS215932/agent-core", tag = "v0.8.0" }
agent-core = { git = "https://github.com/AS215932/agent-core", rev = "46bea255d60501276825fb9be7a75746f6062841" }
4 changes: 2 additions & 2 deletions schema/learning-event.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"properties": {
"id": {"type": "string", "pattern": "^learn_[a-f0-9]{32}$"},
"ledger_version": {"type": "string", "const": "learning_ledger_v1"},
"event_type": {"type": "string", "enum": ["context_pack_used", "engineering_loop_run_summary", "noc_shadow_eval_summary", "eval_run_summary", "policy_review_summary", "lesson_candidate"]},
"event_type": {"type": "string", "enum": ["context_pack_used", "engineering_loop_run_summary", "noc_shadow_eval_summary", "soc_case_outcome", "eval_run_summary", "policy_review_summary", "lesson_candidate"]},
"event_time": {"type": "string"},
"producer": {"type": "string", "enum": ["knowledge", "engineering_loop", "noc_shadow", "human_review", "fixture"]},
"producer": {"type": "string", "enum": ["knowledge", "engineering_loop", "noc_shadow", "soc_shadow", "human_review", "fixture"]},
"subject": {"type": "string"},
"summary": {"type": "string", "minLength": 1},
"status": {"type": "string", "enum": ["fixture", "proposed", "reviewed", "rejected", "superseded"]},
Expand Down
25 changes: 24 additions & 1 deletion src/hyrule_knowledge/context_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ def build_context_pack(

if role == "noc_shadow":
sections = _noc_shadow_sections(task, included_refs, claims, conflicts, stale, unresolved)
elif role == "soc_shadow":
sections = _soc_shadow_sections(task, included_refs, claims, conflicts, stale, unresolved)
else:
sections = _engineering_sections(task, included_refs, claims, conflicts, stale, unresolved)
sections = _truncate_sections(sections, max_chars=max_chars)
Expand Down Expand Up @@ -235,7 +237,6 @@ def _relevant_enrichment_ids(task: str, *, parsed: ParsedTask, store: KnowledgeS
out.append(concept_id)
return out


def _explicit_enrichment_ids(task: str, *, parsed: ParsedTask) -> list[str]:
ids = [concept_id for concept_id in parsed.concept_ids if concept_id.startswith("generated/enriched/")]
for token in task.replace("`", " ").split():
Expand Down Expand Up @@ -626,6 +627,28 @@ def _noc_shadow_sections(task: str, included_refs: list[dict[str, Any]], claims:
]


def _soc_shadow_sections(
task: str,
included_refs: list[dict[str, Any]],
claims: list[dict[str, Any]],
conflicts: list[dict[str, Any]],
stale: list[dict[str, Any]],
unresolved: list[str],
) -> list[ContextPackSection]:
intended = [claim for claim in claims if claim.get("authority_tier") in {"A0", "A1"}]
observed = [claim for claim in claims if claim.get("authority_tier") == "A3"]
return [
ContextPackSection("task_summary", f"SOC governed-context task: {task}", []),
ContextPackSection("security_intended_state", _format_claims(intended, set()), _claim_refs(intended)),
ContextPackSection("sanitized_observed_state", _format_claims(observed, set()) or "No sanitized observed-state claims included.", _claim_refs(observed)),
ContextPackSection("drift_conflicts_and_unknowns", _format_conflicts_stale(conflicts, stale) + "\n" + _format_list(unresolved, "No unresolved security-context questions detected."), _claim_refs(stale)),
ContextPackSection("related_security_runbooks", _format_refs([ref for ref in included_refs if ref.get("type") in {"Runbook", "Policy", "Architecture", "Lesson"}], None), _ref_ids(included_refs, None)),
ContextPackSection("safe_security_boundaries", "Use sanitized, cited context only. No raw logs, packet captures, credentials, broad scans, production mutation, or unapproved live diagnostics.", []),
ContextPackSection("forbidden_actions", "Knowledge retrieval does not authorize remediation or active probing. Any RT-2 probe requires an immutable senior approval bound to its exact scope.", []),
ContextPackSection("unresolved_questions", _format_list(unresolved, "No unresolved questions detected."), []),
]


def _advisory_enrichment_refs(refs: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [ref for ref in refs if str(ref.get("concept_id") or "").startswith("generated/enriched/")]

Expand Down
Loading