@miller-tech/uap 1.40.1 → 1.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +17 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver-defaults.d.ts +23 -0
- package/dist/cli/deliver-defaults.d.ts.map +1 -0
- package/dist/cli/deliver-defaults.js +121 -0
- package/dist/cli/deliver-defaults.js.map +1 -0
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +50 -2
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +29 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/react.d.ts +25 -0
- package/dist/cli/react.d.ts.map +1 -0
- package/dist/cli/react.js +59 -0
- package/dist/cli/react.js.map +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +19 -0
- package/dist/cli/setup.js.map +1 -1
- package/dist/coordination/reactor.d.ts +38 -0
- package/dist/coordination/reactor.d.ts.map +1 -0
- package/dist/coordination/reactor.js +124 -0
- package/dist/coordination/reactor.js.map +1 -0
- package/dist/mcp-router/server.d.ts +2 -1
- package/dist/mcp-router/server.d.ts.map +1 -1
- package/dist/mcp-router/server.js +5 -2
- package/dist/mcp-router/server.js.map +1 -1
- package/dist/mcp-router/tools/react.d.ts +58 -0
- package/dist/mcp-router/tools/react.d.ts.map +1 -0
- package/dist/mcp-router/tools/react.js +57 -0
- package/dist/mcp-router/tools/react.js.map +1 -0
- package/dist/memory/model-router.d.ts +1 -1
- package/dist/memory/model-router.d.ts.map +1 -1
- package/dist/memory/model-router.js +27 -1
- package/dist/memory/model-router.js.map +1 -1
- package/dist/models/openai-compat-client.d.ts.map +1 -1
- package/dist/models/openai-compat-client.js +5 -0
- package/dist/models/openai-compat-client.js.map +1 -1
- package/dist/models/types.d.ts +8 -0
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +22 -0
- package/dist/models/types.js.map +1 -1
- package/dist/policies/policy-tools.d.ts +7 -0
- package/dist/policies/policy-tools.d.ts.map +1 -1
- package/dist/policies/policy-tools.js +24 -2
- package/dist/policies/policy-tools.js.map +1 -1
- package/dist/types/config.d.ts +12 -0
- package/dist/types/config.d.ts.map +1 -1
- package/docs/design/UAP_REACTOR.md +170 -0
- package/package.json +3 -1
- package/src/policies/enforcers/7ebbc721-7540-4e9f-879a-770e0213a09b_architecture_review.py +101 -0
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/_common.py +100 -0
- package/src/policies/enforcers/artifact_hygiene.py +52 -0
- package/src/policies/enforcers/cluster_routing.py +63 -0
- package/src/policies/enforcers/codebase_read_before_plan.py +52 -0
- package/src/policies/enforcers/coord_overlap.py +81 -0
- package/src/policies/enforcers/delivery_enforcement.py +97 -0
- package/src/policies/enforcers/doc_live_over_report.py +50 -0
- package/src/policies/enforcers/expert_review_required.py +135 -0
- package/src/policies/enforcers/iac_parity.py +53 -0
- package/src/policies/enforcers/mcp_router_first.py +37 -0
- package/src/policies/enforcers/memory_before_plan.py +61 -0
- package/src/policies/enforcers/parallel_reads.py +50 -0
- package/src/policies/enforcers/rtk_wrap.py +44 -0
- package/src/policies/enforcers/schema_diff_gate.py +80 -0
- package/src/policies/enforcers/session_memory_write.py +52 -0
- package/src/policies/enforcers/task_required.py +131 -0
- package/src/policies/enforcers/test_gate.py +58 -0
- package/src/policies/enforcers/validate_plan_before_build.py +75 -0
- package/src/policies/enforcers/worktree_required.py +57 -0
- package/src/policies/schemas/policies/architecture-review.md +51 -0
- package/src/policies/schemas/policies/artifact-hygiene.md +29 -0
- package/src/policies/schemas/policies/cluster-routing.md +31 -0
- package/src/policies/schemas/policies/codebase-read-before-plan.md +30 -0
- package/src/policies/schemas/policies/coord-overlap.md +24 -0
- package/src/policies/schemas/policies/delivery-enforcement.md +45 -0
- package/src/policies/schemas/policies/doc-live-over-report.md +32 -0
- package/src/policies/schemas/policies/expert-review-required.md +60 -0
- package/src/policies/schemas/policies/iac-parity.md +31 -0
- package/src/policies/schemas/policies/mandatory-testing-deployment.md +147 -0
- package/src/policies/schemas/policies/mcp-router-first.md +24 -0
- package/src/policies/schemas/policies/memory-before-plan.md +24 -0
- package/src/policies/schemas/policies/merge-deploy-monitor-verify.md +145 -0
- package/src/policies/schemas/policies/parallel-reads.md +24 -0
- package/src/policies/schemas/policies/rtk-wrap.md +26 -0
- package/src/policies/schemas/policies/schema-diff-gate.md +30 -0
- package/src/policies/schemas/policies/session-memory-write.md +24 -0
- package/src/policies/schemas/policies/task-required.md +49 -0
- package/src/policies/schemas/policies/test-gate.md +24 -0
- package/src/policies/schemas/policies/validate-plan-before-build.md +28 -0
- package/src/policies/schemas/policies/worktree-required.md +28 -0
- package/templates/hooks/uap-policy-gate.sh +5 -0
- package/templates/hooks/uap-reactor-prompt.sh +44 -0
- package/templates/hooks/uap-schema-post.sh +26 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Shared helpers for UAP policy enforcers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_cli() -> tuple[str, dict[str, Any]]:
|
|
13
|
+
p = argparse.ArgumentParser()
|
|
14
|
+
p.add_argument("--operation", required=True)
|
|
15
|
+
p.add_argument("--args", default="{}")
|
|
16
|
+
ns = p.parse_args()
|
|
17
|
+
try:
|
|
18
|
+
args = json.loads(ns.args)
|
|
19
|
+
except json.JSONDecodeError:
|
|
20
|
+
args = {}
|
|
21
|
+
return ns.operation, args
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def emit(allowed: bool, reason: str, **extra: Any) -> None:
|
|
25
|
+
payload: dict[str, Any] = {"allowed": allowed, "reason": reason}
|
|
26
|
+
payload.update(extra)
|
|
27
|
+
json.dump(payload, sys.stdout)
|
|
28
|
+
sys.exit(0 if allowed else 2)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def repo_root() -> Path:
|
|
32
|
+
env = os.environ.get("UAP_REPO_ROOT")
|
|
33
|
+
if env:
|
|
34
|
+
return Path(env)
|
|
35
|
+
cwd = Path.cwd()
|
|
36
|
+
for p in [cwd, *cwd.parents]:
|
|
37
|
+
if (p / ".git").exists():
|
|
38
|
+
return p
|
|
39
|
+
return cwd
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def worktree_root() -> Path:
|
|
43
|
+
"""Root of the current WORKING TREE for git operations.
|
|
44
|
+
|
|
45
|
+
Distinct from repo_root() (the main checkout, where runtime data like
|
|
46
|
+
policies.db lives). git-diff based enforcers must run against the working
|
|
47
|
+
tree — which is the worktree when an operation runs from inside one. The
|
|
48
|
+
policy gate exports UAP_WORKTREE_ROOT; fall back to `git rev-parse` from cwd,
|
|
49
|
+
then to repo_root().
|
|
50
|
+
"""
|
|
51
|
+
env = os.environ.get("UAP_WORKTREE_ROOT")
|
|
52
|
+
if env:
|
|
53
|
+
return Path(env)
|
|
54
|
+
try:
|
|
55
|
+
r = subprocess.run(
|
|
56
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
57
|
+
capture_output=True, text=True, timeout=3, env=_clean_env(),
|
|
58
|
+
)
|
|
59
|
+
if r.returncode == 0 and r.stdout.strip():
|
|
60
|
+
return Path(r.stdout.strip())
|
|
61
|
+
except Exception: # noqa: BLE001
|
|
62
|
+
pass
|
|
63
|
+
return repo_root()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# git exports repo-context vars (GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, ...)
|
|
67
|
+
# into hook environments. An enforcer spawned during a hook would then run its
|
|
68
|
+
# own git calls against the HOOK'S repo instead of cwd — silently no-op'ing
|
|
69
|
+
# every git-diff based check. Strip them so cwd decides the repo.
|
|
70
|
+
_GIT_CONTEXT_VARS = (
|
|
71
|
+
"GIT_DIR",
|
|
72
|
+
"GIT_WORK_TREE",
|
|
73
|
+
"GIT_INDEX_FILE",
|
|
74
|
+
"GIT_COMMON_DIR",
|
|
75
|
+
"GIT_OBJECT_DIRECTORY",
|
|
76
|
+
"GIT_PREFIX",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _clean_env() -> dict[str, str]:
|
|
81
|
+
return {k: v for k, v in os.environ.items() if k not in _GIT_CONTEXT_VARS}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def run(cmd: list[str], cwd: Path | None = None, timeout: int = 5) -> tuple[int, str, str]:
|
|
85
|
+
try:
|
|
86
|
+
r = subprocess.run(
|
|
87
|
+
cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout,
|
|
88
|
+
env=_clean_env(),
|
|
89
|
+
)
|
|
90
|
+
return r.returncode, r.stdout, r.stderr
|
|
91
|
+
except Exception as e: # noqa: BLE001
|
|
92
|
+
return 1, "", str(e)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def arg_str(args: dict[str, Any]) -> str:
|
|
96
|
+
"""Flatten args to a single lowercase string for substring checks."""
|
|
97
|
+
try:
|
|
98
|
+
return json.dumps(args, default=str).lower()
|
|
99
|
+
except Exception: # noqa: BLE001
|
|
100
|
+
return str(args).lower()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""artifact-hygiene enforcer: block binary artifacts outside curated dirs."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
9
|
+
from _common import emit, parse_cli # noqa: E402
|
|
10
|
+
|
|
11
|
+
BINARY_RE = re.compile(r"\.(png|jpe?g|gif|pdf|zip|tar\.gz|tgz|db|sqlite\d?)$", re.I)
|
|
12
|
+
ALLOWED_PREFIXES = (
|
|
13
|
+
"docs/",
|
|
14
|
+
"tests/",
|
|
15
|
+
"apps/",
|
|
16
|
+
"agents/data/memory/",
|
|
17
|
+
".playwright-mcp/",
|
|
18
|
+
"observability/",
|
|
19
|
+
)
|
|
20
|
+
ALLOWED_SUBSTRINGS = ("/__screenshots__/", "/public/", "/static/", "/assets/")
|
|
21
|
+
WRITE_OPS = {"Write", "write", "create-file"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
op, args = parse_cli()
|
|
26
|
+
if op not in WRITE_OPS:
|
|
27
|
+
emit(True, "not a write op")
|
|
28
|
+
|
|
29
|
+
path = (args.get("file_path") or args.get("path") or "").replace("\\", "/")
|
|
30
|
+
if not BINARY_RE.search(path):
|
|
31
|
+
emit(True, "not a binary artifact")
|
|
32
|
+
|
|
33
|
+
rel = path
|
|
34
|
+
for marker in ("/pay2u/", "/miller-tech/"):
|
|
35
|
+
if marker in rel:
|
|
36
|
+
rel = rel.split(marker, 1)[1]
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
if any(rel.startswith(p) for p in ALLOWED_PREFIXES):
|
|
40
|
+
emit(True, f"allowed prefix: {rel}")
|
|
41
|
+
if any(s in rel for s in ALLOWED_SUBSTRINGS):
|
|
42
|
+
emit(True, f"allowed subdir: {rel}")
|
|
43
|
+
|
|
44
|
+
emit(
|
|
45
|
+
False,
|
|
46
|
+
f"artifact-hygiene: binary '{rel}' must live under docs/, tests/, or apps/**/public|static|assets. "
|
|
47
|
+
"Do not litter repo root.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
main()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cluster-routing enforcer: kubectl/helm context must match component domain."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
9
|
+
from _common import arg_str, emit, parse_cli, run # noqa: E402
|
|
10
|
+
|
|
11
|
+
DOMAINS = {
|
|
12
|
+
"openobserve": re.compile(
|
|
13
|
+
r"grafana|prometheus|openobserve|fluent-?bit|servicemonitor|alertmanager|loki|tempo|jaeger",
|
|
14
|
+
re.I,
|
|
15
|
+
),
|
|
16
|
+
"zitadel": re.compile(r"zitadel|oidc|keycloak|iam-crd", re.I),
|
|
17
|
+
}
|
|
18
|
+
CONTEXTS = {
|
|
19
|
+
"openobserve": "do-syd1-pay2u-openobserve",
|
|
20
|
+
"zitadel": "do-syd1-zitadel",
|
|
21
|
+
"main": "do-syd1-pay2u",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def pick_domain(blob: str) -> str:
|
|
26
|
+
for d, rx in DOMAINS.items():
|
|
27
|
+
if rx.search(blob):
|
|
28
|
+
return d
|
|
29
|
+
return "main"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> None:
|
|
33
|
+
op, args = parse_cli()
|
|
34
|
+
blob = f"{op} {arg_str(args)}"
|
|
35
|
+
|
|
36
|
+
if not re.search(r"\b(kubectl|helm)\b", blob):
|
|
37
|
+
emit(True, "not a kubectl/helm call")
|
|
38
|
+
|
|
39
|
+
if not re.search(
|
|
40
|
+
r"\b(apply|patch|create|edit|delete|install|upgrade|uninstall|rollout)\b", blob
|
|
41
|
+
):
|
|
42
|
+
emit(True, "read-only kubectl/helm call")
|
|
43
|
+
|
|
44
|
+
rc, out, _ = run(["kubectl", "config", "current-context"])
|
|
45
|
+
ctx = out.strip() if rc == 0 else ""
|
|
46
|
+
|
|
47
|
+
wanted_domain = pick_domain(blob)
|
|
48
|
+
wanted_ctx = CONTEXTS[wanted_domain]
|
|
49
|
+
|
|
50
|
+
if ctx != wanted_ctx:
|
|
51
|
+
emit(
|
|
52
|
+
False,
|
|
53
|
+
f"cluster-routing: context '{ctx}' does not match domain "
|
|
54
|
+
f"'{wanted_domain}'. Run: kubectl config use-context {wanted_ctx}",
|
|
55
|
+
wanted_context=wanted_ctx,
|
|
56
|
+
current_context=ctx,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
emit(True, f"context '{ctx}' matches domain '{wanted_domain}'")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""codebase-read-before-plan enforcer: plans require prior reads of target paths."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
11
|
+
from _common import arg_str, emit, parse_cli # noqa: E402
|
|
12
|
+
|
|
13
|
+
PLAN_OPS = {"ExitPlanMode", "Plan", "TodoWrite"}
|
|
14
|
+
PLAN_WORD_RE = re.compile(r"(?<![-\w/])(plan the|design the|architect the|propose a plan|spec the)", re.I)
|
|
15
|
+
READ_LOG = Path(os.environ.get("UAP_STATE_DIR", ".uap")) / "read_log.state"
|
|
16
|
+
RECENT_SEC = 1800
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def recent_reads() -> set[str]:
|
|
20
|
+
if not READ_LOG.exists():
|
|
21
|
+
return set()
|
|
22
|
+
out: set[str] = set()
|
|
23
|
+
now = time.time()
|
|
24
|
+
for line in READ_LOG.read_text().splitlines():
|
|
25
|
+
try:
|
|
26
|
+
ts, path = line.split("\t", 1)
|
|
27
|
+
if now - float(ts) < RECENT_SEC:
|
|
28
|
+
out.add(path)
|
|
29
|
+
except ValueError:
|
|
30
|
+
continue
|
|
31
|
+
return out
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> None:
|
|
35
|
+
op, args = parse_cli()
|
|
36
|
+
blob = f"{op} {arg_str(args)}"
|
|
37
|
+
if op not in PLAN_OPS and not PLAN_WORD_RE.search(blob):
|
|
38
|
+
emit(True, "not a plan op")
|
|
39
|
+
|
|
40
|
+
reads = recent_reads()
|
|
41
|
+
if reads:
|
|
42
|
+
emit(True, f"{len(reads)} recent codebase reads on record")
|
|
43
|
+
|
|
44
|
+
emit(
|
|
45
|
+
False,
|
|
46
|
+
"codebase-read-before-plan: no Read/Grep/Glob within the last 30 min. "
|
|
47
|
+
"Read the existing codebase in the target scope before emitting a plan.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
main()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""coord-overlap enforcer: check for in-flight agent path reservations."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import sqlite3
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
9
|
+
from _common import emit, parse_cli, repo_root # noqa: E402
|
|
10
|
+
|
|
11
|
+
AGENT_OPS = {"Agent", "spawn-agent", "subagent", "delegate"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def overlapping_reservations(root: Path, paths: list[str]) -> list[str]:
|
|
15
|
+
db = root / "agents" / "data" / "coordination" / "coordination.db"
|
|
16
|
+
if not db.exists() or not paths:
|
|
17
|
+
return []
|
|
18
|
+
try:
|
|
19
|
+
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=1.0)
|
|
20
|
+
# Best-effort: look for any reservations table with path-like column
|
|
21
|
+
cur = con.execute(
|
|
22
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
23
|
+
)
|
|
24
|
+
tables = [r[0] for r in cur.fetchall()]
|
|
25
|
+
hits: list[str] = []
|
|
26
|
+
for t in tables:
|
|
27
|
+
try:
|
|
28
|
+
cols = [r[1] for r in con.execute(f"PRAGMA table_info({t})")]
|
|
29
|
+
path_col = next(
|
|
30
|
+
(c for c in cols if c.lower() in ("path", "paths", "file", "scope")),
|
|
31
|
+
None,
|
|
32
|
+
)
|
|
33
|
+
status_col = next(
|
|
34
|
+
(c for c in cols if c.lower() in ("status", "state", "active")), None
|
|
35
|
+
)
|
|
36
|
+
if not path_col:
|
|
37
|
+
continue
|
|
38
|
+
where = f" WHERE {status_col} IN ('active','in_progress',1)" if status_col else ""
|
|
39
|
+
rows = con.execute(f"SELECT {path_col} FROM {t}{where}").fetchall()
|
|
40
|
+
for (v,) in rows:
|
|
41
|
+
if not v:
|
|
42
|
+
continue
|
|
43
|
+
for p in paths:
|
|
44
|
+
if p and p in str(v):
|
|
45
|
+
hits.append(f"{t}:{v}")
|
|
46
|
+
except sqlite3.Error:
|
|
47
|
+
continue
|
|
48
|
+
con.close()
|
|
49
|
+
return hits
|
|
50
|
+
except sqlite3.Error:
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main() -> None:
|
|
55
|
+
op, args = parse_cli()
|
|
56
|
+
if op not in AGENT_OPS:
|
|
57
|
+
emit(True, "not an agent-spawn op")
|
|
58
|
+
|
|
59
|
+
paths_raw = (
|
|
60
|
+
args.get("paths")
|
|
61
|
+
or args.get("scope")
|
|
62
|
+
or args.get("prompt", "")
|
|
63
|
+
)
|
|
64
|
+
if isinstance(paths_raw, list):
|
|
65
|
+
paths = [str(p) for p in paths_raw]
|
|
66
|
+
else:
|
|
67
|
+
paths = [p for p in str(paths_raw).split() if "/" in p]
|
|
68
|
+
|
|
69
|
+
hits = overlapping_reservations(repo_root(), paths)
|
|
70
|
+
if hits:
|
|
71
|
+
emit(
|
|
72
|
+
False,
|
|
73
|
+
f"coord-overlap: active reservations on: {', '.join(hits[:5])}. "
|
|
74
|
+
"Run `uap coordination check` before spawning.",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
emit(True, "no overlapping reservations")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""delivery-enforcement enforcer: route substantive coding through `uap deliver`.
|
|
3
|
+
|
|
4
|
+
Fires on Edit/Write/MultiEdit to source-code files. The intent is that
|
|
5
|
+
non-trivial coding work goes through the `uap deliver` convergence loop (which
|
|
6
|
+
drives a model to verified completion against the real gates) rather than
|
|
7
|
+
ad-hoc hand edits.
|
|
8
|
+
|
|
9
|
+
SAFETY — default mode is ADVISORY (always allows, logs a nudge), so installing
|
|
10
|
+
this policy never breaks editing. Strict enforcement is opt-in:
|
|
11
|
+
|
|
12
|
+
UAP_ENFORCE_DELIVERY=block # direct source edits outside a deliver context
|
|
13
|
+
# are blocked (exit 2)
|
|
14
|
+
|
|
15
|
+
Escape hatches (always honored, even in block mode):
|
|
16
|
+
- UAP_DELIVER_ACTIVE=1 set by the deliver loop for its own subprocesses
|
|
17
|
+
- UAP_DELIVER_BYPASS=1 explicit operator override for a sanctioned manual edit
|
|
18
|
+
|
|
19
|
+
Exempt by construction: non-source files, docs/configs/scripts/policies, test
|
|
20
|
+
files (deliver protects those itself), and tooling dot-dirs.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
28
|
+
from _common import emit, parse_cli, repo_root # noqa: E402
|
|
29
|
+
|
|
30
|
+
EDIT_OPS = {"Edit", "Write", "MultiEdit", "edit", "write", "multiedit"}
|
|
31
|
+
|
|
32
|
+
# Only real implementation code is gated.
|
|
33
|
+
SOURCE_EXTS = (
|
|
34
|
+
".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs",
|
|
35
|
+
".py", ".go", ".rs", ".java", ".rb", ".php", ".cs", ".swift", ".kt",
|
|
36
|
+
".c", ".cc", ".cpp", ".h", ".hpp",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
EXEMPT_PREFIXES = (
|
|
40
|
+
".claude/", ".cursor/", ".opencode/", ".codex/", ".forge/", ".omp/",
|
|
41
|
+
".uap/", ".policy-tools/", ".worktrees/",
|
|
42
|
+
"src/policies/", "scripts/", "docs/", "policies/", "test/", "tests/",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Test files are protected by deliver itself; never gate them here.
|
|
46
|
+
TEST_MARKERS = (".test.", ".spec.", "_test.", "/test/", "/tests/", "/__tests__/")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> None:
|
|
50
|
+
op, args = parse_cli()
|
|
51
|
+
if op not in EDIT_OPS:
|
|
52
|
+
emit(True, "not a file-edit operation")
|
|
53
|
+
|
|
54
|
+
target = args.get("file_path") or args.get("path") or args.get("target") or ""
|
|
55
|
+
if not target:
|
|
56
|
+
emit(True, "no file path in args")
|
|
57
|
+
|
|
58
|
+
root = repo_root()
|
|
59
|
+
try:
|
|
60
|
+
rel = str(Path(target).resolve().relative_to(root))
|
|
61
|
+
except ValueError:
|
|
62
|
+
emit(True, "target outside repo")
|
|
63
|
+
|
|
64
|
+
rel_posix = rel.replace(os.sep, "/")
|
|
65
|
+
low = rel_posix.lower()
|
|
66
|
+
|
|
67
|
+
if not low.endswith(SOURCE_EXTS):
|
|
68
|
+
emit(True, "not source code")
|
|
69
|
+
if any(rel_posix.startswith(p) for p in EXEMPT_PREFIXES):
|
|
70
|
+
emit(True, f"exempt path: {rel_posix}")
|
|
71
|
+
if any(m in "/" + low for m in TEST_MARKERS):
|
|
72
|
+
emit(True, "test file (protected by deliver itself)")
|
|
73
|
+
|
|
74
|
+
# Escape hatches.
|
|
75
|
+
if os.environ.get("UAP_DELIVER_ACTIVE") == "1":
|
|
76
|
+
emit(True, "inside a deliver-driven run")
|
|
77
|
+
if os.environ.get("UAP_DELIVER_BYPASS") == "1":
|
|
78
|
+
emit(True, "UAP_DELIVER_BYPASS override set")
|
|
79
|
+
|
|
80
|
+
msg = (
|
|
81
|
+
f"delivery-enforcement: '{rel_posix}' is source code being edited directly. "
|
|
82
|
+
"Route substantive coding through `uap deliver` (drives a model to verified "
|
|
83
|
+
"completion against the gates), or set UAP_DELIVER_BYPASS=1 for a sanctioned "
|
|
84
|
+
"manual edit."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
mode = os.environ.get("UAP_ENFORCE_DELIVERY", "advisory").lower()
|
|
88
|
+
if mode == "block":
|
|
89
|
+
emit(False, msg)
|
|
90
|
+
|
|
91
|
+
# Advisory (default): never blocks. Surface the nudge, then allow.
|
|
92
|
+
print(f"[delivery-enforcement advisory] {msg}", file=sys.stderr)
|
|
93
|
+
emit(True, "advisory: nudge logged (set UAP_ENFORCE_DELIVERY=block to enforce)")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""doc-live-over-report enforcer: block new *_REPORT/*_COMPLETE/*_SUMMARY/*_PLAN md files."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
9
|
+
from _common import emit, parse_cli # noqa: E402
|
|
10
|
+
|
|
11
|
+
BLOCKED_RE = re.compile(
|
|
12
|
+
r"(_REPORT|_COMPLETE|_SUMMARY|_PLAN|_FIX_\d|_\d{4}-\d{2}-\d{2})\.md$",
|
|
13
|
+
re.I,
|
|
14
|
+
)
|
|
15
|
+
SCOPED_DIRS = ("infra/", "docs/", "")
|
|
16
|
+
WRITE_OPS = {"Write", "write", "create-file"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> None:
|
|
20
|
+
op, args = parse_cli()
|
|
21
|
+
if op not in WRITE_OPS:
|
|
22
|
+
emit(True, "not a write op")
|
|
23
|
+
|
|
24
|
+
path = args.get("file_path") or args.get("path") or ""
|
|
25
|
+
if not path.endswith(".md"):
|
|
26
|
+
emit(True, "not a markdown file")
|
|
27
|
+
|
|
28
|
+
rel = path.replace("\\", "/")
|
|
29
|
+
# Strip leading abs path prefix if present
|
|
30
|
+
for marker in ("/pay2u/", "/miller-tech/"):
|
|
31
|
+
if marker in rel:
|
|
32
|
+
rel = rel.split(marker, 1)[1]
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
is_scoped = any(rel.startswith(d) for d in SCOPED_DIRS if d) or "/" not in rel
|
|
36
|
+
if not is_scoped:
|
|
37
|
+
emit(True, "out-of-scope path")
|
|
38
|
+
|
|
39
|
+
if BLOCKED_RE.search(rel):
|
|
40
|
+
emit(
|
|
41
|
+
False,
|
|
42
|
+
f"doc-live-over-report: '{rel}' is a retrospective/dated doc pattern. "
|
|
43
|
+
"Update canonical README/runbook instead.",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
emit(True, "not a blocked doc pattern")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
main()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""expert-review-required enforcer: a parallel expert review must precede ship.
|
|
3
|
+
|
|
4
|
+
Blocks ship actions (git commit / git push / gh pr create / merge / pr-ready /
|
|
5
|
+
signoff) unless a review artifact exists for the current branch AND covers the
|
|
6
|
+
current HEAD. This makes the `parallel-expert-review` skill's "REQUIRED by
|
|
7
|
+
policy" claim real rather than advisory.
|
|
8
|
+
|
|
9
|
+
Review artifact: .uap/reviews/<branch-slug>.json, written by the
|
|
10
|
+
parallel-expert-review flow on consolidation. Recognised shape:
|
|
11
|
+
{ "head": "<sha>", "verdict": "approve|...", "reviewers": [...] }
|
|
12
|
+
If the artifact carries a `head` that differs from the current HEAD, the review
|
|
13
|
+
is stale and the op is blocked.
|
|
14
|
+
|
|
15
|
+
Fail-open: if branch/HEAD cannot be resolved, the op is allowed — non-UAP repos
|
|
16
|
+
and detached states are unaffected. Override: set UAP_NO_REVIEW=1 to bypass.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
27
|
+
from _common import emit, parse_cli, repo_root, run # noqa: E402
|
|
28
|
+
|
|
29
|
+
# Ship verbs are anchored to their tool prefix so that the bare tokens "merge"
|
|
30
|
+
# or "signoff" inside read-only commands (git diff --merge-base, rg merge,
|
|
31
|
+
# cat docs/merge-strategy.md) do not trip the gate.
|
|
32
|
+
SHIP_PATTERNS = (
|
|
33
|
+
re.compile(r"\bgit\s+(commit|push|merge)\b"),
|
|
34
|
+
re.compile(r"\bgh\s+pr\s+(create|merge|ready)\b"),
|
|
35
|
+
re.compile(r"\b(pr[-_ ]?ready|sign[-_ ]?off|ready[-_ ]for[-_ ]review)\b", re.I),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def current_branch(root: Path) -> str | None:
|
|
40
|
+
# symbolic-ref resolves the branch name even on an unborn branch (no commits
|
|
41
|
+
# yet); rev-parse --abbrev-ref returns "HEAD" in that state.
|
|
42
|
+
rc, out, _ = run(["git", "symbolic-ref", "--short", "HEAD"], cwd=root)
|
|
43
|
+
if rc == 0 and out.strip():
|
|
44
|
+
return out.strip()
|
|
45
|
+
rc, out, _ = run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=root)
|
|
46
|
+
if rc == 0 and out.strip() and out.strip() != "HEAD":
|
|
47
|
+
return out.strip()
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def slug_for(branch: str) -> str:
|
|
52
|
+
"""Injective filename slug for a branch ref.
|
|
53
|
+
|
|
54
|
+
A naive `/`->`-` substitution collapses distinct refs (`feature/foo` and
|
|
55
|
+
`feature-foo`, which can coexist) onto the same artifact, silently bypassing
|
|
56
|
+
the gate. Percent-encode `%` first, then `/`, so the mapping is reversible
|
|
57
|
+
and collision-free: `feature/foo` -> `feature%2Ffoo`, `feature-foo` stays
|
|
58
|
+
`feature-foo`.
|
|
59
|
+
"""
|
|
60
|
+
return branch.replace("%", "%25").replace("/", "%2F")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def head_sha(root: Path) -> str | None:
|
|
64
|
+
rc, out, _ = run(["git", "rev-parse", "HEAD"], cwd=root)
|
|
65
|
+
return out.strip() if rc == 0 and out.strip() else None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main() -> None:
|
|
69
|
+
op, args = parse_cli()
|
|
70
|
+
|
|
71
|
+
if os.environ.get("UAP_NO_REVIEW") == "1":
|
|
72
|
+
emit(True, "UAP_NO_REVIEW override set")
|
|
73
|
+
|
|
74
|
+
op_l = op.lower()
|
|
75
|
+
if op_l != "bash":
|
|
76
|
+
emit(True, "not a ship operation")
|
|
77
|
+
|
|
78
|
+
cmd = args.get("command") or args.get("cmd") or ""
|
|
79
|
+
if not any(p.search(cmd) for p in SHIP_PATTERNS):
|
|
80
|
+
emit(True, "not a ship action")
|
|
81
|
+
|
|
82
|
+
root = repo_root()
|
|
83
|
+
branch = current_branch(root)
|
|
84
|
+
if branch is None:
|
|
85
|
+
emit(True, "branch not resolvable (detached/non-git) — fail-open")
|
|
86
|
+
slug = slug_for(branch)
|
|
87
|
+
|
|
88
|
+
review = root / ".uap" / "reviews" / f"{slug}.json"
|
|
89
|
+
if not review.exists():
|
|
90
|
+
emit(
|
|
91
|
+
False,
|
|
92
|
+
f"expert-review-required: no review artifact at .uap/reviews/{slug}.json. "
|
|
93
|
+
"Run the parallel-expert-review skill (code-quality, security, performance, "
|
|
94
|
+
"docs, test-coverage reviewers) and record the consolidated verdict before "
|
|
95
|
+
"shipping. Override for one-off meta-work: UAP_NO_REVIEW=1.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
head = head_sha(root)
|
|
99
|
+
try:
|
|
100
|
+
data = json.loads(review.read_text())
|
|
101
|
+
except Exception: # noqa: BLE001
|
|
102
|
+
data = {}
|
|
103
|
+
|
|
104
|
+
# Defense-in-depth against artifact reuse across branches: if the artifact
|
|
105
|
+
# records the branch it covers, it must match the current branch.
|
|
106
|
+
artifact_branch = data.get("branch") if isinstance(data, dict) else None
|
|
107
|
+
if artifact_branch and artifact_branch != branch:
|
|
108
|
+
emit(
|
|
109
|
+
False,
|
|
110
|
+
f"expert-review-required: review at .uap/reviews/{slug}.json covers branch "
|
|
111
|
+
f"'{artifact_branch}', not '{branch}'. Re-run the parallel expert review on "
|
|
112
|
+
"this branch. Override: UAP_NO_REVIEW=1.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Stale check relative to current HEAD.
|
|
116
|
+
reviewed_head = data.get("head") if isinstance(data, dict) else None
|
|
117
|
+
if reviewed_head and head and reviewed_head != head:
|
|
118
|
+
emit(
|
|
119
|
+
False,
|
|
120
|
+
f"expert-review-required: review at .uap/reviews/{slug}.json covers "
|
|
121
|
+
f"{reviewed_head[:8]} but HEAD is {head[:8]} — the review is stale. "
|
|
122
|
+
"Re-run the parallel expert review for the current changes. "
|
|
123
|
+
"Override: UAP_NO_REVIEW=1.",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
emit(
|
|
127
|
+
True,
|
|
128
|
+
f"expert-review satisfied (.uap/reviews/{slug}.json"
|
|
129
|
+
+ (f", head {reviewed_head[:8]}" if reviewed_head else "")
|
|
130
|
+
+ ")",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""iac-parity enforcer: live-state changes must have matching IaC diff."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
9
|
+
from _common import arg_str, emit, parse_cli, run, worktree_root # noqa: E402
|
|
10
|
+
|
|
11
|
+
MUTATING_RE = re.compile(
|
|
12
|
+
r"\b(kubectl|helm|doctl|aws|gcloud)\b.*?\b(apply|patch|create|edit|delete|install|upgrade|rollout|scale|set)\b",
|
|
13
|
+
re.I,
|
|
14
|
+
)
|
|
15
|
+
IAC_PATHS = (
|
|
16
|
+
"infra/terraform/",
|
|
17
|
+
"infra/helm_charts/",
|
|
18
|
+
"infra/kubernetes/",
|
|
19
|
+
"infra/k8s/",
|
|
20
|
+
"infra/policies/",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
op, args = parse_cli()
|
|
26
|
+
blob = f"{op} {arg_str(args)}"
|
|
27
|
+
|
|
28
|
+
if not MUTATING_RE.search(blob):
|
|
29
|
+
emit(True, "not a mutating IaC-scope command")
|
|
30
|
+
|
|
31
|
+
root = worktree_root() # git status must run against the working tree, not MAIN_ROOT
|
|
32
|
+
rc, out, _ = run(["git", "status", "--porcelain"], cwd=root)
|
|
33
|
+
if rc != 0:
|
|
34
|
+
emit(True, "git status unavailable; deferring to post-commit check")
|
|
35
|
+
|
|
36
|
+
has_iac = any(
|
|
37
|
+
line[3:].startswith(IAC_PATHS) or line[3:].lstrip().startswith(IAC_PATHS)
|
|
38
|
+
for line in out.splitlines()
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if not has_iac:
|
|
42
|
+
emit(
|
|
43
|
+
False,
|
|
44
|
+
"iac-parity: live-state mutation without matching IaC diff under "
|
|
45
|
+
+ ", ".join(IAC_PATHS)
|
|
46
|
+
+ ". Update Terraform/Helm/K8s manifests in the same worktree.",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
emit(True, "IaC diff present in worktree")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|