@arbiterforge/ca-pi 0.6.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/LICENSE +661 -0
- package/README.md +558 -0
- package/package.json +35 -0
- package/plugins/ca-pi/CHANGELOG.md +1030 -0
- package/plugins/ca-pi/COMMANDS.md +90 -0
- package/plugins/ca-pi/ORCHESTRATOR.md +159 -0
- package/plugins/ca-pi/SKILLS.md +47 -0
- package/plugins/ca-pi/SPRINT.md +142 -0
- package/plugins/ca-pi/agents/INDEX.md +31 -0
- package/plugins/ca-pi/agents/architecture-drift-reviewer.md +86 -0
- package/plugins/ca-pi/agents/auth-crypto-reviewer.md +60 -0
- package/plugins/ca-pi/agents/backend-author.md +60 -0
- package/plugins/ca-pi/agents/checkpoint-aggregator.md +111 -0
- package/plugins/ca-pi/agents/coverage-auditor.md +71 -0
- package/plugins/ca-pi/agents/decision-challenger.md +116 -0
- package/plugins/ca-pi/agents/dependency-reviewer.md +79 -0
- package/plugins/ca-pi/agents/design-quality-reviewer.md +80 -0
- package/plugins/ca-pi/agents/finding-triage.md +86 -0
- package/plugins/ca-pi/agents/frontend-author.md +64 -0
- package/plugins/ca-pi/agents/grader.md +173 -0
- package/plugins/ca-pi/agents/infra-author.md +64 -0
- package/plugins/ca-pi/agents/map-deps.md +35 -0
- package/plugins/ca-pi/agents/map-structure.md +37 -0
- package/plugins/ca-pi/agents/migration-reviewer.md +65 -0
- package/plugins/ca-pi/agents/scout.md +127 -0
- package/plugins/ca-pi/agents/security-reviewer.md +72 -0
- package/plugins/ca-pi/agents/tribunal-lens-reviewer.md +65 -0
- package/plugins/ca-pi/extensions/codearbiter-child.js +1885 -0
- package/plugins/ca-pi/extensions/codearbiter.js +9802 -0
- package/plugins/ca-pi/generated/command-catalog.json +197 -0
- package/plugins/ca-pi/generated/roles.json +213 -0
- package/plugins/ca-pi/helpers/windows-supervisor.js +205 -0
- package/plugins/ca-pi/hooks/_activationlib.py +196 -0
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +208 -0
- package/plugins/ca-pi/hooks/_babysitlib.py +76 -0
- package/plugins/ca-pi/hooks/_bashguardlib.py +1667 -0
- package/plugins/ca-pi/hooks/_boxlib.py +131 -0
- package/plugins/ca-pi/hooks/_colorlib.py +304 -0
- package/plugins/ca-pi/hooks/_durabilitylib.py +186 -0
- package/plugins/ca-pi/hooks/_entrylib.py +41 -0
- package/plugins/ca-pi/hooks/_fmtlib.py +161 -0
- package/plugins/ca-pi/hooks/_gitexec.py +45 -0
- package/plugins/ca-pi/hooks/_githooks.py +920 -0
- package/plugins/ca-pi/hooks/_gitlib.py +110 -0
- package/plugins/ca-pi/hooks/_hooklib.py +595 -0
- package/plugins/ca-pi/hooks/_host.py +115 -0
- package/plugins/ca-pi/hooks/_intentlib.py +242 -0
- package/plugins/ca-pi/hooks/_ledgerlib.py +1035 -0
- package/plugins/ca-pi/hooks/_metricslib.py +709 -0
- package/plugins/ca-pi/hooks/_pathnorm.py +74 -0
- package/plugins/ca-pi/hooks/_planfilelib.py +664 -0
- package/plugins/ca-pi/hooks/_previewlib.py +193 -0
- package/plugins/ca-pi/hooks/_protectedlib.py +312 -0
- package/plugins/ca-pi/hooks/_protectedstatelib.py +411 -0
- package/plugins/ca-pi/hooks/_provenancelib.py +971 -0
- package/plugins/ca-pi/hooks/_prunelib.py +1398 -0
- package/plugins/ca-pi/hooks/_prunepolicy.py +235 -0
- package/plugins/ca-pi/hooks/_readinjectlib.py +1080 -0
- package/plugins/ca-pi/hooks/_releaselib.py +2657 -0
- package/plugins/ca-pi/hooks/_scopelib.py +262 -0
- package/plugins/ca-pi/hooks/_segmentslib.py +278 -0
- package/plugins/ca-pi/hooks/_sensitivelib.py +270 -0
- package/plugins/ca-pi/hooks/_sessionlib.py +78 -0
- package/plugins/ca-pi/hooks/_sloplib.py +244 -0
- package/plugins/ca-pi/hooks/_standuplib.py +214 -0
- package/plugins/ca-pi/hooks/_subagentslib.py +219 -0
- package/plugins/ca-pi/hooks/_taskboardlib.py +1088 -0
- package/plugins/ca-pi/hooks/_updatelib.py +278 -0
- package/plugins/ca-pi/hooks/babysit.py +47 -0
- package/plugins/ca-pi/hooks/boardsync.py +129 -0
- package/plugins/ca-pi/hooks/doctor.py +420 -0
- package/plugins/ca-pi/hooks/git-enforce.py +325 -0
- package/plugins/ca-pi/hooks/hostapi.py +460 -0
- package/plugins/ca-pi/hooks/init-codearbiter.py +225 -0
- package/plugins/ca-pi/hooks/metrics.py +62 -0
- package/plugins/ca-pi/hooks/migration-pass.py +129 -0
- package/plugins/ca-pi/hooks/pi-bridge.py +543 -0
- package/plugins/ca-pi/hooks/post-write-edit.py +231 -0
- package/plugins/ca-pi/hooks/pre-bash.py +90 -0
- package/plugins/ca-pi/hooks/pre-edit.py +284 -0
- package/plugins/ca-pi/hooks/pre-read.py +81 -0
- package/plugins/ca-pi/hooks/pre-write.py +217 -0
- package/plugins/ca-pi/hooks/preview.py +69 -0
- package/plugins/ca-pi/hooks/prune-transcript.py +232 -0
- package/plugins/ca-pi/hooks/releasehash.py +216 -0
- package/plugins/ca-pi/hooks/security-pass.py +139 -0
- package/plugins/ca-pi/hooks/session-start.py +1218 -0
- package/plugins/ca-pi/hooks/statusline.py +736 -0
- package/plugins/ca-pi/hooks/taskwrite.py +351 -0
- package/plugins/ca-pi/hooks/update-refresh.py +51 -0
- package/plugins/ca-pi/hooks/wire-statusline.py +435 -0
- package/plugins/ca-pi/includes/anti-slop-design/INDEX.md +55 -0
- package/plugins/ca-pi/includes/anti-slop-design/color.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/core.md +244 -0
- package/plugins/ca-pi/includes/anti-slop-design/images.md +32 -0
- package/plugins/ca-pi/includes/anti-slop-design/layout.md +45 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-cli.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-dataviz.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-diagram.md +35 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-documents.md +70 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-slides.md +30 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-web.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/typography.md +51 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +14 -0
- package/plugins/ca-pi/includes/compaction-charter.md +16 -0
- package/plugins/ca-pi/includes/cut-docs.md +16 -0
- package/plugins/ca-pi/includes/dev-mode.md +30 -0
- package/plugins/ca-pi/includes/farm.md +237 -0
- package/plugins/ca-pi/includes/fresh-verification.md +14 -0
- package/plugins/ca-pi/includes/harvest.md +69 -0
- package/plugins/ca-pi/includes/maturity-coverage.md +102 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +69 -0
- package/plugins/ca-pi/includes/redirect.md +69 -0
- package/plugins/ca-pi/includes/reference-map.md +22 -0
- package/plugins/ca-pi/includes/review-matrix.md +14 -0
- package/plugins/ca-pi/includes/reviewer-contract.md +53 -0
- package/plugins/ca-pi/includes/routing-table.md +47 -0
- package/plugins/ca-pi/includes/security-gate-record.md +22 -0
- package/plugins/ca-pi/includes/smarts/core.md +90 -0
- package/plugins/ca-pi/includes/smarts/decision-log-format.md +56 -0
- package/plugins/ca-pi/routines/INDEX.md +32 -0
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +122 -0
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +151 -0
- package/plugins/ca-pi/routines/context-check/SKILL.md +85 -0
- package/plugins/ca-pi/routines/context-creation/SKILL.md +171 -0
- package/plugins/ca-pi/routines/crypto-compliance/SKILL.md +41 -0
- package/plugins/ca-pi/routines/debug/SKILL.md +99 -0
- package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +104 -0
- package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +74 -0
- package/plugins/ca-pi/routines/decision-variance/SKILL.md +147 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +168 -0
- package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +76 -0
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +83 -0
- package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +91 -0
- package/plugins/ca-pi/routines/post-merge-cleanup/SKILL.md +233 -0
- package/plugins/ca-pi/routines/refactor/SKILL.md +91 -0
- package/plugins/ca-pi/routines/release/SKILL.md +315 -0
- package/plugins/ca-pi/routines/secret-handling/SKILL.md +67 -0
- package/plugins/ca-pi/routines/security-architecture/SKILL.md +63 -0
- package/plugins/ca-pi/routines/skill-author/SKILL.md +108 -0
- package/plugins/ca-pi/routines/skill-author/references/skill-template.md +58 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +149 -0
- package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +145 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +139 -0
- package/plugins/ca-pi/routines/tribunal/SKILL.md +109 -0
- package/plugins/ca-pi/routines/tribunal/references/ai-markers.md +29 -0
- package/plugins/ca-pi/routines/tribunal/references/cost-and-models.md +64 -0
- package/plugins/ca-pi/routines/tribunal/references/finding-record.md +27 -0
- package/plugins/ca-pi/routines/tribunal/references/issue-filing.md +47 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/appsec.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/architecture.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/coverage.md +20 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/infra.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/migration.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/observability.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/performance.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/reliability.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/secrets-supply.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/test-fidelity.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/typesafety.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/report.md +19 -0
- package/plugins/ca-pi/routines/tribunal/references/schemas.md +58 -0
- package/plugins/ca-pi/routines/tribunal/references/telemetry.md +28 -0
- package/plugins/ca-pi/routines/tribunal/references/triage.md +53 -0
- package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +85 -0
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +129 -0
- package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +50 -0
- package/plugins/ca-pi/skills/ca-add-dep/SKILL.md +88 -0
- package/plugins/ca-pi/skills/ca-adr/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-adr-status/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-arbiter/SKILL.md +36 -0
- package/plugins/ca-pi/skills/ca-audit/SKILL.md +51 -0
- package/plugins/ca-pi/skills/ca-btw/SKILL.md +23 -0
- package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +50 -0
- package/plugins/ca-pi/skills/ca-chore/SKILL.md +58 -0
- package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +55 -0
- package/plugins/ca-pi/skills/ca-commands/SKILL.md +21 -0
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +27 -0
- package/plugins/ca-pi/skills/ca-conflict/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-dev/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-doctor/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +105 -0
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-init/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-metrics/SKILL.md +80 -0
- package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +34 -0
- package/plugins/ca-pi/skills/ca-override/SKILL.md +72 -0
- package/plugins/ca-pi/skills/ca-pr/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-preview/SKILL.md +86 -0
- package/plugins/ca-pi/skills/ca-prune/SKILL.md +100 -0
- package/plugins/ca-pi/skills/ca-reconcile/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-refactor/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-release/SKILL.md +57 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +63 -0
- package/plugins/ca-pi/skills/ca-spike/SKILL.md +41 -0
- package/plugins/ca-pi/skills/ca-sprint/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-standup/SKILL.md +112 -0
- package/plugins/ca-pi/skills/ca-status/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-threat-model/SKILL.md +45 -0
- package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-watch/SKILL.md +78 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Bounded one-request Pi codec for exactly one shared core entry."""
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import binascii
|
|
6
|
+
import contextlib
|
|
7
|
+
import importlib.util
|
|
8
|
+
import io
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
sys.path.insert(0, HERE)
|
|
18
|
+
from _host import PiHost # noqa: E402
|
|
19
|
+
from _prunepolicy import PrunePolicy, SemanticEntry, plan_prune # noqa: E402
|
|
20
|
+
import _arbiterstatelib # noqa: E402
|
|
21
|
+
import _hooklib # noqa: E402
|
|
22
|
+
import _ledgerlib # noqa: E402
|
|
23
|
+
import _planfilelib # noqa: E402
|
|
24
|
+
import _segmentslib # noqa: E402
|
|
25
|
+
import _taskboardlib # noqa: E402
|
|
26
|
+
|
|
27
|
+
MAX_REQUEST_BYTES = 262_144
|
|
28
|
+
MAX_PLAN_CONTENT_BYTES = 92_160
|
|
29
|
+
MAX_CAPTURE_CHARS = 1_048_576
|
|
30
|
+
ALLOWED_KEYS = frozenset({"version", "event", "cwd", "sessionId", "tool", "input", "result", "correlation"})
|
|
31
|
+
# Opaque join key for one originating tool call: a SHA-256 digest in lowercase hex,
|
|
32
|
+
# never the raw host tool-call id and never derived from parameters or prompt text.
|
|
33
|
+
CORRELATION_RE = re.compile(r"\A[0-9a-f]{64}\Z")
|
|
34
|
+
EVENT_KEYS = {
|
|
35
|
+
"session_start": (frozenset({"version", "event", "cwd"}), frozenset({"version", "event", "cwd", "sessionId"})),
|
|
36
|
+
"before_agent_start": (frozenset({"version", "event", "cwd"}), frozenset({"version", "event", "cwd", "sessionId"})),
|
|
37
|
+
"tool_call": (frozenset({"version", "event", "cwd", "tool", "input"}), frozenset({"version", "event", "cwd", "sessionId", "tool", "input", "correlation"})),
|
|
38
|
+
"tool_result": (frozenset({"version", "event", "cwd", "tool", "input", "result"}), frozenset({"version", "event", "cwd", "sessionId", "tool", "input", "result", "correlation"})),
|
|
39
|
+
"prune_plan": (frozenset({"version", "event", "cwd", "input"}), frozenset({"version", "event", "cwd", "input"})),
|
|
40
|
+
"footer_usage_update": (frozenset({"version", "event", "cwd", "input"}), frozenset({"version", "event", "cwd", "input"})),
|
|
41
|
+
"footer_status_snapshot": (frozenset({"version", "event", "cwd"}), frozenset({"version", "event", "cwd", "sessionId"})),
|
|
42
|
+
"plan_file": (frozenset({"version", "event", "cwd", "input"}), frozenset({"version", "event", "cwd", "input"})),
|
|
43
|
+
}
|
|
44
|
+
ENTRY_BY_EVENT = {
|
|
45
|
+
("session_start", None): "session-start.py",
|
|
46
|
+
("tool_call", "EXEC"): "pre-bash.py",
|
|
47
|
+
("tool_call", "WRITE"): "pre-write.py",
|
|
48
|
+
("tool_call", "EDIT"): "pre-edit.py",
|
|
49
|
+
("tool_call", "READ"): "pre-read.py",
|
|
50
|
+
("tool_result", "WRITE"): "post-write-edit.py",
|
|
51
|
+
("tool_result", "EDIT"): "post-write-edit.py",
|
|
52
|
+
}
|
|
53
|
+
RULE_RE = re.compile(r"\[((?:H|PI)-[A-Za-z0-9]+)\]")
|
|
54
|
+
ANSI_RE = re.compile(r"\x1b(?:\[[0-?]*[ -/]*[@-~]?|\][^\x07\x1b]*(?:\x07|\x1b\\)?|[@-_])")
|
|
55
|
+
FOOTER_USAGE_KEYS = frozenset({"sessionKey", "scanStart", "scanEnd", "facts"})
|
|
56
|
+
FOOTER_USAGE_RESULT_KEYS = frozenset({
|
|
57
|
+
"status", "session", "today", "acceptedThrough", "highWater",
|
|
58
|
+
})
|
|
59
|
+
FOOTER_USAGE_TOTAL_KEYS = frozenset({
|
|
60
|
+
"inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "costUsd",
|
|
61
|
+
})
|
|
62
|
+
FOOTER_USAGE_STATUSES = frozenset({"ok", "invalid", "corrupt", "lock_failed", "write_failed"})
|
|
63
|
+
FOOTER_MAX_COUNT = 1_000_000
|
|
64
|
+
FOOTER_MAX_STAGE = 128
|
|
65
|
+
FOOTER_MAX_PRUNE = 256
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ProtocolError(ValueError):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class BoundedText(io.TextIOBase):
|
|
73
|
+
"""Text writer that never retains more than its UTF-8 byte budget."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, max_bytes):
|
|
76
|
+
self.max_bytes = max_bytes
|
|
77
|
+
self.parts = []
|
|
78
|
+
self.size = 0
|
|
79
|
+
self.overflowed = False
|
|
80
|
+
|
|
81
|
+
def writable(self):
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def write(self, value):
|
|
85
|
+
if not isinstance(value, str):
|
|
86
|
+
raise TypeError("text writer requires str")
|
|
87
|
+
encoded = value.encode("utf-8", "replace")
|
|
88
|
+
remaining = max(0, self.max_bytes - self.size)
|
|
89
|
+
if remaining:
|
|
90
|
+
self.parts.append(encoded[:remaining])
|
|
91
|
+
self.size += min(len(encoded), remaining)
|
|
92
|
+
if len(encoded) > remaining:
|
|
93
|
+
self.overflowed = True
|
|
94
|
+
return len(value)
|
|
95
|
+
|
|
96
|
+
def flush(self):
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def getvalue(self):
|
|
100
|
+
return b"".join(self.parts).decode("utf-8", "replace")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _json_value(value, depth=0):
|
|
104
|
+
if depth > 16:
|
|
105
|
+
raise ProtocolError("JSON nesting exceeds limit")
|
|
106
|
+
if value is None or isinstance(value, (bool, int, str)):
|
|
107
|
+
return
|
|
108
|
+
if isinstance(value, float):
|
|
109
|
+
if not math.isfinite(value):
|
|
110
|
+
raise ProtocolError("JSON number is not finite")
|
|
111
|
+
return
|
|
112
|
+
if isinstance(value, list):
|
|
113
|
+
if len(value) > 10_000:
|
|
114
|
+
raise ProtocolError("JSON list exceeds limit")
|
|
115
|
+
for item in value:
|
|
116
|
+
_json_value(item, depth + 1)
|
|
117
|
+
return
|
|
118
|
+
if isinstance(value, dict):
|
|
119
|
+
if len(value) > 1_000 or any(not isinstance(key, str) for key in value):
|
|
120
|
+
raise ProtocolError("JSON object exceeds limit")
|
|
121
|
+
for item in value.values():
|
|
122
|
+
_json_value(item, depth + 1)
|
|
123
|
+
return
|
|
124
|
+
raise ProtocolError("unsupported JSON value")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _request(raw):
|
|
128
|
+
def unique_object(pairs):
|
|
129
|
+
output = {}
|
|
130
|
+
for key, item in pairs:
|
|
131
|
+
if key in output:
|
|
132
|
+
raise ProtocolError("duplicate JSON object key")
|
|
133
|
+
output[key] = item
|
|
134
|
+
return output
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
value = json.loads(
|
|
138
|
+
raw.decode("utf-8", "strict"),
|
|
139
|
+
parse_constant=lambda token: (_ for _ in ()).throw(ProtocolError(f"invalid JSON constant {token}")),
|
|
140
|
+
object_pairs_hook=unique_object,
|
|
141
|
+
)
|
|
142
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
143
|
+
raise ProtocolError("request is not one UTF-8 JSON object") from exc
|
|
144
|
+
if not isinstance(value, dict) or set(value) - ALLOWED_KEYS:
|
|
145
|
+
raise ProtocolError("request keys are invalid")
|
|
146
|
+
if type(value.get("version")) is not int or value["version"] != 1:
|
|
147
|
+
raise ProtocolError("request version is invalid")
|
|
148
|
+
for key in ("event", "cwd"):
|
|
149
|
+
if not isinstance(value.get(key), str) or not value[key] or len(value[key]) > 32_768:
|
|
150
|
+
raise ProtocolError(f"request {key} is invalid")
|
|
151
|
+
schema = EVENT_KEYS.get(value["event"])
|
|
152
|
+
if schema is None:
|
|
153
|
+
raise ProtocolError("request event is invalid")
|
|
154
|
+
required, allowed = schema
|
|
155
|
+
if not required.issubset(value) or not set(value).issubset(allowed):
|
|
156
|
+
raise ProtocolError("request shape is invalid for event")
|
|
157
|
+
if not os.path.isabs(value["cwd"]) or not os.path.isdir(value["cwd"]):
|
|
158
|
+
raise ProtocolError("request cwd is not an absolute directory")
|
|
159
|
+
for key in ("sessionId", "tool"):
|
|
160
|
+
if key in value and (not isinstance(value[key], str) or len(value[key]) > 1_024):
|
|
161
|
+
raise ProtocolError(f"request {key} is invalid")
|
|
162
|
+
if "correlation" in value and (
|
|
163
|
+
not isinstance(value["correlation"], str) or not CORRELATION_RE.match(value["correlation"])
|
|
164
|
+
):
|
|
165
|
+
raise ProtocolError("request correlation is invalid")
|
|
166
|
+
for key in ("input", "result"):
|
|
167
|
+
if key in value:
|
|
168
|
+
if not isinstance(value[key], dict):
|
|
169
|
+
raise ProtocolError(f"request {key} must be an object")
|
|
170
|
+
_json_value(value[key])
|
|
171
|
+
return value
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _load_entry(filename):
|
|
175
|
+
path = os.path.realpath(os.path.join(HERE, filename))
|
|
176
|
+
if os.path.commonpath((path, os.path.realpath(HERE))) != os.path.realpath(HERE):
|
|
177
|
+
raise ProtocolError("entry escapes package")
|
|
178
|
+
spec = importlib.util.spec_from_file_location("ca_pi_entry_" + uuid.uuid4().hex, path)
|
|
179
|
+
if spec is None or spec.loader is None:
|
|
180
|
+
raise RuntimeError("shared entry cannot be loaded")
|
|
181
|
+
module = importlib.util.module_from_spec(spec)
|
|
182
|
+
spec.loader.exec_module(module)
|
|
183
|
+
if not callable(getattr(module, "run", None)):
|
|
184
|
+
raise RuntimeError("shared entry has no run seam")
|
|
185
|
+
return module
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _payload(request, host):
|
|
189
|
+
tool = request.get("tool", "")
|
|
190
|
+
tool_input = request.get("input", {})
|
|
191
|
+
if request["event"] == "tool_call" and host.normalize_tool(tool) == "READ":
|
|
192
|
+
tool_input = host.normalize_tool_input(tool, tool_input)
|
|
193
|
+
return {
|
|
194
|
+
"cwd": request["cwd"],
|
|
195
|
+
"session_id": request.get("sessionId", ""),
|
|
196
|
+
"hook_event_name": request["event"],
|
|
197
|
+
"tool_name": tool,
|
|
198
|
+
"tool_input": tool_input,
|
|
199
|
+
"tool_response": request.get("result", {}),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _run_entry(request, filename, host):
|
|
204
|
+
module = _load_entry(filename)
|
|
205
|
+
stdin = io.StringIO(json.dumps(_payload(request, host), ensure_ascii=False))
|
|
206
|
+
stdout = BoundedText(MAX_CAPTURE_CHARS)
|
|
207
|
+
stderr = BoundedText(MAX_CAPTURE_CHARS)
|
|
208
|
+
previous = sys.stdin
|
|
209
|
+
code = 0
|
|
210
|
+
try:
|
|
211
|
+
sys.stdin = stdin
|
|
212
|
+
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
213
|
+
try:
|
|
214
|
+
result = module.run(host)
|
|
215
|
+
code = int(result or 0)
|
|
216
|
+
except SystemExit as exc:
|
|
217
|
+
code = int(exc.code or 0) if isinstance(exc.code, (int, type(None))) else 1
|
|
218
|
+
finally:
|
|
219
|
+
sys.stdin = previous
|
|
220
|
+
out = stdout.getvalue()
|
|
221
|
+
err = stderr.getvalue()
|
|
222
|
+
if stdout.overflowed or stderr.overflowed:
|
|
223
|
+
raise RuntimeError("shared entry output overflow")
|
|
224
|
+
return code, out, err
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _rule(text):
|
|
228
|
+
match = RULE_RE.search(text)
|
|
229
|
+
return match.group(1) if match else None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _prune_plan(request):
|
|
233
|
+
value = request.get("input")
|
|
234
|
+
if not isinstance(value, dict) or set(value) != {"entries", "policy"}:
|
|
235
|
+
raise ProtocolError("prune plan input is invalid")
|
|
236
|
+
rows, raw_policy = value["entries"], value["policy"]
|
|
237
|
+
if not isinstance(rows, list) or not rows or len(rows) > 10_000:
|
|
238
|
+
raise ProtocolError("prune plan entries are invalid")
|
|
239
|
+
if not isinstance(raw_policy, dict) or set(raw_policy) != {"tier", "keepRecent", "maxBytes"}:
|
|
240
|
+
raise ProtocolError("prune policy is invalid")
|
|
241
|
+
tier = raw_policy["tier"]
|
|
242
|
+
keep_recent = raw_policy["keepRecent"]
|
|
243
|
+
max_bytes = raw_policy["maxBytes"]
|
|
244
|
+
if tier not in ("gentle", "standard", "aggressive") \
|
|
245
|
+
or type(keep_recent) is not int or not 0 <= keep_recent <= 1000 \
|
|
246
|
+
or type(max_bytes) is not int or not 1 <= max_bytes <= MAX_REQUEST_BYTES:
|
|
247
|
+
raise ProtocolError("prune policy is invalid")
|
|
248
|
+
entries = []
|
|
249
|
+
entry_ids = set()
|
|
250
|
+
expected = {"id", "ordinal", "role", "kind", "byteSize", "toolBearing", "marked"}
|
|
251
|
+
for row in rows:
|
|
252
|
+
if not isinstance(row, dict) or set(row) != expected:
|
|
253
|
+
raise ProtocolError("semantic entry is invalid")
|
|
254
|
+
if not isinstance(row["id"], str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,1023}", row["id"]) \
|
|
255
|
+
or row["id"] in entry_ids \
|
|
256
|
+
or type(row["ordinal"]) is not int or row["ordinal"] != len(entries) \
|
|
257
|
+
or row["role"] not in ("user", "assistant", "tool", "system", "other") \
|
|
258
|
+
or row["kind"] not in ("message", "tool-result", "compaction", "metadata") \
|
|
259
|
+
or type(row["byteSize"]) is not int or not 0 <= row["byteSize"] <= MAX_REQUEST_BYTES \
|
|
260
|
+
or type(row["toolBearing"]) is not bool or type(row["marked"]) is not bool:
|
|
261
|
+
raise ProtocolError("semantic entry is invalid")
|
|
262
|
+
entry_ids.add(row["id"])
|
|
263
|
+
entries.append(SemanticEntry(
|
|
264
|
+
id=row["id"], ordinal=row["ordinal"], role=row["role"], kind=row["kind"],
|
|
265
|
+
byte_size=row["byteSize"], tool_bearing=row["toolBearing"], marked=row["marked"],
|
|
266
|
+
))
|
|
267
|
+
plan = plan_prune(entries, PrunePolicy(
|
|
268
|
+
tier=tier, keep_recent=keep_recent, max_bytes=max_bytes,
|
|
269
|
+
))
|
|
270
|
+
if plan.first_kept_id is None:
|
|
271
|
+
raise ProtocolError("prune plan has no kept boundary")
|
|
272
|
+
return {
|
|
273
|
+
"version": 1,
|
|
274
|
+
"outcome": "notice",
|
|
275
|
+
"auditCode": "PI_PRUNE_PLAN",
|
|
276
|
+
"resultPatch": {"prunePlan": {
|
|
277
|
+
"firstKeptEntryId": plan.first_kept_id,
|
|
278
|
+
"protectedIds": list(plan.protected_ids),
|
|
279
|
+
"actions": [{"entryId": entry_id, "action": action}
|
|
280
|
+
for entry_id, action in plan.actions],
|
|
281
|
+
"metrics": {
|
|
282
|
+
"entriesBefore": plan.metrics["entries_before"],
|
|
283
|
+
"candidateEntries": plan.metrics["candidate_entries"],
|
|
284
|
+
"protectedEntries": plan.metrics["protected_entries"],
|
|
285
|
+
"markedCandidates": plan.metrics["marked_candidates"],
|
|
286
|
+
},
|
|
287
|
+
"auditCodes": list(plan.audit_codes),
|
|
288
|
+
"fingerprint": plan.fingerprint,
|
|
289
|
+
}},
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _footer_usage_totals(value):
|
|
294
|
+
if not isinstance(value, dict) or set(value) != FOOTER_USAGE_TOTAL_KEYS:
|
|
295
|
+
return None
|
|
296
|
+
for key in FOOTER_USAGE_TOTAL_KEYS - {"costUsd"}:
|
|
297
|
+
amount = value[key]
|
|
298
|
+
if type(amount) is not int or not 0 <= amount <= _ledgerlib.PI_MAX_TOKENS:
|
|
299
|
+
return None
|
|
300
|
+
cost = value["costUsd"]
|
|
301
|
+
if type(cost) not in (int, float) or not math.isfinite(cost) \
|
|
302
|
+
or not 0 <= cost <= _ledgerlib.PI_MAX_COST_USD:
|
|
303
|
+
return None
|
|
304
|
+
return {
|
|
305
|
+
"inputTokens": value["inputTokens"],
|
|
306
|
+
"outputTokens": value["outputTokens"],
|
|
307
|
+
"cacheReadTokens": value["cacheReadTokens"],
|
|
308
|
+
"cacheWriteTokens": value["cacheWriteTokens"],
|
|
309
|
+
"costUsd": round(float(cost), 9),
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _footer_usage_result(value):
|
|
314
|
+
if not isinstance(value, dict) or set(value) != FOOTER_USAGE_RESULT_KEYS \
|
|
315
|
+
or value.get("status") not in FOOTER_USAGE_STATUSES:
|
|
316
|
+
return None
|
|
317
|
+
session = _footer_usage_totals(value.get("session"))
|
|
318
|
+
today = _footer_usage_totals(value.get("today"))
|
|
319
|
+
accepted_through = value.get("acceptedThrough")
|
|
320
|
+
high_water = value.get("highWater")
|
|
321
|
+
if session is None or today is None or type(accepted_through) is not int \
|
|
322
|
+
or not -1 <= accepted_through <= _ledgerlib.PI_MAX_POSITION \
|
|
323
|
+
or type(high_water) is not int \
|
|
324
|
+
or not -1 <= high_water <= _ledgerlib.PI_MAX_POSITION:
|
|
325
|
+
return None
|
|
326
|
+
return {
|
|
327
|
+
"status": value["status"],
|
|
328
|
+
"session": session,
|
|
329
|
+
"today": today,
|
|
330
|
+
"acceptedThrough": accepted_through,
|
|
331
|
+
"highWater": high_water,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _footer_usage_update(request):
|
|
336
|
+
"""Project-independent bounded crossing into the shared user-global ledger."""
|
|
337
|
+
value = request.get("input")
|
|
338
|
+
if not isinstance(value, dict) or set(value) != FOOTER_USAGE_KEYS:
|
|
339
|
+
result = None
|
|
340
|
+
else:
|
|
341
|
+
try:
|
|
342
|
+
result = _footer_usage_result(_ledgerlib.pi_ledger_update(
|
|
343
|
+
value["sessionKey"], value["scanStart"], value["scanEnd"], value["facts"]
|
|
344
|
+
))
|
|
345
|
+
except Exception: # noqa: BLE001 - footer accounting is advisory and fail-soft
|
|
346
|
+
result = None
|
|
347
|
+
if result is None:
|
|
348
|
+
blank = {
|
|
349
|
+
"inputTokens": 0,
|
|
350
|
+
"outputTokens": 0,
|
|
351
|
+
"cacheReadTokens": 0,
|
|
352
|
+
"cacheWriteTokens": 0,
|
|
353
|
+
"costUsd": 0.0,
|
|
354
|
+
}
|
|
355
|
+
result = {
|
|
356
|
+
"status": "corrupt",
|
|
357
|
+
"session": dict(blank),
|
|
358
|
+
"today": dict(blank),
|
|
359
|
+
"acceptedThrough": -1,
|
|
360
|
+
"highWater": -1,
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
"version": 1,
|
|
364
|
+
"outcome": "notice",
|
|
365
|
+
"auditCode": "PI_FOOTER_USAGE",
|
|
366
|
+
"resultPatch": {"footerUsage": result},
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _bounded_footer_text(value, maximum):
|
|
371
|
+
if not isinstance(value, str):
|
|
372
|
+
return None
|
|
373
|
+
clean = ANSI_RE.sub("", value)
|
|
374
|
+
clean = "".join(char for char in clean if ord(char) >= 32 and not 127 <= ord(char) <= 159)
|
|
375
|
+
return "".join(list(clean)[:maximum]) or None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _footer_status_snapshot(request):
|
|
379
|
+
"""Read the existing shared status sources after the TypeScript trust gate."""
|
|
380
|
+
unavailable = {"version": 1, "outcome": "allow", "auditCode": "PI_FOOTER_STATUS_UNAVAILABLE"}
|
|
381
|
+
try:
|
|
382
|
+
state = _arbiterstatelib.arbiter_state(
|
|
383
|
+
request["cwd"],
|
|
384
|
+
_taskboardlib.count_in_flight,
|
|
385
|
+
_taskboardlib.read_board,
|
|
386
|
+
_hooklib.frontmatter_enabled,
|
|
387
|
+
)
|
|
388
|
+
except Exception: # noqa: BLE001 - one unavailable segment never breaks the footer
|
|
389
|
+
state = None
|
|
390
|
+
if not isinstance(state, dict):
|
|
391
|
+
return unavailable
|
|
392
|
+
stage = _bounded_footer_text(state.get("stage"), FOOTER_MAX_STAGE)
|
|
393
|
+
counts = (state.get("tasks"), state.get("q"), state.get("over"))
|
|
394
|
+
sprint = state.get("sprint")
|
|
395
|
+
if stage is None or any(type(value) is not int or not 0 <= value <= FOOTER_MAX_COUNT
|
|
396
|
+
for value in counts) or type(sprint) is not bool:
|
|
397
|
+
return unavailable
|
|
398
|
+
session_id = request.get("sessionId")
|
|
399
|
+
prune = None
|
|
400
|
+
if isinstance(session_id, str) and session_id:
|
|
401
|
+
try:
|
|
402
|
+
prune = _bounded_footer_text(_segmentslib.seg_prune({}, session_id), FOOTER_MAX_PRUNE)
|
|
403
|
+
except Exception: # noqa: BLE001 - prune is an independent optional segment
|
|
404
|
+
prune = None
|
|
405
|
+
try:
|
|
406
|
+
dev = _arbiterstatelib.dev_active(request["cwd"])
|
|
407
|
+
except Exception: # noqa: BLE001 - dev is a fail-soft display fact
|
|
408
|
+
return unavailable
|
|
409
|
+
if type(dev) is not bool:
|
|
410
|
+
return unavailable
|
|
411
|
+
return {
|
|
412
|
+
"version": 1,
|
|
413
|
+
"outcome": "notice",
|
|
414
|
+
"auditCode": "PI_FOOTER_STATUS",
|
|
415
|
+
"resultPatch": {"footerStatus": {
|
|
416
|
+
"status": "ok",
|
|
417
|
+
"stage": stage,
|
|
418
|
+
"tasks": counts[0],
|
|
419
|
+
"questions": counts[1],
|
|
420
|
+
"overrides": counts[2],
|
|
421
|
+
"sprint": sprint,
|
|
422
|
+
"dev": dev,
|
|
423
|
+
"prune": prune,
|
|
424
|
+
}},
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _plan_file_request(value):
|
|
429
|
+
if not isinstance(value, dict) or value.get("action") not in ("read", "replace"):
|
|
430
|
+
raise ProtocolError("plan file input is invalid")
|
|
431
|
+
if value["action"] == "read":
|
|
432
|
+
if set(value) != {"slug", "kind", "action"}:
|
|
433
|
+
raise ProtocolError("plan file input is invalid")
|
|
434
|
+
return value
|
|
435
|
+
if set(value) != {"slug", "kind", "action", "expectedHash", "contentBase64"}:
|
|
436
|
+
raise ProtocolError("plan file input is invalid")
|
|
437
|
+
encoded = value["contentBase64"]
|
|
438
|
+
if not isinstance(encoded, str) or len(encoded) > ((MAX_PLAN_CONTENT_BYTES + 2) // 3) * 4:
|
|
439
|
+
raise ProtocolError("plan file content is invalid")
|
|
440
|
+
try:
|
|
441
|
+
raw = encoded.encode("ascii", "strict")
|
|
442
|
+
decoded = base64.b64decode(raw, validate=True)
|
|
443
|
+
except (UnicodeEncodeError, binascii.Error) as exc:
|
|
444
|
+
raise ProtocolError("plan file content is invalid") from exc
|
|
445
|
+
if base64.b64encode(decoded) != raw or len(decoded) > MAX_PLAN_CONTENT_BYTES:
|
|
446
|
+
raise ProtocolError("plan file content is invalid")
|
|
447
|
+
try:
|
|
448
|
+
content = decoded.decode("utf-8", "strict")
|
|
449
|
+
except UnicodeDecodeError as exc:
|
|
450
|
+
raise ProtocolError("plan file content is invalid") from exc
|
|
451
|
+
return {"slug": value.get("slug"), "kind": value.get("kind"), "action": "replace",
|
|
452
|
+
"expectedHash": value.get("expectedHash"), "content": content}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _plan_file_response(result):
|
|
456
|
+
output = dict(result)
|
|
457
|
+
if "content" in output:
|
|
458
|
+
content = output.pop("content")
|
|
459
|
+
if content is None:
|
|
460
|
+
output["contentBase64"] = None
|
|
461
|
+
elif isinstance(content, str):
|
|
462
|
+
raw = content.encode("utf-8", "strict")
|
|
463
|
+
if len(raw) > MAX_PLAN_CONTENT_BYTES:
|
|
464
|
+
raise ProtocolError("plan file result is invalid")
|
|
465
|
+
output["contentBase64"] = base64.b64encode(raw).decode("ascii")
|
|
466
|
+
else:
|
|
467
|
+
raise ProtocolError("plan file result is invalid")
|
|
468
|
+
return output
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def dispatch(request):
|
|
472
|
+
if request["event"] == "plan_file":
|
|
473
|
+
result = _planfilelib.plan_file_operation(request["cwd"], _plan_file_request(request["input"]))
|
|
474
|
+
return {
|
|
475
|
+
"version": 1,
|
|
476
|
+
"outcome": "notice",
|
|
477
|
+
"auditCode": "PI_PLAN_FILE",
|
|
478
|
+
"resultPatch": {"planFile": _plan_file_response(result)},
|
|
479
|
+
}
|
|
480
|
+
if request["event"] == "footer_usage_update":
|
|
481
|
+
return _footer_usage_update(request)
|
|
482
|
+
if request["event"] == "footer_status_snapshot":
|
|
483
|
+
return _footer_status_snapshot(request)
|
|
484
|
+
if request["event"] == "prune_plan":
|
|
485
|
+
return _prune_plan(request)
|
|
486
|
+
host = PiHost(request["cwd"])
|
|
487
|
+
category = host.normalize_tool(request.get("tool", ""))
|
|
488
|
+
if request["event"] == "tool_call" and category == "OTHER":
|
|
489
|
+
return {
|
|
490
|
+
"version": 1,
|
|
491
|
+
"outcome": "block",
|
|
492
|
+
"ruleId": "PI-UNKNOWN",
|
|
493
|
+
"message": "Unknown Pi tool is potentially mutating and is blocked; classify it or run /ca-doctor.",
|
|
494
|
+
"auditCode": "PI_UNKNOWN_BLOCK",
|
|
495
|
+
}
|
|
496
|
+
filename = ENTRY_BY_EVENT.get((request["event"], category))
|
|
497
|
+
if filename is None:
|
|
498
|
+
filename = ENTRY_BY_EVENT.get((request["event"], None))
|
|
499
|
+
if filename is None:
|
|
500
|
+
return {"version": 1, "outcome": "allow", "auditCode": "PI_NO_ROUTE"}
|
|
501
|
+
code, stdout, stderr = _run_entry(request, filename, host)
|
|
502
|
+
if code == 2:
|
|
503
|
+
message = stderr.strip() or "shared core blocked the operation"
|
|
504
|
+
response = {"version": 1, "outcome": "block", "message": message, "auditCode": "PI_CORE_BLOCK"}
|
|
505
|
+
rule = _rule(message)
|
|
506
|
+
if rule:
|
|
507
|
+
response["ruleId"] = rule
|
|
508
|
+
return response
|
|
509
|
+
if code != 0:
|
|
510
|
+
raise RuntimeError(stderr.strip() or f"shared entry exited {code}")
|
|
511
|
+
if request["event"] == "tool_call" and category == "READ" and stdout.strip():
|
|
512
|
+
try:
|
|
513
|
+
hook_output = json.loads(stdout)
|
|
514
|
+
context = hook_output["hookSpecificOutput"]["additionalContext"]
|
|
515
|
+
except (KeyError, TypeError, json.JSONDecodeError) as exc:
|
|
516
|
+
raise RuntimeError("shared read output is malformed") from exc
|
|
517
|
+
return {"version": 1, "outcome": "notice", "context": context, "auditCode": "PI_CORE_NOTICE"}
|
|
518
|
+
message = stderr.strip()
|
|
519
|
+
if message:
|
|
520
|
+
response = {"version": 1, "outcome": "notice", "message": message, "auditCode": "PI_CORE_NOTICE"}
|
|
521
|
+
rule = _rule(message)
|
|
522
|
+
if rule:
|
|
523
|
+
response["ruleId"] = rule
|
|
524
|
+
return response
|
|
525
|
+
if stdout:
|
|
526
|
+
return {"version": 1, "outcome": "notice", "context": stdout, "auditCode": "PI_CORE_CONTEXT"}
|
|
527
|
+
return {"version": 1, "outcome": "allow", "auditCode": "PI_CORE_ALLOW"}
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def main():
|
|
531
|
+
raw = sys.stdin.buffer.read(MAX_REQUEST_BYTES + 1)
|
|
532
|
+
if len(raw) > MAX_REQUEST_BYTES:
|
|
533
|
+
raise ProtocolError("request overflow")
|
|
534
|
+
response = dispatch(_request(raw))
|
|
535
|
+
sys.stdout.write(json.dumps(response, ensure_ascii=False, separators=(",", ":")))
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
if __name__ == "__main__":
|
|
539
|
+
try:
|
|
540
|
+
main()
|
|
541
|
+
except Exception as exc: # transport reports only a fixed class, never request data
|
|
542
|
+
sys.stderr.write(f"PI bridge error ({type(exc).__name__})\n")
|
|
543
|
+
sys.exit(1)
|