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
34 changes: 34 additions & 0 deletions docs/ops/CREDENTIAL-INVENTORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Credential inventory (names only)

Policy for issue #184. **Never commit secret values.** Rotate expired tokens off-repo.

## Operator rules
- Inventory records name, owner surface, last-used class, and intended scope.
- Values live in GitHub Actions secrets / device keyrings only.
- Classic PATs with unused admin scopes should be rotated to fine-grained tokens.
- Expired fine-grained tokens must be deleted, not reused.

## Named surfaces (no values)

| Name | Class | Intended use | Notes |
|---|---|---|---|
| `OPERATOR` | Actions secret | repo admin automation | Full access granted for steward ops; do not echo |
| `ARCHWIZ_GITHUB_TOKEN` | fine-grained PAT | unused historically | Rotate or delete if still present |
| `DeepSeek_account-1` | fine-grained PAT | expired | Delete; do not renew as classic |
| `Gh_actions` | classic PAT | Actions + API | Prefer fine-grained replacement |
| `gitlab_cicd-w+org_view` | classic PAT | GitLab non-gate | GitLab remains non-blocking |
| `gitlab_cicd` | classic PAT | unused | Candidate delete |
| `timers-full+accessPAT` | classic PAT | unused | Candidate delete |
| `DEEPSEEK_API_KEY` | classic PAT name collision | unused | Name is not an API key store in-repo |

## App classes (non-gate)
CodeRabbit, Qodo, Devin, Jules, Copilot, Vercel, Linear, Grok GitHub App are review/deploy adapters. Dual-gate remains `repo-gate` + `termux-smoke` on the candidate SHA.

## Rotation checklist
1. Confirm last-used class from GitHub settings UI (do not paste tokens).
2. Replace unused classic admin-scope tokens with fine-grained repo-scoped tokens.
3. Delete expired tokens.
4. Rebind Actions secrets by name only.
5. Record rotation date here without values.

Last inventory pass: 2026-09-24 (names-only recon; no secret material).
16 changes: 16 additions & 0 deletions docs/ops/ML-KEEPALIVE-EXTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ML keep-alive extract (AR-23 / MLP-KEEP-001)

Live master at extract time: `e82a9b16`.

## Why this slice
Wholesale ML PRs `#432 #549 #601 #682 #746 #787 #817` stay EXTRACT/WAIT. This package is the smallest observe-mode DAG that can land without pulling those trees.

## Contract
- Package: `ml/pipelines/`
- Mode: `observe` only
- Dependencies: Python stdlib
- Tests: `ml/pipelines/test_keepalive_dag.py`
- No credentials, no remote inference, no GitHub write from the DAG

## Promote rule
Dual-gate on **this** head SHA. Vercel/Copilot/CodeRabbit are non-gate.
5 changes: 5 additions & 0 deletions ml/pipelines/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Slim keep-alive ML pipeline package (AR-23 / MLP-KEEP-001)."""

from .keepalive_dag import DagSpec, default_dag, render_mermaid, validate_dag

__all__ = ["DagSpec", "default_dag", "render_mermaid", "validate_dag"]
99 changes: 99 additions & 0 deletions ml/pipelines/keepalive_dag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Observe-mode keep-alive DAG. Stdlib only. No network. No secrets."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Iterable


@dataclass(frozen=True)
class DagNode:
node_id: str
kind: str
description: str


@dataclass(frozen=True)
class DagEdge:
source: str
target: str


@dataclass(frozen=True)
class DagSpec:
name: str
nodes: tuple[DagNode, ...]
edges: tuple[DagEdge, ...]
mode: str = "observe"
notes: tuple[str, ...] = field(default_factory=tuple)

def node_ids(self) -> set[str]:
return {n.node_id for n in self.nodes}


ALLOWED_KINDS = frozenset({"ingest", "validate", "score", "ledger", "export"})


def default_dag() -> DagSpec:
nodes = (
DagNode("ingest_events", "ingest", "Read local JSONL operator events"),
DagNode("schema_validate", "validate", "Validate event schema with stdlib json"),
DagNode("score_throughput", "score", "Compute agent throughput counters"),
DagNode("write_ledger", "ledger", "Append effectiveness ledger row"),
DagNode("export_status", "export", "Write generated status JSON for dashboards"),
)
edges = (
DagEdge("ingest_events", "schema_validate"),
DagEdge("schema_validate", "score_throughput"),
DagEdge("score_throughput", "write_ledger"),
DagEdge("write_ledger", "export_status"),
)
return DagSpec(
name="ml-keepalive-observe",
nodes=nodes,
edges=edges,
mode="observe",
notes=(
"EXTRACT-only slice. Do not wholesale-merge #682/#746/#787/#817.",
"No credentials. No remote model calls.",
),
)


def validate_dag(spec: DagSpec) -> list[str]:
errors: list[str] = []
ids = spec.node_ids()
if spec.mode != "observe":
errors.append("mode must be observe for this extract")
if len(ids) != len(spec.nodes):
errors.append("duplicate node_id")
for node in spec.nodes:
if node.kind not in ALLOWED_KINDS:
errors.append(f"invalid kind: {node.kind}")
if not node.node_id or not node.description:
errors.append(f"incomplete node: {node.node_id}")
for edge in spec.edges:
if edge.source not in ids:
errors.append(f"missing source: {edge.source}")
if edge.target not in ids:
errors.append(f"missing target: {edge.target}")
if edge.source == edge.target:
errors.append(f"self-edge: {edge.source}")
return errors


def render_mermaid(spec: DagSpec) -> str:
lines = ["flowchart LR"]
for node in spec.nodes:
lines.append(f" {node.node_id}[{node.node_id}]")
for edge in spec.edges:
lines.append(f" {edge.source} --> {edge.target}")
return "\n".join(lines) + "\n"


def iter_ready_nodes(spec: DagSpec, completed: Iterable[str]) -> list[str]:
done = set(completed)
incoming: dict[str, set[str]] = {n.node_id: set() for n in spec.nodes}
for edge in spec.edges:
incoming[edge.target].add(edge.source)
return [nid for nid, deps in incoming.items() if nid not in done and deps <= done]
52 changes: 52 additions & 0 deletions ml/pipelines/test_keepalive_dag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Stdlib tests for the slim keep-alive DAG."""

from __future__ import annotations

import unittest

from ml.pipelines.keepalive_dag import (
DagEdge,
DagNode,
DagSpec,
default_dag,
iter_ready_nodes,
render_mermaid,
validate_dag,
)


class KeepaliveDagTest(unittest.TestCase):
def test_default_valid(self) -> None:
spec = default_dag()
self.assertEqual(validate_dag(spec), [])
self.assertEqual(spec.mode, "observe")
self.assertIn("flowchart LR", render_mermaid(spec))

def test_ready_order(self) -> None:
spec = default_dag()
ready = iter_ready_nodes(spec, [])
self.assertEqual(ready, ["ingest_events"])
ready = iter_ready_nodes(spec, ["ingest_events"])
self.assertEqual(ready, ["schema_validate"])

def test_rejects_bad_kind(self) -> None:
spec = DagSpec(
name="bad",
nodes=(DagNode("a", "remote", "nope"),),
edges=(),
mode="observe",
)
self.assertTrue(validate_dag(spec))

def test_rejects_missing_edge_target(self) -> None:
spec = DagSpec(
name="bad",
nodes=(DagNode("a", "ingest", "ok"),),
edges=(DagEdge("a", "missing"),),
)
self.assertTrue(any("missing target" in e for e in validate_dag(spec)))


if __name__ == "__main__":
unittest.main()
Loading